phasegate 0.91.0 → 0.107.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 (68) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/README.ja.md +15 -7
  3. package/README.md +24 -4
  4. package/docs/ADR/ADR-015-architecture-preset.md +183 -0
  5. package/docs/guide/codex-integration.md +7 -2
  6. package/docs/guide/installation.md +10 -2
  7. package/docs/guide/preset-selection.md +170 -0
  8. package/docs/guide/quick-vs-full-mode.md +3 -3
  9. package/docs/guide/retrofit-adoption.md +19 -2
  10. package/docs/guide/skills-overview.md +1 -1
  11. package/package.json +7 -1
  12. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +108 -100
  13. package/scripts/harness/agent-integration/domain/ports/phase-gate-query-port.ts +3 -3
  14. package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +26 -30
  15. package/scripts/harness/agent-integration/infrastructure/adapters/file-system-story-reflection-query-adapter.ts +6 -2
  16. package/scripts/harness/biome-ast-engine/application/dto/analyze-import-graph-input.ts +3 -0
  17. package/scripts/harness/biome-ast-engine/application/dto/resolve-enabled-rules-output.ts +2 -0
  18. package/scripts/harness/biome-ast-engine/application/mappers/resolve-enabled-rules-output-mapper.ts +4 -1
  19. package/scripts/harness/biome-ast-engine/application/usecases/analyze-import-graph-usecase.ts +4 -1
  20. package/scripts/harness/biome-ast-engine/application/usecases/execute-lint-usecase.ts +2 -0
  21. package/scripts/harness/biome-ast-engine/application/usecases/resolve-enabled-rules-usecase.ts +31 -3
  22. package/scripts/harness/biome-ast-engine/composition-root.ts +10 -2
  23. package/scripts/harness/biome-ast-engine/domain/ports/rule-config-provider-port.ts +16 -0
  24. package/scripts/harness/biome-ast-engine/domain/ports/source-module-analyzer-port.ts +5 -1
  25. package/scripts/harness/biome-ast-engine/domain/services/lint-runner.ts +5 -1
  26. package/scripts/harness/biome-ast-engine/domain/value-objects/architecture-spec.ts +38 -0
  27. package/scripts/harness/biome-ast-engine/domain/value-objects/layer-boundary.ts +7 -8
  28. package/scripts/harness/biome-ast-engine/domain/value-objects/layer-name.ts +15 -23
  29. package/scripts/harness/biome-ast-engine/domain/value-objects/source-module-snapshot.ts +11 -4
  30. package/scripts/harness/biome-ast-engine/infrastructure/adapters/harness-config-provider-adapter.ts +29 -3
  31. package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +22 -15
  32. package/scripts/harness/biome-ast-engine/infrastructure/mappers/source-module-snapshot-mapper.ts +26 -17
  33. package/scripts/harness/config-foundation/application/dto/resolved-config-output.ts +1 -0
  34. package/scripts/harness/config-foundation/application/usecases/load-resolved-config-use-case.ts +34 -2
  35. package/scripts/harness/config-foundation/application/usecases/migrate-schema-use-case.ts +89 -0
  36. package/scripts/harness/config-foundation/composition-root.ts +8 -0
  37. package/scripts/harness/config-foundation/domain/harness-config.ts +6 -0
  38. package/scripts/harness/config-foundation/domain/services/architecture-resolution-service.ts +257 -0
  39. package/scripts/harness/config-foundation/domain/value-objects/architecture-config.ts +66 -0
  40. package/scripts/harness/config-foundation/domain/value-objects/architecture-preset-catalog.ts +75 -0
  41. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +546 -0
  42. package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts +18 -6
  43. package/scripts/harness/config-foundation/presentation/cli/migrate-schema-command-handler.ts +83 -0
  44. package/scripts/harness/integrations/pre-commit.ts +211 -52
  45. package/scripts/harness/main.ts +460 -340
  46. package/scripts/harness/phase-dependency-model/domain/ports/story-reflection-file-system-port.ts +2 -4
  47. package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +54 -17
  48. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +154 -36
  49. package/scripts/harness/setup/skill-deployer.ts +140 -102
  50. package/scripts/harness/skill-quality/domain/errors/skill-quality-error.ts +24 -23
  51. package/scripts/harness/skill-quality/domain/value-objects/commit-message.ts +23 -7
  52. package/scripts/harness/traceability-model/application/usecases/apply-work-item-migration-usecase.ts +52 -0
  53. package/scripts/harness/traceability-model/application/usecases/plan-work-item-migration-usecase.ts +29 -0
  54. package/scripts/harness/traceability-model/application/usecases/validate-design-story-annotations-usecase.ts +83 -18
  55. package/scripts/harness/traceability-model/composition-root.ts +48 -30
  56. package/scripts/harness/traceability-model/domain/ports/design-document-port.ts +9 -15
  57. package/scripts/harness/traceability-model/domain/ports/work-item-migration-apply-port.ts +11 -0
  58. package/scripts/harness/traceability-model/domain/ports/work-item-migration-source-port.ts +9 -0
  59. package/scripts/harness/traceability-model/domain/services/work-item-migration-planner.ts +162 -0
  60. package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +57 -0
  61. package/scripts/harness/traceability-model/domain/value-objects/work-item-migration-candidate.ts +47 -0
  62. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-migration-apply-gateway.ts +110 -0
  63. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-migration-source-gateway.ts +182 -0
  64. package/scripts/harness/traceability-model/infrastructure/gateways/markdown-design-document-gateway.ts +29 -43
  65. package/scripts/harness/traceability-model/infrastructure/parsers/work-item-frontmatter-parser.ts +136 -0
  66. package/scripts/harness/traceability-model/presentation/cli/migrate-work-items-command-handler.ts +186 -0
  67. package/skills/quick-implementor/SKILL.md +17 -1
  68. package/templates/.husky/commit-msg +1 -0
@@ -5,11 +5,13 @@
5
5
  import { ConfigValidationError } from '../../domain/errors/config-validation-error.js';
6
6
  import {
7
7
  HarnessConfig,
8
+ type HarnessConfigResolvedDocument,
8
9
  type HarnessConfigSourceDocument,
9
10
  type PresetId,
10
11
  } from '../../domain/harness-config.js';
11
12
  import type { ConfigRepositoryPort } from '../../domain/ports/config-repository-port.js';
12
13
  import type { ConfigSchemaValidatorPort } from '../../domain/ports/config-schema-validator-port.js';
14
+ import { ArchitectureResolutionService } from '../../domain/services/architecture-resolution-service.js';
13
15
  import {
14
16
  PresetResolutionService,
15
17
  type PresetDefinition,
@@ -24,11 +26,13 @@ export interface LoadResolvedConfigUseCaseDependencies {
24
26
  readonly schemaValidator: ConfigSchemaValidatorPort;
25
27
  readonly presetDefinitions: PresetDefinitions;
26
28
  readonly presetResolutionService: PresetResolutionService;
29
+ readonly architectureResolutionService?: ArchitectureResolutionService;
27
30
  }
28
31
 
29
32
  export interface LoadedHarnessConfig {
30
33
  readonly aggregate: HarnessConfig;
31
34
  readonly path: string;
35
+ readonly schemaVersion: 'v2' | 'v3';
32
36
  }
33
37
 
34
38
  const PRESET_IDS = ['minimal', 'standard', 'strict'] as const satisfies readonly PresetId[];
@@ -94,6 +98,20 @@ export function resolveSourceDocument(
94
98
  );
95
99
  }
96
100
 
101
+ function resolveArchitecture(
102
+ sourceDocument: HarnessConfigSourceDocument,
103
+ resolvedDocument: HarnessConfigResolvedDocument,
104
+ architectureResolutionService: ArchitectureResolutionService | undefined,
105
+ ): HarnessConfigResolvedDocument {
106
+ const service = architectureResolutionService ?? new ArchitectureResolutionService();
107
+ const { document } = service.resolve(sourceDocument.architecture);
108
+
109
+ return {
110
+ ...resolvedDocument,
111
+ architecture: document,
112
+ };
113
+ }
114
+
97
115
  export async function loadHarnessConfig(
98
116
  dependencies: LoadResolvedConfigUseCaseDependencies,
99
117
  configPath?: string,
@@ -103,18 +121,28 @@ export async function loadHarnessConfig(
103
121
  validateDocumentOrThrow(dependencies.schemaValidator, document);
104
122
 
105
123
  const sourceDocument = toSourceDocument(document);
106
- const resolvedDocument = resolveSourceDocument(
124
+ const resolvedFromPreset = resolveSourceDocument(
107
125
  sourceDocument,
108
126
  dependencies.presetDefinitions,
109
127
  dependencies.presetResolutionService,
110
128
  );
111
129
 
130
+ const resolvedDocument = resolveArchitecture(
131
+ sourceDocument,
132
+ resolvedFromPreset,
133
+ dependencies.architectureResolutionService,
134
+ );
135
+
136
+ const schemaVersion: 'v2' | 'v3' =
137
+ sourceDocument.architecture === undefined ? 'v2' : 'v3';
138
+
112
139
  return {
113
140
  aggregate: HarnessConfig.reconstitute({
114
141
  sourceDocument,
115
142
  resolvedDocument,
116
143
  }),
117
144
  path,
145
+ schemaVersion,
118
146
  };
119
147
  }
120
148
 
@@ -123,21 +151,24 @@ export class LoadResolvedConfigUseCase {
123
151
  private readonly schemaValidator: ConfigSchemaValidatorPort;
124
152
  private readonly presetDefinitions: PresetDefinitions;
125
153
  private readonly presetResolutionService: PresetResolutionService;
154
+ private readonly architectureResolutionService: ArchitectureResolutionService | undefined;
126
155
 
127
156
  constructor(dependencies: LoadResolvedConfigUseCaseDependencies) {
128
157
  this.configRepository = dependencies.configRepository;
129
158
  this.schemaValidator = dependencies.schemaValidator;
130
159
  this.presetDefinitions = dependencies.presetDefinitions;
131
160
  this.presetResolutionService = dependencies.presetResolutionService;
161
+ this.architectureResolutionService = dependencies.architectureResolutionService;
132
162
  }
133
163
 
134
164
  async execute(configPath?: string): Promise<ResolvedConfigOutput> {
135
- const { aggregate, path } = await loadHarnessConfig(
165
+ const { aggregate, path, schemaVersion } = await loadHarnessConfig(
136
166
  {
137
167
  configRepository: this.configRepository,
138
168
  schemaValidator: this.schemaValidator,
139
169
  presetDefinitions: this.presetDefinitions,
140
170
  presetResolutionService: this.presetResolutionService,
171
+ architectureResolutionService: this.architectureResolutionService,
141
172
  },
142
173
  configPath,
143
174
  );
@@ -145,6 +176,7 @@ export class LoadResolvedConfigUseCase {
145
176
  return {
146
177
  config: aggregate.toResolvedConfig(),
147
178
  sourcePath: path,
179
+ schemaVersion,
148
180
  };
149
181
  }
150
182
  }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @layer application
3
+ * @unit config-foundation
4
+ *
5
+ * MigrateSchemaUseCase
6
+ * phasegate.config.json の schema を v2 から v3 へアップグレードする。
7
+ * v3 は `architecture` キーの存在で判定する。既に v3 の場合は no-op(alreadyUpToDate=true)。
8
+ *
9
+ * 現時点では v3 のみサポート。targetVersion != 'v3' は UnsupportedSchemaVersionError。
10
+ */
11
+
12
+ import type { ConfigRepositoryPort } from '../../domain/ports/config-repository-port.js';
13
+
14
+ export type MigrateSchemaInput = {
15
+ readonly targetVersion: 'v3';
16
+ readonly configPath?: string;
17
+ };
18
+
19
+ export type MigrateSchemaOutput = {
20
+ readonly configPath: string;
21
+ readonly migrated: boolean;
22
+ readonly alreadyUpToDate: boolean;
23
+ readonly appliedChanges: readonly string[];
24
+ };
25
+
26
+ export class UnsupportedSchemaVersionError extends Error {
27
+ constructor(version: string) {
28
+ super(`Unsupported schema version: ${version}. Only 'v3' is supported.`);
29
+ this.name = 'UnsupportedSchemaVersionError';
30
+ Object.setPrototypeOf(this, new.target.prototype);
31
+ }
32
+ }
33
+
34
+ export class InvalidConfigShapeError extends Error {
35
+ constructor(configPath: string) {
36
+ super(`Config is not a JSON object: ${configPath}`);
37
+ this.name = 'InvalidConfigShapeError';
38
+ Object.setPrototypeOf(this, new.target.prototype);
39
+ }
40
+ }
41
+
42
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
43
+ typeof value === 'object' && value !== null && !Array.isArray(value);
44
+
45
+ const V3_ARCHITECTURE_DEFAULT = Object.freeze({ preset: 'clean' });
46
+
47
+ export class MigrateSchemaUseCase {
48
+ private readonly configRepository: ConfigRepositoryPort;
49
+
50
+ constructor(deps: { configRepository: ConfigRepositoryPort }) {
51
+ this.configRepository = deps.configRepository;
52
+ }
53
+
54
+ async execute(input: MigrateSchemaInput): Promise<Readonly<MigrateSchemaOutput>> {
55
+ if (input.targetVersion !== 'v3') {
56
+ throw new UnsupportedSchemaVersionError(input.targetVersion);
57
+ }
58
+
59
+ const loaded = await this.configRepository.load(input.configPath);
60
+
61
+ if (!isPlainObject(loaded.document)) {
62
+ throw new InvalidConfigShapeError(loaded.path);
63
+ }
64
+
65
+ if ('architecture' in loaded.document) {
66
+ return Object.freeze({
67
+ configPath: loaded.path,
68
+ migrated: false,
69
+ alreadyUpToDate: true,
70
+ appliedChanges: Object.freeze([]),
71
+ });
72
+ }
73
+
74
+ const upgraded: Record<string, unknown> = {
75
+ ...loaded.document,
76
+ architecture: { ...V3_ARCHITECTURE_DEFAULT },
77
+ };
78
+ await this.configRepository.save(loaded.path, upgraded);
79
+
80
+ return Object.freeze({
81
+ configPath: loaded.path,
82
+ migrated: true,
83
+ alreadyUpToDate: false,
84
+ appliedChanges: Object.freeze([
85
+ 'architecture: { preset: "clean" } を追加',
86
+ ]),
87
+ });
88
+ }
89
+ }
@@ -15,8 +15,10 @@ import { EnableFeatureUseCase } from './application/usecases/enable-feature-use-
15
15
  import { DisableFeatureUseCase } from './application/usecases/disable-feature-use-case.js';
16
16
  import { ListAvailableFeaturesUseCase } from './application/usecases/list-available-features-use-case.js';
17
17
  import { LoadResolvedConfigUseCase } from './application/usecases/load-resolved-config-use-case.js';
18
+ import { MigrateSchemaUseCase } from './application/usecases/migrate-schema-use-case.js';
18
19
  import { EnableFeatureCommandHandler } from './presentation/cli/enable-feature-command-handler.js';
19
20
  import { DisableFeatureCommandHandler } from './presentation/cli/disable-feature-command-handler.js';
21
+ import { MigrateSchemaCommandHandler } from './presentation/cli/migrate-schema-command-handler.js';
20
22
 
21
23
  export function createConfigFoundationModule() {
22
24
  // Infrastructure
@@ -50,6 +52,7 @@ export function createConfigFoundationModule() {
50
52
  presetDefinitions,
51
53
  presetResolutionService,
52
54
  });
55
+ const migrateSchemaUseCase = new MigrateSchemaUseCase({ configRepository });
53
56
 
54
57
  // Presentation handlers
55
58
  const enableFeatureCommandHandler = new EnableFeatureCommandHandler({
@@ -60,17 +63,22 @@ export function createConfigFoundationModule() {
60
63
  disableFeatureUseCase,
61
64
  listAvailableFeaturesUseCase,
62
65
  });
66
+ const migrateSchemaCommandHandler = new MigrateSchemaCommandHandler({
67
+ migrateSchemaUseCase,
68
+ });
63
69
 
64
70
  return {
65
71
  handlers: {
66
72
  enableFeatureCommandHandler,
67
73
  disableFeatureCommandHandler,
74
+ migrateSchemaCommandHandler,
68
75
  },
69
76
  usecases: {
70
77
  enableFeatureUseCase,
71
78
  disableFeatureUseCase,
72
79
  listAvailableFeaturesUseCase,
73
80
  loadResolvedConfigUseCase,
81
+ migrateSchemaUseCase,
74
82
  },
75
83
  } as const;
76
84
  }
@@ -20,6 +20,10 @@ import type { L1Config } from './value-objects/l1-config.js';
20
20
  import type { L2Config } from './value-objects/l2-config.js';
21
21
  import type { L3Config } from './value-objects/l3-config.js';
22
22
  import type { L4Config } from './value-objects/l4-config.js';
23
+ import type {
24
+ ArchitectureConfigDocument,
25
+ ArchitectureConfigSource,
26
+ } from './value-objects/architecture-config.js';
23
27
 
24
28
  export type LayerId = 'L1' | 'L2' | 'L3' | 'L4';
25
29
  export type PresetId = 'minimal' | 'standard' | 'strict';
@@ -45,6 +49,7 @@ export interface HarnessConfigSourceDocument {
45
49
  harnesses: Partial<HarnessConfigResolvedDocument['harnesses']>;
46
50
  paths: HarnessConfigResolvedDocument['paths'];
47
51
  reporting: HarnessConfigResolvedDocument['reporting'];
52
+ architecture?: ArchitectureConfigSource;
48
53
  }
49
54
 
50
55
  export interface HarnessConfigResolvedDocument {
@@ -103,6 +108,7 @@ export interface HarnessConfigResolvedDocument {
103
108
  format: string;
104
109
  outputDir: string;
105
110
  };
111
+ architecture?: ArchitectureConfigDocument;
106
112
  }
107
113
 
108
114
  export type HarnessConfigV2 = HarnessConfigResolvedDocument;
@@ -0,0 +1,257 @@
1
+ /**
2
+ * @layer domain
3
+ * @unit config-foundation
4
+ */
5
+
6
+ import { ConfigValidationError } from '../errors/config-validation-error.js';
7
+ import {
8
+ freezeArchitectureDocument,
9
+ type ArchitectureConfigDocument,
10
+ type ArchitectureConfigSource,
11
+ type ArchitectureLayerDetection,
12
+ type ArchitectureMetadataTags,
13
+ type ArchitecturePresetId,
14
+ } from '../value-objects/architecture-config.js';
15
+ import {
16
+ ARCHITECTURE_PRESET_CATALOG,
17
+ type ArchitecturePresetDefinition,
18
+ } from '../value-objects/architecture-preset-catalog.js';
19
+
20
+ const DEFAULT_METADATA_TAGS: ArchitectureMetadataTags = Object.freeze({
21
+ layer: '@layer',
22
+ unit: '@unit',
23
+ });
24
+
25
+ const DEFAULT_LAYER_DETECTION: ArchitectureLayerDetection = Object.freeze({
26
+ byPath: true,
27
+ byTag: true,
28
+ });
29
+
30
+ const CLEAN_DEFAULT_SOURCE: ArchitectureConfigSource = Object.freeze({
31
+ preset: 'clean',
32
+ });
33
+
34
+ export interface ArchitectureResolutionResult {
35
+ readonly document: ArchitectureConfigDocument;
36
+ readonly warnings: readonly string[];
37
+ }
38
+
39
+ const cloneDependencies = (
40
+ source: Readonly<Record<string, readonly string[]>>
41
+ ): Record<string, string[]> => {
42
+ const cloned: Record<string, string[]> = {};
43
+
44
+ for (const [key, value] of Object.entries(source)) {
45
+ cloned[key] = [...value];
46
+ }
47
+
48
+ return cloned;
49
+ };
50
+
51
+ const lookupPresetDefinition = (
52
+ preset: ArchitecturePresetId
53
+ ): ArchitecturePresetDefinition | null => {
54
+ if (preset === 'custom') {
55
+ return null;
56
+ }
57
+
58
+ return ARCHITECTURE_PRESET_CATALOG[preset];
59
+ };
60
+
61
+ const ensureCustomHasExplicitLayers = (source: ArchitectureConfigSource): void => {
62
+ if (source.preset !== 'custom') {
63
+ return;
64
+ }
65
+
66
+ if (source.layers === undefined || source.allowedDependencies === undefined) {
67
+ throw new ConfigValidationError(
68
+ 'architecture.preset "custom" では layers と allowedDependencies の明示指定が必須です'
69
+ );
70
+ }
71
+ };
72
+
73
+ const mergeLayers = (
74
+ preset: ArchitecturePresetDefinition | null,
75
+ override: readonly string[] | undefined
76
+ ): string[] => {
77
+ if (override !== undefined) {
78
+ return [...override];
79
+ }
80
+
81
+ return preset !== null ? [...preset.layers] : [];
82
+ };
83
+
84
+ const mergeDependencies = (
85
+ preset: ArchitecturePresetDefinition | null,
86
+ override: Readonly<Record<string, readonly string[]>> | undefined
87
+ ): Record<string, string[]> => {
88
+ if (override !== undefined) {
89
+ return cloneDependencies(override);
90
+ }
91
+
92
+ return preset !== null ? cloneDependencies(preset.allowedDependencies) : {};
93
+ };
94
+
95
+ const mergeMetadataTags = (
96
+ override: Partial<ArchitectureMetadataTags> | undefined
97
+ ): ArchitectureMetadataTags => {
98
+ return {
99
+ layer: override?.layer ?? DEFAULT_METADATA_TAGS.layer,
100
+ unit: override?.unit ?? DEFAULT_METADATA_TAGS.unit,
101
+ };
102
+ };
103
+
104
+ const mergeLayerDetection = (
105
+ override: Partial<ArchitectureLayerDetection> | undefined
106
+ ): ArchitectureLayerDetection => {
107
+ return {
108
+ byPath: override?.byPath ?? DEFAULT_LAYER_DETECTION.byPath,
109
+ byTag: override?.byTag ?? DEFAULT_LAYER_DETECTION.byTag,
110
+ };
111
+ };
112
+
113
+ const validateLayerDetection = (detection: ArchitectureLayerDetection): void => {
114
+ if (!detection.byPath && !detection.byTag) {
115
+ throw new ConfigValidationError(
116
+ 'architecture.layerDetection は byPath / byTag の少なくとも一方を true にする必要があります'
117
+ );
118
+ }
119
+ };
120
+
121
+ const validateDependencyKeys = (
122
+ layers: readonly string[],
123
+ allowedDependencies: Record<string, string[]>
124
+ ): void => {
125
+ for (const key of Object.keys(allowedDependencies)) {
126
+ if (!layers.includes(key)) {
127
+ throw new ConfigValidationError(
128
+ `architecture.allowedDependencies のキー "${key}" は layers 配列に含まれません (C2)`
129
+ );
130
+ }
131
+ }
132
+ };
133
+
134
+ const validateDependencyValues = (
135
+ layers: readonly string[],
136
+ allowedDependencies: Record<string, string[]>
137
+ ): void => {
138
+ for (const [key, targets] of Object.entries(allowedDependencies)) {
139
+ for (const target of targets) {
140
+ if (!layers.includes(target)) {
141
+ throw new ConfigValidationError(
142
+ `architecture.allowedDependencies["${key}"] の依存先 "${target}" は layers 配列に含まれません (C3)`
143
+ );
144
+ }
145
+ }
146
+ }
147
+ };
148
+
149
+ const autofillSelfReference = (
150
+ layers: readonly string[],
151
+ allowedDependencies: Record<string, string[]>,
152
+ warnings: string[]
153
+ ): void => {
154
+ for (const layer of layers) {
155
+ const targets = allowedDependencies[layer];
156
+
157
+ if (targets === undefined) {
158
+ continue;
159
+ }
160
+
161
+ if (!targets.includes(layer)) {
162
+ targets.unshift(layer);
163
+ warnings.push(
164
+ `architecture.allowedDependencies["${layer}"] に自己参照 "${layer}" を補完しました (C1)`
165
+ );
166
+ }
167
+ }
168
+ };
169
+
170
+ const autofillMissingLayers = (
171
+ layers: readonly string[],
172
+ allowedDependencies: Record<string, string[]>,
173
+ warnings: string[]
174
+ ): void => {
175
+ for (const layer of layers) {
176
+ if (!(layer in allowedDependencies)) {
177
+ allowedDependencies[layer] = [layer];
178
+ warnings.push(
179
+ `architecture.allowedDependencies に layer "${layer}" のエントリが無いため自己参照のみで補完しました (C4)`
180
+ );
181
+ }
182
+ }
183
+ };
184
+
185
+ const warnCircularDependencies = (
186
+ allowedDependencies: Record<string, string[]>,
187
+ warnings: string[]
188
+ ): void => {
189
+ const entries = Object.entries(allowedDependencies);
190
+
191
+ for (const [source, targets] of entries) {
192
+ for (const target of targets) {
193
+ if (target === source) {
194
+ continue;
195
+ }
196
+
197
+ const reverseTargets = allowedDependencies[target];
198
+
199
+ if (reverseTargets !== undefined && reverseTargets.includes(source)) {
200
+ warnings.push(
201
+ `architecture.allowedDependencies に循環依存の疑いがあります: "${source}" → "${target}" と "${target}" → "${source}" (C5)`
202
+ );
203
+ }
204
+ }
205
+ }
206
+ };
207
+
208
+ const freezeDependencies = (
209
+ dependencies: Record<string, string[]>
210
+ ): Record<string, readonly string[]> => {
211
+ const frozen: Record<string, readonly string[]> = {};
212
+
213
+ for (const [key, value] of Object.entries(dependencies)) {
214
+ frozen[key] = Object.freeze([...value]);
215
+ }
216
+
217
+ return frozen;
218
+ };
219
+
220
+ export class ArchitectureResolutionService {
221
+ resolve(source: ArchitectureConfigSource | undefined): ArchitectureResolutionResult {
222
+ const warnings: string[] = [];
223
+ const effectiveSource = source ?? CLEAN_DEFAULT_SOURCE;
224
+
225
+ ensureCustomHasExplicitLayers(effectiveSource);
226
+
227
+ const presetDefinition = lookupPresetDefinition(effectiveSource.preset);
228
+ const layers = mergeLayers(presetDefinition, effectiveSource.layers);
229
+ const allowedDependencies = mergeDependencies(
230
+ presetDefinition,
231
+ effectiveSource.allowedDependencies
232
+ );
233
+
234
+ validateDependencyKeys(layers, allowedDependencies);
235
+ validateDependencyValues(layers, allowedDependencies);
236
+ autofillMissingLayers(layers, allowedDependencies, warnings);
237
+ autofillSelfReference(layers, allowedDependencies, warnings);
238
+ warnCircularDependencies(allowedDependencies, warnings);
239
+
240
+ const layerDetection = mergeLayerDetection(effectiveSource.layerDetection);
241
+
242
+ validateLayerDetection(layerDetection);
243
+
244
+ const document: ArchitectureConfigDocument = freezeArchitectureDocument({
245
+ preset: effectiveSource.preset,
246
+ layers,
247
+ allowedDependencies: freezeDependencies(allowedDependencies),
248
+ metadataTags: mergeMetadataTags(effectiveSource.metadataTags),
249
+ layerDetection,
250
+ });
251
+
252
+ return Object.freeze({
253
+ document,
254
+ warnings: Object.freeze([...warnings]),
255
+ });
256
+ }
257
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @layer domain
3
+ * @unit config-foundation
4
+ */
5
+
6
+ export const ARCHITECTURE_PRESET_IDS = [
7
+ 'clean',
8
+ 'strict-ddd',
9
+ 'onion',
10
+ 'hexagonal',
11
+ 'layered',
12
+ 'flat',
13
+ 'custom',
14
+ ] as const;
15
+
16
+ export type ArchitecturePresetId = (typeof ARCHITECTURE_PRESET_IDS)[number];
17
+
18
+ export interface ArchitectureMetadataTags {
19
+ readonly layer: string;
20
+ readonly unit: string;
21
+ }
22
+
23
+ export interface ArchitectureLayerDetection {
24
+ readonly byPath: boolean;
25
+ readonly byTag: boolean;
26
+ }
27
+
28
+ export interface ArchitectureConfigSource {
29
+ readonly preset: ArchitecturePresetId;
30
+ readonly layers?: readonly string[];
31
+ readonly allowedDependencies?: Readonly<Record<string, readonly string[]>>;
32
+ readonly metadataTags?: Partial<ArchitectureMetadataTags>;
33
+ readonly layerDetection?: Partial<ArchitectureLayerDetection>;
34
+ }
35
+
36
+ export interface ArchitectureConfigDocument {
37
+ readonly preset: ArchitecturePresetId;
38
+ readonly layers: readonly string[];
39
+ readonly allowedDependencies: Readonly<Record<string, readonly string[]>>;
40
+ readonly metadataTags: ArchitectureMetadataTags;
41
+ readonly layerDetection: ArchitectureLayerDetection;
42
+ }
43
+
44
+ export const isArchitecturePresetId = (value: unknown): value is ArchitecturePresetId => {
45
+ return (
46
+ typeof value === 'string' && ARCHITECTURE_PRESET_IDS.includes(value as ArchitecturePresetId)
47
+ );
48
+ };
49
+
50
+ export const freezeArchitectureDocument = (
51
+ document: ArchitectureConfigDocument
52
+ ): ArchitectureConfigDocument => {
53
+ const frozenDependencies: Record<string, readonly string[]> = {};
54
+
55
+ for (const [key, value] of Object.entries(document.allowedDependencies)) {
56
+ frozenDependencies[key] = Object.freeze([...value]);
57
+ }
58
+
59
+ return Object.freeze({
60
+ preset: document.preset,
61
+ layers: Object.freeze([...document.layers]),
62
+ allowedDependencies: Object.freeze(frozenDependencies),
63
+ metadataTags: Object.freeze({ ...document.metadataTags }),
64
+ layerDetection: Object.freeze({ ...document.layerDetection }),
65
+ });
66
+ };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @layer domain
3
+ * @unit config-foundation
4
+ */
5
+
6
+ import type { ArchitecturePresetId } from './architecture-config.js';
7
+
8
+ export interface ArchitecturePresetDefinition {
9
+ readonly layers: readonly string[];
10
+ readonly allowedDependencies: Readonly<Record<string, readonly string[]>>;
11
+ }
12
+
13
+ const freeze = (definition: ArchitecturePresetDefinition): ArchitecturePresetDefinition => {
14
+ const dependencies: Record<string, readonly string[]> = {};
15
+
16
+ for (const [key, value] of Object.entries(definition.allowedDependencies)) {
17
+ dependencies[key] = Object.freeze([...value]);
18
+ }
19
+
20
+ return Object.freeze({
21
+ layers: Object.freeze([...definition.layers]),
22
+ allowedDependencies: Object.freeze(dependencies),
23
+ });
24
+ };
25
+
26
+ export const ARCHITECTURE_PRESET_CATALOG: Readonly<
27
+ Record<Exclude<ArchitecturePresetId, 'custom'>, ArchitecturePresetDefinition>
28
+ > = Object.freeze({
29
+ clean: freeze({
30
+ layers: ['domain', 'application', 'infrastructure', 'presentation'],
31
+ allowedDependencies: {
32
+ domain: ['domain'],
33
+ application: ['application', 'domain'],
34
+ infrastructure: ['infrastructure', 'application', 'domain'],
35
+ presentation: ['presentation', 'application', 'domain'],
36
+ },
37
+ }),
38
+ 'strict-ddd': freeze({
39
+ layers: ['domain', 'application', 'infrastructure', 'presentation'],
40
+ allowedDependencies: {
41
+ domain: ['domain'],
42
+ application: ['application', 'domain'],
43
+ infrastructure: ['infrastructure', 'application', 'domain'],
44
+ presentation: ['presentation', 'application'],
45
+ },
46
+ }),
47
+ onion: freeze({
48
+ layers: ['domain', 'application', 'interface'],
49
+ allowedDependencies: {
50
+ domain: ['domain'],
51
+ application: ['application', 'domain'],
52
+ interface: ['interface', 'application', 'domain'],
53
+ },
54
+ }),
55
+ hexagonal: freeze({
56
+ layers: ['core', 'ports', 'adapters'],
57
+ allowedDependencies: {
58
+ core: ['core'],
59
+ ports: ['ports', 'core'],
60
+ adapters: ['adapters', 'ports', 'core'],
61
+ },
62
+ }),
63
+ layered: freeze({
64
+ layers: ['controller', 'service', 'repository'],
65
+ allowedDependencies: {
66
+ controller: ['controller', 'service'],
67
+ service: ['service', 'repository'],
68
+ repository: ['repository'],
69
+ },
70
+ }),
71
+ flat: freeze({
72
+ layers: [],
73
+ allowedDependencies: {},
74
+ }),
75
+ });