phasegate 0.140.0 → 0.141.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.ja.md +3 -2
- package/README.md +4 -4
- package/docs/guide/cli-reference.md +1 -1
- package/docs/guide/layer-model.md +6 -1
- package/package.json +1 -1
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/harness-error-formatter-adapter.ts +15 -1
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +31 -1
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +1 -1
- package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +39 -4
- package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +28 -11
- package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +5 -4
- package/scripts/harness/harness-api/domain/value-objects/drift-report-summary.ts +107 -8
- package/scripts/harness/harness-api/domain/value-objects/layer-health.ts +24 -2
- package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts +2 -0
- package/scripts/harness/integrations/pre-commit.ts +7 -4
- package/scripts/harness/main.ts +8 -1
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +47 -1
- package/scripts/harness/validator-system/application/use-cases/run-l1-validators-usecase.ts +1 -44
- package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +30 -0
- package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +1 -1
- package/scripts/harness/validator-system/composition-root.ts +16 -13
- package/scripts/harness/validator-system/domain/services/cli-e2e-test-existence-service.ts +36 -5
- package/scripts/harness/validator-system/domain/value-objects/cli-e2e-test-coverage-report.ts +8 -2
- package/scripts/harness/validator-system/infrastructure/adapters/e2e-test-file-registry-adapter.ts +12 -3
- package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +1 -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.141.0] - 2026-05-09
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **WI-024 follow-up — metadataTags suggestion text** — `architecture.metadataTags.unit` / `architecture.metadataTags.layer` を使った L1-001 / L1-002 の `suggestion` も、固定の `@unit` / `@layer` ではなく設定タグ名を表示するようにした。
|
|
15
|
+
|
|
10
16
|
## [0.140.0] - 2026-05-09
|
|
11
17
|
|
|
12
18
|
### Added
|
package/README.ja.md
CHANGED
|
@@ -148,7 +148,8 @@ npx phasegate update-skills # スキルを最新版に再デプロイ
|
|
|
148
148
|
| security, performance, coverage 90%/95%, 要件カバレッジ |
|
|
149
149
|
+------------------------------------------------------------------+
|
|
150
150
|
| L4 週次 (default off) |
|
|
151
|
-
| 設計-コード乖離, 文書整合性,
|
|
151
|
+
| 設計-コード乖離, 文書整合性, デッドコード, |
|
|
152
|
+
| doc-freshness, pointer-validation |
|
|
152
153
|
+------------------------------------------------------------------+
|
|
153
154
|
```
|
|
154
155
|
|
|
@@ -455,7 +456,7 @@ reports/
|
|
|
455
456
|
|
|
456
457
|
| Work Item | 内容 |
|
|
457
458
|
|---|---|
|
|
458
|
-
| **[WI-033](docs/inception/_cross/WI-033/description.md)** |
|
|
459
|
+
| **[WI-033](docs/inception/_cross/WI-033/description.md)** | L4 運用ロールアウトの仕上げ。`doc-freshness` / `pointer-validation` は L4-004 / L4-005 として登録済みで、`p2:*` 互換コマンドも維持。残りは scheduling / default / 運用 docs の整理。@work-item-id WI-116 |
|
|
459
460
|
|
|
460
461
|
L3 Nyquist Validation の `requirement-test-matrix.json` 自動生成はまだ未自動化です。現時点では手動セットアップで利用できます。
|
|
461
462
|
|
package/README.md
CHANGED
|
@@ -156,7 +156,7 @@ npx phasegate update-skills
|
|
|
156
156
|
| L1 | Editor save / `phasegate lint` | `@unit` / `@layer` metadata, layer violations, AI anti-patterns, dead code |
|
|
157
157
|
| L2 | Pre-commit (also evaluated inside PreToolUse at L0) | Phase gate, metadata completeness, `@work-item-id` reflection (`L2-STORY-REFLECTION`), test quality |
|
|
158
158
|
| L3 | CI/CD pipeline | Security, performance, coverage (90%/95%), requirements traceability |
|
|
159
|
-
| L4 | Scheduled (weekly). Currently `layers.L4.enabled: false` by default — opt-in per project | Design-code drift, cross-document consistency, dead code
|
|
159
|
+
| L4 | Scheduled (weekly). Currently `layers.L4.enabled: false` by default — opt-in per project | Design-code drift, cross-document consistency, dead code, doc freshness, and pointer validation. `p2:*` standalone commands remain available as compatibility entry points. |
|
|
160
160
|
|
|
161
161
|
---
|
|
162
162
|
|
|
@@ -466,8 +466,8 @@ npx phasegate <command> [options]
|
|
|
466
466
|
|---|---|
|
|
467
467
|
| `init --name <name>` | Initialize project, deploy skills, generate config. Use `--with-ci` to deploy GitHub Actions workflows. |
|
|
468
468
|
| `lint` | Run L1 Biome AST checks |
|
|
469
|
-
| `validate --layer <
|
|
470
|
-
| `ci-check` | Full CI check (L2-L4) |
|
|
469
|
+
| `validate --layer <L1-L4\|all>` | Run validators for specified layer (`--layer L0` prints runtime hook guidance; explicit L4 runs even when scheduled L4 is disabled) |
|
|
470
|
+
| `ci-check` | Full CI check (L2-L4; disabled L4 is reported as skipped) |
|
|
471
471
|
| `ci:generate-template --type <aidlc-gate\|consistency-check\|pre-commit\|agent-context-refresh> --render` | Render the bundled CI/hook template to stdout |
|
|
472
472
|
| `ci:auto-refresh-agent-context --dry-run` / `--apply` | Refresh AGENTS.md pointers and CLAUDE.md standard sections |
|
|
473
473
|
| `refresh-claude-md --dry-run` / `--apply` | Refresh only CLAUDE.md while preserving the user-owned section |
|
|
@@ -517,7 +517,7 @@ The main path is ready for project use, but a few documented behaviors still req
|
|
|
517
517
|
|
|
518
518
|
| Work Item | Title | Why it matters |
|
|
519
519
|
|---|---|---|
|
|
520
|
-
| **[WI-033](docs/inception/_cross/WI-033/description.md)** |
|
|
520
|
+
| **[WI-033](docs/inception/_cross/WI-033/description.md)** | Finish L4 operational rollout | `doc-freshness` and `pointer-validation` are registered as L4-004/L4-005 and also remain available through `p2:*` compatibility commands. Remaining WI-033 work is rollout polish around scheduling, defaults, and operational docs. @work-item-id WI-116 |
|
|
521
521
|
|
|
522
522
|
`requirement-test-matrix.json` auto-generation for L3 Nyquist Validation is not automated yet; see the L3 guide for the current manual setup.
|
|
523
523
|
|
|
@@ -261,7 +261,7 @@ Commands exposed as npm scripts (`npm run <command>`).
|
|
|
261
261
|
| `phasegate:status` | `--json` | Health summary |
|
|
262
262
|
| `phasegate:check-ready` | `--json` | Phase Gate pass status for all stories |
|
|
263
263
|
| `phasegate:check-phase` | `--unit <unitId>` `--json` | Current phase for a unit |
|
|
264
|
-
| `phasegate:ci-check` | `--json` |
|
|
264
|
+
| `phasegate:ci-check` | `--json` | Full CI check (L2-L4; disabled L4 is reported as skipped) |
|
|
265
265
|
| `phasegate:detect-drift` | `--json` | Design-code drift report |
|
|
266
266
|
| `phasegate:lint` | `--target <path>` `--json` | Lint via harness-api |
|
|
267
267
|
| `phasegate:complete-check` | `--json` | L2-L4 full check |
|
|
@@ -147,8 +147,11 @@ L4 validators are designed to run on a weekly schedule and detect slow-moving dr
|
|
|
147
147
|
| **drift-detect** | L4-001 | Bidirectional design-code drift detection. Compares design documents against the actual codebase to find divergence in either direction. |
|
|
148
148
|
| **consistency-check** | L4-002 | Cross-document layer consistency. Ensures that references between design documents, ADRs, and code remain coherent. |
|
|
149
149
|
| **dead-code** | L4-003 | Detects unused exports and unreachable code that should be removed. |
|
|
150
|
+
| **doc-freshness** | L4-004 | Checks design document freshness against the configured threshold. Also available through the `p2:check-freshness` compatibility command. |
|
|
151
|
+
| **pointer-validation** | L4-005 | Resolves and validates design document pointers. Also available through the `p2:validate-pointers` compatibility command. |
|
|
150
152
|
|
|
151
|
-
|
|
153
|
+
<!-- @work-item-id WI-116 -->
|
|
154
|
+
`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; WI-033's remaining scope is operational rollout, not validator registration.
|
|
152
155
|
|
|
153
156
|
### Drift-detect design pointers
|
|
154
157
|
|
|
@@ -219,6 +222,8 @@ Each error catalog entry declares a `defaultSeverity` of `error` or `warning`. A
|
|
|
219
222
|
|
|
220
223
|
**Opt-in to strict mode** via `phasegate.config.json` `validate.failOnWarning: true` or CLI `--fail-on-warning` (CLI > config). The `strict` preset defaults to `failOnWarning: true` to match the precedent set by the `ci-governance` preset adapter.
|
|
221
224
|
|
|
225
|
+
For L4 specifically, `validate --layer L4` is an explicit operator request and runs L4 validators even when `layers.L4.enabled` is false. `validate --layer all` and `phasegate:ci-check` honor disabled L4 as skipped results, so skipped L4 validators are visible in the report but do not become failures under `--fail-on-warning`. @work-item-id WI-107
|
|
226
|
+
|
|
222
227
|
History: prior to v0.131.0, the aggregator's `failOnWarning` flag was effectively dead code (`hasFail = !result.passed || ...` masked it), so every warning-only fail produced exit 1 regardless of severity declaration. See ADR-017 for the rationale.
|
|
223
228
|
|
|
224
229
|
---
|
package/package.json
CHANGED
package/scripts/harness/biome-ast-engine/infrastructure/adapters/harness-error-formatter-adapter.ts
CHANGED
|
@@ -18,6 +18,20 @@ const RULE_SUGGESTIONS: Readonly<Record<string, string>> = {
|
|
|
18
18
|
'no-comment-flood': '不要なコメントを整理し自己説明的なコードへ改善する',
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
const METADATA_TAG_MESSAGE_PATTERN = /(@[a-z][a-zA-Z0-9]*)コメントが必要です/;
|
|
22
|
+
|
|
23
|
+
const resolveSuggestion = (ruleName: string, message: string): string => {
|
|
24
|
+
if (ruleName === 'require-unit-comment' || ruleName === 'require-layer-comment') {
|
|
25
|
+
const tagName = message.match(METADATA_TAG_MESSAGE_PATTERN)?.[1];
|
|
26
|
+
|
|
27
|
+
if (tagName) {
|
|
28
|
+
return `ファイル先頭に ${tagName} コメントを追加する`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return RULE_SUGGESTIONS[ruleName] ?? '';
|
|
33
|
+
};
|
|
34
|
+
|
|
21
35
|
/**
|
|
22
36
|
* ViolationFormatterPort の実装。
|
|
23
37
|
* RuleViolation をルール名から L1-001〜L1-008 コードへマッピングし、
|
|
@@ -40,7 +54,7 @@ export class HarnessErrorFormatterAdapter implements ViolationFormatterPort {
|
|
|
40
54
|
violations.map((v) => {
|
|
41
55
|
const ruleName = v.ruleName.toString();
|
|
42
56
|
const code = mapRuleNameToCode(ruleName);
|
|
43
|
-
const suggestion =
|
|
57
|
+
const suggestion = resolveSuggestion(ruleName, v.message);
|
|
44
58
|
|
|
45
59
|
const entry: {
|
|
46
60
|
code: string;
|
|
@@ -19,6 +19,36 @@ export interface TypeScriptSourceModuleAnalyzerAdapterDeps {
|
|
|
19
19
|
readonly rootDir: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
const HARNESS_ROOT_PREFIX = 'scripts/harness/';
|
|
23
|
+
const TESTS_SEGMENT = '__tests__';
|
|
24
|
+
const TEST_SCOPE_SEGMENTS = new Set(['unit', 'integration', 'e2e']);
|
|
25
|
+
|
|
26
|
+
function deriveUnitNameFromPath(filePath: FilePath): string | null {
|
|
27
|
+
const normalizedPath = filePath.toString();
|
|
28
|
+
if (!normalizedPath.startsWith(HARNESS_ROOT_PREFIX)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const parts = normalizedPath.slice(HARNESS_ROOT_PREFIX.length).split('/');
|
|
33
|
+
if (parts.length < 2) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (parts[0] !== TESTS_SEGMENT) {
|
|
38
|
+
return parts[0] || null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (parts.length < 3) {
|
|
42
|
+
return parts[1] || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (TEST_SCOPE_SEGMENTS.has(parts[1])) {
|
|
46
|
+
return parts[2] || null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return parts[1] || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
22
52
|
/**
|
|
23
53
|
* SourceModuleAnalyzerPort の実装。
|
|
24
54
|
* TypeScript Compiler API を使用して各ファイルから
|
|
@@ -69,7 +99,7 @@ export class TypeScriptSourceModuleAnalyzerAdapter implements SourceModuleAnalyz
|
|
|
69
99
|
SourceModuleSnapshot.create(
|
|
70
100
|
{
|
|
71
101
|
filePath,
|
|
72
|
-
declaredUnit: unitResult.unitNames[0] ??
|
|
102
|
+
declaredUnit: unitResult.unitNames[0] ?? deriveUnitNameFromPath(filePath),
|
|
73
103
|
declaredLayer: layerResult.layerName,
|
|
74
104
|
imports,
|
|
75
105
|
anyTypeCount: anyCount,
|
package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts
CHANGED
|
@@ -11,7 +11,7 @@ export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefi
|
|
|
11
11
|
return {
|
|
12
12
|
project: { preset: resolvedConfig.project.preset },
|
|
13
13
|
layers: {
|
|
14
|
-
L2: { enabled: resolvedConfig.layers.L2.enabled },
|
|
14
|
+
L2: { enabled: resolvedConfig.layers.L2.enabled, validators: ['L2-001', 'L2-002', 'L2-003', 'L2-013'] },
|
|
15
15
|
L3: { enabled: resolvedConfig.layers.L3.enabled },
|
|
16
16
|
L4: { enabled: resolvedConfig.layers.L4.enabled, validators: resolvedConfig.layers.L4.validators },
|
|
17
17
|
},
|
|
@@ -13,6 +13,7 @@ import type { BiomeLintPort } from '../ports/biome-lint-port.js';
|
|
|
13
13
|
import type { ImpactAnalysisPort } from '../ports/impact-analysis-port.js';
|
|
14
14
|
import type { ArtifactScannerPort } from '../ports/artifact-scanner-port.js';
|
|
15
15
|
import type { ConfigQueryPort } from '../ports/config-query-port.js';
|
|
16
|
+
import type { LayerId } from '../value-objects/layer-health.js';
|
|
16
17
|
|
|
17
18
|
export interface CommandDispatchPorts {
|
|
18
19
|
validatorExecutionPort: ValidatorExecutionPort;
|
|
@@ -35,6 +36,29 @@ function makeError(message: string): HarnessError {
|
|
|
35
36
|
return { code: 'HARNESS_ERROR', severity: 'error', message };
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
type LiveValidationState = 'pass' | 'fail' | 'skipped' | 'not-run' | 'error';
|
|
40
|
+
|
|
41
|
+
function layerIdFromValidatorId(validatorId: string): LayerId | null {
|
|
42
|
+
const prefix = validatorId.slice(0, 2);
|
|
43
|
+
return prefix === 'L1' || prefix === 'L2' || prefix === 'L3' || prefix === 'L4' ? prefix : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function summarizeLayerResults(items: readonly { validatorId: string; passed: boolean; skipped?: boolean }[]): Partial<Record<LayerId, LiveValidationState>> {
|
|
47
|
+
const result: Partial<Record<LayerId, LiveValidationState>> = {};
|
|
48
|
+
for (const layerId of ['L2', 'L3', 'L4'] as const) {
|
|
49
|
+
const layerItems = items.filter((item) => layerIdFromValidatorId(item.validatorId) === layerId);
|
|
50
|
+
if (layerItems.length === 0) continue;
|
|
51
|
+
if (layerItems.every((item) => item.skipped === true)) {
|
|
52
|
+
result[layerId] = 'skipped';
|
|
53
|
+
} else if (layerItems.some((item) => !item.passed && item.skipped !== true)) {
|
|
54
|
+
result[layerId] = 'fail';
|
|
55
|
+
} else {
|
|
56
|
+
result[layerId] = 'pass';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
38
62
|
const KNOWN_COMMANDS = new Set([
|
|
39
63
|
'phasegate:check-ready',
|
|
40
64
|
'phasegate:check-phase',
|
|
@@ -131,7 +155,7 @@ export class CommandDispatchService {
|
|
|
131
155
|
}
|
|
132
156
|
|
|
133
157
|
case 'phasegate:ci-check': {
|
|
134
|
-
const validatorResults = await this.ports.validatorExecutionPort.
|
|
158
|
+
const validatorResults = await this.ports.validatorExecutionPort.runAllValidators();
|
|
135
159
|
const result = CiCheckResult.fromResults(validatorResults);
|
|
136
160
|
if (result.allPassed) {
|
|
137
161
|
const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, result);
|
|
@@ -152,13 +176,23 @@ export class CommandDispatchService {
|
|
|
152
176
|
const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, result);
|
|
153
177
|
return { status: 'pass', errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
|
|
154
178
|
}
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
return { status: 'fail', errors: r.errors, summary: r.summary, data: result as unknown as T, exitCode: 1 };
|
|
179
|
+
const r = HarnessApiResponse.pass({ ...summary, passed: 1, warnings: drifts.length }, result);
|
|
180
|
+
return { status: 'pass', errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
|
|
158
181
|
}
|
|
159
182
|
|
|
160
183
|
case 'phasegate:status': {
|
|
161
184
|
const scanResult = await this.ports.artifactScannerPort.scan();
|
|
185
|
+
const liveValidationByLayer: Partial<Record<LayerId, LiveValidationState>> = {};
|
|
186
|
+
try {
|
|
187
|
+
const [lintResult, validatorResults] = await Promise.all([
|
|
188
|
+
this.ports.biomeLintPort.runLint(),
|
|
189
|
+
this.ports.validatorExecutionPort.runAllValidators(),
|
|
190
|
+
]);
|
|
191
|
+
liveValidationByLayer.L1 = lintResult.passed ? 'pass' : 'fail';
|
|
192
|
+
Object.assign(liveValidationByLayer, summarizeLayerResults(validatorResults));
|
|
193
|
+
} catch {
|
|
194
|
+
liveValidationByLayer.L1 = liveValidationByLayer.L1 ?? 'error';
|
|
195
|
+
}
|
|
162
196
|
let presetInfo = { name: 'standard' as const, enabledLayers: ['L1', 'L2', 'L3'] as ('L1' | 'L2' | 'L3' | 'L4')[] };
|
|
163
197
|
const configPort = this.ports.configQueryPort;
|
|
164
198
|
if (configPort.getPresetInfo) {
|
|
@@ -172,6 +206,7 @@ export class CommandDispatchService {
|
|
|
172
206
|
presetInfo,
|
|
173
207
|
configSummary: { configPath: 'phasegate.config.json', lastModified: '', version: '2' },
|
|
174
208
|
phaseGateSummary: { totalStories: 0, passedStories: 0, pendingStories: 0 },
|
|
209
|
+
liveValidationByLayer,
|
|
175
210
|
});
|
|
176
211
|
const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, statusSummary);
|
|
177
212
|
return { status: 'pass', errors: [], summary: r.summary, data: statusSummary as unknown as T, exitCode: 0 };
|
|
@@ -88,27 +88,44 @@ export class StatusDerivationService {
|
|
|
88
88
|
presetInfo: PresetInfo;
|
|
89
89
|
configSummary: ConfigSummary;
|
|
90
90
|
phaseGateSummary: PhaseGateSummary;
|
|
91
|
+
liveValidationByLayer?: Partial<Record<LayerId, 'pass' | 'fail' | 'skipped' | 'not-run' | 'error'>>;
|
|
91
92
|
}): HarnessStatusSummary {
|
|
92
|
-
const { scanResult, presetInfo, configSummary, phaseGateSummary } = input;
|
|
93
|
+
const { scanResult, presetInfo, configSummary, phaseGateSummary, liveValidationByLayer } = input;
|
|
93
94
|
|
|
94
95
|
const layers: LayerHealth[] = ALL_LAYER_IDS.map((layerId) => {
|
|
95
96
|
const enabled = presetInfo.enabledLayers.includes(layerId);
|
|
97
|
+
const configurationState = enabled ? 'enabled' : 'disabled';
|
|
96
98
|
|
|
97
99
|
// Find from derivedLayerHealth
|
|
98
100
|
const existing = scanResult.derivedLayerHealth.find((l) => l.layerId === layerId);
|
|
101
|
+
const layerArtifacts = scanResult.foundArtifacts.filter((a) => getLayerId(a) === layerId);
|
|
102
|
+
const hasPresent = layerArtifacts.some((a) => a.present === true);
|
|
103
|
+
const cachedArtifactState = hasPresent ? 'present' : 'missing';
|
|
104
|
+
const liveValidationState = liveValidationByLayer?.[layerId] ?? 'not-run';
|
|
105
|
+
const liveLastResult =
|
|
106
|
+
liveValidationState === 'pass' || liveValidationState === 'fail'
|
|
107
|
+
? liveValidationState
|
|
108
|
+
: undefined;
|
|
99
109
|
|
|
100
110
|
if (enabled) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
const lastResult = liveLastResult ?? existing?.lastResult ?? (hasPresent ? 'pass' : 'unknown');
|
|
112
|
+
return LayerHealth.create({
|
|
113
|
+
layerId,
|
|
114
|
+
enabled,
|
|
115
|
+
lastResult,
|
|
116
|
+
configurationState,
|
|
117
|
+
cachedArtifactState,
|
|
118
|
+
liveValidationState,
|
|
119
|
+
});
|
|
110
120
|
} else {
|
|
111
|
-
return LayerHealth.create({
|
|
121
|
+
return LayerHealth.create({
|
|
122
|
+
layerId,
|
|
123
|
+
enabled: false,
|
|
124
|
+
lastResult: undefined,
|
|
125
|
+
configurationState,
|
|
126
|
+
cachedArtifactState,
|
|
127
|
+
liveValidationState,
|
|
128
|
+
});
|
|
112
129
|
}
|
|
113
130
|
});
|
|
114
131
|
|
|
@@ -6,6 +6,7 @@ import type { HarnessError } from './harness-api-response.js';
|
|
|
6
6
|
export interface ValidatorCheckItem {
|
|
7
7
|
validatorId: string;
|
|
8
8
|
passed: boolean;
|
|
9
|
+
skipped?: boolean;
|
|
9
10
|
errors?: readonly HarnessError[];
|
|
10
11
|
}
|
|
11
12
|
|
|
@@ -29,8 +30,8 @@ export class CiCheckResult {
|
|
|
29
30
|
if (!props.validatorResults || props.validatorResults.length === 0) {
|
|
30
31
|
throw new Error('EmptyValidatorResultsError: validatorResults must have at least one item (INV-5)');
|
|
31
32
|
}
|
|
32
|
-
// INV-6: allPassed === validatorResults.every(r => r.passed)
|
|
33
|
-
const computedAllPassed = props.validatorResults.every((r) => r.passed);
|
|
33
|
+
// INV-6: allPassed === validatorResults.every(r => r.passed || r.skipped)
|
|
34
|
+
const computedAllPassed = props.validatorResults.every((r) => r.passed || r.skipped);
|
|
34
35
|
if (props.allPassed !== computedAllPassed) {
|
|
35
36
|
throw new Error(
|
|
36
37
|
`HarnessApiDomainError: allPassed=${props.allPassed} does not match validatorResults state (computed: ${computedAllPassed})`
|
|
@@ -43,12 +44,12 @@ export class CiCheckResult {
|
|
|
43
44
|
if (!validatorResults || validatorResults.length === 0) {
|
|
44
45
|
throw new Error('EmptyValidatorResultsError: validatorResults must have at least one item (INV-5)');
|
|
45
46
|
}
|
|
46
|
-
const allPassed = validatorResults.every((r) => r.passed);
|
|
47
|
+
const allPassed = validatorResults.every((r) => r.passed || r.skipped);
|
|
47
48
|
return new CiCheckResult(validatorResults, allPassed);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
getFailedValidators(): readonly ValidatorCheckItem[] {
|
|
51
|
-
return this.validatorResults.filter((r) => !r.passed);
|
|
52
|
+
return this.validatorResults.filter((r) => !r.passed && !r.skipped);
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
collectAllErrors(): readonly HarnessError[] {
|
|
@@ -8,18 +8,117 @@ export interface DriftItem {
|
|
|
8
8
|
recommendation: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
type DriftCategory =
|
|
12
|
+
| 'code-missing-design'
|
|
13
|
+
| 'design-missing-code'
|
|
14
|
+
| 'missing-pointer'
|
|
15
|
+
| 'design-granularity-mismatch'
|
|
16
|
+
| 'uncategorized';
|
|
17
|
+
type DriftSeverity = 'info' | 'warning' | 'error';
|
|
18
|
+
|
|
19
|
+
export interface ActionableDriftItem extends DriftItem {
|
|
20
|
+
category: DriftCategory;
|
|
21
|
+
severity: DriftSeverity;
|
|
22
|
+
nextAction: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DriftCategorySummary {
|
|
26
|
+
category: DriftCategory;
|
|
27
|
+
severity: DriftSeverity;
|
|
28
|
+
count: number;
|
|
29
|
+
nextAction: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
11
32
|
export interface DriftReportSummaryProps {
|
|
12
33
|
drifts: readonly DriftItem[];
|
|
13
34
|
totalCount: number;
|
|
35
|
+
sampleLimit?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const DEFAULT_SAMPLE_LIMIT = 20;
|
|
39
|
+
|
|
40
|
+
function classifyDrift(drift: DriftItem): { category: DriftCategory; severity: DriftSeverity; nextAction: string } {
|
|
41
|
+
const direction = drift.direction.toLowerCase();
|
|
42
|
+
const recommendation = drift.recommendation.toLowerCase();
|
|
43
|
+
const element = drift.element.toLowerCase();
|
|
44
|
+
|
|
45
|
+
if (recommendation.includes('pointer') || recommendation.includes('@work-item-id')) {
|
|
46
|
+
return {
|
|
47
|
+
category: 'missing-pointer',
|
|
48
|
+
severity: 'warning',
|
|
49
|
+
nextAction: 'Add or repair the traceability pointer before changing gating policy.',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (element.includes('heading') || recommendation.includes('granularity')) {
|
|
53
|
+
return {
|
|
54
|
+
category: 'design-granularity-mismatch',
|
|
55
|
+
severity: 'info',
|
|
56
|
+
nextAction: 'Review the design granularity or baseline this class of advisory drift.',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (direction.includes('code') && direction.includes('design')) {
|
|
60
|
+
return {
|
|
61
|
+
category: direction.indexOf('code') < direction.indexOf('design') ? 'code-missing-design' : 'design-missing-code',
|
|
62
|
+
severity: 'warning',
|
|
63
|
+
nextAction:
|
|
64
|
+
direction.indexOf('code') < direction.indexOf('design')
|
|
65
|
+
? 'Update the matching product/construction docs with the implementation contract.'
|
|
66
|
+
: 'Implement the described design element or remove stale design text.',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
category: 'uncategorized',
|
|
71
|
+
severity: 'warning',
|
|
72
|
+
nextAction: 'Inspect the sampled drift and assign a narrower category or baseline rule.',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function enrichDrift(drift: DriftItem): ActionableDriftItem {
|
|
77
|
+
return { ...drift, ...classifyDrift(drift) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function summarizeByCategory(drifts: readonly ActionableDriftItem[]): readonly DriftCategorySummary[] {
|
|
81
|
+
const byCategory = new Map<DriftCategory, DriftCategorySummary>();
|
|
82
|
+
for (const drift of drifts) {
|
|
83
|
+
const existing = byCategory.get(drift.category);
|
|
84
|
+
if (existing) {
|
|
85
|
+
byCategory.set(drift.category, { ...existing, count: existing.count + 1 });
|
|
86
|
+
} else {
|
|
87
|
+
byCategory.set(drift.category, {
|
|
88
|
+
category: drift.category,
|
|
89
|
+
severity: drift.severity,
|
|
90
|
+
count: 1,
|
|
91
|
+
nextAction: drift.nextAction,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return [...byCategory.values()].sort((a, b) => b.count - a.count || a.category.localeCompare(b.category));
|
|
14
96
|
}
|
|
15
97
|
|
|
16
98
|
export class DriftReportSummary {
|
|
17
|
-
readonly drifts: readonly
|
|
99
|
+
readonly drifts: readonly ActionableDriftItem[];
|
|
18
100
|
readonly totalCount: number;
|
|
101
|
+
readonly rawDriftCount: number;
|
|
102
|
+
readonly sampleLimit: number;
|
|
103
|
+
readonly truncated: boolean;
|
|
104
|
+
readonly categorySummaries: readonly DriftCategorySummary[];
|
|
105
|
+
readonly actionPlan: readonly DriftCategorySummary[];
|
|
19
106
|
|
|
20
|
-
private constructor(
|
|
21
|
-
|
|
107
|
+
private constructor(
|
|
108
|
+
drifts: readonly DriftItem[],
|
|
109
|
+
totalCount: number,
|
|
110
|
+
sampleLimit: number = DEFAULT_SAMPLE_LIMIT,
|
|
111
|
+
summarySource: readonly DriftItem[] = drifts,
|
|
112
|
+
) {
|
|
113
|
+
const enriched = drifts.map(enrichDrift);
|
|
114
|
+
const enrichedSummarySource = summarySource.map(enrichDrift);
|
|
115
|
+
this.drifts = Object.freeze(enriched);
|
|
22
116
|
this.totalCount = totalCount;
|
|
117
|
+
this.rawDriftCount = totalCount;
|
|
118
|
+
this.sampleLimit = sampleLimit;
|
|
119
|
+
this.truncated = totalCount > enriched.length;
|
|
120
|
+
this.categorySummaries = Object.freeze(summarizeByCategory(enrichedSummarySource));
|
|
121
|
+
this.actionPlan = Object.freeze(this.categorySummaries.slice(0, 5));
|
|
23
122
|
Object.freeze(this);
|
|
24
123
|
}
|
|
25
124
|
|
|
@@ -30,18 +129,18 @@ export class DriftReportSummary {
|
|
|
30
129
|
`HarnessApiDomainError: totalCount=${props.totalCount} does not match drifts.length=${props.drifts.length} (INV-7)`
|
|
31
130
|
);
|
|
32
131
|
}
|
|
33
|
-
return new DriftReportSummary(props.drifts, props.totalCount);
|
|
132
|
+
return new DriftReportSummary(props.drifts, props.totalCount, props.sampleLimit);
|
|
34
133
|
}
|
|
35
134
|
|
|
36
|
-
static fromDrifts(drifts: readonly DriftItem[]): DriftReportSummary {
|
|
37
|
-
return new DriftReportSummary(drifts, drifts.length);
|
|
135
|
+
static fromDrifts(drifts: readonly DriftItem[], sampleLimit: number = DEFAULT_SAMPLE_LIMIT): DriftReportSummary {
|
|
136
|
+
return new DriftReportSummary(drifts.slice(0, sampleLimit), drifts.length, sampleLimit, drifts);
|
|
38
137
|
}
|
|
39
138
|
|
|
40
139
|
hasDrift(): boolean {
|
|
41
|
-
return this.
|
|
140
|
+
return this.totalCount > 0;
|
|
42
141
|
}
|
|
43
142
|
|
|
44
|
-
filterByUnit(unitId: string): readonly
|
|
143
|
+
filterByUnit(unitId: string): readonly ActionableDriftItem[] {
|
|
45
144
|
return this.drifts.filter((d) => d.unit === unitId);
|
|
46
145
|
}
|
|
47
146
|
}
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
export type LayerId = 'L1' | 'L2' | 'L3' | 'L4';
|
|
5
5
|
type LastResult = 'pass' | 'fail' | 'unknown' | undefined;
|
|
6
|
+
type ConfigurationState = 'enabled' | 'disabled';
|
|
7
|
+
type CachedArtifactState = 'present' | 'missing' | 'unknown';
|
|
8
|
+
type LiveValidationState = 'pass' | 'fail' | 'skipped' | 'not-run' | 'error';
|
|
6
9
|
|
|
7
10
|
const VALID_LAYER_IDS: readonly LayerId[] = ['L1', 'L2', 'L3', 'L4'];
|
|
8
11
|
const VALID_LAST_RESULTS: readonly string[] = ['pass', 'fail', 'unknown'];
|
|
@@ -11,17 +14,33 @@ export interface LayerHealthProps {
|
|
|
11
14
|
layerId: string;
|
|
12
15
|
enabled: boolean;
|
|
13
16
|
lastResult?: string;
|
|
17
|
+
configurationState?: ConfigurationState;
|
|
18
|
+
cachedArtifactState?: CachedArtifactState;
|
|
19
|
+
liveValidationState?: LiveValidationState;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
export class LayerHealth {
|
|
17
23
|
readonly layerId: LayerId;
|
|
18
24
|
readonly enabled: boolean;
|
|
19
25
|
readonly lastResult: LastResult;
|
|
26
|
+
readonly configurationState: ConfigurationState;
|
|
27
|
+
readonly cachedArtifactState: CachedArtifactState;
|
|
28
|
+
readonly liveValidationState: LiveValidationState;
|
|
20
29
|
|
|
21
|
-
private constructor(
|
|
30
|
+
private constructor(
|
|
31
|
+
layerId: LayerId,
|
|
32
|
+
enabled: boolean,
|
|
33
|
+
lastResult: LastResult,
|
|
34
|
+
configurationState: ConfigurationState,
|
|
35
|
+
cachedArtifactState: CachedArtifactState,
|
|
36
|
+
liveValidationState: LiveValidationState,
|
|
37
|
+
) {
|
|
22
38
|
this.layerId = layerId;
|
|
23
39
|
this.enabled = enabled;
|
|
24
40
|
this.lastResult = lastResult;
|
|
41
|
+
this.configurationState = configurationState;
|
|
42
|
+
this.cachedArtifactState = cachedArtifactState;
|
|
43
|
+
this.liveValidationState = liveValidationState;
|
|
25
44
|
Object.freeze(this);
|
|
26
45
|
}
|
|
27
46
|
|
|
@@ -35,7 +54,10 @@ export class LayerHealth {
|
|
|
35
54
|
return new LayerHealth(
|
|
36
55
|
props.layerId as LayerId,
|
|
37
56
|
props.enabled,
|
|
38
|
-
props.lastResult as LastResult
|
|
57
|
+
props.lastResult as LastResult,
|
|
58
|
+
props.configurationState ?? (props.enabled ? 'enabled' : 'disabled'),
|
|
59
|
+
props.cachedArtifactState ?? 'unknown',
|
|
60
|
+
props.liveValidationState ?? 'not-run',
|
|
39
61
|
);
|
|
40
62
|
}
|
|
41
63
|
|
package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts
CHANGED
|
@@ -45,6 +45,7 @@ export class ValidatorSystemExecutionAdapter implements ValidatorExecutionPort {
|
|
|
45
45
|
return results.map((r) => ({
|
|
46
46
|
validatorId: r.validatorId,
|
|
47
47
|
passed: r.passed,
|
|
48
|
+
skipped: r.skipped,
|
|
48
49
|
errors: r.errors.map((e) => ({ code: e.code, severity: e.severity, message: e.message })),
|
|
49
50
|
}));
|
|
50
51
|
},
|
|
@@ -56,6 +57,7 @@ export class ValidatorSystemExecutionAdapter implements ValidatorExecutionPort {
|
|
|
56
57
|
return report.results.map((r) => ({
|
|
57
58
|
validatorId: r.validatorId,
|
|
58
59
|
passed: r.passed,
|
|
60
|
+
skipped: r.skipped,
|
|
59
61
|
errors: r.errors.map((e) => ({ code: e.code, severity: e.severity, message: e.message })),
|
|
60
62
|
}));
|
|
61
63
|
},
|
|
@@ -18,7 +18,6 @@ import { execSync } from "node:child_process";
|
|
|
18
18
|
import { readFile } from "node:fs/promises";
|
|
19
19
|
import { createConfigFoundationModule } from "../config-foundation/composition-root.js";
|
|
20
20
|
import { toValidatorSystemConfig } from "../config-foundation/application/mappers/validator-system-config-mapper.js";
|
|
21
|
-
import { ConfigNotFoundError } from "../config-foundation/infrastructure/repositories/file-system-config-repository.js";
|
|
22
21
|
import { createTraceabilityModelModule } from "../traceability-model/composition-root.js";
|
|
23
22
|
import type { ValidateMetadataCommandOutput } from "../traceability-model/presentation/cli/validate-metadata-command-handler.js";
|
|
24
23
|
import type { AggregatedValidationReport } from "../validator-system/application/dto/aggregated-validation-report.js";
|
|
@@ -38,13 +37,17 @@ const WORK_ITEM_PATH_PATTERN = /(?:^|\/)WI-\d+(?:\/|$)/;
|
|
|
38
37
|
const WORK_ITEM_TRAILER_PATTERN = /^Work-Item:\s*WI-\d+\s*$/m;
|
|
39
38
|
const TEST_FILE_SUFFIXES = Object.freeze([".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx"]);
|
|
40
39
|
|
|
40
|
+
function isConfigNotFoundError(err: unknown): boolean {
|
|
41
|
+
return err instanceof Error && err.name === "ConfigNotFoundError";
|
|
42
|
+
}
|
|
43
|
+
|
|
41
44
|
async function loadValidatorSystemConfig(): Promise<object | undefined> {
|
|
42
45
|
const configMod = createConfigFoundationModule();
|
|
43
46
|
try {
|
|
44
47
|
const resolvedConfig = await configMod.usecases.loadResolvedConfigUseCase.execute();
|
|
45
48
|
return toValidatorSystemConfig(resolvedConfig.config);
|
|
46
49
|
} catch (err) {
|
|
47
|
-
if (err
|
|
50
|
+
if (isConfigNotFoundError(err)) return undefined;
|
|
48
51
|
throw err;
|
|
49
52
|
}
|
|
50
53
|
}
|
|
@@ -57,7 +60,7 @@ async function loadTraceabilityModelOptions(): Promise<
|
|
|
57
60
|
const resolvedConfig = await configMod.usecases.loadResolvedConfigUseCase.execute();
|
|
58
61
|
return { pathRoots: { designDocsRoot: resolvedConfig.config.paths.designDocs } };
|
|
59
62
|
} catch (err) {
|
|
60
|
-
if (err
|
|
63
|
+
if (isConfigNotFoundError(err)) return undefined;
|
|
61
64
|
throw err;
|
|
62
65
|
}
|
|
63
66
|
}
|
|
@@ -68,7 +71,7 @@ async function loadPreCommitImplementationExtensions(): Promise<readonly string[
|
|
|
68
71
|
const resolvedConfig = await configMod.usecases.loadResolvedConfigUseCase.execute();
|
|
69
72
|
return resolvedConfig.config.preCommit?.implementationExtensions;
|
|
70
73
|
} catch (err) {
|
|
71
|
-
if (err
|
|
74
|
+
if (isConfigNotFoundError(err)) return undefined;
|
|
72
75
|
throw err;
|
|
73
76
|
}
|
|
74
77
|
}
|
package/scripts/harness/main.ts
CHANGED
|
@@ -192,6 +192,13 @@ function parseTriStateFlag(args: readonly string[], positiveFlag: string, negati
|
|
|
192
192
|
return positiveIdx > negativeIdx;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
function parseValidateFormat(args: readonly string[]): "human" | "agent" | "ci" | undefined {
|
|
196
|
+
const raw = parseFlag(args, "--format");
|
|
197
|
+
if (raw === undefined) return undefined;
|
|
198
|
+
if (raw === "human" || raw === "agent" || raw === "ci") return raw;
|
|
199
|
+
throw new Error(`Invalid --format value for validate: '${raw}'. Supported values: human, agent, ci.`);
|
|
200
|
+
}
|
|
201
|
+
|
|
195
202
|
function levenshtein(a: string, b: string): number {
|
|
196
203
|
const m = a.length;
|
|
197
204
|
const n = b.length;
|
|
@@ -1024,7 +1031,7 @@ async function main(): Promise<void> {
|
|
|
1024
1031
|
const layer = parseFlag(args, "--layer") as "L0" | "L2" | "L3" | "L4" | "all" | undefined;
|
|
1025
1032
|
const unit = parseFlag(args, "--unit");
|
|
1026
1033
|
const phase = parseFlag(args, "--phase");
|
|
1027
|
-
const format =
|
|
1034
|
+
const format = parseValidateFormat(args);
|
|
1028
1035
|
// WI-094 / ADR-017: --fail-on-warning / --no-fail-on-warning / 未指定→config値
|
|
1029
1036
|
const failOnWarning = parseTriStateFlag(args, "--fail-on-warning", "--no-fail-on-warning");
|
|
1030
1037
|
const noL4 = hasFlag(args, "--no-l4");
|
|
@@ -146,7 +146,12 @@ export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSyst
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
const legacyId = await this.readLegacyId(storyId);
|
|
149
|
-
|
|
149
|
+
if (legacyId === null || !annotationIds.includes(legacyId)) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const unitId = this.extractProductUnitId(productPath);
|
|
154
|
+
return !(await this.isAmbiguousLegacyId(legacyId, storyId, unitId));
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
private extractAnnotationIds(content: string): readonly string[] {
|
|
@@ -182,6 +187,47 @@ export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSyst
|
|
|
182
187
|
return null;
|
|
183
188
|
}
|
|
184
189
|
|
|
190
|
+
private extractProductUnitId(productPath: string): string | null {
|
|
191
|
+
const normalized = productPath.split(path.sep).join("/");
|
|
192
|
+
const match = /^docs\/product\/construction\/([^/]+)\//.exec(normalized);
|
|
193
|
+
return match?.[1] ?? null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private async isAmbiguousLegacyId(legacyId: string, storyId: string, unitId: string | null): Promise<boolean> {
|
|
197
|
+
const scopedStoryIds = unitId === null
|
|
198
|
+
? await this.listAllWorkItemDirectories()
|
|
199
|
+
: await this.listStoryDirectories(unitId);
|
|
200
|
+
const matchingStoryIds: string[] = [];
|
|
201
|
+
|
|
202
|
+
for (const candidateStoryId of scopedStoryIds) {
|
|
203
|
+
const candidateLegacyId = await this.readLegacyId(candidateStoryId);
|
|
204
|
+
if (candidateLegacyId === legacyId) {
|
|
205
|
+
matchingStoryIds.push(candidateStoryId);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return matchingStoryIds.length > 1 || (matchingStoryIds.length === 1 && matchingStoryIds[0] !== storyId);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private async listAllWorkItemDirectories(): Promise<readonly string[]> {
|
|
213
|
+
const ids = new Set<string>();
|
|
214
|
+
for (const id of await this.listCrossWorkItemDirectories()) {
|
|
215
|
+
ids.add(id);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const inceptionDir = path.join(this.rootDir, this.inceptionRoot);
|
|
219
|
+
for (const entry of await this.readDirectories(inceptionDir)) {
|
|
220
|
+
if (!entry.isDirectory()) continue;
|
|
221
|
+
const name = entry.name;
|
|
222
|
+
if (name.startsWith("_") || name.startsWith(".") || name === "issues") continue;
|
|
223
|
+
for (const id of await this.listUnitWorkItemDirectories(name)) {
|
|
224
|
+
ids.add(id);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return [...ids].sort();
|
|
229
|
+
}
|
|
230
|
+
|
|
185
231
|
private async listDescriptionCandidates(storyId: string): Promise<readonly string[]> {
|
|
186
232
|
const inceptionDir = path.join(this.rootDir, this.inceptionRoot);
|
|
187
233
|
const candidates: string[] = [path.join(inceptionDir, "_cross", storyId, "description.md")];
|
|
@@ -9,38 +9,28 @@ import { ValidatorId } from '../../domain/value-objects/validator-id.js';
|
|
|
9
9
|
import { ValidationResult } from '../../domain/value-objects/validation-result.js';
|
|
10
10
|
import { ItTestMockDetectionService } from '../../domain/services/it-test-mock-detection-service.js';
|
|
11
11
|
import { StubCommentDetectionService } from '../../domain/services/stub-comment-detection-service.js';
|
|
12
|
-
import { CliE2eTestExistenceService } from '../../domain/services/cli-e2e-test-existence-service.js';
|
|
13
12
|
import { ValidationResultContractMapper } from '../mappers/validation-result-contract-mapper.js';
|
|
14
13
|
import type { ValidationResultContract } from '../dto/validation-result-contract.js';
|
|
15
14
|
import type { RunL1ValidatorsInput } from '../dto/run-l1-validators-input.js';
|
|
16
15
|
import type { ItTestFileAnalyzerPort } from '../../domain/ports/it-test-file-analyzer-port.js';
|
|
17
16
|
import type { SourceFileTextScannerPort } from '../../domain/ports/source-file-text-scanner-port.js';
|
|
18
|
-
import type { CliCommandRegistryPort } from '../../domain/ports/cli-command-registry-port.js';
|
|
19
|
-
import type { E2eTestFileRegistryPort } from '../../domain/ports/e2e-test-file-registry-port.js';
|
|
20
17
|
|
|
21
18
|
export interface RunL1ValidatorsUseCaseDeps {
|
|
22
19
|
itTestFileAnalyzerPort: ItTestFileAnalyzerPort;
|
|
23
20
|
sourceFileTextScannerPort: SourceFileTextScannerPort;
|
|
24
|
-
cliCommandRegistryPort?: CliCommandRegistryPort;
|
|
25
|
-
e2eTestFileRegistryPort?: E2eTestFileRegistryPort;
|
|
26
21
|
contractMapper: ValidationResultContractMapper;
|
|
27
22
|
}
|
|
28
23
|
|
|
29
24
|
export class RunL1ValidatorsUseCase {
|
|
30
25
|
private readonly itTestFileAnalyzerPort: ItTestFileAnalyzerPort;
|
|
31
26
|
private readonly sourceFileTextScannerPort: SourceFileTextScannerPort;
|
|
32
|
-
private readonly cliCommandRegistryPort: CliCommandRegistryPort | undefined;
|
|
33
|
-
private readonly e2eTestFileRegistryPort: E2eTestFileRegistryPort | undefined;
|
|
34
27
|
private readonly mapper: ValidationResultContractMapper;
|
|
35
28
|
private readonly itTestMockDetectionService = new ItTestMockDetectionService();
|
|
36
29
|
private readonly stubCommentDetectionService = new StubCommentDetectionService();
|
|
37
|
-
private readonly cliE2eTestExistenceService = new CliE2eTestExistenceService();
|
|
38
30
|
|
|
39
31
|
constructor(deps: RunL1ValidatorsUseCaseDeps) {
|
|
40
32
|
this.itTestFileAnalyzerPort = deps.itTestFileAnalyzerPort;
|
|
41
33
|
this.sourceFileTextScannerPort = deps.sourceFileTextScannerPort;
|
|
42
|
-
this.cliCommandRegistryPort = deps.cliCommandRegistryPort;
|
|
43
|
-
this.e2eTestFileRegistryPort = deps.e2eTestFileRegistryPort;
|
|
44
34
|
this.mapper = deps.contractMapper;
|
|
45
35
|
}
|
|
46
36
|
|
|
@@ -49,7 +39,6 @@ export class RunL1ValidatorsUseCase {
|
|
|
49
39
|
|
|
50
40
|
results.push(await this.runL1017(input));
|
|
51
41
|
results.push(await this.runL1018(input));
|
|
52
|
-
results.push(await this.runL2013());
|
|
53
42
|
|
|
54
43
|
return this.mapper.toContracts(results);
|
|
55
44
|
}
|
|
@@ -115,38 +104,6 @@ export class RunL1ValidatorsUseCase {
|
|
|
115
104
|
}
|
|
116
105
|
}
|
|
117
106
|
|
|
118
|
-
/** H08-09: L2-013 CLIコマンドE2Eテスト存在チェック (Should) */
|
|
119
|
-
private async runL2013(): Promise<ValidationResult> {
|
|
120
|
-
const validatorId = ValidatorId.create('L2-013');
|
|
121
|
-
if (!this.cliCommandRegistryPort || !this.e2eTestFileRegistryPort) {
|
|
122
|
-
return ValidationResult.skip(validatorId);
|
|
123
|
-
}
|
|
124
|
-
const start = Date.now();
|
|
125
|
-
try {
|
|
126
|
-
const commands = await this.cliCommandRegistryPort.getRegisteredCommands();
|
|
127
|
-
const e2eFiles = await this.e2eTestFileRegistryPort.getE2eTestFiles();
|
|
128
|
-
const report = this.cliE2eTestExistenceService.check(commands, e2eFiles);
|
|
129
|
-
const durationMs = Date.now() - start;
|
|
130
|
-
if (report.hasViolations()) {
|
|
131
|
-
const errors = report.toMessages().map((msg) => ({
|
|
132
|
-
code: { value: 'L2-013', toString: () => 'L2-013' },
|
|
133
|
-
severity: { value: 'error', toString: () => 'error' },
|
|
134
|
-
message: msg,
|
|
135
|
-
suggestion: 'Add an E2E test for each registered CLI command in cli-harness.test.ts.',
|
|
136
|
-
}));
|
|
137
|
-
return ValidationResult.fail(validatorId, errors, durationMs);
|
|
138
|
-
}
|
|
139
|
-
return ValidationResult.pass(validatorId, durationMs);
|
|
140
|
-
} catch (err) {
|
|
141
|
-
const durationMs = Date.now() - start;
|
|
142
|
-
return ValidationResult.fail(validatorId, [{
|
|
143
|
-
code: { value: 'L2-013', toString: () => 'L2-013' },
|
|
144
|
-
severity: { value: 'error', toString: () => 'error' },
|
|
145
|
-
message: `L2-013 execution failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
146
|
-
suggestion: '',
|
|
147
|
-
}], durationMs);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
107
|
}
|
|
151
108
|
|
|
152
|
-
// @story-id H08-07
|
|
109
|
+
// @story-id H08-07
|
|
@@ -16,6 +16,9 @@ import type { ValidatorConfigPort } from '../../domain/ports/validator-config-po
|
|
|
16
16
|
import type { PhaseGatePolicyPort } from '../../domain/ports/phase-gate-policy-port.js';
|
|
17
17
|
import type { MetadataPolicyPort } from '../../domain/ports/metadata-policy-port.js';
|
|
18
18
|
import type { TestQualityAnalyzerPort } from '../../domain/ports/test-quality-analyzer-port.js';
|
|
19
|
+
import type { CliCommandRegistryPort } from '../../domain/ports/cli-command-registry-port.js';
|
|
20
|
+
import type { E2eTestFileRegistryPort } from '../../domain/ports/e2e-test-file-registry-port.js';
|
|
21
|
+
import { CliE2eTestExistenceService } from '../../domain/services/cli-e2e-test-existence-service.js';
|
|
19
22
|
|
|
20
23
|
export interface RunL2ValidatorsUseCaseDeps {
|
|
21
24
|
validatorRegistry: ValidatorRegistry;
|
|
@@ -25,6 +28,8 @@ export interface RunL2ValidatorsUseCaseDeps {
|
|
|
25
28
|
phaseGatePolicyPort?: PhaseGatePolicyPort;
|
|
26
29
|
metadataPolicyPort?: MetadataPolicyPort;
|
|
27
30
|
testQualityAnalyzerPort?: TestQualityAnalyzerPort;
|
|
31
|
+
cliCommandRegistryPort?: CliCommandRegistryPort;
|
|
32
|
+
e2eTestFileRegistryPort?: E2eTestFileRegistryPort;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
export class RunL2ValidatorsUseCase {
|
|
@@ -35,6 +40,9 @@ export class RunL2ValidatorsUseCase {
|
|
|
35
40
|
private readonly phaseGatePolicyPort?: PhaseGatePolicyPort;
|
|
36
41
|
private readonly metadataPolicyPort?: MetadataPolicyPort;
|
|
37
42
|
private readonly testQualityAnalyzerPort?: TestQualityAnalyzerPort;
|
|
43
|
+
private readonly cliCommandRegistryPort?: CliCommandRegistryPort;
|
|
44
|
+
private readonly e2eTestFileRegistryPort?: E2eTestFileRegistryPort;
|
|
45
|
+
private readonly cliE2eTestExistenceService = new CliE2eTestExistenceService();
|
|
38
46
|
|
|
39
47
|
constructor(deps: RunL2ValidatorsUseCaseDeps) {
|
|
40
48
|
this.registry = deps.validatorRegistry;
|
|
@@ -44,6 +52,8 @@ export class RunL2ValidatorsUseCase {
|
|
|
44
52
|
this.phaseGatePolicyPort = deps.phaseGatePolicyPort;
|
|
45
53
|
this.metadataPolicyPort = deps.metadataPolicyPort;
|
|
46
54
|
this.testQualityAnalyzerPort = deps.testQualityAnalyzerPort;
|
|
55
|
+
this.cliCommandRegistryPort = deps.cliCommandRegistryPort;
|
|
56
|
+
this.e2eTestFileRegistryPort = deps.e2eTestFileRegistryPort;
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
async execute(input: RunL2ValidatorsInput): Promise<readonly ValidationResultContract[]> {
|
|
@@ -122,6 +132,26 @@ export class RunL2ValidatorsUseCase {
|
|
|
122
132
|
}
|
|
123
133
|
}
|
|
124
134
|
|
|
135
|
+
if (this.cliCommandRegistryPort && this.e2eTestFileRegistryPort) {
|
|
136
|
+
const l2013Result = overrideMap.get('L2-013');
|
|
137
|
+
if (l2013Result && !l2013Result.skipped) {
|
|
138
|
+
const commands = await this.cliCommandRegistryPort.getRegisteredCommands();
|
|
139
|
+
const e2eFiles = await this.e2eTestFileRegistryPort.getE2eTestFiles();
|
|
140
|
+
const report = this.cliE2eTestExistenceService.check(commands, e2eFiles);
|
|
141
|
+
if (report.hasViolations()) {
|
|
142
|
+
const errors = report.toMessages().map((msg) => ({
|
|
143
|
+
code: { value: 'L2-013', toString: () => 'L2-013' },
|
|
144
|
+
severity: { value: 'error', toString: () => 'error' },
|
|
145
|
+
message: msg,
|
|
146
|
+
suggestion: 'Add an E2E test for each registered CLI command in cli-harness.test.ts.',
|
|
147
|
+
}));
|
|
148
|
+
overrideMap.set('L2-013', ValidationResult.fail(ValidatorId.create('L2-013'), errors, 0));
|
|
149
|
+
} else {
|
|
150
|
+
overrideMap.set('L2-013', ValidationResult.pass(ValidatorId.create('L2-013'), 0));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
125
155
|
const finalResults = definitions.map(
|
|
126
156
|
(definition) => overrideMap.get(definition.validatorId.value) ?? ValidationResult.skip(definition.validatorId),
|
|
127
157
|
);
|
|
@@ -109,7 +109,7 @@ export class RunL4ValidatorsUseCase {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
if (!layerConfig.enabled && !input.forceLayerEnabled) {
|
|
112
|
-
return
|
|
112
|
+
return this.mapper.toContracts(definitions.map((definition) => ValidationResult.skip(definition.validatorId)));
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
const effectiveLayerConfig = input.forceLayerEnabled && !layerConfig.enabled
|
|
@@ -45,7 +45,7 @@ import { join } from 'node:path';
|
|
|
45
45
|
const DEFAULT_CONFIG = {
|
|
46
46
|
preset: 'standard' as const,
|
|
47
47
|
layers: {
|
|
48
|
-
L2: { enabled: true, validators: ['L2-001', 'L2-002', 'L2-003'] },
|
|
48
|
+
L2: { enabled: true, validators: ['L2-001', 'L2-002', 'L2-003', 'L2-013'] },
|
|
49
49
|
L3: { enabled: true, validators: ['L3-001', 'L3-002', 'L3-003', 'L3-004'], coverageThreshold: 90, bundleSizeLimit: 512000 },
|
|
50
50
|
L4: { enabled: true, validators: ['L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005'] },
|
|
51
51
|
},
|
|
@@ -77,6 +77,7 @@ function buildDefaultRegistry(): ValidatorRegistry {
|
|
|
77
77
|
createDef('L2-001', 'L2', 'always', 'PhaseGatePolicyPort'),
|
|
78
78
|
createDef('L2-002', 'L2', 'always', 'MetadataPolicyPort'),
|
|
79
79
|
createDef('L2-003', 'L2', 'always'),
|
|
80
|
+
createDef('L2-013', 'L2', 'always', 'CliE2eTestExistenceService'),
|
|
80
81
|
createDef('L3-001', 'L3', 'always'),
|
|
81
82
|
createDef('L3-002', 'L3', 'strictOnly'),
|
|
82
83
|
createDef('L3-003', 'L3', 'always'),
|
|
@@ -123,6 +124,18 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
123
124
|
const securityScannerPort = new FileSystemSecurityPatternScannerAdapter();
|
|
124
125
|
const performanceScannerPort = new AstPerformanceScannerAdapter();
|
|
125
126
|
|
|
127
|
+
const docsRoot = join(process.cwd(), 'docs/product/construction');
|
|
128
|
+
const cwd = process.cwd();
|
|
129
|
+
const e2eTestFileRegistryPort = new E2eTestFileRegistryAdapter({ e2eTestRoot: join(cwd, 'scripts/harness/__tests__/e2e') });
|
|
130
|
+
const cliCommandRegistryPort = new CliCommandRegistryAdapter({
|
|
131
|
+
commands: [
|
|
132
|
+
'validate', 'lint', 'ci-check',
|
|
133
|
+
'phasegate:check-ready', 'phasegate:check-phase', 'phasegate:ci-check',
|
|
134
|
+
'phasegate:detect-drift', 'phasegate:lint', 'phasegate:complete-check',
|
|
135
|
+
'phasegate:impact-analysis', 'phasegate:status',
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
|
|
126
139
|
const runL2ValidatorsUseCase = new RunL2ValidatorsUseCase({
|
|
127
140
|
validatorRegistry: registry,
|
|
128
141
|
validatorExecutionService: executionService,
|
|
@@ -131,6 +144,8 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
131
144
|
phaseGatePolicyPort,
|
|
132
145
|
metadataPolicyPort,
|
|
133
146
|
testQualityAnalyzerPort,
|
|
147
|
+
e2eTestFileRegistryPort,
|
|
148
|
+
cliCommandRegistryPort,
|
|
134
149
|
});
|
|
135
150
|
|
|
136
151
|
const runL3ValidatorsUseCase = new RunL3ValidatorsUseCase({
|
|
@@ -143,7 +158,6 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
143
158
|
performanceScannerPort,
|
|
144
159
|
});
|
|
145
160
|
|
|
146
|
-
const docsRoot = join(process.cwd(), 'docs/product/construction');
|
|
147
161
|
const markdownDesignDocumentPort = new MarkdownDesignDocumentAdapter(docsRoot);
|
|
148
162
|
const sourceCodeAnalyzerAdapter = new BiomeAstSourceCodeAnalyzerAdapter();
|
|
149
163
|
const adrReferencePort = new AdrFoundationReferenceAdapter();
|
|
@@ -182,26 +196,15 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
182
196
|
});
|
|
183
197
|
|
|
184
198
|
const aggregateValidationResultsUseCase = new AggregateValidationResultsUseCase();
|
|
185
|
-
const KNOWN_CLI_COMMANDS = [
|
|
186
|
-
'validate', 'lint', 'ci-check', 'detect-drift',
|
|
187
|
-
'phasegate:check-ready', 'phasegate:check-phase', 'phasegate:ci-check',
|
|
188
|
-
'phasegate:detect-drift', 'phasegate:lint', 'phasegate:complete-check',
|
|
189
|
-
'phasegate:impact-analysis', 'phasegate:status',
|
|
190
|
-
];
|
|
191
|
-
const cwd = process.cwd();
|
|
192
199
|
const itTestFileAnalyzerPort = new ItTestFileAnalyzerAdapter({
|
|
193
200
|
itTestRoot: join(cwd, 'scripts/harness/__tests__/integration'),
|
|
194
201
|
// アダプター自体のテストファイルは vi.mock() を使用しているため誤検知防止で除外
|
|
195
202
|
excludePattern: /it-test-file-analyzer-adapter\.test\.ts$/,
|
|
196
203
|
});
|
|
197
204
|
const sourceFileTextScannerPort = new SourceFileTextScannerAdapter({ sourceRoot: join(cwd, 'scripts/harness') });
|
|
198
|
-
const e2eTestFileRegistryPort = new E2eTestFileRegistryAdapter({ e2eTestRoot: join(cwd, 'scripts/harness/__tests__/e2e') });
|
|
199
|
-
const cliCommandRegistryPort = new CliCommandRegistryAdapter({ commands: KNOWN_CLI_COMMANDS });
|
|
200
205
|
const runL1ValidatorsUseCase = new RunL1ValidatorsUseCase({
|
|
201
206
|
itTestFileAnalyzerPort,
|
|
202
207
|
sourceFileTextScannerPort,
|
|
203
|
-
e2eTestFileRegistryPort,
|
|
204
|
-
cliCommandRegistryPort,
|
|
205
208
|
contractMapper,
|
|
206
209
|
});
|
|
207
210
|
|
|
@@ -5,15 +5,46 @@ import { CliE2eTestCoverageReport, type CliCommandCoverageEntry } from '../value
|
|
|
5
5
|
|
|
6
6
|
export class CliE2eTestExistenceService {
|
|
7
7
|
check(commands: readonly string[], e2eTestFiles: readonly string[]): CliE2eTestCoverageReport {
|
|
8
|
+
if (e2eTestFiles.length === 0) {
|
|
9
|
+
return CliE2eTestCoverageReport.create(
|
|
10
|
+
commands.map((commandName) => ({
|
|
11
|
+
commandName,
|
|
12
|
+
hasE2eTest: false,
|
|
13
|
+
status: 'limitation',
|
|
14
|
+
evidence: 'No CLI E2E test suite found in this project.',
|
|
15
|
+
})),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
const e2eContent = e2eTestFiles.join('\n').toLowerCase();
|
|
9
20
|
|
|
10
|
-
const entries: CliCommandCoverageEntry[] = commands.map((commandName) =>
|
|
11
|
-
commandName,
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
const entries: CliCommandCoverageEntry[] = commands.map((commandName) => {
|
|
22
|
+
const evidence = this.findCoverageEvidence(commandName, e2eContent);
|
|
23
|
+
return {
|
|
24
|
+
commandName,
|
|
25
|
+
hasE2eTest: evidence !== null,
|
|
26
|
+
status: evidence === null ? 'missing' : 'covered',
|
|
27
|
+
evidence: evidence ?? undefined,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
14
30
|
|
|
15
31
|
return CliE2eTestCoverageReport.create(entries);
|
|
16
32
|
}
|
|
33
|
+
|
|
34
|
+
private findCoverageEvidence(commandName: string, e2eContent: string): string | null {
|
|
35
|
+
const escaped = commandName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
36
|
+
const invocationPatterns = [
|
|
37
|
+
new RegExp(`\\brun\\(\\s*['"\`]${escaped}['"\`]`),
|
|
38
|
+
new RegExp(`\\brunin(?:cwd)?\\([^\\n]*['"\`]${escaped}['"\`]`),
|
|
39
|
+
new RegExp(`unknown command:\\s*${escaped}`),
|
|
40
|
+
new RegExp(`usage:\\s*phasegate\\s+${escaped}`),
|
|
41
|
+
];
|
|
42
|
+
if (invocationPatterns.some((pattern) => pattern.test(e2eContent))) {
|
|
43
|
+
return commandName;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return e2eContent.includes(commandName.toLowerCase()) ? commandName : null;
|
|
47
|
+
}
|
|
17
48
|
}
|
|
18
49
|
|
|
19
|
-
// @story-id H08-07
|
|
50
|
+
// @story-id H08-07
|
package/scripts/harness/validator-system/domain/value-objects/cli-e2e-test-coverage-report.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
export interface CliCommandCoverageEntry {
|
|
10
10
|
readonly commandName: string;
|
|
11
11
|
readonly hasE2eTest: boolean;
|
|
12
|
+
readonly status?: 'covered' | 'missing' | 'limitation';
|
|
13
|
+
readonly evidence?: string;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export class CliE2eTestCoverageReport {
|
|
@@ -28,7 +30,11 @@ export class CliE2eTestCoverageReport {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
uncoveredCommands(): readonly CliCommandCoverageEntry[] {
|
|
31
|
-
return this.entries.filter((e) => !e.hasE2eTest);
|
|
33
|
+
return this.entries.filter((e) => !e.hasE2eTest && e.status !== 'limitation');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
limitations(): readonly CliCommandCoverageEntry[] {
|
|
37
|
+
return this.entries.filter((e) => e.status === 'limitation');
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
hasViolations(): boolean {
|
|
@@ -42,4 +48,4 @@ export class CliE2eTestCoverageReport {
|
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
// @story-id H08-07
|
|
51
|
+
// @story-id H08-07
|
package/scripts/harness/validator-system/infrastructure/adapters/e2e-test-file-registry-adapter.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* E2Eテストファイルのパス一覧を提供する。
|
|
7
7
|
*/
|
|
8
8
|
import type { E2eTestFileRegistryPort } from '../../domain/ports/e2e-test-file-registry-port.js';
|
|
9
|
-
import { readdir } from 'node:fs/promises';
|
|
9
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
|
|
12
12
|
export interface E2eTestFileRegistryAdapterOptions {
|
|
@@ -22,7 +22,16 @@ export class E2eTestFileRegistryAdapter implements E2eTestFileRegistryPort {
|
|
|
22
22
|
|
|
23
23
|
async getE2eTestFiles(): Promise<readonly string[]> {
|
|
24
24
|
if (!this.e2eTestRoot) return [];
|
|
25
|
-
|
|
25
|
+
const filePaths = await this.findFiles(this.e2eTestRoot, /\.test\.ts$/);
|
|
26
|
+
const contents: string[] = [];
|
|
27
|
+
for (const filePath of filePaths) {
|
|
28
|
+
try {
|
|
29
|
+
contents.push(`${filePath}\n${await readFile(filePath, 'utf-8')}`);
|
|
30
|
+
} catch {
|
|
31
|
+
contents.push(filePath);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return contents;
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
private async findFiles(dir: string, pattern: RegExp): Promise<string[]> {
|
|
@@ -45,4 +54,4 @@ export class E2eTestFileRegistryAdapter implements E2eTestFileRegistryPort {
|
|
|
45
54
|
}
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
// @story-id H08-07
|
|
57
|
+
// @story-id H08-07
|
|
@@ -38,7 +38,7 @@ export class HarnessConfigValidatorConfigAdapter implements ValidatorConfigPort
|
|
|
38
38
|
const layerData = this.config.layers?.[layer] ?? {};
|
|
39
39
|
|
|
40
40
|
const defaultValidators: Record<string, string[]> = {
|
|
41
|
-
L2: ['L2-001', 'L2-002', 'L2-003'],
|
|
41
|
+
L2: ['L2-001', 'L2-002', 'L2-003', 'L2-013'],
|
|
42
42
|
L3: ['L3-001', 'L3-002', 'L3-003', 'L3-004'],
|
|
43
43
|
L4: ['L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005'],
|
|
44
44
|
};
|