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.
- package/dist/commands/init-agent.js +6 -0
- package/dist/commands/init-project.js +267 -29
- package/dist/index.js +6 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
1585
|
-
//
|
|
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
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
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
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
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
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
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 =
|
|
2149
|
-
|
|
2150
|
-
|
|
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 === "
|
|
2154
|
-
?
|
|
2155
|
-
:
|
|
2156
|
-
? "
|
|
2157
|
-
:
|
|
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 === "
|
|
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
|
-
--
|
|
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)
|