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.
Files changed (59) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +6 -1
  3. package/docs/guide/cli-reference.md +1 -1
  4. package/docs/guide/configuration.md +31 -1
  5. package/docs/guide/skills-overview.md +2 -0
  6. package/docs/principles/model-routing.md +1 -0
  7. package/docs/templates/personal/phasegate-local-config.json +4 -1
  8. package/docs/templates/project/phasegate.config.json +4 -1
  9. package/package.json +1 -1
  10. package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +54 -5
  11. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +5 -1
  12. package/scripts/harness/config-foundation/domain/harness-config.ts +9 -0
  13. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +2 -0
  14. package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +20 -5
  15. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +25 -0
  16. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +25 -0
  17. package/scripts/harness/installation/application/usecases/run-install.ts +16 -3
  18. package/scripts/harness/installation/application/usecases/run-reconcile.ts +48 -5
  19. package/scripts/harness/main.ts +31 -1
  20. package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +6 -1
  21. package/scripts/harness/setup/skill-deployer.ts +47 -0
  22. package/scripts/harness/skill-quality/domain/services/skill-structure-validator.ts +21 -0
  23. package/scripts/harness/skill-quality/domain/value-objects/skill-structure.ts +2 -0
  24. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +18 -5
  25. package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +16 -3
  26. package/scripts/harness/validator-system/domain/ports/validator-config-port.ts +2 -0
  27. package/scripts/harness/validator-system/domain/services/validator-language-capability-service.ts +64 -0
  28. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +7 -1
  29. package/skills/cascade-updater/SKILL.md +1 -0
  30. package/skills/codebase-mapper/SKILL.md +1 -0
  31. package/skills/codex-delegator/SKILL.md +1 -0
  32. package/skills/consistency-checker/SKILL.md +1 -0
  33. package/skills/doc-freshness-checker/SKILL.md +1 -0
  34. package/skills/domain-designer/SKILL.md +1 -0
  35. package/skills/engineering-perspective/SKILL.md +1 -0
  36. package/skills/environment-designer/SKILL.md +1 -0
  37. package/skills/implementation-planner/SKILL.md +1 -0
  38. package/skills/implementation-readiness-checker/SKILL.md +1 -0
  39. package/skills/it-test-designer/SKILL.md +1 -0
  40. package/skills/it-test-logic-designer/SKILL.md +1 -0
  41. package/skills/logical-designer/SKILL.md +1 -0
  42. package/skills/mock-designer/SKILL.md +1 -0
  43. package/skills/phasegate-config-doctor/SKILL.md +1 -0
  44. package/skills/phasegate-toolkit-guide/SKILL.md +1 -0
  45. package/skills/pointer-validator/SKILL.md +1 -0
  46. package/skills/product-architect/SKILL.md +1 -0
  47. package/skills/quick-implementor/SKILL.md +1 -0
  48. package/skills/scenario-test-designer/SKILL.md +1 -0
  49. package/skills/scenario-test-logic-designer/SKILL.md +1 -0
  50. package/skills/skill-creator/SKILL.md +1 -0
  51. package/skills/story-implementor/SKILL.md +1 -0
  52. package/skills/story-mapper/SKILL.md +1 -0
  53. package/skills/story-writer/SKILL.md +1 -0
  54. package/skills/test-coverage-checker/SKILL.md +1 -0
  55. package/skills/uiux-designer/SKILL.md +1 -0
  56. package/skills/unit-designer/SKILL.md +1 -0
  57. package/skills/unit-test-designer/SKILL.md +1 -0
  58. package/skills/unit-test-logic-designer/SKILL.md +1 -0
  59. 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(harnessRoot: string, targetRoot: string, skills: readonly string[]): Promise<void> {
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 changed = versionContent === null || !versionContent.includes(expectedVersion) || missingSkill || missingManifest;
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 changed = versionContent === null || !versionContent.includes(expectedVersion) || missingSkill || missingManifest;
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`,
@@ -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
- return this.maintainedLayers.includes(validatorId);
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 = definitions.filter((d) => d.validatorId.value !== 'L3-003');
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(definitions, [layerConfig]);
108
- const overrideMap = new Map<string, ValidationResult>(results.map((result) => [result.validatorId.value, result]));
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 results = this.executionService.execute(definitions, [effectiveLayerConfig]);
148
- const overrideMap = new Map<string, ValidationResult>(results.map((result) => [result.validatorId.value, result]));
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
  }
@@ -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: cascade-updater
3
3
  description: 下位フェーズの発見・変更を上位設計文書にフィードバックし影響範囲を整合的に更新
4
4
  model: sonnet
5
5
  review: opus
6
+ languages: [typescript]
6
7
  ---
7
8
 
8
9
  # Cascade Updater
@@ -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
@@ -6,6 +6,7 @@ description: |
6
6
  使用タイミング: 「codexに委任して」「codexで並列に作成」「codexに実装させて」「agentに依頼して」など、
7
7
  codex CLIへのタスク委任・並列実行が必要な場面。
8
8
  model: codex
9
+ languages: [typescript]
9
10
  ---
10
11
 
11
12
  # Codex Delegator
@@ -3,6 +3,7 @@ name: consistency-checker
3
3
  description: AIDLC設計文書群のレイヤー間整合性チェック・矛盾/漏れ検出
4
4
  model: sonnet
5
5
  review: opus
6
+ languages: [typescript]
6
7
  ---
7
8
 
8
9
  # Consistency Checker
@@ -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
@@ -2,6 +2,7 @@
2
2
  name: domain-designer
3
3
  description: DDD戦術パターンでドメインモデル(集約・エンティティ・VO・イベント)を設計(AIDLC Step 2.1)
4
4
  model: opus
5
+ languages: [typescript]
5
6
  ---
6
7
 
7
8
  # Domain Designer
@@ -9,6 +9,7 @@ description: |
9
9
  (4) TDD/リファクタリング/DDDのベストプラクティスを適用したい
10
10
  (5) チーム議論で共通認識を構築したい
11
11
  トリガー例: 「レビューして」「これどう思う?」「設計見て」「SOLID的にどう?」「リファクタ必要?」「ドメイン的に正しい?」
12
+ languages: [typescript]
12
13
  ---
13
14
 
14
15
  # Engineering Perspective