@cfbender/cesium 0.6.1 → 0.7.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 (35) hide show
  1. package/CHANGELOG.md +100 -1
  2. package/package.json +1 -1
  3. package/src/index.ts +2 -0
  4. package/src/prompt/system-fragment.md +73 -8
  5. package/src/render/annotate-frozen.ts +90 -0
  6. package/src/render/blocks/render.ts +20 -0
  7. package/src/render/blocks/renderers/callout.ts +3 -2
  8. package/src/render/blocks/renderers/code.ts +17 -2
  9. package/src/render/blocks/renderers/compare-table.ts +3 -2
  10. package/src/render/blocks/renderers/diagram.ts +3 -2
  11. package/src/render/blocks/renderers/diff.ts +23 -9
  12. package/src/render/blocks/renderers/hero.ts +3 -2
  13. package/src/render/blocks/renderers/kv.ts +3 -2
  14. package/src/render/blocks/renderers/list.ts +5 -4
  15. package/src/render/blocks/renderers/pill-row.ts +3 -2
  16. package/src/render/blocks/renderers/prose.ts +8 -2
  17. package/src/render/blocks/renderers/raw-html.ts +8 -2
  18. package/src/render/blocks/renderers/risk-table.ts +3 -2
  19. package/src/render/blocks/renderers/section.ts +4 -2
  20. package/src/render/blocks/renderers/timeline.ts +3 -2
  21. package/src/render/blocks/renderers/tldr.ts +3 -2
  22. package/src/render/client-js.ts +804 -6
  23. package/src/render/critique.ts +5 -335
  24. package/src/render/theme.ts +431 -6
  25. package/src/render/validate.ts +353 -97
  26. package/src/render/wrap.ts +67 -9
  27. package/src/server/api.ts +162 -3
  28. package/src/storage/index-gen.ts +4 -2
  29. package/src/storage/mutate.ts +433 -27
  30. package/src/tools/annotate.ts +336 -0
  31. package/src/tools/ask.ts +2 -6
  32. package/src/tools/critique.ts +15 -45
  33. package/src/tools/publish.ts +16 -56
  34. package/src/tools/styleguide.ts +7 -1
  35. package/src/tools/wait.ts +77 -24
@@ -1,13 +1,8 @@
1
- // Pure body analyzer mode-aware: html or blocks.
1
+ // Pure block-body analyzer for cesium artifacts.
2
2
  // Deterministic and pure: same input always yields the same output.
3
3
 
4
- import { parseFragment, defaultTreeAdapter as ta } from "parse5";
5
- import type { DefaultTreeAdapterMap, DefaultTreeAdapterTypes, ParserOptions } from "parse5";
6
4
  import type { Block } from "./blocks/types.ts";
7
5
 
8
- type ChildNode = DefaultTreeAdapterTypes.ChildNode;
9
- type Element = DefaultTreeAdapterTypes.Element;
10
-
11
6
  // ---------------------------------------------------------------------------
12
7
  // Public types
13
8
  // ---------------------------------------------------------------------------
@@ -22,7 +17,7 @@ export interface CritiqueFinding {
22
17
  message: string;
23
18
  /** Populated when the rule represents an aggregate count. */
24
19
  count?: number;
25
- /** Path in the block tree (blocks mode only), e.g. "blocks[2].children[1]". */
20
+ /** Path in the block tree, e.g. "blocks[2].children[1]". */
26
21
  path?: string;
27
22
  }
28
23
 
@@ -31,27 +26,14 @@ export interface CritiqueResult {
31
26
  score: number;
32
27
  /** Ordered: warn → suggest → info, then alphabetically by code. */
33
28
  findings: CritiqueFinding[];
34
- /** Sum of all text node values in the body (visible text content length). */
29
+ /** Sum of all text characters in the block tree (visible content length). */
35
30
  textLength: number;
36
- /** Which input mode was detected. */
37
- mode: "html" | "blocks";
38
31
  }
39
32
 
40
33
  // ---------------------------------------------------------------------------
41
34
  // Internal constants
42
35
  // ---------------------------------------------------------------------------
43
36
 
44
- const HTTP_RE = /^https?:\/\//i;
45
-
46
- /** The only cesium-* class the framework ships with. All others are unknown. */
47
- const KNOWN_CESIUM_CLASSES = new Set(["cesium-back", "cesium-eyebrow"]);
48
-
49
- /** Callout severity modifiers — a callout needs at least one of these. */
50
- const CALLOUT_MODIFIERS = new Set(["note", "warn", "risk"]);
51
-
52
- /** Inline highlight span classes for .code blocks. */
53
- const CODE_HIGHLIGHT_CLASSES = new Set(["kw", "str", "cm", "fn"]);
54
-
55
37
  const SEVERITY_DEDUCTION: Record<CritiqueSeverity, number> = {
56
38
  warn: 10,
57
39
  suggest: 3,
@@ -64,58 +46,6 @@ const SEVERITY_ORDER: Record<CritiqueSeverity, number> = {
64
46
  info: 2,
65
47
  };
66
48
 
67
- // ---------------------------------------------------------------------------
68
- // Tree-walking helpers (HTML mode)
69
- // ---------------------------------------------------------------------------
70
-
71
- function walkNodes(nodes: readonly ChildNode[], visitor: (node: ChildNode) => void): void {
72
- for (const node of nodes) {
73
- visitor(node);
74
- if (ta.isElementNode(node)) {
75
- walkNodes(ta.getChildNodes(node as Element) as ChildNode[], visitor);
76
- }
77
- }
78
- }
79
-
80
- function getClasses(el: Element): Set<string> {
81
- const attr = ta.getAttrList(el).find((a) => a.name === "class");
82
- if (!attr || !attr.value.trim()) return new Set();
83
- return new Set(attr.value.trim().split(/\s+/));
84
- }
85
-
86
- function attrVal(el: Element, name: string): string | undefined {
87
- return ta.getAttrList(el).find((a) => a.name === name)?.value;
88
- }
89
-
90
- /** Recursively sum all text-node values — gives the visible text content length. */
91
- function collectTextLength(nodes: readonly ChildNode[]): number {
92
- let total = 0;
93
- for (const node of nodes) {
94
- if (ta.isTextNode(node)) {
95
- total += node.value.length;
96
- } else if (ta.isElementNode(node)) {
97
- total += collectTextLength(ta.getChildNodes(node as Element) as ChildNode[]);
98
- }
99
- }
100
- return total;
101
- }
102
-
103
- /** Returns true if any descendant of `el` carries a code-highlight class. */
104
- function hasHighlightDescendant(el: Element): boolean {
105
- let found = false;
106
- walkNodes(ta.getChildNodes(el) as ChildNode[], (node) => {
107
- if (found || !ta.isElementNode(node)) return;
108
- const cls = getClasses(node as Element);
109
- for (const c of CODE_HIGHLIGHT_CLASSES) {
110
- if (cls.has(c)) {
111
- found = true;
112
- return;
113
- }
114
- }
115
- });
116
- return found;
117
- }
118
-
119
49
  // ---------------------------------------------------------------------------
120
50
  // Scoring + ordering
121
51
  // ---------------------------------------------------------------------------
@@ -136,254 +66,6 @@ function sortFindings(findings: CritiqueFinding[]): CritiqueFinding[] {
136
66
  });
137
67
  }
138
68
 
139
- // ---------------------------------------------------------------------------
140
- // HTML mode — preserves all existing rules + adds prefer-blocks
141
- // ---------------------------------------------------------------------------
142
-
143
- export function critiqueHtml(htmlBody: string): CritiqueResult {
144
- const fragment = parseFragment(htmlBody);
145
- const children = ta.getChildNodes(fragment) as ChildNode[];
146
- const findings: CritiqueFinding[] = [];
147
-
148
- // Visible text content length
149
- const textLength = collectTextLength(children);
150
-
151
- // Counters / flags accumulated in the single tree walk
152
- let hDisplayCount = 0;
153
- let hSectionCount = 0;
154
- let tldrCount = 0;
155
- let eyebrowCount = 0;
156
- let calloutNoModifierCount = 0;
157
- let externalResourceFound = false;
158
- let inlineStyleCount = 0;
159
- let hasSection = false;
160
- const unknownCesiumClasses = new Set<string>();
161
- let codeBlocksWithoutHighlights = 0;
162
-
163
- // prefer-blocks heuristic counters
164
- let structuralElementCount = 0;
165
-
166
- walkNodes(children, (node) => {
167
- if (!ta.isElementNode(node)) return;
168
- const el = node as Element;
169
- const tag = ta.getTagName(el);
170
- const cls = getClasses(el);
171
-
172
- // Section tag present?
173
- if (tag === "section") hasSection = true;
174
-
175
- // --- External resource detection (warn) ---
176
- if (!externalResourceFound) {
177
- if (tag === "script" && attrVal(el, "src") !== undefined) {
178
- externalResourceFound = true;
179
- } else if (tag === "link") {
180
- const rel = (attrVal(el, "rel") ?? "").toLowerCase();
181
- const href = attrVal(el, "href") ?? "";
182
- if (rel === "stylesheet" && HTTP_RE.test(href)) {
183
- externalResourceFound = true;
184
- }
185
- } else if (tag === "img") {
186
- const src = attrVal(el, "src") ?? "";
187
- if (HTTP_RE.test(src)) {
188
- externalResourceFound = true;
189
- }
190
- }
191
- }
192
-
193
- // --- Class-based counts ---
194
- if (cls.has("h-display")) hDisplayCount++;
195
- if (cls.has("h-section")) {
196
- hSectionCount++;
197
- structuralElementCount++;
198
- }
199
- if (cls.has("tldr")) tldrCount++;
200
- if (cls.has("eyebrow")) eyebrowCount++;
201
-
202
- // Callout without a severity modifier
203
- if (cls.has("callout")) {
204
- structuralElementCount++;
205
- let hasModifier = false;
206
- for (const mod of CALLOUT_MODIFIERS) {
207
- if (cls.has(mod)) {
208
- hasModifier = true;
209
- break;
210
- }
211
- }
212
- if (!hasModifier) calloutNoModifierCount++;
213
- }
214
-
215
- // compare-table counts as structural
216
- if (cls.has("compare-table")) structuralElementCount++;
217
-
218
- // Unknown cesium-* class names
219
- for (const c of cls) {
220
- if (c.startsWith("cesium-") && !KNOWN_CESIUM_CLASSES.has(c)) {
221
- unknownCesiumClasses.add(c);
222
- }
223
- }
224
-
225
- // .code block without inline highlights
226
- if (cls.has("code") && !hasHighlightDescendant(el)) {
227
- codeBlocksWithoutHighlights++;
228
- }
229
-
230
- // Inline style attribute
231
- if (attrVal(el, "style") !== undefined) inlineStyleCount++;
232
- });
233
-
234
- // Detect parse errors via a second parseFragment pass with onParseError
235
- let parseErrors = 0;
236
- const parseOpts: ParserOptions<DefaultTreeAdapterMap> = {
237
- onParseError: () => {
238
- parseErrors++;
239
- },
240
- };
241
- parseFragment(htmlBody, parseOpts);
242
-
243
- // ---------------------------------------------------------------------------
244
- // Emit warn-level findings
245
- // ---------------------------------------------------------------------------
246
-
247
- if (externalResourceFound) {
248
- findings.push({
249
- severity: "warn",
250
- code: "external-resource",
251
- message:
252
- "External resource will be stripped at publish time. Use inline styles/scripts/data URIs.",
253
- });
254
- }
255
-
256
- if (hDisplayCount > 1) {
257
- findings.push({
258
- severity: "warn",
259
- code: "multiple-h-display",
260
- message: "Only one .h-display per artifact (it's the page title).",
261
- });
262
- }
263
-
264
- if (parseErrors > 0) {
265
- findings.push({
266
- severity: "warn",
267
- code: "unbalanced-html",
268
- message:
269
- "Body has structural HTML issues; the browser will attempt recovery but layout may break.",
270
- });
271
- }
272
-
273
- if (unknownCesiumClasses.size > 0) {
274
- findings.push({
275
- severity: "warn",
276
- code: "unknown-cesium-class",
277
- message: `Found ${unknownCesiumClasses.size} unknown cesium-* class names; the framework only ships with the documented vocabulary.`,
278
- count: unknownCesiumClasses.size,
279
- });
280
- }
281
-
282
- // ---------------------------------------------------------------------------
283
- // Emit suggest-level findings
284
- // ---------------------------------------------------------------------------
285
-
286
- if (hDisplayCount === 0) {
287
- findings.push({
288
- severity: "suggest",
289
- code: "no-h-display",
290
- message: "No .h-display heading; agent should give the artifact a clear page title.",
291
- });
292
- }
293
-
294
- if (tldrCount === 0 && textLength > 1500) {
295
- findings.push({
296
- severity: "suggest",
297
- code: "no-tldr",
298
- message: "Long artifact with no .tldr summary; consider adding one near the top.",
299
- });
300
- }
301
-
302
- if (eyebrowCount === 0) {
303
- findings.push({
304
- severity: "suggest",
305
- code: "no-eyebrow",
306
- message: "No .eyebrow micro-labels — they help anchor sections and document type.",
307
- });
308
- }
309
-
310
- if (textLength > 1200 && hSectionCount === 0 && !hasSection) {
311
- findings.push({
312
- severity: "suggest",
313
- code: "unsectioned-long-body",
314
- message: "Long body has no section markers; readability suffers.",
315
- });
316
- }
317
-
318
- if (calloutNoModifierCount > 0) {
319
- findings.push({
320
- severity: "suggest",
321
- code: "callout-without-modifier",
322
- message: `${calloutNoModifierCount} callout${calloutNoModifierCount === 1 ? "" : "s"} have no severity modifier (.note/.warn/.risk).`,
323
- count: calloutNoModifierCount,
324
- });
325
- }
326
-
327
- if (textLength < 250) {
328
- findings.push({
329
- severity: "suggest",
330
- code: "body-too-short",
331
- message:
332
- "Very short body; a terminal reply may be more appropriate than a published artifact.",
333
- });
334
- }
335
-
336
- // prefer-blocks: suggest when ≥3 structural elements could be expressed as blocks
337
- if (structuralElementCount >= 3) {
338
- findings.push({
339
- severity: "suggest",
340
- code: "prefer-blocks",
341
- message: `This document has ${structuralElementCount} structural elements that could be expressed as blocks. Consider using cesium_publish({ blocks: [...] }).`,
342
- count: structuralElementCount,
343
- });
344
- }
345
-
346
- // ---------------------------------------------------------------------------
347
- // Emit info-level findings
348
- // ---------------------------------------------------------------------------
349
-
350
- if (textLength > 25000) {
351
- findings.push({
352
- severity: "info",
353
- code: "body-very-long",
354
- message: "Very long artifact; consider splitting into linked smaller pieces.",
355
- });
356
- }
357
-
358
- if (codeBlocksWithoutHighlights > 0) {
359
- findings.push({
360
- severity: "info",
361
- code: "code-without-highlights",
362
- message:
363
- "Code blocks render without inline highlights; readers benefit from .kw/.str/.cm/.fn spans.",
364
- count: codeBlocksWithoutHighlights,
365
- });
366
- }
367
-
368
- if (inlineStyleCount > 8) {
369
- findings.push({
370
- severity: "info",
371
- code: "inline-style-heavy",
372
- message: "Heavy reliance on inline styles; named classes are usually clearer.",
373
- count: inlineStyleCount,
374
- });
375
- }
376
-
377
- // ---------------------------------------------------------------------------
378
- // Sort, score, return
379
- // ---------------------------------------------------------------------------
380
-
381
- const sorted = sortFindings(findings);
382
- const score = computeScore(sorted, 100);
383
-
384
- return { score, findings: sorted, textLength, mode: "html" };
385
- }
386
-
387
69
  // ---------------------------------------------------------------------------
388
70
  // Blocks mode — quality-focused rules
389
71
  // ---------------------------------------------------------------------------
@@ -620,7 +302,7 @@ function hasTldr(blocks: readonly Block[]): boolean {
620
302
  return false;
621
303
  }
622
304
 
623
- export function critiqueBlocks(blocks: Block[]): CritiqueResult {
305
+ export function critique(blocks: Block[]): CritiqueResult {
624
306
  const findings: CritiqueFinding[] = [];
625
307
 
626
308
  // Total text content for ratio checks
@@ -744,17 +426,5 @@ export function critiqueBlocks(blocks: Block[]): CritiqueResult {
744
426
  // blocks mode starts at 105, capped at 100 — gives a +5 bonus for well-formed docs
745
427
  const score = computeScore(sorted, 105);
746
428
 
747
- return { score, findings: sorted, textLength: totalChars, mode: "blocks" };
748
- }
749
-
750
- // ---------------------------------------------------------------------------
751
- // Backwards-compat wrapper — accepts an HTML body string (html mode)
752
- // ---------------------------------------------------------------------------
753
-
754
- /**
755
- * @deprecated Use critiqueHtml() or critiqueBlocks() directly.
756
- * Kept for backwards compatibility with existing call sites.
757
- */
758
- export function critique(htmlBody: string): CritiqueResult {
759
- return critiqueHtml(htmlBody);
429
+ return { score, findings: sorted, textLength: totalChars };
760
430
  }