Skip to content

Commit 3fd9ecb

Browse files
committed
fix(deck-neon): address code-review findings
- SKILL.md: replace stale fixed-px sizes in the slide-grammar table (chapter 120px/40px, data 120-160px, copied from the fixed-1280px editorial skill) with clamp()/relative values, matching this skill's own clamp() mandate; de-ambiguate the --type-label scale row (letter-spacing was in the line-height slot); add a 'content must fit one screen, no scroll' rule since overflow:hidden + justify-center silently clips tall slides top and bottom - cli.py / chat.py: derive the deck label from DEFAULT_DECK_SKILL instead of a hardcoded literal (the label could silently report the wrong skill after a future default change); note the --help default must track the constant - tests: add test_deck_neon_prompt.py guarding the now-default neon skill's frontmatter + od contract (previously only deck-editorial was pinned)
1 parent a2baa39 commit 3fd9ecb

4 files changed

Lines changed: 105 additions & 5 deletions

File tree

openkb/agent/chat.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,8 @@ async def _handle_slash_deck(arg: str, kb_dir: Path, style: Style) -> None:
659659
model = config.get("model", DEFAULT_CONFIG["model"])
660660

661661
from openkb.skill.generator import Generator
662-
skill_label = skill_name if skill_name else "openkb-deck-neon (default)"
662+
from openkb.deck.creator import DEFAULT_DECK_SKILL
663+
skill_label = skill_name if skill_name else f"{DEFAULT_DECK_SKILL} (default)"
663664
_fmt(
664665
style,
665666
("class:slash.help", f"Generating deck '{name}' via skill {skill_label}...\n"),

openkb/cli.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2261,6 +2261,8 @@ def deck():
22612261
"--skill", "skill_name",
22622262
metavar="SKILL_NAME",
22632263
default=None,
2264+
# NOTE: 'openkb-deck-neon' below must stay in sync with
2265+
# DEFAULT_DECK_SKILL in openkb/deck/creator.py.
22642266
help=(
22652267
"Which deck skill to use. Defaults to 'openkb-deck-neon' "
22662268
"(the built-in). Pass e.g. 'deck-guizang-editorial' to route to "
@@ -2341,7 +2343,8 @@ def deck_new(ctx, name, intent, yes_flag, critique_flag, skill_name):
23412343

23422344
# Run the generator.
23432345
from openkb.skill.generator import Generator
2344-
skill_label = skill_name if skill_name else "openkb-deck-neon (default)"
2346+
from openkb.deck.creator import DEFAULT_DECK_SKILL
2347+
skill_label = skill_name if skill_name else f"{DEFAULT_DECK_SKILL} (default)"
23452348
click.echo(f"Generating deck '{name}' via skill {skill_label}...")
23462349
gen = Generator(
23472350
target_type="deck",

skills/openkb-deck-neon/SKILL.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ on a 2560px screen). vw drives the middle value; the rem caps keep it sane:
9898
* `--type-title`: clamp(1.9rem, 3.2vw, 3rem) / 1.12 — normal slide titles
9999
* `--type-body`: clamp(1.05rem, 1.4vw, 1.4rem) / 1.6 — body copy (var(--ink))
100100
* `--type-quote`: clamp(1.6rem, 2.6vw, 2.2rem) / 1.35 — pull quotes
101-
* `--type-label`: clamp(.66rem, .8vw, .8rem) / .22em uppercase mono — tracks
101+
* `--type-label`: size `clamp(.66rem,.8vw,.8rem)`, `letter-spacing:.22em`, uppercase, mono — label tracks
102102

103103
### Atmosphere (fixed background layers on every slide)
104104

@@ -130,6 +130,12 @@ These three layers are what stop the dark background from looking flat/cheap.
130130
* The aurora, dot-grid, top label row and the bottom signature bar all span the
131131
FULL viewport width (they live on `.slide`, which is the whole window). Inner
132132
padding uses the `clamp()` values above so content breathes on any size.
133+
* **One screen, no scroll — content MUST fit the viewport height.** With
134+
`overflow:hidden` + `justify-content:center`, a slide taller than the window
135+
is clipped at BOTH top and bottom and is unreachable. Stay within the
136+
bullet/word caps (§Failure modes), lean on the `vh` terms in the type scale so
137+
text shrinks on short viewports, and if a slide would overflow, CUT content —
138+
never let it clip.
133139
* **Visual signature:** a glowing 3px bar along the bottom edge, gradient
134140
`teal → sky → magenta`, with `box-shadow:0 0 14px` of --teal. This is the
135141
deck's signature — present on every slide.
@@ -189,11 +195,11 @@ shrink to a corner. Never size a content box to `min-content`/`fit-content`
189195
| `data-type` | Use | Neon signature |
190196
|---|---|---|
191197
| `cover` | tag + huge gradient title + 1-line subtitle | strongest aurora; mono "OPENKB" top-left; gradient display title |
192-
| `chapter` | section divider: oversize number + name | number 120px mono, teal with glow; name 40px display |
198+
| `chapter` | section divider: oversize number + name | number `clamp(4rem,12vh,9rem)` mono, teal with glow; name in `--type-display` |
193199
| `thesis` | one claim + short explanation | title fills ~60% height; ONE keyword in teal+glow; rest --ink |
194200
| `quote` | italic pull-quote + attribution | centered; left teal glowing vertical rule; quote in --soft |
195201
| `compare` | two-column comparison, 3–5 lines each | two glass panels; glowing teal vertical rule between them |
196-
| `data` | one number + label + one-line read | number 120–160px gradient+glow; micro-copy 12px mono |
202+
| `data` | one number + label + one-line read | number `clamp(3.5rem,11vh,9rem)` gradient+glow (per §Composition); micro-copy in `--type-label` |
197203
| `closing` | mirrors cover; thanks / next step | same scale as cover; aurora dims toward calm |
198204

199205
Cover/closing have no chapter context → top-left label is "OPENKB".

tests/test_deck_neon_prompt.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Sanity tests for the built-in deck-neon skill.
2+
3+
``openkb-deck-neon`` is the DEFAULT deck skill (``DEFAULT_DECK_SKILL`` in
4+
``openkb/deck/creator.py``), so a typo in its frontmatter or a broken ``od``
5+
contract would break the most-traveled ``openkb deck new`` path at runtime
6+
while every other test stayed green. These tests pin the structural anchors
7+
the validator and generator depend on — mirroring ``test_deck_prompt.py``
8+
which guards the sibling deck-editorial skill.
9+
"""
10+
from __future__ import annotations
11+
12+
from pathlib import Path
13+
14+
from openkb.agent.skills import _parse_frontmatter
15+
from openkb.deck.creator import DEFAULT_DECK_SKILL
16+
17+
SKILL_MD = (
18+
Path(__file__).resolve().parent.parent
19+
/ "skills"
20+
/ "openkb-deck-neon"
21+
/ "SKILL.md"
22+
)
23+
24+
25+
def _load() -> tuple[dict, str]:
26+
return _parse_frontmatter(SKILL_MD.read_text(encoding="utf-8"))
27+
28+
29+
def test_neon_is_the_default_deck_skill():
30+
# The skill dir name, the frontmatter name, and the routed default must agree.
31+
assert DEFAULT_DECK_SKILL == "openkb-deck-neon"
32+
meta, _ = _load()
33+
assert meta.get("name") == "openkb-deck-neon"
34+
35+
36+
def test_skill_file_present_and_substantive():
37+
assert SKILL_MD.is_file(), f"skill file missing at {SKILL_MD}"
38+
meta, body = _load()
39+
assert isinstance(meta.get("description"), str) and len(meta["description"]) > 40
40+
assert len(body) > 1000, "skill body is suspiciously short"
41+
42+
43+
def test_od_contract_is_intact():
44+
"""run_skill reads od.mode/output_path_template; validate_deck reads
45+
od.deck_grammar. A missing key silently skips output enforcement."""
46+
meta, _ = _load()
47+
od = meta.get("od")
48+
assert isinstance(od, dict), "frontmatter must carry an `od:` block"
49+
assert od.get("mode") == "deck"
50+
assert od.get("output_path_template") == "output/decks/{slug}/index.html"
51+
grammar = od.get("deck_grammar")
52+
assert isinstance(grammar, dict), "od.deck_grammar must be present"
53+
assert grammar.get("kind_attr") == "data-type"
54+
assert "cover" in grammar.get("required", [])
55+
assert "closing" in grammar.get("required", [])
56+
allowed = grammar.get("allowed", [])
57+
for t in ("cover", "chapter", "thesis", "quote", "compare", "data", "closing"):
58+
assert t in allowed, f"deck_grammar.allowed must include {t}"
59+
60+
61+
def test_skill_lists_all_allowed_data_types():
62+
_, body = _load()
63+
for t in ("cover", "chapter", "thesis", "quote", "compare", "data", "closing"):
64+
assert t in body, f"slide grammar must mention data-type={t}"
65+
66+
67+
def test_skill_lists_aurora_glass_tokens():
68+
_, body = _load()
69+
# Neon palette values must appear so the agent can copy them verbatim.
70+
for hex_value in ("#080b11", "#2dd4bf", "#38bdf8", "#e879f9", "#f6b94b"):
71+
assert hex_value in body, f"palette token {hex_value} missing"
72+
assert "JetBrains Mono" in body # mono stack
73+
# Fill-viewport frame, not a fixed card.
74+
assert "position:fixed" in body and "inset:0" in body
75+
76+
77+
def test_skill_is_self_contained_and_no_web_fonts():
78+
_, body = _load()
79+
# The deck must be self-contained; the skill must forbid web fonts.
80+
assert "self-contained" in body.lower()
81+
assert "fonts.googleapis.com" in body # mentioned as a forbidden pattern
82+
83+
84+
def test_skill_description_triggers_on_deck_requests():
85+
meta, _ = _load()
86+
desc = meta["description"].lower()
87+
assert any(
88+
word in desc
89+
for word in ("deck", "slide", "ppt", "presentation", "演示", "幻灯")
90+
)

0 commit comments

Comments
 (0)