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
|
@@ -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,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// @unit harness-api
|
|
3
3
|
// harness-status-summary.ts — HarnessStatusSummary Value Object
|
|
4
4
|
|
|
5
|
-
import type { LayerHealth, LayerId } from
|
|
5
|
+
import type { LayerHealth, LayerId } from "./layer-health.js";
|
|
6
6
|
|
|
7
7
|
export interface PhaseGateSummary {
|
|
8
8
|
totalStories: number;
|
|
@@ -11,7 +11,7 @@ export interface PhaseGateSummary {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export interface PresetInfo {
|
|
14
|
-
name:
|
|
14
|
+
name: "minimal" | "standard" | "strict";
|
|
15
15
|
enabledLayers: LayerId[];
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -21,9 +21,21 @@ export interface ConfigSummary {
|
|
|
21
21
|
version: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* WI-328 (github#39 残課題): 実効言語リストとその出所。
|
|
26
|
+
* source:
|
|
27
|
+
* - 'declared' — config の project.languages 宣言
|
|
28
|
+
* - 'detected' — ファイルシステムマーカーからの自動検出(WI-319)
|
|
29
|
+
* - 'fallback' — 検出ゼロで typescript フォールバック
|
|
30
|
+
*/
|
|
31
|
+
export interface LanguageInfo {
|
|
32
|
+
effective: readonly string[];
|
|
33
|
+
source: "declared" | "detected" | "fallback";
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
export interface HookSkipState {
|
|
25
37
|
hookType: string;
|
|
26
|
-
reason:
|
|
38
|
+
reason: "HOOK_DISABLED" | "TIMEOUT_EXCEEDED" | "REENTRY_DETECTED" | string;
|
|
27
39
|
targetPaths: readonly string[];
|
|
28
40
|
observedAt: string;
|
|
29
41
|
}
|
|
@@ -35,7 +47,7 @@ export interface HookHealth {
|
|
|
35
47
|
skipCountsByReason: Readonly<Record<string, number>>;
|
|
36
48
|
applyPatchBypass: {
|
|
37
49
|
nativeApplyPatchIntercepted: boolean;
|
|
38
|
-
backstop:
|
|
50
|
+
backstop: "pre-commit";
|
|
39
51
|
documentationUrl: string;
|
|
40
52
|
};
|
|
41
53
|
}
|
|
@@ -60,18 +72,20 @@ export interface HarnessStatusSummaryProps {
|
|
|
60
72
|
phaseGateSummary: PhaseGateSummary;
|
|
61
73
|
presetInfo: PresetInfo;
|
|
62
74
|
configSummary: ConfigSummary;
|
|
75
|
+
languages?: LanguageInfo;
|
|
63
76
|
hookHealth?: HookHealth;
|
|
64
77
|
baselineHealth?: BaselineHealth;
|
|
65
78
|
operationalWarnings?: readonly OperationalWarning[];
|
|
66
79
|
}
|
|
67
80
|
|
|
68
|
-
const REQUIRED_LAYER_IDS: readonly LayerId[] = [
|
|
81
|
+
const REQUIRED_LAYER_IDS: readonly LayerId[] = ["L1", "L2", "L3", "L4"];
|
|
69
82
|
|
|
70
83
|
export class HarnessStatusSummary {
|
|
71
84
|
readonly layers: readonly LayerHealth[];
|
|
72
85
|
readonly phaseGateSummary: PhaseGateSummary;
|
|
73
86
|
readonly presetInfo: PresetInfo;
|
|
74
87
|
readonly configSummary: ConfigSummary;
|
|
88
|
+
readonly languages: LanguageInfo | undefined;
|
|
75
89
|
readonly hookHealth: HookHealth | undefined;
|
|
76
90
|
readonly baselineHealth: BaselineHealth | undefined;
|
|
77
91
|
readonly operationalWarnings: readonly OperationalWarning[];
|
|
@@ -81,6 +95,7 @@ export class HarnessStatusSummary {
|
|
|
81
95
|
this.phaseGateSummary = props.phaseGateSummary;
|
|
82
96
|
this.presetInfo = props.presetInfo;
|
|
83
97
|
this.configSummary = props.configSummary;
|
|
98
|
+
this.languages = props.languages;
|
|
84
99
|
this.hookHealth = props.hookHealth;
|
|
85
100
|
this.baselineHealth = props.baselineHealth;
|
|
86
101
|
this.operationalWarnings = Object.freeze([...(props.operationalWarnings ?? [])]);
|
|
@@ -91,13 +106,13 @@ export class HarnessStatusSummary {
|
|
|
91
106
|
// INV: 4レイヤー必須
|
|
92
107
|
if (props.layers.length !== 4) {
|
|
93
108
|
throw new Error(
|
|
94
|
-
`HarnessApiDomainError: HarnessStatusSummary requires exactly 4 layers (L1-L4), got ${props.layers.length}
|
|
109
|
+
`HarnessApiDomainError: HarnessStatusSummary requires exactly 4 layers (L1-L4), got ${props.layers.length}`,
|
|
95
110
|
);
|
|
96
111
|
}
|
|
97
112
|
// 重複チェック
|
|
98
113
|
const ids = props.layers.map((l) => l.layerId);
|
|
99
114
|
if (new Set(ids).size !== ids.length) {
|
|
100
|
-
throw new Error(
|
|
115
|
+
throw new Error("HarnessApiDomainError: HarnessStatusSummary has duplicate layerIds");
|
|
101
116
|
}
|
|
102
117
|
// L1-L4 必須チェック
|
|
103
118
|
for (const required of REQUIRED_LAYER_IDS) {
|
|
@@ -113,6 +128,6 @@ export class HarnessStatusSummary {
|
|
|
113
128
|
}
|
|
114
129
|
|
|
115
130
|
isAllLayersHealthy(): boolean {
|
|
116
|
-
return this.layers.every((l) => !l.enabled || l.lastResult ===
|
|
131
|
+
return this.layers.every((l) => !l.enabled || l.lastResult === "pass");
|
|
117
132
|
}
|
|
118
133
|
}
|
package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @layer infrastructure
|
|
2
2
|
// biome-ast-engine-lint-adapter.ts — BiomeAstEngineLintAdapter
|
|
3
3
|
// Wave 2完了後にリアル実装へ差し替え(旧: @stub: wave2-pending)
|
|
4
|
+
// @work-item-id WI-311
|
|
4
5
|
|
|
5
6
|
import type { BiomeLintPort } from '../../domain/ports/biome-lint-port.js';
|
|
6
7
|
import type { HarnessError } from '../../domain/value-objects/harness-api-response.js';
|
|
@@ -36,12 +37,11 @@ function violationToHarnessError(v: RuleViolation): HarnessError {
|
|
|
36
37
|
export class BiomeAstEngineLintAdapter implements BiomeLintPort {
|
|
37
38
|
private readonly stub: IBiomeAstEngineStub;
|
|
38
39
|
|
|
39
|
-
constructor(stub?: IBiomeAstEngineStub) {
|
|
40
|
-
this.stub = stub ?? BiomeAstEngineLintAdapter.createRealImpl();
|
|
40
|
+
constructor(stub?: IBiomeAstEngineStub, rootDir = process.cwd()) {
|
|
41
|
+
this.stub = stub ?? BiomeAstEngineLintAdapter.createRealImpl(rootDir);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
private static createRealImpl(): IBiomeAstEngineStub {
|
|
44
|
-
const rootDir = process.cwd();
|
|
44
|
+
private static createRealImpl(rootDir: string): IBiomeAstEngineStub {
|
|
45
45
|
return {
|
|
46
46
|
async runLint(): Promise<BiomeAstEngineResult> {
|
|
47
47
|
const { createBiomeAstEngineModule } = await import('../../../biome-ast-engine/composition-root.js');
|
|
@@ -2,20 +2,30 @@
|
|
|
2
2
|
// @unit harness-api
|
|
3
3
|
// @work-item-id WI-123
|
|
4
4
|
// @work-item-id WI-096
|
|
5
|
+
// @work-item-id WI-328
|
|
5
6
|
// harness-config-query-adapter.ts — HarnessConfigQueryAdapter
|
|
6
7
|
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import { dirname, join, resolve } from
|
|
10
|
-
import
|
|
11
|
-
import type {
|
|
12
|
-
import type {
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import * as fs from "node:fs/promises";
|
|
10
|
+
import { dirname, join, resolve } from "node:path";
|
|
11
|
+
import { resolveProjectLanguages } from "../../../validator-system/infrastructure/adapters/harness-config-validator-config-adapter.js";
|
|
12
|
+
import type { ConfigQueryPort } from "../../domain/ports/config-query-port.js";
|
|
13
|
+
import type {
|
|
14
|
+
BaselineHealth,
|
|
15
|
+
ConfigSummary,
|
|
16
|
+
HookHealth,
|
|
17
|
+
LanguageInfo,
|
|
18
|
+
PhaseGateSummary,
|
|
19
|
+
PresetInfo,
|
|
20
|
+
} from "../../domain/value-objects/harness-status-summary.js";
|
|
21
|
+
import type { LayerId } from "../../domain/value-objects/layer-health.js";
|
|
13
22
|
|
|
14
23
|
interface HarnessConfigJson {
|
|
15
24
|
version: number;
|
|
16
25
|
project: {
|
|
17
26
|
name: string;
|
|
18
|
-
preset:
|
|
27
|
+
preset: "minimal" | "standard" | "strict";
|
|
28
|
+
languages?: string[];
|
|
19
29
|
};
|
|
20
30
|
layers?: Partial<Record<LayerId, { enabled?: boolean }>>;
|
|
21
31
|
paths?: {
|
|
@@ -29,9 +39,9 @@ interface HarnessConfigJson {
|
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
const PRESET_LAYERS: Record<string, LayerId[]> = {
|
|
32
|
-
minimal: [
|
|
33
|
-
standard: [
|
|
34
|
-
strict: [
|
|
42
|
+
minimal: ["L1"],
|
|
43
|
+
standard: ["L1", "L2", "L3"],
|
|
44
|
+
strict: ["L1", "L2", "L3", "L4"],
|
|
35
45
|
};
|
|
36
46
|
|
|
37
47
|
export interface HarnessConfigQueryAdapterOptions {
|
|
@@ -48,7 +58,7 @@ export class HarnessConfigQueryAdapter implements ConfigQueryPort {
|
|
|
48
58
|
|
|
49
59
|
private async readConfig(): Promise<HarnessConfigJson> {
|
|
50
60
|
if (this.cachedConfig !== null) return this.cachedConfig;
|
|
51
|
-
const content = await fs.readFile(this.configPath,
|
|
61
|
+
const content = await fs.readFile(this.configPath, "utf-8");
|
|
52
62
|
this.cachedConfig = JSON.parse(content) as HarnessConfigJson;
|
|
53
63
|
return this.cachedConfig;
|
|
54
64
|
}
|
|
@@ -56,7 +66,7 @@ export class HarnessConfigQueryAdapter implements ConfigQueryPort {
|
|
|
56
66
|
async getPresetInfo(): Promise<PresetInfo> {
|
|
57
67
|
const config = await this.readConfig();
|
|
58
68
|
const preset = config.project.preset;
|
|
59
|
-
const presetLayers = PRESET_LAYERS[preset] ?? [
|
|
69
|
+
const presetLayers = PRESET_LAYERS[preset] ?? ["L1", "L2", "L3"];
|
|
60
70
|
const enabledLayerSet = new Set<LayerId>(presetLayers);
|
|
61
71
|
|
|
62
72
|
for (const [layerId, layerConfig] of Object.entries(config.layers ?? {}) as [LayerId, { enabled?: boolean }][]) {
|
|
@@ -67,17 +77,29 @@ export class HarnessConfigQueryAdapter implements ConfigQueryPort {
|
|
|
67
77
|
}
|
|
68
78
|
}
|
|
69
79
|
|
|
70
|
-
const enabledLayers = ([
|
|
80
|
+
const enabledLayers = (["L1", "L2", "L3", "L4"] as const).filter((layerId) => enabledLayerSet.has(layerId));
|
|
71
81
|
return { name: preset, enabledLayers };
|
|
72
82
|
}
|
|
73
83
|
|
|
84
|
+
/**
|
|
85
|
+
* WI-328 (github#39 残課題): 実効言語リストと出所を返す。
|
|
86
|
+
* 解決ロジックは validator-system の resolveProjectLanguages()(WI-319 の
|
|
87
|
+
* 検出テーブル)を再利用し、validator の有効/SKIP 判定と同じ結果を表示する。
|
|
88
|
+
*/
|
|
89
|
+
async getLanguageInfo(): Promise<LanguageInfo> {
|
|
90
|
+
const config = await this.readConfig();
|
|
91
|
+
const rootDir = dirname(resolve(this.configPath));
|
|
92
|
+
const resolved = resolveProjectLanguages(config.project?.languages, rootDir);
|
|
93
|
+
return { effective: resolved.languages, source: resolved.source };
|
|
94
|
+
}
|
|
95
|
+
|
|
74
96
|
async getConfigSummary(): Promise<ConfigSummary> {
|
|
75
97
|
await this.readConfig();
|
|
76
98
|
const stat = await fs.stat(this.configPath);
|
|
77
99
|
return {
|
|
78
100
|
configPath: this.configPath,
|
|
79
101
|
lastModified: stat.mtime.toISOString(),
|
|
80
|
-
version:
|
|
102
|
+
version: "2",
|
|
81
103
|
};
|
|
82
104
|
}
|
|
83
105
|
|
|
@@ -89,7 +111,7 @@ export class HarnessConfigQueryAdapter implements ConfigQueryPort {
|
|
|
89
111
|
async getHookHealth(): Promise<HookHealth> {
|
|
90
112
|
const rootDir = dirname(this.configPath);
|
|
91
113
|
const configuredHooks: string[] = [];
|
|
92
|
-
for (const hookPath of [
|
|
114
|
+
for (const hookPath of [".claude/settings.json", ".codex/hooks.json", ".husky/pre-commit", ".husky/pre-push"]) {
|
|
93
115
|
try {
|
|
94
116
|
await fs.access(join(rootDir, hookPath));
|
|
95
117
|
configuredHooks.push(hookPath);
|
|
@@ -98,7 +120,7 @@ export class HarnessConfigQueryAdapter implements ConfigQueryPort {
|
|
|
98
120
|
}
|
|
99
121
|
}
|
|
100
122
|
|
|
101
|
-
const skipEvents = await readHookSkipEvents(join(rootDir,
|
|
123
|
+
const skipEvents = await readHookSkipEvents(join(rootDir, ".phasegate/hook-skip-events.jsonl"));
|
|
102
124
|
const skipCountsByReason: Record<string, number> = {};
|
|
103
125
|
for (const event of skipEvents) {
|
|
104
126
|
skipCountsByReason[event.reason] = (skipCountsByReason[event.reason] ?? 0) + 1;
|
|
@@ -111,8 +133,8 @@ export class HarnessConfigQueryAdapter implements ConfigQueryPort {
|
|
|
111
133
|
skipCountsByReason,
|
|
112
134
|
applyPatchBypass: {
|
|
113
135
|
nativeApplyPatchIntercepted: false,
|
|
114
|
-
backstop:
|
|
115
|
-
documentationUrl:
|
|
136
|
+
backstop: "pre-commit",
|
|
137
|
+
documentationUrl: "docs/guide/codex-integration.md",
|
|
116
138
|
},
|
|
117
139
|
};
|
|
118
140
|
}
|
|
@@ -121,50 +143,73 @@ export class HarnessConfigQueryAdapter implements ConfigQueryPort {
|
|
|
121
143
|
const config = await this.readConfig();
|
|
122
144
|
const rootDir = dirname(this.configPath);
|
|
123
145
|
const enabled = config.baseline?.enabled ?? true;
|
|
124
|
-
const relativePath = config.baseline?.path ??
|
|
146
|
+
const relativePath = config.baseline?.path ?? ".phasegate/baseline.json";
|
|
125
147
|
const baselinePath = resolve(rootDir, relativePath);
|
|
126
148
|
|
|
127
149
|
if (!enabled) {
|
|
128
|
-
return {
|
|
150
|
+
return {
|
|
151
|
+
enabled: false,
|
|
152
|
+
path: relativePath,
|
|
153
|
+
grandfatheredFileCount: 0,
|
|
154
|
+
shaMismatchCount: 0,
|
|
155
|
+
missingFileCount: 0,
|
|
156
|
+
removalRate: 0,
|
|
157
|
+
};
|
|
129
158
|
}
|
|
130
159
|
|
|
131
160
|
let raw: string;
|
|
132
161
|
try {
|
|
133
|
-
raw = await fs.readFile(baselinePath,
|
|
162
|
+
raw = await fs.readFile(baselinePath, "utf-8");
|
|
134
163
|
} catch {
|
|
135
|
-
return {
|
|
164
|
+
return {
|
|
165
|
+
enabled: true,
|
|
166
|
+
path: relativePath,
|
|
167
|
+
grandfatheredFileCount: 0,
|
|
168
|
+
shaMismatchCount: 0,
|
|
169
|
+
missingFileCount: 0,
|
|
170
|
+
removalRate: 0,
|
|
171
|
+
};
|
|
136
172
|
}
|
|
137
173
|
|
|
138
|
-
const parsed = JSON.parse(raw) as {
|
|
174
|
+
const parsed = JSON.parse(raw) as {
|
|
175
|
+
files?: Array<{ path?: string; sha1?: string }>;
|
|
176
|
+
entries?: Array<{ path?: string; sha1?: string }>;
|
|
177
|
+
};
|
|
139
178
|
const entries = parsed.files ?? parsed.entries ?? [];
|
|
140
179
|
let shaMismatchCount = 0;
|
|
141
180
|
let missingFileCount = 0;
|
|
142
181
|
for (const entry of entries) {
|
|
143
|
-
if (typeof entry.path !==
|
|
182
|
+
if (typeof entry.path !== "string" || typeof entry.sha1 !== "string") continue;
|
|
144
183
|
try {
|
|
145
184
|
const current = await fs.readFile(resolve(rootDir, entry.path));
|
|
146
|
-
const sha1 = createHash(
|
|
185
|
+
const sha1 = createHash("sha1").update(current).digest("hex");
|
|
147
186
|
if (sha1 !== entry.sha1) shaMismatchCount += 1;
|
|
148
187
|
} catch {
|
|
149
188
|
missingFileCount += 1;
|
|
150
189
|
}
|
|
151
190
|
}
|
|
152
191
|
const grandfatheredFileCount = entries.length;
|
|
153
|
-
const removalRate =
|
|
154
|
-
? 1
|
|
155
|
-
|
|
156
|
-
|
|
192
|
+
const removalRate =
|
|
193
|
+
grandfatheredFileCount === 0 ? 1 : (shaMismatchCount + missingFileCount) / grandfatheredFileCount;
|
|
194
|
+
return {
|
|
195
|
+
enabled: true,
|
|
196
|
+
path: relativePath,
|
|
197
|
+
grandfatheredFileCount,
|
|
198
|
+
shaMismatchCount,
|
|
199
|
+
missingFileCount,
|
|
200
|
+
removalRate,
|
|
201
|
+
};
|
|
157
202
|
}
|
|
158
203
|
}
|
|
159
204
|
|
|
160
|
-
async function readHookSkipEvents(filePath: string): Promise<Array<NonNullable<HookHealth[
|
|
205
|
+
async function readHookSkipEvents(filePath: string): Promise<Array<NonNullable<HookHealth["latestSkip"]>>> {
|
|
161
206
|
try {
|
|
162
|
-
const raw = await fs.readFile(filePath,
|
|
207
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
163
208
|
return raw
|
|
164
|
-
.split(
|
|
209
|
+
.split("\n")
|
|
165
210
|
.filter((line) => line.trim().length > 0)
|
|
166
|
-
.map((line) => JSON.parse(line) as HookHealth[
|
|
167
|
-
.filter((event): event is NonNullable<HookHealth[
|
|
211
|
+
.map((line) => JSON.parse(line) as HookHealth["latestSkip"])
|
|
212
|
+
.filter((event): event is NonNullable<HookHealth["latestSkip"]> => event !== null);
|
|
168
213
|
} catch {
|
|
169
214
|
return [];
|
|
170
215
|
}
|