mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-18 07:40:09 +02:00
fix(catalog): preserve routing prose when first sentence exceeds 200 chars
splitCatalogDescription truncated the lead BEFORE computing routing extraction, which meant skills whose first sentence was over 200 chars (design-consultation: 207 chars) had their entire routing prose silently dropped — the "## When to invoke" body section came out empty. Root cause: routing was extracted via `collapsed.indexOf(lead)` after lead was suffixed with "...". The "..." never appeared in the original string, so indexOf returned -1 and routingProse fell back to empty. Fix: compute routing from sentenceLead (the untruncated first sentence) BEFORE truncating the displayed lead. The displayed lead still gets "..." when over 200 chars, but the routing extraction uses the real boundary. Also: refresh golden snapshots for claude/codex/factory ship and update two unit tests that asserted v1.44 behavior: - skill-validation.test.ts: trigger-phrase + proactive-routing tests now search whole content, not just frontmatter (T4 moved them to a body "## When to invoke" section) - writing-style-resolver.test.ts: jargon-list assertion now expects the T3 reference pointer, not the inline list Test plan: - bun test test/skill-validation.test.ts test/writing-style-resolver.test.ts test/host-config.test.ts test/skill-size-budget.test.ts test/parity-suite.test.ts test/skill-coverage-matrix.test.ts test/skill-coverage-floor.test.ts test/cso-preserved.test.ts test/resolver-entry.test.ts test/helpers/capture-parity-baseline.test.ts test/gen-skill-docs.test.ts: 1134 pass, 0 fail - Manual verify: design-consultation/SKILL.md "## When to invoke this skill" body section now contains "Use when asked to..." + "Proactively suggest..." Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+20
-11
@@ -245,29 +245,38 @@ export function splitCatalogDescription(description: string): CatalogParts {
|
||||
// First normalize to single-line for sentence detection, then back out.
|
||||
const collapsed = working.replace(/\s+/g, ' ').trim();
|
||||
const sentenceMatch = collapsed.match(/^([^.!?]*[.!?])(?:\s|$)/);
|
||||
let lead = sentenceMatch ? sentenceMatch[1].trim() : collapsed.split(/\s/).slice(0, 20).join(' ');
|
||||
// sentenceLead is the FULL first sentence (no truncation). We compute routing
|
||||
// from this position, then optionally truncate the displayed lead afterwards.
|
||||
// Truncating first then computing routing was the v1.45.0.0 bug — when the
|
||||
// first sentence exceeded 200 chars, the routing extraction would lose the
|
||||
// entire tail of the description (design-consultation's "Use when..."
|
||||
// routing prose silently dropped).
|
||||
const sentenceLead = sentenceMatch ? sentenceMatch[1].trim() : collapsed.split(/\s/).slice(0, 20).join(' ');
|
||||
|
||||
// If the lead would be too long, trim to the first 140 chars at a word boundary
|
||||
// Routing prose: everything AFTER the first sentence boundary in the collapsed view.
|
||||
const leadInCollapsed = collapsed.indexOf(sentenceLead);
|
||||
const routingCollapsed = leadInCollapsed >= 0
|
||||
? collapsed.slice(leadInCollapsed + sentenceLead.length).trim()
|
||||
: '';
|
||||
|
||||
// Now produce the displayed lead — truncated if too long. The original
|
||||
// sentenceLead is preserved for routing extraction below.
|
||||
let lead = sentenceLead;
|
||||
if (lead.length > 200) {
|
||||
const trunc = lead.slice(0, 197);
|
||||
const lastSpace = trunc.lastIndexOf(' ');
|
||||
lead = (lastSpace > 60 ? trunc.slice(0, lastSpace) : trunc) + '...';
|
||||
}
|
||||
|
||||
const leadInCollapsed = collapsed.indexOf(lead);
|
||||
const routingCollapsed = leadInCollapsed >= 0
|
||||
? collapsed.slice(leadInCollapsed + lead.length).trim()
|
||||
: '';
|
||||
// Restore line breaks for routing prose by mapping back to original layout.
|
||||
// Use original whitespace structure where possible; fall back to collapsed.
|
||||
// Anchor recovery on sentenceLead (the untruncated first sentence) — not
|
||||
// `lead` (which may have a "..." suffix and won't substring-match `working`).
|
||||
let routingProse = routingCollapsed;
|
||||
// Try to recover the multi-line layout: split working at the lead boundary.
|
||||
const collapsedLeadIdx = working.replace(/\s+/g, ' ').indexOf(lead);
|
||||
const collapsedLeadIdx = working.replace(/\s+/g, ' ').indexOf(sentenceLead);
|
||||
if (collapsedLeadIdx >= 0) {
|
||||
// Walk the original working string until we've consumed lead.length collapsed chars
|
||||
let consumed = 0;
|
||||
let cut = 0;
|
||||
for (let i = 0; i < working.length && consumed < collapsedLeadIdx + lead.length; i++) {
|
||||
for (let i = 0; i < working.length && consumed < collapsedLeadIdx + sentenceLead.length; i++) {
|
||||
if (/\s/.test(working[i])) {
|
||||
if (i === 0 || /\s/.test(working[i - 1])) continue;
|
||||
consumed += 1;
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
},
|
||||
"design-consultation": {
|
||||
"lead": "Design consultation: understands your product, researches the landscape, proposes a complete design system (aesthetic, typography, color, layout, spacing, motion), and generates font+color preview...",
|
||||
"routing": "",
|
||||
"routing": "Creates DESIGN.md as your project's design source\nof truth. For existing sites, use /plan-design-review to infer the system instead.\nUse when asked to \"design system\", \"brand guidelines\", or \"create DESIGN.md\".\nProactively suggest when starting a new project's UI with no existing\ndesign system or DESIGN.md.",
|
||||
"voice_line": null
|
||||
},
|
||||
"learn": {
|
||||
|
||||
Reference in New Issue
Block a user