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.
- package/CHANGELOG.md +255 -0
- package/README.ja.md +17 -7
- package/README.md +26 -4
- package/docs/ADR/ADR-015-architecture-preset.md +183 -0
- package/docs/guide/cli-reference.md +108 -0
- package/docs/guide/codex-integration.md +7 -2
- package/docs/guide/installation.md +10 -2
- package/docs/guide/preset-selection.md +170 -0
- package/docs/guide/quick-vs-full-mode.md +3 -3
- package/docs/guide/retrofit-adoption.md +19 -2
- package/docs/guide/skills-overview.md +1 -1
- package/package.json +7 -1
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +108 -100
- package/scripts/harness/agent-integration/domain/ports/phase-gate-query-port.ts +3 -3
- package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +26 -30
- package/scripts/harness/agent-integration/infrastructure/adapters/file-system-story-reflection-query-adapter.ts +6 -2
- package/scripts/harness/biome-ast-engine/application/dto/analyze-import-graph-input.ts +3 -0
- package/scripts/harness/biome-ast-engine/application/dto/resolve-enabled-rules-output.ts +2 -0
- package/scripts/harness/biome-ast-engine/application/mappers/resolve-enabled-rules-output-mapper.ts +4 -1
- package/scripts/harness/biome-ast-engine/application/usecases/analyze-import-graph-usecase.ts +4 -1
- package/scripts/harness/biome-ast-engine/application/usecases/execute-lint-usecase.ts +2 -0
- package/scripts/harness/biome-ast-engine/application/usecases/resolve-enabled-rules-usecase.ts +31 -3
- package/scripts/harness/biome-ast-engine/composition-root.ts +10 -2
- package/scripts/harness/biome-ast-engine/domain/ports/rule-config-provider-port.ts +16 -0
- package/scripts/harness/biome-ast-engine/domain/ports/source-module-analyzer-port.ts +5 -1
- package/scripts/harness/biome-ast-engine/domain/services/lint-runner.ts +5 -1
- package/scripts/harness/biome-ast-engine/domain/value-objects/architecture-spec.ts +38 -0
- package/scripts/harness/biome-ast-engine/domain/value-objects/layer-boundary.ts +7 -8
- package/scripts/harness/biome-ast-engine/domain/value-objects/layer-name.ts +15 -23
- package/scripts/harness/biome-ast-engine/domain/value-objects/source-module-snapshot.ts +11 -4
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/harness-config-provider-adapter.ts +29 -3
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +22 -15
- package/scripts/harness/biome-ast-engine/infrastructure/mappers/source-module-snapshot-mapper.ts +26 -17
- package/scripts/harness/config-foundation/application/dto/resolved-config-output.ts +1 -0
- package/scripts/harness/config-foundation/application/usecases/load-resolved-config-use-case.ts +34 -2
- package/scripts/harness/config-foundation/application/usecases/migrate-schema-use-case.ts +89 -0
- package/scripts/harness/config-foundation/composition-root.ts +8 -0
- package/scripts/harness/config-foundation/domain/harness-config.ts +6 -0
- package/scripts/harness/config-foundation/domain/services/architecture-resolution-service.ts +257 -0
- package/scripts/harness/config-foundation/domain/value-objects/architecture-config.ts +66 -0
- package/scripts/harness/config-foundation/domain/value-objects/architecture-preset-catalog.ts +75 -0
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +546 -0
- package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts +18 -6
- package/scripts/harness/config-foundation/presentation/cli/migrate-schema-command-handler.ts +83 -0
- package/scripts/harness/integrations/pre-commit.ts +211 -52
- package/scripts/harness/main.ts +460 -340
- package/scripts/harness/phase-dependency-model/domain/ports/story-reflection-file-system-port.ts +2 -4
- package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +54 -17
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +154 -36
- package/scripts/harness/setup/skill-deployer.ts +140 -102
- package/scripts/harness/skill-quality/domain/errors/skill-quality-error.ts +24 -23
- package/scripts/harness/skill-quality/domain/value-objects/commit-message.ts +23 -7
- package/scripts/harness/traceability-model/application/usecases/apply-work-item-migration-usecase.ts +52 -0
- package/scripts/harness/traceability-model/application/usecases/plan-work-item-migration-usecase.ts +29 -0
- package/scripts/harness/traceability-model/application/usecases/validate-design-story-annotations-usecase.ts +83 -18
- package/scripts/harness/traceability-model/composition-root.ts +48 -30
- package/scripts/harness/traceability-model/domain/ports/design-document-port.ts +9 -15
- package/scripts/harness/traceability-model/domain/ports/work-item-migration-apply-port.ts +11 -0
- package/scripts/harness/traceability-model/domain/ports/work-item-migration-source-port.ts +9 -0
- package/scripts/harness/traceability-model/domain/services/work-item-migration-planner.ts +162 -0
- package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +57 -0
- package/scripts/harness/traceability-model/domain/value-objects/work-item-migration-candidate.ts +47 -0
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-migration-apply-gateway.ts +110 -0
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-migration-source-gateway.ts +182 -0
- package/scripts/harness/traceability-model/infrastructure/gateways/markdown-design-document-gateway.ts +29 -43
- package/scripts/harness/traceability-model/infrastructure/parsers/work-item-frontmatter-parser.ts +136 -0
- package/scripts/harness/traceability-model/presentation/cli/migrate-work-items-command-handler.ts +186 -0
- package/skills/quick-implementor/SKILL.md +17 -1
- package/templates/.husky/commit-msg +1 -0
package/scripts/harness/config-foundation/application/usecases/load-resolved-config-use-case.ts
CHANGED
|
@@ -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
|
|
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
|
+
});
|