From 234de4d75268803fad761c786ef9d408173f98dc Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Wed, 1 Apr 2026 12:49:37 +0300 Subject: [PATCH 1/9] refactor(mcp): remove childScopes and additionalScopes # BREAKING: These component metadata fields are no longer needed as Ignite UI for Angular now renders all floating elements in compound components under the host root using the Popover API. Themes can now be scoped under the parent selector alone. --- .claude/commands/opsx/apply.md | 2 +- .claude/commands/opsx/archive.md | 4 +- .claude/commands/opsx/bulk-archive.md | 2 +- .claude/commands/opsx/continue.md | 2 +- .claude/commands/opsx/explore.md | 7 +- .claude/commands/opsx/ff.md | 5 +- .claude/commands/opsx/onboard.md | 53 ++++-- .claude/skills/openspec-apply-change/SKILL.md | 2 +- .../skills/openspec-archive-change/SKILL.md | 4 +- .../openspec-bulk-archive-change/SKILL.md | 4 +- .../skills/openspec-continue-change/SKILL.md | 2 +- .claude/skills/openspec-explore/SKILL.md | 12 +- .claude/skills/openspec-ff-change/SKILL.md | 2 +- .claude/skills/openspec-new-change/SKILL.md | 2 +- .claude/skills/openspec-onboard/SKILL.md | 55 ++++-- .claude/skills/openspec-sync-specs/SKILL.md | 2 +- .../skills/openspec-verify-change/SKILL.md | 2 +- .github/prompts/opsx-apply.prompt.md | 2 +- .github/prompts/opsx-archive.prompt.md | 4 +- .github/prompts/opsx-bulk-archive.prompt.md | 2 +- .github/prompts/opsx-continue.prompt.md | 2 +- .github/prompts/opsx-explore.prompt.md | 7 +- .github/prompts/opsx-ff.prompt.md | 5 +- .github/prompts/opsx-onboard.prompt.md | 53 ++++-- .github/skills/openspec-apply-change/SKILL.md | 2 +- .../skills/openspec-archive-change/SKILL.md | 4 +- .../openspec-bulk-archive-change/SKILL.md | 4 +- .../skills/openspec-continue-change/SKILL.md | 2 +- .github/skills/openspec-explore/SKILL.md | 12 +- .github/skills/openspec-ff-change/SKILL.md | 2 +- .github/skills/openspec-new-change/SKILL.md | 2 +- .github/skills/openspec-onboard/SKILL.md | 55 ++++-- .github/skills/openspec-sync-specs/SKILL.md | 2 +- .../skills/openspec-verify-change/SKILL.md | 2 +- .opencode/command/opsx-apply.md | 8 +- .opencode/command/opsx-archive.md | 6 +- .opencode/command/opsx-bulk-archive.md | 2 +- .opencode/command/opsx-continue.md | 6 +- .opencode/command/opsx-explore.md | 9 +- .opencode/command/opsx-ff.md | 9 +- .opencode/command/opsx-new.md | 6 +- .opencode/command/opsx-onboard.md | 75 +++++--- .opencode/command/opsx-sync.md | 2 +- .opencode/command/opsx-verify.md | 2 +- .../skills/openspec-apply-change/SKILL.md | 4 +- .../skills/openspec-archive-change/SKILL.md | 4 +- .../openspec-bulk-archive-change/SKILL.md | 4 +- .../skills/openspec-continue-change/SKILL.md | 2 +- .opencode/skills/openspec-explore/SKILL.md | 14 +- .opencode/skills/openspec-ff-change/SKILL.md | 4 +- .opencode/skills/openspec-new-change/SKILL.md | 2 +- .opencode/skills/openspec-onboard/SKILL.md | 77 +++++--- .opencode/skills/openspec-sync-specs/SKILL.md | 2 +- .../skills/openspec-verify-change/SKILL.md | 2 +- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/biome-linting/spec.md | 0 .../specs/lint-staged-integration/spec.md | 0 .../tasks.md | 0 .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/component-theming/spec.md | 0 .../tasks.md | 0 .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/component-theming/spec.md | 0 .../specs/generic-platform/spec.md | 0 .../specs/layout-overrides/spec.md | 0 .../specs/platform-detection/spec.md | 0 .../specs/theme-generation/spec.md | 0 .../tasks.md | 0 .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/spec.md | 0 .../tasks.md | 0 .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/spec.md | 0 .../2026-04-01-mcp-prompt-resources}/tasks.md | 0 .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/spec.md | 0 .../tasks.md | 0 .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/spec.md | 0 .../tasks.md | 0 .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/documentation-organization/spec.md | 0 .../specs/vite-markdown-imports/spec.md | 0 .../tasks.md | 0 .../.openspec.yaml | 2 +- .../design.md | 89 +++++++++ .../proposal.md | 32 ++++ .../component-metadata-unification/spec.md | 79 ++++++++ .../specs/component-theming/spec.md | 80 ++++++++ .../tasks.md | 35 ++++ .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/component-theming/spec.md | 0 .../tasks.md | 0 .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../component-metadata-unification/spec.md | 0 .../tasks.md | 0 .../changes/component-theme-alias/design.md | 45 ----- .../changes/component-theme-alias/proposal.md | 26 --- .../specs/component-theming/spec.md | 25 --- .../changes/component-theme-alias/tasks.md | 20 -- .../simplify-compound-theming/.openspec.yaml | 2 - .../simplify-compound-theming/design.md | 180 ------------------ .../simplify-compound-theming/proposal.md | 38 ---- .../specs/component-theming/spec.md | 84 -------- .../specs/compound-theming-guidance/spec.md | 112 ----------- .../specs/css-output/spec.md | 16 -- .../simplify-compound-theming/tasks.md | 41 ---- openspec/config.yaml | 2 +- openspec/specs/biome-linting/spec.md | 150 +++++++++++++++ .../component-metadata-unification/spec.md | 161 ++++++++++++++++ .../specs/component-theme-alias/spec.md | 6 +- openspec/specs/component-theming/spec.md | 103 +++++++++- .../specs/documentation-organization/spec.md | 61 ++++++ openspec/specs/generic-platform/spec.md | 168 ++++++++++++++++ openspec/specs/layout-overrides/spec.md | 24 ++- .../specs/lint-staged-integration/spec.md | 127 ++++++++++++ openspec/specs/platform-detection/spec.md | 32 +++- openspec/specs/theme-generation/spec.md | 41 ++-- openspec/specs/vite-markdown-imports/spec.md | 47 +++++ packages/mcp/README.md | 17 +- .../knowledge/component-metadata.test.ts | 73 +------ .../tools/handlers/component-tokens.test.ts | 62 +++--- .../mcp/src/knowledge/component-metadata.ts | 59 ------ packages/mcp/src/knowledge/index.ts | 1 - packages/mcp/src/tools/descriptions.ts | 28 ++- .../src/tools/handlers/component-tokens.ts | 155 +++------------ 146 files changed, 1615 insertions(+), 1172 deletions(-) rename openspec/changes/{add-biome-prettier-linting => archive/2026-04-01-add-biome-prettier-linting}/.openspec.yaml (100%) rename openspec/changes/{add-biome-prettier-linting => archive/2026-04-01-add-biome-prettier-linting}/design.md (100%) rename openspec/changes/{add-biome-prettier-linting => archive/2026-04-01-add-biome-prettier-linting}/proposal.md (100%) rename openspec/changes/{add-biome-prettier-linting => archive/2026-04-01-add-biome-prettier-linting}/specs/biome-linting/spec.md (100%) rename openspec/changes/{add-biome-prettier-linting => archive/2026-04-01-add-biome-prettier-linting}/specs/lint-staged-integration/spec.md (100%) rename openspec/changes/{add-biome-prettier-linting => archive/2026-04-01-add-biome-prettier-linting}/tasks.md (100%) rename openspec/changes/{compound-scoped-selectors => archive/2026-04-01-compound-scoped-selectors}/.openspec.yaml (100%) rename openspec/changes/{compound-scoped-selectors => archive/2026-04-01-compound-scoped-selectors}/design.md (100%) rename openspec/changes/{compound-scoped-selectors => archive/2026-04-01-compound-scoped-selectors}/proposal.md (100%) rename openspec/changes/{compound-scoped-selectors => archive/2026-04-01-compound-scoped-selectors}/specs/component-theming/spec.md (100%) rename openspec/changes/{compound-scoped-selectors => archive/2026-04-01-compound-scoped-selectors}/tasks.md (100%) rename openspec/changes/{generic-platform-detection => archive/2026-04-01-generic-platform-detection}/.openspec.yaml (100%) rename openspec/changes/{generic-platform-detection => archive/2026-04-01-generic-platform-detection}/design.md (100%) rename openspec/changes/{generic-platform-detection => archive/2026-04-01-generic-platform-detection}/proposal.md (100%) rename openspec/changes/{generic-platform-detection => archive/2026-04-01-generic-platform-detection}/specs/component-theming/spec.md (100%) rename openspec/changes/{generic-platform-detection => archive/2026-04-01-generic-platform-detection}/specs/generic-platform/spec.md (100%) rename openspec/changes/{generic-platform-detection => archive/2026-04-01-generic-platform-detection}/specs/layout-overrides/spec.md (100%) rename openspec/changes/{generic-platform-detection => archive/2026-04-01-generic-platform-detection}/specs/platform-detection/spec.md (100%) rename openspec/changes/{generic-platform-detection => archive/2026-04-01-generic-platform-detection}/specs/theme-generation/spec.md (100%) rename openspec/changes/{generic-platform-detection => archive/2026-04-01-generic-platform-detection}/tasks.md (100%) rename openspec/changes/{mcp-color-intelligence => archive/2026-04-01-mcp-color-intelligence}/.openspec.yaml (100%) rename openspec/changes/{mcp-color-intelligence => archive/2026-04-01-mcp-color-intelligence}/design.md (100%) rename openspec/changes/{mcp-color-intelligence => archive/2026-04-01-mcp-color-intelligence}/proposal.md (100%) rename openspec/changes/{mcp-color-intelligence => archive/2026-04-01-mcp-color-intelligence}/specs/spec.md (100%) rename openspec/changes/{mcp-color-intelligence => archive/2026-04-01-mcp-color-intelligence}/tasks.md (100%) rename openspec/changes/{mcp-prompt-resources => archive/2026-04-01-mcp-prompt-resources}/.openspec.yaml (100%) rename openspec/changes/{mcp-prompt-resources => archive/2026-04-01-mcp-prompt-resources}/design.md (100%) rename openspec/changes/{mcp-prompt-resources => archive/2026-04-01-mcp-prompt-resources}/proposal.md (100%) rename openspec/changes/{mcp-prompt-resources => archive/2026-04-01-mcp-prompt-resources}/specs/spec.md (100%) rename openspec/changes/{mcp-prompt-resources => archive/2026-04-01-mcp-prompt-resources}/tasks.md (100%) rename openspec/changes/{mcp-typography-utilities => archive/2026-04-01-mcp-typography-utilities}/.openspec.yaml (100%) rename openspec/changes/{mcp-typography-utilities => archive/2026-04-01-mcp-typography-utilities}/design.md (100%) rename openspec/changes/{mcp-typography-utilities => archive/2026-04-01-mcp-typography-utilities}/proposal.md (100%) rename openspec/changes/{mcp-typography-utilities => archive/2026-04-01-mcp-typography-utilities}/specs/spec.md (100%) rename openspec/changes/{mcp-typography-utilities => archive/2026-04-01-mcp-typography-utilities}/tasks.md (100%) rename openspec/changes/{mcp-validation-intelligence => archive/2026-04-01-mcp-validation-intelligence}/.openspec.yaml (100%) rename openspec/changes/{mcp-validation-intelligence => archive/2026-04-01-mcp-validation-intelligence}/design.md (100%) rename openspec/changes/{mcp-validation-intelligence => archive/2026-04-01-mcp-validation-intelligence}/proposal.md (100%) rename openspec/changes/{mcp-validation-intelligence => archive/2026-04-01-mcp-validation-intelligence}/specs/spec.md (100%) rename openspec/changes/{mcp-validation-intelligence => archive/2026-04-01-mcp-validation-intelligence}/tasks.md (100%) rename openspec/changes/{migrate-mcp-docs-to-vite-markdown => archive/2026-04-01-migrate-mcp-docs-to-vite-markdown}/.openspec.yaml (100%) rename openspec/changes/{migrate-mcp-docs-to-vite-markdown => archive/2026-04-01-migrate-mcp-docs-to-vite-markdown}/design.md (100%) rename openspec/changes/{migrate-mcp-docs-to-vite-markdown => archive/2026-04-01-migrate-mcp-docs-to-vite-markdown}/proposal.md (100%) rename openspec/changes/{migrate-mcp-docs-to-vite-markdown => archive/2026-04-01-migrate-mcp-docs-to-vite-markdown}/specs/documentation-organization/spec.md (100%) rename openspec/changes/{migrate-mcp-docs-to-vite-markdown => archive/2026-04-01-migrate-mcp-docs-to-vite-markdown}/specs/vite-markdown-imports/spec.md (100%) rename openspec/changes/{migrate-mcp-docs-to-vite-markdown => archive/2026-04-01-migrate-mcp-docs-to-vite-markdown}/tasks.md (100%) rename openspec/changes/{component-theme-alias => archive/2026-04-01-remove-compound-scopes}/.openspec.yaml (50%) create mode 100644 openspec/changes/archive/2026-04-01-remove-compound-scopes/design.md create mode 100644 openspec/changes/archive/2026-04-01-remove-compound-scopes/proposal.md create mode 100644 openspec/changes/archive/2026-04-01-remove-compound-scopes/specs/component-metadata-unification/spec.md create mode 100644 openspec/changes/archive/2026-04-01-remove-compound-scopes/specs/component-theming/spec.md create mode 100644 openspec/changes/archive/2026-04-01-remove-compound-scopes/tasks.md rename openspec/changes/{restructure-token-descriptions => archive/2026-04-01-restructure-token-descriptions}/.openspec.yaml (100%) rename openspec/changes/{restructure-token-descriptions => archive/2026-04-01-restructure-token-descriptions}/design.md (100%) rename openspec/changes/{restructure-token-descriptions => archive/2026-04-01-restructure-token-descriptions}/proposal.md (100%) rename openspec/changes/{restructure-token-descriptions => archive/2026-04-01-restructure-token-descriptions}/specs/component-theming/spec.md (100%) rename openspec/changes/{restructure-token-descriptions => archive/2026-04-01-restructure-token-descriptions}/tasks.md (100%) rename openspec/changes/{unify-component-metadata => archive/2026-04-01-unify-component-metadata}/.openspec.yaml (100%) rename openspec/changes/{unify-component-metadata => archive/2026-04-01-unify-component-metadata}/design.md (100%) rename openspec/changes/{unify-component-metadata => archive/2026-04-01-unify-component-metadata}/proposal.md (100%) rename openspec/changes/{unify-component-metadata => archive/2026-04-01-unify-component-metadata}/specs/component-metadata-unification/spec.md (100%) rename openspec/changes/{unify-component-metadata => archive/2026-04-01-unify-component-metadata}/tasks.md (100%) delete mode 100644 openspec/changes/component-theme-alias/design.md delete mode 100644 openspec/changes/component-theme-alias/proposal.md delete mode 100644 openspec/changes/component-theme-alias/specs/component-theming/spec.md delete mode 100644 openspec/changes/component-theme-alias/tasks.md delete mode 100644 openspec/changes/simplify-compound-theming/.openspec.yaml delete mode 100644 openspec/changes/simplify-compound-theming/design.md delete mode 100644 openspec/changes/simplify-compound-theming/proposal.md delete mode 100644 openspec/changes/simplify-compound-theming/specs/component-theming/spec.md delete mode 100644 openspec/changes/simplify-compound-theming/specs/compound-theming-guidance/spec.md delete mode 100644 openspec/changes/simplify-compound-theming/specs/css-output/spec.md delete mode 100644 openspec/changes/simplify-compound-theming/tasks.md create mode 100644 openspec/specs/biome-linting/spec.md create mode 100644 openspec/specs/component-metadata-unification/spec.md rename openspec/{changes/component-theme-alias => }/specs/component-theme-alias/spec.md (87%) create mode 100644 openspec/specs/documentation-organization/spec.md create mode 100644 openspec/specs/generic-platform/spec.md create mode 100644 openspec/specs/lint-staged-integration/spec.md create mode 100644 openspec/specs/vite-markdown-imports/spec.md diff --git a/.claude/commands/opsx/apply.md b/.claude/commands/opsx/apply.md index 645bbdb8..bf23721d 100644 --- a/.claude/commands/opsx/apply.md +++ b/.claude/commands/opsx/apply.md @@ -111,7 +111,7 @@ Working on task 4/7: - [x] Task 2 ... -All tasks complete! Ready to archive this change. +All tasks complete! You can archive this change with `/opsx:archive`. ``` **Output On Pause (Issue Encountered)** diff --git a/.claude/commands/opsx/archive.md b/.claude/commands/opsx/archive.md index 7275c854..5e916083 100644 --- a/.claude/commands/opsx/archive.md +++ b/.claude/commands/opsx/archive.md @@ -59,7 +59,7 @@ Archive a completed change in the experimental workflow. - If changes needed: "Sync now (recommended)", "Archive without syncing" - If already synced: "Archive now", "Sync anyway", "Cancel" - If user chooses sync, execute `/opsx:sync` logic. Proceed to archive regardless of choice. + If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''. Delta spec analysis: "). Proceed to archive regardless of choice. 5. **Perform the archive** @@ -153,5 +153,5 @@ Target archive directory already exists. - Don't block archive on warnings - just inform and confirm - Preserve .openspec.yaml when moving to archive (it moves with the directory) - Show clear summary of what happened -- If sync is requested, use /opsx:sync approach (agent-driven) +- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven) - If delta specs exist, always run the sync assessment and show the combined summary before prompting diff --git a/.claude/commands/opsx/bulk-archive.md b/.claude/commands/opsx/bulk-archive.md index b700261c..a1514105 100644 --- a/.claude/commands/opsx/bulk-archive.md +++ b/.claude/commands/opsx/bulk-archive.md @@ -225,7 +225,7 @@ Failed K changes: ``` ## No Changes to Archive -No active changes found. Use `/opsx:new` to create a new change. +No active changes found. Create a new change to get started. ``` **Guardrails** diff --git a/.claude/commands/opsx/continue.md b/.claude/commands/opsx/continue.md index 49daaa7a..af255c6f 100644 --- a/.claude/commands/opsx/continue.md +++ b/.claude/commands/opsx/continue.md @@ -41,7 +41,7 @@ Continue working on a change by creating the next artifact. **If all artifacts are complete (`isComplete: true`)**: - Congratulate the user - Show final status including the schema used - - Suggest: "All artifacts created! You can now implement this change or archive it." + - Suggest: "All artifacts created! You can now implement this change with `/opsx:apply` or archive it with `/opsx:archive`." - STOP --- diff --git a/.claude/commands/opsx/explore.md b/.claude/commands/opsx/explore.md index 202566bd..30d9c57a 100644 --- a/.claude/commands/opsx/explore.md +++ b/.claude/commands/opsx/explore.md @@ -7,7 +7,7 @@ tags: [workflow, explore, experimental, thinking] Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. -**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first (e.g., start a change with `/opsx:new` or `/opsx:ff`). You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. +**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. **This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore. @@ -100,8 +100,7 @@ If the user mentioned a specific change name, read its artifacts for context. Think freely. When insights crystallize, you might offer: -- "This feels solid enough to start a change. Want me to create one?" - → Can transition to `/opsx:new` or `/opsx:ff` +- "This feels solid enough to start a change. Want me to create a proposal?" - Or keep exploring - no pressure to formalize ### When a change exists @@ -153,7 +152,7 @@ If the user mentions a change or you detect one is relevant: There's no required ending. Discovery might: -- **Flow into action**: "Ready to start? `/opsx:new` or `/opsx:ff`" +- **Flow into a proposal**: "Ready to start? I can create a change proposal." - **Result in artifact updates**: "Updated design.md with these decisions" - **Just provide clarity**: User has what they need, moves on - **Continue later**: "We can pick this up anytime" diff --git a/.claude/commands/opsx/ff.md b/.claude/commands/opsx/ff.md index bea9d610..69f749c9 100644 --- a/.claude/commands/opsx/ff.md +++ b/.claude/commands/opsx/ff.md @@ -84,7 +84,10 @@ After completing all artifacts, summarize: - Follow the `instruction` field from `openspec instructions` for each artifact type - The schema defines what each artifact should contain - follow it - Read dependency artifacts for context before creating new ones -- Use the `template` as a starting point, filling in based on context +- Use `template` as the structure for your output file - fill in its sections +- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file + - Do NOT copy ``, ``, `` blocks into the artifact + - These guide what you write, but should never appear in the output **Guardrails** - Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`) diff --git a/.claude/commands/opsx/onboard.md b/.claude/commands/opsx/onboard.md index e15790e5..7df677c4 100644 --- a/.claude/commands/opsx/onboard.md +++ b/.claude/commands/opsx/onboard.md @@ -11,16 +11,19 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t ## Preflight -Before starting, check if OpenSpec is initialized: +Before starting, check if the OpenSpec CLI is installed: ```bash -openspec status --json 2>&1 || echo "NOT_INITIALIZED" +# Unix/macOS +openspec --version 2>&1 || echo "CLI_NOT_INSTALLED" +# Windows (PowerShell) +# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" } ``` -**If not initialized:** -> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`. +**If CLI not installed:** +> OpenSpec CLI is not installed. Install it first, then come back to `/opsx:onboard`. -Stop here if not initialized. +Stop here if not installed. --- @@ -63,7 +66,10 @@ Scan the codebase for small improvement opportunities. Look for: Also check recent git activity: ```bash +# Unix/macOS git log --oneline -10 2>/dev/null || echo "No git history" +# Windows (PowerShell) +# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" } ``` ### Present Suggestions @@ -258,7 +264,10 @@ For a small task like this, we might only need one spec file. **DO:** Create the spec file: ```bash +# Unix/macOS mkdir -p openspec/changes//specs/ +# Windows (PowerShell) +# New-Item -ItemType Directory -Force -Path "openspec/changes//specs/" ``` Draft the spec content: @@ -453,21 +462,29 @@ This same rhythm works for any size change—a small fix or a major feature. ## Command Reference +**Core workflow:** + | Command | What it does | |---------|--------------| +| `/opsx:propose` | Create a change and generate all artifacts | | `/opsx:explore` | Think through problems before/during work | -| `/opsx:new` | Start a new change, step through artifacts | -| `/opsx:ff` | Fast-forward: create all artifacts at once | -| `/opsx:continue` | Continue working on an existing change | | `/opsx:apply` | Implement tasks from a change | -| `/opsx:verify` | Verify implementation matches artifacts | | `/opsx:archive` | Archive a completed change | +**Additional commands:** + +| Command | What it does | +|---------|--------------| +| `/opsx:new` | Start a new change, step through artifacts one at a time | +| `/opsx:continue` | Continue working on an existing change | +| `/opsx:ff` | Fast-forward: create all artifacts at once | +| `/opsx:verify` | Verify implementation matches artifacts | + --- ## What's Next? -Try `/opsx:new` or `/opsx:ff` on something you actually want to build. You've got the rhythm now! +Try `/opsx:propose` on something you actually want to build. You've got the rhythm now! ``` --- @@ -497,17 +514,25 @@ If the user says they just want to see the commands or skip the tutorial: ``` ## OpenSpec Quick Reference +**Core workflow:** + | Command | What it does | |---------|--------------| +| `/opsx:propose ` | Create a change and generate all artifacts | | `/opsx:explore` | Think through problems (no code changes) | +| `/opsx:apply ` | Implement tasks | +| `/opsx:archive ` | Archive when done | + +**Additional commands:** + +| Command | What it does | +|---------|--------------| | `/opsx:new ` | Start a new change, step by step | -| `/opsx:ff ` | Fast-forward: all artifacts at once | | `/opsx:continue ` | Continue an existing change | -| `/opsx:apply ` | Implement tasks | +| `/opsx:ff ` | Fast-forward: all artifacts at once | | `/opsx:verify ` | Verify implementation | -| `/opsx:archive ` | Archive when done | -Try `/opsx:new` to start your first change, or `/opsx:ff` if you want to move fast. +Try `/opsx:propose` to start your first change. ``` Exit gracefully. diff --git a/.claude/skills/openspec-apply-change/SKILL.md b/.claude/skills/openspec-apply-change/SKILL.md index bc95df43..d474dc13 100644 --- a/.claude/skills/openspec-apply-change/SKILL.md +++ b/.claude/skills/openspec-apply-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Implement tasks from an OpenSpec change. diff --git a/.claude/skills/openspec-archive-change/SKILL.md b/.claude/skills/openspec-archive-change/SKILL.md index 9ea63e8a..9b1f851a 100644 --- a/.claude/skills/openspec-archive-change/SKILL.md +++ b/.claude/skills/openspec-archive-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Archive a completed change in the experimental workflow. @@ -63,7 +63,7 @@ Archive a completed change in the experimental workflow. - If changes needed: "Sync now (recommended)", "Archive without syncing" - If already synced: "Archive now", "Sync anyway", "Cancel" - If user chooses sync, execute /opsx:sync logic (use the openspec-sync-specs skill). Proceed to archive regardless of choice. + If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''. Delta spec analysis: "). Proceed to archive regardless of choice. 5. **Perform the archive** diff --git a/.claude/skills/openspec-bulk-archive-change/SKILL.md b/.claude/skills/openspec-bulk-archive-change/SKILL.md index 5ce056a9..d2f199af 100644 --- a/.claude/skills/openspec-bulk-archive-change/SKILL.md +++ b/.claude/skills/openspec-bulk-archive-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Archive multiple completed changes in a single operation. @@ -229,7 +229,7 @@ Failed K changes: ``` ## No Changes to Archive -No active changes found. Use `/opsx:new` to create a new change. +No active changes found. Create a new change to get started. ``` **Guardrails** diff --git a/.claude/skills/openspec-continue-change/SKILL.md b/.claude/skills/openspec-continue-change/SKILL.md index 79aaac48..a2856f04 100644 --- a/.claude/skills/openspec-continue-change/SKILL.md +++ b/.claude/skills/openspec-continue-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Continue working on a change by creating the next artifact. diff --git a/.claude/skills/openspec-explore/SKILL.md b/.claude/skills/openspec-explore/SKILL.md index 49d051da..ffa10cad 100644 --- a/.claude/skills/openspec-explore/SKILL.md +++ b/.claude/skills/openspec-explore/SKILL.md @@ -6,12 +6,12 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. -**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first (e.g., start a change with `/opsx:new` or `/opsx:ff`). You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. +**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. **This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore. @@ -95,8 +95,7 @@ This tells you: Think freely. When insights crystallize, you might offer: -- "This feels solid enough to start a change. Want me to create one?" - → Can transition to `/opsx:new` or `/opsx:ff` +- "This feels solid enough to start a change. Want me to create a proposal?" - Or keep exploring - no pressure to formalize ### When a change exists @@ -252,7 +251,7 @@ You: That changes everything. There's no required ending. Discovery might: -- **Flow into action**: "Ready to start? /opsx:new or /opsx:ff" +- **Flow into a proposal**: "Ready to start? I can create a change proposal." - **Result in artifact updates**: "Updated design.md with these decisions" - **Just provide clarity**: User has what they need, moves on - **Continue later**: "We can pick this up anytime" @@ -269,8 +268,7 @@ When it feels like things are crystallizing, you might summarize: **Open questions**: [if any remain] **Next steps** (if ready): -- Create a change: /opsx:new -- Fast-forward to tasks: /opsx:ff +- Create a change proposal - Keep exploring: just keep talking ``` diff --git a/.claude/skills/openspec-ff-change/SKILL.md b/.claude/skills/openspec-ff-change/SKILL.md index 64f058ca..d5f12043 100644 --- a/.claude/skills/openspec-ff-change/SKILL.md +++ b/.claude/skills/openspec-ff-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Fast-forward through artifact creation - generate everything needed to start implementation in one go. diff --git a/.claude/skills/openspec-new-change/SKILL.md b/.claude/skills/openspec-new-change/SKILL.md index 53d96b99..607391aa 100644 --- a/.claude/skills/openspec-new-change/SKILL.md +++ b/.claude/skills/openspec-new-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Start a new change using the experimental artifact-driven approach. diff --git a/.claude/skills/openspec-onboard/SKILL.md b/.claude/skills/openspec-onboard/SKILL.md index 40080aa1..9076b5d8 100644 --- a/.claude/skills/openspec-onboard/SKILL.md +++ b/.claude/skills/openspec-onboard/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step. @@ -15,16 +15,19 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t ## Preflight -Before starting, check if OpenSpec is initialized: +Before starting, check if the OpenSpec CLI is installed: ```bash -openspec status --json 2>&1 || echo "NOT_INITIALIZED" +# Unix/macOS +openspec --version 2>&1 || echo "CLI_NOT_INSTALLED" +# Windows (PowerShell) +# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" } ``` -**If not initialized:** -> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`. +**If CLI not installed:** +> OpenSpec CLI is not installed. Install it first, then come back to `/opsx:onboard`. -Stop here if not initialized. +Stop here if not installed. --- @@ -67,7 +70,10 @@ Scan the codebase for small improvement opportunities. Look for: Also check recent git activity: ```bash +# Unix/macOS git log --oneline -10 2>/dev/null || echo "No git history" +# Windows (PowerShell) +# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" } ``` ### Present Suggestions @@ -262,7 +268,10 @@ For a small task like this, we might only need one spec file. **DO:** Create the spec file: ```bash +# Unix/macOS mkdir -p openspec/changes//specs/ +# Windows (PowerShell) +# New-Item -ItemType Directory -Force -Path "openspec/changes//specs/" ``` Draft the spec content: @@ -457,21 +466,29 @@ This same rhythm works for any size change—a small fix or a major feature. ## Command Reference +**Core workflow:** + | Command | What it does | |---------|--------------| +| `/opsx:propose` | Create a change and generate all artifacts | | `/opsx:explore` | Think through problems before/during work | -| `/opsx:new` | Start a new change, step through artifacts | -| `/opsx:ff` | Fast-forward: create all artifacts at once | -| `/opsx:continue` | Continue working on an existing change | | `/opsx:apply` | Implement tasks from a change | -| `/opsx:verify` | Verify implementation matches artifacts | | `/opsx:archive` | Archive a completed change | +**Additional commands:** + +| Command | What it does | +|---------|--------------| +| `/opsx:new` | Start a new change, step through artifacts one at a time | +| `/opsx:continue` | Continue working on an existing change | +| `/opsx:ff` | Fast-forward: create all artifacts at once | +| `/opsx:verify` | Verify implementation matches artifacts | + --- ## What's Next? -Try `/opsx:new` or `/opsx:ff` on something you actually want to build. You've got the rhythm now! +Try `/opsx:propose` on something you actually want to build. You've got the rhythm now! ``` --- @@ -501,17 +518,25 @@ If the user says they just want to see the commands or skip the tutorial: ``` ## OpenSpec Quick Reference +**Core workflow:** + | Command | What it does | |---------|--------------| +| `/opsx:propose ` | Create a change and generate all artifacts | | `/opsx:explore` | Think through problems (no code changes) | +| `/opsx:apply ` | Implement tasks | +| `/opsx:archive ` | Archive when done | + +**Additional commands:** + +| Command | What it does | +|---------|--------------| | `/opsx:new ` | Start a new change, step by step | -| `/opsx:ff ` | Fast-forward: all artifacts at once | | `/opsx:continue ` | Continue an existing change | -| `/opsx:apply ` | Implement tasks | +| `/opsx:ff ` | Fast-forward: all artifacts at once | | `/opsx:verify ` | Verify implementation | -| `/opsx:archive ` | Archive when done | -Try `/opsx:new` to start your first change, or `/opsx:ff` if you want to move fast. +Try `/opsx:propose` to start your first change. ``` Exit gracefully. diff --git a/.claude/skills/openspec-sync-specs/SKILL.md b/.claude/skills/openspec-sync-specs/SKILL.md index 632681c7..353bfac9 100644 --- a/.claude/skills/openspec-sync-specs/SKILL.md +++ b/.claude/skills/openspec-sync-specs/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Sync delta specs from a change to main specs. diff --git a/.claude/skills/openspec-verify-change/SKILL.md b/.claude/skills/openspec-verify-change/SKILL.md index 21cbc508..744a0883 100644 --- a/.claude/skills/openspec-verify-change/SKILL.md +++ b/.claude/skills/openspec-verify-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Verify that an implementation matches the change artifacts (specs, tasks, design). diff --git a/.github/prompts/opsx-apply.prompt.md b/.github/prompts/opsx-apply.prompt.md index 89fb9ed4..494e10e9 100644 --- a/.github/prompts/opsx-apply.prompt.md +++ b/.github/prompts/opsx-apply.prompt.md @@ -108,7 +108,7 @@ Working on task 4/7: - [x] Task 2 ... -All tasks complete! Ready to archive this change. +All tasks complete! You can archive this change with `/opsx:archive`. ``` **Output On Pause (Issue Encountered)** diff --git a/.github/prompts/opsx-archive.prompt.md b/.github/prompts/opsx-archive.prompt.md index 4e2ee189..1163776d 100644 --- a/.github/prompts/opsx-archive.prompt.md +++ b/.github/prompts/opsx-archive.prompt.md @@ -56,7 +56,7 @@ Archive a completed change in the experimental workflow. - If changes needed: "Sync now (recommended)", "Archive without syncing" - If already synced: "Archive now", "Sync anyway", "Cancel" - If user chooses sync, execute `/opsx:sync` logic. Proceed to archive regardless of choice. + If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''. Delta spec analysis: "). Proceed to archive regardless of choice. 5. **Perform the archive** @@ -150,5 +150,5 @@ Target archive directory already exists. - Don't block archive on warnings - just inform and confirm - Preserve .openspec.yaml when moving to archive (it moves with the directory) - Show clear summary of what happened -- If sync is requested, use /opsx:sync approach (agent-driven) +- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven) - If delta specs exist, always run the sync assessment and show the combined summary before prompting diff --git a/.github/prompts/opsx-bulk-archive.prompt.md b/.github/prompts/opsx-bulk-archive.prompt.md index f8e773fe..be3f9019 100644 --- a/.github/prompts/opsx-bulk-archive.prompt.md +++ b/.github/prompts/opsx-bulk-archive.prompt.md @@ -222,7 +222,7 @@ Failed K changes: ``` ## No Changes to Archive -No active changes found. Use `/opsx:new` to create a new change. +No active changes found. Create a new change to get started. ``` **Guardrails** diff --git a/.github/prompts/opsx-continue.prompt.md b/.github/prompts/opsx-continue.prompt.md index f91ec4bc..24b480d7 100644 --- a/.github/prompts/opsx-continue.prompt.md +++ b/.github/prompts/opsx-continue.prompt.md @@ -38,7 +38,7 @@ Continue working on a change by creating the next artifact. **If all artifacts are complete (`isComplete: true`)**: - Congratulate the user - Show final status including the schema used - - Suggest: "All artifacts created! You can now implement this change or archive it." + - Suggest: "All artifacts created! You can now implement this change with `/opsx:apply` or archive it with `/opsx:archive`." - STOP --- diff --git a/.github/prompts/opsx-explore.prompt.md b/.github/prompts/opsx-explore.prompt.md index fd588622..b21a2266 100644 --- a/.github/prompts/opsx-explore.prompt.md +++ b/.github/prompts/opsx-explore.prompt.md @@ -4,7 +4,7 @@ description: Enter explore mode - think through ideas, investigate problems, cla Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. -**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first (e.g., start a change with `/opsx:new` or `/opsx:ff`). You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. +**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. **This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore. @@ -97,8 +97,7 @@ If the user mentioned a specific change name, read its artifacts for context. Think freely. When insights crystallize, you might offer: -- "This feels solid enough to start a change. Want me to create one?" - → Can transition to `/opsx:new` or `/opsx:ff` +- "This feels solid enough to start a change. Want me to create a proposal?" - Or keep exploring - no pressure to formalize ### When a change exists @@ -150,7 +149,7 @@ If the user mentions a change or you detect one is relevant: There's no required ending. Discovery might: -- **Flow into action**: "Ready to start? `/opsx:new` or `/opsx:ff`" +- **Flow into a proposal**: "Ready to start? I can create a change proposal." - **Result in artifact updates**: "Updated design.md with these decisions" - **Just provide clarity**: User has what they need, moves on - **Continue later**: "We can pick this up anytime" diff --git a/.github/prompts/opsx-ff.prompt.md b/.github/prompts/opsx-ff.prompt.md index 6b3dc00c..06cea280 100644 --- a/.github/prompts/opsx-ff.prompt.md +++ b/.github/prompts/opsx-ff.prompt.md @@ -81,7 +81,10 @@ After completing all artifacts, summarize: - Follow the `instruction` field from `openspec instructions` for each artifact type - The schema defines what each artifact should contain - follow it - Read dependency artifacts for context before creating new ones -- Use the `template` as a starting point, filling in based on context +- Use `template` as the structure for your output file - fill in its sections +- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file + - Do NOT copy ``, ``, `` blocks into the artifact + - These guide what you write, but should never appear in the output **Guardrails** - Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`) diff --git a/.github/prompts/opsx-onboard.prompt.md b/.github/prompts/opsx-onboard.prompt.md index 1414f1e1..8100b390 100644 --- a/.github/prompts/opsx-onboard.prompt.md +++ b/.github/prompts/opsx-onboard.prompt.md @@ -8,16 +8,19 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t ## Preflight -Before starting, check if OpenSpec is initialized: +Before starting, check if the OpenSpec CLI is installed: ```bash -openspec status --json 2>&1 || echo "NOT_INITIALIZED" +# Unix/macOS +openspec --version 2>&1 || echo "CLI_NOT_INSTALLED" +# Windows (PowerShell) +# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" } ``` -**If not initialized:** -> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`. +**If CLI not installed:** +> OpenSpec CLI is not installed. Install it first, then come back to `/opsx:onboard`. -Stop here if not initialized. +Stop here if not installed. --- @@ -60,7 +63,10 @@ Scan the codebase for small improvement opportunities. Look for: Also check recent git activity: ```bash +# Unix/macOS git log --oneline -10 2>/dev/null || echo "No git history" +# Windows (PowerShell) +# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" } ``` ### Present Suggestions @@ -255,7 +261,10 @@ For a small task like this, we might only need one spec file. **DO:** Create the spec file: ```bash +# Unix/macOS mkdir -p openspec/changes//specs/ +# Windows (PowerShell) +# New-Item -ItemType Directory -Force -Path "openspec/changes//specs/" ``` Draft the spec content: @@ -450,21 +459,29 @@ This same rhythm works for any size change—a small fix or a major feature. ## Command Reference +**Core workflow:** + | Command | What it does | |---------|--------------| +| `/opsx:propose` | Create a change and generate all artifacts | | `/opsx:explore` | Think through problems before/during work | -| `/opsx:new` | Start a new change, step through artifacts | -| `/opsx:ff` | Fast-forward: create all artifacts at once | -| `/opsx:continue` | Continue working on an existing change | | `/opsx:apply` | Implement tasks from a change | -| `/opsx:verify` | Verify implementation matches artifacts | | `/opsx:archive` | Archive a completed change | +**Additional commands:** + +| Command | What it does | +|---------|--------------| +| `/opsx:new` | Start a new change, step through artifacts one at a time | +| `/opsx:continue` | Continue working on an existing change | +| `/opsx:ff` | Fast-forward: create all artifacts at once | +| `/opsx:verify` | Verify implementation matches artifacts | + --- ## What's Next? -Try `/opsx:new` or `/opsx:ff` on something you actually want to build. You've got the rhythm now! +Try `/opsx:propose` on something you actually want to build. You've got the rhythm now! ``` --- @@ -494,17 +511,25 @@ If the user says they just want to see the commands or skip the tutorial: ``` ## OpenSpec Quick Reference +**Core workflow:** + | Command | What it does | |---------|--------------| +| `/opsx:propose ` | Create a change and generate all artifacts | | `/opsx:explore` | Think through problems (no code changes) | +| `/opsx:apply ` | Implement tasks | +| `/opsx:archive ` | Archive when done | + +**Additional commands:** + +| Command | What it does | +|---------|--------------| | `/opsx:new ` | Start a new change, step by step | -| `/opsx:ff ` | Fast-forward: all artifacts at once | | `/opsx:continue ` | Continue an existing change | -| `/opsx:apply ` | Implement tasks | +| `/opsx:ff ` | Fast-forward: all artifacts at once | | `/opsx:verify ` | Verify implementation | -| `/opsx:archive ` | Archive when done | -Try `/opsx:new` to start your first change, or `/opsx:ff` if you want to move fast. +Try `/opsx:propose` to start your first change. ``` Exit gracefully. diff --git a/.github/skills/openspec-apply-change/SKILL.md b/.github/skills/openspec-apply-change/SKILL.md index bc95df43..d474dc13 100644 --- a/.github/skills/openspec-apply-change/SKILL.md +++ b/.github/skills/openspec-apply-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Implement tasks from an OpenSpec change. diff --git a/.github/skills/openspec-archive-change/SKILL.md b/.github/skills/openspec-archive-change/SKILL.md index 9ea63e8a..9b1f851a 100644 --- a/.github/skills/openspec-archive-change/SKILL.md +++ b/.github/skills/openspec-archive-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Archive a completed change in the experimental workflow. @@ -63,7 +63,7 @@ Archive a completed change in the experimental workflow. - If changes needed: "Sync now (recommended)", "Archive without syncing" - If already synced: "Archive now", "Sync anyway", "Cancel" - If user chooses sync, execute /opsx:sync logic (use the openspec-sync-specs skill). Proceed to archive regardless of choice. + If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''. Delta spec analysis: "). Proceed to archive regardless of choice. 5. **Perform the archive** diff --git a/.github/skills/openspec-bulk-archive-change/SKILL.md b/.github/skills/openspec-bulk-archive-change/SKILL.md index 5ce056a9..d2f199af 100644 --- a/.github/skills/openspec-bulk-archive-change/SKILL.md +++ b/.github/skills/openspec-bulk-archive-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Archive multiple completed changes in a single operation. @@ -229,7 +229,7 @@ Failed K changes: ``` ## No Changes to Archive -No active changes found. Use `/opsx:new` to create a new change. +No active changes found. Create a new change to get started. ``` **Guardrails** diff --git a/.github/skills/openspec-continue-change/SKILL.md b/.github/skills/openspec-continue-change/SKILL.md index 79aaac48..a2856f04 100644 --- a/.github/skills/openspec-continue-change/SKILL.md +++ b/.github/skills/openspec-continue-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Continue working on a change by creating the next artifact. diff --git a/.github/skills/openspec-explore/SKILL.md b/.github/skills/openspec-explore/SKILL.md index 49d051da..ffa10cad 100644 --- a/.github/skills/openspec-explore/SKILL.md +++ b/.github/skills/openspec-explore/SKILL.md @@ -6,12 +6,12 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. -**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first (e.g., start a change with `/opsx:new` or `/opsx:ff`). You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. +**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. **This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore. @@ -95,8 +95,7 @@ This tells you: Think freely. When insights crystallize, you might offer: -- "This feels solid enough to start a change. Want me to create one?" - → Can transition to `/opsx:new` or `/opsx:ff` +- "This feels solid enough to start a change. Want me to create a proposal?" - Or keep exploring - no pressure to formalize ### When a change exists @@ -252,7 +251,7 @@ You: That changes everything. There's no required ending. Discovery might: -- **Flow into action**: "Ready to start? /opsx:new or /opsx:ff" +- **Flow into a proposal**: "Ready to start? I can create a change proposal." - **Result in artifact updates**: "Updated design.md with these decisions" - **Just provide clarity**: User has what they need, moves on - **Continue later**: "We can pick this up anytime" @@ -269,8 +268,7 @@ When it feels like things are crystallizing, you might summarize: **Open questions**: [if any remain] **Next steps** (if ready): -- Create a change: /opsx:new -- Fast-forward to tasks: /opsx:ff +- Create a change proposal - Keep exploring: just keep talking ``` diff --git a/.github/skills/openspec-ff-change/SKILL.md b/.github/skills/openspec-ff-change/SKILL.md index 64f058ca..d5f12043 100644 --- a/.github/skills/openspec-ff-change/SKILL.md +++ b/.github/skills/openspec-ff-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Fast-forward through artifact creation - generate everything needed to start implementation in one go. diff --git a/.github/skills/openspec-new-change/SKILL.md b/.github/skills/openspec-new-change/SKILL.md index 53d96b99..607391aa 100644 --- a/.github/skills/openspec-new-change/SKILL.md +++ b/.github/skills/openspec-new-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Start a new change using the experimental artifact-driven approach. diff --git a/.github/skills/openspec-onboard/SKILL.md b/.github/skills/openspec-onboard/SKILL.md index 40080aa1..9076b5d8 100644 --- a/.github/skills/openspec-onboard/SKILL.md +++ b/.github/skills/openspec-onboard/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step. @@ -15,16 +15,19 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t ## Preflight -Before starting, check if OpenSpec is initialized: +Before starting, check if the OpenSpec CLI is installed: ```bash -openspec status --json 2>&1 || echo "NOT_INITIALIZED" +# Unix/macOS +openspec --version 2>&1 || echo "CLI_NOT_INSTALLED" +# Windows (PowerShell) +# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" } ``` -**If not initialized:** -> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`. +**If CLI not installed:** +> OpenSpec CLI is not installed. Install it first, then come back to `/opsx:onboard`. -Stop here if not initialized. +Stop here if not installed. --- @@ -67,7 +70,10 @@ Scan the codebase for small improvement opportunities. Look for: Also check recent git activity: ```bash +# Unix/macOS git log --oneline -10 2>/dev/null || echo "No git history" +# Windows (PowerShell) +# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" } ``` ### Present Suggestions @@ -262,7 +268,10 @@ For a small task like this, we might only need one spec file. **DO:** Create the spec file: ```bash +# Unix/macOS mkdir -p openspec/changes//specs/ +# Windows (PowerShell) +# New-Item -ItemType Directory -Force -Path "openspec/changes//specs/" ``` Draft the spec content: @@ -457,21 +466,29 @@ This same rhythm works for any size change—a small fix or a major feature. ## Command Reference +**Core workflow:** + | Command | What it does | |---------|--------------| +| `/opsx:propose` | Create a change and generate all artifacts | | `/opsx:explore` | Think through problems before/during work | -| `/opsx:new` | Start a new change, step through artifacts | -| `/opsx:ff` | Fast-forward: create all artifacts at once | -| `/opsx:continue` | Continue working on an existing change | | `/opsx:apply` | Implement tasks from a change | -| `/opsx:verify` | Verify implementation matches artifacts | | `/opsx:archive` | Archive a completed change | +**Additional commands:** + +| Command | What it does | +|---------|--------------| +| `/opsx:new` | Start a new change, step through artifacts one at a time | +| `/opsx:continue` | Continue working on an existing change | +| `/opsx:ff` | Fast-forward: create all artifacts at once | +| `/opsx:verify` | Verify implementation matches artifacts | + --- ## What's Next? -Try `/opsx:new` or `/opsx:ff` on something you actually want to build. You've got the rhythm now! +Try `/opsx:propose` on something you actually want to build. You've got the rhythm now! ``` --- @@ -501,17 +518,25 @@ If the user says they just want to see the commands or skip the tutorial: ``` ## OpenSpec Quick Reference +**Core workflow:** + | Command | What it does | |---------|--------------| +| `/opsx:propose ` | Create a change and generate all artifacts | | `/opsx:explore` | Think through problems (no code changes) | +| `/opsx:apply ` | Implement tasks | +| `/opsx:archive ` | Archive when done | + +**Additional commands:** + +| Command | What it does | +|---------|--------------| | `/opsx:new ` | Start a new change, step by step | -| `/opsx:ff ` | Fast-forward: all artifacts at once | | `/opsx:continue ` | Continue an existing change | -| `/opsx:apply ` | Implement tasks | +| `/opsx:ff ` | Fast-forward: all artifacts at once | | `/opsx:verify ` | Verify implementation | -| `/opsx:archive ` | Archive when done | -Try `/opsx:new` to start your first change, or `/opsx:ff` if you want to move fast. +Try `/opsx:propose` to start your first change. ``` Exit gracefully. diff --git a/.github/skills/openspec-sync-specs/SKILL.md b/.github/skills/openspec-sync-specs/SKILL.md index 632681c7..353bfac9 100644 --- a/.github/skills/openspec-sync-specs/SKILL.md +++ b/.github/skills/openspec-sync-specs/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Sync delta specs from a change to main specs. diff --git a/.github/skills/openspec-verify-change/SKILL.md b/.github/skills/openspec-verify-change/SKILL.md index 21cbc508..744a0883 100644 --- a/.github/skills/openspec-verify-change/SKILL.md +++ b/.github/skills/openspec-verify-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Verify that an implementation matches the change artifacts (specs, tasks, design). diff --git a/.opencode/command/opsx-apply.md b/.opencode/command/opsx-apply.md index 89fb9ed4..94b8c1ee 100644 --- a/.opencode/command/opsx-apply.md +++ b/.opencode/command/opsx-apply.md @@ -4,7 +4,7 @@ description: Implement tasks from an OpenSpec change (Experimental) Implement tasks from an OpenSpec change. -**Input**: Optionally specify a change name (e.g., `/opsx:apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. +**Input**: Optionally specify a change name (e.g., `/opsx-apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** @@ -15,7 +15,7 @@ Implement tasks from an OpenSpec change. - Auto-select if only one active change exists - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select - Always announce: "Using change: " and how to override (e.g., `/opsx:apply `). + Always announce: "Using change: " and how to override (e.g., `/opsx-apply `). 2. **Check status to understand the schema** ```bash @@ -38,7 +38,7 @@ Implement tasks from an OpenSpec change. - Dynamic instruction based on current state **Handle states:** - - If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue` + - If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx-continue` - If `state: "all_done"`: congratulate, suggest archive - Otherwise: proceed to implementation @@ -108,7 +108,7 @@ Working on task 4/7: - [x] Task 2 ... -All tasks complete! Ready to archive this change. +All tasks complete! You can archive this change with `/opsx-archive`. ``` **Output On Pause (Issue Encountered)** diff --git a/.opencode/command/opsx-archive.md b/.opencode/command/opsx-archive.md index 4e2ee189..2bd807a7 100644 --- a/.opencode/command/opsx-archive.md +++ b/.opencode/command/opsx-archive.md @@ -4,7 +4,7 @@ description: Archive a completed change in the experimental workflow Archive a completed change in the experimental workflow. -**Input**: Optionally specify a change name after `/opsx:archive` (e.g., `/opsx:archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. +**Input**: Optionally specify a change name after `/opsx-archive` (e.g., `/opsx-archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** @@ -56,7 +56,7 @@ Archive a completed change in the experimental workflow. - If changes needed: "Sync now (recommended)", "Archive without syncing" - If already synced: "Archive now", "Sync anyway", "Cancel" - If user chooses sync, execute `/opsx:sync` logic. Proceed to archive regardless of choice. + If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''. Delta spec analysis: "). Proceed to archive regardless of choice. 5. **Perform the archive** @@ -150,5 +150,5 @@ Target archive directory already exists. - Don't block archive on warnings - just inform and confirm - Preserve .openspec.yaml when moving to archive (it moves with the directory) - Show clear summary of what happened -- If sync is requested, use /opsx:sync approach (agent-driven) +- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven) - If delta specs exist, always run the sync assessment and show the combined summary before prompting diff --git a/.opencode/command/opsx-bulk-archive.md b/.opencode/command/opsx-bulk-archive.md index f8e773fe..be3f9019 100644 --- a/.opencode/command/opsx-bulk-archive.md +++ b/.opencode/command/opsx-bulk-archive.md @@ -222,7 +222,7 @@ Failed K changes: ``` ## No Changes to Archive -No active changes found. Use `/opsx:new` to create a new change. +No active changes found. Create a new change to get started. ``` **Guardrails** diff --git a/.opencode/command/opsx-continue.md b/.opencode/command/opsx-continue.md index f91ec4bc..1a648109 100644 --- a/.opencode/command/opsx-continue.md +++ b/.opencode/command/opsx-continue.md @@ -4,7 +4,7 @@ description: Continue working on a change - create the next artifact (Experiment Continue working on a change by creating the next artifact. -**Input**: Optionally specify a change name after `/opsx:continue` (e.g., `/opsx:continue add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. +**Input**: Optionally specify a change name after `/opsx-continue` (e.g., `/opsx-continue add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** @@ -38,7 +38,7 @@ Continue working on a change by creating the next artifact. **If all artifacts are complete (`isComplete: true`)**: - Congratulate the user - Show final status including the schema used - - Suggest: "All artifacts created! You can now implement this change or archive it." + - Suggest: "All artifacts created! You can now implement this change with `/opsx-apply` or archive it with `/opsx-archive`." - STOP --- @@ -82,7 +82,7 @@ After each invocation, show: - Schema workflow being used - Current progress (N/M complete) - What artifacts are now unlocked -- Prompt: "Run `/opsx:continue` to create the next artifact" +- Prompt: "Run `/opsx-continue` to create the next artifact" **Artifact Creation Guidelines** diff --git a/.opencode/command/opsx-explore.md b/.opencode/command/opsx-explore.md index fd588622..1d542150 100644 --- a/.opencode/command/opsx-explore.md +++ b/.opencode/command/opsx-explore.md @@ -4,11 +4,11 @@ description: Enter explore mode - think through ideas, investigate problems, cla Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. -**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first (e.g., start a change with `/opsx:new` or `/opsx:ff`). You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. +**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. **This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore. -**Input**: The argument after `/opsx:explore` is whatever the user wants to think about. Could be: +**Input**: The argument after `/opsx-explore` is whatever the user wants to think about. Could be: - A vague idea: "real-time collaboration" - A specific problem: "the auth system is getting unwieldy" - A change name: "add-dark-mode" (to explore in context of that change) @@ -97,8 +97,7 @@ If the user mentioned a specific change name, read its artifacts for context. Think freely. When insights crystallize, you might offer: -- "This feels solid enough to start a change. Want me to create one?" - → Can transition to `/opsx:new` or `/opsx:ff` +- "This feels solid enough to start a change. Want me to create a proposal?" - Or keep exploring - no pressure to formalize ### When a change exists @@ -150,7 +149,7 @@ If the user mentions a change or you detect one is relevant: There's no required ending. Discovery might: -- **Flow into action**: "Ready to start? `/opsx:new` or `/opsx:ff`" +- **Flow into a proposal**: "Ready to start? I can create a change proposal." - **Result in artifact updates**: "Updated design.md with these decisions" - **Just provide clarity**: User has what they need, moves on - **Continue later**: "We can pick this up anytime" diff --git a/.opencode/command/opsx-ff.md b/.opencode/command/opsx-ff.md index 6b3dc00c..b7736704 100644 --- a/.opencode/command/opsx-ff.md +++ b/.opencode/command/opsx-ff.md @@ -4,7 +4,7 @@ description: Create a change and generate all artifacts needed for implementatio Fast-forward through artifact creation - generate everything needed to start implementation. -**Input**: The argument after `/opsx:ff` is the change name (kebab-case), OR a description of what the user wants to build. +**Input**: The argument after `/opsx-ff` is the change name (kebab-case), OR a description of what the user wants to build. **Steps** @@ -74,14 +74,17 @@ After completing all artifacts, summarize: - Change name and location - List of artifacts created with brief descriptions - What's ready: "All artifacts created! Ready for implementation." -- Prompt: "Run `/opsx:apply` to start implementing." +- Prompt: "Run `/opsx-apply` to start implementing." **Artifact Creation Guidelines** - Follow the `instruction` field from `openspec instructions` for each artifact type - The schema defines what each artifact should contain - follow it - Read dependency artifacts for context before creating new ones -- Use the `template` as a starting point, filling in based on context +- Use `template` as the structure for your output file - fill in its sections +- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file + - Do NOT copy ``, ``, `` blocks into the artifact + - These guide what you write, but should never appear in the output **Guardrails** - Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`) diff --git a/.opencode/command/opsx-new.md b/.opencode/command/opsx-new.md index ec2253d9..0f30abd7 100644 --- a/.opencode/command/opsx-new.md +++ b/.opencode/command/opsx-new.md @@ -4,7 +4,7 @@ description: Start a new change using the experimental artifact workflow (OPSX) Start a new change using the experimental artifact-driven approach. -**Input**: The argument after `/opsx:new` is the change name (kebab-case), OR a description of what the user wants to build. +**Input**: The argument after `/opsx-new` is the change name (kebab-case), OR a description of what the user wants to build. **Steps** @@ -56,11 +56,11 @@ After completing the steps, summarize: - Schema/workflow being used and its artifact sequence - Current status (0/N artifacts complete) - The template for the first artifact -- Prompt: "Ready to create the first artifact? Run `/opsx:continue` or just describe what this change is about and I'll draft it." +- Prompt: "Ready to create the first artifact? Run `/opsx-continue` or just describe what this change is about and I'll draft it." **Guardrails** - Do NOT create any artifacts yet - just show the instructions - Do NOT advance beyond showing the first artifact template - If the name is invalid (not kebab-case), ask for a valid name -- If a change with that name already exists, suggest using `/opsx:continue` instead +- If a change with that name already exists, suggest using `/opsx-continue` instead - Pass --schema if using a non-default workflow diff --git a/.opencode/command/opsx-onboard.md b/.opencode/command/opsx-onboard.md index 1414f1e1..68abef4c 100644 --- a/.opencode/command/opsx-onboard.md +++ b/.opencode/command/opsx-onboard.md @@ -8,16 +8,19 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t ## Preflight -Before starting, check if OpenSpec is initialized: +Before starting, check if the OpenSpec CLI is installed: ```bash -openspec status --json 2>&1 || echo "NOT_INITIALIZED" +# Unix/macOS +openspec --version 2>&1 || echo "CLI_NOT_INSTALLED" +# Windows (PowerShell) +# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" } ``` -**If not initialized:** -> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`. +**If CLI not installed:** +> OpenSpec CLI is not installed. Install it first, then come back to `/opsx-onboard`. -Stop here if not initialized. +Stop here if not installed. --- @@ -60,7 +63,10 @@ Scan the codebase for small improvement opportunities. Look for: Also check recent git activity: ```bash +# Unix/macOS git log --oneline -10 2>/dev/null || echo "No git history" +# Windows (PowerShell) +# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" } ``` ### Present Suggestions @@ -139,7 +145,7 @@ Spend 1-2 minutes investigating the relevant code: │ [Optional: ASCII diagram if helpful] │ └─────────────────────────────────────────┘ -Explore mode (`/opsx:explore`) is for this kind of thinking—investigating before implementing. You can use it anytime you need to think through a problem. +Explore mode (`/opsx-explore`) is for this kind of thinking—investigating before implementing. You can use it anytime you need to think through a problem. Now let's create a change to hold our work. ``` @@ -255,7 +261,10 @@ For a small task like this, we might only need one spec file. **DO:** Create the spec file: ```bash +# Unix/macOS mkdir -p openspec/changes//specs/ +# Windows (PowerShell) +# New-Item -ItemType Directory -Force -Path "openspec/changes//specs/" ``` Draft the spec content: @@ -450,21 +459,29 @@ This same rhythm works for any size change—a small fix or a major feature. ## Command Reference +**Core workflow:** + +| Command | What it does | +|---------|--------------| +| `/opsx-propose` | Create a change and generate all artifacts | +| `/opsx-explore` | Think through problems before/during work | +| `/opsx-apply` | Implement tasks from a change | +| `/opsx-archive` | Archive a completed change | + +**Additional commands:** + | Command | What it does | |---------|--------------| -| `/opsx:explore` | Think through problems before/during work | -| `/opsx:new` | Start a new change, step through artifacts | -| `/opsx:ff` | Fast-forward: create all artifacts at once | -| `/opsx:continue` | Continue working on an existing change | -| `/opsx:apply` | Implement tasks from a change | -| `/opsx:verify` | Verify implementation matches artifacts | -| `/opsx:archive` | Archive a completed change | +| `/opsx-new` | Start a new change, step through artifacts one at a time | +| `/opsx-continue` | Continue working on an existing change | +| `/opsx-ff` | Fast-forward: create all artifacts at once | +| `/opsx-verify` | Verify implementation matches artifacts | --- ## What's Next? -Try `/opsx:new` or `/opsx:ff` on something you actually want to build. You've got the rhythm now! +Try `/opsx-propose` on something you actually want to build. You've got the rhythm now! ``` --- @@ -479,8 +496,8 @@ If the user says they need to stop, want to pause, or seem disengaged: No problem! Your change is saved at `openspec/changes//`. To pick up where we left off later: -- `/opsx:continue ` - Resume artifact creation -- `/opsx:apply ` - Jump to implementation (if tasks exist) +- `/opsx-continue ` - Resume artifact creation +- `/opsx-apply ` - Jump to implementation (if tasks exist) The work won't be lost. Come back whenever you're ready. ``` @@ -494,17 +511,25 @@ If the user says they just want to see the commands or skip the tutorial: ``` ## OpenSpec Quick Reference +**Core workflow:** + | Command | What it does | |---------|--------------| -| `/opsx:explore` | Think through problems (no code changes) | -| `/opsx:new ` | Start a new change, step by step | -| `/opsx:ff ` | Fast-forward: all artifacts at once | -| `/opsx:continue ` | Continue an existing change | -| `/opsx:apply ` | Implement tasks | -| `/opsx:verify ` | Verify implementation | -| `/opsx:archive ` | Archive when done | - -Try `/opsx:new` to start your first change, or `/opsx:ff` if you want to move fast. +| `/opsx-propose ` | Create a change and generate all artifacts | +| `/opsx-explore` | Think through problems (no code changes) | +| `/opsx-apply ` | Implement tasks | +| `/opsx-archive ` | Archive when done | + +**Additional commands:** + +| Command | What it does | +|---------|--------------| +| `/opsx-new ` | Start a new change, step by step | +| `/opsx-continue ` | Continue an existing change | +| `/opsx-ff ` | Fast-forward: all artifacts at once | +| `/opsx-verify ` | Verify implementation | + +Try `/opsx-propose` to start your first change. ``` Exit gracefully. diff --git a/.opencode/command/opsx-sync.md b/.opencode/command/opsx-sync.md index 56b5b33c..1208c4b4 100644 --- a/.opencode/command/opsx-sync.md +++ b/.opencode/command/opsx-sync.md @@ -6,7 +6,7 @@ Sync delta specs from a change to main specs. This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement). -**Input**: Optionally specify a change name after `/opsx:sync` (e.g., `/opsx:sync add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. +**Input**: Optionally specify a change name after `/opsx-sync` (e.g., `/opsx-sync add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** diff --git a/.opencode/command/opsx-verify.md b/.opencode/command/opsx-verify.md index 81118734..7bd3abab 100644 --- a/.opencode/command/opsx-verify.md +++ b/.opencode/command/opsx-verify.md @@ -4,7 +4,7 @@ description: Verify implementation matches change artifacts before archiving Verify that an implementation matches the change artifacts (specs, tasks, design). -**Input**: Optionally specify a change name after `/opsx:verify` (e.g., `/opsx:verify add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. +**Input**: Optionally specify a change name after `/opsx-verify` (e.g., `/opsx-verify add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** diff --git a/.opencode/skills/openspec-apply-change/SKILL.md b/.opencode/skills/openspec-apply-change/SKILL.md index bc95df43..9f31f2c2 100644 --- a/.opencode/skills/openspec-apply-change/SKILL.md +++ b/.opencode/skills/openspec-apply-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Implement tasks from an OpenSpec change. @@ -22,7 +22,7 @@ Implement tasks from an OpenSpec change. - Auto-select if only one active change exists - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select - Always announce: "Using change: " and how to override (e.g., `/opsx:apply `). + Always announce: "Using change: " and how to override (e.g., `/opsx-apply `). 2. **Check status to understand the schema** ```bash diff --git a/.opencode/skills/openspec-archive-change/SKILL.md b/.opencode/skills/openspec-archive-change/SKILL.md index 9ea63e8a..9b1f851a 100644 --- a/.opencode/skills/openspec-archive-change/SKILL.md +++ b/.opencode/skills/openspec-archive-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Archive a completed change in the experimental workflow. @@ -63,7 +63,7 @@ Archive a completed change in the experimental workflow. - If changes needed: "Sync now (recommended)", "Archive without syncing" - If already synced: "Archive now", "Sync anyway", "Cancel" - If user chooses sync, execute /opsx:sync logic (use the openspec-sync-specs skill). Proceed to archive regardless of choice. + If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''. Delta spec analysis: "). Proceed to archive regardless of choice. 5. **Perform the archive** diff --git a/.opencode/skills/openspec-bulk-archive-change/SKILL.md b/.opencode/skills/openspec-bulk-archive-change/SKILL.md index 5ce056a9..d2f199af 100644 --- a/.opencode/skills/openspec-bulk-archive-change/SKILL.md +++ b/.opencode/skills/openspec-bulk-archive-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Archive multiple completed changes in a single operation. @@ -229,7 +229,7 @@ Failed K changes: ``` ## No Changes to Archive -No active changes found. Use `/opsx:new` to create a new change. +No active changes found. Create a new change to get started. ``` **Guardrails** diff --git a/.opencode/skills/openspec-continue-change/SKILL.md b/.opencode/skills/openspec-continue-change/SKILL.md index 79aaac48..a2856f04 100644 --- a/.opencode/skills/openspec-continue-change/SKILL.md +++ b/.opencode/skills/openspec-continue-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Continue working on a change by creating the next artifact. diff --git a/.opencode/skills/openspec-explore/SKILL.md b/.opencode/skills/openspec-explore/SKILL.md index 49d051da..2510ac44 100644 --- a/.opencode/skills/openspec-explore/SKILL.md +++ b/.opencode/skills/openspec-explore/SKILL.md @@ -6,12 +6,12 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. -**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first (e.g., start a change with `/opsx:new` or `/opsx:ff`). You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. +**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. **This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore. @@ -95,8 +95,7 @@ This tells you: Think freely. When insights crystallize, you might offer: -- "This feels solid enough to start a change. Want me to create one?" - → Can transition to `/opsx:new` or `/opsx:ff` +- "This feels solid enough to start a change. Want me to create a proposal?" - Or keep exploring - no pressure to formalize ### When a change exists @@ -202,7 +201,7 @@ You: [reads codebase] **User is stuck mid-implementation:** ``` -User: /opsx:explore add-auth-system +User: /opsx-explore add-auth-system The OAuth integration is more complex than expected You: [reads change artifacts] @@ -252,7 +251,7 @@ You: That changes everything. There's no required ending. Discovery might: -- **Flow into action**: "Ready to start? /opsx:new or /opsx:ff" +- **Flow into a proposal**: "Ready to start? I can create a change proposal." - **Result in artifact updates**: "Updated design.md with these decisions" - **Just provide clarity**: User has what they need, moves on - **Continue later**: "We can pick this up anytime" @@ -269,8 +268,7 @@ When it feels like things are crystallizing, you might summarize: **Open questions**: [if any remain] **Next steps** (if ready): -- Create a change: /opsx:new -- Fast-forward to tasks: /opsx:ff +- Create a change proposal - Keep exploring: just keep talking ``` diff --git a/.opencode/skills/openspec-ff-change/SKILL.md b/.opencode/skills/openspec-ff-change/SKILL.md index 64f058ca..1efd60c9 100644 --- a/.opencode/skills/openspec-ff-change/SKILL.md +++ b/.opencode/skills/openspec-ff-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Fast-forward through artifact creation - generate everything needed to start implementation in one go. @@ -81,7 +81,7 @@ After completing all artifacts, summarize: - Change name and location - List of artifacts created with brief descriptions - What's ready: "All artifacts created! Ready for implementation." -- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks." +- Prompt: "Run `/opsx-apply` or ask me to implement to start working on the tasks." **Artifact Creation Guidelines** diff --git a/.opencode/skills/openspec-new-change/SKILL.md b/.opencode/skills/openspec-new-change/SKILL.md index 53d96b99..607391aa 100644 --- a/.opencode/skills/openspec-new-change/SKILL.md +++ b/.opencode/skills/openspec-new-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Start a new change using the experimental artifact-driven approach. diff --git a/.opencode/skills/openspec-onboard/SKILL.md b/.opencode/skills/openspec-onboard/SKILL.md index 40080aa1..e470c603 100644 --- a/.opencode/skills/openspec-onboard/SKILL.md +++ b/.opencode/skills/openspec-onboard/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step. @@ -15,16 +15,19 @@ Guide the user through their first complete OpenSpec workflow cycle. This is a t ## Preflight -Before starting, check if OpenSpec is initialized: +Before starting, check if the OpenSpec CLI is installed: ```bash -openspec status --json 2>&1 || echo "NOT_INITIALIZED" +# Unix/macOS +openspec --version 2>&1 || echo "CLI_NOT_INSTALLED" +# Windows (PowerShell) +# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" } ``` -**If not initialized:** -> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`. +**If CLI not installed:** +> OpenSpec CLI is not installed. Install it first, then come back to `/opsx-onboard`. -Stop here if not initialized. +Stop here if not installed. --- @@ -67,7 +70,10 @@ Scan the codebase for small improvement opportunities. Look for: Also check recent git activity: ```bash +# Unix/macOS git log --oneline -10 2>/dev/null || echo "No git history" +# Windows (PowerShell) +# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" } ``` ### Present Suggestions @@ -146,7 +152,7 @@ Spend 1-2 minutes investigating the relevant code: │ [Optional: ASCII diagram if helpful] │ └─────────────────────────────────────────┘ -Explore mode (`/opsx:explore`) is for this kind of thinking—investigating before implementing. You can use it anytime you need to think through a problem. +Explore mode (`/opsx-explore`) is for this kind of thinking—investigating before implementing. You can use it anytime you need to think through a problem. Now let's create a change to hold our work. ``` @@ -262,7 +268,10 @@ For a small task like this, we might only need one spec file. **DO:** Create the spec file: ```bash +# Unix/macOS mkdir -p openspec/changes//specs/ +# Windows (PowerShell) +# New-Item -ItemType Directory -Force -Path "openspec/changes//specs/" ``` Draft the spec content: @@ -457,21 +466,29 @@ This same rhythm works for any size change—a small fix or a major feature. ## Command Reference +**Core workflow:** + +| Command | What it does | +|---------|--------------| +| `/opsx-propose` | Create a change and generate all artifacts | +| `/opsx-explore` | Think through problems before/during work | +| `/opsx-apply` | Implement tasks from a change | +| `/opsx-archive` | Archive a completed change | + +**Additional commands:** + | Command | What it does | |---------|--------------| -| `/opsx:explore` | Think through problems before/during work | -| `/opsx:new` | Start a new change, step through artifacts | -| `/opsx:ff` | Fast-forward: create all artifacts at once | -| `/opsx:continue` | Continue working on an existing change | -| `/opsx:apply` | Implement tasks from a change | -| `/opsx:verify` | Verify implementation matches artifacts | -| `/opsx:archive` | Archive a completed change | +| `/opsx-new` | Start a new change, step through artifacts one at a time | +| `/opsx-continue` | Continue working on an existing change | +| `/opsx-ff` | Fast-forward: create all artifacts at once | +| `/opsx-verify` | Verify implementation matches artifacts | --- ## What's Next? -Try `/opsx:new` or `/opsx:ff` on something you actually want to build. You've got the rhythm now! +Try `/opsx-propose` on something you actually want to build. You've got the rhythm now! ``` --- @@ -486,8 +503,8 @@ If the user says they need to stop, want to pause, or seem disengaged: No problem! Your change is saved at `openspec/changes//`. To pick up where we left off later: -- `/opsx:continue ` - Resume artifact creation -- `/opsx:apply ` - Jump to implementation (if tasks exist) +- `/opsx-continue ` - Resume artifact creation +- `/opsx-apply ` - Jump to implementation (if tasks exist) The work won't be lost. Come back whenever you're ready. ``` @@ -501,17 +518,25 @@ If the user says they just want to see the commands or skip the tutorial: ``` ## OpenSpec Quick Reference +**Core workflow:** + | Command | What it does | |---------|--------------| -| `/opsx:explore` | Think through problems (no code changes) | -| `/opsx:new ` | Start a new change, step by step | -| `/opsx:ff ` | Fast-forward: all artifacts at once | -| `/opsx:continue ` | Continue an existing change | -| `/opsx:apply ` | Implement tasks | -| `/opsx:verify ` | Verify implementation | -| `/opsx:archive ` | Archive when done | - -Try `/opsx:new` to start your first change, or `/opsx:ff` if you want to move fast. +| `/opsx-propose ` | Create a change and generate all artifacts | +| `/opsx-explore` | Think through problems (no code changes) | +| `/opsx-apply ` | Implement tasks | +| `/opsx-archive ` | Archive when done | + +**Additional commands:** + +| Command | What it does | +|---------|--------------| +| `/opsx-new ` | Start a new change, step by step | +| `/opsx-continue ` | Continue an existing change | +| `/opsx-ff ` | Fast-forward: all artifacts at once | +| `/opsx-verify ` | Verify implementation | + +Try `/opsx-propose` to start your first change. ``` Exit gracefully. diff --git a/.opencode/skills/openspec-sync-specs/SKILL.md b/.opencode/skills/openspec-sync-specs/SKILL.md index 632681c7..353bfac9 100644 --- a/.opencode/skills/openspec-sync-specs/SKILL.md +++ b/.opencode/skills/openspec-sync-specs/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Sync delta specs from a change to main specs. diff --git a/.opencode/skills/openspec-verify-change/SKILL.md b/.opencode/skills/openspec-verify-change/SKILL.md index 21cbc508..744a0883 100644 --- a/.opencode/skills/openspec-verify-change/SKILL.md +++ b/.opencode/skills/openspec-verify-change/SKILL.md @@ -6,7 +6,7 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.0.2" + generatedBy: "1.2.0" --- Verify that an implementation matches the change artifacts (specs, tasks, design). diff --git a/openspec/changes/add-biome-prettier-linting/.openspec.yaml b/openspec/changes/archive/2026-04-01-add-biome-prettier-linting/.openspec.yaml similarity index 100% rename from openspec/changes/add-biome-prettier-linting/.openspec.yaml rename to openspec/changes/archive/2026-04-01-add-biome-prettier-linting/.openspec.yaml diff --git a/openspec/changes/add-biome-prettier-linting/design.md b/openspec/changes/archive/2026-04-01-add-biome-prettier-linting/design.md similarity index 100% rename from openspec/changes/add-biome-prettier-linting/design.md rename to openspec/changes/archive/2026-04-01-add-biome-prettier-linting/design.md diff --git a/openspec/changes/add-biome-prettier-linting/proposal.md b/openspec/changes/archive/2026-04-01-add-biome-prettier-linting/proposal.md similarity index 100% rename from openspec/changes/add-biome-prettier-linting/proposal.md rename to openspec/changes/archive/2026-04-01-add-biome-prettier-linting/proposal.md diff --git a/openspec/changes/add-biome-prettier-linting/specs/biome-linting/spec.md b/openspec/changes/archive/2026-04-01-add-biome-prettier-linting/specs/biome-linting/spec.md similarity index 100% rename from openspec/changes/add-biome-prettier-linting/specs/biome-linting/spec.md rename to openspec/changes/archive/2026-04-01-add-biome-prettier-linting/specs/biome-linting/spec.md diff --git a/openspec/changes/add-biome-prettier-linting/specs/lint-staged-integration/spec.md b/openspec/changes/archive/2026-04-01-add-biome-prettier-linting/specs/lint-staged-integration/spec.md similarity index 100% rename from openspec/changes/add-biome-prettier-linting/specs/lint-staged-integration/spec.md rename to openspec/changes/archive/2026-04-01-add-biome-prettier-linting/specs/lint-staged-integration/spec.md diff --git a/openspec/changes/add-biome-prettier-linting/tasks.md b/openspec/changes/archive/2026-04-01-add-biome-prettier-linting/tasks.md similarity index 100% rename from openspec/changes/add-biome-prettier-linting/tasks.md rename to openspec/changes/archive/2026-04-01-add-biome-prettier-linting/tasks.md diff --git a/openspec/changes/compound-scoped-selectors/.openspec.yaml b/openspec/changes/archive/2026-04-01-compound-scoped-selectors/.openspec.yaml similarity index 100% rename from openspec/changes/compound-scoped-selectors/.openspec.yaml rename to openspec/changes/archive/2026-04-01-compound-scoped-selectors/.openspec.yaml diff --git a/openspec/changes/compound-scoped-selectors/design.md b/openspec/changes/archive/2026-04-01-compound-scoped-selectors/design.md similarity index 100% rename from openspec/changes/compound-scoped-selectors/design.md rename to openspec/changes/archive/2026-04-01-compound-scoped-selectors/design.md diff --git a/openspec/changes/compound-scoped-selectors/proposal.md b/openspec/changes/archive/2026-04-01-compound-scoped-selectors/proposal.md similarity index 100% rename from openspec/changes/compound-scoped-selectors/proposal.md rename to openspec/changes/archive/2026-04-01-compound-scoped-selectors/proposal.md diff --git a/openspec/changes/compound-scoped-selectors/specs/component-theming/spec.md b/openspec/changes/archive/2026-04-01-compound-scoped-selectors/specs/component-theming/spec.md similarity index 100% rename from openspec/changes/compound-scoped-selectors/specs/component-theming/spec.md rename to openspec/changes/archive/2026-04-01-compound-scoped-selectors/specs/component-theming/spec.md diff --git a/openspec/changes/compound-scoped-selectors/tasks.md b/openspec/changes/archive/2026-04-01-compound-scoped-selectors/tasks.md similarity index 100% rename from openspec/changes/compound-scoped-selectors/tasks.md rename to openspec/changes/archive/2026-04-01-compound-scoped-selectors/tasks.md diff --git a/openspec/changes/generic-platform-detection/.openspec.yaml b/openspec/changes/archive/2026-04-01-generic-platform-detection/.openspec.yaml similarity index 100% rename from openspec/changes/generic-platform-detection/.openspec.yaml rename to openspec/changes/archive/2026-04-01-generic-platform-detection/.openspec.yaml diff --git a/openspec/changes/generic-platform-detection/design.md b/openspec/changes/archive/2026-04-01-generic-platform-detection/design.md similarity index 100% rename from openspec/changes/generic-platform-detection/design.md rename to openspec/changes/archive/2026-04-01-generic-platform-detection/design.md diff --git a/openspec/changes/generic-platform-detection/proposal.md b/openspec/changes/archive/2026-04-01-generic-platform-detection/proposal.md similarity index 100% rename from openspec/changes/generic-platform-detection/proposal.md rename to openspec/changes/archive/2026-04-01-generic-platform-detection/proposal.md diff --git a/openspec/changes/generic-platform-detection/specs/component-theming/spec.md b/openspec/changes/archive/2026-04-01-generic-platform-detection/specs/component-theming/spec.md similarity index 100% rename from openspec/changes/generic-platform-detection/specs/component-theming/spec.md rename to openspec/changes/archive/2026-04-01-generic-platform-detection/specs/component-theming/spec.md diff --git a/openspec/changes/generic-platform-detection/specs/generic-platform/spec.md b/openspec/changes/archive/2026-04-01-generic-platform-detection/specs/generic-platform/spec.md similarity index 100% rename from openspec/changes/generic-platform-detection/specs/generic-platform/spec.md rename to openspec/changes/archive/2026-04-01-generic-platform-detection/specs/generic-platform/spec.md diff --git a/openspec/changes/generic-platform-detection/specs/layout-overrides/spec.md b/openspec/changes/archive/2026-04-01-generic-platform-detection/specs/layout-overrides/spec.md similarity index 100% rename from openspec/changes/generic-platform-detection/specs/layout-overrides/spec.md rename to openspec/changes/archive/2026-04-01-generic-platform-detection/specs/layout-overrides/spec.md diff --git a/openspec/changes/generic-platform-detection/specs/platform-detection/spec.md b/openspec/changes/archive/2026-04-01-generic-platform-detection/specs/platform-detection/spec.md similarity index 100% rename from openspec/changes/generic-platform-detection/specs/platform-detection/spec.md rename to openspec/changes/archive/2026-04-01-generic-platform-detection/specs/platform-detection/spec.md diff --git a/openspec/changes/generic-platform-detection/specs/theme-generation/spec.md b/openspec/changes/archive/2026-04-01-generic-platform-detection/specs/theme-generation/spec.md similarity index 100% rename from openspec/changes/generic-platform-detection/specs/theme-generation/spec.md rename to openspec/changes/archive/2026-04-01-generic-platform-detection/specs/theme-generation/spec.md diff --git a/openspec/changes/generic-platform-detection/tasks.md b/openspec/changes/archive/2026-04-01-generic-platform-detection/tasks.md similarity index 100% rename from openspec/changes/generic-platform-detection/tasks.md rename to openspec/changes/archive/2026-04-01-generic-platform-detection/tasks.md diff --git a/openspec/changes/mcp-color-intelligence/.openspec.yaml b/openspec/changes/archive/2026-04-01-mcp-color-intelligence/.openspec.yaml similarity index 100% rename from openspec/changes/mcp-color-intelligence/.openspec.yaml rename to openspec/changes/archive/2026-04-01-mcp-color-intelligence/.openspec.yaml diff --git a/openspec/changes/mcp-color-intelligence/design.md b/openspec/changes/archive/2026-04-01-mcp-color-intelligence/design.md similarity index 100% rename from openspec/changes/mcp-color-intelligence/design.md rename to openspec/changes/archive/2026-04-01-mcp-color-intelligence/design.md diff --git a/openspec/changes/mcp-color-intelligence/proposal.md b/openspec/changes/archive/2026-04-01-mcp-color-intelligence/proposal.md similarity index 100% rename from openspec/changes/mcp-color-intelligence/proposal.md rename to openspec/changes/archive/2026-04-01-mcp-color-intelligence/proposal.md diff --git a/openspec/changes/mcp-color-intelligence/specs/spec.md b/openspec/changes/archive/2026-04-01-mcp-color-intelligence/specs/spec.md similarity index 100% rename from openspec/changes/mcp-color-intelligence/specs/spec.md rename to openspec/changes/archive/2026-04-01-mcp-color-intelligence/specs/spec.md diff --git a/openspec/changes/mcp-color-intelligence/tasks.md b/openspec/changes/archive/2026-04-01-mcp-color-intelligence/tasks.md similarity index 100% rename from openspec/changes/mcp-color-intelligence/tasks.md rename to openspec/changes/archive/2026-04-01-mcp-color-intelligence/tasks.md diff --git a/openspec/changes/mcp-prompt-resources/.openspec.yaml b/openspec/changes/archive/2026-04-01-mcp-prompt-resources/.openspec.yaml similarity index 100% rename from openspec/changes/mcp-prompt-resources/.openspec.yaml rename to openspec/changes/archive/2026-04-01-mcp-prompt-resources/.openspec.yaml diff --git a/openspec/changes/mcp-prompt-resources/design.md b/openspec/changes/archive/2026-04-01-mcp-prompt-resources/design.md similarity index 100% rename from openspec/changes/mcp-prompt-resources/design.md rename to openspec/changes/archive/2026-04-01-mcp-prompt-resources/design.md diff --git a/openspec/changes/mcp-prompt-resources/proposal.md b/openspec/changes/archive/2026-04-01-mcp-prompt-resources/proposal.md similarity index 100% rename from openspec/changes/mcp-prompt-resources/proposal.md rename to openspec/changes/archive/2026-04-01-mcp-prompt-resources/proposal.md diff --git a/openspec/changes/mcp-prompt-resources/specs/spec.md b/openspec/changes/archive/2026-04-01-mcp-prompt-resources/specs/spec.md similarity index 100% rename from openspec/changes/mcp-prompt-resources/specs/spec.md rename to openspec/changes/archive/2026-04-01-mcp-prompt-resources/specs/spec.md diff --git a/openspec/changes/mcp-prompt-resources/tasks.md b/openspec/changes/archive/2026-04-01-mcp-prompt-resources/tasks.md similarity index 100% rename from openspec/changes/mcp-prompt-resources/tasks.md rename to openspec/changes/archive/2026-04-01-mcp-prompt-resources/tasks.md diff --git a/openspec/changes/mcp-typography-utilities/.openspec.yaml b/openspec/changes/archive/2026-04-01-mcp-typography-utilities/.openspec.yaml similarity index 100% rename from openspec/changes/mcp-typography-utilities/.openspec.yaml rename to openspec/changes/archive/2026-04-01-mcp-typography-utilities/.openspec.yaml diff --git a/openspec/changes/mcp-typography-utilities/design.md b/openspec/changes/archive/2026-04-01-mcp-typography-utilities/design.md similarity index 100% rename from openspec/changes/mcp-typography-utilities/design.md rename to openspec/changes/archive/2026-04-01-mcp-typography-utilities/design.md diff --git a/openspec/changes/mcp-typography-utilities/proposal.md b/openspec/changes/archive/2026-04-01-mcp-typography-utilities/proposal.md similarity index 100% rename from openspec/changes/mcp-typography-utilities/proposal.md rename to openspec/changes/archive/2026-04-01-mcp-typography-utilities/proposal.md diff --git a/openspec/changes/mcp-typography-utilities/specs/spec.md b/openspec/changes/archive/2026-04-01-mcp-typography-utilities/specs/spec.md similarity index 100% rename from openspec/changes/mcp-typography-utilities/specs/spec.md rename to openspec/changes/archive/2026-04-01-mcp-typography-utilities/specs/spec.md diff --git a/openspec/changes/mcp-typography-utilities/tasks.md b/openspec/changes/archive/2026-04-01-mcp-typography-utilities/tasks.md similarity index 100% rename from openspec/changes/mcp-typography-utilities/tasks.md rename to openspec/changes/archive/2026-04-01-mcp-typography-utilities/tasks.md diff --git a/openspec/changes/mcp-validation-intelligence/.openspec.yaml b/openspec/changes/archive/2026-04-01-mcp-validation-intelligence/.openspec.yaml similarity index 100% rename from openspec/changes/mcp-validation-intelligence/.openspec.yaml rename to openspec/changes/archive/2026-04-01-mcp-validation-intelligence/.openspec.yaml diff --git a/openspec/changes/mcp-validation-intelligence/design.md b/openspec/changes/archive/2026-04-01-mcp-validation-intelligence/design.md similarity index 100% rename from openspec/changes/mcp-validation-intelligence/design.md rename to openspec/changes/archive/2026-04-01-mcp-validation-intelligence/design.md diff --git a/openspec/changes/mcp-validation-intelligence/proposal.md b/openspec/changes/archive/2026-04-01-mcp-validation-intelligence/proposal.md similarity index 100% rename from openspec/changes/mcp-validation-intelligence/proposal.md rename to openspec/changes/archive/2026-04-01-mcp-validation-intelligence/proposal.md diff --git a/openspec/changes/mcp-validation-intelligence/specs/spec.md b/openspec/changes/archive/2026-04-01-mcp-validation-intelligence/specs/spec.md similarity index 100% rename from openspec/changes/mcp-validation-intelligence/specs/spec.md rename to openspec/changes/archive/2026-04-01-mcp-validation-intelligence/specs/spec.md diff --git a/openspec/changes/mcp-validation-intelligence/tasks.md b/openspec/changes/archive/2026-04-01-mcp-validation-intelligence/tasks.md similarity index 100% rename from openspec/changes/mcp-validation-intelligence/tasks.md rename to openspec/changes/archive/2026-04-01-mcp-validation-intelligence/tasks.md diff --git a/openspec/changes/migrate-mcp-docs-to-vite-markdown/.openspec.yaml b/openspec/changes/archive/2026-04-01-migrate-mcp-docs-to-vite-markdown/.openspec.yaml similarity index 100% rename from openspec/changes/migrate-mcp-docs-to-vite-markdown/.openspec.yaml rename to openspec/changes/archive/2026-04-01-migrate-mcp-docs-to-vite-markdown/.openspec.yaml diff --git a/openspec/changes/migrate-mcp-docs-to-vite-markdown/design.md b/openspec/changes/archive/2026-04-01-migrate-mcp-docs-to-vite-markdown/design.md similarity index 100% rename from openspec/changes/migrate-mcp-docs-to-vite-markdown/design.md rename to openspec/changes/archive/2026-04-01-migrate-mcp-docs-to-vite-markdown/design.md diff --git a/openspec/changes/migrate-mcp-docs-to-vite-markdown/proposal.md b/openspec/changes/archive/2026-04-01-migrate-mcp-docs-to-vite-markdown/proposal.md similarity index 100% rename from openspec/changes/migrate-mcp-docs-to-vite-markdown/proposal.md rename to openspec/changes/archive/2026-04-01-migrate-mcp-docs-to-vite-markdown/proposal.md diff --git a/openspec/changes/migrate-mcp-docs-to-vite-markdown/specs/documentation-organization/spec.md b/openspec/changes/archive/2026-04-01-migrate-mcp-docs-to-vite-markdown/specs/documentation-organization/spec.md similarity index 100% rename from openspec/changes/migrate-mcp-docs-to-vite-markdown/specs/documentation-organization/spec.md rename to openspec/changes/archive/2026-04-01-migrate-mcp-docs-to-vite-markdown/specs/documentation-organization/spec.md diff --git a/openspec/changes/migrate-mcp-docs-to-vite-markdown/specs/vite-markdown-imports/spec.md b/openspec/changes/archive/2026-04-01-migrate-mcp-docs-to-vite-markdown/specs/vite-markdown-imports/spec.md similarity index 100% rename from openspec/changes/migrate-mcp-docs-to-vite-markdown/specs/vite-markdown-imports/spec.md rename to openspec/changes/archive/2026-04-01-migrate-mcp-docs-to-vite-markdown/specs/vite-markdown-imports/spec.md diff --git a/openspec/changes/migrate-mcp-docs-to-vite-markdown/tasks.md b/openspec/changes/archive/2026-04-01-migrate-mcp-docs-to-vite-markdown/tasks.md similarity index 100% rename from openspec/changes/migrate-mcp-docs-to-vite-markdown/tasks.md rename to openspec/changes/archive/2026-04-01-migrate-mcp-docs-to-vite-markdown/tasks.md diff --git a/openspec/changes/component-theme-alias/.openspec.yaml b/openspec/changes/archive/2026-04-01-remove-compound-scopes/.openspec.yaml similarity index 50% rename from openspec/changes/component-theme-alias/.openspec.yaml rename to openspec/changes/archive/2026-04-01-remove-compound-scopes/.openspec.yaml index d2997483..0f528039 100644 --- a/openspec/changes/component-theme-alias/.openspec.yaml +++ b/openspec/changes/archive/2026-04-01-remove-compound-scopes/.openspec.yaml @@ -1,2 +1,2 @@ schema: spec-driven -created: 2026-02-19 +created: 2026-04-01 diff --git a/openspec/changes/archive/2026-04-01-remove-compound-scopes/design.md b/openspec/changes/archive/2026-04-01-remove-compound-scopes/design.md new file mode 100644 index 00000000..b138ca71 --- /dev/null +++ b/openspec/changes/archive/2026-04-01-remove-compound-scopes/design.md @@ -0,0 +1,89 @@ +## Context + +Compound components (combo, date-picker, select, etc.) contain child components that need independent theming. The current metadata model uses two fields to handle scoping: + +- `additionalScopes` — maps scope names (e.g., `"overlay"`) to per-platform CSS selectors for DOM contexts outside the host element +- `childScopes` — maps each child theme name to a scope name (`"inline"` or a key from `additionalScopes`) + +This existed because Angular rendered overlay content (drop-downs, date pickers) in a body-level overlay container, outside the host component's DOM. The popover API migration in Ignite UI for Angular eliminated this — all children now render inside the host component's DOM tree. The scoping distinction is dead. + +The current output format repeats platform sections 4 times (Angular, Web Components, React, Blazor) with scope tables and related-themes tables. Most rows are identical across platforms since WC/React/Blazor share selectors, and the "Scope" column adds no value when every child resolves to the parent selector. + +## Goals / Non-Goals + +**Goals:** + +- Remove `additionalScopes`, `childScopes`, and `ScopeSelectors` from the data model +- Simplify `get_component_design_tokens` compound output to reduce token count and improve LLM comprehension +- Preserve all information the LLM needs: child theme list, parent selector per platform, token derivations, guidance +- Keep the change strictly within the metadata and formatting layers — do not touch generators +- Update the raletad MCP tool descriptions to reflect the new format where needed + +**Non-Goals:** + +- Changing how `create_component_theme` works (it has no compound awareness, remains unchanged) +- Modifying token derivation logic or guidance content +- Restructuring the `CompoundInfo` type beyond removing the two scope fields + +## Decisions + +### Decision 1: Collapse compound output to a single platform-aware block + +**Choice:** Replace 4 per-platform sections (each with scope table + related-themes table) with one block: + +``` +**Related themes:** `calendar`, `flat-button`, `input-group` +Scope all related themes under the parent component selector: +- **Angular:** `igx-date-picker` +- **Web Components / React / Blazor:** `igc-date-picker` +``` + +**Rationale:** Every child now scopes under the parent selector. Repeating this per-platform in a 3-column table (Theme | Scope | Selector) is noise — every row has the same Scope and Selector value. The flat format states the rule once and lists platform selectors. This cuts token count substantially for complex compounds like grid (12+ related themes × 4 platforms = 48+ rows → 1 list + 2 lines). + +**Alternative considered:** Keep per-platform tables but remove the Scope column. Rejected because even without Scope, the tables duplicate the same selector in every row. + +### Decision 2: Omit platform lines where the component is unavailable + +**Choice:** Only show selector lines for platforms where the component has a non-null selector entry. + +Example for `time-picker` (Angular-only): + +``` +Scope all related themes under the parent component selector: +- **Angular:** `igx-time-picker` +``` + +**Rationale:** Showing "Web Components: N/A" adds no actionable information. The existing behavior already filters N/A scope rows — we extend that to the simplified format. + +### Decision 3: Group WC/React/Blazor into one line + +**Choice:** Show a single line for "Web Components / React / Blazor" since they all share `igc-` selectors. + +**Rationale:** This mirrors the existing `PLATFORM_GROUPS` pattern where React and Blazor map to `webcomponents` for selector resolution. Rather than 3 lines with identical selectors, one combined line is clearer. The `PLATFORM_GROUPS` constant itself can be removed — the new code just needs to check which platforms have non-null selectors and group WC-based ones. + +### Decision 4: Remove helpers, inline the simplified logic + +**Choice:** Delete `getScopeSelectorForPlatform()`, `resolveChildScopeName()`, and the `PLATFORM_GROUPS` constant. Replace with a small inline block in the compound section that: + +1. Reads `compoundInfo.relatedThemes` as a flat list +2. Calls `getComponentSelector()` for Angular and Web Components to get the parent selectors +3. Formats the output + +**Rationale:** These functions existed to resolve the scope indirection (child → scope name → scope selector). Without that indirection, they're over-abstraction. The replacement is ~15 lines of straightforward logic. + +### Decision 5: Keep Steps section removal + +**Choice:** Remove the numbered "Steps" block from compound output. + +**Rationale:** The steps ("1. Choose your platform...", "2. For each related theme...") were procedural scaffolding. The `create_component_theme` tool description already explains the workflow. The compound output should provide data (which themes, which selectors, which derivations), not repeat process instructions. + +## Risks / Trade-offs + +**[Spec output format change] → May affect LLM behavior for compound theming** +The output format change is intentionally simpler, but LLMs that learned patterns from the old per-platform tables might initially produce slightly different code structure. Mitigation: the `create_component_theme` tool description and its `selector` parameter documentation remain unchanged, so the LLM still knows how to pass selectors. The new format makes the correct selector more obvious, not less. + +**[Breaking metadata interface] → External consumers of CompoundInfo type** +If any code outside this repo imports `CompoundInfo` and reads `additionalScopes`/`childScopes`, it will break. Mitigation: the `igniteui-theming` package's public API does not expose `CompoundInfo` — it's internal to the MCP server. The only consumer is `component-tokens.ts`. + +**[Reduced output detail] → Loss of per-child scope specificity** +Before: each child had an explicit scope assignment. After: all children implicitly scope under the parent. If a future Angular version re-introduces overlay rendering for some component, we'd need to re-add scope machinery. Mitigation: this is unlikely given the direction toward popover API. If needed, the old code is in git history and the architecture supports re-adding the fields. diff --git a/openspec/changes/archive/2026-04-01-remove-compound-scopes/proposal.md b/openspec/changes/archive/2026-04-01-remove-compound-scopes/proposal.md new file mode 100644 index 00000000..ceca89e9 --- /dev/null +++ b/openspec/changes/archive/2026-04-01-remove-compound-scopes/proposal.md @@ -0,0 +1,32 @@ +## Why + +Ignite UI for Angular previously rendered floating elements (drop-downs, date pickers, time pickers) in an overlay container outside the host component's DOM tree. This forced compound component metadata to declare `additionalScopes` and `childScopes` so the MCP server could tell LLMs to scope child themes under overlay-specific selectors. The latest Angular version uses the popover API instead, keeping all content in the host DOM. The extra scope machinery is now dead complexity — confusing for both humans writing themes manually and for LLMs generating them. + +## What Changes + +- **BREAKING** Remove the `additionalScopes` field from `CompoundInfo` interface and all 6 component entries that use it (combo, simple-combo, date-picker, date-range-picker, select, time-picker) +- **BREAKING** Remove the `childScopes` field from `CompoundInfo` interface and the same 6 component entries +- Remove the `ScopeSelectors` interface (only used by `additionalScopes`) +- Simplify `get_component_design_tokens` output for compound components: replace per-platform scope tables and per-platform related-themes tables with a flat list of related themes and one parent-selector-per-platform line +- Remove the `getScopeSelectorForPlatform()` and `resolveChildScopeName()` helper functions +- Update tests to validate the simplified output format and the new invariant that all child themes scope under the parent selector + +## Capabilities + +### New Capabilities + +_(none)_ + +### Modified Capabilities + +- `component-metadata-unification`: Remove `additionalScopes`, `childScopes`, and `ScopeSelectors` from the compound component data model. Remove the "Additional scopes for non-inline contexts" requirement entirely — all children are now inline. +- `component-theming`: Simplify compound component output format — remove per-platform scope tables, remove Scope column from related themes, collapse platform sections into a single related-themes list with parent selectors. + +## Impact + +- **Source files (4):** `component-metadata.ts`, `component-tokens.ts`, `knowledge/index.ts`, and the `component-theming` spec +- **Test files (2):** `component-metadata.test.ts`, `component-tokens.test.ts` +- **Living specs (2):** `openspec/specs/component-metadata-unification/spec.md`, `openspec/specs/component-theming/spec.md` +- **No generator changes:** `component-theme.ts`, `sass.ts`, `css.ts` have no compound scope awareness and are unaffected +- **MCP tool descriptions:** The `create_component_theme` and `get_component_design_tokens` tool descriptions reference the scope concept and compound examples — these will need review +- **Rollback plan:** Revert the commit. The `additionalScopes`/`childScopes` data and the two helper functions can be restored from git history. No migrations or data transformations involved. diff --git a/openspec/changes/archive/2026-04-01-remove-compound-scopes/specs/component-metadata-unification/spec.md b/openspec/changes/archive/2026-04-01-remove-compound-scopes/specs/component-metadata-unification/spec.md new file mode 100644 index 00000000..3fcba834 --- /dev/null +++ b/openspec/changes/archive/2026-04-01-remove-compound-scopes/specs/component-metadata-unification/spec.md @@ -0,0 +1,79 @@ +## MODIFIED Requirements + +### Requirement: Single unified component metadata map + +All component metadata (selectors, variants, compound info) SHALL be stored in a single `COMPONENT_METADATA` map exported from `component-metadata.ts`. Each component SHALL have exactly one entry keyed by its theme name. + +#### Scenario: Simple component lookup + +- **WHEN** a simple component (e.g., `avatar`) is looked up in `COMPONENT_METADATA` +- **THEN** the entry contains a `selectors` field with `angular` and `webcomponents` keys +- **AND** no `compound` or `variants` fields are present + +#### Scenario: Compound component lookup + +- **WHEN** a compound component (e.g., `combo`) is looked up in `COMPONENT_METADATA` +- **THEN** the entry contains `selectors` and a `compound` field +- **AND** `compound` includes `description`, `relatedThemes`, and optionally `tokenDerivations` and `guidance` +- **AND** `compound` SHALL NOT contain `additionalScopes` or `childScopes` fields + +#### Scenario: Variant component lookup + +- **WHEN** a base component with variants (e.g., `button`) is looked up in `COMPONENT_METADATA` +- **THEN** the entry contains `selectors` and a `variants` field listing variant theme names + +### Requirement: Inline scope derived from base selectors + +The scope for compound component child theming SHALL always be derived from the parent component's `selectors` field. All child themes scope under the parent component selector. + +#### Scenario: Handler resolves child scope for Angular + +- **WHEN** the handler needs the scoping selector for any child theme of `combo` on Angular +- **THEN** it reads `COMPONENT_METADATA['combo'].selectors.angular` (which is `'igx-combo'`) +- **AND** uses that value as the scoping selector for all child themes + +#### Scenario: Handler resolves child scope for Web Components + +- **WHEN** the handler needs the scoping selector for any child theme of `combo` on Web Components +- **THEN** it reads `COMPONENT_METADATA['combo'].selectors.webcomponents` (which is `'igc-combo'`) +- **AND** uses that value as the scoping selector for all child themes + +#### Scenario: No scope indirection in data + +- **WHEN** any compound component's metadata is inspected +- **THEN** there SHALL be no `additionalScopes` field +- **AND** there SHALL be no `childScopes` field +- **AND** there SHALL be no `ScopeSelectors` interface in the codebase + +### Requirement: Accessor functions preserve existing signatures + +All public accessor functions SHALL maintain their existing call signatures and return types. Functions that are eliminated SHALL have no callers in production code. + +#### Scenario: getComponentSelector unchanged + +- **WHEN** `getComponentSelector(name, platform)` is called +- **THEN** it returns the same value as before, read from `COMPONENT_METADATA[name].selectors[platform]` + +#### Scenario: isCompoundComponent uses unified map + +- **WHEN** `isCompoundComponent(name)` is called +- **THEN** it returns `true` if and only if `COMPONENT_METADATA[name]?.compound` is defined + +#### Scenario: hasVariants uses embedded field + +- **WHEN** `hasVariants(name)` is called +- **THEN** it returns `true` if and only if `COMPONENT_METADATA[name]?.variants` is defined and non-empty + +#### Scenario: getCompoundComponentInfo returns compound data without scope fields + +- **WHEN** `getCompoundComponentInfo(name)` is called for a compound component +- **THEN** it returns the `CompoundInfo` object including `description`, `relatedThemes`, and optionally `tokenDerivations` and `guidance` +- **AND** the returned object SHALL NOT contain `additionalScopes` or `childScopes` + +## REMOVED Requirements + +### Requirement: Additional scopes for non-inline contexts + +**Reason**: The popover API migration in Ignite UI for Angular eliminated overlay rendering outside the host DOM. All child components now render inside the parent component's DOM tree, making additional scopes unnecessary. + +**Migration**: Remove all `additionalScopes` and `childScopes` entries from component metadata. All child themes scope under the parent component's base selector. diff --git a/openspec/changes/archive/2026-04-01-remove-compound-scopes/specs/component-theming/spec.md b/openspec/changes/archive/2026-04-01-remove-compound-scopes/specs/component-theming/spec.md new file mode 100644 index 00000000..14a99ec1 --- /dev/null +++ b/openspec/changes/archive/2026-04-01-remove-compound-scopes/specs/component-theming/spec.md @@ -0,0 +1,80 @@ +## MODIFIED Requirements + +### Requirement: Component token schemas are exposed + +The `get_component_design_tokens` tool SHALL use an instruction-oriented output format that varies based on whether the component is compound or simple. For compound components, the response SHALL include a flat related-themes list, parent selector per available platform, token derivations, and guidance. For simple components, the response SHALL include the theme function, primary tokens, and the token table without compound sections. + +#### Scenario: Compound component response uses simplified format + +- **WHEN** `get_component_design_tokens` is called for a compound component +- **THEN** the response opens with `Implement a theme for the `` component using the following guidance.` +- **AND** includes a **Related themes** line listing all child theme names as inline code spans +- **AND** includes a scoping instruction stating all related themes scope under the parent component selector +- **AND** lists the parent selector for each available platform (Angular, Web Components / React / Blazor) +- **AND** does NOT include numbered Steps, per-platform scope tables, or per-platform related-themes tables + +#### Scenario: Simple component response omits compound sections + +- **WHEN** `get_component_design_tokens` is called for a non-compound component +- **THEN** the response opens with `Implement a theme for the `` component using the following guidance.` +- **AND** includes the theme function name, primary tokens, and available tokens table +- **AND** does NOT include related themes, scoping instructions, token derivations, or guidance sections + +#### Scenario: Token schema lookup + +- **WHEN** `get_component_design_tokens` is called with a component name +- **THEN** the response lists supported tokens and variant hints + +### Requirement: Compound checklist is ordered and minimal + +The compound component response SHALL present related themes as a flat inline-code list, followed by a single scoping instruction with per-platform parent selectors. + +#### Scenario: Related themes list format + +- **WHEN** the compound component response is generated +- **THEN** it lists related themes as inline code spans on a single **Related themes** line (e.g., `` `calendar`, `flat-button`, `input-group` ``) +- **AND** does NOT use tables for the related themes list + +#### Scenario: Scoping instruction format + +- **WHEN** the compound component response includes platform selectors +- **THEN** it states "Scope all related themes under the parent component selector:" followed by a bullet per available platform +- **AND** each bullet shows the platform name in bold and the selector in backticks + +### Requirement: Platform groups reflect theming strategy + +The `get_component_design_tokens` response SHALL group platforms into two theming strategies: "Angular" and "Web Components / React / Blazor". React and Blazor wrap Web Components and use identical selectors (`igc-`), variable prefixes (`--ig-`), and scoping. + +#### Scenario: Two platform lines for compound components + +- **WHEN** `get_component_design_tokens` is called for a compound component available on both Angular and Web Components +- **THEN** the scoping instruction shows exactly two bullet lines: one for "Angular" and one for "Web Components / React / Blazor" +- **AND** uses `webcomponents` as the representative platform for selector resolution in the WC group + +#### Scenario: Angular-only component omits WC line + +- **WHEN** `get_component_design_tokens` is called for a compound component with `webcomponents: null` (e.g., `time-picker`) +- **THEN** the scoping instruction shows only one bullet line for "Angular" +- **AND** does NOT show a "Web Components / React / Blazor" line + +### Requirement: Irrelevant platform entries are omitted + +The response SHALL omit platform lines where the component has no selector. This prevents showing misleading N/A entries for platforms where the component is not available. + +#### Scenario: Unavailable platform omitted + +- **WHEN** a compound component has `null` for a platform's selector +- **THEN** the scoping instruction does NOT include a bullet line for that platform + +#### Scenario: All available platforms shown + +- **WHEN** a compound component has non-null selectors for both Angular and Web Components +- **THEN** the scoping instruction includes bullet lines for both "Angular" and "Web Components / React / Blazor" + +## REMOVED Requirements + +### Requirement: Irrelevant scope rows are omitted + +**Reason**: The scope concept (inline vs overlay) has been removed entirely. There are no scope rows to omit — all children scope under the parent selector. The "omit N/A scope rows" behavior is subsumed by the new "omit unavailable platform lines" requirement. + +**Migration**: No migration needed. The replacement requirement "Irrelevant platform entries are omitted" covers the same intent (don't show N/A entries) but at the platform level rather than the scope level. diff --git a/openspec/changes/archive/2026-04-01-remove-compound-scopes/tasks.md b/openspec/changes/archive/2026-04-01-remove-compound-scopes/tasks.md new file mode 100644 index 00000000..c3902006 --- /dev/null +++ b/openspec/changes/archive/2026-04-01-remove-compound-scopes/tasks.md @@ -0,0 +1,35 @@ +## 1. Data model cleanup + +- [x] 1.1 Remove `ScopeSelectors` interface from `component-metadata.ts` +- [x] 1.2 Remove `additionalScopes` field from `CompoundInfo` interface +- [x] 1.3 Remove `childScopes` field from `CompoundInfo` interface +- [x] 1.4 Strip `additionalScopes` and `childScopes` from all 6 component entries: combo, simple-combo, date-picker, date-range-picker, select, time-picker +- [x] 1.5 Remove `ScopeSelectors` re-export from `knowledge/index.ts` + +## 2. Handler simplification + +- [x] 2.1 Delete `getScopeSelectorForPlatform()` helper from `component-tokens.ts` +- [x] 2.2 Delete `resolveChildScopeName()` helper from `component-tokens.ts` +- [x] 2.3 Delete `PLATFORM_GROUPS` constant from `component-tokens.ts` +- [x] 2.4 Remove the numbered **Steps** block from compound output (lines 162-173) +- [x] 2.5 Replace per-platform scope tables and related-themes tables with simplified output: flat **Related themes** list + scoping instruction with per-platform parent selectors +- [x] 2.6 Implement platform availability filtering — omit platform lines where selectors are null +- [x] 2.7 Group WC/React/Blazor into a single "Web Components / React / Blazor" line + +## 3. Test updates + +- [x] 3.1 Remove `childScopes references should be valid` test from `component-metadata.test.ts` +- [x] 3.2 Remove `no inline scope should appear in additionalScopes` test from `component-metadata.test.ts` +- [x] 3.3 Remove `childScopes children should be listed in relatedThemes` test from `component-metadata.test.ts` +- [x] 3.4 Remove `additionalScopes` production invariant test from `component-metadata.test.ts` +- [x] 3.5 Add test: compound `CompoundInfo` entries SHALL NOT contain `additionalScopes` or `childScopes` fields +- [x] 3.6 Rewrite `component-tokens.test.ts` compound output tests to match new format — verify **Related themes** list, scoping instruction with parent selectors, no scope tables +- [x] 3.7 Add test: Angular-only compound (e.g., `time-picker`) omits WC platform line +- [x] 3.8 Add test: compound with both platforms shows both Angular and WC lines +- [x] 3.9 Verify simple component output (e.g., `avatar`) is unchanged — no compound sections present + +## 4. Verification + +- [x] 4.1 Run `npm run build` — confirm no TypeScript compilation errors +- [x] 4.2 Run `npm test` — confirm all tests pass +- [x] 4.3 Manually call `get_component_design_tokens` for `date-picker`, `combo`, `grid`, `time-picker`, and `avatar` to verify output format diff --git a/openspec/changes/restructure-token-descriptions/.openspec.yaml b/openspec/changes/archive/2026-04-01-restructure-token-descriptions/.openspec.yaml similarity index 100% rename from openspec/changes/restructure-token-descriptions/.openspec.yaml rename to openspec/changes/archive/2026-04-01-restructure-token-descriptions/.openspec.yaml diff --git a/openspec/changes/restructure-token-descriptions/design.md b/openspec/changes/archive/2026-04-01-restructure-token-descriptions/design.md similarity index 100% rename from openspec/changes/restructure-token-descriptions/design.md rename to openspec/changes/archive/2026-04-01-restructure-token-descriptions/design.md diff --git a/openspec/changes/restructure-token-descriptions/proposal.md b/openspec/changes/archive/2026-04-01-restructure-token-descriptions/proposal.md similarity index 100% rename from openspec/changes/restructure-token-descriptions/proposal.md rename to openspec/changes/archive/2026-04-01-restructure-token-descriptions/proposal.md diff --git a/openspec/changes/restructure-token-descriptions/specs/component-theming/spec.md b/openspec/changes/archive/2026-04-01-restructure-token-descriptions/specs/component-theming/spec.md similarity index 100% rename from openspec/changes/restructure-token-descriptions/specs/component-theming/spec.md rename to openspec/changes/archive/2026-04-01-restructure-token-descriptions/specs/component-theming/spec.md diff --git a/openspec/changes/restructure-token-descriptions/tasks.md b/openspec/changes/archive/2026-04-01-restructure-token-descriptions/tasks.md similarity index 100% rename from openspec/changes/restructure-token-descriptions/tasks.md rename to openspec/changes/archive/2026-04-01-restructure-token-descriptions/tasks.md diff --git a/openspec/changes/unify-component-metadata/.openspec.yaml b/openspec/changes/archive/2026-04-01-unify-component-metadata/.openspec.yaml similarity index 100% rename from openspec/changes/unify-component-metadata/.openspec.yaml rename to openspec/changes/archive/2026-04-01-unify-component-metadata/.openspec.yaml diff --git a/openspec/changes/unify-component-metadata/design.md b/openspec/changes/archive/2026-04-01-unify-component-metadata/design.md similarity index 100% rename from openspec/changes/unify-component-metadata/design.md rename to openspec/changes/archive/2026-04-01-unify-component-metadata/design.md diff --git a/openspec/changes/unify-component-metadata/proposal.md b/openspec/changes/archive/2026-04-01-unify-component-metadata/proposal.md similarity index 100% rename from openspec/changes/unify-component-metadata/proposal.md rename to openspec/changes/archive/2026-04-01-unify-component-metadata/proposal.md diff --git a/openspec/changes/unify-component-metadata/specs/component-metadata-unification/spec.md b/openspec/changes/archive/2026-04-01-unify-component-metadata/specs/component-metadata-unification/spec.md similarity index 100% rename from openspec/changes/unify-component-metadata/specs/component-metadata-unification/spec.md rename to openspec/changes/archive/2026-04-01-unify-component-metadata/specs/component-metadata-unification/spec.md diff --git a/openspec/changes/unify-component-metadata/tasks.md b/openspec/changes/archive/2026-04-01-unify-component-metadata/tasks.md similarity index 100% rename from openspec/changes/unify-component-metadata/tasks.md rename to openspec/changes/archive/2026-04-01-unify-component-metadata/tasks.md diff --git a/openspec/changes/component-theme-alias/design.md b/openspec/changes/component-theme-alias/design.md deleted file mode 100644 index 24fcab96..00000000 --- a/openspec/changes/component-theme-alias/design.md +++ /dev/null @@ -1,45 +0,0 @@ -## Context - -Component metadata currently assumes each component has its own theme definition present in `themes.json`. Some components (e.g., tree-grid) style themselves using another component's Sass theme function, which causes `get_component_design_tokens` to fail when the requested theme is missing from `themes.json`. The proposal introduces a metadata-level alias to resolve these shared themes and avoid redundant Sass definitions. - -## Goals / Non-Goals - -**Goals:** - -- Allow component metadata to declare a `theme` alias pointing to another component's theme. -- Resolve theme lookups in `get_component_design_tokens` via alias when the theme entry is missing. -- Preserve current behavior and error handling for components without aliases or with invalid references. - -**Non-Goals:** - -- Redesigning the `themes.json` format or theming system overall. -- Changing how Sass theme functions are implemented or generated. -- Adding new theming variants beyond alias resolution. - -## Decisions - -- Add optional `theme` field to component metadata entries. Rationale: keeps aliasing colocated with component definition and avoids duplicating theme records. - - Alternative: duplicate theme entries in `themes.json` for each alias. Rejected because it perpetuates redundancy and requires manual syncing. -- Update `get_component_design_tokens` to resolve the effective theme in this order: direct theme match, metadata alias, then error. Rationale: preserves backward compatibility while enabling alias resolution. - - Alternative: always resolve through metadata regardless of `themes.json`. Rejected to avoid breaking existing theme lookups and to keep `themes.json` authoritative when present. -- Validate alias references during metadata loading. Rationale: early detection of invalid component references prevents confusing runtime errors in the MCP tool. - - Alternative: resolve lazily at lookup time only. Rejected due to poorer diagnostics and harder troubleshooting. - -## Risks / Trade-offs - -- Alias cycles or invalid references could cause confusing errors → Mitigation: add validation with clear error messages for unknown or self-referential aliases. -- Tooling behavior changes could affect consumers expecting failures for missing themes → Mitigation: only apply alias when explicitly configured; keep current errors otherwise. -- Additional metadata field increases schema complexity → Mitigation: make `theme` optional and document clearly in component metadata schema. - -## Migration Plan - -1. Introduce optional `theme` field in component metadata schema and validators. -2. Add alias entries for affected components (e.g., tree-grid -> grid) in metadata. -3. Update `get_component_design_tokens` to resolve alias and adjust error reporting. -4. Verify existing components without aliases behave unchanged. -5. Rollback: remove alias field usage and revert lookup to `themes.json` only. - -## Open Questions - -- Should alias resolution also apply when a theme exists but is incomplete compared to the referenced theme? -- Do we need a lint or CI check to prevent alias cycles and enforce allowed targets? diff --git a/openspec/changes/component-theme-alias/proposal.md b/openspec/changes/component-theme-alias/proposal.md deleted file mode 100644 index ae4ca5d1..00000000 --- a/openspec/changes/component-theme-alias/proposal.md +++ /dev/null @@ -1,26 +0,0 @@ -## Why - -Some components use a theme implemented by another component (e.g., tree-grid uses the grid theme), but `get_component_design_tokens` fails when the requested theme is not in `themes.json`. This forces redundant Sass theme definitions to work around an MCP limitation; we need a metadata-level alias to make theme lookup reliable and reduce duplication. - -## What Changes - -- Allow component metadata to declare a `theme` field that references another component's theme. -- Update `get_component_design_tokens` to resolve a requested theme via metadata alias when the theme is missing from `themes.json`. -- Preserve existing behavior for components without aliases and keep error handling for invalid references. - -## Capabilities - -### New Capabilities - -- `component-theme-alias`: Support component metadata aliasing for theme resolution in design token tooling. - -### Modified Capabilities - -- `component-theming`: Add requirements for alias-based theme resolution in `get_component_design_tokens` when a theme is missing from `themes.json`. - -## Impact - -- Component metadata schema and validation. -- MCP tooling behavior for `get_component_design_tokens` and theme lookup logic. -- Sass theme usage for affected components (e.g., tree-grid). -- Rollback plan: remove the metadata `theme` field usage and revert theme resolution to require entries in `themes.json`. diff --git a/openspec/changes/component-theme-alias/specs/component-theming/spec.md b/openspec/changes/component-theme-alias/specs/component-theming/spec.md deleted file mode 100644 index 8b417708..00000000 --- a/openspec/changes/component-theme-alias/specs/component-theming/spec.md +++ /dev/null @@ -1,25 +0,0 @@ -## ADDED Requirements - -### Requirement: Theme resolution uses metadata alias - -When a component's requested theme is missing from `themes.json`, `get_component_design_tokens` SHALL resolve the effective theme using the component metadata `theme` alias if present. - -#### Scenario: Missing theme resolved by alias - -- **GIVEN** component metadata defines `theme: "grid"` for `tree-grid` -- **AND** `themes.json` does not include `tree-grid` -- **WHEN** `get_component_design_tokens` is called for `tree-grid` with `theme: "tree-grid"` -- **THEN** the tool resolves tokens using the `grid` theme definition - -#### Scenario: Missing theme without alias - -- **GIVEN** component metadata has no `theme` alias -- **AND** the requested theme is not present in `themes.json` -- **WHEN** `get_component_design_tokens` is called -- **THEN** the tool returns the existing missing-theme error - -#### Scenario: Alias target is invalid - -- **GIVEN** component metadata defines `theme: "missing-component"` -- **WHEN** `get_component_design_tokens` is called for that component -- **THEN** the tool returns an error indicating the alias target is invalid diff --git a/openspec/changes/component-theme-alias/tasks.md b/openspec/changes/component-theme-alias/tasks.md deleted file mode 100644 index 4d9ae309..00000000 --- a/openspec/changes/component-theme-alias/tasks.md +++ /dev/null @@ -1,20 +0,0 @@ -## 1. Metadata schema and validation - -- [x] 1.1 Locate component metadata schema/loader and document current shape -- [x] 1.2 Add optional `theme` field to the metadata schema/types -- [x] 1.3 Implement validation for alias targets (exists, not self-referential, no cycles if applicable) -- [x] 1.4 Add or update metadata fixtures for an alias example (e.g., tree-grid -> grid) - -## 2. Theme resolution behavior - -- [x] 2.1 Identify `get_component_design_tokens` theme lookup path and error behavior -- [x] 2.2 Implement resolution order: direct `themes.json` match, metadata alias, then error -- [x] 2.3 Ensure invalid alias surfaces a clear error distinct from missing theme -- [x] 2.4 Verify non-aliased components preserve existing behavior - -## 3. Tests and verification - -- [x] 3.1 Add tests for alias resolution when theme is missing -- [x] 3.2 Add tests for invalid alias targets and self-references -- [x] 3.3 Update or add snapshots/fixtures if required by test harness -- [x] 3.4 Run relevant test suite (vitest) for component theming tools diff --git a/openspec/changes/simplify-compound-theming/.openspec.yaml b/openspec/changes/simplify-compound-theming/.openspec.yaml deleted file mode 100644 index 95d284af..00000000 --- a/openspec/changes/simplify-compound-theming/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-02-12 diff --git a/openspec/changes/simplify-compound-theming/design.md b/openspec/changes/simplify-compound-theming/design.md deleted file mode 100644 index 595e5d2f..00000000 --- a/openspec/changes/simplify-compound-theming/design.md +++ /dev/null @@ -1,180 +0,0 @@ -## Context - -The MCP theming server helps AI models generate component themes for Ignite UI. Compound components (date-picker, combo, grid, etc.) are composed of multiple child components that each need their own theme. Today, the model receives a "compound checklist" with inner CSS selectors for targeting each child — but the new `tokens` mixin makes these selectors unnecessary, and the model still lacks semantic knowledge about _how_ to derive child token values from the parent's theme intent. - -Current state: - -- `component-selectors.ts` stores `COMPOUND_COMPONENTS` with `relatedThemes` and `innerSelectors` -- `component-tokens.ts` handler formats inner selectors into a checklist table -- `generators/sass.ts` emits `@include css-vars-from-theme(...)` for each component theme -- The model receives no guidance about token relationships between parent and child components - -## Goals / Non-Goals - -**Goals:** - -- Replace inner-selector-based compound guidance with token-derivation-based guidance -- Give the model deterministic rules for setting child component tokens (e.g., "flat-button foreground = adaptive-contrast of calendar content-background") -- Switch Sass output from `css-vars-from-theme` to the `tokens` mixin (global mode) -- Remove all `innerSelectors` infrastructure from the codebase -- Update chart themes to use `tokens` mixin - -**Non-Goals:** - -- Automating compound theme generation in a single tool call (multi-call with hints is the chosen approach) -- Changing individual theme functions (calendar-theme, flat-button-theme, etc.) -- Changing the `tokens` mixin implementation itself -- Populating `tokenDerivations` for all 10 compounds (initial implementation covers the data structure; compound-specific derivations are populated incrementally) - -## Decisions - -### Decision 1: New `compound-theming.ts` knowledge file - -**Choice:** Create a single `src/mcp/knowledge/compound-theming.ts` file containing all compound theming metadata. - -**Alternatives considered:** - -- Per-compound files (`compound-theming/date-picker.ts`, etc.) — rejected because there are only 10 compounds; separate files add overhead without meaningful organizational benefit. -- Extending `component-selectors.ts` with new properties — rejected to maintain separation of concerns: `component-selectors.ts` owns structural data (selectors, which children exist), `compound-theming.ts` owns semantic data (how to theme children). - -**Data structure:** - -```typescript -interface TokenDerivation { - /** Source token: 'componentName.tokenName' */ - from: string; - /** Transform: 'identity' | 'adaptive-contrast' | 'dynamic-shade' */ - transform: 'identity' | 'adaptive-contrast' | 'dynamic-shade'; - /** Optional transform arguments (e.g., shade amount for dynamic-shade) */ - args?: Record; -} - -interface CompoundThemingInfo { - /** Token derivation rules. Key: 'childTheme.childToken' */ - tokenDerivations?: Record; - /** Platform-specific selector overrides for the compound wrapper */ - selectorOverrides?: { - angular?: string; - webcomponents?: string; - }; - /** Free-form guidance for edge cases */ - guidance?: string; -} -``` - -The `tokenDerivations` key format is `'childTheme.childToken'` (e.g., `'flat-button.foreground'`). The value describes where the token value comes from and how to transform it. - -The `selectorOverrides` field handles edge cases where the compound component's wrapper selector differs from `COMPONENT_SELECTORS`. For example, the date-picker needs `igx-date-picker, .igx-date-picker` (extra class for the calendar outlet popup) rather than just `igx-date-picker`. Falls back to `COMPONENT_SELECTORS` when not specified. - -### Decision 2: Multi-call with derivation hints (not single-call automation) - -**Choice:** The model continues to make separate `create_component_theme` calls per child component. The `get_component_design_tokens` response includes derivation hints that tell the model what values to pass. - -**Alternatives considered:** - -- Single-call compound generation (one call produces all child themes) — rejected because it prevents the model from customizing individual child tokens based on user intent. -- Single-call with `childOverrides` parameter — rejected as it adds API complexity; the multi-call pattern is already established and gives the model full flexibility. - -**How it works:** - -1. Model calls `get_component_design_tokens("date-picker")` -2. Response includes derivation hints like: `flat-button.foreground → adaptive-contrast(calendar.content-background)` -3. Model calls `create_component_theme("calendar", tokens: { content-background: purple })` with the compound's selector -4. Model follows the hint and calls `create_component_theme("flat-button", tokens: { foreground: "adaptive-contrast(purple)" })` with the same selector -5. User can override: "make the button text coral" → model uses `foreground: coral` instead of the derived value - -### Decision 3: Generator emits `@include tokens(...)` in global mode - -**Choice:** `generateComponentTheme` in `generators/sass.ts` emits `@include tokens($theme)` instead of `@include css-vars-from-theme($theme, '$prefix-$name')`. - -**Current output:** - -```scss -igx-avatar { - @include css-vars-from-theme($custom-avatar-theme, 'igx-avatar'); -} -``` - -**New output:** - -```scss -igx-avatar { - @include tokens($custom-avatar-theme); -} -``` - -Global mode is the default for `tokens`. It emits `--ig-{component}-{token}` CSS variables. The second argument to `css-vars-from-theme` (the variable name prefix) is no longer needed since global mode reads the component name from the theme's `_meta.name`. - -### Decision 4: Remove `innerSelectors` entirely (no deprecation period) - -**Choice:** Remove `CompoundInnerSelectors` interface, `innerSelectors` property from `CompoundComponentInfo`, and all 11 accessor functions in one step. - -**Alternatives considered:** - -- Deprecation period (mark as deprecated, remove later) — rejected because the inner selector data is incomplete (many WC selectors are `TODO`) and the `tokens` mixin is already the correct approach. Keeping dead code adds maintenance burden. - -**Removal surface:** - -- `component-selectors.ts`: `CompoundInnerSelectors` interface, `innerSelectors` property on `CompoundComponentInfo`, `innerSelectors` data on all 10 compound entries, 11 functions (`getPartSelector`, `getAngularInnerSelector`, `getInnerSelector`, `hasPartSelectors`, `hasAngularInnerSelectors`, `hasInnerSelectors`, `getAllPartSelectors`, `getAllAngularInnerSelectors`, `getAllInnerSelectors`) -- `knowledge/index.ts`: re-exports of the 11 functions -- `component-tokens.ts`: imports of `hasPartSelectors`, `hasAngularInnerSelectors`; inner selector table formatting logic -- `component-selectors.test.ts`: all inner selector test blocks (`getPartSelector()`, `hasPartSelectors()`, `getAllPartSelectors()`) - -### Decision 5: Compound selector resolution order - -**Choice:** When generating Sass for a compound component's child theme, the selector is resolved as: - -1. `selectorOverrides[platform]` from `compound-theming.ts` (if present) -2. `COMPONENT_SELECTORS[compoundName][platform]` (default) - -This keeps the common case simple (most compounds just use their standard selector) while handling edge cases like the date-picker outlet. - -### Decision 6: Handler output format for compound components - -**Choice:** The `get_component_design_tokens` handler output for compound components changes from an inner-selector table to a derivation-aware checklist. - -**Current format:** - -``` -**Compound checklist (required):** -1. `flat-button`: Angular `.igx-date-picker__actions .igx-button--flat` | WC `...` - -| Theme | Angular Selector | Web Components Selector | -|-------|------------------|------------------------| -``` - -**New format:** - -``` -**Compound Component:** - -**Theming approach:** Use `@include tokens(child-theme(...))` for each related -theme inside the compound component's selector. - -**Compound selector:** `` - -**Related themes and token derivations:** -1. `calendar` — primary visual element -2. `flat-button` — action buttons - - `foreground` → `adaptive-contrast` of `calendar.content-background` -3. `input-group` — trigger input - - `border-color` → same as `calendar.header-background` - -**Guidance:** -``` - -The handler reads from both `COMPOUND_COMPONENTS` (for `relatedThemes` and `description`) and `COMPOUND_THEMING` (for `tokenDerivations`, `selectorOverrides`, `guidance`) and merges them into this output. - -## Risks / Trade-offs - -**[Model compliance]** The derivation hints are guidance, not enforcement. A model might ignore them. -→ Mitigation: Format hints as a numbered checklist with explicit token-to-token mappings. The `descriptions.ts` tool description reinforces that derivations should be followed. Over time, monitor model compliance and tighten guidance wording if needed. - -**[Incomplete derivations]** The `tokenDerivations` for all 10 compounds must be authored manually. Missing derivations mean the model falls back to guessing. -→ Mitigation: Start with the most-used compounds (date-picker, combo, select). The `guidance` prose field provides a fallback for compounds without full derivation rules. Grid is the largest effort — defer detailed derivations and rely on guidance prose initially. - -**[Breaking change]** Removing `innerSelectors` and 11 functions is a breaking API change for any code importing from `component-selectors.ts`. -→ Mitigation: The only consumers are internal MCP code (handler, tests, index re-exports). No external packages depend on these APIs. Ship as a single commit with all consumers updated. - -**[Chart theme regression]** Switching chart themes from `css-vars(...)` to `tokens(...)` changes output from scoped mode to global mode. -→ Mitigation: `tokens()` in global mode at root level emits into `:root {}`, which is equivalent to the previous scoped behavior at root level. Verify with existing chart theme tests. diff --git a/openspec/changes/simplify-compound-theming/proposal.md b/openspec/changes/simplify-compound-theming/proposal.md deleted file mode 100644 index be1e2321..00000000 --- a/openspec/changes/simplify-compound-theming/proposal.md +++ /dev/null @@ -1,38 +0,0 @@ -## Why - -Compound component theming through the MCP is unreliable because the model receives only structural information (which children exist, what selectors to use) but no semantic guidance about how to set child component tokens relative to the parent's intent. The new `tokens` mixin eliminates the need for inner selectors entirely — child components consume CSS variables via `var(--ig-{component}-{token}, fallback)`, so scoping is handled by the CSS cascade. This makes `innerSelectors` obsolete while exposing a new gap: the model still doesn't know that, for example, a flat-button's foreground inside a date-picker should be `adaptive-contrast` of the calendar's content-background. This change addresses both: simplifying the selector story and adding the missing semantic layer. - -## What Changes - -- **BREAKING**: Remove `innerSelectors` and `CompoundInnerSelectors` from `CompoundComponentInfo` in `component-selectors.ts`, along with all associated accessor functions (`getPartSelector`, `getAngularInnerSelector`, `getInnerSelector`, `hasPartSelectors`, `hasAngularInnerSelectors`, `hasInnerSelectors`, `getAllPartSelectors`, `getAllAngularInnerSelectors`, `getAllInnerSelectors`) -- Add new `compound-theming.ts` knowledge file containing per-compound `tokenDerivations`, optional `selectorOverrides`, and `guidance` prose -- Update `component-tokens.ts` handler to merge structural data (`relatedThemes`) with semantic data (`tokenDerivations`, `guidance`) and present derivation hints to the model alongside the compound checklist -- Update `generators/sass.ts` to emit `@include tokens(...)` (global mode) instead of `@include css-vars-from-theme(...)` -- Update tool descriptions in `descriptions.ts` to reference the `tokens` mixin pattern, remove inner selector references, and update compound component examples -- Replace `@include css-vars(...)` with `@include tokens(...)` in `sass/themes/charts/_theme.scss` -- Remove inner selector tests from `component-selectors.test.ts` and add tests for compound theming data and updated handler output - -## Capabilities - -### New Capabilities - -- `compound-theming-guidance`: Token derivation rules and prose guidance for compound components, enabling the model to deterministically set child component tokens based on the parent component's theme intent - -### Modified Capabilities - -- `component-theming`: Compound component responses shift from inner-selector-based checklists to derivation-hint-based guidance using the `tokens` mixin. The `create_component_theme` Sass output changes from `css-vars-from-theme` to `tokens`. Inner selector references are removed entirely. -- `css-output`: Sass output for component themes uses `@include tokens(...)` instead of `@include css-vars-from-theme(...)`. Chart themes switch from `@include css-vars(...)` to `@include tokens(...)`. - -## Impact - -- **MCP knowledge layer** (`src/mcp/knowledge/`): New `compound-theming.ts` file. Significant removals from `component-selectors.ts` (interfaces, data, 11 functions). -- **MCP tools** (`src/mcp/tools/`): Handler and description changes for `get_component_design_tokens` and `create_component_theme`. -- **MCP generators** (`src/mcp/generators/`): Sass code generation output format changes. -- **Sass source** (`sass/themes/`): Chart theme mixin calls updated. -- **Tests** (`src/mcp/__tests__/`): Inner selector tests removed, compound theming and updated generator tests added. -- **Breaking for MCP consumers**: Any external code relying on `getPartSelector`, `getAngularInnerSelector`, `getInnerSelector`, or `hasInnerSelectors` APIs will break. The `CompoundInnerSelectors` type is removed. -- **Model behavior change**: Models consuming the MCP will receive different compound component guidance format. The new format is richer (derivations + guidance) but structurally different from the current inner-selector table. - -### Rollback Plan - -Revert the commit. The `innerSelectors` data and accessor functions can be restored from git history. The `css-vars` mixin remains functional (it wraps `tokens` in scoped mode), so reverting the generator output is safe. The new `compound-theming.ts` file is additive and can simply be deleted. diff --git a/openspec/changes/simplify-compound-theming/specs/component-theming/spec.md b/openspec/changes/simplify-compound-theming/specs/component-theming/spec.md deleted file mode 100644 index 401361b8..00000000 --- a/openspec/changes/simplify-compound-theming/specs/component-theming/spec.md +++ /dev/null @@ -1,84 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Component theming requires platform - -The `create_component_theme` tool requires a `platform` parameter and SHALL specify compound-component completeness rules to reduce incomplete outputs. - -#### Scenario: Missing platform - -- **WHEN** `platform` is not provided -- **THEN** the tool returns an error indicating `platform` is required - -#### Scenario: Compound guidance treated as completeness criteria - -- **WHEN** a user requests theming for a compound component -- **THEN** the guidance states the response is incomplete if related theme calls are omitted -- **AND** the guidance instructs the model to use `@include tokens(child-theme(...))` inside the compound component's selector for each related theme - -#### Scenario: Canonical compound example provided - -- **WHEN** a compound component is detected -- **THEN** guidance includes a short canonical example demonstrating the multi-call flow using the `tokens` mixin and derivation hints - -### Requirement: Component token schemas are exposed - -The `get_component_design_tokens` tool SHALL explicitly indicate when a component is compound and provide an actionable checklist with token derivation hints for generating related themes. - -#### Scenario: Compound component response includes derivation-aware checklist - -- **WHEN** `get_component_design_tokens` is called for a compound component -- **THEN** the response includes a "Related themes and token derivations" section listing each related theme -- **AND** each related theme with derivation rules SHALL show the target token, transform function, and source token -- **AND** the response includes the resolved compound selector for the target platform -- **AND** the response instructs the model to call `get_component_design_tokens` and `create_component_theme` for each related theme using the compound selector - -#### Scenario: Token schema lookup - -- **WHEN** `get_component_design_tokens` is called with a component name -- **THEN** the response lists supported tokens and variant hints - -### Requirement: Sass output uses tokens mixin - -The `create_component_theme` tool SHALL generate Sass code using `@include tokens(...)` in global mode instead of `@include css-vars-from-theme(...)`. - -#### Scenario: Component theme Sass output - -- **WHEN** `create_component_theme` generates Sass code -- **THEN** the output uses `@include tokens($theme-variable)` inside the component selector -- **AND** the output does NOT use `css-vars-from-theme` - -#### Scenario: Theme variable has no prefix argument - -- **WHEN** `@include tokens(...)` is emitted -- **THEN** it takes only the theme variable as an argument (no variable name prefix) - -### Requirement: Compound checklist is ordered and minimal - -The checklist SHALL be ordered and concise, including derivation hints inline, to minimize response length while preserving determinism. - -#### Scenario: Checklist with derivations - -- **WHEN** the checklist is generated for a compound component with token derivations -- **THEN** each related theme is listed with its derivation rules indented beneath it - -#### Scenario: Checklist without derivations - -- **WHEN** the checklist is generated for a compound component without token derivations -- **THEN** each related theme is listed by name only, without derivation sub-items - -## REMOVED Requirements - -### Requirement: Inner selector handling - -**Reason**: The `tokens` mixin emits `--ig-{component}-{token}` CSS variables that are consumed by child components via `var()` fallback. Inner selectors are no longer needed for scoping child themes within compound components. - -**Migration**: Use the compound component's own selector (or `selectorOverrides` from `compound-theming.ts`) as the wrapper for all `@include tokens(...)` calls. Derivation hints from `COMPOUND_THEMING` replace the structural inner-selector table. - -The following are removed: - -- `CompoundInnerSelectors` interface -- `innerSelectors` property on `CompoundComponentInfo` -- All inner selector data on compound component entries -- Functions: `getPartSelector`, `getAngularInnerSelector`, `getInnerSelector`, `hasPartSelectors`, `hasAngularInnerSelectors`, `hasInnerSelectors`, `getAllPartSelectors`, `getAllAngularInnerSelectors`, `getAllInnerSelectors` -- Handler logic that formats inner selectors into a table -- "Missing selector entries are handled" scenario (selectors are no longer per-child-theme) diff --git a/openspec/changes/simplify-compound-theming/specs/compound-theming-guidance/spec.md b/openspec/changes/simplify-compound-theming/specs/compound-theming-guidance/spec.md deleted file mode 100644 index 9027928d..00000000 --- a/openspec/changes/simplify-compound-theming/specs/compound-theming-guidance/spec.md +++ /dev/null @@ -1,112 +0,0 @@ -## Purpose - -Define the data structures, accessors, and model-facing output for compound component token derivation rules and theming guidance. - -## Requirements - -### Requirement: Compound theming data is stored separately from structural data - -The compound theming knowledge (token derivations, selector overrides, guidance) SHALL be stored in a dedicated `compound-theming.ts` file, separate from the structural data in `component-selectors.ts`. - -#### Scenario: Compound theming file exists - -- **WHEN** the knowledge layer is loaded -- **THEN** `src/mcp/knowledge/compound-theming.ts` exports a `COMPOUND_THEMING` record keyed by compound component name - -#### Scenario: Keys align with COMPOUND_COMPONENTS - -- **WHEN** a key exists in `COMPOUND_THEMING` -- **THEN** that key also exists in `COMPOUND_COMPONENTS` - -### Requirement: Token derivation rules map child tokens to source tokens with transforms - -Each compound component MAY define `tokenDerivations` that describe how child component tokens are derived from sibling or parent component tokens. - -#### Scenario: Derivation rule structure - -- **WHEN** a `tokenDerivations` entry exists -- **THEN** the key SHALL be in the format `'childTheme.childToken'` (e.g., `'flat-button.foreground'`) -- **AND** the value SHALL contain a `from` field in the format `'sourceComponent.sourceToken'` (e.g., `'calendar.content-background'`) -- **AND** the value SHALL contain a `transform` field with one of: `'identity'`, `'adaptive-contrast'`, `'dynamic-shade'` - -#### Scenario: Identity transform - -- **WHEN** a derivation has `transform: 'identity'` -- **THEN** the derived token value is the same as the source token value - -#### Scenario: Adaptive-contrast transform - -- **WHEN** a derivation has `transform: 'adaptive-contrast'` -- **THEN** the derived token value is `adaptive-contrast()`, producing a contrasting foreground color - -#### Scenario: Dynamic-shade transform - -- **WHEN** a derivation has `transform: 'dynamic-shade'` -- **THEN** the derivation SHALL include an `args` field with shade parameters -- **AND** the derived token value uses `dynamic-shade(, )` - -### Requirement: Compound selector overrides handle edge cases - -A compound component MAY define `selectorOverrides` when its theming wrapper selector differs from the standard `COMPONENT_SELECTORS` entry. - -#### Scenario: Selector override present - -- **WHEN** a compound component has `selectorOverrides` for the target platform -- **THEN** the override selector SHALL be used as the wrapper for all `@include tokens(...)` calls - -#### Scenario: No selector override - -- **WHEN** a compound component has no `selectorOverrides` for the target platform -- **THEN** the selector from `COMPONENT_SELECTORS` SHALL be used as the wrapper - -### Requirement: Guidance prose provides context for edge cases - -A compound component MAY define a `guidance` string with natural-language context about theming relationships that cannot be fully expressed as token derivation rules. - -#### Scenario: Guidance present - -- **WHEN** a compound component has a `guidance` field -- **THEN** the handler output SHALL include the guidance text in the compound component section - -#### Scenario: No guidance - -- **WHEN** a compound component has no `guidance` field -- **THEN** the handler output SHALL omit the guidance section without error - -### Requirement: Handler merges structural and semantic data for model output - -The `get_component_design_tokens` handler SHALL merge data from `COMPOUND_COMPONENTS` (structural) and `COMPOUND_THEMING` (semantic) into a unified output for the model. - -#### Scenario: Compound component with derivations - -- **WHEN** `get_component_design_tokens` is called for a compound component that has `tokenDerivations` -- **THEN** the response SHALL include a "Related themes and token derivations" section -- **AND** each related theme SHALL be listed with any associated derivation rules showing the target token, transform, and source token - -#### Scenario: Compound component without derivations - -- **WHEN** `get_component_design_tokens` is called for a compound component that has no entry in `COMPOUND_THEMING` or no `tokenDerivations` -- **THEN** the response SHALL list related themes without derivation hints -- **AND** the response SHALL still include the compound selector and theming approach - -#### Scenario: Compound selector is included in output - -- **WHEN** `get_component_design_tokens` is called for a compound component -- **THEN** the response SHALL include the resolved compound selector for the target platform (using override if present, otherwise from `COMPONENT_SELECTORS`) - -### Requirement: Accessor functions provide programmatic access to compound theming data - -#### Scenario: Get compound theming info - -- **WHEN** `getCompoundThemingInfo(componentName)` is called with a valid compound component name -- **THEN** it returns the `CompoundThemingInfo` for that compound, or `undefined` if no entry exists - -#### Scenario: Get resolved compound selector - -- **WHEN** `getCompoundSelector(componentName, platform)` is called -- **THEN** it returns the `selectorOverrides[platform]` value if present, otherwise the `COMPONENT_SELECTORS[componentName][platform]` value - -#### Scenario: Get token derivations for a child theme - -- **WHEN** `getTokenDerivationsForChild(compoundName, childThemeName)` is called -- **THEN** it returns all derivation entries where the key starts with `'childThemeName.'`, or an empty record if none exist diff --git a/openspec/changes/simplify-compound-theming/specs/css-output/spec.md b/openspec/changes/simplify-compound-theming/specs/css-output/spec.md deleted file mode 100644 index 2e097081..00000000 --- a/openspec/changes/simplify-compound-theming/specs/css-output/spec.md +++ /dev/null @@ -1,16 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Component Sass output uses tokens mixin - -#### Scenario: Component Sass output - -- **WHEN** component Sass output is generated by `create_component_theme` -- **THEN** the output uses `@include tokens($theme-variable)` inside a selector block -- **AND** the output does NOT contain `@include css-vars-from-theme(...)` - -### Requirement: Chart theme Sass output uses tokens mixin - -#### Scenario: Chart theme mixin calls - -- **WHEN** the `chart-themes` mixin generates chart component themes -- **THEN** each chart theme call uses `@include tokens(chart-theme(...))` instead of `@include css-vars(chart-theme(...))` diff --git a/openspec/changes/simplify-compound-theming/tasks.md b/openspec/changes/simplify-compound-theming/tasks.md deleted file mode 100644 index eb225ec3..00000000 --- a/openspec/changes/simplify-compound-theming/tasks.md +++ /dev/null @@ -1,41 +0,0 @@ -## 1. Create compound-theming.ts knowledge file - -- [x] 1.1 Create `src/mcp/knowledge/compound-theming.ts` with `TokenDerivation`, `CompoundThemingInfo` interfaces and `COMPOUND_THEMING` record -- [x] 1.2 Add accessor functions: `getCompoundThemingInfo`, `getCompoundSelector`, `getTokenDerivationsForChild` -- [x] 1.3 Populate `COMPOUND_THEMING` entries for all 10 compound components with `selectorOverrides` and `guidance` (tokenDerivations populated incrementally — start with date-picker, combo, select) -- [x] 1.4 Re-export new types and functions from `src/mcp/knowledge/index.ts` - -## 2. Remove innerSelectors infrastructure from component-selectors.ts - -- [x] 2.1 Remove `CompoundInnerSelectors` interface and `innerSelectors` property from `CompoundComponentInfo` -- [x] 2.2 Remove `innerSelectors` data from all 10 compound component entries in `COMPOUND_COMPONENTS` -- [x] 2.3 Remove 11 accessor functions: `getPartSelector`, `getAngularInnerSelector`, `getInnerSelector`, `hasPartSelectors`, `hasAngularInnerSelectors`, `hasInnerSelectors`, `getAllPartSelectors`, `getAllAngularInnerSelectors`, `getAllInnerSelectors` -- [x] 2.4 Remove re-exports of the 11 functions from `src/mcp/knowledge/index.ts` - -## 3. Update component-tokens handler - -- [x] 3.1 Remove `hasPartSelectors` and `hasAngularInnerSelectors` imports from `component-tokens.ts` -- [x] 3.2 Add imports for `getCompoundThemingInfo`, `getCompoundSelector`, `getTokenDerivationsForChild` from compound-theming -- [x] 3.3 Replace inner-selector table and checklist formatting with new derivation-aware output (compound selector, related themes with derivation hints, guidance prose) - -## 4. Update Sass generator output - -- [x] 4.1 Change `generateComponentTheme` in `generators/sass.ts` to emit `@include tokens($theme-variable)` instead of `@include css-vars-from-theme($theme-variable, '$prefix-$name')` -- [x] 4.2 Remove the `varName` / variable-prefix logic that was only needed for `css-vars-from-theme` - -## 5. Update tool descriptions - -- [x] 5.1 Update `get_component_design_tokens` description in `descriptions.ts` to reference tokens mixin, remove inner selector references, mention derivation hints -- [x] 5.2 Update `create_component_theme` description in `descriptions.ts` to show `@include tokens(...)` output, update compound example to use tokens mixin pattern with derivation hints - -## 6. Update chart themes - -- [x] 6.1 Replace all `@include css-vars(...)` calls with `@include tokens(...)` in `sass/themes/charts/_theme.scss` - -## 7. Tests - -- [x] 7.1 Remove inner selector test blocks from `component-selectors.test.ts` (`getPartSelector()`, `hasPartSelectors()`, `getAllPartSelectors()`) -- [x] 7.2 Add tests for `compound-theming.ts`: data structure validation (keys align with COMPOUND_COMPONENTS), derivation rule format, accessor functions -- [x] 7.3 Update `component-tokens` handler tests (if they exist) to verify new derivation-aware output format -- [x] 7.4 Update generator tests to verify `@include tokens(...)` output instead of `css-vars-from-theme` -- [x] 7.5 Run full test suite and fix any failures diff --git a/openspec/config.yaml b/openspec/config.yaml index 02684823..10c12e80 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -1,7 +1,7 @@ schema: spec-driven context: | - Tech stack: TypeScript, Sass, Node.js + Tech stack: TypeScript, Sass, Node.js, ModelContextProtocol Testing: vitest We use conventional commits diff --git a/openspec/specs/biome-linting/spec.md b/openspec/specs/biome-linting/spec.md new file mode 100644 index 00000000..baafbe0b --- /dev/null +++ b/openspec/specs/biome-linting/spec.md @@ -0,0 +1,150 @@ +# biome-linting Specification + +## Purpose +TBD - created by archiving change add-biome-prettier-linting. Update Purpose after archive. +## Requirements +### Requirement: Biome configuration file exists + +The system SHALL provide a `biome.json` configuration file at the project root that defines linting rules and file inclusion patterns. + +#### Scenario: Configuration file is present + +- **WHEN** the project root is examined +- **THEN** a `biome.json` file SHALL exist + +#### Scenario: Configuration targets TypeScript and JavaScript files + +- **WHEN** the Biome configuration is read +- **THEN** the file inclusion patterns SHALL include `scripts/**/*.{js,mjs,cjs}` and `src/mcp/**/*.ts` + +### Requirement: Biome linting rules are configured + +The system SHALL configure Biome with specific linting rules that enforce code quality standards. + +#### Scenario: Recommended rules are enabled + +- **WHEN** Biome configuration is loaded +- **THEN** the `linter.rules.recommended` SHALL be set to `true` + +#### Scenario: Import extensions are required + +- **WHEN** Biome lints TypeScript/JavaScript files +- **THEN** the `correctness.useImportExtensions` rule SHALL enforce `.js` extensions with error level + +#### Scenario: Unused code is detected + +- **WHEN** Biome lints files with unused variables, imports, or function parameters +- **THEN** the `correctness.noUnusedVariables`, `correctness.noUnusedImports`, and `correctness.noUnusedFunctionParameters` rules SHALL report errors + +#### Scenario: Console statements are prevented + +- **WHEN** Biome lints files containing console statements or debugger keywords +- **THEN** the `suspicious.noConsole` and `suspicious.noDebugger` rules SHALL report errors + +#### Scenario: Style rules are enforced + +- **WHEN** Biome lints files +- **THEN** style rules SHALL enforce patterns including namespace prevention, collapsed else-if, enum initializers, self-closing elements, single variable declarators, number namespace usage, and redundant type annotations removal + +### Requirement: Biome formatter is disabled + +The system SHALL disable Biome's formatting capabilities in favor of Prettier. + +#### Scenario: Formatter is disabled in configuration + +- **WHEN** the Biome configuration is read +- **THEN** the `formatter.enabled` field SHALL be set to `false` + +### Requirement: Import organization is enabled + +The system SHALL enable automatic import organization through Biome's assist feature. + +#### Scenario: Assist organizes imports automatically + +- **WHEN** Biome processes TypeScript files with the assist feature +- **THEN** the `assist.actions.source.organizeImports` SHALL be set to `"on"` + +#### Scenario: Assist targets source files + +- **WHEN** the Biome assist configuration is read +- **THEN** the assist includes pattern SHALL target source files in `src/mcp/**/*.ts` and `scripts/**/*.{js,mjs,cjs}` + +### Requirement: VCS integration is configured + +The system SHALL integrate with Git to respect ignore patterns. + +#### Scenario: Git client is specified + +- **WHEN** the Biome configuration is read +- **THEN** the `vcs.clientKind` SHALL be set to `"git"` + +#### Scenario: Gitignore patterns are respected + +- **WHEN** Biome runs linting checks +- **THEN** the `vcs.useIgnoreFile` SHALL be set to `true` to exclude files listed in `.gitignore` + +### Requirement: Biome CLI command is available + +The system SHALL provide an npm script to run Biome linting checks. + +#### Scenario: lint:biome script exists + +- **WHEN** `package.json` scripts are examined +- **THEN** a `lint:biome` script SHALL exist + +#### Scenario: lint:biome script runs Biome checks + +- **WHEN** `npm run lint:biome` is executed +- **THEN** the script SHALL execute `biome check --write` to lint files and apply fixes + +### Requirement: Biome is integrated into format workflow + +The system SHALL include Biome checks in the format script before Prettier. + +#### Scenario: Format script includes Biome + +- **WHEN** the `format` npm script is examined +- **THEN** it SHALL include `biome check --fix` before any Prettier commands + +#### Scenario: Format script maintains Prettier integration + +- **WHEN** the `format` npm script is executed +- **THEN** it SHALL run Prettier formatting after Biome checks complete + +### Requirement: Biome is integrated into lint workflow + +The system SHALL include Biome checks in the main lint script. + +#### Scenario: Lint script includes Biome + +- **WHEN** the `lint` npm script is examined +- **THEN** it SHALL include `npm run lint:biome` alongside other linting commands + +### Requirement: Biome package is installed + +The system SHALL include Biome as a development dependency. + +#### Scenario: Biome is in devDependencies + +- **WHEN** `package.json` dependencies are examined +- **THEN** `@biomejs/biome` SHALL be listed in `devDependencies` + +### Requirement: Biome auto-fixes linting issues + +The system SHALL automatically fix correctable linting violations when using fix flags. + +#### Scenario: Auto-fix adds missing import extensions + +- **WHEN** Biome runs with `--fix` flag on files with imports missing `.js` extensions +- **THEN** the extensions SHALL be added automatically + +#### Scenario: Auto-fix removes unused imports + +- **WHEN** Biome runs with `--fix` flag on files with unused imports +- **THEN** the unused imports SHALL be removed automatically + +#### Scenario: Auto-fix organizes imports + +- **WHEN** Biome runs with `--fix` flag on files with disorganized imports +- **THEN** the imports SHALL be sorted and organized according to configured rules + diff --git a/openspec/specs/component-metadata-unification/spec.md b/openspec/specs/component-metadata-unification/spec.md new file mode 100644 index 00000000..1f0dfafa --- /dev/null +++ b/openspec/specs/component-metadata-unification/spec.md @@ -0,0 +1,161 @@ +# component-metadata-unification Specification + +## Purpose +TBD - created by archiving change unify-component-metadata. Update Purpose after archive. +## Requirements +### Requirement: Single unified component metadata map + +All component metadata (selectors, variants, compound info) SHALL be stored in a single `COMPONENT_METADATA` map exported from `component-metadata.ts`. Each component SHALL have exactly one entry keyed by its theme name. + +#### Scenario: Simple component lookup + +- **WHEN** a simple component (e.g., `avatar`) is looked up in `COMPONENT_METADATA` +- **THEN** the entry contains a `selectors` field with `angular` and `webcomponents` keys +- **AND** no `compound` or `variants` fields are present + +#### Scenario: Compound component lookup + +- **WHEN** a compound component (e.g., `combo`) is looked up in `COMPONENT_METADATA` +- **THEN** the entry contains `selectors` and a `compound` field +- **AND** `compound` includes `description`, `relatedThemes`, and optionally `tokenDerivations`, `guidance`, `additionalScopes`, and `childScopes` + +#### Scenario: Variant component lookup + +- **WHEN** a base component with variants (e.g., `button`) is looked up in `COMPONENT_METADATA` +- **THEN** the entry contains `selectors` and a `variants` field listing variant theme names + +### Requirement: Inline scope derived from base selectors + +The inline scope for compound component child theming SHALL always be derived from the component's `selectors` field. Inline scopes SHALL NOT be declared explicitly in any data structure. + +#### Scenario: Handler resolves inline scope for Angular + +- **WHEN** the handler needs the inline scope for `combo` on Angular +- **THEN** it reads `COMPONENT_METADATA['combo'].selectors.angular` (which is `'igx-combo'`) +- **AND** uses that value directly as the scoping selector + +#### Scenario: Handler resolves inline scope for Web Components + +- **WHEN** the handler needs the inline scope for `combo` on Web Components +- **THEN** it reads `COMPONENT_METADATA['combo'].selectors.webcomponents` (which is `'igc-combo'`) +- **AND** uses that value directly as the scoping selector + +#### Scenario: No explicit inline scope in data + +- **WHEN** any component's metadata is inspected +- **THEN** there SHALL be no `inline` key in `additionalScopes` +- **AND** no scope field that duplicates the base selector value + +### Requirement: Additional scopes for non-inline contexts + +Compound components that require scoping beyond their base selector SHALL declare those scopes in `additionalScopes`. The `childScopes` map SHALL reference either `'inline'` or a key from `additionalScopes`. + +#### Scenario: Compound with overlay scope + +- **WHEN** `date-picker` metadata is inspected +- **THEN** `compound.additionalScopes` contains an `overlay` key with platform-specific selectors +- **AND** `compound.childScopes` references `'overlay'` for children rendered in the overlay + +#### Scenario: Compound with inline-only scoping + +- **WHEN** `combo` metadata is inspected +- **THEN** `compound.additionalScopes` is either absent or empty +- **AND** `compound.childScopes` references `'inline'` for all children (or `childScopes` is absent) + +#### Scenario: childScopes reference validity + +- **WHEN** any compound component's `childScopes` is inspected +- **THEN** every value SHALL be either `'inline'` or a key that exists in `additionalScopes` + +### Requirement: Accessor functions preserve existing signatures + +All public accessor functions SHALL maintain their existing call signatures and return types. Functions that are eliminated SHALL have no callers in production code. + +#### Scenario: getComponentSelector unchanged + +- **WHEN** `getComponentSelector(name, platform)` is called +- **THEN** it returns the same value as before, read from `COMPONENT_METADATA[name].selectors[platform]` + +#### Scenario: isCompoundComponent uses unified map + +- **WHEN** `isCompoundComponent(name)` is called +- **THEN** it returns `true` if and only if `COMPONENT_METADATA[name]?.compound` is defined + +#### Scenario: hasVariants uses embedded field + +- **WHEN** `hasVariants(name)` is called +- **THEN** it returns `true` if and only if `COMPONENT_METADATA[name]?.variants` is defined and non-empty + +#### Scenario: getCompoundComponentInfo returns full compound data + +- **WHEN** `getCompoundComponentInfo(name)` is called for a compound component +- **THEN** it returns the full `CompoundInfo` object including `description`, `relatedThemes`, `tokenDerivations`, `guidance`, `additionalScopes`, and `childScopes` + +#### Scenario: getCompoundThemingInfo is eliminated + +- **WHEN** the codebase is searched for `getCompoundThemingInfo` +- **THEN** no references exist — the function has been removed along with all call sites + +#### Scenario: getCompoundSelector is eliminated + +- **WHEN** the codebase is searched for `getCompoundSelector` +- **THEN** no references exist — the function has been removed (it had no production callers) + +### Requirement: VARIANT_THEME_NAMES derived at init + +The set of all variant theme names SHALL be derived from `COMPONENT_METADATA` at module initialization time, not maintained as a separate data structure. + +#### Scenario: Variant set computed from metadata + +- **WHEN** the module initializes +- **THEN** `VARIANT_THEME_NAMES` is a `Set` containing every string from every component's `variants` array +- **AND** `isVariantTheme(name)` checks membership in this derived set + +### Requirement: No stale or test data in production metadata + +The `COMPONENT_METADATA` map SHALL contain only valid, production-ready data. No test fixtures, placeholder values, or debugging artifacts. + +#### Scenario: No stray scope entries + +- **WHEN** the metadata for `date-picker` is inspected +- **THEN** there is no scope entry named `shit` or any other non-production key +- **AND** all scope keys correspond to real UI contexts (e.g., `inline`, `overlay`) + +### Requirement: Handler output is byte-for-byte identical + +The refactored knowledge layer SHALL produce identical handler output for all components across all platforms. + +#### Scenario: Simple component output unchanged + +- **WHEN** `get_component_design_tokens` is called for `avatar` on Angular +- **THEN** the response text is identical to the pre-refactor output + +#### Scenario: Compound component output unchanged + +- **WHEN** `get_component_design_tokens` is called for `date-picker` on Angular +- **THEN** the response text is identical to the pre-refactor output, including all scope selectors, checklists, and guidance + +#### Scenario: Variant error output unchanged + +- **WHEN** `get_component_design_tokens` is called for `button` (base with variants) +- **THEN** the error response listing available variants is identical to the pre-refactor output + +### Requirement: Old files and exports are removed + +After the refactor, the old file structure and removed exports SHALL not exist in the codebase. + +#### Scenario: compound-theming.ts deleted + +- **WHEN** the file system is checked +- **THEN** `src/mcp/knowledge/compound-theming.ts` does not exist + +#### Scenario: compound-theming.test.ts deleted + +- **WHEN** the file system is checked +- **THEN** `src/mcp/__tests__/knowledge/compound-theming.test.ts` does not exist + +#### Scenario: Old imports resolved + +- **WHEN** the codebase is searched for imports from `compound-theming` +- **THEN** no import statements reference the deleted module + diff --git a/openspec/changes/component-theme-alias/specs/component-theme-alias/spec.md b/openspec/specs/component-theme-alias/spec.md similarity index 87% rename from openspec/changes/component-theme-alias/specs/component-theme-alias/spec.md rename to openspec/specs/component-theme-alias/spec.md index be02c888..7f8d96e0 100644 --- a/openspec/changes/component-theme-alias/specs/component-theme-alias/spec.md +++ b/openspec/specs/component-theme-alias/spec.md @@ -1,5 +1,8 @@ -## ADDED Requirements +# component-theme-alias Specification +## Purpose +TBD - created by archiving change component-theme-alias. Update Purpose after archive. +## Requirements ### Requirement: Component metadata supports theme alias Component metadata SHALL allow an optional `theme` field that names another component whose theme implementation is used for styling. @@ -25,3 +28,4 @@ Theme alias values MUST reference an existing component metadata entry and MUST - **GIVEN** component metadata sets `theme` equal to its own component name - **WHEN** metadata is validated - **THEN** validation fails with an error indicating the alias is self-referential + diff --git a/openspec/specs/component-theming/spec.md b/openspec/specs/component-theming/spec.md index 3486601a..25d6b207 100644 --- a/openspec/specs/component-theming/spec.md +++ b/openspec/specs/component-theming/spec.md @@ -1,9 +1,7 @@ ## Purpose Define component theming requirements, validation, and platform-specific output rules. - ## Requirements - ### Requirement: Component theming requires platform The `create_component_theme` tool requires a `platform` parameter and SHALL specify compound-component completeness rules to reduce incomplete outputs. @@ -13,6 +11,12 @@ The `create_component_theme` tool requires a `platform` parameter and SHALL spec - **WHEN** `platform` is not provided - **THEN** the tool returns an error indicating `platform` is required +#### Scenario: Generic platform rejected + +- **WHEN** `platform: "generic"` is provided +- **THEN** the tool SHALL return an error stating that `create_component_theme` requires a specific Ignite UI product platform (angular, webcomponents, react, or blazor) +- **AND** the error SHALL explain that component theming requires platform-specific selectors and variable prefixes that do not exist in generic mode + #### Scenario: Compound guidance treated as completeness criteria - **WHEN** a user requests theming for a compound component @@ -25,20 +29,28 @@ The `create_component_theme` tool requires a `platform` parameter and SHALL spec ### Requirement: Component token schemas are exposed -The `get_component_design_tokens` tool SHALL explicitly indicate when a component is compound and provide an actionable checklist for generating related themes. +The `get_component_design_tokens` tool SHALL use an instruction-oriented output format that varies based on whether the component is compound or simple. For compound components, the response SHALL include numbered steps, per-platform scope tables, related theme tables, token derivations, and guidance. For simple components, the response SHALL include the theme function, primary tokens, and the token table without compound sections. -#### Scenario: Compound component response includes required checklist +#### Scenario: Compound component response uses instruction-oriented format - **WHEN** `get_component_design_tokens` is called for a compound component -- **THEN** the response includes a "Compound checklist (required)" section listing each related theme and its scoped selector -- **AND** the checklist instructs the model to call `get_component_design_tokens` and `create_component_theme` for each related theme using the provided selector +- **THEN** the response opens with `Implement a theme for the \`\` component using the following guidance.` +- **AND** includes a numbered **Steps** section instructing the model to identify the platform, call `get_component_design_tokens` for each related theme, and apply themes to scopes +- **AND** includes per-platform sections with scope tables and related theme tables + +#### Scenario: Simple component response omits compound sections + +- **WHEN** `get_component_design_tokens` is called for a non-compound component +- **THEN** the response opens with `Implement a theme for the \`\` component using the following guidance.` +- **AND** includes the theme function name, primary tokens, and available tokens table +- **AND** does NOT include steps, scope tables, related theme tables, token derivations, or guidance sections #### Scenario: Missing selector entries are handled - **WHEN** a compound component has related themes without scoped selectors (e.g., selector is `TODO`) - **THEN** the checklist marks those related themes as skipped and explains that selector data is missing -#### Scenario: Token schema lookup (legacy) +#### Scenario: Token schema lookup - **WHEN** `get_component_design_tokens` is called with a component name - **THEN** the response lists supported tokens and variant hints @@ -83,9 +95,80 @@ The `get_component_design_tokens` tool SHALL explicitly indicate when a componen ### Requirement: Compound checklist is ordered and minimal -The checklist SHALL be ordered and concise to minimize response length while preserving determinism. +The compound component response SHALL present related themes in ordered tables grouped by platform, without additional narrative beyond the guidance paragraph. #### Scenario: Checklist length control -- **WHEN** the checklist is generated -- **THEN** it lists only related themes and their selectors, without additional narrative +- **WHEN** the compound component response is generated +- **THEN** it lists related themes in platform-specific tables with columns: Theme, Scope, Selector +- **AND** does NOT include additional narrative in the tables themselves + +### Requirement: Platform groups reflect theming strategy + +The `get_component_design_tokens` response SHALL group platforms into two theming strategies: "Angular" and "Web Components / React / Blazor". React and Blazor wrap Web Components and use identical selectors (`igc-`), variable prefixes (`--ig-`), and inline-only scoping. + +#### Scenario: Two platform sections for compound components + +- **WHEN** `get_component_design_tokens` is called for a compound component +- **THEN** the response shows exactly two platform sections: "Angular" and "Web Components / React / Blazor" +- **AND** uses `webcomponents` as the representative platform for selector resolution in the WC group + +#### Scenario: No duplicate tables for WC-based platforms + +- **WHEN** the response includes platform scope tables +- **THEN** React and Blazor do NOT get separate tables +- **AND** are covered by the "Web Components / React / Blazor" section + +### Requirement: Irrelevant scope rows are omitted + +The response SHALL omit scope rows where the platform group has no selector. This prevents showing misleading N/A entries for scopes that don't apply to a platform. + +#### Scenario: Overlay scope omitted for WC-based platforms + +- **WHEN** a compound component defines an `overlay` scope with an Angular selector but no `webcomponents` selector +- **THEN** the "Web Components / React / Blazor" scope table does NOT include an overlay row + +#### Scenario: All defined scopes shown for Angular + +- **WHEN** a compound component defines both `inline` and `overlay` scopes with Angular selectors +- **THEN** the "Angular" scope table includes both rows + +### Requirement: PRIMARY TOKENS are structured data + +The build pipeline SHALL extract PRIMARY TOKENS from SassDoc descriptions into a structured `primaryTokens` field in `themes.json`, separate from the free-form `description` field. + +#### Scenario: themes.json contains primaryTokens field + +- **WHEN** `buildComponentDocs.mjs` processes a theme function with a PRIMARY TOKENS block in its SassDoc description +- **THEN** `themes.json` includes a `primaryTokens` array with `{ name, description }` objects for each primary token +- **AND** the `description` field contains only the title line and optional summary (not the PRIMARY TOKENS block) + +#### Scenario: Theme function without PRIMARY TOKENS + +- **WHEN** a theme function has no PRIMARY TOKENS block in its SassDoc description +- **THEN** `themes.json` includes an empty `primaryTokens` array for that component +- **AND** the `description` field contains the full original description + +#### Scenario: Handler renders primary tokens from structured data + +- **WHEN** `get_component_design_tokens` renders the response +- **THEN** it reads the `primaryTokens` field from the theme data +- **AND** renders them as a concise bullet list under a **Primary Tokens** section + +### Requirement: SassDoc descriptions are concise + +The SassDoc description block in `*-theme.scss` files SHALL use concise one-line summaries for PRIMARY TOKENS entries, removing verbose "Auto-derives: X, Y, Z" enumeration lists. Individual `@param` annotations are unchanged. + +#### Scenario: Trimmed PRIMARY TOKENS format + +- **GIVEN** a theme function SassDoc block with PRIMARY TOKENS +- **WHEN** the description is read +- **THEN** each PRIMARY TOKEN entry is a single line: `- \`$name\` — Summary sentence.` +- **AND** there are no multi-line "Auto-derives:" continuation lines in the PRIMARY TOKENS block + +#### Scenario: Param annotations preserved + +- **GIVEN** a theme function with `@param` SassDoc annotations +- **WHEN** the SassDoc descriptions are trimmed +- **THEN** all `@param` annotations remain unchanged with their full derivation descriptions + diff --git a/openspec/specs/documentation-organization/spec.md b/openspec/specs/documentation-organization/spec.md new file mode 100644 index 00000000..9942aa40 --- /dev/null +++ b/openspec/specs/documentation-organization/spec.md @@ -0,0 +1,61 @@ +# documentation-organization Specification + +## Purpose +TBD - created by archiving change migrate-mcp-docs-to-vite-markdown. Update Purpose after archive. +## Requirements +### Requirement: Documentation files are organized hierarchically + +The documentation SHALL be organized in a hierarchical directory structure under `src/mcp/knowledge/docs/` that mirrors the conceptual organization of theming topics. + +#### Scenario: Layout documentation structure + +- **WHEN** layout-related documentation is stored +- **THEN** it is organized under `docs/layout/` with subdirectories for functions and mixins + +#### Scenario: Color documentation structure + +- **WHEN** color-related documentation is stored +- **THEN** it is organized under `docs/colors/` as individual topic files + +### Requirement: Documentation structure mirrors concepts not code + +The documentation directory structure SHALL reflect conceptual groupings (layout, colors) rather than source code structure (file names, module paths). + +#### Scenario: Conceptual grouping for discoverability + +- **WHEN** a developer looks for documentation about sizing functions +- **THEN** they can navigate to `docs/layout/functions/` to find all sizing-related function documentation + +#### Scenario: Independent from code refactoring + +- **WHEN** source code is refactored or reorganized +- **THEN** documentation structure remains stable and unchanged + +### Requirement: Each documentation topic has its own file + +Each distinct documentation topic SHALL be stored in its own markdown file with a descriptive filename. + +#### Scenario: Single-purpose documentation files + +- **WHEN** documentation for the `pad()` function is needed +- **THEN** it is available in `docs/layout/functions/pad.md` as a standalone file + +#### Scenario: Documentation file naming + +- **WHEN** creating documentation files +- **THEN** filenames use kebab-case and describe the documented topic (e.g., `border-radius.md`, `custom-palettes.md`) + +### Requirement: Documentation guidelines are provided + +A README file SHALL exist at the root of the documentation directory to guide contributors. + +#### Scenario: Contributor guidance + +- **WHEN** a developer wants to add or edit documentation +- **THEN** they can read `docs/README.md` for structure guidelines, editing conventions, and testing instructions + +#### Scenario: Adding new documentation + +- **WHEN** a developer needs to add new documentation +- **THEN** the README explains where to place files, how to import them, and how to register MCP resources + diff --git a/openspec/specs/generic-platform/spec.md b/openspec/specs/generic-platform/spec.md new file mode 100644 index 00000000..bd2548a9 --- /dev/null +++ b/openspec/specs/generic-platform/spec.md @@ -0,0 +1,168 @@ +# generic-platform Specification + +## Purpose +TBD - created by archiving change generic-platform-detection. Update Purpose after archive. +## Requirements +### Requirement: Generic is a first-class platform value + +The `Platform` type SHALL include `"generic"` as a valid member alongside `"angular"`, `"webcomponents"`, `"react"`, and `"blazor"`. All `Record` mappings SHALL include a `generic` entry. + +#### Scenario: PLATFORMS array includes generic + +- **WHEN** the `PLATFORMS` constant is defined +- **THEN** it SHALL contain `"generic"` as a member +- **AND** the derived `Platform` type includes `"generic"` in its union + +#### Scenario: Ignite package patterns for generic + +- **WHEN** `IGNITE_PACKAGE_PATTERNS` is defined as `Record` +- **THEN** the `generic` entry SHALL be an empty array +- **AND** the compiler enforces its presence + +#### Scenario: Variable prefix for generic + +- **WHEN** `PLATFORM_VARIABLE_PREFIX` is defined as `Record` +- **THEN** the `generic` entry SHALL be `"ig"` + +#### Scenario: Platform metadata for generic + +- **WHEN** `PLATFORM_METADATA` is accessed with `"generic"` +- **THEN** it SHALL return an entry with `name: "Ignite UI Theming (Standalone)"`, `themingModule: "igniteui-theming"`, and a description stating this is for projects without a specific Ignite UI product + +### Requirement: Generic platform uses igniteui-theming import path + +The Sass import path for `"generic"` SHALL be `"igniteui-theming"`, consistent with other non-Angular platforms. + +#### Scenario: IMPORT_PATHS contains generic key + +- **WHEN** `IMPORT_PATHS` is defined +- **THEN** it SHALL contain a `generic` key with value `"igniteui-theming"` +- **AND** the former `default` key SHALL be renamed to `generic` + +#### Scenario: getImportPath resolves generic + +- **WHEN** `getImportPath("generic")` is called +- **THEN** it SHALL return `"igniteui-theming"` + +#### Scenario: generateUseStatement for generic + +- **WHEN** `generateUseStatement("generic")` is called +- **THEN** it SHALL return `@use 'igniteui-theming' as *;` + +### Requirement: Generic platform has a resource URI + +The resource system SHALL expose a `theming://platforms/generic` resource. + +#### Scenario: Generic resource is registered + +- **WHEN** the resource definitions are loaded +- **THEN** `theming://platforms/generic` SHALL be a valid resource URI + +#### Scenario: Generic resource returns common presets + +- **WHEN** `theming://platforms/generic` is read +- **THEN** the response SHALL include schemas, palettes, typefaces, typography, and elevation presets +- **AND** the response SHALL include the generic platform metadata + +#### Scenario: Platforms list includes generic + +- **WHEN** the `theming://platforms` resource is read +- **THEN** the `platforms` array SHALL include `"generic"` + +### Requirement: Generic detection response lists tool eligibility + +When the detected platform is `"generic"`, the `detect_platform` response SHALL clearly communicate which tools are usable and which are not. + +#### Scenario: Usable tools are listed + +- **WHEN** `detect_platform` returns `platform: "generic"` +- **THEN** the response SHALL list the following tools as usable: `create_palette`, `create_custom_palette`, `create_typography`, `create_elevations`, `create_theme`, `set_size`, `set_spacing`, `set_roundness`, `get_color`, `read_resource` + +#### Scenario: Non-usable tools are listed with reasons + +- **WHEN** `detect_platform` returns `platform: "generic"` +- **THEN** the response SHALL list `create_component_theme` and `get_component_design_tokens` as not useful in generic mode +- **AND** the reason SHALL state these tools target Ignite UI framework components + +#### Scenario: Layout tools have a scope caveat + +- **WHEN** `detect_platform` returns `platform: "generic"` +- **THEN** the response SHALL note that layout tools (`set_size`, `set_spacing`, `set_roundness`) work globally via `:root` or with a custom `scope` selector, but the `component` parameter SHALL NOT be used because it resolves Ignite UI component selectors + +### Requirement: Generic detection response provides Sass load path guidance + +When the detected platform is `"generic"` and the output format is Sass, the response SHALL include project configuration guidance for Sass load paths. + +#### Scenario: Angular config file detected + +- **WHEN** `detect_platform` returns `platform: "generic"` and `angular.json` was detected as a signal +- **THEN** the response SHALL include guidance to add `"stylePreprocessorOptions": { "includePaths": ["node_modules"] }` in `angular.json` (Angular CLI uses `includePaths`) + +#### Scenario: Vite config file detected + +- **WHEN** `detect_platform` returns `platform: "generic"` and a `vite.config.*` file was detected as a signal +- **THEN** the response SHALL include guidance to add `css: { preprocessorOptions: { scss: { loadPaths: ['node_modules'] } } }` in the Vite config + +#### Scenario: Next.js config file detected + +- **WHEN** `detect_platform` returns `platform: "generic"` and a `next.config.*` file was detected as a signal +- **THEN** the response SHALL include guidance to add `sassOptions: { loadPaths: ['node_modules'] }` in the Next.js config + +#### Scenario: No config file detected + +- **WHEN** `detect_platform` returns `platform: "generic"` and no framework config file was detected +- **THEN** the response SHALL include general guidance to ensure `node_modules` is in the Sass compiler's `loadPaths` +- **AND** the response SHALL instruct the model to investigate the project's build configuration + +#### Scenario: CSS output does not need load paths + +- **WHEN** the user requests CSS output from any tool +- **THEN** the response SHALL note that CSS output is compiled server-side by the MCP and requires no local `igniteui-theming` installation or load path configuration + +#### Scenario: Theming package not in project dependencies + +- **WHEN** `igniteui-theming` is not found in `package.json` dependencies or devDependencies +- **THEN** the response SHALL note that for Sass output, the user needs `igniteui-theming` resolvable in their project (via `npm install igniteui-theming` or appropriate `loadPaths`) +- **AND** the response SHALL note that CSS output works regardless since the MCP compiles it server-side + +### Requirement: Display names resolve correctly for all platforms + +All handlers that display a platform name SHALL use `PLATFORM_METADATA` lookups instead of binary ternary expressions. + +#### Scenario: Generic platform display name + +- **WHEN** a handler formats a display name for `platform: "generic"` +- **THEN** it SHALL display `"Ignite UI Theming (Standalone)"` (from `PLATFORM_METADATA.generic.name`) +- **AND** it SHALL NOT display `"Ignite UI for Web Components"` or any other product name + +#### Scenario: Undefined platform display name + +- **WHEN** a handler formats a display name and `platform` is `undefined` +- **THEN** it SHALL display `"Not specified (generic output)"` + +### Requirement: Tool descriptions guide generic-mode behavior + +Tool descriptions SHALL be updated to guide the model on correct behavior when `platform` is `"generic"`. + +#### Scenario: Platform parameter description includes generic + +- **WHEN** the `FRAGMENTS.PLATFORM` description is read by the model +- **THEN** it SHALL list `"generic"` as a valid option for platform-agnostic output + +#### Scenario: Layout tool descriptions mention scope-only for generic + +- **WHEN** the `set_size`, `set_spacing`, or `set_roundness` tool description is read +- **THEN** it SHALL note that when `platform` is `"generic"`, the `component` parameter SHALL NOT be used +- **AND** it SHALL recommend using `scope` with a custom CSS selector instead + +#### Scenario: Component tokens tool description notes generic limitation + +- **WHEN** the `get_component_design_tokens` tool description is read +- **THEN** it SHALL note that this tool returns tokens for Ignite UI framework components and is not useful when the detected platform is `"generic"` + +#### Scenario: Detect platform tool description documents generic output + +- **WHEN** the `detect_platform` tool description is read +- **THEN** it SHALL document that `"generic"` is a possible platform value returned when no Ignite UI product is detected +- **AND** it SHALL describe what `"generic"` means for tool usage + diff --git a/openspec/specs/layout-overrides/spec.md b/openspec/specs/layout-overrides/spec.md index a4795038..152a14d0 100644 --- a/openspec/specs/layout-overrides/spec.md +++ b/openspec/specs/layout-overrides/spec.md @@ -1,16 +1,14 @@ ## Purpose Document size, spacing, and roundness overrides emitted by layout tools. - ## Requirements - ### Requirement: Layout overrides emit CSS variable blocks The `set_size`, `set_spacing`, and `set_roundness` tools emit CSS variable overrides scoped to component selectors or custom scopes. #### Scenario: Size override with component selector -- **WHEN** `set_size` is called with `component` and `platform` +- **WHEN** `set_size` is called with `component` and a product `platform` (angular, webcomponents, react, or blazor) - **THEN** the response includes a platform-specific selector - **AND** the block sets `--ig-size` @@ -32,6 +30,24 @@ The `set_size`, `set_spacing`, and `set_roundness` tools emit CSS variable overr #### Scenario: Roundness override emits platform selector -- **WHEN** `set_roundness` is called with `component` and `platform` +- **WHEN** `set_roundness` is called with `component` and a product `platform` (angular, webcomponents, react, or blazor) - **THEN** the response includes a platform-specific selector - **AND** the block sets `--ig-radius-factor` + +#### Scenario: Generic platform treats scope resolution like undefined + +- **WHEN** any layout tool is called with `platform: "generic"` and `component` is provided +- **THEN** the tool SHALL treat the scope resolution the same as when `platform` is `undefined` +- **AND** the response SHALL merge both Angular and Web Components selectors for the component + +#### Scenario: Generic platform with scope only + +- **WHEN** any layout tool is called with `platform: "generic"` and `scope` but no `component` +- **THEN** the response SHALL use the provided `scope` selector +- **AND** the block SHALL set the appropriate CSS variable + +#### Scenario: Generic platform with no component or scope + +- **WHEN** any layout tool is called with `platform: "generic"` and neither `component` nor `scope` +- **THEN** the response SHALL use `:root` as the selector + diff --git a/openspec/specs/lint-staged-integration/spec.md b/openspec/specs/lint-staged-integration/spec.md new file mode 100644 index 00000000..767f33b5 --- /dev/null +++ b/openspec/specs/lint-staged-integration/spec.md @@ -0,0 +1,127 @@ +# lint-staged-integration Specification + +## Purpose +TBD - created by archiving change add-biome-prettier-linting. Update Purpose after archive. +## Requirements +### Requirement: lint-staged configuration includes Biome checks + +The system SHALL configure lint-staged to run Biome checks on TypeScript and JavaScript files before Prettier. + +#### Scenario: lint-staged targets JS/TS file types + +- **WHEN** `package.json` lint-staged configuration is examined +- **THEN** a configuration entry SHALL exist for pattern `*.{js,ts,cjs,mjs,jsx,tsx}` + +#### Scenario: Biome runs before Prettier in lint-staged + +- **WHEN** lint-staged processes TypeScript or JavaScript files +- **THEN** `biome check --fix --no-errors-on-unmatched` SHALL execute before `prettier --write` + +### Requirement: Biome uses no-errors-on-unmatched flag + +The system SHALL prevent lint-staged failures when Biome has no matching files to process. + +#### Scenario: Biome command includes no-errors-on-unmatched + +- **WHEN** the lint-staged Biome command is examined +- **THEN** it SHALL include the `--no-errors-on-unmatched` flag + +#### Scenario: No error when staged files don't match Biome patterns + +- **WHEN** staged files exist but none match Biome's inclusion patterns +- **THEN** lint-staged SHALL complete successfully without Biome errors + +### Requirement: Biome auto-fixes issues during pre-commit + +The system SHALL automatically fix linting violations on staged files before commit. + +#### Scenario: Biome fixes are applied to staged files + +- **WHEN** a file with fixable Biome violations is staged +- **THEN** Biome SHALL apply fixes during the pre-commit hook + +#### Scenario: Fixed files remain staged + +- **WHEN** Biome applies fixes to a staged file +- **THEN** the fixed version SHALL be included in the commit + +### Requirement: Prettier runs after Biome in pre-commit workflow + +The system SHALL ensure Prettier formatting occurs after Biome linting and fixes. + +#### Scenario: Prettier receives Biome-fixed code + +- **WHEN** lint-staged executes on staged TypeScript/JavaScript files +- **THEN** Prettier SHALL run on files after Biome fixes have been applied + +#### Scenario: Prettier formatting is final + +- **WHEN** both Biome and Prettier complete in lint-staged +- **THEN** the committed files SHALL have Prettier's formatting as the final state + +### Requirement: Pre-commit hook runs Biome checks + +The system SHALL execute Biome linting checks during Git pre-commit hooks via lint-staged. + +#### Scenario: Pre-commit triggers lint-staged with Biome + +- **WHEN** a Git commit is attempted with staged TypeScript or JavaScript files +- **THEN** the pre-commit hook SHALL invoke lint-staged which runs Biome checks + +#### Scenario: Commit is blocked on Biome errors + +- **WHEN** Biome detects unfixable errors in staged files +- **THEN** the pre-commit hook SHALL fail and prevent the commit + +#### Scenario: Commit succeeds after Biome auto-fixes + +- **WHEN** Biome successfully fixes all violations in staged files +- **THEN** the pre-commit hook SHALL complete and allow the commit + +### Requirement: lint-staged targets only staged changes + +The system SHALL run Biome checks only on files that are staged for commit. + +#### Scenario: Unstaged files are not checked + +- **WHEN** TypeScript files exist but are not staged +- **THEN** Biome SHALL NOT check those files during pre-commit + +#### Scenario: Only staged portions are checked + +- **WHEN** a file has both staged and unstaged changes +- **THEN** Biome SHALL check only the staged version of the file + +### Requirement: Multiple file types are supported in lint-staged + +The system SHALL process various JavaScript and TypeScript file extensions through lint-staged. + +#### Scenario: Standard TypeScript files are processed + +- **WHEN** `.ts` files are staged +- **THEN** lint-staged SHALL run Biome and Prettier on them + +#### Scenario: JavaScript module files are processed + +- **WHEN** `.js`, `.mjs`, or `.cjs` files are staged +- **THEN** lint-staged SHALL run Biome and Prettier on them + +#### Scenario: React files are supported + +- **WHEN** `.jsx` or `.tsx` files are staged +- **THEN** lint-staged SHALL run Biome and Prettier on them + +### Requirement: Existing SCSS lint-staged configuration is preserved + +The system SHALL maintain existing lint-staged configuration for SCSS files alongside new TypeScript/JavaScript configuration. + +#### Scenario: SCSS files continue to be formatted + +- **WHEN** SCSS files are staged +- **THEN** the existing format script SHALL still execute for those files + +#### Scenario: Both JS and SCSS configurations coexist + +- **WHEN** `package.json` lint-staged configuration is examined +- **THEN** both `*.{js,ts,cjs,mjs,jsx,tsx}` and `sass/**/*.{scss,css}` patterns SHALL be configured + diff --git a/openspec/specs/platform-detection/spec.md b/openspec/specs/platform-detection/spec.md index ce5186ae..5d3125c0 100644 --- a/openspec/specs/platform-detection/spec.md +++ b/openspec/specs/platform-detection/spec.md @@ -1,9 +1,7 @@ ## Purpose Define how the MCP server detects the target platform and reports confidence and signals. - ## Requirements - ### Requirement: Platform detection provides confidence and signals The system detects a target platform using package dependencies, config files, and framework fallbacks, returning confidence and contributing signals. @@ -22,18 +20,38 @@ The system detects a target platform using package dependencies, config files, a #### Scenario: Framework fallback detection -- **WHEN** only framework packages are detected (`@angular/core`, `react`, `lit`, Blazor SDK) -- **THEN** the platform is set to the matching target -- **AND** confidence is `low` with a `framework_package` signal +- **WHEN** only framework packages are detected (`@angular/core`, `react`, `lit`, Blazor SDK) without any Ignite UI product package +- **THEN** the platform is set to `"generic"` +- **AND** confidence is `low` with the detected `framework_package` signal(s) included + +#### Scenario: Config file detected without Ignite UI product + +- **WHEN** a framework config file is detected (e.g., `angular.json`, `vite.config.*`, `next.config.*`) but no Ignite UI product package is found +- **THEN** the platform is set to `"generic"` +- **AND** confidence is `low` with the `config_file` signal included +- **AND** the response SHALL include Sass load path guidance specific to the detected config file #### Scenario: Ambiguous detection -- **WHEN** multiple platforms are detected with high-confidence signals +- **WHEN** multiple Ignite UI product platforms are detected with high-confidence signals - **THEN** `platform` is `null` - **AND** `ambiguous` is `true` with `alternatives` and a helpful reason #### Scenario: No detection - **WHEN** no matching packages or config files exist +- **THEN** the platform is set to `"generic"` +- **AND** confidence is `"none"` with an empty signals list + +#### Scenario: Error reading package.json + +- **WHEN** `package.json` cannot be read or parsed - **THEN** `platform` is `null` -- **AND** confidence is `none` with an empty signals list +- **AND** confidence is `"none"` with a reason describing the error + +#### Scenario: Invalid package.json structure + +- **WHEN** `package.json` is readable but has an invalid structure +- **THEN** `platform` is `null` +- **AND** confidence is `"none"` with a reason describing the validation failure + diff --git a/openspec/specs/theme-generation/spec.md b/openspec/specs/theme-generation/spec.md index 1abd7374..bccd9641 100644 --- a/openspec/specs/theme-generation/spec.md +++ b/openspec/specs/theme-generation/spec.md @@ -1,12 +1,10 @@ ## Purpose Document theme generation behavior and platform-specific output patterns. - ## Requirements - ### Requirement: Theme generation is platform-aware -The `create_theme` tool generates Sass output that follows platform-specific conventions. When no platform is specified (generic path), the output SHALL include the necessary preset imports for typography and elevation variables. +The `create_theme` tool generates Sass output that follows platform-specific conventions. #### Scenario: Angular output @@ -26,36 +24,18 @@ The `create_theme` tool generates Sass output that follows platform-specific con - **THEN** the output uses the Web Components mixin pattern - **AND** `core()` and Angular `theme()` mixins are not used -#### Scenario: Platform hint - -- **WHEN** `platform` is not specified -- **THEN** the response includes a hint to specify `platform` - -#### Scenario: Generic output includes typography preset import - -- **WHEN** `platform` is not specified (generic path) -- **AND** `includeTypography` is not `false` -- **THEN** the output MUST include `@use 'igniteui-theming/sass/typography/presets/' as *;` -- **AND** this import MUST appear after the base `@use 'igniteui-theming' as *;` statement +#### Scenario: Generic platform output -#### Scenario: Generic output includes elevations preset import +- **WHEN** `platform: "generic"` is provided +- **THEN** the output SHALL use the generic theme generator (same as the current `undefined` platform path) +- **AND** the output SHALL use `@use 'igniteui-theming' as *;` as the import +- **AND** the response platform note SHALL display `"Platform: Ignite UI Theming (Standalone)"` instead of `"Platform: Not specified (generic output)"` -- **WHEN** `platform` is not specified (generic path) -- **AND** `includeElevations` is not `false` -- **THEN** the output MUST include `@use 'igniteui-theming/sass/elevations/presets' as *;` -- **AND** this import MUST appear after the base `@use 'igniteui-theming' as *;` statement - -#### Scenario: Generic output omits typography preset import when excluded - -- **WHEN** `platform` is not specified (generic path) -- **AND** `includeTypography: false` is provided -- **THEN** the output MUST NOT include a typography preset `@use` import - -#### Scenario: Generic output omits elevations preset import when excluded +#### Scenario: Platform hint -- **WHEN** `platform` is not specified (generic path) -- **AND** `includeElevations: false` is provided -- **THEN** the output MUST NOT include an elevations preset `@use` import +- **WHEN** `platform` is not specified (undefined) +- **THEN** the response includes a hint to specify `platform` +- **AND** the hint SHALL mention `"generic"` as a valid option for platform-agnostic output ### Requirement: Theme includes optional sections by flags @@ -83,3 +63,4 @@ The `create_theme` tool includes typography, elevations, and spacing by default - **WHEN** provided colors are unsuitable for the chosen variant - **THEN** the response includes a warning message - **AND** the response still returns generated code + diff --git a/openspec/specs/vite-markdown-imports/spec.md b/openspec/specs/vite-markdown-imports/spec.md new file mode 100644 index 00000000..b645c372 --- /dev/null +++ b/openspec/specs/vite-markdown-imports/spec.md @@ -0,0 +1,47 @@ +# vite-markdown-imports Specification + +## Purpose +TBD - created by archiving change migrate-mcp-docs-to-vite-markdown. Update Purpose after archive. +## Requirements +### Requirement: Markdown files can be imported as strings + +The build system SHALL support importing markdown files as raw string content using the `?raw` query parameter suffix. + +#### Scenario: Import markdown file as string + +- **WHEN** a TypeScript file imports a markdown file with `?raw` suffix +- **THEN** the imported value is a string containing the markdown file's content + +#### Scenario: TypeScript recognizes markdown imports + +- **WHEN** a TypeScript file imports a markdown file with `?raw` suffix +- **THEN** TypeScript compiler recognizes the import as valid and types it as `string` + +### Requirement: Markdown content is embedded at build time + +The build system SHALL embed markdown file content into the compiled JavaScript output at build time. + +#### Scenario: Markdown content bundled with code + +- **WHEN** the build process runs +- **THEN** markdown file content is inlined into the compiled JavaScript output + +#### Scenario: No runtime file I/O required + +- **WHEN** the compiled code accesses imported markdown content +- **THEN** the content is available as a string constant without filesystem access + +### Requirement: Import paths resolve correctly + +The build system SHALL resolve markdown file paths relative to the importing TypeScript file. + +#### Scenario: Relative path resolution + +- **WHEN** a TypeScript file imports `'./docs/example.md?raw'` +- **THEN** the build system resolves the path relative to the importing file's directory + +#### Scenario: Deep nested imports + +- **WHEN** a TypeScript file imports `'./docs/subfolder/nested.md?raw'` +- **THEN** the build system correctly resolves and imports the nested markdown file + diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 01b9ff49..f80de191 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -461,7 +461,7 @@ Sets the roundness scale for all components or a specific component by updating Discovers available design tokens (customizable properties) for a specific Ignite UI component. Use this tool **before** `create_component_theme` to see what tokens are available. -For compound components (e.g., combo, select, grid), the response includes a **Compound checklist (required)** with scoped selectors. Follow the checklist by calling `get_component_design_tokens` and `create_component_theme` for each related theme. Items with missing selectors are marked as skipped. +For compound components (e.g., combo, select, grid), the response lists related child themes that should also be themed. For each related theme, call `get_component_design_tokens` and then `create_component_theme` using the parent component's selector. | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ----------------------------------------------- | @@ -496,7 +496,7 @@ For compound components (e.g., combo, select, grid), the response includes a **C Generates Sass code to customize a specific component's appearance using design tokens. Call `get_component_design_tokens` first to discover available tokens. -If the requested component is compound and selectors are available, include the related theme calls from the checklist. Otherwise the response is incomplete. +If the requested component is compound and selectors are available, include the related theme calls scoped under the parent component's selector. Otherwise the response is incomplete. | Parameter | Type | Required | Description | | -------------- | ----------------------------------------------------------- | -------- | ------------------------------------- | @@ -536,14 +536,15 @@ AI: Perfect! Let me create that theme for you. **Compound example (combo, Angular):** ``` -AI: First, let me get compound selectors for combo. +AI: First, let me get the design tokens and related themes for combo. [calls get_component_design_tokens with component="combo"] -AI: Now I will generate themes for combo and its related components. -[calls create_component_theme with component="combo"] -[calls create_component_theme with component="input-group" selector="igx-combo igx-input-group"] -[calls create_component_theme with component="drop-down" selector=".igx-drop-down__list"] -[calls create_component_theme with component="checkbox" selector="igx-combo-item igx-checkbox"] +AI: Now I will generate themes for combo and its related components, + all scoped under the parent selector. +[calls create_component_theme with component="combo" platform="angular"] +[calls create_component_theme with component="input-group" selector="igx-combo"] +[calls create_component_theme with component="drop-down" selector="igx-combo"] +[calls create_component_theme with component="checkbox" selector="igx-combo"] ``` --- diff --git a/packages/mcp/src/__tests__/knowledge/component-metadata.test.ts b/packages/mcp/src/__tests__/knowledge/component-metadata.test.ts index 44ecc123..93b4f36e 100644 --- a/packages/mcp/src/__tests__/knowledge/component-metadata.test.ts +++ b/packages/mcp/src/__tests__/knowledge/component-metadata.test.ts @@ -4,7 +4,7 @@ * These tests verify: * 1. COMPONENT_METADATA unified data structure (selectors, variants, compound info) * 2. Accessor function behavior - * 3. Structural invariants (inline scope derivation, childScopes validity, etc.) + * 3. Structural invariants (compound field validation, etc.) * 4. TokenDerivation format validation */ @@ -180,59 +180,6 @@ describe("Component Metadata Knowledge Base", () => { } } }); - - it('childScopes references should be valid ("inline" or key in additionalScopes)', () => { - for (const [name, metadata] of Object.entries(COMPONENT_METADATA)) { - if (!metadata.compound?.childScopes) continue; - - const additionalScopeKeys = Object.keys( - metadata.compound.additionalScopes ?? {}, - ); - - for (const [childName, scopeTargets] of Object.entries( - metadata.compound.childScopes, - )) { - if (scopeTargets.angular) { - expect( - scopeTargets.angular === "inline" || - additionalScopeKeys.includes(scopeTargets.angular), - `${name} childScope '${childName}' angular target '${scopeTargets.angular}' should be 'inline' or a key in additionalScopes`, - ).toBe(true); - } - if (scopeTargets.webcomponents) { - expect( - scopeTargets.webcomponents === "inline" || - additionalScopeKeys.includes(scopeTargets.webcomponents), - `${name} childScope '${childName}' webcomponents target '${scopeTargets.webcomponents}' should be 'inline' or a key in additionalScopes`, - ).toBe(true); - } - } - } - }); - - it("no inline scope should appear in additionalScopes", () => { - for (const [name, metadata] of Object.entries(COMPONENT_METADATA)) { - if (!metadata.compound?.additionalScopes) continue; - - expect( - metadata.compound.additionalScopes, - `${name}.compound.additionalScopes should not have an 'inline' key`, - ).not.toHaveProperty("inline"); - } - }); - - it("childScopes children should be listed in relatedThemes", () => { - for (const [name, metadata] of Object.entries(COMPONENT_METADATA)) { - if (!metadata.compound?.childScopes) continue; - - for (const childName of Object.keys(metadata.compound.childScopes)) { - expect( - metadata.compound.relatedThemes, - `${name} childScope child '${childName}' should be in relatedThemes`, - ).toContain(childName); - } - } - }); }); // ===== Token Derivation Validation ===== @@ -676,22 +623,4 @@ describe("Component Metadata Knowledge Base", () => { }); // ===== Production Data Invariants ===== - - describe("production data invariants", () => { - it("should not contain stray or test scope entries", () => { - const validScopeNames = ["overlay", "input"]; // Known non-inline scope names - for (const [name, metadata] of Object.entries(COMPONENT_METADATA)) { - if (!metadata.compound?.additionalScopes) continue; - - for (const scopeName of Object.keys( - metadata.compound.additionalScopes, - )) { - expect( - validScopeNames, - `${name} additionalScopes contains unexpected scope '${scopeName}'`, - ).toContain(scopeName); - } - } - }); - }); }); diff --git a/packages/mcp/src/__tests__/tools/handlers/component-tokens.test.ts b/packages/mcp/src/__tests__/tools/handlers/component-tokens.test.ts index dc34dc98..d5dd3752 100644 --- a/packages/mcp/src/__tests__/tools/handlers/component-tokens.test.ts +++ b/packages/mcp/src/__tests__/tools/handlers/component-tokens.test.ts @@ -17,52 +17,48 @@ describe("handleGetComponentDesignTokens", () => { ); }); - it("shows two platform sections: Angular and WC/React/Blazor", async () => { + it("shows flat related themes list for compound components", async () => { const result = await handleGetComponentDesignTokens({ component: "date-range-picker", }); const text = result.content[0].text; - expect(text).toContain("**Angular:**"); - expect(text).toContain("**Web Components:**"); - // Old format should NOT be present - expect(text).not.toContain("**Scopes by Platform:**"); + expect(text).toContain( + "**Related themes:** `flat-button`, `input-group`, `calendar`", + ); + expect(text).toContain( + "Scope all related themes under the parent component selector:", + ); }); - it("shows overlay scope for Angular but omits it for WC group (date-range-picker)", async () => { + it("shows both Angular and WC platform lines for cross-platform compound", async () => { const result = await handleGetComponentDesignTokens({ component: "date-range-picker", }); const text = result.content[0].text; - // Angular section has both inline and overlay scopes - expect(text).toContain("| inline | `igx-date-range-picker` |"); - expect(text).toContain("| overlay | `.igx-date-picker` |"); - - // WC section has inline only - overlay row should NOT appear - expect(text).toContain("| inline | `igc-date-range-picker` |"); - // Overlay N/A row should be gone - expect(text).not.toContain("| overlay | N/A |"); + expect(text).toContain("- **Angular:** `igx-date-range-picker`"); + expect(text).toContain( + "- **Web Components / React / Blazor:** `igc-date-range-picker`", + ); }); - it("shows per-platform related themes for compound components", async () => { + it("omits WC platform line for Angular-only compound (time-picker)", async () => { const result = await handleGetComponentDesignTokens({ - component: "date-range-picker", + component: "time-picker", }); const text = result.content[0].text; - // Angular: calendar in overlay scope - expect(text).toContain("| `calendar` | overlay | `.igx-date-picker` |"); - // WC: calendar in inline scope - expect(text).toContain("| `calendar` | inline | `igc-date-range-picker` |"); + expect(text).toContain("- **Angular:** `igx-time-picker`"); + expect(text).not.toContain("Web Components / React / Blazor"); }); - it("shows inline scope for grid", async () => { + it("shows parent selector for grid compound", async () => { const result = await handleGetComponentDesignTokens({ component: "grid" }); const text = result.content[0].text; - expect(text).toContain("| inline | `igx-grid` |"); - expect(text).toContain("| inline | `igc-grid` |"); + expect(text).toContain("- **Angular:** `igx-grid`"); + expect(text).toContain("- **Web Components / React / Blazor:** `igc-grid`"); }); it("resolves theme aliases when theme is missing", async () => { @@ -95,13 +91,6 @@ describe("handleGetComponentDesignTokens", () => { // Should have tokens table expect(text).toContain("**Available Tokens"); - - // Should NOT have compound sections - expect(text).not.toContain("**Compound Component:**"); - expect(text).not.toContain("**Steps:**"); - expect(text).not.toContain("| Scope | Selector |"); - expect(text).not.toContain("**Token derivations:**"); - expect(text).not.toContain("**Guidance:**"); }); it("renders primary tokens from structured data", async () => { @@ -120,17 +109,16 @@ describe("handleGetComponentDesignTokens", () => { ); }); - it("renders banner with inline scope for both platform groups", async () => { + it("renders banner compound with both platform lines", async () => { const result = await handleGetComponentDesignTokens({ component: "banner", }); const text = result.content[0].text; - //Platforms should have inline scope - expect(text).toContain("**Angular:**"); - expect(text).toContain("**Web Components:**"); - // Banner should have inline scope for both - expect(text).toContain("| inline | `igx-banner` |"); - expect(text).toContain("| inline | `igc-banner` |"); + expect(text).toContain("**Related themes:** `flat-button`"); + expect(text).toContain("- **Angular:** `igx-banner`"); + expect(text).toContain( + "- **Web Components / React / Blazor:** `igc-banner`", + ); }); }); diff --git a/packages/mcp/src/knowledge/component-metadata.ts b/packages/mcp/src/knowledge/component-metadata.ts index ae24b574..7ced4ef5 100644 --- a/packages/mcp/src/knowledge/component-metadata.ts +++ b/packages/mcp/src/knowledge/component-metadata.ts @@ -23,14 +23,6 @@ export interface TokenDerivation { args?: Record; } -/** - * Platform-specific selector(s) for a named scope. - */ -export interface ScopeSelectors { - angular?: string | string[]; - webcomponents?: string | string[]; -} - /** * Information about a compound component (one that contains multiple themeable sub-components). */ @@ -39,13 +31,6 @@ export interface CompoundInfo { description: string; /** Related theme functions needed for full customization */ relatedThemes: string[]; - /** - * Non-inline scopes (e.g., overlay). Inline scope is always derived from base selectors. - * Most compounds won't have this field. - */ - additionalScopes?: Record; - /** Maps child theme name to a scope name per platform. Values: 'inline' or a key in additionalScopes */ - childScopes?: Record; /** Token derivation rules. Key: 'childTheme.childToken' */ tokenDerivations?: Record; /** Free-form guidance for edge cases */ @@ -259,15 +244,6 @@ If customizing the banner background, ensure flat-button foreground contrasts ag transform: "identity", }, }, - additionalScopes: { - overlay: { angular: ".igx-drop-down__list" }, - input: { angular: "igx-combo, .igx-drop-down__list" }, - }, - childScopes: { - "drop-down": { angular: "overlay" }, - checkbox: { angular: "overlay" }, - "input-group": { angular: "input" }, - }, guidance: "The combo's input-group, drop-down, and checkbox should share a consistent color scheme.", }, @@ -291,12 +267,6 @@ If customizing the banner background, ensure flat-button foreground contrasts ag transform: "identity", }, }, - additionalScopes: { - overlay: { angular: ".igx-drop-down__list" }, - }, - childScopes: { - "drop-down": { angular: "overlay" }, - }, guidance: "The simple-combo's input-group and drop-down should share a consistent color scheme.", }, @@ -307,14 +277,6 @@ If customizing the banner background, ensure flat-button foreground contrasts ag description: "The date-picker combines input, calendar, and flat-button components.", relatedThemes: ["flat-button", "input-group", "calendar"], - additionalScopes: { - overlay: { angular: ".igx-date-picker" }, - }, - childScopes: { - calendar: { angular: "overlay" }, - "flat-button": { angular: "overlay" }, - "input-group": { angular: "inline" }, - }, tokenDerivations: { "flat-button.foreground": { from: "calendar.content-background", @@ -334,14 +296,6 @@ If customizing the banner background, ensure flat-button foreground contrasts ag description: "The date-range-picker combines input, calendar, and flat-button components.", relatedThemes: ["flat-button", "input-group", "calendar"], - additionalScopes: { - overlay: { angular: ".igx-date-picker" }, - }, - childScopes: { - calendar: { angular: "overlay" }, - "flat-button": { angular: "overlay" }, - "input-group": { angular: "inline" }, - }, tokenDerivations: { "flat-button.foreground": { from: "calendar.content-background", @@ -571,12 +525,6 @@ chips for displaying conditions, and buttons/button-groups for adding and groupi description: "The select component combines input-group and drop-down components.", relatedThemes: ["input-group", "drop-down"], - additionalScopes: { - overlay: { angular: ".igx-drop-down__list" }, - }, - childScopes: { - "drop-down": { angular: "overlay" }, - }, tokenDerivations: { "input-group.focused-border-color": { from: "select.toggle-button-background", @@ -614,13 +562,6 @@ The drop-down background should match the select surface intent.`, compound: { description: "The time picker uses an input-group for the input field.", relatedThemes: ["input-group", "time-picker", "flat-button"], - additionalScopes: { - overlay: { angular: ".igx-time-picker" }, - }, - childScopes: { - "time-picker": { angular: "overlay" }, - "flat-button": { angular: "overlay" }, - }, guidance: `The time-picker input-group and the time-picker dial should share a consistent color scheme. \ The input-group text color should coordinate with the time-picker header.`, }, diff --git a/packages/mcp/src/knowledge/index.ts b/packages/mcp/src/knowledge/index.ts index 493e9600..b0da2595 100644 --- a/packages/mcp/src/knowledge/index.ts +++ b/packages/mcp/src/knowledge/index.ts @@ -36,7 +36,6 @@ export { isComponentAvailable, isCompoundComponent, isVariantTheme, - type ScopeSelectors, type TokenDerivation, VARIANT_THEME_NAMES, } from "./component-metadata.js"; diff --git a/packages/mcp/src/tools/descriptions.ts b/packages/mcp/src/tools/descriptions.ts index d996cbc1..e24c3518 100644 --- a/packages/mcp/src/tools/descriptions.ts +++ b/packages/mcp/src/tools/descriptions.ts @@ -749,15 +749,14 @@ export const TOOL_DESCRIPTIONS = { COMPOUND COMPONENTS: - Some components like "combo", "grid", "select" are compound - they use multiple internal components that each need their own theme - - The response includes "Related themes and token derivations" listing each related - theme and, where available, derivation hints showing how child token values relate - to parent/sibling tokens (e.g., "foreground → adaptive-contrast of calendar.content-background") + - The response lists related themes and, where available, token derivation hints + showing how child token values relate to parent/sibling tokens + (e.g., "foreground → adaptive-contrast of calendar.content-background") - Follow derivation hints when setting child token values. If the user specifies an explicit value, use that instead of the derived value. + - All related themes should be scoped under the parent component's selector - For each related theme: call get_component_design_tokens, then create_component_theme - using the selector assigned to that child under "Scopes by Platform" and "Related themes" - - Use \`@include tokens(child-theme(...))\` for each related theme inside the appropriate - scope selector + using the parent component's selector for the target platform VARIANTS INFO: - If you query a base component that has variants (e.g., "button"), the response @@ -919,19 +918,16 @@ export const TOOL_DESCRIPTIONS = { - Date Picker (compound) — child themes may use different scoped selectors per platform. - Follow token derivation hints and scope assignments from get_component_design_tokens: + Date Picker (compound) — all child themes use the parent component's selector. + Follow token derivation hints from get_component_design_tokens: 1) get_component_design_tokens { "component": "date-picker" } - 2) create_component_theme { "component": "date-picker", ... } - 3) create_component_theme { "component": "calendar", "selector": ".igx-date-picker", ... } - 4) create_component_theme { "component": "flat-button", "selector": ".igx-date-picker", ... } + 2) create_component_theme { "component": "date-picker", "platform": "angular", ... } + 3) create_component_theme { "component": "calendar", "selector": "igx-date-picker", ... } + 4) create_component_theme { "component": "flat-button", "selector": "igx-date-picker", ... } 5) create_component_theme { "component": "input-group", "selector": "igx-date-picker", ... } - Each call generates \`@include tokens($theme)\` inside the assigned scope selector. - - If you are targeting Angular, use the Angular selectors from the scopes table. - - If you are targeting Web Components, use the Web Components selectors from the scopes table. - - If you are targeting React, use the React selectors from the scopes table. - - If you are targeting Blazor, use the Blazor selectors from the scopes table. + Each child theme uses the parent's platform selector (e.g., \`igx-date-picker\` for Angular, + \`igc-date-picker\` for Web Components / React / Blazor). The tokens mixin emits --ig-{component}-{token} variables that child components consume via var() fallback — no per-child selectors needed. diff --git a/packages/mcp/src/tools/handlers/component-tokens.ts b/packages/mcp/src/tools/handlers/component-tokens.ts index 0efb3a01..03d9dcc8 100644 --- a/packages/mcp/src/tools/handlers/component-tokens.ts +++ b/packages/mcp/src/tools/handlers/component-tokens.ts @@ -1,4 +1,3 @@ -import type { CompoundInfo } from "../../knowledge/index.js"; import { COMPONENT_NAMES, getComponentSelector, @@ -10,7 +9,7 @@ import { resolveComponentTheme, searchComponents, } from "../../knowledge/index.js"; -import type { GetComponentDesignTokensParams, Platform } from "../schemas.js"; +import type { GetComponentDesignTokensParams } from "../schemas.js"; export async function handleGetComponentDesignTokens( params: GetComponentDesignTokensParams, @@ -18,60 +17,6 @@ export async function handleGetComponentDesignTokens( const { component } = params; const normalizedName = component.toLowerCase().trim(); - const formatSelectorList = (selectors?: string | string[] | null): string => { - if (!selectors || selectors.length === 0) { - return "N/A"; - } - - const selectorText = Array.isArray(selectors) - ? selectors.join(" | ") - : selectors; - - return `\`${selectorText}\``; - }; - - const getScopeSelectorForPlatform = ( - compoundInfo: CompoundInfo, - componentName: string, - scopeName: string, - platform: Platform, - ): string => { - // React/Blazor share Web Components selectors - const selectorPlatform: "angular" | "webcomponents" = - platform === "angular" ? "angular" : "webcomponents"; - - if (scopeName === "inline") { - // Derive inline scope from base selectors - const selectors = getComponentSelector(componentName, platform); - return formatSelectorList( - selectors.length > 0 - ? selectors.length === 1 - ? selectors[0] - : selectors - : null, - ); - } - - const scope = compoundInfo.additionalScopes?.[scopeName]; - if (!scope) { - return "N/A"; - } - - return formatSelectorList(scope[selectorPlatform]); - }; - - const resolveChildScopeName = ( - compoundInfo: CompoundInfo | undefined, - childThemeName: string, - platform: Platform, - ): string => { - // React/Blazor share Web Components scoping - const scopePlatform: "angular" | "webcomponents" = - platform === "angular" ? "angular" : "webcomponents"; - const childScope = compoundInfo?.childScopes?.[childThemeName]; - return childScope?.[scopePlatform] ?? "inline"; - }; - const resolution = resolveComponentTheme(normalizedName); const theme = resolution?.theme; @@ -116,14 +61,6 @@ ${suggestions.length === 0 ? `\nTotal available: ${COMPONENT_NAMES.length} compo }; } - // Platform groups for compound component output - const PLATFORM_GROUPS: { label: string; platform: Platform }[] = [ - { label: "Angular", platform: "angular" }, - { label: "Web Components", platform: "webcomponents" }, - { label: "Blazor", platform: "blazor" }, - { label: "React", platform: "react" }, - ]; - // Build response parts const responseParts: string[] = []; @@ -159,78 +96,38 @@ ${suggestions.length === 0 ? `\nTotal available: ${COMPONENT_NAMES.length} compo responseParts.push(compoundInfo.description); responseParts.push(""); - // 4. Steps - responseParts.push("**Steps:**"); - responseParts.push( - "1. Choose your platform and use the matching scopes below.", - ); + // 4. Related themes list responseParts.push( - "2. For each related theme: call `get_component_design_tokens`, then `create_component_theme` using the selector for that platform scope.", + `**Related themes:** ${compoundInfo.relatedThemes.map((t) => `\`${t}\``).join(", ")}`, ); - responseParts.push( - "3. Apply `@include tokens(child-theme(...))` inside the scope selector.", - ); - responseParts.push(""); - // 5. Per-platform sections — scopes derived from compoundInfo - // Collect all scope names: 'inline' + any additionalScopes keys - const allScopeNames = [ - "inline", - ...Object.keys(compoundInfo.additionalScopes ?? {}), - ]; - - for (const group of PLATFORM_GROUPS) { - responseParts.push(`**${group.label}:**`); - - // Scope table - only rows with non-N/A selectors - const scopeRows = allScopeNames - .map((scopeName) => { - const selectorText = getScopeSelectorForPlatform( - compoundInfo, - normalizedName, - scopeName, - group.platform, - ); - return { scopeName, selectorText }; - }) - .filter((row) => row.selectorText !== "N/A"); - - if (scopeRows.length > 0) { - responseParts.push("| Scope | Selector |"); - responseParts.push("| --- | --- |"); - responseParts.push( - scopeRows - .map((row) => `| ${row.scopeName} | ${row.selectorText} |`) - .join("\n"), - ); - responseParts.push(""); - } + // 5. Scoping instruction with per-platform parent selectors + const angularSelectors = getComponentSelector(normalizedName, "angular"); + const wcSelectors = getComponentSelector(normalizedName, "webcomponents"); + + const platformLines: string[] = []; + if (angularSelectors.length > 0) { + const selectorText = + angularSelectors.length === 1 + ? angularSelectors[0] + : angularSelectors.join(" | "); + platformLines.push(`- **Angular:** \`${selectorText}\``); + } + if (wcSelectors.length > 0) { + const selectorText = + wcSelectors.length === 1 ? wcSelectors[0] : wcSelectors.join(" | "); + platformLines.push( + `- **Web Components / React / Blazor:** \`${selectorText}\``, + ); + } - // Related themes table - responseParts.push(`**Related themes (${group.label})**`); - responseParts.push("| Theme | Scope | Selector |"); - responseParts.push("| --- | --- | --- |"); + if (platformLines.length > 0) { responseParts.push( - compoundInfo.relatedThemes - .map((relatedTheme) => { - const scopeName = resolveChildScopeName( - compoundInfo, - relatedTheme, - group.platform, - ); - const selectorText = getScopeSelectorForPlatform( - compoundInfo, - normalizedName, - scopeName, - group.platform, - ); - - return `| \`${relatedTheme}\` | ${scopeName} | ${selectorText} |`; - }) - .join("\n"), + "Scope all related themes under the parent component selector:", ); - responseParts.push(""); + responseParts.push(platformLines.join("\n")); } + responseParts.push(""); // 6. Token derivations (platform-agnostic) const derivationRows = compoundInfo.relatedThemes.flatMap( From 599c8b498f1090e8402074b1ccbf84e2c6739d19 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Wed, 1 Apr 2026 17:55:04 +0300 Subject: [PATCH 2/9] fix(mcp): models sometimes can't resolve children of main components Closes #534 --- .../.openspec.yaml | 2 + .../design.md | 140 +++++++++++ .../proposal.md | 31 +++ .../specs/child-component-resolution/spec.md | 144 +++++++++++ .../component-metadata-unification/spec.md | 68 +++++ .../specs/component-theming/spec.md | 96 +++++++ .../child-component-theme-resolution/tasks.md | 39 +++ .../knowledge/component-metadata.test.ts | 236 +++++++++++++++++- .../tools/handlers/component-tokens.test.ts | 56 +++++ .../__tests__/tools/handlers/handlers.test.ts | 58 +++++ packages/mcp/src/generators/css.ts | 22 +- packages/mcp/src/generators/sass.ts | 25 +- .../mcp/src/knowledge/component-metadata.ts | 78 +++++- .../mcp/src/knowledge/component-themes.ts | 5 +- packages/mcp/src/knowledge/index.ts | 1 + packages/mcp/src/tools/descriptions.ts | 39 ++- .../mcp/src/tools/handlers/component-theme.ts | 13 +- .../src/tools/handlers/component-tokens.ts | 13 + packages/mcp/src/tools/handlers/layout.ts | 6 + 19 files changed, 1032 insertions(+), 40 deletions(-) create mode 100644 openspec/changes/child-component-theme-resolution/.openspec.yaml create mode 100644 openspec/changes/child-component-theme-resolution/design.md create mode 100644 openspec/changes/child-component-theme-resolution/proposal.md create mode 100644 openspec/changes/child-component-theme-resolution/specs/child-component-resolution/spec.md create mode 100644 openspec/changes/child-component-theme-resolution/specs/component-metadata-unification/spec.md create mode 100644 openspec/changes/child-component-theme-resolution/specs/component-theming/spec.md create mode 100644 openspec/changes/child-component-theme-resolution/tasks.md diff --git a/openspec/changes/child-component-theme-resolution/.openspec.yaml b/openspec/changes/child-component-theme-resolution/.openspec.yaml new file mode 100644 index 00000000..0f528039 --- /dev/null +++ b/openspec/changes/child-component-theme-resolution/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-01 diff --git a/openspec/changes/child-component-theme-resolution/design.md b/openspec/changes/child-component-theme-resolution/design.md new file mode 100644 index 00000000..bef6b641 --- /dev/null +++ b/openspec/changes/child-component-theme-resolution/design.md @@ -0,0 +1,140 @@ +## Context + +The MCP component theming flow has three relationship types between component names and themes: + +1. **Direct** — component name matches a theme function (e.g., `avatar` → `avatar-theme()`) +2. **Same-element alias** — component shares another's theme but has its own selector (e.g., `textarea` → `input-group-theme()`, scoped to `.igx-input-group--textarea-group`) +3. **Compound** — parent component contains independently-themeable child components with their own theme functions (e.g., `combo` contains `input-group`, `drop-down`, `checkbox`) + +A fourth relationship exists in the UI but has no representation in the metadata: **child sub-components** whose styling is controlled by prefixed tokens on the parent's theme function (e.g., `list-item` → `list-theme()`'s `item-*` tokens). These children have their own DOM elements and selectors but no separate theme function. + +Currently, selector resolution for theming scope happens in three places independently: + +- `handleCreateComponentTheme` (handler, line ~188-198) +- `generateComponentTheme` (Sass generator, line ~432-439) +- `generateComponentThemeCss` (CSS generator, line ~261-267) + +All three call `getComponentSelector(componentName, platform)` using the component name passed to them. For child components, this must resolve to the **parent's** selector instead. + +## Goals / Non-Goals + +**Goals:** + +- Users can request theming for child sub-components by name and get correct results in a single tool call +- Generated code scopes tokens to the parent component's selector (where theme tokens are consumed) +- The `get_component_design_tokens` response clearly communicates the parent-child relationship +- Zero impact on existing alias, variant, and compound component flows + +**Non-Goals:** + +- Filtering tokens to only show child-relevant ones (e.g., showing only `item-*` tokens for `list-item`) — token descriptions are sufficient for LLM guidance +- Automatic derivation of child-to-parent mappings from token naming conventions — deferred until the token naming overhaul lands +- Supporting deeply nested child relationships (child-of-child) — no known use case + +## Decisions + +### Decision 1: Add `childOf` field to `ComponentMetadata` + +Add an optional `childOf?: string` field to the `ComponentMetadata` interface. When present, it names the parent component whose selectors should be used for theming scope. + +```typescript +export interface ComponentMetadata { + selectors: ComponentSelectors; + theme?: string; + childOf?: string; // NEW — parent component for selector resolution + variants?: string[]; + compound?: CompoundInfo; +} +``` + +A child component entry uses both `theme` and `childOf` together: + +- `theme` → resolves which theme function to use (existing mechanism via `resolveComponentTheme`) +- `childOf` → resolves which selector to scope the generated code to (new mechanism) + +**Why not reuse `theme` for selector resolution?** Because `theme` already has a defined meaning (same-element alias) where the component's own selector is correct. The `textarea` → `input-group` alias must continue using `textarea`'s selector. `childOf` is a distinct relationship that requires the parent's selector. + +**Why not a generic `selectorSource` field?** `childOf` communicates semantic intent (this is a child sub-component), not just a mechanical override. The handlers use this to generate appropriate messaging ("list-item is a child of list"). + +**Alternatives considered:** + +- Omitting `childOf` and relying on improved fuzzy search: rejected because it doesn't solve the scoping problem and still requires an extra round-trip +- Using `selectors: { angular: null, webcomponents: null }` with `theme` only: rejected because `null` selectors signal "not available on platform" which is incorrect — the child exists on the platform, it just inherits theming scope from its parent + +### Decision 2: Centralize theming selector resolution + +Add a new accessor function `getThemingSelector` to `component-metadata.ts` that encapsulates the `childOf` lookup: + +```typescript +export function getThemingSelector(componentName: string, platform: Platform): string[] { + const metadata = COMPONENT_METADATA[componentName]; + if (!metadata) return []; + + // Child components use parent's selector for theming scope + if (metadata.childOf) { + return getComponentSelector(metadata.childOf, platform); + } + + return getComponentSelector(componentName, platform); +} +``` + +All three selector resolution sites (handler, Sass generator, CSS generator) switch from `getComponentSelector` to `getThemingSelector` for determining the scope of generated theme code. `getComponentSelector` itself remains unchanged — it returns the component's own selectors, which is still needed for other purposes (e.g., platform availability checks, compound component scope tables). + +**Why centralize?** The same `childOf` check would otherwise be duplicated in three places. A single function ensures consistent behavior and is easier to test. + +### Decision 3: Handler messaging for child components + +When `handleGetComponentDesignTokens` resolves a child component (detected by `childOf` being present on the resolved metadata), it prepends a relationship note before the standard token output: + +``` +**Note:** `list-item` is a child of the `list` component. +Its styling is controlled through the `list` theme — all tokens below apply at the list level. +``` + +This uses the `childOf` value to auto-generate the message. No per-entry `guidance` field is needed for the relationship note. The existing token descriptions guide the LLM to the relevant tokens. + +The `handleCreateComponentTheme` handler needs no special messaging — it generates correct code silently by using `getThemingSelector`. + +### Decision 4: Initial child component entries + +The following 11 entries cover the most common child sub-components users would reference. Selection criteria: the child has a well-known selector name, meaningful tokens exist in the parent theme, and the entry fixes a real discoverability or scoping gap. + +| Entry | `theme` | `childOf` | Rationale | +| ------------------------ | ------------------- | ------------------- | ---------------------------------------------- | +| `list-item` | `"list"` | `"list"` | 25 item-\* tokens, very common | +| `list-header` | `"list"` | `"list"` | 2 header-\* tokens | +| `drop-down-item` | `"drop-down"` | `"drop-down"` | 18 item-\* tokens | +| `nav-drawer-item` | `"navdrawer"` | `"navdrawer"` | Fixes naming mismatch (nav-drawer ≠ navdrawer) | +| `tab-item` | `"tabs"` | `"tabs"` | 17 item-\* tokens | +| `step` | `"stepper"` | `"stepper"` | 53 step/indicator/title tokens | +| `card-header` | `"card"` | `"card"` | header-text-color, subtitle-text-color | +| `card-content` | `"card"` | `"card"` | content-text-color | +| `card-actions` | `"card"` | `"card"` | actions-text-color | +| `expansion-panel-header` | `"expansion-panel"` | `"expansion-panel"` | 7 header-_/disabled-_ tokens | +| `expansion-panel-body` | `"expansion-panel"` | `"expansion-panel"` | 2 body-\* tokens | + +Each entry includes real selectors for both platforms (or `null` where the child doesn't exist on a platform). + +**Why these 11 specifically?** + +- Tier 1 (list-item, list-header, drop-down-item, nav-drawer-item, tab-item): high-frequency components with rich token surfaces +- Tier 2 (step, card-_, expansion-panel-_): common sub-parts with meaningful token coverage +- Excluded: `combo-item` / `select-item` (no own tokens, styled via drop-down theme — already handled as compound), `card-media` / `banner-actions` (zero tokens in parent theme), calendar sub-views (users ask about "calendar" not "days-view") + +### Decision 5: Validation constraint for `childOf` + +The `childOf` value must reference an existing key in `COMPONENT_METADATA`. This is validated the same way `theme` alias targets are validated — at resolution time in the accessor function. No build-time validation is added. + +Additionally, `childOf` and `compound` are mutually exclusive on the same entry — a child sub-component cannot itself be a compound component. + +## Risks / Trade-offs + +**[Manual maintenance of child entries] → Mitigated by small, stable set** +The 11 entries are manually curated. If new child components are added to the UI libraries, corresponding metadata entries must be added here. The set is small and changes infrequently. When the token naming convention overhaul lands, automatic derivation can replace manual entries. + +**[`childOf` + `theme` always point to the same target] → Accepted redundancy** +For all initial entries, `childOf` and `theme` have the same value (e.g., both `"list"`). This is because the parent component name matches the theme name. In theory they could diverge (if a parent used a theme alias itself), but currently they don't. The two fields serve different purposes (theme function vs. selector scope), so the redundancy is acceptable for clarity. + +**[LLM may still set tokens that don't apply to the child] → Acceptable** +Since we show all parent tokens unfiltered, the LLM could set `header-background` when the user asked about `list-item`. The token descriptions (e.g., "The list header background color") are the primary guardrail. This is a conscious trade-off: filtering tokens by prefix is fragile with current naming and would be worse than showing all tokens with good descriptions. diff --git a/openspec/changes/child-component-theme-resolution/proposal.md b/openspec/changes/child-component-theme-resolution/proposal.md new file mode 100644 index 00000000..c175950f --- /dev/null +++ b/openspec/changes/child-component-theme-resolution/proposal.md @@ -0,0 +1,31 @@ +## Why + +Child sub-components (e.g., `igx-list-item`, `igx-card-header`, `igx-step`) don't have their own theme functions — their styling is controlled through tokens on the parent theme (e.g., `list-theme()` has `item-background`, `item-text-color`, etc.). When a user asks the MCP to style a child component by name, the system either returns "not found" or requires an extra round-trip via fuzzy search. In some cases (e.g., `nav-drawer-item` → `navdrawer`), fuzzy search fails entirely due to naming mismatches. + +Additionally, the existing `theme` alias mechanism (used by `textarea` → `input-group`) scopes generated code to the child's own CSS selector. For child sub-components, the generated code must scope to the **parent's** selector instead, since the parent's theme tokens are consumed at the parent element level. This requires a new relationship type distinct from the existing same-element alias. + +## What Changes + +- Add a `childOf` optional field to the `ComponentMetadata` interface, naming the parent component whose selector should be used for theming scope +- Add ~11 child component entries to `COMPONENT_METADATA` using both `theme` (for theme function resolution) and `childOf` (for selector resolution) +- Modify `handleGetComponentDesignTokens` to prepend a relationship note when resolving a child component (e.g., "list-item is a child of list. Its styling is controlled through the list theme.") +- Modify `handleCreateComponentTheme` and `generateComponentTheme` to resolve the scoping selector from `COMPONENT_METADATA[childOf].selectors` instead of the child's own selectors when `childOf` is present +- Modify `generateComponentThemeCss` to apply the same parent-selector resolution for CSS output + +## Capabilities + +### New Capabilities + +- `child-component-resolution`: Defines the `childOf` metadata field, child-to-parent selector resolution behavior, handler messaging for child components, and the initial set of child component entries + +### Modified Capabilities + +- `component-metadata-unification`: The `ComponentMetadata` interface gains a new optional `childOf` field. Existing accessor functions are unchanged, but a new accessor may be needed for parent selector lookup. +- `component-theming`: The `get_component_design_tokens` and `create_component_theme` handlers gain child-component-aware behavior (relationship messaging and parent-scoped selectors) + +## Impact + +- **Code**: `component-metadata.ts` (interface + data entries), `component-tokens.ts` handler, `component-theme.ts` handler, `generators/sass.ts`, `generators/css.ts` +- **Tests**: New test cases for child resolution in both handlers and generators; existing tests unchanged (no breaking changes to current aliases) +- **APIs**: No tool schema changes — the MCP tools accept the same parameters. The behavioral change is internal to resolution logic. +- **Rollback**: Remove the `childOf` field from the interface and delete the child entries from `COMPONENT_METADATA`. All existing functionality (theme aliases, compound components) is unaffected. diff --git a/openspec/changes/child-component-theme-resolution/specs/child-component-resolution/spec.md b/openspec/changes/child-component-theme-resolution/specs/child-component-resolution/spec.md new file mode 100644 index 00000000..b6194e01 --- /dev/null +++ b/openspec/changes/child-component-theme-resolution/specs/child-component-resolution/spec.md @@ -0,0 +1,144 @@ +## ADDED Requirements + +### Requirement: ComponentMetadata supports childOf field + +The `ComponentMetadata` interface SHALL include an optional `childOf` field of type `string` that names the parent component whose selectors are used for theming scope. When `childOf` is present, `theme` MUST also be present on the same entry. + +#### Scenario: Child component entry has both childOf and theme + +- **GIVEN** a component metadata entry for `list-item` +- **WHEN** the entry is inspected +- **THEN** `childOf` is `"list"` and `theme` is `"list"` +- **AND** `selectors` contains the child's own platform selectors (e.g., `angular: "igx-list-item"`) + +#### Scenario: childOf and compound are mutually exclusive + +- **GIVEN** a component metadata entry with `childOf` set +- **WHEN** the entry is inspected +- **THEN** `compound` SHALL NOT be present on the same entry + +#### Scenario: childOf references a valid parent + +- **GIVEN** a component metadata entry with `childOf: "list"` +- **WHEN** the metadata is loaded +- **THEN** `COMPONENT_METADATA["list"]` SHALL exist +- **AND** `COMPONENT_METADATA["list"].selectors` SHALL have at least one non-null platform selector + +### Requirement: Theming selector resolution uses parent for child components + +A `getThemingSelector(componentName, platform)` accessor function SHALL return the selector to use for scoping generated theme code. For child components (where `childOf` is set), it SHALL return the parent's selectors. For all other components, it SHALL return the component's own selectors. + +#### Scenario: Child component resolves to parent selector + +- **WHEN** `getThemingSelector("list-item", "angular")` is called +- **THEN** it returns `["igx-list"]` (the parent `list` component's Angular selector) + +#### Scenario: Child component resolves to parent selector on Web Components + +- **WHEN** `getThemingSelector("nav-drawer-item", "webcomponents")` is called +- **THEN** it returns `["igc-nav-drawer"]` (the parent `navdrawer` component's WC selector) + +#### Scenario: Non-child component resolves to own selector + +- **WHEN** `getThemingSelector("avatar", "angular")` is called +- **THEN** it returns `["igx-avatar"]` (the component's own selector, same as `getComponentSelector`) + +#### Scenario: Same-element alias resolves to own selector + +- **WHEN** `getThemingSelector("textarea", "angular")` is called +- **THEN** it returns `[".igx-input-group--textarea-group"]` (the component's own selector, not the aliased `input-group` selector) + +#### Scenario: Unknown component returns empty + +- **WHEN** `getThemingSelector("nonexistent", "angular")` is called +- **THEN** it returns `[]` + +### Requirement: getComponentSelector is unchanged + +The existing `getComponentSelector` function SHALL continue to return the component's own selectors regardless of `childOf`. It is NOT affected by the child component resolution. + +#### Scenario: getComponentSelector ignores childOf + +- **WHEN** `getComponentSelector("list-item", "angular")` is called +- **THEN** it returns `["igx-list-item"]` (the child's own selector, not the parent's) + +### Requirement: Initial child component entries + +The following child component entries SHALL be present in `COMPONENT_METADATA`. Each entry SHALL include `selectors` with real platform values, `theme` pointing to the parent's theme, and `childOf` pointing to the parent component. + +#### Scenario: list-item entry + +- **WHEN** `COMPONENT_METADATA["list-item"]` is inspected +- **THEN** `selectors.angular` is `"igx-list-item"` and `selectors.webcomponents` is `"igc-list-item"` +- **AND** `theme` is `"list"` and `childOf` is `"list"` + +#### Scenario: list-header entry + +- **WHEN** `COMPONENT_METADATA["list-header"]` is inspected +- **THEN** `selectors.angular` is `"igx-list-header"` or the appropriate Angular selector +- **AND** `selectors.webcomponents` is `"igc-list-header"` +- **AND** `theme` is `"list"` and `childOf` is `"list"` + +#### Scenario: drop-down-item entry + +- **WHEN** `COMPONENT_METADATA["drop-down-item"]` is inspected +- **THEN** `selectors.angular` is `"igx-drop-down-item"` and `selectors.webcomponents` is `"igc-dropdown-item"` +- **AND** `theme` is `"drop-down"` and `childOf` is `"drop-down"` + +#### Scenario: nav-drawer-item entry + +- **WHEN** `COMPONENT_METADATA["nav-drawer-item"]` is inspected +- **THEN** `selectors.angular` is `"igx-nav-drawer"` or the appropriate Angular selector +- **AND** `selectors.webcomponents` is `"igc-nav-drawer-item"` +- **AND** `theme` is `"navdrawer"` and `childOf` is `"navdrawer"` + +#### Scenario: tab-item entry + +- **WHEN** `COMPONENT_METADATA["tab-item"]` is inspected +- **THEN** `selectors.angular` is `"igx-tab-item"` and `selectors.webcomponents` is `"igc-tab"` +- **AND** `theme` is `"tabs"` and `childOf` is `"tabs"` + +#### Scenario: step entry + +- **WHEN** `COMPONENT_METADATA["step"]` is inspected +- **THEN** `selectors.angular` is `"igx-step"` and `selectors.webcomponents` is `"igc-step"` +- **AND** `theme` is `"stepper"` and `childOf` is `"stepper"` + +#### Scenario: card-header entry + +- **WHEN** `COMPONENT_METADATA["card-header"]` is inspected +- **THEN** `selectors.angular` is `"igx-card-header"` and `selectors.webcomponents` is `"igc-card-header"` +- **AND** `theme` is `"card"` and `childOf` is `"card"` + +#### Scenario: card-content entry + +- **WHEN** `COMPONENT_METADATA["card-content"]` is inspected +- **THEN** `selectors.angular` is `"igx-card-content"` and `selectors.webcomponents` is `"igc-card-content"` +- **AND** `theme` is `"card"` and `childOf` is `"card"` + +#### Scenario: card-actions entry + +- **WHEN** `COMPONENT_METADATA["card-actions"]` is inspected +- **THEN** `selectors.angular` is `"igx-card-actions"` and `selectors.webcomponents` is `"igc-card-actions"` +- **AND** `theme` is `"card"` and `childOf` is `"card"` + +#### Scenario: expansion-panel-header entry + +- **WHEN** `COMPONENT_METADATA["expansion-panel-header"]` is inspected +- **THEN** `selectors.angular` is `"igx-expansion-panel-header"` and `selectors.webcomponents` is a valid WC selector or `null` +- **AND** `theme` is `"expansion-panel"` and `childOf` is `"expansion-panel"` + +#### Scenario: expansion-panel-body entry + +- **WHEN** `COMPONENT_METADATA["expansion-panel-body"]` is inspected +- **THEN** `selectors.angular` is `"igx-expansion-panel-body"` and `selectors.webcomponents` is a valid WC selector or `null` +- **AND** `theme` is `"expansion-panel"` and `childOf` is `"expansion-panel"` + +### Requirement: Child entries do not appear in VARIANT_THEME_NAMES + +Child component entries SHALL NOT have a `variants` field. The `VARIANT_THEME_NAMES` set SHALL NOT contain any child component names. + +#### Scenario: Child entry excluded from variant set + +- **WHEN** `isVariantTheme("list-item")` is called +- **THEN** it returns `false` diff --git a/openspec/changes/child-component-theme-resolution/specs/component-metadata-unification/spec.md b/openspec/changes/child-component-theme-resolution/specs/component-metadata-unification/spec.md new file mode 100644 index 00000000..b95ee649 --- /dev/null +++ b/openspec/changes/child-component-theme-resolution/specs/component-metadata-unification/spec.md @@ -0,0 +1,68 @@ +## MODIFIED Requirements + +### Requirement: Single unified component metadata map + +All component metadata (selectors, variants, compound info, child-of-parent relationships) SHALL be stored in a single `COMPONENT_METADATA` map exported from `component-metadata.ts`. Each component SHALL have exactly one entry keyed by its theme name or child component name. + +#### Scenario: Simple component lookup + +- **WHEN** a simple component (e.g., `avatar`) is looked up in `COMPONENT_METADATA` +- **THEN** the entry contains a `selectors` field with `angular` and `webcomponents` keys +- **AND** no `compound`, `variants`, or `childOf` fields are present + +#### Scenario: Compound component lookup + +- **WHEN** a compound component (e.g., `combo`) is looked up in `COMPONENT_METADATA` +- **THEN** the entry contains `selectors` and a `compound` field +- **AND** `compound` includes `description`, `relatedThemes`, and optionally `tokenDerivations`, `guidance`, `additionalScopes`, and `childScopes` + +#### Scenario: Variant component lookup + +- **WHEN** a base component with variants (e.g., `button`) is looked up in `COMPONENT_METADATA` +- **THEN** the entry contains `selectors` and a `variants` field listing variant theme names + +#### Scenario: Child component lookup + +- **WHEN** a child component (e.g., `list-item`) is looked up in `COMPONENT_METADATA` +- **THEN** the entry contains `selectors`, `theme`, and `childOf` fields +- **AND** `childOf` names the parent component whose selectors are used for theming scope +- **AND** no `compound` or `variants` fields are present + +### Requirement: Accessor functions preserve existing signatures + +All public accessor functions SHALL maintain their existing call signatures and return types. New accessor functions MAY be added for child component resolution. + +#### Scenario: getComponentSelector unchanged + +- **WHEN** `getComponentSelector(name, platform)` is called +- **THEN** it returns the same value as before, read from `COMPONENT_METADATA[name].selectors[platform]` + +#### Scenario: isCompoundComponent uses unified map + +- **WHEN** `isCompoundComponent(name)` is called +- **THEN** it returns `true` if and only if `COMPONENT_METADATA[name]?.compound` is defined + +#### Scenario: hasVariants uses embedded field + +- **WHEN** `hasVariants(name)` is called +- **THEN** it returns `true` if and only if `COMPONENT_METADATA[name]?.variants` is defined and non-empty + +#### Scenario: getCompoundComponentInfo returns full compound data + +- **WHEN** `getCompoundComponentInfo(name)` is called for a compound component +- **THEN** it returns the full `CompoundInfo` object including `description`, `relatedThemes`, `tokenDerivations`, `guidance`, `additionalScopes`, and `childScopes` + +#### Scenario: getCompoundThemingInfo is eliminated + +- **WHEN** the codebase is searched for `getCompoundThemingInfo` +- **THEN** no references exist — the function has been removed along with all call sites + +#### Scenario: getCompoundSelector is eliminated + +- **WHEN** the codebase is searched for `getCompoundSelector` +- **THEN** no references exist — the function has been removed (it had no production callers) + +#### Scenario: New getThemingSelector accessor added + +- **WHEN** `getThemingSelector(name, platform)` is called +- **THEN** it returns the parent's selectors if `childOf` is set, or the component's own selectors otherwise diff --git a/openspec/changes/child-component-theme-resolution/specs/component-theming/spec.md b/openspec/changes/child-component-theme-resolution/specs/component-theming/spec.md new file mode 100644 index 00000000..f9c42d31 --- /dev/null +++ b/openspec/changes/child-component-theme-resolution/specs/component-theming/spec.md @@ -0,0 +1,96 @@ +## MODIFIED Requirements + +### Requirement: Component token schemas are exposed + +The `get_component_design_tokens` tool SHALL use an instruction-oriented output format that varies based on whether the component is compound, simple, or a child sub-component. For compound components, the response SHALL include numbered steps, per-platform scope tables, related theme tables, token derivations, and guidance. For simple components, the response SHALL include the theme function, primary tokens, and the token table without compound sections. For child sub-components, the response SHALL include a relationship note followed by the full parent theme's tokens. + +#### Scenario: Compound component response uses instruction-oriented format + +- **WHEN** `get_component_design_tokens` is called for a compound component +- **THEN** the response opens with `Implement a theme for the \`\` component using the following guidance.` +- **AND** includes a numbered **Steps** section instructing the model to identify the platform, call `get_component_design_tokens` for each related theme, and apply themes to scopes +- **AND** includes per-platform sections with scope tables and related theme tables + +#### Scenario: Simple component response omits compound sections + +- **WHEN** `get_component_design_tokens` is called for a non-compound component +- **THEN** the response opens with `Implement a theme for the \`\` component using the following guidance.` +- **AND** includes the theme function name, primary tokens, and available tokens table +- **AND** does NOT include steps, scope tables, related theme tables, token derivations, or guidance sections + +#### Scenario: Child component response includes relationship note + +- **WHEN** `get_component_design_tokens` is called for a child component (one with `childOf` set in metadata) +- **THEN** the response includes a note stating that the component is a child of the parent component +- **AND** the note states that styling is controlled through the parent's theme +- **AND** the response shows the full parent theme's tokens (unfiltered) +- **AND** the theme function name shown is the parent's theme function (e.g., `list-theme()` for `list-item`) + +#### Scenario: Child component relationship note format + +- **WHEN** `get_component_design_tokens` is called for `list-item` +- **THEN** the response includes text indicating `list-item` is a child of `list` +- **AND** indicates that tokens apply at the list level + +#### Scenario: Missing selector entries are handled + +- **WHEN** a compound component has related themes without scoped selectors (e.g., selector is `TODO`) +- **THEN** the checklist marks those related themes as skipped and explains that selector data is missing + +#### Scenario: Token schema lookup + +- **WHEN** `get_component_design_tokens` is called with a component name +- **THEN** the response lists supported tokens and variant hints + +### Requirement: Generated theme code uses parent selector for child components + +The `create_component_theme` tool SHALL scope generated theme code to the parent component's selector when the component has a `childOf` field. This applies to both Sass and CSS output. + +#### Scenario: Sass output for child component uses parent selector + +- **GIVEN** `create_component_theme` is called with `component: "list-item"` and `platform: "angular"` +- **WHEN** no custom `selector` is provided +- **THEN** the generated Sass code scopes the `@include tokens(...)` call to `igx-list` (the parent's selector) +- **AND** the theme function call uses `list-theme()` + +#### Scenario: CSS output for child component uses parent selector + +- **GIVEN** `create_component_theme` is called with `component: "list-item"`, `platform: "webcomponents"`, and `output: "css"` +- **WHEN** no custom `selector` is provided +- **THEN** the generated CSS scopes custom properties to `igc-list` (the parent's selector) + +#### Scenario: Custom selector overrides parent selector for child component + +- **GIVEN** `create_component_theme` is called with `component: "list-item"` and `selector: ".my-custom-list"` +- **WHEN** a custom selector is provided +- **THEN** the generated code uses `.my-custom-list` as the scope +- **AND** the parent selector resolution is bypassed + +#### Scenario: Same-element alias continues to use own selector + +- **GIVEN** `create_component_theme` is called with `component: "textarea"` and `platform: "angular"` +- **WHEN** no custom `selector` is provided +- **THEN** the generated code scopes to `.igx-input-group--textarea-group` (textarea's own selector) +- **AND** NOT to `igx-input-group` (the aliased theme's selector) + +#### Scenario: Direct component continues to use own selector + +- **GIVEN** `create_component_theme` is called with `component: "avatar"` and `platform: "angular"` +- **WHEN** no custom `selector` is provided +- **THEN** the generated code scopes to `igx-avatar` (the component's own selector) + +### Requirement: Child component platform availability uses own selectors + +Platform availability checks for child components SHALL use the child's own selectors, not the parent's. A child component is available on a platform if its own `selectors` entry for that platform is non-null. + +#### Scenario: Child component available on both platforms + +- **GIVEN** `list-item` has selectors `{ angular: "igx-list-item", webcomponents: "igc-list-item" }` +- **WHEN** `isComponentAvailable("list-item", "angular")` is called +- **THEN** it returns `true` + +#### Scenario: Child component not available on a platform + +- **GIVEN** `expansion-panel-body` has `selectors.webcomponents` set to `null` +- **WHEN** `isComponentAvailable("expansion-panel-body", "webcomponents")` is called +- **THEN** it returns `false` diff --git a/openspec/changes/child-component-theme-resolution/tasks.md b/openspec/changes/child-component-theme-resolution/tasks.md new file mode 100644 index 00000000..1d23b180 --- /dev/null +++ b/openspec/changes/child-component-theme-resolution/tasks.md @@ -0,0 +1,39 @@ +## 1. Interface and accessor + +- [x] 1.1 Add `childOf?: string` to the `ComponentMetadata` interface in `component-metadata.ts` +- [x] 1.2 Add `getThemingSelector(componentName, platform)` accessor function that returns parent selectors when `childOf` is set, own selectors otherwise +- [x] 1.3 Export `getThemingSelector` from `knowledge/index.ts` + +## 2. Child component entries + +- [x] 2.1 Add `list-item` and `list-header` entries to `COMPONENT_METADATA` with selectors, `theme: "list"`, and `childOf: "list"` +- [x] 2.2 Add `drop-down-item` entry with selectors, `theme: "drop-down"`, and `childOf: "drop-down"` +- [x] 2.3 Add `nav-drawer-item` entry with selectors, `theme: "navdrawer"`, and `childOf: "navdrawer"` +- [x] 2.4 Add `tab-item` entry with selectors, `theme: "tabs"`, and `childOf: "tabs"` +- [x] 2.5 Add `step` entry with selectors, `theme: "stepper"`, and `childOf: "stepper"` +- [x] 2.6 Add `card-header`, `card-content`, and `card-actions` entries with selectors, `theme: "card"`, and `childOf: "card"` +- [x] 2.7 Add `expansion-panel-header` and `expansion-panel-body` entries with selectors, `theme: "expansion-panel"`, and `childOf: "expansion-panel"` + +## 3. Handler: get_component_design_tokens + +- [x] 3.1 In `handleGetComponentDesignTokens`, after resolving the theme, detect `childOf` on the metadata entry and prepend a relationship note (e.g., "`list-item` is a child of the `list` component. Its styling is controlled through the `list` theme.") + +## 4. Selector resolution in generators + +- [x] 4.1 In `handleCreateComponentTheme` (`component-theme.ts`), switch from `getComponentSelector` to `getThemingSelector` for determining the default scoping selector +- [x] 4.2 In `generateComponentTheme` (`generators/sass.ts`), switch from `getComponentSelector` to `getThemingSelector` for the fallback selector resolution +- [x] 4.3 In `generateComponentThemeCss` (`generators/css.ts`), switch from `getComponentSelector` to `getThemingSelector` for the default selector resolution + +## 5. Tests + +- [x] 5.1 Add unit tests for `getThemingSelector`: child resolves to parent, non-child resolves to own, same-element alias resolves to own, unknown returns empty +- [x] 5.2 Add unit tests for child component metadata entries: verify `childOf`, `theme`, and `selectors` are correct for all 11 entries +- [x] 5.3 Add handler test for `get_component_design_tokens("list-item")`: verify the response includes the relationship note and the list theme's tokens +- [x] 5.4 Add handler test for `create_component_theme("list-item", ...)`: verify Sass output scopes to the parent selector (`igx-list` / `igc-list`) +- [x] 5.5 Add handler test for `create_component_theme("list-item", ..., output: "css")`: verify CSS output scopes to the parent selector +- [x] 5.6 Verify existing tests pass: `textarea` alias, compound components, direct components all unchanged + +## 6. Verify end-to-end + +- [x] 6.1 Run the full test suite (`vitest`) and confirm all tests pass +- [x] 6.2 Build the project and confirm no type errors diff --git a/packages/mcp/src/__tests__/knowledge/component-metadata.test.ts b/packages/mcp/src/__tests__/knowledge/component-metadata.test.ts index 93b4f36e..8a7cbc1a 100644 --- a/packages/mcp/src/__tests__/knowledge/component-metadata.test.ts +++ b/packages/mcp/src/__tests__/knowledge/component-metadata.test.ts @@ -15,6 +15,7 @@ import { getComponentSelector, getComponentsForPlatform, getCompoundComponentInfo, + getThemingSelector, getTokenDerivationsForChild, getVariants, hasVariants, @@ -36,8 +37,9 @@ describe("Component Metadata Knowledge Base", () => { expect(Object.keys(COMPONENT_METADATA).length).toBeGreaterThan(0); }); - it("each entry should have a selectors object with angular and webcomponents properties", () => { + it("non-childOf entries should have a selectors object with angular and webcomponents properties", () => { for (const [name, metadata] of Object.entries(COMPONENT_METADATA)) { + if (metadata.childOf) continue; // childOf entries don't need selectors expect(metadata, `${name} should have selectors`).toHaveProperty( "selectors", ); @@ -52,8 +54,19 @@ describe("Component Metadata Knowledge Base", () => { } }); + it("childOf entries should not have selectors", () => { + for (const [name, metadata] of Object.entries(COMPONENT_METADATA)) { + if (!metadata.childOf) continue; + expect( + metadata.selectors, + `${name} has childOf and should not have selectors`, + ).toBeUndefined(); + } + }); + it("selectors should be strings, arrays of strings, or null", () => { for (const [name, metadata] of Object.entries(COMPONENT_METADATA)) { + if (!metadata.selectors) continue; // childOf entries have no selectors const { angular, webcomponents } = metadata.selectors; const angularIsValid = angular === null || @@ -321,7 +334,7 @@ describe("Component Metadata Knowledge Base", () => { it("should normalize single selector to array", () => { const componentWithSingleSelector = Object.entries( COMPONENT_METADATA, - ).find(([, m]) => typeof m.selectors.angular === "string"); + ).find(([, m]) => m.selectors && typeof m.selectors.angular === "string"); if (componentWithSingleSelector) { const [name] = componentWithSingleSelector; @@ -334,13 +347,13 @@ describe("Component Metadata Knowledge Base", () => { it("should preserve array selectors as-is", () => { const componentWithArraySelector = Object.entries( COMPONENT_METADATA, - ).find(([, m]) => Array.isArray(m.selectors.angular)); + ).find(([, m]) => m.selectors && Array.isArray(m.selectors.angular)); if (componentWithArraySelector) { const [name, m] = componentWithArraySelector; const result = getComponentSelector(name, "angular"); expect(Array.isArray(result)).toBe(true); - expect(result.length).toBe((m.selectors.angular as string[]).length); + expect(result.length).toBe((m.selectors!.angular as string[]).length); } }); }); @@ -348,7 +361,8 @@ describe("Component Metadata Knowledge Base", () => { describe("isComponentAvailable()", () => { it("should return true for components available on platform", () => { const component = Object.entries(COMPONENT_METADATA).find( - ([, m]) => m.selectors.angular !== null, + ([, m]) => + m.selectors?.angular !== null && m.selectors?.angular !== undefined, ); if (component) { expect(isComponentAvailable(component[0], "angular")).toBe(true); @@ -357,7 +371,7 @@ describe("Component Metadata Knowledge Base", () => { it("should return false for components not available on platform", () => { const component = Object.entries(COMPONENT_METADATA).find( - ([, m]) => m.selectors.angular === null, + ([, m]) => m.selectors && m.selectors.angular === null, ); if (component) { expect(isComponentAvailable(component[0], "angular")).toBe(false); @@ -367,6 +381,10 @@ describe("Component Metadata Knowledge Base", () => { it("should return false for unknown components", () => { expect(isComponentAvailable("__nonexistent__", "angular")).toBe(false); }); + + it("should return false for childOf entries (no selectors)", () => { + expect(isComponentAvailable("list-item", "angular")).toBe(false); + }); }); describe("getComponentsForPlatform()", () => { @@ -383,9 +401,15 @@ describe("Component Metadata Knowledge Base", () => { it("should only include components available on that platform", () => { const angularComponents = getComponentsForPlatform("angular"); for (const name of angularComponents) { - expect(COMPONENT_METADATA[name].selectors.angular).not.toBeNull(); + expect(COMPONENT_METADATA[name].selectors!.angular).not.toBeNull(); } }); + + it("should not include childOf entries", () => { + const angularComponents = getComponentsForPlatform("angular"); + expect(angularComponents).not.toContain("list-item"); + expect(angularComponents).not.toContain("card-header"); + }); }); describe("getComponentPlatformAvailability()", () => { @@ -622,5 +646,201 @@ describe("Component Metadata Knowledge Base", () => { }); }); - // ===== Production Data Invariants ===== + // ===== Child Component (childOf) Tests ===== + + describe("childOf structure", () => { + const CHILD_ENTRIES = Object.entries(COMPONENT_METADATA).filter( + ([_, m]) => m.childOf, + ); + + it("should have child component entries", () => { + expect(CHILD_ENTRIES.length).toBeGreaterThan(0); + }); + + it("childOf entries should not have theme (resolved via parent)", () => { + for (const [name, metadata] of CHILD_ENTRIES) { + expect( + metadata.theme, + `${name} has childOf and should not have theme (theme is resolved via childOf)`, + ).toBeUndefined(); + } + }); + + it("childOf should reference an existing component in COMPONENT_METADATA", () => { + for (const [name, metadata] of CHILD_ENTRIES) { + expect( + COMPONENT_METADATA, + `${name}.childOf '${metadata.childOf}' should exist in COMPONENT_METADATA`, + ).toHaveProperty(metadata.childOf!); + } + }); + + it("childOf parent should have at least one non-null selector", () => { + for (const [name, metadata] of CHILD_ENTRIES) { + const parent = COMPONENT_METADATA[metadata.childOf!]; + const hasSelector = + parent.selectors?.angular !== null || + parent.selectors?.webcomponents !== null; + + expect( + hasSelector, + `${name}.childOf parent '${metadata.childOf}' should have at least one non-null selector`, + ).toBe(true); + } + }); + + it("childOf and compound should be mutually exclusive", () => { + for (const [name, metadata] of CHILD_ENTRIES) { + expect( + metadata.compound, + `${name} has childOf and should not have compound`, + ).toBeUndefined(); + } + }); + + it("child entries should not have variants", () => { + for (const [name, metadata] of CHILD_ENTRIES) { + expect( + metadata.variants, + `${name} has childOf and should not have variants`, + ).toBeUndefined(); + } + }); + + it("child entries should not appear in VARIANT_THEME_NAMES", () => { + for (const [name] of CHILD_ENTRIES) { + expect( + VARIANT_THEME_NAMES.has(name), + `child component '${name}' should not be in VARIANT_THEME_NAMES`, + ).toBe(false); + } + }); + }); + + describe("specific child component entries", () => { + it("accordion-header should be a child of accordion", () => { + const entry = COMPONENT_METADATA["accordion-header"]; + expect(entry).toBeDefined(); + expect(entry.childOf).toBe("accordion"); + }); + + it("accordion-body should be a child of accordion", () => { + const entry = COMPONENT_METADATA["accordion-body"]; + expect(entry).toBeDefined(); + expect(entry.childOf).toBe("accordion"); + }); + + it("list-item should be a child of list", () => { + const entry = COMPONENT_METADATA["list-item"]; + + expect(entry).toBeDefined(); + expect(entry.childOf).toBe("list"); + expect(entry.selectors).toBeUndefined(); + expect(entry.theme).toBeUndefined(); + }); + + it("list-header should be a child of list", () => { + const entry = COMPONENT_METADATA["list-header"]; + + expect(entry).toBeDefined(); + expect(entry.childOf).toBe("list"); + }); + + it("drop-down-item should be a child of drop-down", () => { + const entry = COMPONENT_METADATA["drop-down-item"]; + + expect(entry).toBeDefined(); + expect(entry.childOf).toBe("drop-down"); + }); + + it("nav-drawer-item should be a child of navdrawer", () => { + const entry = COMPONENT_METADATA["nav-drawer-item"]; + + expect(entry).toBeDefined(); + expect(entry.childOf).toBe("navdrawer"); + }); + + it("tab-item should be a child of tabs", () => { + const entry = COMPONENT_METADATA["tab-item"]; + + expect(entry).toBeDefined(); + expect(entry.childOf).toBe("tabs"); + }); + + it("step should be a child of stepper", () => { + const entry = COMPONENT_METADATA.step; + + expect(entry).toBeDefined(); + expect(entry.childOf).toBe("stepper"); + }); + + it("card-header should be a child of card", () => { + const entry = COMPONENT_METADATA["card-header"]; + + expect(entry).toBeDefined(); + expect(entry.childOf).toBe("card"); + }); + + it("card-content should be a child of card", () => { + const entry = COMPONENT_METADATA["card-content"]; + + expect(entry).toBeDefined(); + expect(entry.childOf).toBe("card"); + }); + + it("card-actions should be a child of card", () => { + const entry = COMPONENT_METADATA["card-actions"]; + + expect(entry).toBeDefined(); + expect(entry.childOf).toBe("card"); + }); + + it("expansion-panel-header should be a child of expansion-panel", () => { + const entry = COMPONENT_METADATA["expansion-panel-header"]; + + expect(entry).toBeDefined(); + expect(entry.childOf).toBe("expansion-panel"); + }); + + it("expansion-panel-body should be a child of expansion-panel", () => { + const entry = COMPONENT_METADATA["expansion-panel-body"]; + + expect(entry).toBeDefined(); + expect(entry.childOf).toBe("expansion-panel"); + }); + }); + + // ===== getThemingSelector Tests ===== + + describe("getThemingSelector()", () => { + it("should return parent selector for child component", () => { + const result = getThemingSelector("list-item", "angular"); + + expect(result).toEqual(["igx-list"]); + }); + + it("should return parent WC selector for child component", () => { + const result = getThemingSelector("nav-drawer-item", "webcomponents"); + + expect(result).toEqual(["igc-nav-drawer"]); + }); + + it("should return own selector for non-child component", () => { + const result = getThemingSelector("avatar", "angular"); + + expect(result).toEqual(["igx-avatar"]); + }); + + it("should return own selector for same-element alias (textarea)", () => { + const result = getThemingSelector("textarea", "angular"); + + expect(result).toEqual([".igx-input-group--textarea-group"]); + }); + + it("should return empty array for unknown component", () => { + const result = getThemingSelector("nonexistent", "angular"); + + expect(result).toEqual([]); + }); + }); }); diff --git a/packages/mcp/src/__tests__/tools/handlers/component-tokens.test.ts b/packages/mcp/src/__tests__/tools/handlers/component-tokens.test.ts index d5dd3752..84c9f3e0 100644 --- a/packages/mcp/src/__tests__/tools/handlers/component-tokens.test.ts +++ b/packages/mcp/src/__tests__/tools/handlers/component-tokens.test.ts @@ -109,6 +109,62 @@ describe("handleGetComponentDesignTokens", () => { ); }); + // ===== Child Component Tests ===== + + it("shows relationship note for child component (list-item)", async () => { + const result = await handleGetComponentDesignTokens({ + component: "list-item", + }); + const text = result.content[0].text; + + expect(text).toContain("`list-item` is a child of the `list` component"); + expect(text).toContain("styling is controlled through the `list` theme"); + }); + + it("shows parent theme function for child component", async () => { + const result = await handleGetComponentDesignTokens({ + component: "list-item", + }); + const text = result.content[0].text; + + expect(text).toContain("**Theme Function:** `list-theme()`"); + }); + + it("shows parent tokens for child component (unfiltered)", async () => { + const result = await handleGetComponentDesignTokens({ + component: "list-item", + }); + const text = result.content[0].text; + + // Should include item-* tokens + expect(text).toContain("item-background"); + + // Should also include header-* tokens (unfiltered) + expect(text).toContain("header-background"); + }); + + it("shows relationship note for nav-drawer-item (naming mismatch case)", async () => { + const result = await handleGetComponentDesignTokens({ + component: "nav-drawer-item", + }); + const text = result.content[0].text; + + expect(text).toContain( + "`nav-drawer-item` is a child of the `navdrawer` component", + ); + expect(text).toContain("**Theme Function:** `navdrawer-theme()`"); + }); + + it("shows relationship note for step child component", async () => { + const result = await handleGetComponentDesignTokens({ + component: "step", + }); + const text = result.content[0].text; + + expect(text).toContain("`step` is a child of the `stepper` component"); + expect(text).toContain("**Theme Function:** `stepper-theme()`"); + }); + it("renders banner compound with both platform lines", async () => { const result = await handleGetComponentDesignTokens({ component: "banner", diff --git a/packages/mcp/src/__tests__/tools/handlers/handlers.test.ts b/packages/mcp/src/__tests__/tools/handlers/handlers.test.ts index fa0418af..3d1112d9 100644 --- a/packages/mcp/src/__tests__/tools/handlers/handlers.test.ts +++ b/packages/mcp/src/__tests__/tools/handlers/handlers.test.ts @@ -662,4 +662,62 @@ describe("handleCreateComponentTheme", () => { const text = result.content[0].text; expect(text).toContain("$schema: $light-bootstrap-schema"); }); + + // ===== Child Component Tests ===== + + it("uses parent selector for child component (Sass)", async () => { + const result = await handleCreateComponentTheme({ + platform: "angular", + component: "list-item", + tokens: { + "item-background": "#ff0000", + }, + }); + + expect(result.isError).toBeUndefined(); + + const text = result.content[0].text; + + // Should use parent's selector (igx-list), not child's (igx-list-item) + expect(text).toContain("igx-list {"); + expect(text).not.toContain("igx-list-item {"); + + // Should use parent's theme function + expect(text).toContain("list-theme("); + }); + + it("uses parent selector for child component (CSS output)", async () => { + const result = await handleCreateComponentTheme({ + platform: "webcomponents", + component: "list-item", + output: "css", + tokens: { + "item-background": "#ff0000", + }, + }); + + expect(result.isError).toBeUndefined(); + + const text = result.content[0].text; + + // Should scope to parent's selector + expect(text).toContain("igc-list"); + }); + + it("textarea alias still uses own selector (not parent)", async () => { + const result = await handleCreateComponentTheme({ + platform: "angular", + component: "textarea", + tokens: { + "box-background": "#ffffff", + }, + }); + + expect(result.isError).toBeUndefined(); + + const text = result.content[0].text; + + // textarea is a same-element alias, should use its own selector + expect(text).toContain(".igx-input-group--textarea-group"); + }); }); diff --git a/packages/mcp/src/generators/css.ts b/packages/mcp/src/generators/css.ts index 0b18b43c..05626407 100644 --- a/packages/mcp/src/generators/css.ts +++ b/packages/mcp/src/generators/css.ts @@ -225,8 +225,12 @@ export async function generateComponentThemeCss( options: ComponentThemeCssOptions, ): Promise { // Import functions we need (dynamic import to avoid circular dependencies) - const { getComponentTheme, getComponentSelector, SCHEMA_PRESETS } = - await import("../knowledge/index.js"); + const { + COMPONENT_METADATA, + getComponentTheme, + getThemingSelector, + SCHEMA_PRESETS, + } = await import("../knowledge/index.js"); const { toVariableName } = await import("../utils/sass.js"); // Validate component exists @@ -239,9 +243,12 @@ export async function generateComponentThemeCss( const designSystem = options.designSystem ?? "material"; const variant = options.variant ?? "light"; const themeFn = theme.themeFunctionName; + // For child components, derive variable name from parent + const themeComponentName = + COMPONENT_METADATA[options.component]?.childOf ?? options.component; const themeName = options.name ? `$${toVariableName(options.name)}` - : `$custom-${options.component}-theme`; + : `$custom-${themeComponentName}-theme`; // Get the schema variable based on design system and variant const schemaVar = @@ -257,8 +264,9 @@ export async function generateComponentThemeCss( tokenArgs.push(`$${tokenName}: ${stringValue}`); } - // Determine selector - use platform-specific component selector as default - const defaultSelectors = getComponentSelector( + // Determine selector - use platform-specific theming selector as default + // For child components, this resolves to the parent's selector + const defaultSelectors = getThemingSelector( options.component, options.platform, ); @@ -271,7 +279,7 @@ export async function generateComponentThemeCss( @use 'igniteui-theming/sass/themes' as *; @use 'igniteui-theming/sass/themes/schemas' as *; -// Custom ${options.component} theme +// Custom ${themeComponentName} theme ${themeName}: ${themeFn}( ${tokenArgs.join(",\n ")} ); @@ -291,7 +299,7 @@ ${selector} { return { css: result.css, - description: `Generated CSS custom properties for ${options.component} component with ${Object.keys(options.tokens).length} token(s) using ${designSystem} design system (${variant} variant)`, + description: `Generated CSS custom properties for ${themeComponentName} component with ${Object.keys(options.tokens).length} token(s) using ${designSystem} design system (${variant} variant)`, }; } catch (error) { const message = error instanceof Error ? error.message : String(error); diff --git a/packages/mcp/src/generators/sass.ts b/packages/mcp/src/generators/sass.ts index ff293462..56a7a3f6 100644 --- a/packages/mcp/src/generators/sass.ts +++ b/packages/mcp/src/generators/sass.ts @@ -9,7 +9,10 @@ * - React: Uses `igniteui-theming` directly with individual mixins */ -import { getComponentSelector } from "../knowledge/component-metadata.js"; +import { + COMPONENT_METADATA, + getThemingSelector, +} from "../knowledge/component-metadata.js"; import { getComponentTheme } from "../knowledge/component-themes.js"; import { generateAngularThemeSass } from "../knowledge/platforms/angular.js"; import { SCHEMAS as SCHEMA_PRESETS } from "../knowledge/platforms/common.js"; @@ -414,9 +417,13 @@ export function generateComponentTheme( const designSystem: DesignSystem = input.designSystem ?? "material"; const variant: ThemeVariant = input.variant ?? "light"; const themeFn = theme.themeFunctionName; + // For child components, derive variable name from parent to avoid + // generating a separate variable that signals "different theme" + const themeComponentName = + COMPONENT_METADATA[input.component]?.childOf ?? input.component; const themeName = input.name ? `$${toVariableName(input.name)}` - : `$custom-${input.component}-theme`; + : `$custom-${themeComponentName}-theme`; // Get the schema variable based on design system and variant const schemaVar = SCHEMA_PRESETS[variant][designSystem]; @@ -429,21 +436,19 @@ export function generateComponentTheme( tokenArgs.push(`$${tokenName}: ${stringValue}`); } - // Determine selector - use platform-specific component selector as default - const defaultSelectors = getComponentSelector( - input.component, - input.platform, - ); + // Determine selector - use platform-specific theming selector as default + // For child components, this resolves to the parent's selector + const defaultSelectors = getThemingSelector(input.component, input.platform); const selector = input.selector || (defaultSelectors.length > 0 ? defaultSelectors[0] : input.component); // Generate the code const sections: string[] = [ - generateHeader(`Custom ${input.component} theme`), + generateHeader(`Custom ${themeComponentName} theme`), generateUseStatement(input.platform, input.licensed), "", - `// Custom ${input.component} theme`, + `// Custom ${themeComponentName} theme`, `${themeName}: ${themeFn}(`, ]; @@ -464,7 +469,7 @@ export function generateComponentTheme( return { code, - description: `Generated custom ${input.component} theme with ${Object.keys(input.tokens).length} token(s) using ${designSystem} design system (${variant} variant)`, + description: `Generated custom ${themeComponentName} theme with ${Object.keys(input.tokens).length} token(s) using ${designSystem} design system (${variant} variant)`, variables: [themeName], }; } diff --git a/packages/mcp/src/knowledge/component-metadata.ts b/packages/mcp/src/knowledge/component-metadata.ts index 7ced4ef5..d1152162 100644 --- a/packages/mcp/src/knowledge/component-metadata.ts +++ b/packages/mcp/src/knowledge/component-metadata.ts @@ -64,10 +64,12 @@ and descriptions from get_component_design_tokens for each child to guide value }; export interface ComponentMetadata { - /** Platform-specific CSS selectors */ - selectors: ComponentSelectors; + /** Platform-specific CSS selectors. Optional for `childOf` entries (theming scope comes from the parent). */ + selectors?: ComponentSelectors; /** Optional theme alias for components that reuse another component theme */ theme?: string; + /** Parent component name for child sub-components. When set, theming scope uses the parent's selector instead of the child's own. Mutually exclusive with `compound`. */ + childOf?: string; /** Present only for components with variant-specific themes (e.g., button) */ variants?: string[]; /** Present only for compound components */ @@ -78,6 +80,12 @@ export const COMPONENT_METADATA: Record = { accordion: { selectors: { angular: "igx-accordion", webcomponents: "igc-accordion" }, }, + "accordion-body": { + childOf: "accordion", + }, + "accordion-header": { + childOf: "accordion", + }, "action-strip": { selectors: { angular: "igx-action-strip", webcomponents: null }, }, @@ -181,6 +189,15 @@ If customizing the banner background, ensure flat-button foreground contrasts ag For the icon theme, the color should also coordinate with the card background while maintaining sufficient contrast.`, }, }, + "card-actions": { + childOf: "card", + }, + "card-content": { + childOf: "card", + }, + "card-header": { + childOf: "card", + }, carousel: { selectors: { angular: "igx-carousel", webcomponents: "igc-carousel" }, }, @@ -344,12 +361,21 @@ If customizing the dialog background, ensure flat-button foreground contrasts ag webcomponents: "igc-dropdown", }, }, + "drop-down-item": { + childOf: "drop-down", + }, "expansion-panel": { selectors: { angular: "igx-expansion-panel", webcomponents: "igc-expansion-panel", }, }, + "expansion-panel-body": { + childOf: "expansion-panel", + }, + "expansion-panel-header": { + childOf: "expansion-panel", + }, "file-input": { selectors: { angular: 'igx-input-group[class~="igx-input-group--file"]', @@ -428,6 +454,12 @@ Both themes should share the same visual treatment as the file-input wrapper.`, list: { selectors: { angular: "igx-list", webcomponents: "igc-list" }, }, + "list-header": { + childOf: "list", + }, + "list-item": { + childOf: "list", + }, navbar: { selectors: { angular: "igx-navbar", webcomponents: "igc-navbar" }, compound: { @@ -448,6 +480,9 @@ Both themes should share the same visual treatment as the file-input wrapper.`, navdrawer: { selectors: { angular: "igx-nav-drawer", webcomponents: "igc-nav-drawer" }, }, + "nav-drawer-item": { + childOf: "navdrawer", + }, overlay: { selectors: { angular: ".igx-overlay__content", webcomponents: null }, }, @@ -548,6 +583,9 @@ The drop-down background should match the select surface intent.`, splitter: { selectors: { angular: "igx-splitter", webcomponents: "igc-splitter" }, }, + step: { + childOf: "stepper", + }, stepper: { selectors: { angular: "igx-stepper", webcomponents: "igc-stepper" }, }, @@ -557,6 +595,9 @@ The drop-down background should match the select surface intent.`, tabs: { selectors: { angular: "igx-tabs", webcomponents: "igc-tabs" }, }, + "tab-item": { + childOf: "tabs", + }, "time-picker": { selectors: { angular: "igx-time-picker", webcomponents: null }, compound: { @@ -616,6 +657,10 @@ export function getComponentSelector( return []; } + if (!metadata.selectors) { + return []; + } + const platformSelectors = platform === "angular" ? metadata.selectors.angular @@ -630,6 +675,30 @@ export function getComponentSelector( : [platformSelectors]; } +/** + * Get the selector(s) to use for theming scope. + * For child components (with `childOf`), returns the parent's selectors. + * For all other components, returns the component's own selectors. + * @param componentName - The component name + * @param platform - The target platform + * @returns Array of selectors for theming scope, empty array if not found + */ +export function getThemingSelector( + componentName: string, + platform: Platform, +): string[] { + const metadata = COMPONENT_METADATA[componentName]; + if (!metadata) { + return []; + } + + if (metadata.childOf) { + return getComponentSelector(metadata.childOf, platform); + } + + return getComponentSelector(componentName, platform); +} + /** * Check if a component is available on a specific platform. * @param componentName - The component name @@ -641,7 +710,7 @@ export function isComponentAvailable( platform: Platform, ): boolean { const metadata = COMPONENT_METADATA[componentName]; - if (!metadata) { + if (!metadata?.selectors) { return false; } @@ -660,6 +729,7 @@ export function isComponentAvailable( export function getComponentsForPlatform(platform: Platform): string[] { return Object.entries(COMPONENT_METADATA) .filter(([, metadata]) => { + if (!metadata.selectors) return false; const platformSelector = platform === "angular" ? metadata.selectors.angular @@ -678,7 +748,7 @@ export function getComponentPlatformAvailability( componentName: string, ): { angular: boolean; webcomponents: boolean } | undefined { const metadata = COMPONENT_METADATA[componentName]; - if (!metadata) { + if (!metadata?.selectors) { return undefined; } diff --git a/packages/mcp/src/knowledge/component-themes.ts b/packages/mcp/src/knowledge/component-themes.ts index ccbf8ced..cc6a096e 100644 --- a/packages/mcp/src/knowledge/component-themes.ts +++ b/packages/mcp/src/knowledge/component-themes.ts @@ -88,13 +88,12 @@ export function resolveComponentTheme( } const metadata = COMPONENT_METADATA[componentName]; + const alias = metadata?.theme ?? metadata?.childOf; - if (!metadata?.theme) { + if (!alias) { return {}; } - const alias = metadata.theme; - if (alias === componentName) { return { error: `Theme alias target "${alias}" cannot reference itself.`, diff --git a/packages/mcp/src/knowledge/index.ts b/packages/mcp/src/knowledge/index.ts index b0da2595..f921c886 100644 --- a/packages/mcp/src/knowledge/index.ts +++ b/packages/mcp/src/knowledge/index.ts @@ -29,6 +29,7 @@ export { getComponentSelector, getComponentsForPlatform, getCompoundComponentInfo, + getThemingSelector, getTokenDerivationsForChild, getVariants, hasVariants, diff --git a/packages/mcp/src/tools/descriptions.ts b/packages/mcp/src/tools/descriptions.ts index e24c3518..671dad9f 100644 --- a/packages/mcp/src/tools/descriptions.ts +++ b/packages/mcp/src/tools/descriptions.ts @@ -745,6 +745,20 @@ export const TOOL_DESCRIPTIONS = { - Button variants: Use specific variant names like "flat-button", "contained-button", "outlined-button", "fab-button" (NOT just "button") - Icon button variants: "flat-icon-button", "contained-icon-button", "outlined-icon-button" + - Child sub-components: Use names like "list-item", "card-header", "accordion-header", + "tab-item", "step", "expansion-panel-header". These resolve to the parent component's + theme automatically. + + CHILD SUB-COMPONENTS: + - Some component parts (e.g., "list-item", "card-header", "accordion-header") don't have + their own theme function — they are styled through the parent component's theme tokens. + - When you query a child sub-component, the response includes a note explaining the + parent-child relationship and shows the parent theme's full token list. + - The token descriptions guide you to the relevant tokens (e.g., "item-background" + for list items, "header-text-color" for card headers). + - When you then call create_component_theme with a child name, it automatically + uses the parent's theme function, variable name, and selector — producing output + that is merge-compatible with the parent component's theme. COMPOUND COMPONENTS: - Some components like "combo", "grid", "select" are compound - they use multiple @@ -771,6 +785,7 @@ export const TOOL_DESCRIPTIONS = { - tokens: Array of { name, type, description } for each available token - variants: (if applicable) List of variant-specific theme names - compoundInfo: (if applicable) Related themes with token derivation hints and guidance + - childNote: (if child sub-component) A note explaining the parent-child relationship @@ -785,6 +800,14 @@ export const TOOL_DESCRIPTIONS = { } Returns tokens like: background, foreground, hover-background, border-radius, etc. + + Get tokens for a child sub-component: + { + "component": "list-item" + } + + Returns the list theme's tokens with a note: "list-item is a child of the list component. + Its styling is controlled through the list theme." @@ -832,10 +855,20 @@ export const TOOL_DESCRIPTIONS = { SELECTORS: - Default selector is auto-detected based on platform and component + - For child sub-components (e.g., "list-item", "card-header"), the selector + automatically resolves to the parent component's selector (e.g., "igx-list", "igx-card") - Angular: Uses "igx-*" element selectors or attribute selectors - Web Components: Uses "igc-*" element selectors - Custom selectors supported for targeted styling (e.g., ".my-button") + CHILD SUB-COMPONENTS: + - When creating a theme for a child sub-component (e.g., "card-actions"), the output + uses the parent's theme function, variable name, and selector. + - This means the output for "card" and "card-actions" is merge-compatible: + both produce \`$custom-card-theme: card-theme(...)\` scoped to \`igx-card\`. + - To add tokens for multiple sub-parts, merge the token arguments into a single + theme function call rather than creating separate themes. + SASS OUTPUT: - Generated code uses \`@include tokens($theme)\` to apply the theme - The tokens mixin emits --ig-{component}-{token} CSS custom properties in global mode @@ -1122,13 +1155,13 @@ Important: Gray progression is INVERTED for dark themes (50=darkest, 900=lightes // --------------------------------------------------------------------------- // Component theming parameters // --------------------------------------------------------------------------- - component: `Component name to get design tokens for (e.g., "button", "avatar", "grid"). Use exact names like "flat-button" for button variants. Call this tool to discover available tokens BEFORE using create_component_theme.`, + component: `Component name to get design tokens for (e.g., "button", "avatar", "grid"). Use exact names like "flat-button" for button variants. Child sub-component names like "list-item", "card-header", "accordion-header", "tab-item", "step" are also supported — they resolve to the parent component's theme. Call this tool to discover available tokens BEFORE using create_component_theme.`, - componentTheme: `Component name to theme (e.g., "button", "avatar", "flat-button", "grid"). Must match a valid component from get_component_design_tokens. For button/icon-button variants, use specific names like "flat-button", "contained-button", "outlined-button", "fab-button".`, + componentTheme: `Component name to theme (e.g., "button", "avatar", "flat-button", "grid"). Must match a valid component from get_component_design_tokens. For button/icon-button variants, use specific names like "flat-button", "contained-button", "outlined-button", "fab-button". Child sub-component names (e.g., "list-item", "card-header") are supported and automatically resolve to the parent theme with the parent's selector and variable name.`, tokens: `Object mapping token names to values. Token names must be valid for the component (use get_component_design_tokens to discover them). Values can be CSS colors, dimensions with units, or other Sass-compatible values. Example: { "background": "#1976D2", "border-radius": "8px" }`, - selector: `Optional CSS selector to scope the theme. If omitted, uses the platform's default selector for the component. For Angular: "igx-*" selectors, for Web Components: "igc-*" selectors. You can specify custom selectors like ".my-custom-button" for targeted styling.`, + selector: `Optional CSS selector to scope the theme. If omitted, uses the platform's default selector for the component. For child sub-components (e.g., "list-item"), the default selector is the parent component's selector (e.g., "igx-list"). For Angular: "igx-*" selectors, for Web Components: "igc-*" selectors. You can specify custom selectors like ".my-custom-button" for targeted styling.`, themeName: `Optional name for the generated theme variable (without $ prefix). If omitted, auto-generates based on component name (e.g., "$custom-button-theme").`, diff --git a/packages/mcp/src/tools/handlers/component-theme.ts b/packages/mcp/src/tools/handlers/component-theme.ts index a605f52b..9d8d7a1c 100644 --- a/packages/mcp/src/tools/handlers/component-theme.ts +++ b/packages/mcp/src/tools/handlers/component-theme.ts @@ -5,10 +5,11 @@ import { generateComponentTheme } from "../../generators/sass.js"; import { + COMPONENT_METADATA, COMPONENT_NAMES, getComponentPlatformAvailability, - getComponentSelector, getComponentTheme, + getThemingSelector, getVariants, hasVariants, isComponentAvailable, @@ -122,11 +123,13 @@ Please use \`create_component_theme\` with one of the specific variant names abo } if (platform) { - const isAvailable = isComponentAvailable(normalizedComponent, platform); + // For child components, check parent's availability + const metadata = COMPONENT_METADATA[normalizedComponent]; + const availabilityTarget = metadata?.childOf ?? normalizedComponent; + const isAvailable = isComponentAvailable(availabilityTarget, platform); if (!isAvailable) { - const availability = - getComponentPlatformAvailability(normalizedComponent); + const availability = getComponentPlatformAvailability(availabilityTarget); const availablePlatforms: string[] = []; if (availability?.angular) availablePlatforms.push("Angular"); @@ -189,7 +192,7 @@ Use \`get_component_design_tokens\` to see all tokens with descriptions.`, if (!finalSelector && platform) { // Get platform-specific default selector - const selectors = getComponentSelector(normalizedComponent, platform); + const selectors = getThemingSelector(normalizedComponent, platform); if (selectors.length > 0) { // Use the first selector as default diff --git a/packages/mcp/src/tools/handlers/component-tokens.ts b/packages/mcp/src/tools/handlers/component-tokens.ts index 03d9dcc8..01347656 100644 --- a/packages/mcp/src/tools/handlers/component-tokens.ts +++ b/packages/mcp/src/tools/handlers/component-tokens.ts @@ -1,4 +1,5 @@ import { + COMPONENT_METADATA, COMPONENT_NAMES, getComponentSelector, getCompoundComponentInfo, @@ -70,6 +71,18 @@ ${suggestions.length === 0 ? `\nTotal available: ${COMPONENT_NAMES.length} compo ); responseParts.push(""); + // 1b. Child component relationship note + const metadata = COMPONENT_METADATA[normalizedName]; + + if (metadata?.childOf) { + const parentName = metadata.childOf; + + responseParts.push( + `**Note:** \`${normalizedName}\` is a child of the \`${parentName}\` component. Its styling is controlled through the \`${parentName}\` theme — all tokens below apply at the ${parentName} level.`, + ); + responseParts.push(""); + } + // 2. Theme function responseParts.push(`**Theme Function:** \`${theme.themeFunctionName}()\``); responseParts.push(""); diff --git a/packages/mcp/src/tools/handlers/layout.ts b/packages/mcp/src/tools/handlers/layout.ts index 4599dfdb..b5109ea6 100644 --- a/packages/mcp/src/tools/handlers/layout.ts +++ b/packages/mcp/src/tools/handlers/layout.ts @@ -71,6 +71,12 @@ ${list.map((name) => `- ${name}`).join("\n")}`, const selectorsEntry = COMPONENT_METADATA[normalized].selectors; let selectors: string[] = []; + if (!selectorsEntry) { + return { + error: `**Error:** Component "${component}" does not have platform selectors. It may be a child sub-component — use its parent component for layout overrides.`, + }; + } + if (platform && platform !== "generic") { selectors = getComponentSelector(normalized, platform); From 84a301c56b4bd7f2c17e57068b607da13e4bd804 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Wed, 1 Apr 2026 18:00:07 +0300 Subject: [PATCH 3/9] spec: update and archive completed items --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/child-component-resolution/spec.md | 152 ++++++++++++++++++ .../component-metadata-unification/spec.md | 25 ++- .../specs/component-theming/spec.md | 31 ++-- .../tasks.md | 0 .../specs/child-component-resolution/spec.md | 144 ----------------- .../specs/child-component-resolution/spec.md | 152 ++++++++++++++++++ .../component-metadata-unification/spec.md | 37 ++++- openspec/specs/component-theming/spec.md | 75 ++++++++- 11 files changed, 452 insertions(+), 164 deletions(-) rename openspec/changes/{child-component-theme-resolution => archive/2026-04-01-child-component-theme-resolution}/.openspec.yaml (100%) rename openspec/changes/{child-component-theme-resolution => archive/2026-04-01-child-component-theme-resolution}/design.md (100%) rename openspec/changes/{child-component-theme-resolution => archive/2026-04-01-child-component-theme-resolution}/proposal.md (100%) create mode 100644 openspec/changes/archive/2026-04-01-child-component-theme-resolution/specs/child-component-resolution/spec.md rename openspec/changes/{child-component-theme-resolution => archive/2026-04-01-child-component-theme-resolution}/specs/component-metadata-unification/spec.md (75%) rename openspec/changes/{child-component-theme-resolution => archive/2026-04-01-child-component-theme-resolution}/specs/component-theming/spec.md (76%) rename openspec/changes/{child-component-theme-resolution => archive/2026-04-01-child-component-theme-resolution}/tasks.md (100%) delete mode 100644 openspec/changes/child-component-theme-resolution/specs/child-component-resolution/spec.md create mode 100644 openspec/specs/child-component-resolution/spec.md diff --git a/openspec/changes/child-component-theme-resolution/.openspec.yaml b/openspec/changes/archive/2026-04-01-child-component-theme-resolution/.openspec.yaml similarity index 100% rename from openspec/changes/child-component-theme-resolution/.openspec.yaml rename to openspec/changes/archive/2026-04-01-child-component-theme-resolution/.openspec.yaml diff --git a/openspec/changes/child-component-theme-resolution/design.md b/openspec/changes/archive/2026-04-01-child-component-theme-resolution/design.md similarity index 100% rename from openspec/changes/child-component-theme-resolution/design.md rename to openspec/changes/archive/2026-04-01-child-component-theme-resolution/design.md diff --git a/openspec/changes/child-component-theme-resolution/proposal.md b/openspec/changes/archive/2026-04-01-child-component-theme-resolution/proposal.md similarity index 100% rename from openspec/changes/child-component-theme-resolution/proposal.md rename to openspec/changes/archive/2026-04-01-child-component-theme-resolution/proposal.md diff --git a/openspec/changes/archive/2026-04-01-child-component-theme-resolution/specs/child-component-resolution/spec.md b/openspec/changes/archive/2026-04-01-child-component-theme-resolution/specs/child-component-resolution/spec.md new file mode 100644 index 00000000..b606b20d --- /dev/null +++ b/openspec/changes/archive/2026-04-01-child-component-theme-resolution/specs/child-component-resolution/spec.md @@ -0,0 +1,152 @@ +## ADDED Requirements + +### Requirement: ComponentMetadata supports childOf field + +The `ComponentMetadata` interface SHALL include an optional `childOf` field of type `string` that names the parent component. When `childOf` is present, `selectors` and `theme` SHALL NOT be present — both are resolved from the parent. The `selectors` field on `ComponentMetadata` SHALL be optional to support this. + +#### Scenario: Child component entry has only childOf + +- **GIVEN** a component metadata entry for `list-item` +- **WHEN** the entry is inspected +- **THEN** `childOf` is `"list"` +- **AND** `selectors` is undefined +- **AND** `theme` is undefined + +#### Scenario: childOf and compound are mutually exclusive + +- **GIVEN** a component metadata entry with `childOf` set +- **WHEN** the entry is inspected +- **THEN** `compound` SHALL NOT be present on the same entry + +#### Scenario: childOf references a valid parent + +- **GIVEN** a component metadata entry with `childOf: "list"` +- **WHEN** the metadata is loaded +- **THEN** `COMPONENT_METADATA["list"]` SHALL exist +- **AND** `COMPONENT_METADATA["list"].selectors` SHALL have at least one non-null platform selector + +### Requirement: Theme resolution falls back to childOf + +The `resolveComponentTheme` function SHALL resolve the theme via `metadata.childOf` when `metadata.theme` is not set. This allows child component entries to omit the `theme` field entirely. + +#### Scenario: Child component resolves theme via childOf + +- **GIVEN** `COMPONENT_METADATA["list-item"]` has `childOf: "list"` and no `theme` field +- **WHEN** `resolveComponentTheme("list-item")` is called +- **THEN** it returns the `list` theme (same as `resolveComponentTheme("list")`) + +#### Scenario: Same-element alias still resolves via theme + +- **GIVEN** `COMPONENT_METADATA["textarea"]` has `theme: "input-group"` and no `childOf` field +- **WHEN** `resolveComponentTheme("textarea")` is called +- **THEN** it returns the `input-group` theme (unchanged behavior) + +### Requirement: Theming selector resolution uses parent for child components + +A `getThemingSelector(componentName, platform)` accessor function SHALL return the selector to use for scoping generated theme code. For child components (where `childOf` is set), it SHALL return the parent's selectors. For all other components, it SHALL return the component's own selectors. + +#### Scenario: Child component resolves to parent selector + +- **WHEN** `getThemingSelector("list-item", "angular")` is called +- **THEN** it returns `["igx-list"]` (the parent `list` component's Angular selector) + +#### Scenario: Child component resolves to parent selector on Web Components + +- **WHEN** `getThemingSelector("nav-drawer-item", "webcomponents")` is called +- **THEN** it returns `["igc-nav-drawer"]` (the parent `navdrawer` component's WC selector) + +#### Scenario: Non-child component resolves to own selector + +- **WHEN** `getThemingSelector("avatar", "angular")` is called +- **THEN** it returns `["igx-avatar"]` (the component's own selector, same as `getComponentSelector`) + +#### Scenario: Same-element alias resolves to own selector + +- **WHEN** `getThemingSelector("textarea", "angular")` is called +- **THEN** it returns `[".igx-input-group--textarea-group"]` (the component's own selector, not the aliased `input-group` selector) + +#### Scenario: Unknown component returns empty + +- **WHEN** `getThemingSelector("nonexistent", "angular")` is called +- **THEN** it returns `[]` + +### Requirement: getComponentSelector is unchanged + +The existing `getComponentSelector` function SHALL continue to return the component's own selectors. For child components without selectors, it returns an empty array. + +#### Scenario: getComponentSelector returns empty for childOf entries + +- **WHEN** `getComponentSelector("list-item", "angular")` is called +- **THEN** it returns `[]` (childOf entries have no own selectors) + +### Requirement: Generated theme variable name uses parent + +When generating theme code for a child component, the variable name and code comments SHALL use the parent component's name, not the child's. This ensures output is merge-compatible when a user incrementally styles different sub-parts of the same component. + +#### Scenario: Child component produces parent-named variable + +- **GIVEN** `create_component_theme` is called with `component: "card-actions"` +- **WHEN** no custom `name` is provided +- **THEN** the generated variable is `$custom-card-theme` (not `$custom-card-actions-theme`) +- **AND** comments reference "Custom card theme" (not "Custom card-actions theme") + +#### Scenario: Explicit name overrides parent derivation + +- **GIVEN** `create_component_theme` is called with `component: "card-actions"` and `name: "my-theme"` +- **WHEN** a custom name is provided +- **THEN** the generated variable is `$my-theme` + +### Requirement: Platform availability delegates to parent for child components + +Platform availability checks for child components SHALL delegate to the parent component. A child component is available on a platform if the parent component is available. + +#### Scenario: Child component available via parent + +- **GIVEN** `list-item` has `childOf: "list"` and `list` has `selectors.angular: "igx-list"` +- **WHEN** the `create_component_theme` handler checks platform availability for `list-item` on Angular +- **THEN** it checks `isComponentAvailable("list", "angular")` and returns true + +### Requirement: Initial child component entries + +The following child component entries SHALL be present in `COMPONENT_METADATA`. Each entry SHALL contain only `childOf` pointing to the parent component. + +| Entry | `childOf` | Notes | +| ------------------------ | ------------------- | -------------------------------------------------- | +| `list-item` | `"list"` | | +| `list-header` | `"list"` | | +| `drop-down-item` | `"drop-down"` | | +| `nav-drawer-item` | `"navdrawer"` | Fixes naming mismatch (nav-drawer ≠ navdrawer) | +| `tab-item` | `"tabs"` | | +| `step` | `"stepper"` | | +| `card-header` | `"card"` | | +| `card-content` | `"card"` | | +| `card-actions` | `"card"` | | +| `expansion-panel-header` | `"expansion-panel"` | | +| `expansion-panel-body` | `"expansion-panel"` | | +| `accordion-header` | `"accordion"` | Virtual child (no real element on either platform) | +| `accordion-body` | `"accordion"` | Virtual child (no real element on either platform) | + +### Requirement: Child entries do not appear in VARIANT_THEME_NAMES + +Child component entries SHALL NOT have a `variants` field. The `VARIANT_THEME_NAMES` set SHALL NOT contain any child component names. + +#### Scenario: Child entry excluded from variant set + +- **WHEN** `isVariantTheme("list-item")` is called +- **THEN** it returns `false` + +### Requirement: Tool descriptions document child sub-components + +The `get_component_design_tokens` and `create_component_theme` tool descriptions SHALL mention child sub-component names as valid inputs and explain the parent resolution behavior, merge-compatible output, and parent-scoped selectors. + +#### Scenario: get_component_design_tokens description mentions child components + +- **WHEN** the tool description for `get_component_design_tokens` is read +- **THEN** it lists child sub-component names (e.g., "list-item", "card-header", "accordion-header") as valid inputs +- **AND** explains that they resolve to the parent component's theme + +#### Scenario: create_component_theme description explains merge-compatible output + +- **WHEN** the tool description for `create_component_theme` is read +- **THEN** it explains that child sub-component output uses the parent's theme function, variable name, and selector +- **AND** recommends merging token arguments into a single theme function call diff --git a/openspec/changes/child-component-theme-resolution/specs/component-metadata-unification/spec.md b/openspec/changes/archive/2026-04-01-child-component-theme-resolution/specs/component-metadata-unification/spec.md similarity index 75% rename from openspec/changes/child-component-theme-resolution/specs/component-metadata-unification/spec.md rename to openspec/changes/archive/2026-04-01-child-component-theme-resolution/specs/component-metadata-unification/spec.md index b95ee649..f8ec7875 100644 --- a/openspec/changes/child-component-theme-resolution/specs/component-metadata-unification/spec.md +++ b/openspec/changes/archive/2026-04-01-child-component-theme-resolution/specs/component-metadata-unification/spec.md @@ -2,7 +2,7 @@ ### Requirement: Single unified component metadata map -All component metadata (selectors, variants, compound info, child-of-parent relationships) SHALL be stored in a single `COMPONENT_METADATA` map exported from `component-metadata.ts`. Each component SHALL have exactly one entry keyed by its theme name or child component name. +All component metadata (selectors, variants, compound info, child-of-parent relationships) SHALL be stored in a single `COMPONENT_METADATA` map exported from `component-metadata.ts`. Each component SHALL have exactly one entry keyed by its theme name or child component name. The `selectors` field SHALL be optional — `childOf` entries omit it. #### Scenario: Simple component lookup @@ -24,18 +24,18 @@ All component metadata (selectors, variants, compound info, child-of-parent rela #### Scenario: Child component lookup - **WHEN** a child component (e.g., `list-item`) is looked up in `COMPONENT_METADATA` -- **THEN** the entry contains `selectors`, `theme`, and `childOf` fields -- **AND** `childOf` names the parent component whose selectors are used for theming scope -- **AND** no `compound` or `variants` fields are present +- **THEN** the entry contains only a `childOf` field +- **AND** `selectors`, `theme`, `compound`, and `variants` fields are absent ### Requirement: Accessor functions preserve existing signatures -All public accessor functions SHALL maintain their existing call signatures and return types. New accessor functions MAY be added for child component resolution. +All public accessor functions SHALL maintain their existing call signatures and return types. New accessor functions MAY be added for child component resolution. Existing accessor functions SHALL handle missing `selectors` gracefully. #### Scenario: getComponentSelector unchanged - **WHEN** `getComponentSelector(name, platform)` is called - **THEN** it returns the same value as before, read from `COMPONENT_METADATA[name].selectors[platform]` +- **AND** returns `[]` if `selectors` is undefined (childOf entries) #### Scenario: isCompoundComponent uses unified map @@ -66,3 +66,18 @@ All public accessor functions SHALL maintain their existing call signatures and - **WHEN** `getThemingSelector(name, platform)` is called - **THEN** it returns the parent's selectors if `childOf` is set, or the component's own selectors otherwise + +#### Scenario: isComponentAvailable handles missing selectors + +- **WHEN** `isComponentAvailable(name, platform)` is called for a childOf entry +- **THEN** it returns `false` (no own selectors) + +#### Scenario: getComponentsForPlatform excludes childOf entries + +- **WHEN** `getComponentsForPlatform(platform)` is called +- **THEN** childOf entries are excluded from the result (they have no selectors) + +#### Scenario: getComponentPlatformAvailability handles missing selectors + +- **WHEN** `getComponentPlatformAvailability(name)` is called for a childOf entry +- **THEN** it returns `undefined` diff --git a/openspec/changes/child-component-theme-resolution/specs/component-theming/spec.md b/openspec/changes/archive/2026-04-01-child-component-theme-resolution/specs/component-theming/spec.md similarity index 76% rename from openspec/changes/child-component-theme-resolution/specs/component-theming/spec.md rename to openspec/changes/archive/2026-04-01-child-component-theme-resolution/specs/component-theming/spec.md index f9c42d31..cb2677c7 100644 --- a/openspec/changes/child-component-theme-resolution/specs/component-theming/spec.md +++ b/openspec/changes/archive/2026-04-01-child-component-theme-resolution/specs/component-theming/spec.md @@ -79,18 +79,29 @@ The `create_component_theme` tool SHALL scope generated theme code to the parent - **WHEN** no custom `selector` is provided - **THEN** the generated code scopes to `igx-avatar` (the component's own selector) -### Requirement: Child component platform availability uses own selectors +### Requirement: Generated theme variable name uses parent for child components -Platform availability checks for child components SHALL use the child's own selectors, not the parent's. A child component is available on a platform if its own `selectors` entry for that platform is non-null. +When generating theme code for a child component, the variable name SHALL derive from the parent component's name, not the child's. This ensures merge-compatible output across sub-parts of the same component. -#### Scenario: Child component available on both platforms +#### Scenario: Child component Sass output uses parent variable name -- **GIVEN** `list-item` has selectors `{ angular: "igx-list-item", webcomponents: "igc-list-item" }` -- **WHEN** `isComponentAvailable("list-item", "angular")` is called -- **THEN** it returns `true` +- **GIVEN** `create_component_theme` is called with `component: "card-actions"` and `platform: "angular"` +- **WHEN** no custom `name` is provided +- **THEN** the generated variable is `$custom-card-theme` and the comment says "Custom card theme" -#### Scenario: Child component not available on a platform +#### Scenario: Child component CSS output uses parent in description -- **GIVEN** `expansion-panel-body` has `selectors.webcomponents` set to `null` -- **WHEN** `isComponentAvailable("expansion-panel-body", "webcomponents")` is called -- **THEN** it returns `false` +- **GIVEN** `create_component_theme` is called with `component: "card-actions"` and `output: "css"` +- **WHEN** no custom `name` is provided +- **THEN** the description says "Generated CSS custom properties for card component" + +### Requirement: Child component platform availability delegates to parent + +When the `create_component_theme` handler checks platform availability for a child component, it SHALL check the parent component's availability instead of the child's. + +#### Scenario: Child component passes platform check via parent + +- **GIVEN** `create_component_theme` is called with `component: "list-item"` and `platform: "angular"` +- **WHEN** the handler checks platform availability +- **THEN** it checks `isComponentAvailable("list", "angular")` (the parent) +- **AND** the request succeeds diff --git a/openspec/changes/child-component-theme-resolution/tasks.md b/openspec/changes/archive/2026-04-01-child-component-theme-resolution/tasks.md similarity index 100% rename from openspec/changes/child-component-theme-resolution/tasks.md rename to openspec/changes/archive/2026-04-01-child-component-theme-resolution/tasks.md diff --git a/openspec/changes/child-component-theme-resolution/specs/child-component-resolution/spec.md b/openspec/changes/child-component-theme-resolution/specs/child-component-resolution/spec.md deleted file mode 100644 index b6194e01..00000000 --- a/openspec/changes/child-component-theme-resolution/specs/child-component-resolution/spec.md +++ /dev/null @@ -1,144 +0,0 @@ -## ADDED Requirements - -### Requirement: ComponentMetadata supports childOf field - -The `ComponentMetadata` interface SHALL include an optional `childOf` field of type `string` that names the parent component whose selectors are used for theming scope. When `childOf` is present, `theme` MUST also be present on the same entry. - -#### Scenario: Child component entry has both childOf and theme - -- **GIVEN** a component metadata entry for `list-item` -- **WHEN** the entry is inspected -- **THEN** `childOf` is `"list"` and `theme` is `"list"` -- **AND** `selectors` contains the child's own platform selectors (e.g., `angular: "igx-list-item"`) - -#### Scenario: childOf and compound are mutually exclusive - -- **GIVEN** a component metadata entry with `childOf` set -- **WHEN** the entry is inspected -- **THEN** `compound` SHALL NOT be present on the same entry - -#### Scenario: childOf references a valid parent - -- **GIVEN** a component metadata entry with `childOf: "list"` -- **WHEN** the metadata is loaded -- **THEN** `COMPONENT_METADATA["list"]` SHALL exist -- **AND** `COMPONENT_METADATA["list"].selectors` SHALL have at least one non-null platform selector - -### Requirement: Theming selector resolution uses parent for child components - -A `getThemingSelector(componentName, platform)` accessor function SHALL return the selector to use for scoping generated theme code. For child components (where `childOf` is set), it SHALL return the parent's selectors. For all other components, it SHALL return the component's own selectors. - -#### Scenario: Child component resolves to parent selector - -- **WHEN** `getThemingSelector("list-item", "angular")` is called -- **THEN** it returns `["igx-list"]` (the parent `list` component's Angular selector) - -#### Scenario: Child component resolves to parent selector on Web Components - -- **WHEN** `getThemingSelector("nav-drawer-item", "webcomponents")` is called -- **THEN** it returns `["igc-nav-drawer"]` (the parent `navdrawer` component's WC selector) - -#### Scenario: Non-child component resolves to own selector - -- **WHEN** `getThemingSelector("avatar", "angular")` is called -- **THEN** it returns `["igx-avatar"]` (the component's own selector, same as `getComponentSelector`) - -#### Scenario: Same-element alias resolves to own selector - -- **WHEN** `getThemingSelector("textarea", "angular")` is called -- **THEN** it returns `[".igx-input-group--textarea-group"]` (the component's own selector, not the aliased `input-group` selector) - -#### Scenario: Unknown component returns empty - -- **WHEN** `getThemingSelector("nonexistent", "angular")` is called -- **THEN** it returns `[]` - -### Requirement: getComponentSelector is unchanged - -The existing `getComponentSelector` function SHALL continue to return the component's own selectors regardless of `childOf`. It is NOT affected by the child component resolution. - -#### Scenario: getComponentSelector ignores childOf - -- **WHEN** `getComponentSelector("list-item", "angular")` is called -- **THEN** it returns `["igx-list-item"]` (the child's own selector, not the parent's) - -### Requirement: Initial child component entries - -The following child component entries SHALL be present in `COMPONENT_METADATA`. Each entry SHALL include `selectors` with real platform values, `theme` pointing to the parent's theme, and `childOf` pointing to the parent component. - -#### Scenario: list-item entry - -- **WHEN** `COMPONENT_METADATA["list-item"]` is inspected -- **THEN** `selectors.angular` is `"igx-list-item"` and `selectors.webcomponents` is `"igc-list-item"` -- **AND** `theme` is `"list"` and `childOf` is `"list"` - -#### Scenario: list-header entry - -- **WHEN** `COMPONENT_METADATA["list-header"]` is inspected -- **THEN** `selectors.angular` is `"igx-list-header"` or the appropriate Angular selector -- **AND** `selectors.webcomponents` is `"igc-list-header"` -- **AND** `theme` is `"list"` and `childOf` is `"list"` - -#### Scenario: drop-down-item entry - -- **WHEN** `COMPONENT_METADATA["drop-down-item"]` is inspected -- **THEN** `selectors.angular` is `"igx-drop-down-item"` and `selectors.webcomponents` is `"igc-dropdown-item"` -- **AND** `theme` is `"drop-down"` and `childOf` is `"drop-down"` - -#### Scenario: nav-drawer-item entry - -- **WHEN** `COMPONENT_METADATA["nav-drawer-item"]` is inspected -- **THEN** `selectors.angular` is `"igx-nav-drawer"` or the appropriate Angular selector -- **AND** `selectors.webcomponents` is `"igc-nav-drawer-item"` -- **AND** `theme` is `"navdrawer"` and `childOf` is `"navdrawer"` - -#### Scenario: tab-item entry - -- **WHEN** `COMPONENT_METADATA["tab-item"]` is inspected -- **THEN** `selectors.angular` is `"igx-tab-item"` and `selectors.webcomponents` is `"igc-tab"` -- **AND** `theme` is `"tabs"` and `childOf` is `"tabs"` - -#### Scenario: step entry - -- **WHEN** `COMPONENT_METADATA["step"]` is inspected -- **THEN** `selectors.angular` is `"igx-step"` and `selectors.webcomponents` is `"igc-step"` -- **AND** `theme` is `"stepper"` and `childOf` is `"stepper"` - -#### Scenario: card-header entry - -- **WHEN** `COMPONENT_METADATA["card-header"]` is inspected -- **THEN** `selectors.angular` is `"igx-card-header"` and `selectors.webcomponents` is `"igc-card-header"` -- **AND** `theme` is `"card"` and `childOf` is `"card"` - -#### Scenario: card-content entry - -- **WHEN** `COMPONENT_METADATA["card-content"]` is inspected -- **THEN** `selectors.angular` is `"igx-card-content"` and `selectors.webcomponents` is `"igc-card-content"` -- **AND** `theme` is `"card"` and `childOf` is `"card"` - -#### Scenario: card-actions entry - -- **WHEN** `COMPONENT_METADATA["card-actions"]` is inspected -- **THEN** `selectors.angular` is `"igx-card-actions"` and `selectors.webcomponents` is `"igc-card-actions"` -- **AND** `theme` is `"card"` and `childOf` is `"card"` - -#### Scenario: expansion-panel-header entry - -- **WHEN** `COMPONENT_METADATA["expansion-panel-header"]` is inspected -- **THEN** `selectors.angular` is `"igx-expansion-panel-header"` and `selectors.webcomponents` is a valid WC selector or `null` -- **AND** `theme` is `"expansion-panel"` and `childOf` is `"expansion-panel"` - -#### Scenario: expansion-panel-body entry - -- **WHEN** `COMPONENT_METADATA["expansion-panel-body"]` is inspected -- **THEN** `selectors.angular` is `"igx-expansion-panel-body"` and `selectors.webcomponents` is a valid WC selector or `null` -- **AND** `theme` is `"expansion-panel"` and `childOf` is `"expansion-panel"` - -### Requirement: Child entries do not appear in VARIANT_THEME_NAMES - -Child component entries SHALL NOT have a `variants` field. The `VARIANT_THEME_NAMES` set SHALL NOT contain any child component names. - -#### Scenario: Child entry excluded from variant set - -- **WHEN** `isVariantTheme("list-item")` is called -- **THEN** it returns `false` diff --git a/openspec/specs/child-component-resolution/spec.md b/openspec/specs/child-component-resolution/spec.md new file mode 100644 index 00000000..b606b20d --- /dev/null +++ b/openspec/specs/child-component-resolution/spec.md @@ -0,0 +1,152 @@ +## ADDED Requirements + +### Requirement: ComponentMetadata supports childOf field + +The `ComponentMetadata` interface SHALL include an optional `childOf` field of type `string` that names the parent component. When `childOf` is present, `selectors` and `theme` SHALL NOT be present — both are resolved from the parent. The `selectors` field on `ComponentMetadata` SHALL be optional to support this. + +#### Scenario: Child component entry has only childOf + +- **GIVEN** a component metadata entry for `list-item` +- **WHEN** the entry is inspected +- **THEN** `childOf` is `"list"` +- **AND** `selectors` is undefined +- **AND** `theme` is undefined + +#### Scenario: childOf and compound are mutually exclusive + +- **GIVEN** a component metadata entry with `childOf` set +- **WHEN** the entry is inspected +- **THEN** `compound` SHALL NOT be present on the same entry + +#### Scenario: childOf references a valid parent + +- **GIVEN** a component metadata entry with `childOf: "list"` +- **WHEN** the metadata is loaded +- **THEN** `COMPONENT_METADATA["list"]` SHALL exist +- **AND** `COMPONENT_METADATA["list"].selectors` SHALL have at least one non-null platform selector + +### Requirement: Theme resolution falls back to childOf + +The `resolveComponentTheme` function SHALL resolve the theme via `metadata.childOf` when `metadata.theme` is not set. This allows child component entries to omit the `theme` field entirely. + +#### Scenario: Child component resolves theme via childOf + +- **GIVEN** `COMPONENT_METADATA["list-item"]` has `childOf: "list"` and no `theme` field +- **WHEN** `resolveComponentTheme("list-item")` is called +- **THEN** it returns the `list` theme (same as `resolveComponentTheme("list")`) + +#### Scenario: Same-element alias still resolves via theme + +- **GIVEN** `COMPONENT_METADATA["textarea"]` has `theme: "input-group"` and no `childOf` field +- **WHEN** `resolveComponentTheme("textarea")` is called +- **THEN** it returns the `input-group` theme (unchanged behavior) + +### Requirement: Theming selector resolution uses parent for child components + +A `getThemingSelector(componentName, platform)` accessor function SHALL return the selector to use for scoping generated theme code. For child components (where `childOf` is set), it SHALL return the parent's selectors. For all other components, it SHALL return the component's own selectors. + +#### Scenario: Child component resolves to parent selector + +- **WHEN** `getThemingSelector("list-item", "angular")` is called +- **THEN** it returns `["igx-list"]` (the parent `list` component's Angular selector) + +#### Scenario: Child component resolves to parent selector on Web Components + +- **WHEN** `getThemingSelector("nav-drawer-item", "webcomponents")` is called +- **THEN** it returns `["igc-nav-drawer"]` (the parent `navdrawer` component's WC selector) + +#### Scenario: Non-child component resolves to own selector + +- **WHEN** `getThemingSelector("avatar", "angular")` is called +- **THEN** it returns `["igx-avatar"]` (the component's own selector, same as `getComponentSelector`) + +#### Scenario: Same-element alias resolves to own selector + +- **WHEN** `getThemingSelector("textarea", "angular")` is called +- **THEN** it returns `[".igx-input-group--textarea-group"]` (the component's own selector, not the aliased `input-group` selector) + +#### Scenario: Unknown component returns empty + +- **WHEN** `getThemingSelector("nonexistent", "angular")` is called +- **THEN** it returns `[]` + +### Requirement: getComponentSelector is unchanged + +The existing `getComponentSelector` function SHALL continue to return the component's own selectors. For child components without selectors, it returns an empty array. + +#### Scenario: getComponentSelector returns empty for childOf entries + +- **WHEN** `getComponentSelector("list-item", "angular")` is called +- **THEN** it returns `[]` (childOf entries have no own selectors) + +### Requirement: Generated theme variable name uses parent + +When generating theme code for a child component, the variable name and code comments SHALL use the parent component's name, not the child's. This ensures output is merge-compatible when a user incrementally styles different sub-parts of the same component. + +#### Scenario: Child component produces parent-named variable + +- **GIVEN** `create_component_theme` is called with `component: "card-actions"` +- **WHEN** no custom `name` is provided +- **THEN** the generated variable is `$custom-card-theme` (not `$custom-card-actions-theme`) +- **AND** comments reference "Custom card theme" (not "Custom card-actions theme") + +#### Scenario: Explicit name overrides parent derivation + +- **GIVEN** `create_component_theme` is called with `component: "card-actions"` and `name: "my-theme"` +- **WHEN** a custom name is provided +- **THEN** the generated variable is `$my-theme` + +### Requirement: Platform availability delegates to parent for child components + +Platform availability checks for child components SHALL delegate to the parent component. A child component is available on a platform if the parent component is available. + +#### Scenario: Child component available via parent + +- **GIVEN** `list-item` has `childOf: "list"` and `list` has `selectors.angular: "igx-list"` +- **WHEN** the `create_component_theme` handler checks platform availability for `list-item` on Angular +- **THEN** it checks `isComponentAvailable("list", "angular")` and returns true + +### Requirement: Initial child component entries + +The following child component entries SHALL be present in `COMPONENT_METADATA`. Each entry SHALL contain only `childOf` pointing to the parent component. + +| Entry | `childOf` | Notes | +| ------------------------ | ------------------- | -------------------------------------------------- | +| `list-item` | `"list"` | | +| `list-header` | `"list"` | | +| `drop-down-item` | `"drop-down"` | | +| `nav-drawer-item` | `"navdrawer"` | Fixes naming mismatch (nav-drawer ≠ navdrawer) | +| `tab-item` | `"tabs"` | | +| `step` | `"stepper"` | | +| `card-header` | `"card"` | | +| `card-content` | `"card"` | | +| `card-actions` | `"card"` | | +| `expansion-panel-header` | `"expansion-panel"` | | +| `expansion-panel-body` | `"expansion-panel"` | | +| `accordion-header` | `"accordion"` | Virtual child (no real element on either platform) | +| `accordion-body` | `"accordion"` | Virtual child (no real element on either platform) | + +### Requirement: Child entries do not appear in VARIANT_THEME_NAMES + +Child component entries SHALL NOT have a `variants` field. The `VARIANT_THEME_NAMES` set SHALL NOT contain any child component names. + +#### Scenario: Child entry excluded from variant set + +- **WHEN** `isVariantTheme("list-item")` is called +- **THEN** it returns `false` + +### Requirement: Tool descriptions document child sub-components + +The `get_component_design_tokens` and `create_component_theme` tool descriptions SHALL mention child sub-component names as valid inputs and explain the parent resolution behavior, merge-compatible output, and parent-scoped selectors. + +#### Scenario: get_component_design_tokens description mentions child components + +- **WHEN** the tool description for `get_component_design_tokens` is read +- **THEN** it lists child sub-component names (e.g., "list-item", "card-header", "accordion-header") as valid inputs +- **AND** explains that they resolve to the parent component's theme + +#### Scenario: create_component_theme description explains merge-compatible output + +- **WHEN** the tool description for `create_component_theme` is read +- **THEN** it explains that child sub-component output uses the parent's theme function, variable name, and selector +- **AND** recommends merging token arguments into a single theme function call diff --git a/openspec/specs/component-metadata-unification/spec.md b/openspec/specs/component-metadata-unification/spec.md index 1f0dfafa..b1ac0659 100644 --- a/openspec/specs/component-metadata-unification/spec.md +++ b/openspec/specs/component-metadata-unification/spec.md @@ -1,17 +1,26 @@ # component-metadata-unification Specification ## Purpose + TBD - created by archiving change unify-component-metadata. Update Purpose after archive. + ## Requirements + ### Requirement: Single unified component metadata map -All component metadata (selectors, variants, compound info) SHALL be stored in a single `COMPONENT_METADATA` map exported from `component-metadata.ts`. Each component SHALL have exactly one entry keyed by its theme name. +All component metadata (selectors, variants, compound info, child-of-parent relationships) SHALL be stored in a single `COMPONENT_METADATA` map exported from `component-metadata.ts`. Each component SHALL have exactly one entry keyed by its theme name or child component name. The `selectors` field SHALL be optional — `childOf` entries omit it. #### Scenario: Simple component lookup - **WHEN** a simple component (e.g., `avatar`) is looked up in `COMPONENT_METADATA` - **THEN** the entry contains a `selectors` field with `angular` and `webcomponents` keys -- **AND** no `compound` or `variants` fields are present +- **AND** no `compound`, `variants`, or `childOf` fields are present + +#### Scenario: Child component lookup + +- **WHEN** a child component (e.g., `list-item`) is looked up in `COMPONENT_METADATA` +- **THEN** the entry contains only a `childOf` field +- **AND** `selectors`, `theme`, `compound`, and `variants` fields are absent #### Scenario: Compound component lookup @@ -69,12 +78,13 @@ Compound components that require scoping beyond their base selector SHALL declar ### Requirement: Accessor functions preserve existing signatures -All public accessor functions SHALL maintain their existing call signatures and return types. Functions that are eliminated SHALL have no callers in production code. +All public accessor functions SHALL maintain their existing call signatures and return types. Functions that are eliminated SHALL have no callers in production code. New accessor functions MAY be added for child component resolution. Existing accessor functions SHALL handle missing `selectors` gracefully. #### Scenario: getComponentSelector unchanged - **WHEN** `getComponentSelector(name, platform)` is called - **THEN** it returns the same value as before, read from `COMPONENT_METADATA[name].selectors[platform]` +- **AND** returns `[]` if `selectors` is undefined (childOf entries) #### Scenario: isCompoundComponent uses unified map @@ -101,6 +111,26 @@ All public accessor functions SHALL maintain their existing call signatures and - **WHEN** the codebase is searched for `getCompoundSelector` - **THEN** no references exist — the function has been removed (it had no production callers) +#### Scenario: New getThemingSelector accessor added + +- **WHEN** `getThemingSelector(name, platform)` is called +- **THEN** it returns the parent's selectors if `childOf` is set, or the component's own selectors otherwise + +#### Scenario: isComponentAvailable handles missing selectors + +- **WHEN** `isComponentAvailable(name, platform)` is called for a childOf entry +- **THEN** it returns `false` (no own selectors) + +#### Scenario: getComponentsForPlatform excludes childOf entries + +- **WHEN** `getComponentsForPlatform(platform)` is called +- **THEN** childOf entries are excluded from the result (they have no selectors) + +#### Scenario: getComponentPlatformAvailability handles missing selectors + +- **WHEN** `getComponentPlatformAvailability(name)` is called for a childOf entry +- **THEN** it returns `undefined` + ### Requirement: VARIANT_THEME_NAMES derived at init The set of all variant theme names SHALL be derived from `COMPONENT_METADATA` at module initialization time, not maintained as a separate data structure. @@ -158,4 +188,3 @@ After the refactor, the old file structure and removed exports SHALL not exist i - **WHEN** the codebase is searched for imports from `compound-theming` - **THEN** no import statements reference the deleted module - diff --git a/openspec/specs/component-theming/spec.md b/openspec/specs/component-theming/spec.md index 25d6b207..90b6efd0 100644 --- a/openspec/specs/component-theming/spec.md +++ b/openspec/specs/component-theming/spec.md @@ -1,7 +1,9 @@ ## Purpose Define component theming requirements, validation, and platform-specific output rules. + ## Requirements + ### Requirement: Component theming requires platform The `create_component_theme` tool requires a `platform` parameter and SHALL specify compound-component completeness rules to reduce incomplete outputs. @@ -29,7 +31,7 @@ The `create_component_theme` tool requires a `platform` parameter and SHALL spec ### Requirement: Component token schemas are exposed -The `get_component_design_tokens` tool SHALL use an instruction-oriented output format that varies based on whether the component is compound or simple. For compound components, the response SHALL include numbered steps, per-platform scope tables, related theme tables, token derivations, and guidance. For simple components, the response SHALL include the theme function, primary tokens, and the token table without compound sections. +The `get_component_design_tokens` tool SHALL use an instruction-oriented output format that varies based on whether the component is compound, simple, or a child sub-component. For compound components, the response SHALL include numbered steps, per-platform scope tables, related theme tables, token derivations, and guidance. For simple components, the response SHALL include the theme function, primary tokens, and the token table without compound sections. For child sub-components, the response SHALL include a relationship note followed by the full parent theme's tokens. #### Scenario: Compound component response uses instruction-oriented format @@ -45,6 +47,20 @@ The `get_component_design_tokens` tool SHALL use an instruction-oriented output - **AND** includes the theme function name, primary tokens, and available tokens table - **AND** does NOT include steps, scope tables, related theme tables, token derivations, or guidance sections +#### Scenario: Child component response includes relationship note + +- **WHEN** `get_component_design_tokens` is called for a child component (one with `childOf` set in metadata) +- **THEN** the response includes a note stating that the component is a child of the parent component +- **AND** the note states that styling is controlled through the parent's theme +- **AND** the response shows the full parent theme's tokens (unfiltered) +- **AND** the theme function name shown is the parent's theme function (e.g., `list-theme()` for `list-item`) + +#### Scenario: Child component relationship note format + +- **WHEN** `get_component_design_tokens` is called for `list-item` +- **THEN** the response includes text indicating `list-item` is a child of `list` +- **AND** indicates that tokens apply at the list level + #### Scenario: Missing selector entries are handled - **WHEN** a compound component has related themes without scoped selectors (e.g., selector is `TODO`) @@ -172,3 +188,60 @@ The SassDoc description block in `*-theme.scss` files SHALL use concise one-line - **WHEN** the SassDoc descriptions are trimmed - **THEN** all `@param` annotations remain unchanged with their full derivation descriptions +### Requirement: Generated theme code uses parent selector for child components + +The `create_component_theme` tool SHALL scope generated theme code to the parent component's selector when the component has a `childOf` field. This applies to both Sass and CSS output. + +#### Scenario: Sass output for child component uses parent selector + +- **GIVEN** `create_component_theme` is called with `component: "list-item"` and `platform: "angular"` +- **WHEN** no custom `selector` is provided +- **THEN** the generated Sass code scopes the `@include tokens(...)` call to `igx-list` (the parent's selector) +- **AND** the theme function call uses `list-theme()` + +#### Scenario: CSS output for child component uses parent selector + +- **GIVEN** `create_component_theme` is called with `component: "list-item"`, `platform: "webcomponents"`, and `output: "css"` +- **WHEN** no custom `selector` is provided +- **THEN** the generated CSS scopes custom properties to `igc-list` (the parent's selector) + +#### Scenario: Custom selector overrides parent selector for child component + +- **GIVEN** `create_component_theme` is called with `component: "list-item"` and `selector: ".my-custom-list"` +- **WHEN** a custom selector is provided +- **THEN** the generated code uses `.my-custom-list` as the scope +- **AND** the parent selector resolution is bypassed + +#### Scenario: Same-element alias continues to use own selector + +- **GIVEN** `create_component_theme` is called with `component: "textarea"` and `platform: "angular"` +- **WHEN** no custom `selector` is provided +- **THEN** the generated code scopes to `.igx-input-group--textarea-group` (textarea's own selector) +- **AND** NOT to `igx-input-group` (the aliased theme's selector) + +### Requirement: Generated theme variable name uses parent for child components + +When generating theme code for a child component, the variable name SHALL derive from the parent component's name, not the child's. This ensures merge-compatible output across sub-parts of the same component. + +#### Scenario: Child component Sass output uses parent variable name + +- **GIVEN** `create_component_theme` is called with `component: "card-actions"` and `platform: "angular"` +- **WHEN** no custom `name` is provided +- **THEN** the generated variable is `$custom-card-theme` and the comment says "Custom card theme" + +#### Scenario: Child component CSS output uses parent in description + +- **GIVEN** `create_component_theme` is called with `component: "card-actions"` and `output: "css"` +- **WHEN** no custom `name` is provided +- **THEN** the description says "Generated CSS custom properties for card component" + +### Requirement: Child component platform availability delegates to parent + +When the `create_component_theme` handler checks platform availability for a child component, it SHALL check the parent component's availability instead of the child's. + +#### Scenario: Child component passes platform check via parent + +- **GIVEN** `create_component_theme` is called with `component: "list-item"` and `platform: "angular"` +- **WHEN** the handler checks platform availability +- **THEN** it checks `isComponentAvailable("list", "angular")` (the parent) +- **AND** the request succeeds From ad12dfdef1425cdc02781488855ddaf08c3712fa Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Thu, 2 Apr 2026 08:32:54 +0300 Subject: [PATCH 4/9] feat(mcp): add normalized component search with synonym aliases and typo recovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace naive substring matching in searchComponents with a ranked, token-based search engine that handles order-independent queries (e.g. "linear progress" → progress-linear), framework prefix normalization (igx-/igc-), explicit synonym aliases (toggle → switch), and single-character typo recovery via bounded Levenshtein distance. - Extract search logic into reusable component-search.ts module - Add optional aliases field to ComponentMetadata for true synonyms - Seed aliases for ~15 components (dialog, navdrawer, progress-*, etc.) - Add dedicated unit tests for the search engine in isolation Closes #538 --- .../.openspec.yaml | 2 + .../design.md | 111 ++++++ .../proposal.md | 27 ++ .../component-metadata-unification/spec.md | 35 ++ .../specs/component-name-aliases/spec.md | 68 ++++ .../tasks.md | 24 ++ .../component-metadata-unification/spec.md | 9 +- openspec/specs/component-name-aliases/spec.md | 74 ++++ .../knowledge/component-metadata.test.ts | 34 ++ .../knowledge/component-search.test.ts | 217 +++++++++++ .../knowledge/component-themes.test.ts | 39 ++ .../tools/handlers/component-tokens.test.ts | 25 ++ .../__tests__/tools/handlers/handlers.test.ts | 37 ++ .../mcp/src/knowledge/component-metadata.ts | 21 ++ .../mcp/src/knowledge/component-search.ts | 357 ++++++++++++++++++ .../mcp/src/knowledge/component-themes.ts | 11 +- packages/mcp/src/knowledge/index.ts | 6 + 17 files changed, 1092 insertions(+), 5 deletions(-) create mode 100644 openspec/changes/archive/2026-04-02-component-name-aliases/.openspec.yaml create mode 100644 openspec/changes/archive/2026-04-02-component-name-aliases/design.md create mode 100644 openspec/changes/archive/2026-04-02-component-name-aliases/proposal.md create mode 100644 openspec/changes/archive/2026-04-02-component-name-aliases/specs/component-metadata-unification/spec.md create mode 100644 openspec/changes/archive/2026-04-02-component-name-aliases/specs/component-name-aliases/spec.md create mode 100644 openspec/changes/archive/2026-04-02-component-name-aliases/tasks.md create mode 100644 openspec/specs/component-name-aliases/spec.md create mode 100644 packages/mcp/src/__tests__/knowledge/component-search.test.ts create mode 100644 packages/mcp/src/knowledge/component-search.ts diff --git a/openspec/changes/archive/2026-04-02-component-name-aliases/.openspec.yaml b/openspec/changes/archive/2026-04-02-component-name-aliases/.openspec.yaml new file mode 100644 index 00000000..0f528039 --- /dev/null +++ b/openspec/changes/archive/2026-04-02-component-name-aliases/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-01 diff --git a/openspec/changes/archive/2026-04-02-component-name-aliases/design.md b/openspec/changes/archive/2026-04-02-component-name-aliases/design.md new file mode 100644 index 00000000..d67036a9 --- /dev/null +++ b/openspec/changes/archive/2026-04-02-component-name-aliases/design.md @@ -0,0 +1,111 @@ +## Context + +`searchComponents` currently relies on substring matching over canonical theme names, which misses common user phrasing like `linear progress` for `progress-linear`. This creates low-quality suggestions in MCP handlers (for example, suggesting `slider` instead of the progress components). + +The change must improve recognition without changing tool input/output schemas. The solution should stay deterministic, maintainable, and easy to extend as component metadata evolves. + +## Goals / Non-Goals + +**Goals:** + +- Resolve order and delimiter variation mismatches (`linear progress`, `linear-progress`, `progress linear`) +- Support true synonym naming (`toggle` -> `switch`) without large manual permutation lists +- Improve ranking quality so the best component appears first for ambiguous inputs +- Keep canonical component names unchanged and preserve existing MCP tool contracts + +**Non-Goals:** + +- Building an NLP or semantic intent model +- Changing canonical component IDs in theme maps or public tool schemas +- Supporting arbitrary misspellings with edit-distance heuristics across all inputs + +## Decisions + +### 1) Use a normalization pipeline before matching + +All query terms and candidate names will be normalized into a common representation: + +- lowercased text +- separators normalized (`-`, `_`, and whitespace treated consistently) +- framework prefixes removed where applicable (`igx-`, `igc-`) +- token array extracted for order-independent comparison + +Rationale: This addresses formatting noise once, then enables simpler matching logic. + +Alternatives considered: + +- Regex-only direct matching: easy to start, but difficult to score and rank consistently. +- Levenshtein/fuzzy distance first: handles typos but increases false positives for short component names. + +### 2) Build candidate signals from metadata, not permutations + +Each component candidate will be represented by a set of searchable signals: + +- canonical component name +- metadata keys and selector-derived terms (where available) +- optional explicit `aliases` from `ComponentMetadata` + +Rationale: This captures meaningful naming sources while avoiding manual expansion of every word-order permutation. + +Alternatives considered: + +- Hardcoding permutation aliases for every component: high maintenance and easy to miss cases. + +### 3) Use deterministic, tiered ranking + +`searchComponents` will score matches in strict tiers: + +1. exact normalized string match +2. exact token-set match (order independent) +3. strong token overlap (query tokens fully covered by candidate signals) +4. guarded substring fallback (with minimum threshold) + +Tie-breaks are deterministic (score, then stable lexical order), so results are predictable across runs. + +Rationale: Tiered scoring gives high precision for exact intent while still recovering near matches. + +Alternatives considered: + +- Single blended score without tiers: harder to reason about regressions and edge ranking. + +### 4) Keep aliases minimal and synonym-focused + +`ComponentMetadata` will gain an optional `aliases?: string[]` field, but aliases are only for genuine synonyms/legacy shorthand (for example, `toggle` for `switch`). + +Rationale: Synonyms are semantic gaps that normalization cannot infer; permutations are structural and should be handled algorithmically. + +Alternatives considered: + +- Alias-first strategy for both synonyms and permutations: works initially, but scales poorly and duplicates logic. + +### 5) Add focused regression and ranking tests + +Tests will cover: + +- issue #538 regression (`linear progress` -> `progress-linear`) +- order-independent matches for progress components +- selector-like phrasing (`igx-linear-bar`) resolution +- synonym alias resolution (`toggle` -> `switch`) +- ambiguity/ranking cases to ensure relevant components outrank unrelated ones + +Rationale: Ranking logic is behavior-heavy; tests must protect ordering, not just inclusion. + +## Risks / Trade-offs + +- [Risk] Token overlap may overmatch short generic queries (for example, `bar`) -> Mitigation: require minimum score/coverage thresholds and prefer exact/tiered matches. +- [Risk] Selector-derived signals might bias results toward framework naming -> Mitigation: weight canonical and exact query matches higher than selector-derived tokens. +- [Risk] Alias set can grow inconsistently over time -> Mitigation: document alias policy (synonyms only) and enforce via review + tests. +- [Risk] Ranking changes can surprise users for edge inputs -> Mitigation: add explicit ranking regression tests for known ambiguous terms. + +## Migration Plan + +1. Add optional `aliases` support to `ComponentMetadata` and seed minimal synonym aliases. +2. Implement normalization helpers and tiered ranking in `searchComponents`. +3. Add/update tests for normalization, ranking tiers, and issue #538 regression. +4. Run MCP/unit test suite and verify handler suggestion behavior remains stable. +5. Rollback path: remove alias field usage and restore prior substring matcher if regressions are severe. + +## Open Questions + +- Should we normalize additional framework prefixes beyond `igx-`/`igc-` in this change, or defer until a concrete failure case appears? +- Do we want to expose debug scoring in tests only, or keep scoring fully internal? diff --git a/openspec/changes/archive/2026-04-02-component-name-aliases/proposal.md b/openspec/changes/archive/2026-04-02-component-name-aliases/proposal.md new file mode 100644 index 00000000..4bbfa24b --- /dev/null +++ b/openspec/changes/archive/2026-04-02-component-name-aliases/proposal.md @@ -0,0 +1,27 @@ +## Why + +Users reference components using names that differ from the internal theme names — "linear progress" vs `progress-linear`, "circular progress" vs `progress-circular`, "nav drawer" vs `navdrawer`, "toggle" vs `switch`, "datepicker" vs `date-picker`, etc. The current `searchComponents` function uses simple substring matching against theme names, which fails when the user's name doesn't appear as a substring (e.g., `"linear-progress".includes("progress-linear")` is false). This causes incorrect suggestions (e.g., "slider" instead of "progress-linear") and forces extra round-trips. Issue: https://github.com/IgniteUI/igniteui-theming/issues/538 + +## What Changes + +- Normalize user input and component names into comparable tokens (case-insensitive, delimiter-agnostic, and with framework prefixes like `igx-`/`igc-` removed) +- Rework `searchComponents` to use deterministic ranking: exact normalized match first, then order-independent token-set matches, then strong partial overlaps/substring fallbacks with thresholds +- Add a small, explicit alias list in `COMPONENT_METADATA` only for true synonyms and legacy shorthand (for example, `toggle` -> `switch`), not for word-order permutations +- Use selector-derived terms and metadata keys as additional search signals where helpful, while keeping canonical component names unchanged + +## Capabilities + +### New Capabilities + +- `component-name-aliases`: Defines the alias data structure on `ComponentMetadata`, the improved search algorithm, and the initial set of aliases + +### Modified Capabilities + +- `component-metadata-unification`: The `ComponentMetadata` interface gains a new optional `aliases` field for synonym support. The search function is enhanced to use normalized token matching across metadata keys and aliases. + +## Impact + +- **Code**: `component-themes.ts` (normalization + token-based ranking in `searchComponents`), `component-metadata.ts` (small synonym alias support), both handler files (already use `searchComponents`, no changes needed) +- **Tests**: New tests for order-independent matching, normalization behavior, synonym alias resolution, ranking quality, and edge cases +- **APIs**: No tool schema changes — the MCP tools accept the same `component` parameter. Improved search is transparent. +- **Rollback**: Remove the `aliases` field and revert `searchComponents` to the original substring match. All existing functionality is unaffected. diff --git a/openspec/changes/archive/2026-04-02-component-name-aliases/specs/component-metadata-unification/spec.md b/openspec/changes/archive/2026-04-02-component-name-aliases/specs/component-metadata-unification/spec.md new file mode 100644 index 00000000..9e55a3a5 --- /dev/null +++ b/openspec/changes/archive/2026-04-02-component-name-aliases/specs/component-metadata-unification/spec.md @@ -0,0 +1,35 @@ +## MODIFIED Requirements + +### Requirement: Single unified component metadata map + +All component metadata (selectors, variants, compound info, child-of-parent relationships, and optional synonym aliases) SHALL be stored in a single `COMPONENT_METADATA` map exported from `component-metadata.ts`. Each component SHALL have exactly one entry keyed by its canonical theme name or child component name. The `selectors` field SHALL be optional — `childOf` entries omit it. The `aliases` field SHALL be optional and, when present, SHALL contain synonym terms that resolve to the canonical key. + +#### Scenario: Simple component lookup + +- **WHEN** a simple component (e.g., `avatar`) is looked up in `COMPONENT_METADATA` +- **THEN** the entry contains a `selectors` field with `angular` and `webcomponents` keys +- **AND** no `compound`, `variants`, or `childOf` fields are present + +#### Scenario: Child component lookup + +- **WHEN** a child component (e.g., `list-item`) is looked up in `COMPONENT_METADATA` +- **THEN** the entry contains only a `childOf` field +- **AND** `selectors`, `theme`, `compound`, and `variants` fields are absent + +#### Scenario: Compound component lookup + +- **WHEN** a compound component (e.g., `combo`) is looked up in `COMPONENT_METADATA` +- **THEN** the entry contains `selectors` and a `compound` field +- **AND** `compound` includes `description`, `relatedThemes`, and optionally `tokenDerivations`, `guidance`, `additionalScopes`, and `childScopes` + +#### Scenario: Variant component lookup + +- **WHEN** a base component with variants (e.g., `button`) is looked up in `COMPONENT_METADATA` +- **THEN** the entry contains `selectors` and a `variants` field listing variant theme names + +#### Scenario: Synonym aliases are optional metadata + +- **GIVEN** `switch` includes an `aliases` field with `toggle` +- **WHEN** `COMPONENT_METADATA["switch"]` is inspected +- **THEN** the canonical key remains `switch` +- **AND** `aliases` is present as optional metadata for search resolution diff --git a/openspec/changes/archive/2026-04-02-component-name-aliases/specs/component-name-aliases/spec.md b/openspec/changes/archive/2026-04-02-component-name-aliases/specs/component-name-aliases/spec.md new file mode 100644 index 00000000..7c381360 --- /dev/null +++ b/openspec/changes/archive/2026-04-02-component-name-aliases/specs/component-name-aliases/spec.md @@ -0,0 +1,68 @@ +## ADDED Requirements + +### Requirement: Component search normalizes query and candidate names + +The `searchComponents` capability SHALL normalize user queries and candidate search signals before matching. Normalization SHALL be case-insensitive, treat hyphens/underscores/spaces as equivalent separators, and remove known framework element prefixes (`igx-`, `igc-`) from searchable terms. + +#### Scenario: Delimiter and case normalization + +- **GIVEN** canonical component metadata includes `progress-linear` +- **WHEN** the user searches for `Linear_Progress` +- **THEN** `searchComponents` treats it as equivalent to `linear progress` +- **AND** `progress-linear` is included in results + +#### Scenario: Framework prefix normalization + +- **GIVEN** searchable signals include selector-like terms for progress components +- **WHEN** the user searches for `igx-linear-bar` +- **THEN** the `igx-` prefix is ignored for matching purposes +- **AND** the best-matching progress component is returned + +### Requirement: Order-independent token matching + +The matching algorithm SHALL compare normalized tokens independent of order. Queries that use the same token set as a component signal SHALL be treated as high-confidence matches even when word order differs. + +#### Scenario: Reversed token order resolves correctly + +- **GIVEN** the component `progress-linear` is available +- **WHEN** the user searches for `linear progress` +- **THEN** `progress-linear` is returned as a top-ranked match + +#### Scenario: Canonical order still matches + +- **GIVEN** the component `progress-linear` is available +- **WHEN** the user searches for `progress linear` +- **THEN** `progress-linear` is returned as a top-ranked match + +### Requirement: Deterministic ranked matching + +`searchComponents` SHALL rank matches deterministically using tiered confidence signals in this order: exact normalized string match, exact token-set match, strong token overlap, and guarded substring fallback. Results with insufficient confidence SHALL be excluded. + +#### Scenario: Exact normalized match outranks partial overlap + +- **GIVEN** candidates include one exact normalized match and one partial token-overlap match +- **WHEN** `searchComponents` ranks results +- **THEN** the exact normalized match appears before the partial overlap candidate + +#### Scenario: Stable ordering for tied results + +- **GIVEN** two candidates receive the same confidence tier and score +- **WHEN** `searchComponents` returns results +- **THEN** the tie is resolved using a stable deterministic rule +- **AND** repeated calls return the same order + +### Requirement: Synonym aliases are explicit and minimal + +The system SHALL support optional explicit aliases for true semantic synonyms and legacy shorthand that normalization cannot infer. Alias coverage SHALL NOT be used to encode mechanical word-order permutations. + +#### Scenario: Semantic synonym resolves to canonical component + +- **GIVEN** metadata defines `toggle` as an alias for `switch` +- **WHEN** the user searches for `toggle` +- **THEN** `switch` is returned as a high-confidence match + +#### Scenario: Permutation is handled algorithmically, not via alias list + +- **GIVEN** `progress-linear` is available with no alias for `linear-progress` +- **WHEN** the user searches for `linear-progress` +- **THEN** `progress-linear` is still matched via normalization and token ranking diff --git a/openspec/changes/archive/2026-04-02-component-name-aliases/tasks.md b/openspec/changes/archive/2026-04-02-component-name-aliases/tasks.md new file mode 100644 index 00000000..4e316cfb --- /dev/null +++ b/openspec/changes/archive/2026-04-02-component-name-aliases/tasks.md @@ -0,0 +1,24 @@ +## 1. Metadata and Search Inputs + +- [x] 1.1 Add optional `aliases` to `ComponentMetadata` and seed synonym-only aliases (for example, `switch` includes `toggle`) +- [x] 1.2 Add or expose candidate search signals from metadata (canonical name, metadata keys, selector-derived terms) for `searchComponents` +- [x] 1.3 Ensure alias policy is documented in code comments or nearby docs as "synonyms only, not permutations" + +## 2. Normalization and Ranking Engine + +- [x] 2.1 Implement normalization helpers for case-insensitive matching, delimiter equivalence, and `igx-`/`igc-` prefix stripping +- [x] 2.2 Rework `searchComponents` to use deterministic tiered ranking (exact normalized, token-set, strong overlap, guarded substring) +- [x] 2.3 Add deterministic tie-breaking and minimum confidence filtering so low-signal matches are excluded + +## 3. Regression and Ranking Tests + +- [x] 3.1 Add regression test for issue #538 (`linear progress` resolves to `progress-linear` as top result) +- [x] 3.2 Add tests for order-independent matching and selector-like inputs (`igx-linear-bar`) +- [x] 3.3 Add tests for explicit synonym resolution (`toggle` resolves to `switch`) and verify permutations work without alias entries +- [x] 3.4 Add ranking determinism tests to validate exact matches outrank partial overlaps and ties are stable + +## 4. Validation and Integration Checks + +- [x] 4.1 Verify MCP handlers that consume `searchComponents` return improved suggestions without schema changes +- [x] 4.2 Run relevant test suites (knowledge + handler tests) and fix any regressions +- [x] 4.3 Confirm no canonical component names or public tool contracts changed diff --git a/openspec/specs/component-metadata-unification/spec.md b/openspec/specs/component-metadata-unification/spec.md index b1ac0659..66fc9cbc 100644 --- a/openspec/specs/component-metadata-unification/spec.md +++ b/openspec/specs/component-metadata-unification/spec.md @@ -8,7 +8,7 @@ TBD - created by archiving change unify-component-metadata. Update Purpose after ### Requirement: Single unified component metadata map -All component metadata (selectors, variants, compound info, child-of-parent relationships) SHALL be stored in a single `COMPONENT_METADATA` map exported from `component-metadata.ts`. Each component SHALL have exactly one entry keyed by its theme name or child component name. The `selectors` field SHALL be optional — `childOf` entries omit it. +All component metadata (selectors, variants, compound info, child-of-parent relationships, and optional synonym aliases) SHALL be stored in a single `COMPONENT_METADATA` map exported from `component-metadata.ts`. Each component SHALL have exactly one entry keyed by its canonical theme name or child component name. The `selectors` field SHALL be optional — `childOf` entries omit it. The `aliases` field SHALL be optional and, when present, SHALL contain synonym terms that resolve to the canonical key. #### Scenario: Simple component lookup @@ -33,6 +33,13 @@ All component metadata (selectors, variants, compound info, child-of-parent rela - **WHEN** a base component with variants (e.g., `button`) is looked up in `COMPONENT_METADATA` - **THEN** the entry contains `selectors` and a `variants` field listing variant theme names +#### Scenario: Synonym aliases are optional metadata + +- **GIVEN** `switch` includes an `aliases` field with `toggle` +- **WHEN** `COMPONENT_METADATA["switch"]` is inspected +- **THEN** the canonical key remains `switch` +- **AND** `aliases` is present as optional metadata for search resolution + ### Requirement: Inline scope derived from base selectors The inline scope for compound component child theming SHALL always be derived from the component's `selectors` field. Inline scopes SHALL NOT be declared explicitly in any data structure. diff --git a/openspec/specs/component-name-aliases/spec.md b/openspec/specs/component-name-aliases/spec.md new file mode 100644 index 00000000..f204fea6 --- /dev/null +++ b/openspec/specs/component-name-aliases/spec.md @@ -0,0 +1,74 @@ +# component-name-aliases Specification + +## Purpose + +Defines the search, normalization, matching, and alias resolution behavior for component name lookups. Ensures users can find components regardless of casing, delimiter style, token order, or framework prefix, and supports explicit synonym aliases for true semantic equivalences. + +## Requirements + +### Requirement: Component search normalizes query and candidate names + +The `searchComponents` capability SHALL normalize user queries and candidate search signals before matching. Normalization SHALL be case-insensitive, treat hyphens/underscores/spaces as equivalent separators, and remove known framework element prefixes (`igx-`, `igc-`) from searchable terms. + +#### Scenario: Delimiter and case normalization + +- **GIVEN** canonical component metadata includes `progress-linear` +- **WHEN** the user searches for `Linear_Progress` +- **THEN** `searchComponents` treats it as equivalent to `linear progress` +- **AND** `progress-linear` is included in results + +#### Scenario: Framework prefix normalization + +- **GIVEN** searchable signals include selector-like terms for progress components +- **WHEN** the user searches for `igx-linear-bar` +- **THEN** the `igx-` prefix is ignored for matching purposes +- **AND** the best-matching progress component is returned + +### Requirement: Order-independent token matching + +The matching algorithm SHALL compare normalized tokens independent of order. Queries that use the same token set as a component signal SHALL be treated as high-confidence matches even when word order differs. + +#### Scenario: Reversed token order resolves correctly + +- **GIVEN** the component `progress-linear` is available +- **WHEN** the user searches for `linear progress` +- **THEN** `progress-linear` is returned as a top-ranked match + +#### Scenario: Canonical order still matches + +- **GIVEN** the component `progress-linear` is available +- **WHEN** the user searches for `progress linear` +- **THEN** `progress-linear` is returned as a top-ranked match + +### Requirement: Deterministic ranked matching + +`searchComponents` SHALL rank matches deterministically using tiered confidence signals in this order: exact normalized string match, exact token-set match, strong token overlap, and guarded substring fallback. Results with insufficient confidence SHALL be excluded. + +#### Scenario: Exact normalized match outranks partial overlap + +- **GIVEN** candidates include one exact normalized match and one partial token-overlap match +- **WHEN** `searchComponents` ranks results +- **THEN** the exact normalized match appears before the partial overlap candidate + +#### Scenario: Stable ordering for tied results + +- **GIVEN** two candidates receive the same confidence tier and score +- **WHEN** `searchComponents` returns results +- **THEN** the tie is resolved using a stable deterministic rule +- **AND** repeated calls return the same order + +### Requirement: Synonym aliases are explicit and minimal + +The system SHALL support optional explicit aliases for true semantic synonyms and legacy shorthand that normalization cannot infer. Alias coverage SHALL NOT be used to encode mechanical word-order permutations. + +#### Scenario: Semantic synonym resolves to canonical component + +- **GIVEN** metadata defines `toggle` as an alias for `switch` +- **WHEN** the user searches for `toggle` +- **THEN** `switch` is returned as a high-confidence match + +#### Scenario: Permutation is handled algorithmically, not via alias list + +- **GIVEN** `progress-linear` is available with no alias for `linear-progress` +- **WHEN** the user searches for `linear-progress` +- **THEN** `progress-linear` is still matched via normalization and token ranking diff --git a/packages/mcp/src/__tests__/knowledge/component-metadata.test.ts b/packages/mcp/src/__tests__/knowledge/component-metadata.test.ts index 8a7cbc1a..a0dfa301 100644 --- a/packages/mcp/src/__tests__/knowledge/component-metadata.test.ts +++ b/packages/mcp/src/__tests__/knowledge/component-metadata.test.ts @@ -133,6 +133,40 @@ describe("Component Metadata Knowledge Base", () => { }); }); + describe("synonym aliases structure", () => { + it("aliases should be non-empty string arrays when present", () => { + for (const [name, metadata] of Object.entries(COMPONENT_METADATA)) { + if (!metadata.aliases) continue; + + expect( + Array.isArray(metadata.aliases), + `${name}.aliases should be an array`, + ).toBe(true); + + expect( + metadata.aliases.length, + `${name}.aliases should be non-empty`, + ).toBeGreaterThan(0); + + for (const alias of metadata.aliases) { + expect(typeof alias, `${name} alias should be a string`).toBe( + "string", + ); + + expect( + alias.trim().length, + `${name} alias should not be empty`, + ).toBeGreaterThan(0); + + expect( + alias, + `${name} alias should not duplicate canonical name`, + ).not.toBe(name); + } + } + }); + }); + describe("variants structure", () => { it("components with variants should have non-empty string arrays", () => { for (const [name, metadata] of Object.entries(COMPONENT_METADATA)) { diff --git a/packages/mcp/src/__tests__/knowledge/component-search.test.ts b/packages/mcp/src/__tests__/knowledge/component-search.test.ts new file mode 100644 index 00000000..3e595f48 --- /dev/null +++ b/packages/mcp/src/__tests__/knowledge/component-search.test.ts @@ -0,0 +1,217 @@ +/** + * Isolated unit tests for the component search engine. + * + * Uses a small synthetic component set so scoring behaviour is tested + * independently of the real COMPONENT_METADATA / COMPONENT_THEMES data. + */ + +import { describe, expect, it } from "vitest"; +import type { ComponentMetadata } from "../../knowledge/component-metadata.js"; +import { createComponentSearcher } from "../../knowledge/component-search.js"; + +function buildSearcher( + metadata: Record, + extraThemeNames: string[] = [], +) { + const componentNames = [ + ...new Set([...Object.keys(metadata), ...extraThemeNames]), + ]; + return createComponentSearcher({ componentNames, metadata }); +} + +const MINI_METADATA: Record = { + avatar: { + selectors: { angular: "igx-avatar", webcomponents: "igc-avatar" }, + }, + switch: { + selectors: { angular: "igx-switch", webcomponents: "igc-switch" }, + aliases: ["toggle"], + }, + "progress-linear": { + selectors: { + angular: "igx-linear-bar", + webcomponents: "igc-linear-progress", + }, + }, + "progress-circular": { + selectors: { + angular: "igx-circular-bar", + webcomponents: "igc-circular-progress", + }, + aliases: ["spinner"], + }, + "flat-button": { + selectors: { + angular: ".igx-button--flat", + webcomponents: 'igc-button[variant="flat"]', + }, + }, + "date-picker": { + selectors: { + angular: "igx-date-picker", + webcomponents: "igc-date-picker", + }, + }, +}; + +describe("Component Search Engine", () => { + const searcher = buildSearcher(MINI_METADATA); + + // ===== Normalization ===== + + describe("normalization", () => { + it("returns empty for blank / whitespace / punctuation-only input", () => { + expect(searcher.search("")).toEqual([]); + expect(searcher.search(" ")).toEqual([]); + expect(searcher.search("---")).toEqual([]); + }); + + it("is case-insensitive", () => { + expect(searcher.search("AVATAR")).toContain("avatar"); + }); + + it("treats hyphens, underscores, and spaces as equivalent", () => { + const a = searcher.search("date-picker"); + const b = searcher.search("date_picker"); + const c = searcher.search("date picker"); + + expect(a).toContain("date-picker"); + expect(b).toContain("date-picker"); + expect(c).toContain("date-picker"); + }); + + it("strips igx- and igc- framework prefixes", () => { + const results = searcher.search("igx-linear-bar"); + expect(results[0]).toBe("progress-linear"); + }); + + it("strips igx/igc prefix tokens without hyphen", () => { + const results = searcher.search("igxavatar"); + expect(results).toContain("avatar"); + }); + }); + + // ===== Scoring tiers ===== + + describe("scoring tiers", () => { + it("exact normalised match ranks highest", () => { + const results = searcher.search("avatar"); + expect(results[0]).toBe("avatar"); + }); + + it("order-independent token-set match ranks high", () => { + const results = searcher.search("linear progress"); + expect(results[0]).toBe("progress-linear"); + }); + + it("token-coverage match works for partial overlap", () => { + const results = searcher.search("flat button"); + expect(results[0]).toBe("flat-button"); + }); + + it("substring fallback matches concatenated forms", () => { + const results = searcher.search("datepicker"); + expect(results).toContain("date-picker"); + }); + + it("short single tokens (< 4 chars) are filtered out", () => { + expect(searcher.search("bar")).toEqual([]); + expect(searcher.search("nav")).toEqual([]); + }); + }); + + // ===== Synonym aliases ===== + + describe("synonym aliases", () => { + it("resolves explicit synonym to canonical name", () => { + const results = searcher.search("toggle"); + expect(results[0]).toBe("switch"); + }); + + it("resolves multi-signal aliases", () => { + const results = searcher.search("spinner"); + expect(results[0]).toBe("progress-circular"); + }); + }); + + // ===== Selector matching ===== + + describe("selector signals", () => { + it("matches angular selector as search signal", () => { + const results = searcher.search("igx-circular-bar"); + expect(results[0]).toBe("progress-circular"); + }); + + it("matches web components selector", () => { + const results = searcher.search("igc-linear-progress"); + expect(results[0]).toBe("progress-linear"); + }); + + it("handles selector with attribute syntax", () => { + const results = searcher.search('igc-button[variant="flat"]'); + expect(results).toContain("flat-button"); + }); + }); + + // ===== Typo recovery ===== + + describe("typo recovery", () => { + it("recovers single-char typo for tokens >= 5 chars", () => { + const results = searcher.search("avatr"); + expect(results).toContain("avatar"); + }); + + it("does not fire for short tokens (< 5 chars)", () => { + expect(searcher.search("swch")).toEqual([]); + }); + + it("does not recover distance-2 typos", () => { + expect(searcher.search("pogrss")).toEqual([]); + }); + + it("ranks typo matches below exact matches", () => { + const exact = searcher.search("avatar"); + const typo = searcher.search("avatr"); + + expect(exact[0]).toBe("avatar"); + expect(typo).toContain("avatar"); + }); + }); + + // ===== Determinism ===== + + describe("determinism", () => { + it("returns identical results across repeated calls", () => { + const a = searcher.search("progress"); + const b = searcher.search("progress"); + expect(a).toEqual(b); + }); + + it("breaks ties lexicographically", () => { + const results = searcher.search("progress"); + const progressEntries = results.filter((r) => r.startsWith("progress-")); + + expect(progressEntries).toEqual([...progressEntries].sort()); + }); + }); + + // ===== Edge cases ===== + + describe("edge cases", () => { + it("deduplicates repeated tokens in query", () => { + const single = searcher.search("avatar"); + const doubled = searcher.search("avatar avatar"); + expect(doubled).toEqual(single); + }); + + it("returns empty for gibberish", () => { + expect(searcher.search("xyznonexistent123")).toEqual([]); + }); + + it("returns empty for very long unrelated query", () => { + expect( + searcher.search("this query has nothing to do with any component"), + ).toEqual([]); + }); + }); +}); diff --git a/packages/mcp/src/__tests__/knowledge/component-themes.test.ts b/packages/mcp/src/__tests__/knowledge/component-themes.test.ts index b226dd44..fc2f644e 100644 --- a/packages/mcp/src/__tests__/knowledge/component-themes.test.ts +++ b/packages/mcp/src/__tests__/knowledge/component-themes.test.ts @@ -145,6 +145,8 @@ describe("Component Themes Knowledge Base", () => { describe("searchComponents()", () => { it("should find components by partial name", () => { const results = searchComponents("button"); + + expect(results[0]).toBe("button"); expect(results).toContain("button"); expect(results).toContain("flat-button"); expect(results).toContain("button-group"); @@ -159,5 +161,42 @@ describe("Component Themes Knowledge Base", () => { const results = searchComponents("AVATAR"); expect(results).toContain("avatar"); }); + + it("should match order-independent component names", () => { + const results = searchComponents("linear progress"); + + expect(COMPONENT_METADATA["progress-linear"]?.aliases).not.toContain( + "linear-progress", + ); + expect(results[0]).toBe("progress-linear"); + }); + + it("should match selector-like names with framework prefixes", () => { + const results = searchComponents("igx-linear-bar"); + expect(results[0]).toBe("progress-linear"); + }); + + it("should match hyphenless forms through normalization", () => { + const results = searchComponents("datepicker"); + expect(results).toContain("date-picker"); + }); + + it("should resolve explicit synonym aliases", () => { + const results = searchComponents("toggle"); + expect(results[0]).toBe("switch"); + }); + + it("should recover common single-token typos", () => { + const results = searchComponents("pogress"); + expect(results).toContain("progress-linear"); + expect(results).toContain("progress-circular"); + }); + + it("should return deterministic ordering for the same query", () => { + const firstRun = searchComponents("linear progress"); + const secondRun = searchComponents("linear progress"); + + expect(secondRun).toEqual(firstRun); + }); }); }); diff --git a/packages/mcp/src/__tests__/tools/handlers/component-tokens.test.ts b/packages/mcp/src/__tests__/tools/handlers/component-tokens.test.ts index 84c9f3e0..7176c4a9 100644 --- a/packages/mcp/src/__tests__/tools/handlers/component-tokens.test.ts +++ b/packages/mcp/src/__tests__/tools/handlers/component-tokens.test.ts @@ -109,6 +109,31 @@ describe("handleGetComponentDesignTokens", () => { ); }); + it("suggests progress-linear for linear progress phrasing", async () => { + const result = await handleGetComponentDesignTokens({ + component: "linear progress", + }); + const text = result.content[0].text; + + expect(text).toContain('Component "linear progress" not found.'); + expect(text).toContain("**Similar components:**"); + + const suggestions = text.split("**Similar components:**")[1] ?? ""; + expect(suggestions.trimStart().startsWith("- progress-linear")).toBe(true); + }); + + it("suggests progress components for common typo phrasing", async () => { + const result = await handleGetComponentDesignTokens({ + component: "pogress", + }); + const text = result.content[0].text; + + expect(text).toContain('Component "pogress" not found.'); + expect(text).toContain("**Similar components:**"); + expect(text).toContain("- progress-circular"); + expect(text).toContain("- progress-linear"); + }); + // ===== Child Component Tests ===== it("shows relationship note for child component (list-item)", async () => { diff --git a/packages/mcp/src/__tests__/tools/handlers/handlers.test.ts b/packages/mcp/src/__tests__/tools/handlers/handlers.test.ts index 3d1112d9..1c21fe7a 100644 --- a/packages/mcp/src/__tests__/tools/handlers/handlers.test.ts +++ b/packages/mcp/src/__tests__/tools/handlers/handlers.test.ts @@ -439,6 +439,43 @@ describe("handleCreateComponentTheme", () => { expect(text).toContain("not supported"); }); + it("suggests progress-linear for linear progress phrasing", async () => { + const result = await handleCreateComponentTheme({ + platform: "angular", + component: "linear progress", + tokens: { + background: "#ff5722", + }, + }); + + expect(result.isError).toBe(true); + + const text = result.content[0].text; + expect(text).toContain('Component "linear progress" not found.'); + expect(text).toContain("**Similar components:**"); + + const suggestions = text.split("**Similar components:**")[1] ?? ""; + expect(suggestions.trimStart().startsWith("- progress-linear")).toBe(true); + }); + + it("suggests progress components for common typo phrasing", async () => { + const result = await handleCreateComponentTheme({ + platform: "angular", + component: "pogress", + tokens: { + background: "#ff5722", + }, + }); + + expect(result.isError).toBe(true); + + const text = result.content[0].text; + expect(text).toContain('Component "pogress" not found.'); + expect(text).toContain("**Similar components:**"); + expect(text).toContain("- progress-circular"); + expect(text).toContain("- progress-linear"); + }); + it("returns MCP response format for valid component", async () => { const result = await handleCreateComponentTheme({ platform: "webcomponents", diff --git a/packages/mcp/src/knowledge/component-metadata.ts b/packages/mcp/src/knowledge/component-metadata.ts index d1152162..30365e76 100644 --- a/packages/mcp/src/knowledge/component-metadata.ts +++ b/packages/mcp/src/knowledge/component-metadata.ts @@ -68,6 +68,8 @@ export interface ComponentMetadata { selectors?: ComponentSelectors; /** Optional theme alias for components that reuse another component theme */ theme?: string; + /** Optional synonym aliases for search (synonyms only, not word-order permutations). */ + aliases?: string[]; /** Parent component name for child sub-components. When set, theming scope uses the parent's selector instead of the child's own. Mutually exclusive with `compound`. */ childOf?: string; /** Present only for components with variant-specific themes (e.g., button) */ @@ -239,6 +241,7 @@ If customizing the banner background, ensure flat-button foreground contrasts ag }, combo: { selectors: { angular: "igx-combo", webcomponents: "igc-combo" }, + aliases: ["combobox", "autocomplete"], compound: { description: "The combo component combines input, drop-down, and checkbox components.", @@ -333,9 +336,11 @@ If customizing the banner background, ensure flat-button foreground contrasts ag }, "date-time-input": { selectors: { angular: null, webcomponents: "igc-date-time-input" }, + aliases: ["datetime input", "date time input"], }, dialog: { selectors: { angular: ".igx-dialog", webcomponents: "igc-dialog" }, + aliases: ["modal", "popup"], compound: { description: "The dialog component uses flat-buttons for the actions", relatedThemes: ["flat-button"], @@ -360,6 +365,7 @@ If customizing the dialog background, ensure flat-button foreground contrasts ag angular: ".igx-drop-down__list", webcomponents: "igc-dropdown", }, + aliases: ["dropdown menu"], }, "drop-down-item": { childOf: "drop-down", @@ -479,6 +485,14 @@ Both themes should share the same visual treatment as the file-input wrapper.`, }, navdrawer: { selectors: { angular: "igx-nav-drawer", webcomponents: "igc-nav-drawer" }, + aliases: [ + "drawer", + "side drawer", + "side nav", + "sidenav", + "navigation drawer", + "navigation panel", + ], }, "nav-drawer-item": { childOf: "navdrawer", @@ -488,6 +502,7 @@ Both themes should share the same visual treatment as the file-input wrapper.`, }, paginator: { selectors: { angular: "igx-paginator", webcomponents: "igc-paginator" }, + aliases: ["pagination", "pager"], compound: { description: "The paginator uses combo and flat-icon-buttons for the page controls.", @@ -516,15 +531,18 @@ and descriptions from get_component_design_tokens for each child to guide value angular: "igx-circular-bar", webcomponents: "igc-circular-progress", }, + aliases: ["spinner", "circular loader", "loading spinner"], }, "progress-linear": { selectors: { angular: "igx-linear-bar", webcomponents: "igc-linear-progress", }, + aliases: ["progress-bar", "loading bar", "linear loader"], }, "query-builder": { selectors: { angular: "igx-query-builder", webcomponents: null }, + aliases: ["filter builder"], compound: { description: "The query builder uses inputs, dropdowns, chips, buttons and button-groups for building query expressions.", @@ -547,6 +565,7 @@ chips for displaying conditions, and buttons/button-groups for adding and groupi }, rating: { selectors: { angular: "igc-rating", webcomponents: "igc-rating" }, + aliases: ["star rating"], }, ripple: { selectors: { angular: "igx-ripple", webcomponents: "igc-ripple" }, @@ -556,6 +575,7 @@ chips for displaying conditions, and buttons/button-groups for adding and groupi }, select: { selectors: { angular: "igx-select", webcomponents: "igc-select" }, + aliases: ["select box"], compound: { description: "The select component combines input-group and drop-down components.", @@ -591,6 +611,7 @@ The drop-down background should match the select surface intent.`, }, switch: { selectors: { angular: "igx-switch", webcomponents: "igc-switch" }, + aliases: ["toggle"], }, tabs: { selectors: { angular: "igx-tabs", webcomponents: "igc-tabs" }, diff --git a/packages/mcp/src/knowledge/component-search.ts b/packages/mcp/src/knowledge/component-search.ts new file mode 100644 index 00000000..46e5bb2e --- /dev/null +++ b/packages/mcp/src/knowledge/component-search.ts @@ -0,0 +1,357 @@ +/** + * Normalized, ranked component search engine. + * + * Matches user queries against component names, metadata aliases, and + * platform selectors using order-independent token comparison, substring + * fallback, and single-token typo recovery (Levenshtein distance ≤ 1). + */ + +import type { + ComponentMetadata, + ComponentSelectors, +} from "./component-metadata.js"; + +interface NormalizedSearchTerm { + /** Tokens concatenated without separators — used for substring matching (order-sensitive). */ + compact: string; + /** Sorted, pipe-delimited unique tokens — used for order-independent set equality. */ + tokenSetKey: string; + tokens: string[]; +} + +interface ComponentSearchEntry { + name: string; + signals: NormalizedSearchTerm[]; +} + +/** Options accepted by {@link createComponentSearcher}. */ +export interface CreateComponentSearcherOptions { + componentNames: string[]; + metadata: Record; +} + +/** Pre-built search index with a single `search` method. */ +export interface ComponentSearcher { + search(query: string): string[]; +} + +/* + * Prefix stripping happens in two complementary phases: + * + * 1. FRAMEWORK_PREFIX_PATTERN strips `igx-` / `igc-` (with hyphen) from the + * raw string before tokenisation — handles `igx-linear-bar` → `linear bar`. + * + * 2. stripFrameworkPrefixToken strips `igx` / `igc` (without hyphen) from + * individual tokens after splitting — handles `igxbutton` → `button` when + * no hyphen is present and the regex cannot match. + */ +const FRAMEWORK_PREFIX_PATTERN = /\big[cx]-/g; +const NON_ALPHANUMERIC_PATTERN = /[^a-z0-9]+/g; +const MIN_SEARCH_SCORE = 500; + +function stripFrameworkPrefixToken(token: string): string { + if (token.startsWith("igx") && token.length > 3) { + return token.slice(3); + } + + if (token.startsWith("igc") && token.length > 3) { + return token.slice(3); + } + + return token; +} + +function normalizeSearchTerm(term: string): NormalizedSearchTerm | undefined { + const lowerTerm = term.toLowerCase().trim(); + + if (!lowerTerm) { + return undefined; + } + + const normalizedDelimiters = lowerTerm + .replace(FRAMEWORK_PREFIX_PATTERN, "") + .replace(NON_ALPHANUMERIC_PATTERN, " ") + .trim(); + + if (!normalizedDelimiters) { + return undefined; + } + + const tokens = normalizedDelimiters + .split(/\s+/) + .map(stripFrameworkPrefixToken) + .filter((token) => token.length > 0); + + if (tokens.length === 0) { + return undefined; + } + + const uniqueTokens = [...new Set(tokens)]; + + return { + compact: tokens.join(""), + tokenSetKey: uniqueTokens.slice().sort().join("|"), + tokens: uniqueTokens, + }; +} + +function getSelectorSearchSignals(selectors?: ComponentSelectors): string[] { + if (!selectors) { + return []; + } + + const values = [selectors.angular, selectors.webcomponents]; + const signals: string[] = []; + + for (const value of values) { + if (!value) { + continue; + } + + if (Array.isArray(value)) { + signals.push(...value); + continue; + } + + signals.push(value); + } + + return signals; +} + +function getComponentSearchSignals( + componentName: string, + metadataByName: Record, +): string[] { + const metadata = metadataByName[componentName]; + const signals = new Set([componentName]); + + if (!metadata) { + return [...signals]; + } + + metadata.aliases?.forEach((alias) => { + signals.add(alias); + }); + + getSelectorSearchSignals(metadata.selectors).forEach((selector) => { + signals.add(selector); + }); + + return [...signals]; +} + +function buildComponentSearchIndex( + searchableNames: string[], + metadataByName: Record, +): ComponentSearchEntry[] { + return searchableNames.map((name) => { + const signals = getComponentSearchSignals(name, metadataByName) + .map(normalizeSearchTerm) + .filter((signal): signal is NormalizedSearchTerm => !!signal); + + return { name, signals }; + }); +} + +function getTokenCoverageScore( + query: NormalizedSearchTerm, + signal: NormalizedSearchTerm, +): number { + if (query.tokens.length === 0 || signal.tokens.length === 0) { + return 0; + } + + const signalTokens = new Set(signal.tokens); + let overlapCount = 0; + + for (const token of query.tokens) { + if (signalTokens.has(token)) { + overlapCount++; + } + } + + if (overlapCount === 0) { + return 0; + } + + const queryCoverage = overlapCount / query.tokens.length; + const signalCoverage = overlapCount / signal.tokens.length; + + if (query.tokens.length === 1 && query.tokens[0].length < 4) { + return queryCoverage === 1 && signalCoverage === 1 ? 900 : 0; + } + + if (queryCoverage === 1) { + return ( + 800 + + overlapCount * 10 - + Math.max(0, signal.tokens.length - query.tokens.length) + ); + } + + if (query.tokens.length > 1 && queryCoverage >= 0.5) { + return 650 + Math.round(queryCoverage * 100 + signalCoverage * 50); + } + + return 0; +} + +function getSubstringFallbackScore( + query: NormalizedSearchTerm, + signal: NormalizedSearchTerm, +): number { + if (query.compact.length < 4 || signal.compact.length === 0) { + return 0; + } + + if (signal.compact.includes(query.compact)) { + return 500 + Math.min(query.compact.length, 100); + } + + return 0; +} + +function getEditDistanceWithinLimit( + source: string, + target: string, + limit: number, +): number | undefined { + if (source === target) { + return 0; + } + + const sourceLength = source.length; + const targetLength = target.length; + + if (Math.abs(sourceLength - targetLength) > limit) { + return undefined; + } + + let previous = new Array(targetLength + 1); + let current = new Array(targetLength + 1); + + for (let j = 0; j <= targetLength; j++) { + previous[j] = j; + } + + for (let i = 1; i <= sourceLength; i++) { + current[0] = i; + let rowMin = current[0]; + + for (let j = 1; j <= targetLength; j++) { + const substitutionCost = source[i - 1] === target[j - 1] ? 0 : 1; + + current[j] = Math.min( + previous[j] + 1, + current[j - 1] + 1, + previous[j - 1] + substitutionCost, + ); + + rowMin = Math.min(rowMin, current[j]); + } + + if (rowMin > limit) { + return undefined; + } + + [previous, current] = [current, previous]; + } + + return previous[targetLength] <= limit ? previous[targetLength] : undefined; +} + +function getTypoFallbackScore( + query: NormalizedSearchTerm, + signal: NormalizedSearchTerm, +): number { + if (query.tokens.length !== 1) { + return 0; + } + + const [queryToken] = query.tokens; + + if (queryToken.length < 5) { + return 0; + } + + let bestScore = 0; + + for (const signalToken of signal.tokens) { + if (signalToken.length < 5) { + continue; + } + + const distance = getEditDistanceWithinLimit(queryToken, signalToken, 1); + + if (distance === 1) { + bestScore = Math.max(bestScore, 540); + } + } + + return bestScore; +} + +function scoreSearchEntry( + query: NormalizedSearchTerm, + entry: ComponentSearchEntry, +): number { + let bestScore = 0; + + for (const signal of entry.signals) { + if (signal.compact === query.compact) { + bestScore = Math.max(bestScore, 1000); + continue; + } + + if (signal.tokenSetKey === query.tokenSetKey) { + bestScore = Math.max(bestScore, 900); + continue; + } + + bestScore = Math.max(bestScore, getTokenCoverageScore(query, signal)); + bestScore = Math.max(bestScore, getSubstringFallbackScore(query, signal)); + bestScore = Math.max(bestScore, getTypoFallbackScore(query, signal)); + } + + return bestScore; +} + +/** + * Build a pre-indexed component searcher from theme names and metadata. + * + * The returned searcher normalises queries, scores them against an + * index of canonical names / aliases / selectors, and returns results + * ranked by confidence (exact > token-set > overlap > substring > typo). + */ +export function createComponentSearcher( + options: CreateComponentSearcherOptions, +): ComponentSearcher { + const searchableNames = Array.from( + new Set([...options.componentNames, ...Object.keys(options.metadata)]), + ).sort((a, b) => a.localeCompare(b)); + + const componentSearchIndex = buildComponentSearchIndex( + searchableNames, + options.metadata, + ); + + return { + search(query: string): string[] { + const normalizedQuery = normalizeSearchTerm(query); + + if (!normalizedQuery) { + return []; + } + + return componentSearchIndex + .map((entry) => ({ + name: entry.name, + score: scoreSearchEntry(normalizedQuery, entry), + })) + .filter((match) => match.score >= MIN_SEARCH_SCORE) + .sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)) + .map((match) => match.name); + }, + }; +} diff --git a/packages/mcp/src/knowledge/component-themes.ts b/packages/mcp/src/knowledge/component-themes.ts index cc6a096e..97b612d6 100644 --- a/packages/mcp/src/knowledge/component-themes.ts +++ b/packages/mcp/src/knowledge/component-themes.ts @@ -5,6 +5,7 @@ import { ComponentThemes as themesData } from "igniteui-theming"; import { COMPONENT_METADATA } from "./component-metadata.js"; +import { createComponentSearcher } from "./component-search.js"; /** * Represents a design token (themeable property) for a component. @@ -62,6 +63,11 @@ export const COMPONENT_THEMES = themesData as Record; */ export const COMPONENT_NAMES = Object.keys(COMPONENT_THEMES); +const componentSearcher = createComponentSearcher({ + componentNames: COMPONENT_NAMES, + metadata: COMPONENT_METADATA, +}); + /** * Get a component theme by name. * @param componentName - The component name (e.g., 'button', 'avatar') @@ -158,8 +164,5 @@ export function validateTokens( * @returns Array of matching component names */ export function searchComponents(query: string): string[] { - const lowerQuery = query.toLowerCase(); - return COMPONENT_NAMES.filter((name) => - name.toLowerCase().includes(lowerQuery), - ); + return componentSearcher.search(query); } diff --git a/packages/mcp/src/knowledge/index.ts b/packages/mcp/src/knowledge/index.ts index f921c886..3be52b74 100644 --- a/packages/mcp/src/knowledge/index.ts +++ b/packages/mcp/src/knowledge/index.ts @@ -40,6 +40,12 @@ export { type TokenDerivation, VARIANT_THEME_NAMES, } from "./component-metadata.js"; +// Component Search +export { + type ComponentSearcher, + type CreateComponentSearcherOptions, + createComponentSearcher, +} from "./component-search.js"; // Component Themes export { COMPONENT_NAMES, From 5d70159c2b5a600efa57323581be348fb45471b1 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Thu, 2 Apr 2026 10:50:49 +0300 Subject: [PATCH 5/9] fix(mcp): add @use placement guidance to prevent agents misplacing imports LLM agents sometimes place @use rules mid-file when combining Sass outputs from multiple tool calls, breaking compilation. Add three layers of guidance to prevent this: - Inline comment above @use lines in all generated Sass output - Assembly note after each Sass code block in handler response text - SASS FILE PLACEMENT section in all Sass-generating tool descriptions Centralise guidance text in utils/sass.ts (constants) and descriptions.ts (FRAGMENTS.SASS_FILE_PLACEMENT) for single-source maintenance. Closes #539 --- .../.openspec.yaml | 2 + .../design.md | 84 +++++++++++++++++++ .../proposal.md | 27 ++++++ .../specs/component-theming/spec.md | 36 ++++++++ .../specs/palette-generation/spec.md | 43 ++++++++++ .../specs/sass-use-placement-guidance/spec.md | 68 +++++++++++++++ .../specs/theme-generation/spec.md | 43 ++++++++++ .../tasks.md | 36 ++++++++ openspec/specs/component-theming/spec.md | 12 ++- openspec/specs/palette-generation/spec.md | 12 ++- .../specs/sass-use-placement-guidance/spec.md | 72 ++++++++++++++++ openspec/specs/theme-generation/spec.md | 12 ++- .../src/__tests__/knowledge/sass-api.test.ts | 6 +- .../__tests__/tools/handlers/handlers.test.ts | 75 +++++++++++++++++ packages/mcp/src/__tests__/utils/sass.test.ts | 18 ++-- packages/mcp/src/tools/descriptions.ts | 18 ++++ .../mcp/src/tools/handlers/component-theme.ts | 2 + .../mcp/src/tools/handlers/custom-palette.ts | 2 + packages/mcp/src/tools/handlers/elevations.ts | 2 + packages/mcp/src/tools/handlers/palette.ts | 2 + packages/mcp/src/tools/handlers/theme.ts | 2 + packages/mcp/src/tools/handlers/typography.ts | 2 + packages/mcp/src/utils/sass.ts | 14 +++- 23 files changed, 575 insertions(+), 15 deletions(-) create mode 100644 openspec/changes/archive/2026-04-02-sass-use-placement-guidance/.openspec.yaml create mode 100644 openspec/changes/archive/2026-04-02-sass-use-placement-guidance/design.md create mode 100644 openspec/changes/archive/2026-04-02-sass-use-placement-guidance/proposal.md create mode 100644 openspec/changes/archive/2026-04-02-sass-use-placement-guidance/specs/component-theming/spec.md create mode 100644 openspec/changes/archive/2026-04-02-sass-use-placement-guidance/specs/palette-generation/spec.md create mode 100644 openspec/changes/archive/2026-04-02-sass-use-placement-guidance/specs/sass-use-placement-guidance/spec.md create mode 100644 openspec/changes/archive/2026-04-02-sass-use-placement-guidance/specs/theme-generation/spec.md create mode 100644 openspec/changes/archive/2026-04-02-sass-use-placement-guidance/tasks.md create mode 100644 openspec/specs/sass-use-placement-guidance/spec.md diff --git a/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/.openspec.yaml b/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/.openspec.yaml new file mode 100644 index 00000000..6a5db8c7 --- /dev/null +++ b/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-02 diff --git a/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/design.md b/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/design.md new file mode 100644 index 00000000..58ee254e --- /dev/null +++ b/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/design.md @@ -0,0 +1,84 @@ +## Context + +Each Sass-generating MCP tool (`create_palette`, `create_theme`, `create_typography`, `create_elevations`, `create_custom_palette`, `create_component_theme`) emits a standalone code block with its own `@use` statement at the top. When an LLM combines outputs from multiple tool calls into a single `.scss` file, it sometimes places `@use` rules after other statements or duplicates them, breaking Sass compilation. + +The `@use` lines are generated centrally in `utils/sass.ts` (`generateUseStatement`, `generatePresetImports`) and assembled into output by `generators/sass.ts` and platform-specific generators. Handler response text wraps the Sass in a markdown code fence but includes no assembly guidance. + +## Goals / Non-Goals + +**Goals:** + +- Make the `@use` placement constraint visible to the LLM at three levels: inline in generated code, in response text after the code block, and in tool descriptions +- Keep the guidance minimal so it doesn't bloat output or distract from the primary content + +**Non-Goals:** + +- Restructuring tool output format (splitting imports from code into separate sections) — reserved for a follow-up if this lighter approach is insufficient +- Changing how `@use` statements are generated or resolved +- Handling non-Sass output (CSS output mode is unaffected) + +## Decisions + +### 1) Add an inline comment above `@use` lines in generated Sass + +Modify `generateUseStatement` in `utils/sass.ts` to prepend a short comment: + +```scss +// NOTE: @use rules must be at the top of the file. Deduplicate when combining multiple outputs. +@use 'igniteui-theming' as *; +``` + +Rationale: This is the most visible placement — it's inside the code the LLM reads and copies. One line, no ambiguity. + +Alternatives considered: + +- Comment after the `@use` line: less prominent, LLM may not associate it with placement. +- Multi-line comment block: too verbose for something that should be a brief nudge. + +### 2) Append an assembly note after each Sass code block in handler response text + +Each Sass-generating handler will append a brief note after the code fence: + +``` +> **File placement:** `@use` rules must appear at the very top of the `.scss` file, +> before any other statements. When combining outputs from multiple tools, +> keep only one `@use` per module path. +``` + +This applies to handlers in: `palette.ts`, `custom-palette.ts`, `typography.ts`, `elevations.ts`, `theme.ts`, `component-theme.ts`. + +Rationale: The inline comment handles the code-level signal; this handles the prose-level instruction that the LLM processes when deciding how to assemble the file. + +Alternatives considered: + +- Only inline comment (no prose): some LLMs weight prose instructions more than code comments. +- Longer assembly instructions with examples: too verbose for every tool response. + +### 3) Add brief `@use` note to Sass-generating tool descriptions + +Add a short `` entry or append to existing notes in tool descriptions for `create_palette`, `create_theme`, `create_typography`, `create_elevations`, `create_custom_palette`, `create_component_theme`: + +``` +When combining Sass output from multiple tools into one file, all @use rules +must appear at the top before any other statements. Deduplicate @use lines +that share the same module path. +``` + +Rationale: Tool descriptions are read by the LLM before it calls the tool. This primes the LLM to think about placement before it even sees the output. + +Alternatives considered: + +- Only adding to `create_component_theme` (most common composition target): misses cases where palette + typography are combined without component themes. + +### 4) Centralize the assembly note text + +Define the assembly note string once in `utils/sass.ts` and import it in each handler, rather than duplicating the text. + +Rationale: Single source of truth; easy to tune wording later without touching six files. + +## Risks / Trade-offs + +- [Risk] Inline comment adds one line to every Sass output, including cases where the user never combines files → Acceptable; the line is short and informative. +- [Risk] Assembly note in response text may be ignored by some LLM clients → Mitigated by the inline comment and description-level guidance working in parallel. +- [Risk] Description additions make already-long tool descriptions slightly longer → Mitigated by keeping the addition to 2-3 sentences. +- [Risk] This approach may not fully prevent the issue for all LLM agents → If misplacement persists, the structured output approach (option C from exploration) can be implemented as a follow-up. diff --git a/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/proposal.md b/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/proposal.md new file mode 100644 index 00000000..24fdabbd --- /dev/null +++ b/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/proposal.md @@ -0,0 +1,27 @@ +## Why + +LLM agents sometimes place `@use` rules in the middle of generated `.scss` files when combining outputs from multiple MCP tool calls (e.g. palette + component theme). Sass requires `@use` before any other statements, so misplacement breaks compilation. The MCP server currently provides no guidance about this constraint — each tool emits standalone code with its own `@use`, and there is no instruction on how to merge them. + +## What Changes + +- Add an inline comment above `@use` lines in all generated Sass output reminding that `@use` must appear at the top of the file and be deduplicated when combining outputs +- Add a placement/assembly note after each Sass code block in handler response text explaining the file-level `@use` constraint +- Add brief `@use` placement guidance to Sass-generating tool descriptions so the constraint is visible in tool metadata + +## Capabilities + +### New Capabilities + +- `sass-use-placement-guidance`: Defines the `@use` placement guidance that Sass-generating tools must include in their output (inline comments, assembly notes, description text) + +### Modified Capabilities + +- `theme-generation`: Tool output now includes `@use` placement guidance in generated Sass and response text +- `palette-generation`: Tool output now includes `@use` placement guidance in generated Sass and response text +- `component-theming`: Tool output now includes `@use` placement guidance in generated Sass and response text + +## Impact + +- **Code**: `utils/sass.ts` (inline comment in `generateUseStatement`), all Sass-generating handlers (assembly note appended to response text), `tools/descriptions.ts` (brief `@use` note added to relevant tool descriptions) +- **Tests**: Verify inline comment and assembly note presence in handler output +- **Rollback**: Remove inline comments, assembly notes, and description additions; revert to current output format diff --git a/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/specs/component-theming/spec.md b/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/specs/component-theming/spec.md new file mode 100644 index 00000000..57532922 --- /dev/null +++ b/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/specs/component-theming/spec.md @@ -0,0 +1,36 @@ +## MODIFIED Requirements + +### Requirement: Component theming requires platform + +The `create_component_theme` tool requires a `platform` parameter and SHALL specify compound-component completeness rules to reduce incomplete outputs. Generated Sass SHALL include an inline `@use` placement comment, and the handler response text SHALL include an assembly note about `@use` top-of-file placement and deduplication. + +#### Scenario: Missing platform + +- **WHEN** `platform` is not provided +- **THEN** the tool returns an error indicating `platform` is required + +#### Scenario: Generic platform rejected + +- **WHEN** `platform: "generic"` is provided +- **THEN** the tool SHALL return an error stating that `create_component_theme` requires a specific Ignite UI product platform (angular, webcomponents, react, or blazor) +- **AND** the error SHALL explain that component theming requires platform-specific selectors and variable prefixes that do not exist in generic mode + +#### Scenario: Compound guidance treated as completeness criteria + +- **WHEN** a user requests theming for a compound component +- **THEN** the guidance states the response is incomplete if related theme calls are omitted when selectors are available + +#### Scenario: Canonical compound example provided + +- **WHEN** a compound component is detected +- **THEN** guidance includes a short canonical example (e.g., combo) demonstrating the full multi-call flow + +#### Scenario: Inline placement comment in Sass + +- **WHEN** `create_component_theme` returns Sass output +- **THEN** the Sass code block SHALL contain a comment above the first `@use` about top-of-file placement and deduplication + +#### Scenario: Assembly note in response + +- **WHEN** `create_component_theme` returns Sass output +- **THEN** the handler response text SHALL include a placement note after the code block about `@use` top-of-file and deduplication diff --git a/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/specs/palette-generation/spec.md b/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/specs/palette-generation/spec.md new file mode 100644 index 00000000..f8850fe2 --- /dev/null +++ b/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/specs/palette-generation/spec.md @@ -0,0 +1,43 @@ +## MODIFIED Requirements + +### Requirement: Palette generation returns Sass by default + +The `create_palette` tool returns an MCP text response with a Sass code block when `output` is not specified. Generated Sass SHALL include an inline `@use` placement comment, and the handler response text SHALL include an assembly note about `@use` top-of-file placement and deduplication. + +#### Scenario: Required colors + +- **WHEN** `primary`, `secondary`, and `surface` are provided +- **THEN** the response includes a Sass block containing `palette(` +- **AND** the Sass block includes `$primary`, `$secondary`, and `$surface` values + +#### Scenario: Optional colors + +- **WHEN** `gray`, `info`, `success`, `warn`, or `error` are provided +- **THEN** the Sass block includes those arguments in the palette definition + +#### Scenario: Named palette variable + +- **WHEN** `name` is provided +- **THEN** the Sass block uses `$-palette` as the variable name + +#### Scenario: Platform info + +- **WHEN** `platform` is specified +- **THEN** the response includes the platform label +- **WHEN** `platform` is not specified +- **THEN** the response includes a hint to specify `platform` + +#### Scenario: Suitability warnings + +- **WHEN** `surface` conflicts with `variant` (light vs dark) +- **THEN** the response includes a warning but still returns code + +#### Scenario: Inline placement comment in Sass + +- **WHEN** `create_palette` returns Sass output +- **THEN** the Sass code block SHALL contain a comment above the first `@use` about top-of-file placement and deduplication + +#### Scenario: Assembly note in response + +- **WHEN** `create_palette` returns Sass output +- **THEN** the handler response text SHALL include a placement note after the code block about `@use` top-of-file and deduplication diff --git a/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/specs/sass-use-placement-guidance/spec.md b/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/specs/sass-use-placement-guidance/spec.md new file mode 100644 index 00000000..69cf8142 --- /dev/null +++ b/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/specs/sass-use-placement-guidance/spec.md @@ -0,0 +1,68 @@ +## ADDED Requirements + +### Requirement: Generated Sass includes inline @use placement comment + +All Sass output that contains `@use` statements SHALL include a single-line comment above the first `@use` rule reminding that `@use` must appear at the top of the file and be deduplicated when combining multiple tool outputs. + +#### Scenario: Inline comment present in generated Sass + +- **GIVEN** a Sass-generating tool is called with default (Sass) output +- **WHEN** the tool returns a Sass code block +- **THEN** the code block SHALL contain a comment line above the first `@use` that mentions top-of-file placement and deduplication + +#### Scenario: CSS output is unaffected + +- **GIVEN** a tool is called with `output: "css"` +- **WHEN** the tool returns CSS custom properties +- **THEN** no `@use` placement comment SHALL be present + +### Requirement: Handler response text includes assembly note after Sass code + +Every Sass-generating handler SHALL append an assembly note after the Sass code block in its response text. The note SHALL instruct that `@use` rules must appear at the top of the `.scss` file and be deduplicated when combining outputs from multiple tools. + +#### Scenario: Assembly note present for palette handler + +- **GIVEN** `create_palette` is called with Sass output +- **WHEN** the handler returns its response text +- **THEN** the text after the Sass code fence SHALL contain a placement note mentioning `@use` top-of-file and deduplication + +#### Scenario: Assembly note present for component theme handler + +- **GIVEN** `create_component_theme` is called with Sass output +- **WHEN** the handler returns its response text +- **THEN** the text after the Sass code fence SHALL contain a placement note mentioning `@use` top-of-file and deduplication + +#### Scenario: Assembly note present for theme handler + +- **GIVEN** `create_theme` is called with Sass output +- **WHEN** the handler returns its response text +- **THEN** the text after the Sass code fence SHALL contain a placement note mentioning `@use` top-of-file and deduplication + +### Requirement: Sass-generating tool descriptions include @use guidance + +Each Sass-generating tool description SHALL include a brief note about `@use` placement and deduplication when combining outputs into a single file. The note SHALL be within the tool's `` or equivalent section. + +#### Scenario: create_palette description includes guidance + +- **WHEN** the `create_palette` tool description is inspected +- **THEN** it SHALL contain text about `@use` placement at the top and deduplication + +#### Scenario: create_theme description includes guidance + +- **WHEN** the `create_theme` tool description is inspected +- **THEN** it SHALL contain text about `@use` placement at the top and deduplication + +#### Scenario: create_component_theme description includes guidance + +- **WHEN** the `create_component_theme` tool description is inspected +- **THEN** it SHALL contain text about `@use` placement at the top and deduplication + +### Requirement: Assembly note text is centralised + +The assembly note string SHALL be defined once and imported by all handlers that emit Sass output, rather than duplicated across handler files. + +#### Scenario: Single source of truth for note text + +- **GIVEN** the assembly note wording is updated +- **WHEN** only one source definition is changed +- **THEN** all handlers SHALL reflect the updated wording diff --git a/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/specs/theme-generation/spec.md b/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/specs/theme-generation/spec.md new file mode 100644 index 00000000..e11e1c00 --- /dev/null +++ b/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/specs/theme-generation/spec.md @@ -0,0 +1,43 @@ +## MODIFIED Requirements + +### Requirement: Theme generation is platform-aware + +The `create_theme` tool generates Sass output that follows platform-specific conventions. Generated Sass SHALL include an inline `@use` placement comment, and the handler response text SHALL include an assembly note about `@use` top-of-file placement and deduplication. + +#### Scenario: Angular output + +- **WHEN** `platform: angular` is provided +- **THEN** the output includes `@include core()` +- **AND** the output includes `@include theme(` with schema and palette +- **AND** the Sass code block includes an inline comment about `@use` placement + +#### Scenario: Web Components output + +- **WHEN** `platform: webcomponents` is provided +- **THEN** the output uses `palette`, `typography`, and `elevations` mixins +- **AND** spacing is included by default +- **AND** the Sass code block includes an inline comment about `@use` placement + +#### Scenario: React/Blazor output + +- **WHEN** `platform: react` or `platform: blazor` is provided +- **THEN** the output uses the Web Components mixin pattern +- **AND** `core()` and Angular `theme()` mixins are not used + +#### Scenario: Generic platform output + +- **WHEN** `platform: "generic"` is provided +- **THEN** the output SHALL use the generic theme generator (same as the current `undefined` platform path) +- **AND** the output SHALL use `@use 'igniteui-theming' as *;` as the import +- **AND** the response platform note SHALL display `"Platform: Ignite UI Theming (Standalone)"` instead of `"Platform: Not specified (generic output)"` + +#### Scenario: Platform hint + +- **WHEN** `platform` is not specified (undefined) +- **THEN** the response includes a hint to specify `platform` +- **AND** the hint SHALL mention `"generic"` as a valid option for platform-agnostic output + +#### Scenario: Assembly note in response + +- **WHEN** `create_theme` returns Sass output +- **THEN** the handler response text SHALL include a placement note after the code block about `@use` top-of-file and deduplication diff --git a/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/tasks.md b/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/tasks.md new file mode 100644 index 00000000..3f735580 --- /dev/null +++ b/openspec/changes/archive/2026-04-02-sass-use-placement-guidance/tasks.md @@ -0,0 +1,36 @@ +## 1. Centralized Guidance Constants + +- [x] 1.1 Add `SASS_USE_INLINE_COMMENT` constant to `utils/sass.ts` with the single-line inline comment text +- [x] 1.2 Add `SASS_USE_ASSEMBLY_NOTE` constant to `utils/sass.ts` with the markdown assembly note text +- [x] 1.3 Export both constants from `utils/sass.ts` + +## 2. Inline Comment in Generated Sass + +- [x] 2.1 Modify `generateUseStatement` in `utils/sass.ts` to prepend the inline comment above the `@use` line +- [x] 2.2 Modify `generatePresetImports` to not duplicate the comment (only `generateUseStatement` emits it) +- [x] 2.3 Verify platform-specific generators (Angular, Web Components) include the comment via `generateUseStatement` + +## 3. Assembly Note in Handler Response Text + +- [x] 3.1 Append `SASS_USE_ASSEMBLY_NOTE` to Sass output in `handlers/palette.ts` +- [x] 3.2 Append `SASS_USE_ASSEMBLY_NOTE` to Sass output in `handlers/custom-palette.ts` +- [x] 3.3 Append `SASS_USE_ASSEMBLY_NOTE` to Sass output in `handlers/typography.ts` +- [x] 3.4 Append `SASS_USE_ASSEMBLY_NOTE` to Sass output in `handlers/elevations.ts` +- [x] 3.5 Append `SASS_USE_ASSEMBLY_NOTE` to Sass output in `handlers/theme.ts` +- [x] 3.6 Append `SASS_USE_ASSEMBLY_NOTE` to Sass output in `handlers/component-theme.ts` + +## 4. Tool Description Guidance + +- [x] 4.1 Add `@use` placement note to `create_palette` description in `tools/descriptions.ts` +- [x] 4.2 Add `@use` placement note to `create_custom_palette` description in `tools/descriptions.ts` +- [x] 4.3 Add `@use` placement note to `create_typography` description in `tools/descriptions.ts` +- [x] 4.4 Add `@use` placement note to `create_elevations` description in `tools/descriptions.ts` +- [x] 4.5 Add `@use` placement note to `create_theme` description in `tools/descriptions.ts` +- [x] 4.6 Add `@use` placement note to `create_component_theme` description in `tools/descriptions.ts` + +## 5. Tests and Validation + +- [x] 5.1 Add tests verifying inline comment presence in generated Sass output for palette, theme, and component-theme handlers +- [x] 5.2 Add tests verifying assembly note presence in handler response text for palette, theme, and component-theme handlers +- [x] 5.3 Verify CSS output mode does not include the inline comment or assembly note +- [x] 5.4 Run full test suite and fix any regressions diff --git a/openspec/specs/component-theming/spec.md b/openspec/specs/component-theming/spec.md index 90b6efd0..ec231d3f 100644 --- a/openspec/specs/component-theming/spec.md +++ b/openspec/specs/component-theming/spec.md @@ -6,7 +6,7 @@ Define component theming requirements, validation, and platform-specific output ### Requirement: Component theming requires platform -The `create_component_theme` tool requires a `platform` parameter and SHALL specify compound-component completeness rules to reduce incomplete outputs. +The `create_component_theme` tool requires a `platform` parameter and SHALL specify compound-component completeness rules to reduce incomplete outputs. Generated Sass SHALL include an inline `@use` placement comment, and the handler response text SHALL include an assembly note about `@use` top-of-file placement and deduplication. #### Scenario: Missing platform @@ -29,6 +29,16 @@ The `create_component_theme` tool requires a `platform` parameter and SHALL spec - **WHEN** a compound component is detected - **THEN** guidance includes a short canonical example (e.g., combo) demonstrating the full multi-call flow +#### Scenario: Inline placement comment in Sass + +- **WHEN** `create_component_theme` returns Sass output +- **THEN** the Sass code block SHALL contain a comment above the first `@use` about top-of-file placement and deduplication + +#### Scenario: Assembly note in response + +- **WHEN** `create_component_theme` returns Sass output +- **THEN** the handler response text SHALL include a placement note after the code block about `@use` top-of-file and deduplication + ### Requirement: Component token schemas are exposed The `get_component_design_tokens` tool SHALL use an instruction-oriented output format that varies based on whether the component is compound, simple, or a child sub-component. For compound components, the response SHALL include numbered steps, per-platform scope tables, related theme tables, token derivations, and guidance. For simple components, the response SHALL include the theme function, primary tokens, and the token table without compound sections. For child sub-components, the response SHALL include a relationship note followed by the full parent theme's tokens. diff --git a/openspec/specs/palette-generation/spec.md b/openspec/specs/palette-generation/spec.md index e0a065e9..8b98942f 100644 --- a/openspec/specs/palette-generation/spec.md +++ b/openspec/specs/palette-generation/spec.md @@ -6,7 +6,7 @@ Describe palette generation outputs for Sass and CSS, including warnings and pla ### Requirement: Palette generation returns Sass by default -The `create_palette` tool returns an MCP text response with a Sass code block when `output` is not specified. +The `create_palette` tool returns an MCP text response with a Sass code block when `output` is not specified. Generated Sass SHALL include an inline `@use` placement comment, and the handler response text SHALL include an assembly note about `@use` top-of-file placement and deduplication. #### Scenario: Required colors @@ -36,6 +36,16 @@ The `create_palette` tool returns an MCP text response with a Sass code block wh - **WHEN** `surface` conflicts with `variant` (light vs dark) - **THEN** the response includes a warning but still returns code +#### Scenario: Inline placement comment in Sass + +- **WHEN** `create_palette` returns Sass output +- **THEN** the Sass code block SHALL contain a comment above the first `@use` about top-of-file placement and deduplication + +#### Scenario: Assembly note in response + +- **WHEN** `create_palette` returns Sass output +- **THEN** the handler response text SHALL include a placement note after the code block about `@use` top-of-file and deduplication + ### Requirement: Palette CSS output uses Sass compilation The `create_palette` tool can emit CSS custom properties when `output: css` is requested. diff --git a/openspec/specs/sass-use-placement-guidance/spec.md b/openspec/specs/sass-use-placement-guidance/spec.md new file mode 100644 index 00000000..de2da0dc --- /dev/null +++ b/openspec/specs/sass-use-placement-guidance/spec.md @@ -0,0 +1,72 @@ +## Purpose + +Ensure all Sass-generating tools include inline `@use` placement comments and assembly notes to guide users on correct file assembly. + +## Requirements + +### Requirement: Generated Sass includes inline @use placement comment + +All Sass output that contains `@use` statements SHALL include a single-line comment above the first `@use` rule reminding that `@use` must appear at the top of the file and be deduplicated when combining multiple tool outputs. + +#### Scenario: Inline comment present in generated Sass + +- **GIVEN** a Sass-generating tool is called with default (Sass) output +- **WHEN** the tool returns a Sass code block +- **THEN** the code block SHALL contain a comment line above the first `@use` that mentions top-of-file placement and deduplication + +#### Scenario: CSS output is unaffected + +- **GIVEN** a tool is called with `output: "css"` +- **WHEN** the tool returns CSS custom properties +- **THEN** no `@use` placement comment SHALL be present + +### Requirement: Handler response text includes assembly note after Sass code + +Every Sass-generating handler SHALL append an assembly note after the Sass code block in its response text. The note SHALL instruct that `@use` rules must appear at the top of the `.scss` file and be deduplicated when combining outputs from multiple tools. + +#### Scenario: Assembly note present for palette handler + +- **GIVEN** `create_palette` is called with Sass output +- **WHEN** the handler returns its response text +- **THEN** the text after the Sass code fence SHALL contain a placement note mentioning `@use` top-of-file and deduplication + +#### Scenario: Assembly note present for component theme handler + +- **GIVEN** `create_component_theme` is called with Sass output +- **WHEN** the handler returns its response text +- **THEN** the text after the Sass code fence SHALL contain a placement note mentioning `@use` top-of-file and deduplication + +#### Scenario: Assembly note present for theme handler + +- **GIVEN** `create_theme` is called with Sass output +- **WHEN** the handler returns its response text +- **THEN** the text after the Sass code fence SHALL contain a placement note mentioning `@use` top-of-file and deduplication + +### Requirement: Sass-generating tool descriptions include @use guidance + +Each Sass-generating tool description SHALL include a brief note about `@use` placement and deduplication when combining outputs into a single file. The note SHALL be within the tool's `` or equivalent section. + +#### Scenario: create_palette description includes guidance + +- **WHEN** the `create_palette` tool description is inspected +- **THEN** it SHALL contain text about `@use` placement at the top and deduplication + +#### Scenario: create_theme description includes guidance + +- **WHEN** the `create_theme` tool description is inspected +- **THEN** it SHALL contain text about `@use` placement at the top and deduplication + +#### Scenario: create_component_theme description includes guidance + +- **WHEN** the `create_component_theme` tool description is inspected +- **THEN** it SHALL contain text about `@use` placement at the top and deduplication + +### Requirement: Assembly note text is centralised + +The assembly note string SHALL be defined once and imported by all handlers that emit Sass output, rather than duplicated across handler files. + +#### Scenario: Single source of truth for note text + +- **GIVEN** the assembly note wording is updated +- **WHEN** only one source definition is changed +- **THEN** all handlers SHALL reflect the updated wording diff --git a/openspec/specs/theme-generation/spec.md b/openspec/specs/theme-generation/spec.md index bccd9641..1d84a6f5 100644 --- a/openspec/specs/theme-generation/spec.md +++ b/openspec/specs/theme-generation/spec.md @@ -1,22 +1,26 @@ ## Purpose Document theme generation behavior and platform-specific output patterns. + ## Requirements + ### Requirement: Theme generation is platform-aware -The `create_theme` tool generates Sass output that follows platform-specific conventions. +The `create_theme` tool generates Sass output that follows platform-specific conventions. Generated Sass SHALL include an inline `@use` placement comment, and the handler response text SHALL include an assembly note about `@use` top-of-file placement and deduplication. #### Scenario: Angular output - **WHEN** `platform: angular` is provided - **THEN** the output includes `@include core()` - **AND** the output includes `@include theme(` with schema and palette +- **AND** the Sass code block includes an inline comment about `@use` placement #### Scenario: Web Components output - **WHEN** `platform: webcomponents` is provided - **THEN** the output uses `palette`, `typography`, and `elevations` mixins - **AND** spacing is included by default +- **AND** the Sass code block includes an inline comment about `@use` placement #### Scenario: React/Blazor output @@ -37,6 +41,11 @@ The `create_theme` tool generates Sass output that follows platform-specific con - **THEN** the response includes a hint to specify `platform` - **AND** the hint SHALL mention `"generic"` as a valid option for platform-agnostic output +#### Scenario: Assembly note in response + +- **WHEN** `create_theme` returns Sass output +- **THEN** the handler response text SHALL include a placement note after the code block about `@use` top-of-file and deduplication + ### Requirement: Theme includes optional sections by flags The `create_theme` tool includes typography, elevations, and spacing by default and can exclude them. @@ -63,4 +72,3 @@ The `create_theme` tool includes typography, elevations, and spacing by default - **WHEN** provided colors are unsuitable for the chosen variant - **THEN** the response includes a warning message - **AND** the response still returns generated code - diff --git a/packages/mcp/src/__tests__/knowledge/sass-api.test.ts b/packages/mcp/src/__tests__/knowledge/sass-api.test.ts index 4f8a01c2..e367e707 100644 --- a/packages/mcp/src/__tests__/knowledge/sass-api.test.ts +++ b/packages/mcp/src/__tests__/knowledge/sass-api.test.ts @@ -260,17 +260,17 @@ describe("VARIABLE_PATTERNS", () => { describe("generateUseStatement", () => { it("generates Angular use statement with double quotes", () => { const result = generateUseStatement("angular"); - expect(result).toBe('@use "igniteui-angular/theming" as *;'); + expect(result).toContain('@use "igniteui-angular/theming" as *;'); }); it("generates Web Components use statement with single quotes", () => { const result = generateUseStatement("webcomponents"); - expect(result).toBe("@use 'igniteui-theming' as *;"); + expect(result).toContain("@use 'igniteui-theming' as *;"); }); it("generates default use statement when platform is undefined", () => { const result = generateUseStatement(); - expect(result).toBe("@use 'igniteui-theming' as *;"); + expect(result).toContain("@use 'igniteui-theming' as *;"); }); }); diff --git a/packages/mcp/src/__tests__/tools/handlers/handlers.test.ts b/packages/mcp/src/__tests__/tools/handlers/handlers.test.ts index 1c21fe7a..ea8d22d9 100644 --- a/packages/mcp/src/__tests__/tools/handlers/handlers.test.ts +++ b/packages/mcp/src/__tests__/tools/handlers/handlers.test.ts @@ -19,6 +19,10 @@ import { import { handleCreatePalette } from "../../../tools/handlers/palette.js"; import { handleCreateTheme } from "../../../tools/handlers/theme.js"; import { handleCreateTypography } from "../../../tools/handlers/typography.js"; +import { + SASS_USE_ASSEMBLY_NOTE, + SASS_USE_INLINE_COMMENT, +} from "../../../utils/sass.js"; describe("handleCreatePalette", () => { it("returns MCP response format", async () => { @@ -95,6 +99,28 @@ describe("handleCreatePalette", () => { const text = result.content[0].text; expect(text).toContain("$my-brand-palette"); }); + + it("includes inline @use placement comment in generated Sass", async () => { + const result = await handleCreatePalette({ + primary: "#2ab759", + secondary: "#f7bd32", + surface: "white", + }); + + const text = result.content[0].text; + expect(text).toContain(SASS_USE_INLINE_COMMENT); + }); + + it("includes assembly note after Sass code block", async () => { + const result = await handleCreatePalette({ + primary: "#2ab759", + secondary: "#f7bd32", + surface: "white", + }); + + const text = result.content[0].text; + expect(text).toContain(SASS_USE_ASSEMBLY_NOTE); + }); }); describe("handleCreateTheme", () => { @@ -195,6 +221,20 @@ describe("handleCreateTheme", () => { const text = result.content[0].text; expect(text).toContain("Platform: Ignite UI for Blazor"); }); + + it("includes inline @use placement comment in generated Sass", async () => { + const result = await handleCreateTheme(baseThemeParams); + + const text = result.content[0].text; + expect(text).toContain(SASS_USE_INLINE_COMMENT); + }); + + it("includes assembly note after Sass code block", async () => { + const result = await handleCreateTheme(baseThemeParams); + + const text = result.content[0].text; + expect(text).toContain(SASS_USE_ASSEMBLY_NOTE); + }); }); describe("handleCreateTypography", () => { @@ -757,4 +797,39 @@ describe("handleCreateComponentTheme", () => { // textarea is a same-element alias, should use its own selector expect(text).toContain(".igx-input-group--textarea-group"); }); + + it("includes inline @use placement comment in generated Sass", async () => { + const result = await handleCreateComponentTheme({ + platform: "webcomponents", + component: "avatar", + tokens: { background: "#ff5722" }, + }); + + const text = result.content[0].text; + expect(text).toContain(SASS_USE_INLINE_COMMENT); + }); + + it("includes assembly note after Sass code block", async () => { + const result = await handleCreateComponentTheme({ + platform: "webcomponents", + component: "avatar", + tokens: { background: "#ff5722" }, + }); + + const text = result.content[0].text; + expect(text).toContain(SASS_USE_ASSEMBLY_NOTE); + }); + + it("CSS output does not include inline comment or assembly note", async () => { + const result = await handleCreateComponentTheme({ + platform: "webcomponents", + component: "avatar", + output: "css", + tokens: { background: "#ff5722" }, + }); + + const text = result.content[0].text; + expect(text).not.toContain(SASS_USE_INLINE_COMMENT); + expect(text).not.toContain(SASS_USE_ASSEMBLY_NOTE); + }); }); diff --git a/packages/mcp/src/__tests__/utils/sass.test.ts b/packages/mcp/src/__tests__/utils/sass.test.ts index 33d891d3..705f04da 100644 --- a/packages/mcp/src/__tests__/utils/sass.test.ts +++ b/packages/mcp/src/__tests__/utils/sass.test.ts @@ -94,27 +94,35 @@ describe("quoteFontFamily", () => { describe("generateUseStatement", () => { it("generates Angular import for angular platform", () => { const result = generateUseStatement("angular"); - expect(result).toBe('@use "igniteui-angular/theming" as *;'); + expect(result).toContain('@use "igniteui-angular/theming" as *;'); }); it("generates licensed Angular import when licensed is true", () => { const result = generateUseStatement("angular", true); - expect(result).toBe('@use "@infragistics/igniteui-angular/theming" as *;'); + expect(result).toContain( + '@use "@infragistics/igniteui-angular/theming" as *;', + ); }); it("generates generic import for webcomponents platform", () => { const result = generateUseStatement("webcomponents"); - expect(result).toBe("@use 'igniteui-theming' as *;"); + expect(result).toContain("@use 'igniteui-theming' as *;"); }); it("ignores licensed flag for webcomponents (always free)", () => { const result = generateUseStatement("webcomponents", true); - expect(result).toBe("@use 'igniteui-theming' as *;"); + expect(result).toContain("@use 'igniteui-theming' as *;"); }); it("generates generic import when platform is undefined", () => { const result = generateUseStatement(); - expect(result).toBe("@use 'igniteui-theming' as *;"); + expect(result).toContain("@use 'igniteui-theming' as *;"); + }); + + it("prepends inline placement comment", () => { + const result = generateUseStatement(); + expect(result).toContain("// NOTE: @use rules must be at the top"); + expect(result.indexOf("// NOTE")).toBeLessThan(result.indexOf("@use")); }); }); diff --git a/packages/mcp/src/tools/descriptions.ts b/packages/mcp/src/tools/descriptions.ts index 671dad9f..ab77c844 100644 --- a/packages/mcp/src/tools/descriptions.ts +++ b/packages/mcp/src/tools/descriptions.ts @@ -47,6 +47,12 @@ export const FRAGMENTS = { MONOCHROMATIC_RULE: "MONOCHROMATIC REQUIREMENT: All shades in a color group (e.g., primary) must be the SAME HUE. Shades are lighter/darker versions of ONE color, NOT different colors. Example: primary shades should all be blue (#E3F2FD → #0D47A1), not blue→green→purple. Vary only lightness and saturation, keep hue constant (±30° tolerance).", + /** Sass @use placement guidance for tools that generate Sass output */ + SASS_FILE_PLACEMENT: `SASS FILE PLACEMENT: + - When combining Sass output from multiple tools into one file, all @use rules + must appear at the top before any other statements. Deduplicate @use lines + that share the same module path.`, + /** Resource scheme */ RESOURCE_SCHEME: "theming://", } as const; @@ -155,6 +161,8 @@ export const TOOL_DESCRIPTIONS = { The palette() function always generates 50=lightest to 900=darkest. - Only gray shades behave differently based on variant (for text contrast). - DO NOT manually invert primary/secondary colors for dark themes. + + ${FRAGMENTS.SASS_FILE_PLACEMENT} @@ -302,6 +310,8 @@ export const TOOL_DESCRIPTIONS = { MIXING MODES: - You can use "shades" mode for some colors and "explicit" for others - Example: explicit primary, shades-based secondary and surface + + ${FRAGMENTS.SASS_FILE_PLACEMENT} @@ -415,6 +425,8 @@ export const TOOL_DESCRIPTIONS = { - Quote font names that contain spaces: '"Segoe UI"' not 'Segoe UI' - Design system affects: font sizes, line heights, letter spacing, font weights - Type styles include: h1-h6, subtitle-1/2, body-1/2, button, caption, overline + + ${FRAGMENTS.SASS_FILE_PLACEMENT} @@ -467,6 +479,8 @@ export const TOOL_DESCRIPTIONS = { - "indigo" preset: Infragistics Indigo shadow specifications - Elevation 0 = no shadow, elevation 24 = maximum shadow depth - Components use elevation() function to apply specific levels + + ${FRAGMENTS.SASS_FILE_PLACEMENT} @@ -529,6 +543,8 @@ export const TOOL_DESCRIPTIONS = { - Web Components: Uses igniteui-theming directly with palette(), typography(), elevations() mixins - React: Uses igniteui-theming directly (same as Web Components), common with Vite/Next.js - Blazor: Uses igniteui-theming for Sass compilation, theme CSS referenced in Blazor components + + ${FRAGMENTS.SASS_FILE_PLACEMENT} @@ -879,6 +895,8 @@ export const TOOL_DESCRIPTIONS = { - Use the related themes list from get_component_design_tokens to drive the sequence - All related themes should use the compound component's selector as the wrapper - Follow token derivation hints to set child token values consistently + + ${FRAGMENTS.SASS_FILE_PLACEMENT} diff --git a/packages/mcp/src/tools/handlers/component-theme.ts b/packages/mcp/src/tools/handlers/component-theme.ts index 9d8d7a1c..d1c34aa8 100644 --- a/packages/mcp/src/tools/handlers/component-theme.ts +++ b/packages/mcp/src/tools/handlers/component-theme.ts @@ -17,6 +17,7 @@ import { validateTokens, } from "../../knowledge/index.js"; import { PLATFORM_METADATA } from "../../knowledge/platforms/index.js"; +import { SASS_USE_ASSEMBLY_NOTE } from "../../utils/sass.js"; import type { CreateComponentThemeParams } from "../schemas.js"; export async function handleCreateComponentTheme( @@ -314,6 +315,7 @@ Use \`get_component_design_tokens\` to see all tokens with descriptions.`, responseParts.push("```scss"); responseParts.push(result.code.trimEnd()); responseParts.push("```"); + responseParts.push(SASS_USE_ASSEMBLY_NOTE); // Add usage hint responseParts.push(""); diff --git a/packages/mcp/src/tools/handlers/custom-palette.ts b/packages/mcp/src/tools/handlers/custom-palette.ts index 300beee7..fc49181d 100644 --- a/packages/mcp/src/tools/handlers/custom-palette.ts +++ b/packages/mcp/src/tools/handlers/custom-palette.ts @@ -15,6 +15,7 @@ import { generateCustomPaletteCode, generateHeader, generateUseStatement, + SASS_USE_ASSEMBLY_NOTE, toVariableName, } from "../../utils/sass.js"; import type { @@ -308,6 +309,7 @@ ${paletteLines.join("\n")} responseParts.push("```scss"); responseParts.push(code.trimEnd()); responseParts.push("```"); + responseParts.push(SASS_USE_ASSEMBLY_NOTE); return { content: [ diff --git a/packages/mcp/src/tools/handlers/elevations.ts b/packages/mcp/src/tools/handlers/elevations.ts index 31c1db2e..d80799d9 100644 --- a/packages/mcp/src/tools/handlers/elevations.ts +++ b/packages/mcp/src/tools/handlers/elevations.ts @@ -4,6 +4,7 @@ import { generateElevations } from "../../generators/sass.js"; import { PLATFORM_METADATA } from "../../knowledge/platforms/index.js"; +import { SASS_USE_ASSEMBLY_NOTE } from "../../utils/sass.js"; import type { CreateElevationsParams } from "../schemas.js"; export function handleCreateElevations(params: CreateElevationsParams) { @@ -30,6 +31,7 @@ export function handleCreateElevations(params: CreateElevationsParams) { responseParts.push("```scss"); responseParts.push(result.code.trimEnd()); responseParts.push("```"); + responseParts.push(SASS_USE_ASSEMBLY_NOTE); return { content: [ diff --git a/packages/mcp/src/tools/handlers/palette.ts b/packages/mcp/src/tools/handlers/palette.ts index 4acac9d3..49f3ce3e 100644 --- a/packages/mcp/src/tools/handlers/palette.ts +++ b/packages/mcp/src/tools/handlers/palette.ts @@ -5,6 +5,7 @@ import { formatCssOutput, generatePaletteCss } from "../../generators/css.js"; import { generatePalette } from "../../generators/sass.js"; import { PLATFORM_METADATA } from "../../knowledge/platforms/index.js"; +import { SASS_USE_ASSEMBLY_NOTE } from "../../utils/sass.js"; import { formatValidationResult, generateWarningComments, @@ -156,6 +157,7 @@ function handleSassOutput( responseParts.push("```scss"); responseParts.push(finalCode.trimEnd()); responseParts.push("```"); + responseParts.push(SASS_USE_ASSEMBLY_NOTE); return { content: [ diff --git a/packages/mcp/src/tools/handlers/theme.ts b/packages/mcp/src/tools/handlers/theme.ts index f397241d..47b7e8d2 100644 --- a/packages/mcp/src/tools/handlers/theme.ts +++ b/packages/mcp/src/tools/handlers/theme.ts @@ -3,6 +3,7 @@ */ import { generateTheme } from "../../generators/sass.js"; +import { SASS_USE_ASSEMBLY_NOTE } from "../../utils/sass.js"; import { analyzeThemeColorsForPalette, formatPaletteSuitabilityWarnings, @@ -126,6 +127,7 @@ export async function handleCreateTheme(params: CreateThemeParams) { responseParts.push("```scss"); responseParts.push(finalCode.trimEnd()); responseParts.push("```"); + responseParts.push(SASS_USE_ASSEMBLY_NOTE); return { content: [ diff --git a/packages/mcp/src/tools/handlers/typography.ts b/packages/mcp/src/tools/handlers/typography.ts index a06ee601..7a75f23e 100644 --- a/packages/mcp/src/tools/handlers/typography.ts +++ b/packages/mcp/src/tools/handlers/typography.ts @@ -4,6 +4,7 @@ import { generateTypography } from "../../generators/sass.js"; import { PLATFORM_METADATA } from "../../knowledge/platforms/index.js"; +import { SASS_USE_ASSEMBLY_NOTE } from "../../utils/sass.js"; import type { CreateTypographyParams } from "../schemas.js"; export function handleCreateTypography(params: CreateTypographyParams) { @@ -32,6 +33,7 @@ export function handleCreateTypography(params: CreateTypographyParams) { responseParts.push("```scss"); responseParts.push(result.code.trimEnd()); responseParts.push("```"); + responseParts.push(SASS_USE_ASSEMBLY_NOTE); return { content: [ diff --git a/packages/mcp/src/utils/sass.ts b/packages/mcp/src/utils/sass.ts index 1b09fb19..92c98785 100644 --- a/packages/mcp/src/utils/sass.ts +++ b/packages/mcp/src/utils/sass.ts @@ -95,13 +95,21 @@ export function generateHeader(description: string): string { // standalone tools and platform-specific theme generators. // ============================================================================ +/** Inline comment prepended above @use lines in generated Sass. */ +export const SASS_USE_INLINE_COMMENT = + "// NOTE: @use rules must be at the top of the file. Deduplicate when combining multiple outputs."; + +/** Markdown assembly note appended after Sass code blocks in handler responses. */ +export const SASS_USE_ASSEMBLY_NOTE = + "\n> **File placement:** `@use` rules must appear at the very top of the `.scss` file, before any other statements. When combining outputs from multiple tools, keep only one `@use` per module path."; + /** * Generate the Sass @use statement for the theming library. * Uses platform-specific import paths when platform is specified. * * @param platform - Target platform (angular or webcomponents) * @param licensed - Whether to use the licensed @infragistics package (Angular only, defaults to false) - * @returns The appropriate @use statement for the platform + * @returns The inline placement comment + @use statement for the platform */ export function generateUseStatement( platform?: Platform, @@ -111,10 +119,10 @@ export function generateUseStatement( const packagePath = licensed ? "@infragistics/igniteui-angular" : "igniteui-angular"; - return `@use "${packagePath}/theming" as *;`; + return `${SASS_USE_INLINE_COMMENT}\n@use "${packagePath}/theming" as *;`; } // Web Components, React, Blazor, or unspecified (always use igniteui-theming - it's free) - return "@use 'igniteui-theming' as *;"; + return `${SASS_USE_INLINE_COMMENT}\n@use 'igniteui-theming' as *;`; } /** From 14b4793d5eb236624283ce2b3f3ed33ba55b02ca Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Fri, 3 Apr 2026 13:23:27 +0300 Subject: [PATCH 6/9] chore: initialize taskplane tasks --- taskplane-tasks/CONTEXT.md | 31 ++++++ .../EXAMPLE-001-hello-world/PROMPT.md | 98 +++++++++++++++++++ .../EXAMPLE-001-hello-world/STATUS.md | 73 ++++++++++++++ .../EXAMPLE-002-parallel-smoke/PROMPT.md | 97 ++++++++++++++++++ .../EXAMPLE-002-parallel-smoke/STATUS.md | 73 ++++++++++++++ 5 files changed, 372 insertions(+) create mode 100644 taskplane-tasks/CONTEXT.md create mode 100644 taskplane-tasks/EXAMPLE-001-hello-world/PROMPT.md create mode 100644 taskplane-tasks/EXAMPLE-001-hello-world/STATUS.md create mode 100644 taskplane-tasks/EXAMPLE-002-parallel-smoke/PROMPT.md create mode 100644 taskplane-tasks/EXAMPLE-002-parallel-smoke/STATUS.md diff --git a/taskplane-tasks/CONTEXT.md b/taskplane-tasks/CONTEXT.md new file mode 100644 index 00000000..f950ac0e --- /dev/null +++ b/taskplane-tasks/CONTEXT.md @@ -0,0 +1,31 @@ +# General — Context + +**Last Updated:** 2026-04-03 +**Status:** Active +**Next Task ID:** IGT-002 + +--- + +## Current State + +This is the default task area for igniteui-theming. Tasks that don't belong +to a specific domain area are created here. + +Taskplane is configured and ready for task execution. Use `/task` for single +tasks or `/orch all` for parallel batch execution. + +--- + +## Key Files + +| Category | Path | +|----------|------| +| Tasks | `taskplane-tasks/` | +| Config | `.pi/task-runner.yaml` | +| Config | `.pi/task-orchestrator.yaml` | + +--- + +## Technical Debt / Future Work + +_Items discovered during task execution are logged here by agents._ diff --git a/taskplane-tasks/EXAMPLE-001-hello-world/PROMPT.md b/taskplane-tasks/EXAMPLE-001-hello-world/PROMPT.md new file mode 100644 index 00000000..24f4ec3a --- /dev/null +++ b/taskplane-tasks/EXAMPLE-001-hello-world/PROMPT.md @@ -0,0 +1,98 @@ +# Task: EXAMPLE-001 — Hello World + +**Created:** 2026-04-03 +**Size:** S + +## Review Level: 0 (None) + +**Assessment:** Trivial single-file task to verify Taskplane is working. +**Score:** 0/8 — Blast radius: 0, Pattern novelty: 0, Security: 0, Reversibility: 0 + +## Canonical Task Folder + +``` +taskplane-tasks/EXAMPLE-001-hello-world/ +├── PROMPT.md ← This file (immutable above --- divider) +├── STATUS.md ← Execution state (worker updates this) +├── .reviews/ ← Reviewer output (task-runner creates this) +└── .DONE ← Created when complete +``` + +## Mission + +Create a simple `hello-taskplane.md` file in the project root to verify that +Taskplane task execution is working correctly. This is a smoke test — if the +worker can read this prompt, create the file, checkpoint progress, and mark the +task done, the installation is healthy. + +## Expected File Content + +`hello-taskplane.md` should include: + +- A title line (for example: `# Hello from Taskplane`) +- A line containing the task ID: `EXAMPLE-001` +- A line containing today's date + +## Dependencies + +- **None** + +## Context to Read First + +_No additional context needed._ + +## Environment + +- **Workspace:** Project root +- **Services required:** None + +## File Scope + +- `hello-taskplane.md` + +## Steps + +### Step 0: Preflight + +- [ ] Verify this PROMPT.md is readable +- [ ] Verify STATUS.md exists in the same folder + +### Step 1: Create Hello File + +- [ ] Create `hello-taskplane.md` in the project root +- [ ] Add a title plus lines containing today's date and task ID `EXAMPLE-001` + +### Step 2: Verification + +- [ ] Verify `hello-taskplane.md` exists and matches the expected content + +### Step 3: Delivery + + + +## Documentation Requirements + +**Must Update:** None +**Check If Affected:** None + +## Completion Criteria + +- [ ] `hello-taskplane.md` exists in the project root +- [ ] `hello-taskplane.md` includes a title, task ID (`EXAMPLE-001`), and current date + +## Git Commit Convention + +- **Implementation:** `feat(EXAMPLE-001): description` +- **Checkpoints:** `checkpoint: EXAMPLE-001 description` + +## Do NOT + +- Modify any existing project files +- Create files outside the project root +- Over-engineer this — it's a smoke test + +--- + +## Amendments (Added During Execution) + + diff --git a/taskplane-tasks/EXAMPLE-001-hello-world/STATUS.md b/taskplane-tasks/EXAMPLE-001-hello-world/STATUS.md new file mode 100644 index 00000000..51200c2a --- /dev/null +++ b/taskplane-tasks/EXAMPLE-001-hello-world/STATUS.md @@ -0,0 +1,73 @@ +# EXAMPLE-001: Hello World — Status + +**Current Step:** Not Started +**Status:** 🔵 Ready for Execution +**Last Updated:** 2026-04-03 +**Review Level:** 0 +**Review Counter:** 0 +**Iteration:** 0 +**Size:** S + +--- + +### Step 0: Preflight +**Status:** ⬜ Not Started + +- [ ] Verify PROMPT.md is readable +- [ ] Verify STATUS.md exists + +--- + +### Step 1: Create Hello File +**Status:** ⬜ Not Started + +- [ ] Create `hello-taskplane.md` in project root +- [ ] Add title, date, and task ID (EXAMPLE-001) + +--- + +### Step 2: Verification +**Status:** ⬜ Not Started + +- [ ] Verify file exists and matches expected content + +--- + +### Step 3: Delivery +**Status:** ⬜ Not Started + + + +--- + +## Reviews + +| # | Type | Step | Verdict | File | +|---|------|------|---------|------| + +--- + +## Discoveries + +| Discovery | Disposition | Location | +|-----------|-------------|----------| + +--- + +## Execution Log + +| Timestamp | Action | Outcome | +|-----------|--------|---------| +| 2026-04-03 | Task staged | PROMPT.md and STATUS.md created | + +--- + +## Blockers + +*None* + +--- + +## Notes + +*This is an example task created by `taskplane init`. Delete it after verifying your setup works.* diff --git a/taskplane-tasks/EXAMPLE-002-parallel-smoke/PROMPT.md b/taskplane-tasks/EXAMPLE-002-parallel-smoke/PROMPT.md new file mode 100644 index 00000000..32d8526a --- /dev/null +++ b/taskplane-tasks/EXAMPLE-002-parallel-smoke/PROMPT.md @@ -0,0 +1,97 @@ +# Task: EXAMPLE-002 — Parallel Smoke + +**Created:** 2026-04-03 +**Size:** S + +## Review Level: 0 (None) + +**Assessment:** Trivial parallel-safe smoke task to demonstrate orchestrator lanes. +**Score:** 0/8 — Blast radius: 0, Pattern novelty: 0, Security: 0, Reversibility: 0 + +## Canonical Task Folder + +``` +taskplane-tasks/EXAMPLE-002-parallel-smoke/ +├── PROMPT.md ← This file (immutable above --- divider) +├── STATUS.md ← Execution state (worker updates this) +├── .reviews/ ← Reviewer output (task-runner creates this) +└── .DONE ← Created when complete +``` + +## Mission + +Create a simple `hello-taskplane-2.md` file in the project root. This task is +intentionally independent from EXAMPLE-001 so both can run in parallel when +using `/orch`. + +## Expected File Content + +`hello-taskplane-2.md` should include: + +- A title line (for example: `# Parallel Hello from Taskplane`) +- A line containing the task ID: `EXAMPLE-002` +- A short note that this task is parallel-safe + +## Dependencies + +- **None** + +## Context to Read First + +_No additional context needed._ + +## Environment + +- **Workspace:** Project root +- **Services required:** None + +## File Scope + +- `hello-taskplane-2.md` + +## Steps + +### Step 0: Preflight + +- [ ] Verify this PROMPT.md is readable +- [ ] Verify STATUS.md exists in the same folder + +### Step 1: Create Parallel Hello File + +- [ ] Create `hello-taskplane-2.md` in the project root +- [ ] Add title plus lines containing task ID `EXAMPLE-002` and a parallel-safe note + +### Step 2: Verification + +- [ ] Verify `hello-taskplane-2.md` exists and matches the expected content + +### Step 3: Delivery + + + +## Documentation Requirements + +**Must Update:** None +**Check If Affected:** None + +## Completion Criteria + +- [ ] `hello-taskplane-2.md` exists in the project root +- [ ] `hello-taskplane-2.md` includes a title, task ID (`EXAMPLE-002`), and a parallel-safe note + +## Git Commit Convention + +- **Implementation:** `feat(EXAMPLE-002): description` +- **Checkpoints:** `checkpoint: EXAMPLE-002 description` + +## Do NOT + +- Modify any existing project files +- Create files outside the project root +- Add dependencies between EXAMPLE-001 and EXAMPLE-002 + +--- + +## Amendments (Added During Execution) + + diff --git a/taskplane-tasks/EXAMPLE-002-parallel-smoke/STATUS.md b/taskplane-tasks/EXAMPLE-002-parallel-smoke/STATUS.md new file mode 100644 index 00000000..a6602d0a --- /dev/null +++ b/taskplane-tasks/EXAMPLE-002-parallel-smoke/STATUS.md @@ -0,0 +1,73 @@ +# EXAMPLE-002: Parallel Smoke — Status + +**Current Step:** Not Started +**Status:** 🔵 Ready for Execution +**Last Updated:** 2026-04-03 +**Review Level:** 0 +**Review Counter:** 0 +**Iteration:** 0 +**Size:** S + +--- + +### Step 0: Preflight +**Status:** ⬜ Not Started + +- [ ] Verify PROMPT.md is readable +- [ ] Verify STATUS.md exists + +--- + +### Step 1: Create Parallel Hello File +**Status:** ⬜ Not Started + +- [ ] Create `hello-taskplane-2.md` in project root +- [ ] Add title, task ID (EXAMPLE-002), and parallel-safe note + +--- + +### Step 2: Verification +**Status:** ⬜ Not Started + +- [ ] Verify file exists and matches expected content + +--- + +### Step 3: Delivery +**Status:** ⬜ Not Started + + + +--- + +## Reviews + +| # | Type | Step | Verdict | File | +|---|------|------|---------|------| + +--- + +## Discoveries + +| Discovery | Disposition | Location | +|-----------|-------------|----------| + +--- + +## Execution Log + +| Timestamp | Action | Outcome | +|-----------|--------|---------| +| 2026-04-03 | Task staged | PROMPT.md and STATUS.md created | + +--- + +## Blockers + +*None* + +--- + +## Notes + +*This is an example task created by `taskplane init` to demonstrate orchestrator-first onboarding.* From d543fdeeb325702b97e8bd13ba63c7667965e080 Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Fri, 3 Apr 2026 14:10:23 +0300 Subject: [PATCH 7/9] chore: stage task files for orchestrator wave 1 (IGT-002-mcp-composed-compound-flag) --- .../PROMPT.md | 165 ++++++++++++++++++ .../STATUS.md | 113 ++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 taskplane-tasks/IGT-002-mcp-composed-compound-flag/PROMPT.md create mode 100644 taskplane-tasks/IGT-002-mcp-composed-compound-flag/STATUS.md diff --git a/taskplane-tasks/IGT-002-mcp-composed-compound-flag/PROMPT.md b/taskplane-tasks/IGT-002-mcp-composed-compound-flag/PROMPT.md new file mode 100644 index 00000000..45be64d2 --- /dev/null +++ b/taskplane-tasks/IGT-002-mcp-composed-compound-flag/PROMPT.md @@ -0,0 +1,165 @@ +# Task: IGT-002 - MCP Composed Compound Flag + +**Created:** 2026-04-03 +**Size:** S + +## Review Level: 1 (Plan Only) + +**Assessment:** Touches handler response logic and tool descriptions across the MCP package — pattern is new (composed vs standard compound distinction) but scoped to a single package. +**Score:** 2/8 — Blast radius: 1, Pattern novelty: 1, Security: 0, Reversibility: 0 + +## Canonical Task Folder + +``` +taskplane-tasks/IGT-002-mcp-composed-compound-flag/ +├── PROMPT.md ← This file (immutable above --- divider) +├── STATUS.md ← Execution state (worker updates this) +├── .reviews/ ← Reviewer output (created by the orchestrator runtime) +└── .DONE ← Created when complete +``` + +## Mission + +Add a `composed` boolean flag to the `CompoundInfo` interface in the MCP server's +component metadata. When `composed: true`, the `get_design_tokens` tool must tell +LLMs to **only set the three primary tokens** (`background`, `foreground`, +`accent-color`) — child component themes are auto-derived internally by the Sass +layer. This replaces the current "create themes for each related theme" guidance +for composed compounds. + +The grid is the first component to use this mechanism. Its Sass layer already +derives all child themes from the three primary tokens (see `_grid-theme.scss` +Theme Builder Logic section). The MCP layer needs to match. + +**Why:** Without this change, LLMs calling `get_design_tokens` for the grid +receive contradictory guidance — "here are 3 primary tokens that derive +everything" AND "create separate themes for 14 child components." The composed +flag resolves this by making the instruction unambiguous. + +## Dependencies + +- **None** + +## Context to Read First + +**Tier 3 (load only if needed):** +- `packages/theming/sass/themes/components/grid/_grid-theme.scss` — Reference for PRIMARY tokens and Theme Builder Logic that auto-derives child tokens + +## Environment + +- **Workspace:** `packages/mcp/` +- **Services required:** None + +## File Scope + +- `packages/mcp/src/knowledge/component-metadata.ts` +- `packages/mcp/src/tools/handlers/component-tokens.ts` +- `packages/mcp/src/tools/descriptions.ts` +- `packages/mcp/src/__tests__/tools/handlers/component-tokens.test.ts` + +## Steps + +### Step 0: Preflight + +- [ ] Verify `packages/mcp/src/knowledge/component-metadata.ts` exists and contains `CompoundInfo` interface +- [ ] Verify `GRID_COMPOUND_INFO` exists with 14 `relatedThemes` entries +- [ ] Verify `packages/mcp/src/tools/handlers/component-tokens.ts` contains the compound component handler logic +- [ ] Verify `packages/mcp/src/tools/descriptions.ts` contains `get_design_tokens` and `create_component_theme` descriptions + +### Step 1: Add `composed` flag to CompoundInfo and set on grid + +- [ ] Add `composed?: boolean` field to `CompoundInfo` interface in `component-metadata.ts` with JSDoc explaining its purpose +- [ ] Set `composed: true` on `GRID_COMPOUND_INFO` (without disturbing the 14-item `relatedThemes` array) +- [ ] Update `GRID_COMPOUND_INFO.guidance` text to reflect composed behavior — tell LLMs to set only primary tokens and not create separate child themes +- [ ] Run targeted tests: `npx vitest run packages/mcp/src/__tests__/tools/handlers/component-tokens.test.ts` + +**Artifacts:** +- `packages/mcp/src/knowledge/component-metadata.ts` (modified) + +### Step 2: Update handler guidance for composed compounds + +- [ ] In `component-tokens.ts`, add a conditional branch inside the `if (compoundInfo)` block: when `compoundInfo.composed === true`, emit composed-specific guidance instead of the standard compound guidance +- [ ] Composed guidance must: label as "Composed Compound Component", state that only background/foreground/accent-color are needed, explicitly say "Do NOT create separate themes", and list `relatedThemes` as "Internally themed children (auto-derived)" for reference only +- [ ] Standard compound guidance (the existing code) must remain unchanged for non-composed compounds (e.g., combo, select, date-picker) +- [ ] Run targeted tests: `npx vitest run packages/mcp/src/__tests__/tools/handlers/component-tokens.test.ts` + +**Artifacts:** +- `packages/mcp/src/tools/handlers/component-tokens.ts` (modified) + +### Step 3: Update tool descriptions for composed distinction + +- [ ] In `descriptions.ts`, update the `get_design_tokens` COMPOUND COMPONENTS section to distinguish "Standard compounds" (LLM creates separate child themes) from "Composed compounds" (LLM only sets 3 primary tokens, child themes auto-derive) +- [ ] In `descriptions.ts`, update the `create_component_theme` COMPOUND COMPLETENESS section to document composed compound behavior (do NOT generate separate child themes) +- [ ] Run targeted tests: `npx vitest run packages/mcp` + +**Artifacts:** +- `packages/mcp/src/tools/descriptions.ts` (modified) + +### Step 4: Update tests + +- [ ] Update the grid compound test in `component-tokens.test.ts` to assert composed output: expects "Composed Compound Component", "Do NOT create separate themes", "Internally themed children (auto-derived)", and does NOT contain "Scope all related themes under" +- [ ] Verify existing non-composed compound tests still pass (date-range-picker, time-picker, etc.) +- [ ] Run targeted tests: `npx vitest run packages/mcp/src/__tests__/tools/handlers/component-tokens.test.ts` + +**Artifacts:** +- `packages/mcp/src/__tests__/tools/handlers/component-tokens.test.ts` (modified) + +### Step 5: Testing & Verification + +> ZERO test failures allowed. This step runs the FULL test suite as a quality gate. + +- [ ] Run FULL test suite: `npm test` +- [ ] Fix all failures +- [ ] Build passes: `npm run build` + +### Step 6: Documentation & Delivery + +- [ ] "Must Update" docs modified +- [ ] "Check If Affected" docs reviewed +- [ ] Discoveries logged in STATUS.md + +## Documentation Requirements + +**Must Update:** +- None — the code changes are self-documenting via JSDoc and the updated tool descriptions + +**Check If Affected:** +- `packages/mcp/README.md` — update if it documents compound component behavior + +## Completion Criteria + +- [ ] `CompoundInfo` interface has `composed?: boolean` field +- [ ] `GRID_COMPOUND_INFO` has `composed: true` +- [ ] `get_design_tokens` for grid returns composed guidance (not standard compound scoping) +- [ ] `get_design_tokens` for non-composed compounds (combo, select, etc.) returns standard guidance unchanged +- [ ] Tool descriptions in `descriptions.ts` document the composed vs standard distinction +- [ ] All tests passing (667+ tests) +- [ ] Build passes + +## Git Commit Convention + +Commits happen at **step boundaries** (not after every checkbox). All commits +for this task MUST include the task ID for traceability: + +- **Step completion:** `feat(IGT-002): complete Step N — description` +- **Bug fixes:** `fix(IGT-002): description` +- **Tests:** `test(IGT-002): description` +- **Hydration:** `hydrate: IGT-002 expand Step N checkboxes` + +## Do NOT + +- Modify `_grid-theme.scss` or any Sass files — the Sass derivation layer is already complete +- Create a multi-phase task covering card → combo rollout — scope is MCP layer only for now +- Emit "create themes for each related theme" guidance when `compoundInfo.composed` is `true` +- Remove or modify `relatedThemes` entries on `GRID_COMPOUND_INFO` — they stay as informational metadata +- Add `composed: true` to any compound other than grid (future rollout is out of scope) + +--- + +## Amendments (Added During Execution) + + diff --git a/taskplane-tasks/IGT-002-mcp-composed-compound-flag/STATUS.md b/taskplane-tasks/IGT-002-mcp-composed-compound-flag/STATUS.md new file mode 100644 index 00000000..09aec2a5 --- /dev/null +++ b/taskplane-tasks/IGT-002-mcp-composed-compound-flag/STATUS.md @@ -0,0 +1,113 @@ +# IGT-002: MCP Composed Compound Flag — Status + +**Current Step:** Not Started +**Status:** 🔵 Ready for Execution +**Last Updated:** 2026-04-03 +**Review Level:** 1 +**Review Counter:** 0 +**Iteration:** 0 +**Size:** S + +> **Hydration:** Checkboxes represent meaningful outcomes, not individual code +> changes. Workers expand steps when runtime discoveries warrant it — aim for +> 2-5 outcome-level items per step, not exhaustive implementation scripts. + +--- + +### Step 0: Preflight +**Status:** ⬜ Not Started + +- [ ] Verify `CompoundInfo` interface exists in `component-metadata.ts` +- [ ] Verify `GRID_COMPOUND_INFO` exists with 14 relatedThemes +- [ ] Verify compound handler logic in `component-tokens.ts` +- [ ] Verify tool descriptions in `descriptions.ts` + +--- + +### Step 1: Add `composed` flag to CompoundInfo and set on grid +**Status:** ⬜ Not Started + +- [ ] Add `composed?: boolean` to `CompoundInfo` interface with JSDoc +- [ ] Set `composed: true` on `GRID_COMPOUND_INFO` +- [ ] Update grid guidance text for composed behavior +- [ ] Targeted tests pass + +--- + +### Step 2: Update handler guidance for composed compounds +**Status:** ⬜ Not Started + +- [ ] Add composed branch in `component-tokens.ts` handler +- [ ] Composed guidance emits correct labels and "Do NOT create separate themes" +- [ ] Standard compound guidance unchanged +- [ ] Targeted tests pass + +--- + +### Step 3: Update tool descriptions for composed distinction +**Status:** ⬜ Not Started + +- [ ] `get_design_tokens` description distinguishes standard vs composed compounds +- [ ] `create_component_theme` description covers composed compound behavior +- [ ] Targeted tests pass + +--- + +### Step 4: Update tests +**Status:** ⬜ Not Started + +- [ ] Grid test asserts composed output format +- [ ] Non-composed compound tests still pass +- [ ] Targeted tests pass + +--- + +### Step 5: Testing & Verification +**Status:** ⬜ Not Started + +- [ ] FULL test suite passing +- [ ] All failures fixed +- [ ] Build passes + +--- + +### Step 6: Documentation & Delivery +**Status:** ⬜ Not Started + +- [ ] "Must Update" docs modified +- [ ] "Check If Affected" docs reviewed +- [ ] Discoveries logged + +--- + +## Reviews + +| # | Type | Step | Verdict | File | +|---|------|------|---------|------| + +--- + +## Discoveries + +| Discovery | Disposition | Location | +|-----------|-------------|----------| + +--- + +## Execution Log + +| Timestamp | Action | Outcome | +|-----------|--------|---------| +| 2026-04-03 | Task staged | PROMPT.md and STATUS.md created | + +--- + +## Blockers + +*None* + +--- + +## Notes + +*Reserved for execution notes* From f8b842b199b83061e2604abfab34ad97e7611adc Mon Sep 17 00:00:00 2001 From: Simeon Simeonoff Date: Fri, 3 Apr 2026 21:20:29 +0300 Subject: [PATCH 8/9] refactor(mcp): restructure composed component token output with two-tier hierarchy - Introduce primary tokens (actionable) vs. refinement tokens (reference) for composed components - Primary tokens shown prominently with 'use only these' instruction - Refinement tokens rendered as compact name list with 'reference only' guidance - Update get_component_design_tokens handler to support two-tier format for composed components - Add warning to create_component_theme when non-primary tokens used on composed components - Update tool descriptions with composed vs. standard compound guidance - Update test expectations for new unified output format - Archive completed OpenSpec change for improved composed theme guidance - Remove obsolete .opencode directory --- .gitignore | 4 + .opencode/command/opsx-apply.md | 149 ----- .opencode/command/opsx-archive.md | 154 ----- .opencode/command/opsx-bulk-archive.md | 239 -------- .opencode/command/opsx-continue.md | 111 ---- .opencode/command/opsx-explore.md | 170 ------ .opencode/command/opsx-ff.md | 94 --- .opencode/command/opsx-new.md | 66 --- .opencode/command/opsx-onboard.md | 547 ----------------- .opencode/command/opsx-sync.md | 131 ----- .opencode/command/opsx-verify.md | 161 ----- .../skills/openspec-apply-change/SKILL.md | 156 ----- .../skills/openspec-archive-change/SKILL.md | 114 ---- .../openspec-bulk-archive-change/SKILL.md | 246 -------- .../skills/openspec-continue-change/SKILL.md | 118 ---- .opencode/skills/openspec-explore/SKILL.md | 288 --------- .opencode/skills/openspec-ff-change/SKILL.md | 101 ---- .opencode/skills/openspec-new-change/SKILL.md | 74 --- .opencode/skills/openspec-onboard/SKILL.md | 554 ------------------ .opencode/skills/openspec-sync-specs/SKILL.md | 138 ----- .../skills/openspec-verify-change/SKILL.md | 168 ------ .../.openspec.yaml | 2 + .../design.md | 73 +++ .../proposal.md | 28 + .../specs/component-theming/spec.md | 96 +++ .../tasks.md | 34 ++ openspec/specs/component-theming/spec.md | 65 +- .../tools/handlers/component-tokens.test.ts | 95 ++- .../mcp/src/knowledge/component-metadata.ts | 13 +- packages/mcp/src/tools/descriptions.ts | 34 +- .../mcp/src/tools/handlers/component-theme.ts | 51 +- .../src/tools/handlers/component-tokens.ts | 222 ++++--- .../themes/components/grid/_grid-theme.scss | 26 +- 33 files changed, 598 insertions(+), 3924 deletions(-) delete mode 100644 .opencode/command/opsx-apply.md delete mode 100644 .opencode/command/opsx-archive.md delete mode 100644 .opencode/command/opsx-bulk-archive.md delete mode 100644 .opencode/command/opsx-continue.md delete mode 100644 .opencode/command/opsx-explore.md delete mode 100644 .opencode/command/opsx-ff.md delete mode 100644 .opencode/command/opsx-new.md delete mode 100644 .opencode/command/opsx-onboard.md delete mode 100644 .opencode/command/opsx-sync.md delete mode 100644 .opencode/command/opsx-verify.md delete mode 100644 .opencode/skills/openspec-apply-change/SKILL.md delete mode 100644 .opencode/skills/openspec-archive-change/SKILL.md delete mode 100644 .opencode/skills/openspec-bulk-archive-change/SKILL.md delete mode 100644 .opencode/skills/openspec-continue-change/SKILL.md delete mode 100644 .opencode/skills/openspec-explore/SKILL.md delete mode 100644 .opencode/skills/openspec-ff-change/SKILL.md delete mode 100644 .opencode/skills/openspec-new-change/SKILL.md delete mode 100644 .opencode/skills/openspec-onboard/SKILL.md delete mode 100644 .opencode/skills/openspec-sync-specs/SKILL.md delete mode 100644 .opencode/skills/openspec-verify-change/SKILL.md create mode 100644 openspec/changes/archive/2026-04-03-improve-composed-theme-guidance/.openspec.yaml create mode 100644 openspec/changes/archive/2026-04-03-improve-composed-theme-guidance/design.md create mode 100644 openspec/changes/archive/2026-04-03-improve-composed-theme-guidance/proposal.md create mode 100644 openspec/changes/archive/2026-04-03-improve-composed-theme-guidance/specs/component-theming/spec.md create mode 100644 openspec/changes/archive/2026-04-03-improve-composed-theme-guidance/tasks.md diff --git a/.gitignore b/.gitignore index 4cbb8f87..4cad133b 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,7 @@ Loading complete # Artifacts packages/theming/tests/e2e/*.css packages/theming/tests/e2e/*.css.map + +# Agents and related files +.opencode +.pi diff --git a/.opencode/command/opsx-apply.md b/.opencode/command/opsx-apply.md deleted file mode 100644 index 94b8c1ee..00000000 --- a/.opencode/command/opsx-apply.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -description: Implement tasks from an OpenSpec change (Experimental) ---- - -Implement tasks from an OpenSpec change. - -**Input**: Optionally specify a change name (e.g., `/opsx-apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. - -**Steps** - -1. **Select the change** - - If a name is provided, use it. Otherwise: - - Infer from conversation context if the user mentioned a change - - Auto-select if only one active change exists - - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select - - Always announce: "Using change: " and how to override (e.g., `/opsx-apply `). - -2. **Check status to understand the schema** - ```bash - openspec status --change "" --json - ``` - Parse the JSON to understand: - - `schemaName`: The workflow being used (e.g., "spec-driven") - - Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others) - -3. **Get apply instructions** - - ```bash - openspec instructions apply --change "" --json - ``` - - This returns: - - Context file paths (varies by schema) - - Progress (total, complete, remaining) - - Task list with status - - Dynamic instruction based on current state - - **Handle states:** - - If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx-continue` - - If `state: "all_done"`: congratulate, suggest archive - - Otherwise: proceed to implementation - -4. **Read context files** - - Read the files listed in `contextFiles` from the apply instructions output. - The files depend on the schema being used: - - **spec-driven**: proposal, specs, design, tasks - - Other schemas: follow the contextFiles from CLI output - -5. **Show current progress** - - Display: - - Schema being used - - Progress: "N/M tasks complete" - - Remaining tasks overview - - Dynamic instruction from CLI - -6. **Implement tasks (loop until done or blocked)** - - For each pending task: - - Show which task is being worked on - - Make the code changes required - - Keep changes minimal and focused - - Mark task complete in the tasks file: `- [ ]` → `- [x]` - - Continue to next task - - **Pause if:** - - Task is unclear → ask for clarification - - Implementation reveals a design issue → suggest updating artifacts - - Error or blocker encountered → report and wait for guidance - - User interrupts - -7. **On completion or pause, show status** - - Display: - - Tasks completed this session - - Overall progress: "N/M tasks complete" - - If all done: suggest archive - - If paused: explain why and wait for guidance - -**Output During Implementation** - -``` -## Implementing: (schema: ) - -Working on task 3/7: -[...implementation happening...] -✓ Task complete - -Working on task 4/7: -[...implementation happening...] -✓ Task complete -``` - -**Output On Completion** - -``` -## Implementation Complete - -**Change:** -**Schema:** -**Progress:** 7/7 tasks complete ✓ - -### Completed This Session -- [x] Task 1 -- [x] Task 2 -... - -All tasks complete! You can archive this change with `/opsx-archive`. -``` - -**Output On Pause (Issue Encountered)** - -``` -## Implementation Paused - -**Change:** -**Schema:** -**Progress:** 4/7 tasks complete - -### Issue Encountered - - -**Options:** -1.