phasegate 0.264.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 (29) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/docs/guide/installation.md +1 -1
  3. package/docs/templates/ci/aidlc-gate.yml +22 -4
  4. package/package.json +2 -2
  5. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +30 -27
  6. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
  7. package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
  8. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +5 -1
  9. package/scripts/harness/config-foundation/domain/harness-config.ts +10 -7
  10. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +4 -1
  11. package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
  12. package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +27 -18
  13. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +1 -8
  14. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +1 -8
  15. package/scripts/harness/harness-api/domain/ports/config-query-port.ts +11 -1
  16. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +125 -86
  17. package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +31 -21
  18. package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +23 -8
  19. package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +4 -4
  20. package/scripts/harness/harness-api/infrastructure/adapters/harness-config-query-adapter.ts +79 -34
  21. package/scripts/harness/installation/application/usecases/run-install.ts +199 -51
  22. package/scripts/harness/installation/application/usecases/run-reconcile.ts +277 -69
  23. package/scripts/harness/installation/domain/deployment-manifest.ts +43 -0
  24. package/scripts/harness/main.ts +26 -6
  25. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +24 -7
  26. package/scripts/harness/validator-system/composition-root.ts +4 -1
  27. package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
  28. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +89 -3
  29. package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +49 -20
@@ -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
  }
@@ -1,22 +1,27 @@
1
1
  // @layer domain
2
2
  // @unit harness-api
3
- // @work-item-id WI-108 / WI-114, WI-186
3
+ // @work-item-id WI-108 / WI-114, WI-186, WI-318, WI-321, WI-328
4
4
  // command-dispatch-service.ts — CommandDispatchService Domain Service
5
5
 
6
- import { CommandRegistry } from './command-registry.js';
7
- import { StatusDerivationService } from './status-derivation-service.js';
8
- import { HarnessApiResponse, type ExitCode, type HarnessError } from '../value-objects/harness-api-response.js';
9
- import { CheckReadyResult } from '../value-objects/check-ready-result.js';
10
- import { CiCheckResult } from '../value-objects/ci-check-result.js';
11
- import { DriftReportSummary } from '../value-objects/drift-report-summary.js';
12
- import type { ValidatorExecutionPort } from '../ports/validator-execution-port.js';
13
- import type { PhaseGateQueryPort } from '../ports/phase-gate-query-port.js';
14
- import type { BiomeLintPort } from '../ports/biome-lint-port.js';
15
- import type { ImpactAnalysisPort } from '../ports/impact-analysis-port.js';
16
- import type { ArtifactScannerPort } from '../ports/artifact-scanner-port.js';
17
- import type { ConfigQueryPort } from '../ports/config-query-port.js';
18
- import type { LayerId } from '../value-objects/layer-health.js';
19
- import type { BaselineHealth, HookHealth, OperationalWarning } from '../value-objects/harness-status-summary.js';
6
+ import type { ArtifactScannerPort } from "../ports/artifact-scanner-port.js";
7
+ import type { BiomeLintPort } from "../ports/biome-lint-port.js";
8
+ import type { ConfigQueryPort } from "../ports/config-query-port.js";
9
+ import type { ImpactAnalysisPort } from "../ports/impact-analysis-port.js";
10
+ import type { PhaseGateQueryPort } from "../ports/phase-gate-query-port.js";
11
+ import type { ValidatorExecutionPort } from "../ports/validator-execution-port.js";
12
+ import { CheckReadyResult } from "../value-objects/check-ready-result.js";
13
+ import { CiCheckResult } from "../value-objects/ci-check-result.js";
14
+ import { DriftReportSummary } from "../value-objects/drift-report-summary.js";
15
+ import { type ExitCode, HarnessApiResponse, type HarnessError } from "../value-objects/harness-api-response.js";
16
+ import type {
17
+ BaselineHealth,
18
+ HookHealth,
19
+ LanguageInfo,
20
+ OperationalWarning,
21
+ } from "../value-objects/harness-status-summary.js";
22
+ import type { LayerId } from "../value-objects/layer-health.js";
23
+ import { CommandRegistry } from "./command-registry.js";
24
+ import { StatusDerivationService } from "./status-derivation-service.js";
20
25
 
21
26
  export interface CommandDispatchPorts {
22
27
  validatorExecutionPort: ValidatorExecutionPort;
@@ -28,6 +33,7 @@ export interface CommandDispatchPorts {
28
33
  getConfig?: () => Promise<unknown>;
29
34
  getPresetInfo?: () => Promise<unknown>;
30
35
  getConfigSummary?: () => Promise<unknown>;
36
+ getLanguageInfo?: () => Promise<LanguageInfo>;
31
37
  getHookHealth?: () => Promise<HookHealth>;
32
38
  getBaselineHealth?: () => Promise<BaselineHealth>;
33
39
  };
@@ -42,27 +48,29 @@ export interface DispatchResult<T = unknown> {
42
48
  }
43
49
 
44
50
  function makeError(message: string): HarnessError {
45
- return { code: 'HARNESS_ERROR', severity: 'error', message };
51
+ return { code: "HARNESS_ERROR", severity: "error", message };
46
52
  }
47
53
 
48
- type LiveValidationState = 'pass' | 'fail' | 'skipped' | 'not-run' | 'error';
54
+ type LiveValidationState = "pass" | "fail" | "skipped" | "not-run" | "error";
49
55
 
50
56
  function layerIdFromValidatorId(validatorId: string): LayerId | null {
51
57
  const prefix = validatorId.slice(0, 2);
52
- return prefix === 'L1' || prefix === 'L2' || prefix === 'L3' || prefix === 'L4' ? prefix : null;
58
+ return prefix === "L1" || prefix === "L2" || prefix === "L3" || prefix === "L4" ? prefix : null;
53
59
  }
54
60
 
55
- function summarizeLayerResults(items: readonly { validatorId: string; passed: boolean; skipped?: boolean }[]): Partial<Record<LayerId, LiveValidationState>> {
61
+ function summarizeLayerResults(
62
+ items: readonly { validatorId: string; passed: boolean; skipped?: boolean }[],
63
+ ): Partial<Record<LayerId, LiveValidationState>> {
56
64
  const result: Partial<Record<LayerId, LiveValidationState>> = {};
57
- for (const layerId of ['L2', 'L3', 'L4'] as const) {
65
+ for (const layerId of ["L2", "L3", "L4"] as const) {
58
66
  const layerItems = items.filter((item) => layerIdFromValidatorId(item.validatorId) === layerId);
59
67
  if (layerItems.length === 0) continue;
60
68
  if (layerItems.every((item) => item.skipped === true)) {
61
- result[layerId] = 'skipped';
69
+ result[layerId] = "skipped";
62
70
  } else if (layerItems.some((item) => !item.passed && item.skipped !== true)) {
63
- result[layerId] = 'fail';
71
+ result[layerId] = "fail";
64
72
  } else {
65
- result[layerId] = 'pass';
73
+ result[layerId] = "pass";
66
74
  }
67
75
  }
68
76
  return result;
@@ -74,7 +82,7 @@ function hasEnabledLiveFailure(
74
82
  ): boolean {
75
83
  return enabledLayers.some((layerId) => {
76
84
  const liveState = liveValidationByLayer[layerId];
77
- return liveState === 'fail' || liveState === 'error';
85
+ return liveState === "fail" || liveState === "error";
78
86
  });
79
87
  }
80
88
 
@@ -83,42 +91,41 @@ function buildOperationalWarnings(
83
91
  baselineHealth: BaselineHealth | undefined,
84
92
  ): OperationalWarning[] {
85
93
  const warnings: OperationalWarning[] = [];
86
- const skipCount = hookHealth === undefined
87
- ? 0
88
- : Object.values(hookHealth.skipCountsByReason).reduce((sum, count) => sum + count, 0);
94
+ const skipCount =
95
+ hookHealth === undefined ? 0 : Object.values(hookHealth.skipCountsByReason).reduce((sum, count) => sum + count, 0);
89
96
  if (hookHealth !== undefined && skipCount > 0) {
90
97
  warnings.push({
91
- code: 'HOOK_SKIP_OBSERVED',
98
+ code: "HOOK_SKIP_OBSERVED",
92
99
  message: `Hook skip events observed: ${skipCount}`,
93
- nextAction: 'Inspect hookHealth.latestSkip and re-enable hooks or resolve reentry/timeout causes.',
100
+ nextAction: "Inspect hookHealth.latestSkip and re-enable hooks or resolve reentry/timeout causes.",
94
101
  });
95
102
  }
96
103
  if (baselineHealth !== undefined && baselineHealth.shaMismatchCount > 0) {
97
104
  warnings.push({
98
- code: 'BASELINE_SHA_MISMATCH',
105
+ code: "BASELINE_SHA_MISMATCH",
99
106
  message: `${baselineHealth.shaMismatchCount} baseline files changed since the snapshot.`,
100
- nextAction: 'Add or update design coverage for changed files and remove resolved entries from the baseline.',
107
+ nextAction: "Add or update design coverage for changed files and remove resolved entries from the baseline.",
101
108
  });
102
109
  }
103
110
  if (baselineHealth !== undefined && baselineHealth.grandfatheredFileCount > 50 && baselineHealth.removalRate < 0.5) {
104
111
  warnings.push({
105
- code: 'BASELINE_DEBT_HIGH',
112
+ code: "BASELINE_DEBT_HIGH",
106
113
  message: `Baseline grandfather debt remains high: ${baselineHealth.grandfatheredFileCount} files.`,
107
- nextAction: 'Plan a retrofit cleanup batch and reduce the baseline snapshot.',
114
+ nextAction: "Plan a retrofit cleanup batch and reduce the baseline snapshot.",
108
115
  });
109
116
  }
110
117
  return warnings;
111
118
  }
112
119
 
113
120
  const KNOWN_COMMANDS = new Set([
114
- 'phasegate:check-ready',
115
- 'phasegate:check-phase',
116
- 'phasegate:ci-check',
117
- 'phasegate:detect-drift',
118
- 'phasegate:status',
119
- 'phasegate:lint',
120
- 'phasegate:complete-check',
121
- 'phasegate:impact-analysis',
121
+ "phasegate:check-ready",
122
+ "phasegate:check-phase",
123
+ "phasegate:ci-check",
124
+ "phasegate:detect-drift",
125
+ "phasegate:status",
126
+ "phasegate:lint",
127
+ "phasegate:complete-check",
128
+ "phasegate:impact-analysis",
122
129
  ]);
123
130
 
124
131
  export class CommandDispatchService {
@@ -129,7 +136,7 @@ export class CommandDispatchService {
129
136
  constructor(
130
137
  registryOrPorts: CommandRegistry | CommandDispatchPorts,
131
138
  ports?: CommandDispatchPorts,
132
- statusDerivationService?: StatusDerivationService
139
+ statusDerivationService?: StatusDerivationService,
133
140
  ) {
134
141
  if (registryOrPorts instanceof CommandRegistry) {
135
142
  this.registry = registryOrPorts;
@@ -164,7 +171,7 @@ export class CommandDispatchService {
164
171
  const message = err instanceof Error ? err.message : String(err);
165
172
  const response = HarnessApiResponse.error<T>([makeError(message)], { ...summary, failed: 1 });
166
173
  return {
167
- status: 'error',
174
+ status: "error",
168
175
  errors: response.errors,
169
176
  summary: response.summary,
170
177
  data: undefined,
@@ -177,61 +184,62 @@ export class CommandDispatchService {
177
184
  commandName: string,
178
185
  args: Record<string, string>,
179
186
  _flags: Record<string, boolean | string>,
180
- summary: { totalChecks: number; passed: number; failed: number; warnings: number }
187
+ summary: { totalChecks: number; passed: number; failed: number; warnings: number },
181
188
  ): Promise<DispatchResult<T>> {
182
189
  switch (commandName) {
183
- case 'phasegate:check-ready': {
190
+ case "phasegate:check-ready": {
184
191
  const stories = await this.ports.phaseGateQueryPort.queryAllStories();
185
192
  const result = CheckReadyResult.fromStories(stories);
186
193
  if (result.allPassed) {
187
194
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, result);
188
- return { status: 'pass', errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
195
+ return { status: "pass", errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
189
196
  }
190
197
  const failed = result.getFailedStories();
191
198
  const errors = failed.map((s) => makeError(`Story ${s.storyId} failed Phase Gate`));
192
199
  const r = HarnessApiResponse.fail(errors, { ...summary, failed: 1 }, result);
193
- return { status: 'fail', errors: r.errors, summary: r.summary, data: result as unknown as T, exitCode: 1 };
200
+ return { status: "fail", errors: r.errors, summary: r.summary, data: result as unknown as T, exitCode: 1 };
194
201
  }
195
202
 
196
- case 'phasegate:check-phase': {
197
- const unitId = args.unit ?? '';
203
+ case "phasegate:check-phase": {
204
+ const unitId = args.unit ?? "";
198
205
  const phaseInfo = await this.ports.phaseGateQueryPort.queryUnit(unitId);
199
206
  if (phaseInfo === null) {
200
207
  const errors = [makeError(`Unit '${unitId}' not found`)];
201
208
  const r = HarnessApiResponse.fail(errors, { ...summary, failed: 1 });
202
- return { status: 'fail', errors: r.errors, summary: r.summary, data: undefined, exitCode: 1 };
209
+ return { status: "fail", errors: r.errors, summary: r.summary, data: undefined, exitCode: 1 };
203
210
  }
204
211
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, phaseInfo);
205
- return { status: 'pass', errors: [], summary: r.summary, data: phaseInfo as unknown as T, exitCode: 0 };
212
+ return { status: "pass", errors: [], summary: r.summary, data: phaseInfo as unknown as T, exitCode: 0 };
206
213
  }
207
214
 
208
- case 'phasegate:ci-check': {
215
+ case "phasegate:ci-check": {
209
216
  const validatorResults = await this.ports.validatorExecutionPort.runAllValidators();
210
217
  const result = CiCheckResult.fromResults(validatorResults);
211
218
  if (result.allPassed) {
212
219
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, result);
213
- return { status: 'pass', errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
220
+ return { status: "pass", errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
214
221
  }
215
222
  const collected = result.collectAllErrors();
216
- const errors = collected.length > 0
217
- ? collected
218
- : result.getFailedValidators().map((v) => makeError(`Validator ${v.validatorId} failed`));
223
+ const errors =
224
+ collected.length > 0
225
+ ? collected
226
+ : result.getFailedValidators().map((v) => makeError(`Validator ${v.validatorId} failed`));
219
227
  const r = HarnessApiResponse.fail(errors, { ...summary, failed: 1 }, result);
220
- return { status: 'fail', errors: r.errors, summary: r.summary, data: result as unknown as T, exitCode: 1 };
228
+ return { status: "fail", errors: r.errors, summary: r.summary, data: result as unknown as T, exitCode: 1 };
221
229
  }
222
230
 
223
- case 'phasegate:detect-drift': {
231
+ case "phasegate:detect-drift": {
224
232
  const drifts = await this.ports.validatorExecutionPort.runDriftDetection();
225
233
  const result = DriftReportSummary.fromDrifts(drifts);
226
234
  if (!result.hasDrift()) {
227
235
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, result);
228
- return { status: 'pass', errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
236
+ return { status: "pass", errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
229
237
  }
230
238
  const r = HarnessApiResponse.pass({ ...summary, passed: 1, warnings: drifts.length }, result);
231
- return { status: 'pass', errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
239
+ return { status: "pass", errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
232
240
  }
233
241
 
234
- case 'phasegate:status': {
242
+ case "phasegate:status": {
235
243
  const scanResult = await this.ports.artifactScannerPort.scan();
236
244
  const liveValidationByLayer: Partial<Record<LayerId, LiveValidationState>> = {};
237
245
  try {
@@ -239,12 +247,15 @@ export class CommandDispatchService {
239
247
  this.ports.biomeLintPort.runLint(),
240
248
  this.ports.validatorExecutionPort.runAllValidators(),
241
249
  ]);
242
- liveValidationByLayer.L1 = lintResult.passed ? 'pass' : 'fail';
250
+ liveValidationByLayer.L1 = lintResult.passed ? "pass" : "fail";
243
251
  Object.assign(liveValidationByLayer, summarizeLayerResults(validatorResults));
244
252
  } catch {
245
- liveValidationByLayer.L1 = liveValidationByLayer.L1 ?? 'error';
253
+ liveValidationByLayer.L1 = liveValidationByLayer.L1 ?? "error";
246
254
  }
247
- let presetInfo = { name: 'standard' as const, enabledLayers: ['L1', 'L2', 'L3'] as ('L1' | 'L2' | 'L3' | 'L4')[] };
255
+ let presetInfo = {
256
+ name: "standard" as const,
257
+ enabledLayers: ["L1", "L2", "L3"] as ("L1" | "L2" | "L3" | "L4")[],
258
+ };
248
259
  const configPort = this.ports.configQueryPort;
249
260
  if (configPort.getPresetInfo) {
250
261
  const pi = await configPort.getPresetInfo();
@@ -252,15 +263,19 @@ export class CommandDispatchService {
252
263
  } else if (configPort.getConfig) {
253
264
  await configPort.getConfig();
254
265
  }
255
- const [hookHealth, baselineHealth] = await Promise.all([
266
+ // WI-328 (github#39 残課題): 実効言語と出所を status に載せる。
267
+ // ポート未実装(後方互換)の場合は languages フィールドを出さない。
268
+ const [hookHealth, baselineHealth, languages] = await Promise.all([
256
269
  configPort.getHookHealth?.(),
257
270
  configPort.getBaselineHealth?.(),
271
+ configPort.getLanguageInfo?.(),
258
272
  ]);
259
273
  const operationalWarnings: OperationalWarning[] = buildOperationalWarnings(hookHealth, baselineHealth);
260
274
  const statusSummary = this.statusDerivationService.derive({
261
275
  scanResult,
262
276
  presetInfo,
263
- configSummary: { configPath: 'phasegate.config.json', lastModified: '', version: '2' },
277
+ languages,
278
+ configSummary: { configPath: "phasegate.config.json", lastModified: "", version: "2" },
264
279
  phaseGateSummary: { totalStories: 0, passedStories: 0, pendingStories: 0 },
265
280
  liveValidationByLayer,
266
281
  hookHealth,
@@ -269,60 +284,84 @@ export class CommandDispatchService {
269
284
  });
270
285
  if (hasEnabledLiveFailure(presetInfo.enabledLayers, liveValidationByLayer)) {
271
286
  const errors = statusSummary.layers
272
- .filter((layer) => layer.enabled && (layer.liveValidationState === 'fail' || layer.liveValidationState === 'error'))
287
+ .filter(
288
+ (layer) =>
289
+ layer.enabled && (layer.liveValidationState === "fail" || layer.liveValidationState === "error"),
290
+ )
273
291
  .map((layer) => makeError(`Layer ${layer.layerId} live validation ${layer.liveValidationState}`));
274
292
  const r = HarnessApiResponse.fail(errors, { ...summary, failed: 1 }, statusSummary);
275
- return { status: 'fail', errors: r.errors, summary: r.summary, data: statusSummary as unknown as T, exitCode: 0 };
293
+ return {
294
+ status: "fail",
295
+ errors: r.errors,
296
+ summary: r.summary,
297
+ data: statusSummary as unknown as T,
298
+ exitCode: 0,
299
+ };
276
300
  }
277
301
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, statusSummary);
278
- return { status: 'pass', errors: [], summary: r.summary, data: statusSummary as unknown as T, exitCode: 0 };
302
+ return { status: "pass", errors: [], summary: r.summary, data: statusSummary as unknown as T, exitCode: 0 };
279
303
  }
280
304
 
281
- case 'phasegate:lint': {
305
+ case "phasegate:lint": {
282
306
  const lintResult = await this.ports.biomeLintPort.runLint();
283
307
  if (lintResult.passed) {
284
308
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 });
285
- return { status: 'pass', errors: [], summary: r.summary, data: undefined, exitCode: 0 };
309
+ return { status: "pass", errors: [], summary: r.summary, data: undefined, exitCode: 0 };
286
310
  }
287
- const errors = lintResult.errors.length > 0 ? lintResult.errors : [makeError('Lint failed')];
311
+ const errors = lintResult.errors.length > 0 ? lintResult.errors : [makeError("Lint failed")];
288
312
  const r = HarnessApiResponse.fail(errors, { ...summary, failed: 1 });
289
- return { status: 'fail', errors: r.errors, summary: r.summary, data: undefined, exitCode: 1 };
313
+ return { status: "fail", errors: r.errors, summary: r.summary, data: undefined, exitCode: 1 };
290
314
  }
291
315
 
292
- case 'phasegate:complete-check': {
316
+ case "phasegate:complete-check": {
293
317
  const [validatorResults, lintResult] = await Promise.all([
294
318
  this.ports.validatorExecutionPort.runAllValidators(),
295
319
  this.ports.biomeLintPort.runLint(),
296
320
  ]);
297
321
  const allErrors: HarnessError[] = [];
298
- for (const v of validatorResults) {
299
- if (!v.passed) {
300
- const errs = v.errors && v.errors.length > 0 ? v.errors : [makeError(`Validator ${v.validatorId} failed`)];
322
+ let warningCount = 0;
323
+ // WI-318 (github#38): ci-check case と同一の CiCheckResult.fromResults() を通して
324
+ // ADR-017 / WI-260 severity-aware 集約(isEffectivelyPassed)を適用する。
325
+ // これにより warning-only failure(例: L2-016 の ungated-legacy marker)が
326
+ // Stop hook の complete-check 経路でも validate --layer L2 / ci-check と同じく
327
+ // 実質 pass となる。lint 結果の合流(lint fail → exit 1)は従来どおり維持。
328
+ // WI-321 (github#38 残課題): warning が summary の件数のみで内容不明だったため、
329
+ // ci-check case と同じく CiCheckResult を data ペイロードとして pass/fail 双方で返す。
330
+ // validatorResults が空の場合のみ従来どおり data なし。
331
+ let result: CiCheckResult | undefined;
332
+ if (validatorResults.length > 0) {
333
+ result = CiCheckResult.fromResults(validatorResults);
334
+ warningCount = result.collectAllErrors().filter((e) => e.severity === "warning").length;
335
+ for (const v of result.getFailedValidators()) {
336
+ const errs =
337
+ v.errors !== undefined && v.errors.length > 0
338
+ ? v.errors
339
+ : [makeError(`Validator ${v.validatorId} failed`)];
301
340
  allErrors.push(...errs);
302
341
  }
303
342
  }
304
343
  if (!lintResult.passed) {
305
- const errs = lintResult.errors.length > 0 ? lintResult.errors : [makeError('Lint failed')];
344
+ const errs = lintResult.errors.length > 0 ? lintResult.errors : [makeError("Lint failed")];
306
345
  allErrors.push(...errs);
307
346
  }
308
347
  if (allErrors.length === 0) {
309
- const r = HarnessApiResponse.pass({ ...summary, passed: 1 });
310
- return { status: 'pass', errors: [], summary: r.summary, data: undefined, exitCode: 0 };
348
+ const r = HarnessApiResponse.pass({ ...summary, passed: 1, warnings: warningCount }, result);
349
+ return { status: "pass", errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
311
350
  }
312
- const r = HarnessApiResponse.fail(allErrors, { ...summary, failed: 1 });
313
- return { status: 'fail', errors: r.errors, summary: r.summary, data: undefined, exitCode: 1 };
351
+ const r = HarnessApiResponse.fail(allErrors, { ...summary, failed: 1, warnings: warningCount }, result);
352
+ return { status: "fail", errors: r.errors, summary: r.summary, data: result as unknown as T, exitCode: 1 };
314
353
  }
315
354
 
316
- case 'phasegate:impact-analysis': {
317
- const storyId = args.storyId ?? '';
355
+ case "phasegate:impact-analysis": {
356
+ const storyId = args.storyId ?? "";
318
357
  const result = await this.ports.impactAnalysisPort.analyze(storyId);
319
358
  if (result === null) {
320
359
  const errors = [makeError(`Story '${storyId}' not found`)];
321
360
  const r = HarnessApiResponse.fail(errors, { ...summary, failed: 1 });
322
- return { status: 'fail', errors: r.errors, summary: r.summary, data: undefined, exitCode: 1 };
361
+ return { status: "fail", errors: r.errors, summary: r.summary, data: undefined, exitCode: 1 };
323
362
  }
324
363
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, result);
325
- return { status: 'pass', errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
364
+ return { status: "pass", errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
326
365
  }
327
366
 
328
367
  default: