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.
- package/CHANGELOG.md +19 -0
- package/README.ja.md +7 -3
- package/README.md +8 -5
- package/docs/guide/cli-reference.md +1 -1
- package/docs/guide/layer-model.md +6 -1
- package/package.json +1 -1
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/harness-error-formatter-adapter.ts +15 -1
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +31 -1
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +1 -1
- package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +40 -4
- package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +29 -11
- package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +6 -4
- package/scripts/harness/harness-api/domain/value-objects/drift-report-summary.ts +108 -8
- package/scripts/harness/harness-api/domain/value-objects/layer-health.ts +25 -2
- package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts +2 -0
- package/scripts/harness/integrations/pre-commit.ts +7 -4
- package/scripts/harness/main.ts +37 -1
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +47 -1
- package/scripts/harness/traceability-model/application/usecases/apply-work-item-status-usecase.ts +30 -0
- package/scripts/harness/traceability-model/application/usecases/derive-work-item-status-usecase.ts +27 -0
- package/scripts/harness/traceability-model/composition-root.ts +20 -0
- package/scripts/harness/traceability-model/domain/ports/work-item-status-port.ts +16 -0
- package/scripts/harness/traceability-model/domain/services/work-item-status-derivation-service.ts +121 -0
- package/scripts/harness/traceability-model/domain/value-objects/work-item-status-report.ts +47 -0
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +240 -0
- package/scripts/harness/traceability-model/presentation/cli/work-item-status-command-handler.ts +103 -0
- package/scripts/harness/validator-system/application/use-cases/run-l1-validators-usecase.ts +1 -44
- package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +30 -0
- package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +1 -1
- package/scripts/harness/validator-system/composition-root.ts +16 -13
- package/scripts/harness/validator-system/domain/services/cli-e2e-test-existence-service.ts +36 -5
- package/scripts/harness/validator-system/domain/value-objects/cli-e2e-test-coverage-report.ts +8 -2
- package/scripts/harness/validator-system/infrastructure/adapters/e2e-test-file-registry-adapter.ts +12 -3
- 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-
|
|
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
|
|
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 <
|
|
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-
|
|
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` |
|
|
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
|
-
|
|
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
package/scripts/harness/biome-ast-engine/infrastructure/adapters/harness-error-formatter-adapter.ts
CHANGED
|
@@ -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 =
|
|
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] ??
|
|
102
|
+
declaredUnit: unitResult.unitNames[0] ?? deriveUnitNameFromPath(filePath),
|
|
73
103
|
declaredLayer: layerResult.layerName,
|
|
74
104
|
imports,
|
|
75
105
|
anyTypeCount: anyCount,
|
package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts
CHANGED
|
@@ -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.
|
|
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
|
|
156
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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({
|
|
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
|
|
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(
|
|
21
|
-
|
|
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.
|
|
141
|
+
return this.totalCount > 0;
|
|
42
142
|
}
|
|
43
143
|
|
|
44
|
-
filterByUnit(unitId: string): readonly
|
|
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(
|
|
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
|
|