phasegate 0.160.1 → 0.160.2
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/package.json +1 -1
- package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +18 -1
- package/scripts/harness/main.ts +7 -1
- package/scripts/harness/phase2-extensions/application/usecases/check-doc-freshness-usecase.ts +4 -5
- package/scripts/harness/phase2-extensions/application/usecases/validate-doc-pointers-usecase.ts +4 -6
- package/scripts/harness/phase2-extensions/infrastructure/adapters/harness-config-freshness-adapter.ts +8 -4
- package/scripts/harness/phase2-extensions/presentation/handlers/validate-pointers-handler.ts +6 -1
- package/scripts/harness/skill-quality/application/dto/check-coverage-output.ts +2 -0
- package/scripts/harness/skill-quality/application/usecases/check-coverage-usecase.ts +16 -0
- package/scripts/harness/skill-quality/domain/errors/skill-quality-error.ts +1 -0
- package/scripts/harness/skill-quality/infrastructure/adapters/file-system-requirement-test-matrix-adapter.ts +7 -1
- package/scripts/harness/skill-quality/infrastructure/adapters/vitest-coverage-runner-adapter.ts +30 -15
- package/scripts/harness/skill-quality/presentation/handlers/check-coverage-handler.ts +5 -0
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.160.2] - 2026-05-14
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **WI-185 / WI-186 / WI-188 — validator and gate trust** — fixes downstream P2 freshness/pointer scans so package-bin execution resolves caller project docs, makes `phasegate:status` JSON fail when enabled live layer validation fails while preserving its informational exit-code contract, and prevents `skill:check-coverage` from launching Vitest or network-prone auto-install paths before story/test prerequisites are validated.
|
|
15
|
+
|
|
10
16
|
## [0.160.1] - 2026-05-14
|
|
11
17
|
|
|
12
18
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @layer domain
|
|
2
2
|
// @unit harness-api
|
|
3
|
-
// @work-item-id WI-108 / WI-114
|
|
3
|
+
// @work-item-id WI-108 / WI-114, WI-186
|
|
4
4
|
// command-dispatch-service.ts — CommandDispatchService Domain Service
|
|
5
5
|
|
|
6
6
|
import { CommandRegistry } from './command-registry.js';
|
|
@@ -68,6 +68,16 @@ function summarizeLayerResults(items: readonly { validatorId: string; passed: bo
|
|
|
68
68
|
return result;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function hasEnabledLiveFailure(
|
|
72
|
+
enabledLayers: readonly LayerId[],
|
|
73
|
+
liveValidationByLayer: Partial<Record<LayerId, LiveValidationState>>,
|
|
74
|
+
): boolean {
|
|
75
|
+
return enabledLayers.some((layerId) => {
|
|
76
|
+
const liveState = liveValidationByLayer[layerId];
|
|
77
|
+
return liveState === 'fail' || liveState === 'error';
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
function buildOperationalWarnings(
|
|
72
82
|
hookHealth: HookHealth | undefined,
|
|
73
83
|
baselineHealth: BaselineHealth | undefined,
|
|
@@ -257,6 +267,13 @@ export class CommandDispatchService {
|
|
|
257
267
|
baselineHealth,
|
|
258
268
|
operationalWarnings,
|
|
259
269
|
});
|
|
270
|
+
if (hasEnabledLiveFailure(presetInfo.enabledLayers, liveValidationByLayer)) {
|
|
271
|
+
const errors = statusSummary.layers
|
|
272
|
+
.filter((layer) => layer.enabled && (layer.liveValidationState === 'fail' || layer.liveValidationState === 'error'))
|
|
273
|
+
.map((layer) => makeError(`Layer ${layer.layerId} live validation ${layer.liveValidationState}`));
|
|
274
|
+
const r = HarnessApiResponse.fail(errors, { ...summary, failed: 1 }, statusSummary);
|
|
275
|
+
return { status: 'fail', errors: r.errors, summary: r.summary, data: statusSummary as unknown as T, exitCode: 0 };
|
|
276
|
+
}
|
|
260
277
|
const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, statusSummary);
|
|
261
278
|
return { status: 'pass', errors: [], summary: r.summary, data: statusSummary as unknown as T, exitCode: 0 };
|
|
262
279
|
}
|
package/scripts/harness/main.ts
CHANGED
|
@@ -193,6 +193,12 @@ Commands:
|
|
|
193
193
|
phasegate:impact-analysis Impact analysis for story (<storyId>, --json)
|
|
194
194
|
phasegate:generate-matrix Generate requirement-test matrix (--requirements, --tests, --out, --json)
|
|
195
195
|
|
|
196
|
+
Gate semantics:
|
|
197
|
+
phasegate:complete-check Gate: lint + all validators; exits 1 on failure
|
|
198
|
+
phasegate:check-ready Gate: story phase readiness; exits 1 on pending/failed stories
|
|
199
|
+
validate --layer <Lx> Gate: requested validator layer; exits 1 on layer failure
|
|
200
|
+
phasegate:status Informational health; JSON status may be fail, exit remains 0 unless command error
|
|
201
|
+
|
|
196
202
|
ci:generate-template Generate CI template (--preset <id>, default: standard; --type <aidlc-gate|consistency-check|pre-commit|agent-context-refresh>, --render, --json)
|
|
197
203
|
ci:migrate-agents-md Migrate AGENTS.md (--dry-run, --validate-only, --json)
|
|
198
204
|
ci:auto-refresh-agent-context Refresh AGENTS.md / CLAUDE.md (--dry-run, --apply, --json)
|
|
@@ -219,7 +225,7 @@ Commands:
|
|
|
219
225
|
regression:migrate-v0-tests Execute V0 test migration (--confirm)
|
|
220
226
|
|
|
221
227
|
p2:check-freshness Check doc freshness (--pattern <glob>, --dry-run, --format text|json)
|
|
222
|
-
p2:validate-pointers Validate doc pointers (--include-urls, --format text|json)
|
|
228
|
+
p2:validate-pointers Validate doc pointers (--pattern <glob>, --include-urls, --format text|json)
|
|
223
229
|
p2:generate-e2e-template Generate E2E test template (--phase <phase>, --output <path>)
|
|
224
230
|
p2:check-initial-creation Public compatibility detector for long-lived initial_creation:true docs (--pattern <glob>, --format text|json)
|
|
225
231
|
hook <pre-tool-use|post-tool-use|stop|session-start|user-prompt-submit> Run agent hook (reads JSON from stdin; writes JSON to stdout for session-start/user-prompt-submit)
|
package/scripts/harness/phase2-extensions/application/usecases/check-doc-freshness-usecase.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer application
|
|
3
3
|
* @unit phase2-extensions
|
|
4
|
+
* @work-item-id WI-185
|
|
4
5
|
*/
|
|
5
6
|
import type { CheckDocFreshnessInput } from '../dto/check-doc-freshness-input.js';
|
|
6
7
|
import type { CheckDocFreshnessOutput } from '../dto/check-doc-freshness-output.js';
|
|
@@ -29,18 +30,16 @@ export class CheckDocFreshnessUseCase {
|
|
|
29
30
|
async execute(input: CheckDocFreshnessInput): Promise<CheckDocFreshnessOutput> {
|
|
30
31
|
try {
|
|
31
32
|
const rules = await this.freshnessConfigPort.loadRules();
|
|
32
|
-
const filteredRules = input.targetPattern
|
|
33
|
-
? rules.filter((rule) => rule.documentPattern === input.targetPattern)
|
|
34
|
-
: rules;
|
|
35
33
|
|
|
36
34
|
const results = [];
|
|
37
35
|
|
|
38
|
-
for (const rule of
|
|
36
|
+
for (const rule of rules) {
|
|
39
37
|
if (!rule.isEnabled()) {
|
|
40
38
|
continue;
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
const
|
|
41
|
+
const scanPattern = input.targetPattern ?? rule.documentPattern;
|
|
42
|
+
const documentPaths = await this.documentScannerPort.scan(scanPattern);
|
|
44
43
|
for (const documentPath of documentPaths) {
|
|
45
44
|
const age = await this.documentAgePort.getAge(documentPath);
|
|
46
45
|
results.push(this.freshnessCheckService.check(rule, age, documentPath));
|
package/scripts/harness/phase2-extensions/application/usecases/validate-doc-pointers-usecase.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer application
|
|
3
3
|
* @unit phase2-extensions
|
|
4
|
-
* @work-item-id WI-122
|
|
4
|
+
* @work-item-id WI-122, WI-185
|
|
5
5
|
*/
|
|
6
6
|
import type { FreshnessConfigPort } from '../../domain/ports/freshness-config-port.js';
|
|
7
7
|
import type { DocumentScannerPort } from '../../domain/ports/document-scanner-port.js';
|
|
@@ -30,16 +30,14 @@ export class ValidateDocPointersUseCase {
|
|
|
30
30
|
async execute(input: ValidateDocPointersInput): Promise<ValidateDocPointersOutput> {
|
|
31
31
|
try {
|
|
32
32
|
const rules = await this.freshnessConfigPort.loadPointerRules();
|
|
33
|
-
const filteredRules = input.targetPattern
|
|
34
|
-
? rules.filter((rule) => rule.documentPattern === input.targetPattern)
|
|
35
|
-
: rules;
|
|
36
33
|
|
|
37
34
|
const results = [];
|
|
38
35
|
let totalDocuments = 0;
|
|
39
36
|
let skippedUrlPointers = 0;
|
|
40
37
|
|
|
41
|
-
for (const rule of
|
|
42
|
-
const
|
|
38
|
+
for (const rule of rules) {
|
|
39
|
+
const scanPattern = input.targetPattern ?? rule.documentPattern;
|
|
40
|
+
const documentPaths = await this.documentScannerPort.scan(scanPattern);
|
|
43
41
|
totalDocuments += documentPaths.length;
|
|
44
42
|
|
|
45
43
|
for (const documentPath of documentPaths) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer infrastructure
|
|
3
3
|
* @unit phase2-extensions
|
|
4
|
-
* @work-item-id WI-122
|
|
4
|
+
* @work-item-id WI-122, WI-185
|
|
5
5
|
*/
|
|
6
6
|
import type { HarnessConfigV2 } from '../../../config-foundation/domain/harness-config.js';
|
|
7
7
|
import { DocFreshnessRule } from '../../domain/aggregates/doc-freshness-rule.js';
|
|
@@ -32,16 +32,20 @@ type Phase2RuleConfig = {
|
|
|
32
32
|
};
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
function getPhase2RuleConfig(config: HarnessConfigV2 | Phase2RuleConfig | undefined): Phase2RuleConfig['phase2Extensions'] {
|
|
36
|
+
return (config as Phase2RuleConfig | undefined)?.phase2Extensions;
|
|
37
|
+
}
|
|
38
|
+
|
|
35
39
|
function normalizePathPatternRoot(value: string | undefined): string {
|
|
36
40
|
const normalized = value?.replace(/\\/g, '/').replace(/\/+$/g, '');
|
|
37
|
-
return normalized && normalized.length > 0 ? normalized : 'docs
|
|
41
|
+
return normalized && normalized.length > 0 ? normalized : 'docs';
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
export class HarnessConfigFreshnessAdapter implements FreshnessConfigPort {
|
|
41
45
|
constructor(private readonly config?: HarnessConfigV2 | Phase2RuleConfig) {}
|
|
42
46
|
|
|
43
47
|
async loadRules(): Promise<DocFreshnessRule[]> {
|
|
44
|
-
const configRules = this.config
|
|
48
|
+
const configRules = getPhase2RuleConfig(this.config)?.freshnessRules;
|
|
45
49
|
if (!configRules || configRules.length === 0) {
|
|
46
50
|
const designDocsRoot = normalizePathPatternRoot(this.config?.paths?.designDocs);
|
|
47
51
|
return [
|
|
@@ -68,7 +72,7 @@ export class HarnessConfigFreshnessAdapter implements FreshnessConfigPort {
|
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
async loadPointerRules(): Promise<PointerRule[]> {
|
|
71
|
-
const configRules = this.config
|
|
75
|
+
const configRules = getPhase2RuleConfig(this.config)?.pointerRules;
|
|
72
76
|
if (!configRules || configRules.length === 0) {
|
|
73
77
|
const designDocsRoot = normalizePathPatternRoot(this.config?.paths?.designDocs);
|
|
74
78
|
return [
|
package/scripts/harness/phase2-extensions/presentation/handlers/validate-pointers-handler.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer presentation
|
|
3
3
|
* @unit phase2-extensions
|
|
4
|
+
* @work-item-id WI-185
|
|
4
5
|
*/
|
|
5
6
|
import type { ValidateDocPointersUseCase } from '../../application/usecases/validate-doc-pointers-usecase.js';
|
|
6
7
|
import { PointerResultFormatter } from '../formatters/pointer-result-formatter.js';
|
|
@@ -13,18 +14,22 @@ export class ValidatePointersHandler {
|
|
|
13
14
|
async handle(args: string[]): Promise<{ exitCode: number; stdout: string }> {
|
|
14
15
|
let includeUrlPointers = false;
|
|
15
16
|
let format: 'text' | 'json' = 'text';
|
|
17
|
+
let targetPattern: string | undefined;
|
|
16
18
|
|
|
17
19
|
for (let index = 0; index < args.length; index += 1) {
|
|
18
20
|
const arg = args[index];
|
|
19
21
|
if (arg === '--include-urls') {
|
|
20
22
|
includeUrlPointers = true;
|
|
23
|
+
} else if (arg === '--pattern') {
|
|
24
|
+
targetPattern = args[index + 1];
|
|
25
|
+
index += 1;
|
|
21
26
|
} else if (arg === '--format') {
|
|
22
27
|
format = (args[index + 1] as 'text' | 'json') ?? 'text';
|
|
23
28
|
index += 1;
|
|
24
29
|
}
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
const result = await this.useCase.execute({ includeUrlPointers, format });
|
|
32
|
+
const result = await this.useCase.execute({ targetPattern, includeUrlPointers, format });
|
|
28
33
|
return {
|
|
29
34
|
exitCode: result.passed ? 0 : 1,
|
|
30
35
|
stdout: format === 'json' ? this.formatter.formatJson(result) : this.formatter.formatText(result),
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer application
|
|
3
3
|
* @unit skill-quality
|
|
4
|
+
* @work-item-id WI-188
|
|
4
5
|
*/
|
|
5
6
|
import { CoverageReport } from '../../domain/value-objects/coverage-report.js';
|
|
6
7
|
import { RequirementCoverageResult } from '../../domain/value-objects/requirement-coverage-result.js';
|
|
8
|
+
import { CodeCoverageResult } from '../../domain/value-objects/code-coverage-result.js';
|
|
7
9
|
import type { RequirementTestMatrixPort } from '../../domain/ports/requirement-test-matrix-port.js';
|
|
8
10
|
import type { CoverageRunnerPort } from '../../domain/ports/coverage-runner-port.js';
|
|
9
11
|
import type { ConfigQueryPort } from '../../domain/ports/config-query-port.js';
|
|
@@ -25,6 +27,20 @@ export class CheckCoverageUseCase {
|
|
|
25
27
|
matrix.covered,
|
|
26
28
|
matrix.uncoveredIds,
|
|
27
29
|
);
|
|
30
|
+
if (matrix.total === 0) {
|
|
31
|
+
const coverageReport = CoverageReport.create(
|
|
32
|
+
requirementCoverage,
|
|
33
|
+
CodeCoverageResult.create(100, 100, 100),
|
|
34
|
+
);
|
|
35
|
+
return {
|
|
36
|
+
coverageReport,
|
|
37
|
+
meetsThreshold: true,
|
|
38
|
+
requirementThreshold: threshold.requirement,
|
|
39
|
+
codeThreshold: threshold.code,
|
|
40
|
+
skipped: true,
|
|
41
|
+
skipReason: 'no-tests',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
28
44
|
const codeCoverage = await this.coverageRunnerPort.run(input.storyId);
|
|
29
45
|
const coverageReport = CoverageReport.create(requirementCoverage, codeCoverage);
|
|
30
46
|
const meetsThreshold = coverageReport.meetsThreshold(threshold.requirement, threshold.code);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer infrastructure
|
|
3
3
|
* @unit skill-quality
|
|
4
|
+
* @work-item-id WI-188
|
|
4
5
|
*/
|
|
5
6
|
import { readFile } from 'node:fs/promises';
|
|
6
7
|
import type { RequirementTestMatrixPort, RequirementTestMatrix } from '../../domain/ports/requirement-test-matrix-port.js';
|
|
@@ -17,7 +18,12 @@ export class FileSystemRequirementTestMatrixAdapter implements RequirementTestMa
|
|
|
17
18
|
throw new SkillQualityError('MATRIX_FILE_NOT_FOUND', `requirement-test-matrix.json not found at ${this.matrixFilePath}`);
|
|
18
19
|
}
|
|
19
20
|
const data = JSON.parse(raw) as Record<string, unknown>;
|
|
20
|
-
const
|
|
21
|
+
const rootEntry = data as { total?: unknown; covered?: unknown; uncoveredIds?: unknown };
|
|
22
|
+
const explicitEntry = data[storyId];
|
|
23
|
+
if (explicitEntry === undefined && rootEntry.total === undefined) {
|
|
24
|
+
throw new SkillQualityError('STORY_NOT_FOUND', `Story ${storyId} not found in ${this.matrixFilePath}`);
|
|
25
|
+
}
|
|
26
|
+
const entry = (explicitEntry ?? data) as { total: number; covered: number; uncoveredIds: string[] };
|
|
21
27
|
return {
|
|
22
28
|
storyId,
|
|
23
29
|
total: entry.total ?? 0,
|
package/scripts/harness/skill-quality/infrastructure/adapters/vitest-coverage-runner-adapter.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer infrastructure
|
|
3
3
|
* @unit skill-quality
|
|
4
|
+
* @work-item-id WI-188
|
|
4
5
|
*/
|
|
5
|
-
import {
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
6
7
|
import { readFileSync, existsSync } from 'node:fs';
|
|
7
8
|
import * as path from 'node:path';
|
|
8
9
|
import type { CoverageRunnerPort } from '../../domain/ports/coverage-runner-port.js';
|
|
9
10
|
import { CodeCoverageResult } from '../../domain/value-objects/code-coverage-result.js';
|
|
10
11
|
import { SkillQualityError } from '../../domain/errors/skill-quality-error.js';
|
|
11
12
|
|
|
12
|
-
const COVERAGE_SUMMARY_PATH = path.join(process.cwd(), '.harness', 'coverage-summary.json');
|
|
13
|
-
const COVERAGE_REPORTS_DIR = path.join(process.cwd(), '.harness');
|
|
14
|
-
|
|
15
13
|
interface CoverageSummary {
|
|
16
14
|
total: {
|
|
17
15
|
lines: { pct: number };
|
|
@@ -20,15 +18,15 @@ interface CoverageSummary {
|
|
|
20
18
|
};
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
function parseCoverageSummary(): CodeCoverageResult {
|
|
24
|
-
if (!existsSync(
|
|
21
|
+
function parseCoverageSummary(coverageSummaryPath: string): CodeCoverageResult {
|
|
22
|
+
if (!existsSync(coverageSummaryPath)) {
|
|
25
23
|
throw new SkillQualityError(
|
|
26
24
|
'INVALID_COVERAGE_REPORT',
|
|
27
|
-
`Coverage summary not found at ${
|
|
25
|
+
`Coverage summary not found at ${coverageSummaryPath}. Run: pnpm test --coverage first.`,
|
|
28
26
|
);
|
|
29
27
|
}
|
|
30
28
|
try {
|
|
31
|
-
const raw = readFileSync(
|
|
29
|
+
const raw = readFileSync(coverageSummaryPath, 'utf-8');
|
|
32
30
|
const data = JSON.parse(raw) as CoverageSummary;
|
|
33
31
|
const line = Math.round(data.total.lines.pct);
|
|
34
32
|
const branch = Math.round(data.total.branches.pct);
|
|
@@ -43,19 +41,36 @@ function parseCoverageSummary(): CodeCoverageResult {
|
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
export class VitestCoverageRunnerAdapter implements CoverageRunnerPort {
|
|
44
|
+
constructor(
|
|
45
|
+
private readonly projectRoot: string = process.cwd(),
|
|
46
|
+
private readonly execFile: typeof execFileSync = execFileSync,
|
|
47
|
+
) {}
|
|
48
|
+
|
|
46
49
|
async run(_storyId: string): Promise<CodeCoverageResult> {
|
|
50
|
+
const coverageSummaryPath = path.join(this.projectRoot, '.harness', 'coverage-summary.json');
|
|
51
|
+
const coverageReportsDir = path.join(this.projectRoot, '.harness');
|
|
52
|
+
const vitestBin = path.join(this.projectRoot, 'node_modules', '.bin', 'vitest');
|
|
53
|
+
|
|
47
54
|
// Use pre-existing summary if available
|
|
48
|
-
if (existsSync(
|
|
49
|
-
return parseCoverageSummary();
|
|
55
|
+
if (existsSync(coverageSummaryPath)) {
|
|
56
|
+
return parseCoverageSummary(coverageSummaryPath);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!existsSync(vitestBin) && !existsSync(path.join(this.projectRoot, 'node_modules', 'vitest', 'package.json'))) {
|
|
60
|
+
throw new SkillQualityError(
|
|
61
|
+
'COVERAGE_RUN_FAILED',
|
|
62
|
+
'Install vitest locally before running coverage: npm install -D vitest @vitest/coverage-v8',
|
|
63
|
+
);
|
|
50
64
|
}
|
|
51
65
|
|
|
52
|
-
//
|
|
66
|
+
// Run the local Vitest binary only; never use npx auto-install.
|
|
53
67
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
this.execFile(
|
|
69
|
+
vitestBin,
|
|
70
|
+
['run', '--coverage', '--coverage.reporter=json-summary', `--coverage.reportsDirectory=${coverageReportsDir}`],
|
|
71
|
+
{ stdio: 'pipe', cwd: this.projectRoot },
|
|
57
72
|
);
|
|
58
|
-
return parseCoverageSummary();
|
|
73
|
+
return parseCoverageSummary(coverageSummaryPath);
|
|
59
74
|
} catch (err) {
|
|
60
75
|
const msg = err instanceof Error ? err.message : String(err);
|
|
61
76
|
if (msg.includes('coverage-v8') || msg.includes('MISSING DEPENDENCY')) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer presentation
|
|
3
3
|
* @unit skill-quality
|
|
4
|
+
* @work-item-id WI-188
|
|
4
5
|
*/
|
|
5
6
|
import type { CheckCoverageUseCase } from '../../application/usecases/check-coverage-usecase.js';
|
|
6
7
|
|
|
@@ -25,6 +26,10 @@ export class CheckCoverageHandler {
|
|
|
25
26
|
const codeRate = output.coverageReport.codeCoverage.lineCoverage.toFixed(1);
|
|
26
27
|
const msg = `Requirement coverage: ${reqRate}% (threshold: ${output.requirementThreshold}%)\nCode coverage: ${codeRate}% (threshold: ${output.codeThreshold}%)`;
|
|
27
28
|
|
|
29
|
+
if (output.skipped === true && output.skipReason === 'no-tests') {
|
|
30
|
+
return { exitCode: 0, message: `Coverage SKIPPED (no tests)\n${msg}` };
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
if (output.meetsThreshold) {
|
|
29
34
|
return { exitCode: 0, message: `Coverage OK\n${msg}` };
|
|
30
35
|
}
|