phasegate 0.254.0 → 0.283.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 (82) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.ja.md +6 -3
  3. package/README.md +6 -3
  4. package/docs/contracts/attestation-v2.schema.json +110 -0
  5. package/docs/guide/cli-reference.md +12 -1
  6. package/docs/guide/configuration.md +56 -0
  7. package/docs/guide/installation.md +1 -1
  8. package/docs/templates/ci/aidlc-gate.yml +60 -2
  9. package/package.json +2 -2
  10. package/scripts/harness/agent-integration/application/dto/open-world-obligations-context-dto.ts +30 -0
  11. package/scripts/harness/agent-integration/application/ports/world-obligations-query-port.ts +30 -0
  12. package/scripts/harness/agent-integration/application/usecases/get-open-world-obligations-context-usecase.ts +66 -0
  13. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +30 -27
  14. package/scripts/harness/agent-integration/infrastructure/adapters/world-model-open-obligations-query-adapter.ts +58 -0
  15. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
  16. package/scripts/harness/agent-integration/presentation/session-start-hook.ts +37 -1
  17. package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
  18. package/scripts/harness/agent-integration/presentation/world-obligations-session-context.ts +60 -0
  19. package/scripts/harness/attestation/application/dto/attestation-document.ts +17 -4
  20. package/scripts/harness/attestation/application/mappers/attestation-record-mapper.ts +55 -4
  21. package/scripts/harness/attestation/application/ports/world-snapshot-root-provider.ts +10 -0
  22. package/scripts/harness/attestation/application/usecases/produce-attestation-usecase.ts +33 -7
  23. package/scripts/harness/attestation/composition-root.ts +5 -0
  24. package/scripts/harness/attestation/domain/entities/attestation-record.ts +37 -2
  25. package/scripts/harness/attestation/index.ts +7 -1
  26. package/scripts/harness/attestation/presentation/handlers/attest-handler.ts +5 -2
  27. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +40 -9
  28. package/scripts/harness/config-foundation/application/mappers/world-model-config-mapper.ts +15 -0
  29. package/scripts/harness/config-foundation/domain/harness-config.ts +70 -87
  30. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +80 -88
  31. package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
  32. package/scripts/harness/config-foundation/domain/value-objects/world-config.ts +198 -0
  33. package/scripts/harness/config-foundation/index.ts +14 -15
  34. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +24 -0
  35. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +24 -0
  36. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +24 -0
  37. package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +27 -18
  38. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +84 -8
  39. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +84 -8
  40. package/scripts/harness/harness-api/domain/ports/config-query-port.ts +11 -1
  41. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +125 -86
  42. package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +31 -21
  43. package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +18 -4
  44. package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +23 -8
  45. package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +4 -4
  46. package/scripts/harness/harness-api/infrastructure/adapters/harness-config-query-adapter.ts +79 -34
  47. package/scripts/harness/installation/application/usecases/run-install.ts +199 -51
  48. package/scripts/harness/installation/application/usecases/run-reconcile.ts +277 -69
  49. package/scripts/harness/installation/domain/deployment-manifest.ts +43 -0
  50. package/scripts/harness/integrations/pre-commit.ts +169 -27
  51. package/scripts/harness/main.ts +200 -136
  52. package/scripts/harness/traceability-model/application/dto/changed-design-fragment-dto.ts +31 -0
  53. package/scripts/harness/traceability-model/application/facades/design-change-read-facade.ts +14 -0
  54. package/scripts/harness/traceability-model/application/ports/staged-design-change-source-port.ts +9 -0
  55. package/scripts/harness/traceability-model/composition-root.ts +5 -0
  56. package/scripts/harness/traceability-model/index.ts +9 -0
  57. package/scripts/harness/traceability-model/infrastructure/adapters/git-staged-design-change-adapter.ts +155 -0
  58. package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +31 -0
  59. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +57 -6
  60. package/scripts/harness/validator-system/composition-root.ts +25 -7
  61. package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
  62. package/scripts/harness/validator-system/domain/ports/world-constraint-admission-policy-port.ts +28 -0
  63. package/scripts/harness/validator-system/domain/ports/world-constraint-rederivation-policy-port.ts +11 -0
  64. package/scripts/harness/validator-system/domain/services/design-change-declaration-policy.ts +75 -0
  65. package/scripts/harness/validator-system/domain/services/world-constraint-admission-service.ts +78 -0
  66. package/scripts/harness/validator-system/domain/services/world-constraint-rederivation-service.ts +76 -0
  67. package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +4 -0
  68. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +126 -18
  69. package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +49 -20
  70. package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-admission-adapter.ts +56 -0
  71. package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-rederivation-adapter.ts +56 -0
  72. package/scripts/harness/world-model/application/dto/pinned-design-endpoint-dto.ts +19 -0
  73. package/scripts/harness/world-model/application/dto/world-resolved-config-input.ts +92 -27
  74. package/scripts/harness/world-model/application/dto/world-snapshot-root-dto.ts +8 -0
  75. package/scripts/harness/world-model/application/facades/pinned-design-endpoint-facade.ts +63 -0
  76. package/scripts/harness/world-model/application/facades/world-snapshot-root-facade.ts +19 -0
  77. package/scripts/harness/world-model/application/usecases/derive-world-obligations-use-case.ts +3 -1
  78. package/scripts/harness/world-model/composition-root.ts +62 -33
  79. package/scripts/harness/world-model/index.ts +11 -0
  80. package/scripts/harness/world-model/infrastructure/adapters/attestation-fact-extractor.ts +48 -13
  81. package/scripts/harness/world-model/infrastructure/adapters/file-system-world-control-repository-adapters.ts +6 -4
  82. package/scripts/harness/world-model/presentation/cli/world-derive-command-handler.ts +6 -2
@@ -4,36 +4,37 @@
4
4
  * @work-item-id WI-012
5
5
  * @work-item-id WI-212
6
6
  * @work-item-id WI-219
7
+ * @work-item-id WI-300
8
+ * @work-item-id WI-327
7
9
  */
8
- import { ConfigFoundationDomainError } from './errors/config-foundation-domain-error.js';
9
- import { ConfigValidationError } from './errors/config-validation-error.js';
10
- import { UnsupportedFeatureError } from './errors/unsupported-feature-error.js';
11
- import { LayersConfig } from './value-objects/layers-config.js';
12
- import { PathsConfig } from './value-objects/paths-config.js';
13
- import { PhaseDependenciesConfig } from './value-objects/phase-dependencies-config.js';
14
- import type { PhaseDependenciesPresetId } from './value-objects/phase-dependencies-config.js';
15
- import { PlanningModeConfig } from './value-objects/planning-mode-config.js';
16
- import { ProjectConfig } from './value-objects/project-config.js';
17
- import { QuickModeConfig } from './value-objects/quick-mode-config.js';
18
- import { ReportingConfig } from './value-objects/reporting-config.js';
19
- import { ValidateConfig } from './value-objects/validate-config.js';
20
- import { HarnessesConfig } from './value-objects/harnesses-config.js';
21
- import type { FeatureNameValue } from './value-objects/feature-name.js';
22
- import type { FeatureName } from './value-objects/feature-name.js';
23
- import type { L1Config } from './value-objects/l1-config.js';
24
- import type { L2Config } from './value-objects/l2-config.js';
25
- import type { L3Config } from './value-objects/l3-config.js';
26
- import type { L4Config } from './value-objects/l4-config.js';
27
- import type {
28
- ArchitectureConfigDocument,
29
- ArchitectureConfigSource,
30
- } from './value-objects/architecture-config.js';
31
-
32
- export type LayerId = 'L1' | 'L2' | 'L3' | 'L4';
33
- export type PresetId = 'minimal' | 'standard' | 'strict';
34
- export type PlanningModeValue = 'interactive' | 'embedded-qa' | 'manual';
35
- export type ModelDelegationPolicy = 'delegate-sonnet' | 'none';
10
+ import { ConfigFoundationDomainError } from "./errors/config-foundation-domain-error.js";
11
+ import { ConfigValidationError } from "./errors/config-validation-error.js";
12
+ import { UnsupportedFeatureError } from "./errors/unsupported-feature-error.js";
13
+ import type { ArchitectureConfigDocument, ArchitectureConfigSource } from "./value-objects/architecture-config.js";
14
+ import type { FeatureName, FeatureNameValue } from "./value-objects/feature-name.js";
15
+ import { HarnessesConfig } from "./value-objects/harnesses-config.js";
16
+ import type { L1Config } from "./value-objects/l1-config.js";
17
+ import type { L2Config } from "./value-objects/l2-config.js";
18
+ import type { L3Config } from "./value-objects/l3-config.js";
19
+ import type { L4Config } from "./value-objects/l4-config.js";
20
+ import { LayersConfig } from "./value-objects/layers-config.js";
21
+ import { PathsConfig } from "./value-objects/paths-config.js";
22
+ import type { PhaseDependenciesPresetId } from "./value-objects/phase-dependencies-config.js";
23
+ import { PhaseDependenciesConfig } from "./value-objects/phase-dependencies-config.js";
24
+ import { PlanningModeConfig } from "./value-objects/planning-mode-config.js";
25
+ import { ProjectConfig } from "./value-objects/project-config.js";
26
+ import { QuickModeConfig } from "./value-objects/quick-mode-config.js";
27
+ import { ReportingConfig } from "./value-objects/reporting-config.js";
28
+ import { ValidateConfig } from "./value-objects/validate-config.js";
29
+ import type { WorldConfigDocument, WorldConfigSourceDocument } from "./value-objects/world-config.js";
30
+ import { WORLD_CONFIG_DEFAULTS, WorldConfig } from "./value-objects/world-config.js";
31
+
32
+ export type LayerId = "L1" | "L2" | "L3" | "L4";
33
+ export type PresetId = "minimal" | "standard" | "strict";
34
+ export type PlanningModeValue = "interactive" | "embedded-qa" | "manual";
35
+ export type ModelDelegationPolicy = "delegate-sonnet" | "none";
36
36
  export type { PhaseDependenciesPresetId };
37
+
37
38
  type DeepPartial<T> = {
38
39
  [K in keyof T]?: T[K] extends Array<infer TItem>
39
40
  ? Array<DeepPartial<TItem>>
@@ -48,19 +49,22 @@ export interface HarnessConfigSourceDocument {
48
49
  preset: PresetId;
49
50
  languages?: string[];
50
51
  };
51
- layers: DeepPartial<HarnessConfigResolvedDocument['layers']>;
52
- quickMode: Partial<HarnessConfigResolvedDocument['quickMode']>;
53
- phaseDependencies: HarnessConfigResolvedDocument['phaseDependencies'];
54
- planningMode: HarnessConfigResolvedDocument['planningMode'];
55
- harnesses: Partial<HarnessConfigResolvedDocument['harnesses']>;
56
- paths: HarnessConfigResolvedDocument['paths'];
57
- reporting: HarnessConfigResolvedDocument['reporting'];
58
- ci?: HarnessConfigResolvedDocument['ci'];
59
- validate?: HarnessConfigResolvedDocument['validate'];
60
- preCommit?: Partial<HarnessConfigResolvedDocument['preCommit']>;
61
- modelRouting?: HarnessConfigResolvedDocument['modelRouting'];
62
- phase2Extensions?: HarnessConfigResolvedDocument['phase2Extensions'];
52
+ // WI-327: project 以外の top-level セクションは手書き config で省略可。
53
+ // 省略時は PresetResolutionService が防御プリセット(project.preset)のデフォルト値で補完する。
54
+ layers?: DeepPartial<HarnessConfigResolvedDocument["layers"]>;
55
+ quickMode?: Partial<HarnessConfigResolvedDocument["quickMode"]>;
56
+ phaseDependencies?: HarnessConfigResolvedDocument["phaseDependencies"];
57
+ planningMode?: HarnessConfigResolvedDocument["planningMode"];
58
+ harnesses?: Partial<HarnessConfigResolvedDocument["harnesses"]>;
59
+ paths?: HarnessConfigResolvedDocument["paths"];
60
+ reporting?: HarnessConfigResolvedDocument["reporting"];
61
+ ci?: HarnessConfigResolvedDocument["ci"];
62
+ validate?: HarnessConfigResolvedDocument["validate"];
63
+ preCommit?: Partial<HarnessConfigResolvedDocument["preCommit"]>;
64
+ modelRouting?: HarnessConfigResolvedDocument["modelRouting"];
65
+ phase2Extensions?: HarnessConfigResolvedDocument["phase2Extensions"];
63
66
  architecture?: ArchitectureConfigSource;
67
+ world?: WorldConfigSourceDocument;
64
68
  }
65
69
 
66
70
  export interface HarnessConfigResolvedDocument {
@@ -142,11 +146,12 @@ export interface HarnessConfigResolvedDocument {
142
146
  documentPattern: string;
143
147
  daysThreshold: number;
144
148
  commitCountThreshold: number;
145
- evaluationMode: 'or' | 'and';
149
+ evaluationMode: "or" | "and";
146
150
  enabled?: boolean;
147
151
  }>;
148
152
  };
149
153
  architecture?: ArchitectureConfigDocument;
154
+ world?: WorldConfigDocument;
150
155
  }
151
156
 
152
157
  export type HarnessConfigV2 = HarnessConfigResolvedDocument;
@@ -159,7 +164,7 @@ export interface HarnessConfigReconstitutionProps {
159
164
  }
160
165
 
161
166
  export interface FeatureToggled {
162
- type: 'FeatureToggled';
167
+ type: "FeatureToggled";
163
168
  occurredAt: Date;
164
169
  projectName: string;
165
170
  featureName: FeatureNameValue | string;
@@ -168,7 +173,7 @@ export interface FeatureToggled {
168
173
  }
169
174
 
170
175
  export interface PresetApplied {
171
- type: 'PresetApplied';
176
+ type: "PresetApplied";
172
177
  occurredAt: Date;
173
178
  projectName: string;
174
179
  preset: PresetId;
@@ -178,23 +183,23 @@ export interface PresetApplied {
178
183
  export type DomainEvent = FeatureToggled | PresetApplied;
179
184
 
180
185
  const SUPPORTED_FEATURE_NAMES = [
181
- 'agentLessonCollection',
182
- 'cascadeUpdate',
183
- 'bundleSizeLimit',
184
- 'deadCodeGC',
186
+ "agentLessonCollection",
187
+ "cascadeUpdate",
188
+ "bundleSizeLimit",
189
+ "deadCodeGC",
185
190
  ] as const satisfies readonly FeatureNameValue[];
186
191
 
187
192
  export class UnknownLayerError extends ConfigFoundationDomainError {
188
193
  constructor(layerId: string) {
189
- super(`未知のレイヤーです: ${layerId} [L1-006]`, 'L1-006');
190
- this.name = 'UnknownLayerError';
194
+ super(`未知のレイヤーです: ${layerId} [L1-006]`, "L1-006");
195
+ this.name = "UnknownLayerError";
191
196
  }
192
197
  }
193
198
 
194
199
  export class FeatureActivationRuleError extends ConfigFoundationDomainError {
195
200
  constructor(message: string) {
196
- super(`${message} [L1-005]`, 'L1-005');
197
- this.name = 'FeatureActivationRuleError';
201
+ super(`${message} [L1-005]`, "L1-005");
202
+ this.name = "FeatureActivationRuleError";
198
203
  }
199
204
  }
200
205
 
@@ -207,11 +212,8 @@ function deepClone<T>(value: T): T {
207
212
  return value.map((item) => deepClone(item)) as T;
208
213
  }
209
214
 
210
- if (value !== null && typeof value === 'object') {
211
- const clonedEntries = Object.entries(value).map(([key, entryValue]) => [
212
- key,
213
- deepClone(entryValue),
214
- ]);
215
+ if (value !== null && typeof value === "object") {
216
+ const clonedEntries = Object.entries(value).map(([key, entryValue]) => [key, deepClone(entryValue)]);
215
217
 
216
218
  return Object.fromEntries(clonedEntries) as T;
217
219
  }
@@ -226,7 +228,7 @@ function createFeatureToggledEvent(
226
228
  currentState: boolean,
227
229
  ): FeatureToggled {
228
230
  return Object.freeze({
229
- type: 'FeatureToggled',
231
+ type: "FeatureToggled",
230
232
  occurredAt: new Date(),
231
233
  projectName,
232
234
  featureName,
@@ -240,14 +242,14 @@ function assertSupportedFeature(name: FeatureName): FeatureNameValue {
240
242
 
241
243
  if (!SUPPORTED_FEATURE_NAMES.includes(raw as FeatureNameValue)) {
242
244
  throw new UnsupportedFeatureError(
243
- `機能名 "${raw}" は利用できません。利用可能: ${SUPPORTED_FEATURE_NAMES.join(', ')}`,
245
+ `機能名 "${raw}" は利用できません。利用可能: ${SUPPORTED_FEATURE_NAMES.join(", ")}`,
244
246
  );
245
247
  }
246
248
 
247
249
  return raw as FeatureNameValue;
248
250
  }
249
251
 
250
- function toHarnessesDocument(harnesses: HarnessesConfig): HarnessConfigResolvedDocument['harnesses'] {
252
+ function toHarnessesDocument(harnesses: HarnessesConfig): HarnessConfigResolvedDocument["harnesses"] {
251
253
  return {
252
254
  agentLessonCollection: harnesses.agentLessonCollection,
253
255
  cascadeUpdate: harnesses.cascadeUpdate,
@@ -303,22 +305,17 @@ export class HarnessConfig {
303
305
  const project = ProjectConfig.create(props.resolvedDocument.project);
304
306
  const layers = LayersConfig.create(props.resolvedDocument.layers);
305
307
  const quickMode = QuickModeConfig.create(props.resolvedDocument.quickMode);
306
- const phaseDependencies = PhaseDependenciesConfig.create(
307
- props.resolvedDocument.phaseDependencies,
308
- );
309
- const planningMode = PlanningModeConfig.create(
310
- props.resolvedDocument.planningMode,
311
- );
308
+ const phaseDependencies = PhaseDependenciesConfig.create(props.resolvedDocument.phaseDependencies);
309
+ const planningMode = PlanningModeConfig.create(props.resolvedDocument.planningMode);
312
310
  const harnesses = HarnessesConfig.create(props.resolvedDocument.harnesses);
313
311
  const paths = PathsConfig.create(props.resolvedDocument.paths);
314
312
  const reporting = ReportingConfig.create(props.resolvedDocument.reporting);
315
313
  const validate = ValidateConfig.create(props.resolvedDocument.validate);
314
+ const world = WorldConfig.create(props.resolvedDocument.world ?? WORLD_CONFIG_DEFAULTS).toDocument();
316
315
 
317
- if (
318
- props.sourceDocument.project.preset !== props.resolvedDocument.project.preset
319
- ) {
316
+ if (props.sourceDocument.project.preset !== props.resolvedDocument.project.preset) {
320
317
  throw new ConfigValidationError(
321
- 'sourceDocument.project.preset と resolvedDocument.project.preset は一致しなければなりません',
318
+ "sourceDocument.project.preset と resolvedDocument.project.preset は一致しなければなりません",
322
319
  );
323
320
  }
324
321
 
@@ -333,7 +330,7 @@ export class HarnessConfig {
333
330
  reporting,
334
331
  validate,
335
332
  sourceDocument: props.sourceDocument,
336
- resolvedDocument: props.resolvedDocument,
333
+ resolvedDocument: { ...props.resolvedDocument, world },
337
334
  pendingEvents: props.pendingEvents ?? [],
338
335
  });
339
336
  }
@@ -355,14 +352,7 @@ export class HarnessConfig {
355
352
  ...this.resolvedDocument,
356
353
  harnesses: toHarnessesDocument(nextHarnesses),
357
354
  };
358
- this.pendingEvents.push(
359
- createFeatureToggledEvent(
360
- this.project.name,
361
- featureName,
362
- previousState,
363
- true,
364
- ),
365
- );
355
+ this.pendingEvents.push(createFeatureToggledEvent(this.project.name, featureName, previousState, true));
366
356
  }
367
357
 
368
358
  disableFeature(name: FeatureName): void {
@@ -382,18 +372,11 @@ export class HarnessConfig {
382
372
  ...this.resolvedDocument,
383
373
  harnesses: toHarnessesDocument(nextHarnesses),
384
374
  };
385
- this.pendingEvents.push(
386
- createFeatureToggledEvent(
387
- this.project.name,
388
- featureName,
389
- previousState,
390
- false,
391
- ),
392
- );
375
+ this.pendingEvents.push(createFeatureToggledEvent(this.project.name, featureName, previousState, false));
393
376
  }
394
377
 
395
378
  getLayerConfig(layerId: LayerId): L1Config | L2Config | L3Config | L4Config {
396
- if (!['L1', 'L2', 'L3', 'L4'].includes(layerId)) {
379
+ if (!["L1", "L2", "L3", "L4"].includes(layerId)) {
397
380
  throw new UnknownLayerError(layerId);
398
381
  }
399
382
 
@@ -2,43 +2,44 @@
2
2
  * @layer domain
3
3
  * @unit config-foundation
4
4
  * @work-item-id WI-212
5
+ * @work-item-id WI-300
6
+ * @work-item-id WI-320
5
7
  */
6
- import { ConfigFoundationDomainError } from '../errors/config-foundation-domain-error.js';
7
- import type {
8
- HarnessConfigResolvedDocument,
9
- HarnessConfigSourceDocument,
10
- } from '../harness-config.js';
11
- import { HarnessesConfig } from '../value-objects/harnesses-config.js';
12
- import type { FeatureToggle } from '../value-objects/feature-toggle.js';
8
+ import { ConfigFoundationDomainError } from "../errors/config-foundation-domain-error.js";
9
+ import type { HarnessConfigResolvedDocument, HarnessConfigSourceDocument } from "../harness-config.js";
10
+ import type { FeatureToggle } from "../value-objects/feature-toggle.js";
11
+ import { HarnessesConfig } from "../value-objects/harnesses-config.js";
12
+ import { WORLD_CONFIG_DEFAULTS, WorldConfig, type WorldConfigDocument } from "../value-objects/world-config.js";
13
13
 
14
14
  export interface PresetDefinition {
15
- layers: HarnessConfigResolvedDocument['layers'];
16
- quickMode: HarnessConfigResolvedDocument['quickMode'];
17
- phaseDependencies: HarnessConfigResolvedDocument['phaseDependencies'];
18
- planningMode: HarnessConfigResolvedDocument['planningMode'];
19
- harnesses: HarnessConfigResolvedDocument['harnesses'];
20
- paths: HarnessConfigResolvedDocument['paths'];
21
- reporting: HarnessConfigResolvedDocument['reporting'];
22
- validate: HarnessConfigResolvedDocument['validate'];
23
- preCommit?: HarnessConfigResolvedDocument['preCommit'];
15
+ layers: HarnessConfigResolvedDocument["layers"];
16
+ quickMode: HarnessConfigResolvedDocument["quickMode"];
17
+ phaseDependencies: HarnessConfigResolvedDocument["phaseDependencies"];
18
+ planningMode: HarnessConfigResolvedDocument["planningMode"];
19
+ harnesses: HarnessConfigResolvedDocument["harnesses"];
20
+ paths: HarnessConfigResolvedDocument["paths"];
21
+ reporting: HarnessConfigResolvedDocument["reporting"];
22
+ validate: HarnessConfigResolvedDocument["validate"];
23
+ preCommit?: HarnessConfigResolvedDocument["preCommit"];
24
+ world?: WorldConfigDocument;
24
25
  }
25
26
 
26
27
  export class InvalidPresetDefinitionError extends ConfigFoundationDomainError {
27
28
  constructor(message: string) {
28
- super(`${message} [L1-007]`, 'L1-007');
29
- this.name = 'InvalidPresetDefinitionError';
29
+ super(`${message} [L1-007]`, "L1-007");
30
+ this.name = "InvalidPresetDefinitionError";
30
31
  }
31
32
  }
32
33
 
33
34
  export class ConfigMergeError extends ConfigFoundationDomainError {
34
35
  constructor(message: string) {
35
- super(`${message} [L1-008]`, 'L1-008');
36
- this.name = 'ConfigMergeError';
36
+ super(`${message} [L1-008]`, "L1-008");
37
+ this.name = "ConfigMergeError";
37
38
  }
38
39
  }
39
40
 
40
41
  function isPlainObject(value: unknown): value is Record<string, unknown> {
41
- return value !== null && typeof value === 'object' && !Array.isArray(value);
42
+ return value !== null && typeof value === "object" && !Array.isArray(value);
42
43
  }
43
44
 
44
45
  function deepClone<T>(value: T): T {
@@ -47,9 +48,7 @@ function deepClone<T>(value: T): T {
47
48
  }
48
49
 
49
50
  if (isPlainObject(value)) {
50
- return Object.fromEntries(
51
- Object.entries(value).map(([key, entryValue]) => [key, deepClone(entryValue)]),
52
- ) as T;
51
+ return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, deepClone(entryValue)])) as T;
53
52
  }
54
53
 
55
54
  return value;
@@ -74,10 +73,7 @@ function deepMerge<T>(baseValue: T, overrideValue: unknown, path: string): T {
74
73
  }
75
74
 
76
75
  const merged: Record<string, unknown> = {};
77
- const keys = new Set([
78
- ...Object.keys(baseValue),
79
- ...Object.keys(overrideValue),
80
- ]);
76
+ const keys = new Set([...Object.keys(baseValue), ...Object.keys(overrideValue)]);
81
77
 
82
78
  for (const key of keys) {
83
79
  const nestedBaseValue = (baseValue as Record<string, unknown>)[key];
@@ -88,11 +84,7 @@ function deepMerge<T>(baseValue: T, overrideValue: unknown, path: string): T {
88
84
  continue;
89
85
  }
90
86
 
91
- merged[key] = deepMerge(
92
- nestedBaseValue,
93
- nestedOverrideValue,
94
- `${path}.${key}`,
95
- );
87
+ merged[key] = deepMerge(nestedBaseValue, nestedOverrideValue, `${path}.${key}`);
96
88
  }
97
89
 
98
90
  return merged as T;
@@ -105,35 +97,26 @@ function deepMerge<T>(baseValue: T, overrideValue: unknown, path: string): T {
105
97
  return deepClone(overrideValue as T);
106
98
  }
107
99
 
108
- function assertValidPresetDefinition(
109
- presetDefinition: PresetDefinition,
110
- ): void {
100
+ function assertValidPresetDefinition(presetDefinition: PresetDefinition): void {
111
101
  const requiredKeys: Array<keyof PresetDefinition> = [
112
- 'layers',
113
- 'quickMode',
114
- 'phaseDependencies',
115
- 'planningMode',
116
- 'harnesses',
117
- 'paths',
118
- 'reporting',
119
- 'validate',
102
+ "layers",
103
+ "quickMode",
104
+ "phaseDependencies",
105
+ "planningMode",
106
+ "harnesses",
107
+ "paths",
108
+ "reporting",
109
+ "validate",
120
110
  ];
121
111
 
122
112
  for (const key of requiredKeys) {
123
- if (
124
- !(key in presetDefinition) ||
125
- (presetDefinition as unknown as Record<string, unknown>)[key] === undefined
126
- ) {
127
- throw new InvalidPresetDefinitionError(
128
- `presetDefinition.${key} は必須です`,
129
- );
113
+ if (!(key in presetDefinition) || (presetDefinition as unknown as Record<string, unknown>)[key] === undefined) {
114
+ throw new InvalidPresetDefinitionError(`presetDefinition.${key} は必須です`);
130
115
  }
131
116
  }
132
117
  }
133
118
 
134
- function toHarnessesDocument(
135
- harnesses: HarnessesConfig,
136
- ): HarnessConfigResolvedDocument['harnesses'] {
119
+ function toHarnessesDocument(harnesses: HarnessesConfig): HarnessConfigResolvedDocument["harnesses"] {
137
120
  return {
138
121
  agentLessonCollection: harnesses.agentLessonCollection,
139
122
  cascadeUpdate: harnesses.cascadeUpdate,
@@ -149,49 +132,60 @@ export class PresetResolutionService {
149
132
  ): HarnessConfigResolvedDocument {
150
133
  assertValidPresetDefinition(presetDefinition);
151
134
 
135
+ const layers = deepMerge(presetDefinition.layers, sourceDocument.layers, "layers");
136
+ const paths = deepMerge(presetDefinition.paths, sourceDocument.paths, "paths");
137
+ const world = deepMerge(presetDefinition.world ?? WORLD_CONFIG_DEFAULTS, sourceDocument.world, "world");
138
+ const explicitCorpus = sourceDocument.world?.corpus;
139
+ const explicitInputs = sourceDocument.world?.inputs;
140
+ const derivedProductRoot = paths.designDocs.endsWith("/construction")
141
+ ? paths.designDocs.slice(0, -"/construction".length)
142
+ : paths.designDocs;
143
+ const resolvedWorld: WorldConfigDocument = {
144
+ ...world,
145
+ corpus: {
146
+ ...world.corpus,
147
+ productRoots:
148
+ explicitCorpus?.productRoots === undefined && !world.corpus.productRoots.includes(derivedProductRoot)
149
+ ? [...world.corpus.productRoots, derivedProductRoot]
150
+ : world.corpus.productRoots,
151
+ inceptionRoots:
152
+ explicitCorpus?.inceptionRoots === undefined ? [paths.inceptionDocs] : world.corpus.inceptionRoots,
153
+ },
154
+ inputs: {
155
+ ...world.inputs,
156
+ matrixPath:
157
+ explicitInputs?.matrixPath === undefined
158
+ ? (layers.L3.requirementMatrixPath ?? world.inputs.matrixPath)
159
+ : world.inputs.matrixPath,
160
+ },
161
+ };
162
+
152
163
  return {
153
164
  project: {
154
165
  name: sourceDocument.project.name,
155
166
  preset: sourceDocument.project.preset,
156
- languages: sourceDocument.project.languages ?? ['typescript'],
167
+ // WI-320 (github#39): 未宣言時に ["typescript"] を注入しない。「未宣言」を resolved config まで
168
+ // 生存させ、validator-system adapter のファイルシステム言語検出(WI-319)を実 CLI 経路で有効にする。
169
+ ...(sourceDocument.project.languages === undefined ? {} : { languages: sourceDocument.project.languages }),
157
170
  },
158
- layers: deepMerge(presetDefinition.layers, sourceDocument.layers, 'layers'),
159
- quickMode: deepMerge(
160
- presetDefinition.quickMode,
161
- sourceDocument.quickMode,
162
- 'quickMode',
163
- ),
171
+ layers,
172
+ quickMode: deepMerge(presetDefinition.quickMode, sourceDocument.quickMode, "quickMode"),
164
173
  phaseDependencies: deepMerge(
165
174
  presetDefinition.phaseDependencies,
166
175
  sourceDocument.phaseDependencies,
167
- 'phaseDependencies',
168
- ),
169
- planningMode: deepMerge(
170
- presetDefinition.planningMode,
171
- sourceDocument.planningMode,
172
- 'planningMode',
173
- ),
174
- harnesses: deepMerge(
175
- presetDefinition.harnesses,
176
- sourceDocument.harnesses,
177
- 'harnesses',
178
- ),
179
- paths: deepMerge(presetDefinition.paths, sourceDocument.paths, 'paths'),
180
- reporting: deepMerge(
181
- presetDefinition.reporting,
182
- sourceDocument.reporting,
183
- 'reporting',
184
- ),
185
- validate: deepMerge(
186
- presetDefinition.validate,
187
- sourceDocument.validate,
188
- 'validate',
176
+ "phaseDependencies",
189
177
  ),
178
+ planningMode: deepMerge(presetDefinition.planningMode, sourceDocument.planningMode, "planningMode"),
179
+ harnesses: deepMerge(presetDefinition.harnesses, sourceDocument.harnesses, "harnesses"),
180
+ paths,
181
+ reporting: deepMerge(presetDefinition.reporting, sourceDocument.reporting, "reporting"),
182
+ validate: deepMerge(presetDefinition.validate, sourceDocument.validate, "validate"),
190
183
  preCommit: deepMerge(
191
- presetDefinition.preCommit ?? { implementationExtensions: ['.ts'] },
184
+ presetDefinition.preCommit ?? { implementationExtensions: [".ts"] },
192
185
  sourceDocument.preCommit,
193
- 'preCommit',
186
+ "preCommit",
194
187
  ),
188
+ world: WorldConfig.create(resolvedWorld).toDocument(),
195
189
  };
196
190
  }
197
191
 
@@ -200,9 +194,7 @@ export class PresetResolutionService {
200
194
  override: FeatureToggle,
201
195
  ): HarnessConfigResolvedDocument {
202
196
  const harnesses = HarnessesConfig.create(document.harnesses);
203
- const nextHarnesses = override.enabled
204
- ? harnesses.enable(override.name)
205
- : harnesses.disable(override.name);
197
+ const nextHarnesses = override.enabled ? harnesses.enable(override.name) : harnesses.disable(override.name);
206
198
 
207
199
  return {
208
200
  ...deepClone(document),
@@ -2,11 +2,12 @@
2
2
  * @layer domain
3
3
  * @unit config-foundation
4
4
  * @work-item-id WI-212
5
+ * @work-item-id WI-320
5
6
  *
6
7
  * ProjectConfig値オブジェクト - プロジェクト名とPresetを保持する
7
8
  */
8
- import { ConfigValidationError } from '../errors/config-validation-error.js';
9
- import { Preset } from './preset.js';
9
+ import { ConfigValidationError } from "../errors/config-validation-error.js";
10
+ import { Preset } from "./preset.js";
10
11
 
11
12
  interface ProjectConfigProps {
12
13
  readonly name: string;
@@ -17,29 +18,37 @@ interface ProjectConfigProps {
17
18
  export class ProjectConfig {
18
19
  readonly name: string;
19
20
  readonly preset: Preset;
20
- readonly languages: readonly string[];
21
+ /**
22
+ * WI-320 (github#39): 宣言があった場合のみ保持する(undefined = 未宣言)。
23
+ * default `['typescript']` の注入をやめ、「未宣言」シグナルを下流
24
+ * (validator-system のファイルシステム言語検出、WI-319)まで生存させる。
25
+ */
26
+ readonly languages?: readonly string[];
21
27
 
22
28
  constructor(props: ProjectConfigProps) {
23
- if (!props.name || props.name.trim() === '') {
24
- throw new ConfigValidationError('ProjectConfig: name must not be empty');
25
- }
26
- const sourceLanguages = props.languages ?? ['typescript'];
27
- if (sourceLanguages.length === 0) {
28
- throw new ConfigValidationError('ProjectConfig: languages must not be empty');
29
+ if (!props.name || props.name.trim() === "") {
30
+ throw new ConfigValidationError("ProjectConfig: name must not be empty");
29
31
  }
30
- const normalizedLanguages = sourceLanguages.map((language) => language.trim()).filter((language) => language.length > 0);
31
- if (normalizedLanguages.length !== sourceLanguages.length) {
32
- throw new ConfigValidationError('ProjectConfig: languages must contain non-empty strings');
32
+ if (props.languages !== undefined) {
33
+ if (props.languages.length === 0) {
34
+ throw new ConfigValidationError("ProjectConfig: languages must not be empty");
35
+ }
36
+ const normalizedLanguages = props.languages
37
+ .map((language) => language.trim())
38
+ .filter((language) => language.length > 0);
39
+ if (normalizedLanguages.length !== props.languages.length) {
40
+ throw new ConfigValidationError("ProjectConfig: languages must contain non-empty strings");
41
+ }
42
+ this.languages = Object.freeze([...normalizedLanguages]);
33
43
  }
34
44
  this.name = props.name;
35
45
  this.preset = props.preset;
36
- this.languages = Object.freeze([...normalizedLanguages]);
37
46
  Object.freeze(this);
38
47
  }
39
48
 
40
49
  static create(raw: { name: string; preset: string; languages?: readonly string[] }): ProjectConfig {
41
50
  const preset = Preset.create(raw.preset);
42
- return new ProjectConfig({ name: raw.name, preset, languages: raw.languages ?? ['typescript'] });
51
+ return new ProjectConfig({ name: raw.name, preset, languages: raw.languages });
43
52
  }
44
53
 
45
54
  rename(name: string): ProjectConfig {
@@ -51,9 +60,16 @@ export class ProjectConfig {
51
60
  }
52
61
 
53
62
  equals(other: ProjectConfig): boolean {
54
- return this.name === other.name
55
- && this.preset.equals(other.preset)
56
- && this.languages.length === other.languages.length
57
- && this.languages.every((language, index) => language === other.languages[index]);
63
+ return this.name === other.name && this.preset.equals(other.preset) && this.languagesEqual(other.languages);
64
+ }
65
+
66
+ private languagesEqual(otherLanguages: readonly string[] | undefined): boolean {
67
+ if (this.languages === undefined || otherLanguages === undefined) {
68
+ return this.languages === otherLanguages;
69
+ }
70
+ return (
71
+ this.languages.length === otherLanguages.length &&
72
+ this.languages.every((language, index) => language === otherLanguages[index])
73
+ );
58
74
  }
59
75
  }