@ankhorage/doctor 0.11.95 → 0.12.0

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 (34) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/dist/analysis.js +22 -22
  4. package/dist/analysis.js.map +1 -1
  5. package/dist/bunPolicyAnalysis.d.ts.map +1 -1
  6. package/dist/bunPolicyAnalysis.js +5 -14
  7. package/dist/bunPolicyAnalysis.js.map +1 -1
  8. package/dist/cliLayoutAnalysis.d.ts.map +1 -1
  9. package/dist/cliLayoutAnalysis.js +39 -29
  10. package/dist/cliLayoutAnalysis.js.map +1 -1
  11. package/dist/repositorySourceRulesAnalysis.d.ts +16 -0
  12. package/dist/repositorySourceRulesAnalysis.d.ts.map +1 -0
  13. package/dist/repositorySourceRulesAnalysis.js +103 -0
  14. package/dist/repositorySourceRulesAnalysis.js.map +1 -0
  15. package/dist/sourceArchitectureAnalysis.d.ts +3 -6
  16. package/dist/sourceArchitectureAnalysis.d.ts.map +1 -1
  17. package/dist/sourceArchitectureAnalysis.js +93 -97
  18. package/dist/sourceArchitectureAnalysis.js.map +1 -1
  19. package/dist/utils/getRepositoryRule.d.ts +9 -0
  20. package/dist/utils/getRepositoryRule.d.ts.map +1 -0
  21. package/dist/utils/getRepositoryRule.js +15 -0
  22. package/dist/utils/getRepositoryRule.js.map +1 -0
  23. package/package.json +7 -5
  24. package/src/analysis.ts +22 -24
  25. package/src/bunPolicyAnalysis.ts +7 -17
  26. package/src/cliLayoutAnalysis.ts +51 -30
  27. package/src/repositorySourceRulesAnalysis.ts +153 -0
  28. package/src/sourceArchitectureAnalysis.ts +102 -177
  29. package/src/utils/getRepositoryRule.ts +20 -0
  30. package/dist/utils/getArchitecturePolicyRule.d.ts +0 -286
  31. package/dist/utils/getArchitecturePolicyRule.d.ts.map +0 -1
  32. package/dist/utils/getArchitecturePolicyRule.js +0 -12
  33. package/dist/utils/getArchitecturePolicyRule.js.map +0 -1
  34. 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 { ARCHITECTURE_POLICY } from '@ankhorage/policy/architecture';
4
- import { getArchitecturePolicyRule } from './utils/getArchitecturePolicyRule.js';
5
- const SOURCE_POLICY = ARCHITECTURE_POLICY.source;
6
- /*** Validate folder-role combinations and inward source dependency direction. */
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
- return [...(await analyzeDirectoryVocabularyAsync(input)), ...analyzeImportDirection(input)];
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
- /*** Validate architectural directory names only when a repository has introduced them. */
11
- async function analyzeDirectoryVocabularyAsync(input) {
12
- const diagnostics = [];
13
- const sourceRoot = path.join(input.targetPath, 'src');
14
- for (const directoryName of SOURCE_POLICY.catchAllDirectories) {
15
- const directoryPath = path.join(sourceRoot, directoryName);
16
- if (await pathExistsAsync(directoryPath)) {
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
- }
19
- }
20
- // Folder-role combination enforcement stays deferred until rules-architecture is canonical.
21
- return diagnostics;
22
- }
23
- /*** Validate that relative source imports point inward across recognized architecture roles. */
24
- function analyzeImportDirection(input) {
25
- return input.activeSourceImports.flatMap((sourceImport) => analyzeOneImportDirection(input, sourceImport));
26
- }
27
- /*** Validate one relative import against repository and layer boundaries. */
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
- /*** Resolve the dependency rule owned by one recognized inner source role. */
61
- function resolveSourceRole(sourceSegments) {
62
- for (const role of Object.values(SOURCE_POLICY.roles)) {
63
- if (sourceSegments.some((segment) => role.segments.some((roleSegment) => roleSegment === segment))) {
64
- return {
65
- forbiddenSegments: new Set(role.forbiddenOutwardSegments),
66
- label: role.label,
67
- rule: getArchitecturePolicyRule(role.ruleId),
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
- return null;
72
- }
73
- /*** Report one outward dependency when the imported path crosses a forbidden role. */
74
- function createOutwardImportDiagnostic(input, sourceImport, targetSegments, role) {
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
- /*** Build one deterministic architecture diagnostic from Policy-owned metadata. */
91
- function createDiagnostic(input, diagnosticPath, rule, message) {
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: diagnosticPath,
82
+ message: finding.message,
83
+ path: path.resolve(input.targetPath, subjectPath),
96
84
  profile: input.profile,
97
- ruleId: rule.id,
98
- severity: rule.severity,
85
+ ruleId: toDoctorRuleId(finding.ruleId),
86
+ severity: toDoctorSeverity(finding.severity),
99
87
  };
100
88
  }
101
- /*** Check whether a filesystem path exists. */
102
- async function pathExistsAsync(targetPath) {
103
- try {
104
- await fs.access(targetPath);
105
- return true;
106
- }
107
- catch {
108
- return false;
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.11.95",
3
+ "version": "0.12.0",
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.80",
88
- "@ankhorage/devtools": "^2.0.114",
89
+ "@ankhorage/ankh": "^0.10.81",
90
+ "@ankhorage/devtools": "^2.0.115",
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 { ARCHITECTURE_POLICY } from '@ankhorage/policy/architecture';
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 { getArchitecturePolicyRule } from './utils/getArchitecturePolicyRule.js';
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 = ARCHITECTURE_POLICY.publicPackage;
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
- ...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.privateDisallowedRuleId),
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
- ...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.packageType.ruleId),
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
- ...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.publishAccess.ruleId),
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
- ...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.packageManager.requiredRuleId),
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
- ...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.packageManager.bunRuleId),
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
- ...architecturePolicyRuleFields(requirement.ruleId),
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
- ...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.dependencyRules.typescript),
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
- ...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.dependencyRules.bunTypes),
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
- ...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.dependencyRules.nodeTypes),
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
- ...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.dependencyRules.devtools),
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
- ...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.dependencyRules.paradox),
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
- ...architecturePolicyRuleFields(PUBLIC_PACKAGE_POLICY.dependencyRules.changesets),
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
- ...architecturePolicyRuleFields(ruleId),
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
- ...architecturePolicyRuleFields(request.ruleId),
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
- ...architecturePolicyRuleFields(request.ruleId),
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
- ...architecturePolicyRuleFields(request.ruleId),
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 for a Policy-owned generic package rule. */
1117
- function architecturePolicyRuleFields(
1118
- ruleId: DoctorRuleId,
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
 
@@ -1,16 +1,17 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
- import { REPOSITORY_POLICY } from '@ankhorage/policy/repository';
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 = REPOSITORY_POLICY.runtime.bun;
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 RepositoryPolicyRule = (typeof REPOSITORY_POLICY.rules)[keyof typeof REPOSITORY_POLICY.rules];
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: REPOSITORY_POLICY.rules.packageManager,
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: REPOSITORY_POLICY.rules.bunTypes,
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: RepositoryPolicyRule;
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
  */