@praxisflux/gates 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/codebase-to-course/gates/cli.mjs +20 -0
  4. package/codebase-to-course/gates/course.mjs +58 -0
  5. package/codebase-to-course/lib/README.md +8 -0
  6. package/codebase-to-course/lib/cli.mjs +23 -0
  7. package/codebase-to-course/lib/dates.mjs +11 -0
  8. package/codebase-to-course/lib/gate-runner.mjs +69 -0
  9. package/codebase-to-course/lib/handoff.mjs +75 -0
  10. package/codebase-to-course/lib/html/base.html +129 -0
  11. package/codebase-to-course/lib/installer.mjs +52 -0
  12. package/codebase-to-course/lib/lifecycle.mjs +67 -0
  13. package/codebase-to-course/lib/markdown.mjs +81 -0
  14. package/codebase-to-course/lib/project-root.mjs +52 -0
  15. package/codebase-to-course/lib/selfcontained.mjs +37 -0
  16. package/codebase-to-course/lib/spec-derive.mjs +114 -0
  17. package/codebase-to-course/lib/template.mjs +11 -0
  18. package/codebase-to-course/lib/toolkit/README.md +45 -0
  19. package/codebase-to-course/lib/toolkit/code-translation.md +50 -0
  20. package/codebase-to-course/lib/toolkit/diagrams.md +57 -0
  21. package/codebase-to-course/lib/toolkit/pedagogy.md +39 -0
  22. package/codebase-to-course/lib/toolkit/quiz-patterns.md +65 -0
  23. package/codebase-to-course/lib/toolkit/svg-diagrams.md +59 -0
  24. package/codebase-to-course/lib/toolkit/tooltip.md +83 -0
  25. package/codebase-to-course/skills/codebase-to-course/references/validate.mjs +301 -0
  26. package/grounding-wiki/gates/cli.mjs +26 -0
  27. package/grounding-wiki/gates/freshness.mjs +92 -0
  28. package/grounding-wiki/lib/README.md +8 -0
  29. package/grounding-wiki/lib/cli.mjs +23 -0
  30. package/grounding-wiki/lib/dates.mjs +11 -0
  31. package/grounding-wiki/lib/gate-runner.mjs +69 -0
  32. package/grounding-wiki/lib/handoff.mjs +75 -0
  33. package/grounding-wiki/lib/html/base.html +129 -0
  34. package/grounding-wiki/lib/installer.mjs +52 -0
  35. package/grounding-wiki/lib/lifecycle.mjs +67 -0
  36. package/grounding-wiki/lib/markdown.mjs +81 -0
  37. package/grounding-wiki/lib/project-root.mjs +52 -0
  38. package/grounding-wiki/lib/selfcontained.mjs +37 -0
  39. package/grounding-wiki/lib/spec-derive.mjs +114 -0
  40. package/grounding-wiki/lib/template.mjs +11 -0
  41. package/grounding-wiki/lib/toolkit/README.md +45 -0
  42. package/grounding-wiki/lib/toolkit/code-translation.md +50 -0
  43. package/grounding-wiki/lib/toolkit/diagrams.md +57 -0
  44. package/grounding-wiki/lib/toolkit/pedagogy.md +39 -0
  45. package/grounding-wiki/lib/toolkit/quiz-patterns.md +65 -0
  46. package/grounding-wiki/lib/toolkit/svg-diagrams.md +59 -0
  47. package/grounding-wiki/lib/toolkit/tooltip.md +83 -0
  48. package/lib/README.md +8 -0
  49. package/lib/cli.mjs +23 -0
  50. package/lib/dates.mjs +11 -0
  51. package/lib/gate-runner.mjs +69 -0
  52. package/lib/handoff.mjs +75 -0
  53. package/lib/html/base.html +129 -0
  54. package/lib/installer.mjs +52 -0
  55. package/lib/lifecycle.mjs +67 -0
  56. package/lib/markdown.mjs +81 -0
  57. package/lib/project-root.mjs +52 -0
  58. package/lib/selfcontained.mjs +37 -0
  59. package/lib/spec-derive.mjs +114 -0
  60. package/lib/template.mjs +11 -0
  61. package/lib/toolkit/README.md +45 -0
  62. package/lib/toolkit/code-translation.md +50 -0
  63. package/lib/toolkit/diagrams.md +57 -0
  64. package/lib/toolkit/pedagogy.md +39 -0
  65. package/lib/toolkit/quiz-patterns.md +65 -0
  66. package/lib/toolkit/svg-diagrams.md +59 -0
  67. package/lib/toolkit/tooltip.md +83 -0
  68. package/package.json +17 -0
  69. package/scripts/run-gates.mjs +91 -0
  70. package/spec-bridge/gates/bridge.mjs +130 -0
  71. package/spec-bridge/gates/cli.mjs +37 -0
  72. package/spec-bridge/lib/README.md +8 -0
  73. package/spec-bridge/lib/cli.mjs +23 -0
  74. package/spec-bridge/lib/dates.mjs +11 -0
  75. package/spec-bridge/lib/gate-runner.mjs +69 -0
  76. package/spec-bridge/lib/handoff.mjs +75 -0
  77. package/spec-bridge/lib/html/base.html +129 -0
  78. package/spec-bridge/lib/installer.mjs +52 -0
  79. package/spec-bridge/lib/lifecycle.mjs +67 -0
  80. package/spec-bridge/lib/markdown.mjs +81 -0
  81. package/spec-bridge/lib/project-root.mjs +52 -0
  82. package/spec-bridge/lib/selfcontained.mjs +37 -0
  83. package/spec-bridge/lib/spec-derive.mjs +114 -0
  84. package/spec-bridge/lib/template.mjs +11 -0
  85. package/spec-bridge/lib/toolkit/README.md +45 -0
  86. package/spec-bridge/lib/toolkit/code-translation.md +50 -0
  87. package/spec-bridge/lib/toolkit/diagrams.md +57 -0
  88. package/spec-bridge/lib/toolkit/pedagogy.md +39 -0
  89. package/spec-bridge/lib/toolkit/quiz-patterns.md +65 -0
  90. package/spec-bridge/lib/toolkit/svg-diagrams.md +59 -0
  91. package/spec-bridge/lib/toolkit/tooltip.md +83 -0
@@ -0,0 +1,45 @@
1
+ # lib/toolkit — shared educational content modules
2
+
3
+ Reusable teaching and visual tools shared across the praxis plugins. Where the rest of `lib/`
4
+ is shared *plumbing* (code the gates and scripts import), `toolkit/` is shared *content*:
5
+ authoring guidance and copy-paste CSS/JS snippets a skill reads while producing a deck, course,
6
+ or briefing. The full convention lives in
7
+ [`docs/skill-patterns.md`](../../docs/skill-patterns.md) ("Shared content modules — the toolkit").
8
+
9
+ The convention in brief:
10
+
11
+ - **Vendored, not linked.** `scripts/build.mjs` copies `lib/` wholesale into every packaged
12
+ plugin, so each installed plugin carries its own copy — no runtime cross-plugin dependency.
13
+ - **Referenced as `${CLAUDE_PLUGIN_ROOT}/lib/toolkit/<module>.md`** from skill prose and
14
+ templates. Gate code doesn't import from toolkit/ (it's content, not plumbing).
15
+ - **Optional, always.** A skill that uses a module must state a one-line inline fallback and
16
+ still function when the module is absent (graceful degradation).
17
+ - **Styled by the shared token schema.** Snippets use the shared CSS custom-property *names*
18
+ (see `lib/html/base.html`) so they inherit whichever palette the consuming plugin defines.
19
+
20
+ ## Modules
21
+
22
+ | Module | What it provides | Status |
23
+ |---|---|---|
24
+ | `tooltip.md` | Jargon-glossary hover/tap tooltip (overflow-safe, mouse + touch; stamped into consumers by `scripts/sync-shared.mjs`) | shipped |
25
+ | `pedagogy.md` | Visual-teaching principles: one idea per screen, show don't tell, lead with the point, split don't shrink, let visuals breathe | shipped |
26
+ | `svg-diagrams.md` | Inline-SVG authoring rules and pitfalls (`<tspan>` not `<b>`, no `var()` in SVG attrs, both-theme colors) | shipped |
27
+ | `code-translation.md` | Code ↔ plain-English translation (rules + zero-JS comments-on-top panel) | shipped |
28
+ | `quiz-patterns.md` | Quiz design (coverage rule, application-not-memory) + zero-JS reveal quiz | shipped |
29
+ | `diagrams.md` | HTML/CSS flow + annotated file-tree diagram idioms | shipped |
30
+
31
+ ## Plugin-owned versioned chrome (indexed here, lives with its owner)
32
+
33
+ Some shared visual machinery is too heavy and too domain-specific to vendor into every plugin,
34
+ but still needs one canonical, versioned home. Those modules stay in their owning plugin and are
35
+ *indexed* here so "where does shared visual machinery live" has a single answer:
36
+
37
+ | Chrome | Canonical source | Version | Convention |
38
+ |---|---|---|---|
39
+ | Course chrome (`styles.css`, `main.js`, `_footer.html`, `build.sh`, `validate.mjs`) | `codebase-to-course/skills/codebase-to-course/references/` | `chrome v2 — inline translation engine (comments-on-top)` | `docs/skill-patterns.md` "Versioned course chrome" |
40
+
41
+ The rules that keep it from fossilizing: rendering files open with the version stamp (no stamp
42
+ = v1, the retired side-by-side renderer); `validate.mjs` carries `CHROME_VERSION`, and both
43
+ `build.sh` and the course gate fail unstamped or version-mixed copies; `build.sh` refreshes the
44
+ chrome from the canonical source whenever `CLAUDE_PLUGIN_ROOT`/`C2C_REFERENCES` resolves.
45
+ Vendored course copies are build artifacts, never templates.
@@ -0,0 +1,50 @@
1
+ # Code ↔ plain-English translation
2
+
3
+ The single most valuable tool for teaching code to non-technical readers: never show code
4
+ without a plain-English translation of what each line does, and never write a paragraph
5
+ *about* code when you can annotate the code itself. Originally codebase-to-course's pattern.
6
+
7
+ **Graceful degradation:** if this module isn't available, follow the code with a short
8
+ "in plain English:" list — one bullet per line that matters.
9
+
10
+ ## The rules (medium-independent)
11
+
12
+ - **Use real code, verbatim.** Never modify, simplify, or trim snippets — the reader should
13
+ be able to open the file and see exactly what they learned from. *Choose* naturally short,
14
+ punchy snippets (5–10 lines) instead of butchering long ones.
15
+ - **No horizontal scrollbars, ever.** `white-space: pre-wrap; word-break: break-word` — this
16
+ is teaching material, not an IDE; readability beats preserved indentation.
17
+ - **One note per line that matters,** in everyday language, positioned so the pairing is
18
+ unmistakable. Skip noise lines (braces, imports) rather than annotating them emptily.
19
+ - A metaphor inside a note is welcome; jargon inside a note defeats the point (gloss it —
20
+ see `tooltip.md`).
21
+
22
+ ## Portable snippet (decks, briefings — zero JS)
23
+
24
+ Comments-on-top: each plain-English note is a styled line directly ABOVE the code it
25
+ explains, inside one token-styled panel. Inherits the consumer's palette.
26
+
27
+ ```html
28
+ <style>
29
+ .translate{background:var(--ink);color:var(--bg);border-radius:12px;padding:1em 1.2em;
30
+ font-family:var(--mono);font-size:.9em;line-height:1.6;
31
+ white-space:pre-wrap;word-break:break-word}
32
+ .translate .note{display:block;color:var(--accent2);font-family:var(--sans);
33
+ font-style:italic;filter:brightness(1.25)}
34
+ </style>
35
+
36
+ <pre class="translate"><span class="note">// Ask the server for this user's saved items…</span>
37
+ const res = await fetch(`/api/favorites/${userId}`);
38
+ <span class="note">// …and unpack the JSON body once it arrives.</span>
39
+ const favorites = await res.json();</pre>
40
+ ```
41
+
42
+ Deck usage: give the translation its own slide (shared pedagogy — one idea per slide); the
43
+ deck's 30px body type means 4–8 code lines fill a slide comfortably.
44
+
45
+ ## In codebase-to-course
46
+
47
+ Courses do NOT use this snippet — the prebuilt assets ship a far richer engine (side-by-side
48
+ panels rebuilt as comments-on-top at render time, syntax highlighting, per-block "hide notes"
49
+ toggle). Author against the `.translation-block` contract in the skill's
50
+ `interactive-elements.md`; this module is the portable variant for every other surface.
@@ -0,0 +1,57 @@
1
+ # HTML/CSS diagram idioms
2
+
3
+ Diagrams that are plain HTML + CSS (boxes, arrows, trees — often with an emoji as the icon)
4
+ inherit the theme tokens, wrap responsively, and never hit SVG's text pitfalls. This is
5
+ codebase-to-course's native diagram idiom, portable to any praxis surface. For hand-drawn
6
+ SVG figures instead, see `svg-diagrams.md`.
7
+
8
+ **Graceful degradation:** if this module isn't available, a numbered list of steps or an
9
+ indented mono file listing does the job.
10
+
11
+ ## Flow of steps (boxes with arrow separators)
12
+
13
+ ```html
14
+ <style>
15
+ .flow{display:flex;align-items:stretch;gap:0;flex-wrap:wrap}
16
+ .flow .step{flex:1;min-width:9em;background:var(--card);border:1px solid var(--line);
17
+ border-radius:12px;padding:.8em 1em;text-align:center}
18
+ .flow .step b{display:block;margin-bottom:.25em}
19
+ .flow .arrow{align-self:center;padding:0 .5em;color:var(--muted);font-size:1.4em}
20
+ </style>
21
+
22
+ <div class="flow">
23
+ <div class="step">📨 <b>Input</b><span class="muted">the click arrives</span></div>
24
+ <div class="arrow">→</div>
25
+ <div class="step">⚙️ <b>Process</b><span class="muted">the server decides</span></div>
26
+ <div class="arrow">→</div>
27
+ <div class="step">📬 <b>Output</b><span class="muted">the page updates</span></div>
28
+ </div>
29
+ ```
30
+
31
+ ## Annotated file tree
32
+
33
+ Use instead of paragraphs saying "this folder does X, that folder does Y":
34
+
35
+ ```html
36
+ <style>
37
+ .tree{font-family:var(--mono);background:var(--chip);border:1px solid var(--line);
38
+ border-radius:12px;padding:1em 1.2em;line-height:1.8;white-space:pre}
39
+ .tree .why{font-family:var(--sans);color:var(--muted);font-style:italic}
40
+ </style>
41
+
42
+ <div class="tree">src/
43
+ ├─ api/ <span class="why">← every route the frontend can call</span>
44
+ ├─ models/ <span class="why">← what the data looks like</span>
45
+ └─ jobs/ <span class="why">← work that runs on a schedule</span></div>
46
+ ```
47
+
48
+ Rules: label every box/branch with WHY it matters, not just its name; keep a flow to 3–5
49
+ steps (split longer chains — shared pedagogy: one idea per screen); emoji are icons, pick
50
+ one each, don't decorate.
51
+
52
+ ## In codebase-to-course
53
+
54
+ Courses get richer, *animated* versions from the prebuilt assets (flow animations, group-chat
55
+ visualizations, interactive architecture diagrams) — author against the contracts in the
56
+ skill's `interactive-elements.md`. These static idioms are the portable variants for decks
57
+ and briefings.
@@ -0,0 +1,39 @@
1
+ # Visual-teaching principles
2
+
3
+ The shared pedagogy behind every praxis teaching surface — codebase-to-course's course screens,
4
+ educate's deck slides, research's briefing pages. Written once here; each skill keeps only its
5
+ medium-specific rules (and a one-line distillation as fallback) and points at this module.
6
+
7
+ **Graceful degradation:** if this module isn't available, the one-liner to keep: *one idea per
8
+ screen; show, don't tell; lead with the point; split, don't shrink.*
9
+
10
+ ## The principles
11
+
12
+ 1. **One idea per screen/slide.** Each unit of display teaches exactly one thing. If it feels
13
+ crammed, the fix is always MORE screens/slides, never smaller type or denser text. A
14
+ 20-slide deck that's effortless beats a 6-slide deck you squint at.
15
+
16
+ 2. **Show, don't tell.** Eyes glaze over text blocks. Prefer a diagram, card row, flow, code
17
+ translation, or animation to a paragraph — if you're writing a fourth sentence, stop and
18
+ convert it into a visual. Aim closer to an infographic than a textbook.
19
+
20
+ 3. **Lead with the point.** State the claim/verdict/takeaway first, then justify it. A briefing
21
+ leads with the analysis's verdict; a deck leads with the lesson's claim; a course screen
22
+ leads with the concept, not the buildup. Don't make the reader reverse-engineer the answer.
23
+
24
+ 4. **Split, don't shrink.** Never reduce type, tighten spacing, or trim whitespace to make
25
+ content fit. Cut words, or add another screen/slide. Readability is the budget everything
26
+ else pays from.
27
+
28
+ 5. **Let visuals breathe.** A figure that matters gets the room it needs — its own slide, a
29
+ full-width screen block, generous whitespace around it. Alternate big visuals with narrow
30
+ text to create rhythm; don't wedge a diagram into a corner.
31
+
32
+ ## How each medium applies them
33
+
34
+ - **Course screens** (codebase-to-course): every screen ≥50% visual, max 2–3 sentences per text
35
+ block, one "hero visual" per module — see the skill's `content-philosophy.md`.
36
+ - **Deck slides** (educate): large type is non-negotiable; verify nothing clips at 16:9 — see
37
+ the deck template's header rules.
38
+ - **Briefing pages** (research): verdict first, every figure traces to the analysis — see
39
+ `artifact-layer.md`.
@@ -0,0 +1,65 @@
1
+ # Quiz patterns — test application, not memory
2
+
3
+ A quiz exists to check whether the learner can *use* what they learned on a new problem, not
4
+ whether they can regurgitate a definition. Originally codebase-to-course's quiz doctrine;
5
+ the design rules are medium-independent.
6
+
7
+ **Graceful degradation:** if this module isn't available, end the lesson/module with 2–3
8
+ "what would you do?" questions in plain prose and discuss the answers.
9
+
10
+ ## The coverage rule (hard requirement)
11
+
12
+ A question may only use terms, concepts, components, and scenarios the material has already
13
+ introduced *before* the quiz. Applying knowledge to a brand-new situation is the whole point —
14
+ but every term the question leans on must be 100% guaranteed already taught. A learner should
15
+ never hit a quiz and think "we never talked about this." If a great question needs an
16
+ unintroduced concept, teach it first or cut the question.
17
+
18
+ ## What to quiz (in order of value)
19
+
20
+ 1. **"What would you do?" scenarios** — a new situation, apply what you learned. The gold standard.
21
+ 2. **Debugging scenarios** — "X is broken; where would you look first?"
22
+ 3. **Architecture decisions** — "would you put this logic here or there — and why?"
23
+ 4. **Tracing exercises** — "when a user does X, what path does the data take?"
24
+
25
+ **Never quiz:** definitions (that's what glossing is for), file-name recall, syntax details,
26
+ or anything answerable by scrolling up and copying.
27
+
28
+ ## Tone
29
+
30
+ Wrong answers get encouraging explanations that teach something new ("Not quite — here's
31
+ why…"); right answers get brief reinforcement of the principle ("Exactly — this works
32
+ because…"). Never punitive, never score-focused.
33
+
34
+ ## Portable snippet (decks, briefings — zero JS)
35
+
36
+ `<details>`/`<summary>` gives tap-to-reveal answers with no script, keyboard-accessible,
37
+ token-styled:
38
+
39
+ ```html
40
+ <style>
41
+ .quiz details{border:1px solid var(--line);border-radius:10px;margin:.4em 0;
42
+ background:var(--chip)}
43
+ .quiz summary{cursor:pointer;padding:.55em .9em;font-weight:600}
44
+ .quiz details p{margin:0;padding:.2em .9em .7em;color:var(--muted)}
45
+ .quiz details.right summary{border-left:4px solid var(--accent2)}
46
+ </style>
47
+
48
+ <div class="quiz">
49
+ <p><b>You want to add a "save to favorites" feature. Where does the logic live?</b></p>
50
+ <details><summary>A. In the browser extension</summary>
51
+ <p>Not quite — the extension only displays data; it never owns it. Look one hop back.</p></details>
52
+ <details class="right"><summary>B. In the backend API</summary>
53
+ <p>Exactly — the API owns the data, so a new feature that stores state starts there.</p></details>
54
+ </div>
55
+ ```
56
+
57
+ Deck usage: one question per slide, reveal by tapping — a natural end-of-lesson slide before
58
+ the takeaways.
59
+
60
+ ## In codebase-to-course
61
+
62
+ Courses do NOT use this snippet — the prebuilt assets ship graded quiz engines
63
+ (multiple-choice, drag-and-drop, spot-the-bug, scenario) with per-answer explanations; author
64
+ against the contracts in the skill's `interactive-elements.md`. The coverage rule above
65
+ applies everywhere.
@@ -0,0 +1,59 @@
1
+ # Inline-SVG diagram rules
2
+
3
+ Hand-drawn inline SVG is the praxis idiom for explanatory figures (educate decks draw
4
+ diagrams this way; research briefings draw charts as inline SVG). SVG has sharp edges that
5
+ HTML habits walk straight into — these are the hard-won rules, originally from educate's
6
+ deck template.
7
+
8
+ **Graceful degradation:** if this module isn't available, the two rules to keep: *no
9
+ `<b>`/`<i>` inside SVG `<text>` (use `<tspan>`), and CSS `var(--…)` does not resolve in SVG
10
+ presentation attributes — use literal values.*
11
+
12
+ ## The pitfalls (hard rules)
13
+
14
+ 1. **Never put `<b>` or `<i>` inside an SVG `<text>` element.** HTML formatting tags are
15
+ invisible-but-broken inside SVG — the text silently fails to render or renders unstyled.
16
+ Use `<tspan font-weight="700">` / `<tspan font-style="italic">` instead.
17
+
18
+ 2. **CSS `var(--token)` does NOT resolve in SVG presentation attributes** (`fill="var(--accent)"`
19
+ silently paints black). Use **literal color values** in SVG attributes — and since literals
20
+ can't follow the theme, pick colors that read on BOTH light and dark backgrounds, or give
21
+ the figure its own contrasting panel. (`var()` does work in a `style=""` attribute or CSS
22
+ rule targeting SVG elements — but only for properties CSS controls, and it's easy to get
23
+ this half-right; literals are the predictable path.)
24
+
25
+ 3. **Size for reflow:** `width:100%; height:auto` on the `<svg>` plus a `viewBox` — never fixed
26
+ pixel width/height attributes alone, or the figure won't scale with its container.
27
+
28
+ 4. **Accessibility:** `role="img"` and an `aria-label` describing what the diagram shows.
29
+
30
+ 5. **Give the figure room** (shared pedagogy — "let visuals breathe"): a diagram that carries
31
+ the point gets its own slide / a full-width block, filling most of the available width and
32
+ height. Don't wedge it beside a paragraph.
33
+
34
+ ## Skeleton — a boxes-and-arrows flow
35
+
36
+ ```html
37
+ <svg viewBox="0 0 800 300" role="img" aria-label="input flows through process to output">
38
+ <rect x="40" y="110" width="200" height="80" rx="12" fill="#eef0ff" stroke="#4f46e5" stroke-width="2"/>
39
+ <text x="140" y="158" text-anchor="middle" font-size="26" fill="#1b2733" font-family="sans-serif">Input</text>
40
+ <line x1="240" y1="150" x2="320" y2="150" stroke="#5a6776" stroke-width="3" marker-end="url(#a)"/>
41
+ <rect x="320" y="110" width="200" height="80" rx="12" fill="#eefaf7" stroke="#0e9488" stroke-width="2"/>
42
+ <text x="420" y="158" text-anchor="middle" font-size="26" fill="#1b2733" font-family="sans-serif">Process</text>
43
+ <defs><marker id="a" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
44
+ <path d="M0,0 L8,3 L0,6 Z" fill="#5a6776"/></marker></defs>
45
+ </svg>
46
+ ```
47
+
48
+ Notes: pastel fills + saturated strokes read on both themes; the `<marker>` def gives every
49
+ arrow a clean head — define it once per SVG and reference with `marker-end="url(#a)"`; keep
50
+ marker `id`s unique per page if a page holds several SVGs.
51
+
52
+ ## Who uses this
53
+
54
+ - **educate decks** — the native idiom; the deck template's header carries the one-line
55
+ distillation and a sample diagram slide.
56
+ - **research briefings** — charts are inline SVG; these rules apply on top of the `dataviz`
57
+ skill's chart guidance.
58
+ - **codebase-to-course** — courses prefer the HTML/CSS + emoji diagram patterns in the skill's
59
+ `interactive-elements.md`; when a course page does hand-draw inline SVG, these rules apply.
@@ -0,0 +1,83 @@
1
+ # Jargon glossary tooltip
2
+
3
+ Gloss every technical term where it appears, so the reader never has to leave the page (or
4
+ ask) to decode jargon. The definition rides a `data-tip` attribute; a popover shows it on
5
+ hover (desktop) or tap (touch). This is the single most valuable accessibility feature for
6
+ non-technical audiences — codebase-to-course's content rules call it "No Term Left Behind."
7
+
8
+ **Graceful degradation:** if this module isn't available, gloss terms in parentheses on
9
+ first use — `the manifest (the file that declares what the extension may do)` — and move on.
10
+
11
+ ## The contract
12
+
13
+ Any element — HTML *or* SVG — may carry a `data-tip` attribute with a 1–2 sentence,
14
+ everyday-language definition:
15
+
16
+ ```html
17
+ <span data-tip="A service worker is a background script that runs independently of the
18
+ page — a behind-the-scenes assistant that's on even when you're not looking.">service
19
+ worker</span>
20
+ ```
21
+
22
+ - Give the reader a **visible affordance** so glossed terms are discoverable: a dashed
23
+ underline, or a small `?` badge (educate's decks use the badge — see the deck template's
24
+ sample slide).
25
+ - Keep definitions to 1–2 sentences, everyday language; a metaphor helps.
26
+ - Gloss on **first use per screen/slide**, not every repetition.
27
+
28
+ ## The snippet
29
+
30
+ Self-contained, and written against the **shared token names** (`--ink`, `--bg`) so it
31
+ inherits the consuming plugin's palette in both light and dark themes (the bubble is
32
+ inverted-contrast: dark-on-light pages get a dark bubble and vice versa). Popover text size
33
+ follows `--tip-size` (default `1rem`) — decks set it larger. The popover is appended to
34
+ `document.body` with `position: fixed`, so it is **never clipped** by an ancestor's
35
+ `overflow: hidden`, and it hides when printing.
36
+
37
+ Consumers embed the marked regions verbatim; `scripts/sync-shared.mjs` re-stamps them (drift
38
+ fails the test suite). Hand-edit only here.
39
+
40
+ ```css
41
+ /* praxis:tooltip-css:start */
42
+ [data-tip]{cursor:help}
43
+ .tip-pop{position:fixed;z-index:1000;max-width:min(420px,80vw);background:var(--ink);color:var(--bg);
44
+ font-size:var(--tip-size,1rem);line-height:1.4;padding:.65em .9em;border-radius:10px;
45
+ pointer-events:none;box-shadow:0 8px 24px rgba(0,0,0,.28);opacity:0;transform:translateY(4px);
46
+ transition:opacity .12s ease,transform .12s ease}
47
+ .tip-pop.show{opacity:1;transform:translateY(0)}
48
+ @media print{ .tip-pop{display:none} }
49
+ /* praxis:tooltip-css:end */
50
+ ```
51
+
52
+ ```js
53
+ // praxis:tooltip-js:start
54
+ // One shared popover: follows the cursor on hover, toggles on click/tap (touch), clamps to
55
+ // the viewport, and closes on tap-away. Works for HTML and SVG [data-tip] elements alike.
56
+ (function(){
57
+ const pop=document.createElement('div'); pop.className='tip-pop'; document.body.appendChild(pop);
58
+ let shownFor=null;
59
+ const place=(x,y)=>{ const pad=14, r=pop.getBoundingClientRect();
60
+ let px=x+pad, py=y+pad;
61
+ if(px+r.width>innerWidth-8) px=Math.max(8,x-r.width-pad);
62
+ if(py+r.height>innerHeight-8) py=Math.max(8,y-r.height-pad);
63
+ pop.style.left=px+'px'; pop.style.top=py+'px'; };
64
+ const show=(t,x,y)=>{ pop.textContent=t.getAttribute('data-tip'); pop.classList.add('show'); shownFor=t; place(x,y); };
65
+ const hide=()=>{ pop.classList.remove('show'); shownFor=null; };
66
+ document.addEventListener('mouseover',e=>{ const t=e.target.closest&&e.target.closest('[data-tip]'); if(t) show(t,e.clientX,e.clientY); });
67
+ document.addEventListener('mousemove',e=>{ if(shownFor) place(e.clientX,e.clientY); });
68
+ document.addEventListener('mouseout',e=>{ const t=e.target.closest&&e.target.closest('[data-tip]'); if(t && !t.contains(e.relatedTarget)) hide(); });
69
+ document.addEventListener('click',e=>{ const t=e.target.closest&&e.target.closest('[data-tip]');
70
+ if(t){ e.stopPropagation(); (shownFor===t)?hide():show(t,e.clientX,e.clientY); } else hide(); });
71
+ })();
72
+ // praxis:tooltip-js:end
73
+ ```
74
+
75
+ ## Where each plugin gets it
76
+
77
+ - **educate decks** — the deck template carries the stamped regions; nothing to author.
78
+ - **research briefings** — copy both regions into the page (alongside the base.html blocks).
79
+ - **codebase-to-course pages** — courses do NOT use this snippet: the prebuilt assets
80
+ (`references/styles.css` + `references/main.js`, copied verbatim into every course) ship
81
+ the same pattern as a course-native engine with the `.term` + `data-definition` contract
82
+ and a dashed-underline affordance. Author course HTML against that contract and never
83
+ inline tooltip CSS/JS there (see the skill's `interactive-elements.md`).
package/lib/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # lib — the shared Node chassis
2
+
3
+ Zero-dependency Node modules shared by every plugin, **vendored into each plugin at build time**
4
+ (a shipped `.plugin` is self-contained; `${CLAUDE_PLUGIN_ROOT}/lib/…` resolves at runtime).
5
+
6
+ Planned modules (**TASK-1.2**): `project-root` · `gate-runner` (Stop-hook harness) · `markdown`
7
+ · `selfcontained` (HTML verifier) · `lifecycle` (status-cannot-exceed-proven-artifacts) ·
8
+ `installer` · `dates` · `template`.
package/lib/cli.mjs ADDED
@@ -0,0 +1,23 @@
1
+ // cli.mjs — the run-as-CLI guard for dual-use modules (importable library + executable
2
+ // script).
3
+ //
4
+ // The naive `import.meta.url === `file://${process.argv[1]}`` comparison breaks through
5
+ // symlinks: Node resolves import.meta.url to the module's physical path, while argv[1] stays
6
+ // as typed, so a script invoked via a symlinked checkout (~/projects -> Claude/Code) compares
7
+ // unequal and silently runs none of its CLI body — for a gate runner that means exit 0 having
8
+ // checked nothing, the exact silent skip the gates doctrine forbids. Realpath both sides so
9
+ // the comparison is invocation-path-independent.
10
+ import { realpathSync } from "node:fs";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ /** True when the module at `moduleUrl` (pass import.meta.url) is the script Node was asked
14
+ * to run, regardless of symlinks on either side. False when imported as a module, or when
15
+ * there is no entry script (`node -e`, REPL) or argv[1] is a runner's virtual entry. */
16
+ export function runAsCli(moduleUrl) {
17
+ if (!process.argv[1]) return false;
18
+ try {
19
+ return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(process.argv[1]);
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
package/lib/dates.mjs ADDED
@@ -0,0 +1,11 @@
1
+ // dates.mjs — ISO date helpers shared across the suite.
2
+
3
+ /** Today's date (or `date`'s) as an ISO `YYYY-MM-DD` string. */
4
+ export function today(date = new Date()) {
5
+ return date.toISOString().slice(0, 10);
6
+ }
7
+
8
+ /** Rewrite an `updated:` frontmatter line to today's date. No-op if none present. */
9
+ export function bumpUpdated(text, date = new Date()) {
10
+ return (text || "").replace(/^(updated:\s*).*$/m, `$1${today(date)}`);
11
+ }
@@ -0,0 +1,69 @@
1
+ // gate-runner.mjs — the shared Stop-hook harness.
2
+ //
3
+ // Each plugin ships a Stop hook whose command is a one-line Node entry that calls runStopHook
4
+ // with that plugin's gates. When several plugins are installed, each fires its own hook — so
5
+ // "run every applicable gate" happens naturally across plugins; within one plugin the runner
6
+ // runs that plugin's gates additively over every root they resolve.
7
+ //
8
+ // A gate is: { name, resolveRoots(startDir) -> string[], check(root) -> string[] (problems),
9
+ // warn?(root) -> string[] (non-blocking notices) }.
10
+ // A gate that resolves no roots is a no-op (this isn't its kind of project). `check` problems
11
+ // block the stop (exit 2); optional `warn` notices are surfaced on stderr but never block (exit 0)
12
+ // — for freshness reminders and the like that shouldn't refuse to let the model finish.
13
+ //
14
+ // Contract (Claude Code Stop hook): stdin is JSON with { stop_hook_active, cwd, … };
15
+ // exit 0 = allow the model to stop; exit 2 = block, and stderr becomes the message it sees.
16
+
17
+ /** Read all of stdin as a string (empty string on a TTY, so it's safe to run by hand). */
18
+ export function readStdin() {
19
+ return new Promise((resolve) => {
20
+ if (process.stdin.isTTY) return resolve("");
21
+ let data = "";
22
+ process.stdin.setEncoding("utf8");
23
+ process.stdin.on("data", (c) => (data += c));
24
+ process.stdin.on("end", () => resolve(data));
25
+ });
26
+ }
27
+
28
+ /**
29
+ * Pure core: given the parsed hook input and the gates, return { block, message }.
30
+ * Separated from process I/O so it is unit-testable.
31
+ */
32
+ export function evaluate(input, gates, { cwd = process.cwd() } = {}) {
33
+ if (input && input.stop_hook_active === true) return { block: false, message: "", warnings: "" };
34
+ const start = process.env.CLAUDE_PROJECT_DIR || (input && input.cwd) || cwd;
35
+ const problems = [];
36
+ const warnings = [];
37
+ for (const gate of gates) {
38
+ let roots = [];
39
+ try { roots = gate.resolveRoots(start) || []; } catch { roots = []; }
40
+ for (const root of roots) {
41
+ try { problems.push(...(gate.check(root) || [])); } catch (e) {
42
+ problems.push(`[${gate.name || "gate"}] crashed on ${root}: ${e.message}`);
43
+ }
44
+ if (typeof gate.warn === "function") {
45
+ try { warnings.push(...(gate.warn(root) || [])); } catch { /* warnings are best-effort */ }
46
+ }
47
+ }
48
+ }
49
+ return { block: problems.length > 0, message: problems.join("\n"), warnings: warnings.join("\n") };
50
+ }
51
+
52
+ /**
53
+ * Full harness: read stdin, evaluate the gates, exit 0 (allow) or 2 (block, message on stderr).
54
+ * Blocking problems win; otherwise any non-blocking warnings are written to stderr and we still
55
+ * allow the stop (exit 0).
56
+ */
57
+ export async function runStopHook({ gates, exit = process.exit } = {}) {
58
+ const raw = await readStdin();
59
+ let input = {};
60
+ try { input = JSON.parse(raw || "{}"); } catch { input = {}; }
61
+ const { block, message, warnings } = evaluate(input, gates);
62
+ if (block) {
63
+ process.stderr.write([message, warnings].filter(Boolean).join("\n") + "\n");
64
+ exit(2);
65
+ } else {
66
+ if (warnings) process.stderr.write(warnings + "\n");
67
+ exit(0);
68
+ }
69
+ }
@@ -0,0 +1,75 @@
1
+ // handoff.mjs — the shared inter-plugin handoff TRANSPORT (envelope + lifecycle, not semantics).
2
+ //
3
+ // One plugin hands work to another through files, but those messages are transient plumbing —
4
+ // they must NOT clutter git. So payloads live in a gitignored `.handoff/` at the project root;
5
+ // the durable EVIDENCE that a handoff happened is the consumer's job to record in its own tracked
6
+ // state (e.g. progress.json). The chassis moves opaque payloads and never touches a plugin's ledger.
7
+ //
8
+ // The payload SCHEMA is per plugin pair (educate→build sends a SPEC; build→educate returns
9
+ // findings). This module only knows the envelope: id, kind (request|response), from, to, ref.
10
+
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, renameSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { ensureGitignore } from "./installer.mjs";
14
+ import { parseFrontmatter } from "./markdown.mjs";
15
+
16
+ export const HANDOFF_DIR = ".handoff";
17
+
18
+ export function handoffDir(root) { return join(root, HANDOFF_DIR); }
19
+
20
+ /** Ensure `.handoff/` exists and is gitignored (so it never shows in `git status`). */
21
+ export function ensureHandoffDir(root) {
22
+ const dir = handoffDir(root);
23
+ mkdirSync(dir, { recursive: true });
24
+ ensureGitignore(root, HANDOFF_DIR + "/");
25
+ return dir;
26
+ }
27
+
28
+ /**
29
+ * Write a handoff. Envelope fields are frontmatter; `body` is the opaque, plugin-defined payload.
30
+ * kind: "request" (e.g. a SPEC) | "response" (e.g. findings)
31
+ * ref: correlation id tying a response back to its request
32
+ * Returns the file path.
33
+ */
34
+ export function writeHandoff(root, { id, kind, from, to, ref = "", title = "", body = "" }) {
35
+ if (!id || !kind || !from || !to) throw new Error("handoff requires id, kind, from, to");
36
+ const dir = ensureHandoffDir(root);
37
+ const fm = [`id: ${id}`, `kind: ${kind}`, `from: ${from}`, `to: ${to}`, `ref: ${ref}`, `title: ${title}`].join("\n");
38
+ const path = join(dir, `${id}.md`);
39
+ writeFileSync(path, `---\n${fm}\n---\n${body}\n`);
40
+ return path;
41
+ }
42
+
43
+ /** Read a handoff → { envelope, body, path } or null if absent. */
44
+ export function readHandoff(root, id) {
45
+ const path = join(handoffDir(root), `${id}.md`);
46
+ if (!existsSync(path)) return null;
47
+ const text = readFileSync(path, "utf8");
48
+ const envelope = parseFrontmatter(text) || {};
49
+ const body = text.replace(/^---[\s\S]*?\n---\s*\n?/, "");
50
+ return { envelope, body, path };
51
+ }
52
+
53
+ /** List pending (unconsumed) handoffs, optionally filtered by envelope fields, e.g. {to, kind, ref}. */
54
+ export function listHandoffs(root, filter = {}) {
55
+ const dir = handoffDir(root);
56
+ if (!existsSync(dir)) return [];
57
+ return readdirSync(dir)
58
+ .filter((f) => f.endsWith(".md"))
59
+ .map((f) => readHandoff(root, f.replace(/\.md$/, "")))
60
+ .filter((h) => h && Object.entries(filter).every(([k, v]) => h.envelope[k] === v));
61
+ }
62
+
63
+ /**
64
+ * Consume a handoff: move it into `.handoff/consumed/` so it stops appearing as pending. The
65
+ * durable record ("this handoff happened, here's what it changed") is the CONSUMER's job to write
66
+ * into its own tracked state — the chassis never edits a plugin's ledger. Returns false if absent.
67
+ */
68
+ export function markConsumed(root, id) {
69
+ const src = join(handoffDir(root), `${id}.md`);
70
+ if (!existsSync(src)) return false;
71
+ const cdir = join(handoffDir(root), "consumed");
72
+ mkdirSync(cdir, { recursive: true });
73
+ renameSync(src, join(cdir, `${id}.md`));
74
+ return true;
75
+ }