@ankhorage/doctor 0.11.96 → 0.12.1
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 +14 -0
- package/README.md +1 -1
- package/dist/analysis.js +22 -22
- package/dist/analysis.js.map +1 -1
- package/dist/bunPolicyAnalysis.d.ts.map +1 -1
- package/dist/bunPolicyAnalysis.js +5 -14
- package/dist/bunPolicyAnalysis.js.map +1 -1
- package/dist/cliLayoutAnalysis.d.ts.map +1 -1
- package/dist/cliLayoutAnalysis.js +39 -29
- package/dist/cliLayoutAnalysis.js.map +1 -1
- package/dist/repositorySourceRulesAnalysis.d.ts +16 -0
- package/dist/repositorySourceRulesAnalysis.d.ts.map +1 -0
- package/dist/repositorySourceRulesAnalysis.js +103 -0
- package/dist/repositorySourceRulesAnalysis.js.map +1 -0
- package/dist/sourceArchitectureAnalysis.d.ts +3 -6
- package/dist/sourceArchitectureAnalysis.d.ts.map +1 -1
- package/dist/sourceArchitectureAnalysis.js +93 -97
- package/dist/sourceArchitectureAnalysis.js.map +1 -1
- package/dist/utils/getRepositoryRule.d.ts +9 -0
- package/dist/utils/getRepositoryRule.d.ts.map +1 -0
- package/dist/utils/getRepositoryRule.js +15 -0
- package/dist/utils/getRepositoryRule.js.map +1 -0
- package/package.json +7 -5
- package/src/analysis.ts +22 -24
- package/src/bunPolicyAnalysis.ts +7 -17
- package/src/cliLayoutAnalysis.ts +51 -30
- package/src/repositorySourceRulesAnalysis.ts +153 -0
- package/src/sourceArchitectureAnalysis.ts +102 -177
- package/src/utils/getRepositoryRule.ts +20 -0
- package/dist/utils/getArchitecturePolicyRule.d.ts +0 -286
- package/dist/utils/getArchitecturePolicyRule.d.ts.map +0 -1
- package/dist/utils/getArchitecturePolicyRule.js +0 -12
- package/dist/utils/getArchitecturePolicyRule.js.map +0 -1
- package/src/utils/getArchitecturePolicyRule.ts +0 -15
|
@@ -1,111 +1,107 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
1
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
const
|
|
6
|
-
|
|
2
|
+
import { createSourceGraphAsync } from '@ankhorage/dependency-graph';
|
|
3
|
+
import { evaluateArchitectureProfile } from '@ankhorage/rules-architecture';
|
|
4
|
+
const ENABLED_ARCHITECTURE_RULES = [
|
|
5
|
+
'package.architecture.domain-outward-import.disallowed',
|
|
6
|
+
'package.architecture.application-outward-import.disallowed',
|
|
7
|
+
'package.architecture.port-outward-import.disallowed',
|
|
8
|
+
'package.architecture.delivery-concrete-adapter-import.disallowed',
|
|
9
|
+
];
|
|
10
|
+
const ACTIVE_SOURCE_ROOTS = ['src', 'app', 'apps', 'packages', 'scripts'];
|
|
11
|
+
const IGNORED_SOURCE_DIRECTORIES = new Set([
|
|
12
|
+
'.expo',
|
|
13
|
+
'.git',
|
|
14
|
+
'.next',
|
|
15
|
+
'__fixtures__',
|
|
16
|
+
'__tests__',
|
|
17
|
+
'build',
|
|
18
|
+
'coverage',
|
|
19
|
+
'dist',
|
|
20
|
+
'docs',
|
|
21
|
+
'fixtures',
|
|
22
|
+
'node_modules',
|
|
23
|
+
'paradox',
|
|
24
|
+
'test',
|
|
25
|
+
'tests',
|
|
26
|
+
]);
|
|
27
|
+
/***
|
|
28
|
+
* Evaluates Doctor's currently enforced source-architecture subset through rules-architecture.
|
|
29
|
+
*/
|
|
7
30
|
export async function analyzeSourceArchitecture(input) {
|
|
8
|
-
|
|
31
|
+
const graph = await createSourceGraphAsync({
|
|
32
|
+
projects: [{ id: 'doctor-target', rootPath: input.targetPath }],
|
|
33
|
+
});
|
|
34
|
+
const result = evaluateArchitectureProfile(filterActiveSourceGraph(graph), 'ankhorage', {
|
|
35
|
+
config: {
|
|
36
|
+
version: 1,
|
|
37
|
+
rules: ENABLED_ARCHITECTURE_RULES.map((id) => ({ enabled: true, id })),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
return result.findings.map((finding) => toDoctorDiagnostic(input, finding));
|
|
9
41
|
}
|
|
10
|
-
/***
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function analyzeOneImportDirection(input, sourceImport) {
|
|
29
|
-
if (!sourceImport.specifier.startsWith('.'))
|
|
30
|
-
return [];
|
|
31
|
-
const targetPath = path.resolve(path.dirname(sourceImport.filePath), sourceImport.specifier);
|
|
32
|
-
const escaped = createEscapedRepositoryDiagnostic(input, sourceImport, targetPath);
|
|
33
|
-
if (escaped !== null)
|
|
34
|
-
return [escaped];
|
|
35
|
-
return analyzeRecognizedRoleDirection(input, sourceImport, targetPath);
|
|
36
|
-
}
|
|
37
|
-
/*** Report a relative import that escapes the standalone repository boundary. */
|
|
38
|
-
function createEscapedRepositoryDiagnostic(input, sourceImport, targetPath) {
|
|
39
|
-
const relativeTarget = path.relative(input.targetPath, targetPath);
|
|
40
|
-
if (!relativeTarget.startsWith('..') && !path.isAbsolute(relativeTarget))
|
|
41
|
-
return null;
|
|
42
|
-
return createDiagnostic(input, sourceImport.filePath, getArchitecturePolicyRule(SOURCE_POLICY.repositoryBoundaryRuleId), `Relative import "${sourceImport.specifier}" escapes the standalone repository root.`);
|
|
43
|
-
}
|
|
44
|
-
/*** Apply inward dependency rules for recognized source architecture roles. */
|
|
45
|
-
function analyzeRecognizedRoleDirection(input, sourceImport, targetPath) {
|
|
46
|
-
const sourceSegments = splitRelativePath(input.targetPath, sourceImport.filePath);
|
|
47
|
-
const targetSegments = splitRelativePath(input.targetPath, targetPath);
|
|
48
|
-
const deliveryPolicy = SOURCE_POLICY.thinDeliveryAdapter;
|
|
49
|
-
if (includesSegmentSequence(sourceSegments, deliveryPolicy.pathSegments) &&
|
|
50
|
-
targetSegments.includes(deliveryPolicy.concreteAdapterSegment)) {
|
|
51
|
-
return [
|
|
52
|
-
createDiagnostic(input, sourceImport.filePath, getArchitecturePolicyRule(deliveryPolicy.ruleId), `Thin delivery adapter must not wire concrete adapter implementation through "${sourceImport.specifier}". Import an application operation or composition boundary instead.`),
|
|
53
|
-
];
|
|
54
|
-
}
|
|
55
|
-
const role = resolveSourceRole(sourceSegments);
|
|
56
|
-
if (role === null)
|
|
57
|
-
return [];
|
|
58
|
-
return createOutwardImportDiagnostic(input, sourceImport, targetSegments, role);
|
|
42
|
+
/***
|
|
43
|
+
* Keeps architecture evaluation aligned with Doctor's existing active-source scan contract.
|
|
44
|
+
*/
|
|
45
|
+
function filterActiveSourceGraph(graph) {
|
|
46
|
+
const nodeById = new Map(graph.graph.nodes.map((node) => [node.id, node.data]));
|
|
47
|
+
return {
|
|
48
|
+
...graph,
|
|
49
|
+
graph: {
|
|
50
|
+
nodes: graph.graph.nodes,
|
|
51
|
+
edges: graph.graph.edges.filter((edge) => {
|
|
52
|
+
if (edge.data.kind !== 'imports')
|
|
53
|
+
return true;
|
|
54
|
+
const source = nodeById.get(edge.source);
|
|
55
|
+
const sourcePath = source?.path ?? source?.filePath;
|
|
56
|
+
return sourcePath === undefined || isActiveSourcePath(sourcePath);
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
59
60
|
}
|
|
60
|
-
/***
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
};
|
|
69
|
-
}
|
|
61
|
+
/***
|
|
62
|
+
* Checks whether one portable path belongs to Doctor's active production-source surface.
|
|
63
|
+
*/
|
|
64
|
+
function isActiveSourcePath(sourcePath) {
|
|
65
|
+
const normalized = sourcePath.split(path.sep).join('/');
|
|
66
|
+
const [root, ...segments] = normalized.split('/').filter(Boolean);
|
|
67
|
+
if (root === undefined || !ACTIVE_SOURCE_ROOTS.some((candidate) => candidate === root)) {
|
|
68
|
+
return false;
|
|
70
69
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const outwardRole = targetSegments.find((segment) => role.forbiddenSegments.has(segment));
|
|
76
|
-
if (outwardRole === undefined)
|
|
77
|
-
return [];
|
|
78
|
-
return [
|
|
79
|
-
createDiagnostic(input, sourceImport.filePath, role.rule, `${role.label} must not import outward ${outwardRole}/ implementation through "${sourceImport.specifier}".`),
|
|
80
|
-
];
|
|
81
|
-
}
|
|
82
|
-
/*** Check whether one path segment sequence occurs contiguously in another. */
|
|
83
|
-
function includesSegmentSequence(segments, expected) {
|
|
84
|
-
return segments.some((_, index) => expected.every((segment, offset) => segments[index + offset] === segment));
|
|
85
|
-
}
|
|
86
|
-
/*** Split one repository path into normalized architecture segments. */
|
|
87
|
-
function splitRelativePath(targetPath, filePath) {
|
|
88
|
-
return path.relative(targetPath, filePath).split(path.sep).filter(Boolean);
|
|
70
|
+
if (segments.some((segment) => IGNORED_SOURCE_DIRECTORIES.has(segment)))
|
|
71
|
+
return false;
|
|
72
|
+
const fileName = segments.at(-1) ?? '';
|
|
73
|
+
return !/(?:^|\.)(?:spec|test)\.[cm]?[jt]sx?$/u.test(fileName);
|
|
89
74
|
}
|
|
90
|
-
/***
|
|
91
|
-
|
|
75
|
+
/***
|
|
76
|
+
* Converts one architecture Rule finding into Doctor's stable diagnostic surface.
|
|
77
|
+
*/
|
|
78
|
+
function toDoctorDiagnostic(input, finding) {
|
|
79
|
+
const subjectPath = finding.sourceLocation?.path ?? finding.subjects[0]?.path ?? '.';
|
|
92
80
|
return {
|
|
93
81
|
code: 'field-invalid',
|
|
94
|
-
message,
|
|
95
|
-
path:
|
|
82
|
+
message: finding.message,
|
|
83
|
+
path: path.resolve(input.targetPath, subjectPath),
|
|
96
84
|
profile: input.profile,
|
|
97
|
-
ruleId:
|
|
98
|
-
severity:
|
|
85
|
+
ruleId: toDoctorRuleId(finding.ruleId),
|
|
86
|
+
severity: toDoctorSeverity(finding.severity),
|
|
99
87
|
};
|
|
100
88
|
}
|
|
101
|
-
/***
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
89
|
+
/***
|
|
90
|
+
* Narrows architecture-provider rule ids to the subset Doctor intentionally enforces.
|
|
91
|
+
*/
|
|
92
|
+
function toDoctorRuleId(ruleId) {
|
|
93
|
+
const match = ENABLED_ARCHITECTURE_RULES.find((candidate) => candidate === ruleId);
|
|
94
|
+
if (match === undefined)
|
|
95
|
+
throw new Error('Unexpected Doctor architecture rule: ' + ruleId);
|
|
96
|
+
return match;
|
|
97
|
+
}
|
|
98
|
+
/***
|
|
99
|
+
* Narrows generic Rules severity to Doctor's diagnostic severity surface.
|
|
100
|
+
*/
|
|
101
|
+
function toDoctorSeverity(severity) {
|
|
102
|
+
if (severity === 'info') {
|
|
103
|
+
throw new Error('Doctor does not support info-level architecture diagnostics.');
|
|
109
104
|
}
|
|
105
|
+
return severity;
|
|
110
106
|
}
|
|
111
107
|
//# sourceMappingURL=sourceArchitectureAnalysis.js.map
|
|
@@ -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 // 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"]}
|
|
1
|
+
{"version":3,"file":"sourceArchitectureAnalysis.js","sourceRoot":"","sources":["../src/sourceArchitectureAnalysis.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'node:path';\n\nimport { createSourceGraphAsync, type SourceGraph } from '@ankhorage/dependency-graph';\nimport { evaluateArchitectureProfile } from '@ankhorage/rules-architecture';\n\nimport type {\n DoctorDiagnostic,\n DoctorDiagnosticSeverity,\n DoctorPolicyProfile,\n DoctorRuleId,\n} from './diagnostics.js';\n\nconst ENABLED_ARCHITECTURE_RULES = [\n 'package.architecture.domain-outward-import.disallowed',\n 'package.architecture.application-outward-import.disallowed',\n 'package.architecture.port-outward-import.disallowed',\n 'package.architecture.delivery-concrete-adapter-import.disallowed',\n] as const satisfies readonly DoctorRuleId[];\n\nconst ACTIVE_SOURCE_ROOTS = ['src', 'app', 'apps', 'packages', 'scripts'] as const;\nconst IGNORED_SOURCE_DIRECTORIES = new Set([\n '.expo',\n '.git',\n '.next',\n '__fixtures__',\n '__tests__',\n 'build',\n 'coverage',\n 'dist',\n 'docs',\n 'fixtures',\n 'node_modules',\n 'paradox',\n 'test',\n 'tests',\n]);\n\ninterface AnalyzeSourceArchitectureInput {\n readonly profile: DoctorPolicyProfile;\n readonly targetPath: string;\n}\n\n/***\n * Evaluates Doctor's currently enforced source-architecture subset through rules-architecture.\n */\nexport async function analyzeSourceArchitecture(\n input: AnalyzeSourceArchitectureInput,\n): Promise<DoctorDiagnostic[]> {\n const graph = await createSourceGraphAsync({\n projects: [{ id: 'doctor-target', rootPath: input.targetPath }],\n });\n const result = evaluateArchitectureProfile(filterActiveSourceGraph(graph), 'ankhorage', {\n config: {\n version: 1,\n rules: ENABLED_ARCHITECTURE_RULES.map((id) => ({ enabled: true, id })),\n },\n });\n\n return result.findings.map((finding) => toDoctorDiagnostic(input, finding));\n}\n\n/***\n * Keeps architecture evaluation aligned with Doctor's existing active-source scan contract.\n */\nfunction filterActiveSourceGraph(graph: SourceGraph): SourceGraph {\n const nodeById = new Map(graph.graph.nodes.map((node) => [node.id, node.data]));\n return {\n ...graph,\n graph: {\n nodes: graph.graph.nodes,\n edges: graph.graph.edges.filter((edge) => {\n if (edge.data.kind !== 'imports') return true;\n const source = nodeById.get(edge.source);\n const sourcePath = source?.path ?? source?.filePath;\n return sourcePath === undefined || isActiveSourcePath(sourcePath);\n }),\n },\n };\n}\n\n/***\n * Checks whether one portable path belongs to Doctor's active production-source surface.\n */\nfunction isActiveSourcePath(sourcePath: string): boolean {\n const normalized = sourcePath.split(path.sep).join('/');\n const [root, ...segments] = normalized.split('/').filter(Boolean);\n if (root === undefined || !ACTIVE_SOURCE_ROOTS.some((candidate) => candidate === root)) {\n return false;\n }\n if (segments.some((segment) => IGNORED_SOURCE_DIRECTORIES.has(segment))) return false;\n const fileName = segments.at(-1) ?? '';\n return !/(?:^|\\.)(?:spec|test)\\.[cm]?[jt]sx?$/u.test(fileName);\n}\n\n/***\n * Converts one architecture Rule finding into Doctor's stable diagnostic surface.\n */\nfunction toDoctorDiagnostic(\n input: AnalyzeSourceArchitectureInput,\n finding: ReturnType<typeof evaluateArchitectureProfile>['findings'][number],\n): DoctorDiagnostic {\n const subjectPath = finding.sourceLocation?.path ?? finding.subjects[0]?.path ?? '.';\n return {\n code: 'field-invalid',\n message: finding.message,\n path: path.resolve(input.targetPath, subjectPath),\n profile: input.profile,\n ruleId: toDoctorRuleId(finding.ruleId),\n severity: toDoctorSeverity(finding.severity),\n };\n}\n\n/***\n * Narrows architecture-provider rule ids to the subset Doctor intentionally enforces.\n */\nfunction toDoctorRuleId(ruleId: string): DoctorRuleId {\n const match = ENABLED_ARCHITECTURE_RULES.find((candidate) => candidate === ruleId);\n if (match === undefined) throw new Error('Unexpected Doctor architecture rule: ' + ruleId);\n return match;\n}\n\n/***\n * Narrows generic Rules severity to Doctor's diagnostic severity surface.\n */\nfunction toDoctorSeverity(severity: 'error' | 'info' | 'warning'): DoctorDiagnosticSeverity {\n if (severity === 'info') {\n throw new Error('Doctor does not support info-level architecture diagnostics.');\n }\n return severity;\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DoctorDiagnosticSeverity, DoctorRuleId } from '../diagnostics.js';
|
|
2
|
+
/***
|
|
3
|
+
* Resolves one canonical repository rule by its stable Doctor rule id.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getRepositoryRule(ruleId: DoctorRuleId): {
|
|
6
|
+
readonly id: DoctorRuleId;
|
|
7
|
+
readonly severity: DoctorDiagnosticSeverity;
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=getRepositoryRule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getRepositoryRule.d.ts","sourceRoot":"","sources":["../../src/utils/getRepositoryRule.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEhF;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,GAAG;IACvD,QAAQ,CAAC,EAAE,EAAE,YAAY,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,wBAAwB,CAAC;CAC7C,CASA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createRepositoryRuleSet } from '@ankhorage/rules-repository';
|
|
2
|
+
/***
|
|
3
|
+
* Resolves one canonical repository rule by its stable Doctor rule id.
|
|
4
|
+
*/
|
|
5
|
+
export function getRepositoryRule(ruleId) {
|
|
6
|
+
const rule = createRepositoryRuleSet().rules.find((candidate) => candidate.id === ruleId);
|
|
7
|
+
if (rule === undefined) {
|
|
8
|
+
throw new Error('Unknown repository rule: ' + ruleId);
|
|
9
|
+
}
|
|
10
|
+
if (rule.defaultSeverity === 'info') {
|
|
11
|
+
throw new Error('Doctor does not support info-level repository diagnostics: ' + ruleId);
|
|
12
|
+
}
|
|
13
|
+
return { id: ruleId, severity: rule.defaultSeverity };
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=getRepositoryRule.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getRepositoryRule.js","sourceRoot":"","sources":["../../src/utils/getRepositoryRule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAItE;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAoB;IAIpD,MAAM,IAAI,GAAG,uBAAuB,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,MAAM,CAAC,CAAC;IAC1F,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,2BAA2B,GAAG,MAAM,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,IAAI,CAAC,eAAe,KAAK,MAAM,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,6DAA6D,GAAG,MAAM,CAAC,CAAC;IAC1F,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,CAAC;AACxD,CAAC","sourcesContent":["import { createRepositoryRuleSet } from '@ankhorage/rules-repository';\n\nimport type { DoctorDiagnosticSeverity, DoctorRuleId } from '../diagnostics.js';\n\n/***\n * Resolves one canonical repository rule by its stable Doctor rule id.\n */\nexport function getRepositoryRule(ruleId: DoctorRuleId): {\n readonly id: DoctorRuleId;\n readonly severity: DoctorDiagnosticSeverity;\n} {\n const rule = createRepositoryRuleSet().rules.find((candidate) => candidate.id === ruleId);\n if (rule === undefined) {\n throw new Error('Unknown repository rule: ' + ruleId);\n }\n if (rule.defaultSeverity === 'info') {\n throw new Error('Doctor does not support info-level repository diagnostics: ' + ruleId);\n }\n return { id: ruleId, severity: rule.defaultSeverity };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/doctor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Executable Ankh provider and standalone CLI for lightweight repo and package compliance diagnostics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -79,13 +79,15 @@
|
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@ankhorage/contracts": "^22.8.91",
|
|
82
|
+
"@ankhorage/dependency-graph": "^1.1.1",
|
|
83
|
+
"@ankhorage/rules-architecture": "^0.4.0",
|
|
84
|
+
"@ankhorage/rules-repository": "^0.2.0",
|
|
82
85
|
"@ankhorage/supabase-auth": "^1.2.27",
|
|
83
|
-
"@ankhorage/utility": "^1.9.77"
|
|
84
|
-
"@ankhorage/policy": "^0.6.0"
|
|
86
|
+
"@ankhorage/utility": "^1.9.77"
|
|
85
87
|
},
|
|
86
88
|
"devDependencies": {
|
|
87
|
-
"@ankhorage/ankh": "^0.10.
|
|
88
|
-
"@ankhorage/devtools": "^2.
|
|
89
|
+
"@ankhorage/ankh": "^0.10.83",
|
|
90
|
+
"@ankhorage/devtools": "^2.1.1",
|
|
89
91
|
"@ankhorage/paradox": "^0.2.25",
|
|
90
92
|
"@types/bun": "^1.4.2",
|
|
91
93
|
"@types/node": "^26.6.4",
|
package/src/analysis.ts
CHANGED
|
@@ -2,12 +2,12 @@ import { promises as fs } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { REPOSITORY_RULE_METADATA } from '@ankhorage/rules-repository';
|
|
6
6
|
import { uniqueSortedStrings } from '@ankhorage/utility/array';
|
|
7
7
|
|
|
8
8
|
import type { DoctorDiagnostic, DoctorPolicyProfile, DoctorRuleId } from './diagnostics.js';
|
|
9
9
|
import type { DoctorReadiness } from './readiness.js';
|
|
10
|
-
import {
|
|
10
|
+
import { getRepositoryRule } from './utils/getRepositoryRule.js';
|
|
11
11
|
|
|
12
12
|
export type DoctorTargetCheck = 'manifest' | 'package' | 'repo';
|
|
13
13
|
export type DoctorTargetMode = 'fix' | 'package' | 'repo' | 'validate';
|
|
@@ -67,7 +67,7 @@ const REPO_MARKERS = [
|
|
|
67
67
|
{ label: 'yarn.lock', relativePath: 'yarn.lock' },
|
|
68
68
|
] as const;
|
|
69
69
|
|
|
70
|
-
const PUBLIC_PACKAGE_POLICY =
|
|
70
|
+
const PUBLIC_PACKAGE_POLICY = REPOSITORY_RULE_METADATA.publicPackage;
|
|
71
71
|
|
|
72
72
|
const PROVIDER_SOURCE_CANDIDATES = [
|
|
73
73
|
path.join('src', 'ankh.provider.ts'),
|
|
@@ -348,7 +348,7 @@ async function analyzePackagePolicy(request: {
|
|
|
348
348
|
message: 'Public package profiles must not set private: true.',
|
|
349
349
|
path: request.packageJsonPath,
|
|
350
350
|
profile: request.profile,
|
|
351
|
-
...
|
|
351
|
+
...repositoryRuleFields(PUBLIC_PACKAGE_POLICY.privateDisallowedRuleId),
|
|
352
352
|
}),
|
|
353
353
|
);
|
|
354
354
|
}
|
|
@@ -363,7 +363,7 @@ async function analyzePackagePolicy(request: {
|
|
|
363
363
|
message: `package.json "type" must be "${PUBLIC_PACKAGE_POLICY.packageType.value}".`,
|
|
364
364
|
path: request.packageJsonPath,
|
|
365
365
|
profile: request.profile,
|
|
366
|
-
...
|
|
366
|
+
...repositoryRuleFields(PUBLIC_PACKAGE_POLICY.packageType.ruleId),
|
|
367
367
|
}),
|
|
368
368
|
);
|
|
369
369
|
}
|
|
@@ -379,7 +379,7 @@ async function analyzePackagePolicy(request: {
|
|
|
379
379
|
message: `package.json publishConfig.access must exist and equal "${PUBLIC_PACKAGE_POLICY.publishAccess.value}".`,
|
|
380
380
|
path: request.packageJsonPath,
|
|
381
381
|
profile: request.profile,
|
|
382
|
-
...
|
|
382
|
+
...repositoryRuleFields(PUBLIC_PACKAGE_POLICY.publishAccess.ruleId),
|
|
383
383
|
}),
|
|
384
384
|
);
|
|
385
385
|
}
|
|
@@ -391,7 +391,7 @@ async function analyzePackagePolicy(request: {
|
|
|
391
391
|
message: 'package.json must define a Bun-aligned packageManager field.',
|
|
392
392
|
path: request.packageJsonPath,
|
|
393
393
|
profile: request.profile,
|
|
394
|
-
...
|
|
394
|
+
...repositoryRuleFields(PUBLIC_PACKAGE_POLICY.packageManager.requiredRuleId),
|
|
395
395
|
}),
|
|
396
396
|
);
|
|
397
397
|
request.plannedChanges.push(
|
|
@@ -409,7 +409,7 @@ async function analyzePackagePolicy(request: {
|
|
|
409
409
|
message: 'package.json packageManager must be Bun-aligned, for example "bun@1.x".',
|
|
410
410
|
path: request.packageJsonPath,
|
|
411
411
|
profile: request.profile,
|
|
412
|
-
...
|
|
412
|
+
...repositoryRuleFields(PUBLIC_PACKAGE_POLICY.packageManager.bunRuleId),
|
|
413
413
|
}),
|
|
414
414
|
);
|
|
415
415
|
}
|
|
@@ -423,7 +423,7 @@ async function analyzePackagePolicy(request: {
|
|
|
423
423
|
message: `Missing required package script: ${requirement.name}`,
|
|
424
424
|
path: request.packageJsonPath,
|
|
425
425
|
profile: request.profile,
|
|
426
|
-
...
|
|
426
|
+
...repositoryRuleFields(requirement.ruleId),
|
|
427
427
|
}),
|
|
428
428
|
);
|
|
429
429
|
}
|
|
@@ -440,7 +440,7 @@ async function analyzePackagePolicy(request: {
|
|
|
440
440
|
message: 'Public package repos must declare TypeScript in dependencies or devDependencies.',
|
|
441
441
|
path: request.packageJsonPath,
|
|
442
442
|
profile: request.profile,
|
|
443
|
-
...
|
|
443
|
+
...repositoryRuleFields(PUBLIC_PACKAGE_POLICY.dependencyRules.typescript),
|
|
444
444
|
}),
|
|
445
445
|
);
|
|
446
446
|
}
|
|
@@ -463,7 +463,7 @@ async function analyzePackagePolicy(request: {
|
|
|
463
463
|
message: 'Public package repos must declare @types/bun in devDependencies.',
|
|
464
464
|
path: request.packageJsonPath,
|
|
465
465
|
profile: request.profile,
|
|
466
|
-
...
|
|
466
|
+
...repositoryRuleFields(PUBLIC_PACKAGE_POLICY.dependencyRules.bunTypes),
|
|
467
467
|
}),
|
|
468
468
|
);
|
|
469
469
|
}
|
|
@@ -475,7 +475,7 @@ async function analyzePackagePolicy(request: {
|
|
|
475
475
|
message: 'Public package repos must declare @types/node in devDependencies.',
|
|
476
476
|
path: request.packageJsonPath,
|
|
477
477
|
profile: request.profile,
|
|
478
|
-
...
|
|
478
|
+
...repositoryRuleFields(PUBLIC_PACKAGE_POLICY.dependencyRules.nodeTypes),
|
|
479
479
|
}),
|
|
480
480
|
);
|
|
481
481
|
}
|
|
@@ -488,7 +488,7 @@ async function analyzePackagePolicy(request: {
|
|
|
488
488
|
'This package consumes shared Ankhorage lint/format/knip tooling and must declare @ankhorage/devtools.',
|
|
489
489
|
path: request.packageJsonPath,
|
|
490
490
|
profile: request.profile,
|
|
491
|
-
...
|
|
491
|
+
...repositoryRuleFields(PUBLIC_PACKAGE_POLICY.dependencyRules.devtools),
|
|
492
492
|
}),
|
|
493
493
|
);
|
|
494
494
|
}
|
|
@@ -504,7 +504,7 @@ async function analyzePackagePolicy(request: {
|
|
|
504
504
|
message: 'This package owns Paradox docs generation and must declare @ankhorage/paradox.',
|
|
505
505
|
path: request.packageJsonPath,
|
|
506
506
|
profile: request.profile,
|
|
507
|
-
...
|
|
507
|
+
...repositoryRuleFields(PUBLIC_PACKAGE_POLICY.dependencyRules.paradox),
|
|
508
508
|
}),
|
|
509
509
|
);
|
|
510
510
|
}
|
|
@@ -546,7 +546,7 @@ function createChangesetsDependencyDiagnostic(request: {
|
|
|
546
546
|
: 'Public package repos must not declare @changesets/cli directly; @ankhorage/devtools owns Changesets execution.',
|
|
547
547
|
path: request.packageJsonPath,
|
|
548
548
|
profile: request.profile,
|
|
549
|
-
...
|
|
549
|
+
...repositoryRuleFields(PUBLIC_PACKAGE_POLICY.dependencyRules.changesets),
|
|
550
550
|
});
|
|
551
551
|
}
|
|
552
552
|
|
|
@@ -943,7 +943,7 @@ async function maybeRequirePath(
|
|
|
943
943
|
message: `Missing expected repo artifact: ${relativePath}`,
|
|
944
944
|
path: absolutePath,
|
|
945
945
|
profile: 'public-package',
|
|
946
|
-
...
|
|
946
|
+
...repositoryRuleFields(ruleId),
|
|
947
947
|
}),
|
|
948
948
|
);
|
|
949
949
|
plannedChanges.push(
|
|
@@ -974,7 +974,7 @@ function requireNonEmptyStringField(request: {
|
|
|
974
974
|
message: `package.json must define a non-empty "${request.fieldName}" field.`,
|
|
975
975
|
path: request.packageJsonPath,
|
|
976
976
|
profile: request.profile,
|
|
977
|
-
...
|
|
977
|
+
...repositoryRuleFields(request.ruleId),
|
|
978
978
|
}),
|
|
979
979
|
);
|
|
980
980
|
}
|
|
@@ -997,7 +997,7 @@ function requireRecordField(request: {
|
|
|
997
997
|
message: `package.json must define an object "${request.fieldName}" field.`,
|
|
998
998
|
path: request.packageJsonPath,
|
|
999
999
|
profile: request.profile,
|
|
1000
|
-
...
|
|
1000
|
+
...repositoryRuleFields(request.ruleId),
|
|
1001
1001
|
}),
|
|
1002
1002
|
);
|
|
1003
1003
|
}
|
|
@@ -1020,7 +1020,7 @@ function requireStringArrayField(request: {
|
|
|
1020
1020
|
message: `package.json must define a non-empty string array "${request.fieldName}" field.`,
|
|
1021
1021
|
path: request.packageJsonPath,
|
|
1022
1022
|
profile: request.profile,
|
|
1023
|
-
...
|
|
1023
|
+
...repositoryRuleFields(request.ruleId),
|
|
1024
1024
|
}),
|
|
1025
1025
|
);
|
|
1026
1026
|
}
|
|
@@ -1113,11 +1113,9 @@ function createEarlyResult(targetPath: string, diagnostic: DoctorDiagnostic): Do
|
|
|
1113
1113
|
};
|
|
1114
1114
|
}
|
|
1115
1115
|
|
|
1116
|
-
/*** Return canonical rule id and severity
|
|
1117
|
-
function
|
|
1118
|
-
ruleId
|
|
1119
|
-
): Pick<DoctorDiagnostic, 'ruleId' | 'severity'> {
|
|
1120
|
-
const rule = getArchitecturePolicyRule(ruleId);
|
|
1116
|
+
/*** Return canonical rule id and severity from the repository Rules provider. */
|
|
1117
|
+
function repositoryRuleFields(ruleId: DoctorRuleId): Pick<DoctorDiagnostic, 'ruleId' | 'severity'> {
|
|
1118
|
+
const rule = getRepositoryRule(ruleId);
|
|
1121
1119
|
return { ruleId: rule.id, severity: rule.severity };
|
|
1122
1120
|
}
|
|
1123
1121
|
|
package/src/bunPolicyAnalysis.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { REPOSITORY_RULE_IDS, REPOSITORY_RULE_METADATA } from '@ankhorage/rules-repository';
|
|
5
5
|
|
|
6
6
|
import type { DoctorAnalysisResult } from './analysis.js';
|
|
7
7
|
import type { DoctorDiagnostic } from './diagnostics.js';
|
|
8
|
+
import { getRepositoryRule } from './utils/getRepositoryRule.js';
|
|
8
9
|
|
|
9
|
-
const BUN_POLICY =
|
|
10
|
+
const BUN_POLICY = REPOSITORY_RULE_METADATA.runtime.bun;
|
|
10
11
|
const REPAIR_HINT = 'Run "ankh devtools sync" to repair the managed Bun state.';
|
|
11
12
|
const BUN_VERSION_PATTERN = /^\s*bun-version:\s*['"]?([^'"\s#]+)['"]?\s*(?:#.*)?$/mu;
|
|
12
13
|
|
|
13
|
-
type
|
|
14
|
+
type RepositoryRule = ReturnType<typeof getRepositoryRule>;
|
|
14
15
|
type WorkflowTarget = (typeof BUN_POLICY.workflowTargets)[number];
|
|
15
16
|
|
|
16
17
|
/***
|
|
@@ -53,7 +54,7 @@ function analyzePackageBunPolicy(
|
|
|
53
54
|
location: 'package.json#packageManager',
|
|
54
55
|
path: packageJsonPath,
|
|
55
56
|
profile: result.profile,
|
|
56
|
-
rule:
|
|
57
|
+
rule: getRepositoryRule(REPOSITORY_RULE_IDS.bunPackageManager),
|
|
57
58
|
});
|
|
58
59
|
}
|
|
59
60
|
|
|
@@ -68,7 +69,7 @@ function analyzePackageBunPolicy(
|
|
|
68
69
|
location: 'package.json#devDependencies.@types/bun',
|
|
69
70
|
path: packageJsonPath,
|
|
70
71
|
profile: result.profile,
|
|
71
|
-
rule:
|
|
72
|
+
rule: getRepositoryRule(REPOSITORY_RULE_IDS.bunTypes),
|
|
72
73
|
});
|
|
73
74
|
}
|
|
74
75
|
|
|
@@ -152,7 +153,7 @@ function pushMismatch(
|
|
|
152
153
|
readonly location: string;
|
|
153
154
|
readonly path: string;
|
|
154
155
|
readonly profile: DoctorAnalysisResult['profile'];
|
|
155
|
-
readonly rule:
|
|
156
|
+
readonly rule: RepositoryRule;
|
|
156
157
|
},
|
|
157
158
|
): void {
|
|
158
159
|
if (input.actual === input.expected) return;
|
|
@@ -167,17 +168,6 @@ function pushMismatch(
|
|
|
167
168
|
});
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
/***
|
|
171
|
-
* Resolves one stable repository rule by its Policy-owned id.
|
|
172
|
-
*/
|
|
173
|
-
function getRepositoryRule(ruleId: string): RepositoryPolicyRule {
|
|
174
|
-
const rule = Object.values(REPOSITORY_POLICY.rules).find((entry) => entry.id === ruleId);
|
|
175
|
-
if (rule === undefined) {
|
|
176
|
-
throw new Error(`Unknown repository policy rule: ${ruleId}`);
|
|
177
|
-
}
|
|
178
|
-
return rule;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
171
|
/***
|
|
182
172
|
* Copies updated diagnostics into the analysis result and optional fix plan.
|
|
183
173
|
*/
|