@hamak/smart-data-dico 1.19.0 → 1.22.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 (98) hide show
  1. package/backend/dist/server.mjs +857 -528
  2. package/frontend/dist/assets/{abnfDiagram-VRR7QNED-5bf36645.js → abnfDiagram-VRR7QNED-6f198720.js} +1 -1
  3. package/frontend/dist/assets/{arc-77b99720.js → arc-54f6dc83.js} +1 -1
  4. package/frontend/dist/assets/architecture-TIHT7OUA-8222218c.js +1 -0
  5. package/frontend/dist/assets/{architectureDiagram-ZJ3FMSHR-842d4b7e.js → architectureDiagram-ZJ3FMSHR-6dabc0f0.js} +1 -1
  6. package/frontend/dist/assets/{blockDiagram-677ZJIJ3-ce2d6065.js → blockDiagram-677ZJIJ3-90ab5147.js} +1 -1
  7. package/frontend/dist/assets/{c4Diagram-LMCZKHZV-f6f33075.js → c4Diagram-LMCZKHZV-b5a971e3.js} +1 -1
  8. package/frontend/dist/assets/channel-98106291.js +1 -0
  9. package/frontend/dist/assets/{chunk-2Q5K7J3B-e048f182.js → chunk-2Q5K7J3B-ac60a5fb.js} +1 -1
  10. package/frontend/dist/assets/{chunk-32BRIVSS-9f8a0696.js → chunk-32BRIVSS-ddb07474.js} +1 -1
  11. package/frontend/dist/assets/{chunk-5VM5RSS4-8207b825.js → chunk-5VM5RSS4-2db6a06c.js} +1 -1
  12. package/frontend/dist/assets/{chunk-EX3LRPZG-4964b359.js → chunk-EX3LRPZG-baa20613.js} +1 -1
  13. package/frontend/dist/assets/{chunk-JWPE2WC7-8cf4213d.js → chunk-JWPE2WC7-18647660.js} +1 -1
  14. package/frontend/dist/assets/{chunk-MOJQB5TN-7c50afe9.js → chunk-MOJQB5TN-dc09b962.js} +1 -1
  15. package/frontend/dist/assets/{chunk-PUDLZKDR-49760488.js → chunk-PUDLZKDR-6287f125.js} +1 -1
  16. package/frontend/dist/assets/{chunk-RYQCIY6F-0690c709.js → chunk-RYQCIY6F-9786102e.js} +1 -1
  17. package/frontend/dist/assets/{chunk-V7JOEXUC-4c92f24d.js → chunk-V7JOEXUC-158db19a.js} +1 -1
  18. package/frontend/dist/assets/{chunk-VR4S4FIN-73477ca7.js → chunk-VR4S4FIN-8a5a7523.js} +1 -1
  19. package/frontend/dist/assets/{chunk-XXDRQBXY-9faa344e.js → chunk-XXDRQBXY-f0aeb3a2.js} +1 -1
  20. package/frontend/dist/assets/classDiagram-OUVF2IWQ-6fddb962.js +1 -0
  21. package/frontend/dist/assets/classDiagram-v2-EOCWNBFH-6fddb962.js +1 -0
  22. package/frontend/dist/assets/{cose-bilkent-JH36ORCC-eca50abe.js → cose-bilkent-JH36ORCC-97aedf6d.js} +1 -1
  23. package/frontend/dist/assets/cynefin-VYW2F7L2-39a09b7c.js +1 -0
  24. package/frontend/dist/assets/{cynefinDiagram-TSTJHNR4-9daa8c60.js → cynefinDiagram-TSTJHNR4-7414b003.js} +1 -1
  25. package/frontend/dist/assets/{dagre-VKFMJZFB-8cadf293.js → dagre-VKFMJZFB-22f0ea2d.js} +1 -1
  26. package/frontend/dist/assets/{diagram-FQU43EPY-5e4d3a2a.js → diagram-FQU43EPY-54e670d8.js} +1 -1
  27. package/frontend/dist/assets/{diagram-G47NLZAW-07630f46.js → diagram-G47NLZAW-41216f63.js} +1 -1
  28. package/frontend/dist/assets/{diagram-NH7WQ7WH-58c6af5e.js → diagram-NH7WQ7WH-2de1d465.js} +1 -1
  29. package/frontend/dist/assets/{diagram-OA4YK3LP-b0124115.js → diagram-OA4YK3LP-4be5e2c2.js} +1 -1
  30. package/frontend/dist/assets/{diagram-WEI45ONY-f942b2f7.js → diagram-WEI45ONY-0d1d5dd3.js} +1 -1
  31. package/frontend/dist/assets/{ebnfDiagram-CCIWWBDH-42fc377c.js → ebnfDiagram-CCIWWBDH-235afe69.js} +1 -1
  32. package/frontend/dist/assets/{erDiagram-Q63AITRT-46209ae5.js → erDiagram-Q63AITRT-bfbc0736.js} +1 -1
  33. package/frontend/dist/assets/eventmodeling-45OFAUF4-c1d13b17.js +1 -0
  34. package/frontend/dist/assets/flowDiagram-23GEKE2U-50c416a3.js +1 -0
  35. package/frontend/dist/assets/{ganttDiagram-NO4QXBWP-e368e881.js → ganttDiagram-NO4QXBWP-d767d5bd.js} +1 -1
  36. package/frontend/dist/assets/gitGraph-TEB2WS4Q-53567b0e.js +1 -0
  37. package/frontend/dist/assets/{gitGraphDiagram-IHSO6WYX-245b8d31.js → gitGraphDiagram-IHSO6WYX-0af7483d.js} +1 -1
  38. package/frontend/dist/assets/{index-419b7a82.css → index-017e890a.css} +1 -1
  39. package/frontend/dist/assets/index-25c4649a.js +574 -0
  40. package/frontend/dist/assets/info-DKCQHKI2-dfc9537f.js +1 -0
  41. package/frontend/dist/assets/{infoDiagram-FWYZ7A6U-af51f32a.js → infoDiagram-FWYZ7A6U-703eef1c.js} +1 -1
  42. package/frontend/dist/assets/{ishikawaDiagram-FXEZZL3T-9f2be8c6.js → ishikawaDiagram-FXEZZL3T-b6e22c6c.js} +1 -1
  43. package/frontend/dist/assets/{journeyDiagram-5HDEW3XC-1e5b0d35.js → journeyDiagram-5HDEW3XC-7ff9040e.js} +1 -1
  44. package/frontend/dist/assets/{kanban-definition-HUTT4EX6-89d91f2e.js → kanban-definition-HUTT4EX6-3ec9df2c.js} +1 -1
  45. package/frontend/dist/assets/{linear-5deaa3d1.js → linear-a840de21.js} +1 -1
  46. package/frontend/dist/assets/{mermaid-parser.core-a7dd4921.js → mermaid-parser.core-35fa1177.js} +2 -2
  47. package/frontend/dist/assets/{mermaid.core-d2196c74.js → mermaid.core-33299d26.js} +3 -3
  48. package/frontend/dist/assets/{mindmap-definition-LN4V7U3C-36c63353.js → mindmap-definition-LN4V7U3C-29c921b2.js} +1 -1
  49. package/frontend/dist/assets/packet-7NZHBO7P-e3add55d.js +1 -0
  50. package/frontend/dist/assets/{pegDiagram-2B236MQR-30a8ce0f.js → pegDiagram-2B236MQR-c9fd64ea.js} +1 -1
  51. package/frontend/dist/assets/pie-RZYD4A2V-3cdcefc3.js +1 -0
  52. package/frontend/dist/assets/{pieDiagram-ENE6RG2P-639c88ff.js → pieDiagram-ENE6RG2P-d21bc176.js} +1 -1
  53. package/frontend/dist/assets/{quadrantDiagram-ABIIQ3AL-6ff9cdb3.js → quadrantDiagram-ABIIQ3AL-b4dc829d.js} +1 -1
  54. package/frontend/dist/assets/radar-I7S5WNFK-29afeb7a.js +1 -0
  55. package/frontend/dist/assets/railroad-3IZDKUUU-6e6a0bb5.js +1 -0
  56. package/frontend/dist/assets/railroad-abnf-AHOZXSZD-8e00f421.js +1 -0
  57. package/frontend/dist/assets/railroad-ebnf-EBAXGLYW-89a4f0dc.js +1 -0
  58. package/frontend/dist/assets/railroad-peg-LSFZ7HO6-b2782816.js +1 -0
  59. package/frontend/dist/assets/{railroadDiagram-RFXS5EU6-ac789252.js → railroadDiagram-RFXS5EU6-404ec27d.js} +1 -1
  60. package/frontend/dist/assets/{requirementDiagram-TGXJPOKE-0ce87e1d.js → requirementDiagram-TGXJPOKE-9b582bdd.js} +1 -1
  61. package/frontend/dist/assets/{sankeyDiagram-HTMAVEWB-4a0abb39.js → sankeyDiagram-HTMAVEWB-f6e0379c.js} +1 -1
  62. package/frontend/dist/assets/{sequenceDiagram-DBY2YBRQ-284dc47a.js → sequenceDiagram-DBY2YBRQ-508afe00.js} +1 -1
  63. package/frontend/dist/assets/{sizeCapture-X5ZJPWSS-253d1130.js → sizeCapture-X5ZJPWSS-ee602857.js} +1 -1
  64. package/frontend/dist/assets/{stateDiagram-2N3HPSRC-5baf911d.js → stateDiagram-2N3HPSRC-1e66e9f0.js} +1 -1
  65. package/frontend/dist/assets/stateDiagram-v2-6OUMAXLB-660e6df7.js +1 -0
  66. package/frontend/dist/assets/{swimlanes-5IMT3BWC-1587bceb.js → swimlanes-5IMT3BWC-5fa2813b.js} +1 -1
  67. package/frontend/dist/assets/swimlanesDiagram-G3AALYLV-f402ef76.js +8 -0
  68. package/frontend/dist/assets/{timeline-definition-FHXFAJF6-644a682a.js → timeline-definition-FHXFAJF6-12edf3b3.js} +1 -1
  69. package/frontend/dist/assets/treeView-QDETBFTQ-144f174a.js +1 -0
  70. package/frontend/dist/assets/treemap-6X3UGDF4-1c06331f.js +1 -0
  71. package/frontend/dist/assets/{vennDiagram-L72KCM5P-27901c69.js → vennDiagram-L72KCM5P-6f3acb6a.js} +1 -1
  72. package/frontend/dist/assets/wardley-OPB4EBWU-abf74bee.js +1 -0
  73. package/frontend/dist/assets/{wardleyDiagram-EHGQE667-7816c0a5.js → wardleyDiagram-EHGQE667-bf6c6922.js} +1 -1
  74. package/frontend/dist/assets/{xychartDiagram-FW5EYKEG-a3ce4246.js → xychartDiagram-FW5EYKEG-3f6dfba6.js} +1 -1
  75. package/frontend/dist/index.html +2 -2
  76. package/package.json +1 -1
  77. package/frontend/dist/assets/architecture-TIHT7OUA-a639eab7.js +0 -1
  78. package/frontend/dist/assets/channel-58a90d4a.js +0 -1
  79. package/frontend/dist/assets/classDiagram-OUVF2IWQ-2985892f.js +0 -1
  80. package/frontend/dist/assets/classDiagram-v2-EOCWNBFH-2985892f.js +0 -1
  81. package/frontend/dist/assets/cynefin-VYW2F7L2-68d8e42f.js +0 -1
  82. package/frontend/dist/assets/eventmodeling-45OFAUF4-ca978038.js +0 -1
  83. package/frontend/dist/assets/flowDiagram-23GEKE2U-e940d995.js +0 -1
  84. package/frontend/dist/assets/gitGraph-TEB2WS4Q-8176fbb9.js +0 -1
  85. package/frontend/dist/assets/index-647c7909.js +0 -559
  86. package/frontend/dist/assets/info-DKCQHKI2-fa612c43.js +0 -1
  87. package/frontend/dist/assets/packet-7NZHBO7P-069cb37a.js +0 -1
  88. package/frontend/dist/assets/pie-RZYD4A2V-58f1df35.js +0 -1
  89. package/frontend/dist/assets/radar-I7S5WNFK-72e8d492.js +0 -1
  90. package/frontend/dist/assets/railroad-3IZDKUUU-c75f2799.js +0 -1
  91. package/frontend/dist/assets/railroad-abnf-AHOZXSZD-c58af3ba.js +0 -1
  92. package/frontend/dist/assets/railroad-ebnf-EBAXGLYW-99be0db3.js +0 -1
  93. package/frontend/dist/assets/railroad-peg-LSFZ7HO6-e22021de.js +0 -1
  94. package/frontend/dist/assets/stateDiagram-v2-6OUMAXLB-f84f63b8.js +0 -1
  95. package/frontend/dist/assets/swimlanesDiagram-G3AALYLV-b6a155f6.js +0 -8
  96. package/frontend/dist/assets/treeView-QDETBFTQ-d9f2bb66.js +0 -1
  97. package/frontend/dist/assets/treemap-6X3UGDF4-3377c08d.js +0 -1
  98. package/frontend/dist/assets/wardley-OPB4EBWU-05517e1a.js +0 -1
@@ -26807,9 +26807,9 @@ var require_range2 = __commonJS({
26807
26807
  parseRange(range) {
26808
26808
  const memoOpts = (this.options.includePrerelease && FLAG_INCLUDE_PRERELEASE) | (this.options.loose && FLAG_LOOSE);
26809
26809
  const memoKey = memoOpts + ":" + range;
26810
- const cached2 = cache2.get(memoKey);
26811
- if (cached2) {
26812
- return cached2;
26810
+ const cached3 = cache2.get(memoKey);
26811
+ if (cached3) {
26812
+ return cached3;
26813
26813
  }
26814
26814
  const loose = this.options.loose;
26815
26815
  const hr = loose ? re2[t.HYPHENRANGELOOSE] : re2[t.HYPHENRANGE];
@@ -37683,7 +37683,8 @@ function normalizeRelationship(rel) {
37683
37683
  }
37684
37684
  function fillAttributeDefaults(entity) {
37685
37685
  const fix = /* @__PURE__ */ __name((attrs) => {
37686
- for (const a of attrs ?? []) {
37686
+ if (!Array.isArray(attrs)) return;
37687
+ for (const a of attrs) {
37687
37688
  if (a.required === void 0) a.required = false;
37688
37689
  if (a.description === void 0) a.description = "";
37689
37690
  if (a.properties) fix(a.properties);
@@ -44015,10 +44016,10 @@ var init_git_file_info_enricher = __esm({
44015
44016
  * Get cached status or fetch new one
44016
44017
  */
44017
44018
  async getCachedStatus(cacheKey, workspaceId, relativePath) {
44018
- const cached2 = this.statusCache.get(cacheKey);
44019
+ const cached3 = this.statusCache.get(cacheKey);
44019
44020
  const now2 = Date.now();
44020
- if (cached2 && now2 - cached2.timestamp < this.cacheTimeout) {
44021
- return cached2.status;
44021
+ if (cached3 && now2 - cached3.timestamp < this.cacheTimeout) {
44022
+ return cached3.status;
44022
44023
  }
44023
44024
  try {
44024
44025
  const status = await this.gitService.getStatus(workspaceId, relativePath);
@@ -44603,8 +44604,8 @@ function invalidatePackageCache(packageName) {
44603
44604
  else _packageCache.delete(packageName);
44604
44605
  }
44605
44606
  async function loadPackage(packageName) {
44606
- const cached2 = _packageCache.get(packageName);
44607
- if (cached2 && cached2.expires > Date.now()) return cached2.model;
44607
+ const cached3 = _packageCache.get(packageName);
44608
+ if (cached3 && cached3.expires > Date.now()) return cached3.model;
44608
44609
  const files = await listPackageYamlFilePaths(packageName);
44609
44610
  const parsed = await Promise.all(
44610
44611
  files.map(async (p) => ({
@@ -45760,7 +45761,7 @@ var init_dictionaryService = __esm({
45760
45761
  let relationships = [];
45761
45762
  let cases = [];
45762
45763
  try {
45763
- const model = await loadPackage(packageName);
45764
+ const model = await loadPackage(String(dirPath));
45764
45765
  entities = model.entities;
45765
45766
  relationships = model.relationships;
45766
45767
  cases = model.cases.map((c) => ({
@@ -48507,9 +48508,25 @@ var init_serviceService = __esm({
48507
48508
  * user to disambiguate.
48508
48509
  */
48509
48510
  async findEntityAcrossPackages(entityName, preferredPackage) {
48511
+ const matches = await this.findEntityMatches(entityName, preferredPackage);
48512
+ if (matches.length === 0) return null;
48513
+ if (matches.length === 1) return matches[0];
48514
+ const where = matches.map((m) => m.packageName).join(", ");
48515
+ throw new Error(
48516
+ `Entity "${entityName}" exists in multiple packages: ${where}. Specify the package explicitly.`
48517
+ );
48518
+ }
48519
+ /**
48520
+ * All `{ entity, packageName }` matches for a name. A hit in
48521
+ * `preferredPackage` wins outright (single match, no wider scan) —
48522
+ * same precedence findEntityAcrossPackages always had. Callers that
48523
+ * want a disambiguation list instead of a thrown ambiguity error
48524
+ * (e.g. the AI getEntityDetails tool) consume this directly.
48525
+ */
48526
+ async findEntityMatches(entityName, preferredPackage) {
48510
48527
  if (preferredPackage) {
48511
48528
  const e = await this.getEntitySchema(preferredPackage, entityName);
48512
- if (e) return { entity: e, packageName: preferredPackage };
48529
+ if (e) return [{ entity: e, packageName: preferredPackage }];
48513
48530
  }
48514
48531
  const allPackages = await listMicroservices();
48515
48532
  const matches = [];
@@ -48518,12 +48535,7 @@ var init_serviceService = __esm({
48518
48535
  const e = await this.getEntitySchema(p, entityName);
48519
48536
  if (e) matches.push({ entity: e, packageName: p });
48520
48537
  }
48521
- if (matches.length === 0) return null;
48522
- if (matches.length === 1) return matches[0];
48523
- const where = matches.map((m) => m.packageName).join(", ");
48524
- throw new Error(
48525
- `Entity "${entityName}" exists in multiple packages: ${where}. Specify the package explicitly.`
48526
- );
48538
+ return matches;
48527
48539
  }
48528
48540
  async createEntity(service, entity) {
48529
48541
  logger.info(`Creating entity: ${service}.${entity.name}`);
@@ -48645,6 +48657,51 @@ var init_serviceService = __esm({
48645
48657
  };
48646
48658
  }
48647
48659
  }
48660
+ /**
48661
+ * Move an entity from one package to another (#move-entity). The entity keeps
48662
+ * its UUID, so every UUID-based reference (relationships — cross-package is
48663
+ * first-class — cases, diagrams, perspectives) keeps resolving; nothing is
48664
+ * orphaned. We write the entity into the target package, then remove it from
48665
+ * the source via the projection directly — this bypasses deleteEntity's
48666
+ * same-package relationship guard, which is meant for destruction, not a
48667
+ * relocation where the references remain valid.
48668
+ */
48669
+ async moveEntity(sourcePackage, entityName, targetPackage) {
48670
+ logger.info(`Moving entity: ${sourcePackage}.${entityName} -> ${targetPackage}`);
48671
+ if (!targetPackage || targetPackage === sourcePackage) {
48672
+ return { success: false, errors: ["Target package must differ from the source package"] };
48673
+ }
48674
+ try {
48675
+ const entity = await readEntityFile(sourcePackage, entityName);
48676
+ if (!entity) {
48677
+ return { success: false, errors: [`Entity ${sourcePackage}.${entityName} not found`] };
48678
+ }
48679
+ const packages = await listMicroservices();
48680
+ if (!packages.includes(targetPackage)) {
48681
+ return { success: false, errors: [`Target package '${targetPackage}' does not exist`] };
48682
+ }
48683
+ const existingInTarget = await readEntityFile(targetPackage, entityName);
48684
+ if (existingInTarget) {
48685
+ return {
48686
+ success: false,
48687
+ errors: [`Target package '${targetPackage}' already has an entity named '${entityName}'`]
48688
+ };
48689
+ }
48690
+ const projection = getProjection(wsId("dictionaries"));
48691
+ entity.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
48692
+ await projection.writeEntity(`packages/${targetPackage}/entities/${entity.name}`, entity);
48693
+ const removed = await projection.deleteEntity(`packages/${sourcePackage}/entities/${entity.name}`);
48694
+ if (!removed) {
48695
+ await projection.deleteEntity(`packages/${targetPackage}/entities/${entity.name}`).catch(() => {
48696
+ });
48697
+ return { success: false, errors: ["Failed to remove the entity from the source package"] };
48698
+ }
48699
+ return { success: true, errors: [] };
48700
+ } catch (error48) {
48701
+ logger.error(`Error moving entity: ${error48}`);
48702
+ return { success: false, errors: [`Error moving entity: ${error48}`] };
48703
+ }
48704
+ }
48648
48705
  // --- Relationship CRUD ---
48649
48706
  async getPackageRelationships(packageName) {
48650
48707
  const packagePath = getPackagePath(packageName);
@@ -49107,424 +49164,6 @@ var init_serviceService = __esm({
49107
49164
  }
49108
49165
  });
49109
49166
 
49110
- // backend/src/models/Rule.ts
49111
- function validateRule(rule) {
49112
- const errors = [];
49113
- if (!rule.name || typeof rule.name !== "string") {
49114
- errors.push("Rule name is required");
49115
- } else if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(rule.name)) {
49116
- errors.push("Rule name must be kebab-case (lowercase letters, numbers, hyphens)");
49117
- }
49118
- if (!rule.description || typeof rule.description !== "string") {
49119
- errors.push("Rule description is required");
49120
- }
49121
- if (!rule.severity || !["info", "warning", "error"].includes(rule.severity)) {
49122
- errors.push("Rule severity must be one of: info, warning, error");
49123
- }
49124
- if (!rule.enforcement || !["save", "process", "advisory"].includes(rule.enforcement)) {
49125
- errors.push("Rule enforcement must be one of: save, process, advisory");
49126
- }
49127
- if (!rule.scope || !["entity", "package", "case", "global"].includes(rule.scope)) {
49128
- errors.push("Rule scope must be one of: entity, package, case, global");
49129
- }
49130
- if (!Array.isArray(rule.targets) || rule.targets.length === 0) {
49131
- errors.push("Rule must have at least one target");
49132
- }
49133
- if (rule.scope === "entity" && !rule.entityUuid) {
49134
- errors.push("Entity-scoped rules must specify entityUuid");
49135
- }
49136
- if (rule.scope === "package" && !rule.packageName) {
49137
- errors.push("Package-scoped rules must specify packageName");
49138
- }
49139
- if (rule.scope === "case" && !rule.caseUuid) {
49140
- errors.push("Case-scoped rules must specify caseUuid");
49141
- }
49142
- if (rule.enforcement === "process") {
49143
- const stageField = (rule.metadata || []).find((m) => m.name === "process-stage-field");
49144
- if (!stageField || !stageField.value) {
49145
- errors.push("Process-enforcement rules must reference a metadata field via process-stage-field");
49146
- }
49147
- }
49148
- return errors;
49149
- }
49150
- var init_Rule = __esm({
49151
- "backend/src/models/Rule.ts"() {
49152
- "use strict";
49153
- __name(validateRule, "validateRule");
49154
- }
49155
- });
49156
-
49157
- // backend/src/services/ruleService.ts
49158
- var ruleService_exports = {};
49159
- __export(ruleService_exports, {
49160
- ruleService: () => ruleService
49161
- });
49162
- var RuleService, ruleService;
49163
- var init_ruleService = __esm({
49164
- "backend/src/services/ruleService.ts"() {
49165
- "use strict";
49166
- init_Rule();
49167
- init_fileOperations();
49168
- init_uuid();
49169
- init_logger();
49170
- init_ProjectionRegistry();
49171
- init_types();
49172
- RuleService = class {
49173
- static {
49174
- __name(this, "RuleService");
49175
- }
49176
- /** List all rules from all storage scopes, optionally filtered. */
49177
- async listRules(filters = {}) {
49178
- const all = [];
49179
- if (!filters.scope || filters.scope === "entity") {
49180
- const files = await listAllEntityRuleFiles();
49181
- for (const { service, entityUuid } of files) {
49182
- const rules = await readEntityRules(service, entityUuid);
49183
- all.push(...rules);
49184
- }
49185
- }
49186
- if (!filters.scope || filters.scope === "package") {
49187
- const packages = filters.packageName ? [filters.packageName] : await listPackagesWithRules();
49188
- for (const pkg of packages) {
49189
- const rules = await readPackageRules(pkg);
49190
- all.push(...rules);
49191
- }
49192
- }
49193
- if (!filters.scope || filters.scope === "global") {
49194
- const globalRules = await readGlobalRules();
49195
- all.push(...globalRules);
49196
- }
49197
- if (!filters.scope || filters.scope === "case") {
49198
- if (filters.caseUuid) {
49199
- const rules = await readCaseRules(filters.caseUuid);
49200
- all.push(...rules);
49201
- } else {
49202
- const cases = await listCases();
49203
- for (const c of cases) {
49204
- const rules = c.rules || [];
49205
- all.push(...rules);
49206
- }
49207
- }
49208
- }
49209
- for (const rule of all) {
49210
- if (!rule.enforcement) rule.enforcement = "advisory";
49211
- }
49212
- return all.filter((rule) => {
49213
- if (filters.severity && rule.severity !== filters.severity) return false;
49214
- if (filters.enforcement && rule.enforcement !== filters.enforcement) return false;
49215
- if (filters.targetUuid) {
49216
- const hit = rule.targets.some(
49217
- (t) => t.uuid === filters.targetUuid || t.entityUuid === filters.targetUuid
49218
- );
49219
- if (!hit) return false;
49220
- }
49221
- return true;
49222
- });
49223
- }
49224
- /** Get a single rule by uuid (scans all scopes). */
49225
- async getRule(uuid3) {
49226
- const all = await this.listRules();
49227
- return all.find((r) => r.uuid === uuid3) || null;
49228
- }
49229
- /** List rules whose targets include this entity (or any of its attributes). */
49230
- async listRulesForEntity(entityUuid) {
49231
- const all = await this.listRules();
49232
- return all.filter(
49233
- (rule) => rule.targets.some(
49234
- (t) => t.uuid === entityUuid || t.entityUuid === entityUuid
49235
- )
49236
- );
49237
- }
49238
- /** Create a rule. Storage location is determined by `scope`. */
49239
- async createRule(input) {
49240
- if (!input.enforcement) input.enforcement = "advisory";
49241
- const errors = validateRule(input);
49242
- if (errors.length > 0) {
49243
- return { success: false, errors };
49244
- }
49245
- let rule = {
49246
- uuid: generateUUID(),
49247
- name: input.name,
49248
- description: input.description,
49249
- severity: input.severity,
49250
- enforcement: input.enforcement,
49251
- scope: input.scope,
49252
- targets: input.targets,
49253
- packageName: input.packageName,
49254
- entityUuid: input.entityUuid,
49255
- caseUuid: input.caseUuid,
49256
- expression: input.expression,
49257
- tags: input.tags,
49258
- metadata: input.metadata,
49259
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
49260
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
49261
- };
49262
- rule = await this.applyAutoScope(rule);
49263
- const saved = await this.saveRuleToScope(rule);
49264
- if (!saved) return { success: false, errors: ["Failed to write rule to storage"] };
49265
- return { success: true, rule };
49266
- }
49267
- /** Update an existing rule by uuid. Re-routes to new scope if scope changed. */
49268
- async updateRule(uuid3, input) {
49269
- const existing = await this.getRule(uuid3);
49270
- if (!existing) return { success: false, errors: [`Rule ${uuid3} not found`] };
49271
- let merged = {
49272
- ...existing,
49273
- ...input,
49274
- uuid: existing.uuid,
49275
- createdAt: existing.createdAt,
49276
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
49277
- };
49278
- const errors = validateRule(merged);
49279
- if (errors.length > 0) return { success: false, errors };
49280
- merged = await this.applyAutoScope(merged, existing.scope, existing.packageName);
49281
- const scopeMoved = existing.scope !== merged.scope || existing.packageName !== merged.packageName || existing.entityUuid !== merged.entityUuid || existing.caseUuid !== merged.caseUuid;
49282
- if (scopeMoved) {
49283
- await this.removeRuleFromScope(existing);
49284
- }
49285
- const saved = await this.saveRuleToScope(merged);
49286
- if (!saved) return { success: false, errors: ["Failed to write updated rule to storage"] };
49287
- return { success: true, rule: merged };
49288
- }
49289
- /** Delete a rule by uuid. */
49290
- async deleteRule(uuid3) {
49291
- const existing = await this.getRule(uuid3);
49292
- if (!existing) return { success: false, errors: [`Rule ${uuid3} not found`] };
49293
- const removed = await this.removeRuleFromScope(existing);
49294
- if (!removed) return { success: false, errors: ["Failed to remove rule from storage"] };
49295
- return { success: true };
49296
- }
49297
- // ─── Private helpers: route to the right storage location ────────────────
49298
- async saveRuleToScope(rule) {
49299
- const projection = getProjection(wsId("dictionaries"));
49300
- switch (rule.scope) {
49301
- case "entity": {
49302
- if (!rule.entityUuid) return false;
49303
- const files = await listAllEntityRuleFiles();
49304
- let service = files.find((f) => f.entityUuid === rule.entityUuid)?.service;
49305
- if (!service) service = rule.packageName;
49306
- if (!service) {
49307
- logger.error(`Cannot determine service for entity ${rule.entityUuid}`);
49308
- return false;
49309
- }
49310
- const entity = await this.findEntityByUuid(service, rule.entityUuid);
49311
- if (!entity) {
49312
- logger.error(`Entity ${rule.entityUuid} not found in service ${service}`);
49313
- return false;
49314
- }
49315
- const existing = entity.rules || [];
49316
- const updatedRules = [...existing.filter((r) => r.uuid !== rule.uuid), rule];
49317
- entity.rules = updatedRules.length > 0 ? updatedRules : void 0;
49318
- const logicalPath = `packages/${service}/entities/${entity.name}`;
49319
- try {
49320
- await projection.writeEntity(logicalPath, entity);
49321
- return true;
49322
- } catch (e) {
49323
- logger.error(`Failed to write entity-scope rule via projection: ${e.message}`);
49324
- return false;
49325
- }
49326
- }
49327
- case "package": {
49328
- if (!rule.packageName) return false;
49329
- const existing = await readPackageRules(rule.packageName);
49330
- const updated = [...existing.filter((r) => r.uuid !== rule.uuid), rule];
49331
- const packageLogicalPath = `packages/${rule.packageName}`;
49332
- try {
49333
- await projection.writePackageRules(packageLogicalPath, updated);
49334
- return true;
49335
- } catch (e) {
49336
- logger.error(`Failed to write package-scope rules via projection: ${e.message}`);
49337
- return false;
49338
- }
49339
- }
49340
- case "case": {
49341
- if (!rule.caseUuid) return false;
49342
- const c = await readCaseFile(rule.caseUuid);
49343
- if (!c) return false;
49344
- const existing = c.rules || [];
49345
- c.rules = [...existing.filter((r) => r.uuid !== rule.uuid), rule];
49346
- c.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
49347
- const targetPackage = await this.resolveCaseHomePackage(c);
49348
- if (!targetPackage) return false;
49349
- const caseLogicalPath = `packages/${targetPackage}/cases/${c.name}`;
49350
- try {
49351
- await projection.writeCase(caseLogicalPath, c);
49352
- return true;
49353
- } catch (e) {
49354
- logger.error(`Failed to write case-scope rule via projection: ${e.message}`);
49355
- return false;
49356
- }
49357
- }
49358
- case "global": {
49359
- const existing = await readGlobalRules();
49360
- const updated = [...existing.filter((r) => r.uuid !== rule.uuid), rule];
49361
- try {
49362
- await projection.writeGlobalRules(updated);
49363
- return true;
49364
- } catch (e) {
49365
- logger.error(`Failed to write global rules via projection: ${e.message}`);
49366
- return false;
49367
- }
49368
- }
49369
- default:
49370
- return false;
49371
- }
49372
- }
49373
- async removeRuleFromScope(rule) {
49374
- const projection = getProjection(wsId("dictionaries"));
49375
- switch (rule.scope) {
49376
- case "entity": {
49377
- if (!rule.entityUuid) return false;
49378
- const files = await listAllEntityRuleFiles();
49379
- const service = files.find((f) => f.entityUuid === rule.entityUuid)?.service ?? rule.packageName;
49380
- if (!service) return false;
49381
- const entity = await this.findEntityByUuid(service, rule.entityUuid);
49382
- if (!entity) return false;
49383
- const existing = entity.rules || [];
49384
- const updatedRules = existing.filter((r) => r.uuid !== rule.uuid);
49385
- entity.rules = updatedRules.length > 0 ? updatedRules : void 0;
49386
- const logicalPath = `packages/${service}/entities/${entity.name}`;
49387
- try {
49388
- await projection.writeEntity(logicalPath, entity);
49389
- return true;
49390
- } catch (e) {
49391
- logger.error(`Failed to remove entity-scope rule via projection: ${e.message}`);
49392
- return false;
49393
- }
49394
- }
49395
- case "package": {
49396
- if (!rule.packageName) return false;
49397
- const existing = await readPackageRules(rule.packageName);
49398
- const updated = existing.filter((r) => r.uuid !== rule.uuid);
49399
- const packageLogicalPath = `packages/${rule.packageName}`;
49400
- try {
49401
- await projection.writePackageRules(packageLogicalPath, updated);
49402
- return true;
49403
- } catch (e) {
49404
- logger.error(`Failed to remove package-scope rule via projection: ${e.message}`);
49405
- return false;
49406
- }
49407
- }
49408
- case "case": {
49409
- if (!rule.caseUuid) return false;
49410
- const c = await readCaseFile(rule.caseUuid);
49411
- if (!c) return false;
49412
- const existing = c.rules || [];
49413
- const updated = existing.filter((r) => r.uuid !== rule.uuid);
49414
- c.rules = updated.length > 0 ? updated : void 0;
49415
- c.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
49416
- const targetPackage = await this.resolveCaseHomePackage(c);
49417
- if (!targetPackage) return false;
49418
- const caseLogicalPath = `packages/${targetPackage}/cases/${c.name}`;
49419
- try {
49420
- await projection.writeCase(caseLogicalPath, c);
49421
- return true;
49422
- } catch (e) {
49423
- logger.error(`Failed to remove case-scope rule via projection: ${e.message}`);
49424
- return false;
49425
- }
49426
- }
49427
- case "global": {
49428
- const existing = await readGlobalRules();
49429
- const updated = existing.filter((r) => r.uuid !== rule.uuid);
49430
- try {
49431
- await projection.writeGlobalRules(updated);
49432
- return true;
49433
- } catch (e) {
49434
- logger.error(`Failed to remove global rule via projection: ${e.message}`);
49435
- return false;
49436
- }
49437
- }
49438
- default:
49439
- return false;
49440
- }
49441
- }
49442
- /**
49443
- * Load an entity from a package by uuid. Returns null if not found.
49444
- * Used by entity-scope rule writes to round-trip the parent entity
49445
- * through `projection.writeEntity`.
49446
- */
49447
- async findEntityByUuid(service, entityUuid) {
49448
- try {
49449
- const pkg = await loadPackage(service);
49450
- return pkg.entities.find((e) => e.uuid === entityUuid) ?? null;
49451
- } catch (error48) {
49452
- logger.error(`Error loading package ${service} for entity lookup: ${error48}`);
49453
- return null;
49454
- }
49455
- }
49456
- /**
49457
- * Resolve the home package for a case so we can construct a
49458
- * `packages/<pkg>/cases/<name>` logical path. Mirrors the heuristic in
49459
- * caseService.ts / fileOperations.ts: existing-owner lookup by uuid first,
49460
- * then first root entity's package, then first package on disk.
49461
- */
49462
- async resolveCaseHomePackage(c) {
49463
- const { listPackages: listPackages3 } = await Promise.resolve().then(() => (init_fileOperations(), fileOperations_exports));
49464
- const packages = await listPackages3();
49465
- for (const pkg of packages) {
49466
- try {
49467
- const model = await loadPackage(pkg);
49468
- if (model.ownership.caseByUuid.has(c.uuid)) return pkg;
49469
- } catch {
49470
- }
49471
- }
49472
- if (c.rootEntities && c.rootEntities.length > 0) {
49473
- const rootUuid = c.rootEntities[0];
49474
- for (const pkg of packages) {
49475
- try {
49476
- const model = await loadPackage(pkg);
49477
- if (model.ownership.entityByUuid.has(rootUuid)) return pkg;
49478
- } catch {
49479
- }
49480
- }
49481
- }
49482
- return packages[0] || null;
49483
- }
49484
- // ─── Auto-promote / auto-demote (#75) ────────────────────────────────
49485
- /**
49486
- * Determine the correct scope for a rule based on its targets.
49487
- * Called before save. If targets span 2+ packages → global.
49488
- * If all targets are in one package → that package's scope (keep
49489
- * the caller's intent for entity vs package).
49490
- */
49491
- resolveScope(rule) {
49492
- const packages = /* @__PURE__ */ new Set();
49493
- for (const t of rule.targets) {
49494
- if (t.packageName) packages.add(t.packageName);
49495
- }
49496
- if (packages.size >= 2) {
49497
- return { scope: "global" };
49498
- }
49499
- if (packages.size === 1) {
49500
- const pkg = [...packages][0];
49501
- if (rule.scope === "global") {
49502
- return { scope: "package", packageName: pkg };
49503
- }
49504
- }
49505
- return { scope: rule.scope, packageName: rule.packageName };
49506
- }
49507
- /**
49508
- * Wrap around createRule/updateRule to apply auto-promote/demote.
49509
- * Checks if the resolved scope differs from what the caller requested
49510
- * and adjusts accordingly — including moving the rule between files.
49511
- */
49512
- async applyAutoScope(rule, _oldScope, _oldPkg) {
49513
- const resolved = this.resolveScope(rule);
49514
- if (resolved.scope !== rule.scope) {
49515
- logger.info(`Auto-${resolved.scope === "global" ? "promote" : "demote"} rule ${rule.uuid}: ${rule.scope} \u2192 ${resolved.scope}`);
49516
- }
49517
- rule.scope = resolved.scope;
49518
- if (resolved.packageName !== void 0) {
49519
- rule.packageName = resolved.packageName;
49520
- }
49521
- return rule;
49522
- }
49523
- };
49524
- ruleService = new RuleService();
49525
- }
49526
- });
49527
-
49528
49167
  // backend/node_modules/zod/v4/core/core.js
49529
49168
  // @__NO_SIDE_EFFECTS__
49530
49169
  function $constructor(name21, initializer3, params) {
@@ -50508,7 +50147,7 @@ var init_errors = __esm({
50508
50147
  });
50509
50148
 
50510
50149
  // backend/node_modules/zod/v4/core/parse.js
50511
- var _parse, parse4, _parseAsync, parseAsync, _safeParse, safeParse, _safeParseAsync, safeParseAsync, _encode, encode, _decode, decode, _encodeAsync, encodeAsync, _decodeAsync, decodeAsync, _safeEncode, safeEncode, _safeDecode, safeDecode, _safeEncodeAsync, safeEncodeAsync, _safeDecodeAsync, safeDecodeAsync;
50150
+ var _parse, parse3, _parseAsync, parseAsync, _safeParse, safeParse, _safeParseAsync, safeParseAsync, _encode, encode, _decode, decode, _encodeAsync, encodeAsync, _decodeAsync, decodeAsync, _safeEncode, safeEncode, _safeDecode, safeDecode, _safeEncodeAsync, safeEncodeAsync, _safeDecodeAsync, safeDecodeAsync;
50512
50151
  var init_parse = __esm({
50513
50152
  "backend/node_modules/zod/v4/core/parse.js"() {
50514
50153
  init_core();
@@ -50527,7 +50166,7 @@ var init_parse = __esm({
50527
50166
  }
50528
50167
  return result.value;
50529
50168
  }, "_parse");
50530
- parse4 = /* @__PURE__ */ _parse($ZodRealError);
50169
+ parse3 = /* @__PURE__ */ _parse($ZodRealError);
50531
50170
  _parseAsync = /* @__PURE__ */ __name((_Err) => async (schema, value, _ctx, params) => {
50532
50171
  const ctx = _ctx ? Object.assign(_ctx, { async: true }) : { async: true };
50533
50172
  let result = schema._zod.run({ value, issues: [] }, ctx);
@@ -53285,10 +52924,10 @@ var init_schemas = __esm({
53285
52924
  throw new Error("implement() must be called with a function");
53286
52925
  }
53287
52926
  return function(...args) {
53288
- const parsedArgs = inst._def.input ? parse4(inst._def.input, args) : args;
52927
+ const parsedArgs = inst._def.input ? parse3(inst._def.input, args) : args;
53289
52928
  const result = Reflect.apply(func, this, parsedArgs);
53290
52929
  if (inst._def.output) {
53291
- return parse4(inst._def.output, result);
52930
+ return parse3(inst._def.output, result);
53292
52931
  }
53293
52932
  return result;
53294
52933
  };
@@ -61906,7 +61545,7 @@ __export(core_exports2, {
61906
61545
  isValidJWT: () => isValidJWT,
61907
61546
  locales: () => locales_exports,
61908
61547
  meta: () => meta,
61909
- parse: () => parse4,
61548
+ parse: () => parse3,
61910
61549
  parseAsync: () => parseAsync,
61911
61550
  prettifyError: () => prettifyError,
61912
61551
  process: () => process2,
@@ -62085,12 +61724,12 @@ var init_errors2 = __esm({
62085
61724
  });
62086
61725
 
62087
61726
  // backend/node_modules/zod/v4/classic/parse.js
62088
- var parse5, parseAsync2, safeParse2, safeParseAsync2, encode2, decode2, encodeAsync2, decodeAsync2, safeEncode2, safeDecode2, safeEncodeAsync2, safeDecodeAsync2;
61727
+ var parse4, parseAsync2, safeParse2, safeParseAsync2, encode2, decode2, encodeAsync2, decodeAsync2, safeEncode2, safeDecode2, safeEncodeAsync2, safeDecodeAsync2;
62089
61728
  var init_parse2 = __esm({
62090
61729
  "backend/node_modules/zod/v4/classic/parse.js"() {
62091
61730
  init_core2();
62092
61731
  init_errors2();
62093
- parse5 = /* @__PURE__ */ _parse(ZodRealError);
61732
+ parse4 = /* @__PURE__ */ _parse(ZodRealError);
62094
61733
  parseAsync2 = /* @__PURE__ */ _parseAsync(ZodRealError);
62095
61734
  safeParse2 = /* @__PURE__ */ _safeParse(ZodRealError);
62096
61735
  safeParseAsync2 = /* @__PURE__ */ _safeParseAsync(ZodRealError);
@@ -62766,7 +62405,7 @@ var init_schemas2 = __esm({
62766
62405
  reg.add(inst, meta3);
62767
62406
  return inst;
62768
62407
  });
62769
- inst.parse = (data, params) => parse5(inst, data, params, { callee: inst.parse });
62408
+ inst.parse = (data, params) => parse4(inst, data, params, { callee: inst.parse });
62770
62409
  inst.safeParse = (data, params) => safeParse2(inst, data, params);
62771
62410
  inst.parseAsync = async (data, params) => parseAsync2(inst, data, params, { callee: inst.parseAsync });
62772
62411
  inst.safeParseAsync = async (data, params) => safeParseAsync2(inst, data, params);
@@ -64193,7 +63832,7 @@ __export(external_exports, {
64193
63832
  object: () => object,
64194
63833
  optional: () => optional,
64195
63834
  overwrite: () => _overwrite,
64196
- parse: () => parse5,
63835
+ parse: () => parse4,
64197
63836
  parseAsync: () => parseAsync2,
64198
63837
  partialRecord: () => partialRecord,
64199
63838
  pipe: () => pipe,
@@ -64308,6 +63947,424 @@ var init_agentToolRegistry = __esm({
64308
63947
  }
64309
63948
  });
64310
63949
 
63950
+ // backend/src/models/Rule.ts
63951
+ function validateRule(rule) {
63952
+ const errors = [];
63953
+ if (!rule.name || typeof rule.name !== "string") {
63954
+ errors.push("Rule name is required");
63955
+ } else if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(rule.name)) {
63956
+ errors.push("Rule name must be kebab-case (lowercase letters, numbers, hyphens)");
63957
+ }
63958
+ if (!rule.description || typeof rule.description !== "string") {
63959
+ errors.push("Rule description is required");
63960
+ }
63961
+ if (!rule.severity || !["info", "warning", "error"].includes(rule.severity)) {
63962
+ errors.push("Rule severity must be one of: info, warning, error");
63963
+ }
63964
+ if (!rule.enforcement || !["save", "process", "advisory"].includes(rule.enforcement)) {
63965
+ errors.push("Rule enforcement must be one of: save, process, advisory");
63966
+ }
63967
+ if (!rule.scope || !["entity", "package", "case", "global"].includes(rule.scope)) {
63968
+ errors.push("Rule scope must be one of: entity, package, case, global");
63969
+ }
63970
+ if (!Array.isArray(rule.targets) || rule.targets.length === 0) {
63971
+ errors.push("Rule must have at least one target");
63972
+ }
63973
+ if (rule.scope === "entity" && !rule.entityUuid) {
63974
+ errors.push("Entity-scoped rules must specify entityUuid");
63975
+ }
63976
+ if (rule.scope === "package" && !rule.packageName) {
63977
+ errors.push("Package-scoped rules must specify packageName");
63978
+ }
63979
+ if (rule.scope === "case" && !rule.caseUuid) {
63980
+ errors.push("Case-scoped rules must specify caseUuid");
63981
+ }
63982
+ if (rule.enforcement === "process") {
63983
+ const stageField = (rule.metadata || []).find((m) => m.name === "process-stage-field");
63984
+ if (!stageField || !stageField.value) {
63985
+ errors.push("Process-enforcement rules must reference a metadata field via process-stage-field");
63986
+ }
63987
+ }
63988
+ return errors;
63989
+ }
63990
+ var init_Rule = __esm({
63991
+ "backend/src/models/Rule.ts"() {
63992
+ "use strict";
63993
+ __name(validateRule, "validateRule");
63994
+ }
63995
+ });
63996
+
63997
+ // backend/src/services/ruleService.ts
63998
+ var ruleService_exports = {};
63999
+ __export(ruleService_exports, {
64000
+ ruleService: () => ruleService
64001
+ });
64002
+ var RuleService, ruleService;
64003
+ var init_ruleService = __esm({
64004
+ "backend/src/services/ruleService.ts"() {
64005
+ "use strict";
64006
+ init_Rule();
64007
+ init_fileOperations();
64008
+ init_uuid();
64009
+ init_logger();
64010
+ init_ProjectionRegistry();
64011
+ init_types();
64012
+ RuleService = class {
64013
+ static {
64014
+ __name(this, "RuleService");
64015
+ }
64016
+ /** List all rules from all storage scopes, optionally filtered. */
64017
+ async listRules(filters = {}) {
64018
+ const all = [];
64019
+ if (!filters.scope || filters.scope === "entity") {
64020
+ const files = await listAllEntityRuleFiles();
64021
+ for (const { service, entityUuid } of files) {
64022
+ const rules = await readEntityRules(service, entityUuid);
64023
+ all.push(...rules);
64024
+ }
64025
+ }
64026
+ if (!filters.scope || filters.scope === "package") {
64027
+ const packages = filters.packageName ? [filters.packageName] : await listPackagesWithRules();
64028
+ for (const pkg of packages) {
64029
+ const rules = await readPackageRules(pkg);
64030
+ all.push(...rules);
64031
+ }
64032
+ }
64033
+ if (!filters.scope || filters.scope === "global") {
64034
+ const globalRules = await readGlobalRules();
64035
+ all.push(...globalRules);
64036
+ }
64037
+ if (!filters.scope || filters.scope === "case") {
64038
+ if (filters.caseUuid) {
64039
+ const rules = await readCaseRules(filters.caseUuid);
64040
+ all.push(...rules);
64041
+ } else {
64042
+ const cases = await listCases();
64043
+ for (const c of cases) {
64044
+ const rules = c.rules || [];
64045
+ all.push(...rules);
64046
+ }
64047
+ }
64048
+ }
64049
+ for (const rule of all) {
64050
+ if (!rule.enforcement) rule.enforcement = "advisory";
64051
+ }
64052
+ return all.filter((rule) => {
64053
+ if (filters.severity && rule.severity !== filters.severity) return false;
64054
+ if (filters.enforcement && rule.enforcement !== filters.enforcement) return false;
64055
+ if (filters.targetUuid) {
64056
+ const hit = rule.targets.some(
64057
+ (t) => t.uuid === filters.targetUuid || t.entityUuid === filters.targetUuid
64058
+ );
64059
+ if (!hit) return false;
64060
+ }
64061
+ return true;
64062
+ });
64063
+ }
64064
+ /** Get a single rule by uuid (scans all scopes). */
64065
+ async getRule(uuid3) {
64066
+ const all = await this.listRules();
64067
+ return all.find((r) => r.uuid === uuid3) || null;
64068
+ }
64069
+ /** List rules whose targets include this entity (or any of its attributes). */
64070
+ async listRulesForEntity(entityUuid) {
64071
+ const all = await this.listRules();
64072
+ return all.filter(
64073
+ (rule) => rule.targets.some(
64074
+ (t) => t.uuid === entityUuid || t.entityUuid === entityUuid
64075
+ )
64076
+ );
64077
+ }
64078
+ /** Create a rule. Storage location is determined by `scope`. */
64079
+ async createRule(input) {
64080
+ if (!input.enforcement) input.enforcement = "advisory";
64081
+ const errors = validateRule(input);
64082
+ if (errors.length > 0) {
64083
+ return { success: false, errors };
64084
+ }
64085
+ let rule = {
64086
+ uuid: generateUUID(),
64087
+ name: input.name,
64088
+ description: input.description,
64089
+ severity: input.severity,
64090
+ enforcement: input.enforcement,
64091
+ scope: input.scope,
64092
+ targets: input.targets,
64093
+ packageName: input.packageName,
64094
+ entityUuid: input.entityUuid,
64095
+ caseUuid: input.caseUuid,
64096
+ expression: input.expression,
64097
+ tags: input.tags,
64098
+ metadata: input.metadata,
64099
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
64100
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
64101
+ };
64102
+ rule = await this.applyAutoScope(rule);
64103
+ const saved = await this.saveRuleToScope(rule);
64104
+ if (!saved) return { success: false, errors: ["Failed to write rule to storage"] };
64105
+ return { success: true, rule };
64106
+ }
64107
+ /** Update an existing rule by uuid. Re-routes to new scope if scope changed. */
64108
+ async updateRule(uuid3, input) {
64109
+ const existing = await this.getRule(uuid3);
64110
+ if (!existing) return { success: false, errors: [`Rule ${uuid3} not found`] };
64111
+ let merged = {
64112
+ ...existing,
64113
+ ...input,
64114
+ uuid: existing.uuid,
64115
+ createdAt: existing.createdAt,
64116
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
64117
+ };
64118
+ const errors = validateRule(merged);
64119
+ if (errors.length > 0) return { success: false, errors };
64120
+ merged = await this.applyAutoScope(merged, existing.scope, existing.packageName);
64121
+ const scopeMoved = existing.scope !== merged.scope || existing.packageName !== merged.packageName || existing.entityUuid !== merged.entityUuid || existing.caseUuid !== merged.caseUuid;
64122
+ if (scopeMoved) {
64123
+ await this.removeRuleFromScope(existing);
64124
+ }
64125
+ const saved = await this.saveRuleToScope(merged);
64126
+ if (!saved) return { success: false, errors: ["Failed to write updated rule to storage"] };
64127
+ return { success: true, rule: merged };
64128
+ }
64129
+ /** Delete a rule by uuid. */
64130
+ async deleteRule(uuid3) {
64131
+ const existing = await this.getRule(uuid3);
64132
+ if (!existing) return { success: false, errors: [`Rule ${uuid3} not found`] };
64133
+ const removed = await this.removeRuleFromScope(existing);
64134
+ if (!removed) return { success: false, errors: ["Failed to remove rule from storage"] };
64135
+ return { success: true };
64136
+ }
64137
+ // ─── Private helpers: route to the right storage location ────────────────
64138
+ async saveRuleToScope(rule) {
64139
+ const projection = getProjection(wsId("dictionaries"));
64140
+ switch (rule.scope) {
64141
+ case "entity": {
64142
+ if (!rule.entityUuid) return false;
64143
+ const files = await listAllEntityRuleFiles();
64144
+ let service = files.find((f) => f.entityUuid === rule.entityUuid)?.service;
64145
+ if (!service) service = rule.packageName;
64146
+ if (!service) {
64147
+ logger.error(`Cannot determine service for entity ${rule.entityUuid}`);
64148
+ return false;
64149
+ }
64150
+ const entity = await this.findEntityByUuid(service, rule.entityUuid);
64151
+ if (!entity) {
64152
+ logger.error(`Entity ${rule.entityUuid} not found in service ${service}`);
64153
+ return false;
64154
+ }
64155
+ const existing = entity.rules || [];
64156
+ const updatedRules = [...existing.filter((r) => r.uuid !== rule.uuid), rule];
64157
+ entity.rules = updatedRules.length > 0 ? updatedRules : void 0;
64158
+ const logicalPath = `packages/${service}/entities/${entity.name}`;
64159
+ try {
64160
+ await projection.writeEntity(logicalPath, entity);
64161
+ return true;
64162
+ } catch (e) {
64163
+ logger.error(`Failed to write entity-scope rule via projection: ${e.message}`);
64164
+ return false;
64165
+ }
64166
+ }
64167
+ case "package": {
64168
+ if (!rule.packageName) return false;
64169
+ const existing = await readPackageRules(rule.packageName);
64170
+ const updated = [...existing.filter((r) => r.uuid !== rule.uuid), rule];
64171
+ const packageLogicalPath = `packages/${rule.packageName}`;
64172
+ try {
64173
+ await projection.writePackageRules(packageLogicalPath, updated);
64174
+ return true;
64175
+ } catch (e) {
64176
+ logger.error(`Failed to write package-scope rules via projection: ${e.message}`);
64177
+ return false;
64178
+ }
64179
+ }
64180
+ case "case": {
64181
+ if (!rule.caseUuid) return false;
64182
+ const c = await readCaseFile(rule.caseUuid);
64183
+ if (!c) return false;
64184
+ const existing = c.rules || [];
64185
+ c.rules = [...existing.filter((r) => r.uuid !== rule.uuid), rule];
64186
+ c.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
64187
+ const targetPackage = await this.resolveCaseHomePackage(c);
64188
+ if (!targetPackage) return false;
64189
+ const caseLogicalPath = `packages/${targetPackage}/cases/${c.name}`;
64190
+ try {
64191
+ await projection.writeCase(caseLogicalPath, c);
64192
+ return true;
64193
+ } catch (e) {
64194
+ logger.error(`Failed to write case-scope rule via projection: ${e.message}`);
64195
+ return false;
64196
+ }
64197
+ }
64198
+ case "global": {
64199
+ const existing = await readGlobalRules();
64200
+ const updated = [...existing.filter((r) => r.uuid !== rule.uuid), rule];
64201
+ try {
64202
+ await projection.writeGlobalRules(updated);
64203
+ return true;
64204
+ } catch (e) {
64205
+ logger.error(`Failed to write global rules via projection: ${e.message}`);
64206
+ return false;
64207
+ }
64208
+ }
64209
+ default:
64210
+ return false;
64211
+ }
64212
+ }
64213
+ async removeRuleFromScope(rule) {
64214
+ const projection = getProjection(wsId("dictionaries"));
64215
+ switch (rule.scope) {
64216
+ case "entity": {
64217
+ if (!rule.entityUuid) return false;
64218
+ const files = await listAllEntityRuleFiles();
64219
+ const service = files.find((f) => f.entityUuid === rule.entityUuid)?.service ?? rule.packageName;
64220
+ if (!service) return false;
64221
+ const entity = await this.findEntityByUuid(service, rule.entityUuid);
64222
+ if (!entity) return false;
64223
+ const existing = entity.rules || [];
64224
+ const updatedRules = existing.filter((r) => r.uuid !== rule.uuid);
64225
+ entity.rules = updatedRules.length > 0 ? updatedRules : void 0;
64226
+ const logicalPath = `packages/${service}/entities/${entity.name}`;
64227
+ try {
64228
+ await projection.writeEntity(logicalPath, entity);
64229
+ return true;
64230
+ } catch (e) {
64231
+ logger.error(`Failed to remove entity-scope rule via projection: ${e.message}`);
64232
+ return false;
64233
+ }
64234
+ }
64235
+ case "package": {
64236
+ if (!rule.packageName) return false;
64237
+ const existing = await readPackageRules(rule.packageName);
64238
+ const updated = existing.filter((r) => r.uuid !== rule.uuid);
64239
+ const packageLogicalPath = `packages/${rule.packageName}`;
64240
+ try {
64241
+ await projection.writePackageRules(packageLogicalPath, updated);
64242
+ return true;
64243
+ } catch (e) {
64244
+ logger.error(`Failed to remove package-scope rule via projection: ${e.message}`);
64245
+ return false;
64246
+ }
64247
+ }
64248
+ case "case": {
64249
+ if (!rule.caseUuid) return false;
64250
+ const c = await readCaseFile(rule.caseUuid);
64251
+ if (!c) return false;
64252
+ const existing = c.rules || [];
64253
+ const updated = existing.filter((r) => r.uuid !== rule.uuid);
64254
+ c.rules = updated.length > 0 ? updated : void 0;
64255
+ c.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
64256
+ const targetPackage = await this.resolveCaseHomePackage(c);
64257
+ if (!targetPackage) return false;
64258
+ const caseLogicalPath = `packages/${targetPackage}/cases/${c.name}`;
64259
+ try {
64260
+ await projection.writeCase(caseLogicalPath, c);
64261
+ return true;
64262
+ } catch (e) {
64263
+ logger.error(`Failed to remove case-scope rule via projection: ${e.message}`);
64264
+ return false;
64265
+ }
64266
+ }
64267
+ case "global": {
64268
+ const existing = await readGlobalRules();
64269
+ const updated = existing.filter((r) => r.uuid !== rule.uuid);
64270
+ try {
64271
+ await projection.writeGlobalRules(updated);
64272
+ return true;
64273
+ } catch (e) {
64274
+ logger.error(`Failed to remove global rule via projection: ${e.message}`);
64275
+ return false;
64276
+ }
64277
+ }
64278
+ default:
64279
+ return false;
64280
+ }
64281
+ }
64282
+ /**
64283
+ * Load an entity from a package by uuid. Returns null if not found.
64284
+ * Used by entity-scope rule writes to round-trip the parent entity
64285
+ * through `projection.writeEntity`.
64286
+ */
64287
+ async findEntityByUuid(service, entityUuid) {
64288
+ try {
64289
+ const pkg = await loadPackage(service);
64290
+ return pkg.entities.find((e) => e.uuid === entityUuid) ?? null;
64291
+ } catch (error48) {
64292
+ logger.error(`Error loading package ${service} for entity lookup: ${error48}`);
64293
+ return null;
64294
+ }
64295
+ }
64296
+ /**
64297
+ * Resolve the home package for a case so we can construct a
64298
+ * `packages/<pkg>/cases/<name>` logical path. Mirrors the heuristic in
64299
+ * caseService.ts / fileOperations.ts: existing-owner lookup by uuid first,
64300
+ * then first root entity's package, then first package on disk.
64301
+ */
64302
+ async resolveCaseHomePackage(c) {
64303
+ const { listPackages: listPackages3 } = await Promise.resolve().then(() => (init_fileOperations(), fileOperations_exports));
64304
+ const packages = await listPackages3();
64305
+ for (const pkg of packages) {
64306
+ try {
64307
+ const model = await loadPackage(pkg);
64308
+ if (model.ownership.caseByUuid.has(c.uuid)) return pkg;
64309
+ } catch {
64310
+ }
64311
+ }
64312
+ if (c.rootEntities && c.rootEntities.length > 0) {
64313
+ const rootUuid = c.rootEntities[0];
64314
+ for (const pkg of packages) {
64315
+ try {
64316
+ const model = await loadPackage(pkg);
64317
+ if (model.ownership.entityByUuid.has(rootUuid)) return pkg;
64318
+ } catch {
64319
+ }
64320
+ }
64321
+ }
64322
+ return packages[0] || null;
64323
+ }
64324
+ // ─── Auto-promote / auto-demote (#75) ────────────────────────────────
64325
+ /**
64326
+ * Determine the correct scope for a rule based on its targets.
64327
+ * Called before save. If targets span 2+ packages → global.
64328
+ * If all targets are in one package → that package's scope (keep
64329
+ * the caller's intent for entity vs package).
64330
+ */
64331
+ resolveScope(rule) {
64332
+ const packages = /* @__PURE__ */ new Set();
64333
+ for (const t of rule.targets) {
64334
+ if (t.packageName) packages.add(t.packageName);
64335
+ }
64336
+ if (packages.size >= 2) {
64337
+ return { scope: "global" };
64338
+ }
64339
+ if (packages.size === 1) {
64340
+ const pkg = [...packages][0];
64341
+ if (rule.scope === "global") {
64342
+ return { scope: "package", packageName: pkg };
64343
+ }
64344
+ }
64345
+ return { scope: rule.scope, packageName: rule.packageName };
64346
+ }
64347
+ /**
64348
+ * Wrap around createRule/updateRule to apply auto-promote/demote.
64349
+ * Checks if the resolved scope differs from what the caller requested
64350
+ * and adjusts accordingly — including moving the rule between files.
64351
+ */
64352
+ async applyAutoScope(rule, _oldScope, _oldPkg) {
64353
+ const resolved = this.resolveScope(rule);
64354
+ if (resolved.scope !== rule.scope) {
64355
+ logger.info(`Auto-${resolved.scope === "global" ? "promote" : "demote"} rule ${rule.uuid}: ${rule.scope} \u2192 ${resolved.scope}`);
64356
+ }
64357
+ rule.scope = resolved.scope;
64358
+ if (resolved.packageName !== void 0) {
64359
+ rule.packageName = resolved.packageName;
64360
+ }
64361
+ return rule;
64362
+ }
64363
+ };
64364
+ ruleService = new RuleService();
64365
+ }
64366
+ });
64367
+
64311
64368
  // backend/src/services/versionService.ts
64312
64369
  var versionService_exports = {};
64313
64370
  __export(versionService_exports, {
@@ -83464,6 +83521,79 @@ var init_authoringRules = __esm({
83464
83521
  }
83465
83522
  });
83466
83523
 
83524
+ // backend/src/services/ai/physicalMapping.ts
83525
+ function metaValue2(meta3, name21) {
83526
+ const e = meta3?.find((m) => m.name === name21);
83527
+ return e && e.value != null ? String(e.value) : void 0;
83528
+ }
83529
+ function metaFlag(meta3, name21) {
83530
+ const v = meta3?.find((m) => m.name === name21)?.value;
83531
+ return v === true || /^(true|yes|1)$/i.test(String(v ?? ""));
83532
+ }
83533
+ function resolveAttributePhysical(a) {
83534
+ const columnName = metaValue2(a.metadata, "physical.columnName");
83535
+ const dbType = metaValue2(a.metadata, "physical.dbType");
83536
+ return {
83537
+ ...columnName ? { columnName } : {},
83538
+ ...dbType ? { dbType } : {},
83539
+ // == null: a YAML `primaryKey:` left empty means "unset", not false —
83540
+ // fall back to the legacy metadata flag in that case too.
83541
+ primaryKey: a.primaryKey == null ? metaFlag(a.metadata, "isPrimaryKey") : !!a.primaryKey
83542
+ };
83543
+ }
83544
+ var init_physicalMapping = __esm({
83545
+ "backend/src/services/ai/physicalMapping.ts"() {
83546
+ "use strict";
83547
+ __name(metaValue2, "metaValue");
83548
+ __name(metaFlag, "metaFlag");
83549
+ __name(resolveAttributePhysical, "resolveAttributePhysical");
83550
+ }
83551
+ });
83552
+
83553
+ // backend/src/services/ai/modelOverviewCache.ts
83554
+ var modelOverviewCache_exports = {};
83555
+ __export(modelOverviewCache_exports, {
83556
+ MODEL_OVERVIEW_TTL_MS: () => MODEL_OVERVIEW_TTL_MS,
83557
+ getModelOverviewCached: () => getModelOverviewCached,
83558
+ invalidateModelOverviewCache: () => invalidateModelOverviewCache,
83559
+ subscribeModelOverviewCache: () => subscribeModelOverviewCache
83560
+ });
83561
+ function invalidateModelOverviewCache() {
83562
+ cached2 = null;
83563
+ generation++;
83564
+ }
83565
+ function subscribeModelOverviewCache(projection) {
83566
+ return projection.onInvalidate(() => invalidateModelOverviewCache());
83567
+ }
83568
+ async function getModelOverviewCached(build) {
83569
+ if (cached2 && Date.now() - cached2.builtAt < MODEL_OVERVIEW_TTL_MS) {
83570
+ return cached2.value;
83571
+ }
83572
+ if (!inflight) {
83573
+ const startedAt = generation;
83574
+ inflight = build().then((value) => {
83575
+ if (generation === startedAt) cached2 = { value, builtAt: Date.now() };
83576
+ return value;
83577
+ }).finally(() => {
83578
+ inflight = null;
83579
+ });
83580
+ }
83581
+ return inflight;
83582
+ }
83583
+ var MODEL_OVERVIEW_TTL_MS, cached2, inflight, generation;
83584
+ var init_modelOverviewCache = __esm({
83585
+ "backend/src/services/ai/modelOverviewCache.ts"() {
83586
+ "use strict";
83587
+ MODEL_OVERVIEW_TTL_MS = 6e4;
83588
+ cached2 = null;
83589
+ inflight = null;
83590
+ generation = 0;
83591
+ __name(invalidateModelOverviewCache, "invalidateModelOverviewCache");
83592
+ __name(subscribeModelOverviewCache, "subscribeModelOverviewCache");
83593
+ __name(getModelOverviewCached, "getModelOverviewCached");
83594
+ }
83595
+ });
83596
+
83467
83597
  // backend/src/services/systemPromptStore.ts
83468
83598
  import { createHash as createHash2 } from "crypto";
83469
83599
  function systemPromptDigest(body) {
@@ -104496,10 +104626,6 @@ var init_aiMermaid = __esm({
104496
104626
  });
104497
104627
 
104498
104628
  // backend/src/controllers/aiSql.ts
104499
- function metaValue2(meta3, name21) {
104500
- const e = meta3?.find((m) => m.name === name21);
104501
- return e && e.value != null ? String(e.value) : void 0;
104502
- }
104503
104629
  function nameList(names, max = 8) {
104504
104630
  if (!names.length) return "";
104505
104631
  return names.length <= max ? names.join(", ") : `${names.slice(0, max).join(", ")} +${names.length - max} more`;
@@ -104541,44 +104667,107 @@ function sqlType(base, v, dialect) {
104541
104667
  }
104542
104668
  }
104543
104669
  function columnFor(a, derived, dialect) {
104544
- const physName = metaValue2(a.metadata, "physical.columnName");
104545
- const physType = metaValue2(a.metadata, "physical.dbType");
104670
+ const phys = resolveAttributePhysical(a);
104546
104671
  const resolved = resolveBase(String(a.type || "string"), a.validation, derived);
104547
104672
  return {
104548
104673
  attribute: a.name,
104549
- column: physName ?? a.name,
104550
- dbType: physType ?? sqlType(resolved.base, resolved.validation, dialect),
104674
+ column: phys.columnName ?? a.name,
104675
+ dbType: phys.dbType ?? sqlType(resolved.base, resolved.validation, dialect),
104551
104676
  nullable: !a.required,
104552
- primaryKey: !!a.primaryKey,
104553
- ...physName || physType ? {} : { physicalMappingMissing: true }
104677
+ primaryKey: phys.primaryKey,
104678
+ ...phys.columnName || phys.dbType ? {} : { physicalMappingMissing: true }
104554
104679
  };
104555
104680
  }
104556
104681
  async function buildSqlSchema(input, services, opts) {
104557
104682
  const dialect = input.dialect ?? "generic";
104558
104683
  const { listMicroservices: listMicroservices2 } = await Promise.resolve().then(() => (init_fileOperations(), fileOperations_exports));
104559
- const pkgs = input.packageName ? [input.packageName] : await listMicroservices2();
104684
+ const allPkgs = await listMicroservices2();
104685
+ if (input.entityNames !== void 0 && !Array.isArray(input.entityNames)) {
104686
+ return {
104687
+ error: `entityNames must be an array of entity names, e.g. entityNames: ['Order']. Retry with an array, or call searchModel({ query: '<entity or business term>' }) to locate the entity first.`
104688
+ };
104689
+ }
104690
+ const requestedNames = Array.isArray(input.entityNames) ? input.entityNames.map((n) => String(n).trim()).filter(Boolean) : void 0;
104691
+ if (input.packageName && !allPkgs.includes(input.packageName)) {
104692
+ return {
104693
+ error: `Package '${input.packageName}' not found. Known packages: ${nameList(allPkgs, 12) || "none"}. Call searchModel({ query: '<entity or business term>' }) to locate the element, or retry with entityNames: ['<EntityName>'] \u2014 entity names resolve across all packages.`
104694
+ };
104695
+ }
104560
104696
  const derivedList = await services.derivedTypes.list().catch(() => []);
104561
104697
  const derived = new Map(derivedList.map((d) => [d.name, d]));
104562
- const allPkgs = await listMicroservices2();
104563
- const nameByUuid = /* @__PURE__ */ new Map();
104698
+ const entitiesByPkg = /* @__PURE__ */ new Map();
104564
104699
  for (const p of allPkgs) {
104565
- for (const e of await services.serviceService.getServiceEntities(p).catch(() => [])) nameByUuid.set(e.uuid, e.name);
104700
+ entitiesByPkg.set(p, await services.serviceService.getServiceEntities(p).catch(() => []));
104701
+ }
104702
+ const nameByUuid = /* @__PURE__ */ new Map();
104703
+ for (const ents of entitiesByPkg.values()) {
104704
+ for (const e of ents) nameByUuid.set(e.uuid, e.name);
104705
+ }
104706
+ const relsByPkg = /* @__PURE__ */ new Map();
104707
+ const relsFor = /* @__PURE__ */ __name(async (p) => {
104708
+ if (!relsByPkg.has(p)) relsByPkg.set(p, await services.serviceService.getPackageRelationships(p).catch(() => []));
104709
+ return relsByPkg.get(p);
104710
+ }, "relsFor");
104711
+ let pkgs;
104712
+ let include = null;
104713
+ let scope;
104714
+ const unresolved = [];
104715
+ if (requestedNames?.length) {
104716
+ pkgs = allPkgs;
104717
+ const wanted = /* @__PURE__ */ new Set();
104718
+ for (const name21 of requestedNames) {
104719
+ const exact = [];
104720
+ const loose = [];
104721
+ for (const ents of entitiesByPkg.values()) {
104722
+ for (const e of ents) {
104723
+ if (e.name === name21) exact.push(e.uuid);
104724
+ else if (e.name.toLowerCase() === name21.toLowerCase()) loose.push(e.uuid);
104725
+ }
104726
+ }
104727
+ const hits = exact.length ? exact : loose;
104728
+ if (hits.length) hits.forEach((u) => wanted.add(u));
104729
+ else unresolved.push(name21);
104730
+ }
104731
+ if (!wanted.size) {
104732
+ return {
104733
+ error: `None of the requested entities (${requestedNames.join(", ")}) exist in any package. Call searchModel({ query: '<entity or business term>' }) to locate the right entity names, then retry.`
104734
+ };
104735
+ }
104736
+ include = new Set(wanted);
104737
+ for (const p of allPkgs) {
104738
+ for (const r of await relsFor(p)) {
104739
+ if (wanted.has(r.source.entity)) include.add(r.target.entity);
104740
+ if (wanted.has(r.target.entity)) include.add(r.source.entity);
104741
+ }
104742
+ }
104743
+ const resolved = requestedNames.filter((n) => !unresolved.includes(n));
104744
+ scope = `entities: ${resolved.join(", ")} (+directly related)`;
104745
+ } else {
104746
+ pkgs = input.packageName ? [input.packageName] : allPkgs;
104747
+ scope = input.packageName ?? "all packages";
104566
104748
  }
104567
104749
  const tables = [];
104568
104750
  const relationships = [];
104569
104751
  let missing = 0;
104752
+ const fallbackTables = [];
104570
104753
  for (const p of pkgs) {
104571
- const entities = await services.serviceService.getServiceEntities(p).catch(() => []);
104754
+ const entities = (entitiesByPkg.get(p) ?? []).filter((e) => !include || include.has(e.uuid));
104572
104755
  for (const e of entities) {
104573
104756
  const schema = metaValue2(e.metadata, "physical.schema") ?? (opts?.defaultSchema?.trim() || void 0);
104574
104757
  const table = metaValue2(e.metadata, "physical.tableName") ?? e.name;
104758
+ let entityMissing = 0;
104575
104759
  const columns = (e.attributes ?? []).map((a) => {
104576
104760
  const c = columnFor(a, derived, dialect);
104577
- if ("physicalMappingMissing" in c) missing++;
104761
+ if ("physicalMappingMissing" in c) entityMissing++;
104578
104762
  return c;
104579
104763
  });
104764
+ if (entityMissing > 0) {
104765
+ missing += entityMissing;
104766
+ if (!fallbackTables.includes(table)) fallbackTables.push(table);
104767
+ }
104580
104768
  tables.push({
104581
104769
  entity: e.name,
104770
+ package: p,
104582
104771
  table,
104583
104772
  ...schema ? { schema } : {},
104584
104773
  // Ready-to-use name for SQL — schema.table when a schema is known.
@@ -104586,7 +104775,8 @@ async function buildSqlSchema(input, services, opts) {
104586
104775
  columns
104587
104776
  });
104588
104777
  }
104589
- for (const r of await services.serviceService.getPackageRelationships(p).catch(() => [])) {
104778
+ for (const r of await relsFor(p)) {
104779
+ if (include && !(include.has(r.source.entity) && include.has(r.target.entity))) continue;
104590
104780
  const from = nameByUuid.get(r.source.entity);
104591
104781
  const to = nameByUuid.get(r.target.entity);
104592
104782
  if (!from || !to) continue;
@@ -104599,7 +104789,6 @@ async function buildSqlSchema(input, services, opts) {
104599
104789
  });
104600
104790
  }
104601
104791
  }
104602
- const scope = input.packageName ?? "all packages";
104603
104792
  const tableNames = tables.map((t) => t.entity);
104604
104793
  return {
104605
104794
  // #tool-summary — concise line, NAMES the tables for the tool card.
@@ -104609,7 +104798,9 @@ async function buildSqlSchema(input, services, opts) {
104609
104798
  schemaQualifyTables: !!opts?.schemaQualifyTables,
104610
104799
  tables,
104611
104800
  relationships,
104612
- note: "Use these PHYSICAL table/column names and dbTypes when writing SQL. " + (opts?.schemaQualifyTables ? "Schema-qualify every table (use each table's qualifiedName). " : "") + (missing > 0 ? `${missing} column(s) have no physical mapping \u2014 their dbType is a fallback derived from the logical type; flag this to the user.` : "All columns have an explicit physical mapping.")
104801
+ ...unresolved.length ? { unresolvedEntityNames: unresolved } : {},
104802
+ ...fallbackTables.length ? { tablesWithFallbackColumns: fallbackTables } : {},
104803
+ note: "Use these PHYSICAL table/column names and dbTypes when writing SQL. " + (opts?.schemaQualifyTables ? "Schema-qualify every table (use each table's qualifiedName). " : "") + (missing > 0 ? `${missing} column(s) in table(s) ${nameList(fallbackTables, 6)} have no explicit physical mapping \u2014 their column names/dbTypes are derived from the logical model; WARN the user before relying on them.` : "All columns have an explicit physical mapping.") + (unresolved.length ? ` Requested entities not found: ${unresolved.join(", ")} \u2014 call searchModel({ query: '<name>' }) to locate them.` : "")
104613
104804
  };
104614
104805
  }
104615
104806
  var getSqlSchemaInputSchema, getSqlSchemaParameters;
@@ -104617,18 +104808,20 @@ var init_aiSql = __esm({
104617
104808
  "backend/src/controllers/aiSql.ts"() {
104618
104809
  "use strict";
104619
104810
  init_zod();
104811
+ init_physicalMapping();
104620
104812
  getSqlSchemaInputSchema = external_exports.object({
104621
104813
  packageName: external_exports.string().optional().describe("Scope to one package (omit for the whole model)"),
104814
+ entityNames: external_exports.array(external_exports.string()).optional().describe("PREFERRED scope on large models: the target entity names (resolved across all packages). Their directly-related entities are included automatically so JOINs can be derived."),
104622
104815
  dialect: external_exports.enum(["generic", "postgres", "mysql", "mssql", "oracle", "sqlite"]).optional().describe("Target SQL dialect \u2014 tunes fallback column types (default generic)")
104623
104816
  });
104624
104817
  getSqlSchemaParameters = {
104625
104818
  type: "object",
104626
104819
  properties: {
104627
104820
  packageName: { type: "string", description: "Scope to one package (omit for the whole model)" },
104821
+ entityNames: { type: "array", items: { type: "string" }, description: "PREFERRED scope on large models: target entity names, resolved across all packages; directly-related entities are included automatically for JOINs" },
104628
104822
  dialect: { type: "string", enum: ["generic", "postgres", "mysql", "mssql", "oracle", "sqlite"], description: "Target SQL dialect (default generic)" }
104629
104823
  }
104630
104824
  };
104631
- __name(metaValue2, "metaValue");
104632
104825
  __name(nameList, "nameList");
104633
104826
  __name(resolveBase, "resolveBase");
104634
104827
  __name(sqlType, "sqlType");
@@ -116829,16 +117022,15 @@ function buildEntityDetails(entity, packageName) {
116829
117022
  const tableName = metaValue3(entity.metadata, "physical.tableName");
116830
117023
  const schema = metaValue3(entity.metadata, "physical.schema");
116831
117024
  const attributes = (entity.attributes ?? []).map((a) => {
116832
- const columnName = metaValue3(a.metadata, "physical.columnName");
116833
- const dbType = metaValue3(a.metadata, "physical.dbType");
117025
+ const phys = resolveAttributePhysical(a);
116834
117026
  return {
116835
117027
  name: a.name,
116836
117028
  type: a.type,
116837
117029
  description: a.description,
116838
117030
  required: a.required,
116839
- primaryKey: a.primaryKey,
117031
+ primaryKey: phys.primaryKey,
116840
117032
  ...a.validation ? { validation: a.validation } : {},
116841
- ...columnName || dbType ? { physical: { ...columnName ? { columnName } : {}, ...dbType ? { dbType } : {} } } : {}
117033
+ ...phys.columnName || phys.dbType ? { physical: { ...phys.columnName ? { columnName: phys.columnName } : {}, ...phys.dbType ? { dbType: phys.dbType } : {} } } : {}
116842
117034
  };
116843
117035
  });
116844
117036
  const hasPhysical = !!(tableName || schema) || attributes.some((a) => a.physical);
@@ -116850,6 +117042,7 @@ function buildEntityDetails(entity, packageName) {
116850
117042
  // #tool-summary — concise self-describing line for the tool card.
116851
117043
  summary: `${entity.name}${packageName ? ` (${packageName})` : ""} \u2014 ${attributes.length} attribute${attributes.length === 1 ? "" : "s"}${extras.length ? ` (${extras.join(", ")})` : ""}`,
116852
117044
  name: entity.name,
117045
+ ...packageName ? { packageName } : {},
116853
117046
  description: entity.description,
116854
117047
  stereotype: entity.stereotype,
116855
117048
  ...tableName || schema ? { physical: { ...tableName ? { tableName } : {}, ...schema ? { schema } : {} } } : {},
@@ -116858,6 +117051,46 @@ function buildEntityDetails(entity, packageName) {
116858
117051
  ...entity.rules?.length ? { rules: entity.rules.map((r) => ({ name: r.name, description: r.description, severity: r.severity })) } : {}
116859
117052
  };
116860
117053
  }
117054
+ async function executeGetEntityDetails(args, services) {
117055
+ if (!args?.entityName) return { error: "entityName is required." };
117056
+ const matches = await services.serviceService.findEntityMatches(args.entityName, args.packageName || void 0);
117057
+ if (matches.length === 1) {
117058
+ return buildEntityDetails(matches[0].entity, matches[0].packageName);
117059
+ }
117060
+ if (matches.length > 1) {
117061
+ return {
117062
+ summary: `'${args.entityName}' matches ${matches.length} entities \u2014 specify packageName`,
117063
+ ambiguous: true,
117064
+ candidates: matches.map((m) => ({
117065
+ entityName: m.entity.name,
117066
+ packageName: m.packageName,
117067
+ ...m.entity.description ? { description: m.entity.description } : {}
117068
+ })),
117069
+ note: "Several packages define an entity with this name. Call getEntityDetails again with the intended packageName."
117070
+ };
117071
+ }
117072
+ return {
117073
+ error: `Entity '${args.entityName}' not found in ${args.packageName ? `package '${args.packageName}' or ` : ""}any package. Call searchModel({ query: '${args.entityName}' }) to locate it by full-text search \u2014 the model may use a different name.`
117074
+ };
117075
+ }
117076
+ async function executeListEntities(args, services) {
117077
+ const { listMicroservices: listMicroservices2 } = await Promise.resolve().then(() => (init_fileOperations(), fileOperations_exports));
117078
+ if (args?.packageName) {
117079
+ const packages2 = await listMicroservices2().catch(() => []);
117080
+ if (!packages2.includes(args.packageName)) {
117081
+ return {
117082
+ error: `Package '${args.packageName}' not found. Known packages: ${nameList2(packages2, 12) || "none"}. Call searchModel({ query: '<entity or business term>' }) to locate the element you are after.`
117083
+ };
117084
+ }
117085
+ const entities = await services.serviceService.getServiceEntities(args.packageName);
117086
+ return {
117087
+ summary: `${args.packageName}: ${nameList2(entities.map((e) => e.name)) || "no entities"}`,
117088
+ entities: entities.map((e) => ({ name: e.name, description: e.description, attrCount: e.attributes?.length || 0 }))
117089
+ };
117090
+ }
117091
+ const packages = await listMicroservices2();
117092
+ return { summary: `packages: ${nameList2(packages) || "none"}`, packages };
117093
+ }
116861
117094
  async function buildModelOverview(services) {
116862
117095
  const { listMicroservices: listMicroservices2 } = await Promise.resolve().then(() => (init_fileOperations(), fileOperations_exports));
116863
117096
  const pkgNames = await listMicroservices2().catch(() => []);
@@ -116916,9 +117149,35 @@ function formatModelOutline(o) {
116916
117149
  Packages:
116917
117150
  ${pkgLines}${extra.length ? "\n" + extra.join("\n") : ""}`;
116918
117151
  }
117152
+ function formatModelOutlineWithinBudget(o, maxChars = MODEL_OUTLINE_MAX_CHARS) {
117153
+ const full = formatModelOutline(o);
117154
+ if (full.length <= maxChars) return full;
117155
+ const t = o.totals;
117156
+ const head = `Current model snapshot \u2014 ${t.packages} package(s), ${t.entities} entities, ${t.relationships} relationships, ${t.cases} cases, ${t.rules} rules, ${t.events} events, ${t.actions} actions, ${t.stateMachines} state machines, ${t.derivedTypes} derived types, ${t.stereotypes} stereotypes.`;
117157
+ const banner = `Entity lists omitted (${t.entities} entities across ${t.packages} packages \u2014 too large to inline). You do NOT have the full entity list. To locate any entity, call searchModel with its name or a business term, then getEntityDetails (packageName optional) or getSqlSchema with entityNames. Do NOT guess entity, package, or table names.`;
117158
+ const pkgLines = o.packages.map((p) => ` - ${p.name}: ${p.entities.length} entit${p.entities.length === 1 ? "y" : "ies"}, ${p.relationships} relationship${p.relationships === 1 ? "" : "s"}`);
117159
+ const fixed = head.length + banner.length + "Packages:\n".length + 4;
117160
+ const lines = [];
117161
+ let used = fixed;
117162
+ for (let i = 0; i < pkgLines.length; i++) {
117163
+ const remaining = pkgLines.length - i;
117164
+ if (used + pkgLines[i].length + 1 > maxChars - 80 && remaining > 1) {
117165
+ lines.push(` \u2026 +${remaining} more packages (call listEntities to enumerate)`);
117166
+ break;
117167
+ }
117168
+ lines.push(pkgLines[i]);
117169
+ used += pkgLines[i].length + 1;
117170
+ }
117171
+ return `${head}
117172
+ ${banner}
117173
+ Packages:
117174
+ ${lines.join("\n")}`;
117175
+ }
116919
117176
  async function safeModelOutline(services) {
116920
117177
  try {
116921
- return formatModelOutline(await buildModelOverview(services));
117178
+ return formatModelOutlineWithinBudget(
117179
+ await getModelOverviewCached(() => buildModelOverview(services))
117180
+ );
116922
117181
  } catch {
116923
117182
  return "";
116924
117183
  }
@@ -116930,8 +117189,8 @@ function resolveToolCategory(toolName, trustByName) {
116930
117189
  const clean = toolName.replace(/^functions\./, "").split(":")[0];
116931
117190
  const builtin = TOOL_CATEGORY_MAP[clean];
116932
117191
  if (builtin) return builtin;
116933
- const registered4 = getAgentTool(clean)?.category;
116934
- if (registered4) return registered4;
117192
+ const registered5 = getAgentTool(clean)?.category;
117193
+ if (registered5) return registered5;
116935
117194
  if (clean.includes(".")) {
116936
117195
  const trust = trustByName.get(clean);
116937
117196
  return trust === "auto" ? "read" : "modify";
@@ -117084,7 +117343,7 @@ function buildSystemPrompt(pageContext, conversationSystemPrompt, mode = "design
117084
117343
  if (typeof modelOutline === "string" && modelOutline.trim().length > 0) {
117085
117344
  out += `
117086
117345
 
117087
- ${modelOutline.trim().slice(0, 2e3)}`;
117346
+ ${modelOutline.trim().slice(0, MODEL_OUTLINE_MAX_CHARS)}`;
117088
117347
  }
117089
117348
  if (typeof pageContext === "string") {
117090
117349
  const trimmed2 = pageContext.trim();
@@ -117096,7 +117355,7 @@ Page context: ${trimmed2.slice(0, 500)}`;
117096
117355
  }
117097
117356
  return out;
117098
117357
  }
117099
- function buildDirectChatMessages(rawMessages, maxToolResultChars = 2e3) {
117358
+ function buildDirectChatMessages(rawMessages, maxToolResultChars = DIRECT_TOOL_RESULT_MAX_CHARS) {
117100
117359
  const out = [];
117101
117360
  for (const msg of rawMessages) {
117102
117361
  const text2 = msg.parts?.find((p) => p.type === "text")?.text || msg.content || "";
@@ -117112,10 +117371,13 @@ function buildDirectChatMessages(rawMessages, maxToolResultChars = 2e3) {
117112
117371
  }))
117113
117372
  });
117114
117373
  for (const tc of toolCalls) {
117374
+ const serialized = JSON.stringify(tc.output ?? {});
117115
117375
  out.push({
117116
117376
  role: "tool",
117117
117377
  tool_call_id: String(tc.id),
117118
- content: JSON.stringify(tc.output ?? {}).slice(0, maxToolResultChars)
117378
+ // Truncation must be LOUD — a silent cut reads as a complete result
117379
+ // and the model trusts a partial schema instead of narrowing scope.
117380
+ content: serialized.length > maxToolResultChars ? serialized.slice(0, maxToolResultChars) + TOOL_RESULT_TRUNCATION_MARKER : serialized
117119
117381
  });
117120
117382
  }
117121
117383
  } else if (text2) {
@@ -117170,10 +117432,10 @@ async function handleDirectChat(req, res, cfg, rawMessages, services, pageContex
117170
117432
  { type: "function", function: { name: "createAction", description: "Create an Action/command (e.g. PlaceOrder) on an aggregate entity (ownerEntityName), optionally CQRS-classified, with an optional flow of steps. Use flow steps emitEvent {name} and wait {for} to wire a saga/process across actions+events.", parameters: createActionParameters } },
117171
117433
  { type: "function", function: { name: "createStateMachine", description: "Create a state machine on an entity (ownerEntityName) \u2014 its states, initialState, and transitions (from/to/on). Model an entity lifecycle, e.g. Order PENDING\u2192PAID\u2192SHIPPED.", parameters: createStateMachineParameters } },
117172
117434
  { type: "function", function: { name: "listEntities", description: "List packages or entities in a package", parameters: { type: "object", properties: { packageName: { type: "string", description: "Package name (omit to list all)" } } } } },
117173
- { type: "function", function: { name: "getEntityDetails", description: "Get full detail for one entity: attributes with type/required/primaryKey AND field validation, the PHYSICAL mapping (entity table name + schema, each attribute physical column name + DB type), physical constraints, and inline business rules. Call this before writing SQL/DDL so you use the real physical names and types.", parameters: { type: "object", required: ["packageName", "entityName"], properties: { packageName: { type: "string" }, entityName: { type: "string" } } } } },
117435
+ { type: "function", function: { name: "getEntityDetails", description: "Get full detail for one entity: attributes with type/required/primaryKey AND field validation, the PHYSICAL mapping (entity table name + schema, each attribute physical column name + DB type), physical constraints, and inline business rules. packageName is OPTIONAL \u2014 omit it and the entity name is resolved across every package (ambiguous names return a candidate list). Call this before writing SQL/DDL so you use the real physical names and types.", parameters: { type: "object", required: ["entityName"], properties: { packageName: { type: "string", description: "Owning package if known (optional \u2014 resolved across packages when omitted)" }, entityName: { type: "string" } } } } },
117174
117436
  { type: "function", function: { name: "getModelOverview", description: "Get a whole-model outline: every package with its entity names, and a count of each concept (entities, relationships, cases, rules, events, actions, state machines, derived types, stereotypes). A snapshot is already in your context; call this to refresh after changes.", parameters: { type: "object", properties: {} } } },
117175
117437
  { type: "function", function: { name: "generateMermaid", description: 'Convert the model to Mermaid diagram source. diagram: "er" (entity-relationship of a package, or all), "class" (class diagram), "state" (a single entity state machine \u2014 pass entityName), "flow" (actions+events saga). Returns { mermaid }. Present it inside a ```mermaid code block.', parameters: generateMermaidParameters } },
117176
- { type: "function", function: { name: "getSqlSchema", description: "Get the PHYSICAL relational schema for writing SQL: per-entity table name + schema, each column (physical name, dbType, nullable, primaryKey), and relationships as join hints. Optional packageName scope and dialect. Call this before writing any SQL query or DDL so you use real physical names/types, not the conceptual (logical) names.", parameters: getSqlSchemaParameters } },
117438
+ { type: "function", function: { name: "getSqlSchema", description: "Get the PHYSICAL relational schema for writing SQL: per-entity table name + schema, each column (physical name, dbType, nullable, primaryKey), and relationships as join hints. PREFER scoping with entityNames (target entities, resolved across packages; directly-related entities are included automatically for JOINs) or packageName \u2014 an unscoped call on a large model returns a huge result that may be truncated. Optional dialect. Call this before writing any SQL query or DDL so you use real physical names/types, not the conceptual (logical) names.", parameters: getSqlSchemaParameters } },
117177
117439
  { type: "function", function: { name: "listStereotypes", description: "List available stereotypes", parameters: { type: "object", properties: {} } } },
117178
117440
  { type: "function", function: { name: "navigateTo", description: 'Navigate user to a page. The path MUST be an absolute URL beginning with "/" that matches one of the patterns returned by listRoutes \u2014 call listRoutes first if you are unsure of the exact shape.', parameters: { type: "object", required: ["path", "reason"], properties: { path: { type: "string" }, reason: { type: "string" } } } } },
117179
117441
  { type: "function", function: { name: "listRoutes", description: "List every valid URL pattern in the app with a short description and (where useful) a concrete example. Call this BEFORE navigateTo if you are unsure of the exact path shape \u2014 e.g. plural vs singular, where attribute pages live, what the case route is.", parameters: { type: "object", properties: {} } } }
@@ -117235,21 +117497,13 @@ async function handleDirectChat(req, res, cfg, rawMessages, services, pageContex
117235
117497
  return await executeCreateStateMachine(args, conceptServices);
117236
117498
  }
117237
117499
  if (name21 === "listEntities") {
117238
- if (args.packageName) {
117239
- const entities = await services.serviceService.getServiceEntities(args.packageName);
117240
- return { summary: `${args.packageName}: ${nameList2(entities.map((e) => e.name)) || "no entities"}`, entities: entities.map((e) => ({ name: e.name, description: e.description })) };
117241
- }
117242
- const { listMicroservices: listMicroservices2 } = await Promise.resolve().then(() => (init_fileOperations(), fileOperations_exports));
117243
- const packages = await listMicroservices2();
117244
- return { summary: `packages: ${nameList2(packages) || "none"}`, packages };
117500
+ return await executeListEntities(args, services);
117245
117501
  }
117246
117502
  if (name21 === "getEntityDetails") {
117247
- const entity = await services.serviceService.getEntitySchema(args.packageName || "default", args.entityName);
117248
- if (!entity) return { error: "Entity not found" };
117249
- return buildEntityDetails(entity, args.packageName);
117503
+ return await executeGetEntityDetails(args, services);
117250
117504
  }
117251
117505
  if (name21 === "getModelOverview") {
117252
- return await buildModelOverview(services);
117506
+ return await getModelOverviewCached(() => buildModelOverview(services));
117253
117507
  }
117254
117508
  if (name21 === "generateMermaid") {
117255
117509
  return await generateMermaidDiagram(args, services);
@@ -117288,8 +117542,11 @@ async function handleDirectChat(req, res, cfg, rawMessages, services, pageContex
117288
117542
  }
117289
117543
  }
117290
117544
  const out = await runTool(name21, args);
117291
- if (isGatedCategory(category) && out && typeof out === "object" && out.success === true) {
117292
- mutatingSuccessCount++;
117545
+ if (isGatedCategory(category)) {
117546
+ invalidateModelOverviewCache();
117547
+ if (out && typeof out === "object" && out.success === true) {
117548
+ mutatingSuccessCount++;
117549
+ }
117293
117550
  }
117294
117551
  return out;
117295
117552
  }, "executeTool");
@@ -117415,7 +117672,7 @@ ${lines.join("\n")}` : "";
117415
117672
  return "";
117416
117673
  }
117417
117674
  }
117418
- var TOOL_CATEGORY_MAP, READ_ONLY_TOOLS, MODE_SYSTEM_SUFFIX, GATED_CATEGORIES, DENIED_RESULT, SYSTEM_PROMPT, aiChat, aiChatApprove, aiStatus, aiGetConfig, aiSaveConfig, aiTestTools, aiTools, aiMentionsSearch, listConversations, getConversation, saveConversation, patchConversation, deleteConversation, getSystemPromptByDigest, listPrompts, getPrompt, createPrompt, updatePrompt, deletePrompt;
117675
+ var TOOL_CATEGORY_MAP, READ_ONLY_TOOLS, MODE_SYSTEM_SUFFIX, GATED_CATEGORIES, MODEL_OUTLINE_MAX_CHARS, DENIED_RESULT, SYSTEM_PROMPT, DIRECT_TOOL_RESULT_MAX_CHARS, TOOL_RESULT_TRUNCATION_MARKER, aiChat, aiChatApprove, aiStatus, aiGetConfig, aiSaveConfig, aiTestTools, aiTools, aiMentionsSearch, listConversations, getConversation, saveConversation, patchConversation, deleteConversation, getSystemPromptByDigest, listPrompts, getPrompt, createPrompt, updatePrompt, deletePrompt;
117419
117676
  var init_aiController = __esm({
117420
117677
  "backend/src/controllers/aiController.ts"() {
117421
117678
  "use strict";
@@ -117425,6 +117682,8 @@ var init_aiController = __esm({
117425
117682
  init_config();
117426
117683
  init_agentToolRegistry();
117427
117684
  init_authoringRules();
117685
+ init_physicalMapping();
117686
+ init_modelOverviewCache();
117428
117687
  init_systemPromptStore();
117429
117688
  init_appDir();
117430
117689
  init_conversationService();
@@ -117496,8 +117755,12 @@ Mode: REVIEW. You are reviewing the data model for quality issues. Use read-only
117496
117755
  __name(nameList2, "nameList");
117497
117756
  __name(metaValue3, "metaValue");
117498
117757
  __name(buildEntityDetails, "buildEntityDetails");
117758
+ __name(executeGetEntityDetails, "executeGetEntityDetails");
117759
+ __name(executeListEntities, "executeListEntities");
117499
117760
  __name(buildModelOverview, "buildModelOverview");
117500
117761
  __name(formatModelOutline, "formatModelOutline");
117762
+ MODEL_OUTLINE_MAX_CHARS = 4e3;
117763
+ __name(formatModelOutlineWithinBudget, "formatModelOutlineWithinBudget");
117501
117764
  __name(safeModelOutline, "safeModelOutline");
117502
117765
  __name(isGatedCategory, "isGatedCategory");
117503
117766
  __name(resolveToolCategory, "resolveToolCategory");
@@ -117523,21 +117786,24 @@ This system models a RICH domain \u2014 not just entities. You can author all of
117523
117786
  - Events \u2014 domain events emitted by aggregates, e.g. OrderPlaced (createEvent)
117524
117787
  - Actions \u2014 commands/queries on aggregates with a flow; emitEvent/wait steps wire a saga/process (createAction)
117525
117788
  - State machines \u2014 entity lifecycles: states + transitions, e.g. Order PENDING\u2192PAID\u2192SHIPPED (createStateMachine)
117526
- - Read/inspect: searchModel (full-text search across the whole dictionary \u2014 use it to LOCATE an entity/attribute/rule when you don't know its exact name, then drill in), getModelOverview (whole-model outline \u2014 packages, entities, and concept counts; a current snapshot is already shown to you below), getEntityDetails (one entity in full incl. physical mapping), listEntities, listStereotypes; navigate with navigateTo (call listRoutes first if unsure)
117789
+ - Read/inspect: searchModel (full-text search across the whole dictionary \u2014 use it to LOCATE an entity/attribute/rule when you don't know its exact name, then drill in), getModelOverview (whole-model outline \u2014 packages, entities, and concept counts; a current snapshot is already shown to you below), getEntityDetails (one entity in full incl. physical mapping; packageName optional \u2014 the name resolves across packages), listEntities, listStereotypes; navigate with navigateTo (call listRoutes first if unsure)
117527
117790
  - Diagram: generateMermaid converts the model to Mermaid source \u2014 diagram: "er" (entity-relationship of a package/all), "class" (class diagram), "state" (an entity's state machine, needs entityName), or "flow" (actions+events saga). Use it when the user asks for a diagram, ERD, or visualization, and present the result in a fenced mermaid code block.
117528
117791
 
117529
117792
  A "process" or "saga" is NOT a separate object \u2014 it is the graph that emerges from Actions (with emitEvent/wait flow steps) and Events. To model a process, create the Actions and Events; the saga view is derived automatically.
117530
117793
 
117531
117794
  CONCEPTUAL vs PHYSICAL model \u2014 keep these two layers distinct:
117532
117795
  - CONCEPTUAL / LOGICAL is the business model: entity names (PascalCase, e.g. Order), attribute names (camelCase, e.g. orderNumber), and logical or derived types (e.g. money, email, currency-code). This is what you author and what users discuss.
117533
- - PHYSICAL is how it is persisted in a database: a table name (physical.tableName) in a schema, per-column physical names (physical.columnName) and DB types (physical.dbType), plus constraints. The two layers can differ completely (logical Order.orderNumber \u2192 physical orders.order_no VARCHAR). getEntityDetails returns the physical mapping for one entity; getSqlSchema returns the whole physical relational schema.
117796
+ - PHYSICAL is how it is persisted in a database: a table name (physical.tableName) in a schema, per-column physical names (physical.columnName) and DB types (physical.dbType), plus constraints. The two layers can differ completely (logical Order.orderNumber \u2192 physical orders.order_no VARCHAR). getEntityDetails returns the physical mapping for one entity; getSqlSchema returns the physical relational schema (scope it with entityNames or packageName on anything but a small model).
117534
117797
  - A logical/derived type (money, email) is NOT a DB type \u2014 it maps to a physical dbType (money \u2192 DECIMAL(12,2), email \u2192 VARCHAR(254)).
117535
117798
  - NEVER write DDL or SQL using conceptual names. Always resolve to the physical table/column names and dbTypes first. If an element has no physical mapping yet, getSqlSchema returns a fallback dbType derived from the logical type and flags it \u2014 pass that flag on to the user.
117536
117799
 
117537
117800
  Generating database queries: when the user asks for a SQL query (SELECT / INSERT / UPDATE / DELETE), a report, or DDL \u2014
117538
- 1. Call getSqlSchema (optionally a packageName scope and the target dialect) to get the real table names, columns + dbTypes, primary keys, and relationships.
117539
- 2. Write the query using ONLY physical names; derive JOINs from the relationships (join the PK of the "one" side to the FK on the "many" side) and respect the requested dialect.
117540
- 3. Present the query in a fenced sql code block, and briefly note any assumption (an inferred join column, a missing physical mapping). You generate queries for the user to run \u2014 you do NOT execute them against a live database.
117801
+ 1. LOCATE the entity first. Unless you already know its owning package (from the snapshot, page context, or an @mention), call searchModel with the entity name or business term to resolve entity \u2192 package. NEVER guess a package, entity, or table name \u2014 the snapshot above may not list every entity.
117802
+ 2. Call getSqlSchema scoped NARROWLY: pass entityNames: [...] for the target entities (their directly-related entities are included automatically so you can derive JOINs), or a packageName; pass the target dialect. Do not call it unscoped on a large model \u2014 the result gets truncated and misleads you.
117803
+ 3. Write the query using ONLY the physical names returned by the tool; derive JOINs from the relationships (join the PK of the "one" side to the FK on the "many" side) and respect the requested dialect.
117804
+ 4. If any column is flagged physicalMappingMissing, tell the user those column names/types are unverified fallbacks derived from the logical model \u2014 do not silently pass them off as real.
117805
+ 5. Present the query in a fenced sql code block, and briefly note any assumption (an inferred join column, a missing physical mapping). You generate queries for the user to run \u2014 you do NOT execute them against a live database.
117806
+ If a lookup tool returns an error or an empty result, do not retry the same guess \u2014 follow the error's guidance (usually: call searchModel), or ask the user.
117541
117807
 
117542
117808
  When creating data models:
117543
117809
  - Use meaningful names (PascalCase for entities/events, camelCase for attributes)
@@ -117568,6 +117834,8 @@ Be concise in your responses. Show a summary of what you created.`;
117568
117834
  __name(sqlSettingsInstruction, "sqlSettingsInstruction");
117569
117835
  __name(standingSystemPrompt, "standingSystemPrompt");
117570
117836
  __name(buildSystemPrompt, "buildSystemPrompt");
117837
+ DIRECT_TOOL_RESULT_MAX_CHARS = 2e4;
117838
+ TOOL_RESULT_TRUNCATION_MARKER = "\u2026[truncated \u2014 result too large; retry with a narrower scope, e.g. packageName or entityNames filter]";
117571
117839
  __name(buildDirectChatMessages, "buildDirectChatMessages");
117572
117840
  __name(buildUiMessagesWithToolParts, "buildUiMessagesWithToolParts");
117573
117841
  __name(handleDirectChat, "handleDirectChat");
@@ -117620,7 +117888,11 @@ Be concise in your responses. Show a summary of what you created.`;
117620
117888
  const decision = await awaitApproval(streamId, toolCallId);
117621
117889
  if (decision === "deny") return { ...DENIED_RESULT };
117622
117890
  }
117623
- return run();
117891
+ try {
117892
+ return await run();
117893
+ } finally {
117894
+ if (isGatedCategory(category)) invalidateModelOverviewCache();
117895
+ }
117624
117896
  }, "gate");
117625
117897
  const mcpToolEntries = {};
117626
117898
  for (const mcpTool of mcpTools) {
@@ -117791,29 +118063,21 @@ Be concise in your responses. Show a summary of what you created.`;
117791
118063
  }),
117792
118064
  execute: /* @__PURE__ */ __name(async (params) => {
117793
118065
  try {
117794
- if (params.packageName) {
117795
- const entities = await services.serviceService.getServiceEntities(params.packageName || "default");
117796
- return { summary: `${params.packageName}: ${nameList2(entities.map((e) => e.name)) || "no entities"}`, entities: entities.map((e) => ({ name: e.name, description: e.description, attrCount: e.attributes?.length || 0 })) };
117797
- }
117798
- const { listMicroservices: listMicroservices2 } = await Promise.resolve().then(() => (init_fileOperations(), fileOperations_exports));
117799
- const packages = await listMicroservices2();
117800
- return { summary: `packages: ${nameList2(packages) || "none"}`, packages };
118066
+ return await executeListEntities(params, services);
117801
118067
  } catch (err) {
117802
118068
  return { error: err.message };
117803
118069
  }
117804
118070
  }, "execute")
117805
118071
  }),
117806
118072
  getEntityDetails: tool({
117807
- description: "Get full detail for an entity: attributes with type/required/primaryKey AND field validation, the PHYSICAL mapping (entity table name + schema, each attribute physical column name + DB type), physical constraints, and inline business rules. Use this before writing SQL/DDL so you use the real physical names and types.",
118073
+ description: "Get full detail for an entity: attributes with type/required/primaryKey AND field validation, the PHYSICAL mapping (entity table name + schema, each attribute physical column name + DB type), physical constraints, and inline business rules. packageName is OPTIONAL \u2014 omit it and the entity name is resolved across every package (ambiguous names return a candidate list). Use this before writing SQL/DDL so you use the real physical names and types.",
117808
118074
  inputSchema: external_exports.object({
117809
- packageName: external_exports.string(),
118075
+ packageName: external_exports.string().optional().describe("Owning package if known (optional \u2014 resolved across packages when omitted)"),
117810
118076
  entityName: external_exports.string()
117811
118077
  }),
117812
118078
  execute: /* @__PURE__ */ __name(async (params) => {
117813
118079
  try {
117814
- const entity = await services.serviceService.getEntitySchema(params.packageName || "default", params.entityName);
117815
- if (!entity) return { error: "Entity not found" };
117816
- return buildEntityDetails(entity, params.packageName);
118080
+ return await executeGetEntityDetails(params, services);
117817
118081
  } catch (err) {
117818
118082
  return { error: err.message };
117819
118083
  }
@@ -117824,7 +118088,7 @@ Be concise in your responses. Show a summary of what you created.`;
117824
118088
  inputSchema: external_exports.object({}),
117825
118089
  execute: /* @__PURE__ */ __name(async () => {
117826
118090
  try {
117827
- return await buildModelOverview(services);
118091
+ return await getModelOverviewCached(() => buildModelOverview(services));
117828
118092
  } catch (err) {
117829
118093
  return { error: err.message };
117830
118094
  }
@@ -117842,7 +118106,7 @@ Be concise in your responses. Show a summary of what you created.`;
117842
118106
  }, "execute")
117843
118107
  }),
117844
118108
  getSqlSchema: tool({
117845
- description: "Get the PHYSICAL relational schema for writing SQL: per-entity table name + schema, each column (physical name, dbType, nullable, primaryKey), and relationships as join hints. Optional packageName scope and dialect. Call this before writing any SQL query or DDL so you use real physical names/types, not the conceptual (logical) names.",
118109
+ description: "Get the PHYSICAL relational schema for writing SQL: per-entity table name + schema, each column (physical name, dbType, nullable, primaryKey), and relationships as join hints. PREFER scoping with entityNames (target entities, resolved across packages; directly-related entities are included automatically for JOINs) or packageName \u2014 an unscoped call on a large model returns a huge result that may be truncated. Optional dialect. Call this before writing any SQL query or DDL so you use real physical names/types, not the conceptual (logical) names.",
117846
118110
  inputSchema: getSqlSchemaInputSchema,
117847
118111
  execute: /* @__PURE__ */ __name(async (params) => {
117848
118112
  try {
@@ -118275,7 +118539,7 @@ Be concise in your responses. Show a summary of what you created.`;
118275
118539
  description: "Get full entity detail: attributes + validation, the physical mapping (table/schema, column names, DB types), constraints, and inline rules. Use before writing SQL/DDL.",
118276
118540
  source: "builtin",
118277
118541
  parameters: [
118278
- { name: "packageName", type: "string", required: true, description: "Package name" },
118542
+ { name: "packageName", type: "string", required: false, description: "Owning package if known (omit to resolve the entity name across all packages)" },
118279
118543
  { name: "entityName", type: "string", required: true, description: "Entity name" }
118280
118544
  ]
118281
118545
  },
@@ -118297,10 +118561,11 @@ Be concise in your responses. Show a summary of what you created.`;
118297
118561
  },
118298
118562
  {
118299
118563
  name: "getSqlSchema",
118300
- description: "Physical relational schema for writing SQL: tables, columns (physical name + dbType + nullable + PK), and relationships as join hints. Use before generating any SQL query/DDL.",
118564
+ description: "Physical relational schema for writing SQL: tables, columns (physical name + dbType + nullable + PK), and relationships as join hints. Use before generating any SQL query/DDL. Prefer scoping with entityNames or packageName on large models.",
118301
118565
  source: "builtin",
118302
118566
  parameters: [
118303
118567
  { name: "packageName", type: "string", required: false, description: "Scope to one package (omit for all)" },
118568
+ { name: "entityNames", type: "string[]", required: false, description: "Preferred scope: target entity names, resolved across packages; directly-related entities are included automatically for JOINs" },
118304
118569
  { name: "dialect", type: "generic|postgres|mysql|mssql|oracle|sqlite", required: false, description: "Target SQL dialect (default generic)" }
118305
118570
  ]
118306
118571
  },
@@ -150095,8 +150360,8 @@ var require_inflight = __commonJS({
150095
150360
  var wrappy = require_wrappy();
150096
150361
  var reqs = /* @__PURE__ */ Object.create(null);
150097
150362
  var once = require_once();
150098
- module.exports = wrappy(inflight);
150099
- function inflight(key2, cb) {
150363
+ module.exports = wrappy(inflight2);
150364
+ function inflight2(key2, cb) {
150100
150365
  if (reqs[key2]) {
150101
150366
  reqs[key2].push(cb);
150102
150367
  return null;
@@ -150105,7 +150370,7 @@ var require_inflight = __commonJS({
150105
150370
  return makeres(key2);
150106
150371
  }
150107
150372
  }
150108
- __name(inflight, "inflight");
150373
+ __name(inflight2, "inflight");
150109
150374
  function makeres(key2) {
150110
150375
  return once(/* @__PURE__ */ __name(function RES() {
150111
150376
  var cbs = reqs[key2];
@@ -150157,7 +150422,7 @@ var require_glob = __commonJS({
150157
150422
  var alphasorti = common.alphasorti;
150158
150423
  var setopts = common.setopts;
150159
150424
  var ownProp = common.ownProp;
150160
- var inflight = require_inflight();
150425
+ var inflight2 = require_inflight();
150161
150426
  var util2 = __require("util");
150162
150427
  var childrenIgnored = common.childrenIgnored;
150163
150428
  var isIgnored = common.isIgnored;
@@ -150489,7 +150754,7 @@ var require_glob = __commonJS({
150489
150754
  return this._readdir(abs, false, cb);
150490
150755
  var lstatkey = "lstat\0" + abs;
150491
150756
  var self2 = this;
150492
- var lstatcb = inflight(lstatkey, lstatcb_);
150757
+ var lstatcb = inflight2(lstatkey, lstatcb_);
150493
150758
  if (lstatcb)
150494
150759
  fs16.lstat(abs, lstatcb);
150495
150760
  function lstatcb_(er, lstat) {
@@ -150508,7 +150773,7 @@ var require_glob = __commonJS({
150508
150773
  Glob.prototype._readdir = function(abs, inGlobStar, cb) {
150509
150774
  if (this.aborted)
150510
150775
  return;
150511
- cb = inflight("readdir\0" + abs + "\0" + inGlobStar, cb);
150776
+ cb = inflight2("readdir\0" + abs + "\0" + inGlobStar, cb);
150512
150777
  if (!cb)
150513
150778
  return;
150514
150779
  if (inGlobStar && !ownProp(this.symlinks, abs))
@@ -150666,7 +150931,7 @@ var require_glob = __commonJS({
150666
150931
  }
150667
150932
  }
150668
150933
  var self2 = this;
150669
- var statcb = inflight("stat\0" + abs, lstatcb_);
150934
+ var statcb = inflight2("stat\0" + abs, lstatcb_);
150670
150935
  if (statcb)
150671
150936
  fs16.lstat(abs, statcb);
150672
150937
  function lstatcb_(er, lstat) {
@@ -159537,6 +159802,24 @@ var setEntityHidden = /* @__PURE__ */ __name(async (req, res) => {
159537
159802
  res.status(500).json({ message: "Error setting entity visibility", error: error48 });
159538
159803
  }
159539
159804
  }, "setEntityHidden");
159805
+ var moveEntity = /* @__PURE__ */ __name(async (req, res) => {
159806
+ try {
159807
+ const { service, entity } = req.params;
159808
+ const targetPackage = typeof req.body?.targetPackage === "string" ? req.body.targetPackage : "";
159809
+ if (!targetPackage) {
159810
+ return res.status(400).json({ message: "targetPackage is required" });
159811
+ }
159812
+ const result = await serviceService.moveEntity(service, entity, targetPackage);
159813
+ if (!result.success) {
159814
+ const notFound = result.errors.some((e) => e.includes("not found"));
159815
+ return res.status(notFound ? 404 : 400).json({ message: "Failed to move entity", errors: result.errors });
159816
+ }
159817
+ res.json({ message: "Entity moved", data: { service, entity, targetPackage } });
159818
+ } catch (error48) {
159819
+ logger.error(`Error moving entity: ${error48}`);
159820
+ res.status(500).json({ message: "Error moving entity", error: error48 });
159821
+ }
159822
+ }, "moveEntity");
159540
159823
  var setEntityStyle = /* @__PURE__ */ __name(async (req, res) => {
159541
159824
  try {
159542
159825
  const { service, entity } = req.params;
@@ -159763,7 +160046,46 @@ var deleteRelationship = /* @__PURE__ */ __name(async (req, res) => {
159763
160046
  }
159764
160047
  }, "deleteRelationship");
159765
160048
 
160049
+ // backend/src/services/entityMove/agentTools.ts
160050
+ init_zod();
160051
+ init_agentToolRegistry();
160052
+ init_serviceService();
160053
+ var registered = false;
160054
+ function registerMoveEntityAgentTool() {
160055
+ if (registered) return;
160056
+ registered = true;
160057
+ registerAgentTool({
160058
+ name: "moveEntity",
160059
+ category: "modify",
160060
+ description: "Move an entity from one package to another. The entity keeps its UUID, so relationships (cross-package is first-class), cases and diagrams that reference it keep resolving \u2014 nothing is orphaned. Fails if the target package doesn't exist or already has an entity with the same name.",
160061
+ jsonSchema: {
160062
+ type: "object",
160063
+ required: ["sourcePackage", "entityName", "targetPackage"],
160064
+ properties: {
160065
+ sourcePackage: { type: "string", description: "Package the entity currently lives in" },
160066
+ entityName: { type: "string", description: "Name of the entity to move" },
160067
+ targetPackage: { type: "string", description: "Package to move the entity into" }
160068
+ }
160069
+ },
160070
+ inputSchema: external_exports.object({
160071
+ sourcePackage: external_exports.string(),
160072
+ entityName: external_exports.string(),
160073
+ targetPackage: external_exports.string()
160074
+ }),
160075
+ execute: /* @__PURE__ */ __name(async (args) => {
160076
+ const r = await serviceService.moveEntity(
160077
+ String(args.sourcePackage),
160078
+ String(args.entityName),
160079
+ String(args.targetPackage)
160080
+ );
160081
+ return r.success ? { success: true, entity: args.entityName, from: args.sourcePackage, to: args.targetPackage } : { success: false, errors: r.errors };
160082
+ }, "execute")
160083
+ });
160084
+ }
160085
+ __name(registerMoveEntityAgentTool, "registerMoveEntityAgentTool");
160086
+
159766
160087
  // backend/src/routes/data-dictionary/entity.routes.ts
160088
+ registerMoveEntityAgentTool();
159767
160089
  var router3 = (0, import_express4.Router)();
159768
160090
  router3.get("/api/entities/:microservice/:entityName/attributes", getEntityAttributes);
159769
160091
  router3.get("/api/entities/:microservice/:entityName/related", getRelatedEntities);
@@ -159777,6 +160099,7 @@ router3.put("/api/services/:service/entities/:entity", authorizeJwt(["admin" /*
159777
160099
  router3.delete("/api/services/:service/entities/:entity", authorizeJwt(["admin" /* ADMIN */]), deleteEntity);
159778
160100
  router3.put("/api/services/:service/entities/:entity/hidden", authorizeJwt(["admin" /* ADMIN */, "editor" /* EDITOR */]), setEntityHidden);
159779
160101
  router3.put("/api/services/:service/entities/:entity/style", authorizeJwt(["admin" /* ADMIN */, "editor" /* EDITOR */]), setEntityStyle);
160102
+ router3.put("/api/services/:service/entities/:entity/move", authorizeJwt(["admin" /* ADMIN */, "editor" /* EDITOR */]), moveEntity);
159780
160103
  router3.post("/api/services/:service/entities/:entity/submit", authorizeJwt(["admin" /* ADMIN */, "editor" /* EDITOR */]), submitEntity);
159781
160104
  router3.post("/api/services/:service/entities/:entity/approve", authorizeJwt(["admin" /* ADMIN */]), approveEntity);
159782
160105
  router3.post("/api/services/:service/entities/:entity/return", authorizeJwt(["admin" /* ADMIN */]), returnEntity);
@@ -160321,10 +160644,10 @@ init_zod();
160321
160644
  init_agentToolRegistry();
160322
160645
  init_dicoConfigService();
160323
160646
  init_serviceService();
160324
- var registered = false;
160647
+ var registered2 = false;
160325
160648
  function registerElementStyleAgentTools() {
160326
- if (registered) return;
160327
- registered = true;
160649
+ if (registered2) return;
160650
+ registered2 = true;
160328
160651
  registerAgentTool({
160329
160652
  name: "defineElementStyle",
160330
160653
  category: "modify",
@@ -165089,10 +165412,10 @@ init_zod();
165089
165412
  init_agentToolRegistry();
165090
165413
  init_searchIndexService();
165091
165414
  var KINDS = ["entity", "attribute", "package", "relationship", "rule", "metadata", "case"];
165092
- var registered2 = false;
165415
+ var registered3 = false;
165093
165416
  function registerSearchAgentTools() {
165094
- if (registered2) return;
165095
- registered2 = true;
165417
+ if (registered3) return;
165418
+ registered3 = true;
165096
165419
  registerAgentTool({
165097
165420
  name: "searchModel",
165098
165421
  category: "read",
@@ -173535,10 +173858,10 @@ function getSynthesisBrief(dataDir, entityName) {
173535
173858
  __name(getSynthesisBrief, "getSynthesisBrief");
173536
173859
 
173537
173860
  // backend/src/services/reverseEngineer/agentTools.ts
173538
- var registered3 = false;
173861
+ var registered4 = false;
173539
173862
  function registerReverseEngineerAgentTools() {
173540
- if (registered3) return;
173541
- registered3 = true;
173863
+ if (registered4) return;
173864
+ registered4 = true;
173542
173865
  registerAgentTool({
173543
173866
  name: "listSynthesisBriefs",
173544
173867
  category: "read",
@@ -174207,6 +174530,12 @@ async function mountFrameworkRoutes() {
174207
174530
  } catch (searchError) {
174208
174531
  logger.warn(`SearchIndex initialization failed: ${searchError instanceof Error ? searchError.message : String(searchError)}`);
174209
174532
  }
174533
+ try {
174534
+ const { subscribeModelOverviewCache: subscribeModelOverviewCache2 } = await Promise.resolve().then(() => (init_modelOverviewCache(), modelOverviewCache_exports));
174535
+ subscribeModelOverviewCache2(projection);
174536
+ } catch (cacheError) {
174537
+ logger.warn(`ModelOverviewCache subscription failed: ${cacheError instanceof Error ? cacheError.message : String(cacheError)}`);
174538
+ }
174210
174539
  } catch (e) {
174211
174540
  logger.warn(`Projection/UuidIndex initialization failed: ${e instanceof Error ? e.message : String(e)}`);
174212
174541
  }