phasegate 0.264.0 → 0.315.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/docs/ADR/017-warning-severity-aggregation.md +17 -0
  3. package/docs/ADR/038-config-state-operation-permission-policy.md +78 -0
  4. package/docs/guide/installation.md +1 -1
  5. package/docs/guide/layer-model.md +2 -0
  6. package/docs/guide/quick-vs-full-mode.md +28 -3
  7. package/docs/guide/troubleshooting.md +37 -0
  8. package/docs/templates/agent-context/CLAUDE.md.template.md +6 -6
  9. package/docs/templates/ci/aidlc-gate.yml +22 -4
  10. package/package.json +2 -2
  11. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +98 -18
  12. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +25 -3
  13. package/scripts/harness/agent-integration/infrastructure/adapters/file-system-full-mode-session-query-adapter.ts +75 -23
  14. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +45 -27
  15. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
  16. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +51 -6
  17. package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
  18. package/scripts/harness/ci-governance/composition-root.ts +2 -2
  19. package/scripts/harness/ci-governance/domain/services/claude-md-composer.ts +20 -11
  20. package/scripts/harness/ci-governance/presentation/handlers/check-repetition-handler.ts +10 -2
  21. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +5 -1
  22. package/scripts/harness/config-foundation/domain/harness-config.ts +10 -7
  23. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +4 -1
  24. package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
  25. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
  26. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
  27. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -1
  28. package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +27 -18
  29. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +1 -8
  30. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +1 -8
  31. package/scripts/harness/harness-api/domain/ports/config-query-port.ts +11 -1
  32. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +135 -87
  33. package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +31 -21
  34. package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +2 -22
  35. package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +23 -8
  36. package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +4 -4
  37. package/scripts/harness/harness-api/infrastructure/adapters/harness-config-query-adapter.ts +79 -34
  38. package/scripts/harness/harness-error/application/dto/create-harness-error-input.ts +3 -1
  39. package/scripts/harness/harness-error/application/dto/harness-error-contract.ts +3 -1
  40. package/scripts/harness/harness-error/application/mappers/harness-error-contract-mapper.ts +9 -17
  41. package/scripts/harness/harness-error/application/usecases/create-harness-error-use-case.ts +7 -7
  42. package/scripts/harness/harness-error/domain/services/harness-error-factory.ts +30 -33
  43. package/scripts/harness/harness-error/domain/value-objects/error-definition.ts +28 -17
  44. package/scripts/harness/harness-error/domain/value-objects/harness-error.ts +30 -9
  45. package/scripts/harness/harness-error/domain/value-objects/remediation-type.ts +30 -0
  46. package/scripts/harness/harness-error/infrastructure/registry/l2-error-definitions.ts +44 -29
  47. package/scripts/harness/harness-error/infrastructure/registry/l3-error-definitions.ts +44 -27
  48. package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +47 -32
  49. package/scripts/harness/installation/application/checks/claude-context-missing-check.ts +13 -7
  50. package/scripts/harness/installation/application/checks/config-status-check.ts +52 -0
  51. package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +6 -0
  52. package/scripts/harness/installation/application/ports/config-status-probe-port.ts +9 -0
  53. package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +30 -8
  54. package/scripts/harness/installation/application/usecases/run-install.ts +234 -53
  55. package/scripts/harness/installation/application/usecases/run-reconcile.ts +311 -70
  56. package/scripts/harness/installation/composition-root.ts +13 -3
  57. package/scripts/harness/installation/domain/check-id.ts +2 -0
  58. package/scripts/harness/installation/domain/config-status.ts +17 -0
  59. package/scripts/harness/installation/domain/deployment-manifest.ts +43 -0
  60. package/scripts/harness/installation/domain/ports/heuristic-check.ts +10 -1
  61. package/scripts/harness/installation/infrastructure/adapters/config-status-probe-adapter.ts +79 -0
  62. package/scripts/harness/installation/presentation/cli/doctor-handler.ts +6 -1
  63. package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +19 -5
  64. package/scripts/harness/integrations/pre-commit.ts +17 -3
  65. package/scripts/harness/main.ts +83 -15
  66. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +60 -32
  67. package/scripts/harness/phase2-extensions/presentation/handlers/check-freshness-handler.ts +17 -9
  68. package/scripts/harness/quick-mode/application/ports/file-existence-port.ts +15 -0
  69. package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +60 -22
  70. package/scripts/harness/quick-mode/composition-root.ts +25 -15
  71. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +72 -3
  72. package/scripts/harness/quick-mode/infrastructure/adapters/fs-file-existence-adapter.ts +38 -0
  73. package/scripts/harness/skill-quality/infrastructure/adapters/file-system-requirement-test-matrix-adapter.ts +51 -8
  74. package/scripts/harness/skill-quality/presentation/handlers/check-coverage-handler.ts +13 -6
  75. package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +5 -1
  76. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-identity-gateway.ts +6 -1
  77. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +15 -2
  78. package/scripts/harness/validator-system/application/use-cases/aggregate-validation-results-usecase.ts +11 -14
  79. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +32 -7
  80. package/scripts/harness/validator-system/composition-root.ts +4 -1
  81. package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
  82. package/scripts/harness/validator-system/domain/services/effective-severity-policy.ts +39 -0
  83. package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +6 -4
  84. package/scripts/harness/validator-system/domain/value-objects/drift-report.ts +12 -7
  85. package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +11 -3
  86. package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +20 -17
  87. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +89 -3
  88. package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +49 -20
  89. package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +35 -14
  90. package/scripts/harness/validator-system/infrastructure/adapters/traceability-metadata-policy-adapter.ts +13 -8
  91. package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +19 -10
  92. package/skills/quick-implementor/SKILL.md +19 -0
@@ -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 'node:fs/promises';
7
- import type { RequirementTestMatrixPort, RequirementTestMatrix } from '../../domain/ports/requirement-test-matrix-port.js';
8
- import { SkillQualityError } from '../../domain/errors/skill-quality-error.js';
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 = '.harness/requirement-test-matrix.json') {}
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, 'utf-8');
30
+ raw = await readFile(this.matrixFilePath, "utf-8");
17
31
  } catch {
18
- throw new SkillQualityError('MATRIX_FILE_NOT_FOUND', `requirement-test-matrix.json not found at ${this.matrixFilePath}`);
32
+ throw new SkillQualityError(
33
+ "MATRIX_FILE_NOT_FOUND",
34
+ `requirement-test-matrix.json not found at ${this.matrixFilePath}`,
35
+ );
19
36
  }
20
- const data = JSON.parse(raw) as Record<string, unknown>;
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('STORY_NOT_FOUND', `Story ${storyId} not found in ${this.matrixFilePath}`);
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 '../../application/usecases/check-coverage-usecase.js';
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?: 'human' | 'json';
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 ?? 'human';
21
+ const format = args.format ?? "human";
20
22
 
21
- if (format === 'json') {
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 === 'no-tests') {
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
- return { exitCode: 2, message: `Error: ${err instanceof Error ? err.message : String(err)}` };
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 type { WorkItemFrontmatter } from "../../domain/value-objects/work-item-frontmatter.js";
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
- const frontmatter = parseWorkItemFrontmatter(content);
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 type { ValidationResultContract } from '../dto/validation-result-contract.js';
8
- import type { AggregateResultsInput } from '../dto/aggregate-results-input.js';
9
- import type { AggregatedValidationReport } from '../dto/aggregated-validation-report.js';
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<'L2' | 'L3' | 'L4', number> = { L2: 0, L3: 0, L4: 0 };
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、true で従来挙動 (FAIL)
36
- const hasNonWarningError = result.errors.some((e) => e.severity !== 'warning');
37
- const hasWarnings = result.errors.some((e) => e.severity === 'warning');
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 === 'warning') {
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('L2-')) errorsByLayer.L2++;
66
- else if (code.startsWith('L3-')) errorsByLayer.L3++;
67
- else if (code.startsWith('L4-')) errorsByLayer.L4++;
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
 
@@ -3,6 +3,8 @@
3
3
  * @unit validator-system
4
4
  * @work-item-id WI-212
5
5
  * @work-item-id WI-302
6
+ * @work-item-id WI-317
7
+ * @work-item-id WI-324
6
8
  *
7
9
  * RunL3ValidatorsUseCase — H08-02: L3バリデータ実行
8
10
  */
@@ -22,8 +24,8 @@ import {
22
24
  type ValidatorExecutionService,
23
25
  } from "../../domain/services/validator-execution-service.js";
24
26
  import { ValidatorLanguageCapabilityService } from "../../domain/services/validator-language-capability-service.js";
25
- import { WorldConstraintRederivationService } from "../../domain/services/world-constraint-rederivation-service.js";
26
27
  import type { ValidatorRegistry } from "../../domain/services/validator-registry.js";
28
+ import { WorldConstraintRederivationService } from "../../domain/services/world-constraint-rederivation-service.js";
27
29
  import type { HarnessErrorLike } from "../../domain/value-objects/validation-result.js";
28
30
  import { ValidationResult } from "../../domain/value-objects/validation-result.js";
29
31
  import { ValidatorId } from "../../domain/value-objects/validator-id.js";
@@ -133,8 +135,10 @@ export class RunL3ValidatorsUseCase {
133
135
  );
134
136
 
135
137
  // L3-003: カバレッジ判定(カバレッジゲートはオプトイン)
136
- // - coverageThreshold 未設定 → SKIP(透過的に判定をスキップ。getCoverage() は呼ばない)
137
- // - coverageThreshold 設定あり → getCoverage() try/catch で包み FAIL-CLOSED で判定する
138
+ // - coverageThreshold 未設定 or 0 → SKIP(透過的に判定をスキップ。getCoverage() は呼ばない)
139
+ // 0 は正規の opt-out(ドメイン VO L3Config.hasCoverageGate() の「threshold > 0 でのみ有効」と整合。
140
+ // minimal preset の coverageThreshold: 0 もこの opt-out 意図。WI-317 / github#37)
141
+ // - coverageThreshold > 0 → getCoverage() を try/catch で包み FAIL-CLOSED で判定する
138
142
  // - 閾値未満 → FAIL / 閾値以上 → PASS
139
143
  // - レポート不在などで getCoverage() が失敗 → FAIL(合格扱いにしない)
140
144
  // このブロックは例外を送出せず、L3-003 の per-validator 結果のみを差し替える。
@@ -145,12 +149,12 @@ export class RunL3ValidatorsUseCase {
145
149
  const l3003Id = ValidatorId.create("L3-003");
146
150
  const threshold = layerConfig.getThreshold("coverageThreshold");
147
151
 
148
- if (threshold === null) {
152
+ if (threshold === null || threshold === 0) {
149
153
  overrideMap.set(
150
154
  "L3-003",
151
155
  ValidationResult.skipWithReason(
152
156
  l3003Id,
153
- "coverageThreshold が未設定のためカバレッジ判定をスキップ(カバレッジゲートはオプトイン)",
157
+ "coverageThreshold が未設定/0 のためカバレッジ判定をスキップ(カバレッジゲートはオプトイン。0 で opt-out)",
154
158
  ),
155
159
  );
156
160
  } else {
@@ -167,6 +171,9 @@ export class RunL3ValidatorsUseCase {
167
171
  severity: "error",
168
172
  message: `カバレッジ不足: 現在値 ${coverageData.overallCoverage}%、不足 ${threshold - coverageData.overallCoverage}%`,
169
173
  suggestion: `テストカバレッジを ${threshold}% 以上に引き上げてください`,
174
+ // WI-335: 閾値未達の解消はテスト追加(AI/人間の判断)が必要。
175
+ // L3-003 の registry 既定は mechanical だが、この finding は機械適用不能なので明示的に上書きする。
176
+ remediationType: "ai-assisted",
170
177
  },
171
178
  ],
172
179
  0,
@@ -186,7 +193,13 @@ export class RunL3ValidatorsUseCase {
186
193
  code: "L3-003",
187
194
  severity: "error",
188
195
  message: `coverageThreshold=${threshold}% が設定されていますがカバレッジレポートが見つかりません(テストをカバレッジ付きで実行してください)`,
189
- suggestion: "vitest --coverage 等でカバレッジレポートを生成してから再実行してください",
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 する。
200
+ suggestion:
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",
190
203
  },
191
204
  ],
192
205
  0,
@@ -224,7 +237,19 @@ export class RunL3ValidatorsUseCase {
224
237
  const policyResult = await this.acCoveragePolicyPort.checkCoverage({
225
238
  matrixFilePath: input.requirementMatrixPath,
226
239
  });
227
- if (!policyResult.passed) {
240
+ // WI-324: フレッシュプロジェクト(story 未作成・matrix 未生成)は policy adapter が
241
+ // skipped=true を返すので、L3-003 の opt-out と同じ表現で skipWithReason に変換する。
242
+ // story が存在するのに matrix が不在の場合は従来どおり fail-closed(下の分岐)。
243
+ if (policyResult.skipped) {
244
+ overrideMap.set(
245
+ "L3-004",
246
+ ValidationResult.skipWithReason(
247
+ ValidatorId.create("L3-004"),
248
+ policyResult.skipReason ??
249
+ "story 未作成のため L3-004 をスキップ(story 作成後に requirement-test-matrix を生成すると有効化されます)",
250
+ ),
251
+ );
252
+ } else if (!policyResult.passed) {
228
253
  overrideMap.set("L3-004", ValidationResult.fail(ValidatorId.create("L3-004"), [...policyResult.errors], 0));
229
254
  }
230
255
  }
@@ -8,6 +8,7 @@
8
8
  * @work-item-id WI-301
9
9
  * @work-item-id WI-302
10
10
  * @work-item-id WI-305
11
+ * @work-item-id WI-322
11
12
  */
12
13
 
13
14
  import { join } from "node:path";
@@ -73,7 +74,9 @@ const DEFAULT_CONFIG = {
73
74
  L3: {
74
75
  enabled: true,
75
76
  validators: ["L3-001", "L3-002", "L3-003", "L3-004", "L3-006", "L3-007"],
76
- coverageThreshold: 90,
77
+ // WI-322 (github#37 残課題): カバレッジゲートはオプトイン(WI-317)。config なし環境の
78
+ // fallback で 90% を強制しない。0 = 正規の opt-out(L3-003 は透過 SKIP になる)。
79
+ coverageThreshold: 0,
77
80
  bundleSizeLimit: 512000,
78
81
  requirementMatrixPath: ".harness/requirement-test-matrix.json",
79
82
  },
@@ -4,11 +4,20 @@
4
4
  *
5
5
  * AcCoveragePolicyPort — nyquist-validation AcCoverageGatePolicy(L3-004)
6
6
  */
7
- import type { HarnessErrorLike } from '../value-objects/validation-result.js';
7
+ import type { HarnessErrorLike } from "../value-objects/validation-result.js";
8
8
 
9
9
  export interface AcCoveragePolicyPort {
10
10
  checkCoverage(context: { matrixFilePath?: string }): Promise<{
11
11
  passed: boolean;
12
12
  errors: readonly HarnessErrorLike[];
13
+ /**
14
+ * WI-324: フレッシュプロジェクト(story 未作成・matrix 未生成)では L3-004 を
15
+ * fail-closed ではなく SKIP として扱う。true の場合、上位(RunL3ValidatorsUseCase)は
16
+ * skipReason 付きの skipWithReason 結果へ変換する。省略時(既存実装・既存モック)は
17
+ * 従来どおり passed/errors のみで判定される(後方互換 optional)。
18
+ */
19
+ skipped?: boolean;
20
+ /** skipped=true のときの人間可読なスキップ理由。 */
21
+ skipReason?: string;
13
22
  }>;
14
23
  }
@@ -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
+ }
@@ -6,7 +6,7 @@
6
6
  * ConsistencyReport 値オブジェクト
7
7
  * 設計文書間のレイヤー整合性検証結果VO(L4-002専用)
8
8
  */
9
- import type { HarnessErrorLike } from './validation-result.js';
9
+ import type { HarnessErrorLike } from "./validation-result.js";
10
10
 
11
11
  export interface MismatchPair {
12
12
  readonly expected: string;
@@ -48,10 +48,12 @@ export class ConsistencyReport {
48
48
  // ADR-017 / WI-094: error catalog の defaultSeverity: warning と整合
49
49
  toHarnessErrors(): readonly HarnessErrorLike[] {
50
50
  return this.mismatchPairs.map((pair) => ({
51
- code: { value: 'L4-002', toString: () => 'L4-002' },
52
- severity: { value: 'warning', toString: () => 'warning' },
51
+ code: { value: "L4-002", toString: () => "L4-002" },
52
+ severity: { value: "warning", toString: () => "warning" },
53
53
  message: `レイヤー整合性違反: expected "${pair.expected}" but got "${pair.actual}" at ${pair.location}`,
54
- suggestion: pair.nextAction ?? '設計文書間のレイヤー依存方向を統一してください',
54
+ suggestion: pair.nextAction ?? "設計文書間のレイヤー依存方向を統一してください",
55
+ // WI-335: 文書間整合の回復は「どちらの記述が正か」の判断を伴う(ai-assisted)。
56
+ remediationType: "ai-assisted" as const,
55
57
  }));
56
58
  }
57
59
  }
@@ -5,9 +5,9 @@
5
5
  * DriftReport 値オブジェクト
6
6
  * 設計文書とコード実装の双方向乖離検出結果VO(L4-001専用)
7
7
  */
8
- import type { HarnessErrorLike } from './validation-result.js';
8
+ import type { HarnessErrorLike } from "./validation-result.js";
9
9
 
10
- export type DriftDirection = 'design→code' | 'code→design';
10
+ export type DriftDirection = "design→code" | "code→design";
11
11
 
12
12
  export interface DriftReportProps {
13
13
  readonly direction: DriftDirection;
@@ -29,14 +29,16 @@ export class DriftReport {
29
29
  this.direction = props.direction;
30
30
  this.unitName = props.unitName;
31
31
  this.element = props.element;
32
- this.recommendation = props.recommendation ?? (props.description ?? '');
32
+ this.recommendation = props.recommendation ?? props.description ?? "";
33
33
  this.location = Object.freeze(props.location ?? {});
34
34
  Object.freeze(this);
35
35
  }
36
36
 
37
37
  static create(props: DriftReportProps): DriftReport {
38
- if (props.direction !== 'design→code' && props.direction !== 'code→design') {
39
- throw new Error(`Invalid DriftReport direction: "${props.direction}". Must be "design→code" or "code→design" (INV-10)`);
38
+ if (props.direction !== "design→code" && props.direction !== "code→design") {
39
+ throw new Error(
40
+ `Invalid DriftReport direction: "${props.direction}". Must be "design→code" or "code→design" (INV-10)`,
41
+ );
40
42
  }
41
43
  return new DriftReport(props);
42
44
  }
@@ -44,10 +46,13 @@ export class DriftReport {
44
46
  toHarnessError(): HarnessErrorLike {
45
47
  // ADR-017 / WI-094: error catalog の defaultSeverity: warning と整合
46
48
  return {
47
- code: { value: 'L4-001', toString: () => 'L4-001' },
48
- severity: { value: 'warning', toString: () => 'warning' },
49
+ code: { value: "L4-001", toString: () => "L4-001" },
50
+ severity: { value: "warning", toString: () => "warning" },
49
51
  message: `乖離検出 [${this.direction}] Unit: ${this.unitName}, Element: ${this.element}`,
50
52
  suggestion: this.recommendation,
53
+ // WI-335: design drift の解消は設計意図の理解が必要で機械適用不能。AI が設計文書を
54
+ // 読んで自己修正できる分類(ai-assisted)。
55
+ remediationType: "ai-assisted",
51
56
  };
52
57
  }
53
58
 
@@ -5,7 +5,7 @@
5
5
  * ValidationResult 値オブジェクト
6
6
  * バリデータ実行結果のスナップショット(不変)
7
7
  */
8
- import type { ValidatorId } from './validator-id.js';
8
+ import type { ValidatorId } from "./validator-id.js";
9
9
 
10
10
  /** HarnessError の最小互換型(harness-error Unit の HarnessError との疎結合) */
11
11
  export interface HarnessErrorLike {
@@ -13,6 +13,12 @@ export interface HarnessErrorLike {
13
13
  readonly severity: { readonly value?: string; toString(): string };
14
14
  readonly message: string;
15
15
  readonly suggestion: string;
16
+ /**
17
+ * WI-335: suggestion の修復方式分類。未設定は 'manual' 扱い(機械適用可能と過剰宣言しない)。
18
+ * 'mechanical' を宣言したエラーは remediation-round-trip テストで
19
+ * 「エラー → suggestion を機械適用 → 再実行 → pass」が CI 保証される。
20
+ */
21
+ readonly remediationType?: "mechanical" | "ai-assisted" | "manual";
16
22
  [key: string]: unknown;
17
23
  }
18
24
 
@@ -49,10 +55,12 @@ export class ValidationResult {
49
55
  throw new Error(`ValidationResult durationMs must be >= 0 (got: ${props.durationMs})`);
50
56
  }
51
57
  if (props.passed && props.errors.length > 0) {
52
- throw new Error('ValidationResult invariant violation: passed=true but errors is not empty (INV-5)');
58
+ throw new Error("ValidationResult invariant violation: passed=true but errors is not empty (INV-5)");
53
59
  }
54
60
  if (props.skipped && (!props.passed || props.errors.length > 0)) {
55
- throw new Error('ValidationResult invariant violation: skipped=true requires passed=true and empty errors (INV-8)');
61
+ throw new Error(
62
+ "ValidationResult invariant violation: skipped=true requires passed=true and empty errors (INV-8)",
63
+ );
56
64
  }
57
65
  return new ValidationResult(props);
58
66
  }
@@ -5,11 +5,12 @@
5
5
  *
6
6
  * FileSystemSecurityPatternScannerAdapter — SecurityPatternScannerPort実装
7
7
  */
8
- import type { SecurityPatternScannerPort } from '../../domain/ports/security-pattern-scanner-port.js';
9
- import type { HarnessErrorLike } from '../../domain/value-objects/validation-result.js';
10
- import { readFile } from 'node:fs/promises';
11
8
 
12
- const ALLOWLIST_MARKER = 'phasegate-allow-secret-fixture';
9
+ import { readFile } from "node:fs/promises";
10
+ import type { SecurityPatternScannerPort } from "../../domain/ports/security-pattern-scanner-port.js";
11
+ import type { HarnessErrorLike } from "../../domain/value-objects/validation-result.js";
12
+
13
+ const ALLOWLIST_MARKER = "phasegate-allow-secret-fixture";
13
14
 
14
15
  interface SecurityPattern {
15
16
  readonly ruleId: string;
@@ -18,15 +19,15 @@ interface SecurityPattern {
18
19
  }
19
20
 
20
21
  const SECURITY_PATTERNS: readonly SecurityPattern[] = Object.freeze([
21
- { ruleId: 'secret.openai', pattern: /\b(?:sk|rk|sess)-[a-zA-Z0-9_-]{20,}\b/g, description: 'OpenAI token family' },
22
- { ruleId: 'secret.github', pattern: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g, description: 'GitHub token family' },
23
- { ruleId: 'secret.aws-access-key', pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, description: 'AWS access key id' },
24
- { ruleId: 'secret.npm', pattern: /\bnpm_[A-Za-z0-9]{24,}\b/g, description: 'npm token family' },
25
- { ruleId: 'secret.slack', pattern: /\bxox[abprs]-[A-Za-z0-9-]{20,}\b/g, description: 'Slack token family' },
22
+ { ruleId: "secret.openai", pattern: /\b(?:sk|rk|sess)-[a-zA-Z0-9_-]{20,}\b/g, description: "OpenAI token family" },
23
+ { ruleId: "secret.github", pattern: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g, description: "GitHub token family" },
24
+ { ruleId: "secret.aws-access-key", pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, description: "AWS access key id" },
25
+ { ruleId: "secret.npm", pattern: /\bnpm_[A-Za-z0-9]{24,}\b/g, description: "npm token family" },
26
+ { ruleId: "secret.slack", pattern: /\bxox[abprs]-[A-Za-z0-9-]{20,}\b/g, description: "Slack token family" },
26
27
  {
27
- ruleId: 'secret.keyword-context',
28
+ ruleId: "secret.keyword-context",
28
29
  pattern: /\b(?:API_KEY|api_key|apikey|password|PASSWORD|passwd|secret|token)\b\s*[:=]\s*["'][^"']{8,}["']/g,
29
- description: 'keyword-context secret',
30
+ description: "keyword-context secret",
30
31
  },
31
32
  ]);
32
33
 
@@ -39,8 +40,8 @@ export class FileSystemSecurityPatternScannerAdapter implements SecurityPatternS
39
40
 
40
41
  for (const filePath of targetPaths) {
41
42
  try {
42
- const content = await readFile(filePath, 'utf-8');
43
- const lines = content.split('\n');
43
+ const content = await readFile(filePath, "utf-8");
44
+ const lines = content.split("\n");
44
45
  lines.forEach((line, idx) => {
45
46
  // WI-120: allowlist は行/領域スコープ。以前はファイル内のどこかに
46
47
  // マーカーが 1 つでもあればファイル全体をスキップしていたため、同一
@@ -54,12 +55,14 @@ export class FileSystemSecurityPatternScannerAdapter implements SecurityPatternS
54
55
  pattern.lastIndex = 0;
55
56
  const matches = [...line.matchAll(pattern)];
56
57
  for (const match of matches) {
57
- const secretValue = match[0] ?? '';
58
+ const secretValue = match[0] ?? "";
58
59
  findings.push({
59
- code: { value: 'L3-001', toString: () => 'L3-001' },
60
- severity: { value: 'error', toString: () => 'error' },
60
+ code: { value: "L3-001", toString: () => "L3-001" },
61
+ severity: { value: "error", toString: () => "error" },
61
62
  message: `セキュリティ問題: ${description} (${ruleId}) at ${filePath}:${idx + 1} value=${redactSecret(secretValue)}`,
62
63
  suggestion: `${ruleId}: 秘密情報は環境変数または秘密管理サービスを使用してください。fixture/docs のダミー値は ${ALLOWLIST_MARKER} を明示してください。`,
64
+ // WI-335: 秘密情報の無効化・ローテーション・保管方式の選定は人間の判断が必須(manual)。
65
+ remediationType: "manual",
63
66
  });
64
67
  }
65
68
  }
@@ -88,6 +91,6 @@ function isAllowlisted(line: string, previousLine: string | undefined): boolean
88
91
 
89
92
  function redactSecret(secretValue: string): string {
90
93
  const value = secretValue.trim();
91
- if (value.length <= 8) return '<redacted>';
94
+ if (value.length <= 8) return "<redacted>";
92
95
  return `${value.slice(0, 3)}...<redacted:${value.length}>`;
93
96
  }