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.
- package/CHANGELOG.md +54 -0
- package/docs/ADR/017-warning-severity-aggregation.md +17 -0
- package/docs/ADR/038-config-state-operation-permission-policy.md +78 -0
- package/docs/guide/installation.md +1 -1
- package/docs/guide/layer-model.md +2 -0
- package/docs/guide/quick-vs-full-mode.md +28 -3
- package/docs/guide/troubleshooting.md +37 -0
- package/docs/templates/agent-context/CLAUDE.md.template.md +6 -6
- package/docs/templates/ci/aidlc-gate.yml +22 -4
- package/package.json +2 -2
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +98 -18
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +25 -3
- package/scripts/harness/agent-integration/infrastructure/adapters/file-system-full-mode-session-query-adapter.ts +75 -23
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +45 -27
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +51 -6
- package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
- package/scripts/harness/ci-governance/composition-root.ts +2 -2
- package/scripts/harness/ci-governance/domain/services/claude-md-composer.ts +20 -11
- package/scripts/harness/ci-governance/presentation/handlers/check-repetition-handler.ts +10 -2
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +5 -1
- package/scripts/harness/config-foundation/domain/harness-config.ts +10 -7
- package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +4 -1
- package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +27 -18
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +1 -8
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +1 -8
- package/scripts/harness/harness-api/domain/ports/config-query-port.ts +11 -1
- package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +135 -87
- package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +31 -21
- package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +2 -22
- package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +23 -8
- package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +4 -4
- package/scripts/harness/harness-api/infrastructure/adapters/harness-config-query-adapter.ts +79 -34
- package/scripts/harness/harness-error/application/dto/create-harness-error-input.ts +3 -1
- package/scripts/harness/harness-error/application/dto/harness-error-contract.ts +3 -1
- package/scripts/harness/harness-error/application/mappers/harness-error-contract-mapper.ts +9 -17
- package/scripts/harness/harness-error/application/usecases/create-harness-error-use-case.ts +7 -7
- package/scripts/harness/harness-error/domain/services/harness-error-factory.ts +30 -33
- package/scripts/harness/harness-error/domain/value-objects/error-definition.ts +28 -17
- package/scripts/harness/harness-error/domain/value-objects/harness-error.ts +30 -9
- package/scripts/harness/harness-error/domain/value-objects/remediation-type.ts +30 -0
- package/scripts/harness/harness-error/infrastructure/registry/l2-error-definitions.ts +44 -29
- package/scripts/harness/harness-error/infrastructure/registry/l3-error-definitions.ts +44 -27
- package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +47 -32
- package/scripts/harness/installation/application/checks/claude-context-missing-check.ts +13 -7
- package/scripts/harness/installation/application/checks/config-status-check.ts +52 -0
- package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +6 -0
- package/scripts/harness/installation/application/ports/config-status-probe-port.ts +9 -0
- package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +30 -8
- package/scripts/harness/installation/application/usecases/run-install.ts +234 -53
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +311 -70
- package/scripts/harness/installation/composition-root.ts +13 -3
- package/scripts/harness/installation/domain/check-id.ts +2 -0
- package/scripts/harness/installation/domain/config-status.ts +17 -0
- package/scripts/harness/installation/domain/deployment-manifest.ts +43 -0
- package/scripts/harness/installation/domain/ports/heuristic-check.ts +10 -1
- package/scripts/harness/installation/infrastructure/adapters/config-status-probe-adapter.ts +79 -0
- package/scripts/harness/installation/presentation/cli/doctor-handler.ts +6 -1
- package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +19 -5
- package/scripts/harness/integrations/pre-commit.ts +17 -3
- package/scripts/harness/main.ts +83 -15
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +60 -32
- package/scripts/harness/phase2-extensions/presentation/handlers/check-freshness-handler.ts +17 -9
- package/scripts/harness/quick-mode/application/ports/file-existence-port.ts +15 -0
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +60 -22
- package/scripts/harness/quick-mode/composition-root.ts +25 -15
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +72 -3
- package/scripts/harness/quick-mode/infrastructure/adapters/fs-file-existence-adapter.ts +38 -0
- package/scripts/harness/skill-quality/infrastructure/adapters/file-system-requirement-test-matrix-adapter.ts +51 -8
- package/scripts/harness/skill-quality/presentation/handlers/check-coverage-handler.ts +13 -6
- package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +5 -1
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-identity-gateway.ts +6 -1
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +15 -2
- package/scripts/harness/validator-system/application/use-cases/aggregate-validation-results-usecase.ts +11 -14
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +32 -7
- package/scripts/harness/validator-system/composition-root.ts +4 -1
- package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
- package/scripts/harness/validator-system/domain/services/effective-severity-policy.ts +39 -0
- package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +6 -4
- package/scripts/harness/validator-system/domain/value-objects/drift-report.ts +12 -7
- package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +11 -3
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +20 -17
- package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +89 -3
- package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +49 -20
- package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +35 -14
- package/scripts/harness/validator-system/infrastructure/adapters/traceability-metadata-policy-adapter.ts +13 -8
- package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +19 -10
- 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 {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
13
|
-
import type
|
|
14
|
-
import
|
|
15
|
-
import type
|
|
16
|
-
import type {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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:
|
|
51
|
+
return { code: "HARNESS_ERROR", severity: "error", message };
|
|
46
52
|
}
|
|
47
53
|
|
|
48
|
-
type LiveValidationState =
|
|
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 ===
|
|
58
|
+
return prefix === "L1" || prefix === "L2" || prefix === "L3" || prefix === "L4" ? prefix : null;
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
function summarizeLayerResults(
|
|
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 [
|
|
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] =
|
|
69
|
+
result[layerId] = "skipped";
|
|
62
70
|
} else if (layerItems.some((item) => !item.passed && item.skipped !== true)) {
|
|
63
|
-
result[layerId] =
|
|
71
|
+
result[layerId] = "fail";
|
|
64
72
|
} else {
|
|
65
|
-
result[layerId] =
|
|
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 ===
|
|
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 =
|
|
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:
|
|
107
|
+
code: "HOOK_SKIP_OBSERVED",
|
|
92
108
|
message: `Hook skip events observed: ${skipCount}`,
|
|
93
|
-
nextAction:
|
|
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:
|
|
114
|
+
code: "BASELINE_SHA_MISMATCH",
|
|
99
115
|
message: `${baselineHealth.shaMismatchCount} baseline files changed since the snapshot.`,
|
|
100
|
-
nextAction:
|
|
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:
|
|
121
|
+
code: "BASELINE_DEBT_HIGH",
|
|
106
122
|
message: `Baseline grandfather debt remains high: ${baselineHealth.grandfatheredFileCount} files.`,
|
|
107
|
-
nextAction:
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
209
|
+
return { status: "fail", errors: r.errors, summary: r.summary, data: result as unknown as T, exitCode: 1 };
|
|
194
210
|
}
|
|
195
211
|
|
|
196
|
-
case
|
|
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:
|
|
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:
|
|
221
|
+
return { status: "pass", errors: [], summary: r.summary, data: phaseInfo as unknown as T, exitCode: 0 };
|
|
206
222
|
}
|
|
207
223
|
|
|
208
|
-
case
|
|
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:
|
|
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 =
|
|
217
|
-
|
|
218
|
-
|
|
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:
|
|
237
|
+
return { status: "fail", errors: r.errors, summary: r.summary, data: result as unknown as T, exitCode: 1 };
|
|
221
238
|
}
|
|
222
239
|
|
|
223
|
-
case
|
|
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:
|
|
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:
|
|
248
|
+
return { status: "pass", errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
|
|
232
249
|
}
|
|
233
250
|
|
|
234
|
-
case
|
|
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 ?
|
|
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 ??
|
|
262
|
+
liveValidationByLayer.L1 = liveValidationByLayer.L1 ?? "error";
|
|
246
263
|
}
|
|
247
|
-
let presetInfo = {
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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:
|
|
311
|
+
return { status: "pass", errors: [], summary: r.summary, data: statusSummary as unknown as T, exitCode: 0 };
|
|
279
312
|
}
|
|
280
313
|
|
|
281
|
-
case
|
|
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:
|
|
318
|
+
return { status: "pass", errors: [], summary: r.summary, data: undefined, exitCode: 0 };
|
|
286
319
|
}
|
|
287
|
-
const errors = lintResult.errors.length > 0 ? lintResult.errors : [makeError(
|
|
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:
|
|
322
|
+
return { status: "fail", errors: r.errors, summary: r.summary, data: undefined, exitCode: 1 };
|
|
290
323
|
}
|
|
291
324
|
|
|
292
|
-
case
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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 {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 ===
|
|
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 ?
|
|
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 ?
|
|
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:
|
|
62
|
-
enabledLayers: [
|
|
71
|
+
name: "standard",
|
|
72
|
+
enabledLayers: ["L1", "L2", "L3"],
|
|
63
73
|
};
|
|
64
74
|
|
|
65
75
|
const effectiveConfigSummary: ConfigSummary = configSummary ?? {
|
|
66
|
-
configPath:
|
|
76
|
+
configPath: "phasegate.config.json",
|
|
67
77
|
lastModified: new Date().toISOString(),
|
|
68
|
-
version:
|
|
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,
|
|
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 ?
|
|
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 ?
|
|
109
|
-
const liveValidationState = liveValidationByLayer?.[layerId] ??
|
|
119
|
+
const cachedArtifactState = hasPresent ? "present" : "missing";
|
|
120
|
+
const liveValidationState = liveValidationByLayer?.[layerId] ?? "not-run";
|
|
110
121
|
const liveLastResult =
|
|
111
|
-
liveValidationState ===
|
|
112
|
-
? liveValidationState
|
|
113
|
-
: undefined;
|
|
122
|
+
liveValidationState === "pass" || liveValidationState === "fail" ? liveValidationState : undefined;
|
|
114
123
|
|
|
115
124
|
if (enabled) {
|
|
116
|
-
const lastResult = liveLastResult ?? existing?.lastResult ?? (hasPresent ?
|
|
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 だけを
|