@ankhorage/doctor 0.2.0 → 0.3.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.
- package/CHANGELOG.md +12 -0
- package/README.md +12 -4
- package/dist/analysis.d.ts +20 -3
- package/dist/analysis.d.ts.map +1 -1
- package/dist/analysis.js +816 -89
- package/dist/analysis.js.map +1 -1
- package/dist/ankh.provider.d.ts +2 -2
- package/dist/ankh.provider.d.ts.map +1 -1
- package/dist/ankh.provider.js +2 -2
- package/dist/ankh.provider.js.map +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -4
- package/dist/cli.js.map +1 -1
- package/dist/commandContext.js +1 -1
- package/dist/commandContext.js.map +1 -1
- package/dist/commands.d.ts +7 -7
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +78 -68
- package/dist/commands.js.map +1 -1
- package/dist/diagnostics.d.ts +6 -2
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +2 -4
- package/dist/diagnostics.js.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/packageMetadata.d.ts.map +1 -1
- package/dist/packageMetadata.js +7 -7
- package/dist/packageMetadata.js.map +1 -1
- package/dist/readme-usage.js +13 -5
- package/dist/readme-usage.js.map +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +1082 -145
- package/src/commands.ts +22 -5
- package/src/diagnostics.ts +67 -10
- package/src/index.ts +5 -0
- package/src/readme-usage.ts +11 -3
package/dist/analysis.js
CHANGED
|
@@ -1,135 +1,807 @@
|
|
|
1
|
-
import { promises as fs } from
|
|
2
|
-
import path from
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
3
4
|
const REPO_MARKERS = [
|
|
4
|
-
{ label:
|
|
5
|
-
{ label:
|
|
6
|
-
{ label:
|
|
7
|
-
{ label:
|
|
8
|
-
{ label:
|
|
9
|
-
{ label:
|
|
10
|
-
{ label:
|
|
11
|
-
{ label:
|
|
12
|
-
{ label:
|
|
5
|
+
{ label: '.git', relativePath: '.git' },
|
|
6
|
+
{ label: '.github/workflows', relativePath: path.join('.github', 'workflows') },
|
|
7
|
+
{ label: '.changeset', relativePath: '.changeset' },
|
|
8
|
+
{ label: 'package.json', relativePath: 'package.json' },
|
|
9
|
+
{ label: 'pnpm-workspace.yaml', relativePath: 'pnpm-workspace.yaml' },
|
|
10
|
+
{ label: 'bun.lock', relativePath: 'bun.lock' },
|
|
11
|
+
{ label: 'bun.lockb', relativePath: 'bun.lockb' },
|
|
12
|
+
{ label: 'package-lock.json', relativePath: 'package-lock.json' },
|
|
13
|
+
{ label: 'yarn.lock', relativePath: 'yarn.lock' },
|
|
14
|
+
];
|
|
15
|
+
const REQUIRED_PUBLIC_PACKAGE_SCRIPTS = [
|
|
16
|
+
'build',
|
|
17
|
+
'typecheck',
|
|
18
|
+
'lint',
|
|
19
|
+
'lint:fix',
|
|
20
|
+
'format',
|
|
21
|
+
'format:check',
|
|
22
|
+
'test',
|
|
23
|
+
'knip',
|
|
24
|
+
'docs',
|
|
25
|
+
'changeset',
|
|
26
|
+
'changeset:status',
|
|
27
|
+
'version-packages',
|
|
28
|
+
];
|
|
29
|
+
const PROVIDER_SOURCE_CANDIDATES = [
|
|
30
|
+
path.join('src', 'ankh.provider.ts'),
|
|
31
|
+
path.join('src', 'ankh.provider.tsx'),
|
|
32
|
+
path.join('src', 'ankh.provider.js'),
|
|
33
|
+
path.join('src', 'ankh.provider.mjs'),
|
|
13
34
|
];
|
|
14
35
|
export async function analyzeDoctorTarget(request) {
|
|
15
|
-
const targetPath = path.resolve(request.cwd, request.inputPath ??
|
|
36
|
+
const targetPath = path.resolve(request.cwd, request.inputPath ?? '.');
|
|
16
37
|
const stats = await statOrNull(targetPath);
|
|
17
38
|
if (stats === null) {
|
|
18
|
-
return {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
};
|
|
39
|
+
return createEarlyResult(targetPath, createDiagnostic({
|
|
40
|
+
code: 'target-not-found',
|
|
41
|
+
message: `Target path does not exist: ${targetPath}`,
|
|
42
|
+
path: targetPath,
|
|
43
|
+
profile: 'unknown',
|
|
44
|
+
ruleId: 'target.path.exists',
|
|
45
|
+
severity: 'error',
|
|
46
|
+
}));
|
|
27
47
|
}
|
|
28
48
|
if (!stats.isDirectory()) {
|
|
29
|
-
return {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
};
|
|
49
|
+
return createEarlyResult(targetPath, createDiagnostic({
|
|
50
|
+
code: 'target-not-directory',
|
|
51
|
+
message: `Target path must be a directory: ${targetPath}`,
|
|
52
|
+
path: targetPath,
|
|
53
|
+
profile: 'unknown',
|
|
54
|
+
ruleId: 'target.path.directory',
|
|
55
|
+
severity: 'error',
|
|
56
|
+
}));
|
|
38
57
|
}
|
|
39
|
-
const repoMarkers =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const packageJsonPath = path.join(targetPath, "package.json");
|
|
43
|
-
const hasPackageJson = repoMarkers.includes("package.json");
|
|
58
|
+
const repoMarkers = await findRepoMarkers(targetPath);
|
|
59
|
+
const packageJsonPath = path.join(targetPath, 'package.json');
|
|
60
|
+
const hasPackageJson = repoMarkers.includes('package.json');
|
|
44
61
|
const appliedChecks = [];
|
|
45
62
|
const diagnostics = [];
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
(
|
|
50
|
-
if (request.mode === "repo" && repoMarkers.length === 0) {
|
|
51
|
-
diagnostics.push(createDiagnostic("error", "repo-markers-missing", targetPath, "Target is not a repo/workspace-root candidate because no repo markers were found."));
|
|
63
|
+
const plannedChanges = [];
|
|
64
|
+
const packageJson = hasPackageJson ? await readPackageJson(packageJsonPath) : null;
|
|
65
|
+
if (packageJson !== null && packageJson.diagnostic !== null) {
|
|
66
|
+
diagnostics.push(packageJson.diagnostic);
|
|
52
67
|
}
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
const profile = detectProfile(repoMarkers, packageJson?.parsed ?? null);
|
|
69
|
+
const shouldRunRepoChecks = request.mode === 'repo' ||
|
|
70
|
+
((request.mode === 'validate' || request.mode === 'fix') && repoMarkers.length > 0);
|
|
71
|
+
const shouldRunPackageChecks = request.mode === 'package' ||
|
|
72
|
+
((request.mode === 'validate' || request.mode === 'fix') && hasPackageJson);
|
|
73
|
+
if (request.mode === 'repo' && repoMarkers.length === 0) {
|
|
74
|
+
diagnostics.push(createDiagnostic({
|
|
75
|
+
code: 'unsupported-target',
|
|
76
|
+
message: 'Target is not a repo/workspace-root candidate because no repo markers were found.',
|
|
77
|
+
path: targetPath,
|
|
78
|
+
profile,
|
|
79
|
+
ruleId: 'target.repo-markers.required',
|
|
80
|
+
severity: 'error',
|
|
81
|
+
}));
|
|
55
82
|
}
|
|
56
|
-
if (
|
|
83
|
+
if (request.mode === 'package' && !hasPackageJson) {
|
|
84
|
+
diagnostics.push(createDiagnostic({
|
|
85
|
+
code: 'unsupported-target',
|
|
86
|
+
message: 'Target does not contain a readable package.json file.',
|
|
87
|
+
path: packageJsonPath,
|
|
88
|
+
profile,
|
|
89
|
+
ruleId: 'target.package-json.required',
|
|
90
|
+
severity: 'error',
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
if ((request.mode === 'validate' || request.mode === 'fix') &&
|
|
57
94
|
repoMarkers.length === 0 &&
|
|
58
95
|
!hasPackageJson) {
|
|
59
|
-
diagnostics.push(createDiagnostic(
|
|
96
|
+
diagnostics.push(createDiagnostic({
|
|
97
|
+
code: 'unsupported-target',
|
|
98
|
+
message: 'Target is neither a repo/workspace-root candidate nor a package root.',
|
|
99
|
+
path: targetPath,
|
|
100
|
+
profile,
|
|
101
|
+
ruleId: 'target.profile.supported',
|
|
102
|
+
severity: 'error',
|
|
103
|
+
}));
|
|
60
104
|
}
|
|
61
105
|
if (shouldRunRepoChecks && repoMarkers.length > 0) {
|
|
62
|
-
appliedChecks.push(
|
|
63
|
-
diagnostics.push(...(await
|
|
106
|
+
appliedChecks.push('repo');
|
|
107
|
+
diagnostics.push(...(await analyzeRepoPolicy({
|
|
108
|
+
packageJson: packageJson?.parsed ?? null,
|
|
109
|
+
plannedChanges,
|
|
110
|
+
profile,
|
|
111
|
+
repoMarkers,
|
|
112
|
+
targetPath,
|
|
113
|
+
})));
|
|
64
114
|
}
|
|
65
115
|
if (shouldRunPackageChecks && hasPackageJson) {
|
|
66
|
-
appliedChecks.push(
|
|
67
|
-
diagnostics.push(...(await
|
|
116
|
+
appliedChecks.push('package');
|
|
117
|
+
diagnostics.push(...(await analyzePackagePolicy({
|
|
118
|
+
packageJson: packageJson?.parsed ?? null,
|
|
119
|
+
packageJsonPath,
|
|
120
|
+
plannedChanges,
|
|
121
|
+
profile,
|
|
122
|
+
targetPath,
|
|
123
|
+
})));
|
|
68
124
|
}
|
|
125
|
+
const fixPlan = request.mode === 'fix'
|
|
126
|
+
? {
|
|
127
|
+
changes: plannedChanges,
|
|
128
|
+
diagnostics,
|
|
129
|
+
profile,
|
|
130
|
+
targetPath,
|
|
131
|
+
}
|
|
132
|
+
: null;
|
|
69
133
|
return {
|
|
70
134
|
appliedChecks,
|
|
71
135
|
diagnostics,
|
|
136
|
+
fixPlan,
|
|
72
137
|
hasPackageJson,
|
|
138
|
+
plannedChanges,
|
|
139
|
+
profile,
|
|
73
140
|
repoMarkers,
|
|
74
141
|
targetPath,
|
|
75
142
|
};
|
|
76
143
|
}
|
|
77
|
-
async function
|
|
144
|
+
async function analyzeRepoPolicy(request) {
|
|
78
145
|
const diagnostics = [];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
146
|
+
if (request.profile === 'public-package') {
|
|
147
|
+
await maybeRequirePath(diagnostics, request.plannedChanges, request.targetPath, 'README.md', 'repo.readme.required', 'create-file');
|
|
148
|
+
await maybeRequirePath(diagnostics, request.plannedChanges, request.targetPath, 'CHANGELOG.md', 'repo.changelog.required', 'create-file');
|
|
149
|
+
await maybeRequirePath(diagnostics, request.plannedChanges, request.targetPath, 'LICENSE', 'repo.license.required', 'create-file');
|
|
150
|
+
await maybeRequirePath(diagnostics, request.plannedChanges, request.targetPath, '.changeset', 'repo.changeset.required', 'create-directory');
|
|
151
|
+
await maybeRequirePath(diagnostics, request.plannedChanges, request.targetPath, path.join('.github', 'workflows'), 'repo.workflows.required', 'create-directory');
|
|
152
|
+
}
|
|
153
|
+
if (request.profile === 'integration-monorepo') {
|
|
154
|
+
if (!request.repoMarkers.includes('package.json')) {
|
|
155
|
+
diagnostics.push(createDiagnostic({
|
|
156
|
+
code: 'missing-path',
|
|
157
|
+
message: 'The integration monorepo root must include a package.json file.',
|
|
158
|
+
path: path.join(request.targetPath, 'package.json'),
|
|
159
|
+
profile: request.profile,
|
|
160
|
+
ruleId: 'target.package-json.required',
|
|
161
|
+
severity: 'error',
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
if (request.packageJson !== null) {
|
|
165
|
+
if (request.packageJson.ankh !== undefined) {
|
|
166
|
+
diagnostics.push(createDiagnostic({
|
|
167
|
+
code: 'field-invalid',
|
|
168
|
+
message: 'The integration monorepo root must not publish top-level package.json.ankh metadata.',
|
|
169
|
+
path: path.join(request.targetPath, 'package.json'),
|
|
170
|
+
profile: request.profile,
|
|
171
|
+
ruleId: 'integration.package.ankh.disallowed',
|
|
172
|
+
severity: 'error',
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
if (request.packageJson.private !== true) {
|
|
176
|
+
diagnostics.push(createDiagnostic({
|
|
177
|
+
code: 'field-invalid',
|
|
178
|
+
message: 'The integration monorepo root must remain private.',
|
|
179
|
+
path: path.join(request.targetPath, 'package.json'),
|
|
180
|
+
profile: request.profile,
|
|
181
|
+
ruleId: 'integration.package.private.required',
|
|
182
|
+
severity: 'error',
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
if (!Array.isArray(request.packageJson.workspaces)) {
|
|
186
|
+
diagnostics.push(createDiagnostic({
|
|
187
|
+
code: 'field-missing',
|
|
188
|
+
message: 'The integration monorepo root must declare workspaces.',
|
|
189
|
+
path: path.join(request.targetPath, 'package.json'),
|
|
190
|
+
profile: request.profile,
|
|
191
|
+
ruleId: 'integration.package.workspaces.required',
|
|
192
|
+
severity: 'error',
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
84
197
|
return diagnostics;
|
|
85
198
|
}
|
|
86
|
-
async function
|
|
199
|
+
async function analyzePackagePolicy(request) {
|
|
200
|
+
if (request.packageJson === null) {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
const diagnostics = [];
|
|
204
|
+
const { packageJson } = request;
|
|
205
|
+
if (request.profile === 'public-package') {
|
|
206
|
+
requireNonEmptyStringField({
|
|
207
|
+
diagnostics,
|
|
208
|
+
packageJson,
|
|
209
|
+
packageJsonPath: request.packageJsonPath,
|
|
210
|
+
profile: request.profile,
|
|
211
|
+
ruleId: 'package.json.name.required',
|
|
212
|
+
fieldName: 'name',
|
|
213
|
+
});
|
|
214
|
+
requireNonEmptyStringField({
|
|
215
|
+
diagnostics,
|
|
216
|
+
packageJson,
|
|
217
|
+
packageJsonPath: request.packageJsonPath,
|
|
218
|
+
profile: request.profile,
|
|
219
|
+
ruleId: 'package.json.version.required',
|
|
220
|
+
fieldName: 'version',
|
|
221
|
+
});
|
|
222
|
+
requireNonEmptyStringField({
|
|
223
|
+
diagnostics,
|
|
224
|
+
packageJson,
|
|
225
|
+
packageJsonPath: request.packageJsonPath,
|
|
226
|
+
profile: request.profile,
|
|
227
|
+
ruleId: 'package.json.type.required',
|
|
228
|
+
fieldName: 'type',
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (request.profile !== 'public-package') {
|
|
232
|
+
return diagnostics.concat(await validateAnkhMetadataAndProvider({
|
|
233
|
+
diagnostics,
|
|
234
|
+
packageJson,
|
|
235
|
+
packageJsonPath: request.packageJsonPath,
|
|
236
|
+
plannedChanges: request.plannedChanges,
|
|
237
|
+
profile: request.profile,
|
|
238
|
+
targetPath: request.targetPath,
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
requireNonEmptyStringField({
|
|
242
|
+
diagnostics,
|
|
243
|
+
packageJson,
|
|
244
|
+
packageJsonPath: request.packageJsonPath,
|
|
245
|
+
profile: request.profile,
|
|
246
|
+
ruleId: 'package.json.description.required',
|
|
247
|
+
fieldName: 'description',
|
|
248
|
+
});
|
|
249
|
+
requireRecordField({
|
|
250
|
+
diagnostics,
|
|
251
|
+
packageJson,
|
|
252
|
+
packageJsonPath: request.packageJsonPath,
|
|
253
|
+
profile: request.profile,
|
|
254
|
+
ruleId: 'package.json.repository.required',
|
|
255
|
+
fieldName: 'repository',
|
|
256
|
+
});
|
|
257
|
+
requireNonEmptyStringField({
|
|
258
|
+
diagnostics,
|
|
259
|
+
packageJson,
|
|
260
|
+
packageJsonPath: request.packageJsonPath,
|
|
261
|
+
profile: request.profile,
|
|
262
|
+
ruleId: 'package.json.homepage.required',
|
|
263
|
+
fieldName: 'homepage',
|
|
264
|
+
});
|
|
265
|
+
requireRecordField({
|
|
266
|
+
diagnostics,
|
|
267
|
+
packageJson,
|
|
268
|
+
packageJsonPath: request.packageJsonPath,
|
|
269
|
+
profile: request.profile,
|
|
270
|
+
ruleId: 'package.json.bugs.required',
|
|
271
|
+
fieldName: 'bugs',
|
|
272
|
+
});
|
|
273
|
+
requireNonEmptyStringField({
|
|
274
|
+
diagnostics,
|
|
275
|
+
packageJson,
|
|
276
|
+
packageJsonPath: request.packageJsonPath,
|
|
277
|
+
profile: request.profile,
|
|
278
|
+
ruleId: 'package.json.license.required',
|
|
279
|
+
fieldName: 'license',
|
|
280
|
+
});
|
|
281
|
+
requireStringArrayField({
|
|
282
|
+
diagnostics,
|
|
283
|
+
packageJson,
|
|
284
|
+
packageJsonPath: request.packageJsonPath,
|
|
285
|
+
profile: request.profile,
|
|
286
|
+
ruleId: 'package.json.keywords.required',
|
|
287
|
+
fieldName: 'keywords',
|
|
288
|
+
});
|
|
289
|
+
requireStringArrayField({
|
|
290
|
+
diagnostics,
|
|
291
|
+
packageJson,
|
|
292
|
+
packageJsonPath: request.packageJsonPath,
|
|
293
|
+
profile: request.profile,
|
|
294
|
+
ruleId: 'package.json.files.required',
|
|
295
|
+
fieldName: 'files',
|
|
296
|
+
});
|
|
297
|
+
requireRecordField({
|
|
298
|
+
diagnostics,
|
|
299
|
+
packageJson,
|
|
300
|
+
packageJsonPath: request.packageJsonPath,
|
|
301
|
+
profile: request.profile,
|
|
302
|
+
ruleId: 'package.json.exports.required',
|
|
303
|
+
fieldName: 'exports',
|
|
304
|
+
});
|
|
305
|
+
requireRecordField({
|
|
306
|
+
diagnostics,
|
|
307
|
+
packageJson,
|
|
308
|
+
packageJsonPath: request.packageJsonPath,
|
|
309
|
+
profile: request.profile,
|
|
310
|
+
ruleId: 'package.json.publish-config.required',
|
|
311
|
+
fieldName: 'publishConfig',
|
|
312
|
+
});
|
|
313
|
+
if (packageJson.private === true) {
|
|
314
|
+
diagnostics.push(createDiagnostic({
|
|
315
|
+
code: 'field-invalid',
|
|
316
|
+
message: 'Public package profiles must not set private: true.',
|
|
317
|
+
path: request.packageJsonPath,
|
|
318
|
+
profile: request.profile,
|
|
319
|
+
ruleId: 'package.json.private.public-package-disallowed',
|
|
320
|
+
severity: 'error',
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
if (packageJson.type !== undefined && packageJson.type !== 'module') {
|
|
324
|
+
diagnostics.push(createDiagnostic({
|
|
325
|
+
code: 'field-invalid',
|
|
326
|
+
message: 'package.json "type" must be "module".',
|
|
327
|
+
path: request.packageJsonPath,
|
|
328
|
+
profile: request.profile,
|
|
329
|
+
ruleId: 'package.json.type.module',
|
|
330
|
+
severity: 'error',
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
const { publishConfig } = packageJson;
|
|
334
|
+
if (isRecord(publishConfig) && publishConfig.access !== 'public') {
|
|
335
|
+
diagnostics.push(createDiagnostic({
|
|
336
|
+
code: isNonEmptyString(publishConfig.access) ? 'field-invalid' : 'field-missing',
|
|
337
|
+
message: 'package.json publishConfig.access must exist and equal "public".',
|
|
338
|
+
path: request.packageJsonPath,
|
|
339
|
+
profile: request.profile,
|
|
340
|
+
ruleId: 'package.json.publish-config.public',
|
|
341
|
+
severity: 'error',
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
if (!isNonEmptyString(packageJson.packageManager)) {
|
|
345
|
+
diagnostics.push(createDiagnostic({
|
|
346
|
+
code: 'field-missing',
|
|
347
|
+
message: 'package.json must define a Bun-aligned packageManager field.',
|
|
348
|
+
path: request.packageJsonPath,
|
|
349
|
+
profile: request.profile,
|
|
350
|
+
ruleId: 'package.json.package-manager.required',
|
|
351
|
+
severity: 'error',
|
|
352
|
+
}));
|
|
353
|
+
request.plannedChanges.push(createPlannedChange({
|
|
354
|
+
description: `Add packageManager: "bun@${Bun.version}" to package.json.`,
|
|
355
|
+
filePath: request.packageJsonPath,
|
|
356
|
+
kind: 'update-json',
|
|
357
|
+
ruleId: 'package.json.package-manager.required',
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
else if (!packageJson.packageManager.startsWith('bun@')) {
|
|
361
|
+
diagnostics.push(createDiagnostic({
|
|
362
|
+
code: 'field-invalid',
|
|
363
|
+
message: 'package.json packageManager must be Bun-aligned, for example "bun@1.x".',
|
|
364
|
+
path: request.packageJsonPath,
|
|
365
|
+
profile: request.profile,
|
|
366
|
+
ruleId: 'package.json.package-manager.bun',
|
|
367
|
+
severity: 'error',
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
const scripts = isRecord(packageJson.scripts) ? packageJson.scripts : null;
|
|
371
|
+
for (const scriptName of REQUIRED_PUBLIC_PACKAGE_SCRIPTS) {
|
|
372
|
+
if (scripts === null || !isNonEmptyString(scripts[scriptName])) {
|
|
373
|
+
diagnostics.push(createDiagnostic({
|
|
374
|
+
code: 'missing-script',
|
|
375
|
+
message: `Missing required package script: ${scriptName}`,
|
|
376
|
+
path: request.packageJsonPath,
|
|
377
|
+
profile: request.profile,
|
|
378
|
+
ruleId: mapScriptRule(scriptName),
|
|
379
|
+
severity: 'error',
|
|
380
|
+
}));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const devDependencies = isRecord(packageJson.devDependencies)
|
|
384
|
+
? packageJson.devDependencies
|
|
385
|
+
: null;
|
|
386
|
+
if (!hasDependency(devDependencies, 'typescript')) {
|
|
387
|
+
diagnostics.push(createDiagnostic({
|
|
388
|
+
code: 'missing-dependency',
|
|
389
|
+
message: 'Public package repos must declare TypeScript in devDependencies.',
|
|
390
|
+
path: request.packageJsonPath,
|
|
391
|
+
profile: request.profile,
|
|
392
|
+
ruleId: 'package.dependencies.typescript.required',
|
|
393
|
+
severity: 'error',
|
|
394
|
+
}));
|
|
395
|
+
}
|
|
396
|
+
if (!hasDependency(devDependencies, '@changesets/cli')) {
|
|
397
|
+
diagnostics.push(createDiagnostic({
|
|
398
|
+
code: 'missing-dependency',
|
|
399
|
+
message: 'Public package repos must declare @changesets/cli in devDependencies.',
|
|
400
|
+
path: request.packageJsonPath,
|
|
401
|
+
profile: request.profile,
|
|
402
|
+
ruleId: 'package.dependencies.changesets.required',
|
|
403
|
+
severity: 'error',
|
|
404
|
+
}));
|
|
405
|
+
}
|
|
406
|
+
if (!hasDependency(devDependencies, '@types/bun')) {
|
|
407
|
+
diagnostics.push(createDiagnostic({
|
|
408
|
+
code: 'missing-dependency',
|
|
409
|
+
message: 'Public package repos must declare @types/bun in devDependencies.',
|
|
410
|
+
path: request.packageJsonPath,
|
|
411
|
+
profile: request.profile,
|
|
412
|
+
ruleId: 'package.dependencies.types-bun.required',
|
|
413
|
+
severity: 'error',
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
if (!hasDependency(devDependencies, '@types/node')) {
|
|
417
|
+
diagnostics.push(createDiagnostic({
|
|
418
|
+
code: 'missing-dependency',
|
|
419
|
+
message: 'Public package repos must declare @types/node in devDependencies.',
|
|
420
|
+
path: request.packageJsonPath,
|
|
421
|
+
profile: request.profile,
|
|
422
|
+
ruleId: 'package.dependencies.types-node.required',
|
|
423
|
+
severity: 'error',
|
|
424
|
+
}));
|
|
425
|
+
}
|
|
426
|
+
if (expectsDevtools(packageJson) && !hasDependency(devDependencies, '@ankhorage/devtools')) {
|
|
427
|
+
diagnostics.push(createDiagnostic({
|
|
428
|
+
code: 'missing-dependency',
|
|
429
|
+
message: 'This package consumes shared Ankhorage lint/format/knip tooling and must declare @ankhorage/devtools.',
|
|
430
|
+
path: request.packageJsonPath,
|
|
431
|
+
profile: request.profile,
|
|
432
|
+
ruleId: 'package.dependencies.devtools.required',
|
|
433
|
+
severity: 'error',
|
|
434
|
+
}));
|
|
435
|
+
}
|
|
436
|
+
if ((await expectsParadox(request.targetPath, packageJson)) &&
|
|
437
|
+
!hasDependency(devDependencies, '@ankhorage/paradox')) {
|
|
438
|
+
diagnostics.push(createDiagnostic({
|
|
439
|
+
code: 'missing-dependency',
|
|
440
|
+
message: 'This package owns Paradox docs generation and must declare @ankhorage/paradox.',
|
|
441
|
+
path: request.packageJsonPath,
|
|
442
|
+
profile: request.profile,
|
|
443
|
+
ruleId: 'package.dependencies.paradox.required',
|
|
444
|
+
severity: 'error',
|
|
445
|
+
}));
|
|
446
|
+
}
|
|
447
|
+
return diagnostics.concat(await validateAnkhMetadataAndProvider({
|
|
448
|
+
diagnostics,
|
|
449
|
+
packageJson,
|
|
450
|
+
packageJsonPath: request.packageJsonPath,
|
|
451
|
+
plannedChanges: request.plannedChanges,
|
|
452
|
+
profile: request.profile,
|
|
453
|
+
targetPath: request.targetPath,
|
|
454
|
+
}));
|
|
455
|
+
}
|
|
456
|
+
async function validateAnkhMetadataAndProvider(request) {
|
|
87
457
|
const diagnostics = [];
|
|
458
|
+
const providerPackage = await isProviderPackage(request.targetPath);
|
|
459
|
+
const metadata = request.packageJson.ankh;
|
|
460
|
+
if (providerPackage && metadata === undefined) {
|
|
461
|
+
diagnostics.push(createDiagnostic({
|
|
462
|
+
code: 'field-missing',
|
|
463
|
+
message: 'Provider packages must define package.json.ankh metadata.',
|
|
464
|
+
path: request.packageJsonPath,
|
|
465
|
+
profile: request.profile,
|
|
466
|
+
ruleId: 'package.ankh.required-for-provider',
|
|
467
|
+
severity: 'error',
|
|
468
|
+
}));
|
|
469
|
+
request.plannedChanges.push(createPlannedChange({
|
|
470
|
+
description: 'Add a package.json.ankh block describing the implemented provider category, provider path, and capabilities.',
|
|
471
|
+
filePath: request.packageJsonPath,
|
|
472
|
+
kind: 'update-json',
|
|
473
|
+
ruleId: 'package.ankh.required-for-provider',
|
|
474
|
+
}));
|
|
475
|
+
return diagnostics;
|
|
476
|
+
}
|
|
477
|
+
if (metadata === undefined) {
|
|
478
|
+
return diagnostics;
|
|
479
|
+
}
|
|
480
|
+
const metadataValidation = validateAnkhMetadataShape(metadata, providerPackage);
|
|
481
|
+
if (metadataValidation !== null) {
|
|
482
|
+
diagnostics.push(createDiagnostic({
|
|
483
|
+
code: 'invalid-ankh-metadata',
|
|
484
|
+
message: metadataValidation,
|
|
485
|
+
path: request.packageJsonPath,
|
|
486
|
+
profile: request.profile,
|
|
487
|
+
ruleId: 'package.ankh.present.valid-shape',
|
|
488
|
+
severity: 'error',
|
|
489
|
+
}));
|
|
490
|
+
return diagnostics;
|
|
491
|
+
}
|
|
492
|
+
if (!providerPackage) {
|
|
493
|
+
return diagnostics;
|
|
494
|
+
}
|
|
495
|
+
if (!isRecord(metadata) || !isNonEmptyString(metadata.provider)) {
|
|
496
|
+
diagnostics.push(createDiagnostic({
|
|
497
|
+
code: 'invalid-ankh-metadata',
|
|
498
|
+
message: 'Provider package metadata must define a package-relative provider path.',
|
|
499
|
+
path: request.packageJsonPath,
|
|
500
|
+
profile: request.profile,
|
|
501
|
+
ruleId: 'package.ankh.provider-path.required',
|
|
502
|
+
severity: 'error',
|
|
503
|
+
}));
|
|
504
|
+
return diagnostics;
|
|
505
|
+
}
|
|
506
|
+
const inspection = await inspectProviderSource({
|
|
507
|
+
metadataProviderPath: metadata.provider,
|
|
508
|
+
packageRoot: request.targetPath,
|
|
509
|
+
profile: request.profile,
|
|
510
|
+
});
|
|
511
|
+
if (inspection.diagnostic !== null) {
|
|
512
|
+
diagnostics.push(inspection.diagnostic);
|
|
513
|
+
return diagnostics;
|
|
514
|
+
}
|
|
515
|
+
if (inspection.result === null) {
|
|
516
|
+
return diagnostics;
|
|
517
|
+
}
|
|
518
|
+
const metadataCapabilities = Array.isArray(metadata.capabilities)
|
|
519
|
+
? metadata.capabilities.filter(isNonEmptyString)
|
|
520
|
+
: [];
|
|
521
|
+
const providerCapabilities = uniqueSorted(inspection.result.capabilities);
|
|
522
|
+
const commandCapabilities = uniqueSorted(inspection.result.commandCapabilities);
|
|
523
|
+
if (!sameStringSet(metadataCapabilities, commandCapabilities)) {
|
|
524
|
+
diagnostics.push(createDiagnostic({
|
|
525
|
+
code: 'field-invalid',
|
|
526
|
+
message: 'package.json.ankh capabilities must match the implemented provider command capabilities exactly.',
|
|
527
|
+
path: request.packageJsonPath,
|
|
528
|
+
profile: request.profile,
|
|
529
|
+
ruleId: 'package.ankh.capabilities.match-provider',
|
|
530
|
+
severity: 'error',
|
|
531
|
+
}));
|
|
532
|
+
}
|
|
533
|
+
if (!sameStringSet(providerCapabilities, commandCapabilities)) {
|
|
534
|
+
diagnostics.push(createDiagnostic({
|
|
535
|
+
code: 'field-invalid',
|
|
536
|
+
message: 'Provider export capabilities must match the provider command capability surface.',
|
|
537
|
+
path: inspection.result.sourcePath,
|
|
538
|
+
profile: request.profile,
|
|
539
|
+
ruleId: 'provider.commands.match-capabilities',
|
|
540
|
+
severity: 'error',
|
|
541
|
+
}));
|
|
542
|
+
}
|
|
543
|
+
if (!sameStringSet(inspection.result.commandPaths, inspection.result.handlerPaths)) {
|
|
544
|
+
diagnostics.push(createDiagnostic({
|
|
545
|
+
code: 'field-invalid',
|
|
546
|
+
message: 'Every provider command descriptor must have a matching handler path.',
|
|
547
|
+
path: inspection.result.sourcePath,
|
|
548
|
+
profile: request.profile,
|
|
549
|
+
ruleId: 'provider.commands.match-handlers',
|
|
550
|
+
severity: 'error',
|
|
551
|
+
}));
|
|
552
|
+
}
|
|
553
|
+
return diagnostics;
|
|
554
|
+
}
|
|
555
|
+
async function inspectProviderSource(request) {
|
|
556
|
+
const sourcePath = await resolveProviderSourcePath(request.packageRoot, request.metadataProviderPath);
|
|
557
|
+
if (sourcePath === null) {
|
|
558
|
+
return {
|
|
559
|
+
diagnostic: createDiagnostic({
|
|
560
|
+
code: 'provider-source-missing',
|
|
561
|
+
message: 'Provider package metadata is present, but no provider source file could be resolved for inspection.',
|
|
562
|
+
path: request.packageRoot,
|
|
563
|
+
profile: request.profile,
|
|
564
|
+
ruleId: 'provider.source.required',
|
|
565
|
+
severity: 'error',
|
|
566
|
+
}),
|
|
567
|
+
result: null,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
const importedModule = await import(pathToFileURL(sourcePath).href);
|
|
572
|
+
const provider = isRecord(importedModule) ? importedModule.default : undefined;
|
|
573
|
+
if (!isRecord(provider)) {
|
|
574
|
+
return {
|
|
575
|
+
diagnostic: createDiagnostic({
|
|
576
|
+
code: 'provider-source-import-failed',
|
|
577
|
+
message: 'Provider source default export must be an object.',
|
|
578
|
+
path: sourcePath,
|
|
579
|
+
profile: request.profile,
|
|
580
|
+
ruleId: 'provider.source.importable',
|
|
581
|
+
severity: 'error',
|
|
582
|
+
}),
|
|
583
|
+
result: null,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
const capabilities = readStringArray(provider.capabilities);
|
|
587
|
+
const commands = Array.isArray(provider.commands) ? provider.commands : [];
|
|
588
|
+
const handlers = Array.isArray(provider.handlers) ? provider.handlers : [];
|
|
589
|
+
const commandCapabilities = commands.flatMap((command) => isRecord(command) && isNonEmptyString(command.capability) ? [command.capability] : []);
|
|
590
|
+
const commandPaths = commands.flatMap((command) => {
|
|
591
|
+
const pathValue = isRecord(command) ? command.path : undefined;
|
|
592
|
+
return isStringTuple(pathValue) ? [pathValue.join(' ')] : [];
|
|
593
|
+
});
|
|
594
|
+
const handlerPaths = handlers.flatMap((handler) => {
|
|
595
|
+
const pathValue = isRecord(handler) ? handler.path : undefined;
|
|
596
|
+
const handlerFunction = isRecord(handler) ? handler.handler : undefined;
|
|
597
|
+
return isStringTuple(pathValue) && typeof handlerFunction === 'function'
|
|
598
|
+
? [pathValue.join(' ')]
|
|
599
|
+
: [];
|
|
600
|
+
});
|
|
601
|
+
return {
|
|
602
|
+
diagnostic: null,
|
|
603
|
+
result: {
|
|
604
|
+
capabilities,
|
|
605
|
+
commandCapabilities,
|
|
606
|
+
commandPaths,
|
|
607
|
+
handlerPaths,
|
|
608
|
+
sourcePath,
|
|
609
|
+
},
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
614
|
+
return {
|
|
615
|
+
diagnostic: createDiagnostic({
|
|
616
|
+
code: 'provider-source-import-failed',
|
|
617
|
+
message: `Failed to import provider source: ${message}`,
|
|
618
|
+
path: sourcePath,
|
|
619
|
+
profile: request.profile,
|
|
620
|
+
ruleId: 'provider.source.importable',
|
|
621
|
+
severity: 'error',
|
|
622
|
+
}),
|
|
623
|
+
result: null,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
async function resolveProviderSourcePath(packageRoot, metadataProviderPath) {
|
|
628
|
+
const candidates = new Set();
|
|
629
|
+
candidates.add(path.resolve(packageRoot, metadataProviderPath));
|
|
630
|
+
if (metadataProviderPath.startsWith('./dist/') && metadataProviderPath.endsWith('.js')) {
|
|
631
|
+
const relativeStem = metadataProviderPath.slice('./dist/'.length, -'.js'.length);
|
|
632
|
+
candidates.add(path.resolve(packageRoot, 'src', `${relativeStem}.ts`));
|
|
633
|
+
candidates.add(path.resolve(packageRoot, 'src', `${relativeStem}.tsx`));
|
|
634
|
+
candidates.add(path.resolve(packageRoot, 'src', `${relativeStem}.js`));
|
|
635
|
+
candidates.add(path.resolve(packageRoot, 'src', `${relativeStem}.mjs`));
|
|
636
|
+
}
|
|
637
|
+
for (const providerSourceCandidate of PROVIDER_SOURCE_CANDIDATES) {
|
|
638
|
+
candidates.add(path.resolve(packageRoot, providerSourceCandidate));
|
|
639
|
+
}
|
|
640
|
+
for (const candidate of candidates) {
|
|
641
|
+
if (await pathExists(candidate)) {
|
|
642
|
+
return candidate;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
async function findRepoMarkers(targetPath) {
|
|
648
|
+
return (await Promise.all(REPO_MARKERS.map(async (marker) => (await pathExists(path.join(targetPath, marker.relativePath))) ? marker.label : null))).filter((marker) => marker !== null);
|
|
649
|
+
}
|
|
650
|
+
async function readPackageJson(packageJsonPath) {
|
|
88
651
|
let parsed;
|
|
89
652
|
try {
|
|
90
|
-
parsed = JSON.parse(await fs.readFile(packageJsonPath,
|
|
653
|
+
parsed = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
91
654
|
}
|
|
92
655
|
catch (error) {
|
|
93
|
-
|
|
94
|
-
|
|
656
|
+
return {
|
|
657
|
+
diagnostic: createDiagnostic({
|
|
658
|
+
code: 'invalid-package-json',
|
|
659
|
+
message: createInvalidPackageJsonMessage(error),
|
|
660
|
+
path: packageJsonPath,
|
|
661
|
+
profile: 'unknown',
|
|
662
|
+
ruleId: 'target.package-json.required',
|
|
663
|
+
severity: 'error',
|
|
664
|
+
}),
|
|
665
|
+
parsed: null,
|
|
666
|
+
};
|
|
95
667
|
}
|
|
96
668
|
if (!isRecord(parsed)) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
669
|
+
return {
|
|
670
|
+
diagnostic: createDiagnostic({
|
|
671
|
+
code: 'invalid-package-json',
|
|
672
|
+
message: 'package.json must contain a JSON object.',
|
|
673
|
+
path: packageJsonPath,
|
|
674
|
+
profile: 'unknown',
|
|
675
|
+
ruleId: 'target.package-json.required',
|
|
676
|
+
severity: 'error',
|
|
677
|
+
}),
|
|
678
|
+
parsed: null,
|
|
679
|
+
};
|
|
102
680
|
}
|
|
103
|
-
|
|
104
|
-
|
|
681
|
+
return {
|
|
682
|
+
diagnostic: null,
|
|
683
|
+
parsed,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
function detectProfile(repoMarkers, packageJson) {
|
|
687
|
+
if (packageJson !== null) {
|
|
688
|
+
if (packageJson.name === 'ankhorage4') {
|
|
689
|
+
return 'integration-monorepo';
|
|
690
|
+
}
|
|
691
|
+
if (isNonEmptyString(packageJson.name) && packageJson.name.startsWith('@ankhorage/')) {
|
|
692
|
+
return packageJson.private === true ? 'unknown' : 'public-package';
|
|
693
|
+
}
|
|
105
694
|
}
|
|
106
|
-
if (
|
|
107
|
-
|
|
695
|
+
if (repoMarkers.length > 0) {
|
|
696
|
+
return 'unknown';
|
|
108
697
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
698
|
+
return 'unknown';
|
|
699
|
+
}
|
|
700
|
+
async function isProviderPackage(targetPath) {
|
|
701
|
+
for (const candidate of PROVIDER_SOURCE_CANDIDATES) {
|
|
702
|
+
if (await pathExists(path.join(targetPath, candidate))) {
|
|
703
|
+
return true;
|
|
113
704
|
}
|
|
114
705
|
}
|
|
115
|
-
return
|
|
706
|
+
return false;
|
|
116
707
|
}
|
|
117
|
-
async function
|
|
708
|
+
async function maybeRequirePath(diagnostics, plannedChanges, rootPath, relativePath, ruleId, kind) {
|
|
118
709
|
const absolutePath = path.join(rootPath, relativePath);
|
|
119
710
|
if (await pathExists(absolutePath)) {
|
|
120
711
|
return;
|
|
121
712
|
}
|
|
122
|
-
diagnostics.push(createDiagnostic(
|
|
713
|
+
diagnostics.push(createDiagnostic({
|
|
714
|
+
code: 'missing-path',
|
|
715
|
+
message: `Missing expected repo artifact: ${relativePath}`,
|
|
716
|
+
path: absolutePath,
|
|
717
|
+
profile: 'public-package',
|
|
718
|
+
ruleId,
|
|
719
|
+
severity: 'error',
|
|
720
|
+
}));
|
|
721
|
+
plannedChanges.push(createPlannedChange({
|
|
722
|
+
description: `Create missing ${relativePath}.`,
|
|
723
|
+
filePath: absolutePath,
|
|
724
|
+
kind,
|
|
725
|
+
ruleId,
|
|
726
|
+
}));
|
|
727
|
+
}
|
|
728
|
+
function requireNonEmptyStringField(request) {
|
|
729
|
+
if (isNonEmptyString(request.packageJson[request.fieldName])) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
request.diagnostics.push(createDiagnostic({
|
|
733
|
+
code: 'field-missing',
|
|
734
|
+
message: `package.json must define a non-empty "${request.fieldName}" field.`,
|
|
735
|
+
path: request.packageJsonPath,
|
|
736
|
+
profile: request.profile,
|
|
737
|
+
ruleId: request.ruleId,
|
|
738
|
+
severity: 'error',
|
|
739
|
+
}));
|
|
740
|
+
}
|
|
741
|
+
function requireRecordField(request) {
|
|
742
|
+
if (isRecord(request.packageJson[request.fieldName])) {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
request.diagnostics.push(createDiagnostic({
|
|
746
|
+
code: 'field-missing',
|
|
747
|
+
message: `package.json must define an object "${request.fieldName}" field.`,
|
|
748
|
+
path: request.packageJsonPath,
|
|
749
|
+
profile: request.profile,
|
|
750
|
+
ruleId: request.ruleId,
|
|
751
|
+
severity: 'error',
|
|
752
|
+
}));
|
|
753
|
+
}
|
|
754
|
+
function requireStringArrayField(request) {
|
|
755
|
+
if (readStringArray(request.packageJson[request.fieldName]).length > 0) {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
request.diagnostics.push(createDiagnostic({
|
|
759
|
+
code: 'field-missing',
|
|
760
|
+
message: `package.json must define a non-empty string array "${request.fieldName}" field.`,
|
|
761
|
+
path: request.packageJsonPath,
|
|
762
|
+
profile: request.profile,
|
|
763
|
+
ruleId: request.ruleId,
|
|
764
|
+
severity: 'error',
|
|
765
|
+
}));
|
|
766
|
+
}
|
|
767
|
+
async function expectsParadox(targetPath, packageJson) {
|
|
768
|
+
const scripts = isRecord(packageJson.scripts) ? packageJson.scripts : null;
|
|
769
|
+
const docsScript = scripts?.docs;
|
|
770
|
+
if (isNonEmptyString(docsScript) &&
|
|
771
|
+
(docsScript.includes('paradox') || docsScript.includes('@ankhorage/paradox'))) {
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
return ((await pathExists(path.join(targetPath, 'paradox.config.ts'))) ||
|
|
775
|
+
(await pathExists(path.join(targetPath, 'src', 'readme-usage.ts'))));
|
|
123
776
|
}
|
|
124
|
-
function
|
|
777
|
+
function expectsDevtools(packageJson) {
|
|
778
|
+
const scripts = isRecord(packageJson.scripts) ? packageJson.scripts : null;
|
|
779
|
+
if (scripts === null) {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
return Object.values(scripts).some((scriptValue) => isNonEmptyString(scriptValue) &&
|
|
783
|
+
(scriptValue.includes('ankhorage-eslint') ||
|
|
784
|
+
scriptValue.includes('ankhorage-prettier') ||
|
|
785
|
+
scriptValue.includes('ankhorage-knip')));
|
|
786
|
+
}
|
|
787
|
+
function hasDependency(dependencyMap, dependencyName) {
|
|
788
|
+
return dependencyMap !== null && isNonEmptyString(dependencyMap[dependencyName]);
|
|
789
|
+
}
|
|
790
|
+
function validateAnkhMetadataShape(value, providerPackage) {
|
|
125
791
|
if (!isRecord(value)) {
|
|
126
792
|
return 'package.json "ankh" metadata must be an object when present.';
|
|
127
793
|
}
|
|
128
794
|
if (!isNonEmptyString(value.category)) {
|
|
129
795
|
return 'package.json "ankh.category" must be a non-empty string.';
|
|
130
796
|
}
|
|
131
|
-
if (
|
|
132
|
-
|
|
797
|
+
if (providerPackage &&
|
|
798
|
+
!(isNonEmptyString(value.provider) && isPackageRelativeProviderPath(value.provider))) {
|
|
799
|
+
return 'Provider package metadata must define a package-relative "./..." provider path.';
|
|
800
|
+
}
|
|
801
|
+
if (!providerPackage &&
|
|
802
|
+
value.provider !== undefined &&
|
|
803
|
+
!(value.provider === null || isPackageRelativeProviderPath(value.provider))) {
|
|
804
|
+
return 'Non-provider package metadata, when present, must use null or a package-relative "./..." provider value.';
|
|
133
805
|
}
|
|
134
806
|
if (!Array.isArray(value.capabilities) ||
|
|
135
807
|
value.capabilities.some((capability) => !isNonEmptyString(capability))) {
|
|
@@ -137,12 +809,53 @@ function validateAnkhMetadataShape(value) {
|
|
|
137
809
|
}
|
|
138
810
|
return null;
|
|
139
811
|
}
|
|
140
|
-
function
|
|
812
|
+
function mapScriptRule(scriptName) {
|
|
813
|
+
switch (scriptName) {
|
|
814
|
+
case 'build':
|
|
815
|
+
return 'package.scripts.build.required';
|
|
816
|
+
case 'typecheck':
|
|
817
|
+
return 'package.scripts.typecheck.required';
|
|
818
|
+
case 'lint':
|
|
819
|
+
return 'package.scripts.lint.required';
|
|
820
|
+
case 'lint:fix':
|
|
821
|
+
return 'package.scripts.lint-fix.required';
|
|
822
|
+
case 'format':
|
|
823
|
+
return 'package.scripts.format.required';
|
|
824
|
+
case 'format:check':
|
|
825
|
+
return 'package.scripts.format-check.required';
|
|
826
|
+
case 'test':
|
|
827
|
+
return 'package.scripts.test.required';
|
|
828
|
+
case 'knip':
|
|
829
|
+
return 'package.scripts.knip.required';
|
|
830
|
+
case 'docs':
|
|
831
|
+
return 'package.scripts.docs.required';
|
|
832
|
+
case 'changeset':
|
|
833
|
+
return 'package.scripts.changeset.required';
|
|
834
|
+
case 'changeset:status':
|
|
835
|
+
return 'package.scripts.changeset-status.required';
|
|
836
|
+
case 'version-packages':
|
|
837
|
+
return 'package.scripts.version-packages.required';
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function createEarlyResult(targetPath, diagnostic) {
|
|
141
841
|
return {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
842
|
+
appliedChecks: [],
|
|
843
|
+
diagnostics: [diagnostic],
|
|
844
|
+
fixPlan: null,
|
|
845
|
+
hasPackageJson: false,
|
|
846
|
+
plannedChanges: [],
|
|
847
|
+
profile: 'unknown',
|
|
848
|
+
repoMarkers: [],
|
|
849
|
+
targetPath,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
function createDiagnostic(diagnostic) {
|
|
853
|
+
return diagnostic;
|
|
854
|
+
}
|
|
855
|
+
function createPlannedChange(change) {
|
|
856
|
+
return {
|
|
857
|
+
...change,
|
|
858
|
+
safe: true,
|
|
146
859
|
};
|
|
147
860
|
}
|
|
148
861
|
function createInvalidPackageJsonMessage(error) {
|
|
@@ -165,17 +878,31 @@ async function statOrNull(targetPath) {
|
|
|
165
878
|
}
|
|
166
879
|
function isMissingPathError(error) {
|
|
167
880
|
return (error instanceof Error &&
|
|
168
|
-
|
|
169
|
-
typeof error.code ===
|
|
170
|
-
error.code ===
|
|
881
|
+
'code' in error &&
|
|
882
|
+
typeof error.code === 'string' &&
|
|
883
|
+
error.code === 'ENOENT');
|
|
171
884
|
}
|
|
172
885
|
function isNonEmptyString(value) {
|
|
173
|
-
return typeof value ===
|
|
886
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
174
887
|
}
|
|
175
888
|
function isPackageRelativeProviderPath(value) {
|
|
176
|
-
return isNonEmptyString(value) && value.startsWith(
|
|
889
|
+
return isNonEmptyString(value) && value.startsWith('./');
|
|
177
890
|
}
|
|
178
891
|
function isRecord(value) {
|
|
179
|
-
return typeof value ===
|
|
892
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
893
|
+
}
|
|
894
|
+
function isStringTuple(value) {
|
|
895
|
+
return Array.isArray(value) && value.length > 0 && value.every(isNonEmptyString);
|
|
896
|
+
}
|
|
897
|
+
function readStringArray(value) {
|
|
898
|
+
return Array.isArray(value) ? value.filter(isNonEmptyString) : [];
|
|
899
|
+
}
|
|
900
|
+
function sameStringSet(left, right) {
|
|
901
|
+
const leftSet = uniqueSorted(left);
|
|
902
|
+
const rightSet = uniqueSorted(right);
|
|
903
|
+
return (leftSet.length === rightSet.length && leftSet.every((value, index) => value === rightSet[index]));
|
|
904
|
+
}
|
|
905
|
+
function uniqueSorted(values) {
|
|
906
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
180
907
|
}
|
|
181
908
|
//# sourceMappingURL=analysis.js.map
|