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/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts
CHANGED
|
@@ -45,6 +45,7 @@ export class ValidatorSystemExecutionAdapter implements ValidatorExecutionPort {
|
|
|
45
45
|
return results.map((r) => ({
|
|
46
46
|
validatorId: r.validatorId,
|
|
47
47
|
passed: r.passed,
|
|
48
|
+
skipped: r.skipped,
|
|
48
49
|
errors: r.errors.map((e) => ({ code: e.code, severity: e.severity, message: e.message })),
|
|
49
50
|
}));
|
|
50
51
|
},
|
|
@@ -56,6 +57,7 @@ export class ValidatorSystemExecutionAdapter implements ValidatorExecutionPort {
|
|
|
56
57
|
return report.results.map((r) => ({
|
|
57
58
|
validatorId: r.validatorId,
|
|
58
59
|
passed: r.passed,
|
|
60
|
+
skipped: r.skipped,
|
|
59
61
|
errors: r.errors.map((e) => ({ code: e.code, severity: e.severity, message: e.message })),
|
|
60
62
|
}));
|
|
61
63
|
},
|
|
@@ -18,7 +18,6 @@ import { execSync } from "node:child_process";
|
|
|
18
18
|
import { readFile } from "node:fs/promises";
|
|
19
19
|
import { createConfigFoundationModule } from "../config-foundation/composition-root.js";
|
|
20
20
|
import { toValidatorSystemConfig } from "../config-foundation/application/mappers/validator-system-config-mapper.js";
|
|
21
|
-
import { ConfigNotFoundError } from "../config-foundation/infrastructure/repositories/file-system-config-repository.js";
|
|
22
21
|
import { createTraceabilityModelModule } from "../traceability-model/composition-root.js";
|
|
23
22
|
import type { ValidateMetadataCommandOutput } from "../traceability-model/presentation/cli/validate-metadata-command-handler.js";
|
|
24
23
|
import type { AggregatedValidationReport } from "../validator-system/application/dto/aggregated-validation-report.js";
|
|
@@ -38,13 +37,17 @@ const WORK_ITEM_PATH_PATTERN = /(?:^|\/)WI-\d+(?:\/|$)/;
|
|
|
38
37
|
const WORK_ITEM_TRAILER_PATTERN = /^Work-Item:\s*WI-\d+\s*$/m;
|
|
39
38
|
const TEST_FILE_SUFFIXES = Object.freeze([".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx"]);
|
|
40
39
|
|
|
40
|
+
function isConfigNotFoundError(err: unknown): boolean {
|
|
41
|
+
return err instanceof Error && err.name === "ConfigNotFoundError";
|
|
42
|
+
}
|
|
43
|
+
|
|
41
44
|
async function loadValidatorSystemConfig(): Promise<object | undefined> {
|
|
42
45
|
const configMod = createConfigFoundationModule();
|
|
43
46
|
try {
|
|
44
47
|
const resolvedConfig = await configMod.usecases.loadResolvedConfigUseCase.execute();
|
|
45
48
|
return toValidatorSystemConfig(resolvedConfig.config);
|
|
46
49
|
} catch (err) {
|
|
47
|
-
if (err
|
|
50
|
+
if (isConfigNotFoundError(err)) return undefined;
|
|
48
51
|
throw err;
|
|
49
52
|
}
|
|
50
53
|
}
|
|
@@ -57,7 +60,7 @@ async function loadTraceabilityModelOptions(): Promise<
|
|
|
57
60
|
const resolvedConfig = await configMod.usecases.loadResolvedConfigUseCase.execute();
|
|
58
61
|
return { pathRoots: { designDocsRoot: resolvedConfig.config.paths.designDocs } };
|
|
59
62
|
} catch (err) {
|
|
60
|
-
if (err
|
|
63
|
+
if (isConfigNotFoundError(err)) return undefined;
|
|
61
64
|
throw err;
|
|
62
65
|
}
|
|
63
66
|
}
|
|
@@ -68,7 +71,7 @@ async function loadPreCommitImplementationExtensions(): Promise<readonly string[
|
|
|
68
71
|
const resolvedConfig = await configMod.usecases.loadResolvedConfigUseCase.execute();
|
|
69
72
|
return resolvedConfig.config.preCommit?.implementationExtensions;
|
|
70
73
|
} catch (err) {
|
|
71
|
-
if (err
|
|
74
|
+
if (isConfigNotFoundError(err)) return undefined;
|
|
72
75
|
throw err;
|
|
73
76
|
}
|
|
74
77
|
}
|
package/scripts/harness/main.ts
CHANGED
|
@@ -98,6 +98,7 @@ Commands:
|
|
|
98
98
|
disable-feature <name> Disable a harness feature
|
|
99
99
|
list-features List available features
|
|
100
100
|
migrate Migrate phasegate.config.json (--schema v3, --config <path>)
|
|
101
|
+
work-items:status Report or apply derived WI frontmatter status (--dry-run|--apply, --id, --fail-on-stale, --json)
|
|
101
102
|
|
|
102
103
|
render-errors Render harness errors (--format human|agent|ci)
|
|
103
104
|
validate-fix Validate fix examples (--code <code>)
|
|
@@ -192,6 +193,13 @@ function parseTriStateFlag(args: readonly string[], positiveFlag: string, negati
|
|
|
192
193
|
return positiveIdx > negativeIdx;
|
|
193
194
|
}
|
|
194
195
|
|
|
196
|
+
function parseValidateFormat(args: readonly string[]): "human" | "agent" | "ci" | undefined {
|
|
197
|
+
const raw = parseFlag(args, "--format");
|
|
198
|
+
if (raw === undefined) return undefined;
|
|
199
|
+
if (raw === "human" || raw === "agent" || raw === "ci") return raw;
|
|
200
|
+
throw new Error(`Invalid --format value for validate: '${raw}'. Supported values: human, agent, ci.`);
|
|
201
|
+
}
|
|
202
|
+
|
|
195
203
|
function levenshtein(a: string, b: string): number {
|
|
196
204
|
const m = a.length;
|
|
197
205
|
const n = b.length;
|
|
@@ -299,6 +307,17 @@ Options:
|
|
|
299
307
|
"phasegate:status": `Usage: phasegate phasegate:status
|
|
300
308
|
|
|
301
309
|
Display harness status (enabled validators, schema version, hook deployment).`,
|
|
310
|
+
"work-items:status": `Usage: phasegate work-items:status (--dry-run|--apply) [options]
|
|
311
|
+
|
|
312
|
+
Derive WI frontmatter status from inception, product reflection, implementation, and test evidence.
|
|
313
|
+
|
|
314
|
+
Options:
|
|
315
|
+
--dry-run Print current status, derived status, reason, and next action.
|
|
316
|
+
--apply Update only the status line in each stale description.md frontmatter.
|
|
317
|
+
--id <WI-XXX> Limit report/apply to one work item.
|
|
318
|
+
--fail-on-stale Return exit code 1 when dry-run finds stale status.
|
|
319
|
+
--json Output machine-readable JSON.
|
|
320
|
+
--help, -h Show this help`,
|
|
302
321
|
"phasegate:detect-drift": `Usage: phasegate phasegate:detect-drift [options]
|
|
303
322
|
|
|
304
323
|
Run L4-001 drift detection between design documents and source code. WARNING: scans the project filesystem.
|
|
@@ -955,6 +974,23 @@ async function main(): Promise<void> {
|
|
|
955
974
|
break;
|
|
956
975
|
}
|
|
957
976
|
|
|
977
|
+
case "work-items:status": {
|
|
978
|
+
const mod = createTraceabilityModelModule(
|
|
979
|
+
rootDir,
|
|
980
|
+
toTraceabilityModelOptions(resolvedConfig),
|
|
981
|
+
);
|
|
982
|
+
const result = await mod.workItemStatusCommandHandler.execute({
|
|
983
|
+
dryRun: hasFlag(args, "--dry-run"),
|
|
984
|
+
apply: hasFlag(args, "--apply"),
|
|
985
|
+
failOnStale: hasFlag(args, "--fail-on-stale"),
|
|
986
|
+
id: parseFlag(args, "--id"),
|
|
987
|
+
json,
|
|
988
|
+
});
|
|
989
|
+
console.log(result.text);
|
|
990
|
+
process.exit(result.exitCode);
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
|
|
958
994
|
// ── phase-dependency-model ──
|
|
959
995
|
case "check-phase-gate": {
|
|
960
996
|
const phaseConfig = resolvedConfig ? toPhaseConfigSection(resolvedConfig) : undefined;
|
|
@@ -1024,7 +1060,7 @@ async function main(): Promise<void> {
|
|
|
1024
1060
|
const layer = parseFlag(args, "--layer") as "L0" | "L2" | "L3" | "L4" | "all" | undefined;
|
|
1025
1061
|
const unit = parseFlag(args, "--unit");
|
|
1026
1062
|
const phase = parseFlag(args, "--phase");
|
|
1027
|
-
const format =
|
|
1063
|
+
const format = parseValidateFormat(args);
|
|
1028
1064
|
// WI-094 / ADR-017: --fail-on-warning / --no-fail-on-warning / 未指定→config値
|
|
1029
1065
|
const failOnWarning = parseTriStateFlag(args, "--fail-on-warning", "--no-fail-on-warning");
|
|
1030
1066
|
const noL4 = hasFlag(args, "--no-l4");
|
|
@@ -146,7 +146,12 @@ export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSyst
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
const legacyId = await this.readLegacyId(storyId);
|
|
149
|
-
|
|
149
|
+
if (legacyId === null || !annotationIds.includes(legacyId)) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const unitId = this.extractProductUnitId(productPath);
|
|
154
|
+
return !(await this.isAmbiguousLegacyId(legacyId, storyId, unitId));
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
private extractAnnotationIds(content: string): readonly string[] {
|
|
@@ -182,6 +187,47 @@ export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSyst
|
|
|
182
187
|
return null;
|
|
183
188
|
}
|
|
184
189
|
|
|
190
|
+
private extractProductUnitId(productPath: string): string | null {
|
|
191
|
+
const normalized = productPath.split(path.sep).join("/");
|
|
192
|
+
const match = /^docs\/product\/construction\/([^/]+)\//.exec(normalized);
|
|
193
|
+
return match?.[1] ?? null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private async isAmbiguousLegacyId(legacyId: string, storyId: string, unitId: string | null): Promise<boolean> {
|
|
197
|
+
const scopedStoryIds = unitId === null
|
|
198
|
+
? await this.listAllWorkItemDirectories()
|
|
199
|
+
: await this.listStoryDirectories(unitId);
|
|
200
|
+
const matchingStoryIds: string[] = [];
|
|
201
|
+
|
|
202
|
+
for (const candidateStoryId of scopedStoryIds) {
|
|
203
|
+
const candidateLegacyId = await this.readLegacyId(candidateStoryId);
|
|
204
|
+
if (candidateLegacyId === legacyId) {
|
|
205
|
+
matchingStoryIds.push(candidateStoryId);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return matchingStoryIds.length > 1 || (matchingStoryIds.length === 1 && matchingStoryIds[0] !== storyId);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private async listAllWorkItemDirectories(): Promise<readonly string[]> {
|
|
213
|
+
const ids = new Set<string>();
|
|
214
|
+
for (const id of await this.listCrossWorkItemDirectories()) {
|
|
215
|
+
ids.add(id);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const inceptionDir = path.join(this.rootDir, this.inceptionRoot);
|
|
219
|
+
for (const entry of await this.readDirectories(inceptionDir)) {
|
|
220
|
+
if (!entry.isDirectory()) continue;
|
|
221
|
+
const name = entry.name;
|
|
222
|
+
if (name.startsWith("_") || name.startsWith(".") || name === "issues") continue;
|
|
223
|
+
for (const id of await this.listUnitWorkItemDirectories(name)) {
|
|
224
|
+
ids.add(id);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return [...ids].sort();
|
|
229
|
+
}
|
|
230
|
+
|
|
185
231
|
private async listDescriptionCandidates(storyId: string): Promise<readonly string[]> {
|
|
186
232
|
const inceptionDir = path.join(this.rootDir, this.inceptionRoot);
|
|
187
233
|
const candidates: string[] = [path.join(inceptionDir, "_cross", storyId, "description.md")];
|
package/scripts/harness/traceability-model/application/usecases/apply-work-item-status-usecase.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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 type { WorkItemStatusApplyResult } from "../../domain/value-objects/work-item-status-report.js";
|
|
7
|
+
import type { DeriveWorkItemStatusUseCase } from "./derive-work-item-status-usecase.js";
|
|
8
|
+
|
|
9
|
+
export interface ApplyWorkItemStatusUseCaseDeps {
|
|
10
|
+
readonly deriveWorkItemStatusUseCase: Pick<DeriveWorkItemStatusUseCase, "execute">;
|
|
11
|
+
readonly workItemStatusPort: WorkItemStatusPort;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ApplyWorkItemStatusUseCase {
|
|
15
|
+
private readonly deriveWorkItemStatusUseCase: Pick<DeriveWorkItemStatusUseCase, "execute">;
|
|
16
|
+
private readonly workItemStatusPort: WorkItemStatusPort;
|
|
17
|
+
|
|
18
|
+
constructor(deps: ApplyWorkItemStatusUseCaseDeps) {
|
|
19
|
+
this.deriveWorkItemStatusUseCase = deps.deriveWorkItemStatusUseCase;
|
|
20
|
+
this.workItemStatusPort = deps.workItemStatusPort;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async execute(input: { readonly id?: string } = {}): Promise<WorkItemStatusApplyResult> {
|
|
24
|
+
const reports = await this.deriveWorkItemStatusUseCase.execute();
|
|
25
|
+
const targetReports = input.id
|
|
26
|
+
? reports.filter((report) => report.id === input.id)
|
|
27
|
+
: reports;
|
|
28
|
+
return this.workItemStatusPort.applyDerivedStatuses(targetReports);
|
|
29
|
+
}
|
|
30
|
+
}
|
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,121 @@
|
|
|
1
|
+
// @unit traceability-model
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-126
|
|
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
|
+
reflectedUnits: Object.freeze([...this.reflectedUnits(input)]),
|
|
38
|
+
missingReflectionUnits: Object.freeze([...this.missingReflectionUnits(input)]),
|
|
39
|
+
implementationPaths: Object.freeze([...input.implementationPaths]),
|
|
40
|
+
testPaths: Object.freeze([...input.testPaths]),
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private deriveStatus(input: WorkItemStatusInput): WorkItemStatus {
|
|
46
|
+
const type = input.frontmatter.type;
|
|
47
|
+
if (type === "chore") return "drafted";
|
|
48
|
+
|
|
49
|
+
const reflected = this.missingReflectionUnits(input).length === 0;
|
|
50
|
+
const implemented = input.implementationPaths.length > 0;
|
|
51
|
+
const tested = input.testPaths.length > 0;
|
|
52
|
+
|
|
53
|
+
if (type === "fix") {
|
|
54
|
+
if (implemented) return "implemented";
|
|
55
|
+
if (reflected) return "reflected";
|
|
56
|
+
return "drafted";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (tested) return "tested";
|
|
60
|
+
if (implemented) return "implemented";
|
|
61
|
+
if (reflected) return "reflected";
|
|
62
|
+
return "drafted";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private reasonFor(input: WorkItemStatusInput, status: WorkItemStatus): string {
|
|
66
|
+
if (!this.hasRequiredInceptionArtifacts(input)) {
|
|
67
|
+
return `missing inception artifacts: ${this.missingInceptionArtifacts(input).join(", ")}`;
|
|
68
|
+
}
|
|
69
|
+
if (status === "tested") return "test evidence with @work-item-id exists";
|
|
70
|
+
if (status === "implemented") return "implementation evidence with @work-item-id exists";
|
|
71
|
+
if (status === "reflected") return "all affected units have product reflection";
|
|
72
|
+
if (input.frontmatter.type === "chore") return "chore work items complete at drafted";
|
|
73
|
+
return "product reflection is not complete";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private nextActionFor(input: WorkItemStatusInput, status: WorkItemStatus): string {
|
|
77
|
+
const missingArtifacts = this.missingInceptionArtifacts(input);
|
|
78
|
+
if (missingArtifacts.length > 0) {
|
|
79
|
+
return `create inception artifacts: ${missingArtifacts.join(", ")}`;
|
|
80
|
+
}
|
|
81
|
+
const missingUnits = this.missingReflectionUnits(input);
|
|
82
|
+
if (missingUnits.length > 0) {
|
|
83
|
+
return `reflect @work-item-id ${input.frontmatter.id} in product docs for: ${missingUnits.join(", ")}`;
|
|
84
|
+
}
|
|
85
|
+
if (input.frontmatter.type === "chore") return "no further status transition required";
|
|
86
|
+
if (status === "reflected") return `add implementation annotated with @work-item-id ${input.frontmatter.id}`;
|
|
87
|
+
if (status === "implemented" && input.frontmatter.type !== "fix") {
|
|
88
|
+
return `add tests annotated with @work-item-id ${input.frontmatter.id}`;
|
|
89
|
+
}
|
|
90
|
+
return "status is up to date";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private hasRequiredInceptionArtifacts(input: WorkItemStatusInput): boolean {
|
|
94
|
+
return this.missingInceptionArtifacts(input).length === 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private missingInceptionArtifacts(input: WorkItemStatusInput): readonly string[] {
|
|
98
|
+
const existing = new Set(input.existingInceptionArtifacts);
|
|
99
|
+
return input.requiredInceptionArtifacts.filter((artifact) => !existing.has(artifact));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private reflectedUnits(input: WorkItemStatusInput): readonly string[] {
|
|
103
|
+
const reflected = new Set<string>();
|
|
104
|
+
for (const filePath of input.productReflectionPaths) {
|
|
105
|
+
const unit = this.extractConstructionUnit(filePath);
|
|
106
|
+
if (unit) reflected.add(unit);
|
|
107
|
+
}
|
|
108
|
+
return input.affectedUnits.filter((unit) => reflected.has(unit));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private missingReflectionUnits(input: WorkItemStatusInput): readonly string[] {
|
|
112
|
+
if (input.frontmatter.type === "chore") return [];
|
|
113
|
+
const reflected = new Set(this.reflectedUnits(input));
|
|
114
|
+
return input.affectedUnits.filter((unit) => !reflected.has(unit));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private extractConstructionUnit(filePath: string): string | null {
|
|
118
|
+
const match = /^docs\/product\/construction\/([^/]+)\//.exec(filePath);
|
|
119
|
+
return match?.[1] ?? null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @unit traceability-model
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-126
|
|
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 reflectedUnits: readonly string[];
|
|
14
|
+
readonly missingReflectionUnits: readonly string[];
|
|
15
|
+
readonly implementationPaths: readonly string[];
|
|
16
|
+
readonly testPaths: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface WorkItemStatusInput {
|
|
20
|
+
readonly descriptionPath: string;
|
|
21
|
+
readonly directoryId: string;
|
|
22
|
+
readonly ownerUnit: string | null;
|
|
23
|
+
readonly frontmatter: WorkItemFrontmatter;
|
|
24
|
+
readonly requiredInceptionArtifacts: readonly string[];
|
|
25
|
+
readonly existingInceptionArtifacts: readonly string[];
|
|
26
|
+
readonly affectedUnits: readonly string[];
|
|
27
|
+
readonly productReflectionPaths: readonly string[];
|
|
28
|
+
readonly implementationPaths: readonly string[];
|
|
29
|
+
readonly testPaths: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface WorkItemStatusReport {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly type: WorkItemType;
|
|
35
|
+
readonly descriptionPath: string;
|
|
36
|
+
readonly currentStatus: WorkItemStatus;
|
|
37
|
+
readonly derivedStatus: WorkItemStatus;
|
|
38
|
+
readonly stale: boolean;
|
|
39
|
+
readonly reason: string;
|
|
40
|
+
readonly nextAction: string;
|
|
41
|
+
readonly evidence: WorkItemStatusEvidence;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WorkItemStatusApplyResult {
|
|
45
|
+
readonly updated: readonly WorkItemStatusReport[];
|
|
46
|
+
readonly unchanged: readonly WorkItemStatusReport[];
|
|
47
|
+
}
|