similarbuild 0.1.0

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.
Files changed (76) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/LICENSE +21 -0
  3. package/README.md +301 -0
  4. package/bin/install.js +256 -0
  5. package/lib/copy-templates.mjs +52 -0
  6. package/lib/install-deps.mjs +62 -0
  7. package/lib/prompt-config.mjs +83 -0
  8. package/lib/verify-env.mjs +19 -0
  9. package/package.json +63 -0
  10. package/scripts/sync-templates.mjs +71 -0
  11. package/templates/commands/build-page.md +490 -0
  12. package/templates/commands/build-site.md +548 -0
  13. package/templates/commands/clip-section.md +519 -0
  14. package/templates/memory/anti-patterns.md +212 -0
  15. package/templates/memory/design-knowledge.md +225 -0
  16. package/templates/memory/fixes.md +163 -0
  17. package/templates/memory/patterns.md +681 -0
  18. package/templates/presets/shopify-section.yaml +51 -0
  19. package/templates/presets/wp-elementor.yaml +49 -0
  20. package/templates/reports/fixtures/mock-run-1.json +115 -0
  21. package/templates/reports/fixtures/mock-run-2.json +72 -0
  22. package/templates/reports/report-renderer.mjs +218 -0
  23. package/templates/reports/report-template.html +571 -0
  24. package/templates/skills/sb-build-shopify/SKILL.md +104 -0
  25. package/templates/skills/sb-build-shopify/references/shopify-build-rules.md +563 -0
  26. package/templates/skills/sb-build-shopify/scripts/build-shopify.mjs +637 -0
  27. package/templates/skills/sb-build-shopify/scripts/tests/test-build-shopify.mjs +424 -0
  28. package/templates/skills/sb-build-wp/SKILL.md +83 -0
  29. package/templates/skills/sb-build-wp/references/wp-build-rules.md +376 -0
  30. package/templates/skills/sb-build-wp/scripts/build-wp.mjs +327 -0
  31. package/templates/skills/sb-build-wp/scripts/tests/test-build-wp.mjs +224 -0
  32. package/templates/skills/sb-compare-visual/SKILL.md +121 -0
  33. package/templates/skills/sb-compare-visual/scripts/compare-visual.mjs +387 -0
  34. package/templates/skills/sb-compare-visual/scripts/lib/compare-tokens.mjs +273 -0
  35. package/templates/skills/sb-compare-visual/scripts/tests/test-compare-tokens.mjs +350 -0
  36. package/templates/skills/sb-compare-visual/scripts/tests/test-compare-visual.mjs +626 -0
  37. package/templates/skills/sb-crawl-and-list/SKILL.md +99 -0
  38. package/templates/skills/sb-crawl-and-list/scripts/crawl-and-list.mjs +437 -0
  39. package/templates/skills/sb-crawl-and-list/scripts/lib/blocklist-filter.mjs +176 -0
  40. package/templates/skills/sb-crawl-and-list/scripts/lib/fallback-crawler.mjs +107 -0
  41. package/templates/skills/sb-crawl-and-list/scripts/lib/page-classifier.mjs +89 -0
  42. package/templates/skills/sb-crawl-and-list/scripts/lib/sitemap-parser.mjs +118 -0
  43. package/templates/skills/sb-crawl-and-list/scripts/tests/test-blocklist-filter.mjs +204 -0
  44. package/templates/skills/sb-crawl-and-list/scripts/tests/test-crawl-and-list.mjs +276 -0
  45. package/templates/skills/sb-crawl-and-list/scripts/tests/test-fallback-crawler.mjs +243 -0
  46. package/templates/skills/sb-crawl-and-list/scripts/tests/test-page-classifier.mjs +120 -0
  47. package/templates/skills/sb-crawl-and-list/scripts/tests/test-sitemap-parser.mjs +157 -0
  48. package/templates/skills/sb-extract-assets/SKILL.md +112 -0
  49. package/templates/skills/sb-extract-assets/scripts/extract-assets.mjs +484 -0
  50. package/templates/skills/sb-extract-assets/scripts/tests/test-extract-assets.mjs +112 -0
  51. package/templates/skills/sb-inspect-live/SKILL.md +105 -0
  52. package/templates/skills/sb-inspect-live/scripts/inspect-live.mjs +693 -0
  53. package/templates/skills/sb-inspect-live/scripts/tests/test-inspect-live.mjs +181 -0
  54. package/templates/skills/sb-review-checks/SKILL.md +113 -0
  55. package/templates/skills/sb-review-checks/references/review-rules.md +195 -0
  56. package/templates/skills/sb-review-checks/scripts/lib/anti-patterns.mjs +379 -0
  57. package/templates/skills/sb-review-checks/scripts/lib/cross-reference.mjs +115 -0
  58. package/templates/skills/sb-review-checks/scripts/lib/design-quality.mjs +541 -0
  59. package/templates/skills/sb-review-checks/scripts/review-checks.mjs +250 -0
  60. package/templates/skills/sb-review-checks/scripts/tests/test-anti-patterns.mjs +343 -0
  61. package/templates/skills/sb-review-checks/scripts/tests/test-cross-reference.mjs +170 -0
  62. package/templates/skills/sb-review-checks/scripts/tests/test-design-quality.mjs +493 -0
  63. package/templates/skills/sb-review-checks/scripts/tests/test-review-checks.mjs +267 -0
  64. package/templates/skills/sb-tweak/SKILL.md +130 -0
  65. package/templates/skills/sb-tweak/references/tweak-patterns.md +157 -0
  66. package/templates/skills/sb-tweak/scripts/lib/diff-summarizer.mjs +140 -0
  67. package/templates/skills/sb-tweak/scripts/lib/element-locator.mjs +507 -0
  68. package/templates/skills/sb-tweak/scripts/lib/intent-parser.mjs +324 -0
  69. package/templates/skills/sb-tweak/scripts/tests/test-diff-summarizer.mjs +248 -0
  70. package/templates/skills/sb-tweak/scripts/tests/test-element-locator.mjs +418 -0
  71. package/templates/skills/sb-tweak/scripts/tests/test-intent-parser.mjs +496 -0
  72. package/templates/skills/sb-tweak/scripts/tests/test-tweak.mjs +407 -0
  73. package/templates/skills/sb-tweak/scripts/tweak.mjs +656 -0
  74. package/templates/skills/sb-validate-render/SKILL.md +120 -0
  75. package/templates/skills/sb-validate-render/scripts/tests/test-validate-render.mjs +304 -0
  76. package/templates/skills/sb-validate-render/scripts/validate-render.mjs +645 -0
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env node
2
+ // test-review-checks.mjs — CLI integration tests for review-checks.mjs.
3
+ // Covers: --help, arg validation, exit codes. Plus optional integration tests
4
+ // that exercise the full end-to-end path against fixture HTML/Liquid (skipped
5
+ // gracefully when cheerio isn't installed).
6
+
7
+ import { spawnSync } from 'node:child_process'
8
+ import { fileURLToPath } from 'node:url'
9
+ import { dirname, resolve, join } from 'node:path'
10
+ import { mkdir, writeFile, readFile, rm } from 'node:fs/promises'
11
+ import { strict as assert } from 'node:assert'
12
+ import { tmpdir } from 'node:os'
13
+
14
+ const here = dirname(fileURLToPath(import.meta.url))
15
+ const SCRIPT = resolve(here, '..', 'review-checks.mjs')
16
+
17
+ let passed = 0
18
+ let failed = 0
19
+
20
+ function test(name, fn) {
21
+ try {
22
+ const result = fn()
23
+ if (result && typeof result.then === 'function') {
24
+ return result.then(
25
+ () => { process.stdout.write(`ok - ${name}\n`); passed++ },
26
+ (err) => { process.stdout.write(`not ok - ${name}\n ${err.message}\n`); failed++ },
27
+ )
28
+ }
29
+ process.stdout.write(`ok - ${name}\n`)
30
+ passed++
31
+ } catch (err) {
32
+ process.stdout.write(`not ok - ${name}\n ${err.message}\n`)
33
+ failed++
34
+ }
35
+ }
36
+
37
+ function skip(name, reason) {
38
+ process.stdout.write(`ok - ${name} # SKIP ${reason}\n`)
39
+ passed++
40
+ }
41
+
42
+ function run(args) {
43
+ return spawnSync('node', [SCRIPT, ...args], { encoding: 'utf8' })
44
+ }
45
+
46
+ // ─── help + arg validation (no deps required) ──────────────────────────────
47
+
48
+ test('--help exits 0 and prints usage', () => {
49
+ const r = run(['--help'])
50
+ assert.equal(r.status, 0, `exit code was ${r.status}\n${r.stderr}`)
51
+ assert.match(r.stdout, /review-checks\.mjs/)
52
+ assert.match(r.stdout, /--file/)
53
+ assert.match(r.stdout, /--preset/)
54
+ assert.match(r.stdout, /--output-dir/)
55
+ assert.match(r.stdout, /--compare-diffs/)
56
+ })
57
+
58
+ test('missing --file exits 2', () => {
59
+ const r = run(['--preset', 'wp-elementor', '--output-dir', '/tmp/x'])
60
+ assert.equal(r.status, 2)
61
+ assert.match(r.stderr, /missing --file/)
62
+ })
63
+
64
+ test('missing --preset exits 2', () => {
65
+ const r = run(['--file', '/tmp/x.html', '--output-dir', '/tmp/x'])
66
+ assert.equal(r.status, 2)
67
+ assert.match(r.stderr, /missing --preset/)
68
+ })
69
+
70
+ test('missing --output-dir exits 2', () => {
71
+ const r = run(['--file', '/tmp/x.html', '--preset', 'wp-elementor'])
72
+ assert.equal(r.status, 2)
73
+ assert.match(r.stderr, /missing --output-dir/)
74
+ })
75
+
76
+ test('invalid --preset value exits 2', () => {
77
+ const r = run([
78
+ '--file', '/tmp/x.html',
79
+ '--preset', 'wordpress',
80
+ '--output-dir', '/tmp/x',
81
+ ])
82
+ assert.equal(r.status, 2)
83
+ assert.match(r.stderr, /--preset must be one of/)
84
+ })
85
+
86
+ test('non-existent --file exits 2', () => {
87
+ const r = run([
88
+ '--file', '/tmp/sb-review-does-not-exist-xyz.html',
89
+ '--preset', 'wp-elementor',
90
+ '--output-dir', '/tmp/sb-review-test-out',
91
+ ])
92
+ assert.equal(r.status, 2)
93
+ assert.match(r.stderr, /not found/)
94
+ })
95
+
96
+ test('stderr always carries the [sb-review-checks] prefix', () => {
97
+ const r = run(['--file', '/tmp/nope.html', '--preset', 'wp-elementor', '--output-dir', '/tmp/x'])
98
+ assert.equal(r.status, 2)
99
+ assert.match(r.stderr, /^\[sb-review-checks\]/)
100
+ })
101
+
102
+ // ─── integration: cheerio-gated ─────────────────────────────────────────────
103
+
104
+ let cheerioAvailable = false
105
+ try { await import('cheerio'); cheerioAvailable = true } catch {}
106
+
107
+ const TMPROOT = join(tmpdir(), `sb-review-checks-test-${Date.now()}`)
108
+
109
+ if (!cheerioAvailable) {
110
+ skip('integration: clean fragment exits 0 with passed=true', 'cheerio not installed')
111
+ skip('integration: dirty fragment exits 3 with violations', 'cheerio not installed')
112
+ skip('integration: writes report.json + emits stdout JSON', 'cheerio not installed')
113
+ skip('integration: cross-reference escalates correlated violations', 'cheerio not installed')
114
+ skip('integration: low-only violations still pass (exit 0)', 'cheerio not installed')
115
+ } else {
116
+ await mkdir(TMPROOT, { recursive: true })
117
+
118
+ const cleanHtml = `<!-- clean fragment -->
119
+ <link rel="preconnect" href="https://fonts.googleapis.com">
120
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
121
+ <link rel="preload" as="image" href="hero.jpg">
122
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter&display=swap">
123
+ <style>
124
+ .scope { box-sizing: border-box; }
125
+ .scope .scope__title { font-size: 24px !important; }
126
+ .scope .scope__cta.scope__cta { background: #d8112a !important; color: #fff !important; }
127
+ </style>
128
+ <section class="scope">
129
+ <h1 class="scope__title">Hello</h1>
130
+ <img src="hero.jpg" alt="Hero" loading="eager">
131
+ <button type="button" class="scope__cta">Buy now</button>
132
+ </section>`
133
+
134
+ const dirtyHtml = `<style>
135
+ .hero { height: 100vh; }
136
+ .title { font-size: 24px !important; }
137
+ .cta { background: red; color: white; }
138
+ .scope a { color: inherit; }
139
+ .x { opacity: 0.5; }
140
+ </style>
141
+ <section class="hero">
142
+ <h3>Bad first heading</h3>
143
+ <img src="hero.jpg">
144
+ <img src="other.jpg">
145
+ <button><svg></svg></button>
146
+ <div class="modal">x</div>
147
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter">
148
+ </section>`
149
+
150
+ await test('integration: clean fragment exits 0 with passed=true', async () => {
151
+ const dir = join(TMPROOT, 'clean')
152
+ await mkdir(dir, { recursive: true })
153
+ const file = join(dir, 'clean.html')
154
+ await writeFile(file, cleanHtml)
155
+ const out = join(dir, 'out')
156
+ const r = run(['--file', file, '--preset', 'wp-elementor', '--output-dir', out])
157
+ assert.equal(r.status, 0, `exit was ${r.status}\nstderr: ${r.stderr}\nstdout: ${r.stdout}`)
158
+ const json = JSON.parse(r.stdout.trim())
159
+ assert.equal(json.passed, true)
160
+ assert.equal(json.violationCount.high, 0)
161
+ assert.equal(json.violationCount.medium, 0)
162
+ })
163
+
164
+ await test('integration: dirty fragment exits 3 with violations', async () => {
165
+ const dir = join(TMPROOT, 'dirty')
166
+ await mkdir(dir, { recursive: true })
167
+ const file = join(dir, 'dirty.html')
168
+ await writeFile(file, dirtyHtml)
169
+ const out = join(dir, 'out')
170
+ const r = run(['--file', file, '--preset', 'wp-elementor', '--output-dir', out])
171
+ assert.equal(r.status, 3, `exit was ${r.status}\nstderr: ${r.stderr}\nstdout: ${r.stdout.slice(0, 500)}`)
172
+ const json = JSON.parse(r.stdout.trim())
173
+ assert.equal(json.passed, false)
174
+ assert.ok(json.violationCount.high >= 2, `expected >=2 high, got ${json.violationCount.high}`)
175
+ // Each violation must have the contract fields and a non-trivial candidateFix
176
+ for (const v of json.violations) {
177
+ assert.ok(v.group, 'group required')
178
+ assert.ok(v.id, 'id required')
179
+ assert.ok(v.location, 'location required')
180
+ assert.ok(v.issue, 'issue required')
181
+ assert.ok(v.candidateFix && v.candidateFix.length > 10, `candidateFix must be specific: ${v.candidateFix}`)
182
+ assert.ok(['high', 'medium', 'low'].includes(v.severity))
183
+ }
184
+ })
185
+
186
+ await test('integration: writes report.json + emits stdout JSON', async () => {
187
+ const dir = join(TMPROOT, 'report')
188
+ await mkdir(dir, { recursive: true })
189
+ const file = join(dir, 'x.html')
190
+ await writeFile(file, dirtyHtml)
191
+ const out = join(dir, 'out')
192
+ const r = run(['--file', file, '--preset', 'wp-elementor', '--output-dir', out])
193
+ assert.ok([0, 3].includes(r.status))
194
+ const reportPath = join(out, 'report.json')
195
+ const fileJson = JSON.parse(await readFile(reportPath, 'utf8'))
196
+ const stdoutJson = JSON.parse(r.stdout.trim())
197
+ // The two should be deeply equivalent
198
+ assert.equal(fileJson.violationCount.high, stdoutJson.violationCount.high)
199
+ assert.equal(fileJson.violationCount.medium, stdoutJson.violationCount.medium)
200
+ assert.equal(fileJson.violations.length, stdoutJson.violations.length)
201
+ })
202
+
203
+ await test('integration: cross-reference escalates correlated violations', async () => {
204
+ const dir = join(TMPROOT, 'xref')
205
+ await mkdir(dir, { recursive: true })
206
+ const file = join(dir, 'x.html')
207
+ // Has a button-type violation (medium); diff has high-severity button issue
208
+ await writeFile(file, '<section class="scope"><button class="cta">Click</button></section>')
209
+ const diffsPath = join(dir, 'compare.json')
210
+ await writeFile(diffsPath, JSON.stringify({
211
+ structuredDiffs: [
212
+ { area: 'button', severity: 'high', issue: 'background-color drift', live: '#d8112a', build: '#d82a11' },
213
+ ],
214
+ }))
215
+ const out = join(dir, 'out')
216
+ const r = run([
217
+ '--file', file,
218
+ '--preset', 'wp-elementor',
219
+ '--output-dir', out,
220
+ '--compare-diffs', diffsPath,
221
+ ])
222
+ const json = JSON.parse(r.stdout.trim())
223
+ const correlated = json.violations.find((v) => v.correlatedDiff)
224
+ assert.ok(correlated, 'expected at least one correlated violation')
225
+ assert.equal(correlated.severity, 'high')
226
+ })
227
+
228
+ await test('integration: low-only violations still pass (exit 0)', async () => {
229
+ // Build a fragment whose only violations are `low` (e.g. just opacity 0.5)
230
+ const dir = join(TMPROOT, 'low-only')
231
+ await mkdir(dir, { recursive: true })
232
+ const file = join(dir, 'x.html')
233
+ const html = `<style>
234
+ .scope { box-sizing: border-box; }
235
+ .scope .scope__title { font-size: 24px !important; }
236
+ .scope .x { opacity: 0.5; }
237
+ </style>
238
+ <link rel="preconnect" href="https://fonts.googleapis.com">
239
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
240
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter&display=swap">
241
+ <link rel="preload" as="image" href="hero.jpg">
242
+ <section class="scope">
243
+ <h1 class="scope__title">Hi</h1>
244
+ <img src="hero.jpg" alt="hero" loading="eager">
245
+ <button type="button">Buy</button>
246
+ </section>`
247
+ await writeFile(file, html)
248
+ const out = join(dir, 'out')
249
+ const r = run(['--file', file, '--preset', 'wp-elementor', '--output-dir', out])
250
+ const json = JSON.parse(r.stdout.trim())
251
+ assert.equal(json.violationCount.high, 0)
252
+ assert.equal(json.violationCount.medium, 0)
253
+ assert.ok(json.violationCount.low >= 1, `expected >=1 low, got ${json.violationCount.low}`)
254
+ assert.equal(r.status, 0)
255
+ assert.equal(json.passed, true)
256
+ })
257
+
258
+ // Cleanup
259
+ await rm(TMPROOT, { recursive: true, force: true }).catch(() => {})
260
+ }
261
+
262
+ if (failed > 0) {
263
+ process.stdout.write(`\n${failed} failed, ${passed} passed\n`)
264
+ process.exit(1)
265
+ }
266
+ process.stdout.write(`\n${passed} passed\n`)
267
+ process.exit(0)
@@ -0,0 +1,130 @@
1
+ ---
2
+ name: sb-tweak
3
+ description: Edits a built HTML/Liquid fragment in place from a natural-language request (PT/EN) — locates the targeted element, applies a precise change (text, attribute, CSS rule, or inline style), optionally re-validates the render, and emits a summarized diff. Use when the user asks to 'tweak', 'ajustar', 'aumentar/diminuir', 'mudar/trocar', 'adicionar/remover' something on a previously delivered file, or when the SimilarBuild orchestrator needs a post-delivery edit pass.
4
+ ---
5
+
6
+ # sb-tweak
7
+
8
+ ## Overview
9
+
10
+ Post-delivery editor for files produced by `sb-build-wp` / `sb-build-shopify`. Takes a natural-language request like *"aumenta o título do hero pra 32px"* or *"change the hero image alt to 'AlphaInfuse hero'"* and applies a single, surgical edit to the file — without re-cloning the page.
11
+
12
+ Three parts work together:
13
+
14
+ 1. **`lib/intent-parser.mjs`** turns the request (PT or EN) into a structured `{action, target, property, value, confidence}` intent. Pure function, no I/O.
15
+ 2. **`lib/element-locator.mjs`** uses cheerio + heuristics to find the element(s) the intent points at, scores them, and picks the application scope (text, attribute, CSS rule, inline style). Pure function, no I/O.
16
+ 3. **`scripts/tweak.mjs`** is the CLI: file I/O, applies the edit, optionally invokes `sb-validate-render`, emits a summarized diff via `lib/diff-summarizer.mjs`.
17
+
18
+ The split is deliberate (pattern #11): the script handles **mechanical** work (parse, locate, apply, validate, diff). The LLM handles **semantic** work (interpret ambiguous requests, pick between candidates when scores are close, decide if the result looks right). When the parser's confidence is low or the locator returns multiple plausible candidates, the script exits with **code 4 (disambiguation needed)** and surfaces machine-readable candidates — it never guesses.
19
+
20
+ This skill is **standalone** and **invocable directly by the user** — it is not part of the `/build-page`, `/build-site`, or `/clip-section` flow. A future V2 may wrap it in a `/tweak` slash command following the same orchestrator pattern as the others.
21
+
22
+ ## Inputs
23
+
24
+ | Argument | Required | Default | Notes |
25
+ | ------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------ |
26
+ | `--file` | yes | — | Path to the HTML/Liquid file. Edited in place unless `--output-path` is given. |
27
+ | `--request` | yes | — | Natural-language request in PT or EN, e.g. `"aumenta o título do hero pra 32px"`. |
28
+ | `--target-selector` | no | — | Bypass the locator and apply the edit to this exact CSS selector. Used after a disambiguation round. |
29
+ | `--output-path` | no | — | Write the edited file to this path instead of editing in place. Parent dirs are created. |
30
+ | `--validate` | no | `true` | After editing, invoke `sb-validate-render` to confirm the layout still renders. Pass `--no-validate` to skip. |
31
+ | `--preset` | cond. | — | `wp-elementor` or `shopify-section`. **Required when `--validate` is true.** |
32
+ | `--output-dir` | cond. | — | Directory for `tweak.json` and (when validating) the validation artifacts. Required. |
33
+ | `--dry-run` | no | `false` | Show the proposed change without writing it. The diff is computed but the file is not modified. |
34
+
35
+ ## Output
36
+
37
+ A single JSON object printed to stdout AND saved to `{output-dir}/tweak.json`:
38
+
39
+ ```json
40
+ {
41
+ "file": "/abs/path/build.html",
42
+ "request": "aumenta o título do hero pra 32px",
43
+ "intent": {
44
+ "action": "increase",
45
+ "target": { "type": "heading", "qualifier": "hero" },
46
+ "property": "font-size",
47
+ "value": { "kind": "pixels", "raw": "32px", "parsed": 32 },
48
+ "confidence": 0.9,
49
+ "language": "pt"
50
+ },
51
+ "selector": ".hero .hero__title",
52
+ "changes": [
53
+ { "selector": ".hero .hero__title", "property": "font-size", "before": "27px", "after": "32px", "scope": "css-rule", "line": 87 }
54
+ ],
55
+ "diff": "Changed `.hero .hero__title` font-size: 27px → 32px (line 87)",
56
+ "validation": { "screenshot": "{output-dir}/screenshot.png", "passed": true, "warnings": [] },
57
+ "needsClarification": false,
58
+ "outputPath": "/abs/path/build.html",
59
+ "dryRun": false
60
+ }
61
+ ```
62
+
63
+ When the parser is unsure or the locator returns multiple plausible candidates:
64
+
65
+ ```json
66
+ {
67
+ "needsClarification": true,
68
+ "reason": "multiple-candidates",
69
+ "candidates": [
70
+ { "selector": ".hero .hero__title", "snippet": "<h1 class=\"hero__title\">Welcome</h1>", "score": 0.85, "line": 42 },
71
+ { "selector": ".pricing__title", "snippet": "<h2 class=\"pricing__title\">Plans</h2>", "score": 0.62, "line": 118 }
72
+ ],
73
+ "hint": "re-invoke with --target-selector \"<chosen-selector>\""
74
+ }
75
+ ```
76
+
77
+ ## Dependencies
78
+
79
+ The host project must have `cheerio` installed (the SimilarBuild installer handles it). When `--validate` is true, `sb-validate-render` and its own deps must be available. Node ≥ 20.
80
+
81
+ ## On Activation
82
+
83
+ 1. **Resolve inputs.** Collect `--file`, `--request`, `--output-dir`. Optional: `--target-selector`, `--output-path`, `--validate` / `--no-validate`, `--preset`, `--dry-run`.
84
+
85
+ 2. **Run the script.** From the project root:
86
+
87
+ ```bash
88
+ node {skill-root}/scripts/tweak.mjs \
89
+ --file "{built-file}" \
90
+ --request "{natural-language-request}" \
91
+ --output-dir "{output-dir}" \
92
+ [--target-selector "{selector}"] \
93
+ [--output-path "{new-path}"] \
94
+ [--no-validate] [--preset wp-elementor] [--dry-run]
95
+ ```
96
+
97
+ The script handles: file read, intent parse, element location, edit application (text / attribute / CSS rule / inline style), optional validate-render invocation, diff summarization, and persistence.
98
+
99
+ 3. **Branch on exit code.**
100
+ - `0` — `needsClarification: false`, edit applied (or dry-run preview produced). Show the diff.
101
+ - `4` — `needsClarification: true`. **Not an error.** Either intent confidence was low, language was unrecognized, or multiple candidates tied. Read the JSON, ask the user to disambiguate (or pick the top candidate yourself if the snippet makes it obvious), then re-invoke with `--target-selector "<chosen>"`.
102
+ - `3` — validate-render ran after the edit and reported failure. The script reverts the file and returns the validate-render error. Treat as escalation: show the user what broke and ask whether to retry with a different approach.
103
+ - `2` — invalid args. Fix the call.
104
+ - `1` — script error (missing dep, malformed file). Surface stderr to the user and stop.
105
+
106
+ 4. **Forward the JSON unchanged** to the caller. Downstream consumers (an orchestrator that wraps `sb-tweak`, the user reading the diff) read every field directly.
107
+
108
+ ## Idempotence
109
+
110
+ Running the same request twice on the same file produces the same file. The second run reports `changes: []` and `diff: "no changes (already applied)"` — the script reads the current value before applying and skips when it already matches.
111
+
112
+ ## Failure modes
113
+
114
+ | Symptom | Likely cause | What to surface |
115
+ | --------------------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
116
+ | Exit 4, `reason: "low-confidence"` | Request couldn't be parsed (vague, unsupported lang) | Ask the user to rephrase. Show `intent` so they see what was understood. |
117
+ | Exit 4, `reason: "multiple-candidates"` | Locator found ≥2 elements with similar scores | Show the snippets; ask which one. Re-invoke with `--target-selector`. |
118
+ | Exit 4, `reason: "no-candidates"` | Target type not present in the file | Tell the user. Suggest checking the file (maybe the section was already removed) or rephrasing. |
119
+ | Exit 4, `reason: "language-unsupported"` | Request was not PT or EN | Tell the user only PT and EN are supported. |
120
+ | Exit 3, validation failed | Edit broke the render | The file was reverted automatically. Show the validation warnings. |
121
+ | Exit 1, stderr mentions `cheerio` | Dependency not installed | Pass stderr verbatim. Suggest `npm i cheerio`. |
122
+ | `changes: []`, `diff: "no changes (already applied)"` | Idempotent re-run | This is success. Tell the user the file was already in the requested state. |
123
+
124
+ ## Conventions
125
+
126
+ - Bare paths (e.g. `scripts/tweak.mjs`) resolve from the skill root.
127
+ - `{skill-root}` resolves to this skill's installed directory.
128
+ - `{project-root}` resolves to the project working directory.
129
+ - Stderr is prefixed `[sb-tweak]`.
130
+ - The script edits a single file (pattern #15: `--output-path` for single-file artifacts).
@@ -0,0 +1,157 @@
1
+ ---
2
+ name: tweak-patterns
3
+ description: Catalog of common natural-language tweak requests in PT/EN with expected parsed intent, locator behavior, and edit application. Used by sb-tweak's intent-parser as a calibration reference and by humans reading the skill to understand what's supported.
4
+ ---
5
+
6
+ # Tweak Patterns
7
+
8
+ This is the canonical catalog of supported natural-language requests. The intent parser does not need to recognize every variation listed here — it only needs to find the **action verb**, **target type**, optional **qualifier**, optional **property**, and **value**. The patterns below show what the parser produces for each shape, so future contributors can extend the parser with confidence.
9
+
10
+ ## Action verbs
11
+
12
+ | Verb (PT) | Verb (EN) | Action |
13
+ | ----------------------------------------------------- | ------------------------------------------ | ----------- |
14
+ | aumenta, aumentar, maior, cresce | increase, enlarge, grow, bigger, larger | `increase` |
15
+ | diminui, diminuir, menor, encolhe, reduz, reduzir | decrease, reduce, shrink, smaller | `decrease` |
16
+ | muda, mudar, troca, trocar, altera, alterar, seta, define, define-se | set, change, replace, update, swap | `set` |
17
+ | adiciona, adicionar, coloca, colocar, põe, insere, insira | add, insert, put | `add` |
18
+ | remove, remover, tira, tirar, apaga, apagar, deleta | remove, delete, drop | `remove` |
19
+
20
+ ## Target types
21
+
22
+ | Words (PT) | Words (EN) | `target.type` | Default property guess |
23
+ | ----------------------------------------- | ----------------------------------- | ----------------- | ---------------------- |
24
+ | título, titulo, h1, h2, h3, headline | title, heading, headline, h1/h2/h3 | `heading` | `font-size` (size verbs); `text` (set with quoted value) |
25
+ | subtítulo, subtitulo | subtitle, subheading | `subheading` | same |
26
+ | imagem, foto, img | image, picture, photo, img | `image` | `src` (set + URL); `alt` (set + quoted text) |
27
+ | botão, botao, cta | button, btn, cta | `button` | `text` (set + quoted); `background-color` (color verbs) |
28
+ | texto, parágrafo, paragrafo, descrição | text, paragraph, copy, description | `text` | `text` (set + quoted); `font-size` (size verbs) |
29
+ | link, âncora | link, anchor | `link` | `href` (set + URL); `text` (set + quoted) |
30
+ | ícone, icone | icon | `icon` | `src` |
31
+ | fundo, background | background | `background` | `background-color` / `background-image` |
32
+ | seção, secao, section | section | `section` | (qualifier-driven) |
33
+
34
+ ## Qualifiers
35
+
36
+ | Words (PT) | Words (EN) | `target.qualifier` | Locator behavior |
37
+ | ----------------------------------- | ---------------------- | ------------------ | --------------------------------------------------- |
38
+ | hero, principal | hero, main | `hero` | element has class containing "hero"; or has ancestor with "hero" class |
39
+ | footer, rodapé, rodape | footer | `footer` | element inside `<footer>` or class containing "footer" |
40
+ | header, topo, cabeçalho, cabecalho | header, top | `header` | inside `<header>` / class containing "header" |
41
+ | primeiro, primeira, 1º, 1°, 1a, 1o | first, 1st | `first` | first match in document order |
42
+ | segundo, segunda, 2º, 2°, 2a, 2o | second, 2nd | `second` | second match |
43
+ | terceiro, terceira, 3º, 3° | third, 3rd | `third` | third match |
44
+ | último, ultima, last | last, final | `last` | last match |
45
+
46
+ When no qualifier is present and there's only one match for the target type, the locator picks it with high confidence. When there are multiple matches and no qualifier, the locator returns all candidates with `needsClarification: true`.
47
+
48
+ ## Property phrases
49
+
50
+ | Phrase (PT) | Phrase (EN) | `property` |
51
+ | ------------------------------------------- | ------------------------------------ | --------------------- |
52
+ | tamanho, font-size | size, font size | `font-size` |
53
+ | cor (alone, on text/heading) | color | `color` |
54
+ | cor de fundo, cor do fundo, background | background color, bg color | `background-color` |
55
+ | peso, font-weight | weight, font weight | `font-weight` |
56
+ | largura | width | `width` |
57
+ | altura | height | `height` |
58
+ | espaçamento, padding | padding, spacing | `padding` |
59
+ | margem, margin | margin | `margin` |
60
+ | alt, descrição alt | alt, alt text | `alt` (HTML attribute)|
61
+ | src, fonte | src, source | `src` (HTML attribute)|
62
+ | href, link | href, link | `href` (HTML attribute)|
63
+ | texto | text, content, copy | `text` (DOM text) |
64
+
65
+ ## Value extraction
66
+
67
+ | Pattern | `value.kind` | Example |
68
+ | --------------------------------------------------- | ------------- | --------------------------------------------- |
69
+ | `\d+(\.\d+)?\s*px` | `pixels` | `32px`, `1.5px` |
70
+ | `\d+(\.\d+)?\s*(rem\|em\|%\|vw\|vh)` | `length` | `2rem`, `100%` |
71
+ | `\d+(\.\d+)?` (no unit, after size verb) | `pixels` | `32` → `32px` (when context implies size) |
72
+ | `#[0-9a-f]{3,8}` | `color` | `#fff`, `#1a2b3c` |
73
+ | `rgb(...)`, `rgba(...)`, `hsl(...)` | `color` | `rgb(255,0,0)` |
74
+ | color names (red, blue, green, white, black, …) | `color` | `red` |
75
+ | `'...'` or `"..."` | `text` | `"AlphaInfuse hero"` |
76
+ | `https?://...` | `url` | `https://cdn.example.com/img.png` |
77
+ | `\d+(\.\d+)?` bare | `number` | `32`, `0.85` |
78
+
79
+ ## Examples
80
+
81
+ Each row shows: request → produced intent (key fields) → locator behavior → edit applied.
82
+
83
+ ### Sizing (PT)
84
+
85
+ > **"aumenta o título do hero pra 32px"**
86
+ - intent: `{action: 'increase', target: {type: 'heading', qualifier: 'hero'}, property: 'font-size', value: {kind: 'pixels', parsed: 32}}`
87
+ - locator: finds `h1.hero__title` (or whatever heading is inside the hero scope) → CSS rule scope.
88
+ - edit: rewrites the `font-size` declaration of the matching CSS rule from `27px` (or whatever) to `32px`.
89
+
90
+ > **"diminui o subtítulo para 14px"**
91
+ - intent: `{action: 'decrease', target: {type: 'subheading'}, property: 'font-size', value: {kind: 'pixels', parsed: 14}}`
92
+ - locator: looks for `h2`/`h3` or class containing `subtitle`/`subheading`.
93
+
94
+ ### Sizing (EN)
95
+
96
+ > **"increase the hero title to 32px"** — same as the PT version above.
97
+
98
+ > **"shrink the button text to 16px"**
99
+ - intent: `{action: 'decrease', target: {type: 'button'}, property: 'font-size', value: {kind: 'pixels', parsed: 16}}`
100
+
101
+ ### Color (PT)
102
+
103
+ > **"muda a cor do botão pra #fff"**
104
+ - intent: `{action: 'set', target: {type: 'button'}, property: 'color', value: {kind: 'color', parsed: '#fff'}}`
105
+ - Note: ambiguous between text color and background — defaults to `color`. User can say "cor de fundo" for the bg variant.
106
+
107
+ > **"troca a cor de fundo do hero para #1a1a1a"**
108
+ - intent: `{action: 'set', target: {type: 'section', qualifier: 'hero'}, property: 'background-color', value: {kind: 'color', parsed: '#1a1a1a'}}`
109
+
110
+ ### Text content (PT)
111
+
112
+ > **'troca o título do hero para "Welcome back"'**
113
+ - intent: `{action: 'set', target: {type: 'heading', qualifier: 'hero'}, property: 'text', value: {kind: 'text', parsed: 'Welcome back'}}`
114
+ - edit: cheerio `.text("Welcome back")` on the located `<h1>`.
115
+
116
+ ### Attribute (EN)
117
+
118
+ > **'set the hero image alt to "AlphaInfuse hero shot"'**
119
+ - intent: `{action: 'set', target: {type: 'image', qualifier: 'hero'}, property: 'alt', value: {kind: 'text', parsed: 'AlphaInfuse hero shot'}}`
120
+ - edit: `.attr('alt', 'AlphaInfuse hero shot')`.
121
+
122
+ > **"change the hero image src to https://cdn.example.com/new.png"**
123
+ - intent: `{action: 'set', target: {type: 'image', qualifier: 'hero'}, property: 'src', value: {kind: 'url', parsed: '...'}}`
124
+
125
+ ### Add / Remove
126
+
127
+ > **'adiciona alt="Logo" na primeira imagem'**
128
+ - intent: `{action: 'add', target: {type: 'image', qualifier: 'first'}, property: 'alt', value: {kind: 'text', parsed: 'Logo'}}`
129
+ - edit: `.attr('alt', 'Logo')` (add and set are the same when the attr is missing).
130
+
131
+ > **"remove the second button"**
132
+ - intent: `{action: 'remove', target: {type: 'button', qualifier: 'second'}, value: null}`
133
+ - edit: cheerio `.remove()` on the matched element.
134
+
135
+ ## Ambiguity rules
136
+
137
+ The parser's confidence drops below `0.7` (which exits with code 4) when:
138
+
139
+ - The action verb is not recognized (e.g. "tweak the hero" — no specific verb).
140
+ - The target type is not recognized (e.g. "increase the thing").
141
+ - A `set` action has no value, or a sizing verb has neither a number nor a unit.
142
+ - The language is not PT or EN (no recognized words from either dictionary).
143
+
144
+ The locator returns multiple candidates (also exit 4) when:
145
+
146
+ - Two or more elements match the target type and have similar scores (top-2 within `0.15`).
147
+ - There's no qualifier and there are ≥ 2 matches of the target type.
148
+
149
+ In both cases, the script emits machine-readable candidates and the orchestrator/user picks one before re-invoking with `--target-selector`.
150
+
151
+ ## Out of scope (V1)
152
+
153
+ - **Animations / transitions.** The parser does not recognize "anima isso" or "fade in".
154
+ - **Multi-element edits in one request.** "muda o título e a imagem" is two separate tweaks.
155
+ - **Layout restructuring.** "move the button below the title" is not an edit, it's a recompose. Re-run the build flow instead.
156
+ - **Cross-file references.** "use the hero from page X" — the skill operates on a single file.
157
+ - **CSS rules that don't exist yet.** If the property has no rule and no inline style, the script falls back to inline-style; it does not synthesize new CSS rules in the `<style>` block (V2 enhancement).