@memoarchitect/tools 0.8.8 → 0.11.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.
Files changed (116) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/packages/tools/lib/analysis/dsm.d.ts.map +1 -1
  4. package/packages/tools/lib/analysis/dsm.js +9 -4
  5. package/packages/tools/lib/analysis/dsm.js.map +1 -1
  6. package/packages/tools/lib/commands/export.js +1 -1
  7. package/packages/tools/lib/commands/export.js.map +1 -1
  8. package/packages/tools/lib/commands/init.d.ts.map +1 -1
  9. package/packages/tools/lib/commands/init.js +16 -5
  10. package/packages/tools/lib/commands/init.js.map +1 -1
  11. package/packages/tools/lib/commands/rules.d.ts.map +1 -1
  12. package/packages/tools/lib/commands/rules.js +3 -2
  13. package/packages/tools/lib/commands/rules.js.map +1 -1
  14. package/packages/tools/lib/commands/validate.d.ts.map +1 -1
  15. package/packages/tools/lib/commands/validate.js +71 -4
  16. package/packages/tools/lib/commands/validate.js.map +1 -1
  17. package/packages/tools/lib/completeness/tracker.d.ts.map +1 -1
  18. package/packages/tools/lib/completeness/tracker.js +11 -4
  19. package/packages/tools/lib/completeness/tracker.js.map +1 -1
  20. package/packages/tools/lib/dhf/document-registry.js +1 -1
  21. package/packages/tools/lib/dhf/document-registry.js.map +1 -1
  22. package/packages/tools/lib/dhf/query-executor.d.ts +1 -1
  23. package/packages/tools/lib/dhf/query-executor.d.ts.map +1 -1
  24. package/packages/tools/lib/dhf/query-executor.js +4 -4
  25. package/packages/tools/lib/dhf/query-executor.js.map +1 -1
  26. package/packages/tools/lib/dhf/standards-library.d.ts.map +1 -1
  27. package/packages/tools/lib/dhf/standards-library.js +53 -10
  28. package/packages/tools/lib/dhf/standards-library.js.map +1 -1
  29. package/packages/tools/lib/dhf/template-lint.js +1 -1
  30. package/packages/tools/lib/dhf/template-lint.js.map +1 -1
  31. package/packages/tools/lib/language/generated/ast.d.ts +75 -34
  32. package/packages/tools/lib/language/generated/ast.d.ts.map +1 -1
  33. package/packages/tools/lib/language/generated/ast.js +36 -2
  34. package/packages/tools/lib/language/generated/ast.js.map +1 -1
  35. package/packages/tools/lib/language/generated/grammar.d.ts.map +1 -1
  36. package/packages/tools/lib/language/generated/grammar.js +516 -384
  37. package/packages/tools/lib/language/generated/grammar.js.map +1 -1
  38. package/packages/tools/lib/mcp/tools.d.ts.map +1 -1
  39. package/packages/tools/lib/mcp/tools.js +2 -1
  40. package/packages/tools/lib/mcp/tools.js.map +1 -1
  41. package/packages/tools/lib/model/builder.d.ts +2 -0
  42. package/packages/tools/lib/model/builder.d.ts.map +1 -1
  43. package/packages/tools/lib/model/builder.js +303 -42
  44. package/packages/tools/lib/model/builder.js.map +1 -1
  45. package/packages/tools/lib/model/config-loader.d.ts.map +1 -1
  46. package/packages/tools/lib/model/config-loader.js +7 -1
  47. package/packages/tools/lib/model/config-loader.js.map +1 -1
  48. package/packages/tools/lib/model/config.d.ts +9 -0
  49. package/packages/tools/lib/model/config.d.ts.map +1 -1
  50. package/packages/tools/lib/model/identity-registry.d.ts +39 -0
  51. package/packages/tools/lib/model/identity-registry.d.ts.map +1 -0
  52. package/packages/tools/lib/model/identity-registry.js +118 -0
  53. package/packages/tools/lib/model/identity-registry.js.map +1 -0
  54. package/packages/tools/lib/model/native-project.d.ts +6 -0
  55. package/packages/tools/lib/model/native-project.d.ts.map +1 -1
  56. package/packages/tools/lib/model/native-project.js +6 -0
  57. package/packages/tools/lib/model/native-project.js.map +1 -1
  58. package/packages/tools/lib/model/relationship-registry.d.ts.map +1 -1
  59. package/packages/tools/lib/model/relationship-registry.js +22 -0
  60. package/packages/tools/lib/model/relationship-registry.js.map +1 -1
  61. package/packages/tools/lib/model/semantic.d.ts +57 -2
  62. package/packages/tools/lib/model/semantic.d.ts.map +1 -1
  63. package/packages/tools/lib/model/semantic.js +38 -0
  64. package/packages/tools/lib/model/semantic.js.map +1 -1
  65. package/packages/tools/lib/model/short-id.d.ts +22 -4
  66. package/packages/tools/lib/model/short-id.d.ts.map +1 -1
  67. package/packages/tools/lib/model/short-id.js +46 -7
  68. package/packages/tools/lib/model/short-id.js.map +1 -1
  69. package/packages/tools/lib/model/view-deriver.d.ts.map +1 -1
  70. package/packages/tools/lib/model/view-deriver.js +71 -22
  71. package/packages/tools/lib/model/view-deriver.js.map +1 -1
  72. package/packages/tools/lib/operations/project-snapshot.d.ts.map +1 -1
  73. package/packages/tools/lib/operations/project-snapshot.js +24 -2
  74. package/packages/tools/lib/operations/project-snapshot.js.map +1 -1
  75. package/packages/tools/lib/protocol/messages.d.ts +29 -0
  76. package/packages/tools/lib/protocol/messages.d.ts.map +1 -1
  77. package/packages/tools/lib/server/config-resolver.d.ts.map +1 -1
  78. package/packages/tools/lib/server/config-resolver.js +6 -1
  79. package/packages/tools/lib/server/config-resolver.js.map +1 -1
  80. package/packages/tools/lib/server/dev-server.d.ts.map +1 -1
  81. package/packages/tools/lib/server/dev-server.js +40 -4
  82. package/packages/tools/lib/server/dev-server.js.map +1 -1
  83. package/packages/tools/lib/server/dhf-doc-store.d.ts.map +1 -1
  84. package/packages/tools/lib/server/dhf-doc-store.js +10 -2
  85. package/packages/tools/lib/server/dhf-doc-store.js.map +1 -1
  86. package/packages/tools/lib/server/package-source.d.ts +20 -1
  87. package/packages/tools/lib/server/package-source.d.ts.map +1 -1
  88. package/packages/tools/lib/server/package-source.js +23 -4
  89. package/packages/tools/lib/server/package-source.js.map +1 -1
  90. package/packages/tools/lib/server/package-writer.d.ts +18 -0
  91. package/packages/tools/lib/server/package-writer.d.ts.map +1 -1
  92. package/packages/tools/lib/server/package-writer.js +58 -5
  93. package/packages/tools/lib/server/package-writer.js.map +1 -1
  94. package/packages/tools/lib/server/persistor.d.ts.map +1 -1
  95. package/packages/tools/lib/server/persistor.js +4 -1
  96. package/packages/tools/lib/server/persistor.js.map +1 -1
  97. package/packages/tools/lib/server/view-layout-store.d.ts.map +1 -1
  98. package/packages/tools/lib/server/view-layout-store.js +6 -3
  99. package/packages/tools/lib/server/view-layout-store.js.map +1 -1
  100. package/packages/tools/lib/validator/behavior-validator.d.ts +3 -5
  101. package/packages/tools/lib/validator/behavior-validator.d.ts.map +1 -1
  102. package/packages/tools/lib/validator/behavior-validator.js +28 -54
  103. package/packages/tools/lib/validator/behavior-validator.js.map +1 -1
  104. package/packages/tools/lib/validator/builtin-rules.js +12 -12
  105. package/packages/tools/lib/validator/builtin-rules.js.map +1 -1
  106. package/packages/tools/lib/validator/constraint-eval.d.ts +39 -2
  107. package/packages/tools/lib/validator/constraint-eval.d.ts.map +1 -1
  108. package/packages/tools/lib/validator/constraint-eval.js +104 -4
  109. package/packages/tools/lib/validator/constraint-eval.js.map +1 -1
  110. package/packages/tools/lib/validator/constraint-loader.js +37 -2
  111. package/packages/tools/lib/validator/constraint-loader.js.map +1 -1
  112. package/packages/tools/lib/validator/rule-engine.d.ts +3 -1
  113. package/packages/tools/lib/validator/rule-engine.d.ts.map +1 -1
  114. package/packages/tools/lib/validator/rule-engine.js +4 -2
  115. package/packages/tools/lib/validator/rule-engine.js.map +1 -1
  116. package/packages/tools/src/grammar/memo-sysml.langium +41 -6
@@ -37,6 +37,7 @@ import { AstUtils } from 'langium';
37
37
  import { isRequirementVerificationMember, isSatisfyRequirementUsage, isPerformActionUsage, isIncludeUseCaseUsage, isExhibitStateUsage, isFramedConcernMember, isSubjectMember, isDependency, isVariantMember, isActorMember, } from '../language/generated/ast.js';
38
38
  import { PackageRegistry } from './package-registry.js';
39
39
  import { assignSequentialShortIds, kindToPrefix } from './short-id.js';
40
+ import { identityKey, priorShortIds } from './identity-registry.js';
40
41
  import { LANGUAGE_NATIVE_RELATIONS, REFINEMENT_METADATA, pascalToCamelCase } from './relationship-registry.js';
41
42
  import { indexKinds, kindConformsTo } from './relationship-legality.js';
42
43
  import { CONTROL_KIND_METACLASS } from './activity-notation.js';
@@ -142,12 +143,13 @@ export function buildMemoModel(documents, config, parseErrors = [], registries)
142
143
  // so emit one per nesting. A model that nests natively then looks identical
143
144
  // to one that wrote `Composes`, which is what lets R10-S7 delete the
144
145
  // relationship without the containment vanishing. Ports carry `owner` too,
145
- // so this is confined to part-to-part nesting.
146
+ // so this covers part and item nesting, and nothing else.
147
+ const CONTAINING_CONSTRUCTS = new Set(['part', 'item']);
146
148
  for (const el of elements.values()) {
147
- if (el.construct !== 'part' || !el.owner)
149
+ if (!CONTAINING_CONSTRUCTS.has(el.construct) || !el.owner)
148
150
  continue;
149
151
  const parent = elements.get(el.owner);
150
- if (!parent || parent.construct !== 'part')
152
+ if (!parent || !CONTAINING_CONSTRUCTS.has(parent.construct))
151
153
  continue;
152
154
  relationships.push({
153
155
  id: `rel-${++relationshipCounter}`,
@@ -215,15 +217,27 @@ export function buildMemoModel(documents, config, parseErrors = [], registries)
215
217
  prefixElements.push(element);
216
218
  elementsByPrefix.set(prefix, prefixElements);
217
219
  }
220
+ // Identities assigned on earlier builds win. Both of these are quoted
221
+ // outside the model, so recomputing them from current content — which is
222
+ // what happened before `memo.identity.yaml` — silently repointed every
223
+ // external reference whenever a file moved or an element was inserted.
224
+ const priorIdentities = config.priorIdentities ?? {};
225
+ const priorShortIdsByElement = priorShortIds(priorIdentities);
218
226
  for (const [prefix, prefixElements] of elementsByPrefix) {
219
- const idToShortId = assignSequentialShortIds(prefix, prefixElements.map(e => e.id));
227
+ const idToShortId = assignSequentialShortIds(prefix, prefixElements.map(e => e.id), priorShortIdsByElement);
220
228
  for (const el of prefixElements) {
221
229
  el.shortId = idToShortId.get(el.id);
222
230
  // `uuid` is part of the ontology's identification core, so a model
223
231
  // may author it. An authored value wins: it is the whole point of
224
232
  // writing one — the element keeps its identity across a file move
225
233
  // or an id change, which the derived hash cannot survive.
226
- el.uuid = el.attributes.uuid?.trim() || stableElementUuid(el);
234
+ // Precedence: an authored uuid, then the one this project already
235
+ // assigned, then a fresh derivation. The middle term is what makes a
236
+ // uuid survive a file move — the derivation hashes the file path, so
237
+ // without it the same element gets a new identity when it moves.
238
+ el.uuid = el.attributes.uuid?.trim()
239
+ || priorIdentities[identityKey(el)]?.uuid
240
+ || stableElementUuid(el);
227
241
  }
228
242
  }
229
243
  // Phase 4: Validate relationship end types (warnings only).
@@ -393,7 +407,10 @@ function extractFromPackage(pkg, filePath, parentPackage, config, elements, defe
393
407
  break;
394
408
  case 'UseCaseDeclaration': {
395
409
  const useCase = member;
396
- if (!useCase.isDefinition) {
410
+ if (useCase.isDefinition) {
411
+ extractDefinitionElement({ name: useCase.name, specialization: useCase.specialization, body: useCase.body ?? useCase.usageBody }, 'use case', filePath, packageName, config, elements, registry, registries);
412
+ }
413
+ else {
397
414
  extractUsage({ name: useCase.name, type: useCase.type, body: useCase.usageBody }, 'use case', filePath, packageName, config, elements, registry, registries);
398
415
  }
399
416
  break;
@@ -416,6 +433,9 @@ function extractFromPackage(pkg, filePath, parentPackage, config, elements, defe
416
433
  case 'ViewUsage':
417
434
  extractUsage(member, 'view', filePath, packageName, config, elements, registry, registries);
418
435
  break;
436
+ case 'ViewpointUsage':
437
+ extractViewpointUsage(member, filePath, packageName, config, elements, registry, registries);
438
+ break;
419
439
  case 'ConnectionUsage':
420
440
  // Defer connection resolution until all elements are extracted
421
441
  deferredConnections.push({
@@ -500,10 +520,24 @@ function extractFromPackage(pkg, filePath, parentPackage, config, elements, defe
500
520
  case 'PortDefinition':
501
521
  case 'InterfaceDefinition':
502
522
  case 'ConnectionDefinition':
523
+ extractDefinitionElement(member, DEFINITION_CONSTRUCTS[member.$type], filePath, packageName, config, elements, registry, registries);
503
524
  extractDefinitionPorts(member, filePath, packageName, config, elements, registry, registries);
525
+ // A definition's body is composition: `part def Pump { part
526
+ // motor : Motor; }` is what a BDD draws and what an IBD looks
527
+ // inside. Nested usages become elements owned by the
528
+ // definition, exactly as they do under a part usage.
529
+ extractNestedParts(member, filePath, packageName, config, elements, registry, registries);
504
530
  break;
505
- // Other definitions (viewpoint def, view def, etc.) inside packages are
506
- // ontology-level — we don't extract them as model elements in device projects
531
+ case 'RequirementDefinition':
532
+ case 'VerificationDefinition':
533
+ case 'StateDefinition':
534
+ case 'AttributeDefinition':
535
+ case 'EnumDefinition':
536
+ extractDefinitionElement(member, DEFINITION_CONSTRUCTS[member.$type], filePath, packageName, config, elements, registry, registries);
537
+ break;
538
+ // A `view def` / `viewpoint def` stays out: those are presentation
539
+ // apparatus, read through the viewpoint tree rather than as model
540
+ // content.
507
541
  }
508
542
  }
509
543
  }
@@ -625,8 +659,12 @@ function extractUsage(usage, construct, filePath, packageName, config, elements,
625
659
  // Without this, an inline-authored port silently has no element: it
626
660
  // parses, every relationship endpoint referencing it resolves to a
627
661
  // dangling id, and nothing downstream reports why.
628
- if (construct === 'part') {
662
+ // Items nest too — a standard contains its clauses — so the nested-usage
663
+ // walk runs for both. Ports stay part-only: an item has no boundary.
664
+ if (construct === 'part' || construct === 'item') {
629
665
  extractNestedParts(usage, filePath, packageName, config, elements, registry, registries);
666
+ }
667
+ if (construct === 'part') {
630
668
  extractDefinitionPorts(usage, filePath, packageName, config, elements, registry, registries);
631
669
  }
632
670
  // A native `transition` lives inside the state machine that owns it, which
@@ -710,13 +748,30 @@ function extractActionDefinition(actionDef, filePath, packageName, config, eleme
710
748
  const attributes = extractAttributes(bodyMembers);
711
749
  const doc = extractDocComment(bodyMembers);
712
750
  const behaviorKind = actionDef.behaviorKind ?? 'action';
751
+ // `action def AcquireSensorData :> FunctionalAction` defines a
752
+ // FunctionalAction. Stamping it `ActionDefinition` named the SysML
753
+ // METACLASS as though it were a MEMO kind — the ontology declares no such
754
+ // kind, so every project action def landed in the explorer's "not in
755
+ // ontology" bucket, and a validator had to look it up by that literal.
756
+ // The metaclass is what `construct` and `isDefinition` say; the kind is
757
+ // what the definition specializes.
758
+ const superType = actionDef.specialization?.superType;
759
+ const resolved = superType ? resolveKindDef(superType, config, registries) : undefined;
760
+ const ontologyKind = resolved?.kindDef && resolved.kindDef.layer && resolved.kindDef.layer !== 'unknown'
761
+ ? resolved
762
+ : undefined;
713
763
  const element = {
714
764
  id,
715
765
  name: id,
716
- kind: behaviorKind === 'operator' ? 'OperatorDefinition'
717
- : behaviorKind === 'function' ? 'FunctionDefinition' : 'ActionDefinition',
766
+ // Only where nothing resolves does the metaclass stand in, and there it
767
+ // is the truthful answer: this is a SysML ActionDefinition and nothing
768
+ // more is known about it.
769
+ kind: ontologyKind?.resolvedKind
770
+ ?? (behaviorKind === 'operator' ? 'OperatorDefinition'
771
+ : behaviorKind === 'function' ? 'FunctionDefinition' : 'ActionDefinition'),
718
772
  construct: 'action',
719
- layer: 'behavior',
773
+ isDefinition: true,
774
+ layer: ontologyKind?.kindDef?.layer ?? 'behavior',
720
775
  file: filePath,
721
776
  package: packageName || undefined,
722
777
  attributes: { ...attributes, behaviorKind },
@@ -802,6 +857,7 @@ function extractItemDefinition(itemDef, filePath, packageName, config, elements,
802
857
  name: id,
803
858
  kind: kindDef?.layer && kindDef.layer !== 'unknown' ? resolvedKind : 'ItemDefinition',
804
859
  construct: 'item',
860
+ isDefinition: true,
805
861
  layer: kindDef?.layer && kindDef.layer !== 'unknown' ? kindDef.layer : 'behavior',
806
862
  file: filePath,
807
863
  package: packageName || undefined,
@@ -826,6 +882,72 @@ function extractItemDefinition(itemDef, filePath, packageName, config, elements,
826
882
  * or a state. So the native form was the only correct one, and it was the one
827
883
  * that did not work.
828
884
  */
885
+ /**
886
+ * The SysML construct each definition declares, in the spelling its usages
887
+ * carry — a `part def` and a `part` are the same construct, one defining and
888
+ * one using.
889
+ */
890
+ const DEFINITION_CONSTRUCTS = {
891
+ PartDefinition: 'part',
892
+ PortDefinition: 'port',
893
+ InterfaceDefinition: 'interface',
894
+ ConnectionDefinition: 'connection',
895
+ RequirementDefinition: 'requirement',
896
+ VerificationDefinition: 'verification',
897
+ StateDefinition: 'state',
898
+ AttributeDefinition: 'attribute',
899
+ EnumDefinition: 'enumeration',
900
+ };
901
+ /**
902
+ * A definition the PROJECT declares, as a model element.
903
+ *
904
+ * `part def Pump :> LogicalComponent` defines a Pump. It was registered as a
905
+ * kind and nothing else, so no view could select it and no BDD or IBD could
906
+ * draw it — which is most of what a definition is for. It is an element now,
907
+ * and its usages read beneath it: a usage's `kind` is the definition's name,
908
+ * which is the link every explorer already follows.
909
+ *
910
+ * The kind it carries is the ONTOLOGY kind it specializes, so a view, a
911
+ * viewpoint filter and a palette that admit `LogicalComponent` admit the
912
+ * definition of one without being edited. `isDefinition` is what tells the two
913
+ * apart afterwards — a definition is a type, not an instance of one, and
914
+ * anything counting instances must say so.
915
+ *
916
+ * This is only ever project content: the ontology's own definitions are read
917
+ * by the kind registry and are never parsed into the model.
918
+ */
919
+ function extractDefinitionElement(definition, construct, filePath, packageName, config, elements, registry, registries) {
920
+ const id = definition.name;
921
+ if (!id || elements.has(id))
922
+ return;
923
+ // A `library package` is how a project writes ontology-style content: its
924
+ // definitions are types for other models to use, not this model's content.
925
+ // The user's distinction exactly — model definitions become elements,
926
+ // library ones stay kinds, which is what they already were.
927
+ if (registry.isLibraryPackage(packageName))
928
+ return;
929
+ const superType = definition.specialization?.superType;
930
+ const { kindDef, resolvedKind } = superType
931
+ ? resolveKindDef(superType, config, registries)
932
+ : { kindDef: undefined, resolvedKind: undefined };
933
+ const attributes = extractAttributes(definition.body ?? []);
934
+ const doc = extractDocComment(definition.body ?? []);
935
+ elements.set(id, {
936
+ id,
937
+ name: attributes['name'] || attributes['title'] || id,
938
+ // A definition that specializes nothing the ontology knows is still a
939
+ // definition; it says so rather than borrowing a kind it has not got.
940
+ kind: resolvedKind ?? `${construct.charAt(0).toUpperCase()}${construct.slice(1)}Definition`,
941
+ construct,
942
+ layer: kindDef?.layer || 'unknown',
943
+ file: filePath,
944
+ package: packageName || undefined,
945
+ attributes,
946
+ doc,
947
+ isDefinition: true,
948
+ });
949
+ registry.registerElement(id, packageName);
950
+ }
829
951
  function extractDefinitionPorts(def, filePath, packageName, config, elements, registry, registries) {
830
952
  const ownerId = def.name;
831
953
  const body = def.body || [];
@@ -849,8 +971,8 @@ function extractDefinitionPorts(def, filePath, packageName, config, elements, re
849
971
  }
850
972
  }
851
973
  /**
852
- * Walk a part usage's body to extract nested parts as owned elements, setting
853
- * `owner` on each child.
974
+ * Walk a usage's body to extract nested parts AND items as owned elements,
975
+ * setting `owner` on each child.
854
976
  *
855
977
  * `extractUsage` flattens the `PartMember` shapes that are view metadata — a
856
978
  * reference binding (`part viewpoint :> vp;`) and an untyped body
@@ -864,20 +986,117 @@ function extractDefinitionPorts(def, filePath, packageName, config, elements, re
864
986
  function extractNestedParts(owner, filePath, packageName, config, elements, registry, registries) {
865
987
  const ownerId = owner.name;
866
988
  for (const member of owner.body || []) {
867
- if (member.$type !== 'PartMember')
868
- continue;
869
989
  const pm = member;
870
- // Only a TYPED nested part is containment. Untyped bodies (view
990
+ const isPart = pm.$type === 'PartMember';
991
+ // A nested ITEM is containment too — `item iec62304 { item clause4 …}`
992
+ // is how every standard declares its clauses. Without this branch the
993
+ // child parses cleanly in both parsers and never becomes an element at
994
+ // all: not a missing edge, a missing element. That is the
995
+ // BindingUsage / ExposeMember / untyped-flow shape again.
996
+ //
997
+ // A DIRECTED item is excluded: `in item payload : BubbleSignal;` is a
998
+ // port's payload parameter, not something the port contains. The same
999
+ // `direction` test already separates the two at the action-parameter
1000
+ // and flow sites.
1001
+ const isItem = pm.$type === 'ItemUsage' && !pm.direction;
1002
+ if (!isPart && !isItem)
1003
+ continue;
1004
+ // Only a TYPED nested usage is containment. Untyped bodies (view
871
1005
  // selectionQuery) and reference bindings are view metadata, flattened
872
1006
  // to attributes in extractUsage — skip them here or a view's metadata
873
1007
  // becomes a phantom element and its selection query is lost.
874
1008
  if (!pm.type || pm.boundRef || !pm.name)
875
1009
  continue;
876
- extractUsage({ name: pm.name, type: pm.type, body: pm.body }, 'part', filePath, packageName, config, elements, registry, registries);
1010
+ extractUsage({ name: pm.name, type: pm.type, body: pm.body }, isPart ? 'part' : 'item', filePath, packageName, config, elements, registry, registries);
877
1011
  const childEl = elements.get(pm.name);
878
1012
  if (childEl)
879
1013
  childEl.owner = ownerId;
880
1014
  }
1015
+ extractNestedPackages(owner, filePath, packageName, config, elements, registry, registries);
1016
+ }
1017
+ /**
1018
+ * A `package` nested inside a usage body — `part scrHome : UIElement { package
1019
+ * grpHeader { part btnBack : UIElement; } }`.
1020
+ *
1021
+ * A package GROUPS; it does not decompose. So the group becomes an element (the
1022
+ * explorer needs a node to render, and the user asked to create these to
1023
+ * organise a screen or a leaf of the model), but the things inside it stay
1024
+ * contained by the OWNER — putting them under the package would assert a part
1025
+ * decomposition the author did not write, and would make the containment
1026
+ * family rules see a package "containing" software.
1027
+ *
1028
+ * Without this the package and everything in it parses in both parsers and
1029
+ * never becomes an element: the BindingUsage / ExposeMember / nested-item shape
1030
+ * for the fourth time.
1031
+ */
1032
+ function extractNestedPackages(owner, filePath, packageName, config, elements, registry, registries) {
1033
+ for (const member of owner.body || []) {
1034
+ const pm = member;
1035
+ if (pm.$type !== 'PackageDeclaration' || !pm.name)
1036
+ continue;
1037
+ elements.set(pm.name, {
1038
+ id: pm.name,
1039
+ name: pm.name,
1040
+ kind: ELEMENT_PACKAGE_KIND,
1041
+ construct: 'package',
1042
+ layer: elements.get(owner.name)?.layer ?? 'unknown',
1043
+ file: filePath,
1044
+ package: packageName,
1045
+ attributes: {},
1046
+ owner: owner.name,
1047
+ });
1048
+ for (const grouped of pm.members ?? []) {
1049
+ const gm = grouped;
1050
+ // `part btnBack : UIElement;` parses as a PartMember and
1051
+ // `part btnBack : UIElement { … }` as a PartUsage — the same
1052
+ // declaration with and without a body. Accepting only the first
1053
+ // dropped every grouped element that declares anything, which is
1054
+ // most of them.
1055
+ const isPart = gm.$type === 'PartMember' || gm.$type === 'PartUsage';
1056
+ const isItem = gm.$type === 'ItemUsage' && !gm.direction;
1057
+ if (!isPart && !isItem)
1058
+ continue;
1059
+ if (!gm.type || gm.boundRef || !gm.name)
1060
+ continue;
1061
+ extractUsage({ name: gm.name, type: gm.type, body: gm.body }, isPart ? 'part' : 'item', filePath, packageName, config, elements, registry, registries);
1062
+ const childEl = elements.get(gm.name);
1063
+ if (childEl) {
1064
+ // Contained by the OWNER; the package only labels the grouping.
1065
+ childEl.owner = owner.name;
1066
+ childEl.attributes['elementPackage'] = pm.name;
1067
+ }
1068
+ }
1069
+ }
1070
+ }
1071
+ /** The kind a nested grouping package becomes, distinct from a namespace package. */
1072
+ export const ELEMENT_PACKAGE_KIND = 'ElementPackage';
1073
+ /**
1074
+ * A `viewpoint` usage, and any viewpoint declared inside it.
1075
+ *
1076
+ * A viewpoint nests for system-of-systems modelling: the viewpoint that frames
1077
+ * the whole system of systems declares one viewpoint per constituent system,
1078
+ * which is how ISO 42010 composes a description of a description. Native SysML
1079
+ * already permits it (`ViewpointUsage` is a `UsageBodyMember`) and the grammar
1080
+ * already parsed it — nothing extracted it, so a nested viewpoint became no
1081
+ * element at all and every consumer saw one flat list.
1082
+ *
1083
+ * The nested viewpoint carries its parent in `owner`, the same way a nested
1084
+ * part does. Unlike a part, no `composes` edge is synthesized: a viewpoint is
1085
+ * not a `part` construct, and framing is not containment of hardware.
1086
+ */
1087
+ function extractViewpointUsage(usage, filePath, packageName, config, elements, registry, registries, ownerId) {
1088
+ if (!usage.name)
1089
+ return;
1090
+ extractUsage({ name: usage.name, type: usage.type, body: usage.body }, 'viewpoint', filePath, packageName, config, elements, registry, registries);
1091
+ const element = elements.get(usage.name);
1092
+ if (element && ownerId)
1093
+ element.owner = ownerId;
1094
+ for (const member of usage.body || []) {
1095
+ const nested = member;
1096
+ if (nested.$type !== 'ViewpointUsage')
1097
+ continue;
1098
+ extractViewpointUsage(nested, filePath, packageName, config, elements, registry, registries, usage.name);
1099
+ }
881
1100
  }
882
1101
  /**
883
1102
  * Extract an ActionUsage, including nested actions, flows, and successions.
@@ -1001,14 +1220,18 @@ function resolveConnection(conn, filePath, packageName, config, elements, relati
1001
1220
  return;
1002
1221
  // A named connection usage carries its own stable ID; anonymous ones fall
1003
1222
  // back to a positional counter that shifts whenever the file changes.
1004
- const attributes = { ...extractAttributes(conn.body), ...extractRelationshipMetadata(conn.body) };
1223
+ // `extractAttributes` lifts metadata applications itself now, so the
1224
+ // connection path no longer needs its own second pass.
1225
+ const attributes = extractAttributes(conn.body);
1005
1226
  const rel = {
1006
1227
  id: conn.name || `rel-${++relationshipCounter}`,
1007
1228
  type: normalizedType,
1008
1229
  sourceId,
1009
- sourceEnd: conn.source.endName,
1230
+ // `connect a to b` omits the end names; the ends are then the inherited
1231
+ // `source`/`target` participants. csv-io already defaults the same way.
1232
+ sourceEnd: conn.source.endName ?? 'source',
1010
1233
  targetId,
1011
- targetEnd: conn.target.endName,
1234
+ targetEnd: conn.target.endName ?? 'target',
1012
1235
  file: filePath,
1013
1236
  sourceRange: conn.$cstNode
1014
1237
  ? { offset: conn.$cstNode.offset, length: conn.$cstNode.length }
@@ -1046,7 +1269,15 @@ function resolveConnection(conn, filePath, packageName, config, elements, relati
1046
1269
  * mismatch on whichever edge happened to carry a different item from its
1047
1270
  * parent's.
1048
1271
  *
1049
- * Walk the path through `owner` and take the deepest real element.
1272
+ * Walk the path through the parent link and take the deepest real element.
1273
+ *
1274
+ * A nested part or port records its parent in `owner`; a nested ACTION records
1275
+ * it in `parentAction`, because the two are set by separate extraction paths.
1276
+ * Reading only `owner` broke the walk at the first hop of every function path:
1277
+ * `flow from fnDeliverTherapy.fnMeterFluid.fnMeasureDelivered to …` became a
1278
+ * self-loop on `fnDeliverTherapy`, which is a plausible-looking edge pointing
1279
+ * at the wrong function — the exact failure the comment above describes, one
1280
+ * field away from where it was fixed for ports.
1050
1281
  */
1051
1282
  function resolveEndpoint(ref, elements) {
1052
1283
  const parts = ref.split('.');
@@ -1054,7 +1285,7 @@ function resolveEndpoint(ref, elements) {
1054
1285
  let i = 1;
1055
1286
  while (i < parts.length) {
1056
1287
  const child = elements.get(parts[i]);
1057
- if (!child || child.owner !== id)
1288
+ if (!child || (child.owner !== id && child.parentAction !== id))
1058
1289
  break;
1059
1290
  id = parts[i];
1060
1291
  i++;
@@ -1125,7 +1356,13 @@ function resolveFlowConnection(flow, filePath, packageName, parentActionId, rela
1125
1356
  }
1126
1357
  /**
1127
1358
  * Resolve a succession usage into pairs of MemoRelationships.
1128
- * "first start then A then B then done" → (start→A), (A→B), (B→done)
1359
+ * "first A then B" → (A→B).
1360
+ *
1361
+ * The grammar admits exactly one `then` per statement, matching KerML
1362
+ * `SuccessionDeclaration` (:845), so `steps` is always a pair here. The loop
1363
+ * below is still written over the whole array: it costs nothing, and it keeps
1364
+ * this function correct for the chained input the deferred-succession path at
1365
+ * the call site synthesizes.
1129
1366
  */
1130
1367
  function resolveSuccession(succession, filePath, packageName, parentActionId, relationships, allElementIds) {
1131
1368
  const steps = succession.steps || [];
@@ -1408,8 +1645,9 @@ function resolveActorParticipation(actor, filePath, relationships, registry, all
1408
1645
  * name/doc, not in a relationship type.
1409
1646
  */
1410
1647
  function resolveDependency(dependency, filePath, relationships, registry, allElementIds) {
1411
- const isRefinement = (dependency.prefixMetadata ?? [])
1412
- .some(annotation => annotation.type?.split('::').pop() === REFINEMENT_METADATA);
1648
+ const prefixes = (dependency.prefixMetadata ?? [])
1649
+ .map(annotation => annotation.type?.split('::').pop());
1650
+ const isRefinement = prefixes.includes(REFINEMENT_METADATA);
1413
1651
  const { packageName } = nativeUsageContext(dependency);
1414
1652
  for (const client of dependency.clients ?? []) {
1415
1653
  const sourceId = resolveEndpointId(client, packageName, registry, allElementIds);
@@ -1547,33 +1785,56 @@ function extractAttributes(body) {
1547
1785
  }
1548
1786
  }
1549
1787
  }
1788
+ // Metadata applications last, so an annotation overrides a directly declared
1789
+ // attribute of the same name. That order is what R10-S5 established for
1790
+ // connections and it is preserved here rather than re-litigated: while the
1791
+ // identification core is migrating off the base defs, an element may carry
1792
+ // both spellings, and the annotation is the newer statement of intent.
1793
+ Object.assign(attrs, extractMetadataApplications(body));
1550
1794
  return attrs;
1551
1795
  }
1552
1796
  /**
1553
- * Read the standard `Rationale`/`StatusInfo` metadata (ModelingMetadata.sysml)
1554
- * off a connection usage's body into the same flat attribute keys the deleted
1555
- * `MemoRelationship.rationale`/`.status` used to occupy (R10-S5, 2026-08-13).
1797
+ * Standard metadata whose field name differs from the flat key MEMO reads.
1798
+ * Everything else lifts under its own field name, which is what makes one
1799
+ * `metadata def` usable as a carrier for any element.
1800
+ */
1801
+ const METADATA_KEY_ALIASES = {
1802
+ Rationale: { text: 'rationale' },
1803
+ };
1804
+ /**
1805
+ * Lift every `@Foo { ... }` metadata application in a body into the flat
1806
+ * attribute map, under each field's own name.
1807
+ *
1808
+ * `extractAttributes`'s member walk covers `AttributeMember`/`RefMember`/
1809
+ * `MetadataFeatureMember`; a `MetadataApplication` is a different node type and
1810
+ * is invisible to it, so without this an annotation parses cleanly in both
1811
+ * parsers and then silently vanishes from the model — the BindingUsage /
1812
+ * ExposeMember / untyped-flow shape this codebase has already hit three times.
1556
1813
  *
1557
- * `extractAttributes` only walks `AttributeMember`/`RefMember`/
1558
- * `MetadataFeatureMember`; a `MetadataApplication` in a usage body (the `@Foo
1559
- * {...}` form) is invisible to it, so without this a `@Rationale`/`@StatusInfo`
1560
- * annotation would parse and silently vanish from the model — the same
1561
- * BindingUsage/ExposeMember shape this epic exists to stop repeating.
1814
+ * This started (R10-S5, 2026-08-13) as a connection-only reader hardcoded to
1815
+ * `Rationale.text` and `StatusInfo.status`, because those were the two base-type
1816
+ * attributes being deleted at the time. It is general now for one reason: a
1817
+ * `metadata def` is the only carrier in SysML v2 that attaches to ANY metaclass,
1818
+ * so it is how MEMO can hold common fields (`id`, `uuid`, `sourceReference`, …)
1819
+ * without re-declaring them per metaclass. A carrier the builder cannot read is
1820
+ * not a carrier, so the reader has to be as general as the mechanism.
1821
+ *
1822
+ * Fields lift under their own names; `METADATA_KEY_ALIASES` covers the standard
1823
+ * definitions whose field name differs from the key MEMO reads.
1562
1824
  */
1563
- function extractRelationshipMetadata(body) {
1825
+ function extractMetadataApplications(body) {
1564
1826
  if (!body)
1565
1827
  return {};
1566
1828
  const attrs = {};
1567
1829
  for (const member of body) {
1568
1830
  if (member.$type !== 'MetadataApplication')
1569
1831
  continue;
1570
- const metadataType = member.type?.split('::').pop();
1571
- const fields = extractAttributes(member.body);
1572
- if (metadataType === 'Rationale' && fields.text) {
1573
- attrs.rationale = fields.text;
1574
- }
1575
- else if (metadataType === 'StatusInfo' && fields.status) {
1576
- attrs.status = fields.status;
1832
+ const metadataType = member.type?.split('::').pop() ?? '';
1833
+ const aliases = METADATA_KEY_ALIASES[metadataType] ?? {};
1834
+ for (const [field, value] of Object.entries(extractAttributes(member.body))) {
1835
+ if (value === '')
1836
+ continue;
1837
+ attrs[aliases[field] ?? field] = value;
1577
1838
  }
1578
1839
  }
1579
1840
  return attrs;