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
@@ -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, WI-339
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, type ValidatorCheckItem } 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,39 +48,50 @@ 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;
69
77
  }
70
78
 
79
+ /**
80
+ * WI-339: status の layer 集計へ渡す前に、ci-check / complete-check と同じ
81
+ * CiCheckResult 投影で raw passed を実効 passed に正規化する。
82
+ * failOnWarning は既存 harness-api 経路と同じ既定値 false を使用する。
83
+ */
84
+ function toEffectiveValidatorResults(items: readonly ValidatorCheckItem[]): readonly ValidatorCheckItem[] {
85
+ return items.length === 0 ? items : CiCheckResult.fromResults(items).validatorResults;
86
+ }
87
+
71
88
  function hasEnabledLiveFailure(
72
89
  enabledLayers: readonly LayerId[],
73
90
  liveValidationByLayer: Partial<Record<LayerId, LiveValidationState>>,
74
91
  ): boolean {
75
92
  return enabledLayers.some((layerId) => {
76
93
  const liveState = liveValidationByLayer[layerId];
77
- return liveState === 'fail' || liveState === 'error';
94
+ return liveState === "fail" || liveState === "error";
78
95
  });
79
96
  }
80
97
 
@@ -83,42 +100,41 @@ function buildOperationalWarnings(
83
100
  baselineHealth: BaselineHealth | undefined,
84
101
  ): OperationalWarning[] {
85
102
  const warnings: OperationalWarning[] = [];
86
- const skipCount = hookHealth === undefined
87
- ? 0
88
- : Object.values(hookHealth.skipCountsByReason).reduce((sum, count) => sum + count, 0);
103
+ const skipCount =
104
+ hookHealth === undefined ? 0 : Object.values(hookHealth.skipCountsByReason).reduce((sum, count) => sum + count, 0);
89
105
  if (hookHealth !== undefined && skipCount > 0) {
90
106
  warnings.push({
91
- code: 'HOOK_SKIP_OBSERVED',
107
+ code: "HOOK_SKIP_OBSERVED",
92
108
  message: `Hook skip events observed: ${skipCount}`,
93
- nextAction: 'Inspect hookHealth.latestSkip and re-enable hooks or resolve reentry/timeout causes.',
109
+ nextAction: "Inspect hookHealth.latestSkip and re-enable hooks or resolve reentry/timeout causes.",
94
110
  });
95
111
  }
96
112
  if (baselineHealth !== undefined && baselineHealth.shaMismatchCount > 0) {
97
113
  warnings.push({
98
- code: 'BASELINE_SHA_MISMATCH',
114
+ code: "BASELINE_SHA_MISMATCH",
99
115
  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.',
116
+ nextAction: "Add or update design coverage for changed files and remove resolved entries from the baseline.",
101
117
  });
102
118
  }
103
119
  if (baselineHealth !== undefined && baselineHealth.grandfatheredFileCount > 50 && baselineHealth.removalRate < 0.5) {
104
120
  warnings.push({
105
- code: 'BASELINE_DEBT_HIGH',
121
+ code: "BASELINE_DEBT_HIGH",
106
122
  message: `Baseline grandfather debt remains high: ${baselineHealth.grandfatheredFileCount} files.`,
107
- nextAction: 'Plan a retrofit cleanup batch and reduce the baseline snapshot.',
123
+ nextAction: "Plan a retrofit cleanup batch and reduce the baseline snapshot.",
108
124
  });
109
125
  }
110
126
  return warnings;
111
127
  }
112
128
 
113
129
  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',
130
+ "phasegate:check-ready",
131
+ "phasegate:check-phase",
132
+ "phasegate:ci-check",
133
+ "phasegate:detect-drift",
134
+ "phasegate:status",
135
+ "phasegate:lint",
136
+ "phasegate:complete-check",
137
+ "phasegate:impact-analysis",
122
138
  ]);
123
139
 
124
140
  export class CommandDispatchService {
@@ -129,7 +145,7 @@ export class CommandDispatchService {
129
145
  constructor(
130
146
  registryOrPorts: CommandRegistry | CommandDispatchPorts,
131
147
  ports?: CommandDispatchPorts,
132
- statusDerivationService?: StatusDerivationService
148
+ statusDerivationService?: StatusDerivationService,
133
149
  ) {
134
150
  if (registryOrPorts instanceof CommandRegistry) {
135
151
  this.registry = registryOrPorts;
@@ -164,7 +180,7 @@ export class CommandDispatchService {
164
180
  const message = err instanceof Error ? err.message : String(err);
165
181
  const response = HarnessApiResponse.error<T>([makeError(message)], { ...summary, failed: 1 });
166
182
  return {
167
- status: 'error',
183
+ status: "error",
168
184
  errors: response.errors,
169
185
  summary: response.summary,
170
186
  data: undefined,
@@ -177,61 +193,62 @@ export class CommandDispatchService {
177
193
  commandName: string,
178
194
  args: Record<string, string>,
179
195
  _flags: Record<string, boolean | string>,
180
- summary: { totalChecks: number; passed: number; failed: number; warnings: number }
196
+ summary: { totalChecks: number; passed: number; failed: number; warnings: number },
181
197
  ): Promise<DispatchResult<T>> {
182
198
  switch (commandName) {
183
- case 'phasegate:check-ready': {
199
+ case "phasegate:check-ready": {
184
200
  const stories = await this.ports.phaseGateQueryPort.queryAllStories();
185
201
  const result = CheckReadyResult.fromStories(stories);
186
202
  if (result.allPassed) {
187
203
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, result);
188
- return { status: 'pass', errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
204
+ return { status: "pass", errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
189
205
  }
190
206
  const failed = result.getFailedStories();
191
207
  const errors = failed.map((s) => makeError(`Story ${s.storyId} failed Phase Gate`));
192
208
  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 };
209
+ return { status: "fail", errors: r.errors, summary: r.summary, data: result as unknown as T, exitCode: 1 };
194
210
  }
195
211
 
196
- case 'phasegate:check-phase': {
197
- const unitId = args.unit ?? '';
212
+ case "phasegate:check-phase": {
213
+ const unitId = args.unit ?? "";
198
214
  const phaseInfo = await this.ports.phaseGateQueryPort.queryUnit(unitId);
199
215
  if (phaseInfo === null) {
200
216
  const errors = [makeError(`Unit '${unitId}' not found`)];
201
217
  const r = HarnessApiResponse.fail(errors, { ...summary, failed: 1 });
202
- return { status: 'fail', errors: r.errors, summary: r.summary, data: undefined, exitCode: 1 };
218
+ return { status: "fail", errors: r.errors, summary: r.summary, data: undefined, exitCode: 1 };
203
219
  }
204
220
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, phaseInfo);
205
- return { status: 'pass', errors: [], summary: r.summary, data: phaseInfo as unknown as T, exitCode: 0 };
221
+ return { status: "pass", errors: [], summary: r.summary, data: phaseInfo as unknown as T, exitCode: 0 };
206
222
  }
207
223
 
208
- case 'phasegate:ci-check': {
224
+ case "phasegate:ci-check": {
209
225
  const validatorResults = await this.ports.validatorExecutionPort.runAllValidators();
210
226
  const result = CiCheckResult.fromResults(validatorResults);
211
227
  if (result.allPassed) {
212
228
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, result);
213
- return { status: 'pass', errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
229
+ return { status: "pass", errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
214
230
  }
215
231
  const collected = result.collectAllErrors();
216
- const errors = collected.length > 0
217
- ? collected
218
- : result.getFailedValidators().map((v) => makeError(`Validator ${v.validatorId} failed`));
232
+ const errors =
233
+ collected.length > 0
234
+ ? collected
235
+ : result.getFailedValidators().map((v) => makeError(`Validator ${v.validatorId} failed`));
219
236
  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 };
237
+ return { status: "fail", errors: r.errors, summary: r.summary, data: result as unknown as T, exitCode: 1 };
221
238
  }
222
239
 
223
- case 'phasegate:detect-drift': {
240
+ case "phasegate:detect-drift": {
224
241
  const drifts = await this.ports.validatorExecutionPort.runDriftDetection();
225
242
  const result = DriftReportSummary.fromDrifts(drifts);
226
243
  if (!result.hasDrift()) {
227
244
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, result);
228
- return { status: 'pass', errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
245
+ return { status: "pass", errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
229
246
  }
230
247
  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 };
248
+ return { status: "pass", errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
232
249
  }
233
250
 
234
- case 'phasegate:status': {
251
+ case "phasegate:status": {
235
252
  const scanResult = await this.ports.artifactScannerPort.scan();
236
253
  const liveValidationByLayer: Partial<Record<LayerId, LiveValidationState>> = {};
237
254
  try {
@@ -239,12 +256,15 @@ export class CommandDispatchService {
239
256
  this.ports.biomeLintPort.runLint(),
240
257
  this.ports.validatorExecutionPort.runAllValidators(),
241
258
  ]);
242
- liveValidationByLayer.L1 = lintResult.passed ? 'pass' : 'fail';
243
- Object.assign(liveValidationByLayer, summarizeLayerResults(validatorResults));
259
+ liveValidationByLayer.L1 = lintResult.passed ? "pass" : "fail";
260
+ Object.assign(liveValidationByLayer, summarizeLayerResults(toEffectiveValidatorResults(validatorResults)));
244
261
  } catch {
245
- liveValidationByLayer.L1 = liveValidationByLayer.L1 ?? 'error';
262
+ liveValidationByLayer.L1 = liveValidationByLayer.L1 ?? "error";
246
263
  }
247
- let presetInfo = { name: 'standard' as const, enabledLayers: ['L1', 'L2', 'L3'] as ('L1' | 'L2' | 'L3' | 'L4')[] };
264
+ let presetInfo = {
265
+ name: "standard" as const,
266
+ enabledLayers: ["L1", "L2", "L3"] as ("L1" | "L2" | "L3" | "L4")[],
267
+ };
248
268
  const configPort = this.ports.configQueryPort;
249
269
  if (configPort.getPresetInfo) {
250
270
  const pi = await configPort.getPresetInfo();
@@ -252,15 +272,19 @@ export class CommandDispatchService {
252
272
  } else if (configPort.getConfig) {
253
273
  await configPort.getConfig();
254
274
  }
255
- const [hookHealth, baselineHealth] = await Promise.all([
275
+ // WI-328 (github#39 残課題): 実効言語と出所を status に載せる。
276
+ // ポート未実装(後方互換)の場合は languages フィールドを出さない。
277
+ const [hookHealth, baselineHealth, languages] = await Promise.all([
256
278
  configPort.getHookHealth?.(),
257
279
  configPort.getBaselineHealth?.(),
280
+ configPort.getLanguageInfo?.(),
258
281
  ]);
259
282
  const operationalWarnings: OperationalWarning[] = buildOperationalWarnings(hookHealth, baselineHealth);
260
283
  const statusSummary = this.statusDerivationService.derive({
261
284
  scanResult,
262
285
  presetInfo,
263
- configSummary: { configPath: 'phasegate.config.json', lastModified: '', version: '2' },
286
+ languages,
287
+ configSummary: { configPath: "phasegate.config.json", lastModified: "", version: "2" },
264
288
  phaseGateSummary: { totalStories: 0, passedStories: 0, pendingStories: 0 },
265
289
  liveValidationByLayer,
266
290
  hookHealth,
@@ -269,60 +293,84 @@ export class CommandDispatchService {
269
293
  });
270
294
  if (hasEnabledLiveFailure(presetInfo.enabledLayers, liveValidationByLayer)) {
271
295
  const errors = statusSummary.layers
272
- .filter((layer) => layer.enabled && (layer.liveValidationState === 'fail' || layer.liveValidationState === 'error'))
296
+ .filter(
297
+ (layer) =>
298
+ layer.enabled && (layer.liveValidationState === "fail" || layer.liveValidationState === "error"),
299
+ )
273
300
  .map((layer) => makeError(`Layer ${layer.layerId} live validation ${layer.liveValidationState}`));
274
301
  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 };
302
+ return {
303
+ status: "fail",
304
+ errors: r.errors,
305
+ summary: r.summary,
306
+ data: statusSummary as unknown as T,
307
+ exitCode: 0,
308
+ };
276
309
  }
277
310
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, statusSummary);
278
- return { status: 'pass', errors: [], summary: r.summary, data: statusSummary as unknown as T, exitCode: 0 };
311
+ return { status: "pass", errors: [], summary: r.summary, data: statusSummary as unknown as T, exitCode: 0 };
279
312
  }
280
313
 
281
- case 'phasegate:lint': {
314
+ case "phasegate:lint": {
282
315
  const lintResult = await this.ports.biomeLintPort.runLint();
283
316
  if (lintResult.passed) {
284
317
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 });
285
- return { status: 'pass', errors: [], summary: r.summary, data: undefined, exitCode: 0 };
318
+ return { status: "pass", errors: [], summary: r.summary, data: undefined, exitCode: 0 };
286
319
  }
287
- const errors = lintResult.errors.length > 0 ? lintResult.errors : [makeError('Lint failed')];
320
+ const errors = lintResult.errors.length > 0 ? lintResult.errors : [makeError("Lint failed")];
288
321
  const r = HarnessApiResponse.fail(errors, { ...summary, failed: 1 });
289
- return { status: 'fail', errors: r.errors, summary: r.summary, data: undefined, exitCode: 1 };
322
+ return { status: "fail", errors: r.errors, summary: r.summary, data: undefined, exitCode: 1 };
290
323
  }
291
324
 
292
- case 'phasegate:complete-check': {
325
+ case "phasegate:complete-check": {
293
326
  const [validatorResults, lintResult] = await Promise.all([
294
327
  this.ports.validatorExecutionPort.runAllValidators(),
295
328
  this.ports.biomeLintPort.runLint(),
296
329
  ]);
297
330
  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`)];
331
+ let warningCount = 0;
332
+ // WI-318 (github#38): ci-check case と同一の CiCheckResult.fromResults() を通して
333
+ // ADR-017 / WI-260 severity-aware 集約(isEffectivelyPassed)を適用する。
334
+ // これにより warning-only failure(例: L2-016 の ungated-legacy marker)が
335
+ // Stop hook の complete-check 経路でも validate --layer L2 / ci-check と同じく
336
+ // 実質 pass となる。lint 結果の合流(lint fail → exit 1)は従来どおり維持。
337
+ // WI-321 (github#38 残課題): warning が summary の件数のみで内容不明だったため、
338
+ // ci-check case と同じく CiCheckResult を data ペイロードとして pass/fail 双方で返す。
339
+ // validatorResults が空の場合のみ従来どおり data なし。
340
+ let result: CiCheckResult | undefined;
341
+ if (validatorResults.length > 0) {
342
+ result = CiCheckResult.fromResults(validatorResults);
343
+ warningCount = result.collectAllErrors().filter((e) => e.severity === "warning").length;
344
+ for (const v of result.getFailedValidators()) {
345
+ const errs =
346
+ v.errors !== undefined && v.errors.length > 0
347
+ ? v.errors
348
+ : [makeError(`Validator ${v.validatorId} failed`)];
301
349
  allErrors.push(...errs);
302
350
  }
303
351
  }
304
352
  if (!lintResult.passed) {
305
- const errs = lintResult.errors.length > 0 ? lintResult.errors : [makeError('Lint failed')];
353
+ const errs = lintResult.errors.length > 0 ? lintResult.errors : [makeError("Lint failed")];
306
354
  allErrors.push(...errs);
307
355
  }
308
356
  if (allErrors.length === 0) {
309
- const r = HarnessApiResponse.pass({ ...summary, passed: 1 });
310
- return { status: 'pass', errors: [], summary: r.summary, data: undefined, exitCode: 0 };
357
+ const r = HarnessApiResponse.pass({ ...summary, passed: 1, warnings: warningCount }, result);
358
+ return { status: "pass", errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
311
359
  }
312
- const r = HarnessApiResponse.fail(allErrors, { ...summary, failed: 1 });
313
- return { status: 'fail', errors: r.errors, summary: r.summary, data: undefined, exitCode: 1 };
360
+ const r = HarnessApiResponse.fail(allErrors, { ...summary, failed: 1, warnings: warningCount }, result);
361
+ return { status: "fail", errors: r.errors, summary: r.summary, data: result as unknown as T, exitCode: 1 };
314
362
  }
315
363
 
316
- case 'phasegate:impact-analysis': {
317
- const storyId = args.storyId ?? '';
364
+ case "phasegate:impact-analysis": {
365
+ const storyId = args.storyId ?? "";
318
366
  const result = await this.ports.impactAnalysisPort.analyze(storyId);
319
367
  if (result === null) {
320
368
  const errors = [makeError(`Story '${storyId}' not found`)];
321
369
  const r = HarnessApiResponse.fail(errors, { ...summary, failed: 1 });
322
- return { status: 'fail', errors: r.errors, summary: r.summary, data: undefined, exitCode: 1 };
370
+ return { status: "fail", errors: r.errors, summary: r.summary, data: undefined, exitCode: 1 };
323
371
  }
324
372
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, result);
325
- return { status: 'pass', errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
373
+ return { status: "pass", errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
326
374
  }
327
375
 
328
376
  default:
@@ -1,17 +1,27 @@
1
1
  // @layer domain
2
2
  // @unit harness-api
3
3
  // @work-item-id WI-112
4
+ // @work-item-id WI-328
4
5
  // status-derivation-service.ts — StatusDerivationService Domain Service
5
6
 
6
- import { LayerHealth, type LayerId } from '../value-objects/layer-health.js';
7
- import { HarnessStatusSummary, type PresetInfo, type ConfigSummary, type PhaseGateSummary, type HookHealth, type BaselineHealth, type OperationalWarning } from '../value-objects/harness-status-summary.js';
8
- import type { ArtifactScanResult, ArtifactPresence } from '../value-objects/artifact-scan-result.js';
9
-
10
- const ALL_LAYER_IDS: readonly LayerId[] = ['L1', 'L2', 'L3', 'L4'];
7
+ import type { ArtifactPresence, ArtifactScanResult } from "../value-objects/artifact-scan-result.js";
8
+ import {
9
+ type BaselineHealth,
10
+ type ConfigSummary,
11
+ HarnessStatusSummary,
12
+ type HookHealth,
13
+ type LanguageInfo,
14
+ type OperationalWarning,
15
+ type PhaseGateSummary,
16
+ type PresetInfo,
17
+ } from "../value-objects/harness-status-summary.js";
18
+ import { LayerHealth, type LayerId } from "../value-objects/layer-health.js";
19
+
20
+ const ALL_LAYER_IDS: readonly LayerId[] = ["L1", "L2", "L3", "L4"];
11
21
 
12
22
  function getLayerId(artifact: ArtifactPresence): LayerId | null {
13
23
  const id = artifact.layer ?? artifact.layerId;
14
- if (id === 'L1' || id === 'L2' || id === 'L3' || id === 'L4') return id;
24
+ if (id === "L1" || id === "L2" || id === "L3" || id === "L4") return id;
15
25
  return null;
16
26
  }
17
27
 
@@ -22,7 +32,7 @@ export class StatusDerivationService {
22
32
  deriveLayerHealth(scanResult: ArtifactScanResult, layerId: LayerId): LayerHealth {
23
33
  const layerArtifacts = scanResult.foundArtifacts.filter((a) => getLayerId(a) === layerId);
24
34
  const hasPresent = layerArtifacts.some((a) => a.present === true);
25
- const lastResult = hasPresent ? 'pass' : 'unknown';
35
+ const lastResult = hasPresent ? "pass" : "unknown";
26
36
  return LayerHealth.create({ layerId, enabled: true, lastResult });
27
37
  }
28
38
 
@@ -34,7 +44,7 @@ export class StatusDerivationService {
34
44
  config: { layers?: Record<string, { enabled: boolean }> },
35
45
  presetInfo?: PresetInfo,
36
46
  configSummary?: ConfigSummary,
37
- phaseGateSummary?: PhaseGateSummary
47
+ phaseGateSummary?: PhaseGateSummary,
38
48
  ): HarnessStatusSummary {
39
49
  const derivedLayers = scanResult.derivedLayerHealth;
40
50
 
@@ -53,19 +63,19 @@ export class StatusDerivationService {
53
63
  // Derive from scan result
54
64
  const layerArtifacts = scanResult.foundArtifacts.filter((a) => getLayerId(a) === layerId);
55
65
  const hasPresent = layerArtifacts.some((a) => a.present === true);
56
- const lastResult = enabled ? (hasPresent ? 'pass' : 'unknown') : undefined;
66
+ const lastResult = enabled ? (hasPresent ? "pass" : "unknown") : undefined;
57
67
  return LayerHealth.create({ layerId, enabled, lastResult });
58
68
  });
59
69
 
60
70
  const effectivePresetInfo: PresetInfo = presetInfo ?? {
61
- name: 'standard',
62
- enabledLayers: ['L1', 'L2', 'L3'],
71
+ name: "standard",
72
+ enabledLayers: ["L1", "L2", "L3"],
63
73
  };
64
74
 
65
75
  const effectiveConfigSummary: ConfigSummary = configSummary ?? {
66
- configPath: 'phasegate.config.json',
76
+ configPath: "phasegate.config.json",
67
77
  lastModified: new Date().toISOString(),
68
- version: '2',
78
+ version: "2",
69
79
  };
70
80
 
71
81
  const effectivePhaseGateSummary: PhaseGateSummary = phaseGateSummary ?? {
@@ -90,7 +100,8 @@ export class StatusDerivationService {
90
100
  presetInfo: PresetInfo;
91
101
  configSummary: ConfigSummary;
92
102
  phaseGateSummary: PhaseGateSummary;
93
- liveValidationByLayer?: Partial<Record<LayerId, 'pass' | 'fail' | 'skipped' | 'not-run' | 'error'>>;
103
+ liveValidationByLayer?: Partial<Record<LayerId, "pass" | "fail" | "skipped" | "not-run" | "error">>;
104
+ languages?: LanguageInfo;
94
105
  hookHealth?: HookHealth;
95
106
  baselineHealth?: BaselineHealth;
96
107
  operationalWarnings?: readonly OperationalWarning[];
@@ -99,21 +110,19 @@ export class StatusDerivationService {
99
110
 
100
111
  const layers: LayerHealth[] = ALL_LAYER_IDS.map((layerId) => {
101
112
  const enabled = presetInfo.enabledLayers.includes(layerId);
102
- const configurationState = enabled ? 'enabled' : 'disabled';
113
+ const configurationState = enabled ? "enabled" : "disabled";
103
114
 
104
115
  // Find from derivedLayerHealth
105
116
  const existing = scanResult.derivedLayerHealth.find((l) => l.layerId === layerId);
106
117
  const layerArtifacts = scanResult.foundArtifacts.filter((a) => getLayerId(a) === layerId);
107
118
  const hasPresent = layerArtifacts.some((a) => a.present === true);
108
- const cachedArtifactState = hasPresent ? 'present' : 'missing';
109
- const liveValidationState = liveValidationByLayer?.[layerId] ?? 'not-run';
119
+ const cachedArtifactState = hasPresent ? "present" : "missing";
120
+ const liveValidationState = liveValidationByLayer?.[layerId] ?? "not-run";
110
121
  const liveLastResult =
111
- liveValidationState === 'pass' || liveValidationState === 'fail'
112
- ? liveValidationState
113
- : undefined;
122
+ liveValidationState === "pass" || liveValidationState === "fail" ? liveValidationState : undefined;
114
123
 
115
124
  if (enabled) {
116
- const lastResult = liveLastResult ?? existing?.lastResult ?? (hasPresent ? 'pass' : 'unknown');
125
+ const lastResult = liveLastResult ?? existing?.lastResult ?? (hasPresent ? "pass" : "unknown");
117
126
  return LayerHealth.create({
118
127
  layerId,
119
128
  enabled,
@@ -139,6 +148,7 @@ export class StatusDerivationService {
139
148
  phaseGateSummary,
140
149
  presetInfo,
141
150
  configSummary,
151
+ languages: input.languages,
142
152
  hookHealth: input.hookHealth,
143
153
  baselineHealth: input.baselineHealth,
144
154
  operationalWarnings: input.operationalWarnings,
@@ -1,8 +1,10 @@
1
1
  // @layer domain
2
2
  // @unit harness-api
3
3
  // @work-item-id WI-307
4
+ // @work-item-id WI-332
4
5
  // ci-check-result.ts — CiCheckResult Value Object
5
6
 
7
+ import { isEffectivelyPassed } from "../../../validator-system/domain/services/effective-severity-policy.js";
6
8
  import type { HarnessError } from "./harness-api-response.js";
7
9
 
8
10
  export interface ValidatorCheckItem {
@@ -19,28 +21,6 @@ export interface CiCheckResultProps {
19
21
  failOnWarning?: boolean;
20
22
  }
21
23
 
22
- /**
23
- * WI-260 / ADR-017: validator 1件が「実質 pass」か判定する severity-aware ルール。
24
- * `validate` 経路の AggregateValidationResultsUseCase と同一の判定式を harness-api の
25
- * domain VO 側に持たせ、ci-check と validate の集約挙動を一致させる(レイヤー越境回避のため
26
- * usecase を直接 import せず判定ロジックを共有)。
27
- *
28
- * - skipped: 実質 pass
29
- * - passed=true: 実質 pass
30
- * - passed=false かつ error severity(!= warning)を含む: fail
31
- * - passed=false かつ errors=[](severity 判定不能): 安全側に倒して fail
32
- * - passed=false かつ warning のみ: failOnWarning=false(既定)で実質 pass、true で fail
33
- */
34
- function isEffectivelyPassed(item: ValidatorCheckItem, failOnWarning: boolean): boolean {
35
- if (item.skipped || item.passed) return true;
36
- const errors = item.errors ?? [];
37
- const hasNonWarningError = errors.some((e) => e.severity !== "warning");
38
- const hasWarnings = errors.some((e) => e.severity === "warning");
39
- const isEmptyFail = errors.length === 0;
40
- const hasFail = isEmptyFail || hasNonWarningError || (failOnWarning && hasWarnings);
41
- return !hasFail;
42
- }
43
-
44
24
  /**
45
25
  * WI-307: public ci-check projection の `passed` を aggregate policy と一致させる。
46
26
  * skipped は独立した green state のため raw passed を維持し、warning-only failure だけを