nexarch 0.9.9 → 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.
- package/dist/commands/init-project.js +211 -267
- 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)) {
|
|
@@ -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,275 +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
|
-
|
|
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 / platform_component : --type "runs_on" (NOT uses or depends_on)
|
|
1552
|
-
any → model : --type "uses_model"
|
|
1553
|
-
|
|
1554
|
-
Pre-resolved dependencies per sub-package (already wired by init-project; review only):
|
|
1555
|
-
${subPackages.map((sp) => {
|
|
1556
|
-
const resolved = sp.depSpecs
|
|
1557
|
-
.map((d) => resolvedByInput.get(d.name))
|
|
1558
|
-
.filter((r) => !!r?.canonicalExternalRef);
|
|
1559
|
-
if (resolved.length === 0)
|
|
1560
|
-
return ` • ${sp.name}: (no pre-resolved deps — check package.json manually)`;
|
|
1561
|
-
const lines = resolved.map((r) => {
|
|
1562
|
-
const relType = pickRelationshipType(r.entityTypeCode, sp.entityType);
|
|
1563
|
-
const key = `${relType}::${sp.externalKey}::${r.canonicalExternalRef}`;
|
|
1564
|
-
const status = preWiredRelationshipKeys.has(key) ? "[pre-wired]" : "[not pre-wired]";
|
|
1565
|
-
return ` ${status} --from "${sp.externalKey}" --to "${r.canonicalExternalRef}" --type "${relType}" # ${r.canonicalName}`;
|
|
1566
|
-
});
|
|
1567
|
-
return ` • ${sp.name}:\n${lines.join("\n")}`;
|
|
1568
|
-
}).join("\n\n")}
|
|
1569
|
-
`;
|
|
1570
|
-
const adrStepNumber = subPackages.length > 0 ? "STEP 4" : "STEP 3";
|
|
1571
|
-
const finalStep = subPackages.length > 0 ? "STEP 5" : "STEP 4";
|
|
1572
|
-
const adrSection = `
|
|
1573
|
-
${adrStepNumber} — Ask the enrichment agent to extract ADRs and register decision records.
|
|
1574
|
-
|
|
1575
|
-
Do NOT rely on init-project to parse ADR files. The enrichment agent should read ADR markdown and
|
|
1576
|
-
explicitly register decision records with relationships.
|
|
1577
|
-
|
|
1578
|
-
Agent task:
|
|
1579
|
-
• Find ADR files (e.g. docs/adr, adrs, decisions, ADR-*.md)
|
|
1580
|
-
• For each ADR, create/update a decision record entity
|
|
1581
|
-
• Link it using: decision_record --decides--> application/sub-app
|
|
1582
|
-
|
|
1583
|
-
REQUIRED fields to pass per decision (strict):
|
|
1584
|
-
• key: decision_record:<project>-<adr-slug>
|
|
1585
|
-
• subtype: decision_architecture
|
|
1586
|
-
• name: ADR title
|
|
1587
|
-
• decision summary: short business summary (1–3 sentences)
|
|
1588
|
-
• decision detail: full decision detail in markdown (context + decision + consequences)
|
|
1589
|
-
|
|
1590
|
-
Command pattern:
|
|
1591
|
-
npx nexarch update-entity \\
|
|
1592
|
-
--key "decision_record:<project>-<adr-slug>" \\
|
|
1593
|
-
--entity-type "decision_record" \\
|
|
1594
|
-
--subtype "decision_architecture" \\
|
|
1595
|
-
--name "<ADR title>" \\
|
|
1596
|
-
--attributes-json '{"decision":{"summary":"<short summary>","detail":"## Context\\n<context>\\n\\n## Decision\\n<decision>\\n\\n## Consequences\\n<consequences>"}}'
|
|
1597
|
-
|
|
1598
|
-
npx nexarch add-relationship \\
|
|
1599
|
-
--from "decision_record:<project>-<adr-slug>" \\
|
|
1600
|
-
--to "<application-or-sub-app-key>" \\
|
|
1601
|
-
--type "decides"
|
|
1602
|
-
`;
|
|
1603
|
-
const gapCheckSection = `
|
|
1604
|
-
${finalStep} — Identify architecturally significant components not auto-detected.
|
|
1605
|
-
|
|
1606
|
-
The mechanical scan only finds declared dependencies, env var names, and a few config files.
|
|
1607
|
-
It systematically misses hosting platforms, managed infrastructure, SaaS integrations,
|
|
1608
|
-
and external APIs that are only mentioned in README or deployment config.
|
|
1609
|
-
|
|
1610
|
-
Review what you've read and ask yourself: are there any of the following that are
|
|
1611
|
-
clearly part of this system's architecture but weren't auto-detected?
|
|
1612
|
-
|
|
1613
|
-
• Hosting / deployment platforms (Vercel, Railway, Fly.io, AWS, GCP, Azure…)
|
|
1614
|
-
• Managed data services (Neon, PlanetScale, Supabase, Upstash, Atlas…)
|
|
1615
|
-
• Auth / identity providers (Clerk, Auth0, WorkOS, Okta…)
|
|
1616
|
-
• External SaaS integrations (Stripe, Resend, SendGrid, Twilio, Sentry…)
|
|
1617
|
-
• CDN / storage / queues (Cloudflare, S3, CloudFront, SQS, Redis…)
|
|
1618
|
-
• CI/CD platforms (GitHub Actions, GitLab CI, CircleCI…)
|
|
1619
|
-
|
|
1620
|
-
ONLY register entries you are highly confident about — i.e. explicitly mentioned in
|
|
1621
|
-
the README, a config file, or a deployment manifest. Do not guess.
|
|
1622
|
-
|
|
1623
|
-
For each one you identify:
|
|
1624
|
-
|
|
1625
|
-
1. Resolve it to a canonical reference:
|
|
1626
|
-
npx nexarch resolve-names --names "<platform name>" --json
|
|
1627
|
-
|
|
1628
|
-
2. If resolved, instantiate the entity in the graph (REQUIRED before add-relationship):
|
|
1629
|
-
npx nexarch update-entity \\
|
|
1630
|
-
--key "<resolved-ref>" \\
|
|
1631
|
-
--entity-type "<entityTypeCode from resolve-names>" \\
|
|
1632
|
-
--name "<canonicalName from resolve-names>"
|
|
1633
|
-
|
|
1634
|
-
3. Wire the relationship:
|
|
1635
|
-
npx nexarch add-relationship \\
|
|
1636
|
-
--from "${projectExternalKey}" \\
|
|
1637
|
-
--to "<resolved-ref>" \\
|
|
1638
|
-
--type "runs_on" # for platforms (Vercel, Neon, etc.)
|
|
1639
|
-
# use --type "depends_on" for external APIs/SaaS (Stripe, Resend, etc.)
|
|
1640
|
-
|
|
1641
|
-
4. If unresolved (not in the reference library), skip it for now — it will appear
|
|
1642
|
-
as a reference candidate on the next scan once you register an alias for it.
|
|
1643
|
-
`;
|
|
1644
|
-
const fallbackTemplate = `
|
|
1645
|
-
╔══════════════════════════════════════════════════════════════════╗
|
|
1646
|
-
║ ENRICHMENT TASK — action required ║
|
|
1647
|
-
╚══════════════════════════════════════════════════════════════════╝
|
|
1648
|
-
|
|
1649
|
-
The mechanical scan has written a skeleton graph entry for this project (NOT fully enriched).
|
|
1650
|
-
You now need to enrich it with meaningful content before considering registration complete.
|
|
1651
|
-
|
|
1652
|
-
PROJECT ENTITY : ${projectExternalKey}
|
|
1653
|
-
PROJECT DIR : ${dir}
|
|
1654
|
-
ECOSYSTEM(S) : ${ecosystemLabel}
|
|
1655
|
-
MANIFEST : ${manifestHint}
|
|
1656
|
-
${readmeHints.length > 0 ? `README FILES : ${readmeHints.join(", ")}` : "README FILES : (none found — check docs/)"}
|
|
1657
|
-
|
|
1658
|
-
IMPORTANT REPORTING RULE:
|
|
1659
|
-
Do NOT report "fully registered" or "fully done" after init-project alone.
|
|
1660
|
-
Correct status after init-project is: "skeleton created; enrichment pending" until Steps 1+ are completed.
|
|
1661
|
-
|
|
1662
|
-
STEP 1 — Read the project README / docs and build your understanding of:
|
|
1663
|
-
• What does this project do? (business purpose, key features)
|
|
1664
|
-
• Who are the users?
|
|
1665
|
-
• What is the high-level architecture?
|
|
1666
|
-
• Any notable technical decisions or constraints?
|
|
1667
|
-
|
|
1668
|
-
STEP 2 — Enrich the project entity. Run this command with the description you've written:
|
|
1669
|
-
|
|
1670
|
-
ENRICHMENT QUALITY RULE:
|
|
1671
|
-
• Do enrichment as explicit per-entity updates.
|
|
1672
|
-
• Do NOT shortcut with bulk entity updates for semantic enrichment.
|
|
1673
|
-
• The goal is accurate, evidence-based descriptions/subtypes per entity, not just write throughput.
|
|
1674
|
-
|
|
1675
|
-
npx nexarch update-entity \\
|
|
1676
|
-
--key "${projectExternalKey}" \\
|
|
1677
|
-
--entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? " \\\n --subtype \"app_custom_built\" \\\n --icon \"<curated icon>\"" : ""} \\
|
|
1678
|
-
--name "<proper product name from README>" \\
|
|
1679
|
-
--description "<2–4 sentence summary of what it does and why>"
|
|
1680
|
-
${subPkgSection}${adrSection}${gapCheckSection}`;
|
|
1681
|
-
return fallbackTemplate;
|
|
1682
|
-
}
|
|
1683
|
-
const enrichmentTask = {
|
|
1684
|
-
template: {
|
|
1685
|
-
code: "builtin:init-project-enrichment",
|
|
1686
|
-
source: "builtin",
|
|
1687
|
-
registryVersion: null,
|
|
1688
|
-
},
|
|
1689
|
-
instructions: buildEnrichmentInstructions(),
|
|
1690
|
-
iconHints: {
|
|
1691
|
-
provider: "lucide",
|
|
1692
|
-
note: "Use any Lucide icon name for agent-selected enrichment icons; omit icon when low confidence.",
|
|
1693
|
-
},
|
|
1694
|
-
projectEntity: {
|
|
1695
|
-
externalKey: projectExternalKey,
|
|
1696
|
-
entityType: entityTypeOverride,
|
|
1697
|
-
readmeFiles: readmeHints,
|
|
1698
|
-
},
|
|
1699
|
-
subPackages: subPackages.map((sp) => {
|
|
1700
|
-
const resolvedDeps = sp.depSpecs
|
|
1701
|
-
.map((d) => resolvedByInput.get(d.name))
|
|
1702
|
-
.filter((r) => !!r?.canonicalExternalRef)
|
|
1703
|
-
.map((r) => {
|
|
1704
|
-
const relationshipTypeCode = pickRelationshipType(r.entityTypeCode, sp.entityType);
|
|
1705
|
-
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
|
+
});
|
|
1706
1537
|
return {
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
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)),
|
|
1712
1546
|
};
|
|
1713
|
-
})
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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();
|
|
1725
1661
|
const output = {
|
|
1726
1662
|
ok: Number(entitiesResult.summary?.failed ?? 0) === 0,
|
|
1663
|
+
status: "enrichment_required",
|
|
1664
|
+
mode: refreshMode ? "refresh" : "init",
|
|
1727
1665
|
project: { name: displayName, externalKey: projectExternalKey, entityType: entityTypeOverride, detectedEcosystems },
|
|
1728
1666
|
entities: entitiesResult.summary ?? {},
|
|
1729
1667
|
relationships: relsResult?.summary ?? { requested: 0, succeeded: 0, failed: 0 },
|
|
@@ -1735,10 +1673,9 @@ ${subPkgSection}${adrSection}${gapCheckSection}`;
|
|
|
1735
1673
|
},
|
|
1736
1674
|
resolved: resolvedItems.length,
|
|
1737
1675
|
unresolved: unresolvedItems.length,
|
|
1738
|
-
unresolvedSample: unresolvedItems.slice(0, 10).map((r) => r.input),
|
|
1739
1676
|
entityErrors: entitiesResult.errors ?? [],
|
|
1740
1677
|
relationshipErrors: relsResult?.errors ?? [],
|
|
1741
|
-
|
|
1678
|
+
enrichmentRequired,
|
|
1742
1679
|
profile: profileEnabled
|
|
1743
1680
|
? {
|
|
1744
1681
|
startedAt: profile.startedAt,
|
|
@@ -1757,6 +1694,7 @@ ${subPkgSection}${adrSection}${gapCheckSection}`;
|
|
|
1757
1694
|
return;
|
|
1758
1695
|
}
|
|
1759
1696
|
console.log(`\nDone.`);
|
|
1697
|
+
console.log(` Mode : ${refreshMode ? "refresh (update-project)" : "init"}`);
|
|
1760
1698
|
console.log(` Entities : ${output.entities.succeeded ?? 0} written, ${output.entities.failed ?? 0} failed`);
|
|
1761
1699
|
console.log(` Relationships: ${output.relationships.succeeded ?? 0} written`);
|
|
1762
1700
|
console.log(" Status : skeleton created; enrichment pending");
|
|
@@ -1766,13 +1704,19 @@ ${subPkgSection}${adrSection}${gapCheckSection}`;
|
|
|
1766
1704
|
if (unresolvedItems.length > 0) {
|
|
1767
1705
|
console.log(` Candidates : ${unresolvedItems.length} added to reference candidates`);
|
|
1768
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
|
+
}
|
|
1769
1713
|
if (output.entityErrors.length > 0) {
|
|
1770
1714
|
console.log("\nEntity errors:");
|
|
1771
1715
|
for (const err of output.entityErrors) {
|
|
1772
1716
|
console.log(` ${err.externalKey}: ${err.error} — ${err.message}`);
|
|
1773
1717
|
}
|
|
1774
1718
|
}
|
|
1775
|
-
// ─── Enrichment
|
|
1776
|
-
console.log(
|
|
1719
|
+
// ─── Enrichment directive ───────────────────────────────────────────────────
|
|
1720
|
+
console.log(buildEnrichmentDirective());
|
|
1777
1721
|
logProgress("complete", `ok=${output.ok}`);
|
|
1778
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
|