@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.
- package/CHANGELOG.md +100 -1
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/prompt/system-fragment.md +73 -8
- package/src/render/annotate-frozen.ts +90 -0
- package/src/render/blocks/render.ts +20 -0
- package/src/render/blocks/renderers/callout.ts +3 -2
- package/src/render/blocks/renderers/code.ts +17 -2
- package/src/render/blocks/renderers/compare-table.ts +3 -2
- package/src/render/blocks/renderers/diagram.ts +3 -2
- package/src/render/blocks/renderers/diff.ts +23 -9
- package/src/render/blocks/renderers/hero.ts +3 -2
- package/src/render/blocks/renderers/kv.ts +3 -2
- package/src/render/blocks/renderers/list.ts +5 -4
- package/src/render/blocks/renderers/pill-row.ts +3 -2
- package/src/render/blocks/renderers/prose.ts +8 -2
- package/src/render/blocks/renderers/raw-html.ts +8 -2
- package/src/render/blocks/renderers/risk-table.ts +3 -2
- package/src/render/blocks/renderers/section.ts +4 -2
- package/src/render/blocks/renderers/timeline.ts +3 -2
- package/src/render/blocks/renderers/tldr.ts +3 -2
- package/src/render/client-js.ts +804 -6
- package/src/render/critique.ts +5 -335
- package/src/render/theme.ts +431 -6
- package/src/render/validate.ts +353 -97
- package/src/render/wrap.ts +67 -9
- package/src/server/api.ts +162 -3
- package/src/storage/index-gen.ts +4 -2
- package/src/storage/mutate.ts +433 -27
- package/src/tools/annotate.ts +336 -0
- package/src/tools/ask.ts +2 -6
- package/src/tools/critique.ts +15 -45
- package/src/tools/publish.ts +16 -56
- package/src/tools/styleguide.ts +7 -1
- package/src/tools/wait.ts +77 -24
package/src/render/critique.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
// Pure body analyzer
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|