phasegate 0.157.0 → 0.159.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 (26) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/docs/ADR/ADR-004-scheduled-validators.md +1 -0
  4. package/docs/guide/cli-reference.md +1 -1
  5. package/docs/guide/configuration.md +1 -1
  6. package/docs/guide/layer-model.md +2 -1
  7. package/docs/guide/troubleshooting.md +1 -1
  8. package/package.json +1 -1
  9. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +2 -1
  10. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
  11. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
  12. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -1
  13. package/scripts/harness/harness-error/infrastructure/adapters/validator-registry-bridge-adapter.ts +2 -0
  14. package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +8 -0
  15. package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +7 -1
  16. package/scripts/harness/quick-mode/infrastructure/adapters/validator-system-validator-id-registry-adapter.ts +2 -2
  17. package/scripts/harness/skill-quality/infrastructure/adapters/validator-id-registry-bridge-adapter.ts +2 -1
  18. package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +21 -1
  19. package/scripts/harness/validator-system/composition-root.ts +6 -2
  20. package/scripts/harness/validator-system/domain/ports/skill-catalog-drift-port.ts +10 -0
  21. package/scripts/harness/validator-system/domain/services/l4/skill-catalog-drift-service.ts +108 -0
  22. package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +5 -2
  23. package/scripts/harness/validator-system/infrastructure/adapters/file-system-skill-catalog-drift-adapter.ts +126 -0
  24. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +2 -1
  25. package/skills/phasegate-config-doctor/SKILL.md +1 -1
  26. package/skills/phasegate-toolkit-guide/SKILL.md +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.159.0] - 2026-05-13
11
+
12
+ ### Added
13
+
14
+ - **WI-156 — documentation drift guardrail** — adds `L4-006 skill-catalog-drift` to compare the shipped `skills/*/SKILL.md` catalog with maintained skill-count declarations and skills overview category totals, plus release checklist guidance for remaining manual drift checks.
15
+
16
+ ## [0.158.0] - 2026-05-13
17
+
18
+ ### Changed
19
+
20
+ - **WI-180 — scoped-out doctor effective repair contract** — adds current-scope repair target and repair-mode applicability fields to doctor JSON, lists scoped-out check IDs in human output, and updates docs/skills so single-agent diagnostics are harder to misread.
21
+
10
22
  ## [0.157.0] - 2026-05-13
11
23
 
12
24
  ### Changed
package/README.md CHANGED
@@ -551,7 +551,7 @@ README keeps only the entry points most users need. The full public/compatibilit
551
551
 
552
552
  <!-- @work-item-id WI-151 -->
553
553
 
554
- Standard projects keep `layers.L4.enabled: false`; `validate --layer all` and `phasegate:ci-check` report disabled L4 validators as skipped. Explicit `validate --layer L4` runs the scheduled validators on demand. Warning-only L4 drift/consistency/dead-code findings remain advisory unless `validate.failOnWarning: true`, the `strict` preset, or `--fail-on-warning` is used. Use that strict mode only after the project has real drift keys, consistency targets, pointer/freshness ownership, and semantic drift coverage.
554
+ Standard projects keep `layers.L4.enabled: false`; `validate --layer all` and `phasegate:ci-check` report disabled L4 validators as skipped. Explicit `validate --layer L4` runs the scheduled validators on demand. Warning-only L4 drift/consistency/dead-code/skill-catalog findings remain advisory unless `validate.failOnWarning: true`, the `strict` preset, or `--fail-on-warning` is used. Use that strict mode only after the project has real drift keys, consistency targets, pointer/freshness ownership, semantic drift coverage, and maintained skill-count declarations.
555
555
 
556
556
  See the [CLI Reference](docs/guide/cli-reference.md) for the complete catalog and the [Japanese README](README.ja.md) for Japanese-language onboarding.
557
557
 
@@ -19,6 +19,7 @@ L4(Scheduled)で以下のバリデータを週次または明示実行で扱
19
19
  | dead-code | L4-003 | 未使用エクスポート、到達不能コード |
20
20
  | doc-freshness | L4-004 | 設計文書の freshness threshold 超過 |
21
21
  | pointer-validation | L4-005 | 設計文書 pointer の owner/type/source/severity/nextAction 付き検証 |
22
+ | skill-catalog-drift | L4-006 | `skills/*/SKILL.md` 実数と maintained docs の skill count 宣言差分検出 | <!-- @work-item-id WI-156 -->
22
23
 
23
24
  ### 技術的実装
24
25
 
@@ -40,7 +40,7 @@ Command names in this document are split into three surfaces:
40
40
 
41
41
  <!-- @work-item-id WI-158 -->
42
42
 
43
- Setup lifecycle commands support JSON for automation where shown by help: `install --json`, `reconcile --json`, `uninstall --json`, and `doctor --json`. `doctor --agent claude --json` and `doctor --agent codex --json` include `scope` and `scopedOutFindings` so agents can distinguish selected-agent readiness from full-install diagnostics. Scoped-out findings suppress immediate repair guidance with `repairHint: null`, `suggestedSkill: null`, and `repairHintApplicability: "only-if-agent-selected"`. `doctor --report-out <path>` persists the doctor JSON payload to that exact path. Relative paths are resolved from the project root; absolute paths are used as-is. <!-- @work-item-id WI-178, WI-179 -->
43
+ Setup lifecycle commands support JSON for automation where shown by help: `install --json`, `reconcile --json`, `uninstall --json`, and `doctor --json`. `doctor --agent claude --json` and `doctor --agent codex --json` include `scope` and `scopedOutFindings` so agents can distinguish selected-agent readiness from full-install diagnostics. Scoped-out findings suppress immediate repair guidance with `repairHint: null`, `suggestedSkill: null`, `currentScopeRepairTarget: false`, `repairHintApplicability: "only-if-agent-selected"`, and `repairModeApplicability: "only-if-agent-selected"`; applicable `findings[]` use `currentScopeRepairTarget: true` with applicable repair fields. `doctor --report-out <path>` persists the doctor JSON payload to that exact path. Relative paths are resolved from the project root; absolute paths are used as-is. <!-- @work-item-id WI-178, WI-179, WI-180 -->
44
44
 
45
45
  This is separate from `reporting.outputDir`. The configured report directory is used by phase-dependency / phase-gate reporting, while regression-suite result files are fixed under `reports/regression/` and status/drift JSON is emitted to stdout.
46
46
 
@@ -458,7 +458,7 @@ Controls how warning-severity validator failures are aggregated into the overall
458
458
 
459
459
  | Sub-field | Type | Default by preset | Description |
460
460
  |------------------|-----------|------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
461
- | `failOnWarning` | `boolean` | `false` (`minimal` / `standard`), `true` (`strict`) | When `true`, warning-only validator fails (for example L4-001 drift, L4-002 consistency, L4-003 dead-code, L4-004 doc-freshness, or L4-005 pointer-validation) count as overall FAIL / exit 1. When `false`, they count as PASS / exit 0. |
461
+ | `failOnWarning` | `boolean` | `false` (`minimal` / `standard`), `true` (`strict`) | When `true`, warning-only validator fails (for example L4-001 drift, L4-002 consistency, L4-003 dead-code, L4-004 doc-freshness, L4-005 pointer-validation, or L4-006 skill-catalog-drift) count as overall FAIL / exit 1. When `false`, they count as PASS / exit 0. |
462
462
 
463
463
  CLI override: `--fail-on-warning` / `--no-fail-on-warning` (CLI > config). Both unspecified → config value used.
464
464
 
@@ -167,6 +167,7 @@ L4 validators are designed to run on a weekly schedule and detect slow-moving dr
167
167
  | **dead-code** | L4-003 | Detects unused exports and unreachable code that should be removed. |
168
168
  | **doc-freshness** | L4-004 | Checks design document freshness against the configured threshold. Also available through the `p2:check-freshness` compatibility command. |
169
169
  | **pointer-validation** | L4-005 | Resolves and validates design document pointers. Also available through the `p2:validate-pointers` compatibility command. |
170
+ | **skill-catalog-drift** | L4-006 | Compares `skills/*/SKILL.md` count with maintained public/operator skill-count declarations and `skills-overview` category totals. |
170
171
 
171
172
  <!-- @work-item-id WI-116 -->
172
173
  `doc-freshness` and `pointer-validation` are registered as L4-004/L4-005. The standalone `p2:check-freshness` and `p2:validate-pointers` commands remain as compatibility entry points; canonical L4 execution is `validate --layer L4`. WI-033 remains closed as validator registration work; WI-128 tracks scheduling/default/policy rollout.
@@ -190,7 +191,7 @@ The canonical validator catalog is:
190
191
  |---|---|
191
192
  | L2 | `L2-001`, `L2-002`, `L2-003`, `L2-013`, `L2-014`, `L2-015` |
192
193
  | L3 | `L3-001`, `L3-002`, `L3-003`, `L3-004` |
193
- | L4 | `L4-001`, `L4-002`, `L4-003`, `L4-004`, `L4-005` |
194
+ | L4 | `L4-001`, `L4-002`, `L4-003`, `L4-004`, `L4-005`, `L4-006` |
194
195
 
195
196
  `validate --layer L2` runs all enabled L2 validators. `validate --layer L4` is an explicit operator request and runs L4 even when standard preset config leaves `layers.L4.enabled: false`. `validate --layer all`, `phasegate:ci-check`, and aggregate CI-style commands preserve disabled L4 entries as skipped results; skipped results do not fail the process and are not promoted by `--fail-on-warning`.
196
197
 
@@ -63,7 +63,7 @@ The planner output is intentionally agent-readable: detected state, questions, p
63
63
 
64
64
  `doctor` green means the local managed setup is consistent for the inspected targets. It does not prove user-level Codex feature flags, hosted GitHub Actions execution, npm registry state, or team policy acceptance.
65
65
 
66
- By default, `phasegate doctor` inspects the full Claude + Codex + shared setup. For a deliberate single-agent setup, use `phasegate doctor --agent claude --json` or `phasegate doctor --agent codex --json`. Single-agent reports keep shared targets applicable and place the unselected agent's findings under `scopedOutFindings` with `applicability: "not-applicable"`; those findings are explanatory, not repair targets for the selected agent. Their `repairHint` and `suggestedSkill` fields are intentionally `null`; `repairHintApplicability: "only-if-agent-selected"` means the repair guidance matters only if the user chooses that agent. <!-- @work-item-id WI-178, WI-179 -->
66
+ By default, `phasegate doctor` inspects the full Claude + Codex + shared setup. For a deliberate single-agent setup, use `phasegate doctor --agent claude --json` or `phasegate doctor --agent codex --json`. Single-agent reports keep shared targets applicable and place the unselected agent's findings under `scopedOutFindings` with `applicability: "not-applicable"`; those findings are explanatory, not repair targets for the selected agent. Their `repairHint` and `suggestedSkill` fields are intentionally `null`; `currentScopeRepairTarget: false`, `repairHintApplicability: "only-if-agent-selected"`, and `repairModeApplicability: "only-if-agent-selected"` mean the original repair fields only become actionable if the user chooses that agent. Applicable `findings[]` use `currentScopeRepairTarget: true` and applicable repair fields. <!-- @work-item-id WI-178, WI-179, WI-180 -->
67
67
 
68
68
  Check `plan.completeness` in `setup:agent --json`:
69
69
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.157.0",
3
+ "version": "0.159.0",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "MIT",
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @layer application
3
3
  * @unit config-foundation
4
- * @work-item-id WI-133
4
+ * @work-item-id WI-133 / WI-156
5
5
  */
6
6
  import type { HarnessConfigV2 } from '../../domain/harness-config.js';
7
7
 
@@ -25,6 +25,7 @@ export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefi
25
25
  'doc-freshness-checker': 'L4-004',
26
26
  'pointer-validation': 'L4-005',
27
27
  'pointer-validator': 'L4-005',
28
+ 'skill-catalog-drift': 'L4-006',
28
29
  }, /^L4-\d{3}$/);
29
30
 
30
31
  return {
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "L4": {
17
17
  "enabled": false,
18
- "validators": ["drift-detector", "doc-freshness-checker", "pointer-validator"],
18
+ "validators": ["drift-detector", "doc-freshness-checker", "pointer-validator", "skill-catalog-drift"],
19
19
  "schedule": "0 0 * * *"
20
20
  }
21
21
  },
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "L4": {
17
17
  "enabled": false,
18
- "validators": ["drift-detector", "doc-freshness-checker", "pointer-validator"],
18
+ "validators": ["drift-detector", "doc-freshness-checker", "pointer-validator", "skill-catalog-drift"],
19
19
  "schedule": "0 0 * * *"
20
20
  }
21
21
  },
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "L4": {
17
17
  "enabled": true,
18
- "validators": ["drift-detector", "consistency-checker", "dead-code-detector", "doc-freshness-checker", "pointer-validator"],
18
+ "validators": ["drift-detector", "consistency-checker", "dead-code-detector", "doc-freshness-checker", "pointer-validator", "skill-catalog-drift"],
19
19
  "schedule": "0 1 * * *"
20
20
  }
21
21
  },
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer infrastructure
3
3
  * @unit harness-error
4
+ * @work-item-id WI-156
4
5
  */
5
6
  export interface ValidatorIssueSnapshot {
6
7
  readonly code: string;
@@ -66,6 +67,7 @@ const DEFAULT_VALIDATOR_IDS = [
66
67
  'dead-code',
67
68
  'doc-freshness',
68
69
  'pointer-validation',
70
+ 'skill-catalog-drift',
69
71
  ] as const;
70
72
 
71
73
  export const DEFAULT_VALIDATOR_ENTRYPOINTS = new Map<
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer infrastructure
3
3
  * @unit harness-error
4
+ * @work-item-id WI-156
4
5
  */
5
6
  import { AdrRef } from '../../domain/value-objects/adr-ref.js';
6
7
  import { ErrorCode } from '../../domain/value-objects/error-code.js';
@@ -64,4 +65,11 @@ export const L4_ERROR_DEFINITIONS = Object.freeze([
64
65
  ownerValidatorId: 'pointer-validation',
65
66
  defaultFixExample: 'const actual = "fix unresolved document pointer";',
66
67
  }),
68
+ createDefinition({
69
+ code: 'L4-006',
70
+ title: 'スキルカタログ数とドキュメント宣言が一致しない',
71
+ category: 'consistency',
72
+ ownerValidatorId: 'skill-catalog-drift',
73
+ defaultFixExample: 'const actual = "update skill count declarations with the skill catalog";',
74
+ }),
67
75
  ]);
@@ -3,6 +3,7 @@
3
3
  // @work-item-id WI-145
4
4
  // @work-item-id WI-178
5
5
  // @work-item-id WI-179
6
+ // @work-item-id WI-180
6
7
 
7
8
  import type { DoctorAgentScope, ScopedOutDiagnosticFinding } from "../../application/usecases/run-doctor-diagnostics.js";
8
9
  import type { DiagnosticReport } from "../../domain/diagnostic-report.js";
@@ -31,7 +32,9 @@ export class DiagnosticReportFormatter {
31
32
  findings: input.report.findings.map((finding) => ({
32
33
  ...finding.toJSON(),
33
34
  applicability: "applicable",
35
+ currentScopeRepairTarget: true,
34
36
  repairHintApplicability: "applicable",
37
+ repairModeApplicability: "applicable",
35
38
  })),
36
39
  scopedOutFindings: input.scopedOutFindings.map(({ finding, scopeReason }) => {
37
40
  const json = finding.toJSON();
@@ -40,7 +43,9 @@ export class DiagnosticReportFormatter {
40
43
  repairHint: null,
41
44
  suggestedSkill: null,
42
45
  applicability: "not-applicable",
46
+ currentScopeRepairTarget: false,
43
47
  repairHintApplicability: "only-if-agent-selected",
48
+ repairModeApplicability: "only-if-agent-selected",
44
49
  scopeReason,
45
50
  };
46
51
  }),
@@ -73,7 +78,8 @@ export class DiagnosticReportFormatter {
73
78
  const warnCount = input.report.findings.filter((finding) => finding.severity === "warn").length;
74
79
  lines.push(`Status: ${input.report.overallStatus.toUpperCase()} (${input.report.findings.length} findings: ${redCount} red, ${warnCount} warn)`);
75
80
  if (input.scopedOutFindings.length > 0) {
76
- lines.push(`Scoped out: ${input.scopedOutFindings.length} informational findings not applicable to --agent ${input.agent}; not repair targets for this scope.`);
81
+ const checkIds = input.scopedOutFindings.map(({ finding }) => finding.checkId).join(", ");
82
+ lines.push(`Scoped out: ${input.scopedOutFindings.length} informational findings not applicable to --agent ${input.agent}; not repair targets for this scope: ${checkIds}.`);
77
83
  }
78
84
  lines.push(`Exit: ${input.exitCode}`);
79
85
  return lines.join("\n");
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @layer infrastructure
3
3
  * @unit quick-mode
4
- * @work-item-id WI-140
4
+ * @work-item-id WI-140 / WI-156
5
5
  *
6
6
  * integration_contract.md §9 の確定ID一覧を静的定義で保持する ValidatorIdRegistry Adapter
7
7
  */
@@ -10,7 +10,7 @@ const STATIC_VALIDATOR_IDS: readonly string[] = Object.freeze([
10
10
  'L1-001', 'L1-002', 'L1-003', 'L1-004', 'L1-005', 'L1-006', 'L1-007', 'L1-008',
11
11
  'L2-001', 'L2-002', 'L2-003', 'L2-013', 'L2-014', 'L2-015',
12
12
  'L3-001', 'L3-002', 'L3-003', 'L3-004',
13
- 'L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005',
13
+ 'L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005', 'L4-006',
14
14
  ]);
15
15
 
16
16
  export class ValidatorSystemValidatorIdRegistryAdapter {
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer infrastructure
3
3
  * @unit skill-quality
4
+ * @work-item-id WI-156
4
5
  */
5
6
  import type { ValidatorIdRegistryPort } from '../../domain/ports/validator-id-registry-port.js';
6
7
 
@@ -8,7 +9,7 @@ const FALLBACK_VALIDATOR_IDS = [
8
9
  'L1-001', 'L1-002', 'L1-003', 'L1-004', 'L1-005', 'L1-006', 'L1-007', 'L1-008',
9
10
  'L2-001', 'L2-002', 'L2-003',
10
11
  'L3-001', 'L3-002', 'L3-003', 'L3-004',
11
- 'L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005',
12
+ 'L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005', 'L4-006',
12
13
  ];
13
14
 
14
15
  export class ValidatorIdRegistryBridgeAdapter implements ValidatorIdRegistryPort {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @layer application
3
3
  * @unit validator-system
4
- * @work-item-id WI-107
4
+ * @work-item-id WI-107 / WI-156
5
5
  *
6
6
  * RunL4ValidatorsUseCase — H08-03: L4バリデータ実行
7
7
  */
@@ -18,6 +18,8 @@ import type { DriftDetectionService } from '../../domain/services/l4/drift-detec
18
18
  import type { ConsistencyCheckService } from '../../domain/services/l4/consistency-check-service.js';
19
19
  import type { DeadCodeDetectionService } from '../../domain/services/l4/dead-code-detection-service.js';
20
20
  import type { ArchitectureSemanticAnalysisService } from '../../domain/services/l4/architecture-semantic-analysis-service.js';
21
+ import { SkillCatalogDriftService } from '../../domain/services/l4/skill-catalog-drift-service.js';
22
+ import type { SkillCatalogDriftPort } from '../../domain/ports/skill-catalog-drift-port.js';
21
23
 
22
24
  interface ScheduledHarnessErrorContract {
23
25
  readonly severity: string;
@@ -67,6 +69,7 @@ export interface RunL4ValidatorsUseCaseDeps {
67
69
  consistencyCheckService?: ConsistencyCheckService;
68
70
  deadCodeDetectionService?: DeadCodeDetectionService;
69
71
  architectureSemanticAnalysisService?: ArchitectureSemanticAnalysisService;
72
+ skillCatalogDriftPort?: SkillCatalogDriftPort;
70
73
  checkDocFreshnessUseCase?: CheckDocFreshnessUseCasePort;
71
74
  validateDocPointersUseCase?: ValidateDocPointersUseCasePort;
72
75
  }
@@ -80,6 +83,8 @@ export class RunL4ValidatorsUseCase {
80
83
  private readonly consistencyCheckService?: ConsistencyCheckService;
81
84
  private readonly deadCodeDetectionService?: DeadCodeDetectionService;
82
85
  private readonly architectureSemanticAnalysisService?: ArchitectureSemanticAnalysisService;
86
+ private readonly skillCatalogDriftPort?: SkillCatalogDriftPort;
87
+ private readonly skillCatalogDriftService = new SkillCatalogDriftService();
83
88
  private readonly checkDocFreshnessUseCase?: CheckDocFreshnessUseCasePort;
84
89
  private readonly validateDocPointersUseCase?: ValidateDocPointersUseCasePort;
85
90
 
@@ -92,6 +97,7 @@ export class RunL4ValidatorsUseCase {
92
97
  this.consistencyCheckService = deps.consistencyCheckService;
93
98
  this.deadCodeDetectionService = deps.deadCodeDetectionService;
94
99
  this.architectureSemanticAnalysisService = deps.architectureSemanticAnalysisService;
100
+ this.skillCatalogDriftPort = deps.skillCatalogDriftPort;
95
101
  this.checkDocFreshnessUseCase = deps.checkDocFreshnessUseCase;
96
102
  this.validateDocPointersUseCase = deps.validateDocPointersUseCase;
97
103
  }
@@ -206,6 +212,20 @@ export class RunL4ValidatorsUseCase {
206
212
  }
207
213
  }
208
214
 
215
+ if (this.skillCatalogDriftPort) {
216
+ const l4006Result = overrideMap.get('L4-006');
217
+ if (l4006Result && !l4006Result.skipped) {
218
+ const snapshot = await this.skillCatalogDriftPort.collect();
219
+ const report = this.skillCatalogDriftService.check(snapshot);
220
+ overrideMap.set(
221
+ 'L4-006',
222
+ report.hasFindings()
223
+ ? ValidationResult.fail(ValidatorId.create('L4-006'), report.toHarnessErrors(), 0)
224
+ : ValidationResult.pass(ValidatorId.create('L4-006'), 0),
225
+ );
226
+ }
227
+ }
228
+
209
229
  const finalResults = definitions.map(
210
230
  (definition) => overrideMap.get(definition.validatorId.value) ?? ValidationResult.skip(definition.validatorId),
211
231
  );
@@ -3,7 +3,7 @@
3
3
  * @unit validator-system
4
4
  *
5
5
  * DI 組み立て — validator-system の全依存関係を構築する
6
- * @work-item-id WI-110 / WI-111 / WI-132 / WI-133 / WI-136 / WI-137 / WI-138
6
+ * @work-item-id WI-110 / WI-111 / WI-132 / WI-133 / WI-136 / WI-137 / WI-138 / WI-156
7
7
  */
8
8
  import { ValidatorId } from './domain/value-objects/validator-id.js';
9
9
  import { ValidatorDefinition } from './domain/value-objects/validator-definition.js';
@@ -36,6 +36,7 @@ import { BiomeAstSourceCodeAnalyzerAdapter } from './infrastructure/adapters/bio
36
36
  import { AdrFoundationReferenceAdapter } from './infrastructure/adapters/adr-foundation-reference-adapter.js';
37
37
  import { ImportGraphSourceAnalysisAdapter } from './infrastructure/adapters/import-graph-source-analysis-adapter.js';
38
38
  import { FileSystemArchitectureSemanticSourceAdapter } from './infrastructure/adapters/file-system-architecture-semantic-source-adapter.js';
39
+ import { FileSystemSkillCatalogDriftAdapter } from './infrastructure/adapters/file-system-skill-catalog-drift-adapter.js';
39
40
  import { DriftDetectionService } from './domain/services/l4/drift-detection-service.js';
40
41
  import { ConsistencyCheckService } from './domain/services/l4/consistency-check-service.js';
41
42
  import { DeadCodeDetectionService } from './domain/services/l4/dead-code-detection-service.js';
@@ -52,7 +53,7 @@ const DEFAULT_CONFIG = {
52
53
  layers: {
53
54
  L2: { enabled: true, validators: ['L2-001', 'L2-002', 'L2-003', 'L2-013', 'L2-014', 'L2-015'] },
54
55
  L3: { enabled: true, validators: ['L3-001', 'L3-002', 'L3-003', 'L3-004'], coverageThreshold: 90, bundleSizeLimit: 512000 },
55
- L4: { enabled: true, validators: ['L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005'] },
56
+ L4: { enabled: true, validators: ['L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005', 'L4-006'] },
56
57
  },
57
58
  validate: { failOnWarning: false },
58
59
  architecture: {
@@ -108,6 +109,7 @@ export function buildDefaultRegistry(): ValidatorRegistry {
108
109
  createDef('L4-003', 'L4', 'strictOnly'),
109
110
  createDef('L4-004', 'L4', 'always'),
110
111
  createDef('L4-005', 'L4', 'always'),
112
+ createDef('L4-006', 'L4', 'always', 'SkillCatalogDriftPort'),
111
113
  ];
112
114
 
113
115
  return new ValidatorRegistry(definitions);
@@ -188,6 +190,7 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
188
190
  const adrReferencePort = new AdrFoundationReferenceAdapter();
189
191
  const sourceAnalysisPort = new ImportGraphSourceAnalysisAdapter();
190
192
  const architectureSemanticSourcePort = new FileSystemArchitectureSemanticSourceAdapter();
193
+ const skillCatalogDriftPort = new FileSystemSkillCatalogDriftAdapter(cwd);
191
194
 
192
195
  const driftDetectionService = new DriftDetectionService({
193
196
  designDocumentPort: markdownDesignDocumentPort,
@@ -215,6 +218,7 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
215
218
  consistencyCheckService,
216
219
  deadCodeDetectionService,
217
220
  architectureSemanticAnalysisService,
221
+ skillCatalogDriftPort,
218
222
  checkDocFreshnessUseCase: phase2Extensions.checkDocFreshnessUseCase,
219
223
  validateDocPointersUseCase: phase2Extensions.validateDocPointersUseCase,
220
224
  });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @layer domain
3
+ * @unit validator-system
4
+ * @work-item-id WI-156
5
+ */
6
+ import type { SkillCatalogSnapshot } from '../services/l4/skill-catalog-drift-service.js';
7
+
8
+ export interface SkillCatalogDriftPort {
9
+ collect(): Promise<SkillCatalogSnapshot>;
10
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @layer domain
3
+ * @unit validator-system
4
+ * @work-item-id WI-156
5
+ */
6
+
7
+ export interface SkillCountDeclaration {
8
+ readonly sourcePath: string;
9
+ readonly declaredCount: number;
10
+ readonly line: number;
11
+ }
12
+
13
+ export interface SkillCategoryDeclaration {
14
+ readonly sourcePath: string;
15
+ readonly categoryName: string;
16
+ readonly declaredCount: number;
17
+ readonly line: number;
18
+ }
19
+
20
+ export interface SkillCatalogSnapshot {
21
+ readonly actualSkillNames: readonly string[];
22
+ readonly countDeclarations: readonly SkillCountDeclaration[];
23
+ readonly categoryDeclarations: readonly SkillCategoryDeclaration[];
24
+ }
25
+
26
+ export interface SkillCatalogDriftFinding {
27
+ readonly kind: 'skill-count-mismatch' | 'skill-category-total-mismatch';
28
+ readonly sourcePath: string;
29
+ readonly message: string;
30
+ readonly suggestion: string;
31
+ readonly expectedCount: number;
32
+ readonly actualCount: number;
33
+ readonly line?: number;
34
+ }
35
+
36
+ export class SkillCatalogDriftReport {
37
+ readonly findings: readonly SkillCatalogDriftFinding[];
38
+
39
+ constructor(findings: readonly SkillCatalogDriftFinding[]) {
40
+ this.findings = Object.freeze([...findings]);
41
+ Object.freeze(this);
42
+ }
43
+
44
+ hasFindings(): boolean {
45
+ return this.findings.length > 0;
46
+ }
47
+
48
+ toHarnessErrors(): readonly {
49
+ readonly code: { readonly value: string; toString(): string };
50
+ readonly severity: { readonly value: string; toString(): string };
51
+ readonly message: string;
52
+ readonly suggestion: string;
53
+ readonly kind: string;
54
+ readonly sourcePath: string;
55
+ readonly expectedCount: number;
56
+ readonly actualCount: number;
57
+ readonly line?: number;
58
+ }[] {
59
+ return this.findings.map((finding) => ({
60
+ code: { value: 'L4-006', toString: () => 'L4-006' },
61
+ severity: { value: 'warning', toString: () => 'warning' },
62
+ message: finding.message,
63
+ suggestion: finding.suggestion,
64
+ kind: finding.kind,
65
+ sourcePath: finding.sourcePath,
66
+ expectedCount: finding.expectedCount,
67
+ actualCount: finding.actualCount,
68
+ line: finding.line,
69
+ }));
70
+ }
71
+ }
72
+
73
+ export class SkillCatalogDriftService {
74
+ check(snapshot: SkillCatalogSnapshot): SkillCatalogDriftReport {
75
+ const actualCount = snapshot.actualSkillNames.length;
76
+ const findings: SkillCatalogDriftFinding[] = [];
77
+
78
+ for (const declaration of snapshot.countDeclarations) {
79
+ if (declaration.declaredCount !== actualCount) {
80
+ findings.push({
81
+ kind: 'skill-count-mismatch',
82
+ sourcePath: declaration.sourcePath,
83
+ line: declaration.line,
84
+ expectedCount: actualCount,
85
+ actualCount: declaration.declaredCount,
86
+ message: `${declaration.sourcePath}:${declaration.line} declares ${declaration.declaredCount} skills, but skills/*/SKILL.md contains ${actualCount}.`,
87
+ suggestion: 'Update the documented skill count or add/remove the corresponding skill directory in the same release change.',
88
+ });
89
+ }
90
+ }
91
+
92
+ if (snapshot.categoryDeclarations.length > 0) {
93
+ const categoryTotal = snapshot.categoryDeclarations.reduce((sum, declaration) => sum + declaration.declaredCount, 0);
94
+ if (categoryTotal !== actualCount) {
95
+ findings.push({
96
+ kind: 'skill-category-total-mismatch',
97
+ sourcePath: snapshot.categoryDeclarations[0]?.sourcePath ?? 'docs/guide/skills-overview.md',
98
+ expectedCount: actualCount,
99
+ actualCount: categoryTotal,
100
+ message: `Skill category headings sum to ${categoryTotal} skills, but skills/*/SKILL.md contains ${actualCount}.`,
101
+ suggestion: 'Update docs/guide/skills-overview.md category heading counts so their sum matches the actual skill catalog.',
102
+ });
103
+ }
104
+ }
105
+
106
+ return new SkillCatalogDriftReport(findings);
107
+ }
108
+ }
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit validator-system
4
- * @work-item-id WI-116
4
+ * @work-item-id WI-116 / WI-156
5
5
  *
6
6
  * ValidatorId 値オブジェクト
7
- * L1-001〜L4-005 のバリデータを識別する不変値オブジェクト
7
+ * L1-001〜L4-006 のバリデータを識別する不変値オブジェクト
8
8
  * Wave 2A で L1-017, L1-018, L2-013 を追加
9
9
  * WI-140 で L2-014 を追加
10
10
  * WI-132/WI-133/WI-136/WI-137/WI-138 で L2-015 を追加
11
+ * WI-156 で L4-006 を追加
11
12
  */
12
13
 
13
14
  export class InvalidValidatorIdError extends Error {
@@ -40,6 +41,7 @@ const VALIDATOR_NAME_MAP: Record<string, string> = {
40
41
  'L4-003': 'dead-code',
41
42
  'L4-004': 'doc-freshness',
42
43
  'L4-005': 'pointer-validation',
44
+ 'L4-006': 'skill-catalog-drift',
43
45
  };
44
46
 
45
47
  /** バリデータ名 -> バリデータID の逆引きマップ */
@@ -50,6 +52,7 @@ const NAME_TO_ID_MAP: Record<string, string> = {
50
52
  'dead-code-detector': 'L4-003',
51
53
  'doc-freshness-checker': 'L4-004',
52
54
  'pointer-validator': 'L4-005',
55
+ 'skill-catalog-drift': 'L4-006',
53
56
  };
54
57
 
55
58
  /** 有効なValidatorID集合 */
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @layer infrastructure
3
+ * @unit validator-system
4
+ * @work-item-id WI-156
5
+ */
6
+ import { readdir, readFile } from 'node:fs/promises';
7
+ import { join } from 'node:path';
8
+ import type {
9
+ SkillCatalogSnapshot,
10
+ SkillCategoryDeclaration,
11
+ SkillCountDeclaration,
12
+ } from '../../domain/services/l4/skill-catalog-drift-service.js';
13
+ import type { SkillCatalogDriftPort } from '../../domain/ports/skill-catalog-drift-port.js';
14
+
15
+ const MAINTAINED_COUNT_DOCS = [
16
+ 'skills/README.md',
17
+ 'README.md',
18
+ 'DEVELOPMENT.md',
19
+ 'docs/guide/installation.md',
20
+ 'docs/guide/quick-vs-full-mode.md',
21
+ 'docs/guide/skills-overview.md',
22
+ ] as const;
23
+
24
+ const TOTAL_COUNT_PATTERNS = [
25
+ /\b(?:provides|contains|deploys)\s+(\d+)\s+skills\b/gi,
26
+ /\bfull catalogue of\s+(\d+)\s+skills\b/gi,
27
+ /--\s*(\d+)\s+skills\b/gi,
28
+ /#\s*(\d+)\s+skills\b/gi,
29
+ /配布対象は\s+(\d+)\s+skills\s+です/g,
30
+ ] as const;
31
+
32
+ const CATEGORY_PATTERN = /^###\s+(.+?)\s+\((\d+)\s+skills\)\s*$/gm;
33
+
34
+ export class FileSystemSkillCatalogDriftAdapter implements SkillCatalogDriftPort {
35
+ constructor(private readonly cwd: string = process.cwd()) {}
36
+
37
+ async collect(): Promise<SkillCatalogSnapshot> {
38
+ const actualSkillNames = await this.readActualSkillNames();
39
+ const countDeclarations: SkillCountDeclaration[] = [];
40
+ let categoryDeclarations: SkillCategoryDeclaration[] = [];
41
+
42
+ for (const relativePath of MAINTAINED_COUNT_DOCS) {
43
+ const text = await this.readOptional(relativePath);
44
+ if (text === null) continue;
45
+ countDeclarations.push(...this.extractCountDeclarations(relativePath, text));
46
+ if (relativePath === 'docs/guide/skills-overview.md') {
47
+ categoryDeclarations = this.extractCategoryDeclarations(relativePath, text);
48
+ }
49
+ }
50
+
51
+ return {
52
+ actualSkillNames,
53
+ countDeclarations,
54
+ categoryDeclarations,
55
+ };
56
+ }
57
+
58
+ private async readActualSkillNames(): Promise<readonly string[]> {
59
+ const skillsRoot = join(this.cwd, 'skills');
60
+ let entries;
61
+ try {
62
+ entries = await readdir(skillsRoot, { withFileTypes: true });
63
+ } catch {
64
+ return Object.freeze([]);
65
+ }
66
+ const skillNames: string[] = [];
67
+ for (const entry of entries) {
68
+ if (!entry.isDirectory()) continue;
69
+ const skillPath = `skills/${entry.name}/SKILL.md`;
70
+ const text = await this.readOptional(skillPath);
71
+ if (text !== null) skillNames.push(entry.name);
72
+ }
73
+ return Object.freeze(skillNames.sort((a, b) => a.localeCompare(b)));
74
+ }
75
+
76
+ private async readOptional(relativePath: string): Promise<string | null> {
77
+ try {
78
+ return await readFile(join(this.cwd, relativePath), 'utf-8');
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ private extractCountDeclarations(sourcePath: string, text: string): SkillCountDeclaration[] {
85
+ const declarations: SkillCountDeclaration[] = [];
86
+ for (const pattern of TOTAL_COUNT_PATTERNS) {
87
+ for (const match of text.matchAll(pattern)) {
88
+ const rawCount = match[1];
89
+ const index = match.index;
90
+ if (!rawCount || index === undefined) continue;
91
+ if (this.lineTextAt(text, index).trimStart().startsWith('###')) continue;
92
+ declarations.push({
93
+ sourcePath,
94
+ declaredCount: Number.parseInt(rawCount, 10),
95
+ line: this.lineOf(text, index),
96
+ });
97
+ }
98
+ }
99
+ return declarations;
100
+ }
101
+
102
+ private extractCategoryDeclarations(sourcePath: string, text: string): SkillCategoryDeclaration[] {
103
+ return [...text.matchAll(CATEGORY_PATTERN)].flatMap((match) => {
104
+ const categoryName = match[1];
105
+ const rawCount = match[2];
106
+ const index = match.index;
107
+ if (!categoryName || !rawCount || index === undefined) return [];
108
+ return [{
109
+ sourcePath,
110
+ categoryName,
111
+ declaredCount: Number.parseInt(rawCount, 10),
112
+ line: this.lineOf(text, index),
113
+ }];
114
+ });
115
+ }
116
+
117
+ private lineOf(text: string, index: number): number {
118
+ return text.slice(0, index).split('\n').length;
119
+ }
120
+
121
+ private lineTextAt(text: string, index: number): string {
122
+ const lineStart = text.lastIndexOf('\n', index - 1) + 1;
123
+ const lineEnd = text.indexOf('\n', index);
124
+ return text.slice(lineStart, lineEnd === -1 ? text.length : lineEnd);
125
+ }
126
+ }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer infrastructure
3
3
  * @unit validator-system
4
+ * @work-item-id WI-156
4
5
  *
5
6
  * HarnessConfigValidatorConfigAdapter — ValidatorConfigPort実装
6
7
  * HarnessConfigV2からLayerConfig VOを構築する
@@ -40,7 +41,7 @@ export class HarnessConfigValidatorConfigAdapter implements ValidatorConfigPort
40
41
  const defaultValidators: Record<string, string[]> = {
41
42
  L2: ['L2-001', 'L2-002', 'L2-003', 'L2-013', 'L2-014', 'L2-015'],
42
43
  L3: ['L3-001', 'L3-002', 'L3-003', 'L3-004'],
43
- L4: ['L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005'],
44
+ L4: ['L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005', 'L4-006'],
44
45
  };
45
46
 
46
47
  const thresholds: Record<string, number> = {};
@@ -127,7 +127,7 @@ product-architect で Unit を作り、いくつかの logical_design を書い
127
127
  #### 観点 9: setup lifecycle と doctor finding
128
128
 
129
129
  - `phasegate doctor --json` の finding に `repairMode: "ai-assisted"` と `suggestedSkill.skillName = "phasegate-config-doctor"` がある → 本 skill が merge 方針、保持する user content、実行すべき `install --apply` / `--force` / `reconcile --apply` を提案する
130
- - Claude-only / Codex-only 導入後は `phasegate doctor --agent claude --json` または `phasegate doctor --agent codex --json` を使って selected agent の readiness を読む。`scopedOutFindings` は未選択 agent の `not-applicable` 情報なので、ユーザーがその agent を導入したいと言っていない限り repair 提案にしない。`repairHint: null` / `suggestedSkill: null` は意図的な抑制で、`repairHintApplicability: "only-if-agent-selected"` はその agent を選んだ場合だけ修復 guidance が意味を持つという印である。<!-- @work-item-id WI-178, WI-179 -->
130
+ - Claude-only / Codex-only 導入後は `phasegate doctor --agent claude --json` または `phasegate doctor --agent codex --json` を使って selected agent の readiness を読む。`scopedOutFindings` は未選択 agent の `not-applicable` 情報なので、ユーザーがその agent を導入したいと言っていない限り repair 提案にしない。`repairHint: null` / `suggestedSkill: null` は意図的な抑制で、`currentScopeRepairTarget: false` と `repairModeApplicability: "only-if-agent-selected"` raw `repairMode` current scope の修復指示ではないという印である。<!-- @work-item-id WI-178, WI-179, WI-180 -->
131
131
  - `repairHint` がある mechanical finding → 原則として hint のコマンドを優先し、実行前に対象ファイルと manifest の差分を確認
132
132
  - manifest parse error → `.phasegate/manifest.json` を手で修復する前に backup / uninstall / reinstall の選択肢を提示
133
133
  - reconcile / uninstall が refuse → user modified managed target として扱い、`--force` のリスクと backup path を説明して承認を取る
@@ -132,7 +132,7 @@ docs/guide/ # phasegate リポジトリ自体 (dogfood)
132
132
 
133
133
  `setup-artifacts.md` は managed target / generated artifact / runtime state / legacy artifact / user-level setting の分類を持つ。`doctor --report-out` は明示 path への出力で、`.phasegate/last-doctor-report.json` は固定生成物ではない点もここを参照する。<!-- @work-item-id WI-153 -->
134
134
 
135
- Claude-only / Codex-only setup の確認では、full `phasegate doctor` と scoped doctor を区別する。ユーザーが `setup:agent --agent claude` を選んだ場合は `phasegate doctor --agent claude --json` を優先し、`scopedOutFindings` の Codex finding は「未選択 agent の not-applicable 情報」であり修復対象ではないと説明する。`repairHint: null` / `suggestedSkill: null` は意図的な抑制で、`repairHintApplicability: "only-if-agent-selected"` はその agent を選んだ場合だけ修復 guidance が意味を持つという印である。full doctor は両 agent を導入したい場合の診断として扱う。<!-- @work-item-id WI-178, WI-179 -->
135
+ Claude-only / Codex-only setup の確認では、full `phasegate doctor` と scoped doctor を区別する。ユーザーが `setup:agent --agent claude` を選んだ場合は `phasegate doctor --agent claude --json` を優先し、`scopedOutFindings` の Codex finding は「未選択 agent の not-applicable 情報」であり修復対象ではないと説明する。`repairHint: null` / `suggestedSkill: null` は意図的な抑制で、`currentScopeRepairTarget: false` と `repairModeApplicability: "only-if-agent-selected"` `repairMode` current scope の修復指示ではないという印である。full doctor は両 agent を導入したい場合の診断として扱う。<!-- @work-item-id WI-178, WI-179, WI-180 -->
136
136
 
137
137
  `setup:agent` は初回 setup / retrofit / CI-only / agent hook 有効化の agent-readable planner、`config:plan` は安全な設定変更 intent planner。質問が「次に何を実行するか」「この変更はどの file/validation に対応するか」に寄っている場合は、これらの guide と CLI を案内する。<!-- @work-item-id WI-171, WI-172, WI-173 -->
138
138