nexarch 0.9.10 → 0.9.12

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)) {
@@ -1029,6 +1040,7 @@ export async function initProject(args) {
1029
1040
  const autoMapApplication = parseFlag(args, "--auto-map-application");
1030
1041
  const nonInteractive = parseFlag(args, "--non-interactive");
1031
1042
  const upsertBatchSize = Number(parseOptionValue(args, "--batch-size") ?? "10") || 10;
1043
+ const refreshMode = parseFlag(args, "--refresh");
1032
1044
  const creds = requireCredentials();
1033
1045
  const mcpOpts = { companyId: creds.companyId };
1034
1046
  if (!asJson)
@@ -1222,8 +1234,24 @@ export async function initProject(args) {
1222
1234
  }
1223
1235
  }
1224
1236
  logProgress("application.target", projectExternalKey);
1237
+ // In refresh mode, snapshot the current graph state for this project before writing,
1238
+ // so we can diff what changed and surface stale relationships to the agent.
1239
+ let currentOutgoingRels = [];
1240
+ let currentPartOfRels = [];
1241
+ if (refreshMode) {
1242
+ logProgress("refresh.fetch.start");
1243
+ if (!asJson)
1244
+ console.log("\nFetching current graph state…");
1245
+ const [outgoingRaw, partOfRaw] = await Promise.all([
1246
+ callMcpProfiled("nexarch_list_relationships", { companyId: creds.companyId, fromEntityExternalKey: projectExternalKey, limit: 500 }, { phase: "refresh.outgoing" }),
1247
+ callMcpProfiled("nexarch_list_relationships", { companyId: creds.companyId, toEntityExternalKey: projectExternalKey, relationshipTypeCode: "part_of", limit: 500 }, { phase: "refresh.partOf" }),
1248
+ ]);
1249
+ currentOutgoingRels = parseToolText(outgoingRaw).relationships ?? [];
1250
+ currentPartOfRels = parseToolText(partOfRaw).relationships ?? [];
1251
+ logProgress("refresh.fetch.done", `outgoing=${currentOutgoingRels.length}, partOf=${currentPartOfRels.length}`);
1252
+ }
1225
1253
  const agentContext = {
1226
- agentId: "nexarch-cli:init-project",
1254
+ agentId: refreshMode ? "nexarch-cli:update-project" : "nexarch-cli:init-project",
1227
1255
  agentRunId: `init-project-${Date.now()}`,
1228
1256
  repoRef,
1229
1257
  repoPath,
@@ -1393,6 +1421,22 @@ export async function initProject(args) {
1393
1421
  if (detectedRepo?.canonicalRepoRef) {
1394
1422
  addRel("depends_on", projectExternalKey, detectedRepo.canonicalRepoRef, 0.95);
1395
1423
  }
1424
+ let graphDiff = null;
1425
+ if (refreshMode) {
1426
+ const newRelKeySet = new Set(relationships.map((r) => `${r.relationshipTypeCode}::${r.fromEntityExternalKey}::${r.toEntityExternalKey}`));
1427
+ const currentRelKeySet = new Set(currentOutgoingRels
1428
+ .filter((r) => r.fromEntityExternalKey && r.toEntityExternalKey)
1429
+ .map((r) => `${r.relationshipTypeCode}::${r.fromEntityExternalKey}::${r.toEntityExternalKey}`));
1430
+ const newRelationshipsInDiff = relationships.filter((r) => !currentRelKeySet.has(`${r.relationshipTypeCode}::${r.fromEntityExternalKey}::${r.toEntityExternalKey}`));
1431
+ const staleRelationships = currentOutgoingRels.filter((r) => r.fromEntityExternalKey &&
1432
+ r.toEntityExternalKey &&
1433
+ !newRelKeySet.has(`${r.relationshipTypeCode}::${r.fromEntityExternalKey}::${r.toEntityExternalKey}`));
1434
+ const currentSubPackageKeys = new Set(currentPartOfRels.map((r) => r.fromEntityExternalKey).filter((k) => Boolean(k)));
1435
+ const newSubPackageKeys = new Set(subPackages.map((sp) => sp.externalKey));
1436
+ const removedSubPackageKeys = [...currentSubPackageKeys].filter((k) => !newSubPackageKeys.has(k));
1437
+ graphDiff = { newRelationships: newRelationshipsInDiff, staleRelationships, removedSubPackageKeys };
1438
+ logProgress("refresh.diff", `newRels=${newRelationshipsInDiff.length}, staleRels=${staleRelationships.length}, removedSubPkgs=${removedSubPackageKeys.length}`);
1439
+ }
1396
1440
  if (!asJson)
1397
1441
  console.log(`\nWriting to graph…`);
1398
1442
  // Upsert entities (chunked for progressive feedback)
@@ -1455,276 +1499,169 @@ export async function initProject(args) {
1455
1499
  }
1456
1500
  logProgress("upsert.relationships.done", `succeeded=${relsResult.summary?.succeeded ?? 0}, failed=${relsResult.summary?.failed ?? 0}`);
1457
1501
  }
1458
- // Build structured enrichment task (included in JSON output and printed in human mode)
1459
1502
  const readmeHints = ["README.md", "README.mdx", "docs/README.md", "docs/index.md"]
1460
1503
  .filter((f) => existsSync(join(dir, f)));
1461
1504
  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}`;
1505
+ // Confidence heuristic for sub-package classification based on path signals.
1506
+ function subPackageConfidence(sp) {
1507
+ if (sp.relativePath.startsWith("packages/"))
1508
+ return 0.75;
1509
+ if (sp.relativePath.startsWith("apps/"))
1510
+ return 0.70;
1511
+ return 0.60;
1512
+ }
1513
+ // Build the structured enrichment payload for JSON output.
1514
+ function buildEnrichmentPayload() {
1515
+ return {
1516
+ status: "enrichment_required",
1517
+ projectEntity: {
1518
+ externalKey: projectExternalKey,
1519
+ entityType: entityTypeOverride,
1520
+ },
1521
+ readFiles: readmeHints,
1522
+ classifyPackages: subPackages.map((sp) => {
1523
+ const resolvedDeps = sp.depSpecs
1524
+ .map((d) => resolvedByInput.get(d.name))
1525
+ .filter((r) => !!r?.canonicalExternalRef)
1526
+ .map((r) => {
1527
+ const relationshipTypeCode = pickRelationshipType(r.entityTypeCode, sp.entityType);
1528
+ const relationshipKey = `${relationshipTypeCode}::${sp.externalKey}::${r.canonicalExternalRef}`;
1529
+ return {
1530
+ canonicalExternalRef: r.canonicalExternalRef,
1531
+ canonicalName: r.canonicalName,
1532
+ entityTypeCode: r.entityTypeCode,
1533
+ relationshipTypeCode,
1534
+ preWired: preWiredRelationshipKeys.has(relationshipKey),
1535
+ };
1536
+ });
1707
1537
  return {
1708
- canonicalExternalRef: r.canonicalExternalRef,
1709
- canonicalName: r.canonicalName,
1710
- entityTypeCode: r.entityTypeCode,
1711
- relationshipTypeCode,
1712
- preWired: preWiredRelationshipKeys.has(relationshipKey),
1538
+ name: sp.name,
1539
+ relativePath: sp.relativePath,
1540
+ externalKey: sp.externalKey,
1541
+ heuristicEntityType: sp.entityType,
1542
+ heuristicSubtype: sp.subtype,
1543
+ confidence: subPackageConfidence(sp),
1544
+ resolvedDeps,
1545
+ unresolvedDeps: sp.depSpecs.map((d) => d.name).filter((d) => !resolvedByInput.has(d)),
1713
1546
  };
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
- };
1547
+ }),
1548
+ unresolvedNames: unresolvedItems.map((r) => r.input),
1549
+ iconHints: {
1550
+ provider: "lucide",
1551
+ note: "Use any Lucide icon name (kebab-case); omit when confidence is low.",
1552
+ },
1553
+ ...(graphDiff
1554
+ ? {
1555
+ diff: {
1556
+ newRelationships: graphDiff.newRelationships.map((r) => ({
1557
+ type: r.relationshipTypeCode,
1558
+ from: r.fromEntityExternalKey,
1559
+ to: r.toEntityExternalKey,
1560
+ })),
1561
+ staleRelationships: graphDiff.staleRelationships.map((r) => ({
1562
+ type: r.relationshipTypeCode,
1563
+ from: r.fromEntityExternalKey,
1564
+ to: r.toEntityExternalKey,
1565
+ fromName: r.fromEntityName ?? null,
1566
+ toName: r.toEntityName ?? null,
1567
+ })),
1568
+ removedSubPackageKeys: graphDiff.removedSubPackageKeys,
1569
+ },
1570
+ }
1571
+ : {}),
1572
+ };
1573
+ }
1574
+ // Build an agent-directed directive block for human/terminal output.
1575
+ // This is addressed to the *calling agent* (Claude Code, Codex, etc.) and tells it
1576
+ // to act inline in this same session — not to queue enrichment for later.
1577
+ function buildEnrichmentDirective() {
1578
+ const lines = [];
1579
+ lines.push("─── ENRICHMENT REQUIRED ─────────────────────────────────────────────────");
1580
+ lines.push("init-project wrote a skeleton. You must complete enrichment NOW, inline,");
1581
+ lines.push("in this same session. Do NOT report the project as registered until done.");
1582
+ lines.push("");
1583
+ lines.push(`PROJECT : ${projectExternalKey}`);
1584
+ lines.push(`DIR : ${dir}`);
1585
+ if (readmeHints.length > 0) {
1586
+ lines.push("");
1587
+ lines.push("READ_THESE first:");
1588
+ for (const f of readmeHints)
1589
+ lines.push(` ${join(dir, f)}`);
1590
+ }
1591
+ else {
1592
+ lines.push("");
1593
+ lines.push("READ_THESE first: (no README found — check docs/ for any markdown overview)");
1594
+ }
1595
+ if (subPackages.length > 0) {
1596
+ lines.push("");
1597
+ lines.push(`CLASSIFY_THESE (${subPackages.length} sub-package(s) — read each, confirm type, then update):`);
1598
+ for (const sp of subPackages) {
1599
+ const conf = subPackageConfidence(sp);
1600
+ lines.push(` ${sp.relativePath}`);
1601
+ lines.push(` key : ${sp.externalKey}`);
1602
+ lines.push(` inferred : ${sp.entityType} / ${sp.subtype} (confidence ${conf.toFixed(2)})`);
1603
+ const unresolvedDeps = sp.depSpecs.map((d) => d.name).filter((d) => !resolvedByInput.has(d));
1604
+ if (unresolvedDeps.length > 0) {
1605
+ lines.push(` unresolved deps (${unresolvedDeps.length}): ${unresolvedDeps.slice(0, 5).join(", ")}${unresolvedDeps.length > 5 ? ` … +${unresolvedDeps.length - 5} more` : ""}`);
1606
+ }
1607
+ lines.push(` → nexarch update-entity --key "${sp.externalKey}" --entity-type "${sp.entityType}" --subtype "${sp.subtype}" --name "..." --description "..."`);
1608
+ }
1609
+ }
1610
+ if (unresolvedItems.length > 0) {
1611
+ lines.push("");
1612
+ lines.push(`UNRESOLVED (${unresolvedItems.length} names not matched in reference library):`);
1613
+ const sample = unresolvedItems.slice(0, 20).map((r) => r.input);
1614
+ lines.push(` ${JSON.stringify(sample)}`);
1615
+ if (unresolvedItems.length > 20)
1616
+ lines.push(` … and ${unresolvedItems.length - 20} more`);
1617
+ }
1618
+ lines.push("");
1619
+ lines.push("THEN_RUN (once you have read the READMEs):");
1620
+ let step = 1;
1621
+ lines.push(` ${step++}. nexarch update-entity --key "${projectExternalKey}" --entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? ' --subtype "app_custom_built" --icon "<lucide-icon>"' : ""} --name "..." --description "..."`);
1622
+ if (subPackages.length > 0) {
1623
+ lines.push(` ${step++}. Update each sub-package listed under CLASSIFY_THESE above.`);
1624
+ }
1625
+ lines.push(` ${step++}. Scan the READMEs for platforms/SaaS not auto-detected (Vercel, Neon, Stripe, etc.).`);
1626
+ lines.push(` For each found: nexarch resolve-names --names "..." --json → nexarch update-entity → nexarch add-relationship`);
1627
+ lines.push(` ${step++}. Look for ADRs (docs/adr/, decisions/, ADR-*.md) and register decision_record entities.`);
1628
+ 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":"..."}}'`);
1629
+ lines.push(` nexarch add-relationship --from "decision_record:..." --to "${projectExternalKey}" --type decides`);
1630
+ lines.push("");
1631
+ lines.push("RELATIONSHIP DIRECTION RULES (for sub-packages):");
1632
+ lines.push(" apps/* (deployable components) → part_of → parent application");
1633
+ lines.push(" packages/* (shared libraries) → parent depends_on → library");
1634
+ lines.push(" Deps wired from manifests are already pre-wired; do not re-add unless key changed.");
1635
+ if (graphDiff) {
1636
+ if (graphDiff.staleRelationships.length > 0) {
1637
+ lines.push("");
1638
+ lines.push(`STALE_RELATIONSHIPS (${graphDiff.staleRelationships.length} — in graph but no longer in manifests; review and retire if confirmed removed):`);
1639
+ for (const r of graphDiff.staleRelationships) {
1640
+ const from = r.fromEntityExternalKey ?? r.fromEntityName ?? "?";
1641
+ const to = r.toEntityExternalKey ?? r.toEntityName ?? "?";
1642
+ lines.push(` [${r.relationshipTypeCode}] ${from} → ${to}`);
1643
+ }
1644
+ }
1645
+ if (graphDiff.removedSubPackageKeys.length > 0) {
1646
+ lines.push("");
1647
+ lines.push(`REMOVED_SUB_PACKAGES (${graphDiff.removedSubPackageKeys.length} — previously registered but no longer found in filesystem; retire or reassign):`);
1648
+ for (const k of graphDiff.removedSubPackageKeys)
1649
+ lines.push(` ${k}`);
1650
+ }
1651
+ if (graphDiff.staleRelationships.length === 0 && graphDiff.removedSubPackageKeys.length === 0) {
1652
+ lines.push("");
1653
+ lines.push("DIFF: No stale relationships or removed sub-packages detected.");
1654
+ }
1655
+ }
1656
+ lines.push("");
1657
+ lines.push("─────────────────────────────────────────────────────────────────────────");
1658
+ return lines.join("\n");
1659
+ }
1660
+ const enrichmentRequired = buildEnrichmentPayload();
1726
1661
  const output = {
1727
1662
  ok: Number(entitiesResult.summary?.failed ?? 0) === 0,
1663
+ status: "enrichment_required",
1664
+ mode: refreshMode ? "refresh" : "init",
1728
1665
  project: { name: displayName, externalKey: projectExternalKey, entityType: entityTypeOverride, detectedEcosystems },
1729
1666
  entities: entitiesResult.summary ?? {},
1730
1667
  relationships: relsResult?.summary ?? { requested: 0, succeeded: 0, failed: 0 },
@@ -1736,10 +1673,9 @@ ${subPkgSection}${adrSection}${gapCheckSection}`;
1736
1673
  },
1737
1674
  resolved: resolvedItems.length,
1738
1675
  unresolved: unresolvedItems.length,
1739
- unresolvedSample: unresolvedItems.slice(0, 10).map((r) => r.input),
1740
1676
  entityErrors: entitiesResult.errors ?? [],
1741
1677
  relationshipErrors: relsResult?.errors ?? [],
1742
- enrichmentTask,
1678
+ enrichmentRequired,
1743
1679
  profile: profileEnabled
1744
1680
  ? {
1745
1681
  startedAt: profile.startedAt,
@@ -1758,6 +1694,7 @@ ${subPkgSection}${adrSection}${gapCheckSection}`;
1758
1694
  return;
1759
1695
  }
1760
1696
  console.log(`\nDone.`);
1697
+ console.log(` Mode : ${refreshMode ? "refresh (update-project)" : "init"}`);
1761
1698
  console.log(` Entities : ${output.entities.succeeded ?? 0} written, ${output.entities.failed ?? 0} failed`);
1762
1699
  console.log(` Relationships: ${output.relationships.succeeded ?? 0} written`);
1763
1700
  console.log(" Status : skeleton created; enrichment pending");
@@ -1767,13 +1704,19 @@ ${subPkgSection}${adrSection}${gapCheckSection}`;
1767
1704
  if (unresolvedItems.length > 0) {
1768
1705
  console.log(` Candidates : ${unresolvedItems.length} added to reference candidates`);
1769
1706
  }
1707
+ if (graphDiff && graphDiff.staleRelationships.length > 0) {
1708
+ console.log(` Stale rels : ${graphDiff.staleRelationships.length} in graph but absent from manifests (see STALE_RELATIONSHIPS below)`);
1709
+ }
1710
+ if (graphDiff && graphDiff.removedSubPackageKeys.length > 0) {
1711
+ console.log(` Removed pkgs : ${graphDiff.removedSubPackageKeys.length} sub-packages no longer on filesystem (see REMOVED_SUB_PACKAGES below)`);
1712
+ }
1770
1713
  if (output.entityErrors.length > 0) {
1771
1714
  console.log("\nEntity errors:");
1772
1715
  for (const err of output.entityErrors) {
1773
1716
  console.log(` ${err.externalKey}: ${err.error} — ${err.message}`);
1774
1717
  }
1775
1718
  }
1776
- // ─── Enrichment task ────────────────────────────────────────────────────────
1777
- console.log(enrichmentTask.instructions);
1719
+ // ─── Enrichment directive ───────────────────────────────────────────────────
1720
+ console.log(buildEnrichmentDirective());
1778
1721
  logProgress("complete", `ok=${output.ok}`);
1779
1722
  }
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.12",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",