@webpieces/nx-webpieces-rules 0.4.831 → 0.4.833

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/package.json +8 -8
  2. package/src/executors/generate/executor.js +2 -0
  3. package/src/executors/generate/executor.js.map +1 -1
  4. package/src/executors/openapi-components-generate/executor.d.ts +2 -1
  5. package/src/executors/openapi-components-generate/executor.js +5 -2
  6. package/src/executors/openapi-components-generate/executor.js.map +1 -1
  7. package/src/executors/openapi-generate/executor.d.ts +15 -1
  8. package/src/executors/openapi-generate/executor.js +30 -2
  9. package/src/executors/openapi-generate/executor.js.map +1 -1
  10. package/src/executors/validate-api-lib-tag/executor.d.ts +5 -3
  11. package/src/executors/validate-api-lib-tag/executor.js +7 -5
  12. package/src/executors/validate-api-lib-tag/executor.js.map +1 -1
  13. package/src/executors/validate-architecture-unchanged/executor.js +2 -0
  14. package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
  15. package/src/lib/api-docs/components-wiring.d.ts +6 -0
  16. package/src/lib/api-docs/components-wiring.js +18 -1
  17. package/src/lib/api-docs/components-wiring.js.map +1 -1
  18. package/src/lib/api-usage/api-doc-rules-scan.d.ts +17 -1
  19. package/src/lib/api-usage/api-doc-rules-scan.js +28 -3
  20. package/src/lib/api-usage/api-doc-rules-scan.js.map +1 -1
  21. package/src/lib/api-usage/api-lib-tag-validator.d.ts +50 -14
  22. package/src/lib/api-usage/api-lib-tag-validator.js +189 -22
  23. package/src/lib/api-usage/api-lib-tag-validator.js.map +1 -1
  24. package/src/lib/api-usage/api-scanner.d.ts +3 -1
  25. package/src/lib/api-usage/api-scanner.js +6 -3
  26. package/src/lib/api-usage/api-scanner.js.map +1 -1
  27. package/src/lib/api-usage/wire-closure.d.ts +49 -0
  28. package/src/lib/api-usage/wire-closure.js +126 -0
  29. package/src/lib/api-usage/wire-closure.js.map +1 -0
  30. package/src/lib/di-graph/analyzer-strategy.d.ts +1 -0
  31. package/src/lib/di-graph/analyzer-strategy.js +3 -0
  32. package/src/lib/di-graph/analyzer-strategy.js.map +1 -1
  33. package/src/lib/framework-resolver.d.ts +1 -1
  34. package/src/lib/framework-resolver.js +5 -2
  35. package/src/lib/framework-resolver.js.map +1 -1
  36. package/src/lib/graph-metadata.d.ts +9 -2
  37. package/src/lib/graph-metadata.js +12 -3
  38. package/src/lib/graph-metadata.js.map +1 -1
  39. package/src/lib/graph-visualizer.js +6 -0
  40. package/src/lib/graph-visualizer.js.map +1 -1
  41. package/src/lib/role-resolver.d.ts +9 -3
  42. package/src/lib/role-resolver.js +12 -4
  43. package/src/lib/role-resolver.js.map +1 -1
  44. package/src/lib/tag-truth.d.ts +106 -0
  45. package/src/lib/tag-truth.js +323 -0
  46. package/src/lib/tag-truth.js.map +1 -0
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The WIRE CLOSURE of a contract (#1064, D4): every named type an `@ApiPath` contract reaches must be
3
+ * DECLARED in a `role:api-lib` project, and must end in a suffix that project's `required-type-suffix`
4
+ * entry demands.
5
+ *
6
+ * Why it lives here and not in the dependency rule: `api-lib-dependencies` only sees project EDGES. A
7
+ * relative-path import into another library, or a DTO library that itself reaches outside, puts a type
8
+ * on the wire without any edge the graph rule could refuse. The extractor already follows every type a
9
+ * contract reaches through the compiler — the same walk `api-rules-for-openapi` / `api-rules-for-mcp`
10
+ * run, with no generate tag needed — so this is one more question asked of each type that walk found.
11
+ *
12
+ * Every api-lib rule before this one (`required-type-suffix`, `one-enum-spelling-in-api-lib`,
13
+ * `no-utility-types-in-api-lib`, `no-inline-import-in-api-lib`) reads only source files INSIDE the api
14
+ * library. A type declared in another package was, by construction, outside every one of them — which is
15
+ * how `AiProvider` and the `BrowserTelemetry` trio reached the wire from a general library unchecked.
16
+ *
17
+ * A type declared OUTSIDE the workspace (a published package under node_modules) is not judged here:
18
+ * its own package owns its schema, and the generator's fail-closed components check covers it.
19
+ */
20
+ import { DocumentedType } from '@webpieces/api-doc-model';
21
+ import { RequiredTypeSuffixEntry } from '@webpieces/rules-config';
22
+ import { ProjectInfo } from '../project-info';
23
+ /** The `required-type-suffix` entries the wire closure enforces; empty when that rule is OFF. */
24
+ export declare class WireClosureRule {
25
+ readonly suffixEntries: readonly RequiredTypeSuffixEntry[];
26
+ constructor(suffixEntries: readonly RequiredTypeSuffixEntry[]);
27
+ /** Only the role check — what a unit test or a repo with `required-type-suffix` OFF gets. */
28
+ static withoutSuffixes(): WireClosureRule;
29
+ /** `required-type-suffix`'s entries, unless that rule is OFF or time-boxed / branch-scoped off. */
30
+ static fromConfig(workspaceRoot: string): WireClosureRule;
31
+ }
32
+ /** ONE wire-closure finding: what is wrong with a reached type, and what to do. */
33
+ export declare class WireClosureVerdict {
34
+ readonly what: string;
35
+ readonly cure: string;
36
+ constructor(what: string, cure: string);
37
+ }
38
+ /** Judges every type a contract reaches against the api-lib role and the suffix rule. */
39
+ export declare class WireClosure {
40
+ private readonly workspaceRoot;
41
+ private readonly rule;
42
+ private readonly roots;
43
+ private readonly picker;
44
+ constructor(workspaceRoot: string, projectInfos: Map<string, ProjectInfo>, rule: WireClosureRule);
45
+ /** Every verdict for ONE reached type, declared in the file `absFile`. */
46
+ judge(type: DocumentedType, absFile: string): WireClosureVerdict[];
47
+ private endsInSuffix;
48
+ private ownerOf;
49
+ }
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ /**
3
+ * The WIRE CLOSURE of a contract (#1064, D4): every named type an `@ApiPath` contract reaches must be
4
+ * DECLARED in a `role:api-lib` project, and must end in a suffix that project's `required-type-suffix`
5
+ * entry demands.
6
+ *
7
+ * Why it lives here and not in the dependency rule: `api-lib-dependencies` only sees project EDGES. A
8
+ * relative-path import into another library, or a DTO library that itself reaches outside, puts a type
9
+ * on the wire without any edge the graph rule could refuse. The extractor already follows every type a
10
+ * contract reaches through the compiler — the same walk `api-rules-for-openapi` / `api-rules-for-mcp`
11
+ * run, with no generate tag needed — so this is one more question asked of each type that walk found.
12
+ *
13
+ * Every api-lib rule before this one (`required-type-suffix`, `one-enum-spelling-in-api-lib`,
14
+ * `no-utility-types-in-api-lib`, `no-inline-import-in-api-lib`) reads only source files INSIDE the api
15
+ * library. A type declared in another package was, by construction, outside every one of them — which is
16
+ * how `AiProvider` and the `BrowserTelemetry` trio reached the wire from a general library unchecked.
17
+ *
18
+ * A type declared OUTSIDE the workspace (a published package under node_modules) is not judged here:
19
+ * its own package owns its schema, and the generator's fail-closed components check covers it.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.WireClosure = exports.WireClosureVerdict = exports.WireClosureRule = void 0;
23
+ const tslib_1 = require("tslib");
24
+ const path = tslib_1.__importStar(require("path"));
25
+ const code_rules_1 = require("@webpieces/code-rules");
26
+ const rules_config_1 = require("@webpieces/rules-config");
27
+ const role_resolver_1 = require("../role-resolver");
28
+ const rule_gate_1 = require("../rule-gate");
29
+ const API_LIB_ROLE = 'api-lib';
30
+ /** The `required-type-suffix` entries the wire closure enforces; empty when that rule is OFF. */
31
+ class WireClosureRule {
32
+ suffixEntries;
33
+ constructor(suffixEntries) {
34
+ this.suffixEntries = suffixEntries;
35
+ }
36
+ /** Only the role check — what a unit test or a repo with `required-type-suffix` OFF gets. */
37
+ // webpieces-disable no-function-outside-class -- static factory of this class
38
+ static withoutSuffixes() {
39
+ return new WireClosureRule([]);
40
+ }
41
+ /** `required-type-suffix`'s entries, unless that rule is OFF or time-boxed / branch-scoped off. */
42
+ // webpieces-disable no-function-outside-class -- static factory of this class
43
+ static fromConfig(workspaceRoot) {
44
+ if (new rule_gate_1.RuleGate().skipReason(workspaceRoot, rules_config_1.RULE_NAMES.REQUIRED_TYPE_SUFFIX, true) !== null) {
45
+ return WireClosureRule.withoutSuffixes();
46
+ }
47
+ const rule = (0, rules_config_1.loadAndValidate)(workspaceRoot).resolved.rules.get(rules_config_1.RULE_NAMES.REQUIRED_TYPE_SUFFIX);
48
+ const entries = rule?.options['entries'];
49
+ return new WireClosureRule(Array.isArray(entries) ? entries : []);
50
+ }
51
+ }
52
+ exports.WireClosureRule = WireClosureRule;
53
+ /** ONE wire-closure finding: what is wrong with a reached type, and what to do. */
54
+ class WireClosureVerdict {
55
+ what;
56
+ cure;
57
+ constructor(what, cure) {
58
+ this.what = what;
59
+ this.cure = cure;
60
+ }
61
+ }
62
+ exports.WireClosureVerdict = WireClosureVerdict;
63
+ /** A workspace project root, absolute, for the longest-prefix owner lookup. */
64
+ class OwnedRoot {
65
+ info;
66
+ abs;
67
+ constructor(info, abs) {
68
+ this.info = info;
69
+ this.abs = abs;
70
+ }
71
+ }
72
+ /** Judges every type a contract reaches against the api-lib role and the suffix rule. */
73
+ class WireClosure {
74
+ workspaceRoot;
75
+ rule;
76
+ roots;
77
+ picker = new code_rules_1.SuffixEntryPicker();
78
+ constructor(workspaceRoot, projectInfos, rule) {
79
+ this.workspaceRoot = workspaceRoot;
80
+ this.rule = rule;
81
+ this.roots = [...projectInfos.values()]
82
+ .filter((info) => info.root !== '' && info.root !== '.')
83
+ .map((info) => new OwnedRoot(info, path.resolve(workspaceRoot, info.root)))
84
+ // Longest root first so a nested project wins over its parent.
85
+ .sort((a, b) => b.abs.length - a.abs.length);
86
+ }
87
+ /** Every verdict for ONE reached type, declared in the file `absFile`. */
88
+ judge(type, absFile) {
89
+ const abs = path.resolve(this.workspaceRoot, absFile);
90
+ if (abs.split(path.sep).includes('node_modules'))
91
+ return [];
92
+ const owner = this.ownerOf(abs);
93
+ if (owner === null)
94
+ return [];
95
+ const relFile = path.relative(this.workspaceRoot, abs).split(path.sep).join('/');
96
+ const role = (0, role_resolver_1.resolveRole)(owner).role ?? 'none';
97
+ const declared = `${type.packageName ?? 'no package'} (project '${owner.name}', role:${role})`;
98
+ const verdicts = [];
99
+ if (role !== API_LIB_ROLE) {
100
+ verdicts.push(new WireClosureVerdict(`'${type.name}' goes over the wire but is declared in ${declared}, which is not a role:api-lib ` +
101
+ 'project — every type a contract reaches must be declared in an api library', `Move '${type.name}' into a role:api-lib project — as a '…Dto' string enum when it is a ` +
102
+ `string-literal union. Retagging '${owner.name}' is not a way out unless it holds only ` +
103
+ 'contracts and wire types (validate-api-lib-tag refuses a role:api-lib with an implementation).'));
104
+ }
105
+ const governing = this.picker.winner(relFile, this.rule.suffixEntries);
106
+ if (governing !== undefined && !this.endsInSuffix(type.name, governing.entry.suffixes)) {
107
+ verdicts.push(new WireClosureVerdict(`'${type.name}' goes over the wire from ${relFile} but does not end in one of ` +
108
+ `[${governing.entry.suffixes.join(', ')}], which the required-type-suffix entry ` +
109
+ `'${governing.glob}' demands of it`, `Rename '${type.name}' to end in one of ${governing.entry.suffixes.join(', ')} — the suffix tells a ` +
110
+ 'reader which layer a type belongs to, and a wire type is the layer a partner reads.'));
111
+ }
112
+ return verdicts;
113
+ }
114
+ endsInSuffix(name, suffixes) {
115
+ return suffixes.some((suffix) => name.length > suffix.length && name.endsWith(suffix));
116
+ }
117
+ ownerOf(abs) {
118
+ for (const root of this.roots) {
119
+ if (abs === root.abs || abs.startsWith(root.abs + path.sep))
120
+ return root.info;
121
+ }
122
+ return null;
123
+ }
124
+ }
125
+ exports.WireClosure = WireClosure;
126
+ //# sourceMappingURL=wire-closure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wire-closure.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/wire-closure.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The WIRE CLOSURE of a contract (#1064, D4): every named type an `@ApiPath` contract reaches must be\n * DECLARED in a `role:api-lib` project, and must end in a suffix that project's `required-type-suffix`\n * entry demands.\n *\n * Why it lives here and not in the dependency rule: `api-lib-dependencies` only sees project EDGES. A\n * relative-path import into another library, or a DTO library that itself reaches outside, puts a type\n * on the wire without any edge the graph rule could refuse. The extractor already follows every type a\n * contract reaches through the compiler — the same walk `api-rules-for-openapi` / `api-rules-for-mcp`\n * run, with no generate tag needed — so this is one more question asked of each type that walk found.\n *\n * Every api-lib rule before this one (`required-type-suffix`, `one-enum-spelling-in-api-lib`,\n * `no-utility-types-in-api-lib`, `no-inline-import-in-api-lib`) reads only source files INSIDE the api\n * library. A type declared in another package was, by construction, outside every one of them — which is\n * how `AiProvider` and the `BrowserTelemetry` trio reached the wire from a general library unchecked.\n *\n * A type declared OUTSIDE the workspace (a published package under node_modules) is not judged here:\n * its own package owns its schema, and the generator's fail-closed components check covers it.\n */\n\nimport * as path from 'path';\nimport { DocumentedType } from '@webpieces/api-doc-model';\nimport { SuffixEntryPicker } from '@webpieces/code-rules';\nimport { loadAndValidate, RequiredTypeSuffixEntry, RULE_NAMES } from '@webpieces/rules-config';\nimport { ProjectInfo } from '../project-info';\nimport { resolveRole } from '../role-resolver';\nimport { RuleGate } from '../rule-gate';\n\nconst API_LIB_ROLE = 'api-lib';\n\n/** The `required-type-suffix` entries the wire closure enforces; empty when that rule is OFF. */\nexport class WireClosureRule {\n constructor(public readonly suffixEntries: readonly RequiredTypeSuffixEntry[]) {}\n\n /** Only the role check — what a unit test or a repo with `required-type-suffix` OFF gets. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static withoutSuffixes(): WireClosureRule {\n return new WireClosureRule([]);\n }\n\n /** `required-type-suffix`'s entries, unless that rule is OFF or time-boxed / branch-scoped off. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static fromConfig(workspaceRoot: string): WireClosureRule {\n if (new RuleGate().skipReason(workspaceRoot, RULE_NAMES.REQUIRED_TYPE_SUFFIX, true) !== null) {\n return WireClosureRule.withoutSuffixes();\n }\n const rule = loadAndValidate(workspaceRoot).resolved.rules.get(RULE_NAMES.REQUIRED_TYPE_SUFFIX);\n const entries = rule?.options['entries'];\n return new WireClosureRule(Array.isArray(entries) ? (entries as RequiredTypeSuffixEntry[]) : []);\n }\n}\n\n/** ONE wire-closure finding: what is wrong with a reached type, and what to do. */\nexport class WireClosureVerdict {\n constructor(\n public readonly what: string,\n public readonly cure: string,\n ) {}\n}\n\n/** A workspace project root, absolute, for the longest-prefix owner lookup. */\nclass OwnedRoot {\n constructor(\n public readonly info: ProjectInfo,\n public readonly abs: string,\n ) {}\n}\n\n/** Judges every type a contract reaches against the api-lib role and the suffix rule. */\nexport class WireClosure {\n private readonly roots: OwnedRoot[];\n private readonly picker = new SuffixEntryPicker();\n\n constructor(\n private readonly workspaceRoot: string,\n projectInfos: Map<string, ProjectInfo>,\n private readonly rule: WireClosureRule,\n ) {\n this.roots = [...projectInfos.values()]\n .filter((info: ProjectInfo) => info.root !== '' && info.root !== '.')\n .map((info: ProjectInfo) => new OwnedRoot(info, path.resolve(workspaceRoot, info.root)))\n // Longest root first so a nested project wins over its parent.\n .sort((a: OwnedRoot, b: OwnedRoot) => b.abs.length - a.abs.length);\n }\n\n /** Every verdict for ONE reached type, declared in the file `absFile`. */\n judge(type: DocumentedType, absFile: string): WireClosureVerdict[] {\n const abs = path.resolve(this.workspaceRoot, absFile);\n if (abs.split(path.sep).includes('node_modules')) return [];\n const owner = this.ownerOf(abs);\n if (owner === null) return [];\n const relFile = path.relative(this.workspaceRoot, abs).split(path.sep).join('/');\n const role = resolveRole(owner).role ?? 'none';\n const declared = `${type.packageName ?? 'no package'} (project '${owner.name}', role:${role})`;\n const verdicts: WireClosureVerdict[] = [];\n if (role !== API_LIB_ROLE) {\n verdicts.push(new WireClosureVerdict(\n `'${type.name}' goes over the wire but is declared in ${declared}, which is not a role:api-lib ` +\n 'project — every type a contract reaches must be declared in an api library',\n `Move '${type.name}' into a role:api-lib project — as a '…Dto' string enum when it is a ` +\n `string-literal union. Retagging '${owner.name}' is not a way out unless it holds only ` +\n 'contracts and wire types (validate-api-lib-tag refuses a role:api-lib with an implementation).',\n ));\n }\n const governing = this.picker.winner(relFile, this.rule.suffixEntries);\n if (governing !== undefined && !this.endsInSuffix(type.name, governing.entry.suffixes)) {\n verdicts.push(new WireClosureVerdict(\n `'${type.name}' goes over the wire from ${relFile} but does not end in one of ` +\n `[${governing.entry.suffixes.join(', ')}], which the required-type-suffix entry ` +\n `'${governing.glob}' demands of it`,\n `Rename '${type.name}' to end in one of ${governing.entry.suffixes.join(', ')} — the suffix tells a ` +\n 'reader which layer a type belongs to, and a wire type is the layer a partner reads.',\n ));\n }\n return verdicts;\n }\n\n private endsInSuffix(name: string, suffixes: readonly string[]): boolean {\n return suffixes.some((suffix: string) => name.length > suffix.length && name.endsWith(suffix));\n }\n\n private ownerOf(abs: string): ProjectInfo | null {\n for (const root of this.roots) {\n if (abs === root.abs || abs.startsWith(root.abs + path.sep)) return root.info;\n }\n return null;\n }\n}\n"]}
@@ -68,6 +68,7 @@ export declare function explicitRoleTag(tags: readonly string[]): string | null;
68
68
  * - `client` → Angular design for angular apps; otherwise skip
69
69
  * - `lib` → skip (plain libraries get no design)
70
70
  * - `api-lib` → skip (contract-only libraries get no design)
71
+ * - `api-client` → skip (a contract plus its one default SDK adapter; no design of its own)
71
72
  * - role absent → legacy framework/marker selection
72
73
  */
73
74
  export declare function selectAnalyzer(role: string | null, frameworks: string[], markers: FrameworkMarkers): DiAnalyzer;
@@ -113,6 +113,7 @@ function explicitRoleTag(tags) {
113
113
  * - `client` → Angular design for angular apps; otherwise skip
114
114
  * - `lib` → skip (plain libraries get no design)
115
115
  * - `api-lib` → skip (contract-only libraries get no design)
116
+ * - `api-client` → skip (a contract plus its one default SDK adapter; no design of its own)
116
117
  * - role absent → legacy framework/marker selection
117
118
  */
118
119
  function selectAnalyzer(role, frameworks, markers) {
@@ -130,6 +131,8 @@ function selectAnalyzer(role, frameworks, markers) {
130
131
  return new EmptyAnalyzer();
131
132
  if (role === 'api-lib')
132
133
  return new EmptyAnalyzer();
134
+ if (role === 'api-client')
135
+ return new EmptyAnalyzer();
133
136
  // Role tag absent — fall back to the legacy framework/marker selection so
134
137
  // designs stay identical until a project is retagged.
135
138
  if (frameworks.includes('express'))
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer-strategy.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/analyzer-strategy.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AA2EH,oDAEC;AAGD,sCASC;AAGD,0CAEC;AAoBD,wCAqBC;AApID,mCAAkC;AAClC,yCAAsD;AACtD,yDAAyD;AAEzD,MAAM,oBAAoB,GAAG,YAAY,CAAC;AAC1C,MAAM,eAAe,GAAG,OAAO,CAAC;AAEhC,sFAAsF;AACtF,MAAa,gBAAgB;IACzB,OAAO,CAAU;IACjB,UAAU,CAAU;IAEpB,YAAY,OAAgB,EAAE,UAAmB;QAC7C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IACjC,CAAC;CACJ;AARD,4CAQC;AAOD;;;;;GAKG;AACH,MAAa,iBAAiB;IACT,QAAQ,CAAa;IAEtC,YAAY,WAAuB,YAAY;QAC3C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC7B,CAAC;IAED,cAAc,CAAC,OAAmB,EAAE,aAAqB,EAAE,WAAmB,EAAE,WAAmB;QAC/F,OAAO,IAAA,uBAAY,EAAC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChG,CAAC;CACJ;AAVD,8CAUC;AAED,oEAAoE;AACpE,MAAa,eAAe;IACxB,cAAc,CAAC,OAAmB,EAAE,aAAqB,EAAE,WAAmB,EAAE,WAAmB;QAC/F,OAAO,IAAA,sCAAmB,EAAC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;IACjF,CAAC;CACJ;AAJD,0CAIC;AAED,+DAA+D;AAC/D,MAAa,aAAa;IACtB,cAAc,CACV,QAAoB,EACpB,cAAsB,EACtB,YAAoB,EACpB,WAAmB;QAEnB,OAAO,IAAI,eAAO,CAAC,WAAW,CAAC,CAAC;IACpC,CAAC;CACJ;AATD,sCASC;AAED,wFAAwF;AACxF,SAAS,WAAW,CAAC,IAAuB,EAAE,MAAc;IACxD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YACzB,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;YAC9C,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;gBAAE,OAAO,KAAK,CAAC;QACvC,CAAC;IACL,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,gGAAgG;AAChG,SAAgB,oBAAoB,CAAC,IAAuB;IACxD,OAAO,WAAW,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAC;AACnD,CAAC;AAED,+FAA+F;AAC/F,SAAgB,aAAa,CAAC,IAAuB;IACjD,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,GAAG,CAAC,UAAU,CAAC,oBAAoB,CAAC,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;YAC5D,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;gBAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7C,CAAC;IACL,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,qFAAqF;AACrF,SAAgB,eAAe,CAAC,IAAuB;IACnD,OAAO,WAAW,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC;AAC9C,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAgB,cAAc,CAC1B,IAAmB,EACnB,UAAoB,EACpB,OAAyB;IAEzB,IAAI,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAClE,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,IAAI,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAC/D,IAAI,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,aAAa,EAAE,CAAC;IAClD,IAAI,IAAI,KAAK,cAAc;QAAE,OAAO,IAAI,iBAAiB,CAAC,mBAAmB,CAAC,CAAC;IAC/E,IAAI,IAAI,KAAK,QAAQ;QAAE,OAAO,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,eAAe,EAAE,CAAC,CAAC,CAAC,IAAI,aAAa,EAAE,CAAC;IAC3G,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,IAAI,aAAa,EAAE,CAAC;IAC/C,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,IAAI,aAAa,EAAE,CAAC;IAEnD,0EAA0E;IAC1E,sDAAsD;IACtD,IAAI,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAC/E,IAAI,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,eAAe,EAAE,CAAC;IACjE,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,aAAa,EAAE,CAAC;IACtD,IAAI,OAAO,CAAC,OAAO;QAAE,OAAO,IAAI,eAAe,EAAE,CAAC;IAClD,IAAI,OAAO,CAAC,UAAU;QAAE,OAAO,IAAI,iBAAiB,CAAC,YAAY,CAAC,CAAC;IACnE,OAAO,IAAI,aAAa,EAAE,CAAC;AAC/B,CAAC","sourcesContent":["/**\n * DI Analyzer Strategy\n *\n * Picks which DI analyzer generates a project's design graph. The primary\n * driver is the project's `role` nx tag; the `framework` env set (its libType)\n * only distinguishes the client runtime by set membership:\n *\n * - env set includes `express` → {@link InversifyAnalyzer} (roots on `@DocumentDesign`)\n * - env set includes `angular` → {@link AngularAnalyzer} (roots on the bootstrap/route components)\n * - anything else (`react`, `browser`, `node`, ...) → {@link EmptyAnalyzer} (skip)\n *\n * The explicit tags are the source of truth. When the role tag is ABSENT (e.g.\n * before retag), a cheap marker pre-scan corroborates: `@Component(` /\n * `bootstrapApplication` → angular; `@DocumentDesign(` → Inversify. This makes the\n * committed design.* identical whether selection is tag- or marker-driven.\n */\n\nimport type * as ts from 'typescript';\nimport { DiGraph } from './model';\nimport { buildDiGraph, DiRootMode } from './analyzer';\nimport { buildAngularDiGraph } from './angular-analyzer';\n\nconst FRAMEWORK_TAG_PREFIX = 'framework:';\nconst ROLE_TAG_PREFIX = 'role:';\n\n/** Which framework a source tree's decorators point at (marker pre-scan fallback). */\nexport class FrameworkMarkers {\n angular: boolean;\n controller: boolean;\n\n constructor(angular: boolean, controller: boolean) {\n this.angular = angular;\n this.controller = controller;\n }\n}\n\n/** Statically analyzes one project's DI dependency DAG into a `DiGraph`. */\nexport interface DiAnalyzer {\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph;\n}\n\n/**\n * Inversify analyzer: one design per `@DocumentDesign` root. The `rootMode` only\n * sets the root box kind:\n * - `'controller'` (server projects) → `controller`.\n * - `'apiImplementation'` (role:designed-lib) → `apiImplementation`.\n */\nexport class InversifyAnalyzer implements DiAnalyzer {\n private readonly rootMode: DiRootMode;\n\n constructor(rootMode: DiRootMode = 'controller') {\n this.rootMode = rootMode;\n }\n\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph {\n return buildDiGraph(program, workspaceRoot, projectRoot, projectName, false, this.rootMode);\n }\n}\n\n/** Angular: one design per entry component (bootstrap + routed). */\nexport class AngularAnalyzer implements DiAnalyzer {\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph {\n return buildAngularDiGraph(program, workspaceRoot, projectRoot, projectName);\n }\n}\n\n/** Non-angular/non-express projects: an empty graph (skip). */\nexport class EmptyAnalyzer implements DiAnalyzer {\n analyzeProject(\n _program: ts.Program,\n _workspaceRoot: string,\n _projectRoot: string,\n projectName: string,\n ): DiGraph {\n return new DiGraph(projectName);\n }\n}\n\n/** Extract the explicit `<prefix><value>` nx tag, or null when the project has none. */\nfunction explicitTag(tags: readonly string[], prefix: string): string | null {\n for (const tag of tags) {\n if (tag.startsWith(prefix)) {\n const value = tag.slice(prefix.length).trim();\n if (value.length > 0) return value;\n }\n }\n return null;\n}\n\n/** Extract the FIRST explicit `framework:<value>` nx tag, or null when the project has none. */\nexport function explicitFrameworkTag(tags: readonly string[]): string | null {\n return explicitTag(tags, FRAMEWORK_TAG_PREFIX);\n}\n\n/** Extract every explicit `framework:<value>` nx tag as an env set (the project's libType). */\nexport function frameworkTags(tags: readonly string[]): string[] {\n const values: string[] = [];\n for (const tag of tags) {\n if (tag.startsWith(FRAMEWORK_TAG_PREFIX)) {\n const value = tag.slice(FRAMEWORK_TAG_PREFIX.length).trim();\n if (value.length > 0) values.push(value);\n }\n }\n return values;\n}\n\n/** Extract the explicit `role:<value>` nx tag, or null when the project has none. */\nexport function explicitRoleTag(tags: readonly string[]): string | null {\n return explicitTag(tags, ROLE_TAG_PREFIX);\n}\n\n/**\n * Choose the analyzer for a project. `role` (server|designed-lib|lib|client) is\n * the source of truth for WHAT to root on; `frameworks` is the project's env set\n * (its libType) and distinguishes the client runtime (angular vs other) by set\n * membership; `markers` corroborate only when the role tag is ABSENT (rollout\n * fallback keeps pre-retag designs identical).\n *\n * - `server` → Inversify, roots on `@DocumentDesign` (controller kind)\n * - `app` → Inversify, roots on `@DocumentDesign` (controller kind) — a runnable\n * (non-HTTP) application bootstrapped via `container.get(XxxApp)`, e.g. the\n * tooling packages (AiRulesApp, RulesApp). Drawn exactly like a server.\n * - `bundle` → skip (an aggregator of apps; no design of its own)\n * - `designed-lib` → Inversify, roots on `@DocumentDesign` (apiImplementation kind)\n * - `client` → Angular design for angular apps; otherwise skip\n * - `lib` → skip (plain libraries get no design)\n * - `api-lib` → skip (contract-only libraries get no design)\n * - role absent → legacy framework/marker selection\n */\nexport function selectAnalyzer(\n role: string | null,\n frameworks: string[],\n markers: FrameworkMarkers,\n): DiAnalyzer {\n if (role === 'server') return new InversifyAnalyzer('controller');\n if (role === 'app') return new InversifyAnalyzer('controller');\n if (role === 'bundle') return new EmptyAnalyzer();\n if (role === 'designed-lib') return new InversifyAnalyzer('apiImplementation');\n if (role === 'client') return frameworks.includes('angular') ? new AngularAnalyzer() : new EmptyAnalyzer();\n if (role === 'lib') return new EmptyAnalyzer();\n if (role === 'api-lib') return new EmptyAnalyzer();\n\n // Role tag absent — fall back to the legacy framework/marker selection so\n // designs stay identical until a project is retagged.\n if (frameworks.includes('express')) return new InversifyAnalyzer('controller');\n if (frameworks.includes('angular')) return new AngularAnalyzer();\n if (frameworks.length > 0) return new EmptyAnalyzer();\n if (markers.angular) return new AngularAnalyzer();\n if (markers.controller) return new InversifyAnalyzer('controller');\n return new EmptyAnalyzer();\n}\n"]}
1
+ {"version":3,"file":"analyzer-strategy.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/analyzer-strategy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Analyzer Strategy\n *\n * Picks which DI analyzer generates a project's design graph. The primary\n * driver is the project's `role` nx tag; the `framework` env set (its libType)\n * only distinguishes the client runtime by set membership:\n *\n * - env set includes `express` → {@link InversifyAnalyzer} (roots on `@DocumentDesign`)\n * - env set includes `angular` → {@link AngularAnalyzer} (roots on the bootstrap/route components)\n * - anything else (`react`, `browser`, `node`, ...) → {@link EmptyAnalyzer} (skip)\n *\n * The explicit tags are the source of truth. When the role tag is ABSENT (e.g.\n * before retag), a cheap marker pre-scan corroborates: `@Component(` /\n * `bootstrapApplication` → angular; `@DocumentDesign(` → Inversify. This makes the\n * committed design.* identical whether selection is tag- or marker-driven.\n */\n\nimport type * as ts from 'typescript';\nimport { DiGraph } from './model';\nimport { buildDiGraph, DiRootMode } from './analyzer';\nimport { buildAngularDiGraph } from './angular-analyzer';\n\nconst FRAMEWORK_TAG_PREFIX = 'framework:';\nconst ROLE_TAG_PREFIX = 'role:';\n\n/** Which framework a source tree's decorators point at (marker pre-scan fallback). */\nexport class FrameworkMarkers {\n angular: boolean;\n controller: boolean;\n\n constructor(angular: boolean, controller: boolean) {\n this.angular = angular;\n this.controller = controller;\n }\n}\n\n/** Statically analyzes one project's DI dependency DAG into a `DiGraph`. */\nexport interface DiAnalyzer {\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph;\n}\n\n/**\n * Inversify analyzer: one design per `@DocumentDesign` root. The `rootMode` only\n * sets the root box kind:\n * - `'controller'` (server projects) → `controller`.\n * - `'apiImplementation'` (role:designed-lib) → `apiImplementation`.\n */\nexport class InversifyAnalyzer implements DiAnalyzer {\n private readonly rootMode: DiRootMode;\n\n constructor(rootMode: DiRootMode = 'controller') {\n this.rootMode = rootMode;\n }\n\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph {\n return buildDiGraph(program, workspaceRoot, projectRoot, projectName, false, this.rootMode);\n }\n}\n\n/** Angular: one design per entry component (bootstrap + routed). */\nexport class AngularAnalyzer implements DiAnalyzer {\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph {\n return buildAngularDiGraph(program, workspaceRoot, projectRoot, projectName);\n }\n}\n\n/** Non-angular/non-express projects: an empty graph (skip). */\nexport class EmptyAnalyzer implements DiAnalyzer {\n analyzeProject(\n _program: ts.Program,\n _workspaceRoot: string,\n _projectRoot: string,\n projectName: string,\n ): DiGraph {\n return new DiGraph(projectName);\n }\n}\n\n/** Extract the explicit `<prefix><value>` nx tag, or null when the project has none. */\nfunction explicitTag(tags: readonly string[], prefix: string): string | null {\n for (const tag of tags) {\n if (tag.startsWith(prefix)) {\n const value = tag.slice(prefix.length).trim();\n if (value.length > 0) return value;\n }\n }\n return null;\n}\n\n/** Extract the FIRST explicit `framework:<value>` nx tag, or null when the project has none. */\nexport function explicitFrameworkTag(tags: readonly string[]): string | null {\n return explicitTag(tags, FRAMEWORK_TAG_PREFIX);\n}\n\n/** Extract every explicit `framework:<value>` nx tag as an env set (the project's libType). */\nexport function frameworkTags(tags: readonly string[]): string[] {\n const values: string[] = [];\n for (const tag of tags) {\n if (tag.startsWith(FRAMEWORK_TAG_PREFIX)) {\n const value = tag.slice(FRAMEWORK_TAG_PREFIX.length).trim();\n if (value.length > 0) values.push(value);\n }\n }\n return values;\n}\n\n/** Extract the explicit `role:<value>` nx tag, or null when the project has none. */\nexport function explicitRoleTag(tags: readonly string[]): string | null {\n return explicitTag(tags, ROLE_TAG_PREFIX);\n}\n\n/**\n * Choose the analyzer for a project. `role` (server|designed-lib|lib|client) is\n * the source of truth for WHAT to root on; `frameworks` is the project's env set\n * (its libType) and distinguishes the client runtime (angular vs other) by set\n * membership; `markers` corroborate only when the role tag is ABSENT (rollout\n * fallback keeps pre-retag designs identical).\n *\n * - `server` → Inversify, roots on `@DocumentDesign` (controller kind)\n * - `app` → Inversify, roots on `@DocumentDesign` (controller kind) — a runnable\n * (non-HTTP) application bootstrapped via `container.get(XxxApp)`, e.g. the\n * tooling packages (AiRulesApp, RulesApp). Drawn exactly like a server.\n * - `bundle` → skip (an aggregator of apps; no design of its own)\n * - `designed-lib` → Inversify, roots on `@DocumentDesign` (apiImplementation kind)\n * - `client` → Angular design for angular apps; otherwise skip\n * - `lib` → skip (plain libraries get no design)\n * - `api-lib` → skip (contract-only libraries get no design)\n * - `api-client` → skip (a contract plus its one default SDK adapter; no design of its own)\n * - role absent → legacy framework/marker selection\n */\nexport function selectAnalyzer(\n role: string | null,\n frameworks: string[],\n markers: FrameworkMarkers,\n): DiAnalyzer {\n if (role === 'server') return new InversifyAnalyzer('controller');\n if (role === 'app') return new InversifyAnalyzer('controller');\n if (role === 'bundle') return new EmptyAnalyzer();\n if (role === 'designed-lib') return new InversifyAnalyzer('apiImplementation');\n if (role === 'client') return frameworks.includes('angular') ? new AngularAnalyzer() : new EmptyAnalyzer();\n if (role === 'lib') return new EmptyAnalyzer();\n if (role === 'api-lib') return new EmptyAnalyzer();\n if (role === 'api-client') return new EmptyAnalyzer();\n\n // Role tag absent — fall back to the legacy framework/marker selection so\n // designs stay identical until a project is retagged.\n if (frameworks.includes('express')) return new InversifyAnalyzer('controller');\n if (frameworks.includes('angular')) return new AngularAnalyzer();\n if (frameworks.length > 0) return new EmptyAnalyzer();\n if (markers.angular) return new AngularAnalyzer();\n if (markers.controller) return new InversifyAnalyzer('controller');\n return new EmptyAnalyzer();\n}\n"]}
@@ -6,7 +6,7 @@
6
6
  * architecture/dependencies.json. A project carries a MULTI-VALUE env set drawn
7
7
  * from the atomic values:
8
8
  *
9
- * browser · react · angular · node · express
9
+ * browser · react · angular · node · express · react-native
10
10
  *
11
11
  * A project lists every environment it promises to run in, e.g.
12
12
  * `framework:browser` + `framework:node`. This is the field the
@@ -7,7 +7,7 @@
7
7
  * architecture/dependencies.json. A project carries a MULTI-VALUE env set drawn
8
8
  * from the atomic values:
9
9
  *
10
- * browser · react · angular · node · express
10
+ * browser · react · angular · node · express · react-native
11
11
  *
12
12
  * A project lists every environment it promises to run in, e.g.
13
13
  * `framework:browser` + `framework:node`. This is the field the
@@ -45,10 +45,13 @@ class FrameworkMarker {
45
45
  }
46
46
  }
47
47
  /**
48
- * Dependency name → framework, checked in order (first match wins).
48
+ * Dependency name → framework, checked in order (first match wins). `react-native` comes BEFORE
49
+ * `react` because every react-native project also depends on `react`, and inferring `react` would
50
+ * place it under browser in the lattice — the exact mistake #1064 (D6) removes.
49
51
  */
50
52
  const FRAMEWORK_DEPENDENCY_MARKERS = [
51
53
  new FrameworkMarker('@angular/core', 'angular'),
54
+ new FrameworkMarker('react-native', 'react-native'),
52
55
  new FrameworkMarker('react', 'react'),
53
56
  new FrameworkMarker('express', 'express'),
54
57
  ];
@@ -1 +1 @@
1
- {"version":3,"file":"framework-resolver.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/framework-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Framework Resolver\n *\n * Determines the `framework` field (a project's \"libType\" — the SET of runtime\n * environments it is validated to run in) written per project into\n * architecture/dependencies.json. A project carries a MULTI-VALUE env set drawn\n * from the atomic values:\n *\n * browser · react · angular · node · express\n *\n * A project lists every environment it promises to run in, e.g.\n * `framework:browser` + `framework:node`. This is the field the\n * `library-types-match-client` rule reads (via the up-set lattice) to keep an\n * express project from depending on a browser-only lib (and vice-versa). The\n * legacy single-value `all` \"usable by any side\" bucket is REMOVED — a project\n * must declare its actual env set.\n *\n * Resolution order:\n * 1. Explicit nx tags `framework:<value>` on the project (project.json tags) —\n * any value is allowed so new frameworks need no code change here, and MANY\n * are allowed (the env set). This is the source of truth; every project\n * should carry at least one.\n * 2. Inference from the project's package.json dependencies (a single-element\n * set fallback).\n * 3. FAILURE: no tag and nothing inferable is a problem, never a silent 'all'.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { ProjectInfo } from './project-info';\nimport { toError } from '../toError';\n\nexport const FRAMEWORK_TAG_PREFIX = 'framework:';\n\n/**\n * A package.json dependency name that identifies a framework.\n */\nclass FrameworkMarker {\n constructor(\n public readonly dependency: string,\n public readonly framework: string\n ) {}\n}\n\n/**\n * Dependency name → framework, checked in order (first match wins).\n */\nconst FRAMEWORK_DEPENDENCY_MARKERS: ReadonlyArray<FrameworkMarker> = [\n new FrameworkMarker('@angular/core', 'angular'),\n new FrameworkMarker('react', 'react'),\n new FrameworkMarker('express', 'express'),\n];\n\nexport class FrameworkResolution {\n constructor(\n /** Resolved env set (never empty), or null when resolution failed. */\n public readonly frameworks: string[] | null,\n /** Problem description when resolution failed, otherwise null */\n public readonly problem: string | null\n ) {}\n}\n\nexport function resolveFramework(info: ProjectInfo, workspaceRoot: string): FrameworkResolution {\n const tagValues = info.tags\n .filter((tag: string) => tag.startsWith(FRAMEWORK_TAG_PREFIX))\n .map((tag: string) => tag.slice(FRAMEWORK_TAG_PREFIX.length).trim());\n\n if (tagValues.length >= 1) {\n if (tagValues.some((value: string) => value.length === 0)) {\n return new FrameworkResolution(null, `${info.name}: 'framework:' tag has an empty value`);\n }\n // Preserve declaration order, de-duplicating repeated envs.\n return new FrameworkResolution(Array.from(new Set(tagValues)), null);\n }\n\n const inferred = inferFromPackageJson(info, workspaceRoot);\n if (inferred === null) {\n return new FrameworkResolution(\n null,\n `${info.name}: no 'framework:' tag and no framework inferable from package.json — declare the ` +\n `env set this project runs in, e.g. framework:browser + framework:node`\n );\n }\n return new FrameworkResolution([inferred], null);\n}\n\nfunction inferFromPackageJson(info: ProjectInfo, workspaceRoot: string): string | null {\n const pkgJsonPath = path.join(workspaceRoot, info.root, 'package.json');\n if (!fs.existsSync(pkgJsonPath)) {\n return null;\n }\n\n let allDeps: Record<string, string>;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));\n allDeps = {\n ...(pkgJson.dependencies ?? {}),\n ...(pkgJson.devDependencies ?? {}),\n ...(pkgJson.peerDependencies ?? {}),\n };\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`Failed to parse ${pkgJsonPath} while inferring framework for ${info.name}`, {\n cause: error,\n });\n }\n\n for (const marker of FRAMEWORK_DEPENDENCY_MARKERS) {\n if (marker.dependency in allDeps) {\n return marker.framework;\n }\n }\n return null;\n}\n"]}
1
+ {"version":3,"file":"framework-resolver.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/framework-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Framework Resolver\n *\n * Determines the `framework` field (a project's \"libType\" — the SET of runtime\n * environments it is validated to run in) written per project into\n * architecture/dependencies.json. A project carries a MULTI-VALUE env set drawn\n * from the atomic values:\n *\n * browser · react · angular · node · express · react-native\n *\n * A project lists every environment it promises to run in, e.g.\n * `framework:browser` + `framework:node`. This is the field the\n * `library-types-match-client` rule reads (via the up-set lattice) to keep an\n * express project from depending on a browser-only lib (and vice-versa). The\n * legacy single-value `all` \"usable by any side\" bucket is REMOVED — a project\n * must declare its actual env set.\n *\n * Resolution order:\n * 1. Explicit nx tags `framework:<value>` on the project (project.json tags) —\n * any value is allowed so new frameworks need no code change here, and MANY\n * are allowed (the env set). This is the source of truth; every project\n * should carry at least one.\n * 2. Inference from the project's package.json dependencies (a single-element\n * set fallback).\n * 3. FAILURE: no tag and nothing inferable is a problem, never a silent 'all'.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { ProjectInfo } from './project-info';\nimport { toError } from '../toError';\n\nexport const FRAMEWORK_TAG_PREFIX = 'framework:';\n\n/**\n * A package.json dependency name that identifies a framework.\n */\nclass FrameworkMarker {\n constructor(\n public readonly dependency: string,\n public readonly framework: string\n ) {}\n}\n\n/**\n * Dependency name → framework, checked in order (first match wins). `react-native` comes BEFORE\n * `react` because every react-native project also depends on `react`, and inferring `react` would\n * place it under browser in the lattice — the exact mistake #1064 (D6) removes.\n */\nconst FRAMEWORK_DEPENDENCY_MARKERS: ReadonlyArray<FrameworkMarker> = [\n new FrameworkMarker('@angular/core', 'angular'),\n new FrameworkMarker('react-native', 'react-native'),\n new FrameworkMarker('react', 'react'),\n new FrameworkMarker('express', 'express'),\n];\n\nexport class FrameworkResolution {\n constructor(\n /** Resolved env set (never empty), or null when resolution failed. */\n public readonly frameworks: string[] | null,\n /** Problem description when resolution failed, otherwise null */\n public readonly problem: string | null\n ) {}\n}\n\nexport function resolveFramework(info: ProjectInfo, workspaceRoot: string): FrameworkResolution {\n const tagValues = info.tags\n .filter((tag: string) => tag.startsWith(FRAMEWORK_TAG_PREFIX))\n .map((tag: string) => tag.slice(FRAMEWORK_TAG_PREFIX.length).trim());\n\n if (tagValues.length >= 1) {\n if (tagValues.some((value: string) => value.length === 0)) {\n return new FrameworkResolution(null, `${info.name}: 'framework:' tag has an empty value`);\n }\n // Preserve declaration order, de-duplicating repeated envs.\n return new FrameworkResolution(Array.from(new Set(tagValues)), null);\n }\n\n const inferred = inferFromPackageJson(info, workspaceRoot);\n if (inferred === null) {\n return new FrameworkResolution(\n null,\n `${info.name}: no 'framework:' tag and no framework inferable from package.json — declare the ` +\n `env set this project runs in, e.g. framework:browser + framework:node`\n );\n }\n return new FrameworkResolution([inferred], null);\n}\n\nfunction inferFromPackageJson(info: ProjectInfo, workspaceRoot: string): string | null {\n const pkgJsonPath = path.join(workspaceRoot, info.root, 'package.json');\n if (!fs.existsSync(pkgJsonPath)) {\n return null;\n }\n\n let allDeps: Record<string, string>;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));\n allDeps = {\n ...(pkgJson.dependencies ?? {}),\n ...(pkgJson.devDependencies ?? {}),\n ...(pkgJson.peerDependencies ?? {}),\n };\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`Failed to parse ${pkgJsonPath} while inferring framework for ${info.name}`, {\n cause: error,\n });\n }\n\n for (const marker of FRAMEWORK_DEPENDENCY_MARKERS) {\n if (marker.dependency in allDeps) {\n return marker.framework;\n }\n }\n return null;\n}\n"]}
@@ -47,6 +47,11 @@ export declare const APP_ROLES: ReadonlyArray<string>;
47
47
  * PLUS every ancestor it can legally consume code from (specialization edges
48
48
  * child → parent: react → browser, angular → browser, express → node). A
49
49
  * consumer promising env `c` can be satisfied by any dependency env in `up(c)`.
50
+ *
51
+ * `react-native` is a first-class runtime and NOT a specialisation of browser (#1064, D6): it has no
52
+ * DOM, no IndexedDB and no OPFS, so a react-native consumer may only consume libraries that are
53
+ * themselves tagged `framework:react-native` (a universal browser+node+react-native library, a
54
+ * browser+react-native one, or a react-native-only one).
50
55
  */
51
56
  export declare const ENV_UP_SETS: Readonly<Record<string, ReadonlyArray<string>>>;
52
57
  /**
@@ -54,7 +59,7 @@ export declare const ENV_UP_SETS: Readonly<Record<string, ReadonlyArray<string>>
54
59
  *
55
60
  * A project's `framework` field is its libType — the SET of runtime
56
61
  * environments it is validated to run in (browser | react | angular | node |
57
- * express). For a dependency edge Consumer C → Library L, the edge is LEGAL iff
62
+ * express | react-native). For a dependency edge Consumer C → Library L, the edge is LEGAL iff
58
63
  * for EVERY env `c` in C's set, up(c) ∩ L's set ≠ ∅ — i.e. every environment
59
64
  * the consumer promises to run in can be satisfied by the dependency. This keeps
60
65
  * an express app from depending on a browser-only lib, and lets a `browser+node`
@@ -65,7 +70,9 @@ export declare function validateLibraryTypesMatch(graph: EnhancedGraph, problems
65
70
  /**
66
71
  * `role-dependency` rule.
67
72
  *
68
- * A project's `role` is its function (server | designed-lib | lib | client).
73
+ * A project's `role` is its function (server | app | bundle | designed-lib | lib | client |
74
+ * api-lib | api-client). The two api roles are libraries, never apps, so this rule leaves them alone;
75
+ * what THEY may depend on is `api-lib-dependencies` (tag-truth.ts).
69
76
  * Apps are terminal — libraries and clients consume them, never the reverse:
70
77
  * - a `client` is fully terminal: NOTHING may depend on it.
71
78
  * - a `server` may only be depended upon by another `server` — the one
@@ -159,6 +159,11 @@ exports.APP_ROLES = ['server', 'app', 'client'];
159
159
  * PLUS every ancestor it can legally consume code from (specialization edges
160
160
  * child → parent: react → browser, angular → browser, express → node). A
161
161
  * consumer promising env `c` can be satisfied by any dependency env in `up(c)`.
162
+ *
163
+ * `react-native` is a first-class runtime and NOT a specialisation of browser (#1064, D6): it has no
164
+ * DOM, no IndexedDB and no OPFS, so a react-native consumer may only consume libraries that are
165
+ * themselves tagged `framework:react-native` (a universal browser+node+react-native library, a
166
+ * browser+react-native one, or a react-native-only one).
162
167
  */
163
168
  exports.ENV_UP_SETS = {
164
169
  react: ['react', 'browser'],
@@ -166,6 +171,7 @@ exports.ENV_UP_SETS = {
166
171
  browser: ['browser'],
167
172
  express: ['express', 'node'],
168
173
  node: ['node'],
174
+ 'react-native': ['react-native'],
169
175
  };
170
176
  /** The up-set of an env (env itself + ancestors); unknown envs map to just themselves. */
171
177
  function upSet(env) {
@@ -176,7 +182,7 @@ function upSet(env) {
176
182
  *
177
183
  * A project's `framework` field is its libType — the SET of runtime
178
184
  * environments it is validated to run in (browser | react | angular | node |
179
- * express). For a dependency edge Consumer C → Library L, the edge is LEGAL iff
185
+ * express | react-native). For a dependency edge Consumer C → Library L, the edge is LEGAL iff
180
186
  * for EVERY env `c` in C's set, up(c) ∩ L's set ≠ ∅ — i.e. every environment
181
187
  * the consumer promises to run in can be satisfied by the dependency. This keeps
182
188
  * an express app from depending on a browser-only lib, and lets a `browser+node`
@@ -199,7 +205,8 @@ function validateLibraryTypesMatch(graph, problems) {
199
205
  problems.push(`library-types-match-client: '${projectName}' [${fromSet.join(', ')}] must not depend on ` +
200
206
  `'${dep}' [${toSet.join(', ')}] — the consumer env(s) ${unsatisfied.join(', ')} cannot be ` +
201
207
  `satisfied by the dependency (each consumer env must resolve to itself or an ancestor it ` +
202
- `consumes from: react/angular→browser, express→node). Widen '${dep}' framework tags or ` +
208
+ `consumes from: react/angular→browser, express→node, react-native→react-native only). ` +
209
+ `Widen '${dep}' framework tags or ` +
203
210
  `remove the dependency.`);
204
211
  }
205
212
  }
@@ -207,7 +214,9 @@ function validateLibraryTypesMatch(graph, problems) {
207
214
  /**
208
215
  * `role-dependency` rule.
209
216
  *
210
- * A project's `role` is its function (server | designed-lib | lib | client).
217
+ * A project's `role` is its function (server | app | bundle | designed-lib | lib | client |
218
+ * api-lib | api-client). The two api roles are libraries, never apps, so this rule leaves them alone;
219
+ * what THEY may depend on is `api-lib-dependencies` (tag-truth.ts).
211
220
  * Apps are terminal — libraries and clients consume them, never the reverse:
212
221
  * - a `client` is fully terminal: NOTHING may depend on it.
213
222
  * - a `server` may only be depended upon by another `server` — the one
@@ -1 +1 @@
1
- {"version":3,"file":"graph-metadata.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Metadata Enrichment\n *\n * Fills the AI-oriented fields on each architecture/dependencies.json entry:\n * framework — from `framework:<x>` nx tag or package.json inference\n * serviceName — from project.json metadata.webpieces.serviceName; the\n * name clients address this app by at runtime\n * shortDescription — first paragraph of the project's responsibilities.md\n * responsibilitiesFile — repo-relative path to the required responsibilities.md\n * designFile — repo-relative path to the generated DI design.json\n *\n * Validation is aggregated: ALL problems across ALL projects are collected and\n * thrown as one MetadataValidationError so a repo adopting this sees the full\n * seeding list in a single run. Callers must enrich BEFORE writing any file so\n * a failed run never clobbers dependencies.json.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { createProjectGraphAsync } from '@nx/devkit';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport { ProjectInfo } from './project-info';\nimport { resolveFramework } from './framework-resolver';\nimport { resolveRole } from './role-resolver';\nimport { resolveDrawOnGraph } from './draw-on-graph-resolver';\nimport { resolveCutLegacyCycles } from './cut-legacy-cycle-resolver';\nimport { resolveRuntimeParticipant } from './runtime-participant-resolver';\nimport { resolveCallsService, resolveServiceName, validateUniqueServiceNames } from './service-name-resolver';\nimport { extractShortDescription, validateShortDescription } from './responsibilities';\nimport { toError } from '../toError';\n\nexport const RESPONSIBILITIES_FILE_NAME = 'responsibilities.md';\n\n/**\n * Thrown when one or more projects fail metadata validation (missing/invalid\n * responsibilities.md, bad framework tags, ...). Executors catch this to point\n * AI at the webpieces.responsibilities.md instructions template.\n */\nexport class MetadataValidationError extends Error {\n constructor(public readonly problems: string[]) {\n super(\n `Architecture metadata validation failed (${problems.length} problem(s)):\\n` +\n problems.map((problem: string) => ` - ${problem}`).join('\\n')\n );\n this.name = 'MetadataValidationError';\n }\n}\n\n/**\n * Read per-project root + tags from nx's project graph.\n */\nexport async function collectProjectInfo(): Promise<Map<string, ProjectInfo>> {\n const projectGraph = await createProjectGraphAsync();\n const infos = new Map<string, ProjectInfo>();\n for (const [name, node] of Object.entries(projectGraph.nodes)) {\n infos.set(name, new ProjectInfo(name, node.data.root, node.data.tags ?? []));\n }\n return infos;\n}\n\n/**\n * Enrich every graph entry in place with framework, shortDescription,\n * responsibilitiesFile and designFile. Throws MetadataValidationError listing\n * every problem when any project fails validation.\n */\nexport function enrichGraph(\n graph: EnhancedGraph,\n infos: Map<string, ProjectInfo>,\n workspaceRoot: string\n): void {\n const problems: string[] = [];\n // project -> declared serviceName, collected so duplicates can be reported across the workspace.\n const serviceNames = new Map<string, string>();\n\n for (const [projectName, entry] of Object.entries(graph)) {\n const info = infos.get(projectName);\n if (!info) {\n problems.push(`${projectName}: not found in nx project graph`);\n continue;\n }\n\n enrichDeclarations(entry, info, workspaceRoot, problems);\n enrichClientNames(entry, info, workspaceRoot, projectName, serviceNames, problems);\n enrichResponsibilities(entry, info, workspaceRoot, problems);\n\n // Set designFile ONLY when the project has a REAL generated design (a\n // non-empty `designs[]`), i.e. it has a @DocumentDesign root. A project\n // without one has NO design.json at all (di-graph-generate stopped writing\n // empty ones and reaps stale ones), and legacy repos may still carry an\n // empty `{ designs: [] }` until their next build — both must read the same\n // way, and neither gets a \"View Design\" item on its node menu in the arch\n // viz (designLinks keys off designFile). See graph-visualizer.ts.\n if (hasGeneratedDesign(workspaceRoot, info.root)) {\n entry.designFile = toRepoRelative(info.root, 'design.json');\n }\n }\n\n validateUniqueServiceNames(serviceNames, problems);\n validateLibraryTypesMatch(graph, problems);\n validateRoleDependencies(graph, problems);\n\n if (problems.length > 0) {\n throw new MetadataValidationError(problems);\n }\n}\n\n/**\n * The fields a project DECLARES about itself — its framework env set and role (nx tags), the\n * webpieces runtime packages it depends on (package.json), and whether it opts out of the drawings\n * (nx tag). Grouped because they share a shape: resolve, record the problem, or write the field.\n *\n * Distinct from enrichResponsibilities/designFile, which read generated or hand-written FILES.\n */\n// webpieces-disable no-function-outside-class -- enrichGraph step helper, matching enrichClientNames/enrichResponsibilities in this file\nfunction enrichDeclarations(\n entry: GraphEntry,\n info: ProjectInfo,\n workspaceRoot: string,\n problems: string[]\n): void {\n const resolution = resolveFramework(info, workspaceRoot);\n if (resolution.problem !== null) {\n problems.push(resolution.problem);\n } else if (resolution.frameworks !== null) {\n entry.framework = resolution.frameworks;\n }\n\n const roleResolution = resolveRole(info);\n if (roleResolution.problem !== null) {\n problems.push(roleResolution.problem);\n } else if (roleResolution.role !== null) {\n entry.role = roleResolution.role;\n }\n\n // Only persist when the project declares at least one — a repo full of\n // \"webpiecesRuntime\": [] lines would be noise, and absence already means \"declares none\".\n const participantResolution = resolveRuntimeParticipant(info, workspaceRoot);\n if (participantResolution.problem !== null) {\n problems.push(participantResolution.problem);\n } else if (participantResolution.markers !== null && participantResolution.markers.length > 0) {\n entry.webpiecesRuntime = participantResolution.markers;\n }\n\n // Only persist the field when hidden (false); drawn projects (the\n // default) stay clean in dependencies.json with no drawOnGraph line.\n const drawResolution = resolveDrawOnGraph(info);\n if (drawResolution.problem !== null) {\n problems.push(drawResolution.problem);\n } else if (drawResolution.drawOnGraph === false) {\n entry.drawOnGraph = false;\n }\n\n // Only persist when the project actually cuts an edge — absence is the normal state, and a repo\n // full of \"cutLegacyCycle\": [] lines would bury the ones that mean something.\n const cutResolution = resolveCutLegacyCycles(info);\n if (cutResolution.problem !== null) {\n problems.push(cutResolution.problem);\n } else if (cutResolution.targets !== null && cutResolution.targets.length > 0) {\n entry.cutLegacyCycle = cutResolution.targets;\n }\n}\n\n/**\n * Roles that are terminal APPS — nothing may depend on them. A server, a\n * non-HTTP `app`, or a client is a top-level runnable; being depended upon means\n * it is really a library and should be retagged `role:lib`/`role:designed-lib`.\n */\nexport const APP_ROLES: ReadonlyArray<string> = ['server', 'app', 'client'];\n\n/**\n * Compatibility lattice — the \"up-set\" of each atomic env is the env itself\n * PLUS every ancestor it can legally consume code from (specialization edges\n * child → parent: react → browser, angular → browser, express → node). A\n * consumer promising env `c` can be satisfied by any dependency env in `up(c)`.\n */\nexport const ENV_UP_SETS: Readonly<Record<string, ReadonlyArray<string>>> = {\n react: ['react', 'browser'],\n angular: ['angular', 'browser'],\n browser: ['browser'],\n express: ['express', 'node'],\n node: ['node'],\n};\n\n/** The up-set of an env (env itself + ancestors); unknown envs map to just themselves. */\nfunction upSet(env: string): ReadonlyArray<string> {\n return ENV_UP_SETS[env] ?? [env];\n}\n\n/**\n * `library-types-match-client` rule.\n *\n * A project's `framework` field is its libType — the SET of runtime\n * environments it is validated to run in (browser | react | angular | node |\n * express). For a dependency edge Consumer C → Library L, the edge is LEGAL iff\n * for EVERY env `c` in C's set, up(c) ∩ L's set ≠ ∅ — i.e. every environment\n * the consumer promises to run in can be satisfied by the dependency. This keeps\n * an express app from depending on a browser-only lib, and lets a `browser+node`\n * lib be consumed by both react and express projects. Every violation is\n * appended to `problems` so `arch:generate` fails with the full list.\n */\nexport function validateLibraryTypesMatch(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromSet = entry.framework;\n if (fromSet === undefined) continue; // framework resolution already flagged this project\n\n for (const dep of entry.dependsOn) {\n const depEntry = graph[dep];\n const toSet = depEntry?.framework;\n if (toSet === undefined) continue;\n\n const unsatisfied = fromSet.filter(\n (env: string) => !upSet(env).some((up: string) => toSet.includes(up))\n );\n if (unsatisfied.length === 0) continue;\n\n problems.push(\n `library-types-match-client: '${projectName}' [${fromSet.join(', ')}] must not depend on ` +\n `'${dep}' [${toSet.join(', ')}] — the consumer env(s) ${unsatisfied.join(', ')} cannot be ` +\n `satisfied by the dependency (each consumer env must resolve to itself or an ancestor it ` +\n `consumes from: react/angular→browser, express→node). Widen '${dep}' framework tags or ` +\n `remove the dependency.`\n );\n }\n }\n}\n\n/**\n * `role-dependency` rule.\n *\n * A project's `role` is its function (server | designed-lib | lib | client).\n * Apps are terminal — libraries and clients consume them, never the reverse:\n * - a `client` is fully terminal: NOTHING may depend on it.\n * - a `server` may only be depended upon by another `server` — the one\n * legitimate case is a server-side orchestrator/e2e harness that boots\n * other servers. A `lib`/`designed-lib`/`client` depending on a `server`\n * inverts the dependency direction and is a violation.\n * - a `bundle` is the one role permitted to depend on ANY app: it aggregates\n * several apps into one distributable (e.g. an nx plugin re-exposing multiple\n * tooling apps), so a `bundle → app` edge is legitimate, not inverted.\n */\nexport function validateRoleDependencies(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromRole = entry.role;\n for (const dep of entry.dependsOn) {\n const toRole = graph[dep]?.role;\n if (toRole === undefined) continue; // role resolution already flagged this project\n if (!APP_ROLES.includes(toRole)) continue;\n // A server may orchestrate/boot other servers (e.g. an e2e harness).\n if (toRole === 'server' && fromRole === 'server') continue;\n // A bundle aggregates apps — it may depend on any app role.\n if (fromRole === 'bundle') continue;\n\n const why =\n toRole === 'client'\n ? `a 'client' app is terminal and may never be depended upon`\n : `a 'server' may only be depended upon by another 'server' (an orchestrator/e2e harness)`;\n problems.push(\n `role-dependency: '${projectName}' (role:${fromRole ?? 'none'}) must not depend on ` +\n `'${dep}' (role:${toRole}) — ${why}. Retag '${dep}' role:lib/role:designed-lib if it ` +\n `is actually a library, or remove the dependency.`\n );\n }\n }\n}\n\n/**\n * Fill the two symmetric client-addressing fields — `serviceName` (the name clients address THIS app\n * by) and `callsService` (the name THIS app's clients call when no literal ClientConfig sits at the\n * call site). Both are declared in project.json, never derived (see service-name-resolver.ts).\n */\n// webpieces-disable no-function-outside-class -- pure enrichment helper, mirrors enrichResponsibilities\nfunction enrichClientNames(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n projectName: string,\n serviceNames: Map<string, string>,\n problems: string[]\n): void {\n const serviceResolution = resolveServiceName(info, workspaceRoot);\n if (serviceResolution.problem !== null) {\n problems.push(serviceResolution.problem);\n } else if (serviceResolution.serviceName !== null) {\n entry.serviceName = serviceResolution.serviceName;\n serviceNames.set(projectName, serviceResolution.serviceName);\n }\n\n // Consumed by the runtime graph's target resolution (runtime-graph.ts), between the call-site\n // literal and the fan-out fallback.\n const callsResolution = resolveCallsService(info, workspaceRoot);\n if (callsResolution.problem !== null) {\n problems.push(callsResolution.problem);\n } else if (callsResolution.callsService !== null) {\n entry.callsService = callsResolution.callsService;\n }\n}\n\nfunction enrichResponsibilities(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n problems: string[]\n): void {\n const responsibilitiesFile = toRepoRelative(info.root, RESPONSIBILITIES_FILE_NAME);\n entry.responsibilitiesFile = responsibilitiesFile;\n\n const absolutePath = path.join(workspaceRoot, info.root, RESPONSIBILITIES_FILE_NAME);\n if (!fs.existsSync(absolutePath)) {\n problems.push(\n `${info.name}: missing required ${responsibilitiesFile} — create it with a heading, ` +\n `one short summary paragraph, then the full responsibilities of the module`\n );\n return;\n }\n\n const summary = extractShortDescription(fs.readFileSync(absolutePath, 'utf-8'));\n const summaryProblem = validateShortDescription(summary, responsibilitiesFile);\n if (summaryProblem !== null) {\n problems.push(`${info.name}: ${summaryProblem}`);\n return;\n }\n entry.shortDescription = summary;\n}\n\n/**\n * Repo-relative path with forward slashes (stable across platforms in the\n * committed JSON).\n */\nfunction toRepoRelative(projectRoot: string, fileName: string): string {\n return [projectRoot.replace(/\\\\/g, '/').replace(/\\/+$/, ''), fileName].join('/');\n}\n\n/**\n * True when the project has a REAL generated DI design — a committed design.json\n * whose `designs[]` is non-empty (i.e. it has ≥1 @DocumentDesign root).\n *\n * MISSING, empty-`designs[]`, and unparseable all read as \"no design\", and that\n * equivalence is load-bearing rather than incidental: di-graph-generate writes NO\n * file for a project without a design root (and reaps a stale one), so \"absent\"\n * is now the normal state for every plain lib and legacy service. Repos that have\n * not rebuilt since still carry `{ designs: [] }` files. Either way the box's node\n * menu must offer no \"View Design\" item. See design-file-emission.spec.ts.\n */\nfunction hasGeneratedDesign(workspaceRoot: string, projectRoot: string): boolean {\n const designPath = path.join(workspaceRoot, projectRoot, 'design.json');\n if (!fs.existsSync(designPath)) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const parsed = JSON.parse(fs.readFileSync(designPath, 'utf-8'));\n return Array.isArray(parsed.designs) && parsed.designs.length > 0;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Skipping unparseable ${designPath}: ${error.message}`);\n return false;\n }\n}\n"]}
1
+ {"version":3,"file":"graph-metadata.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Metadata Enrichment\n *\n * Fills the AI-oriented fields on each architecture/dependencies.json entry:\n * framework — from `framework:<x>` nx tag or package.json inference\n * serviceName — from project.json metadata.webpieces.serviceName; the\n * name clients address this app by at runtime\n * shortDescription — first paragraph of the project's responsibilities.md\n * responsibilitiesFile — repo-relative path to the required responsibilities.md\n * designFile — repo-relative path to the generated DI design.json\n *\n * Validation is aggregated: ALL problems across ALL projects are collected and\n * thrown as one MetadataValidationError so a repo adopting this sees the full\n * seeding list in a single run. Callers must enrich BEFORE writing any file so\n * a failed run never clobbers dependencies.json.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { createProjectGraphAsync } from '@nx/devkit';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport { ProjectInfo } from './project-info';\nimport { resolveFramework } from './framework-resolver';\nimport { resolveRole } from './role-resolver';\nimport { resolveDrawOnGraph } from './draw-on-graph-resolver';\nimport { resolveCutLegacyCycles } from './cut-legacy-cycle-resolver';\nimport { resolveRuntimeParticipant } from './runtime-participant-resolver';\nimport { resolveCallsService, resolveServiceName, validateUniqueServiceNames } from './service-name-resolver';\nimport { extractShortDescription, validateShortDescription } from './responsibilities';\nimport { toError } from '../toError';\n\nexport const RESPONSIBILITIES_FILE_NAME = 'responsibilities.md';\n\n/**\n * Thrown when one or more projects fail metadata validation (missing/invalid\n * responsibilities.md, bad framework tags, ...). Executors catch this to point\n * AI at the webpieces.responsibilities.md instructions template.\n */\nexport class MetadataValidationError extends Error {\n constructor(public readonly problems: string[]) {\n super(\n `Architecture metadata validation failed (${problems.length} problem(s)):\\n` +\n problems.map((problem: string) => ` - ${problem}`).join('\\n')\n );\n this.name = 'MetadataValidationError';\n }\n}\n\n/**\n * Read per-project root + tags from nx's project graph.\n */\nexport async function collectProjectInfo(): Promise<Map<string, ProjectInfo>> {\n const projectGraph = await createProjectGraphAsync();\n const infos = new Map<string, ProjectInfo>();\n for (const [name, node] of Object.entries(projectGraph.nodes)) {\n infos.set(name, new ProjectInfo(name, node.data.root, node.data.tags ?? []));\n }\n return infos;\n}\n\n/**\n * Enrich every graph entry in place with framework, shortDescription,\n * responsibilitiesFile and designFile. Throws MetadataValidationError listing\n * every problem when any project fails validation.\n */\nexport function enrichGraph(\n graph: EnhancedGraph,\n infos: Map<string, ProjectInfo>,\n workspaceRoot: string\n): void {\n const problems: string[] = [];\n // project -> declared serviceName, collected so duplicates can be reported across the workspace.\n const serviceNames = new Map<string, string>();\n\n for (const [projectName, entry] of Object.entries(graph)) {\n const info = infos.get(projectName);\n if (!info) {\n problems.push(`${projectName}: not found in nx project graph`);\n continue;\n }\n\n enrichDeclarations(entry, info, workspaceRoot, problems);\n enrichClientNames(entry, info, workspaceRoot, projectName, serviceNames, problems);\n enrichResponsibilities(entry, info, workspaceRoot, problems);\n\n // Set designFile ONLY when the project has a REAL generated design (a\n // non-empty `designs[]`), i.e. it has a @DocumentDesign root. A project\n // without one has NO design.json at all (di-graph-generate stopped writing\n // empty ones and reaps stale ones), and legacy repos may still carry an\n // empty `{ designs: [] }` until their next build — both must read the same\n // way, and neither gets a \"View Design\" item on its node menu in the arch\n // viz (designLinks keys off designFile). See graph-visualizer.ts.\n if (hasGeneratedDesign(workspaceRoot, info.root)) {\n entry.designFile = toRepoRelative(info.root, 'design.json');\n }\n }\n\n validateUniqueServiceNames(serviceNames, problems);\n validateLibraryTypesMatch(graph, problems);\n validateRoleDependencies(graph, problems);\n\n if (problems.length > 0) {\n throw new MetadataValidationError(problems);\n }\n}\n\n/**\n * The fields a project DECLARES about itself — its framework env set and role (nx tags), the\n * webpieces runtime packages it depends on (package.json), and whether it opts out of the drawings\n * (nx tag). Grouped because they share a shape: resolve, record the problem, or write the field.\n *\n * Distinct from enrichResponsibilities/designFile, which read generated or hand-written FILES.\n */\n// webpieces-disable no-function-outside-class -- enrichGraph step helper, matching enrichClientNames/enrichResponsibilities in this file\nfunction enrichDeclarations(\n entry: GraphEntry,\n info: ProjectInfo,\n workspaceRoot: string,\n problems: string[]\n): void {\n const resolution = resolveFramework(info, workspaceRoot);\n if (resolution.problem !== null) {\n problems.push(resolution.problem);\n } else if (resolution.frameworks !== null) {\n entry.framework = resolution.frameworks;\n }\n\n const roleResolution = resolveRole(info);\n if (roleResolution.problem !== null) {\n problems.push(roleResolution.problem);\n } else if (roleResolution.role !== null) {\n entry.role = roleResolution.role;\n }\n\n // Only persist when the project declares at least one — a repo full of\n // \"webpiecesRuntime\": [] lines would be noise, and absence already means \"declares none\".\n const participantResolution = resolveRuntimeParticipant(info, workspaceRoot);\n if (participantResolution.problem !== null) {\n problems.push(participantResolution.problem);\n } else if (participantResolution.markers !== null && participantResolution.markers.length > 0) {\n entry.webpiecesRuntime = participantResolution.markers;\n }\n\n // Only persist the field when hidden (false); drawn projects (the\n // default) stay clean in dependencies.json with no drawOnGraph line.\n const drawResolution = resolveDrawOnGraph(info);\n if (drawResolution.problem !== null) {\n problems.push(drawResolution.problem);\n } else if (drawResolution.drawOnGraph === false) {\n entry.drawOnGraph = false;\n }\n\n // Only persist when the project actually cuts an edge — absence is the normal state, and a repo\n // full of \"cutLegacyCycle\": [] lines would bury the ones that mean something.\n const cutResolution = resolveCutLegacyCycles(info);\n if (cutResolution.problem !== null) {\n problems.push(cutResolution.problem);\n } else if (cutResolution.targets !== null && cutResolution.targets.length > 0) {\n entry.cutLegacyCycle = cutResolution.targets;\n }\n}\n\n/**\n * Roles that are terminal APPS — nothing may depend on them. A server, a\n * non-HTTP `app`, or a client is a top-level runnable; being depended upon means\n * it is really a library and should be retagged `role:lib`/`role:designed-lib`.\n */\nexport const APP_ROLES: ReadonlyArray<string> = ['server', 'app', 'client'];\n\n/**\n * Compatibility lattice — the \"up-set\" of each atomic env is the env itself\n * PLUS every ancestor it can legally consume code from (specialization edges\n * child → parent: react → browser, angular → browser, express → node). A\n * consumer promising env `c` can be satisfied by any dependency env in `up(c)`.\n *\n * `react-native` is a first-class runtime and NOT a specialisation of browser (#1064, D6): it has no\n * DOM, no IndexedDB and no OPFS, so a react-native consumer may only consume libraries that are\n * themselves tagged `framework:react-native` (a universal browser+node+react-native library, a\n * browser+react-native one, or a react-native-only one).\n */\nexport const ENV_UP_SETS: Readonly<Record<string, ReadonlyArray<string>>> = {\n react: ['react', 'browser'],\n angular: ['angular', 'browser'],\n browser: ['browser'],\n express: ['express', 'node'],\n node: ['node'],\n 'react-native': ['react-native'],\n};\n\n/** The up-set of an env (env itself + ancestors); unknown envs map to just themselves. */\nfunction upSet(env: string): ReadonlyArray<string> {\n return ENV_UP_SETS[env] ?? [env];\n}\n\n/**\n * `library-types-match-client` rule.\n *\n * A project's `framework` field is its libType — the SET of runtime\n * environments it is validated to run in (browser | react | angular | node |\n * express | react-native). For a dependency edge Consumer C → Library L, the edge is LEGAL iff\n * for EVERY env `c` in C's set, up(c) ∩ L's set ≠ ∅ — i.e. every environment\n * the consumer promises to run in can be satisfied by the dependency. This keeps\n * an express app from depending on a browser-only lib, and lets a `browser+node`\n * lib be consumed by both react and express projects. Every violation is\n * appended to `problems` so `arch:generate` fails with the full list.\n */\nexport function validateLibraryTypesMatch(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromSet = entry.framework;\n if (fromSet === undefined) continue; // framework resolution already flagged this project\n\n for (const dep of entry.dependsOn) {\n const depEntry = graph[dep];\n const toSet = depEntry?.framework;\n if (toSet === undefined) continue;\n\n const unsatisfied = fromSet.filter(\n (env: string) => !upSet(env).some((up: string) => toSet.includes(up))\n );\n if (unsatisfied.length === 0) continue;\n\n problems.push(\n `library-types-match-client: '${projectName}' [${fromSet.join(', ')}] must not depend on ` +\n `'${dep}' [${toSet.join(', ')}] — the consumer env(s) ${unsatisfied.join(', ')} cannot be ` +\n `satisfied by the dependency (each consumer env must resolve to itself or an ancestor it ` +\n `consumes from: react/angular→browser, express→node, react-native→react-native only). ` +\n `Widen '${dep}' framework tags or ` +\n `remove the dependency.`\n );\n }\n }\n}\n\n/**\n * `role-dependency` rule.\n *\n * A project's `role` is its function (server | app | bundle | designed-lib | lib | client |\n * api-lib | api-client). The two api roles are libraries, never apps, so this rule leaves them alone;\n * what THEY may depend on is `api-lib-dependencies` (tag-truth.ts).\n * Apps are terminal — libraries and clients consume them, never the reverse:\n * - a `client` is fully terminal: NOTHING may depend on it.\n * - a `server` may only be depended upon by another `server` — the one\n * legitimate case is a server-side orchestrator/e2e harness that boots\n * other servers. A `lib`/`designed-lib`/`client` depending on a `server`\n * inverts the dependency direction and is a violation.\n * - a `bundle` is the one role permitted to depend on ANY app: it aggregates\n * several apps into one distributable (e.g. an nx plugin re-exposing multiple\n * tooling apps), so a `bundle → app` edge is legitimate, not inverted.\n */\nexport function validateRoleDependencies(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromRole = entry.role;\n for (const dep of entry.dependsOn) {\n const toRole = graph[dep]?.role;\n if (toRole === undefined) continue; // role resolution already flagged this project\n if (!APP_ROLES.includes(toRole)) continue;\n // A server may orchestrate/boot other servers (e.g. an e2e harness).\n if (toRole === 'server' && fromRole === 'server') continue;\n // A bundle aggregates apps — it may depend on any app role.\n if (fromRole === 'bundle') continue;\n\n const why =\n toRole === 'client'\n ? `a 'client' app is terminal and may never be depended upon`\n : `a 'server' may only be depended upon by another 'server' (an orchestrator/e2e harness)`;\n problems.push(\n `role-dependency: '${projectName}' (role:${fromRole ?? 'none'}) must not depend on ` +\n `'${dep}' (role:${toRole}) — ${why}. Retag '${dep}' role:lib/role:designed-lib if it ` +\n `is actually a library, or remove the dependency.`\n );\n }\n }\n}\n\n/**\n * Fill the two symmetric client-addressing fields — `serviceName` (the name clients address THIS app\n * by) and `callsService` (the name THIS app's clients call when no literal ClientConfig sits at the\n * call site). Both are declared in project.json, never derived (see service-name-resolver.ts).\n */\n// webpieces-disable no-function-outside-class -- pure enrichment helper, mirrors enrichResponsibilities\nfunction enrichClientNames(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n projectName: string,\n serviceNames: Map<string, string>,\n problems: string[]\n): void {\n const serviceResolution = resolveServiceName(info, workspaceRoot);\n if (serviceResolution.problem !== null) {\n problems.push(serviceResolution.problem);\n } else if (serviceResolution.serviceName !== null) {\n entry.serviceName = serviceResolution.serviceName;\n serviceNames.set(projectName, serviceResolution.serviceName);\n }\n\n // Consumed by the runtime graph's target resolution (runtime-graph.ts), between the call-site\n // literal and the fan-out fallback.\n const callsResolution = resolveCallsService(info, workspaceRoot);\n if (callsResolution.problem !== null) {\n problems.push(callsResolution.problem);\n } else if (callsResolution.callsService !== null) {\n entry.callsService = callsResolution.callsService;\n }\n}\n\nfunction enrichResponsibilities(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n problems: string[]\n): void {\n const responsibilitiesFile = toRepoRelative(info.root, RESPONSIBILITIES_FILE_NAME);\n entry.responsibilitiesFile = responsibilitiesFile;\n\n const absolutePath = path.join(workspaceRoot, info.root, RESPONSIBILITIES_FILE_NAME);\n if (!fs.existsSync(absolutePath)) {\n problems.push(\n `${info.name}: missing required ${responsibilitiesFile} — create it with a heading, ` +\n `one short summary paragraph, then the full responsibilities of the module`\n );\n return;\n }\n\n const summary = extractShortDescription(fs.readFileSync(absolutePath, 'utf-8'));\n const summaryProblem = validateShortDescription(summary, responsibilitiesFile);\n if (summaryProblem !== null) {\n problems.push(`${info.name}: ${summaryProblem}`);\n return;\n }\n entry.shortDescription = summary;\n}\n\n/**\n * Repo-relative path with forward slashes (stable across platforms in the\n * committed JSON).\n */\nfunction toRepoRelative(projectRoot: string, fileName: string): string {\n return [projectRoot.replace(/\\\\/g, '/').replace(/\\/+$/, ''), fileName].join('/');\n}\n\n/**\n * True when the project has a REAL generated DI design — a committed design.json\n * whose `designs[]` is non-empty (i.e. it has ≥1 @DocumentDesign root).\n *\n * MISSING, empty-`designs[]`, and unparseable all read as \"no design\", and that\n * equivalence is load-bearing rather than incidental: di-graph-generate writes NO\n * file for a project without a design root (and reaps a stale one), so \"absent\"\n * is now the normal state for every plain lib and legacy service. Repos that have\n * not rebuilt since still carry `{ designs: [] }` files. Either way the box's node\n * menu must offer no \"View Design\" item. See design-file-emission.spec.ts.\n */\nfunction hasGeneratedDesign(workspaceRoot: string, projectRoot: string): boolean {\n const designPath = path.join(workspaceRoot, projectRoot, 'design.json');\n if (!fs.existsSync(designPath)) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const parsed = JSON.parse(fs.readFileSync(designPath, 'utf-8'));\n return Array.isArray(parsed.designs) && parsed.designs.length > 0;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Skipping unparseable ${designPath}: ${error.message}`);\n return false;\n }\n}\n"]}
@@ -150,6 +150,8 @@ class GraphVisualizer {
150
150
  return ', color="red", penwidth=3';
151
151
  if (role === 'api-lib')
152
152
  return ', color="#EF6C00", penwidth=2';
153
+ if (role === 'api-client')
154
+ return ', color="#00838F", penwidth=2';
153
155
  if (role === 'designed-lib')
154
156
  return ', penwidth=2';
155
157
  return '';
@@ -560,6 +562,10 @@ class GraphVisualizer {
560
562
  <div class="legend-item">
561
563
  <span class="legend-box" style="border: 2px solid #EF6C00;"></span>
562
564
  <strong>api-lib:</strong> API-contract library (defines <code>@ApiPath</code>/<code>@Rpc</code>/<code>@PubSub</code> <code>*Api</code> classes)
565
+ </div>
566
+ <div class="legend-item">
567
+ <span class="legend-box" style="border: 2px solid #00838F;"></span>
568
+ <strong>api-client:</strong> a contract plus its default implementation that talks to an outside system through its SDK
563
569
  </div>`;
564
570
  }
565
571
  // Column 3 — edge line style keyed on WHY a project depends on an api-lib.