@ankhorage/doctor 0.11.13 → 0.11.15
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.15
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1478d4c: Update dependencies from Renovate pull request #183.
|
|
8
|
+
|
|
9
|
+
## 0.11.14
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 5ca125e: Temporarily defer feature role-combination enforcement until
|
|
14
|
+
`ankhorage/rules-architecture` is the canonical machine-readable architecture rules source, while
|
|
15
|
+
keeping the remaining architecture diagnostics active.
|
|
16
|
+
|
|
3
17
|
## 0.11.13
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# @ankhorage/doctor
|
|
5
5
|
|
|
6
|
-
         
|
|
7
7
|
|
|
8
8
|
Executable Ankh provider and standalone CLI for lightweight repo and package compliance diagnostics.
|
|
9
9
|
|
|
@@ -17,30 +17,7 @@ async function analyzeDirectoryVocabularyAsync(input) {
|
|
|
17
17
|
diagnostics.push(createDiagnostic(input, directoryPath, ARCHITECTURE_POLICY.rules.catchAllDirectory, `src/${directoryName}/ is a generic catch-all. Move code to an owning domain, feature, package edge, or utils/ according to the repository profile.`));
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
return diagnostics;
|
|
22
|
-
}
|
|
23
|
-
/*** Validate feature-first role combinations without requiring unused ceremonial layers. */
|
|
24
|
-
async function analyzeFeatureCombinationsAsync(input, sourceRoot) {
|
|
25
|
-
const featuresRoot = path.join(sourceRoot, 'features');
|
|
26
|
-
if (!(await pathExistsAsync(featuresRoot)))
|
|
27
|
-
return [];
|
|
28
|
-
const featureEntries = (await fs.readdir(featuresRoot, { withFileTypes: true }))
|
|
29
|
-
.filter((entry) => entry.isDirectory())
|
|
30
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
31
|
-
const diagnostics = [];
|
|
32
|
-
for (const entry of featureEntries) {
|
|
33
|
-
const featureRoot = path.join(featuresRoot, entry.name);
|
|
34
|
-
const roleNames = new Set((await fs.readdir(featureRoot, { withFileTypes: true }))
|
|
35
|
-
.filter((child) => child.isDirectory())
|
|
36
|
-
.map((child) => child.name));
|
|
37
|
-
for (const [roleName, requirement] of Object.entries(SOURCE_POLICY.featureCombinations)) {
|
|
38
|
-
if (roleNames.has(roleName) &&
|
|
39
|
-
!requirement.requiresAnyOf.some((requiredRole) => roleNames.has(requiredRole))) {
|
|
40
|
-
diagnostics.push(createDiagnostic(input, path.join(featureRoot, roleName), getArchitecturePolicyRule(requirement.ruleId), `Feature "${entry.name}" declares ${roleName}/ without one of the required inward roles: ${requirement.requiresAnyOf.join(', ')}.`));
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
20
|
+
// Folder-role combination enforcement stays deferred until rules-architecture is canonical.
|
|
44
21
|
return diagnostics;
|
|
45
22
|
}
|
|
46
23
|
/*** Validate that relative source imports point inward across recognized architecture roles. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sourceArchitectureAnalysis.js","sourceRoot":"","sources":["../src/sourceArchitectureAnalysis.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\n\nimport { ARCHITECTURE_POLICY } from '@ankhorage/policy/architecture';\n\nimport type { DoctorDiagnostic, DoctorPolicyProfile } from './diagnostics.js';\nimport { getArchitecturePolicyRule } from './utils/getArchitecturePolicyRule.js';\n\ninterface SourceImport {\n readonly filePath: string;\n readonly specifier: string;\n}\n\ninterface AnalyzeSourceArchitectureInput {\n readonly activeSourceImports: readonly SourceImport[];\n readonly profile: DoctorPolicyProfile;\n readonly targetPath: string;\n}\n\nconst SOURCE_POLICY = ARCHITECTURE_POLICY.source;\n\n/*** Validate folder-role combinations and inward source dependency direction. */\nexport async function analyzeSourceArchitecture(\n input: AnalyzeSourceArchitectureInput,\n): Promise<DoctorDiagnostic[]> {\n return [...(await analyzeDirectoryVocabularyAsync(input)), ...analyzeImportDirection(input)];\n}\n\n/*** Validate architectural directory names only when a repository has introduced them. */\nasync function analyzeDirectoryVocabularyAsync(\n input: AnalyzeSourceArchitectureInput,\n): Promise<DoctorDiagnostic[]> {\n const diagnostics: DoctorDiagnostic[] = [];\n const sourceRoot = path.join(input.targetPath, 'src');\n\n for (const directoryName of SOURCE_POLICY.catchAllDirectories) {\n const directoryPath = path.join(sourceRoot, directoryName);\n if (await pathExistsAsync(directoryPath)) {\n diagnostics.push(\n createDiagnostic(\n input,\n directoryPath,\n ARCHITECTURE_POLICY.rules.catchAllDirectory,\n `src/${directoryName}/ is a generic catch-all. Move code to an owning domain, feature, package edge, or utils/ according to the repository profile.`,\n ),\n );\n }\n }\n\n diagnostics.push(...(await analyzeFeatureCombinationsAsync(input, sourceRoot)));\n return diagnostics;\n}\n\n/*** Validate feature-first role combinations without requiring unused ceremonial layers. */\nasync function analyzeFeatureCombinationsAsync(\n input: AnalyzeSourceArchitectureInput,\n sourceRoot: string,\n): Promise<DoctorDiagnostic[]> {\n const featuresRoot = path.join(sourceRoot, 'features');\n if (!(await pathExistsAsync(featuresRoot))) return [];\n\n const featureEntries = (await fs.readdir(featuresRoot, { withFileTypes: true }))\n .filter((entry) => entry.isDirectory())\n .sort((left, right) => left.name.localeCompare(right.name));\n const diagnostics: DoctorDiagnostic[] = [];\n\n for (const entry of featureEntries) {\n const featureRoot = path.join(featuresRoot, entry.name);\n const roleNames = new Set(\n (await fs.readdir(featureRoot, { withFileTypes: true }))\n .filter((child) => child.isDirectory())\n .map((child) => child.name),\n );\n\n for (const [roleName, requirement] of Object.entries(SOURCE_POLICY.featureCombinations)) {\n if (\n roleNames.has(roleName) &&\n !requirement.requiresAnyOf.some((requiredRole) => roleNames.has(requiredRole))\n ) {\n diagnostics.push(\n createDiagnostic(\n input,\n path.join(featureRoot, roleName),\n getArchitecturePolicyRule(requirement.ruleId),\n `Feature \"${entry.name}\" declares ${roleName}/ without one of the required inward roles: ${requirement.requiresAnyOf.join(', ')}.`,\n ),\n );\n }\n }\n }\n\n return diagnostics;\n}\n\n/*** Validate that relative source imports point inward across recognized architecture roles. */\nfunction analyzeImportDirection(input: AnalyzeSourceArchitectureInput): DoctorDiagnostic[] {\n return input.activeSourceImports.flatMap((sourceImport) =>\n analyzeOneImportDirection(input, sourceImport),\n );\n}\n\n/*** Validate one relative import against repository and layer boundaries. */\nfunction analyzeOneImportDirection(\n input: AnalyzeSourceArchitectureInput,\n sourceImport: SourceImport,\n): DoctorDiagnostic[] {\n if (!sourceImport.specifier.startsWith('.')) return [];\n\n const targetPath = path.resolve(path.dirname(sourceImport.filePath), sourceImport.specifier);\n const escaped = createEscapedRepositoryDiagnostic(input, sourceImport, targetPath);\n if (escaped !== null) return [escaped];\n\n return analyzeRecognizedRoleDirection(input, sourceImport, targetPath);\n}\n\n/*** Report a relative import that escapes the standalone repository boundary. */\nfunction createEscapedRepositoryDiagnostic(\n input: AnalyzeSourceArchitectureInput,\n sourceImport: SourceImport,\n targetPath: string,\n): DoctorDiagnostic | null {\n const relativeTarget = path.relative(input.targetPath, targetPath);\n if (!relativeTarget.startsWith('..') && !path.isAbsolute(relativeTarget)) return null;\n\n return createDiagnostic(\n input,\n sourceImport.filePath,\n getArchitecturePolicyRule(SOURCE_POLICY.repositoryBoundaryRuleId),\n `Relative import \"${sourceImport.specifier}\" escapes the standalone repository root.`,\n );\n}\n\n/*** Apply inward dependency rules for recognized source architecture roles. */\nfunction analyzeRecognizedRoleDirection(\n input: AnalyzeSourceArchitectureInput,\n sourceImport: SourceImport,\n targetPath: string,\n): DoctorDiagnostic[] {\n const sourceSegments = splitRelativePath(input.targetPath, sourceImport.filePath);\n const targetSegments = splitRelativePath(input.targetPath, targetPath);\n const deliveryPolicy = SOURCE_POLICY.thinDeliveryAdapter;\n\n if (\n includesSegmentSequence(sourceSegments, deliveryPolicy.pathSegments) &&\n targetSegments.includes(deliveryPolicy.concreteAdapterSegment)\n ) {\n return [\n createDiagnostic(\n input,\n sourceImport.filePath,\n getArchitecturePolicyRule(deliveryPolicy.ruleId),\n `Thin delivery adapter must not wire concrete adapter implementation through \"${sourceImport.specifier}\". Import an application operation or composition boundary instead.`,\n ),\n ];\n }\n\n const role = resolveSourceRole(sourceSegments);\n if (role === null) return [];\n\n return createOutwardImportDiagnostic(input, sourceImport, targetSegments, role);\n}\n\n/*** Resolve the dependency rule owned by one recognized inner source role. */\nfunction resolveSourceRole(sourceSegments: readonly string[]): SourceRoleRule | null {\n for (const role of Object.values(SOURCE_POLICY.roles)) {\n if (\n sourceSegments.some((segment) => role.segments.some((roleSegment) => roleSegment === segment))\n ) {\n return {\n forbiddenSegments: new Set<string>(role.forbiddenOutwardSegments),\n label: role.label,\n rule: getArchitecturePolicyRule(role.ruleId),\n };\n }\n }\n\n return null;\n}\n\ninterface SourceRoleRule {\n readonly forbiddenSegments: ReadonlySet<string>;\n readonly label: string;\n readonly rule: ReturnType<typeof getArchitecturePolicyRule>;\n}\n\n/*** Report one outward dependency when the imported path crosses a forbidden role. */\nfunction createOutwardImportDiagnostic(\n input: AnalyzeSourceArchitectureInput,\n sourceImport: SourceImport,\n targetSegments: readonly string[],\n role: SourceRoleRule,\n): DoctorDiagnostic[] {\n const outwardRole = targetSegments.find((segment) => role.forbiddenSegments.has(segment));\n if (outwardRole === undefined) return [];\n\n return [\n createDiagnostic(\n input,\n sourceImport.filePath,\n role.rule,\n `${role.label} must not import outward ${outwardRole}/ implementation through \"${sourceImport.specifier}\".`,\n ),\n ];\n}\n\n/*** Check whether one path segment sequence occurs contiguously in another. */\nfunction includesSegmentSequence(\n segments: readonly string[],\n expected: readonly string[],\n): boolean {\n return segments.some((_, index) =>\n expected.every((segment, offset) => segments[index + offset] === segment),\n );\n}\n\n/*** Split one repository path into normalized architecture segments. */\nfunction splitRelativePath(targetPath: string, filePath: string): readonly string[] {\n return path.relative(targetPath, filePath).split(path.sep).filter(Boolean);\n}\n\n/*** Build one deterministic architecture diagnostic from Policy-owned metadata. */\nfunction createDiagnostic(\n input: AnalyzeSourceArchitectureInput,\n diagnosticPath: string,\n rule: ReturnType<typeof getArchitecturePolicyRule>,\n message: string,\n): DoctorDiagnostic {\n return {\n code: 'field-invalid',\n message,\n path: diagnosticPath,\n profile: input.profile,\n ruleId: rule.id,\n severity: rule.severity,\n };\n}\n\n/*** Check whether a filesystem path exists. */\nasync function pathExistsAsync(targetPath: string): Promise<boolean> {\n try {\n await fs.access(targetPath);\n return true;\n } catch {\n return false;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sourceArchitectureAnalysis.js","sourceRoot":"","sources":["../src/sourceArchitectureAnalysis.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\n\nimport { ARCHITECTURE_POLICY } from '@ankhorage/policy/architecture';\n\nimport type { DoctorDiagnostic, DoctorPolicyProfile } from './diagnostics.js';\nimport { getArchitecturePolicyRule } from './utils/getArchitecturePolicyRule.js';\n\ninterface SourceImport {\n readonly filePath: string;\n readonly specifier: string;\n}\n\ninterface AnalyzeSourceArchitectureInput {\n readonly activeSourceImports: readonly SourceImport[];\n readonly profile: DoctorPolicyProfile;\n readonly targetPath: string;\n}\n\nconst SOURCE_POLICY = ARCHITECTURE_POLICY.source;\n\n/*** Validate folder-role combinations and inward source dependency direction. */\nexport async function analyzeSourceArchitecture(\n input: AnalyzeSourceArchitectureInput,\n): Promise<DoctorDiagnostic[]> {\n return [...(await analyzeDirectoryVocabularyAsync(input)), ...analyzeImportDirection(input)];\n}\n\n/*** Validate architectural directory names only when a repository has introduced them. */\nasync function analyzeDirectoryVocabularyAsync(\n input: AnalyzeSourceArchitectureInput,\n): Promise<DoctorDiagnostic[]> {\n const diagnostics: DoctorDiagnostic[] = [];\n const sourceRoot = path.join(input.targetPath, 'src');\n\n for (const directoryName of SOURCE_POLICY.catchAllDirectories) {\n const directoryPath = path.join(sourceRoot, directoryName);\n if (await pathExistsAsync(directoryPath)) {\n diagnostics.push(\n createDiagnostic(\n input,\n directoryPath,\n ARCHITECTURE_POLICY.rules.catchAllDirectory,\n `src/${directoryName}/ is a generic catch-all. Move code to an owning domain, feature, package edge, or utils/ according to the repository profile.`,\n ),\n );\n }\n }\n\n // Folder-role combination enforcement stays deferred until rules-architecture is canonical.\n return diagnostics;\n}\n\n/*** Validate that relative source imports point inward across recognized architecture roles. */\nfunction analyzeImportDirection(input: AnalyzeSourceArchitectureInput): DoctorDiagnostic[] {\n return input.activeSourceImports.flatMap((sourceImport) =>\n analyzeOneImportDirection(input, sourceImport),\n );\n}\n\n/*** Validate one relative import against repository and layer boundaries. */\nfunction analyzeOneImportDirection(\n input: AnalyzeSourceArchitectureInput,\n sourceImport: SourceImport,\n): DoctorDiagnostic[] {\n if (!sourceImport.specifier.startsWith('.')) return [];\n\n const targetPath = path.resolve(path.dirname(sourceImport.filePath), sourceImport.specifier);\n const escaped = createEscapedRepositoryDiagnostic(input, sourceImport, targetPath);\n if (escaped !== null) return [escaped];\n\n return analyzeRecognizedRoleDirection(input, sourceImport, targetPath);\n}\n\n/*** Report a relative import that escapes the standalone repository boundary. */\nfunction createEscapedRepositoryDiagnostic(\n input: AnalyzeSourceArchitectureInput,\n sourceImport: SourceImport,\n targetPath: string,\n): DoctorDiagnostic | null {\n const relativeTarget = path.relative(input.targetPath, targetPath);\n if (!relativeTarget.startsWith('..') && !path.isAbsolute(relativeTarget)) return null;\n\n return createDiagnostic(\n input,\n sourceImport.filePath,\n getArchitecturePolicyRule(SOURCE_POLICY.repositoryBoundaryRuleId),\n `Relative import \"${sourceImport.specifier}\" escapes the standalone repository root.`,\n );\n}\n\n/*** Apply inward dependency rules for recognized source architecture roles. */\nfunction analyzeRecognizedRoleDirection(\n input: AnalyzeSourceArchitectureInput,\n sourceImport: SourceImport,\n targetPath: string,\n): DoctorDiagnostic[] {\n const sourceSegments = splitRelativePath(input.targetPath, sourceImport.filePath);\n const targetSegments = splitRelativePath(input.targetPath, targetPath);\n const deliveryPolicy = SOURCE_POLICY.thinDeliveryAdapter;\n\n if (\n includesSegmentSequence(sourceSegments, deliveryPolicy.pathSegments) &&\n targetSegments.includes(deliveryPolicy.concreteAdapterSegment)\n ) {\n return [\n createDiagnostic(\n input,\n sourceImport.filePath,\n getArchitecturePolicyRule(deliveryPolicy.ruleId),\n `Thin delivery adapter must not wire concrete adapter implementation through \"${sourceImport.specifier}\". Import an application operation or composition boundary instead.`,\n ),\n ];\n }\n\n const role = resolveSourceRole(sourceSegments);\n if (role === null) return [];\n\n return createOutwardImportDiagnostic(input, sourceImport, targetSegments, role);\n}\n\n/*** Resolve the dependency rule owned by one recognized inner source role. */\nfunction resolveSourceRole(sourceSegments: readonly string[]): SourceRoleRule | null {\n for (const role of Object.values(SOURCE_POLICY.roles)) {\n if (\n sourceSegments.some((segment) => role.segments.some((roleSegment) => roleSegment === segment))\n ) {\n return {\n forbiddenSegments: new Set<string>(role.forbiddenOutwardSegments),\n label: role.label,\n rule: getArchitecturePolicyRule(role.ruleId),\n };\n }\n }\n\n return null;\n}\n\ninterface SourceRoleRule {\n readonly forbiddenSegments: ReadonlySet<string>;\n readonly label: string;\n readonly rule: ReturnType<typeof getArchitecturePolicyRule>;\n}\n\n/*** Report one outward dependency when the imported path crosses a forbidden role. */\nfunction createOutwardImportDiagnostic(\n input: AnalyzeSourceArchitectureInput,\n sourceImport: SourceImport,\n targetSegments: readonly string[],\n role: SourceRoleRule,\n): DoctorDiagnostic[] {\n const outwardRole = targetSegments.find((segment) => role.forbiddenSegments.has(segment));\n if (outwardRole === undefined) return [];\n\n return [\n createDiagnostic(\n input,\n sourceImport.filePath,\n role.rule,\n `${role.label} must not import outward ${outwardRole}/ implementation through \"${sourceImport.specifier}\".`,\n ),\n ];\n}\n\n/*** Check whether one path segment sequence occurs contiguously in another. */\nfunction includesSegmentSequence(\n segments: readonly string[],\n expected: readonly string[],\n): boolean {\n return segments.some((_, index) =>\n expected.every((segment, offset) => segments[index + offset] === segment),\n );\n}\n\n/*** Split one repository path into normalized architecture segments. */\nfunction splitRelativePath(targetPath: string, filePath: string): readonly string[] {\n return path.relative(targetPath, filePath).split(path.sep).filter(Boolean);\n}\n\n/*** Build one deterministic architecture diagnostic from Policy-owned metadata. */\nfunction createDiagnostic(\n input: AnalyzeSourceArchitectureInput,\n diagnosticPath: string,\n rule: ReturnType<typeof getArchitecturePolicyRule>,\n message: string,\n): DoctorDiagnostic {\n return {\n code: 'field-invalid',\n message,\n path: diagnosticPath,\n profile: input.profile,\n ruleId: rule.id,\n severity: rule.severity,\n };\n}\n\n/*** Check whether a filesystem path exists. */\nasync function pathExistsAsync(targetPath: string): Promise<boolean> {\n try {\n await fs.access(targetPath);\n return true;\n } catch {\n return false;\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/doctor",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.15",
|
|
4
4
|
"description": "Executable Ankh provider and standalone CLI for lightweight repo and package compliance diagnostics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"@ankhorage/ankh": "^0.10.4",
|
|
88
88
|
"@ankhorage/devtools": "^2.0.0",
|
|
89
89
|
"@ankhorage/paradox": "^0.2.6",
|
|
90
|
-
"@types/bun": "^1.4.
|
|
90
|
+
"@types/bun": "^1.4.2",
|
|
91
91
|
"@types/node": "^25.6.0",
|
|
92
92
|
"typescript": "^5.9.3"
|
|
93
93
|
},
|
|
@@ -47,48 +47,7 @@ async function analyzeDirectoryVocabularyAsync(
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
return diagnostics;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/*** Validate feature-first role combinations without requiring unused ceremonial layers. */
|
|
55
|
-
async function analyzeFeatureCombinationsAsync(
|
|
56
|
-
input: AnalyzeSourceArchitectureInput,
|
|
57
|
-
sourceRoot: string,
|
|
58
|
-
): Promise<DoctorDiagnostic[]> {
|
|
59
|
-
const featuresRoot = path.join(sourceRoot, 'features');
|
|
60
|
-
if (!(await pathExistsAsync(featuresRoot))) return [];
|
|
61
|
-
|
|
62
|
-
const featureEntries = (await fs.readdir(featuresRoot, { withFileTypes: true }))
|
|
63
|
-
.filter((entry) => entry.isDirectory())
|
|
64
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
65
|
-
const diagnostics: DoctorDiagnostic[] = [];
|
|
66
|
-
|
|
67
|
-
for (const entry of featureEntries) {
|
|
68
|
-
const featureRoot = path.join(featuresRoot, entry.name);
|
|
69
|
-
const roleNames = new Set(
|
|
70
|
-
(await fs.readdir(featureRoot, { withFileTypes: true }))
|
|
71
|
-
.filter((child) => child.isDirectory())
|
|
72
|
-
.map((child) => child.name),
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
for (const [roleName, requirement] of Object.entries(SOURCE_POLICY.featureCombinations)) {
|
|
76
|
-
if (
|
|
77
|
-
roleNames.has(roleName) &&
|
|
78
|
-
!requirement.requiresAnyOf.some((requiredRole) => roleNames.has(requiredRole))
|
|
79
|
-
) {
|
|
80
|
-
diagnostics.push(
|
|
81
|
-
createDiagnostic(
|
|
82
|
-
input,
|
|
83
|
-
path.join(featureRoot, roleName),
|
|
84
|
-
getArchitecturePolicyRule(requirement.ruleId),
|
|
85
|
-
`Feature "${entry.name}" declares ${roleName}/ without one of the required inward roles: ${requirement.requiresAnyOf.join(', ')}.`,
|
|
86
|
-
),
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
50
|
+
// Folder-role combination enforcement stays deferred until rules-architecture is canonical.
|
|
92
51
|
return diagnostics;
|
|
93
52
|
}
|
|
94
53
|
|