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.
Files changed (29) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/docs/guide/installation.md +1 -1
  3. package/docs/templates/ci/aidlc-gate.yml +22 -4
  4. package/package.json +2 -2
  5. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +30 -27
  6. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
  7. package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
  8. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +5 -1
  9. package/scripts/harness/config-foundation/domain/harness-config.ts +10 -7
  10. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +4 -1
  11. package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
  12. package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +27 -18
  13. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +1 -8
  14. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +1 -8
  15. package/scripts/harness/harness-api/domain/ports/config-query-port.ts +11 -1
  16. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +125 -86
  17. package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +31 -21
  18. package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +23 -8
  19. package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +4 -4
  20. package/scripts/harness/harness-api/infrastructure/adapters/harness-config-query-adapter.ts +79 -34
  21. package/scripts/harness/installation/application/usecases/run-install.ts +199 -51
  22. package/scripts/harness/installation/application/usecases/run-reconcile.ts +277 -69
  23. package/scripts/harness/installation/domain/deployment-manifest.ts +43 -0
  24. package/scripts/harness/main.ts +26 -6
  25. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +24 -7
  26. package/scripts/harness/validator-system/composition-root.ts +4 -1
  27. package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
  28. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +89 -3
  29. 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 { LayerHealth, type LayerId } from '../value-objects/layer-health.js';
7
- import { HarnessStatusSummary, type PresetInfo, type ConfigSummary, type PhaseGateSummary, type HookHealth, type BaselineHealth, type OperationalWarning } from '../value-objects/harness-status-summary.js';
8
- import type { ArtifactScanResult, ArtifactPresence } from '../value-objects/artifact-scan-result.js';
9
-
10
- const ALL_LAYER_IDS: readonly LayerId[] = ['L1', 'L2', 'L3', 'L4'];
7
+ import type { ArtifactPresence, ArtifactScanResult } from "../value-objects/artifact-scan-result.js";
8
+ import {
9
+ type BaselineHealth,
10
+ type ConfigSummary,
11
+ HarnessStatusSummary,
12
+ type HookHealth,
13
+ type LanguageInfo,
14
+ type OperationalWarning,
15
+ type PhaseGateSummary,
16
+ type PresetInfo,
17
+ } from "../value-objects/harness-status-summary.js";
18
+ import { LayerHealth, type LayerId } from "../value-objects/layer-health.js";
19
+
20
+ const ALL_LAYER_IDS: readonly LayerId[] = ["L1", "L2", "L3", "L4"];
11
21
 
12
22
  function getLayerId(artifact: ArtifactPresence): LayerId | null {
13
23
  const id = artifact.layer ?? artifact.layerId;
14
- if (id === 'L1' || id === 'L2' || id === 'L3' || id === 'L4') return id;
24
+ if (id === "L1" || id === "L2" || id === "L3" || id === "L4") return id;
15
25
  return null;
16
26
  }
17
27
 
@@ -22,7 +32,7 @@ export class StatusDerivationService {
22
32
  deriveLayerHealth(scanResult: ArtifactScanResult, layerId: LayerId): LayerHealth {
23
33
  const layerArtifacts = scanResult.foundArtifacts.filter((a) => getLayerId(a) === layerId);
24
34
  const hasPresent = layerArtifacts.some((a) => a.present === true);
25
- const lastResult = hasPresent ? 'pass' : 'unknown';
35
+ const lastResult = hasPresent ? "pass" : "unknown";
26
36
  return LayerHealth.create({ layerId, enabled: true, lastResult });
27
37
  }
28
38
 
@@ -34,7 +44,7 @@ export class StatusDerivationService {
34
44
  config: { layers?: Record<string, { enabled: boolean }> },
35
45
  presetInfo?: PresetInfo,
36
46
  configSummary?: ConfigSummary,
37
- phaseGateSummary?: PhaseGateSummary
47
+ phaseGateSummary?: PhaseGateSummary,
38
48
  ): HarnessStatusSummary {
39
49
  const derivedLayers = scanResult.derivedLayerHealth;
40
50
 
@@ -53,19 +63,19 @@ export class StatusDerivationService {
53
63
  // Derive from scan result
54
64
  const layerArtifacts = scanResult.foundArtifacts.filter((a) => getLayerId(a) === layerId);
55
65
  const hasPresent = layerArtifacts.some((a) => a.present === true);
56
- const lastResult = enabled ? (hasPresent ? 'pass' : 'unknown') : undefined;
66
+ const lastResult = enabled ? (hasPresent ? "pass" : "unknown") : undefined;
57
67
  return LayerHealth.create({ layerId, enabled, lastResult });
58
68
  });
59
69
 
60
70
  const effectivePresetInfo: PresetInfo = presetInfo ?? {
61
- name: 'standard',
62
- enabledLayers: ['L1', 'L2', 'L3'],
71
+ name: "standard",
72
+ enabledLayers: ["L1", "L2", "L3"],
63
73
  };
64
74
 
65
75
  const effectiveConfigSummary: ConfigSummary = configSummary ?? {
66
- configPath: 'phasegate.config.json',
76
+ configPath: "phasegate.config.json",
67
77
  lastModified: new Date().toISOString(),
68
- version: '2',
78
+ version: "2",
69
79
  };
70
80
 
71
81
  const effectivePhaseGateSummary: PhaseGateSummary = phaseGateSummary ?? {
@@ -90,7 +100,8 @@ export class StatusDerivationService {
90
100
  presetInfo: PresetInfo;
91
101
  configSummary: ConfigSummary;
92
102
  phaseGateSummary: PhaseGateSummary;
93
- liveValidationByLayer?: Partial<Record<LayerId, 'pass' | 'fail' | 'skipped' | 'not-run' | 'error'>>;
103
+ liveValidationByLayer?: Partial<Record<LayerId, "pass" | "fail" | "skipped" | "not-run" | "error">>;
104
+ languages?: LanguageInfo;
94
105
  hookHealth?: HookHealth;
95
106
  baselineHealth?: BaselineHealth;
96
107
  operationalWarnings?: readonly OperationalWarning[];
@@ -99,21 +110,19 @@ export class StatusDerivationService {
99
110
 
100
111
  const layers: LayerHealth[] = ALL_LAYER_IDS.map((layerId) => {
101
112
  const enabled = presetInfo.enabledLayers.includes(layerId);
102
- const configurationState = enabled ? 'enabled' : 'disabled';
113
+ const configurationState = enabled ? "enabled" : "disabled";
103
114
 
104
115
  // Find from derivedLayerHealth
105
116
  const existing = scanResult.derivedLayerHealth.find((l) => l.layerId === layerId);
106
117
  const layerArtifacts = scanResult.foundArtifacts.filter((a) => getLayerId(a) === layerId);
107
118
  const hasPresent = layerArtifacts.some((a) => a.present === true);
108
- const cachedArtifactState = hasPresent ? 'present' : 'missing';
109
- const liveValidationState = liveValidationByLayer?.[layerId] ?? 'not-run';
119
+ const cachedArtifactState = hasPresent ? "present" : "missing";
120
+ const liveValidationState = liveValidationByLayer?.[layerId] ?? "not-run";
110
121
  const liveLastResult =
111
- liveValidationState === 'pass' || liveValidationState === 'fail'
112
- ? liveValidationState
113
- : undefined;
122
+ liveValidationState === "pass" || liveValidationState === "fail" ? liveValidationState : undefined;
114
123
 
115
124
  if (enabled) {
116
- const lastResult = liveLastResult ?? existing?.lastResult ?? (hasPresent ? 'pass' : 'unknown');
125
+ const lastResult = liveLastResult ?? existing?.lastResult ?? (hasPresent ? "pass" : "unknown");
117
126
  return LayerHealth.create({
118
127
  layerId,
119
128
  enabled,
@@ -139,6 +148,7 @@ export class StatusDerivationService {
139
148
  phaseGateSummary,
140
149
  presetInfo,
141
150
  configSummary,
151
+ languages: input.languages,
142
152
  hookHealth: input.hookHealth,
143
153
  baselineHealth: input.baselineHealth,
144
154
  operationalWarnings: input.operationalWarnings,
@@ -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 './layer-health.js';
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: 'minimal' | 'standard' | 'strict';
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: 'HOOK_DISABLED' | 'TIMEOUT_EXCEEDED' | 'REENTRY_DETECTED' | string;
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: 'pre-commit';
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[] = ['L1', 'L2', 'L3', 'L4'];
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('HarnessApiDomainError: HarnessStatusSummary has duplicate layerIds');
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 === 'pass');
131
+ return this.layers.every((l) => !l.enabled || l.lastResult === "pass");
117
132
  }
118
133
  }
@@ -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 * as fs from 'node:fs/promises';
8
- import { createHash } from 'node:crypto';
9
- import { dirname, join, resolve } from 'node:path';
10
- import type { ConfigQueryPort } from '../../domain/ports/config-query-port.js';
11
- import type { PresetInfo, ConfigSummary, PhaseGateSummary, HookHealth, BaselineHealth } from '../../domain/value-objects/harness-status-summary.js';
12
- import type { LayerId } from '../../domain/value-objects/layer-health.js';
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: 'minimal' | 'standard' | 'strict';
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: ['L1'],
33
- standard: ['L1', 'L2', 'L3'],
34
- strict: ['L1', 'L2', 'L3', 'L4'],
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, 'utf-8');
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] ?? ['L1', 'L2', 'L3'];
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 = (['L1', 'L2', 'L3', 'L4'] as const).filter((layerId) => enabledLayerSet.has(layerId));
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: '2',
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 ['.claude/settings.json', '.codex/hooks.json', '.husky/pre-commit', '.husky/pre-push']) {
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, '.phasegate/hook-skip-events.jsonl'));
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: 'pre-commit',
115
- documentationUrl: 'docs/guide/codex-integration.md',
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 ?? '.phasegate/baseline.json';
146
+ const relativePath = config.baseline?.path ?? ".phasegate/baseline.json";
125
147
  const baselinePath = resolve(rootDir, relativePath);
126
148
 
127
149
  if (!enabled) {
128
- return { enabled: false, path: relativePath, grandfatheredFileCount: 0, shaMismatchCount: 0, missingFileCount: 0, removalRate: 0 };
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, 'utf-8');
162
+ raw = await fs.readFile(baselinePath, "utf-8");
134
163
  } catch {
135
- return { enabled: true, path: relativePath, grandfatheredFileCount: 0, shaMismatchCount: 0, missingFileCount: 0, removalRate: 0 };
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 { files?: Array<{ path?: string; sha1?: string }>; entries?: Array<{ path?: string; sha1?: string }> };
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 !== 'string' || typeof entry.sha1 !== 'string') continue;
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('sha1').update(current).digest('hex');
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 = grandfatheredFileCount === 0
154
- ? 1
155
- : (shaMismatchCount + missingFileCount) / grandfatheredFileCount;
156
- return { enabled: true, path: relativePath, grandfatheredFileCount, shaMismatchCount, missingFileCount, removalRate };
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['latestSkip']>>> {
205
+ async function readHookSkipEvents(filePath: string): Promise<Array<NonNullable<HookHealth["latestSkip"]>>> {
161
206
  try {
162
- const raw = await fs.readFile(filePath, 'utf-8');
207
+ const raw = await fs.readFile(filePath, "utf-8");
163
208
  return raw
164
- .split('\n')
209
+ .split("\n")
165
210
  .filter((line) => line.trim().length > 0)
166
- .map((line) => JSON.parse(line) as HookHealth['latestSkip'])
167
- .filter((event): event is NonNullable<HookHealth['latestSkip']> => event !== null);
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
  }