@dittowords/spec-cli 0.0.1-alpha.10 → 0.0.1-alpha.12

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 CHANGED
@@ -159,6 +159,7 @@ ditto-spec create-rule --name "Use active voice" --description "Lead CTAs with a
159
159
  |---|---|
160
160
  | `--name` | Rule name (required) |
161
161
  | `--description` | What the rule enforces (required) |
162
+ | `--styleguide` | Target style guide name or ID (optional, overrides `defaultStyleguide` config) |
162
163
  | `--tags` | Comma-separated tags to scope the rule (optional) |
163
164
  | `--examples` | JSON array of `{from, to}` pairs (optional) |
164
165
 
@@ -181,9 +182,9 @@ Create `dittospec.config.json` at your repo root (or any ancestor directory):
181
182
  | `apiBase` | Ditto API base URL |
182
183
  | `workspaceId` | Your workspace ID |
183
184
  | `roots` | Repo-relative directories to search for `.ditto.md` files. Defaults to `["."]`. |
184
- | `styleguides` | Optional list of style guide names to pull. Defaults to all. |
185
+ | `styleguides` | Optional list of style guide names or IDs to pull. Defaults to all. |
185
186
  | `locales` | Optional list of locale codes (e.g. `["de-DE", "fr-FR"]`). Includes locale-scoped style guides matching these codes. Base (no-variant) guides are always included. |
186
- | `styleguideId` | Optional style guide name for `create-rule`. Defaults to the first guide returned by the API. |
187
+ | `defaultStyleguide` | Optional style guide name or ID for `create-rule`. Overridable with the `--styleguide` flag. Defaults to the first guide returned by the API. |
187
188
 
188
189
  Set `DITTO_API_KEY` in your environment or in a `.env` file at the repo root.
189
190
 
package/dist/api.d.ts CHANGED
@@ -25,6 +25,7 @@ export interface StyleguideSection {
25
25
  rules: StyleRule[] | WordlistEntry[];
26
26
  }
27
27
  export interface Styleguide {
28
+ id: string;
28
29
  name: string;
29
30
  description: string;
30
31
  variant: StyleguideVariant | null;
@@ -58,7 +59,7 @@ export declare class DittoApi {
58
59
  private readonly apiKey;
59
60
  constructor(config: Config, apiKey: string);
60
61
  getStyleguides(): Promise<Styleguide[]>;
61
- getStyleguideInfo(): Promise<{
62
+ getStyleguideInfo(styleguideOverride?: string): Promise<{
62
63
  styleguideId: string;
63
64
  sectionId: string;
64
65
  } | null>;
package/dist/api.js CHANGED
@@ -3,7 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DittoApi = void 0;
4
4
  exports.flattenStyleguides = flattenStyleguides;
5
5
  function flattenStyleguides(guides, filter) {
6
- const selected = filter?.length ? guides.filter((g) => filter.includes(g.name)) : guides;
6
+ const selected = filter?.length
7
+ ? guides.filter((g) => filter.includes(g.name) || filter.includes(g.id))
8
+ : guides;
7
9
  const result = [];
8
10
  for (const guide of selected) {
9
11
  for (const section of guide.sections) {
@@ -50,16 +52,24 @@ class DittoApi {
50
52
  const data = (await res.json());
51
53
  return data.styleguides;
52
54
  }
53
- async getStyleguideInfo() {
55
+ async getStyleguideInfo(styleguideOverride) {
54
56
  const guides = await this.getStyleguides();
55
57
  if (guides.length === 0)
56
58
  return null;
57
- const guide = this.config.styleguideId
58
- ? guides.find((g) => g.name === this.config.styleguideId) ?? guides[0]
59
- : guides[0];
59
+ const target = styleguideOverride ?? this.config.defaultStyleguide;
60
+ let guide;
61
+ if (target) {
62
+ const match = guides.find((g) => g.name === target || g.id === target);
63
+ if (!match) {
64
+ throw new Error(`Style guide "${target}" not found. Available: ${guides.map((g) => g.name).join(", ")}`);
65
+ }
66
+ guide = match;
67
+ }
68
+ else {
69
+ guide = guides[0];
70
+ }
60
71
  if (guide.sections.length === 0)
61
72
  return null;
62
- // Use the first rules section; the API requires a section ID for rule creation
63
73
  const section = guide.sections.find((s) => s.kind === "rules") ?? guide.sections[0];
64
74
  return { styleguideId: guide.name, sectionId: section.name };
65
75
  }
package/dist/cli.js CHANGED
@@ -29,10 +29,10 @@ const COMMANDS = {
29
29
  const name = getFlag("--name");
30
30
  const description = getFlag("--description");
31
31
  if (!name || !description) {
32
- process.stderr.write('Usage: ditto-spec create-rule --name "<name>" --description "<desc>" [--tags "<t1,t2>"] [--examples \'<json>\']\n');
32
+ process.stderr.write('Usage: ditto-spec create-rule --name "<name>" --description "<desc>" [--styleguide "<name-or-id>"] [--tags "<t1,t2>"] [--examples \'<json>\']\n');
33
33
  process.exit(2);
34
34
  }
35
- return (0, create_rule_1.createRule)({ name, description, tags: getFlag("--tags"), examples: getFlag("--examples") });
35
+ return (0, create_rule_1.createRule)({ name, description, styleguide: getFlag("--styleguide"), tags: getFlag("--tags"), examples: getFlag("--examples") });
36
36
  },
37
37
  };
38
38
  const HELP = `ditto-spec — sync .ditto.md content specs with the Ditto platform.
@@ -51,10 +51,11 @@ Commands:
51
51
  check Parse every spec file; exit non-zero on any malformed file.
52
52
  list Print every component spec with its surfaces and tags.
53
53
  create-rule Create a rule on the platform.
54
- --name "<name>" Rule name (required)
55
- --description "<desc>" Rule description (required)
56
- --tags "<t1,t2>" Comma-separated tags (optional)
57
- --examples '<json>' JSON array of {from,to} pairs (optional)
54
+ --name "<name>" Rule name (required)
55
+ --description "<desc>" Rule description (required)
56
+ --styleguide "<name-or-id>" Target style guide (optional, overrides config)
57
+ --tags "<t1,t2>" Comma-separated tags (optional)
58
+ --examples '<json>' JSON array of {from,to} pairs (optional)
58
59
 
59
60
  Environment:
60
61
  DITTO_API_KEY Workspace API key (required for pull).
@@ -3,6 +3,7 @@ interface CreateRuleOptions {
3
3
  description: string;
4
4
  tags?: string;
5
5
  examples?: string;
6
+ styleguide?: string;
6
7
  }
7
8
  export declare function createRule(opts: CreateRuleOptions): Promise<void>;
8
9
  export {};
@@ -7,7 +7,7 @@ async function createRule(opts) {
7
7
  const { config } = (0, config_1.loadConfig)();
8
8
  const apiKey = (0, config_1.getApiKey)();
9
9
  const api = new api_1.DittoApi(config, apiKey);
10
- const info = await api.getStyleguideInfo();
10
+ const info = await api.getStyleguideInfo(opts.styleguide);
11
11
  if (!info) {
12
12
  throw new Error("Could not discover style guide sections. Check that your workspace has at least one style guide with rules.");
13
13
  }
package/dist/config.d.ts CHANGED
@@ -4,6 +4,8 @@ export interface Config {
4
4
  roots?: string[];
5
5
  styleguides?: string[];
6
6
  locales?: string[];
7
+ defaultStyleguide?: string;
8
+ /** @deprecated Use defaultStyleguide instead. */
7
9
  styleguideId?: string;
8
10
  }
9
11
  export declare class ConfigNotFoundError extends Error {
package/dist/config.js CHANGED
@@ -30,7 +30,11 @@ function loadConfig(cwd = process.cwd()) {
30
30
  throw new Error(`${candidate}: invalid JSON. Check for syntax errors.`);
31
31
  }
32
32
  validate(parsed, candidate);
33
- return { config: parsed, root: dir };
33
+ const config = parsed;
34
+ if (config.styleguideId && !config.defaultStyleguide) {
35
+ config.defaultStyleguide = config.styleguideId;
36
+ }
37
+ return { config, root: dir };
34
38
  }
35
39
  const parent = path_1.default.dirname(dir);
36
40
  if (parent === dir)
@@ -68,10 +72,17 @@ function validate(c, source) {
68
72
  throw new Error(`${source}: every entry in "locales" must be a string.`);
69
73
  }
70
74
  }
75
+ if (obj.defaultStyleguide !== undefined) {
76
+ if (typeof obj.defaultStyleguide !== "string")
77
+ throw new Error(`${source}: "defaultStyleguide" must be a string if present.`);
78
+ }
71
79
  if (obj.styleguideId !== undefined) {
72
80
  if (typeof obj.styleguideId !== "string")
73
81
  throw new Error(`${source}: "styleguideId" must be a string if present.`);
74
82
  }
83
+ if (obj.defaultStyleguide !== undefined && obj.styleguideId !== undefined) {
84
+ throw new Error(`${source}: specify "defaultStyleguide" or "styleguideId", not both.`);
85
+ }
75
86
  }
76
87
  function getApiKey() {
77
88
  try {
package/dist/skills.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export declare const SKILL_SPEC_COMPONENT = "---\ndescription: Analyze a component, create ditto specs for it and its dependencies, and auto-fill surfaces and tags\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, and sync rules from the platform.\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 - Whether the target already has a spec (and what surfaces it declares)\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\n**If the component already has a spec**, compare the existing surfaces against what the code declares. Report any new surfaces that aren't in the spec yet, and any spec surfaces that no longer exist in the code. Propose additions/removals.\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 or modifying any files.** Let them add, remove, or modify surfaces and tags.\n\n---\n\n## Phase 3: Create or Update Specs\n\nAfter user approval:\n\n**For new specs:**\n\n1. 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` or `locales` by hand.\n\n**For existing specs getting new surfaces:**\n\n1. Edit the existing `index.ditto.md` to add the approved new surfaces and tags.\n\n**Then for all specs (new and updated):**\n\n3. Run `npx ditto-spec pull` to populate the `rules` and `locales` sections from the platform.\n4. Run `npx ditto-spec check` to validate all spec files.\n\nReport which specs were created/updated and how many rules were pulled.\n\n---\n\n## Reference: Ditto Spec Conventions\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`, `locales` (and workspace `tags`) \u2014 **never write by hand**. Populated by `ditto-spec pull`.\n- **Rule shapes**: style rules have `name`, `description`, optional `examples`, and `section`. Terminology entries have `term`, `disallowed`, `description`, and `section`.\n- **Locale-scoped rules**: the `locales` key contains rules keyed by locale code (e.g. `de-DE`). These apply in addition to base `rules` when writing for that locale.\n- **Rule hierarchy**: workspace rules (apply everywhere) \u2192 component-level rules (no `surface` field) \u2192 per-surface rules (with `surface: \"<key>\"`). Locale-scoped rules follow the same hierarchy.\n- **Parent-child layering**: if a parent passes text to a child, the parent declares a surface in its own spec. 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.\n";
2
- export declare const SKILL_SPEC_AUDIT = "---\ndescription: Audit component copy against ditto spec rules and report violations\nargument-hint: <ComponentName or path> (optional \u2014 omit to audit all)\nallowed-tools: [Read, Bash, Grep, Glob, Agent]\n---\n\n# Spec Audit\n\nEvaluate user-facing copy in component instances against the rules in their `.ditto.md` specs. Reports violations with file locations and suggested corrections.\n\n## Input\n\n$ARGUMENTS\n\nIf a component name or path is given, audit only that component. If omitted, audit all components that have specs.\n\n---\n\n## Phase 1: Load Specs\n\n1. Read `workspace.ditto.md` at the project root for workspace-level rules, tags, and locale-scoped rules.\n2. If a specific component was given, find its `index.ditto.md`. If auditing all, run `npx ditto-spec list` to discover all specced components, then read each `index.ditto.md`.\n3. For each component, build the full rule set:\n - Workspace rules (apply to all surfaces)\n - Component-level rules (no `surface` field \u2014 apply to all surfaces in this component)\n - Per-surface rules (with `surface: \"<key>\"` \u2014 apply only to that surface)\n - Locale-scoped rules from `locales` (if present, at workspace or component level)\n\n---\n\n## Phase 2: Find Instances\n\n1. Search the codebase for all files that import and render each component being audited.\n2. For each instance, read the **actual copy** being passed to the component's text surfaces:\n - String literals and template strings passed as props\n - Children rendered as text\n - Variables with obvious values (trace one level if needed)\n - Hardcoded strings in the component source itself\n\n---\n\n## Phase 3: Evaluate\n\nFor each instance, evaluate the copy against every applicable rule:\n\n- **Style rules** (`name`/`description`/`examples`): check that copy follows the described guidance. Use `examples` (before/after pairs) as concrete reference for tone, length, and shape.\n- **Terminology entries** (`term`/`disallowed`): check that copy uses the `term` form and never uses any `disallowed` alternative. Check all surface text, not just exact matches \u2014 look for the disallowed forms appearing within longer strings.\n- **`maxLength` / `minLength`**: flag any text that exceeds or falls short of the constraint.\n- **Locale-scoped rules**: if the copy targets a specific locale (identifiable from file path, i18n keys, or surrounding context), also evaluate against the matching `locales.<code>` rules.\n\nEach rule carries a `section` field (e.g. \"Voice & Tone\", \"Terminology\") \u2014 use this to group violations in the report.\n\n---\n\n## Phase 4: Report\n\nPresent violations grouped by component, then by file:\n\nFor each violation:\n- **File and line** where the instance appears\n- **Surface** the text maps to\n- **Text** that violates the rule\n- **Rule** that is violated (name or term, plus section)\n- **Suggested correction**\n\nIf no violations are found, say so explicitly \u2014 a clean audit is useful information.\n\nEnd with a summary: total components audited, total instances checked, total violations found.\n\n---\n\n## Reference: Ditto Spec Conventions\n\n- **Rule shapes**: style rules have `name`, `description`, optional `examples`, and `section`. Terminology entries have `term`, `disallowed`, `description`, and `section`.\n- **Locale-scoped rules**: the `locales` key contains rules keyed by locale code (e.g. `de-DE`). These apply in addition to base `rules` when writing for that locale.\n- **Rule hierarchy**: workspace rules (apply everywhere) \u2192 component-level rules (no `surface` field) \u2192 per-surface rules (with `surface: \"<key>\"`). Locale-scoped rules follow the same hierarchy.\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";
3
- export declare const SKILL_SPEC_GAPS = "---\ndescription: Analyze copy across component instances to find rule gaps, then create new rules on the platform\nargument-hint: <ComponentName or path> (optional \u2014 omit to analyze all)\nallowed-tools: [Read, Bash, Edit, Grep, Glob, Agent]\n---\n\n# Spec Gaps\n\nIdentify copy patterns in component instances that should be rules but aren't. Propose new style rules or terminology entries, and create approved ones on the Ditto platform.\n\n## Input\n\n$ARGUMENTS\n\nIf a component name or path is given, analyze only that component's instances. If omitted, analyze all components that have specs.\n\n---\n\n## Phase 1: Load Existing Rules\n\n1. Read `workspace.ditto.md` for workspace-level rules and tags.\n2. Read each relevant component's `index.ditto.md` for component-level and per-surface rules.\n3. Build a complete picture of what's already covered \u2014 every style rule, terminology entry, and locale-scoped rule currently in the spec files.\n\n---\n\n## Phase 2: Analyze Copy\n\n1. Search the codebase for **instances where each component is used** \u2014 find all files that import and render it.\n2. For each instance, read the **actual copy** being passed to the component's text surfaces (string literals, template strings, variables with obvious values).\n3. Analyze the copy across all instances for patterns no existing rule covers:\n - **Terminology inconsistencies**: the same concept referred to with different forms (e.g. \"sign up\" vs \"signup\" vs \"sign-up\"). These should become terminology entries.\n - **Tone mismatches**: some instances formal, others casual. These should become style rules.\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\").\n\n---\n\n## Phase 3: Propose Rules\n\nIf **no meaningful gaps** are found, say so and stop.\n\nIf gaps are found, propose new rules. Rules come in two shapes:\n\n**Style rules:**\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\n**Terminology entries:**\n- **term**: the canonical form\n- **disallowed**: list of variant forms to reject\n- **description**: why this form is preferred\n- **tags**: which tags it should apply to\n\nIf any proposed tags don't exist in the workspace yet, warn the user. Only existing workspace tags can be assigned to rules.\n\n**STOP HERE. Present proposals and ask the user which rules (if any) to create.**\n\n---\n\n## Phase 4: Create Rules\n\nFor each approved rule, run:\n\n```\nnpx ditto-spec create-rule --name \"<rule name>\" --description \"<description>\" --tags \"<tag1,tag2>\" --examples '<json array of {from,to}>'\n```\n\nIf a rule uses tags that don't exist yet, warn the user and omit those tags from the command.\n\nAfter all rules are created:\n\n1. Run `npx ditto-spec pull` to sync the new rules into spec files.\n2. Read the updated `index.ditto.md` files and verify the new rules appear.\n3. Report which rules were created and which specs they landed in.\n\n---\n\n## Reference: Ditto Spec Conventions\n\n- **Rule shapes**: style rules have `name`, `description`, optional `examples`, and `section`. Terminology entries have `term`, `disallowed`, `description`, and `section`.\n- **CLI-managed keys**: `rules`, `locales` (and workspace `tags`) \u2014 **never write by hand**. Populated by `ditto-spec pull`.\n- **Tag matching**: rules match specs by tag intersection. Any shared tag triggers the match. Rules with no tags are workspace-universal (apply to all surfaces).\n- **Rule hierarchy**: workspace rules (apply everywhere) \u2192 component-level rules (no `surface` field) \u2192 per-surface rules (with `surface: \"<key>\"`).\n";
2
+ export declare const SKILL_SPEC_AUDIT = "---\ndescription: Audit component copy against ditto spec rules and report violations\nargument-hint: <ComponentName or path> (optional \u2014 omit to audit all)\nallowed-tools: [Read, Bash, Grep, Glob, Agent]\n---\n\n# Spec Audit\n\nEvaluate user-facing copy in component instances against the rules in their `.ditto.md` specs. Reports violations with file locations and suggested corrections.\n\n## Input\n\n$ARGUMENTS\n\nIf a component name or path is given, audit only that component. If omitted, audit all components that have specs.\n\n---\n\n## Phase 1: Load Specs\n\n1. Read `workspace.ditto.md` at the project root for workspace-level rules, tags, and locale-scoped rules.\n2. If a specific component was given, find its `index.ditto.md`. If auditing all, run `npx ditto-spec list` to discover all specced components, then read each `index.ditto.md`.\n3. For each component, build the full rule set:\n - Workspace rules (apply to all surfaces)\n - Component-level rules (no `surface` field \u2014 apply to all surfaces in this component)\n - Per-surface rules (with `surface: \"<key>\"` \u2014 apply only to that surface)\n - Locale-scoped rules from `locales` (if present, at workspace or component level)\n\n---\n\n## Phase 2: Find & Resolve Instances\n\n1. Search the codebase for all files that import and render each component being audited.\n2. For each instance, resolve the **actual copy** bound to each text surface. Copy is often not an inline literal \u2014 it may be a reference to a string stored elsewhere. Follow the binding to wherever the real text lives, working down this ladder:\n\n **a. Inline** \u2014 string literals, template strings passed as props, children rendered as text, hardcoded strings in the component source, and variables with obvious values (trace one level if needed). Read the value directly.\n\n **b. i18n key** \u2014 the surface is bound to a translation lookup such as `t('dialog.headline')`, `i18n.t(...)`, `$t('key')`, `<FormattedMessage id=\"...\">`, `intl.formatMessage({ id })`, `<Trans i18nKey=\"...\">`, or a Rails `t('.key')`. Resolve it:\n - Locate the catalog file(s) by convention (see **Resolving copy sources** below) and look up the key, supporting nested/dot keys and namespaces.\n - Resolve the value in **every** available locale catalog, not just the default. Each locale's value is a separate instance to evaluate, and it activates that locale's `locales.<code>` rules.\n - Resolve **plural forms** (i18next `_one`/`_other`/`_zero`/`_many`, ICU `{count, plural, \u2026}`, `.stringsdict`, Android `<plurals>`) \u2014 evaluate each form.\n - Treat **interpolation placeholders** (`{{var}}`, `{var}`, `%s`, `%@`, `%1$s`, ICU `{name}`) as opaque tokens \u2014 audit the copy around them, never the token.\n\n **c. Ditto text item** \u2014 the surface resolves to a string managed by Ditto: a `ditto/` folder (or the `outDir` set in `ditto/config.yml`) holding `{project}___{variant}.json` files keyed by **Developer ID**, with `_one`/`_other` plural keys, a `variables.json`, and `{{variableId}}` placeholders. Resolve the referenced Developer ID to its value(s) per variant. Ditto frequently emits i18next-format JSON, so reuse the i18n resolution above \u2014 but record the source as **Ditto** for the report.\n\n **d. Unresolvable / dynamic** \u2014 the value is computed at runtime, the key is itself a variable (e.g. `t('item.' + type)`), the key or catalog can't be found, or the string is assembled from fragments. **Never skip silently.** Evaluate whatever is statically knowable (literal fragments, the message skeleton) and record an explicit **unresolved \u2014 manual review** entry naming the surface, the binding you found, and why it could not be resolved.\n\nThis skill **reads** from every source but **never modifies any of them** \u2014 inline code, i18n catalogs, and Ditto text items are each owned by their own workflow. Auditing is report-only.\n\n---\n\n## Phase 3: Evaluate\n\nFor each instance, evaluate the copy against every applicable rule:\n\n- **Style rules** (`name`/`description`/`examples`): check that copy follows the described guidance. Use `examples` (before/after pairs) as concrete reference for tone, length, and shape.\n- **Terminology entries** (`term`/`disallowed`): check that copy uses the `term` form and never uses any `disallowed` alternative. Check all surface text, not just exact matches \u2014 look for the disallowed forms appearing within longer strings.\n- **`maxLength` / `minLength`**: flag any text that exceeds or falls short of the constraint.\n- **Locale-scoped rules**: when a resolved string comes from a specific locale (identifiable from its catalog file path/name, i18n key, Ditto variant, or surrounding context), also evaluate it against the matching `locales.<code>` rules, in addition to the base rules.\n- **Interpolation placeholders**: treat `{{var}}`, `{var}`, `%s`, `%@`, `%1$s`, and ICU `{name}` tokens as opaque \u2014 evaluate the copy around them, not the token itself.\n\nEach rule carries a `section` field (e.g. \"Voice & Tone\", \"Terminology\") \u2014 use this to group violations in the report.\n\n---\n\n## Phase 4: Report\n\nPresent violations grouped by component, then by file:\n\nFor each violation:\n- **File and line** where the instance appears\n- **Surface** the text maps to\n- **Source** where the copy lives: `inline (code)`, `i18n catalog: <file>#<key> [locale]`, or `ditto text item: <developer-id> [variant]`\n- **Text** that violates the rule\n- **Rule** that is violated (name or term, plus section)\n- **Suggested correction** \u2014 for inline copy this is a code change; for i18n catalogs and Ditto text items it is **advisory only**: the string is owned by that workflow and must be changed there, not edited in component code. Never present it as a code edit, and never modify the catalog or text item.\n\nIf no violations are found, say so explicitly \u2014 a clean audit is useful information.\n\nEnd with a summary: total components audited, total instances checked, total violations found. If any surfaces could not be resolved (Phase 2d), list them under **Unresolved surfaces** with their bindings \u2014 never let a resolution gap pass silently.\n\n---\n\n## Reference: Resolving copy sources\n\nConventions for locating externally-stored copy. Infer the **locale** from the file path or name (e.g. `de-DE`, `/de/`, `__spanish`, `values-de/`) so locale-scoped rules apply.\n\n- **Ditto** \u2014 `ditto/` (or the `outDir` in `ditto/config.yml`); files `{project}___{variant}.json`, `components__{variant}.json`, plus `variables.json`. Keys are Developer IDs; plurals use `_one`/`_other` suffixes; variables are `{{variableId}}`. Output may also be Android XML or iOS `.strings`/`.stringsdict`.\n- **i18next / react-intl / FormatJS** \u2014 `locales/`, `public/locales/<lng>/<ns>.json`, `i18n/`, `lang/`, `messages.*.json`.\n- **Flutter** \u2014 `.arb` files. **gettext** \u2014 `.po`/`.pot`. **Rails** \u2014 `config/locales/*.yml`. **Apple** \u2014 `*.strings`, `*.stringsdict`, `*.xcstrings`. **Android** \u2014 `res/values*/strings.xml`.\n\nWhen key resolution is ambiguous (e.g. multiple catalogs or namespaces in a monorepo), prefer the catalog nearest the importing instance or matching the configured namespace; if still ambiguous, mark the surface unresolved (Phase 2d) rather than guessing. Look copy up by key \u2014 do not read entire catalogs into context.\n\n---\n\n## Reference: Ditto Spec Conventions\n\n- **Rule shapes**: style rules have `name`, `description`, optional `examples`, and `section`. Terminology entries have `term`, `disallowed`, `description`, and `section`.\n- **Locale-scoped rules**: the `locales` key contains rules keyed by locale code (e.g. `de-DE`). These apply in addition to base `rules` when writing for that locale.\n- **Rule hierarchy**: workspace rules (apply everywhere) \u2192 component-level rules (no `surface` field) \u2192 per-surface rules (with `surface: \"<key>\"`). Locale-scoped rules follow the same hierarchy.\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";
3
+ export declare const SKILL_SPEC_GAPS = "---\ndescription: Analyze copy across component instances to find rule gaps, then create new rules on the platform\nargument-hint: <ComponentName or path> (optional \u2014 omit to analyze all)\nallowed-tools: [Read, Bash, Edit, Grep, Glob, Agent]\n---\n\n# Spec Gaps\n\nIdentify copy patterns in component instances that should be rules but aren't. Propose new style rules or terminology entries, and create approved ones on the Ditto platform.\n\n## Input\n\n$ARGUMENTS\n\nIf a component name or path is given, analyze only that component's instances. If omitted, analyze all components that have specs.\n\n---\n\n## Phase 1: Load Existing Rules\n\n1. Read `workspace.ditto.md` for workspace-level rules and tags.\n2. Read each relevant component's `index.ditto.md` for component-level and per-surface rules.\n3. Build a complete picture of what's already covered \u2014 every style rule, terminology entry, and locale-scoped rule currently in the spec files.\n\n---\n\n## Phase 2: Analyze Copy\n\n1. Search the codebase for **instances where each component is used** \u2014 find all files that import and render it.\n2. For each instance, resolve the **actual copy** bound to each text surface \u2014 following the binding to wherever the string actually lives, not just inline literals:\n - **Inline** \u2014 string literals, template strings, children, and variables with obvious values.\n - **i18n key** \u2014 a lookup like `t('key')`, `<FormattedMessage id=\"...\">`, or `$t('key')`: resolve the key in the catalog file(s) (`locales/`, `public/locales/<lng>/<ns>.json`, `.arb`, `.po`, `config/locales/*.yml`, `*.strings`, `res/values*/strings.xml`), across **all** locales, including plural forms. Look copy up by key \u2014 do not read entire catalogs into context.\n - **Ditto text item** \u2014 a string in a `ditto/` folder (`{project}___{variant}.json` keyed by Developer ID, plurals suffixed `_one`/`_other`, variables as `{{variableId}}`); resolve per variant.\n - **Unresolvable / dynamic** \u2014 runtime-computed values or missing keys: analyze what is statically knowable and skip the rest. This skill only **reads** these sources; it never modifies catalogs or text items.\n3. Analyze the copy across all instances for patterns no existing rule covers:\n - **Terminology inconsistencies**: the same concept referred to with different forms (e.g. \"sign up\" vs \"signup\" vs \"sign-up\"). These should become terminology entries.\n - **Tone mismatches**: some instances formal, others casual. These should become style rules.\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\").\n\n---\n\n## Phase 3: Propose Rules\n\nIf **no meaningful gaps** are found, say so and stop.\n\nIf gaps are found, propose new rules. Rules come in two shapes:\n\n**Style rules:**\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\n**Terminology entries:**\n- **term**: the canonical form\n- **disallowed**: list of variant forms to reject\n- **description**: why this form is preferred\n- **tags**: which tags it should apply to\n\nIf any proposed tags don't exist in the workspace yet, warn the user. Only existing workspace tags can be assigned to rules.\n\n**STOP HERE. Present proposals and ask the user which rules (if any) to create.**\n\n---\n\n## Phase 4: Create Rules\n\nBefore creating any rules, determine which style guide to target:\n\n1. Check `dittospec.config.json` for a `defaultStyleguide` value.\n2. If present, tell the user which style guide will be used and ask for confirmation.\n3. If absent (or the user wants a different one), ask the user for the exact style guide name to create the rules in \u2014 it must match a style guide that exists in the workspace (visible on the Ditto platform). `create-rule` rejects an unrecognized `--styleguide` value and lists the valid names, so a wrong name fails loudly instead of silently targeting the wrong guide.\n\nFor each approved rule, run:\n\n```\nnpx ditto-spec create-rule --name \"<rule name>\" --description \"<description>\" --styleguide \"<chosen style guide>\" --tags \"<tag1,tag2>\" --examples '<json array of {from,to}>'\n```\n\nIf a rule uses tags that don't exist yet, warn the user and omit those tags from the command.\n\nAfter all rules are created:\n\n1. Run `npx ditto-spec pull` to sync the new rules into spec files.\n2. Read the updated `index.ditto.md` files and verify the new rules appear.\n3. Report which rules were created and which specs they landed in.\n\n---\n\n## Reference: Ditto Spec Conventions\n\n- **Rule shapes**: style rules have `name`, `description`, optional `examples`, and `section`. Terminology entries have `term`, `disallowed`, `description`, and `section`.\n- **CLI-managed keys**: `rules`, `locales` (and workspace `tags`) \u2014 **never write by hand**. Populated by `ditto-spec pull`.\n- **Tag matching**: rules match specs by tag intersection. Any shared tag triggers the match. Rules with no tags are workspace-universal (apply to all surfaces).\n- **Rule hierarchy**: workspace rules (apply everywhere) \u2192 component-level rules (no `surface` field) \u2192 per-surface rules (with `surface: \"<key>\"`).\n";
4
4
  export declare const SKILLS: Record<string, string>;
package/dist/skills.js CHANGED
@@ -136,14 +136,24 @@ If a component name or path is given, audit only that component. If omitted, aud
136
136
 
137
137
  ---
138
138
 
139
- ## Phase 2: Find Instances
139
+ ## Phase 2: Find & Resolve Instances
140
140
 
141
141
  1. Search the codebase for all files that import and render each component being audited.
142
- 2. For each instance, read the **actual copy** being passed to the component's text surfaces:
143
- - String literals and template strings passed as props
144
- - Children rendered as text
145
- - Variables with obvious values (trace one level if needed)
146
- - Hardcoded strings in the component source itself
142
+ 2. For each instance, resolve the **actual copy** bound to each text surface. Copy is often not an inline literal — it may be a reference to a string stored elsewhere. Follow the binding to wherever the real text lives, working down this ladder:
143
+
144
+ **a. Inline** — string literals, template strings passed as props, children rendered as text, hardcoded strings in the component source, and variables with obvious values (trace one level if needed). Read the value directly.
145
+
146
+ **b. i18n key** the surface is bound to a translation lookup such as \`t('dialog.headline')\`, \`i18n.t(...)\`, \`$t('key')\`, \`<FormattedMessage id="...">\`, \`intl.formatMessage({ id })\`, \`<Trans i18nKey="...">\`, or a Rails \`t('.key')\`. Resolve it:
147
+ - Locate the catalog file(s) by convention (see **Resolving copy sources** below) and look up the key, supporting nested/dot keys and namespaces.
148
+ - Resolve the value in **every** available locale catalog, not just the default. Each locale's value is a separate instance to evaluate, and it activates that locale's \`locales.<code>\` rules.
149
+ - Resolve **plural forms** (i18next \`_one\`/\`_other\`/\`_zero\`/\`_many\`, ICU \`{count, plural, …}\`, \`.stringsdict\`, Android \`<plurals>\`) — evaluate each form.
150
+ - Treat **interpolation placeholders** (\`{{var}}\`, \`{var}\`, \`%s\`, \`%@\`, \`%1$s\`, ICU \`{name}\`) as opaque tokens — audit the copy around them, never the token.
151
+
152
+ **c. Ditto text item** — the surface resolves to a string managed by Ditto: a \`ditto/\` folder (or the \`outDir\` set in \`ditto/config.yml\`) holding \`{project}___{variant}.json\` files keyed by **Developer ID**, with \`_one\`/\`_other\` plural keys, a \`variables.json\`, and \`{{variableId}}\` placeholders. Resolve the referenced Developer ID to its value(s) per variant. Ditto frequently emits i18next-format JSON, so reuse the i18n resolution above — but record the source as **Ditto** for the report.
153
+
154
+ **d. Unresolvable / dynamic** — the value is computed at runtime, the key is itself a variable (e.g. \`t('item.' + type)\`), the key or catalog can't be found, or the string is assembled from fragments. **Never skip silently.** Evaluate whatever is statically knowable (literal fragments, the message skeleton) and record an explicit **unresolved — manual review** entry naming the surface, the binding you found, and why it could not be resolved.
155
+
156
+ This skill **reads** from every source but **never modifies any of them** — inline code, i18n catalogs, and Ditto text items are each owned by their own workflow. Auditing is report-only.
147
157
 
148
158
  ---
149
159
 
@@ -154,7 +164,8 @@ For each instance, evaluate the copy against every applicable rule:
154
164
  - **Style rules** (\`name\`/\`description\`/\`examples\`): check that copy follows the described guidance. Use \`examples\` (before/after pairs) as concrete reference for tone, length, and shape.
155
165
  - **Terminology entries** (\`term\`/\`disallowed\`): check that copy uses the \`term\` form and never uses any \`disallowed\` alternative. Check all surface text, not just exact matches — look for the disallowed forms appearing within longer strings.
156
166
  - **\`maxLength\` / \`minLength\`**: flag any text that exceeds or falls short of the constraint.
157
- - **Locale-scoped rules**: if the copy targets a specific locale (identifiable from file path, i18n keys, or surrounding context), also evaluate against the matching \`locales.<code>\` rules.
167
+ - **Locale-scoped rules**: when a resolved string comes from a specific locale (identifiable from its catalog file path/name, i18n key, Ditto variant, or surrounding context), also evaluate it against the matching \`locales.<code>\` rules, in addition to the base rules.
168
+ - **Interpolation placeholders**: treat \`{{var}}\`, \`{var}\`, \`%s\`, \`%@\`, \`%1$s\`, and ICU \`{name}\` tokens as opaque — evaluate the copy around them, not the token itself.
158
169
 
159
170
  Each rule carries a \`section\` field (e.g. "Voice & Tone", "Terminology") — use this to group violations in the report.
160
171
 
@@ -167,13 +178,26 @@ Present violations grouped by component, then by file:
167
178
  For each violation:
168
179
  - **File and line** where the instance appears
169
180
  - **Surface** the text maps to
181
+ - **Source** where the copy lives: \`inline (code)\`, \`i18n catalog: <file>#<key> [locale]\`, or \`ditto text item: <developer-id> [variant]\`
170
182
  - **Text** that violates the rule
171
183
  - **Rule** that is violated (name or term, plus section)
172
- - **Suggested correction**
184
+ - **Suggested correction** — for inline copy this is a code change; for i18n catalogs and Ditto text items it is **advisory only**: the string is owned by that workflow and must be changed there, not edited in component code. Never present it as a code edit, and never modify the catalog or text item.
173
185
 
174
186
  If no violations are found, say so explicitly — a clean audit is useful information.
175
187
 
176
- End with a summary: total components audited, total instances checked, total violations found.
188
+ End with a summary: total components audited, total instances checked, total violations found. If any surfaces could not be resolved (Phase 2d), list them under **Unresolved surfaces** with their bindings — never let a resolution gap pass silently.
189
+
190
+ ---
191
+
192
+ ## Reference: Resolving copy sources
193
+
194
+ Conventions for locating externally-stored copy. Infer the **locale** from the file path or name (e.g. \`de-DE\`, \`/de/\`, \`__spanish\`, \`values-de/\`) so locale-scoped rules apply.
195
+
196
+ - **Ditto** — \`ditto/\` (or the \`outDir\` in \`ditto/config.yml\`); files \`{project}___{variant}.json\`, \`components__{variant}.json\`, plus \`variables.json\`. Keys are Developer IDs; plurals use \`_one\`/\`_other\` suffixes; variables are \`{{variableId}}\`. Output may also be Android XML or iOS \`.strings\`/\`.stringsdict\`.
197
+ - **i18next / react-intl / FormatJS** — \`locales/\`, \`public/locales/<lng>/<ns>.json\`, \`i18n/\`, \`lang/\`, \`messages.*.json\`.
198
+ - **Flutter** — \`.arb\` files. **gettext** — \`.po\`/\`.pot\`. **Rails** — \`config/locales/*.yml\`. **Apple** — \`*.strings\`, \`*.stringsdict\`, \`*.xcstrings\`. **Android** — \`res/values*/strings.xml\`.
199
+
200
+ When key resolution is ambiguous (e.g. multiple catalogs or namespaces in a monorepo), prefer the catalog nearest the importing instance or matching the configured namespace; if still ambiguous, mark the surface unresolved (Phase 2d) rather than guessing. Look copy up by key — do not read entire catalogs into context.
177
201
 
178
202
  ---
179
203
 
@@ -213,7 +237,11 @@ If a component name or path is given, analyze only that component's instances. I
213
237
  ## Phase 2: Analyze Copy
214
238
 
215
239
  1. Search the codebase for **instances where each component is used** — find all files that import and render it.
216
- 2. For each instance, read the **actual copy** being passed to the component's text surfaces (string literals, template strings, variables with obvious values).
240
+ 2. For each instance, resolve the **actual copy** bound to each text surface — following the binding to wherever the string actually lives, not just inline literals:
241
+ - **Inline** — string literals, template strings, children, and variables with obvious values.
242
+ - **i18n key** — a lookup like \`t('key')\`, \`<FormattedMessage id="...">\`, or \`$t('key')\`: resolve the key in the catalog file(s) (\`locales/\`, \`public/locales/<lng>/<ns>.json\`, \`.arb\`, \`.po\`, \`config/locales/*.yml\`, \`*.strings\`, \`res/values*/strings.xml\`), across **all** locales, including plural forms. Look copy up by key — do not read entire catalogs into context.
243
+ - **Ditto text item** — a string in a \`ditto/\` folder (\`{project}___{variant}.json\` keyed by Developer ID, plurals suffixed \`_one\`/\`_other\`, variables as \`{{variableId}}\`); resolve per variant.
244
+ - **Unresolvable / dynamic** — runtime-computed values or missing keys: analyze what is statically knowable and skip the rest. This skill only **reads** these sources; it never modifies catalogs or text items.
217
245
  3. Analyze the copy across all instances for patterns no existing rule covers:
218
246
  - **Terminology inconsistencies**: the same concept referred to with different forms (e.g. "sign up" vs "signup" vs "sign-up"). These should become terminology entries.
219
247
  - **Tone mismatches**: some instances formal, others casual. These should become style rules.
@@ -248,10 +276,16 @@ If any proposed tags don't exist in the workspace yet, warn the user. Only exist
248
276
 
249
277
  ## Phase 4: Create Rules
250
278
 
279
+ Before creating any rules, determine which style guide to target:
280
+
281
+ 1. Check \`dittospec.config.json\` for a \`defaultStyleguide\` value.
282
+ 2. If present, tell the user which style guide will be used and ask for confirmation.
283
+ 3. If absent (or the user wants a different one), ask the user for the exact style guide name to create the rules in — it must match a style guide that exists in the workspace (visible on the Ditto platform). \`create-rule\` rejects an unrecognized \`--styleguide\` value and lists the valid names, so a wrong name fails loudly instead of silently targeting the wrong guide.
284
+
251
285
  For each approved rule, run:
252
286
 
253
287
  \`\`\`
254
- npx ditto-spec create-rule --name "<rule name>" --description "<description>" --tags "<tag1,tag2>" --examples '<json array of {from,to}>'
288
+ npx ditto-spec create-rule --name "<rule name>" --description "<description>" --styleguide "<chosen style guide>" --tags "<tag1,tag2>" --examples '<json array of {from,to}>'
255
289
  \`\`\`
256
290
 
257
291
  If a rule uses tags that don't exist yet, warn the user and omit those tags from the command.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dittowords/spec-cli",
3
- "version": "0.0.1-alpha.10",
3
+ "version": "0.0.1-alpha.12",
4
4
  "description": "CLI for syncing .ditto.md content specs with the Ditto platform.",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {