phasegate 0.140.0 → 0.142.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 (34) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.ja.md +7 -3
  3. package/README.md +8 -5
  4. package/docs/guide/cli-reference.md +1 -1
  5. package/docs/guide/layer-model.md +6 -1
  6. package/package.json +1 -1
  7. package/scripts/harness/biome-ast-engine/infrastructure/adapters/harness-error-formatter-adapter.ts +15 -1
  8. package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +31 -1
  9. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +1 -1
  10. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +40 -4
  11. package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +29 -11
  12. package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +6 -4
  13. package/scripts/harness/harness-api/domain/value-objects/drift-report-summary.ts +108 -8
  14. package/scripts/harness/harness-api/domain/value-objects/layer-health.ts +25 -2
  15. package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts +2 -0
  16. package/scripts/harness/integrations/pre-commit.ts +7 -4
  17. package/scripts/harness/main.ts +37 -1
  18. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +47 -1
  19. package/scripts/harness/traceability-model/application/usecases/apply-work-item-status-usecase.ts +30 -0
  20. package/scripts/harness/traceability-model/application/usecases/derive-work-item-status-usecase.ts +27 -0
  21. package/scripts/harness/traceability-model/composition-root.ts +20 -0
  22. package/scripts/harness/traceability-model/domain/ports/work-item-status-port.ts +16 -0
  23. package/scripts/harness/traceability-model/domain/services/work-item-status-derivation-service.ts +121 -0
  24. package/scripts/harness/traceability-model/domain/value-objects/work-item-status-report.ts +47 -0
  25. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +240 -0
  26. package/scripts/harness/traceability-model/presentation/cli/work-item-status-command-handler.ts +103 -0
  27. package/scripts/harness/validator-system/application/use-cases/run-l1-validators-usecase.ts +1 -44
  28. package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +30 -0
  29. package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +1 -1
  30. package/scripts/harness/validator-system/composition-root.ts +16 -13
  31. package/scripts/harness/validator-system/domain/services/cli-e2e-test-existence-service.ts +36 -5
  32. package/scripts/harness/validator-system/domain/value-objects/cli-e2e-test-coverage-report.ts +8 -2
  33. package/scripts/harness/validator-system/infrastructure/adapters/e2e-test-file-registry-adapter.ts +12 -3
  34. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.142.0] - 2026-05-10
11
+
12
+ ### Added
13
+
14
+ - **WI-126 — work item status derivation** — `work-items:status --dry-run|--apply` を追加し、WI frontmatter `status` を inception / product reflection / implementation / test evidence から導出できるようにした。
15
+ - `--id WI-XXX` で単一 WI に絞り込み、`--fail-on-stale` で stale status を exit code 1 として検出できる。
16
+ - `--apply` は stale な `description.md` の YAML frontmatter `status:` 行だけを書き戻す。
17
+ - `fix` / `chore` の shortcut path は README / README.ja の説明と一致する。
18
+
19
+ ### Fixed
20
+
21
+ - publish 前回帰として、標準 L2 validator set に `L2-013` を含む現行契約と `preCommit.implementationExtensions` default をテスト期待値に反映した。
22
+
23
+ ## [0.141.0] - 2026-05-09
24
+
25
+ ### Fixed
26
+
27
+ - **WI-024 follow-up — metadataTags suggestion text** — `architecture.metadataTags.unit` / `architecture.metadataTags.layer` を使った L1-001 / L1-002 の `suggestion` も、固定の `@unit` / `@layer` ではなく設定タグ名を表示するようにした。
28
+
10
29
  ## [0.140.0] - 2026-05-09
11
30
 
12
31
  ### Added
package/README.ja.md CHANGED
@@ -148,7 +148,8 @@ npx phasegate update-skills # スキルを最新版に再デプロイ
148
148
  | security, performance, coverage 90%/95%, 要件カバレッジ |
149
149
  +------------------------------------------------------------------+
150
150
  | L4 週次 (default off) |
151
- | 設計-コード乖離, 文書整合性, デッドコード |
151
+ | 設計-コード乖離, 文書整合性, デッドコード, |
152
+ | doc-freshness, pointer-validation |
152
153
  +------------------------------------------------------------------+
153
154
  ```
154
155
 
@@ -292,6 +293,7 @@ npx phasegate <command> [options]
292
293
  | `baseline` | retrofit grandfather snapshot 生成(`--dry-run`, `--force`, `--paths <glob>`, `--json`) |
293
294
  | `scaffold-design --unit <id> --phase <logical\|domain\|uiux\|unit-test\|it-test>` | 最小構成の設計文書を `templates/` から生成 |
294
295
  | `phasegate:status` | 全体の健全性サマリ |
296
+ | `work-items:status --dry-run` / `--apply` | 成果物から WI status を導出し、必要に応じて `description.md` frontmatter を更新 |
295
297
  | `phasegate:check-phase --unit <id>` | 指定 Unit の現在フェーズ |
296
298
  | `phasegate:detect-drift` | 設計-コード乖離レポート |
297
299
  | `migrate work-items --dry-run` / `--apply` | 既存リポジトリの旧 `ISSUE-XXX` / `H{NN}-{NN}` directory を WI 統一レイアウト(`_cross/{WI-XXX}/` / `{unit}/{WI-XXX}/`)へ移行。frontmatter(`type` / `severity` / `legacy_id` / `affects`)を自動注入。冪等。`--json` で CI/スクリプト連携可。詳細: [Work Item Migration](docs/guide/cli-reference.md#work-item-migration) |
@@ -405,6 +407,8 @@ TESTED (@work-item-id 付きテストあり / green)
405
407
 
406
408
  `type: chore` は DRAFTED で完結。`type: fix` は DRAFTED → REFLECTED → IMPLEMENTED の簡略パス。`status` は PhaseGate が自動更新します。
407
409
 
410
+ `phasegate work-items:status --dry-run` で current status / derived status / reason / next action を確認できます。単一 WI に絞る場合は `--id WI-XXX`、CI 風に stale status を検出する場合は `--fail-on-stale`、`description.md` frontmatter の `status:` 行だけを書き戻す場合は `--apply` を指定します。
411
+
408
412
  詳細仕様: [`docs/folder_management_rules.md`](docs/folder_management_rules.md)
409
413
 
410
414
  ---
@@ -455,9 +459,9 @@ reports/
455
459
 
456
460
  | Work Item | 内容 |
457
461
  |---|---|
458
- | **[WI-033](docs/inception/_cross/WI-033/description.md)** | `doc-freshness` / `pointer-validation` L4 validator に昇格 |
462
+ | **[WI-128](docs/inception/_cross/WI-128/description.md)** | L4 運用ロールアウトの仕上げ。`doc-freshness` / `pointer-validation` L4-004 / L4-005 として登録済みで、`p2:*` 互換コマンドも維持。WI-033 は完了済みとして閉じ、残りの scheduling / default / 運用 docs は後続 WI で扱う。@work-item-id WI-128 |
459
463
 
460
- L3 Nyquist Validation の `requirement-test-matrix.json` 自動生成はまだ未自動化です。現時点では手動セットアップで利用できます。
464
+ L3 Nyquist Validation の `requirement-test-matrix.json` 自動生成はまだ未自動化です。[WI-125](docs/inception/_cross/WI-125/description.md) と L3 guide を参照してください。現時点では手動セットアップで利用できます。
461
465
 
462
466
  ---
463
467
 
package/README.md CHANGED
@@ -156,7 +156,7 @@ npx phasegate update-skills
156
156
  | L1 | Editor save / `phasegate lint` | `@unit` / `@layer` metadata, layer violations, AI anti-patterns, dead code |
157
157
  | L2 | Pre-commit (also evaluated inside PreToolUse at L0) | Phase gate, metadata completeness, `@work-item-id` reflection (`L2-STORY-REFLECTION`), test quality |
158
158
  | L3 | CI/CD pipeline | Security, performance, coverage (90%/95%), requirements traceability |
159
- | L4 | Scheduled (weekly). Currently `layers.L4.enabled: false` by default — opt-in per project | Design-code drift, cross-document consistency, dead code. Doc freshness and pointer checks are standalone `p2:*` commands until WI-033. |
159
+ | L4 | Scheduled (weekly). Currently `layers.L4.enabled: false` by default — opt-in per project | Design-code drift, cross-document consistency, dead code, doc freshness, and pointer validation. `p2:*` standalone commands remain available as compatibility entry points. |
160
160
 
161
161
  ---
162
162
 
@@ -247,6 +247,8 @@ TESTED (test files annotated with @work-item-id, all green)
247
247
 
248
248
  `type: chore` ends at DRAFTED. `type: fix` shortcuts via DRAFTED → REFLECTED → IMPLEMENTED. PhaseGate auto-updates `status`.
249
249
 
250
+ Use `phasegate work-items:status --dry-run` to inspect current status, derived status, reason, and next action. Add `--id WI-XXX` to scope the report, `--fail-on-stale` for CI-style stale status detection, or `--apply` to update only the `status:` line in each stale `description.md` frontmatter.
251
+
250
252
  Full spec: [`docs/folder_management_rules.md`](docs/folder_management_rules.md)
251
253
 
252
254
  ---
@@ -466,14 +468,15 @@ npx phasegate <command> [options]
466
468
  |---|---|
467
469
  | `init --name <name>` | Initialize project, deploy skills, generate config. Use `--with-ci` to deploy GitHub Actions workflows. |
468
470
  | `lint` | Run L1 Biome AST checks |
469
- | `validate --layer <L2-L4\|all>` | Run validators for specified layer (`--layer L0` prints runtime hook guidance) |
470
- | `ci-check` | Full CI check (L2-L4) |
471
+ | `validate --layer <L1-L4\|all>` | Run validators for specified layer (`--layer L0` prints runtime hook guidance; explicit L4 runs even when scheduled L4 is disabled) |
472
+ | `ci-check` | Full CI check (L2-L4; disabled L4 is reported as skipped) |
471
473
  | `ci:generate-template --type <aidlc-gate\|consistency-check\|pre-commit\|agent-context-refresh> --render` | Render the bundled CI/hook template to stdout |
472
474
  | `ci:auto-refresh-agent-context --dry-run` / `--apply` | Refresh AGENTS.md pointers and CLAUDE.md standard sections |
473
475
  | `refresh-claude-md --dry-run` / `--apply` | Refresh only CLAUDE.md while preserving the user-owned section |
474
476
  | `p2:check-agent-context` | Check AGENTS.md / CLAUDE.md freshness |
475
477
  | `update-skills` | Update skills to latest version |
476
478
  | `phasegate:status` | Display overall harness health summary |
479
+ | `work-items:status --dry-run` / `--apply` | Derive WI status from artifacts and optionally update stale `description.md` frontmatter |
477
480
  | `phasegate:check-phase --unit <id>` | Check current phase for a Unit |
478
481
  | `check-change-category --paths <csv>` | Classify changed files into Quick Mode categories and report whether Full Mode is required (`--format json`, `--fail-on-full-required`) |
479
482
  | `baseline` | Create `.phasegate/baseline.json` snapshot for Phase A-2 retrofit grandfather (`--dry-run`, `--force`, `--paths <glob,glob,...>`, `--json`). `baseline.enabled` defaults to `true` since v0.71.0. |
@@ -517,9 +520,9 @@ The main path is ready for project use, but a few documented behaviors still req
517
520
 
518
521
  | Work Item | Title | Why it matters |
519
522
  |---|---|---|
520
- | **[WI-033](docs/inception/_cross/WI-033/description.md)** | Promote `doc-freshness` / `pointer-validation` to L4 validators | Both capabilities exist as `p2:check-freshness` / `p2:validate-pointers` CLI commands but are not registered as L4 validators, so `validate --layer L4` skips them. WI-033 plumbs them through `validator-system` so they run via the standard L4 path and presets. |
523
+ | **[WI-128](docs/inception/_cross/WI-128/description.md)** | Finish L4 operational rollout | `doc-freshness` and `pointer-validation` are registered as L4-004/L4-005 and also remain available through `p2:*` compatibility commands. WI-033 is closed; remaining rollout polish around scheduling, defaults, and operational docs is tracked separately. @work-item-id WI-128 |
521
524
 
522
- `requirement-test-matrix.json` auto-generation for L3 Nyquist Validation is not automated yet; see the L3 guide for the current manual setup.
525
+ `requirement-test-matrix.json` auto-generation for L3 Nyquist Validation is not automated yet; see [WI-125](docs/inception/_cross/WI-125/description.md) and the L3 guide for the current manual setup.
523
526
 
524
527
  ---
525
528
 
@@ -261,7 +261,7 @@ Commands exposed as npm scripts (`npm run <command>`).
261
261
  | `phasegate:status` | `--json` | Health summary |
262
262
  | `phasegate:check-ready` | `--json` | Phase Gate pass status for all stories |
263
263
  | `phasegate:check-phase` | `--unit <unitId>` `--json` | Current phase for a unit |
264
- | `phasegate:ci-check` | `--json` | All L3 validators |
264
+ | `phasegate:ci-check` | `--json` | Full CI check (L2-L4; disabled L4 is reported as skipped) |
265
265
  | `phasegate:detect-drift` | `--json` | Design-code drift report |
266
266
  | `phasegate:lint` | `--target <path>` `--json` | Lint via harness-api |
267
267
  | `phasegate:complete-check` | `--json` | L2-L4 full check |
@@ -147,8 +147,11 @@ L4 validators are designed to run on a weekly schedule and detect slow-moving dr
147
147
  | **drift-detect** | L4-001 | Bidirectional design-code drift detection. Compares design documents against the actual codebase to find divergence in either direction. |
148
148
  | **consistency-check** | L4-002 | Cross-document layer consistency. Ensures that references between design documents, ADRs, and code remain coherent. |
149
149
  | **dead-code** | L4-003 | Detects unused exports and unreachable code that should be removed. |
150
+ | **doc-freshness** | L4-004 | Checks design document freshness against the configured threshold. Also available through the `p2:check-freshness` compatibility command. |
151
+ | **pointer-validation** | L4-005 | Resolves and validates design document pointers. Also available through the `p2:validate-pointers` compatibility command. |
150
152
 
151
- `doc-freshness` and `pointer-validation` are implemented as `phase2-extensions` CLI commands (`p2:check-freshness` and `p2:validate-pointers`). They are not registered as L4 validators yet, so `validate --layer L4` does not run them until WI-033 promotes them into `validator-system`.
153
+ <!-- @work-item-id WI-116 -->
154
+ `doc-freshness` and `pointer-validation` are registered as L4-004/L4-005. The standalone `p2:check-freshness` and `p2:validate-pointers` commands remain as compatibility entry points; WI-033's remaining scope is operational rollout, not validator registration.
152
155
 
153
156
  ### Drift-detect design pointers
154
157
 
@@ -219,6 +222,8 @@ Each error catalog entry declares a `defaultSeverity` of `error` or `warning`. A
219
222
 
220
223
  **Opt-in to strict mode** via `phasegate.config.json` `validate.failOnWarning: true` or CLI `--fail-on-warning` (CLI > config). The `strict` preset defaults to `failOnWarning: true` to match the precedent set by the `ci-governance` preset adapter.
221
224
 
225
+ For L4 specifically, `validate --layer L4` is an explicit operator request and runs L4 validators even when `layers.L4.enabled` is false. `validate --layer all` and `phasegate:ci-check` honor disabled L4 as skipped results, so skipped L4 validators are visible in the report but do not become failures under `--fail-on-warning`. @work-item-id WI-107
226
+
222
227
  History: prior to v0.131.0, the aggregator's `failOnWarning` flag was effectively dead code (`hasFail = !result.passed || ...` masked it), so every warning-only fail produced exit 1 regardless of severity declaration. See ADR-017 for the rationale.
223
228
 
224
229
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.140.0",
3
+ "version": "0.142.0",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "MIT",
@@ -18,6 +18,20 @@ const RULE_SUGGESTIONS: Readonly<Record<string, string>> = {
18
18
  'no-comment-flood': '不要なコメントを整理し自己説明的なコードへ改善する',
19
19
  };
20
20
 
21
+ const METADATA_TAG_MESSAGE_PATTERN = /(@[a-z][a-zA-Z0-9]*)コメントが必要です/;
22
+
23
+ const resolveSuggestion = (ruleName: string, message: string): string => {
24
+ if (ruleName === 'require-unit-comment' || ruleName === 'require-layer-comment') {
25
+ const tagName = message.match(METADATA_TAG_MESSAGE_PATTERN)?.[1];
26
+
27
+ if (tagName) {
28
+ return `ファイル先頭に ${tagName} コメントを追加する`;
29
+ }
30
+ }
31
+
32
+ return RULE_SUGGESTIONS[ruleName] ?? '';
33
+ };
34
+
21
35
  /**
22
36
  * ViolationFormatterPort の実装。
23
37
  * RuleViolation をルール名から L1-001〜L1-008 コードへマッピングし、
@@ -40,7 +54,7 @@ export class HarnessErrorFormatterAdapter implements ViolationFormatterPort {
40
54
  violations.map((v) => {
41
55
  const ruleName = v.ruleName.toString();
42
56
  const code = mapRuleNameToCode(ruleName);
43
- const suggestion = RULE_SUGGESTIONS[ruleName] ?? '';
57
+ const suggestion = resolveSuggestion(ruleName, v.message);
44
58
 
45
59
  const entry: {
46
60
  code: string;
@@ -19,6 +19,36 @@ export interface TypeScriptSourceModuleAnalyzerAdapterDeps {
19
19
  readonly rootDir: string;
20
20
  }
21
21
 
22
+ const HARNESS_ROOT_PREFIX = 'scripts/harness/';
23
+ const TESTS_SEGMENT = '__tests__';
24
+ const TEST_SCOPE_SEGMENTS = new Set(['unit', 'integration', 'e2e']);
25
+
26
+ function deriveUnitNameFromPath(filePath: FilePath): string | null {
27
+ const normalizedPath = filePath.toString();
28
+ if (!normalizedPath.startsWith(HARNESS_ROOT_PREFIX)) {
29
+ return null;
30
+ }
31
+
32
+ const parts = normalizedPath.slice(HARNESS_ROOT_PREFIX.length).split('/');
33
+ if (parts.length < 2) {
34
+ return null;
35
+ }
36
+
37
+ if (parts[0] !== TESTS_SEGMENT) {
38
+ return parts[0] || null;
39
+ }
40
+
41
+ if (parts.length < 3) {
42
+ return parts[1] || null;
43
+ }
44
+
45
+ if (TEST_SCOPE_SEGMENTS.has(parts[1])) {
46
+ return parts[2] || null;
47
+ }
48
+
49
+ return parts[1] || null;
50
+ }
51
+
22
52
  /**
23
53
  * SourceModuleAnalyzerPort の実装。
24
54
  * TypeScript Compiler API を使用して各ファイルから
@@ -69,7 +99,7 @@ export class TypeScriptSourceModuleAnalyzerAdapter implements SourceModuleAnalyz
69
99
  SourceModuleSnapshot.create(
70
100
  {
71
101
  filePath,
72
- declaredUnit: unitResult.unitNames[0] ?? null,
102
+ declaredUnit: unitResult.unitNames[0] ?? deriveUnitNameFromPath(filePath),
73
103
  declaredLayer: layerResult.layerName,
74
104
  imports,
75
105
  anyTypeCount: anyCount,
@@ -11,7 +11,7 @@ export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefi
11
11
  return {
12
12
  project: { preset: resolvedConfig.project.preset },
13
13
  layers: {
14
- L2: { enabled: resolvedConfig.layers.L2.enabled },
14
+ L2: { enabled: resolvedConfig.layers.L2.enabled, validators: ['L2-001', 'L2-002', 'L2-003', 'L2-013'] },
15
15
  L3: { enabled: resolvedConfig.layers.L3.enabled },
16
16
  L4: { enabled: resolvedConfig.layers.L4.enabled, validators: resolvedConfig.layers.L4.validators },
17
17
  },
@@ -1,4 +1,5 @@
1
1
  // @layer domain
2
+ // @unit harness-api
2
3
  // command-dispatch-service.ts — CommandDispatchService Domain Service
3
4
 
4
5
  import { CommandRegistry } from './command-registry.js';
@@ -13,6 +14,7 @@ import type { BiomeLintPort } from '../ports/biome-lint-port.js';
13
14
  import type { ImpactAnalysisPort } from '../ports/impact-analysis-port.js';
14
15
  import type { ArtifactScannerPort } from '../ports/artifact-scanner-port.js';
15
16
  import type { ConfigQueryPort } from '../ports/config-query-port.js';
17
+ import type { LayerId } from '../value-objects/layer-health.js';
16
18
 
17
19
  export interface CommandDispatchPorts {
18
20
  validatorExecutionPort: ValidatorExecutionPort;
@@ -35,6 +37,29 @@ function makeError(message: string): HarnessError {
35
37
  return { code: 'HARNESS_ERROR', severity: 'error', message };
36
38
  }
37
39
 
40
+ type LiveValidationState = 'pass' | 'fail' | 'skipped' | 'not-run' | 'error';
41
+
42
+ function layerIdFromValidatorId(validatorId: string): LayerId | null {
43
+ const prefix = validatorId.slice(0, 2);
44
+ return prefix === 'L1' || prefix === 'L2' || prefix === 'L3' || prefix === 'L4' ? prefix : null;
45
+ }
46
+
47
+ function summarizeLayerResults(items: readonly { validatorId: string; passed: boolean; skipped?: boolean }[]): Partial<Record<LayerId, LiveValidationState>> {
48
+ const result: Partial<Record<LayerId, LiveValidationState>> = {};
49
+ for (const layerId of ['L2', 'L3', 'L4'] as const) {
50
+ const layerItems = items.filter((item) => layerIdFromValidatorId(item.validatorId) === layerId);
51
+ if (layerItems.length === 0) continue;
52
+ if (layerItems.every((item) => item.skipped === true)) {
53
+ result[layerId] = 'skipped';
54
+ } else if (layerItems.some((item) => !item.passed && item.skipped !== true)) {
55
+ result[layerId] = 'fail';
56
+ } else {
57
+ result[layerId] = 'pass';
58
+ }
59
+ }
60
+ return result;
61
+ }
62
+
38
63
  const KNOWN_COMMANDS = new Set([
39
64
  'phasegate:check-ready',
40
65
  'phasegate:check-phase',
@@ -131,7 +156,7 @@ export class CommandDispatchService {
131
156
  }
132
157
 
133
158
  case 'phasegate:ci-check': {
134
- const validatorResults = await this.ports.validatorExecutionPort.runL3Validators();
159
+ const validatorResults = await this.ports.validatorExecutionPort.runAllValidators();
135
160
  const result = CiCheckResult.fromResults(validatorResults);
136
161
  if (result.allPassed) {
137
162
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, result);
@@ -152,13 +177,23 @@ export class CommandDispatchService {
152
177
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, result);
153
178
  return { status: 'pass', errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
154
179
  }
155
- const errors = drifts.map((d) => makeError(`Drift detected in ${d.unit}: ${d.element}`));
156
- const r = HarnessApiResponse.fail(errors, { ...summary, failed: 1 }, result);
157
- return { status: 'fail', errors: r.errors, summary: r.summary, data: result as unknown as T, exitCode: 1 };
180
+ const r = HarnessApiResponse.pass({ ...summary, passed: 1, warnings: drifts.length }, result);
181
+ return { status: 'pass', errors: [], summary: r.summary, data: result as unknown as T, exitCode: 0 };
158
182
  }
159
183
 
160
184
  case 'phasegate:status': {
161
185
  const scanResult = await this.ports.artifactScannerPort.scan();
186
+ const liveValidationByLayer: Partial<Record<LayerId, LiveValidationState>> = {};
187
+ try {
188
+ const [lintResult, validatorResults] = await Promise.all([
189
+ this.ports.biomeLintPort.runLint(),
190
+ this.ports.validatorExecutionPort.runAllValidators(),
191
+ ]);
192
+ liveValidationByLayer.L1 = lintResult.passed ? 'pass' : 'fail';
193
+ Object.assign(liveValidationByLayer, summarizeLayerResults(validatorResults));
194
+ } catch {
195
+ liveValidationByLayer.L1 = liveValidationByLayer.L1 ?? 'error';
196
+ }
162
197
  let presetInfo = { name: 'standard' as const, enabledLayers: ['L1', 'L2', 'L3'] as ('L1' | 'L2' | 'L3' | 'L4')[] };
163
198
  const configPort = this.ports.configQueryPort;
164
199
  if (configPort.getPresetInfo) {
@@ -172,6 +207,7 @@ export class CommandDispatchService {
172
207
  presetInfo,
173
208
  configSummary: { configPath: 'phasegate.config.json', lastModified: '', version: '2' },
174
209
  phaseGateSummary: { totalStories: 0, passedStories: 0, pendingStories: 0 },
210
+ liveValidationByLayer,
175
211
  });
176
212
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 }, statusSummary);
177
213
  return { status: 'pass', errors: [], summary: r.summary, data: statusSummary as unknown as T, exitCode: 0 };
@@ -1,4 +1,5 @@
1
1
  // @layer domain
2
+ // @unit harness-api
2
3
  // status-derivation-service.ts — StatusDerivationService Domain Service
3
4
 
4
5
  import { LayerHealth, type LayerId } from '../value-objects/layer-health.js';
@@ -88,27 +89,44 @@ export class StatusDerivationService {
88
89
  presetInfo: PresetInfo;
89
90
  configSummary: ConfigSummary;
90
91
  phaseGateSummary: PhaseGateSummary;
92
+ liveValidationByLayer?: Partial<Record<LayerId, 'pass' | 'fail' | 'skipped' | 'not-run' | 'error'>>;
91
93
  }): HarnessStatusSummary {
92
- const { scanResult, presetInfo, configSummary, phaseGateSummary } = input;
94
+ const { scanResult, presetInfo, configSummary, phaseGateSummary, liveValidationByLayer } = input;
93
95
 
94
96
  const layers: LayerHealth[] = ALL_LAYER_IDS.map((layerId) => {
95
97
  const enabled = presetInfo.enabledLayers.includes(layerId);
98
+ const configurationState = enabled ? 'enabled' : 'disabled';
96
99
 
97
100
  // Find from derivedLayerHealth
98
101
  const existing = scanResult.derivedLayerHealth.find((l) => l.layerId === layerId);
102
+ const layerArtifacts = scanResult.foundArtifacts.filter((a) => getLayerId(a) === layerId);
103
+ const hasPresent = layerArtifacts.some((a) => a.present === true);
104
+ const cachedArtifactState = hasPresent ? 'present' : 'missing';
105
+ const liveValidationState = liveValidationByLayer?.[layerId] ?? 'not-run';
106
+ const liveLastResult =
107
+ liveValidationState === 'pass' || liveValidationState === 'fail'
108
+ ? liveValidationState
109
+ : undefined;
99
110
 
100
111
  if (enabled) {
101
- // Check artifacts
102
- const layerArtifacts = scanResult.foundArtifacts.filter((a) => getLayerId(a) === layerId);
103
- const hasPresent = layerArtifacts.some((a) => a.present === true);
104
-
105
- if (existing && existing.lastResult) {
106
- return LayerHealth.create({ layerId, enabled, lastResult: existing.lastResult });
107
- }
108
- const lastResult = hasPresent ? 'pass' : 'unknown';
109
- return LayerHealth.create({ layerId, enabled, lastResult });
112
+ const lastResult = liveLastResult ?? existing?.lastResult ?? (hasPresent ? 'pass' : 'unknown');
113
+ return LayerHealth.create({
114
+ layerId,
115
+ enabled,
116
+ lastResult,
117
+ configurationState,
118
+ cachedArtifactState,
119
+ liveValidationState,
120
+ });
110
121
  } else {
111
- return LayerHealth.create({ layerId, enabled: false, lastResult: undefined });
122
+ return LayerHealth.create({
123
+ layerId,
124
+ enabled: false,
125
+ lastResult: undefined,
126
+ configurationState,
127
+ cachedArtifactState,
128
+ liveValidationState,
129
+ });
112
130
  }
113
131
  });
114
132
 
@@ -1,4 +1,5 @@
1
1
  // @layer domain
2
+ // @unit harness-api
2
3
  // ci-check-result.ts — CiCheckResult Value Object
3
4
 
4
5
  import type { HarnessError } from './harness-api-response.js';
@@ -6,6 +7,7 @@ import type { HarnessError } from './harness-api-response.js';
6
7
  export interface ValidatorCheckItem {
7
8
  validatorId: string;
8
9
  passed: boolean;
10
+ skipped?: boolean;
9
11
  errors?: readonly HarnessError[];
10
12
  }
11
13
 
@@ -29,8 +31,8 @@ export class CiCheckResult {
29
31
  if (!props.validatorResults || props.validatorResults.length === 0) {
30
32
  throw new Error('EmptyValidatorResultsError: validatorResults must have at least one item (INV-5)');
31
33
  }
32
- // INV-6: allPassed === validatorResults.every(r => r.passed)
33
- const computedAllPassed = props.validatorResults.every((r) => r.passed);
34
+ // INV-6: allPassed === validatorResults.every(r => r.passed || r.skipped)
35
+ const computedAllPassed = props.validatorResults.every((r) => r.passed || r.skipped);
34
36
  if (props.allPassed !== computedAllPassed) {
35
37
  throw new Error(
36
38
  `HarnessApiDomainError: allPassed=${props.allPassed} does not match validatorResults state (computed: ${computedAllPassed})`
@@ -43,12 +45,12 @@ export class CiCheckResult {
43
45
  if (!validatorResults || validatorResults.length === 0) {
44
46
  throw new Error('EmptyValidatorResultsError: validatorResults must have at least one item (INV-5)');
45
47
  }
46
- const allPassed = validatorResults.every((r) => r.passed);
48
+ const allPassed = validatorResults.every((r) => r.passed || r.skipped);
47
49
  return new CiCheckResult(validatorResults, allPassed);
48
50
  }
49
51
 
50
52
  getFailedValidators(): readonly ValidatorCheckItem[] {
51
- return this.validatorResults.filter((r) => !r.passed);
53
+ return this.validatorResults.filter((r) => !r.passed && !r.skipped);
52
54
  }
53
55
 
54
56
  collectAllErrors(): readonly HarnessError[] {
@@ -1,4 +1,5 @@
1
1
  // @layer domain
2
+ // @unit harness-api
2
3
  // drift-report-summary.ts — DriftReportSummary Value Object
3
4
 
4
5
  export interface DriftItem {
@@ -8,18 +9,117 @@ export interface DriftItem {
8
9
  recommendation: string;
9
10
  }
10
11
 
12
+ type DriftCategory =
13
+ | 'code-missing-design'
14
+ | 'design-missing-code'
15
+ | 'missing-pointer'
16
+ | 'design-granularity-mismatch'
17
+ | 'uncategorized';
18
+ type DriftSeverity = 'info' | 'warning' | 'error';
19
+
20
+ export interface ActionableDriftItem extends DriftItem {
21
+ category: DriftCategory;
22
+ severity: DriftSeverity;
23
+ nextAction: string;
24
+ }
25
+
26
+ export interface DriftCategorySummary {
27
+ category: DriftCategory;
28
+ severity: DriftSeverity;
29
+ count: number;
30
+ nextAction: string;
31
+ }
32
+
11
33
  export interface DriftReportSummaryProps {
12
34
  drifts: readonly DriftItem[];
13
35
  totalCount: number;
36
+ sampleLimit?: number;
37
+ }
38
+
39
+ const DEFAULT_SAMPLE_LIMIT = 20;
40
+
41
+ function classifyDrift(drift: DriftItem): { category: DriftCategory; severity: DriftSeverity; nextAction: string } {
42
+ const direction = drift.direction.toLowerCase();
43
+ const recommendation = drift.recommendation.toLowerCase();
44
+ const element = drift.element.toLowerCase();
45
+
46
+ if (recommendation.includes('pointer') || recommendation.includes('@work-item-id')) {
47
+ return {
48
+ category: 'missing-pointer',
49
+ severity: 'warning',
50
+ nextAction: 'Add or repair the traceability pointer before changing gating policy.',
51
+ };
52
+ }
53
+ if (element.includes('heading') || recommendation.includes('granularity')) {
54
+ return {
55
+ category: 'design-granularity-mismatch',
56
+ severity: 'info',
57
+ nextAction: 'Review the design granularity or baseline this class of advisory drift.',
58
+ };
59
+ }
60
+ if (direction.includes('code') && direction.includes('design')) {
61
+ return {
62
+ category: direction.indexOf('code') < direction.indexOf('design') ? 'code-missing-design' : 'design-missing-code',
63
+ severity: 'warning',
64
+ nextAction:
65
+ direction.indexOf('code') < direction.indexOf('design')
66
+ ? 'Update the matching product/construction docs with the implementation contract.'
67
+ : 'Implement the described design element or remove stale design text.',
68
+ };
69
+ }
70
+ return {
71
+ category: 'uncategorized',
72
+ severity: 'warning',
73
+ nextAction: 'Inspect the sampled drift and assign a narrower category or baseline rule.',
74
+ };
75
+ }
76
+
77
+ function enrichDrift(drift: DriftItem): ActionableDriftItem {
78
+ return { ...drift, ...classifyDrift(drift) };
79
+ }
80
+
81
+ function summarizeByCategory(drifts: readonly ActionableDriftItem[]): readonly DriftCategorySummary[] {
82
+ const byCategory = new Map<DriftCategory, DriftCategorySummary>();
83
+ for (const drift of drifts) {
84
+ const existing = byCategory.get(drift.category);
85
+ if (existing) {
86
+ byCategory.set(drift.category, { ...existing, count: existing.count + 1 });
87
+ } else {
88
+ byCategory.set(drift.category, {
89
+ category: drift.category,
90
+ severity: drift.severity,
91
+ count: 1,
92
+ nextAction: drift.nextAction,
93
+ });
94
+ }
95
+ }
96
+ return [...byCategory.values()].sort((a, b) => b.count - a.count || a.category.localeCompare(b.category));
14
97
  }
15
98
 
16
99
  export class DriftReportSummary {
17
- readonly drifts: readonly DriftItem[];
100
+ readonly drifts: readonly ActionableDriftItem[];
18
101
  readonly totalCount: number;
102
+ readonly rawDriftCount: number;
103
+ readonly sampleLimit: number;
104
+ readonly truncated: boolean;
105
+ readonly categorySummaries: readonly DriftCategorySummary[];
106
+ readonly actionPlan: readonly DriftCategorySummary[];
19
107
 
20
- private constructor(drifts: readonly DriftItem[], totalCount: number) {
21
- this.drifts = Object.freeze([...drifts]);
108
+ private constructor(
109
+ drifts: readonly DriftItem[],
110
+ totalCount: number,
111
+ sampleLimit: number = DEFAULT_SAMPLE_LIMIT,
112
+ summarySource: readonly DriftItem[] = drifts,
113
+ ) {
114
+ const enriched = drifts.map(enrichDrift);
115
+ const enrichedSummarySource = summarySource.map(enrichDrift);
116
+ this.drifts = Object.freeze(enriched);
22
117
  this.totalCount = totalCount;
118
+ this.rawDriftCount = totalCount;
119
+ this.sampleLimit = sampleLimit;
120
+ this.truncated = totalCount > enriched.length;
121
+ this.categorySummaries = Object.freeze(summarizeByCategory(enrichedSummarySource));
122
+ this.actionPlan = Object.freeze(this.categorySummaries.slice(0, 5));
23
123
  Object.freeze(this);
24
124
  }
25
125
 
@@ -30,18 +130,18 @@ export class DriftReportSummary {
30
130
  `HarnessApiDomainError: totalCount=${props.totalCount} does not match drifts.length=${props.drifts.length} (INV-7)`
31
131
  );
32
132
  }
33
- return new DriftReportSummary(props.drifts, props.totalCount);
133
+ return new DriftReportSummary(props.drifts, props.totalCount, props.sampleLimit);
34
134
  }
35
135
 
36
- static fromDrifts(drifts: readonly DriftItem[]): DriftReportSummary {
37
- return new DriftReportSummary(drifts, drifts.length);
136
+ static fromDrifts(drifts: readonly DriftItem[], sampleLimit: number = DEFAULT_SAMPLE_LIMIT): DriftReportSummary {
137
+ return new DriftReportSummary(drifts.slice(0, sampleLimit), drifts.length, sampleLimit, drifts);
38
138
  }
39
139
 
40
140
  hasDrift(): boolean {
41
- return this.drifts.length > 0;
141
+ return this.totalCount > 0;
42
142
  }
43
143
 
44
- filterByUnit(unitId: string): readonly DriftItem[] {
144
+ filterByUnit(unitId: string): readonly ActionableDriftItem[] {
45
145
  return this.drifts.filter((d) => d.unit === unitId);
46
146
  }
47
147
  }
@@ -1,8 +1,12 @@
1
1
  // @layer domain
2
+ // @unit harness-api
2
3
  // layer-health.ts — LayerHealth Value Object
3
4
 
4
5
  export type LayerId = 'L1' | 'L2' | 'L3' | 'L4';
5
6
  type LastResult = 'pass' | 'fail' | 'unknown' | undefined;
7
+ type ConfigurationState = 'enabled' | 'disabled';
8
+ type CachedArtifactState = 'present' | 'missing' | 'unknown';
9
+ type LiveValidationState = 'pass' | 'fail' | 'skipped' | 'not-run' | 'error';
6
10
 
7
11
  const VALID_LAYER_IDS: readonly LayerId[] = ['L1', 'L2', 'L3', 'L4'];
8
12
  const VALID_LAST_RESULTS: readonly string[] = ['pass', 'fail', 'unknown'];
@@ -11,17 +15,33 @@ export interface LayerHealthProps {
11
15
  layerId: string;
12
16
  enabled: boolean;
13
17
  lastResult?: string;
18
+ configurationState?: ConfigurationState;
19
+ cachedArtifactState?: CachedArtifactState;
20
+ liveValidationState?: LiveValidationState;
14
21
  }
15
22
 
16
23
  export class LayerHealth {
17
24
  readonly layerId: LayerId;
18
25
  readonly enabled: boolean;
19
26
  readonly lastResult: LastResult;
27
+ readonly configurationState: ConfigurationState;
28
+ readonly cachedArtifactState: CachedArtifactState;
29
+ readonly liveValidationState: LiveValidationState;
20
30
 
21
- private constructor(layerId: LayerId, enabled: boolean, lastResult: LastResult) {
31
+ private constructor(
32
+ layerId: LayerId,
33
+ enabled: boolean,
34
+ lastResult: LastResult,
35
+ configurationState: ConfigurationState,
36
+ cachedArtifactState: CachedArtifactState,
37
+ liveValidationState: LiveValidationState,
38
+ ) {
22
39
  this.layerId = layerId;
23
40
  this.enabled = enabled;
24
41
  this.lastResult = lastResult;
42
+ this.configurationState = configurationState;
43
+ this.cachedArtifactState = cachedArtifactState;
44
+ this.liveValidationState = liveValidationState;
25
45
  Object.freeze(this);
26
46
  }
27
47
 
@@ -35,7 +55,10 @@ export class LayerHealth {
35
55
  return new LayerHealth(
36
56
  props.layerId as LayerId,
37
57
  props.enabled,
38
- props.lastResult as LastResult
58
+ props.lastResult as LastResult,
59
+ props.configurationState ?? (props.enabled ? 'enabled' : 'disabled'),
60
+ props.cachedArtifactState ?? 'unknown',
61
+ props.liveValidationState ?? 'not-run',
39
62
  );
40
63
  }
41
64