nexarch 0.13.1 → 0.13.2

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.
@@ -610,6 +610,12 @@ function injectInitProjectReportingContract(path) {
610
610
  "unit of estate — a Terraform or OpenTofu root module, each with its own state.",
611
611
  "Until they run, the graph knows the repository exists but nothing it provisions.",
612
612
  "",
613
+ "**One exception: a possible duplicate.** If init-project returns `status: \"review_required\"`",
614
+ "or prints `STOP — POSSIBLE DUPLICATE`, an application already in the workspace looks like",
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.",
618
+ "",
613
619
  "Stop early only when a step genuinely cannot run — missing cloud credentials, a root",
614
620
  "module that has not been initialised, or an environment you should not touch without",
615
621
  "asking. When that happens, say which step stopped you and why, then ask. Do not",
@@ -1238,6 +1238,108 @@ export function scoreApplicationCandidate(app, projectName, repoUrl, homepageUrl
1238
1238
  return null;
1239
1239
  return { entityRef, name: app.name, score: Math.min(1, score), reasons };
1240
1240
  }
1241
+ /** Every `--map-deployable` value, in order. The flag may be repeated. */
1242
+ export function parseDeployableMappings(args) {
1243
+ const mappings = [];
1244
+ for (let i = 0; i < args.length; i += 1) {
1245
+ if (args[i] !== "--map-deployable")
1246
+ continue;
1247
+ const value = args[i + 1] ?? "";
1248
+ const eq = value.indexOf("=");
1249
+ mappings.push(eq > 0 ? { deployable: value.slice(0, eq).trim(), entityRef: value.slice(eq + 1).trim() } : { deployable: value, entityRef: "" });
1250
+ }
1251
+ return mappings;
1252
+ }
1253
+ /**
1254
+ * Points deployables at applications that already exist.
1255
+ *
1256
+ * A deployable is named by its directory (`web`) or its path (`apps/web`). The
1257
+ * errors are returned rather than thrown so the caller can refuse before
1258
+ * writing anything, naming every problem at once.
1259
+ */
1260
+ export function applyDeployableMappings(subPackages, mappings) {
1261
+ const errors = [];
1262
+ for (const mapping of mappings) {
1263
+ if (!mapping.deployable || !mapping.entityRef.startsWith("application:")) {
1264
+ errors.push(`--map-deployable expects <deployable>=application:<ref>, got "${mapping.deployable}${mapping.entityRef ? `=${mapping.entityRef}` : ""}".`);
1265
+ continue;
1266
+ }
1267
+ const target = subPackages.find((sp) => isApplicationLikeEntityType(sp.entityType)
1268
+ && (sp.relativePath === mapping.deployable || basename(sp.relativePath) === mapping.deployable));
1269
+ if (!target) {
1270
+ const known = subPackages.filter((sp) => isApplicationLikeEntityType(sp.entityType)).map((sp) => sp.relativePath);
1271
+ errors.push(`--map-deployable: no deployable called "${mapping.deployable}". This repository has: ${known.join(", ") || "none"}.`);
1272
+ continue;
1273
+ }
1274
+ target.mappedFrom = target.mappedFrom ?? target.externalKey;
1275
+ target.externalKey = mapping.entityRef;
1276
+ target.mappedBy = "flag";
1277
+ }
1278
+ return errors;
1279
+ }
1280
+ /**
1281
+ * Whether to register, or stop and ask first.
1282
+ *
1283
+ * Paul's decision (16 September): when an application already in the workspace
1284
+ * resembles what a scan is about to register, the scan writes NOTHING and asks.
1285
+ * ADR 8d had made the default "create a proposed application and flag it", and
1286
+ * even once agents were told to stop, the proposed duplicate already existed.
1287
+ *
1288
+ * Only a NEW application can be a duplicate. On a re-run everything already
1289
+ * registered was decided the first time, so asking again every run would be
1290
+ * noise -- and the matches are dropped rather than reported, for the same
1291
+ * reason. `--create-application` is the human having decided.
1292
+ */
1293
+ export function registrationGate(params) {
1294
+ if (params.createAnyway || params.newApplicationRefs.length === 0)
1295
+ return { stop: false, similar: [] };
1296
+ return { stop: params.similar.length > 0, similar: params.similar };
1297
+ }
1298
+ /**
1299
+ * The registration status an agent acts on.
1300
+ *
1301
+ * A possible duplicate outranks everything. In 0.13.1 the output said "Do not
1302
+ * pause or ask for confirmation between steps" in the same object whose first
1303
+ * step said "tell the human before enriching anything", and CLAUDE.md's
1304
+ * completion contract says stopping early is not a finished task. Told twice to
1305
+ * keep going and once to stop, an agent kept going and enriched the duplicate
1306
+ * -- on Paul's 16 September re-run, which the similarity check had correctly
1307
+ * flagged.
1308
+ */
1309
+ export function enrichmentStatusFor(params) {
1310
+ if (params.similarApplicationCount > 0)
1311
+ return "review_required";
1312
+ if (params.refreshMode && params.nothingWritten && params.preservedEntityCount > 0)
1313
+ return "review_enrichment";
1314
+ return "enrichment_required";
1315
+ }
1316
+ export const REVIEW_REQUIRED_NOTE = "STOP. Nothing was written. Existing applications in this workspace resemble what this scan would register"
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.";
1319
+ /**
1320
+ * The possible-duplicate warning for the text directive, first thing in it.
1321
+ *
1322
+ * The directive goes to stderr precisely so an agent sees it without parsing
1323
+ * JSON, and it used to carry no duplicate warning at all -- the only one was a
1324
+ * JSON field under a note saying not to pause.
1325
+ */
1326
+ export function possibleDuplicateDirective(matches, isMonorepo) {
1327
+ if (matches.length === 0)
1328
+ return [];
1329
+ return [
1330
+ "",
1331
+ "STOP — POSSIBLE DUPLICATE. Nothing was written.",
1332
+ "Existing applications in this workspace resemble what this scan would register:",
1333
+ ...matches.map((m) => ` ${m.name} (${m.entityRef}) score ${m.score.toFixed(2)} [${m.reasons.join(", ")}]`),
1334
+ isMonorepo
1335
+ ? "Tell the human which deployable you think is the same system, and why. Then re-run with their choice:"
1336
+ : "Tell the human which one you think is the same application, and why. Then re-run with their choice:",
1337
+ isMonorepo
1338
+ ? " same system npx nexarch@latest init-project --dir . --map-deployable <deployable>=<entityRef>"
1339
+ : " same application npx nexarch@latest init-project --dir . --application-ref <entityRef>",
1340
+ " all different npx nexarch@latest init-project --dir . --create-application",
1341
+ ];
1342
+ }
1241
1343
  /**
1242
1344
  * For each detected name, the entity_refs of what declares it.
1243
1345
  *
@@ -1440,6 +1542,42 @@ export async function initProject(args) {
1440
1542
  seenSubSlugs.add(dirSlug);
1441
1543
  sp.externalKey = `${sp.entityType}:${projectDirSlug}_${dirSlug}`;
1442
1544
  }
1545
+ // Refuse bad mappings before anything is written, naming every problem.
1546
+ const deployableMappings = parseDeployableMappings(args);
1547
+ if (deployableMappings.length > 0) {
1548
+ const mappingErrors = isMonorepo
1549
+ ? applyDeployableMappings(subPackages, deployableMappings)
1550
+ : ["--map-deployable applies to a repository with several deployables. For a single-package repository use --application-ref."];
1551
+ if (mappingErrors.length > 0)
1552
+ throw new Error(mappingErrors.join("\n"));
1553
+ }
1554
+ // A merge leaves the losing application's ref behind as an alias of the
1555
+ // survivor. Without honouring it, the next scan of the repository would
1556
+ // re-create the application a human had just merged away. Looked up with
1557
+ // recordUnresolved: false -- an unmapped "application:acme_web" is not a
1558
+ // technology to put in front of catalogue reviewers.
1559
+ if (projectConstruct && isMonorepo && !forceCreateApplication) {
1560
+ const unmapped = subPackages.filter((sp) => isApplicationLikeEntityType(sp.entityType) && !sp.mappedFrom);
1561
+ if (unmapped.length > 0) {
1562
+ try {
1563
+ const aliasRaw = await callMcpProfiled("nexarch_resolve_reference", { names: unmapped.map((sp) => sp.externalKey), companyId: creds.companyId, recordUnresolved: false }, { phase: "deployable.alias" });
1564
+ for (const hit of parseToolText(aliasRaw).results ?? []) {
1565
+ const sp = unmapped.find((candidate) => candidate.externalKey === hit.input);
1566
+ const ref = hit.canonicalExternalRef;
1567
+ if (!sp || !hit.resolved || hit.entityTypeCode !== "application" || !ref?.startsWith("application:") || ref === sp.externalKey)
1568
+ continue;
1569
+ sp.mappedFrom = sp.externalKey;
1570
+ sp.externalKey = ref;
1571
+ sp.mappedBy = "alias";
1572
+ if (!asJson)
1573
+ console.log(` Mapped : ${sp.relativePath} → ${ref} (${hit.canonicalName ?? "existing application"}), from an earlier merge`);
1574
+ }
1575
+ }
1576
+ catch {
1577
+ // Best effort. If the lookup fails, the duplicate check below still runs.
1578
+ }
1579
+ }
1580
+ }
1443
1581
  if (!asJson) {
1444
1582
  console.log(` Project : ${displayName} (${entityTypeOverride})`);
1445
1583
  console.log(` Packages: ${packageJsonCount} package.json file(s)`);
@@ -1542,6 +1680,10 @@ export async function initProject(args) {
1542
1680
  const sourceProvider = detectedRepo?.provider ?? "unknown";
1543
1681
  const declaredHomepage = readDeclaredHomepage(dir);
1544
1682
  let similarApplications = [];
1683
+ // Set when the scan must stop and ask instead of registering anything.
1684
+ let stopForReview = null;
1685
+ // Names of applications already in the workspace, by ref, for mapped deployables.
1686
+ const existingApplicationNames = new Map();
1545
1687
  if (projectConstruct && isMonorepo && (applicationRefOverride || forceCreateApplication) && !asJson) {
1546
1688
  console.log("\nNote: monorepo detected — no root application is created (ADR 8a); --application-ref/--create-application apply only to single-package repositories.");
1547
1689
  }
@@ -1575,16 +1717,18 @@ export async function initProject(args) {
1575
1717
  if (!applicationRefOverride && projectExternalKey === `${entityTypeOverride}:${projectSlug}`) {
1576
1718
  const appsRaw = await callMcpProfiled("nexarch_list_entities", { entityTypeCode: "application", status: "active", limit: 500, companyId: creds.companyId }, { entityTypeCode: "application", limit: 500 });
1577
1719
  const appsData = parseToolText(appsRaw);
1578
- const apps = (appsData.entities ?? []).filter((e) => (e.entityRef ?? e.externalKey));
1720
+ const listed = (appsData.entities ?? []).filter((e) => (e.entityRef ?? e.externalKey));
1721
+ // This repository's own application, on a re-run, matches its own name
1722
+ // exactly. Now that a match stops the scan, that would stop every re-run.
1723
+ const alreadyRegistered = listed.some((e) => (e.entityRef ?? e.externalKey) === projectExternalKey);
1724
+ const apps = listed.filter((e) => (e.entityRef ?? e.externalKey) !== projectExternalKey);
1579
1725
  if (apps.length > 0) {
1580
1726
  const matches = apps
1581
1727
  .map((a) => scoreApplicationCandidate(a, displayName, repoUrl, declaredHomepage))
1582
1728
  .filter((m) => Boolean(m))
1583
1729
  .sort((a, b) => b.score - a.score);
1584
- // Captured for the JSON output. Until this existed the branch below was
1585
- // guarded by `!asJson`, so an agent -- which always runs --json -- got a
1586
- // result with no hint that a similar application already existed, and
1587
- // reported the new proposal as if nothing resembled it.
1730
+ // Captured for the JSON output, not only printed: an agent always runs
1731
+ // --json, and a match it never sees is a match nobody acts on.
1588
1732
  similarApplications = matches.slice(0, 5);
1589
1733
  const suggested = matches.length > 0 ? matches[0] : null;
1590
1734
  const highConfidence = suggested && suggested.score >= 0.85;
@@ -1598,15 +1742,19 @@ export async function initProject(args) {
1598
1742
  // remains available via --application-ref and --auto-map-application.
1599
1743
  if (autoMapApplication && highConfidence) {
1600
1744
  projectExternalKey = suggested.entityRef;
1745
+ similarApplications = [];
1601
1746
  if (!asJson)
1602
1747
  console.log(`\nAuto-mapped to existing application: ${suggested.name} (${projectExternalKey})`);
1603
1748
  }
1604
- else if (matches.length > 0 && !asJson) {
1605
- console.log("\nSimilar existing applications found (registering a new proposed application anyway — the reviewer sees these again at activation):");
1606
- for (const m of matches.slice(0, 5)) {
1607
- console.log(` - ${m.name} (${m.entityRef}) score=${m.score.toFixed(2)} [${m.reasons.join(", ")}]`);
1608
- }
1609
- console.log(" To map to one instead, re-run with: --application-ref <entityRef>");
1749
+ else {
1750
+ const gate = registrationGate({
1751
+ similar: similarApplications,
1752
+ newApplicationRefs: alreadyRegistered ? [] : [projectExternalKey],
1753
+ createAnyway: false,
1754
+ });
1755
+ similarApplications = gate.similar;
1756
+ if (gate.stop)
1757
+ stopForReview = { newApplications: [{ deployable: basename(dir), entityRef: projectExternalKey }] };
1610
1758
  }
1611
1759
  }
1612
1760
  }
@@ -1629,18 +1777,69 @@ export async function initProject(args) {
1629
1777
  // reports the repo as a duplicate of itself.
1630
1778
  const ownRefs = new Set(subPackages.map((sp) => sp.externalKey));
1631
1779
  const appsRaw = await callMcpProfiled("nexarch_list_entities", { entityTypeCode: "application", status: "active", limit: 500, companyId: creds.companyId }, { entityTypeCode: "application", limit: 500, phase: "monorepo.similar" });
1632
- similarApplications = similarApplicationsForRepository(parseToolText(appsRaw).entities ?? [], {
1633
- repositoryName: displayName,
1634
- repoUrl,
1635
- homepageUrl: declaredHomepage,
1636
- ownRefs,
1780
+ const listed = parseToolText(appsRaw).entities ?? [];
1781
+ for (const entity of listed) {
1782
+ const ref = entity.entityRef ?? entity.externalKey;
1783
+ if (ref)
1784
+ existingApplicationNames.set(ref, entity.name);
1785
+ }
1786
+ // A mapped deployable writes no stub of its own, so a mapping to an
1787
+ // application that is not there would leave its relationships pointing at
1788
+ // nothing. Refuse before writing.
1789
+ const badTargets = subPackages.filter((sp) => sp.mappedBy === "flag" && !existingApplicationNames.has(sp.externalKey));
1790
+ if (badTargets.length > 0) {
1791
+ throw new Error(badTargets
1792
+ .map((sp) => `--map-deployable: ${sp.externalKey} is not an active application in this workspace (mapping ${sp.relativePath}).`)
1793
+ .join("\n"));
1794
+ }
1795
+ const newDeployables = subPackages.filter((sp) => isApplicationLikeEntityType(sp.entityType) && !existingApplicationNames.has(sp.externalKey));
1796
+ const gate = registrationGate({
1797
+ similar: similarApplicationsForRepository(listed, { repositoryName: displayName, repoUrl, homepageUrl: declaredHomepage, ownRefs }),
1798
+ newApplicationRefs: newDeployables.map((sp) => sp.externalKey),
1799
+ createAnyway: false,
1637
1800
  });
1638
- if (similarApplications.length > 0 && !asJson) {
1639
- console.log("\nExisting applications resemble this repository — one of its deployables may already be registered:");
1640
- for (const match of similarApplications) {
1641
- console.log(` - ${match.name} (${match.entityRef}) score=${match.score.toFixed(2)} [${match.reasons.join(", ")}]`);
1801
+ similarApplications = gate.similar;
1802
+ if (gate.stop) {
1803
+ stopForReview = {
1804
+ newApplications: newDeployables.map((sp) => ({ deployable: basename(sp.relativePath), entityRef: sp.externalKey })),
1805
+ };
1806
+ }
1807
+ }
1808
+ // Paul's decision: a possible duplicate stops the scan before it writes
1809
+ // anything. Resolution has already run (it only reads, and logs names not in
1810
+ // the catalogue); nothing below this point has.
1811
+ if (stopForReview) {
1812
+ const directive = possibleDuplicateDirective(similarApplications, isMonorepo);
1813
+ const commands = isMonorepo
1814
+ ? {
1815
+ sameSystem: "npx nexarch@latest init-project --dir . --map-deployable <deployable>=<entityRef>",
1816
+ allDifferent: "npx nexarch@latest init-project --dir . --create-application",
1642
1817
  }
1818
+ : {
1819
+ sameApplication: "npx nexarch@latest init-project --dir . --application-ref <entityRef>",
1820
+ allDifferent: "npx nexarch@latest init-project --dir . --create-application",
1821
+ };
1822
+ if (asJson) {
1823
+ process.stdout.write(`${JSON.stringify({
1824
+ ok: true,
1825
+ status: "review_required",
1826
+ wrote: false,
1827
+ project: projectConstruct
1828
+ ? { name: projectDirName, externalKey: projectEntityKey, entityType: "project", subtype: isMonorepo ? "project_monorepo" : "project_repository" }
1829
+ : { name: displayName, externalKey: projectExternalKey, entityType: entityTypeOverride },
1830
+ similarApplications,
1831
+ newApplications: stopForReview.newApplications,
1832
+ decision: { note: REVIEW_REQUIRED_NOTE, commands },
1833
+ }, null, 2)}\n`);
1834
+ process.stderr.write(`${directive.join("\n")}\n`);
1835
+ }
1836
+ else {
1837
+ console.log(directive.join("\n"));
1838
+ console.log("\nWould have registered:");
1839
+ for (const app of stopForReview.newApplications)
1840
+ console.log(` ${app.deployable} → ${app.entityRef}`);
1643
1841
  }
1842
+ return;
1644
1843
  }
1645
1844
  logProgress(projectConstruct ? "project.target" : "application.target", projectConstruct ? (isMonorepo ? projectEntityKey : `${projectEntityKey} + ${projectExternalKey}`) : projectExternalKey);
1646
1845
  // In refresh mode, snapshot the current graph state for this project before writing,
@@ -1760,6 +1959,10 @@ export async function initProject(args) {
1760
1959
  if (seenSubKeys.has(sp.externalKey))
1761
1960
  continue;
1762
1961
  seenSubKeys.add(sp.externalKey);
1962
+ // The application already exists and a human may have named and described
1963
+ // it. Its relationships below still use its ref.
1964
+ if (sp.mappedFrom)
1965
+ continue;
1763
1966
  const dirDisplayName = basename(sp.relativePath);
1764
1967
  entities.push({
1765
1968
  externalKey: sp.externalKey,
@@ -1969,6 +2172,25 @@ export async function initProject(args) {
1969
2172
  // ability to resolve via tier 2; the scan itself already succeeded.
1970
2173
  }
1971
2174
  }
2175
+ // Remember an explicit --map-deployable, so the next scan does not ask again.
2176
+ // A merge registers the same alias; this is the same decision made up front.
2177
+ for (const sp of subPackages) {
2178
+ if (sp.mappedBy !== "flag" || !sp.mappedFrom)
2179
+ continue;
2180
+ try {
2181
+ await callMcpTool("nexarch_register_alias", {
2182
+ alias: sp.mappedFrom,
2183
+ canonicalExternalKey: sp.externalKey,
2184
+ canonicalName: existingApplicationNames.get(sp.externalKey) ?? sp.externalKey,
2185
+ entityTypeCode: "application",
2186
+ description: `Registered by init-project: the ${sp.relativePath} deployable is this application.`,
2187
+ companyId: creds.companyId,
2188
+ }, { companyId: creds.companyId });
2189
+ }
2190
+ catch {
2191
+ // Non-fatal: without it the next scan asks again, which is safe.
2192
+ }
2193
+ }
1972
2194
  // Upsert relationships (chunked)
1973
2195
  let relsResult = null;
1974
2196
  if (relationships.length > 0) {
@@ -2145,16 +2367,21 @@ export async function initProject(args) {
2145
2367
  });
2146
2368
  const preservedEntities = entitiesResult.preserved ?? [];
2147
2369
  const nothingWritten = (entitiesResult.summary?.succeeded ?? 0) === 0;
2148
- const status = refreshMode && nothingWritten && preservedEntities.length > 0
2149
- ? "review_enrichment"
2150
- : "enrichment_required";
2370
+ const status = enrichmentStatusFor({
2371
+ similarApplicationCount: similarApplications.length,
2372
+ refreshMode,
2373
+ nothingWritten,
2374
+ preservedEntityCount: preservedEntities.length,
2375
+ });
2151
2376
  return {
2152
2377
  status,
2153
- note: status === "review_enrichment"
2154
- ? "No new skeletons were written — all entities were already enriched and preserved. Review the preservedEntities list and confirm scan values match. Update with nexarch update-entity only if something has genuinely changed."
2155
- : refreshMode
2156
- ? "Execute ALL pendingSteps. In refresh mode, compare graphState vs scanState to identify gaps — do not blindly remove relationships the scanner missed; the scanner cannot detect platforms, integrations, data assets, or application_functions."
2157
- : "Execute ALL pendingSteps before reporting the project as registered. Do not pause or ask for confirmation between steps.",
2378
+ note: status === "review_required"
2379
+ ? REVIEW_REQUIRED_NOTE
2380
+ : status === "review_enrichment"
2381
+ ? "No new skeletons were written — all entities were already enriched and preserved. Review the preservedEntities list and confirm scan values match. Update with nexarch update-entity only if something has genuinely changed."
2382
+ : refreshMode
2383
+ ? "Execute ALL pendingSteps. In refresh mode, compare graphState vs scanState to identify gaps — do not blindly remove relationships the scanner missed; the scanner cannot detect platforms, integrations, data assets, or application_functions."
2384
+ : "Execute ALL pendingSteps before reporting the project as registered. Do not pause or ask for confirmation between steps.",
2158
2385
  ...(refreshMode && preservedEntities.length > 0
2159
2386
  ? {
2160
2387
  preservedEntities: {
@@ -2276,6 +2503,7 @@ export async function initProject(args) {
2276
2503
  lines.push("init-project wrote a skeleton. The steps below complete the registration.");
2277
2504
  lines.push("Until they are done, this project is registered as a skeleton only.");
2278
2505
  }
2506
+ lines.push(...possibleDuplicateDirective(similarApplications, isMonorepo));
2279
2507
  lines.push("");
2280
2508
  lines.push(`PROJECT : ${projectConstruct ? `${projectEntityKey} (${isMonorepo ? "monorepo" : "repository"})` : projectExternalKey}`);
2281
2509
  lines.push(`DIR : ${dir}`);
@@ -2300,6 +2528,12 @@ export async function initProject(args) {
2300
2528
  const conf = subPackageConfidence(sp);
2301
2529
  lines.push(` ${sp.relativePath}`);
2302
2530
  lines.push(` key : ${sp.externalKey}`);
2531
+ if (sp.mappedFrom) {
2532
+ // "Confirm type, then update" is right for a new skeleton and wrong
2533
+ // here: this is an application a human already registered and named.
2534
+ 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.");
2536
+ }
2303
2537
  lines.push(` inferred : ${sp.entityType} / ${sp.subtype} (confidence ${conf.toFixed(2)})`);
2304
2538
  const unresolvedDeps = sp.depSpecs.map((d) => d.name).filter((d) => !resolvedByInput.has(d));
2305
2539
  if (unresolvedDeps.length > 0) {
@@ -2460,7 +2694,11 @@ export async function initProject(args) {
2460
2694
  const preservedCount = output.entities.preserved ?? 0;
2461
2695
  console.log(` Entities : ${output.entities.succeeded ?? 0} written, ${preservedCount > 0 ? `${preservedCount} preserved (enriched), ` : ""}${output.entities.failed ?? 0} failed`);
2462
2696
  console.log(` Relationships: ${output.relationships.succeeded ?? 0} written`);
2463
- console.log(` Status : ${output.status === "review_enrichment" ? "all enriched; review preserved entities" : "skeleton created; enrichment pending"}`);
2697
+ console.log(` Status : ${output.status === "review_required"
2698
+ ? "possible duplicate; ask before enriching"
2699
+ : output.status === "review_enrichment"
2700
+ ? "all enriched; review preserved entities"
2701
+ : "skeleton created; enrichment pending"}`);
2464
2702
  if (output.metrics.relationshipsSkippedAsDuplicate > 0) {
2465
2703
  console.log(` Deduped rels : ${output.metrics.relationshipsSkippedAsDuplicate} skipped as duplicates before upsert`);
2466
2704
  }
package/dist/index.js CHANGED
@@ -166,7 +166,12 @@ Usage:
166
166
  --name <name> override project name
167
167
  --entity-type <code> (default: application)
168
168
  --application-ref <entityRef> force mapping target
169
- --create-application force new application entity
169
+ --map-deployable <dir>=<entityRef> a monorepo deployable is an
170
+ existing application (repeatable)
171
+ --create-application register even if similar applications exist
172
+ If an application already in the workspace resembles one this
173
+ would register, nothing is written: it reports the matches and
174
+ the command for each choice.
170
175
  --auto-map-application auto-map only when high confidence
171
176
  --non-interactive deprecated (mapping no longer prompts)
172
177
  --batch-size <n> upsert batch size (default: 10)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.13.1",
3
+ "version": "0.13.2",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",