@hamak/smart-data-dico 1.8.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -321,6 +321,89 @@ Set via `PROFILE` environment variable:
321
321
  | **team** | Basic/JWT | Remote | Small team sharing a repo |
322
322
  | **server** | Auth0/SSO | Remote | Organization-wide deployment |
323
323
 
324
+ ## AI Assistance
325
+
326
+ The dictionary ships with three AI-aware surfaces, all optional. They share one set of credentials stored at `~/.dico-app/dico-app.json` (mode 0600 — never checked into git):
327
+
328
+ | Surface | What it is | Where to find it |
329
+ |---|---|---|
330
+ | **In-app AI chat panel** | Sidebar chat that grounds against the live dictionary — list packages, describe entities, generate docs, propose edits | Toolbar button **AI Assistant** in the running web app |
331
+ | **Slash commands** | Built-in chat shortcuts (`/list`, `/quality`, `/describe`, `/create`, `/relate`, `/export`, `/diagram`, `/help`) + your own saved prompts | Type `/` in the chat composer |
332
+ | **MCP server** (`dico-mcp`) | A [Model Context Protocol](https://modelcontextprotocol.io) stdio server that exposes the same operations to external clients — Claude Desktop, Cursor, Roo Code, Claude Code | `npx @hamak/smart-data-dico-mcp` or `node bin/dico-mcp.js` from source |
333
+
334
+ ### Configure the in-app chat (one-time)
335
+
336
+ 1. Start the app (Option A, B, or C above).
337
+ 2. Open **Settings → AI** (or click the AI Assistant button → ⚙ icon).
338
+ 3. Pick a **provider** (`anthropic`, `openai`, or `openai-compatible`), paste an API key, and choose a **model** (e.g. `claude-opus-4-7`, `gpt-4o`, or any model your `openai-compatible` endpoint exposes).
339
+ 4. Save. The config writes to `~/.dico-app/dico-app.json` with mode `0600`; no env vars are required.
340
+
341
+ Alternatively, if you set `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` before launching the server, the provider is auto-detected for the first session — but the **Settings dialog is the source of truth**; saving overrides env-var defaults.
342
+
343
+ Conversations and saved prompts live under `~/.dico-app/storage/` as JSON files — portable, diffable, deletable.
344
+
345
+ ### Slash commands
346
+
347
+ | Command | What it does |
348
+ |---|---|
349
+ | `/help` | List all available slash commands |
350
+ | `/list` | List every package and its entity count |
351
+ | `/describe` | Describe the current entity in detail (uses page context) |
352
+ | `/quality` | Quality review of the current package (severity-grouped findings) |
353
+ | `/create` | Skeleton: create a new entity with suggested attributes |
354
+ | `/relate` | Skeleton: create a relationship between two entities |
355
+ | `/export` | Generate Markdown documentation for the current package |
356
+ | `/diagram` | Navigate to the organization diagram |
357
+
358
+ The `pageContext` placeholder is filled automatically from the current route (entity / package / etc.). User-saved prompts (via **Prompts** tab in the chat panel) appear in the same slash-command picker.
359
+
360
+ ### MCP server (`dico-mcp`)
361
+
362
+ The MCP server reuses the same backend services as the web app — it's a different **transport** for the same operations, not a duplicate code path. You can run the web UI and the MCP server side-by-side against the same project folder.
363
+
364
+ **Tools exposed:** `listPackages`, `listEntities`, `getEntityDetails`, `createEntity`, `createRelationship`, `listStereotypes`, `listRoutes`.
365
+
366
+ **Launch (from npm):**
367
+
368
+ ```bash
369
+ # stdio server — talks JSON-RPC over stdin/stdout
370
+ npx @hamak/smart-data-dico --data-dir /path/to/your/project # web UI
371
+ # (the MCP entrypoint is bin/dico-mcp.js inside the package)
372
+ node "$(npm root -g)/@hamak/smart-data-dico/bin/dico-mcp.js" --data-dir /path/to/your/project
373
+ ```
374
+
375
+ **Register with Claude Desktop** — edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
376
+
377
+ ```json
378
+ {
379
+ "mcpServers": {
380
+ "smart-data-dico": {
381
+ "command": "npx",
382
+ "args": ["-y", "@hamak/smart-data-dico", "dico-mcp", "--data-dir", "/absolute/path/to/your/project"]
383
+ }
384
+ }
385
+ }
386
+ ```
387
+
388
+ Or, if installed from source, point at the bin script directly:
389
+
390
+ ```json
391
+ {
392
+ "command": "node",
393
+ "args": ["/absolute/path/to/smart-data-dico/bin/dico-mcp.js", "--data-dir", "/absolute/path/to/your/project"]
394
+ }
395
+ ```
396
+
397
+ **Register with Cursor** — `.cursor/mcp.json` (project-level) or `~/.cursor/mcp.json` (global): identical shape.
398
+
399
+ **Register with Roo Code** — `.roo/mcp.json`: identical shape.
400
+
401
+ The MCP process speaks JSON-RPC on stdio and stays attached to its parent; all logging goes to stderr so it doesn't corrupt the stream. Git auto-commit honours the same `GIT_AUTO_COMMIT` env var as the web backend.
402
+
403
+ ### Connect external MCP servers *to* the in-app chat
404
+
405
+ The flip side of the above: the in-app chat can also consume external MCP servers (Filesystem, GitHub, etc.). Go to **Settings → MCP** for a curated registry — pick a server, paste any required tokens, save. The chat will then surface those server's tools alongside the built-in ones.
406
+
324
407
  ## Technologies
325
408
 
326
409
  | Layer | Stack |
@@ -331,3 +414,65 @@ Set via `PROFILE` environment variable:
331
414
  | Visualization | Cytoscape.js (dagre + fcose layouts) |
332
415
  | Auth | JWT + Auth0 (mock mode for dev) |
333
416
  | Deployment | Docker (multi-stage build) |
417
+
418
+ ## Documentation
419
+
420
+ | Doc | What it covers |
421
+ |-----|----------------|
422
+ | [Format reference](docs/format-reference.md) | Authoritative spec for the on-disk format — project layout, `dico.config.json`, entities, validation, constraints, relationships, stereotypes, rules, cases, actions, state machines |
423
+ | [User guide](docs/user-guide.md) | Task-oriented walkthrough of the app |
424
+ | [API reference](docs/api-reference.md) | REST endpoints (live Swagger UI at `/api-docs` when the backend is running) |
425
+ | [Deployment](docs/deployment.md) | Desktop vs. server modes, file layout, configuration |
426
+ | [Migration plan](docs/migration-plan.md) | @hamak/app-framework migration notes |
427
+ | [ADRs](docs/adr/) | Architecture decision records |
428
+
429
+ ### Claude Code skill
430
+
431
+ The `docs/` folder doubles as a [Claude Code](https://claude.com/claude-code) skill
432
+ (`docs/SKILL.md` + the format reference) for authoring and validating dictionary files in
433
+ any project. Installing it means copying `docs/` into a skills directory as `smart-data-dico`.
434
+ Claude then loads it automatically whenever you work in a folder containing `dico.config.json`.
435
+
436
+ **From a local checkout of this repo:**
437
+
438
+ ```bash
439
+ npm run install:skill # → ~/.claude/skills/smart-data-dico
440
+ # or into a specific project:
441
+ scripts/install-skill.sh /path/to/project/.claude/skills
442
+ ```
443
+
444
+ **From another repo / machine (no checkout):** pull just `docs/` into your skills directory
445
+ with [`degit`](https://github.com/Rich-Harris/degit) — no auth needed:
446
+
447
+ ```bash
448
+ # global → ~/.claude/skills/smart-data-dico
449
+ npx degit amah/smart-data-dico/docs ~/.claude/skills/smart-data-dico
450
+ ```
451
+
452
+ Or, without Node, via the release tarball:
453
+
454
+ ```bash
455
+ tmp=$(mktemp -d) && curl -fsSL https://codeload.github.com/amah/smart-data-dico/tar.gz/refs/heads/main | tar -xz -C "$tmp" \
456
+ && rm -rf ~/.claude/skills/smart-data-dico \
457
+ && cp -R "$tmp"/*/docs ~/.claude/skills/smart-data-dico && rm -rf "$tmp"
458
+ ```
459
+
460
+ Swap the destination for `<project>/.claude/skills/smart-data-dico` to scope it to one project.
461
+ Re-run either command to update.
462
+
463
+ **Behind a proxy:** `degit` reads the lowercase `https_proxy` env var (not the CLI), while
464
+ `curl` takes an explicit `--proxy` flag — the curl form is usually the more reliable in
465
+ locked-down networks:
466
+
467
+ ```bash
468
+ # degit
469
+ https_proxy=http://proxy.example.com:8080 npx degit amah/smart-data-dico/docs ~/.claude/skills/smart-data-dico
470
+
471
+ # curl
472
+ tmp=$(mktemp -d) && curl -fsSL --proxy http://proxy.example.com:8080 https://codeload.github.com/amah/smart-data-dico/tar.gz/refs/heads/main | tar -xz -C "$tmp" \
473
+ && rm -rf ~/.claude/skills/smart-data-dico \
474
+ && cp -R "$tmp"/*/docs ~/.claude/skills/smart-data-dico && rm -rf "$tmp"
475
+ ```
476
+
477
+ If the proxy does TLS interception, point Node/curl at your CA bundle (`NODE_EXTRA_CA_CERTS=…`
478
+ for degit, `--cacert …` for curl).
@@ -37667,6 +37667,18 @@ function normalizeRelationshipEnds(rel) {
37667
37667
  }
37668
37668
  ];
37669
37669
  }
37670
+ function normalizeRelationship(rel) {
37671
+ const hasEnds = !!(rel.ends && rel.ends.length >= 2);
37672
+ const hasLegacy = !!(rel.source && rel.target);
37673
+ if (!hasEnds && !hasLegacy) return null;
37674
+ const [a, b] = normalizeRelationshipEnds(rel);
37675
+ return {
37676
+ ...rel,
37677
+ ends: hasEnds ? rel.ends : [a, b],
37678
+ source: rel.source ?? { entity: a.entity, cardinality: a.cardinality, name: a.role, referenceAttributes: a.referenceAttributes },
37679
+ target: rel.target ?? { entity: b.entity, cardinality: b.cardinality, name: b.role, referenceAttributes: b.referenceAttributes }
37680
+ };
37681
+ }
37670
37682
  function validateEntity(entity) {
37671
37683
  const validator = new import_jsonschema.Validator();
37672
37684
  const result = validator.validate(entity, entitySchema);
@@ -37721,12 +37733,13 @@ var init_EntitySchema = __esm({
37721
37733
  AttributeType3["UUID"] = "uuid";
37722
37734
  return AttributeType3;
37723
37735
  })(AttributeType || {});
37724
- Cardinality = /* @__PURE__ */ ((Cardinality4) => {
37725
- Cardinality4["ONE"] = "one";
37726
- Cardinality4["MANY"] = "many";
37727
- return Cardinality4;
37736
+ Cardinality = /* @__PURE__ */ ((Cardinality3) => {
37737
+ Cardinality3["ONE"] = "one";
37738
+ Cardinality3["MANY"] = "many";
37739
+ return Cardinality3;
37728
37740
  })(Cardinality || {});
37729
37741
  __name(normalizeRelationshipEnds, "normalizeRelationshipEnds");
37742
+ __name(normalizeRelationship, "normalizeRelationship");
37730
37743
  entitySchema = {
37731
37744
  type: "object",
37732
37745
  required: ["uuid", "name", "attributes"],
@@ -37891,11 +37904,27 @@ var init_StorageBackendToken = __esm({
37891
37904
  STORAGE_BACKEND_TOKEN = /* @__PURE__ */ Symbol("STORAGE_BACKEND");
37892
37905
  DICTIONARY_QUERY_TOKEN = /* @__PURE__ */ Symbol("DICTIONARY_QUERY");
37893
37906
  StorageRegistry = class {
37907
+ constructor() {
37908
+ this.changeListeners = [];
37909
+ }
37894
37910
  static {
37895
37911
  __name(this, "StorageRegistry");
37896
37912
  }
37913
+ /**
37914
+ * Subscribe to backend swaps/resets. Caches keyed on backend contents
37915
+ * (e.g. the loadPackage cache) register here so they clear when the
37916
+ * backend changes — chiefly so tests that install a fresh in-memory
37917
+ * backend per case don't read stale cached data.
37918
+ */
37919
+ onBackendChange(fn) {
37920
+ this.changeListeners.push(fn);
37921
+ }
37922
+ notifyChange() {
37923
+ for (const fn of this.changeListeners) fn();
37924
+ }
37897
37925
  setBackend(b) {
37898
37926
  this.backend = b;
37927
+ this.notifyChange();
37899
37928
  }
37900
37929
  getBackend() {
37901
37930
  if (!this.backend) throw new Error("STORAGE_BACKEND not registered. server.ts must call storageRegistry.setBackend() at startup.");
@@ -37911,6 +37940,7 @@ var init_StorageBackendToken = __esm({
37911
37940
  reset() {
37912
37941
  this.backend = void 0;
37913
37942
  this.query = void 0;
37943
+ this.notifyChange();
37914
37944
  }
37915
37945
  };
37916
37946
  storageRegistry = new StorageRegistry();
@@ -44170,6 +44200,7 @@ __export(fileOperations_exports, {
44170
44200
  getPackagePath: () => getPackagePath,
44171
44201
  getReservedPackageFiles: () => getReservedPackageFiles,
44172
44202
  getSchemaPackagePath: () => getSchemaPackagePath,
44203
+ invalidatePackageCache: () => invalidatePackageCache,
44173
44204
  listAllDictionaries: () => listAllDictionaries,
44174
44205
  listAllEntities: () => listAllEntities,
44175
44206
  listAllEntityRuleFiles: () => listAllEntityRuleFiles,
@@ -44507,6 +44538,8 @@ async function writeSectionsToStorage(p, sections) {
44507
44538
  if (sections.cases.length > 0) payload.cases = sections.cases;
44508
44539
  if (sections.actions.length > 0) payload.actions = sections.actions;
44509
44540
  if (sections.stateMachines.length > 0) payload.stateMachines = sections.stateMachines;
44541
+ const pkgSeg = String(p).replace(/^\/+/, "").split("/")[0];
44542
+ if (pkgSeg) invalidatePackageCache(pkgSeg);
44510
44543
  if (Object.keys(payload).length === 0) {
44511
44544
  await deleteIfExists(p);
44512
44545
  return;
@@ -44519,7 +44552,13 @@ async function listPackageYamlFilePaths(packageName) {
44519
44552
  const entries = await getStorage().list(WS, dir);
44520
44553
  return entries.filter((e) => !e.isDirectory).map((e) => e.name).filter((f) => f.endsWith(".yaml") && !RESERVED_PACKAGE_FILES.has(f)).sort().map((f) => pathOf(`${packageName}/${f}`));
44521
44554
  }
44555
+ function invalidatePackageCache(packageName) {
44556
+ if (packageName === void 0) _packageCache.clear();
44557
+ else _packageCache.delete(packageName);
44558
+ }
44522
44559
  async function loadPackage(packageName) {
44560
+ const cached2 = _packageCache.get(packageName);
44561
+ if (cached2 && cached2.expires > Date.now()) return cached2.model;
44523
44562
  const files = await listPackageYamlFilePaths(packageName);
44524
44563
  const parsed = await Promise.all(
44525
44564
  files.map(async (p) => ({
@@ -44527,7 +44566,9 @@ async function loadPackage(packageName) {
44527
44566
  sections: await parseSectionsFromStorage(p, String(p))
44528
44567
  }))
44529
44568
  );
44530
- return mergePackageSections(packageName, parsed);
44569
+ const model = mergePackageSections(packageName, parsed);
44570
+ _packageCache.set(packageName, { model, expires: Date.now() + PACKAGE_CACHE_TTL_MS });
44571
+ return model;
44531
44572
  }
44532
44573
  function getSchemaPackagePath() {
44533
44574
  return path4.join(getDataDir(), ".dico", "schemas");
@@ -44684,7 +44725,13 @@ async function readRelationshipsFile(packagePath) {
44684
44725
  try {
44685
44726
  const packageName = path4.basename(packagePath);
44686
44727
  const pkg = await loadPackage(packageName);
44687
- return pkg.relationships;
44728
+ const normalized = [];
44729
+ for (const rel of pkg.relationships) {
44730
+ const n = normalizeRelationship(rel);
44731
+ if (n) normalized.push(n);
44732
+ else logger.warn(`Skipping malformed relationship ${rel.uuid} in package '${packageName}': no ends and no source/target`);
44733
+ }
44734
+ return normalized;
44688
44735
  } catch (error48) {
44689
44736
  logger.error(`Error reading relationships file: ${error48}`);
44690
44737
  return [];
@@ -45244,7 +45291,7 @@ async function deleteStateMachine(uuid3) {
45244
45291
  return { ok: false };
45245
45292
  }
45246
45293
  }
45247
- var import_yaml, WS, getDataDir, RESERVED_DIRS, RESERVED_PACKAGE_FILES, VALIDATION_FIELD_NAMES, gitServiceInstance;
45294
+ var import_yaml, WS, getDataDir, RESERVED_DIRS, RESERVED_PACKAGE_FILES, VALIDATION_FIELD_NAMES, gitServiceInstance, PACKAGE_CACHE_TTL_MS, _packageCache;
45248
45295
  var init_fileOperations = __esm({
45249
45296
  "backend/src/utils/fileOperations.ts"() {
45250
45297
  "use strict";
@@ -45289,6 +45336,10 @@ var init_fileOperations = __esm({
45289
45336
  __name(getReservedPackageFiles, "getReservedPackageFiles");
45290
45337
  __name(writeSectionsToStorage, "writeSectionsToStorage");
45291
45338
  __name(listPackageYamlFilePaths, "listPackageYamlFilePaths");
45339
+ PACKAGE_CACHE_TTL_MS = 2e3;
45340
+ _packageCache = /* @__PURE__ */ new Map();
45341
+ __name(invalidatePackageCache, "invalidatePackageCache");
45342
+ storageRegistry.onBackendChange(() => invalidatePackageCache());
45292
45343
  __name(loadPackage, "loadPackage");
45293
45344
  __name(getSchemaPackagePath, "getSchemaPackagePath");
45294
45345
  __name(listSchemaPackageYamlFilePaths, "listSchemaPackageYamlFilePaths");
@@ -45629,21 +45680,18 @@ var init_dictionaryService = __esm({
45629
45680
  try {
45630
45681
  if (id.startsWith("microservices/")) {
45631
45682
  const microservice = id.replace("microservices/", "");
45632
- const entityNames = await listMicroserviceEntities(microservice);
45683
+ const pkg = await loadPackage(microservice);
45633
45684
  const entries = [];
45634
- for (const entityName of entityNames) {
45635
- const entity = await readEntityFile(microservice, entityName);
45636
- if (entity) {
45637
- for (const attr of entity.attributes || []) {
45638
- entries.push({
45639
- id: `${entity.uuid || ""}_${attr.name}`,
45640
- name: attr.name,
45641
- description: attr.description || "",
45642
- type: attr.type || "string",
45643
- format: attr.validation?.format,
45644
- required: attr.required || false
45645
- });
45646
- }
45685
+ for (const entity of pkg.entities) {
45686
+ for (const attr of entity.attributes || []) {
45687
+ entries.push({
45688
+ id: `${entity.uuid || ""}_${attr.name}`,
45689
+ name: attr.name,
45690
+ description: attr.description || "",
45691
+ type: attr.type || "string",
45692
+ format: attr.validation?.format,
45693
+ required: attr.required || false
45694
+ });
45647
45695
  }
45648
45696
  }
45649
45697
  return entries;
@@ -45791,9 +45839,8 @@ var init_dictionaryService = __esm({
45791
45839
  const microservices = await listMicroservices();
45792
45840
  const result = [];
45793
45841
  for (const microservice of microservices) {
45794
- const entityNames = await listMicroserviceEntities(microservice);
45795
- for (const entityName of entityNames) {
45796
- const entity = await readEntityFile(microservice, entityName);
45842
+ const pkg = await loadPackage(microservice);
45843
+ for (const entity of pkg.entities) {
45797
45844
  if (!entity) continue;
45798
45845
  if (filters.name && !entity.name.toLowerCase().includes(filters.name.toLowerCase())) {
45799
45846
  continue;
@@ -45882,13 +45929,8 @@ var init_dictionaryService = __esm({
45882
45929
  }
45883
45930
  }
45884
45931
  async getServiceEntities(service) {
45885
- const entityNames = await listMicroserviceEntities(service);
45886
- const entities = [];
45887
- for (const name21 of entityNames) {
45888
- const entity = await readEntityFile(service, name21);
45889
- if (entity) entities.push(entity);
45890
- }
45891
- return entities;
45932
+ const pkg = await loadPackage(service);
45933
+ return pkg.entities;
45892
45934
  }
45893
45935
  };
45894
45936
  dictionaryService = new DictionaryService();
@@ -47321,29 +47363,13 @@ var init_serviceService = __esm({
47321
47363
  }
47322
47364
  }
47323
47365
  async getServiceEntities(service) {
47324
- logger.info(`Getting entities for service: ${service}`);
47325
47366
  const startTime = process.hrtime();
47326
47367
  try {
47327
- const listStartTime = process.hrtime();
47328
- const entityNames = await listMicroserviceEntities(service);
47329
- const listEndTime = process.hrtime(listStartTime);
47330
- const listTimeMs = Number((listEndTime[0] * 1e3 + listEndTime[1] / 1e6).toFixed(2));
47331
- logger.info(`Listed ${entityNames.length} entity names for service ${service} in ${listTimeMs}ms`);
47332
- const entities = [];
47333
- const readStartTime = process.hrtime();
47334
- for (const entityName of entityNames) {
47335
- const entity = await readEntityFile(service, entityName);
47336
- if (entity) {
47337
- entities.push(entity);
47338
- }
47339
- }
47340
- const readEndTime = process.hrtime(readStartTime);
47341
- const readTimeMs = Number((readEndTime[0] * 1e3 + readEndTime[1] / 1e6).toFixed(2));
47342
- logger.info(`Read ${entities.length} entity files for service ${service} in ${readTimeMs}ms`);
47368
+ const pkg = await loadPackage(service);
47343
47369
  const endTime = process.hrtime(startTime);
47344
47370
  const totalTimeMs = Number((endTime[0] * 1e3 + endTime[1] / 1e6).toFixed(2));
47345
- logger.info(`Total time to get entities for service ${service}: ${totalTimeMs}ms (list: ${listTimeMs}ms, read: ${readTimeMs}ms)`);
47346
- return entities;
47371
+ logger.info(`Got ${pkg.entities.length} entities for service ${service} in ${totalTimeMs}ms`);
47372
+ return pkg.entities;
47347
47373
  } catch (error48) {
47348
47374
  logger.error(`Error getting service entities: ${error48}`);
47349
47375
  return [];
@@ -47970,6 +47996,17 @@ async function getGitService2() {
47970
47996
  return null;
47971
47997
  }
47972
47998
  }
47999
+ function describeError(e) {
48000
+ if (e instanceof Error) return e.message;
48001
+ try {
48002
+ return JSON.stringify(e);
48003
+ } catch {
48004
+ return String(e);
48005
+ }
48006
+ }
48007
+ function isNotAGitRepo(detail) {
48008
+ return /not a git repo|could not find .*git repo|NotGitRepo|NOT_A_REPO|fatal: not a git/i.test(detail);
48009
+ }
47973
48010
  var gitServiceInstance2, VersionService, versionService;
47974
48011
  var init_versionService = __esm({
47975
48012
  "backend/src/services/versionService.ts"() {
@@ -47978,6 +48015,8 @@ var init_versionService = __esm({
47978
48015
  init_config();
47979
48016
  gitServiceInstance2 = null;
47980
48017
  __name(getGitService2, "getGitService");
48018
+ __name(describeError, "describeError");
48019
+ __name(isNotAGitRepo, "isNotAGitRepo");
47981
48020
  VersionService = class {
47982
48021
  static {
47983
48022
  __name(this, "VersionService");
@@ -47996,7 +48035,12 @@ var init_versionService = __esm({
47996
48035
  const ddFiles = (status.files || []).map((f) => f.path).filter((p) => p.startsWith(ddPrefix));
47997
48036
  return { clean: ddFiles.length === 0, files: ddFiles };
47998
48037
  } catch (error48) {
47999
- logger.warn(`getWorkingTreeStatus failed: ${error48}`);
48038
+ const detail = describeError(error48);
48039
+ if (isNotAGitRepo(detail)) {
48040
+ logger.debug(`getWorkingTreeStatus: project is not a git repository \u2014 treating as clean (${detail})`);
48041
+ } else {
48042
+ logger.warn(`getWorkingTreeStatus failed: ${detail}`);
48043
+ }
48000
48044
  return { clean: true, files: [] };
48001
48045
  }
48002
48046
  }
@@ -99846,7 +99890,7 @@ async function auth(provider, options2) {
99846
99890
  throw error48;
99847
99891
  }
99848
99892
  }
99849
- async function authInternal(provider, { serverUrl: serverUrl2, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
99893
+ async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
99850
99894
  const cachedState = await provider.discoveryState?.();
99851
99895
  let resourceMetadata;
99852
99896
  let authorizationServerUrl;
@@ -99861,7 +99905,7 @@ async function authInternal(provider, { serverUrl: serverUrl2, authorizationCode
99861
99905
  metadata = cachedState.authorizationServerMetadata ?? await discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn });
99862
99906
  if (!resourceMetadata) {
99863
99907
  try {
99864
- resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl2, { resourceMetadataUrl: effectiveResourceMetadataUrl }, fetchFn);
99908
+ resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl }, fetchFn);
99865
99909
  } catch {
99866
99910
  }
99867
99911
  }
@@ -99874,7 +99918,7 @@ async function authInternal(provider, { serverUrl: serverUrl2, authorizationCode
99874
99918
  });
99875
99919
  }
99876
99920
  } else {
99877
- const serverInfo = await discoverOAuthServerInfo(serverUrl2, { resourceMetadataUrl: effectiveResourceMetadataUrl, fetchFn });
99921
+ const serverInfo = await discoverOAuthServerInfo(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl, fetchFn });
99878
99922
  authorizationServerUrl = serverInfo.authorizationServerUrl;
99879
99923
  metadata = serverInfo.authorizationServerMetadata;
99880
99924
  resourceMetadata = serverInfo.resourceMetadata;
@@ -99885,7 +99929,7 @@ async function authInternal(provider, { serverUrl: serverUrl2, authorizationCode
99885
99929
  authorizationServerMetadata: metadata
99886
99930
  });
99887
99931
  }
99888
- const resource = await selectResourceURL(serverUrl2, provider, resourceMetadata);
99932
+ const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
99889
99933
  const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(" ") || provider.clientMetadata.scope;
99890
99934
  let clientInformation = await Promise.resolve(provider.clientInformation());
99891
99935
  if (!clientInformation) {
@@ -99971,8 +100015,8 @@ function isHttpsUrl(value) {
99971
100015
  return false;
99972
100016
  }
99973
100017
  }
99974
- async function selectResourceURL(serverUrl2, provider, resourceMetadata) {
99975
- const defaultResource = resourceUrlFromServerUrl(serverUrl2);
100018
+ async function selectResourceURL(serverUrl, provider, resourceMetadata) {
100019
+ const defaultResource = resourceUrlFromServerUrl(serverUrl);
99976
100020
  if (provider.validateResourceURL) {
99977
100021
  return await provider.validateResourceURL(defaultResource, resourceMetadata?.resource);
99978
100022
  }
@@ -100021,8 +100065,8 @@ function extractFieldFromWwwAuth(response, fieldName) {
100021
100065
  }
100022
100066
  return null;
100023
100067
  }
100024
- async function discoverOAuthProtectedResourceMetadata(serverUrl2, opts, fetchFn = fetch) {
100025
- const response = await discoverMetadataWithFallback(serverUrl2, "oauth-protected-resource", fetchFn, {
100068
+ async function discoverOAuthProtectedResourceMetadata(serverUrl, opts, fetchFn = fetch) {
100069
+ const response = await discoverMetadataWithFallback(serverUrl, "oauth-protected-resource", fetchFn, {
100026
100070
  protocolVersion: opts?.protocolVersion,
100027
100071
  metadataUrl: opts?.resourceMetadataUrl
100028
100072
  });
@@ -100065,8 +100109,8 @@ async function tryMetadataDiscovery(url2, protocolVersion, fetchFn = fetch) {
100065
100109
  function shouldAttemptFallback(response, pathname) {
100066
100110
  return !response || response.status >= 400 && response.status < 500 && pathname !== "/";
100067
100111
  }
100068
- async function discoverMetadataWithFallback(serverUrl2, wellKnownType, fetchFn, opts) {
100069
- const issuer = new URL(serverUrl2);
100112
+ async function discoverMetadataWithFallback(serverUrl, wellKnownType, fetchFn, opts) {
100113
+ const issuer = new URL(serverUrl);
100070
100114
  const protocolVersion = opts?.protocolVersion ?? LATEST_PROTOCOL_VERSION;
100071
100115
  let url2;
100072
100116
  if (opts?.metadataUrl) {
@@ -100142,18 +100186,18 @@ async function discoverAuthorizationServerMetadata(authorizationServerUrl, { fet
100142
100186
  }
100143
100187
  return void 0;
100144
100188
  }
100145
- async function discoverOAuthServerInfo(serverUrl2, opts) {
100189
+ async function discoverOAuthServerInfo(serverUrl, opts) {
100146
100190
  let resourceMetadata;
100147
100191
  let authorizationServerUrl;
100148
100192
  try {
100149
- resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl2, { resourceMetadataUrl: opts?.resourceMetadataUrl }, opts?.fetchFn);
100193
+ resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: opts?.resourceMetadataUrl }, opts?.fetchFn);
100150
100194
  if (resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) {
100151
100195
  authorizationServerUrl = resourceMetadata.authorization_servers[0];
100152
100196
  }
100153
100197
  } catch {
100154
100198
  }
100155
100199
  if (!authorizationServerUrl) {
100156
- authorizationServerUrl = String(new URL("/", serverUrl2));
100200
+ authorizationServerUrl = String(new URL("/", serverUrl));
100157
100201
  }
100158
100202
  const authorizationServerMetadata = await discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn: opts?.fetchFn });
100159
100203
  return {
@@ -148774,7 +148818,8 @@ var init_UuidIndex = __esm({
148774
148818
  }
148775
148819
  this.rebuildInFlight = true;
148776
148820
  try {
148777
- const { listPackages: listPackages2 } = await Promise.resolve().then(() => (init_fileOperations(), fileOperations_exports));
148821
+ const { listPackages: listPackages2, invalidatePackageCache: invalidatePackageCache2 } = await Promise.resolve().then(() => (init_fileOperations(), fileOperations_exports));
148822
+ invalidatePackageCache2();
148778
148823
  const top = await listPackages2();
148779
148824
  const allPackages = [];
148780
148825
  for (const pkg of top) {
@@ -148899,6 +148944,8 @@ var init_UuidIndex = __esm({
148899
148944
  while (this.rebuildInFlight) {
148900
148945
  await new Promise((r) => setTimeout(r, 0));
148901
148946
  }
148947
+ const { invalidatePackageCache: invalidatePackageCache2 } = await Promise.resolve().then(() => (init_fileOperations(), fileOperations_exports));
148948
+ invalidatePackageCache2(packageName);
148902
148949
  const packagePrefix = `packages/${packageName}/entities/`;
148903
148950
  const previouslyMappedUuids = /* @__PURE__ */ new Set();
148904
148951
  for (const [uuid3, lp] of this.uuidToPath) {
@@ -156981,13 +157028,13 @@ async function resolveProjectPrefixAtRef(ref) {
156981
157028
  logger.warn(`Could not resolve git repo root: ${e}`);
156982
157029
  return null;
156983
157030
  }
156984
- const candidates2 = [];
157031
+ const candidates = [];
156985
157032
  const rel = path6.relative(repoRoot, config.dataDir);
156986
157033
  const primary = !rel || rel === "." || rel.startsWith("..") ? "" : rel.replace(/\\/g, "/") + "/";
156987
- candidates2.push(primary);
156988
- if (primary !== "data-dictionaries/") candidates2.push("data-dictionaries/");
156989
- if (primary !== "") candidates2.push("");
156990
- for (const prefix of candidates2) {
157034
+ candidates.push(primary);
157035
+ if (primary !== "data-dictionaries/") candidates.push("data-dictionaries/");
157036
+ if (primary !== "") candidates.push("");
157037
+ for (const prefix of candidates) {
156991
157038
  const marker21 = await readFileAtRef(ref, `${prefix}dico.config.json`);
156992
157039
  if (marker21 !== null) return prefix;
156993
157040
  }
@@ -160474,9 +160521,9 @@ var QualityService = class {
160474
160521
  const services = service ? [service] : await listMicroservices();
160475
160522
  const packages = [];
160476
160523
  for (const svc of services) {
160477
- const entityNames = await listMicroserviceEntities(svc);
160524
+ const pkg = await loadPackage(svc);
160478
160525
  const entities = [];
160479
- let relEntityUuids = /* @__PURE__ */ new Set();
160526
+ const relEntityUuids = /* @__PURE__ */ new Set();
160480
160527
  try {
160481
160528
  const rels = await readRelationshipsFile(getPackagePath(svc));
160482
160529
  for (const rel of rels) {
@@ -160485,9 +160532,7 @@ var QualityService = class {
160485
160532
  }
160486
160533
  } catch {
160487
160534
  }
160488
- for (const rawName of entityNames) {
160489
- const name21 = rawName.includes("_") ? rawName.split("_").slice(1).join("_") : rawName;
160490
- const entity = await readEntityFile(svc, name21);
160535
+ for (const entity of pkg.entities) {
160491
160536
  if (!entity) continue;
160492
160537
  const descFilled = !!entity.description;
160493
160538
  const totalAttrs = entity.attributes.length;
@@ -162295,6 +162340,13 @@ async function mountFrameworkRoutes() {
162295
162340
  workspaceRoots
162296
162341
  });
162297
162342
  enricherRegistry2.register(gitEnricher);
162343
+ app.use("/api/git/dictionaries/status", (_req, res, next) => {
162344
+ if (!fs5.existsSync(path13.join(config.dataDir, ".git"))) {
162345
+ res.json({ files: [], hasUncommittedChanges: false });
162346
+ return;
162347
+ }
162348
+ next();
162349
+ });
162298
162350
  const gitRoutes = gitModule.createGitRoutes({ gitService, debug: !config.isProduction });
162299
162351
  app.use("/api/git", gitRoutes);
162300
162352
  app.use("/api/git", gitModule.gitErrorHandler);
@@ -162317,18 +162369,16 @@ mountFrameworkRoutes().catch((err) => {
162317
162369
  logger.warn(`Failed to mount framework routes: ${err}`);
162318
162370
  });
162319
162371
  if (config.isProduction) {
162320
- const serverUrl = eval("import.meta.url");
162321
- const serverDir = path13.dirname(new URL(serverUrl).pathname);
162322
162372
  const candidates = [
162323
- path13.join(serverDir, "..", "..", "frontend", "dist"),
162324
- // npm package (server is at backend/src/)
162373
+ process.env.SDD_FRONTEND_DIST || "",
162374
+ // npm package (set by bin/cli.js)
162325
162375
  path13.join(process.cwd(), "public"),
162326
162376
  // Docker (copied to public/)
162327
162377
  path13.join(process.cwd(), "..", "frontend", "dist"),
162328
162378
  // monorepo dev
162329
162379
  path13.join(process.cwd(), "frontend", "dist")
162330
162380
  // alt layout
162331
- ];
162381
+ ].filter(Boolean);
162332
162382
  const publicDir = candidates.find((d) => {
162333
162383
  try {
162334
162384
  return fs5.statSync(d).isDirectory();
package/bin/cli.js CHANGED
@@ -114,6 +114,7 @@ const child = spawn(bin, binArgs, {
114
114
  NODE_ENV: 'production',
115
115
  PROFILE: process.env.PROFILE || 'local',
116
116
  DATA_DIR: dataDir,
117
+ SDD_FRONTEND_DIST: frontendDist,
117
118
  },
118
119
  stdio: 'inherit',
119
120
  });