@webpieces/nx-webpieces-rules 0.4.882 → 0.4.883

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 (76) hide show
  1. package/README.md +40 -13
  2. package/package.json +7 -7
  3. package/src/configs/tag-truth-configs.d.ts +15 -0
  4. package/src/configs/tag-truth-configs.js +20 -1
  5. package/src/configs/tag-truth-configs.js.map +1 -1
  6. package/src/executors/generate/executor.js +3 -0
  7. package/src/executors/generate/executor.js.map +1 -1
  8. package/src/index.d.ts +6 -3
  9. package/src/index.js +15 -1
  10. package/src/index.js.map +1 -1
  11. package/src/lib/graph-color-modes.d.ts +50 -6
  12. package/src/lib/graph-color-modes.js +88 -8
  13. package/src/lib/graph-color-modes.js.map +1 -1
  14. package/src/lib/graph-comparator.js +2 -0
  15. package/src/lib/graph-comparator.js.map +1 -1
  16. package/src/lib/graph-entry-levels.d.ts +35 -0
  17. package/src/lib/graph-entry-levels.js +60 -0
  18. package/src/lib/graph-entry-levels.js.map +1 -0
  19. package/src/lib/graph-impact.d.ts +5 -5
  20. package/src/lib/graph-impact.js +5 -5
  21. package/src/lib/graph-impact.js.map +1 -1
  22. package/src/lib/graph-legend.d.ts +11 -1
  23. package/src/lib/graph-legend.js +44 -6
  24. package/src/lib/graph-legend.js.map +1 -1
  25. package/src/lib/graph-level-bands.js +3 -1
  26. package/src/lib/graph-level-bands.js.map +1 -1
  27. package/src/lib/graph-loader.d.ts +7 -1
  28. package/src/lib/graph-loader.js +10 -1
  29. package/src/lib/graph-loader.js.map +1 -1
  30. package/src/lib/graph-metadata.d.ts +2 -0
  31. package/src/lib/graph-metadata.js +19 -0
  32. package/src/lib/graph-metadata.js.map +1 -1
  33. package/src/lib/graph-page-shell.d.ts +28 -6
  34. package/src/lib/graph-page-shell.js +55 -18
  35. package/src/lib/graph-page-shell.js.map +1 -1
  36. package/src/lib/graph-page-styles.js +3 -1
  37. package/src/lib/graph-page-styles.js.map +1 -1
  38. package/src/lib/graph-products.d.ts +79 -0
  39. package/src/lib/graph-products.js +199 -0
  40. package/src/lib/graph-products.js.map +1 -0
  41. package/src/lib/graph-render-model.d.ts +13 -2
  42. package/src/lib/graph-render-model.js +36 -4
  43. package/src/lib/graph-render-model.js.map +1 -1
  44. package/src/lib/graph-sorter.d.ts +13 -0
  45. package/src/lib/graph-sorter.js.map +1 -1
  46. package/src/lib/graph-visualizer.client.d.ts +20 -10
  47. package/src/lib/graph-visualizer.client.js +67 -31
  48. package/src/lib/graph-visualizer.client.js.map +1 -1
  49. package/src/lib/graph-visualizer.js +7 -6
  50. package/src/lib/graph-visualizer.js.map +1 -1
  51. package/src/lib/product-resolver.d.ts +38 -0
  52. package/src/lib/product-resolver.js +63 -0
  53. package/src/lib/product-resolver.js.map +1 -0
  54. package/src/lib/runtime-graph-model.d.ts +7 -0
  55. package/src/lib/runtime-graph-model.js.map +1 -1
  56. package/src/lib/runtime-graph.js +3 -0
  57. package/src/lib/runtime-graph.js.map +1 -1
  58. package/src/lib/runtime-html-page.d.ts +7 -0
  59. package/src/lib/runtime-html-page.js +29 -0
  60. package/src/lib/runtime-html-page.js.map +1 -1
  61. package/src/lib/runtime-visualizer.client.d.ts +7 -0
  62. package/src/lib/runtime-visualizer.client.js +32 -0
  63. package/src/lib/runtime-visualizer.client.js.map +1 -1
  64. package/src/lib/runtime-visualizer.js +3 -1
  65. package/src/lib/runtime-visualizer.js.map +1 -1
  66. package/src/lib/saved-snapshot.js +6 -2
  67. package/src/lib/saved-snapshot.js.map +1 -1
  68. package/src/lib/tag-truth.d.ts +20 -3
  69. package/src/lib/tag-truth.js +48 -7
  70. package/src/lib/tag-truth.js.map +1 -1
  71. package/src/nx-policy-registry.js +1 -0
  72. package/src/nx-policy-registry.js.map +1 -1
  73. package/src/rule-help.js +1 -0
  74. package/src/rule-help.js.map +1 -1
  75. package/src/rule-settings.js +12 -1
  76. package/src/rule-settings.js.map +1 -1
@@ -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, shortDescription,\n * responsibilitiesFile, designFile, dependsOn }\n * }\n * }\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 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 * \"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"]}
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * Fills the AI-oriented fields on each architecture/dependencies.json entry:
5
5
  * framework — from `framework:<x>` nx tag or package.json inference
6
+ * products — the products reaching it, from the `product:<name>` nx tags (#1179)
7
+ * level — re-levelled so servers, clients and apps form the top row (#1179)
6
8
  * serviceName — from project.json metadata.webpieces.serviceName; the
7
9
  * name clients address this app by at runtime
8
10
  * shortDescription — first paragraph of the project's responsibilities.md
@@ -4,6 +4,8 @@
4
4
  *
5
5
  * Fills the AI-oriented fields on each architecture/dependencies.json entry:
6
6
  * framework — from `framework:<x>` nx tag or package.json inference
7
+ * products — the products reaching it, from the `product:<name>` nx tags (#1179)
8
+ * level — re-levelled so servers, clients and apps form the top row (#1179)
7
9
  * serviceName — from project.json metadata.webpieces.serviceName; the
8
10
  * name clients address this app by at runtime
9
11
  * shortDescription — first paragraph of the project's responsibilities.md
@@ -30,6 +32,9 @@ const framework_resolver_1 = require("./framework-resolver");
30
32
  const role_resolver_1 = require("./role-resolver");
31
33
  const draw_on_graph_resolver_1 = require("./draw-on-graph-resolver");
32
34
  const cut_legacy_cycle_resolver_1 = require("./cut-legacy-cycle-resolver");
35
+ const product_resolver_1 = require("./product-resolver");
36
+ const graph_products_1 = require("./graph-products");
37
+ const graph_entry_levels_1 = require("./graph-entry-levels");
33
38
  const runtime_participant_resolver_1 = require("./runtime-participant-resolver");
34
39
  const service_name_resolver_1 = require("./service-name-resolver");
35
40
  const responsibilities_1 = require("./responsibilities");
@@ -70,6 +75,9 @@ function enrichGraph(graph, infos, workspaceRoot) {
70
75
  const problems = [];
71
76
  // project -> declared serviceName, collected so duplicates can be reported across the workspace.
72
77
  const serviceNames = new Map();
78
+ // project -> the products it DECLARES (`product:` tags); membership is derived from these below.
79
+ const productSeeds = new Map();
80
+ const products = new product_resolver_1.ProductResolver();
73
81
  for (const [projectName, entry] of Object.entries(graph)) {
74
82
  const info = infos.get(projectName);
75
83
  if (!info) {
@@ -77,6 +85,10 @@ function enrichGraph(graph, infos, workspaceRoot) {
77
85
  continue;
78
86
  }
79
87
  enrichDeclarations(entry, info, workspaceRoot, problems);
88
+ const declared = products.resolve(info);
89
+ problems.push(...declared.problems);
90
+ if (declared.products.length > 0)
91
+ productSeeds.set(projectName, declared.products);
80
92
  enrichClientNames(entry, info, workspaceRoot, projectName, serviceNames, problems);
81
93
  enrichResponsibilities(entry, info, workspaceRoot, problems);
82
94
  // Set designFile ONLY when the project has a REAL generated design (a
@@ -96,6 +108,13 @@ function enrichGraph(graph, infos, workspaceRoot) {
96
108
  if (problems.length > 0) {
97
109
  throw new MetadataValidationError(problems);
98
110
  }
111
+ // #1179: a library belongs to every product whose tagged project reaches it — derived, never tagged.
112
+ new graph_products_1.ProductMembership().compute(productSeeds, graph).forEach((member, projectName) => {
113
+ graph[projectName].products = member;
114
+ });
115
+ // #1179: servers, clients and apps form the top row. Here, once roles are resolved, so generate,
116
+ // validate-architecture-unchanged and validate-api-relations all compute identical levels.
117
+ new graph_entry_levels_1.EntryPointLevels(exports.APP_ROLES).promote(graph);
99
118
  }
100
119
  /**
101
120
  * The fields a project DECLARES about itself — its framework env set and role (nx tags), the
@@ -1 +1 @@
1
- {"version":3,"file":"graph-metadata.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Metadata Enrichment\n *\n * Fills the AI-oriented fields on each architecture/dependencies.json entry:\n * framework — from `framework:<x>` nx tag or package.json inference\n * serviceName — from project.json metadata.webpieces.serviceName; the\n * name clients address this app by at runtime\n * shortDescription — first paragraph of the project's responsibilities.md\n * responsibilitiesFile — repo-relative path to the required responsibilities.md\n * designFile — repo-relative path to the generated DI design.json\n *\n * Validation is aggregated: ALL problems across ALL projects are collected and\n * thrown as one MetadataValidationError so a repo adopting this sees the full\n * seeding list in a single run. Callers must enrich BEFORE writing any file so\n * a failed run never clobbers dependencies.json.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { createProjectGraphAsync } from '@nx/devkit';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport { ProjectInfo } from './project-info';\nimport { resolveFramework } from './framework-resolver';\nimport { resolveRole } from './role-resolver';\nimport { resolveDrawOnGraph } from './draw-on-graph-resolver';\nimport { resolveCutLegacyCycles } from './cut-legacy-cycle-resolver';\nimport { resolveRuntimeParticipant } from './runtime-participant-resolver';\nimport { resolveCallsService, resolveServiceName, validateUniqueServiceNames } from './service-name-resolver';\nimport { extractShortDescription, validateShortDescription } from './responsibilities';\nimport { toError } from '../toError';\n\nexport const RESPONSIBILITIES_FILE_NAME = 'responsibilities.md';\n\n/**\n * Thrown when one or more projects fail metadata validation (missing/invalid\n * responsibilities.md, bad framework tags, ...). Executors catch this to point\n * AI at the webpieces.responsibilities.md instructions template.\n */\nexport class MetadataValidationError extends Error {\n constructor(public readonly problems: string[]) {\n super(\n `Architecture metadata validation failed (${problems.length} problem(s)):\\n` +\n problems.map((problem: string) => ` - ${problem}`).join('\\n')\n );\n this.name = 'MetadataValidationError';\n }\n}\n\n/**\n * Read per-project root + tags from nx's project graph.\n */\nexport async function collectProjectInfo(): Promise<Map<string, ProjectInfo>> {\n const projectGraph = await createProjectGraphAsync();\n const infos = new Map<string, ProjectInfo>();\n for (const [name, node] of Object.entries(projectGraph.nodes)) {\n infos.set(name, new ProjectInfo(name, node.data.root, node.data.tags ?? []));\n }\n return infos;\n}\n\n/**\n * Enrich every graph entry in place with framework, shortDescription,\n * responsibilitiesFile and designFile. Throws MetadataValidationError listing\n * every problem when any project fails validation.\n */\nexport function enrichGraph(\n graph: EnhancedGraph,\n infos: Map<string, ProjectInfo>,\n workspaceRoot: string\n): void {\n const problems: string[] = [];\n // project -> declared serviceName, collected so duplicates can be reported across the workspace.\n const serviceNames = new Map<string, string>();\n\n for (const [projectName, entry] of Object.entries(graph)) {\n const info = infos.get(projectName);\n if (!info) {\n problems.push(`${projectName}: not found in nx project graph`);\n continue;\n }\n\n enrichDeclarations(entry, info, workspaceRoot, problems);\n enrichClientNames(entry, info, workspaceRoot, projectName, serviceNames, problems);\n enrichResponsibilities(entry, info, workspaceRoot, problems);\n\n // Set designFile ONLY when the project has a REAL generated design (a\n // non-empty `designs[]`), i.e. it has a @DocumentDesign root. A project\n // without one has NO design.json at all (di-graph-generate stopped writing\n // empty ones and reaps stale ones), and legacy repos may still carry an\n // empty `{ designs: [] }` until their next build — both must read the same\n // way, and neither gets a \"View Design\" item on its node menu in the arch\n // viz (designLinks keys off designFile). See graph-visualizer.ts.\n if (hasGeneratedDesign(workspaceRoot, info.root)) {\n entry.designFile = toRepoRelative(info.root, 'design.json');\n }\n }\n\n validateUniqueServiceNames(serviceNames, problems);\n validateLibraryTypesMatch(graph, problems);\n validateRoleDependencies(graph, problems);\n\n if (problems.length > 0) {\n throw new MetadataValidationError(problems);\n }\n}\n\n/**\n * The fields a project DECLARES about itself — its framework env set and role (nx tags), the\n * webpieces runtime packages it depends on (package.json), and whether it opts out of the drawings\n * (nx tag). Grouped because they share a shape: resolve, record the problem, or write the field.\n *\n * Distinct from enrichResponsibilities/designFile, which read generated or hand-written FILES.\n */\n// webpieces-disable no-function-outside-class -- enrichGraph step helper, matching enrichClientNames/enrichResponsibilities in this file\nfunction enrichDeclarations(\n entry: GraphEntry,\n info: ProjectInfo,\n workspaceRoot: string,\n problems: string[]\n): void {\n const resolution = resolveFramework(info, workspaceRoot);\n if (resolution.problem !== null) {\n problems.push(resolution.problem);\n } else if (resolution.frameworks !== null) {\n entry.framework = resolution.frameworks;\n }\n\n const roleResolution = resolveRole(info);\n if (roleResolution.problem !== null) {\n problems.push(roleResolution.problem);\n } else if (roleResolution.role !== null) {\n entry.role = roleResolution.role;\n }\n\n // Only persist when the project declares at least one — a repo full of\n // \"webpiecesRuntime\": [] lines would be noise, and absence already means \"declares none\".\n const participantResolution = resolveRuntimeParticipant(info, workspaceRoot);\n if (participantResolution.problem !== null) {\n problems.push(participantResolution.problem);\n } else if (participantResolution.markers !== null && participantResolution.markers.length > 0) {\n entry.webpiecesRuntime = participantResolution.markers;\n }\n\n // Only persist the field when hidden (false); drawn projects (the\n // default) stay clean in dependencies.json with no drawOnGraph line.\n const drawResolution = resolveDrawOnGraph(info);\n if (drawResolution.problem !== null) {\n problems.push(drawResolution.problem);\n } else if (drawResolution.drawOnGraph === false) {\n entry.drawOnGraph = false;\n }\n\n // Only persist when the project actually cuts an edge — absence is the normal state, and a repo\n // full of \"cutLegacyCycle\": [] lines would bury the ones that mean something.\n const cutResolution = resolveCutLegacyCycles(info);\n if (cutResolution.problem !== null) {\n problems.push(cutResolution.problem);\n } else if (cutResolution.targets !== null && cutResolution.targets.length > 0) {\n entry.cutLegacyCycle = cutResolution.targets;\n }\n}\n\n/**\n * Roles that are terminal APPS — nothing may depend on them. A server, a\n * non-HTTP `app`, or a client is a top-level runnable; being depended upon means\n * it is really a library and should be retagged `role:lib`/`role:designed-lib`.\n */\nexport const APP_ROLES: ReadonlyArray<string> = ['server', 'app', 'client'];\n\n/**\n * Compatibility lattice — the \"up-set\" of each atomic env is the env itself\n * PLUS every ancestor it can legally consume code from (specialization edges\n * child → parent: react → browser, angular → browser, express → node). A\n * consumer promising env `c` can be satisfied by any dependency env in `up(c)`.\n *\n * `react-native` is a first-class runtime and NOT a specialisation of browser (#1064, D6): it has no\n * DOM, no IndexedDB and no OPFS, so a react-native consumer may only consume libraries that are\n * themselves tagged `framework:react-native` (a universal browser+node+react-native library, a\n * browser+react-native one, or a react-native-only one).\n */\nexport const ENV_UP_SETS: Readonly<Record<string, ReadonlyArray<string>>> = {\n react: ['react', 'browser'],\n angular: ['angular', 'browser'],\n browser: ['browser'],\n express: ['express', 'node'],\n node: ['node'],\n 'react-native': ['react-native'],\n};\n\n/** The up-set of an env (env itself + ancestors); unknown envs map to just themselves. */\nfunction upSet(env: string): ReadonlyArray<string> {\n return ENV_UP_SETS[env] ?? [env];\n}\n\n/**\n * `library-types-match-client` rule.\n *\n * A project's `framework` field is its libType — the SET of runtime\n * environments it is validated to run in (browser | react | angular | node |\n * express | react-native). For a dependency edge Consumer C → Library L, the edge is LEGAL iff\n * for EVERY env `c` in C's set, up(c) ∩ L's set ≠ ∅ — i.e. every environment\n * the consumer promises to run in can be satisfied by the dependency. This keeps\n * an express app from depending on a browser-only lib, and lets a `browser+node`\n * lib be consumed by both react and express projects. Every violation is\n * appended to `problems` so `arch:generate` fails with the full list.\n */\nexport function validateLibraryTypesMatch(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromSet = entry.framework;\n if (fromSet === undefined) continue; // framework resolution already flagged this project\n\n for (const dep of entry.dependsOn) {\n const depEntry = graph[dep];\n const toSet = depEntry?.framework;\n if (toSet === undefined) continue;\n\n const unsatisfied = fromSet.filter(\n (env: string) => !upSet(env).some((up: string) => toSet.includes(up))\n );\n if (unsatisfied.length === 0) continue;\n\n problems.push(\n `library-types-match-client: '${projectName}' [${fromSet.join(', ')}] must not depend on ` +\n `'${dep}' [${toSet.join(', ')}] — the consumer env(s) ${unsatisfied.join(', ')} cannot be ` +\n `satisfied by the dependency (each consumer env must resolve to itself or an ancestor it ` +\n `consumes from: react/angular→browser, express→node, react-native→react-native only). ` +\n `Widen '${dep}' framework tags or ` +\n `remove the dependency.`\n );\n }\n }\n}\n\n/**\n * `role-dependency` rule.\n *\n * A project's `role` is its function (server | app | bundle | designed-lib | lib | client |\n * api-lib | api-client). The two api roles are libraries, never apps, so this rule leaves them alone;\n * what THEY may depend on is `api-lib-dependencies` (tag-truth.ts).\n * Apps are terminal — libraries and clients consume them, never the reverse:\n * - a `client` is fully terminal: NOTHING may depend on it.\n * - a `server` may only be depended upon by another `server` — the one\n * legitimate case is a server-side orchestrator/e2e harness that boots\n * other servers. A `lib`/`designed-lib`/`client` depending on a `server`\n * inverts the dependency direction and is a violation.\n * - a `bundle` is the one role permitted to depend on ANY app: it aggregates\n * several apps into one distributable (e.g. an nx plugin re-exposing multiple\n * tooling apps), so a `bundle → app` edge is legitimate, not inverted.\n */\nexport function validateRoleDependencies(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromRole = entry.role;\n for (const dep of entry.dependsOn) {\n const toRole = graph[dep]?.role;\n if (toRole === undefined) continue; // role resolution already flagged this project\n if (!APP_ROLES.includes(toRole)) continue;\n // A server may orchestrate/boot other servers (e.g. an e2e harness).\n if (toRole === 'server' && fromRole === 'server') continue;\n // A bundle aggregates apps — it may depend on any app role.\n if (fromRole === 'bundle') continue;\n\n const why =\n toRole === 'client'\n ? `a 'client' app is terminal and may never be depended upon`\n : `a 'server' may only be depended upon by another 'server' (an orchestrator/e2e harness)`;\n problems.push(\n `role-dependency: '${projectName}' (role:${fromRole ?? 'none'}) must not depend on ` +\n `'${dep}' (role:${toRole}) — ${why}. Retag '${dep}' role:lib/role:designed-lib if it ` +\n `is actually a library, or remove the dependency.`\n );\n }\n }\n}\n\n/**\n * Fill the two symmetric client-addressing fields — `serviceName` (the name clients address THIS app\n * by) and `callsService` (the name THIS app's clients call when no literal ClientConfig sits at the\n * call site). Both are declared in project.json, never derived (see service-name-resolver.ts).\n */\n// webpieces-disable no-function-outside-class -- pure enrichment helper, mirrors enrichResponsibilities\nfunction enrichClientNames(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n projectName: string,\n serviceNames: Map<string, string>,\n problems: string[]\n): void {\n const serviceResolution = resolveServiceName(info, workspaceRoot);\n if (serviceResolution.problem !== null) {\n problems.push(serviceResolution.problem);\n } else if (serviceResolution.serviceName !== null) {\n entry.serviceName = serviceResolution.serviceName;\n serviceNames.set(projectName, serviceResolution.serviceName);\n }\n\n // Consumed by the runtime graph's target resolution (runtime-graph.ts), between the call-site\n // literal and the fan-out fallback.\n const callsResolution = resolveCallsService(info, workspaceRoot);\n if (callsResolution.problem !== null) {\n problems.push(callsResolution.problem);\n } else if (callsResolution.callsService !== null) {\n entry.callsService = callsResolution.callsService;\n }\n}\n\nfunction enrichResponsibilities(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n problems: string[]\n): void {\n const responsibilitiesFile = toRepoRelative(info.root, RESPONSIBILITIES_FILE_NAME);\n entry.responsibilitiesFile = responsibilitiesFile;\n\n const absolutePath = path.join(workspaceRoot, info.root, RESPONSIBILITIES_FILE_NAME);\n if (!fs.existsSync(absolutePath)) {\n problems.push(\n `${info.name}: missing required ${responsibilitiesFile} — create it with a heading, ` +\n `one short summary paragraph, then the full responsibilities of the module`\n );\n return;\n }\n\n const summary = extractShortDescription(fs.readFileSync(absolutePath, 'utf-8'));\n const summaryProblem = validateShortDescription(summary, responsibilitiesFile);\n if (summaryProblem !== null) {\n problems.push(`${info.name}: ${summaryProblem}`);\n return;\n }\n entry.shortDescription = summary;\n}\n\n/**\n * Repo-relative path with forward slashes (stable across platforms in the\n * committed JSON).\n */\nfunction toRepoRelative(projectRoot: string, fileName: string): string {\n return [projectRoot.replace(/\\\\/g, '/').replace(/\\/+$/, ''), fileName].join('/');\n}\n\n/**\n * True when the project has a REAL generated DI design — a committed design.json\n * whose `designs[]` is non-empty (i.e. it has ≥1 @DocumentDesign root).\n *\n * MISSING, empty-`designs[]`, and unparseable all read as \"no design\", and that\n * equivalence is load-bearing rather than incidental: di-graph-generate writes NO\n * file for a project without a design root (and reaps a stale one), so \"absent\"\n * is now the normal state for every plain lib and legacy service. Repos that have\n * not rebuilt since still carry `{ designs: [] }` files. Either way the box's node\n * menu must offer no \"View Design\" item. See design-file-emission.spec.ts.\n */\nfunction hasGeneratedDesign(workspaceRoot: string, projectRoot: string): boolean {\n const designPath = path.join(workspaceRoot, projectRoot, 'design.json');\n if (!fs.existsSync(designPath)) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const parsed = JSON.parse(fs.readFileSync(designPath, 'utf-8'));\n return Array.isArray(parsed.designs) && parsed.designs.length > 0;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Skipping unparseable ${designPath}: ${error.message}`);\n return false;\n }\n}\n"]}
1
+ {"version":3,"file":"graph-metadata.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Metadata Enrichment\n *\n * Fills the AI-oriented fields on each architecture/dependencies.json entry:\n * framework — from `framework:<x>` nx tag or package.json inference\n * products — the products reaching it, from the `product:<name>` nx tags (#1179)\n * level — re-levelled so servers, clients and apps form the top row (#1179)\n * serviceName — from project.json metadata.webpieces.serviceName; the\n * name clients address this app by at runtime\n * shortDescription — first paragraph of the project's responsibilities.md\n * responsibilitiesFile — repo-relative path to the required responsibilities.md\n * designFile — repo-relative path to the generated DI design.json\n *\n * Validation is aggregated: ALL problems across ALL projects are collected and\n * thrown as one MetadataValidationError so a repo adopting this sees the full\n * seeding list in a single run. Callers must enrich BEFORE writing any file so\n * a failed run never clobbers dependencies.json.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { createProjectGraphAsync } from '@nx/devkit';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport { ProjectInfo } from './project-info';\nimport { resolveFramework } from './framework-resolver';\nimport { resolveRole } from './role-resolver';\nimport { resolveDrawOnGraph } from './draw-on-graph-resolver';\nimport { resolveCutLegacyCycles } from './cut-legacy-cycle-resolver';\nimport { ProductResolver } from './product-resolver';\nimport { ProductMembership } from './graph-products';\nimport { EntryPointLevels } from './graph-entry-levels';\nimport { resolveRuntimeParticipant } from './runtime-participant-resolver';\nimport { resolveCallsService, resolveServiceName, validateUniqueServiceNames } from './service-name-resolver';\nimport { extractShortDescription, validateShortDescription } from './responsibilities';\nimport { toError } from '../toError';\n\nexport const RESPONSIBILITIES_FILE_NAME = 'responsibilities.md';\n\n/**\n * Thrown when one or more projects fail metadata validation (missing/invalid\n * responsibilities.md, bad framework tags, ...). Executors catch this to point\n * AI at the webpieces.responsibilities.md instructions template.\n */\nexport class MetadataValidationError extends Error {\n constructor(public readonly problems: string[]) {\n super(\n `Architecture metadata validation failed (${problems.length} problem(s)):\\n` +\n problems.map((problem: string) => ` - ${problem}`).join('\\n')\n );\n this.name = 'MetadataValidationError';\n }\n}\n\n/**\n * Read per-project root + tags from nx's project graph.\n */\nexport async function collectProjectInfo(): Promise<Map<string, ProjectInfo>> {\n const projectGraph = await createProjectGraphAsync();\n const infos = new Map<string, ProjectInfo>();\n for (const [name, node] of Object.entries(projectGraph.nodes)) {\n infos.set(name, new ProjectInfo(name, node.data.root, node.data.tags ?? []));\n }\n return infos;\n}\n\n/**\n * Enrich every graph entry in place with framework, shortDescription,\n * responsibilitiesFile and designFile. Throws MetadataValidationError listing\n * every problem when any project fails validation.\n */\nexport function enrichGraph(\n graph: EnhancedGraph,\n infos: Map<string, ProjectInfo>,\n workspaceRoot: string\n): void {\n const problems: string[] = [];\n // project -> declared serviceName, collected so duplicates can be reported across the workspace.\n const serviceNames = new Map<string, string>();\n // project -> the products it DECLARES (`product:` tags); membership is derived from these below.\n const productSeeds = new Map<string, string[]>();\n const products = new ProductResolver();\n\n for (const [projectName, entry] of Object.entries(graph)) {\n const info = infos.get(projectName);\n if (!info) {\n problems.push(`${projectName}: not found in nx project graph`);\n continue;\n }\n\n enrichDeclarations(entry, info, workspaceRoot, problems);\n const declared = products.resolve(info);\n problems.push(...declared.problems);\n if (declared.products.length > 0) productSeeds.set(projectName, declared.products);\n enrichClientNames(entry, info, workspaceRoot, projectName, serviceNames, problems);\n enrichResponsibilities(entry, info, workspaceRoot, problems);\n\n // Set designFile ONLY when the project has a REAL generated design (a\n // non-empty `designs[]`), i.e. it has a @DocumentDesign root. A project\n // without one has NO design.json at all (di-graph-generate stopped writing\n // empty ones and reaps stale ones), and legacy repos may still carry an\n // empty `{ designs: [] }` until their next build — both must read the same\n // way, and neither gets a \"View Design\" item on its node menu in the arch\n // viz (designLinks keys off designFile). See graph-visualizer.ts.\n if (hasGeneratedDesign(workspaceRoot, info.root)) {\n entry.designFile = toRepoRelative(info.root, 'design.json');\n }\n }\n\n validateUniqueServiceNames(serviceNames, problems);\n validateLibraryTypesMatch(graph, problems);\n validateRoleDependencies(graph, problems);\n\n if (problems.length > 0) {\n throw new MetadataValidationError(problems);\n }\n\n // #1179: a library belongs to every product whose tagged project reaches it — derived, never tagged.\n new ProductMembership().compute(productSeeds, graph).forEach((member: string[], projectName: string): void => {\n graph[projectName].products = member;\n });\n // #1179: servers, clients and apps form the top row. Here, once roles are resolved, so generate,\n // validate-architecture-unchanged and validate-api-relations all compute identical levels.\n new EntryPointLevels(APP_ROLES).promote(graph);\n}\n\n/**\n * The fields a project DECLARES about itself — its framework env set and role (nx tags), the\n * webpieces runtime packages it depends on (package.json), and whether it opts out of the drawings\n * (nx tag). Grouped because they share a shape: resolve, record the problem, or write the field.\n *\n * Distinct from enrichResponsibilities/designFile, which read generated or hand-written FILES.\n */\n// webpieces-disable no-function-outside-class -- enrichGraph step helper, matching enrichClientNames/enrichResponsibilities in this file\nfunction enrichDeclarations(\n entry: GraphEntry,\n info: ProjectInfo,\n workspaceRoot: string,\n problems: string[]\n): void {\n const resolution = resolveFramework(info, workspaceRoot);\n if (resolution.problem !== null) {\n problems.push(resolution.problem);\n } else if (resolution.frameworks !== null) {\n entry.framework = resolution.frameworks;\n }\n\n const roleResolution = resolveRole(info);\n if (roleResolution.problem !== null) {\n problems.push(roleResolution.problem);\n } else if (roleResolution.role !== null) {\n entry.role = roleResolution.role;\n }\n\n // Only persist when the project declares at least one — a repo full of\n // \"webpiecesRuntime\": [] lines would be noise, and absence already means \"declares none\".\n const participantResolution = resolveRuntimeParticipant(info, workspaceRoot);\n if (participantResolution.problem !== null) {\n problems.push(participantResolution.problem);\n } else if (participantResolution.markers !== null && participantResolution.markers.length > 0) {\n entry.webpiecesRuntime = participantResolution.markers;\n }\n\n // Only persist the field when hidden (false); drawn projects (the\n // default) stay clean in dependencies.json with no drawOnGraph line.\n const drawResolution = resolveDrawOnGraph(info);\n if (drawResolution.problem !== null) {\n problems.push(drawResolution.problem);\n } else if (drawResolution.drawOnGraph === false) {\n entry.drawOnGraph = false;\n }\n\n // Only persist when the project actually cuts an edge — absence is the normal state, and a repo\n // full of \"cutLegacyCycle\": [] lines would bury the ones that mean something.\n const cutResolution = resolveCutLegacyCycles(info);\n if (cutResolution.problem !== null) {\n problems.push(cutResolution.problem);\n } else if (cutResolution.targets !== null && cutResolution.targets.length > 0) {\n entry.cutLegacyCycle = cutResolution.targets;\n }\n}\n\n/**\n * Roles that are terminal APPS — nothing may depend on them. A server, a\n * non-HTTP `app`, or a client is a top-level runnable; being depended upon means\n * it is really a library and should be retagged `role:lib`/`role:designed-lib`.\n */\nexport const APP_ROLES: ReadonlyArray<string> = ['server', 'app', 'client'];\n\n/**\n * Compatibility lattice — the \"up-set\" of each atomic env is the env itself\n * PLUS every ancestor it can legally consume code from (specialization edges\n * child → parent: react → browser, angular → browser, express → node). A\n * consumer promising env `c` can be satisfied by any dependency env in `up(c)`.\n *\n * `react-native` is a first-class runtime and NOT a specialisation of browser (#1064, D6): it has no\n * DOM, no IndexedDB and no OPFS, so a react-native consumer may only consume libraries that are\n * themselves tagged `framework:react-native` (a universal browser+node+react-native library, a\n * browser+react-native one, or a react-native-only one).\n */\nexport const ENV_UP_SETS: Readonly<Record<string, ReadonlyArray<string>>> = {\n react: ['react', 'browser'],\n angular: ['angular', 'browser'],\n browser: ['browser'],\n express: ['express', 'node'],\n node: ['node'],\n 'react-native': ['react-native'],\n};\n\n/** The up-set of an env (env itself + ancestors); unknown envs map to just themselves. */\nfunction upSet(env: string): ReadonlyArray<string> {\n return ENV_UP_SETS[env] ?? [env];\n}\n\n/**\n * `library-types-match-client` rule.\n *\n * A project's `framework` field is its libType — the SET of runtime\n * environments it is validated to run in (browser | react | angular | node |\n * express | react-native). For a dependency edge Consumer C → Library L, the edge is LEGAL iff\n * for EVERY env `c` in C's set, up(c) ∩ L's set ≠ ∅ — i.e. every environment\n * the consumer promises to run in can be satisfied by the dependency. This keeps\n * an express app from depending on a browser-only lib, and lets a `browser+node`\n * lib be consumed by both react and express projects. Every violation is\n * appended to `problems` so `arch:generate` fails with the full list.\n */\nexport function validateLibraryTypesMatch(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromSet = entry.framework;\n if (fromSet === undefined) continue; // framework resolution already flagged this project\n\n for (const dep of entry.dependsOn) {\n const depEntry = graph[dep];\n const toSet = depEntry?.framework;\n if (toSet === undefined) continue;\n\n const unsatisfied = fromSet.filter(\n (env: string) => !upSet(env).some((up: string) => toSet.includes(up))\n );\n if (unsatisfied.length === 0) continue;\n\n problems.push(\n `library-types-match-client: '${projectName}' [${fromSet.join(', ')}] must not depend on ` +\n `'${dep}' [${toSet.join(', ')}] — the consumer env(s) ${unsatisfied.join(', ')} cannot be ` +\n `satisfied by the dependency (each consumer env must resolve to itself or an ancestor it ` +\n `consumes from: react/angular→browser, express→node, react-native→react-native only). ` +\n `Widen '${dep}' framework tags or ` +\n `remove the dependency.`\n );\n }\n }\n}\n\n/**\n * `role-dependency` rule.\n *\n * A project's `role` is its function (server | app | bundle | designed-lib | lib | client |\n * api-lib | api-client). The two api roles are libraries, never apps, so this rule leaves them alone;\n * what THEY may depend on is `api-lib-dependencies` (tag-truth.ts).\n * Apps are terminal — libraries and clients consume them, never the reverse:\n * - a `client` is fully terminal: NOTHING may depend on it.\n * - a `server` may only be depended upon by another `server` — the one\n * legitimate case is a server-side orchestrator/e2e harness that boots\n * other servers. A `lib`/`designed-lib`/`client` depending on a `server`\n * inverts the dependency direction and is a violation.\n * - a `bundle` is the one role permitted to depend on ANY app: it aggregates\n * several apps into one distributable (e.g. an nx plugin re-exposing multiple\n * tooling apps), so a `bundle → app` edge is legitimate, not inverted.\n */\nexport function validateRoleDependencies(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromRole = entry.role;\n for (const dep of entry.dependsOn) {\n const toRole = graph[dep]?.role;\n if (toRole === undefined) continue; // role resolution already flagged this project\n if (!APP_ROLES.includes(toRole)) continue;\n // A server may orchestrate/boot other servers (e.g. an e2e harness).\n if (toRole === 'server' && fromRole === 'server') continue;\n // A bundle aggregates apps — it may depend on any app role.\n if (fromRole === 'bundle') continue;\n\n const why =\n toRole === 'client'\n ? `a 'client' app is terminal and may never be depended upon`\n : `a 'server' may only be depended upon by another 'server' (an orchestrator/e2e harness)`;\n problems.push(\n `role-dependency: '${projectName}' (role:${fromRole ?? 'none'}) must not depend on ` +\n `'${dep}' (role:${toRole}) — ${why}. Retag '${dep}' role:lib/role:designed-lib if it ` +\n `is actually a library, or remove the dependency.`\n );\n }\n }\n}\n\n/**\n * Fill the two symmetric client-addressing fields — `serviceName` (the name clients address THIS app\n * by) and `callsService` (the name THIS app's clients call when no literal ClientConfig sits at the\n * call site). Both are declared in project.json, never derived (see service-name-resolver.ts).\n */\n// webpieces-disable no-function-outside-class -- pure enrichment helper, mirrors enrichResponsibilities\nfunction enrichClientNames(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n projectName: string,\n serviceNames: Map<string, string>,\n problems: string[]\n): void {\n const serviceResolution = resolveServiceName(info, workspaceRoot);\n if (serviceResolution.problem !== null) {\n problems.push(serviceResolution.problem);\n } else if (serviceResolution.serviceName !== null) {\n entry.serviceName = serviceResolution.serviceName;\n serviceNames.set(projectName, serviceResolution.serviceName);\n }\n\n // Consumed by the runtime graph's target resolution (runtime-graph.ts), between the call-site\n // literal and the fan-out fallback.\n const callsResolution = resolveCallsService(info, workspaceRoot);\n if (callsResolution.problem !== null) {\n problems.push(callsResolution.problem);\n } else if (callsResolution.callsService !== null) {\n entry.callsService = callsResolution.callsService;\n }\n}\n\nfunction enrichResponsibilities(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n problems: string[]\n): void {\n const responsibilitiesFile = toRepoRelative(info.root, RESPONSIBILITIES_FILE_NAME);\n entry.responsibilitiesFile = responsibilitiesFile;\n\n const absolutePath = path.join(workspaceRoot, info.root, RESPONSIBILITIES_FILE_NAME);\n if (!fs.existsSync(absolutePath)) {\n problems.push(\n `${info.name}: missing required ${responsibilitiesFile} — create it with a heading, ` +\n `one short summary paragraph, then the full responsibilities of the module`\n );\n return;\n }\n\n const summary = extractShortDescription(fs.readFileSync(absolutePath, 'utf-8'));\n const summaryProblem = validateShortDescription(summary, responsibilitiesFile);\n if (summaryProblem !== null) {\n problems.push(`${info.name}: ${summaryProblem}`);\n return;\n }\n entry.shortDescription = summary;\n}\n\n/**\n * Repo-relative path with forward slashes (stable across platforms in the\n * committed JSON).\n */\nfunction toRepoRelative(projectRoot: string, fileName: string): string {\n return [projectRoot.replace(/\\\\/g, '/').replace(/\\/+$/, ''), fileName].join('/');\n}\n\n/**\n * True when the project has a REAL generated DI design — a committed design.json\n * whose `designs[]` is non-empty (i.e. it has ≥1 @DocumentDesign root).\n *\n * MISSING, empty-`designs[]`, and unparseable all read as \"no design\", and that\n * equivalence is load-bearing rather than incidental: di-graph-generate writes NO\n * file for a project without a design root (and reaps a stale one), so \"absent\"\n * is now the normal state for every plain lib and legacy service. Repos that have\n * not rebuilt since still carry `{ designs: [] }` files. Either way the box's node\n * menu must offer no \"View Design\" item. See design-file-emission.spec.ts.\n */\nfunction hasGeneratedDesign(workspaceRoot: string, projectRoot: string): boolean {\n const designPath = path.join(workspaceRoot, projectRoot, 'design.json');\n if (!fs.existsSync(designPath)) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const parsed = JSON.parse(fs.readFileSync(designPath, 'utf-8'));\n return Array.isArray(parsed.designs) && parsed.designs.length > 0;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Skipping unparseable ${designPath}: ${error.message}`);\n return false;\n }\n}\n"]}
@@ -14,7 +14,11 @@
14
14
  * The Filter popover's "Changes on this branch" choices. The page computes each set from the Impact
15
15
  * sidecar (graph-impact.ts): CHANGED = touched; DEPENDENTS = touched + affected (nx affected: their
16
16
  * tests re-run); DEPENDENCIES = touched + every transitive dependency of it; BUILD = touched +
17
- * affected + every build input.
17
+ * affected + every build input — the dependencies of the DEPENDENTS too, which is why it is more than
18
+ * the union of the two rows above it.
19
+ *
20
+ * "Dependents" are what USE a project and "dependencies" what it uses (#1179). "Dependee" would mean
21
+ * the thing depended ON — the opposite of the dependents row — so it is never used.
18
22
  */
19
23
  export declare enum ChangeScope {
20
24
  EVERYTHING = "everything",
@@ -23,10 +27,16 @@ export declare enum ChangeScope {
23
27
  DEPENDENCIES = "dependencies",
24
28
  BUILD = "build"
25
29
  }
30
+ /**
31
+ * One change scope: its name and the grey second line under it, like the Color-by menu. These are the
32
+ * ONE copy of the labels — the page script reads each name back from its `data-wp-scope-label` (the
33
+ * top-bar pill), so the two cannot drift apart.
34
+ */
26
35
  export declare class ScopeOption {
27
36
  readonly scope: ChangeScope;
28
37
  readonly label: string;
29
- constructor(scope: ChangeScope, label: string);
38
+ readonly subtitle: string;
39
+ constructor(scope: ChangeScope, label: string, subtitle: string);
30
40
  }
31
41
  export declare const SCOPE_OPTIONS: readonly ScopeOption[];
32
42
  /** Everything the shell frames, already rendered by its owner. */
@@ -38,24 +48,36 @@ export declare class ShellParts {
38
48
  readonly filterStatus: string;
39
49
  readonly snapshot: string;
40
50
  readonly responsibilities: string;
41
- constructor(title: string, lockControl: string, legend: string, edgeKey: string, filterStatus: string, snapshot: string, responsibilities: string);
51
+ /** Every declared product, sorted (#1179); empty hides the Filter's Product group. */
52
+ readonly products: string[];
53
+ constructor(title: string, lockControl: string, legend: string, edgeKey: string, filterStatus: string, snapshot: string, responsibilities: string,
54
+ /** Every declared product, sorted (#1179); empty hides the Filter's Product group. */
55
+ products: string[]);
42
56
  }
43
57
  export declare class GraphPageShell {
44
58
  body(parts: ShellParts): string;
45
59
  private drawer;
46
60
  /**
47
61
  * "Color by": ONE pill-shaped trigger showing the current mode (name + subtitle) that opens a
48
- * menu of the three. Keys 1/2/3 and the #runtime / #architecture / #impact links still switch.
62
+ * menu of the four. Keys 1/2/3/4 and the #runtime / #architecture / #impact / #product links still
63
+ * switch.
49
64
  */
50
65
  private modePulldown;
51
66
  /** One menu option, two lines: the mode's name, then its grey subtitle. */
52
67
  private modeItem;
53
68
  /**
54
- * The Filter popover. Its three groups INTERSECT: a box is shown when it is in the chosen change
69
+ * The Filter popover. Its four groups INTERSECT: a box is shown when it is in the chosen change
55
70
  * scope AND carries any selected runtime chip (when any is selected) AND any selected role chip
56
- * (when any is selected). Nothing applies until "Show N projects".
71
+ * (when any is selected) AND belongs to any selected product (when any is selected). Nothing
72
+ * applies until "Show N projects".
57
73
  */
58
74
  private filterPopover;
75
+ /**
76
+ * The Product group (#1179): one chip per declared product, each with its palette swatch. Several
77
+ * chips select the UNION of their closures; the group intersects with the others. Omitted when the
78
+ * workspace declares no product.
79
+ */
80
+ private productGroup;
59
81
  /**
60
82
  * Which Impact scan the page shows (#1163): "Changed on this branch" (fork point → working tree)
61
83
  * or "Last commit" (HEAD^ → working tree). Hidden until the page finds TWO scans in the sidecar;
@@ -14,11 +14,16 @@
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.GraphPageShell = exports.ShellParts = exports.SCOPE_OPTIONS = exports.ScopeOption = exports.ChangeScope = void 0;
16
16
  const graph_color_modes_1 = require("./graph-color-modes");
17
+ const graph_products_1 = require("./graph-products");
17
18
  /**
18
19
  * The Filter popover's "Changes on this branch" choices. The page computes each set from the Impact
19
20
  * sidecar (graph-impact.ts): CHANGED = touched; DEPENDENTS = touched + affected (nx affected: their
20
21
  * tests re-run); DEPENDENCIES = touched + every transitive dependency of it; BUILD = touched +
21
- * affected + every build input.
22
+ * affected + every build input — the dependencies of the DEPENDENTS too, which is why it is more than
23
+ * the union of the two rows above it.
24
+ *
25
+ * "Dependents" are what USE a project and "dependencies" what it uses (#1179). "Dependee" would mean
26
+ * the thing depended ON — the opposite of the dependents row — so it is never used.
22
27
  */
23
28
  var ChangeScope;
24
29
  (function (ChangeScope) {
@@ -28,21 +33,28 @@ var ChangeScope;
28
33
  ChangeScope["DEPENDENCIES"] = "dependencies";
29
34
  ChangeScope["BUILD"] = "build";
30
35
  })(ChangeScope || (exports.ChangeScope = ChangeScope = {}));
36
+ /**
37
+ * One change scope: its name and the grey second line under it, like the Color-by menu. These are the
38
+ * ONE copy of the labels — the page script reads each name back from its `data-wp-scope-label` (the
39
+ * top-bar pill), so the two cannot drift apart.
40
+ */
31
41
  class ScopeOption {
32
42
  scope;
33
43
  label;
34
- constructor(scope, label) {
44
+ subtitle;
45
+ constructor(scope, label, subtitle) {
35
46
  this.scope = scope;
36
47
  this.label = label;
48
+ this.subtitle = subtitle;
37
49
  }
38
50
  }
39
51
  exports.ScopeOption = ScopeOption;
40
52
  exports.SCOPE_OPTIONS = [
41
- new ScopeOption(ChangeScope.EVERYTHING, 'Everything'),
42
- new ScopeOption(ChangeScope.CHANGED, 'Changed'),
43
- new ScopeOption(ChangeScope.DEPENDENTS, 'Changed + what uses them'),
44
- new ScopeOption(ChangeScope.DEPENDENCIES, 'Changed + what they use'),
45
- new ScopeOption(ChangeScope.BUILD, 'Whole build of this branch'),
53
+ new ScopeOption(ChangeScope.EVERYTHING, 'All', 'every project'),
54
+ new ScopeOption(ChangeScope.CHANGED, 'Changed', 'owns a changed file'),
55
+ new ScopeOption(ChangeScope.DEPENDENTS, 'Changed + dependents', '+ everything that uses them (their tests re-run)'),
56
+ new ScopeOption(ChangeScope.DEPENDENCIES, 'Changed + dependencies', '+ everything they use'),
57
+ new ScopeOption(ChangeScope.BUILD, 'Changed + dependents + dependencies', 'everything CI builds for this branch'),
46
58
  ];
47
59
  /** Everything the shell frames, already rendered by its owner. */
48
60
  class ShellParts {
@@ -53,7 +65,10 @@ class ShellParts {
53
65
  filterStatus;
54
66
  snapshot;
55
67
  responsibilities;
56
- constructor(title, lockControl, legend, edgeKey, filterStatus, snapshot, responsibilities) {
68
+ products;
69
+ constructor(title, lockControl, legend, edgeKey, filterStatus, snapshot, responsibilities,
70
+ /** Every declared product, sorted (#1179); empty hides the Filter's Product group. */
71
+ products) {
57
72
  this.title = title;
58
73
  this.lockControl = lockControl;
59
74
  this.legend = legend;
@@ -61,6 +76,7 @@ class ShellParts {
61
76
  this.filterStatus = filterStatus;
62
77
  this.snapshot = snapshot;
63
78
  this.responsibilities = responsibilities;
79
+ this.products = products;
64
80
  }
65
81
  }
66
82
  exports.ShellParts = ShellParts;
@@ -108,7 +124,8 @@ class GraphPageShell {
108
124
  }
109
125
  /**
110
126
  * "Color by": ONE pill-shaped trigger showing the current mode (name + subtitle) that opens a
111
- * menu of the three. Keys 1/2/3 and the #runtime / #architecture / #impact links still switch.
127
+ * menu of the four. Keys 1/2/3/4 and the #runtime / #architecture / #impact / #product links still
128
+ * switch.
112
129
  */
113
130
  modePulldown() {
114
131
  const first = graph_color_modes_1.GRAPH_MODES[0];
@@ -126,13 +143,16 @@ class GraphPageShell {
126
143
  `<span class="wp-mode-name">${info.name}</span><span class="wp-mode-sub">${info.subtitle}</span></button>`);
127
144
  }
128
145
  /**
129
- * The Filter popover. Its three groups INTERSECT: a box is shown when it is in the chosen change
146
+ * The Filter popover. Its four groups INTERSECT: a box is shown when it is in the chosen change
130
147
  * scope AND carries any selected runtime chip (when any is selected) AND any selected role chip
131
- * (when any is selected). Nothing applies until "Show N projects".
148
+ * (when any is selected) AND belongs to any selected product (when any is selected). Nothing
149
+ * applies until "Show N projects".
132
150
  */
133
- filterPopover() {
151
+ filterPopover(products) {
134
152
  const scopes = exports.SCOPE_OPTIONS.map((option) => `<label class="wp-scope"><input type="radio" name="wp-scope" value="${option.scope}"${option.scope === ChangeScope.EVERYTHING ? ' checked' : ''}>` +
135
- `<span class="wp-scope-name">${option.label}</span><span class="wp-count" data-wp-scope-count="${option.scope}"></span></label>`).join('');
153
+ `<span class="wp-scope-text"><span class="wp-scope-name" data-wp-scope-label="${option.scope}">${option.label}</span>` +
154
+ `<span class="wp-scope-sub">${option.subtitle}</span></span>` +
155
+ `<span class="wp-count" data-wp-scope-count="${option.scope}"></span></label>`).join('');
136
156
  const chips = (group, values) => values
137
157
  .map((value) => `<button type="button" class="wp-chip" data-wp-chip-group="${group}" data-wp-chip="${value}" aria-pressed="false">${value}</button>`)
138
158
  .join('');
@@ -141,9 +161,26 @@ class GraphPageShell {
141
161
  <fieldset class="wp-fgroup" id="wp-scope-group"><legend id="wp-scope-legend">Changes on this branch</legend>${this.impactKinds('filter')}${scopes}<p class="wp-scope-reason" id="wp-scope-reason" hidden></p></fieldset>
142
162
  <fieldset class="wp-fgroup"><legend>Runtime <small>any match</small></legend><div class="wp-chips">${chips('runtime', graph_color_modes_1.FRAMEWORK_ORDER)}</div></fieldset>
143
163
  <fieldset class="wp-fgroup"><legend>Role <small>any match</small></legend><div class="wp-chips">${chips('role', graph_color_modes_1.ROLE_STYLES.map((style) => style.role))}</div></fieldset>
164
+ ${this.productGroup(products)}
144
165
  <footer><button type="button" class="wp-text-btn" id="wp-filter-clear">Clear all</button><button type="button" class="wp-primary" id="wp-filter-apply">Show all projects</button></footer>
145
166
  </div>`;
146
167
  }
168
+ /**
169
+ * The Product group (#1179): one chip per declared product, each with its palette swatch. Several
170
+ * chips select the UNION of their closures; the group intersects with the others. Omitted when the
171
+ * workspace declares no product.
172
+ */
173
+ productGroup(products) {
174
+ if (products.length === 0)
175
+ return '';
176
+ const palette = new graph_products_1.ProductPalette(products);
177
+ const chips = palette.colors
178
+ .map((color) => `<button type="button" class="wp-chip" data-wp-chip-group="product" data-wp-chip="${color.product}" aria-pressed="false">` +
179
+ `<svg class="wp-chip-swatch" width="10" height="10" viewBox="0 0 10 10" aria-hidden="true"><circle cx="5" cy="5" r="4.5" fill="${color.color}"/></svg>` +
180
+ `${color.product}</button>`)
181
+ .join('');
182
+ return `<fieldset class="wp-fgroup" id="wp-product-group"><legend>Product <small>any match</small></legend><div class="wp-chips">${chips}</div></fieldset>`;
183
+ }
147
184
  /**
148
185
  * Which Impact scan the page shows (#1163): "Changed on this branch" (fork point → working tree)
149
186
  * or "Last commit" (HEAD^ → working tree). Hidden until the page finds TWO scans in the sidecar;
@@ -169,7 +206,7 @@ class GraphPageShell {
169
206
  ${parts.legend}
170
207
  </div>
171
208
  ${this.help()}
172
- ${this.filterPopover()}
209
+ ${this.filterPopover(parts.products)}
173
210
  <div class="wp-pop wp-snapshot" id="wp-snapshot" hidden>${parts.snapshot}</div>
174
211
  <aside class="wp-panel" id="wp-resp-panel" aria-label="Responsibilities" hidden>
175
212
  <header><span>Responsibilities</span><button type="button" class="wp-icon wp-icon-mini" id="wp-resp-close" aria-label="Close responsibilities">✕</button></header>
@@ -180,12 +217,12 @@ class GraphPageShell {
180
217
  help() {
181
218
  return `<div class="wp-pop wp-help" id="wp-help" role="dialog" aria-label="How to read this graph" hidden>
182
219
  <header><span>Reading the graph</span><button type="button" class="wp-icon wp-icon-mini" id="wp-help-close" aria-label="Close help">✕</button></header>
183
- <p>Every row is one dependency level: the HIGHEST level is the top row and L0, the foundation libraries, is always the bottom row. Transitive dependencies are allowed but not drawn.</p>
220
+ <p>Every row is one dependency level: the HIGHEST level is the top row and L0, the foundation libraries, is always the bottom row. Servers, clients and apps always form the top row (a bundle sits above the apps it aggregates). Transitive dependencies are allowed but not drawn.</p>
184
221
  <p>💡 <strong>Click any box</strong> for its menu: <strong>View Design</strong> (only where the project has a generated <strong>design.html</strong>, i.e. what the AI sees inside it), <strong>Lock/Unlock</strong> (the same lock as the Focus field), <strong>Filter Unconnected</strong> (removes unrelated boxes, keeping each L-number row).</p>
185
- <p><strong>Filter</strong> narrows the graph by what changed on this branch or in the last commit, by runtime and by role; the three groups combine. Every box keeps its own L-row, and a level left empty shows as a thin labeled band.</p>
222
+ <p><strong>Filter</strong> narrows the graph by what changed on this branch or in the last commit, by runtime, by role and by product; the groups combine. A product keeps its servers, clients and apps plus everything they depend on, so <strong>Product</strong> + <strong>Changed + dependents + dependencies</strong> is exactly what this branch builds that can affect that product. Every box keeps its own L-row, and a level left empty shows as a thin labeled band.</p>
186
223
  <p>🔦 <strong>Hover any box</strong> to trace its <em>entire</em> dependency chain — every ancestor above it and every dependency below it, with the lines between — while the rest of the graph dims.</p>
187
- <p><strong>Runtime</strong>: a box may depend on a box carrying every color it has. <strong>Architecture</strong>: what each box is in the system. <strong>Impact</strong>: what changed on this branch or in the last commit (amber) and what depends on it (amber outline); with both available, a toggle picks which.</p>
188
- <p><kbd>/</kbd> focus the lock search · <kbd>1</kbd> <kbd>2</kbd> <kbd>3</kbd> switch mode · <kbd>Esc</kbd> unlock</p>
224
+ <p><strong>Runtime</strong>: a box may depend on a box carrying every color it has. <strong>Architecture</strong>: what each box is in the system. <strong>Impact</strong>: what changed on this branch or in the last commit (amber) and its dependents (amber outline); with both available, a toggle picks which. <strong>Product</strong>: which products each box belongs to — one color per product, stripes for several, grey for every product, a dashed white box for none.</p>
225
+ <p><kbd>/</kbd> focus the lock search · <kbd>1</kbd> <kbd>2</kbd> <kbd>3</kbd> <kbd>4</kbd> switch mode · <kbd>Esc</kbd> unlock</p>
189
226
  </div>`;
190
227
  }
191
228
  brandIcon() {