chore(config): default feature videos off until real clips ship - #9315
Conversation
|
Intent: Stop feature videos from switching themselves on for every install the moment the frontend merges. The kill switch already works; only its default direction changes, so turning the feature on becomes a decision someone makes after looking at it. Not a goal: Removing or weakening the feature, the catalog, or the switch itself. Recording real clips. Any change to how selection, the probes, or the permanence of a verdict behave. Flipping the default back — that is a later change, once real clips exist. |
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Backend existence gate is the right layer — the only place that can know — and withholding-without-writing makes every failure mode recoverable. Suggestions
[DESIGN-REVIEWED] f73ab14 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of Both counts from the description verified: the flag has one consumer ( First-Principles-Verdict: PASS Both items are derived from one nameable defect — a blank-player dialog writes a permanent What this change shipsIntent: stop an unfinished startup-dialog feature from reaching users before its clips exist — a FIX (pre-launch gating).
Verification notes: flag consumers in [FIRST-PRINCIPLES-REVIEWED] f73ab14 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
CI note: the three reds on this head are one inherited failure, not this diff
The failure It is on Identical assertion, no changes from this branch present. That is stronger evidence Root cause, since the diff makes it easy to name. The dicts are otherwise equal, so the extra key is the whole failure. (The Why Why this diff cannot reach it. This PR changes the I did try the re-run first, as asked. The gates that DO cover this diff are green: 116 tests across |
28159ef to
7d51871
Compare
7d51871 to
545806a
Compare
|
Scope change, by owner decision — not an edit to the intent above. The intent comment said "no change to how selection behaves". This head ( Left as posted so later retrospectives measure against the original; this note is the record of when and why the scope grew. Selection ORDER, the probes, and verdict permanence are still untouched — the gate only removes entries from consideration, and |
…t on disk Two changes to the same gate, both about not showing a clip nobody should see yet. Neither touches selection order, the probes, or verdict permanence. 1. `dashboard.feature_videos_enabled` defaults to `false`. The backend landed in #9168 defaulting to `true`; nothing shows only because no frontend mounts the dialog yet, so the moment #9169 merges every install would start playing a clip at startup with placeholder media. A startup dialog is the most intrusive surface here and its verdict is permanent, so turning it on should be a deliberate decision. Flipped in the dataclass field and the loader fallback; `config-baseline.json` regenerated. 2. "Asset shipped" is now a precondition of "on offer". `_entry_is_valid` checked the SHAPE of `src`/`poster` but never whether the file existed, so the catalog offered entries whose media is not shipped. The frontend cannot catch that: its `<video>` is `preload="none"`, so nothing is fetched -- and no media error can fire -- until the user presses play. The dialog opens on the JSON alone, around a blank player, and "Got it" writes a permanent `seen` that retires the real intro before anyone saw it. New `offerable()` = `catalog()` filtered to entries whose clip AND poster are on disk under `static/dist/app-assets`; `select_next` walks that. `catalog()` keeps structural-only semantics on purpose, because the feedback route checks membership against it and a user already shown a clip must still be able to record a verdict if its asset later vanishes. Tests: the three `TestConfigFlag` cases pin the new default (the non-bool case is MORE load-bearing now, since `bool("false")` is `True`); an autouse fixture treats media as shipped so the 100+ selection/route tests keep testing what they are about; `TestAssetExistenceGate` (7 cases) replaces that default with a real temp directory and covers the URL->disk mapping, withholding on a missing clip or poster, skipping past an unshipped entry, recovery once the clip lands, and that `catalog()` and `offerable()` genuinely differ. Mutation-verified: routing `select_next` back through `catalog()` fails 3 of the 7. A pause, not a retreat: turn it on locally, record real clips, then flip the default back in its own change. The existence gate stays.
545806a to
f73ab14
Compare
chenmingwei23
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: chore/config (7 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: chore(config) default-off config flag for feature videos, config loader/sections + docs + tests, no runtime security impact.
Problem / Motivation
Two ways the feature-video dialog can show a user something nobody should see yet.
It ships on. The backend landed in #9168 with
dashboard.feature_videos_enableddefaulting to
true. Nothing shows yet only because no frontend mounts the dialog.The moment #9169 merges, every install starts playing a clip at startup — a feature
nobody has used end to end, with placeholder media rather than real recordings.
It offers clips that are not on disk.
_entry_is_validchecks the shape of anentry's
srcandposterbut never whether the file exists. The catalog alreadynames
feature-tips.mp4andmonitor-loops.mp4, and neither has been recorded.Why it matters
A startup dialog is the most intrusive surface this app has, and its verdict is
permanent: watch or close a clip and that clip never comes back.
The second problem is the sharper one, because the frontend cannot defend against
it. The dialog opens on the JSON answer alone, and its
<video>ispreload="none"— nothing is fetched, and no media error can fire, until the user presses play. So an
entry whose clip is not shipped opens a real dialog titled "Feature tips above the
composer" around a blank player. The natural "Got it" writes a permanent
seen, andthe real intro is retired before anyone could see it. Design Review on #9169 found
this and named the backend check as the smaller, more general fix; it is.
What changed (motivation → approach → change)
Both changes sit in the same gate and change nothing about selection order, the
probes, or what a verdict means.
The default flips to
falsein the two places that decide it: the dataclassfield (
config/sections.py) and the loader's fallback for a config that does notmention the key (
config/loader.py).config-baseline.jsonis regenerated from thedataclass registry — it records the default in two spots, so a hand-edit would have
missed one. To turn it on:
{ "dashboard": { "feature_videos_enabled": true } }in~/.kiro/crew/config.json."Asset shipped" becomes a precondition of "on offer". A new
offerable()iscatalog()filtered to entries whose clip AND poster exist on disk understatic/dist/app-assets— the directoryserver.pymounts at/app-assets.select_nextwalksofferable(). An entry it withholds is logged and left alone:nothing is written, so it comes back on its own the launch after its clip lands.
catalog()deliberately keeps its structural-only meaning. The feedback route checksmembership against it, and a user who was shown a clip must still be able to record a
verdict on it even if its asset later goes missing. The two sets differing is pinned
by a test.
flowchart LR subgraph Before A1[CATALOG]:::ctx --> B1[_entry_is_valid<br/>shape only]:::ctx --> C1[select_next]:::ctx --> D1[dialog opens<br/>blank player]:::removed end subgraph After A2[CATALOG]:::ctx --> B2[_entry_is_valid<br/>shape only]:::ctx --> E2[offerable<br/>clip + poster on disk?]:::added --> C2[select_next]:::ctx E2 -. withheld, nothing written .-> F2[offered again<br/>once clip lands]:::added end classDef added fill:#DCFCE7,stroke:#16A34A,color:#14532D,stroke-width:2px classDef changed fill:#FEF3C7,stroke:#D97706,color:#78350F,stroke-width:2px classDef removed fill:#FEE2E2,stroke:#DC2626,color:#7F1D1D,stroke-dasharray:4 3 classDef ctx fill:#E0F2FE,stroke:#0284C7,color:#0C4A6E linkStyle 2 stroke:#DC2626,stroke-dasharray:4 3 linkStyle 4,5,6 stroke:#16A34A,stroke-width:2px🟩 added · 🟨 changed · 🟥 removed · 🟦 unchanged
A clip with no file on disk is now withheld by the backend, so the dialog never
opens around a blank player.
Both docs pages say
falseis the default, and the feature-videos page's selectionrule now names the shipped-asset precondition.
This is a pause, not a retreat: land this, turn it on locally, record real clips,
then flip the default back to
truein its own change. The existence gate stays.Tests
test/test_feature_videos.py::TestConfigFlagpinned the old default in three places;all three now pin the new one.
test_a_non_bool_value_keeps_the_defaultis moreload-bearing now:
bool("false")isTrue, so without_safe_boolthat config wouldturn the feature ON against the operator's wishes.
An autouse fixture treats every asset as shipped, so the 100+ existing selection,
probe and route tests keep testing what they were written for instead of all
exercising the withholding branch.
TestAssetExistenceGate(7 cases) replaces thatdefault with a real temp directory and covers: the
/app-assets/...→ disk mapping(and that an off-prefix path is never "present"); withholding on a missing clip;
withholding on a missing poster alone; a shipped entry being offered; selection
skipping past an unshipped entry to a shipped one; the entry coming back once its
clip lands; and
catalog()still containing whatofferable()withholds.Mutation-verified: routing
select_nextback throughcatalog()fails 3 of the 7.123 tests pass across this file and
test_config_baseline.py.Manual verification
N/A — unit coverage is sufficient. The default is read through the real loader from
a real config file, and the existence gate is exercised against a real directory.
Blast radius, checked:
_enabled()(feature_videos.py:555) is the flag's onlyconsumer in
src/;select_nextis the only caller that moved toofferable(), andthe feedback route's membership check stays on
catalog()by design.Related Issues
no linked issue: pre-launch gating, not a tracked defect.
Sequencing note: this should land before or with #9169. Merging #9169 first opens
a window where an unvalidated feature is on for everyone, offering clips that do not
exist.
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)