phasegate 0.161.0 → 0.163.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 +16 -0
- package/docs/contracts/requirement-test-matrix.schema.json +1 -1
- package/package.json +6 -3
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +91 -23
- package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +4 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/ci-governance-baseline-grandfather-adapter.ts +25 -2
- package/scripts/harness/agent-integration/infrastructure/adapters/phase-gate-query-adapter.ts +17 -2
- package/scripts/harness/biome-ast-engine/domain/value-objects/import-graph.ts +28 -1
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +1 -0
- package/scripts/harness/config-foundation/domain/harness-config.ts +1 -0
- package/scripts/harness/config-foundation/domain/value-objects/l3-config.ts +4 -0
- package/scripts/harness/config-foundation/domain/value-objects/layers-config.ts +1 -1
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +2 -1
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +2 -1
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +2 -1
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +14 -9
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +14 -9
- package/scripts/harness/harness-api/infrastructure/adapters/phase-dependency-model-query-adapter.ts +18 -4
- package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts +27 -4
- package/scripts/harness/installation/application/ports/model-delegation-port.ts +10 -0
- package/scripts/harness/installation/application/usecases/run-install.ts +56 -7
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +12 -7
- package/scripts/harness/installation/composition-root.ts +4 -2
- package/scripts/harness/installation/infrastructure/adapters/skill-deployer-model-delegation-adapter.ts +16 -0
- package/scripts/harness/installation/presentation/cli/install-handler.ts +2 -1
- package/scripts/harness/main.ts +68 -12
- package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +9 -7
- package/scripts/harness/nyquist-validation/domain/services/matrix-validation-service.ts +27 -2
- package/scripts/harness/nyquist-validation/domain/services/requirement-intent-coverage-service.ts +4 -4
- package/scripts/harness/nyquist-validation/domain/value-objects/intent-coverage.ts +33 -0
- package/scripts/harness/nyquist-validation/infrastructure/adapters/markdown-requirement-source-adapter.ts +3 -1
- package/scripts/harness/nyquist-validation/infrastructure/adapters/type-script-test-reference-source-adapter.ts +4 -1
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +8 -3
- package/scripts/harness/quick-mode/composition-root.ts +3 -0
- package/scripts/harness/quick-mode/infrastructure/adapters/validator-system-quick-mode-execution-adapter.ts +18 -0
- package/scripts/harness/quick-mode/presentation/handlers/ci-check-quick-mode-handler.ts +51 -5
- package/scripts/harness/skill-quality/infrastructure/adapters/l1-biome-validator-adapter.ts +12 -2
- package/scripts/harness/skill-quality/infrastructure/adapters/l2-validator-system-adapter.ts +12 -2
- package/scripts/harness/traceability-model/application/usecases/apply-work-item-status-usecase.ts +6 -1
- package/scripts/harness/traceability-model/infrastructure/parsers/story-catalog-parser.ts +28 -9
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +65 -27
- package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +6 -7
- package/scripts/harness/validator-system/composition-root.ts +8 -1
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +16 -6
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-work-item-reflection-adapter.ts +10 -1
- package/scripts/harness/validator-system/infrastructure/adapters/import-graph-source-analysis-adapter.ts +35 -3
- package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +86 -14
- package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +29 -3
- package/templates/.claude/scripts/deny-check.sh +73 -12
|
@@ -50,11 +50,16 @@ export class ClassifyChangeCategoryUseCase {
|
|
|
50
50
|
const targetChanges = new Map((input.targetChanges ?? []).map((change) => [change.filePath, change]));
|
|
51
51
|
const changedFiles = input.paths.map((p) => {
|
|
52
52
|
const targetChange = targetChanges.get(p);
|
|
53
|
+
const beforeContent = targetChange?.beforeContent ?? null;
|
|
54
|
+
const afterContent = targetChange?.afterContent ?? null;
|
|
53
55
|
return ChangedFile.create({
|
|
54
56
|
filePath: p,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
// 変更前の内容が無く変更後の内容がある場合は新規作成 (CREATE) とみなす。
|
|
58
|
+
// 以前は無条件で MODIFY 固定だったため、新規 domain/ ファイルが
|
|
59
|
+
// NEW_DOMAIN 判定を回避して quick mode をすり抜けていた。
|
|
60
|
+
changeKind: beforeContent === null && afterContent !== null ? 'CREATE' : 'MODIFY',
|
|
61
|
+
beforeContent,
|
|
62
|
+
afterContent,
|
|
58
63
|
});
|
|
59
64
|
});
|
|
60
65
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { GitDiffChangedFilesAdapter } from './infrastructure/adapters/git-diff-changed-files-adapter.js';
|
|
9
9
|
import { HarnessConfigQuickModeConfigAdapter } from './infrastructure/adapters/harness-config-quick-mode-config-adapter.js';
|
|
10
10
|
import { ValidatorSystemValidatorIdRegistryAdapter } from './infrastructure/adapters/validator-system-validator-id-registry-adapter.js';
|
|
11
|
+
import { ValidatorSystemQuickModeExecutionAdapter } from './infrastructure/adapters/validator-system-quick-mode-execution-adapter.js';
|
|
11
12
|
import { QuickModeJudgmentEngine } from './domain/services/quick-mode-judgment-engine.js';
|
|
12
13
|
import { ValidatorRelaxationService } from './domain/services/validator-relaxation-service.js';
|
|
13
14
|
import { QuickModeDecisionContractMapper } from './application/mappers/quick-mode-decision-contract-mapper.js';
|
|
@@ -32,6 +33,7 @@ export function createQuickModeCompositionRoot(): QuickModeCompositionRoot {
|
|
|
32
33
|
const gitDiffAdapter = new GitDiffChangedFilesAdapter();
|
|
33
34
|
const harnessConfigAdapter = new HarnessConfigQuickModeConfigAdapter();
|
|
34
35
|
const validatorIdRegistryAdapter = new ValidatorSystemValidatorIdRegistryAdapter();
|
|
36
|
+
const validatorExecutionPort = new ValidatorSystemQuickModeExecutionAdapter();
|
|
35
37
|
|
|
36
38
|
// Domain Services
|
|
37
39
|
const judgmentEngine = new QuickModeJudgmentEngine();
|
|
@@ -64,6 +66,7 @@ export function createQuickModeCompositionRoot(): QuickModeCompositionRoot {
|
|
|
64
66
|
const executeUseCase = new ExecuteQuickCiCheckUseCase({
|
|
65
67
|
judgeUseCase,
|
|
66
68
|
buildUseCase,
|
|
69
|
+
validatorExecutionPort,
|
|
67
70
|
});
|
|
68
71
|
|
|
69
72
|
const classifyUseCase = new ClassifyChangeCategoryUseCase({
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @layer infrastructure
|
|
2
|
+
// @unit quick-mode
|
|
3
|
+
|
|
4
|
+
import type { ValidatorExecutionPort } from '../../application/ports/validator-execution-port.js';
|
|
5
|
+
import type { ValidatorRelaxationProfileContract } from '../../application/dto/validator-relaxation-profile-contract.js';
|
|
6
|
+
|
|
7
|
+
export class ValidatorSystemQuickModeExecutionAdapter implements ValidatorExecutionPort {
|
|
8
|
+
async executeWithProfile(profile: ValidatorRelaxationProfileContract): Promise<void> {
|
|
9
|
+
const { createValidatorSystemModule } = await import('../../../validator-system/composition-root.js');
|
|
10
|
+
const module = createValidatorSystemModule();
|
|
11
|
+
await module.runQuickModeUseCase.execute({
|
|
12
|
+
relaxationProfile: profile,
|
|
13
|
+
targetPaths: [],
|
|
14
|
+
unitName: '',
|
|
15
|
+
currentPhase: '',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -5,12 +5,56 @@
|
|
|
5
5
|
* phasegate:ci-check --quick フラグを受け取り、ExecuteQuickCiCheckUseCase を呼ぶハンドラー
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import * as childProcess from 'node:child_process';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
8
10
|
import { HumanQuickModeFormatter } from '../formatters/human-quick-mode-formatter.js';
|
|
9
11
|
import { AgentQuickModeFormatter } from '../formatters/agent-quick-mode-formatter.js';
|
|
10
12
|
import { JsonQuickModeFormatter } from '../formatters/json-quick-mode-formatter.js';
|
|
11
13
|
import type { ExecuteQuickCiCheckUseCase } from '../../application/usecases/execute-quick-ci-check-usecase.js';
|
|
12
14
|
import type { QuickModeRenderOptions } from '../dto/quick-mode-render-options.js';
|
|
13
15
|
|
|
16
|
+
/**
|
|
17
|
+
* `--files` で列挙されたパスの変更種別 (CREATE/MODIFY) を推定する。
|
|
18
|
+
*
|
|
19
|
+
* WI: 以前は無条件で MODIFY 固定だったため、新規 domain/ ファイルが
|
|
20
|
+
* NEW_DOMAIN 判定を回避して quick mode をすり抜けていた。git のステージ状態を
|
|
21
|
+
* 参照し、追加 (A) なら CREATE、それ以外は MODIFY とする。git 情報が取れない
|
|
22
|
+
* 場合は「ファイルが未追跡(作業ツリーにのみ存在しコミット履歴に無い)」を
|
|
23
|
+
* CREATE のヒューリスティックとして用いる。
|
|
24
|
+
*/
|
|
25
|
+
function resolveChangeKind(filePath: string): 'CREATE' | 'MODIFY' {
|
|
26
|
+
const gitStatus = readGitStatus(filePath);
|
|
27
|
+
if (gitStatus === 'A') return 'CREATE';
|
|
28
|
+
if (gitStatus !== null) return 'MODIFY';
|
|
29
|
+
return isUntrackedNewFile(filePath) ? 'CREATE' : 'MODIFY';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readGitStatus(filePath: string): string | null {
|
|
33
|
+
try {
|
|
34
|
+
const output = childProcess.execSync(
|
|
35
|
+
`git diff --name-status --cached -- ${JSON.stringify(filePath)}`,
|
|
36
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] },
|
|
37
|
+
) as string;
|
|
38
|
+
const line = output.trim().split('\n').find((l) => l.trim().length > 0);
|
|
39
|
+
return line ? (line.split('\t')[0]?.trim() ?? null) : null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isUntrackedNewFile(filePath: string): boolean {
|
|
46
|
+
if (!existsSync(filePath)) return false;
|
|
47
|
+
try {
|
|
48
|
+
childProcess.execSync(
|
|
49
|
+
`git ls-files --error-unmatch -- ${JSON.stringify(filePath)}`,
|
|
50
|
+
{ stdio: ['pipe', 'pipe', 'pipe'] },
|
|
51
|
+
);
|
|
52
|
+
return false; // tracked → 既存ファイル
|
|
53
|
+
} catch {
|
|
54
|
+
return true; // 未追跡 → 新規ファイル
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
14
58
|
export interface CiCheckQuickModeHandlerDeps {
|
|
15
59
|
useCase: Pick<ExecuteQuickCiCheckUseCase, 'execute'>;
|
|
16
60
|
}
|
|
@@ -25,13 +69,15 @@ export class CiCheckQuickModeHandler {
|
|
|
25
69
|
async handle(options: QuickModeRenderOptions): Promise<void> {
|
|
26
70
|
const { files, dryRun = false, format = 'human', failOnReject = false } = options;
|
|
27
71
|
|
|
28
|
-
// --files 解析: カンマ区切りのファイルパスを
|
|
72
|
+
// --files 解析: カンマ区切りのファイルパスを git ステージ状態から
|
|
73
|
+
// CREATE/MODIFY へ分類する(新規 domain/ ファイルの NEW_DOMAIN 判定のため)
|
|
29
74
|
let changedFiles: { filePath: string; changeKind: string }[] | undefined;
|
|
30
75
|
if (files) {
|
|
31
|
-
changedFiles = files
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
76
|
+
changedFiles = files
|
|
77
|
+
.split(',')
|
|
78
|
+
.map((p) => p.trim())
|
|
79
|
+
.filter((p) => p.length > 0)
|
|
80
|
+
.map((filePath) => ({ filePath, changeKind: resolveChangeKind(filePath) }));
|
|
35
81
|
}
|
|
36
82
|
|
|
37
83
|
let decision: Awaited<ReturnType<typeof this.useCase.execute>>;
|
|
@@ -18,8 +18,18 @@ export class L1BiomeValidatorAdapter implements L1ValidatorPort {
|
|
|
18
18
|
message: violation.message,
|
|
19
19
|
location: `${violation.filePath.toString()}:${violation.line}:${violation.column}`,
|
|
20
20
|
}));
|
|
21
|
-
} catch (
|
|
22
|
-
|
|
21
|
+
} catch (err) {
|
|
22
|
+
// Fail-closed: a validator failure must NOT be treated as "合格".
|
|
23
|
+
// Surface the error as a blocking violation so the commit gate stays closed.
|
|
24
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
25
|
+
console.error(`[skill-quality] L1 Biome validator failed: ${message}`);
|
|
26
|
+
return [
|
|
27
|
+
{
|
|
28
|
+
ruleId: 'L1-VALIDATOR-ERROR',
|
|
29
|
+
message: `L1 Biome validator failed to run; treating as NOT compliant (fail-closed): ${message}`,
|
|
30
|
+
location: '',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
23
33
|
}
|
|
24
34
|
}
|
|
25
35
|
}
|
package/scripts/harness/skill-quality/infrastructure/adapters/l2-validator-system-adapter.ts
CHANGED
|
@@ -23,8 +23,18 @@ export class L2ValidatorSystemAdapter implements L2ValidatorPort {
|
|
|
23
23
|
message: error.message,
|
|
24
24
|
location: '',
|
|
25
25
|
}));
|
|
26
|
-
} catch {
|
|
27
|
-
|
|
26
|
+
} catch (err) {
|
|
27
|
+
// Fail-closed: a validator failure must NOT be treated as "合格".
|
|
28
|
+
// Surface the error as a blocking violation so the commit gate stays closed.
|
|
29
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30
|
+
console.error(`[skill-quality] L2 validator-system failed: ${message}`);
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
ruleId: 'L2-VALIDATOR-ERROR',
|
|
34
|
+
message: `L2 validator-system failed to run; treating as NOT compliant (fail-closed): ${message}`,
|
|
35
|
+
location: '',
|
|
36
|
+
},
|
|
37
|
+
];
|
|
28
38
|
}
|
|
29
39
|
}
|
|
30
40
|
}
|
package/scripts/harness/traceability-model/application/usecases/apply-work-item-status-usecase.ts
CHANGED
|
@@ -42,12 +42,17 @@ export class ApplyWorkItemStatusUseCase {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// `completed` is a manually promoted terminal state that the automatic
|
|
46
|
+
// derivation never emits (derivation tops out at `tested`). It must rank
|
|
47
|
+
// strictly above `tested` here so that deriving `tested` for an already
|
|
48
|
+
// `completed` work item is detected as a downgrade and blocked, preventing a
|
|
49
|
+
// `completed` -> `tested` regression on `work-items --apply`.
|
|
45
50
|
const STATUS_ORDER: Record<WorkItemStatus, number> = {
|
|
46
51
|
drafted: 0,
|
|
47
52
|
reflected: 1,
|
|
48
53
|
implemented: 2,
|
|
49
54
|
tested: 3,
|
|
50
|
-
completed:
|
|
55
|
+
completed: 4,
|
|
51
56
|
};
|
|
52
57
|
|
|
53
58
|
function statusOrder(status: WorkItemStatus): number {
|
|
@@ -12,9 +12,10 @@ export interface ParsedStoryCatalog {
|
|
|
12
12
|
|
|
13
13
|
const STORY_ID_LINE_PATTERN = /\bH(?:F\d+|[0-9]{2})-[0-9]{2}\b/g;
|
|
14
14
|
const TABLE_ALIAS_PATTERN =
|
|
15
|
-
/\|\s*(H[0-9]{2}-[0-9]{2})\s*\|.*?\|\s*(US-[0-9]{3})\s*\|/g;
|
|
15
|
+
/\|\s*(H(?:F\d+|[0-9]{2})-[0-9]{2})\s*\|.*?\|\s*(US-[0-9]{3})\s*\|/g;
|
|
16
|
+
// 旧US ラベルは Markdown 強調(**旧US**:)で囲まれることがあるため `\**` を許容する。
|
|
16
17
|
const INLINE_ALIAS_PATTERN =
|
|
17
|
-
/(H[0-9]{2}-[0-9]{2}).*?旧US
|
|
18
|
+
/(H(?:F\d+|[0-9]{2})-[0-9]{2}).*?旧US\**\s*[::]\s*(US-[0-9]{3})/g;
|
|
18
19
|
|
|
19
20
|
function execAll(pattern: RegExp, text: string): RegExpExecArray[] {
|
|
20
21
|
const results: RegExpExecArray[] = [];
|
|
@@ -56,14 +57,32 @@ export function parseStoryCatalog(content: string): ParsedStoryCatalog {
|
|
|
56
57
|
aliasMap.set(inlineMatches[i][2], inlineMatches[i][1]);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
//
|
|
60
|
+
// 見出しスコープ形式:
|
|
61
|
+
// ### H01-01: タイトル
|
|
62
|
+
//
|
|
63
|
+
// **旧US**: US-036
|
|
64
|
+
// のように、`### HXX-XX:` 見出しの後、次の見出しが現れるまでの範囲に `旧US: US-xxx`
|
|
65
|
+
// が出現するパターンに対応する。旧実装は直後行(lines[i+1])のみを見ていたため、
|
|
66
|
+
// 実際の user_stories.md(見出しと旧US行の間に空行や Epic 行が挟まる)では alias が
|
|
67
|
+
// 一切抽出されず、レガシー StoryId 解決が事実上機能していなかった。
|
|
68
|
+
const HEADING_STORY_ID = /^#{1,6}\s+(H(?:F\d+|[0-9]{2})-[0-9]{2})\b/;
|
|
69
|
+
const LEGACY_IN_LINE = /旧US\**\s*[::]\s*(US-[0-9]{3})/;
|
|
70
|
+
let currentHeadingStoryId: string | null = null;
|
|
60
71
|
for (let i = 0; i < lines.length; i++) {
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
const headingMatch = HEADING_STORY_ID.exec(lines[i]);
|
|
73
|
+
if (headingMatch) {
|
|
74
|
+
currentHeadingStoryId = headingMatch[1];
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (/^#{1,6}\s+/.test(lines[i])) {
|
|
78
|
+
// StoryId を含まない別の見出しに入ったらスコープを閉じる
|
|
79
|
+
currentHeadingStoryId = null;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (currentHeadingStoryId) {
|
|
83
|
+
const legacyMatch = LEGACY_IN_LINE.exec(lines[i]);
|
|
84
|
+
if (legacyMatch && !aliasMap.has(legacyMatch[1])) {
|
|
85
|
+
aliasMap.set(legacyMatch[1], currentHeadingStoryId);
|
|
67
86
|
}
|
|
68
87
|
}
|
|
69
88
|
}
|
|
@@ -84,38 +84,76 @@ export class RunL3ValidatorsUseCase {
|
|
|
84
84
|
const { executableDefinitions, unsupportedResults, unsupportedValidatorIds } =
|
|
85
85
|
this.languageCapabilityService.splitDefinitions(definitions, projectLanguages);
|
|
86
86
|
|
|
87
|
-
// coverageReportPort が存在する場合、カバレッジを取得して判定
|
|
88
|
-
if (this.coverageReportPort && !unsupportedValidatorIds.has('L3-003')) {
|
|
89
|
-
const coverageData = await this.coverageReportPort.getCoverage();
|
|
90
|
-
const threshold = layerConfig.getThreshold('coverageThreshold');
|
|
91
|
-
|
|
92
|
-
// カバレッジ不足の場合は fail 結果を生成
|
|
93
|
-
if (threshold !== null && coverageData.overallCoverage < threshold) {
|
|
94
|
-
const deficit = threshold - coverageData.overallCoverage;
|
|
95
|
-
|
|
96
|
-
// 他のバリデータを実行
|
|
97
|
-
const otherDefs = executableDefinitions.filter((d) => d.validatorId.value !== 'L3-003');
|
|
98
|
-
const otherResults = this.executionService.execute(otherDefs, [layerConfig]);
|
|
99
|
-
const otherContracts = this.mapper.toContracts([...unsupportedResults, ...otherResults]);
|
|
100
|
-
|
|
101
|
-
return [
|
|
102
|
-
...otherContracts.filter((r) => r.validatorId !== 'L3-003'),
|
|
103
|
-
{
|
|
104
|
-
validatorId: 'L3-003',
|
|
105
|
-
passed: false,
|
|
106
|
-
errors: [{ code: 'L3-003', severity: 'error', message: `カバレッジ不足: 現在値 ${coverageData.overallCoverage}%、不足 ${deficit}%` , suggestion: `テストカバレッジを ${threshold}% 以上に引き上げてください` }],
|
|
107
|
-
durationMs: 0,
|
|
108
|
-
skipped: false,
|
|
109
|
-
},
|
|
110
|
-
];
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
87
|
const results = this.executionService.execute(executableDefinitions, [layerConfig]);
|
|
115
88
|
const overrideMap = new Map<string, ValidationResult>(
|
|
116
89
|
[...unsupportedResults, ...results].map((result) => [result.validatorId.value, result]),
|
|
117
90
|
);
|
|
118
91
|
|
|
92
|
+
// L3-003: カバレッジ判定(カバレッジゲートはオプトイン)
|
|
93
|
+
// - coverageThreshold 未設定 → SKIP(透過的に判定をスキップ。getCoverage() は呼ばない)
|
|
94
|
+
// - coverageThreshold 設定あり → getCoverage() を try/catch で包み FAIL-CLOSED で判定する
|
|
95
|
+
// - 閾値未満 → FAIL / 閾値以上 → PASS
|
|
96
|
+
// - レポート不在などで getCoverage() が失敗 → FAIL(合格扱いにしない)
|
|
97
|
+
// このブロックは例外を送出せず、L3-003 の per-validator 結果のみを差し替える。
|
|
98
|
+
// これにより兄弟バリデータ(L3-001/002/004 および L2/L4 バッチ)は常に通常実行される。
|
|
99
|
+
const l3003InScope =
|
|
100
|
+
!unsupportedValidatorIds.has('L3-003') &&
|
|
101
|
+
definitions.some((d) => d.validatorId.value === 'L3-003');
|
|
102
|
+
if (this.coverageReportPort && l3003InScope) {
|
|
103
|
+
const l3003Id = ValidatorId.create('L3-003');
|
|
104
|
+
const threshold = layerConfig.getThreshold('coverageThreshold');
|
|
105
|
+
|
|
106
|
+
if (threshold === null) {
|
|
107
|
+
overrideMap.set(
|
|
108
|
+
'L3-003',
|
|
109
|
+
ValidationResult.skipWithReason(
|
|
110
|
+
l3003Id,
|
|
111
|
+
'coverageThreshold が未設定のためカバレッジ判定をスキップ(カバレッジゲートはオプトイン)',
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
} else {
|
|
115
|
+
try {
|
|
116
|
+
const coverageData = await this.coverageReportPort.getCoverage();
|
|
117
|
+
if (coverageData.overallCoverage < threshold) {
|
|
118
|
+
overrideMap.set(
|
|
119
|
+
'L3-003',
|
|
120
|
+
ValidationResult.fail(
|
|
121
|
+
l3003Id,
|
|
122
|
+
[
|
|
123
|
+
{
|
|
124
|
+
code: 'L3-003',
|
|
125
|
+
severity: 'error',
|
|
126
|
+
message: `カバレッジ不足: 現在値 ${coverageData.overallCoverage}%、不足 ${threshold - coverageData.overallCoverage}%`,
|
|
127
|
+
suggestion: `テストカバレッジを ${threshold}% 以上に引き上げてください`,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
0,
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
} else {
|
|
134
|
+
overrideMap.set('L3-003', ValidationResult.pass(l3003Id, 0));
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// レポート不在などで取得失敗 → FAIL-CLOSED(例外は握りつぶし per-validator FAIL に変換)
|
|
138
|
+
overrideMap.set(
|
|
139
|
+
'L3-003',
|
|
140
|
+
ValidationResult.fail(
|
|
141
|
+
l3003Id,
|
|
142
|
+
[
|
|
143
|
+
{
|
|
144
|
+
code: 'L3-003',
|
|
145
|
+
severity: 'error',
|
|
146
|
+
message: `coverageThreshold=${threshold}% が設定されていますがカバレッジレポートが見つかりません(テストをカバレッジ付きで実行してください)`,
|
|
147
|
+
suggestion: 'vitest --coverage 等でカバレッジレポートを生成してから再実行してください',
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
0,
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
119
157
|
if (this.securityScannerPort) {
|
|
120
158
|
const l3001Result = overrideMap.get('L3-001');
|
|
121
159
|
if (l3001Result && !l3001Result.skipped) {
|
|
@@ -177,9 +177,12 @@ export class RunL4ValidatorsUseCase {
|
|
|
177
177
|
const l4002Result = overrideMap.get('L4-002');
|
|
178
178
|
if (l4002Result && !l4002Result.skipped) {
|
|
179
179
|
const report = await this.consistencyCheckService.check(input.targetUnits ? [...input.targetUnits] : undefined);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
// WI-217: 標準レイアウト (docs/inception, docs/product/construction) でも
|
|
181
|
+
// reflection を必ず実行する。以前は usesConfiguredDocumentRoots() が false を
|
|
182
|
+
// 返すため標準構成では reflection が恒久的にスキップされ、L4-002 が常に PASS
|
|
183
|
+
// していた。description が存在しない場合はアダプタ側が skipReason を返すため、
|
|
184
|
+
// 常時実行しても未整備プロジェクトを誤って失敗させることはない。
|
|
185
|
+
const reflectionResult = await this.consistencyCheckService.checkWorkItemReflection(this.pathRoots);
|
|
183
186
|
const architectureSemanticErrors = this.architectureSemanticAnalysisService
|
|
184
187
|
? await this.architectureSemanticAnalysisService.analyze()
|
|
185
188
|
: [];
|
|
@@ -289,10 +292,6 @@ export class RunL4ValidatorsUseCase {
|
|
|
289
292
|
return this.configPort.getProjectLanguages ? await this.configPort.getProjectLanguages() : ['typescript'];
|
|
290
293
|
}
|
|
291
294
|
|
|
292
|
-
private usesConfiguredDocumentRoots(): boolean {
|
|
293
|
-
return this.pathRoots.inceptionRoot !== 'docs/inception' || this.pathRoots.designRoot !== 'docs/product/construction';
|
|
294
|
-
}
|
|
295
|
-
|
|
296
295
|
private toPointerValidationHarnessErrors(output: ValidateDocPointersOutputContract): readonly ValidationResult['errors'][number][] {
|
|
297
296
|
const executionErrors = output.errors.map((error) => this.toHarnessErrorLike(
|
|
298
297
|
'L4-005',
|
|
@@ -29,6 +29,7 @@ import { FileSystemContractTraceabilityPolicyAdapter } from './infrastructure/ad
|
|
|
29
29
|
import { PhaseDependencyPhaseGatePolicyAdapter } from './infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.js';
|
|
30
30
|
import { TraceabilityMetadataPolicyAdapter } from './infrastructure/adapters/traceability-metadata-policy-adapter.js';
|
|
31
31
|
import { NyquistAcCoveragePolicyAdapter } from './infrastructure/adapters/nyquist-ac-coverage-policy-adapter.js';
|
|
32
|
+
import { JsonCoverageReportAdapter } from './infrastructure/adapters/json-coverage-report-adapter.js';
|
|
32
33
|
import { BiomeAstTestQualityAnalyzerAdapter } from './infrastructure/adapters/biome-ast-test-quality-analyzer-adapter.js';
|
|
33
34
|
import { FileSystemSecurityPatternScannerAdapter } from './infrastructure/adapters/file-system-security-pattern-scanner-adapter.js';
|
|
34
35
|
import { AstPerformanceScannerAdapter } from './infrastructure/adapters/ast-performance-scanner-adapter.js';
|
|
@@ -54,7 +55,7 @@ const DEFAULT_CONFIG = {
|
|
|
54
55
|
preset: 'standard' as const,
|
|
55
56
|
layers: {
|
|
56
57
|
L2: { enabled: true, validators: ['L2-001', 'L2-002', 'L2-003', 'L2-013', 'L2-014', 'L2-015'] },
|
|
57
|
-
L3: { enabled: true, validators: ['L3-001', 'L3-002', 'L3-003', 'L3-004'], coverageThreshold: 90, bundleSizeLimit: 512000 },
|
|
58
|
+
L3: { enabled: true, validators: ['L3-001', 'L3-002', 'L3-003', 'L3-004'], coverageThreshold: 90, bundleSizeLimit: 512000, requirementMatrixPath: '.harness/requirement-test-matrix.json' },
|
|
58
59
|
L4: { enabled: true, validators: ['L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005', 'L4-006'] },
|
|
59
60
|
},
|
|
60
61
|
paths: {
|
|
@@ -149,6 +150,11 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
149
150
|
const phaseGatePolicyPort = new PhaseDependencyPhaseGatePolicyAdapter();
|
|
150
151
|
const metadataPolicyPort = new TraceabilityMetadataPolicyAdapter();
|
|
151
152
|
const acCoveragePolicyPort = new NyquistAcCoveragePolicyAdapter();
|
|
153
|
+
// L3-003: テストカバレッジレポート(vitest --coverage の json-summary 出力)を読み取る。
|
|
154
|
+
// 未配線だと L3-003 は装飾的な pass になり 90% 閾値が形骸化するため必ず配線する。
|
|
155
|
+
const coverageReportPort = new JsonCoverageReportAdapter(
|
|
156
|
+
join(process.cwd(), 'coverage', 'coverage-summary.json'),
|
|
157
|
+
);
|
|
152
158
|
const testQualityAnalyzerPort = new BiomeAstTestQualityAnalyzerAdapter();
|
|
153
159
|
const securityScannerPort = new FileSystemSecurityPatternScannerAdapter();
|
|
154
160
|
const performanceScannerPort = new AstPerformanceScannerAdapter();
|
|
@@ -189,6 +195,7 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
189
195
|
validatorConfigPort: configPort,
|
|
190
196
|
contractMapper,
|
|
191
197
|
acCoveragePolicyPort,
|
|
198
|
+
coverageReportPort,
|
|
192
199
|
securityScannerPort,
|
|
193
200
|
performanceScannerPort,
|
|
194
201
|
});
|
|
@@ -40,12 +40,14 @@ export class FileSystemSecurityPatternScannerAdapter implements SecurityPatternS
|
|
|
40
40
|
for (const filePath of targetPaths) {
|
|
41
41
|
try {
|
|
42
42
|
const content = await readFile(filePath, 'utf-8');
|
|
43
|
-
if (content.includes(ALLOWLIST_MARKER)) {
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
43
|
const lines = content.split('\n');
|
|
47
44
|
lines.forEach((line, idx) => {
|
|
48
|
-
|
|
45
|
+
// WI-120: allowlist は行/領域スコープ。以前はファイル内のどこかに
|
|
46
|
+
// マーカーが 1 つでもあればファイル全体をスキップしていたため、同一
|
|
47
|
+
// ファイル内の本物の秘密情報も検出漏れしていた。マーカーは当該行、
|
|
48
|
+
// または直前行(次行抑止コメント)にある場合のみ有効とする。
|
|
49
|
+
const previousLine = idx > 0 ? lines[idx - 1] : undefined;
|
|
50
|
+
if (isAllowlisted(line, previousLine)) {
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
51
53
|
for (const { pattern, description, ruleId } of SECURITY_PATTERNS) {
|
|
@@ -71,9 +73,17 @@ export class FileSystemSecurityPatternScannerAdapter implements SecurityPatternS
|
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
/**
|
|
77
|
+
* 当該行が allowlist されているか判定する(行/領域スコープ)。
|
|
78
|
+
*
|
|
79
|
+
* - 当該行にマーカーがある → 許可
|
|
80
|
+
* - 直前行にマーカーがある(次行抑止コメント) → 許可
|
|
81
|
+
*
|
|
82
|
+
* ファイル全体を対象にしないことで、同一ファイル内の本物の秘密情報を検出できる。
|
|
83
|
+
*/
|
|
84
|
+
function isAllowlisted(line: string, previousLine: string | undefined): boolean {
|
|
75
85
|
if (line.includes(ALLOWLIST_MARKER)) return true;
|
|
76
|
-
return
|
|
86
|
+
return previousLine?.includes(ALLOWLIST_MARKER) ?? false;
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
function redactSecret(secretValue: string): string {
|
|
@@ -18,6 +18,13 @@ function normalizePath(value: string): string {
|
|
|
18
18
|
return value.replace(/\\/g, '/');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// Separator-agnostic check for whether a path points at a work-item
|
|
22
|
+
// description file. Exported so the Windows (`\`) normalization is unit
|
|
23
|
+
// testable without depending on the host filesystem separator.
|
|
24
|
+
export function isDescriptionFilePath(value: string): boolean {
|
|
25
|
+
return normalizePath(value).endsWith(`/${DESCRIPTION_FILE}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
async function listFiles(root: string): Promise<string[]> {
|
|
22
29
|
try {
|
|
23
30
|
const entries = await readdir(root, { withFileTypes: true });
|
|
@@ -66,7 +73,9 @@ export class FileSystemWorkItemReflectionAdapter implements WorkItemReflectionPo
|
|
|
66
73
|
const inceptionRoot = join(this.projectRoot, input.inceptionRoot);
|
|
67
74
|
const designRoot = join(this.projectRoot, input.designRoot);
|
|
68
75
|
const descriptionFiles = (await listFiles(inceptionRoot))
|
|
69
|
-
|
|
76
|
+
// Normalize separators first so Windows paths (which use `\`) still match
|
|
77
|
+
// the `/description.md` suffix.
|
|
78
|
+
.filter(isDescriptionFilePath);
|
|
70
79
|
|
|
71
80
|
if (descriptionFiles.length === 0) {
|
|
72
81
|
return {
|
|
@@ -137,9 +137,7 @@ function extractExports(content: string): string[] {
|
|
|
137
137
|
function resolveImportTarget(fromFile: string, specifier: string, fileSet: ReadonlySet<string>): string | null {
|
|
138
138
|
if (!specifier.startsWith('.')) return null;
|
|
139
139
|
const base = resolve(dirname(fromFile), specifier);
|
|
140
|
-
const candidates =
|
|
141
|
-
? [base]
|
|
142
|
-
: [`${base}.ts`, `${base}.tsx`, join(base, 'index.ts'), join(base, 'index.tsx')];
|
|
140
|
+
const candidates = buildResolutionCandidates(base);
|
|
143
141
|
for (const candidate of candidates) {
|
|
144
142
|
const normalized = normalize(candidate);
|
|
145
143
|
if (fileSet.has(normalized)) return normalized;
|
|
@@ -147,6 +145,40 @@ function resolveImportTarget(fromFile: string, specifier: string, fileSet: Reado
|
|
|
147
145
|
return null;
|
|
148
146
|
}
|
|
149
147
|
|
|
148
|
+
/**
|
|
149
|
+
* import 指定子から実ファイル候補を生成する。
|
|
150
|
+
*
|
|
151
|
+
* このプロジェクトは ESM 規約に従い `./foo.js` と書いて実体 `foo.ts` を参照する
|
|
152
|
+
* (TypeScript の `.js` 拡張子付き import)。素朴に extname を見ると `.js` 実体を
|
|
153
|
+
* 探しに行き解決に失敗するため、`.js`/`.jsx`/`.mjs`/`.cjs` 拡張子は対応する
|
|
154
|
+
* TypeScript 拡張子(およびディレクトリの index)へマッピングした候補も加える。
|
|
155
|
+
*/
|
|
156
|
+
function buildResolutionCandidates(base: string): string[] {
|
|
157
|
+
const ext = extname(base);
|
|
158
|
+
const JS_TO_TS: Record<string, readonly string[]> = {
|
|
159
|
+
'.js': ['.ts', '.tsx'],
|
|
160
|
+
'.jsx': ['.tsx', '.ts'],
|
|
161
|
+
'.mjs': ['.mts', '.ts'],
|
|
162
|
+
'.cjs': ['.cts', '.ts'],
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (ext) {
|
|
166
|
+
const mapped = JS_TO_TS[ext];
|
|
167
|
+
if (mapped) {
|
|
168
|
+
const withoutExt = base.slice(0, base.length - ext.length);
|
|
169
|
+
// 例: `./foo.js` → `foo.ts` / `foo.tsx`、`./dir.js`(ディレクトリ)→ `dir/index.ts`
|
|
170
|
+
return [
|
|
171
|
+
...mapped.map((tsExt) => `${withoutExt}${tsExt}`),
|
|
172
|
+
base,
|
|
173
|
+
...mapped.map((tsExt) => join(withoutExt, `index${tsExt}`)),
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
return [base];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return [`${base}.ts`, `${base}.tsx`, join(base, 'index.ts'), join(base, 'index.tsx')];
|
|
180
|
+
}
|
|
181
|
+
|
|
150
182
|
function classifyDeadCodeExclusion(filePath: string): string | undefined {
|
|
151
183
|
const normalized = filePath.replaceAll('\\', '/');
|
|
152
184
|
if (/(^|\/)__tests__\//.test(normalized) || /\.test\.ts$/.test(normalized) || /\.it\.test\.ts$/.test(normalized)) {
|