@ankhorage/doctor 0.11.2 → 0.11.3

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,31 +1,21 @@
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
3
 
6
- import type { DoctorAnalysisResult } from './analysis.js';
7
- import type { DoctorDiagnostic, DoctorRuleId } from './diagnostics.js';
4
+ import { REPOSITORY_POLICY } from '@ankhorage/policy/repository';
8
5
 
9
- interface BunRuntimePolicySnapshot {
10
- readonly packageManager: string;
11
- readonly typesRange: string;
12
- readonly version: string;
13
- }
6
+ import type { DoctorAnalysisResult } from './analysis.js';
7
+ import type { DoctorDiagnostic } from './diagnostics.js';
14
8
 
15
- const DEVTOOLS_POLICY_SPECIFIER = '@ankhorage/devtools/policy';
9
+ const BUN_POLICY = REPOSITORY_POLICY.runtime.bun;
16
10
  const REPAIR_HINT = 'Run "ankh devtools sync" to repair the managed Bun state.';
17
11
  const BUN_VERSION_PATTERN = /^\s*bun-version:\s*['"]?([^'"\s#]+)['"]?\s*(?:#.*)?$/mu;
18
- const WORKFLOWS = [
19
- {
20
- relativePath: path.join('.github', 'workflows', 'ci.yml'),
21
- ruleId: 'repo.workflows.ci.bun-policy',
22
- },
23
- {
24
- relativePath: path.join('.github', 'workflows', 'release.yml'),
25
- ruleId: 'repo.workflows.release.bun-policy',
26
- },
27
- ] as const satisfies readonly { readonly relativePath: string; readonly ruleId: DoctorRuleId }[];
28
12
 
13
+ type RepositoryPolicyRule = (typeof REPOSITORY_POLICY.rules)[keyof typeof REPOSITORY_POLICY.rules];
14
+ type WorkflowTarget = (typeof BUN_POLICY.workflowTargets)[number];
15
+
16
+ /***
17
+ * Applies canonical repository Bun-policy diagnostics to a Doctor analysis result.
18
+ */
29
19
  export async function applyBunRuntimePolicy(
30
20
  result: DoctorAnalysisResult,
31
21
  ): Promise<DoctorAnalysisResult> {
@@ -33,54 +23,37 @@ export async function applyBunRuntimePolicy(
33
23
  return result;
34
24
  }
35
25
 
36
- const policy = await loadTargetBunRuntimePolicy(result.targetPath);
37
26
  const packageJsonPath = path.join(result.targetPath, 'package.json');
38
27
  const packageJson = await readPackageJson(packageJsonPath);
39
- if (policy === null || packageJson === null) {
40
- return result;
41
- }
28
+ if (packageJson === null) return result;
42
29
 
43
30
  const diagnostics = [
44
31
  ...result.diagnostics,
45
- ...analyzePackageBunPolicy(packageJson, packageJsonPath, result, policy),
46
- ...(await analyzeWorkflowBunPolicy(result, policy)),
32
+ ...analyzePackageBunPolicy(packageJson, packageJsonPath, result),
33
+ ...(await analyzeWorkflowBunPolicy(result)),
47
34
  ];
48
35
 
49
36
  return withDiagnostics(result, diagnostics);
50
37
  }
51
38
 
52
- async function loadTargetBunRuntimePolicy(
53
- targetPath: string,
54
- ): Promise<BunRuntimePolicySnapshot | null> {
55
- const requireFromTarget = createRequire(path.join(targetPath, 'package.json'));
56
- try {
57
- const modulePath = requireFromTarget.resolve(DEVTOOLS_POLICY_SPECIFIER);
58
- const importedModule = (await import(pathToFileURL(modulePath).href)) as unknown;
59
- return parseBunRuntimePolicy(importedModule);
60
- } catch (error) {
61
- if (isUnavailablePolicyError(error)) {
62
- return null;
63
- }
64
- throw error;
65
- }
66
- }
67
-
39
+ /***
40
+ * Reports package-manifest drift from the canonical Bun runtime contract.
41
+ */
68
42
  function analyzePackageBunPolicy(
69
43
  packageJson: Record<string, unknown>,
70
44
  packageJsonPath: string,
71
45
  result: DoctorAnalysisResult,
72
- policy: BunRuntimePolicySnapshot,
73
46
  ): DoctorDiagnostic[] {
74
47
  const diagnostics: DoctorDiagnostic[] = [];
75
48
  const { packageManager } = packageJson;
76
49
  if (isNonEmptyString(packageManager) && packageManager.startsWith('bun@')) {
77
50
  pushMismatch(diagnostics, {
78
51
  actual: packageManager,
79
- expected: policy.packageManager,
52
+ expected: BUN_POLICY.packageManager,
80
53
  location: 'package.json#packageManager',
81
54
  path: packageJsonPath,
82
55
  profile: result.profile,
83
- ruleId: 'package.json.package-manager.policy',
56
+ rule: REPOSITORY_POLICY.rules.packageManager,
84
57
  });
85
58
  }
86
59
 
@@ -91,77 +64,86 @@ function analyzePackageBunPolicy(
91
64
  if (isNonEmptyString(bunTypes)) {
92
65
  pushMismatch(diagnostics, {
93
66
  actual: bunTypes,
94
- expected: policy.typesRange,
67
+ expected: BUN_POLICY.typesRange,
95
68
  location: 'package.json#devDependencies.@types/bun',
96
69
  path: packageJsonPath,
97
70
  profile: result.profile,
98
- ruleId: 'package.dependencies.types-bun.policy',
71
+ rule: REPOSITORY_POLICY.rules.bunTypes,
99
72
  });
100
73
  }
101
74
 
102
75
  return diagnostics;
103
76
  }
104
77
 
105
- async function analyzeWorkflowBunPolicy(
106
- result: DoctorAnalysisResult,
107
- policy: BunRuntimePolicySnapshot,
108
- ): Promise<DoctorDiagnostic[]> {
78
+ /***
79
+ * Reports managed workflow drift from the canonical Bun setup version.
80
+ */
81
+ async function analyzeWorkflowBunPolicy(result: DoctorAnalysisResult): Promise<DoctorDiagnostic[]> {
109
82
  const diagnostics = await Promise.all(
110
- WORKFLOWS.map(async (workflow) => await analyzeWorkflow(result, workflow, policy)),
83
+ BUN_POLICY.workflowTargets.map(async (workflow) => await analyzeWorkflow(result, workflow)),
111
84
  );
112
85
  return diagnostics.flat();
113
86
  }
114
87
 
88
+ /***
89
+ * Checks one managed workflow for the canonical Bun setup version.
90
+ */
115
91
  async function analyzeWorkflow(
116
92
  result: DoctorAnalysisResult,
117
- workflow: (typeof WORKFLOWS)[number],
118
- policy: BunRuntimePolicySnapshot,
93
+ workflow: WorkflowTarget,
119
94
  ): Promise<DoctorDiagnostic[]> {
120
- const workflowPath = path.join(result.targetPath, workflow.relativePath);
95
+ const workflowPath = path.join(result.targetPath, workflow.path);
121
96
  const contents = await readTextOrNull(workflowPath);
122
97
  if (contents === null) {
123
- return [createWorkflowDiagnostic(result, workflow, policy, workflowPath, 'missing-path')];
98
+ return [createWorkflowDiagnostic(result, workflow, workflowPath, 'missing-path')];
124
99
  }
125
100
 
126
101
  const match = BUN_VERSION_PATTERN.exec(contents);
127
102
  const actual = match?.[1];
128
103
  if (!isNonEmptyString(actual)) {
129
- return [createWorkflowDiagnostic(result, workflow, policy, workflowPath, 'field-missing')];
104
+ return [createWorkflowDiagnostic(result, workflow, workflowPath, 'field-missing')];
130
105
  }
131
106
 
132
107
  const diagnostics: DoctorDiagnostic[] = [];
133
108
  pushMismatch(diagnostics, {
134
109
  actual,
135
- expected: policy.version,
136
- location: `${workflow.relativePath}#bun-version`,
110
+ expected: BUN_POLICY.version,
111
+ location: `${workflow.path}#bun-version`,
137
112
  path: workflowPath,
138
113
  profile: result.profile,
139
- ruleId: workflow.ruleId,
114
+ rule: getRepositoryRule(workflow.ruleId),
140
115
  });
141
116
  return diagnostics;
142
117
  }
143
118
 
119
+ /***
120
+ * Creates a missing or malformed managed-workflow diagnostic from central rule metadata.
121
+ */
144
122
  function createWorkflowDiagnostic(
145
123
  result: DoctorAnalysisResult,
146
- workflow: (typeof WORKFLOWS)[number],
147
- policy: BunRuntimePolicySnapshot,
124
+ workflow: WorkflowTarget,
148
125
  workflowPath: string,
149
126
  code: DoctorDiagnostic['code'],
150
127
  ): DoctorDiagnostic {
151
128
  const detail =
152
129
  code === 'missing-path'
153
- ? `Managed workflow is missing: ${workflow.relativePath}.`
154
- : `${workflow.relativePath} does not define bun-version.`;
130
+ ? `Managed workflow is missing: ${workflow.path}.`
131
+ : `${workflow.path} does not define bun-version.`;
132
+ const rule = getRepositoryRule(workflow.ruleId);
133
+
155
134
  return {
156
135
  code,
157
- message: `${detail} Expected Bun ${policy.version}. ${REPAIR_HINT}`,
136
+ message: `${detail} Expected Bun ${BUN_POLICY.version}. ${REPAIR_HINT}`,
158
137
  path: workflowPath,
159
138
  profile: result.profile,
160
- ruleId: workflow.ruleId,
161
- severity: 'error',
139
+ ruleId: rule.id,
140
+ severity: rule.severity,
162
141
  };
163
142
  }
164
143
 
144
+ /***
145
+ * Adds one value-mismatch diagnostic using the owning Policy rule metadata.
146
+ */
165
147
  function pushMismatch(
166
148
  diagnostics: DoctorDiagnostic[],
167
149
  input: {
@@ -170,34 +152,35 @@ function pushMismatch(
170
152
  readonly location: string;
171
153
  readonly path: string;
172
154
  readonly profile: DoctorAnalysisResult['profile'];
173
- readonly ruleId: DoctorRuleId;
155
+ readonly rule: RepositoryPolicyRule;
174
156
  },
175
157
  ): void {
176
- if (input.actual === input.expected) {
177
- return;
178
- }
158
+ if (input.actual === input.expected) return;
159
+
179
160
  diagnostics.push({
180
161
  code: 'field-invalid',
181
162
  message: `Bun policy drift at ${input.location}: expected "${input.expected}", found "${input.actual}". ${REPAIR_HINT}`,
182
163
  path: input.path,
183
164
  profile: input.profile,
184
- ruleId: input.ruleId,
185
- severity: 'error',
165
+ ruleId: input.rule.id,
166
+ severity: input.rule.severity,
186
167
  });
187
168
  }
188
169
 
189
- function parseBunRuntimePolicy(importedModule: unknown): BunRuntimePolicySnapshot | null {
190
- if (!isRecord(importedModule) || !isRecord(importedModule.bunRuntimePolicy)) {
191
- return null;
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}`);
192
177
  }
193
- const { packageManager, typesRange, version } = importedModule.bunRuntimePolicy;
194
- return isNonEmptyString(packageManager) &&
195
- isNonEmptyString(typesRange) &&
196
- isNonEmptyString(version)
197
- ? { packageManager, typesRange, version }
198
- : null;
178
+ return rule;
199
179
  }
200
180
 
181
+ /***
182
+ * Copies updated diagnostics into the analysis result and optional fix plan.
183
+ */
201
184
  function withDiagnostics(
202
185
  result: DoctorAnalysisResult,
203
186
  diagnostics: readonly DoctorDiagnostic[],
@@ -209,6 +192,9 @@ function withDiagnostics(
209
192
  };
210
193
  }
211
194
 
195
+ /***
196
+ * Reads a package manifest when it contains a JSON object.
197
+ */
212
198
  async function readPackageJson(packageJsonPath: string): Promise<Record<string, unknown> | null> {
213
199
  try {
214
200
  const parsed = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) as unknown;
@@ -218,44 +204,35 @@ async function readPackageJson(packageJsonPath: string): Promise<Record<string,
218
204
  }
219
205
  }
220
206
 
207
+ /***
208
+ * Reads a UTF-8 file while treating a missing managed path as absent.
209
+ */
221
210
  async function readTextOrNull(filePath: string): Promise<string | null> {
222
211
  try {
223
212
  return await fs.readFile(filePath, 'utf8');
224
213
  } catch (error) {
225
- if (isNodeError(error) && error.code === 'ENOENT') {
226
- return null;
227
- }
214
+ if (isNodeError(error) && error.code === 'ENOENT') return null;
228
215
  throw error;
229
216
  }
230
217
  }
231
218
 
232
- function isUnavailablePolicyError(error: unknown): boolean {
233
- if (
234
- isNodeError(error) &&
235
- (error.code === 'MODULE_NOT_FOUND' ||
236
- error.code === 'ERR_MODULE_NOT_FOUND' ||
237
- error.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED')
238
- ) {
239
- return true;
240
- }
241
-
242
- const message = error instanceof Error ? error.message : String(error);
243
- return (
244
- message.includes(DEVTOOLS_POLICY_SPECIFIER) &&
245
- (message.includes('Cannot find module') ||
246
- message.includes('not exported') ||
247
- message.includes('Package subpath'))
248
- );
219
+ /***
220
+ * Narrows unknown JSON data to a non-array record.
221
+ */
222
+ function isRecord(value: unknown): value is Record<string, unknown> {
223
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
249
224
  }
250
225
 
226
+ /***
227
+ * Narrows a value to a non-empty string.
228
+ */
251
229
  function isNonEmptyString(value: unknown): value is string {
252
230
  return typeof value === 'string' && value.trim() !== '';
253
231
  }
254
232
 
255
- function isRecord(value: unknown): value is Record<string, unknown> {
256
- return typeof value === 'object' && value !== null && !Array.isArray(value);
257
- }
258
-
233
+ /***
234
+ * Narrows unknown failures to Node errors with stable error codes.
235
+ */
259
236
  function isNodeError(error: unknown): error is NodeJS.ErrnoException {
260
237
  return error instanceof Error && 'code' in error;
261
238
  }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=readme-usage.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"readme-usage.d.ts","sourceRoot":"","sources":["../src/readme-usage.ts"],"names":[],"mappings":""}
@@ -1,51 +0,0 @@
1
- import { runCli } from './cli/standalone.js';
2
- /***
3
- * Provider and CLI surface
4
- *
5
- * `@ankhorage/doctor` is the repo/package compliance provider for Ankhorage.
6
- *
7
- * The same shared command implementation backs both:
8
- *
9
- * - `ankh doctor ...`
10
- * - `bunx @ankhorage/doctor ...`
11
- *
12
- * Current command surface:
13
- *
14
- * - `validate`
15
- * - `fix`
16
- * - `repo`
17
- * - `package`
18
- *
19
- * `doctor#2` adds a profile-based policy engine:
20
- *
21
- * - strict `public-package` validation for extracted public package repos
22
- * - light recognized `integration-monorepo` validation for `ankhorage4`
23
- * - canonical app-manifest authentication validation for JSON file targets
24
- * - non-mutating `fix` plans for deterministic mechanical changes only
25
- *
26
- * Public-package script validation follows the canonical Devtools contract. In particular,
27
- * packages must expose `knip:check`; the obsolete `knip` script does not satisfy that policy.
28
- * Changesets dependency placement follows the same ownership boundary: `@ankhorage/devtools`
29
- * publishes `@changesets/cli` from `dependencies`, while consumers use the Devtools-owned
30
- * command without declaring the CLI directly.
31
- *
32
- * Manifest validation accepts `infra.auth.flow` as the only auth-flow location, rejects
33
- * `settings.authFlow` with a manual-migration diagnostic, and permits authentication without
34
- * an authorization block.
35
- *
36
- * Still deferred:
37
- *
38
- * - GitHub checks
39
- * - CI checks
40
- * - on-disk `fix --apply`
41
- * - deeper cross-repo policy enforcement
42
- *
43
- * Path handling:
44
- *
45
- * - pass a repo/package directory or app-manifest JSON file as `[path]`, or
46
- * - omit it to inspect the current working directory
47
- *
48
- * @usage
49
- */
50
- await runCli(['--help']);
51
- //# sourceMappingURL=readme-usage.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"readme-usage.js","sourceRoot":"","sources":["../src/readme-usage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,MAAM,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC","sourcesContent":["import { runCli } from './cli/standalone.js';\n\n/***\n * Provider and CLI surface\n *\n * `@ankhorage/doctor` is the repo/package compliance provider for Ankhorage.\n *\n * The same shared command implementation backs both:\n *\n * - `ankh doctor ...`\n * - `bunx @ankhorage/doctor ...`\n *\n * Current command surface:\n *\n * - `validate`\n * - `fix`\n * - `repo`\n * - `package`\n *\n * `doctor#2` adds a profile-based policy engine:\n *\n * - strict `public-package` validation for extracted public package repos\n * - light recognized `integration-monorepo` validation for `ankhorage4`\n * - canonical app-manifest authentication validation for JSON file targets\n * - non-mutating `fix` plans for deterministic mechanical changes only\n *\n * Public-package script validation follows the canonical Devtools contract. In particular,\n * packages must expose `knip:check`; the obsolete `knip` script does not satisfy that policy.\n * Changesets dependency placement follows the same ownership boundary: `@ankhorage/devtools`\n * publishes `@changesets/cli` from `dependencies`, while consumers use the Devtools-owned\n * command without declaring the CLI directly.\n *\n * Manifest validation accepts `infra.auth.flow` as the only auth-flow location, rejects\n * `settings.authFlow` with a manual-migration diagnostic, and permits authentication without\n * an authorization block.\n *\n * Still deferred:\n *\n * - GitHub checks\n * - CI checks\n * - on-disk `fix --apply`\n * - deeper cross-repo policy enforcement\n *\n * Path handling:\n *\n * - pass a repo/package directory or app-manifest JSON file as `[path]`, or\n * - omit it to inspect the current working directory\n *\n * @usage\n */\nawait runCli(['--help']);\n"]}
@@ -1,51 +0,0 @@
1
- import { runCli } from './cli/standalone.js';
2
-
3
- /***
4
- * Provider and CLI surface
5
- *
6
- * `@ankhorage/doctor` is the repo/package compliance provider for Ankhorage.
7
- *
8
- * The same shared command implementation backs both:
9
- *
10
- * - `ankh doctor ...`
11
- * - `bunx @ankhorage/doctor ...`
12
- *
13
- * Current command surface:
14
- *
15
- * - `validate`
16
- * - `fix`
17
- * - `repo`
18
- * - `package`
19
- *
20
- * `doctor#2` adds a profile-based policy engine:
21
- *
22
- * - strict `public-package` validation for extracted public package repos
23
- * - light recognized `integration-monorepo` validation for `ankhorage4`
24
- * - canonical app-manifest authentication validation for JSON file targets
25
- * - non-mutating `fix` plans for deterministic mechanical changes only
26
- *
27
- * Public-package script validation follows the canonical Devtools contract. In particular,
28
- * packages must expose `knip:check`; the obsolete `knip` script does not satisfy that policy.
29
- * Changesets dependency placement follows the same ownership boundary: `@ankhorage/devtools`
30
- * publishes `@changesets/cli` from `dependencies`, while consumers use the Devtools-owned
31
- * command without declaring the CLI directly.
32
- *
33
- * Manifest validation accepts `infra.auth.flow` as the only auth-flow location, rejects
34
- * `settings.authFlow` with a manual-migration diagnostic, and permits authentication without
35
- * an authorization block.
36
- *
37
- * Still deferred:
38
- *
39
- * - GitHub checks
40
- * - CI checks
41
- * - on-disk `fix --apply`
42
- * - deeper cross-repo policy enforcement
43
- *
44
- * Path handling:
45
- *
46
- * - pass a repo/package directory or app-manifest JSON file as `[path]`, or
47
- * - omit it to inspect the current working directory
48
- *
49
- * @usage
50
- */
51
- await runCli(['--help']);