nexarch 0.9.10 → 0.9.13
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-project.js +224 -273
- package/dist/commands/update-entity.js +1 -1
- package/dist/index.js +14 -0
- package/package.json +1 -1
|
@@ -549,6 +549,17 @@ function findPackageJsonPaths(rootDir) {
|
|
|
549
549
|
const l1Pkg = join(l1Path, "package.json");
|
|
550
550
|
if (existsSync(l1Pkg)) {
|
|
551
551
|
pushPath(l1Pkg);
|
|
552
|
+
// Resolve workspace sub-packages declared inside this project, so that
|
|
553
|
+
// running init-project from a multi-project root still discovers nested
|
|
554
|
+
// apps/* / packages/* within each child project.
|
|
555
|
+
const l1PkgData = readRootPackage(l1Pkg);
|
|
556
|
+
if (l1PkgData.workspaces) {
|
|
557
|
+
for (const wsDir of resolveWorkspaceDirs(l1Path, l1PkgData.workspaces)) {
|
|
558
|
+
const wsPkg = join(wsDir, "package.json");
|
|
559
|
+
if (existsSync(wsPkg))
|
|
560
|
+
pushPath(wsPkg);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
552
563
|
continue;
|
|
553
564
|
}
|
|
554
565
|
for (const l2 of readdirSync(l1Path)) {
|
|
@@ -887,14 +898,17 @@ function scanProject(dir) {
|
|
|
887
898
|
};
|
|
888
899
|
}
|
|
889
900
|
// ─── Relationship type selection ──────────────────────────────────────────────
|
|
890
|
-
function pickRelationshipType(toEntityTypeCode, _fromEntityTypeCode = "application") {
|
|
901
|
+
function pickRelationshipType(toEntityTypeCode, toEntitySubtypeCode, _fromEntityTypeCode = "application") {
|
|
891
902
|
switch (toEntityTypeCode) {
|
|
892
903
|
case "model":
|
|
893
904
|
return "uses_model";
|
|
894
905
|
case "platform":
|
|
895
|
-
case "platform_component":
|
|
896
|
-
// runs_on is the valid ontology relationship from application/tech to platform
|
|
897
906
|
return "runs_on";
|
|
907
|
+
case "platform_component":
|
|
908
|
+
// platform_service (Vercel Analytics, Vercel Postgres, Cloudinary, etc.) are opt-in
|
|
909
|
+
// managed services — integrates_with is the semantically correct relationship.
|
|
910
|
+
// Other platform_component subtypes (platform_runtime, platform_control_plane) use depends_on.
|
|
911
|
+
return toEntitySubtypeCode === "platform_service" ? "integrates_with" : "depends_on";
|
|
898
912
|
case "skill":
|
|
899
913
|
return "uses";
|
|
900
914
|
case "technology_component":
|
|
@@ -1029,6 +1043,7 @@ export async function initProject(args) {
|
|
|
1029
1043
|
const autoMapApplication = parseFlag(args, "--auto-map-application");
|
|
1030
1044
|
const nonInteractive = parseFlag(args, "--non-interactive");
|
|
1031
1045
|
const upsertBatchSize = Number(parseOptionValue(args, "--batch-size") ?? "10") || 10;
|
|
1046
|
+
const refreshMode = parseFlag(args, "--refresh");
|
|
1032
1047
|
const creds = requireCredentials();
|
|
1033
1048
|
const mcpOpts = { companyId: creds.companyId };
|
|
1034
1049
|
if (!asJson)
|
|
@@ -1222,8 +1237,24 @@ export async function initProject(args) {
|
|
|
1222
1237
|
}
|
|
1223
1238
|
}
|
|
1224
1239
|
logProgress("application.target", projectExternalKey);
|
|
1240
|
+
// In refresh mode, snapshot the current graph state for this project before writing,
|
|
1241
|
+
// so we can diff what changed and surface stale relationships to the agent.
|
|
1242
|
+
let currentOutgoingRels = [];
|
|
1243
|
+
let currentPartOfRels = [];
|
|
1244
|
+
if (refreshMode) {
|
|
1245
|
+
logProgress("refresh.fetch.start");
|
|
1246
|
+
if (!asJson)
|
|
1247
|
+
console.log("\nFetching current graph state…");
|
|
1248
|
+
const [outgoingRaw, partOfRaw] = await Promise.all([
|
|
1249
|
+
callMcpProfiled("nexarch_list_relationships", { companyId: creds.companyId, fromEntityExternalKey: projectExternalKey, limit: 500 }, { phase: "refresh.outgoing" }),
|
|
1250
|
+
callMcpProfiled("nexarch_list_relationships", { companyId: creds.companyId, toEntityExternalKey: projectExternalKey, relationshipTypeCode: "part_of", limit: 500 }, { phase: "refresh.partOf" }),
|
|
1251
|
+
]);
|
|
1252
|
+
currentOutgoingRels = parseToolText(outgoingRaw).relationships ?? [];
|
|
1253
|
+
currentPartOfRels = parseToolText(partOfRaw).relationships ?? [];
|
|
1254
|
+
logProgress("refresh.fetch.done", `outgoing=${currentOutgoingRels.length}, partOf=${currentPartOfRels.length}`);
|
|
1255
|
+
}
|
|
1225
1256
|
const agentContext = {
|
|
1226
|
-
agentId: "nexarch-cli:init-project",
|
|
1257
|
+
agentId: refreshMode ? "nexarch-cli:update-project" : "nexarch-cli:init-project",
|
|
1227
1258
|
agentRunId: `init-project-${Date.now()}`,
|
|
1228
1259
|
repoRef,
|
|
1229
1260
|
repoPath,
|
|
@@ -1343,7 +1374,7 @@ export async function initProject(args) {
|
|
|
1343
1374
|
if (!rootDepNames.has(r.input) && !rootDepNames.has(r.normalised))
|
|
1344
1375
|
continue;
|
|
1345
1376
|
const depSpec = rootDepVersions.get(r.input) ?? rootDepVersions.get(r.normalised);
|
|
1346
|
-
addRel(pickRelationshipType(r.entityTypeCode), projectExternalKey, r.canonicalExternalRef, 0.9, {
|
|
1377
|
+
addRel(pickRelationshipType(r.entityTypeCode, r.entitySubtypeCode), projectExternalKey, r.canonicalExternalRef, 0.9, {
|
|
1347
1378
|
source: depSpec?.source ?? "manifest_scan",
|
|
1348
1379
|
detected_at: nowIso,
|
|
1349
1380
|
...buildVersionAttributes(depSpec?.versionRaw ?? null, depSpec?.source ?? "manifest_scan"),
|
|
@@ -1363,7 +1394,7 @@ export async function initProject(args) {
|
|
|
1363
1394
|
const r = resolvedByInput.get(dep.name);
|
|
1364
1395
|
if (!r?.canonicalExternalRef || !r.entityTypeCode)
|
|
1365
1396
|
continue;
|
|
1366
|
-
addRel(pickRelationshipType(r.entityTypeCode, sp.entityType), sp.externalKey, r.canonicalExternalRef, 0.9, {
|
|
1397
|
+
addRel(pickRelationshipType(r.entityTypeCode, r.entitySubtypeCode, sp.entityType), sp.externalKey, r.canonicalExternalRef, 0.9, {
|
|
1367
1398
|
source: dep.source,
|
|
1368
1399
|
detected_at: nowIso,
|
|
1369
1400
|
...buildVersionAttributes(dep.versionRaw, dep.source),
|
|
@@ -1393,6 +1424,22 @@ export async function initProject(args) {
|
|
|
1393
1424
|
if (detectedRepo?.canonicalRepoRef) {
|
|
1394
1425
|
addRel("depends_on", projectExternalKey, detectedRepo.canonicalRepoRef, 0.95);
|
|
1395
1426
|
}
|
|
1427
|
+
let graphDiff = null;
|
|
1428
|
+
if (refreshMode) {
|
|
1429
|
+
const newRelKeySet = new Set(relationships.map((r) => `${r.relationshipTypeCode}::${r.fromEntityExternalKey}::${r.toEntityExternalKey}`));
|
|
1430
|
+
const currentRelKeySet = new Set(currentOutgoingRels
|
|
1431
|
+
.filter((r) => r.fromEntityExternalKey && r.toEntityExternalKey)
|
|
1432
|
+
.map((r) => `${r.relationshipTypeCode}::${r.fromEntityExternalKey}::${r.toEntityExternalKey}`));
|
|
1433
|
+
const newRelationshipsInDiff = relationships.filter((r) => !currentRelKeySet.has(`${r.relationshipTypeCode}::${r.fromEntityExternalKey}::${r.toEntityExternalKey}`));
|
|
1434
|
+
const staleRelationships = currentOutgoingRels.filter((r) => r.fromEntityExternalKey &&
|
|
1435
|
+
r.toEntityExternalKey &&
|
|
1436
|
+
!newRelKeySet.has(`${r.relationshipTypeCode}::${r.fromEntityExternalKey}::${r.toEntityExternalKey}`));
|
|
1437
|
+
const currentSubPackageKeys = new Set(currentPartOfRels.map((r) => r.fromEntityExternalKey).filter((k) => Boolean(k)));
|
|
1438
|
+
const newSubPackageKeys = new Set(subPackages.map((sp) => sp.externalKey));
|
|
1439
|
+
const removedSubPackageKeys = [...currentSubPackageKeys].filter((k) => !newSubPackageKeys.has(k));
|
|
1440
|
+
graphDiff = { newRelationships: newRelationshipsInDiff, staleRelationships, removedSubPackageKeys };
|
|
1441
|
+
logProgress("refresh.diff", `newRels=${newRelationshipsInDiff.length}, staleRels=${staleRelationships.length}, removedSubPkgs=${removedSubPackageKeys.length}`);
|
|
1442
|
+
}
|
|
1396
1443
|
if (!asJson)
|
|
1397
1444
|
console.log(`\nWriting to graph…`);
|
|
1398
1445
|
// Upsert entities (chunked for progressive feedback)
|
|
@@ -1455,276 +1502,174 @@ export async function initProject(args) {
|
|
|
1455
1502
|
}
|
|
1456
1503
|
logProgress("upsert.relationships.done", `succeeded=${relsResult.summary?.succeeded ?? 0}, failed=${relsResult.summary?.failed ?? 0}`);
|
|
1457
1504
|
}
|
|
1458
|
-
// Build structured enrichment task (included in JSON output and printed in human mode)
|
|
1459
1505
|
const readmeHints = ["README.md", "README.mdx", "docs/README.md", "docs/index.md"]
|
|
1460
1506
|
.filter((f) => existsSync(join(dir, f)));
|
|
1461
1507
|
const preWiredRelationshipKeys = new Set(relationships.map((rel) => `${rel.relationshipTypeCode}::${rel.fromEntityExternalKey}::${rel.toEntityExternalKey}`));
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
→ --entity-type application_component --subtype app_comp_worker / app_comp_job / app_comp_etl / app_comp_data_pipeline
|
|
1495
|
-
Shared internal library or package (imported by other packages, not deployed on its own)
|
|
1496
|
-
→ --entity-type technology_component --subtype tech_library
|
|
1497
|
-
Shared UI component library
|
|
1498
|
-
→ --entity-type technology_component --subtype tech_framework
|
|
1499
|
-
Type definitions or utility package with no runtime
|
|
1500
|
-
→ --entity-type technology_component --subtype tech_library
|
|
1501
|
-
|
|
1502
|
-
ICON ASSIGNMENT (applications only):
|
|
1503
|
-
• Pick one icon from the full Lucide icon set for each application/sub-app.
|
|
1504
|
-
• If confidence is low, skip --icon rather than guessing.
|
|
1505
|
-
• Use kebab-case icon names (example: server, workflow, shield-check).
|
|
1506
|
-
|
|
1507
|
-
For each sub-package, run update-entity to register it:
|
|
1508
|
-
|
|
1509
|
-
npx nexarch update-entity \\
|
|
1510
|
-
--key "<entity-type>:<slugified-package-name>" \\
|
|
1511
|
-
--entity-type "<chosen entity type>" \\
|
|
1512
|
-
--subtype "<chosen subtype>" \\
|
|
1513
|
-
--name "<human readable name>" \\
|
|
1514
|
-
--description "<what this package does and its role in the project>" \\
|
|
1515
|
-
--icon "<lucide icon>" # applications only
|
|
1516
|
-
|
|
1517
|
-
Then register it as a resolvable alias so future scans don't re-surface it as a candidate:
|
|
1518
|
-
|
|
1519
|
-
npx nexarch register-alias \\
|
|
1520
|
-
--alias "<original package name e.g. @scope/name>" \\
|
|
1521
|
-
--key "<same entity-type>:<slugified-package-name>" \\
|
|
1522
|
-
--name "<same human readable name>" \\
|
|
1523
|
-
--entity-type "<same entity type>"
|
|
1524
|
-
|
|
1525
|
-
⚠️ DIRECTION MATTERS — wire relationships as follows:
|
|
1526
|
-
|
|
1527
|
-
Deployable sub-apps (apps/*) are PART OF the parent application — child points to parent:
|
|
1528
|
-
npx nexarch add-relationship \\
|
|
1529
|
-
--from "<sub-app-key>" \\
|
|
1530
|
-
--to "${projectExternalKey}" \\
|
|
1531
|
-
--type "part_of"
|
|
1532
|
-
(FROM = the sub-app, TO = ${projectExternalKey})
|
|
1533
|
-
|
|
1534
|
-
Shared libraries (packages/*) are depended on by the parent — parent points to library:
|
|
1535
|
-
npx nexarch add-relationship \\
|
|
1536
|
-
--from "${projectExternalKey}" \\
|
|
1537
|
-
--to "<library-key>" \\
|
|
1538
|
-
--type "depends_on"
|
|
1539
|
-
(FROM = ${projectExternalKey}, TO = the library)
|
|
1540
|
-
|
|
1541
|
-
⚠️ WIRE DEPENDENCIES TO THE SUB-APP THAT DECLARES THEM, NOT TO THE PARENT.
|
|
1542
|
-
|
|
1543
|
-
IMPORTANT: init-project has already written baseline dependency relationships from manifests.
|
|
1544
|
-
Do NOT re-run add-relationship for entries listed as pre-wired below unless one of these is true:
|
|
1545
|
-
• you changed the target entity key (e.g. you replaced a temporary key), or
|
|
1546
|
-
• you are intentionally adding/changing relationship attributes beyond the scanned defaults.
|
|
1547
|
-
|
|
1548
|
-
RELATIONSHIP TYPE RULES:
|
|
1549
|
-
application → technology_component : --type "depends_on"
|
|
1550
|
-
technology_component → technology_component : --type "depends_on"
|
|
1551
|
-
any → platform : --type "runs_on"
|
|
1552
|
-
any → platform_component : --type "depends_on" (or "integrates_with" where appropriate)
|
|
1553
|
-
any → model : --type "uses_model"
|
|
1554
|
-
|
|
1555
|
-
Pre-resolved dependencies per sub-package (already wired by init-project; review only):
|
|
1556
|
-
${subPackages.map((sp) => {
|
|
1557
|
-
const resolved = sp.depSpecs
|
|
1558
|
-
.map((d) => resolvedByInput.get(d.name))
|
|
1559
|
-
.filter((r) => !!r?.canonicalExternalRef);
|
|
1560
|
-
if (resolved.length === 0)
|
|
1561
|
-
return ` • ${sp.name}: (no pre-resolved deps — check package.json manually)`;
|
|
1562
|
-
const lines = resolved.map((r) => {
|
|
1563
|
-
const relType = pickRelationshipType(r.entityTypeCode, sp.entityType);
|
|
1564
|
-
const key = `${relType}::${sp.externalKey}::${r.canonicalExternalRef}`;
|
|
1565
|
-
const status = preWiredRelationshipKeys.has(key) ? "[pre-wired]" : "[not pre-wired]";
|
|
1566
|
-
return ` ${status} --from "${sp.externalKey}" --to "${r.canonicalExternalRef}" --type "${relType}" # ${r.canonicalName}`;
|
|
1567
|
-
});
|
|
1568
|
-
return ` • ${sp.name}:\n${lines.join("\n")}`;
|
|
1569
|
-
}).join("\n\n")}
|
|
1570
|
-
`;
|
|
1571
|
-
const adrStepNumber = subPackages.length > 0 ? "STEP 4" : "STEP 3";
|
|
1572
|
-
const finalStep = subPackages.length > 0 ? "STEP 5" : "STEP 4";
|
|
1573
|
-
const adrSection = `
|
|
1574
|
-
${adrStepNumber} — Ask the enrichment agent to extract ADRs and register decision records.
|
|
1575
|
-
|
|
1576
|
-
Do NOT rely on init-project to parse ADR files. The enrichment agent should read ADR markdown and
|
|
1577
|
-
explicitly register decision records with relationships.
|
|
1578
|
-
|
|
1579
|
-
Agent task:
|
|
1580
|
-
• Find ADR files (e.g. docs/adr, adrs, decisions, ADR-*.md)
|
|
1581
|
-
• For each ADR, create/update a decision record entity
|
|
1582
|
-
• Link it using: decision_record --decides--> application/sub-app
|
|
1583
|
-
|
|
1584
|
-
REQUIRED fields to pass per decision (strict):
|
|
1585
|
-
• key: decision_record:<project>-<adr-slug>
|
|
1586
|
-
• subtype: decision_architecture
|
|
1587
|
-
• name: ADR title
|
|
1588
|
-
• decision summary: short business summary (1–3 sentences)
|
|
1589
|
-
• decision detail: full decision detail in markdown (context + decision + consequences)
|
|
1590
|
-
|
|
1591
|
-
Command pattern:
|
|
1592
|
-
npx nexarch update-entity \\
|
|
1593
|
-
--key "decision_record:<project>-<adr-slug>" \\
|
|
1594
|
-
--entity-type "decision_record" \\
|
|
1595
|
-
--subtype "decision_architecture" \\
|
|
1596
|
-
--name "<ADR title>" \\
|
|
1597
|
-
--attributes-json '{"decision":{"summary":"<short summary>","detail":"## Context\\n<context>\\n\\n## Decision\\n<decision>\\n\\n## Consequences\\n<consequences>"}}'
|
|
1598
|
-
|
|
1599
|
-
npx nexarch add-relationship \\
|
|
1600
|
-
--from "decision_record:<project>-<adr-slug>" \\
|
|
1601
|
-
--to "<application-or-sub-app-key>" \\
|
|
1602
|
-
--type "decides"
|
|
1603
|
-
`;
|
|
1604
|
-
const gapCheckSection = `
|
|
1605
|
-
${finalStep} — Identify architecturally significant components not auto-detected.
|
|
1606
|
-
|
|
1607
|
-
The mechanical scan only finds declared dependencies, env var names, and a few config files.
|
|
1608
|
-
It systematically misses hosting platforms, managed infrastructure, SaaS integrations,
|
|
1609
|
-
and external APIs that are only mentioned in README or deployment config.
|
|
1610
|
-
|
|
1611
|
-
Review what you've read and ask yourself: are there any of the following that are
|
|
1612
|
-
clearly part of this system's architecture but weren't auto-detected?
|
|
1613
|
-
|
|
1614
|
-
• Hosting / deployment platforms (Vercel, Railway, Fly.io, AWS, GCP, Azure…)
|
|
1615
|
-
• Managed data services (Neon, PlanetScale, Supabase, Upstash, Atlas…)
|
|
1616
|
-
• Auth / identity providers (Clerk, Auth0, WorkOS, Okta…)
|
|
1617
|
-
• External SaaS integrations (Stripe, Resend, SendGrid, Twilio, Sentry…)
|
|
1618
|
-
• CDN / storage / queues (Cloudflare, S3, CloudFront, SQS, Redis…)
|
|
1619
|
-
• CI/CD platforms (GitHub Actions, GitLab CI, CircleCI…)
|
|
1620
|
-
|
|
1621
|
-
ONLY register entries you are highly confident about — i.e. explicitly mentioned in
|
|
1622
|
-
the README, a config file, or a deployment manifest. Do not guess.
|
|
1623
|
-
|
|
1624
|
-
For each one you identify:
|
|
1625
|
-
|
|
1626
|
-
1. Resolve it to a canonical reference:
|
|
1627
|
-
npx nexarch resolve-names --names "<platform name>" --json
|
|
1628
|
-
|
|
1629
|
-
2. If resolved, instantiate the entity in the graph (REQUIRED before add-relationship):
|
|
1630
|
-
npx nexarch update-entity \\
|
|
1631
|
-
--key "<resolved-ref>" \\
|
|
1632
|
-
--entity-type "<entityTypeCode from resolve-names>" \\
|
|
1633
|
-
--name "<canonicalName from resolve-names>"
|
|
1634
|
-
|
|
1635
|
-
3. Wire the relationship:
|
|
1636
|
-
npx nexarch add-relationship \\
|
|
1637
|
-
--from "${projectExternalKey}" \\
|
|
1638
|
-
--to "<resolved-ref>" \\
|
|
1639
|
-
--type "runs_on" # for platforms (Vercel, Neon, etc.)
|
|
1640
|
-
# use --type "depends_on" for external APIs/SaaS (Stripe, Resend, etc.)
|
|
1641
|
-
|
|
1642
|
-
4. If unresolved (not in the reference library), skip it for now — it will appear
|
|
1643
|
-
as a reference candidate on the next scan once you register an alias for it.
|
|
1644
|
-
`;
|
|
1645
|
-
const fallbackTemplate = `
|
|
1646
|
-
╔══════════════════════════════════════════════════════════════════╗
|
|
1647
|
-
║ ENRICHMENT TASK — action required ║
|
|
1648
|
-
╚══════════════════════════════════════════════════════════════════╝
|
|
1649
|
-
|
|
1650
|
-
The mechanical scan has written a skeleton graph entry for this project (NOT fully enriched).
|
|
1651
|
-
You now need to enrich it with meaningful content before considering registration complete.
|
|
1652
|
-
|
|
1653
|
-
PROJECT ENTITY : ${projectExternalKey}
|
|
1654
|
-
PROJECT DIR : ${dir}
|
|
1655
|
-
ECOSYSTEM(S) : ${ecosystemLabel}
|
|
1656
|
-
MANIFEST : ${manifestHint}
|
|
1657
|
-
${readmeHints.length > 0 ? `README FILES : ${readmeHints.join(", ")}` : "README FILES : (none found — check docs/)"}
|
|
1658
|
-
|
|
1659
|
-
IMPORTANT REPORTING RULE:
|
|
1660
|
-
Do NOT report "fully registered" or "fully done" after init-project alone.
|
|
1661
|
-
Correct status after init-project is: "skeleton created; enrichment pending" until Steps 1+ are completed.
|
|
1662
|
-
|
|
1663
|
-
STEP 1 — Read the project README / docs and build your understanding of:
|
|
1664
|
-
• What does this project do? (business purpose, key features)
|
|
1665
|
-
• Who are the users?
|
|
1666
|
-
• What is the high-level architecture?
|
|
1667
|
-
• Any notable technical decisions or constraints?
|
|
1668
|
-
|
|
1669
|
-
STEP 2 — Enrich the project entity. Run this command with the description you've written:
|
|
1670
|
-
|
|
1671
|
-
ENRICHMENT QUALITY RULE:
|
|
1672
|
-
• Do enrichment as explicit per-entity updates.
|
|
1673
|
-
• Do NOT shortcut with bulk entity updates for semantic enrichment.
|
|
1674
|
-
• The goal is accurate, evidence-based descriptions/subtypes per entity, not just write throughput.
|
|
1675
|
-
|
|
1676
|
-
npx nexarch update-entity \\
|
|
1677
|
-
--key "${projectExternalKey}" \\
|
|
1678
|
-
--entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? " \\\n --subtype \"app_custom_built\" \\\n --icon \"<curated icon>\"" : ""} \\
|
|
1679
|
-
--name "<proper product name from README>" \\
|
|
1680
|
-
--description "<2–4 sentence summary of what it does and why>"
|
|
1681
|
-
${subPkgSection}${adrSection}${gapCheckSection}`;
|
|
1682
|
-
return fallbackTemplate;
|
|
1683
|
-
}
|
|
1684
|
-
const enrichmentTask = {
|
|
1685
|
-
template: {
|
|
1686
|
-
code: "builtin:init-project-enrichment",
|
|
1687
|
-
source: "builtin",
|
|
1688
|
-
registryVersion: null,
|
|
1689
|
-
},
|
|
1690
|
-
instructions: buildEnrichmentInstructions(),
|
|
1691
|
-
iconHints: {
|
|
1692
|
-
provider: "lucide",
|
|
1693
|
-
note: "Use any Lucide icon name for agent-selected enrichment icons; omit icon when low confidence.",
|
|
1694
|
-
},
|
|
1695
|
-
projectEntity: {
|
|
1696
|
-
externalKey: projectExternalKey,
|
|
1697
|
-
entityType: entityTypeOverride,
|
|
1698
|
-
readmeFiles: readmeHints,
|
|
1699
|
-
},
|
|
1700
|
-
subPackages: subPackages.map((sp) => {
|
|
1701
|
-
const resolvedDeps = sp.depSpecs
|
|
1702
|
-
.map((d) => resolvedByInput.get(d.name))
|
|
1703
|
-
.filter((r) => !!r?.canonicalExternalRef)
|
|
1704
|
-
.map((r) => {
|
|
1705
|
-
const relationshipTypeCode = pickRelationshipType(r.entityTypeCode, sp.entityType);
|
|
1706
|
-
const relationshipKey = `${relationshipTypeCode}::${sp.externalKey}::${r.canonicalExternalRef}`;
|
|
1508
|
+
// Confidence heuristic for sub-package classification based on path signals.
|
|
1509
|
+
function subPackageConfidence(sp) {
|
|
1510
|
+
if (sp.relativePath.startsWith("packages/"))
|
|
1511
|
+
return 0.75;
|
|
1512
|
+
if (sp.relativePath.startsWith("apps/"))
|
|
1513
|
+
return 0.70;
|
|
1514
|
+
return 0.60;
|
|
1515
|
+
}
|
|
1516
|
+
// Build the structured enrichment payload for JSON output.
|
|
1517
|
+
function buildEnrichmentPayload() {
|
|
1518
|
+
return {
|
|
1519
|
+
status: "enrichment_required",
|
|
1520
|
+
projectEntity: {
|
|
1521
|
+
externalKey: projectExternalKey,
|
|
1522
|
+
entityType: entityTypeOverride,
|
|
1523
|
+
},
|
|
1524
|
+
readFiles: readmeHints,
|
|
1525
|
+
classifyPackages: subPackages.map((sp) => {
|
|
1526
|
+
const resolvedDeps = sp.depSpecs
|
|
1527
|
+
.map((d) => resolvedByInput.get(d.name))
|
|
1528
|
+
.filter((r) => !!r?.canonicalExternalRef)
|
|
1529
|
+
.map((r) => {
|
|
1530
|
+
const relationshipTypeCode = pickRelationshipType(r.entityTypeCode, r.entitySubtypeCode, sp.entityType);
|
|
1531
|
+
const relationshipKey = `${relationshipTypeCode}::${sp.externalKey}::${r.canonicalExternalRef}`;
|
|
1532
|
+
return {
|
|
1533
|
+
canonicalExternalRef: r.canonicalExternalRef,
|
|
1534
|
+
canonicalName: r.canonicalName,
|
|
1535
|
+
entityTypeCode: r.entityTypeCode,
|
|
1536
|
+
relationshipTypeCode,
|
|
1537
|
+
preWired: preWiredRelationshipKeys.has(relationshipKey),
|
|
1538
|
+
};
|
|
1539
|
+
});
|
|
1707
1540
|
return {
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1541
|
+
name: sp.name,
|
|
1542
|
+
relativePath: sp.relativePath,
|
|
1543
|
+
externalKey: sp.externalKey,
|
|
1544
|
+
heuristicEntityType: sp.entityType,
|
|
1545
|
+
heuristicSubtype: sp.subtype,
|
|
1546
|
+
confidence: subPackageConfidence(sp),
|
|
1547
|
+
resolvedDeps,
|
|
1548
|
+
unresolvedDeps: sp.depSpecs.map((d) => d.name).filter((d) => !resolvedByInput.has(d)),
|
|
1713
1549
|
};
|
|
1714
|
-
})
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1550
|
+
}),
|
|
1551
|
+
unresolvedNames: unresolvedItems.map((r) => r.input),
|
|
1552
|
+
iconHints: {
|
|
1553
|
+
provider: "lucide",
|
|
1554
|
+
note: "Use any Lucide icon name (kebab-case); omit when confidence is low.",
|
|
1555
|
+
},
|
|
1556
|
+
...(graphDiff
|
|
1557
|
+
? {
|
|
1558
|
+
diff: {
|
|
1559
|
+
newRelationships: graphDiff.newRelationships.map((r) => ({
|
|
1560
|
+
type: r.relationshipTypeCode,
|
|
1561
|
+
from: r.fromEntityExternalKey,
|
|
1562
|
+
to: r.toEntityExternalKey,
|
|
1563
|
+
})),
|
|
1564
|
+
staleRelationships: graphDiff.staleRelationships.map((r) => ({
|
|
1565
|
+
type: r.relationshipTypeCode,
|
|
1566
|
+
from: r.fromEntityExternalKey,
|
|
1567
|
+
to: r.toEntityExternalKey,
|
|
1568
|
+
fromName: r.fromEntityName ?? null,
|
|
1569
|
+
toName: r.toEntityName ?? null,
|
|
1570
|
+
})),
|
|
1571
|
+
removedSubPackageKeys: graphDiff.removedSubPackageKeys,
|
|
1572
|
+
},
|
|
1573
|
+
}
|
|
1574
|
+
: {}),
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
// Build an agent-directed directive block for human/terminal output.
|
|
1578
|
+
// This is addressed to the *calling agent* (Claude Code, Codex, etc.) and tells it
|
|
1579
|
+
// to act inline in this same session — not to queue enrichment for later.
|
|
1580
|
+
function buildEnrichmentDirective() {
|
|
1581
|
+
const lines = [];
|
|
1582
|
+
lines.push("─── ENRICHMENT REQUIRED ─────────────────────────────────────────────────");
|
|
1583
|
+
lines.push("init-project wrote a skeleton. Complete enrichment NOW — do not ask the");
|
|
1584
|
+
lines.push("user for permission, do not summarise and pause, just execute the steps.");
|
|
1585
|
+
lines.push("Do NOT report the project as registered until enrichment is done.");
|
|
1586
|
+
lines.push("");
|
|
1587
|
+
lines.push(`PROJECT : ${projectExternalKey}`);
|
|
1588
|
+
lines.push(`DIR : ${dir}`);
|
|
1589
|
+
if (readmeHints.length > 0) {
|
|
1590
|
+
lines.push("");
|
|
1591
|
+
lines.push("READ_THESE first:");
|
|
1592
|
+
for (const f of readmeHints)
|
|
1593
|
+
lines.push(` ${join(dir, f)}`);
|
|
1594
|
+
}
|
|
1595
|
+
else {
|
|
1596
|
+
lines.push("");
|
|
1597
|
+
lines.push("READ_THESE first: (no README found — check docs/ for any markdown overview)");
|
|
1598
|
+
}
|
|
1599
|
+
if (subPackages.length > 0) {
|
|
1600
|
+
lines.push("");
|
|
1601
|
+
lines.push(`CLASSIFY_THESE (${subPackages.length} sub-package(s) — read each, confirm type, then update):`);
|
|
1602
|
+
for (const sp of subPackages) {
|
|
1603
|
+
const conf = subPackageConfidence(sp);
|
|
1604
|
+
lines.push(` ${sp.relativePath}`);
|
|
1605
|
+
lines.push(` key : ${sp.externalKey}`);
|
|
1606
|
+
lines.push(` inferred : ${sp.entityType} / ${sp.subtype} (confidence ${conf.toFixed(2)})`);
|
|
1607
|
+
const unresolvedDeps = sp.depSpecs.map((d) => d.name).filter((d) => !resolvedByInput.has(d));
|
|
1608
|
+
if (unresolvedDeps.length > 0) {
|
|
1609
|
+
lines.push(` unresolved deps (${unresolvedDeps.length}): ${unresolvedDeps.slice(0, 5).join(", ")}${unresolvedDeps.length > 5 ? ` … +${unresolvedDeps.length - 5} more` : ""}`);
|
|
1610
|
+
}
|
|
1611
|
+
lines.push(` → nexarch update-entity --key "${sp.externalKey}" --entity-type "${sp.entityType}" --subtype "${sp.subtype}" --name "..." --description "..."`);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (unresolvedItems.length > 0) {
|
|
1615
|
+
lines.push("");
|
|
1616
|
+
lines.push(`UNRESOLVED (${unresolvedItems.length} names not matched in reference library):`);
|
|
1617
|
+
const sample = unresolvedItems.slice(0, 20).map((r) => r.input);
|
|
1618
|
+
lines.push(` ${JSON.stringify(sample)}`);
|
|
1619
|
+
if (unresolvedItems.length > 20)
|
|
1620
|
+
lines.push(` … and ${unresolvedItems.length - 20} more`);
|
|
1621
|
+
}
|
|
1622
|
+
lines.push("");
|
|
1623
|
+
lines.push("THEN_RUN (execute these steps immediately — do not pause or ask for confirmation):");
|
|
1624
|
+
let step = 1;
|
|
1625
|
+
lines.push(` ${step++}. nexarch update-entity --key "${projectExternalKey}" --entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? ' --subtype "<subtype>" --icon "<lucide-icon>"' : ""} --name "..." --description "..."`);
|
|
1626
|
+
if (entityTypeOverride === "application") {
|
|
1627
|
+
lines.push(` Valid subtypes: app_custom_built app_saas app_mobile app_cli app_integration_service app_data_pipeline`);
|
|
1628
|
+
lines.push(` (choose app_custom_built as the default if none of the others clearly apply)`);
|
|
1629
|
+
}
|
|
1630
|
+
if (subPackages.length > 0) {
|
|
1631
|
+
lines.push(` ${step++}. Update each sub-package listed under CLASSIFY_THESE above.`);
|
|
1632
|
+
}
|
|
1633
|
+
lines.push(` ${step++}. Scan the READMEs for platforms/SaaS not auto-detected (Vercel, Neon, Stripe, etc.).`);
|
|
1634
|
+
lines.push(` For each found: nexarch resolve-names --names "..." --json → nexarch update-entity → nexarch add-relationship`);
|
|
1635
|
+
lines.push(` ${step++}. Look for ADRs (docs/adr/, decisions/, ADR-*.md) and register decision_record entities.`);
|
|
1636
|
+
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":"..."}}'`);
|
|
1637
|
+
lines.push(` nexarch add-relationship --from "decision_record:..." --to "${projectExternalKey}" --type decides`);
|
|
1638
|
+
lines.push("");
|
|
1639
|
+
lines.push("RELATIONSHIP DIRECTION RULES (for sub-packages):");
|
|
1640
|
+
lines.push(" apps/* (deployable components) → part_of → parent application");
|
|
1641
|
+
lines.push(" packages/* (shared libraries) → parent depends_on → library");
|
|
1642
|
+
lines.push(" Deps wired from manifests are already pre-wired; do not re-add unless key changed.");
|
|
1643
|
+
if (graphDiff) {
|
|
1644
|
+
if (graphDiff.staleRelationships.length > 0) {
|
|
1645
|
+
lines.push("");
|
|
1646
|
+
lines.push(`STALE_RELATIONSHIPS (${graphDiff.staleRelationships.length} — in graph but no longer in manifests; review and retire if confirmed removed):`);
|
|
1647
|
+
for (const r of graphDiff.staleRelationships) {
|
|
1648
|
+
const from = r.fromEntityExternalKey ?? r.fromEntityName ?? "?";
|
|
1649
|
+
const to = r.toEntityExternalKey ?? r.toEntityName ?? "?";
|
|
1650
|
+
lines.push(` [${r.relationshipTypeCode}] ${from} → ${to}`);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
if (graphDiff.removedSubPackageKeys.length > 0) {
|
|
1654
|
+
lines.push("");
|
|
1655
|
+
lines.push(`REMOVED_SUB_PACKAGES (${graphDiff.removedSubPackageKeys.length} — previously registered but no longer found in filesystem; retire or reassign):`);
|
|
1656
|
+
for (const k of graphDiff.removedSubPackageKeys)
|
|
1657
|
+
lines.push(` ${k}`);
|
|
1658
|
+
}
|
|
1659
|
+
if (graphDiff.staleRelationships.length === 0 && graphDiff.removedSubPackageKeys.length === 0) {
|
|
1660
|
+
lines.push("");
|
|
1661
|
+
lines.push("DIFF: No stale relationships or removed sub-packages detected.");
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
lines.push("");
|
|
1665
|
+
lines.push("─────────────────────────────────────────────────────────────────────────");
|
|
1666
|
+
return lines.join("\n");
|
|
1667
|
+
}
|
|
1668
|
+
const enrichmentRequired = buildEnrichmentPayload();
|
|
1726
1669
|
const output = {
|
|
1727
1670
|
ok: Number(entitiesResult.summary?.failed ?? 0) === 0,
|
|
1671
|
+
status: "enrichment_required",
|
|
1672
|
+
mode: refreshMode ? "refresh" : "init",
|
|
1728
1673
|
project: { name: displayName, externalKey: projectExternalKey, entityType: entityTypeOverride, detectedEcosystems },
|
|
1729
1674
|
entities: entitiesResult.summary ?? {},
|
|
1730
1675
|
relationships: relsResult?.summary ?? { requested: 0, succeeded: 0, failed: 0 },
|
|
@@ -1736,10 +1681,9 @@ ${subPkgSection}${adrSection}${gapCheckSection}`;
|
|
|
1736
1681
|
},
|
|
1737
1682
|
resolved: resolvedItems.length,
|
|
1738
1683
|
unresolved: unresolvedItems.length,
|
|
1739
|
-
unresolvedSample: unresolvedItems.slice(0, 10).map((r) => r.input),
|
|
1740
1684
|
entityErrors: entitiesResult.errors ?? [],
|
|
1741
1685
|
relationshipErrors: relsResult?.errors ?? [],
|
|
1742
|
-
|
|
1686
|
+
enrichmentRequired,
|
|
1743
1687
|
profile: profileEnabled
|
|
1744
1688
|
? {
|
|
1745
1689
|
startedAt: profile.startedAt,
|
|
@@ -1758,6 +1702,7 @@ ${subPkgSection}${adrSection}${gapCheckSection}`;
|
|
|
1758
1702
|
return;
|
|
1759
1703
|
}
|
|
1760
1704
|
console.log(`\nDone.`);
|
|
1705
|
+
console.log(` Mode : ${refreshMode ? "refresh (update-project)" : "init"}`);
|
|
1761
1706
|
console.log(` Entities : ${output.entities.succeeded ?? 0} written, ${output.entities.failed ?? 0} failed`);
|
|
1762
1707
|
console.log(` Relationships: ${output.relationships.succeeded ?? 0} written`);
|
|
1763
1708
|
console.log(" Status : skeleton created; enrichment pending");
|
|
@@ -1767,13 +1712,19 @@ ${subPkgSection}${adrSection}${gapCheckSection}`;
|
|
|
1767
1712
|
if (unresolvedItems.length > 0) {
|
|
1768
1713
|
console.log(` Candidates : ${unresolvedItems.length} added to reference candidates`);
|
|
1769
1714
|
}
|
|
1715
|
+
if (graphDiff && graphDiff.staleRelationships.length > 0) {
|
|
1716
|
+
console.log(` Stale rels : ${graphDiff.staleRelationships.length} in graph but absent from manifests (see STALE_RELATIONSHIPS below)`);
|
|
1717
|
+
}
|
|
1718
|
+
if (graphDiff && graphDiff.removedSubPackageKeys.length > 0) {
|
|
1719
|
+
console.log(` Removed pkgs : ${graphDiff.removedSubPackageKeys.length} sub-packages no longer on filesystem (see REMOVED_SUB_PACKAGES below)`);
|
|
1720
|
+
}
|
|
1770
1721
|
if (output.entityErrors.length > 0) {
|
|
1771
1722
|
console.log("\nEntity errors:");
|
|
1772
1723
|
for (const err of output.entityErrors) {
|
|
1773
1724
|
console.log(` ${err.externalKey}: ${err.error} — ${err.message}`);
|
|
1774
1725
|
}
|
|
1775
1726
|
}
|
|
1776
|
-
// ─── Enrichment
|
|
1777
|
-
console.log(
|
|
1727
|
+
// ─── Enrichment directive ───────────────────────────────────────────────────
|
|
1728
|
+
console.log(buildEnrichmentDirective());
|
|
1778
1729
|
logProgress("complete", `ok=${output.ok}`);
|
|
1779
1730
|
}
|
|
@@ -52,7 +52,7 @@ export async function updateEntity(args) {
|
|
|
52
52
|
console.error("error: batch entity updates were removed. Use explicit per-entity update-entity commands for enrichment quality.");
|
|
53
53
|
process.exit(1);
|
|
54
54
|
}
|
|
55
|
-
const externalKey = parseOptionValue(args, "--key");
|
|
55
|
+
const externalKey = parseOptionValue(args, "--key") ?? parseOptionValue(args, "--external-key");
|
|
56
56
|
const name = parseOptionValue(args, "--name");
|
|
57
57
|
const description = parseOptionValue(args, "--description");
|
|
58
58
|
const entityTypeCode = parseOptionValue(args, "--entity-type") ?? "application";
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ const commands = {
|
|
|
32
32
|
"init-agent": initAgent,
|
|
33
33
|
"agent-identify": agentIdentify,
|
|
34
34
|
"init-project": initProject,
|
|
35
|
+
"update-project": (args) => initProject(["--refresh", ...args]),
|
|
35
36
|
"update-entity": updateEntity,
|
|
36
37
|
"add-relationship": addRelationship,
|
|
37
38
|
"register-alias": registerAlias,
|
|
@@ -98,6 +99,19 @@ Usage:
|
|
|
98
99
|
--profile include timing/profile data in JSON output
|
|
99
100
|
--dry-run preview without writing
|
|
100
101
|
--json
|
|
102
|
+
nexarch update-project
|
|
103
|
+
Re-scan a previously registered project directory, refresh
|
|
104
|
+
entities and relationships in the graph, and diff the new scan
|
|
105
|
+
against the current graph state to surface stale relationships
|
|
106
|
+
and removed sub-packages for the calling agent to review.
|
|
107
|
+
Accepts all the same options as init-project plus:
|
|
108
|
+
--application-ref <entityRef> target project key (recommended)
|
|
109
|
+
--auto-map-application auto-select best-match application
|
|
110
|
+
Output includes enrichmentRequired.diff with:
|
|
111
|
+
newRelationships — detected but not yet in graph
|
|
112
|
+
staleRelationships — in graph but absent from manifests
|
|
113
|
+
removedSubPackages — previously registered, no longer on disk
|
|
114
|
+
--json
|
|
101
115
|
nexarch update-entity
|
|
102
116
|
Update the name and/or description of an existing graph entity.
|
|
103
117
|
Use this after init-project to enrich the entity with meaningful
|