@istar-ts/core 0.13.7 → 0.14.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
@@ -176,7 +176,25 @@ const modelIssues = validateModelProperties(model, [resourceSchema]);
176
176
  ```
177
177
 
178
178
  `read` / `validate` accept `customProperties`, a full element/link, or `undefined`. `write` merges
179
- patches, preserves unknown keys, and serializes typed values back to strings.
179
+ patches, preserves unknown keys, and serializes typed values back to strings. A value that
180
+ didn't change keeps its string as read (`.2` stays `.2`).
181
+
182
+ Two optional hints:
183
+
184
+ - `prop.number({ min: 0, max: 1 })`: with `max: 1` (a probability) the reader also accepts a
185
+ percentage, so `0.2`, `.2` and `20.00%` read as 0.2. A changed value is written in the form
186
+ the old one was read in (`20.00%` → `35.00%`, `.2` → `.35`).
187
+ - `prop.reference({ kinds })`: the value is another element's (or link's) id. The schema only
188
+ checks it isn't empty; `validateModelProperties` reports an id that isn't in the model, or
189
+ whose kind isn't in `kinds`.
190
+
191
+ Formulas (a KAOS `FormalSpec`, a `BoundaryCondition`) stay `prop.string`: core has no formula
192
+ language.
193
+
194
+ A schema can be declared for the kinds an n-ary link generates (its hub, legs and end) like for
195
+ any kind; `HubPartKind<'kaos.RefinementLink'>` is `'kaos.RefinementLinkHub' |
196
+ 'kaos.RefinementLinkStart' | 'kaos.RefinementLinkEnd'`. Legs keep their `customProperties` on
197
+ disk.
180
198
 
181
199
  ## Extending the metamodel
182
200
 
package/dist/index.cjs CHANGED
@@ -2500,19 +2500,50 @@ function string(options) {
2500
2500
  }
2501
2501
  };
2502
2502
  }
2503
+ /** `20.00%` → 20.00 as text, or undefined when `raw` isn't a percentage. */
2504
+ function percentBody(raw) {
2505
+ const text = raw.trim();
2506
+ if (!text.endsWith("%")) return void 0;
2507
+ const body = text.slice(0, -1).trim();
2508
+ return body === "" ? void 0 : body;
2509
+ }
2510
+ /** Digits after the decimal point of a number written as text (`20.00` → 2). */
2511
+ function decimals(text) {
2512
+ const dot = text.indexOf(".");
2513
+ return dot === -1 ? 0 : text.length - dot - 1;
2514
+ }
2515
+ /** `value` without floating-point noise from a ×100 (0.35 * 100 = 35.00000000000001). */
2516
+ function plain(value) {
2517
+ return String(Number(value.toPrecision(12)));
2518
+ }
2519
+ /** Writes `value` in the form of `previous`: a percentage, a leading dot, or plain. */
2520
+ function formatNumber(value, previous, percent) {
2521
+ if (previous === void 0) return String(value);
2522
+ const body = percent ? percentBody(previous) : void 0;
2523
+ if (body !== void 0) {
2524
+ const text = plain(value * 100);
2525
+ const places = decimals(body);
2526
+ return `${places > decimals(text) ? (value * 100).toFixed(places) : text}%`;
2527
+ }
2528
+ if (/^\s*[+-]?\.\d/.test(previous) && Math.abs(value) < 1) return String(value).replace(/^(-?)0\./, "$1.");
2529
+ return String(value);
2530
+ }
2503
2531
  function number(options) {
2504
2532
  const opts = options ?? {};
2505
2533
  const integer = options?.integer;
2506
2534
  const min = options?.min;
2507
2535
  const max = options?.max;
2536
+ const percent = max === 1;
2508
2537
  return {
2509
2538
  type: "number",
2510
2539
  options: opts,
2511
2540
  integer,
2512
2541
  min,
2513
2542
  max,
2543
+ ...percent ? { percent } : {},
2514
2544
  parse(raw) {
2515
- const value = Number(raw);
2545
+ const body = percent ? percentBody(raw) : void 0;
2546
+ const value = body !== void 0 ? Number(body) / 100 : Number(raw);
2516
2547
  if (!Number.isFinite(value)) return {
2517
2548
  ok: false,
2518
2549
  error: "must be a finite number"
@@ -2534,8 +2565,8 @@ function number(options) {
2534
2565
  value
2535
2566
  };
2536
2567
  },
2537
- serialize(value) {
2538
- return String(value);
2568
+ serialize(value, previous) {
2569
+ return formatNumber(value, previous, percent);
2539
2570
  }
2540
2571
  };
2541
2572
  }
@@ -2582,11 +2613,34 @@ function enumOf(values, options) {
2582
2613
  }
2583
2614
  };
2584
2615
  }
2616
+ /**
2617
+ * A value that is another element's or link's id, e.g. the goal a KAOS resolution restores. The
2618
+ * schema only checks it is non-empty; `validateModelProperties` reports an id that isn't in the
2619
+ * model or names a kind outside `kinds`.
2620
+ */
2621
+ function reference(options) {
2622
+ const { kinds, ...opts } = options ?? {};
2623
+ return {
2624
+ type: "reference",
2625
+ options: opts,
2626
+ ...kinds ? { kinds } : {},
2627
+ parse(raw) {
2628
+ return {
2629
+ ok: true,
2630
+ value: raw
2631
+ };
2632
+ },
2633
+ serialize(value) {
2634
+ return value;
2635
+ }
2636
+ };
2637
+ }
2585
2638
  const prop = {
2586
2639
  string,
2587
2640
  number,
2588
2641
  boolean,
2589
- enum: enumOf
2642
+ enum: enumOf,
2643
+ reference
2590
2644
  };
2591
2645
  function isCustomPropertiesRecord(source) {
2592
2646
  if ("customProperties" in source) return false;
@@ -2628,7 +2682,7 @@ function readShape(shape, source) {
2628
2682
  }
2629
2683
  issues.push({
2630
2684
  key,
2631
- message: field.type === "boolean" ? "must be true or false" : field.type === "enum" ? `must be one of: ${field.values?.join(", ") ?? ""}` : "must be a finite number"
2685
+ message: field.type === "boolean" ? "must be true or false" : field.type === "enum" ? `must be one of: ${field.values?.join(", ") ?? ""}` : field.type === "reference" ? "must be an element or link id" : "must be a finite number"
2632
2686
  });
2633
2687
  continue;
2634
2688
  }
@@ -2647,6 +2701,15 @@ function readShape(shape, source) {
2647
2701
  issues
2648
2702
  };
2649
2703
  }
2704
+ /**
2705
+ * A patched value as a string, written back as read: the current string is kept when it already
2706
+ * holds the value (`.2` stays `.2`), and otherwise the type writes the value in its form.
2707
+ */
2708
+ function writeField(field, value, current) {
2709
+ const parsed = field.parse(current);
2710
+ if (parsed.ok && Object.is(parsed.value, value)) return current;
2711
+ return field.serialize(value, current);
2712
+ }
2650
2713
  function defineProperties(kind, shape, options) {
2651
2714
  const crossValidate = options?.validate;
2652
2715
  return {
@@ -2668,7 +2731,7 @@ function defineProperties(kind, shape, options) {
2668
2731
  const patched = patch[key];
2669
2732
  if (patched === void 0) continue;
2670
2733
  const field = shape[key];
2671
- if (field) result[key] = field.serialize(patched);
2734
+ if (field) result[key] = writeField(field, patched, value);
2672
2735
  } else result[key] = value;
2673
2736
  seen.add(key);
2674
2737
  }
@@ -2702,20 +2765,39 @@ function validateModelProperties(model, schemas) {
2702
2765
  byKind.set(schema.kind, list);
2703
2766
  }
2704
2767
  const issues = [];
2705
- for (const element of model.elements.values()) {
2706
- const kindSchemas = byKind.get(element.kind);
2707
- if (!kindSchemas) continue;
2708
- for (const schema of kindSchemas) for (const issue of schema.validate(element)) issues.push({
2709
- ...issue,
2710
- id: element.id
2768
+ const check = (owner) => {
2769
+ const kindSchemas = byKind.get(owner.kind);
2770
+ if (!kindSchemas) return;
2771
+ for (const schema of kindSchemas) {
2772
+ for (const issue of schema.validate(owner)) issues.push({
2773
+ ...issue,
2774
+ id: owner.id
2775
+ });
2776
+ for (const issue of referenceIssues(model, schema.shape, owner.customProperties)) issues.push({
2777
+ ...issue,
2778
+ id: owner.id
2779
+ });
2780
+ }
2781
+ };
2782
+ for (const element of model.elements.values()) check(element);
2783
+ for (const link of model.links.values()) check(link);
2784
+ return issues;
2785
+ }
2786
+ /** References (`prop.reference`) to ids that aren't in the model, or to the wrong kind. */
2787
+ function referenceIssues(model, shape, custom) {
2788
+ const issues = [];
2789
+ for (const [key, field] of Object.entries(shape)) {
2790
+ if (field.type !== "reference") continue;
2791
+ const id = custom?.[key];
2792
+ if (id === void 0 || id === "") continue;
2793
+ const target = model.elements.get(id) ?? model.links.get(id);
2794
+ if (!target) issues.push({
2795
+ key,
2796
+ message: `refers to "${id}", which is not in the model`
2711
2797
  });
2712
- }
2713
- for (const link of model.links.values()) {
2714
- const kindSchemas = byKind.get(link.kind);
2715
- if (!kindSchemas) continue;
2716
- for (const schema of kindSchemas) for (const issue of schema.validate(link)) issues.push({
2717
- ...issue,
2718
- id: link.id
2798
+ else if (field.kinds && !field.kinds.includes(target.kind)) issues.push({
2799
+ key,
2800
+ message: `must refer to a ${field.kinds.join(" or ")}`
2719
2801
  });
2720
2802
  }
2721
2803
  return issues;
@@ -2989,6 +3071,7 @@ exports.nestElement = nestElement;
2989
3071
  exports.number = number;
2990
3072
  exports.parsePistar = parsePistar;
2991
3073
  exports.prop = prop;
3074
+ exports.reference = reference;
2992
3075
  exports.removeElement = removeElement;
2993
3076
  exports.removeElements = removeElements;
2994
3077
  exports.removeLeg = removeLeg;