phasegate 0.158.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.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/docs/ADR/ADR-004-scheduled-validators.md +1 -0
- package/docs/guide/configuration.md +1 -1
- package/docs/guide/layer-model.md +2 -1
- package/package.json +1 -1
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +2 -1
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -1
- package/scripts/harness/harness-error/infrastructure/adapters/validator-registry-bridge-adapter.ts +2 -0
- package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +8 -0
- package/scripts/harness/quick-mode/infrastructure/adapters/validator-system-validator-id-registry-adapter.ts +2 -2
- package/scripts/harness/skill-quality/infrastructure/adapters/validator-id-registry-bridge-adapter.ts +2 -1
- package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +21 -1
- package/scripts/harness/validator-system/composition-root.ts +6 -2
- package/scripts/harness/validator-system/domain/ports/skill-catalog-drift-port.ts +10 -0
- package/scripts/harness/validator-system/domain/services/l4/skill-catalog-drift-service.ts +108 -0
- package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +5 -2
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-skill-catalog-drift-adapter.ts +126 -0
- package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ 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
|
+
|
|
10
16
|
## [0.158.0] - 2026-05-13
|
|
11
17
|
|
|
12
18
|
### 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,
|
|
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
|
|
|
@@ -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,
|
|
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
|
|
package/package.json
CHANGED
package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts
CHANGED
|
@@ -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
|
},
|
package/scripts/harness/harness-error/infrastructure/adapters/validator-registry-bridge-adapter.ts
CHANGED
|
@@ -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
|
]);
|
|
@@ -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-
|
|
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> = {};
|