@clay-design-system/kb 0.0.0-stage → 0.14.1
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/dist/contract.d.ts +227 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +134 -0
- package/dist/contract.js.map +1 -0
- package/dist/core/memory.d.ts +20 -0
- package/dist/core/memory.d.ts.map +1 -0
- package/dist/core/memory.js +23 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/model.d.ts +47 -0
- package/dist/core/model.d.ts.map +1 -0
- package/dist/core/model.js +21 -0
- package/dist/core/model.js.map +1 -0
- package/dist/core/prose.d.ts +40 -0
- package/dist/core/prose.d.ts.map +1 -0
- package/dist/core/prose.js +106 -0
- package/dist/core/prose.js.map +1 -0
- package/dist/core/query.d.ts +87 -0
- package/dist/core/query.d.ts.map +1 -0
- package/dist/core/query.js +637 -0
- package/dist/core/query.js.map +1 -0
- package/dist/core/rank.d.ts +123 -0
- package/dist/core/rank.d.ts.map +1 -0
- package/dist/core/rank.js +429 -0
- package/dist/core/rank.js.map +1 -0
- package/dist/core/route-email.d.ts +48 -0
- package/dist/core/route-email.d.ts.map +1 -0
- package/dist/core/route-email.js +172 -0
- package/dist/core/route-email.js.map +1 -0
- package/dist/core/route.d.ts +431 -0
- package/dist/core/route.d.ts.map +1 -0
- package/dist/core/route.js +1769 -0
- package/dist/core/route.js.map +1 -0
- package/dist/core/search.d.ts +64 -0
- package/dist/core/search.d.ts.map +1 -0
- package/dist/core/search.js +123 -0
- package/dist/core/search.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/snapshot-email.json +1 -0
- package/dist/snapshot-foundations.json +1 -0
- package/dist/snapshot-web.json +1 -0
- package/package.json +28 -3
- 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
|