phasegate 0.142.0 → 0.144.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 +8 -0
- package/README.ja.md +5 -4
- package/README.md +6 -5
- package/docs/guide/layer-model.md +3 -2
- package/docs/guide/quick-vs-full-mode.md +1 -1
- package/docs/principles/testing-rules.md +18 -0
- package/docs/templates/ci/aidlc-gate.yml +26 -3
- package/docs/templates/hooks/pre-push +6 -0
- package/package.json +1 -1
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +2 -2
- package/scripts/harness/integrations/pre-commit.ts +251 -3
- package/scripts/harness/main.ts +23 -0
- package/scripts/harness/quick-mode/domain/value-objects/validator-relaxation-profile.ts +4 -3
- package/scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts +2 -1
- package/scripts/harness/quick-mode/infrastructure/adapters/validator-system-validator-id-registry-adapter.ts +2 -1
- package/scripts/harness/setup/skill-deployer.ts +19 -0
- package/scripts/harness/traceability-model/application/usecases/apply-work-item-status-usecase.ts +29 -4
- package/scripts/harness/traceability-model/domain/services/work-item-status-derivation-service.ts +20 -4
- package/scripts/harness/traceability-model/domain/value-objects/work-item-status-report.ts +10 -1
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +2 -1
- package/scripts/harness/traceability-model/presentation/cli/work-item-status-command-handler.ts +13 -4
- package/scripts/harness/validator-system/application/mappers/validation-result-contract-mapper.ts +8 -4
- package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +28 -0
- package/scripts/harness/validator-system/composition-root.ts +5 -1
- package/scripts/harness/validator-system/domain/ports/work-item-status-policy-port.ts +11 -0
- package/scripts/harness/validator-system/domain/value-objects/test-quality-semantics.ts +58 -0
- package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +2 -0
- package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-test-quality-analyzer-adapter.ts +439 -21
- package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +1 -1
- package/scripts/harness/validator-system/infrastructure/adapters/traceability-work-item-status-policy-adapter.ts +52 -0
- package/templates/.husky/pre-push +1 -0
package/scripts/harness/main.ts
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
deployHookScripts,
|
|
47
47
|
deployHuskyCommitMsgHook,
|
|
48
48
|
deployHuskyHook,
|
|
49
|
+
deployHuskyPrePushHook,
|
|
49
50
|
deploySkills,
|
|
50
51
|
getCategoryForSkill,
|
|
51
52
|
getDeployedVersion,
|
|
@@ -154,6 +155,7 @@ Commands:
|
|
|
154
155
|
hook <pre-tool-use|post-tool-use|stop|session-start|user-prompt-submit> Run agent hook (reads JSON from stdin; writes JSON to stdout for session-start/user-prompt-submit)
|
|
155
156
|
pre-commit Run L2 pre-commit validators on staged files
|
|
156
157
|
commit-msg <message-file> Validate commit message trailers against staged files
|
|
158
|
+
bypass:audit --base <ref> [--head <ref>] Audit bypass evidence for a push/CI commit range
|
|
157
159
|
delegate-sonnet [...args] Delegate task to Sonnet 4.6 (forwards args to scripts/delegate-sonnet.sh)
|
|
158
160
|
|
|
159
161
|
Skills:
|
|
@@ -316,6 +318,8 @@ Options:
|
|
|
316
318
|
--apply Update only the status line in each stale description.md frontmatter.
|
|
317
319
|
--id <WI-XXX> Limit report/apply to one work item.
|
|
318
320
|
--fail-on-stale Return exit code 1 when dry-run finds stale status.
|
|
321
|
+
--allow-downgrade Allow apply to lower a frontmatter status.
|
|
322
|
+
--changed-only Reserve apply scope for changed WI files; currently accepted as a no-op policy flag.
|
|
319
323
|
--json Output machine-readable JSON.
|
|
320
324
|
--help, -h Show this help`,
|
|
321
325
|
"phasegate:detect-drift": `Usage: phasegate phasegate:detect-drift [options]
|
|
@@ -716,6 +720,7 @@ async function main(): Promise<void> {
|
|
|
716
720
|
const withHusky = hasFlag(args, "--with-husky");
|
|
717
721
|
const huskyResult = withHusky ? await deployHuskyHook(harnessRoot, rootDir) : null;
|
|
718
722
|
const huskyCommitMsgResult = withHusky ? await deployHuskyCommitMsgHook(harnessRoot, rootDir) : null;
|
|
723
|
+
const huskyPrePushResult = withHusky ? await deployHuskyPrePushHook(harnessRoot, rootDir) : null;
|
|
719
724
|
const ciWorkflowResult = withCi ? await deployCiWorkflows(harnessRoot, rootDir) : null;
|
|
720
725
|
console.log(
|
|
721
726
|
`✓ Skills deployed to ${result.targetDir} (${result.deployedSkills.length} skills, set: ${skillSet})`,
|
|
@@ -781,6 +786,13 @@ async function main(): Promise<void> {
|
|
|
781
786
|
console.log(` .husky/commit-msg already exists, skipped`);
|
|
782
787
|
}
|
|
783
788
|
}
|
|
789
|
+
if (huskyPrePushResult !== null) {
|
|
790
|
+
if (huskyPrePushResult.created) {
|
|
791
|
+
console.log(`✓ .husky/pre-push deployed`);
|
|
792
|
+
} else {
|
|
793
|
+
console.log(` .husky/pre-push already exists, skipped`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
784
796
|
if (ciWorkflowResult !== null) {
|
|
785
797
|
if (ciWorkflowResult.copiedFiles.length > 0) {
|
|
786
798
|
console.log(`✓ CI workflows deployed (${ciWorkflowResult.copiedFiles.length} files)`);
|
|
@@ -983,6 +995,8 @@ async function main(): Promise<void> {
|
|
|
983
995
|
dryRun: hasFlag(args, "--dry-run"),
|
|
984
996
|
apply: hasFlag(args, "--apply"),
|
|
985
997
|
failOnStale: hasFlag(args, "--fail-on-stale"),
|
|
998
|
+
allowDowngrade: hasFlag(args, "--allow-downgrade"),
|
|
999
|
+
changedOnly: hasFlag(args, "--changed-only"),
|
|
986
1000
|
id: parseFlag(args, "--id"),
|
|
987
1001
|
json,
|
|
988
1002
|
});
|
|
@@ -1583,6 +1597,15 @@ Examples:
|
|
|
1583
1597
|
break;
|
|
1584
1598
|
}
|
|
1585
1599
|
|
|
1600
|
+
case "bypass:audit": {
|
|
1601
|
+
const preCommitPath = join(harnessRoot, "scripts/harness/integrations/pre-commit.js");
|
|
1602
|
+
const preCommitMod = (await import(preCommitPath)) as {
|
|
1603
|
+
runBypassAuditCli: (args: readonly string[]) => Promise<void>;
|
|
1604
|
+
};
|
|
1605
|
+
await preCommitMod.runBypassAuditCli(args.slice(1));
|
|
1606
|
+
break;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1586
1609
|
case "delegate-sonnet": {
|
|
1587
1610
|
const { spawn } = await import("node:child_process");
|
|
1588
1611
|
const scriptPath = join(harnessRoot, "scripts/delegate-sonnet.sh");
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer domain
|
|
3
3
|
* @unit quick-mode
|
|
4
|
+
* @work-item-id WI-140
|
|
4
5
|
*
|
|
5
6
|
* Quick Mode時のバリデータ実行構成を表す値オブジェクト
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
const L2_IDS = ['L2-001', 'L2-002', 'L2-003'] as const;
|
|
9
|
+
const L2_IDS = ['L2-001', 'L2-002', 'L2-003', 'L2-014'] as const;
|
|
9
10
|
const L3_IDS = ['L3-001', 'L3-002', 'L3-003', 'L3-004'] as const;
|
|
10
11
|
|
|
11
12
|
type L2Id = (typeof L2_IDS)[number];
|
|
@@ -48,7 +49,7 @@ export class ValidatorRelaxationProfile {
|
|
|
48
49
|
return new ValidatorRelaxationProfile({
|
|
49
50
|
levelDependencyRelaxed: false,
|
|
50
51
|
l1: { all: true },
|
|
51
|
-
l2: { maintained: ['L2-002', 'L2-003'], skipped: ['L2-001'] },
|
|
52
|
+
l2: { maintained: ['L2-002', 'L2-003', 'L2-014'], skipped: ['L2-001'] },
|
|
52
53
|
l3: { maintained: ['L3-001'], skipped: ['L3-002', 'L3-003', 'L3-004'] },
|
|
53
54
|
l4: { all: false },
|
|
54
55
|
phaseExecution: { twoPhaseRequired: false },
|
|
@@ -65,7 +66,7 @@ export class ValidatorRelaxationProfile {
|
|
|
65
66
|
}): ValidatorRelaxationProfile {
|
|
66
67
|
const { l2, l3 } = params;
|
|
67
68
|
|
|
68
|
-
// INV-P5: l2.maintained ∪ l2.skipped = {L2-001, L2-002, L2-003}
|
|
69
|
+
// INV-P5: l2.maintained ∪ l2.skipped = {L2-001, L2-002, L2-003, L2-014}
|
|
69
70
|
const l2Union = [...l2.maintained, ...l2.skipped].sort();
|
|
70
71
|
const l2Expected = [...L2_IDS].sort();
|
|
71
72
|
if (JSON.stringify(l2Union) !== JSON.stringify(l2Expected)) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer infrastructure
|
|
3
3
|
* @unit quick-mode
|
|
4
|
+
* @work-item-id WI-140
|
|
4
5
|
*
|
|
5
6
|
* phasegate.config.json から QuickModeConfig を取得する Adapter
|
|
6
7
|
*/
|
|
@@ -25,7 +26,7 @@ export class HarnessConfigParseError extends Error {
|
|
|
25
26
|
|
|
26
27
|
const DEFAULT_QUICK_MODE_CONFIG = {
|
|
27
28
|
allowedCategories: ['bugfix', 'docs', 'test', 'config'],
|
|
28
|
-
maintainedLayers: ['L1', 'L2-002', 'L2-003', 'L3-001'],
|
|
29
|
+
maintainedLayers: ['L1', 'L2-002', 'L2-003', 'L2-014', 'L3-001'],
|
|
29
30
|
relaxedGates: ['L2-001', 'L3-002', 'L3-003', 'L3-004', 'L4'],
|
|
30
31
|
};
|
|
31
32
|
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer infrastructure
|
|
3
3
|
* @unit quick-mode
|
|
4
|
+
* @work-item-id WI-140
|
|
4
5
|
*
|
|
5
6
|
* integration_contract.md §9 の確定ID一覧を静的定義で保持する ValidatorIdRegistry Adapter
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
const STATIC_VALIDATOR_IDS: readonly string[] = Object.freeze([
|
|
9
10
|
'L1-001', 'L1-002', 'L1-003', 'L1-004', 'L1-005', 'L1-006', 'L1-007', 'L1-008',
|
|
10
|
-
'L2-001', 'L2-002', 'L2-003',
|
|
11
|
+
'L2-001', 'L2-002', 'L2-003', 'L2-014',
|
|
11
12
|
'L3-001', 'L3-002', 'L3-003', 'L3-004',
|
|
12
13
|
'L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005',
|
|
13
14
|
]);
|
|
@@ -617,6 +617,25 @@ export async function deployHuskyCommitMsgHook(
|
|
|
617
617
|
return { created: true, path: targetPath };
|
|
618
618
|
}
|
|
619
619
|
|
|
620
|
+
export async function deployHuskyPrePushHook(
|
|
621
|
+
harnessRoot: string,
|
|
622
|
+
projectRoot: string,
|
|
623
|
+
): Promise<DeployHuskyHookResult> {
|
|
624
|
+
const targetPath = join(projectRoot, ".husky", "pre-push");
|
|
625
|
+
|
|
626
|
+
try {
|
|
627
|
+
await fs.access(targetPath);
|
|
628
|
+
return { created: false, path: targetPath };
|
|
629
|
+
} catch {}
|
|
630
|
+
|
|
631
|
+
const sourcePath = join(harnessRoot, "docs", "templates", "hooks", "pre-push");
|
|
632
|
+
await fs.mkdir(join(projectRoot, ".husky"), { recursive: true });
|
|
633
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
634
|
+
await fs.chmod(targetPath, 0o755);
|
|
635
|
+
|
|
636
|
+
return { created: true, path: targetPath };
|
|
637
|
+
}
|
|
638
|
+
|
|
620
639
|
export interface DeployCodexHooksResult {
|
|
621
640
|
created: boolean;
|
|
622
641
|
path: string;
|
package/scripts/harness/traceability-model/application/usecases/apply-work-item-status-usecase.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// @unit traceability-model
|
|
2
2
|
// @layer application
|
|
3
|
-
// @work-item-id WI-126
|
|
3
|
+
// @work-item-id WI-126 / WI-140
|
|
4
4
|
|
|
5
5
|
import type { WorkItemStatusPort } from "../../domain/ports/work-item-status-port.js";
|
|
6
|
-
import type { WorkItemStatusApplyResult } from "../../domain/value-objects/work-item-status-report.js";
|
|
6
|
+
import type { WorkItemStatusApplyResult, WorkItemStatusReport } from "../../domain/value-objects/work-item-status-report.js";
|
|
7
7
|
import type { DeriveWorkItemStatusUseCase } from "./derive-work-item-status-usecase.js";
|
|
8
|
+
import type { WorkItemStatus } from "../../domain/value-objects/work-item-frontmatter.js";
|
|
8
9
|
|
|
9
10
|
export interface ApplyWorkItemStatusUseCaseDeps {
|
|
10
11
|
readonly deriveWorkItemStatusUseCase: Pick<DeriveWorkItemStatusUseCase, "execute">;
|
|
@@ -20,11 +21,35 @@ export class ApplyWorkItemStatusUseCase {
|
|
|
20
21
|
this.workItemStatusPort = deps.workItemStatusPort;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
async execute(input: { readonly id?: string } = {}): Promise<WorkItemStatusApplyResult> {
|
|
24
|
+
async execute(input: { readonly id?: string; readonly allowDowngrade?: boolean; readonly changedOnly?: boolean } = {}): Promise<WorkItemStatusApplyResult> {
|
|
24
25
|
const reports = await this.deriveWorkItemStatusUseCase.execute();
|
|
25
26
|
const targetReports = input.id
|
|
26
27
|
? reports.filter((report) => report.id === input.id)
|
|
27
28
|
: reports;
|
|
28
|
-
|
|
29
|
+
const blocked = input.allowDowngrade ? [] : targetReports.filter((report) => this.isDowngrade(report));
|
|
30
|
+
const allowed = input.allowDowngrade ? targetReports : targetReports.filter((report) => !this.isDowngrade(report));
|
|
31
|
+
const result = await this.workItemStatusPort.applyDerivedStatuses(allowed);
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
updated: result.updated,
|
|
34
|
+
unchanged: result.unchanged,
|
|
35
|
+
blocked: Object.freeze([...result.blocked, ...blocked]),
|
|
36
|
+
});
|
|
29
37
|
}
|
|
38
|
+
|
|
39
|
+
private isDowngrade(report: WorkItemStatusReport): boolean {
|
|
40
|
+
if (!report.stale) return false;
|
|
41
|
+
return statusOrder(report.derivedStatus) < statusOrder(report.currentStatus);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const STATUS_ORDER: Record<WorkItemStatus, number> = {
|
|
46
|
+
drafted: 0,
|
|
47
|
+
reflected: 1,
|
|
48
|
+
implemented: 2,
|
|
49
|
+
tested: 3,
|
|
50
|
+
completed: 3,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function statusOrder(status: WorkItemStatus): number {
|
|
54
|
+
return STATUS_ORDER[status];
|
|
30
55
|
}
|
package/scripts/harness/traceability-model/domain/services/work-item-status-derivation-service.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @unit traceability-model
|
|
2
2
|
// @layer domain
|
|
3
|
-
// @work-item-id WI-126
|
|
3
|
+
// @work-item-id WI-126 / WI-140
|
|
4
4
|
|
|
5
5
|
import type { WorkItemStatus } from "../value-objects/work-item-frontmatter.js";
|
|
6
6
|
import type {
|
|
@@ -34,10 +34,18 @@ export class WorkItemStatusDerivationService {
|
|
|
34
34
|
nextAction,
|
|
35
35
|
evidence: Object.freeze({
|
|
36
36
|
hasRequiredInceptionArtifacts: this.hasRequiredInceptionArtifacts(input),
|
|
37
|
+
missingInceptionArtifacts: Object.freeze([...this.missingInceptionArtifacts(input)]),
|
|
37
38
|
reflectedUnits: Object.freeze([...this.reflectedUnits(input)]),
|
|
38
39
|
missingReflectionUnits: Object.freeze([...this.missingReflectionUnits(input)]),
|
|
39
40
|
implementationPaths: Object.freeze([...input.implementationPaths]),
|
|
40
41
|
testPaths: Object.freeze([...input.testPaths]),
|
|
42
|
+
missingImplementation: this.missingImplementation(input),
|
|
43
|
+
missingTests: this.missingTests(input),
|
|
44
|
+
validation: Object.freeze({
|
|
45
|
+
state: "not-run" as const,
|
|
46
|
+
source: "work-items:status",
|
|
47
|
+
blockingValidation: Object.freeze([]),
|
|
48
|
+
}),
|
|
41
49
|
}),
|
|
42
50
|
});
|
|
43
51
|
}
|
|
@@ -56,10 +64,10 @@ export class WorkItemStatusDerivationService {
|
|
|
56
64
|
return "drafted";
|
|
57
65
|
}
|
|
58
66
|
|
|
67
|
+
if (!reflected) return "drafted";
|
|
68
|
+
if (!implemented) return "reflected";
|
|
59
69
|
if (tested) return "tested";
|
|
60
|
-
|
|
61
|
-
if (reflected) return "reflected";
|
|
62
|
-
return "drafted";
|
|
70
|
+
return "implemented";
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
private reasonFor(input: WorkItemStatusInput, status: WorkItemStatus): string {
|
|
@@ -114,6 +122,14 @@ export class WorkItemStatusDerivationService {
|
|
|
114
122
|
return input.affectedUnits.filter((unit) => !reflected.has(unit));
|
|
115
123
|
}
|
|
116
124
|
|
|
125
|
+
private missingImplementation(input: WorkItemStatusInput): boolean {
|
|
126
|
+
return input.frontmatter.type !== "chore" && this.missingReflectionUnits(input).length === 0 && input.implementationPaths.length === 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private missingTests(input: WorkItemStatusInput): boolean {
|
|
130
|
+
return input.frontmatter.type !== "chore" && input.frontmatter.type !== "fix" && input.implementationPaths.length > 0 && input.testPaths.length === 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
117
133
|
private extractConstructionUnit(filePath: string): string | null {
|
|
118
134
|
const match = /^docs\/product\/construction\/([^/]+)\//.exec(filePath);
|
|
119
135
|
return match?.[1] ?? null;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @unit traceability-model
|
|
2
2
|
// @layer domain
|
|
3
|
-
// @work-item-id WI-126
|
|
3
|
+
// @work-item-id WI-126 / WI-140
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
6
|
WorkItemFrontmatter,
|
|
@@ -10,10 +10,18 @@ import type {
|
|
|
10
10
|
|
|
11
11
|
export interface WorkItemStatusEvidence {
|
|
12
12
|
readonly hasRequiredInceptionArtifacts: boolean;
|
|
13
|
+
readonly missingInceptionArtifacts: readonly string[];
|
|
13
14
|
readonly reflectedUnits: readonly string[];
|
|
14
15
|
readonly missingReflectionUnits: readonly string[];
|
|
15
16
|
readonly implementationPaths: readonly string[];
|
|
16
17
|
readonly testPaths: readonly string[];
|
|
18
|
+
readonly missingImplementation: boolean;
|
|
19
|
+
readonly missingTests: boolean;
|
|
20
|
+
readonly validation: {
|
|
21
|
+
readonly state: "passed" | "failed" | "not-run";
|
|
22
|
+
readonly source: string;
|
|
23
|
+
readonly blockingValidation: readonly string[];
|
|
24
|
+
};
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
export interface WorkItemStatusInput {
|
|
@@ -44,4 +52,5 @@ export interface WorkItemStatusReport {
|
|
|
44
52
|
export interface WorkItemStatusApplyResult {
|
|
45
53
|
readonly updated: readonly WorkItemStatusReport[];
|
|
46
54
|
readonly unchanged: readonly WorkItemStatusReport[];
|
|
55
|
+
readonly blocked: readonly WorkItemStatusReport[];
|
|
47
56
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @unit traceability-model
|
|
2
2
|
// @layer infrastructure
|
|
3
|
-
// @work-item-id WI-126
|
|
3
|
+
// @work-item-id WI-126 / WI-140
|
|
4
4
|
|
|
5
5
|
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
6
6
|
import * as path from "node:path";
|
|
@@ -97,6 +97,7 @@ export class FileSystemWorkItemStatusGateway implements WorkItemStatusPort {
|
|
|
97
97
|
return Object.freeze({
|
|
98
98
|
updated: Object.freeze(updated),
|
|
99
99
|
unchanged: Object.freeze(unchanged),
|
|
100
|
+
blocked: Object.freeze([]),
|
|
100
101
|
});
|
|
101
102
|
}
|
|
102
103
|
|
package/scripts/harness/traceability-model/presentation/cli/work-item-status-command-handler.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @unit traceability-model
|
|
2
2
|
// @layer presentation
|
|
3
|
-
// @work-item-id WI-126
|
|
3
|
+
// @work-item-id WI-126 / WI-140
|
|
4
4
|
|
|
5
5
|
import type { ApplyWorkItemStatusUseCase } from "../../application/usecases/apply-work-item-status-usecase.js";
|
|
6
6
|
import type { DeriveWorkItemStatusUseCase } from "../../application/usecases/derive-work-item-status-usecase.js";
|
|
@@ -14,6 +14,8 @@ export interface WorkItemStatusCommandInput {
|
|
|
14
14
|
readonly apply?: boolean;
|
|
15
15
|
readonly json?: boolean;
|
|
16
16
|
readonly failOnStale?: boolean;
|
|
17
|
+
readonly allowDowngrade?: boolean;
|
|
18
|
+
readonly changedOnly?: boolean;
|
|
17
19
|
readonly id?: string;
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -54,10 +56,14 @@ export class WorkItemStatusCommandHandler {
|
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
if (input.apply) {
|
|
57
|
-
const result = await this.applyWorkItemStatusUseCase.execute({
|
|
58
|
-
|
|
59
|
+
const result = await this.applyWorkItemStatusUseCase.execute({
|
|
60
|
+
id: input.id,
|
|
61
|
+
allowDowngrade: input.allowDowngrade,
|
|
62
|
+
changedOnly: input.changedOnly,
|
|
63
|
+
});
|
|
64
|
+
const reports = Object.freeze([...result.updated, ...result.unchanged, ...result.blocked]);
|
|
59
65
|
return Object.freeze({
|
|
60
|
-
exitCode: 0,
|
|
66
|
+
exitCode: result.blocked.length > 0 ? 1 : 0,
|
|
61
67
|
text: input.json ? JSON.stringify(result, null, 2) : this.formatApply(result),
|
|
62
68
|
reports,
|
|
63
69
|
});
|
|
@@ -97,6 +103,9 @@ export class WorkItemStatusCommandHandler {
|
|
|
97
103
|
if (result.updated.length === 0) {
|
|
98
104
|
lines.push("no updates required");
|
|
99
105
|
}
|
|
106
|
+
for (const report of result.blocked) {
|
|
107
|
+
lines.push(`blocked ${report.id}: ${report.currentStatus} -> ${report.derivedStatus} requires --allow-downgrade`);
|
|
108
|
+
}
|
|
100
109
|
lines.push(`unchanged: ${result.unchanged.length}`);
|
|
101
110
|
return lines.join("\n");
|
|
102
111
|
}
|
package/scripts/harness/validator-system/application/mappers/validation-result-contract-mapper.ts
CHANGED
|
@@ -12,12 +12,16 @@ export class ValidationResultContractMapper {
|
|
|
12
12
|
return {
|
|
13
13
|
validatorId: result.validatorId.value,
|
|
14
14
|
passed: result.passed,
|
|
15
|
-
errors: result.errors.map((e) =>
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
errors: result.errors.map((e) => {
|
|
16
|
+
const { code, severity, message, suggestion, ...details } = e;
|
|
17
|
+
return {
|
|
18
|
+
...details,
|
|
19
|
+
code: typeof code === 'string' ? code : code.toString(),
|
|
20
|
+
severity: typeof severity === 'string' ? severity : severity.toString(),
|
|
18
21
|
message: e.message,
|
|
19
22
|
suggestion: e.suggestion,
|
|
20
|
-
|
|
23
|
+
};
|
|
24
|
+
}),
|
|
21
25
|
durationMs: result.durationMs,
|
|
22
26
|
skipped: result.skipped,
|
|
23
27
|
};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @unit validator-system
|
|
4
4
|
*
|
|
5
5
|
* RunL2ValidatorsUseCase — H08-01: L2バリデータ実行
|
|
6
|
+
* @work-item-id WI-140
|
|
6
7
|
*/
|
|
7
8
|
import { readFile } from 'node:fs/promises';
|
|
8
9
|
import { ValidatorId, InvalidValidatorIdError } from '../../domain/value-objects/validator-id.js';
|
|
@@ -19,6 +20,7 @@ import type { TestQualityAnalyzerPort } from '../../domain/ports/test-quality-an
|
|
|
19
20
|
import type { CliCommandRegistryPort } from '../../domain/ports/cli-command-registry-port.js';
|
|
20
21
|
import type { E2eTestFileRegistryPort } from '../../domain/ports/e2e-test-file-registry-port.js';
|
|
21
22
|
import { CliE2eTestExistenceService } from '../../domain/services/cli-e2e-test-existence-service.js';
|
|
23
|
+
import type { WorkItemStatusPolicyPort } from '../../domain/ports/work-item-status-policy-port.js';
|
|
22
24
|
|
|
23
25
|
export interface RunL2ValidatorsUseCaseDeps {
|
|
24
26
|
validatorRegistry: ValidatorRegistry;
|
|
@@ -30,6 +32,7 @@ export interface RunL2ValidatorsUseCaseDeps {
|
|
|
30
32
|
testQualityAnalyzerPort?: TestQualityAnalyzerPort;
|
|
31
33
|
cliCommandRegistryPort?: CliCommandRegistryPort;
|
|
32
34
|
e2eTestFileRegistryPort?: E2eTestFileRegistryPort;
|
|
35
|
+
workItemStatusPolicyPort?: WorkItemStatusPolicyPort;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export class RunL2ValidatorsUseCase {
|
|
@@ -42,6 +45,7 @@ export class RunL2ValidatorsUseCase {
|
|
|
42
45
|
private readonly testQualityAnalyzerPort?: TestQualityAnalyzerPort;
|
|
43
46
|
private readonly cliCommandRegistryPort?: CliCommandRegistryPort;
|
|
44
47
|
private readonly e2eTestFileRegistryPort?: E2eTestFileRegistryPort;
|
|
48
|
+
private readonly workItemStatusPolicyPort?: WorkItemStatusPolicyPort;
|
|
45
49
|
private readonly cliE2eTestExistenceService = new CliE2eTestExistenceService();
|
|
46
50
|
|
|
47
51
|
constructor(deps: RunL2ValidatorsUseCaseDeps) {
|
|
@@ -54,6 +58,7 @@ export class RunL2ValidatorsUseCase {
|
|
|
54
58
|
this.testQualityAnalyzerPort = deps.testQualityAnalyzerPort;
|
|
55
59
|
this.cliCommandRegistryPort = deps.cliCommandRegistryPort;
|
|
56
60
|
this.e2eTestFileRegistryPort = deps.e2eTestFileRegistryPort;
|
|
61
|
+
this.workItemStatusPolicyPort = deps.workItemStatusPolicyPort;
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
async execute(input: RunL2ValidatorsInput): Promise<readonly ValidationResultContract[]> {
|
|
@@ -152,6 +157,29 @@ export class RunL2ValidatorsUseCase {
|
|
|
152
157
|
}
|
|
153
158
|
}
|
|
154
159
|
|
|
160
|
+
if (this.workItemStatusPolicyPort) {
|
|
161
|
+
const l2014Result = overrideMap.get('L2-014');
|
|
162
|
+
if (l2014Result && !l2014Result.skipped) {
|
|
163
|
+
const staleReports = await this.workItemStatusPolicyPort.findStaleReports(input.targetPaths);
|
|
164
|
+
if (staleReports.length > 0) {
|
|
165
|
+
const errors = staleReports.map((report) => ({
|
|
166
|
+
code: { value: 'L2-014', toString: () => 'L2-014' },
|
|
167
|
+
severity: { value: 'error', toString: () => 'error' },
|
|
168
|
+
message: `${report.id} status is stale: current=${report.currentStatus}, derived=${report.derivedStatus}`,
|
|
169
|
+
suggestion: report.nextAction,
|
|
170
|
+
workItemId: report.id,
|
|
171
|
+
descriptionPath: report.descriptionPath,
|
|
172
|
+
currentStatus: report.currentStatus,
|
|
173
|
+
derivedStatus: report.derivedStatus,
|
|
174
|
+
evidence: report.evidence,
|
|
175
|
+
}));
|
|
176
|
+
overrideMap.set('L2-014', ValidationResult.fail(ValidatorId.create('L2-014'), errors, 0));
|
|
177
|
+
} else {
|
|
178
|
+
overrideMap.set('L2-014', ValidationResult.pass(ValidatorId.create('L2-014'), 0));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
155
183
|
const finalResults = definitions.map(
|
|
156
184
|
(definition) => overrideMap.get(definition.validatorId.value) ?? ValidationResult.skip(definition.validatorId),
|
|
157
185
|
);
|
|
@@ -22,6 +22,7 @@ import { ItTestFileAnalyzerAdapter } from './infrastructure/adapters/it-test-fil
|
|
|
22
22
|
import { SourceFileTextScannerAdapter } from './infrastructure/adapters/source-file-text-scanner-adapter.js';
|
|
23
23
|
import { E2eTestFileRegistryAdapter } from './infrastructure/adapters/e2e-test-file-registry-adapter.js';
|
|
24
24
|
import { CliCommandRegistryAdapter } from './infrastructure/adapters/cli-command-registry-adapter.js';
|
|
25
|
+
import { TraceabilityWorkItemStatusPolicyAdapter } from './infrastructure/adapters/traceability-work-item-status-policy-adapter.js';
|
|
25
26
|
import { PhaseDependencyPhaseGatePolicyAdapter } from './infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.js';
|
|
26
27
|
import { TraceabilityMetadataPolicyAdapter } from './infrastructure/adapters/traceability-metadata-policy-adapter.js';
|
|
27
28
|
import { NyquistAcCoveragePolicyAdapter } from './infrastructure/adapters/nyquist-ac-coverage-policy-adapter.js';
|
|
@@ -45,7 +46,7 @@ import { join } from 'node:path';
|
|
|
45
46
|
const DEFAULT_CONFIG = {
|
|
46
47
|
preset: 'standard' as const,
|
|
47
48
|
layers: {
|
|
48
|
-
L2: { enabled: true, validators: ['L2-001', 'L2-002', 'L2-003', 'L2-013'] },
|
|
49
|
+
L2: { enabled: true, validators: ['L2-001', 'L2-002', 'L2-003', 'L2-013', 'L2-014'] },
|
|
49
50
|
L3: { enabled: true, validators: ['L3-001', 'L3-002', 'L3-003', 'L3-004'], coverageThreshold: 90, bundleSizeLimit: 512000 },
|
|
50
51
|
L4: { enabled: true, validators: ['L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005'] },
|
|
51
52
|
},
|
|
@@ -78,6 +79,7 @@ function buildDefaultRegistry(): ValidatorRegistry {
|
|
|
78
79
|
createDef('L2-002', 'L2', 'always', 'MetadataPolicyPort'),
|
|
79
80
|
createDef('L2-003', 'L2', 'always'),
|
|
80
81
|
createDef('L2-013', 'L2', 'always', 'CliE2eTestExistenceService'),
|
|
82
|
+
createDef('L2-014', 'L2', 'always', 'WorkItemStatusPolicyPort'),
|
|
81
83
|
createDef('L3-001', 'L3', 'always'),
|
|
82
84
|
createDef('L3-002', 'L3', 'strictOnly'),
|
|
83
85
|
createDef('L3-003', 'L3', 'always'),
|
|
@@ -123,6 +125,7 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
123
125
|
const testQualityAnalyzerPort = new BiomeAstTestQualityAnalyzerAdapter();
|
|
124
126
|
const securityScannerPort = new FileSystemSecurityPatternScannerAdapter();
|
|
125
127
|
const performanceScannerPort = new AstPerformanceScannerAdapter();
|
|
128
|
+
const workItemStatusPolicyPort = new TraceabilityWorkItemStatusPolicyAdapter(process.cwd());
|
|
126
129
|
|
|
127
130
|
const docsRoot = join(process.cwd(), 'docs/product/construction');
|
|
128
131
|
const cwd = process.cwd();
|
|
@@ -146,6 +149,7 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
146
149
|
testQualityAnalyzerPort,
|
|
147
150
|
e2eTestFileRegistryPort,
|
|
148
151
|
cliCommandRegistryPort,
|
|
152
|
+
workItemStatusPolicyPort,
|
|
149
153
|
});
|
|
150
154
|
|
|
151
155
|
const runL3ValidatorsUseCase = new RunL3ValidatorsUseCase({
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer domain
|
|
3
|
+
* @unit validator-system
|
|
4
|
+
* @work-item-id WI-140
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { WorkItemStatusReport } from "../../../traceability-model/domain/value-objects/work-item-status-report.js";
|
|
8
|
+
|
|
9
|
+
export interface WorkItemStatusPolicyPort {
|
|
10
|
+
findStaleReports(targetPaths?: readonly string[]): Promise<readonly WorkItemStatusReport[]>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// @unit validator-system
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-129
|
|
4
|
+
// @work-item-id WI-130
|
|
5
|
+
|
|
6
|
+
export type TestCaseKind = 'unit' | 'integration' | 'e2e' | 'lifecycle';
|
|
7
|
+
|
|
8
|
+
export type TestStepKind = 'arrange' | 'act' | 'assert';
|
|
9
|
+
|
|
10
|
+
export type AssertionTarget =
|
|
11
|
+
| 'observed-output'
|
|
12
|
+
| 'state'
|
|
13
|
+
| 'emitted-event'
|
|
14
|
+
| 'persisted-effect'
|
|
15
|
+
| 'error-contract'
|
|
16
|
+
| 'interaction';
|
|
17
|
+
|
|
18
|
+
export type AssertionStrength =
|
|
19
|
+
| 'exact-value'
|
|
20
|
+
| 'shape'
|
|
21
|
+
| 'invariant'
|
|
22
|
+
| 'range'
|
|
23
|
+
| 'weak-truthiness'
|
|
24
|
+
| 'snapshot-only'
|
|
25
|
+
| 'interaction-only'
|
|
26
|
+
| 'length-only';
|
|
27
|
+
|
|
28
|
+
export interface SemanticAssertion {
|
|
29
|
+
readonly target: AssertionTarget;
|
|
30
|
+
readonly strength: AssertionStrength;
|
|
31
|
+
readonly subject: string;
|
|
32
|
+
readonly line: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TestStep {
|
|
36
|
+
readonly kind: TestStepKind;
|
|
37
|
+
readonly expression: string;
|
|
38
|
+
readonly line: number;
|
|
39
|
+
readonly observedName?: string;
|
|
40
|
+
readonly assertion?: SemanticAssertion;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TestDoubleReplacement {
|
|
44
|
+
readonly target: string;
|
|
45
|
+
readonly line: number;
|
|
46
|
+
readonly dependencyKind: 'external' | 'domain-internal';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface TestCaseStructure {
|
|
50
|
+
readonly filePath: string;
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly line: number;
|
|
53
|
+
readonly kind: TestCaseKind;
|
|
54
|
+
readonly steps: readonly TestStep[];
|
|
55
|
+
readonly assertions: readonly SemanticAssertion[];
|
|
56
|
+
readonly mocks: readonly TestDoubleReplacement[];
|
|
57
|
+
readonly allowsMultipleActs: boolean;
|
|
58
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* ValidatorId 値オブジェクト
|
|
6
6
|
* L1-001〜L4-005 のバリデータを識別する不変値オブジェクト
|
|
7
7
|
* Wave 2A で L1-017, L1-018, L2-013 を追加
|
|
8
|
+
* WI-140 で L2-014 を追加
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
export class InvalidValidatorIdError extends Error {
|
|
@@ -26,6 +27,7 @@ const VALIDATOR_NAME_MAP: Record<string, string> = {
|
|
|
26
27
|
'L2-002': 'metadata',
|
|
27
28
|
'L2-003': 'test-quality',
|
|
28
29
|
'L2-013': 'cli-e2e-test-existence',
|
|
30
|
+
'L2-014': 'work-item-status-staleness',
|
|
29
31
|
'L3-001': 'security',
|
|
30
32
|
'L3-002': 'performance',
|
|
31
33
|
'L3-003': 'coverage',
|