phasegate 0.144.1 → 0.145.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 (38) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/package.json +1 -1
  3. package/scripts/harness/installation/application/checks/check-utils.ts +65 -0
  4. package/scripts/harness/installation/application/checks/ci-workflow-missing-check.ts +29 -0
  5. package/scripts/harness/installation/application/checks/claude-hook-missing-check.ts +45 -0
  6. package/scripts/harness/installation/application/checks/claude-skills-symlink-check.ts +25 -0
  7. package/scripts/harness/installation/application/checks/codex-hook-missing-check.ts +44 -0
  8. package/scripts/harness/installation/application/checks/codex-skills-symlink-check.ts +25 -0
  9. package/scripts/harness/installation/application/checks/husky-commit-msg-missing-check.ts +25 -0
  10. package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +37 -0
  11. package/scripts/harness/installation/application/checks/husky-pre-push-missing-check.ts +25 -0
  12. package/scripts/harness/installation/application/checks/package-json-devdep-missing-check.ts +37 -0
  13. package/scripts/harness/installation/application/ports/file-inspector-port.ts +11 -0
  14. package/scripts/harness/installation/application/ports/hash-calculator-port.ts +9 -0
  15. package/scripts/harness/installation/application/ports/manifest-repository-port.ts +12 -0
  16. package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +44 -0
  17. package/scripts/harness/installation/application/wrappers/skill-deployer-manifest-builder.ts +38 -0
  18. package/scripts/harness/installation/composition-root.ts +39 -0
  19. package/scripts/harness/installation/domain/check-id.ts +21 -0
  20. package/scripts/harness/installation/domain/deployment-entry.ts +86 -0
  21. package/scripts/harness/installation/domain/deployment-manifest.ts +90 -0
  22. package/scripts/harness/installation/domain/diagnostic-finding.ts +71 -0
  23. package/scripts/harness/installation/domain/diagnostic-report.ts +52 -0
  24. package/scripts/harness/installation/domain/hash.ts +30 -0
  25. package/scripts/harness/installation/domain/managed-block.ts +37 -0
  26. package/scripts/harness/installation/domain/ports/heuristic-check.ts +12 -0
  27. package/scripts/harness/installation/domain/ports/merge-strategy.ts +13 -0
  28. package/scripts/harness/installation/domain/ports/reconcile-strategy.ts +15 -0
  29. package/scripts/harness/installation/domain/ports/uninstall-reverse-strategy.ts +10 -0
  30. package/scripts/harness/installation/domain/repair-mode.ts +11 -0
  31. package/scripts/harness/installation/domain/repair-table.ts +41 -0
  32. package/scripts/harness/installation/domain/suggested-skill.ts +37 -0
  33. package/scripts/harness/installation/infrastructure/adapters/file-system-manifest-repository-adapter.ts +62 -0
  34. package/scripts/harness/installation/infrastructure/adapters/node-crypto-hash-adapter.ts +13 -0
  35. package/scripts/harness/installation/infrastructure/adapters/node-fs-file-inspector-adapter.ts +53 -0
  36. package/scripts/harness/installation/presentation/cli/doctor-handler.ts +51 -0
  37. package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +53 -0
  38. package/scripts/harness/main.ts +106 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.145.0] - 2026-05-11
11
+
12
+ ### Added
13
+
14
+ - **WI-145 — installation doctor / manifest foundation** — `phasegate doctor` を追加し、Claude / Codex hooks、Husky hooks、CI workflow、package dependency、skills symlink の 9 checks で inert / partial installation を診断できるようにした。
15
+ - `--json`、`--strict`、`--report-out` に対応し、4 fixture (`no-phasegate` / `inert-install` / `partial-install` / `full-install`) の golden integration test で出力と exit code を固定した。
16
+ - init / update-skills 経路で `.phasegate/manifest.json` の薄い書き出しを開始し、manifest entries の `path` / `mode` / `hash` / `deployedAt` を unit test で固定した。
17
+ - doctor の false positive / false negative を防ぐため、9 checks すべてに unit test を追加した。
18
+
19
+ ### Fixed
20
+
21
+ - **WI-145 follow-up — L1 blocker cleanup** — validator-system の integration test fixture 内にあった internal module mock 文字列が L1-017 に検出されていた既存違反を解消した。
22
+
10
23
  ## [0.144.1] - 2026-05-10
11
24
 
12
25
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.144.1",
3
+ "version": "0.145.0",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "MIT",
@@ -0,0 +1,65 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-145
4
+
5
+ import { join } from "node:path";
6
+ import type { CheckId } from "../../domain/check-id.js";
7
+ import { DiagnosticFinding, type DiagnosticSeverity } from "../../domain/diagnostic-finding.js";
8
+ import type { RepairMode } from "../../domain/repair-mode.js";
9
+ import { RepairTable } from "../../domain/repair-table.js";
10
+ import type { SuggestedSkill } from "../../domain/suggested-skill.js";
11
+
12
+ const PHASEGATE_REPAIR_HINT = "npx phasegate install --apply";
13
+
14
+ export function projectPath(projectRoot: string, relativePath: string): string {
15
+ return join(projectRoot, relativePath);
16
+ }
17
+
18
+ export function containsPhasegateHook(value: unknown): boolean {
19
+ if (typeof value === "string") {
20
+ return value.includes("npx phasegate hook") || value.includes("phasegate hook");
21
+ }
22
+ if (Array.isArray(value)) return value.some((item) => containsPhasegateHook(item));
23
+ if (value !== null && typeof value === "object") {
24
+ return Object.values(value as Record<string, unknown>).some((item) => containsPhasegateHook(item));
25
+ }
26
+ return false;
27
+ }
28
+
29
+ export function hasUserCustomization(value: unknown): boolean {
30
+ if (value === null || value === undefined) return false;
31
+ if (typeof value === "string") return value.trim().length > 0;
32
+ if (Array.isArray(value)) return value.length > 0;
33
+ if (typeof value === "object") return Object.keys(value as Record<string, unknown>).length > 0;
34
+ return false;
35
+ }
36
+
37
+ export function scriptHasPhasegateBlock(content: string, commands: readonly string[]): boolean {
38
+ return commands.some((command) => content.includes(command));
39
+ }
40
+
41
+ export function createFinding(input: {
42
+ readonly checkId: CheckId;
43
+ readonly severity: DiagnosticSeverity;
44
+ readonly target: string;
45
+ readonly message: string;
46
+ readonly repairMode: RepairMode;
47
+ readonly repairHint?: string | null;
48
+ readonly suggestedSkill?: SuggestedSkill | null;
49
+ }): DiagnosticFinding {
50
+ return DiagnosticFinding.create({
51
+ checkId: input.checkId,
52
+ severity: input.severity,
53
+ target: input.target,
54
+ message: input.message,
55
+ repairMode: input.repairMode,
56
+ repairHint: input.repairHint ?? (input.repairMode === "mechanical" ? PHASEGATE_REPAIR_HINT : null),
57
+ suggestedSkill: input.suggestedSkill ?? new RepairTable().lookup(input.checkId),
58
+ });
59
+ }
60
+
61
+ export function skillTargetLooksValid(target: string | null): boolean {
62
+ if (target === null) return false;
63
+ const normalized = target.replaceAll("\\", "/").replace(/\/+$/, "");
64
+ return normalized === "skills" || normalized === "../skills" || normalized.endsWith("/skills");
65
+ }
@@ -0,0 +1,29 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-145
4
+
5
+ import type { FileInspectorPort } from "../ports/file-inspector-port.js";
6
+ import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
7
+ import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
8
+ import { createFinding, projectPath } from "./check-utils.js";
9
+
10
+ export class CiWorkflowMissingCheck implements HeuristicCheck {
11
+ readonly checkId = "ci-workflow-missing" as const;
12
+ private readonly target = ".github/workflows/";
13
+
14
+ async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
15
+ const files = await inspector.listFiles(projectPath(projectRoot, this.target));
16
+ const hasPhasegateWorkflow = files.some((file) => {
17
+ const normalized = file.toLowerCase();
18
+ return normalized.includes("phasegate") || normalized.includes("aidlc-gate") || normalized.includes("consistency-check");
19
+ });
20
+ if (hasPhasegateWorkflow) return null;
21
+ return createFinding({
22
+ checkId: this.checkId,
23
+ severity: "warn",
24
+ target: this.target,
25
+ message: ".github/workflows/ に phasegate workflow がありません",
26
+ repairMode: "manual",
27
+ });
28
+ }
29
+ }
@@ -0,0 +1,45 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-145
4
+
5
+ import type { FileInspectorPort } from "../ports/file-inspector-port.js";
6
+ import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
7
+ import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
8
+ import { containsPhasegateHook, createFinding, hasUserCustomization, projectPath } from "./check-utils.js";
9
+
10
+ export class ClaudeHookMissingCheck implements HeuristicCheck {
11
+ readonly checkId = "claude-hook-missing" as const;
12
+ private readonly target = ".claude/settings.json";
13
+
14
+ async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
15
+ const absolutePath = projectPath(projectRoot, this.target);
16
+ const exists = await inspector.exists(absolutePath);
17
+ if (!exists) {
18
+ return createFinding({
19
+ checkId: this.checkId,
20
+ severity: "red",
21
+ target: this.target,
22
+ message: ".claude/settings.json に phasegate hook が登録されていません",
23
+ repairMode: "mechanical",
24
+ });
25
+ }
26
+ const json = await inspector.readJson(absolutePath);
27
+ if (json === null) {
28
+ return createFinding({
29
+ checkId: this.checkId,
30
+ severity: "red",
31
+ target: this.target,
32
+ message: ".claude/settings.json を JSON として読めないため phasegate hook を確認できません",
33
+ repairMode: "manual",
34
+ });
35
+ }
36
+ if (containsPhasegateHook(json)) return null;
37
+ return createFinding({
38
+ checkId: this.checkId,
39
+ severity: "red",
40
+ target: this.target,
41
+ message: ".claude/settings.json に phasegate hook が登録されていません",
42
+ repairMode: hasUserCustomization(json) ? "ai-assisted" : "mechanical",
43
+ });
44
+ }
45
+ }
@@ -0,0 +1,25 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-145
4
+
5
+ import type { FileInspectorPort } from "../ports/file-inspector-port.js";
6
+ import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
7
+ import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
8
+ import { createFinding, projectPath, skillTargetLooksValid } from "./check-utils.js";
9
+
10
+ export class ClaudeSkillsSymlinkCheck implements HeuristicCheck {
11
+ readonly checkId = "claude-skills-symlink" as const;
12
+ private readonly target = ".claude/skills";
13
+
14
+ async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
15
+ const link = await inspector.readSymlink(projectPath(projectRoot, this.target));
16
+ if (skillTargetLooksValid(link)) return null;
17
+ return createFinding({
18
+ checkId: this.checkId,
19
+ severity: "red",
20
+ target: this.target,
21
+ message: ".claude/skills が phasegate skills を指していません",
22
+ repairMode: link === null ? "mechanical" : "manual",
23
+ });
24
+ }
25
+ }
@@ -0,0 +1,44 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-145
4
+
5
+ import type { FileInspectorPort } from "../ports/file-inspector-port.js";
6
+ import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
7
+ import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
8
+ import { containsPhasegateHook, createFinding, hasUserCustomization, projectPath } from "./check-utils.js";
9
+
10
+ export class CodexHookMissingCheck implements HeuristicCheck {
11
+ readonly checkId = "codex-hook-missing" as const;
12
+ private readonly target = ".codex/hooks.json";
13
+
14
+ async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
15
+ const absolutePath = projectPath(projectRoot, this.target);
16
+ if (!(await inspector.exists(absolutePath))) {
17
+ return createFinding({
18
+ checkId: this.checkId,
19
+ severity: "red",
20
+ target: this.target,
21
+ message: ".codex/hooks.json に phasegate hook が登録されていません",
22
+ repairMode: "mechanical",
23
+ });
24
+ }
25
+ const json = await inspector.readJson(absolutePath);
26
+ if (json === null) {
27
+ return createFinding({
28
+ checkId: this.checkId,
29
+ severity: "red",
30
+ target: this.target,
31
+ message: ".codex/hooks.json を JSON として読めないため phasegate hook を確認できません",
32
+ repairMode: "manual",
33
+ });
34
+ }
35
+ if (containsPhasegateHook(json)) return null;
36
+ return createFinding({
37
+ checkId: this.checkId,
38
+ severity: "red",
39
+ target: this.target,
40
+ message: ".codex/hooks.json に phasegate hook が登録されていません",
41
+ repairMode: hasUserCustomization(json) ? "ai-assisted" : "mechanical",
42
+ });
43
+ }
44
+ }
@@ -0,0 +1,25 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-145
4
+
5
+ import type { FileInspectorPort } from "../ports/file-inspector-port.js";
6
+ import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
7
+ import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
8
+ import { createFinding, projectPath, skillTargetLooksValid } from "./check-utils.js";
9
+
10
+ export class CodexSkillsSymlinkCheck implements HeuristicCheck {
11
+ readonly checkId = "codex-skills-symlink" as const;
12
+ private readonly target = ".codex/skills";
13
+
14
+ async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
15
+ const link = await inspector.readSymlink(projectPath(projectRoot, this.target));
16
+ if (skillTargetLooksValid(link)) return null;
17
+ return createFinding({
18
+ checkId: this.checkId,
19
+ severity: "red",
20
+ target: this.target,
21
+ message: ".codex/skills が phasegate skills を指していません",
22
+ repairMode: link === null ? "mechanical" : "manual",
23
+ });
24
+ }
25
+ }
@@ -0,0 +1,25 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-145
4
+
5
+ import type { FileInspectorPort } from "../ports/file-inspector-port.js";
6
+ import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
7
+ import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
8
+ import { createFinding, projectPath, scriptHasPhasegateBlock } from "./check-utils.js";
9
+
10
+ export class HuskyCommitMsgMissingCheck implements HeuristicCheck {
11
+ readonly checkId = "husky-commit-msg-missing" as const;
12
+ private readonly target = ".husky/commit-msg";
13
+
14
+ async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
15
+ const content = await inspector.readText(projectPath(projectRoot, this.target));
16
+ if (content !== null && scriptHasPhasegateBlock(content, ["phasegate commit-msg"])) return null;
17
+ return createFinding({
18
+ checkId: this.checkId,
19
+ severity: "red",
20
+ target: this.target,
21
+ message: ".husky/commit-msg に phasegate commit-msg hook がありません",
22
+ repairMode: content === null || content.trim().length === 0 ? "mechanical" : "ai-assisted",
23
+ });
24
+ }
25
+ }
@@ -0,0 +1,37 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-145
4
+
5
+ import type { FileInspectorPort } from "../ports/file-inspector-port.js";
6
+ import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
7
+ import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
8
+ import { createFinding, projectPath, scriptHasPhasegateBlock } from "./check-utils.js";
9
+
10
+ export class HuskyPreCommitMissingCheck implements HeuristicCheck {
11
+ readonly checkId = "husky-pre-commit-missing" as const;
12
+ private readonly target = ".husky/pre-commit";
13
+
14
+ async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
15
+ const content = await inspector.readText(projectPath(projectRoot, this.target));
16
+ if (
17
+ content !== null &&
18
+ scriptHasPhasegateBlock(content, [
19
+ "phasegate lint",
20
+ "phasegate check-phase-gate",
21
+ "main.ts lint",
22
+ "main.ts check-phase-gate",
23
+ "$HARNESS_CMD lint",
24
+ "$HARNESS_CMD check-phase-gate",
25
+ ])
26
+ ) {
27
+ return null;
28
+ }
29
+ return createFinding({
30
+ checkId: this.checkId,
31
+ severity: "red",
32
+ target: this.target,
33
+ message: ".husky/pre-commit に phasegate lint hook がありません",
34
+ repairMode: content === null || content.trim().length === 0 ? "mechanical" : "ai-assisted",
35
+ });
36
+ }
37
+ }
@@ -0,0 +1,25 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-145
4
+
5
+ import type { FileInspectorPort } from "../ports/file-inspector-port.js";
6
+ import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
7
+ import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
8
+ import { createFinding, projectPath, scriptHasPhasegateBlock } from "./check-utils.js";
9
+
10
+ export class HuskyPrePushMissingCheck implements HeuristicCheck {
11
+ readonly checkId = "husky-pre-push-missing" as const;
12
+ private readonly target = ".husky/pre-push";
13
+
14
+ async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
15
+ const content = await inspector.readText(projectPath(projectRoot, this.target));
16
+ if (content !== null && scriptHasPhasegateBlock(content, ["phasegate bypass:audit"])) return null;
17
+ return createFinding({
18
+ checkId: this.checkId,
19
+ severity: "warn",
20
+ target: this.target,
21
+ message: ".husky/pre-push に phasegate bypass:audit hook がありません",
22
+ repairMode: "mechanical",
23
+ });
24
+ }
25
+ }
@@ -0,0 +1,37 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-145
4
+
5
+ import type { FileInspectorPort } from "../ports/file-inspector-port.js";
6
+ import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
7
+ import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
8
+ import { createFinding, projectPath } from "./check-utils.js";
9
+
10
+ interface PackageJson {
11
+ readonly name?: string;
12
+ readonly dependencies?: Record<string, string>;
13
+ readonly devDependencies?: Record<string, string>;
14
+ }
15
+
16
+ export class PackageJsonDevdepMissingCheck implements HeuristicCheck {
17
+ readonly checkId = "package-json-devdep-missing" as const;
18
+ private readonly target = "package.json";
19
+
20
+ async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
21
+ const pkg = await inspector.readJson<PackageJson>(projectPath(projectRoot, this.target));
22
+ if (
23
+ pkg?.name === "phasegate" ||
24
+ pkg?.devDependencies?.phasegate !== undefined ||
25
+ pkg?.dependencies?.phasegate !== undefined
26
+ ) {
27
+ return null;
28
+ }
29
+ return createFinding({
30
+ checkId: this.checkId,
31
+ severity: "red",
32
+ target: this.target,
33
+ message: "package.json の devDependencies に phasegate がありません",
34
+ repairMode: "mechanical",
35
+ });
36
+ }
37
+ }
@@ -0,0 +1,11 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-145
4
+
5
+ export interface FileInspectorPort {
6
+ exists(absolutePath: string): Promise<boolean>;
7
+ readText(absolutePath: string): Promise<string | null>;
8
+ readJson<T = unknown>(absolutePath: string): Promise<T | null>;
9
+ readSymlink(absolutePath: string): Promise<string | null>;
10
+ listFiles(absolutePath: string): Promise<string[]>;
11
+ }
@@ -0,0 +1,9 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-145
4
+
5
+ import type { Hash } from "../../domain/hash.js";
6
+
7
+ export interface HashCalculatorPort {
8
+ compute(content: string | Buffer): Hash;
9
+ }
@@ -0,0 +1,12 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-145
4
+
5
+ import type { DeploymentManifest } from "../../domain/deployment-manifest.js";
6
+
7
+ export interface ManifestRepositoryPort {
8
+ load(projectRoot: string): Promise<DeploymentManifest | null>;
9
+ save(projectRoot: string, manifest: DeploymentManifest): Promise<void>;
10
+ exists(projectRoot: string): Promise<boolean>;
11
+ archive(projectRoot: string): Promise<void>;
12
+ }
@@ -0,0 +1,44 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-145
4
+
5
+ import { DiagnosticReport } from "../../domain/diagnostic-report.js";
6
+ import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
7
+ import type { FileInspectorPort } from "../ports/file-inspector-port.js";
8
+ import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
9
+
10
+ export interface RunDoctorDiagnosticsInput {
11
+ readonly projectRoot: string;
12
+ readonly strict: boolean;
13
+ }
14
+
15
+ export interface RunDoctorDiagnosticsOutput {
16
+ readonly report: DiagnosticReport;
17
+ readonly exitCode: number;
18
+ }
19
+
20
+ export class RunDoctorDiagnosticsUseCase {
21
+ constructor(
22
+ private readonly checks: readonly HeuristicCheck[],
23
+ private readonly inspector: FileInspectorPort,
24
+ private readonly manifestRepository: ManifestRepositoryPort,
25
+ ) {}
26
+
27
+ async execute(input: RunDoctorDiagnosticsInput): Promise<RunDoctorDiagnosticsOutput> {
28
+ await this.manifestRepository.load(input.projectRoot).catch(() => null);
29
+ const findings = (await Promise.all(
30
+ this.checks.map((check) => check.run(input.projectRoot, this.inspector)),
31
+ )).filter((finding) => finding !== null);
32
+ const report = DiagnosticReport.create(findings);
33
+ return {
34
+ report,
35
+ exitCode: this.decideExitCode(report, input.strict),
36
+ };
37
+ }
38
+
39
+ private decideExitCode(report: DiagnosticReport, strict: boolean): number {
40
+ if (report.hasRedFlag()) return 1;
41
+ if (strict && report.hasWarning()) return 1;
42
+ return 0;
43
+ }
44
+ }
@@ -0,0 +1,38 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-145
4
+
5
+ import { DeploymentEntry } from "../../domain/deployment-entry.js";
6
+ import { DeploymentManifest } from "../../domain/deployment-manifest.js";
7
+ import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
8
+
9
+ export interface DeployManifestRecord {
10
+ readonly path: string;
11
+ readonly mode: "created" | "merged" | "symlink";
12
+ readonly contentForHash: string;
13
+ readonly block?: {
14
+ readonly start: string;
15
+ readonly end: string;
16
+ readonly content: string;
17
+ } | null;
18
+ }
19
+
20
+ export class SkillDeployerManifestBuilder {
21
+ constructor(private readonly hashCalculator: HashCalculatorPort) {}
22
+
23
+ build(version: string, records: readonly DeployManifestRecord[], deployedAt = new Date().toISOString()): DeploymentManifest {
24
+ let manifest = DeploymentManifest.create(version, deployedAt);
25
+ for (const record of records) {
26
+ manifest = manifest.addEntry(
27
+ DeploymentEntry.create({
28
+ path: record.path,
29
+ mode: record.mode,
30
+ block: record.block ?? null,
31
+ hash: this.hashCalculator.compute(record.contentForHash),
32
+ deployedAt,
33
+ }),
34
+ );
35
+ }
36
+ return manifest;
37
+ }
38
+ }
@@ -0,0 +1,39 @@
1
+ // @unit installation
2
+ // @layer presentation
3
+ // @work-item-id WI-145
4
+
5
+ import { ClaudeHookMissingCheck } from "./application/checks/claude-hook-missing-check.js";
6
+ import { ClaudeSkillsSymlinkCheck } from "./application/checks/claude-skills-symlink-check.js";
7
+ import { CodexHookMissingCheck } from "./application/checks/codex-hook-missing-check.js";
8
+ import { CodexSkillsSymlinkCheck } from "./application/checks/codex-skills-symlink-check.js";
9
+ import { CiWorkflowMissingCheck } from "./application/checks/ci-workflow-missing-check.js";
10
+ import { HuskyCommitMsgMissingCheck } from "./application/checks/husky-commit-msg-missing-check.js";
11
+ import { HuskyPreCommitMissingCheck } from "./application/checks/husky-pre-commit-missing-check.js";
12
+ import { HuskyPrePushMissingCheck } from "./application/checks/husky-pre-push-missing-check.js";
13
+ import { PackageJsonDevdepMissingCheck } from "./application/checks/package-json-devdep-missing-check.js";
14
+ import { RunDoctorDiagnosticsUseCase } from "./application/usecases/run-doctor-diagnostics.js";
15
+ import { FileSystemManifestRepositoryAdapter } from "./infrastructure/adapters/file-system-manifest-repository-adapter.js";
16
+ import { NodeFsFileInspectorAdapter } from "./infrastructure/adapters/node-fs-file-inspector-adapter.js";
17
+ import { DoctorHandler } from "./presentation/cli/doctor-handler.js";
18
+
19
+ export function createInstallationModule() {
20
+ const inspector = new NodeFsFileInspectorAdapter();
21
+ const manifestRepository = new FileSystemManifestRepositoryAdapter();
22
+ const checks = [
23
+ new ClaudeHookMissingCheck(),
24
+ new CodexHookMissingCheck(),
25
+ new HuskyPreCommitMissingCheck(),
26
+ new HuskyCommitMsgMissingCheck(),
27
+ new HuskyPrePushMissingCheck(),
28
+ new CiWorkflowMissingCheck(),
29
+ new PackageJsonDevdepMissingCheck(),
30
+ new ClaudeSkillsSymlinkCheck(),
31
+ new CodexSkillsSymlinkCheck(),
32
+ ];
33
+ const runDoctorDiagnosticsUseCase = new RunDoctorDiagnosticsUseCase(checks, inspector, manifestRepository);
34
+ return {
35
+ manifestRepository,
36
+ runDoctorDiagnosticsUseCase,
37
+ doctorHandler: new DoctorHandler(runDoctorDiagnosticsUseCase),
38
+ };
39
+ }
@@ -0,0 +1,21 @@
1
+ // @unit installation
2
+ // @layer domain
3
+ // @work-item-id WI-145
4
+
5
+ export const CHECK_IDS = [
6
+ "claude-hook-missing",
7
+ "codex-hook-missing",
8
+ "husky-pre-commit-missing",
9
+ "husky-commit-msg-missing",
10
+ "husky-pre-push-missing",
11
+ "ci-workflow-missing",
12
+ "package-json-devdep-missing",
13
+ "claude-skills-symlink",
14
+ "codex-skills-symlink",
15
+ ] as const;
16
+
17
+ export type CheckId = (typeof CHECK_IDS)[number];
18
+
19
+ export function isCheckId(value: string): value is CheckId {
20
+ return (CHECK_IDS as readonly string[]).includes(value);
21
+ }