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.
- package/dist/commands/add-relationship.js +79 -41
- package/dist/commands/init-project.js +74 -14
- package/dist/commands/policy-audit-submit.js +12 -7
- package/dist/commands/update-entity.js +107 -52
- package/dist/index.js +19 -17
- package/package.json +1 -1
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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
|
|
90
|
-
|
|
91
|
-
.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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
|
|
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 ??
|
|
125
|
-
const toEndpoint = err.toEntityExternalKey ??
|
|
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
|
-
|
|
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/*
|
|
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 === "
|
|
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
|
|
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
|
-
|
|
1457
|
-
→ --entity-type application --subtype
|
|
1458
|
-
Deployable
|
|
1459
|
-
→ --entity-type
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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}
|
|
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) =>
|
|
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(
|
|
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
|
-
...(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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.
|