phasegate 0.144.0 → 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.
- package/CHANGELOG.md +20 -0
- package/docs/guide/cli-reference.md +1 -1
- package/package.json +1 -1
- package/scripts/harness/installation/application/checks/check-utils.ts +65 -0
- package/scripts/harness/installation/application/checks/ci-workflow-missing-check.ts +29 -0
- package/scripts/harness/installation/application/checks/claude-hook-missing-check.ts +45 -0
- package/scripts/harness/installation/application/checks/claude-skills-symlink-check.ts +25 -0
- package/scripts/harness/installation/application/checks/codex-hook-missing-check.ts +44 -0
- package/scripts/harness/installation/application/checks/codex-skills-symlink-check.ts +25 -0
- package/scripts/harness/installation/application/checks/husky-commit-msg-missing-check.ts +25 -0
- package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +37 -0
- package/scripts/harness/installation/application/checks/husky-pre-push-missing-check.ts +25 -0
- package/scripts/harness/installation/application/checks/package-json-devdep-missing-check.ts +37 -0
- package/scripts/harness/installation/application/ports/file-inspector-port.ts +11 -0
- package/scripts/harness/installation/application/ports/hash-calculator-port.ts +9 -0
- package/scripts/harness/installation/application/ports/manifest-repository-port.ts +12 -0
- package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +44 -0
- package/scripts/harness/installation/application/wrappers/skill-deployer-manifest-builder.ts +38 -0
- package/scripts/harness/installation/composition-root.ts +39 -0
- package/scripts/harness/installation/domain/check-id.ts +21 -0
- package/scripts/harness/installation/domain/deployment-entry.ts +86 -0
- package/scripts/harness/installation/domain/deployment-manifest.ts +90 -0
- package/scripts/harness/installation/domain/diagnostic-finding.ts +71 -0
- package/scripts/harness/installation/domain/diagnostic-report.ts +52 -0
- package/scripts/harness/installation/domain/hash.ts +30 -0
- package/scripts/harness/installation/domain/managed-block.ts +37 -0
- package/scripts/harness/installation/domain/ports/heuristic-check.ts +12 -0
- package/scripts/harness/installation/domain/ports/merge-strategy.ts +13 -0
- package/scripts/harness/installation/domain/ports/reconcile-strategy.ts +15 -0
- package/scripts/harness/installation/domain/ports/uninstall-reverse-strategy.ts +10 -0
- package/scripts/harness/installation/domain/repair-mode.ts +11 -0
- package/scripts/harness/installation/domain/repair-table.ts +41 -0
- package/scripts/harness/installation/domain/suggested-skill.ts +37 -0
- package/scripts/harness/installation/infrastructure/adapters/file-system-manifest-repository-adapter.ts +62 -0
- package/scripts/harness/installation/infrastructure/adapters/node-crypto-hash-adapter.ts +13 -0
- package/scripts/harness/installation/infrastructure/adapters/node-fs-file-inspector-adapter.ts +53 -0
- package/scripts/harness/installation/presentation/cli/doctor-handler.ts +51 -0
- package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +53 -0
- package/scripts/harness/main.ts +113 -7
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ 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
|
+
|
|
23
|
+
## [0.144.1] - 2026-05-10
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **WI-142 — `ci:generate-template` default preset** — `--preset` 未指定時に存在しない `default` ではなく `standard` を使うようにし、`ci:generate-template --type agent-context-refresh --render` がそのまま成功するようにした。
|
|
28
|
+
- help / CLI reference も `--preset` の既定値が `standard` である説明に更新した。
|
|
29
|
+
|
|
10
30
|
## [0.144.0] - 2026-05-10
|
|
11
31
|
|
|
12
32
|
### Added
|
|
@@ -319,7 +319,7 @@ ISSUE-005 P3-10 で明確化された境界:
|
|
|
319
319
|
|
|
320
320
|
| Command | Options | Description |
|
|
321
321
|
|---|---|---|
|
|
322
|
-
| `ci:generate-template` | `--preset <id
|
|
322
|
+
| `ci:generate-template` | `--preset <id>`(省略時 `standard`) `--type <type>` `--render` `--json` | Generate CI/CD template |
|
|
323
323
|
| `ci:migrate-agents-md` | `--dry-run` `--validate-only` `--json` | Migrate AGENTS.md to pointer format |
|
|
324
324
|
| `ci:auto-refresh-agent-context` | `--dry-run` `--apply` `--json` | Refresh AGENTS.md pointers and CLAUDE.md standard sections |
|
|
325
325
|
| `refresh-claude-md` | `--dry-run` `--apply` `--json` | Refresh CLAUDE.md while preserving the user-owned section |
|
package/package.json
CHANGED
|
@@ -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,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
|
+
}
|