phasegate 0.160.21 → 0.161.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 +18 -0
- package/README.md +6 -1
- package/docs/guide/cli-reference.md +1 -1
- package/docs/guide/configuration.md +31 -1
- package/docs/guide/skills-overview.md +2 -0
- package/docs/principles/model-routing.md +1 -0
- package/docs/templates/personal/phasegate-local-config.json +4 -1
- package/docs/templates/project/phasegate.config.json +4 -1
- package/package.json +1 -1
- package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +54 -5
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +5 -1
- package/scripts/harness/config-foundation/domain/harness-config.ts +9 -0
- package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +2 -0
- package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +20 -5
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +25 -0
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +25 -0
- package/scripts/harness/installation/application/usecases/run-install.ts +16 -3
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +48 -5
- package/scripts/harness/main.ts +31 -1
- package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +6 -1
- package/scripts/harness/setup/skill-deployer.ts +47 -0
- package/scripts/harness/skill-quality/domain/services/skill-structure-validator.ts +21 -0
- package/scripts/harness/skill-quality/domain/value-objects/skill-structure.ts +2 -0
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +18 -5
- package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +16 -3
- package/scripts/harness/validator-system/domain/ports/validator-config-port.ts +2 -0
- package/scripts/harness/validator-system/domain/services/validator-language-capability-service.ts +64 -0
- package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +7 -1
- package/skills/cascade-updater/SKILL.md +1 -0
- package/skills/codebase-mapper/SKILL.md +1 -0
- package/skills/codex-delegator/SKILL.md +1 -0
- package/skills/consistency-checker/SKILL.md +1 -0
- package/skills/doc-freshness-checker/SKILL.md +1 -0
- package/skills/domain-designer/SKILL.md +1 -0
- package/skills/engineering-perspective/SKILL.md +1 -0
- package/skills/environment-designer/SKILL.md +1 -0
- package/skills/implementation-planner/SKILL.md +1 -0
- package/skills/implementation-readiness-checker/SKILL.md +1 -0
- package/skills/it-test-designer/SKILL.md +1 -0
- package/skills/it-test-logic-designer/SKILL.md +1 -0
- package/skills/logical-designer/SKILL.md +1 -0
- package/skills/mock-designer/SKILL.md +1 -0
- package/skills/phasegate-config-doctor/SKILL.md +1 -0
- package/skills/phasegate-toolkit-guide/SKILL.md +1 -0
- package/skills/pointer-validator/SKILL.md +1 -0
- package/skills/product-architect/SKILL.md +1 -0
- package/skills/quick-implementor/SKILL.md +1 -0
- package/skills/scenario-test-designer/SKILL.md +1 -0
- package/skills/scenario-test-logic-designer/SKILL.md +1 -0
- package/skills/skill-creator/SKILL.md +1 -0
- package/skills/story-implementor/SKILL.md +1 -0
- package/skills/story-mapper/SKILL.md +1 -0
- package/skills/story-writer/SKILL.md +1 -0
- package/skills/test-coverage-checker/SKILL.md +1 -0
- package/skills/uiux-designer/SKILL.md +1 -0
- package/skills/unit-designer/SKILL.md +1 -0
- package/skills/unit-test-designer/SKILL.md +1 -0
- package/skills/unit-test-logic-designer/SKILL.md +1 -0
- package/templates/.claude/settings.json +4 -0
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
// @work-item-id WI-198
|
|
6
6
|
// @work-item-id WI-210
|
|
7
7
|
// @work-item-id WI-216
|
|
8
|
+
// @work-item-id WI-219
|
|
8
9
|
|
|
9
10
|
import { access, chmod, copyFile, lstat, mkdir, readFile, readlink, readdir, rm, symlink, writeFile } from "node:fs/promises";
|
|
10
11
|
import { dirname, join, relative, resolve } from "node:path";
|
|
12
|
+
import { readModelDelegationPolicy, renderSkillForModelDelegation } from "../../../setup/skill-deployer.js";
|
|
11
13
|
import { DeploymentEntry } from "../../domain/deployment-entry.js";
|
|
12
14
|
import { DeploymentManifest } from "../../domain/deployment-manifest.js";
|
|
13
15
|
import type { ManagedBlockInput } from "../../domain/managed-block.js";
|
|
@@ -96,13 +98,24 @@ async function copyDirectory(src: string, dest: string): Promise<void> {
|
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
async function copySelectedSkillDirectories(
|
|
101
|
+
async function copySelectedSkillDirectories(
|
|
102
|
+
harnessRoot: string,
|
|
103
|
+
projectRoot: string,
|
|
104
|
+
targetRoot: string,
|
|
105
|
+
skills: readonly string[],
|
|
106
|
+
): Promise<void> {
|
|
100
107
|
await mkdir(targetRoot, { recursive: true });
|
|
108
|
+
const modelDelegationPolicy = await readModelDelegationPolicy(projectRoot);
|
|
101
109
|
for (const skill of skills) {
|
|
102
110
|
const source = join(harnessRoot, "skills", skill);
|
|
103
111
|
const target = join(targetRoot, skill);
|
|
104
112
|
await rm(target, { recursive: true, force: true });
|
|
105
113
|
await copyDirectory(source, target);
|
|
114
|
+
if (modelDelegationPolicy === "none") {
|
|
115
|
+
const skillPath = join(target, "SKILL.md");
|
|
116
|
+
const content = await readFile(skillPath, "utf8");
|
|
117
|
+
await writeFile(skillPath, renderSkillForModelDelegation(content, modelDelegationPolicy), "utf8");
|
|
118
|
+
}
|
|
106
119
|
}
|
|
107
120
|
}
|
|
108
121
|
|
|
@@ -115,6 +128,24 @@ async function listSelectedBundledSkills(harnessRoot: string, skillSet: SkillSet
|
|
|
115
128
|
.sort();
|
|
116
129
|
}
|
|
117
130
|
|
|
131
|
+
async function hasRenderedSkillDrift(
|
|
132
|
+
harnessRoot: string,
|
|
133
|
+
projectRoot: string,
|
|
134
|
+
targetRoot: string,
|
|
135
|
+
skills: readonly string[],
|
|
136
|
+
): Promise<boolean> {
|
|
137
|
+
const modelDelegationPolicy = await readModelDelegationPolicy(projectRoot);
|
|
138
|
+
for (const skill of skills) {
|
|
139
|
+
const current = await readTextOrNull(join(targetRoot, skill, "SKILL.md"));
|
|
140
|
+
if (current === null) return true;
|
|
141
|
+
const source = await readTextOrNull(join(harnessRoot, "skills", skill, "SKILL.md"));
|
|
142
|
+
if (source === null) continue;
|
|
143
|
+
const expected = renderSkillForModelDelegation(source, modelDelegationPolicy);
|
|
144
|
+
if (current !== expected) return true;
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
118
149
|
function normalizeJsonEntry(value: unknown): string {
|
|
119
150
|
return JSON.stringify(value);
|
|
120
151
|
}
|
|
@@ -491,7 +522,13 @@ export class RunReconcileUseCase {
|
|
|
491
522
|
}
|
|
492
523
|
const missingManifest = manifest.findEntry(SHARED_SKILLS_VERSION_PATH) === null
|
|
493
524
|
|| skills.some((skill) => manifest.findEntry(`skills/${skill}`) === null);
|
|
494
|
-
const
|
|
525
|
+
const renderedSkillDrift = await hasRenderedSkillDrift(
|
|
526
|
+
input.harnessRoot,
|
|
527
|
+
input.projectRoot,
|
|
528
|
+
this.resolveProjectPath(input.projectRoot, "skills"),
|
|
529
|
+
skills,
|
|
530
|
+
);
|
|
531
|
+
const changed = versionContent === null || !versionContent.includes(expectedVersion) || missingSkill || missingManifest || renderedSkillDrift;
|
|
495
532
|
return {
|
|
496
533
|
item: this.item(
|
|
497
534
|
"skills",
|
|
@@ -514,7 +551,7 @@ export class RunReconcileUseCase {
|
|
|
514
551
|
|
|
515
552
|
private async deploySharedSkills(input: RunReconcileInput, skills: readonly string[], skillSet: SkillSet): Promise<void> {
|
|
516
553
|
const targetRoot = this.resolveProjectPath(input.projectRoot, "skills");
|
|
517
|
-
await copySelectedSkillDirectories(input.harnessRoot, targetRoot, skills);
|
|
554
|
+
await copySelectedSkillDirectories(input.harnessRoot, input.projectRoot, targetRoot, skills);
|
|
518
555
|
await writeFile(
|
|
519
556
|
join(targetRoot, ".harness-version"),
|
|
520
557
|
`${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
|
|
@@ -541,7 +578,13 @@ export class RunReconcileUseCase {
|
|
|
541
578
|
}
|
|
542
579
|
const missingManifest = manifest.findEntry(`${relativePath}/.harness-version`) === null
|
|
543
580
|
|| skills.some((skill) => manifest.findEntry(`${relativePath}/${skill}`) === null);
|
|
544
|
-
const
|
|
581
|
+
const renderedSkillDrift = await hasRenderedSkillDrift(
|
|
582
|
+
input.harnessRoot,
|
|
583
|
+
input.projectRoot,
|
|
584
|
+
this.resolveProjectPath(input.projectRoot, relativePath),
|
|
585
|
+
skills,
|
|
586
|
+
);
|
|
587
|
+
const changed = versionContent === null || !versionContent.includes(expectedVersion) || missingSkill || missingManifest || renderedSkillDrift;
|
|
545
588
|
return {
|
|
546
589
|
item: this.item(
|
|
547
590
|
relativePath,
|
|
@@ -557,7 +600,7 @@ export class RunReconcileUseCase {
|
|
|
557
600
|
apply: async () => {
|
|
558
601
|
if (!changed) return null;
|
|
559
602
|
const targetRoot = this.resolveProjectPath(input.projectRoot, relativePath);
|
|
560
|
-
await copySelectedSkillDirectories(input.harnessRoot, targetRoot, skills);
|
|
603
|
+
await copySelectedSkillDirectories(input.harnessRoot, input.projectRoot, targetRoot, skills);
|
|
561
604
|
await writeFile(
|
|
562
605
|
join(targetRoot, ".harness-version"),
|
|
563
606
|
`${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
|
package/scripts/harness/main.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* @work-item-id WI-189
|
|
11
11
|
* @work-item-id WI-191
|
|
12
12
|
* @work-item-id WI-195
|
|
13
|
+
* @work-item-id WI-212
|
|
13
14
|
* @work-item-id WI-196
|
|
14
15
|
* @work-item-id WI-197
|
|
15
16
|
* @work-item-id WI-200
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
* @work-item-id WI-206
|
|
20
21
|
* @work-item-id WI-213
|
|
21
22
|
* @work-item-id WI-217
|
|
23
|
+
* @work-item-id WI-219
|
|
22
24
|
*
|
|
23
25
|
* Phasegate CLI エントリポイント。
|
|
24
26
|
* 各Unitの Composition Root からハンドラーを取得し、コマンドに応じてディスパッチする。
|
|
@@ -122,6 +124,19 @@ function isJsonRecord(value: unknown): value is Record<string, unknown> {
|
|
|
122
124
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
async function isModelDelegationDisabled(rootDir: string): Promise<boolean> {
|
|
128
|
+
try {
|
|
129
|
+
const raw = await fsReadFile(join(rootDir, "phasegate.config.json"), "utf-8");
|
|
130
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
131
|
+
if (!isJsonRecord(parsed) || !isJsonRecord(parsed.modelRouting)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
return parsed.modelRouting.delegation === "none";
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
125
140
|
async function ensurePhasegatePackageDependency(rootDir: string, version: string): Promise<PackageDependencyResult> {
|
|
126
141
|
const packageJsonPath = join(rootDir, "package.json");
|
|
127
142
|
let pkg: PackageJsonDocument = {};
|
|
@@ -167,7 +182,7 @@ Setup:
|
|
|
167
182
|
init Initialize project: deploy skills + design docs + phasegate.config.json
|
|
168
183
|
(--name <project-name>, --preset <full|standard|minimal|custom>,
|
|
169
184
|
--skills <core|all>, --agent <claude|codex|both>, --workflow <standard|strict>,
|
|
170
|
-
--with-husky, --with-ci, --yes)
|
|
185
|
+
--language <language>, --with-husky, --with-ci, --yes)
|
|
171
186
|
update-skills Alias for reconcile (kept for compatibility)
|
|
172
187
|
doctor Diagnose silent installation failures (--json, --strict, --personal, --agent <claude|codex|both>, --report-out <path>)
|
|
173
188
|
scaffold-wi <unit|_cross> <story|issue|fix|refactor|chore>
|
|
@@ -605,6 +620,7 @@ Options:
|
|
|
605
620
|
--skills <core|all> Skill set to deploy (default: "all")
|
|
606
621
|
--agent <claude|codex|both> Agent integration target (default: "claude")
|
|
607
622
|
--workflow <standard|strict> Workflow enforcement defaults (default: "standard")
|
|
623
|
+
--language <language> Project language declaration (default: "typescript")
|
|
608
624
|
--with-husky Install Husky pre-commit hooks
|
|
609
625
|
--with-ci Install GitHub Actions workflows
|
|
610
626
|
--yes Skip confirmation prompts
|
|
@@ -1740,6 +1756,7 @@ async function main(): Promise<void> {
|
|
|
1740
1756
|
"--skills",
|
|
1741
1757
|
"--agent",
|
|
1742
1758
|
"--workflow",
|
|
1759
|
+
"--language",
|
|
1743
1760
|
"--with-husky",
|
|
1744
1761
|
"--with-ci",
|
|
1745
1762
|
"--yes",
|
|
@@ -1780,6 +1797,11 @@ async function main(): Promise<void> {
|
|
|
1780
1797
|
process.exit(2);
|
|
1781
1798
|
}
|
|
1782
1799
|
const workflow = parseWorkflowMode(workflowRaw);
|
|
1800
|
+
const language = parseFlag(args, "--language") ?? "typescript";
|
|
1801
|
+
if (language.trim() === "") {
|
|
1802
|
+
console.error("Invalid --language value: value must not be empty.");
|
|
1803
|
+
process.exit(2);
|
|
1804
|
+
}
|
|
1783
1805
|
const deployClaude = agent === "claude" || agent === "both";
|
|
1784
1806
|
const deployCodex = agent === "codex" || agent === "both";
|
|
1785
1807
|
const result = await deploySkills(harnessRoot, rootDir, skillSet);
|
|
@@ -1792,6 +1814,7 @@ async function main(): Promise<void> {
|
|
|
1792
1814
|
const configResult = await initHarnessConfig(rootDir, projectName, phasePreset, {
|
|
1793
1815
|
ciEnabled: withCi,
|
|
1794
1816
|
workflow,
|
|
1817
|
+
language,
|
|
1795
1818
|
});
|
|
1796
1819
|
if (workflow === "strict") {
|
|
1797
1820
|
await scaffoldInceptionRoots(rootDir);
|
|
@@ -3133,6 +3156,13 @@ Examples:
|
|
|
3133
3156
|
printSubcommandHelp("delegate-sonnet");
|
|
3134
3157
|
process.exit(0);
|
|
3135
3158
|
}
|
|
3159
|
+
if (!hasFlag(args, "--dry-run") && await isModelDelegationDisabled(rootDir)) {
|
|
3160
|
+
console.error(JSON.stringify({
|
|
3161
|
+
code: "MODEL_DELEGATION_DISABLED",
|
|
3162
|
+
message: "phasegate delegate-sonnet is disabled by modelRouting.delegation=none",
|
|
3163
|
+
}));
|
|
3164
|
+
process.exit(1);
|
|
3165
|
+
}
|
|
3136
3166
|
const { spawn } = await import("node:child_process");
|
|
3137
3167
|
const scriptPath = join(harnessRoot, "scripts/delegate-sonnet.sh");
|
|
3138
3168
|
const forwardArgs = args.slice(1);
|
|
@@ -77,7 +77,12 @@ export class QuickModeConfig {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
isMaintained(validatorId: string): boolean {
|
|
80
|
-
|
|
80
|
+
// maintainedLayers の各エントリ P について、レイヤー名エントリ(例 "L2")は
|
|
81
|
+
// そのレイヤーの全 ID(例 "L2-002")にマッチし、ID 完全一致(例 "L2-002")も
|
|
82
|
+
// 引き続きサポートする。
|
|
83
|
+
return this.maintainedLayers.some(
|
|
84
|
+
(entry) => validatorId === entry || validatorId.startsWith(`${entry}-`)
|
|
85
|
+
);
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
isRelaxed(validatorId: string): boolean {
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// @work-item-id WI-127
|
|
5
5
|
// @work-item-id WI-184
|
|
6
6
|
// @work-item-id WI-202
|
|
7
|
+
// @work-item-id WI-212
|
|
8
|
+
// @work-item-id WI-219
|
|
7
9
|
// Note: import.meta.url を使わず、呼び出し元 (main.ts) がパスを解決して渡す設計。
|
|
8
10
|
|
|
9
11
|
import { promises as fs } from "node:fs";
|
|
@@ -151,6 +153,44 @@ async function copyDirectory(src: string, dest: string): Promise<number> {
|
|
|
151
153
|
return count;
|
|
152
154
|
}
|
|
153
155
|
|
|
156
|
+
type ModelDelegationPolicy = "delegate-sonnet" | "none";
|
|
157
|
+
|
|
158
|
+
export async function readModelDelegationPolicy(projectRoot: string): Promise<ModelDelegationPolicy> {
|
|
159
|
+
try {
|
|
160
|
+
const raw = await fs.readFile(join(projectRoot, HARNESS_CONFIG_FILE), "utf-8");
|
|
161
|
+
const config = JSON.parse(raw) as { modelRouting?: { delegation?: unknown } };
|
|
162
|
+
return config.modelRouting?.delegation === "none" ? "none" : "delegate-sonnet";
|
|
163
|
+
} catch {
|
|
164
|
+
return "delegate-sonnet";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function renderSkillForModelDelegation(content: string, policy: ModelDelegationPolicy): string {
|
|
169
|
+
if (policy !== "none") {
|
|
170
|
+
return content;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return content
|
|
174
|
+
.replace(/^model:\s+\S+\n/gm, "")
|
|
175
|
+
.replace(/^review:\s+\S+\n/gm, "")
|
|
176
|
+
.replaceAll("Sonnet 4.6 に委任して成果物を生成する(`npx phasegate delegate-sonnet` 経由)", "メインセッションが成果物を生成する")
|
|
177
|
+
.replaceAll("Opus が", "メインセッションが")
|
|
178
|
+
.replaceAll("Sonnet 4.6", "メインセッション")
|
|
179
|
+
.replaceAll("`npx phasegate delegate-sonnet`", "メインセッション");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function applyModelDelegationPolicyToSkill(skillDir: string, policy: ModelDelegationPolicy): Promise<void> {
|
|
183
|
+
if (policy !== "none") {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const skillPath = join(skillDir, "SKILL.md");
|
|
188
|
+
try {
|
|
189
|
+
const content = await fs.readFile(skillPath, "utf-8");
|
|
190
|
+
await fs.writeFile(skillPath, renderSkillForModelDelegation(content, policy), "utf-8");
|
|
191
|
+
} catch {}
|
|
192
|
+
}
|
|
193
|
+
|
|
154
194
|
export async function deploySkills(
|
|
155
195
|
harnessRoot: string,
|
|
156
196
|
projectRoot: string,
|
|
@@ -160,6 +200,7 @@ export async function deploySkills(
|
|
|
160
200
|
const skillsTarget = join(projectRoot, SKILLS_TARGET_DIR);
|
|
161
201
|
const version = await getHarnessVersion(harnessRoot);
|
|
162
202
|
const deployedAt = new Date().toISOString();
|
|
203
|
+
const modelDelegationPolicy = await readModelDelegationPolicy(projectRoot);
|
|
163
204
|
|
|
164
205
|
const skillDirs = await fs.readdir(skillsSource, { withFileTypes: true });
|
|
165
206
|
const allSkills = skillDirs.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
@@ -172,6 +213,7 @@ export async function deploySkills(
|
|
|
172
213
|
const srcSkillDir = join(skillsSource, skill);
|
|
173
214
|
const destSkillDir = join(skillsTarget, skill);
|
|
174
215
|
await copyDirectory(srcSkillDir, destSkillDir);
|
|
216
|
+
await applyModelDelegationPolicyToSkill(destSkillDir, modelDelegationPolicy);
|
|
175
217
|
}
|
|
176
218
|
|
|
177
219
|
const versionInfo: VersionInfo = { version, deployedAt, skillSet };
|
|
@@ -456,6 +498,7 @@ export async function deployHookScripts(harnessRoot: string, projectRoot: string
|
|
|
456
498
|
export interface InitHarnessConfigOptions {
|
|
457
499
|
ciEnabled?: boolean;
|
|
458
500
|
workflow?: "standard" | "strict";
|
|
501
|
+
language?: string;
|
|
459
502
|
}
|
|
460
503
|
|
|
461
504
|
export async function initHarnessConfig(
|
|
@@ -477,6 +520,7 @@ export async function initHarnessConfig(
|
|
|
477
520
|
project: {
|
|
478
521
|
name: projectName,
|
|
479
522
|
preset: "standard",
|
|
523
|
+
languages: [options.language ?? "typescript"],
|
|
480
524
|
},
|
|
481
525
|
architecture: {
|
|
482
526
|
preset: "clean",
|
|
@@ -497,6 +541,9 @@ export async function initHarnessConfig(
|
|
|
497
541
|
default: "interactive",
|
|
498
542
|
perPhase: {},
|
|
499
543
|
},
|
|
544
|
+
modelRouting: {
|
|
545
|
+
delegation: "delegate-sonnet",
|
|
546
|
+
},
|
|
500
547
|
harnesses: {},
|
|
501
548
|
paths: {
|
|
502
549
|
designDocs: "docs/product/construction",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer domain
|
|
3
3
|
* @unit skill-quality
|
|
4
|
+
* @work-item-id WI-212
|
|
4
5
|
*/
|
|
5
6
|
import { SkillStructure } from '../value-objects/skill-structure.js';
|
|
6
7
|
import { SkillValidationResult } from '../value-objects/skill-validation-result.js';
|
|
@@ -29,6 +30,9 @@ export class SkillStructureValidator {
|
|
|
29
30
|
// Check for frontmatter (YAML between --- delimiters)
|
|
30
31
|
if (lines[0]?.trim() === '---') {
|
|
31
32
|
sections.push('frontmatter');
|
|
33
|
+
if (this.hasLanguageMetadata(lines)) {
|
|
34
|
+
sections.push('languageMetadata');
|
|
35
|
+
}
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
// Look for headings that correspond to section names
|
|
@@ -69,4 +73,21 @@ export class SkillStructureValidator {
|
|
|
69
73
|
|
|
70
74
|
return sections;
|
|
71
75
|
}
|
|
76
|
+
|
|
77
|
+
private hasLanguageMetadata(lines: readonly string[]): boolean {
|
|
78
|
+
const closingDelimiterIndex = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
|
|
79
|
+
if (closingDelimiterIndex < 0) return false;
|
|
80
|
+
|
|
81
|
+
const frontmatterLines = lines.slice(1, closingDelimiterIndex);
|
|
82
|
+
const languageLineIndex = frontmatterLines.findIndex((line) => /^\s*languages\s*:/.test(line));
|
|
83
|
+
if (languageLineIndex < 0) return false;
|
|
84
|
+
|
|
85
|
+
const languageLine = frontmatterLines[languageLineIndex] ?? '';
|
|
86
|
+
if (/\[[^\]]*\S[^\]]*\]/.test(languageLine)) return true;
|
|
87
|
+
|
|
88
|
+
return frontmatterLines.slice(languageLineIndex + 1).some((line) => {
|
|
89
|
+
if (/^\S/.test(line)) return false;
|
|
90
|
+
return /^\s*-\s*\S+/.test(line);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
72
93
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer domain
|
|
3
3
|
* @unit skill-quality
|
|
4
|
+
* @work-item-id WI-212
|
|
4
5
|
*/
|
|
5
6
|
import type { SectionName } from '../types/section-name.js';
|
|
6
7
|
|
|
7
8
|
const REQUIRED_SECTIONS: readonly SectionName[] = [
|
|
8
9
|
'frontmatter',
|
|
10
|
+
'languageMetadata',
|
|
9
11
|
'purpose',
|
|
10
12
|
'inputs',
|
|
11
13
|
'outputs',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer application
|
|
3
3
|
* @unit validator-system
|
|
4
|
+
* @work-item-id WI-212
|
|
4
5
|
*
|
|
5
6
|
* RunL3ValidatorsUseCase — H08-02: L3バリデータ実行
|
|
6
7
|
*/
|
|
@@ -8,6 +9,7 @@ import { ValidatorId } from '../../domain/value-objects/validator-id.js';
|
|
|
8
9
|
import { ValidationResult } from '../../domain/value-objects/validation-result.js';
|
|
9
10
|
import { ValidatorRegistry } from '../../domain/services/validator-registry.js';
|
|
10
11
|
import { ValidatorExecutionService, ValidatorExecutionError } from '../../domain/services/validator-execution-service.js';
|
|
12
|
+
import { ValidatorLanguageCapabilityService } from '../../domain/services/validator-language-capability-service.js';
|
|
11
13
|
import { ValidationResultContractMapper } from '../mappers/validation-result-contract-mapper.js';
|
|
12
14
|
import type { ValidationResultContract } from '../dto/validation-result-contract.js';
|
|
13
15
|
import type { RunL3ValidatorsInput } from '../dto/run-l3-validators-input.js';
|
|
@@ -43,6 +45,7 @@ export class RunL3ValidatorsUseCase {
|
|
|
43
45
|
private readonly coverageReportPort?: RunL3ValidatorsUseCaseDeps['coverageReportPort'];
|
|
44
46
|
private readonly securityScannerPort?: SecurityPatternScannerPort;
|
|
45
47
|
private readonly performanceScannerPort?: PerformanceScannerPort;
|
|
48
|
+
private readonly languageCapabilityService = new ValidatorLanguageCapabilityService();
|
|
46
49
|
|
|
47
50
|
constructor(deps: RunL3ValidatorsUseCaseDeps) {
|
|
48
51
|
this.registry = deps.validatorRegistry;
|
|
@@ -77,8 +80,12 @@ export class RunL3ValidatorsUseCase {
|
|
|
77
80
|
return [];
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
const projectLanguages = await this.getProjectLanguages();
|
|
84
|
+
const { executableDefinitions, unsupportedResults, unsupportedValidatorIds } =
|
|
85
|
+
this.languageCapabilityService.splitDefinitions(definitions, projectLanguages);
|
|
86
|
+
|
|
80
87
|
// coverageReportPort が存在する場合、カバレッジを取得して判定
|
|
81
|
-
if (this.coverageReportPort) {
|
|
88
|
+
if (this.coverageReportPort && !unsupportedValidatorIds.has('L3-003')) {
|
|
82
89
|
const coverageData = await this.coverageReportPort.getCoverage();
|
|
83
90
|
const threshold = layerConfig.getThreshold('coverageThreshold');
|
|
84
91
|
|
|
@@ -87,9 +94,9 @@ export class RunL3ValidatorsUseCase {
|
|
|
87
94
|
const deficit = threshold - coverageData.overallCoverage;
|
|
88
95
|
|
|
89
96
|
// 他のバリデータを実行
|
|
90
|
-
const otherDefs =
|
|
97
|
+
const otherDefs = executableDefinitions.filter((d) => d.validatorId.value !== 'L3-003');
|
|
91
98
|
const otherResults = this.executionService.execute(otherDefs, [layerConfig]);
|
|
92
|
-
const otherContracts = this.mapper.toContracts(otherResults);
|
|
99
|
+
const otherContracts = this.mapper.toContracts([...unsupportedResults, ...otherResults]);
|
|
93
100
|
|
|
94
101
|
return [
|
|
95
102
|
...otherContracts.filter((r) => r.validatorId !== 'L3-003'),
|
|
@@ -104,8 +111,10 @@ export class RunL3ValidatorsUseCase {
|
|
|
104
111
|
}
|
|
105
112
|
}
|
|
106
113
|
|
|
107
|
-
const results = this.executionService.execute(
|
|
108
|
-
const overrideMap = new Map<string, ValidationResult>(
|
|
114
|
+
const results = this.executionService.execute(executableDefinitions, [layerConfig]);
|
|
115
|
+
const overrideMap = new Map<string, ValidationResult>(
|
|
116
|
+
[...unsupportedResults, ...results].map((result) => [result.validatorId.value, result]),
|
|
117
|
+
);
|
|
109
118
|
|
|
110
119
|
if (this.securityScannerPort) {
|
|
111
120
|
const l3001Result = overrideMap.get('L3-001');
|
|
@@ -153,4 +162,8 @@ export class RunL3ValidatorsUseCase {
|
|
|
153
162
|
);
|
|
154
163
|
return this.mapper.toContracts(finalResults);
|
|
155
164
|
}
|
|
165
|
+
|
|
166
|
+
private async getProjectLanguages(): Promise<readonly string[]> {
|
|
167
|
+
return this.configPort.getProjectLanguages ? await this.configPort.getProjectLanguages() : ['typescript'];
|
|
168
|
+
}
|
|
156
169
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @unit validator-system
|
|
4
4
|
* @work-item-id WI-107 / WI-156
|
|
5
5
|
* @work-item-id WI-217
|
|
6
|
+
* @work-item-id WI-212
|
|
6
7
|
*
|
|
7
8
|
* RunL4ValidatorsUseCase — H08-03: L4バリデータ実行
|
|
8
9
|
*/
|
|
@@ -11,6 +12,7 @@ import { ValidationResult } from '../../domain/value-objects/validation-result.j
|
|
|
11
12
|
import { LayerConfig } from '../../domain/value-objects/layer-config.js';
|
|
12
13
|
import { ValidatorRegistry } from '../../domain/services/validator-registry.js';
|
|
13
14
|
import { ValidatorExecutionService, ValidatorExecutionError } from '../../domain/services/validator-execution-service.js';
|
|
15
|
+
import { ValidatorLanguageCapabilityService } from '../../domain/services/validator-language-capability-service.js';
|
|
14
16
|
import { ValidationResultContractMapper } from '../mappers/validation-result-contract-mapper.js';
|
|
15
17
|
import type { ValidationResultContract } from '../dto/validation-result-contract.js';
|
|
16
18
|
import type { RunL4ValidatorsInput } from '../dto/run-l4-validators-input.js';
|
|
@@ -93,6 +95,7 @@ export class RunL4ValidatorsUseCase {
|
|
|
93
95
|
private readonly pathRoots: { readonly inceptionRoot: string; readonly designRoot: string };
|
|
94
96
|
private readonly checkDocFreshnessUseCase?: CheckDocFreshnessUseCasePort;
|
|
95
97
|
private readonly validateDocPointersUseCase?: ValidateDocPointersUseCasePort;
|
|
98
|
+
private readonly languageCapabilityService = new ValidatorLanguageCapabilityService();
|
|
96
99
|
|
|
97
100
|
constructor(deps: RunL4ValidatorsUseCaseDeps) {
|
|
98
101
|
this.registry = deps.validatorRegistry;
|
|
@@ -141,11 +144,17 @@ export class RunL4ValidatorsUseCase {
|
|
|
141
144
|
thresholds: { ...layerConfig.thresholds },
|
|
142
145
|
strictOnly: layerConfig.strictOnly,
|
|
143
146
|
preset: layerConfig.preset,
|
|
144
|
-
|
|
147
|
+
})
|
|
145
148
|
: layerConfig;
|
|
146
149
|
|
|
147
|
-
const
|
|
148
|
-
const
|
|
150
|
+
const projectLanguages = await this.getProjectLanguages();
|
|
151
|
+
const { executableDefinitions, unsupportedResults } =
|
|
152
|
+
this.languageCapabilityService.splitDefinitions(definitions, projectLanguages);
|
|
153
|
+
|
|
154
|
+
const results = this.executionService.execute(executableDefinitions, [effectiveLayerConfig]);
|
|
155
|
+
const overrideMap = new Map<string, ValidationResult>(
|
|
156
|
+
[...unsupportedResults, ...results].map((result) => [result.validatorId.value, result]),
|
|
157
|
+
);
|
|
149
158
|
|
|
150
159
|
if (this.driftDetectionService) {
|
|
151
160
|
const l4001Result = overrideMap.get('L4-001');
|
|
@@ -276,6 +285,10 @@ export class RunL4ValidatorsUseCase {
|
|
|
276
285
|
return [...executionErrors, ...freshnessFindings];
|
|
277
286
|
}
|
|
278
287
|
|
|
288
|
+
private async getProjectLanguages(): Promise<readonly string[]> {
|
|
289
|
+
return this.configPort.getProjectLanguages ? await this.configPort.getProjectLanguages() : ['typescript'];
|
|
290
|
+
}
|
|
291
|
+
|
|
279
292
|
private usesConfiguredDocumentRoots(): boolean {
|
|
280
293
|
return this.pathRoots.inceptionRoot !== 'docs/inception' || this.pathRoots.designRoot !== 'docs/product/construction';
|
|
281
294
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer domain
|
|
3
3
|
* @unit validator-system
|
|
4
|
+
* @work-item-id WI-212
|
|
4
5
|
*
|
|
5
6
|
* ValidatorConfigPort — HarnessConfigV2からLayerConfig取得
|
|
6
7
|
*/
|
|
@@ -8,4 +9,5 @@ import type { LayerConfig } from '../value-objects/layer-config.js';
|
|
|
8
9
|
|
|
9
10
|
export interface ValidatorConfigPort {
|
|
10
11
|
getLayerConfig(layer: 'L2' | 'L3' | 'L4'): Promise<LayerConfig>;
|
|
12
|
+
getProjectLanguages?(): Promise<readonly string[]>;
|
|
11
13
|
}
|
package/scripts/harness/validator-system/domain/services/validator-language-capability-service.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer domain
|
|
3
|
+
* @unit validator-system
|
|
4
|
+
* @work-item-id WI-212
|
|
5
|
+
*/
|
|
6
|
+
import type { ValidatorId } from '../value-objects/validator-id.js';
|
|
7
|
+
import { ValidationResult } from '../value-objects/validation-result.js';
|
|
8
|
+
import type { ValidatorDefinition } from '../value-objects/validator-definition.js';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_SUPPORTED_LANGUAGES = ['typescript'] as const;
|
|
11
|
+
|
|
12
|
+
const VALIDATOR_LANGUAGE_CAPABILITIES: Readonly<Record<string, readonly string[]>> = {
|
|
13
|
+
'L3-002': DEFAULT_SUPPORTED_LANGUAGES,
|
|
14
|
+
'L3-003': DEFAULT_SUPPORTED_LANGUAGES,
|
|
15
|
+
'L4-003': DEFAULT_SUPPORTED_LANGUAGES,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class ValidatorLanguageCapabilityService {
|
|
19
|
+
getUnsupportedValidatorIds(
|
|
20
|
+
validatorIds: readonly ValidatorId[],
|
|
21
|
+
projectLanguages: readonly string[],
|
|
22
|
+
): ReadonlySet<string> {
|
|
23
|
+
const normalizedProjectLanguages = new Set(projectLanguages.map((language) => language.toLowerCase()));
|
|
24
|
+
const unsupportedIds = validatorIds
|
|
25
|
+
.filter((validatorId) => {
|
|
26
|
+
const supportedLanguages = VALIDATOR_LANGUAGE_CAPABILITIES[validatorId.value];
|
|
27
|
+
if (!supportedLanguages) return false;
|
|
28
|
+
return !supportedLanguages.some((language) => normalizedProjectLanguages.has(language));
|
|
29
|
+
})
|
|
30
|
+
.map((validatorId) => validatorId.value);
|
|
31
|
+
|
|
32
|
+
return new Set(unsupportedIds);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
splitDefinitions(
|
|
36
|
+
definitions: readonly ValidatorDefinition[],
|
|
37
|
+
projectLanguages: readonly string[],
|
|
38
|
+
): {
|
|
39
|
+
readonly executableDefinitions: readonly ValidatorDefinition[];
|
|
40
|
+
readonly unsupportedResults: readonly ValidationResult[];
|
|
41
|
+
readonly unsupportedValidatorIds: ReadonlySet<string>;
|
|
42
|
+
} {
|
|
43
|
+
const unsupportedValidatorIds = this.getUnsupportedValidatorIds(
|
|
44
|
+
definitions.map((definition) => definition.validatorId),
|
|
45
|
+
projectLanguages,
|
|
46
|
+
);
|
|
47
|
+
const executableDefinitions = definitions.filter(
|
|
48
|
+
(definition) => !unsupportedValidatorIds.has(definition.validatorId.value),
|
|
49
|
+
);
|
|
50
|
+
const unsupportedResults = definitions
|
|
51
|
+
.filter((definition) => unsupportedValidatorIds.has(definition.validatorId.value))
|
|
52
|
+
.map((definition) => ValidationResult.skipWithReason(
|
|
53
|
+
definition.validatorId,
|
|
54
|
+
this.createSkipReason(definition.validatorId, projectLanguages),
|
|
55
|
+
));
|
|
56
|
+
|
|
57
|
+
return { executableDefinitions, unsupportedResults, unsupportedValidatorIds };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private createSkipReason(validatorId: ValidatorId, projectLanguages: readonly string[]): string {
|
|
61
|
+
const supportedLanguages = VALIDATOR_LANGUAGE_CAPABILITIES[validatorId.value] ?? DEFAULT_SUPPORTED_LANGUAGES;
|
|
62
|
+
return `unsupported-language: ${validatorId.value} currently supports ${supportedLanguages.join(', ')}; project languages are ${projectLanguages.join(', ')}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @layer infrastructure
|
|
3
3
|
* @unit validator-system
|
|
4
4
|
* @work-item-id WI-156
|
|
5
|
+
* @work-item-id WI-212
|
|
5
6
|
*
|
|
6
7
|
* HarnessConfigValidatorConfigAdapter — ValidatorConfigPort実装
|
|
7
8
|
* HarnessConfigV2からLayerConfig VOを構築する
|
|
@@ -18,7 +19,7 @@ export interface HarnessConfigLayers {
|
|
|
18
19
|
|
|
19
20
|
export interface HarnessConfigV2Like {
|
|
20
21
|
preset?: 'minimal' | 'standard' | 'strict';
|
|
21
|
-
project?: { preset?: 'minimal' | 'standard' | 'strict' };
|
|
22
|
+
project?: { preset?: 'minimal' | 'standard' | 'strict'; languages?: readonly string[] };
|
|
22
23
|
layers?: HarnessConfigLayers;
|
|
23
24
|
harnesses?: {
|
|
24
25
|
bundleSizeLimit?: number;
|
|
@@ -71,6 +72,11 @@ export class HarnessConfigValidatorConfigAdapter implements ValidatorConfigPort
|
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
async getProjectLanguages(): Promise<readonly string[]> {
|
|
76
|
+
const languages = this.config.project?.languages;
|
|
77
|
+
return languages && languages.length > 0 ? [...languages] : ['typescript'];
|
|
78
|
+
}
|
|
79
|
+
|
|
74
80
|
private normalizeValidatorId(idOrName: string): string {
|
|
75
81
|
try {
|
|
76
82
|
return ValidatorId.create(idOrName).value;
|
|
@@ -3,6 +3,7 @@ name: codebase-mapper
|
|
|
3
3
|
description: コードベースの構造マップを生成するスキル。全ソースファイルの `@unit`/`@layer` アノテーションを解析し、Unit・レイヤー分布・Unit間依存関係・循環依存を可視化したマップ文書を出力する。L4バリデータ(drift-detection/dead-code)の入力精度向上にも使用。使用タイミング: 「コードベースの構造を把握したい」「Unitマップを作って」「どのファイルがどのUnitに属するか調べて」「依存関係を可視化して」など。
|
|
4
4
|
model: sonnet
|
|
5
5
|
review: opus
|
|
6
|
+
languages: [typescript]
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
# Codebase Mapper
|
|
@@ -3,6 +3,7 @@ name: doc-freshness-checker
|
|
|
3
3
|
description: 設計文書の鮮度チェック(L4バリデータ拡張)。`phasegate check-freshness` CLIを使い、設計文書の最終更新日が閾値を超えていないか、コード変更と設計文書の乖離がないかを検出する。使用タイミング: 「設計文書が古くなっていないか確認して」「ドキュメントの鮮度チェックを実行して」「L4 freshness チェック」「設計とコードの乖離を調べて」など。
|
|
4
4
|
model: sonnet
|
|
5
5
|
review: opus
|
|
6
|
+
languages: [typescript]
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
# Doc Freshness Checker
|