phasegate 0.91.0 → 0.107.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 +169 -0
- package/README.ja.md +15 -7
- package/README.md +24 -4
- package/docs/ADR/ADR-015-architecture-preset.md +183 -0
- package/docs/guide/codex-integration.md +7 -2
- package/docs/guide/installation.md +10 -2
- package/docs/guide/preset-selection.md +170 -0
- package/docs/guide/quick-vs-full-mode.md +3 -3
- package/docs/guide/retrofit-adoption.md +19 -2
- package/docs/guide/skills-overview.md +1 -1
- package/package.json +7 -1
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +108 -100
- package/scripts/harness/agent-integration/domain/ports/phase-gate-query-port.ts +3 -3
- package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +26 -30
- package/scripts/harness/agent-integration/infrastructure/adapters/file-system-story-reflection-query-adapter.ts +6 -2
- package/scripts/harness/biome-ast-engine/application/dto/analyze-import-graph-input.ts +3 -0
- package/scripts/harness/biome-ast-engine/application/dto/resolve-enabled-rules-output.ts +2 -0
- package/scripts/harness/biome-ast-engine/application/mappers/resolve-enabled-rules-output-mapper.ts +4 -1
- package/scripts/harness/biome-ast-engine/application/usecases/analyze-import-graph-usecase.ts +4 -1
- package/scripts/harness/biome-ast-engine/application/usecases/execute-lint-usecase.ts +2 -0
- package/scripts/harness/biome-ast-engine/application/usecases/resolve-enabled-rules-usecase.ts +31 -3
- package/scripts/harness/biome-ast-engine/composition-root.ts +10 -2
- package/scripts/harness/biome-ast-engine/domain/ports/rule-config-provider-port.ts +16 -0
- package/scripts/harness/biome-ast-engine/domain/ports/source-module-analyzer-port.ts +5 -1
- package/scripts/harness/biome-ast-engine/domain/services/lint-runner.ts +5 -1
- package/scripts/harness/biome-ast-engine/domain/value-objects/architecture-spec.ts +38 -0
- package/scripts/harness/biome-ast-engine/domain/value-objects/layer-boundary.ts +7 -8
- package/scripts/harness/biome-ast-engine/domain/value-objects/layer-name.ts +15 -23
- package/scripts/harness/biome-ast-engine/domain/value-objects/source-module-snapshot.ts +11 -4
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/harness-config-provider-adapter.ts +29 -3
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +22 -15
- package/scripts/harness/biome-ast-engine/infrastructure/mappers/source-module-snapshot-mapper.ts +26 -17
- package/scripts/harness/config-foundation/application/dto/resolved-config-output.ts +1 -0
- package/scripts/harness/config-foundation/application/usecases/load-resolved-config-use-case.ts +34 -2
- package/scripts/harness/config-foundation/application/usecases/migrate-schema-use-case.ts +89 -0
- package/scripts/harness/config-foundation/composition-root.ts +8 -0
- package/scripts/harness/config-foundation/domain/harness-config.ts +6 -0
- package/scripts/harness/config-foundation/domain/services/architecture-resolution-service.ts +257 -0
- package/scripts/harness/config-foundation/domain/value-objects/architecture-config.ts +66 -0
- package/scripts/harness/config-foundation/domain/value-objects/architecture-preset-catalog.ts +75 -0
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +546 -0
- package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts +18 -6
- package/scripts/harness/config-foundation/presentation/cli/migrate-schema-command-handler.ts +83 -0
- package/scripts/harness/integrations/pre-commit.ts +211 -52
- package/scripts/harness/main.ts +460 -340
- package/scripts/harness/phase-dependency-model/domain/ports/story-reflection-file-system-port.ts +2 -4
- package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +54 -17
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +154 -36
- package/scripts/harness/setup/skill-deployer.ts +140 -102
- package/scripts/harness/skill-quality/domain/errors/skill-quality-error.ts +24 -23
- package/scripts/harness/skill-quality/domain/value-objects/commit-message.ts +23 -7
- package/scripts/harness/traceability-model/application/usecases/apply-work-item-migration-usecase.ts +52 -0
- package/scripts/harness/traceability-model/application/usecases/plan-work-item-migration-usecase.ts +29 -0
- package/scripts/harness/traceability-model/application/usecases/validate-design-story-annotations-usecase.ts +83 -18
- package/scripts/harness/traceability-model/composition-root.ts +48 -30
- package/scripts/harness/traceability-model/domain/ports/design-document-port.ts +9 -15
- package/scripts/harness/traceability-model/domain/ports/work-item-migration-apply-port.ts +11 -0
- package/scripts/harness/traceability-model/domain/ports/work-item-migration-source-port.ts +9 -0
- package/scripts/harness/traceability-model/domain/services/work-item-migration-planner.ts +162 -0
- package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +57 -0
- package/scripts/harness/traceability-model/domain/value-objects/work-item-migration-candidate.ts +47 -0
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-migration-apply-gateway.ts +110 -0
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-migration-source-gateway.ts +182 -0
- package/scripts/harness/traceability-model/infrastructure/gateways/markdown-design-document-gateway.ts +29 -43
- package/scripts/harness/traceability-model/infrastructure/parsers/work-item-frontmatter-parser.ts +136 -0
- package/scripts/harness/traceability-model/presentation/cli/migrate-work-items-command-handler.ts +186 -0
- package/skills/quick-implementor/SKILL.md +17 -1
- package/templates/.husky/commit-msg +1 -0
|
@@ -1,67 +1,64 @@
|
|
|
1
|
+
// @unit harness-api
|
|
1
2
|
// @layer infrastructure
|
|
2
3
|
// Note: import.meta.url を使わず、呼び出し元 (main.ts) がパスを解決して渡す設計。
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { promises as fs } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
6
7
|
|
|
7
|
-
const HARNESS_VERSION_FILE =
|
|
8
|
-
const SKILLS_SOURCE_DIR =
|
|
9
|
-
const SKILLS_TARGET_DIR =
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
8
|
+
const HARNESS_VERSION_FILE = ".harness-version";
|
|
9
|
+
const SKILLS_SOURCE_DIR = "skills";
|
|
10
|
+
const SKILLS_TARGET_DIR = "skills";
|
|
11
|
+
const SKILLS_LINK_TARGET = "../skills";
|
|
12
|
+
const CLAUDE_SKILLS_LINK_DIR = ".claude";
|
|
13
|
+
const CODEX_SKILLS_LINK_DIR = ".codex";
|
|
14
|
+
const HARNESS_CONFIG_FILE = "phasegate.config.json";
|
|
15
|
+
const HOOKS_TEMPLATE_DIR = join("templates", ".claude");
|
|
16
|
+
const HOOKS_TARGET_DIR = ".claude";
|
|
13
17
|
|
|
14
18
|
// ── Skill Category Map ──
|
|
15
19
|
|
|
16
|
-
export type SkillCategory =
|
|
17
|
-
export type SkillSet =
|
|
20
|
+
export type SkillCategory = "core" | "aidlc" | "utility";
|
|
21
|
+
export type SkillSet = "core" | "all";
|
|
18
22
|
|
|
19
23
|
export const SKILL_CATEGORIES: Record<SkillCategory, readonly string[]> = {
|
|
20
24
|
core: [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
"cascade-updater",
|
|
26
|
+
"codebase-mapper",
|
|
27
|
+
"consistency-checker",
|
|
28
|
+
"doc-freshness-checker",
|
|
29
|
+
"engineering-perspective",
|
|
30
|
+
"implementation-readiness-checker",
|
|
31
|
+
"pointer-validator",
|
|
32
|
+
"test-coverage-checker",
|
|
29
33
|
],
|
|
30
34
|
aidlc: [
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
],
|
|
50
|
-
utility: [
|
|
51
|
-
'codex-delegator',
|
|
52
|
-
'skill-creator',
|
|
35
|
+
"domain-designer",
|
|
36
|
+
"environment-designer",
|
|
37
|
+
"implementation-planner",
|
|
38
|
+
"it-test-designer",
|
|
39
|
+
"it-test-logic-designer",
|
|
40
|
+
"logical-designer",
|
|
41
|
+
"mock-designer",
|
|
42
|
+
"product-architect",
|
|
43
|
+
"quick-implementor",
|
|
44
|
+
"scenario-test-designer",
|
|
45
|
+
"scenario-test-logic-designer",
|
|
46
|
+
"story-implementor",
|
|
47
|
+
"story-mapper",
|
|
48
|
+
"story-writer",
|
|
49
|
+
"uiux-designer",
|
|
50
|
+
"unit-designer",
|
|
51
|
+
"unit-test-designer",
|
|
52
|
+
"unit-test-logic-designer",
|
|
53
53
|
],
|
|
54
|
+
utility: ["codex-delegator", "skill-creator"],
|
|
54
55
|
} as const;
|
|
55
56
|
|
|
56
57
|
export function getSkillsForSet(skillSet: SkillSet): string[] {
|
|
57
|
-
if (skillSet ===
|
|
58
|
+
if (skillSet === "core") {
|
|
58
59
|
return [...SKILL_CATEGORIES.core];
|
|
59
60
|
}
|
|
60
|
-
return [
|
|
61
|
-
...SKILL_CATEGORIES.core,
|
|
62
|
-
...SKILL_CATEGORIES.aidlc,
|
|
63
|
-
...SKILL_CATEGORIES.utility,
|
|
64
|
-
];
|
|
61
|
+
return [...SKILL_CATEGORIES.core, ...SKILL_CATEGORIES.aidlc, ...SKILL_CATEGORIES.utility];
|
|
65
62
|
}
|
|
66
63
|
|
|
67
64
|
export function getCategoryForSkill(skillName: string): SkillCategory | null {
|
|
@@ -92,8 +89,8 @@ interface PkgJson {
|
|
|
92
89
|
}
|
|
93
90
|
|
|
94
91
|
export async function getHarnessVersion(harnessRoot: string): Promise<string> {
|
|
95
|
-
const packageJsonPath = join(harnessRoot,
|
|
96
|
-
const content = await fs.readFile(packageJsonPath,
|
|
92
|
+
const packageJsonPath = join(harnessRoot, "package.json");
|
|
93
|
+
const content = await fs.readFile(packageJsonPath, "utf-8");
|
|
97
94
|
const pkg: PkgJson = JSON.parse(content);
|
|
98
95
|
return pkg.version;
|
|
99
96
|
}
|
|
@@ -118,7 +115,7 @@ async function copyDirectory(src: string, dest: string): Promise<number> {
|
|
|
118
115
|
export async function deploySkills(
|
|
119
116
|
harnessRoot: string,
|
|
120
117
|
projectRoot: string,
|
|
121
|
-
skillSet: SkillSet =
|
|
118
|
+
skillSet: SkillSet = "all",
|
|
122
119
|
): Promise<DeployResult> {
|
|
123
120
|
const skillsSource = join(harnessRoot, SKILLS_SOURCE_DIR);
|
|
124
121
|
const skillsTarget = join(projectRoot, SKILLS_TARGET_DIR);
|
|
@@ -139,11 +136,7 @@ export async function deploySkills(
|
|
|
139
136
|
}
|
|
140
137
|
|
|
141
138
|
const versionInfo: VersionInfo = { version, deployedAt, skillSet };
|
|
142
|
-
await fs.writeFile(
|
|
143
|
-
join(skillsTarget, HARNESS_VERSION_FILE),
|
|
144
|
-
JSON.stringify(versionInfo, null, 2) + '\n',
|
|
145
|
-
'utf-8',
|
|
146
|
-
);
|
|
139
|
+
await fs.writeFile(join(skillsTarget, HARNESS_VERSION_FILE), JSON.stringify(versionInfo, null, 2) + "\n", "utf-8");
|
|
147
140
|
|
|
148
141
|
return { deployedSkills: skills, targetDir: skillsTarget, version, deployedAt, skillSet };
|
|
149
142
|
}
|
|
@@ -151,7 +144,7 @@ export async function deploySkills(
|
|
|
151
144
|
export async function getDeployedVersion(projectRoot: string): Promise<VersionInfo | null> {
|
|
152
145
|
const versionFile = join(projectRoot, SKILLS_TARGET_DIR, HARNESS_VERSION_FILE);
|
|
153
146
|
try {
|
|
154
|
-
const content = await fs.readFile(versionFile,
|
|
147
|
+
const content = await fs.readFile(versionFile, "utf-8");
|
|
155
148
|
const info: VersionInfo = JSON.parse(content);
|
|
156
149
|
return info;
|
|
157
150
|
} catch {
|
|
@@ -159,21 +152,60 @@ export async function getDeployedVersion(projectRoot: string): Promise<VersionIn
|
|
|
159
152
|
}
|
|
160
153
|
}
|
|
161
154
|
|
|
155
|
+
export interface AgentSkillLinkResult {
|
|
156
|
+
created: boolean;
|
|
157
|
+
path: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface DeployAgentSkillLinksResult {
|
|
161
|
+
claude: AgentSkillLinkResult | null;
|
|
162
|
+
codex: AgentSkillLinkResult | null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function ensureSkillLink(projectRoot: string, agentDir: string): Promise<AgentSkillLinkResult> {
|
|
166
|
+
const parentDir = join(projectRoot, agentDir);
|
|
167
|
+
const targetPath = join(parentDir, "skills");
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const stats = await fs.lstat(targetPath);
|
|
171
|
+
if (stats.isSymbolicLink()) {
|
|
172
|
+
const existingTarget = await fs.readlink(targetPath);
|
|
173
|
+
if (existingTarget === SKILLS_LINK_TARGET) {
|
|
174
|
+
return { created: false, path: targetPath };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { created: false, path: targetPath };
|
|
178
|
+
} catch {}
|
|
179
|
+
|
|
180
|
+
await fs.mkdir(parentDir, { recursive: true });
|
|
181
|
+
await fs.symlink(SKILLS_LINK_TARGET, targetPath, process.platform === "win32" ? "junction" : "dir");
|
|
182
|
+
return { created: true, path: targetPath };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function deployAgentSkillLinks(
|
|
186
|
+
projectRoot: string,
|
|
187
|
+
agents: {
|
|
188
|
+
claude: boolean;
|
|
189
|
+
codex: boolean;
|
|
190
|
+
},
|
|
191
|
+
): Promise<DeployAgentSkillLinksResult> {
|
|
192
|
+
const claude = agents.claude ? await ensureSkillLink(projectRoot, CLAUDE_SKILLS_LINK_DIR) : null;
|
|
193
|
+
const codex = agents.codex ? await ensureSkillLink(projectRoot, CODEX_SKILLS_LINK_DIR) : null;
|
|
194
|
+
return { claude, codex };
|
|
195
|
+
}
|
|
196
|
+
|
|
162
197
|
export interface DeployHooksResult {
|
|
163
198
|
scriptsDeployed: number;
|
|
164
199
|
settingsCreated: boolean;
|
|
165
200
|
}
|
|
166
201
|
|
|
167
|
-
export async function deployHookScripts(
|
|
168
|
-
harnessRoot: string,
|
|
169
|
-
projectRoot: string,
|
|
170
|
-
): Promise<DeployHooksResult> {
|
|
202
|
+
export async function deployHookScripts(harnessRoot: string, projectRoot: string): Promise<DeployHooksResult> {
|
|
171
203
|
const templateDir = join(harnessRoot, HOOKS_TEMPLATE_DIR);
|
|
172
204
|
const targetDir = join(projectRoot, HOOKS_TARGET_DIR);
|
|
173
205
|
|
|
174
206
|
// scripts/ ディレクトリをコピー
|
|
175
|
-
const scriptsSource = join(templateDir,
|
|
176
|
-
const scriptsTarget = join(targetDir,
|
|
207
|
+
const scriptsSource = join(templateDir, "scripts");
|
|
208
|
+
const scriptsTarget = join(targetDir, "scripts");
|
|
177
209
|
let scriptsDeployed = 0;
|
|
178
210
|
|
|
179
211
|
try {
|
|
@@ -183,16 +215,15 @@ export async function deployHookScripts(
|
|
|
183
215
|
// シェルスクリプトに実行権限を付与
|
|
184
216
|
const entries = await fs.readdir(scriptsTarget);
|
|
185
217
|
for (const entry of entries) {
|
|
186
|
-
if (entry.endsWith(
|
|
218
|
+
if (entry.endsWith(".sh")) {
|
|
187
219
|
await fs.chmod(join(scriptsTarget, entry), 0o755);
|
|
188
220
|
}
|
|
189
221
|
}
|
|
190
|
-
} catch {
|
|
191
|
-
}
|
|
222
|
+
} catch {}
|
|
192
223
|
|
|
193
224
|
// settings.json を作成(既存があればスキップ)
|
|
194
|
-
const settingsSource = join(templateDir,
|
|
195
|
-
const settingsTarget = join(targetDir,
|
|
225
|
+
const settingsSource = join(templateDir, "settings.json");
|
|
226
|
+
const settingsTarget = join(targetDir, "settings.json");
|
|
196
227
|
let settingsCreated = false;
|
|
197
228
|
|
|
198
229
|
try {
|
|
@@ -204,8 +235,7 @@ export async function deployHookScripts(
|
|
|
204
235
|
await fs.mkdir(targetDir, { recursive: true });
|
|
205
236
|
await fs.copyFile(settingsSource, settingsTarget);
|
|
206
237
|
settingsCreated = true;
|
|
207
|
-
} catch {
|
|
208
|
-
}
|
|
238
|
+
} catch {}
|
|
209
239
|
}
|
|
210
240
|
|
|
211
241
|
return { scriptsDeployed, settingsCreated };
|
|
@@ -214,7 +244,7 @@ export async function deployHookScripts(
|
|
|
214
244
|
export async function initHarnessConfig(
|
|
215
245
|
projectRoot: string,
|
|
216
246
|
projectName: string,
|
|
217
|
-
phasePreset?:
|
|
247
|
+
phasePreset?: "full" | "standard" | "minimal" | "custom",
|
|
218
248
|
): Promise<{ created: boolean; path: string }> {
|
|
219
249
|
const configPath = join(projectRoot, HARNESS_CONFIG_FILE);
|
|
220
250
|
try {
|
|
@@ -227,31 +257,31 @@ export async function initHarnessConfig(
|
|
|
227
257
|
const template = {
|
|
228
258
|
project: {
|
|
229
259
|
name: projectName,
|
|
230
|
-
preset:
|
|
260
|
+
preset: "standard",
|
|
231
261
|
},
|
|
232
262
|
layers: {},
|
|
233
263
|
quickMode: {},
|
|
234
264
|
phaseDependencies: {
|
|
235
|
-
preset: phasePreset ??
|
|
265
|
+
preset: phasePreset ?? "default",
|
|
236
266
|
override: false,
|
|
237
267
|
customRules: [],
|
|
238
268
|
},
|
|
239
269
|
planningMode: {
|
|
240
|
-
default:
|
|
270
|
+
default: "interactive",
|
|
241
271
|
perPhase: {},
|
|
242
272
|
},
|
|
243
273
|
harnesses: {},
|
|
244
274
|
paths: {
|
|
245
|
-
designDocs:
|
|
246
|
-
inceptionDocs:
|
|
275
|
+
designDocs: "docs/product/construction",
|
|
276
|
+
inceptionDocs: "docs/inception",
|
|
247
277
|
},
|
|
248
278
|
reporting: {
|
|
249
|
-
format:
|
|
250
|
-
outputDir:
|
|
279
|
+
format: "json",
|
|
280
|
+
outputDir: "reports",
|
|
251
281
|
},
|
|
252
282
|
};
|
|
253
283
|
|
|
254
|
-
await fs.writeFile(configPath, JSON.stringify(template, null, 2) +
|
|
284
|
+
await fs.writeFile(configPath, JSON.stringify(template, null, 2) + "\n", "utf-8");
|
|
255
285
|
return { created: true, path: configPath };
|
|
256
286
|
}
|
|
257
287
|
|
|
@@ -260,19 +290,16 @@ export interface DeployDesignDocsResult {
|
|
|
260
290
|
skippedFiles: string[];
|
|
261
291
|
}
|
|
262
292
|
|
|
263
|
-
export async function deployDesignDocs(
|
|
264
|
-
harnessRoot: string,
|
|
265
|
-
projectRoot: string,
|
|
266
|
-
): Promise<DeployDesignDocsResult> {
|
|
293
|
+
export async function deployDesignDocs(harnessRoot: string, projectRoot: string): Promise<DeployDesignDocsResult> {
|
|
267
294
|
const copiedFiles: string[] = [];
|
|
268
295
|
const skippedFiles: string[] = [];
|
|
269
|
-
const docsTargetDir = join(projectRoot,
|
|
270
|
-
const principlesTargetDir = join(docsTargetDir,
|
|
296
|
+
const docsTargetDir = join(projectRoot, "docs");
|
|
297
|
+
const principlesTargetDir = join(docsTargetDir, "principles");
|
|
271
298
|
|
|
272
299
|
await fs.mkdir(docsTargetDir, { recursive: true });
|
|
273
300
|
await fs.mkdir(principlesTargetDir, { recursive: true });
|
|
274
301
|
|
|
275
|
-
const folderRulesRelativePath = join(
|
|
302
|
+
const folderRulesRelativePath = join("docs", "folder_management_rules.md");
|
|
276
303
|
const folderRulesSource = join(harnessRoot, folderRulesRelativePath);
|
|
277
304
|
const folderRulesTarget = join(projectRoot, folderRulesRelativePath);
|
|
278
305
|
|
|
@@ -289,17 +316,17 @@ export async function deployDesignDocs(
|
|
|
289
316
|
}
|
|
290
317
|
}
|
|
291
318
|
|
|
292
|
-
const principlesSourceDir = join(harnessRoot,
|
|
319
|
+
const principlesSourceDir = join(harnessRoot, "docs", "principles");
|
|
293
320
|
|
|
294
321
|
try {
|
|
295
322
|
const principleEntries = await fs.readdir(principlesSourceDir, { withFileTypes: true });
|
|
296
323
|
const principleFiles = principleEntries
|
|
297
|
-
.filter((entry) => entry.isFile() && entry.name.endsWith(
|
|
324
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
|
|
298
325
|
.map((entry) => entry.name)
|
|
299
326
|
.sort();
|
|
300
327
|
|
|
301
328
|
for (const principleFile of principleFiles) {
|
|
302
|
-
const relativePath = join(
|
|
329
|
+
const relativePath = join("docs", "principles", principleFile);
|
|
303
330
|
const sourcePath = join(principlesSourceDir, principleFile);
|
|
304
331
|
const targetPath = join(projectRoot, relativePath);
|
|
305
332
|
|
|
@@ -323,20 +350,35 @@ export interface DeployHuskyHookResult {
|
|
|
323
350
|
path: string;
|
|
324
351
|
}
|
|
325
352
|
|
|
326
|
-
export async function deployHuskyHook(
|
|
353
|
+
export async function deployHuskyHook(harnessRoot: string, projectRoot: string): Promise<DeployHuskyHookResult> {
|
|
354
|
+
const targetPath = join(projectRoot, ".husky", "pre-commit");
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
await fs.access(targetPath);
|
|
358
|
+
return { created: false, path: targetPath };
|
|
359
|
+
} catch {}
|
|
360
|
+
|
|
361
|
+
const sourcePath = join(harnessRoot, "templates", ".husky", "pre-commit");
|
|
362
|
+
await fs.mkdir(join(projectRoot, ".husky"), { recursive: true });
|
|
363
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
364
|
+
await fs.chmod(targetPath, 0o755);
|
|
365
|
+
|
|
366
|
+
return { created: true, path: targetPath };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export async function deployHuskyCommitMsgHook(
|
|
327
370
|
harnessRoot: string,
|
|
328
371
|
projectRoot: string,
|
|
329
372
|
): Promise<DeployHuskyHookResult> {
|
|
330
|
-
const targetPath = join(projectRoot,
|
|
373
|
+
const targetPath = join(projectRoot, ".husky", "commit-msg");
|
|
331
374
|
|
|
332
375
|
try {
|
|
333
376
|
await fs.access(targetPath);
|
|
334
377
|
return { created: false, path: targetPath };
|
|
335
|
-
} catch {
|
|
336
|
-
}
|
|
378
|
+
} catch {}
|
|
337
379
|
|
|
338
|
-
const sourcePath = join(harnessRoot,
|
|
339
|
-
await fs.mkdir(join(projectRoot,
|
|
380
|
+
const sourcePath = join(harnessRoot, "templates", ".husky", "commit-msg");
|
|
381
|
+
await fs.mkdir(join(projectRoot, ".husky"), { recursive: true });
|
|
340
382
|
await fs.copyFile(sourcePath, targetPath);
|
|
341
383
|
await fs.chmod(targetPath, 0o755);
|
|
342
384
|
|
|
@@ -348,20 +390,16 @@ export interface DeployCodexHooksResult {
|
|
|
348
390
|
path: string;
|
|
349
391
|
}
|
|
350
392
|
|
|
351
|
-
export async function deployCodexHooks(
|
|
352
|
-
|
|
353
|
-
projectRoot: string,
|
|
354
|
-
): Promise<DeployCodexHooksResult> {
|
|
355
|
-
const targetPath = join(projectRoot, '.codex', 'hooks.json');
|
|
393
|
+
export async function deployCodexHooks(harnessRoot: string, projectRoot: string): Promise<DeployCodexHooksResult> {
|
|
394
|
+
const targetPath = join(projectRoot, ".codex", "hooks.json");
|
|
356
395
|
|
|
357
396
|
try {
|
|
358
397
|
await fs.access(targetPath);
|
|
359
398
|
return { created: false, path: targetPath };
|
|
360
|
-
} catch {
|
|
361
|
-
}
|
|
399
|
+
} catch {}
|
|
362
400
|
|
|
363
|
-
const sourcePath = join(harnessRoot,
|
|
364
|
-
await fs.mkdir(join(projectRoot,
|
|
401
|
+
const sourcePath = join(harnessRoot, "templates", ".codex", "hooks.json");
|
|
402
|
+
await fs.mkdir(join(projectRoot, ".codex"), { recursive: true });
|
|
365
403
|
await fs.copyFile(sourcePath, targetPath);
|
|
366
404
|
|
|
367
405
|
return { created: true, path: targetPath };
|
|
@@ -6,28 +6,29 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export type SkillQualityErrorCode =
|
|
9
|
-
|
|
|
10
|
-
|
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
|
9
|
+
| "LOOP_ALREADY_COMPLETED"
|
|
10
|
+
| "LOOP_MAX_RETRIES_EXCEEDED"
|
|
11
|
+
| "LOOP_GAPS_NOT_EMPTY"
|
|
12
|
+
| "INVALID_STORY_ID"
|
|
13
|
+
| "DUPLICATE_LESSON_FINGERPRINT"
|
|
14
|
+
| "EMPTY_COMMIT_FIELD"
|
|
15
|
+
| "INVALID_WORK_ITEM_ID"
|
|
16
|
+
| "INVALID_COVERAGE_REPORT"
|
|
17
|
+
| "INVALID_REQUIREMENT_COVERAGE"
|
|
18
|
+
| "INVALID_COVERAGE_RANGE"
|
|
19
|
+
| "INVALID_LOOP_ATTEMPT"
|
|
20
|
+
| "EMPTY_LESSON_CONTENT"
|
|
21
|
+
| "EMPTY_SOURCE_CONTEXT"
|
|
22
|
+
| "EMPTY_FILE_PATH"
|
|
23
|
+
| "INVALID_UPDATED_COUNT"
|
|
24
|
+
| "EMPTY_MISSING_SECTIONS"
|
|
25
|
+
| "EMPTY_VIOLATIONS"
|
|
26
|
+
| "TDD_CYCLE_INCOMPLETE"
|
|
27
|
+
| "SKILL_FILE_NOT_FOUND"
|
|
28
|
+
| "GIT_COMMIT_FAILED"
|
|
29
|
+
| "COVERAGE_RUN_FAILED"
|
|
30
|
+
| "MATRIX_FILE_NOT_FOUND"
|
|
31
|
+
| "LESSON_ARTIFACT_SCHEMA_VIOLATION";
|
|
31
32
|
|
|
32
33
|
export class SkillQualityError extends Error {
|
|
33
34
|
readonly code: string;
|
|
@@ -35,6 +36,6 @@ export class SkillQualityError extends Error {
|
|
|
35
36
|
constructor(code: SkillQualityErrorCode, message: string) {
|
|
36
37
|
super(message);
|
|
37
38
|
this.code = code;
|
|
38
|
-
this.name =
|
|
39
|
+
this.name = "SkillQualityError";
|
|
39
40
|
}
|
|
40
41
|
}
|
|
@@ -2,32 +2,48 @@
|
|
|
2
2
|
* @layer domain
|
|
3
3
|
* @unit skill-quality
|
|
4
4
|
*/
|
|
5
|
-
import { SkillQualityError } from
|
|
5
|
+
import { SkillQualityError } from "../errors/skill-quality-error.js";
|
|
6
|
+
|
|
7
|
+
const WORK_ITEM_ID_PATTERN = /^WI-\d+$/;
|
|
6
8
|
|
|
7
9
|
export class CommitMessage {
|
|
8
10
|
readonly unit: string;
|
|
9
11
|
readonly storyId: string;
|
|
10
12
|
readonly description: string;
|
|
13
|
+
readonly workItemId: string | undefined;
|
|
11
14
|
|
|
12
|
-
private constructor(unit: string, storyId: string, description: string) {
|
|
15
|
+
private constructor(unit: string, storyId: string, description: string, workItemId?: string) {
|
|
13
16
|
this.unit = unit;
|
|
14
17
|
this.storyId = storyId;
|
|
15
18
|
this.description = description;
|
|
19
|
+
this.workItemId = workItemId;
|
|
16
20
|
Object.freeze(this);
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
static create(unit: string, storyId: string, description: string): CommitMessage {
|
|
23
|
+
static create(unit: string, storyId: string, description: string, workItemId?: string): CommitMessage {
|
|
20
24
|
if (!unit || !storyId || !description) {
|
|
21
|
-
throw new SkillQualityError(
|
|
25
|
+
throw new SkillQualityError("EMPTY_COMMIT_FIELD", "unit, storyId, description must be non-empty");
|
|
26
|
+
}
|
|
27
|
+
if (workItemId !== undefined && !WORK_ITEM_ID_PATTERN.test(workItemId)) {
|
|
28
|
+
throw new SkillQualityError("INVALID_WORK_ITEM_ID", "workItemId must match WI-<number>");
|
|
22
29
|
}
|
|
23
|
-
return new CommitMessage(unit, storyId, description);
|
|
30
|
+
return new CommitMessage(unit, storyId, description, workItemId);
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
format(): string {
|
|
27
|
-
|
|
34
|
+
const subject = `feat(${this.unit}/${this.storyId}): ${this.description}`;
|
|
35
|
+
if (this.workItemId === undefined) {
|
|
36
|
+
return subject;
|
|
37
|
+
}
|
|
38
|
+
return `${subject}\n\nWork-Item: ${this.workItemId}`;
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
equals(other: CommitMessage): boolean {
|
|
31
|
-
return
|
|
42
|
+
return (
|
|
43
|
+
this.unit === other.unit &&
|
|
44
|
+
this.storyId === other.storyId &&
|
|
45
|
+
this.description === other.description &&
|
|
46
|
+
this.workItemId === other.workItemId
|
|
47
|
+
);
|
|
32
48
|
}
|
|
33
49
|
}
|
package/scripts/harness/traceability-model/application/usecases/apply-work-item-migration-usecase.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @unit traceability-model
|
|
2
|
+
// @layer application
|
|
3
|
+
|
|
4
|
+
import type { WorkItemMigrationApplyPort } from "../../domain/ports/work-item-migration-apply-port.js";
|
|
5
|
+
import type {
|
|
6
|
+
WorkItemMigrationAppliedCandidate,
|
|
7
|
+
WorkItemMigrationApplyResult,
|
|
8
|
+
WorkItemMigrationCandidate,
|
|
9
|
+
} from "../../domain/value-objects/work-item-migration-candidate.js";
|
|
10
|
+
import type { PlanWorkItemMigrationUseCase } from "./plan-work-item-migration-usecase.js";
|
|
11
|
+
|
|
12
|
+
type PlanUseCase = Pick<PlanWorkItemMigrationUseCase, "execute">;
|
|
13
|
+
|
|
14
|
+
export interface ApplyWorkItemMigrationUseCaseDeps {
|
|
15
|
+
readonly planWorkItemMigrationUseCase: PlanUseCase;
|
|
16
|
+
readonly applyPort: WorkItemMigrationApplyPort;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class ApplyWorkItemMigrationUseCase {
|
|
20
|
+
private readonly planUseCase: PlanUseCase;
|
|
21
|
+
private readonly applyPort: WorkItemMigrationApplyPort;
|
|
22
|
+
|
|
23
|
+
constructor(deps: ApplyWorkItemMigrationUseCaseDeps) {
|
|
24
|
+
this.planUseCase = deps.planWorkItemMigrationUseCase;
|
|
25
|
+
this.applyPort = deps.applyPort;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async execute(): Promise<WorkItemMigrationApplyResult> {
|
|
29
|
+
const plan = await this.planUseCase.execute();
|
|
30
|
+
const conflicts = plan.candidates.filter((candidate) => candidate.conflict);
|
|
31
|
+
if (conflicts.length > 0) {
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
applied: Object.freeze([]),
|
|
34
|
+
skipped: Object.freeze(conflicts),
|
|
35
|
+
warnings: plan.warnings,
|
|
36
|
+
blocked: true,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const applied: WorkItemMigrationAppliedCandidate[] = [];
|
|
41
|
+
for (const candidate of plan.candidates) {
|
|
42
|
+
applied.push(await this.applyPort.apply(candidate));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
applied: Object.freeze(applied),
|
|
47
|
+
skipped: Object.freeze([] as WorkItemMigrationCandidate[]),
|
|
48
|
+
warnings: plan.warnings,
|
|
49
|
+
blocked: false,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
package/scripts/harness/traceability-model/application/usecases/plan-work-item-migration-usecase.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// @unit traceability-model
|
|
2
|
+
// @layer application
|
|
3
|
+
|
|
4
|
+
import type { WorkItemMigrationSourcePort } from "../../domain/ports/work-item-migration-source-port.js";
|
|
5
|
+
import { WorkItemMigrationPlanner } from "../../domain/services/work-item-migration-planner.js";
|
|
6
|
+
import type { WorkItemMigrationPlan } from "../../domain/value-objects/work-item-migration-candidate.js";
|
|
7
|
+
|
|
8
|
+
export interface PlanWorkItemMigrationUseCaseDeps {
|
|
9
|
+
readonly sourcePort: WorkItemMigrationSourcePort;
|
|
10
|
+
readonly planner?: WorkItemMigrationPlanner;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class PlanWorkItemMigrationUseCase {
|
|
14
|
+
private readonly sourcePort: WorkItemMigrationSourcePort;
|
|
15
|
+
private readonly planner: WorkItemMigrationPlanner;
|
|
16
|
+
|
|
17
|
+
constructor(deps: PlanWorkItemMigrationUseCaseDeps) {
|
|
18
|
+
this.sourcePort = deps.sourcePort;
|
|
19
|
+
this.planner = deps.planner ?? new WorkItemMigrationPlanner();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async execute(): Promise<WorkItemMigrationPlan> {
|
|
23
|
+
const [entries, existingWorkItemIds] = await Promise.all([
|
|
24
|
+
this.sourcePort.listLegacyIssueDirectories(),
|
|
25
|
+
this.sourcePort.listExistingWorkItemIds(),
|
|
26
|
+
]);
|
|
27
|
+
return this.planner.plan(entries, existingWorkItemIds);
|
|
28
|
+
}
|
|
29
|
+
}
|