@webpieces/nx-webpieces-rules 0.3.347 → 0.3.349

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/executors.json CHANGED
@@ -15,11 +15,6 @@
15
15
  "schema": "./src/executors/visualize-runtime/schema.json",
16
16
  "description": "Render the runtime microservice graph (runtime-dependencies.json)"
17
17
  },
18
- "validate-runtime-markers": {
19
- "implementation": "./src/executors/validate-runtime-markers/executor",
20
- "schema": "./src/executors/validate-runtime-markers/schema.json",
21
- "description": "Per-project: validate live.json matches the project's api-project dependencies"
22
- },
23
18
  "validate-runtime-architecture": {
24
19
  "implementation": "./src/executors/validate-runtime-architecture/executor",
25
20
  "schema": "./src/executors/validate-runtime-architecture/schema.json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/nx-webpieces-rules",
3
- "version": "0.3.347",
3
+ "version": "0.3.349",
4
4
  "description": "Nx-specific webpieces validation rules and graph tooling. Bundles all @webpieces rule packages with Nx graph validators and an inference plugin.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -21,11 +21,11 @@
21
21
  "README.md"
22
22
  ],
23
23
  "dependencies": {
24
- "@webpieces/ai-hook-rules": "0.3.347",
25
- "@webpieces/code-rules": "0.3.347",
26
- "@webpieces/eslint-rules": "0.3.347",
27
- "@webpieces/pr-gate": "0.3.347",
28
- "@webpieces/rules-config": "0.3.347",
24
+ "@webpieces/ai-hook-rules": "0.3.349",
25
+ "@webpieces/code-rules": "0.3.349",
26
+ "@webpieces/eslint-rules": "0.3.349",
27
+ "@webpieces/pr-gate": "0.3.349",
28
+ "@webpieces/rules-config": "0.3.349",
29
29
  "madge": "8.0.0"
30
30
  },
31
31
  "peerDependencies": {
@@ -19,14 +19,15 @@ const graph_visualizer_1 = require("../../lib/graph-visualizer");
19
19
  const runtime_graph_1 = require("../../lib/runtime-graph");
20
20
  const toError_1 = require("../../toError");
21
21
  /**
22
- * Generate the runtime microservice graph alongside the compile-time graph, from the SAME source
23
- * scan (the derived apiRelations) — one regenerate produces both committed files. rpc APIs become
24
- * direct runtime edges; pubsub APIs become edges the viz draws through a queue.
22
+ * Generate the runtime microservice graph alongside the compile-time graph, DERIVED from the same
23
+ * dependencies.json (its per-project apiRelations) — one regenerate produces both committed files, and
24
+ * validate derives from the SAME source so they can't diverge. rpc APIs become direct runtime edges;
25
+ * pubsub APIs become edges the viz draws through a queue.
25
26
  */
26
27
  // webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file
27
- function generateRuntimeGraph(workspaceRoot, scan, hiddenProjects) {
28
- console.log('📡 Generating runtime graph from the source scan (implements × uses per API)...');
29
- const runtimeGraph = (0, runtime_graph_1.assembleRuntimeGraphFromScan)(scan, hiddenProjects);
28
+ function generateRuntimeGraph(workspaceRoot, graph, hiddenProjects) {
29
+ console.log('📡 Deriving runtime graph from dependencies.json (implements × uses per API)...');
30
+ const runtimeGraph = (0, runtime_graph_1.deriveRuntimeGraph)(graph, hiddenProjects);
30
31
  (0, runtime_graph_1.saveRuntimeGraph)(runtimeGraph, workspaceRoot);
31
32
  const serviceCount = Object.keys(runtimeGraph.services).length;
32
33
  console.log(`✅ Runtime graph saved (${serviceCount} services, ${runtimeGraph.runtimeEdges.length} runtime edges)`);
@@ -53,7 +54,7 @@ async function runExecutor(options, context) {
53
54
  // scanning source, so dependencies.json + the viz + the runtime graph all
54
55
  // read the same derived truth.
55
56
  console.log('🔎 Scanning source for implements/uses API relations...');
56
- const scanResult = (0, api_scanner_1.scanAndAttachApiRelations)(workspaceRoot, enhancedGraph, projectInfos);
57
+ (0, api_scanner_1.scanAndAttachApiRelations)(workspaceRoot, enhancedGraph, projectInfos);
57
58
  // Step 4: Save the graph
58
59
  console.log('💾 Saving graph to architecture/dependencies.json...');
59
60
  (0, graph_loader_1.saveGraph)(enhancedGraph, workspaceRoot, graphPath);
@@ -70,7 +71,7 @@ async function runExecutor(options, context) {
70
71
  if (enhancedGraph[name].drawOnGraph === false)
71
72
  hiddenProjects.add(name);
72
73
  }
73
- generateRuntimeGraph(workspaceRoot, scanResult, hiddenProjects);
74
+ generateRuntimeGraph(workspaceRoot, enhancedGraph, hiddenProjects);
74
75
  // Print summary
75
76
  const projectCount = Object.keys(enhancedGraph).length;
76
77
  const levels = new Set(Object.values(enhancedGraph).map((e) => e.level));
@@ -1 +1 @@
1
- {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/generate/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;AAsCH,8BAqEC;AAxGD,0DAAwD;AACxD,+DAAiE;AACjE,yDAAgE;AAChE,yDAAmD;AACnD,6DAAoG;AACpG,iEAA4E;AAE5E,iEAA6D;AAC7D,2DAAyF;AACzF,2CAAwC;AAUxC;;;;GAIG;AACH,2GAA2G;AAC3G,SAAS,oBAAoB,CAAC,aAAqB,EAAE,IAAmB,EAAE,cAA2B;IACjG,OAAO,CAAC,GAAG,CAAC,iFAAiF,CAAC,CAAC;IAC/F,MAAM,YAAY,GAAG,IAAA,4CAA4B,EAAC,IAAI,EAAE,cAAc,CAAC,CAAC;IACxE,IAAA,gCAAgB,EAAC,YAAY,EAAE,aAAa,CAAC,CAAC;IAC9C,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;IAC/D,OAAO,CAAC,GAAG,CACP,0BAA0B,YAAY,cAAc,YAAY,CAAC,YAAY,CAAC,MAAM,iBAAiB,CACxG,CAAC;AACN,CAAC;AAEc,KAAK,UAAU,WAAW,CACrC,OAAgC,EAChC,OAAwB;IAExB,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;IACpC,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAEnC,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;IAEnD,8DAA8D;IAC9D,IAAI,CAAC;QACD,gFAAgF;QAChF,OAAO,CAAC,GAAG,CAAC,2DAA2D,CAAC,CAAC;QACzE,MAAM,YAAY,GAAG,MAAM,IAAA,sCAAoB,GAAE,CAAC;QAElD,gEAAgE;QAChE,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;QAClD,MAAM,aAAa,GAAG,IAAA,qCAAsB,EAAC,YAAY,CAAC,CAAC;QAE3D,qEAAqE;QACrE,uEAAuE;QACvE,qEAAqE;QACrE,OAAO,CAAC,GAAG,CAAC,oEAAoE,CAAC,CAAC;QAClF,MAAM,YAAY,GAAG,MAAM,IAAA,mCAAkB,GAAE,CAAC;QAChD,IAAA,4BAAW,EAAC,aAAa,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC;QAExD,wEAAwE;QACxE,0EAA0E;QAC1E,+BAA+B;QAC/B,OAAO,CAAC,GAAG,CAAC,yDAAyD,CAAC,CAAC;QACvE,MAAM,UAAU,GAAG,IAAA,uCAAyB,EAAC,aAAa,EAAE,aAAa,EAAE,YAAY,CAAC,CAAC;QAEzF,yBAAyB;QACzB,OAAO,CAAC,GAAG,CAAC,sDAAsD,CAAC,CAAC;QACpE,IAAA,wBAAS,EAAC,aAAa,EAAE,aAAa,EAAE,SAAS,CAAC,CAAC;QACnD,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;QAE1C,wEAAwE;QACxE,qEAAqE;QACrE,MAAM,QAAQ,GAAG,IAAI,kCAAe,EAAE,CAAC,kBAAkB,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;QACxF,OAAO,CAAC,GAAG,CAAC,WAAW,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;QAE5C,sEAAsE;QACtE,wEAAwE;QACxE,yEAAyE;QACzE,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAC;QACzC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC;YAC5C,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,WAAW,KAAK,KAAK;gBAAE,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC5E,CAAC;QACD,oBAAoB,CAAC,aAAa,EAAE,UAAU,EAAE,cAAc,CAAC,CAAC;QAEhE,gBAAgB;QAChB,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,MAAM,CAAC;QACvD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QACzE,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;QACnC,OAAO,CAAC,GAAG,CAAC,gBAAgB,YAAY,EAAE,CAAC,CAAC;QAC5C,OAAO,CAAC,GAAG,CAAC,cAAc,MAAM,CAAC,IAAI,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAEpE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC7B,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QAC3D,IAAI,KAAK,YAAY,wCAAuB,EAAE,CAAC;YAC3C,MAAM,MAAM,GAAG,IAAA,4BAAa,EAAC,aAAa,EAAE,+BAA+B,CAAC,CAAC;YAC7E,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAClB,OAAO,CAAC,KAAK,CAAC,mBAAmB,GAAG,MAAM,GAAG,qDAAqD,CAAC,CAAC;QACxG,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;AACL,CAAC","sourcesContent":["/**\n * Generate Executor\n *\n * Generates the architecture dependency graph and saves it to architecture/dependencies.json.\n *\n * Usage:\n * nx run architecture:generate\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { writeTemplate } from '@webpieces/rules-config';\nimport { generateReducedGraph } from '../../lib/graph-generator';\nimport { sortGraphTopologically } from '../../lib/graph-sorter';\nimport { saveGraph } from '../../lib/graph-loader';\nimport { collectProjectInfo, enrichGraph, MetadataValidationError } from '../../lib/graph-metadata';\nimport { scanAndAttachApiRelations } from '../../lib/api-usage/api-scanner';\nimport type { ApiScanResult } from '../../lib/api-usage/api-scanner';\nimport { GraphVisualizer } from '../../lib/graph-visualizer';\nimport { assembleRuntimeGraphFromScan, saveRuntimeGraph } from '../../lib/runtime-graph';\nimport { toError } from '../../toError';\n\nexport interface GenerateExecutorOptions {\n graphPath?: string;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\n/**\n * Generate the runtime microservice graph alongside the compile-time graph, from the SAME source\n * scan (the derived apiRelations) — one regenerate produces both committed files. rpc APIs become\n * direct runtime edges; pubsub APIs become edges the viz draws through a queue.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction generateRuntimeGraph(workspaceRoot: string, scan: ApiScanResult, hiddenProjects: Set<string>): void {\n console.log('📡 Generating runtime graph from the source scan (implements × uses per API)...');\n const runtimeGraph = assembleRuntimeGraphFromScan(scan, hiddenProjects);\n saveRuntimeGraph(runtimeGraph, workspaceRoot);\n const serviceCount = Object.keys(runtimeGraph.services).length;\n console.log(\n `✅ Runtime graph saved (${serviceCount} services, ${runtimeGraph.runtimeEdges.length} runtime edges)`,\n );\n}\n\nexport default async function runExecutor(\n options: GenerateExecutorOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const graphPath = options.graphPath;\n const workspaceRoot = context.root;\n\n console.log('\\n📊 Architecture Graph Generator\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // Step 1: Build the full graph from nx, then transitively reduce it to the view\n console.log(\"📊 Generating dependency graph from nx's project graph...\");\n const reducedGraph = await generateReducedGraph();\n\n // Step 2: Topological sort (to assign levels for visualization)\n console.log('🔄 Computing topological layers...');\n const enhancedGraph = sortGraphTopologically(reducedGraph);\n\n // Step 3: Enrich with AI metadata (framework, shortDescription, file\n // pointers). This VALIDATES (responsibilities.md required per project)\n // and throws before any write, so a failure never clobbers the file.\n console.log('🏷️ Enriching graph with framework + responsibilities metadata...');\n const projectInfos = await collectProjectInfo();\n enrichGraph(enhancedGraph, projectInfos, workspaceRoot);\n\n // Step 3b: Classify each api-lib edge (implements/uses + rpc/pubsub) by\n // scanning source, so dependencies.json + the viz + the runtime graph all\n // read the same derived truth.\n console.log('🔎 Scanning source for implements/uses API relations...');\n const scanResult = scanAndAttachApiRelations(workspaceRoot, enhancedGraph, projectInfos);\n\n // Step 4: Save the graph\n console.log('💾 Saving graph to architecture/dependencies.json...');\n saveGraph(enhancedGraph, workspaceRoot, graphPath);\n console.log('✅ Graph saved successfully');\n\n // Step 4b: Write the committed, clickable HTML view next to the JSON so\n // dependencies.html regenerates in lock-step with dependencies.json.\n const vizPaths = new GraphVisualizer().writeVisualization(enhancedGraph, workspaceRoot);\n console.log(`✅ Wrote ${vizPaths.htmlPath}`);\n\n // Step 5: Generate the runtime microservice graph from the same scan.\n // Projects tagged drawOnGraph:false are threaded through so the runtime\n // graph hides them too (they stay flagged in runtime-dependencies.json).\n const hiddenProjects = new Set<string>();\n for (const name of Object.keys(enhancedGraph)) {\n if (enhancedGraph[name].drawOnGraph === false) hiddenProjects.add(name);\n }\n generateRuntimeGraph(workspaceRoot, scanResult, hiddenProjects);\n\n // Print summary\n const projectCount = Object.keys(enhancedGraph).length;\n const levels = new Set(Object.values(enhancedGraph).map((e) => e.level));\n console.log(`\\n📈 Graph Summary:`);\n console.log(` Projects: ${projectCount}`);\n console.log(` Levels: ${levels.size} (0-${Math.max(...levels)})`);\n\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n console.error('❌ Graph generation failed:', error.message);\n if (error instanceof MetadataValidationError) {\n const mdPath = writeTemplate(workspaceRoot, 'webpieces.responsibilities.md');\n console.error('');\n console.error('⚠️ *** Refer to ' + mdPath + ' for how to author responsibilities.md files *** ⚠️');\n }\n return { success: false };\n }\n}\n"]}
1
+ {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/generate/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Generate Executor\n *\n * Generates the architecture dependency graph and saves it to architecture/dependencies.json.\n *\n * Usage:\n * nx run architecture:generate\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { writeTemplate } from '@webpieces/rules-config';\nimport { generateReducedGraph } from '../../lib/graph-generator';\nimport { sortGraphTopologically } from '../../lib/graph-sorter';\nimport { saveGraph } from '../../lib/graph-loader';\nimport { collectProjectInfo, enrichGraph, MetadataValidationError } from '../../lib/graph-metadata';\nimport { scanAndAttachApiRelations } from '../../lib/api-usage/api-scanner';\nimport type { EnhancedGraph } from '../../lib/graph-sorter';\nimport { GraphVisualizer } from '../../lib/graph-visualizer';\nimport { deriveRuntimeGraph, saveRuntimeGraph } from '../../lib/runtime-graph';\nimport { toError } from '../../toError';\n\nexport interface GenerateExecutorOptions {\n graphPath?: string;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\n/**\n * Generate the runtime microservice graph alongside the compile-time graph, DERIVED from the same\n * dependencies.json (its per-project apiRelations) — one regenerate produces both committed files, and\n * validate derives from the SAME source so they can't diverge. rpc APIs become direct runtime edges;\n * pubsub APIs become edges the viz draws through a queue.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction generateRuntimeGraph(workspaceRoot: string, graph: EnhancedGraph, hiddenProjects: Set<string>): void {\n console.log('📡 Deriving runtime graph from dependencies.json (implements × uses per API)...');\n const runtimeGraph = deriveRuntimeGraph(graph, hiddenProjects);\n saveRuntimeGraph(runtimeGraph, workspaceRoot);\n const serviceCount = Object.keys(runtimeGraph.services).length;\n console.log(\n `✅ Runtime graph saved (${serviceCount} services, ${runtimeGraph.runtimeEdges.length} runtime edges)`,\n );\n}\n\nexport default async function runExecutor(\n options: GenerateExecutorOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const graphPath = options.graphPath;\n const workspaceRoot = context.root;\n\n console.log('\\n📊 Architecture Graph Generator\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // Step 1: Build the full graph from nx, then transitively reduce it to the view\n console.log(\"📊 Generating dependency graph from nx's project graph...\");\n const reducedGraph = await generateReducedGraph();\n\n // Step 2: Topological sort (to assign levels for visualization)\n console.log('🔄 Computing topological layers...');\n const enhancedGraph = sortGraphTopologically(reducedGraph);\n\n // Step 3: Enrich with AI metadata (framework, shortDescription, file\n // pointers). This VALIDATES (responsibilities.md required per project)\n // and throws before any write, so a failure never clobbers the file.\n console.log('🏷️ Enriching graph with framework + responsibilities metadata...');\n const projectInfos = await collectProjectInfo();\n enrichGraph(enhancedGraph, projectInfos, workspaceRoot);\n\n // Step 3b: Classify each api-lib edge (implements/uses + rpc/pubsub) by\n // scanning source, so dependencies.json + the viz + the runtime graph all\n // read the same derived truth.\n console.log('🔎 Scanning source for implements/uses API relations...');\n scanAndAttachApiRelations(workspaceRoot, enhancedGraph, projectInfos);\n\n // Step 4: Save the graph\n console.log('💾 Saving graph to architecture/dependencies.json...');\n saveGraph(enhancedGraph, workspaceRoot, graphPath);\n console.log('✅ Graph saved successfully');\n\n // Step 4b: Write the committed, clickable HTML view next to the JSON so\n // dependencies.html regenerates in lock-step with dependencies.json.\n const vizPaths = new GraphVisualizer().writeVisualization(enhancedGraph, workspaceRoot);\n console.log(`✅ Wrote ${vizPaths.htmlPath}`);\n\n // Step 5: Generate the runtime microservice graph from the same scan.\n // Projects tagged drawOnGraph:false are threaded through so the runtime\n // graph hides them too (they stay flagged in runtime-dependencies.json).\n const hiddenProjects = new Set<string>();\n for (const name of Object.keys(enhancedGraph)) {\n if (enhancedGraph[name].drawOnGraph === false) hiddenProjects.add(name);\n }\n generateRuntimeGraph(workspaceRoot, enhancedGraph, hiddenProjects);\n\n // Print summary\n const projectCount = Object.keys(enhancedGraph).length;\n const levels = new Set(Object.values(enhancedGraph).map((e) => e.level));\n console.log(`\\n📈 Graph Summary:`);\n console.log(` Projects: ${projectCount}`);\n console.log(` Levels: ${levels.size} (0-${Math.max(...levels)})`);\n\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n console.error('❌ Graph generation failed:', error.message);\n if (error instanceof MetadataValidationError) {\n const mdPath = writeTemplate(workspaceRoot, 'webpieces.responsibilities.md');\n console.error('');\n console.error('⚠️ *** Refer to ' + mdPath + ' for how to author responsibilities.md files *** ⚠️');\n }\n return { success: false };\n }\n}\n"]}
@@ -15,7 +15,7 @@
15
15
  */
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.default = runExecutor;
18
- const runtime_markers_1 = require("../../lib/runtime-markers");
18
+ const graph_loader_1 = require("../../lib/graph-loader");
19
19
  const runtime_graph_1 = require("../../lib/runtime-graph");
20
20
  const runtime_cycles_1 = require("../../lib/runtime-cycles");
21
21
  const runtime_config_1 = require("../../lib/runtime-config");
@@ -64,13 +64,22 @@ async function runExecutor(_options, context) {
64
64
  console.log(`\n⏭️ Skipping ${runtime_config_1.RUNTIME_RULE_NAME} (mode: OFF)\n`);
65
65
  return { success: true };
66
66
  }
67
- if (config.servicePaths.length === 0) {
68
- console.log(`\n⏭️ ${runtime_config_1.RUNTIME_RULE_NAME}: no servicePaths configured — nothing to validate\n`);
69
- return { success: true };
70
- }
71
67
  console.log('\n📡 Validating runtime microservice architecture\n');
72
- const model = await (0, runtime_markers_1.buildWorkspaceModel)(workspaceRoot, config.apiProjectPaths, config.servicePaths);
73
- const graph = (0, runtime_graph_1.assembleRuntimeGraph)(model, workspaceRoot);
68
+ // Derive the "current" runtime graph from the committed dependencies.json — the SAME source
69
+ // architecture:generate derives from so the unchanged-check compares like-for-like and can
70
+ // never fail on a clean, freshly-generated tree. (validate-architecture-unchanged separately
71
+ // guarantees dependencies.json itself is fresh.)
72
+ const depsFile = (0, graph_loader_1.loadBlessedGraph)(workspaceRoot);
73
+ if (depsFile === null) {
74
+ console.error('❌ No architecture/dependencies.json — run: nx run architecture:generate');
75
+ return { success: false };
76
+ }
77
+ const hiddenProjects = new Set();
78
+ for (const name of Object.keys(depsFile.projects)) {
79
+ if (depsFile.projects[name].drawOnGraph === false)
80
+ hiddenProjects.add(name);
81
+ }
82
+ const graph = (0, runtime_graph_1.deriveRuntimeGraph)(depsFile.projects, hiddenProjects);
74
83
  const problems = checkCycles(graph, config.allowedCycles);
75
84
  const unchanged = checkUnchanged(workspaceRoot, graph);
76
85
  if (unchanged)
@@ -1 +1 @@
1
- {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-runtime-architecture/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate Runtime Architecture Executor (workspace)\n *\n * Two workspace-level checks on the runtime microservice graph:\n * 1. No-cycles: every runtime cycle must be in the `allowedCycles` allowlist\n * with an unexpired `until`; any other cycle fails the build.\n * 2. Unchanged: the freshly-assembled graph must match the committed\n * architecture/runtime-dependencies.json (run `architecture:generate`).\n *\n * On/off + a whole-rule grace window come from webpieces.config.json\n * (rule: runtime-architecture).\n *\n * Usage: nx run architecture:validate-runtime-architecture\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { buildWorkspaceModel } from '../../lib/runtime-markers';\nimport {\n assembleRuntimeGraph,\n loadRuntimeGraph,\n runtimeAdjacency,\n runtimeGraphFileExists,\n serializeRuntimeGraph,\n} from '../../lib/runtime-graph';\nimport type { RuntimeGraph } from '../../lib/runtime-graph';\nimport { findRuntimeCycles, cycleKey } from '../../lib/runtime-cycles';\nimport type { AllowedCycle } from '../../lib/runtime-config';\nimport { loadRuntimeConfig, runtimeReportOnly, RUNTIME_RULE_NAME } from '../../lib/runtime-config';\n\nexport interface ValidateRuntimeArchitectureOptions {\n // Config comes from webpieces.config.json at runtime.\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\n/** Allowlist keys (sorted-services -> entry) that are still in their grace window. */\nfunction activeAllowedKeys(allowed: AllowedCycle[]): Map<string, AllowedCycle> {\n const nowSeconds = Date.now() / 1000;\n const map = new Map<string, AllowedCycle>();\n for (const entry of allowed) {\n const active = entry.until === undefined || nowSeconds < entry.until;\n if (active) map.set(cycleKey(entry.services), entry);\n }\n return map;\n}\n\n/** Returns disallowed-cycle messages (empty = all cycles allowed or none). */\nfunction checkCycles(graph: RuntimeGraph, allowed: AllowedCycle[]): string[] {\n const cycles = findRuntimeCycles(runtimeAdjacency(graph));\n if (cycles.length === 0) return [];\n\n const activeAllow = activeAllowedKeys(allowed);\n const problems: string[] = [];\n for (const cycle of cycles) {\n const entry = activeAllow.get(cycle.key);\n if (entry) {\n console.log(`⏳ Allowed runtime cycle [${cycle.services.join(' <-> ')}] — ${entry.reason ?? 'no reason given'}`);\n continue;\n }\n problems.push(`runtime cycle: ${cycle.services.join(' -> ')} -> ${cycle.services[0]}`);\n }\n return problems;\n}\n\n/** Returns an unchanged-check message, or null if the committed graph matches. */\nfunction checkUnchanged(workspaceRoot: string, current: RuntimeGraph): string | null {\n if (!runtimeGraphFileExists(workspaceRoot)) {\n return 'No committed architecture/runtime-dependencies.json — run: nx run architecture:generate';\n }\n const saved = loadRuntimeGraph(workspaceRoot);\n if (saved && serializeRuntimeGraph(saved) === serializeRuntimeGraph(current)) return null;\n return 'Runtime graph changed since last commit — run: nx run architecture:generate and commit the result';\n}\n\nexport default async function runExecutor(\n _options: ValidateRuntimeArchitectureOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n const workspaceRoot = context.root;\n const config = loadRuntimeConfig(workspaceRoot);\n\n if (config.off) {\n console.log(`\\n⏭️ Skipping ${RUNTIME_RULE_NAME} (mode: OFF)\\n`);\n return { success: true };\n }\n if (config.servicePaths.length === 0) {\n console.log(`\\n⏭️ ${RUNTIME_RULE_NAME}: no servicePaths configured nothing to validate\\n`);\n return { success: true };\n }\n\n console.log('\\n📡 Validating runtime microservice architecture\\n');\n const model = await buildWorkspaceModel(workspaceRoot, config.apiProjectPaths, config.servicePaths);\n const graph = assembleRuntimeGraph(model, workspaceRoot);\n\n const problems: string[] = checkCycles(graph, config.allowedCycles);\n const unchanged = checkUnchanged(workspaceRoot, graph);\n if (unchanged) problems.push(unchanged);\n\n for (const u of graph.unresolvedUses) {\n console.log(`⚠️ ${u.service} uses \"${u.api}\" but no in-repo service implements it (external?)`);\n }\n\n if (problems.length === 0) {\n console.log('✅ Runtime architecture valid (no disallowed cycles, graph unchanged)\\n');\n return { success: true };\n }\n\n console.error('\\n❌ Runtime architecture validation failed:\\n');\n for (const p of problems) console.error(` - ${p}`);\n console.error('\\nAllow a cycle temporarily via runtime-architecture.allowedCycles (services + reason + until) in webpieces.config.json.\\n');\n\n const reportOnly = runtimeReportOnly(config);\n if (reportOnly.skip) {\n console.log(`⏳ Reported but not failing (${reportOnly.reason}).\\n`);\n return { success: true };\n }\n return { success: false };\n}\n"]}
1
+ {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-runtime-architecture/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate Runtime Architecture Executor (workspace)\n *\n * Two workspace-level checks on the runtime microservice graph:\n * 1. No-cycles: every runtime cycle must be in the `allowedCycles` allowlist\n * with an unexpired `until`; any other cycle fails the build.\n * 2. Unchanged: the freshly-assembled graph must match the committed\n * architecture/runtime-dependencies.json (run `architecture:generate`).\n *\n * On/off + a whole-rule grace window come from webpieces.config.json\n * (rule: runtime-architecture).\n *\n * Usage: nx run architecture:validate-runtime-architecture\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { loadBlessedGraph } from '../../lib/graph-loader';\nimport {\n deriveRuntimeGraph,\n loadRuntimeGraph,\n runtimeAdjacency,\n runtimeGraphFileExists,\n serializeRuntimeGraph,\n} from '../../lib/runtime-graph';\nimport type { RuntimeGraph } from '../../lib/runtime-graph';\nimport { findRuntimeCycles, cycleKey } from '../../lib/runtime-cycles';\nimport type { AllowedCycle } from '../../lib/runtime-config';\nimport { loadRuntimeConfig, runtimeReportOnly, RUNTIME_RULE_NAME } from '../../lib/runtime-config';\n\nexport interface ValidateRuntimeArchitectureOptions {\n // Config comes from webpieces.config.json at runtime.\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\n/** Allowlist keys (sorted-services -> entry) that are still in their grace window. */\nfunction activeAllowedKeys(allowed: AllowedCycle[]): Map<string, AllowedCycle> {\n const nowSeconds = Date.now() / 1000;\n const map = new Map<string, AllowedCycle>();\n for (const entry of allowed) {\n const active = entry.until === undefined || nowSeconds < entry.until;\n if (active) map.set(cycleKey(entry.services), entry);\n }\n return map;\n}\n\n/** Returns disallowed-cycle messages (empty = all cycles allowed or none). */\nfunction checkCycles(graph: RuntimeGraph, allowed: AllowedCycle[]): string[] {\n const cycles = findRuntimeCycles(runtimeAdjacency(graph));\n if (cycles.length === 0) return [];\n\n const activeAllow = activeAllowedKeys(allowed);\n const problems: string[] = [];\n for (const cycle of cycles) {\n const entry = activeAllow.get(cycle.key);\n if (entry) {\n console.log(`⏳ Allowed runtime cycle [${cycle.services.join(' <-> ')}] — ${entry.reason ?? 'no reason given'}`);\n continue;\n }\n problems.push(`runtime cycle: ${cycle.services.join(' -> ')} -> ${cycle.services[0]}`);\n }\n return problems;\n}\n\n/** Returns an unchanged-check message, or null if the committed graph matches. */\nfunction checkUnchanged(workspaceRoot: string, current: RuntimeGraph): string | null {\n if (!runtimeGraphFileExists(workspaceRoot)) {\n return 'No committed architecture/runtime-dependencies.json — run: nx run architecture:generate';\n }\n const saved = loadRuntimeGraph(workspaceRoot);\n if (saved && serializeRuntimeGraph(saved) === serializeRuntimeGraph(current)) return null;\n return 'Runtime graph changed since last commit — run: nx run architecture:generate and commit the result';\n}\n\nexport default async function runExecutor(\n _options: ValidateRuntimeArchitectureOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n const workspaceRoot = context.root;\n const config = loadRuntimeConfig(workspaceRoot);\n\n if (config.off) {\n console.log(`\\n⏭️ Skipping ${RUNTIME_RULE_NAME} (mode: OFF)\\n`);\n return { success: true };\n }\n\n console.log('\\n📡 Validating runtime microservice architecture\\n');\n // Derive the \"current\" runtime graph from the committed dependencies.json — the SAME source\n // architecture:generate derives from — so the unchanged-check compares like-for-like and can\n // never fail on a clean, freshly-generated tree. (validate-architecture-unchanged separately\n // guarantees dependencies.json itself is fresh.)\n const depsFile = loadBlessedGraph(workspaceRoot);\n if (depsFile === null) {\n console.error('❌ No architecture/dependencies.jsonrun: nx run architecture:generate');\n return { success: false };\n }\n const hiddenProjects = new Set<string>();\n for (const name of Object.keys(depsFile.projects)) {\n if (depsFile.projects[name].drawOnGraph === false) hiddenProjects.add(name);\n }\n const graph = deriveRuntimeGraph(depsFile.projects, hiddenProjects);\n\n const problems: string[] = checkCycles(graph, config.allowedCycles);\n const unchanged = checkUnchanged(workspaceRoot, graph);\n if (unchanged) problems.push(unchanged);\n\n for (const u of graph.unresolvedUses) {\n console.log(`⚠️ ${u.service} uses \"${u.api}\" but no in-repo service implements it (external?)`);\n }\n\n if (problems.length === 0) {\n console.log('✅ Runtime architecture valid (no disallowed cycles, graph unchanged)\\n');\n return { success: true };\n }\n\n console.error('\\n❌ Runtime architecture validation failed:\\n');\n for (const p of problems) console.error(` - ${p}`);\n console.error('\\nAllow a cycle temporarily via runtime-architecture.allowedCycles (services + reason + until) in webpieces.config.json.\\n');\n\n const reportOnly = runtimeReportOnly(config);\n if (reportOnly.skip) {\n console.log(`⏳ Reported but not failing (${reportOnly.reason}).\\n`);\n return { success: true };\n }\n return { success: false };\n}\n"]}
@@ -6,7 +6,6 @@
6
6
  *
7
7
  * "runtime-architecture": {
8
8
  * "mode": "ON", // "OFF" disables the whole feature
9
- * "apiProjectPaths": ["libraries/apis/*"],
10
9
  * "ignoreModifiedUntilEpoch": 0, // whole-rule punt (epoch seconds)
11
10
  * "allowedCycles": [ { "services": ["a","b"], "reason": "...", "until": 1771931925 } ]
12
11
  * }
@@ -20,8 +19,6 @@ export interface AllowedCycle {
20
19
  }
21
20
  export interface RuntimeRuleConfig {
22
21
  off: boolean;
23
- apiProjectPaths: string[];
24
- servicePaths: string[];
25
22
  ignoreModifiedUntilEpoch?: number;
26
23
  ignoreRuleWhileOnBranch?: string;
27
24
  allowedCycles: AllowedCycle[];
@@ -7,7 +7,6 @@
7
7
  *
8
8
  * "runtime-architecture": {
9
9
  * "mode": "ON", // "OFF" disables the whole feature
10
- * "apiProjectPaths": ["libraries/apis/*"],
11
10
  * "ignoreModifiedUntilEpoch": 0, // whole-rule punt (epoch seconds)
12
11
  * "allowedCycles": [ { "services": ["a","b"], "reason": "...", "until": 1771931925 } ]
13
12
  * }
@@ -30,8 +29,6 @@ function loadRuntimeConfig(workspaceRoot) {
30
29
  const raw = (rule?.options ?? {});
31
30
  return {
32
31
  off: rule?.isOff ?? false,
33
- apiProjectPaths: Array.isArray(raw.apiProjectPaths) ? raw.apiProjectPaths : [],
34
- servicePaths: Array.isArray(raw.servicePaths) ? raw.servicePaths : [],
35
32
  ignoreModifiedUntilEpoch: typeof raw.ignoreModifiedUntilEpoch === 'number' ? raw.ignoreModifiedUntilEpoch : undefined,
36
33
  ignoreRuleWhileOnBranch: typeof raw.ignoreRuleWhileOnBranch === 'string' ? raw.ignoreRuleWhileOnBranch : undefined,
37
34
  allowedCycles: Array.isArray(raw.allowedCycles) ? raw.allowedCycles.filter(isUsableCycle) : [],
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-config.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-config.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;;AAuCH,8CAcC;AAQD,8CAEC;AAMD,sCAGC;AAGD,8BAEC;AA3ED,0DAA0F;AAE7E,QAAA,iBAAiB,GAAG,sBAAsB,CAAC;AA8BxD,SAAS,aAAa,CAAC,KAAmB;IACtC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;AACtE,CAAC;AAED,sEAAsE;AACtE,SAAgB,iBAAiB,CAAC,aAAqB;IACnD,MAAM,MAAM,GAAG,IAAA,8BAAe,EAAC,aAAa,CAAC,CAAC,QAAQ,CAAC;IACvD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,yBAAiB,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,EAAE,CAAmB,CAAC;IACpD,OAAO;QACH,GAAG,EAAE,IAAI,EAAE,KAAK,IAAI,KAAK;QACzB,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE;QAC9E,YAAY,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE;QACrE,wBAAwB,EACpB,OAAO,GAAG,CAAC,wBAAwB,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC,CAAC,SAAS;QAC/F,uBAAuB,EACnB,OAAO,GAAG,CAAC,uBAAuB,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC,CAAC,SAAS;QAC7F,aAAa,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE;KACjG,CAAC;AACN,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,MAAyB;IACvD,OAAO,IAAA,6BAAc,EAAC,MAAM,CAAC,wBAAwB,EAAE,MAAM,CAAC,uBAAuB,CAAC,CAAC;AAC3F,CAAC;AAED;;;GAGG;AACH,SAAgB,aAAa,CAAC,KAAyB;IACnD,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,KAAK,CAAC;AACrC,CAAC;AAED,wDAAwD;AACxD,SAAgB,SAAS,CAAC,KAAa;IACnC,OAAO,IAAI,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9D,CAAC","sourcesContent":["/**\n * Runtime Config\n *\n * Loads the `runtime-architecture` rule from webpieces.config.json and exposes\n * typed accessors shared by the generate + validate + visualize executors.\n *\n * \"runtime-architecture\": {\n * \"mode\": \"ON\", // \"OFF\" disables the whole feature\n * \"apiProjectPaths\": [\"libraries/apis/*\"],\n * \"ignoreModifiedUntilEpoch\": 0, // whole-rule punt (epoch seconds)\n * \"allowedCycles\": [ { \"services\": [\"a\",\"b\"], \"reason\": \"...\", \"until\": 1771931925 } ]\n * }\n */\n\nimport { loadAndValidate, shouldSkipRule, SkipRuleResult } from '@webpieces/rules-config';\n\nexport const RUNTIME_RULE_NAME = 'runtime-architecture';\n\nexport interface AllowedCycle {\n services: string[];\n reason?: string;\n until?: number;\n}\n\nexport interface RuntimeRuleConfig {\n off: boolean;\n apiProjectPaths: string[];\n servicePaths: string[];\n ignoreModifiedUntilEpoch?: number;\n ignoreRuleWhileOnBranch?: string;\n allowedCycles: AllowedCycle[];\n}\n\n/**\n * Typed view of the opaque webpieces.config.json option bag for this rule. The\n * config is trusted (it is the workspace's own file), so we cast once here and\n * defensively narrow arrays/numbers rather than threading `unknown` everywhere.\n */\ninterface RuntimeRuleRaw {\n apiProjectPaths?: string[];\n servicePaths?: string[];\n ignoreModifiedUntilEpoch?: number;\n ignoreRuleWhileOnBranch?: string;\n allowedCycles?: AllowedCycle[];\n}\n\nfunction isUsableCycle(cycle: AllowedCycle): boolean {\n return Array.isArray(cycle.services) && cycle.services.length > 0;\n}\n\n/** Load the runtime-architecture rule config (with safe defaults). */\nexport function loadRuntimeConfig(workspaceRoot: string): RuntimeRuleConfig {\n const shared = loadAndValidate(workspaceRoot).resolved;\n const rule = shared.rules.get(RUNTIME_RULE_NAME);\n const raw = (rule?.options ?? {}) as RuntimeRuleRaw;\n return {\n off: rule?.isOff ?? false,\n apiProjectPaths: Array.isArray(raw.apiProjectPaths) ? raw.apiProjectPaths : [],\n servicePaths: Array.isArray(raw.servicePaths) ? raw.servicePaths : [],\n ignoreModifiedUntilEpoch:\n typeof raw.ignoreModifiedUntilEpoch === 'number' ? raw.ignoreModifiedUntilEpoch : undefined,\n ignoreRuleWhileOnBranch:\n typeof raw.ignoreRuleWhileOnBranch === 'string' ? raw.ignoreRuleWhileOnBranch : undefined,\n allowedCycles: Array.isArray(raw.allowedCycles) ? raw.allowedCycles.filter(isUsableCycle) : [],\n };\n}\n\n/**\n * Whole-rule report-only window honoring BOTH escape hatches: skip while on the\n * named branch (ignoreRuleWhileOnBranch) or until the epoch passes\n * (ignoreModifiedUntilEpoch). When `.skip` is true, problems are reported but\n * the build is not failed.\n */\nexport function runtimeReportOnly(config: RuntimeRuleConfig): SkipRuleResult {\n return shouldSkipRule(config.ignoreModifiedUntilEpoch, config.ignoreRuleWhileOnBranch);\n}\n\n/**\n * Whole-rule grace window: while now < epoch, failures are reported but do not\n * fail the build (warn). Mirrors the other webpieces rules.\n */\nexport function isGraceActive(epoch: number | undefined): boolean {\n if (epoch === undefined) return false;\n return Date.now() / 1000 < epoch;\n}\n\n/** Format the epoch as an ISO date for log messages. */\nexport function epochDate(epoch: number): string {\n return new Date(epoch * 1000).toISOString().split('T')[0];\n}\n"]}
1
+ {"version":3,"file":"runtime-config.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-config.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;AAmCH,8CAYC;AAQD,8CAEC;AAMD,sCAGC;AAGD,8BAEC;AArED,0DAA0F;AAE7E,QAAA,iBAAiB,GAAG,sBAAsB,CAAC;AA0BxD,SAAS,aAAa,CAAC,KAAmB;IACtC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;AACtE,CAAC;AAED,sEAAsE;AACtE,SAAgB,iBAAiB,CAAC,aAAqB;IACnD,MAAM,MAAM,GAAG,IAAA,8BAAe,EAAC,aAAa,CAAC,CAAC,QAAQ,CAAC;IACvD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,yBAAiB,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,EAAE,CAAmB,CAAC;IACpD,OAAO;QACH,GAAG,EAAE,IAAI,EAAE,KAAK,IAAI,KAAK;QACzB,wBAAwB,EACpB,OAAO,GAAG,CAAC,wBAAwB,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC,CAAC,SAAS;QAC/F,uBAAuB,EACnB,OAAO,GAAG,CAAC,uBAAuB,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC,CAAC,SAAS;QAC7F,aAAa,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE;KACjG,CAAC;AACN,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,MAAyB;IACvD,OAAO,IAAA,6BAAc,EAAC,MAAM,CAAC,wBAAwB,EAAE,MAAM,CAAC,uBAAuB,CAAC,CAAC;AAC3F,CAAC;AAED;;;GAGG;AACH,SAAgB,aAAa,CAAC,KAAyB;IACnD,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,KAAK,CAAC;AACrC,CAAC;AAED,wDAAwD;AACxD,SAAgB,SAAS,CAAC,KAAa;IACnC,OAAO,IAAI,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9D,CAAC","sourcesContent":["/**\n * Runtime Config\n *\n * Loads the `runtime-architecture` rule from webpieces.config.json and exposes\n * typed accessors shared by the generate + validate + visualize executors.\n *\n * \"runtime-architecture\": {\n * \"mode\": \"ON\", // \"OFF\" disables the whole feature\n * \"ignoreModifiedUntilEpoch\": 0, // whole-rule punt (epoch seconds)\n * \"allowedCycles\": [ { \"services\": [\"a\",\"b\"], \"reason\": \"...\", \"until\": 1771931925 } ]\n * }\n */\n\nimport { loadAndValidate, shouldSkipRule, SkipRuleResult } from '@webpieces/rules-config';\n\nexport const RUNTIME_RULE_NAME = 'runtime-architecture';\n\nexport interface AllowedCycle {\n services: string[];\n reason?: string;\n until?: number;\n}\n\nexport interface RuntimeRuleConfig {\n off: boolean;\n ignoreModifiedUntilEpoch?: number;\n ignoreRuleWhileOnBranch?: string;\n allowedCycles: AllowedCycle[];\n}\n\n/**\n * Typed view of the opaque webpieces.config.json option bag for this rule. The\n * config is trusted (it is the workspace's own file), so we cast once here and\n * defensively narrow arrays/numbers rather than threading `unknown` everywhere.\n */\ninterface RuntimeRuleRaw {\n ignoreModifiedUntilEpoch?: number;\n ignoreRuleWhileOnBranch?: string;\n allowedCycles?: AllowedCycle[];\n}\n\nfunction isUsableCycle(cycle: AllowedCycle): boolean {\n return Array.isArray(cycle.services) && cycle.services.length > 0;\n}\n\n/** Load the runtime-architecture rule config (with safe defaults). */\nexport function loadRuntimeConfig(workspaceRoot: string): RuntimeRuleConfig {\n const shared = loadAndValidate(workspaceRoot).resolved;\n const rule = shared.rules.get(RUNTIME_RULE_NAME);\n const raw = (rule?.options ?? {}) as RuntimeRuleRaw;\n return {\n off: rule?.isOff ?? false,\n ignoreModifiedUntilEpoch:\n typeof raw.ignoreModifiedUntilEpoch === 'number' ? raw.ignoreModifiedUntilEpoch : undefined,\n ignoreRuleWhileOnBranch:\n typeof raw.ignoreRuleWhileOnBranch === 'string' ? raw.ignoreRuleWhileOnBranch : undefined,\n allowedCycles: Array.isArray(raw.allowedCycles) ? raw.allowedCycles.filter(isUsableCycle) : [],\n };\n}\n\n/**\n * Whole-rule report-only window honoring BOTH escape hatches: skip while on the\n * named branch (ignoreRuleWhileOnBranch) or until the epoch passes\n * (ignoreModifiedUntilEpoch). When `.skip` is true, problems are reported but\n * the build is not failed.\n */\nexport function runtimeReportOnly(config: RuntimeRuleConfig): SkipRuleResult {\n return shouldSkipRule(config.ignoreModifiedUntilEpoch, config.ignoreRuleWhileOnBranch);\n}\n\n/**\n * Whole-rule grace window: while now < epoch, failures are reported but do not\n * fail the build (warn). Mirrors the other webpieces rules.\n */\nexport function isGraceActive(epoch: number | undefined): boolean {\n if (epoch === undefined) return false;\n return Date.now() / 1000 < epoch;\n}\n\n/** Format the epoch as an ISO date for log messages. */\nexport function epochDate(epoch: number): string {\n return new Date(epoch * 1000).toISOString().split('T')[0];\n}\n"]}
@@ -1,16 +1,18 @@
1
1
  /**
2
2
  * Runtime Graph
3
3
  *
4
- * Assembles the runtime microservice graph from the per-service
5
- * `service-contract.json` files, and saves/loads the committed
6
- * `architecture/runtime-dependencies.json`.
4
+ * Derives the runtime microservice graph SOLELY from architecture/dependencies.json
5
+ * (the single source of truth): each project's `apiRelations` carry which api
6
+ * classes it implements/uses and their transport (rpc | pubsub). Both
7
+ * `architecture:generate` and `architecture:validate-runtime-architecture` call
8
+ * the SAME `deriveRuntimeGraph`, so the committed graph and the validated graph
9
+ * can never diverge.
7
10
  *
8
11
  * The runtime edge Z -> X (Z depends on X at runtime) is INFERRED: Z `uses` api
9
12
  * Y and X `implements` api Y. This edge does not exist in the compile-time
10
13
  * dependencies.json (both Z and X only compile-depend on the api library Y).
11
14
  */
12
- import type { WorkspaceModel } from './runtime-markers';
13
- import type { ApiScanResult } from './api-usage/api-scanner';
15
+ import type { EnhancedGraph } from './graph-sorter';
14
16
  import type { ApiTransport } from './api-usage/api-relations';
15
17
  export declare const DEFAULT_RUNTIME_GRAPH_PATH = "architecture/runtime-dependencies.json";
16
18
  export interface RuntimeService {
@@ -56,14 +58,13 @@ export interface RuntimeGraph {
56
58
  }
57
59
  /** Adjacency (service -> [targets]) from a loaded runtime graph. */
58
60
  export declare function runtimeAdjacency(graph: RuntimeGraph): Record<string, string[]>;
59
- /** Assemble the full runtime graph from the workspace model + service contracts. */
60
- export declare function assembleRuntimeGraph(model: WorkspaceModel, workspaceRoot: string): RuntimeGraph;
61
61
  /**
62
- * Assemble the runtime graph from the source scan (the derived apiRelations).
63
- * `hiddenProjects` (drawOnGraph:false, defaults to none) are kept in the graph
64
- * but flagged so the runtime visualizer omits their nodes + edges.
62
+ * Derive the runtime graph from dependencies.json's project `apiRelations` the
63
+ * single source of truth shared by generate + validate. `hiddenProjects`
64
+ * (drawOnGraph:false, defaults to none) are kept in the graph but flagged so the
65
+ * runtime visualizer omits their nodes + edges.
65
66
  */
66
- export declare function assembleRuntimeGraphFromScan(scan: ApiScanResult, hiddenProjects?: Set<string>): RuntimeGraph;
67
+ export declare function deriveRuntimeGraph(projects: EnhancedGraph, hiddenProjects?: Set<string>): RuntimeGraph;
67
68
  export declare function saveRuntimeGraph(graph: RuntimeGraph, workspaceRoot: string, graphPath?: string): void;
68
69
  export declare function runtimeGraphFileExists(workspaceRoot: string, graphPath?: string): boolean;
69
70
  export declare function loadRuntimeGraph(workspaceRoot: string, graphPath?: string): RuntimeGraph | null;
@@ -2,9 +2,12 @@
2
2
  /**
3
3
  * Runtime Graph
4
4
  *
5
- * Assembles the runtime microservice graph from the per-service
6
- * `service-contract.json` files, and saves/loads the committed
7
- * `architecture/runtime-dependencies.json`.
5
+ * Derives the runtime microservice graph SOLELY from architecture/dependencies.json
6
+ * (the single source of truth): each project's `apiRelations` carry which api
7
+ * classes it implements/uses and their transport (rpc | pubsub). Both
8
+ * `architecture:generate` and `architecture:validate-runtime-architecture` call
9
+ * the SAME `deriveRuntimeGraph`, so the committed graph and the validated graph
10
+ * can never diverge.
8
11
  *
9
12
  * The runtime edge Z -> X (Z depends on X at runtime) is INFERRED: Z `uses` api
10
13
  * Y and X `implements` api Y. This edge does not exist in the compile-time
@@ -13,8 +16,7 @@
13
16
  Object.defineProperty(exports, "__esModule", { value: true });
14
17
  exports.DEFAULT_RUNTIME_GRAPH_PATH = void 0;
15
18
  exports.runtimeAdjacency = runtimeAdjacency;
16
- exports.assembleRuntimeGraph = assembleRuntimeGraph;
17
- exports.assembleRuntimeGraphFromScan = assembleRuntimeGraphFromScan;
19
+ exports.deriveRuntimeGraph = deriveRuntimeGraph;
18
20
  exports.saveRuntimeGraph = saveRuntimeGraph;
19
21
  exports.runtimeGraphFileExists = runtimeGraphFileExists;
20
22
  exports.loadRuntimeGraph = loadRuntimeGraph;
@@ -23,81 +25,9 @@ const tslib_1 = require("tslib");
23
25
  const fs = tslib_1.__importStar(require("fs"));
24
26
  const path = tslib_1.__importStar(require("path"));
25
27
  const graph_sorter_1 = require("./graph-sorter");
26
- const runtime_markers_1 = require("./runtime-markers");
27
28
  const api_relations_1 = require("./api-usage/api-relations");
28
29
  const toError_1 = require("../toError");
29
30
  exports.DEFAULT_RUNTIME_GRAPH_PATH = 'architecture/runtime-dependencies.json';
30
- /** Collect every service project (servicePaths), with its declarations resolved to projects. */
31
- function collectServiceDecls(model, workspaceRoot) {
32
- const decls = [];
33
- for (const info of model.projects.values()) {
34
- if (!info.isService)
35
- continue;
36
- // A service missing its contract still appears as a node (empty edges);
37
- // validate-runtime-markers fails it separately for the missing file.
38
- const contract = (0, runtime_markers_1.readServiceContract)(workspaceRoot, info.root);
39
- const implementsPkgs = contract ? contract.implements : [];
40
- const usesPkgs = contract ? contract.uses : [];
41
- decls.push({
42
- name: info.name,
43
- implements: (0, runtime_markers_1.resolvePackageNames)(model, implementsPkgs).projects.sort(),
44
- uses: (0, runtime_markers_1.resolvePackageNames)(model, usesPkgs).projects.sort(),
45
- });
46
- }
47
- return decls.sort((a, b) => a.name.localeCompare(b.name));
48
- }
49
- function buildApiIndex(decls) {
50
- const apis = new Map();
51
- const ensure = (api) => {
52
- let entry = apis.get(api);
53
- if (!entry) {
54
- entry = { implementedBy: [], usedBy: [] };
55
- apis.set(api, entry);
56
- }
57
- return entry;
58
- };
59
- for (const decl of decls) {
60
- for (const api of decl.implements)
61
- ensure(api).implementedBy.push(decl.name);
62
- for (const api of decl.uses)
63
- ensure(api).usedBy.push(decl.name);
64
- }
65
- for (const entry of apis.values()) {
66
- entry.implementedBy.sort();
67
- entry.usedBy.sort();
68
- }
69
- return apis;
70
- }
71
- /** Build inferred runtime edges (Z -> X via Y) and the unresolved-uses list. */
72
- function buildEdges(decls, apis) {
73
- const viaByEdge = new Map();
74
- const unresolved = [];
75
- for (const decl of decls) {
76
- for (const api of decl.uses) {
77
- const implementers = apis.get(api)?.implementedBy ?? [];
78
- if (implementers.length === 0) {
79
- unresolved.push({ service: decl.name, api });
80
- continue;
81
- }
82
- for (const target of implementers) {
83
- if (target === decl.name)
84
- continue;
85
- const key = `${decl.name} ${target}`;
86
- if (!viaByEdge.has(key))
87
- viaByEdge.set(key, new Set());
88
- viaByEdge.get(key).add(api);
89
- }
90
- }
91
- }
92
- const edges = [];
93
- for (const key of viaByEdge.keys()) {
94
- const parts = key.split(' ');
95
- edges.push({ from: parts[0], to: parts[1], via: Array.from(viaByEdge.get(key)).sort() });
96
- }
97
- edges.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
98
- unresolved.sort((a, b) => a.service.localeCompare(b.service) || a.api.localeCompare(b.api));
99
- return { edges, unresolved };
100
- }
101
31
  /** Adjacency (service -> [targets]) used for leveling + cycle checks. */
102
32
  function adjacencyFromEdges(serviceNames, edges) {
103
33
  const adj = {};
@@ -131,41 +61,22 @@ function assignLevels(adjacency) {
131
61
  }
132
62
  return levels;
133
63
  }
134
- /** Assemble the full runtime graph from the workspace model + service contracts. */
135
- function assembleRuntimeGraph(model, workspaceRoot) {
136
- const decls = collectServiceDecls(model, workspaceRoot);
137
- const apis = buildApiIndex(decls);
138
- const edgeResult = buildEdges(decls, apis);
139
- const services = {};
140
- for (const decl of decls) {
141
- const dependsOn = Array.from(new Set(edgeResult.edges.filter((e) => e.from === decl.name).map((e) => e.to))).sort();
142
- services[decl.name] = { level: 0, implements: decl.implements, uses: decl.uses, dependsOn };
143
- }
144
- const levels = assignLevels(adjacencyFromEdges(Object.keys(services), edgeResult.edges));
145
- for (const name of Object.keys(services))
146
- services[name].level = levels[name] ?? 0;
147
- const apisObj = {};
148
- for (const api of Array.from(apis.keys()).sort())
149
- apisObj[api] = apis.get(api);
150
- return {
151
- services,
152
- apis: apisObj,
153
- runtimeEdges: edgeResult.edges,
154
- unresolvedUses: edgeResult.unresolved,
155
- };
156
- }
157
64
  /**
158
- * Assembles the runtime microservice graph from the SOURCE SCAN (apiRelations) instead of
159
- * service-contract.json: implementers × users per API, split by transport. An rpc edge is a direct
160
- * call; a pubsub edge flows through a queue (drawn producer → queue → consumer by the visualizer).
65
+ * Derives the runtime microservice graph from architecture/dependencies.json `apiRelations`:
66
+ * implementers × users per API, split by transport. An rpc edge is a direct call; a pubsub edge flows
67
+ * through a queue (drawn producer → queue → consumer by the visualizer).
68
+ *
69
+ * Nodes are the runnable apps only — role:server / role:client. A library is NEVER a node; its
70
+ * implements/uses (a shared client-factory or route-registration lib) are attributed to the
71
+ * servers/clients that embed it, walked transitively over dependsOn (see collectDecls).
161
72
  */
162
- class ScanRuntimeAssembler {
163
- scan;
73
+ class RuntimeGraphDeriver {
74
+ projects;
164
75
  hiddenProjects;
165
- constructor(scan,
76
+ constructor(projects,
166
77
  /** Project names tagged drawOnGraph:false — kept in the JSON but flagged so the viz omits them. */
167
78
  hiddenProjects) {
168
- this.scan = scan;
79
+ this.projects = projects;
169
80
  this.hiddenProjects = hiddenProjects;
170
81
  }
171
82
  assemble() {
@@ -178,23 +89,64 @@ class ScanRuntimeAssembler {
178
89
  apisObj[api] = apis.get(api);
179
90
  return { services, apis: apisObj, runtimeEdges: edgeResult.edges, unresolvedUses: edgeResult.unresolved };
180
91
  }
181
- /** Flatten each project's api-lib relations into implements/uses ApiRef lists. */
92
+ /**
93
+ * One ScanDecl per RUNTIME NODE (role:server / role:client). A node's effective
94
+ * relations are its OWN apiRelations PLUS those of every library in its transitive
95
+ * dependsOn closure: a lib that calls createRpcClient/addRoutes runs inside the
96
+ * server/client that embeds it, so the relation is attributed to that node — the
97
+ * lib is never a runtime node. Recursion stops at other nodes (each server/client
98
+ * owns its own relations). dependsOn is transitively reduced, but reduction
99
+ * preserves reachability, so the recursive walk still reaches every lib a node
100
+ * embeds through libraries.
101
+ */
182
102
  collectDecls() {
183
103
  const decls = [];
184
- for (const name of Array.from(this.scan.relationsByProject.keys()).sort()) {
185
- const relations = this.scan.relationsByProject.get(name);
104
+ for (const name of Object.keys(this.projects).sort()) {
105
+ if (!this.isNode(name))
106
+ continue;
186
107
  const implementsApis = [];
187
108
  const usesApis = [];
188
- for (const owner of Object.keys(relations)) {
189
- implementsApis.push(...relations[owner].implements);
190
- usesApis.push(...relations[owner].uses);
191
- }
109
+ this.collectEffectiveRelations(name, implementsApis, usesApis, new Set([name]));
192
110
  if (implementsApis.length > 0 || usesApis.length > 0) {
193
- decls.push({ name, implementsApis: (0, api_relations_1.sortApiRefs)(implementsApis), usesApis: (0, api_relations_1.sortApiRefs)(usesApis) });
111
+ decls.push({
112
+ name,
113
+ implementsApis: dedupApiRefs((0, api_relations_1.sortApiRefs)(implementsApis)),
114
+ usesApis: dedupApiRefs((0, api_relations_1.sortApiRefs)(usesApis)),
115
+ });
194
116
  }
195
117
  }
196
118
  return decls;
197
119
  }
120
+ /** True when a project is a runtime node — a server or client app (never a library). */
121
+ isNode(name) {
122
+ const role = this.projects[name]?.role;
123
+ return role === 'server' || role === 'client';
124
+ }
125
+ /**
126
+ * Accumulate `name`'s own api relations, then recurse through its LIBRARY deps
127
+ * (skipping other nodes, which own their own relations). `visited` guards against
128
+ * re-walking a lib reachable by more than one path (and any cycle).
129
+ */
130
+ collectEffectiveRelations(name, impl, uses, visited) {
131
+ const entry = this.projects[name];
132
+ if (entry === undefined)
133
+ return;
134
+ const relations = entry.apiRelations;
135
+ if (relations !== undefined) {
136
+ for (const owner of Object.keys(relations)) {
137
+ impl.push(...relations[owner].implements);
138
+ uses.push(...relations[owner].uses);
139
+ }
140
+ }
141
+ for (const dep of entry.dependsOn) {
142
+ if (visited.has(dep))
143
+ continue;
144
+ visited.add(dep);
145
+ if (this.isNode(dep))
146
+ continue; // another server/client owns its own relations
147
+ this.collectEffectiveRelations(dep, impl, uses, visited);
148
+ }
149
+ }
198
150
  /** apiClassName -> { implementedBy, usedBy, type }. */
199
151
  buildApis(decls) {
200
152
  const apis = new Map();
@@ -270,13 +222,29 @@ class ScanRuntimeAssembler {
270
222
  }
271
223
  }
272
224
  /**
273
- * Assemble the runtime graph from the source scan (the derived apiRelations).
274
- * `hiddenProjects` (drawOnGraph:false, defaults to none) are kept in the graph
275
- * but flagged so the runtime visualizer omits their nodes + edges.
225
+ * Derive the runtime graph from dependencies.json's project `apiRelations` the
226
+ * single source of truth shared by generate + validate. `hiddenProjects`
227
+ * (drawOnGraph:false, defaults to none) are kept in the graph but flagged so the
228
+ * runtime visualizer omits their nodes + edges.
276
229
  */
277
- // webpieces-disable no-function-outside-class -- module entry point, mirrors assembleRuntimeGraph
278
- function assembleRuntimeGraphFromScan(scan, hiddenProjects = new Set()) {
279
- return new ScanRuntimeAssembler(scan, hiddenProjects).assemble();
230
+ // webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation
231
+ function deriveRuntimeGraph(projects, hiddenProjects = new Set()) {
232
+ return new RuntimeGraphDeriver(projects, hiddenProjects).assemble();
233
+ }
234
+ /** Drop duplicate api refs (same api class), keeping the first — needed after a node absorbs the
235
+ * same api from both its own relations and an embedded lib's. Input is pre-sorted, so output stays
236
+ * deterministic. */
237
+ // webpieces-disable no-function-outside-class -- pure list helper, matches the sibling helpers in this file
238
+ function dedupApiRefs(refs) {
239
+ const seen = new Set();
240
+ const out = [];
241
+ for (const ref of refs) {
242
+ if (seen.has(ref.api))
243
+ continue;
244
+ seen.add(ref.api);
245
+ out.push(ref);
246
+ }
247
+ return out;
280
248
  }
281
249
  // webpieces-disable no-function-outside-class -- pure sort helper, matches the sibling helpers in this file
282
250
  function sortUnresolved(unresolved) {