nexarch 0.13.2 → 0.13.4

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.
@@ -613,8 +613,9 @@ function injectInitProjectReportingContract(path) {
613
613
  "**One exception: a possible duplicate.** If init-project returns `status: \"review_required\"`",
614
614
  "or prints `STOP — POSSIBLE DUPLICATE`, an application already in the workspace looks like",
615
615
  "one it would register, and it wrote nothing. Tell the human which one you think matches and",
616
- "why, wait for their answer, then re-run init-project with the command for their choice",
617
- "(`decision.commands` in the output). Only then continue with enrichment.",
616
+ "why, wait for their answer, then run the command for their choice yourself",
617
+ "(`decision.commands` in the output) and continue. If it is the same system, enrich the",
618
+ "existing application — nothing needs merging.",
618
619
  "",
619
620
  "Stop early only when a step genuinely cannot run — missing cloud credentials, a root",
620
621
  "module that has not been initialised, or an environment you should not touch without",
@@ -1315,7 +1315,8 @@ export function enrichmentStatusFor(params) {
1315
1315
  }
1316
1316
  export const REVIEW_REQUIRED_NOTE = "STOP. Nothing was written. Existing applications in this workspace resemble what this scan would register"
1317
1317
  + " (see similarApplications). Tell the human, say which one you think is the same system and why, and wait for"
1318
- + " their answer. Then re-run init-project with the command in `decision.commands` that matches their choice.";
1318
+ + " their answer. Then run the command in `decision.commands` for their choice yourself and carry on. If it is the"
1319
+ + " same system, enrich the existing application: nothing needs merging.";
1319
1320
  /**
1320
1321
  * The possible-duplicate warning for the text directive, first thing in it.
1321
1322
  *
@@ -1338,6 +1339,7 @@ export function possibleDuplicateDirective(matches, isMonorepo) {
1338
1339
  ? " same system npx nexarch@latest init-project --dir . --map-deployable <deployable>=<entityRef>"
1339
1340
  : " same application npx nexarch@latest init-project --dir . --application-ref <entityRef>",
1340
1341
  " all different npx nexarch@latest init-project --dir . --create-application",
1342
+ "Run the command for their answer yourself. If it is the same system, enrich the existing application — nothing needs merging.",
1341
1343
  ];
1342
1344
  }
1343
1345
  /**
@@ -1684,6 +1686,11 @@ export async function initProject(args) {
1684
1686
  let stopForReview = null;
1685
1687
  // Names of applications already in the workspace, by ref, for mapped deployables.
1686
1688
  const existingApplicationNames = new Map();
1689
+ // Refs of those that are still proposals: a mapped deployable's leftover
1690
+ // duplicate from an earlier scan is folded away only if nobody reviewed it.
1691
+ const proposedApplicationRefs = new Set();
1692
+ // What happened to each leftover duplicate, for the output.
1693
+ const foldedDuplicates = [];
1687
1694
  if (projectConstruct && isMonorepo && (applicationRefOverride || forceCreateApplication) && !asJson) {
1688
1695
  console.log("\nNote: monorepo detected — no root application is created (ADR 8a); --application-ref/--create-application apply only to single-package repositories.");
1689
1696
  }
@@ -1780,8 +1787,11 @@ export async function initProject(args) {
1780
1787
  const listed = parseToolText(appsRaw).entities ?? [];
1781
1788
  for (const entity of listed) {
1782
1789
  const ref = entity.entityRef ?? entity.externalKey;
1783
- if (ref)
1784
- existingApplicationNames.set(ref, entity.name);
1790
+ if (!ref)
1791
+ continue;
1792
+ existingApplicationNames.set(ref, entity.name);
1793
+ if (String(entity.attributes?.workflow_state ?? "").toLowerCase() === "proposed")
1794
+ proposedApplicationRefs.add(ref);
1785
1795
  }
1786
1796
  // A mapped deployable writes no stub of its own, so a mapping to an
1787
1797
  // application that is not there would leave its relationships pointing at
@@ -2172,6 +2182,42 @@ export async function initProject(args) {
2172
2182
  // ability to resolve via tier 2; the scan itself already succeeded.
2173
2183
  }
2174
2184
  }
2185
+ // "There should be nothing to merge" (Paul, 16 September). When the human has
2186
+ // confirmed a deployable is an existing application, an earlier scan may
2187
+ // already have registered that deployable as a proposal of its own -- the
2188
+ // 0.13.1 behaviour, or any run before the duplicate check stopped the scan.
2189
+ // Fold it into the existing application here, so the agent carries on
2190
+ // enriching the real one and nobody has to press Merge. Only unreviewed
2191
+ // proposals: the gateway refuses anything a human has activated.
2192
+ for (const sp of subPackages) {
2193
+ if (sp.mappedBy !== "flag" || !sp.mappedFrom || !proposedApplicationRefs.has(sp.mappedFrom))
2194
+ continue;
2195
+ try {
2196
+ const raw = await callMcpTool("nexarch_merge_proposed_application", {
2197
+ proposedApplicationRef: sp.mappedFrom,
2198
+ intoApplicationRef: sp.externalKey,
2199
+ reason: `The human confirmed the ${sp.relativePath} deployable is ${existingApplicationNames.get(sp.externalKey) ?? sp.externalKey}.`,
2200
+ companyId: creds.companyId,
2201
+ }, { companyId: creds.companyId });
2202
+ const result = parseToolText(raw);
2203
+ foldedDuplicates.push({
2204
+ proposedApplicationRef: sp.mappedFrom,
2205
+ intoApplicationRef: sp.externalKey,
2206
+ folded: true,
2207
+ detail: `folded in; ${result.relationshipsRepointed ?? 0} relationship(s) moved to ${sp.externalKey}`,
2208
+ });
2209
+ }
2210
+ catch (error) {
2211
+ // An older gateway without the tool, or a credential without the scope.
2212
+ // Say so plainly: the duplicate is still there and the human should know.
2213
+ foldedDuplicates.push({
2214
+ proposedApplicationRef: sp.mappedFrom,
2215
+ intoApplicationRef: sp.externalKey,
2216
+ folded: false,
2217
+ detail: error instanceof Error ? error.message : String(error),
2218
+ });
2219
+ }
2220
+ }
2175
2221
  // Remember an explicit --map-deployable, so the next scan does not ask again.
2176
2222
  // A merge registers the same alias; this is the same decision made up front.
2177
2223
  for (const sp of subPackages) {
@@ -2249,8 +2295,7 @@ export async function initProject(args) {
2249
2295
  if (similarApplications.length > 0 && isMonorepo) {
2250
2296
  // --application-ref does not apply to a monorepo (the CLI says so above),
2251
2297
  // so the single-repo remedy would hand the agent a command that does
2252
- // nothing. The resolution here is a merge, which a human does on the
2253
- // application page.
2298
+ // nothing. The resolution is --map-deployable, which the agent runs.
2254
2299
  pendingSteps.push({
2255
2300
  step: stepNum++,
2256
2301
  action: "review_similar_applications",
@@ -2258,8 +2303,9 @@ export async function initProject(args) {
2258
2303
  + ` workspace resemble${similarApplications.length === 1 ? "s" : ""} this repository: ${matchList}.`
2259
2304
  + " One of the deployables just registered may be the same system — for example a web app registered"
2260
2305
  + " earlier from its public website. Tell the human which deployable you think matches and why, before"
2261
- + " enriching anything. The new deployables are PROPOSED, so nothing is broken yet; if one is a"
2262
- + " duplicate, the human merges the two with \"Merge with…\" on the application's page.",
2306
+ + " enriching anything. If the human confirms one is the same system, re-run with"
2307
+ + " --map-deployable <deployable>=<entityRef> yourself and enrich the existing application: the scan"
2308
+ + " folds this proposal into it, so there is nothing for anyone to merge.",
2263
2309
  notes: [
2264
2310
  "Do not report the registration as complete without mentioning these.",
2265
2311
  "The match is on the repository, not a specific deployable: say which one you believe it is, and why.",
@@ -2297,7 +2343,7 @@ export async function initProject(args) {
2297
2343
  pendingSteps.push({
2298
2344
  step: stepNum++,
2299
2345
  action: "enrich_entity",
2300
- instruction: `Enrich the application entity with a meaningful name, description, subtype, and icon.`,
2346
+ instruction: `Enrich the application entity with a meaningful name, description, subtype, and icon. Find a real icon name with \`nexarch search-icons "<what the application is>"\`; a guessed name that doesn't exist is dropped without an error.`,
2301
2347
  command: `nexarch update-entity --key "${projectExternalKey}" --entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? ' --subtype "<subtype>" --icon "<lucide-icon>"' : ""} --name "..." --description "..."`,
2302
2348
  ...(entityTypeOverride === "application"
2303
2349
  ? { notes: [APPLICATION_SUBTYPE_HINT, "Choose app_custom_built as the default if none of the others clearly apply."] }
@@ -2338,9 +2384,11 @@ export async function initProject(args) {
2338
2384
  instruction: `Review the codebase to identify discrete application functions (what the application does). ` +
2339
2385
  `Examine named modules, route layout, service boundaries, and any architecture documentation. ` +
2340
2386
  `Register functions as application_function entities with subtype core_function (primary business function), supporting_function (auxiliary/enablement), integration_function (external connectivity), or data_function (data processing). ` +
2341
- `Only register functions clearly evidenced by the codebase — do not invent them. ${DESCRIPTION_GUIDANCE}`,
2387
+ `Only register functions clearly evidenced by the codebase — do not invent them. ${DESCRIPTION_GUIDANCE} ` +
2388
+ `Give each function an icon for what it does: find a real name with \`nexarch search-icons "<what it does>"\` (or nexarch_search_icons) and pass it as --icon. Don't guess a name; one that doesn't exist is dropped without an error. If two searches find nothing fitting, leave the icon off.`,
2342
2389
  commandTemplates: {
2343
- updateEntity: `nexarch update-entity --key "application_function:${projectSlug}_<function_slug>" --entity-type application_function --subtype core_function --name "..." --description "..."`,
2390
+ searchIcons: `nexarch search-icons "<what the function does>"`,
2391
+ updateEntity: `nexarch update-entity --key "application_function:${projectSlug}_<function_slug>" --entity-type application_function --subtype core_function --icon "<name from search-icons>" --name "..." --description "..."`,
2344
2392
  addRelationship: `nexarch add-relationship --from "application_function:${projectSlug}_<function_slug>" --to ${projectConstruct && isMonorepo ? '"<owning application key from classifyPackages>"' : `"${projectExternalKey}"`} --type part_of`,
2345
2393
  note: "Functions are structural decomposition only: do not add runtime dependencies from an application_function. Model independently deployable services as applications or application components, then relate those boundaries with depends_on or integrates_with.",
2346
2394
  },
@@ -2359,7 +2407,7 @@ export async function initProject(args) {
2359
2407
  pendingSteps.push({
2360
2408
  step: stepNum++,
2361
2409
  action: "register_decision_records",
2362
- instruction: `Look for ADRs (docs/adr/, decisions/, ADR-*.md) and register each as a decision_record entity.`,
2410
+ instruction: `Look for ADRs (docs/adr/, decisions/, ADR-*.md) and register the architectural ones as decision_record entities: a decision that crosses a boundary between applications, services or teams; changes an integration, data ownership, security or deployment; adopts, replaces or retires a technology others depend on; or is expensive to reverse. Skip design decisions inside one application, even when they are written up as ADRs.`,
2363
2411
  commandTemplates: {
2364
2412
  updateEntity: `nexarch update-entity --key "decision_record:${projectSlug}_<adr_slug>" --entity-type decision_record --subtype decision_architecture --name "..." --attributes-json '{"decision":{"summary":"...","detail":"..."}}'`,
2365
2413
  addRelationship: `nexarch add-relationship --from "decision_record:${projectSlug}_<adr_slug>" --to ${projectConstruct && isMonorepo ? '"<the relevant application key>"' : `"${projectExternalKey}"`} --type decides`,
@@ -2473,7 +2521,7 @@ export async function initProject(args) {
2473
2521
  unresolvedNames: unresolvedItems.map((r) => r.input),
2474
2522
  iconHints: {
2475
2523
  provider: "lucide",
2476
- note: "Use any Lucide icon name (kebab-case); omit when confidence is low.",
2524
+ note: "Use a name returned by `nexarch search-icons` (or nexarch_search_icons); a guessed name that doesn't exist is dropped without an error. Omit when nothing fits.",
2477
2525
  },
2478
2526
  };
2479
2527
  }
@@ -2532,7 +2580,8 @@ export async function initProject(args) {
2532
2580
  // "Confirm type, then update" is right for a new skeleton and wrong
2533
2581
  // here: this is an application a human already registered and named.
2534
2582
  lines.push(` MAPPED : this deployable is the existing application ${sp.externalKey}${sp.mappedBy === "alias" ? " (from an earlier merge)" : ""}.`);
2535
- lines.push(" Add what the code shows; do not rename it or replace its description.");
2583
+ lines.push(" It is this deployable's application: enrich it — name, description, type and");
2584
+ lines.push(" functions — from the code, as you would a new one.");
2536
2585
  }
2537
2586
  lines.push(` inferred : ${sp.entityType} / ${sp.subtype} (confidence ${conf.toFixed(2)})`);
2538
2587
  const unresolvedDeps = sp.depSpecs.map((d) => d.name).filter((d) => !resolvedByInput.has(d));
@@ -2596,15 +2645,19 @@ export async function initProject(args) {
2596
2645
  lines.push(` Use subtype core_function (primary business function), supporting_function (auxiliary/enablement),`);
2597
2646
  lines.push(` integration_function (external connectivity), or data_function (data processing).`);
2598
2647
  lines.push(` Only register functions clearly evidenced by the codebase — do not invent them. ${DESCRIPTION_GUIDANCE}`);
2648
+ lines.push(` Give each function an icon for what it does. Find a real name first; a guessed one is dropped without an error:`);
2649
+ lines.push(` nexarch search-icons "<what the function does>"`);
2599
2650
  lines.push(` For each one found:`);
2600
2651
  const fnOwnerTarget = projectConstruct && isMonorepo ? '"<owning application key from CLASSIFY_THESE>"' : `"${projectExternalKey}"`;
2601
- lines.push(` nexarch update-entity --key "application_function:${projectExternalKey.split(":")[1] ?? "project"}_<function_slug>" --entity-type application_function --subtype core_function --name "..." --description "..."`);
2652
+ lines.push(` nexarch update-entity --key "application_function:${projectExternalKey.split(":")[1] ?? "project"}_<function_slug>" --entity-type application_function --subtype core_function --icon "<name from search-icons>" --name "..." --description "..."`);
2602
2653
  lines.push(` nexarch add-relationship --from "application_function:${projectExternalKey.split(":")[1] ?? "project"}_<function_slug>" --to ${fnOwnerTarget} --type part_of`);
2603
2654
  lines.push(" Functions are structural only: model runtime/service dependencies between application or application_component boundaries, never from an application_function.");
2604
2655
  }
2605
2656
  lines.push(` ${step++}. Scan the READMEs for platforms/SaaS not auto-detected (Vercel, Neon, Stripe, etc.).`);
2606
2657
  lines.push(` For each found: nexarch resolve-names --names "..." --json → nexarch update-entity → nexarch add-relationship`);
2607
- lines.push(` ${step++}. Look for ADRs (docs/adr/, decisions/, ADR-*.md) and register decision_record entities.`);
2658
+ lines.push(` ${step++}. Look for ADRs (docs/adr/, decisions/, ADR-*.md) and register the architectural ones as decision_record entities.`);
2659
+ lines.push(` Architectural means it crosses a boundary between applications, services or teams; changes an integration, data ownership, security or deployment;`);
2660
+ lines.push(` adopts or retires a technology others depend on; or is expensive to reverse. Skip design decisions inside one application.`);
2608
2661
  lines.push(` nexarch update-entity --key "decision_record:${projectExternalKey.split(":")[1] ?? "project"}_<adr_slug>" --entity-type decision_record --subtype decision_architecture --name "..." --attributes-json '{"decision":{"summary":"...","detail":"..."}}'`);
2609
2662
  lines.push(` nexarch add-relationship --from "decision_record:..." --to ${projectConstruct && isMonorepo ? '"<the relevant application key>"' : `"${projectExternalKey}"`} --type decides`);
2610
2663
  lines.push("");
@@ -2666,6 +2719,7 @@ export async function initProject(args) {
2666
2719
  resolved: resolvedItems.length,
2667
2720
  unresolved: unresolvedItems.length,
2668
2721
  similarApplications,
2722
+ ...(foldedDuplicates.length > 0 ? { foldedDuplicates } : {}),
2669
2723
  entityErrors: entitiesResult.errors ?? [],
2670
2724
  relationshipErrors: relsResult?.errors ?? [],
2671
2725
  enrichmentRequired,
@@ -0,0 +1,64 @@
1
+ import process from "process";
2
+ import { requireCredentials } from "../lib/credentials.js";
3
+ import { callMcpTool } from "../lib/mcp.js";
4
+ function parseOptionValue(args, option) {
5
+ const idx = args.indexOf(option);
6
+ if (idx === -1)
7
+ return null;
8
+ const value = args[idx + 1];
9
+ if (!value || value.startsWith("--"))
10
+ return null;
11
+ return value;
12
+ }
13
+ /**
14
+ * The words to search for: everything that isn't an option or an option's
15
+ * value, so both `search-icons database` and `search-icons "event stream"` work.
16
+ */
17
+ export function searchIconsQuery(args) {
18
+ const valued = new Set(["--limit"]);
19
+ const words = [];
20
+ for (let i = 0; i < args.length; i++) {
21
+ if (valued.has(args[i])) {
22
+ i++;
23
+ continue;
24
+ }
25
+ if (args[i].startsWith("--"))
26
+ continue;
27
+ words.push(args[i]);
28
+ }
29
+ return words.join(" ").trim();
30
+ }
31
+ /**
32
+ * Finds real icon names for `update-entity --icon`. The gateway silently drops
33
+ * an icon name that doesn't exist, so an agent guessing from memory ends up
34
+ * with no icon and no error; this searches the same validated set it checks.
35
+ */
36
+ export async function searchIcons(args) {
37
+ const asJson = args.includes("--json");
38
+ const query = searchIconsQuery(args);
39
+ if (!query) {
40
+ console.error('error: say what the icon should show, e.g. nexarch search-icons "credit card"');
41
+ process.exit(1);
42
+ }
43
+ const limitRaw = parseOptionValue(args, "--limit");
44
+ const limit = limitRaw ? Number(limitRaw) : undefined;
45
+ if (limitRaw && (!Number.isInteger(limit) || limit < 1)) {
46
+ console.error("error: --limit must be a whole number of 1 or more");
47
+ process.exit(1);
48
+ }
49
+ const creds = requireCredentials();
50
+ const raw = await callMcpTool("nexarch_search_icons", { query, ...(limit ? { limit } : {}) }, { companyId: creds.companyId });
51
+ const result = JSON.parse(raw.content?.[0]?.text ?? "{}");
52
+ if (asJson) {
53
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
54
+ return;
55
+ }
56
+ if (!result.matches?.length) {
57
+ console.log(`No icons match "${query}".`);
58
+ console.log("Icon names are literal objects and actions (database, file, box). Try one more concrete word; if that also finds nothing, leave the icon unset.");
59
+ return;
60
+ }
61
+ for (const name of result.matches)
62
+ console.log(name);
63
+ console.log(`\n${result.matches.length} of ${result.totalMatches} match${result.totalMatches === 1 ? "" : "es"}. Use one with: nexarch update-entity --key "<key>" --icon "${result.matches[0]}"`);
64
+ }
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import { updateEntity } from "./commands/update-entity.js";
13
13
  import { addRelationship } from "./commands/add-relationship.js";
14
14
  import { registerAlias } from "./commands/register-alias.js";
15
15
  import { resolveNames } from "./commands/resolve-names.js";
16
+ import { searchIcons } from "./commands/search-icons.js";
16
17
  import { feedback } from "./commands/feedback.js";
17
18
  import { listEntities } from "./commands/list-entities.js";
18
19
  import { listRelationships } from "./commands/list-relationships.js";
@@ -57,6 +58,7 @@ const commands = {
57
58
  "add-relationship": addRelationship,
58
59
  "register-alias": registerAlias,
59
60
  "resolve-names": resolveNames,
61
+ "search-icons": searchIcons,
60
62
  feedback,
61
63
  "list-entities": listEntities,
62
64
  "list-relationships": listRelationships,
@@ -232,6 +234,12 @@ Usage:
232
234
  results before calling add-relationship.
233
235
  Options: --names <csv> (required, e.g. "vercel,neon")
234
236
  --json
237
+ nexarch search-icons <words>
238
+ Find real icon names for update-entity --icon. An icon
239
+ name that doesn't exist is dropped without an error, so
240
+ look one up rather than guessing.
241
+ Options: --limit <n> (default 20, max 50)
242
+ --json
235
243
  nexarch feedback
236
244
  Report a Nexarch usability problem or tool error.
237
245
  Options: --kind <usability|error> (required)
@@ -240,7 +240,7 @@ evidence — never invent a compliance status that isn't stored.
240
240
  `;
241
241
  const DECISION_RECORDS_SKILL_BODY = `---
242
242
  name: nexarch-decision-records
243
- description: Record an architectural decision in Nexarch, or check what's already been decided about something. Use when a decision is being made (a technology, pattern, or approach chosen over alternatives), when ADR/RFC documents are found in a repository, when asked what's already been decided about a topic, or when a new decision replaces an older one. Requires the Nexarch MCP tools (nexarch_*).
243
+ description: Record an architectural decision in Nexarch, or check what's already been decided about something. Use when a decision crosses a boundary between applications, services or teams, changes an integration, data ownership, security or deployment, adopts a technology others depend on, or is expensive to reverse; when ADR/RFC documents are found in a repository; when asked what's already been decided about a topic; or when a new decision replaces an older one. Not for design decisions inside one application. Requires the Nexarch MCP tools (nexarch_*).
244
244
  ---
245
245
 
246
246
  # Nexarch Decision Records
@@ -252,6 +252,23 @@ skill is for recording one as it happens — mining a whole repository's ADR
252
252
  history systematically is a separate, human-triggered Decision Review
253
253
  command from the application's page in the workspace.
254
254
 
255
+ ## Architectural, not design
256
+
257
+ Record only architectural decisions. A decision is architectural when it does
258
+ at least one of these:
259
+
260
+ - crosses a boundary between applications, services or teams
261
+ - changes an integration, data ownership, security or authentication, or how
262
+ the system is deployed
263
+ - adopts, replaces or retires a technology that other systems or teams depend on
264
+ - is expensive to reverse once other work builds on it
265
+
266
+ Anything else is a design decision — screen layouts, interaction patterns,
267
+ component library choices, internal module structure, naming. It belongs in
268
+ the repository's design documentation, not in the graph. When unsure, ask:
269
+ if this decision changed, would another team or system have to change too? If
270
+ not, do not record it, even when it is written up as an ADR.
271
+
255
272
  ## Before recording
256
273
 
257
274
  \`nexarch_resolve_reference\` / \`nexarch_list_entities\` for an existing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.13.2",
3
+ "version": "0.13.4",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",
@@ -26,7 +26,7 @@
26
26
  "prepublishOnly": "tsc",
27
27
  "dev": "tsx src/index.ts",
28
28
  "typecheck": "tsc --noEmit",
29
- "test": "tsx scripts/test-mcp-proxy-response.ts && tsx scripts/test-trust-reattest.ts && tsx scripts/test-similar-applications.ts && tsx scripts/test-iac-tool.ts && tsx scripts/test-ansible.ts && tsx scripts/test-reference-sightings.ts"
29
+ "test": "tsx scripts/test-mcp-proxy-response.ts && tsx scripts/test-trust-reattest.ts && tsx scripts/test-similar-applications.ts && tsx scripts/test-iac-tool.ts && tsx scripts/test-ansible.ts && tsx scripts/test-reference-sightings.ts && tsx scripts/test-search-icons.ts"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^22",