phasegate 0.264.0 → 0.315.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 (92) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/docs/ADR/017-warning-severity-aggregation.md +17 -0
  3. package/docs/ADR/038-config-state-operation-permission-policy.md +78 -0
  4. package/docs/guide/installation.md +1 -1
  5. package/docs/guide/layer-model.md +2 -0
  6. package/docs/guide/quick-vs-full-mode.md +28 -3
  7. package/docs/guide/troubleshooting.md +37 -0
  8. package/docs/templates/agent-context/CLAUDE.md.template.md +6 -6
  9. package/docs/templates/ci/aidlc-gate.yml +22 -4
  10. package/package.json +2 -2
  11. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +98 -18
  12. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +25 -3
  13. package/scripts/harness/agent-integration/infrastructure/adapters/file-system-full-mode-session-query-adapter.ts +75 -23
  14. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +45 -27
  15. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
  16. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +51 -6
  17. package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
  18. package/scripts/harness/ci-governance/composition-root.ts +2 -2
  19. package/scripts/harness/ci-governance/domain/services/claude-md-composer.ts +20 -11
  20. package/scripts/harness/ci-governance/presentation/handlers/check-repetition-handler.ts +10 -2
  21. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +5 -1
  22. package/scripts/harness/config-foundation/domain/harness-config.ts +10 -7
  23. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +4 -1
  24. package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
  25. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
  26. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
  27. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -1
  28. package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +27 -18
  29. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +1 -8
  30. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +1 -8
  31. package/scripts/harness/harness-api/domain/ports/config-query-port.ts +11 -1
  32. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +135 -87
  33. package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +31 -21
  34. package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +2 -22
  35. package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +23 -8
  36. package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +4 -4
  37. package/scripts/harness/harness-api/infrastructure/adapters/harness-config-query-adapter.ts +79 -34
  38. package/scripts/harness/harness-error/application/dto/create-harness-error-input.ts +3 -1
  39. package/scripts/harness/harness-error/application/dto/harness-error-contract.ts +3 -1
  40. package/scripts/harness/harness-error/application/mappers/harness-error-contract-mapper.ts +9 -17
  41. package/scripts/harness/harness-error/application/usecases/create-harness-error-use-case.ts +7 -7
  42. package/scripts/harness/harness-error/domain/services/harness-error-factory.ts +30 -33
  43. package/scripts/harness/harness-error/domain/value-objects/error-definition.ts +28 -17
  44. package/scripts/harness/harness-error/domain/value-objects/harness-error.ts +30 -9
  45. package/scripts/harness/harness-error/domain/value-objects/remediation-type.ts +30 -0
  46. package/scripts/harness/harness-error/infrastructure/registry/l2-error-definitions.ts +44 -29
  47. package/scripts/harness/harness-error/infrastructure/registry/l3-error-definitions.ts +44 -27
  48. package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +47 -32
  49. package/scripts/harness/installation/application/checks/claude-context-missing-check.ts +13 -7
  50. package/scripts/harness/installation/application/checks/config-status-check.ts +52 -0
  51. package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +6 -0
  52. package/scripts/harness/installation/application/ports/config-status-probe-port.ts +9 -0
  53. package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +30 -8
  54. package/scripts/harness/installation/application/usecases/run-install.ts +234 -53
  55. package/scripts/harness/installation/application/usecases/run-reconcile.ts +311 -70
  56. package/scripts/harness/installation/composition-root.ts +13 -3
  57. package/scripts/harness/installation/domain/check-id.ts +2 -0
  58. package/scripts/harness/installation/domain/config-status.ts +17 -0
  59. package/scripts/harness/installation/domain/deployment-manifest.ts +43 -0
  60. package/scripts/harness/installation/domain/ports/heuristic-check.ts +10 -1
  61. package/scripts/harness/installation/infrastructure/adapters/config-status-probe-adapter.ts +79 -0
  62. package/scripts/harness/installation/presentation/cli/doctor-handler.ts +6 -1
  63. package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +19 -5
  64. package/scripts/harness/integrations/pre-commit.ts +17 -3
  65. package/scripts/harness/main.ts +83 -15
  66. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +60 -32
  67. package/scripts/harness/phase2-extensions/presentation/handlers/check-freshness-handler.ts +17 -9
  68. package/scripts/harness/quick-mode/application/ports/file-existence-port.ts +15 -0
  69. package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +60 -22
  70. package/scripts/harness/quick-mode/composition-root.ts +25 -15
  71. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +72 -3
  72. package/scripts/harness/quick-mode/infrastructure/adapters/fs-file-existence-adapter.ts +38 -0
  73. package/scripts/harness/skill-quality/infrastructure/adapters/file-system-requirement-test-matrix-adapter.ts +51 -8
  74. package/scripts/harness/skill-quality/presentation/handlers/check-coverage-handler.ts +13 -6
  75. package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +5 -1
  76. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-identity-gateway.ts +6 -1
  77. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +15 -2
  78. package/scripts/harness/validator-system/application/use-cases/aggregate-validation-results-usecase.ts +11 -14
  79. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +32 -7
  80. package/scripts/harness/validator-system/composition-root.ts +4 -1
  81. package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
  82. package/scripts/harness/validator-system/domain/services/effective-severity-policy.ts +39 -0
  83. package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +6 -4
  84. package/scripts/harness/validator-system/domain/value-objects/drift-report.ts +12 -7
  85. package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +11 -3
  86. package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +20 -17
  87. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +89 -3
  88. package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +49 -20
  89. package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +35 -14
  90. package/scripts/harness/validator-system/infrastructure/adapters/traceability-metadata-policy-adapter.ts +13 -8
  91. package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +19 -10
  92. package/skills/quick-implementor/SKILL.md +19 -0
@@ -126,11 +126,11 @@ export function buildCiGovernance(baseDir: string, harnessRoot: string = baseDir
126
126
 
127
127
  // Handlers
128
128
  const generateCiTemplateHandler = new GenerateCiTemplateHandler(generateCiTemplateUseCase, renderCiTemplateUseCase);
129
- const migrateAgentsMdHandler = new MigrateAgentsMdHandler(migrateAgentsMdUseCase);
129
+ const migrateAgentsMdHandler = new MigrateAgentsMdHandler(migrateAgentsMdUseCase, validatePointersUseCase);
130
130
  const refreshAgentContextHandler = new RefreshAgentContextHandler(refreshAgentContextUseCase);
131
131
  const refreshClaudeMdHandler = new RefreshClaudeMdHandler(refreshClaudeMdUseCase);
132
132
  const checkAgentContextHandler = new CheckAgentContextHandler(checkAgentContextUseCase);
133
- const checkRepetitionHandler = new CheckRepetitionHandler(checkEscalationUseCase);
133
+ const checkRepetitionHandler = new CheckRepetitionHandler(checkEscalationUseCase, resetRepetitionUseCase);
134
134
  const createBaselineHandler = new CreateBaselineHandler(createBaselineUseCase);
135
135
  const scaffoldDesignHandler = new ScaffoldDesignHandler(scaffoldDesignUseCase);
136
136
 
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit ci-governance
4
+ * @work-item-id WI-331
4
5
  */
5
6
 
6
7
  export interface ClaudeMdTemplateValues {
@@ -9,20 +10,28 @@ export interface ClaudeMdTemplateValues {
9
10
  readonly presets: readonly string[];
10
11
  }
11
12
 
12
- const USER_SECTION_START = '<!-- phasegate:user-section:start -->';
13
- const USER_SECTION_END = '<!-- phasegate:user-section:end -->';
14
- const DEFAULT_USER_SECTION = 'Project-specific agent instructions go here.';
13
+ const USER_SECTION_START = "<!-- phasegate:user-section:start -->";
14
+ const USER_SECTION_END = "<!-- phasegate:user-section:end -->";
15
+ const DEFAULT_USER_SECTION = "Project-specific agent instructions go here.";
15
16
 
16
17
  export class ClaudeMdComposer {
18
+ // Rebuilds CLAUDE.md from the current template, carrying over the existing
19
+ // user-section body. Because the whole file is re-rendered, a legacy file
20
+ // whose user-section still sits inside the managed block (pre-WI-331
21
+ // template shape) is migrated to the current outside-of-block structure.
17
22
  compose(template: string, existing: string | null, values: ClaudeMdTemplateValues): string {
18
23
  const userSection = this.extractUserSection(existing) ?? DEFAULT_USER_SECTION;
19
- return template
20
- .replace('{{PHASEGATE_COMMANDS}}', this.toList(values.commands))
21
- .replace('{{PHASEGATE_SKILLS}}', this.toList(values.skills))
22
- .replace('{{PHASEGATE_PRESETS}}', this.toList(values.presets))
23
- .replace('{{PHASEGATE_USER_SECTION}}', userSection)
24
- .replace(/\n{3,}/g, '\n\n')
25
- .trimEnd() + '\n';
24
+ const rendered = template
25
+ .replace("{{PHASEGATE_COMMANDS}}", this.toList(values.commands))
26
+ .replace("{{PHASEGATE_SKILLS}}", this.toList(values.skills))
27
+ .replace("{{PHASEGATE_PRESETS}}", this.toList(values.presets))
28
+ // Collapse blank-line runs before injecting the user body so
29
+ // user-authored spacing is preserved byte-for-byte.
30
+ .replace(/\n{3,}/g, "\n\n")
31
+ // Replacer function keeps `$`-sequences in the user body from being
32
+ // interpreted as String.replace substitution patterns.
33
+ .replace("{{PHASEGATE_USER_SECTION}}", () => userSection);
34
+ return `${rendered.trimEnd()}\n`;
26
35
  }
27
36
 
28
37
  private extractUserSection(existing: string | null): string | null {
@@ -34,6 +43,6 @@ export class ClaudeMdComposer {
34
43
  }
35
44
 
36
45
  private toList(values: readonly string[]): string {
37
- return values.map((value) => `- \`${value}\``).join('\n');
46
+ return values.map((value) => `- \`${value}\``).join("\n");
38
47
  }
39
48
  }
@@ -29,6 +29,13 @@ export class CheckRepetitionHandler {
29
29
  async handle(args: CheckRepetitionHandlerArgs): Promise<CheckRepetitionHandlerResult> {
30
30
  const { errorCode, reset = false, format = 'human' } = args;
31
31
 
32
+ if (errorCode.trim().length === 0) {
33
+ return {
34
+ exitCode: 1,
35
+ output: 'Error: --code <errorCode> is required. Usage: phasegate ci:check-repetition --code <errorCode> [--reset] [--json]',
36
+ };
37
+ }
38
+
32
39
  if (reset && this.resetUseCase) {
33
40
  const result = await this.resetUseCase.execute({ errorCode, confirmedResolution: true });
34
41
 
@@ -49,9 +56,10 @@ export class CheckRepetitionHandler {
49
56
  }
50
57
 
51
58
  const result = await this.checkUseCase.execute({ errorCode });
59
+ const exitCode = result.escalated === true ? 1 : 0;
52
60
 
53
61
  if (format === 'json') {
54
- return { exitCode: result.exists ? 0 : 1, output: JSON.stringify(result, null, 2) };
62
+ return { exitCode, output: JSON.stringify(result, null, 2) };
55
63
  }
56
64
 
57
65
  const lines: string[] = [];
@@ -63,6 +71,6 @@ export class CheckRepetitionHandler {
63
71
  lines.push(` Escalated: ${result.escalated}`);
64
72
  }
65
73
 
66
- return { exitCode: result.exists ? 0 : 1, output: lines.join('\n') };
74
+ return { exitCode, output: lines.join('\n') };
67
75
  }
68
76
  }
@@ -10,6 +10,7 @@
10
10
  * @work-item-id WI-300
11
11
  * @work-item-id WI-301
12
12
  * @work-item-id WI-302
13
+ * @work-item-id WI-320
13
14
  */
14
15
  import type { HarnessConfigV2 } from "../../domain/harness-config.js";
15
16
 
@@ -54,7 +55,10 @@ export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefi
54
55
  return {
55
56
  project: {
56
57
  preset: resolvedConfig.project.preset,
57
- languages: resolvedConfig.project.languages ?? ["typescript"],
58
+ // WI-320 (github#39): 未宣言時に ["typescript"] を注入せず undefined のまま渡す。
59
+ // HarnessConfigValidatorConfigAdapter.getProjectLanguages() が未宣言時のみ
60
+ // ファイルシステム検出(WI-319)にフォールバックできるようにするため。
61
+ ...(resolvedConfig.project.languages === undefined ? {} : { languages: resolvedConfig.project.languages }),
58
62
  },
59
63
  paths: {
60
64
  designDocs: resolvedConfig.paths.designDocs,
@@ -5,6 +5,7 @@
5
5
  * @work-item-id WI-212
6
6
  * @work-item-id WI-219
7
7
  * @work-item-id WI-300
8
+ * @work-item-id WI-327
8
9
  */
9
10
  import { ConfigFoundationDomainError } from "./errors/config-foundation-domain-error.js";
10
11
  import { ConfigValidationError } from "./errors/config-validation-error.js";
@@ -48,13 +49,15 @@ 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"];
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"];
58
61
  ci?: HarnessConfigResolvedDocument["ci"];
59
62
  validate?: HarnessConfigResolvedDocument["validate"];
60
63
  preCommit?: Partial<HarnessConfigResolvedDocument["preCommit"]>;
@@ -3,6 +3,7 @@
3
3
  * @unit config-foundation
4
4
  * @work-item-id WI-212
5
5
  * @work-item-id WI-300
6
+ * @work-item-id WI-320
6
7
  */
7
8
  import { ConfigFoundationDomainError } from "../errors/config-foundation-domain-error.js";
8
9
  import type { HarnessConfigResolvedDocument, HarnessConfigSourceDocument } from "../harness-config.js";
@@ -163,7 +164,9 @@ export class PresetResolutionService {
163
164
  project: {
164
165
  name: sourceDocument.project.name,
165
166
  preset: sourceDocument.project.preset,
166
- 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 }),
167
170
  },
168
171
  layers,
169
172
  quickMode: deepMerge(presetDefinition.quickMode, sourceDocument.quickMode, "quickMode"),
@@ -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
  }
@@ -21,7 +21,7 @@
21
21
  }
22
22
  },
23
23
  "quickMode": {
24
- "allowedCategories": ["bugfix"],
24
+ "allowedCategories": ["bugfix", "docs", "test", "config"],
25
25
  "maintainedLayers": ["L1", "L2"],
26
26
  "relaxedGates": []
27
27
  },
@@ -21,7 +21,7 @@
21
21
  }
22
22
  },
23
23
  "quickMode": {
24
- "allowedCategories": ["bugfix"],
24
+ "allowedCategories": ["bugfix", "docs", "test", "config"],
25
25
  "maintainedLayers": ["L1", "L2"],
26
26
  "relaxedGates": []
27
27
  },
@@ -21,7 +21,7 @@
21
21
  }
22
22
  },
23
23
  "quickMode": {
24
- "allowedCategories": ["bugfix"],
24
+ "allowedCategories": ["bugfix", "docs", "test", "config"],
25
25
  "maintainedLayers": ["L1", "L2"],
26
26
  "relaxedGates": []
27
27
  },
@@ -2,19 +2,19 @@
2
2
  * @layer infrastructure
3
3
  * @unit config-foundation
4
4
  */
5
- import * as fs from 'node:fs/promises';
6
- import path from 'node:path';
7
- import type { ConfigRepositoryPort } from '../../domain/ports/config-repository-port.js';
5
+ import * as fs from "node:fs/promises";
6
+ import path from "node:path";
7
+ import type { ConfigRepositoryPort } from "../../domain/ports/config-repository-port.js";
8
8
 
9
- const DEFAULT_CONFIG_FILE_NAME = 'phasegate.config.json';
10
- const PERSONAL_CONFIG_PATH = path.join('.phasegate-local', DEFAULT_CONFIG_FILE_NAME);
9
+ const DEFAULT_CONFIG_FILE_NAME = "phasegate.config.json";
10
+ const PERSONAL_CONFIG_PATH = path.join(".phasegate-local", DEFAULT_CONFIG_FILE_NAME);
11
11
 
12
12
  export class ConfigNotFoundError extends Error {
13
13
  readonly configPath: string;
14
14
 
15
15
  constructor(configPath: string) {
16
16
  super(`Config file not found: ${configPath}`);
17
- this.name = 'ConfigNotFoundError';
17
+ this.name = "ConfigNotFoundError";
18
18
  this.configPath = configPath;
19
19
  Object.setPrototypeOf(this, new.target.prototype);
20
20
  }
@@ -24,15 +24,28 @@ export class ConfigPersistenceError extends Error {
24
24
  readonly configPath: string;
25
25
  readonly cause?: unknown;
26
26
 
27
- constructor(configPath: string, cause?: unknown) {
28
- super(`Failed to persist config: ${configPath}`);
29
- this.name = 'ConfigPersistenceError';
27
+ constructor(configPath: string, cause?: unknown, message?: string) {
28
+ super(message ?? `Failed to persist config: ${configPath}`);
29
+ this.name = "ConfigPersistenceError";
30
30
  this.configPath = configPath;
31
31
  this.cause = cause;
32
32
  Object.setPrototypeOf(this, new.target.prototype);
33
33
  }
34
34
  }
35
35
 
36
+ /**
37
+ * WI-325: load 経路の JSON パース失敗専用エラー。
38
+ * ConfigPersistenceError のサブクラスなので、既存の
39
+ * `instanceof ConfigPersistenceError` fail-open 判定(main.ts / WI-314)は無変更で動く。
40
+ */
41
+ export class ConfigParseError extends ConfigPersistenceError {
42
+ constructor(configPath: string, cause?: unknown) {
43
+ super(configPath, cause, `Failed to parse config JSON: ${configPath}`);
44
+ this.name = "ConfigParseError";
45
+ Object.setPrototypeOf(this, new.target.prototype);
46
+ }
47
+ }
48
+
36
49
  async function exists(targetPath: string): Promise<boolean> {
37
50
  try {
38
51
  await fs.access(targetPath);
@@ -69,14 +82,10 @@ async function findNearestConfig(startDirectory: string): Promise<string | null>
69
82
 
70
83
  export class FileSystemConfigRepository implements ConfigRepositoryPort {
71
84
  async load(configPath?: string): Promise<{ path: string; document: unknown }> {
72
- const resolvedPath = configPath
73
- ? path.resolve(configPath)
74
- : await findNearestConfig(process.cwd());
85
+ const resolvedPath = configPath ? path.resolve(configPath) : await findNearestConfig(process.cwd());
75
86
 
76
87
  if (!resolvedPath) {
77
- throw new ConfigNotFoundError(
78
- path.resolve(process.cwd(), DEFAULT_CONFIG_FILE_NAME),
79
- );
88
+ throw new ConfigNotFoundError(path.resolve(process.cwd(), DEFAULT_CONFIG_FILE_NAME));
80
89
  }
81
90
 
82
91
  if (!(await exists(resolvedPath))) {
@@ -84,7 +93,7 @@ export class FileSystemConfigRepository implements ConfigRepositoryPort {
84
93
  }
85
94
 
86
95
  try {
87
- const raw = await fs.readFile(resolvedPath, 'utf8');
96
+ const raw = await fs.readFile(resolvedPath, "utf8");
88
97
 
89
98
  return {
90
99
  path: resolvedPath,
@@ -92,7 +101,7 @@ export class FileSystemConfigRepository implements ConfigRepositoryPort {
92
101
  };
93
102
  } catch (error) {
94
103
  if (error instanceof SyntaxError) {
95
- throw new ConfigPersistenceError(resolvedPath, error);
104
+ throw new ConfigParseError(resolvedPath, error);
96
105
  }
97
106
 
98
107
  throw error;
@@ -104,7 +113,7 @@ export class FileSystemConfigRepository implements ConfigRepositoryPort {
104
113
 
105
114
  try {
106
115
  const serialized = `${JSON.stringify(document, null, 2)}\n`;
107
- await fs.writeFile(resolvedPath, serialized, 'utf8');
116
+ await fs.writeFile(resolvedPath, serialized, "utf8");
108
117
  } catch (error) {
109
118
  throw new ConfigPersistenceError(resolvedPath, error);
110
119
  }
@@ -3,14 +3,7 @@
3
3
  "type": "object",
4
4
  "additionalProperties": false,
5
5
  "required": [
6
- "project",
7
- "layers",
8
- "quickMode",
9
- "phaseDependencies",
10
- "planningMode",
11
- "harnesses",
12
- "paths",
13
- "reporting"
6
+ "project"
14
7
  ],
15
8
  "properties": {
16
9
  "project": {
@@ -3,14 +3,7 @@
3
3
  "type": "object",
4
4
  "additionalProperties": false,
5
5
  "required": [
6
- "project",
7
- "layers",
8
- "quickMode",
9
- "phaseDependencies",
10
- "planningMode",
11
- "harnesses",
12
- "paths",
13
- "reporting"
6
+ "project"
14
7
  ],
15
8
  "$defs": {
16
9
  "effectCapability": {
@@ -1,10 +1,20 @@
1
1
  // @layer domain
2
2
  // config-query-port.ts
3
3
 
4
- import type { PresetInfo, ConfigSummary, PhaseGateSummary } from '../value-objects/harness-status-summary.js';
4
+ import type {
5
+ ConfigSummary,
6
+ LanguageInfo,
7
+ PhaseGateSummary,
8
+ PresetInfo,
9
+ } from "../value-objects/harness-status-summary.js";
5
10
 
6
11
  export interface ConfigQueryPort {
7
12
  getPresetInfo(): Promise<PresetInfo>;
8
13
  getConfigSummary(): Promise<ConfigSummary>;
9
14
  getPhaseGateSummary(): Promise<PhaseGateSummary>;
15
+ /**
16
+ * WI-328: 実効言語リストと出所(declared / detected / fallback)を返す。
17
+ * 後方互換のため optional — 未実装アダプタでは status に languages を出さない。
18
+ */
19
+ getLanguageInfo?(): Promise<LanguageInfo>;
10
20
  }