nexarch 0.12.16 → 0.12.19

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.
@@ -174,7 +174,13 @@ export function buildIngestPayload(params) {
174
174
  attributes: { source: "terraform_ingest", terraform_version: projection.terraformVersion },
175
175
  },
176
176
  ];
177
- const relationships = [];
177
+ const relationships = [
178
+ // The environment lives on the platform. Without this edge the two nodes
179
+ // this payload always creates float beside each other, and no query can
180
+ // answer "which cloud is dev on" — every resource knows both, but the
181
+ // environment itself knows neither.
182
+ { relationshipTypeCode: "runs_on", fromEntityRef: environmentRef, toEntityRef: platformRef },
183
+ ];
178
184
  // Lookup tables for the topology pass below, populated alongside entities so
179
185
  // a single loop over projection.resources is enough for both.
180
186
  const entityRefByTypeAndName = new Map();
@@ -189,6 +195,7 @@ export function buildIngestPayload(params) {
189
195
  const identityEntityRefByName = new Map();
190
196
  const identityRefByPrincipalId = new Map();
191
197
  const catalogueByResourceType = new Map(catalogue.map((entry) => [entry.resourceType, entry]));
198
+ const canonicalTechnologies = new Set();
192
199
  for (const resource of projection.resources) {
193
200
  const entityRef = `${resource.entityTypeCode}:${slug(resource.name ?? resource.address)}`;
194
201
  entities.push({
@@ -219,6 +226,22 @@ export function buildIngestPayload(params) {
219
226
  relationships.push({ relationshipTypeCode: "runs_on", fromEntityRef: entityRef, toEntityRef: environmentRef });
220
227
  relationships.push({ relationshipTypeCode: "runs_on", fromEntityRef: entityRef, toEntityRef: platformRef });
221
228
  }
229
+ // What product is this? Without it the graph holds a resource that knows
230
+ // where it runs and whose cloud it is on, but not what it *is* — the only
231
+ // record of that being a raw `terraform_type` attribute, which no reader
232
+ // can join across environments or tenants. The canonical entry is shared
233
+ // vocabulary, so one query answers "everything we run on Azure AI
234
+ // Services" across every environment and every IaC tool.
235
+ const canonicalRef = catalogueByResourceType.get(resource.resourceType)?.canonicalEntityRef;
236
+ if (canonicalRef && !canonicalTechnologies.has(canonicalRef)) {
237
+ canonicalTechnologies.add(canonicalRef);
238
+ }
239
+ if (canonicalRef) {
240
+ // `instance_of`, not `runs_on`: this vault does not run on Azure Key
241
+ // Vault, it is one. runs_on is a hosting relation and is already covered
242
+ // above by the edges to the platform and the environment.
243
+ relationships.push({ relationshipTypeCode: "instance_of", fromEntityRef: entityRef, toEntityRef: canonicalRef });
244
+ }
222
245
  if (resource.name) {
223
246
  entityRefByTypeAndName.set(`${resource.resourceType}::${resource.name}`, entityRef);
224
247
  entityRefByName.set(resource.name, entityRef);
@@ -298,8 +321,38 @@ export function buildIngestPayload(params) {
298
321
  addTopologyEdge("depends_on", entityRef, identityEntityRefByName.get(identityName));
299
322
  }
300
323
  }
324
+ // The canonical technologies referenced above, upserted so the edges have
325
+ // something to land on. Named from the ref rather than invented per tenant:
326
+ // these are shared vocabulary, and a tenant-local "Azure AI Services" that
327
+ // differs by a word defeats the point of having a canonical entry at all.
328
+ for (const canonicalRef of canonicalTechnologies) {
329
+ const canonicalTypeCode = canonicalRef.split(":")[1] ?? "platform_component";
330
+ entities.push({
331
+ entityRef: canonicalRef,
332
+ entityTypeCode: canonicalTypeCode,
333
+ name: canonicalTechnologyName(canonicalRef),
334
+ description: "Cloud service referenced by infrastructure ingestion.",
335
+ attributes: { source: "terraform_ingest", canonical: true },
336
+ });
337
+ // A managed service belongs to the cloud that offers it. Only from a
338
+ // platform_component: part_of does not accept a technology_component
339
+ // source, so a canonical that is one is left unattached rather than sent
340
+ // to fail governance.
341
+ if (canonicalTypeCode === "platform_component") {
342
+ relationships.push({ relationshipTypeCode: "part_of", fromEntityRef: canonicalRef, toEntityRef: platformRef });
343
+ }
344
+ }
301
345
  return { entities, relationships, environmentRef };
302
346
  }
347
+ /** Turns global:platform_component:azure_ai_services into "Azure AI Services". */
348
+ function canonicalTechnologyName(canonicalRef) {
349
+ const slugPart = canonicalRef.split(":").slice(2).join(":") || canonicalRef;
350
+ return slugPart
351
+ .split("_")
352
+ .filter(Boolean)
353
+ .map((word) => (word.length <= 3 && word === word.toLowerCase() && ["ai", "api", "dns", "sql", "cdn"].includes(word) ? word.toUpperCase() : word[0].toUpperCase() + word.slice(1)))
354
+ .join(" ");
355
+ }
303
356
  function chunk(items, size) {
304
357
  const out = [];
305
358
  for (let i = 0; i < items.length; i += size)
@@ -338,6 +391,22 @@ export async function ingestInfra(args) {
338
391
  ` 4. if this repository has several root modules, ingest each separately with --dir\n` +
339
392
  ` The estate may also simply not have been applied yet.`);
340
393
  }
394
+ // Nothing recognised is a coverage problem, and has to be said before the
395
+ // environment is even considered. Otherwise a root whose every resource type
396
+ // is missing from the catalogue fails with "could not determine the
397
+ // environment — 0 of those carried any tags", which sends the reader off to
398
+ // tag resources that would still not be ingested afterwards. The environment
399
+ // is the next question only once there is something to attach it to.
400
+ if (projection.stats.resourcesProjected === 0) {
401
+ const seen = projection.unknownTypes
402
+ .slice(0, 12)
403
+ .map((entry) => ` ${String(entry.count).padStart(3)} ${entry.resourceType}`)
404
+ .join("\n");
405
+ throw new Error(`Nothing in this root module is in the catalogue yet — read ${projection.stats.resourcesSeen} resources, matched none.\n` +
406
+ ` Tagging or naming an environment will not help: there would still be nothing to ingest.\n` +
407
+ `\n Resource types found here:\n${seen}\n` +
408
+ `\n These need adding to the platform catalogue before this root can be ingested.`);
409
+ }
341
410
  const { environment, how } = resolveEnvironment(projection, environmentOverride, workingDir, statePath ? null : workingDir);
342
411
  projection.environment = environment;
343
412
  // Azure is the only provider the seed catalogue covers today; resolving it
@@ -359,6 +428,26 @@ export async function ingestInfra(args) {
359
428
  // Reference resolution is an optimisation, not a requirement.
360
429
  }
361
430
  }
431
+ // Types the catalogue does not cover are filed as reference candidates, the
432
+ // same way an unrecognised package name is. Printing them to a console nobody
433
+ // aggregates meant the platform could never learn which services customers
434
+ // actually run: curation happened when someone happened to notice. A resource
435
+ // type is vendor vocabulary — `azurerm_static_web_app` contains nothing of the
436
+ // tenant's — so it is safe to file in a cross-tenant queue, where seen_count
437
+ // accumulates the evidence for covering it.
438
+ //
439
+ // Resolving doubles as filing: a type someone has already promoted comes back
440
+ // resolved, and only genuine misses become candidates.
441
+ if (!dryRun && projection.unknownTypes.length > 0) {
442
+ try {
443
+ await callMcpTool("nexarch_resolve_reference", {
444
+ names: projection.unknownTypes.slice(0, 200).map((entry) => entry.resourceType),
445
+ });
446
+ }
447
+ catch {
448
+ // Coverage reporting must never fail an ingest: the estate is the point.
449
+ }
450
+ }
362
451
  const { entities, relationships } = buildIngestPayload({ projection, environment, platformRef, platformName, catalogue });
363
452
  const report = {
364
453
  ok: true,
@@ -12,7 +12,7 @@
12
12
  * ahead of it: editing it by hand recreates the two-sources-of-truth problem
13
13
  * moving the catalogue out of the CLI was meant to end.
14
14
  *
15
- * 25 entries, exported 2026-08-21.
15
+ * 30 entries, exported 2026-08-21.
16
16
  */
17
17
  export const IAC_CATALOGUE_SEED = [
18
18
  {
@@ -22,6 +22,7 @@ export const IAC_CATALOGUE_SEED = [
22
22
  role: "compute_environment",
23
23
  attributes: ["name", "location", "resource_group_name", "default_domain", "infrastructure_subnet_id", "internal_load_balancer_enabled", "static_ip_address", "zone_redundancy_enabled"],
24
24
  references: { "infrastructure_subnet_id": "azurerm_subnet" },
25
+ canonicalEntityRef: "global:platform_component:azure_container_apps",
25
26
  cloudResourceType: "Microsoft.App/managedEnvironments",
26
27
  },
27
28
  {
@@ -30,6 +31,7 @@ export const IAC_CATALOGUE_SEED = [
30
31
  entitySubtypeCode: "platform_runtime",
31
32
  role: "compute_environment",
32
33
  attributes: ["name", "location", "resource_group_name", "sku_name", "os_type", "worker_count"],
34
+ canonicalEntityRef: "global:platform_component:azure_app_service",
33
35
  cloudResourceType: "Microsoft.Web/serverfarms",
34
36
  },
35
37
  {
@@ -38,6 +40,7 @@ export const IAC_CATALOGUE_SEED = [
38
40
  entitySubtypeCode: "platform_service",
39
41
  role: "compute_host",
40
42
  attributes: ["name", "location", "resource_group_name", "kind", "sku_name", "endpoint", "custom_subdomain_name", "public_network_access_enabled", "local_auth_enabled"],
43
+ canonicalEntityRef: "global:platform_component:azure_ai_services",
41
44
  cloudResourceType: "Microsoft.CognitiveServices/accounts",
42
45
  },
43
46
  {
@@ -48,6 +51,7 @@ export const IAC_CATALOGUE_SEED = [
48
51
  bindable: true,
49
52
  attributes: ["name", "location", "resource_group_name", "container_app_environment_id", "revision_mode", "workload_profile_name"],
50
53
  references: { "container_app_environment_id": "azurerm_container_app_environment" },
54
+ canonicalEntityRef: "global:platform_component:azure_container_apps",
51
55
  cloudResourceType: "Microsoft.App/containerApps",
52
56
  },
53
57
  {
@@ -58,6 +62,7 @@ export const IAC_CATALOGUE_SEED = [
58
62
  bindable: true,
59
63
  attributes: ["name", "location", "resource_group_name", "container_app_environment_id", "replica_timeout_in_seconds", "workload_profile_name"],
60
64
  references: { "container_app_environment_id": "azurerm_container_app_environment" },
65
+ canonicalEntityRef: "global:platform_component:azure_container_apps",
61
66
  cloudResourceType: "Microsoft.App/jobs",
62
67
  },
63
68
  {
@@ -68,14 +73,26 @@ export const IAC_CATALOGUE_SEED = [
68
73
  bindable: true,
69
74
  attributes: ["name", "location", "resource_group_name", "service_plan_id", "https_only", "default_hostname"],
70
75
  references: { "service_plan_id": "azurerm_service_plan" },
76
+ canonicalEntityRef: "global:platform_component:azure_app_service",
71
77
  cloudResourceType: "Microsoft.Web/sites",
72
78
  },
79
+ {
80
+ resourceType: "azurerm_static_web_app",
81
+ entityTypeCode: "platform_component",
82
+ entitySubtypeCode: "platform_service",
83
+ role: "compute_host",
84
+ bindable: true,
85
+ attributes: ["name", "location", "resource_group_name", "default_host_name", "sku_tier", "sku_size", "public_network_access_enabled", "preview_environments_enabled", "repository_url"],
86
+ canonicalEntityRef: "global:platform_component:azure_static_web_apps",
87
+ cloudResourceType: "Microsoft.Web/staticSites",
88
+ },
73
89
  {
74
90
  resourceType: "azurerm_cosmosdb_account",
75
91
  entityTypeCode: "data_store",
76
92
  entitySubtypeCode: "store_document",
77
93
  role: "data_store",
78
94
  attributes: ["name", "location", "resource_group_name", "kind", "offer_type", "public_network_access_enabled", "endpoint"],
95
+ canonicalEntityRef: "global:platform_component:azure_cosmos_db",
79
96
  cloudResourceType: "Microsoft.DocumentDB/databaseAccounts",
80
97
  },
81
98
  {
@@ -84,6 +101,7 @@ export const IAC_CATALOGUE_SEED = [
84
101
  entitySubtypeCode: "store_relational",
85
102
  role: "data_store",
86
103
  attributes: ["name", "location", "resource_group_name", "fqdn", "version", "sku_name", "storage_mb", "storage_tier", "public_network_access_enabled", "delegated_subnet_id", "private_dns_zone_id", "backup_retention_days", "geo_redundant_backup_enabled", "zone"],
104
+ canonicalEntityRef: "global:platform_component:azure_database_for_postgresql",
87
105
  cloudResourceType: "Microsoft.DBforPostgreSQL/flexibleServers",
88
106
  },
89
107
  {
@@ -92,6 +110,7 @@ export const IAC_CATALOGUE_SEED = [
92
110
  entitySubtypeCode: "store_relational",
93
111
  role: "data_store",
94
112
  attributes: ["name", "server_id", "charset", "collation"],
113
+ canonicalEntityRef: "global:platform_component:azure_database_for_postgresql",
95
114
  cloudResourceType: "Microsoft.DBforPostgreSQL/flexibleServers/databases",
96
115
  },
97
116
  {
@@ -100,6 +119,7 @@ export const IAC_CATALOGUE_SEED = [
100
119
  entitySubtypeCode: "store_object",
101
120
  role: "data_store",
102
121
  attributes: ["name", "location", "resource_group_name", "account_kind", "account_tier", "account_replication_type", "access_tier", "https_traffic_only_enabled", "min_tls_version", "public_network_access_enabled", "allow_nested_items_to_be_public", "shared_access_key_enabled", "primary_blob_endpoint", "primary_location", "secondary_location"],
122
+ canonicalEntityRef: "global:platform_component:azure_storage",
103
123
  cloudResourceType: "Microsoft.Storage/storageAccounts",
104
124
  },
105
125
  {
@@ -108,6 +128,7 @@ export const IAC_CATALOGUE_SEED = [
108
128
  entitySubtypeCode: "store_object",
109
129
  role: "data_store",
110
130
  attributes: ["name", "container_access_type", "storage_account_id", "has_immutability_policy"],
131
+ canonicalEntityRef: "global:platform_component:azure_storage",
111
132
  cloudResourceType: "Microsoft.Storage/storageAccounts/blobServices/containers",
112
133
  },
113
134
  {
@@ -117,6 +138,7 @@ export const IAC_CATALOGUE_SEED = [
117
138
  role: "edge",
118
139
  attributes: ["name", "host_name", "enabled", "cdn_frontdoor_profile_id"],
119
140
  references: { "cdn_frontdoor_profile_id": "azurerm_cdn_frontdoor_profile" },
141
+ canonicalEntityRef: "global:platform_component:azure_front_door",
120
142
  cloudResourceType: "Microsoft.Cdn/profiles/afdEndpoints",
121
143
  },
122
144
  {
@@ -124,6 +146,7 @@ export const IAC_CATALOGUE_SEED = [
124
146
  entityTypeCode: "policy_control",
125
147
  role: "edge",
126
148
  attributes: ["name", "location", "resource_group_name", "sku_name", "mode", "enabled"],
149
+ canonicalEntityRef: "global:platform_component:azure_web_application_firewall",
127
150
  cloudResourceType: "Microsoft.Network/frontDoorWebApplicationFirewallPolicies",
128
151
  },
129
152
  {
@@ -132,13 +155,44 @@ export const IAC_CATALOGUE_SEED = [
132
155
  entitySubtypeCode: "platform_service",
133
156
  role: "edge",
134
157
  attributes: ["name", "location", "resource_group_name", "sku_name", "response_timeout_seconds"],
158
+ canonicalEntityRef: "global:platform_component:azure_front_door",
135
159
  cloudResourceType: "Microsoft.Cdn/profiles",
136
160
  },
161
+ {
162
+ resourceType: "azurerm_static_web_app_custom_domain",
163
+ entityTypeCode: "platform_component",
164
+ entitySubtypeCode: "platform_service",
165
+ role: "edge",
166
+ attributes: ["domain_name", "validation_type"],
167
+ references: { "static_web_app_id": "azurerm_static_web_app" },
168
+ canonicalEntityRef: "global:platform_component:azure_static_web_apps",
169
+ cloudResourceType: "Microsoft.Web/staticSites/customDomains",
170
+ },
171
+ {
172
+ resourceType: "azurerm_traffic_manager_external_endpoint",
173
+ entityTypeCode: "platform_component",
174
+ entitySubtypeCode: "platform_service",
175
+ role: "edge",
176
+ attributes: ["name", "target", "endpoint_location", "enabled", "priority", "weight", "always_serve_enabled"],
177
+ references: { "profile_id": "azurerm_traffic_manager_profile" },
178
+ canonicalEntityRef: "global:platform_component:azure_traffic_manager",
179
+ cloudResourceType: "Microsoft.Network/trafficManagerProfiles/externalEndpoints",
180
+ },
181
+ {
182
+ resourceType: "azurerm_traffic_manager_profile",
183
+ entityTypeCode: "platform_component",
184
+ entitySubtypeCode: "platform_service",
185
+ role: "edge",
186
+ attributes: ["name", "resource_group_name", "fqdn", "traffic_routing_method", "profile_status", "traffic_view_enabled", "max_return"],
187
+ canonicalEntityRef: "global:platform_component:azure_traffic_manager",
188
+ cloudResourceType: "Microsoft.Network/trafficManagerProfiles",
189
+ },
137
190
  {
138
191
  resourceType: "azurerm_role_assignment",
139
192
  entityTypeCode: "policy_control",
140
193
  role: "identity",
141
194
  attributes: ["name", "principal_id", "principal_type", "role_definition_name", "scope"],
195
+ canonicalEntityRef: "global:platform_component:azure_rbac",
142
196
  cloudResourceType: "Microsoft.Authorization/roleAssignments",
143
197
  },
144
198
  {
@@ -147,6 +201,7 @@ export const IAC_CATALOGUE_SEED = [
147
201
  entitySubtypeCode: "platform_control_plane",
148
202
  role: "identity",
149
203
  attributes: ["name", "location", "resource_group_name", "client_id", "principal_id", "tenant_id"],
204
+ canonicalEntityRef: "global:platform_component:azure_managed_identity",
150
205
  cloudResourceType: "Microsoft.ManagedIdentity/userAssignedIdentities",
151
206
  },
152
207
  {
@@ -155,6 +210,7 @@ export const IAC_CATALOGUE_SEED = [
155
210
  entitySubtypeCode: "platform_service",
156
211
  role: "messaging",
157
212
  attributes: ["name", "location", "resource_group_name", "sku", "public_network_access_enabled"],
213
+ canonicalEntityRef: "global:platform_component:azure_service_bus",
158
214
  cloudResourceType: "Microsoft.ServiceBus/namespaces",
159
215
  },
160
216
  {
@@ -164,6 +220,7 @@ export const IAC_CATALOGUE_SEED = [
164
220
  role: "messaging",
165
221
  attributes: ["name", "namespace_id", "max_delivery_count", "requires_session"],
166
222
  references: { "namespace_id": "azurerm_servicebus_namespace" },
223
+ canonicalEntityRef: "global:platform_component:azure_service_bus",
167
224
  cloudResourceType: "Microsoft.ServiceBus/namespaces/queues",
168
225
  },
169
226
  {
@@ -172,6 +229,7 @@ export const IAC_CATALOGUE_SEED = [
172
229
  entitySubtypeCode: "platform_service",
173
230
  role: "network_boundary",
174
231
  attributes: ["name", "resource_group_name", "number_of_record_sets"],
232
+ canonicalEntityRef: "global:platform_component:azure_private_dns",
175
233
  cloudResourceType: "Microsoft.Network/privateDnsZones",
176
234
  },
177
235
  {
@@ -181,6 +239,7 @@ export const IAC_CATALOGUE_SEED = [
181
239
  role: "network_boundary",
182
240
  attributes: ["name", "location", "resource_group_name", "subnet_id"],
183
241
  references: { "subnet_id": "azurerm_subnet" },
242
+ canonicalEntityRef: "global:platform_component:azure_private_link",
184
243
  cloudResourceType: "Microsoft.Network/privateEndpoints",
185
244
  },
186
245
  {
@@ -190,6 +249,7 @@ export const IAC_CATALOGUE_SEED = [
190
249
  role: "network_boundary",
191
250
  attributes: ["name", "resource_group_name", "virtual_network_name", "address_prefixes", "default_outbound_access_enabled", "private_endpoint_network_policies"],
192
251
  nameReferences: { "virtual_network_name": "azurerm_virtual_network" },
252
+ canonicalEntityRef: "global:platform_component:azure_virtual_network",
193
253
  cloudResourceType: "Microsoft.Network/virtualNetworks/subnets",
194
254
  },
195
255
  {
@@ -198,6 +258,7 @@ export const IAC_CATALOGUE_SEED = [
198
258
  entitySubtypeCode: "platform_service",
199
259
  role: "network_boundary",
200
260
  attributes: ["name", "location", "resource_group_name", "address_space", "guid"],
261
+ canonicalEntityRef: "global:platform_component:azure_virtual_network",
201
262
  cloudResourceType: "Microsoft.Network/virtualNetworks",
202
263
  },
203
264
  {
@@ -206,6 +267,7 @@ export const IAC_CATALOGUE_SEED = [
206
267
  entitySubtypeCode: "platform_service",
207
268
  role: "observability",
208
269
  attributes: ["name", "location", "resource_group_name", "sku", "retention_in_days", "workspace_id"],
270
+ canonicalEntityRef: "global:platform_component:azure_monitor",
209
271
  cloudResourceType: "Microsoft.OperationalInsights/workspaces",
210
272
  },
211
273
  {
@@ -214,14 +276,25 @@ export const IAC_CATALOGUE_SEED = [
214
276
  entitySubtypeCode: "platform_service",
215
277
  role: "registry",
216
278
  attributes: ["name", "location", "resource_group_name", "login_server", "sku", "admin_enabled", "public_network_access_enabled"],
279
+ canonicalEntityRef: "global:platform_component:azure_container_registry",
217
280
  cloudResourceType: "Microsoft.ContainerRegistry/registries",
218
281
  },
282
+ {
283
+ resourceType: "azurerm_resource_group",
284
+ entityTypeCode: "platform_component",
285
+ entitySubtypeCode: "platform_service",
286
+ role: "resource_group",
287
+ attributes: ["name", "location"],
288
+ canonicalEntityRef: "global:platform_component:azure_resource_group",
289
+ cloudResourceType: "Microsoft.Resources/resourceGroups",
290
+ },
219
291
  {
220
292
  resourceType: "azurerm_key_vault",
221
293
  entityTypeCode: "platform_component",
222
294
  entitySubtypeCode: "platform_service",
223
295
  role: "secret_store",
224
296
  attributes: ["name", "location", "resource_group_name", "vault_uri", "sku_name", "enable_rbac_authorization", "rbac_authorization_enabled", "public_network_access_enabled", "purge_protection_enabled", "soft_delete_retention_days"],
297
+ canonicalEntityRef: "global:platform_component:azure_key_vault",
225
298
  cloudResourceType: "Microsoft.KeyVault/vaults",
226
299
  },
227
300
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.12.16",
3
+ "version": "0.12.19",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",