phasegate 0.254.0 → 0.283.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 (82) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.ja.md +6 -3
  3. package/README.md +6 -3
  4. package/docs/contracts/attestation-v2.schema.json +110 -0
  5. package/docs/guide/cli-reference.md +12 -1
  6. package/docs/guide/configuration.md +56 -0
  7. package/docs/guide/installation.md +1 -1
  8. package/docs/templates/ci/aidlc-gate.yml +60 -2
  9. package/package.json +2 -2
  10. package/scripts/harness/agent-integration/application/dto/open-world-obligations-context-dto.ts +30 -0
  11. package/scripts/harness/agent-integration/application/ports/world-obligations-query-port.ts +30 -0
  12. package/scripts/harness/agent-integration/application/usecases/get-open-world-obligations-context-usecase.ts +66 -0
  13. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +30 -27
  14. package/scripts/harness/agent-integration/infrastructure/adapters/world-model-open-obligations-query-adapter.ts +58 -0
  15. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
  16. package/scripts/harness/agent-integration/presentation/session-start-hook.ts +37 -1
  17. package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
  18. package/scripts/harness/agent-integration/presentation/world-obligations-session-context.ts +60 -0
  19. package/scripts/harness/attestation/application/dto/attestation-document.ts +17 -4
  20. package/scripts/harness/attestation/application/mappers/attestation-record-mapper.ts +55 -4
  21. package/scripts/harness/attestation/application/ports/world-snapshot-root-provider.ts +10 -0
  22. package/scripts/harness/attestation/application/usecases/produce-attestation-usecase.ts +33 -7
  23. package/scripts/harness/attestation/composition-root.ts +5 -0
  24. package/scripts/harness/attestation/domain/entities/attestation-record.ts +37 -2
  25. package/scripts/harness/attestation/index.ts +7 -1
  26. package/scripts/harness/attestation/presentation/handlers/attest-handler.ts +5 -2
  27. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +40 -9
  28. package/scripts/harness/config-foundation/application/mappers/world-model-config-mapper.ts +15 -0
  29. package/scripts/harness/config-foundation/domain/harness-config.ts +70 -87
  30. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +80 -88
  31. package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
  32. package/scripts/harness/config-foundation/domain/value-objects/world-config.ts +198 -0
  33. package/scripts/harness/config-foundation/index.ts +14 -15
  34. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +24 -0
  35. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +24 -0
  36. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +24 -0
  37. package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +27 -18
  38. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +84 -8
  39. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +84 -8
  40. package/scripts/harness/harness-api/domain/ports/config-query-port.ts +11 -1
  41. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +125 -86
  42. package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +31 -21
  43. package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +18 -4
  44. package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +23 -8
  45. package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +4 -4
  46. package/scripts/harness/harness-api/infrastructure/adapters/harness-config-query-adapter.ts +79 -34
  47. package/scripts/harness/installation/application/usecases/run-install.ts +199 -51
  48. package/scripts/harness/installation/application/usecases/run-reconcile.ts +277 -69
  49. package/scripts/harness/installation/domain/deployment-manifest.ts +43 -0
  50. package/scripts/harness/integrations/pre-commit.ts +169 -27
  51. package/scripts/harness/main.ts +200 -136
  52. package/scripts/harness/traceability-model/application/dto/changed-design-fragment-dto.ts +31 -0
  53. package/scripts/harness/traceability-model/application/facades/design-change-read-facade.ts +14 -0
  54. package/scripts/harness/traceability-model/application/ports/staged-design-change-source-port.ts +9 -0
  55. package/scripts/harness/traceability-model/composition-root.ts +5 -0
  56. package/scripts/harness/traceability-model/index.ts +9 -0
  57. package/scripts/harness/traceability-model/infrastructure/adapters/git-staged-design-change-adapter.ts +155 -0
  58. package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +31 -0
  59. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +57 -6
  60. package/scripts/harness/validator-system/composition-root.ts +25 -7
  61. package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
  62. package/scripts/harness/validator-system/domain/ports/world-constraint-admission-policy-port.ts +28 -0
  63. package/scripts/harness/validator-system/domain/ports/world-constraint-rederivation-policy-port.ts +11 -0
  64. package/scripts/harness/validator-system/domain/services/design-change-declaration-policy.ts +75 -0
  65. package/scripts/harness/validator-system/domain/services/world-constraint-admission-service.ts +78 -0
  66. package/scripts/harness/validator-system/domain/services/world-constraint-rederivation-service.ts +76 -0
  67. package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +4 -0
  68. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +126 -18
  69. package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +49 -20
  70. package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-admission-adapter.ts +56 -0
  71. package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-rederivation-adapter.ts +56 -0
  72. package/scripts/harness/world-model/application/dto/pinned-design-endpoint-dto.ts +19 -0
  73. package/scripts/harness/world-model/application/dto/world-resolved-config-input.ts +92 -27
  74. package/scripts/harness/world-model/application/dto/world-snapshot-root-dto.ts +8 -0
  75. package/scripts/harness/world-model/application/facades/pinned-design-endpoint-facade.ts +63 -0
  76. package/scripts/harness/world-model/application/facades/world-snapshot-root-facade.ts +19 -0
  77. package/scripts/harness/world-model/application/usecases/derive-world-obligations-use-case.ts +3 -1
  78. package/scripts/harness/world-model/composition-root.ts +62 -33
  79. package/scripts/harness/world-model/index.ts +11 -0
  80. package/scripts/harness/world-model/infrastructure/adapters/attestation-fact-extractor.ts +48 -13
  81. package/scripts/harness/world-model/infrastructure/adapters/file-system-world-control-repository-adapters.ts +6 -4
  82. package/scripts/harness/world-model/presentation/cli/world-derive-command-handler.ts +6 -2
@@ -0,0 +1,28 @@
1
+ // @unit validator-system
2
+ // @layer domain
3
+ // @work-item-id WI-301
4
+
5
+ export type WorldAdmissionClassification = "adopted-legacy" | "new-structural" | "invalid-declaration" | "waived";
6
+
7
+ export interface WorldConstraintAdmissionObligation {
8
+ readonly ruleId: string;
9
+ readonly violationFingerprint: string;
10
+ readonly constraintId: string | null;
11
+ readonly classification: WorldAdmissionClassification;
12
+ }
13
+
14
+ export interface WorldConstraintAdmissionDiagnostic {
15
+ readonly code: string;
16
+ readonly path: string;
17
+ readonly message: string;
18
+ readonly scope: "constraint" | "other";
19
+ }
20
+
21
+ export interface WorldConstraintAdmissionObservation {
22
+ readonly obligations: readonly WorldConstraintAdmissionObligation[];
23
+ readonly diagnostics: readonly WorldConstraintAdmissionDiagnostic[];
24
+ }
25
+
26
+ export interface WorldConstraintAdmissionPolicyPort {
27
+ collect(): Promise<WorldConstraintAdmissionObservation>;
28
+ }
@@ -0,0 +1,11 @@
1
+ // @unit validator-system
2
+ // @layer domain
3
+ // @work-item-id WI-302
4
+
5
+ import type { WorldConstraintAdmissionObservation } from "./world-constraint-admission-policy-port.js";
6
+
7
+ export type WorldConstraintRederivationObservation = WorldConstraintAdmissionObservation;
8
+
9
+ export interface WorldConstraintRederivationPolicyPort {
10
+ collect(): Promise<WorldConstraintRederivationObservation>;
11
+ }
@@ -0,0 +1,75 @@
1
+ // @unit validator-system
2
+ // @layer domain
3
+ // @work-item-id WI-305
4
+
5
+ export interface DesignChangedFragmentInput {
6
+ readonly corpusRole: "product" | "inception";
7
+ readonly declaredKey: string;
8
+ readonly path: string;
9
+ readonly workItemIds: readonly string[];
10
+ }
11
+
12
+ export interface DesignPinnedEndpointInput {
13
+ readonly constraintId: string;
14
+ readonly endpoint: "claimant" | "premise";
15
+ readonly corpusRole: "product" | "inception";
16
+ readonly declaredKey: string;
17
+ }
18
+
19
+ export interface DesignChangeDeclarationFinding {
20
+ readonly code: "design-change-declaration-missing";
21
+ readonly corpusRole: "product" | "inception";
22
+ readonly declaredKey: string;
23
+ readonly path: string;
24
+ readonly expectedWorkItemIds: readonly string[];
25
+ readonly constraintIds: readonly string[];
26
+ }
27
+
28
+ export interface DesignChangeDeclarationEvaluation {
29
+ readonly checkedFragmentCount: number;
30
+ readonly findings: readonly DesignChangeDeclarationFinding[];
31
+ }
32
+
33
+ export interface DesignChangeDeclarationInput {
34
+ readonly changedFragments: readonly DesignChangedFragmentInput[];
35
+ readonly pinnedEndpoints: readonly DesignPinnedEndpointInput[];
36
+ readonly trailerWorkItemIds: readonly string[];
37
+ }
38
+
39
+ const compare = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
40
+ const keyOf = (value: { readonly corpusRole: string; readonly declaredKey: string }): string =>
41
+ `${value.corpusRole}\u0000${value.declaredKey}`;
42
+ const uniqueSorted = (values: readonly string[]): readonly string[] =>
43
+ Object.freeze([...new Set(values)].sort(compare));
44
+
45
+ export class DesignChangeDeclarationPolicy {
46
+ evaluate(input: DesignChangeDeclarationInput): DesignChangeDeclarationEvaluation {
47
+ const endpointsByFragment = new Map<string, DesignPinnedEndpointInput[]>();
48
+ for (const endpoint of input.pinnedEndpoints) {
49
+ const key = keyOf(endpoint);
50
+ const current = endpointsByFragment.get(key) ?? [];
51
+ current.push(endpoint);
52
+ endpointsByFragment.set(key, current);
53
+ }
54
+ const trailerIds = new Set(input.trailerWorkItemIds);
55
+ const pinnedChanges = input.changedFragments
56
+ .filter((fragment) => endpointsByFragment.has(keyOf(fragment)))
57
+ .sort((left, right) => compare(keyOf(left), keyOf(right)) || compare(left.path, right.path));
58
+ const findings = pinnedChanges.flatMap((fragment): readonly DesignChangeDeclarationFinding[] => {
59
+ if (fragment.workItemIds.some((workItemId) => trailerIds.has(workItemId))) return [];
60
+ return [
61
+ Object.freeze({
62
+ code: "design-change-declaration-missing" as const,
63
+ corpusRole: fragment.corpusRole,
64
+ declaredKey: fragment.declaredKey,
65
+ path: fragment.path,
66
+ expectedWorkItemIds: uniqueSorted(fragment.workItemIds),
67
+ constraintIds: uniqueSorted(
68
+ (endpointsByFragment.get(keyOf(fragment)) ?? []).map((endpoint) => endpoint.constraintId),
69
+ ),
70
+ }),
71
+ ];
72
+ });
73
+ return Object.freeze({ checkedFragmentCount: pinnedChanges.length, findings: Object.freeze(findings) });
74
+ }
75
+ }
@@ -0,0 +1,78 @@
1
+ // @unit validator-system
2
+ // @layer domain
3
+ // @work-item-id WI-301
4
+
5
+ import type {
6
+ WorldAdmissionClassification,
7
+ WorldConstraintAdmissionObservation,
8
+ } from "../ports/world-constraint-admission-policy-port.js";
9
+
10
+ export interface WorldConstraintAdmissionFinding {
11
+ readonly severity: "error" | "warning";
12
+ readonly ruleId: string | null;
13
+ readonly violationFingerprint: string | null;
14
+ readonly constraintId: string | null;
15
+ readonly classification: WorldAdmissionClassification | "constraint-input-invalid" | "local-evaluation-incomplete";
16
+ readonly message: string;
17
+ readonly suggestion: string;
18
+ readonly sourcePath: string | null;
19
+ }
20
+
21
+ const LOCAL_TRUST_NOTICE = "Local L2 evidence can be forged; authoritative L3 re-derivation is required.";
22
+ const AUTHORITATIVE_SUGGESTION =
23
+ "Repair the declaration or structural finding, then rely on authoritative L3 clean-corpus re-derivation.";
24
+
25
+ const compareNullable = (left: string | null, right: string | null): number =>
26
+ (left ?? "") < (right ?? "") ? -1 : (left ?? "") > (right ?? "") ? 1 : 0;
27
+
28
+ export class WorldConstraintAdmissionService {
29
+ evaluate(observation: WorldConstraintAdmissionObservation): readonly WorldConstraintAdmissionFinding[] {
30
+ const obligationFindings = observation.obligations.map((obligation): WorldConstraintAdmissionFinding => {
31
+ const blocking =
32
+ obligation.classification === "new-structural" || obligation.classification === "invalid-declaration";
33
+ const label =
34
+ obligation.classification === "invalid-declaration"
35
+ ? "non-adoptable invalid World constraint declaration"
36
+ : obligation.classification === "new-structural"
37
+ ? "new World structural violation"
38
+ : obligation.classification === "adopted-legacy"
39
+ ? "adopted legacy World violation"
40
+ : "actively waived World violation";
41
+ return Object.freeze({
42
+ severity: blocking ? "error" : "warning",
43
+ ruleId: obligation.ruleId,
44
+ violationFingerprint: obligation.violationFingerprint,
45
+ constraintId: obligation.constraintId,
46
+ classification: obligation.classification,
47
+ message: `L2 local fast-path reports ${label}: ${obligation.ruleId} ${obligation.violationFingerprint}. ${LOCAL_TRUST_NOTICE}`,
48
+ suggestion: AUTHORITATIVE_SUGGESTION,
49
+ sourcePath: null,
50
+ });
51
+ });
52
+ const diagnosticFindings = observation.diagnostics.map((item): WorldConstraintAdmissionFinding => {
53
+ const constraintInput = item.scope === "constraint";
54
+ return Object.freeze({
55
+ severity: constraintInput ? "error" : "warning",
56
+ ruleId: constraintInput ? "WCR-001" : null,
57
+ violationFingerprint: null,
58
+ constraintId: null,
59
+ classification: constraintInput ? "constraint-input-invalid" : "local-evaluation-incomplete",
60
+ message: constraintInput
61
+ ? `L2 local fast-path cannot admit constraint declaration input (${item.code}): ${item.message}. ${LOCAL_TRUST_NOTICE}`
62
+ : `L2 local fast-path could not complete non-constraint policy observation (${item.code}): ${item.message}. ${LOCAL_TRUST_NOTICE}`,
63
+ suggestion: AUTHORITATIVE_SUGGESTION,
64
+ sourcePath: item.path,
65
+ });
66
+ });
67
+
68
+ return Object.freeze(
69
+ [...obligationFindings, ...diagnosticFindings].sort(
70
+ (left, right) =>
71
+ compareNullable(left.ruleId, right.ruleId) ||
72
+ compareNullable(left.violationFingerprint, right.violationFingerprint) ||
73
+ compareNullable(left.sourcePath, right.sourcePath) ||
74
+ left.classification.localeCompare(right.classification),
75
+ ),
76
+ );
77
+ }
78
+ }
@@ -0,0 +1,76 @@
1
+ // @unit validator-system
2
+ // @layer domain
3
+ // @work-item-id WI-302
4
+
5
+ import type { WorldAdmissionClassification } from "../ports/world-constraint-admission-policy-port.js";
6
+ import type { WorldConstraintRederivationObservation } from "../ports/world-constraint-rederivation-policy-port.js";
7
+
8
+ export interface WorldConstraintRederivationFinding {
9
+ readonly severity: "error" | "warning";
10
+ readonly ruleId: string | null;
11
+ readonly violationFingerprint: string | null;
12
+ readonly constraintId: string | null;
13
+ readonly classification:
14
+ | WorldAdmissionClassification
15
+ | "constraint-input-invalid"
16
+ | "authoritative-evaluation-failed";
17
+ readonly message: string;
18
+ readonly suggestion: string;
19
+ readonly sourcePath: string | null;
20
+ }
21
+
22
+ const REPAIR_SUGGESTION =
23
+ "Repair the versioned declaration or current corpus finding, then rerun authoritative L3 clean-corpus re-derivation.";
24
+
25
+ const compareNullable = (left: string | null, right: string | null): number =>
26
+ (left ?? "") < (right ?? "") ? -1 : (left ?? "") > (right ?? "") ? 1 : 0;
27
+
28
+ export class WorldConstraintRederivationService {
29
+ evaluate(observation: WorldConstraintRederivationObservation): readonly WorldConstraintRederivationFinding[] {
30
+ const obligationFindings = observation.obligations.map((obligation): WorldConstraintRederivationFinding => {
31
+ const blocking =
32
+ obligation.classification === "new-structural" || obligation.classification === "invalid-declaration";
33
+ const label =
34
+ obligation.classification === "invalid-declaration"
35
+ ? "non-adoptable invalid World constraint declaration"
36
+ : obligation.classification === "new-structural"
37
+ ? "new World structural violation"
38
+ : obligation.classification === "adopted-legacy"
39
+ ? "adopted legacy World violation"
40
+ : "actively waived World violation";
41
+ return Object.freeze({
42
+ severity: blocking ? "error" : "warning",
43
+ ruleId: obligation.ruleId,
44
+ violationFingerprint: obligation.violationFingerprint,
45
+ constraintId: obligation.constraintId,
46
+ classification: obligation.classification,
47
+ message: `L3 authoritative clean-corpus re-derivation reports ${label}: ${obligation.ruleId} ${obligation.violationFingerprint}.`,
48
+ suggestion: REPAIR_SUGGESTION,
49
+ sourcePath: null,
50
+ });
51
+ });
52
+ const diagnosticFindings = observation.diagnostics.map((item): WorldConstraintRederivationFinding => {
53
+ const constraintInput = item.scope === "constraint";
54
+ return Object.freeze({
55
+ severity: "error",
56
+ ruleId: constraintInput ? "WCR-001" : null,
57
+ violationFingerprint: null,
58
+ constraintId: null,
59
+ classification: constraintInput ? "constraint-input-invalid" : "authoritative-evaluation-failed",
60
+ message: `L3 authoritative clean-corpus re-derivation cannot produce a trustworthy result (${item.code}): ${item.message}.`,
61
+ suggestion: REPAIR_SUGGESTION,
62
+ sourcePath: item.path,
63
+ });
64
+ });
65
+
66
+ return Object.freeze(
67
+ [...obligationFindings, ...diagnosticFindings].sort(
68
+ (left, right) =>
69
+ compareNullable(left.ruleId, right.ruleId) ||
70
+ compareNullable(left.violationFingerprint, right.violationFingerprint) ||
71
+ compareNullable(left.sourcePath, right.sourcePath) ||
72
+ left.classification.localeCompare(right.classification),
73
+ ),
74
+ );
75
+ }
76
+ }
@@ -14,6 +14,8 @@
14
14
  * WI-258 (ADR-030 §Decision.3.②) で L2-016(coverage-attestation-gating, fail-closed)を追加
15
15
  * WI-259 (ADR-030 §Decision.3.④) で L3-006(injection-scan, advisory warning-only)を追加
16
16
  * WI-268 (ADR-030 §Decision.1・§Decision.3.② 第2段) で L3-007(coverage-attestation-verification, fail-closed)を追加
17
+ * WI-301 で L2-017(world-constraint-admission, local fast-path)を追加
18
+ * WI-302 で L3-008(world-constraint-rederivation, authoritative)を追加
17
19
  */
18
20
 
19
21
  export class InvalidValidatorIdError extends Error {
@@ -38,6 +40,7 @@ const VALIDATOR_NAME_MAP: Record<string, string> = {
38
40
  "L2-014": "work-item-status-staleness",
39
41
  "L2-015": "contract-traceability-coverage",
40
42
  "L2-016": "coverage-attestation-gating",
43
+ "L2-017": "world-constraint-admission",
41
44
  "L3-001": "security",
42
45
  "L3-002": "performance",
43
46
  "L3-003": "coverage",
@@ -45,6 +48,7 @@ const VALIDATOR_NAME_MAP: Record<string, string> = {
45
48
  "L3-005": "ac-bound-coverage",
46
49
  "L3-006": "injection-scan",
47
50
  "L3-007": "coverage-attestation-verification",
51
+ "L3-008": "world-constraint-rederivation",
48
52
  "L4-001": "drift-detect",
49
53
  "L4-002": "consistency-check",
50
54
  "L4-003": "dead-code",
@@ -3,51 +3,150 @@
3
3
  * @unit validator-system
4
4
  * @work-item-id WI-156
5
5
  * @work-item-id WI-212
6
+ * @work-item-id WI-301
7
+ * @work-item-id WI-302
8
+ * @work-item-id WI-319
9
+ * @work-item-id WI-328
6
10
  *
7
11
  * HarnessConfigValidatorConfigAdapter — ValidatorConfigPort実装
8
12
  * HarnessConfigV2からLayerConfig VOを構築する
9
13
  */
10
- import { LayerConfig } from '../../domain/value-objects/layer-config.js';
11
- import { ValidatorId } from '../../domain/value-objects/validator-id.js';
12
- import type { ValidatorConfigPort } from '../../domain/ports/validator-config-port.js';
14
+
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import type { ValidatorConfigPort } from "../../domain/ports/validator-config-port.js";
18
+ import { LayerConfig } from "../../domain/value-objects/layer-config.js";
19
+ import { ValidatorId } from "../../domain/value-objects/validator-id.js";
13
20
 
14
21
  export interface HarnessConfigLayers {
15
22
  L2?: { enabled?: boolean; validators?: string[]; strictOnly?: boolean; preset?: string };
16
- L3?: { enabled?: boolean; validators?: string[]; strictOnly?: boolean; preset?: string; coverageThreshold?: number; bundleSizeLimit?: number };
23
+ L3?: {
24
+ enabled?: boolean;
25
+ validators?: string[];
26
+ strictOnly?: boolean;
27
+ preset?: string;
28
+ coverageThreshold?: number;
29
+ bundleSizeLimit?: number;
30
+ };
17
31
  L4?: { enabled?: boolean; validators?: string[]; strictOnly?: boolean; preset?: string; deadCodeGC?: boolean };
18
32
  }
19
33
 
20
34
  export interface HarnessConfigV2Like {
21
- preset?: 'minimal' | 'standard' | 'strict';
22
- project?: { preset?: 'minimal' | 'standard' | 'strict'; languages?: readonly string[] };
35
+ preset?: "minimal" | "standard" | "strict";
36
+ project?: { preset?: "minimal" | "standard" | "strict"; languages?: readonly string[] };
23
37
  layers?: HarnessConfigLayers;
24
38
  harnesses?: {
25
39
  bundleSizeLimit?: number;
26
40
  deadCodeGC?: boolean;
27
41
  };
42
+ world?: { enabled?: boolean };
43
+ }
44
+
45
+ /**
46
+ * WI-319 (github#39): project.languages 未宣言時にファイルシステムから言語を検出するためのマーカー定義。
47
+ * typescript は package.json の存在自体を根拠にできない(phasegate 導入時に phasegate 用
48
+ * package.json が置かれるため)ので、この表には含めず hasTypescriptMarker() で個別判定する。
49
+ */
50
+ const LANGUAGE_MARKER_FILES: ReadonlyArray<{ readonly language: string; readonly markers: readonly string[] }> = [
51
+ { language: "python", markers: ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt"] },
52
+ { language: "go", markers: ["go.mod"] },
53
+ { language: "rust", markers: ["Cargo.toml"] },
54
+ { language: "java", markers: ["pom.xml", "build.gradle", "build.gradle.kts"] },
55
+ { language: "ruby", markers: ["Gemfile"] },
56
+ { language: "php", markers: ["composer.json"] },
57
+ ];
58
+
59
+ /** WI-328: 実効言語リストの出所。status 表示等で「なぜこの言語判定になったか」を示す。 */
60
+ export type ProjectLanguageSource = "declared" | "detected" | "fallback";
61
+
62
+ export interface ResolvedProjectLanguages {
63
+ readonly languages: readonly string[];
64
+ readonly source: ProjectLanguageSource;
65
+ }
66
+
67
+ /**
68
+ * WI-328 (github#39 残課題): 実効言語リストとその出所を解決する唯一の実装。
69
+ * getProjectLanguages()(WI-319 の検出ロジック)と phasegate:status の言語表示が
70
+ * 同じテーブル・同じ優先順位を共有するために export する。
71
+ *
72
+ * 優先順位:
73
+ * 1. declared — config の project.languages 宣言(従来どおり最優先)
74
+ * 2. detected — project root のマーカーファイルから検出(WI-319 / github#39)
75
+ * 3. fallback — 検出ゼロなら typescript フォールバック(純 JS リポジトリ等の挙動維持)
76
+ */
77
+ export function resolveProjectLanguages(
78
+ declaredLanguages: readonly string[] | undefined,
79
+ rootDir: string,
80
+ ): ResolvedProjectLanguages {
81
+ if (declaredLanguages && declaredLanguages.length > 0) {
82
+ return { languages: [...declaredLanguages], source: "declared" };
83
+ }
84
+ const detected = detectLanguagesFromFilesystem(rootDir);
85
+ if (detected.length > 0) {
86
+ return { languages: detected, source: "detected" };
87
+ }
88
+ return { languages: ["typescript"], source: "fallback" };
89
+ }
90
+
91
+ function detectLanguagesFromFilesystem(rootDir: string): string[] {
92
+ const detected: string[] = [];
93
+ if (hasTypescriptMarker(rootDir)) detected.push("typescript");
94
+ for (const { language, markers } of LANGUAGE_MARKER_FILES) {
95
+ if (markers.some((marker) => existsSync(join(rootDir, marker)))) {
96
+ detected.push(language);
97
+ }
98
+ }
99
+ return detected;
100
+ }
101
+
102
+ /**
103
+ * typescript の根拠は tsconfig.json の存在、または package.json の
104
+ * dependencies / devDependencies に typescript があることのみ。
105
+ * package.json の存在自体は根拠にしない(phasegate 導入時に phasegate 用
106
+ * package.json が置かれるため、誤検出の原因になる)。
107
+ */
108
+ function hasTypescriptMarker(rootDir: string): boolean {
109
+ if (existsSync(join(rootDir, "tsconfig.json"))) return true;
110
+ const packageJsonPath = join(rootDir, "package.json");
111
+ if (!existsSync(packageJsonPath)) return false;
112
+ try {
113
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
114
+ dependencies?: Record<string, string>;
115
+ devDependencies?: Record<string, string>;
116
+ };
117
+ return parsed.dependencies?.typescript !== undefined || parsed.devDependencies?.typescript !== undefined;
118
+ } catch {
119
+ // parse 失敗は typescript 根拠なしとして扱う(throw しない)
120
+ return false;
121
+ }
28
122
  }
29
123
 
30
124
  export class HarnessConfigValidatorConfigAdapter implements ValidatorConfigPort {
31
125
  private readonly config: HarnessConfigV2Like;
126
+ private readonly rootDir: string;
32
127
 
33
- constructor(config: HarnessConfigV2Like) {
128
+ constructor(config: HarnessConfigV2Like, rootDir: string = process.cwd()) {
34
129
  this.config = config;
130
+ this.rootDir = rootDir;
35
131
  }
36
132
 
37
- async getLayerConfig(layer: 'L2' | 'L3' | 'L4'): Promise<LayerConfig> {
38
- const preset = (this.config.project?.preset ?? this.config.preset ?? 'standard') as 'minimal' | 'standard' | 'strict';
39
- const strictOnly = preset === 'strict';
133
+ async getLayerConfig(layer: "L2" | "L3" | "L4"): Promise<LayerConfig> {
134
+ const preset = (this.config.project?.preset ?? this.config.preset ?? "standard") as
135
+ | "minimal"
136
+ | "standard"
137
+ | "strict";
138
+ const strictOnly = preset === "strict";
40
139
  const layerData = this.config.layers?.[layer] ?? {};
41
140
 
42
141
  const defaultValidators: Record<string, string[]> = {
43
- L2: ['L2-001', 'L2-002', 'L2-003', 'L2-013', 'L2-014', 'L2-015'],
44
- L3: ['L3-001', 'L3-002', 'L3-003', 'L3-004'],
45
- L4: ['L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005', 'L4-006'],
142
+ L2: ["L2-001", "L2-002", "L2-003", "L2-013", "L2-014", "L2-015"],
143
+ L3: ["L3-001", "L3-002", "L3-003", "L3-004"],
144
+ L4: ["L4-001", "L4-002", "L4-003", "L4-004", "L4-005", "L4-006"],
46
145
  };
47
146
 
48
147
  const thresholds: Record<string, number> = {};
49
148
 
50
- if (layer === 'L3') {
149
+ if (layer === "L3") {
51
150
  const l3 = this.config.layers?.L3;
52
151
  if (l3?.coverageThreshold !== undefined) thresholds.coverageThreshold = l3.coverageThreshold;
53
152
  if (l3?.bundleSizeLimit !== undefined) thresholds.bundleSizeLimit = l3.bundleSizeLimit;
@@ -56,16 +155,24 @@ export class HarnessConfigValidatorConfigAdapter implements ValidatorConfigPort
56
155
  }
57
156
  }
58
157
 
59
- if (layer === 'L4') {
158
+ if (layer === "L4") {
60
159
  if (this.config.harnesses?.deadCodeGC !== undefined) {
61
160
  thresholds.deadCodeGC = this.config.harnesses.deadCodeGC ? 1 : 0;
62
161
  }
63
162
  }
64
163
 
164
+ const configuredValidatorIds = layerData.validators ?? defaultValidators[layer];
165
+ const automaticWorldValidatorId = layer === "L2" ? "L2-017" : layer === "L3" ? "L3-008" : null;
166
+ const selectedValidatorIds =
167
+ automaticWorldValidatorId === null
168
+ ? configuredValidatorIds
169
+ : this.config.world?.enabled === true
170
+ ? [...configuredValidatorIds, automaticWorldValidatorId]
171
+ : configuredValidatorIds.filter((validatorId) => validatorId !== automaticWorldValidatorId);
65
172
  return LayerConfig.create({
66
173
  layer,
67
174
  enabled: layerData.enabled !== false,
68
- validatorIds: (layerData.validators ?? defaultValidators[layer]).map((idOrName) => this.normalizeValidatorId(idOrName)),
175
+ validatorIds: [...new Set(selectedValidatorIds.map((idOrName) => this.normalizeValidatorId(idOrName)))],
69
176
  thresholds,
70
177
  strictOnly: layerData.strictOnly ?? strictOnly,
71
178
  preset,
@@ -73,8 +180,9 @@ export class HarnessConfigValidatorConfigAdapter implements ValidatorConfigPort
73
180
  }
74
181
 
75
182
  async getProjectLanguages(): Promise<readonly string[]> {
76
- const languages = this.config.project?.languages;
77
- return languages && languages.length > 0 ? [...languages] : ['typescript'];
183
+ // WI-319 の宣言優先 → FS 検出 → typescript フォールバックの解決は
184
+ // resolveProjectLanguages() に一本化(WI-328 status 表示と共有)。
185
+ return [...resolveProjectLanguages(this.config.project?.languages, this.rootDir).languages];
78
186
  }
79
187
 
80
188
  private normalizeValidatorId(idOrName: string): string {
@@ -18,18 +18,25 @@
18
18
  * (layers.L3.requirementMatrixPath)から供給される。未指定時は既定値
19
19
  * `.harness/requirement-test-matrix.json` を用いる。マトリクスが不在の場合は
20
20
  * fail-closed で「設定されているが不在」という実行可能なメッセージを返す。
21
+ *
22
+ * FRESH PROJECT SKIP (WI-324): 例外として「matrix 不在 かつ StoryCatalog の
23
+ * story がゼロ」の場合のみ skipped=true を返す。phasegate 導入直後(story 未作成・
24
+ * matrix 未生成)のオンボーディングを fail-closed で阻害しないため。story が
25
+ * 1件でも存在するのに matrix が不在なら「あるべき matrix が消えた事故」として
26
+ * 従来どおり fail-closed。story 数の取得自体に失敗した場合も判定不能として
27
+ * 保守的に fail-closed 側へ倒す。
21
28
  */
22
- import { access } from 'node:fs/promises';
23
- import { isAbsolute, join } from 'node:path';
24
- import type { AcCoveragePolicyPort } from '../../domain/ports/ac-coverage-policy-port.js';
25
- import type { HarnessErrorLike } from '../../domain/value-objects/validation-result.js';
29
+ import { access } from "node:fs/promises";
30
+ import { isAbsolute, join } from "node:path";
31
+ import type { AcCoveragePolicyPort } from "../../domain/ports/ac-coverage-policy-port.js";
32
+ import type { HarnessErrorLike } from "../../domain/value-objects/validation-result.js";
26
33
 
27
- const DEFAULT_MATRIX_PATH = '.harness/requirement-test-matrix.json';
34
+ const DEFAULT_MATRIX_PATH = ".harness/requirement-test-matrix.json";
28
35
 
29
36
  function toL3004Error(message: string, suggestion: string): HarnessErrorLike {
30
37
  return {
31
- code: { value: 'L3-004', toString: () => 'L3-004' },
32
- severity: { value: 'error', toString: () => 'error' },
38
+ code: { value: "L3-004", toString: () => "L3-004" },
39
+ severity: { value: "error", toString: () => "error" },
33
40
  message,
34
41
  suggestion,
35
42
  };
@@ -39,21 +46,29 @@ export class NyquistAcCoveragePolicyAdapter implements AcCoveragePolicyPort {
39
46
  async checkCoverage(context: { matrixFilePath?: string }): Promise<{
40
47
  passed: boolean;
41
48
  errors: readonly HarnessErrorLike[];
49
+ skipped?: boolean;
50
+ skipReason?: string;
42
51
  }> {
43
52
  const rootDir = process.cwd();
44
53
  const relativeOrAbsolute =
45
- context.matrixFilePath && context.matrixFilePath.length > 0
46
- ? context.matrixFilePath
47
- : DEFAULT_MATRIX_PATH;
48
- const matrixFilePath = isAbsolute(relativeOrAbsolute)
49
- ? relativeOrAbsolute
50
- : join(rootDir, relativeOrAbsolute);
54
+ context.matrixFilePath && context.matrixFilePath.length > 0 ? context.matrixFilePath : DEFAULT_MATRIX_PATH;
55
+ const matrixFilePath = isAbsolute(relativeOrAbsolute) ? relativeOrAbsolute : join(rootDir, relativeOrAbsolute);
51
56
 
52
57
  // マトリクス不在は threshold-configured-but-absent と同様に fail-closed で
53
58
  // 明確なメッセージを返す(ENOENT の生メッセージで落とさない)。
59
+ // 例外(WI-324): story がゼロのフレッシュプロジェクトのみ SKIP を返す。
54
60
  try {
55
61
  await access(matrixFilePath);
56
62
  } catch {
63
+ if (await this.hasZeroStories(rootDir)) {
64
+ return {
65
+ passed: true,
66
+ errors: [],
67
+ skipped: true,
68
+ skipReason:
69
+ "story 未作成のため L3-004 をスキップ(story 作成後に requirement-test-matrix を生成すると有効化されます)",
70
+ };
71
+ }
57
72
  return {
58
73
  passed: false,
59
74
  errors: [
@@ -69,7 +84,7 @@ export class NyquistAcCoveragePolicyAdapter implements AcCoveragePolicyPort {
69
84
  // 有効 storyId 一覧を traceability-model の StoryCatalog から取得する(REAL registry)
70
85
  const storyIds = await this.loadValidStoryIds(rootDir);
71
86
 
72
- const { createNyquistValidationModule } = await import('../../../nyquist-validation/composition-root.js');
87
+ const { createNyquistValidationModule } = await import("../../../nyquist-validation/composition-root.js");
73
88
  const mod = createNyquistValidationModule({
74
89
  getStoryIds: async () => storyIds,
75
90
  });
@@ -79,9 +94,7 @@ export class NyquistAcCoveragePolicyAdapter implements AcCoveragePolicyPort {
79
94
 
80
95
  return {
81
96
  passed: output.passed,
82
- errors: output.errors.map((err) =>
83
- toL3004Error(err.message, ''),
84
- ),
97
+ errors: output.errors.map((err) => toL3004Error(err.message, "")),
85
98
  };
86
99
  } catch (error) {
87
100
  const message = error instanceof Error ? error.message : String(error);
@@ -91,19 +104,35 @@ export class NyquistAcCoveragePolicyAdapter implements AcCoveragePolicyPort {
91
104
  errors: [
92
105
  toL3004Error(
93
106
  `AC網羅ゲートの検査に失敗しました(fail-closed): ${message}`,
94
- 'requirement-test-matrix.json の存在・形式・スキーマ整合性を確認してください',
107
+ "requirement-test-matrix.json の存在・形式・スキーマ整合性を確認してください",
95
108
  ),
96
109
  ],
97
110
  };
98
111
  }
99
112
  }
100
113
 
114
+ /**
115
+ * WI-324: 「story がゼロ」判定。StoryCatalog(traceability-model)の登録 story 数を
116
+ * シグナルとする。StoryCatalog は requirement-test-matrix の生成元と同じ
117
+ * user_stories.md を読むため、「matrix が本来存在しうるか」と一貫した判定になる。
118
+ * catalog 読み込み自体が失敗した場合(config 不正等)は判定不能として false を返し、
119
+ * 呼び出し側を保守的に fail-closed へ倒す。
120
+ */
121
+ private async hasZeroStories(rootDir: string): Promise<boolean> {
122
+ try {
123
+ const storyIds = await this.loadValidStoryIds(rootDir);
124
+ return storyIds.length === 0;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+
101
130
  private async loadValidStoryIds(rootDir: string): Promise<readonly string[]> {
102
- const { createConfigFoundationModule } = await import('../../../config-foundation/composition-root.js');
131
+ const { createConfigFoundationModule } = await import("../../../config-foundation/composition-root.js");
103
132
  const configModule = createConfigFoundationModule();
104
133
  const resolvedConfig = await configModule.usecases.loadResolvedConfigUseCase.execute();
105
134
 
106
- const { createTraceabilityModelModule } = await import('../../../traceability-model/composition-root.js');
135
+ const { createTraceabilityModelModule } = await import("../../../traceability-model/composition-root.js");
107
136
  const traceModule = createTraceabilityModelModule(rootDir, {
108
137
  pathRoots: { designDocsRoot: resolvedConfig.config.paths.designDocs },
109
138
  });