@clay-design-system/kb 0.0.0-stage → 0.14.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 (45) hide show
  1. package/dist/contract.d.ts +227 -0
  2. package/dist/contract.d.ts.map +1 -0
  3. package/dist/contract.js +134 -0
  4. package/dist/contract.js.map +1 -0
  5. package/dist/core/memory.d.ts +20 -0
  6. package/dist/core/memory.d.ts.map +1 -0
  7. package/dist/core/memory.js +23 -0
  8. package/dist/core/memory.js.map +1 -0
  9. package/dist/core/model.d.ts +47 -0
  10. package/dist/core/model.d.ts.map +1 -0
  11. package/dist/core/model.js +21 -0
  12. package/dist/core/model.js.map +1 -0
  13. package/dist/core/prose.d.ts +40 -0
  14. package/dist/core/prose.d.ts.map +1 -0
  15. package/dist/core/prose.js +106 -0
  16. package/dist/core/prose.js.map +1 -0
  17. package/dist/core/query.d.ts +87 -0
  18. package/dist/core/query.d.ts.map +1 -0
  19. package/dist/core/query.js +637 -0
  20. package/dist/core/query.js.map +1 -0
  21. package/dist/core/rank.d.ts +123 -0
  22. package/dist/core/rank.d.ts.map +1 -0
  23. package/dist/core/rank.js +429 -0
  24. package/dist/core/rank.js.map +1 -0
  25. package/dist/core/route-email.d.ts +48 -0
  26. package/dist/core/route-email.d.ts.map +1 -0
  27. package/dist/core/route-email.js +172 -0
  28. package/dist/core/route-email.js.map +1 -0
  29. package/dist/core/route.d.ts +431 -0
  30. package/dist/core/route.d.ts.map +1 -0
  31. package/dist/core/route.js +1769 -0
  32. package/dist/core/route.js.map +1 -0
  33. package/dist/core/search.d.ts +64 -0
  34. package/dist/core/search.d.ts.map +1 -0
  35. package/dist/core/search.js +123 -0
  36. package/dist/core/search.js.map +1 -0
  37. package/dist/index.d.ts +3 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +2 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/snapshot-email.json +1 -0
  42. package/dist/snapshot-foundations.json +1 -0
  43. package/dist/snapshot-web.json +1 -0
  44. package/package.json +28 -3
  45. package/README.md +0 -3
@@ -0,0 +1,637 @@
1
+ import { markLine, markServed, noteSection, seenBody, seenLine } from './memory.js';
2
+ import { STAGES, estimateTokens } from './model.js';
3
+ import { layoutLine, limitShort, route as runRoute } from './route.js';
4
+ import { search as runSearch } from './search.js';
5
+ export const tokensOf = estimateTokens;
6
+ export const tight = (body) => body.replace(/\n[ \t]*\n+/g, '\n').trim();
7
+ /* ── get ───────────────────────────────────────────────────────────────────────────────────── */
8
+ /** Full units. An id ending in `*` expands to every id with that prefix (max 25). */
9
+ export function get(ctx, ids, opts = {}) {
10
+ const units = [];
11
+ const missing = [];
12
+ const repeated = [];
13
+ for (const raw of ids) {
14
+ if (raw.endsWith('*')) {
15
+ const pre = raw.slice(0, -1);
16
+ const hits = ctx.kb.units.filter((u) => u.id.startsWith(pre)).slice(0, 25);
17
+ if (!hits.length)
18
+ missing.push(raw);
19
+ units.push(...hits);
20
+ continue;
21
+ }
22
+ const u = ctx.byId.get(raw);
23
+ if (u)
24
+ units.push(u);
25
+ else
26
+ missing.push(raw);
27
+ }
28
+ // K8 session: a unit whose body this build was already served comes back as one line (`--full` re-prints it).
29
+ const fresh = units.filter((u) => {
30
+ if (!opts.full && seenBody(opts.session, u.id)) {
31
+ repeated.push(u.id);
32
+ return false;
33
+ }
34
+ return true;
35
+ });
36
+ for (const u of fresh)
37
+ markServed(opts.session, u.id, 'get');
38
+ const text = [
39
+ ...(repeated.length
40
+ ? [
41
+ `Served earlier in this session (\`--full\` to re-print): ${repeated.map((id) => `\`${id}\` (${opts.session.served[id]})`).join(' · ')}`,
42
+ ]
43
+ : []),
44
+ ...fresh.map((u) => {
45
+ const meta = [
46
+ u.kind,
47
+ u.authority,
48
+ u.severity,
49
+ u.modes?.length ? `mode ${u.modes.join('/')}` : '',
50
+ `stages ${u.stages.join(', ') || '—'}`,
51
+ ]
52
+ .filter(Boolean)
53
+ .join(' · ');
54
+ const rel = Object.entries(u.relations ?? {})
55
+ .filter(([, v]) => v?.length)
56
+ .map(([k, v]) => `${k}: ${v.join(', ')}`)
57
+ .join(' · ');
58
+ // K8: one header line (id · meta · relations); the source path only with --full (agents never open it).
59
+ return `## ${u.id} · _${meta}${rel ? ` · ${rel}` : ''}_${opts.full ? ` · source \`${u.source.path}${u.source.anchor ? `#${u.source.anchor}` : ''}\`` : ''}\n${opts.full ? tight(u.body) : clipUnions(ctx, tight(u.body))}`;
60
+ }),
61
+ ...(missing.length ? [`Not found: ${missing.join(', ')} (try \`clay search\`).`] : []),
62
+ ].join('\n');
63
+ return { text: `${text}\n`, data: { units, missing, repeated } };
64
+ }
65
+ /** Members a string-literal union keeps in `get` output before it is clipped (K11: `component.Icon` printed ~10 KB). */
66
+ const UNION_KEEP = 8;
67
+ /**
68
+ * K11: a long string-literal union in a code span (`'activity' \| 'airplay' \| …`, 597 icon names) is clipped to its
69
+ * first few members plus the count; `get --full` prints it whole. When the values are icon names, the line says how
70
+ * to look one up instead.
71
+ */
72
+ export function clipUnions(ctx, body) {
73
+ const notes = new Set();
74
+ const out = body.replace(/`('[^'`]*'(?:\s*\\?\|\s*'[^'`]*')+)`/g, (whole, union) => {
75
+ const vals = [...union.matchAll(/'([^']*)'/g)].map((m) => m[1]);
76
+ if (vals.length <= UNION_KEEP * 2)
77
+ return whole;
78
+ const sep = union.includes('\\|') ? ' \\| ' : ' | ';
79
+ if (vals.slice(0, 3).every((v) => ctx.byId.has(`asset.icon.${v}`)))
80
+ notes.add(`Icon names: \`clay search "<what it shows> icon" --kind asset\` (or \`clay get asset.icon.<name>\`).`);
81
+ return `\`${vals
82
+ .slice(0, UNION_KEEP)
83
+ .map((v) => `'${v}'`)
84
+ .join(sep)}${sep}…\` (${vals.length} values; \`--full\` prints all)`;
85
+ });
86
+ return notes.size ? `${out}\n${[...notes].join('\n')}` : out;
87
+ }
88
+ /* ── search ────────────────────────────────────────────────────────────────────────────────── */
89
+ /** One search hit; folded near-duplicate assets (K11) are named after it, at most two. */
90
+ function hitLine(h) {
91
+ const al = h.aliases ?? [];
92
+ const also = al.length
93
+ ? ` (also \`${al.slice(0, 2).join('`, `')}\`${al.length > 2 ? ` +${al.length - 2} more` : ''})`
94
+ : '';
95
+ return `\`${h.id}\` (${h.kind}, ${h.authority}, ${h.score.toFixed(1)}) — ${h.summary}${also}`;
96
+ }
97
+ /**
98
+ * `clay search --each a,b,c` (K11): one search per term, the best hit(s) for each — resolves several brand / icon /
99
+ * section names in one call (`--limit n` per term, default 1).
100
+ */
101
+ export function searchEach(ctx, termsList, opts = {}) {
102
+ const results = termsList.map((term) => ({
103
+ term,
104
+ hits: runSearch(ctx.kb, ctx.index, term, { ...opts, k: opts.k ?? 1 }),
105
+ }));
106
+ const text = results
107
+ .map(({ term, hits }) => hits.length === 1
108
+ ? `- ${term} → ${hitLine(hits[0])}`
109
+ : hits.length
110
+ ? [`- ${term}:`, ...hits.map((h, i) => ` ${i + 1}. ${hitLine(h)}`)].join('\n')
111
+ : `- ${term} → no match`)
112
+ .join('\n');
113
+ return { text: `${text}\n`, data: { results } };
114
+ }
115
+ export function search(ctx, query, opts = {}) {
116
+ const hits = runSearch(ctx.kb, ctx.index, query, opts);
117
+ const emailHint = !opts.scope?.length &&
118
+ /\b(?:e-?mails?|mjml|braze|preheader|subject lines?|newsletter|mj-[a-z]+)\b/i.test(query);
119
+ const text = (hits.length ? hits.map((h, i) => `${i + 1}. ${hitLine(h)}`).join('\n') : 'No units match.') +
120
+ (emailHint
121
+ ? '\n(Email knowledge is a separate scope: add `--scope email`, or `--scope all`.)'
122
+ : '');
123
+ return { text: `${text}\n`, data: { hits } };
124
+ }
125
+ /* ── route ─────────────────────────────────────────────────────────────────────────────────── */
126
+ /**
127
+ * `--session` (K8): units this build was already served are named, not repeated; Watch drops lines it saw and units
128
+ * about sections it never looked at. `--since <stage>`: the same dedupe without a session file — the routes of
129
+ * `<stage>` up to the stage before this one (same skill and mode) count as served.
130
+ */
131
+ export function route(ctx, opts) {
132
+ const brief = opts.brief;
133
+ if (!STAGES.includes(opts.stage))
134
+ throw new Error(`route: unknown stage "${opts.stage}" (one of ${STAGES.join(', ')})`);
135
+ const sess = opts.session;
136
+ const seen = {
137
+ bodies: new Set(Object.keys(sess?.served ?? {})),
138
+ lines: new Set(Object.keys(sess?.lines ?? {})),
139
+ };
140
+ if (opts.since) {
141
+ if (!STAGES.includes(opts.since))
142
+ throw new Error(`route: unknown --since stage "${opts.since}"`);
143
+ for (const st of STAGES.slice(STAGES.indexOf(opts.since), STAGES.indexOf(opts.stage))) {
144
+ const prev = runRoute(ctx.kb, ctx.index, {
145
+ skill: opts.skill,
146
+ mode: opts.mode,
147
+ stage: st,
148
+ brief: st === 'mapping' || st === 'structure' ? brief : undefined,
149
+ });
150
+ for (const u of prev.units)
151
+ (u.inlined === 'body' ? seen.bodies : seen.lines).add(u.id);
152
+ }
153
+ }
154
+ const r = runRoute(ctx.kb, ctx.index, {
155
+ ...opts,
156
+ brief,
157
+ briefLabel: opts.briefLabel ?? opts.briefPath,
158
+ seen: sess || opts.since ? seen : undefined,
159
+ sections: sess?.sections,
160
+ layouts: sess?.layouts,
161
+ });
162
+ if (sess) {
163
+ const where = `route ${opts.stage}`;
164
+ for (const u of r.units)
165
+ (u.inlined === 'body' ? markServed : markLine)(sess, u.id, where);
166
+ if (!sess.stages.includes(opts.stage))
167
+ sess.stages.push(opts.stage);
168
+ }
169
+ return { text: r.text, data: r };
170
+ }
171
+ function limitText(ctx, id) {
172
+ if (!id)
173
+ return '';
174
+ const u = ctx.byId.get(id);
175
+ return u
176
+ ? u.summary.replace(/^[^:]+:\s*/, '').replace(/\s*·\s*soft \(copy guidance\)\.?$/, '')
177
+ : '';
178
+ }
179
+ /** Labelled fragment-doc paragraphs `clay section email.<id>` leaves out (provenance / Storybook filing, not build knowledge). */
180
+ const EMAIL_NOTE_SKIP = /^(?:source|source reference|re-file to|qa history|related)$/i;
181
+ const EMAIL_NOTE_BLOCKS = [
182
+ ['Structure', /^structure$/i],
183
+ ['Tokens', /^tokens$/i],
184
+ ['Mobile', /^mobile behaviou?r$/i],
185
+ ['Dark mode', /^dark mode$/i],
186
+ ];
187
+ function slotLines(slots, depth = 0, out = []) {
188
+ for (const n of slots) {
189
+ const attrs = Object.entries(n.attrs)
190
+ .map(([k, v]) => `${k}="${v}"`)
191
+ .join(' ');
192
+ if (n.el === 'repeat')
193
+ out.push(`${' '.repeat(depth)}- ×${n.count} (these ${n.children.length} elements repeat as a group; copy differs per repeat)`);
194
+ else
195
+ out.push(`${' '.repeat(depth)}- ${n.count > 1 ? `×${n.count} ` : ''}\`${n.el}\`${attrs ? ` ${attrs}` : ''}${n.text ? ` — "${n.text}"` : ''}`);
196
+ slotLines(n.children, depth + 1, out);
197
+ }
198
+ return out;
199
+ }
200
+ /**
201
+ * `clay section email.<id>` (K7 r1): the email analogue of the web section contract — flags + include, the brief fields with
202
+ * their limits (the "props"), the fragment's structure / tokens / mobile lines, the `.mjml` slot tree (each element's
203
+ * padding / mj-class / css-class to keep) and the gotchas (divergences + hand-written units about the section), so an agent
204
+ * does not need to open the `.md`, `.mjml` or `compile.ts`.
205
+ */
206
+ function emailSection(ctx, name, sec) {
207
+ const f = sec.facts;
208
+ const used = [sec.id];
209
+ const id = f.emailId;
210
+ const L = [
211
+ `# ${name} · ${f.title}`,
212
+ sec.summary.replace(/^Email (?:section|component) `[^`]+` \([^)]*\): /, ''),
213
+ '',
214
+ `Flags: ${f.flags.join(' · ')}.`,
215
+ `Include: \`${f.include}\` (paste the fragment inline to change copy).`,
216
+ ];
217
+ const about = f.notes.find((n) => n.label === 'Note' || n.label === 'Intro');
218
+ if (about)
219
+ L.splice(2, 0, '', about.text.replace(/\s*\n\s*/g, ' '));
220
+ if (f.fields.length) {
221
+ L.push('', `## Brief fields → slots (limits: \`clay get limit.email.<key>\`)`);
222
+ const blocks = [...new Set(f.fields.map((x) => x.block))];
223
+ for (const b of blocks) {
224
+ const fs = f.fields.filter((x) => x.block === b);
225
+ L.push(`- ${fs[0].repeat ? 'repeating block' : 'block'}: ${fs
226
+ .map((x) => {
227
+ const lim = x.limitId ? ctx.byId.get(x.limitId) : undefined;
228
+ if (lim)
229
+ used.push(lim.id);
230
+ return `${x.label} (${x.kind}${lim ? ` — ${/\*\*Email [^:]+: ([^*]+?)\.?\*\*/.exec(lim.body)?.[1] ?? lim.summary}` : ''})`;
231
+ })
232
+ .join(' · ')}`);
233
+ }
234
+ }
235
+ else
236
+ L.push('', '## Brief fields: none (structural — keep the fragment as is)');
237
+ const notes = f.notes.filter((n) => !EMAIL_NOTE_SKIP.test(n.label));
238
+ const taken = new Set();
239
+ for (const [title, re] of EMAIL_NOTE_BLOCKS) {
240
+ const n = notes.find((x) => re.test(x.label));
241
+ if (!n)
242
+ continue;
243
+ taken.add(n.label);
244
+ L.push('', `## ${title}`, n.text);
245
+ if (title === 'Structure' && f.slots.length) {
246
+ const lines = slotLines(f.slots);
247
+ L.push('', `## Slots (\`${f.mjmlPath}\` — keep each element's attributes; copy and asset placeholders are replaced)`, ...lines.slice(0, 40), ...(lines.length > 40 ? [`- … ${lines.length - 40} more lines (\`${f.mjmlPath}\`)`] : []));
248
+ }
249
+ }
250
+ if (!taken.has('Structure') && f.slots.length) {
251
+ const lines = slotLines(f.slots);
252
+ L.push('', `## Slots (\`${f.mjmlPath}\` — keep each element's attributes; copy and asset placeholders are replaced)`, ...lines.slice(0, 40), ...(lines.length > 40 ? [`- … ${lines.length - 40} more lines`] : []));
253
+ }
254
+ const gotchas = notes.filter((n) => !taken.has(n.label) && n.label !== 'Intro' && n.label !== 'Note');
255
+ const hand = ctx.kb.units
256
+ .filter((u) => u.scope === 'email' &&
257
+ u.authority !== 'derived' &&
258
+ u.authority !== 'background' &&
259
+ u.entities.includes(`section:email.${id}`) &&
260
+ u.entities.filter((e) => e.startsWith('section:email.')).length <= 3)
261
+ .sort((a, b) => (a.kind === 'gotcha' ? 0 : 1) - (b.kind === 'gotcha' ? 0 : 1) || (a.id < b.id ? -1 : 1));
262
+ if (gotchas.length || hand.length) {
263
+ L.push('', '## Gotchas and usage (`clay get <id>`)');
264
+ for (const n of gotchas) {
265
+ // Re-flow wrapped lines; a list under the label becomes nested bullets.
266
+ const items = n.text
267
+ .split('\n')
268
+ .reduce((acc, l) => /^\s*-\s/.test(l) || !acc.length
269
+ ? [...acc, l.trim()]
270
+ : [...acc.slice(0, -1), `${acc[acc.length - 1]} ${l.trim()}`], [])
271
+ .filter(Boolean);
272
+ if (items.length > 1 && items.every((x) => x.startsWith('- ')))
273
+ L.push(`- **${n.label}:**`, ...items.map((x) => ` ${x}`));
274
+ else
275
+ L.push(`- **${n.label}:** ${items.join(' ')}`);
276
+ }
277
+ for (const u of hand) {
278
+ L.push(`- \`${u.id}\`${u.severity ? ` (${u.severity})` : ''} — ${u.summary}`);
279
+ used.push(u.id);
280
+ }
281
+ }
282
+ return { text: `${L.join('\n')}\n`, data: { section: name, units: used } };
283
+ }
284
+ /** `clay section` with no name (K8): every web section with its layouts and use, plus the atoms `clay section` also takes. */
285
+ export function sectionList(ctx, opts = {}) {
286
+ const email = opts.scope === 'email';
287
+ const secs = ctx.kb.units.filter((u) => u.kind === 'section' &&
288
+ u.authority === 'derived' &&
289
+ (email ? u.scope === 'email' : u.scope !== 'email'));
290
+ const L = [
291
+ `# ${email ? 'Email' : 'Web'} sections (${secs.length}) — \`clay section <name> [--layout <l>]\``,
292
+ ];
293
+ for (const u of secs) {
294
+ const name = u.id.replace(/^section\./, '');
295
+ const lays = ctx.kb.units
296
+ .filter((x) => x.kind === 'layout' && x.authority === 'derived' && x.id.startsWith(`layout.${name}.`))
297
+ .map((x) => x.id.split('.').pop());
298
+ const what = / — ([^—]+)$/.exec(u.summary)?.[1]?.replace(/\.$/, '') ?? '';
299
+ L.push(email
300
+ ? `- \`${name}\` — ${u.summary.replace(/^Email section `[^`]+` /, '').slice(0, 90)}`
301
+ : `- \`${name}\`${what ? ` (${what})` : ''}${lays.length ? `: ${lays.join(' · ')}` : ''}`);
302
+ }
303
+ if (!email) {
304
+ const atoms = ctx.kb.units
305
+ .filter((u) => u.kind === 'component' &&
306
+ u.authority === 'derived' &&
307
+ u.scope !== 'email' &&
308
+ !u.id.includes('.', 'component.'.length))
309
+ .map((u) => u.id.slice('component.'.length));
310
+ L.push('', `Atoms (\`clay section <Atom>\` for props): ${atoms.join(', ')}.`, 'Email sections: `clay section --scope email`.');
311
+ }
312
+ return { text: `${L.join('\n')}\n`, data: { sections: secs.map((u) => u.id) } };
313
+ }
314
+ /* K10 compact section view ─────────────────────────────────────────────────────────────────── */
315
+ /** A prop or item-field type in a few characters: string is implied, unions keep their values, objects their keys. */
316
+ function shortType(t) {
317
+ const s = t.trim();
318
+ if (s === 'string')
319
+ return '';
320
+ if (/^\(.*\)\s*=>/.test(s) || /^\(\(.*\)\)$/.test(s))
321
+ return '()';
322
+ if (/^\{/.test(s))
323
+ return `{${[...s.matchAll(/(\w+)\??:/g)]
324
+ .map((m) => m[1])
325
+ .filter((k) => k !== 'onClick')
326
+ .join(',')}}`;
327
+ if (/^'[^']*'(\s*\|\s*'[^']*')+$/.test(s))
328
+ return s.replace(/\s*\|\s*/g, '|');
329
+ if (s === 'ReactNode' || s === 'React.ReactNode')
330
+ return 'node';
331
+ return s.length > 40 ? `${s.slice(0, 37)}…` : s;
332
+ }
333
+ /** `●name type =default limit` — one prop or item field, compact. */
334
+ function fieldShort(ctx, f) {
335
+ const t = shortType(f.type);
336
+ const lim = f.limitId ? limitShort(ctx.byId.get(f.limitId)) : '';
337
+ const def = f.defaultValue && f.defaultValue !== '[]'
338
+ ? `=${f.defaultValue.length > 28 ? `${f.defaultValue.slice(0, 26)}…` : f.defaultValue}`
339
+ : '';
340
+ return [`${f.required ? '●' : ''}${f.name}`, t, def, lim].filter(Boolean).join(' ');
341
+ }
342
+ /** The derived layout body lines a compact view keeps (CTAs, tab label, media, unguarded defaults, item-count code). */
343
+ const LAYOUT_KEEP = /^(?:Use for:|Tab label:|Section CTA:|Item CTAs|Media slots|Always rendered|Item count|Grid|Columns|Note)/;
344
+ /**
345
+ * `clay section <S> --layout <l>[,<l2>…]` inside a session (K10): per layout ~6–10 lines instead of ~25–50 — the range and
346
+ * heading tier on the title line, the rendered props on one line, one line per rendered item type, the CTA / media
347
+ * lines, and at most 4 new gotchas (the rest as ids). Several layouts of one section share the call.
348
+ */
349
+ function compactSection(ctx, name, sec, layouts, sess) {
350
+ const f = (sec.facts ?? {});
351
+ const used = [sec.id];
352
+ const L = [];
353
+ const what = / — ([^—]+)$/.exec(sec.summary)?.[1]?.replace(/\.$/, '') ?? '';
354
+ const about = (u) => u.entities.includes(`section:${name}`) || u.entities.includes(`component:${name}`);
355
+ if (!layouts.length) {
356
+ // Overview: the layouts with their ranges, every prop on one line, the item types.
357
+ const lays = ctx.kb.units.filter((u) => u.kind === 'layout' && u.authority === 'derived' && u.id.startsWith(`layout.${name}.`));
358
+ const vp = variantPropOf(lays[0]);
359
+ L.push(`# ${name}${what ? ` — ${what}` : ''}${lays.length ? ` (${lays.length} ${vp === 'layout' ? 'layouts' : `\`${vp}\` values, passed as \`${vp}="…"\``}; \`clay section ${name} --layout a,b\` for what they render)` : ''}`);
360
+ for (const u of lays) {
361
+ const use = /Use for: ([^\n]*)/.exec(u.body)?.[1]?.replace(/\.$/, '');
362
+ L.push([
363
+ `- \`${u.id.split('.').pop()}\``,
364
+ layoutLine(u).replace(/^\S+ \S+ ?/, ''),
365
+ use ? `— ${use}` : '',
366
+ ]
367
+ .filter(Boolean)
368
+ .join(' '));
369
+ used.push(u.id);
370
+ }
371
+ const props = (f.props ?? []).filter((p) => p.name !== 'layout');
372
+ if (props.length)
373
+ L.push(`Props (● required; c chars · w words): ${props.map((p) => fieldShort(ctx, p)).join(' · ')}`);
374
+ for (const it of [
375
+ ...new Set(props.map((p) => /^([A-Z][A-Za-z0-9]*)\[\]$/.exec(p.type)?.[1]).filter(Boolean)),
376
+ ]) {
377
+ const iu = ctx.byId.get(`item.${it}`);
378
+ if (!iu)
379
+ continue;
380
+ used.push(iu.id);
381
+ L.push(`${it}: ${((iu.facts ?? {}).fields ?? []).map((x) => fieldShort(ctx, x)).join(' · ')}`);
382
+ }
383
+ }
384
+ for (const layout of layouts) {
385
+ const lu = ctx.byId.get(`layout.${name}.${layout}`);
386
+ if (!lu) {
387
+ const lays = ctx.kb.units
388
+ .filter((u) => u.kind === 'layout' && u.id.startsWith(`layout.${name}.`) && u.authority === 'derived')
389
+ .map((u) => u.id.split('.').pop());
390
+ throw new Error(`section: ${name} has no layout "${layout}". Layouts: ${lays.join(', ')}`);
391
+ }
392
+ used.push(lu.id);
393
+ const lf = (lu.facts ?? {});
394
+ const range = layoutLine(lu)
395
+ .replace(/^\S+ \S+ ?/, '')
396
+ .replace(/^\(|\)$/g, '');
397
+ L.push(`# ${name} · ${variantPropOf(lu)}="${layout}"${lf.headingTier ? ` · heading tier ${lf.headingTier}` : ''}${range ? ` · ${range}` : ' · items: no documented range'}`);
398
+ const rendered = new Set(lf.propsRendered ?? []);
399
+ const props = (f.props ?? []).filter((p) => rendered.has(p.name));
400
+ if (props.length)
401
+ L.push(`Props: ${props.map((p) => fieldShort(ctx, p)).join(' · ')}`);
402
+ if (lf.propsIgnored?.length)
403
+ L.push(`Ignored here: ${lf.propsIgnored.join(' · ')}`);
404
+ for (const [it, { rendered: rf }] of Object.entries(lf.itemFields ?? {})) {
405
+ const iu = ctx.byId.get(`item.${it}`);
406
+ const fields = (iu?.facts ?? {}).fields ?? [];
407
+ if (iu)
408
+ used.push(iu.id);
409
+ const shown = fields.filter((x) => rf.includes(x.name));
410
+ const not = fields.filter((x) => !rf.includes(x.name)).map((x) => x.name);
411
+ if (shown.length)
412
+ L.push(`${it}: ${shown.map((x) => fieldShort(ctx, x)).join(' · ')}${not.length ? ` (not rendered by this ${variantPropOf(lu)}: ${not.join(', ')})` : ''}`);
413
+ }
414
+ for (const l of tight(lu.body).split('\n'))
415
+ if (LAYOUT_KEEP.test(l))
416
+ L.push(l);
417
+ }
418
+ // Gotchas / usage about this section (and these layouts), new ones first, at most 4 printed.
419
+ const specific = (u) => u.kind !== 'policy' ||
420
+ u.id.split('.').includes(name) ||
421
+ u.entities.some((e) => e.startsWith(`layout:${name}:`));
422
+ const notes = ctx.kb.units
423
+ .filter((u) => u.authority !== 'background' &&
424
+ u.authority !== 'derived' &&
425
+ about(u) &&
426
+ ['gotcha', 'section', 'policy', 'component', 'layout'].includes(u.kind) &&
427
+ specific(u))
428
+ .filter((u) => !u.id.startsWith('gotcha.page.') &&
429
+ !u.id.startsWith('gotcha.ai-page.') &&
430
+ !u.id.includes('.example.'))
431
+ .filter((u) => !layouts.length ||
432
+ !u.entities.some((e) => e.startsWith(`layout:${name}:`)) ||
433
+ layouts.some((l) => u.entities.includes(`layout:${name}:${l}`)))
434
+ .sort((a, b) => (SEV[b.severity ?? ''] ?? 0) - (SEV[a.severity ?? ''] ?? 0) ||
435
+ (a.kind === 'gotcha' ? 0 : 1) - (b.kind === 'gotcha' ? 0 : 1) ||
436
+ (a.id < b.id ? -1 : 1));
437
+ const fresh = notes.filter((u) => !seenLine(sess, u.id));
438
+ const shown = fresh.slice(0, 4);
439
+ for (const u of shown) {
440
+ L.push(`- \`${u.id}\`${u.severity ? ` (${u.severity})` : ''} — ${u.summary}`);
441
+ used.push(u.id);
442
+ }
443
+ const rest = fresh.slice(4).map((u) => `\`${u.id}\``);
444
+ const old = notes.length - fresh.length;
445
+ if (rest.length || old)
446
+ L.push(`${rest.length ? `More about ${name}: ${rest.join(' · ')}` : ''}${rest.length && old ? ' · ' : ''}${old ? `${old} listed earlier this session` : ''}`);
447
+ const ex = ctx.kb.units.filter((u) => u.id.startsWith(`section.${name}.example.`) &&
448
+ (!layouts.length || layouts.some((l) => u.id.endsWith(`.example.${l}`))));
449
+ if (ex.length)
450
+ L.push(`Example${ex.length > 1 ? 's' : ''}: ${ex.map((u) => `\`clay get ${u.id}\``).join(' · ')}`);
451
+ if (sess) {
452
+ for (const l of layouts.length ? layouts : [undefined])
453
+ noteSection(sess, name, l);
454
+ const where = `section ${name}${layouts.length ? ` --layout ${layouts.join(',')}` : ''}`;
455
+ if (!layouts.length)
456
+ markServed(sess, `${sec.id}#overview`, where);
457
+ for (const id of used)
458
+ (id === sec.id || id.startsWith('layout.') || id.startsWith('item.') ? markServed : markLine)(sess, id, where);
459
+ }
460
+ return {
461
+ text: `${L.join('\n')}\n`,
462
+ data: { section: name, layout: layouts.join(',') || undefined, units: used },
463
+ };
464
+ }
465
+ const SEV = { Critical: 3, Major: 2, Minor: 1 };
466
+ /** The section prop a layout unit is a value of (`layout`, `colorMode`, `variant`, …). */
467
+ function variantPropOf(u) {
468
+ return (u?.facts ?? {}).variantProp ?? 'layout';
469
+ }
470
+ export function section(ctx, name, layout, opts = {}) {
471
+ const layouts = (layout ?? '')
472
+ .split(',')
473
+ .map((x) => x.trim())
474
+ .filter(Boolean);
475
+ if (opts.compact || layouts.length > 1) {
476
+ const sec0 = ctx.byId.get(`section.${name}`) ?? ctx.byId.get(`component.${name}`);
477
+ if (sec0 &&
478
+ !(sec0.scope === 'email' && sec0.facts?.emailId)) {
479
+ if (opts.compact)
480
+ return compactSection(ctx, name, sec0, layouts, opts.session);
481
+ const parts = layouts.map((l) => section(ctx, name, l, { session: opts.session }));
482
+ return {
483
+ text: parts.map((p) => p.text).join(''),
484
+ data: {
485
+ section: name,
486
+ layout: layouts.join(','),
487
+ units: parts.flatMap((p) => p.data.units),
488
+ },
489
+ };
490
+ }
491
+ }
492
+ const sess = opts.session;
493
+ const sec = ctx.byId.get(`section.${name}`) ?? ctx.byId.get(`component.${name}`);
494
+ if (sec && sec.scope === 'email' && sec.facts?.emailId)
495
+ return emailSection(ctx, name, sec);
496
+ if (!sec) {
497
+ const near = ctx.kb.units
498
+ .filter((u) => u.kind === 'section' && u.authority === 'derived' && u.scope !== 'email')
499
+ .map((u) => u.id.slice('section.'.length));
500
+ throw new Error(`section: no section or component "${name}". Sections: ${near.join(', ')} (web; atoms: \`clay section\`; several: \`clay section A,B --layout x,y\`)`);
501
+ }
502
+ const f = (sec.facts ?? {});
503
+ const used = [sec.id];
504
+ const L = [
505
+ `# ${name}${layout ? ` · ${variantPropOf(ctx.byId.get(`layout.${name}.${layout}`))}="${layout}"` : ''}`,
506
+ sec.summary,
507
+ ];
508
+ // K8 session: the section's prop table / an item's fields were already printed → one line instead.
509
+ const propsKey = `${sec.id}#props`;
510
+ const propsSeen = seenBody(sess, propsKey);
511
+ if (!layout && seenBody(sess, `${sec.id}#overview`)) {
512
+ noteSection(sess, name);
513
+ return {
514
+ text: `# ${name}\nServed earlier in this session (\`clay section ${name}\`); \`clay section ${name} --layout <l>\` for one layout.\n`,
515
+ data: { section: name, units: [] },
516
+ };
517
+ }
518
+ const about = (u) => u.entities.includes(`section:${name}`) || u.entities.includes(`component:${name}`);
519
+ if (layout) {
520
+ const lu = ctx.byId.get(`layout.${name}.${layout}`);
521
+ if (!lu) {
522
+ const lays = ctx.kb.units
523
+ .filter((u) => u.kind === 'layout' && u.id.startsWith(`layout.${name}.`) && u.authority === 'derived')
524
+ .map((u) => u.id.split('.').pop());
525
+ throw new Error(`section: ${name} has no layout "${layout}". Layouts: ${lays.join(', ')}`);
526
+ }
527
+ used.push(lu.id);
528
+ L.push('', `## ${lu.id}`, tight(lu.body));
529
+ }
530
+ else {
531
+ const lays = ctx.kb.units.filter((u) => u.kind === 'layout' && u.authority === 'derived' && u.id.startsWith(`layout.${name}.`));
532
+ if (lays.length) {
533
+ const vp = variantPropOf(lays[0]);
534
+ L.push('', `## Layouts${vp === 'layout' ? '' : ` — values of the \`${vp}\` prop`} (\`clay section ${name} --layout <l>\` for what one renders)`);
535
+ for (const u of lays) {
536
+ L.push(`- \`${u.id.split('.').pop()}\` ${layoutLine(u).replace(/^\S+ \S+ ?/, '')}`);
537
+ used.push(u.id);
538
+ }
539
+ }
540
+ }
541
+ // Props (compact) — on a layout view, only the props that layout renders.
542
+ const rendered = layout
543
+ ? new Set((ctx.byId.get(`layout.${name}.${layout}`)?.facts ?? {})
544
+ .propsRendered ?? [])
545
+ : undefined;
546
+ const props = (f.props ?? []).filter((p) => !rendered || rendered.has(p.name));
547
+ if (props.length && propsSeen) {
548
+ L.push('', `Props${rendered ? ' this layout renders' : ''}: ${props.map((p) => `${p.required ? '●' : ''}\`${p.name}\``).join(' · ')} (types and limits: printed earlier by \`clay section ${name}\`).`);
549
+ }
550
+ else if (props.length) {
551
+ L.push('', `## Props${rendered ? ' this layout renders' : ''} (● required)`);
552
+ for (const p of props) {
553
+ const lim = limitText(ctx, p.limitId);
554
+ L.push(`- ${p.required ? '●' : '○'} \`${p.name}\`: \`${p.type.length > 70 ? `${p.type.slice(0, 67)}…` : p.type}\`${p.defaultValue ? ` = ${p.defaultValue.length > 40 ? `${p.defaultValue.slice(0, 37)}…` : p.defaultValue}` : ''}${lim ? ` · ${lim}` : ''}`);
555
+ }
556
+ }
557
+ // Item shapes used by the props shown.
558
+ const itemNames = [
559
+ ...new Set(props.map((p) => /^([A-Z][A-Za-z0-9]*)\[\]$/.exec(p.type)?.[1]).filter(Boolean)),
560
+ ];
561
+ for (const it of itemNames) {
562
+ const iu = ctx.byId.get(`item.${it}`);
563
+ if (!iu)
564
+ continue;
565
+ if (seenBody(sess, iu.id)) {
566
+ L.push('', `${it}: fields printed earlier in this session (\`clay get ${iu.id} --full\`).`);
567
+ continue;
568
+ }
569
+ used.push(iu.id);
570
+ const fields = (iu.facts ?? {}).fields ?? [];
571
+ L.push('', `## ${it}`, ...fields.map((x) => `- ${x.required ? '●' : '○'} \`${x.name}\`: \`${x.type.length > 60 ? `${x.type.slice(0, 57)}…` : x.type}\`${x.limitId ? ` · ${limitText(ctx, x.limitId)}` : ''}`));
572
+ }
573
+ // Gotchas / usage (hand-written) about this section (and this layout).
574
+ // Policies only when they are specific to this section (named in the id, or about one of its layouts).
575
+ const specific = (u) => u.kind !== 'policy' ||
576
+ u.id.split('.').includes(name) ||
577
+ u.entities.some((e) => e.startsWith(`layout:${name}:`));
578
+ const notes = ctx.kb.units
579
+ .filter((u) => u.authority !== 'background' &&
580
+ u.authority !== 'derived' &&
581
+ about(u) &&
582
+ ['gotcha', 'section', 'policy', 'component', 'layout'].includes(u.kind) &&
583
+ specific(u))
584
+ .filter((u) => !u.id.startsWith('gotcha.page.') && !u.id.startsWith('gotcha.ai-page.'))
585
+ .filter((u) => !layout ||
586
+ !u.entities.some((e) => e.startsWith(`layout:${name}:`)) ||
587
+ u.entities.includes(`layout:${name}:${layout}`))
588
+ .sort((a, b) => (a.kind === 'gotcha' ? 0 : 1) - (b.kind === 'gotcha' ? 0 : 1) || (a.id < b.id ? -1 : 1));
589
+ const newNotes = notes.filter((u) => !seenLine(sess, u.id));
590
+ const oldNotes = notes.filter((u) => seenLine(sess, u.id));
591
+ if (newNotes.length) {
592
+ L.push('', '## Gotchas and usage (`clay get <id>`)');
593
+ for (const u of newNotes) {
594
+ L.push(`- \`${u.id}\`${u.severity ? ` (${u.severity})` : ''} — ${u.summary}`);
595
+ used.push(u.id);
596
+ }
597
+ }
598
+ if (oldNotes.length)
599
+ L.push('', `Also about ${name} (listed earlier this session): ${oldNotes.map((u) => `\`${u.id}\``).join(', ')}`);
600
+ if (sess) {
601
+ noteSection(sess, name, layout);
602
+ const where = `section ${name}${layout ? ` --layout ${layout}` : ''}`;
603
+ if (props.length && !propsSeen && !layout)
604
+ markServed(sess, propsKey, where);
605
+ if (!layout)
606
+ markServed(sess, `${sec.id}#overview`, where);
607
+ for (const id of used)
608
+ (id === sec.id || id.startsWith('layout.') || id.startsWith('item.') ? markServed : markLine)(sess, id, where);
609
+ }
610
+ return { text: `${L.join('\n')}\n`, data: { section: name, layout, units: used } };
611
+ }
612
+ /* ── rules ─────────────────────────────────────────────────────────────────────────────────── */
613
+ export function rules(ctx, opts = {}) {
614
+ // K7: `--scope email` lists the email rules (email-rules.json, what `pnpm email:check` enforces); the default is web.
615
+ const email = opts.scope === 'email';
616
+ const fits = (u) => (!opts.stage || u.stages.includes(opts.stage)) &&
617
+ (!opts.mode || !u.modes || u.modes.includes(opts.mode)) &&
618
+ (email ? u.scope === 'email' : u.scope !== 'email');
619
+ const hard = ctx.kb.units.filter((u) => u.kind === 'rule' && fits(u));
620
+ const pol = ctx.kb.units
621
+ .filter((u) => u.kind === 'policy' &&
622
+ u.authority === 'canonical' &&
623
+ (u.severity === 'Critical' || u.severity === 'Major') &&
624
+ u.enforcement?.length &&
625
+ fits(u))
626
+ .sort((a, b) => a.severity === b.severity ? (a.id < b.id ? -1 : 1) : a.severity === 'Critical' ? -1 : 1);
627
+ const line = (u) => `- \`${u.id}\`${u.severity ? ` ${u.severity}` : ''}${u.enforcement?.length ? ` · ${u.enforcement.join('/')}` : ''} — ${u.summary}`;
628
+ const text = [
629
+ `# Rules${opts.stage ? ` · stage ${opts.stage}` : ''}${opts.mode ? ` · mode ${opts.mode}` : ''}`,
630
+ `## ${email ? 'Email rules' : 'Hard Rules'} (${hard.length})`,
631
+ ...hard.map(line),
632
+ `## Policies with enforcement (${pol.length}; Critical first)`,
633
+ ...pol.map(line),
634
+ ].join('\n');
635
+ return { text: `${text}\n`, data: { units: [...hard, ...pol].map((u) => u.id) } };
636
+ }
637
+ //# sourceMappingURL=query.js.map