@webpieces/nx-webpieces-rules 0.4.467 → 0.4.469
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/nx-webpieces-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.469",
|
|
4
4
|
"description": "Nx-specific webpieces validation rules and graph tooling. Bundles all @webpieces rule packages with Nx graph validators and an inference plugin.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"README.md"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@webpieces/ai-hook-rules": "0.4.
|
|
25
|
-
"@webpieces/code-rules": "0.4.
|
|
26
|
-
"@webpieces/eslint-rules": "0.4.
|
|
27
|
-
"@webpieces/pr-gate": "0.4.
|
|
28
|
-
"@webpieces/rules-config": "0.4.
|
|
24
|
+
"@webpieces/ai-hook-rules": "0.4.469",
|
|
25
|
+
"@webpieces/code-rules": "0.4.469",
|
|
26
|
+
"@webpieces/eslint-rules": "0.4.469",
|
|
27
|
+
"@webpieces/pr-gate": "0.4.469",
|
|
28
|
+
"@webpieces/rules-config": "0.4.469",
|
|
29
29
|
"madge": "8.0.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
package/src/lib/graph-loader.js
CHANGED
|
@@ -144,6 +144,7 @@ function formatEntryLines(entry) {
|
|
|
144
144
|
pushOptionalArrayField(lines, 'framework', entry.framework);
|
|
145
145
|
pushOptionalField(lines, 'role', entry.role);
|
|
146
146
|
pushOptionalField(lines, 'serviceName', entry.serviceName);
|
|
147
|
+
pushOptionalCallsServiceField(lines, entry.callsService);
|
|
147
148
|
pushOptionalBooleanField(lines, 'drawOnGraph', entry.drawOnGraph);
|
|
148
149
|
pushOptionalField(lines, 'shortDescription', entry.shortDescription);
|
|
149
150
|
pushOptionalField(lines, 'responsibilitiesFile', entry.responsibilitiesFile);
|
|
@@ -170,6 +171,19 @@ function pushOptionalField(lines, field, value) {
|
|
|
170
171
|
lines.push(` ${JSON.stringify(field)}: ${JSON.stringify(value)},`);
|
|
171
172
|
}
|
|
172
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Emit the optional `callsService` field (12-space indent). Unlike serviceName it may be EITHER a
|
|
176
|
+
* single service name (string) OR an { apiClassName: serviceName } map, so it is emitted with
|
|
177
|
+
* JSON.stringify to cover both inline. MUST be persisted here: the runtime graph's target resolution
|
|
178
|
+
* reads it back from the committed dependencies.json during validate — dropping it would make
|
|
179
|
+
* generate (which enriches it in-memory) and validate (which loads this file) derive different graphs.
|
|
180
|
+
*/
|
|
181
|
+
// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling push*Field helpers here
|
|
182
|
+
function pushOptionalCallsServiceField(lines, value) {
|
|
183
|
+
if (value !== undefined) {
|
|
184
|
+
lines.push(` "callsService": ${JSON.stringify(value)},`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
173
187
|
/**
|
|
174
188
|
* Emit one optional boolean field (12-space indent) as a raw JSON boolean
|
|
175
189
|
* (e.g. `"drawOnGraph": false,`), skipped when undefined.
|
|
@@ -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 * \"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 type { ProjectApiRelations } from './api-usage/api-relations';\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.';\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 and ' +\n 'architecture/runtime-dependencies.json; run after adding/removing project dependencies',\n visualizeArchitecture:\n 'pnpm nx run architecture:visualize — opens the monorepo dependency graph (this file) ' +\n 'as HTML in a browser',\n visualizeRuntimeArchitecture:\n 'pnpm nx run architecture:visualize-runtime — opens the runtime microservice call graph ' +\n 'as HTML in a browser',\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}\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' && 'projects' in parsed) {\n return new DependenciesFile(\n typeof parsed.aiInstructions === 'string' ? parsed.aiInstructions : '',\n parsed.commands !== null && typeof parsed.commands === 'object' ? (parsed.commands as CommandMap) : {},\n parsed.projects as EnhancedGraph\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 throw new Error(`Failed to load graph from ${fullPath}`, { cause: error });\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(` ${JSON.stringify(name)}: ${JSON.stringify(file.commands[name])}${comma}`);\n });\n lines.push(` },`);\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 * 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 pushOptionalField(lines, 'serviceName', entry.serviceName);\n pushOptionalBooleanField(lines, 'drawOnGraph', entry.drawOnGraph);\n pushOptionalField(lines, 'shortDescription', entry.shortDescription);\n pushOptionalField(lines, 'responsibilitiesFile', entry.responsibilitiesFile);\n pushOptionalField(lines, 'designFile', entry.designFile);\n pushApiRelationsField(lines, entry.apiRelations);\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 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(lines: string[], field: string, value: boolean | undefined): 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): 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(new DependenciesFile(AI_INSTRUCTIONS, GRAPH_COMMANDS, sortedGraph));\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 * \"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 type { ProjectApiRelations } from './api-usage/api-relations';\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.';\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 and ' +\n 'architecture/runtime-dependencies.json; run after adding/removing project dependencies',\n visualizeArchitecture:\n 'pnpm nx run architecture:visualize — opens the monorepo dependency graph (this file) ' +\n 'as HTML in a browser',\n visualizeRuntimeArchitecture:\n 'pnpm nx run architecture:visualize-runtime — opens the runtime microservice call graph ' +\n 'as HTML in a browser',\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}\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' && 'projects' in parsed) {\n return new DependenciesFile(\n typeof parsed.aiInstructions === 'string' ? parsed.aiInstructions : '',\n parsed.commands !== null && typeof parsed.commands === 'object' ? (parsed.commands as CommandMap) : {},\n parsed.projects as EnhancedGraph\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 throw new Error(`Failed to load graph from ${fullPath}`, { cause: error });\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(` ${JSON.stringify(name)}: ${JSON.stringify(file.commands[name])}${comma}`);\n });\n lines.push(` },`);\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 * 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 pushOptionalField(lines, 'serviceName', entry.serviceName);\n pushOptionalCallsServiceField(lines, entry.callsService);\n pushOptionalBooleanField(lines, 'drawOnGraph', entry.drawOnGraph);\n pushOptionalField(lines, 'shortDescription', entry.shortDescription);\n pushOptionalField(lines, 'responsibilitiesFile', entry.responsibilitiesFile);\n pushOptionalField(lines, 'designFile', entry.designFile);\n pushApiRelationsField(lines, entry.apiRelations);\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(lines: string[], field: string, value: boolean | undefined): 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): 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(new DependenciesFile(AI_INSTRUCTIONS, GRAPH_COMMANDS, sortedGraph));\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"]}
|