@patterkit/core 0.5.0 → 0.6.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/index.cjs +60 -3
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +61 -4
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -167,11 +167,44 @@ function validateProject(input) {
|
|
|
167
167
|
const sceneGameIds = /* @__PURE__ */ new Map();
|
|
168
168
|
const jumps = [];
|
|
169
169
|
const castNames = new Set((project.cast ?? []).map((c) => c.name));
|
|
170
|
-
const qualifierIds = new Set((0, import_model.projectQualifiers)(project).
|
|
170
|
+
const qualifierIds = project.qualifiers === void 0 || Array.isArray(project.qualifiers) ? new Set((0, import_model.projectQualifiers)(project).flatMap((q) => isQualifierEntry(q) && typeof q.gameId === "string" ? [q.gameId] : [])) : null;
|
|
171
|
+
const checkPad = (value, what, id) => {
|
|
172
|
+
if (value === void 0) return;
|
|
173
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < import_model.PAD_AFTER_MIN || value > import_model.PAD_AFTER_MAX) {
|
|
174
|
+
issues.push({ code: "invalid-pad", message: `${what} pause '${String(value)}' must be a number of seconds from ${import_model.PAD_AFTER_MIN} to ${import_model.PAD_AFTER_MAX}`, id });
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const checkSeamPad = (beats) => {
|
|
178
|
+
const last = (beats ?? []).at(-1);
|
|
179
|
+
if (!last || last.kind !== "line" && last.kind !== "text" || typeof last.padAfter !== "number" || last.padAfter >= 0) return;
|
|
180
|
+
issues.push({
|
|
181
|
+
code: "pad-overlaps-seam",
|
|
182
|
+
severity: "warning",
|
|
183
|
+
id: last.id,
|
|
184
|
+
message: `beat '${last.id}' is its snippet's last line, so its negative pause can't cut in on what follows; it plays as no pause`
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
const checkPromptPad = (option) => {
|
|
188
|
+
const p = option.prompt;
|
|
189
|
+
if (!p || p.kind !== "line" && p.kind !== "text" || typeof p.padAfter !== "number" || p.padAfter >= 0) return;
|
|
190
|
+
if ((option.selector ?? "run") !== "run" || !Array.isArray(option.children)) return;
|
|
191
|
+
const first = option.children[0];
|
|
192
|
+
if (first && (first.type !== "snippet" || first.condition || (first.beats?.length ?? 0) > 0)) return;
|
|
193
|
+
issues.push({
|
|
194
|
+
code: "prompt-pad-without-beat",
|
|
195
|
+
severity: "warning",
|
|
196
|
+
id: p.id,
|
|
197
|
+
message: `prompt '${p.id}' has a negative pause, but its option plays nothing before it moves on, so nothing cuts in on it`
|
|
198
|
+
});
|
|
199
|
+
};
|
|
171
200
|
const checkQualifier = (beat, what) => {
|
|
172
|
-
if (beat.kind
|
|
201
|
+
if (beat.kind !== "line" || beat.qualifier === void 0) return;
|
|
202
|
+
if (qualifierIds && !qualifierIds.has(beat.qualifier)) {
|
|
173
203
|
issues.push({ code: "unknown-qualifier", message: `${what} '${beat.id}' qualifier '${beat.qualifier}' is not in the project's qualifiers`, id: beat.id });
|
|
174
204
|
}
|
|
205
|
+
if (!beat.character) {
|
|
206
|
+
issues.push({ code: "qualifier-without-speaker", severity: "warning", message: `${what} '${beat.id}' has the qualifier '${beat.qualifier}' but no speaker`, id: beat.id });
|
|
207
|
+
}
|
|
175
208
|
};
|
|
176
209
|
const checkTags = (tags, where, id) => {
|
|
177
210
|
for (const t of tags ?? []) {
|
|
@@ -197,6 +230,7 @@ function validateProject(input) {
|
|
|
197
230
|
validateProjectFile(project, issues);
|
|
198
231
|
for (const scene of scenes) {
|
|
199
232
|
seeId(scene.id, "scene", "project");
|
|
233
|
+
checkPad(scene.padAfterDefault, `scene '${scene.id}' default`, scene.id);
|
|
200
234
|
const where = `scene '${scene.id}'`;
|
|
201
235
|
if (typeof scene.id === "string") addressable.add(scene.id);
|
|
202
236
|
if (!scene.name?.trim()) {
|
|
@@ -232,6 +266,7 @@ function validateProject(input) {
|
|
|
232
266
|
}
|
|
233
267
|
for (const block of scene.blocks) {
|
|
234
268
|
seeId(block.id, "block", where);
|
|
269
|
+
checkPad(block.padAfterDefault, `block '${block.id}' default`, block.id);
|
|
235
270
|
checkTags(block.tags, `block '${block.id}'`, block.id);
|
|
236
271
|
if (typeof block.id === "string") addressable.add(block.id);
|
|
237
272
|
if (!block.name?.trim()) {
|
|
@@ -272,6 +307,12 @@ function validateProject(input) {
|
|
|
272
307
|
(0, import_model.walkNodes)(block.children, (node) => {
|
|
273
308
|
const inBlock = `block '${block.id}'`;
|
|
274
309
|
checkTags(node.tags, `${node.type} '${node.id}'`, node.id);
|
|
310
|
+
checkPad(node.padAfterDefault, `${node.type} '${node.id}' default`, node.id);
|
|
311
|
+
if (node.type === "group" && node.prompt) checkPad(node.prompt.padAfter, `prompt '${node.prompt.id}'`, node.prompt.id);
|
|
312
|
+
if (node.type === "snippet") {
|
|
313
|
+
for (const beat of node.beats ?? []) if (beat.kind !== "gameEvent") checkPad(beat.padAfter, `beat '${beat.id}'`, beat.id);
|
|
314
|
+
checkSeamPad(node.beats);
|
|
315
|
+
}
|
|
275
316
|
if (node.type === "group") {
|
|
276
317
|
seeId(node.id, "group", inBlock);
|
|
277
318
|
if (!Array.isArray(node.children)) {
|
|
@@ -297,6 +338,7 @@ function validateProject(input) {
|
|
|
297
338
|
issues.push({ code: "unknown-character", message: `prompt '${p.id}' speaker '${p.character}' is not in the project cast`, id: p.id });
|
|
298
339
|
}
|
|
299
340
|
checkQualifier(p, "prompt");
|
|
341
|
+
checkPromptPad(opt);
|
|
300
342
|
}
|
|
301
343
|
}
|
|
302
344
|
if (fallbacks > 1) {
|
|
@@ -348,6 +390,9 @@ function validateProject(input) {
|
|
|
348
390
|
}
|
|
349
391
|
return issues;
|
|
350
392
|
}
|
|
393
|
+
function isQualifierEntry(q) {
|
|
394
|
+
return typeof q === "object" && q !== null && !Array.isArray(q);
|
|
395
|
+
}
|
|
351
396
|
function validateProjectFile(project, issues) {
|
|
352
397
|
checkDecls(project.properties, "project properties", issues);
|
|
353
398
|
for (const scope of project.scopeRegistry?.scopes ?? []) {
|
|
@@ -399,8 +444,20 @@ function validateProjectFile(project, issues) {
|
|
|
399
444
|
if (project.recordingStatuses) {
|
|
400
445
|
checkLadderNames(project.recordingStatuses.map((s) => s.name), "recordingStatuses", issues);
|
|
401
446
|
}
|
|
447
|
+
const pad = project.padAfterDefault;
|
|
448
|
+
if (pad !== void 0 && (typeof pad !== "number" || !Number.isFinite(pad) || pad < import_model.PAD_AFTER_MIN || pad > import_model.PAD_AFTER_MAX)) {
|
|
449
|
+
issues.push({ code: "invalid-pad", message: `the project default pause '${String(pad)}' must be a number of seconds from ${import_model.PAD_AFTER_MIN} to ${import_model.PAD_AFTER_MAX}` });
|
|
450
|
+
}
|
|
402
451
|
const seenQualifiers = /* @__PURE__ */ new Set();
|
|
403
|
-
|
|
452
|
+
const qualifiers = project.qualifiers;
|
|
453
|
+
if (qualifiers !== void 0 && !Array.isArray(qualifiers)) {
|
|
454
|
+
issues.push({ code: "invalid-qualifier", message: "qualifiers must be a list" });
|
|
455
|
+
}
|
|
456
|
+
for (const q of Array.isArray(qualifiers) ? qualifiers : []) {
|
|
457
|
+
if (!isQualifierEntry(q)) {
|
|
458
|
+
issues.push({ code: "invalid-qualifier", message: `qualifier '${String(q)}' is not a qualifier (a name and an address)` });
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
404
461
|
if (typeof q.name !== "string" || !q.name.trim()) {
|
|
405
462
|
issues.push({ code: "invalid-qualifier", message: `qualifier '${String(q.gameId)}' has no name` });
|
|
406
463
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -26,7 +26,7 @@ interface StringifyOptions {
|
|
|
26
26
|
declare function canonicalStringify(value: unknown, opts?: StringifyOptions): string;
|
|
27
27
|
|
|
28
28
|
interface ValidationIssue {
|
|
29
|
-
code: "duplicate-id" | "missing-id" | "malformed-node" | "missing-name" | "empty-scene" | "empty-snippet" | "empty-container" | "dangling-jump" | "jump-into-non-addressable" | "invalid-gameid" | "duplicate-gameid" | "unknown-character" | "unknown-qualifier" | "invalid-qualifier" | "invalid-temporary" | "invalid-declaration" | "invalid-status-ladder" | "invalid-locales" | "invalid-captions" | "missing-prompt" | "invalid-prompt" | "multiple-fallbacks" | "choice-can-empty" | "invalid-status-value" | "unknown-doc-class" | "invalid-tag" | "invalid-gamedata-field" | "jump-cycle";
|
|
29
|
+
code: "duplicate-id" | "missing-id" | "malformed-node" | "missing-name" | "empty-scene" | "empty-snippet" | "empty-container" | "dangling-jump" | "jump-into-non-addressable" | "invalid-gameid" | "duplicate-gameid" | "unknown-character" | "unknown-qualifier" | "invalid-qualifier" | "invalid-pad" | "pad-overlaps-seam" | "prompt-pad-without-beat" | "qualifier-without-speaker" | "invalid-temporary" | "invalid-declaration" | "invalid-status-ladder" | "invalid-locales" | "invalid-captions" | "missing-prompt" | "invalid-prompt" | "multiple-fallbacks" | "choice-can-empty" | "invalid-status-value" | "unknown-doc-class" | "invalid-tag" | "invalid-gamedata-field" | "jump-cycle";
|
|
30
30
|
message: string;
|
|
31
31
|
/** Id of the offending node/beat, where applicable. */
|
|
32
32
|
id?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ interface StringifyOptions {
|
|
|
26
26
|
declare function canonicalStringify(value: unknown, opts?: StringifyOptions): string;
|
|
27
27
|
|
|
28
28
|
interface ValidationIssue {
|
|
29
|
-
code: "duplicate-id" | "missing-id" | "malformed-node" | "missing-name" | "empty-scene" | "empty-snippet" | "empty-container" | "dangling-jump" | "jump-into-non-addressable" | "invalid-gameid" | "duplicate-gameid" | "unknown-character" | "unknown-qualifier" | "invalid-qualifier" | "invalid-temporary" | "invalid-declaration" | "invalid-status-ladder" | "invalid-locales" | "invalid-captions" | "missing-prompt" | "invalid-prompt" | "multiple-fallbacks" | "choice-can-empty" | "invalid-status-value" | "unknown-doc-class" | "invalid-tag" | "invalid-gamedata-field" | "jump-cycle";
|
|
29
|
+
code: "duplicate-id" | "missing-id" | "malformed-node" | "missing-name" | "empty-scene" | "empty-snippet" | "empty-container" | "dangling-jump" | "jump-into-non-addressable" | "invalid-gameid" | "duplicate-gameid" | "unknown-character" | "unknown-qualifier" | "invalid-qualifier" | "invalid-pad" | "pad-overlaps-seam" | "prompt-pad-without-beat" | "qualifier-without-speaker" | "invalid-temporary" | "invalid-declaration" | "invalid-status-ladder" | "invalid-locales" | "invalid-captions" | "missing-prompt" | "invalid-prompt" | "multiple-fallbacks" | "choice-can-empty" | "invalid-status-value" | "unknown-doc-class" | "invalid-tag" | "invalid-gamedata-field" | "jump-cycle";
|
|
30
30
|
message: string;
|
|
31
31
|
/** Id of the offending node/beat, where applicable. */
|
|
32
32
|
id?: string;
|
package/dist/index.js
CHANGED
|
@@ -55,7 +55,7 @@ ${indent}}`;
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
// src/validate.ts
|
|
58
|
-
import { walkNodes, DEFAULT_WRITING_STATUSES, DEFAULT_RECORDING_STATUSES, DEFAULT_DOCUMENTATION_CLASSES, projectQualifiers } from "@patterkit/model";
|
|
58
|
+
import { walkNodes, DEFAULT_WRITING_STATUSES, DEFAULT_RECORDING_STATUSES, DEFAULT_DOCUMENTATION_CLASSES, projectQualifiers, PAD_AFTER_MIN, PAD_AFTER_MAX } from "@patterkit/model";
|
|
59
59
|
import { isValidGameId, effectiveGameId } from "@patterkit/model";
|
|
60
60
|
import { isValidPropertyName, propertyNameify, RESERVED_PROPERTY_NAMES } from "@patterkit/model";
|
|
61
61
|
function choiceLoopsBack(choice, ...targets) {
|
|
@@ -129,11 +129,44 @@ function validateProject(input) {
|
|
|
129
129
|
const sceneGameIds = /* @__PURE__ */ new Map();
|
|
130
130
|
const jumps = [];
|
|
131
131
|
const castNames = new Set((project.cast ?? []).map((c) => c.name));
|
|
132
|
-
const qualifierIds = new Set(projectQualifiers(project).
|
|
132
|
+
const qualifierIds = project.qualifiers === void 0 || Array.isArray(project.qualifiers) ? new Set(projectQualifiers(project).flatMap((q) => isQualifierEntry(q) && typeof q.gameId === "string" ? [q.gameId] : [])) : null;
|
|
133
|
+
const checkPad = (value, what, id) => {
|
|
134
|
+
if (value === void 0) return;
|
|
135
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < PAD_AFTER_MIN || value > PAD_AFTER_MAX) {
|
|
136
|
+
issues.push({ code: "invalid-pad", message: `${what} pause '${String(value)}' must be a number of seconds from ${PAD_AFTER_MIN} to ${PAD_AFTER_MAX}`, id });
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const checkSeamPad = (beats) => {
|
|
140
|
+
const last = (beats ?? []).at(-1);
|
|
141
|
+
if (!last || last.kind !== "line" && last.kind !== "text" || typeof last.padAfter !== "number" || last.padAfter >= 0) return;
|
|
142
|
+
issues.push({
|
|
143
|
+
code: "pad-overlaps-seam",
|
|
144
|
+
severity: "warning",
|
|
145
|
+
id: last.id,
|
|
146
|
+
message: `beat '${last.id}' is its snippet's last line, so its negative pause can't cut in on what follows; it plays as no pause`
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
const checkPromptPad = (option) => {
|
|
150
|
+
const p = option.prompt;
|
|
151
|
+
if (!p || p.kind !== "line" && p.kind !== "text" || typeof p.padAfter !== "number" || p.padAfter >= 0) return;
|
|
152
|
+
if ((option.selector ?? "run") !== "run" || !Array.isArray(option.children)) return;
|
|
153
|
+
const first = option.children[0];
|
|
154
|
+
if (first && (first.type !== "snippet" || first.condition || (first.beats?.length ?? 0) > 0)) return;
|
|
155
|
+
issues.push({
|
|
156
|
+
code: "prompt-pad-without-beat",
|
|
157
|
+
severity: "warning",
|
|
158
|
+
id: p.id,
|
|
159
|
+
message: `prompt '${p.id}' has a negative pause, but its option plays nothing before it moves on, so nothing cuts in on it`
|
|
160
|
+
});
|
|
161
|
+
};
|
|
133
162
|
const checkQualifier = (beat, what) => {
|
|
134
|
-
if (beat.kind
|
|
163
|
+
if (beat.kind !== "line" || beat.qualifier === void 0) return;
|
|
164
|
+
if (qualifierIds && !qualifierIds.has(beat.qualifier)) {
|
|
135
165
|
issues.push({ code: "unknown-qualifier", message: `${what} '${beat.id}' qualifier '${beat.qualifier}' is not in the project's qualifiers`, id: beat.id });
|
|
136
166
|
}
|
|
167
|
+
if (!beat.character) {
|
|
168
|
+
issues.push({ code: "qualifier-without-speaker", severity: "warning", message: `${what} '${beat.id}' has the qualifier '${beat.qualifier}' but no speaker`, id: beat.id });
|
|
169
|
+
}
|
|
137
170
|
};
|
|
138
171
|
const checkTags = (tags, where, id) => {
|
|
139
172
|
for (const t of tags ?? []) {
|
|
@@ -159,6 +192,7 @@ function validateProject(input) {
|
|
|
159
192
|
validateProjectFile(project, issues);
|
|
160
193
|
for (const scene of scenes) {
|
|
161
194
|
seeId(scene.id, "scene", "project");
|
|
195
|
+
checkPad(scene.padAfterDefault, `scene '${scene.id}' default`, scene.id);
|
|
162
196
|
const where = `scene '${scene.id}'`;
|
|
163
197
|
if (typeof scene.id === "string") addressable.add(scene.id);
|
|
164
198
|
if (!scene.name?.trim()) {
|
|
@@ -194,6 +228,7 @@ function validateProject(input) {
|
|
|
194
228
|
}
|
|
195
229
|
for (const block of scene.blocks) {
|
|
196
230
|
seeId(block.id, "block", where);
|
|
231
|
+
checkPad(block.padAfterDefault, `block '${block.id}' default`, block.id);
|
|
197
232
|
checkTags(block.tags, `block '${block.id}'`, block.id);
|
|
198
233
|
if (typeof block.id === "string") addressable.add(block.id);
|
|
199
234
|
if (!block.name?.trim()) {
|
|
@@ -234,6 +269,12 @@ function validateProject(input) {
|
|
|
234
269
|
walkNodes(block.children, (node) => {
|
|
235
270
|
const inBlock = `block '${block.id}'`;
|
|
236
271
|
checkTags(node.tags, `${node.type} '${node.id}'`, node.id);
|
|
272
|
+
checkPad(node.padAfterDefault, `${node.type} '${node.id}' default`, node.id);
|
|
273
|
+
if (node.type === "group" && node.prompt) checkPad(node.prompt.padAfter, `prompt '${node.prompt.id}'`, node.prompt.id);
|
|
274
|
+
if (node.type === "snippet") {
|
|
275
|
+
for (const beat of node.beats ?? []) if (beat.kind !== "gameEvent") checkPad(beat.padAfter, `beat '${beat.id}'`, beat.id);
|
|
276
|
+
checkSeamPad(node.beats);
|
|
277
|
+
}
|
|
237
278
|
if (node.type === "group") {
|
|
238
279
|
seeId(node.id, "group", inBlock);
|
|
239
280
|
if (!Array.isArray(node.children)) {
|
|
@@ -259,6 +300,7 @@ function validateProject(input) {
|
|
|
259
300
|
issues.push({ code: "unknown-character", message: `prompt '${p.id}' speaker '${p.character}' is not in the project cast`, id: p.id });
|
|
260
301
|
}
|
|
261
302
|
checkQualifier(p, "prompt");
|
|
303
|
+
checkPromptPad(opt);
|
|
262
304
|
}
|
|
263
305
|
}
|
|
264
306
|
if (fallbacks > 1) {
|
|
@@ -310,6 +352,9 @@ function validateProject(input) {
|
|
|
310
352
|
}
|
|
311
353
|
return issues;
|
|
312
354
|
}
|
|
355
|
+
function isQualifierEntry(q) {
|
|
356
|
+
return typeof q === "object" && q !== null && !Array.isArray(q);
|
|
357
|
+
}
|
|
313
358
|
function validateProjectFile(project, issues) {
|
|
314
359
|
checkDecls(project.properties, "project properties", issues);
|
|
315
360
|
for (const scope of project.scopeRegistry?.scopes ?? []) {
|
|
@@ -361,8 +406,20 @@ function validateProjectFile(project, issues) {
|
|
|
361
406
|
if (project.recordingStatuses) {
|
|
362
407
|
checkLadderNames(project.recordingStatuses.map((s) => s.name), "recordingStatuses", issues);
|
|
363
408
|
}
|
|
409
|
+
const pad = project.padAfterDefault;
|
|
410
|
+
if (pad !== void 0 && (typeof pad !== "number" || !Number.isFinite(pad) || pad < PAD_AFTER_MIN || pad > PAD_AFTER_MAX)) {
|
|
411
|
+
issues.push({ code: "invalid-pad", message: `the project default pause '${String(pad)}' must be a number of seconds from ${PAD_AFTER_MIN} to ${PAD_AFTER_MAX}` });
|
|
412
|
+
}
|
|
364
413
|
const seenQualifiers = /* @__PURE__ */ new Set();
|
|
365
|
-
|
|
414
|
+
const qualifiers = project.qualifiers;
|
|
415
|
+
if (qualifiers !== void 0 && !Array.isArray(qualifiers)) {
|
|
416
|
+
issues.push({ code: "invalid-qualifier", message: "qualifiers must be a list" });
|
|
417
|
+
}
|
|
418
|
+
for (const q of Array.isArray(qualifiers) ? qualifiers : []) {
|
|
419
|
+
if (!isQualifierEntry(q)) {
|
|
420
|
+
issues.push({ code: "invalid-qualifier", message: `qualifier '${String(q)}' is not a qualifier (a name and an address)` });
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
366
423
|
if (typeof q.name !== "string" || !q.name.trim()) {
|
|
367
424
|
issues.push({ code: "invalid-qualifier", message: `qualifier '${String(q.gameId)}' has no name` });
|
|
368
425
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patterkit/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Patter core: stable id generation, readable-handle computation, canonical (de)serialisation, and the structural validator.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"build": "tsup src/index.ts --format esm,cjs --dts --clean"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@patterkit/model": "0.
|
|
37
|
+
"@patterkit/model": "0.12.0",
|
|
38
38
|
"@wildwinter/toolkit": "^0.1.0",
|
|
39
39
|
"json5": "^2.2.3"
|
|
40
40
|
}
|