phasegate 0.141.0 → 0.143.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 +13 -0
- package/README.ja.md +7 -3
- package/README.md +8 -4
- package/docs/guide/layer-model.md +2 -1
- 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/harness-api/domain/services/command-dispatch-service.ts +1 -0
- package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +1 -0
- package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +1 -0
- package/scripts/harness/harness-api/domain/value-objects/drift-report-summary.ts +1 -0
- package/scripts/harness/harness-api/domain/value-objects/layer-health.ts +1 -0
- package/scripts/harness/integrations/pre-commit.ts +251 -3
- package/scripts/harness/main.ts +52 -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 +55 -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 +137 -0
- package/scripts/harness/traceability-model/domain/value-objects/work-item-status-report.ts +56 -0
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +241 -0
- package/scripts/harness/traceability-model/presentation/cli/work-item-status-command-handler.ts +112 -0
- 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/validator-id.ts +2 -0
- 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,
|
|
@@ -98,6 +99,7 @@ Commands:
|
|
|
98
99
|
disable-feature <name> Disable a harness feature
|
|
99
100
|
list-features List available features
|
|
100
101
|
migrate Migrate phasegate.config.json (--schema v3, --config <path>)
|
|
102
|
+
work-items:status Report or apply derived WI frontmatter status (--dry-run|--apply, --id, --fail-on-stale, --json)
|
|
101
103
|
|
|
102
104
|
render-errors Render harness errors (--format human|agent|ci)
|
|
103
105
|
validate-fix Validate fix examples (--code <code>)
|
|
@@ -153,6 +155,7 @@ Commands:
|
|
|
153
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)
|
|
154
156
|
pre-commit Run L2 pre-commit validators on staged files
|
|
155
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
|
|
156
159
|
delegate-sonnet [...args] Delegate task to Sonnet 4.6 (forwards args to scripts/delegate-sonnet.sh)
|
|
157
160
|
|
|
158
161
|
Skills:
|
|
@@ -306,6 +309,19 @@ Options:
|
|
|
306
309
|
"phasegate:status": `Usage: phasegate phasegate:status
|
|
307
310
|
|
|
308
311
|
Display harness status (enabled validators, schema version, hook deployment).`,
|
|
312
|
+
"work-items:status": `Usage: phasegate work-items:status (--dry-run|--apply) [options]
|
|
313
|
+
|
|
314
|
+
Derive WI frontmatter status from inception, product reflection, implementation, and test evidence.
|
|
315
|
+
|
|
316
|
+
Options:
|
|
317
|
+
--dry-run Print current status, derived status, reason, and next action.
|
|
318
|
+
--apply Update only the status line in each stale description.md frontmatter.
|
|
319
|
+
--id <WI-XXX> Limit report/apply to one work item.
|
|
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.
|
|
323
|
+
--json Output machine-readable JSON.
|
|
324
|
+
--help, -h Show this help`,
|
|
309
325
|
"phasegate:detect-drift": `Usage: phasegate phasegate:detect-drift [options]
|
|
310
326
|
|
|
311
327
|
Run L4-001 drift detection between design documents and source code. WARNING: scans the project filesystem.
|
|
@@ -704,6 +720,7 @@ async function main(): Promise<void> {
|
|
|
704
720
|
const withHusky = hasFlag(args, "--with-husky");
|
|
705
721
|
const huskyResult = withHusky ? await deployHuskyHook(harnessRoot, rootDir) : null;
|
|
706
722
|
const huskyCommitMsgResult = withHusky ? await deployHuskyCommitMsgHook(harnessRoot, rootDir) : null;
|
|
723
|
+
const huskyPrePushResult = withHusky ? await deployHuskyPrePushHook(harnessRoot, rootDir) : null;
|
|
707
724
|
const ciWorkflowResult = withCi ? await deployCiWorkflows(harnessRoot, rootDir) : null;
|
|
708
725
|
console.log(
|
|
709
726
|
`✓ Skills deployed to ${result.targetDir} (${result.deployedSkills.length} skills, set: ${skillSet})`,
|
|
@@ -769,6 +786,13 @@ async function main(): Promise<void> {
|
|
|
769
786
|
console.log(` .husky/commit-msg already exists, skipped`);
|
|
770
787
|
}
|
|
771
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
|
+
}
|
|
772
796
|
if (ciWorkflowResult !== null) {
|
|
773
797
|
if (ciWorkflowResult.copiedFiles.length > 0) {
|
|
774
798
|
console.log(`✓ CI workflows deployed (${ciWorkflowResult.copiedFiles.length} files)`);
|
|
@@ -962,6 +986,25 @@ async function main(): Promise<void> {
|
|
|
962
986
|
break;
|
|
963
987
|
}
|
|
964
988
|
|
|
989
|
+
case "work-items:status": {
|
|
990
|
+
const mod = createTraceabilityModelModule(
|
|
991
|
+
rootDir,
|
|
992
|
+
toTraceabilityModelOptions(resolvedConfig),
|
|
993
|
+
);
|
|
994
|
+
const result = await mod.workItemStatusCommandHandler.execute({
|
|
995
|
+
dryRun: hasFlag(args, "--dry-run"),
|
|
996
|
+
apply: hasFlag(args, "--apply"),
|
|
997
|
+
failOnStale: hasFlag(args, "--fail-on-stale"),
|
|
998
|
+
allowDowngrade: hasFlag(args, "--allow-downgrade"),
|
|
999
|
+
changedOnly: hasFlag(args, "--changed-only"),
|
|
1000
|
+
id: parseFlag(args, "--id"),
|
|
1001
|
+
json,
|
|
1002
|
+
});
|
|
1003
|
+
console.log(result.text);
|
|
1004
|
+
process.exit(result.exitCode);
|
|
1005
|
+
break;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
965
1008
|
// ── phase-dependency-model ──
|
|
966
1009
|
case "check-phase-gate": {
|
|
967
1010
|
const phaseConfig = resolvedConfig ? toPhaseConfigSection(resolvedConfig) : undefined;
|
|
@@ -1554,6 +1597,15 @@ Examples:
|
|
|
1554
1597
|
break;
|
|
1555
1598
|
}
|
|
1556
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
|
+
|
|
1557
1609
|
case "delegate-sonnet": {
|
|
1558
1610
|
const { spawn } = await import("node:child_process");
|
|
1559
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
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// @unit traceability-model
|
|
2
|
+
// @layer application
|
|
3
|
+
// @work-item-id WI-126 / WI-140
|
|
4
|
+
|
|
5
|
+
import type { WorkItemStatusPort } from "../../domain/ports/work-item-status-port.js";
|
|
6
|
+
import type { WorkItemStatusApplyResult, WorkItemStatusReport } from "../../domain/value-objects/work-item-status-report.js";
|
|
7
|
+
import type { DeriveWorkItemStatusUseCase } from "./derive-work-item-status-usecase.js";
|
|
8
|
+
import type { WorkItemStatus } from "../../domain/value-objects/work-item-frontmatter.js";
|
|
9
|
+
|
|
10
|
+
export interface ApplyWorkItemStatusUseCaseDeps {
|
|
11
|
+
readonly deriveWorkItemStatusUseCase: Pick<DeriveWorkItemStatusUseCase, "execute">;
|
|
12
|
+
readonly workItemStatusPort: WorkItemStatusPort;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ApplyWorkItemStatusUseCase {
|
|
16
|
+
private readonly deriveWorkItemStatusUseCase: Pick<DeriveWorkItemStatusUseCase, "execute">;
|
|
17
|
+
private readonly workItemStatusPort: WorkItemStatusPort;
|
|
18
|
+
|
|
19
|
+
constructor(deps: ApplyWorkItemStatusUseCaseDeps) {
|
|
20
|
+
this.deriveWorkItemStatusUseCase = deps.deriveWorkItemStatusUseCase;
|
|
21
|
+
this.workItemStatusPort = deps.workItemStatusPort;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async execute(input: { readonly id?: string; readonly allowDowngrade?: boolean; readonly changedOnly?: boolean } = {}): Promise<WorkItemStatusApplyResult> {
|
|
25
|
+
const reports = await this.deriveWorkItemStatusUseCase.execute();
|
|
26
|
+
const targetReports = input.id
|
|
27
|
+
? reports.filter((report) => report.id === input.id)
|
|
28
|
+
: reports;
|
|
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
|
+
});
|
|
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];
|
|
55
|
+
}
|
package/scripts/harness/traceability-model/application/usecases/derive-work-item-status-usecase.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// @unit traceability-model
|
|
2
|
+
// @layer application
|
|
3
|
+
// @work-item-id WI-126
|
|
4
|
+
|
|
5
|
+
import type { WorkItemStatusPort } from "../../domain/ports/work-item-status-port.js";
|
|
6
|
+
import { WorkItemStatusDerivationService } from "../../domain/services/work-item-status-derivation-service.js";
|
|
7
|
+
import type { WorkItemStatusReport } from "../../domain/value-objects/work-item-status-report.js";
|
|
8
|
+
|
|
9
|
+
export interface DeriveWorkItemStatusUseCaseDeps {
|
|
10
|
+
readonly workItemStatusPort: WorkItemStatusPort;
|
|
11
|
+
readonly derivationService: WorkItemStatusDerivationService;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class DeriveWorkItemStatusUseCase {
|
|
15
|
+
private readonly workItemStatusPort: WorkItemStatusPort;
|
|
16
|
+
private readonly derivationService: WorkItemStatusDerivationService;
|
|
17
|
+
|
|
18
|
+
constructor(deps: DeriveWorkItemStatusUseCaseDeps) {
|
|
19
|
+
this.workItemStatusPort = deps.workItemStatusPort;
|
|
20
|
+
this.derivationService = deps.derivationService;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async execute(): Promise<readonly WorkItemStatusReport[]> {
|
|
24
|
+
const inputs = await this.workItemStatusPort.listWorkItemStatusInputs();
|
|
25
|
+
return Object.freeze(inputs.map((input) => this.derivationService.derive(input)));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { ApplyWorkItemMigrationUseCase } from "./application/usecases/apply-work-item-migration-usecase.js";
|
|
10
|
+
import { ApplyWorkItemStatusUseCase } from "./application/usecases/apply-work-item-status-usecase.js";
|
|
11
|
+
import { DeriveWorkItemStatusUseCase } from "./application/usecases/derive-work-item-status-usecase.js";
|
|
10
12
|
import { PlanWorkItemMigrationUseCase } from "./application/usecases/plan-work-item-migration-usecase.js";
|
|
11
13
|
import { ValidateDesignStoryAnnotationsUseCase } from "./application/usecases/validate-design-story-annotations-usecase.js";
|
|
12
14
|
import { ValidateImplementationMetadataUseCase } from "./application/usecases/validate-implementation-metadata-usecase.js";
|
|
@@ -14,17 +16,20 @@ import { ValidateTestStoryMetadataUseCase } from "./application/usecases/validat
|
|
|
14
16
|
import { MetadataValidator } from "./domain/services/metadata-validator.js";
|
|
15
17
|
import { StoryIdAliasResolver } from "./domain/services/story-id-alias-resolver.js";
|
|
16
18
|
import { TraceabilityChainBuilder } from "./domain/services/traceability-chain-builder.js";
|
|
19
|
+
import { WorkItemStatusDerivationService } from "./domain/services/work-item-status-derivation-service.js";
|
|
17
20
|
import { ProjectRelativePath } from "./domain/value-objects/project-relative-path.js";
|
|
18
21
|
import { FileSystemInceptionPlanGateway } from "./infrastructure/gateways/file-system-inception-plan-gateway.js";
|
|
19
22
|
import { FileSystemMetadataReader } from "./infrastructure/gateways/file-system-metadata-reader.js";
|
|
20
23
|
import { FileSystemWorkItemMigrationApplyGateway } from "./infrastructure/gateways/file-system-work-item-migration-apply-gateway.js";
|
|
21
24
|
import { FileSystemWorkItemMigrationSourceGateway } from "./infrastructure/gateways/file-system-work-item-migration-source-gateway.js";
|
|
22
25
|
import { FileSystemWorkItemIdentityGateway } from "./infrastructure/gateways/file-system-work-item-identity-gateway.js";
|
|
26
|
+
import { FileSystemWorkItemStatusGateway } from "./infrastructure/gateways/file-system-work-item-status-gateway.js";
|
|
23
27
|
import { MarkdownDesignDocumentGateway } from "./infrastructure/gateways/markdown-design-document-gateway.js";
|
|
24
28
|
import { MarkdownStoryCatalogGateway } from "./infrastructure/gateways/markdown-story-catalog-gateway.js";
|
|
25
29
|
import { MarkdownUnitDefinitionGateway } from "./infrastructure/gateways/markdown-unit-definition-gateway.js";
|
|
26
30
|
import { MigrateWorkItemsCommandHandler } from "./presentation/cli/migrate-work-items-command-handler.js";
|
|
27
31
|
import { ValidateMetadataCommandHandler } from "./presentation/cli/validate-metadata-command-handler.js";
|
|
32
|
+
import { WorkItemStatusCommandHandler } from "./presentation/cli/work-item-status-command-handler.js";
|
|
28
33
|
|
|
29
34
|
export interface TraceabilityModelPathRoots {
|
|
30
35
|
readonly designDocsRoot: string;
|
|
@@ -63,6 +68,7 @@ export function createTraceabilityModelModule(
|
|
|
63
68
|
const workItemMigrationSource = new FileSystemWorkItemMigrationSourceGateway({ rootDir });
|
|
64
69
|
const workItemMigrationApply = new FileSystemWorkItemMigrationApplyGateway({ rootDir });
|
|
65
70
|
const workItemIdentity = new FileSystemWorkItemIdentityGateway({ rootDir });
|
|
71
|
+
const workItemStatus = new FileSystemWorkItemStatusGateway({ rootDir });
|
|
66
72
|
|
|
67
73
|
// Domain services
|
|
68
74
|
const metadataValidator = new MetadataValidator({
|
|
@@ -78,6 +84,7 @@ export function createTraceabilityModelModule(
|
|
|
78
84
|
storyCatalogPath,
|
|
79
85
|
});
|
|
80
86
|
const storyIdAliasResolver = new StoryIdAliasResolver(storyCatalog);
|
|
87
|
+
const workItemStatusDerivationService = new WorkItemStatusDerivationService();
|
|
81
88
|
|
|
82
89
|
// Usecases
|
|
83
90
|
const validateImplementationMetadataUseCase = new ValidateImplementationMetadataUseCase({
|
|
@@ -100,6 +107,14 @@ export function createTraceabilityModelModule(
|
|
|
100
107
|
planWorkItemMigrationUseCase,
|
|
101
108
|
applyPort: workItemMigrationApply,
|
|
102
109
|
});
|
|
110
|
+
const deriveWorkItemStatusUseCase = new DeriveWorkItemStatusUseCase({
|
|
111
|
+
workItemStatusPort: workItemStatus,
|
|
112
|
+
derivationService: workItemStatusDerivationService,
|
|
113
|
+
});
|
|
114
|
+
const applyWorkItemStatusUseCase = new ApplyWorkItemStatusUseCase({
|
|
115
|
+
deriveWorkItemStatusUseCase,
|
|
116
|
+
workItemStatusPort: workItemStatus,
|
|
117
|
+
});
|
|
103
118
|
|
|
104
119
|
// Presentation handlers
|
|
105
120
|
const validateMetadataCommandHandler = new ValidateMetadataCommandHandler({
|
|
@@ -112,10 +127,15 @@ export function createTraceabilityModelModule(
|
|
|
112
127
|
planWorkItemMigrationUseCase,
|
|
113
128
|
applyWorkItemMigrationUseCase,
|
|
114
129
|
});
|
|
130
|
+
const workItemStatusCommandHandler = new WorkItemStatusCommandHandler({
|
|
131
|
+
deriveWorkItemStatusUseCase,
|
|
132
|
+
applyWorkItemStatusUseCase,
|
|
133
|
+
});
|
|
115
134
|
|
|
116
135
|
return {
|
|
117
136
|
validateMetadataCommandHandler,
|
|
118
137
|
migrateWorkItemsCommandHandler,
|
|
138
|
+
workItemStatusCommandHandler,
|
|
119
139
|
// expose key services for cross-unit use
|
|
120
140
|
storyCatalog,
|
|
121
141
|
traceabilityChainBuilder,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @unit traceability-model
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-126
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
WorkItemStatusApplyResult,
|
|
7
|
+
WorkItemStatusInput,
|
|
8
|
+
WorkItemStatusReport,
|
|
9
|
+
} from "../value-objects/work-item-status-report.js";
|
|
10
|
+
|
|
11
|
+
export interface WorkItemStatusPort {
|
|
12
|
+
listWorkItemStatusInputs(): Promise<readonly WorkItemStatusInput[]>;
|
|
13
|
+
applyDerivedStatuses(
|
|
14
|
+
reports: readonly WorkItemStatusReport[],
|
|
15
|
+
): Promise<WorkItemStatusApplyResult>;
|
|
16
|
+
}
|
package/scripts/harness/traceability-model/domain/services/work-item-status-derivation-service.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// @unit traceability-model
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-126 / WI-140
|
|
4
|
+
|
|
5
|
+
import type { WorkItemStatus } from "../value-objects/work-item-frontmatter.js";
|
|
6
|
+
import type {
|
|
7
|
+
WorkItemStatusInput,
|
|
8
|
+
WorkItemStatusReport,
|
|
9
|
+
} from "../value-objects/work-item-status-report.js";
|
|
10
|
+
|
|
11
|
+
const ORDER: Record<WorkItemStatus, number> = {
|
|
12
|
+
drafted: 0,
|
|
13
|
+
reflected: 1,
|
|
14
|
+
implemented: 2,
|
|
15
|
+
tested: 3,
|
|
16
|
+
completed: 3,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class WorkItemStatusDerivationService {
|
|
20
|
+
derive(input: WorkItemStatusInput): WorkItemStatusReport {
|
|
21
|
+
const derivedStatus = this.deriveStatus(input);
|
|
22
|
+
const currentStatus = input.frontmatter.status ?? "drafted";
|
|
23
|
+
const reason = this.reasonFor(input, derivedStatus);
|
|
24
|
+
const nextAction = this.nextActionFor(input, derivedStatus);
|
|
25
|
+
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
id: input.frontmatter.id,
|
|
28
|
+
type: input.frontmatter.type,
|
|
29
|
+
descriptionPath: input.descriptionPath,
|
|
30
|
+
currentStatus,
|
|
31
|
+
derivedStatus,
|
|
32
|
+
stale: currentStatus !== derivedStatus,
|
|
33
|
+
reason,
|
|
34
|
+
nextAction,
|
|
35
|
+
evidence: Object.freeze({
|
|
36
|
+
hasRequiredInceptionArtifacts: this.hasRequiredInceptionArtifacts(input),
|
|
37
|
+
missingInceptionArtifacts: Object.freeze([...this.missingInceptionArtifacts(input)]),
|
|
38
|
+
reflectedUnits: Object.freeze([...this.reflectedUnits(input)]),
|
|
39
|
+
missingReflectionUnits: Object.freeze([...this.missingReflectionUnits(input)]),
|
|
40
|
+
implementationPaths: Object.freeze([...input.implementationPaths]),
|
|
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
|
+
}),
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private deriveStatus(input: WorkItemStatusInput): WorkItemStatus {
|
|
54
|
+
const type = input.frontmatter.type;
|
|
55
|
+
if (type === "chore") return "drafted";
|
|
56
|
+
|
|
57
|
+
const reflected = this.missingReflectionUnits(input).length === 0;
|
|
58
|
+
const implemented = input.implementationPaths.length > 0;
|
|
59
|
+
const tested = input.testPaths.length > 0;
|
|
60
|
+
|
|
61
|
+
if (type === "fix") {
|
|
62
|
+
if (implemented) return "implemented";
|
|
63
|
+
if (reflected) return "reflected";
|
|
64
|
+
return "drafted";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!reflected) return "drafted";
|
|
68
|
+
if (!implemented) return "reflected";
|
|
69
|
+
if (tested) return "tested";
|
|
70
|
+
return "implemented";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private reasonFor(input: WorkItemStatusInput, status: WorkItemStatus): string {
|
|
74
|
+
if (!this.hasRequiredInceptionArtifacts(input)) {
|
|
75
|
+
return `missing inception artifacts: ${this.missingInceptionArtifacts(input).join(", ")}`;
|
|
76
|
+
}
|
|
77
|
+
if (status === "tested") return "test evidence with @work-item-id exists";
|
|
78
|
+
if (status === "implemented") return "implementation evidence with @work-item-id exists";
|
|
79
|
+
if (status === "reflected") return "all affected units have product reflection";
|
|
80
|
+
if (input.frontmatter.type === "chore") return "chore work items complete at drafted";
|
|
81
|
+
return "product reflection is not complete";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private nextActionFor(input: WorkItemStatusInput, status: WorkItemStatus): string {
|
|
85
|
+
const missingArtifacts = this.missingInceptionArtifacts(input);
|
|
86
|
+
if (missingArtifacts.length > 0) {
|
|
87
|
+
return `create inception artifacts: ${missingArtifacts.join(", ")}`;
|
|
88
|
+
}
|
|
89
|
+
const missingUnits = this.missingReflectionUnits(input);
|
|
90
|
+
if (missingUnits.length > 0) {
|
|
91
|
+
return `reflect @work-item-id ${input.frontmatter.id} in product docs for: ${missingUnits.join(", ")}`;
|
|
92
|
+
}
|
|
93
|
+
if (input.frontmatter.type === "chore") return "no further status transition required";
|
|
94
|
+
if (status === "reflected") return `add implementation annotated with @work-item-id ${input.frontmatter.id}`;
|
|
95
|
+
if (status === "implemented" && input.frontmatter.type !== "fix") {
|
|
96
|
+
return `add tests annotated with @work-item-id ${input.frontmatter.id}`;
|
|
97
|
+
}
|
|
98
|
+
return "status is up to date";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private hasRequiredInceptionArtifacts(input: WorkItemStatusInput): boolean {
|
|
102
|
+
return this.missingInceptionArtifacts(input).length === 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private missingInceptionArtifacts(input: WorkItemStatusInput): readonly string[] {
|
|
106
|
+
const existing = new Set(input.existingInceptionArtifacts);
|
|
107
|
+
return input.requiredInceptionArtifacts.filter((artifact) => !existing.has(artifact));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private reflectedUnits(input: WorkItemStatusInput): readonly string[] {
|
|
111
|
+
const reflected = new Set<string>();
|
|
112
|
+
for (const filePath of input.productReflectionPaths) {
|
|
113
|
+
const unit = this.extractConstructionUnit(filePath);
|
|
114
|
+
if (unit) reflected.add(unit);
|
|
115
|
+
}
|
|
116
|
+
return input.affectedUnits.filter((unit) => reflected.has(unit));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private missingReflectionUnits(input: WorkItemStatusInput): readonly string[] {
|
|
120
|
+
if (input.frontmatter.type === "chore") return [];
|
|
121
|
+
const reflected = new Set(this.reflectedUnits(input));
|
|
122
|
+
return input.affectedUnits.filter((unit) => !reflected.has(unit));
|
|
123
|
+
}
|
|
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
|
+
|
|
133
|
+
private extractConstructionUnit(filePath: string): string | null {
|
|
134
|
+
const match = /^docs\/product\/construction\/([^/]+)\//.exec(filePath);
|
|
135
|
+
return match?.[1] ?? null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// @unit traceability-model
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-126 / WI-140
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
WorkItemFrontmatter,
|
|
7
|
+
WorkItemStatus,
|
|
8
|
+
WorkItemType,
|
|
9
|
+
} from "./work-item-frontmatter.js";
|
|
10
|
+
|
|
11
|
+
export interface WorkItemStatusEvidence {
|
|
12
|
+
readonly hasRequiredInceptionArtifacts: boolean;
|
|
13
|
+
readonly missingInceptionArtifacts: readonly string[];
|
|
14
|
+
readonly reflectedUnits: readonly string[];
|
|
15
|
+
readonly missingReflectionUnits: readonly string[];
|
|
16
|
+
readonly implementationPaths: readonly string[];
|
|
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
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface WorkItemStatusInput {
|
|
28
|
+
readonly descriptionPath: string;
|
|
29
|
+
readonly directoryId: string;
|
|
30
|
+
readonly ownerUnit: string | null;
|
|
31
|
+
readonly frontmatter: WorkItemFrontmatter;
|
|
32
|
+
readonly requiredInceptionArtifacts: readonly string[];
|
|
33
|
+
readonly existingInceptionArtifacts: readonly string[];
|
|
34
|
+
readonly affectedUnits: readonly string[];
|
|
35
|
+
readonly productReflectionPaths: readonly string[];
|
|
36
|
+
readonly implementationPaths: readonly string[];
|
|
37
|
+
readonly testPaths: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface WorkItemStatusReport {
|
|
41
|
+
readonly id: string;
|
|
42
|
+
readonly type: WorkItemType;
|
|
43
|
+
readonly descriptionPath: string;
|
|
44
|
+
readonly currentStatus: WorkItemStatus;
|
|
45
|
+
readonly derivedStatus: WorkItemStatus;
|
|
46
|
+
readonly stale: boolean;
|
|
47
|
+
readonly reason: string;
|
|
48
|
+
readonly nextAction: string;
|
|
49
|
+
readonly evidence: WorkItemStatusEvidence;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WorkItemStatusApplyResult {
|
|
53
|
+
readonly updated: readonly WorkItemStatusReport[];
|
|
54
|
+
readonly unchanged: readonly WorkItemStatusReport[];
|
|
55
|
+
readonly blocked: readonly WorkItemStatusReport[];
|
|
56
|
+
}
|