@texuli/spec 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.
@@ -2,6 +2,7 @@ import type { SpecDiagnostic, SpecDiagnosticCode, SpecValidation, SpecWarning }
2
2
  import type { SpecCatalog } from './catalog.js';
3
3
  import { type SpecStateType } from './contract.js';
4
4
  import { type Schema } from './dialect.js';
5
+ import { type SpecSourceDescriptor } from './source.js';
5
6
  export type { SpecDiagnostic, SpecDiagnosticCode, SpecValidation, SpecWarning };
6
7
  /**
7
8
  * Checks one workflow graph against the workflow schema:
@@ -20,6 +21,12 @@ export interface SpecDocuments {
20
21
  workflowSchema?: Schema;
21
22
  /** `validateWorkflowGraph` from `@texuli/spec/workflow`, for hosts that render workflows. */
22
23
  validateWorkflowGraph?: WorkflowGraphValidator;
24
+ /**
25
+ * The host's data sources (spec 0.8.0), as `registry.describe()` from
26
+ * `@texuli/core/data-source` returns them. A `$source` binding is checked
27
+ * against them; without them every binding is refused (`source`).
28
+ */
29
+ sources?: readonly SpecSourceDescriptor[];
23
30
  }
24
31
  /** The workflow schema and its validator, when the host supplied both. */
25
32
  export interface WorkflowSupport {
@@ -58,6 +65,8 @@ export interface TreeOptions {
58
65
  warnings?: SpecWarning[];
59
66
  /** Checks `workflowGraph` positions; without it they fail closed. */
60
67
  workflow?: WorkflowSupport;
68
+ /** The host's data sources; without them `$source` bindings fail closed. */
69
+ sources?: readonly SpecSourceDescriptor[];
61
70
  }
62
71
  /**
63
72
  * Validate a blueprint envelope against the spec alone: the envelope schema,
package/dist/validate.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ACTION_FIELDS, contractRoot, DATA_BINDING_KEY, SPEC_ACTION_OPS, SPEC_LIMITS, SPEC_REGEX, SPEC_STATE_TYPES, STATE_BINDING_KEY, STATE_FIELDS, toDialectSchema, } from "./contract.js";
2
2
  import { isRecord, validateInstance } from "./dialect.js";
3
+ import { checkSourceBinding, isSourceBinding, SOURCE_BINDING_KEY, } from "./source.js";
3
4
  import { checkStateSurface, surfaceInputs } from "./surface.js";
4
5
  const MAX_TREE_DEPTH = 64;
5
6
  const ACTION_KEYS = new Set(Object.values(ACTION_FIELDS).flatMap((f) => f.allowed));
@@ -53,6 +54,7 @@ export function validateBlueprint(input, documents, options = {}) {
53
54
  root: contractRoot(documents.blueprintSchema, workflow?.schema),
54
55
  workflow,
55
56
  warnings,
57
+ ...(documents.sources ? { sources: documents.sources } : {}),
56
58
  }));
57
59
  }
58
60
  return {
@@ -301,6 +303,7 @@ export function validateTree(tree, catalog, options = {}) {
301
303
  insideForm,
302
304
  out,
303
305
  workflow: options.workflow,
306
+ sources: options.sources,
304
307
  });
305
308
  if (!component) {
306
309
  context.fail('unknown-id', `${path}.id`, `"${node.id}" is not in the catalog.`);
@@ -418,6 +421,10 @@ function checkProps(given, bind, component, path, context) {
418
421
  fail('extension-prop', here, `"${name}" is a ${prop.extension} extension (${prop.kind}) and cannot come from data.`);
419
422
  continue;
420
423
  }
424
+ if (isSourceBinding(value)) {
425
+ checkPropSource(value, prop, component, here, context);
426
+ continue;
427
+ }
421
428
  if (isBindingShaped(value)) {
422
429
  checkPropBinding(value, prop, here, context);
423
430
  continue;
@@ -443,6 +450,38 @@ function checkProps(given, bind, component, path, context) {
443
450
  }
444
451
  }
445
452
  }
453
+ /**
454
+ * A whole prop read from a host data source (spec 0.8.0): a sourceable prop,
455
+ * the closed `$defs.sourceBinding` shape, then the binding against the
456
+ * host's sources (fail closed without them).
457
+ */
458
+ function checkPropSource(value, prop, component, path, context) {
459
+ const fail = (code, at, message) => context.fail(code, `${path}${at}`, message);
460
+ if (!(component.sourceable ?? []).includes(prop.name)) {
461
+ fail('source', '', `"${prop.name}" of "${component.id}" cannot read a data source.`);
462
+ return;
463
+ }
464
+ const shape = context.root.$defs?.sourceBinding;
465
+ if (!shape) {
466
+ fail('source', '', 'The blueprint schema has no data-source binding.');
467
+ return;
468
+ }
469
+ const issues = validateInstance(shape, value, context.root);
470
+ const first = issues[0];
471
+ if (first) {
472
+ fail('source', first.path.slice(1), `A data-source binding is ${SHAPE_HINT}: ${first.message}`);
473
+ return;
474
+ }
475
+ const itemSchema = prop.schema?.items;
476
+ const itemFields = itemSchema?.properties ? Object.keys(itemSchema.properties) : undefined;
477
+ for (const issue of checkSourceBinding(value, {
478
+ ...(context.sources ? { sources: context.sources } : {}),
479
+ ...(itemFields ? { itemFields } : {}),
480
+ })) {
481
+ fail(issue.code, issue.at, issue.message);
482
+ }
483
+ }
484
+ const SHAPE_HINT = `{"${SOURCE_BINDING_KEY}": name, "query": {...}, "map": {...}}`;
446
485
  /** Whether `value` is a legal value of `prop` (kind, enum and property schema). */
447
486
  function fits(prop, value, root) {
448
487
  if (kindProblem(prop, value) !== null)
@@ -494,6 +533,10 @@ function walkOpaque(value, path, depth, context) {
494
533
  }
495
534
  if (!isRecord(value))
496
535
  return;
536
+ if (isSourceBinding(value)) {
537
+ context.fail('source', path, 'A data-source binding reads only a whole top-level prop.');
538
+ return;
539
+ }
497
540
  if (isBindingShaped(value)) {
498
541
  nestedBinding(value, path, context, undefined);
499
542
  return;
@@ -577,6 +620,10 @@ function checkPropSchemaValue(name, propSchema, value, path, context) {
577
620
  }
578
621
  return schemaIssues;
579
622
  }
623
+ if (isSourceBinding(current)) {
624
+ sub.fail('source', at(relative), 'A data-source binding reads only a whole top-level prop.');
625
+ return [];
626
+ }
580
627
  if (isBindingShaped(current)) {
581
628
  nestedBinding(current, at(relative), sub, node);
582
629
  return [];
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The semantic version of the spec documents in this package. */
2
- export declare const SPEC_VERSION = "0.7.0";
2
+ export declare const SPEC_VERSION = "0.8.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The semantic version of the spec documents in this package. */
2
- export const SPEC_VERSION = '0.7.0';
2
+ export const SPEC_VERSION = '0.8.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@texuli/spec",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "The schema-first blueprint contract: JSON schemas, the catalog vocabulary, deterministic TypeScript and Kotlin projections, and the conformance harness.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -50,6 +50,10 @@
50
50
  "./stream": {
51
51
  "types": "./dist/stream/index.d.ts",
52
52
  "default": "./dist/stream/index.js"
53
+ },
54
+ "./source": {
55
+ "types": "./dist/source.d.ts",
56
+ "default": "./dist/source.js"
53
57
  }
54
58
  },
55
59
  "files": [