@qaecy/cue-cli 0.0.47 → 0.0.49

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/main.js CHANGED
@@ -5141,6 +5141,7 @@ var ENDPOINT_COMMANDS_PROFILE_API_KEYS = "/commands/admin/profile/api-keys";
5141
5141
  var ENDPOINT_COMMANDS_PROFILE_TERMS = "/commands/admin/profile/terms";
5142
5142
  var ENDPOINT_ORG_MEMBERS = (orgId) => `/data-views/admin/organizations/${orgId}/members`;
5143
5143
  var ENDPOINT_CREATE_PROJECT = "/commands/admin/project";
5144
+ var ENDPOINT_DELETE_PROJECT = (projectId) => `/commands/admin/project/${projectId}`;
5144
5145
  var ENDPOINT_SEARCH = "/assistant/search";
5145
5146
  var ENDPOINT_FUSEKI_QUERY = "/triplestore/query";
5146
5147
  var ENDPOINT_FUSEKI_UPDATE = "/triplestore/update";
@@ -5268,6 +5269,11 @@ var CueAuth = class {
5268
5269
  const result = await (0, import_auth3.signInWithCustomToken)(this._auth, token);
5269
5270
  return result.user;
5270
5271
  }
5272
+ /** Sign in with a Firebase custom token (e.g. minted server-side for MCP/OAuth flows). */
5273
+ async signInWithCustomToken(token) {
5274
+ const result = await (0, import_auth3.signInWithCustomToken)(this._auth, token);
5275
+ return result.user;
5276
+ }
5271
5277
  /** Sign out the current user */
5272
5278
  async signOut() {
5273
5279
  await (0, import_auth3.signOut)(this._auth);
@@ -5339,6 +5345,46 @@ var CueAuth = class {
5339
5345
  }
5340
5346
  };
5341
5347
 
5348
+ // libs/js/cue-sdk/src/lib/storage.ts
5349
+ var CueStorage = class {
5350
+ constructor(_blob) {
5351
+ this._blob = _blob;
5352
+ }
5353
+ /**
5354
+ * Returns a Firebase authenticated download URL for a document stored in Cue.
5355
+ *
5356
+ * The storage path is `{projectId}/{uuid}{suffix}`, e.g. `my-project/abc-123.pdf`.
5357
+ *
5358
+ * @param projectId - The Cue project (space) ID.
5359
+ * @param uuid - The document UUID.
5360
+ * @param suffix - File suffix including the leading dot, e.g. `'.pdf'`, `'.ifc'`.
5361
+ * @param bucket - `'raw'` (default, original uploads) or `'processed'` (derived artefacts).
5362
+ */
5363
+ async getDownloadUrl(projectId, uuid, suffix, bucket = "raw") {
5364
+ const path = `${projectId}/${uuid}${suffix}`;
5365
+ const url = await this._blob.getDownloadURL(bucket, path);
5366
+ if (!url)
5367
+ throw new Error(`File not found in storage: ${path} (bucket: ${bucket})`);
5368
+ return url;
5369
+ }
5370
+ /**
5371
+ * Returns a Firebase authenticated download URL for an alternative representation
5372
+ * using its full `qcy:remoteRelativePath` stored in the processed bucket.
5373
+ *
5374
+ * Use this instead of `getDownloadUrl` when the document info was obtained via
5375
+ * `fetchAlternativeRepresentations` and carries a `remoteRelativePath`.
5376
+ *
5377
+ * @param remoteRelativePath - The full path in the processed bucket,
5378
+ * e.g. `{projectId}/fragments/{uuid}.fragments`.
5379
+ */
5380
+ async getAltRepDownloadUrl(remoteRelativePath) {
5381
+ const url = await this._blob.getDownloadURL("processed", remoteRelativePath);
5382
+ if (!url)
5383
+ throw new Error(`Alternative representation not found in storage: ${remoteRelativePath}`);
5384
+ return url;
5385
+ }
5386
+ };
5387
+
5342
5388
  // libs/js/cue-sdk/src/lib/tables.ts
5343
5389
  var ENDPOINT_DATA_VIEWS_TABLES = "/data-views/tables";
5344
5390
  var ENDPOINT_COMMANDS_TABLES = "/commands/tables";
@@ -5382,6 +5428,55 @@ var CueTables = class {
5382
5428
  }
5383
5429
  };
5384
5430
 
5431
+ // libs/js/cue-sdk/src/lib/extraction.ts
5432
+ var ENDPOINT_EXTRACTION = "/semantic-extraction/extract";
5433
+ var CueExtraction = class {
5434
+ constructor(_auth, _gatewayUrl) {
5435
+ this._auth = _auth;
5436
+ this._gatewayUrl = _gatewayUrl;
5437
+ }
5438
+ /**
5439
+ * Run semantic extraction on a document page image.
5440
+ *
5441
+ * Sends a multipart/form-data POST to `/semantic-extraction/extract`.
5442
+ * Always requests JSON-LD so the result can be displayed directly in
5443
+ * `cue-rdf-graph` without any further parsing.
5444
+ */
5445
+ async extract(request) {
5446
+ const fmt = request.rdfFormat ?? "json-ld";
5447
+ const body = new FormData();
5448
+ body.append("file", request.image, "page.png");
5449
+ body.append("template", JSON.stringify(request.template));
5450
+ body.append("space_id", request.projectId);
5451
+ body.append("rdf_format", fmt);
5452
+ if (request.category)
5453
+ body.append("category", request.category);
5454
+ if (request.text)
5455
+ body.append("text", request.text);
5456
+ const response = await this._auth.authenticatedFetch(
5457
+ `${this._gatewayUrl}${ENDPOINT_EXTRACTION}`,
5458
+ {
5459
+ method: "POST",
5460
+ // Do NOT set Content-Type; browser sets it with the correct boundary.
5461
+ headers: {
5462
+ "Accept": "application/ld+json",
5463
+ "x-project-id": request.projectId,
5464
+ "cue-project-id": request.projectId
5465
+ },
5466
+ body
5467
+ }
5468
+ );
5469
+ if (!response.ok) {
5470
+ const msg = await response.text().catch(() => "");
5471
+ throw new Error(
5472
+ `Extraction failed: ${response.status} ${response.statusText}${msg ? " \u2014 " + msg.slice(0, 300) : ""}`
5473
+ );
5474
+ }
5475
+ const jsonld = await response.json();
5476
+ return { jsonld };
5477
+ }
5478
+ };
5479
+
5385
5480
  // libs/js/cue-sdk/src/lib/api.ts
5386
5481
  var CueApi = class {
5387
5482
  constructor(_auth, _gatewayUrl, projects, sync) {
@@ -5390,8 +5485,11 @@ var CueApi = class {
5390
5485
  this.projects = projects;
5391
5486
  this.sync = sync;
5392
5487
  this.tables = new CueTables(_auth, _gatewayUrl);
5488
+ this.extraction = new CueExtraction(_auth, _gatewayUrl);
5393
5489
  }
5394
5490
  tables;
5491
+ /** Semantic extraction client — call document pages against a SemanticTemplate. */
5492
+ extraction;
5395
5493
  /** Active language used for language-sensitive SPARQL queries across all project classes. */
5396
5494
  language = "en";
5397
5495
  /** Updates the active language. All project classes (`CueProjectSchema`, `CueProjectDocuments`, `CueProjectEntities`) read this at query time. */
@@ -7354,6 +7452,19 @@ var CueProjects = class {
7354
7452
  const fn = (0, import_functions2.httpsCallable)(this._functions, "removeUserFromProject");
7355
7453
  await fn({ uid, spaceId: projectId });
7356
7454
  }
7455
+ /**
7456
+ * Delete a project by ID. Requires superadmin privileges on the server.
7457
+ */
7458
+ async deleteProject(projectId) {
7459
+ const response = await this._auth.authenticatedFetch(
7460
+ `${this._gatewayUrl}${ENDPOINT_DELETE_PROJECT(projectId)}`,
7461
+ { method: "DELETE" }
7462
+ );
7463
+ if (!response.ok) {
7464
+ const body = await response.text().catch(() => "");
7465
+ throw new Error(`Failed to delete project: ${response.status} ${response.statusText}${body ? ` \u2014 ${body}` : ""}`);
7466
+ }
7467
+ }
7357
7468
  };
7358
7469
 
7359
7470
  // libs/js/cue-sdk/src/lib/profile.ts
@@ -7527,6 +7638,7 @@ var REQUIRED_ROLES = {
7527
7638
  createProvider: "superadmin",
7528
7639
  changeContentCategories: "syncer",
7529
7640
  deleteDocuments: "superadmin",
7641
+ deleteProject: "admin",
7530
7642
  deleteUserFromProject: "admin",
7531
7643
  downloadDocuments: "member",
7532
7644
  editContentCategories: "syncer",
@@ -7541,8 +7653,10 @@ function defaultPrivileges() {
7541
7653
  return {
7542
7654
  changeContentCategories: false,
7543
7655
  createEntities: false,
7656
+ createProject: false,
7544
7657
  createProvider: false,
7545
7658
  deleteDocuments: false,
7659
+ deleteProject: false,
7546
7660
  deleteUserFromProject: false,
7547
7661
  downloadDocuments: false,
7548
7662
  editContentCategories: false,
@@ -7558,18 +7672,31 @@ var CuePrivileges = class {
7558
7672
  constructor(_isSuperAdmin) {
7559
7673
  this._isSuperAdmin = _isSuperAdmin;
7560
7674
  this._projectRoles = new CueSignal([]);
7675
+ this._orgRole = new CueSignal(null);
7561
7676
  this.privileges = cueComputed(
7562
- [this._projectRoles, _isSuperAdmin],
7677
+ [this._projectRoles, this._orgRole, _isSuperAdmin],
7563
7678
  () => this._compute()
7564
7679
  );
7565
7680
  }
7566
7681
  _projectRoles;
7682
+ _orgRole;
7567
7683
  /**
7568
7684
  * Reactive signal — current user's privileges for the selected project.
7569
7685
  * Recomputes automatically when `setProjectRoles()` is called or when
7570
7686
  * the `isSuperAdmin` signal changes.
7571
7687
  */
7572
7688
  privileges;
7689
+ /**
7690
+ * Set the user's role within the organisation that owns the selected project.
7691
+ * Pass `null` to clear the organisation role (e.g. when deselecting a project).
7692
+ *
7693
+ * Only organisation admins (and superadmins) may create projects.
7694
+ */
7695
+ setOrgRole(role) {
7696
+ if (this._orgRole.get() !== role) {
7697
+ this._orgRole.set(role);
7698
+ }
7699
+ }
7573
7700
  /**
7574
7701
  * Set the user's roles for the currently selected project.
7575
7702
  *
@@ -7596,10 +7723,12 @@ var CuePrivileges = class {
7596
7723
  }
7597
7724
  _compute() {
7598
7725
  const roles = this._projectRoles.get();
7726
+ const isSuperAdmin = this._isSuperAdmin.get();
7599
7727
  const result = defaultPrivileges();
7600
7728
  for (const key of Object.keys(REQUIRED_ROLES)) {
7601
7729
  result[key] = roles.includes(REQUIRED_ROLES[key]);
7602
7730
  }
7731
+ result.createProject = isSuperAdmin || this._orgRole.get() === "admin";
7603
7732
  return result;
7604
7733
  }
7605
7734
  };
@@ -7852,11 +7981,12 @@ GROUP BY ?iri ?parent`;
7852
7981
  var OSM_ENDPOINT = "https://qlever.dev/api/osm-planet";
7853
7982
  var EXCLUDE_OSM_RELATION = true;
7854
7983
  var CueProjectEntities = class {
7855
- constructor(_api, _projectId, rdfBase = RESOURCE_BASE, _queryCache, _graphType) {
7984
+ constructor(_api, _projectId, rdfBase = RESOURCE_BASE, _queryCache, _graphType, _verbose = false) {
7856
7985
  this._api = _api;
7857
7986
  this._projectId = _projectId;
7858
7987
  this._queryCache = _queryCache;
7859
7988
  this._graphType = _graphType;
7989
+ this._verbose = _verbose;
7860
7990
  this.baseURL = `${rdfBase}${_projectId}/`;
7861
7991
  this.entityInfoMap = this._entityInfoMapComputed;
7862
7992
  this.entityGraph = this._entityGraph.asReadonly();
@@ -7876,6 +8006,8 @@ var CueProjectEntities = class {
7876
8006
  _entityOSMMap = new CueSignal({});
7877
8007
  _osmWKTMap = new CueSignal({});
7878
8008
  _fetchingOSMIds = /* @__PURE__ */ new Set();
8009
+ /** Cumulative unique entity UUIDs ever passed to request methods (survives cache hits). */
8010
+ _seenIds = /* @__PURE__ */ new Set();
7879
8011
  _entityGraph = new CueSignal(void 0);
7880
8012
  // ── Derived signals ────────────────────────────────────────────────────────
7881
8013
  _entityInfoMapComputed = cueComputed(
@@ -7900,6 +8032,15 @@ var CueProjectEntities = class {
7900
8032
  entityIri(uuid) {
7901
8033
  return `${this.baseURL}${uuid}`;
7902
8034
  }
8035
+ /** @internal Builds a full resource IRI from a UUID without a SPARQL round-trip. */
8036
+ _resourceIri(uuid) {
8037
+ return `${this.baseURL}${uuid}`;
8038
+ }
8039
+ _log(message) {
8040
+ if (this._verbose) {
8041
+ console.debug(`[CueProjectEntities pid=${this._projectId}] ${message}`);
8042
+ }
8043
+ }
7903
8044
  /**
7904
8045
  * Resets all entity state and re-fetches the entity graph.
7905
8046
  * Call when the active project changes.
@@ -7912,6 +8053,7 @@ var CueProjectEntities = class {
7912
8053
  this._osmWKTMap.set({});
7913
8054
  this._entityGraph.set(void 0);
7914
8055
  this._fetchingOSMIds.clear();
8056
+ this._seenIds.clear();
7915
8057
  this._fetchEntityGraph().catch(
7916
8058
  (err) => console.error(
7917
8059
  "[CueProjectEntities] Entity graph fetch failed after reset:",
@@ -7927,11 +8069,14 @@ var CueProjectEntities = class {
7927
8069
  * Data is merged into `entityInfoMap` once the SPARQL response arrives.
7928
8070
  */
7929
8071
  requestEntityData(uuids, includeMentionCount = false) {
8072
+ for (const id of uuids)
8073
+ this._seenIds.add(id);
7930
8074
  const newUUIDs = uuids.filter(
7931
8075
  (id) => this._entityDetails.get()[id] === void 0
7932
8076
  );
7933
8077
  if (newUUIDs.length === 0)
7934
8078
  return;
8079
+ this._log(`requestEntityData: ${newUUIDs.length} new / ${uuids.length} requested | cumulative: ${this._seenIds.size} seen`);
7935
8080
  const values = newUUIDs.map((id) => `r:${id}`).join(" ");
7936
8081
  const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
7937
8082
  PREFIX r: <${this.baseURL}>
@@ -7961,6 +8106,7 @@ GROUP BY ?id ?mentionCount`;
7961
8106
  };
7962
8107
  });
7963
8108
  this._entityDetails.set(updates);
8109
+ this._log(`entityDetails: ${Object.keys(updates).length} with metadata / ${this._seenIds.size} seen (cumulative)`);
7964
8110
  }).catch(
7965
8111
  (err) => console.error("[CueProjectEntities] requestEntityData failed:", err)
7966
8112
  );
@@ -7978,6 +8124,9 @@ GROUP BY ?id ?mentionCount`;
7978
8124
  );
7979
8125
  if (newUUIDs.length === 0)
7980
8126
  return;
8127
+ for (const id of uuids)
8128
+ this._seenIds.add(id);
8129
+ this._log(`requestEntityLocations: ${newUUIDs.length} new / ${uuids.length} requested | cumulative: ${this._seenIds.size} seen`);
7981
8130
  const osmInit = { ...this._entityOSMMap.get() };
7982
8131
  for (const id of newUUIDs)
7983
8132
  osmInit[id] = { direct: [], indirect: [] };
@@ -8034,6 +8183,7 @@ WHERE {
8034
8183
  update[id] = entry;
8035
8184
  });
8036
8185
  this._entityOSMMap.set(update);
8186
+ this._log(`entityOSMMap: ${Object.keys(update).length} with OSM / ${this._seenIds.size} seen (cumulative)`);
8037
8187
  }
8038
8188
  /**
8039
8189
  * Fetches incoming and outgoing relationships for a single entity IRI.
@@ -8124,7 +8274,51 @@ ORDER BY ${orderByOccurences ? "DESC(?count)" : "ASC(?label)"}`;
8124
8274
  };
8125
8275
  });
8126
8276
  }
8127
- async buildSummaryGraph(format) {
8277
+ async entitiesByCategory(categoryIris, includeMetadata = false) {
8278
+ if (categoryIris.length === 0)
8279
+ return [];
8280
+ const ce = CompactExpand.getInstance();
8281
+ const normalise = (iri) => iri.startsWith("http://") || iri.startsWith("https://") ? iri : ce.expandIRI(iri);
8282
+ const values = categoryIris.map((c) => `<${normalise(c)}>`).join(" ");
8283
+ const q = includeMetadata ? `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
8284
+ SELECT ?id (SAMPLE(?val) AS ?value) (GROUP_CONCAT(DISTINCT STR(?allCat); SEPARATOR=";") AS ?categories)
8285
+ WHERE {
8286
+ VALUES ?filterCat { ${values} }
8287
+ ?iri a qcy:CanonicalEntity ;
8288
+ qcy:hasEntityCategory ?filterCat ;
8289
+ qcy:value ?val ;
8290
+ qcy:hasEntityCategory ?allCat .
8291
+ BIND(REPLACE(STR(?iri), "^.*/", "") AS ?id)
8292
+ }
8293
+ GROUP BY ?id` : `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
8294
+ SELECT DISTINCT ?id
8295
+ WHERE {
8296
+ VALUES ?filterCat { ${values} }
8297
+ ?iri a qcy:CanonicalEntity ;
8298
+ qcy:hasEntityCategory ?filterCat .
8299
+ BIND(REPLACE(STR(?iri), "^.*/", "") AS ?id)
8300
+ }`;
8301
+ const data = await this._api.sparql(
8302
+ q,
8303
+ this._projectId,
8304
+ this._graphType
8305
+ );
8306
+ return data.results.bindings.filter((b) => b["id"]).map((b) => {
8307
+ const uuid = b["id"].value;
8308
+ const base = { iri: this._resourceIri(uuid), uuid };
8309
+ if (!includeMetadata)
8310
+ return base;
8311
+ return {
8312
+ ...base,
8313
+ value: b["value"]?.value ?? "",
8314
+ categories: b["categories"]?.value?.split(";").filter(Boolean) ?? []
8315
+ };
8316
+ });
8317
+ }
8318
+ async buildSummaryGraph(format, entityIRI) {
8319
+ const ce = CompactExpand.getInstance();
8320
+ const expandedIRI = entityIRI ? entityIRI.includes("://") ? entityIRI : ce.expandIRI(entityIRI) : void 0;
8321
+ const neighbourFilter = expandedIRI ? ` FILTER(?sourceCat = <${expandedIRI}> || ?targetCat = <${expandedIRI}>)` : "";
8128
8322
  const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
8129
8323
  SELECT
8130
8324
  ?sourceCat
@@ -8139,6 +8333,7 @@ WHERE {
8139
8333
  ?s ?predicate ?o .
8140
8334
  FILTER(isIRI(?s) && isIRI(?o))
8141
8335
  FILTER(?predicate != qcy:relatedEntity)
8336
+ ${neighbourFilter}
8142
8337
  }
8143
8338
  GROUP BY ?sourceCat ?predicate ?targetCat
8144
8339
  ORDER BY DESC(?weight)`;
@@ -8168,7 +8363,6 @@ ORDER BY DESC(?weight)`;
8168
8363
  };
8169
8364
  }
8170
8365
  if (format === "md") {
8171
- const ce = CompactExpand.getInstance();
8172
8366
  const rows = bindings.map((b) => ({
8173
8367
  src: ce.compactIRI(b["sourceCat"].value),
8174
8368
  pred: ce.compactIRI(b["predicate"].value),
@@ -8413,411 +8607,6 @@ SELECT * WHERE {
8413
8607
  }
8414
8608
  };
8415
8609
 
8416
- // libs/js/cue-sdk/src/lib/documents.ts
8417
- var CueProjectDocuments = class {
8418
- constructor(_api, _projectId, language, rdfBase = RESOURCE_BASE, _queryCache, _graphType) {
8419
- this._api = _api;
8420
- this._projectId = _projectId;
8421
- this._queryCache = _queryCache;
8422
- this._graphType = _graphType;
8423
- this.baseURL = `${rdfBase}${_projectId}/`;
8424
- this._currentLang = language ?? this._api.language;
8425
- this.documentInfoMap = this._documentInfoMap.asReadonly();
8426
- this.projectDocumentsData = this._projectDocumentsData.asReadonly();
8427
- }
8428
- /** Full RDF base URL for this project, e.g. `https://cue.qaecy.com/r/{pid}/` */
8429
- baseURL;
8430
- /** Tracks the language for which `_documentInfoMap` is currently populated. */
8431
- _currentLang;
8432
- _documentInfoMap = new CueSignal({});
8433
- _projectDocumentsData = new CueSignal({
8434
- duplicateCount: 0,
8435
- documentsBySuffix: {},
8436
- documentsByContentCategory: {}
8437
- });
8438
- /** Lazily populated per-document detail map. */
8439
- documentInfoMap;
8440
- /** Project-level document overview (grouped counts + sizes). */
8441
- projectDocumentsData;
8442
- // ── Lifecycle ──────────────────────────────────────────────────────────────
8443
- /**
8444
- * Resets all document state. Call when the active project changes.
8445
- * Follow with `fetchOverview()` once the triplestore is ready.
8446
- */
8447
- reset() {
8448
- this._documentInfoMap.set({});
8449
- this._projectDocumentsData.set({
8450
- duplicateCount: 0,
8451
- documentsBySuffix: {},
8452
- documentsByContentCategory: {}
8453
- });
8454
- }
8455
- /**
8456
- * Updates the active language and clears the document info map so that
8457
- * language-sensitive fields (subject, summary) are re-fetched on the next
8458
- * `requestDocumentData()` call.
8459
- */
8460
- setLanguage(lang) {
8461
- if (this._currentLang === lang)
8462
- return;
8463
- this._currentLang = lang;
8464
- this._api.setLanguage(lang);
8465
- this._documentInfoMap.set({});
8466
- }
8467
- // ── Public API ─────────────────────────────────────────────────────────────
8468
- /**
8469
- * Fetches the three-part project overview (by suffix, by content category,
8470
- * duplicate count) in parallel and writes them as a single atomic update to
8471
- * `projectDocumentsData`. Safe to call again to refresh.
8472
- */
8473
- async fetchOverview() {
8474
- this._projectDocumentsData.set({
8475
- duplicateCount: 0,
8476
- documentsBySuffix: {},
8477
- documentsByContentCategory: {}
8478
- });
8479
- const qSuffix = this._buildDocumentsBySuffixQuery();
8480
- const qCategory = this._buildDocumentsByContentCategoryQuery();
8481
- const qDuplicates = this._buildDuplicateCountQuery();
8482
- await staleWhileRevalidate(
8483
- qSuffix + qCategory + qDuplicates,
8484
- async () => {
8485
- const [bySuffix, byCategory, duplicateCount] = await Promise.all([
8486
- this._runDocumentsBySuffixQuery(qSuffix),
8487
- this._runDocumentsByContentCategoryQuery(qCategory),
8488
- this._runDuplicateCountQuery(qDuplicates)
8489
- ]);
8490
- return { duplicateCount, documentsBySuffix: bySuffix, documentsByContentCategory: byCategory };
8491
- },
8492
- (overview) => this._projectDocumentsData.set(overview),
8493
- this._queryCache
8494
- );
8495
- }
8496
- /**
8497
- * Lazily batch-fetches core metadata for the given document UUIDs.
8498
- * Already-cached UUIDs are skipped. Data is merged into `documentInfoMap`
8499
- * once the SPARQL response arrives.
8500
- */
8501
- requestDocumentData(uuids) {
8502
- const newUUIDs = uuids.filter((id) => this._documentInfoMap.get()[id] === void 0);
8503
- if (newUUIDs.length === 0)
8504
- return;
8505
- const values = newUUIDs.map((id) => `r:${id}`).join(" ");
8506
- const lang = this._api.language;
8507
- const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
8508
- PREFIX r: <${this.baseURL}>
8509
- SELECT ?id ?contentIRI ?suffix ?size ?subject ?summary
8510
- (SAMPLE(?fp) AS ?path)
8511
- (GROUP_CONCAT(DISTINCT ?tag; SEPARATOR=";") AS ?tags)
8512
- (GROUP_CONCAT(DISTINCT STR(?cat); SEPARATOR=";") AS ?categories)
8513
- WHERE {
8514
- VALUES ?contentIRI { ${values} }
8515
- ?contentIRI qcy:sizeBytes ?size ;
8516
- qcy:hasFileLocation ?loc .
8517
- ?loc qcy:filePath ?fp ;
8518
- qcy:suffix ?suffix .
8519
- OPTIONAL { ?contentIRI qcy:hasContentCategory ?cat }
8520
- OPTIONAL { ?contentIRI qcy:tag ?tag }
8521
- OPTIONAL { ?loc qcy:remoteProviderId ?pid }
8522
-
8523
- OPTIONAL { ?contentIRI qcy:subject ?lang_subj FILTER(LANG(?lang_subj) = "${lang}") }
8524
- OPTIONAL { ?contentIRI qcy:subject ?no_lang_subj }
8525
- BIND(COALESCE(?lang_subj, ?no_lang_subj) AS ?subject)
8526
-
8527
- OPTIONAL { ?contentIRI qcy:textSummary ?lang_summary FILTER(LANG(?lang_summary) = "${lang}") }
8528
- OPTIONAL { ?contentIRI qcy:textSummary ?no_lang_summary }
8529
- BIND(COALESCE(?lang_summary, ?no_lang_summary) AS ?summary)
8530
-
8531
- BIND(REPLACE(STR(?contentIRI), "^.*/([^/]*)$", "$1") AS ?id)
8532
- }
8533
- GROUP BY ?id ?contentIRI ?suffix ?size ?subject ?summary`;
8534
- this._api.sparql(q, this._projectId).then((data) => {
8535
- const result = data;
8536
- const updates = { ...this._documentInfoMap.get() };
8537
- result.results.bindings.forEach((b) => {
8538
- if (!b["id"] || !b["contentIRI"])
8539
- return;
8540
- const id = b["id"].value;
8541
- updates[id] = {
8542
- id,
8543
- contentIRI: b["contentIRI"].value,
8544
- path: b["path"]?.value ?? "",
8545
- suffix: b["suffix"]?.value ?? "",
8546
- size: b["size"] ? parseInt(b["size"].value, 10) : 0,
8547
- tags: b["tags"]?.value?.split(";").filter(Boolean) ?? [],
8548
- categories: b["categories"]?.value?.split(";").filter(Boolean) ?? [],
8549
- subject: b["subject"]?.value,
8550
- summary: b["summary"]?.value,
8551
- providerId: b["pid"]?.value
8552
- };
8553
- });
8554
- this._documentInfoMap.set(updates);
8555
- }).catch(
8556
- (err) => console.error("[CueProjectDocuments] requestDocumentData failed:", err)
8557
- );
8558
- }
8559
- /**
8560
- * Returns a single arbitrary file path from the project's triplestore.
8561
- * Useful for pre-filling path-based query inputs with a realistic example.
8562
- */
8563
- async randomFilePath() {
8564
- const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
8565
- SELECT ?path
8566
- WHERE {
8567
- ?fl a qcy:FileLocation ;
8568
- qcy:filePath ?path .
8569
- }
8570
- LIMIT 1`;
8571
- const data = await this._api.sparql(q, this._projectId, this._graphType);
8572
- return data.results.bindings[0]?.["path"]?.value ?? null;
8573
- }
8574
- // ── Private helpers ────────────────────────────────────────────────────────
8575
- async _fetchDocumentsBySuffix() {
8576
- return this._runDocumentsBySuffixQuery(this._buildDocumentsBySuffixQuery());
8577
- }
8578
- _buildDocumentsBySuffixQuery() {
8579
- return `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
8580
- SELECT ?ext (SUM(?size) AS ?totalSize) (COUNT(*) AS ?docCount)
8581
- WHERE {
8582
- {
8583
- SELECT DISTINCT ?fc ?ext ?size
8584
- WHERE {
8585
- ?fc a qcy:FileContent ;
8586
- qcy:sizeBytes ?size ;
8587
- qcy:hasFileLocation/qcy:suffix ?ext .
8588
- FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?fc }
8589
- }
8590
- }
8591
- }
8592
- GROUP BY ?ext
8593
- ORDER BY DESC(?docCount)`;
8594
- }
8595
- async _runDocumentsBySuffixQuery(q) {
8596
- const data = await this._api.sparql(q, this._projectId, this._graphType);
8597
- const result = {};
8598
- data.results.bindings.forEach((b) => {
8599
- if (!b["ext"])
8600
- return;
8601
- result[b["ext"].value] = {
8602
- size: b["totalSize"] ? parseInt(b["totalSize"].value, 10) : 0,
8603
- count: b["docCount"] ? parseInt(b["docCount"].value, 10) : 0
8604
- };
8605
- });
8606
- return result;
8607
- }
8608
- async _fetchDocumentsByContentCategory() {
8609
- return this._runDocumentsByContentCategoryQuery(this._buildDocumentsByContentCategoryQuery());
8610
- }
8611
- _buildDocumentsByContentCategoryQuery() {
8612
- return `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
8613
- SELECT ?cat (SUM(?size) AS ?totalSize) (COUNT(*) AS ?docCount)
8614
- WHERE {
8615
- {
8616
- SELECT DISTINCT ?fc ?cat ?size
8617
- WHERE {
8618
- ?fc a qcy:FileContent ;
8619
- qcy:hasContentCategory ?cat ;
8620
- qcy:sizeBytes ?size .
8621
- FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?fc }
8622
- }
8623
- }
8624
- }
8625
- GROUP BY ?cat
8626
- ORDER BY DESC(?docCount)`;
8627
- }
8628
- async _runDocumentsByContentCategoryQuery(q) {
8629
- const data = await this._api.sparql(q, this._projectId, this._graphType);
8630
- const result = {};
8631
- data.results.bindings.forEach((b) => {
8632
- if (!b["cat"])
8633
- return;
8634
- result[b["cat"].value] = {
8635
- size: b["totalSize"] ? parseInt(b["totalSize"].value, 10) : 0,
8636
- count: b["docCount"] ? parseInt(b["docCount"].value, 10) : 0
8637
- };
8638
- });
8639
- return result;
8640
- }
8641
- async _fetchDuplicateCount() {
8642
- return this._runDuplicateCountQuery(this._buildDuplicateCountQuery());
8643
- }
8644
- _buildDuplicateCountQuery() {
8645
- return `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
8646
- SELECT (COUNT(*) AS ?count)
8647
- WHERE {
8648
- SELECT ?fc
8649
- WHERE {
8650
- ?fc a qcy:FileContent ;
8651
- qcy:hasFileLocation ?fl .
8652
- FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?fc }
8653
- }
8654
- GROUP BY ?fc
8655
- HAVING (COUNT(?fl) > 1)
8656
- }`;
8657
- }
8658
- async _runDuplicateCountQuery(q) {
8659
- const data = await this._api.sparql(q, this._projectId, this._graphType);
8660
- const first = data.results.bindings[0];
8661
- return first?.["count"] ? parseInt(first["count"].value, 10) : 0;
8662
- }
8663
- };
8664
-
8665
- // libs/js/cue-sdk/src/lib/project-view.ts
8666
- var CueProjectView = class {
8667
- constructor(_api, _projectId, { language, queryCache, rdfBase = RESOURCE_BASE, graphType }) {
8668
- this._api = _api;
8669
- this._projectId = _projectId;
8670
- this.schema = new CueProjectSchema(_api, _projectId, language, queryCache, graphType);
8671
- this.entities = new CueProjectEntities(_api, _projectId, rdfBase, queryCache, graphType);
8672
- this.documents = new CueProjectDocuments(_api, _projectId, language, rdfBase, queryCache, graphType);
8673
- this.availableContentCategories = this.schema.availableContentCategories;
8674
- this.availableEntityCategories = this.schema.availableEntityCategories;
8675
- this.availableEntityRelationships = this.schema.availableEntityRelationships;
8676
- this.schemaReady = this.schema.ready;
8677
- this.entityInfoMap = this.entities.entityInfoMap;
8678
- this.entityGraph = this.entities.entityGraph;
8679
- this.documentInfoMap = this.documents.documentInfoMap;
8680
- this.projectDocumentsData = this.documents.projectDocumentsData;
8681
- this.searchResults = this._searchResults.asReadonly();
8682
- this.documents.fetchOverview().catch((err) => console.error("[CueProjectView] fetchOverview failed:", err));
8683
- }
8684
- /** Direct access to the schema data class (available categories / relationships). */
8685
- schema;
8686
- /** Direct access to the entity data class. */
8687
- entities;
8688
- /** Direct access to the document data class. */
8689
- documents;
8690
- // ── Proxied signals ────────────────────────────────────────────────────────
8691
- /** Available content category definitions for this project. Auto-fetched on init. */
8692
- availableContentCategories;
8693
- /** Available entity category definitions for this project. Auto-fetched on init. */
8694
- availableEntityCategories;
8695
- /** Available entity relationship types. Auto-fetched on init. */
8696
- availableEntityRelationships;
8697
- /**
8698
- * Resolves when the initial schema load has completed. Await before reading
8699
- * schema signal values imperatively.
8700
- */
8701
- schemaReady;
8702
- /** Merged per-entity detail map. Populated lazily via `requestEntityData()` etc. */
8703
- entityInfoMap;
8704
- /** Project-level entity co-occurrence graph. Fetched once on init. */
8705
- entityGraph;
8706
- /** Per-document info map. Populated lazily via `requestDocumentData()`. */
8707
- documentInfoMap;
8708
- /** Project document overview (counts by suffix and category). Fetched on init. */
8709
- projectDocumentsData;
8710
- // ── Search state ───────────────────────────────────────────────────────────
8711
- _searchResults = new CueSignal(void 0);
8712
- /** The result of the most recent `search()` call. `undefined` before first search. */
8713
- searchResults;
8714
- _destroyed = false;
8715
- // ── Entity methods ─────────────────────────────────────────────────────────
8716
- /**
8717
- * Lazily batch-fetch core data (label + categories) for the given entity UUIDs.
8718
- * Already-fetched UUIDs are skipped. Populates `entityInfoMap`.
8719
- */
8720
- requestEntityData(uuids, includeMentionCount = false) {
8721
- if (this._destroyed)
8722
- return;
8723
- this.entities.requestEntityData(uuids, includeMentionCount);
8724
- }
8725
- /**
8726
- * Lazily fetch OSM location data for the given entity UUIDs.
8727
- * Already-fetched UUIDs are skipped. Populates `entityInfoMap` geometry fields.
8728
- */
8729
- async requestEntityLocations(uuids) {
8730
- if (this._destroyed)
8731
- return;
8732
- return this.entities.requestEntityLocations(uuids);
8733
- }
8734
- /**
8735
- * Fetch incoming and outgoing relationships for a single entity IRI.
8736
- * Result is stored in `entityInfoMap[uuid].relationshipData`.
8737
- */
8738
- async fetchEntityRelationships(iri) {
8739
- if (this._destroyed)
8740
- throw new Error("CueProjectView is destroyed");
8741
- return this.entities.fetchEntityRelationships(iri);
8742
- }
8743
- /**
8744
- * Fetch UUIDs of documents that reference the given entity IRI.
8745
- * Result is stored in `entityInfoMap[uuid].documentRefs`.
8746
- */
8747
- async fetchEntityDocuments(iri) {
8748
- if (this._destroyed)
8749
- throw new Error("CueProjectView is destroyed");
8750
- return this.entities.fetchEntityDocuments(iri);
8751
- }
8752
- /** Constructs the full RDF IRI for an entity UUID. */
8753
- entityIri(uuid) {
8754
- return this.entities.entityIri(uuid);
8755
- }
8756
- // ── Document methods ───────────────────────────────────────────────────────
8757
- /**
8758
- * Lazily batch-fetch document info for the given UUIDs.
8759
- * Already-fetched UUIDs are skipped. Populates `documentInfoMap`.
8760
- */
8761
- requestDocumentData(uuids) {
8762
- if (this._destroyed)
8763
- return;
8764
- this.documents.requestDocumentData(uuids);
8765
- }
8766
- // ── Search ─────────────────────────────────────────────────────────────────
8767
- /**
8768
- * Run a natural-language search against the project.
8769
- * The result is stored in `searchResults` and replaces any previous result.
8770
- */
8771
- async search(term, options) {
8772
- if (this._destroyed)
8773
- return;
8774
- const result = await this._api.search({
8775
- term,
8776
- projectId: this._projectId,
8777
- categories: options?.categories
8778
- });
8779
- if (!this._destroyed) {
8780
- this._searchResults.set(result);
8781
- }
8782
- }
8783
- // ── Lifecycle ──────────────────────────────────────────────────────────────
8784
- /**
8785
- * Switch the active language for schema labels and document text fields.
8786
- * Schema responses are cached per language (instant if previously loaded).
8787
- * The document info map is cleared and lazily re-populated on next access.
8788
- */
8789
- setLanguage(lang) {
8790
- if (this._destroyed)
8791
- return;
8792
- this.schema.setLanguage(lang);
8793
- this.documents.setLanguage(lang);
8794
- }
8795
- /**
8796
- * Reset all entity and document state and re-fetch the project overview.
8797
- * Prefer creating a fresh `CueProjectView` when switching projects.
8798
- * Use `reset()` only when the same project's data needs to be invalidated.
8799
- */
8800
- reset() {
8801
- if (this._destroyed)
8802
- return;
8803
- this.entities.reset();
8804
- this.documents.reset();
8805
- this._searchResults.set(void 0);
8806
- this.documents.fetchOverview().catch((err) => console.error("[CueProjectView] fetchOverview failed after reset:", err));
8807
- }
8808
- /**
8809
- * Tear down this view instance. Clears all reactive state and blocks further
8810
- * updates. Call from the Angular adapter's `ngOnDestroy` or equivalent.
8811
- */
8812
- destroy() {
8813
- this._destroyed = true;
8814
- this._searchResults.set(void 0);
8815
- }
8816
- };
8817
-
8818
- // libs/js/file-metadata-helpers/src/lib/js-file-metadata-helpers.ts
8819
- init_src();
8820
-
8821
8610
  // libs/js/models/src/lib/file-extensions.ts
8822
8611
  var fileExtensionsInfo = {
8823
8612
  [".aac" /* AAC */]: {
@@ -9680,41 +9469,706 @@ var fileExtensionsInfo = {
9680
9469
  }
9681
9470
  };
9682
9471
 
9683
- // libs/js/models/src/lib/file-mime-categories.ts
9684
- var fileTypeToMimeCategory = {
9685
- ["audio" /* AUDIO */]: "AudioFileContent" /* AUDIO */,
9686
- ["video" /* VIDEO */]: "VideoFileContent" /* VIDEO */,
9687
- ["image" /* IMAGE */]: "ImageFileContent" /* IMAGE */,
9688
- ["text" /* TEXT */]: "TextFileContent" /* TEXT */,
9689
- ["markup" /* MARKUP */]: "MarkupFileContent" /* MARKUP */,
9690
- ["script" /* SCRIPT */]: "ScriptFileContent" /* SCRIPT */,
9691
- ["data" /* DATA */]: "DataFileContent" /* DATA */,
9692
- ["archive" /* ARCHIVE */]: "ArchiveFileContent" /* ARCHIVE */,
9693
- ["installer" /* INSTALLER */]: "InstallerFileContent" /* INSTALLER */,
9694
- ["binary" /* BINARY */]: "BinaryFileContent" /* BINARY */,
9695
- ["backup" /* BACKUP */]: "BackupFileContent" /* BACKUP */,
9696
- ["automation" /* AUTOMATION */]: "AutomationFileContent" /* AUTOMATION */,
9697
- ["presentation" /* PRESENTATION */]: "PresentationFileContent" /* PRESENTATION */,
9698
- ["spreadsheet" /* SPREADSHEET */]: "SpreadsheetFileContent" /* SPREADSHEET */,
9699
- ["font" /* FONT */]: "FontFileContent" /* FONT */,
9700
- ["geospatial" /* GEOSPATIAL */]: "GeospatialFileContent" /* GEOSPATIAL */,
9701
- ["3d" /* THREE_D */]: "ThreeDFileContent" /* THREE_D */,
9702
- ["cad" /* CAD */]: "CADFileContent" /* CAD */,
9703
- ["bim" /* BIM */]: "BIMFileContent" /* BIM */,
9704
- ["planning" /* PLANNING */]: "PlanningFileContent" /* PLANNING */,
9705
- ["email" /* EMAIL */]: "EmailFileContent" /* EMAIL */,
9706
- ["multimedia" /* MULTIMEDIA */]: "MultimediaFileContent" /* MULTIMEDIA */,
9707
- ["unknown" /* UNKNOWN */]: "UnknownFileContent" /* UNKNOWN */
9472
+ // libs/js/models/src/lib/file-mime-categories.ts
9473
+ var fileTypeToMimeCategory = {
9474
+ ["audio" /* AUDIO */]: "AudioFileContent" /* AUDIO */,
9475
+ ["video" /* VIDEO */]: "VideoFileContent" /* VIDEO */,
9476
+ ["image" /* IMAGE */]: "ImageFileContent" /* IMAGE */,
9477
+ ["text" /* TEXT */]: "TextFileContent" /* TEXT */,
9478
+ ["markup" /* MARKUP */]: "MarkupFileContent" /* MARKUP */,
9479
+ ["script" /* SCRIPT */]: "ScriptFileContent" /* SCRIPT */,
9480
+ ["data" /* DATA */]: "DataFileContent" /* DATA */,
9481
+ ["archive" /* ARCHIVE */]: "ArchiveFileContent" /* ARCHIVE */,
9482
+ ["installer" /* INSTALLER */]: "InstallerFileContent" /* INSTALLER */,
9483
+ ["binary" /* BINARY */]: "BinaryFileContent" /* BINARY */,
9484
+ ["backup" /* BACKUP */]: "BackupFileContent" /* BACKUP */,
9485
+ ["automation" /* AUTOMATION */]: "AutomationFileContent" /* AUTOMATION */,
9486
+ ["presentation" /* PRESENTATION */]: "PresentationFileContent" /* PRESENTATION */,
9487
+ ["spreadsheet" /* SPREADSHEET */]: "SpreadsheetFileContent" /* SPREADSHEET */,
9488
+ ["font" /* FONT */]: "FontFileContent" /* FONT */,
9489
+ ["geospatial" /* GEOSPATIAL */]: "GeospatialFileContent" /* GEOSPATIAL */,
9490
+ ["3d" /* THREE_D */]: "ThreeDFileContent" /* THREE_D */,
9491
+ ["cad" /* CAD */]: "CADFileContent" /* CAD */,
9492
+ ["bim" /* BIM */]: "BIMFileContent" /* BIM */,
9493
+ ["planning" /* PLANNING */]: "PlanningFileContent" /* PLANNING */,
9494
+ ["email" /* EMAIL */]: "EmailFileContent" /* EMAIL */,
9495
+ ["multimedia" /* MULTIMEDIA */]: "MultimediaFileContent" /* MULTIMEDIA */,
9496
+ ["unknown" /* UNKNOWN */]: "UnknownFileContent" /* UNKNOWN */
9497
+ };
9498
+
9499
+ // libs/js/models/src/lib/project.ts
9500
+ var import_uuid3 = require("uuid");
9501
+
9502
+ // libs/js/models/src/lib/search-log-entry.ts
9503
+ var import_uuid4 = require("uuid");
9504
+
9505
+ // libs/js/models/src/lib/search-response.ts
9506
+ var import_uuid5 = require("uuid");
9507
+
9508
+ // libs/js/cue-sdk/src/lib/documents.ts
9509
+ var CueProjectDocuments = class {
9510
+ constructor(_api, _projectId, language, rdfBase = RESOURCE_BASE, _queryCache, _graphType, _verbose = false) {
9511
+ this._api = _api;
9512
+ this._projectId = _projectId;
9513
+ this._queryCache = _queryCache;
9514
+ this._graphType = _graphType;
9515
+ this._verbose = _verbose;
9516
+ this.baseURL = `${rdfBase}${_projectId}/`;
9517
+ this._currentLang = language ?? this._api.language;
9518
+ this.documentInfoMap = this._documentInfoMap.asReadonly();
9519
+ this.projectDocumentsData = this._projectDocumentsData.asReadonly();
9520
+ }
9521
+ /** Full RDF base URL for this project, e.g. `https://cue.qaecy.com/r/{pid}/` */
9522
+ baseURL;
9523
+ /** Tracks the language for which `_documentInfoMap` is currently populated. */
9524
+ _currentLang;
9525
+ _documentInfoMap = new CueSignal({});
9526
+ /** Cumulative unique document UUIDs ever passed to request methods (survives cache hits). */
9527
+ _seenIds = /* @__PURE__ */ new Set();
9528
+ _projectDocumentsData = new CueSignal({
9529
+ duplicateCount: 0,
9530
+ documentsBySuffix: {},
9531
+ documentsByContentCategory: {}
9532
+ });
9533
+ /** Lazily populated per-document detail map. */
9534
+ documentInfoMap;
9535
+ /** Project-level document overview (grouped counts + sizes). */
9536
+ projectDocumentsData;
9537
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
9538
+ /**
9539
+ * Resets all document state. Call when the active project changes.
9540
+ * Follow with `fetchOverview()` once the triplestore is ready.
9541
+ */
9542
+ reset() {
9543
+ this._documentInfoMap.set({});
9544
+ this._seenIds.clear();
9545
+ this._projectDocumentsData.set({
9546
+ duplicateCount: 0,
9547
+ documentsBySuffix: {},
9548
+ documentsByContentCategory: {}
9549
+ });
9550
+ }
9551
+ /**
9552
+ * Updates the active language and clears the document info map so that
9553
+ * language-sensitive fields (subject, summary) are re-fetched on the next
9554
+ * `requestDocumentData()` call.
9555
+ */
9556
+ setLanguage(lang) {
9557
+ if (this._currentLang === lang)
9558
+ return;
9559
+ this._currentLang = lang;
9560
+ this._api.setLanguage(lang);
9561
+ this._documentInfoMap.set({});
9562
+ }
9563
+ // ── Public API ─────────────────────────────────────────────────────────────
9564
+ /**
9565
+ * Fetches the three-part project overview (by suffix, by content category,
9566
+ * duplicate count) in parallel and writes them as a single atomic update to
9567
+ * `projectDocumentsData`. Safe to call again to refresh.
9568
+ */
9569
+ async fetchOverview() {
9570
+ this._projectDocumentsData.set({
9571
+ duplicateCount: 0,
9572
+ documentsBySuffix: {},
9573
+ documentsByContentCategory: {}
9574
+ });
9575
+ const qSuffix = this._buildDocumentsBySuffixQuery();
9576
+ const qCategory = this._buildDocumentsByContentCategoryQuery();
9577
+ const qDuplicates = this._buildDuplicateCountQuery();
9578
+ await staleWhileRevalidate(
9579
+ qSuffix + qCategory + qDuplicates,
9580
+ async () => {
9581
+ const [bySuffix, byCategory, duplicateCount] = await Promise.all([
9582
+ this._runDocumentsBySuffixQuery(qSuffix),
9583
+ this._runDocumentsByContentCategoryQuery(qCategory),
9584
+ this._runDuplicateCountQuery(qDuplicates)
9585
+ ]);
9586
+ return { duplicateCount, documentsBySuffix: bySuffix, documentsByContentCategory: byCategory };
9587
+ },
9588
+ (overview) => this._projectDocumentsData.set(overview),
9589
+ this._queryCache
9590
+ );
9591
+ }
9592
+ /**
9593
+ * Lazily batch-fetches core metadata for the given document UUIDs.
9594
+ * Already-cached UUIDs are skipped. Data is merged into `documentInfoMap`
9595
+ * once the SPARQL response arrives.
9596
+ */
9597
+ requestDocumentData(uuids) {
9598
+ for (const id of uuids)
9599
+ this._seenIds.add(id);
9600
+ const newUUIDs = uuids.filter((id) => this._documentInfoMap.get()[id] === void 0);
9601
+ if (newUUIDs.length === 0)
9602
+ return;
9603
+ this._log(`requestDocumentData: ${newUUIDs.length} new / ${uuids.length} requested | cumulative: ${this._seenIds.size} seen`);
9604
+ this._fetchDocumentInfoBatch(newUUIDs).catch(
9605
+ (err) => console.error("[CueProjectDocuments] requestDocumentData failed:", err)
9606
+ );
9607
+ }
9608
+ /**
9609
+ * Promise-based alternative to {@link requestDocumentData} for non-reactive contexts.
9610
+ *
9611
+ * Resolves with the `DocumentInfo` entries for every requested UUID once the
9612
+ * SPARQL response arrives. UUIDs already present in the cache are returned
9613
+ * immediately without a network request. The result is also written into
9614
+ * `documentInfoMap` so reactive consumers stay in sync.
9615
+ *
9616
+ * UUIDs not found in the triplestore are omitted from the returned map.
9617
+ *
9618
+ * @example
9619
+ * ```ts
9620
+ * const docs = await cueProjectDocs.fetchDocumentData(['uuid1', 'uuid2']);
9621
+ * console.log(docs['uuid1'].subject);
9622
+ * ```
9623
+ */
9624
+ async fetchDocumentData(uuids) {
9625
+ const current = this._documentInfoMap.get();
9626
+ const newUUIDs = uuids.filter((id) => current[id] === void 0);
9627
+ for (const id of uuids)
9628
+ this._seenIds.add(id);
9629
+ if (newUUIDs.length > 0) {
9630
+ await this._fetchDocumentInfoBatch(newUUIDs);
9631
+ }
9632
+ const updated = this._documentInfoMap.get();
9633
+ return Object.fromEntries(
9634
+ uuids.filter((id) => updated[id] !== void 0).map((id) => [id, updated[id]])
9635
+ );
9636
+ }
9637
+ /**
9638
+ * Fetches a lightweight document metadata shape (id/path/suffix/size) for
9639
+ * the given UUIDs and merges the results into `documentInfoMap`.
9640
+ *
9641
+ * This is useful for list/table contexts that do not need language-tagged
9642
+ * fields (`subject`, `summary`) or category/tag enrichment.
9643
+ *
9644
+ * UUIDs already present in `documentInfoMap` are skipped.
9645
+ */
9646
+ async fetchDocumentDataSimple(uuids) {
9647
+ const current = this._documentInfoMap.get();
9648
+ const newUUIDs = uuids.filter((id) => current[id] === void 0);
9649
+ for (const id of uuids)
9650
+ this._seenIds.add(id);
9651
+ if (newUUIDs.length > 0) {
9652
+ await this._fetchSimpleDocumentInfoBatch(newUUIDs);
9653
+ }
9654
+ const updated = this._documentInfoMap.get();
9655
+ return Object.fromEntries(
9656
+ uuids.filter((id) => updated[id] !== void 0).map((id) => [id, updated[id]])
9657
+ );
9658
+ }
9659
+ /**
9660
+ * Returns the alternative representations of the given document UUID.
9661
+ *
9662
+ * Alternative representations are derived artefacts stored under
9663
+ * `qcy:alternativeRepresentation` in the triplestore — for example a
9664
+ * `.fragments` BIM tile derived from an `.ifc` source file.
9665
+ *
9666
+ * The returned `DocumentInfo` entries are also merged into
9667
+ * `documentInfoMap` so reactive consumers stay in sync.
9668
+ *
9669
+ * @example
9670
+ * ```ts
9671
+ * const alts = await docs.fetchAlternativeRepresentations('abc-123');
9672
+ * // alts[0].suffix => '.fragments'
9673
+ * ```
9674
+ */
9675
+ async fetchAlternativeRepresentations(uuid) {
9676
+ const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9677
+ PREFIX r: <${this.baseURL}>
9678
+ SELECT ?altId ?contentIRI ?suffix ?rrp
9679
+ WHERE {
9680
+ r:${uuid} qcy:alternativeRepresentation ?contentIRI .
9681
+ BIND(REPLACE(STR(?contentIRI), "^.*/([^/]*)$", "$1") AS ?altId)
9682
+ ?contentIRI qcy:hasFileLocation ?loc .
9683
+ ?loc qcy:suffix ?suffix .
9684
+ OPTIONAL { ?loc qcy:remoteRelativePath ?rrp }
9685
+ }`;
9686
+ const data = await this._api.sparql(q, this._projectId, this._graphType);
9687
+ const updates = { ...this._documentInfoMap.get() };
9688
+ const alts = [];
9689
+ data.results.bindings.forEach((b) => {
9690
+ if (!b["altId"] || !b["contentIRI"])
9691
+ return;
9692
+ const id = b["altId"].value;
9693
+ const info = {
9694
+ id,
9695
+ contentIRI: b["contentIRI"].value,
9696
+ path: "",
9697
+ suffix: b["suffix"]?.value ?? "",
9698
+ size: 0,
9699
+ tags: [],
9700
+ categories: [],
9701
+ remoteRelativePath: b["rrp"]?.value
9702
+ };
9703
+ updates[id] = info;
9704
+ alts.push(info);
9705
+ });
9706
+ this._documentInfoMap.set(updates);
9707
+ return alts;
9708
+ }
9709
+ /**
9710
+ * Returns a single arbitrary file path from the project's triplestore.
9711
+ * Useful for pre-filling path-based query inputs with a realistic example.
9712
+ */
9713
+ async randomFilePath() {
9714
+ const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9715
+ SELECT ?path
9716
+ WHERE {
9717
+ ?fl a qcy:FileLocation ;
9718
+ qcy:filePath ?path .
9719
+ }
9720
+ LIMIT 1`;
9721
+ const data = await this._api.sparql(q, this._projectId, this._graphType);
9722
+ return data.results.bindings[0]?.["path"]?.value ?? null;
9723
+ }
9724
+ async documentsBySuffix(suffixes, includeMetadata = false) {
9725
+ if (suffixes.length === 0)
9726
+ return [];
9727
+ const normalised = suffixes.map(
9728
+ (s) => (s.startsWith(".") ? s : `.${s}`).toLowerCase()
9729
+ );
9730
+ const values = normalised.map((s) => `"${s}"`).join(" ");
9731
+ const q = includeMetadata ? `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9732
+ SELECT ?id (SAMPLE(?fp) AS ?path) ?suffix (MAX(?sz) AS ?size)
9733
+ WHERE {
9734
+ VALUES ?suffix { ${values} }
9735
+ ?iri a qcy:FileContent ;
9736
+ qcy:sizeBytes ?sz ;
9737
+ qcy:hasFileLocation ?loc .
9738
+ ?loc qcy:suffix ?suffix ;
9739
+ qcy:filePath ?fp .
9740
+ FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?iri }
9741
+ BIND(REPLACE(STR(?iri), "^.*/", "") AS ?id)
9742
+ }
9743
+ GROUP BY ?id ?suffix` : `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9744
+ SELECT DISTINCT ?id
9745
+ WHERE {
9746
+ VALUES ?suffix { ${values} }
9747
+ ?iri a qcy:FileContent ;
9748
+ qcy:hasFileLocation/qcy:suffix ?suffix .
9749
+ FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?iri }
9750
+ BIND(REPLACE(STR(?iri), "^.*/", "") AS ?id)
9751
+ }`;
9752
+ const data = await this._api.sparql(q, this._projectId, this._graphType);
9753
+ return data.results.bindings.filter((b) => b["id"]).map((b) => {
9754
+ const uuid = b["id"].value;
9755
+ const base = { iri: this._resourceIri(uuid), uuid };
9756
+ if (!includeMetadata)
9757
+ return base;
9758
+ return {
9759
+ ...base,
9760
+ path: b["path"]?.value ?? "",
9761
+ suffix: b["suffix"]?.value ?? "",
9762
+ size: b["size"] ? parseInt(b["size"].value, 10) : 0
9763
+ };
9764
+ });
9765
+ }
9766
+ documentsByFileType(fileTypes, includeMetadata = false) {
9767
+ const typeSet = new Set(fileTypes);
9768
+ const suffixes = Object.values(fileExtensionsInfo).filter((info) => typeSet.has(info.type)).map((info) => info.suffix);
9769
+ return this.documentsBySuffix(suffixes, includeMetadata);
9770
+ }
9771
+ async documentsByContentCategory(categoryIRIs, includeMetadata = false) {
9772
+ if (categoryIRIs.length === 0)
9773
+ return [];
9774
+ const values = categoryIRIs.map((iri) => `<${iri}>`).join(" ");
9775
+ const q = includeMetadata ? `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9776
+ SELECT ?id (SAMPLE(?fp) AS ?path) ?suffix (MAX(?sz) AS ?size)
9777
+ WHERE {
9778
+ VALUES ?cat { ${values} }
9779
+ ?iri a qcy:FileContent ;
9780
+ qcy:hasContentCategory ?cat ;
9781
+ qcy:sizeBytes ?sz ;
9782
+ qcy:hasFileLocation ?loc .
9783
+ ?loc qcy:suffix ?suffix ;
9784
+ qcy:filePath ?fp .
9785
+ FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?iri }
9786
+ BIND(REPLACE(STR(?iri), "^.*/", "") AS ?id)
9787
+ }
9788
+ GROUP BY ?id ?suffix` : `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9789
+ SELECT DISTINCT ?id
9790
+ WHERE {
9791
+ VALUES ?cat { ${values} }
9792
+ ?iri a qcy:FileContent ;
9793
+ qcy:hasContentCategory ?cat .
9794
+ FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?iri }
9795
+ BIND(REPLACE(STR(?iri), "^.*/", "") AS ?id)
9796
+ }`;
9797
+ const data = await this._api.sparql(q, this._projectId, this._graphType);
9798
+ return data.results.bindings.filter((b) => b["id"]).map((b) => {
9799
+ const uuid = b["id"].value;
9800
+ const base = { iri: this._resourceIri(uuid), uuid };
9801
+ if (!includeMetadata)
9802
+ return base;
9803
+ return {
9804
+ ...base,
9805
+ path: b["path"]?.value ?? "",
9806
+ suffix: b["suffix"]?.value ?? "",
9807
+ size: b["size"] ? parseInt(b["size"].value, 10) : 0
9808
+ };
9809
+ });
9810
+ }
9811
+ documentsByMime(mimeTypes, includeMetadata = false) {
9812
+ const mimeSet = new Set(mimeTypes);
9813
+ const suffixes = Object.values(fileExtensionsInfo).filter((info) => mimeSet.has(info.mime)).map((info) => info.suffix);
9814
+ return this.documentsBySuffix(suffixes, includeMetadata);
9815
+ }
9816
+ // ── Private helpers ────────────────────────────────────────────────────────
9817
+ /** Builds a full resource IRI from a UUID without a SPARQL round-trip. */
9818
+ _resourceIri(uuid) {
9819
+ return `${this.baseURL}${uuid}`;
9820
+ }
9821
+ _log(message) {
9822
+ if (this._verbose) {
9823
+ console.debug(`[CueProjectDocuments pid=${this._projectId}] ${message}`);
9824
+ }
9825
+ }
9826
+ /** Executes the document-info SPARQL query for the given UUIDs, merges results
9827
+ * into `documentInfoMap`, and returns the newly fetched entries. */
9828
+ async _fetchDocumentInfoBatch(uuids) {
9829
+ const values = uuids.map((id) => `r:${id}`).join(" ");
9830
+ const lang = this._api.language;
9831
+ const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9832
+ PREFIX r: <${this.baseURL}>
9833
+ SELECT ?id ?suffix ?size ?subject ?summary
9834
+ (SAMPLE(?fp) AS ?path)
9835
+ (GROUP_CONCAT(DISTINCT ?tag; SEPARATOR=";") AS ?tags)
9836
+ (GROUP_CONCAT(DISTINCT STR(?cat); SEPARATOR=";") AS ?categories)
9837
+ WHERE {
9838
+ VALUES ?contentIRI { ${values} }
9839
+ ?contentIRI qcy:sizeBytes ?size ;
9840
+ qcy:hasFileLocation ?loc .
9841
+ ?loc qcy:filePath ?fp ;
9842
+ qcy:suffix ?suffix .
9843
+ OPTIONAL { ?contentIRI qcy:hasContentCategory ?cat }
9844
+ OPTIONAL { ?contentIRI qcy:tag ?tag }
9845
+ OPTIONAL { ?loc qcy:remoteProviderId ?pid }
9846
+
9847
+ OPTIONAL { ?contentIRI qcy:subject ?lang_subj FILTER(LANG(?lang_subj) = "${lang}") }
9848
+ OPTIONAL { ?contentIRI qcy:subject ?no_lang_subj }
9849
+ BIND(COALESCE(?lang_subj, ?no_lang_subj) AS ?subject)
9850
+
9851
+ OPTIONAL { ?contentIRI qcy:textSummary ?lang_summary FILTER(LANG(?lang_summary) = "${lang}") }
9852
+ OPTIONAL { ?contentIRI qcy:textSummary ?no_lang_summary }
9853
+ BIND(COALESCE(?lang_summary, ?no_lang_summary) AS ?summary)
9854
+
9855
+ BIND(REPLACE(STR(?contentIRI), "^.*/", "") AS ?id)
9856
+ }
9857
+ GROUP BY ?id ?suffix ?size ?subject ?summary`;
9858
+ const result = await this._api.sparql(q, this._projectId);
9859
+ const updates = { ...this._documentInfoMap.get() };
9860
+ const fetched = {};
9861
+ result.results.bindings.forEach((b) => {
9862
+ if (!b["id"])
9863
+ return;
9864
+ const id = b["id"].value;
9865
+ const info = {
9866
+ id,
9867
+ contentIRI: this._resourceIri(id),
9868
+ path: b["path"]?.value ?? "",
9869
+ suffix: b["suffix"]?.value ?? "",
9870
+ size: b["size"] ? parseInt(b["size"].value, 10) : 0,
9871
+ tags: b["tags"]?.value?.split(";").filter(Boolean) ?? [],
9872
+ categories: b["categories"]?.value?.split(";").filter(Boolean) ?? [],
9873
+ subject: b["subject"]?.value,
9874
+ summary: b["summary"]?.value,
9875
+ providerId: b["pid"]?.value
9876
+ };
9877
+ updates[id] = info;
9878
+ fetched[id] = info;
9879
+ });
9880
+ this._documentInfoMap.set(updates);
9881
+ this._log(`fetchDocumentInfoBatch: ${Object.keys(fetched).length} fetched | cumulative: ${Object.keys(updates).length} with metadata / ${this._seenIds.size} seen`);
9882
+ return fetched;
9883
+ }
9884
+ /** Executes a reduced document-info query (id/path/suffix/size only), merges
9885
+ * into `documentInfoMap`, and returns newly fetched entries. */
9886
+ async _fetchSimpleDocumentInfoBatch(uuids) {
9887
+ const values = uuids.map((id) => `r:${id}`).join(" ");
9888
+ const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9889
+ PREFIX r: <${this.baseURL}>
9890
+ SELECT ?id ?suffix ?size (SAMPLE(?fp) AS ?path)
9891
+ WHERE {
9892
+ VALUES ?contentIRI { ${values} }
9893
+ ?contentIRI qcy:sizeBytes ?size ;
9894
+ qcy:hasFileLocation ?loc .
9895
+ ?loc qcy:filePath ?fp ;
9896
+ qcy:suffix ?suffix .
9897
+ BIND(REPLACE(STR(?contentIRI), "^.*/", "") AS ?id)
9898
+ }
9899
+ GROUP BY ?id ?suffix ?size`;
9900
+ const result = await this._api.sparql(q, this._projectId, this._graphType);
9901
+ const updates = { ...this._documentInfoMap.get() };
9902
+ const fetched = {};
9903
+ result.results.bindings.forEach((b) => {
9904
+ if (!b["id"])
9905
+ return;
9906
+ const id = b["id"].value;
9907
+ const existing = updates[id];
9908
+ const info = {
9909
+ id,
9910
+ contentIRI: this._resourceIri(id),
9911
+ path: b["path"]?.value ?? existing?.path ?? id,
9912
+ suffix: b["suffix"]?.value ?? existing?.suffix ?? "",
9913
+ size: b["size"] ? parseInt(b["size"].value, 10) : existing?.size ?? 0,
9914
+ tags: existing?.tags ?? [],
9915
+ categories: existing?.categories ?? [],
9916
+ subject: existing?.subject,
9917
+ summary: existing?.summary,
9918
+ providerId: existing?.providerId
9919
+ };
9920
+ updates[id] = info;
9921
+ fetched[id] = info;
9922
+ });
9923
+ this._documentInfoMap.set(updates);
9924
+ this._log(`fetchSimpleDocumentInfoBatch: ${Object.keys(fetched).length} fetched | cumulative: ${Object.keys(updates).length} with metadata / ${this._seenIds.size} seen`);
9925
+ return fetched;
9926
+ }
9927
+ async _fetchDocumentsBySuffix() {
9928
+ return this._runDocumentsBySuffixQuery(this._buildDocumentsBySuffixQuery());
9929
+ }
9930
+ _buildDocumentsBySuffixQuery() {
9931
+ return `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9932
+ SELECT ?ext (SUM(?size) AS ?totalSize) (COUNT(*) AS ?docCount)
9933
+ WHERE {
9934
+ {
9935
+ SELECT DISTINCT ?fc ?ext ?size
9936
+ WHERE {
9937
+ ?fc a qcy:FileContent ;
9938
+ qcy:sizeBytes ?size ;
9939
+ qcy:hasFileLocation/qcy:suffix ?ext .
9940
+ FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?fc }
9941
+ }
9942
+ }
9943
+ }
9944
+ GROUP BY ?ext
9945
+ ORDER BY DESC(?docCount)`;
9946
+ }
9947
+ async _runDocumentsBySuffixQuery(q) {
9948
+ const data = await this._api.sparql(q, this._projectId, this._graphType);
9949
+ const result = {};
9950
+ data.results.bindings.forEach((b) => {
9951
+ if (!b["ext"])
9952
+ return;
9953
+ result[b["ext"].value] = {
9954
+ size: b["totalSize"] ? parseInt(b["totalSize"].value, 10) : 0,
9955
+ count: b["docCount"] ? parseInt(b["docCount"].value, 10) : 0
9956
+ };
9957
+ });
9958
+ return result;
9959
+ }
9960
+ async _fetchDocumentsByContentCategory() {
9961
+ return this._runDocumentsByContentCategoryQuery(this._buildDocumentsByContentCategoryQuery());
9962
+ }
9963
+ _buildDocumentsByContentCategoryQuery() {
9964
+ return `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9965
+ SELECT ?cat (SUM(?size) AS ?totalSize) (COUNT(*) AS ?docCount)
9966
+ WHERE {
9967
+ {
9968
+ SELECT DISTINCT ?fc ?cat ?size
9969
+ WHERE {
9970
+ ?fc a qcy:FileContent ;
9971
+ qcy:hasContentCategory ?cat ;
9972
+ qcy:sizeBytes ?size .
9973
+ FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?fc }
9974
+ }
9975
+ }
9976
+ }
9977
+ GROUP BY ?cat
9978
+ ORDER BY DESC(?docCount)`;
9979
+ }
9980
+ async _runDocumentsByContentCategoryQuery(q) {
9981
+ const data = await this._api.sparql(q, this._projectId, this._graphType);
9982
+ const result = {};
9983
+ data.results.bindings.forEach((b) => {
9984
+ if (!b["cat"])
9985
+ return;
9986
+ result[b["cat"].value] = {
9987
+ size: b["totalSize"] ? parseInt(b["totalSize"].value, 10) : 0,
9988
+ count: b["docCount"] ? parseInt(b["docCount"].value, 10) : 0
9989
+ };
9990
+ });
9991
+ return result;
9992
+ }
9993
+ async _fetchDuplicateCount() {
9994
+ return this._runDuplicateCountQuery(this._buildDuplicateCountQuery());
9995
+ }
9996
+ _buildDuplicateCountQuery() {
9997
+ return `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
9998
+ SELECT (COUNT(*) AS ?count)
9999
+ WHERE {
10000
+ SELECT ?fc
10001
+ WHERE {
10002
+ ?fc a qcy:FileContent ;
10003
+ qcy:hasFileLocation ?fl .
10004
+ FILTER NOT EXISTS { ?x qcy:alternativeRepresentation ?fc }
10005
+ }
10006
+ GROUP BY ?fc
10007
+ HAVING (COUNT(?fl) > 1)
10008
+ }`;
10009
+ }
10010
+ async _runDuplicateCountQuery(q) {
10011
+ const data = await this._api.sparql(q, this._projectId, this._graphType);
10012
+ const first = data.results.bindings[0];
10013
+ return first?.["count"] ? parseInt(first["count"].value, 10) : 0;
10014
+ }
10015
+ };
10016
+
10017
+ // libs/js/cue-sdk/src/lib/project-view.ts
10018
+ var CueProjectView = class {
10019
+ constructor(_api, _projectId, { language, queryCache, rdfBase = RESOURCE_BASE, graphType, verbose }) {
10020
+ this._api = _api;
10021
+ this._projectId = _projectId;
10022
+ this.schema = new CueProjectSchema(_api, _projectId, language, queryCache, graphType);
10023
+ this.entities = new CueProjectEntities(_api, _projectId, rdfBase, queryCache, graphType, verbose);
10024
+ this.documents = new CueProjectDocuments(_api, _projectId, language, rdfBase, queryCache, graphType, verbose);
10025
+ this.availableContentCategories = this.schema.availableContentCategories;
10026
+ this.availableEntityCategories = this.schema.availableEntityCategories;
10027
+ this.availableEntityRelationships = this.schema.availableEntityRelationships;
10028
+ this.schemaReady = this.schema.ready;
10029
+ this.entityInfoMap = this.entities.entityInfoMap;
10030
+ this.entityGraph = this.entities.entityGraph;
10031
+ this.documentInfoMap = this.documents.documentInfoMap;
10032
+ this.projectDocumentsData = this.documents.projectDocumentsData;
10033
+ this.searchResults = this._searchResults.asReadonly();
10034
+ this.documents.fetchOverview().catch((err) => console.error("[CueProjectView] fetchOverview failed:", err));
10035
+ }
10036
+ /** Direct access to the schema data class (available categories / relationships). */
10037
+ schema;
10038
+ /** Direct access to the entity data class. */
10039
+ entities;
10040
+ /** Direct access to the document data class. */
10041
+ documents;
10042
+ // ── Proxied signals ────────────────────────────────────────────────────────
10043
+ /** Available content category definitions for this project. Auto-fetched on init. */
10044
+ availableContentCategories;
10045
+ /** Available entity category definitions for this project. Auto-fetched on init. */
10046
+ availableEntityCategories;
10047
+ /** Available entity relationship types. Auto-fetched on init. */
10048
+ availableEntityRelationships;
10049
+ /**
10050
+ * Resolves when the initial schema load has completed. Await before reading
10051
+ * schema signal values imperatively.
10052
+ */
10053
+ schemaReady;
10054
+ /** Merged per-entity detail map. Populated lazily via `requestEntityData()` etc. */
10055
+ entityInfoMap;
10056
+ /** Project-level entity co-occurrence graph. Fetched once on init. */
10057
+ entityGraph;
10058
+ /** Per-document info map. Populated lazily via `requestDocumentData()`. */
10059
+ documentInfoMap;
10060
+ /** Project document overview (counts by suffix and category). Fetched on init. */
10061
+ projectDocumentsData;
10062
+ // ── Search state ───────────────────────────────────────────────────────────
10063
+ _searchResults = new CueSignal(void 0);
10064
+ /** The result of the most recent `search()` call. `undefined` before first search. */
10065
+ searchResults;
10066
+ _destroyed = false;
10067
+ // ── Entity methods ─────────────────────────────────────────────────────────
10068
+ /**
10069
+ * Lazily batch-fetch core data (label + categories) for the given entity UUIDs.
10070
+ * Already-fetched UUIDs are skipped. Populates `entityInfoMap`.
10071
+ */
10072
+ requestEntityData(uuids, includeMentionCount = false) {
10073
+ if (this._destroyed)
10074
+ return;
10075
+ this.entities.requestEntityData(uuids, includeMentionCount);
10076
+ }
10077
+ /**
10078
+ * Lazily fetch OSM location data for the given entity UUIDs.
10079
+ * Already-fetched UUIDs are skipped. Populates `entityInfoMap` geometry fields.
10080
+ */
10081
+ async requestEntityLocations(uuids) {
10082
+ if (this._destroyed)
10083
+ return;
10084
+ return this.entities.requestEntityLocations(uuids);
10085
+ }
10086
+ /**
10087
+ * Fetch incoming and outgoing relationships for a single entity IRI.
10088
+ * Result is stored in `entityInfoMap[uuid].relationshipData`.
10089
+ */
10090
+ async fetchEntityRelationships(iri) {
10091
+ if (this._destroyed)
10092
+ throw new Error("CueProjectView is destroyed");
10093
+ return this.entities.fetchEntityRelationships(iri);
10094
+ }
10095
+ /**
10096
+ * Fetch UUIDs of documents that reference the given entity IRI.
10097
+ * Result is stored in `entityInfoMap[uuid].documentRefs`.
10098
+ */
10099
+ async fetchEntityDocuments(iri) {
10100
+ if (this._destroyed)
10101
+ throw new Error("CueProjectView is destroyed");
10102
+ return this.entities.fetchEntityDocuments(iri);
10103
+ }
10104
+ /** Constructs the full RDF IRI for an entity UUID. */
10105
+ entityIri(uuid) {
10106
+ return this.entities.entityIri(uuid);
10107
+ }
10108
+ // ── Document methods ───────────────────────────────────────────────────────
10109
+ /**
10110
+ * Lazily batch-fetch document info for the given UUIDs.
10111
+ * Already-fetched UUIDs are skipped. Populates `documentInfoMap`.
10112
+ */
10113
+ requestDocumentData(uuids) {
10114
+ if (this._destroyed)
10115
+ return;
10116
+ this.documents.requestDocumentData(uuids);
10117
+ }
10118
+ // ── Search ─────────────────────────────────────────────────────────────────
10119
+ /**
10120
+ * Run a natural-language search against the project.
10121
+ * The result is stored in `searchResults` and replaces any previous result.
10122
+ */
10123
+ async search(term, options) {
10124
+ if (this._destroyed)
10125
+ return;
10126
+ const result = await this._api.search({
10127
+ term,
10128
+ projectId: this._projectId,
10129
+ categories: options?.categories
10130
+ });
10131
+ if (!this._destroyed) {
10132
+ this._searchResults.set(result);
10133
+ }
10134
+ }
10135
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
10136
+ /**
10137
+ * Switch the active language for schema labels and document text fields.
10138
+ * Schema responses are cached per language (instant if previously loaded).
10139
+ * The document info map is cleared and lazily re-populated on next access.
10140
+ */
10141
+ setLanguage(lang) {
10142
+ if (this._destroyed)
10143
+ return;
10144
+ this.schema.setLanguage(lang);
10145
+ this.documents.setLanguage(lang);
10146
+ }
10147
+ /**
10148
+ * Reset all entity and document state and re-fetch the project overview.
10149
+ * Prefer creating a fresh `CueProjectView` when switching projects.
10150
+ * Use `reset()` only when the same project's data needs to be invalidated.
10151
+ */
10152
+ reset() {
10153
+ if (this._destroyed)
10154
+ return;
10155
+ this.entities.reset();
10156
+ this.documents.reset();
10157
+ this._searchResults.set(void 0);
10158
+ this.documents.fetchOverview().catch((err) => console.error("[CueProjectView] fetchOverview failed after reset:", err));
10159
+ }
10160
+ /**
10161
+ * Tear down this view instance. Clears all reactive state and blocks further
10162
+ * updates. Call from the Angular adapter's `ngOnDestroy` or equivalent.
10163
+ */
10164
+ destroy() {
10165
+ this._destroyed = true;
10166
+ this._searchResults.set(void 0);
10167
+ }
9708
10168
  };
9709
10169
 
9710
- // libs/js/models/src/lib/project.ts
9711
- var import_uuid3 = require("uuid");
9712
-
9713
- // libs/js/models/src/lib/search-log-entry.ts
9714
- var import_uuid4 = require("uuid");
9715
-
9716
- // libs/js/models/src/lib/search-response.ts
9717
- var import_uuid5 = require("uuid");
10170
+ // libs/js/file-metadata-helpers/src/lib/js-file-metadata-helpers.ts
10171
+ init_src();
9718
10172
 
9719
10173
  // libs/js/rdf-document-writers/src/lib/alternative-representation.ts
9720
10174
  var import_n33 = require("n3");
@@ -10506,10 +10960,15 @@ var Cue = class _Cue {
10506
10960
  profile;
10507
10961
  privileges;
10508
10962
  cache;
10963
+ storage;
10509
10964
  _app;
10510
10965
  _endpoints;
10511
10966
  _isEmulator;
10967
+ _storageRaw;
10968
+ _storageProcessed;
10512
10969
  _gis = null;
10970
+ _projectDocuments = /* @__PURE__ */ new Map();
10971
+ _verbose;
10513
10972
  /**
10514
10973
  * Reactive GIS service. Lazily constructed on first access.
10515
10974
  *
@@ -10543,6 +11002,7 @@ var Cue = class _Cue {
10543
11002
  const env = config.environment ?? "production";
10544
11003
  this._endpoints = { ...ENDPOINTS[env], ...config.endpoints };
10545
11004
  this._isEmulator = env === "emulator";
11005
+ this._verbose = config.verbose ?? false;
10546
11006
  this._app = (0, import_app2.getApps)().find((a5) => a5.name === "[DEFAULT]") ?? (0, import_app2.initializeApp)({
10547
11007
  apiKey,
10548
11008
  appId,
@@ -10553,7 +11013,26 @@ var Cue = class _Cue {
10553
11013
  });
10554
11014
  this.auth = new CueAuth(this._app, this._isEmulator, this._endpoints);
10555
11015
  this.projects = new CueProjects(this.auth, this._app, this._isEmulator, this._endpoints);
11016
+ this._storageRaw = (0, import_storage5.getStorage)(this._app, BUCKET_RAW2);
11017
+ this._storageProcessed = (0, import_storage5.getStorage)(this._app, BUCKET_PROCESSED2);
11018
+ const storagePublic = (0, import_storage5.getStorage)(this._app, BUCKET_PUBLIC2);
11019
+ const storageLogs = (0, import_storage5.getStorage)(this._app, BUCKET_LOGS2);
11020
+ const storageChatSessions = (0, import_storage5.getStorage)(this._app, BUCKET_CHAT_SESSIONS2);
11021
+ const storagePersistence = (0, import_storage5.getStorage)(this._app, BUCKET_PERSISTENCE2);
11022
+ if (this._isEmulator) {
11023
+ (0, import_storage5.connectStorageEmulator)(this._storageRaw, this._endpoints.storageEmulatorHost, this._endpoints.storageEmulatorPort);
11024
+ (0, import_storage5.connectStorageEmulator)(this._storageProcessed, this._endpoints.storageEmulatorHost, this._endpoints.storageEmulatorPort);
11025
+ }
10556
11026
  this.api = this._buildApi(this.projects);
11027
+ const blob = new CueBlobStorage({
11028
+ storageRaw: this._storageRaw,
11029
+ storageProcessed: this._storageProcessed,
11030
+ storagePublic,
11031
+ storageLogs,
11032
+ storageChatSessions,
11033
+ storagePersistence
11034
+ });
11035
+ this.storage = new CueStorage(blob);
10557
11036
  this.profile = new CueProfile(
10558
11037
  this.auth,
10559
11038
  this._app,
@@ -10561,7 +11040,6 @@ var Cue = class _Cue {
10561
11040
  this._endpoints.gatewayUrl
10562
11041
  );
10563
11042
  this.privileges = new CuePrivileges(this.auth.isSuperAdmin);
10564
- const storagePersistence = (0, import_storage5.getStorage)(this._app, BUCKET_PERSISTENCE2);
10565
11043
  if (this._isEmulator) {
10566
11044
  (0, import_storage5.connectStorageEmulator)(storagePersistence, this._endpoints.storageEmulatorHost, this._endpoints.storageEmulatorPort);
10567
11045
  }
@@ -10609,16 +11087,20 @@ var Cue = class _Cue {
10609
11087
  const instance = Object.create(_Cue.prototype);
10610
11088
  const privileges = new CuePrivileges(auth.isSuperAdmin);
10611
11089
  const cache = new CueCache(storagePersistence);
11090
+ const storage = new CueStorage(blob);
10612
11091
  Object.assign(instance, {
10613
11092
  _app: app,
10614
11093
  _endpoints: endpoints,
10615
11094
  _isEmulator: env === "emulator",
11095
+ _storageRaw: storageRaw,
11096
+ _storageProcessed: storageProcessed,
10616
11097
  auth,
10617
11098
  api,
10618
11099
  projects,
10619
11100
  profile,
10620
11101
  privileges,
10621
- cache
11102
+ cache,
11103
+ storage
10622
11104
  });
10623
11105
  return instance;
10624
11106
  }
@@ -10649,29 +11131,110 @@ var Cue = class _Cue {
10649
11131
  get: (key) => this.cache.getQueryCache(projectId, key).then((entry) => entry?.results),
10650
11132
  set: (key, data) => this.cache.setQueryCache(projectId, key, { query: key, results: data })
10651
11133
  };
10652
- return new CueProjectView(this.api, projectId, { ...opts, queryCache });
11134
+ return new CueProjectView(this.api, projectId, { verbose: this._verbose, ...opts, queryCache });
11135
+ }
11136
+ /**
11137
+ * Creates a `CueProjectEntities` instance for the given project, with the
11138
+ * SDK query cache wired automatically.
11139
+ *
11140
+ * Prefer this over `new CueProjectEntities(cue.api, projectId)` — it avoids
11141
+ * the manual cache setup and keeps the instance bound to the correct API.
11142
+ *
11143
+ * @example
11144
+ * ```ts
11145
+ * const entities = cue.createProjectEntities('my-project');
11146
+ * entities.requestEntityData(['uuid1']);
11147
+ * const info = entities.entityInfoMap.get()['uuid1'];
11148
+ *
11149
+ * // Promise-based (no signal polling needed):
11150
+ * const graph = await entities.buildSummaryGraph('graph');
11151
+ * ```
11152
+ */
11153
+ createProjectEntities(projectId, opts) {
11154
+ const queryCache = opts?.queryCache ?? {
11155
+ get: (key) => this.cache.getQueryCache(projectId, key).then((entry) => entry?.results),
11156
+ set: (key, data) => this.cache.setQueryCache(projectId, key, { query: key, results: data })
11157
+ };
11158
+ return new CueProjectEntities(
11159
+ this.api,
11160
+ projectId,
11161
+ opts?.rdfBase,
11162
+ queryCache,
11163
+ opts?.graphType,
11164
+ opts?.verbose ?? this._verbose
11165
+ );
11166
+ }
11167
+ /**
11168
+ * Creates a `CueProjectDocuments` instance for the given project, with the
11169
+ * SDK query cache wired automatically.
11170
+ *
11171
+ * Prefer this over `new CueProjectDocuments(cue.api, projectId)` — it avoids
11172
+ * the manual cache setup and keeps the instance bound to the correct API.
11173
+ *
11174
+ * @example
11175
+ * ```ts
11176
+ * const docs = cue.createProjectDocuments('my-project');
11177
+ * await docs.fetchOverview();
11178
+ * const data = await docs.fetchDocumentData(['uuid1', 'uuid2']);
11179
+ * ```
11180
+ */
11181
+ createProjectDocuments(projectId, opts) {
11182
+ const hasCustomOptions = opts?.language !== void 0 || opts?.rdfBase !== void 0 || opts?.graphType !== void 0 || opts?.queryCache !== void 0;
11183
+ if (!hasCustomOptions) {
11184
+ const existing = this._projectDocuments.get(projectId);
11185
+ if (existing) {
11186
+ existing.setLanguage(this.api.language);
11187
+ return existing;
11188
+ }
11189
+ const queryCache2 = {
11190
+ get: (key) => this.cache.getQueryCache(projectId, key).then((entry) => entry?.results),
11191
+ set: (key, data) => this.cache.setQueryCache(projectId, key, { query: key, results: data })
11192
+ };
11193
+ const docs = new CueProjectDocuments(
11194
+ this.api,
11195
+ projectId,
11196
+ this.api.language,
11197
+ void 0,
11198
+ queryCache2,
11199
+ void 0,
11200
+ opts?.verbose ?? this._verbose
11201
+ );
11202
+ this._projectDocuments.set(projectId, docs);
11203
+ return docs;
11204
+ }
11205
+ const queryCache = opts?.queryCache ?? {
11206
+ get: (key) => this.cache.getQueryCache(projectId, key).then((entry) => entry?.results),
11207
+ set: (key, data) => this.cache.setQueryCache(projectId, key, { query: key, results: data })
11208
+ };
11209
+ return new CueProjectDocuments(
11210
+ this.api,
11211
+ projectId,
11212
+ opts?.language ?? this.api.language,
11213
+ opts?.rdfBase,
11214
+ queryCache,
11215
+ opts?.graphType,
11216
+ opts?.verbose ?? this._verbose
11217
+ );
10653
11218
  }
10654
11219
  };
10655
11220
 
10656
11221
  // libs/js/cue-sdk/src/lib/cue-node.ts
10657
- var import_storage6 = require("firebase/storage");
11222
+ var import_storage8 = require("firebase/storage");
10658
11223
  var CueNode = class extends Cue {
10659
11224
  constructor(config) {
10660
11225
  super(config);
10661
11226
  }
10662
11227
  _buildApi(projects) {
10663
- const storageChatSessions = (0, import_storage6.getStorage)(this._app, BUCKET_CHAT_SESSIONS2);
10664
- const storageLogs = (0, import_storage6.getStorage)(this._app, BUCKET_LOGS2);
10665
- const storageRaw = (0, import_storage6.getStorage)(this._app, BUCKET_RAW2);
10666
- const storagePersistence = (0, import_storage6.getStorage)(this._app, BUCKET_PERSISTENCE2);
10667
- const storageProcessed = (0, import_storage6.getStorage)(this._app, BUCKET_PROCESSED2);
10668
- const storagePublic = (0, import_storage6.getStorage)(this._app, BUCKET_PUBLIC2);
11228
+ const storageChatSessions = (0, import_storage8.getStorage)(this._app, BUCKET_CHAT_SESSIONS2);
11229
+ const storageLogs = (0, import_storage8.getStorage)(this._app, BUCKET_LOGS2);
11230
+ const storageRaw = this._storageRaw;
11231
+ const storagePersistence = (0, import_storage8.getStorage)(this._app, BUCKET_PERSISTENCE2);
11232
+ const storageProcessed = this._storageProcessed;
11233
+ const storagePublic = (0, import_storage8.getStorage)(this._app, BUCKET_PUBLIC2);
10669
11234
  if (this._isEmulator) {
10670
11235
  const storageHost = this._endpoints.storageEmulatorHost;
10671
11236
  const storagePort = this._endpoints.storageEmulatorPort;
10672
- (0, import_storage6.connectStorageEmulator)(storageRaw, storageHost, storagePort);
10673
- (0, import_storage6.connectStorageEmulator)(storageProcessed, storageHost, storagePort);
10674
- (0, import_storage6.connectStorageEmulator)(storagePublic, storageHost, storagePort);
11237
+ (0, import_storage8.connectStorageEmulator)(storagePublic, storageHost, storagePort);
10675
11238
  }
10676
11239
  const blob = new CueBlobStorage({
10677
11240
  storageChatSessions,
@@ -10834,21 +11397,21 @@ async function compareHandler(options) {
10834
11397
  }
10835
11398
 
10836
11399
  // apps/desktop/cue-cli/src/helpers/get-files-containing-substring.ts
10837
- var import_storage7 = require("firebase/storage");
11400
+ var import_storage9 = require("firebase/storage");
10838
11401
  var import_path2 = require("path");
10839
11402
  var import_promises = require("fs/promises");
10840
11403
  async function getFilesContainingSubstring(bucket, subDir = "", subString = "") {
10841
11404
  const firebase = CueFirebase.getInstance();
10842
11405
  const storage = bucket === "raw" ? firebase.storageRaw : firebase.storageProcessed;
10843
11406
  console.log("Fetching files from storage bucket:", bucket, "in subdir:", subDir, "containing substring:", subString);
10844
- const listResult = await (0, import_storage7.listAll)((0, import_storage7.ref)(storage, subDir));
11407
+ const listResult = await (0, import_storage9.listAll)((0, import_storage9.ref)(storage, subDir));
10845
11408
  const outputDir = (0, import_path2.join)(process.cwd(), "downloaded_blobs", bucket, subDir);
10846
11409
  await (0, import_promises.mkdir)(outputDir, { recursive: true });
10847
11410
  for (const fileRef of listResult.items) {
10848
11411
  console.log(fileRef.name);
10849
11412
  if (subDir && !fileRef.name.includes(subString))
10850
11413
  continue;
10851
- const bytes = await (0, import_storage7.getBytes)(fileRef);
11414
+ const bytes = await (0, import_storage9.getBytes)(fileRef);
10852
11415
  const outputPath = (0, import_path2.join)(outputDir, fileRef.name);
10853
11416
  await (0, import_promises.writeFile)(outputPath, Buffer.from(bytes));
10854
11417
  console.log(`Downloaded ${fileRef.name} to ${outputPath} \u2705`);
@@ -11242,16 +11805,16 @@ async function dumpHandler(options) {
11242
11805
  }
11243
11806
 
11244
11807
  // apps/desktop/cue-cli/src/helpers/repair-remote-ttl.ts
11245
- var import_storage8 = require("firebase/storage");
11808
+ var import_storage10 = require("firebase/storage");
11246
11809
  async function repairRemoteTTL(space, subString, regex, substituteString) {
11247
11810
  const firebase = CueFirebase.getInstance();
11248
11811
  const storage = firebase.storageProcessed;
11249
11812
  console.log("Fetching files from storage bucket 'triples' containing substring:", subString);
11250
- const listResult = await (0, import_storage8.listAll)((0, import_storage8.ref)(storage, `${space}/triples`));
11813
+ const listResult = await (0, import_storage10.listAll)((0, import_storage10.ref)(storage, `${space}/triples`));
11251
11814
  for (const fileRef of listResult.items) {
11252
11815
  if (subString && !fileRef.name.match(subString))
11253
11816
  continue;
11254
- const stream = await (0, import_storage8.getStream)(fileRef);
11817
+ const stream = await (0, import_storage10.getStream)(fileRef);
11255
11818
  const reader = stream.getReader();
11256
11819
  const chunks = [];
11257
11820
  let done = false;
@@ -11275,13 +11838,13 @@ async function repairRemoteTTL(space, subString, regex, substituteString) {
11275
11838
  const buffer = Buffer.from(fileContent, "utf8");
11276
11839
  let existingMetadata = {};
11277
11840
  try {
11278
- existingMetadata = await (0, import_storage8.getMetadata)(fileRef);
11841
+ existingMetadata = await (0, import_storage10.getMetadata)(fileRef);
11279
11842
  } catch (err) {
11280
11843
  console.warn(`Could not fetch metadata for ${fileRef.name}, proceeding with default.`);
11281
11844
  }
11282
11845
  const customMetadata = { ...existingMetadata.customMetadata || {}, stored: "False" };
11283
11846
  const metadata = { customMetadata };
11284
- await (0, import_storage8.uploadBytesResumable)((0, import_storage8.ref)(storage, `${space}/triples/${fileRef.name}`), buffer, metadata);
11847
+ await (0, import_storage10.uploadBytesResumable)((0, import_storage10.ref)(storage, `${space}/triples/${fileRef.name}`), buffer, metadata);
11285
11848
  console.log(`Fixed ${fileRef.name} \u2705`);
11286
11849
  } else {
11287
11850
  console.log(`No changes for ${fileRef.name}`);
@@ -12011,6 +12574,10 @@ async function buildCueClient(options) {
12011
12574
  console.error("API key is required. Provide it via --key or CUE_API_KEY env variable.");
12012
12575
  process.exit(1);
12013
12576
  }
12577
+ if (options.verbose) {
12578
+ const source = options.key ? "--key flag" : "CUE_API_KEY env variable";
12579
+ console.error(`Authenticating with API key from ${source}...`);
12580
+ }
12014
12581
  const cue = new CueNode({
12015
12582
  apiKey: FIREBASE_CONFIG().apiKey,
12016
12583
  appId: FIREBASE_CONFIG().appId,
@@ -12036,6 +12603,7 @@ async function listProjectsHandler(options) {
12036
12603
  lastSync: p.lastSync
12037
12604
  }));
12038
12605
  console.log(JSON.stringify(output, null, 2));
12606
+ process.exit(0);
12039
12607
  } catch (err) {
12040
12608
  console.error("Error:", err instanceof Error ? err.message : String(err));
12041
12609
  process.exit(1);
@@ -12045,20 +12613,32 @@ async function entitySummaryGraphHandler(options) {
12045
12613
  try {
12046
12614
  const cue = await buildCueClient(options);
12047
12615
  if (options.verbose)
12048
- console.error(`Fetching entity summary graph for project ${options.space}...`);
12616
+ console.error(`Fetching entity summary graph for project ${options.space}${options.entity ? ` (neighbourhood of ${options.entity})` : ""}...`);
12049
12617
  const entities = new CueProjectEntities(cue.api, options.space);
12050
12618
  if (options.format === "graph") {
12051
- const graph = await entities.buildSummaryGraph("graph");
12619
+ const graph = await entities.buildSummaryGraph("graph", options.entity);
12052
12620
  console.log(JSON.stringify(graph, null, 2));
12053
12621
  } else if (options.format === "md") {
12054
- const md = await entities.buildSummaryGraph("md");
12622
+ const md = await entities.buildSummaryGraph("md", options.entity);
12055
12623
  console.log(md);
12056
12624
  } else {
12057
- const raw = await entities.buildSummaryGraph();
12625
+ const raw = await entities.buildSummaryGraph(void 0, options.entity);
12058
12626
  console.log(JSON.stringify(raw, null, 2));
12059
12627
  }
12628
+ process.exit(0);
12060
12629
  } catch (err) {
12061
- console.error("Error:", err instanceof Error ? err.message : String(err));
12630
+ const msg = err instanceof Error ? err.message : String(err);
12631
+ if (msg.includes("403")) {
12632
+ console.error(`Error: ${msg}`);
12633
+ console.error(
12634
+ `Hint: 403 Forbidden usually means the API key does not have access to project "${options.space}".
12635
+ - If targeting a local environment, add the -e / --emulators flag.
12636
+ - If using a production project, verify the API key has access.
12637
+ - API key source: ${options.key ? "--key flag" : "CUE_API_KEY env variable"}`
12638
+ );
12639
+ } else {
12640
+ console.error("Error:", msg);
12641
+ }
12062
12642
  process.exit(1);
12063
12643
  }
12064
12644
  }
@@ -12084,6 +12664,7 @@ async function sparqlHandler(options) {
12084
12664
  console.error(`Executing SPARQL query against project ${options.space}...`);
12085
12665
  const result = await cue.api.sparql(sparqlQuery, options.space);
12086
12666
  console.log(JSON.stringify(result, null, 2));
12667
+ process.exit(0);
12087
12668
  } catch (err) {
12088
12669
  console.error("Error:", err instanceof Error ? err.message : String(err));
12089
12670
  process.exit(1);
@@ -12091,7 +12672,7 @@ async function sparqlHandler(options) {
12091
12672
  }
12092
12673
  var appBuilderToolsCommand = new import_commander.Command("app-builder-tools").description("Tools for agent-assisted Cue app development. Outputs JSON for machine consumption.");
12093
12674
  appBuilderToolsCommand.command("list-projects").description("List all projects accessible to the authenticated user").option("-k, --key <api-key>", "Specify the API key (or set CUE_API_KEY env variable)").option("-e, --emulators", "Use emulators", false).option("-v, --verbose", "Enable verbose output", false).action(listProjectsHandler);
12094
- appBuilderToolsCommand.command("entity-summary-graph").description("Fetch the entity category relationship summary graph for a project").requiredOption("-s, --space <id>", "Project ID (required)").option("-k, --key <api-key>", "Specify the API key (or set CUE_API_KEY env variable)").option("-e, --emulators", "Use emulators", false).option("-f, --format <format>", "Output format: md (aligned text), graph (JSON nodes+edges), json (raw SPARQL)", "md").option("-v, --verbose", "Enable verbose output", false).action(entitySummaryGraphHandler);
12675
+ appBuilderToolsCommand.command("entity-summary-graph").description("Fetch the entity category relationship summary graph for a project").requiredOption("-s, --space <id>", "Project ID (required)").option("-k, --key <api-key>", "Specify the API key (or set CUE_API_KEY env variable)").option("-e, --emulators", "Use emulators", false).option("-f, --format <format>", "Output format: md (aligned text), graph (JSON nodes+edges), json (raw SPARQL)", "md").option("--entity <iri>", "Filter to the one-hop neighbourhood of this category IRI (prefixed or full, e.g. qcy:Building)").option("-v, --verbose", "Enable verbose output", false).action(entitySummaryGraphHandler);
12095
12676
  appBuilderToolsCommand.command("sparql").description("Execute a SPARQL SELECT query against a project triplestore. Query via --query or stdin.").requiredOption("-s, --space <id>", "Project ID (required)").option("-q, --query <sparql>", "SPARQL query string (or pipe query via stdin)").option("-k, --key <api-key>", "Specify the API key (or set CUE_API_KEY env variable)").option("-e, --emulators", "Use emulators", false).option("-v, --verbose", "Enable verbose output", false).action(sparqlHandler);
12096
12677
 
12097
12678
  // apps/desktop/cue-cli/src/main.ts