phasegate 0.264.0 → 0.283.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/docs/guide/installation.md +1 -1
- package/docs/templates/ci/aidlc-gate.yml +22 -4
- package/package.json +2 -2
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +30 -27
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
- package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
- 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/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 +125 -86
- package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +31 -21
- 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/installation/application/usecases/run-install.ts +199 -51
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +277 -69
- package/scripts/harness/installation/domain/deployment-manifest.ts +43 -0
- package/scripts/harness/main.ts +26 -6
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +24 -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/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
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
* @layer infrastructure
|
|
3
3
|
* @unit config-foundation
|
|
4
4
|
*/
|
|
5
|
-
import * as fs from
|
|
6
|
-
import path from
|
|
7
|
-
import type { ConfigRepositoryPort } from
|
|
5
|
+
import * as fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import type { ConfigRepositoryPort } from "../../domain/ports/config-repository-port.js";
|
|
8
8
|
|
|
9
|
-
const DEFAULT_CONFIG_FILE_NAME =
|
|
10
|
-
const PERSONAL_CONFIG_PATH = path.join(
|
|
9
|
+
const DEFAULT_CONFIG_FILE_NAME = "phasegate.config.json";
|
|
10
|
+
const PERSONAL_CONFIG_PATH = path.join(".phasegate-local", DEFAULT_CONFIG_FILE_NAME);
|
|
11
11
|
|
|
12
12
|
export class ConfigNotFoundError extends Error {
|
|
13
13
|
readonly configPath: string;
|
|
14
14
|
|
|
15
15
|
constructor(configPath: string) {
|
|
16
16
|
super(`Config file not found: ${configPath}`);
|
|
17
|
-
this.name =
|
|
17
|
+
this.name = "ConfigNotFoundError";
|
|
18
18
|
this.configPath = configPath;
|
|
19
19
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
20
20
|
}
|
|
@@ -24,15 +24,28 @@ export class ConfigPersistenceError extends Error {
|
|
|
24
24
|
readonly configPath: string;
|
|
25
25
|
readonly cause?: unknown;
|
|
26
26
|
|
|
27
|
-
constructor(configPath: string, cause?: unknown) {
|
|
28
|
-
super(`Failed to persist config: ${configPath}`);
|
|
29
|
-
this.name =
|
|
27
|
+
constructor(configPath: string, cause?: unknown, message?: string) {
|
|
28
|
+
super(message ?? `Failed to persist config: ${configPath}`);
|
|
29
|
+
this.name = "ConfigPersistenceError";
|
|
30
30
|
this.configPath = configPath;
|
|
31
31
|
this.cause = cause;
|
|
32
32
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* WI-325: load 経路の JSON パース失敗専用エラー。
|
|
38
|
+
* ConfigPersistenceError のサブクラスなので、既存の
|
|
39
|
+
* `instanceof ConfigPersistenceError` fail-open 判定(main.ts / WI-314)は無変更で動く。
|
|
40
|
+
*/
|
|
41
|
+
export class ConfigParseError extends ConfigPersistenceError {
|
|
42
|
+
constructor(configPath: string, cause?: unknown) {
|
|
43
|
+
super(configPath, cause, `Failed to parse config JSON: ${configPath}`);
|
|
44
|
+
this.name = "ConfigParseError";
|
|
45
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
async function exists(targetPath: string): Promise<boolean> {
|
|
37
50
|
try {
|
|
38
51
|
await fs.access(targetPath);
|
|
@@ -69,14 +82,10 @@ async function findNearestConfig(startDirectory: string): Promise<string | null>
|
|
|
69
82
|
|
|
70
83
|
export class FileSystemConfigRepository implements ConfigRepositoryPort {
|
|
71
84
|
async load(configPath?: string): Promise<{ path: string; document: unknown }> {
|
|
72
|
-
const resolvedPath = configPath
|
|
73
|
-
? path.resolve(configPath)
|
|
74
|
-
: await findNearestConfig(process.cwd());
|
|
85
|
+
const resolvedPath = configPath ? path.resolve(configPath) : await findNearestConfig(process.cwd());
|
|
75
86
|
|
|
76
87
|
if (!resolvedPath) {
|
|
77
|
-
throw new ConfigNotFoundError(
|
|
78
|
-
path.resolve(process.cwd(), DEFAULT_CONFIG_FILE_NAME),
|
|
79
|
-
);
|
|
88
|
+
throw new ConfigNotFoundError(path.resolve(process.cwd(), DEFAULT_CONFIG_FILE_NAME));
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
if (!(await exists(resolvedPath))) {
|
|
@@ -84,7 +93,7 @@ export class FileSystemConfigRepository implements ConfigRepositoryPort {
|
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
try {
|
|
87
|
-
const raw = await fs.readFile(resolvedPath,
|
|
96
|
+
const raw = await fs.readFile(resolvedPath, "utf8");
|
|
88
97
|
|
|
89
98
|
return {
|
|
90
99
|
path: resolvedPath,
|
|
@@ -92,7 +101,7 @@ export class FileSystemConfigRepository implements ConfigRepositoryPort {
|
|
|
92
101
|
};
|
|
93
102
|
} catch (error) {
|
|
94
103
|
if (error instanceof SyntaxError) {
|
|
95
|
-
throw new
|
|
104
|
+
throw new ConfigParseError(resolvedPath, error);
|
|
96
105
|
}
|
|
97
106
|
|
|
98
107
|
throw error;
|
|
@@ -104,7 +113,7 @@ export class FileSystemConfigRepository implements ConfigRepositoryPort {
|
|
|
104
113
|
|
|
105
114
|
try {
|
|
106
115
|
const serialized = `${JSON.stringify(document, null, 2)}\n`;
|
|
107
|
-
await fs.writeFile(resolvedPath, serialized,
|
|
116
|
+
await fs.writeFile(resolvedPath, serialized, "utf8");
|
|
108
117
|
} catch (error) {
|
|
109
118
|
throw new ConfigPersistenceError(resolvedPath, error);
|
|
110
119
|
}
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
// @layer domain
|
|
2
2
|
// config-query-port.ts
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
ConfigSummary,
|
|
6
|
+
LanguageInfo,
|
|
7
|
+
PhaseGateSummary,
|
|
8
|
+
PresetInfo,
|
|
9
|
+
} from "../value-objects/harness-status-summary.js";
|
|
5
10
|
|
|
6
11
|
export interface ConfigQueryPort {
|
|
7
12
|
getPresetInfo(): Promise<PresetInfo>;
|
|
8
13
|
getConfigSummary(): Promise<ConfigSummary>;
|
|
9
14
|
getPhaseGateSummary(): Promise<PhaseGateSummary>;
|
|
15
|
+
/**
|
|
16
|
+
* WI-328: 実効言語リストと出所(declared / detected / fallback)を返す。
|
|
17
|
+
* 後方互換のため optional — 未実装アダプタでは status に languages を出さない。
|
|
18
|
+
*/
|
|
19
|
+
getLanguageInfo?(): Promise<LanguageInfo>;
|
|
10
20
|
}
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
// @layer domain
|
|
2
2
|
// @unit harness-api
|
|
3
|
-
// @work-item-id WI-108 / WI-114, WI-186
|
|
3
|
+
// @work-item-id WI-108 / WI-114, WI-186, WI-318, WI-321, WI-328
|
|
4
4
|
// command-dispatch-service.ts — CommandDispatchService Domain Service
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
13
|
-
import
|
|
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 } 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:
|
|
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;
|
|
@@ -74,7 +82,7 @@ function hasEnabledLiveFailure(
|
|
|
74
82
|
): boolean {
|
|
75
83
|
return enabledLayers.some((layerId) => {
|
|
76
84
|
const liveState = liveValidationByLayer[layerId];
|
|
77
|
-
return liveState ===
|
|
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 =
|
|
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:
|
|
98
|
+
code: "HOOK_SKIP_OBSERVED",
|
|
92
99
|
message: `Hook skip events observed: ${skipCount}`,
|
|
93
|
-
nextAction:
|
|
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:
|
|
105
|
+
code: "BASELINE_SHA_MISMATCH",
|
|
99
106
|
message: `${baselineHealth.shaMismatchCount} baseline files changed since the snapshot.`,
|
|
100
|
-
nextAction:
|
|
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:
|
|
112
|
+
code: "BASELINE_DEBT_HIGH",
|
|
106
113
|
message: `Baseline grandfather debt remains high: ${baselineHealth.grandfatheredFileCount} files.`,
|
|
107
|
-
nextAction:
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
200
|
+
return { status: "fail", errors: r.errors, summary: r.summary, data: result as unknown as T, exitCode: 1 };
|
|
194
201
|
}
|
|
195
202
|
|
|
196
|
-
case
|
|
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:
|
|
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:
|
|
212
|
+
return { status: "pass", errors: [], summary: r.summary, data: phaseInfo as unknown as T, exitCode: 0 };
|
|
206
213
|
}
|
|
207
214
|
|
|
208
|
-
case
|
|
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:
|
|
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 =
|
|
217
|
-
|
|
218
|
-
|
|
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:
|
|
228
|
+
return { status: "fail", errors: r.errors, summary: r.summary, data: result as unknown as T, exitCode: 1 };
|
|
221
229
|
}
|
|
222
230
|
|
|
223
|
-
case
|
|
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:
|
|
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:
|
|
239
|
+
return { status: "pass", errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
|
|
232
240
|
}
|
|
233
241
|
|
|
234
|
-
case
|
|
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 ?
|
|
250
|
+
liveValidationByLayer.L1 = lintResult.passed ? "pass" : "fail";
|
|
243
251
|
Object.assign(liveValidationByLayer, summarizeLayerResults(validatorResults));
|
|
244
252
|
} catch {
|
|
245
|
-
liveValidationByLayer.L1 = liveValidationByLayer.L1 ??
|
|
253
|
+
liveValidationByLayer.L1 = liveValidationByLayer.L1 ?? "error";
|
|
246
254
|
}
|
|
247
|
-
let presetInfo = {
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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:
|
|
302
|
+
return { status: "pass", errors: [], summary: r.summary, data: statusSummary as unknown as T, exitCode: 0 };
|
|
279
303
|
}
|
|
280
304
|
|
|
281
|
-
case
|
|
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:
|
|
309
|
+
return { status: "pass", errors: [], summary: r.summary, data: undefined, exitCode: 0 };
|
|
286
310
|
}
|
|
287
|
-
const errors = lintResult.errors.length > 0 ? lintResult.errors : [makeError(
|
|
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:
|
|
313
|
+
return { status: "fail", errors: r.errors, summary: r.summary, data: undefined, exitCode: 1 };
|
|
290
314
|
}
|
|
291
315
|
|
|
292
|
-
case
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
364
|
+
return { status: "pass", errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
|
|
326
365
|
}
|
|
327
366
|
|
|
328
367
|
default:
|