@ankhorage/doctor 0.4.0 → 0.6.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.
@@ -1 +1 @@
1
- {"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAqFA,MAAM,UAAU,qBAAqB,CAAC,WAAwC;IAC5E,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,MAAM,CAAC;AACpF,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,WAAwC;IAC9E,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,MAAM,CAAC;AACtF,CAAC","sourcesContent":["export type DoctorDiagnosticCode =\n | 'field-invalid'\n | 'field-missing'\n | 'invalid-ankh-metadata'\n | 'invalid-package-json'\n | 'missing-dependency'\n | 'missing-path'\n | 'missing-script'\n | 'provider-source-import-failed'\n | 'provider-source-missing'\n | 'target-not-directory'\n | 'target-not-found'\n | 'unsupported-target';\n\nexport type DoctorDiagnosticSeverity = 'error' | 'warning';\nexport type DoctorPolicyProfile = 'integration-monorepo' | 'public-package' | 'unknown';\nexport type DoctorRuleId =\n | 'integration.package.ankh.disallowed'\n | 'integration.package.private.required'\n | 'integration.package.workspaces.required'\n | 'package.ankh.capabilities.match-provider'\n | 'package.ankh.present.valid-shape'\n | 'package.ankh.provider-path.required'\n | 'package.ankh.required-for-provider'\n | 'package.cli.index.required'\n | 'package.cli.root-file.disallowed'\n | 'package.dependencies.changesets.required'\n | 'package.dependencies.devtools.required'\n | 'package.dependencies.paradox.required'\n | 'package.dependencies.types-bun.required'\n | 'package.dependencies.types-node.required'\n | 'package.dependencies.typescript.required'\n | 'package.json.bugs.required'\n | 'package.json.description.required'\n | 'package.json.exports.required'\n | 'package.json.files.required'\n | 'package.json.homepage.required'\n | 'package.json.keywords.required'\n | 'package.json.license.required'\n | 'package.json.name.required'\n | 'package.json.package-manager.bun'\n | 'package.json.package-manager.required'\n | 'package.json.private.public-package-disallowed'\n | 'package.json.publish-config.public'\n | 'package.json.publish-config.required'\n | 'package.json.repository.required'\n | 'package.json.type.module'\n | 'package.json.type.required'\n | 'package.json.version.required'\n | 'package.scripts.build.required'\n | 'package.scripts.changeset-status.required'\n | 'package.scripts.changeset.required'\n | 'package.scripts.docs.required'\n | 'package.scripts.format-check.required'\n | 'package.scripts.format.required'\n | 'package.scripts.knip.required'\n | 'package.scripts.lint-fix.required'\n | 'package.scripts.lint.required'\n | 'package.scripts.test.required'\n | 'package.scripts.typecheck.required'\n | 'package.scripts.version-packages.required'\n | 'provider.commands.match-capabilities'\n | 'provider.commands.match-handlers'\n | 'provider.source.importable'\n | 'provider.source.required'\n | 'repo.changelog.required'\n | 'repo.changeset.required'\n | 'repo.license.required'\n | 'repo.readme.required'\n | 'repo.workflows.required'\n | 'target.package-json.required'\n | 'target.path.directory'\n | 'target.path.exists'\n | 'target.profile.supported'\n | 'target.repo-markers.required';\n\nexport interface DoctorDiagnostic {\n readonly code: DoctorDiagnosticCode;\n readonly message: string;\n readonly path: string;\n readonly profile: DoctorPolicyProfile;\n readonly ruleId: DoctorRuleId;\n readonly severity: DoctorDiagnosticSeverity;\n}\n\nexport function countErrorDiagnostics(diagnostics: readonly DoctorDiagnostic[]): number {\n return diagnostics.filter((diagnostic) => diagnostic.severity === 'error').length;\n}\n\nexport function countWarningDiagnostics(diagnostics: readonly DoctorDiagnostic[]): number {\n return diagnostics.filter((diagnostic) => diagnostic.severity === 'warning').length;\n}\n"]}
1
+ {"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AA+GA,MAAM,UAAU,qBAAqB,CAAC,WAAwC;IAC5E,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,MAAM,CAAC;AACpF,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,WAAwC;IAC9E,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,MAAM,CAAC;AACtF,CAAC","sourcesContent":["export type DoctorDiagnosticCode =\n | 'field-invalid'\n | 'field-missing'\n | 'invalid-ankh-metadata'\n | 'invalid-app-manifest-json'\n | 'invalid-package-json'\n | 'missing-dependency'\n | 'missing-path'\n | 'missing-script'\n | 'provider-source-import-failed'\n | 'provider-source-missing'\n | 'target-not-directory'\n | 'target-not-found'\n | 'unsupported-target';\n\nexport type DoctorDiagnosticSeverity = 'error' | 'warning';\nexport type DoctorPolicyProfile =\n 'app-manifest' | 'integration-monorepo' | 'public-package' | 'unknown';\nexport type DoctorRuleId =\n | 'integration.package.ankh.disallowed'\n | 'integration.package.private.required'\n | 'integration.package.workspaces.required'\n | 'manifest.auth.authorization.engine.valid'\n | 'manifest.auth.authorization.kind.valid'\n | 'manifest.auth.authorization.valid-shape'\n | 'manifest.auth.flow.post-sign-in.distinct'\n | 'manifest.auth.flow.route.required'\n | 'manifest.auth.flow.route.unique'\n | 'manifest.auth.flow.route.valid'\n | 'manifest.auth.flow.unauthorized-route.valid'\n | 'manifest.auth.flow.valid-shape'\n | 'manifest.auth.valid-shape'\n | 'manifest.infra.valid-shape'\n | 'manifest.json.readable'\n | 'manifest.json.valid'\n | 'manifest.root.valid-shape'\n | 'manifest.settings.auth-flow.removed'\n | 'package.ankh.capabilities.match-provider'\n | 'package.ankh.present.valid-shape'\n | 'package.ankh.provider-path.required'\n | 'package.ankh.required-for-provider'\n | 'package.cli.export.required'\n | 'package.cli.index.required'\n | 'package.cli.root-file.disallowed'\n | 'package.dependencies.ankh-workspace-alias.disallowed'\n | 'package.dependencies.ankhorage4-source.disallowed'\n | 'package.dependencies.changesets.required'\n | 'package.dependencies.devtools.required'\n | 'package.dependencies.paradox.required'\n | 'package.dependencies.types-bun.required'\n | 'package.dependencies.types-node.required'\n | 'package.dependencies.typescript.required'\n | 'package.imports.ankh-workspace-alias.disallowed'\n | 'package.imports.ankhorage4-source.disallowed'\n | 'package.json.bugs.required'\n | 'package.json.description.required'\n | 'package.json.exports.required'\n | 'package.json.files.required'\n | 'package.json.homepage.required'\n | 'package.json.keywords.required'\n | 'package.json.license.required'\n | 'package.json.name.required'\n | 'package.json.package-manager.bun'\n | 'package.json.package-manager.required'\n | 'package.json.private.public-package-disallowed'\n | 'package.json.publish-config.public'\n | 'package.json.publish-config.required'\n | 'package.json.repository.required'\n | 'package.json.type.module'\n | 'package.json.type.required'\n | 'package.json.version.required'\n | 'package.scripts.build.required'\n | 'package.scripts.changeset-status.required'\n | 'package.scripts.changeset.required'\n | 'package.scripts.docs.required'\n | 'package.scripts.format-check.required'\n | 'package.scripts.format.required'\n | 'package.scripts.knip.required'\n | 'package.scripts.lint-fix.required'\n | 'package.scripts.lint.required'\n | 'package.scripts.test.required'\n | 'package.scripts.typecheck.required'\n | 'package.scripts.version-packages.required'\n | 'provider.commands.match-capabilities'\n | 'provider.commands.match-handlers'\n | 'provider.source.importable'\n | 'provider.source.required'\n | 'repo.changelog.required'\n | 'repo.changeset.required'\n | 'repo.license.required'\n | 'repo.readme.required'\n | 'repo.workflows.required'\n | 'studio.dependencies.dnd.required'\n | 'studio.dependencies.runtime.required'\n | 'studio.imports.dnd.required'\n | 'studio.imports.runtime.required'\n | 'target.package-json.required'\n | 'target.path.directory'\n | 'target.path.exists'\n | 'target.profile.supported'\n | 'target.repo-markers.required';\n\nexport interface DoctorDiagnostic {\n readonly code: DoctorDiagnosticCode;\n readonly message: string;\n readonly path: string;\n readonly profile: DoctorPolicyProfile;\n readonly ruleId: DoctorRuleId;\n readonly severity: DoctorDiagnosticSeverity;\n}\n\nexport function countErrorDiagnostics(diagnostics: readonly DoctorDiagnostic[]): number {\n return diagnostics.filter((diagnostic) => diagnostic.severity === 'error').length;\n}\n\nexport function countWarningDiagnostics(diagnostics: readonly DoctorDiagnostic[]): number {\n return diagnostics.filter((diagnostic) => diagnostic.severity === 'warning').length;\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -6,5 +6,6 @@ export { analyzeDoctorTargetWithCliLayout } from './cliLayoutAnalysis.js';
6
6
  export type { DoctorCommandContext, DoctorCommandRunResult } from './commandContext.js';
7
7
  export { createDefaultCommandContext } from './commandContext.js';
8
8
  export type { DoctorDiagnostic, DoctorDiagnosticCode, DoctorDiagnosticSeverity, DoctorPolicyProfile, DoctorRuleId, } from './diagnostics.js';
9
+ export { analyzeAppManifest, analyzeAppManifestFile, analyzeAppManifestTarget, } from './manifestAnalysis.js';
9
10
  export { DOCTOR_CAPABILITIES, DOCTOR_COMMAND_CATEGORY, DOCTOR_PACKAGE_METADATA, DOCTOR_PACKAGE_NAME, DOCTOR_PACKAGE_VERSION, } from './packageMetadata.js';
10
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,qBAAqB,EACrB,oBAAoB,EACpB,aAAa,EACb,mBAAmB,EACnB,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EACL,2BAA2B,EAC3B,KAAK,kCAAkC,GACxC,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,gCAAgC,EAAE,MAAM,wBAAwB,CAAC;AAC1E,YAAY,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACxF,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAClE,YAAY,EACV,gBAAgB,EAChB,oBAAoB,EACpB,wBAAwB,EACxB,mBAAmB,EACnB,YAAY,GACb,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,qBAAqB,EACrB,oBAAoB,EACpB,aAAa,EACb,mBAAmB,EACnB,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EACL,2BAA2B,EAC3B,KAAK,kCAAkC,GACxC,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,gCAAgC,EAAE,MAAM,wBAAwB,CAAC;AAC1E,YAAY,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACxF,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAClE,YAAY,EACV,gBAAgB,EAChB,oBAAoB,EACpB,wBAAwB,EACxB,mBAAmB,EACnB,YAAY,GACb,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -3,5 +3,6 @@ export { createDoctorRuntimeProvider, } from './cli/index.js';
3
3
  export { runCli } from './cli/standalone.js';
4
4
  export { analyzeDoctorTargetWithCliLayout } from './cliLayoutAnalysis.js';
5
5
  export { createDefaultCommandContext } from './commandContext.js';
6
+ export { analyzeAppManifest, analyzeAppManifestFile, analyzeAppManifestTarget, } from './manifestAnalysis.js';
6
7
  export { DOCTOR_CAPABILITIES, DOCTOR_COMMAND_CATEGORY, DOCTOR_PACKAGE_METADATA, DOCTOR_PACKAGE_NAME, DOCTOR_PACKAGE_VERSION, } from './packageMetadata.js';
7
8
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EACL,2BAA2B,GAE5B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,gCAAgC,EAAE,MAAM,wBAAwB,CAAC;AAE1E,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAQlE,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC","sourcesContent":["export type {\n DoctorAnalysisRequest,\n DoctorAnalysisResult,\n DoctorFixPlan,\n DoctorPlannedChange,\n DoctorPlannedChangeKind,\n DoctorTargetCheck,\n DoctorTargetMode,\n} from './analysis.js';\nexport { analyzeDoctorTarget } from './analysis.js';\nexport {\n createDoctorRuntimeProvider,\n type CreateDoctorRuntimeProviderOptions,\n} from './cli/index.js';\nexport { runCli } from './cli/standalone.js';\nexport { analyzeDoctorTargetWithCliLayout } from './cliLayoutAnalysis.js';\nexport type { DoctorCommandContext, DoctorCommandRunResult } from './commandContext.js';\nexport { createDefaultCommandContext } from './commandContext.js';\nexport type {\n DoctorDiagnostic,\n DoctorDiagnosticCode,\n DoctorDiagnosticSeverity,\n DoctorPolicyProfile,\n DoctorRuleId,\n} from './diagnostics.js';\nexport {\n DOCTOR_CAPABILITIES,\n DOCTOR_COMMAND_CATEGORY,\n DOCTOR_PACKAGE_METADATA,\n DOCTOR_PACKAGE_NAME,\n DOCTOR_PACKAGE_VERSION,\n} from './packageMetadata.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EACL,2BAA2B,GAE5B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,gCAAgC,EAAE,MAAM,wBAAwB,CAAC;AAE1E,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAQlE,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC","sourcesContent":["export type {\n DoctorAnalysisRequest,\n DoctorAnalysisResult,\n DoctorFixPlan,\n DoctorPlannedChange,\n DoctorPlannedChangeKind,\n DoctorTargetCheck,\n DoctorTargetMode,\n} from './analysis.js';\nexport { analyzeDoctorTarget } from './analysis.js';\nexport {\n createDoctorRuntimeProvider,\n type CreateDoctorRuntimeProviderOptions,\n} from './cli/index.js';\nexport { runCli } from './cli/standalone.js';\nexport { analyzeDoctorTargetWithCliLayout } from './cliLayoutAnalysis.js';\nexport type { DoctorCommandContext, DoctorCommandRunResult } from './commandContext.js';\nexport { createDefaultCommandContext } from './commandContext.js';\nexport type {\n DoctorDiagnostic,\n DoctorDiagnosticCode,\n DoctorDiagnosticSeverity,\n DoctorPolicyProfile,\n DoctorRuleId,\n} from './diagnostics.js';\nexport {\n analyzeAppManifest,\n analyzeAppManifestFile,\n analyzeAppManifestTarget,\n} from './manifestAnalysis.js';\nexport {\n DOCTOR_CAPABILITIES,\n DOCTOR_COMMAND_CATEGORY,\n DOCTOR_PACKAGE_METADATA,\n DOCTOR_PACKAGE_NAME,\n DOCTOR_PACKAGE_VERSION,\n} from './packageMetadata.js';\n"]}
@@ -0,0 +1,6 @@
1
+ import type { DoctorAnalysisRequest, DoctorAnalysisResult } from './analysis.js';
2
+ import type { DoctorDiagnostic } from './diagnostics.js';
3
+ export declare function analyzeAppManifestTarget(request: DoctorAnalysisRequest): Promise<DoctorAnalysisResult | null>;
4
+ export declare function analyzeAppManifestFile(filePath: string): Promise<DoctorDiagnostic[]>;
5
+ export declare function analyzeAppManifest(manifest: unknown, manifestPath?: string): DoctorDiagnostic[];
6
+ //# sourceMappingURL=manifestAnalysis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifestAnalysis.d.ts","sourceRoot":"","sources":["../src/manifestAnalysis.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,qBAAqB,EACrB,oBAAoB,EAGrB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,gBAAgB,EAIjB,MAAM,kBAAkB,CAAC;AAqB1B,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAmCtC;AAED,wBAAsB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAgC1F;AAED,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,OAAO,EACjB,YAAY,SAAkB,GAC7B,gBAAgB,EAAE,CAqEpB"}
@@ -0,0 +1,322 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { AUTHZ_ENGINES, AUTHZ_KINDS } from '@ankhorage/contracts';
4
+ import { resolveAuthFlow } from '@ankhorage/contracts/auth';
5
+ const MANIFEST_PROFILE = 'app-manifest';
6
+ const AUTH_ACTION_ROUTE_KEYS = [
7
+ 'signInRoute',
8
+ 'signUpRoute',
9
+ 'signOutRoute',
10
+ 'forgotPasswordRoute',
11
+ 'otpRoute',
12
+ ];
13
+ const OPTIONAL_ROUTE_KEYS = [
14
+ 'signUpRoute',
15
+ 'signOutRoute',
16
+ 'forgotPasswordRoute',
17
+ 'otpRoute',
18
+ 'unauthorizedRoute',
19
+ ];
20
+ export async function analyzeAppManifestTarget(request) {
21
+ const targetPath = path.resolve(request.cwd, request.inputPath ?? '.');
22
+ const stats = await statOrNull(targetPath);
23
+ if (stats === null ||
24
+ !stats.isFile() ||
25
+ path.extname(targetPath).toLowerCase() !== '.json' ||
26
+ (request.mode !== 'validate' && request.mode !== 'fix')) {
27
+ return null;
28
+ }
29
+ const diagnostics = await analyzeAppManifestFile(targetPath);
30
+ const plannedChanges = [];
31
+ const fixPlan = request.mode === 'fix'
32
+ ? {
33
+ changes: plannedChanges,
34
+ diagnostics,
35
+ profile: MANIFEST_PROFILE,
36
+ targetPath,
37
+ }
38
+ : null;
39
+ return {
40
+ appliedChecks: ['manifest'],
41
+ diagnostics,
42
+ fixPlan,
43
+ hasPackageJson: false,
44
+ plannedChanges,
45
+ profile: MANIFEST_PROFILE,
46
+ repoMarkers: [],
47
+ targetPath,
48
+ };
49
+ }
50
+ export async function analyzeAppManifestFile(filePath) {
51
+ let source;
52
+ try {
53
+ source = await fs.readFile(filePath, 'utf8');
54
+ }
55
+ catch (error) {
56
+ return [
57
+ createManifestDiagnostic({
58
+ code: 'invalid-app-manifest-json',
59
+ message: `Could not read app manifest JSON: ${toErrorMessage(error)}`,
60
+ path: filePath,
61
+ ruleId: 'manifest.json.readable',
62
+ }),
63
+ ];
64
+ }
65
+ let manifest;
66
+ try {
67
+ manifest = JSON.parse(source);
68
+ }
69
+ catch (error) {
70
+ return [
71
+ createManifestDiagnostic({
72
+ code: 'invalid-app-manifest-json',
73
+ message: `App manifest must contain valid JSON: ${toErrorMessage(error)}`,
74
+ path: filePath,
75
+ ruleId: 'manifest.json.valid',
76
+ }),
77
+ ];
78
+ }
79
+ return analyzeAppManifest(manifest, filePath);
80
+ }
81
+ export function analyzeAppManifest(manifest, manifestPath = 'manifest.json') {
82
+ if (!isRecord(manifest)) {
83
+ return [
84
+ createManifestDiagnostic({
85
+ code: 'field-invalid',
86
+ message: 'App manifest root must be a JSON object.',
87
+ path: manifestPath,
88
+ ruleId: 'manifest.root.valid-shape',
89
+ }),
90
+ ];
91
+ }
92
+ const diagnostics = [];
93
+ const { settings } = manifest;
94
+ if (isRecord(settings) && hasOwn(settings, 'authFlow')) {
95
+ diagnostics.push(createManifestDiagnostic({
96
+ code: 'field-invalid',
97
+ message: 'settings.authFlow was removed. Move this configuration manually to infra.auth.flow; Doctor will not migrate it automatically.',
98
+ path: manifestPath,
99
+ ruleId: 'manifest.settings.auth-flow.removed',
100
+ }));
101
+ }
102
+ const { infra } = manifest;
103
+ if (infra === undefined) {
104
+ return diagnostics;
105
+ }
106
+ if (!isRecord(infra)) {
107
+ diagnostics.push(createManifestDiagnostic({
108
+ code: 'field-invalid',
109
+ message: 'manifest.infra must be a JSON object when present.',
110
+ path: manifestPath,
111
+ ruleId: 'manifest.infra.valid-shape',
112
+ }));
113
+ return diagnostics;
114
+ }
115
+ const { auth } = infra;
116
+ if (auth === undefined) {
117
+ return diagnostics;
118
+ }
119
+ if (!isRecord(auth)) {
120
+ diagnostics.push(createManifestDiagnostic({
121
+ code: 'field-invalid',
122
+ message: 'manifest.infra.auth must be a JSON object when present.',
123
+ path: manifestPath,
124
+ ruleId: 'manifest.auth.valid-shape',
125
+ }));
126
+ return diagnostics;
127
+ }
128
+ const flow = parseAuthFlow(auth.flow, manifestPath, diagnostics);
129
+ if (flow !== null) {
130
+ validateAuthFlowRelationships(flow, manifestPath, diagnostics);
131
+ }
132
+ validateAuthorization(auth.authorization, manifestPath, diagnostics);
133
+ return diagnostics;
134
+ }
135
+ function parseAuthFlow(value, manifestPath, diagnostics) {
136
+ if (value === undefined) {
137
+ return resolveAuthFlow();
138
+ }
139
+ if (!isRecord(value)) {
140
+ diagnostics.push(createManifestDiagnostic({
141
+ code: 'field-invalid',
142
+ message: 'manifest.infra.auth.flow must be a JSON object when present.',
143
+ path: manifestPath,
144
+ ruleId: 'manifest.auth.flow.valid-shape',
145
+ }));
146
+ return null;
147
+ }
148
+ const signInRoute = readRequiredRoute(value, 'signInRoute', manifestPath, diagnostics);
149
+ const postSignInRoute = readRequiredRoute(value, 'postSignInRoute', manifestPath, diagnostics);
150
+ const optionalRoutes = readOptionalRoutes(value, manifestPath, diagnostics);
151
+ if (signInRoute === null || postSignInRoute === null || optionalRoutes === null) {
152
+ return null;
153
+ }
154
+ return resolveAuthFlow({
155
+ signInRoute,
156
+ postSignInRoute,
157
+ ...optionalRoutes,
158
+ });
159
+ }
160
+ function readRequiredRoute(flow, key, manifestPath, diagnostics) {
161
+ const value = flow[key];
162
+ if (typeof value !== 'string' || value.trim() === '') {
163
+ diagnostics.push(createManifestDiagnostic({
164
+ code: value === undefined ? 'field-missing' : 'field-invalid',
165
+ message: `manifest.infra.auth.flow.${key} must be a non-empty string.`,
166
+ path: manifestPath,
167
+ ruleId: 'manifest.auth.flow.route.required',
168
+ }));
169
+ return null;
170
+ }
171
+ return value;
172
+ }
173
+ function readOptionalRoutes(flow, manifestPath, diagnostics) {
174
+ const routes = {};
175
+ let valid = true;
176
+ for (const key of OPTIONAL_ROUTE_KEYS) {
177
+ const value = flow[key];
178
+ if (value === undefined) {
179
+ continue;
180
+ }
181
+ if (typeof value !== 'string' || value.trim() === '') {
182
+ valid = false;
183
+ diagnostics.push(createManifestDiagnostic({
184
+ code: 'field-invalid',
185
+ message: `manifest.infra.auth.flow.${key} must be a non-empty string when present.`,
186
+ path: manifestPath,
187
+ ruleId: 'manifest.auth.flow.route.valid',
188
+ }));
189
+ continue;
190
+ }
191
+ routes[key] = value;
192
+ }
193
+ return valid ? routes : null;
194
+ }
195
+ function validateAuthFlowRelationships(flow, manifestPath, diagnostics) {
196
+ const actionRoutes = AUTH_ACTION_ROUTE_KEYS.flatMap((key) => {
197
+ const value = flow[key];
198
+ return typeof value === 'string' ? [{ key, route: normalizeRoute(value) }] : [];
199
+ });
200
+ const routesByName = new Map();
201
+ for (const entry of actionRoutes) {
202
+ const keys = routesByName.get(entry.route) ?? [];
203
+ keys.push(entry.key);
204
+ routesByName.set(entry.route, keys);
205
+ }
206
+ for (const [route, keys] of routesByName) {
207
+ if (keys.length < 2) {
208
+ continue;
209
+ }
210
+ diagnostics.push(createManifestDiagnostic({
211
+ code: 'field-invalid',
212
+ message: `Auth action routes must be unique; ${keys.join(', ')} all resolve to "${route}".`,
213
+ path: manifestPath,
214
+ ruleId: 'manifest.auth.flow.route.unique',
215
+ }));
216
+ }
217
+ const postSignInRoute = normalizeRoute(flow.postSignInRoute);
218
+ const collidingAction = actionRoutes.find((entry) => entry.route === postSignInRoute);
219
+ if (collidingAction !== undefined) {
220
+ diagnostics.push(createManifestDiagnostic({
221
+ code: 'field-invalid',
222
+ message: `postSignInRoute must resolve outside the auth action routes; it collides with ${collidingAction.key}.`,
223
+ path: manifestPath,
224
+ ruleId: 'manifest.auth.flow.post-sign-in.distinct',
225
+ }));
226
+ }
227
+ if (flow.unauthorizedRoute === undefined) {
228
+ return;
229
+ }
230
+ const unauthorizedRoute = normalizeRoute(flow.unauthorizedRoute);
231
+ const signInRoute = normalizeRoute(flow.signInRoute);
232
+ if (unauthorizedRoute === postSignInRoute) {
233
+ diagnostics.push(createManifestDiagnostic({
234
+ code: 'field-invalid',
235
+ message: 'unauthorizedRoute must not resolve to postSignInRoute.',
236
+ path: manifestPath,
237
+ ruleId: 'manifest.auth.flow.unauthorized-route.valid',
238
+ }));
239
+ }
240
+ if (unauthorizedRoute === signInRoute) {
241
+ return;
242
+ }
243
+ const collidingActionRoute = actionRoutes.find((entry) => entry.key !== 'signInRoute' && entry.route === unauthorizedRoute);
244
+ if (collidingActionRoute !== undefined) {
245
+ diagnostics.push(createManifestDiagnostic({
246
+ code: 'field-invalid',
247
+ message: `unauthorizedRoute may alias signInRoute, but it must not collide with ${collidingActionRoute.key}.`,
248
+ path: manifestPath,
249
+ ruleId: 'manifest.auth.flow.unauthorized-route.valid',
250
+ }));
251
+ }
252
+ }
253
+ function validateAuthorization(value, manifestPath, diagnostics) {
254
+ if (value === undefined) {
255
+ return;
256
+ }
257
+ if (!isRecord(value)) {
258
+ diagnostics.push(createManifestDiagnostic({
259
+ code: 'field-invalid',
260
+ message: 'manifest.infra.auth.authorization must be a JSON object when explicitly configured.',
261
+ path: manifestPath,
262
+ ruleId: 'manifest.auth.authorization.valid-shape',
263
+ }));
264
+ return;
265
+ }
266
+ validateAuthorizationField({
267
+ allowed: AUTHZ_KINDS,
268
+ diagnostics,
269
+ fieldName: 'kind',
270
+ manifestPath,
271
+ value: value.kind,
272
+ ruleId: 'manifest.auth.authorization.kind.valid',
273
+ });
274
+ validateAuthorizationField({
275
+ allowed: AUTHZ_ENGINES,
276
+ diagnostics,
277
+ fieldName: 'engine',
278
+ manifestPath,
279
+ value: value.engine,
280
+ ruleId: 'manifest.auth.authorization.engine.valid',
281
+ });
282
+ }
283
+ function validateAuthorizationField(request) {
284
+ if (typeof request.value === 'string' && request.allowed.includes(request.value)) {
285
+ return;
286
+ }
287
+ request.diagnostics.push(createManifestDiagnostic({
288
+ code: request.value === undefined ? 'field-missing' : 'field-invalid',
289
+ message: `Explicit authorization.${request.fieldName} must be one of: ${request.allowed.join(', ')}.`,
290
+ path: request.manifestPath,
291
+ ruleId: request.ruleId,
292
+ }));
293
+ }
294
+ function createManifestDiagnostic(request) {
295
+ return {
296
+ ...request,
297
+ profile: MANIFEST_PROFILE,
298
+ severity: 'error',
299
+ };
300
+ }
301
+ function normalizeRoute(route) {
302
+ const normalized = route.trim().replace(/^\/+/, '').replace(/\/+$/, '');
303
+ return normalized === '' ? 'index' : normalized;
304
+ }
305
+ function hasOwn(record, key) {
306
+ return Object.prototype.hasOwnProperty.call(record, key);
307
+ }
308
+ function isRecord(value) {
309
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
310
+ }
311
+ async function statOrNull(filePath) {
312
+ try {
313
+ return await fs.stat(filePath);
314
+ }
315
+ catch {
316
+ return null;
317
+ }
318
+ }
319
+ function toErrorMessage(error) {
320
+ return error instanceof Error ? error.message : String(error);
321
+ }
322
+ //# sourceMappingURL=manifestAnalysis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifestAnalysis.js","sourceRoot":"","sources":["../src/manifestAnalysis.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\n\nimport { AUTHZ_ENGINES, AUTHZ_KINDS } from '@ankhorage/contracts';\nimport { type AuthFlowConfig, resolveAuthFlow } from '@ankhorage/contracts/auth';\n\nimport type {\n DoctorAnalysisRequest,\n DoctorAnalysisResult,\n DoctorFixPlan,\n DoctorPlannedChange,\n} from './analysis.js';\nimport type {\n DoctorDiagnostic,\n DoctorDiagnosticCode,\n DoctorPolicyProfile,\n DoctorRuleId,\n} from './diagnostics.js';\n\nconst MANIFEST_PROFILE = 'app-manifest' satisfies DoctorPolicyProfile;\nconst AUTH_ACTION_ROUTE_KEYS = [\n 'signInRoute',\n 'signUpRoute',\n 'signOutRoute',\n 'forgotPasswordRoute',\n 'otpRoute',\n] as const satisfies readonly (keyof AuthFlowConfig)[];\nconst OPTIONAL_ROUTE_KEYS = [\n 'signUpRoute',\n 'signOutRoute',\n 'forgotPasswordRoute',\n 'otpRoute',\n 'unauthorizedRoute',\n] as const satisfies readonly (keyof AuthFlowConfig)[];\n\ntype AuthActionRouteKey = (typeof AUTH_ACTION_ROUTE_KEYS)[number];\ntype OptionalRouteKey = (typeof OPTIONAL_ROUTE_KEYS)[number];\n\nexport async function analyzeAppManifestTarget(\n request: DoctorAnalysisRequest,\n): Promise<DoctorAnalysisResult | null> {\n const targetPath = path.resolve(request.cwd, request.inputPath ?? '.');\n const stats = await statOrNull(targetPath);\n\n if (\n stats === null ||\n !stats.isFile() ||\n path.extname(targetPath).toLowerCase() !== '.json' ||\n (request.mode !== 'validate' && request.mode !== 'fix')\n ) {\n return null;\n }\n\n const diagnostics = await analyzeAppManifestFile(targetPath);\n const plannedChanges: DoctorPlannedChange[] = [];\n const fixPlan: DoctorFixPlan | null =\n request.mode === 'fix'\n ? {\n changes: plannedChanges,\n diagnostics,\n profile: MANIFEST_PROFILE,\n targetPath,\n }\n : null;\n\n return {\n appliedChecks: ['manifest'],\n diagnostics,\n fixPlan,\n hasPackageJson: false,\n plannedChanges,\n profile: MANIFEST_PROFILE,\n repoMarkers: [],\n targetPath,\n };\n}\n\nexport async function analyzeAppManifestFile(filePath: string): Promise<DoctorDiagnostic[]> {\n let source: string;\n\n try {\n source = await fs.readFile(filePath, 'utf8');\n } catch (error) {\n return [\n createManifestDiagnostic({\n code: 'invalid-app-manifest-json',\n message: `Could not read app manifest JSON: ${toErrorMessage(error)}`,\n path: filePath,\n ruleId: 'manifest.json.readable',\n }),\n ];\n }\n\n let manifest: unknown;\n\n try {\n manifest = JSON.parse(source) as unknown;\n } catch (error) {\n return [\n createManifestDiagnostic({\n code: 'invalid-app-manifest-json',\n message: `App manifest must contain valid JSON: ${toErrorMessage(error)}`,\n path: filePath,\n ruleId: 'manifest.json.valid',\n }),\n ];\n }\n\n return analyzeAppManifest(manifest, filePath);\n}\n\nexport function analyzeAppManifest(\n manifest: unknown,\n manifestPath = 'manifest.json',\n): DoctorDiagnostic[] {\n if (!isRecord(manifest)) {\n return [\n createManifestDiagnostic({\n code: 'field-invalid',\n message: 'App manifest root must be a JSON object.',\n path: manifestPath,\n ruleId: 'manifest.root.valid-shape',\n }),\n ];\n }\n\n const diagnostics: DoctorDiagnostic[] = [];\n const { settings } = manifest;\n\n if (isRecord(settings) && hasOwn(settings, 'authFlow')) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message:\n 'settings.authFlow was removed. Move this configuration manually to infra.auth.flow; Doctor will not migrate it automatically.',\n path: manifestPath,\n ruleId: 'manifest.settings.auth-flow.removed',\n }),\n );\n }\n\n const { infra } = manifest;\n if (infra === undefined) {\n return diagnostics;\n }\n\n if (!isRecord(infra)) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: 'manifest.infra must be a JSON object when present.',\n path: manifestPath,\n ruleId: 'manifest.infra.valid-shape',\n }),\n );\n return diagnostics;\n }\n\n const { auth } = infra;\n if (auth === undefined) {\n return diagnostics;\n }\n\n if (!isRecord(auth)) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: 'manifest.infra.auth must be a JSON object when present.',\n path: manifestPath,\n ruleId: 'manifest.auth.valid-shape',\n }),\n );\n return diagnostics;\n }\n\n const flow = parseAuthFlow(auth.flow, manifestPath, diagnostics);\n if (flow !== null) {\n validateAuthFlowRelationships(flow, manifestPath, diagnostics);\n }\n\n validateAuthorization(auth.authorization, manifestPath, diagnostics);\n\n return diagnostics;\n}\n\nfunction parseAuthFlow(\n value: unknown,\n manifestPath: string,\n diagnostics: DoctorDiagnostic[],\n): AuthFlowConfig | null {\n if (value === undefined) {\n return resolveAuthFlow();\n }\n\n if (!isRecord(value)) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: 'manifest.infra.auth.flow must be a JSON object when present.',\n path: manifestPath,\n ruleId: 'manifest.auth.flow.valid-shape',\n }),\n );\n return null;\n }\n\n const signInRoute = readRequiredRoute(value, 'signInRoute', manifestPath, diagnostics);\n const postSignInRoute = readRequiredRoute(value, 'postSignInRoute', manifestPath, diagnostics);\n const optionalRoutes = readOptionalRoutes(value, manifestPath, diagnostics);\n\n if (signInRoute === null || postSignInRoute === null || optionalRoutes === null) {\n return null;\n }\n\n return resolveAuthFlow({\n signInRoute,\n postSignInRoute,\n ...optionalRoutes,\n });\n}\n\nfunction readRequiredRoute(\n flow: Record<string, unknown>,\n key: 'postSignInRoute' | 'signInRoute',\n manifestPath: string,\n diagnostics: DoctorDiagnostic[],\n): string | null {\n const value = flow[key];\n\n if (typeof value !== 'string' || value.trim() === '') {\n diagnostics.push(\n createManifestDiagnostic({\n code: value === undefined ? 'field-missing' : 'field-invalid',\n message: `manifest.infra.auth.flow.${key} must be a non-empty string.`,\n path: manifestPath,\n ruleId: 'manifest.auth.flow.route.required',\n }),\n );\n return null;\n }\n\n return value;\n}\n\nfunction readOptionalRoutes(\n flow: Record<string, unknown>,\n manifestPath: string,\n diagnostics: DoctorDiagnostic[],\n): Pick<AuthFlowConfig, OptionalRouteKey> | null {\n const routes: Partial<Pick<AuthFlowConfig, OptionalRouteKey>> = {};\n let valid = true;\n\n for (const key of OPTIONAL_ROUTE_KEYS) {\n const value = flow[key];\n if (value === undefined) {\n continue;\n }\n\n if (typeof value !== 'string' || value.trim() === '') {\n valid = false;\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: `manifest.infra.auth.flow.${key} must be a non-empty string when present.`,\n path: manifestPath,\n ruleId: 'manifest.auth.flow.route.valid',\n }),\n );\n continue;\n }\n\n routes[key] = value;\n }\n\n return valid ? routes : null;\n}\n\nfunction validateAuthFlowRelationships(\n flow: AuthFlowConfig,\n manifestPath: string,\n diagnostics: DoctorDiagnostic[],\n): void {\n const actionRoutes = AUTH_ACTION_ROUTE_KEYS.flatMap((key) => {\n const value = flow[key];\n return typeof value === 'string' ? [{ key, route: normalizeRoute(value) }] : [];\n });\n const routesByName = new Map<string, AuthActionRouteKey[]>();\n\n for (const entry of actionRoutes) {\n const keys = routesByName.get(entry.route) ?? [];\n keys.push(entry.key);\n routesByName.set(entry.route, keys);\n }\n\n for (const [route, keys] of routesByName) {\n if (keys.length < 2) {\n continue;\n }\n\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: `Auth action routes must be unique; ${keys.join(', ')} all resolve to \"${route}\".`,\n path: manifestPath,\n ruleId: 'manifest.auth.flow.route.unique',\n }),\n );\n }\n\n const postSignInRoute = normalizeRoute(flow.postSignInRoute);\n const collidingAction = actionRoutes.find((entry) => entry.route === postSignInRoute);\n if (collidingAction !== undefined) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: `postSignInRoute must resolve outside the auth action routes; it collides with ${collidingAction.key}.`,\n path: manifestPath,\n ruleId: 'manifest.auth.flow.post-sign-in.distinct',\n }),\n );\n }\n\n if (flow.unauthorizedRoute === undefined) {\n return;\n }\n\n const unauthorizedRoute = normalizeRoute(flow.unauthorizedRoute);\n const signInRoute = normalizeRoute(flow.signInRoute);\n\n if (unauthorizedRoute === postSignInRoute) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: 'unauthorizedRoute must not resolve to postSignInRoute.',\n path: manifestPath,\n ruleId: 'manifest.auth.flow.unauthorized-route.valid',\n }),\n );\n }\n\n if (unauthorizedRoute === signInRoute) {\n return;\n }\n\n const collidingActionRoute = actionRoutes.find(\n (entry) => entry.key !== 'signInRoute' && entry.route === unauthorizedRoute,\n );\n if (collidingActionRoute !== undefined) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: `unauthorizedRoute may alias signInRoute, but it must not collide with ${collidingActionRoute.key}.`,\n path: manifestPath,\n ruleId: 'manifest.auth.flow.unauthorized-route.valid',\n }),\n );\n }\n}\n\nfunction validateAuthorization(\n value: unknown,\n manifestPath: string,\n diagnostics: DoctorDiagnostic[],\n): void {\n if (value === undefined) {\n return;\n }\n\n if (!isRecord(value)) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message:\n 'manifest.infra.auth.authorization must be a JSON object when explicitly configured.',\n path: manifestPath,\n ruleId: 'manifest.auth.authorization.valid-shape',\n }),\n );\n return;\n }\n\n validateAuthorizationField({\n allowed: AUTHZ_KINDS,\n diagnostics,\n fieldName: 'kind',\n manifestPath,\n value: value.kind,\n ruleId: 'manifest.auth.authorization.kind.valid',\n });\n validateAuthorizationField({\n allowed: AUTHZ_ENGINES,\n diagnostics,\n fieldName: 'engine',\n manifestPath,\n value: value.engine,\n ruleId: 'manifest.auth.authorization.engine.valid',\n });\n}\n\nfunction validateAuthorizationField(request: {\n readonly allowed: readonly string[];\n readonly diagnostics: DoctorDiagnostic[];\n readonly fieldName: 'engine' | 'kind';\n readonly manifestPath: string;\n readonly ruleId: DoctorRuleId;\n readonly value: unknown;\n}): void {\n if (typeof request.value === 'string' && request.allowed.includes(request.value)) {\n return;\n }\n\n request.diagnostics.push(\n createManifestDiagnostic({\n code: request.value === undefined ? 'field-missing' : 'field-invalid',\n message: `Explicit authorization.${request.fieldName} must be one of: ${request.allowed.join(', ')}.`,\n path: request.manifestPath,\n ruleId: request.ruleId,\n }),\n );\n}\n\nfunction createManifestDiagnostic(request: {\n readonly code: DoctorDiagnosticCode;\n readonly message: string;\n readonly path: string;\n readonly ruleId: DoctorRuleId;\n}): DoctorDiagnostic {\n return {\n ...request,\n profile: MANIFEST_PROFILE,\n severity: 'error',\n };\n}\n\nfunction normalizeRoute(route: string): string {\n const normalized = route.trim().replace(/^\\/+/, '').replace(/\\/+$/, '');\n return normalized === '' ? 'index' : normalized;\n}\n\nfunction hasOwn(record: Record<string, unknown>, key: string): boolean {\n return Object.prototype.hasOwnProperty.call(record, key);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nasync function statOrNull(filePath: string) {\n try {\n return await fs.stat(filePath);\n } catch {\n return null;\n }\n}\n\nfunction toErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"]}
@@ -20,8 +20,13 @@ import { runCli } from './cli/standalone.js';
20
20
  *
21
21
  * - strict `public-package` validation for extracted public package repos
22
22
  * - light recognized `integration-monorepo` validation for `ankhorage4`
23
+ * - canonical app-manifest authentication validation for JSON file targets
23
24
  * - non-mutating `fix` plans for deterministic mechanical changes only
24
25
  *
26
+ * Manifest validation accepts `infra.auth.flow` as the only auth-flow location, rejects
27
+ * `settings.authFlow` with a manual-migration diagnostic, and permits authentication without
28
+ * an authorization block.
29
+ *
25
30
  * Still deferred:
26
31
  *
27
32
  * - GitHub checks
@@ -31,7 +36,7 @@ import { runCli } from './cli/standalone.js';
31
36
  *
32
37
  * Path handling:
33
38
  *
34
- * - pass `[path]`, or
39
+ * - pass a repo/package directory or app-manifest JSON file as `[path]`, or
35
40
  * - omit it to inspect the current working directory
36
41
  *
37
42
  * @usage
@@ -1 +1 @@
1
- {"version":3,"file":"readme-usage.js","sourceRoot":"","sources":["../src/readme-usage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;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 * - non-mutating `fix` plans for deterministic mechanical changes only\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 `[path]`, or\n * - omit it to inspect the current working directory\n *\n * @usage\n */\nawait runCli(['--help']);\n"]}
1
+ {"version":3,"file":"readme-usage.js","sourceRoot":"","sources":["../src/readme-usage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;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 * 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/doctor",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Executable Ankh provider and standalone CLI for lightweight repo and package compliance diagnostics.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -74,7 +74,7 @@
74
74
  "version-packages": "changeset version"
75
75
  },
76
76
  "dependencies": {
77
- "@ankhorage/contracts": "^1.19.0"
77
+ "@ankhorage/contracts": "^2.0.0"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@ankhorage/ankh": "^0.4.0",
package/src/analysis.ts CHANGED
@@ -4,7 +4,7 @@ import { pathToFileURL } from 'node:url';
4
4
 
5
5
  import type { DoctorDiagnostic, DoctorPolicyProfile, DoctorRuleId } from './diagnostics.js';
6
6
 
7
- export type DoctorTargetCheck = 'package' | 'repo';
7
+ export type DoctorTargetCheck = 'manifest' | 'package' | 'repo';
8
8
  export type DoctorTargetMode = 'fix' | 'package' | 'repo' | 'validate';
9
9
  export type DoctorPlannedChangeKind =
10
10
  'create-directory' | 'create-file' | 'update-json' | 'update-text';