@ankhorage/doctor 0.3.1 → 0.5.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 23a5302: Validate canonical `./cli` exports, reject `@ankh/*` workspace aliases and active `ankhorage4` source references, and enforce direct Studio ownership of runtime and drag-and-drop packages.
8
+
9
+ ## 0.4.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 1d1a8fd: Enforce the canonical `src/cli/index.ts` provider layout, reject root `src/cli.ts` files, and move the Doctor standalone executable under `src/cli/`.
14
+
3
15
  ## 0.3.1
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # DOCTOR
5
5
 
6
- ![license: MIT](././paradox/badges/license.svg) ![npm: v0.3.0](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)
6
+ ![license: MIT](././paradox/badges/license.svg) ![npm: v0.4.0](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)
7
7
 
8
8
  Executable doctor provider and standalone CLI for lightweight Ankhorage repo and package compliance diagnostics.
9
9
 
@@ -46,7 +46,7 @@ Path handling:
46
46
  Source: `src/readme-usage.ts`
47
47
 
48
48
  ```ts
49
- import { runCli } from './cli.js';
49
+ import { runCli } from './cli/standalone.js';
50
50
 
51
51
  await runCli(['--help']);
52
52
  ```
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAEtF,OAAO,EAGL,KAAK,qBAAqB,EAE1B,KAAK,oBAAoB,EAC1B,MAAM,gBAAgB,CAAC;AAQxB,MAAM,WAAW,kCAAkC;IACjD,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACpD;AAED,wBAAgB,2BAA2B,CACzC,OAAO,GAAE,kCAAuC,GAC/C,0BAA0B,CA4B5B;AAED,QAAA,MAAM,QAAQ,4BAAgC,CAAC;AAE/C,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAGtF,OAAO,EAGL,KAAK,qBAAqB,EAE1B,KAAK,oBAAoB,EAC1B,MAAM,gBAAgB,CAAC;AAQxB,MAAM,WAAW,kCAAkC;IACjD,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACpD;AAED,wBAAgB,2BAA2B,CACzC,OAAO,GAAE,kCAAuC,GAC/C,0BAA0B,CAgC5B;AAED,QAAA,MAAM,QAAQ,4BAAgC,CAAC;AAE/C,eAAe,QAAQ,CAAC"}
package/dist/cli/index.js CHANGED
@@ -1,7 +1,12 @@
1
+ import { analyzeDoctorTargetWithCliLayout } from '../cliLayoutAnalysis.js';
1
2
  import { createProviderCommandDescriptors, DOCTOR_COMMANDS, runDoctorCommand, } from '../commands.js';
2
3
  import { DOCTOR_CAPABILITIES, DOCTOR_COMMAND_CATEGORY, DOCTOR_PACKAGE_NAME, DOCTOR_PACKAGE_VERSION, } from '../packageMetadata.js';
3
4
  export function createDoctorRuntimeProvider(options = {}) {
4
5
  const runCommandImpl = options.runCommandImpl ?? runDoctorCommand;
6
+ const services = {
7
+ analyzeTarget: analyzeDoctorTargetWithCliLayout,
8
+ ...options.services,
9
+ };
5
10
  return {
6
11
  id: DOCTOR_PACKAGE_NAME,
7
12
  category: DOCTOR_COMMAND_CATEGORY,
@@ -14,7 +19,7 @@ export function createDoctorRuntimeProvider(options = {}) {
14
19
  command,
15
20
  context: request.context,
16
21
  }, {
17
- services: options.services,
22
+ services,
18
23
  });
19
24
  return {
20
25
  path: command.path,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,gCAAgC,EAChC,eAAe,EAEf,gBAAgB,GAEjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAO/B,MAAM,UAAU,2BAA2B,CACzC,UAA8C,EAAE;IAEhD,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,gBAAgB,CAAC;IAElE,OAAO;QACL,EAAE,EAAE,mBAAmB;QACvB,QAAQ,EAAE,uBAAuB;QACjC,OAAO,EAAE,sBAAsB;QAC/B,YAAY,EAAE,CAAC,GAAG,mBAAmB,CAAC;QACtC,QAAQ,EAAE,gCAAgC,EAAE;QAC5C,QAAQ,EAAE,eAAe,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;YACxC,MAAM,OAAO,GAAuB,CAAC,OAAO,EAAE,EAAE,CAC9C,cAAc,CACZ;gBACE,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,OAAO;gBACP,OAAO,EAAE,OAAO,CAAC,OAAO;aACzB,EACD;gBACE,QAAQ,EAAE,OAAO,CAAC,QAAQ;aAC3B,CACF,CAAC;YAEJ,OAAO;gBACL,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,OAAO;aACR,CAAC;QACJ,CAAC,CAAC;KACkC,CAAC;AACzC,CAAC;AAED,MAAM,QAAQ,GAAG,2BAA2B,EAAE,CAAC;AAE/C,eAAe,QAAQ,CAAC","sourcesContent":["import type { AnkhCommandHandler, AnkhRuntimeCommandProvider } from '@ankhorage/ankh';\n\nimport {\n createProviderCommandDescriptors,\n DOCTOR_COMMANDS,\n type DoctorCommandServices,\n runDoctorCommand,\n type RunDoctorCommandImpl,\n} from '../commands.js';\nimport {\n DOCTOR_CAPABILITIES,\n DOCTOR_COMMAND_CATEGORY,\n DOCTOR_PACKAGE_NAME,\n DOCTOR_PACKAGE_VERSION,\n} from '../packageMetadata.js';\n\nexport interface CreateDoctorRuntimeProviderOptions {\n readonly runCommandImpl?: RunDoctorCommandImpl;\n readonly services?: Partial<DoctorCommandServices>;\n}\n\nexport function createDoctorRuntimeProvider(\n options: CreateDoctorRuntimeProviderOptions = {},\n): AnkhRuntimeCommandProvider {\n const runCommandImpl = options.runCommandImpl ?? runDoctorCommand;\n\n return {\n id: DOCTOR_PACKAGE_NAME,\n category: DOCTOR_COMMAND_CATEGORY,\n version: DOCTOR_PACKAGE_VERSION,\n capabilities: [...DOCTOR_CAPABILITIES],\n commands: createProviderCommandDescriptors(),\n handlers: DOCTOR_COMMANDS.map((command) => {\n const handler: AnkhCommandHandler = (request) =>\n runCommandImpl(\n {\n argv: request.argv,\n command,\n context: request.context,\n },\n {\n services: options.services,\n },\n );\n\n return {\n path: command.path,\n handler,\n };\n }),\n } satisfies AnkhRuntimeCommandProvider;\n}\n\nconst provider = createDoctorRuntimeProvider();\n\nexport default provider;\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gCAAgC,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,EACL,gCAAgC,EAChC,eAAe,EAEf,gBAAgB,GAEjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAO/B,MAAM,UAAU,2BAA2B,CACzC,UAA8C,EAAE;IAEhD,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,gBAAgB,CAAC;IAClE,MAAM,QAAQ,GAAG;QACf,aAAa,EAAE,gCAAgC;QAC/C,GAAG,OAAO,CAAC,QAAQ;KACY,CAAC;IAElC,OAAO;QACL,EAAE,EAAE,mBAAmB;QACvB,QAAQ,EAAE,uBAAuB;QACjC,OAAO,EAAE,sBAAsB;QAC/B,YAAY,EAAE,CAAC,GAAG,mBAAmB,CAAC;QACtC,QAAQ,EAAE,gCAAgC,EAAE;QAC5C,QAAQ,EAAE,eAAe,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;YACxC,MAAM,OAAO,GAAuB,CAAC,OAAO,EAAE,EAAE,CAC9C,cAAc,CACZ;gBACE,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,OAAO;gBACP,OAAO,EAAE,OAAO,CAAC,OAAO;aACzB,EACD;gBACE,QAAQ;aACT,CACF,CAAC;YAEJ,OAAO;gBACL,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,OAAO;aACR,CAAC;QACJ,CAAC,CAAC;KACkC,CAAC;AACzC,CAAC;AAED,MAAM,QAAQ,GAAG,2BAA2B,EAAE,CAAC;AAE/C,eAAe,QAAQ,CAAC","sourcesContent":["import type { AnkhCommandHandler, AnkhRuntimeCommandProvider } from '@ankhorage/ankh';\n\nimport { analyzeDoctorTargetWithCliLayout } from '../cliLayoutAnalysis.js';\nimport {\n createProviderCommandDescriptors,\n DOCTOR_COMMANDS,\n type DoctorCommandServices,\n runDoctorCommand,\n type RunDoctorCommandImpl,\n} from '../commands.js';\nimport {\n DOCTOR_CAPABILITIES,\n DOCTOR_COMMAND_CATEGORY,\n DOCTOR_PACKAGE_NAME,\n DOCTOR_PACKAGE_VERSION,\n} from '../packageMetadata.js';\n\nexport interface CreateDoctorRuntimeProviderOptions {\n readonly runCommandImpl?: RunDoctorCommandImpl;\n readonly services?: Partial<DoctorCommandServices>;\n}\n\nexport function createDoctorRuntimeProvider(\n options: CreateDoctorRuntimeProviderOptions = {},\n): AnkhRuntimeCommandProvider {\n const runCommandImpl = options.runCommandImpl ?? runDoctorCommand;\n const services = {\n analyzeTarget: analyzeDoctorTargetWithCliLayout,\n ...options.services,\n } satisfies DoctorCommandServices;\n\n return {\n id: DOCTOR_PACKAGE_NAME,\n category: DOCTOR_COMMAND_CATEGORY,\n version: DOCTOR_PACKAGE_VERSION,\n capabilities: [...DOCTOR_CAPABILITIES],\n commands: createProviderCommandDescriptors(),\n handlers: DOCTOR_COMMANDS.map((command) => {\n const handler: AnkhCommandHandler = (request) =>\n runCommandImpl(\n {\n argv: request.argv,\n command,\n context: request.context,\n },\n {\n services,\n },\n );\n\n return {\n path: command.path,\n handler,\n };\n }),\n } satisfies AnkhRuntimeCommandProvider;\n}\n\nconst provider = createDoctorRuntimeProvider();\n\nexport default provider;\n"]}
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env bun
2
- import type { DoctorCommandContext, DoctorCommandRunResult } from './commandContext.js';
3
- import { type DoctorCommandServices, type RunDoctorCommandImpl } from './commands.js';
2
+ import type { DoctorCommandContext, DoctorCommandRunResult } from '../commandContext.js';
3
+ import { type DoctorCommandServices, type RunDoctorCommandImpl } from '../commands.js';
4
4
  export interface DoctorCliOptions {
5
5
  readonly context?: DoctorCommandContext;
6
6
  readonly runCommandImpl?: RunDoctorCommandImpl;
7
7
  readonly services?: Partial<DoctorCommandServices>;
8
8
  }
9
9
  export declare function runCli(argv: readonly string[], options?: DoctorCliOptions): Promise<DoctorCommandRunResult>;
10
- //# sourceMappingURL=cli.d.ts.map
10
+ //# sourceMappingURL=standalone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standalone.d.ts","sourceRoot":"","sources":["../../src/cli/standalone.ts"],"names":[],"mappings":";AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAEzF,OAAO,EACL,KAAK,qBAAqB,EAK1B,KAAK,oBAAoB,EAC1B,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,oBAAoB,CAAC;IACxC,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACpD;AAED,wBAAsB,MAAM,CAC1B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,sBAAsB,CAAC,CAkCjC"}
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
- import { createDefaultCommandContext } from './commandContext.js';
3
- import { findDoctorCommandByStandaloneName, renderRootHelp, renderUnknownCommand, runDoctorCommand, } from './commands.js';
2
+ import { analyzeDoctorTargetWithCliLayout } from '../cliLayoutAnalysis.js';
3
+ import { createDefaultCommandContext } from '../commandContext.js';
4
+ import { findDoctorCommandByStandaloneName, renderRootHelp, renderUnknownCommand, runDoctorCommand, } from '../commands.js';
4
5
  export async function runCli(argv, options = {}) {
5
6
  const context = options.context ?? createDefaultCommandContext();
6
7
  const runCommand = options.runCommandImpl ?? runDoctorCommand;
@@ -23,7 +24,10 @@ export async function runCli(argv, options = {}) {
23
24
  command,
24
25
  context,
25
26
  }, {
26
- services: options.services,
27
+ services: {
28
+ analyzeTarget: analyzeDoctorTargetWithCliLayout,
29
+ ...options.services,
30
+ },
27
31
  });
28
32
  }
29
33
  function isHelpToken(value) {
@@ -36,4 +40,4 @@ if (import.meta.main) {
36
40
  const result = await runCli(process.argv.slice(2));
37
41
  process.exit(result.exitCode);
38
42
  }
39
- //# sourceMappingURL=cli.js.map
43
+ //# sourceMappingURL=standalone.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standalone.js","sourceRoot":"","sources":["../../src/cli/standalone.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,gCAAgC,EAAE,MAAM,yBAAyB,CAAC;AAE3E,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAEL,iCAAiC,EACjC,cAAc,EACd,oBAAoB,EACpB,gBAAgB,GAEjB,MAAM,gBAAgB,CAAC;AAQxB,MAAM,CAAC,KAAK,UAAU,MAAM,CAC1B,IAAuB,EACvB,UAA4B,EAAE;IAE9B,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,2BAA2B,EAAE,CAAC;IACjE,MAAM,UAAU,GAAG,OAAO,CAAC,cAAc,IAAI,gBAAgB,CAAC;IAC9D,MAAM,CAAC,UAAU,EAAE,GAAG,UAAU,CAAC,GAAG,IAAI,CAAC;IAEzC,IAAI,UAAU,KAAK,SAAS,IAAI,WAAW,CAAC,UAAU,CAAC,EAAE,CAAC;QACxD,OAAO,CAAC,WAAW,CAAC,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;QACrD,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IACzB,CAAC;IAED,IAAI,cAAc,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC;QAC5C,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IACzB,CAAC;IAED,MAAM,OAAO,GAAG,iCAAiC,CAAC,UAAU,CAAC,CAAC;IAC9D,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrB,OAAO,CAAC,WAAW,CAAC,oBAAoB,CAAC,UAAU,CAAC,CAAC,CAAC;QACtD,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IACzB,CAAC;IAED,OAAO,UAAU,CACf;QACE,IAAI,EAAE,UAAU;QAChB,OAAO;QACP,OAAO;KACR,EACD;QACE,QAAQ,EAAE;YACR,aAAa,EAAE,gCAAgC;YAC/C,GAAG,OAAO,CAAC,QAAQ;SACpB;KACF,CACF,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,KAAa;IAChC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,MAAM,CAAC;AAClE,CAAC;AAED,SAAS,cAAc,CAAC,KAAa;IACnC,OAAO,KAAK,KAAK,WAAW,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,CAAC;AACxE,CAAC;AAED,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IACrB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACnD,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC","sourcesContent":["#!/usr/bin/env bun\n\nimport { analyzeDoctorTargetWithCliLayout } from '../cliLayoutAnalysis.js';\nimport type { DoctorCommandContext, DoctorCommandRunResult } from '../commandContext.js';\nimport { createDefaultCommandContext } from '../commandContext.js';\nimport {\n type DoctorCommandServices,\n findDoctorCommandByStandaloneName,\n renderRootHelp,\n renderUnknownCommand,\n runDoctorCommand,\n type RunDoctorCommandImpl,\n} from '../commands.js';\n\nexport interface DoctorCliOptions {\n readonly context?: DoctorCommandContext;\n readonly runCommandImpl?: RunDoctorCommandImpl;\n readonly services?: Partial<DoctorCommandServices>;\n}\n\nexport async function runCli(\n argv: readonly string[],\n options: DoctorCliOptions = {},\n): Promise<DoctorCommandRunResult> {\n const context = options.context ?? createDefaultCommandContext();\n const runCommand = options.runCommandImpl ?? runDoctorCommand;\n const [firstToken, ...restTokens] = argv;\n\n if (firstToken === undefined || isHelpToken(firstToken)) {\n context.writeStdout(renderRootHelp(context.version));\n return { exitCode: 0 };\n }\n\n if (isVersionToken(firstToken)) {\n context.writeStdout(`${context.version}\\n`);\n return { exitCode: 0 };\n }\n\n const command = findDoctorCommandByStandaloneName(firstToken);\n if (command === null) {\n context.writeStderr(renderUnknownCommand(firstToken));\n return { exitCode: 1 };\n }\n\n return runCommand(\n {\n argv: restTokens,\n command,\n context,\n },\n {\n services: {\n analyzeTarget: analyzeDoctorTargetWithCliLayout,\n ...options.services,\n },\n },\n );\n}\n\nfunction isHelpToken(value: string): boolean {\n return value === '--help' || value === '-h' || value === 'help';\n}\n\nfunction isVersionToken(value: string): boolean {\n return value === '--version' || value === '-v' || value === 'version';\n}\n\nif (import.meta.main) {\n const result = await runCli(process.argv.slice(2));\n process.exit(result.exitCode);\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import { type DoctorAnalysisRequest, type DoctorAnalysisResult } from './analysis.js';
2
+ export declare function analyzeDoctorTargetWithCliLayout(request: DoctorAnalysisRequest): Promise<DoctorAnalysisResult>;
3
+ //# sourceMappingURL=cliLayoutAnalysis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cliLayoutAnalysis.d.ts","sourceRoot":"","sources":["../src/cliLayoutAnalysis.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EAC1B,MAAM,eAAe,CAAC;AAsDvB,wBAAsB,gCAAgC,CACpD,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CA6B/B"}
@@ -0,0 +1,344 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { analyzeDoctorTarget, } from './analysis.js';
4
+ const LEGACY_ROOT_CLI_SOURCE = path.join('src', 'cli.ts');
5
+ const CANONICAL_CLI_INDEX_SOURCE = path.join('src', 'cli', 'index.ts');
6
+ const ACTIVE_SOURCE_ROOTS = ['src', 'app', 'apps', 'packages', 'scripts'];
7
+ const ACTIVE_SOURCE_EXTENSIONS = new Set([
8
+ '.cjs',
9
+ '.cts',
10
+ '.js',
11
+ '.jsx',
12
+ '.mjs',
13
+ '.mts',
14
+ '.ts',
15
+ '.tsx',
16
+ ]);
17
+ const IGNORED_SOURCE_DIRECTORIES = new Set([
18
+ '.expo',
19
+ '.git',
20
+ '.next',
21
+ '__fixtures__',
22
+ '__tests__',
23
+ 'build',
24
+ 'coverage',
25
+ 'dist',
26
+ 'docs',
27
+ 'fixtures',
28
+ 'node_modules',
29
+ 'paradox',
30
+ 'test',
31
+ 'tests',
32
+ ]);
33
+ const COMPATIBILITY_PACKAGE_PREFIX = '@ankh/';
34
+ const STUDIO_PACKAGE_NAME = '@ankhorage/studio';
35
+ const STUDIO_DND_PACKAGE_NAME = '@ankhorage/react-native-reanimated-dnd-web';
36
+ const STUDIO_RUNTIME_PACKAGE_NAME = '@ankhorage/runtime';
37
+ const DEPENDENCY_FIELD_NAMES = [
38
+ 'dependencies',
39
+ 'devDependencies',
40
+ 'optionalDependencies',
41
+ 'peerDependencies',
42
+ ];
43
+ export async function analyzeDoctorTargetWithCliLayout(request) {
44
+ const result = await analyzeDoctorTarget(request);
45
+ if (!result.hasPackageJson) {
46
+ return result;
47
+ }
48
+ const packageJsonPath = path.join(result.targetPath, 'package.json');
49
+ const packageJson = await readPackageJson(packageJsonPath);
50
+ if (packageJson === null) {
51
+ return result;
52
+ }
53
+ const architectureDiagnostics = await analyzeTargetArchitecture({
54
+ packageJson,
55
+ packageJsonPath,
56
+ profile: result.profile,
57
+ targetPath: result.targetPath,
58
+ });
59
+ if (architectureDiagnostics.length === 0) {
60
+ return result;
61
+ }
62
+ const diagnostics = [...result.diagnostics, ...architectureDiagnostics];
63
+ return {
64
+ ...result,
65
+ diagnostics,
66
+ fixPlan: result.fixPlan === null ? null : { ...result.fixPlan, diagnostics },
67
+ };
68
+ }
69
+ async function analyzeTargetArchitecture(request) {
70
+ const diagnostics = [];
71
+ const legacyRootCliPath = path.join(request.targetPath, LEGACY_ROOT_CLI_SOURCE);
72
+ const canonicalCliIndexPath = path.join(request.targetPath, CANONICAL_CLI_INDEX_SOURCE);
73
+ const exportsField = isRecord(request.packageJson.exports) ? request.packageJson.exports : null;
74
+ const cliCapable = hasAnkhProvider(request.packageJson) ||
75
+ exportsField?.['./cli'] !== undefined ||
76
+ (await pathExists(canonicalCliIndexPath));
77
+ if (await pathExists(legacyRootCliPath)) {
78
+ diagnostics.push({
79
+ code: 'field-invalid',
80
+ message: 'Package CLI code must live under src/cli/; root src/cli.ts is not allowed.',
81
+ path: legacyRootCliPath,
82
+ profile: request.profile,
83
+ ruleId: 'package.cli.root-file.disallowed',
84
+ severity: 'error',
85
+ });
86
+ }
87
+ if (cliCapable && !(await pathExists(canonicalCliIndexPath))) {
88
+ diagnostics.push({
89
+ code: 'missing-path',
90
+ message: 'CLI-capable packages must expose their provider from src/cli/index.ts.',
91
+ path: canonicalCliIndexPath,
92
+ profile: request.profile,
93
+ ruleId: 'package.cli.index.required',
94
+ severity: 'error',
95
+ });
96
+ }
97
+ if (cliCapable && exportsField?.['./cli'] === undefined) {
98
+ diagnostics.push({
99
+ code: 'field-missing',
100
+ message: 'CLI-capable packages must export "./cli" from package.json and point it at the owning src/cli/index.ts build output.',
101
+ path: request.packageJsonPath,
102
+ profile: request.profile,
103
+ ruleId: 'package.cli.export.required',
104
+ severity: 'error',
105
+ });
106
+ }
107
+ const dependencyEntries = collectDependencyEntries(request.packageJson);
108
+ diagnostics.push(...validateDependencyArchitecture({
109
+ dependencyEntries,
110
+ packageJsonPath: request.packageJsonPath,
111
+ profile: request.profile,
112
+ }));
113
+ const activeSourceImports = await collectActiveSourceImports(request.targetPath);
114
+ diagnostics.push(...validateActiveSourceImports({
115
+ activeSourceImports,
116
+ profile: request.profile,
117
+ }));
118
+ if (request.packageJson.name === STUDIO_PACKAGE_NAME) {
119
+ diagnostics.push(...validateStudioOwnership({
120
+ activeSourceImports,
121
+ dependencyEntries,
122
+ packageJsonPath: request.packageJsonPath,
123
+ profile: request.profile,
124
+ }));
125
+ }
126
+ return diagnostics;
127
+ }
128
+ function validateDependencyArchitecture(request) {
129
+ const diagnostics = [];
130
+ for (const dependency of request.dependencyEntries) {
131
+ if (dependency.packageName.startsWith(COMPATIBILITY_PACKAGE_PREFIX)) {
132
+ diagnostics.push({
133
+ code: 'field-invalid',
134
+ message: `Dependency "${dependency.packageName}" in package.json.${dependency.fieldName} is an old ankhorage4 workspace alias. Depend on "${toOwningPackageSpecifier(dependency.packageName)}" directly.`,
135
+ path: request.packageJsonPath,
136
+ profile: request.profile,
137
+ ruleId: 'package.dependencies.ankh-workspace-alias.disallowed',
138
+ severity: 'error',
139
+ });
140
+ }
141
+ if (dependency.packageName.includes('ankhorage4') ||
142
+ dependency.version.includes('ankhorage4')) {
143
+ diagnostics.push({
144
+ code: 'field-invalid',
145
+ message: `Dependency "${dependency.packageName}" references ankhorage4 as active source. Depend on the extracted owning @ankhorage/* package instead.`,
146
+ path: request.packageJsonPath,
147
+ profile: request.profile,
148
+ ruleId: 'package.dependencies.ankhorage4-source.disallowed',
149
+ severity: 'error',
150
+ });
151
+ }
152
+ }
153
+ return diagnostics;
154
+ }
155
+ function validateActiveSourceImports(request) {
156
+ const diagnostics = [];
157
+ for (const sourceImport of request.activeSourceImports) {
158
+ if (sourceImport.specifier.startsWith(COMPATIBILITY_PACKAGE_PREFIX)) {
159
+ diagnostics.push({
160
+ code: 'field-invalid',
161
+ message: `Import "${sourceImport.specifier}" is an old ankhorage4 compatibility boundary. Import "${toOwningPackageSpecifier(sourceImport.specifier)}" directly from the owning package.`,
162
+ path: sourceImport.filePath,
163
+ profile: request.profile,
164
+ ruleId: 'package.imports.ankh-workspace-alias.disallowed',
165
+ severity: 'error',
166
+ });
167
+ }
168
+ if (sourceImport.specifier.includes('ankhorage4')) {
169
+ diagnostics.push({
170
+ code: 'field-invalid',
171
+ message: `Import "${sourceImport.specifier}" references ankhorage4 as active source. Import the extracted owning @ankhorage/* package instead.`,
172
+ path: sourceImport.filePath,
173
+ profile: request.profile,
174
+ ruleId: 'package.imports.ankhorage4-source.disallowed',
175
+ severity: 'error',
176
+ });
177
+ }
178
+ }
179
+ return diagnostics;
180
+ }
181
+ function validateStudioOwnership(request) {
182
+ const diagnostics = [];
183
+ const directDependencies = new Set(request.dependencyEntries
184
+ .filter((dependency) => dependency.fieldName === 'dependencies')
185
+ .map((dependency) => dependency.packageName));
186
+ const importedSpecifiers = request.activeSourceImports.map((sourceImport) => sourceImport.specifier);
187
+ if (!directDependencies.has(STUDIO_DND_PACKAGE_NAME)) {
188
+ diagnostics.push({
189
+ code: 'missing-dependency',
190
+ message: `@ankhorage/studio must depend directly on "${STUDIO_DND_PACKAGE_NAME}" for Studio drag-and-drop ownership.`,
191
+ path: request.packageJsonPath,
192
+ profile: request.profile,
193
+ ruleId: 'studio.dependencies.dnd.required',
194
+ severity: 'error',
195
+ });
196
+ }
197
+ if (!directDependencies.has(STUDIO_RUNTIME_PACKAGE_NAME)) {
198
+ diagnostics.push({
199
+ code: 'missing-dependency',
200
+ message: `@ankhorage/studio must depend directly on "${STUDIO_RUNTIME_PACKAGE_NAME}" for runtime ownership.`,
201
+ path: request.packageJsonPath,
202
+ profile: request.profile,
203
+ ruleId: 'studio.dependencies.runtime.required',
204
+ severity: 'error',
205
+ });
206
+ }
207
+ if (!importedSpecifiers.some((specifier) => isPackageImport(specifier, STUDIO_DND_PACKAGE_NAME))) {
208
+ diagnostics.push({
209
+ code: 'field-missing',
210
+ message: `@ankhorage/studio must import "${STUDIO_DND_PACKAGE_NAME}" directly from its Studio DnD boundary.`,
211
+ path: request.packageJsonPath,
212
+ profile: request.profile,
213
+ ruleId: 'studio.imports.dnd.required',
214
+ severity: 'error',
215
+ });
216
+ }
217
+ if (!importedSpecifiers.some((specifier) => isPackageImport(specifier, STUDIO_RUNTIME_PACKAGE_NAME))) {
218
+ diagnostics.push({
219
+ code: 'field-missing',
220
+ message: `@ankhorage/studio must import "${STUDIO_RUNTIME_PACKAGE_NAME}" directly from the owning runtime package.`,
221
+ path: request.packageJsonPath,
222
+ profile: request.profile,
223
+ ruleId: 'studio.imports.runtime.required',
224
+ severity: 'error',
225
+ });
226
+ }
227
+ return diagnostics;
228
+ }
229
+ async function collectActiveSourceImports(targetPath) {
230
+ const imports = [];
231
+ for (const sourceRoot of ACTIVE_SOURCE_ROOTS) {
232
+ const sourceRootPath = path.join(targetPath, sourceRoot);
233
+ if (!(await pathExists(sourceRootPath))) {
234
+ continue;
235
+ }
236
+ for (const filePath of await listActiveSourceFiles(sourceRootPath)) {
237
+ const source = await fs.readFile(filePath, 'utf8');
238
+ for (const specifier of extractImportSpecifiers(source)) {
239
+ imports.push({ filePath, specifier });
240
+ }
241
+ }
242
+ }
243
+ return imports;
244
+ }
245
+ async function listActiveSourceFiles(rootPath) {
246
+ const files = [];
247
+ await visit(rootPath);
248
+ return files.sort((left, right) => left.localeCompare(right));
249
+ async function visit(currentPath) {
250
+ const entries = await fs.readdir(currentPath, { withFileTypes: true });
251
+ for (const entry of entries) {
252
+ if (entry.isDirectory()) {
253
+ if (!IGNORED_SOURCE_DIRECTORIES.has(entry.name)) {
254
+ await visit(path.join(currentPath, entry.name));
255
+ }
256
+ continue;
257
+ }
258
+ if (!entry.isFile()) {
259
+ continue;
260
+ }
261
+ const filePath = path.join(currentPath, entry.name);
262
+ if (isActiveSourceFile(filePath)) {
263
+ files.push(filePath);
264
+ }
265
+ }
266
+ }
267
+ }
268
+ function isActiveSourceFile(filePath) {
269
+ if (!ACTIVE_SOURCE_EXTENSIONS.has(path.extname(filePath))) {
270
+ return false;
271
+ }
272
+ return !/(?:^|\.)(?:spec|test)\.[cm]?[jt]sx?$/.test(path.basename(filePath));
273
+ }
274
+ function extractImportSpecifiers(source) {
275
+ const specifiers = new Set();
276
+ const patterns = [
277
+ /\b(?:import|export)\s+(?:type\s+)?(?:[^;]*?\sfrom\s*)?['"]([^'"]+)['"]/g,
278
+ /\b(?:import|require)\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
279
+ ];
280
+ for (const pattern of patterns) {
281
+ for (const match of source.matchAll(pattern)) {
282
+ const [, specifier] = match;
283
+ if (specifier !== undefined) {
284
+ specifiers.add(specifier);
285
+ }
286
+ }
287
+ }
288
+ return [...specifiers].sort((left, right) => left.localeCompare(right));
289
+ }
290
+ function collectDependencyEntries(packageJson) {
291
+ const entries = [];
292
+ for (const fieldName of DEPENDENCY_FIELD_NAMES) {
293
+ const dependencies = isRecord(packageJson[fieldName]) ? packageJson[fieldName] : null;
294
+ if (dependencies === null) {
295
+ continue;
296
+ }
297
+ for (const [packageName, version] of Object.entries(dependencies)) {
298
+ if (isNonEmptyString(version)) {
299
+ entries.push({ fieldName, packageName, version });
300
+ }
301
+ }
302
+ }
303
+ return entries.sort((left, right) => {
304
+ const fieldComparison = left.fieldName.localeCompare(right.fieldName);
305
+ return fieldComparison !== 0
306
+ ? fieldComparison
307
+ : left.packageName.localeCompare(right.packageName);
308
+ });
309
+ }
310
+ function hasAnkhProvider(packageJson) {
311
+ const ankh = isRecord(packageJson.ankh) ? packageJson.ankh : null;
312
+ return isNonEmptyString(ankh?.provider);
313
+ }
314
+ function isPackageImport(specifier, packageName) {
315
+ return specifier === packageName || specifier.startsWith(`${packageName}/`);
316
+ }
317
+ function toOwningPackageSpecifier(specifier) {
318
+ return `@ankhorage/${specifier.slice(COMPATIBILITY_PACKAGE_PREFIX.length)}`;
319
+ }
320
+ async function readPackageJson(packageJsonPath) {
321
+ try {
322
+ const parsed = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
323
+ return isRecord(parsed) ? parsed : null;
324
+ }
325
+ catch {
326
+ return null;
327
+ }
328
+ }
329
+ async function pathExists(targetPath) {
330
+ try {
331
+ await fs.access(targetPath);
332
+ return true;
333
+ }
334
+ catch {
335
+ return false;
336
+ }
337
+ }
338
+ function isNonEmptyString(value) {
339
+ return typeof value === 'string' && value.trim().length > 0;
340
+ }
341
+ function isRecord(value) {
342
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
343
+ }
344
+ //# sourceMappingURL=cliLayoutAnalysis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cliLayoutAnalysis.js","sourceRoot":"","sources":["../src/cliLayoutAnalysis.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EACL,mBAAmB,GAGpB,MAAM,eAAe,CAAC;AAGvB,MAAM,sBAAsB,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;AAC1D,MAAM,0BAA0B,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC;AACvE,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,CAAU,CAAC;AACnF,MAAM,wBAAwB,GAAG,IAAI,GAAG,CAAC;IACvC,MAAM;IACN,MAAM;IACN,KAAK;IACL,MAAM;IACN,MAAM;IACN,MAAM;IACN,KAAK;IACL,MAAM;CACP,CAAC,CAAC;AACH,MAAM,0BAA0B,GAAG,IAAI,GAAG,CAAC;IACzC,OAAO;IACP,MAAM;IACN,OAAO;IACP,cAAc;IACd,WAAW;IACX,OAAO;IACP,UAAU;IACV,MAAM;IACN,MAAM;IACN,UAAU;IACV,cAAc;IACd,SAAS;IACT,MAAM;IACN,OAAO;CACR,CAAC,CAAC;AACH,MAAM,4BAA4B,GAAG,QAAQ,CAAC;AAC9C,MAAM,mBAAmB,GAAG,mBAAmB,CAAC;AAChD,MAAM,uBAAuB,GAAG,4CAA4C,CAAC;AAC7E,MAAM,2BAA2B,GAAG,oBAAoB,CAAC;AACzD,MAAM,sBAAsB,GAAG;IAC7B,cAAc;IACd,iBAAiB;IACjB,sBAAsB;IACtB,kBAAkB;CACV,CAAC;AAaX,MAAM,CAAC,KAAK,UAAU,gCAAgC,CACpD,OAA8B;IAE9B,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAClD,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;QAC3B,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,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,WAAW,KAAK,IAAI,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,uBAAuB,GAAG,MAAM,yBAAyB,CAAC;QAC9D,WAAW;QACX,eAAe;QACf,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,UAAU,EAAE,MAAM,CAAC,UAAU;KAC9B,CAAC,CAAC;IACH,IAAI,uBAAuB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzC,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,WAAW,GAAG,CAAC,GAAG,MAAM,CAAC,WAAW,EAAE,GAAG,uBAAuB,CAAC,CAAC;IAExE,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,yBAAyB,CAAC,OAKxC;IACC,MAAM,WAAW,GAAuB,EAAE,CAAC;IAC3C,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,sBAAsB,CAAC,CAAC;IAChF,MAAM,qBAAqB,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,0BAA0B,CAAC,CAAC;IACxF,MAAM,YAAY,GAAG,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IAChG,MAAM,UAAU,GACd,eAAe,CAAC,OAAO,CAAC,WAAW,CAAC;QACpC,YAAY,EAAE,CAAC,OAAO,CAAC,KAAK,SAAS;QACrC,CAAC,MAAM,UAAU,CAAC,qBAAqB,CAAC,CAAC,CAAC;IAE5C,IAAI,MAAM,UAAU,CAAC,iBAAiB,CAAC,EAAE,CAAC;QACxC,WAAW,CAAC,IAAI,CAAC;YACf,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,4EAA4E;YACrF,IAAI,EAAE,iBAAiB;YACvB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,MAAM,EAAE,kCAAkC;YAC1C,QAAQ,EAAE,OAAO;SAClB,CAAC,CAAC;IACL,CAAC;IAED,IAAI,UAAU,IAAI,CAAC,CAAC,MAAM,UAAU,CAAC,qBAAqB,CAAC,CAAC,EAAE,CAAC;QAC7D,WAAW,CAAC,IAAI,CAAC;YACf,IAAI,EAAE,cAAc;YACpB,OAAO,EAAE,wEAAwE;YACjF,IAAI,EAAE,qBAAqB;YAC3B,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,MAAM,EAAE,4BAA4B;YACpC,QAAQ,EAAE,OAAO;SAClB,CAAC,CAAC;IACL,CAAC;IAED,IAAI,UAAU,IAAI,YAAY,EAAE,CAAC,OAAO,CAAC,KAAK,SAAS,EAAE,CAAC;QACxD,WAAW,CAAC,IAAI,CAAC;YACf,IAAI,EAAE,eAAe;YACrB,OAAO,EACL,sHAAsH;YACxH,IAAI,EAAE,OAAO,CAAC,eAAe;YAC7B,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,MAAM,EAAE,6BAA6B;YACrC,QAAQ,EAAE,OAAO;SAClB,CAAC,CAAC;IACL,CAAC;IAED,MAAM,iBAAiB,GAAG,wBAAwB,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACxE,WAAW,CAAC,IAAI,CACd,GAAG,8BAA8B,CAAC;QAChC,iBAAiB;QACjB,eAAe,EAAE,OAAO,CAAC,eAAe;QACxC,OAAO,EAAE,OAAO,CAAC,OAAO;KACzB,CAAC,CACH,CAAC;IAEF,MAAM,mBAAmB,GAAG,MAAM,0BAA0B,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACjF,WAAW,CAAC,IAAI,CACd,GAAG,2BAA2B,CAAC;QAC7B,mBAAmB;QACnB,OAAO,EAAE,OAAO,CAAC,OAAO;KACzB,CAAC,CACH,CAAC;IAEF,IAAI,OAAO,CAAC,WAAW,CAAC,IAAI,KAAK,mBAAmB,EAAE,CAAC;QACrD,WAAW,CAAC,IAAI,CACd,GAAG,uBAAuB,CAAC;YACzB,mBAAmB;YACnB,iBAAiB;YACjB,eAAe,EAAE,OAAO,CAAC,eAAe;YACxC,OAAO,EAAE,OAAO,CAAC,OAAO;SACzB,CAAC,CACH,CAAC;IACJ,CAAC;IAED,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,SAAS,8BAA8B,CAAC,OAIvC;IACC,MAAM,WAAW,GAAuB,EAAE,CAAC;IAE3C,KAAK,MAAM,UAAU,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;QACnD,IAAI,UAAU,CAAC,WAAW,CAAC,UAAU,CAAC,4BAA4B,CAAC,EAAE,CAAC;YACpE,WAAW,CAAC,IAAI,CAAC;gBACf,IAAI,EAAE,eAAe;gBACrB,OAAO,EAAE,eAAe,UAAU,CAAC,WAAW,qBAAqB,UAAU,CAAC,SAAS,qDAAqD,wBAAwB,CAAC,UAAU,CAAC,WAAW,CAAC,aAAa;gBACzM,IAAI,EAAE,OAAO,CAAC,eAAe;gBAC7B,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,MAAM,EAAE,sDAAsD;gBAC9D,QAAQ,EAAE,OAAO;aAClB,CAAC,CAAC;QACL,CAAC;QAED,IACE,UAAU,CAAC,WAAW,CAAC,QAAQ,CAAC,YAAY,CAAC;YAC7C,UAAU,CAAC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,EACzC,CAAC;YACD,WAAW,CAAC,IAAI,CAAC;gBACf,IAAI,EAAE,eAAe;gBACrB,OAAO,EAAE,eAAe,UAAU,CAAC,WAAW,wGAAwG;gBACtJ,IAAI,EAAE,OAAO,CAAC,eAAe;gBAC7B,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,MAAM,EAAE,mDAAmD;gBAC3D,QAAQ,EAAE,OAAO;aAClB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,SAAS,2BAA2B,CAAC,OAGpC;IACC,MAAM,WAAW,GAAuB,EAAE,CAAC;IAE3C,KAAK,MAAM,YAAY,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;QACvD,IAAI,YAAY,CAAC,SAAS,CAAC,UAAU,CAAC,4BAA4B,CAAC,EAAE,CAAC;YACpE,WAAW,CAAC,IAAI,CAAC;gBACf,IAAI,EAAE,eAAe;gBACrB,OAAO,EAAE,WAAW,YAAY,CAAC,SAAS,0DAA0D,wBAAwB,CAAC,YAAY,CAAC,SAAS,CAAC,qCAAqC;gBACzL,IAAI,EAAE,YAAY,CAAC,QAAQ;gBAC3B,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,MAAM,EAAE,iDAAiD;gBACzD,QAAQ,EAAE,OAAO;aAClB,CAAC,CAAC;QACL,CAAC;QAED,IAAI,YAAY,CAAC,SAAS,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;YAClD,WAAW,CAAC,IAAI,CAAC;gBACf,IAAI,EAAE,eAAe;gBACrB,OAAO,EAAE,WAAW,YAAY,CAAC,SAAS,qGAAqG;gBAC/I,IAAI,EAAE,YAAY,CAAC,QAAQ;gBAC3B,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,MAAM,EAAE,8CAA8C;gBACtD,QAAQ,EAAE,OAAO;aAClB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,SAAS,uBAAuB,CAAC,OAKhC;IACC,MAAM,WAAW,GAAuB,EAAE,CAAC;IAC3C,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAChC,OAAO,CAAC,iBAAiB;SACtB,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,SAAS,KAAK,cAAc,CAAC;SAC/D,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,CAC/C,CAAC;IACF,MAAM,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC,GAAG,CACxD,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,SAAS,CACzC,CAAC;IAEF,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,uBAAuB,CAAC,EAAE,CAAC;QACrD,WAAW,CAAC,IAAI,CAAC;YACf,IAAI,EAAE,oBAAoB;YAC1B,OAAO,EAAE,8CAA8C,uBAAuB,uCAAuC;YACrH,IAAI,EAAE,OAAO,CAAC,eAAe;YAC7B,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,MAAM,EAAE,kCAAkC;YAC1C,QAAQ,EAAE,OAAO;SAClB,CAAC,CAAC;IACL,CAAC;IAED,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,2BAA2B,CAAC,EAAE,CAAC;QACzD,WAAW,CAAC,IAAI,CAAC;YACf,IAAI,EAAE,oBAAoB;YAC1B,OAAO,EAAE,8CAA8C,2BAA2B,0BAA0B;YAC5G,IAAI,EAAE,OAAO,CAAC,eAAe;YAC7B,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,MAAM,EAAE,sCAAsC;YAC9C,QAAQ,EAAE,OAAO;SAClB,CAAC,CAAC;IACL,CAAC;IAED,IACE,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,eAAe,CAAC,SAAS,EAAE,uBAAuB,CAAC,CAAC,EAC5F,CAAC;QACD,WAAW,CAAC,IAAI,CAAC;YACf,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,kCAAkC,uBAAuB,0CAA0C;YAC5G,IAAI,EAAE,OAAO,CAAC,eAAe;YAC7B,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,MAAM,EAAE,6BAA6B;YACrC,QAAQ,EAAE,OAAO;SAClB,CAAC,CAAC;IACL,CAAC;IAED,IACE,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,eAAe,CAAC,SAAS,EAAE,2BAA2B,CAAC,CAAC,EAChG,CAAC;QACD,WAAW,CAAC,IAAI,CAAC;YACf,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,kCAAkC,2BAA2B,6CAA6C;YACnH,IAAI,EAAE,OAAO,CAAC,eAAe;YAC7B,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,MAAM,EAAE,iCAAiC;YACzC,QAAQ,EAAE,OAAO;SAClB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,KAAK,UAAU,0BAA0B,CAAC,UAAkB;IAC1D,MAAM,OAAO,GAAyB,EAAE,CAAC;IAEzC,KAAK,MAAM,UAAU,IAAI,mBAAmB,EAAE,CAAC;QAC7C,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QACzD,IAAI,CAAC,CAAC,MAAM,UAAU,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC;YACxC,SAAS;QACX,CAAC;QAED,KAAK,MAAM,QAAQ,IAAI,MAAM,qBAAqB,CAAC,cAAc,CAAC,EAAE,CAAC;YACnE,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YACnD,KAAK,MAAM,SAAS,IAAI,uBAAuB,CAAC,MAAM,CAAC,EAAE,CAAC;gBACxD,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC;YACxC,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,KAAK,UAAU,qBAAqB,CAAC,QAAgB;IACnD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC;IACtB,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;IAE9D,KAAK,UAAU,KAAK,CAAC,WAAmB;QACtC,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAEvE,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,IAAI,CAAC,0BAA0B,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;oBAChD,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;gBAClD,CAAC;gBACD,SAAS;YACX,CAAC;YAED,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;gBACpB,SAAS;YACX,CAAC;YAED,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YACpD,IAAI,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACjC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACvB,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB,CAAC,QAAgB;IAC1C,IAAI,CAAC,wBAAwB,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;QAC1D,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,CAAC,sCAAsC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;AAC/E,CAAC;AAED,SAAS,uBAAuB,CAAC,MAAc;IAC7C,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;IACrC,MAAM,QAAQ,GAAG;QACf,yEAAyE;QACzE,oDAAoD;KACrD,CAAC;IAEF,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC7C,MAAM,CAAC,EAAE,SAAS,CAAC,GAAG,KAAK,CAAC;YAC5B,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;gBAC5B,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;AAC1E,CAAC;AAED,SAAS,wBAAwB,CAAC,WAAoC;IACpE,MAAM,OAAO,GAAsB,EAAE,CAAC;IAEtC,KAAK,MAAM,SAAS,IAAI,sBAAsB,EAAE,CAAC;QAC/C,MAAM,YAAY,GAAG,QAAQ,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACtF,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;YAC1B,SAAS;QACX,CAAC;QAED,KAAK,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;YAClE,IAAI,gBAAgB,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC9B,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,CAAC;YACpD,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QAClC,MAAM,eAAe,GAAG,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACtE,OAAO,eAAe,KAAK,CAAC;YAC1B,CAAC,CAAC,eAAe;YACjB,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,eAAe,CAAC,WAAoC;IAC3D,MAAM,IAAI,GAAG,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IAClE,OAAO,gBAAgB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC1C,CAAC;AAED,SAAS,eAAe,CAAC,SAAiB,EAAE,WAAmB;IAC7D,OAAO,SAAS,KAAK,WAAW,IAAI,SAAS,CAAC,UAAU,CAAC,GAAG,WAAW,GAAG,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,wBAAwB,CAAC,SAAiB;IACjD,OAAO,cAAc,SAAS,CAAC,KAAK,CAAC,4BAA4B,CAAC,MAAM,CAAC,EAAE,CAAC;AAC9E,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,eAAuB;IACpD,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC;QAC/E,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,UAAU,CAAC,UAAkB;IAC1C,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAc;IACtC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AAC9D,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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\n\nimport {\n analyzeDoctorTarget,\n type DoctorAnalysisRequest,\n type DoctorAnalysisResult,\n} from './analysis.js';\nimport type { DoctorDiagnostic, DoctorPolicyProfile } from './diagnostics.js';\n\nconst LEGACY_ROOT_CLI_SOURCE = path.join('src', 'cli.ts');\nconst CANONICAL_CLI_INDEX_SOURCE = path.join('src', 'cli', 'index.ts');\nconst ACTIVE_SOURCE_ROOTS = ['src', 'app', 'apps', 'packages', 'scripts'] as const;\nconst ACTIVE_SOURCE_EXTENSIONS = new Set([\n '.cjs',\n '.cts',\n '.js',\n '.jsx',\n '.mjs',\n '.mts',\n '.ts',\n '.tsx',\n]);\nconst IGNORED_SOURCE_DIRECTORIES = new Set([\n '.expo',\n '.git',\n '.next',\n '__fixtures__',\n '__tests__',\n 'build',\n 'coverage',\n 'dist',\n 'docs',\n 'fixtures',\n 'node_modules',\n 'paradox',\n 'test',\n 'tests',\n]);\nconst COMPATIBILITY_PACKAGE_PREFIX = '@ankh/';\nconst STUDIO_PACKAGE_NAME = '@ankhorage/studio';\nconst STUDIO_DND_PACKAGE_NAME = '@ankhorage/react-native-reanimated-dnd-web';\nconst STUDIO_RUNTIME_PACKAGE_NAME = '@ankhorage/runtime';\nconst DEPENDENCY_FIELD_NAMES = [\n 'dependencies',\n 'devDependencies',\n 'optionalDependencies',\n 'peerDependencies',\n] as const;\n\ninterface ActiveSourceImport {\n readonly filePath: string;\n readonly specifier: string;\n}\n\ninterface DependencyEntry {\n readonly fieldName: (typeof DEPENDENCY_FIELD_NAMES)[number];\n readonly packageName: string;\n readonly version: string;\n}\n\nexport async function analyzeDoctorTargetWithCliLayout(\n request: DoctorAnalysisRequest,\n): Promise<DoctorAnalysisResult> {\n const result = await analyzeDoctorTarget(request);\n if (!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) {\n return result;\n }\n\n const architectureDiagnostics = await analyzeTargetArchitecture({\n packageJson,\n packageJsonPath,\n profile: result.profile,\n targetPath: result.targetPath,\n });\n if (architectureDiagnostics.length === 0) {\n return result;\n }\n\n const diagnostics = [...result.diagnostics, ...architectureDiagnostics];\n\n return {\n ...result,\n diagnostics,\n fixPlan: result.fixPlan === null ? null : { ...result.fixPlan, diagnostics },\n };\n}\n\nasync function analyzeTargetArchitecture(request: {\n readonly packageJson: Record<string, unknown>;\n readonly packageJsonPath: string;\n readonly profile: DoctorPolicyProfile;\n readonly targetPath: string;\n}): Promise<DoctorDiagnostic[]> {\n const diagnostics: DoctorDiagnostic[] = [];\n const legacyRootCliPath = path.join(request.targetPath, LEGACY_ROOT_CLI_SOURCE);\n const canonicalCliIndexPath = path.join(request.targetPath, CANONICAL_CLI_INDEX_SOURCE);\n const exportsField = isRecord(request.packageJson.exports) ? request.packageJson.exports : null;\n const cliCapable =\n hasAnkhProvider(request.packageJson) ||\n exportsField?.['./cli'] !== undefined ||\n (await pathExists(canonicalCliIndexPath));\n\n if (await pathExists(legacyRootCliPath)) {\n diagnostics.push({\n code: 'field-invalid',\n message: 'Package CLI code must live under src/cli/; root src/cli.ts is not allowed.',\n path: legacyRootCliPath,\n profile: request.profile,\n ruleId: 'package.cli.root-file.disallowed',\n severity: 'error',\n });\n }\n\n if (cliCapable && !(await pathExists(canonicalCliIndexPath))) {\n diagnostics.push({\n code: 'missing-path',\n message: 'CLI-capable packages must expose their provider from src/cli/index.ts.',\n path: canonicalCliIndexPath,\n profile: request.profile,\n ruleId: 'package.cli.index.required',\n severity: 'error',\n });\n }\n\n if (cliCapable && exportsField?.['./cli'] === undefined) {\n diagnostics.push({\n code: 'field-missing',\n message:\n 'CLI-capable packages must export \"./cli\" from package.json and point it at the owning src/cli/index.ts build output.',\n path: request.packageJsonPath,\n profile: request.profile,\n ruleId: 'package.cli.export.required',\n severity: 'error',\n });\n }\n\n const dependencyEntries = collectDependencyEntries(request.packageJson);\n diagnostics.push(\n ...validateDependencyArchitecture({\n dependencyEntries,\n packageJsonPath: request.packageJsonPath,\n profile: request.profile,\n }),\n );\n\n const activeSourceImports = await collectActiveSourceImports(request.targetPath);\n diagnostics.push(\n ...validateActiveSourceImports({\n activeSourceImports,\n profile: request.profile,\n }),\n );\n\n if (request.packageJson.name === STUDIO_PACKAGE_NAME) {\n diagnostics.push(\n ...validateStudioOwnership({\n activeSourceImports,\n dependencyEntries,\n packageJsonPath: request.packageJsonPath,\n profile: request.profile,\n }),\n );\n }\n\n return diagnostics;\n}\n\nfunction validateDependencyArchitecture(request: {\n readonly dependencyEntries: readonly DependencyEntry[];\n readonly packageJsonPath: string;\n readonly profile: DoctorPolicyProfile;\n}): DoctorDiagnostic[] {\n const diagnostics: DoctorDiagnostic[] = [];\n\n for (const dependency of request.dependencyEntries) {\n if (dependency.packageName.startsWith(COMPATIBILITY_PACKAGE_PREFIX)) {\n diagnostics.push({\n code: 'field-invalid',\n message: `Dependency \"${dependency.packageName}\" in package.json.${dependency.fieldName} is an old ankhorage4 workspace alias. Depend on \"${toOwningPackageSpecifier(dependency.packageName)}\" directly.`,\n path: request.packageJsonPath,\n profile: request.profile,\n ruleId: 'package.dependencies.ankh-workspace-alias.disallowed',\n severity: 'error',\n });\n }\n\n if (\n dependency.packageName.includes('ankhorage4') ||\n dependency.version.includes('ankhorage4')\n ) {\n diagnostics.push({\n code: 'field-invalid',\n message: `Dependency \"${dependency.packageName}\" references ankhorage4 as active source. Depend on the extracted owning @ankhorage/* package instead.`,\n path: request.packageJsonPath,\n profile: request.profile,\n ruleId: 'package.dependencies.ankhorage4-source.disallowed',\n severity: 'error',\n });\n }\n }\n\n return diagnostics;\n}\n\nfunction validateActiveSourceImports(request: {\n readonly activeSourceImports: readonly ActiveSourceImport[];\n readonly profile: DoctorPolicyProfile;\n}): DoctorDiagnostic[] {\n const diagnostics: DoctorDiagnostic[] = [];\n\n for (const sourceImport of request.activeSourceImports) {\n if (sourceImport.specifier.startsWith(COMPATIBILITY_PACKAGE_PREFIX)) {\n diagnostics.push({\n code: 'field-invalid',\n message: `Import \"${sourceImport.specifier}\" is an old ankhorage4 compatibility boundary. Import \"${toOwningPackageSpecifier(sourceImport.specifier)}\" directly from the owning package.`,\n path: sourceImport.filePath,\n profile: request.profile,\n ruleId: 'package.imports.ankh-workspace-alias.disallowed',\n severity: 'error',\n });\n }\n\n if (sourceImport.specifier.includes('ankhorage4')) {\n diagnostics.push({\n code: 'field-invalid',\n message: `Import \"${sourceImport.specifier}\" references ankhorage4 as active source. Import the extracted owning @ankhorage/* package instead.`,\n path: sourceImport.filePath,\n profile: request.profile,\n ruleId: 'package.imports.ankhorage4-source.disallowed',\n severity: 'error',\n });\n }\n }\n\n return diagnostics;\n}\n\nfunction validateStudioOwnership(request: {\n readonly activeSourceImports: readonly ActiveSourceImport[];\n readonly dependencyEntries: readonly DependencyEntry[];\n readonly packageJsonPath: string;\n readonly profile: DoctorPolicyProfile;\n}): DoctorDiagnostic[] {\n const diagnostics: DoctorDiagnostic[] = [];\n const directDependencies = new Set(\n request.dependencyEntries\n .filter((dependency) => dependency.fieldName === 'dependencies')\n .map((dependency) => dependency.packageName),\n );\n const importedSpecifiers = request.activeSourceImports.map(\n (sourceImport) => sourceImport.specifier,\n );\n\n if (!directDependencies.has(STUDIO_DND_PACKAGE_NAME)) {\n diagnostics.push({\n code: 'missing-dependency',\n message: `@ankhorage/studio must depend directly on \"${STUDIO_DND_PACKAGE_NAME}\" for Studio drag-and-drop ownership.`,\n path: request.packageJsonPath,\n profile: request.profile,\n ruleId: 'studio.dependencies.dnd.required',\n severity: 'error',\n });\n }\n\n if (!directDependencies.has(STUDIO_RUNTIME_PACKAGE_NAME)) {\n diagnostics.push({\n code: 'missing-dependency',\n message: `@ankhorage/studio must depend directly on \"${STUDIO_RUNTIME_PACKAGE_NAME}\" for runtime ownership.`,\n path: request.packageJsonPath,\n profile: request.profile,\n ruleId: 'studio.dependencies.runtime.required',\n severity: 'error',\n });\n }\n\n if (\n !importedSpecifiers.some((specifier) => isPackageImport(specifier, STUDIO_DND_PACKAGE_NAME))\n ) {\n diagnostics.push({\n code: 'field-missing',\n message: `@ankhorage/studio must import \"${STUDIO_DND_PACKAGE_NAME}\" directly from its Studio DnD boundary.`,\n path: request.packageJsonPath,\n profile: request.profile,\n ruleId: 'studio.imports.dnd.required',\n severity: 'error',\n });\n }\n\n if (\n !importedSpecifiers.some((specifier) => isPackageImport(specifier, STUDIO_RUNTIME_PACKAGE_NAME))\n ) {\n diagnostics.push({\n code: 'field-missing',\n message: `@ankhorage/studio must import \"${STUDIO_RUNTIME_PACKAGE_NAME}\" directly from the owning runtime package.`,\n path: request.packageJsonPath,\n profile: request.profile,\n ruleId: 'studio.imports.runtime.required',\n severity: 'error',\n });\n }\n\n return diagnostics;\n}\n\nasync function collectActiveSourceImports(targetPath: string): Promise<ActiveSourceImport[]> {\n const imports: ActiveSourceImport[] = [];\n\n for (const sourceRoot of ACTIVE_SOURCE_ROOTS) {\n const sourceRootPath = path.join(targetPath, sourceRoot);\n if (!(await pathExists(sourceRootPath))) {\n continue;\n }\n\n for (const filePath of await listActiveSourceFiles(sourceRootPath)) {\n const source = await fs.readFile(filePath, 'utf8');\n for (const specifier of extractImportSpecifiers(source)) {\n imports.push({ filePath, specifier });\n }\n }\n }\n\n return imports;\n}\n\nasync function listActiveSourceFiles(rootPath: string): Promise<string[]> {\n const files: string[] = [];\n await visit(rootPath);\n return files.sort((left, right) => left.localeCompare(right));\n\n async function visit(currentPath: string): Promise<void> {\n const entries = await fs.readdir(currentPath, { withFileTypes: true });\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (!IGNORED_SOURCE_DIRECTORIES.has(entry.name)) {\n await visit(path.join(currentPath, entry.name));\n }\n continue;\n }\n\n if (!entry.isFile()) {\n continue;\n }\n\n const filePath = path.join(currentPath, entry.name);\n if (isActiveSourceFile(filePath)) {\n files.push(filePath);\n }\n }\n }\n}\n\nfunction isActiveSourceFile(filePath: string): boolean {\n if (!ACTIVE_SOURCE_EXTENSIONS.has(path.extname(filePath))) {\n return false;\n }\n\n return !/(?:^|\\.)(?:spec|test)\\.[cm]?[jt]sx?$/.test(path.basename(filePath));\n}\n\nfunction extractImportSpecifiers(source: string): string[] {\n const specifiers = new Set<string>();\n const patterns = [\n /\\b(?:import|export)\\s+(?:type\\s+)?(?:[^;]*?\\sfrom\\s*)?['\"]([^'\"]+)['\"]/g,\n /\\b(?:import|require)\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g,\n ];\n\n for (const pattern of patterns) {\n for (const match of source.matchAll(pattern)) {\n const [, specifier] = match;\n if (specifier !== undefined) {\n specifiers.add(specifier);\n }\n }\n }\n\n return [...specifiers].sort((left, right) => left.localeCompare(right));\n}\n\nfunction collectDependencyEntries(packageJson: Record<string, unknown>): DependencyEntry[] {\n const entries: DependencyEntry[] = [];\n\n for (const fieldName of DEPENDENCY_FIELD_NAMES) {\n const dependencies = isRecord(packageJson[fieldName]) ? packageJson[fieldName] : null;\n if (dependencies === null) {\n continue;\n }\n\n for (const [packageName, version] of Object.entries(dependencies)) {\n if (isNonEmptyString(version)) {\n entries.push({ fieldName, packageName, version });\n }\n }\n }\n\n return entries.sort((left, right) => {\n const fieldComparison = left.fieldName.localeCompare(right.fieldName);\n return fieldComparison !== 0\n ? fieldComparison\n : left.packageName.localeCompare(right.packageName);\n });\n}\n\nfunction hasAnkhProvider(packageJson: Record<string, unknown>): boolean {\n const ankh = isRecord(packageJson.ankh) ? packageJson.ankh : null;\n return isNonEmptyString(ankh?.provider);\n}\n\nfunction isPackageImport(specifier: string, packageName: string): boolean {\n return specifier === packageName || specifier.startsWith(`${packageName}/`);\n}\n\nfunction toOwningPackageSpecifier(specifier: string): string {\n return `@ankhorage/${specifier.slice(COMPATIBILITY_PACKAGE_PREFIX.length)}`;\n}\n\nasync function readPackageJson(packageJsonPath: string): Promise<Record<string, unknown> | null> {\n try {\n const parsed: unknown = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));\n return isRecord(parsed) ? parsed : null;\n } catch {\n return null;\n }\n}\n\nasync function pathExists(targetPath: string): Promise<boolean> {\n try {\n await fs.access(targetPath);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.trim().length > 0;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  export type DoctorDiagnosticCode = 'field-invalid' | 'field-missing' | 'invalid-ankh-metadata' | 'invalid-package-json' | 'missing-dependency' | 'missing-path' | 'missing-script' | 'provider-source-import-failed' | 'provider-source-missing' | 'target-not-directory' | 'target-not-found' | 'unsupported-target';
2
2
  export type DoctorDiagnosticSeverity = 'error' | 'warning';
3
3
  export type DoctorPolicyProfile = 'integration-monorepo' | 'public-package' | 'unknown';
4
- export type DoctorRuleId = 'integration.package.ankh.disallowed' | 'integration.package.private.required' | 'integration.package.workspaces.required' | 'package.ankh.capabilities.match-provider' | 'package.ankh.present.valid-shape' | 'package.ankh.provider-path.required' | 'package.ankh.required-for-provider' | 'package.dependencies.changesets.required' | 'package.dependencies.devtools.required' | 'package.dependencies.paradox.required' | 'package.dependencies.types-bun.required' | 'package.dependencies.types-node.required' | 'package.dependencies.typescript.required' | 'package.json.bugs.required' | 'package.json.description.required' | 'package.json.exports.required' | 'package.json.files.required' | 'package.json.homepage.required' | 'package.json.keywords.required' | 'package.json.license.required' | 'package.json.name.required' | 'package.json.package-manager.bun' | 'package.json.package-manager.required' | 'package.json.private.public-package-disallowed' | 'package.json.publish-config.public' | 'package.json.publish-config.required' | 'package.json.repository.required' | 'package.json.type.module' | 'package.json.type.required' | 'package.json.version.required' | 'package.scripts.build.required' | 'package.scripts.changeset-status.required' | 'package.scripts.changeset.required' | 'package.scripts.docs.required' | 'package.scripts.format-check.required' | 'package.scripts.format.required' | 'package.scripts.knip.required' | 'package.scripts.lint-fix.required' | 'package.scripts.lint.required' | 'package.scripts.test.required' | 'package.scripts.typecheck.required' | 'package.scripts.version-packages.required' | 'provider.commands.match-capabilities' | 'provider.commands.match-handlers' | 'provider.source.importable' | 'provider.source.required' | 'repo.changelog.required' | 'repo.changeset.required' | 'repo.license.required' | 'repo.readme.required' | 'repo.workflows.required' | 'target.package-json.required' | 'target.path.directory' | 'target.path.exists' | 'target.profile.supported' | 'target.repo-markers.required';
4
+ export type DoctorRuleId = 'integration.package.ankh.disallowed' | 'integration.package.private.required' | 'integration.package.workspaces.required' | 'package.ankh.capabilities.match-provider' | 'package.ankh.present.valid-shape' | 'package.ankh.provider-path.required' | 'package.ankh.required-for-provider' | 'package.cli.export.required' | 'package.cli.index.required' | 'package.cli.root-file.disallowed' | 'package.dependencies.ankh-workspace-alias.disallowed' | 'package.dependencies.ankhorage4-source.disallowed' | 'package.dependencies.changesets.required' | 'package.dependencies.devtools.required' | 'package.dependencies.paradox.required' | 'package.dependencies.types-bun.required' | 'package.dependencies.types-node.required' | 'package.dependencies.typescript.required' | 'package.imports.ankh-workspace-alias.disallowed' | 'package.imports.ankhorage4-source.disallowed' | 'package.json.bugs.required' | 'package.json.description.required' | 'package.json.exports.required' | 'package.json.files.required' | 'package.json.homepage.required' | 'package.json.keywords.required' | 'package.json.license.required' | 'package.json.name.required' | 'package.json.package-manager.bun' | 'package.json.package-manager.required' | 'package.json.private.public-package-disallowed' | 'package.json.publish-config.public' | 'package.json.publish-config.required' | 'package.json.repository.required' | 'package.json.type.module' | 'package.json.type.required' | 'package.json.version.required' | 'package.scripts.build.required' | 'package.scripts.changeset-status.required' | 'package.scripts.changeset.required' | 'package.scripts.docs.required' | 'package.scripts.format-check.required' | 'package.scripts.format.required' | 'package.scripts.knip.required' | 'package.scripts.lint-fix.required' | 'package.scripts.lint.required' | 'package.scripts.test.required' | 'package.scripts.typecheck.required' | 'package.scripts.version-packages.required' | 'provider.commands.match-capabilities' | 'provider.commands.match-handlers' | 'provider.source.importable' | 'provider.source.required' | 'repo.changelog.required' | 'repo.changeset.required' | 'repo.license.required' | 'repo.readme.required' | 'repo.workflows.required' | 'studio.dependencies.dnd.required' | 'studio.dependencies.runtime.required' | 'studio.imports.dnd.required' | 'studio.imports.runtime.required' | 'target.package-json.required' | 'target.path.directory' | 'target.path.exists' | 'target.profile.supported' | 'target.repo-markers.required';
5
5
  export interface DoctorDiagnostic {
6
6
  readonly code: DoctorDiagnosticCode;
7
7
  readonly message: string;
@@ -1 +1 @@
1
- {"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAC5B,eAAe,GACf,eAAe,GACf,uBAAuB,GACvB,sBAAsB,GACtB,oBAAoB,GACpB,cAAc,GACd,gBAAgB,GAChB,+BAA+B,GAC/B,yBAAyB,GACzB,sBAAsB,GACtB,kBAAkB,GAClB,oBAAoB,CAAC;AAEzB,MAAM,MAAM,wBAAwB,GAAG,OAAO,GAAG,SAAS,CAAC;AAC3D,MAAM,MAAM,mBAAmB,GAAG,sBAAsB,GAAG,gBAAgB,GAAG,SAAS,CAAC;AACxF,MAAM,MAAM,YAAY,GACpB,qCAAqC,GACrC,sCAAsC,GACtC,yCAAyC,GACzC,0CAA0C,GAC1C,kCAAkC,GAClC,qCAAqC,GACrC,oCAAoC,GACpC,0CAA0C,GAC1C,wCAAwC,GACxC,uCAAuC,GACvC,yCAAyC,GACzC,0CAA0C,GAC1C,0CAA0C,GAC1C,4BAA4B,GAC5B,mCAAmC,GACnC,+BAA+B,GAC/B,6BAA6B,GAC7B,gCAAgC,GAChC,gCAAgC,GAChC,+BAA+B,GAC/B,4BAA4B,GAC5B,kCAAkC,GAClC,uCAAuC,GACvC,gDAAgD,GAChD,oCAAoC,GACpC,sCAAsC,GACtC,kCAAkC,GAClC,0BAA0B,GAC1B,4BAA4B,GAC5B,+BAA+B,GAC/B,gCAAgC,GAChC,2CAA2C,GAC3C,oCAAoC,GACpC,+BAA+B,GAC/B,uCAAuC,GACvC,iCAAiC,GACjC,+BAA+B,GAC/B,mCAAmC,GACnC,+BAA+B,GAC/B,+BAA+B,GAC/B,oCAAoC,GACpC,2CAA2C,GAC3C,sCAAsC,GACtC,kCAAkC,GAClC,4BAA4B,GAC5B,0BAA0B,GAC1B,yBAAyB,GACzB,yBAAyB,GACzB,uBAAuB,GACvB,sBAAsB,GACtB,yBAAyB,GACzB,8BAA8B,GAC9B,uBAAuB,GACvB,oBAAoB,GACpB,0BAA0B,GAC1B,8BAA8B,CAAC;AAEnC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,wBAAwB,CAAC;CAC7C;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAEtF;AAED,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAExF"}
1
+ {"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAC5B,eAAe,GACf,eAAe,GACf,uBAAuB,GACvB,sBAAsB,GACtB,oBAAoB,GACpB,cAAc,GACd,gBAAgB,GAChB,+BAA+B,GAC/B,yBAAyB,GACzB,sBAAsB,GACtB,kBAAkB,GAClB,oBAAoB,CAAC;AAEzB,MAAM,MAAM,wBAAwB,GAAG,OAAO,GAAG,SAAS,CAAC;AAC3D,MAAM,MAAM,mBAAmB,GAAG,sBAAsB,GAAG,gBAAgB,GAAG,SAAS,CAAC;AACxF,MAAM,MAAM,YAAY,GACpB,qCAAqC,GACrC,sCAAsC,GACtC,yCAAyC,GACzC,0CAA0C,GAC1C,kCAAkC,GAClC,qCAAqC,GACrC,oCAAoC,GACpC,6BAA6B,GAC7B,4BAA4B,GAC5B,kCAAkC,GAClC,sDAAsD,GACtD,mDAAmD,GACnD,0CAA0C,GAC1C,wCAAwC,GACxC,uCAAuC,GACvC,yCAAyC,GACzC,0CAA0C,GAC1C,0CAA0C,GAC1C,iDAAiD,GACjD,8CAA8C,GAC9C,4BAA4B,GAC5B,mCAAmC,GACnC,+BAA+B,GAC/B,6BAA6B,GAC7B,gCAAgC,GAChC,gCAAgC,GAChC,+BAA+B,GAC/B,4BAA4B,GAC5B,kCAAkC,GAClC,uCAAuC,GACvC,gDAAgD,GAChD,oCAAoC,GACpC,sCAAsC,GACtC,kCAAkC,GAClC,0BAA0B,GAC1B,4BAA4B,GAC5B,+BAA+B,GAC/B,gCAAgC,GAChC,2CAA2C,GAC3C,oCAAoC,GACpC,+BAA+B,GAC/B,uCAAuC,GACvC,iCAAiC,GACjC,+BAA+B,GAC/B,mCAAmC,GACnC,+BAA+B,GAC/B,+BAA+B,GAC/B,oCAAoC,GACpC,2CAA2C,GAC3C,sCAAsC,GACtC,kCAAkC,GAClC,4BAA4B,GAC5B,0BAA0B,GAC1B,yBAAyB,GACzB,yBAAyB,GACzB,uBAAuB,GACvB,sBAAsB,GACtB,yBAAyB,GACzB,kCAAkC,GAClC,sCAAsC,GACtC,6BAA6B,GAC7B,iCAAiC,GACjC,8BAA8B,GAC9B,uBAAuB,GACvB,oBAAoB,GACpB,0BAA0B,GAC1B,8BAA8B,CAAC;AAEnC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,wBAAwB,CAAC;CAC7C;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAEtF;AAED,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAExF"}
@@ -1 +1 @@
1
- {"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAmFA,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.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":"AA8FA,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.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
@@ -1,7 +1,8 @@
1
1
  export type { DoctorAnalysisRequest, DoctorAnalysisResult, DoctorFixPlan, DoctorPlannedChange, DoctorPlannedChangeKind, DoctorTargetCheck, DoctorTargetMode, } from './analysis.js';
2
2
  export { analyzeDoctorTarget } from './analysis.js';
3
- export { runCli } from './cli.js';
4
3
  export { createDoctorRuntimeProvider, type CreateDoctorRuntimeProviderOptions, } from './cli/index.js';
4
+ export { runCli } from './cli/standalone.js';
5
+ export { analyzeDoctorTargetWithCliLayout } from './cliLayoutAnalysis.js';
5
6
  export type { DoctorCommandContext, DoctorCommandRunResult } from './commandContext.js';
6
7
  export { createDefaultCommandContext } from './commandContext.js';
7
8
  export type { DoctorDiagnostic, DoctorDiagnosticCode, DoctorDiagnosticSeverity, DoctorPolicyProfile, DoctorRuleId, } from './diagnostics.js';
@@ -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,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EACL,2BAA2B,EAC3B,KAAK,kCAAkC,GACxC,MAAM,gBAAgB,CAAC;AACxB,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,mBAAmB,EACnB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { analyzeDoctorTarget } from './analysis.js';
2
- export { runCli } from './cli.js';
3
2
  export { createDoctorRuntimeProvider, } from './cli/index.js';
3
+ export { runCli } from './cli/standalone.js';
4
+ export { analyzeDoctorTargetWithCliLayout } from './cliLayoutAnalysis.js';
4
5
  export { createDefaultCommandContext } from './commandContext.js';
5
6
  export { DOCTOR_CAPABILITIES, DOCTOR_COMMAND_CATEGORY, DOCTOR_PACKAGE_METADATA, DOCTOR_PACKAGE_NAME, DOCTOR_PACKAGE_VERSION, } from './packageMetadata.js';
6
7
  //# 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,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EACL,2BAA2B,GAE5B,MAAM,gBAAgB,CAAC;AAExB,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 { runCli } from './cli.js';\nexport {\n createDoctorRuntimeProvider,\n type CreateDoctorRuntimeProviderOptions,\n} from './cli/index.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,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,4 +1,4 @@
1
- import { runCli } from './cli.js';
1
+ import { runCli } from './cli/standalone.js';
2
2
  /***
3
3
  * Provider and CLI surface
4
4
  *
@@ -1 +1 @@
1
- {"version":3,"file":"readme-usage.js","sourceRoot":"","sources":["../src/readme-usage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC","sourcesContent":["import { runCli } from './cli.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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/doctor",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Executable Ankh provider and standalone CLI for lightweight repo and package compliance diagnostics.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -47,7 +47,7 @@
47
47
  "LICENSE"
48
48
  ],
49
49
  "bin": {
50
- "ankhorage-doctor": "./dist/cli.js"
50
+ "ankhorage-doctor": "./dist/cli/standalone.js"
51
51
  },
52
52
  "ankh": {
53
53
  "category": "doctor",
package/src/cli/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { AnkhCommandHandler, AnkhRuntimeCommandProvider } from '@ankhorage/ankh';
2
2
 
3
+ import { analyzeDoctorTargetWithCliLayout } from '../cliLayoutAnalysis.js';
3
4
  import {
4
5
  createProviderCommandDescriptors,
5
6
  DOCTOR_COMMANDS,
@@ -23,6 +24,10 @@ export function createDoctorRuntimeProvider(
23
24
  options: CreateDoctorRuntimeProviderOptions = {},
24
25
  ): AnkhRuntimeCommandProvider {
25
26
  const runCommandImpl = options.runCommandImpl ?? runDoctorCommand;
27
+ const services = {
28
+ analyzeTarget: analyzeDoctorTargetWithCliLayout,
29
+ ...options.services,
30
+ } satisfies DoctorCommandServices;
26
31
 
27
32
  return {
28
33
  id: DOCTOR_PACKAGE_NAME,
@@ -39,7 +44,7 @@ export function createDoctorRuntimeProvider(
39
44
  context: request.context,
40
45
  },
41
46
  {
42
- services: options.services,
47
+ services,
43
48
  },
44
49
  );
45
50
 
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import type { DoctorCommandContext, DoctorCommandRunResult } from './commandContext.js';
4
- import { createDefaultCommandContext } from './commandContext.js';
3
+ import { analyzeDoctorTargetWithCliLayout } from '../cliLayoutAnalysis.js';
4
+ import type { DoctorCommandContext, DoctorCommandRunResult } from '../commandContext.js';
5
+ import { createDefaultCommandContext } from '../commandContext.js';
5
6
  import {
6
7
  type DoctorCommandServices,
7
8
  findDoctorCommandByStandaloneName,
@@ -9,7 +10,7 @@ import {
9
10
  renderUnknownCommand,
10
11
  runDoctorCommand,
11
12
  type RunDoctorCommandImpl,
12
- } from './commands.js';
13
+ } from '../commands.js';
13
14
 
14
15
  export interface DoctorCliOptions {
15
16
  readonly context?: DoctorCommandContext;
@@ -48,7 +49,10 @@ export async function runCli(
48
49
  context,
49
50
  },
50
51
  {
51
- services: options.services,
52
+ services: {
53
+ analyzeTarget: analyzeDoctorTargetWithCliLayout,
54
+ ...options.services,
55
+ },
52
56
  },
53
57
  );
54
58
  }
@@ -0,0 +1,448 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import {
5
+ analyzeDoctorTarget,
6
+ type DoctorAnalysisRequest,
7
+ type DoctorAnalysisResult,
8
+ } from './analysis.js';
9
+ import type { DoctorDiagnostic, DoctorPolicyProfile } from './diagnostics.js';
10
+
11
+ const LEGACY_ROOT_CLI_SOURCE = path.join('src', 'cli.ts');
12
+ const CANONICAL_CLI_INDEX_SOURCE = path.join('src', 'cli', 'index.ts');
13
+ const ACTIVE_SOURCE_ROOTS = ['src', 'app', 'apps', 'packages', 'scripts'] as const;
14
+ const ACTIVE_SOURCE_EXTENSIONS = new Set([
15
+ '.cjs',
16
+ '.cts',
17
+ '.js',
18
+ '.jsx',
19
+ '.mjs',
20
+ '.mts',
21
+ '.ts',
22
+ '.tsx',
23
+ ]);
24
+ const IGNORED_SOURCE_DIRECTORIES = new Set([
25
+ '.expo',
26
+ '.git',
27
+ '.next',
28
+ '__fixtures__',
29
+ '__tests__',
30
+ 'build',
31
+ 'coverage',
32
+ 'dist',
33
+ 'docs',
34
+ 'fixtures',
35
+ 'node_modules',
36
+ 'paradox',
37
+ 'test',
38
+ 'tests',
39
+ ]);
40
+ const COMPATIBILITY_PACKAGE_PREFIX = '@ankh/';
41
+ const STUDIO_PACKAGE_NAME = '@ankhorage/studio';
42
+ const STUDIO_DND_PACKAGE_NAME = '@ankhorage/react-native-reanimated-dnd-web';
43
+ const STUDIO_RUNTIME_PACKAGE_NAME = '@ankhorage/runtime';
44
+ const DEPENDENCY_FIELD_NAMES = [
45
+ 'dependencies',
46
+ 'devDependencies',
47
+ 'optionalDependencies',
48
+ 'peerDependencies',
49
+ ] as const;
50
+
51
+ interface ActiveSourceImport {
52
+ readonly filePath: string;
53
+ readonly specifier: string;
54
+ }
55
+
56
+ interface DependencyEntry {
57
+ readonly fieldName: (typeof DEPENDENCY_FIELD_NAMES)[number];
58
+ readonly packageName: string;
59
+ readonly version: string;
60
+ }
61
+
62
+ export async function analyzeDoctorTargetWithCliLayout(
63
+ request: DoctorAnalysisRequest,
64
+ ): Promise<DoctorAnalysisResult> {
65
+ const result = await analyzeDoctorTarget(request);
66
+ if (!result.hasPackageJson) {
67
+ return result;
68
+ }
69
+
70
+ const packageJsonPath = path.join(result.targetPath, 'package.json');
71
+ const packageJson = await readPackageJson(packageJsonPath);
72
+ if (packageJson === null) {
73
+ return result;
74
+ }
75
+
76
+ const architectureDiagnostics = await analyzeTargetArchitecture({
77
+ packageJson,
78
+ packageJsonPath,
79
+ profile: result.profile,
80
+ targetPath: result.targetPath,
81
+ });
82
+ if (architectureDiagnostics.length === 0) {
83
+ return result;
84
+ }
85
+
86
+ const diagnostics = [...result.diagnostics, ...architectureDiagnostics];
87
+
88
+ return {
89
+ ...result,
90
+ diagnostics,
91
+ fixPlan: result.fixPlan === null ? null : { ...result.fixPlan, diagnostics },
92
+ };
93
+ }
94
+
95
+ async function analyzeTargetArchitecture(request: {
96
+ readonly packageJson: Record<string, unknown>;
97
+ readonly packageJsonPath: string;
98
+ readonly profile: DoctorPolicyProfile;
99
+ readonly targetPath: string;
100
+ }): Promise<DoctorDiagnostic[]> {
101
+ const diagnostics: DoctorDiagnostic[] = [];
102
+ const legacyRootCliPath = path.join(request.targetPath, LEGACY_ROOT_CLI_SOURCE);
103
+ const canonicalCliIndexPath = path.join(request.targetPath, CANONICAL_CLI_INDEX_SOURCE);
104
+ const exportsField = isRecord(request.packageJson.exports) ? request.packageJson.exports : null;
105
+ const cliCapable =
106
+ hasAnkhProvider(request.packageJson) ||
107
+ exportsField?.['./cli'] !== undefined ||
108
+ (await pathExists(canonicalCliIndexPath));
109
+
110
+ if (await pathExists(legacyRootCliPath)) {
111
+ diagnostics.push({
112
+ code: 'field-invalid',
113
+ message: 'Package CLI code must live under src/cli/; root src/cli.ts is not allowed.',
114
+ path: legacyRootCliPath,
115
+ profile: request.profile,
116
+ ruleId: 'package.cli.root-file.disallowed',
117
+ severity: 'error',
118
+ });
119
+ }
120
+
121
+ if (cliCapable && !(await pathExists(canonicalCliIndexPath))) {
122
+ diagnostics.push({
123
+ code: 'missing-path',
124
+ message: 'CLI-capable packages must expose their provider from src/cli/index.ts.',
125
+ path: canonicalCliIndexPath,
126
+ profile: request.profile,
127
+ ruleId: 'package.cli.index.required',
128
+ severity: 'error',
129
+ });
130
+ }
131
+
132
+ if (cliCapable && exportsField?.['./cli'] === undefined) {
133
+ diagnostics.push({
134
+ code: 'field-missing',
135
+ message:
136
+ 'CLI-capable packages must export "./cli" from package.json and point it at the owning src/cli/index.ts build output.',
137
+ path: request.packageJsonPath,
138
+ profile: request.profile,
139
+ ruleId: 'package.cli.export.required',
140
+ severity: 'error',
141
+ });
142
+ }
143
+
144
+ const dependencyEntries = collectDependencyEntries(request.packageJson);
145
+ diagnostics.push(
146
+ ...validateDependencyArchitecture({
147
+ dependencyEntries,
148
+ packageJsonPath: request.packageJsonPath,
149
+ profile: request.profile,
150
+ }),
151
+ );
152
+
153
+ const activeSourceImports = await collectActiveSourceImports(request.targetPath);
154
+ diagnostics.push(
155
+ ...validateActiveSourceImports({
156
+ activeSourceImports,
157
+ profile: request.profile,
158
+ }),
159
+ );
160
+
161
+ if (request.packageJson.name === STUDIO_PACKAGE_NAME) {
162
+ diagnostics.push(
163
+ ...validateStudioOwnership({
164
+ activeSourceImports,
165
+ dependencyEntries,
166
+ packageJsonPath: request.packageJsonPath,
167
+ profile: request.profile,
168
+ }),
169
+ );
170
+ }
171
+
172
+ return diagnostics;
173
+ }
174
+
175
+ function validateDependencyArchitecture(request: {
176
+ readonly dependencyEntries: readonly DependencyEntry[];
177
+ readonly packageJsonPath: string;
178
+ readonly profile: DoctorPolicyProfile;
179
+ }): DoctorDiagnostic[] {
180
+ const diagnostics: DoctorDiagnostic[] = [];
181
+
182
+ for (const dependency of request.dependencyEntries) {
183
+ if (dependency.packageName.startsWith(COMPATIBILITY_PACKAGE_PREFIX)) {
184
+ diagnostics.push({
185
+ code: 'field-invalid',
186
+ message: `Dependency "${dependency.packageName}" in package.json.${dependency.fieldName} is an old ankhorage4 workspace alias. Depend on "${toOwningPackageSpecifier(dependency.packageName)}" directly.`,
187
+ path: request.packageJsonPath,
188
+ profile: request.profile,
189
+ ruleId: 'package.dependencies.ankh-workspace-alias.disallowed',
190
+ severity: 'error',
191
+ });
192
+ }
193
+
194
+ if (
195
+ dependency.packageName.includes('ankhorage4') ||
196
+ dependency.version.includes('ankhorage4')
197
+ ) {
198
+ diagnostics.push({
199
+ code: 'field-invalid',
200
+ message: `Dependency "${dependency.packageName}" references ankhorage4 as active source. Depend on the extracted owning @ankhorage/* package instead.`,
201
+ path: request.packageJsonPath,
202
+ profile: request.profile,
203
+ ruleId: 'package.dependencies.ankhorage4-source.disallowed',
204
+ severity: 'error',
205
+ });
206
+ }
207
+ }
208
+
209
+ return diagnostics;
210
+ }
211
+
212
+ function validateActiveSourceImports(request: {
213
+ readonly activeSourceImports: readonly ActiveSourceImport[];
214
+ readonly profile: DoctorPolicyProfile;
215
+ }): DoctorDiagnostic[] {
216
+ const diagnostics: DoctorDiagnostic[] = [];
217
+
218
+ for (const sourceImport of request.activeSourceImports) {
219
+ if (sourceImport.specifier.startsWith(COMPATIBILITY_PACKAGE_PREFIX)) {
220
+ diagnostics.push({
221
+ code: 'field-invalid',
222
+ message: `Import "${sourceImport.specifier}" is an old ankhorage4 compatibility boundary. Import "${toOwningPackageSpecifier(sourceImport.specifier)}" directly from the owning package.`,
223
+ path: sourceImport.filePath,
224
+ profile: request.profile,
225
+ ruleId: 'package.imports.ankh-workspace-alias.disallowed',
226
+ severity: 'error',
227
+ });
228
+ }
229
+
230
+ if (sourceImport.specifier.includes('ankhorage4')) {
231
+ diagnostics.push({
232
+ code: 'field-invalid',
233
+ message: `Import "${sourceImport.specifier}" references ankhorage4 as active source. Import the extracted owning @ankhorage/* package instead.`,
234
+ path: sourceImport.filePath,
235
+ profile: request.profile,
236
+ ruleId: 'package.imports.ankhorage4-source.disallowed',
237
+ severity: 'error',
238
+ });
239
+ }
240
+ }
241
+
242
+ return diagnostics;
243
+ }
244
+
245
+ function validateStudioOwnership(request: {
246
+ readonly activeSourceImports: readonly ActiveSourceImport[];
247
+ readonly dependencyEntries: readonly DependencyEntry[];
248
+ readonly packageJsonPath: string;
249
+ readonly profile: DoctorPolicyProfile;
250
+ }): DoctorDiagnostic[] {
251
+ const diagnostics: DoctorDiagnostic[] = [];
252
+ const directDependencies = new Set(
253
+ request.dependencyEntries
254
+ .filter((dependency) => dependency.fieldName === 'dependencies')
255
+ .map((dependency) => dependency.packageName),
256
+ );
257
+ const importedSpecifiers = request.activeSourceImports.map(
258
+ (sourceImport) => sourceImport.specifier,
259
+ );
260
+
261
+ if (!directDependencies.has(STUDIO_DND_PACKAGE_NAME)) {
262
+ diagnostics.push({
263
+ code: 'missing-dependency',
264
+ message: `@ankhorage/studio must depend directly on "${STUDIO_DND_PACKAGE_NAME}" for Studio drag-and-drop ownership.`,
265
+ path: request.packageJsonPath,
266
+ profile: request.profile,
267
+ ruleId: 'studio.dependencies.dnd.required',
268
+ severity: 'error',
269
+ });
270
+ }
271
+
272
+ if (!directDependencies.has(STUDIO_RUNTIME_PACKAGE_NAME)) {
273
+ diagnostics.push({
274
+ code: 'missing-dependency',
275
+ message: `@ankhorage/studio must depend directly on "${STUDIO_RUNTIME_PACKAGE_NAME}" for runtime ownership.`,
276
+ path: request.packageJsonPath,
277
+ profile: request.profile,
278
+ ruleId: 'studio.dependencies.runtime.required',
279
+ severity: 'error',
280
+ });
281
+ }
282
+
283
+ if (
284
+ !importedSpecifiers.some((specifier) => isPackageImport(specifier, STUDIO_DND_PACKAGE_NAME))
285
+ ) {
286
+ diagnostics.push({
287
+ code: 'field-missing',
288
+ message: `@ankhorage/studio must import "${STUDIO_DND_PACKAGE_NAME}" directly from its Studio DnD boundary.`,
289
+ path: request.packageJsonPath,
290
+ profile: request.profile,
291
+ ruleId: 'studio.imports.dnd.required',
292
+ severity: 'error',
293
+ });
294
+ }
295
+
296
+ if (
297
+ !importedSpecifiers.some((specifier) => isPackageImport(specifier, STUDIO_RUNTIME_PACKAGE_NAME))
298
+ ) {
299
+ diagnostics.push({
300
+ code: 'field-missing',
301
+ message: `@ankhorage/studio must import "${STUDIO_RUNTIME_PACKAGE_NAME}" directly from the owning runtime package.`,
302
+ path: request.packageJsonPath,
303
+ profile: request.profile,
304
+ ruleId: 'studio.imports.runtime.required',
305
+ severity: 'error',
306
+ });
307
+ }
308
+
309
+ return diagnostics;
310
+ }
311
+
312
+ async function collectActiveSourceImports(targetPath: string): Promise<ActiveSourceImport[]> {
313
+ const imports: ActiveSourceImport[] = [];
314
+
315
+ for (const sourceRoot of ACTIVE_SOURCE_ROOTS) {
316
+ const sourceRootPath = path.join(targetPath, sourceRoot);
317
+ if (!(await pathExists(sourceRootPath))) {
318
+ continue;
319
+ }
320
+
321
+ for (const filePath of await listActiveSourceFiles(sourceRootPath)) {
322
+ const source = await fs.readFile(filePath, 'utf8');
323
+ for (const specifier of extractImportSpecifiers(source)) {
324
+ imports.push({ filePath, specifier });
325
+ }
326
+ }
327
+ }
328
+
329
+ return imports;
330
+ }
331
+
332
+ async function listActiveSourceFiles(rootPath: string): Promise<string[]> {
333
+ const files: string[] = [];
334
+ await visit(rootPath);
335
+ return files.sort((left, right) => left.localeCompare(right));
336
+
337
+ async function visit(currentPath: string): Promise<void> {
338
+ const entries = await fs.readdir(currentPath, { withFileTypes: true });
339
+
340
+ for (const entry of entries) {
341
+ if (entry.isDirectory()) {
342
+ if (!IGNORED_SOURCE_DIRECTORIES.has(entry.name)) {
343
+ await visit(path.join(currentPath, entry.name));
344
+ }
345
+ continue;
346
+ }
347
+
348
+ if (!entry.isFile()) {
349
+ continue;
350
+ }
351
+
352
+ const filePath = path.join(currentPath, entry.name);
353
+ if (isActiveSourceFile(filePath)) {
354
+ files.push(filePath);
355
+ }
356
+ }
357
+ }
358
+ }
359
+
360
+ function isActiveSourceFile(filePath: string): boolean {
361
+ if (!ACTIVE_SOURCE_EXTENSIONS.has(path.extname(filePath))) {
362
+ return false;
363
+ }
364
+
365
+ return !/(?:^|\.)(?:spec|test)\.[cm]?[jt]sx?$/.test(path.basename(filePath));
366
+ }
367
+
368
+ function extractImportSpecifiers(source: string): string[] {
369
+ const specifiers = new Set<string>();
370
+ const patterns = [
371
+ /\b(?:import|export)\s+(?:type\s+)?(?:[^;]*?\sfrom\s*)?['"]([^'"]+)['"]/g,
372
+ /\b(?:import|require)\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
373
+ ];
374
+
375
+ for (const pattern of patterns) {
376
+ for (const match of source.matchAll(pattern)) {
377
+ const [, specifier] = match;
378
+ if (specifier !== undefined) {
379
+ specifiers.add(specifier);
380
+ }
381
+ }
382
+ }
383
+
384
+ return [...specifiers].sort((left, right) => left.localeCompare(right));
385
+ }
386
+
387
+ function collectDependencyEntries(packageJson: Record<string, unknown>): DependencyEntry[] {
388
+ const entries: DependencyEntry[] = [];
389
+
390
+ for (const fieldName of DEPENDENCY_FIELD_NAMES) {
391
+ const dependencies = isRecord(packageJson[fieldName]) ? packageJson[fieldName] : null;
392
+ if (dependencies === null) {
393
+ continue;
394
+ }
395
+
396
+ for (const [packageName, version] of Object.entries(dependencies)) {
397
+ if (isNonEmptyString(version)) {
398
+ entries.push({ fieldName, packageName, version });
399
+ }
400
+ }
401
+ }
402
+
403
+ return entries.sort((left, right) => {
404
+ const fieldComparison = left.fieldName.localeCompare(right.fieldName);
405
+ return fieldComparison !== 0
406
+ ? fieldComparison
407
+ : left.packageName.localeCompare(right.packageName);
408
+ });
409
+ }
410
+
411
+ function hasAnkhProvider(packageJson: Record<string, unknown>): boolean {
412
+ const ankh = isRecord(packageJson.ankh) ? packageJson.ankh : null;
413
+ return isNonEmptyString(ankh?.provider);
414
+ }
415
+
416
+ function isPackageImport(specifier: string, packageName: string): boolean {
417
+ return specifier === packageName || specifier.startsWith(`${packageName}/`);
418
+ }
419
+
420
+ function toOwningPackageSpecifier(specifier: string): string {
421
+ return `@ankhorage/${specifier.slice(COMPATIBILITY_PACKAGE_PREFIX.length)}`;
422
+ }
423
+
424
+ async function readPackageJson(packageJsonPath: string): Promise<Record<string, unknown> | null> {
425
+ try {
426
+ const parsed: unknown = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
427
+ return isRecord(parsed) ? parsed : null;
428
+ } catch {
429
+ return null;
430
+ }
431
+ }
432
+
433
+ async function pathExists(targetPath: string): Promise<boolean> {
434
+ try {
435
+ await fs.access(targetPath);
436
+ return true;
437
+ } catch {
438
+ return false;
439
+ }
440
+ }
441
+
442
+ function isNonEmptyString(value: unknown): value is string {
443
+ return typeof value === 'string' && value.trim().length > 0;
444
+ }
445
+
446
+ function isRecord(value: unknown): value is Record<string, unknown> {
447
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
448
+ }
@@ -22,12 +22,19 @@ export type DoctorRuleId =
22
22
  | 'package.ankh.present.valid-shape'
23
23
  | 'package.ankh.provider-path.required'
24
24
  | 'package.ankh.required-for-provider'
25
+ | 'package.cli.export.required'
26
+ | 'package.cli.index.required'
27
+ | 'package.cli.root-file.disallowed'
28
+ | 'package.dependencies.ankh-workspace-alias.disallowed'
29
+ | 'package.dependencies.ankhorage4-source.disallowed'
25
30
  | 'package.dependencies.changesets.required'
26
31
  | 'package.dependencies.devtools.required'
27
32
  | 'package.dependencies.paradox.required'
28
33
  | 'package.dependencies.types-bun.required'
29
34
  | 'package.dependencies.types-node.required'
30
35
  | 'package.dependencies.typescript.required'
36
+ | 'package.imports.ankh-workspace-alias.disallowed'
37
+ | 'package.imports.ankhorage4-source.disallowed'
31
38
  | 'package.json.bugs.required'
32
39
  | 'package.json.description.required'
33
40
  | 'package.json.exports.required'
@@ -66,6 +73,10 @@ export type DoctorRuleId =
66
73
  | 'repo.license.required'
67
74
  | 'repo.readme.required'
68
75
  | 'repo.workflows.required'
76
+ | 'studio.dependencies.dnd.required'
77
+ | 'studio.dependencies.runtime.required'
78
+ | 'studio.imports.dnd.required'
79
+ | 'studio.imports.runtime.required'
69
80
  | 'target.package-json.required'
70
81
  | 'target.path.directory'
71
82
  | 'target.path.exists'
package/src/index.ts CHANGED
@@ -8,11 +8,12 @@ export type {
8
8
  DoctorTargetMode,
9
9
  } from './analysis.js';
10
10
  export { analyzeDoctorTarget } from './analysis.js';
11
- export { runCli } from './cli.js';
12
11
  export {
13
12
  createDoctorRuntimeProvider,
14
13
  type CreateDoctorRuntimeProviderOptions,
15
14
  } from './cli/index.js';
15
+ export { runCli } from './cli/standalone.js';
16
+ export { analyzeDoctorTargetWithCliLayout } from './cliLayoutAnalysis.js';
16
17
  export type { DoctorCommandContext, DoctorCommandRunResult } from './commandContext.js';
17
18
  export { createDefaultCommandContext } from './commandContext.js';
18
19
  export type {
@@ -1,4 +1,4 @@
1
- import { runCli } from './cli.js';
1
+ import { runCli } from './cli/standalone.js';
2
2
 
3
3
  /***
4
4
  * Provider and CLI surface
package/dist/cli.d.ts.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAExF,OAAO,EACL,KAAK,qBAAqB,EAK1B,KAAK,oBAAoB,EAC1B,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,oBAAoB,CAAC;IACxC,QAAQ,CAAC,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAC/C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACpD;AAED,wBAAsB,MAAM,CAC1B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,sBAAsB,CAAC,CA+BjC"}
package/dist/cli.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAGA,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,EAEL,iCAAiC,EACjC,cAAc,EACd,oBAAoB,EACpB,gBAAgB,GAEjB,MAAM,eAAe,CAAC;AAQvB,MAAM,CAAC,KAAK,UAAU,MAAM,CAC1B,IAAuB,EACvB,UAA4B,EAAE;IAE9B,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,2BAA2B,EAAE,CAAC;IACjE,MAAM,UAAU,GAAG,OAAO,CAAC,cAAc,IAAI,gBAAgB,CAAC;IAC9D,MAAM,CAAC,UAAU,EAAE,GAAG,UAAU,CAAC,GAAG,IAAI,CAAC;IAEzC,IAAI,UAAU,KAAK,SAAS,IAAI,WAAW,CAAC,UAAU,CAAC,EAAE,CAAC;QACxD,OAAO,CAAC,WAAW,CAAC,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;QACrD,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IACzB,CAAC;IAED,IAAI,cAAc,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC;QAC5C,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IACzB,CAAC;IAED,MAAM,OAAO,GAAG,iCAAiC,CAAC,UAAU,CAAC,CAAC;IAC9D,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrB,OAAO,CAAC,WAAW,CAAC,oBAAoB,CAAC,UAAU,CAAC,CAAC,CAAC;QACtD,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IACzB,CAAC;IAED,OAAO,UAAU,CACf;QACE,IAAI,EAAE,UAAU;QAChB,OAAO;QACP,OAAO;KACR,EACD;QACE,QAAQ,EAAE,OAAO,CAAC,QAAQ;KAC3B,CACF,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,KAAa;IAChC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,MAAM,CAAC;AAClE,CAAC;AAED,SAAS,cAAc,CAAC,KAAa;IACnC,OAAO,KAAK,KAAK,WAAW,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,CAAC;AACxE,CAAC;AAED,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IACrB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACnD,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC","sourcesContent":["#!/usr/bin/env bun\n\nimport type { DoctorCommandContext, DoctorCommandRunResult } from './commandContext.js';\nimport { createDefaultCommandContext } from './commandContext.js';\nimport {\n type DoctorCommandServices,\n findDoctorCommandByStandaloneName,\n renderRootHelp,\n renderUnknownCommand,\n runDoctorCommand,\n type RunDoctorCommandImpl,\n} from './commands.js';\n\nexport interface DoctorCliOptions {\n readonly context?: DoctorCommandContext;\n readonly runCommandImpl?: RunDoctorCommandImpl;\n readonly services?: Partial<DoctorCommandServices>;\n}\n\nexport async function runCli(\n argv: readonly string[],\n options: DoctorCliOptions = {},\n): Promise<DoctorCommandRunResult> {\n const context = options.context ?? createDefaultCommandContext();\n const runCommand = options.runCommandImpl ?? runDoctorCommand;\n const [firstToken, ...restTokens] = argv;\n\n if (firstToken === undefined || isHelpToken(firstToken)) {\n context.writeStdout(renderRootHelp(context.version));\n return { exitCode: 0 };\n }\n\n if (isVersionToken(firstToken)) {\n context.writeStdout(`${context.version}\\n`);\n return { exitCode: 0 };\n }\n\n const command = findDoctorCommandByStandaloneName(firstToken);\n if (command === null) {\n context.writeStderr(renderUnknownCommand(firstToken));\n return { exitCode: 1 };\n }\n\n return runCommand(\n {\n argv: restTokens,\n command,\n context,\n },\n {\n services: options.services,\n },\n );\n}\n\nfunction isHelpToken(value: string): boolean {\n return value === '--help' || value === '-h' || value === 'help';\n}\n\nfunction isVersionToken(value: string): boolean {\n return value === '--version' || value === '-v' || value === 'version';\n}\n\nif (import.meta.main) {\n const result = await runCli(process.argv.slice(2));\n process.exit(result.exitCode);\n}\n"]}