phasegate 0.91.0 → 0.108.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +255 -0
  2. package/README.ja.md +17 -7
  3. package/README.md +26 -4
  4. package/docs/ADR/ADR-015-architecture-preset.md +183 -0
  5. package/docs/guide/cli-reference.md +108 -0
  6. package/docs/guide/codex-integration.md +7 -2
  7. package/docs/guide/installation.md +10 -2
  8. package/docs/guide/preset-selection.md +170 -0
  9. package/docs/guide/quick-vs-full-mode.md +3 -3
  10. package/docs/guide/retrofit-adoption.md +19 -2
  11. package/docs/guide/skills-overview.md +1 -1
  12. package/package.json +7 -1
  13. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +108 -100
  14. package/scripts/harness/agent-integration/domain/ports/phase-gate-query-port.ts +3 -3
  15. package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +26 -30
  16. package/scripts/harness/agent-integration/infrastructure/adapters/file-system-story-reflection-query-adapter.ts +6 -2
  17. package/scripts/harness/biome-ast-engine/application/dto/analyze-import-graph-input.ts +3 -0
  18. package/scripts/harness/biome-ast-engine/application/dto/resolve-enabled-rules-output.ts +2 -0
  19. package/scripts/harness/biome-ast-engine/application/mappers/resolve-enabled-rules-output-mapper.ts +4 -1
  20. package/scripts/harness/biome-ast-engine/application/usecases/analyze-import-graph-usecase.ts +4 -1
  21. package/scripts/harness/biome-ast-engine/application/usecases/execute-lint-usecase.ts +2 -0
  22. package/scripts/harness/biome-ast-engine/application/usecases/resolve-enabled-rules-usecase.ts +31 -3
  23. package/scripts/harness/biome-ast-engine/composition-root.ts +10 -2
  24. package/scripts/harness/biome-ast-engine/domain/ports/rule-config-provider-port.ts +16 -0
  25. package/scripts/harness/biome-ast-engine/domain/ports/source-module-analyzer-port.ts +5 -1
  26. package/scripts/harness/biome-ast-engine/domain/services/lint-runner.ts +5 -1
  27. package/scripts/harness/biome-ast-engine/domain/value-objects/architecture-spec.ts +38 -0
  28. package/scripts/harness/biome-ast-engine/domain/value-objects/layer-boundary.ts +7 -8
  29. package/scripts/harness/biome-ast-engine/domain/value-objects/layer-name.ts +15 -23
  30. package/scripts/harness/biome-ast-engine/domain/value-objects/source-module-snapshot.ts +11 -4
  31. package/scripts/harness/biome-ast-engine/infrastructure/adapters/harness-config-provider-adapter.ts +29 -3
  32. package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +22 -15
  33. package/scripts/harness/biome-ast-engine/infrastructure/mappers/source-module-snapshot-mapper.ts +26 -17
  34. package/scripts/harness/config-foundation/application/dto/resolved-config-output.ts +1 -0
  35. package/scripts/harness/config-foundation/application/usecases/load-resolved-config-use-case.ts +34 -2
  36. package/scripts/harness/config-foundation/application/usecases/migrate-schema-use-case.ts +89 -0
  37. package/scripts/harness/config-foundation/composition-root.ts +8 -0
  38. package/scripts/harness/config-foundation/domain/harness-config.ts +6 -0
  39. package/scripts/harness/config-foundation/domain/services/architecture-resolution-service.ts +257 -0
  40. package/scripts/harness/config-foundation/domain/value-objects/architecture-config.ts +66 -0
  41. package/scripts/harness/config-foundation/domain/value-objects/architecture-preset-catalog.ts +75 -0
  42. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +546 -0
  43. package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts +18 -6
  44. package/scripts/harness/config-foundation/presentation/cli/migrate-schema-command-handler.ts +83 -0
  45. package/scripts/harness/integrations/pre-commit.ts +211 -52
  46. package/scripts/harness/main.ts +460 -340
  47. package/scripts/harness/phase-dependency-model/domain/ports/story-reflection-file-system-port.ts +2 -4
  48. package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +54 -17
  49. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +154 -36
  50. package/scripts/harness/setup/skill-deployer.ts +140 -102
  51. package/scripts/harness/skill-quality/domain/errors/skill-quality-error.ts +24 -23
  52. package/scripts/harness/skill-quality/domain/value-objects/commit-message.ts +23 -7
  53. package/scripts/harness/traceability-model/application/usecases/apply-work-item-migration-usecase.ts +52 -0
  54. package/scripts/harness/traceability-model/application/usecases/plan-work-item-migration-usecase.ts +29 -0
  55. package/scripts/harness/traceability-model/application/usecases/validate-design-story-annotations-usecase.ts +83 -18
  56. package/scripts/harness/traceability-model/composition-root.ts +48 -30
  57. package/scripts/harness/traceability-model/domain/ports/design-document-port.ts +9 -15
  58. package/scripts/harness/traceability-model/domain/ports/work-item-migration-apply-port.ts +11 -0
  59. package/scripts/harness/traceability-model/domain/ports/work-item-migration-source-port.ts +9 -0
  60. package/scripts/harness/traceability-model/domain/services/work-item-migration-planner.ts +162 -0
  61. package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +57 -0
  62. package/scripts/harness/traceability-model/domain/value-objects/work-item-migration-candidate.ts +47 -0
  63. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-migration-apply-gateway.ts +110 -0
  64. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-migration-source-gateway.ts +182 -0
  65. package/scripts/harness/traceability-model/infrastructure/gateways/markdown-design-document-gateway.ts +29 -43
  66. package/scripts/harness/traceability-model/infrastructure/parsers/work-item-frontmatter-parser.ts +136 -0
  67. package/scripts/harness/traceability-model/presentation/cli/migrate-work-items-command-handler.ts +186 -0
  68. package/skills/quick-implementor/SKILL.md +17 -1
  69. package/templates/.husky/commit-msg +1 -0
@@ -45,6 +45,7 @@ export class ExecuteLintUseCase {
45
45
  const resolvedRules = await this.resolveEnabledRulesUseCase.execute();
46
46
  const analyzed = await this.analyzeImportGraphUseCase.execute({
47
47
  targets: input.targets,
48
+ architecture: resolvedRules.architectureSpec,
48
49
  });
49
50
 
50
51
  if (input.includeBiomeNative !== false) {
@@ -58,6 +59,7 @@ export class ExecuteLintUseCase {
58
59
  snapshots: analyzed.snapshots,
59
60
  importGraph: analyzed.importGraph,
60
61
  durationMs,
62
+ architecture: resolvedRules.architectureSpec,
61
63
  });
62
64
  const report = LintReport.create({
63
65
  violations: baseReport.violations,
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { RuleConfigProviderPort } from '../../domain/ports/rule-config-provider-port.js';
7
7
  import type { RuleDefinitionRegistry } from '../../domain/services/rule-definition-registry.js';
8
+ import { freezeArchitectureSpec } from '../../domain/value-objects/architecture-spec.js';
8
9
  import type { ResolveEnabledRulesInput } from '../dto/resolve-enabled-rules-input.js';
9
10
  import type { ResolveEnabledRulesOutput } from '../dto/resolve-enabled-rules-output.js';
10
11
  import { toResolveEnabledRulesOutput } from '../mappers/resolve-enabled-rules-output-mapper.js';
@@ -16,6 +17,13 @@ export interface ResolveEnabledRulesUseCaseDeps {
16
17
  readonly ruleDefinitionRegistry: RuleDefinitionResolver;
17
18
  }
18
19
 
20
+ const FLAT_AUTO_DISABLED_RULES = Object.freeze([
21
+ 'require-unit-comment',
22
+ 'require-layer-comment',
23
+ 'no-layer-violation',
24
+ 'enforce-folder-structure',
25
+ ] as const);
26
+
19
27
  export class ResolveEnabledRulesUseCase {
20
28
  private readonly ruleConfigProviderPort: RuleConfigProviderPort;
21
29
  private readonly ruleDefinitionRegistry: RuleDefinitionResolver;
@@ -29,15 +37,35 @@ export class ResolveEnabledRulesUseCase {
29
37
  input: ResolveEnabledRulesInput = {}
30
38
  ): Promise<Readonly<ResolveEnabledRulesOutput>> {
31
39
  const config = await this.ruleConfigProviderPort.getL1Config();
32
- const mergedRules = {
40
+ const architecture = await this.ruleConfigProviderPort.getArchitecture();
41
+ const overrideRules = input.overrideRules ?? {};
42
+ const mergedRules: Record<string, 'error' | 'warning' | 'off'> = {
33
43
  ...config.rules,
34
- ...(input.overrideRules ?? {}),
44
+ ...overrideRules,
35
45
  };
46
+
47
+ if (architecture.preset === 'flat') {
48
+ for (const ruleName of FLAT_AUTO_DISABLED_RULES) {
49
+ if (!(ruleName in mergedRules)) {
50
+ mergedRules[ruleName] = 'off';
51
+ }
52
+ }
53
+ }
54
+
36
55
  const resolved = this.ruleDefinitionRegistry.resolveEnabled({
37
56
  l1Enabled: config.enabled,
38
57
  rules: mergedRules,
39
58
  });
40
59
 
41
- return toResolveEnabledRulesOutput(resolved.enabledRules, resolved.skippedRules);
60
+ const architectureSpec = freezeArchitectureSpec({
61
+ layers: architecture.layers,
62
+ allowedDependencies: architecture.allowedDependencies,
63
+ });
64
+
65
+ return toResolveEnabledRulesOutput(
66
+ resolved.enabledRules,
67
+ resolved.skippedRules,
68
+ architectureSpec
69
+ );
42
70
  }
43
71
  }
@@ -2,7 +2,11 @@
2
2
  // @layer composition
3
3
 
4
4
  import { SystemClockAdapter } from './infrastructure/adapters/system-clock-adapter.js';
5
- import { HarnessConfigProviderAdapter, type L1ConfigInput } from './infrastructure/adapters/harness-config-provider-adapter.js';
5
+ import {
6
+ HarnessConfigProviderAdapter,
7
+ type ArchitectureConfigInput,
8
+ type L1ConfigInput,
9
+ } from './infrastructure/adapters/harness-config-provider-adapter.js';
6
10
  import { HarnessErrorFormatterAdapter } from './infrastructure/adapters/harness-error-formatter-adapter.js';
7
11
  import { NodeWorkspaceFileAdapter } from './infrastructure/adapters/node-workspace-file-adapter.js';
8
12
  import { TypeScriptSourceModuleAnalyzerAdapter } from './infrastructure/adapters/typescript-source-module-analyzer-adapter.js';
@@ -20,6 +24,7 @@ import { HarnessLintCommandHandler } from './presentation/cli/harness-lint-comma
20
24
 
21
25
  export interface BiomeAstEngineModuleOptions {
22
26
  readonly l1Config?: L1ConfigInput;
27
+ readonly architecture?: ArchitectureConfigInput;
23
28
  }
24
29
 
25
30
  export function createBiomeAstEngineModule(
@@ -27,7 +32,10 @@ export function createBiomeAstEngineModule(
27
32
  options?: BiomeAstEngineModuleOptions,
28
33
  ) {
29
34
  const clockPort = new SystemClockAdapter();
30
- const ruleConfigProviderPort = new HarnessConfigProviderAdapter(options?.l1Config);
35
+ const ruleConfigProviderPort = new HarnessConfigProviderAdapter(
36
+ options?.l1Config,
37
+ options?.architecture,
38
+ );
31
39
  const violationFormatterPort = new HarnessErrorFormatterAdapter();
32
40
  const workspaceFilePort = new NodeWorkspaceFileAdapter({ rootDir });
33
41
  const sourceModuleAnalyzerPort = new TypeScriptSourceModuleAnalyzerAdapter({ rootDir });
@@ -3,9 +3,25 @@
3
3
  * @unit biome-ast-engine
4
4
  */
5
5
 
6
+ export type ArchitecturePresetIdValue =
7
+ | 'clean'
8
+ | 'strict-ddd'
9
+ | 'onion'
10
+ | 'hexagonal'
11
+ | 'layered'
12
+ | 'flat'
13
+ | 'custom';
14
+
15
+ export interface ArchitectureProviderInfo {
16
+ readonly preset: ArchitecturePresetIdValue;
17
+ readonly layers: readonly string[];
18
+ readonly allowedDependencies: Readonly<Record<string, readonly string[]>>;
19
+ }
20
+
6
21
  export interface RuleConfigProviderPort {
7
22
  getL1Config(): Promise<{
8
23
  enabled: boolean;
9
24
  rules: Record<string, 'error' | 'warning' | 'off'>;
10
25
  }>;
26
+ getArchitecture(): Promise<ArchitectureProviderInfo>;
11
27
  }
@@ -3,9 +3,13 @@
3
3
  * @unit biome-ast-engine
4
4
  */
5
5
 
6
+ import type { ArchitectureSpec } from '../value-objects/architecture-spec.js';
6
7
  import { FilePath } from '../value-objects/file-path.js';
7
8
  import { SourceModuleSnapshot } from '../value-objects/source-module-snapshot.js';
8
9
 
9
10
  export interface SourceModuleAnalyzerPort {
10
- analyzeMany(files: readonly FilePath[]): Promise<readonly SourceModuleSnapshot[]>;
11
+ analyzeMany(
12
+ files: readonly FilePath[],
13
+ architecture?: ArchitectureSpec
14
+ ): Promise<readonly SourceModuleSnapshot[]>;
11
15
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { RuleDefinitionRegistry } from './rule-definition-registry.js';
7
+ import { CLEAN_PRESET_SPEC, type ArchitectureSpec } from '../value-objects/architecture-spec.js';
7
8
  import { ImportGraph, matchesPattern } from '../value-objects/import-graph.js';
8
9
  import { LayerBoundary } from '../value-objects/layer-boundary.js';
9
10
  import { LintReport } from '../value-objects/lint-report.js';
@@ -16,6 +17,7 @@ type LintRunnerParams = {
16
17
  readonly snapshots: readonly SourceModuleSnapshot[];
17
18
  readonly importGraph: ImportGraph;
18
19
  readonly durationMs: number;
20
+ readonly architecture?: ArchitectureSpec;
19
21
  };
20
22
 
21
23
  const toNumber = (value: unknown, fallback: number): number =>
@@ -37,6 +39,8 @@ export class LintRunner {
37
39
  }
38
40
 
39
41
  run(params: LintRunnerParams): LintReport {
42
+ const architecture = params.architecture ?? CLEAN_PRESET_SPEC;
43
+
40
44
  for (const rule of params.rules) {
41
45
  this.ruleDefinitionRegistry.getByName(rule.name);
42
46
  }
@@ -100,7 +104,7 @@ export class LintRunner {
100
104
  const ignorePatterns = toStringArray(rule.config.ignorePatterns, Object.freeze([]));
101
105
 
102
106
  for (const edge of params.importGraph.findLayerViolations(
103
- LayerBoundary.standardMatrix(),
107
+ LayerBoundary.standardMatrix(architecture),
104
108
  layerByFile,
105
109
  ignorePatterns
106
110
  )) {
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @layer domain
3
+ * @unit biome-ast-engine
4
+ */
5
+
6
+ export type ArchitectureSpec = {
7
+ readonly layers: readonly string[];
8
+ readonly allowedDependencies: Readonly<Record<string, readonly string[]>>;
9
+ };
10
+
11
+ const freezeDependencyMap = (
12
+ map: Record<string, readonly string[]>
13
+ ): Readonly<Record<string, readonly string[]>> => {
14
+ const frozen: Record<string, readonly string[]> = {};
15
+
16
+ for (const [key, value] of Object.entries(map)) {
17
+ frozen[key] = Object.freeze([...value]);
18
+ }
19
+
20
+ return Object.freeze(frozen);
21
+ };
22
+
23
+ export const freezeArchitectureSpec = (spec: ArchitectureSpec): ArchitectureSpec => {
24
+ return Object.freeze({
25
+ layers: Object.freeze([...spec.layers]),
26
+ allowedDependencies: freezeDependencyMap({ ...spec.allowedDependencies }),
27
+ });
28
+ };
29
+
30
+ export const CLEAN_PRESET_SPEC: ArchitectureSpec = freezeArchitectureSpec({
31
+ layers: ['domain', 'application', 'infrastructure', 'presentation'],
32
+ allowedDependencies: {
33
+ domain: ['domain'],
34
+ application: ['application', 'domain'],
35
+ infrastructure: ['infrastructure', 'application', 'domain'],
36
+ presentation: ['presentation', 'application', 'domain'],
37
+ },
38
+ });
@@ -3,6 +3,7 @@
3
3
  * @unit biome-ast-engine
4
4
  */
5
5
 
6
+ import { CLEAN_PRESET_SPEC, type ArchitectureSpec } from './architecture-spec.js';
6
7
  import { LayerName } from './layer-name.js';
7
8
 
8
9
  type LayerBoundaryProps = {
@@ -11,8 +12,6 @@ type LayerBoundaryProps = {
11
12
  readonly allowed: boolean;
12
13
  };
13
14
 
14
- const LAYER_NAMES = Object.freeze(['domain', 'application', 'infrastructure', 'presentation'] as const);
15
-
16
15
  export class LayerBoundary {
17
16
  readonly sourceLayer: LayerName;
18
17
  readonly targetLayer: LayerName;
@@ -28,16 +27,16 @@ export class LayerBoundary {
28
27
  return Object.freeze(new LayerBoundary(props));
29
28
  }
30
29
 
31
- static standardMatrix(): readonly LayerBoundary[] {
30
+ static standardMatrix(spec: ArchitectureSpec = CLEAN_PRESET_SPEC): readonly LayerBoundary[] {
32
31
  return Object.freeze(
33
- LAYER_NAMES.flatMap((source) => {
34
- const sourceLayer = LayerName.fromString(source);
32
+ spec.layers.flatMap((source) => {
33
+ const sourceLayer = LayerName.fromString(source, spec);
35
34
 
36
- return LAYER_NAMES.map((target) =>
35
+ return spec.layers.map((target) =>
37
36
  LayerBoundary.create({
38
37
  sourceLayer,
39
- targetLayer: LayerName.fromString(target),
40
- allowed: sourceLayer.canDependOn(LayerName.fromString(target)),
38
+ targetLayer: LayerName.fromString(target, spec),
39
+ allowed: sourceLayer.canDependOn(LayerName.fromString(target, spec)),
41
40
  })
42
41
  );
43
42
  })
@@ -3,50 +3,40 @@
3
3
  * @unit biome-ast-engine
4
4
  */
5
5
 
6
- export type LayerNameValue = 'domain' | 'application' | 'infrastructure' | 'presentation';
6
+ import { type ArchitectureSpec, CLEAN_PRESET_SPEC } from "./architecture-spec.js";
7
7
 
8
- const VALID_LAYER_NAMES = new Set<LayerNameValue>([
9
- 'domain',
10
- 'application',
11
- 'infrastructure',
12
- 'presentation',
13
- ]);
14
-
15
- const ALLOWED_DEPENDENCIES: Readonly<Record<LayerNameValue, readonly LayerNameValue[]>> = {
16
- domain: Object.freeze(['domain']),
17
- application: Object.freeze(['application', 'domain']),
18
- infrastructure: Object.freeze(['infrastructure', 'application', 'domain']),
19
- presentation: Object.freeze(['presentation', 'application', 'domain']),
20
- };
8
+ export type LayerNameValue = string;
21
9
 
22
10
  export class InvalidLayerNameError extends Error {
23
11
  constructor(value: string) {
24
12
  super(`Invalid LayerName: ${value}`);
25
- this.name = 'InvalidLayerNameError';
13
+ this.name = "InvalidLayerNameError";
26
14
  }
27
15
  }
28
16
 
29
17
  export class LayerName {
30
18
  readonly value: LayerNameValue;
19
+ private readonly spec: ArchitectureSpec;
31
20
 
32
- private constructor(value: LayerNameValue) {
21
+ private constructor(value: LayerNameValue, spec: ArchitectureSpec) {
33
22
  this.value = value;
23
+ this.spec = spec;
34
24
  }
35
25
 
36
- static fromString(value: string): LayerName {
37
- if (!VALID_LAYER_NAMES.has(value as LayerNameValue)) {
26
+ static fromString(value: string, spec: ArchitectureSpec = CLEAN_PRESET_SPEC): LayerName {
27
+ if (!spec.layers.includes(value)) {
38
28
  throw new InvalidLayerNameError(value);
39
29
  }
40
30
 
41
- return Object.freeze(new LayerName(value as LayerNameValue));
31
+ return Object.freeze(new LayerName(value, spec)) as LayerName;
42
32
  }
43
33
 
44
- static tryFromString(value: string): LayerName | null {
45
- if (!VALID_LAYER_NAMES.has(value as LayerNameValue)) {
34
+ static tryFromString(value: string, spec: ArchitectureSpec = CLEAN_PRESET_SPEC): LayerName | null {
35
+ if (!spec.layers.includes(value)) {
46
36
  return null;
47
37
  }
48
38
 
49
- return Object.freeze(new LayerName(value as LayerNameValue));
39
+ return Object.freeze(new LayerName(value, spec)) as LayerName;
50
40
  }
51
41
 
52
42
  equals(other: LayerName): boolean {
@@ -54,7 +44,9 @@ export class LayerName {
54
44
  }
55
45
 
56
46
  canDependOn(target: LayerName): boolean {
57
- return ALLOWED_DEPENDENCIES[this.value].includes(target.value);
47
+ const allowed = this.spec.allowedDependencies[this.value] ?? [];
48
+
49
+ return allowed.includes(target.value);
58
50
  }
59
51
 
60
52
  toPathSegment(): string {
@@ -3,6 +3,7 @@
3
3
  * @unit biome-ast-engine
4
4
  */
5
5
 
6
+ import { CLEAN_PRESET_SPEC, type ArchitectureSpec } from './architecture-spec.js';
6
7
  import { FilePath } from './file-path.js';
7
8
  import { ImportEdge } from './import-edge.js';
8
9
  import { LayerName } from './layer-name.js';
@@ -30,7 +31,10 @@ const ensureNonNegative = (value: number, label: string): number => {
30
31
  return value;
31
32
  };
32
33
 
33
- const normalizeDeclaredLayer = (value: LayerName | string | null): LayerName | null => {
34
+ const normalizeDeclaredLayer = (
35
+ value: LayerName | string | null,
36
+ spec: ArchitectureSpec
37
+ ): LayerName | null => {
34
38
  if (value === null) {
35
39
  return null;
36
40
  }
@@ -40,7 +44,7 @@ const normalizeDeclaredLayer = (value: LayerName | string | null): LayerName | n
40
44
  }
41
45
 
42
46
  if (typeof value === 'string') {
43
- return LayerName.tryFromString(value);
47
+ return LayerName.tryFromString(value, spec);
44
48
  }
45
49
 
46
50
  return null;
@@ -77,8 +81,11 @@ export class SourceModuleSnapshot {
77
81
  this.hasRawLayerAnnotation = props.hasRawLayerAnnotation;
78
82
  }
79
83
 
80
- static create(props: SourceModuleSnapshotProps): SourceModuleSnapshot {
81
- const declaredLayer = normalizeDeclaredLayer(props.declaredLayer);
84
+ static create(
85
+ props: SourceModuleSnapshotProps,
86
+ spec: ArchitectureSpec = CLEAN_PRESET_SPEC
87
+ ): SourceModuleSnapshot {
88
+ const declaredLayer = normalizeDeclaredLayer(props.declaredLayer, spec);
82
89
 
83
90
  return Object.freeze(
84
91
  new SourceModuleSnapshot({
@@ -3,27 +3,49 @@
3
3
  * @unit biome-ast-engine
4
4
  */
5
5
 
6
- import type { RuleConfigProviderPort } from '../../domain/ports/rule-config-provider-port.js';
6
+ import type {
7
+ ArchitectureProviderInfo,
8
+ RuleConfigProviderPort,
9
+ } from '../../domain/ports/rule-config-provider-port.js';
7
10
 
8
11
  export interface L1ConfigInput {
9
12
  readonly enabled: boolean;
10
13
  readonly rules: Record<string, 'error' | 'warning' | 'off'>;
11
14
  }
12
15
 
16
+ export type ArchitectureConfigInput = ArchitectureProviderInfo;
17
+
13
18
  const DEFAULT_L1_CONFIG: L1ConfigInput = {
14
19
  enabled: true,
15
20
  rules: {},
16
21
  };
17
22
 
23
+ const DEFAULT_ARCHITECTURE: ArchitectureConfigInput = Object.freeze({
24
+ preset: 'clean',
25
+ layers: Object.freeze(['domain', 'application', 'infrastructure', 'presentation']),
26
+ allowedDependencies: Object.freeze({
27
+ domain: Object.freeze(['domain']),
28
+ application: Object.freeze(['application', 'domain']),
29
+ infrastructure: Object.freeze(['infrastructure', 'application', 'domain']),
30
+ presentation: Object.freeze(['presentation', 'application', 'domain']),
31
+ }),
32
+ });
33
+
18
34
  /**
19
35
  * RuleConfigProviderPort の実装。
20
- * 外部から L1 設定を注入可能。未指定時はデフォルト(enabled: true, rules: {} — 全ルールデフォルト有効)。
36
+ * 外部から L1 設定と architecture 情報を注入可能。未指定時はデフォルト
37
+ * (L1: enabled=true, rules={}、architecture: clean preset)。
21
38
  */
22
39
  export class HarnessConfigProviderAdapter implements RuleConfigProviderPort {
23
40
  private readonly l1Config: L1ConfigInput;
41
+ private readonly architecture: ArchitectureConfigInput;
24
42
 
25
- constructor(injectedL1Config?: L1ConfigInput) {
43
+ constructor(
44
+ injectedL1Config?: L1ConfigInput,
45
+ injectedArchitecture?: ArchitectureConfigInput,
46
+ ) {
26
47
  this.l1Config = injectedL1Config ?? DEFAULT_L1_CONFIG;
48
+ this.architecture = injectedArchitecture ?? DEFAULT_ARCHITECTURE;
27
49
  }
28
50
 
29
51
  async getL1Config(): Promise<{
@@ -35,4 +57,8 @@ export class HarnessConfigProviderAdapter implements RuleConfigProviderPort {
35
57
  rules: { ...this.l1Config.rules },
36
58
  };
37
59
  }
60
+
61
+ async getArchitecture(): Promise<ArchitectureProviderInfo> {
62
+ return this.architecture;
63
+ }
38
64
  }
@@ -7,6 +7,7 @@ import * as ts from 'typescript';
7
7
  import * as path from 'node:path';
8
8
  import type { FilePath } from '../../domain/value-objects/file-path.js';
9
9
  import type { SourceModuleAnalyzerPort } from '../../domain/ports/source-module-analyzer-port.js';
10
+ import type { ArchitectureSpec } from '../../domain/value-objects/architecture-spec.js';
10
11
  import { SourceModuleSnapshot } from '../../domain/value-objects/source-module-snapshot.js';
11
12
  import { ImportEdge } from '../../domain/value-objects/import-edge.js';
12
13
  import { FilePath as FilePathVO } from '../../domain/value-objects/file-path.js';
@@ -30,7 +31,10 @@ export class TypeScriptSourceModuleAnalyzerAdapter implements SourceModuleAnalyz
30
31
  this.rootDir = deps.rootDir;
31
32
  }
32
33
 
33
- async analyzeMany(files: readonly FilePath[]): Promise<readonly SourceModuleSnapshot[]> {
34
+ async analyzeMany(
35
+ files: readonly FilePath[],
36
+ architecture?: ArchitectureSpec
37
+ ): Promise<readonly SourceModuleSnapshot[]> {
34
38
  const absolutePaths = files.map((f) => path.resolve(this.rootDir, f.toString()));
35
39
 
36
40
  const program = ts.createProgram(absolutePaths, {
@@ -62,20 +66,23 @@ export class TypeScriptSourceModuleAnalyzerAdapter implements SourceModuleAnalyz
62
66
  const isEntrypoint = this.isEntrypointCandidate(filePath);
63
67
 
64
68
  snapshots.push(
65
- SourceModuleSnapshot.create({
66
- filePath,
67
- declaredUnit: unitResult.unitNames[0],
68
- declaredLayer: layerResult.layerName,
69
- imports,
70
- anyTypeCount: anyCount,
71
- typedNodeCount: typedCount,
72
- commentLineCount: densityResult.commentLineCount,
73
- logicalLineCount: densityResult.logicalLineCount,
74
- repeatedCommentBlocks: densityResult.repeatedCommentBlocks,
75
- duplicationFingerprints: [],
76
- exportedSymbols: exports,
77
- isEntrypointCandidate: isEntrypoint,
78
- })
69
+ SourceModuleSnapshot.create(
70
+ {
71
+ filePath,
72
+ declaredUnit: unitResult.unitNames[0],
73
+ declaredLayer: layerResult.layerName,
74
+ imports,
75
+ anyTypeCount: anyCount,
76
+ typedNodeCount: typedCount,
77
+ commentLineCount: densityResult.commentLineCount,
78
+ logicalLineCount: densityResult.logicalLineCount,
79
+ repeatedCommentBlocks: densityResult.repeatedCommentBlocks,
80
+ duplicationFingerprints: [],
81
+ exportedSymbols: exports,
82
+ isEntrypointCandidate: isEntrypoint,
83
+ },
84
+ architecture
85
+ )
79
86
  );
80
87
  }
81
88
 
@@ -3,6 +3,7 @@
3
3
  * @unit biome-ast-engine
4
4
  */
5
5
 
6
+ import type { ArchitectureSpec } from '../../domain/value-objects/architecture-spec.js';
6
7
  import { FilePath } from '../../domain/value-objects/file-path.js';
7
8
  import { ImportEdge } from '../../domain/value-objects/import-edge.js';
8
9
  import { SourceModuleSnapshot } from '../../domain/value-objects/source-module-snapshot.js';
@@ -31,7 +32,10 @@ export type RawModuleData = {
31
32
  /**
32
33
  * AST解析結果の生データを SourceModuleSnapshot 値オブジェクトへ変換する。
33
34
  */
34
- export const toSourceModuleSnapshot = (raw: RawModuleData): SourceModuleSnapshot => {
35
+ export const toSourceModuleSnapshot = (
36
+ raw: RawModuleData,
37
+ architecture?: ArchitectureSpec
38
+ ): SourceModuleSnapshot => {
35
39
  const filePath = FilePath.fromWorkspaceRelative(raw.filePath);
36
40
 
37
41
  const imports = raw.imports.map((edge) =>
@@ -42,25 +46,30 @@ export const toSourceModuleSnapshot = (raw: RawModuleData): SourceModuleSnapshot
42
46
  })
43
47
  );
44
48
 
45
- return SourceModuleSnapshot.create({
46
- filePath,
47
- declaredUnit: raw.declaredUnit,
48
- declaredLayer: raw.declaredLayer,
49
- imports,
50
- anyTypeCount: raw.anyTypeCount,
51
- typedNodeCount: raw.typedNodeCount,
52
- commentLineCount: raw.commentLineCount,
53
- logicalLineCount: raw.logicalLineCount,
54
- repeatedCommentBlocks: raw.repeatedCommentBlocks,
55
- duplicationFingerprints: raw.duplicationFingerprints,
56
- exportedSymbols: raw.exportedSymbols,
57
- isEntrypointCandidate: raw.isEntrypointCandidate,
58
- });
49
+ return SourceModuleSnapshot.create(
50
+ {
51
+ filePath,
52
+ declaredUnit: raw.declaredUnit,
53
+ declaredLayer: raw.declaredLayer,
54
+ imports,
55
+ anyTypeCount: raw.anyTypeCount,
56
+ typedNodeCount: raw.typedNodeCount,
57
+ commentLineCount: raw.commentLineCount,
58
+ logicalLineCount: raw.logicalLineCount,
59
+ repeatedCommentBlocks: raw.repeatedCommentBlocks,
60
+ duplicationFingerprints: raw.duplicationFingerprints,
61
+ exportedSymbols: raw.exportedSymbols,
62
+ isEntrypointCandidate: raw.isEntrypointCandidate,
63
+ },
64
+ architecture
65
+ );
59
66
  };
60
67
 
61
68
  /**
62
69
  * 複数の生データを一括変換する。
63
70
  */
64
71
  export const toSourceModuleSnapshots = (
65
- rawList: readonly RawModuleData[]
66
- ): readonly SourceModuleSnapshot[] => rawList.map(toSourceModuleSnapshot);
72
+ rawList: readonly RawModuleData[],
73
+ architecture?: ArchitectureSpec
74
+ ): readonly SourceModuleSnapshot[] =>
75
+ rawList.map((raw) => toSourceModuleSnapshot(raw, architecture));
@@ -7,4 +7,5 @@ import type { HarnessConfigV2 } from '../../domain/harness-config.js';
7
7
  export interface ResolvedConfigOutput {
8
8
  readonly config: HarnessConfigV2;
9
9
  readonly sourcePath: string;
10
+ readonly schemaVersion: 'v2' | 'v3';
10
11
  }