phasegate 0.254.0 → 0.283.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.ja.md +6 -3
  3. package/README.md +6 -3
  4. package/docs/contracts/attestation-v2.schema.json +110 -0
  5. package/docs/guide/cli-reference.md +12 -1
  6. package/docs/guide/configuration.md +56 -0
  7. package/docs/guide/installation.md +1 -1
  8. package/docs/templates/ci/aidlc-gate.yml +60 -2
  9. package/package.json +2 -2
  10. package/scripts/harness/agent-integration/application/dto/open-world-obligations-context-dto.ts +30 -0
  11. package/scripts/harness/agent-integration/application/ports/world-obligations-query-port.ts +30 -0
  12. package/scripts/harness/agent-integration/application/usecases/get-open-world-obligations-context-usecase.ts +66 -0
  13. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +30 -27
  14. package/scripts/harness/agent-integration/infrastructure/adapters/world-model-open-obligations-query-adapter.ts +58 -0
  15. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
  16. package/scripts/harness/agent-integration/presentation/session-start-hook.ts +37 -1
  17. package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
  18. package/scripts/harness/agent-integration/presentation/world-obligations-session-context.ts +60 -0
  19. package/scripts/harness/attestation/application/dto/attestation-document.ts +17 -4
  20. package/scripts/harness/attestation/application/mappers/attestation-record-mapper.ts +55 -4
  21. package/scripts/harness/attestation/application/ports/world-snapshot-root-provider.ts +10 -0
  22. package/scripts/harness/attestation/application/usecases/produce-attestation-usecase.ts +33 -7
  23. package/scripts/harness/attestation/composition-root.ts +5 -0
  24. package/scripts/harness/attestation/domain/entities/attestation-record.ts +37 -2
  25. package/scripts/harness/attestation/index.ts +7 -1
  26. package/scripts/harness/attestation/presentation/handlers/attest-handler.ts +5 -2
  27. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +40 -9
  28. package/scripts/harness/config-foundation/application/mappers/world-model-config-mapper.ts +15 -0
  29. package/scripts/harness/config-foundation/domain/harness-config.ts +70 -87
  30. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +80 -88
  31. package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
  32. package/scripts/harness/config-foundation/domain/value-objects/world-config.ts +198 -0
  33. package/scripts/harness/config-foundation/index.ts +14 -15
  34. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +24 -0
  35. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +24 -0
  36. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +24 -0
  37. package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +27 -18
  38. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +84 -8
  39. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +84 -8
  40. package/scripts/harness/harness-api/domain/ports/config-query-port.ts +11 -1
  41. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +125 -86
  42. package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +31 -21
  43. package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +18 -4
  44. package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +23 -8
  45. package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +4 -4
  46. package/scripts/harness/harness-api/infrastructure/adapters/harness-config-query-adapter.ts +79 -34
  47. package/scripts/harness/installation/application/usecases/run-install.ts +199 -51
  48. package/scripts/harness/installation/application/usecases/run-reconcile.ts +277 -69
  49. package/scripts/harness/installation/domain/deployment-manifest.ts +43 -0
  50. package/scripts/harness/integrations/pre-commit.ts +169 -27
  51. package/scripts/harness/main.ts +200 -136
  52. package/scripts/harness/traceability-model/application/dto/changed-design-fragment-dto.ts +31 -0
  53. package/scripts/harness/traceability-model/application/facades/design-change-read-facade.ts +14 -0
  54. package/scripts/harness/traceability-model/application/ports/staged-design-change-source-port.ts +9 -0
  55. package/scripts/harness/traceability-model/composition-root.ts +5 -0
  56. package/scripts/harness/traceability-model/index.ts +9 -0
  57. package/scripts/harness/traceability-model/infrastructure/adapters/git-staged-design-change-adapter.ts +155 -0
  58. package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +31 -0
  59. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +57 -6
  60. package/scripts/harness/validator-system/composition-root.ts +25 -7
  61. package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
  62. package/scripts/harness/validator-system/domain/ports/world-constraint-admission-policy-port.ts +28 -0
  63. package/scripts/harness/validator-system/domain/ports/world-constraint-rederivation-policy-port.ts +11 -0
  64. package/scripts/harness/validator-system/domain/services/design-change-declaration-policy.ts +75 -0
  65. package/scripts/harness/validator-system/domain/services/world-constraint-admission-service.ts +78 -0
  66. package/scripts/harness/validator-system/domain/services/world-constraint-rederivation-service.ts +76 -0
  67. package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +4 -0
  68. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +126 -18
  69. package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +49 -20
  70. package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-admission-adapter.ts +56 -0
  71. package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-rederivation-adapter.ts +56 -0
  72. package/scripts/harness/world-model/application/dto/pinned-design-endpoint-dto.ts +19 -0
  73. package/scripts/harness/world-model/application/dto/world-resolved-config-input.ts +92 -27
  74. package/scripts/harness/world-model/application/dto/world-snapshot-root-dto.ts +8 -0
  75. package/scripts/harness/world-model/application/facades/pinned-design-endpoint-facade.ts +63 -0
  76. package/scripts/harness/world-model/application/facades/world-snapshot-root-facade.ts +19 -0
  77. package/scripts/harness/world-model/application/usecases/derive-world-obligations-use-case.ts +3 -1
  78. package/scripts/harness/world-model/composition-root.ts +62 -33
  79. package/scripts/harness/world-model/index.ts +11 -0
  80. package/scripts/harness/world-model/infrastructure/adapters/attestation-fact-extractor.ts +48 -13
  81. package/scripts/harness/world-model/infrastructure/adapters/file-system-world-control-repository-adapters.ts +6 -4
  82. package/scripts/harness/world-model/presentation/cli/world-derive-command-handler.ts +6 -2
@@ -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:
@@ -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,5 +1,6 @@
1
1
  // @layer domain
2
2
  // @unit harness-api
3
+ // @work-item-id WI-307
3
4
  // ci-check-result.ts — CiCheckResult Value Object
4
5
 
5
6
  import type { HarnessError } from "./harness-api-response.js";
@@ -40,12 +41,25 @@ function isEffectivelyPassed(item: ValidatorCheckItem, failOnWarning: boolean):
40
41
  return !hasFail;
41
42
  }
42
43
 
44
+ /**
45
+ * WI-307: public ci-check projection の `passed` を aggregate policy と一致させる。
46
+ * skipped は独立した green state のため raw passed を維持し、warning-only failure だけを
47
+ * failOnWarning=false のとき passed=true に射影する。diagnostic は lossless に保持する。
48
+ */
49
+ function toPublicValidatorResult(item: ValidatorCheckItem, failOnWarning: boolean): ValidatorCheckItem {
50
+ return Object.freeze({
51
+ ...item,
52
+ passed: item.skipped ? item.passed : isEffectivelyPassed(item, failOnWarning),
53
+ errors: item.errors === undefined ? undefined : Object.freeze([...item.errors]),
54
+ });
55
+ }
56
+
43
57
  export class CiCheckResult {
44
58
  readonly validatorResults: readonly ValidatorCheckItem[];
45
59
  readonly allPassed: boolean;
46
60
 
47
- private constructor(validatorResults: readonly ValidatorCheckItem[], allPassed: boolean) {
48
- this.validatorResults = Object.freeze([...validatorResults]);
61
+ private constructor(validatorResults: readonly ValidatorCheckItem[], allPassed: boolean, failOnWarning: boolean) {
62
+ this.validatorResults = Object.freeze(validatorResults.map((item) => toPublicValidatorResult(item, failOnWarning)));
49
63
  this.allPassed = allPassed;
50
64
  Object.freeze(this);
51
65
  }
@@ -63,7 +77,7 @@ export class CiCheckResult {
63
77
  `HarnessApiDomainError: allPassed=${props.allPassed} does not match validatorResults state (computed: ${computedAllPassed})`,
64
78
  );
65
79
  }
66
- return new CiCheckResult(props.validatorResults, props.allPassed);
80
+ return new CiCheckResult(props.validatorResults, props.allPassed, failOnWarning);
67
81
  }
68
82
 
69
83
  static fromResults(validatorResults: readonly ValidatorCheckItem[], failOnWarning = false): CiCheckResult {
@@ -71,7 +85,7 @@ export class CiCheckResult {
71
85
  throw new Error("EmptyValidatorResultsError: validatorResults must have at least one item (INV-5)");
72
86
  }
73
87
  const allPassed = validatorResults.every((r) => isEffectivelyPassed(r, failOnWarning));
74
- return new CiCheckResult(validatorResults, allPassed);
88
+ return new CiCheckResult(validatorResults, allPassed, failOnWarning);
75
89
  }
76
90
 
77
91
  getFailedValidators(): readonly ValidatorCheckItem[] {