@dittowords/spec-cli 0.0.1-alpha.14 → 0.0.1-alpha.15
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 +25 -9
- package/dist/api.d.ts +27 -14
- package/dist/api.js +61 -29
- package/dist/cli.js +45 -21
- package/dist/commands/create-rules.d.ts +7 -0
- package/dist/commands/create-rules.js +156 -0
- package/dist/commands/init.js +3 -2
- package/dist/commands/rules.d.ts +5 -0
- package/dist/commands/rules.js +42 -0
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +15 -5
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -147,21 +147,37 @@ Validates all spec files: YAML parses correctly, required keys are present, surf
|
|
|
147
147
|
|
|
148
148
|
Prints an inventory of all component specs with their surfaces, tags, and constraints.
|
|
149
149
|
|
|
150
|
-
### `ditto-spec
|
|
150
|
+
### `ditto-spec rules`
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
Prints every rule on the platform, grouped by style guide and section (with each section's `sectionId` and kind). Pass `--styleguide "<name-or-id>"` to limit to one guide.
|
|
153
|
+
|
|
154
|
+
### `ditto-spec create-rules`
|
|
155
|
+
|
|
156
|
+
Creates a batch of rules on the Ditto platform. Takes a JSON array of rules on stdin (or via `--file <path>`):
|
|
153
157
|
|
|
154
158
|
```
|
|
155
|
-
ditto-spec create-
|
|
159
|
+
ditto-spec create-rules <<'EOF'
|
|
160
|
+
[
|
|
161
|
+
{"name": "Use active voice", "description": "Lead CTAs with a verb", "tags": ["button", "call-to-action"], "examples": [{"from": "Your settings", "to": "Open settings"}], "section": "UI Patterns"},
|
|
162
|
+
{"term": "sign up", "disallowed": ["signup", "sign-up"], "description": "Two words as a verb", "section": "Word List"}
|
|
163
|
+
]
|
|
164
|
+
EOF
|
|
156
165
|
```
|
|
157
166
|
|
|
167
|
+
Each rule is one of two shapes:
|
|
168
|
+
|
|
169
|
+
| Shape | Fields |
|
|
170
|
+
|---|---|
|
|
171
|
+
| Style rule | `name` (required), `description`, `examples` (array of `{from, to}`), `tags`, `section` |
|
|
172
|
+
| Terminology entry | `term` (required), `disallowed` (array of strings), `description`, `tags`, `section` |
|
|
173
|
+
|
|
174
|
+
Shapes can be mixed in one batch. Each rule's optional `section` (name or ID, as shown by `ditto-spec rules`) maps it to an existing section of the style guide; the section's kind must match the rule's shape (`rules` sections for style rules, `wordlist` sections for terminology entries). Rules without a `section` go to the first section of the matching kind. One API call is made per target section.
|
|
175
|
+
|
|
158
176
|
| Flag | Description |
|
|
159
177
|
|---|---|
|
|
160
|
-
| `--
|
|
161
|
-
| `--description` | What the rule enforces (required) |
|
|
178
|
+
| `--file <path>` | Read the JSON array from a file instead of stdin |
|
|
162
179
|
| `--styleguide` | Target style guide name or ID (optional, overrides `defaultStyleguide` config) |
|
|
163
|
-
| `--
|
|
164
|
-
| `--examples` | JSON array of `{from, to}` pairs (optional) |
|
|
180
|
+
| `--section` | Default section for rules that don't specify their own (optional; applies only to rules matching the section's kind — others fall back to the first section of theirs) |
|
|
165
181
|
|
|
166
182
|
After creating rules, run `ditto-spec pull` to sync them into spec files.
|
|
167
183
|
|
|
@@ -184,7 +200,7 @@ Create `dittospec.config.json` at your repo root (or any ancestor directory):
|
|
|
184
200
|
| `roots` | Repo-relative directories to search for `.ditto.md` files. Defaults to `["."]`. |
|
|
185
201
|
| `styleguides` | Optional list of style guide names or IDs to pull. Defaults to all. |
|
|
186
202
|
| `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. |
|
|
187
|
-
| `defaultStyleguide` | Optional style guide name or ID for `create-
|
|
203
|
+
| `defaultStyleguide` | Optional style guide name or ID for `create-rules`. Overridable with the `--styleguide` flag. Defaults to the first guide returned by the API. |
|
|
188
204
|
|
|
189
205
|
Set `DITTO_API_KEY` in your environment or in a `.env` file at the repo root.
|
|
190
206
|
|
|
@@ -196,7 +212,7 @@ When you run `ditto-spec init --agent` in a Claude Code project, the CLI writes
|
|
|
196
212
|
|---|---|
|
|
197
213
|
| `/spec-component <Name>` | Analyze a component's text surfaces, scaffold a `.ditto.md` spec (or update an existing one), and sync rules from the platform. Handles child components too. |
|
|
198
214
|
| `/spec-audit [Name]` | Audit copy in component instances against spec rules. Reports violations with file locations and suggested corrections. Omit the name to audit all specced components. |
|
|
199
|
-
| `/spec-gaps [Name]` | Find copy patterns that should be rules but aren't. Proposes new style rules and terminology entries, then creates approved ones on the platform via `create-
|
|
215
|
+
| `/spec-gaps [Name]` | Find copy patterns that should be rules but aren't. Proposes new style rules and terminology entries, then creates approved ones on the platform via `create-rules`. |
|
|
200
216
|
|
|
201
217
|
Skills are written into your repo and committed alongside your specs and config. Every team member gets them automatically — no separate plugin install.
|
|
202
218
|
|
package/dist/api.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface WordlistEntry {
|
|
|
20
20
|
tags: string[];
|
|
21
21
|
}
|
|
22
22
|
export interface StyleguideSection {
|
|
23
|
+
sectionId?: string;
|
|
23
24
|
name: string;
|
|
24
25
|
kind: "rules" | "wordlist";
|
|
25
26
|
rules: StyleRule[] | WordlistEntry[];
|
|
@@ -54,28 +55,40 @@ export type FlatRule = {
|
|
|
54
55
|
tags: string[];
|
|
55
56
|
};
|
|
56
57
|
export declare function flattenStyleguides(guides: Styleguide[], filter?: string[]): FlatRule[];
|
|
58
|
+
export type NewStyleRule = {
|
|
59
|
+
name: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
examples?: {
|
|
62
|
+
from: string;
|
|
63
|
+
to: string;
|
|
64
|
+
}[];
|
|
65
|
+
tags?: string[];
|
|
66
|
+
};
|
|
67
|
+
export type NewWordlistEntry = {
|
|
68
|
+
term: string;
|
|
69
|
+
disallowed?: string[];
|
|
70
|
+
description?: string;
|
|
71
|
+
tags?: string[];
|
|
72
|
+
};
|
|
73
|
+
export type NewRule = NewStyleRule | NewWordlistEntry;
|
|
74
|
+
export declare function resolveStyleguide(guides: Styleguide[], config: Config, override?: string): Styleguide;
|
|
75
|
+
export type ResolvedSection = StyleguideSection & {
|
|
76
|
+
sectionId: string;
|
|
77
|
+
};
|
|
78
|
+
export declare function resolveSection(guide: Styleguide, kind: "rules" | "wordlist", override?: string, opts?: {
|
|
79
|
+
softKindFallback?: boolean;
|
|
80
|
+
}): ResolvedSection;
|
|
57
81
|
export declare class DittoApi {
|
|
58
82
|
private readonly config;
|
|
59
83
|
private readonly apiKey;
|
|
60
84
|
constructor(config: Config, apiKey: string);
|
|
61
85
|
getStyleguides(): Promise<Styleguide[]>;
|
|
62
|
-
|
|
63
|
-
styleguideId: string;
|
|
64
|
-
sectionId: string;
|
|
65
|
-
} | null>;
|
|
66
|
-
createRule(opts: {
|
|
86
|
+
createRules(opts: {
|
|
67
87
|
styleguideId: string;
|
|
68
88
|
sectionId: string;
|
|
69
|
-
|
|
70
|
-
description: string;
|
|
71
|
-
examples?: {
|
|
72
|
-
from: string;
|
|
73
|
-
to: string;
|
|
74
|
-
}[];
|
|
75
|
-
tags?: string[];
|
|
89
|
+
rules: NewRule[];
|
|
76
90
|
}): Promise<{
|
|
77
|
-
|
|
78
|
-
name: string;
|
|
91
|
+
ids: string[];
|
|
79
92
|
}>;
|
|
80
93
|
private fetch;
|
|
81
94
|
private headers;
|
package/dist/api.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DittoApi = void 0;
|
|
4
4
|
exports.flattenStyleguides = flattenStyleguides;
|
|
5
|
+
exports.resolveStyleguide = resolveStyleguide;
|
|
6
|
+
exports.resolveSection = resolveSection;
|
|
5
7
|
function flattenStyleguides(guides, filter) {
|
|
6
8
|
const selected = filter?.length
|
|
7
9
|
? guides.filter((g) => filter.includes(g.name) || filter.includes(g.developerId))
|
|
@@ -41,6 +43,53 @@ function flattenStyleguides(guides, filter) {
|
|
|
41
43
|
}
|
|
42
44
|
return result;
|
|
43
45
|
}
|
|
46
|
+
function resolveStyleguide(guides, config, override) {
|
|
47
|
+
if (guides.length === 0) {
|
|
48
|
+
throw new Error("No style guides found in this workspace. Create one on the Ditto platform first.");
|
|
49
|
+
}
|
|
50
|
+
const target = override ?? config.defaultStyleguide;
|
|
51
|
+
if (!target)
|
|
52
|
+
return guides[0];
|
|
53
|
+
const match = guides.find((g) => g.developerId === target || g.name === target);
|
|
54
|
+
if (!match) {
|
|
55
|
+
const available = guides.map((g) => `${g.developerId} (${g.name})`).join(", ");
|
|
56
|
+
throw new Error(`Style guide "${target}" not found. Available: ${available}`);
|
|
57
|
+
}
|
|
58
|
+
return match;
|
|
59
|
+
}
|
|
60
|
+
function resolveSection(guide, kind, override,
|
|
61
|
+
// With softKindFallback, an override that only matches sections of the other
|
|
62
|
+
// kind falls back to the first section of the requested kind instead of
|
|
63
|
+
// throwing — used for the --section default, which applies per kind.
|
|
64
|
+
opts) {
|
|
65
|
+
const describe = (s) => s.sectionId ? `${s.sectionId} (${s.name}, ${s.kind})` : `${s.name} (${s.kind})`;
|
|
66
|
+
const withId = (s) => {
|
|
67
|
+
if (!s.sectionId) {
|
|
68
|
+
throw new Error(`Section "${s.name}" has no sectionId in the API response. ` +
|
|
69
|
+
"Rule creation needs an API that exposes sectionId on GET /v2/styleguides — the server may not be updated yet.");
|
|
70
|
+
}
|
|
71
|
+
return s;
|
|
72
|
+
};
|
|
73
|
+
if (override) {
|
|
74
|
+
const matches = guide.sections.filter((s) => s.sectionId === override || s.name === override);
|
|
75
|
+
if (matches.length === 0) {
|
|
76
|
+
const available = guide.sections.map(describe).join(", ") || "none";
|
|
77
|
+
throw new Error(`Section "${override}" not found in style guide "${guide.name}". Available: ${available}`);
|
|
78
|
+
}
|
|
79
|
+
const match = matches.find((s) => s.kind === kind);
|
|
80
|
+
if (match)
|
|
81
|
+
return withId(match);
|
|
82
|
+
if (!opts?.softKindFallback) {
|
|
83
|
+
throw new Error(`Section "${matches[0].name}" is a ${matches[0].kind} section, but the rules being created need a ${kind} section.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const match = guide.sections.find((s) => s.kind === kind);
|
|
87
|
+
if (!match) {
|
|
88
|
+
const available = guide.sections.map(describe).join(", ") || "none";
|
|
89
|
+
throw new Error(`Style guide "${guide.name}" has no ${kind} section. Available sections: ${available}`);
|
|
90
|
+
}
|
|
91
|
+
return withId(match);
|
|
92
|
+
}
|
|
44
93
|
class DittoApi {
|
|
45
94
|
constructor(config, apiKey) {
|
|
46
95
|
this.config = config;
|
|
@@ -52,41 +101,24 @@ class DittoApi {
|
|
|
52
101
|
const data = (await res.json());
|
|
53
102
|
return data.styleguides;
|
|
54
103
|
}
|
|
55
|
-
async
|
|
56
|
-
const
|
|
57
|
-
if (guides.length === 0)
|
|
58
|
-
return null;
|
|
59
|
-
const target = styleguideOverride ?? this.config.defaultStyleguide;
|
|
60
|
-
let guide;
|
|
61
|
-
if (target) {
|
|
62
|
-
const match = guides.find((g) => g.developerId === target || g.name === target);
|
|
63
|
-
if (!match) {
|
|
64
|
-
const available = guides.map((g) => `${g.developerId} (${g.name})`).join(", ");
|
|
65
|
-
throw new Error(`Style guide "${target}" not found. Available: ${available}`);
|
|
66
|
-
}
|
|
67
|
-
guide = match;
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
guide = guides[0];
|
|
71
|
-
}
|
|
72
|
-
if (guide.sections.length === 0)
|
|
73
|
-
return null;
|
|
74
|
-
const section = guide.sections.find((s) => s.kind === "rules") ?? guide.sections[0];
|
|
75
|
-
return { styleguideId: guide.developerId, sectionId: section.name };
|
|
76
|
-
}
|
|
77
|
-
async createRule(opts) {
|
|
78
|
-
const url = new URL(`/v2/styleguides/${encodeURIComponent(opts.styleguideId)}/rules`, this.config.apiBase);
|
|
104
|
+
async createRules(opts) {
|
|
105
|
+
const url = new URL("/v2/styleguides/rules", this.config.apiBase);
|
|
79
106
|
const res = await this.fetch(url, {
|
|
80
107
|
method: "POST",
|
|
81
108
|
body: JSON.stringify({
|
|
109
|
+
styleguideId: opts.styleguideId,
|
|
82
110
|
sectionId: opts.sectionId,
|
|
83
|
-
|
|
84
|
-
description: opts.description,
|
|
85
|
-
examples: opts.examples,
|
|
86
|
-
tags: opts.tags,
|
|
111
|
+
rules: opts.rules,
|
|
87
112
|
}),
|
|
88
113
|
});
|
|
89
|
-
|
|
114
|
+
// Tolerate a response without a well-formed ids array (proxy body,
|
|
115
|
+
// contract drift): the rules were created — HTTP errors throw in fetch —
|
|
116
|
+
// so report success with whatever ids are usable rather than crashing.
|
|
117
|
+
const data = (await res.json().catch(() => null));
|
|
118
|
+
const ids = data && Array.isArray(data.ids) && data.ids.every((x) => typeof x === "string")
|
|
119
|
+
? data.ids
|
|
120
|
+
: [];
|
|
121
|
+
return { ids };
|
|
90
122
|
}
|
|
91
123
|
async fetch(url, init) {
|
|
92
124
|
const res = await fetch(url.toString(), {
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const check_1 = require("./commands/check");
|
|
4
|
-
const
|
|
4
|
+
const create_rules_1 = require("./commands/create-rules");
|
|
5
5
|
const init_1 = require("./commands/init");
|
|
6
6
|
const list_1 = require("./commands/list");
|
|
7
7
|
const pull_1 = require("./commands/pull");
|
|
8
|
+
const rules_1 = require("./commands/rules");
|
|
8
9
|
const scaffold_1 = require("./commands/scaffold");
|
|
10
|
+
function getFlag(args, flag) {
|
|
11
|
+
const idx = args.indexOf(flag);
|
|
12
|
+
if (idx === -1)
|
|
13
|
+
return undefined;
|
|
14
|
+
const value = args[idx + 1];
|
|
15
|
+
if (value === undefined || value.startsWith("--")) {
|
|
16
|
+
process.stderr.write(`Missing value for ${flag}.\n`);
|
|
17
|
+
process.exit(2);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
9
21
|
const COMMANDS = {
|
|
10
22
|
init: async (args) => (0, init_1.init)({ writeAgent: args.includes("--agent"), force: args.includes("--force") }),
|
|
11
23
|
pull: async (args) => (0, pull_1.pull)({ dryRun: args.includes("--dry-run") }),
|
|
@@ -17,22 +29,28 @@ const COMMANDS = {
|
|
|
17
29
|
process.stderr.write("Usage: ditto-spec scaffold <ComponentName> [--path <dir>]\n");
|
|
18
30
|
process.exit(2);
|
|
19
31
|
}
|
|
20
|
-
|
|
21
|
-
const targetDir = pathIdx !== -1 && args[pathIdx + 1] ? args[pathIdx + 1] : process.cwd();
|
|
22
|
-
return (0, scaffold_1.scaffold)({ componentName: name, targetDir });
|
|
32
|
+
return (0, scaffold_1.scaffold)({ componentName: name, targetDir: getFlag(args, "--path") ?? process.cwd() });
|
|
23
33
|
},
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
rules: async (args) => {
|
|
35
|
+
return (0, rules_1.rules)({ styleguide: getFlag(args, "--styleguide") });
|
|
36
|
+
},
|
|
37
|
+
"create-rules": async (args) => {
|
|
38
|
+
return (0, create_rules_1.createRules)({
|
|
39
|
+
file: getFlag(args, "--file"),
|
|
40
|
+
styleguide: getFlag(args, "--styleguide"),
|
|
41
|
+
section: getFlag(args, "--section"),
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
// Removed in favor of create-rules; skill files generated by older CLI
|
|
45
|
+
// versions still invoke it, so fail with a pointer instead of "Unknown command".
|
|
46
|
+
"create-rule": async () => {
|
|
47
|
+
process.stderr.write("create-rule has been replaced by create-rules, which takes a JSON array of rules on stdin:\n\n" +
|
|
48
|
+
" ditto-spec create-rules <<'EOF'\n" +
|
|
49
|
+
' [{"name": "<rule name>", "description": "<desc>", "tags": ["tag1"], "examples": [{"from": "...", "to": "..."}]}]\n' +
|
|
50
|
+
" EOF\n\n" +
|
|
51
|
+
"Run `ditto-spec --help` for the full input format, and\n" +
|
|
52
|
+
"`ditto-spec init --agent --force` to refresh skill files generated by an older version.\n");
|
|
53
|
+
process.exit(2);
|
|
36
54
|
},
|
|
37
55
|
};
|
|
38
56
|
const HELP = `ditto-spec — sync .ditto.md content specs with the Ditto platform.
|
|
@@ -50,12 +68,18 @@ Commands:
|
|
|
50
68
|
pull --dry-run Show which files would change without writing.
|
|
51
69
|
check Parse every spec file; exit non-zero on any malformed file.
|
|
52
70
|
list Print every component spec with its surfaces and tags.
|
|
53
|
-
|
|
54
|
-
--
|
|
55
|
-
|
|
71
|
+
rules Print every rule on the platform, grouped by style guide and section.
|
|
72
|
+
--styleguide "<name-or-id>" Limit to one style guide (optional)
|
|
73
|
+
create-rules Create rules on the platform from a JSON array (stdin or --file).
|
|
74
|
+
Each rule is either a style rule {name, description?, examples?, tags?, section?}
|
|
75
|
+
or a terminology entry {term, disallowed?, description?, tags?, section?}.
|
|
76
|
+
A rule's "section" (name or id) maps it to an existing section of the style
|
|
77
|
+
guide; the section's kind must match the rule's shape ("rules" for style
|
|
78
|
+
rules, "wordlist" for terminology). Omitted: first section of matching kind.
|
|
79
|
+
--file <path> Read the JSON array from a file instead of stdin
|
|
56
80
|
--styleguide "<name-or-id>" Target style guide (optional, overrides config)
|
|
57
|
-
--
|
|
58
|
-
|
|
81
|
+
--section "<name-or-id>" Default section for rules without their own (optional;
|
|
82
|
+
applies only to rules matching the section's kind)
|
|
59
83
|
|
|
60
84
|
Environment:
|
|
61
85
|
DITTO_API_KEY Workspace API key (required for pull).
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRules = createRules;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const api_1 = require("../api");
|
|
6
|
+
const config_1 = require("../config");
|
|
7
|
+
function readInput(file) {
|
|
8
|
+
if (file)
|
|
9
|
+
return (0, node_fs_1.readFileSync)(file, "utf8");
|
|
10
|
+
if (process.stdin.isTTY) {
|
|
11
|
+
throw new Error("No input. Pipe a JSON array of rules on stdin or pass --file <path>.\n" +
|
|
12
|
+
'Example: ditto-spec create-rules <<\'EOF\'\n[{"name": "Use sentence case", "description": "..."}]\nEOF');
|
|
13
|
+
}
|
|
14
|
+
return (0, node_fs_1.readFileSync)(0, "utf8");
|
|
15
|
+
}
|
|
16
|
+
function isStringArray(v) {
|
|
17
|
+
return Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
18
|
+
}
|
|
19
|
+
function validateRule(rule, index) {
|
|
20
|
+
const at = `Rule at index ${index}`;
|
|
21
|
+
if (typeof rule !== "object" || rule === null || Array.isArray(rule)) {
|
|
22
|
+
throw new Error(`${at} must be an object.`);
|
|
23
|
+
}
|
|
24
|
+
const r = rule;
|
|
25
|
+
const hasName = typeof r.name === "string" && r.name.trim() !== "";
|
|
26
|
+
const hasTerm = typeof r.term === "string" && r.term.trim() !== "";
|
|
27
|
+
if (hasName === hasTerm) {
|
|
28
|
+
throw new Error(`${at} must have exactly one of "name" (style rule) or "term" (terminology entry).`);
|
|
29
|
+
}
|
|
30
|
+
if (r.description !== undefined && typeof r.description !== "string") {
|
|
31
|
+
throw new Error(`${at}: "description" must be a string.`);
|
|
32
|
+
}
|
|
33
|
+
if (r.tags !== undefined && !isStringArray(r.tags)) {
|
|
34
|
+
throw new Error(`${at}: "tags" must be an array of strings.`);
|
|
35
|
+
}
|
|
36
|
+
if (r.section !== undefined && (typeof r.section !== "string" || r.section.trim() === "")) {
|
|
37
|
+
throw new Error(`${at}: "section" must be a non-empty string (section name or sectionId).`);
|
|
38
|
+
}
|
|
39
|
+
const section = r.section;
|
|
40
|
+
if (hasName) {
|
|
41
|
+
if (r.disallowed !== undefined) {
|
|
42
|
+
throw new Error(`${at}: "disallowed" only applies to terminology entries (use "term", not "name").`);
|
|
43
|
+
}
|
|
44
|
+
if (r.examples !== undefined) {
|
|
45
|
+
const ok = Array.isArray(r.examples) &&
|
|
46
|
+
r.examples.every((e) => typeof e === "object" &&
|
|
47
|
+
e !== null &&
|
|
48
|
+
typeof e.from === "string" &&
|
|
49
|
+
typeof e.to === "string");
|
|
50
|
+
if (!ok)
|
|
51
|
+
throw new Error(`${at}: "examples" must be an array of {from, to} string pairs.`);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
section,
|
|
55
|
+
rule: {
|
|
56
|
+
name: r.name,
|
|
57
|
+
description: r.description,
|
|
58
|
+
examples: r.examples,
|
|
59
|
+
tags: r.tags,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (r.examples !== undefined) {
|
|
64
|
+
throw new Error(`${at}: "examples" only applies to style rules (use "name", not "term").`);
|
|
65
|
+
}
|
|
66
|
+
if (r.disallowed !== undefined && !isStringArray(r.disallowed)) {
|
|
67
|
+
throw new Error(`${at}: "disallowed" must be an array of strings.`);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
section,
|
|
71
|
+
rule: {
|
|
72
|
+
term: r.term,
|
|
73
|
+
disallowed: r.disallowed,
|
|
74
|
+
description: r.description,
|
|
75
|
+
tags: r.tags,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async function createRules(opts) {
|
|
80
|
+
const raw = readInput(opts.file);
|
|
81
|
+
let parsed;
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(raw);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw new Error("Input must be valid JSON.");
|
|
87
|
+
}
|
|
88
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
89
|
+
throw new Error("Input must be a non-empty JSON array of rules.");
|
|
90
|
+
}
|
|
91
|
+
const rules = parsed.map(validateRule);
|
|
92
|
+
const { config } = (0, config_1.loadConfig)();
|
|
93
|
+
const apiKey = (0, config_1.getApiKey)();
|
|
94
|
+
const api = new api_1.DittoApi(config, apiKey);
|
|
95
|
+
const guides = await api.getStyleguides();
|
|
96
|
+
const guide = (0, api_1.resolveStyleguide)(guides, config, opts.styleguide);
|
|
97
|
+
// Resolve every rule's section before creating anything, so a bad section
|
|
98
|
+
// fails the batch before any API writes (mid-batch API failures are handled
|
|
99
|
+
// below). A rule's own "section" must match its kind; the --section default
|
|
100
|
+
// applies only to rules of its kind — others get the first section of theirs.
|
|
101
|
+
const resolved = rules.map((input, index) => {
|
|
102
|
+
const kind = "name" in input.rule ? "rules" : "wordlist";
|
|
103
|
+
try {
|
|
104
|
+
const section = input.section
|
|
105
|
+
? (0, api_1.resolveSection)(guide, kind, input.section)
|
|
106
|
+
: (0, api_1.resolveSection)(guide, kind, opts.section, { softKindFallback: true });
|
|
107
|
+
return { rule: input.rule, section };
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
throw new Error(`Rule at index ${index}: ${err instanceof Error ? err.message : err}`);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
// One API call per target section, preserving input order within each.
|
|
114
|
+
const groups = new Map();
|
|
115
|
+
for (const { rule, section } of resolved) {
|
|
116
|
+
const group = groups.get(section.sectionId) ?? { name: section.name, rules: [] };
|
|
117
|
+
group.rules.push(rule);
|
|
118
|
+
groups.set(section.sectionId, group);
|
|
119
|
+
}
|
|
120
|
+
const groupList = [...groups.entries()];
|
|
121
|
+
for (let g = 0; g < groupList.length; g++) {
|
|
122
|
+
const [sectionId, group] = groupList[g];
|
|
123
|
+
let result;
|
|
124
|
+
try {
|
|
125
|
+
result = await api.createRules({
|
|
126
|
+
styleguideId: guide.developerId,
|
|
127
|
+
sectionId,
|
|
128
|
+
rules: group.rules,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
// Earlier groups are already on the platform and there is no delete API;
|
|
133
|
+
// hand back exactly the rules that still need creating so a retry
|
|
134
|
+
// doesn't duplicate the ones that succeeded.
|
|
135
|
+
const created = groupList.slice(0, g).reduce((n, [, grp]) => n + grp.rules.length, 0);
|
|
136
|
+
const remaining = groupList
|
|
137
|
+
.slice(g)
|
|
138
|
+
.flatMap(([sid, grp]) => grp.rules.map((rule) => ({ ...rule, section: sid })));
|
|
139
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
140
|
+
throw new Error(`${message}\n\n` +
|
|
141
|
+
(created > 0
|
|
142
|
+
? `${created} rule(s) were already created (see ✓ lines above) — do not resubmit them.\n`
|
|
143
|
+
: "") +
|
|
144
|
+
`${remaining.length} rule(s) were NOT created. Re-run create-rules with exactly this input:\n` +
|
|
145
|
+
JSON.stringify(remaining, null, 2));
|
|
146
|
+
}
|
|
147
|
+
group.rules.forEach((rule, i) => {
|
|
148
|
+
const label = "name" in rule ? rule.name : rule.term;
|
|
149
|
+
const id = result.ids[i];
|
|
150
|
+
console.log(`✓ Created "${label}" in ${guide.name} › ${group.name}${id ? ` (${id})` : ""}`);
|
|
151
|
+
});
|
|
152
|
+
if (result.ids.length !== group.rules.length) {
|
|
153
|
+
console.log(` ⚠ Server returned ${result.ids.length} id(s) for ${group.rules.length} rule(s) in "${group.name}" — ids above may be incomplete.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -207,8 +207,9 @@ function writeSkillFiles(cwd, force) {
|
|
|
207
207
|
console.log(` ${unchanged} skill file(s) already up to date.`);
|
|
208
208
|
}
|
|
209
209
|
if (skipped.length > 0) {
|
|
210
|
-
console.log(`⚠ Skipped ${skipped.length}
|
|
211
|
-
console.log("
|
|
210
|
+
console.log(`⚠ Skipped ${skipped.length} skill file(s) that differ from the current templates: ${skipped.join(", ")}`);
|
|
211
|
+
console.log(" These may be locally modified, or generated by an older CLI version.");
|
|
212
|
+
console.log(" Re-run with --force to overwrite them with the current versions.");
|
|
212
213
|
}
|
|
213
214
|
}
|
|
214
215
|
function printAgentSuggestions(env) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rules = rules;
|
|
4
|
+
const api_1 = require("../api");
|
|
5
|
+
const config_1 = require("../config");
|
|
6
|
+
async function rules(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
|
+
const guides = await api.getStyleguides();
|
|
11
|
+
// With an explicit override, resolveStyleguide never falls back to
|
|
12
|
+
// config.defaultStyleguide — no override still lists every guide.
|
|
13
|
+
const selected = opts.styleguide ? [(0, api_1.resolveStyleguide)(guides, config, opts.styleguide)] : guides;
|
|
14
|
+
for (const guide of selected) {
|
|
15
|
+
const locale = guide.variant?.localeCode ? ` [locale: ${guide.variant.localeCode}]` : "";
|
|
16
|
+
console.log(`${guide.name} (${guide.developerId})${locale}`);
|
|
17
|
+
for (const section of guide.sections) {
|
|
18
|
+
const sectionId = section.sectionId ? ` (sectionId: ${section.sectionId})` : "";
|
|
19
|
+
console.log(` ${section.name} [${section.kind}]${sectionId}`);
|
|
20
|
+
if (section.rules.length === 0)
|
|
21
|
+
console.log(" (no rules)");
|
|
22
|
+
for (const rule of section.rules) {
|
|
23
|
+
if (section.kind === "rules") {
|
|
24
|
+
const r = rule;
|
|
25
|
+
const tags = r.tags.length > 0 ? ` — tags: ${r.tags.join(", ")}` : "";
|
|
26
|
+
console.log(` - ${r.name}${tags}`);
|
|
27
|
+
if (r.description)
|
|
28
|
+
console.log(` ${r.description}`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const r = rule;
|
|
32
|
+
const disallowed = r.disallowed.length > 0 ? ` (not: ${r.disallowed.join(", ")})` : "";
|
|
33
|
+
const tags = r.tags.length > 0 ? ` — tags: ${r.tags.join(", ")}` : "";
|
|
34
|
+
console.log(` - ${r.term}${disallowed}${tags}`);
|
|
35
|
+
if (r.description)
|
|
36
|
+
console.log(` ${r.description}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
console.log("");
|
|
41
|
+
}
|
|
42
|
+
}
|
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
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, evaluate the **actual copy** bound to **every** text surface. A surface's value may be an inline literal or a reference to a string stored elsewhere \u2014 and a single instance commonly mixes both. These layers are **not** mutually exclusive: evaluate every one that applies, and resolving an externalized source never lets you skip the inline copy.\n\n **a. Inline (always \u2014 the baseline).** Read and audit the literal text present directly in the code: string literals and template strings passed as props, children rendered as text, hardcoded strings in the component/JSX, and variables with obvious values (trace one level if needed). **This pass is mandatory and is never skipped \u2014 auditing hardcoded copy is the default job of this skill, so do it for every instance even when the component also uses i18n or Ditto.**\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 Ditto manages, fetched through a key-lookup call. **The deciding signal is key-resolvability, not the function name:** whenever a surface is bound to any lookup-style call passing a string literal (e.g. `getDittoText('open-in-figma')`, `t('style-guides')`, `ditto.t(...)`, `getText('...')`) that the inline (**a**) and i18n (**b**) branches did not already resolve, treat the literal as a candidate Ditto **Developer ID** and try to resolve it. An accessor name containing \"ditto\" (case-insensitive \u2014 `getDittoText`, `useDittoText`, `dittoText`, `ditto.t`) or an import tracing to a Ditto module is a corroborating hint, not a requirement.\n - Locate the Ditto output: the `ditto/` folder, or the `outDir` in `ditto/config.yml`.\n - **Grep the specific literal id as a key** across the `{project}___{variant}.json` and `components__{variant}.json` files \u2014 do not read whole files or index all keys. Prefer an exact key match; only if none exists, try a normalized (kebab/camel) form of the id.\n - If the key is found, it **is** a Ditto text item: resolve its value in **every** variant file that contains it (each variant is a separate instance, parallel to the every-locale rule in **b**; infer the variant/locale from the file name), resolve `_one`/`_other` plural forms (evaluate each), and treat `{{variableId}}` placeholders as opaque tokens. Record the source as **Ditto**, naming the Developer ID and variant.\n - If the id is **not a string literal** (e.g. `getDittoText('item-' + type)`) or **no ditto file contains the key**, drop to **d** \u2014 never log a Ditto source you could not key-match.\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`. **Detect Ditto bindings by key-resolvability, not function name:** any string-literal argument to a lookup-style call (whatever it's named) that matches a Developer-ID key in these files is a Ditto text item. Grep the specific id as a key \u2014 never read entire files to find it.\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
|
|
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. Run `npx ditto-spec rules` to load the **complete** platform rule set. Spec files only contain rules whose tags match a local component \u2014 a platform rule with non-matching tags is invisible in specs, and proposing a duplicate of it would be a mistake. The output also shows each style guide's sections (name, kind, sectionId), which Phase 4 may need.\n4. Build a complete picture of what's already covered \u2014 every style rule, terminology entry, and locale-scoped rule on the platform.\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, gather the **actual copy** bound to **every** text surface \u2014 inline and externalized alike. An instance often mixes both, so never skip the inline copy just because some surfaces resolve to a catalog:\n - **Inline (always \u2014 the baseline).** String literals, template strings, children, hardcoded strings, and variables with obvious values. Always gather these; hardcoded copy is the default and is never skipped.\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 any lookup-style call passing a string literal (e.g. `getDittoText('id')`, `t('id')`, `ditto.t`) whose id matches a Developer-ID key in the project's Ditto output (`ditto/` folder, or `outDir` in `ditto/config.yml`; `{project}___{variant}.json` keyed by Developer ID, plurals suffixed `_one`/`_other`, variables `{{variableId}}`). Detect by key-resolvability, not function name; a \"ditto\"-ish name or import is just a hint. Grep the specific id as a key \u2014 do not read entire catalogs \u2014 and resolve per variant. If the id isn't a literal or no file has the key, treat as unresolvable.\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- **section**: which existing section of the style guide it belongs in (pick from the sections shown by `npx ditto-spec rules` \u2014 e.g. a button rule belongs alongside other UI-pattern rules, not in a generic first section)\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- **section**: which existing wordlist section it belongs in\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-rules` rejects an unrecognized `--styleguide` value and lists the valid names, so a wrong name fails loudly instead of silently targeting the wrong guide.\n\nCreate **all** approved rules in one invocation, piping a JSON array on stdin with a quoted heredoc (the quoted `'EOF'` delimiter keeps apostrophes and quotes in copy examples intact \u2014 never inline the JSON as a shell argument):\n\n```\nnpx ditto-spec create-rules --styleguide \"<chosen style guide>\" <<'EOF'\n[\n {\"name\": \"<rule name>\", \"description\": \"<description>\", \"tags\": [\"tag1\"], \"examples\": [{\"from\": \"...\", \"to\": \"...\"}], \"section\": \"<existing section>\"},\n {\"term\": \"<canonical form>\", \"disallowed\": [\"variant1\", \"variant2\"], \"description\": \"<why>\", \"tags\": [\"tag1\"], \"section\": \"<existing wordlist section>\"}\n]\nEOF\n```\n\nStyle rules (`name`) and terminology entries (`term`) can be mixed in one batch. Each rule's `section` (name or sectionId, as shown by `npx ditto-spec rules`) maps it to an existing section of the style guide; a rule's section kind must match its shape (`rules` sections for style rules, `wordlist` sections for terminology). If `section` is omitted, the rule lands in the first section of the matching kind.\n\nIf a rule uses tags that don't exist yet, warn the user and omit those tags from the JSON.\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
|
@@ -234,7 +234,8 @@ If a component name or path is given, analyze only that component's instances. I
|
|
|
234
234
|
|
|
235
235
|
1. Read \`workspace.ditto.md\` for workspace-level rules and tags.
|
|
236
236
|
2. Read each relevant component's \`index.ditto.md\` for component-level and per-surface rules.
|
|
237
|
-
3.
|
|
237
|
+
3. Run \`npx ditto-spec rules\` to load the **complete** platform rule set. Spec files only contain rules whose tags match a local component — a platform rule with non-matching tags is invisible in specs, and proposing a duplicate of it would be a mistake. The output also shows each style guide's sections (name, kind, sectionId), which Phase 4 may need.
|
|
238
|
+
4. Build a complete picture of what's already covered — every style rule, terminology entry, and locale-scoped rule on the platform.
|
|
238
239
|
|
|
239
240
|
---
|
|
240
241
|
|
|
@@ -265,12 +266,14 @@ If gaps are found, propose new rules. Rules come in two shapes:
|
|
|
265
266
|
- **description**: what the rule enforces
|
|
266
267
|
- **tags**: which tags it should apply to (determines which surfaces it matches)
|
|
267
268
|
- **examples**: \`{from, to}\` pairs drawn from actual code showing the pattern
|
|
269
|
+
- **section**: which existing section of the style guide it belongs in (pick from the sections shown by \`npx ditto-spec rules\` — e.g. a button rule belongs alongside other UI-pattern rules, not in a generic first section)
|
|
268
270
|
|
|
269
271
|
**Terminology entries:**
|
|
270
272
|
- **term**: the canonical form
|
|
271
273
|
- **disallowed**: list of variant forms to reject
|
|
272
274
|
- **description**: why this form is preferred
|
|
273
275
|
- **tags**: which tags it should apply to
|
|
276
|
+
- **section**: which existing wordlist section it belongs in
|
|
274
277
|
|
|
275
278
|
If any proposed tags don't exist in the workspace yet, warn the user. Only existing workspace tags can be assigned to rules.
|
|
276
279
|
|
|
@@ -284,15 +287,22 @@ Before creating any rules, determine which style guide to target:
|
|
|
284
287
|
|
|
285
288
|
1. Check \`dittospec.config.json\` for a \`defaultStyleguide\` value.
|
|
286
289
|
2. If present, tell the user which style guide will be used and ask for confirmation.
|
|
287
|
-
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-
|
|
290
|
+
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-rules\` rejects an unrecognized \`--styleguide\` value and lists the valid names, so a wrong name fails loudly instead of silently targeting the wrong guide.
|
|
288
291
|
|
|
289
|
-
|
|
292
|
+
Create **all** approved rules in one invocation, piping a JSON array on stdin with a quoted heredoc (the quoted \`'EOF'\` delimiter keeps apostrophes and quotes in copy examples intact — never inline the JSON as a shell argument):
|
|
290
293
|
|
|
291
294
|
\`\`\`
|
|
292
|
-
npx ditto-spec create-
|
|
295
|
+
npx ditto-spec create-rules --styleguide "<chosen style guide>" <<'EOF'
|
|
296
|
+
[
|
|
297
|
+
{"name": "<rule name>", "description": "<description>", "tags": ["tag1"], "examples": [{"from": "...", "to": "..."}], "section": "<existing section>"},
|
|
298
|
+
{"term": "<canonical form>", "disallowed": ["variant1", "variant2"], "description": "<why>", "tags": ["tag1"], "section": "<existing wordlist section>"}
|
|
299
|
+
]
|
|
300
|
+
EOF
|
|
293
301
|
\`\`\`
|
|
294
302
|
|
|
295
|
-
|
|
303
|
+
Style rules (\`name\`) and terminology entries (\`term\`) can be mixed in one batch. Each rule's \`section\` (name or sectionId, as shown by \`npx ditto-spec rules\`) maps it to an existing section of the style guide; a rule's section kind must match its shape (\`rules\` sections for style rules, \`wordlist\` sections for terminology). If \`section\` is omitted, the rule lands in the first section of the matching kind.
|
|
304
|
+
|
|
305
|
+
If a rule uses tags that don't exist yet, warn the user and omit those tags from the JSON.
|
|
296
306
|
|
|
297
307
|
After all rules are created:
|
|
298
308
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dittowords/spec-cli",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.15",
|
|
4
4
|
"description": "CLI for syncing .ditto.md content specs with the Ditto platform.",
|
|
5
5
|
"main": "dist/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"dist"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "tsc",
|
|
13
|
+
"build": "tsc && npm run sync-skills -- --check",
|
|
14
|
+
"sync-skills": "tsx scripts/sync-skills.ts",
|
|
14
15
|
"prepublishOnly": "npm run build",
|
|
15
16
|
"ditto-spec": "tsx src/cli.ts"
|
|
16
17
|
},
|