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.
@@ -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
- function buildEnrichmentInstructions() {
1463
- const ecosystemLabel = detectedEcosystems.length > 0
1464
- ? detectedEcosystems.join(", ")
1465
- : "unknown";
1466
- const manifestHint = detectedEcosystems.includes("nodejs")
1467
- ? "package.json"
1468
- : detectedEcosystems.includes("python")
1469
- ? "pyproject.toml / requirements.txt"
1470
- : detectedEcosystems.includes("go")
1471
- ? "go.mod"
1472
- : detectedEcosystems.includes("ruby")
1473
- ? "Gemfile"
1474
- : detectedEcosystems.includes("rust")
1475
- ? "Cargo.toml"
1476
- : "dependency manifest";
1477
- const subPkgSection = subPackages.length === 0 ? "" : `
1478
- STEP 3 — Classify and register each sub-package as its own entity.
1479
- The scanner found ${subPackages.length} workspace package(s):
1480
-
1481
- ${subPackages.map((sp) => ` • ${sp.name} (${sp.relativePath})`).join("\n")}
1482
-
1483
- For each one, READ its package.json and any README to understand what it actually is,
1484
- then choose the correct entity type and subtype before running update-entity:
1485
-
1486
- CLASSIFICATION GUIDE — pick the best fit:
1487
- Root project / top-level product application
1488
- --entity-type application --subtype app_custom_built
1489
- Deployable sub-app under apps/* with UI
1490
- → --entity-type application_component --subtype app_comp_ui
1491
- Deployable sub-app under apps/* exposing API/backend service
1492
- → --entity-type application_component --subtype app_comp_api
1493
- Deployable background service, worker, job, ETL, or data pipeline under apps/*
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
- canonicalExternalRef: r.canonicalExternalRef,
1709
- canonicalName: r.canonicalName,
1710
- entityTypeCode: r.entityTypeCode,
1711
- relationshipTypeCode,
1712
- preWired: preWiredRelationshipKeys.has(relationshipKey),
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
- return {
1716
- name: sp.name,
1717
- relativePath: sp.relativePath,
1718
- externalKey: sp.externalKey,
1719
- inferredEntityType: sp.entityType,
1720
- inferredSubtype: sp.subtype,
1721
- resolvedDeps,
1722
- unresolvedDeps: sp.depSpecs.map((d) => d.name).filter((d) => !resolvedByInput.has(d)),
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
- enrichmentTask,
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 task ────────────────────────────────────────────────────────
1777
- console.log(enrichmentTask.instructions);
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.9.10",
3
+ "version": "0.9.13",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",