@cohortapp/agent-sdk 2.12.0 → 2.13.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.
- package/bin/maestro.mjs +6 -2
- package/docs/guides/front-door-session.md +49 -0
- package/lib/cli/design.mjs +185 -0
- package/lib/cli/design.test.mjs +270 -0
- package/lib/cli/global-setup-extras.mjs +44 -0
- package/lib/cli/global-setup-extras.test.mjs +95 -0
- package/lib/cli/session.mjs +11 -1
- package/lib/cli/session.test.mjs +17 -6
- package/lib/collective/global-config.mjs +5 -0
- package/lib/collective/global-config.test.mjs +5 -0
- package/lib/collective/vendor-skills.mjs +305 -0
- package/lib/collective/vendor-skills.test.mjs +306 -0
- package/lib/design/design-md.mjs +793 -0
- package/lib/design/design-md.test.mjs +318 -0
- package/lib/design/fixtures/DESIGN.golden.md +238 -0
- package/lib/design/fixtures/PRODUCT.golden.md +67 -0
- package/lib/design/fixtures/foundation.json +133 -0
- package/lib/design/refresh-gate.mjs +154 -0
- package/lib/design/refresh-gate.test.mjs +144 -0
- package/lib/design/write.mjs +275 -0
- package/lib/design/write.test.mjs +241 -0
- package/lib/prompts/parallelism.mjs +79 -0
- package/lib/prompts/parallelism.test.mjs +177 -0
- package/package.json +1 -1
- package/plugins/maestro-skills/plugin.json +4 -0
- package/plugins/maestro-skills/skills/cohort-design.md +153 -0
- package/plugins/maestro-skills/vendor/emilkowalski/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/emilkowalski/UPSTREAM.json +70 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/RECIPES.md +324 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/SKILL.md +199 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animation-vocabulary/SKILL.md +173 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/apple-design/SKILL.md +282 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/emil-design-eng/SKILL.md +674 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/find-animation-opportunities/SKILL.md +132 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/AUDIT.md +115 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/PLAN-TEMPLATE.md +73 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/SKILL.md +101 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/PICKER.md +197 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/SKILL.md +90 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/SKILL.md +112 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/STANDARDS.md +187 -0
- package/plugins/maestro-skills/vendor/impeccable/LICENSE +191 -0
- package/plugins/maestro-skills/vendor/impeccable/NOTICE.md +11 -0
- package/plugins/maestro-skills/vendor/impeccable/SKILL.md +86 -0
- package/plugins/maestro-skills/vendor/impeccable/UPSTREAM.json +201 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-asset-producer.md +42 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-documenter.md +29 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-finish-reviewer.md +43 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-manual-edit-applier.md +97 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/adapt.md +312 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/adapt.native.md +58 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/android.md +46 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/animate.md +89 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/audit.md +136 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/audit.native.md +139 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/bolder.md +33 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/clarify.md +94 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/colorize.md +86 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/craft-floor.md +44 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/craft.md +5 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/critique.md +806 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/asset-producer.md +37 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/documenter.md +24 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/finish-reviewer.md +38 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/delight.md +70 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/distill.md +111 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/doctor.md +54 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/document.md +416 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/extract.md +69 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/harden.md +336 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/hooks.md +111 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/init.md +131 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/ios.md +51 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/layout.md +84 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/live-setup.md +104 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/live.md +325 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/new-work.md +147 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/onboard.md +234 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/operate.md +61 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/optimize.md +258 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/overdrive.md +127 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/polish.md +105 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/quieter.md +99 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/routing.md +24 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/shape.md +59 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/typeset.md +80 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/visualize.md +46 -0
- package/plugins/maestro-skills/vendor/taste-skill/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/taste-skill/UPSTREAM.json +37 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/minimalist-skill/SKILL.md +85 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/redesign-skill/SKILL.md +178 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/soft-skill/SKILL.md +98 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/taste-skill/SKILL.md +1206 -0
- package/plugins/maestro-skills/vendor/unlazy/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/unlazy/SECURITY.md +72 -0
- package/plugins/maestro-skills/vendor/unlazy/SKILL.md +104 -0
- package/plugins/maestro-skills/vendor/unlazy/UPSTREAM.json +94 -0
- package/plugins/maestro-skills/vendor/unlazy/references/dispatch.md +82 -0
- package/plugins/maestro-skills/vendor/unlazy/references/gates.md +149 -0
- package/plugins/maestro-skills/vendor/unlazy/references/method.md +49 -0
- package/plugins/maestro-skills/vendor/unlazy/references/orchestration.md +107 -0
- package/plugins/maestro-skills/vendor/unlazy/references/parallel.md +133 -0
- package/plugins/maestro-skills/vendor/unlazy/references/token-economy.md +48 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/dispatch-check.mjs +139 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/gate-check.mjs +960 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/gate-lint.mjs +245 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/check-supervisor.mjs +46 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/dispatch.mjs +293 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/gates.mjs +953 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/process-tree.mjs +161 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/regex-worker.mjs +9 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/PLAN.md +116 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/gates-leaf.md +51 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/gates-node.md +51 -0
- package/scripts/ci/check-skill-packs.mjs +388 -0
- package/scripts/ci/check-skill-packs.test.mjs +495 -0
- package/scripts/ci/check.mjs +3 -0
- package/scripts/daemon/agent-daemon-design.test.mjs +238 -0
- package/scripts/daemon/agent-daemon.mjs +108 -0
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +61 -2
- package/scripts/daemon/cadence-consumer.mjs +46 -22
- package/scripts/daemon/prompt-builder.mjs +19 -3
- package/scripts/local-triggers/autoupdate.test.mjs +33 -3
- package/scripts/vendor/skill-packs.mjs +354 -0
- package/scripts/vendor/sync-skill-packs.mjs +242 -0
- package/scripts/vendor/sync-skill-packs.test.mjs +103 -0
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/design/design-md.mjs — the brand foundation, rendered as DESIGN.md and
|
|
3
|
+
* PRODUCT.md (WP-M7 mechanic 4).
|
|
4
|
+
*
|
|
5
|
+
* WHAT THIS IS FOR. Every design skill on a seat (vendored or authored) is told
|
|
6
|
+
* to read `$AGENT_ROOT/DESIGN.md` before it picks a colour, a typeface or a
|
|
7
|
+
* radius. That file must therefore be a faithful, mechanical projection of the
|
|
8
|
+
* workspace's SAVED brand foundation — `branding.getFoundation` / the
|
|
9
|
+
* `design_foundation` tool — in the shape the ecosystem already reads: the
|
|
10
|
+
* google-labs-code/design.md spec (YAML frontmatter carrying `colors`,
|
|
11
|
+
* `typography`, `rounded`, `spacing` and `components` written with
|
|
12
|
+
* `{colors.primary}`-style refs, then prose).
|
|
13
|
+
*
|
|
14
|
+
* PURE. Everything that could differ between two runs is injected: there is no
|
|
15
|
+
* clock read, no `process.env`, no filesystem and no network here. Given the
|
|
16
|
+
* same `(foundation, opts)` it returns the same bytes — which is what makes the
|
|
17
|
+
* CLI's "re-running with an unchanged foundation rewrites nothing" claim
|
|
18
|
+
* checkable by comparing rendered bytes, and what makes the golden test honest.
|
|
19
|
+
*
|
|
20
|
+
* TOLERANT BY CONTRACT. The foundation is a stored JSON blob that a newer hq
|
|
21
|
+
* may extend (hq's own `coerceFoundation` is deliberately lenient for the same
|
|
22
|
+
* reason). A key this renderer does not model is NOT dropped: it lands in the
|
|
23
|
+
* "Additional tokens" section verbatim, so a token added upstream is visible to
|
|
24
|
+
* a designer on the seat the day it ships, before anyone teaches this module
|
|
25
|
+
* about it. Silence is the failure mode worth engineering against — a dropped
|
|
26
|
+
* token looks exactly like a token that was never set.
|
|
27
|
+
*
|
|
28
|
+
* Node builtins only (none needed). ESM.
|
|
29
|
+
*
|
|
30
|
+
* @module lib/design/design-md
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
"use strict";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Top-level foundation keys this renderer EMITS somewhere. Anything else is
|
|
37
|
+
* "additional" — see the module docstring.
|
|
38
|
+
*
|
|
39
|
+
* "Models" means emits, not "recognises". `examples` is a real hq field
|
|
40
|
+
* (`z.any()` in `brandFoundationSchema`) that no renderer here reads, so it is
|
|
41
|
+
* deliberately ABSENT from this list and lands in "Additional tokens" verbatim.
|
|
42
|
+
* Listing a key here that nothing emits is how a token gets dropped in silence
|
|
43
|
+
* — the one failure this module is built to make impossible.
|
|
44
|
+
* @type {string[]}
|
|
45
|
+
*/
|
|
46
|
+
export const KNOWN_FOUNDATION_KEYS = [
|
|
47
|
+
"v",
|
|
48
|
+
"colors",
|
|
49
|
+
"fonts",
|
|
50
|
+
"type",
|
|
51
|
+
"space",
|
|
52
|
+
"radii",
|
|
53
|
+
"shadow",
|
|
54
|
+
"border",
|
|
55
|
+
"voice",
|
|
56
|
+
"positioning",
|
|
57
|
+
"taglines",
|
|
58
|
+
"toneTraits",
|
|
59
|
+
"logos",
|
|
60
|
+
"visualLanguage",
|
|
61
|
+
"system",
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* `system.*` sub-keys this renderer emits (DESIGN.md, except `voice` which is
|
|
66
|
+
* PRODUCT.md's). `system` is a known TOP-LEVEL key, so without this list an
|
|
67
|
+
* unmodelled sub-block — `system.imagery` was exactly this — would be invisible
|
|
68
|
+
* to "Additional tokens" as well as to the prose. Keep in step with hq
|
|
69
|
+
* `src/lib/branding/foundation.ts#brandSystemSchema`.
|
|
70
|
+
* @type {string[]}
|
|
71
|
+
*/
|
|
72
|
+
export const KNOWN_SYSTEM_KEYS = [
|
|
73
|
+
"typography",
|
|
74
|
+
"color",
|
|
75
|
+
"imagery",
|
|
76
|
+
"motifs",
|
|
77
|
+
"iconography",
|
|
78
|
+
"layoutPrinciples",
|
|
79
|
+
"voice",
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `visualLanguage.*` sub-keys this renderer emits. Same reasoning as
|
|
84
|
+
* {@link KNOWN_SYSTEM_KEYS}; mirrors hq's `visualLanguageSchema`.
|
|
85
|
+
* @type {string[]}
|
|
86
|
+
*/
|
|
87
|
+
export const KNOWN_VISUAL_LANGUAGE_KEYS = [
|
|
88
|
+
"v",
|
|
89
|
+
"source",
|
|
90
|
+
"imagery",
|
|
91
|
+
"iconography",
|
|
92
|
+
"motion",
|
|
93
|
+
"texture",
|
|
94
|
+
"guardrails",
|
|
95
|
+
"promptSeed",
|
|
96
|
+
"seedLocked",
|
|
97
|
+
"negativeSeed",
|
|
98
|
+
"references",
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
/** Canonical colour ROLES a component ref may name, in preference order. */
|
|
102
|
+
const COLOR_ROLES = [
|
|
103
|
+
"primary",
|
|
104
|
+
"secondary",
|
|
105
|
+
"accent",
|
|
106
|
+
"ink",
|
|
107
|
+
"paper",
|
|
108
|
+
"muted",
|
|
109
|
+
"line",
|
|
110
|
+
"background",
|
|
111
|
+
"foreground",
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
// ── small pure helpers ──────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/** Is this a plain object (not null, not an array)? */
|
|
117
|
+
function isObj(v) {
|
|
118
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** An array, or []. */
|
|
122
|
+
function arr(v) {
|
|
123
|
+
return Array.isArray(v) ? v : [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** A trimmed non-empty string, or "". */
|
|
127
|
+
function str(v) {
|
|
128
|
+
return typeof v === "string" && v.trim() ? v.trim() : "";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A token name → a YAML/ref-safe key. `"Primary · Moss"` → `"primary-moss"`,
|
|
133
|
+
* `"2xl"` → `"2xl"`. Empty input yields `""` so the caller can skip it.
|
|
134
|
+
* @param {unknown} name
|
|
135
|
+
* @returns {string}
|
|
136
|
+
*/
|
|
137
|
+
export function slugToken(name) {
|
|
138
|
+
return String(name ?? "")
|
|
139
|
+
.normalize("NFKD")
|
|
140
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
141
|
+
.toLowerCase()
|
|
142
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
143
|
+
.replace(/^-+|-+$/g, "");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Double-quote a scalar for YAML, escaping backslashes and quotes. */
|
|
147
|
+
function q(v) {
|
|
148
|
+
return `"${String(v ?? "").replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** A dimension token value: numbers become px, strings pass through. */
|
|
152
|
+
function dim(v) {
|
|
153
|
+
if (typeof v === "number" && Number.isFinite(v)) return `${v}px`;
|
|
154
|
+
return str(v);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** `[key, value]` pairs of an ordered map, as YAML lines at `indent`. */
|
|
158
|
+
function yamlMap(pairs, indent) {
|
|
159
|
+
return pairs.map(([k, v]) => `${indent}${k}: ${q(v)}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Insert into an ordered map only when the key is free and the value real.
|
|
164
|
+
* First writer wins — the foundation's own palette outranks a derived alias.
|
|
165
|
+
*/
|
|
166
|
+
function put(map, key, value) {
|
|
167
|
+
const k = slugToken(key);
|
|
168
|
+
const v = str(value);
|
|
169
|
+
if (!k || !v || Object.prototype.hasOwnProperty.call(map, k)) return;
|
|
170
|
+
map[k] = v;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── token extraction ────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The `colors` block: every palette entry by slugified name, then the canonical
|
|
177
|
+
* role aliases a component ref can name (`primary`, `accent`, …) resolved from
|
|
178
|
+
* `system.color.roles` and, failing that, from a `"<role> · <shade>"` palette
|
|
179
|
+
* name. Also folds in `system.color.semantic` and `system.color.neutralRamp`,
|
|
180
|
+
* which are real tokens no other block would carry.
|
|
181
|
+
*
|
|
182
|
+
* @param {object} foundation
|
|
183
|
+
* @returns {{light:Record<string,string>, dark:Record<string,string>}}
|
|
184
|
+
*/
|
|
185
|
+
export function colorTokens(foundation) {
|
|
186
|
+
const light = {};
|
|
187
|
+
const dark = {};
|
|
188
|
+
for (const c of arr(foundation && foundation.colors)) {
|
|
189
|
+
if (!isObj(c)) continue;
|
|
190
|
+
const key = slugToken(c.name);
|
|
191
|
+
if (!key) continue;
|
|
192
|
+
put(light, key, c.value);
|
|
193
|
+
if (str(c.dark)) put(dark, key, c.dark);
|
|
194
|
+
}
|
|
195
|
+
const roles = isObj(foundation && foundation.system) && isObj(foundation.system.color) ? foundation.system.color.roles : null;
|
|
196
|
+
if (isObj(roles)) {
|
|
197
|
+
for (const role of ["primary", "secondary", "accent"]) {
|
|
198
|
+
const r = roles[role];
|
|
199
|
+
if (!isObj(r)) continue;
|
|
200
|
+
put(light, role, r.value);
|
|
201
|
+
if (str(r.dark)) put(dark, role, r.dark);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Alias `primary` → `primary-moss` etc. so `{colors.primary}` always resolves
|
|
205
|
+
// on a palette that names its shades.
|
|
206
|
+
for (const role of COLOR_ROLES) {
|
|
207
|
+
if (light[role]) continue;
|
|
208
|
+
const hit = Object.keys(light).find((k) => k.startsWith(`${role}-`));
|
|
209
|
+
if (hit) {
|
|
210
|
+
light[role] = light[hit];
|
|
211
|
+
if (dark[hit]) dark[role] = dark[hit];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const colorBlock = isObj(foundation && foundation.system) ? foundation.system.color : null;
|
|
215
|
+
if (isObj(colorBlock)) {
|
|
216
|
+
if (isObj(colorBlock.semantic)) {
|
|
217
|
+
for (const [name, value] of Object.entries(colorBlock.semantic)) {
|
|
218
|
+
const k = slugToken(name);
|
|
219
|
+
if (!k) continue;
|
|
220
|
+
put(light, Object.prototype.hasOwnProperty.call(light, k) ? `semantic-${k}` : k, value);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const step of arr(colorBlock.neutralRamp)) {
|
|
224
|
+
if (!isObj(step)) continue;
|
|
225
|
+
put(light, `neutral-${slugToken(step.step)}`, step.value);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return { light, dark };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* A `[{name, value}]` token array → an ordered map of slug → dimension string.
|
|
233
|
+
* @param {unknown} list
|
|
234
|
+
* @returns {Record<string,string>}
|
|
235
|
+
*/
|
|
236
|
+
export function scaleTokens(list) {
|
|
237
|
+
const out = {};
|
|
238
|
+
for (const t of arr(list)) {
|
|
239
|
+
if (!isObj(t)) continue;
|
|
240
|
+
put(out, t.name, dim(t.value));
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The `typography` block: the three faces plus the role scale.
|
|
247
|
+
* @param {object} foundation
|
|
248
|
+
* @returns {{faces:Record<string,string>, scale:Array<{key:string, size:string, weight:string, leading:string, tracking:string}>}}
|
|
249
|
+
*/
|
|
250
|
+
export function typographyTokens(foundation) {
|
|
251
|
+
const fonts = isObj(foundation && foundation.fonts) ? foundation.fonts : {};
|
|
252
|
+
const faces = {};
|
|
253
|
+
for (const role of ["display", "heading", "body", "mono"]) {
|
|
254
|
+
if (str(fonts[role])) faces[role] = str(fonts[role]);
|
|
255
|
+
}
|
|
256
|
+
const families = isObj(foundation && foundation.system) && isObj(foundation.system.typography)
|
|
257
|
+
? foundation.system.typography.families
|
|
258
|
+
: null;
|
|
259
|
+
if (isObj(families)) {
|
|
260
|
+
for (const role of ["display", "heading", "body", "mono"]) {
|
|
261
|
+
if (faces[role]) continue;
|
|
262
|
+
const f = families[role];
|
|
263
|
+
if (isObj(f) && str(f.family)) faces[role] = str(f.family);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// WHERE LEADING AND TRACKING ACTUALLY LIVE. `foundation.type` is hq's
|
|
267
|
+
// `typeRoleSchema` — `{role, size, weight}`, and nothing else, ever. The
|
|
268
|
+
// line-height and letter-spacing a workspace tunes are carried by
|
|
269
|
+
// `system.typography.scale` (`{role, size, weight, leading, tracking}`), a
|
|
270
|
+
// separate array keyed by the same role names. Reading them off `type` was
|
|
271
|
+
// dead code: the fields are never there. So the two are merged by role —
|
|
272
|
+
// `type` wins on size/weight (it is the block the Design surface writes),
|
|
273
|
+
// the system scale supplies leading/tracking and any role `type` omits.
|
|
274
|
+
const systemScale = isObj(foundation && foundation.system) && isObj(foundation.system.typography)
|
|
275
|
+
? arr(foundation.system.typography.scale)
|
|
276
|
+
: [];
|
|
277
|
+
const bySystemRole = new Map();
|
|
278
|
+
for (const t of systemScale) {
|
|
279
|
+
if (!isObj(t)) continue;
|
|
280
|
+
const key = slugToken(t.role);
|
|
281
|
+
if (!key || bySystemRole.has(key)) continue;
|
|
282
|
+
bySystemRole.set(key, t);
|
|
283
|
+
}
|
|
284
|
+
const num = (v) => (Number.isFinite(v) ? String(v) : "");
|
|
285
|
+
const seen = new Set();
|
|
286
|
+
const scale = [];
|
|
287
|
+
for (const t of arr(foundation && foundation.type)) {
|
|
288
|
+
if (!isObj(t)) continue;
|
|
289
|
+
const key = slugToken(t.role);
|
|
290
|
+
if (!key || seen.has(key)) continue;
|
|
291
|
+
seen.add(key);
|
|
292
|
+
const sys = bySystemRole.get(key) || {};
|
|
293
|
+
scale.push({
|
|
294
|
+
key,
|
|
295
|
+
size: dim(t.size) || dim(sys.size),
|
|
296
|
+
weight: num(t.weight) || num(sys.weight),
|
|
297
|
+
leading: num(sys.leading),
|
|
298
|
+
tracking: num(sys.tracking),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
for (const [key, sys] of bySystemRole) {
|
|
302
|
+
if (seen.has(key)) continue;
|
|
303
|
+
seen.add(key);
|
|
304
|
+
const row = { key, size: dim(sys.size), weight: num(sys.weight), leading: num(sys.leading), tracking: num(sys.tracking) };
|
|
305
|
+
if (row.size || row.weight || row.leading || row.tracking) scale.push(row);
|
|
306
|
+
}
|
|
307
|
+
return { faces, scale };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Resolve a `{group.key}` ref against a token map, preferring the named keys
|
|
312
|
+
* and falling back to the map's first key. `null` when the group is empty — the
|
|
313
|
+
* caller then omits the field rather than emitting a ref that resolves to
|
|
314
|
+
* nothing, which is the one thing worse than an absent component field.
|
|
315
|
+
* @param {string} group
|
|
316
|
+
* @param {Record<string,string>} map
|
|
317
|
+
* @param {string[]} prefer
|
|
318
|
+
* @returns {string|null}
|
|
319
|
+
*/
|
|
320
|
+
export function tokenRef(group, map, prefer) {
|
|
321
|
+
const keys = Object.keys(map || {});
|
|
322
|
+
if (keys.length === 0) return null;
|
|
323
|
+
const hit = (prefer || []).find((p) => keys.includes(p)) || keys[0];
|
|
324
|
+
return `{${group}.${hit}}`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The `components` block: the four primitives every design skill reaches for,
|
|
329
|
+
* written entirely as refs so a token change propagates without touching them.
|
|
330
|
+
* A field whose ref cannot resolve is omitted (see {@link tokenRef}).
|
|
331
|
+
* @param {{colors:Record<string,string>, rounded:Record<string,string>, spacing:Record<string,string>, shadow:Record<string,string>, border:Record<string,string>, faces:Record<string,string>}} t
|
|
332
|
+
* @returns {Array<[string, Array<[string,string]>]>}
|
|
333
|
+
*/
|
|
334
|
+
export function componentRefs(t) {
|
|
335
|
+
const c = (prefer) => tokenRef("colors", t.colors, prefer);
|
|
336
|
+
const r = (prefer) => tokenRef("rounded", t.rounded, prefer);
|
|
337
|
+
const s = (prefer) => tokenRef("spacing", t.spacing, prefer);
|
|
338
|
+
const sh = (prefer) => tokenRef("shadow", t.shadow, prefer);
|
|
339
|
+
const b = (prefer) => tokenRef("border", t.border, prefer);
|
|
340
|
+
const f = (prefer) => tokenRef("typography", t.faces, prefer);
|
|
341
|
+
const pad = (a, bb) => {
|
|
342
|
+
const one = s(a);
|
|
343
|
+
const two = s(bb);
|
|
344
|
+
return one && two ? `${one} ${two}` : one || two;
|
|
345
|
+
};
|
|
346
|
+
const spec = [
|
|
347
|
+
["button", [
|
|
348
|
+
["background", c(["primary"])],
|
|
349
|
+
["foreground", c(["paper", "background"])],
|
|
350
|
+
["radius", r(["pill", "chip"])],
|
|
351
|
+
["padding", pad(["sm", "xs"], ["lg", "md"])],
|
|
352
|
+
["font", f(["body"])],
|
|
353
|
+
]],
|
|
354
|
+
["card", [
|
|
355
|
+
["background", c(["paper", "background"])],
|
|
356
|
+
["foreground", c(["ink", "foreground"])],
|
|
357
|
+
["border", b(["hairline"])],
|
|
358
|
+
["radius", r(["card"])],
|
|
359
|
+
["padding", s(["xl", "lg"])],
|
|
360
|
+
["shadow", sh(["md", "sm"])],
|
|
361
|
+
]],
|
|
362
|
+
["input", [
|
|
363
|
+
["background", c(["paper", "background"])],
|
|
364
|
+
["border", b(["hairline"])],
|
|
365
|
+
["radius", r(["field", "chip"])],
|
|
366
|
+
["padding", pad(["sm", "xs"], ["md", "sm"])],
|
|
367
|
+
["font", f(["body"])],
|
|
368
|
+
]],
|
|
369
|
+
["surface", [
|
|
370
|
+
["background", c(["paper", "background"])],
|
|
371
|
+
["foreground", c(["ink", "foreground"])],
|
|
372
|
+
["muted", c(["muted"])],
|
|
373
|
+
["radius", r(["modal", "card"])],
|
|
374
|
+
]],
|
|
375
|
+
];
|
|
376
|
+
return spec
|
|
377
|
+
.map(([name, fields]) => [name, fields.filter(([, v]) => !!v)])
|
|
378
|
+
.filter(([, fields]) => fields.length > 0);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Foundation keys this renderer does not emit, in stable key order — the thing
|
|
383
|
+
* the "Additional tokens" section exists to make visible.
|
|
384
|
+
*
|
|
385
|
+
* Nested one level into `system` and `visualLanguage` as well, reported as
|
|
386
|
+
* dotted paths (`"system.imagery"`). Those two are the blocks hq actually
|
|
387
|
+
* grows, and a top-level-only sweep calls the whole of `system` "modelled"
|
|
388
|
+
* while a new sub-block inside it vanishes. One level is deliberate: it covers
|
|
389
|
+
* every sub-block hq defines today without turning the guarantee into a
|
|
390
|
+
* recursive diff of two schemas.
|
|
391
|
+
*
|
|
392
|
+
* @param {object} foundation
|
|
393
|
+
* @returns {Array<[string, unknown]>}
|
|
394
|
+
*/
|
|
395
|
+
export function additionalTokens(foundation) {
|
|
396
|
+
if (!isObj(foundation)) return [];
|
|
397
|
+
const out = Object.keys(foundation)
|
|
398
|
+
.filter((k) => !KNOWN_FOUNDATION_KEYS.includes(k))
|
|
399
|
+
.sort()
|
|
400
|
+
.map((k) => [k, foundation[k]]);
|
|
401
|
+
for (const [block, known] of [["system", KNOWN_SYSTEM_KEYS], ["visualLanguage", KNOWN_VISUAL_LANGUAGE_KEYS]]) {
|
|
402
|
+
const v = foundation[block];
|
|
403
|
+
if (!isObj(v)) continue;
|
|
404
|
+
for (const k of Object.keys(v).filter((key) => !known.includes(key)).sort()) {
|
|
405
|
+
out.push([`${block}.${k}`, v[k]]);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ── prose helpers ───────────────────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
/** A markdown bullet list, or [] when there is nothing real to list. */
|
|
414
|
+
function bullets(list, map = (x) => str(x)) {
|
|
415
|
+
return arr(list).map(map).filter(Boolean).map((line) => `- ${line}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** `key: value` lines for a plain object's string-ish fields. */
|
|
419
|
+
function fieldLines(obj, fields) {
|
|
420
|
+
const out = [];
|
|
421
|
+
for (const [key, label] of fields) {
|
|
422
|
+
const v = obj ? obj[key] : undefined;
|
|
423
|
+
if (Array.isArray(v)) {
|
|
424
|
+
const items = v.map((x) => str(x)).filter(Boolean);
|
|
425
|
+
if (items.length) out.push(`- **${label}:** ${items.join(", ")}`);
|
|
426
|
+
} else if (typeof v === "number" && Number.isFinite(v)) {
|
|
427
|
+
out.push(`- **${label}:** ${v}`);
|
|
428
|
+
} else if (typeof v === "boolean") {
|
|
429
|
+
out.push(`- **${label}:** ${v ? "yes" : "no"}`);
|
|
430
|
+
} else if (str(v)) {
|
|
431
|
+
out.push(`- **${label}:** ${str(v)}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Drop trailing blank lines and end with exactly one newline. */
|
|
438
|
+
function finish(lines) {
|
|
439
|
+
const out = [...lines];
|
|
440
|
+
while (out.length && out[out.length - 1] === "") out.pop();
|
|
441
|
+
return `${out.join("\n")}\n`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** The provenance footer both files carry. Everything here is injected. */
|
|
445
|
+
function provenance(opts) {
|
|
446
|
+
const o = isObj(opts) ? opts : {};
|
|
447
|
+
const lines = [];
|
|
448
|
+
if (str(o.versionLabel)) lines.push(`- **Version:** ${str(o.versionLabel)}`);
|
|
449
|
+
if (str(o.versionId)) lines.push(`- **Version id:** ${str(o.versionId)}`);
|
|
450
|
+
if (str(o.generatedAt)) lines.push(`- **Synced:** ${str(o.generatedAt)}`);
|
|
451
|
+
lines.push(`- **Source:** ${str(o.source) || "cohort · branding.getFoundation"}`);
|
|
452
|
+
return lines;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ── the two renderers ───────────────────────────────────────────────────────
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Render DESIGN.md — the token contract.
|
|
459
|
+
*
|
|
460
|
+
* @param {object} foundation the saved brand foundation (`branding.getFoundation` → `result.foundation`)
|
|
461
|
+
* @param {object} [opts]
|
|
462
|
+
* @param {string} [opts.name] workspace/brand name for the heading
|
|
463
|
+
* @param {string} [opts.generatedAt] ISO string; INJECTED (this module never reads a clock)
|
|
464
|
+
* @param {string} [opts.versionId] `headVersionId` from the read
|
|
465
|
+
* @param {string} [opts.versionLabel] human label of that version
|
|
466
|
+
* @param {string} [opts.source] provenance line
|
|
467
|
+
* @returns {string} the file's bytes
|
|
468
|
+
*/
|
|
469
|
+
export function renderDesignMd(foundation, opts = {}) {
|
|
470
|
+
const f = isObj(foundation) ? foundation : {};
|
|
471
|
+
const o = isObj(opts) ? opts : {};
|
|
472
|
+
const name = str(o.name) || "Workspace";
|
|
473
|
+
const { light, dark } = colorTokens(f);
|
|
474
|
+
const rounded = scaleTokens(f.radii);
|
|
475
|
+
const spacing = scaleTokens(f.space);
|
|
476
|
+
const shadow = scaleTokens(f.shadow);
|
|
477
|
+
const border = scaleTokens(f.border);
|
|
478
|
+
const { faces, scale } = typographyTokens(f);
|
|
479
|
+
|
|
480
|
+
const fm = ["---", `name: ${q(name)}`];
|
|
481
|
+
if (str(o.versionId)) fm.push(`version: ${q(o.versionId)}`);
|
|
482
|
+
if (str(o.generatedAt)) fm.push(`generatedAt: ${q(o.generatedAt)}`);
|
|
483
|
+
fm.push(`source: ${q(str(o.source) || "cohort · branding.getFoundation")}`);
|
|
484
|
+
|
|
485
|
+
fm.push("colors:");
|
|
486
|
+
const lightPairs = Object.entries(light);
|
|
487
|
+
if (lightPairs.length) fm.push(...yamlMap(lightPairs, " "));
|
|
488
|
+
else fm.push(" {}");
|
|
489
|
+
if (Object.keys(dark).length) {
|
|
490
|
+
fm.push("colorsDark:");
|
|
491
|
+
fm.push(...yamlMap(Object.entries(dark), " "));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
fm.push("typography:");
|
|
495
|
+
if (Object.keys(faces).length) fm.push(...yamlMap(Object.entries(faces), " "));
|
|
496
|
+
if (scale.length) {
|
|
497
|
+
fm.push(" scale:");
|
|
498
|
+
for (const row of scale) {
|
|
499
|
+
const parts = [];
|
|
500
|
+
if (row.size) parts.push(`size: ${q(row.size)}`);
|
|
501
|
+
if (row.weight) parts.push(`weight: ${row.weight}`);
|
|
502
|
+
if (row.leading) parts.push(`leading: ${row.leading}`);
|
|
503
|
+
if (row.tracking) parts.push(`tracking: ${row.tracking}`);
|
|
504
|
+
fm.push(` ${row.key}: { ${parts.join(", ")} }`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (!Object.keys(faces).length && !scale.length) fm.push(" {}");
|
|
508
|
+
|
|
509
|
+
for (const [label, map] of [["rounded", rounded], ["spacing", spacing], ["shadow", shadow], ["border", border]]) {
|
|
510
|
+
fm.push(`${label}:`);
|
|
511
|
+
const pairs = Object.entries(map);
|
|
512
|
+
if (pairs.length) fm.push(...yamlMap(pairs, " "));
|
|
513
|
+
else fm.push(" {}");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const components = componentRefs({ colors: light, rounded, spacing, shadow, border, faces });
|
|
517
|
+
fm.push("components:");
|
|
518
|
+
if (components.length) {
|
|
519
|
+
for (const [comp, fields] of components) {
|
|
520
|
+
fm.push(` ${comp}:`);
|
|
521
|
+
fm.push(...yamlMap(fields, " "));
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
fm.push(" {}");
|
|
525
|
+
}
|
|
526
|
+
fm.push("---");
|
|
527
|
+
|
|
528
|
+
const body = [
|
|
529
|
+
"",
|
|
530
|
+
`# ${name} — design system`,
|
|
531
|
+
"",
|
|
532
|
+
"The frontmatter above is the token contract: it is generated from the workspace's",
|
|
533
|
+
"saved brand foundation and is the source of truth for colour, type, radius and",
|
|
534
|
+
"spacing. Read it before you pick a value. Component entries are written as",
|
|
535
|
+
"`{group.key}` refs, so changing a token changes every component that names it.",
|
|
536
|
+
"",
|
|
537
|
+
"This file is GENERATED — edits here are overwritten on the next sync. To change",
|
|
538
|
+
"the foundation itself, propose the change (`design_propose_change`); a human",
|
|
539
|
+
"reviews and applies it, and the next sync brings it back down.",
|
|
540
|
+
"",
|
|
541
|
+
];
|
|
542
|
+
|
|
543
|
+
body.push("## Palette", "");
|
|
544
|
+
if (lightPairs.length) {
|
|
545
|
+
for (const [k, v] of lightPairs) {
|
|
546
|
+
body.push(`- \`{colors.${k}}\` — ${v}${dark[k] ? ` (dark ${dark[k]})` : ""}`);
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
body.push("_No palette in the foundation._");
|
|
550
|
+
}
|
|
551
|
+
body.push("");
|
|
552
|
+
|
|
553
|
+
body.push("## Typography", "");
|
|
554
|
+
if (Object.keys(faces).length) {
|
|
555
|
+
for (const [role, family] of Object.entries(faces)) body.push(`- **${role}:** ${family}`);
|
|
556
|
+
}
|
|
557
|
+
if (scale.length) {
|
|
558
|
+
body.push("");
|
|
559
|
+
for (const row of scale) {
|
|
560
|
+
const bits = [
|
|
561
|
+
row.size,
|
|
562
|
+
row.weight ? `weight ${row.weight}` : "",
|
|
563
|
+
row.leading ? `leading ${row.leading}` : "",
|
|
564
|
+
row.tracking ? `tracking ${row.tracking}` : "",
|
|
565
|
+
].filter(Boolean).join(" · ");
|
|
566
|
+
body.push(`- \`{typography.scale.${row.key}}\` — ${bits}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (!Object.keys(faces).length && !scale.length) body.push("_No typography in the foundation._");
|
|
570
|
+
body.push("");
|
|
571
|
+
|
|
572
|
+
const usage = isObj(f.system) && isObj(f.system.typography) ? f.system.typography : null;
|
|
573
|
+
const typeRules = bullets(usage && usage.usageRules);
|
|
574
|
+
if (str(usage && usage.pairingRationale) || typeRules.length) {
|
|
575
|
+
body.push("### Type rules", "");
|
|
576
|
+
if (str(usage.pairingRationale)) body.push(str(usage.pairingRationale), "");
|
|
577
|
+
if (typeRules.length) body.push(...typeRules, "");
|
|
578
|
+
}
|
|
579
|
+
const colorRules = bullets(isObj(f.system) && isObj(f.system.color) ? f.system.color.usageRules : null);
|
|
580
|
+
if (colorRules.length) body.push("### Colour rules", "", ...colorRules, "");
|
|
581
|
+
|
|
582
|
+
// VISUAL LANGUAGE — assembled first, emitted only if it has content, so a
|
|
583
|
+
// present-but-empty `visualLanguage` does not leave a bare heading behind.
|
|
584
|
+
//
|
|
585
|
+
// TWO SOURCES, ONE SECTION. `visualLanguage.imagery` is the art-direction
|
|
586
|
+
// block the Design surface writes; `system.imagery` is the brand system's own
|
|
587
|
+
// (medium / style / treatment / mood / subjects / donts). They are different
|
|
588
|
+
// hq schemas and a workspace can carry either or both — rendering only the
|
|
589
|
+
// first is how `system.imagery` came to be dropped in silence.
|
|
590
|
+
const vlLines = [];
|
|
591
|
+
const vl = isObj(f.visualLanguage) ? f.visualLanguage : {};
|
|
592
|
+
const vlImagery = isObj(vl.imagery) ? vl.imagery : {};
|
|
593
|
+
const img = fieldLines(vlImagery, [
|
|
594
|
+
["needsImagery", "Uses imagery"],
|
|
595
|
+
["medium", "Medium"],
|
|
596
|
+
["secondaryMedia", "Secondary media"],
|
|
597
|
+
["colorGrade", "Grade"],
|
|
598
|
+
["grain", "Grain"],
|
|
599
|
+
["crop", "Crop"],
|
|
600
|
+
["aspectRatios", "Aspect ratios"],
|
|
601
|
+
["subjects", "Subjects"],
|
|
602
|
+
["mood", "Mood"],
|
|
603
|
+
["composition", "Composition"],
|
|
604
|
+
]);
|
|
605
|
+
const duo = isObj(vlImagery.duotone) ? vlImagery.duotone : null;
|
|
606
|
+
if (duo && (str(duo.shadow) || str(duo.highlight))) {
|
|
607
|
+
img.push(`- **Duotone:** ${[str(duo.shadow) && `shadow ${str(duo.shadow)}`, str(duo.highlight) && `highlight ${str(duo.highlight)}`].filter(Boolean).join(" · ")}`);
|
|
608
|
+
}
|
|
609
|
+
if (img.length) vlLines.push("### Imagery", "", ...img, "");
|
|
610
|
+
const sysImagery = fieldLines(isObj(f.system) && isObj(f.system.imagery) ? f.system.imagery : {}, [
|
|
611
|
+
["medium", "Medium"],
|
|
612
|
+
["style", "Style"],
|
|
613
|
+
["treatment", "Treatment"],
|
|
614
|
+
["mood", "Mood"],
|
|
615
|
+
["subjects", "Subjects"],
|
|
616
|
+
["donts", "Avoid"],
|
|
617
|
+
]);
|
|
618
|
+
if (sysImagery.length) vlLines.push("### Imagery (brand system)", "", ...sysImagery, "");
|
|
619
|
+
const icon = fieldLines(isObj(vl.iconography) ? vl.iconography : {}, [
|
|
620
|
+
["style", "Style"],
|
|
621
|
+
["weight", "Weight"],
|
|
622
|
+
["corner", "Corner"],
|
|
623
|
+
["grid", "Grid"],
|
|
624
|
+
["notes", "Notes"],
|
|
625
|
+
]);
|
|
626
|
+
if (icon.length) vlLines.push("### Iconography", "", ...icon, "");
|
|
627
|
+
const motion = fieldLines(isObj(vl.motion) ? vl.motion : {}, [
|
|
628
|
+
["character", "Character"],
|
|
629
|
+
["easing", "Easing"],
|
|
630
|
+
["durationMs", "Duration (ms)"],
|
|
631
|
+
["principles", "Principles"],
|
|
632
|
+
]);
|
|
633
|
+
if (motion.length) vlLines.push("### Motion", "", ...motion, "");
|
|
634
|
+
const texture = fieldLines(isObj(vl.texture) ? vl.texture : {}, [
|
|
635
|
+
["surfaces", "Surfaces"],
|
|
636
|
+
["background", "Background"],
|
|
637
|
+
["notes", "Notes"],
|
|
638
|
+
]);
|
|
639
|
+
if (texture.length) vlLines.push("### Texture", "", ...texture, "");
|
|
640
|
+
const guards = isObj(vl.guardrails) ? vl.guardrails : {};
|
|
641
|
+
const dos = bullets(guards.dos);
|
|
642
|
+
const donts = bullets(guards.donts);
|
|
643
|
+
if (dos.length || donts.length) {
|
|
644
|
+
vlLines.push("### Guardrails", "");
|
|
645
|
+
if (dos.length) vlLines.push("Do:", "", ...dos, "");
|
|
646
|
+
if (donts.length) vlLines.push("Do not:", "", ...donts, "");
|
|
647
|
+
}
|
|
648
|
+
// The image-generation seeds and the block's own provenance. Modelled here
|
|
649
|
+
// rather than left to "Additional tokens" because they are what an image
|
|
650
|
+
// prompt is actually built from.
|
|
651
|
+
const vlMeta = fieldLines(vl, [
|
|
652
|
+
["promptSeed", "Prompt seed"],
|
|
653
|
+
["seedLocked", "Seed locked"],
|
|
654
|
+
["negativeSeed", "Negative seed"],
|
|
655
|
+
["references", "References"],
|
|
656
|
+
["source", "Source"],
|
|
657
|
+
["v", "Schema version"],
|
|
658
|
+
]);
|
|
659
|
+
if (vlMeta.length) vlLines.push("### Generation seeds", "", ...vlMeta, "");
|
|
660
|
+
if (vlLines.length) body.push("## Visual language", "", ...vlLines);
|
|
661
|
+
|
|
662
|
+
const layout = bullets(isObj(f.system) ? f.system.layoutPrinciples : null);
|
|
663
|
+
if (layout.length) body.push("## Layout principles", "", ...layout, "");
|
|
664
|
+
|
|
665
|
+
const logos = isObj(f.logos) ? f.logos : null;
|
|
666
|
+
if (logos) {
|
|
667
|
+
const slots = Object.entries(logos)
|
|
668
|
+
.filter(([, v]) => str(v))
|
|
669
|
+
.map(([k]) => `- \`${k}\` — set`);
|
|
670
|
+
if (slots.length) body.push("## Marks", "", ...slots, "", "Fetch the bytes with `design_export_kit`; never inline them here.", "");
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const extra = additionalTokens(f);
|
|
674
|
+
body.push("## Additional tokens", "");
|
|
675
|
+
if (extra.length) {
|
|
676
|
+
body.push(
|
|
677
|
+
"Keys the renderer does not model yet, carried through verbatim so nothing is",
|
|
678
|
+
"lost between an hq that added a token and an SDK that has not learned it.",
|
|
679
|
+
"",
|
|
680
|
+
"```json",
|
|
681
|
+
JSON.stringify(Object.fromEntries(extra), null, 2),
|
|
682
|
+
"```",
|
|
683
|
+
"",
|
|
684
|
+
);
|
|
685
|
+
} else {
|
|
686
|
+
body.push("_None — every key in the foundation is modelled above._", "");
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
body.push("## Provenance", "", ...provenance(o));
|
|
690
|
+
return finish([...fm, ...body]);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Render PRODUCT.md — the voice and positioning half of the same foundation.
|
|
695
|
+
* Same purity contract as {@link renderDesignMd}.
|
|
696
|
+
*
|
|
697
|
+
* @param {object} foundation
|
|
698
|
+
* @param {object} [opts] see {@link renderDesignMd}
|
|
699
|
+
* @returns {string}
|
|
700
|
+
*/
|
|
701
|
+
export function renderProductMd(foundation, opts = {}) {
|
|
702
|
+
const f = isObj(foundation) ? foundation : {};
|
|
703
|
+
const o = isObj(opts) ? opts : {};
|
|
704
|
+
const name = str(o.name) || "Workspace";
|
|
705
|
+
const sys = isObj(f.system) ? f.system : {};
|
|
706
|
+
const sysVoice = isObj(sys.voice) ? sys.voice : {};
|
|
707
|
+
|
|
708
|
+
const lines = [
|
|
709
|
+
"---",
|
|
710
|
+
`name: ${q(name)}`,
|
|
711
|
+
];
|
|
712
|
+
if (str(o.versionId)) lines.push(`version: ${q(o.versionId)}`);
|
|
713
|
+
if (str(o.generatedAt)) lines.push(`generatedAt: ${q(o.generatedAt)}`);
|
|
714
|
+
lines.push(`source: ${q(str(o.source) || "cohort · branding.getFoundation")}`, "---", "");
|
|
715
|
+
|
|
716
|
+
lines.push(
|
|
717
|
+
`# ${name} — product voice`,
|
|
718
|
+
"",
|
|
719
|
+
"Generated from the workspace's saved brand foundation. It says how this",
|
|
720
|
+
"product sounds; `DESIGN.md` beside it says how it looks. Read both before",
|
|
721
|
+
"writing anything a customer sees. Edits here are overwritten on the next",
|
|
722
|
+
"sync — propose foundation changes with `design_propose_change`.",
|
|
723
|
+
"",
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
const positioning = str(f.positioning);
|
|
727
|
+
if (positioning) lines.push("## Positioning", "", positioning, "");
|
|
728
|
+
|
|
729
|
+
const taglines = arr(f.taglines).map((t) => str(t)).filter(Boolean);
|
|
730
|
+
if (taglines.length) lines.push("## Taglines", "", ...taglines.map((t) => `- ${t}`), "");
|
|
731
|
+
|
|
732
|
+
const voice = str(f.voice) || str(sysVoice.voice);
|
|
733
|
+
if (voice) lines.push("## Voice", "", voice, "");
|
|
734
|
+
|
|
735
|
+
const traits = [...arr(f.toneTraits), ...arr(sysVoice.toneTraits)]
|
|
736
|
+
.map((t) => str(t))
|
|
737
|
+
.filter(Boolean)
|
|
738
|
+
.filter((t, i, all) => all.indexOf(t) === i);
|
|
739
|
+
if (traits.length) lines.push("## Tone", "", ...traits.map((t) => `- ${t}`), "");
|
|
740
|
+
|
|
741
|
+
const lex = isObj(sysVoice.lexicon) ? sysVoice.lexicon : {};
|
|
742
|
+
const use = bullets(lex.use);
|
|
743
|
+
const avoid = bullets(lex.avoid);
|
|
744
|
+
if (use.length || avoid.length) {
|
|
745
|
+
lines.push("## Lexicon", "");
|
|
746
|
+
if (use.length) lines.push("Say:", "", ...use, "");
|
|
747
|
+
if (avoid.length) lines.push("Avoid:", "", ...avoid, "");
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const example = str(sysVoice.exampleCopy);
|
|
751
|
+
if (example) lines.push("## In practice", "", example, "");
|
|
752
|
+
|
|
753
|
+
const motifs = bullets(sys.motifs);
|
|
754
|
+
if (motifs.length) lines.push("## Motifs", "", ...motifs, "");
|
|
755
|
+
if (str(sys.iconography)) lines.push("## Iconography", "", str(sys.iconography), "");
|
|
756
|
+
|
|
757
|
+
if (!positioning && !voice && !traits.length && !use.length && !avoid.length) {
|
|
758
|
+
lines.push("## Nothing set yet", "", "The foundation carries no voice or positioning. Ask a human to fill it in from", "the Design surface, or propose it with `design_propose_change`.", "");
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
lines.push("## Provenance", "", ...provenance(o));
|
|
762
|
+
return finish(lines);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Both files, rendered together — the shape the CLI and the daemon both write.
|
|
767
|
+
* Relative paths, so the caller decides the directory.
|
|
768
|
+
* @param {object} foundation
|
|
769
|
+
* @param {object} [opts] see {@link renderDesignMd}
|
|
770
|
+
* @returns {Array<{rel:string, content:string}>}
|
|
771
|
+
*/
|
|
772
|
+
export function renderDesignBundle(foundation, opts = {}) {
|
|
773
|
+
return [
|
|
774
|
+
{ rel: "DESIGN.md", content: renderDesignMd(foundation, opts) },
|
|
775
|
+
{ rel: "PRODUCT.md", content: renderProductMd(foundation, opts) },
|
|
776
|
+
];
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
export default {
|
|
780
|
+
renderDesignMd,
|
|
781
|
+
renderProductMd,
|
|
782
|
+
renderDesignBundle,
|
|
783
|
+
colorTokens,
|
|
784
|
+
scaleTokens,
|
|
785
|
+
typographyTokens,
|
|
786
|
+
componentRefs,
|
|
787
|
+
additionalTokens,
|
|
788
|
+
tokenRef,
|
|
789
|
+
slugToken,
|
|
790
|
+
KNOWN_FOUNDATION_KEYS,
|
|
791
|
+
KNOWN_SYSTEM_KEYS,
|
|
792
|
+
KNOWN_VISUAL_LANGUAGE_KEYS,
|
|
793
|
+
};
|