@webpieces/nx-webpieces-rules 0.4.486 → 0.4.488
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 +6 -6
- package/src/executors/generate/executor.js +42 -19
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/validate-api-relations/executor.js +6 -1
- package/src/executors/validate-api-relations/executor.js.map +1 -1
- package/src/executors/validate-architecture-unchanged/executor.js +65 -15
- package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
- package/src/executors/validate-runtime-architecture/executor.js +3 -1
- package/src/executors/validate-runtime-architecture/executor.js.map +1 -1
- package/src/lib/api-usage/api-ast.d.ts +71 -0
- package/src/lib/api-usage/api-ast.js +250 -0
- package/src/lib/api-usage/api-ast.js.map +1 -0
- package/src/lib/api-usage/api-relations.d.ts +72 -3
- package/src/lib/api-usage/api-relations.js.map +1 -1
- package/src/lib/api-usage/api-scanner.d.ts +43 -4
- package/src/lib/api-usage/api-scanner.js +121 -103
- package/src/lib/api-usage/api-scanner.js.map +1 -1
- package/src/lib/graph-loader.d.ts +21 -2
- package/src/lib/graph-loader.js +38 -4
- package/src/lib/graph-loader.js.map +1 -1
- package/src/lib/runtime-config.d.ts +7 -1
- package/src/lib/runtime-config.js +7 -1
- package/src/lib/runtime-config.js.map +1 -1
- package/src/lib/runtime-graph-io.d.ts +17 -0
- package/src/lib/runtime-graph-io.js +59 -0
- package/src/lib/runtime-graph-io.js.map +1 -0
- package/src/lib/runtime-graph-model.d.ts +121 -0
- package/src/lib/runtime-graph-model.js +14 -0
- package/src/lib/runtime-graph-model.js.map +1 -0
- package/src/lib/runtime-graph.d.ts +6 -75
- package/src/lib/runtime-graph.js +163 -51
- package/src/lib/runtime-graph.js.map +1 -1
- package/src/lib/runtime-visualizer.d.ts +5 -0
- package/src/lib/runtime-visualizer.js +63 -6
- package/src/lib/runtime-visualizer.js.map +1 -1
package/src/lib/graph-loader.js
CHANGED
|
@@ -72,10 +72,21 @@ class DependenciesFile {
|
|
|
72
72
|
aiInstructions;
|
|
73
73
|
commands;
|
|
74
74
|
projects;
|
|
75
|
-
|
|
75
|
+
apiContracts;
|
|
76
|
+
constructor(aiInstructions, commands, projects,
|
|
77
|
+
/**
|
|
78
|
+
* Every API contract's per-method trigger table (kind + queue name + path).
|
|
79
|
+
*
|
|
80
|
+
* MUST be persisted, for the same reason `callsService` must: the runtime graph is derived
|
|
81
|
+
* SOLELY from this file, and `validate-runtime-architecture` re-derives from the LOADED copy
|
|
82
|
+
* while generate derives from the in-memory one. A field that is scanned but not written
|
|
83
|
+
* makes those two inputs differ, and the validator reports a diff no one can fix.
|
|
84
|
+
*/
|
|
85
|
+
apiContracts = {}) {
|
|
76
86
|
this.aiInstructions = aiInstructions;
|
|
77
87
|
this.commands = commands;
|
|
78
88
|
this.projects = projects;
|
|
89
|
+
this.apiContracts = apiContracts;
|
|
79
90
|
}
|
|
80
91
|
}
|
|
81
92
|
exports.DependenciesFile = DependenciesFile;
|
|
@@ -97,7 +108,12 @@ function loadBlessedGraph(workspaceRoot, graphPath = exports.DEFAULT_GRAPH_PATH)
|
|
|
97
108
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
98
109
|
const parsed = JSON.parse(content);
|
|
99
110
|
if (parsed !== null && typeof parsed === 'object' && 'projects' in parsed) {
|
|
100
|
-
return new DependenciesFile(typeof parsed.aiInstructions === 'string' ? parsed.aiInstructions : '', parsed.commands !== null && typeof parsed.commands === 'object' ? parsed.commands : {}, parsed.projects
|
|
111
|
+
return new DependenciesFile(typeof parsed.aiInstructions === 'string' ? parsed.aiInstructions : '', parsed.commands !== null && typeof parsed.commands === 'object' ? parsed.commands : {}, parsed.projects,
|
|
112
|
+
// Absent in a file written before apiContracts existed; an empty table degrades the
|
|
113
|
+
// runtime graph to unnamed per-pair queues rather than failing to load.
|
|
114
|
+
parsed.apiContracts !== null && typeof parsed.apiContracts === 'object'
|
|
115
|
+
? parsed.apiContracts
|
|
116
|
+
: {});
|
|
101
117
|
}
|
|
102
118
|
// Legacy flat format: the whole object is the project map
|
|
103
119
|
return new DependenciesFile('', {}, parsed);
|
|
@@ -120,6 +136,7 @@ function formatGraphJson(file) {
|
|
|
120
136
|
lines.push(` ${JSON.stringify(name)}: ${JSON.stringify(file.commands[name])}${comma}`);
|
|
121
137
|
});
|
|
122
138
|
lines.push(` },`);
|
|
139
|
+
lines.push(...apiContractsLines(file.apiContracts));
|
|
123
140
|
lines.push(` "projects": {`);
|
|
124
141
|
const keys = Object.keys(file.projects).sort();
|
|
125
142
|
keys.forEach((key, index) => {
|
|
@@ -134,6 +151,23 @@ function formatGraphJson(file) {
|
|
|
134
151
|
lines.push('}');
|
|
135
152
|
return lines.join('\n') + '\n';
|
|
136
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* The `"apiContracts": {...}` block (4-space indent), with a trailing comma since `projects` always
|
|
156
|
+
* follows it. Omitted entirely when empty, so a repo with no api contracts keeps the old file shape.
|
|
157
|
+
* The scanner already sorts contracts by name and leaves methods in declaration order, so pretty
|
|
158
|
+
* JSON.stringify is deterministic.
|
|
159
|
+
*/
|
|
160
|
+
// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling formatters here
|
|
161
|
+
function apiContractsLines(contracts) {
|
|
162
|
+
if (Object.keys(contracts).length === 0)
|
|
163
|
+
return [];
|
|
164
|
+
const pretty = JSON.stringify(contracts, null, 4).split('\n');
|
|
165
|
+
return pretty.map((line, index) => {
|
|
166
|
+
const prefix = index === 0 ? '"apiContracts": ' : '';
|
|
167
|
+
const suffix = index === pretty.length - 1 ? ',' : '';
|
|
168
|
+
return ` ${prefix}${line}${suffix}`;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
137
171
|
/**
|
|
138
172
|
* Format one project entry's fields (12-space indent). Optional metadata
|
|
139
173
|
* fields are only emitted when present.
|
|
@@ -227,7 +261,7 @@ function pushApiRelationsField(lines, value) {
|
|
|
227
261
|
* @param workspaceRoot - Absolute path to workspace root
|
|
228
262
|
* @param graphPath - Relative path to graph file (default: architecture/dependencies.json)
|
|
229
263
|
*/
|
|
230
|
-
function saveGraph(graph, workspaceRoot, graphPath = exports.DEFAULT_GRAPH_PATH) {
|
|
264
|
+
function saveGraph(graph, workspaceRoot, graphPath = exports.DEFAULT_GRAPH_PATH, apiContracts = {}) {
|
|
231
265
|
const fullPath = path.join(workspaceRoot, graphPath);
|
|
232
266
|
const dir = path.dirname(fullPath);
|
|
233
267
|
// Ensure directory exists
|
|
@@ -240,7 +274,7 @@ function saveGraph(graph, workspaceRoot, graphPath = exports.DEFAULT_GRAPH_PATH)
|
|
|
240
274
|
for (const key of sortedKeys) {
|
|
241
275
|
sortedGraph[key] = graph[key];
|
|
242
276
|
}
|
|
243
|
-
const content = formatGraphJson(new DependenciesFile(exports.AI_INSTRUCTIONS, exports.GRAPH_COMMANDS, sortedGraph));
|
|
277
|
+
const content = formatGraphJson(new DependenciesFile(exports.AI_INSTRUCTIONS, exports.GRAPH_COMMANDS, sortedGraph, apiContracts));
|
|
244
278
|
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
245
279
|
}
|
|
246
280
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-loader.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-loader.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;;;AA0EH,4CA2BC;AAuID,8BAsBC;AAKD,0CAMC;;AA3QD,+CAAyB;AACzB,mDAA6B;AAG7B,wCAAqC;AAErC;;GAEG;AACU,QAAA,kBAAkB,GAAG,gCAAgC,CAAC;AAEnE;;;GAGG;AACU,QAAA,eAAe,GACxB,sFAAsF;IACtF,oFAAoF;IACpF,kFAAkF;IAClF,mFAAmF;IACnF,kFAAkF;IAClF,2EAA2E;IAC3E,mFAAmF;IACnF,0EAA0E,CAAC;AAO/E;;;;GAIG;AACU,QAAA,cAAc,GAAe;IACtC,sBAAsB,EAClB,kFAAkF;QAClF,wFAAwF;IAC5F,qBAAqB,EACjB,uFAAuF;QACvF,sBAAsB;IAC1B,4BAA4B,EACxB,yFAAyF;QACzF,sBAAsB;IAC1B,iBAAiB,EACb,yEAAyE;QACzE,gEAAgE;IACpE,eAAe,EACX,wFAAwF;QACxF,6FAA6F;CACpG,CAAC;AAEF;;GAEG;AACH,MAAa,gBAAgB;IAEL;IACA;IACA;IAHpB,YACoB,cAAsB,EACtB,QAAoB,EACpB,QAAuB;QAFvB,mBAAc,GAAd,cAAc,CAAQ;QACtB,aAAQ,GAAR,QAAQ,CAAY;QACpB,aAAQ,GAAR,QAAQ,CAAe;IACxC,CAAC;CACP;AAND,4CAMC;AAED;;;;;;;GAOG;AACH,SAAgB,gBAAgB,CAC5B,aAAqB,EACrB,YAAoB,0BAAkB;IAEtC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IAErD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,8DAA8D;IAC9D,IAAI,CAAC;QACD,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACnD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACnC,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,UAAU,IAAI,MAAM,EAAE,CAAC;YACxE,OAAO,IAAI,gBAAgB,CACvB,OAAO,MAAM,CAAC,cAAc,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,EACtE,MAAM,CAAC,QAAQ,KAAK,IAAI,IAAI,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAE,MAAM,CAAC,QAAuB,CAAC,CAAC,CAAC,EAAE,EACtG,MAAM,CAAC,QAAyB,CACnC,CAAC;QACN,CAAC;QACD,0DAA0D;QAC1D,OAAO,IAAI,gBAAgB,CAAC,EAAE,EAAE,EAAE,EAAE,MAAuB,CAAC,CAAC;IACjE,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,6BAA6B,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/E,CAAC;AACL,CAAC;AAED;;GAEG;AACH,SAAS,eAAe,CAAC,IAAsB;IAC3C,MAAM,KAAK,GAAa,CAAC,GAAG,CAAC,CAAC;IAC9B,KAAK,CAAC,IAAI,CAAC,yBAAyB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;IAC5E,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;IAChC,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChD,YAAY,CAAC,OAAO,CAAC,CAAC,IAAY,EAAE,KAAa,EAAE,EAAE;QACjD,MAAM,KAAK,GAAG,KAAK,KAAK,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;QAC3D,KAAK,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,CAAC;IAClG,CAAC,CAAC,CAAC;IACH,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACrB,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;IAEhC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/C,IAAI,CAAC,OAAO,CAAC,CAAC,GAAW,EAAE,KAAa,EAAE,EAAE;QACxC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QACjC,MAAM,MAAM,GAAG,KAAK,KAAK,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QACzC,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;QAEhC,KAAK,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAChD,KAAK,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;QACvC,KAAK,CAAC,IAAI,CAAC,YAAY,KAAK,EAAE,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACpB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChB,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AACnC,CAAC;AAED;;;GAGG;AACH,SAAS,gBAAgB,CAAC,KAAiB;IACvC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,wBAAwB,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC;IAEnD,sBAAsB,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAC5D,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7C,iBAAiB,CAAC,KAAK,EAAE,aAAa,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;IAC3D,6BAA6B,CAAC,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IACzD,wBAAwB,CAAC,KAAK,EAAE,aAAa,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;IAClE,iBAAiB,CAAC,KAAK,EAAE,kBAAkB,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;IACrE,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,EAAE,KAAK,CAAC,oBAAoB,CAAC,CAAC;IAC7E,iBAAiB,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IACzD,qBAAqB,CAAC,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IAEjD,IAAI,KAAK,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,KAAK,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;IAC9C,CAAC;SAAM,CAAC;QACJ,KAAK,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;QACzC,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,GAAW,EAAE,QAAgB,EAAE,EAAE;YACtD,MAAM,QAAQ,GAAG,QAAQ,KAAK,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;YACpE,KAAK,CAAC,IAAI,CAAC,mBAAmB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,QAAQ,EAAE,CAAC,CAAC;QACpE,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAED;;GAEG;AACH,SAAS,iBAAiB,CAAC,KAAe,EAAE,KAAa,EAAE,KAAyB;IAChF,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACtB,KAAK,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAClF,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,qHAAqH;AACrH,SAAS,6BAA6B,CAClC,KAAe,EACf,KAAkD;IAElD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACtB,KAAK,CAAC,IAAI,CAAC,+BAA+B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACxE,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,qHAAqH;AACrH,SAAS,wBAAwB,CAAC,KAAe,EAAE,KAAa,EAAE,KAA0B;IACxF,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACtB,KAAK,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAClF,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,SAAS,sBAAsB,CAAC,KAAe,EAAE,KAAa,EAAE,KAA2B;IACvF,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACtB,KAAK,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAClF,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,qHAAqH;AACrH,SAAS,qBAAqB,CAAC,KAAe,EAAE,KAAsC;IAClF,IAAI,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IACnE,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1D,MAAM,CAAC,OAAO,CAAC,CAAC,IAAY,EAAE,KAAa,EAAE,EAAE;QAC3C,MAAM,MAAM,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE,CAAC;QACrD,MAAM,MAAM,GAAG,KAAK,KAAK,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACtD,KAAK,CAAC,IAAI,CAAC,eAAe,MAAM,GAAG,IAAI,GAAG,MAAM,EAAE,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,SAAS,CACrB,KAAoB,EACpB,aAAqB,EACrB,YAAoB,0BAAkB;IAEtC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IACrD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEnC,0BAA0B;IAC1B,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACtB,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,qCAAqC;IACrC,MAAM,WAAW,GAAkB,EAAE,CAAC;IACtC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7C,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC3B,WAAW,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;IAClC,CAAC;IAED,MAAM,OAAO,GAAG,eAAe,CAAC,IAAI,gBAAgB,CAAC,uBAAe,EAAE,sBAAc,EAAE,WAAW,CAAC,CAAC,CAAC;IACpG,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AACjD,CAAC;AAED;;GAEG;AACH,SAAgB,eAAe,CAC3B,aAAqB,EACrB,YAAoB,0BAAkB;IAEtC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IACrD,OAAO,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;AACnC,CAAC","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"]}
|
|
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 { ApiContracts, 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 * Every API contract's per-method trigger table (kind + queue name + path).\n *\n * MUST be persisted, for the same reason `callsService` must: the runtime graph is derived\n * SOLELY from this file, and `validate-runtime-architecture` re-derives from the LOADED copy\n * while generate derives from the in-memory one. A field that is scanned but not written\n * makes those two inputs differ, and the validator reports a diff no one can fix.\n */\n public readonly apiContracts: ApiContracts = {}\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 // Absent in a file written before apiContracts existed; an empty table degrades the\n // runtime graph to unnamed per-pair queues rather than failing to load.\n parsed.apiContracts !== null && typeof parsed.apiContracts === 'object'\n ? (parsed.apiContracts as ApiContracts)\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 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(...apiContractsLines(file.apiContracts));\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 `\"apiContracts\": {...}` block (4-space indent), with a trailing comma since `projects` always\n * follows it. Omitted entirely when empty, so a repo with no api contracts keeps the old file shape.\n * The scanner already sorts contracts by name and leaves methods in declaration order, so pretty\n * JSON.stringify is deterministic.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling formatters here\nfunction apiContractsLines(contracts: ApiContracts): string[] {\n if (Object.keys(contracts).length === 0) return [];\n const pretty = JSON.stringify(contracts, null, 4).split('\\n');\n return pretty.map((line: string, index: number) => {\n const prefix = index === 0 ? '\"apiContracts\": ' : '';\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 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 apiContracts: ApiContracts = {}\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(AI_INSTRUCTIONS, GRAPH_COMMANDS, sortedGraph, apiContracts)\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"]}
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* "mode": "ON", // "OFF" disables the whole feature
|
|
9
9
|
* "ignoreModifiedUntilEpoch": 0, // whole-rule punt (epoch seconds)
|
|
10
10
|
* "allowedCycles": [ { "services": ["a","b"], "reason": "...", "until": 1771931925 } ],
|
|
11
|
-
* "showExternalNodes": true
|
|
11
|
+
* "showExternalNodes": true, // draw firestore/gmail/... as terminal nodes
|
|
12
|
+
* "externalApiPaths": ["libraries/apis/external/**"] // where those vendor contracts live
|
|
12
13
|
* }
|
|
13
14
|
*/
|
|
14
15
|
import { SkipRuleResult } from '@webpieces/rules-config';
|
|
@@ -25,6 +26,11 @@ export interface RuntimeRuleConfig {
|
|
|
25
26
|
allowedCycles: AllowedCycle[];
|
|
26
27
|
/** Render the dashed external terminal nodes in the runtime viz (default true). */
|
|
27
28
|
showExternalNodes: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Globs of project roots whose exported `*Api` types are contracts for systems OUTSIDE this repo.
|
|
31
|
+
* Empty (the default) means the scan looks for no vendor seams at all.
|
|
32
|
+
*/
|
|
33
|
+
externalApiPaths: string[];
|
|
28
34
|
}
|
|
29
35
|
/** Load the runtime-architecture rule config (with safe defaults). */
|
|
30
36
|
export declare function loadRuntimeConfig(workspaceRoot: string): RuntimeRuleConfig;
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* "mode": "ON", // "OFF" disables the whole feature
|
|
10
10
|
* "ignoreModifiedUntilEpoch": 0, // whole-rule punt (epoch seconds)
|
|
11
11
|
* "allowedCycles": [ { "services": ["a","b"], "reason": "...", "until": 1771931925 } ],
|
|
12
|
-
* "showExternalNodes": true
|
|
12
|
+
* "showExternalNodes": true, // draw firestore/gmail/... as terminal nodes
|
|
13
|
+
* "externalApiPaths": ["libraries/apis/external/**"] // where those vendor contracts live
|
|
13
14
|
* }
|
|
14
15
|
*/
|
|
15
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -36,6 +37,11 @@ function loadRuntimeConfig(workspaceRoot) {
|
|
|
36
37
|
// Opt-OUT: the external systems are the ones that page you at 3am, so they are drawn unless
|
|
37
38
|
// a repo explicitly says its external surface is too noisy to be useful.
|
|
38
39
|
showExternalNodes: raw.showExternalNodes !== false,
|
|
40
|
+
// Opt-IN: there is no safe guess for where a repo keeps its vendor wrappers, and guessing
|
|
41
|
+
// wrong would classify ordinary libraries as systems outside the repo.
|
|
42
|
+
externalApiPaths: Array.isArray(raw.externalApiPaths)
|
|
43
|
+
? raw.externalApiPaths.filter((entry) => typeof entry === 'string' && entry.length > 0)
|
|
44
|
+
: [],
|
|
39
45
|
};
|
|
40
46
|
}
|
|
41
47
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-config.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-config.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"runtime-config.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-config.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG;;;AA4CH,8CAoBC;AAQD,8CAEC;AAMD,sCAGC;AAGD,8BAEC;AAtFD,0DAA0F;AAE7E,QAAA,iBAAiB,GAAG,sBAAsB,CAAC;AAmCxD,SAAS,aAAa,CAAC,KAAmB;IACtC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;AACtE,CAAC;AAED,sEAAsE;AACtE,SAAgB,iBAAiB,CAAC,aAAqB;IACnD,MAAM,MAAM,GAAG,IAAA,8BAAe,EAAC,aAAa,CAAC,CAAC,QAAQ,CAAC;IACvD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,yBAAiB,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,EAAE,CAAmB,CAAC;IACpD,OAAO;QACH,GAAG,EAAE,IAAI,EAAE,KAAK,IAAI,KAAK;QACzB,wBAAwB,EACpB,OAAO,GAAG,CAAC,wBAAwB,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC,CAAC,SAAS;QAC/F,uBAAuB,EACnB,OAAO,GAAG,CAAC,uBAAuB,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC,CAAC,SAAS;QAC7F,aAAa,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE;QAC9F,4FAA4F;QAC5F,yEAAyE;QACzE,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,KAAK,KAAK;QAClD,0FAA0F;QAC1F,uEAAuE;QACvE,gBAAgB,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;YACjD,CAAC,CAAC,GAAG,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,KAAa,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;YAC/F,CAAC,CAAC,EAAE;KACX,CAAC;AACN,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,MAAyB;IACvD,OAAO,IAAA,6BAAc,EAAC,MAAM,CAAC,wBAAwB,EAAE,MAAM,CAAC,uBAAuB,CAAC,CAAC;AAC3F,CAAC;AAED;;;GAGG;AACH,SAAgB,aAAa,CAAC,KAAyB;IACnD,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,KAAK,CAAC;AACrC,CAAC;AAED,wDAAwD;AACxD,SAAgB,SAAS,CAAC,KAAa;IACnC,OAAO,IAAI,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9D,CAAC","sourcesContent":["/**\n * Runtime Config\n *\n * Loads the `runtime-architecture` rule from webpieces.config.json and exposes\n * typed accessors shared by the generate + validate + visualize executors.\n *\n * \"runtime-architecture\": {\n * \"mode\": \"ON\", // \"OFF\" disables the whole feature\n * \"ignoreModifiedUntilEpoch\": 0, // whole-rule punt (epoch seconds)\n * \"allowedCycles\": [ { \"services\": [\"a\",\"b\"], \"reason\": \"...\", \"until\": 1771931925 } ],\n * \"showExternalNodes\": true, // draw firestore/gmail/... as terminal nodes\n * \"externalApiPaths\": [\"libraries/apis/external/**\"] // where those vendor contracts live\n * }\n */\n\nimport { loadAndValidate, shouldSkipRule, SkipRuleResult } from '@webpieces/rules-config';\n\nexport const RUNTIME_RULE_NAME = 'runtime-architecture';\n\nexport interface AllowedCycle {\n services: string[];\n reason?: string;\n until?: number;\n}\n\nexport interface RuntimeRuleConfig {\n off: boolean;\n ignoreModifiedUntilEpoch?: number;\n ignoreRuleWhileOnBranch?: string;\n allowedCycles: AllowedCycle[];\n /** Render the dashed external terminal nodes in the runtime viz (default true). */\n showExternalNodes: boolean;\n /**\n * Globs of project roots whose exported `*Api` types are contracts for systems OUTSIDE this repo.\n * Empty (the default) means the scan looks for no vendor seams at all.\n */\n externalApiPaths: string[];\n}\n\n/**\n * Typed view of the opaque webpieces.config.json option bag for this rule. The\n * config is trusted (it is the workspace's own file), so we cast once here and\n * defensively narrow arrays/numbers rather than threading `unknown` everywhere.\n */\ninterface RuntimeRuleRaw {\n ignoreModifiedUntilEpoch?: number;\n ignoreRuleWhileOnBranch?: string;\n allowedCycles?: AllowedCycle[];\n showExternalNodes?: boolean;\n externalApiPaths?: string[];\n}\n\nfunction isUsableCycle(cycle: AllowedCycle): boolean {\n return Array.isArray(cycle.services) && cycle.services.length > 0;\n}\n\n/** Load the runtime-architecture rule config (with safe defaults). */\nexport function loadRuntimeConfig(workspaceRoot: string): RuntimeRuleConfig {\n const shared = loadAndValidate(workspaceRoot).resolved;\n const rule = shared.rules.get(RUNTIME_RULE_NAME);\n const raw = (rule?.options ?? {}) as RuntimeRuleRaw;\n return {\n off: rule?.isOff ?? false,\n ignoreModifiedUntilEpoch:\n typeof raw.ignoreModifiedUntilEpoch === 'number' ? raw.ignoreModifiedUntilEpoch : undefined,\n ignoreRuleWhileOnBranch:\n typeof raw.ignoreRuleWhileOnBranch === 'string' ? raw.ignoreRuleWhileOnBranch : undefined,\n allowedCycles: Array.isArray(raw.allowedCycles) ? raw.allowedCycles.filter(isUsableCycle) : [],\n // Opt-OUT: the external systems are the ones that page you at 3am, so they are drawn unless\n // a repo explicitly says its external surface is too noisy to be useful.\n showExternalNodes: raw.showExternalNodes !== false,\n // Opt-IN: there is no safe guess for where a repo keeps its vendor wrappers, and guessing\n // wrong would classify ordinary libraries as systems outside the repo.\n externalApiPaths: Array.isArray(raw.externalApiPaths)\n ? raw.externalApiPaths.filter((entry: string) => typeof entry === 'string' && entry.length > 0)\n : [],\n };\n}\n\n/**\n * Whole-rule report-only window honoring BOTH escape hatches: skip while on the\n * named branch (ignoreRuleWhileOnBranch) or until the epoch passes\n * (ignoreModifiedUntilEpoch). When `.skip` is true, problems are reported but\n * the build is not failed.\n */\nexport function runtimeReportOnly(config: RuntimeRuleConfig): SkipRuleResult {\n return shouldSkipRule(config.ignoreModifiedUntilEpoch, config.ignoreRuleWhileOnBranch);\n}\n\n/**\n * Whole-rule grace window: while now < epoch, failures are reported but do not\n * fail the build (warn). Mirrors the other webpieces rules.\n */\nexport function isGraceActive(epoch: number | undefined): boolean {\n if (epoch === undefined) return false;\n return Date.now() / 1000 < epoch;\n}\n\n/** Format the epoch as an ISO date for log messages. */\nexport function epochDate(epoch: number): string {\n return new Date(epoch * 1000).toISOString().split('T')[0];\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Graph persistence
|
|
3
|
+
*
|
|
4
|
+
* Reading, writing and canonical-serializing architecture/runtime-dependencies.json. Split out of
|
|
5
|
+
* runtime-graph.ts (which owns the derivation and had grown past the file-size limit).
|
|
6
|
+
*
|
|
7
|
+
* `formatRuntimeJson` is the ONE canonical rendering: validate-runtime-architecture compares the
|
|
8
|
+
* freshly derived graph to the committed one as STRINGS, so any second way of writing this file
|
|
9
|
+
* would show up as unfixable drift.
|
|
10
|
+
*/
|
|
11
|
+
import type { RuntimeGraph } from './runtime-graph-model';
|
|
12
|
+
export declare const DEFAULT_RUNTIME_GRAPH_PATH = "architecture/runtime-dependencies.json";
|
|
13
|
+
export declare function saveRuntimeGraph(graph: RuntimeGraph, workspaceRoot: string, graphPath?: string): void;
|
|
14
|
+
export declare function runtimeGraphFileExists(workspaceRoot: string, graphPath?: string): boolean;
|
|
15
|
+
export declare function loadRuntimeGraph(workspaceRoot: string, graphPath?: string): RuntimeGraph | null;
|
|
16
|
+
/** Serialize for an in-memory equality check (matches the on-disk format). */
|
|
17
|
+
export declare function serializeRuntimeGraph(graph: RuntimeGraph): string;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Runtime Graph persistence
|
|
4
|
+
*
|
|
5
|
+
* Reading, writing and canonical-serializing architecture/runtime-dependencies.json. Split out of
|
|
6
|
+
* runtime-graph.ts (which owns the derivation and had grown past the file-size limit).
|
|
7
|
+
*
|
|
8
|
+
* `formatRuntimeJson` is the ONE canonical rendering: validate-runtime-architecture compares the
|
|
9
|
+
* freshly derived graph to the committed one as STRINGS, so any second way of writing this file
|
|
10
|
+
* would show up as unfixable drift.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.DEFAULT_RUNTIME_GRAPH_PATH = void 0;
|
|
14
|
+
exports.saveRuntimeGraph = saveRuntimeGraph;
|
|
15
|
+
exports.runtimeGraphFileExists = runtimeGraphFileExists;
|
|
16
|
+
exports.loadRuntimeGraph = loadRuntimeGraph;
|
|
17
|
+
exports.serializeRuntimeGraph = serializeRuntimeGraph;
|
|
18
|
+
const tslib_1 = require("tslib");
|
|
19
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
20
|
+
const path = tslib_1.__importStar(require("path"));
|
|
21
|
+
const toError_1 = require("../toError");
|
|
22
|
+
exports.DEFAULT_RUNTIME_GRAPH_PATH = 'architecture/runtime-dependencies.json';
|
|
23
|
+
/** Deterministic JSON (sorted keys + arrays already sorted during assembly). */
|
|
24
|
+
// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts
|
|
25
|
+
function formatRuntimeJson(graph) {
|
|
26
|
+
return JSON.stringify(graph, null, 4) + '\n';
|
|
27
|
+
}
|
|
28
|
+
// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts
|
|
29
|
+
function saveRuntimeGraph(graph, workspaceRoot, graphPath = exports.DEFAULT_RUNTIME_GRAPH_PATH) {
|
|
30
|
+
const fullPath = path.join(workspaceRoot, graphPath);
|
|
31
|
+
const dir = path.dirname(fullPath);
|
|
32
|
+
if (!fs.existsSync(dir))
|
|
33
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
34
|
+
fs.writeFileSync(fullPath, formatRuntimeJson(graph), 'utf-8');
|
|
35
|
+
}
|
|
36
|
+
// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts
|
|
37
|
+
function runtimeGraphFileExists(workspaceRoot, graphPath = exports.DEFAULT_RUNTIME_GRAPH_PATH) {
|
|
38
|
+
return fs.existsSync(path.join(workspaceRoot, graphPath));
|
|
39
|
+
}
|
|
40
|
+
// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts
|
|
41
|
+
function loadRuntimeGraph(workspaceRoot, graphPath = exports.DEFAULT_RUNTIME_GRAPH_PATH) {
|
|
42
|
+
const fullPath = path.join(workspaceRoot, graphPath);
|
|
43
|
+
if (!fs.existsSync(fullPath))
|
|
44
|
+
return null;
|
|
45
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
const error = (0, toError_1.toError)(err);
|
|
51
|
+
throw new Error(`Failed to load runtime graph from ${fullPath}`, { cause: error });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Serialize for an in-memory equality check (matches the on-disk format). */
|
|
55
|
+
// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts
|
|
56
|
+
function serializeRuntimeGraph(graph) {
|
|
57
|
+
return formatRuntimeJson(graph);
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=runtime-graph-io.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-graph-io.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph-io.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;;AAgBH,4CASC;AAGD,wDAKC;AAGD,4CAaC;AAID,sDAEC;;AArDD,+CAAyB;AACzB,mDAA6B;AAE7B,wCAAqC;AAExB,QAAA,0BAA0B,GAAG,wCAAwC,CAAC;AAEnF,gFAAgF;AAChF,8HAA8H;AAC9H,SAAS,iBAAiB,CAAC,KAAmB;IAC1C,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;AACjD,CAAC;AAED,8HAA8H;AAC9H,SAAgB,gBAAgB,CAC5B,KAAmB,EACnB,aAAqB,EACrB,YAAoB,kCAA0B;IAE9C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IACrD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChE,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,iBAAiB,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;AAClE,CAAC;AAED,8HAA8H;AAC9H,SAAgB,sBAAsB,CAClC,aAAqB,EACrB,YAAoB,kCAA0B;IAE9C,OAAO,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,8HAA8H;AAC9H,SAAgB,gBAAgB,CAC5B,aAAqB,EACrB,YAAoB,kCAA0B;IAE9C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IACrD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,8DAA8D;IAC9D,IAAI,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAiB,CAAC;IAC1E,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,qCAAqC,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IACvF,CAAC;AACL,CAAC;AAED,8EAA8E;AAC9E,8HAA8H;AAC9H,SAAgB,qBAAqB,CAAC,KAAmB;IACrD,OAAO,iBAAiB,CAAC,KAAK,CAAC,CAAC;AACpC,CAAC","sourcesContent":["/**\n * Runtime Graph persistence\n *\n * Reading, writing and canonical-serializing architecture/runtime-dependencies.json. Split out of\n * runtime-graph.ts (which owns the derivation and had grown past the file-size limit).\n *\n * `formatRuntimeJson` is the ONE canonical rendering: validate-runtime-architecture compares the\n * freshly derived graph to the committed one as STRINGS, so any second way of writing this file\n * would show up as unfixable drift.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { RuntimeGraph } from './runtime-graph-model';\nimport { toError } from '../toError';\n\nexport const DEFAULT_RUNTIME_GRAPH_PATH = 'architecture/runtime-dependencies.json';\n\n/** Deterministic JSON (sorted keys + arrays already sorted during assembly). */\n// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts\nfunction formatRuntimeJson(graph: RuntimeGraph): string {\n return JSON.stringify(graph, null, 4) + '\\n';\n}\n\n// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts\nexport function saveRuntimeGraph(\n graph: RuntimeGraph,\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): void {\n const fullPath = path.join(workspaceRoot, graphPath);\n const dir = path.dirname(fullPath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(fullPath, formatRuntimeJson(graph), 'utf-8');\n}\n\n// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts\nexport function runtimeGraphFileExists(\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): boolean {\n return fs.existsSync(path.join(workspaceRoot, graphPath));\n}\n\n// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts\nexport function loadRuntimeGraph(\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): RuntimeGraph | null {\n const fullPath = path.join(workspaceRoot, graphPath);\n if (!fs.existsSync(fullPath)) return null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(fs.readFileSync(fullPath, 'utf-8')) as RuntimeGraph;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`Failed to load runtime graph from ${fullPath}`, { cause: error });\n }\n}\n\n/** Serialize for an in-memory equality check (matches the on-disk format). */\n// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts\nexport function serializeRuntimeGraph(graph: RuntimeGraph): string {\n return formatRuntimeJson(graph);\n}\n"]}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Graph model
|
|
3
|
+
*
|
|
4
|
+
* The serialization DTOs for architecture/runtime-dependencies.json. Split out of runtime-graph.ts,
|
|
5
|
+
* which owns the DERIVATION (and had grown past the file-size limit), so the committed data shape
|
|
6
|
+
* can be read on its own — it is what every consumer of the file, and any future Terraform
|
|
7
|
+
* cross-check, actually programs against.
|
|
8
|
+
*
|
|
9
|
+
* Interfaces rather than classes: these are parsed straight out of JSON with `JSON.parse`, so a
|
|
10
|
+
* class would only ever be a shape assertion over a plain object, never a constructed instance.
|
|
11
|
+
*/
|
|
12
|
+
import type { ApiTransport } from './api-usage/api-relations';
|
|
13
|
+
export interface RuntimeService {
|
|
14
|
+
level: number;
|
|
15
|
+
/**
|
|
16
|
+
* The name clients address this service by (`new ClientConfig('helper-fsdb')`), declared in its
|
|
17
|
+
* project.json. Absent for a service nothing calls by name (e.g. a browser app).
|
|
18
|
+
*/
|
|
19
|
+
serviceName?: string;
|
|
20
|
+
/**
|
|
21
|
+
* The service(s) this node's clients call when the call site carries no literal `ClientConfig`,
|
|
22
|
+
* declared in its project.json (metadata.webpieces.callsService). A single name, or an
|
|
23
|
+
* `{ apiClassName: serviceName }` map. Absent when the node declares no target. Mirrors
|
|
24
|
+
* GraphEntry.callsService; it is the CALLING-side counterpart of `serviceName`.
|
|
25
|
+
*/
|
|
26
|
+
callsService?: string | Record<string, string>;
|
|
27
|
+
implements: string[];
|
|
28
|
+
/**
|
|
29
|
+
* apiClassName -> the LIBRARY project whose apiRelations declared that implements, for the apis
|
|
30
|
+
* this service serves through an embedded library rather than its own source (e.g. a shared
|
|
31
|
+
* route-registration lib). Answers "who implements WarmupApi, and where did that come from?",
|
|
32
|
+
* which previously required walking the dependsOn closure by hand.
|
|
33
|
+
*/
|
|
34
|
+
implementsVia?: Record<string, string>;
|
|
35
|
+
uses: string[];
|
|
36
|
+
dependsOn: string[];
|
|
37
|
+
/**
|
|
38
|
+
* When false, this service is hidden from the rendered runtime graph (its
|
|
39
|
+
* node AND every edge touching it are omitted from the HTML/DOT). It stays
|
|
40
|
+
* in runtime-dependencies.json so the data view is complete. Absent means
|
|
41
|
+
* drawn (the default). Mirrors GraphEntry.drawOnGraph from the `drawOnGraph:`
|
|
42
|
+
* nx tag.
|
|
43
|
+
*/
|
|
44
|
+
drawOnGraph?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface RuntimeApi {
|
|
47
|
+
implementedBy: string[];
|
|
48
|
+
usedBy: string[];
|
|
49
|
+
/** Transport of this API — 'rpc' (direct call) or 'pubsub' (delivered through a queue). */
|
|
50
|
+
type?: ApiTransport;
|
|
51
|
+
/**
|
|
52
|
+
* The api-lib project that OWNS this contract. For a contract nothing in-repo implements, this
|
|
53
|
+
* is the external library the calls leave the repo through (`lib-firestore`, `lib-gmail`), which
|
|
54
|
+
* is what the runtime viz labels its terminal external nodes with.
|
|
55
|
+
*/
|
|
56
|
+
owner?: string;
|
|
57
|
+
}
|
|
58
|
+
export interface RuntimeEdge {
|
|
59
|
+
from: string;
|
|
60
|
+
to: string;
|
|
61
|
+
via: string[];
|
|
62
|
+
/**
|
|
63
|
+
* Transport of this edge. 'rpc' → a direct call arrow. 'pubsub' → the producer enqueues and the
|
|
64
|
+
* consumer is delivered later, so the runtime viz draws it as producer → QUEUE → consumer.
|
|
65
|
+
* Edges are split by transport, so every edge is a single kind.
|
|
66
|
+
*/
|
|
67
|
+
type?: ApiTransport;
|
|
68
|
+
/**
|
|
69
|
+
* `"ApiClassName.methodName"` — the queue this edge flows through. Present iff `type` is
|
|
70
|
+
* 'pubsub'. Queues are per METHOD, not per service pair, because that is the unit Cloud Tasks
|
|
71
|
+
* (and Terraform) actually create, so two services exchanging three queued methods are three
|
|
72
|
+
* queues rather than one arrow.
|
|
73
|
+
*/
|
|
74
|
+
queue?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* One Cloud Tasks queue: the async seam between a producer and a consumer, at METHOD granularity.
|
|
78
|
+
*
|
|
79
|
+
* `producedBy` and `consumedBy` are deliberately not symmetric in confidence — see
|
|
80
|
+
* {@link ApiRef.methodsInferred}. The consumer is derived from `addRoutes` plus the contract's
|
|
81
|
+
* method table and is exact; the producer is attributed to every queued method of the contract it
|
|
82
|
+
* built a client for, because which methods it enqueues is not statically recoverable.
|
|
83
|
+
*/
|
|
84
|
+
export interface RuntimeQueue {
|
|
85
|
+
api: string;
|
|
86
|
+
method: string;
|
|
87
|
+
/** `@Queue(...)` override, else `${Api}-${method}` — the name Terraform must match 1:1. */
|
|
88
|
+
queueName: string;
|
|
89
|
+
producedBy: string[];
|
|
90
|
+
consumedBy: string[];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* An endpoint driven by something that is NOT an in-repo caller — a clock or an outside system.
|
|
94
|
+
* These never appear as runtime EDGES (there is no in-repo `from`), which is exactly why they were
|
|
95
|
+
* invisible until now: a nightly sweep and a GCP push subscription are real runtime entry points
|
|
96
|
+
* with real Terraform behind them, and the graph showed neither.
|
|
97
|
+
*/
|
|
98
|
+
export interface RuntimeTrigger {
|
|
99
|
+
/** 'cron' → a scheduler fires it; 'external' → a system outside this repo posts to it. */
|
|
100
|
+
kind: 'cron' | 'external';
|
|
101
|
+
api: string;
|
|
102
|
+
method: string;
|
|
103
|
+
/** The service that SERVES the endpoint (the arrow's head). */
|
|
104
|
+
service: string;
|
|
105
|
+
/** Present for 'cron': the Cloud Scheduler job / queue name Terraform must match. */
|
|
106
|
+
queueName?: string;
|
|
107
|
+
}
|
|
108
|
+
export interface RuntimeUnresolved {
|
|
109
|
+
service: string;
|
|
110
|
+
api: string;
|
|
111
|
+
}
|
|
112
|
+
export interface RuntimeGraph {
|
|
113
|
+
services: Record<string, RuntimeService>;
|
|
114
|
+
apis: Record<string, RuntimeApi>;
|
|
115
|
+
runtimeEdges: RuntimeEdge[];
|
|
116
|
+
unresolvedUses: RuntimeUnresolved[];
|
|
117
|
+
/** `"Api.method"` -> the queue between its producers and its consumers. */
|
|
118
|
+
queues: Record<string, RuntimeQueue>;
|
|
119
|
+
/** Clock- and outside-driven entry points, sorted for determinism. */
|
|
120
|
+
triggers: RuntimeTrigger[];
|
|
121
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Runtime Graph model
|
|
4
|
+
*
|
|
5
|
+
* The serialization DTOs for architecture/runtime-dependencies.json. Split out of runtime-graph.ts,
|
|
6
|
+
* which owns the DERIVATION (and had grown past the file-size limit), so the committed data shape
|
|
7
|
+
* can be read on its own — it is what every consumer of the file, and any future Terraform
|
|
8
|
+
* cross-check, actually programs against.
|
|
9
|
+
*
|
|
10
|
+
* Interfaces rather than classes: these are parsed straight out of JSON with `JSON.parse`, so a
|
|
11
|
+
* class would only ever be a shape assertion over a plain object, never a constructed instance.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
//# sourceMappingURL=runtime-graph-model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-graph-model.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph-model.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG","sourcesContent":["/**\n * Runtime Graph model\n *\n * The serialization DTOs for architecture/runtime-dependencies.json. Split out of runtime-graph.ts,\n * which owns the DERIVATION (and had grown past the file-size limit), so the committed data shape\n * can be read on its own — it is what every consumer of the file, and any future Terraform\n * cross-check, actually programs against.\n *\n * Interfaces rather than classes: these are parsed straight out of JSON with `JSON.parse`, so a\n * class would only ever be a shape assertion over a plain object, never a constructed instance.\n */\n\nimport type { ApiTransport } from './api-usage/api-relations';\n\nexport interface RuntimeService {\n level: number;\n /**\n * The name clients address this service by (`new ClientConfig('helper-fsdb')`), declared in its\n * project.json. Absent for a service nothing calls by name (e.g. a browser app).\n */\n serviceName?: string;\n /**\n * The service(s) this node's clients call when the call site carries no literal `ClientConfig`,\n * declared in its project.json (metadata.webpieces.callsService). A single name, or an\n * `{ apiClassName: serviceName }` map. Absent when the node declares no target. Mirrors\n * GraphEntry.callsService; it is the CALLING-side counterpart of `serviceName`.\n */\n callsService?: string | Record<string, string>;\n implements: string[];\n /**\n * apiClassName -> the LIBRARY project whose apiRelations declared that implements, for the apis\n * this service serves through an embedded library rather than its own source (e.g. a shared\n * route-registration lib). Answers \"who implements WarmupApi, and where did that come from?\",\n * which previously required walking the dependsOn closure by hand.\n */\n implementsVia?: Record<string, string>;\n uses: string[];\n dependsOn: string[];\n /**\n * When false, this service is hidden from the rendered runtime graph (its\n * node AND every edge touching it are omitted from the HTML/DOT). It stays\n * in runtime-dependencies.json so the data view is complete. Absent means\n * drawn (the default). Mirrors GraphEntry.drawOnGraph from the `drawOnGraph:`\n * nx tag.\n */\n drawOnGraph?: boolean;\n}\n\nexport interface RuntimeApi {\n implementedBy: string[];\n usedBy: string[];\n /** Transport of this API — 'rpc' (direct call) or 'pubsub' (delivered through a queue). */\n type?: ApiTransport;\n /**\n * The api-lib project that OWNS this contract. For a contract nothing in-repo implements, this\n * is the external library the calls leave the repo through (`lib-firestore`, `lib-gmail`), which\n * is what the runtime viz labels its terminal external nodes with.\n */\n owner?: string;\n}\n\nexport interface RuntimeEdge {\n from: string;\n to: string;\n via: string[];\n /**\n * Transport of this edge. 'rpc' → a direct call arrow. 'pubsub' → the producer enqueues and the\n * consumer is delivered later, so the runtime viz draws it as producer → QUEUE → consumer.\n * Edges are split by transport, so every edge is a single kind.\n */\n type?: ApiTransport;\n /**\n * `\"ApiClassName.methodName\"` — the queue this edge flows through. Present iff `type` is\n * 'pubsub'. Queues are per METHOD, not per service pair, because that is the unit Cloud Tasks\n * (and Terraform) actually create, so two services exchanging three queued methods are three\n * queues rather than one arrow.\n */\n queue?: string;\n}\n\n/**\n * One Cloud Tasks queue: the async seam between a producer and a consumer, at METHOD granularity.\n *\n * `producedBy` and `consumedBy` are deliberately not symmetric in confidence — see\n * {@link ApiRef.methodsInferred}. The consumer is derived from `addRoutes` plus the contract's\n * method table and is exact; the producer is attributed to every queued method of the contract it\n * built a client for, because which methods it enqueues is not statically recoverable.\n */\nexport interface RuntimeQueue {\n api: string;\n method: string;\n /** `@Queue(...)` override, else `${Api}-${method}` — the name Terraform must match 1:1. */\n queueName: string;\n producedBy: string[];\n consumedBy: string[];\n}\n\n/**\n * An endpoint driven by something that is NOT an in-repo caller — a clock or an outside system.\n * These never appear as runtime EDGES (there is no in-repo `from`), which is exactly why they were\n * invisible until now: a nightly sweep and a GCP push subscription are real runtime entry points\n * with real Terraform behind them, and the graph showed neither.\n */\nexport interface RuntimeTrigger {\n /** 'cron' → a scheduler fires it; 'external' → a system outside this repo posts to it. */\n kind: 'cron' | 'external';\n api: string;\n method: string;\n /** The service that SERVES the endpoint (the arrow's head). */\n service: string;\n /** Present for 'cron': the Cloud Scheduler job / queue name Terraform must match. */\n queueName?: string;\n}\n\nexport interface RuntimeUnresolved {\n service: string;\n api: string;\n}\n\nexport interface RuntimeGraph {\n services: Record<string, RuntimeService>;\n apis: Record<string, RuntimeApi>;\n runtimeEdges: RuntimeEdge[];\n unresolvedUses: RuntimeUnresolved[];\n /** `\"Api.method\"` -> the queue between its producers and its consumers. */\n queues: Record<string, RuntimeQueue>;\n /** Clock- and outside-driven entry points, sorted for determinism. */\n triggers: RuntimeTrigger[];\n}\n"]}
|
|
@@ -28,74 +28,10 @@
|
|
|
28
28
|
* one, so it must never degrade silently.
|
|
29
29
|
*/
|
|
30
30
|
import type { EnhancedGraph } from './graph-sorter';
|
|
31
|
-
import type {
|
|
32
|
-
|
|
33
|
-
export
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* The name clients address this service by (`new ClientConfig('helper-fsdb')`), declared in its
|
|
37
|
-
* project.json. Absent for a service nothing calls by name (e.g. a browser app).
|
|
38
|
-
*/
|
|
39
|
-
serviceName?: string;
|
|
40
|
-
/**
|
|
41
|
-
* The service(s) this node's clients call when the call site carries no literal `ClientConfig`,
|
|
42
|
-
* declared in its project.json (metadata.webpieces.callsService). A single name, or an
|
|
43
|
-
* `{ apiClassName: serviceName }` map. Absent when the node declares no target. Mirrors
|
|
44
|
-
* GraphEntry.callsService; it is the CALLING-side counterpart of `serviceName`.
|
|
45
|
-
*/
|
|
46
|
-
callsService?: string | Record<string, string>;
|
|
47
|
-
implements: string[];
|
|
48
|
-
/**
|
|
49
|
-
* apiClassName -> the LIBRARY project whose apiRelations declared that implements, for the apis
|
|
50
|
-
* this service serves through an embedded library rather than its own source (e.g. a shared
|
|
51
|
-
* route-registration lib). Answers "who implements WarmupApi, and where did that come from?",
|
|
52
|
-
* which previously required walking the dependsOn closure by hand.
|
|
53
|
-
*/
|
|
54
|
-
implementsVia?: Record<string, string>;
|
|
55
|
-
uses: string[];
|
|
56
|
-
dependsOn: string[];
|
|
57
|
-
/**
|
|
58
|
-
* When false, this service is hidden from the rendered runtime graph (its
|
|
59
|
-
* node AND every edge touching it are omitted from the HTML/DOT). It stays
|
|
60
|
-
* in runtime-dependencies.json so the data view is complete. Absent means
|
|
61
|
-
* drawn (the default). Mirrors GraphEntry.drawOnGraph from the `drawOnGraph:`
|
|
62
|
-
* nx tag.
|
|
63
|
-
*/
|
|
64
|
-
drawOnGraph?: boolean;
|
|
65
|
-
}
|
|
66
|
-
export interface RuntimeApi {
|
|
67
|
-
implementedBy: string[];
|
|
68
|
-
usedBy: string[];
|
|
69
|
-
/** Transport of this API — 'rpc' (direct call) or 'pubsub' (delivered through a queue). */
|
|
70
|
-
type?: ApiTransport;
|
|
71
|
-
/**
|
|
72
|
-
* The api-lib project that OWNS this contract. For a contract nothing in-repo implements, this
|
|
73
|
-
* is the external library the calls leave the repo through (`lib-firestore`, `lib-gmail`), which
|
|
74
|
-
* is what the runtime viz labels its terminal external nodes with.
|
|
75
|
-
*/
|
|
76
|
-
owner?: string;
|
|
77
|
-
}
|
|
78
|
-
export interface RuntimeEdge {
|
|
79
|
-
from: string;
|
|
80
|
-
to: string;
|
|
81
|
-
via: string[];
|
|
82
|
-
/**
|
|
83
|
-
* Transport of this edge. 'rpc' → a direct call arrow. 'pubsub' → the producer enqueues and the
|
|
84
|
-
* consumer is delivered later, so the runtime viz draws it as producer → QUEUE → consumer.
|
|
85
|
-
* Edges are split by transport, so every edge is a single kind.
|
|
86
|
-
*/
|
|
87
|
-
type?: ApiTransport;
|
|
88
|
-
}
|
|
89
|
-
export interface RuntimeUnresolved {
|
|
90
|
-
service: string;
|
|
91
|
-
api: string;
|
|
92
|
-
}
|
|
93
|
-
export interface RuntimeGraph {
|
|
94
|
-
services: Record<string, RuntimeService>;
|
|
95
|
-
apis: Record<string, RuntimeApi>;
|
|
96
|
-
runtimeEdges: RuntimeEdge[];
|
|
97
|
-
unresolvedUses: RuntimeUnresolved[];
|
|
98
|
-
}
|
|
31
|
+
import type { ApiContracts } from './api-usage/api-relations';
|
|
32
|
+
import type { RuntimeGraph } from './runtime-graph-model';
|
|
33
|
+
export type { RuntimeApi, RuntimeEdge, RuntimeGraph, RuntimeQueue, RuntimeService, RuntimeTrigger, RuntimeUnresolved, } from './runtime-graph-model';
|
|
34
|
+
export { DEFAULT_RUNTIME_GRAPH_PATH, saveRuntimeGraph, runtimeGraphFileExists, loadRuntimeGraph, serializeRuntimeGraph, } from './runtime-graph-io';
|
|
99
35
|
/**
|
|
100
36
|
* The derived graph PLUS everything the derivation had to guess at. `warnings` is deliberately not
|
|
101
37
|
* part of RuntimeGraph: it is not committed data, it is the report that stops a guessed edge from
|
|
@@ -133,16 +69,11 @@ export declare function runtimeAdjacency(graph: RuntimeGraph): Record<string, st
|
|
|
133
69
|
* (drawOnGraph:false, defaults to none) are kept in the graph but flagged so the
|
|
134
70
|
* runtime visualizer omits their nodes + edges.
|
|
135
71
|
*/
|
|
136
|
-
export declare function deriveRuntimeGraph(projects: EnhancedGraph, hiddenProjects?: Set<string
|
|
72
|
+
export declare function deriveRuntimeGraph(projects: EnhancedGraph, hiddenProjects?: Set<string>, apiContracts?: ApiContracts): RuntimeGraph;
|
|
137
73
|
/**
|
|
138
74
|
* The same derivation, plus the warnings it produced (every edge it had to GUESS at). Executors use
|
|
139
75
|
* this form and print the warnings; `deriveRuntimeGraph` is the convenience form for callers that
|
|
140
76
|
* only want the data. The warnings are deliberately kept OUT of runtime-dependencies.json — a graph
|
|
141
77
|
* file that records its own doubts would just get committed and stop being read.
|
|
142
78
|
*/
|
|
143
|
-
export declare function deriveRuntimeGraphReport(projects: EnhancedGraph, hiddenProjects?: Set<string
|
|
144
|
-
export declare function saveRuntimeGraph(graph: RuntimeGraph, workspaceRoot: string, graphPath?: string): void;
|
|
145
|
-
export declare function runtimeGraphFileExists(workspaceRoot: string, graphPath?: string): boolean;
|
|
146
|
-
export declare function loadRuntimeGraph(workspaceRoot: string, graphPath?: string): RuntimeGraph | null;
|
|
147
|
-
/** Serialize for an in-memory equality check (matches the on-disk format). */
|
|
148
|
-
export declare function serializeRuntimeGraph(graph: RuntimeGraph): string;
|
|
79
|
+
export declare function deriveRuntimeGraphReport(projects: EnhancedGraph, hiddenProjects?: Set<string>, apiContracts?: ApiContracts): RuntimeGraphReport;
|