nexarch 0.6.7 → 0.8.1

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.
@@ -207,6 +207,63 @@ function parseEnvKeys(content) {
207
207
  .filter((key) => /^[A-Z][A-Z0-9_]{2,}$/.test(key))
208
208
  .filter((key) => !ENV_KEY_NOISE.test(key));
209
209
  }
210
+ function inferEnvServiceNamesFromKey(key) {
211
+ const upper = key.toUpperCase();
212
+ const names = new Set();
213
+ if (upper.includes("ADSENSE")) {
214
+ names.add("google-adsense");
215
+ names.add("adsense");
216
+ }
217
+ if (upper.includes("VERCEL"))
218
+ names.add("vercel");
219
+ if (upper.includes("NEON"))
220
+ names.add("neon");
221
+ if (upper.includes("TURBO")) {
222
+ names.add("turbo");
223
+ names.add("turborepo");
224
+ }
225
+ return Array.from(names);
226
+ }
227
+ function inferEnvServiceNamesFromValue(valueRaw) {
228
+ const value = valueRaw.replace(/^['"]|['"]$/g, "").toLowerCase();
229
+ const names = new Set();
230
+ if (!value)
231
+ return [];
232
+ if (value.includes("neon.tech") || value.includes("@neondatabase/"))
233
+ names.add("neon");
234
+ if (value.includes("vercel.app") || value.includes("vercel.com"))
235
+ names.add("vercel");
236
+ if (value.includes("googleads.g.doubleclick.net") || value.includes("adsense")) {
237
+ names.add("google-adsense");
238
+ names.add("adsense");
239
+ }
240
+ return Array.from(names);
241
+ }
242
+ function parseEnvSignals(content) {
243
+ const names = new Set();
244
+ const keys = parseEnvKeys(content);
245
+ for (const key of keys) {
246
+ names.add(key);
247
+ for (const n of inferEnvServiceNamesFromKey(key))
248
+ names.add(n);
249
+ }
250
+ for (const rawLine of content.split("\n")) {
251
+ const line = rawLine.trim();
252
+ if (!line || line.startsWith("#") || !line.includes("="))
253
+ continue;
254
+ const normalized = line.replace(/^export\s+/, "");
255
+ const eqIdx = normalized.indexOf("=");
256
+ if (eqIdx < 1)
257
+ continue;
258
+ const key = normalized.slice(0, eqIdx).trim();
259
+ const value = normalized.slice(eqIdx + 1).trim();
260
+ if (!/^[A-Z][A-Z0-9_]{2,}$/.test(key))
261
+ continue;
262
+ for (const n of inferEnvServiceNamesFromValue(value))
263
+ names.add(n);
264
+ }
265
+ return Array.from(names);
266
+ }
210
267
  function gitOriginUrl(dir) {
211
268
  const configPath = join(dir, ".git", "config");
212
269
  if (!existsSync(configPath))
@@ -237,38 +294,80 @@ function detectFromGitConfig(dir) {
237
294
  return [];
238
295
  }
239
296
  function detectFromFilesystem(dir) {
240
- const detected = [];
241
- // CI/CD
242
- if (existsSync(join(dir, ".github", "workflows")))
243
- detected.push("github-actions");
244
- if (existsSync(join(dir, ".gitlab-ci.yml")))
245
- detected.push("gitlab");
246
- if (existsSync(join(dir, "Jenkinsfile")))
247
- detected.push("jenkins");
248
- if (existsSync(join(dir, ".circleci", "config.yml")))
249
- detected.push("circleci");
250
- // Deployment / hosting
251
- if (existsSync(join(dir, "vercel.json")) || existsSync(join(dir, ".vercel")))
252
- detected.push("vercel");
253
- if (existsSync(join(dir, "netlify.toml")))
254
- detected.push("netlify");
255
- if (existsSync(join(dir, "fly.toml")))
256
- detected.push("fly");
257
- if (existsSync(join(dir, "railway.json")) || existsSync(join(dir, "railway.toml")))
258
- detected.push("railway");
259
- if (existsSync(join(dir, "render.yaml")))
260
- detected.push("render");
261
- // Containers
262
- if (existsSync(join(dir, "Dockerfile")))
263
- detected.push("docker");
264
- if (existsSync(join(dir, "docker-compose.yml")) || existsSync(join(dir, "docker-compose.yaml")))
265
- detected.push("docker");
266
- // Infrastructure
267
- if (existsSync(join(dir, "terraform")) || existsSync(join(dir, "main.tf")))
268
- detected.push("terraform");
269
- if (existsSync(join(dir, "pulumi.yaml")))
270
- detected.push("pulumi");
271
- return detected;
297
+ const detected = new Set();
298
+ const markIfExists = (relPath, name) => {
299
+ if (existsSync(join(dir, relPath)))
300
+ detected.add(name);
301
+ };
302
+ // Root-level checks first (fast path)
303
+ markIfExists(join(".github", "workflows"), "github-actions");
304
+ markIfExists(".gitlab-ci.yml", "gitlab");
305
+ markIfExists("Jenkinsfile", "jenkins");
306
+ markIfExists(join(".circleci", "config.yml"), "circleci");
307
+ markIfExists("vercel.json", "vercel");
308
+ markIfExists(".vercel", "vercel");
309
+ markIfExists("netlify.toml", "netlify");
310
+ markIfExists("fly.toml", "fly");
311
+ markIfExists("railway.json", "railway");
312
+ markIfExists("railway.toml", "railway");
313
+ markIfExists("render.yaml", "render");
314
+ markIfExists("Dockerfile", "docker");
315
+ markIfExists("docker-compose.yml", "docker");
316
+ markIfExists("docker-compose.yaml", "docker");
317
+ markIfExists("terraform", "terraform");
318
+ markIfExists("main.tf", "terraform");
319
+ markIfExists("pulumi.yaml", "pulumi");
320
+ // Monorepo-aware bounded walk for deployment files in sub-app folders.
321
+ const MAX_DEPTH = 3;
322
+ const walk = (current, depth) => {
323
+ if (depth > MAX_DEPTH)
324
+ return;
325
+ let entries = [];
326
+ try {
327
+ entries = readdirSync(current);
328
+ }
329
+ catch {
330
+ return;
331
+ }
332
+ for (const entry of entries) {
333
+ if (entry.startsWith("."))
334
+ continue;
335
+ if (SKIP_DIRS.has(entry))
336
+ continue;
337
+ const full = join(current, entry);
338
+ let isDir = false;
339
+ try {
340
+ isDir = statSync(full).isDirectory();
341
+ }
342
+ catch {
343
+ continue;
344
+ }
345
+ if (!isDir)
346
+ continue;
347
+ if (existsSync(join(full, "vercel.json")) ||
348
+ existsSync(join(full, ".vercel")))
349
+ detected.add("vercel");
350
+ if (existsSync(join(full, "netlify.toml")))
351
+ detected.add("netlify");
352
+ if (existsSync(join(full, "fly.toml")))
353
+ detected.add("fly");
354
+ if (existsSync(join(full, "railway.json")) || existsSync(join(full, "railway.toml")))
355
+ detected.add("railway");
356
+ if (existsSync(join(full, "render.yaml")))
357
+ detected.add("render");
358
+ if (existsSync(join(full, "Dockerfile")))
359
+ detected.add("docker");
360
+ if (existsSync(join(full, "docker-compose.yml")) || existsSync(join(full, "docker-compose.yaml")))
361
+ detected.add("docker");
362
+ if (existsSync(join(full, "main.tf")))
363
+ detected.add("terraform");
364
+ if (existsSync(join(full, "pulumi.yaml")))
365
+ detected.add("pulumi");
366
+ walk(full, depth + 1);
367
+ }
368
+ };
369
+ walk(dir, 1);
370
+ return Array.from(detected);
272
371
  }
273
372
  const SKIP_DIRS = new Set([
274
373
  // JavaScript / Node
@@ -286,14 +385,30 @@ const SKIP_DIRS = new Set([
286
385
  // Generic
287
386
  "build", ".git", ".cache", "tmp",
288
387
  ]);
388
+ const ARCHITECTURAL_DEV_DEP_ALLOWLIST = new Set([
389
+ "tailwindcss",
390
+ "turbo",
391
+ "turborepo",
392
+ "@vercel/analytics",
393
+ "@vercel/speed-insights",
394
+ "@vercel/postgres",
395
+ "@vercel/blob",
396
+ "@neondatabase/serverless",
397
+ ]);
289
398
  function collectPackageDeps(pkgPath) {
290
399
  try {
291
400
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
292
- // devDependencies are build/test/type tooling — no architectural value
293
401
  const merged = {
294
- ...pkg.dependencies,
295
- ...pkg.peerDependencies,
402
+ ...(pkg.dependencies ?? {}),
403
+ ...(pkg.peerDependencies ?? {}),
296
404
  };
405
+ // Include a small set of architecturally significant dev dependencies
406
+ // (hosting/build/runtime-defining tools) while still filtering test-only noise.
407
+ for (const [name, versionRaw] of Object.entries(pkg.devDependencies ?? {})) {
408
+ if (ARCHITECTURAL_DEV_DEP_ALLOWLIST.has(name)) {
409
+ merged[name] = versionRaw;
410
+ }
411
+ }
297
412
  return Object.entries(merged).map(([name, versionRaw]) => ({
298
413
  name,
299
414
  versionRaw: typeof versionRaw === "string" ? versionRaw : null,
@@ -696,12 +811,12 @@ function scanProject(dir) {
696
811
  catch { }
697
812
  }
698
813
  // ── .env files ─────────────────────────────────────────────────────────────
699
- for (const envFile of [".env", ".env.example", ".env.local", ".env.runtime"]) {
814
+ for (const envFile of [".env", ".env.example", ".env.local", ".env.runtime", ".env.production", ".env.development"]) {
700
815
  const envPath = join(dir, envFile);
701
816
  if (existsSync(envPath)) {
702
817
  try {
703
- for (const key of parseEnvKeys(readFileSync(envPath, "utf8")))
704
- names.add(key);
818
+ for (const signal of parseEnvSignals(readFileSync(envPath, "utf8")))
819
+ names.add(signal);
705
820
  }
706
821
  catch { }
707
822
  }
@@ -0,0 +1,46 @@
1
+ import process from "process";
2
+ import { requireCredentials } from "../lib/credentials.js";
3
+ import { callMcpTool } from "../lib/mcp.js";
4
+ function hasFlag(args, flag) {
5
+ return args.includes(flag);
6
+ }
7
+ function option(args, key) {
8
+ const i = args.indexOf(key);
9
+ if (i === -1)
10
+ return null;
11
+ const v = args[i + 1];
12
+ if (!v || v.startsWith("--"))
13
+ return null;
14
+ return v;
15
+ }
16
+ export async function listEntities(args) {
17
+ const asJson = hasFlag(args, "--json");
18
+ const entityTypeCode = option(args, "--type") ?? undefined;
19
+ const status = option(args, "--status") ?? undefined;
20
+ const query = option(args, "--query") ?? undefined;
21
+ const limitRaw = option(args, "--limit");
22
+ const limit = limitRaw ? Number(limitRaw) : undefined;
23
+ if (limitRaw && (!Number.isInteger(limit) || (limit ?? 0) < 1 || (limit ?? 0) > 500)) {
24
+ console.error("error: --limit must be an integer between 1 and 500");
25
+ process.exit(1);
26
+ }
27
+ const creds = requireCredentials();
28
+ const raw = await callMcpTool("nexarch_list_entities", {
29
+ companyId: creds.companyId,
30
+ entityTypeCode,
31
+ status,
32
+ query,
33
+ limit,
34
+ }, { companyId: creds.companyId });
35
+ const result = JSON.parse(raw.content?.[0]?.text ?? "{}");
36
+ if (asJson) {
37
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
38
+ return;
39
+ }
40
+ console.log(`Found ${result.count ?? 0} entities`);
41
+ for (const e of result.entities ?? []) {
42
+ const subtype = e.entitySubtypeCode ? `/${e.entitySubtypeCode}` : "";
43
+ const key = e.externalKey ? ` (${e.externalKey})` : "";
44
+ console.log(`- ${e.name}${key} [${e.entityTypeCode}${subtype}] status=${e.status}`);
45
+ }
46
+ }
@@ -0,0 +1,49 @@
1
+ import process from "process";
2
+ import { requireCredentials } from "../lib/credentials.js";
3
+ import { callMcpTool } from "../lib/mcp.js";
4
+ function hasFlag(args, flag) {
5
+ return args.includes(flag);
6
+ }
7
+ function option(args, key) {
8
+ const i = args.indexOf(key);
9
+ if (i === -1)
10
+ return null;
11
+ const v = args[i + 1];
12
+ if (!v || v.startsWith("--"))
13
+ return null;
14
+ return v;
15
+ }
16
+ export async function listRelationships(args) {
17
+ const asJson = hasFlag(args, "--json");
18
+ const relationshipTypeCode = option(args, "--type") ?? undefined;
19
+ const status = option(args, "--status") ?? undefined;
20
+ const fromEntityExternalKey = option(args, "--from") ?? undefined;
21
+ const toEntityExternalKey = option(args, "--to") ?? undefined;
22
+ const limitRaw = option(args, "--limit");
23
+ const limit = limitRaw ? Number(limitRaw) : undefined;
24
+ if (limitRaw && (!Number.isInteger(limit) || (limit ?? 0) < 1 || (limit ?? 0) > 500)) {
25
+ console.error("error: --limit must be an integer between 1 and 500");
26
+ process.exit(1);
27
+ }
28
+ const creds = requireCredentials();
29
+ const raw = await callMcpTool("nexarch_list_relationships", {
30
+ companyId: creds.companyId,
31
+ relationshipTypeCode,
32
+ status,
33
+ fromEntityExternalKey,
34
+ toEntityExternalKey,
35
+ limit,
36
+ }, { companyId: creds.companyId });
37
+ const result = JSON.parse(raw.content?.[0]?.text ?? "{}");
38
+ if (asJson) {
39
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
40
+ return;
41
+ }
42
+ console.log(`Found ${result.count ?? 0} relationships`);
43
+ for (const r of result.relationships ?? []) {
44
+ const subtype = r.relationshipSubtypeCode ? `/${r.relationshipSubtypeCode}` : "";
45
+ const from = r.fromEntityExternalKey ?? r.fromEntityName ?? "?";
46
+ const to = r.toEntityExternalKey ?? r.toEntityName ?? "?";
47
+ console.log(`- ${from} -[${r.relationshipTypeCode}${subtype}|${r.status}]-> ${to}`);
48
+ }
49
+ }
@@ -83,18 +83,23 @@ function parseFindings(args) {
83
83
  }
84
84
  return parsed.map((item) => {
85
85
  const value = item;
86
+ const policyControlId = String(value.policyControlId ?? value.controlId ?? "").trim();
87
+ const policyRuleId = String(value.policyRuleId ?? value.ruleId ?? "").trim();
86
88
  const result = String(value.result ?? "").toLowerCase();
87
- if (!value.policyControlId || !value.policyRuleId || !result) {
88
- throw new Error("Each finding must include policyControlId, policyRuleId, result");
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");
89
91
  }
90
92
  if (result !== "pass" && result !== "partial" && result !== "fail") {
91
93
  throw new Error(`Invalid finding result '${String(value.result)}'. Use pass|partial|fail.`);
92
94
  }
95
+ const rationale = value.rationale
96
+ ? String(value.rationale)
97
+ : [value.summary, value.evidence].filter(Boolean).map(String).join("\n\n");
93
98
  return {
94
- policyControlId: String(value.policyControlId),
95
- policyRuleId: String(value.policyRuleId),
99
+ policyControlId,
100
+ policyRuleId,
96
101
  result,
97
- ...(value.rationale ? { rationale: String(value.rationale) } : {}),
102
+ ...(rationale ? { rationale } : {}),
98
103
  ...(Array.isArray(value.missingRequirements) ? { missingRequirements: value.missingRequirements.map(String) } : {}),
99
104
  };
100
105
  });
@@ -107,6 +112,27 @@ function parseFindings(args) {
107
112
  }
108
113
  export async function policyAuditSubmit(args) {
109
114
  const asJson = parseFlag(args, "--json");
115
+ if (parseFlag(args, "--help") || parseFlag(args, "-h")) {
116
+ console.log(`
117
+ Usage:
118
+ nexarch policy-audit-submit --command-id <id> --application-key <key> [options]
119
+
120
+ Options:
121
+ --command-id <id> Required command id
122
+ --application-key <key> Required application key (e.g. application:bad-driving)
123
+ --agent-key <key> Optional agent key (defaults from identity)
124
+ --finding <controlId|ruleId|result|rationale|missing1;missing2> Repeatable
125
+ --findings-json <json> JSON array of findings
126
+ --findings-file <path> Path to JSON array of findings
127
+ --json Print JSON response
128
+
129
+ Notes:
130
+ - Findings are rule-level (policyRuleId is required).
131
+ - You can submit partial findings multiple times for the same command.
132
+ - Get valid rule ids with: nexarch policy-controls --entity <application:key> --json
133
+ `);
134
+ return;
135
+ }
110
136
  const commandId = parseOptionValue(args, "--command-id") ?? parseOptionValue(args, "--id");
111
137
  const applicationEntityKey = parseOptionValue(args, "--application-key") ?? parseOptionValue(args, "--entity");
112
138
  const agentKey = parseOptionValue(args, "--agent-key") ?? loadIdentityAgentKey();
@@ -0,0 +1,95 @@
1
+ import process from "process";
2
+ import { writeFileSync } from "fs";
3
+ import { requireCredentials } from "../lib/credentials.js";
4
+ import { callMcpTool } from "../lib/mcp.js";
5
+ function parseFlag(args, flag) {
6
+ return args.includes(flag);
7
+ }
8
+ function parseOptionValue(args, option) {
9
+ const idx = args.indexOf(option);
10
+ if (idx === -1)
11
+ return null;
12
+ const v = args[idx + 1];
13
+ if (!v || v.startsWith("--"))
14
+ return null;
15
+ return v;
16
+ }
17
+ function parseMultiOptionValues(args, option) {
18
+ const values = [];
19
+ for (let i = 0; i < args.length; i += 1) {
20
+ if (args[i] !== option)
21
+ continue;
22
+ const v = args[i + 1];
23
+ if (!v || v.startsWith("--"))
24
+ continue;
25
+ values.push(v);
26
+ }
27
+ return values;
28
+ }
29
+ function parseToolText(result) {
30
+ const text = result.content?.[0]?.text ?? "{}";
31
+ return JSON.parse(text);
32
+ }
33
+ export async function policyAuditTemplate(args) {
34
+ const asJson = parseFlag(args, "--json");
35
+ const entity = parseOptionValue(args, "--entity") ?? parseOptionValue(args, "--application-key");
36
+ const outputPath = parseOptionValue(args, "--out") ?? parseOptionValue(args, "--output");
37
+ const defaultResult = (parseOptionValue(args, "--default-result") ?? "fail").toLowerCase();
38
+ const selectedControlIds = new Set(parseMultiOptionValues(args, "--control-id"));
39
+ if (parseFlag(args, "--help") || parseFlag(args, "-h")) {
40
+ console.log(`
41
+ Usage:
42
+ nexarch policy-audit-template --entity <application:key> [options]
43
+
44
+ Options:
45
+ --entity <key> Required (e.g. application:bad-driving)
46
+ --control-id <uuid> Optional repeatable filter (selected controls only)
47
+ --default-result <value> pass|partial|fail (default: fail)
48
+ --output, --out <path.json> Write findings array to file
49
+ --json Print JSON to stdout
50
+
51
+ Output shape is ready for:
52
+ nexarch policy-audit-submit --findings-file <path.json>
53
+ `);
54
+ return;
55
+ }
56
+ if (!entity) {
57
+ console.error("error: --entity <externalKey> is required (e.g. application:bad-driving)");
58
+ process.exit(1);
59
+ }
60
+ if (!["pass", "partial", "fail"].includes(defaultResult)) {
61
+ console.error("error: --default-result must be pass|partial|fail");
62
+ process.exit(1);
63
+ }
64
+ const creds = requireCredentials();
65
+ const raw = await callMcpTool("nexarch_get_entity_policy_controls", { entityExternalKey: entity, companyId: creds.companyId }, { companyId: creds.companyId });
66
+ const result = parseToolText(raw);
67
+ if (!result.found) {
68
+ console.error(`error: entity not found: ${entity}`);
69
+ process.exit(1);
70
+ }
71
+ const findings = (result.controls ?? [])
72
+ .filter((control) => selectedControlIds.size === 0 || selectedControlIds.has(control.id))
73
+ .flatMap((control) => (control.rules ?? []).map((rule) => ({
74
+ policyControlId: control.id,
75
+ policyRuleId: rule.id,
76
+ result: defaultResult,
77
+ rationale: "",
78
+ missingRequirements: [],
79
+ })));
80
+ if (findings.length === 0) {
81
+ console.error("error: no rules found for selected controls/entity");
82
+ process.exit(1);
83
+ }
84
+ const payload = JSON.stringify(findings, null, 2);
85
+ if (outputPath) {
86
+ writeFileSync(outputPath, `${payload}\n`, "utf8");
87
+ if (!asJson) {
88
+ console.log(`Template written: ${outputPath}`);
89
+ console.log(`Findings: ${findings.length}`);
90
+ }
91
+ }
92
+ if (asJson || !outputPath) {
93
+ process.stdout.write(`${payload}\n`);
94
+ }
95
+ }
@@ -31,7 +31,17 @@ export async function status(_args) {
31
31
  if (registryInfo) {
32
32
  console.log(`\n Integration registry: v${registryInfo.version} (published ${new Date(registryInfo.publishedAt).toLocaleString()})`);
33
33
  }
34
- console.log(`\n Architecture facts: ${governance.canonicalFactCount}`);
34
+ const architectureFacts = governance.architectureFactCount ?? governance.canonicalFactCount;
35
+ console.log(`\n Architecture facts: ${architectureFacts}`);
36
+ if (typeof governance.graphEntityCount === "number" && typeof governance.graphRelationshipCount === "number") {
37
+ console.log(` Graph breakdown: ${governance.graphEntityCount} entities, ${governance.graphRelationshipCount} relationships`);
38
+ }
39
+ if (typeof governance.canonicalCoverageOverallPct === "number") {
40
+ console.log(` Canonical coverage: ${governance.canonicalCoverageOverallPct}%`);
41
+ }
42
+ if (typeof governance.canonicalRegistryCount === "number") {
43
+ console.log(` Canonical registry: ${governance.canonicalRegistryCount}`);
44
+ }
35
45
  console.log(` Pending review: ${governance.reviewQueue.pending_entities} entities, ${governance.reviewQueue.pending_relationships} relationships`);
36
46
  if (governance.latestSnapshot) {
37
47
  const snapshotDate = new Date(governance.latestSnapshot.created_at).toLocaleDateString();
package/dist/index.js CHANGED
@@ -12,11 +12,14 @@ import { updateEntity } from "./commands/update-entity.js";
12
12
  import { addRelationship } from "./commands/add-relationship.js";
13
13
  import { registerAlias } from "./commands/register-alias.js";
14
14
  import { resolveNames } from "./commands/resolve-names.js";
15
+ import { listEntities } from "./commands/list-entities.js";
16
+ import { listRelationships } from "./commands/list-relationships.js";
15
17
  import { checkIn } from "./commands/check-in.js";
16
18
  import { commandDone } from "./commands/command-done.js";
17
19
  import { commandFail } from "./commands/command-fail.js";
18
20
  import { commandClaim } from "./commands/command-claim.js";
19
21
  import { policyControls } from "./commands/policy-controls.js";
22
+ import { policyAuditTemplate } from "./commands/policy-audit-template.js";
20
23
  import { policyAuditSubmit } from "./commands/policy-audit-submit.js";
21
24
  const [, , command, ...args] = process.argv;
22
25
  const commands = {
@@ -33,11 +36,14 @@ const commands = {
33
36
  "add-relationship": addRelationship,
34
37
  "register-alias": registerAlias,
35
38
  "resolve-names": resolveNames,
39
+ "list-entities": listEntities,
40
+ "list-relationships": listRelationships,
36
41
  "check-in": checkIn,
37
42
  "command-done": commandDone,
38
43
  "command-fail": commandFail,
39
44
  "command-claim": commandClaim,
40
45
  "policy-controls": policyControls,
46
+ "policy-audit-template": policyAuditTemplate,
41
47
  "policy-audit-submit": policyAuditSubmit,
42
48
  };
43
49
  async function main() {
@@ -119,6 +125,21 @@ Usage:
119
125
  results before calling add-relationship.
120
126
  Options: --names <csv> (required, e.g. "vercel,neon")
121
127
  --json
128
+ nexarch list-entities
129
+ List entities from the workspace graph.
130
+ Options: --type <entityTypeCode>
131
+ --status <status>
132
+ --query <text>
133
+ --limit <1-500>
134
+ --json
135
+ nexarch list-relationships
136
+ List relationships from the workspace graph.
137
+ Options: --type <relationshipTypeCode>
138
+ --status <status>
139
+ --from <fromExternalKey>
140
+ --to <toExternalKey>
141
+ --limit <1-500>
142
+ --json
122
143
  nexarch check-in Preview pending application-target commands (no auto-claim).
123
144
  Scope is resolved server-side from active company context.
124
145
  Options: --agent-key <key> override stored agent key
@@ -143,6 +164,13 @@ Usage:
143
164
  Fetch policy controls/rules assigned to an entity (for policy audits).
144
165
  Options: --entity <externalKey> (required, e.g. application:bad-driving)
145
166
  --json
167
+ nexarch policy-audit-template
168
+ Generate a findings JSON template from policy controls/rules for an entity.
169
+ Options: --entity <externalKey> (required)
170
+ --control-id <uuid> (repeatable; optional filter)
171
+ --default-result <pass|partial|fail> (default: fail)
172
+ --output <path.json>
173
+ --json
146
174
  nexarch policy-audit-submit
147
175
  Submit structured policy findings (writes policy_audit_finding rows).
148
176
  Options: --command-id <id> (required)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.6.7",
3
+ "version": "0.8.1",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",