@patterkit/core 0.7.1 → 0.8.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 +66 -18
- package/dist/index.d.cts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +67 -19
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -229,6 +229,7 @@ function validateProject(input) {
|
|
|
229
229
|
return true;
|
|
230
230
|
};
|
|
231
231
|
validateProjectFile(project, issues);
|
|
232
|
+
validateGameDataValues(project, scenes, issues);
|
|
232
233
|
for (const scene of scenes) {
|
|
233
234
|
seeId(scene.id, "scene", "project");
|
|
234
235
|
checkPad(scene.padAfterDefault, `scene '${scene.id}' default`, scene.id);
|
|
@@ -505,12 +506,75 @@ function validateProjectFile(project, issues) {
|
|
|
505
506
|
if (!f.name.trim()) issues.push({ code: "invalid-gamedata-field", message: `${kind} gameData field has no name` });
|
|
506
507
|
else if (seen.has(key)) issues.push({ code: "invalid-gamedata-field", message: `${kind} gameData field '${f.name}' is declared more than once` });
|
|
507
508
|
else seen.add(key);
|
|
509
|
+
const subject = `${kind} gameData field '${f.name}'`;
|
|
508
510
|
if (f.type === "enum" && !(f.values && f.values.length)) {
|
|
509
|
-
issues.push({ code: "invalid-gamedata-field", message: `${
|
|
511
|
+
issues.push({ code: "invalid-gamedata-field", message: `${subject} is an enum but lists no values` });
|
|
512
|
+
} else if (f.type === "flags" && !(f.values && f.values.length)) {
|
|
513
|
+
issues.push({ code: "invalid-gamedata-field", message: `${subject} is flags but lists no values` });
|
|
514
|
+
}
|
|
515
|
+
if (f.type === "quality") {
|
|
516
|
+
for (const problem of (0, import_expr.qualityDeclarationProblems)(f.stages, f.default)) {
|
|
517
|
+
issues.push({ code: "invalid-gamedata-field", message: `${subject} ${qualityProblemWords(problem)}` });
|
|
518
|
+
}
|
|
519
|
+
} else if (f.default !== void 0 && !(0, import_expr.valueFits)(f, f.default, { noneChosen: true })) {
|
|
520
|
+
issues.push({
|
|
521
|
+
code: "invalid-gamedata-field",
|
|
522
|
+
message: `${subject} default ${JSON.stringify(f.default)} does not match type '${f.type}'`
|
|
523
|
+
});
|
|
510
524
|
}
|
|
511
525
|
}
|
|
512
526
|
}
|
|
513
527
|
}
|
|
528
|
+
function qualityProblemWords(problem) {
|
|
529
|
+
switch (problem.kind) {
|
|
530
|
+
case "too-few-stages":
|
|
531
|
+
return `is a quality but declares ${problem.count === 0 ? "no stages" : "only one stage"} (a ladder needs at least two)`;
|
|
532
|
+
case "empty-stage":
|
|
533
|
+
return "has an empty stage name";
|
|
534
|
+
case "duplicate-stage":
|
|
535
|
+
return `declares stage '${problem.stage}' more than once`;
|
|
536
|
+
case "default-not-a-stage":
|
|
537
|
+
return `default ${JSON.stringify(problem.value)} is not one of its stages`;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function validateGameDataValues(project, scenes, issues) {
|
|
541
|
+
const declared = project.gameDataFields ?? {};
|
|
542
|
+
const check = (kind, id, values) => {
|
|
543
|
+
if (!values) return;
|
|
544
|
+
const fields = declared[kind] ?? [];
|
|
545
|
+
if (fields.length === 0) return;
|
|
546
|
+
for (const [name, value] of Object.entries(values)) {
|
|
547
|
+
const f = fields.find((d) => d.name === name);
|
|
548
|
+
if (!f || (0, import_expr.valueFits)(f, value, { noneChosen: true })) continue;
|
|
549
|
+
const where = `${kind} '${id ?? "?"}'`;
|
|
550
|
+
if (f.type === "quality" && typeof value === "string") {
|
|
551
|
+
issues.push({
|
|
552
|
+
code: "invalid-gamedata-value",
|
|
553
|
+
id,
|
|
554
|
+
message: `${where} gameData '${name}' is '${value}', which is not one of its stages`,
|
|
555
|
+
offLadder: { field: name, bad: value, options: (0, import_expr.stagesEitherSide)(f, value) }
|
|
556
|
+
});
|
|
557
|
+
} else {
|
|
558
|
+
issues.push({
|
|
559
|
+
code: "invalid-gamedata-value",
|
|
560
|
+
id,
|
|
561
|
+
message: `${where} gameData '${name}' ${JSON.stringify(value)} does not match type '${f.type}'`
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
for (const scene of scenes) {
|
|
567
|
+
check("scene", scene.id, scene.gameData);
|
|
568
|
+
for (const block of scene.blocks ?? []) {
|
|
569
|
+
check("block", block.id, block.gameData);
|
|
570
|
+
(0, import_model.walkNodes)(block.children ?? [], (node) => {
|
|
571
|
+
if (node.type !== "snippet") return;
|
|
572
|
+
check("snippet", node.id, node.gameData);
|
|
573
|
+
for (const beat of node.beats ?? []) check(beat.kind, beat.id, beat.gameData);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
514
578
|
function validateAuthoring(files, project, allIds, issues) {
|
|
515
579
|
const writingLadder = new Set((project.writingStatuses ?? import_model.DEFAULT_WRITING_STATUSES).map((s) => s.name));
|
|
516
580
|
const recordingLadder = new Set((project.recordingStatuses ?? import_model.DEFAULT_RECORDING_STATUSES).map((s) => s.name));
|
|
@@ -619,7 +683,7 @@ function checkDecls(decls, where, issues) {
|
|
|
619
683
|
}
|
|
620
684
|
}
|
|
621
685
|
}
|
|
622
|
-
if (decl.default !== void 0 && !
|
|
686
|
+
if (decl.default !== void 0 && !(0, import_expr.valueFits)(decl, decl.default)) {
|
|
623
687
|
issues.push({
|
|
624
688
|
code: "invalid-declaration",
|
|
625
689
|
message: `${where}: '${decl.name}' default ${JSON.stringify(decl.default)} does not match type '${decl.type}'`
|
|
@@ -627,22 +691,6 @@ function checkDecls(decls, where, issues) {
|
|
|
627
691
|
}
|
|
628
692
|
}
|
|
629
693
|
}
|
|
630
|
-
function defaultMatchesType(value, decl) {
|
|
631
|
-
switch (decl.type) {
|
|
632
|
-
case "boolean":
|
|
633
|
-
return typeof value === "boolean";
|
|
634
|
-
case "number":
|
|
635
|
-
return typeof value === "number";
|
|
636
|
-
case "string":
|
|
637
|
-
return typeof value === "string";
|
|
638
|
-
case "enum":
|
|
639
|
-
return typeof value === "string" && (decl.values?.includes(value) ?? true);
|
|
640
|
-
case "flags":
|
|
641
|
-
return Array.isArray(value) && value.every((v) => typeof v === "string" && (decl.values?.includes(v) ?? true));
|
|
642
|
-
case "quality":
|
|
643
|
-
return typeof value === "string" && !(0, import_expr.qualityDeclarationProblems)(decl.stages, value).some((p) => p.kind === "default-not-a-stage");
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
694
|
|
|
647
695
|
// src/scene-scaffold.ts
|
|
648
696
|
var import_model4 = require("@patterkit/model");
|
package/dist/index.d.cts
CHANGED
|
@@ -26,13 +26,20 @@ 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-pad" | "pad-overlaps-seam" | "prompt-pad-without-beat" | "qualifier-without-speaker" | "invalid-speaker-label" | "unused-speaker-label" | "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-speaker-label" | "unused-speaker-label" | "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" | "invalid-gamedata-value" | "jump-cycle";
|
|
30
30
|
message: string;
|
|
31
31
|
/** Id of the offending node/beat, where applicable. */
|
|
32
32
|
id?: string;
|
|
33
33
|
/** Absent means an error: the project should not be built. A warning still compiles and plays (a choice
|
|
34
34
|
* that can run dry gathers or falls through at runtime), and every front end shows it as one. */
|
|
35
35
|
severity?: "warning";
|
|
36
|
+
/** A quality gameData value off its field's ladder (a deleted or renamed stage, most often): the field, the
|
|
37
|
+
* value, and the stages to offer in its place, in ladder order. An editor's quick fix reads it. */
|
|
38
|
+
offLadder?: {
|
|
39
|
+
field: string;
|
|
40
|
+
bad: string;
|
|
41
|
+
options: string[];
|
|
42
|
+
};
|
|
36
43
|
}
|
|
37
44
|
interface ProjectInput {
|
|
38
45
|
project: ProjectFile;
|
package/dist/index.d.ts
CHANGED
|
@@ -26,13 +26,20 @@ 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-pad" | "pad-overlaps-seam" | "prompt-pad-without-beat" | "qualifier-without-speaker" | "invalid-speaker-label" | "unused-speaker-label" | "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-speaker-label" | "unused-speaker-label" | "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" | "invalid-gamedata-value" | "jump-cycle";
|
|
30
30
|
message: string;
|
|
31
31
|
/** Id of the offending node/beat, where applicable. */
|
|
32
32
|
id?: string;
|
|
33
33
|
/** Absent means an error: the project should not be built. A warning still compiles and plays (a choice
|
|
34
34
|
* that can run dry gathers or falls through at runtime), and every front end shows it as one. */
|
|
35
35
|
severity?: "warning";
|
|
36
|
+
/** A quality gameData value off its field's ladder (a deleted or renamed stage, most often): the field, the
|
|
37
|
+
* value, and the stages to offer in its place, in ladder order. An editor's quick fix reads it. */
|
|
38
|
+
offLadder?: {
|
|
39
|
+
field: string;
|
|
40
|
+
bad: string;
|
|
41
|
+
options: string[];
|
|
42
|
+
};
|
|
36
43
|
}
|
|
37
44
|
interface ProjectInput {
|
|
38
45
|
project: ProjectFile;
|
package/dist/index.js
CHANGED
|
@@ -58,7 +58,7 @@ ${indent}}`;
|
|
|
58
58
|
import { walkNodes, DEFAULT_WRITING_STATUSES, DEFAULT_RECORDING_STATUSES, DEFAULT_DOCUMENTATION_CLASSES, projectQualifiers, usedSpeakerPairs, 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
|
-
import { qualityDeclarationProblems } from "@wildwinter/expr";
|
|
61
|
+
import { qualityDeclarationProblems, stagesEitherSide, valueFits } from "@wildwinter/expr";
|
|
62
62
|
function choiceLoopsBack(choice, ...targets) {
|
|
63
63
|
const back = new Set(targets.filter((s) => typeof s === "string" && s.length > 0));
|
|
64
64
|
const hits = (n) => n.type === "snippet" ? !!n.jump && back.has(n.jump.to) : (n.children ?? []).some(hits);
|
|
@@ -191,6 +191,7 @@ function validateProject(input) {
|
|
|
191
191
|
return true;
|
|
192
192
|
};
|
|
193
193
|
validateProjectFile(project, issues);
|
|
194
|
+
validateGameDataValues(project, scenes, issues);
|
|
194
195
|
for (const scene of scenes) {
|
|
195
196
|
seeId(scene.id, "scene", "project");
|
|
196
197
|
checkPad(scene.padAfterDefault, `scene '${scene.id}' default`, scene.id);
|
|
@@ -467,12 +468,75 @@ function validateProjectFile(project, issues) {
|
|
|
467
468
|
if (!f.name.trim()) issues.push({ code: "invalid-gamedata-field", message: `${kind} gameData field has no name` });
|
|
468
469
|
else if (seen.has(key)) issues.push({ code: "invalid-gamedata-field", message: `${kind} gameData field '${f.name}' is declared more than once` });
|
|
469
470
|
else seen.add(key);
|
|
471
|
+
const subject = `${kind} gameData field '${f.name}'`;
|
|
470
472
|
if (f.type === "enum" && !(f.values && f.values.length)) {
|
|
471
|
-
issues.push({ code: "invalid-gamedata-field", message: `${
|
|
473
|
+
issues.push({ code: "invalid-gamedata-field", message: `${subject} is an enum but lists no values` });
|
|
474
|
+
} else if (f.type === "flags" && !(f.values && f.values.length)) {
|
|
475
|
+
issues.push({ code: "invalid-gamedata-field", message: `${subject} is flags but lists no values` });
|
|
476
|
+
}
|
|
477
|
+
if (f.type === "quality") {
|
|
478
|
+
for (const problem of qualityDeclarationProblems(f.stages, f.default)) {
|
|
479
|
+
issues.push({ code: "invalid-gamedata-field", message: `${subject} ${qualityProblemWords(problem)}` });
|
|
480
|
+
}
|
|
481
|
+
} else if (f.default !== void 0 && !valueFits(f, f.default, { noneChosen: true })) {
|
|
482
|
+
issues.push({
|
|
483
|
+
code: "invalid-gamedata-field",
|
|
484
|
+
message: `${subject} default ${JSON.stringify(f.default)} does not match type '${f.type}'`
|
|
485
|
+
});
|
|
472
486
|
}
|
|
473
487
|
}
|
|
474
488
|
}
|
|
475
489
|
}
|
|
490
|
+
function qualityProblemWords(problem) {
|
|
491
|
+
switch (problem.kind) {
|
|
492
|
+
case "too-few-stages":
|
|
493
|
+
return `is a quality but declares ${problem.count === 0 ? "no stages" : "only one stage"} (a ladder needs at least two)`;
|
|
494
|
+
case "empty-stage":
|
|
495
|
+
return "has an empty stage name";
|
|
496
|
+
case "duplicate-stage":
|
|
497
|
+
return `declares stage '${problem.stage}' more than once`;
|
|
498
|
+
case "default-not-a-stage":
|
|
499
|
+
return `default ${JSON.stringify(problem.value)} is not one of its stages`;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function validateGameDataValues(project, scenes, issues) {
|
|
503
|
+
const declared = project.gameDataFields ?? {};
|
|
504
|
+
const check = (kind, id, values) => {
|
|
505
|
+
if (!values) return;
|
|
506
|
+
const fields = declared[kind] ?? [];
|
|
507
|
+
if (fields.length === 0) return;
|
|
508
|
+
for (const [name, value] of Object.entries(values)) {
|
|
509
|
+
const f = fields.find((d) => d.name === name);
|
|
510
|
+
if (!f || valueFits(f, value, { noneChosen: true })) continue;
|
|
511
|
+
const where = `${kind} '${id ?? "?"}'`;
|
|
512
|
+
if (f.type === "quality" && typeof value === "string") {
|
|
513
|
+
issues.push({
|
|
514
|
+
code: "invalid-gamedata-value",
|
|
515
|
+
id,
|
|
516
|
+
message: `${where} gameData '${name}' is '${value}', which is not one of its stages`,
|
|
517
|
+
offLadder: { field: name, bad: value, options: stagesEitherSide(f, value) }
|
|
518
|
+
});
|
|
519
|
+
} else {
|
|
520
|
+
issues.push({
|
|
521
|
+
code: "invalid-gamedata-value",
|
|
522
|
+
id,
|
|
523
|
+
message: `${where} gameData '${name}' ${JSON.stringify(value)} does not match type '${f.type}'`
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
for (const scene of scenes) {
|
|
529
|
+
check("scene", scene.id, scene.gameData);
|
|
530
|
+
for (const block of scene.blocks ?? []) {
|
|
531
|
+
check("block", block.id, block.gameData);
|
|
532
|
+
walkNodes(block.children ?? [], (node) => {
|
|
533
|
+
if (node.type !== "snippet") return;
|
|
534
|
+
check("snippet", node.id, node.gameData);
|
|
535
|
+
for (const beat of node.beats ?? []) check(beat.kind, beat.id, beat.gameData);
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
476
540
|
function validateAuthoring(files, project, allIds, issues) {
|
|
477
541
|
const writingLadder = new Set((project.writingStatuses ?? DEFAULT_WRITING_STATUSES).map((s) => s.name));
|
|
478
542
|
const recordingLadder = new Set((project.recordingStatuses ?? DEFAULT_RECORDING_STATUSES).map((s) => s.name));
|
|
@@ -581,7 +645,7 @@ function checkDecls(decls, where, issues) {
|
|
|
581
645
|
}
|
|
582
646
|
}
|
|
583
647
|
}
|
|
584
|
-
if (decl.default !== void 0 && !
|
|
648
|
+
if (decl.default !== void 0 && !valueFits(decl, decl.default)) {
|
|
585
649
|
issues.push({
|
|
586
650
|
code: "invalid-declaration",
|
|
587
651
|
message: `${where}: '${decl.name}' default ${JSON.stringify(decl.default)} does not match type '${decl.type}'`
|
|
@@ -589,22 +653,6 @@ function checkDecls(decls, where, issues) {
|
|
|
589
653
|
}
|
|
590
654
|
}
|
|
591
655
|
}
|
|
592
|
-
function defaultMatchesType(value, decl) {
|
|
593
|
-
switch (decl.type) {
|
|
594
|
-
case "boolean":
|
|
595
|
-
return typeof value === "boolean";
|
|
596
|
-
case "number":
|
|
597
|
-
return typeof value === "number";
|
|
598
|
-
case "string":
|
|
599
|
-
return typeof value === "string";
|
|
600
|
-
case "enum":
|
|
601
|
-
return typeof value === "string" && (decl.values?.includes(value) ?? true);
|
|
602
|
-
case "flags":
|
|
603
|
-
return Array.isArray(value) && value.every((v) => typeof v === "string" && (decl.values?.includes(v) ?? true));
|
|
604
|
-
case "quality":
|
|
605
|
-
return typeof value === "string" && !qualityDeclarationProblems(decl.stages, value).some((p) => p.kind === "default-not-a-stage");
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
656
|
|
|
609
657
|
// src/scene-scaffold.ts
|
|
610
658
|
import { projectLayout, FLOW_SCHEMA, PROJECT_SCHEMA, STRINGS_SCHEMA } from "@patterkit/model";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patterkit/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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,8 +34,8 @@
|
|
|
34
34
|
"build": "tsup src/index.ts --format esm,cjs --dts --clean"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@patterkit/model": "0.
|
|
38
|
-
"@wildwinter/expr": "^0.
|
|
37
|
+
"@patterkit/model": "0.15.0",
|
|
38
|
+
"@wildwinter/expr": "^0.6.0",
|
|
39
39
|
"@wildwinter/toolkit": "^0.1.0",
|
|
40
40
|
"json5": "^2.2.3"
|
|
41
41
|
}
|