@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 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: `${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
+ });
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 && !defaultMatchesType(decl.default, decl)) {
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: `${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
+ });
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 && !defaultMatchesType(decl.default, decl)) {
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.7.1",
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.14.0",
38
- "@wildwinter/expr": "^0.5.2",
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
  }