@dittowords/spec-cli 0.0.1-alpha.6 → 0.0.1-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -5
- package/dist/api.d.ts +45 -3
- package/dist/api.js +39 -3
- package/dist/commands/create-rule.d.ts +8 -0
- package/dist/commands/create-rule.js +37 -0
- package/dist/commands/init.js +2 -0
- package/dist/commands/pull.js +18 -8
- package/dist/config.d.ts +1 -0
- package/dist/config.js +7 -0
- package/dist/skill-content.d.ts +1 -0
- package/dist/skill-content.js +156 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,12 +37,23 @@ surfaces:
|
|
|
37
37
|
rules:
|
|
38
38
|
- name: Confirmation dialogs should be direct
|
|
39
39
|
description: Keep confirmation copy terse and unambiguous
|
|
40
|
+
styleguide: Acme Inc.
|
|
41
|
+
section: Voice & Tone
|
|
40
42
|
- surface: actionText
|
|
41
43
|
name: Calls to action should use active voice
|
|
42
44
|
description: Always lead with a verb
|
|
43
45
|
examples:
|
|
44
46
|
- from: "Your settings"
|
|
45
47
|
to: "Open settings"
|
|
48
|
+
styleguide: Acme Inc.
|
|
49
|
+
section: Voice & Tone
|
|
50
|
+
- term: sign up
|
|
51
|
+
disallowed:
|
|
52
|
+
- signup
|
|
53
|
+
- sign-up
|
|
54
|
+
description: Always use as two words (verb form)
|
|
55
|
+
styleguide: Acme Inc.
|
|
56
|
+
section: Terminology
|
|
46
57
|
---
|
|
47
58
|
```
|
|
48
59
|
|
|
@@ -63,7 +74,7 @@ rules: []
|
|
|
63
74
|
|
|
64
75
|
**Developer-owned keys**: `component`, `tags`, `surfaces`. Edit these freely.
|
|
65
76
|
|
|
66
|
-
**CLI-managed keys**: `rules` and workspace `tags`. Overwritten by `ditto-spec pull`. Do not edit by hand.
|
|
77
|
+
**CLI-managed keys**: `rules` and workspace `tags`. Overwritten by `ditto-spec pull`. Do not edit by hand. Rules come in two shapes: style rules (`name`/`description`/`examples`) and terminology entries (`term`/`disallowed`/`description`).
|
|
67
78
|
|
|
68
79
|
**Surface keys** identify each distinct piece of user-facing text the component renders. For text passed as props, use the prop path as the key — dot-notation works for nested props (e.g., `primaryAction.label`). Use `$children` for text via the `children` prop. For hardcoded or internal strings, use a descriptive role name (e.g., `headline`, `bodyText`, `submitLabel`).
|
|
69
80
|
|
|
@@ -102,9 +113,10 @@ Use `--path <dir>` to specify where the file is created (defaults to the current
|
|
|
102
113
|
Syncs rules from the platform into spec files.
|
|
103
114
|
|
|
104
115
|
1. Discovers all `.ditto.md` files under configured roots
|
|
105
|
-
2. Fetches
|
|
106
|
-
3.
|
|
107
|
-
4.
|
|
116
|
+
2. Fetches style guides from `GET /v2/styleguides`
|
|
117
|
+
3. Flattens rules and wordlist entries across all guides (or only those named in `styleguides` config)
|
|
118
|
+
4. Matches rules to specs by tag intersection (client-side)
|
|
119
|
+
5. Rewrites the `rules` key in each file's YAML frontmatter
|
|
108
120
|
|
|
109
121
|
Use `--dry-run` to see what would change without writing.
|
|
110
122
|
|
|
@@ -133,6 +145,7 @@ Create `dittospec.config.json` at your repo root (or any ancestor directory):
|
|
|
133
145
|
| `apiBase` | Ditto API base URL |
|
|
134
146
|
| `workspaceId` | Your workspace ID |
|
|
135
147
|
| `roots` | Repo-relative directories to search for `.ditto.md` files. Defaults to `["."]`. |
|
|
148
|
+
| `styleguides` | Optional list of style guide names to pull. Defaults to all. |
|
|
136
149
|
|
|
137
150
|
Set `DITTO_API_KEY` in your environment or in a `.env` file at the repo root.
|
|
138
151
|
|
|
@@ -144,7 +157,10 @@ When writing or editing user-facing text for a component:
|
|
|
144
157
|
2. Read the component's `index.ditto.md`. Match each piece of text you're writing to a surface key.
|
|
145
158
|
3. Respect `maxLength` — it's a layout invariant, not a suggestion.
|
|
146
159
|
4. Follow all rules in `rules[]`. Entries without `surface` apply to every surface; entries with `surface` apply only to that surface.
|
|
147
|
-
5.
|
|
160
|
+
5. Rules come in two shapes:
|
|
161
|
+
- **Style rules** have `name`, `description`, and optional `examples` (before/after pairs). Use `examples` as concrete tone/shape guidance.
|
|
162
|
+
- **Terminology entries** have `term` and `disallowed`. Always use the `term` form; never use any of the `disallowed` alternatives.
|
|
163
|
+
6. Each rule carries `styleguide` and `section` metadata indicating its source on the platform.
|
|
148
164
|
|
|
149
165
|
### Creating specs
|
|
150
166
|
|
package/dist/api.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { Config } from "./config";
|
|
2
|
-
export interface
|
|
2
|
+
export interface StyleguideVariant {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
localeCode: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface StyleRule {
|
|
3
8
|
name: string;
|
|
4
|
-
type: "style" | "wordlist";
|
|
5
9
|
description: string;
|
|
6
10
|
examples: {
|
|
7
11
|
from: string;
|
|
@@ -9,11 +13,49 @@ export interface RuleResponse {
|
|
|
9
13
|
}[];
|
|
10
14
|
tags: string[];
|
|
11
15
|
}
|
|
16
|
+
export interface WordlistEntry {
|
|
17
|
+
term: string;
|
|
18
|
+
disallowed: string[];
|
|
19
|
+
description: string;
|
|
20
|
+
tags: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface StyleguideSection {
|
|
23
|
+
name: string;
|
|
24
|
+
kind: "rules" | "wordlist";
|
|
25
|
+
rules: StyleRule[] | WordlistEntry[];
|
|
26
|
+
}
|
|
27
|
+
export interface Styleguide {
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
variant: StyleguideVariant | null;
|
|
31
|
+
sections: StyleguideSection[];
|
|
32
|
+
}
|
|
33
|
+
export type FlatRule = {
|
|
34
|
+
kind: "style";
|
|
35
|
+
styleguide: string;
|
|
36
|
+
section: string;
|
|
37
|
+
name: string;
|
|
38
|
+
description: string;
|
|
39
|
+
examples: {
|
|
40
|
+
from: string;
|
|
41
|
+
to: string;
|
|
42
|
+
}[];
|
|
43
|
+
tags: string[];
|
|
44
|
+
} | {
|
|
45
|
+
kind: "wordlist";
|
|
46
|
+
styleguide: string;
|
|
47
|
+
section: string;
|
|
48
|
+
term: string;
|
|
49
|
+
disallowed: string[];
|
|
50
|
+
description: string;
|
|
51
|
+
tags: string[];
|
|
52
|
+
};
|
|
53
|
+
export declare function flattenStyleguides(guides: Styleguide[], filter?: string[]): FlatRule[];
|
|
12
54
|
export declare class DittoApi {
|
|
13
55
|
private readonly config;
|
|
14
56
|
private readonly apiKey;
|
|
15
57
|
constructor(config: Config, apiKey: string);
|
|
16
|
-
|
|
58
|
+
getStyleguides(): Promise<Styleguide[]>;
|
|
17
59
|
private fetch;
|
|
18
60
|
private headers;
|
|
19
61
|
}
|
package/dist/api.js
CHANGED
|
@@ -1,16 +1,52 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DittoApi = void 0;
|
|
4
|
+
exports.flattenStyleguides = flattenStyleguides;
|
|
5
|
+
function flattenStyleguides(guides, filter) {
|
|
6
|
+
const selected = filter ? guides.filter((g) => filter.includes(g.name)) : guides;
|
|
7
|
+
const result = [];
|
|
8
|
+
for (const guide of selected) {
|
|
9
|
+
for (const section of guide.sections) {
|
|
10
|
+
if (section.kind === "rules") {
|
|
11
|
+
for (const rule of section.rules) {
|
|
12
|
+
result.push({
|
|
13
|
+
kind: "style",
|
|
14
|
+
styleguide: guide.name,
|
|
15
|
+
section: section.name,
|
|
16
|
+
name: rule.name,
|
|
17
|
+
description: rule.description,
|
|
18
|
+
examples: rule.examples,
|
|
19
|
+
tags: rule.tags,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
for (const entry of section.rules) {
|
|
25
|
+
result.push({
|
|
26
|
+
kind: "wordlist",
|
|
27
|
+
styleguide: guide.name,
|
|
28
|
+
section: section.name,
|
|
29
|
+
term: entry.term,
|
|
30
|
+
disallowed: entry.disallowed,
|
|
31
|
+
description: entry.description,
|
|
32
|
+
tags: entry.tags,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
4
40
|
class DittoApi {
|
|
5
41
|
constructor(config, apiKey) {
|
|
6
42
|
this.config = config;
|
|
7
43
|
this.apiKey = apiKey;
|
|
8
44
|
}
|
|
9
|
-
async
|
|
10
|
-
const url = new URL("/v2/
|
|
45
|
+
async getStyleguides() {
|
|
46
|
+
const url = new URL("/v2/styleguides", this.config.apiBase);
|
|
11
47
|
const res = await this.fetch(url, { method: "GET" });
|
|
12
48
|
const data = (await res.json());
|
|
13
|
-
return data.
|
|
49
|
+
return data.styleguides;
|
|
14
50
|
}
|
|
15
51
|
async fetch(url, init) {
|
|
16
52
|
const res = await fetch(url.toString(), {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRule = createRule;
|
|
4
|
+
const api_1 = require("../api");
|
|
5
|
+
const config_1 = require("../config");
|
|
6
|
+
async function createRule(opts) {
|
|
7
|
+
const { config } = (0, config_1.loadConfig)();
|
|
8
|
+
const apiKey = (0, config_1.getApiKey)();
|
|
9
|
+
const api = new api_1.DittoApi(config, apiKey);
|
|
10
|
+
if (!config.styleguideId) {
|
|
11
|
+
throw new Error('No "styleguideId" in dittospec.config.json. Add the ID of the style guide you want rules created in.');
|
|
12
|
+
}
|
|
13
|
+
const info = await api.getStyleguideInfo();
|
|
14
|
+
if (!info) {
|
|
15
|
+
throw new Error("Could not discover style guide sections. Check that your workspace has at least one style guide with rules.");
|
|
16
|
+
}
|
|
17
|
+
const sectionId = info.sectionId;
|
|
18
|
+
let examples;
|
|
19
|
+
if (opts.examples) {
|
|
20
|
+
try {
|
|
21
|
+
examples = JSON.parse(opts.examples);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new Error('--examples must be valid JSON, e.g. \'[{"from":"bad","to":"good"}]\'');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const tags = opts.tags ? opts.tags.split(",").map((t) => t.trim()).filter(Boolean) : undefined;
|
|
28
|
+
const result = await api.createRule({
|
|
29
|
+
styleguideId: config.styleguideId,
|
|
30
|
+
sectionId,
|
|
31
|
+
name: opts.name,
|
|
32
|
+
description: opts.description,
|
|
33
|
+
examples,
|
|
34
|
+
tags,
|
|
35
|
+
});
|
|
36
|
+
console.log(`✓ Created rule "${result.name}" (${result.id})`);
|
|
37
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -42,6 +42,8 @@ When writing or editing text props for a component:
|
|
|
42
42
|
2. Check for ${c("index.ditto.md")} next to the component for surface-specific rules and constraints.
|
|
43
43
|
3. Respect ${c("maxLength")} — it's a layout constraint, not a suggestion.
|
|
44
44
|
4. Follow all rules in the ${c("rules")} key. Rules without a ${c("surface")} field apply to every surface; rules with ${c("surface")} apply only to that surface.
|
|
45
|
+
5. Rules come in two shapes: style rules (${c("name")}, ${c("description")}, ${c("examples")}) and terminology entries (${c("term")}, ${c("disallowed")}). For terminology entries, always use the ${c("term")} form and never use the ${c("disallowed")} alternatives.
|
|
46
|
+
6. Each rule carries ${c("styleguide")} and ${c("section")} metadata indicating its source.
|
|
45
47
|
|
|
46
48
|
${h3} Creating specs
|
|
47
49
|
|
package/dist/commands/pull.js
CHANGED
|
@@ -44,8 +44,9 @@ async function pull(opts = {}) {
|
|
|
44
44
|
const paths = workspaceFiles.map((w) => path_1.default.relative(root, w.file.abs)).join(", ");
|
|
45
45
|
throw new Error(`Found multiple workspace specs (${paths}). Expected at most one.`);
|
|
46
46
|
}
|
|
47
|
-
const
|
|
48
|
-
|
|
47
|
+
const guides = await api.getStyleguides();
|
|
48
|
+
const allRules = (0, api_1.flattenStyleguides)(guides, config.styleguides);
|
|
49
|
+
console.log(`Fetched ${allRules.length} rule${allRules.length === 1 ? "" : "s"} from ${guides.length} style guide(s).`);
|
|
49
50
|
const platformTags = [...new Set(allRules.flatMap((r) => r.tags))].filter(Boolean).sort();
|
|
50
51
|
let written = 0;
|
|
51
52
|
let unchanged = 0;
|
|
@@ -105,13 +106,22 @@ function rulesForTags(rules, tags) {
|
|
|
105
106
|
return rules.filter((r) => r.tags.length > 0 && r.tags.some((t) => tagSet.has(t)));
|
|
106
107
|
}
|
|
107
108
|
function toRuleObj(rule) {
|
|
108
|
-
const out = {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
const out = {};
|
|
110
|
+
if (rule.kind === "wordlist") {
|
|
111
|
+
out.term = rule.term;
|
|
112
|
+
out.disallowed = rule.disallowed;
|
|
113
|
+
if (rule.description)
|
|
114
|
+
out.description = rule.description;
|
|
114
115
|
}
|
|
116
|
+
else {
|
|
117
|
+
out.name = rule.name;
|
|
118
|
+
out.description = rule.description;
|
|
119
|
+
if (rule.examples.length > 0) {
|
|
120
|
+
out.examples = rule.examples.map((ex) => ({ from: ex.from, to: ex.to }));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
out.styleguide = rule.styleguide;
|
|
124
|
+
out.section = rule.section;
|
|
115
125
|
return out;
|
|
116
126
|
}
|
|
117
127
|
function toSurfaceRuleObj(surface, rule) {
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -54,6 +54,13 @@ function validate(c, source) {
|
|
|
54
54
|
throw new Error(`${source}: every entry in "roots" must be a string.`);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
if (obj.styleguides !== undefined) {
|
|
58
|
+
if (!Array.isArray(obj.styleguides))
|
|
59
|
+
throw new Error(`${source}: "styleguides" must be an array of strings if present.`);
|
|
60
|
+
if (obj.styleguides.some((s) => typeof s !== "string")) {
|
|
61
|
+
throw new Error(`${source}: every entry in "styleguides" must be a string.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
57
64
|
}
|
|
58
65
|
function getApiKey() {
|
|
59
66
|
try {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SPEC_COMPONENT_SKILL = "---\ndescription: Analyze a component, create ditto specs for it and its dependencies, identify rule gaps, and audit copy\nargument-hint: <ComponentName or path/to/component>\nallowed-tools: [Read, Bash, Edit, Write, Grep, Glob, Agent]\n---\n\n# Spec Component\n\nAnalyze a design system component, scaffold `.ditto.md` content specs (including child components that lack specs), auto-fill surfaces and tags, identify style guide rule gaps from real code usage, create new rules on the platform, and audit copy compliance.\n\n## Input\n\n$ARGUMENTS\n\nAccept either a component name (e.g. `Button`) or a file path (e.g. `src/components/Button/index.tsx`). If a name is given, search the codebase to find the component. If multiple matches exist, ask the user to clarify.\n\n---\n\n## Phase 1: Discovery\n\nFind the target component and map its dependencies.\n\n1. Locate the component source file.\n2. Read the component code. Walk imports to find **child components that render user-facing text** (components that accept string props, children rendered as text, or contain hardcoded strings). Skip utility imports, style imports, icon imports, and non-text components.\n3. Check which components already have an `index.ditto.md` spec by looking for the file in each component's directory.\n4. Read `workspace.ditto.md` at the project root (if it exists) for existing platform tags and universal rules.\n5. Report:\n - Target component and its location\n - Child components needing specs (no existing `index.ditto.md`)\n - Existing specs found (already covered)\n\n---\n\n## Phase 2: Surface Analysis\n\nFor each component that needs a new spec (target + unspecced children):\n\n1. Read the TypeScript interface / props type definition.\n2. Identify every **text surface** \u2014 each piece of user-facing copy the component renders:\n - **String props** rendered in JSX \u2192 use the prop name as the surface key\n - **`children`** when used as text \u2192 use `$children` as the surface key\n - **Nested props** \u2192 use dot notation (e.g. `primaryAction.label`)\n - **Hardcoded strings** in JSX \u2192 use a descriptive role name (e.g. `headline`, `submitLabel`, `bodyText`)\n3. For each surface, suggest **tags** from the `workspace.ditto.md` tag inventory, matched by semantic role (e.g. `heading`, `body`, `button`, `call-to-action`). Prefer reusing existing tags. Only propose a new tag if nothing fits \u2014 and note it will be new.\n4. Estimate **`maxLength`** where layout context provides constraints (e.g. single-line headings, button widths). Omit if no clear constraint.\n\nPresent the proposed spec for each component in exact YAML format:\n\n```yaml\n---\ncomponent: ComponentName\ntags: [relevant-tags]\nsurfaces:\n surfaceKey:\n tags: [semantic-tags]\n maxLength: 60\n# Managed by Ditto \u2014 do not edit below\nrules: []\n---\n```\n\n**STOP HERE. Ask the user to review and approve the proposed surfaces before creating any files.** Let them add, remove, or modify surfaces and tags.\n\n---\n\n## Phase 3: Create Specs\n\nAfter user approval:\n\n1. For each component, run:\n ```\n npx ditto-spec scaffold <ComponentName> --path <component-directory>\n ```\n2. Edit each created `index.ditto.md` to replace the empty `surfaces: {}` and `tags: []` with the approved values. Only edit developer-owned keys (`component`, `tags`, `surfaces`). Never write `rules` by hand.\n3. Run `npx ditto-spec pull` to populate the `rules` section from the platform.\n4. Run `npx ditto-spec check` to validate all spec files.\n\nReport which specs were created and how many rules were pulled.\n\n---\n\n## Phase 4: Rule Gap Analysis\n\nIdentify copy patterns that should be rules but aren't.\n\n1. Read all rules now in the spec files (workspace-level from `workspace.ditto.md` + component-level and per-surface from each `index.ditto.md`).\n2. Search the codebase for **instances where the target component is used** \u2014 find all files that import and render it.\n3. For each usage instance, read the **actual copy** being passed to the component's text surfaces (string literals, template strings, variables with obvious values).\n4. Analyze the copy across instances for patterns no existing rule covers:\n - Inconsistent terminology across instances\n - Tone mismatches (e.g. some formal, some casual)\n - Anti-patterns: passive voice in CTAs, overly long descriptions, redundant words\n - Surface-specific conventions that should be formalized (e.g. \"all action buttons start with a verb\")\n5. If **no meaningful gaps** are found, say so and skip to Phase 6.\n6. If gaps are found, propose new rules. For each:\n - **name**: short rule name\n - **description**: what the rule enforces\n - **tags**: which tags it should apply to (determines which surfaces it matches)\n - **examples**: `{from, to}` pairs drawn from actual code showing the pattern\n\nIf any proposed tags don't exist in the workspace yet, warn the user. Currently, only existing workspace tags can be assigned to rules. New tags may need to be created on the platform first.\n\n**STOP HERE. Present proposals and ask the user which rules (if any) to create.**\n\n---\n\n## Phase 5: Create Rules and Re-Pull\n\nFor each approved rule:\n\n1. Run:\n ```\n npx ditto-spec create-rule --name \"<rule name>\" --description \"<description>\" --tags \"<tag1,tag2>\" --examples '<json array of {from,to}>'\n ```\n If a rule uses tags that don't exist yet, warn the user and omit those tags from the command (the API rejects unknown tags).\n\n2. After all rules are created, run `npx ditto-spec pull` to sync the new rules into spec files.\n\n3. Verify the specs now contain the expected rules by reading the updated `index.ditto.md` files.\n\n---\n\n## Phase 6: Copy Audit\n\nEvaluate all component instances against the full rule set.\n\n1. Re-read the fully-populated spec files (workspace + component + surface rules).\n2. Find all instances of the component in the codebase.\n3. For each instance, evaluate the actual copy against every applicable rule:\n - Workspace rules apply to all surfaces\n - Component-level rules (no `surface` field) apply to all surfaces\n - Per-surface rules (with `surface` field) apply only to that surface\n - Respect `maxLength` constraints \u2014 flag any text that exceeds the limit\n4. Report violations:\n - File and line where the instance appears\n - The text that violates the rule\n - Which rule is violated\n - Suggested corrected copy\n\n---\n\n## Reference: Ditto Spec Conventions\n\nThese conventions are essential for creating correct specs:\n\n- **Surface keys**: prop name for props, `$children` for children text, dot-notation for nested props (`primaryAction.label`), descriptive role for hardcoded strings (`headline`, `bodyText`).\n- **Tags**: lowercase, hyphenated. Match semantic role (e.g. `heading`, `body`, `button`, `call-to-action`, `dialog-title`). Check `workspace.ditto.md` `tags` key for the full inventory.\n- **Developer-owned keys**: `component`, `tags`, `surfaces` \u2014 edit freely.\n- **CLI-managed keys**: `rules` (and workspace `tags`) \u2014 **never write by hand**. Populated by `ditto-spec pull`.\n- **Three-level rule hierarchy**: workspace rules (apply everywhere) \u2192 component-level rules (no `surface` field, apply to all surfaces in that component) \u2192 per-surface rules (with `surface: \"<key>\"`, apply only to that surface).\n- **Parent-child layering**: if a parent component passes text to a child (e.g. a label to a Button), the parent declares a surface in its own spec. Both parent and child rules apply \u2014 the parent's rules (e.g. dialog-level tone) layer with the child's rules (e.g. button-level constraints). A child having its own spec does NOT exempt the parent from declaring surfaces for text it provides.\n- **Tag matching**: rules match specs by tag intersection. Any shared tag triggers the match. If a rule matches both component-level and surface-level tags, it emits at component level (broader scope wins).\n";
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SPEC_COMPONENT_SKILL = void 0;
|
|
4
|
+
exports.SPEC_COMPONENT_SKILL = `---
|
|
5
|
+
description: Analyze a component, create ditto specs for it and its dependencies, identify rule gaps, and audit copy
|
|
6
|
+
argument-hint: <ComponentName or path/to/component>
|
|
7
|
+
allowed-tools: [Read, Bash, Edit, Write, Grep, Glob, Agent]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Spec Component
|
|
11
|
+
|
|
12
|
+
Analyze a design system component, scaffold \`.ditto.md\` content specs (including child components that lack specs), auto-fill surfaces and tags, identify style guide rule gaps from real code usage, create new rules on the platform, and audit copy compliance.
|
|
13
|
+
|
|
14
|
+
## Input
|
|
15
|
+
|
|
16
|
+
$ARGUMENTS
|
|
17
|
+
|
|
18
|
+
Accept either a component name (e.g. \`Button\`) or a file path (e.g. \`src/components/Button/index.tsx\`). If a name is given, search the codebase to find the component. If multiple matches exist, ask the user to clarify.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Phase 1: Discovery
|
|
23
|
+
|
|
24
|
+
Find the target component and map its dependencies.
|
|
25
|
+
|
|
26
|
+
1. Locate the component source file.
|
|
27
|
+
2. Read the component code. Walk imports to find **child components that render user-facing text** (components that accept string props, children rendered as text, or contain hardcoded strings). Skip utility imports, style imports, icon imports, and non-text components.
|
|
28
|
+
3. Check which components already have an \`index.ditto.md\` spec by looking for the file in each component's directory.
|
|
29
|
+
4. Read \`workspace.ditto.md\` at the project root (if it exists) for existing platform tags and universal rules.
|
|
30
|
+
5. Report:
|
|
31
|
+
- Target component and its location
|
|
32
|
+
- Child components needing specs (no existing \`index.ditto.md\`)
|
|
33
|
+
- Existing specs found (already covered)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Phase 2: Surface Analysis
|
|
38
|
+
|
|
39
|
+
For each component that needs a new spec (target + unspecced children):
|
|
40
|
+
|
|
41
|
+
1. Read the TypeScript interface / props type definition.
|
|
42
|
+
2. Identify every **text surface** — each piece of user-facing copy the component renders:
|
|
43
|
+
- **String props** rendered in JSX → use the prop name as the surface key
|
|
44
|
+
- **\`children\`** when used as text → use \`$children\` as the surface key
|
|
45
|
+
- **Nested props** → use dot notation (e.g. \`primaryAction.label\`)
|
|
46
|
+
- **Hardcoded strings** in JSX → use a descriptive role name (e.g. \`headline\`, \`submitLabel\`, \`bodyText\`)
|
|
47
|
+
3. For each surface, suggest **tags** from the \`workspace.ditto.md\` tag inventory, matched by semantic role (e.g. \`heading\`, \`body\`, \`button\`, \`call-to-action\`). Prefer reusing existing tags. Only propose a new tag if nothing fits — and note it will be new.
|
|
48
|
+
4. Estimate **\`maxLength\`** where layout context provides constraints (e.g. single-line headings, button widths). Omit if no clear constraint.
|
|
49
|
+
|
|
50
|
+
Present the proposed spec for each component in exact YAML format:
|
|
51
|
+
|
|
52
|
+
\`\`\`yaml
|
|
53
|
+
---
|
|
54
|
+
component: ComponentName
|
|
55
|
+
tags: [relevant-tags]
|
|
56
|
+
surfaces:
|
|
57
|
+
surfaceKey:
|
|
58
|
+
tags: [semantic-tags]
|
|
59
|
+
maxLength: 60
|
|
60
|
+
# Managed by Ditto — do not edit below
|
|
61
|
+
rules: []
|
|
62
|
+
---
|
|
63
|
+
\`\`\`
|
|
64
|
+
|
|
65
|
+
**STOP HERE. Ask the user to review and approve the proposed surfaces before creating any files.** Let them add, remove, or modify surfaces and tags.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Phase 3: Create Specs
|
|
70
|
+
|
|
71
|
+
After user approval:
|
|
72
|
+
|
|
73
|
+
1. For each component, run:
|
|
74
|
+
\`\`\`
|
|
75
|
+
npx ditto-spec scaffold <ComponentName> --path <component-directory>
|
|
76
|
+
\`\`\`
|
|
77
|
+
2. Edit each created \`index.ditto.md\` to replace the empty \`surfaces: {}\` and \`tags: []\` with the approved values. Only edit developer-owned keys (\`component\`, \`tags\`, \`surfaces\`). Never write \`rules\` by hand.
|
|
78
|
+
3. Run \`npx ditto-spec pull\` to populate the \`rules\` section from the platform.
|
|
79
|
+
4. Run \`npx ditto-spec check\` to validate all spec files.
|
|
80
|
+
|
|
81
|
+
Report which specs were created and how many rules were pulled.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Phase 4: Rule Gap Analysis
|
|
86
|
+
|
|
87
|
+
Identify copy patterns that should be rules but aren't.
|
|
88
|
+
|
|
89
|
+
1. Read all rules now in the spec files (workspace-level from \`workspace.ditto.md\` + component-level and per-surface from each \`index.ditto.md\`).
|
|
90
|
+
2. Search the codebase for **instances where the target component is used** — find all files that import and render it.
|
|
91
|
+
3. For each usage instance, read the **actual copy** being passed to the component's text surfaces (string literals, template strings, variables with obvious values).
|
|
92
|
+
4. Analyze the copy across instances for patterns no existing rule covers:
|
|
93
|
+
- Inconsistent terminology across instances
|
|
94
|
+
- Tone mismatches (e.g. some formal, some casual)
|
|
95
|
+
- Anti-patterns: passive voice in CTAs, overly long descriptions, redundant words
|
|
96
|
+
- Surface-specific conventions that should be formalized (e.g. "all action buttons start with a verb")
|
|
97
|
+
5. If **no meaningful gaps** are found, say so and skip to Phase 6.
|
|
98
|
+
6. If gaps are found, propose new rules. For each:
|
|
99
|
+
- **name**: short rule name
|
|
100
|
+
- **description**: what the rule enforces
|
|
101
|
+
- **tags**: which tags it should apply to (determines which surfaces it matches)
|
|
102
|
+
- **examples**: \`{from, to}\` pairs drawn from actual code showing the pattern
|
|
103
|
+
|
|
104
|
+
If any proposed tags don't exist in the workspace yet, warn the user. Currently, only existing workspace tags can be assigned to rules. New tags may need to be created on the platform first.
|
|
105
|
+
|
|
106
|
+
**STOP HERE. Present proposals and ask the user which rules (if any) to create.**
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Phase 5: Create Rules and Re-Pull
|
|
111
|
+
|
|
112
|
+
For each approved rule:
|
|
113
|
+
|
|
114
|
+
1. Run:
|
|
115
|
+
\`\`\`
|
|
116
|
+
npx ditto-spec create-rule --name "<rule name>" --description "<description>" --tags "<tag1,tag2>" --examples '<json array of {from,to}>'
|
|
117
|
+
\`\`\`
|
|
118
|
+
If a rule uses tags that don't exist yet, warn the user and omit those tags from the command (the API rejects unknown tags).
|
|
119
|
+
|
|
120
|
+
2. After all rules are created, run \`npx ditto-spec pull\` to sync the new rules into spec files.
|
|
121
|
+
|
|
122
|
+
3. Verify the specs now contain the expected rules by reading the updated \`index.ditto.md\` files.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Phase 6: Copy Audit
|
|
127
|
+
|
|
128
|
+
Evaluate all component instances against the full rule set.
|
|
129
|
+
|
|
130
|
+
1. Re-read the fully-populated spec files (workspace + component + surface rules).
|
|
131
|
+
2. Find all instances of the component in the codebase.
|
|
132
|
+
3. For each instance, evaluate the actual copy against every applicable rule:
|
|
133
|
+
- Workspace rules apply to all surfaces
|
|
134
|
+
- Component-level rules (no \`surface\` field) apply to all surfaces
|
|
135
|
+
- Per-surface rules (with \`surface\` field) apply only to that surface
|
|
136
|
+
- Respect \`maxLength\` constraints — flag any text that exceeds the limit
|
|
137
|
+
4. Report violations:
|
|
138
|
+
- File and line where the instance appears
|
|
139
|
+
- The text that violates the rule
|
|
140
|
+
- Which rule is violated
|
|
141
|
+
- Suggested corrected copy
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Reference: Ditto Spec Conventions
|
|
146
|
+
|
|
147
|
+
These conventions are essential for creating correct specs:
|
|
148
|
+
|
|
149
|
+
- **Surface keys**: prop name for props, \`$children\` for children text, dot-notation for nested props (\`primaryAction.label\`), descriptive role for hardcoded strings (\`headline\`, \`bodyText\`).
|
|
150
|
+
- **Tags**: lowercase, hyphenated. Match semantic role (e.g. \`heading\`, \`body\`, \`button\`, \`call-to-action\`, \`dialog-title\`). Check \`workspace.ditto.md\` \`tags\` key for the full inventory.
|
|
151
|
+
- **Developer-owned keys**: \`component\`, \`tags\`, \`surfaces\` — edit freely.
|
|
152
|
+
- **CLI-managed keys**: \`rules\` (and workspace \`tags\`) — **never write by hand**. Populated by \`ditto-spec pull\`.
|
|
153
|
+
- **Three-level rule hierarchy**: workspace rules (apply everywhere) → component-level rules (no \`surface\` field, apply to all surfaces in that component) → per-surface rules (with \`surface: "<key>"\`, apply only to that surface).
|
|
154
|
+
- **Parent-child layering**: if a parent component passes text to a child (e.g. a label to a Button), the parent declares a surface in its own spec. Both parent and child rules apply — the parent's rules (e.g. dialog-level tone) layer with the child's rules (e.g. button-level constraints). A child having its own spec does NOT exempt the parent from declaring surfaces for text it provides.
|
|
155
|
+
- **Tag matching**: rules match specs by tag intersection. Any shared tag triggers the match. If a rule matches both component-level and surface-level tags, it emits at component level (broader scope wins).
|
|
156
|
+
`;
|