paperlint 14.0.0 → 14.1.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/dist/skills/build-label-page/scripts/label-page-core.d.ts +68 -1
- package/dist/skills/build-label-page/scripts/label-page-presets.d.ts +10 -0
- package/package.json +1 -1
- package/skills/build-label-page/SKILL.md +6 -2
- package/skills/build-label-page/SKILL.md.spec.ts +5 -1
- package/skills/build-label-page/references/closed-choice-generator.md +129 -5
- package/skills/build-label-page/scripts/fixtures/blocks-items.json +214 -0
- package/skills/build-label-page/scripts/fixtures/linter-config-witness-items.json +448 -0
- package/skills/build-label-page/scripts/label-page-blocks.test.ts +335 -0
- package/skills/build-label-page/scripts/label-page-browser.test.ts +328 -2
- package/skills/build-label-page/scripts/label-page-core.js +154 -12
- package/skills/build-label-page/scripts/label-page-core.test.ts +54 -2
- package/skills/build-label-page/scripts/label-page-core.ts +253 -31
- package/skills/build-label-page/scripts/label-page-html.test.ts +22 -0
- package/skills/build-label-page/scripts/label-page-presets.js +222 -0
- package/skills/build-label-page/scripts/label-page-presets.test.ts +175 -0
- package/skills/build-label-page/scripts/label-page-presets.ts +257 -0
- package/skills/build-label-page/scripts/label-page.js +8 -2
- package/skills/build-label-page/scripts/label-page.ts +8 -2
- package/skills/build-label-page/scripts/page-template.html +237 -30
|
@@ -9,6 +9,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { describeRule, } from "./rule-descriptions.js";
|
|
12
|
+
import { applyPreset } from "./label-page-presets.js";
|
|
12
13
|
// ── refusals and output ───────────────────────────────────────────────────────────────────────
|
|
13
14
|
export class Refusal extends Error {
|
|
14
15
|
exitCode;
|
|
@@ -74,6 +75,11 @@ const writeJson = (path, value) => {
|
|
|
74
75
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
75
76
|
};
|
|
76
77
|
const JSON_VALUE = z.json();
|
|
78
|
+
/** The config with its named preset applied (label-page-presets.ts), refused by path. */
|
|
79
|
+
const withPreset = (raw) => {
|
|
80
|
+
const r = applyPreset(raw);
|
|
81
|
+
return r.ok ? JSON_VALUE.parse(r.value) : fail(r.message);
|
|
82
|
+
};
|
|
77
83
|
const readJson = (path) => {
|
|
78
84
|
try {
|
|
79
85
|
return JSON_VALUE.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
@@ -99,11 +105,22 @@ const RULES_FIELD = "rules";
|
|
|
99
105
|
const UNIT_FIELD = "unit";
|
|
100
106
|
// Two readings of one requirement, shown as rows A and B (#374).
|
|
101
107
|
const READINGS_FIELD = "readings";
|
|
108
|
+
// Content blocks (#380): generic, on any kind. Every label and sentence a block shows comes from
|
|
109
|
+
// items.json (or a preset, label-page-presets.ts); the page knows only the block's shape.
|
|
110
|
+
const BLOCK_FIELDS = [
|
|
111
|
+
"situation",
|
|
112
|
+
"excerpt",
|
|
113
|
+
"references",
|
|
114
|
+
"more",
|
|
115
|
+
"table",
|
|
116
|
+
"repeat",
|
|
117
|
+
];
|
|
102
118
|
const ALLOWED_FIELDS = [
|
|
103
119
|
...PAGE_FIELDS,
|
|
104
120
|
RULES_FIELD,
|
|
105
121
|
UNIT_FIELD,
|
|
106
122
|
READINGS_FIELD,
|
|
123
|
+
...BLOCK_FIELDS,
|
|
107
124
|
...PRIVATE_FIELDS,
|
|
108
125
|
];
|
|
109
126
|
const FAMILIARITY = [
|
|
@@ -140,6 +157,8 @@ const string = (required = true, pattern, why) => pattern === undefined
|
|
|
140
157
|
pattern,
|
|
141
158
|
...(why === undefined ? {} : { why }),
|
|
142
159
|
};
|
|
160
|
+
/** A string that may be empty: a blank line of code. */
|
|
161
|
+
const text = () => ({ kind: "text", required: true });
|
|
143
162
|
const integer = (required = true) => ({
|
|
144
163
|
kind: "integer",
|
|
145
164
|
required,
|
|
@@ -164,6 +183,13 @@ const mapping = (values, keyPattern, min = 0) => ({
|
|
|
164
183
|
const obj = (fields, closed = true, required = true) => ({ kind: "object", required, closed, fields });
|
|
165
184
|
// What an item and a practice item both may carry beyond text: a practice card looks like the real
|
|
166
185
|
// cards it rehearses. Each is optional here; a page's `kind` makes some of them required (KINDS).
|
|
186
|
+
const LINK = string(false, "https?://\\S+", "an http(s) link");
|
|
187
|
+
const optional = (fields) => obj(fields, true, false);
|
|
188
|
+
const TEXT_FIELD = obj({
|
|
189
|
+
label: string(false),
|
|
190
|
+
text: string(),
|
|
191
|
+
style: string(false, "code|note", "code or note"),
|
|
192
|
+
});
|
|
167
193
|
const CARD_FIELDS = {
|
|
168
194
|
[RULES_FIELD]: array(obj({
|
|
169
195
|
linter: string(true, "eslint|ruff", "eslint or ruff"),
|
|
@@ -180,6 +206,43 @@ const CARD_FIELDS = {
|
|
|
180
206
|
}, true, false),
|
|
181
207
|
[READINGS_FIELD]: obj({ a: string(), b: string() }, true, false),
|
|
182
208
|
file: string(false),
|
|
209
|
+
situation: optional({ summary: string(), question: string() }),
|
|
210
|
+
excerpt: optional({
|
|
211
|
+
file: string(false),
|
|
212
|
+
url: LINK,
|
|
213
|
+
lines: array(obj({ n: integer(), text: text(), mark: boolean() }), true, 1),
|
|
214
|
+
caption: string(false),
|
|
215
|
+
mark_label: string(false),
|
|
216
|
+
}),
|
|
217
|
+
references: optional({
|
|
218
|
+
summary: string(),
|
|
219
|
+
entries: array(obj({
|
|
220
|
+
label: string(false),
|
|
221
|
+
title: string(),
|
|
222
|
+
fields: array(TEXT_FIELD, true, 1),
|
|
223
|
+
url: LINK,
|
|
224
|
+
url_label: string(false),
|
|
225
|
+
}), true, 1),
|
|
226
|
+
}),
|
|
227
|
+
more: optional({
|
|
228
|
+
summary: string(),
|
|
229
|
+
items: array(string(), true, 1),
|
|
230
|
+
rest: string(false),
|
|
231
|
+
}),
|
|
232
|
+
table: optional({
|
|
233
|
+
summary: string(),
|
|
234
|
+
note: string(false),
|
|
235
|
+
rows: array(obj({
|
|
236
|
+
label: string(),
|
|
237
|
+
value: string(),
|
|
238
|
+
plain: string(false),
|
|
239
|
+
mark: optional({
|
|
240
|
+
tone: string(true, "yes|no", "yes or no"),
|
|
241
|
+
text: string(),
|
|
242
|
+
}),
|
|
243
|
+
}), true, 1),
|
|
244
|
+
}),
|
|
245
|
+
repeat: optional({ group: string(), index: integer(), of: integer() }),
|
|
183
246
|
};
|
|
184
247
|
const KINDS = {
|
|
185
248
|
// keep or drop a drawn requirement: the line it came from, marked, and the rule read for it
|
|
@@ -254,6 +317,15 @@ const configSchema = (kind) => obj({
|
|
|
254
317
|
questions: array(QUESTION_SCHEMA, true, 1),
|
|
255
318
|
practice: array(practiceSchema(kind), true, MIN_PRACTICE),
|
|
256
319
|
practice_round: integer(false),
|
|
320
|
+
// Page-level words the blocks use (references/closed-choice-generator.md §«Content blocks»).
|
|
321
|
+
labels: optional({
|
|
322
|
+
place: string(false),
|
|
323
|
+
body: string(false),
|
|
324
|
+
line: string(false),
|
|
325
|
+
line_part: string(false),
|
|
326
|
+
repeat: string(false),
|
|
327
|
+
practice_example: string(false),
|
|
328
|
+
}),
|
|
257
329
|
items: array(itemSchema(kind), true, 1),
|
|
258
330
|
});
|
|
259
331
|
/** The declared schema (`schema` prints it): every field, those a kind requires left optional. */
|
|
@@ -310,6 +382,14 @@ function checkBoolean(value, where) {
|
|
|
310
382
|
if (typeof value !== "boolean")
|
|
311
383
|
fail(`${where}: must be true or false, got ${repr(value)}`);
|
|
312
384
|
}
|
|
385
|
+
/** A string that may be empty (a blank line of code), or an integer. */
|
|
386
|
+
function checkScalar(value, kind, where) {
|
|
387
|
+
if (kind === "text" && typeof value !== "string")
|
|
388
|
+
fail(`${where}: must be a string, got ${repr(value)}`);
|
|
389
|
+
if (kind === "integer" &&
|
|
390
|
+
(typeof value !== "number" || !Number.isInteger(value)))
|
|
391
|
+
fail(`${where}: must be an integer, got ${repr(value)}`);
|
|
392
|
+
}
|
|
313
393
|
/** Refuse the first place `value` departs from `node`; the message starts with the field's path. */
|
|
314
394
|
function checkValue(value, node, path) {
|
|
315
395
|
const where = path || "items.json";
|
|
@@ -317,10 +397,9 @@ function checkValue(value, node, path) {
|
|
|
317
397
|
case "string":
|
|
318
398
|
checkString(value, node, where);
|
|
319
399
|
break;
|
|
400
|
+
case "text":
|
|
320
401
|
case "integer":
|
|
321
|
-
|
|
322
|
-
fail(`${where}: must be an integer, got ${repr(value)}`);
|
|
323
|
-
}
|
|
402
|
+
checkScalar(value, node.kind, where);
|
|
324
403
|
break;
|
|
325
404
|
case "boolean":
|
|
326
405
|
checkBoolean(value, where);
|
|
@@ -469,6 +548,33 @@ function checkUnits(cfg) {
|
|
|
469
548
|
});
|
|
470
549
|
}
|
|
471
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* A situation's sentence or question may name the excerpt's marked line as `{line}`: the page writes
|
|
553
|
+
* «line N: » and the line's code there, so a question sits next to the code it is about.
|
|
554
|
+
*/
|
|
555
|
+
const LINE_TOKEN = "{line}";
|
|
556
|
+
const SITUATION_TEXTS = ["summary", "question"];
|
|
557
|
+
/** The line `{line}` stands for: the first marked line, its number and its code without the indent. */
|
|
558
|
+
const lineOf = (card) => card.excerpt?.lines
|
|
559
|
+
.filter((l) => l.mark)
|
|
560
|
+
.map((l) => ({ n: l.n, text: l.text.trim() }))[0];
|
|
561
|
+
const usesLine = (card) => SITUATION_TEXTS.some((f) => card.situation?.[f].includes(LINE_TOKEN));
|
|
562
|
+
/**
|
|
563
|
+
* What the block schema cannot say: an excerpt marks a line, `{line}` has a marked line to stand for,
|
|
564
|
+
* and a repeat sits inside its count.
|
|
565
|
+
*/
|
|
566
|
+
function checkBlocks(cfg) {
|
|
567
|
+
cardsOf(cfg).forEach(([at, card]) => {
|
|
568
|
+
if (card.excerpt !== undefined && !card.excerpt.lines.some((l) => l.mark))
|
|
569
|
+
fail(`${at}.excerpt.lines: no line is marked — mark the line the card is about (\`mark: true\`)`);
|
|
570
|
+
const token = SITUATION_TEXTS.find((f) => card.situation?.[f].includes(LINE_TOKEN));
|
|
571
|
+
if (token !== undefined && card.excerpt === undefined)
|
|
572
|
+
fail(`${at}.situation.${token}: \`${LINE_TOKEN}\` names the excerpt's marked line, and the card has no \`excerpt\` — add it, or write the ${token} without \`${LINE_TOKEN}\``);
|
|
573
|
+
const r = card.repeat;
|
|
574
|
+
if (r !== undefined && (r.index < 1 || r.index > r.of))
|
|
575
|
+
fail(`${at}.repeat.index: ${String(r.index)} is not between 1 and ${String(r.of)}`);
|
|
576
|
+
});
|
|
577
|
+
}
|
|
472
578
|
/** Every card of the config, items then practice, by path. */
|
|
473
579
|
const cardsOf = (cfg) => [
|
|
474
580
|
...cfg.items.map((i, n) => [`items[${String(n)}]`, i]),
|
|
@@ -490,12 +596,22 @@ function checkItemFields(cfg) {
|
|
|
490
596
|
fail(`items[${String(n)}].group: ${repr(item.group)} is not in \`groups\``);
|
|
491
597
|
});
|
|
492
598
|
}
|
|
493
|
-
/** Every
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
599
|
+
/** Every string inside a value: each is tested on its own, as the page renders it, never a serialisation. */
|
|
600
|
+
const stringsIn = (v) => typeof v === "string"
|
|
601
|
+
? [v]
|
|
602
|
+
: Array.isArray(v)
|
|
603
|
+
? v.flatMap(stringsIn)
|
|
604
|
+
: typeof v === "object" && v !== null
|
|
605
|
+
? Object.values(v).flatMap(stringsIn)
|
|
606
|
+
: [];
|
|
607
|
+
/**
|
|
608
|
+
* Every text of an item the labeller sees, by field: each string the page shows (the fields as
|
|
609
|
+
* `shownFields` passes them, so a repeat without its group), and every rule it names.
|
|
610
|
+
*/
|
|
611
|
+
const visibleTexts = (item) => Object.entries({
|
|
612
|
+
...shownFields(item),
|
|
613
|
+
[RULES_FIELD]: item.rules ?? [],
|
|
614
|
+
}).flatMap(([f, v]) => stringsIn(v).map((t) => [f, t]));
|
|
499
615
|
const escapeRegExp = (s) => s.replace(/[\\^$.*+?()[\]{}|/]/g, "\\$&");
|
|
500
616
|
/**
|
|
501
617
|
* A source id typed into a visible field gives the source away as well as a field would. Short ids
|
|
@@ -549,6 +665,17 @@ function assignIds(items, seed, keyPath) {
|
|
|
549
665
|
]);
|
|
550
666
|
return { ids, entries };
|
|
551
667
|
}
|
|
668
|
+
// ── a build, short of the page ──────────────────────────────────────────────────────────────────
|
|
669
|
+
/** A card's situation as the page shows it, with the line `{line}` stands for. */
|
|
670
|
+
const situationShown = (item) => item.situation === undefined
|
|
671
|
+
? {}
|
|
672
|
+
: {
|
|
673
|
+
situation: {
|
|
674
|
+
summary: item.situation.summary,
|
|
675
|
+
question: item.situation.question,
|
|
676
|
+
...(usesLine(item) ? { line: lineOf(item) } : {}),
|
|
677
|
+
},
|
|
678
|
+
};
|
|
552
679
|
const shownFields = (item) => ({
|
|
553
680
|
...Object.fromEntries(PAGE_FIELDS.flatMap((f) => {
|
|
554
681
|
const v = item[f];
|
|
@@ -565,15 +692,20 @@ const shownFields = (item) => ({
|
|
|
565
692
|
},
|
|
566
693
|
}),
|
|
567
694
|
...(item.readings === undefined ? {} : { [READINGS_FIELD]: item.readings }),
|
|
695
|
+
...Object.fromEntries(BLOCK_FIELDS.filter((f) => f !== "repeat" && f !== "situation").flatMap((f) => (item[f] === undefined ? [] : [[f, item[f]]]))),
|
|
696
|
+
...situationShown(item),
|
|
697
|
+
...(item.repeat === undefined
|
|
698
|
+
? {}
|
|
699
|
+
: { repeat: { index: item.repeat.index, of: item.repeat.of } }),
|
|
568
700
|
});
|
|
569
701
|
const ruleKey = (r) => `${r.linter} ${r.id}`;
|
|
570
702
|
/** Ask the linter for every rule named; refuse, by path, a rule it cannot describe. */
|
|
571
703
|
function describeRules(cfg, fromDir) {
|
|
572
|
-
return new Map(cardsOf(cfg).flatMap(([at, card]) => (card.rules ?? []).
|
|
704
|
+
return new Map(cardsOf(cfg).flatMap(([at, card]) => (card.rules ?? []).flatMap((r, k) => {
|
|
573
705
|
const text = describeRule(r, fromDir);
|
|
574
706
|
return text === undefined
|
|
575
707
|
? fail(`${at}.rules[${String(k)}]: ${r.linter} ${repr(r.id)} has no description from the linter (${r.linter === "ruff" ? "`ruff rule` does not know it, or ruff is not on PATH" : "not a rule of the ESLint, or of a plugin, installed where items.json is"}) — check the id`)
|
|
576
|
-
: [ruleKey(r), text];
|
|
708
|
+
: [[ruleKey(r), text]];
|
|
577
709
|
})));
|
|
578
710
|
}
|
|
579
711
|
const rulesShown = (card, described) => card.rules === undefined
|
|
@@ -606,12 +738,21 @@ const practiceShown = (cfg, described) => practiceOf(cfg).map((p) => ({
|
|
|
606
738
|
...shownFields(p),
|
|
607
739
|
...rulesShown(p, described),
|
|
608
740
|
}));
|
|
741
|
+
/** The page's own words where `labels` leaves one out (references/closed-choice-generator.md). */
|
|
742
|
+
const DEFAULT_LABELS = {
|
|
743
|
+
place: "{id} · {n} of {total}",
|
|
744
|
+
line: "Line",
|
|
745
|
+
line_part: "{line} · part {part} of {of}",
|
|
746
|
+
repeat: "Card {index} of {of}",
|
|
747
|
+
practice_example: "",
|
|
748
|
+
};
|
|
609
749
|
/** What a page shows: the questions, the practice and the items, blind, with their neutral ids. */
|
|
610
750
|
function payloadOf(cfg, page, described) {
|
|
611
751
|
const used = Object.keys(cfg.groups).filter((g) => page.items.some((i) => i.group === g));
|
|
612
752
|
return {
|
|
613
753
|
title: cfg.title,
|
|
614
754
|
intro: cfg.intro ?? "",
|
|
755
|
+
labels: { ...DEFAULT_LABELS, ...cfg.labels },
|
|
615
756
|
groups: used.map((g) => ({ id: g, label: String(cfg.groups[g]) })),
|
|
616
757
|
familiarity_question: asksFamiliarity(cfg)
|
|
617
758
|
? (cfg.familiarity_question ?? "How well do you know {group}?")
|
|
@@ -689,13 +830,14 @@ function keyOf(cfg, page, earlier) {
|
|
|
689
830
|
* returns what the page shows. A page written by hand can take its blindness and its key from here.
|
|
690
831
|
*/
|
|
691
832
|
export function prepareBuild(configPath, outDir) {
|
|
692
|
-
const cfg = readJson(configPath);
|
|
833
|
+
const cfg = withPreset(readJson(configPath));
|
|
693
834
|
assertConfig(cfg);
|
|
694
835
|
// Once its `kind` is known to be one, the fields that kind needs on every card.
|
|
695
836
|
checkValue(cfg, configSchema(cfg.kind), "");
|
|
696
837
|
checkConfig(cfg);
|
|
697
838
|
checkItemFields(cfg);
|
|
698
839
|
checkUnits(cfg);
|
|
840
|
+
checkBlocks(cfg);
|
|
699
841
|
checkNoSourceShown(cfg);
|
|
700
842
|
const described = describeRules(cfg, dirname(configPath));
|
|
701
843
|
mkdirSync(outDir, { recursive: true });
|
|
@@ -29,7 +29,15 @@ import {
|
|
|
29
29
|
/** A node of the declared config schema, as `example-generator.py schema` prints it. */
|
|
30
30
|
const Node: z.ZodType<SchemaNode> = z.lazy(() =>
|
|
31
31
|
z.object({
|
|
32
|
-
kind: z.enum([
|
|
32
|
+
kind: z.enum([
|
|
33
|
+
"string",
|
|
34
|
+
"text",
|
|
35
|
+
"integer",
|
|
36
|
+
"boolean",
|
|
37
|
+
"map",
|
|
38
|
+
"array",
|
|
39
|
+
"object",
|
|
40
|
+
]),
|
|
33
41
|
required: z.boolean(),
|
|
34
42
|
pattern: z.string().optional(),
|
|
35
43
|
key_pattern: z.string().optional(),
|
|
@@ -42,7 +50,8 @@ const Node: z.ZodType<SchemaNode> = z.lazy(() =>
|
|
|
42
50
|
);
|
|
43
51
|
|
|
44
52
|
interface SchemaNode {
|
|
45
|
-
readonly kind:
|
|
53
|
+
readonly kind:
|
|
54
|
+
"string" | "text" | "integer" | "boolean" | "map" | "array" | "object";
|
|
46
55
|
readonly required: boolean;
|
|
47
56
|
readonly pattern?: string | undefined;
|
|
48
57
|
readonly key_pattern?: string | undefined;
|
|
@@ -73,6 +82,40 @@ const CARD = {
|
|
|
73
82
|
rules: [{ linter: "eslint", id: "no-console" }],
|
|
74
83
|
unit: { id: "L0", text: "One line.", mark: "One", part: 1, of: 1 },
|
|
75
84
|
readings: { a: "on", b: "off" },
|
|
85
|
+
situation: { summary: "S", question: "Q?" },
|
|
86
|
+
excerpt: {
|
|
87
|
+
file: "src/a.ts",
|
|
88
|
+
url: "https://example.com/a.ts",
|
|
89
|
+
lines: [{ n: 1, text: "", mark: true }],
|
|
90
|
+
caption: "C",
|
|
91
|
+
mark_label: "M",
|
|
92
|
+
},
|
|
93
|
+
references: {
|
|
94
|
+
summary: "S",
|
|
95
|
+
entries: [
|
|
96
|
+
{
|
|
97
|
+
label: "L",
|
|
98
|
+
title: "T",
|
|
99
|
+
fields: [{ label: "F", text: "X", style: "code" }],
|
|
100
|
+
url: "https://example.com/r",
|
|
101
|
+
url_label: "U",
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
more: { summary: "S", items: ["`src/b.ts`"], rest: "R" },
|
|
106
|
+
table: {
|
|
107
|
+
summary: "S",
|
|
108
|
+
note: "N",
|
|
109
|
+
rows: [
|
|
110
|
+
{
|
|
111
|
+
label: "L",
|
|
112
|
+
value: "V",
|
|
113
|
+
plain: "P",
|
|
114
|
+
mark: { tone: "yes", text: "Y" },
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
repeat: { group: "g", index: 1, of: 1 },
|
|
76
119
|
};
|
|
77
120
|
/** A config with EVERY optional field present, so every schema path has a value to spoil. */
|
|
78
121
|
const FULL: DocObject = (() => {
|
|
@@ -92,6 +135,14 @@ const FULL: DocObject = (() => {
|
|
|
92
135
|
labeller_knows: ["python", "go", "rust"],
|
|
93
136
|
familiarity: true,
|
|
94
137
|
familiarity_question: "How well do you know {group}?",
|
|
138
|
+
labels: {
|
|
139
|
+
place: "C",
|
|
140
|
+
body: "B",
|
|
141
|
+
line: "L",
|
|
142
|
+
line_part: "{line} {part}/{of}",
|
|
143
|
+
repeat: "R",
|
|
144
|
+
practice_example: "P",
|
|
145
|
+
},
|
|
95
146
|
practice: PRACTICE.map((p, n) => (n === 0 ? { ...p, ...CARD } : p)),
|
|
96
147
|
items: ITEMS.map((i, n) => (n === 0 ? { ...i, ...CARD } : i)),
|
|
97
148
|
}),
|
|
@@ -187,6 +238,7 @@ function spoil(path: string, bad: Doc | undefined): DocObject {
|
|
|
187
238
|
|
|
188
239
|
const WRONG_TYPES: Readonly<Record<SchemaNode["kind"], readonly Doc[]>> = {
|
|
189
240
|
string: [null, 1, true, ["x"], {}, "", " "],
|
|
241
|
+
text: [null, 1, true, ["x"], {}],
|
|
190
242
|
integer: [null, "7", true, 7.5, []],
|
|
191
243
|
boolean: [null, "false", 0, [], {}],
|
|
192
244
|
map: [null, "x", ["x"], 1],
|