@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.
- package/CHANGELOG.md +17 -0
- package/README.md +31 -41
- package/dist/analysis.d.ts.map +1 -1
- package/dist/analysis.js +5 -7
- package/dist/analysis.js.map +1 -1
- package/dist/bunPolicyAnalysis.d.ts +3 -0
- package/dist/bunPolicyAnalysis.d.ts.map +1 -1
- package/dist/bunPolicyAnalysis.js +76 -82
- package/dist/bunPolicyAnalysis.js.map +1 -1
- package/package.json +5 -3
- package/src/analysis.ts +6 -8
- package/src/bunPolicyAnalysis.ts +83 -106
- package/dist/readme-usage.d.ts +0 -2
- package/dist/readme-usage.d.ts.map +0 -1
- package/dist/readme-usage.js +0 -51
- package/dist/readme-usage.js.map +0 -1
- package/src/readme-usage.ts +0 -51
|
@@ -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 {
|
|
5
|
-
const
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 (
|
|
16
|
+
if (packageJson === null)
|
|
26
17
|
return result;
|
|
27
|
-
}
|
|
28
18
|
const diagnostics = [
|
|
29
19
|
...result.diagnostics,
|
|
30
|
-
...analyzePackageBunPolicy(packageJson, packageJsonPath, result
|
|
31
|
-
...(await analyzeWorkflowBunPolicy(result
|
|
20
|
+
...analyzePackageBunPolicy(packageJson, packageJsonPath, result),
|
|
21
|
+
...(await analyzeWorkflowBunPolicy(result)),
|
|
32
22
|
];
|
|
33
23
|
return withDiagnostics(result, diagnostics);
|
|
34
24
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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:
|
|
34
|
+
expected: BUN_POLICY.packageManager,
|
|
56
35
|
location: 'package.json#packageManager',
|
|
57
36
|
path: packageJsonPath,
|
|
58
37
|
profile: result.profile,
|
|
59
|
-
|
|
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:
|
|
48
|
+
expected: BUN_POLICY.typesRange,
|
|
70
49
|
location: 'package.json#devDependencies.@types/bun',
|
|
71
50
|
path: packageJsonPath,
|
|
72
51
|
profile: result.profile,
|
|
73
|
-
|
|
52
|
+
rule: REPOSITORY_POLICY.rules.bunTypes,
|
|
74
53
|
});
|
|
75
54
|
}
|
|
76
55
|
return diagnostics;
|
|
77
56
|
}
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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,
|
|
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,
|
|
76
|
+
return [createWorkflowDiagnostic(result, workflow, workflowPath, 'field-missing')];
|
|
92
77
|
}
|
|
93
78
|
const diagnostics = [];
|
|
94
79
|
pushMismatch(diagnostics, {
|
|
95
80
|
actual,
|
|
96
|
-
expected:
|
|
97
|
-
location: `${workflow.
|
|
81
|
+
expected: BUN_POLICY.version,
|
|
82
|
+
location: `${workflow.path}#bun-version`,
|
|
98
83
|
path: workflowPath,
|
|
99
84
|
profile: result.profile,
|
|
100
|
-
|
|
85
|
+
rule: getRepositoryRule(workflow.ruleId),
|
|
101
86
|
});
|
|
102
87
|
return diagnostics;
|
|
103
88
|
}
|
|
104
|
-
|
|
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.
|
|
107
|
-
: `${workflow.
|
|
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 ${
|
|
99
|
+
message: `${detail} Expected Bun ${BUN_POLICY.version}. ${REPAIR_HINT}`,
|
|
111
100
|
path: workflowPath,
|
|
112
101
|
profile: result.profile,
|
|
113
|
-
ruleId:
|
|
114
|
-
severity:
|
|
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.
|
|
127
|
-
severity:
|
|
117
|
+
ruleId: input.rule.id,
|
|
118
|
+
severity: input.rule.severity,
|
|
128
119
|
});
|
|
129
120
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAWzC,MAAM,yBAAyB,GAAG,4BAA4B,CAAC;AAC/D,MAAM,WAAW,GAAG,2DAA2D,CAAC;AAChF,MAAM,mBAAmB,GAAG,wDAAwD,CAAC;AACrF,MAAM,SAAS,GAAG;IAChB;QACE,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,EAAE,QAAQ,CAAC;QACzD,MAAM,EAAE,8BAA8B;KACvC;IACD;QACE,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,EAAE,aAAa,CAAC;QAC9D,MAAM,EAAE,mCAAmC;KAC5C;CAC6F,CAAC;AAEjG,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,MAA4B;IAE5B,IAAI,MAAM,CAAC,OAAO,KAAK,gBAAgB,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;QAClE,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,0BAA0B,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACnE,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;IACrE,MAAM,WAAW,GAAG,MAAM,eAAe,CAAC,eAAe,CAAC,CAAC;IAC3D,IAAI,MAAM,KAAK,IAAI,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QAC5C,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,WAAW,GAAG;QAClB,GAAG,MAAM,CAAC,WAAW;QACrB,GAAG,uBAAuB,CAAC,WAAW,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC;QACxE,GAAG,CAAC,MAAM,wBAAwB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACpD,CAAC;IAEF,OAAO,eAAe,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAC9C,CAAC;AAED,KAAK,UAAU,0BAA0B,CACvC,UAAkB;IAElB,MAAM,iBAAiB,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC,CAAC;IAC/E,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,iBAAiB,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC;QACxE,MAAM,cAAc,GAAG,CAAC,MAAM,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAY,CAAC;QACjF,OAAO,qBAAqB,CAAC,cAAc,CAAC,CAAC;IAC/C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,wBAAwB,CAAC,KAAK,CAAC,EAAE,CAAC;YACpC,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,uBAAuB,CAC9B,WAAoC,EACpC,eAAuB,EACvB,MAA4B,EAC5B,MAAgC;IAEhC,MAAM,WAAW,GAAuB,EAAE,CAAC;IAC3C,MAAM,EAAE,cAAc,EAAE,GAAG,WAAW,CAAC;IACvC,IAAI,gBAAgB,CAAC,cAAc,CAAC,IAAI,cAAc,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1E,YAAY,CAAC,WAAW,EAAE;YACxB,MAAM,EAAE,cAAc;YACtB,QAAQ,EAAE,MAAM,CAAC,cAAc;YAC/B,QAAQ,EAAE,6BAA6B;YACvC,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,MAAM,EAAE,qCAAqC;SAC9C,CAAC,CAAC;IACL,CAAC;IAED,MAAM,eAAe,GAAG,QAAQ,CAAC,WAAW,CAAC,eAAe,CAAC;QAC3D,CAAC,CAAC,WAAW,CAAC,eAAe;QAC7B,CAAC,CAAC,IAAI,CAAC;IACT,MAAM,QAAQ,GAAG,eAAe,EAAE,CAAC,YAAY,CAAC,CAAC;IACjD,IAAI,gBAAgB,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC/B,YAAY,CAAC,WAAW,EAAE;YACxB,MAAM,EAAE,QAAQ;YAChB,QAAQ,EAAE,MAAM,CAAC,UAAU;YAC3B,QAAQ,EAAE,yCAAyC;YACnD,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,MAAM,EAAE,uCAAuC;SAChD,CAAC,CAAC;IACL,CAAC;IAED,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,KAAK,UAAU,wBAAwB,CACrC,MAA4B,EAC5B,MAAgC;IAEhC,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,GAAG,CACnC,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,CAAC,MAAM,eAAe,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CACnF,CAAC;IACF,OAAO,WAAW,CAAC,IAAI,EAAE,CAAC;AAC5B,CAAC;AAED,KAAK,UAAU,eAAe,CAC5B,MAA4B,EAC5B,QAAoC,EACpC,MAAgC;IAEhC,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,YAAY,CAAC,CAAC;IACpD,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,OAAO,CAAC,wBAAwB,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,cAAc,CAAC,CAAC,CAAC;IAC5F,CAAC;IAED,MAAM,KAAK,GAAG,mBAAmB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IAC1B,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9B,OAAO,CAAC,wBAAwB,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,eAAe,CAAC,CAAC,CAAC;IAC7F,CAAC;IAED,MAAM,WAAW,GAAuB,EAAE,CAAC;IAC3C,YAAY,CAAC,WAAW,EAAE;QACxB,MAAM;QACN,QAAQ,EAAE,MAAM,CAAC,OAAO;QACxB,QAAQ,EAAE,GAAG,QAAQ,CAAC,YAAY,cAAc;QAChD,IAAI,EAAE,YAAY;QAClB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,MAAM,EAAE,QAAQ,CAAC,MAAM;KACxB,CAAC,CAAC;IACH,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,SAAS,wBAAwB,CAC/B,MAA4B,EAC5B,QAAoC,EACpC,MAAgC,EAChC,YAAoB,EACpB,IAA8B;IAE9B,MAAM,MAAM,GACV,IAAI,KAAK,cAAc;QACrB,CAAC,CAAC,gCAAgC,QAAQ,CAAC,YAAY,GAAG;QAC1D,CAAC,CAAC,GAAG,QAAQ,CAAC,YAAY,+BAA+B,CAAC;IAC9D,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,MAAM,iBAAiB,MAAM,CAAC,OAAO,KAAK,WAAW,EAAE;QACnE,IAAI,EAAE,YAAY;QAClB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,QAAQ,EAAE,OAAO;KAClB,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CACnB,WAA+B,EAC/B,KAOC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,QAAQ,EAAE,CAAC;QACpC,OAAO;IACT,CAAC;IACD,WAAW,CAAC,IAAI,CAAC;QACf,IAAI,EAAE,eAAe;QACrB,OAAO,EAAE,uBAAuB,KAAK,CAAC,QAAQ,eAAe,KAAK,CAAC,QAAQ,aAAa,KAAK,CAAC,MAAM,MAAM,WAAW,EAAE;QACvH,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,QAAQ,EAAE,OAAO;KAClB,CAAC,CAAC;AACL,CAAC;AAED,SAAS,qBAAqB,CAAC,cAAuB;IACpD,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,gBAAgB,CAAC,EAAE,CAAC;QAC5E,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,EAAE,cAAc,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,cAAc,CAAC,gBAAgB,CAAC;IAChF,OAAO,gBAAgB,CAAC,cAAc,CAAC;QACrC,gBAAgB,CAAC,UAAU,CAAC;QAC5B,gBAAgB,CAAC,OAAO,CAAC;QACzB,CAAC,CAAC,EAAE,cAAc,EAAE,UAAU,EAAE,OAAO,EAAE;QACzC,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED,SAAS,eAAe,CACtB,MAA4B,EAC5B,WAAwC;IAExC,OAAO;QACL,GAAG,MAAM;QACT,WAAW;QACX,OAAO,EAAE,MAAM,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,OAAO,EAAE,WAAW,EAAE;KAC7E,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,eAAuB;IACpD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC,CAAY,CAAC;QACjF,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,QAAgB;IAC5C,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,WAAW,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAClD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,wBAAwB,CAAC,KAAc;IAC9C,IACE,WAAW,CAAC,KAAK,CAAC;QAClB,CAAC,KAAK,CAAC,IAAI,KAAK,kBAAkB;YAChC,KAAK,CAAC,IAAI,KAAK,sBAAsB;YACrC,KAAK,CAAC,IAAI,KAAK,+BAA+B,CAAC,EACjD,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,OAAO,CACL,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAC;QAC3C,CAAC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAC;YACrC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;YAChC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAC,CACvC,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAc;IACtC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;AAC1D,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,OAAO,KAAK,YAAY,KAAK,IAAI,MAAM,IAAI,KAAK,CAAC;AACnD,CAAC","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.
|
|
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.
|
|
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 =
|
|
783
|
-
const 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 =
|
|
1320
|
-
const rightSet =
|
|
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
|
-
}
|