@webpieces/nx-webpieces-rules 0.4.893 → 0.4.895

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.
Files changed (38) hide show
  1. package/package.json +7 -7
  2. package/src/executors/generate/executor.js +11 -7
  3. package/src/executors/generate/executor.js.map +1 -1
  4. package/src/executors/validate-architecture-unchanged/executor.d.ts +4 -2
  5. package/src/executors/validate-architecture-unchanged/executor.js +32 -5
  6. package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
  7. package/src/executors/validate-runtime-architecture/executor.js +1 -1
  8. package/src/executors/validate-runtime-architecture/executor.js.map +1 -1
  9. package/src/lib/graph-loader.d.ts +16 -2
  10. package/src/lib/graph-loader.js +34 -4
  11. package/src/lib/graph-loader.js.map +1 -1
  12. package/src/lib/remote-service-node-id.d.ts +8 -0
  13. package/src/lib/remote-service-node-id.js +15 -0
  14. package/src/lib/remote-service-node-id.js.map +1 -0
  15. package/src/lib/remote-services.d.ts +53 -0
  16. package/src/lib/remote-services.js +123 -0
  17. package/src/lib/remote-services.js.map +1 -0
  18. package/src/lib/runtime-details.d.ts +7 -0
  19. package/src/lib/runtime-details.js +27 -0
  20. package/src/lib/runtime-details.js.map +1 -1
  21. package/src/lib/runtime-graph-model.d.ts +28 -0
  22. package/src/lib/runtime-graph-model.js.map +1 -1
  23. package/src/lib/runtime-graph.d.ts +3 -2
  24. package/src/lib/runtime-graph.js +15 -3
  25. package/src/lib/runtime-graph.js.map +1 -1
  26. package/src/lib/runtime-remote-calls.d.ts +35 -0
  27. package/src/lib/runtime-remote-calls.js +87 -0
  28. package/src/lib/runtime-remote-calls.js.map +1 -0
  29. package/src/lib/runtime-visualizer.js +41 -0
  30. package/src/lib/runtime-visualizer.js.map +1 -1
  31. package/src/lib/runtime-viz-theme.d.ts +7 -0
  32. package/src/lib/runtime-viz-theme.js +11 -1
  33. package/src/lib/runtime-viz-theme.js.map +1 -1
  34. package/src/lib/runtime-wiring/approved-graph.js +2 -29
  35. package/src/lib/runtime-wiring/approved-graph.js.map +1 -1
  36. package/src/lib/runtime-wiring/service-targets.d.ts +22 -0
  37. package/src/lib/runtime-wiring/service-targets.js +95 -0
  38. package/src/lib/runtime-wiring/service-targets.js.map +1 -0
@@ -9,6 +9,7 @@
9
9
  * {
10
10
  * "aiInstructions": "...how AI should use the per-project fields...",
11
11
  * "apiContractFiles": { "<ApiName>": "apis/<ApiName>.json" },
12
+ * "remoteServices": { "<serviceName>": { "label"?, "implements": [{ "api", "type" }] } },
12
13
  * "projects": {
13
14
  * "<project>": { level, framework, products, shortDescription,
14
15
  * responsibilitiesFile, designFile, dependsOn }
@@ -40,6 +41,7 @@ const fs = tslib_1.__importStar(require("fs"));
40
41
  const path = tslib_1.__importStar(require("path"));
41
42
  const rules_config_1 = require("@webpieces/rules-config");
42
43
  const api_contract_files_1 = require("./api-contract-files");
44
+ const remote_services_1 = require("./remote-services");
43
45
  const toError_1 = require("../toError");
44
46
  /**
45
47
  * Default path for the dependencies file (relative to workspace root)
@@ -90,6 +92,7 @@ class DependenciesFile {
90
92
  projects;
91
93
  apiContractFiles;
92
94
  externalSystems;
95
+ remoteServices;
93
96
  constructor(aiInstructions, commands, projects,
94
97
  /**
95
98
  * apiClassName -> its contract file (`apis/<ApiName>.json`, relative to this file's
@@ -102,12 +105,19 @@ class DependenciesFile {
102
105
  * the runtime graph is derived from the committed files, so a declaration that is scanned but
103
106
  * not written would make generate and validate disagree.
104
107
  */
105
- externalSystems = {}) {
108
+ externalSystems = {},
109
+ /**
110
+ * REVIEWED out-of-workspace services a `uses` may target (#1203), keyed by service name. Like
111
+ * `apiContractFiles` it is approval metadata: generation reads it and writes it back verbatim,
112
+ * and never infers or adds an entry. See remote-services.ts.
113
+ */
114
+ remoteServices = {}) {
106
115
  this.aiInstructions = aiInstructions;
107
116
  this.commands = commands;
108
117
  this.projects = projects;
109
118
  this.apiContractFiles = apiContractFiles;
110
119
  this.externalSystems = externalSystems;
120
+ this.remoteServices = remoteServices;
111
121
  }
112
122
  }
113
123
  exports.DependenciesFile = DependenciesFile;
@@ -141,7 +151,7 @@ function loadBlessedGraph(workspaceRoot, graphPath = exports.DEFAULT_GRAPH_PATH)
141
151
  // table simply draws no shaped nodes, which is exactly the old rendering.
142
152
  parsed.externalSystems !== null && typeof parsed.externalSystems === 'object'
143
153
  ? parsed.externalSystems
144
- : {});
154
+ : {}, new remote_services_1.RemoteServicesCodec().decode(parsed.remoteServices, fullPath));
145
155
  }
146
156
  // Legacy flat format: the whole object is the project map
147
157
  return new DependenciesFile('', {}, parsed);
@@ -181,6 +191,7 @@ function formatGraphJson(file) {
181
191
  lines.push(` },`);
182
192
  lines.push(...apiContractFilesLines(file.apiContractFiles));
183
193
  lines.push(...externalSystemsLines(file.externalSystems));
194
+ lines.push(...remoteServicesLines(file.remoteServices));
184
195
  lines.push(` "projects": {`);
185
196
  const keys = Object.keys(file.projects).sort();
186
197
  keys.forEach((key, index) => {
@@ -232,6 +243,25 @@ function externalSystemsLines(systems) {
232
243
  return ` ${prefix}${line}${suffix}`;
233
244
  });
234
245
  }
246
+ /**
247
+ * The reviewed `"remoteServices": {...}` block (4-space indent), with a trailing comma since `projects`
248
+ * always follows. Omitted entirely when empty, so a repo that declares none keeps its exact file shape.
249
+ * Service names are sorted; each entry is written as it was reviewed.
250
+ */
251
+ // webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling formatters here
252
+ function remoteServicesLines(services) {
253
+ if (Object.keys(services).length === 0)
254
+ return [];
255
+ const sorted = {};
256
+ for (const name of Object.keys(services).sort())
257
+ sorted[name] = services[name];
258
+ const pretty = JSON.stringify(sorted, null, 4).split('\n');
259
+ return pretty.map((line, index) => {
260
+ const prefix = index === 0 ? '"remoteServices": ' : '';
261
+ const suffix = index === pretty.length - 1 ? ',' : '';
262
+ return ` ${prefix}${line}${suffix}`;
263
+ });
264
+ }
235
265
  /**
236
266
  * Format one project entry's fields (12-space indent). Optional metadata
237
267
  * fields are only emitted when present.
@@ -337,7 +367,7 @@ function pushApiRelationsField(lines, value) {
337
367
  * @param workspaceRoot - Absolute path to workspace root
338
368
  * @param graphPath - Relative path to graph file (default: architecture/dependencies.json)
339
369
  */
340
- function saveGraph(graph, workspaceRoot, graphPath = exports.DEFAULT_GRAPH_PATH, apiContractFiles = {}, externalSystems = {}) {
370
+ function saveGraph(graph, workspaceRoot, graphPath = exports.DEFAULT_GRAPH_PATH, apiContractFiles = {}, externalSystems = {}, remoteServices = {}) {
341
371
  const fullPath = path.join(workspaceRoot, graphPath);
342
372
  const dir = path.dirname(fullPath);
343
373
  // Ensure directory exists
@@ -350,7 +380,7 @@ function saveGraph(graph, workspaceRoot, graphPath = exports.DEFAULT_GRAPH_PATH,
350
380
  for (const key of sortedKeys) {
351
381
  sortedGraph[key] = graph[key];
352
382
  }
353
- const content = formatGraphJson(new DependenciesFile(exports.AI_INSTRUCTIONS, exports.GRAPH_COMMANDS, sortedGraph, apiContractFiles, externalSystems));
383
+ const content = formatGraphJson(new DependenciesFile(exports.AI_INSTRUCTIONS, exports.GRAPH_COMMANDS, sortedGraph, apiContractFiles, externalSystems, remoteServices));
354
384
  fs.writeFileSync(fullPath, content, 'utf-8');
355
385
  }
356
386
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"graph-loader.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Loader\n *\n * Handles loading and saving the blessed dependency graph file.\n * The graph is stored at architecture/dependencies.json in the workspace root.\n *\n * File format (schema aimed at AI consumers):\n * {\n * \"aiInstructions\": \"...how AI should use the per-project fields...\",\n * \"apiContractFiles\": { \"<ApiName>\": \"apis/<ApiName>.json\" },\n * \"projects\": {\n * \"<project>\": { level, framework, products, shortDescription,\n * responsibilitiesFile, designFile, dependsOn }\n * }\n * }\n *\n * `level` is the project's dependency depth, except that every server, client and app is pinned to\n * the TOP level (bundles above them), so the top row is exactly the entry points (#1179).\n *\n * `products` (#1179) is the sorted list of products whose `product:<name>`-tagged projects reach\n * this project over `dependsOn` — its blast radius. Absent when no product reaches it.\n *\n * `framework` is the project's libType — the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]). It comes from the project's `framework:` nx tags\n * and is enforced across edges by the `library-types-match-client` rule.\n *\n * The legacy format (flat { \"<project>\": { level, dependsOn } } map) is still\n * readable so validation against a pre-upgrade file produces a clean\n * \"re-run architecture:generate\" diff instead of a parse failure.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport { RuleFailError, Option } from '@webpieces/rules-config';\nimport type { ExternalSystemDecls, ProjectApiRelations } from './api-usage/api-relations';\nimport { API_CONTRACT_FILES_RULE, API_CONTRACTS_DIR } from './api-contract-files';\nimport type { ApiContractFileRefs } from './api-contract-files';\nimport { toError } from '../toError';\n\n/**\n * Default path for the dependencies file (relative to workspace root)\n */\nexport const DEFAULT_GRAPH_PATH = 'architecture/dependencies.json';\n\n/**\n * Top-level instructions embedded in dependencies.json telling AI how to use\n * the per-project metadata fields.\n */\nexport const AI_INSTRUCTIONS =\n \"Each project's shortDescription is only a summary. BEFORE adding code to a project, \" +\n 'read its responsibilitiesFile for the full responsibilities (what belongs in that ' +\n 'project and what does not), and read its designFile to understand the DI design ' +\n 'before reading the code. Use the entries in `commands` to regenerate these files ' +\n 'or display any of the graphs in a browser. To hide a project (its box AND every ' +\n 'edge touching it) from BOTH rendered architecture graphs, add the nx tag ' +\n \"'drawOnGraph:false' to that project's project.json tags and regenerate; it stays \" +\n 'in this file (marked \"drawOnGraph\": false) but is omitted from the HTML. A role:server ' +\n 'with no webpiecesRuntime package anywhere in its dependency closure and no apiRelations is ' +\n 'omitted from the RUNTIME drawing the same way — it speaks none of the webpieces runtime, so ' +\n \"it can only ever draw as a disconnected box; it stays in both JSON files. An API's endpoints \" +\n '(name, kind, path, httpMethod, parameters, queueName, caller) are NOT in this file: ' +\n '`apiContractFiles` maps each API to its generated contract file, apis/<ApiName>.json relative to ' +\n \"this file — read that file for the API's endpoints.\";\n\n/**\n * Named command → \"command — what it does\" map embedded in dependencies.json.\n */\nexport type CommandMap = Record<string, string>;\n\n/**\n * Commands embedded in dependencies.json so AI (and humans) know how to\n * regenerate and DISPLAY the architecture + design graphs. These work in any\n * repo consuming @webpieces/nx-webpieces-rules.\n */\nexport const GRAPH_COMMANDS: CommandMap = {\n regenerateArchitecture:\n 'pnpm nx run architecture:generate — rewrites architecture/dependencies.json, ' +\n 'architecture/apis/<ApiName>.json and architecture/runtime-dependencies.json; run after ' +\n 'reviewing project runtime-deps.json and API contract candidates into architecture/apis; generation reads approvals and never approves endpoint changes',\n visualizeArchitecture:\n 'pnpm nx run architecture:visualize — opens the monorepo dependency graph (this file) ' +\n 'as HTML in a browser from saved data, without refreshing; freshness unknown',\n visualizeRuntimeArchitecture:\n 'pnpm nx run architecture:visualize-runtime — opens the runtime microservice call graph ' +\n 'as HTML in a browser from saved data, without refreshing; freshness unknown',\n regenerateDesigns:\n \"pnpm nx run-many --target=di-graph-generate — rewrites every project's \" +\n 'design.json/design.md (also runs automatically on every build)',\n visualizeDesign:\n \"pnpm wp-design-visualize <project> — opens a project's DI designs (its designFile) as \" +\n 'HTML, one graph per controller with the controller at the top; no args = interactive picker',\n};\n\n/**\n * The full contents of architecture/dependencies.json.\n */\nexport class DependenciesFile {\n constructor(\n public readonly aiInstructions: string,\n public readonly commands: CommandMap,\n public readonly projects: EnhancedGraph,\n /**\n * apiClassName -> its contract file (`apis/<ApiName>.json`, relative to this file's\n * directory). Only the LINK lives here, so adding an endpoint never changes dependencies.json;\n * the contract itself is read with ApiContractFiles.load.\n */\n public readonly apiContractFiles: ApiContractFileRefs = {},\n /**\n * Declared external systems (databases, buckets, ...) keyed by identity. MUST be persisted:\n * the runtime graph is derived from the committed files, so a declaration that is scanned but\n * not written would make generate and validate disagree.\n */\n public readonly externalSystems: ExternalSystemDecls = {},\n ) {}\n}\n\n/**\n * Load the blessed graph from disk. Understands both the current wrapper\n * format and the legacy flat map (which loads with empty aiInstructions).\n *\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: architecture/dependencies.json)\n * @returns The blessed graph file, or null if it doesn't exist\n */\nexport function loadBlessedGraph(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH,\n): DependenciesFile | null {\n const fullPath = path.join(workspaceRoot, graphPath);\n\n if (!fs.existsSync(fullPath)) {\n return null;\n }\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const content = fs.readFileSync(fullPath, 'utf-8');\n const parsed = JSON.parse(content);\n if (parsed !== null && typeof parsed === 'object' && 'apiContracts' in parsed) {\n throw movedApiContractsError(fullPath, graphPath);\n }\n if (parsed !== null && typeof parsed === 'object' && 'projects' in parsed) {\n return new DependenciesFile(\n typeof parsed.aiInstructions === 'string' ? parsed.aiInstructions : '',\n parsed.commands !== null && typeof parsed.commands === 'object'\n ? (parsed.commands as CommandMap)\n : {},\n parsed.projects as EnhancedGraph,\n parsed.apiContractFiles !== null && typeof parsed.apiContractFiles === 'object'\n ? (parsed.apiContractFiles as ApiContractFileRefs)\n : {},\n // Absent in any file written before external systems could be declared; an empty\n // table simply draws no shaped nodes, which is exactly the old rendering.\n parsed.externalSystems !== null && typeof parsed.externalSystems === 'object'\n ? (parsed.externalSystems as ExternalSystemDecls)\n : {},\n );\n }\n // Legacy flat format: the whole object is the project map\n return new DependenciesFile('', {}, parsed as EnhancedGraph);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof RuleFailError) throw error;\n throw new Error(`Failed to load graph from ${fullPath}`, { cause: error });\n }\n}\n\n/**\n * The endpoint table moved out of dependencies.json (#949). A file still carrying `apiContracts` was\n * written by an older generator; it is REJECTED rather than read, so there is one place a contract\n * lives and the runtime graph can never read a stale copy of it.\n */\n// webpieces-disable no-function-outside-class -- error builder for loadBlessedGraph, module-scope like its siblings\nfunction movedApiContractsError(fullPath: string, graphPath: string): RuleFailError {\n const apisDir = path.join(path.dirname(graphPath), API_CONTRACTS_DIR);\n return new RuleFailError(\n API_CONTRACT_FILES_RULE,\n `${fullPath} still carries the \\`apiContracts\\` key. It moved: each API's contract now lives in ` +\n `${apisDir}/<ApiName>.json, and dependencies.json only links to it under \\`apiContractFiles\\`.`,\n undefined,\n undefined,\n [\n new Option(\n `Explicitly move each saved apiContracts.<ApiName> value into ${apisDir}/<ApiName>.json, replace apiContracts with reviewed apiContractFiles entries mapping each name to apis/<ApiName>.json, preserve externalSystems, and approve runtime-deps.json for all runtime owners. Then run pnpm nx run architecture:generate and review the result.`,\n true,\n ),\n ],\n );\n}\n\n/**\n * Format the dependencies file as JSON with multi-line arrays for readability\n */\nfunction formatGraphJson(file: DependenciesFile): string {\n const lines: string[] = ['{'];\n lines.push(` \"aiInstructions\": ${JSON.stringify(file.aiInstructions)},`);\n lines.push(` \"commands\": {`);\n const commandNames = Object.keys(file.commands);\n commandNames.forEach((name: string, index: number) => {\n const comma = index === commandNames.length - 1 ? '' : ',';\n lines.push(\n ` ${JSON.stringify(name)}: ${JSON.stringify(file.commands[name])}${comma}`,\n );\n });\n lines.push(` },`);\n lines.push(...apiContractFilesLines(file.apiContractFiles));\n lines.push(...externalSystemsLines(file.externalSystems));\n lines.push(` \"projects\": {`);\n\n const keys = Object.keys(file.projects).sort();\n keys.forEach((key: string, index: number) => {\n const entry = file.projects[key];\n const isLast = index === keys.length - 1;\n const comma = isLast ? '' : ',';\n\n lines.push(` ${JSON.stringify(key)}: {`);\n lines.push(...formatEntryLines(entry));\n lines.push(` }${comma}`);\n });\n\n lines.push(' }');\n lines.push('}');\n return lines.join('\\n') + '\\n';\n}\n\n/**\n * The `\"apiContractFiles\": {...}` block (4-space indent), with a trailing comma since `projects`\n * always follows it. Omitted entirely when empty. Keys are sorted, so the output is deterministic.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling formatters here\nfunction apiContractFilesLines(refs: ApiContractFileRefs): string[] {\n if (Object.keys(refs).length === 0) return [];\n const sorted: ApiContractFileRefs = {};\n for (const api of Object.keys(refs).sort()) sorted[api] = refs[api];\n const pretty = JSON.stringify(sorted, null, 4).split('\\n');\n return pretty.map((line: string, index: number) => {\n const prefix = index === 0 ? '\"apiContractFiles\": ' : '';\n const suffix = index === pretty.length - 1 ? ',' : '';\n return ` ${prefix}${line}${suffix}`;\n });\n}\n\n/**\n * The `\"externalSystems\": {...}` block (4-space indent), with a trailing comma since `projects`\n * always follows. Omitted entirely when empty, so a repo that declares none keeps the exact file\n * shape it had before this existed — which is what makes adopting it a no-op diff.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling formatters here\nfunction externalSystemsLines(systems: ExternalSystemDecls): string[] {\n if (Object.keys(systems).length === 0) return [];\n const sorted: ExternalSystemDecls = {};\n for (const identity of Object.keys(systems).sort()) sorted[identity] = systems[identity];\n const pretty = JSON.stringify(sorted, null, 4).split('\\n');\n return pretty.map((line: string, index: number) => {\n const prefix = index === 0 ? '\"externalSystems\": ' : '';\n const suffix = index === pretty.length - 1 ? ',' : '';\n return ` ${prefix}${line}${suffix}`;\n });\n}\n\n/**\n * Format one project entry's fields (12-space indent). Optional metadata\n * fields are only emitted when present.\n */\nfunction formatEntryLines(entry: GraphEntry): string[] {\n const lines: string[] = [];\n lines.push(` \"level\": ${entry.level},`);\n\n pushOptionalArrayField(lines, 'framework', entry.framework);\n pushOptionalField(lines, 'role', entry.role);\n // Right after role — role says what the project IS, this says what runtime it speaks. MUST be\n // persisted: validate re-derives the runtime graph from this file, so a field written only\n // in-memory by generate would make the two derive different graphs.\n pushOptionalArrayField(lines, 'webpiecesRuntime', entry.webpiecesRuntime);\n pushOptionalField(lines, 'serviceName', entry.serviceName);\n pushOptionalCallsServiceField(lines, entry.callsService);\n pushOptionalBooleanField(lines, 'drawOnGraph', entry.drawOnGraph);\n // MUST be persisted: validate re-derives the runtime graph from this file, so a cut written only\n // in-memory by generate would make generate and validate level (and cycle-check) different graphs.\n pushOptionalArrayField(lines, 'cutLegacyCycle', entry.cutLegacyCycle);\n // MUST be persisted: the runtime graph's product membership is re-derived from this file during\n // validate, and the page's Product filter / color mode read it (#1179).\n pushOptionalArrayField(lines, 'products', entry.products);\n pushOptionalField(lines, 'shortDescription', entry.shortDescription);\n pushOptionalField(lines, 'responsibilitiesFile', entry.responsibilitiesFile);\n pushOptionalField(lines, 'designFile', entry.designFile);\n pushApiRelationsField(lines, entry.apiRelations);\n pushOptionalField(lines, 'runtimeDeclaration', entry.runtimeDeclaration);\n pushOptionalBooleanField(lines, 'runtimeComposition', entry.runtimeComposition);\n\n if (entry.dependsOn.length === 0) {\n lines.push(` \"dependsOn\": []`);\n } else {\n lines.push(` \"dependsOn\": [`);\n entry.dependsOn.forEach((dep: string, depIndex: number) => {\n const depComma = depIndex === entry.dependsOn.length - 1 ? '' : ',';\n lines.push(` ${JSON.stringify(dep)}${depComma}`);\n });\n lines.push(` ]`);\n }\n return lines;\n}\n\n/**\n * Emit one optional string field (12-space indent), skipped when undefined.\n */\nfunction pushOptionalField(lines: string[], field: string, value: string | undefined): void {\n if (value !== undefined) {\n lines.push(` ${JSON.stringify(field)}: ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit the optional `callsService` field (12-space indent). Unlike serviceName it may be EITHER a\n * single service name (string) OR an { apiClassName: serviceName } map, so it is emitted with\n * JSON.stringify to cover both inline. MUST be persisted here: the runtime graph's target resolution\n * reads it back from the committed dependencies.json during validate — dropping it would make\n * generate (which enriches it in-memory) and validate (which loads this file) derive different graphs.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling push*Field helpers here\nfunction pushOptionalCallsServiceField(\n lines: string[],\n value: string | Record<string, string> | undefined,\n): void {\n if (value !== undefined) {\n lines.push(` \"callsService\": ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit one optional boolean field (12-space indent) as a raw JSON boolean\n * (e.g. `\"drawOnGraph\": false,`), skipped when undefined.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling push*Field helpers here\nfunction pushOptionalBooleanField(\n lines: string[],\n field: string,\n value: boolean | undefined,\n): void {\n if (value !== undefined) {\n lines.push(` ${JSON.stringify(field)}: ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit one optional string-array field (12-space indent) as a compact inline\n * JSON array (e.g. `\"framework\": [\"browser\",\"node\"],`), skipped when undefined.\n */\nfunction pushOptionalArrayField(lines: string[], field: string, value: string[] | undefined): void {\n if (value !== undefined) {\n lines.push(` ${JSON.stringify(field)}: ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit the optional `apiRelations` object (pretty, multi-line, reindented under\n * the 12-space entry block) with a trailing comma, since `dependsOn` always\n * follows it. Skipped when absent/empty so plain-lib-only projects stay compact.\n * The scanner already sorts owners + refs, so the JSON is deterministic.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling push*Field helpers here\nfunction pushApiRelationsField(lines: string[], value: ProjectApiRelations | undefined): void {\n if (value === undefined || Object.keys(value).length === 0) return;\n const pretty = JSON.stringify(value, null, 4).split('\\n');\n pretty.forEach((line: string, index: number) => {\n const prefix = index === 0 ? '\"apiRelations\": ' : '';\n const suffix = index === pretty.length - 1 ? ',' : '';\n lines.push(` ${prefix}${line}${suffix}`);\n });\n}\n\n/**\n * Save the graph to disk in the wrapper format with the standard aiInstructions.\n *\n * @param graph - The enriched project graph to save\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: architecture/dependencies.json)\n */\nexport function saveGraph(\n graph: EnhancedGraph,\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH,\n apiContractFiles: ApiContractFileRefs = {},\n externalSystems: ExternalSystemDecls = {},\n): void {\n const fullPath = path.join(workspaceRoot, graphPath);\n const dir = path.dirname(fullPath);\n\n // Ensure directory exists\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n // Sort keys for deterministic output\n const sortedGraph: EnhancedGraph = {};\n const sortedKeys = Object.keys(graph).sort();\n for (const key of sortedKeys) {\n sortedGraph[key] = graph[key];\n }\n\n const content = formatGraphJson(\n new DependenciesFile(\n AI_INSTRUCTIONS,\n GRAPH_COMMANDS,\n sortedGraph,\n apiContractFiles,\n externalSystems,\n ),\n );\n fs.writeFileSync(fullPath, content, 'utf-8');\n}\n\n/**\n * Check if the graph file exists\n */\nexport function graphFileExists(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH,\n): boolean {\n const fullPath = path.join(workspaceRoot, graphPath);\n return fs.existsSync(fullPath);\n}\n"]}
1
+ {"version":3,"file":"graph-loader.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Loader\n *\n * Handles loading and saving the blessed dependency graph file.\n * The graph is stored at architecture/dependencies.json in the workspace root.\n *\n * File format (schema aimed at AI consumers):\n * {\n * \"aiInstructions\": \"...how AI should use the per-project fields...\",\n * \"apiContractFiles\": { \"<ApiName>\": \"apis/<ApiName>.json\" },\n * \"remoteServices\": { \"<serviceName>\": { \"label\"?, \"implements\": [{ \"api\", \"type\" }] } },\n * \"projects\": {\n * \"<project>\": { level, framework, products, shortDescription,\n * responsibilitiesFile, designFile, dependsOn }\n * }\n * }\n *\n * `level` is the project's dependency depth, except that every server, client and app is pinned to\n * the TOP level (bundles above them), so the top row is exactly the entry points (#1179).\n *\n * `products` (#1179) is the sorted list of products whose `product:<name>`-tagged projects reach\n * this project over `dependsOn` — its blast radius. Absent when no product reaches it.\n *\n * `framework` is the project's libType — the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]). It comes from the project's `framework:` nx tags\n * and is enforced across edges by the `library-types-match-client` rule.\n *\n * The legacy format (flat { \"<project>\": { level, dependsOn } } map) is still\n * readable so validation against a pre-upgrade file produces a clean\n * \"re-run architecture:generate\" diff instead of a parse failure.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport { RuleFailError, Option } from '@webpieces/rules-config';\nimport type { ExternalSystemDecls, ProjectApiRelations } from './api-usage/api-relations';\nimport { API_CONTRACT_FILES_RULE, API_CONTRACTS_DIR } from './api-contract-files';\nimport type { ApiContractFileRefs } from './api-contract-files';\nimport { RemoteServicesCodec } from './remote-services';\nimport type { RemoteServices } from './remote-services';\nimport { toError } from '../toError';\n\n/**\n * Default path for the dependencies file (relative to workspace root)\n */\nexport const DEFAULT_GRAPH_PATH = 'architecture/dependencies.json';\n\n/**\n * Top-level instructions embedded in dependencies.json telling AI how to use\n * the per-project metadata fields.\n */\nexport const AI_INSTRUCTIONS =\n \"Each project's shortDescription is only a summary. BEFORE adding code to a project, \" +\n 'read its responsibilitiesFile for the full responsibilities (what belongs in that ' +\n 'project and what does not), and read its designFile to understand the DI design ' +\n 'before reading the code. Use the entries in `commands` to regenerate these files ' +\n 'or display any of the graphs in a browser. To hide a project (its box AND every ' +\n 'edge touching it) from BOTH rendered architecture graphs, add the nx tag ' +\n \"'drawOnGraph:false' to that project's project.json tags and regenerate; it stays \" +\n 'in this file (marked \"drawOnGraph\": false) but is omitted from the HTML. A role:server ' +\n 'with no webpiecesRuntime package anywhere in its dependency closure and no apiRelations is ' +\n 'omitted from the RUNTIME drawing the same way — it speaks none of the webpieces runtime, so ' +\n \"it can only ever draw as a disconnected box; it stays in both JSON files. An API's endpoints \" +\n '(name, kind, path, httpMethod, parameters, queueName, caller) are NOT in this file: ' +\n '`apiContractFiles` maps each API to its generated contract file, apis/<ApiName>.json relative to ' +\n \"this file — read that file for the API's endpoints.\";\n\n/**\n * Named command → \"command — what it does\" map embedded in dependencies.json.\n */\nexport type CommandMap = Record<string, string>;\n\n/**\n * Commands embedded in dependencies.json so AI (and humans) know how to\n * regenerate and DISPLAY the architecture + design graphs. These work in any\n * repo consuming @webpieces/nx-webpieces-rules.\n */\nexport const GRAPH_COMMANDS: CommandMap = {\n regenerateArchitecture:\n 'pnpm nx run architecture:generate — rewrites architecture/dependencies.json, ' +\n 'architecture/apis/<ApiName>.json and architecture/runtime-dependencies.json; run after ' +\n 'reviewing project runtime-deps.json and API contract candidates into architecture/apis; generation reads approvals and never approves endpoint changes',\n visualizeArchitecture:\n 'pnpm nx run architecture:visualize — opens the monorepo dependency graph (this file) ' +\n 'as HTML in a browser from saved data, without refreshing; freshness unknown',\n visualizeRuntimeArchitecture:\n 'pnpm nx run architecture:visualize-runtime — opens the runtime microservice call graph ' +\n 'as HTML in a browser from saved data, without refreshing; freshness unknown',\n regenerateDesigns:\n \"pnpm nx run-many --target=di-graph-generate — rewrites every project's \" +\n 'design.json/design.md (also runs automatically on every build)',\n visualizeDesign:\n \"pnpm wp-design-visualize <project> — opens a project's DI designs (its designFile) as \" +\n 'HTML, one graph per controller with the controller at the top; no args = interactive picker',\n};\n\n/**\n * The full contents of architecture/dependencies.json.\n */\nexport class DependenciesFile {\n constructor(\n public readonly aiInstructions: string,\n public readonly commands: CommandMap,\n public readonly projects: EnhancedGraph,\n /**\n * apiClassName -> its contract file (`apis/<ApiName>.json`, relative to this file's\n * directory). Only the LINK lives here, so adding an endpoint never changes dependencies.json;\n * the contract itself is read with ApiContractFiles.load.\n */\n public readonly apiContractFiles: ApiContractFileRefs = {},\n /**\n * Declared external systems (databases, buckets, ...) keyed by identity. MUST be persisted:\n * the runtime graph is derived from the committed files, so a declaration that is scanned but\n * not written would make generate and validate disagree.\n */\n public readonly externalSystems: ExternalSystemDecls = {},\n /**\n * REVIEWED out-of-workspace services a `uses` may target (#1203), keyed by service name. Like\n * `apiContractFiles` it is approval metadata: generation reads it and writes it back verbatim,\n * and never infers or adds an entry. See remote-services.ts.\n */\n public readonly remoteServices: RemoteServices = {},\n ) {}\n}\n\n/**\n * Load the blessed graph from disk. Understands both the current wrapper\n * format and the legacy flat map (which loads with empty aiInstructions).\n *\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: architecture/dependencies.json)\n * @returns The blessed graph file, or null if it doesn't exist\n */\nexport function loadBlessedGraph(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH,\n): DependenciesFile | null {\n const fullPath = path.join(workspaceRoot, graphPath);\n\n if (!fs.existsSync(fullPath)) {\n return null;\n }\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const content = fs.readFileSync(fullPath, 'utf-8');\n const parsed = JSON.parse(content);\n if (parsed !== null && typeof parsed === 'object' && 'apiContracts' in parsed) {\n throw movedApiContractsError(fullPath, graphPath);\n }\n if (parsed !== null && typeof parsed === 'object' && 'projects' in parsed) {\n return new DependenciesFile(\n typeof parsed.aiInstructions === 'string' ? parsed.aiInstructions : '',\n parsed.commands !== null && typeof parsed.commands === 'object'\n ? (parsed.commands as CommandMap)\n : {},\n parsed.projects as EnhancedGraph,\n parsed.apiContractFiles !== null && typeof parsed.apiContractFiles === 'object'\n ? (parsed.apiContractFiles as ApiContractFileRefs)\n : {},\n // Absent in any file written before external systems could be declared; an empty\n // table simply draws no shaped nodes, which is exactly the old rendering.\n parsed.externalSystems !== null && typeof parsed.externalSystems === 'object'\n ? (parsed.externalSystems as ExternalSystemDecls)\n : {},\n new RemoteServicesCodec().decode(parsed.remoteServices, fullPath),\n );\n }\n // Legacy flat format: the whole object is the project map\n return new DependenciesFile('', {}, parsed as EnhancedGraph);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof RuleFailError) throw error;\n throw new Error(`Failed to load graph from ${fullPath}`, { cause: error });\n }\n}\n\n/**\n * The endpoint table moved out of dependencies.json (#949). A file still carrying `apiContracts` was\n * written by an older generator; it is REJECTED rather than read, so there is one place a contract\n * lives and the runtime graph can never read a stale copy of it.\n */\n// webpieces-disable no-function-outside-class -- error builder for loadBlessedGraph, module-scope like its siblings\nfunction movedApiContractsError(fullPath: string, graphPath: string): RuleFailError {\n const apisDir = path.join(path.dirname(graphPath), API_CONTRACTS_DIR);\n return new RuleFailError(\n API_CONTRACT_FILES_RULE,\n `${fullPath} still carries the \\`apiContracts\\` key. It moved: each API's contract now lives in ` +\n `${apisDir}/<ApiName>.json, and dependencies.json only links to it under \\`apiContractFiles\\`.`,\n undefined,\n undefined,\n [\n new Option(\n `Explicitly move each saved apiContracts.<ApiName> value into ${apisDir}/<ApiName>.json, replace apiContracts with reviewed apiContractFiles entries mapping each name to apis/<ApiName>.json, preserve externalSystems, and approve runtime-deps.json for all runtime owners. Then run pnpm nx run architecture:generate and review the result.`,\n true,\n ),\n ],\n );\n}\n\n/**\n * Format the dependencies file as JSON with multi-line arrays for readability\n */\nfunction formatGraphJson(file: DependenciesFile): string {\n const lines: string[] = ['{'];\n lines.push(` \"aiInstructions\": ${JSON.stringify(file.aiInstructions)},`);\n lines.push(` \"commands\": {`);\n const commandNames = Object.keys(file.commands);\n commandNames.forEach((name: string, index: number) => {\n const comma = index === commandNames.length - 1 ? '' : ',';\n lines.push(\n ` ${JSON.stringify(name)}: ${JSON.stringify(file.commands[name])}${comma}`,\n );\n });\n lines.push(` },`);\n lines.push(...apiContractFilesLines(file.apiContractFiles));\n lines.push(...externalSystemsLines(file.externalSystems));\n lines.push(...remoteServicesLines(file.remoteServices));\n lines.push(` \"projects\": {`);\n\n const keys = Object.keys(file.projects).sort();\n keys.forEach((key: string, index: number) => {\n const entry = file.projects[key];\n const isLast = index === keys.length - 1;\n const comma = isLast ? '' : ',';\n\n lines.push(` ${JSON.stringify(key)}: {`);\n lines.push(...formatEntryLines(entry));\n lines.push(` }${comma}`);\n });\n\n lines.push(' }');\n lines.push('}');\n return lines.join('\\n') + '\\n';\n}\n\n/**\n * The `\"apiContractFiles\": {...}` block (4-space indent), with a trailing comma since `projects`\n * always follows it. Omitted entirely when empty. Keys are sorted, so the output is deterministic.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling formatters here\nfunction apiContractFilesLines(refs: ApiContractFileRefs): string[] {\n if (Object.keys(refs).length === 0) return [];\n const sorted: ApiContractFileRefs = {};\n for (const api of Object.keys(refs).sort()) sorted[api] = refs[api];\n const pretty = JSON.stringify(sorted, null, 4).split('\\n');\n return pretty.map((line: string, index: number) => {\n const prefix = index === 0 ? '\"apiContractFiles\": ' : '';\n const suffix = index === pretty.length - 1 ? ',' : '';\n return ` ${prefix}${line}${suffix}`;\n });\n}\n\n/**\n * The `\"externalSystems\": {...}` block (4-space indent), with a trailing comma since `projects`\n * always follows. Omitted entirely when empty, so a repo that declares none keeps the exact file\n * shape it had before this existed — which is what makes adopting it a no-op diff.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling formatters here\nfunction externalSystemsLines(systems: ExternalSystemDecls): string[] {\n if (Object.keys(systems).length === 0) return [];\n const sorted: ExternalSystemDecls = {};\n for (const identity of Object.keys(systems).sort()) sorted[identity] = systems[identity];\n const pretty = JSON.stringify(sorted, null, 4).split('\\n');\n return pretty.map((line: string, index: number) => {\n const prefix = index === 0 ? '\"externalSystems\": ' : '';\n const suffix = index === pretty.length - 1 ? ',' : '';\n return ` ${prefix}${line}${suffix}`;\n });\n}\n\n/**\n * The reviewed `\"remoteServices\": {...}` block (4-space indent), with a trailing comma since `projects`\n * always follows. Omitted entirely when empty, so a repo that declares none keeps its exact file shape.\n * Service names are sorted; each entry is written as it was reviewed.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling formatters here\nfunction remoteServicesLines(services: RemoteServices): string[] {\n if (Object.keys(services).length === 0) return [];\n const sorted: RemoteServices = {};\n for (const name of Object.keys(services).sort()) sorted[name] = services[name];\n const pretty = JSON.stringify(sorted, null, 4).split('\\n');\n return pretty.map((line: string, index: number) => {\n const prefix = index === 0 ? '\"remoteServices\": ' : '';\n const suffix = index === pretty.length - 1 ? ',' : '';\n return ` ${prefix}${line}${suffix}`;\n });\n}\n\n/**\n * Format one project entry's fields (12-space indent). Optional metadata\n * fields are only emitted when present.\n */\nfunction formatEntryLines(entry: GraphEntry): string[] {\n const lines: string[] = [];\n lines.push(` \"level\": ${entry.level},`);\n\n pushOptionalArrayField(lines, 'framework', entry.framework);\n pushOptionalField(lines, 'role', entry.role);\n // Right after role — role says what the project IS, this says what runtime it speaks. MUST be\n // persisted: validate re-derives the runtime graph from this file, so a field written only\n // in-memory by generate would make the two derive different graphs.\n pushOptionalArrayField(lines, 'webpiecesRuntime', entry.webpiecesRuntime);\n pushOptionalField(lines, 'serviceName', entry.serviceName);\n pushOptionalCallsServiceField(lines, entry.callsService);\n pushOptionalBooleanField(lines, 'drawOnGraph', entry.drawOnGraph);\n // MUST be persisted: validate re-derives the runtime graph from this file, so a cut written only\n // in-memory by generate would make generate and validate level (and cycle-check) different graphs.\n pushOptionalArrayField(lines, 'cutLegacyCycle', entry.cutLegacyCycle);\n // MUST be persisted: the runtime graph's product membership is re-derived from this file during\n // validate, and the page's Product filter / color mode read it (#1179).\n pushOptionalArrayField(lines, 'products', entry.products);\n pushOptionalField(lines, 'shortDescription', entry.shortDescription);\n pushOptionalField(lines, 'responsibilitiesFile', entry.responsibilitiesFile);\n pushOptionalField(lines, 'designFile', entry.designFile);\n pushApiRelationsField(lines, entry.apiRelations);\n pushOptionalField(lines, 'runtimeDeclaration', entry.runtimeDeclaration);\n pushOptionalBooleanField(lines, 'runtimeComposition', entry.runtimeComposition);\n\n if (entry.dependsOn.length === 0) {\n lines.push(` \"dependsOn\": []`);\n } else {\n lines.push(` \"dependsOn\": [`);\n entry.dependsOn.forEach((dep: string, depIndex: number) => {\n const depComma = depIndex === entry.dependsOn.length - 1 ? '' : ',';\n lines.push(` ${JSON.stringify(dep)}${depComma}`);\n });\n lines.push(` ]`);\n }\n return lines;\n}\n\n/**\n * Emit one optional string field (12-space indent), skipped when undefined.\n */\nfunction pushOptionalField(lines: string[], field: string, value: string | undefined): void {\n if (value !== undefined) {\n lines.push(` ${JSON.stringify(field)}: ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit the optional `callsService` field (12-space indent). Unlike serviceName it may be EITHER a\n * single service name (string) OR an { apiClassName: serviceName } map, so it is emitted with\n * JSON.stringify to cover both inline. MUST be persisted here: the runtime graph's target resolution\n * reads it back from the committed dependencies.json during validate — dropping it would make\n * generate (which enriches it in-memory) and validate (which loads this file) derive different graphs.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling push*Field helpers here\nfunction pushOptionalCallsServiceField(\n lines: string[],\n value: string | Record<string, string> | undefined,\n): void {\n if (value !== undefined) {\n lines.push(` \"callsService\": ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit one optional boolean field (12-space indent) as a raw JSON boolean\n * (e.g. `\"drawOnGraph\": false,`), skipped when undefined.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling push*Field helpers here\nfunction pushOptionalBooleanField(\n lines: string[],\n field: string,\n value: boolean | undefined,\n): void {\n if (value !== undefined) {\n lines.push(` ${JSON.stringify(field)}: ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit one optional string-array field (12-space indent) as a compact inline\n * JSON array (e.g. `\"framework\": [\"browser\",\"node\"],`), skipped when undefined.\n */\nfunction pushOptionalArrayField(lines: string[], field: string, value: string[] | undefined): void {\n if (value !== undefined) {\n lines.push(` ${JSON.stringify(field)}: ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit the optional `apiRelations` object (pretty, multi-line, reindented under\n * the 12-space entry block) with a trailing comma, since `dependsOn` always\n * follows it. Skipped when absent/empty so plain-lib-only projects stay compact.\n * The scanner already sorts owners + refs, so the JSON is deterministic.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling push*Field helpers here\nfunction pushApiRelationsField(lines: string[], value: ProjectApiRelations | undefined): void {\n if (value === undefined || Object.keys(value).length === 0) return;\n const pretty = JSON.stringify(value, null, 4).split('\\n');\n pretty.forEach((line: string, index: number) => {\n const prefix = index === 0 ? '\"apiRelations\": ' : '';\n const suffix = index === pretty.length - 1 ? ',' : '';\n lines.push(` ${prefix}${line}${suffix}`);\n });\n}\n\n/**\n * Save the graph to disk in the wrapper format with the standard aiInstructions.\n *\n * @param graph - The enriched project graph to save\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: architecture/dependencies.json)\n */\nexport function saveGraph(\n graph: EnhancedGraph,\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH,\n apiContractFiles: ApiContractFileRefs = {},\n externalSystems: ExternalSystemDecls = {},\n remoteServices: RemoteServices = {},\n): void {\n const fullPath = path.join(workspaceRoot, graphPath);\n const dir = path.dirname(fullPath);\n\n // Ensure directory exists\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n // Sort keys for deterministic output\n const sortedGraph: EnhancedGraph = {};\n const sortedKeys = Object.keys(graph).sort();\n for (const key of sortedKeys) {\n sortedGraph[key] = graph[key];\n }\n\n const content = formatGraphJson(\n new DependenciesFile(\n AI_INSTRUCTIONS,\n GRAPH_COMMANDS,\n sortedGraph,\n apiContractFiles,\n externalSystems,\n remoteServices,\n ),\n );\n fs.writeFileSync(fullPath, content, 'utf-8');\n}\n\n/**\n * Check if the graph file exists\n */\nexport function graphFileExists(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH,\n): boolean {\n const fullPath = path.join(workspaceRoot, graphPath);\n return fs.existsSync(fullPath);\n}\n"]}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The DOT/node identity of a reviewed remote service (#1203). Prefixed so it can never collide with a
3
+ * workspace service node (whose identity is its project key), and reduced to DOT-safe characters like
4
+ * every other synthetic node id. One owner so the drawing and the detail panel agree on the id.
5
+ */
6
+ export declare class RemoteServiceNodeId {
7
+ of(name: string): string;
8
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RemoteServiceNodeId = void 0;
4
+ /**
5
+ * The DOT/node identity of a reviewed remote service (#1203). Prefixed so it can never collide with a
6
+ * workspace service node (whose identity is its project key), and reduced to DOT-safe characters like
7
+ * every other synthetic node id. One owner so the drawing and the detail panel agree on the id.
8
+ */
9
+ class RemoteServiceNodeId {
10
+ of(name) {
11
+ return `remote__${name.replace(/[^A-Za-z0-9_]/g, '_')}`;
12
+ }
13
+ }
14
+ exports.RemoteServiceNodeId = RemoteServiceNodeId;
15
+ //# sourceMappingURL=remote-service-node-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-service-node-id.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/remote-service-node-id.ts"],"names":[],"mappings":";;;AAAA;;;;GAIG;AACH,MAAa,mBAAmB;IAC5B,EAAE,CAAC,IAAY;QACX,OAAO,WAAW,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,EAAE,CAAC;IAC5D,CAAC;CACJ;AAJD,kDAIC","sourcesContent":["/**\n * The DOT/node identity of a reviewed remote service (#1203). Prefixed so it can never collide with a\n * workspace service node (whose identity is its project key), and reduced to DOT-safe characters like\n * every other synthetic node id. One owner so the drawing and the detail panel agree on the id.\n */\nexport class RemoteServiceNodeId {\n of(name: string): string {\n return `remote__${name.replace(/[^A-Za-z0-9_]/g, '_')}`;\n }\n}\n"]}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Remote services (#1203)
3
+ *
4
+ * The REVIEWED `remoteServices` table of architecture/dependencies.json: services this workspace calls
5
+ * that are NOT Nx projects in it — a service in another repo, a VM, partner endpoints whose host is set
6
+ * per call. Each is keyed by the service name a `uses` relation targets (`targetService`) and lists the
7
+ * contracts it implements, with their transport:
8
+ *
9
+ * ```json
10
+ * "remoteServices": {
11
+ * "reporter-vm": { "label": "reporter VM", "implements": [{ "api": "ReporterTriggerApi", "type": "rpc" }] }
12
+ * }
13
+ * ```
14
+ *
15
+ * It is approval metadata, exactly like `apiContractFiles`: a human writes it, generation READS it and
16
+ * writes it back unchanged, and nothing ever infers or adds an entry. Absent means "this workspace calls
17
+ * no service outside itself", which is the only reading a repo that never wrote the key can have.
18
+ *
19
+ * The shape is decoded strictly at load time — an unknown key, an empty `implements`, a transport that
20
+ * is not rpc/pubsub — fails the load naming the entry, so a typo can never quietly widen what a `uses`
21
+ * may target.
22
+ */
23
+ /** The transports a remote service can implement. A vendor (`external`) seam is an externalSystem. */
24
+ export type RemoteServiceTransport = 'rpc' | 'pubsub';
25
+ /** One contract a remote service implements, with the transport it is served over. */
26
+ export declare class RemoteServiceApi {
27
+ readonly api: string;
28
+ readonly type: RemoteServiceTransport;
29
+ constructor(api: string, type: RemoteServiceTransport);
30
+ }
31
+ /** One out-of-workspace service: an optional display label and the contracts it implements. */
32
+ export declare class RemoteService {
33
+ /** Display text only; the node identity is the table KEY (the name `uses` targets). */
34
+ readonly label: string | undefined;
35
+ /** Never empty: a remote service that implements nothing could be the target of nothing. */
36
+ readonly implements: RemoteServiceApi[];
37
+ constructor(
38
+ /** Display text only; the node identity is the table KEY (the name `uses` targets). */
39
+ label: string | undefined, implementations: RemoteServiceApi[]);
40
+ implementsApi(api: string, type: string): boolean;
41
+ }
42
+ /** service name -> its reviewed declaration. Serialized as the `remoteServices` key of dependencies.json. */
43
+ export type RemoteServices = Record<string, RemoteService>;
44
+ export declare const REMOTE_SERVICES_RULE = "validate-runtime-architecture";
45
+ /** Strict decoder for the `remoteServices` value of a parsed dependencies.json. */
46
+ export declare class RemoteServicesCodec {
47
+ decode(value: unknown, fullPath: string): RemoteServices;
48
+ private implementations;
49
+ private implementation;
50
+ private record;
51
+ private onlyKeys;
52
+ private fail;
53
+ }
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ /**
3
+ * Remote services (#1203)
4
+ *
5
+ * The REVIEWED `remoteServices` table of architecture/dependencies.json: services this workspace calls
6
+ * that are NOT Nx projects in it — a service in another repo, a VM, partner endpoints whose host is set
7
+ * per call. Each is keyed by the service name a `uses` relation targets (`targetService`) and lists the
8
+ * contracts it implements, with their transport:
9
+ *
10
+ * ```json
11
+ * "remoteServices": {
12
+ * "reporter-vm": { "label": "reporter VM", "implements": [{ "api": "ReporterTriggerApi", "type": "rpc" }] }
13
+ * }
14
+ * ```
15
+ *
16
+ * It is approval metadata, exactly like `apiContractFiles`: a human writes it, generation READS it and
17
+ * writes it back unchanged, and nothing ever infers or adds an entry. Absent means "this workspace calls
18
+ * no service outside itself", which is the only reading a repo that never wrote the key can have.
19
+ *
20
+ * The shape is decoded strictly at load time — an unknown key, an empty `implements`, a transport that
21
+ * is not rpc/pubsub — fails the load naming the entry, so a typo can never quietly widen what a `uses`
22
+ * may target.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.RemoteServicesCodec = exports.REMOTE_SERVICES_RULE = exports.RemoteService = exports.RemoteServiceApi = void 0;
26
+ const rules_config_1 = require("@webpieces/rules-config");
27
+ /** One contract a remote service implements, with the transport it is served over. */
28
+ class RemoteServiceApi {
29
+ api;
30
+ type;
31
+ constructor(api, type) {
32
+ this.api = api;
33
+ this.type = type;
34
+ }
35
+ }
36
+ exports.RemoteServiceApi = RemoteServiceApi;
37
+ /** One out-of-workspace service: an optional display label and the contracts it implements. */
38
+ class RemoteService {
39
+ label;
40
+ /** Never empty: a remote service that implements nothing could be the target of nothing. */
41
+ implements;
42
+ constructor(
43
+ /** Display text only; the node identity is the table KEY (the name `uses` targets). */
44
+ label, implementations) {
45
+ this.label = label;
46
+ this.implements = implementations;
47
+ }
48
+ implementsApi(api, type) {
49
+ return this.implements.some((entry) => entry.api === api && entry.type === type);
50
+ }
51
+ }
52
+ exports.RemoteService = RemoteService;
53
+ exports.REMOTE_SERVICES_RULE = 'validate-runtime-architecture';
54
+ const SERVICE_KEYS = ['label', 'implements'];
55
+ const API_KEYS = ['api', 'type'];
56
+ /** Strict decoder for the `remoteServices` value of a parsed dependencies.json. */
57
+ class RemoteServicesCodec {
58
+ // webpieces-disable no-any-unknown -- untrusted JSON is narrowed and validated at the input boundary
59
+ decode(value, fullPath) {
60
+ if (value === undefined)
61
+ return {};
62
+ const table = this.record(value, fullPath, 'remoteServices');
63
+ const services = {};
64
+ for (const name of Object.keys(table).sort()) {
65
+ const where = `remoteServices.${name}`;
66
+ if (name.trim() === '' || name !== name.trim())
67
+ this.fail(fullPath, `${where}: a remote service name must be a non-empty name without surrounding spaces.`);
68
+ const entry = this.record(table[name], fullPath, where);
69
+ this.onlyKeys(entry, SERVICE_KEYS, fullPath, where);
70
+ const label = entry['label'];
71
+ if (label !== undefined && (typeof label !== 'string' || label.trim() === ''))
72
+ this.fail(fullPath, `${where}.label must be a non-empty string when present.`);
73
+ services[name] = new RemoteService(label, this.implementations(entry['implements'], fullPath, where));
74
+ }
75
+ return services;
76
+ }
77
+ // webpieces-disable no-any-unknown -- untrusted JSON is narrowed and validated at the input boundary
78
+ implementations(value, fullPath, where) {
79
+ if (!Array.isArray(value) || value.length === 0)
80
+ this.fail(fullPath, `${where}.implements must be a non-empty array of { "api": "<ApiName>", "type": "rpc" | "pubsub" }.`);
81
+ const seen = new Set();
82
+ const result = [];
83
+ for (let index = 0; index < value.length; index++) {
84
+ const entry = this.implementation(value[index], fullPath, `${where}.implements[${index}]`);
85
+ if (seen.has(entry.api))
86
+ this.fail(fullPath, `${where}.implements[${index}]: ${entry.api} is declared twice in ${where}.implements.`);
87
+ seen.add(entry.api);
88
+ result.push(entry);
89
+ }
90
+ return result;
91
+ }
92
+ // webpieces-disable no-any-unknown -- untrusted JSON is narrowed and validated at the input boundary
93
+ implementation(item, fullPath, at) {
94
+ const entry = this.record(item, fullPath, at);
95
+ this.onlyKeys(entry, API_KEYS, fullPath, at);
96
+ const api = entry['api'];
97
+ const type = entry['type'];
98
+ if (typeof api !== 'string' || api.trim() === '')
99
+ this.fail(fullPath, `${at}.api must be the contract's class name (e.g. "ReporterTriggerApi").`);
100
+ if (type !== 'rpc' && type !== 'pubsub')
101
+ this.fail(fullPath, `${at}.type must be "rpc" or "pubsub", got ${JSON.stringify(type)}. A vendor contract is an externalSystem, not a remote service.`);
102
+ return new RemoteServiceApi(api, type);
103
+ }
104
+ // webpieces-disable no-any-unknown -- untrusted JSON is narrowed and validated at the input boundary
105
+ record(value, fullPath, where) {
106
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
107
+ this.fail(fullPath, `${where} must be a JSON object.`);
108
+ return value;
109
+ }
110
+ // webpieces-disable no-any-unknown -- untrusted JSON is narrowed and validated at the input boundary
111
+ onlyKeys(entry, allowed, fullPath, where) {
112
+ for (const key of Object.keys(entry))
113
+ if (!allowed.includes(key))
114
+ this.fail(fullPath, `${where}: unknown key "${key}" (allowed: ${allowed.join(', ')}).`);
115
+ }
116
+ fail(fullPath, message) {
117
+ throw new rules_config_1.RuleFailError(exports.REMOTE_SERVICES_RULE, `${fullPath}: ${message}`, undefined, undefined, [
118
+ new rules_config_1.Option('Fix the reviewed remoteServices entry in architecture/dependencies.json: each key is a service name outside this workspace, mapped to { "label"?: "<display text>", "implements": [{ "api": "<ApiName>", "type": "rpc" | "pubsub" }] }. Generation never writes or infers this table.', true),
119
+ ]);
120
+ }
121
+ }
122
+ exports.RemoteServicesCodec = RemoteServicesCodec;
123
+ //# sourceMappingURL=remote-services.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-services.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/remote-services.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Remote services (#1203)\n *\n * The REVIEWED `remoteServices` table of architecture/dependencies.json: services this workspace calls\n * that are NOT Nx projects in it — a service in another repo, a VM, partner endpoints whose host is set\n * per call. Each is keyed by the service name a `uses` relation targets (`targetService`) and lists the\n * contracts it implements, with their transport:\n *\n * ```json\n * \"remoteServices\": {\n * \"reporter-vm\": { \"label\": \"reporter VM\", \"implements\": [{ \"api\": \"ReporterTriggerApi\", \"type\": \"rpc\" }] }\n * }\n * ```\n *\n * It is approval metadata, exactly like `apiContractFiles`: a human writes it, generation READS it and\n * writes it back unchanged, and nothing ever infers or adds an entry. Absent means \"this workspace calls\n * no service outside itself\", which is the only reading a repo that never wrote the key can have.\n *\n * The shape is decoded strictly at load time — an unknown key, an empty `implements`, a transport that\n * is not rpc/pubsub — fails the load naming the entry, so a typo can never quietly widen what a `uses`\n * may target.\n */\n\nimport { Option, RuleFailError } from '@webpieces/rules-config';\n\n/** The transports a remote service can implement. A vendor (`external`) seam is an externalSystem. */\nexport type RemoteServiceTransport = 'rpc' | 'pubsub';\n\n/** One contract a remote service implements, with the transport it is served over. */\nexport class RemoteServiceApi {\n constructor(\n public readonly api: string,\n public readonly type: RemoteServiceTransport,\n ) {}\n}\n\n/** One out-of-workspace service: an optional display label and the contracts it implements. */\nexport class RemoteService {\n /** Never empty: a remote service that implements nothing could be the target of nothing. */\n readonly implements: RemoteServiceApi[];\n\n constructor(\n /** Display text only; the node identity is the table KEY (the name `uses` targets). */\n public readonly label: string | undefined,\n implementations: RemoteServiceApi[],\n ) {\n this.implements = implementations;\n }\n\n implementsApi(api: string, type: string): boolean {\n return this.implements.some(\n (entry: RemoteServiceApi) => entry.api === api && entry.type === type,\n );\n }\n}\n\n/** service name -> its reviewed declaration. Serialized as the `remoteServices` key of dependencies.json. */\nexport type RemoteServices = Record<string, RemoteService>;\n\nexport const REMOTE_SERVICES_RULE = 'validate-runtime-architecture';\n\nconst SERVICE_KEYS = ['label', 'implements'];\nconst API_KEYS = ['api', 'type'];\n\n/** Strict decoder for the `remoteServices` value of a parsed dependencies.json. */\nexport class RemoteServicesCodec {\n // webpieces-disable no-any-unknown -- untrusted JSON is narrowed and validated at the input boundary\n decode(value: unknown, fullPath: string): RemoteServices {\n if (value === undefined) return {};\n const table = this.record(value, fullPath, 'remoteServices');\n const services: RemoteServices = {};\n for (const name of Object.keys(table).sort()) {\n const where = `remoteServices.${name}`;\n if (name.trim() === '' || name !== name.trim())\n this.fail(fullPath, `${where}: a remote service name must be a non-empty name without surrounding spaces.`);\n const entry = this.record(table[name], fullPath, where);\n this.onlyKeys(entry, SERVICE_KEYS, fullPath, where);\n const label = entry['label'];\n if (label !== undefined && (typeof label !== 'string' || label.trim() === ''))\n this.fail(fullPath, `${where}.label must be a non-empty string when present.`);\n services[name] = new RemoteService(\n label as string | undefined,\n this.implementations(entry['implements'], fullPath, where),\n );\n }\n return services;\n }\n\n // webpieces-disable no-any-unknown -- untrusted JSON is narrowed and validated at the input boundary\n private implementations(value: unknown, fullPath: string, where: string): RemoteServiceApi[] {\n if (!Array.isArray(value) || value.length === 0)\n this.fail(\n fullPath,\n `${where}.implements must be a non-empty array of { \"api\": \"<ApiName>\", \"type\": \"rpc\" | \"pubsub\" }.`,\n );\n const seen = new Set<string>();\n const result: RemoteServiceApi[] = [];\n for (let index = 0; index < value.length; index++) {\n const entry = this.implementation(value[index], fullPath, `${where}.implements[${index}]`);\n if (seen.has(entry.api))\n this.fail(fullPath, `${where}.implements[${index}]: ${entry.api} is declared twice in ${where}.implements.`);\n seen.add(entry.api);\n result.push(entry);\n }\n return result;\n }\n\n // webpieces-disable no-any-unknown -- untrusted JSON is narrowed and validated at the input boundary\n private implementation(item: unknown, fullPath: string, at: string): RemoteServiceApi {\n const entry = this.record(item, fullPath, at);\n this.onlyKeys(entry, API_KEYS, fullPath, at);\n const api = entry['api'];\n const type = entry['type'];\n if (typeof api !== 'string' || api.trim() === '')\n this.fail(fullPath, `${at}.api must be the contract's class name (e.g. \"ReporterTriggerApi\").`);\n if (type !== 'rpc' && type !== 'pubsub')\n this.fail(\n fullPath,\n `${at}.type must be \"rpc\" or \"pubsub\", got ${JSON.stringify(type)}. A vendor contract is an externalSystem, not a remote service.`,\n );\n return new RemoteServiceApi(api, type);\n }\n\n // webpieces-disable no-any-unknown -- untrusted JSON is narrowed and validated at the input boundary\n private record(value: unknown, fullPath: string, where: string): Record<string, unknown> {\n if (value === null || typeof value !== 'object' || Array.isArray(value))\n this.fail(fullPath, `${where} must be a JSON object.`);\n return value as Record<string, unknown>;\n }\n\n // webpieces-disable no-any-unknown -- untrusted JSON is narrowed and validated at the input boundary\n private onlyKeys(entry: Record<string, unknown>, allowed: string[], fullPath: string, where: string): void {\n for (const key of Object.keys(entry))\n if (!allowed.includes(key))\n this.fail(fullPath, `${where}: unknown key \"${key}\" (allowed: ${allowed.join(', ')}).`);\n }\n\n private fail(fullPath: string, message: string): never {\n throw new RuleFailError(REMOTE_SERVICES_RULE, `${fullPath}: ${message}`, undefined, undefined, [\n new Option(\n 'Fix the reviewed remoteServices entry in architecture/dependencies.json: each key is a service name outside this workspace, mapped to { \"label\"?: \"<display text>\", \"implements\": [{ \"api\": \"<ApiName>\", \"type\": \"rpc\" | \"pubsub\" }] }. Generation never writes or infers this table.',\n true,\n ),\n ]);\n }\n}\n"]}
@@ -10,6 +10,13 @@ export declare class RuntimeDetails {
10
10
  readonly nodes: Record<string, RuntimeNodeDetails>;
11
11
  readonly edges: Record<string, string[]>;
12
12
  constructor(graph: RuntimeGraph, showExternalNodes: boolean, model?: GraphRenderModel | null);
13
+ /** A reviewed remote service's node details: the contracts its declaration says it implements. */
14
+ private remoteNodes;
15
+ /**
16
+ * The edge details for `name`'s calls into reviewed remote services (#1203), also appended to
17
+ * `used`; returns the contracts covered so they are not reported as an unknown target.
18
+ */
19
+ private remoteCalls;
13
20
  /** Bind detail facts to renderer-owned identities, including grouped queue hops. */
14
21
  private renderEdges;
15
22
  script(): string;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RuntimeDetails = exports.RuntimeNodeDetails = void 0;
4
+ const remote_service_node_id_1 = require("./remote-service-node-id");
4
5
  class RuntimeNodeDetails {
5
6
  implemented;
6
7
  used;
@@ -29,9 +30,12 @@ class RuntimeDetails {
29
30
  if (edge.type !== 'pubsub')
30
31
  this.edges[`${edge.from}->${edge.to}`] = entries;
31
32
  }
33
+ const remoteApis = this.remoteCalls(graph, name, service.wiringUses === undefined ? used : []);
32
34
  for (const api of service.wiringUses === undefined ? service.uses : []) {
33
35
  if (graph.runtimeEdges.some((edge) => edge.from === name && edge.via.includes(api)))
34
36
  continue;
37
+ if (remoteApis.has(api))
38
+ continue;
35
39
  const contract = graph.apis[api];
36
40
  const external = contract?.type === 'external' || contract?.implementedBy.length === 0;
37
41
  used.push(`${contract?.owner ?? '?'}#${api} → ${external ? (contract?.externalSystem?.label ?? contract?.owner ?? 'unknown external') : 'unknown / inferred target'}` +
@@ -63,9 +67,32 @@ class RuntimeDetails {
63
67
  }
64
68
  this.nodes[name] = new RuntimeNodeDetails(implemented, [...new Set(used)]);
65
69
  }
70
+ this.remoteNodes(graph);
66
71
  if (model !== null)
67
72
  this.renderEdges(graph, model);
68
73
  }
74
+ /** A reviewed remote service's node details: the contracts its declaration says it implements. */
75
+ remoteNodes(graph) {
76
+ for (const [name, remote] of Object.entries(graph.remoteServices ?? {}))
77
+ this.nodes[new remote_service_node_id_1.RemoteServiceNodeId().of(name)] = new RuntimeNodeDetails(remote.implements.map((api) => `${api} — declared in remoteServices.${name}`), []);
78
+ }
79
+ /**
80
+ * The edge details for `name`'s calls into reviewed remote services (#1203), also appended to
81
+ * `used`; returns the contracts covered so they are not reported as an unknown target.
82
+ */
83
+ remoteCalls(graph, name, used) {
84
+ const covered = new Set();
85
+ for (const [remoteName, remote] of Object.entries(graph.remoteServices ?? {}))
86
+ for (const call of remote.calledBy.filter((entry) => entry.from === name)) {
87
+ const entries = call.via.map((api) => `${graph.apis[api]?.owner ?? '?'}#${api} → ${remoteName} (remote service, ${call.type})`);
88
+ for (const api of call.via)
89
+ covered.add(api);
90
+ used.push(...entries);
91
+ const key = `${name}->${new remote_service_node_id_1.RemoteServiceNodeId().of(remoteName)}`;
92
+ this.edges[key] = [...(this.edges[key] ?? []), ...entries];
93
+ }
94
+ return covered;
95
+ }
69
96
  /** Bind detail facts to renderer-owned identities, including grouped queue hops. */
70
97
  renderEdges(graph, model) {
71
98
  for (const edge of model.edges) {