@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 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).map((q) => q.gameId));
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 === "line" && beat.qualifier !== void 0 && !qualifierIds.has(beat.qualifier)) {
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
- for (const q of project.qualifiers ?? []) {
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).map((q) => q.gameId));
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 === "line" && beat.qualifier !== void 0 && !qualifierIds.has(beat.qualifier)) {
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
- for (const q of project.qualifiers ?? []) {
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.5.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.11.0",
37
+ "@patterkit/model": "0.12.0",
38
38
  "@wildwinter/toolkit": "^0.1.0",
39
39
  "json5": "^2.2.3"
40
40
  }