@easysociable/cli 0.1.17 → 0.1.19

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 (25) hide show
  1. package/README.md +2 -1
  2. package/dist/easysociable.js +11408 -9432
  3. package/dist/easysociable.js.map +1 -1
  4. package/package.json +20 -17
  5. package/skills/ONBOARDING.md +2 -3
  6. package/skills/easysociable-slideshow/SKILL.md +254 -152
  7. package/skills/easysociable-slideshow/references/CONTENT_PACK.md +12 -15
  8. package/skills/easysociable-slideshow/references/PLATFORM_CAPS.md +1 -1
  9. package/skills/easysociable-slideshow/references/REFERENCES.md +97 -0
  10. package/skills/easysociable-slideshow/scripts/check-pages.mjs +136 -0
  11. package/skills/easysociable-slideshow/workflows/CREATE_RUN.md +12 -43
  12. package/skills/easysociable-slideshow/workflows/MCP_FIRST.md +1 -3
  13. package/skills/easysociable-slideshow/workflows/OUT_OF_SCOPE.md +1 -1
  14. package/skills/easysociable-template/SKILL.md +14 -14
  15. package/skills/easysociable-template/references/CATALOG_METADATA.md +2 -2
  16. package/skills/easysociable-template/references/GATEWAY.md +1 -3
  17. package/skills/easysociable-template/references/PLATFORM_SPECS.md +7 -9
  18. package/skills/easysociable-template/scripts/generate-theme-from-hex.mjs +0 -0
  19. package/skills/easysociable-template/workflows/AUTHORING_FROM_BRIEF.md +4 -4
  20. package/skills/easysociable-template/workflows/AUTHORING_FROM_IMAGE.md +1 -1
  21. package/skills/easysociable-template/workflows/AUTHORING_STUDIO.md +4 -4
  22. package/skills/easysociable-template/workflows/BIND_FORMULA_LOOK.md +39 -0
  23. package/skills/easysociable-template/workflows/DESIGN_BRIEF.md +2 -3
  24. package/skills/easysociable-tweet-image/SKILL.md +57 -0
  25. package/skills/easysociable-template/workflows/MULTI_PLATFORM_FAMILY.md +0 -81
@@ -0,0 +1,97 @@
1
+ # Reference designs
2
+
3
+ A reference is a published catalog design (one canvas: the source post's) made
4
+ from a real post. Pages are **adapted from references**: you keep a reference's
5
+ composition, type scale, spacing and chrome, and replace content, colours
6
+ (Brand) and canvas size. You never copy one verbatim and never draw a page with
7
+ no reference at all.
8
+
9
+ Find references for a page role, shuffled, as short summaries:
10
+
11
+ ```bash
12
+ easysociable templates references --role hook,cover [--no-image] [--style <style>] [--exclude <familyIds>] --json
13
+ easysociable templates source --family <familyId> --json # the design's Studio source
14
+ ```
15
+
16
+ Roles overlap in the catalog, so pass related ones together: `hook,cover` ·
17
+ `point,value,steps,checklist` · `metric,proof,evidence` · `compare` ·
18
+ `screenshot` (needs an image) · `recap` · `cta,follow`. Styles: `minimal`,
19
+ `editorial`, `swiss`, `dark`, `bold_promo`.
20
+
21
+ ## Adapting a reference (the rules that make it look designed)
22
+
23
+ 1. Start from the reference's source. Keep its chrome (header, pill, page
24
+ number, rule, footer) only if the deck keeps it on every page; keep its
25
+ margins (`left` of the first column), type scale and fonts.
26
+ 2. Fit the Run's canvas: a reference made for another canvas (3:4, 9:16, 4:5)
27
+ keeps its left margin and type scale; re-space vertically, never squash text.
28
+ 3. Change text, then fix each text box: explicit `<br />` line breaks, and
29
+ `height` = lines × lineHeight + ~12% of fontSize for descenders.
30
+ 4. Remove what you do not need; add only elements copied from another reference
31
+ with the same `style` (rule rows, a stat row).
32
+ 5. Brand chrome: the header label is the Brand name (or the topic when there is
33
+ no Brand); the CTA names the Brand handle. Never leave a reference's original
34
+ name or handle (e.g. "Will.AI.m") on a page. **Bind them** so a Brand change
35
+ updates every page: the element holding only the Brand name gets
36
+ `data-brand="name"`, only the handle `data-brand="handle"`, and an avatar /
37
+ logo `<img>` gets `data-brand="avatar"` with the Brand's `logoUrl` as `src`.
38
+ Without a Brand, leave them unbound (the topic is not the Brand).
39
+ 6. Accent colour only on large text (≥ 36px, or ≥ 28px bold) and blocks. Small
40
+ labels use the text or muted colour.
41
+ 7. Semantic `data-type` values (`title`, `subtitle`, `kicker`, `metric`,
42
+ `caption`) get Brand colours forced by Studio. Text on an accent or image
43
+ block uses `label`, `body` or `statement` with an explicit colour.
44
+ 8. One deck = one `style`. No reference twice in a Run unless the page purpose
45
+ repeats (several numbered points), and no two adjacent pages with the same
46
+ composition.
47
+
48
+ ## Avoid (what makes every Run look the same)
49
+
50
+ - The same cover composition as this Brand's previous Run, unless the user
51
+ asked for "same as last time".
52
+ - Header label + pill + page number + "SWIPE →" on every page by habit: chrome
53
+ comes from the chosen reference, or there is none.
54
+ - An accent-coloured second half of the headline on more than two pages.
55
+ - A deck that is only left-aligned text stacks: at least one page's focal point
56
+ is not a headline (a big number, an image, a quote, a comparison).
57
+ - A generic "Save this / Follow for more" CTA when the content suggests a
58
+ specific action (comment a word, try the prompt, read the source).
59
+
60
+ ## Default palettes (no Brand)
61
+
62
+ Use one in `theme.json` as `{ "color": { "canvas", "text", "textMuted", "accent", "primary" } }`
63
+ (`primary` = `accent`), matched to the deck's style:
64
+
65
+ | Style | canvas / text / textMuted / accent |
66
+ |---|---|
67
+ | `editorial` | `#F4EEE7` / `#121A2B` / `#4A4F5C` / `#D4450F` |
68
+ | `minimal`, `swiss` | `#FFFFFF` / `#15191E` / `#384352` / `#445164` |
69
+ | `dark` | `#06080C` / `#FFFFFF` / `#C7CBD3` / `#4C9BFF` |
70
+ | `bold_promo` | `#F9F9F9` / `#0A0A0A` / `#555555` / `#0066DD` |
71
+
72
+ ## Image placeholder
73
+
74
+ A replaceable image slot for a real screenshot or photo the user will add.
75
+ Studio shows an upload button when it is selected.
76
+
77
+ ```jsx
78
+ <img data-type="image" data-variable="image_0" data-image-mode="slot_default"
79
+ alt="Add: <what goes here>" src="data:image/svg+xml;base64,<SVG>"
80
+ style={{ position: 'absolute', left: 88, top: 760, width: 904, height: 388, objectFit: 'cover', borderRadius: 24 }} />
81
+ ```
82
+
83
+ `<SVG>` is base64 of a grey box with an image icon and the label, sized to the slot:
84
+
85
+ ```xml
86
+ <svg xmlns='http://www.w3.org/2000/svg' width='904' height='388' viewBox='0 0 904 388'>
87
+ <rect width='904' height='388' fill='#DDD6CC'/>
88
+ <g transform='translate(416,124)' fill='none' stroke='#8C8781' stroke-width='5' stroke-linejoin='round'>
89
+ <rect width='72' height='56' rx='8'/><path d='M6 50 L26 28 L40 42 L50 34 L66 50'/><circle cx='52' cy='16' r='6'/>
90
+ </g>
91
+ <text x='50%' y='230' text-anchor='middle' font-family='Inter, Arial, sans-serif' font-size='32' font-weight='600' fill='#6F6A64'>Add: Grok Bot app screenshot</text>
92
+ </svg>
93
+ ```
94
+
95
+ Put the same data URL in the page's `slots` under `image_N`. Number slots
96
+ `image_0`, `image_1`, … per page. Use the style's own colours for the box
97
+ (canvas-adjacent grey on light styles, `#1A1F27` on dark ones).
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ // Render every page of a content-pack.v1 with the system Chrome (no npm deps) and check it
3
+ // the way a phone viewer sees it. Exit 0 = pass, 1 = errors (fix before create), 2 = no Chrome.
4
+ //
5
+ // node check-pages.mjs pack.json --out ./shots [--brand brand.json | --theme theme.json]
6
+ //
7
+ // --brand takes `easysociable brands get --id <id> --json` output; without it the Studio
8
+ // default (dark) theme is used, same as a Run without a Brand.
9
+ import { execFileSync } from 'node:child_process'
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
11
+ import { join, resolve } from 'node:path'
12
+ import { pathToFileURL } from 'node:url'
13
+
14
+ const args = process.argv.slice(2)
15
+ const flag = name => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined }
16
+ const packPath = args.find(a => !a.startsWith('--') && a !== flag('--out') && a !== flag('--brand') && a !== flag('--theme'))
17
+ if (!packPath) { console.error('usage: node check-pages.mjs pack.json --out <dir> [--brand brand.json]'); process.exit(1) }
18
+ const outDir = resolve(flag('--out') ?? 'shots')
19
+ mkdirSync(outDir, { recursive: true })
20
+
21
+ const pack = JSON.parse(readFileSync(packPath, 'utf8'))
22
+ const themeFile = flag('--brand') ?? flag('--theme')
23
+ const brandRaw = themeFile ? JSON.parse(readFileSync(themeFile, 'utf8')) : null
24
+ const brand = brandRaw?.data ?? brandRaw
25
+ const c = brand?.colors ?? brand?.color ?? {}
26
+ // Studio default theme (apps/easysociable/app/lib/themeTokens.ts DEFAULT_AUTHORING_THEME_TOKENS).
27
+ const colors = { canvas: c.canvas ?? '#0f1419', text: c.text ?? '#e8eef5', muted: c.textMuted ?? c.muted ?? '#8b9bb0', accent: c.accent ?? '#5b8cff', primary: c.primary ?? c.accent ?? '#5b8cff' }
28
+
29
+ const chrome = [process.env.CHROME_PATH,
30
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
31
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
32
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
33
+ '/usr/bin/google-chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser',
34
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'].find(p => p && existsSync(p))
35
+ if (!chrome) { console.error('No Chrome/Chromium found (set CHROME_PATH). Render checks skipped.'); process.exit(2) }
36
+
37
+ // ponytail: mirrors Studio's forced semantic colors by data-type; update if themeTokens.ts changes.
38
+ const themeCss = `[data-authoring-theme]{--es-color-canvas:${colors.canvas};--es-color-text:${colors.text};--es-color-muted:${colors.muted};--es-color-accent:${colors.accent};--es-color-primary:${colors.primary}}
39
+ [data-authoring-theme] [data-type="post"]{background-color:var(--es-color-canvas);color:var(--es-color-text)}
40
+ [data-authoring-theme] [data-type="title"],[data-authoring-theme] [data-type="subtitle"]{color:var(--es-color-text)!important}
41
+ [data-authoring-theme] [data-type="kicker"]{color:var(--es-color-accent)!important}
42
+ [data-authoring-theme] [data-type="metric"]{color:var(--es-color-primary)!important}
43
+ [data-authoring-theme] [data-type="caption"]{color:var(--es-color-muted)!important}
44
+ html,body{margin:0;background:#fff}#report{display:none}`
45
+
46
+ // Runs inside the Chrome page: JSX source → DOM, then measure. Source is evaluated in the page sandbox, not in Node.
47
+ const pageScript = String.raw`
48
+ const UNITLESS = new Set(['fontWeight','lineHeight','opacity','zIndex','flex','flexGrow','flexShrink'])
49
+ const toCss = o => Object.entries(o).map(([k,v]) => k.replace(/[A-Z]/g, m => '-' + m.toLowerCase()) + ':' + (typeof v === 'number' && !UNITLESS.has(k) ? v + 'px' : v)).join(';')
50
+ const toHtml = jsx => jsx
51
+ .replace(/style=\{\{([\s\S]*?)\}\}/g, (_, body) => 'style="' + toCss(Function('return {' + body + '}')()).replace(/"/g, "'") + '"')
52
+ .replace(/style="([^"]*)"/g, (_, body) => 'style="' + body.replace(/(^|;)\s*([a-z]+[A-Z][A-Za-z]*)\s*:/g, (m, sep, k) => sep + k.replace(/[A-Z]/g, c => '-' + c.toLowerCase()) + ':') + '"')
53
+ .replace(/className=/g, 'class=').replace(/\{' '\}/g, ' ')
54
+ .replace(/<(div|p|h1|h2|h3|span|img)(\s[^>]*?)\s*\/>/g, (_, t, a) => t === 'img' ? '<img' + a + '>' : '<' + t + a + '></' + t + '>')
55
+ const stage = document.getElementById('stage'), GRID = location.hash === '#grid', S = 0.5
56
+ for (const [i, src] of SOURCES.entries()) {
57
+ const box = document.createElement('div')
58
+ box.setAttribute('data-authoring-theme', '')
59
+ box.innerHTML = toHtml(src.json)
60
+ if (GRID) { box.style.cssText = 'width:' + src.width * S + 'px;height:' + src.height * S + 'px;overflow:hidden'; box.firstElementChild.style.transform = 'scale(' + S + ')'; box.firstElementChild.style.transformOrigin = '0 0' }
61
+ stage.appendChild(box)
62
+ }
63
+ if (GRID) stage.style.cssText = 'display:grid;grid-template-columns:repeat(' + COLS + ',max-content);gap:16px;padding:16px;background:#888'
64
+ const lum = rgb => { const [r,g,b] = rgb.map(v => { v /= 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4 }); return 0.2126*r + 0.7152*g + 0.0722*b }
65
+ const parse = s => { const m = s.match(/rgba?\(([^)]+)\)/); if (!m) return null; const p = m[1].split(/[ ,\/]+/).filter(Boolean).map(Number); return { rgb: p.slice(0,3), a: p[3] ?? 1 } }
66
+ const ratio = (a, b) => { const [x,y] = [lum(a), lum(b)].sort((m,n) => n - m); return (x + 0.05) / (y + 0.05) }
67
+ function measure(root) {
68
+ const R = root.getBoundingClientRect(), W = R.width, H = R.height, k = W / 1080
69
+ const errors = [], warnings = []
70
+ const texts = [...root.querySelectorAll('[data-variable^="text"], p, h1, h2, h3')].filter((e, i, all) => e.textContent.trim() && !all.some(o => o !== e && o.contains(e)))
71
+ const label = e => (e.dataset.variable ? e.dataset.variable + ' ' : '') + JSON.stringify(e.textContent.trim().slice(0, 40))
72
+ let words = 0
73
+ for (const e of texts) {
74
+ const cs = getComputedStyle(e), r = e.getBoundingClientRect(), size = parseFloat(cs.fontSize), n = e.textContent.trim().split(/\s+/).length
75
+ if (e.dataset.type !== 'decoration' && e.dataset.type !== 'label') words += n
76
+ if (e.scrollHeight > e.clientHeight + 2 || e.scrollWidth > e.clientWidth + 2) errors.push('overflow: ' + label(e) + ' needs ' + e.scrollWidth + 'x' + e.scrollHeight + ', box ' + e.clientWidth + 'x' + e.clientHeight)
77
+ if (r.left < R.left - 2 || r.top < R.top - 2 || r.right > R.right + 2 || r.bottom > R.bottom + 2) errors.push('off-canvas: ' + label(e))
78
+ if (n >= 6 && size < 28 * k) warnings.push('small text (' + size + 'px, min ' + Math.round(28 * k) + '): ' + label(e))
79
+ // Contrast: the element and each coloured span against the first opaque background under its centre.
80
+ const stack = document.elementsFromPoint(r.left + r.width / 2, r.top + Math.min(r.height / 2, size))
81
+ let bg = null
82
+ for (const el of stack) { if (el.tagName === 'IMG') break; const p = parse(getComputedStyle(el).backgroundColor); if (p && p.a > 0.5) { bg = p.rgb; break } }
83
+ if (bg) for (const part of [e, ...e.querySelectorAll('span')]) {
84
+ if (part !== e && !part.textContent.trim()) continue
85
+ const pc = getComputedStyle(part), fg = parse(pc.color), fs = parseFloat(pc.fontSize), large = fs >= 36 * k || (fs >= 28 * k && Number(pc.fontWeight) >= 700)
86
+ const cr = fg && ratio(fg.rgb, bg)
87
+ if (cr && cr < 3) errors.push('contrast ' + cr.toFixed(2) + ':1 (min ' + (large ? 3 : 4.5) + '): ' + label(part))
88
+ else if (cr && !large && cr < 4.5) warnings.push('contrast ' + cr.toFixed(2) + ':1 on small text (min 4.5): ' + label(part))
89
+ }
90
+ }
91
+ for (let i = 0; i < texts.length; i++) for (let j = i + 1; j < texts.length; j++) {
92
+ const a = texts[i].getBoundingClientRect(), b = texts[j].getBoundingClientRect()
93
+ const ix = Math.min(a.right, b.right) - Math.max(a.left, b.left), iy = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top)
94
+ if (ix > 4 && iy > 4) errors.push('text overlap: ' + label(texts[i]) + ' / ' + label(texts[j]))
95
+ }
96
+ if (words > 60) warnings.push(words + ' words on one page (aim <= 45): split it')
97
+ // Largest empty horizontal band, ignoring full-bleed backgrounds.
98
+ const spans = [...root.querySelectorAll('*')].map(e => e.getBoundingClientRect()).filter(r => r.width * r.height > 0 && r.width * r.height < 0.8 * W * H).map(r => [r.top - R.top, r.bottom - R.top]).sort((a, b) => a[0] - b[0])
99
+ let cursor = 0, gap = 0
100
+ for (const [t, b] of spans) { gap = Math.max(gap, t - cursor); cursor = Math.max(cursor, b) }
101
+ gap = Math.max(gap, H - cursor)
102
+ if (gap > 0.25 * H) warnings.push('empty band ' + Math.round(gap) + 'px (' + Math.round(100 * gap / H) + '% of height): rebalance the layout')
103
+ return { errors, warnings }
104
+ }
105
+ if (!GRID) document.fonts.ready.then(() => {
106
+ document.getElementById('report').textContent = JSON.stringify([...stage.children].map(box => { try { return measure(box.firstElementChild) } catch (e) { return { errors: ['render failed: ' + e.message], warnings: [] } } }))
107
+ })
108
+ `
109
+
110
+ const pages = pack.pages.map((page, i) => ({ n: i + 1, role: page.role, json: page.layout?.studioJson, ...(page.layout?.canvas ?? { width: 1080, height: 1350 }) })).filter(p => p.json)
111
+ for (const page of pack.pages) if (!page.layout?.studioJson) console.log(`page ${pack.pages.indexOf(page) + 1}: no layout.studioJson (catalog page) — skipped`)
112
+ const COLS = 4
113
+ const html = `<!doctype html><meta charset="utf-8"><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=block">
114
+ <style>${themeCss}</style><div id="stage"></div><pre id="report"></pre>
115
+ <script>const SOURCES = ${JSON.stringify(pages).replace(/</g, '\\u003c')}, COLS = ${COLS};${pageScript}</script>`
116
+ const file = join(outDir, 'pages.html')
117
+ writeFileSync(file, html)
118
+ const W = Math.max(...pages.map(p => p.width)), Hs = pages.map(p => p.height)
119
+ const base = ['--headless=new', '--disable-gpu', '--hide-scrollbars', '--virtual-time-budget=5000']
120
+ // Measure at full size (all pages stacked, so every page is inside the viewport).
121
+ const dom = execFileSync(chrome, [...base, `--window-size=${W},${Hs.reduce((a, b) => a + b, 0)}`, '--dump-dom', pathToFileURL(file).href], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 64 << 20 })
122
+ // One contact sheet at 50% for the visual review.
123
+ const rows = Math.ceil(pages.length / COLS)
124
+ execFileSync(chrome, [...base, `--window-size=${Math.min(COLS, pages.length) * (W / 2 + 16) + 16},${rows * (Math.max(...Hs) / 2 + 16) + 16}`, `--screenshot=${join(outDir, 'grid.png')}`, pathToFileURL(file).href + '#grid'], { stdio: 'ignore' })
125
+ const raw = dom.match(/<pre id="report">([\s\S]*?)<\/pre>/)?.[1]?.replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&')
126
+ const reports = raw ? JSON.parse(raw) : pages.map(() => ({ errors: ['no report (page script did not finish)'], warnings: [] }))
127
+ let failed = false
128
+ for (const [i, page] of pages.entries()) {
129
+ const report = reports[i]
130
+ if (report.errors.length) failed = true
131
+ console.log(`page ${page.n} (${page.role}): ${report.errors.length ? 'FAIL' : 'ok'}`)
132
+ for (const e of report.errors) console.log(` ERROR ${e}`)
133
+ for (const w of report.warnings) console.log(` warn ${w}`)
134
+ }
135
+ console.log(`contact sheet: ${join(outDir, 'grid.png')} — look at it before creating the Run.`)
136
+ process.exit(failed ? 1 : 0)
@@ -1,45 +1,14 @@
1
1
  # Create Run
2
2
 
3
- 1. Write one arc line (`references/ARC.md`). Forbidden default:
4
- Cover → Point → Steps → Recap → CTA.
5
- 2. Prefer a Content Engine `pack.md` that already has **Page Contracts**,
6
- **Media**, and **Template Selection** filled. If contracts are missing,
7
- stop and hand back to **easysociable-content** — do not invent page
8
- purposes from catalog family names.
9
- 3. Build `content-pack.v1` from that argument. Page count follows the beats
10
- (TikTok often 4–8), not the number of P0 families. Each page carries an
11
- explicit `layoutFamilyId` (explicit — server must not silent-replace):
12
- Formula handoff with a Look → `look.slides[slideNumber] ?? look.layouts[beat.role]`; else the pack's
13
- Template Selection. One Run covers every platform the families support.
14
- 4. Check platform hardMin/hardMax.
15
- 5. No Look and no Template Selection → shortlist with
16
- `easysociable slideshows recommend --file contracts.json --seed <slug> --json`
17
- (MCP `recommend_slideshow_templates`). Input is the Page Contracts:
18
- `{ platform, pages: [{ role, density?, slotSummary?: { text, number, image }, requiresImage? }] }`.
19
- Pick one candidate `familyId` per page and record the reason; `supportedRoles` is a
20
- weak tag. `pages[0]` must be cover/hook class. Do not use that class on
21
- later pages. W- judgments (`fits` / `avoid`) with comparable evidence may
22
- override the recommender's order. Surface `gaps` to the user.
23
- 6. If `contentSlug` already has a Run: update only when topology matches
24
- (same page count and Layout Families). Otherwise create a new Run.
25
- 7. **Before** `create_run` / `slideshows create` (every create except
26
- Formula handoff Runs): check the knowledge hard gate — Identity Role / What I build /
27
- Point of view have no `(fill)` and are non-empty; `boundaries.md` exists
28
- with Not covering and Never claim non-empty; if `needs/` exists, ≥3 `N-`
29
- rows, else skip. Read the files or run
30
- `easysociable engine validate --run <run-dir> --require-knowledge`.
31
- Fail → hand back to **easysociable-content** / refuse create; list missing
32
- files. User must say **强制继续** / **force continue** to call Gateway.
33
- No new Gateway or MCP params.
34
- 7b. **Identity chain (ADR-0036):** confirm CLI auth is the user's Dashboard
35
- Workspace API key (not `workspace_local` / `test-key`). If needed run
36
- `easysociable auth login` (browser approval; `--api-key <key>` only when
37
- headless). Refuse create on fixture
38
- identity unless the user asked for `--allow-local-fixture`.
39
- 8. Call `create_run` / `slideshows create` through Gateway with the explicit
40
- `layoutFamilyId`s from the pack. `LAYOUT_FAMILY_NOT_FOUND` → the family has
41
- no template for this platform; tell the user, do not guess.
42
- 9. Save `runId`, `studioUrl`, and `status=draft`. Tell the user to open
43
- `studioUrl` **while logged into the same Studio account**.
44
- 10. Do not call a Deck API, create a carousel template, or send a product
45
- `pageSequence` / `arc` field.
3
+ Follow the Workflow in `SKILL.md` (login → Brand → facts → hook/structure →
4
+ look + pages from references → images → `scripts/check-pages.mjs` → create).
5
+
6
+ Notes for updates:
7
+
8
+ - Revise the same Run (SKILL.md step 8): `slideshows get`, then `slideshows
9
+ update` with `baseRevision` and only the changed `pages[]` (`pageIndex`,
10
+ `layout`, `slots`). Page count and order stay fixed; a new Run only for a
11
+ new topic or an alternative the user asked for.
12
+ - Save `runId` and `studioUrl`; the user opens `studioUrl` while logged into
13
+ the same account.
14
+ - Do not call a Deck API or send a product `pageSequence` / `arc` field.
@@ -5,9 +5,7 @@ Fallback commands use the same Gateway client:
5
5
 
6
6
  ```bash
7
7
  easysociable slideshows create --file ./pack.json --json
8
- easysociable slideshows render --id run_xxx --idempotency-key run_xxx-final-1 --json
9
- easysociable renders wait --id job_xxx --json
10
- easysociable renders export --id job_xxx --output ./exports/run_xxx/tiktok --json
8
+ easysociable slideshows get --id run_xxx --json
11
9
  ```
12
10
 
13
11
  Neither path may call a private service, Local API, Directus, R2 or a separate
@@ -1,7 +1,7 @@
1
1
  # Out of scope
2
2
 
3
3
  - DeckTemplate creation, `list_decks`, `CREATE_DECK`, or `BIND_P0_DECKS`.
4
- - Product-level `arc` / Deck `pageSequence` SKUs (ADR-0026). Structure stays in the Content Pack.
4
+ - Product-level `arc` / Deck `pageSequence` SKUs. Structure stays in the Content Pack.
5
5
  - Defaulting every Run to Cover → Point → Steps → Recap → CTA.
6
6
  - Cover/hook class templates on page 2+ (they belong on `pages[0]` only).
7
7
  - ContentDocument as a render input (ADR-0021).
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: easysociable-template
3
3
  description: >
4
- Create single-page social image templates for EasySociable via the unified
5
- Template MCP or `easysociable` CLI. Default create path is a Layout Family:
6
- one master canvas plus derived multi-platform variants (e.g. XHS 3:4, IG/Threads 4:5,
7
- TikTok 9:16, LinkedIn 1:1) with hand-tuned layouts per preset. Use when the user wants
8
- to author a page template from a reference image or text brief, write a shared visual
4
+ Internal tool (opt-in install): turn a real post's page (reference image) or a
5
+ text brief into a single-canvas reference design for the slideshow skill's
6
+ reference library, then bind it to a Formula Look. One canvas only (the
7
+ source post's size, e.g. TikTok 9:16); no multi-platform variants. Use when the
8
+ maintainer wants to author a reference from a reference image or text brief, write a shared visual
9
9
  design philosophy, explore layout concepts, co-create Theme/preview tokens, pull icons
10
10
  via search_icons/get_icon, open Authoring Studio, or publish. Do not use for
11
11
  fill-slots hosted PNG jobs (`create_render_job` is retired), multi-slide slideshow
@@ -21,7 +21,7 @@ server. Do not install or invoke `easysociable-template`, `easysociable-slidesho
21
21
 
22
22
  **This skill owns template authoring:**
23
23
 
24
- 1. **Author** templates as a **Layout Family** (master + multi-platform variants) → Studio + Publish (`workflows/MULTI_PLATFORM_FAMILY.md`).
24
+ 1. **Author** reference designs: **one canvas**, the source post's size → Studio + Publish, then bind to a Formula Look (`workflows/BIND_FORMULA_LOOK.md`). Other platforms are re-laid out by the slideshow agent at generation time, so never derive variants.
25
25
  2. Fill-slots Hosted Render (`create_render_job`) is **retired** as a paid delivery path. Customer PNG packs come from Slideshow Studio Download.
26
26
 
27
27
  It is a template factory. It is **not** a multi-slide slideshow compositor and it is **not** a PNG API.
@@ -41,9 +41,9 @@ Out of scope (hand off):
41
41
  2. Submit only public Gateway DTOs. Never invent or request private Safe AST / Binding Map / RenderPlan.
42
42
  3. Do not start Local Runtime, read content library SQLite, or call Content MCP/CLI for Template work.
43
43
  4. Catalog and create targets are **single-page** only. Never treat site Carousel packs as Agent templates.
44
- 5. After `create` / `createDraft`, the primary authoring path is `authoringStudioUrl` (`/authoring/{draftId}`). **Default:** run `workflows/MULTI_PLATFORM_FAMILY.md` (derive + hand-tune variants) before handoff unless the user asked for one platform only. Always send the handoff in `AUTHORING_STUDIO.md` (edit → **Save** → tell Agent to publish). Do not require a Hosted Preview Job for interactive authoring.
44
+ 5. After `create` / `createDraft`, the primary authoring path is `authoringStudioUrl` (`/authoring/{draftId}`). One canvas only (never `families derive`). Always send the handoff in `AUTHORING_STUDIO.md` (edit → **Save** → tell Agent to publish). Do not require a Hosted Preview Job for interactive authoring.
45
45
  6. Studio has **Save only** — no Publish button. Publish only after the user saves and **explicitly** asks to publish; then run `workflows/PUBLISH_FROM_DRAFT.md`.
46
- 7. **Do not Publish without complete P0 selection metadata** (`CATALOG_METADATA.md`). Gateway enforces this. Each family variant carries its own `platforms` / `canvasPreset` / `canvas`.
46
+ 7. **Do not Publish without complete P0 selection metadata** (`CATALOG_METADATA.md`). Gateway enforces this. What the slideshow agent filters on must be right: `supportedRoles`, `contentArchetypes`, `requiresImage`, `visualStyle`, `bestFor`.
47
47
  8. Theme/preview: co-create `ThemeTokens` on draft create (`theme.create`) → Family `preview_tokens` (ADR-0033). No authoring TemplateTheme row. Colors/fonts via semantic `--es-*` on page nodes.
48
48
  9. Layout model is **absolute positioning** on a **flat canvas tree** (see prior authoring rules / `AUTHORING_SOURCE.md`).
49
49
  10. **Runtime layout CSS:** put exact geometry in JSX inline `style={{...}}`. Never depend on Tailwind arbitrary-value classes for geometry.
@@ -76,12 +76,12 @@ easysociable auth status --json
76
76
 
77
77
  | User intent | Workflow |
78
78
  |---|---|
79
- | Text description → template (incl. multi-platform) | `DESIGN_BRIEF.md` → … → `AUTHORING_STUDIO.md` → **`MULTI_PLATFORM_FAMILY.md`** (derive + hand-tune) |
80
- | Reference image → template | `AUTHORING_FROM_IMAGE.md` → … → `AUTHORING_STUDIO.md` → **`MULTI_PLATFORM_FAMILY.md`** + reference-compare QA on master |
81
- | “只要某一个平台” | Same authoring path; skip derive / only named presets in `MULTI_PLATFORM_FAMILY.md` |
79
+ | Text description → reference | `DESIGN_BRIEF.md` → … → `AUTHORING_STUDIO.md` → publish |
80
+ | Reference image (a real post's page) → reference | `AUTHORING_FROM_IMAGE.md` → … → `AUTHORING_STUDIO.md` (reference-compare QA) → publish |
81
+ | A viral post → its Formula's Look | each page via `AUTHORING_FROM_IMAGE.md` → publish → **`BIND_FORMULA_LOOK.md`** |
82
82
  | Need gradient / photo surface / icons only | `SURFACE_REVIEW.md` |
83
83
  | **Add / change an icon** (arrow, chevron, check, …) | **MUST** `SURFACE_REVIEW.md` § Icons — search → confirm → get_icon. **Do not** hand-draw SVG paths. |
84
- | Already have authoring payload / continue Studio | `workflows/AUTHORING_STUDIO.md` (+ family derive if variants still missing) |
84
+ | Already have authoring payload / continue Studio | `workflows/AUTHORING_STUDIO.md` |
85
85
  | User says publish / 发布该模板 (after Save) | `workflows/PUBLISH_FROM_DRAFT.md` |
86
86
  | Browse published single-page templates | `list` / `get_template` + public metadata |
87
87
  | **Fill a published template → PNG/JPEG** | `workflows/FILL_AND_RENDER.md` |
@@ -89,14 +89,14 @@ easysociable auth status --json
89
89
 
90
90
  ## Core model
91
91
 
92
- - `workflows/MULTI_PLATFORM_FAMILY.md` — **default** multi-platform create (Family + derive + hand-tune)
92
+ - `workflows/BIND_FORMULA_LOOK.md` — published references → a Formula's Look (role → design)
93
93
  - `references/PLATFORM_SPECS.md`
94
94
  - `references/CATALOG_METADATA.md`
95
95
  - `references/SLOTS.md` — public slot contract for fill-slots render
96
96
  - `references/AUTHORING_SOURCE.md`
97
97
  - `references/THEME_TOKENS.md` — Family `preview_tokens` (no TemplateTheme)
98
98
  - `references/EXTERNAL_RESOURCES.md`
99
- - `references/GATEWAY.md` — CLI / MCP including families derive + `render`
99
+ - `references/GATEWAY.md` — CLI / MCP
100
100
  - `workflows/FILL_AND_RENDER.md` — get_template → slots → Hosted Render
101
101
  - Authoring V2.1 workflows + validators as before
102
102
 
@@ -5,7 +5,7 @@ Private AST/bindings stay server-side; this document is the **public selection c
5
5
 
6
6
  ## Design choices (locked)
7
7
 
8
- 1. **One closed `pageRoles` set** (stored as `supportedRoles`) — a **retrieval tag for layout kind**, not page position and not a story recipe. `cover` means poster-like slots, not “must be page 1”. Do not add a parallel `arc` / `pageSequence` taxonomy (ADR-0026).
8
+ 1. **One closed `pageRoles` set** (stored as `supportedRoles`) — a **retrieval tag for layout kind**, not page position and not a story recipe. `cover` means poster-like slots, not “must be page 1”. Do not add a parallel `arc` / `pageSequence` taxonomy.
9
9
  2. **`platforms` is a multi-select array**. Include `generic` when the page fits any platform at that aspect.
10
10
  3. **P0 fields are required to Publish.** P1 should be filled when known; P2 optional.
11
11
  4. **Family `name` describes layout**, not a narrative beat. Prefer Poster / Stack / Count / Stair / Close (plus platform). Do not name families Cover / Point / Steps / Recap / CTA — choosers will glue those five names into one default pack.
@@ -99,7 +99,7 @@ hero | screenshot | avatar | product
99
99
 
100
100
  | Field | Type | Notes |
101
101
  |---|---|---|
102
- | `name` | string | Human title — **layout**, not beat (e.g. `P0 Poster TikTok`, not `P0 Cover TikTok`) |
102
+ | `name` | string | Public card title: what the layout is + style, then the ratio (e.g. `Five-Step How-To List — Quiet Ledger 9:16`). No internal prefixes (`P0`), no single platform name — the family spans platforms; the ratio suffix is stripped on cards. Keep equal to the page `name` |
103
103
  | `shortDescription` | string | One sentence for chooser UI |
104
104
  | `platforms` | `Platform[]` | Multi-select; use `generic` when truly cross-platform |
105
105
  | `canvasPreset` | preset enum | Plus exact `canvas` pixels |
@@ -12,15 +12,13 @@ easysociable templates update-draft --id <id> --file ./patch.json --json
12
12
  easysociable templates validate --id <id> --json
13
13
  easysociable templates publish --id <id> --json
14
14
  easysociable templates families get --id <familyId> --json
15
- easysociable templates families derive --id <familyId> --presets 4:5,9:16,1:1 --json
16
- easysociable templates families migrate-legacy --json
17
15
  easysociable icons search --query "arrow" --json
18
16
  easysociable icons get --id <id> --idempotency-key <key> --json
19
17
  easysociable renders create --template-id <id> --slots ./slots.json --format png --idempotency-key render_1 --json
20
18
  easysociable renders wait --id <id> --json
21
19
  ```
22
20
 
23
- Multi-platform create flow (derive + hand-tune): `workflows/MULTI_PLATFORM_FAMILY.md`.
21
+ One canvas per reference; binding to a Formula: `workflows/BIND_FORMULA_LOOK.md`.
24
22
 
25
23
  All requests go through Gateway. The client receives public summaries, slot
26
24
  contracts, stable Image references and render Manifests only. It never receives
@@ -120,20 +120,18 @@ No platform chrome assumptions. Still pick a concrete preset (`1:1`, `4:5`, `3:4
120
120
 
121
121
  ---
122
122
 
123
- ## Layout Family (multi-platform create)
123
+ ## One canvas per reference
124
124
 
125
- Creating a template **defaults** to a Layout Family with one master page + derived presets — see `workflows/MULTI_PLATFORM_FAMILY.md`.
126
-
127
- Same-preset platforms share one variant tree (**Instagram + Threads → `4:5`**). Do not invent a second 4:5 page for Threads.
128
-
129
- Typical vertical bucket when master is `3:4` (Xiaohongshu): also derive `4:5`, `9:16`, `1:1`. Absolute layout cannot share one AST across those presets — hand-tune each variant after derive.
125
+ A reference design has exactly one canvas: the source post's (a TikTok post → `9:16`).
126
+ Never derive other presets; the slideshow agent re-lays a reference out for the
127
+ Run's platform.
130
128
 
131
129
  ## Selection rules for Agents
132
130
 
133
- 1. If user names one platform only → set master `platforms: [that]` + that platform’s **Recommended** row; skip family derive unless they later ask for more.
134
- 2. If user names several **or** does not limit platforms → pick a master Recommended canvas, then derive the default vertical targets (`MULTI_PLATFORM_FAMILY.md`).
131
+ 1. Canvas = the source post's platform **Recommended** row (text brief with no post → TikTok 9:16).
132
+ 2. One canvas only; no `families derive`.
135
133
  3. Never mix presets across slides of one deck without an explicit user choice (IG/Threads/TikTok crop to first slide).
136
- 4. Each family variant’s P0 `selectionMetadata` must list only platforms that match that page’s `canvasPreset`.
134
+ 4. P0 `selectionMetadata` lists only platforms that match the page's `canvasPreset`.
137
135
  5. Specs above are **practical authoring targets** (2026 guides + Meta help for IG). Re-verify if a platform announces breaking crop changes.
138
136
 
139
137
  ## Sources (research snapshot)
@@ -4,12 +4,12 @@ Primary intake when the user describes a single-page template in words (no refer
4
4
 
5
5
  ## Goal
6
6
 
7
- One publishable **single-page** template **Family** (master canvas + multi-platform variants by default): Design Brief → Design Philosophy → 2–3 Concepts → Asset Plan → Theme lock → master layout → Authoring Studio → `MULTI_PLATFORM_FAMILY.md`.
7
+ One publishable **single-page, single-canvas** reference design: Design Brief → Design Philosophy → 2–3 Concepts → Asset Plan → Theme lock → layout → Authoring Studio → publish.
8
8
 
9
9
  ## Steps
10
10
 
11
- 1. Confirm this is **one page**, not a slideshow. If they want a multi-slide deck, stop and hand off (`OUT_OF_SCOPE.md`). Multi-platform variants of the same page are expected — that is not a slideshow.
12
- 2. Read `references/PLATFORM_SPECS.md`. Lock **master** platform + recommended canvas, and the default vertical derive targets unless the user limited platforms (`MULTI_PLATFORM_FAMILY.md`).
11
+ 1. Confirm this is **one page**, not a slideshow. If they want a multi-slide deck, stop and hand off (`OUT_OF_SCOPE.md`).
12
+ 2. Read `references/PLATFORM_SPECS.md`. Lock platform + recommended canvas (one canvas).
13
13
  3. **Mandatory — V2.1 design plan first** (no reference image):
14
14
  - Follow `workflows/DESIGN_BRIEF.md`
15
15
  - Follow `workflows/DESIGN_PHILOSOPHY.md`; lock shared `philosophy` (+ companion `design-philosophy.md`)
@@ -30,7 +30,7 @@ One publishable **single-page** template **Family** (master canvas + multi-platf
30
30
  - `authoringSource` per `references/AUTHORING_SOURCE.md` (flat tree + text-nesting exception)
31
31
  - Theme per `references/THEME_TOKENS.md` — use `themeTokens` from validated `theme-tokens.json` for `theme.create`
32
32
  5. Run `scripts/validate-authoring-source.mjs <authoring-source.json>` and fix every error.
33
- 6. Continue with `workflows/AUTHORING_STUDIO.md` (`source.kind = text_brief`) → **`MULTI_PLATFORM_FAMILY.md`** (derive + hand-tune + QA each variant) before handoff.
33
+ 6. Continue with `workflows/AUTHORING_STUDIO.md` (`source.kind = text_brief`) → Visual QA before handoff.
34
34
 
35
35
  ## Checklist pages
36
36
 
@@ -79,7 +79,7 @@ If unsure whether a region is “illustration” vs “icon”: prefer **image s
79
79
  9. Propose Theme reuse or `ThemeTokens` create from `theme-tokens.json` (`references/THEME_TOKENS.md`).
80
80
  10. Show the user: geometry summary, **image inventory → slot map**, selection metadata, Theme. Call out any region you could not match and why. Proceed when they accept.
81
81
  11. Run `scripts/validate-authoring-source.mjs <authoring-source.json>` and fix every error.
82
- 12. Continue with `workflows/AUTHORING_STUDIO.md` (`source.kind = image_reference`, brief = path or short note) → **`MULTI_PLATFORM_FAMILY.md`** (derive + hand-tune other presets). Run Visual QA **including reference compare on the master** before handoff; still QA each derived variant for fit/clipping.
82
+ 12. Continue with `workflows/AUTHORING_STUDIO.md` (`source.kind = image_reference`, brief = path or short note) Run Visual QA **including reference compare** before handoff. One canvas only; after publish, bind it with `BIND_FORMULA_LOOK.md` when it comes from a Formula's post.
83
83
 
84
84
  ## Limits (state upfront when relevant)
85
85
 
@@ -18,11 +18,11 @@ Complete intake first:
18
18
  ## Layout Family (multi-platform)
19
19
 
20
20
  New drafts create a **Layout Family** (`familyId`) plus a master canvas page.
21
- **Default create path:** after the master draft exists, follow `MULTI_PLATFORM_FAMILY.md` (derive + hand-tune variants) before handoff unless the user asked for one platform only.
21
+ One canvas per reference: never derive platform variants.
22
22
 
23
23
  - Authoring URL: `/authoring/{draftId}?platform=…`. Top bar **platform chips** switch variants (do not resize one AST).
24
24
  - Preview skin = Family `preview_tokens` from `theme.create`; fill/render uses Brand snapshot first (ADR-0033).
25
- - Legacy drafts without `familyId` auto-wrap on `getDraft` / `listDrafts`. Bulk: `easysociable templates families migrate-legacy --json`.
25
+ - Legacy drafts without `familyId` auto-wrap on `getDraft` / `listDrafts`.
26
26
 
27
27
  ## Flow
28
28
 
@@ -47,9 +47,9 @@ AST or Binding Maps.
47
47
 
48
48
  3. Read `data.id` / `data.templateId` / `data.familyId` / `data.authoringStudioUrl` (and `data.variants` when present).
49
49
 
50
- 4. **Multi-platform (default):** run `workflows/MULTI_PLATFORM_FAMILY.md` — derive target presets, hand-tune each variant, QA each canvas. Skip only when the user asked for a single platform.
50
+ 4. One canvas only: do not run `families derive`.
51
51
 
52
- 5. **Before handoff, run `workflows/VISUAL_QA.md`** on the master (and each variant from step 4). Open the actual Studio render; inspect screenshots, not only source. When a draft fails, fix authoring JSON, validate, update that page id, and re-render:
52
+ 5. **Before handoff, run `workflows/VISUAL_QA.md`** on the page. Open the actual Studio render; inspect screenshots, not only source. When a draft fails, fix authoring JSON, validate, update that page id, and re-render:
53
53
 
54
54
  ```bash
55
55
  node scripts/validate-authoring-source.mjs ./authoring-source.json
@@ -0,0 +1,39 @@
1
+ # Bind references to a Formula Look
2
+
3
+ A Formula is the structure distilled from a real viral post; its **Look** says
4
+ which reference design each slide uses. The slideshow skill reads it
5
+ (`look.slides["<n>"]`, else `look.layouts[<role>]`) whenever that Formula is used.
6
+
7
+ ## When
8
+
9
+ After publishing the references for a post's pages (one reference per distinct
10
+ page layout, via `AUTHORING_FROM_IMAGE.md`).
11
+
12
+ ## Steps
13
+
14
+ 1. `easysociable formulas get --id <F-…> --json` — note its `beats` (slide order
15
+ and roles) and existing `looks`.
16
+ 2. Map each beat to a published design's `familyId`
17
+ (`easysociable templates list --canvas 9:16 --json` shows `familyId`):
18
+ - `layouts`: one design per role (`hook`, `point`, `cta`, …);
19
+ - `slides`: only for a slide whose design differs from its role's default
20
+ (1-based slide number → `familyId`).
21
+ 3. Write the Look:
22
+
23
+ ```json
24
+ { "id": "L-<short-name>",
25
+ "ref": { "platform": "tiktok", "platform_post_id": "<the post's id>" },
26
+ "layouts": { "hook": "fam_…", "point": "fam_…", "cta": "fam_…" },
27
+ "slides": { "2": "fam_…" } }
28
+ ```
29
+
30
+ 4. There is no public write API for Formulas: hand the JSON to the maintainer
31
+ to append to that Formula's `looks` in the Directus admin (a new Look goes
32
+ last, so the Formula's default Look — the first — does not change unless
33
+ they choose to reorder).
34
+ 5. Check: `easysociable formulas get --id <F-…> --json` shows the Look, and
35
+ every `familyId` in it resolves with
36
+ `easysociable templates source --family <familyId> --json`.
37
+
38
+ Never bind an agent-authored design that was not made from a real post: the
39
+ reference library only holds proven designs.
@@ -16,7 +16,7 @@ Skip this workflow when `AUTHORING_FROM_IMAGE.md` applies and a reference image
16
16
 
17
17
  ### 1. Gather intent (batch questions; skip known answers)
18
18
 
19
- - **Master** platform + canvas from `references/PLATFORM_SPECS.md`, plus default multi-platform derive targets (`MULTI_PLATFORM_FAMILY.md`) unless the user wants one platform only
19
+ - Platform + canvas from `references/PLATFORM_SPECS.md` (one canvas: the source post's)
20
20
  - Page roles (1–2) + contentFamily / archetypes / density (`CATALOG_METADATA.md`)
21
21
  - Content job: what must the viewer understand in one glance?
22
22
  - Must-have slots: title / metric / image / CTA / fixed N checklist lines…
@@ -31,8 +31,7 @@ Use this structure (Chinese or English to match the user):
31
31
 
32
32
  ## 1. Job to be done
33
33
  - One-sentence purpose of this page
34
- - Master platform + canvas (preset + pixels) — cite PLATFORM_SPECS recommended row
35
- - Multi-platform targets (default vertical bucket unless user limited) — `MULTI_PLATFORM_FAMILY.md`
34
+ - Platform + canvas (preset + pixels) — cite PLATFORM_SPECS recommended row
36
35
 
37
36
  ## 2. Visual constraints
38
37
  - Desired/forbidden moods or brand rules