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.
- package/CHANGELOG.md +60 -0
- package/README.ja.md +6 -3
- package/README.md +6 -3
- package/docs/contracts/attestation-v2.schema.json +110 -0
- package/docs/guide/cli-reference.md +12 -1
- package/docs/guide/configuration.md +56 -0
- package/docs/guide/installation.md +1 -1
- package/docs/templates/ci/aidlc-gate.yml +60 -2
- package/package.json +2 -2
- package/scripts/harness/agent-integration/application/dto/open-world-obligations-context-dto.ts +30 -0
- package/scripts/harness/agent-integration/application/ports/world-obligations-query-port.ts +30 -0
- package/scripts/harness/agent-integration/application/usecases/get-open-world-obligations-context-usecase.ts +66 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +30 -27
- package/scripts/harness/agent-integration/infrastructure/adapters/world-model-open-obligations-query-adapter.ts +58 -0
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
- package/scripts/harness/agent-integration/presentation/session-start-hook.ts +37 -1
- package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
- package/scripts/harness/agent-integration/presentation/world-obligations-session-context.ts +60 -0
- package/scripts/harness/attestation/application/dto/attestation-document.ts +17 -4
- package/scripts/harness/attestation/application/mappers/attestation-record-mapper.ts +55 -4
- package/scripts/harness/attestation/application/ports/world-snapshot-root-provider.ts +10 -0
- package/scripts/harness/attestation/application/usecases/produce-attestation-usecase.ts +33 -7
- package/scripts/harness/attestation/composition-root.ts +5 -0
- package/scripts/harness/attestation/domain/entities/attestation-record.ts +37 -2
- package/scripts/harness/attestation/index.ts +7 -1
- package/scripts/harness/attestation/presentation/handlers/attest-handler.ts +5 -2
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +40 -9
- package/scripts/harness/config-foundation/application/mappers/world-model-config-mapper.ts +15 -0
- package/scripts/harness/config-foundation/domain/harness-config.ts +70 -87
- package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +80 -88
- package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
- package/scripts/harness/config-foundation/domain/value-objects/world-config.ts +198 -0
- package/scripts/harness/config-foundation/index.ts +14 -15
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +24 -0
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +24 -0
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +24 -0
- 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 +84 -8
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +84 -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/ci-check-result.ts +18 -4
- 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/integrations/pre-commit.ts +169 -27
- package/scripts/harness/main.ts +200 -136
- package/scripts/harness/traceability-model/application/dto/changed-design-fragment-dto.ts +31 -0
- package/scripts/harness/traceability-model/application/facades/design-change-read-facade.ts +14 -0
- package/scripts/harness/traceability-model/application/ports/staged-design-change-source-port.ts +9 -0
- package/scripts/harness/traceability-model/composition-root.ts +5 -0
- package/scripts/harness/traceability-model/index.ts +9 -0
- package/scripts/harness/traceability-model/infrastructure/adapters/git-staged-design-change-adapter.ts +155 -0
- package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +31 -0
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +57 -6
- package/scripts/harness/validator-system/composition-root.ts +25 -7
- package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
- package/scripts/harness/validator-system/domain/ports/world-constraint-admission-policy-port.ts +28 -0
- package/scripts/harness/validator-system/domain/ports/world-constraint-rederivation-policy-port.ts +11 -0
- package/scripts/harness/validator-system/domain/services/design-change-declaration-policy.ts +75 -0
- package/scripts/harness/validator-system/domain/services/world-constraint-admission-service.ts +78 -0
- package/scripts/harness/validator-system/domain/services/world-constraint-rederivation-service.ts +76 -0
- package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +4 -0
- package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +126 -18
- package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +49 -20
- package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-admission-adapter.ts +56 -0
- package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-rederivation-adapter.ts +56 -0
- package/scripts/harness/world-model/application/dto/pinned-design-endpoint-dto.ts +19 -0
- package/scripts/harness/world-model/application/dto/world-resolved-config-input.ts +92 -27
- package/scripts/harness/world-model/application/dto/world-snapshot-root-dto.ts +8 -0
- package/scripts/harness/world-model/application/facades/pinned-design-endpoint-facade.ts +63 -0
- package/scripts/harness/world-model/application/facades/world-snapshot-root-facade.ts +19 -0
- package/scripts/harness/world-model/application/usecases/derive-world-obligations-use-case.ts +3 -1
- package/scripts/harness/world-model/composition-root.ts +62 -33
- package/scripts/harness/world-model/index.ts +11 -0
- package/scripts/harness/world-model/infrastructure/adapters/attestation-fact-extractor.ts +48 -13
- package/scripts/harness/world-model/infrastructure/adapters/file-system-world-control-repository-adapters.ts +6 -4
- 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 {
|
|
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:
|
|
@@ -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,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(
|
|
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[] {
|