@patterkit/core 0.7.0 → 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 CHANGED
@@ -96,6 +96,7 @@ ${indent}}`;
96
96
  var import_model = require("@patterkit/model");
97
97
  var import_model2 = require("@patterkit/model");
98
98
  var import_model3 = require("@patterkit/model");
99
+ var import_expr = require("@wildwinter/expr");
99
100
  function choiceLoopsBack(choice, ...targets) {
100
101
  const back = new Set(targets.filter((s) => typeof s === "string" && s.length > 0));
101
102
  const hits = (n) => n.type === "snippet" ? !!n.jump && back.has(n.jump.to) : (n.children ?? []).some(hits);
@@ -228,6 +229,7 @@ function validateProject(input) {
228
229
  return true;
229
230
  };
230
231
  validateProjectFile(project, issues);
232
+ validateGameDataValues(project, scenes, issues);
231
233
  for (const scene of scenes) {
232
234
  seeId(scene.id, "scene", "project");
233
235
  checkPad(scene.padAfterDefault, `scene '${scene.id}' default`, scene.id);
@@ -504,12 +506,75 @@ function validateProjectFile(project, issues) {
504
506
  if (!f.name.trim()) issues.push({ code: "invalid-gamedata-field", message: `${kind} gameData field has no name` });
505
507
  else if (seen.has(key)) issues.push({ code: "invalid-gamedata-field", message: `${kind} gameData field '${f.name}' is declared more than once` });
506
508
  else seen.add(key);
509
+ const subject = `${kind} gameData field '${f.name}'`;
507
510
  if (f.type === "enum" && !(f.values && f.values.length)) {
508
- issues.push({ code: "invalid-gamedata-field", message: `${kind} gameData field '${f.name}' is an enum but lists no values` });
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
+ });
509
524
  }
510
525
  }
511
526
  }
512
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
+ }
513
578
  function validateAuthoring(files, project, allIds, issues) {
514
579
  const writingLadder = new Set((project.writingStatuses ?? import_model.DEFAULT_WRITING_STATUSES).map((s) => s.name));
515
580
  const recordingLadder = new Set((project.recordingStatuses ?? import_model.DEFAULT_RECORDING_STATUSES).map((s) => s.name));
@@ -601,21 +666,24 @@ function checkDecls(decls, where, issues) {
601
666
  });
602
667
  }
603
668
  if (decl.type === "quality") {
604
- const stages = decl.stages ?? [];
605
- if (stages.length < 2) {
606
- issues.push({
607
- code: "invalid-declaration",
608
- message: `${where}: '${decl.name}' is a quality but declares ${stages.length === 0 ? "no stages" : "only one stage"} (a ladder needs at least two)`
609
- });
610
- }
611
- const seenStages = /* @__PURE__ */ new Set();
612
- for (const s of stages) {
613
- if (!s.trim()) issues.push({ code: "invalid-declaration", message: `${where}: '${decl.name}' has an empty stage name` });
614
- else if (seenStages.has(s)) issues.push({ code: "invalid-declaration", message: `${where}: '${decl.name}' declares stage '${s}' more than once` });
615
- else seenStages.add(s);
669
+ for (const problem2 of (0, import_expr.qualityDeclarationProblems)(decl.stages)) {
670
+ switch (problem2.kind) {
671
+ case "too-few-stages":
672
+ issues.push({
673
+ code: "invalid-declaration",
674
+ message: `${where}: '${decl.name}' is a quality but declares ${problem2.count === 0 ? "no stages" : "only one stage"} (a ladder needs at least two)`
675
+ });
676
+ break;
677
+ case "empty-stage":
678
+ issues.push({ code: "invalid-declaration", message: `${where}: '${decl.name}' has an empty stage name` });
679
+ break;
680
+ case "duplicate-stage":
681
+ issues.push({ code: "invalid-declaration", message: `${where}: '${decl.name}' declares stage '${problem2.stage}' more than once` });
682
+ break;
683
+ }
616
684
  }
617
685
  }
618
- if (decl.default !== void 0 && !defaultMatchesType(decl.default, decl)) {
686
+ if (decl.default !== void 0 && !(0, import_expr.valueFits)(decl, decl.default)) {
619
687
  issues.push({
620
688
  code: "invalid-declaration",
621
689
  message: `${where}: '${decl.name}' default ${JSON.stringify(decl.default)} does not match type '${decl.type}'`
@@ -623,22 +691,6 @@ function checkDecls(decls, where, issues) {
623
691
  }
624
692
  }
625
693
  }
626
- function defaultMatchesType(value, decl) {
627
- switch (decl.type) {
628
- case "boolean":
629
- return typeof value === "boolean";
630
- case "number":
631
- return typeof value === "number";
632
- case "string":
633
- return typeof value === "string";
634
- case "enum":
635
- return typeof value === "string" && (decl.values?.includes(value) ?? true);
636
- case "flags":
637
- return Array.isArray(value) && value.every((v) => typeof v === "string" && (decl.values?.includes(v) ?? true));
638
- case "quality":
639
- return typeof value === "string" && (decl.stages?.includes(value) ?? true);
640
- }
641
- }
642
694
 
643
695
  // src/scene-scaffold.ts
644
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,6 +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, stagesEitherSide, valueFits } from "@wildwinter/expr";
61
62
  function choiceLoopsBack(choice, ...targets) {
62
63
  const back = new Set(targets.filter((s) => typeof s === "string" && s.length > 0));
63
64
  const hits = (n) => n.type === "snippet" ? !!n.jump && back.has(n.jump.to) : (n.children ?? []).some(hits);
@@ -190,6 +191,7 @@ function validateProject(input) {
190
191
  return true;
191
192
  };
192
193
  validateProjectFile(project, issues);
194
+ validateGameDataValues(project, scenes, issues);
193
195
  for (const scene of scenes) {
194
196
  seeId(scene.id, "scene", "project");
195
197
  checkPad(scene.padAfterDefault, `scene '${scene.id}' default`, scene.id);
@@ -466,12 +468,75 @@ function validateProjectFile(project, issues) {
466
468
  if (!f.name.trim()) issues.push({ code: "invalid-gamedata-field", message: `${kind} gameData field has no name` });
467
469
  else if (seen.has(key)) issues.push({ code: "invalid-gamedata-field", message: `${kind} gameData field '${f.name}' is declared more than once` });
468
470
  else seen.add(key);
471
+ const subject = `${kind} gameData field '${f.name}'`;
469
472
  if (f.type === "enum" && !(f.values && f.values.length)) {
470
- issues.push({ code: "invalid-gamedata-field", message: `${kind} gameData field '${f.name}' is an enum but lists no values` });
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
+ });
471
486
  }
472
487
  }
473
488
  }
474
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
+ }
475
540
  function validateAuthoring(files, project, allIds, issues) {
476
541
  const writingLadder = new Set((project.writingStatuses ?? DEFAULT_WRITING_STATUSES).map((s) => s.name));
477
542
  const recordingLadder = new Set((project.recordingStatuses ?? DEFAULT_RECORDING_STATUSES).map((s) => s.name));
@@ -563,21 +628,24 @@ function checkDecls(decls, where, issues) {
563
628
  });
564
629
  }
565
630
  if (decl.type === "quality") {
566
- const stages = decl.stages ?? [];
567
- if (stages.length < 2) {
568
- issues.push({
569
- code: "invalid-declaration",
570
- message: `${where}: '${decl.name}' is a quality but declares ${stages.length === 0 ? "no stages" : "only one stage"} (a ladder needs at least two)`
571
- });
572
- }
573
- const seenStages = /* @__PURE__ */ new Set();
574
- for (const s of stages) {
575
- if (!s.trim()) issues.push({ code: "invalid-declaration", message: `${where}: '${decl.name}' has an empty stage name` });
576
- else if (seenStages.has(s)) issues.push({ code: "invalid-declaration", message: `${where}: '${decl.name}' declares stage '${s}' more than once` });
577
- else seenStages.add(s);
631
+ for (const problem2 of qualityDeclarationProblems(decl.stages)) {
632
+ switch (problem2.kind) {
633
+ case "too-few-stages":
634
+ issues.push({
635
+ code: "invalid-declaration",
636
+ message: `${where}: '${decl.name}' is a quality but declares ${problem2.count === 0 ? "no stages" : "only one stage"} (a ladder needs at least two)`
637
+ });
638
+ break;
639
+ case "empty-stage":
640
+ issues.push({ code: "invalid-declaration", message: `${where}: '${decl.name}' has an empty stage name` });
641
+ break;
642
+ case "duplicate-stage":
643
+ issues.push({ code: "invalid-declaration", message: `${where}: '${decl.name}' declares stage '${problem2.stage}' more than once` });
644
+ break;
645
+ }
578
646
  }
579
647
  }
580
- if (decl.default !== void 0 && !defaultMatchesType(decl.default, decl)) {
648
+ if (decl.default !== void 0 && !valueFits(decl, decl.default)) {
581
649
  issues.push({
582
650
  code: "invalid-declaration",
583
651
  message: `${where}: '${decl.name}' default ${JSON.stringify(decl.default)} does not match type '${decl.type}'`
@@ -585,22 +653,6 @@ function checkDecls(decls, where, issues) {
585
653
  }
586
654
  }
587
655
  }
588
- function defaultMatchesType(value, decl) {
589
- switch (decl.type) {
590
- case "boolean":
591
- return typeof value === "boolean";
592
- case "number":
593
- return typeof value === "number";
594
- case "string":
595
- return typeof value === "string";
596
- case "enum":
597
- return typeof value === "string" && (decl.values?.includes(value) ?? true);
598
- case "flags":
599
- return Array.isArray(value) && value.every((v) => typeof v === "string" && (decl.values?.includes(v) ?? true));
600
- case "quality":
601
- return typeof value === "string" && (decl.stages?.includes(value) ?? true);
602
- }
603
- }
604
656
 
605
657
  // src/scene-scaffold.ts
606
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.7.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,7 +34,8 @@
34
34
  "build": "tsup src/index.ts --format esm,cjs --dts --clean"
35
35
  },
36
36
  "dependencies": {
37
- "@patterkit/model": "0.14.0",
37
+ "@patterkit/model": "0.15.0",
38
+ "@wildwinter/expr": "^0.6.0",
38
39
  "@wildwinter/toolkit": "^0.1.0",
39
40
  "json5": "^2.2.3"
40
41
  }