@coldsmirk/abacus-core 0.8.0 → 0.10.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/README.md CHANGED
@@ -9,7 +9,7 @@ Part of [abacus](https://github.com/coldsmirk/abacus). To edit expressions in an
9
9
  - **Evaluate** ZEN expressions against a data context, sync or async.
10
10
  - **Compile** a structured, UI-authored condition tree into a single ZEN boolean expression — and **lift** the canonical form back into a tree for round-trip editing — plus **select** a matching branch by priority.
11
11
  - **Type-analyze** an expression against a variable-context type tree — the inferred type of every span, autocomplete metadata, and positioned diagnostics that power an editor.
12
- - **Project and infer JSON Schema** through a lossless field-tree subset, then turn that tree into an expression variable type.
12
+ - **Project a JSON Schema field tree** a [caliper](https://github.com/coldsmirk/caliper) `SchemaTree` onto an expression variable type.
13
13
  - **Localize** all editor-produced text through a typed, extensible message catalog.
14
14
  - **Strong typing** end to end, dual ESM / CJS, and a single lazily-loaded WebAssembly engine instance.
15
15
 
@@ -19,7 +19,7 @@ Part of [abacus](https://github.com/coldsmirk/abacus). To edit expressions in an
19
19
  pnpm add @coldsmirk/abacus-core
20
20
  ```
21
21
 
22
- The ZEN WebAssembly engine (`@gorules/zen-engine-wasm`) is a regular dependency and installs automatically. Node.js >= 24; browsers need ES2023 support (Chrome/Edge 111+, Safari 16.2+, Firefox 115+) — the bundles ship untranspiled.
22
+ The ZEN WebAssembly engine (`@gorules/zen-engine-wasm`) and the schema-tree model (`@coldsmirk/caliper-core`, whose `SchemaTree` appears in this package's public surface) are regular dependencies and install automatically. Node.js >= 24; browsers need ES2023 support (Chrome/Edge 111+, Safari 16.2+, Firefox 115+) — the bundles ship untranspiled.
23
23
 
24
24
  ## Quick start
25
25
 
@@ -216,46 +216,30 @@ interface ExpressionCompletion {
216
216
 
217
217
  ## JSON Schema field trees
218
218
 
219
- The schema helpers provide the data model behind a visual JSON Schema builder without depending on a UI framework. The tree hosts an object-oriented draft 2020-12 subset: `type`, `properties`, `items`, `required`, and `description`, plus the optional root `$schema` declaration. Parsing refuses every unsupported construct with a structured `SchemaTreeIssue`; it never accepts a keyword and then silently removes it on serialization.
219
+ The field-tree model itself lives in [`@coldsmirk/caliper-core`](https://www.npmjs.com/package/@coldsmirk/caliper-core) `parseSchemaTree`, `serializeSchemaTree`, `inferSchema`, `newSchemaTreeField`, and the `SchemaTree*` types (with `<SchemaBuilder>` / `<JsonBuilder>` in `@coldsmirk/caliper-mantine`). This package owns only the bridge to ZEN: `schemaTreeToExpressionType` turns a `SchemaTree` into the variable type an expression is analyzed against.
220
220
 
221
221
  ```ts
222
- import {
223
- inferSchema,
224
- parseSchemaTree,
225
- schemaTreeToExpressionType,
226
- serializeSchemaTree
227
- } from "@coldsmirk/abacus-core";
228
-
229
- const inferred = inferSchema({
222
+ import { schemaTreeToExpressionType } from "@coldsmirk/abacus-core";
223
+ import { inferSchema, parseSchemaTree } from "@coldsmirk/caliper-core";
224
+
225
+ const parsed = parseSchemaTree(JSON.stringify(inferSchema({
230
226
  amount: 120,
231
227
  customer: { name: "Ada" },
232
228
  tags: ["priority"]
233
- });
234
-
235
- const parsed = parseSchemaTree(JSON.stringify(inferred));
229
+ })));
236
230
 
237
231
  if (parsed.ok) {
238
- serializeSchemaTree(parsed.tree); // pretty-printed schema JSON
239
232
  schemaTreeToExpressionType(parsed.tree); // { Object: { amount: "Number", ... } }
240
- } else {
241
- parsed.issue; // a localizable SchemaTreeIssue
242
233
  }
243
234
  ```
244
235
 
245
236
  ```ts
246
- function inferSchema(sample: Json): Record<string, Json>;
247
- function parseSchemaTree(text: string): SchemaTreeParseResult;
248
- function serializeSchemaTree(tree: SchemaTree): string;
249
237
  function schemaTreeToExpressionType(tree: SchemaTree): ExpressionType;
250
- function newSchemaTreeField(overrides?: Partial<Omit<SchemaTreeField, "id">>): SchemaTreeField;
251
- const SCHEMA_DIALECT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
252
238
  ```
253
239
 
254
- `inferSchema` is intentionally conservative: integer samples infer `number`, no property becomes `required` from one observation, `null` says nothing (`{}`), and heterogeneous arrays merge to the loosest schema the field-tree subset can express.
255
-
256
- The parser preserves JSON Schema applicability, not merely its visible fields. A schema with `properties` or `items` but no `type` still permits instances of other types, so `SchemaTree.explicitType` and an object/array field's `explicitType` remember that omission and serialization does not add an object/array type. Hand-built trees that omit the optional metadata keep the original builder default and emit an explicit type.
240
+ `SchemaTree` is imported from `@coldsmirk/caliper-core` a regular dependency of this package, so the emitted types resolve on install and is deliberately **not** re-exported here: a caller holding a tree already imports the module that produced it.
257
241
 
258
- Property names are arbitrary JSON strings. Empty, whitespace-only, and prototype-like names such as `__proto__` round-trip and enter the generated `ExpressionType`. A parsed blank name carries `preserveBlankName: true`; a blank row from `newSchemaTreeField()` has no marker and remains an unfinished UI draft that is omitted from serialization and expression types. Hosts editing a parsed tree should update nodes immutably (for example, with object spread) so these optional source-preservation fields survive.
242
+ Every named field becomes a typed variable: `string` `String`, `number` / `integer` `Number`, `boolean` `Bool`, `object` a nested `Object`, `array` `Array` (elements `Any` when the source declared no `items`), and an untyped field `Any`. Blank draft rows from `newSchemaTreeField()` are skipped, while blank and prototype-like property names that came from real `properties` keys (carrying caliper's `preserveBlankName`) stay in the scope.
259
243
 
260
244
  ## Compiling conditions
261
245
 
@@ -483,10 +467,10 @@ class ExpressionNotReadyError extends ExpressionError { // a *Sync helper calle
483
467
  - **Templates** — `analyzeTemplate` / `analyzeTemplateSync`, `getTemplateDiagnostics` / `getTemplateDiagnosticsSync`, `parseTemplateHoles`, `templateHoleAt`
484
468
  - **Conditions** — `compileCondition`, `compileGroup`, `compileBranch`, `selectBranch`, `selectBranchWith`, `toZenLiteral`, `isZenRepresentableNumber`, `CONDITION_OPERATORS`, `conditionOperatorArity`
485
469
  - **Condition trees** — `compileConditionTree`, `liftConditionTree`, `emptyConditionGroup`, `newConditionNodeId`, `ensureConditionNodeIds`, `MAX_CONDITION_TREE_DEPTH`, `CONDITION_TREE_OPERATORS`
486
- - **JSON Schema** — `inferSchema`, `parseSchemaTree`, `serializeSchemaTree`, `schemaTreeToExpressionType`, `newSchemaTreeField`, `SCHEMA_DIALECT_2020_12`
470
+ - **JSON Schema** — `schemaTreeToExpressionType` (the model itself is `@coldsmirk/caliper-core`)
487
471
  - **Localization** — `configureExpressionMessages`, `registerExpressionLocale`, `getExpressionMessages`, `subscribeExpressionMessages`, `enMessages`, `zhCNMessages`
488
472
  - **Errors** — `ExpressionError`, `ExpressionNotReadyError`
489
- - **Types** — `ExpressionType`, `ExpressionMode`, `TemplateHole`, `ExpressionContext`, `ExpressionEngine`, `LoadEngineOptions`, `ExpressionAnalysis`, `ExpressionTypeSpan`, `ExpressionCompletion`, `ExpressionDiagnostic`, `ExpressionMessages`, `ExpressionMessagesListener`, `ExpressionLocale`, `BuiltInExpressionLocale`, `ConfigureMessagesOptions`, `ConditionInput`, `FieldConditionInput`, `ExpressionConditionInput`, `ConditionGroupInput`, `ConditionBranchInput`, `ConditionOperator`, `ConditionOperatorArity`, `BranchSelection`, `ConditionTreeGroup`, `ConditionTreeNode`, `ConditionTreeRule`, `ConditionTreeOperator`, `ConditionTreeValue`, `ConditionScalar`, `Json`, `SchemaTree`, `SchemaTreeField`, `SchemaTreeFieldType`, `SchemaTreeIssue`, `SchemaTreeParseResult`
473
+ - **Types** — `ExpressionType`, `ExpressionMode`, `TemplateHole`, `ExpressionContext`, `ExpressionEngine`, `LoadEngineOptions`, `ExpressionAnalysis`, `ExpressionTypeSpan`, `ExpressionCompletion`, `ExpressionDiagnostic`, `ExpressionMessages`, `ExpressionMessagesListener`, `ExpressionLocale`, `BuiltInExpressionLocale`, `ConfigureMessagesOptions`, `ConditionInput`, `FieldConditionInput`, `ExpressionConditionInput`, `ConditionGroupInput`, `ConditionBranchInput`, `ConditionOperator`, `ConditionOperatorArity`, `BranchSelection`, `ConditionTreeGroup`, `ConditionTreeNode`, `ConditionTreeRule`, `ConditionTreeOperator`, `ConditionTreeValue`, `ConditionScalar`
490
474
 
491
475
  ## License
492
476
 
package/dist/index.cjs CHANGED
@@ -1258,6 +1258,23 @@ async function satisfiesType(actual, expected) {
1258
1258
  function satisfiesTypeSync(actual, expected) {
1259
1259
  return getEngineSync().satisfies(actual, expected);
1260
1260
  }
1261
+ function fieldType(field) {
1262
+ switch (field.type) {
1263
+ case "string": return "String";
1264
+ case "number": return "Number";
1265
+ case "integer": return "Number";
1266
+ case "boolean": return "Bool";
1267
+ case "object": return { Object: fieldRecord(field.children) };
1268
+ case "array": return { Array: field.items === null ? "Any" : fieldType(field.items) };
1269
+ case "any": return "Any";
1270
+ }
1271
+ }
1272
+ function fieldRecord(fields) {
1273
+ return Object.fromEntries(fields.filter((field) => field.name.trim() !== "" || field.preserveBlankName === true).map((field) => [field.name, fieldType(field)]));
1274
+ }
1275
+ function schemaTreeToExpressionType(tree) {
1276
+ return { Object: fieldRecord(tree.fields) };
1277
+ }
1261
1278
  const HOLE_PATTERN = /\{\{(?<expression>[^{}]*)\}\}/g;
1262
1279
  function parseTemplateHoles(source) {
1263
1280
  const holes = [];
@@ -1318,301 +1335,11 @@ async function getTemplateDiagnostics(source) {
1318
1335
  await loadEngine();
1319
1336
  return getTemplateDiagnosticsSync(source);
1320
1337
  }
1321
- function fieldType(field) {
1322
- switch (field.type) {
1323
- case "string": return "String";
1324
- case "number": return "Number";
1325
- case "integer": return "Number";
1326
- case "boolean": return "Bool";
1327
- case "object": return { Object: fieldRecord(field.children) };
1328
- case "array": return { Array: field.items === null ? "Any" : fieldType(field.items) };
1329
- case "any": return "Any";
1330
- }
1331
- }
1332
- function fieldRecord(fields) {
1333
- return Object.fromEntries(fields.filter((field) => field.name.trim() !== "" || field.preserveBlankName === true).map((field) => [field.name, fieldType(field)]));
1334
- }
1335
- function schemaTreeToExpressionType(tree) {
1336
- return { Object: fieldRecord(tree.fields) };
1337
- }
1338
- function mergeSchemas(a, b) {
1339
- if (a.type === "object" && b.type === "object") {
1340
- const left = a.properties ?? {};
1341
- const right = b.properties ?? {};
1342
- const entries = [];
1343
- const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
1344
- for (const key of keys) {
1345
- const hasLeft = Object.hasOwn(left, key);
1346
- const hasRight = Object.hasOwn(right, key);
1347
- entries.push([key, hasLeft && hasRight ? mergeSchemas(left[key], right[key]) : hasLeft ? left[key] : right[key]]);
1348
- }
1349
- const properties = Object.fromEntries(entries);
1350
- return Object.keys(properties).length > 0 ? {
1351
- type: "object",
1352
- properties
1353
- } : { type: "object" };
1354
- }
1355
- if (a.type === "array" && b.type === "array") return a.items !== void 0 && b.items !== void 0 ? {
1356
- type: "array",
1357
- items: mergeSchemas(a.items, b.items)
1358
- } : { type: "array" };
1359
- return JSON.stringify(a) === JSON.stringify(b) ? a : {};
1360
- }
1361
- function inferSchema(sample) {
1362
- if (sample === null) return {};
1363
- if (Array.isArray(sample)) {
1364
- if (sample.length === 0) return { type: "array" };
1365
- return {
1366
- type: "array",
1367
- items: sample.map((element) => inferSchema(element)).reduce((left, right) => mergeSchemas(left, right))
1368
- };
1369
- }
1370
- if (typeof sample === "string") return { type: "string" };
1371
- if (typeof sample === "number") return { type: "number" };
1372
- if (typeof sample === "boolean") return { type: "boolean" };
1373
- const properties = Object.fromEntries(Object.entries(sample).map(([key, value]) => [key, inferSchema(value)]));
1374
- return Object.keys(properties).length > 0 ? {
1375
- type: "object",
1376
- properties
1377
- } : { type: "object" };
1378
- }
1379
- const SCHEMA_DIALECT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
1380
- const fieldIdRealm = Math.random().toString(36).slice(2, 7);
1381
- let fieldIdCounter = 0;
1382
- function newSchemaTreeField(overrides = {}) {
1383
- fieldIdCounter += 1;
1384
- return {
1385
- id: `sf-${fieldIdRealm}-${fieldIdCounter}`,
1386
- name: "",
1387
- type: "string",
1388
- required: false,
1389
- description: "",
1390
- children: [],
1391
- items: null,
1392
- ...overrides
1393
- };
1394
- }
1395
- const SCALAR_TYPES = new Set([
1396
- "string",
1397
- "number",
1398
- "integer",
1399
- "boolean"
1400
- ]);
1401
- const ROOT_KEYS = new Set([
1402
- "$schema",
1403
- "type",
1404
- "properties",
1405
- "required",
1406
- "description"
1407
- ]);
1408
- const SUBSCHEMA_KEYS = new Set([
1409
- "type",
1410
- "properties",
1411
- "required",
1412
- "items",
1413
- "description"
1414
- ]);
1415
- var Unsupported = class extends Error {
1416
- issue;
1417
- constructor(issue) {
1418
- super(issue.code);
1419
- this.issue = issue;
1420
- }
1421
- };
1422
- function asPlainObject(value) {
1423
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1424
- }
1425
- function checkKeys(schema, allowed) {
1426
- for (const key of Object.keys(schema)) if (!allowed.has(key)) throw new Unsupported({
1427
- code: "unsupported-keyword",
1428
- keyword: key
1429
- });
1430
- }
1431
- function parseObjectFields(schema) {
1432
- const properties = schema.properties === void 0 ? null : asPlainObject(schema.properties);
1433
- if (schema.properties !== void 0 && properties === null) throw new Unsupported({ code: "invalid-properties" });
1434
- const fields = [];
1435
- const entries = Object.entries(properties ?? {});
1436
- for (const [name, subschema] of entries) fields.push({
1437
- ...parseSubschema(subschema, name),
1438
- name,
1439
- ...name.trim() === "" && { preserveBlankName: true }
1440
- });
1441
- if (schema.required !== void 0) {
1442
- if (!Array.isArray(schema.required)) throw new Unsupported({ code: "invalid-required" });
1443
- for (const entry of schema.required) {
1444
- if (typeof entry !== "string") throw new Unsupported({ code: "invalid-required" });
1445
- const field = fields.find((candidate) => candidate.name === entry);
1446
- if (field === void 0) throw new Unsupported({
1447
- code: "unknown-required-field",
1448
- field: entry
1449
- });
1450
- field.required = true;
1451
- }
1452
- }
1453
- return fields;
1454
- }
1455
- function parseDescription(schema) {
1456
- if (schema.description === void 0) return "";
1457
- if (typeof schema.description !== "string") throw new Unsupported({ code: "invalid-description" });
1458
- return schema.description;
1459
- }
1460
- function ensureAbsent(schema, key, holder) {
1461
- if (schema[key] !== void 0) throw new Unsupported({
1462
- code: "misplaced-keyword",
1463
- keyword: key,
1464
- holder
1465
- });
1466
- }
1467
- function parseSubschema(value, name) {
1468
- const schema = asPlainObject(value);
1469
- if (schema === null) throw new Unsupported({
1470
- code: "invalid-field-definition",
1471
- field: name
1472
- });
1473
- checkKeys(schema, SUBSCHEMA_KEYS);
1474
- const { type } = schema;
1475
- if (type !== void 0 && typeof type !== "string") throw new Unsupported({
1476
- code: "invalid-field-type",
1477
- field: name
1478
- });
1479
- const description = parseDescription(schema);
1480
- if (type === "object" || type === void 0 && (schema.properties !== void 0 || schema.required !== void 0)) {
1481
- ensureAbsent(schema, "items", "array");
1482
- return newSchemaTreeField({
1483
- type: "object",
1484
- description,
1485
- children: parseObjectFields(schema),
1486
- explicitType: type === "object"
1487
- });
1488
- }
1489
- if (type === "array" || type === void 0 && schema.items !== void 0) {
1490
- ensureAbsent(schema, "properties", "object");
1491
- ensureAbsent(schema, "required", "object");
1492
- return newSchemaTreeField({
1493
- type: "array",
1494
- description,
1495
- items: schema.items === void 0 ? null : parseSubschema(schema.items, name),
1496
- explicitType: type === "array"
1497
- });
1498
- }
1499
- if (type === void 0) {
1500
- ensureAbsent(schema, "items", "array");
1501
- return newSchemaTreeField({
1502
- type: "any",
1503
- description
1504
- });
1505
- }
1506
- if (!SCALAR_TYPES.has(type)) throw new Unsupported({
1507
- code: "unsupported-field-type",
1508
- field: name,
1509
- type
1510
- });
1511
- ensureAbsent(schema, "properties", "object");
1512
- ensureAbsent(schema, "required", "object");
1513
- ensureAbsent(schema, "items", "array");
1514
- return newSchemaTreeField({
1515
- type,
1516
- description
1517
- });
1518
- }
1519
- function parseSchemaTree(text) {
1520
- if (text.trim() === "") return {
1521
- ok: true,
1522
- tree: {
1523
- fields: [],
1524
- dialect: false,
1525
- description: ""
1526
- }
1527
- };
1528
- let parsed;
1529
- try {
1530
- parsed = JSON.parse(text);
1531
- } catch {
1532
- return {
1533
- ok: false,
1534
- issue: { code: "invalid-json" }
1535
- };
1536
- }
1537
- try {
1538
- const root = asPlainObject(parsed);
1539
- if (root === null) throw new Unsupported({ code: "root-not-object" });
1540
- checkKeys(root, ROOT_KEYS);
1541
- const dialect = root.$schema !== void 0;
1542
- if (dialect && root.$schema !== "https://json-schema.org/draft/2020-12/schema") throw new Unsupported({ code: "unsupported-dialect" });
1543
- if (root.type !== void 0 && root.type !== "object") throw new Unsupported({ code: "root-not-object" });
1544
- return {
1545
- ok: true,
1546
- tree: {
1547
- fields: parseObjectFields(root),
1548
- dialect,
1549
- description: parseDescription(root),
1550
- explicitType: root.type === "object"
1551
- }
1552
- };
1553
- } catch (error) {
1554
- if (error instanceof Unsupported) return {
1555
- ok: false,
1556
- issue: error.issue
1557
- };
1558
- throw error;
1559
- }
1560
- }
1561
- function serializeObjectBody(fields) {
1562
- const named = fields.filter((field) => field.name.trim() !== "" || field.preserveBlankName === true);
1563
- const finalByName = /* @__PURE__ */ new Map();
1564
- const finalFields = [];
1565
- const body = {};
1566
- for (const field of named) finalByName.set(field.name, field);
1567
- finalByName.forEach((field) => {
1568
- finalFields.push(field);
1569
- });
1570
- if (finalFields.length > 0) body.properties = Object.fromEntries(finalFields.map((field) => [field.name, serializeField(field)]));
1571
- const required = finalFields.filter((field) => field.required).map((field) => field.name);
1572
- if (required.length > 0) body.required = required;
1573
- return body;
1574
- }
1575
- function serializeField(field) {
1576
- const description = field.description === "" ? {} : { description: field.description };
1577
- switch (field.type) {
1578
- case "any": return { ...description };
1579
- case "object": return {
1580
- ...field.explicitType !== false && { type: "object" },
1581
- ...description,
1582
- ...serializeObjectBody(field.children)
1583
- };
1584
- case "array": {
1585
- const type = field.explicitType === false ? {} : { type: "array" };
1586
- return field.items === null || field.items.type === "any" && field.items.description === "" ? {
1587
- ...type,
1588
- ...description
1589
- } : {
1590
- ...type,
1591
- ...description,
1592
- items: serializeField(field.items)
1593
- };
1594
- }
1595
- default: return {
1596
- type: field.type,
1597
- ...description
1598
- };
1599
- }
1600
- }
1601
- function serializeSchemaTree(tree) {
1602
- const root = {
1603
- ...tree.dialect && { $schema: "https://json-schema.org/draft/2020-12/schema" },
1604
- ...tree.explicitType !== false && { type: "object" },
1605
- ...tree.description !== "" && { description: tree.description },
1606
- ...serializeObjectBody(tree.fields)
1607
- };
1608
- return `${JSON.stringify(root, null, 2)}\n`;
1609
- }
1610
1338
  exports.CONDITION_OPERATORS = CONDITION_OPERATORS;
1611
1339
  exports.CONDITION_TREE_OPERATORS = CONDITION_TREE_OPERATORS;
1612
1340
  exports.ExpressionError = ExpressionError;
1613
1341
  exports.ExpressionNotReadyError = ExpressionNotReadyError;
1614
1342
  exports.MAX_CONDITION_TREE_DEPTH = MAX_CONDITION_TREE_DEPTH;
1615
- exports.SCHEMA_DIALECT_2020_12 = SCHEMA_DIALECT_2020_12;
1616
1343
  exports.analyzeTemplate = analyzeTemplate;
1617
1344
  exports.analyzeTemplateSync = analyzeTemplateSync;
1618
1345
  exports.analyzeTypes = analyzeTypes;
@@ -1640,14 +1367,11 @@ exports.getEngineSync = getEngineSync;
1640
1367
  exports.getExpressionMessages = getExpressionMessages;
1641
1368
  exports.getTemplateDiagnostics = getTemplateDiagnostics;
1642
1369
  exports.getTemplateDiagnosticsSync = getTemplateDiagnosticsSync;
1643
- exports.inferSchema = inferSchema;
1644
1370
  exports.isEngineReady = isEngineReady;
1645
1371
  exports.isZenRepresentableNumber = isZenRepresentableNumber;
1646
1372
  exports.liftConditionTree = liftConditionTree;
1647
1373
  exports.loadEngine = loadEngine;
1648
1374
  exports.newConditionNodeId = newConditionNodeId;
1649
- exports.newSchemaTreeField = newSchemaTreeField;
1650
- exports.parseSchemaTree = parseSchemaTree;
1651
1375
  exports.parseTemplateHoles = parseTemplateHoles;
1652
1376
  exports.registerExpressionLocale = registerExpressionLocale;
1653
1377
  exports.resetEngine = resetEngine;
@@ -1656,7 +1380,6 @@ exports.satisfiesTypeSync = satisfiesTypeSync;
1656
1380
  exports.schemaTreeToExpressionType = schemaTreeToExpressionType;
1657
1381
  exports.selectBranch = selectBranch;
1658
1382
  exports.selectBranchWith = selectBranchWith;
1659
- exports.serializeSchemaTree = serializeSchemaTree;
1660
1383
  exports.subscribeExpressionMessages = subscribeExpressionMessages;
1661
1384
  exports.templateHoleAt = templateHoleAt;
1662
1385
  exports.toZenLiteral = toZenLiteral;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { InitInput } from "@gorules/zen-engine-wasm";
2
+ import { SchemaTree } from "@coldsmirk/caliper-core";
2
3
 
3
4
  /**
4
5
  * How an expression editor interprets its document:
@@ -669,6 +670,13 @@ declare function getExpressionMessages(): ExpressionMessages;
669
670
  * function; listeners run synchronously after the new catalog becomes active.
670
671
  */
671
672
  declare function subscribeExpressionMessages(listener: ExpressionMessagesListener): () => void;
673
+ /**
674
+ * The expression scope a schema tree declares: each named field becomes a typed variable, so a
675
+ * host can hand the result straight to `<ExpressionInput variables={…}>` (or `analyzeTypes`)
676
+ * and complete against the declared shape. Blank draft rows are skipped, while blank property
677
+ * names preserved by `@coldsmirk/caliper-core`'s `parseSchemaTree` remain part of the scope.
678
+ */
679
+ declare function schemaTreeToExpressionType(tree: SchemaTree): ExpressionType;
672
680
  /**
673
681
  * One `{{ expression }}` hole located in a template document. `from` / `to` are
674
682
  * the character offsets of the inner expression itself — the text between the
@@ -735,158 +743,4 @@ declare function getTemplateDiagnosticsSync(source: string): ExpressionDiagnosti
735
743
  * Async {@link getTemplateDiagnosticsSync}, loading the engine on first use.
736
744
  */
737
745
  declare function getTemplateDiagnostics(source: string): Promise<ExpressionDiagnostic[]>;
738
- /**
739
- * The field-tree model behind the visual JSON Schema builder: a lossless projection of the
740
- * JSON Schema subset the tree can host (`type` / `properties` / `items`, plus `required`).
741
- * The schema JSON stays the single source of truth: {@link parseSchemaTree} REFUSES anything
742
- * outside the subset (never a lossy rewrite), so a hand-written `oneOf` or `format` keeps its
743
- * document JSON-only instead of silently vanishing on the first visual edit.
744
- */
745
- /**
746
- * The one dialect the tree round-trips: a document may omit `$schema` or declare exactly this.
747
- */
748
- declare const SCHEMA_DIALECT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
749
- type SchemaTreeFieldType = "any" | "array" | "boolean" | "integer" | "number" | "object" | "string";
750
- interface SchemaTreeField {
751
- /**
752
- * Stable row identity for React keys and targeted edits — a UI concern, never serialized.
753
- */
754
- id: string;
755
- /**
756
- * The property name under its parent object; unused on an array-element node.
757
- */
758
- name: string;
759
- type: SchemaTreeFieldType;
760
- /**
761
- * Listed in the parent object's `required`; unused on an array-element node.
762
- */
763
- required: boolean;
764
- /**
765
- * The schema's `description` annotation; blank means none (not serialized).
766
- */
767
- description: string;
768
- /**
769
- * Sub-fields, meaningful when `type` is `object`.
770
- */
771
- children: SchemaTreeField[];
772
- /**
773
- * The element node, meaningful when `type` is `array`; `null` means untyped elements.
774
- */
775
- items: SchemaTreeField | null;
776
- /**
777
- * For an object or array node parsed from JSON, whether its source explicitly
778
- * declared that type. `false` preserves a type-less applicator; omitted keeps the
779
- * historical hand-built-tree default of emitting the type.
780
- */
781
- explicitType?: boolean;
782
- /**
783
- * Whether a blank / whitespace-only name came from a real `properties` key and must be
784
- * persisted. Omitted blank names are unfinished UI rows and do not serialize.
785
- */
786
- preserveBlankName?: boolean;
787
- }
788
- interface SchemaTree {
789
- fields: SchemaTreeField[];
790
- /**
791
- * Whether the source declared `$schema` (re-emitted verbatim on serialize).
792
- */
793
- dialect: boolean;
794
- /**
795
- * The root schema's `description` annotation, preserved verbatim across the round trip
796
- * (the field view offers no root-level editor); blank means none.
797
- */
798
- description: string;
799
- /**
800
- * For a tree parsed from JSON, whether the root explicitly declared `type: "object"`.
801
- * `false` preserves a type-less root; omitted keeps the historical hand-built-tree default
802
- * of emitting the type.
803
- */
804
- explicitType?: boolean;
805
- }
806
- /**
807
- * Why a document cannot be projected onto the field tree — structured so a UI can localize
808
- * the explanation. Every code names the offending part where one exists.
809
- */
810
- type SchemaTreeIssue = {
811
- code: "invalid-description";
812
- } | {
813
- code: "invalid-field-definition";
814
- field: string;
815
- } | {
816
- code: "invalid-field-type";
817
- field: string;
818
- } | {
819
- code: "invalid-json";
820
- } | {
821
- code: "invalid-properties";
822
- } | {
823
- code: "invalid-required";
824
- } | {
825
- code: "misplaced-keyword";
826
- keyword: string;
827
- holder: "array" | "object";
828
- } | {
829
- code: "root-not-object";
830
- } | {
831
- code: "unknown-required-field";
832
- field: string;
833
- } | {
834
- code: "unsupported-dialect";
835
- } | {
836
- code: "unsupported-field-type";
837
- field: string;
838
- type: string;
839
- } | {
840
- code: "unsupported-keyword";
841
- keyword: string;
842
- };
843
- type SchemaTreeParseResult = {
844
- ok: true;
845
- tree: SchemaTree;
846
- } | {
847
- ok: false;
848
- issue: SchemaTreeIssue;
849
- };
850
- /**
851
- * A fresh {@link SchemaTreeField}, defaulting to an optional unnamed string field. The id is a
852
- * tagged counter, not a UUID: field identity is a UI concern with no persistence or
853
- * cross-process meaning — exactly the guarantee a React key needs.
854
- */
855
- declare function newSchemaTreeField(overrides?: Partial<Omit<SchemaTreeField, "id">>): SchemaTreeField;
856
- /**
857
- * The expression scope a schema tree declares: each named field becomes a typed variable, so a
858
- * host can hand the result straight to `<ExpressionInput variables={…}>` (or `analyzeTypes`)
859
- * and complete against the declared shape. Blank draft rows are skipped, while blank property
860
- * names preserved by {@link parseSchemaTree} remain part of the scope.
861
- */
862
- declare function schemaTreeToExpressionType(tree: SchemaTree): ExpressionType;
863
- /**
864
- * Infer a draft 2020-12 JSON Schema from a sample payload — the "generate from example" path of
865
- * the visual schema builder. Deliberately emits only the subset {@link parseSchemaTree} hosts
866
- * (`type` / `properties` / `items`): integers stay `number` (a sample's `5` rarely promises
867
- * integers forever) and nothing is marked `required` (validation strictness is an authoring
868
- * decision, not something a single sample can attest).
869
- */
870
- interface JsonObject {
871
- [key: string]: Json;
872
- }
873
- type Json = null | boolean | number | string | Json[] | JsonObject;
874
- /**
875
- * The schema a sample value attests: scalars map to their type, objects carry every observed
876
- * property, arrays take the merged schema of their elements (an empty or mixed array leaves the
877
- * items open). `null` says nothing about the real type, so it infers `{}` (any).
878
- */
879
- declare function inferSchema(sample: Json): JsonObject;
880
- /**
881
- * Project a schema document onto the field tree, or refuse with a structured
882
- * {@link SchemaTreeIssue} when it uses anything beyond the subset. A blank document parses as
883
- * an empty tree.
884
- */
885
- declare function parseSchemaTree(text: string): SchemaTreeParseResult;
886
- /**
887
- * The field tree back as pretty-printed schema JSON. A parsed type-less object root stays
888
- * type-less; a hand-built tree still emits an object root by default. Clearing the contract
889
- * entirely (an empty document) stays a source-mode action.
890
- */
891
- declare function serializeSchemaTree(tree: SchemaTree): string;
892
- export { type BranchSelection, type BuiltInExpressionLocale, CONDITION_OPERATORS, CONDITION_TREE_OPERATORS, type ConditionBranchInput, type ConditionGroupInput, type ConditionInput, type ConditionOperator, type ConditionOperatorArity, type ConditionScalar, type ConditionTreeGroup, type ConditionTreeNode, type ConditionTreeOperator, type ConditionTreeRule, type ConditionTreeValue, type ConfigureMessagesOptions, type ExpressionAnalysis, type ExpressionCompletion, type ExpressionConditionInput, type ExpressionContext, type ExpressionDiagnostic, type ExpressionEngine, ExpressionError, type ExpressionLocale, type ExpressionMessages, type ExpressionMessagesListener, type ExpressionMode, ExpressionNotReadyError, type ExpressionType, type ExpressionTypeSpan, type FieldConditionInput, type Json, type LoadEngineOptions, MAX_CONDITION_TREE_DEPTH, SCHEMA_DIALECT_2020_12, type SchemaTree, type SchemaTreeField, type SchemaTreeFieldType, type SchemaTreeIssue, type SchemaTreeParseResult, type TemplateHole, analyzeTemplate, analyzeTemplateSync, analyzeTypes, analyzeTypesSync, compileBranch, compileCondition, compileConditionTree, compileGroup, conditionOperatorArity, configureEngine, configureExpressionMessages, emptyConditionGroup, enMessages, ensureConditionNodeIds, evaluate, evaluateSync, evaluateUnary, evaluateUnarySync, getCompletionItems, getCompletionItemsSync, getDiagnostics, getDiagnosticsSync, getEngineError, getEngineSync, getExpressionMessages, getTemplateDiagnostics, getTemplateDiagnosticsSync, inferSchema, isEngineReady, isZenRepresentableNumber, liftConditionTree, loadEngine, newConditionNodeId, newSchemaTreeField, parseSchemaTree, parseTemplateHoles, registerExpressionLocale, resetEngine, satisfiesType, satisfiesTypeSync, schemaTreeToExpressionType, selectBranch, selectBranchWith, serializeSchemaTree, subscribeExpressionMessages, templateHoleAt, toZenLiteral, zhCNMessages };
746
+ export { type BranchSelection, type BuiltInExpressionLocale, CONDITION_OPERATORS, CONDITION_TREE_OPERATORS, type ConditionBranchInput, type ConditionGroupInput, type ConditionInput, type ConditionOperator, type ConditionOperatorArity, type ConditionScalar, type ConditionTreeGroup, type ConditionTreeNode, type ConditionTreeOperator, type ConditionTreeRule, type ConditionTreeValue, type ConfigureMessagesOptions, type ExpressionAnalysis, type ExpressionCompletion, type ExpressionConditionInput, type ExpressionContext, type ExpressionDiagnostic, type ExpressionEngine, ExpressionError, type ExpressionLocale, type ExpressionMessages, type ExpressionMessagesListener, type ExpressionMode, ExpressionNotReadyError, type ExpressionType, type ExpressionTypeSpan, type FieldConditionInput, type LoadEngineOptions, MAX_CONDITION_TREE_DEPTH, type TemplateHole, analyzeTemplate, analyzeTemplateSync, analyzeTypes, analyzeTypesSync, compileBranch, compileCondition, compileConditionTree, compileGroup, conditionOperatorArity, configureEngine, configureExpressionMessages, emptyConditionGroup, enMessages, ensureConditionNodeIds, evaluate, evaluateSync, evaluateUnary, evaluateUnarySync, getCompletionItems, getCompletionItemsSync, getDiagnostics, getDiagnosticsSync, getEngineError, getEngineSync, getExpressionMessages, getTemplateDiagnostics, getTemplateDiagnosticsSync, isEngineReady, isZenRepresentableNumber, liftConditionTree, loadEngine, newConditionNodeId, parseTemplateHoles, registerExpressionLocale, resetEngine, satisfiesType, satisfiesTypeSync, schemaTreeToExpressionType, selectBranch, selectBranchWith, subscribeExpressionMessages, templateHoleAt, toZenLiteral, zhCNMessages };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { InitInput } from "@gorules/zen-engine-wasm";
2
+ import { SchemaTree } from "@coldsmirk/caliper-core";
2
3
 
3
4
  /**
4
5
  * How an expression editor interprets its document:
@@ -669,6 +670,13 @@ declare function getExpressionMessages(): ExpressionMessages;
669
670
  * function; listeners run synchronously after the new catalog becomes active.
670
671
  */
671
672
  declare function subscribeExpressionMessages(listener: ExpressionMessagesListener): () => void;
673
+ /**
674
+ * The expression scope a schema tree declares: each named field becomes a typed variable, so a
675
+ * host can hand the result straight to `<ExpressionInput variables={…}>` (or `analyzeTypes`)
676
+ * and complete against the declared shape. Blank draft rows are skipped, while blank property
677
+ * names preserved by `@coldsmirk/caliper-core`'s `parseSchemaTree` remain part of the scope.
678
+ */
679
+ declare function schemaTreeToExpressionType(tree: SchemaTree): ExpressionType;
672
680
  /**
673
681
  * One `{{ expression }}` hole located in a template document. `from` / `to` are
674
682
  * the character offsets of the inner expression itself — the text between the
@@ -735,158 +743,4 @@ declare function getTemplateDiagnosticsSync(source: string): ExpressionDiagnosti
735
743
  * Async {@link getTemplateDiagnosticsSync}, loading the engine on first use.
736
744
  */
737
745
  declare function getTemplateDiagnostics(source: string): Promise<ExpressionDiagnostic[]>;
738
- /**
739
- * The field-tree model behind the visual JSON Schema builder: a lossless projection of the
740
- * JSON Schema subset the tree can host (`type` / `properties` / `items`, plus `required`).
741
- * The schema JSON stays the single source of truth: {@link parseSchemaTree} REFUSES anything
742
- * outside the subset (never a lossy rewrite), so a hand-written `oneOf` or `format` keeps its
743
- * document JSON-only instead of silently vanishing on the first visual edit.
744
- */
745
- /**
746
- * The one dialect the tree round-trips: a document may omit `$schema` or declare exactly this.
747
- */
748
- declare const SCHEMA_DIALECT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
749
- type SchemaTreeFieldType = "any" | "array" | "boolean" | "integer" | "number" | "object" | "string";
750
- interface SchemaTreeField {
751
- /**
752
- * Stable row identity for React keys and targeted edits — a UI concern, never serialized.
753
- */
754
- id: string;
755
- /**
756
- * The property name under its parent object; unused on an array-element node.
757
- */
758
- name: string;
759
- type: SchemaTreeFieldType;
760
- /**
761
- * Listed in the parent object's `required`; unused on an array-element node.
762
- */
763
- required: boolean;
764
- /**
765
- * The schema's `description` annotation; blank means none (not serialized).
766
- */
767
- description: string;
768
- /**
769
- * Sub-fields, meaningful when `type` is `object`.
770
- */
771
- children: SchemaTreeField[];
772
- /**
773
- * The element node, meaningful when `type` is `array`; `null` means untyped elements.
774
- */
775
- items: SchemaTreeField | null;
776
- /**
777
- * For an object or array node parsed from JSON, whether its source explicitly
778
- * declared that type. `false` preserves a type-less applicator; omitted keeps the
779
- * historical hand-built-tree default of emitting the type.
780
- */
781
- explicitType?: boolean;
782
- /**
783
- * Whether a blank / whitespace-only name came from a real `properties` key and must be
784
- * persisted. Omitted blank names are unfinished UI rows and do not serialize.
785
- */
786
- preserveBlankName?: boolean;
787
- }
788
- interface SchemaTree {
789
- fields: SchemaTreeField[];
790
- /**
791
- * Whether the source declared `$schema` (re-emitted verbatim on serialize).
792
- */
793
- dialect: boolean;
794
- /**
795
- * The root schema's `description` annotation, preserved verbatim across the round trip
796
- * (the field view offers no root-level editor); blank means none.
797
- */
798
- description: string;
799
- /**
800
- * For a tree parsed from JSON, whether the root explicitly declared `type: "object"`.
801
- * `false` preserves a type-less root; omitted keeps the historical hand-built-tree default
802
- * of emitting the type.
803
- */
804
- explicitType?: boolean;
805
- }
806
- /**
807
- * Why a document cannot be projected onto the field tree — structured so a UI can localize
808
- * the explanation. Every code names the offending part where one exists.
809
- */
810
- type SchemaTreeIssue = {
811
- code: "invalid-description";
812
- } | {
813
- code: "invalid-field-definition";
814
- field: string;
815
- } | {
816
- code: "invalid-field-type";
817
- field: string;
818
- } | {
819
- code: "invalid-json";
820
- } | {
821
- code: "invalid-properties";
822
- } | {
823
- code: "invalid-required";
824
- } | {
825
- code: "misplaced-keyword";
826
- keyword: string;
827
- holder: "array" | "object";
828
- } | {
829
- code: "root-not-object";
830
- } | {
831
- code: "unknown-required-field";
832
- field: string;
833
- } | {
834
- code: "unsupported-dialect";
835
- } | {
836
- code: "unsupported-field-type";
837
- field: string;
838
- type: string;
839
- } | {
840
- code: "unsupported-keyword";
841
- keyword: string;
842
- };
843
- type SchemaTreeParseResult = {
844
- ok: true;
845
- tree: SchemaTree;
846
- } | {
847
- ok: false;
848
- issue: SchemaTreeIssue;
849
- };
850
- /**
851
- * A fresh {@link SchemaTreeField}, defaulting to an optional unnamed string field. The id is a
852
- * tagged counter, not a UUID: field identity is a UI concern with no persistence or
853
- * cross-process meaning — exactly the guarantee a React key needs.
854
- */
855
- declare function newSchemaTreeField(overrides?: Partial<Omit<SchemaTreeField, "id">>): SchemaTreeField;
856
- /**
857
- * The expression scope a schema tree declares: each named field becomes a typed variable, so a
858
- * host can hand the result straight to `<ExpressionInput variables={…}>` (or `analyzeTypes`)
859
- * and complete against the declared shape. Blank draft rows are skipped, while blank property
860
- * names preserved by {@link parseSchemaTree} remain part of the scope.
861
- */
862
- declare function schemaTreeToExpressionType(tree: SchemaTree): ExpressionType;
863
- /**
864
- * Infer a draft 2020-12 JSON Schema from a sample payload — the "generate from example" path of
865
- * the visual schema builder. Deliberately emits only the subset {@link parseSchemaTree} hosts
866
- * (`type` / `properties` / `items`): integers stay `number` (a sample's `5` rarely promises
867
- * integers forever) and nothing is marked `required` (validation strictness is an authoring
868
- * decision, not something a single sample can attest).
869
- */
870
- interface JsonObject {
871
- [key: string]: Json;
872
- }
873
- type Json = null | boolean | number | string | Json[] | JsonObject;
874
- /**
875
- * The schema a sample value attests: scalars map to their type, objects carry every observed
876
- * property, arrays take the merged schema of their elements (an empty or mixed array leaves the
877
- * items open). `null` says nothing about the real type, so it infers `{}` (any).
878
- */
879
- declare function inferSchema(sample: Json): JsonObject;
880
- /**
881
- * Project a schema document onto the field tree, or refuse with a structured
882
- * {@link SchemaTreeIssue} when it uses anything beyond the subset. A blank document parses as
883
- * an empty tree.
884
- */
885
- declare function parseSchemaTree(text: string): SchemaTreeParseResult;
886
- /**
887
- * The field tree back as pretty-printed schema JSON. A parsed type-less object root stays
888
- * type-less; a hand-built tree still emits an object root by default. Clearing the contract
889
- * entirely (an empty document) stays a source-mode action.
890
- */
891
- declare function serializeSchemaTree(tree: SchemaTree): string;
892
- export { type BranchSelection, type BuiltInExpressionLocale, CONDITION_OPERATORS, CONDITION_TREE_OPERATORS, type ConditionBranchInput, type ConditionGroupInput, type ConditionInput, type ConditionOperator, type ConditionOperatorArity, type ConditionScalar, type ConditionTreeGroup, type ConditionTreeNode, type ConditionTreeOperator, type ConditionTreeRule, type ConditionTreeValue, type ConfigureMessagesOptions, type ExpressionAnalysis, type ExpressionCompletion, type ExpressionConditionInput, type ExpressionContext, type ExpressionDiagnostic, type ExpressionEngine, ExpressionError, type ExpressionLocale, type ExpressionMessages, type ExpressionMessagesListener, type ExpressionMode, ExpressionNotReadyError, type ExpressionType, type ExpressionTypeSpan, type FieldConditionInput, type Json, type LoadEngineOptions, MAX_CONDITION_TREE_DEPTH, SCHEMA_DIALECT_2020_12, type SchemaTree, type SchemaTreeField, type SchemaTreeFieldType, type SchemaTreeIssue, type SchemaTreeParseResult, type TemplateHole, analyzeTemplate, analyzeTemplateSync, analyzeTypes, analyzeTypesSync, compileBranch, compileCondition, compileConditionTree, compileGroup, conditionOperatorArity, configureEngine, configureExpressionMessages, emptyConditionGroup, enMessages, ensureConditionNodeIds, evaluate, evaluateSync, evaluateUnary, evaluateUnarySync, getCompletionItems, getCompletionItemsSync, getDiagnostics, getDiagnosticsSync, getEngineError, getEngineSync, getExpressionMessages, getTemplateDiagnostics, getTemplateDiagnosticsSync, inferSchema, isEngineReady, isZenRepresentableNumber, liftConditionTree, loadEngine, newConditionNodeId, newSchemaTreeField, parseSchemaTree, parseTemplateHoles, registerExpressionLocale, resetEngine, satisfiesType, satisfiesTypeSync, schemaTreeToExpressionType, selectBranch, selectBranchWith, serializeSchemaTree, subscribeExpressionMessages, templateHoleAt, toZenLiteral, zhCNMessages };
746
+ export { type BranchSelection, type BuiltInExpressionLocale, CONDITION_OPERATORS, CONDITION_TREE_OPERATORS, type ConditionBranchInput, type ConditionGroupInput, type ConditionInput, type ConditionOperator, type ConditionOperatorArity, type ConditionScalar, type ConditionTreeGroup, type ConditionTreeNode, type ConditionTreeOperator, type ConditionTreeRule, type ConditionTreeValue, type ConfigureMessagesOptions, type ExpressionAnalysis, type ExpressionCompletion, type ExpressionConditionInput, type ExpressionContext, type ExpressionDiagnostic, type ExpressionEngine, ExpressionError, type ExpressionLocale, type ExpressionMessages, type ExpressionMessagesListener, type ExpressionMode, ExpressionNotReadyError, type ExpressionType, type ExpressionTypeSpan, type FieldConditionInput, type LoadEngineOptions, MAX_CONDITION_TREE_DEPTH, type TemplateHole, analyzeTemplate, analyzeTemplateSync, analyzeTypes, analyzeTypesSync, compileBranch, compileCondition, compileConditionTree, compileGroup, conditionOperatorArity, configureEngine, configureExpressionMessages, emptyConditionGroup, enMessages, ensureConditionNodeIds, evaluate, evaluateSync, evaluateUnary, evaluateUnarySync, getCompletionItems, getCompletionItemsSync, getDiagnostics, getDiagnosticsSync, getEngineError, getEngineSync, getExpressionMessages, getTemplateDiagnostics, getTemplateDiagnosticsSync, isEngineReady, isZenRepresentableNumber, liftConditionTree, loadEngine, newConditionNodeId, parseTemplateHoles, registerExpressionLocale, resetEngine, satisfiesType, satisfiesTypeSync, schemaTreeToExpressionType, selectBranch, selectBranchWith, subscribeExpressionMessages, templateHoleAt, toZenLiteral, zhCNMessages };
package/dist/index.js CHANGED
@@ -1257,6 +1257,23 @@ async function satisfiesType(actual, expected) {
1257
1257
  function satisfiesTypeSync(actual, expected) {
1258
1258
  return getEngineSync().satisfies(actual, expected);
1259
1259
  }
1260
+ function fieldType(field) {
1261
+ switch (field.type) {
1262
+ case "string": return "String";
1263
+ case "number": return "Number";
1264
+ case "integer": return "Number";
1265
+ case "boolean": return "Bool";
1266
+ case "object": return { Object: fieldRecord(field.children) };
1267
+ case "array": return { Array: field.items === null ? "Any" : fieldType(field.items) };
1268
+ case "any": return "Any";
1269
+ }
1270
+ }
1271
+ function fieldRecord(fields) {
1272
+ return Object.fromEntries(fields.filter((field) => field.name.trim() !== "" || field.preserveBlankName === true).map((field) => [field.name, fieldType(field)]));
1273
+ }
1274
+ function schemaTreeToExpressionType(tree) {
1275
+ return { Object: fieldRecord(tree.fields) };
1276
+ }
1260
1277
  const HOLE_PATTERN = /\{\{(?<expression>[^{}]*)\}\}/g;
1261
1278
  function parseTemplateHoles(source) {
1262
1279
  const holes = [];
@@ -1317,293 +1334,4 @@ async function getTemplateDiagnostics(source) {
1317
1334
  await loadEngine();
1318
1335
  return getTemplateDiagnosticsSync(source);
1319
1336
  }
1320
- function fieldType(field) {
1321
- switch (field.type) {
1322
- case "string": return "String";
1323
- case "number": return "Number";
1324
- case "integer": return "Number";
1325
- case "boolean": return "Bool";
1326
- case "object": return { Object: fieldRecord(field.children) };
1327
- case "array": return { Array: field.items === null ? "Any" : fieldType(field.items) };
1328
- case "any": return "Any";
1329
- }
1330
- }
1331
- function fieldRecord(fields) {
1332
- return Object.fromEntries(fields.filter((field) => field.name.trim() !== "" || field.preserveBlankName === true).map((field) => [field.name, fieldType(field)]));
1333
- }
1334
- function schemaTreeToExpressionType(tree) {
1335
- return { Object: fieldRecord(tree.fields) };
1336
- }
1337
- function mergeSchemas(a, b) {
1338
- if (a.type === "object" && b.type === "object") {
1339
- const left = a.properties ?? {};
1340
- const right = b.properties ?? {};
1341
- const entries = [];
1342
- const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
1343
- for (const key of keys) {
1344
- const hasLeft = Object.hasOwn(left, key);
1345
- const hasRight = Object.hasOwn(right, key);
1346
- entries.push([key, hasLeft && hasRight ? mergeSchemas(left[key], right[key]) : hasLeft ? left[key] : right[key]]);
1347
- }
1348
- const properties = Object.fromEntries(entries);
1349
- return Object.keys(properties).length > 0 ? {
1350
- type: "object",
1351
- properties
1352
- } : { type: "object" };
1353
- }
1354
- if (a.type === "array" && b.type === "array") return a.items !== void 0 && b.items !== void 0 ? {
1355
- type: "array",
1356
- items: mergeSchemas(a.items, b.items)
1357
- } : { type: "array" };
1358
- return JSON.stringify(a) === JSON.stringify(b) ? a : {};
1359
- }
1360
- function inferSchema(sample) {
1361
- if (sample === null) return {};
1362
- if (Array.isArray(sample)) {
1363
- if (sample.length === 0) return { type: "array" };
1364
- return {
1365
- type: "array",
1366
- items: sample.map((element) => inferSchema(element)).reduce((left, right) => mergeSchemas(left, right))
1367
- };
1368
- }
1369
- if (typeof sample === "string") return { type: "string" };
1370
- if (typeof sample === "number") return { type: "number" };
1371
- if (typeof sample === "boolean") return { type: "boolean" };
1372
- const properties = Object.fromEntries(Object.entries(sample).map(([key, value]) => [key, inferSchema(value)]));
1373
- return Object.keys(properties).length > 0 ? {
1374
- type: "object",
1375
- properties
1376
- } : { type: "object" };
1377
- }
1378
- const SCHEMA_DIALECT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
1379
- const fieldIdRealm = Math.random().toString(36).slice(2, 7);
1380
- let fieldIdCounter = 0;
1381
- function newSchemaTreeField(overrides = {}) {
1382
- fieldIdCounter += 1;
1383
- return {
1384
- id: `sf-${fieldIdRealm}-${fieldIdCounter}`,
1385
- name: "",
1386
- type: "string",
1387
- required: false,
1388
- description: "",
1389
- children: [],
1390
- items: null,
1391
- ...overrides
1392
- };
1393
- }
1394
- const SCALAR_TYPES = new Set([
1395
- "string",
1396
- "number",
1397
- "integer",
1398
- "boolean"
1399
- ]);
1400
- const ROOT_KEYS = new Set([
1401
- "$schema",
1402
- "type",
1403
- "properties",
1404
- "required",
1405
- "description"
1406
- ]);
1407
- const SUBSCHEMA_KEYS = new Set([
1408
- "type",
1409
- "properties",
1410
- "required",
1411
- "items",
1412
- "description"
1413
- ]);
1414
- var Unsupported = class extends Error {
1415
- issue;
1416
- constructor(issue) {
1417
- super(issue.code);
1418
- this.issue = issue;
1419
- }
1420
- };
1421
- function asPlainObject(value) {
1422
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1423
- }
1424
- function checkKeys(schema, allowed) {
1425
- for (const key of Object.keys(schema)) if (!allowed.has(key)) throw new Unsupported({
1426
- code: "unsupported-keyword",
1427
- keyword: key
1428
- });
1429
- }
1430
- function parseObjectFields(schema) {
1431
- const properties = schema.properties === void 0 ? null : asPlainObject(schema.properties);
1432
- if (schema.properties !== void 0 && properties === null) throw new Unsupported({ code: "invalid-properties" });
1433
- const fields = [];
1434
- const entries = Object.entries(properties ?? {});
1435
- for (const [name, subschema] of entries) fields.push({
1436
- ...parseSubschema(subschema, name),
1437
- name,
1438
- ...name.trim() === "" && { preserveBlankName: true }
1439
- });
1440
- if (schema.required !== void 0) {
1441
- if (!Array.isArray(schema.required)) throw new Unsupported({ code: "invalid-required" });
1442
- for (const entry of schema.required) {
1443
- if (typeof entry !== "string") throw new Unsupported({ code: "invalid-required" });
1444
- const field = fields.find((candidate) => candidate.name === entry);
1445
- if (field === void 0) throw new Unsupported({
1446
- code: "unknown-required-field",
1447
- field: entry
1448
- });
1449
- field.required = true;
1450
- }
1451
- }
1452
- return fields;
1453
- }
1454
- function parseDescription(schema) {
1455
- if (schema.description === void 0) return "";
1456
- if (typeof schema.description !== "string") throw new Unsupported({ code: "invalid-description" });
1457
- return schema.description;
1458
- }
1459
- function ensureAbsent(schema, key, holder) {
1460
- if (schema[key] !== void 0) throw new Unsupported({
1461
- code: "misplaced-keyword",
1462
- keyword: key,
1463
- holder
1464
- });
1465
- }
1466
- function parseSubschema(value, name) {
1467
- const schema = asPlainObject(value);
1468
- if (schema === null) throw new Unsupported({
1469
- code: "invalid-field-definition",
1470
- field: name
1471
- });
1472
- checkKeys(schema, SUBSCHEMA_KEYS);
1473
- const { type } = schema;
1474
- if (type !== void 0 && typeof type !== "string") throw new Unsupported({
1475
- code: "invalid-field-type",
1476
- field: name
1477
- });
1478
- const description = parseDescription(schema);
1479
- if (type === "object" || type === void 0 && (schema.properties !== void 0 || schema.required !== void 0)) {
1480
- ensureAbsent(schema, "items", "array");
1481
- return newSchemaTreeField({
1482
- type: "object",
1483
- description,
1484
- children: parseObjectFields(schema),
1485
- explicitType: type === "object"
1486
- });
1487
- }
1488
- if (type === "array" || type === void 0 && schema.items !== void 0) {
1489
- ensureAbsent(schema, "properties", "object");
1490
- ensureAbsent(schema, "required", "object");
1491
- return newSchemaTreeField({
1492
- type: "array",
1493
- description,
1494
- items: schema.items === void 0 ? null : parseSubschema(schema.items, name),
1495
- explicitType: type === "array"
1496
- });
1497
- }
1498
- if (type === void 0) {
1499
- ensureAbsent(schema, "items", "array");
1500
- return newSchemaTreeField({
1501
- type: "any",
1502
- description
1503
- });
1504
- }
1505
- if (!SCALAR_TYPES.has(type)) throw new Unsupported({
1506
- code: "unsupported-field-type",
1507
- field: name,
1508
- type
1509
- });
1510
- ensureAbsent(schema, "properties", "object");
1511
- ensureAbsent(schema, "required", "object");
1512
- ensureAbsent(schema, "items", "array");
1513
- return newSchemaTreeField({
1514
- type,
1515
- description
1516
- });
1517
- }
1518
- function parseSchemaTree(text) {
1519
- if (text.trim() === "") return {
1520
- ok: true,
1521
- tree: {
1522
- fields: [],
1523
- dialect: false,
1524
- description: ""
1525
- }
1526
- };
1527
- let parsed;
1528
- try {
1529
- parsed = JSON.parse(text);
1530
- } catch {
1531
- return {
1532
- ok: false,
1533
- issue: { code: "invalid-json" }
1534
- };
1535
- }
1536
- try {
1537
- const root = asPlainObject(parsed);
1538
- if (root === null) throw new Unsupported({ code: "root-not-object" });
1539
- checkKeys(root, ROOT_KEYS);
1540
- const dialect = root.$schema !== void 0;
1541
- if (dialect && root.$schema !== "https://json-schema.org/draft/2020-12/schema") throw new Unsupported({ code: "unsupported-dialect" });
1542
- if (root.type !== void 0 && root.type !== "object") throw new Unsupported({ code: "root-not-object" });
1543
- return {
1544
- ok: true,
1545
- tree: {
1546
- fields: parseObjectFields(root),
1547
- dialect,
1548
- description: parseDescription(root),
1549
- explicitType: root.type === "object"
1550
- }
1551
- };
1552
- } catch (error) {
1553
- if (error instanceof Unsupported) return {
1554
- ok: false,
1555
- issue: error.issue
1556
- };
1557
- throw error;
1558
- }
1559
- }
1560
- function serializeObjectBody(fields) {
1561
- const named = fields.filter((field) => field.name.trim() !== "" || field.preserveBlankName === true);
1562
- const finalByName = /* @__PURE__ */ new Map();
1563
- const finalFields = [];
1564
- const body = {};
1565
- for (const field of named) finalByName.set(field.name, field);
1566
- finalByName.forEach((field) => {
1567
- finalFields.push(field);
1568
- });
1569
- if (finalFields.length > 0) body.properties = Object.fromEntries(finalFields.map((field) => [field.name, serializeField(field)]));
1570
- const required = finalFields.filter((field) => field.required).map((field) => field.name);
1571
- if (required.length > 0) body.required = required;
1572
- return body;
1573
- }
1574
- function serializeField(field) {
1575
- const description = field.description === "" ? {} : { description: field.description };
1576
- switch (field.type) {
1577
- case "any": return { ...description };
1578
- case "object": return {
1579
- ...field.explicitType !== false && { type: "object" },
1580
- ...description,
1581
- ...serializeObjectBody(field.children)
1582
- };
1583
- case "array": {
1584
- const type = field.explicitType === false ? {} : { type: "array" };
1585
- return field.items === null || field.items.type === "any" && field.items.description === "" ? {
1586
- ...type,
1587
- ...description
1588
- } : {
1589
- ...type,
1590
- ...description,
1591
- items: serializeField(field.items)
1592
- };
1593
- }
1594
- default: return {
1595
- type: field.type,
1596
- ...description
1597
- };
1598
- }
1599
- }
1600
- function serializeSchemaTree(tree) {
1601
- const root = {
1602
- ...tree.dialect && { $schema: "https://json-schema.org/draft/2020-12/schema" },
1603
- ...tree.explicitType !== false && { type: "object" },
1604
- ...tree.description !== "" && { description: tree.description },
1605
- ...serializeObjectBody(tree.fields)
1606
- };
1607
- return `${JSON.stringify(root, null, 2)}\n`;
1608
- }
1609
- export { CONDITION_OPERATORS, CONDITION_TREE_OPERATORS, ExpressionError, ExpressionNotReadyError, MAX_CONDITION_TREE_DEPTH, SCHEMA_DIALECT_2020_12, analyzeTemplate, analyzeTemplateSync, analyzeTypes, analyzeTypesSync, compileBranch, compileCondition, compileConditionTree, compileGroup, conditionOperatorArity, configureEngine, configureExpressionMessages, emptyConditionGroup, enMessages, ensureConditionNodeIds, evaluate, evaluateSync, evaluateUnary, evaluateUnarySync, getCompletionItems, getCompletionItemsSync, getDiagnostics, getDiagnosticsSync, getEngineError, getEngineSync, getExpressionMessages, getTemplateDiagnostics, getTemplateDiagnosticsSync, inferSchema, isEngineReady, isZenRepresentableNumber, liftConditionTree, loadEngine, newConditionNodeId, newSchemaTreeField, parseSchemaTree, parseTemplateHoles, registerExpressionLocale, resetEngine, satisfiesType, satisfiesTypeSync, schemaTreeToExpressionType, selectBranch, selectBranchWith, serializeSchemaTree, subscribeExpressionMessages, templateHoleAt, toZenLiteral, zhCNMessages };
1337
+ export { CONDITION_OPERATORS, CONDITION_TREE_OPERATORS, ExpressionError, ExpressionNotReadyError, MAX_CONDITION_TREE_DEPTH, analyzeTemplate, analyzeTemplateSync, analyzeTypes, analyzeTypesSync, compileBranch, compileCondition, compileConditionTree, compileGroup, conditionOperatorArity, configureEngine, configureExpressionMessages, emptyConditionGroup, enMessages, ensureConditionNodeIds, evaluate, evaluateSync, evaluateUnary, evaluateUnarySync, getCompletionItems, getCompletionItemsSync, getDiagnostics, getDiagnosticsSync, getEngineError, getEngineSync, getExpressionMessages, getTemplateDiagnostics, getTemplateDiagnosticsSync, isEngineReady, isZenRepresentableNumber, liftConditionTree, loadEngine, newConditionNodeId, parseTemplateHoles, registerExpressionLocale, resetEngine, satisfiesType, satisfiesTypeSync, schemaTreeToExpressionType, selectBranch, selectBranchWith, subscribeExpressionMessages, templateHoleAt, toZenLiteral, zhCNMessages };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coldsmirk/abacus-core",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Framework-agnostic ZEN expression engine: compile, evaluate, and type-analyze expressions over the GoRules ZEN WASM engine.",
5
5
  "keywords": [
6
6
  "zen",
@@ -43,6 +43,7 @@
43
43
  "dist"
44
44
  ],
45
45
  "dependencies": {
46
+ "@coldsmirk/caliper-core": "^0.1.0",
46
47
  "@gorules/zen-engine-wasm": "^0.23.1"
47
48
  },
48
49
  "engines": {