sysml-validate 0.14.5 → 0.15.1

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/out/main.js CHANGED
@@ -30760,6 +30760,9 @@ function isExhibitStateDecl(item) {
30760
30760
  var ExitAction = "ExitAction";
30761
30761
  var ExposeMember = "ExposeMember";
30762
30762
  var ExposePath = "ExposePath";
30763
+ function isExposePath(item) {
30764
+ return reflection2.isInstance(item, ExposePath);
30765
+ }
30763
30766
  var ExpressionDecl = "ExpressionDecl";
30764
30767
  var FeatureDecl = "FeatureDecl";
30765
30768
  function isFeatureDecl(item) {
@@ -58052,14 +58055,14 @@ function evaluateFilterCondition(node, metadataTypes, resolveRefs) {
58052
58055
  return node.items.length === 1 ? evaluateFilterCondition(node.items[0], metadataTypes, resolveRefs) : void 0;
58053
58056
  }
58054
58057
  if (isBinExpr(node)) {
58055
- if (node.op === "and" || node.op === "&") {
58058
+ if (CONJUNCTION_OPERATORS.has(node.op)) {
58056
58059
  const left = evaluateFilterCondition(node.left, metadataTypes, resolveRefs);
58057
58060
  const right = evaluateFilterCondition(node.right, metadataTypes, resolveRefs);
58058
58061
  if (left === false || right === false)
58059
58062
  return false;
58060
58063
  return left === void 0 || right === void 0 ? void 0 : true;
58061
58064
  }
58062
- if (node.op === "or" || node.op === "|") {
58065
+ if (DISJUNCTION_OPERATORS.has(node.op)) {
58063
58066
  const left = evaluateFilterCondition(node.left, metadataTypes, resolveRefs);
58064
58067
  const right = evaluateFilterCondition(node.right, metadataTypes, resolveRefs);
58065
58068
  if (left === true || right === true)
@@ -58070,6 +58073,42 @@ function evaluateFilterCondition(node, metadataTypes, resolveRefs) {
58070
58073
  }
58071
58074
  return void 0;
58072
58075
  }
58076
+ var CONJUNCTION_OPERATORS = /* @__PURE__ */ new Set(["and", "&"]);
58077
+ var DISJUNCTION_OPERATORS = /* @__PURE__ */ new Set(["or", "|"]);
58078
+ function undecidableFilterCondition(condition) {
58079
+ if (!condition)
58080
+ return void 0;
58081
+ if (isSelfClassifyExpr(condition)) {
58082
+ return condition.op === "@" || condition.op === "@@" ? void 0 : { node: condition, construct: `the '${condition.op}' classification operator` };
58083
+ }
58084
+ if (isNotExpr(condition) && condition.operand) {
58085
+ return undecidableFilterCondition(condition.operand);
58086
+ }
58087
+ if (condition.$type === "NotExpr")
58088
+ return void 0;
58089
+ if (isParenExpr(condition)) {
58090
+ return condition.items.length === 1 ? undecidableFilterCondition(condition.items[0]) : { node: condition, construct: "a parenthesized expression list" };
58091
+ }
58092
+ if (isBinExpr(condition)) {
58093
+ if (CONJUNCTION_OPERATORS.has(condition.op) || DISJUNCTION_OPERATORS.has(condition.op)) {
58094
+ return undecidableFilterCondition(condition.left) ?? undecidableFilterCondition(condition.right);
58095
+ }
58096
+ return { node: condition, construct: `the '${condition.op}' operator` };
58097
+ }
58098
+ return { node: condition, construct: describeFilterExpr(condition) };
58099
+ }
58100
+ function describeFilterExpr(node) {
58101
+ if (isPostfixOp(node)) {
58102
+ if ((node.dot || node.select) && node.field)
58103
+ return `the '.${node.field}' feature access`;
58104
+ if (node.arrow && node.invoke)
58105
+ return `the '->${node.invoke}' invocation`;
58106
+ return "this postfix expression";
58107
+ }
58108
+ if (isPathExpr(node))
58109
+ return "a plain feature reference";
58110
+ return "this expression form";
58111
+ }
58073
58112
  function attachedMetadataTypes(node, resolveRefs) {
58074
58113
  const siblings = node.$container?.members;
58075
58114
  if (!Array.isArray(siblings))
@@ -58713,6 +58752,13 @@ var DIAGNOSTIC_MESSAGES = {
58713
58752
  // implicit-redefinition resolution this validator does not yet model.
58714
58753
  RES017_DUPLICATE_MEMBER: (name) => `'${name}' is already declared in this namespace. Members must be distinguishable by name \u2014 rename or remove the duplicate.`,
58715
58754
  RES017_DUPLICATE_SHORT_NAME: (name) => `Short name '<${name}>' is already used in this namespace. Members must be distinguishable \u2014 rename the short name.`,
58755
+ // issue #102 — REQ-387. The filter evaluator (`import-visibility.ts`,
58756
+ // `evaluateFilterCondition`) decides metadata-presence tests and the Boolean
58757
+ // operators over them; every other form fails OPEN, keeping the member. Failing
58758
+ // open is the right default — under-importing would manufacture spurious
58759
+ // RES001s — but an unevaluated filter produces a scope or view that looks
58760
+ // authoritative and is not, so the fail-open is stated rather than assumed.
58761
+ RES018_UNEVALUATED_FILTER: (subject, construct) => `${subject} filter is not evaluated: ${construct} is outside the metadata-filter evaluator, which decides '@'/'@@' metadata tests combined with 'not', 'and'/'&', and 'or'/'|'. The filter fails open \u2014 it is treated as pass-through and removes nothing.`,
58716
58762
  // REQ-007 — SysML v1 → v2 migration guardrail.
58717
58763
  MIG001_V1_KEYWORD: (keyword, v2, note) => `'${keyword}' is a SysML v1 construct, not a SysML v2 keyword. ${note} Use '${v2}'.`,
58718
58764
  MIG002_V1_STEREOTYPE_KNOWN: (stereotype, v2, note) => `SysML v2 has no stereotypes. ${note} Replace \xAB${stereotype}\xBB with '${v2}'.`,
@@ -59496,6 +59542,26 @@ function actionOnlyConstruct(node) {
59496
59542
  const keyword = CONTROL_NODE_KEYWORDS[node.$type];
59497
59543
  return keyword ? { construct: `A '${keyword}' control node`, clause: "7.17.3" } : void 0;
59498
59544
  }
59545
+ function portionKindOf(node) {
59546
+ const mods = node.modifiers;
59547
+ if (!Array.isArray(mods))
59548
+ return void 0;
59549
+ return mods.find((m) => m === "timeslice" || m === "snapshot");
59550
+ }
59551
+ var NAMESPACE_ONLY_TYPES = /* @__PURE__ */ new Set(["Package", "Document", "NamespaceDecl"]);
59552
+ function owningTypeOf(node) {
59553
+ let owner = node.$container;
59554
+ while (owner && (owner.$type === "PrefixMetadataMember" || owner.$type === "ThenActionMember")) {
59555
+ owner = owner.$container;
59556
+ }
59557
+ if (!owner)
59558
+ return void 0;
59559
+ return NAMESPACE_ONLY_TYPES.has(owner.$type) ? void 0 : owner;
59560
+ }
59561
+ function declLabel(node, kind) {
59562
+ const name = node.name;
59563
+ return name ? `'${name}'` : `this '${kind}'`;
59564
+ }
59499
59565
  var SysmlValidator = class _SysmlValidator {
59500
59566
  indexManager;
59501
59567
  langiumDocuments;
@@ -59719,6 +59785,7 @@ var SysmlValidator = class _SysmlValidator {
59719
59785
  this.checkLanguageDisjointness(node, accept);
59720
59786
  this.checkKermlWellFormedness(node, accept);
59721
59787
  this.checkFilterExpressions(node, accept);
59788
+ this.checkOccurrencePortions(node, accept);
59722
59789
  this.checkCrossReferences(node, accept);
59723
59790
  this.checkAutoImportSuggestions(node, accept);
59724
59791
  this.checkUnits(node, accept);
@@ -60546,6 +60613,7 @@ ${baseIndent}}`;
60546
60613
  checkCrossReferences(node, accept) {
60547
60614
  const index = this.buildIndex();
60548
60615
  const imports = [];
60616
+ const exposePaths = [];
60549
60617
  const decls = [];
60550
60618
  const verifyStmts = [];
60551
60619
  const satisfyStmts = [];
@@ -60557,7 +60625,9 @@ ${baseIndent}}`;
60557
60625
  const path10 = importedPath(child);
60558
60626
  if (alias && path10 && importWildcard(child) === "none")
60559
60627
  aliasMap.set(alias, path10);
60560
- } else if (isVerifyStmt(child))
60628
+ } else if (isExposePath(child))
60629
+ exposePaths.push(child);
60630
+ else if (isVerifyStmt(child))
60561
60631
  verifyStmts.push(child);
60562
60632
  else if (isSatisfyStmt(child))
60563
60633
  satisfyStmts.push(child);
@@ -60573,6 +60643,7 @@ ${baseIndent}}`;
60573
60643
  this.checkPrivateImports(imports, index, accept);
60574
60644
  this.checkPrivateWildcardReferences(node, imports, index, accept);
60575
60645
  this.checkImportCycles(imports, index, accept);
60646
+ this.checkUnevaluatedFilters(imports, exposePaths, accept);
60576
60647
  if (sysmlDiagnosticSettings.unusedImports !== "off") {
60577
60648
  this.checkUnusedImports(node, imports, index, sysmlDiagnosticSettings.unusedImports, accept);
60578
60649
  }
@@ -60602,6 +60673,49 @@ ${baseIndent}}`;
60602
60673
  this.checkTypeComposition(decl, index, accept);
60603
60674
  }
60604
60675
  }
60676
+ // ══════════════════════════════════════════════════════════════════════
60677
+ // REQ-160 — occurrence portions: `timeslice` / `snapshot` well-formedness
60678
+ // ══════════════════════════════════════════════════════════════════════
60679
+ // The declaration kinds that are NOT occurrences, so a portion of their life is
60680
+ // meaningless. Kept as a deny-list rather than an allow-list because nearly every
60681
+ // SysML usage kind IS an OccurrenceUsage (part, item, action, state, port,
60682
+ // connection, case, …) while the data-valued ones are few and closed: an
60683
+ // AttributeUsage is typed by a DataType and an enumeration is a special attribute,
60684
+ // neither of which has a life to carve up.
60685
+ static NON_OCCURRENCE_OWNER_TYPES = /* @__PURE__ */ new Set(["AttributeDecl", "EnumDecl", "EnumValueDecl", "MetadataDecl", "MultiplicityDecl"]);
60686
+ // REQ-160 — OMG SysML v2 §8.2.2.9 (Occurrences) with the Kernel Semantic Library
60687
+ // `Occurrences::TimeSlice` / `Occurrences::Snapshot`:
60688
+ //
60689
+ // SSM015 — a portion is a portion OF something. Its owning type must be an
60690
+ // occurrence; declared at package level (or inside an attribute /
60691
+ // enumeration body) there is no life for it to be a portion of.
60692
+ // SSM016 — `Snapshot :> TimeSlice` with a zero duration: a snapshot is the
60693
+ // occurrence at ONE instant. Every portion of a zero-duration
60694
+ // occurrence is itself zero-duration, so a snapshot may own further
60695
+ // snapshots but never a `timeslice`.
60696
+ //
60697
+ // Both are structural — they read modifiers and owners only — so they hold
60698
+ // without resolving a single cross-reference.
60699
+ checkOccurrencePortions(node, accept) {
60700
+ for (const child of ast_utils_exports.streamAllContents(node)) {
60701
+ const kind = portionKindOf(child);
60702
+ if (!kind)
60703
+ continue;
60704
+ const label = declLabel(child, kind);
60705
+ const owner = owningTypeOf(child);
60706
+ if (!owner) {
60707
+ accept(severity("SSM015", "error"), `A '${kind}' is a portion of an occurrence's life, so ${label} must be declared inside an occurrence \u2014 not directly in a package.`, { node: child, code: "SSM015" });
60708
+ continue;
60709
+ }
60710
+ if (_SysmlValidator.NON_OCCURRENCE_OWNER_TYPES.has(owner.$type)) {
60711
+ accept(severity("SSM015", "error"), `A '${kind}' is a portion of an occurrence's life, and '${owner.name ?? owner.$type}' is not an occurrence.`, { node: child, code: "SSM015" });
60712
+ continue;
60713
+ }
60714
+ if (kind === "timeslice" && portionKindOf(owner) === "snapshot") {
60715
+ accept(severity("SSM016", "error"), `A 'snapshot' is a zero-duration timeslice, so ${label} cannot be a timeslice of it \u2014 a portion of an instant is itself a snapshot.`, { node: child, code: "SSM016" });
60716
+ }
60717
+ }
60718
+ }
60605
60719
  // REQ-328 — KSM008: a `filter` membership condition must be Boolean. Only a
60606
60720
  // *definitely* non-Boolean expression is flagged — a string or numeric/quantity
60607
60721
  // literal, or an arithmetic/range binary expression. A bare feature path is left
@@ -61148,6 +61262,34 @@ ${baseIndent}}`;
61148
61262
  }
61149
61263
  return false;
61150
61264
  }
61265
+ /**
61266
+ * REQ-387 — RES018: an `import` or `expose` filter the metadata-filter
61267
+ * evaluator cannot decide (issue #102).
61268
+ *
61269
+ * The evaluator keeps a member it cannot decide, so an unevaluated filter
61270
+ * silently contributes the members it was written to exclude — a scope or a
61271
+ * view that reads as authoritative and is not. Failing open stays (narrowing
61272
+ * on a guess would manufacture unresolved-name errors); what changes is that
61273
+ * the model now says so at the filter that is not applied.
61274
+ *
61275
+ * Default severity is `info` rather than a warning: the model is legal and the
61276
+ * limitation is this evaluator's, not the author's. Teams that want an
61277
+ * unevaluated filter to break a build raise it per code through
61278
+ * `sysml.validation.severities` (REQ-240).
61279
+ */
61280
+ checkUnevaluatedFilters(imports, exposePaths, accept) {
61281
+ const report = (condition, subject) => {
61282
+ const undecidable = undecidableFilterCondition(condition);
61283
+ if (!undecidable)
61284
+ return;
61285
+ accept(severity("RES018", "info"), DIAGNOSTIC_MESSAGES.RES018_UNEVALUATED_FILTER(subject, undecidable.construct), { node: undecidable.node, code: "RES018" });
61286
+ };
61287
+ for (const imp of imports)
61288
+ for (const filter3 of imp.filters)
61289
+ report(filter3.condition, "Import");
61290
+ for (const path10 of exposePaths)
61291
+ report(path10.condition, "Expose");
61292
+ }
61151
61293
  // REQ-324 — RES009 unused import (configurable, OFF by default)
61152
61294
  checkUnusedImports(root3, imports, index, level, accept) {
61153
61295
  if (imports.length === 0)
@@ -68967,7 +69109,7 @@ async function runValidation(command) {
68967
69109
  }
68968
69110
 
68969
69111
  // src/main.ts
68970
- var VERSION2 = true ? "0.14.5" : "dev";
69112
+ var VERSION2 = true ? "0.15.1" : "dev";
68971
69113
  async function main(argv) {
68972
69114
  const command = parseArgs(argv);
68973
69115
  if (command.kind === "help") {