mikeboone Claude Fable 5.1 commited on
Commit
56a7b8e
·
1 Parent(s): 61e0650

muze: Gallery/Showcase tier on every archetype; layout routes on tier before confidence; main-tabs switch defaults ON

Browse files

Leadership 2026-09-22: Gallery-grade Muze (declarative grammar, editable in
Muze Studio) may sit on prod main tabs; Showcase-grade programs (KPI card
boards, risk matrix, ridgelines, year-in-sales) only on the 4th tab beside
the table. All 16 shipped archetypes are Gallery grade; each now declares
tier=GALLERY, tier_of()/muze_tier() expose it, and _fill_showcase sends any
SHOWCASE-tier or untagged Muze tile to the 4th tab regardless of rating.

MUZE_ENGINE_MAIN_TABS: unset now means ON (the OFF default was the 'keep Muze
off prod' rule that leadership lifted); 'false' remains the emergency off.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

demoprep_app/liveboards/golden_layout.py CHANGED
@@ -60,27 +60,25 @@ MUZE_SHOWCASE_MAX = 3 # boone 2026-09-03: 'a little more Muze'
60
  # The data-first Muze engine (Sept 10) on the exec/manager tabs is a PER-SPACE
61
  # switch, read from the environment at build time:
62
  #
63
- # MUZE_ENGINE_MAIN_TABS=true answers whose shape fits a high-confidence
64
- # archetype are drawn as Muze on the main tabs
65
- # unset / false the engine is skipped and the main tabs carry
66
- # native charts exactly as prod has since
67
- # 2026-09-15; Muze appears only as the
68
- # surplus-heatmap upgrade on the showcase tab
69
  #
70
- # OFF is the default on purpose. This used to be a module constant, flipped by
71
- # hand: True on test, False in the emergency prod build (boone 2026-09-15, a
72
- # stakeholder asked that one-day-old Muze stay off prod). A constant meant the
73
- # two Spaces could never run the same commit, so nothing else could ship to
74
- # prod without either carrying Muze along or forking the line. Now the same
75
- # build runs everywhere and the Space VARIABLE (not a secret) decides: set on
76
- # test-mcp, absent on prod (boone 2026-09-21, Agent Q review action item).
77
  #
78
  # The state is never invisible: config_check reports it per Space, and every
79
  # build stamps muze_main_tabs_state() into its change list. An unreadable value
80
  # is refused, not guessed - a typo must not quietly turn Muze on in prod.
81
  MUZE_ENGINE_MAIN_TABS_VAR = "MUZE_ENGINE_MAIN_TABS"
82
- _FLAG_TRUE = ("1", "true", "on", "yes")
83
- _FLAG_FALSE = ("", "0", "false", "off", "no")
84
 
85
 
86
  def muze_main_tabs_enabled() -> bool:
@@ -99,7 +97,7 @@ def muze_main_tabs_state() -> str:
99
  """One line for change lists and config_check: the value AND where it came from."""
100
  raw = (os.getenv(MUZE_ENGINE_MAIN_TABS_VAR) or "").strip()
101
  if not raw:
102
- return f"Muze on main tabs: OFF ({MUZE_ENGINE_MAIN_TABS_VAR} unset - default)"
103
  return (f"Muze on main tabs: {'ON' if muze_main_tabs_enabled() else 'OFF'} "
104
  f"({MUZE_ENGINE_MAIN_TABS_VAR}={raw})")
105
  MUZE_SHOWCASE_MIN = 2 # if surplus yields fewer, upgrade the retained heatmap too
@@ -1132,11 +1130,15 @@ def _fill_showcase(
1132
  # 1. Custom and exotic visuals — the reason this tab exists. A Muze tile the
1133
  # engine drew with HIGH confidence is a first-class chart and stays where
1134
  # its role put it; medium/low confidence and hand-made custom charts come here.
1135
- from demoprep_app.liveboards.muze_engine import muze_confidence
1136
  # No cap here: a medium/low Muze tile must NEVER stay on a main tab. Run
1137
  # 0910_125157 put a medium lollipop on Tixr's exec tab because the 5th one
1138
  # overflowed the showcase target.
1139
- take(lambda v: _chart_type(v) in SHOWCASE_CHART_TYPES and muze_confidence(v) != "high",
 
 
 
 
1140
  10**6)
1141
 
1142
  # 2. Exactly one table.
 
60
  # The data-first Muze engine (Sept 10) on the exec/manager tabs is a PER-SPACE
61
  # switch, read from the environment at build time:
62
  #
63
+ # unset / true answers whose shape fits a high-confidence
64
+ # GALLERY-tier archetype are drawn as Muze on
65
+ # the main tabs (the default)
66
+ # MUZE_ENGINE_MAIN_TABS=false the engine is skipped and the main tabs carry
67
+ # native charts as prod did 2026-09-15..22;
68
+ # Muze appears only on the showcase tab
69
  #
70
+ # ON is the default since 2026-09-22: leadership approved Gallery-grade Muze on
71
+ # prod main tabs (Showcase-grade stays on the 4th tab — see muze_archetypes
72
+ # GALLERY/SHOWCASE). The switch is kept as the emergency off: the 2026-09-15
73
+ # scare (a stakeholder asked that one-day-old Muze stay off prod) is exactly
74
+ # when it earns its keep, and a Space VARIABLE flips it without a deploy.
 
 
75
  #
76
  # The state is never invisible: config_check reports it per Space, and every
77
  # build stamps muze_main_tabs_state() into its change list. An unreadable value
78
  # is refused, not guessed - a typo must not quietly turn Muze on in prod.
79
  MUZE_ENGINE_MAIN_TABS_VAR = "MUZE_ENGINE_MAIN_TABS"
80
+ _FLAG_TRUE = ("", "1", "true", "on", "yes") # unset -> ON (default since 2026-09-22)
81
+ _FLAG_FALSE = ("0", "false", "off", "no")
82
 
83
 
84
  def muze_main_tabs_enabled() -> bool:
 
97
  """One line for change lists and config_check: the value AND where it came from."""
98
  raw = (os.getenv(MUZE_ENGINE_MAIN_TABS_VAR) or "").strip()
99
  if not raw:
100
+ return f"Muze on main tabs: ON ({MUZE_ENGINE_MAIN_TABS_VAR} unset - default)"
101
  return (f"Muze on main tabs: {'ON' if muze_main_tabs_enabled() else 'OFF'} "
102
  f"({MUZE_ENGINE_MAIN_TABS_VAR}={raw})")
103
  MUZE_SHOWCASE_MIN = 2 # if surplus yields fewer, upgrade the retained heatmap too
 
1130
  # 1. Custom and exotic visuals — the reason this tab exists. A Muze tile the
1131
  # engine drew with HIGH confidence is a first-class chart and stays where
1132
  # its role put it; medium/low confidence and hand-made custom charts come here.
1133
+ from demoprep_app.liveboards.muze_engine import muze_confidence, muze_tier
1134
  # No cap here: a medium/low Muze tile must NEVER stay on a main tab. Run
1135
  # 0910_125157 put a medium lollipop on Tixr's exec tab because the 5th one
1136
  # overflowed the showcase target.
1137
+ # Tier first (leadership 2026-09-22): SHOWCASE-grade Muze — and any Muze
1138
+ # tile without a recognised @archetype — goes here regardless of rating.
1139
+ # GALLERY-grade then routes on confidence as before.
1140
+ take(lambda v: _chart_type(v) in SHOWCASE_CHART_TYPES
1141
+ and (muze_tier(v) == "showcase" or muze_confidence(v) != "high"),
1142
  10**6)
1143
 
1144
  # 2. Exactly one table.
demoprep_app/liveboards/muze_archetypes.py CHANGED
@@ -37,6 +37,15 @@ from demoprep_app.liveboards.muze_templates import (
37
 
38
  HIGH, MEDIUM, LOW = "high", "medium", "low"
39
 
 
 
 
 
 
 
 
 
 
40
  # --------------------------------------------------------------------------- safety skeleton
41
  _PRELUDE = """\
42
  // @archetype __ARCHETYPE__
@@ -364,13 +373,13 @@ _STACKED_AREA = """\
364
  # slots: engine slot -> JS constant. Engine slots: measure, measure2, dim, dim2, date.
365
  ARCHETYPES: Dict[str, Dict[str, Any]] = {
366
  "bar_ranking": {
367
- "body": _BAR, "confidence": HIGH, "family": "Ranking",
368
  "needs": {"measures": 1, "dims": 1, "date": False},
369
  "max_card": {"dim": 15},
370
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD"},
371
  },
372
  "hbar_ranking": {
373
- "body": _HBAR, "confidence": HIGH, "family": "Ranking",
374
  "needs": {"measures": 1, "dims": 1, "date": False},
375
  "max_card": {"dim": 15},
376
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD"},
@@ -379,25 +388,25 @@ ARCHETYPES: Dict[str, Dict[str, Any]] = {
379
  # MEDIUM (showcase) 2026-09-16: head size solved — Muze point `size.value`
380
  # is a fraction of the band (1/8/18/90 all saturated at ~100px on 09-10;
381
  # 0.3 measured ~35px on the scatter). 0.15 here; promote after a real render.
382
- "body": _LOLLIPOP, "confidence": MEDIUM, "family": "Ranking",
383
  "needs": {"measures": 1, "dims": 1, "date": False},
384
  "max_card": {"dim": 15},
385
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD"},
386
  },
387
  "stacked_bar": {
388
- "body": _STACKED_BAR, "confidence": HIGH, "family": "Part of a Whole",
389
  "needs": {"measures": 1, "dims": 2, "date": False},
390
  "max_card": {"dim": 12, "dim2": 6},
391
  "slots": {"dim": "D_FIELD", "dim2": "C_FIELD", "measure": "M_FIELD"},
392
  },
393
  "multi_line": {
394
- "body": _MULTI_LINE, "confidence": HIGH, "family": "Trend Analysis",
395
  "needs": {"measures": 1, "dims": 1, "date": True},
396
  "max_card": {"dim": 6},
397
  "slots": {"date": "T_FIELD", "dim": "C_FIELD", "measure": "M_FIELD"},
398
  },
399
  "bullet": {
400
- "body": _BULLET, "confidence": HIGH, "family": "Ranking",
401
  "needs": {"measures": 2, "dims": 1, "date": False, "target": True},
402
  "max_card": {"dim": 12},
403
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD", "measure2": "T_FIELD"},
@@ -406,7 +415,7 @@ ARCHETYPES: Dict[str, Dict[str, Any]] = {
406
  # MEDIUM on purpose: the surplus-heatmap upgrade uses it, and surplus
407
  # heatmaps belong on the showcase tab. The engine's two-dim fallback to it
408
  # therefore lands there too; stacked_bar (few colours) is the main-tab pick.
409
- "body": None, "confidence": MEDIUM, "family": "Distribution", # drawn by muze_templates
410
  "needs": {"measures": 1, "dims": 2, "date": False},
411
  "max_card": {"dim": 12, "dim2": 12},
412
  "slots": {"dim": "x", "dim2": "y", "measure": "measure"},
@@ -416,7 +425,7 @@ ARCHETYPES: Dict[str, Dict[str, Any]] = {
416
  # category, no target) is what 15 native tiles on main tabs were on 2026-09-10.
417
  # HIGH: rendered and inspected 2026-09-10 (GridPuls "DR Capacity by Participant":
418
  # bars left axis, line right axis, compact ticks, both axes titled).
419
- "body": _DUAL_AXIS, "confidence": HIGH, "family": "Multivariate",
420
  "needs": {"measures": 2, "dims": 1, "date": False},
421
  "max_card": {"dim": 12},
422
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD", "measure2": "M2_FIELD"},
@@ -424,7 +433,7 @@ ARCHETYPES: Dict[str, Dict[str, Any]] = {
424
  "donut": {
425
  # HIGH 2026-09-16: Tixr "Tickets by Channel" (6 slices, legend bottom, all
426
  # labels visible). The engine only offers it for <=6 additive slices.
427
- "body": _DONUT, "confidence": HIGH, "family": "Part of a Whole",
428
  "needs": {"measures": 1, "dims": 1, "date": False},
429
  "max_card": {"dim": 6},
430
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD"},
@@ -435,7 +444,7 @@ ARCHETYPES: Dict[str, Dict[str, Any]] = {
435
  "pareto": {
436
  # HIGH 2026-09-16: Tixr "Top 10 Clients by Revenue" — bars in rank order,
437
  # cumulative-share line on a % axis, compact ticks.
438
- "body": _PARETO, "confidence": HIGH, "family": "Ranking",
439
  "needs": {"measures": 1, "dims": 1, "date": False},
440
  "max_card": {"dim": 15},
441
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD"},
@@ -443,7 +452,7 @@ ARCHETYPES: Dict[str, Dict[str, Any]] = {
443
  "polar_area": {
444
  # MEDIUM (showcase): the rose draws (True "Pipeline by Client Segment") but
445
  # small slices vanish — a showcase piece, not an executive read.
446
- "body": _POLAR_AREA, "confidence": MEDIUM, "family": "Distribution",
447
  "needs": {"measures": 1, "dims": 1, "date": False},
448
  "max_card": {"dim": 8},
449
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD"},
@@ -451,20 +460,20 @@ ARCHETYPES: Dict[str, Dict[str, Any]] = {
451
  "scatter": {
452
  # MEDIUM (showcase): renders (Tixr "Ticket & Fee Revenue by Vertical", size
453
  # 0.3) but four points is a sparse tile; earns main tabs once seen on more data.
454
- "body": _SCATTER, "confidence": MEDIUM, "family": "Correlation",
455
  "needs": {"measures": 2, "dims": 1, "date": False},
456
  "max_card": {"dim": 15},
457
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD", "measure2": "M2_FIELD"},
458
  },
459
  "bar_tick": {
460
- "body": _BAR_TICK, "confidence": MEDIUM, "family": "Ranking",
461
  "needs": {"measures": 2, "dims": 1, "date": False},
462
  "max_card": {"dim": 10},
463
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD", "measure2": "M2_FIELD"},
464
  },
465
  "stacked_bar_100": {
466
  # HIGH 2026-09-16: Tixr "Tickets by Genre" x Region — 0-100% axis, legend bottom.
467
- "body": _STACKED_BAR_100, "confidence": HIGH, "family": "Part of a Whole",
468
  "needs": {"measures": 1, "dims": 2, "date": False},
469
  "max_card": {"dim": 12, "dim2": 6},
470
  "slots": {"dim": "D_FIELD", "dim2": "C_FIELD", "measure": "M_FIELD"},
@@ -472,14 +481,14 @@ ARCHETYPES: Dict[str, Dict[str, Any]] = {
472
  "crosstab_bar": {
473
  # MEDIUM (showcase): clean trellis (Tixr "Revenue by Sales Channel" x Payment
474
  # Category) but tall; a third-width tile would cramp it.
475
- "body": _CROSSTAB_BAR, "confidence": MEDIUM, "family": "Ranking",
476
  "needs": {"measures": 1, "dims": 2, "date": False},
477
  "max_card": {"dim": 10, "dim2": 5},
478
  "slots": {"dim": "D_FIELD", "dim2": "C_FIELD", "measure": "M_FIELD"},
479
  },
480
  "stacked_area": {
481
  # HIGH 2026-09-16: True "Monthly Revenue by Service Line", 18 months, month ticks.
482
- "body": _STACKED_AREA, "confidence": HIGH, "family": "Part of a Whole",
483
  "needs": {"measures": 1, "dims": 1, "date": True},
484
  "max_card": {"dim": 6},
485
  "slots": {"date": "T_FIELD", "dim": "C_FIELD", "measure": "M_FIELD"},
@@ -489,6 +498,12 @@ ARCHETYPES: Dict[str, Dict[str, Any]] = {
489
  TARGET_WORDS = ("target", "goal", "budget", "plan", "quota", "forecast", "benchmark")
490
 
491
 
 
 
 
 
 
 
492
  def confidence_of(archetype: str) -> str:
493
  return ARCHETYPES[archetype]["confidence"]
494
 
 
37
 
38
  HIGH, MEDIUM, LOW = "high", "medium", "low"
39
 
40
+ # Two tiers of Muze, as ThoughtSpot itself publishes them (leadership 2026-09-22):
41
+ # GALLERY — Muze Gallery grammar: rows/columns/color, one or two layers, a
42
+ # dozen lines an SE can edit in Muze Studio. Allowed on tabs 1-3.
43
+ # SHOWCASE — Showcase-grade programs (KPI card boards, risk matrix, ridgelines,
44
+ # "year in sales"): transforms, several canvases, custom DOM. Only
45
+ # ever on the 4th tab, next to the table and the experiments.
46
+ # Every archetype declares its tier; the layout routes on it BEFORE confidence.
47
+ GALLERY, SHOWCASE = "gallery", "showcase"
48
+
49
  # --------------------------------------------------------------------------- safety skeleton
50
  _PRELUDE = """\
51
  // @archetype __ARCHETYPE__
 
373
  # slots: engine slot -> JS constant. Engine slots: measure, measure2, dim, dim2, date.
374
  ARCHETYPES: Dict[str, Dict[str, Any]] = {
375
  "bar_ranking": {
376
+ "body": _BAR, "confidence": HIGH, "family": "Ranking", "tier": GALLERY,
377
  "needs": {"measures": 1, "dims": 1, "date": False},
378
  "max_card": {"dim": 15},
379
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD"},
380
  },
381
  "hbar_ranking": {
382
+ "body": _HBAR, "confidence": HIGH, "family": "Ranking", "tier": GALLERY,
383
  "needs": {"measures": 1, "dims": 1, "date": False},
384
  "max_card": {"dim": 15},
385
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD"},
 
388
  # MEDIUM (showcase) 2026-09-16: head size solved — Muze point `size.value`
389
  # is a fraction of the band (1/8/18/90 all saturated at ~100px on 09-10;
390
  # 0.3 measured ~35px on the scatter). 0.15 here; promote after a real render.
391
+ "body": _LOLLIPOP, "confidence": MEDIUM, "family": "Ranking", "tier": GALLERY,
392
  "needs": {"measures": 1, "dims": 1, "date": False},
393
  "max_card": {"dim": 15},
394
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD"},
395
  },
396
  "stacked_bar": {
397
+ "body": _STACKED_BAR, "confidence": HIGH, "family": "Part of a Whole", "tier": GALLERY,
398
  "needs": {"measures": 1, "dims": 2, "date": False},
399
  "max_card": {"dim": 12, "dim2": 6},
400
  "slots": {"dim": "D_FIELD", "dim2": "C_FIELD", "measure": "M_FIELD"},
401
  },
402
  "multi_line": {
403
+ "body": _MULTI_LINE, "confidence": HIGH, "family": "Trend Analysis", "tier": GALLERY,
404
  "needs": {"measures": 1, "dims": 1, "date": True},
405
  "max_card": {"dim": 6},
406
  "slots": {"date": "T_FIELD", "dim": "C_FIELD", "measure": "M_FIELD"},
407
  },
408
  "bullet": {
409
+ "body": _BULLET, "confidence": HIGH, "family": "Ranking", "tier": GALLERY,
410
  "needs": {"measures": 2, "dims": 1, "date": False, "target": True},
411
  "max_card": {"dim": 12},
412
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD", "measure2": "T_FIELD"},
 
415
  # MEDIUM on purpose: the surplus-heatmap upgrade uses it, and surplus
416
  # heatmaps belong on the showcase tab. The engine's two-dim fallback to it
417
  # therefore lands there too; stacked_bar (few colours) is the main-tab pick.
418
+ "body": None, "confidence": MEDIUM, "family": "Distribution", "tier": GALLERY, # drawn by muze_templates
419
  "needs": {"measures": 1, "dims": 2, "date": False},
420
  "max_card": {"dim": 12, "dim2": 12},
421
  "slots": {"dim": "x", "dim2": "y", "measure": "measure"},
 
425
  # category, no target) is what 15 native tiles on main tabs were on 2026-09-10.
426
  # HIGH: rendered and inspected 2026-09-10 (GridPuls "DR Capacity by Participant":
427
  # bars left axis, line right axis, compact ticks, both axes titled).
428
+ "body": _DUAL_AXIS, "confidence": HIGH, "family": "Multivariate", "tier": GALLERY,
429
  "needs": {"measures": 2, "dims": 1, "date": False},
430
  "max_card": {"dim": 12},
431
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD", "measure2": "M2_FIELD"},
 
433
  "donut": {
434
  # HIGH 2026-09-16: Tixr "Tickets by Channel" (6 slices, legend bottom, all
435
  # labels visible). The engine only offers it for <=6 additive slices.
436
+ "body": _DONUT, "confidence": HIGH, "family": "Part of a Whole", "tier": GALLERY,
437
  "needs": {"measures": 1, "dims": 1, "date": False},
438
  "max_card": {"dim": 6},
439
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD"},
 
444
  "pareto": {
445
  # HIGH 2026-09-16: Tixr "Top 10 Clients by Revenue" — bars in rank order,
446
  # cumulative-share line on a % axis, compact ticks.
447
+ "body": _PARETO, "confidence": HIGH, "family": "Ranking", "tier": GALLERY,
448
  "needs": {"measures": 1, "dims": 1, "date": False},
449
  "max_card": {"dim": 15},
450
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD"},
 
452
  "polar_area": {
453
  # MEDIUM (showcase): the rose draws (True "Pipeline by Client Segment") but
454
  # small slices vanish — a showcase piece, not an executive read.
455
+ "body": _POLAR_AREA, "confidence": MEDIUM, "family": "Distribution", "tier": GALLERY,
456
  "needs": {"measures": 1, "dims": 1, "date": False},
457
  "max_card": {"dim": 8},
458
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD"},
 
460
  "scatter": {
461
  # MEDIUM (showcase): renders (Tixr "Ticket & Fee Revenue by Vertical", size
462
  # 0.3) but four points is a sparse tile; earns main tabs once seen on more data.
463
+ "body": _SCATTER, "confidence": MEDIUM, "family": "Correlation", "tier": GALLERY,
464
  "needs": {"measures": 2, "dims": 1, "date": False},
465
  "max_card": {"dim": 15},
466
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD", "measure2": "M2_FIELD"},
467
  },
468
  "bar_tick": {
469
+ "body": _BAR_TICK, "confidence": MEDIUM, "family": "Ranking", "tier": GALLERY,
470
  "needs": {"measures": 2, "dims": 1, "date": False},
471
  "max_card": {"dim": 10},
472
  "slots": {"dim": "D_FIELD", "measure": "M_FIELD", "measure2": "M2_FIELD"},
473
  },
474
  "stacked_bar_100": {
475
  # HIGH 2026-09-16: Tixr "Tickets by Genre" x Region — 0-100% axis, legend bottom.
476
+ "body": _STACKED_BAR_100, "confidence": HIGH, "family": "Part of a Whole", "tier": GALLERY,
477
  "needs": {"measures": 1, "dims": 2, "date": False},
478
  "max_card": {"dim": 12, "dim2": 6},
479
  "slots": {"dim": "D_FIELD", "dim2": "C_FIELD", "measure": "M_FIELD"},
 
481
  "crosstab_bar": {
482
  # MEDIUM (showcase): clean trellis (Tixr "Revenue by Sales Channel" x Payment
483
  # Category) but tall; a third-width tile would cramp it.
484
+ "body": _CROSSTAB_BAR, "confidence": MEDIUM, "family": "Ranking", "tier": GALLERY,
485
  "needs": {"measures": 1, "dims": 2, "date": False},
486
  "max_card": {"dim": 10, "dim2": 5},
487
  "slots": {"dim": "D_FIELD", "dim2": "C_FIELD", "measure": "M_FIELD"},
488
  },
489
  "stacked_area": {
490
  # HIGH 2026-09-16: True "Monthly Revenue by Service Line", 18 months, month ticks.
491
+ "body": _STACKED_AREA, "confidence": HIGH, "family": "Part of a Whole", "tier": GALLERY,
492
  "needs": {"measures": 1, "dims": 1, "date": True},
493
  "max_card": {"dim": 6},
494
  "slots": {"date": "T_FIELD", "dim": "C_FIELD", "measure": "M_FIELD"},
 
498
  TARGET_WORDS = ("target", "goal", "budget", "plan", "quota", "forecast", "benchmark")
499
 
500
 
501
+ def tier_of(archetype: str) -> str:
502
+ """GALLERY or SHOWCASE; unknown archetypes are treated as SHOWCASE (kept off the main tabs)."""
503
+ spec = ARCHETYPES.get(archetype)
504
+ return spec.get("tier", SHOWCASE) if spec else SHOWCASE
505
+
506
+
507
  def confidence_of(archetype: str) -> str:
508
  return ARCHETYPES[archetype]["confidence"]
509
 
demoprep_app/liveboards/muze_engine.py CHANGED
@@ -227,6 +227,17 @@ def convert_to_muze(viz: Dict[str, Any], archetype: str, bindings: Dict[str, str
227
  answer["display_mode"] = "CHART_MODE"
228
 
229
 
 
 
 
 
 
 
 
 
 
 
 
230
  def muze_confidence(viz: Dict[str, Any]) -> Optional[str]:
231
  """Confidence of the archetype that drew a MUZE_STUDIO viz, or None if not Muze."""
232
  name = archetype_of((viz.get("answer") or {}).get("chart") or {})
 
227
  answer["display_mode"] = "CHART_MODE"
228
 
229
 
230
+ def muze_tier(viz: Dict[str, Any]) -> Optional[str]:
231
+ """GALLERY / SHOWCASE for a MUZE_STUDIO viz, None if not Muze. Unknown or
232
+ hand-made Muze (no @archetype tag) is SHOWCASE: off the main tabs."""
233
+ chart = (viz.get("answer") or {}).get("chart") or {}
234
+ if chart.get("type") != MUZE_CHART_TYPE:
235
+ return None
236
+ from demoprep_app.liveboards.muze_archetypes import tier_of
237
+ name = archetype_of(chart)
238
+ return tier_of(name) if name else "showcase"
239
+
240
+
241
  def muze_confidence(viz: Dict[str, Any]) -> Optional[str]:
242
  """Confidence of the archetype that drew a MUZE_STUDIO viz, or None if not Muze."""
243
  name = archetype_of((viz.get("answer") or {}).get("chart") or {})
tests/test_muze_engine.py CHANGED
@@ -210,3 +210,28 @@ def test_pareto_only_for_additive_measures():
210
  assert "pareto" not in picks
211
  total = _viz("V2", "Revenue by Provider", "[Revenue] [Provider Type]", ["Provider Type", "Total Revenue"])
212
  assert "pareto" in {choose_archetype(_shape(total, {"Provider Type": 8}), i)[0] for i in range(12)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  assert "pareto" not in picks
211
  total = _viz("V2", "Revenue by Provider", "[Revenue] [Provider Type]", ["Provider Type", "Total Revenue"])
212
  assert "pareto" in {choose_archetype(_shape(total, {"Provider Type": 8}), i)[0] for i in range(12)}
213
+
214
+
215
+ def test_every_archetype_declares_a_tier_and_all_current_ones_are_gallery_grade():
216
+ from demoprep_app.liveboards.muze_archetypes import GALLERY, SHOWCASE, tier_of
217
+ for name, spec in ARCHETYPES.items():
218
+ assert spec.get("tier") in (GALLERY, SHOWCASE), name
219
+ assert tier_of(name) == GALLERY, f"{name}: every shipped archetype is Gallery grammar (leadership 2026-09-22)"
220
+ assert tier_of("something_new") == SHOWCASE # unknown stays off the main tabs
221
+
222
+
223
+ def test_showcase_tier_muze_always_routes_to_the_fourth_tab():
224
+ """A SHOWCASE-tier tile must leave the main tabs even if someone rates it HIGH."""
225
+ from demoprep_app.liveboards import muze_archetypes as MA
226
+ from demoprep_app.liveboards.golden_layout import _fill_showcase
227
+ viz = _viz("V1", "Revenue by Region", "[Revenue] [Region]", ["Region", "Total Revenue"])
228
+ convert_to_muze(viz, "bar_ranking", {"dim": "Region", "measure": "Total Revenue"})
229
+ saved = dict(MA.ARCHETYPES["bar_ranking"])
230
+ try:
231
+ MA.ARCHETYPES["bar_ranking"]["tier"] = MA.SHOWCASE # pretend it is a program
232
+ MA.ARCHETYPES["bar_ranking"]["confidence"] = HIGH
233
+ buckets = {"breakdown": ["V1"], "ranking": [], "detail": [], "driver": [], "trend": [], "showcase": []}
234
+ _fill_showcase(buckets, [viz])
235
+ assert buckets["showcase"] == ["V1"] and buckets["breakdown"] == []
236
+ finally:
237
+ MA.ARCHETYPES["bar_ranking"] = saved
tests/test_muze_main_tabs_flag.py CHANGED
@@ -5,7 +5,8 @@ Run: pytest tests/test_muze_main_tabs_flag.py -q --noconftest
5
  It used to be a module constant: True on test, False in the emergency prod
6
  build. That meant test and prod could never run the same commit, so nothing
7
  else could ship to prod without carrying Muze along. Now one build runs
8
- everywhere and the Space variable decides. OFF is the default, and the state is
 
9
  stamped into every build's change list so it is never invisible.
10
  """
11
  from __future__ import annotations
@@ -21,22 +22,23 @@ from demoprep_app.liveboards.golden_layout import (
21
 
22
 
23
  class TestReading:
24
- @pytest.mark.parametrize("raw", ["true", "TRUE", " True ", "1", "on", "yes"])
25
  def test_true_spellings(self, monkeypatch, raw):
26
  monkeypatch.setenv(MUZE_ENGINE_MAIN_TABS_VAR, raw)
27
  assert muze_main_tabs_enabled() is True
28
 
29
- @pytest.mark.parametrize("raw", ["false", "FALSE", "0", "off", "no", "", " "])
30
  def test_false_spellings(self, monkeypatch, raw):
31
  monkeypatch.setenv(MUZE_ENGINE_MAIN_TABS_VAR, raw)
32
  assert muze_main_tabs_enabled() is False
33
 
34
- def test_unset_is_off_and_says_so(self, monkeypatch):
35
- """Default OFF: an unconfigured Space (prod) never draws Muze on main tabs."""
 
36
  monkeypatch.delenv(MUZE_ENGINE_MAIN_TABS_VAR, raising=False)
37
- assert muze_main_tabs_enabled() is False
38
  state = muze_main_tabs_state()
39
- assert "OFF" in state and "unset" in state
40
 
41
  def test_a_set_value_names_itself(self, monkeypatch):
42
  monkeypatch.setenv(MUZE_ENGINE_MAIN_TABS_VAR, "true")
@@ -84,7 +86,7 @@ class TestGate:
84
  return calls, changes
85
 
86
  def test_off_skips_the_engine_and_stamps_off(self, monkeypatch):
87
- monkeypatch.delenv(MUZE_ENGINE_MAIN_TABS_VAR, raising=False)
88
  calls, changes = self._run(monkeypatch)
89
  assert calls == []
90
  assert any(c.startswith("Muze on main tabs: OFF") for c in changes), changes
@@ -97,7 +99,7 @@ class TestGate:
97
 
98
  def test_the_same_code_flips_with_the_environment(self, monkeypatch):
99
  """One commit, two Spaces: only the variable differs."""
100
- monkeypatch.delenv(MUZE_ENGINE_MAIN_TABS_VAR, raising=False)
101
  off, _ = self._run(monkeypatch)
102
  monkeypatch.setenv(MUZE_ENGINE_MAIN_TABS_VAR, "true")
103
  on, _ = self._run(monkeypatch)
 
5
  It used to be a module constant: True on test, False in the emergency prod
6
  build. That meant test and prod could never run the same commit, so nothing
7
  else could ship to prod without carrying Muze along. Now one build runs
8
+ everywhere and the Space variable decides. ON is the default since 2026-09-22
9
+ (leadership approved Gallery-grade Muze on prod main tabs), and the state is
10
  stamped into every build's change list so it is never invisible.
11
  """
12
  from __future__ import annotations
 
22
 
23
 
24
  class TestReading:
25
+ @pytest.mark.parametrize("raw", ["true", "TRUE", " True ", "1", "on", "yes", "", " "]) # blank = default = ON
26
  def test_true_spellings(self, monkeypatch, raw):
27
  monkeypatch.setenv(MUZE_ENGINE_MAIN_TABS_VAR, raw)
28
  assert muze_main_tabs_enabled() is True
29
 
30
+ @pytest.mark.parametrize("raw", ["false", "FALSE", "0", "off", "no"])
31
  def test_false_spellings(self, monkeypatch, raw):
32
  monkeypatch.setenv(MUZE_ENGINE_MAIN_TABS_VAR, raw)
33
  assert muze_main_tabs_enabled() is False
34
 
35
+ def test_unset_is_on_and_says_so(self, monkeypatch):
36
+ """Default ON since 2026-09-22: an unconfigured Space draws Gallery-grade
37
+ Muze on the main tabs; only an explicit false switches it off."""
38
  monkeypatch.delenv(MUZE_ENGINE_MAIN_TABS_VAR, raising=False)
39
+ assert muze_main_tabs_enabled() is True
40
  state = muze_main_tabs_state()
41
+ assert "ON" in state and "unset" in state
42
 
43
  def test_a_set_value_names_itself(self, monkeypatch):
44
  monkeypatch.setenv(MUZE_ENGINE_MAIN_TABS_VAR, "true")
 
86
  return calls, changes
87
 
88
  def test_off_skips_the_engine_and_stamps_off(self, monkeypatch):
89
+ monkeypatch.setenv(MUZE_ENGINE_MAIN_TABS_VAR, "false")
90
  calls, changes = self._run(monkeypatch)
91
  assert calls == []
92
  assert any(c.startswith("Muze on main tabs: OFF") for c in changes), changes
 
99
 
100
  def test_the_same_code_flips_with_the_environment(self, monkeypatch):
101
  """One commit, two Spaces: only the variable differs."""
102
+ monkeypatch.setenv(MUZE_ENGINE_MAIN_TABS_VAR, "false")
103
  off, _ = self._run(monkeypatch)
104
  monkeypatch.setenv(MUZE_ENGINE_MAIN_TABS_VAR, "true")
105
  on, _ = self._run(monkeypatch)