nexarch 0.9.20 → 0.9.23

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.
@@ -444,7 +444,7 @@ function readRootPackage(pkgPath) {
444
444
  // Guess entity type + subtype for a sub-package based on its path and package.json scripts.
445
445
  function classifySubPackage(pkgPath, relativePath) {
446
446
  const topDir = relativePath.split("/")[0] ?? "";
447
- // packages/* → shared library/component
447
+ // packages/* → shared library/component (no server, not independently deployable)
448
448
  if (topDir === "packages") {
449
449
  const name = relativePath.split("/")[1] ?? "";
450
450
  if (name.includes("ui") || name.includes("components"))
@@ -453,9 +453,31 @@ function classifySubPackage(pkgPath, relativePath) {
453
453
  return { entityType: "technology_component", subtype: "tech_library" };
454
454
  return { entityType: "technology_component", subtype: "tech_library" };
455
455
  }
456
- // apps/* deployable application component under the root application.
456
+ // Read package.json once for script + bin signals used across all paths below.
457
+ let scripts = [];
458
+ let hasBin = false;
459
+ try {
460
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
461
+ scripts = Object.keys(pkg.scripts ?? {});
462
+ hasBin = pkg.bin !== undefined && pkg.bin !== null;
463
+ }
464
+ catch { }
465
+ const name = relativePath.toLowerCase();
466
+ const hasServerScript = scripts.some((s) => ["start", "dev", "serve"].includes(s));
467
+ const hasBuildScript = scripts.some((s) => s === "build");
468
+ // A bin entry means it is a CLI application regardless of path.
469
+ if (hasBin)
470
+ return { entityType: "application", subtype: "app_cli" };
471
+ // apps/* — treat as full application when it has its own server/dev script
472
+ // (independently deployable); fall back to component classification otherwise.
457
473
  if (topDir === "apps") {
458
- const name = relativePath.toLowerCase();
474
+ if (hasServerScript) {
475
+ if (name.includes("worker") || name.includes("job") || name.includes("etl") || name.includes("crawl")) {
476
+ return { entityType: "application", subtype: "app_integration_service" };
477
+ }
478
+ return { entityType: "application", subtype: "app_custom_built" };
479
+ }
480
+ // No server script → treat as a component of the parent application.
459
481
  if (name.includes("worker"))
460
482
  return { entityType: "application_component", subtype: "app_comp_worker" };
461
483
  if (name.includes("job"))
@@ -475,20 +497,13 @@ function classifySubPackage(pkgPath, relativePath) {
475
497
  return { entityType: "application_component", subtype: "app_comp_api" };
476
498
  return { entityType: "application_component", subtype: "app_comp_api" };
477
499
  }
478
- // src/* deployable application; check scripts
479
- try {
480
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
481
- const scripts = Object.keys(pkg.scripts ?? {});
482
- const hasServer = scripts.some((s) => ["start", "dev", "serve", "build"].includes(s));
483
- if (hasServer) {
484
- const name = relativePath.toLowerCase();
485
- if (name.includes("crawl") || name.includes("worker") || name.includes("job") || name.includes("etl")) {
486
- return { entityType: "application", subtype: "app_integration_service" };
487
- }
488
- return { entityType: "application", subtype: "app_custom_built" };
500
+ // Root-level or other paths use script signals to detect standalone applications.
501
+ if (hasServerScript || hasBuildScript) {
502
+ if (name.includes("crawl") || name.includes("worker") || name.includes("job") || name.includes("etl")) {
503
+ return { entityType: "application", subtype: "app_integration_service" };
489
504
  }
505
+ return { entityType: "application", subtype: "app_custom_built" };
490
506
  }
491
- catch { }
492
507
  return { entityType: "technology_component", subtype: "tech_library" };
493
508
  }
494
509
  // Resolve workspace glob patterns (e.g. "apps/*", "packages/*") to actual directories.
@@ -794,6 +809,14 @@ function scanEcosystems(dir) {
794
809
  function isApplicationLikeEntityType(entityType) {
795
810
  return entityType === "application" || entityType === "application_component";
796
811
  }
812
+ /** Returns the structural relationship that should be wired between the root project and this sub-package. */
813
+ function structuralRelForSubPackage(sp, projectExternalKey) {
814
+ if (sp.entityType === "application")
815
+ return { type: "composes", from: projectExternalKey, to: sp.externalKey };
816
+ if (sp.entityType === "application_component")
817
+ return { type: "part_of", from: sp.externalKey, to: projectExternalKey };
818
+ return null;
819
+ }
797
820
  function scanProject(dir) {
798
821
  const names = new Set();
799
822
  let projectName = basename(dir);
@@ -1367,8 +1390,8 @@ export async function initProject(args) {
1367
1390
  // Build relationships:
1368
1391
  // - Root-level deps → wired to the top-level project entity
1369
1392
  // - Sub-package deps → wired to each sub-package entity
1370
- // - Sub-packages that are apps → part_of the top-level project
1371
- // - Sub-package deps that resolve to another sub-package → integrates_with
1393
+ // - Sub-packages that are apps → root composes sub-app
1394
+ // - Sub-package deps that resolve to another sub-package → depends_on
1372
1395
  const relationships = [];
1373
1396
  const seenRelPairs = new Set();
1374
1397
  let relationshipAddAttempts = 0;
@@ -1403,19 +1426,13 @@ export async function initProject(args) {
1403
1426
  seenSubKeys.add(`rel:${sp.externalKey}`);
1404
1427
  // Wire structural relationship to the top-level project entity:
1405
1428
  // - application_component → part_of → root (component within a single deployable app)
1406
- // - application → root:
1407
- // product root: root composes sub-app (product is composed of its constituent apps)
1408
- // application root: sub-app part_of root (treat as a bounded sub-application)
1429
+ // - application → root composes sub-app (ontology only allows application on part_of FROM
1430
+ // for application_component/application_function, not application itself)
1409
1431
  if (sp.entityType === "application_component") {
1410
1432
  addRel("part_of", sp.externalKey, projectExternalKey);
1411
1433
  }
1412
1434
  else if (sp.entityType === "application") {
1413
- if (entityTypeOverride === "product") {
1414
- addRel("composes", projectExternalKey, sp.externalKey);
1415
- }
1416
- else {
1417
- addRel("part_of", sp.externalKey, projectExternalKey);
1418
- }
1435
+ addRel("composes", projectExternalKey, sp.externalKey);
1419
1436
  }
1420
1437
  // Wire each sub-package's resolved deps to itself
1421
1438
  for (const dep of sp.depSpecs) {
@@ -1572,8 +1589,16 @@ export async function initProject(args) {
1572
1589
  pendingSteps.push({
1573
1590
  step: stepNum++,
1574
1591
  action: "classify_sub_packages",
1575
- instruction: `Update each sub-package in classifyPackages with a confirmed entity type, subtype, name, and description.`,
1576
- command: `nexarch update-entity --key "<subPackageExternalKey>" --entity-type "<entityType>" --subtype "<subtype>" --name "..." --description "..."`,
1592
+ instruction: `For each sub-package in classifyPackages: (1) run update-entity to confirm type/subtype/name/description, then (2) immediately run add-relationship to wire the structural relationship. The external key includes the entity type as a prefix — if you change the entity type, the key changes (e.g. application_component:foo → application:foo). Always run update-entity before add-relationship for each package.`,
1593
+ commandTemplates: {
1594
+ updateEntity: `nexarch update-entity --key "<subPackageExternalKey>" --entity-type "<entityType>" --subtype "<subtype>" --name "..." --description "..."`,
1595
+ wireRelationship: `nexarch add-relationship --from "<from>" --to "<to>" --type <composes|part_of> (see structuralRelationship on each classifyPackages entry)`,
1596
+ },
1597
+ notes: [
1598
+ "application sub-packages: parent composes sub-app → add-relationship --from parent --to sub-app --type composes",
1599
+ "application_component sub-packages: component part_of parent → add-relationship --from sub-pkg --to parent --type part_of",
1600
+ "Do NOT wire the relationship before the entity exists at the correct key.",
1601
+ ],
1577
1602
  });
1578
1603
  }
1579
1604
  if (entityTypeOverride === "application") {
@@ -1582,8 +1607,8 @@ export async function initProject(args) {
1582
1607
  action: "discover_functions",
1583
1608
  instruction: `Review the codebase to identify discrete application functions (what the application does). Examine named modules, route layout, service boundaries, and any architecture documentation. Register functions as application_function entities with subtype core_function (primary business function), supporting_function (auxiliary/enablement), integration_function (external connectivity), or data_function (data processing). Only register functions clearly evidenced by the codebase — do not invent them.`,
1584
1609
  commandTemplates: {
1585
- updateEntity: `nexarch update-entity --key "application_function:${projectSlug}-<function-slug>" --entity-type application_function --subtype core_function --name "..." --description "..."`,
1586
- addRelationship: `nexarch add-relationship --from "application_function:${projectSlug}-<function-slug>" --to "${projectExternalKey}" --type part_of`,
1610
+ updateEntity: `nexarch update-entity --key "application_function:${projectSlug}_<function_slug>" --entity-type application_function --subtype core_function --name "..." --description "..."`,
1611
+ addRelationship: `nexarch add-relationship --from "application_function:${projectSlug}_<function_slug>" --to "${projectExternalKey}" --type part_of`,
1587
1612
  },
1588
1613
  });
1589
1614
  }
@@ -1602,8 +1627,8 @@ export async function initProject(args) {
1602
1627
  action: "register_decision_records",
1603
1628
  instruction: `Look for ADRs (docs/adr/, decisions/, ADR-*.md) and register each as a decision_record entity.`,
1604
1629
  commandTemplates: {
1605
- updateEntity: `nexarch update-entity --key "decision_record:${projectSlug}-<adr-slug>" --entity-type decision_record --subtype decision_architecture --name "..." --attributes-json '{"decision":{"summary":"...","detail":"..."}}'`,
1606
- addRelationship: `nexarch add-relationship --from "decision_record:${projectSlug}-<adr-slug>" --to "${projectExternalKey}" --type decides`,
1630
+ updateEntity: `nexarch update-entity --key "decision_record:${projectSlug}_<adr_slug>" --entity-type decision_record --subtype decision_architecture --name "..." --attributes-json '{"decision":{"summary":"...","detail":"..."}}'`,
1631
+ addRelationship: `nexarch add-relationship --from "decision_record:${projectSlug}_<adr_slug>" --to "${projectExternalKey}" --type decides`,
1607
1632
  },
1608
1633
  });
1609
1634
  return {
@@ -1630,6 +1655,7 @@ export async function initProject(args) {
1630
1655
  preWired: preWiredRelationshipKeys.has(relationshipKey),
1631
1656
  };
1632
1657
  });
1658
+ const structuralRel = structuralRelForSubPackage(sp, projectExternalKey);
1633
1659
  return {
1634
1660
  name: sp.name,
1635
1661
  relativePath: sp.relativePath,
@@ -1637,6 +1663,9 @@ export async function initProject(args) {
1637
1663
  heuristicEntityType: sp.entityType,
1638
1664
  heuristicSubtype: sp.subtype,
1639
1665
  confidence: subPackageConfidence(sp),
1666
+ structuralRelationship: structuralRel
1667
+ ? { ...structuralRel, note: "Wire AFTER update-entity. If entity type changes, update the key prefix in from/to before running add-relationship." }
1668
+ : null,
1640
1669
  resolvedDeps,
1641
1670
  unresolvedDeps: sp.depSpecs.map((d) => d.name).filter((d) => !resolvedByInput.has(d)),
1642
1671
  };
@@ -1692,6 +1721,10 @@ export async function initProject(args) {
1692
1721
  if (subPackages.length > 0) {
1693
1722
  lines.push("");
1694
1723
  lines.push(`CLASSIFY_THESE (${subPackages.length} sub-package(s) — read each, confirm type, then update):`);
1724
+ lines.push(` NOTE: The external key includes the entity type as a prefix (e.g. application:foo).`);
1725
+ lines.push(` If you change the entity type, the key changes too — update-entity will create an entity`);
1726
+ lines.push(` at the new key. Always wire the structural relationship AFTER the update-entity call,`);
1727
+ lines.push(` using the confirmed key. Do not wire relationships before the entity exists.`);
1695
1728
  for (const sp of subPackages) {
1696
1729
  const conf = subPackageConfidence(sp);
1697
1730
  lines.push(` ${sp.relativePath}`);
@@ -1701,7 +1734,12 @@ export async function initProject(args) {
1701
1734
  if (unresolvedDeps.length > 0) {
1702
1735
  lines.push(` unresolved deps (${unresolvedDeps.length}): ${unresolvedDeps.slice(0, 5).join(", ")}${unresolvedDeps.length > 5 ? ` … +${unresolvedDeps.length - 5} more` : ""}`);
1703
1736
  }
1704
- lines.push(` nexarch update-entity --key "${sp.externalKey}" --entity-type "${sp.entityType}" --subtype "${sp.subtype}" --name "..." --description "..."`);
1737
+ lines.push(` 1) nexarch update-entity --key "${sp.externalKey}" --entity-type "${sp.entityType}" --subtype "${sp.subtype}" --name "..." --description "..."`);
1738
+ const rel = structuralRelForSubPackage(sp, projectExternalKey);
1739
+ if (rel) {
1740
+ lines.push(` 2) nexarch add-relationship --from "${rel.from}" --to "${rel.to}" --type ${rel.type}`);
1741
+ lines.push(` (adjust key prefix if you changed the entity type above)`);
1742
+ }
1705
1743
  }
1706
1744
  }
1707
1745
  if (unresolvedItems.length > 0) {
@@ -1730,17 +1768,17 @@ export async function initProject(args) {
1730
1768
  lines.push(` integration_function (external connectivity), or data_function (data processing).`);
1731
1769
  lines.push(` Only register functions clearly evidenced by the codebase — do not invent them.`);
1732
1770
  lines.push(` For each one found:`);
1733
- lines.push(` nexarch update-entity --key "application_function:${projectExternalKey.split(":")[1] ?? "project"}-<function-slug>" --entity-type application_function --subtype core_function --name "..." --description "..."`);
1734
- lines.push(` nexarch add-relationship --from "application_function:${projectExternalKey.split(":")[1] ?? "project"}-<function-slug>" --to "${projectExternalKey}" --type part_of`);
1771
+ lines.push(` nexarch update-entity --key "application_function:${projectExternalKey.split(":")[1] ?? "project"}_<function_slug>" --entity-type application_function --subtype core_function --name "..." --description "..."`);
1772
+ lines.push(` nexarch add-relationship --from "application_function:${projectExternalKey.split(":")[1] ?? "project"}_<function_slug>" --to "${projectExternalKey}" --type part_of`);
1735
1773
  }
1736
1774
  lines.push(` ${step++}. Scan the READMEs for platforms/SaaS not auto-detected (Vercel, Neon, Stripe, etc.).`);
1737
1775
  lines.push(` For each found: nexarch resolve-names --names "..." --json → nexarch update-entity → nexarch add-relationship`);
1738
1776
  lines.push(` ${step++}. Look for ADRs (docs/adr/, decisions/, ADR-*.md) and register decision_record entities.`);
1739
- 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":"..."}}'`);
1777
+ 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":"..."}}'`);
1740
1778
  lines.push(` nexarch add-relationship --from "decision_record:..." --to "${projectExternalKey}" --type decides`);
1741
1779
  lines.push("");
1742
1780
  lines.push("RELATIONSHIP DIRECTION RULES (for sub-packages):");
1743
- lines.push(" apps/* (deployable components) → part_ofparent application");
1781
+ lines.push(" apps/* (deployable components) → parent composes sub-application");
1744
1782
  lines.push(" packages/* (shared libraries) → parent depends_on → library");
1745
1783
  lines.push(" Deps wired from manifests are already pre-wired; do not re-add unless key changed.");
1746
1784
  if (graphDiff) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.9.20",
3
+ "version": "0.9.23",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",