phasegate 0.283.0 → 0.335.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/ADR/017-warning-severity-aggregation.md +17 -0
- package/docs/ADR/038-config-state-operation-permission-policy.md +78 -0
- package/docs/ADR/039-hook-observable-state-as-authorization-unit.md +74 -0
- package/docs/ADR/040-quick-mode-config-via-preset-resolution.md +66 -0
- package/docs/guide/cli-reference.md +89 -7
- package/docs/guide/configuration.md +43 -2
- package/docs/guide/layer-model.md +2 -0
- package/docs/guide/quick-vs-full-mode.md +63 -3
- package/docs/guide/troubleshooting.md +37 -0
- package/docs/templates/agent-context/CLAUDE.md.template.md +6 -6
- package/package.json +2 -2
- package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +5 -1
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +108 -20
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +215 -5
- package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +5 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/file-system-full-mode-session-query-adapter.ts +75 -23
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +18 -3
- package/scripts/harness/agent-integration/infrastructure/adapters/quick-mode-full-mode-requirement-adapter.ts +15 -1
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +57 -9
- package/scripts/harness/ci-governance/application/dto/scaffold-inception-input.ts +9 -0
- package/scripts/harness/ci-governance/application/dto/scaffold-inception-output.ts +13 -0
- package/scripts/harness/ci-governance/application/usecases/list-templates-usecase.ts +30 -0
- package/scripts/harness/ci-governance/application/usecases/scaffold-inception-usecase.ts +63 -0
- package/scripts/harness/ci-governance/application/usecases/show-template-usecase.ts +39 -0
- package/scripts/harness/ci-governance/composition-root.ts +57 -5
- package/scripts/harness/ci-governance/domain/ports/inception-doc-writer-port.ts +19 -0
- package/scripts/harness/ci-governance/domain/ports/inception-template-repository-port.ts +16 -0
- package/scripts/harness/ci-governance/domain/ports/template-catalog-port.ts +23 -0
- package/scripts/harness/ci-governance/domain/services/claude-md-composer.ts +20 -11
- package/scripts/harness/ci-governance/domain/value-objects/inception-doc-kind.ts +109 -0
- package/scripts/harness/ci-governance/domain/value-objects/template-catalog-entry.ts +53 -0
- package/scripts/harness/ci-governance/domain/value-objects/template-name.ts +50 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-doc-writer-adapter.ts +42 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-template-repository-adapter.ts +32 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-template-catalog-adapter.ts +60 -0
- package/scripts/harness/ci-governance/presentation/handlers/check-repetition-handler.ts +10 -2
- package/scripts/harness/ci-governance/presentation/handlers/scaffold-inception-handler.ts +101 -0
- package/scripts/harness/ci-governance/presentation/handlers/templates-handler.ts +103 -0
- package/scripts/harness/config-foundation/domain/harness-config.ts +12 -0
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +3 -3
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +3 -3
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +3 -3
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +44 -1
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +42 -1
- package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +12 -3
- package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +2 -22
- package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +3 -1
- package/scripts/harness/harness-error/application/dto/create-harness-error-input.ts +3 -1
- package/scripts/harness/harness-error/application/dto/harness-error-contract.ts +3 -1
- package/scripts/harness/harness-error/application/mappers/harness-error-contract-mapper.ts +9 -17
- package/scripts/harness/harness-error/application/usecases/create-harness-error-use-case.ts +7 -7
- package/scripts/harness/harness-error/domain/services/harness-error-factory.ts +30 -33
- package/scripts/harness/harness-error/domain/value-objects/error-definition.ts +28 -17
- package/scripts/harness/harness-error/domain/value-objects/harness-error.ts +30 -9
- package/scripts/harness/harness-error/domain/value-objects/remediation-type.ts +30 -0
- package/scripts/harness/harness-error/infrastructure/registry/l2-error-definitions.ts +44 -29
- package/scripts/harness/harness-error/infrastructure/registry/l3-error-definitions.ts +44 -27
- package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +47 -32
- package/scripts/harness/installation/application/checks/claude-context-missing-check.ts +13 -7
- package/scripts/harness/installation/application/checks/config-status-check.ts +52 -0
- package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +6 -0
- package/scripts/harness/installation/application/ports/config-status-probe-port.ts +9 -0
- package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +30 -8
- package/scripts/harness/installation/application/usecases/run-install.ts +44 -11
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +44 -11
- package/scripts/harness/installation/composition-root.ts +13 -3
- package/scripts/harness/installation/domain/check-id.ts +2 -0
- package/scripts/harness/installation/domain/config-status.ts +17 -0
- package/scripts/harness/installation/domain/ports/heuristic-check.ts +10 -1
- package/scripts/harness/installation/infrastructure/adapters/config-status-probe-adapter.ts +79 -0
- package/scripts/harness/installation/presentation/cli/doctor-handler.ts +6 -1
- package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +19 -5
- package/scripts/harness/integrations/pre-commit.ts +17 -3
- package/scripts/harness/main.ts +159 -21
- package/scripts/harness/phase-dependency-model/application/services/evidence-bundle-assembler.ts +2 -1
- package/scripts/harness/phase-dependency-model/domain/ports/plan-document-reader-port.ts +12 -0
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +64 -33
- package/scripts/harness/phase2-extensions/presentation/handlers/check-freshness-handler.ts +17 -9
- package/scripts/harness/quick-mode/application/ports/file-existence-port.ts +15 -0
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +60 -22
- package/scripts/harness/quick-mode/composition-root.ts +25 -15
- package/scripts/harness/quick-mode/domain/errors/quick-mode-config-error.ts +19 -0
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +116 -18
- package/scripts/harness/quick-mode/domain/value-objects/category-override-rules.ts +147 -0
- package/scripts/harness/quick-mode/domain/value-objects/change-category.ts +33 -0
- package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +31 -10
- package/scripts/harness/quick-mode/index.ts +7 -1
- package/scripts/harness/quick-mode/infrastructure/adapters/fs-file-existence-adapter.ts +38 -0
- package/scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts +83 -32
- package/scripts/harness/skill-quality/infrastructure/adapters/file-system-requirement-test-matrix-adapter.ts +51 -8
- package/scripts/harness/skill-quality/presentation/handlers/check-coverage-handler.ts +13 -6
- package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +5 -1
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-identity-gateway.ts +6 -1
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +15 -2
- package/scripts/harness/validator-system/application/use-cases/aggregate-validation-results-usecase.ts +11 -14
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +8 -0
- package/scripts/harness/validator-system/domain/services/effective-severity-policy.ts +39 -0
- package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +6 -4
- package/scripts/harness/validator-system/domain/value-objects/drift-report.ts +12 -7
- package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +11 -3
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +20 -17
- package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +35 -14
- package/scripts/harness/validator-system/infrastructure/adapters/traceability-metadata-policy-adapter.ts +13 -8
- package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +19 -10
- package/skills/product-architect/SKILL.md +19 -0
- package/skills/quick-implementor/SKILL.md +19 -0
- package/skills/story-mapper/SKILL.md +11 -0
- package/skills/story-writer/SKILL.md +11 -0
- package/skills/unit-designer/SKILL.md +11 -0
- package/templates/product_overview.template.md +85 -0
- package/templates/product_overview_plan.template.md +55 -0
- package/templates/story_mapping_plan.template.md +61 -0
- package/templates/story_writer_plan.template.md +61 -0
- package/templates/unit_design_plan.template.md +63 -0
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
* Quick Mode設定を表す値オブジェクト
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
8
|
+
import { QuickModeConfigError } from '../errors/quick-mode-config-error.js';
|
|
9
|
+
import {
|
|
10
|
+
CategoryOverrideRules,
|
|
11
|
+
type CategoryOverrideRulesInput,
|
|
12
|
+
} from './category-override-rules.js';
|
|
13
|
+
import { CHANGE_CATEGORY_VALUES, isChangeCategoryValue } from './change-category.js';
|
|
14
|
+
|
|
15
|
+
export { QuickModeConfigError };
|
|
14
16
|
|
|
15
17
|
export type FullModeRequiredRuleId = 'mixedCategories' | 'newDomainFile' | 'apiContractChange';
|
|
16
18
|
|
|
@@ -32,17 +34,21 @@ export class QuickModeConfig {
|
|
|
32
34
|
readonly maintainedLayers: readonly string[];
|
|
33
35
|
readonly relaxedGates: readonly string[];
|
|
34
36
|
readonly fullModeRequiredWhen: FullModeRequiredRules;
|
|
37
|
+
/** @work-item-id WI-372 プロジェクト固有パスのカテゴリ割当ルール(未設定時は空) */
|
|
38
|
+
readonly categoryOverrides: CategoryOverrideRules;
|
|
35
39
|
|
|
36
40
|
private constructor(
|
|
37
41
|
allowedCategories: readonly string[],
|
|
38
42
|
maintainedLayers: readonly string[],
|
|
39
43
|
relaxedGates: readonly string[],
|
|
40
|
-
fullModeRequiredWhen: FullModeRequiredRules
|
|
44
|
+
fullModeRequiredWhen: FullModeRequiredRules,
|
|
45
|
+
categoryOverrides: CategoryOverrideRules
|
|
41
46
|
) {
|
|
42
47
|
this.allowedCategories = allowedCategories;
|
|
43
48
|
this.maintainedLayers = maintainedLayers;
|
|
44
49
|
this.relaxedGates = relaxedGates;
|
|
45
50
|
this.fullModeRequiredWhen = fullModeRequiredWhen;
|
|
51
|
+
this.categoryOverrides = categoryOverrides;
|
|
46
52
|
Object.freeze(this);
|
|
47
53
|
}
|
|
48
54
|
|
|
@@ -51,13 +57,26 @@ export class QuickModeConfig {
|
|
|
51
57
|
maintainedLayers: string[];
|
|
52
58
|
relaxedGates: string[];
|
|
53
59
|
fullModeRequiredWhen?: Partial<FullModeRequiredRules>;
|
|
60
|
+
categoryOverrides?: CategoryOverrideRulesInput;
|
|
54
61
|
}): QuickModeConfig {
|
|
55
|
-
const { allowedCategories, maintainedLayers, relaxedGates, fullModeRequiredWhen } = raw;
|
|
62
|
+
const { allowedCategories, maintainedLayers, relaxedGates, fullModeRequiredWhen, categoryOverrides } = raw;
|
|
56
63
|
|
|
57
64
|
if (allowedCategories.length === 0) {
|
|
58
65
|
throw new QuickModeConfigError('allowedCategories must not be empty');
|
|
59
66
|
}
|
|
60
67
|
|
|
68
|
+
// WI-373: allowedCategories は ChangeCategory 7 値の enum。
|
|
69
|
+
// 従来は非空チェックのみだったため "typoo" のような未知値が黙って通り、
|
|
70
|
+
// 「設定したのに効かない」状態になっていた。分類結果のキーは常に小文字なので
|
|
71
|
+
// 大文字小文字の正規化はせず厳密一致で拒否する(正規化は効かない設定の黙認になる)。
|
|
72
|
+
for (const category of allowedCategories) {
|
|
73
|
+
if (!isChangeCategoryValue(category)) {
|
|
74
|
+
throw new QuickModeConfigError(
|
|
75
|
+
`allowedCategories contains unknown category "${category}". Valid values: ${CHANGE_CATEGORY_VALUES.join(', ')}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
61
80
|
const mergedRules: FullModeRequiredRules = Object.freeze({
|
|
62
81
|
mixedCategories: fullModeRequiredWhen?.mixedCategories ?? DEFAULT_FULL_MODE_REQUIRED_WHEN.mixedCategories,
|
|
63
82
|
newDomainFile: fullModeRequiredWhen?.newDomainFile ?? DEFAULT_FULL_MODE_REQUIRED_WHEN.newDomainFile,
|
|
@@ -68,7 +87,8 @@ export class QuickModeConfig {
|
|
|
68
87
|
Object.freeze([...allowedCategories]),
|
|
69
88
|
Object.freeze([...maintainedLayers]),
|
|
70
89
|
Object.freeze([...relaxedGates]),
|
|
71
|
-
mergedRules
|
|
90
|
+
mergedRules,
|
|
91
|
+
CategoryOverrideRules.create(categoryOverrides)
|
|
72
92
|
);
|
|
73
93
|
}
|
|
74
94
|
|
|
@@ -100,7 +120,8 @@ export class QuickModeConfig {
|
|
|
100
120
|
JSON.stringify(this.relaxedGates) === JSON.stringify(other.relaxedGates) &&
|
|
101
121
|
this.fullModeRequiredWhen.mixedCategories === other.fullModeRequiredWhen.mixedCategories &&
|
|
102
122
|
this.fullModeRequiredWhen.newDomainFile === other.fullModeRequiredWhen.newDomainFile &&
|
|
103
|
-
this.fullModeRequiredWhen.apiContractChange === other.fullModeRequiredWhen.apiContractChange
|
|
123
|
+
this.fullModeRequiredWhen.apiContractChange === other.fullModeRequiredWhen.apiContractChange &&
|
|
124
|
+
this.categoryOverrides.equals(other.categoryOverrides)
|
|
104
125
|
);
|
|
105
126
|
}
|
|
106
127
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
// @unit quick-mode
|
|
2
2
|
// @layer application
|
|
3
3
|
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
CHANGE_CATEGORY_VALUES,
|
|
6
|
+
ChangeCategory,
|
|
7
|
+
isChangeCategoryValue,
|
|
8
|
+
UnknownChangeCategoryError,
|
|
9
|
+
} from './domain/value-objects/change-category.js';
|
|
10
|
+
export { CategoryOverrideRules } from './domain/value-objects/category-override-rules.js';
|
|
5
11
|
export { ChangedFile } from './domain/value-objects/changed-file.js';
|
|
6
12
|
export { ChangeClassification } from './domain/value-objects/change-classification.js';
|
|
7
13
|
export { QuickModeConfig, QuickModeConfigError } from './domain/value-objects/quick-mode-config.js';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer infrastructure
|
|
3
|
+
* @unit quick-mode
|
|
4
|
+
* @work-item-id WI-334
|
|
5
|
+
*
|
|
6
|
+
* FileExistencePort の fs 実装。
|
|
7
|
+
* 相対パスは baseDir(既定: process.cwd())基準で解決する。
|
|
8
|
+
* stat が ENOENT / ENOTDIR 以外で失敗した場合(権限エラー等)は
|
|
9
|
+
* 「存在する」扱いにして呼び出し側の changeKind 推定を MODIFY 既定
|
|
10
|
+
* (従来どおりの安全側)に倒す。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from "node:fs/promises";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import type { FileExistencePort } from "../../application/ports/file-existence-port.js";
|
|
16
|
+
|
|
17
|
+
export class FsFileExistenceAdapter implements FileExistencePort {
|
|
18
|
+
private readonly baseDir: string;
|
|
19
|
+
|
|
20
|
+
constructor(baseDir?: string) {
|
|
21
|
+
this.baseDir = baseDir ?? process.cwd();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async exists(filePath: string): Promise<boolean> {
|
|
25
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(this.baseDir, filePath);
|
|
26
|
+
try {
|
|
27
|
+
await fs.stat(absolutePath);
|
|
28
|
+
return true;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
const error = err as NodeJS.ErrnoException;
|
|
31
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR") {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
// stat 失敗(権限等)は存在扱い → changeKind は従来どおり MODIFY 既定(安全側)
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -2,81 +2,132 @@
|
|
|
2
2
|
* @layer infrastructure
|
|
3
3
|
* @unit quick-mode
|
|
4
4
|
* @work-item-id WI-140
|
|
5
|
+
* @work-item-id WI-372
|
|
6
|
+
* @work-item-id WI-377
|
|
5
7
|
*
|
|
6
8
|
* phasegate.config.json から QuickModeConfig を取得する Adapter
|
|
9
|
+
*
|
|
10
|
+
* ADR-040: 実効値は config-foundation の防御プリセット解決を経由して決定する。
|
|
11
|
+
* preset 定義(presets/*.json の quickMode)が実効値の所在であり、
|
|
12
|
+
* 下の DEFAULT_QUICK_MODE_CONFIG は preset 解決不能時の fail-open 用フォールバックである。
|
|
7
13
|
*/
|
|
8
14
|
|
|
9
|
-
import * as fs from
|
|
10
|
-
import * as path from
|
|
11
|
-
import {
|
|
15
|
+
import * as fs from "node:fs/promises";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import type {
|
|
18
|
+
HarnessConfigResolvedDocument,
|
|
19
|
+
HarnessConfigSourceDocument,
|
|
20
|
+
PresetId,
|
|
21
|
+
} from "../../../config-foundation/domain/harness-config.js";
|
|
22
|
+
import { PresetResolutionService } from "../../../config-foundation/domain/services/preset-resolution-service.js";
|
|
23
|
+
import { PresetDefinitionStore } from "../../../config-foundation/infrastructure/preset-definition-store.js";
|
|
24
|
+
import { QuickModeConfig } from "../../domain/value-objects/quick-mode-config.js";
|
|
12
25
|
|
|
13
26
|
export class HarnessConfigNotFoundError extends Error {
|
|
14
27
|
constructor(filePath: string) {
|
|
15
28
|
super(`phasegate.config.json not found: ${filePath}`);
|
|
16
|
-
this.name =
|
|
29
|
+
this.name = "HarnessConfigNotFoundError";
|
|
17
30
|
}
|
|
18
31
|
}
|
|
19
32
|
|
|
20
33
|
export class HarnessConfigParseError extends Error {
|
|
21
34
|
constructor(message: string) {
|
|
22
35
|
super(`Failed to parse phasegate.config.json: ${message}`);
|
|
23
|
-
this.name =
|
|
36
|
+
this.name = "HarnessConfigParseError";
|
|
24
37
|
}
|
|
25
38
|
}
|
|
26
39
|
|
|
40
|
+
/**
|
|
41
|
+
* preset 解決が使えない config(未知 preset / project 欠落 / 他セクション不正)でも
|
|
42
|
+
* Quick Mode 判定を止めないためのフォールバック。ADR-038 §3-1 の fail-open 原則に従い、
|
|
43
|
+
* preset 解決の導入によって新たな遮断経路を作らない。
|
|
44
|
+
*/
|
|
27
45
|
const DEFAULT_QUICK_MODE_CONFIG = {
|
|
28
|
-
allowedCategories: [
|
|
29
|
-
maintainedLayers: [
|
|
30
|
-
relaxedGates: [
|
|
46
|
+
allowedCategories: ["bugfix", "docs", "test", "config"],
|
|
47
|
+
maintainedLayers: ["L1", "L2-002", "L2-003", "L2-014", "L3-001"],
|
|
48
|
+
relaxedGates: ["L2-001", "L3-002", "L3-003", "L3-004", "L4"],
|
|
31
49
|
};
|
|
32
50
|
|
|
51
|
+
const PRESET_IDS: readonly PresetId[] = ["minimal", "standard", "strict"];
|
|
52
|
+
|
|
53
|
+
type RawQuickMode = HarnessConfigResolvedDocument["quickMode"];
|
|
54
|
+
|
|
55
|
+
function isPresetId(value: unknown): value is PresetId {
|
|
56
|
+
return typeof value === "string" && PRESET_IDS.includes(value as PresetId);
|
|
57
|
+
}
|
|
58
|
+
|
|
33
59
|
export class HarnessConfigQuickModeConfigAdapter {
|
|
34
60
|
private readonly configPath: string;
|
|
61
|
+
private readonly presetDefinitionStore: PresetDefinitionStore;
|
|
62
|
+
private readonly presetResolutionService: PresetResolutionService;
|
|
35
63
|
|
|
36
64
|
constructor(configPath?: string) {
|
|
37
|
-
this.configPath = configPath ?? path.resolve(process.cwd(),
|
|
65
|
+
this.configPath = configPath ?? path.resolve(process.cwd(), "phasegate.config.json");
|
|
66
|
+
this.presetDefinitionStore = new PresetDefinitionStore();
|
|
67
|
+
this.presetResolutionService = new PresetResolutionService();
|
|
38
68
|
}
|
|
39
69
|
|
|
40
70
|
async getQuickModeConfig(): Promise<QuickModeConfig> {
|
|
71
|
+
const parsed = await this.readSourceDocument();
|
|
72
|
+
const quickMode = this.resolveQuickMode(parsed);
|
|
73
|
+
|
|
74
|
+
return QuickModeConfig.create({
|
|
75
|
+
allowedCategories: quickMode?.allowedCategories ?? DEFAULT_QUICK_MODE_CONFIG.allowedCategories,
|
|
76
|
+
maintainedLayers: quickMode?.maintainedLayers ?? DEFAULT_QUICK_MODE_CONFIG.maintainedLayers,
|
|
77
|
+
relaxedGates: quickMode?.relaxedGates ?? DEFAULT_QUICK_MODE_CONFIG.relaxedGates,
|
|
78
|
+
fullModeRequiredWhen: quickMode?.fullModeRequiredWhen,
|
|
79
|
+
// WI-372 × WI-377 合流点: categoryOverrides は preset 解決経路と raw 縮退経路の
|
|
80
|
+
// 双方が返す `quickMode` から一様に読む。preset 定義は当該キーを宣言しないため
|
|
81
|
+
// deepMerge は source の宣言をそのまま引き継ぎ(base 側 undefined → override を clone)、
|
|
82
|
+
// preset 解決が例外で縮退した場合も raw の宣言がそのまま渡る。未設定時は空ルール
|
|
83
|
+
// = 現行分類のまま。enum 違反は QuickModeConfig.create が QuickModeConfigError を
|
|
84
|
+
// 投げ、この try/catch の外側なので fail-closed 経路へ表面化する。
|
|
85
|
+
categoryOverrides: quickMode?.categoryOverrides,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async readSourceDocument(): Promise<Record<string, unknown>> {
|
|
41
90
|
let content: string;
|
|
42
91
|
|
|
43
92
|
try {
|
|
44
|
-
content = await fs.readFile(this.configPath,
|
|
93
|
+
content = (await fs.readFile(this.configPath, "utf8")) as string;
|
|
45
94
|
} catch (err) {
|
|
46
95
|
const error = err as NodeJS.ErrnoException;
|
|
47
|
-
if (error.code ===
|
|
96
|
+
if (error.code === "ENOENT") {
|
|
48
97
|
throw new HarnessConfigNotFoundError(this.configPath);
|
|
49
98
|
}
|
|
50
99
|
throw err;
|
|
51
100
|
}
|
|
52
101
|
|
|
53
|
-
let parsed: Record<string, unknown>;
|
|
54
102
|
try {
|
|
55
|
-
|
|
103
|
+
return JSON.parse(content) as Record<string, unknown>;
|
|
56
104
|
} catch (err) {
|
|
57
105
|
throw new HarnessConfigParseError(err instanceof Error ? err.message : String(err));
|
|
58
106
|
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 防御プリセット解決を経由した quickMode を返す。
|
|
111
|
+
* preset が特定できない、または解決が失敗する config では raw の quickMode に縮退し、
|
|
112
|
+
* 未宣言キーは呼び出し側で DEFAULT_QUICK_MODE_CONFIG に補完される。
|
|
113
|
+
*/
|
|
114
|
+
private resolveQuickMode(parsed: Record<string, unknown>): Partial<RawQuickMode> | undefined {
|
|
115
|
+
const rawQuickMode = parsed["quickMode"] as Partial<RawQuickMode> | undefined;
|
|
116
|
+
const preset = (parsed["project"] as { preset?: unknown } | undefined)?.preset;
|
|
59
117
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
maintainedLayers?: string[];
|
|
63
|
-
relaxedGates?: string[];
|
|
64
|
-
fullModeRequiredWhen?: {
|
|
65
|
-
mixedCategories?: boolean;
|
|
66
|
-
newDomainFile?: boolean;
|
|
67
|
-
apiContractChange?: boolean;
|
|
68
|
-
};
|
|
69
|
-
} | undefined;
|
|
70
|
-
|
|
71
|
-
if (!quickMode) {
|
|
72
|
-
return QuickModeConfig.create(DEFAULT_QUICK_MODE_CONFIG);
|
|
118
|
+
if (!isPresetId(preset)) {
|
|
119
|
+
return rawQuickMode;
|
|
73
120
|
}
|
|
74
121
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
122
|
+
try {
|
|
123
|
+
const presetDefinitions = this.presetDefinitionStore.load();
|
|
124
|
+
const resolved = this.presetResolutionService.resolve(
|
|
125
|
+
parsed as unknown as HarnessConfigSourceDocument,
|
|
126
|
+
presetDefinitions[preset],
|
|
127
|
+
);
|
|
128
|
+
return resolved.quickMode;
|
|
129
|
+
} catch {
|
|
130
|
+
return rawQuickMode;
|
|
131
|
+
}
|
|
81
132
|
}
|
|
82
133
|
}
|
|
@@ -2,26 +2,69 @@
|
|
|
2
2
|
* @layer infrastructure
|
|
3
3
|
* @unit skill-quality
|
|
4
4
|
* @work-item-id WI-188
|
|
5
|
+
* @work-item-id WI-341
|
|
5
6
|
*/
|
|
6
|
-
import { readFile } from
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
import { SkillQualityError } from "../../domain/errors/skill-quality-error.js";
|
|
9
|
+
import type {
|
|
10
|
+
RequirementTestMatrix,
|
|
11
|
+
RequirementTestMatrixPort,
|
|
12
|
+
} from "../../domain/ports/requirement-test-matrix-port.js";
|
|
13
|
+
|
|
14
|
+
interface GeneratedMatrixAcMapping {
|
|
15
|
+
readonly acId: string;
|
|
16
|
+
readonly testReferences: readonly unknown[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface GeneratedMatrixStory {
|
|
20
|
+
readonly storyId: string;
|
|
21
|
+
readonly storyMappings: readonly GeneratedMatrixAcMapping[];
|
|
22
|
+
}
|
|
9
23
|
|
|
10
24
|
export class FileSystemRequirementTestMatrixAdapter implements RequirementTestMatrixPort {
|
|
11
|
-
constructor(private readonly matrixFilePath: string =
|
|
25
|
+
constructor(private readonly matrixFilePath: string = ".harness/requirement-test-matrix.json") {}
|
|
12
26
|
|
|
13
27
|
async read(storyId: string): Promise<RequirementTestMatrix> {
|
|
14
28
|
let raw: string;
|
|
15
29
|
try {
|
|
16
|
-
raw = await readFile(this.matrixFilePath,
|
|
30
|
+
raw = await readFile(this.matrixFilePath, "utf-8");
|
|
17
31
|
} catch {
|
|
18
|
-
throw new SkillQualityError(
|
|
32
|
+
throw new SkillQualityError(
|
|
33
|
+
"MATRIX_FILE_NOT_FOUND",
|
|
34
|
+
`requirement-test-matrix.json not found at ${this.matrixFilePath}`,
|
|
35
|
+
);
|
|
19
36
|
}
|
|
20
|
-
|
|
37
|
+
let data: Record<string, unknown>;
|
|
38
|
+
try {
|
|
39
|
+
data = JSON.parse(raw) as Record<string, unknown>;
|
|
40
|
+
} catch {
|
|
41
|
+
throw new SkillQualityError(
|
|
42
|
+
"MATRIX_FILE_NOT_FOUND",
|
|
43
|
+
`requirement-test-matrix.json could not be parsed at ${this.matrixFilePath}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (Array.isArray(data.stories)) {
|
|
48
|
+
const story = (data.stories as GeneratedMatrixStory[]).find((candidate) => candidate.storyId === storyId);
|
|
49
|
+
if (story === undefined) {
|
|
50
|
+
throw new SkillQualityError("STORY_NOT_FOUND", `Story ${storyId} not found in ${this.matrixFilePath}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const uncoveredIds = story.storyMappings
|
|
54
|
+
.filter((mapping) => !Array.isArray(mapping.testReferences) || mapping.testReferences.length === 0)
|
|
55
|
+
.map((mapping) => mapping.acId);
|
|
56
|
+
return {
|
|
57
|
+
storyId,
|
|
58
|
+
total: story.storyMappings.length,
|
|
59
|
+
covered: story.storyMappings.length - uncoveredIds.length,
|
|
60
|
+
uncoveredIds,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
21
64
|
const rootEntry = data as { total?: unknown; covered?: unknown; uncoveredIds?: unknown };
|
|
22
65
|
const explicitEntry = data[storyId];
|
|
23
66
|
if (explicitEntry === undefined && rootEntry.total === undefined) {
|
|
24
|
-
throw new SkillQualityError(
|
|
67
|
+
throw new SkillQualityError("STORY_NOT_FOUND", `Story ${storyId} not found in ${this.matrixFilePath}`);
|
|
25
68
|
}
|
|
26
69
|
const entry = (explicitEntry ?? data) as { total: number; covered: number; uncoveredIds: string[] };
|
|
27
70
|
return {
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* @layer presentation
|
|
3
3
|
* @unit skill-quality
|
|
4
4
|
* @work-item-id WI-188
|
|
5
|
+
* @work-item-id WI-341
|
|
5
6
|
*/
|
|
6
|
-
import type { CheckCoverageUseCase } from
|
|
7
|
+
import type { CheckCoverageUseCase } from "../../application/usecases/check-coverage-usecase.js";
|
|
8
|
+
import { SkillQualityError } from "../../domain/errors/skill-quality-error.js";
|
|
7
9
|
|
|
8
10
|
export interface CheckCoverageArgs {
|
|
9
11
|
storyId: string;
|
|
10
|
-
format?:
|
|
12
|
+
format?: "human" | "json";
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export class CheckCoverageHandler {
|
|
@@ -16,9 +18,9 @@ export class CheckCoverageHandler {
|
|
|
16
18
|
async handle(args: CheckCoverageArgs): Promise<{ exitCode: number; message: string }> {
|
|
17
19
|
try {
|
|
18
20
|
const output = await this.useCase.execute({ storyId: args.storyId });
|
|
19
|
-
const format = args.format ??
|
|
21
|
+
const format = args.format ?? "human";
|
|
20
22
|
|
|
21
|
-
if (format ===
|
|
23
|
+
if (format === "json") {
|
|
22
24
|
return { exitCode: output.meetsThreshold ? 0 : 1, message: JSON.stringify(output, null, 2) };
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -26,7 +28,7 @@ export class CheckCoverageHandler {
|
|
|
26
28
|
const codeRate = output.coverageReport.codeCoverage.lineCoverage.toFixed(1);
|
|
27
29
|
const msg = `Requirement coverage: ${reqRate}% (threshold: ${output.requirementThreshold}%)\nCode coverage: ${codeRate}% (threshold: ${output.codeThreshold}%)`;
|
|
28
30
|
|
|
29
|
-
if (output.skipped === true && output.skipReason ===
|
|
31
|
+
if (output.skipped === true && output.skipReason === "no-tests") {
|
|
30
32
|
return { exitCode: 0, message: `Coverage SKIPPED (no tests)\n${msg}` };
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -35,7 +37,12 @@ export class CheckCoverageHandler {
|
|
|
35
37
|
}
|
|
36
38
|
return { exitCode: 1, message: `Coverage FAILED\n${msg}` };
|
|
37
39
|
} catch (err) {
|
|
38
|
-
|
|
40
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
41
|
+
if (args.format === "json") {
|
|
42
|
+
const code = err instanceof SkillQualityError ? err.code : "UNEXPECTED_ERROR";
|
|
43
|
+
return { exitCode: 2, message: JSON.stringify({ error: { code, message } }, null, 2) };
|
|
44
|
+
}
|
|
45
|
+
return { exitCode: 2, message: `Error: ${message}` };
|
|
39
46
|
}
|
|
40
47
|
}
|
|
41
48
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @unit traceability-model
|
|
2
2
|
// @layer domain
|
|
3
|
+
// @work-item-id WI-337
|
|
3
4
|
/**
|
|
4
5
|
* WorkItemFrontmatter — 設計文書 frontmatter から抽出した WI メタデータ(H03-04 / ISSUE-026 Phase A-2)。
|
|
5
6
|
*
|
|
@@ -8,7 +9,7 @@
|
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
export type WorkItemType = 'story' | 'issue' | 'fix' | 'refactor' | 'chore';
|
|
11
|
-
export type WorkItemSeverity = 'trivial' | 'normal' | 'high';
|
|
12
|
+
export type WorkItemSeverity = 'trivial' | 'normal' | 'medium' | 'high' | 'critical' | 'major';
|
|
12
13
|
export type WorkItemStatus =
|
|
13
14
|
| 'drafted'
|
|
14
15
|
| 'reflected'
|
|
@@ -47,7 +48,10 @@ export const WORK_ITEM_TYPES: ReadonlySet<WorkItemType> = new Set([
|
|
|
47
48
|
export const WORK_ITEM_SEVERITIES: ReadonlySet<WorkItemSeverity> = new Set([
|
|
48
49
|
'trivial',
|
|
49
50
|
'normal',
|
|
51
|
+
'medium',
|
|
50
52
|
'high',
|
|
53
|
+
'critical',
|
|
54
|
+
'major',
|
|
51
55
|
]);
|
|
52
56
|
|
|
53
57
|
export const WORK_ITEM_STATUSES: ReadonlySet<WorkItemStatus> = new Set([
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @unit traceability-model
|
|
2
2
|
// @layer infrastructure
|
|
3
3
|
// @work-item-id WI-106
|
|
4
|
+
// @work-item-id WI-337
|
|
4
5
|
|
|
5
6
|
import { readdir, readFile } from "node:fs/promises";
|
|
6
7
|
import * as path from "node:path";
|
|
@@ -8,6 +9,7 @@ import type {
|
|
|
8
9
|
WorkItemIdentityEntry,
|
|
9
10
|
WorkItemIdentityPort,
|
|
10
11
|
} from "../../domain/ports/work-item-identity-port.js";
|
|
12
|
+
import { WorkItemFrontmatterValidationError } from "../../domain/value-objects/work-item-frontmatter.js";
|
|
11
13
|
import { parseWorkItemFrontmatter } from "../parsers/work-item-frontmatter-parser.js";
|
|
12
14
|
|
|
13
15
|
const WI_DIR_PATTERN = /^WI-\d+$/;
|
|
@@ -67,7 +69,10 @@ export class FileSystemWorkItemIdentityGateway implements WorkItemIdentityPort {
|
|
|
67
69
|
try {
|
|
68
70
|
const content = await readFile(path.join(this.rootDir, descriptionPath), "utf8");
|
|
69
71
|
return parseWorkItemFrontmatter(content)?.id ?? null;
|
|
70
|
-
} catch {
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error instanceof WorkItemFrontmatterValidationError) {
|
|
74
|
+
console.warn(`[phasegate] warning: ${descriptionPath} をスキップしました: ${error.message}`);
|
|
75
|
+
}
|
|
71
76
|
return null;
|
|
72
77
|
}
|
|
73
78
|
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// @unit traceability-model
|
|
2
2
|
// @layer infrastructure
|
|
3
3
|
// @work-item-id WI-126 / WI-140
|
|
4
|
+
// @work-item-id WI-337
|
|
4
5
|
|
|
5
6
|
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
6
7
|
import * as path from "node:path";
|
|
7
8
|
import type { WorkItemStatusPort } from "../../domain/ports/work-item-status-port.js";
|
|
8
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
type WorkItemFrontmatter,
|
|
11
|
+
WorkItemFrontmatterValidationError,
|
|
12
|
+
} from "../../domain/value-objects/work-item-frontmatter.js";
|
|
9
13
|
import type {
|
|
10
14
|
WorkItemStatusApplyResult,
|
|
11
15
|
WorkItemStatusInput,
|
|
@@ -52,7 +56,16 @@ export class FileSystemWorkItemStatusGateway implements WorkItemStatusPort {
|
|
|
52
56
|
const inputs: WorkItemStatusInput[] = [];
|
|
53
57
|
for (const entry of entries) {
|
|
54
58
|
const content = await readFile(path.join(this.rootDir, entry.descriptionPath), "utf8");
|
|
55
|
-
|
|
59
|
+
let frontmatter: WorkItemFrontmatter | null;
|
|
60
|
+
try {
|
|
61
|
+
frontmatter = parseWorkItemFrontmatter(content);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error instanceof WorkItemFrontmatterValidationError) {
|
|
64
|
+
console.warn(`[phasegate] warning: ${entry.descriptionPath} をスキップしました: ${error.message}`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
56
69
|
if (frontmatter === null) continue;
|
|
57
70
|
|
|
58
71
|
const aliases = this.aliasesFor(frontmatter);
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* AggregateValidationResultsUseCase — H08-05: バリデータ結果統合集約
|
|
6
6
|
*/
|
|
7
|
-
import
|
|
8
|
-
import type { AggregateResultsInput } from
|
|
9
|
-
import type { AggregatedValidationReport } from
|
|
7
|
+
import { isEffectivelyPassed } from "../../domain/services/effective-severity-policy.js";
|
|
8
|
+
import type { AggregateResultsInput } from "../dto/aggregate-results-input.js";
|
|
9
|
+
import type { AggregatedValidationReport } from "../dto/aggregated-validation-report.js";
|
|
10
10
|
|
|
11
11
|
export class AggregateValidationResultsUseCase {
|
|
12
12
|
execute(input: AggregateResultsInput): AggregatedValidationReport {
|
|
@@ -24,7 +24,7 @@ export class AggregateValidationResultsUseCase {
|
|
|
24
24
|
}[] = [];
|
|
25
25
|
let totalErrors = 0;
|
|
26
26
|
let totalWarnings = 0;
|
|
27
|
-
const errorsByLayer: Record<
|
|
27
|
+
const errorsByLayer: Record<"L2" | "L3" | "L4", number> = { L2: 0, L3: 0, L4: 0 };
|
|
28
28
|
|
|
29
29
|
for (const result of results) {
|
|
30
30
|
if (result.skipped) {
|
|
@@ -32,12 +32,9 @@ export class AggregateValidationResultsUseCase {
|
|
|
32
32
|
continue;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
// ADR-017: warning-only validator fail は failOnWarning=false で overall PASS、
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const isEmptyFail = !result.passed && result.errors.length === 0;
|
|
39
|
-
const hasFail =
|
|
40
|
-
!result.passed && (isEmptyFail || hasNonWarningError || (failOnWarning && hasWarnings));
|
|
35
|
+
// ADR-017 / WI-332: warning-only validator fail は failOnWarning=false で overall PASS、
|
|
36
|
+
// true で従来挙動 (FAIL)。実効判定は domain の共有実装 isEffectivelyPassed に一本化。
|
|
37
|
+
const hasFail = !isEffectivelyPassed(result, failOnWarning);
|
|
41
38
|
|
|
42
39
|
if (hasFail) {
|
|
43
40
|
failedValidators++;
|
|
@@ -54,7 +51,7 @@ export class AggregateValidationResultsUseCase {
|
|
|
54
51
|
};
|
|
55
52
|
allErrors.push(errorEntry);
|
|
56
53
|
|
|
57
|
-
if (error.severity ===
|
|
54
|
+
if (error.severity === "warning") {
|
|
58
55
|
totalWarnings++;
|
|
59
56
|
} else {
|
|
60
57
|
totalErrors++;
|
|
@@ -62,9 +59,9 @@ export class AggregateValidationResultsUseCase {
|
|
|
62
59
|
|
|
63
60
|
// レイヤー別集計
|
|
64
61
|
const code = error.code;
|
|
65
|
-
if (code.startsWith(
|
|
66
|
-
else if (code.startsWith(
|
|
67
|
-
else if (code.startsWith(
|
|
62
|
+
if (code.startsWith("L2-")) errorsByLayer.L2++;
|
|
63
|
+
else if (code.startsWith("L3-")) errorsByLayer.L3++;
|
|
64
|
+
else if (code.startsWith("L4-")) errorsByLayer.L4++;
|
|
68
65
|
}
|
|
69
66
|
}
|
|
70
67
|
|
|
@@ -171,6 +171,9 @@ export class RunL3ValidatorsUseCase {
|
|
|
171
171
|
severity: "error",
|
|
172
172
|
message: `カバレッジ不足: 現在値 ${coverageData.overallCoverage}%、不足 ${threshold - coverageData.overallCoverage}%`,
|
|
173
173
|
suggestion: `テストカバレッジを ${threshold}% 以上に引き上げてください`,
|
|
174
|
+
// WI-335: 閾値未達の解消はテスト追加(AI/人間の判断)が必要。
|
|
175
|
+
// L3-003 の registry 既定は mechanical だが、この finding は機械適用不能なので明示的に上書きする。
|
|
176
|
+
remediationType: "ai-assisted",
|
|
174
177
|
},
|
|
175
178
|
],
|
|
176
179
|
0,
|
|
@@ -190,8 +193,13 @@ export class RunL3ValidatorsUseCase {
|
|
|
190
193
|
code: "L3-003",
|
|
191
194
|
severity: "error",
|
|
192
195
|
message: `coverageThreshold=${threshold}% が設定されていますがカバレッジレポートが見つかりません(テストをカバレッジ付きで実行してください)`,
|
|
196
|
+
// WI-335: 選択肢 (b)「layers.L3.coverageThreshold を 0 に設定する」は config 編集のみで
|
|
197
|
+
// 完結する機械適用可能な opt-out(WI-317 / github#37)なので mechanical と宣言する。
|
|
198
|
+
// この文言(layers.L3.coverageThreshold を 0)を変える場合は remediation-round-trip
|
|
199
|
+
// テスト(機械適用器が同じ文言を解析する)が fail する。
|
|
193
200
|
suggestion:
|
|
194
201
|
'次のいずれかで解消してください: (a) テストをカバレッジ付きで実行してレポートを生成する(例: vitest --coverage)、(b) カバレッジゲートを opt-out するなら config の layers.L3.coverageThreshold を 0 に設定する、(c) 非 JS/TS プロジェクトなら project.languages を宣言する(例: ["python"]。L3-003 自体が unsupported-language SKIP になる)',
|
|
202
|
+
remediationType: "mechanical",
|
|
195
203
|
},
|
|
196
204
|
],
|
|
197
205
|
0,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @unit validator-system
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-332
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* effective-severity-policy — 実効 severity 判定の単一ソース (ADR-017 / ADR-021)。
|
|
7
|
+
*
|
|
8
|
+
* validator 1件が「実質 pass」かを severity-aware に判定するルール。
|
|
9
|
+
* WI-332 以前は同じ判定式が 3 箇所(validate 集約 usecase / harness-api CiCheckResult /
|
|
10
|
+
* pre-commit の手動集約)に複製・乖離しており、#38(complete-check だけ warning-only で
|
|
11
|
+
* exit 1)型の回帰を招いた。以後、実効判定は必ずこの関数を経由すること。
|
|
12
|
+
*
|
|
13
|
+
* 判定ルール(ADR-017 Decision の集計セマンティクスそのもの):
|
|
14
|
+
* - skipped: 実質 pass
|
|
15
|
+
* - passed=true: 実質 pass
|
|
16
|
+
* - passed=false かつ error severity(!= warning)を含む: fail
|
|
17
|
+
* - passed=false かつ errors=[](severity 判定不能): 安全側に倒して fail
|
|
18
|
+
* - passed=false かつ warning のみ: failOnWarning=false(既定)で実質 pass、true で fail
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 判定に必要な最小構造。validator-system の ValidationResultContract と
|
|
23
|
+
* harness-api の ValidatorCheckItem の双方が構造的に満たす。
|
|
24
|
+
*/
|
|
25
|
+
export interface EffectiveSeverityCheckItem {
|
|
26
|
+
readonly passed: boolean;
|
|
27
|
+
readonly skipped?: boolean;
|
|
28
|
+
readonly errors?: readonly { readonly severity: string }[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isEffectivelyPassed(item: EffectiveSeverityCheckItem, failOnWarning = false): boolean {
|
|
32
|
+
if (item.skipped || item.passed) return true;
|
|
33
|
+
const errors = item.errors ?? [];
|
|
34
|
+
const hasNonWarningError = errors.some((e) => e.severity !== "warning");
|
|
35
|
+
const hasWarnings = errors.some((e) => e.severity === "warning");
|
|
36
|
+
const isEmptyFail = errors.length === 0;
|
|
37
|
+
const hasFail = isEmptyFail || hasNonWarningError || (failOnWarning && hasWarnings);
|
|
38
|
+
return !hasFail;
|
|
39
|
+
}
|