From d49ab898db223105b6e07df5b5b2c1bdafb33755 Mon Sep 17 00:00:00 2001 From: John Brecht Date: Mon, 15 Jun 2026 16:33:57 -0700 Subject: [PATCH] fix(core): broaden recap-framing lint to accept accomplishment recaps (#49) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The strict-mode lint that nudges the last step to close with a recap (#36) only matched a fixed cue set ("to recap", "you're ready", …), so a valid past-tense accomplishment recap false-warned. Widen RECAP_CUE_RE to also match "you created/set up/added/…" accomplishment phrasing and the "from here you…" hand-off, bringing it to parity with the much looser INTRO_CUE_RE. All existing cues are kept; advisory-only, never fails a render. Tested with the real umami recap that slipped through. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 4 ++++ packages/core/src/spec.ts | 8 +++++++- packages/core/test/spec.test.ts | 15 +++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76987a6..34d023e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ Everything below is opt-in. Notes for existing consumers: - `StepError` messages are richer (multi-line, with artifact paths). If you parsed them, prefer the new structured `error.artifacts` field. - The timing manifest gained optional fields (`lang`, `capture`). Old kept work dirs still post-process fine. +## Unreleased + +- **Broader recap-framing lint (#49).** The strict-mode lint that nudges the last step to close with a recap (#36) no longer false-warns on valid past-tense / accomplishment recaps (e.g. "you created an event, set up ticketing… from here you can…"). `RECAP_CUE_RE` now also matches accomplishment phrasing and the "from here you…" hand-off, bringing it to parity with the looser objective cue. Advisory-only — it never fails a render. + ## 0.12.0 — chapters that work on YouTube & Vimeo - **Chapters now activate on YouTube & Vimeo (#52).** The emitted chapter artifacts respect each platform's activation rules so they don't silently fail on upload. The YouTube `.chapters.txt` list folds any sub-10s chapter into its neighbor — YouTube ignores the *entire* description chapter list if a single chapter is under 10s. Chapter titles are now capped at **50 characters** (Vimeo's limit, the strictest common target) instead of 60 — a behavior change to the default title cap (`ChapterOptions.maxTitleChars` / `deriveChapterTitle`), so titles of 51–60 chars that previously survived are now truncated. The MP4 chapter track and the `.chapters.vtt` (Vimeo/web) keep the full per-step list. New exports: `enforceMinChapterDuration`, `YOUTUBE_MIN_CHAPTER_MS`. Docs gained a "Getting chapters onto YouTube and Vimeo" section. No breaking signature changes. diff --git a/packages/core/src/spec.ts b/packages/core/src/spec.ts index 6b681e7..a1ca700 100644 --- a/packages/core/src/spec.ts +++ b/packages/core/src/spec.ts @@ -17,7 +17,13 @@ const INSTRUMENTED_ACTION_RE = /\.(click|dblclick|hover|fill|check|uncheck|selec // Loose cues that a line opens with an objective / closes with a recap. Used // only for the heuristic strict-mode framing lints, so false matches are cheap. const INTRO_CUE_RE = /\b(in this|we['’]ll|we will|you['’]ll|you will|let['’]s|let us|this (?:tour|guide|tutorial|walkthrough|video)|by the end)\b/i; -const RECAP_CUE_RE = /\b(that['’]s all|that is all|you['’]re (?:ready|done|all set)|you are (?:ready|done|all set)|in summary|to recap|to sum up|now you (?:can|know)|you['’]ve (?:now|just)|you have (?:now|just))\b/i; +// Kept comparably forgiving to INTRO_CUE_RE so the objective/recap framing lints +// nag symmetrically (#49): besides the explicit "to recap" cues, also accept +// past-tense accomplishment phrasing ("you created/set up/added …") and the +// "from here you …" hand-off connector, which read as recaps but matched none +// of the fixed cues before. +const RECAP_CUE_RE = + /\b(that['’]s all|that is all|you['’]re (?:ready|done|all set)|you are (?:ready|done|all set)|in summary|to recap|to sum up|now you (?:can|know)|you['’]ve (?:now|just)|you have (?:now|just)|from here[,]? you|you (?:['’]ve |have )?(?:created|added|built|set up|set|configured|enabled|published|connected|installed|learned|completed|finished|made|brought|customized|deployed|removed|updated|changed|wired up|saw|seen))\b/i; export function step(narration: string, run: Step['run'], opts?: Partial>): Step { return { narration, run, ...opts }; diff --git a/packages/core/test/spec.test.ts b/packages/core/test/spec.test.ts index f34814d..c9f4391 100644 --- a/packages/core/test/spec.test.ts +++ b/packages/core/test/spec.test.ts @@ -205,6 +205,21 @@ describe('narration lints', () => { const msgs = warn.mock.calls.map((c) => c[0] as string).join('\n'); expect(msgs).not.toMatch(/objective|recap/); }); + + it('accepts a past-tense accomplishment recap without an explicit cue (#49)', () => { + const warn = warns(); + tutorial('x', [ + step("In this guide we'll set up an event.", noop), + // Real umami recap that slipped through the old fixed cue set. + step( + 'That is the core loop. You created an event, set up ticketing, and added a banner. ' + + 'From here you can invite attendees and send broadcasts.', + noop, + ), + ], { lint: { strict: true } }); + const msgs = warn.mock.calls.map((c) => c[0] as string).join('\n'); + expect(msgs).not.toMatch(/recap/); + }); }); });