@ankhorage/doctor 0.11.2 → 0.11.4

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.
@@ -1,62 +1,41 @@
1
1
  import { promises as fs } from 'node:fs';
2
- import { createRequire } from 'node:module';
3
2
  import path from 'node:path';
4
- import { pathToFileURL } from 'node:url';
5
- const DEVTOOLS_POLICY_SPECIFIER = '@ankhorage/devtools/policy';
3
+ import { REPOSITORY_POLICY } from '@ankhorage/policy/repository';
4
+ const BUN_POLICY = REPOSITORY_POLICY.runtime.bun;
6
5
  const REPAIR_HINT = 'Run "ankh devtools sync" to repair the managed Bun state.';
7
6
  const BUN_VERSION_PATTERN = /^\s*bun-version:\s*['"]?([^'"\s#]+)['"]?\s*(?:#.*)?$/mu;
8
- const WORKFLOWS = [
9
- {
10
- relativePath: path.join('.github', 'workflows', 'ci.yml'),
11
- ruleId: 'repo.workflows.ci.bun-policy',
12
- },
13
- {
14
- relativePath: path.join('.github', 'workflows', 'release.yml'),
15
- ruleId: 'repo.workflows.release.bun-policy',
16
- },
17
- ];
7
+ /***
8
+ * Applies canonical repository Bun-policy diagnostics to a Doctor analysis result.
9
+ */
18
10
  export async function applyBunRuntimePolicy(result) {
19
11
  if (result.profile !== 'public-package' || !result.hasPackageJson) {
20
12
  return result;
21
13
  }
22
- const policy = await loadTargetBunRuntimePolicy(result.targetPath);
23
14
  const packageJsonPath = path.join(result.targetPath, 'package.json');
24
15
  const packageJson = await readPackageJson(packageJsonPath);
25
- if (policy === null || packageJson === null) {
16
+ if (packageJson === null)
26
17
  return result;
27
- }
28
18
  const diagnostics = [
29
19
  ...result.diagnostics,
30
- ...analyzePackageBunPolicy(packageJson, packageJsonPath, result, policy),
31
- ...(await analyzeWorkflowBunPolicy(result, policy)),
20
+ ...analyzePackageBunPolicy(packageJson, packageJsonPath, result),
21
+ ...(await analyzeWorkflowBunPolicy(result)),
32
22
  ];
33
23
  return withDiagnostics(result, diagnostics);
34
24
  }
35
- async function loadTargetBunRuntimePolicy(targetPath) {
36
- const requireFromTarget = createRequire(path.join(targetPath, 'package.json'));
37
- try {
38
- const modulePath = requireFromTarget.resolve(DEVTOOLS_POLICY_SPECIFIER);
39
- const importedModule = (await import(pathToFileURL(modulePath).href));
40
- return parseBunRuntimePolicy(importedModule);
41
- }
42
- catch (error) {
43
- if (isUnavailablePolicyError(error)) {
44
- return null;
45
- }
46
- throw error;
47
- }
48
- }
49
- function analyzePackageBunPolicy(packageJson, packageJsonPath, result, policy) {
25
+ /***
26
+ * Reports package-manifest drift from the canonical Bun runtime contract.
27
+ */
28
+ function analyzePackageBunPolicy(packageJson, packageJsonPath, result) {
50
29
  const diagnostics = [];
51
30
  const { packageManager } = packageJson;
52
31
  if (isNonEmptyString(packageManager) && packageManager.startsWith('bun@')) {
53
32
  pushMismatch(diagnostics, {
54
33
  actual: packageManager,
55
- expected: policy.packageManager,
34
+ expected: BUN_POLICY.packageManager,
56
35
  location: 'package.json#packageManager',
57
36
  path: packageJsonPath,
58
37
  profile: result.profile,
59
- ruleId: 'package.json.package-manager.policy',
38
+ rule: REPOSITORY_POLICY.rules.packageManager,
60
39
  });
61
40
  }
62
41
  const devDependencies = isRecord(packageJson.devDependencies)
@@ -66,78 +45,92 @@ function analyzePackageBunPolicy(packageJson, packageJsonPath, result, policy) {
66
45
  if (isNonEmptyString(bunTypes)) {
67
46
  pushMismatch(diagnostics, {
68
47
  actual: bunTypes,
69
- expected: policy.typesRange,
48
+ expected: BUN_POLICY.typesRange,
70
49
  location: 'package.json#devDependencies.@types/bun',
71
50
  path: packageJsonPath,
72
51
  profile: result.profile,
73
- ruleId: 'package.dependencies.types-bun.policy',
52
+ rule: REPOSITORY_POLICY.rules.bunTypes,
74
53
  });
75
54
  }
76
55
  return diagnostics;
77
56
  }
78
- async function analyzeWorkflowBunPolicy(result, policy) {
79
- const diagnostics = await Promise.all(WORKFLOWS.map(async (workflow) => await analyzeWorkflow(result, workflow, policy)));
57
+ /***
58
+ * Reports managed workflow drift from the canonical Bun setup version.
59
+ */
60
+ async function analyzeWorkflowBunPolicy(result) {
61
+ const diagnostics = await Promise.all(BUN_POLICY.workflowTargets.map(async (workflow) => await analyzeWorkflow(result, workflow)));
80
62
  return diagnostics.flat();
81
63
  }
82
- async function analyzeWorkflow(result, workflow, policy) {
83
- const workflowPath = path.join(result.targetPath, workflow.relativePath);
64
+ /***
65
+ * Checks one managed workflow for the canonical Bun setup version.
66
+ */
67
+ async function analyzeWorkflow(result, workflow) {
68
+ const workflowPath = path.join(result.targetPath, workflow.path);
84
69
  const contents = await readTextOrNull(workflowPath);
85
70
  if (contents === null) {
86
- return [createWorkflowDiagnostic(result, workflow, policy, workflowPath, 'missing-path')];
71
+ return [createWorkflowDiagnostic(result, workflow, workflowPath, 'missing-path')];
87
72
  }
88
73
  const match = BUN_VERSION_PATTERN.exec(contents);
89
74
  const actual = match?.[1];
90
75
  if (!isNonEmptyString(actual)) {
91
- return [createWorkflowDiagnostic(result, workflow, policy, workflowPath, 'field-missing')];
76
+ return [createWorkflowDiagnostic(result, workflow, workflowPath, 'field-missing')];
92
77
  }
93
78
  const diagnostics = [];
94
79
  pushMismatch(diagnostics, {
95
80
  actual,
96
- expected: policy.version,
97
- location: `${workflow.relativePath}#bun-version`,
81
+ expected: BUN_POLICY.version,
82
+ location: `${workflow.path}#bun-version`,
98
83
  path: workflowPath,
99
84
  profile: result.profile,
100
- ruleId: workflow.ruleId,
85
+ rule: getRepositoryRule(workflow.ruleId),
101
86
  });
102
87
  return diagnostics;
103
88
  }
104
- function createWorkflowDiagnostic(result, workflow, policy, workflowPath, code) {
89
+ /***
90
+ * Creates a missing or malformed managed-workflow diagnostic from central rule metadata.
91
+ */
92
+ function createWorkflowDiagnostic(result, workflow, workflowPath, code) {
105
93
  const detail = code === 'missing-path'
106
- ? `Managed workflow is missing: ${workflow.relativePath}.`
107
- : `${workflow.relativePath} does not define bun-version.`;
94
+ ? `Managed workflow is missing: ${workflow.path}.`
95
+ : `${workflow.path} does not define bun-version.`;
96
+ const rule = getRepositoryRule(workflow.ruleId);
108
97
  return {
109
98
  code,
110
- message: `${detail} Expected Bun ${policy.version}. ${REPAIR_HINT}`,
99
+ message: `${detail} Expected Bun ${BUN_POLICY.version}. ${REPAIR_HINT}`,
111
100
  path: workflowPath,
112
101
  profile: result.profile,
113
- ruleId: workflow.ruleId,
114
- severity: 'error',
102
+ ruleId: rule.id,
103
+ severity: rule.severity,
115
104
  };
116
105
  }
106
+ /***
107
+ * Adds one value-mismatch diagnostic using the owning Policy rule metadata.
108
+ */
117
109
  function pushMismatch(diagnostics, input) {
118
- if (input.actual === input.expected) {
110
+ if (input.actual === input.expected)
119
111
  return;
120
- }
121
112
  diagnostics.push({
122
113
  code: 'field-invalid',
123
114
  message: `Bun policy drift at ${input.location}: expected "${input.expected}", found "${input.actual}". ${REPAIR_HINT}`,
124
115
  path: input.path,
125
116
  profile: input.profile,
126
- ruleId: input.ruleId,
127
- severity: 'error',
117
+ ruleId: input.rule.id,
118
+ severity: input.rule.severity,
128
119
  });
129
120
  }
130
- function parseBunRuntimePolicy(importedModule) {
131
- if (!isRecord(importedModule) || !isRecord(importedModule.bunRuntimePolicy)) {
132
- return null;
121
+ /***
122
+ * Resolves one stable repository rule by its Policy-owned id.
123
+ */
124
+ function getRepositoryRule(ruleId) {
125
+ const rule = Object.values(REPOSITORY_POLICY.rules).find((entry) => entry.id === ruleId);
126
+ if (rule === undefined) {
127
+ throw new Error(`Unknown repository policy rule: ${ruleId}`);
133
128
  }
134
- const { packageManager, typesRange, version } = importedModule.bunRuntimePolicy;
135
- return isNonEmptyString(packageManager) &&
136
- isNonEmptyString(typesRange) &&
137
- isNonEmptyString(version)
138
- ? { packageManager, typesRange, version }
139
- : null;
129
+ return rule;
140
130
  }
131
+ /***
132
+ * Copies updated diagnostics into the analysis result and optional fix plan.
133
+ */
141
134
  function withDiagnostics(result, diagnostics) {
142
135
  return {
143
136
  ...result,
@@ -145,6 +138,9 @@ function withDiagnostics(result, diagnostics) {
145
138
  fixPlan: result.fixPlan === null ? null : { ...result.fixPlan, diagnostics },
146
139
  };
147
140
  }
141
+ /***
142
+ * Reads a package manifest when it contains a JSON object.
143
+ */
148
144
  async function readPackageJson(packageJsonPath) {
149
145
  try {
150
146
  const parsed = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
@@ -154,36 +150,34 @@ async function readPackageJson(packageJsonPath) {
154
150
  return null;
155
151
  }
156
152
  }
153
+ /***
154
+ * Reads a UTF-8 file while treating a missing managed path as absent.
155
+ */
157
156
  async function readTextOrNull(filePath) {
158
157
  try {
159
158
  return await fs.readFile(filePath, 'utf8');
160
159
  }
161
160
  catch (error) {
162
- if (isNodeError(error) && error.code === 'ENOENT') {
161
+ if (isNodeError(error) && error.code === 'ENOENT')
163
162
  return null;
164
- }
165
163
  throw error;
166
164
  }
167
165
  }
168
- function isUnavailablePolicyError(error) {
169
- if (isNodeError(error) &&
170
- (error.code === 'MODULE_NOT_FOUND' ||
171
- error.code === 'ERR_MODULE_NOT_FOUND' ||
172
- error.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED')) {
173
- return true;
174
- }
175
- const message = error instanceof Error ? error.message : String(error);
176
- return (message.includes(DEVTOOLS_POLICY_SPECIFIER) &&
177
- (message.includes('Cannot find module') ||
178
- message.includes('not exported') ||
179
- message.includes('Package subpath')));
166
+ /***
167
+ * Narrows unknown JSON data to a non-array record.
168
+ */
169
+ function isRecord(value) {
170
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
180
171
  }
172
+ /***
173
+ * Narrows a value to a non-empty string.
174
+ */
181
175
  function isNonEmptyString(value) {
182
176
  return typeof value === 'string' && value.trim() !== '';
183
177
  }
184
- function isRecord(value) {
185
- return typeof value === 'object' && value !== null && !Array.isArray(value);
186
- }
178
+ /***
179
+ * Narrows unknown failures to Node errors with stable error codes.
180
+ */
187
181
  function isNodeError(error) {
188
182
  return error instanceof Error && 'code' in error;
189
183
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bunPolicyAnalysis.js","sourceRoot":"","sources":["../src/bunPolicyAnalysis.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport type { DoctorAnalysisResult } from './analysis.js';\nimport type { DoctorDiagnostic, DoctorRuleId } from './diagnostics.js';\n\ninterface BunRuntimePolicySnapshot {\n readonly packageManager: string;\n readonly typesRange: string;\n readonly version: string;\n}\n\nconst DEVTOOLS_POLICY_SPECIFIER = '@ankhorage/devtools/policy';\nconst REPAIR_HINT = 'Run \"ankh devtools sync\" to repair the managed Bun state.';\nconst BUN_VERSION_PATTERN = /^\\s*bun-version:\\s*['\"]?([^'\"\\s#]+)['\"]?\\s*(?:#.*)?$/mu;\nconst WORKFLOWS = [\n {\n relativePath: path.join('.github', 'workflows', 'ci.yml'),\n ruleId: 'repo.workflows.ci.bun-policy',\n },\n {\n relativePath: path.join('.github', 'workflows', 'release.yml'),\n ruleId: 'repo.workflows.release.bun-policy',\n },\n] as const satisfies readonly { readonly relativePath: string; readonly ruleId: DoctorRuleId }[];\n\nexport async function applyBunRuntimePolicy(\n result: DoctorAnalysisResult,\n): Promise<DoctorAnalysisResult> {\n if (result.profile !== 'public-package' || !result.hasPackageJson) {\n return result;\n }\n\n const policy = await loadTargetBunRuntimePolicy(result.targetPath);\n const packageJsonPath = path.join(result.targetPath, 'package.json');\n const packageJson = await readPackageJson(packageJsonPath);\n if (policy === null || packageJson === null) {\n return result;\n }\n\n const diagnostics = [\n ...result.diagnostics,\n ...analyzePackageBunPolicy(packageJson, packageJsonPath, result, policy),\n ...(await analyzeWorkflowBunPolicy(result, policy)),\n ];\n\n return withDiagnostics(result, diagnostics);\n}\n\nasync function loadTargetBunRuntimePolicy(\n targetPath: string,\n): Promise<BunRuntimePolicySnapshot | null> {\n const requireFromTarget = createRequire(path.join(targetPath, 'package.json'));\n try {\n const modulePath = requireFromTarget.resolve(DEVTOOLS_POLICY_SPECIFIER);\n const importedModule = (await import(pathToFileURL(modulePath).href)) as unknown;\n return parseBunRuntimePolicy(importedModule);\n } catch (error) {\n if (isUnavailablePolicyError(error)) {\n return null;\n }\n throw error;\n }\n}\n\nfunction analyzePackageBunPolicy(\n packageJson: Record<string, unknown>,\n packageJsonPath: string,\n result: DoctorAnalysisResult,\n policy: BunRuntimePolicySnapshot,\n): DoctorDiagnostic[] {\n const diagnostics: DoctorDiagnostic[] = [];\n const { packageManager } = packageJson;\n if (isNonEmptyString(packageManager) && packageManager.startsWith('bun@')) {\n pushMismatch(diagnostics, {\n actual: packageManager,\n expected: policy.packageManager,\n location: 'package.json#packageManager',\n path: packageJsonPath,\n profile: result.profile,\n ruleId: 'package.json.package-manager.policy',\n });\n }\n\n const devDependencies = isRecord(packageJson.devDependencies)\n ? packageJson.devDependencies\n : null;\n const bunTypes = devDependencies?.['@types/bun'];\n if (isNonEmptyString(bunTypes)) {\n pushMismatch(diagnostics, {\n actual: bunTypes,\n expected: policy.typesRange,\n location: 'package.json#devDependencies.@types/bun',\n path: packageJsonPath,\n profile: result.profile,\n ruleId: 'package.dependencies.types-bun.policy',\n });\n }\n\n return diagnostics;\n}\n\nasync function analyzeWorkflowBunPolicy(\n result: DoctorAnalysisResult,\n policy: BunRuntimePolicySnapshot,\n): Promise<DoctorDiagnostic[]> {\n const diagnostics = await Promise.all(\n WORKFLOWS.map(async (workflow) => await analyzeWorkflow(result, workflow, policy)),\n );\n return diagnostics.flat();\n}\n\nasync function analyzeWorkflow(\n result: DoctorAnalysisResult,\n workflow: (typeof WORKFLOWS)[number],\n policy: BunRuntimePolicySnapshot,\n): Promise<DoctorDiagnostic[]> {\n const workflowPath = path.join(result.targetPath, workflow.relativePath);\n const contents = await readTextOrNull(workflowPath);\n if (contents === null) {\n return [createWorkflowDiagnostic(result, workflow, policy, workflowPath, 'missing-path')];\n }\n\n const match = BUN_VERSION_PATTERN.exec(contents);\n const actual = match?.[1];\n if (!isNonEmptyString(actual)) {\n return [createWorkflowDiagnostic(result, workflow, policy, workflowPath, 'field-missing')];\n }\n\n const diagnostics: DoctorDiagnostic[] = [];\n pushMismatch(diagnostics, {\n actual,\n expected: policy.version,\n location: `${workflow.relativePath}#bun-version`,\n path: workflowPath,\n profile: result.profile,\n ruleId: workflow.ruleId,\n });\n return diagnostics;\n}\n\nfunction createWorkflowDiagnostic(\n result: DoctorAnalysisResult,\n workflow: (typeof WORKFLOWS)[number],\n policy: BunRuntimePolicySnapshot,\n workflowPath: string,\n code: DoctorDiagnostic['code'],\n): DoctorDiagnostic {\n const detail =\n code === 'missing-path'\n ? `Managed workflow is missing: ${workflow.relativePath}.`\n : `${workflow.relativePath} does not define bun-version.`;\n return {\n code,\n message: `${detail} Expected Bun ${policy.version}. ${REPAIR_HINT}`,\n path: workflowPath,\n profile: result.profile,\n ruleId: workflow.ruleId,\n severity: 'error',\n };\n}\n\nfunction pushMismatch(\n diagnostics: DoctorDiagnostic[],\n input: {\n readonly actual: string;\n readonly expected: string;\n readonly location: string;\n readonly path: string;\n readonly profile: DoctorAnalysisResult['profile'];\n readonly ruleId: DoctorRuleId;\n },\n): void {\n if (input.actual === input.expected) {\n return;\n }\n diagnostics.push({\n code: 'field-invalid',\n message: `Bun policy drift at ${input.location}: expected \"${input.expected}\", found \"${input.actual}\". ${REPAIR_HINT}`,\n path: input.path,\n profile: input.profile,\n ruleId: input.ruleId,\n severity: 'error',\n });\n}\n\nfunction parseBunRuntimePolicy(importedModule: unknown): BunRuntimePolicySnapshot | null {\n if (!isRecord(importedModule) || !isRecord(importedModule.bunRuntimePolicy)) {\n return null;\n }\n const { packageManager, typesRange, version } = importedModule.bunRuntimePolicy;\n return isNonEmptyString(packageManager) &&\n isNonEmptyString(typesRange) &&\n isNonEmptyString(version)\n ? { packageManager, typesRange, version }\n : null;\n}\n\nfunction withDiagnostics(\n result: DoctorAnalysisResult,\n diagnostics: readonly DoctorDiagnostic[],\n): DoctorAnalysisResult {\n return {\n ...result,\n diagnostics,\n fixPlan: result.fixPlan === null ? null : { ...result.fixPlan, diagnostics },\n };\n}\n\nasync function readPackageJson(packageJsonPath: string): Promise<Record<string, unknown> | null> {\n try {\n const parsed = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) as unknown;\n return isRecord(parsed) ? parsed : null;\n } catch {\n return null;\n }\n}\n\nasync function readTextOrNull(filePath: string): Promise<string | null> {\n try {\n return await fs.readFile(filePath, 'utf8');\n } catch (error) {\n if (isNodeError(error) && error.code === 'ENOENT') {\n return null;\n }\n throw error;\n }\n}\n\nfunction isUnavailablePolicyError(error: unknown): boolean {\n if (\n isNodeError(error) &&\n (error.code === 'MODULE_NOT_FOUND' ||\n error.code === 'ERR_MODULE_NOT_FOUND' ||\n error.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED')\n ) {\n return true;\n }\n\n const message = error instanceof Error ? error.message : String(error);\n return (\n message.includes(DEVTOOLS_POLICY_SPECIFIER) &&\n (message.includes('Cannot find module') ||\n message.includes('not exported') ||\n message.includes('Package subpath'))\n );\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.trim() !== '';\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isNodeError(error: unknown): error is NodeJS.ErrnoException {\n return error instanceof Error && 'code' in error;\n}\n"]}
1
+ {"version":3,"file":"bunPolicyAnalysis.js","sourceRoot":"","sources":["../src/bunPolicyAnalysis.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\n\nimport { REPOSITORY_POLICY } from '@ankhorage/policy/repository';\n\nimport type { DoctorAnalysisResult } from './analysis.js';\nimport type { DoctorDiagnostic } from './diagnostics.js';\n\nconst BUN_POLICY = REPOSITORY_POLICY.runtime.bun;\nconst REPAIR_HINT = 'Run \"ankh devtools sync\" to repair the managed Bun state.';\nconst BUN_VERSION_PATTERN = /^\\s*bun-version:\\s*['\"]?([^'\"\\s#]+)['\"]?\\s*(?:#.*)?$/mu;\n\ntype RepositoryPolicyRule = (typeof REPOSITORY_POLICY.rules)[keyof typeof REPOSITORY_POLICY.rules];\ntype WorkflowTarget = (typeof BUN_POLICY.workflowTargets)[number];\n\n/***\n * Applies canonical repository Bun-policy diagnostics to a Doctor analysis result.\n */\nexport async function applyBunRuntimePolicy(\n result: DoctorAnalysisResult,\n): Promise<DoctorAnalysisResult> {\n if (result.profile !== 'public-package' || !result.hasPackageJson) {\n return result;\n }\n\n const packageJsonPath = path.join(result.targetPath, 'package.json');\n const packageJson = await readPackageJson(packageJsonPath);\n if (packageJson === null) return result;\n\n const diagnostics = [\n ...result.diagnostics,\n ...analyzePackageBunPolicy(packageJson, packageJsonPath, result),\n ...(await analyzeWorkflowBunPolicy(result)),\n ];\n\n return withDiagnostics(result, diagnostics);\n}\n\n/***\n * Reports package-manifest drift from the canonical Bun runtime contract.\n */\nfunction analyzePackageBunPolicy(\n packageJson: Record<string, unknown>,\n packageJsonPath: string,\n result: DoctorAnalysisResult,\n): DoctorDiagnostic[] {\n const diagnostics: DoctorDiagnostic[] = [];\n const { packageManager } = packageJson;\n if (isNonEmptyString(packageManager) && packageManager.startsWith('bun@')) {\n pushMismatch(diagnostics, {\n actual: packageManager,\n expected: BUN_POLICY.packageManager,\n location: 'package.json#packageManager',\n path: packageJsonPath,\n profile: result.profile,\n rule: REPOSITORY_POLICY.rules.packageManager,\n });\n }\n\n const devDependencies = isRecord(packageJson.devDependencies)\n ? packageJson.devDependencies\n : null;\n const bunTypes = devDependencies?.['@types/bun'];\n if (isNonEmptyString(bunTypes)) {\n pushMismatch(diagnostics, {\n actual: bunTypes,\n expected: BUN_POLICY.typesRange,\n location: 'package.json#devDependencies.@types/bun',\n path: packageJsonPath,\n profile: result.profile,\n rule: REPOSITORY_POLICY.rules.bunTypes,\n });\n }\n\n return diagnostics;\n}\n\n/***\n * Reports managed workflow drift from the canonical Bun setup version.\n */\nasync function analyzeWorkflowBunPolicy(result: DoctorAnalysisResult): Promise<DoctorDiagnostic[]> {\n const diagnostics = await Promise.all(\n BUN_POLICY.workflowTargets.map(async (workflow) => await analyzeWorkflow(result, workflow)),\n );\n return diagnostics.flat();\n}\n\n/***\n * Checks one managed workflow for the canonical Bun setup version.\n */\nasync function analyzeWorkflow(\n result: DoctorAnalysisResult,\n workflow: WorkflowTarget,\n): Promise<DoctorDiagnostic[]> {\n const workflowPath = path.join(result.targetPath, workflow.path);\n const contents = await readTextOrNull(workflowPath);\n if (contents === null) {\n return [createWorkflowDiagnostic(result, workflow, workflowPath, 'missing-path')];\n }\n\n const match = BUN_VERSION_PATTERN.exec(contents);\n const actual = match?.[1];\n if (!isNonEmptyString(actual)) {\n return [createWorkflowDiagnostic(result, workflow, workflowPath, 'field-missing')];\n }\n\n const diagnostics: DoctorDiagnostic[] = [];\n pushMismatch(diagnostics, {\n actual,\n expected: BUN_POLICY.version,\n location: `${workflow.path}#bun-version`,\n path: workflowPath,\n profile: result.profile,\n rule: getRepositoryRule(workflow.ruleId),\n });\n return diagnostics;\n}\n\n/***\n * Creates a missing or malformed managed-workflow diagnostic from central rule metadata.\n */\nfunction createWorkflowDiagnostic(\n result: DoctorAnalysisResult,\n workflow: WorkflowTarget,\n workflowPath: string,\n code: DoctorDiagnostic['code'],\n): DoctorDiagnostic {\n const detail =\n code === 'missing-path'\n ? `Managed workflow is missing: ${workflow.path}.`\n : `${workflow.path} does not define bun-version.`;\n const rule = getRepositoryRule(workflow.ruleId);\n\n return {\n code,\n message: `${detail} Expected Bun ${BUN_POLICY.version}. ${REPAIR_HINT}`,\n path: workflowPath,\n profile: result.profile,\n ruleId: rule.id,\n severity: rule.severity,\n };\n}\n\n/***\n * Adds one value-mismatch diagnostic using the owning Policy rule metadata.\n */\nfunction pushMismatch(\n diagnostics: DoctorDiagnostic[],\n input: {\n readonly actual: string;\n readonly expected: string;\n readonly location: string;\n readonly path: string;\n readonly profile: DoctorAnalysisResult['profile'];\n readonly rule: RepositoryPolicyRule;\n },\n): void {\n if (input.actual === input.expected) return;\n\n diagnostics.push({\n code: 'field-invalid',\n message: `Bun policy drift at ${input.location}: expected \"${input.expected}\", found \"${input.actual}\". ${REPAIR_HINT}`,\n path: input.path,\n profile: input.profile,\n ruleId: input.rule.id,\n severity: input.rule.severity,\n });\n}\n\n/***\n * Resolves one stable repository rule by its Policy-owned id.\n */\nfunction getRepositoryRule(ruleId: string): RepositoryPolicyRule {\n const rule = Object.values(REPOSITORY_POLICY.rules).find((entry) => entry.id === ruleId);\n if (rule === undefined) {\n throw new Error(`Unknown repository policy rule: ${ruleId}`);\n }\n return rule;\n}\n\n/***\n * Copies updated diagnostics into the analysis result and optional fix plan.\n */\nfunction withDiagnostics(\n result: DoctorAnalysisResult,\n diagnostics: readonly DoctorDiagnostic[],\n): DoctorAnalysisResult {\n return {\n ...result,\n diagnostics,\n fixPlan: result.fixPlan === null ? null : { ...result.fixPlan, diagnostics },\n };\n}\n\n/***\n * Reads a package manifest when it contains a JSON object.\n */\nasync function readPackageJson(packageJsonPath: string): Promise<Record<string, unknown> | null> {\n try {\n const parsed = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) as unknown;\n return isRecord(parsed) ? parsed : null;\n } catch {\n return null;\n }\n}\n\n/***\n * Reads a UTF-8 file while treating a missing managed path as absent.\n */\nasync function readTextOrNull(filePath: string): Promise<string | null> {\n try {\n return await fs.readFile(filePath, 'utf8');\n } catch (error) {\n if (isNodeError(error) && error.code === 'ENOENT') return null;\n throw error;\n }\n}\n\n/***\n * Narrows unknown JSON data to a non-array record.\n */\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/***\n * Narrows a value to a non-empty string.\n */\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.trim() !== '';\n}\n\n/***\n * Narrows unknown failures to Node errors with stable error codes.\n */\nfunction isNodeError(error: unknown): error is NodeJS.ErrnoException {\n return error instanceof Error && 'code' in error;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/doctor",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
4
4
  "description": "Executable Ankh provider and standalone CLI for lightweight repo and package compliance diagnostics.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -79,12 +79,14 @@
79
79
  },
80
80
  "dependencies": {
81
81
  "@ankhorage/contracts": "^22.8.2",
82
- "@ankhorage/supabase-auth": "^1.2.27"
82
+ "@ankhorage/supabase-auth": "^1.2.27",
83
+ "@ankhorage/utility": "^1.8.1",
84
+ "@ankhorage/policy": "^0.4.0"
83
85
  },
84
86
  "devDependencies": {
85
87
  "@ankhorage/ankh": "^0.10.4",
86
88
  "@ankhorage/devtools": "^1.18.5",
87
- "@ankhorage/paradox": "^0.1.26",
89
+ "@ankhorage/paradox": "^0.2.4",
88
90
  "@types/bun": "^1.4.1",
89
91
  "@types/node": "^25.6.0",
90
92
  "typescript": "^5.9.3"
package/src/analysis.ts CHANGED
@@ -2,6 +2,8 @@ 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 { uniqueSortedStrings } from '@ankhorage/utility/array';
6
+
5
7
  import type { DoctorDiagnostic, DoctorPolicyProfile, DoctorRuleId } from './diagnostics.js';
6
8
  import type { DoctorReadiness } from './readiness.js';
7
9
 
@@ -779,8 +781,8 @@ async function validateAnkhMetadataAndProvider(request: {
779
781
  const metadataCapabilities = Array.isArray(metadata.capabilities)
780
782
  ? metadata.capabilities.filter(isNonEmptyString)
781
783
  : [];
782
- const providerCapabilities = uniqueSorted(inspection.result.capabilities);
783
- const commandCapabilities = uniqueSorted(inspection.result.commandCapabilities);
784
+ const providerCapabilities = uniqueSortedStrings(inspection.result.capabilities);
785
+ const commandCapabilities = uniqueSortedStrings(inspection.result.commandCapabilities);
784
786
 
785
787
  if (!sameStringSet(metadataCapabilities, commandCapabilities)) {
786
788
  diagnostics.push(
@@ -1316,13 +1318,9 @@ function readStringArray(value: unknown): string[] {
1316
1318
  }
1317
1319
 
1318
1320
  function sameStringSet(left: readonly string[], right: readonly string[]): boolean {
1319
- const leftSet = uniqueSorted(left);
1320
- const rightSet = uniqueSorted(right);
1321
+ const leftSet = uniqueSortedStrings(left);
1322
+ const rightSet = uniqueSortedStrings(right);
1321
1323
  return (
1322
1324
  leftSet.length === rightSet.length && leftSet.every((value, index) => value === rightSet[index])
1323
1325
  );
1324
1326
  }
1325
-
1326
- function uniqueSorted(values: readonly string[]): string[] {
1327
- return [...new Set(values)].sort((left, right) => left.localeCompare(right));
1328
- }