nexarch 0.9.5 → 0.9.6

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.
@@ -1,4 +1,5 @@
1
1
  import process from "process";
2
+ import { existsSync, readFileSync } from "node:fs";
2
3
  import { requireCredentials } from "../lib/credentials.js";
3
4
  import { callMcpTool } from "../lib/mcp.js";
4
5
  function parseFlag(args, flag) {
@@ -17,11 +18,65 @@ function parseToolText(result) {
17
18
  const text = result.content?.[0]?.text ?? "{}";
18
19
  return JSON.parse(text);
19
20
  }
21
+ function parseRelationships(args) {
22
+ const relationshipsFile = parseOptionValue(args, "--relationships-file");
23
+ const relationshipsJson = parseOptionValue(args, "--relationships-json");
24
+ if (relationshipsFile && relationshipsJson) {
25
+ throw new Error("Use only one of --relationships-file or --relationships-json");
26
+ }
27
+ if (relationshipsFile || relationshipsJson) {
28
+ let raw = relationshipsJson;
29
+ if (relationshipsFile) {
30
+ if (!existsSync(relationshipsFile))
31
+ throw new Error(`--relationships-file not found: ${relationshipsFile}`);
32
+ raw = readFileSync(relationshipsFile, "utf8");
33
+ }
34
+ let parsed;
35
+ try {
36
+ parsed = JSON.parse(raw ?? "[]");
37
+ }
38
+ catch {
39
+ throw new Error(relationshipsFile ? "--relationships-file must contain valid JSON array" : "--relationships-json must be valid JSON array");
40
+ }
41
+ if (!Array.isArray(parsed) || parsed.length === 0) {
42
+ throw new Error("Relationships input must be a non-empty JSON array");
43
+ }
44
+ return parsed.map((item, index) => {
45
+ const v = (item ?? {});
46
+ const from = String(v.fromEntityExternalKey ?? v.fromEntityRef ?? v.from ?? "").trim();
47
+ const to = String(v.toEntityExternalKey ?? v.toEntityRef ?? v.to ?? "").trim();
48
+ const type = String(v.relationshipTypeCode ?? v.type ?? "").trim();
49
+ const confidence = Number(v.confidence ?? 1);
50
+ if (!from || !to || !type) {
51
+ throw new Error(`Relationship #${index + 1} is missing required fields (from/to/type)`);
52
+ }
53
+ return {
54
+ fromEntityExternalKey: from,
55
+ toEntityExternalKey: to,
56
+ relationshipTypeCode: type,
57
+ confidence: Number.isFinite(confidence) ? confidence : 1,
58
+ };
59
+ });
60
+ }
61
+ const fromKey = parseOptionValue(args, "--from");
62
+ const toKey = parseOptionValue(args, "--to");
63
+ const relType = parseOptionValue(args, "--type");
64
+ if (!fromKey)
65
+ throw new Error("--from <externalKey> is required");
66
+ if (!toKey)
67
+ throw new Error("--to <externalKey> is required");
68
+ if (!relType)
69
+ throw new Error("--type <relationshipTypeCode> is required");
70
+ return [{
71
+ fromEntityExternalKey: fromKey,
72
+ toEntityExternalKey: toKey,
73
+ relationshipTypeCode: relType,
74
+ confidence: 1,
75
+ }];
76
+ }
20
77
  async function tryCreateStubEntity(externalKey, mcpOpts, agentContext, policyContext) {
21
- // Only auto-create global reference entities — company entities must be created explicitly
22
78
  if (!externalKey.startsWith("global:"))
23
79
  return false;
24
- // Derive a normalised alias from the external key (e.g. "global:platform:vercel" → "vercel")
25
80
  const parts = externalKey.split(":");
26
81
  const alias = parts[parts.length - 1];
27
82
  const resolveRaw = await callMcpTool("nexarch_resolve_reference", { names: [alias], companyId: mcpOpts.companyId }, mcpOpts);
@@ -42,21 +97,7 @@ async function tryCreateStubEntity(externalKey, mcpOpts, agentContext, policyCon
42
97
  }
43
98
  export async function addRelationship(args) {
44
99
  const asJson = parseFlag(args, "--json");
45
- const fromKey = parseOptionValue(args, "--from");
46
- const toKey = parseOptionValue(args, "--to");
47
- const relType = parseOptionValue(args, "--type");
48
- if (!fromKey) {
49
- console.error("error: --from <externalKey> is required");
50
- process.exit(1);
51
- }
52
- if (!toKey) {
53
- console.error("error: --to <externalKey> is required");
54
- process.exit(1);
55
- }
56
- if (!relType) {
57
- console.error("error: --type <relationshipTypeCode> is required");
58
- process.exit(1);
59
- }
100
+ const relationships = parseRelationships(args);
60
101
  const creds = requireCredentials();
61
102
  const mcpOpts = { companyId: creds.companyId };
62
103
  const policiesRaw = await callMcpTool("nexarch_get_applied_policies", {}, mcpOpts);
@@ -66,7 +107,7 @@ export async function addRelationship(args) {
66
107
  const agentContext = {
67
108
  agentId: "nexarch-cli:add-relationship",
68
109
  agentRunId: `add-relationship-${Date.now()}`,
69
- repoRef: fromKey,
110
+ repoRef: relationships[0]?.fromEntityExternalKey ?? "batch",
70
111
  observedAt: nowIso,
71
112
  source: "nexarch-cli",
72
113
  model: "n/a",
@@ -75,27 +116,18 @@ export async function addRelationship(args) {
75
116
  const policyContext = policyBundleHash
76
117
  ? { policyBundleHash, alignmentSummary: { score: 1, violations: [], waivers: [] } }
77
118
  : undefined;
78
- const relationship = {
79
- relationshipTypeCode: relType,
80
- fromEntityExternalKey: fromKey,
81
- toEntityExternalKey: toKey,
82
- confidence: 1,
83
- };
84
- let raw = await callMcpTool("nexarch_upsert_relationships", { relationships: [relationship], agentContext, policyContext }, mcpOpts);
119
+ let raw = await callMcpTool("nexarch_upsert_relationships", { relationships, agentContext, policyContext }, mcpOpts);
85
120
  let result = parseToolText(raw);
86
- // Auto-create global reference stubs if endpoints are missing, then retry once
87
121
  const missingEndpoint = result.errors?.some((e) => e.error === "RELATIONSHIP_ENDPOINT_NOT_FOUND");
88
122
  if (missingEndpoint) {
89
- const failedFromKeys = result.errors
90
- ?.filter((e) => e.error === "RELATIONSHIP_ENDPOINT_NOT_FOUND" && e.fromEntityExternalKey)
91
- .map((e) => e.fromEntityExternalKey);
92
- const failedToKeys = result.errors
93
- ?.filter((e) => e.error === "RELATIONSHIP_ENDPOINT_NOT_FOUND" && e.toEntityExternalKey)
94
- .map((e) => e.toEntityExternalKey);
95
- const keysToCreate = [...new Set([...(failedFromKeys ?? []), ...(failedToKeys ?? [])])];
96
- // If no specific endpoint keys returned, fall back to trying both
97
- if (keysToCreate.length === 0) {
98
- keysToCreate.push(fromKey, toKey);
123
+ const keysToCreate = new Set();
124
+ for (const err of result.errors ?? []) {
125
+ if (err.error !== "RELATIONSHIP_ENDPOINT_NOT_FOUND")
126
+ continue;
127
+ if (err.fromEntityExternalKey)
128
+ keysToCreate.add(err.fromEntityExternalKey);
129
+ if (err.toEntityExternalKey)
130
+ keysToCreate.add(err.toEntityExternalKey);
99
131
  }
100
132
  let createdAny = false;
101
133
  for (const key of keysToCreate) {
@@ -104,7 +136,7 @@ export async function addRelationship(args) {
104
136
  createdAny = true;
105
137
  }
106
138
  if (createdAny) {
107
- raw = await callMcpTool("nexarch_upsert_relationships", { relationships: [relationship], agentContext, policyContext }, mcpOpts);
139
+ raw = await callMcpTool("nexarch_upsert_relationships", { relationships, agentContext, policyContext }, mcpOpts);
108
140
  result = parseToolText(raw);
109
141
  }
110
142
  }
@@ -117,12 +149,12 @@ export async function addRelationship(args) {
117
149
  const succeeded = result.summary?.succeeded ?? 0;
118
150
  const failed = result.summary?.failed ?? 0;
119
151
  if (failed > 0) {
120
- console.error(`Failed to add relationship: ${fromKey} -[${relType}]-> ${toKey}`);
152
+ console.error(`Failed to add relationships (${failed}/${relationships.length}).`);
121
153
  const missingFrom = new Set();
122
154
  const missingTo = new Set();
123
155
  for (const err of result.errors ?? []) {
124
- const fromEndpoint = err.fromEntityExternalKey ?? fromKey;
125
- const toEndpoint = err.toEntityExternalKey ?? toKey;
156
+ const fromEndpoint = err.fromEntityExternalKey ?? "(unknown-from)";
157
+ const toEndpoint = err.toEntityExternalKey ?? "(unknown-to)";
126
158
  const message = err.message ?? "(no details returned)";
127
159
  if (err.error === "RELATIONSHIP_ENDPOINT_NOT_FOUND") {
128
160
  if (err.fromEntityExternalKey)
@@ -146,5 +178,11 @@ export async function addRelationship(args) {
146
178
  process.exitCode = 1;
147
179
  return;
148
180
  }
149
- console.log(`Added ${succeeded} relationship: ${fromKey} -[${relType}]-> ${toKey}`);
181
+ if (relationships.length === 1) {
182
+ const r = relationships[0];
183
+ console.log(`Added ${succeeded} relationship: ${r.fromEntityExternalKey} -[${r.relationshipTypeCode}]-> ${r.toEntityExternalKey}`);
184
+ }
185
+ else {
186
+ console.log(`Added ${succeeded}/${relationships.length} relationships.`);
187
+ }
150
188
  }
@@ -453,7 +453,29 @@ function classifySubPackage(pkgPath, relativePath) {
453
453
  return { entityType: "technology_component", subtype: "tech_library" };
454
454
  return { entityType: "technology_component", subtype: "tech_library" };
455
455
  }
456
- // apps/* or src/* → deployable application; check scripts
456
+ // apps/* → deployable application component under the root application.
457
+ if (topDir === "apps") {
458
+ const name = relativePath.toLowerCase();
459
+ if (name.includes("worker"))
460
+ return { entityType: "application_component", subtype: "app_comp_worker" };
461
+ if (name.includes("job"))
462
+ return { entityType: "application_component", subtype: "app_comp_job" };
463
+ if (name.includes("etl"))
464
+ return { entityType: "application_component", subtype: "app_comp_etl" };
465
+ if (name.includes("adapter"))
466
+ return { entityType: "application_component", subtype: "app_comp_adapter" };
467
+ if (name.includes("batch"))
468
+ return { entityType: "application_component", subtype: "app_comp_batch" };
469
+ if (name.includes("pipeline"))
470
+ return { entityType: "application_component", subtype: "app_comp_data_pipeline" };
471
+ if (name.includes("ui") || name.includes("web") || name.includes("front") || name.includes("site") || name.includes("portal") || name.includes("admin")) {
472
+ return { entityType: "application_component", subtype: "app_comp_ui" };
473
+ }
474
+ if (name.includes("api"))
475
+ return { entityType: "application_component", subtype: "app_comp_api" };
476
+ return { entityType: "application_component", subtype: "app_comp_api" };
477
+ }
478
+ // src/* → deployable application; check scripts
457
479
  try {
458
480
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
459
481
  const scripts = Object.keys(pkg.scripts ?? {});
@@ -758,6 +780,9 @@ function scanEcosystems(dir) {
758
780
  }
759
781
  return { names, ecosystems };
760
782
  }
783
+ function isApplicationLikeEntityType(entityType) {
784
+ return entityType === "application" || entityType === "application_component";
785
+ }
761
786
  function scanProject(dir) {
762
787
  const names = new Set();
763
788
  let projectName = basename(dir);
@@ -1299,10 +1324,15 @@ export async function initProject(args) {
1299
1324
  // - Sub-packages that are apps → part_of the top-level project
1300
1325
  const relationships = [];
1301
1326
  const seenRelPairs = new Set();
1327
+ let relationshipAddAttempts = 0;
1328
+ let relationshipSkippedAsDuplicate = 0;
1302
1329
  function addRel(type, from, to, confidence = 0.9, attributes) {
1330
+ relationshipAddAttempts += 1;
1303
1331
  const key = `${type}::${from}::${to}`;
1304
- if (seenRelPairs.has(key))
1332
+ if (seenRelPairs.has(key)) {
1333
+ relationshipSkippedAsDuplicate += 1;
1305
1334
  return;
1335
+ }
1306
1336
  seenRelPairs.add(key);
1307
1337
  relationships.push({ relationshipTypeCode: type, fromEntityExternalKey: from, toEntityExternalKey: to, confidence, attributes });
1308
1338
  }
@@ -1325,7 +1355,7 @@ export async function initProject(args) {
1325
1355
  continue;
1326
1356
  seenSubKeys.add(`rel:${sp.externalKey}`);
1327
1357
  // Apps are part_of the top-level project
1328
- if (sp.entityType === "application") {
1358
+ if (sp.entityType === "application_component") {
1329
1359
  addRel("part_of", sp.externalKey, projectExternalKey);
1330
1360
  }
1331
1361
  // Wire each sub-package's resolved deps to itself
@@ -1355,7 +1385,7 @@ export async function initProject(args) {
1355
1385
  addRel("accountable_for", orgExternalKey, projectExternalKey, 1);
1356
1386
  // Also accountable_for any sub-package applications
1357
1387
  for (const sp of subPackages) {
1358
- if (sp.externalKey && sp.entityType === "application") {
1388
+ if (sp.externalKey && isApplicationLikeEntityType(sp.entityType)) {
1359
1389
  addRel("accountable_for", orgExternalKey, sp.externalKey, 1);
1360
1390
  }
1361
1391
  }
@@ -1428,6 +1458,7 @@ export async function initProject(args) {
1428
1458
  // Build structured enrichment task (included in JSON output and printed in human mode)
1429
1459
  const readmeHints = ["README.md", "README.mdx", "docs/README.md", "docs/index.md"]
1430
1460
  .filter((f) => existsSync(join(dir, f)));
1461
+ const preWiredRelationshipKeys = new Set(relationships.map((rel) => `${rel.relationshipTypeCode}::${rel.fromEntityExternalKey}::${rel.toEntityExternalKey}`));
1431
1462
  function buildEnrichmentInstructions() {
1432
1463
  const ecosystemLabel = detectedEcosystems.length > 0
1433
1464
  ? detectedEcosystems.join(", ")
@@ -1453,10 +1484,14 @@ ${subPackages.map((sp) => ` • ${sp.name} (${sp.relativePath})`).join("\n")
1453
1484
  then choose the correct entity type and subtype before running update-entity:
1454
1485
 
1455
1486
  CLASSIFICATION GUIDE — pick the best fit:
1456
- Deployable web app (has a dev/start/build script, runs in a browser or as a server)
1457
- → --entity-type application --subtype app_web
1458
- Deployable background service, worker, or data pipeline (runs as a process, no UI)
1459
- → --entity-type application --subtype app_custom_built (or app_integration_service for crawlers/ETL)
1487
+ Root project / top-level product application
1488
+ → --entity-type application --subtype app_custom_built
1489
+ Deployable sub-app under apps/* with UI
1490
+ → --entity-type application_component --subtype app_comp_ui
1491
+ Deployable sub-app under apps/* exposing API/backend service
1492
+ → --entity-type application_component --subtype app_comp_api
1493
+ Deployable background service, worker, job, ETL, or data pipeline under apps/*
1494
+ → --entity-type application_component --subtype app_comp_worker / app_comp_job / app_comp_etl / app_comp_data_pipeline
1460
1495
  Shared internal library or package (imported by other packages, not deployed on its own)
1461
1496
  → --entity-type technology_component --subtype tech_library
1462
1497
  Shared UI component library
@@ -1489,7 +1524,7 @@ ${subPackages.map((sp) => ` • ${sp.name} (${sp.relativePath})`).join("\n")
1489
1524
 
1490
1525
  ⚠️ DIRECTION MATTERS — wire relationships as follows:
1491
1526
 
1492
- Deployable sub-apps (apps/*) are PART OF the parent — child points to parent:
1527
+ Deployable sub-apps (apps/*) are PART OF the parent application — child points to parent:
1493
1528
  npx nexarch add-relationship \\
1494
1529
  --from "<sub-app-key>" \\
1495
1530
  --to "${projectExternalKey}" \\
@@ -1504,7 +1539,11 @@ ${subPackages.map((sp) => ` • ${sp.name} (${sp.relativePath})`).join("\n")
1504
1539
  (FROM = ${projectExternalKey}, TO = the library)
1505
1540
 
1506
1541
  ⚠️ WIRE DEPENDENCIES TO THE SUB-APP THAT DECLARES THEM, NOT TO THE PARENT.
1507
- After registering each sub-app/library entity, wire its npm dependencies to IT.
1542
+
1543
+ IMPORTANT: init-project has already written baseline dependency relationships from manifests.
1544
+ Do NOT re-run add-relationship for entries listed as pre-wired below unless one of these is true:
1545
+ • you changed the target entity key (e.g. you replaced a temporary key), or
1546
+ • you are intentionally adding/changing relationship attributes beyond the scanned defaults.
1508
1547
 
1509
1548
  RELATIONSHIP TYPE RULES:
1510
1549
  application → technology_component : --type "depends_on"
@@ -1512,7 +1551,7 @@ ${subPackages.map((sp) => ` • ${sp.name} (${sp.relativePath})`).join("\n")
1512
1551
  any → platform / platform_component : --type "runs_on" (NOT uses or depends_on)
1513
1552
  any → model : --type "uses_model"
1514
1553
 
1515
- Pre-resolved dependencies per sub-package (wire these after registering each entity):
1554
+ Pre-resolved dependencies per sub-package (already wired by init-project; review only):
1516
1555
  ${subPackages.map((sp) => {
1517
1556
  const resolved = sp.depSpecs
1518
1557
  .map((d) => resolvedByInput.get(d.name))
@@ -1521,9 +1560,11 @@ ${subPackages.map((sp) => {
1521
1560
  return ` • ${sp.name}: (no pre-resolved deps — check package.json manually)`;
1522
1561
  const lines = resolved.map((r) => {
1523
1562
  const relType = pickRelationshipType(r.entityTypeCode, sp.entityType);
1524
- return ` --to "${r.canonicalExternalRef}" --type "${relType}" # ${r.canonicalName}`;
1563
+ const key = `${relType}::${sp.externalKey}::${r.canonicalExternalRef}`;
1564
+ const status = preWiredRelationshipKeys.has(key) ? "[pre-wired]" : "[not pre-wired]";
1565
+ return ` ${status} --from "${sp.externalKey}" --to "${r.canonicalExternalRef}" --type "${relType}" # ${r.canonicalName}`;
1525
1566
  });
1526
- return ` • ${sp.name} (--from "${sp.externalKey}"):\n${lines.join("\n")}`;
1567
+ return ` • ${sp.name}:\n${lines.join("\n")}`;
1527
1568
  }).join("\n\n")}
1528
1569
  `;
1529
1570
  const adrStepNumber = subPackages.length > 0 ? "STEP 4" : "STEP 3";
@@ -1650,7 +1691,17 @@ ${subPkgSection}${adrSection}${gapCheckSection}`;
1650
1691
  const resolvedDeps = sp.depSpecs
1651
1692
  .map((d) => resolvedByInput.get(d.name))
1652
1693
  .filter((r) => !!r?.canonicalExternalRef)
1653
- .map((r) => ({ canonicalExternalRef: r.canonicalExternalRef, canonicalName: r.canonicalName, entityTypeCode: r.entityTypeCode }));
1694
+ .map((r) => {
1695
+ const relationshipTypeCode = pickRelationshipType(r.entityTypeCode, sp.entityType);
1696
+ const relationshipKey = `${relationshipTypeCode}::${sp.externalKey}::${r.canonicalExternalRef}`;
1697
+ return {
1698
+ canonicalExternalRef: r.canonicalExternalRef,
1699
+ canonicalName: r.canonicalName,
1700
+ entityTypeCode: r.entityTypeCode,
1701
+ relationshipTypeCode,
1702
+ preWired: preWiredRelationshipKeys.has(relationshipKey),
1703
+ };
1704
+ });
1654
1705
  return {
1655
1706
  name: sp.name,
1656
1707
  relativePath: sp.relativePath,
@@ -1667,6 +1718,12 @@ ${subPkgSection}${adrSection}${gapCheckSection}`;
1667
1718
  project: { name: displayName, externalKey: projectExternalKey, entityType: entityTypeOverride, detectedEcosystems },
1668
1719
  entities: entitiesResult.summary ?? {},
1669
1720
  relationships: relsResult?.summary ?? { requested: 0, succeeded: 0, failed: 0 },
1721
+ metrics: {
1722
+ entitiesSubmitted: entities.length,
1723
+ relationshipsSubmitted: relationships.length,
1724
+ relationshipAddAttempts,
1725
+ relationshipsSkippedAsDuplicate: relationshipSkippedAsDuplicate,
1726
+ },
1670
1727
  resolved: resolvedItems.length,
1671
1728
  unresolved: unresolvedItems.length,
1672
1729
  unresolvedSample: unresolvedItems.slice(0, 10).map((r) => r.input),
@@ -1693,6 +1750,9 @@ ${subPkgSection}${adrSection}${gapCheckSection}`;
1693
1750
  console.log(`\nDone.`);
1694
1751
  console.log(` Entities : ${output.entities.succeeded ?? 0} written, ${output.entities.failed ?? 0} failed`);
1695
1752
  console.log(` Relationships: ${output.relationships.succeeded ?? 0} written`);
1753
+ if (output.metrics.relationshipsSkippedAsDuplicate > 0) {
1754
+ console.log(` Deduped rels : ${output.metrics.relationshipsSkippedAsDuplicate} skipped as duplicates before upsert`);
1755
+ }
1696
1756
  if (unresolvedItems.length > 0) {
1697
1757
  console.log(` Candidates : ${unresolvedItems.length} added to reference candidates`);
1698
1758
  }
@@ -81,26 +81,31 @@ function parseFindings(args) {
81
81
  if (!Array.isArray(parsed) || parsed.length === 0) {
82
82
  throw new Error(findingsFile ? "--findings-file must contain a non-empty JSON array" : "--findings-json must be a non-empty JSON array");
83
83
  }
84
- return parsed.map((item) => {
84
+ return parsed.map((item, index) => {
85
85
  const value = item;
86
- const policyControlId = String(value.policyControlId ?? value.controlId ?? "").trim();
87
- const policyRuleId = String(value.policyRuleId ?? value.ruleId ?? "").trim();
88
- const result = String(value.result ?? "").toLowerCase();
86
+ const policyControlId = String(value.policyControlId ?? value.controlId ?? value.policy_control_id ?? "").trim();
87
+ const policyRuleId = String(value.policyRuleId ?? value.ruleId ?? value.policy_rule_id ?? "").trim();
88
+ const result = String(value.result ?? value.status ?? "").toLowerCase();
89
89
  if (!policyControlId || !policyRuleId || !result) {
90
- throw new Error("Each finding must include policyControlId, policyRuleId, result. Tip: run 'nexarch policy-controls --entity <application:key> --json' and use the rule IDs from controls[].rules[].id");
90
+ throw new Error(`Finding #${index + 1} is missing required fields. Expected keys: policyControlId (or controlId/policy_control_id), policyRuleId (or ruleId/policy_rule_id), and result (or status). Example: [{\"policyControlId\":\"<uuid>\",\"policyRuleId\":\"<uuid>\",\"result\":\"pass\",\"rationale\":\"...\"}]`);
91
91
  }
92
92
  if (result !== "pass" && result !== "partial" && result !== "fail") {
93
- throw new Error(`Invalid finding result '${String(value.result)}'. Use pass|partial|fail.`);
93
+ throw new Error(`Invalid finding result '${String(value.result ?? value.status)}' at finding #${index + 1}. Use pass|partial|fail.`);
94
94
  }
95
95
  const rationale = value.rationale
96
96
  ? String(value.rationale)
97
97
  : [value.summary, value.evidence].filter(Boolean).map(String).join("\n\n");
98
+ const missingRequirements = Array.isArray(value.missingRequirements)
99
+ ? value.missingRequirements.map(String)
100
+ : Array.isArray(value.missing_requirements)
101
+ ? value.missing_requirements.map(String)
102
+ : undefined;
98
103
  return {
99
104
  policyControlId,
100
105
  policyRuleId,
101
106
  result,
102
107
  ...(rationale ? { rationale } : {}),
103
- ...(Array.isArray(value.missingRequirements) ? { missingRequirements: value.missingRequirements.map(String) } : {}),
108
+ ...(missingRequirements ? { missingRequirements } : {}),
104
109
  };
105
110
  });
106
111
  }
@@ -22,48 +22,124 @@ function parseAttributesInput(jsonText, filePath) {
22
22
  if (!jsonText && !filePath)
23
23
  return null;
24
24
  if (jsonText && filePath) {
25
- console.error("error: use only one of --attributes-json or --attributes-file");
26
- process.exit(1);
25
+ throw new Error("Use only one of --attributes-json or --attributes-file");
27
26
  }
28
27
  let raw = jsonText;
29
28
  if (filePath) {
30
29
  if (!existsSync(filePath)) {
31
- console.error(`error: --attributes-file not found: ${filePath}`);
32
- process.exit(1);
30
+ throw new Error(`--attributes-file not found: ${filePath}`);
33
31
  }
34
32
  raw = readFileSync(filePath, "utf8");
35
33
  }
36
34
  try {
37
35
  const parsed = JSON.parse(raw ?? "{}");
38
36
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
39
- console.error("error: attributes input must be a JSON object");
40
- process.exit(1);
37
+ throw new Error("Attributes input must be a JSON object");
41
38
  }
42
39
  return parsed;
43
40
  }
44
41
  catch (error) {
45
- console.error(`error: invalid attributes JSON (${error instanceof Error ? error.message : "parse failed"})`);
46
- process.exit(1);
42
+ throw new Error(`Invalid attributes JSON (${error instanceof Error ? error.message : "parse failed"})`);
47
43
  }
48
44
  }
45
+ function parseEntityBatch(args) {
46
+ const entitiesFile = parseOptionValue(args, "--entities-file");
47
+ const entitiesJson = parseOptionValue(args, "--entities-json");
48
+ if (!entitiesFile && !entitiesJson)
49
+ return null;
50
+ if (entitiesFile && entitiesJson)
51
+ throw new Error("Use only one of --entities-file or --entities-json");
52
+ let raw = entitiesJson;
53
+ if (entitiesFile) {
54
+ if (!existsSync(entitiesFile))
55
+ throw new Error(`--entities-file not found: ${entitiesFile}`);
56
+ raw = readFileSync(entitiesFile, "utf8");
57
+ }
58
+ let parsed;
59
+ try {
60
+ parsed = JSON.parse(raw ?? "[]");
61
+ }
62
+ catch {
63
+ throw new Error(entitiesFile ? "--entities-file must contain valid JSON array" : "--entities-json must be valid JSON array");
64
+ }
65
+ if (!Array.isArray(parsed) || parsed.length === 0) {
66
+ throw new Error("Entities input must be a non-empty JSON array");
67
+ }
68
+ return parsed.map((item, index) => {
69
+ const v = (item ?? {});
70
+ const externalKey = String(v.externalKey ?? v.key ?? "").trim();
71
+ if (!externalKey)
72
+ throw new Error(`Entity #${index + 1} is missing externalKey/key`);
73
+ const entityTypeCode = String(v.entityTypeCode ?? v.entity_type_code ?? "application").trim() || "application";
74
+ const entitySubtypeCode = String(v.entitySubtypeCode ?? v.entity_subtype_code ?? "").trim();
75
+ const out = {
76
+ externalKey,
77
+ entityTypeCode,
78
+ confidence: 1,
79
+ };
80
+ if (typeof v.name === "string" && v.name.trim())
81
+ out.name = v.name.trim();
82
+ if (typeof v.description === "string" && v.description.trim())
83
+ out.description = v.description.trim();
84
+ if (entitySubtypeCode)
85
+ out.entitySubtypeCode = entitySubtypeCode;
86
+ const attrs = {
87
+ ...(v.attributes && typeof v.attributes === "object" && !Array.isArray(v.attributes) ? v.attributes : {}),
88
+ };
89
+ if (typeof v.icon === "string" && v.icon.trim()) {
90
+ attrs.application_icon = { provider: "lucide", name: v.icon.trim() };
91
+ }
92
+ if (Object.keys(attrs).length > 0)
93
+ out.attributes = attrs;
94
+ if (!out.name && !out.description && !out.attributes) {
95
+ throw new Error(`Entity #${index + 1} must include at least one of name, description, icon, or attributes`);
96
+ }
97
+ return out;
98
+ });
99
+ }
49
100
  export async function updateEntity(args) {
50
101
  const asJson = parseFlag(args, "--json");
51
- const externalKey = parseOptionValue(args, "--key");
52
- const name = parseOptionValue(args, "--name");
53
- const description = parseOptionValue(args, "--description");
54
- const entityTypeCode = parseOptionValue(args, "--entity-type") ?? "application";
55
- const entitySubtypeCode = parseOptionValue(args, "--subtype");
56
- const iconName = parseOptionValue(args, "--icon");
57
- const attributesJson = parseOptionValue(args, "--attributes-json");
58
- const attributesFile = parseOptionValue(args, "--attributes-file");
59
- if (!externalKey) {
60
- console.error("error: --key <externalKey> is required");
61
- process.exit(1);
102
+ const batchEntities = parseEntityBatch(args);
103
+ let entities;
104
+ if (batchEntities) {
105
+ entities = batchEntities;
62
106
  }
63
- const explicitAttributes = parseAttributesInput(attributesJson, attributesFile);
64
- if (!name && !description && !iconName && !explicitAttributes) {
65
- console.error("error: provide at least one of --name, --description, --icon, --attributes-json, or --attributes-file");
66
- process.exit(1);
107
+ else {
108
+ const externalKey = parseOptionValue(args, "--key");
109
+ const name = parseOptionValue(args, "--name");
110
+ const description = parseOptionValue(args, "--description");
111
+ const entityTypeCode = parseOptionValue(args, "--entity-type") ?? "application";
112
+ const entitySubtypeCode = parseOptionValue(args, "--subtype");
113
+ const iconName = parseOptionValue(args, "--icon");
114
+ const attributesJson = parseOptionValue(args, "--attributes-json");
115
+ const attributesFile = parseOptionValue(args, "--attributes-file");
116
+ if (!externalKey)
117
+ throw new Error("--key <externalKey> is required (or use --entities-file/--entities-json)");
118
+ const explicitAttributes = parseAttributesInput(attributesJson, attributesFile);
119
+ if (!name && !description && !iconName && !explicitAttributes) {
120
+ throw new Error("Provide at least one of --name, --description, --icon, --attributes-json, or --attributes-file");
121
+ }
122
+ const entity = {
123
+ externalKey,
124
+ entityTypeCode,
125
+ confidence: 1,
126
+ };
127
+ if (name)
128
+ entity.name = name;
129
+ if (description)
130
+ entity.description = description;
131
+ if (entitySubtypeCode)
132
+ entity.entitySubtypeCode = entitySubtypeCode;
133
+ const attributes = {
134
+ ...(explicitAttributes ?? {}),
135
+ };
136
+ if (iconName) {
137
+ attributes.application_icon = { provider: "lucide", name: iconName };
138
+ }
139
+ if (Object.keys(attributes).length > 0) {
140
+ entity.attributes = attributes;
141
+ }
142
+ entities = [entity];
67
143
  }
68
144
  const creds = requireCredentials();
69
145
  const mcpOpts = { companyId: creds.companyId };
@@ -74,7 +150,7 @@ export async function updateEntity(args) {
74
150
  const agentContext = {
75
151
  agentId: "nexarch-cli:update-entity",
76
152
  agentRunId: `update-entity-${Date.now()}`,
77
- repoRef: externalKey,
153
+ repoRef: String(entities[0]?.externalKey ?? "batch"),
78
154
  observedAt: nowIso,
79
155
  source: "nexarch-cli",
80
156
  model: "n/a",
@@ -83,31 +159,7 @@ export async function updateEntity(args) {
83
159
  const policyContext = policyBundleHash
84
160
  ? { policyBundleHash, alignmentSummary: { score: 1, violations: [], waivers: [] } }
85
161
  : undefined;
86
- const entity = {
87
- externalKey,
88
- entityTypeCode,
89
- confidence: 1,
90
- };
91
- if (name)
92
- entity.name = name;
93
- if (description)
94
- entity.description = description;
95
- if (entitySubtypeCode)
96
- entity.entitySubtypeCode = entitySubtypeCode;
97
- const attributes = {
98
- ...(explicitAttributes ?? {}),
99
- };
100
- // Convenience sugar; still generic because user can fully control payload via --attributes-json/file.
101
- if (iconName) {
102
- attributes.application_icon = {
103
- provider: "lucide",
104
- name: iconName,
105
- };
106
- }
107
- if (Object.keys(attributes).length > 0) {
108
- entity.attributes = attributes;
109
- }
110
- const raw = await callMcpTool("nexarch_upsert_entities", { entities: [entity], agentContext, policyContext }, mcpOpts);
162
+ const raw = await callMcpTool("nexarch_upsert_entities", { entities, agentContext, policyContext }, mcpOpts);
111
163
  const result = parseToolText(raw);
112
164
  if (asJson) {
113
165
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
@@ -118,14 +170,17 @@ export async function updateEntity(args) {
118
170
  const succeeded = result.summary?.succeeded ?? 0;
119
171
  const failed = result.summary?.failed ?? 0;
120
172
  if (failed > 0) {
121
- console.error(`Failed to update entity: ${externalKey}`);
173
+ console.error(`Failed to update entities (${failed}/${entities.length}).`);
122
174
  for (const err of result.errors ?? []) {
123
175
  console.error(` ${err.externalKey}: ${err.error} — ${err.message}`);
124
176
  }
125
177
  process.exitCode = 1;
126
178
  return;
127
179
  }
128
- if (!asJson) {
129
- console.log(`Updated ${succeeded} entity: ${externalKey}`);
180
+ if (entities.length === 1) {
181
+ console.log(`Updated ${succeeded} entity: ${String(entities[0].externalKey)}`);
182
+ }
183
+ else {
184
+ console.log(`Updated ${succeeded}/${entities.length} entities.`);
130
185
  }
131
186
  }
package/dist/index.js CHANGED
@@ -99,24 +99,26 @@ Usage:
99
99
  --dry-run preview without writing
100
100
  --json
101
101
  nexarch update-entity
102
- Update the name and/or description of an existing graph entity.
103
- Use this after init-project to enrich the entity with meaningful
104
- content from the project README or docs.
105
- Options: --key <externalKey> (required)
106
- --name <name>
107
- --description <text>
108
- --entity-type <code> (default: application)
109
- --subtype <code>
110
- --icon <lucide-name> (convenience; sets attributes.application_icon)
111
- --attributes-json '<json object>'
112
- --attributes-file <path.json>
113
- --json
102
+ Update existing graph entities (single or batch).
103
+ Single options: --key <externalKey>
104
+ --name <name>
105
+ --description <text>
106
+ --entity-type <code> (default: application)
107
+ --subtype <code>
108
+ --icon <lucide-name> (sets attributes.application_icon)
109
+ --attributes-json '<json object>'
110
+ --attributes-file <path.json>
111
+ Batch options: --entities-json '<json array>'
112
+ --entities-file <path.json>
113
+ --json
114
114
  nexarch add-relationship
115
- Add a relationship between two existing graph entities.
116
- Options: --from <externalKey> (required)
117
- --to <externalKey> (required)
118
- --type <code> (required, e.g. part_of, depends_on)
119
- --json
115
+ Add relationships between existing graph entities (single or batch).
116
+ Single options: --from <externalKey>
117
+ --to <externalKey>
118
+ --type <code> (e.g. part_of, depends_on)
119
+ Batch options: --relationships-json '<json array>'
120
+ --relationships-file <path.json>
121
+ --json
120
122
  nexarch register-alias
121
123
  Register a company-scoped alias for an entity so future
122
124
  scans resolve it instead of logging it as a candidate.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",