Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/keyword-routing-word-boundaries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@open-codesign/core": patch
---

Word-boundary the progressive-disclosure keyword regexes so substring tokens
no longer false-trigger sections. Previously `metric` matched `biometric`,
`graph` matched `paragraph`, and `logo` matched `logout`. English tokens are
now anchored with `\b...\b` (with optional `s?` for plurals); CJK alternations
remain un-anchored.
14 changes: 14 additions & 0 deletions .changeset/progressive-prompt-disclosure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@open-codesign/core': minor
---

System prompt now does progressive disclosure based on user-prompt keywords. The full create-mode prompt was ~41 KB / ~10k tokens — enough to crush small-context models (e.g. minimax-m2.5:free at 8k ctx) and dilute the instructions strong models actually follow.

`composeSystemPrompt()` now accepts an optional `userPrompt` field. When provided in `create` mode, it assembles:

- **Layer 1 (always, ~12 KB):** identity, workflow, output-rules, design-methodology, pre-flight, editmode-protocol, safety, plus a new condensed `antiSlopDigest` section.
- **Layer 2 (keyword-matched):** chart-rendering + dashboard ambient signals for dashboard cues; iOS starter template for mobile cues; single-page / big-numbers / customer-quotes craft subsections for marketing cues; logos subsection for brand cues. No keyword match → fall back to the full craft directives.

Measured size for sample prompts: dashboard 22.6 KB (55%), mobile 21.7 KB (53%), marketing 19.8 KB (48%), no-keyword fallback 24.5 KB (59%).

When `userPrompt` is omitted, or mode is `tweak` / `revise`, the prompt is byte-identical to before — full back-compat.
94 changes: 94 additions & 0 deletions packages/core/src/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,100 @@ describe('composeSystemPrompt()', () => {
});
});

describe('composeSystemPrompt() — progressive disclosure', () => {
const FULL = composeSystemPrompt({ mode: 'create' });

it('back-compat: omitting userPrompt returns the full prompt byte-identical to today', () => {
expect(composeSystemPrompt({ mode: 'create' })).toBe(FULL);
});

it('Layer 1 sections always present regardless of input', () => {
for (const userPrompt of ['做个数据看板', 'iOS 移动端', '随便做点东西', '']) {
const p = composeSystemPrompt({ mode: 'create', userPrompt });
expect(p, `identity missing for "${userPrompt}"`).toContain('open-codesign');
expect(p, `workflow missing for "${userPrompt}"`).toContain('Design workflow');
expect(p, `output rules missing for "${userPrompt}"`).toContain('Output rules');
expect(p, `safety missing for "${userPrompt}"`).toContain('Safety and scope');
expect(p, `anti-slop digest missing for "${userPrompt}"`).toContain('Anti-slop digest');
}
});

it('dashboard prompt: includes chart rendering, excludes iOS starter', () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a negative routing test for substring collisions to prevent regressions (e.g., paragraph/asymmetric should not trigger dashboard routing):

it('does not trigger dashboard routing on substring collisions', () => {
  const p = composeSystemPrompt({ mode: 'create', userPrompt: 'improve paragraph rhythm and asymmetric spacing' });
  expect(p).not.toContain('Chart rendering contract');
  expect(p).not.toContain('Dashboard ambient signals');
});

const p = composeSystemPrompt({ mode: 'create', userPrompt: '做个数据看板' });
expect(p).toContain('Chart rendering contract');
expect(p).toContain('Dashboard ambient signals');
expect(p).not.toContain('iOS frame starter');
});

it('mobile prompt: includes iOS starter template, excludes chart rendering', () => {
const p = composeSystemPrompt({
mode: 'create',
userPrompt: 'iOS 移动端 onboarding',
});
expect(p).toContain('iOS frame starter');
expect(p).not.toContain('Chart rendering contract');
});

it('marketing prompt: includes single-page structure ladder subsection', () => {
const p = composeSystemPrompt({
mode: 'create',
userPrompt: 'indie marketing landing page',
});
expect(p).toContain('Single-page structure ladder');
expect(p).toContain('Customer quotes deserve distinguished treatment');
});

it('no-keyword prompt: falls back to FULL craft directives', () => {
const p = composeSystemPrompt({ mode: 'create', userPrompt: '随便做点东西' });
// Full craft directives includes ALL ten subsections — verify several signal ones
expect(p).toContain('Craft directives');
expect(p).toContain('Artifact-type classification');
expect(p).toContain('Density floor');
expect(p).toContain('Dashboard ambient signals');
expect(p).toContain('Logos and brand marks');
expect(p).toContain('Single-page structure ladder');
});

it('regression guard: matched dashboard prompt stays under 25 KB', () => {
const p = composeSystemPrompt({ mode: 'create', userPrompt: '做个数据看板' });
expect(p.length).toBeLessThan(25_000);
});

it('mode tweak ignores userPrompt and returns the full tweak prompt', () => {
const a = composeSystemPrompt({ mode: 'tweak' });
const b = composeSystemPrompt({ mode: 'tweak', userPrompt: '做个数据看板' });
expect(b).toBe(a);
});

it('mode revise ignores userPrompt and returns the full revise prompt', () => {
const a = composeSystemPrompt({ mode: 'revise' });
const b = composeSystemPrompt({ mode: 'revise', userPrompt: '做个数据看板' });
expect(b).toBe(a);
});

it('does not trigger dashboard routing on substring collisions (paragraph/asymmetric/biometric)', () => {
// Pair the colliding tokens with a mobile cue so the composer does NOT
// fall back to full CRAFT_DIRECTIVES — that fallback would re-introduce
// the dashboard subsection and defeat the substring-collision check.
const p = composeSystemPrompt({
mode: 'create',
userPrompt: 'iOS app screen — paragraph rhythm, asymmetric spacing, biometric login',
});
expect(p).not.toContain('Chart rendering contract');
expect(p).not.toContain('Dashboard ambient signals');
});

it('does not trigger logo routing on "logout" substring', () => {
// Same reason as above — pair with an unrelated mobile cue to avoid the
// no-keyword fallback that would otherwise pull in full craft directives.
const p = composeSystemPrompt({
mode: 'create',
userPrompt: 'iOS app screen for a logout confirmation modal',
});
expect(p).not.toContain('Logos and brand marks');
});
});

describe('prompt section .txt vs TS drift', () => {
const promptsDir = resolve(dirname(fileURLToPath(import.meta.url)), 'prompts');

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,7 @@ export async function generate(input: GenerateInput): Promise<GenerateOutput> {
input.systemPrompt ??
composeSystemPrompt({
mode: 'create',
userPrompt: input.prompt,
...(skillBlobs.length > 0 ? { skills: skillBlobs } : {}),
}),
},
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/prompts/anti-slop-digest.v1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Anti-slop digest (forbidden patterns)

Do not produce these. Each one is the tell of an unconsidered, generated-feeling artifact:

- A "minimal dark" page that is `#0E0E10` end-to-end with a single purple accent and four sparse stat cards.
- A hero section with a gradient blob background, bold sans headline, and a generic screenshot mockup.
- A features section with six 1:1 cards, each with a 24px icon, a two-word title, and a sentence of filler text.
- A testimonials section with circular avatars, a name, a title, and a five-star rating.
- A footer with three columns of nav links and a social media icon row.
- A "case study" that is four metric cards plus a single quote — missing hero, before/after, customer profile, and closing.
- A logo placeholder rendered as a soft-rounded square with a single random letter centered inside. Use a constructed monogram, a wordmark, or an explicit hatched "YOUR LOGO HERE" rectangle instead.
- Decorative emoji used as section icons unless the brief explicitly asks for emoji.
- Default Tailwind blue (`#3b82f6`) or default Tailwind grays as the entire neutral scale.
- Lorem ipsum, "John Doe", "Acme Corp", "100%" / "1,234" round-number filler.
- Fonts in the overused-default set: Inter, Roboto, Arial, Helvetica, Playfair Display (unless explicitly requested).
- Hotlinked photos from any external host (`placeholder.com`, `unsplash.com`, `picsum.photos`, `randomuser.me`, etc.).
- Center-aligned body paragraphs.
- Pure black (`#000`) for text — use near-black with a slight hue cast.

These patterns are forbidden when combined without a distinctive visual angle that makes them feel intentional rather than assembled from a component kit.
Loading
Loading