phasegate 0.160.21 → 0.162.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 +34 -0
- package/README.md +6 -1
- package/docs/contracts/requirement-test-matrix.schema.json +1 -1
- package/docs/guide/cli-reference.md +1 -1
- package/docs/guide/configuration.md +31 -1
- package/docs/guide/skills-overview.md +2 -0
- package/docs/principles/model-routing.md +1 -0
- package/docs/templates/personal/phasegate-local-config.json +4 -1
- package/docs/templates/project/phasegate.config.json +4 -1
- package/package.json +6 -3
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +91 -23
- package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +4 -0
- package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +54 -5
- package/scripts/harness/agent-integration/infrastructure/adapters/ci-governance-baseline-grandfather-adapter.ts +25 -2
- package/scripts/harness/agent-integration/infrastructure/adapters/phase-gate-query-adapter.ts +17 -2
- package/scripts/harness/biome-ast-engine/domain/value-objects/import-graph.ts +28 -1
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +6 -1
- package/scripts/harness/config-foundation/domain/harness-config.ts +10 -0
- package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +2 -0
- package/scripts/harness/config-foundation/domain/value-objects/l3-config.ts +4 -0
- package/scripts/harness/config-foundation/domain/value-objects/layers-config.ts +1 -1
- package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +20 -5
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +2 -1
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +2 -1
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +2 -1
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +39 -9
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +39 -9
- package/scripts/harness/harness-api/infrastructure/adapters/phase-dependency-model-query-adapter.ts +18 -4
- package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts +27 -4
- package/scripts/harness/installation/application/ports/model-delegation-port.ts +10 -0
- package/scripts/harness/installation/application/usecases/run-install.ts +67 -5
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +53 -5
- package/scripts/harness/installation/composition-root.ts +4 -2
- package/scripts/harness/installation/infrastructure/adapters/skill-deployer-model-delegation-adapter.ts +16 -0
- package/scripts/harness/installation/presentation/cli/install-handler.ts +2 -1
- package/scripts/harness/main.ts +99 -13
- package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +9 -7
- package/scripts/harness/nyquist-validation/domain/services/matrix-validation-service.ts +27 -2
- package/scripts/harness/nyquist-validation/domain/services/requirement-intent-coverage-service.ts +4 -4
- package/scripts/harness/nyquist-validation/domain/value-objects/intent-coverage.ts +33 -0
- package/scripts/harness/nyquist-validation/infrastructure/adapters/markdown-requirement-source-adapter.ts +3 -1
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +8 -3
- package/scripts/harness/quick-mode/composition-root.ts +3 -0
- package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +6 -1
- package/scripts/harness/quick-mode/infrastructure/adapters/validator-system-quick-mode-execution-adapter.ts +18 -0
- package/scripts/harness/quick-mode/presentation/handlers/ci-check-quick-mode-handler.ts +51 -5
- package/scripts/harness/setup/skill-deployer.ts +47 -0
- package/scripts/harness/skill-quality/domain/services/skill-structure-validator.ts +21 -0
- package/scripts/harness/skill-quality/domain/value-objects/skill-structure.ts +2 -0
- package/scripts/harness/skill-quality/infrastructure/adapters/l1-biome-validator-adapter.ts +12 -2
- package/scripts/harness/skill-quality/infrastructure/adapters/l2-validator-system-adapter.ts +12 -2
- package/scripts/harness/traceability-model/application/usecases/apply-work-item-status-usecase.ts +6 -1
- package/scripts/harness/traceability-model/infrastructure/parsers/story-catalog-parser.ts +28 -9
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +76 -25
- package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +20 -8
- package/scripts/harness/validator-system/composition-root.ts +8 -1
- package/scripts/harness/validator-system/domain/ports/validator-config-port.ts +2 -0
- package/scripts/harness/validator-system/domain/services/validator-language-capability-service.ts +64 -0
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +16 -6
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-work-item-reflection-adapter.ts +10 -1
- package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +7 -1
- package/scripts/harness/validator-system/infrastructure/adapters/import-graph-source-analysis-adapter.ts +35 -3
- package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +86 -14
- package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +29 -3
- package/skills/cascade-updater/SKILL.md +1 -0
- package/skills/codebase-mapper/SKILL.md +1 -0
- package/skills/codex-delegator/SKILL.md +1 -0
- package/skills/consistency-checker/SKILL.md +1 -0
- package/skills/doc-freshness-checker/SKILL.md +1 -0
- package/skills/domain-designer/SKILL.md +1 -0
- package/skills/engineering-perspective/SKILL.md +1 -0
- package/skills/environment-designer/SKILL.md +1 -0
- package/skills/implementation-planner/SKILL.md +1 -0
- package/skills/implementation-readiness-checker/SKILL.md +1 -0
- package/skills/it-test-designer/SKILL.md +1 -0
- package/skills/it-test-logic-designer/SKILL.md +1 -0
- package/skills/logical-designer/SKILL.md +1 -0
- package/skills/mock-designer/SKILL.md +1 -0
- package/skills/phasegate-config-doctor/SKILL.md +1 -0
- package/skills/phasegate-toolkit-guide/SKILL.md +1 -0
- package/skills/pointer-validator/SKILL.md +1 -0
- package/skills/product-architect/SKILL.md +1 -0
- package/skills/quick-implementor/SKILL.md +1 -0
- package/skills/scenario-test-designer/SKILL.md +1 -0
- package/skills/scenario-test-logic-designer/SKILL.md +1 -0
- package/skills/skill-creator/SKILL.md +1 -0
- package/skills/story-implementor/SKILL.md +1 -0
- package/skills/story-mapper/SKILL.md +1 -0
- package/skills/story-writer/SKILL.md +1 -0
- package/skills/test-coverage-checker/SKILL.md +1 -0
- package/skills/uiux-designer/SKILL.md +1 -0
- package/skills/unit-designer/SKILL.md +1 -0
- package/skills/unit-test-designer/SKILL.md +1 -0
- package/skills/unit-test-logic-designer/SKILL.md +1 -0
- package/templates/.claude/scripts/deny-check.sh +73 -12
- package/templates/.claude/settings.json +4 -0
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// @work-item-id WI-214
|
|
16
16
|
// @work-item-id WI-215
|
|
17
17
|
// @work-item-id WI-216
|
|
18
|
+
// @work-item-id WI-219
|
|
18
19
|
|
|
19
20
|
import { mkdir, readFile, writeFile, copyFile, chmod, access, lstat, readlink, symlink, readdir, rm } from "node:fs/promises";
|
|
20
21
|
import { dirname, join } from "node:path";
|
|
@@ -25,6 +26,7 @@ import type { RepairMode } from "../../domain/repair-mode.js";
|
|
|
25
26
|
import { getBundledSkillsForSet, type SkillSet } from "../bundled-skill-selection.js";
|
|
26
27
|
import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
|
|
27
28
|
import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
|
|
29
|
+
import type { ModelDelegationPort } from "../ports/model-delegation-port.js";
|
|
28
30
|
|
|
29
31
|
type InstallAction = "missing" | "will-merge" | "will-skip" | "will-overwrite";
|
|
30
32
|
type StrategyType = "json" | "shell" | "yaml-add" | "package-json" | "markdown-managed" | "text-managed" | "copy" | "copy-dir" | "symlink";
|
|
@@ -38,6 +40,10 @@ export interface InstallPlanItem {
|
|
|
38
40
|
readonly summary: string;
|
|
39
41
|
readonly diff: string;
|
|
40
42
|
readonly skillHint: string | null;
|
|
43
|
+
// Non-null when a target could not be applied cleanly and the user must act
|
|
44
|
+
// (e.g. a pre-existing git hook that phasegate will not overwrite). Surfaces
|
|
45
|
+
// situations that would otherwise be silently skipped.
|
|
46
|
+
readonly warning: string | null;
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
export interface RunInstallInput {
|
|
@@ -132,13 +138,25 @@ async function copyDirectory(src: string, dest: string): Promise<void> {
|
|
|
132
138
|
}
|
|
133
139
|
}
|
|
134
140
|
|
|
135
|
-
async function copySelectedSkillDirectories(
|
|
141
|
+
async function copySelectedSkillDirectories(
|
|
142
|
+
modelDelegation: ModelDelegationPort,
|
|
143
|
+
harnessRoot: string,
|
|
144
|
+
projectRoot: string,
|
|
145
|
+
targetRoot: string,
|
|
146
|
+
skills: readonly string[],
|
|
147
|
+
): Promise<void> {
|
|
136
148
|
await mkdir(targetRoot, { recursive: true });
|
|
149
|
+
const modelDelegationPolicy = await modelDelegation.readPolicy(projectRoot);
|
|
137
150
|
for (const skill of skills) {
|
|
138
151
|
const source = join(harnessRoot, "skills", skill);
|
|
139
152
|
const target = join(targetRoot, skill);
|
|
140
153
|
await rm(target, { recursive: true, force: true });
|
|
141
154
|
await copyDirectory(source, target);
|
|
155
|
+
if (modelDelegationPolicy === "none") {
|
|
156
|
+
const skillPath = join(target, "SKILL.md");
|
|
157
|
+
const content = await readFile(skillPath, "utf8");
|
|
158
|
+
await writeFile(skillPath, modelDelegation.renderSkill(content, modelDelegationPolicy), "utf8");
|
|
159
|
+
}
|
|
142
160
|
}
|
|
143
161
|
}
|
|
144
162
|
|
|
@@ -152,8 +170,28 @@ async function listSelectedBundledSkills(harnessRoot: string, skillSet: SkillSet
|
|
|
152
170
|
.sort();
|
|
153
171
|
}
|
|
154
172
|
|
|
173
|
+
// Canonical, key-order-independent serialization used as a dedup key when
|
|
174
|
+
// merging hook arrays. Plain `JSON.stringify` is sensitive to object key order,
|
|
175
|
+
// so two structurally identical hook entries authored with different key orders
|
|
176
|
+
// would be treated as distinct and appended on every merge, breaking merge
|
|
177
|
+
// idempotency. Sorting keys recursively makes the key stable.
|
|
178
|
+
function canonicalize(value: unknown): unknown {
|
|
179
|
+
if (Array.isArray(value)) {
|
|
180
|
+
return value.map(canonicalize);
|
|
181
|
+
}
|
|
182
|
+
if (value !== null && typeof value === "object") {
|
|
183
|
+
const record = value as Record<string, unknown>;
|
|
184
|
+
const sorted: Record<string, unknown> = {};
|
|
185
|
+
for (const key of Object.keys(record).sort()) {
|
|
186
|
+
sorted[key] = canonicalize(record[key]);
|
|
187
|
+
}
|
|
188
|
+
return sorted;
|
|
189
|
+
}
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
192
|
+
|
|
155
193
|
function normalizeJsonEntry(value: unknown): string {
|
|
156
|
-
return JSON.stringify(value);
|
|
194
|
+
return JSON.stringify(canonicalize(value));
|
|
157
195
|
}
|
|
158
196
|
|
|
159
197
|
function mergeHookArrays(existing: unknown, incoming: unknown): unknown[] {
|
|
@@ -331,6 +369,7 @@ export class RunInstallUseCase {
|
|
|
331
369
|
constructor(
|
|
332
370
|
private readonly manifestRepository: ManifestRepositoryPort,
|
|
333
371
|
private readonly hashCalculator: HashCalculatorPort,
|
|
372
|
+
private readonly modelDelegation: ModelDelegationPort,
|
|
334
373
|
) {}
|
|
335
374
|
|
|
336
375
|
async execute(input: RunInstallInput): Promise<RunInstallResult> {
|
|
@@ -371,6 +410,18 @@ export class RunInstallUseCase {
|
|
|
371
410
|
const next = unmanagedPersonalRuntimeFile ? before : this.merge(target, before, template, input.phasegateVersion);
|
|
372
411
|
const didChange = before !== next;
|
|
373
412
|
const action = this.actionFor(before, didChange, input.force);
|
|
413
|
+
// A `copy` target with a pre-existing, non-matching file is left untouched
|
|
414
|
+
// (merge returns `before`). For executable git hooks this means the
|
|
415
|
+
// phasegate hook is NOT wired in — warn instead of skipping silently.
|
|
416
|
+
const preservedExistingHook =
|
|
417
|
+
target.strategy === "copy"
|
|
418
|
+
&& target.executable === true
|
|
419
|
+
&& before !== null
|
|
420
|
+
&& before.trim() !== template.trim()
|
|
421
|
+
&& !input.force;
|
|
422
|
+
const warning = preservedExistingHook
|
|
423
|
+
? `${target.path}: an existing hook was found and left unchanged; phasegate's checks are NOT wired in. Merge the phasegate hook manually (or rerun with --force to back up and overwrite).`
|
|
424
|
+
: null;
|
|
374
425
|
const item: InstallPlanItem = {
|
|
375
426
|
path: target.path,
|
|
376
427
|
action,
|
|
@@ -379,9 +430,12 @@ export class RunInstallUseCase {
|
|
|
379
430
|
changed: didChange,
|
|
380
431
|
summary: unmanagedPersonalRuntimeFile
|
|
381
432
|
? `${target.path}: existing non-phasegate runtime path requires manual review`
|
|
382
|
-
:
|
|
433
|
+
: warning !== null
|
|
434
|
+
? warning
|
|
435
|
+
: didChange ? `${target.path}: ${action}` : `${target.path}: already up to date`,
|
|
383
436
|
diff: unmanagedPersonalRuntimeFile ? "manual review required" : this.diffSummary(before, next),
|
|
384
437
|
skillHint: repairMode === "ai-assisted" ? SKILL_HINT : null,
|
|
438
|
+
warning,
|
|
385
439
|
};
|
|
386
440
|
plan.push(item);
|
|
387
441
|
|
|
@@ -445,7 +499,7 @@ export class RunInstallUseCase {
|
|
|
445
499
|
plan.push(item);
|
|
446
500
|
if (input.apply && item.changed && item.repairMode === "mechanical") {
|
|
447
501
|
try {
|
|
448
|
-
await copySelectedSkillDirectories(input.harnessRoot, join(input.projectRoot, skillPath), selectedPersonalSkills);
|
|
502
|
+
await copySelectedSkillDirectories(this.modelDelegation, input.harnessRoot, input.projectRoot, join(input.projectRoot, skillPath), selectedPersonalSkills);
|
|
449
503
|
await writeFile(
|
|
450
504
|
join(input.projectRoot, skillPath, ".harness-version"),
|
|
451
505
|
`${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
|
|
@@ -535,6 +589,7 @@ export class RunInstallUseCase {
|
|
|
535
589
|
summary: "~/.codex/config.toml: personal mode does not write user-level Codex feature flags; enable hooks manually when needed",
|
|
536
590
|
diff: "manual Codex hooks feature enablement may be required",
|
|
537
591
|
skillHint: null,
|
|
592
|
+
warning: null,
|
|
538
593
|
});
|
|
539
594
|
}
|
|
540
595
|
|
|
@@ -790,6 +845,7 @@ export class RunInstallUseCase {
|
|
|
790
845
|
summary: `${relativePath}: existing non-directory skills path requires manual review`,
|
|
791
846
|
diff: "manual review required",
|
|
792
847
|
skillHint: null,
|
|
848
|
+
warning: null,
|
|
793
849
|
};
|
|
794
850
|
}
|
|
795
851
|
} catch {}
|
|
@@ -817,6 +873,7 @@ export class RunInstallUseCase {
|
|
|
817
873
|
summary: changed ? `${relativePath}: deploy bundled skills` : `${relativePath}: already up to date`,
|
|
818
874
|
diff: changed ? `+ ${skills.length} bundled skills (${skillSet})` : "no changes",
|
|
819
875
|
skillHint: null,
|
|
876
|
+
warning: null,
|
|
820
877
|
};
|
|
821
878
|
}
|
|
822
879
|
|
|
@@ -857,12 +914,13 @@ export class RunInstallUseCase {
|
|
|
857
914
|
summary: changed ? "skills: deploy shared bundled skills" : "skills: already up to date",
|
|
858
915
|
diff: changed ? `+ ${skills.length} bundled skills (${skillSet})` : "no changes",
|
|
859
916
|
skillHint: null,
|
|
917
|
+
warning: null,
|
|
860
918
|
};
|
|
861
919
|
}
|
|
862
920
|
|
|
863
921
|
private async deploySharedSkills(input: RunInstallInput, skills: readonly string[], skillSet: SkillSet): Promise<void> {
|
|
864
922
|
const targetRoot = join(input.projectRoot, "skills");
|
|
865
|
-
await copySelectedSkillDirectories(input.harnessRoot, targetRoot, skills);
|
|
923
|
+
await copySelectedSkillDirectories(this.modelDelegation, input.harnessRoot, input.projectRoot, targetRoot, skills);
|
|
866
924
|
await writeFile(
|
|
867
925
|
join(targetRoot, ".harness-version"),
|
|
868
926
|
`${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
|
|
@@ -914,6 +972,7 @@ export class RunInstallUseCase {
|
|
|
914
972
|
summary: `${relativePath}: already linked`,
|
|
915
973
|
diff: "no changes",
|
|
916
974
|
skillHint: null,
|
|
975
|
+
warning: null,
|
|
917
976
|
};
|
|
918
977
|
}
|
|
919
978
|
return {
|
|
@@ -925,6 +984,7 @@ export class RunInstallUseCase {
|
|
|
925
984
|
summary: `${relativePath}: existing non-phasegate path requires manual review`,
|
|
926
985
|
diff: "manual review required",
|
|
927
986
|
skillHint: null,
|
|
987
|
+
warning: null,
|
|
928
988
|
};
|
|
929
989
|
} catch {
|
|
930
990
|
try {
|
|
@@ -939,6 +999,7 @@ export class RunInstallUseCase {
|
|
|
939
999
|
summary: `${relativePath}: parent path exists and requires manual review`,
|
|
940
1000
|
diff: "manual review required",
|
|
941
1001
|
skillHint: null,
|
|
1002
|
+
warning: null,
|
|
942
1003
|
};
|
|
943
1004
|
}
|
|
944
1005
|
} catch {}
|
|
@@ -951,6 +1012,7 @@ export class RunInstallUseCase {
|
|
|
951
1012
|
summary: `${relativePath}: create symlink`,
|
|
952
1013
|
diff: `+ symlink ${target}`,
|
|
953
1014
|
skillHint: null,
|
|
1015
|
+
warning: null,
|
|
954
1016
|
};
|
|
955
1017
|
}
|
|
956
1018
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// @work-item-id WI-198
|
|
6
6
|
// @work-item-id WI-210
|
|
7
7
|
// @work-item-id WI-216
|
|
8
|
+
// @work-item-id WI-219
|
|
8
9
|
|
|
9
10
|
import { access, chmod, copyFile, lstat, mkdir, readFile, readlink, readdir, rm, symlink, writeFile } from "node:fs/promises";
|
|
10
11
|
import { dirname, join, relative, resolve } from "node:path";
|
|
@@ -15,6 +16,7 @@ import type { RepairMode } from "../../domain/repair-mode.js";
|
|
|
15
16
|
import { getBundledSkillsForSet, type SkillSet } from "../bundled-skill-selection.js";
|
|
16
17
|
import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
|
|
17
18
|
import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
|
|
19
|
+
import type { ModelDelegationPort } from "../ports/model-delegation-port.js";
|
|
18
20
|
|
|
19
21
|
type ReconcileAction = "missing-manifest" | "update" | "add" | "link" | "skip" | "refuse";
|
|
20
22
|
type StrategyType = "json" | "shell" | "yaml-add" | "package-json" | "markdown-managed" | "copy-dir" | "symlink" | "unknown";
|
|
@@ -96,13 +98,25 @@ async function copyDirectory(src: string, dest: string): Promise<void> {
|
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
async function copySelectedSkillDirectories(
|
|
101
|
+
async function copySelectedSkillDirectories(
|
|
102
|
+
modelDelegation: ModelDelegationPort,
|
|
103
|
+
harnessRoot: string,
|
|
104
|
+
projectRoot: string,
|
|
105
|
+
targetRoot: string,
|
|
106
|
+
skills: readonly string[],
|
|
107
|
+
): Promise<void> {
|
|
100
108
|
await mkdir(targetRoot, { recursive: true });
|
|
109
|
+
const modelDelegationPolicy = await modelDelegation.readPolicy(projectRoot);
|
|
101
110
|
for (const skill of skills) {
|
|
102
111
|
const source = join(harnessRoot, "skills", skill);
|
|
103
112
|
const target = join(targetRoot, skill);
|
|
104
113
|
await rm(target, { recursive: true, force: true });
|
|
105
114
|
await copyDirectory(source, target);
|
|
115
|
+
if (modelDelegationPolicy === "none") {
|
|
116
|
+
const skillPath = join(target, "SKILL.md");
|
|
117
|
+
const content = await readFile(skillPath, "utf8");
|
|
118
|
+
await writeFile(skillPath, modelDelegation.renderSkill(content, modelDelegationPolicy), "utf8");
|
|
119
|
+
}
|
|
106
120
|
}
|
|
107
121
|
}
|
|
108
122
|
|
|
@@ -115,6 +129,25 @@ async function listSelectedBundledSkills(harnessRoot: string, skillSet: SkillSet
|
|
|
115
129
|
.sort();
|
|
116
130
|
}
|
|
117
131
|
|
|
132
|
+
async function hasRenderedSkillDrift(
|
|
133
|
+
modelDelegation: ModelDelegationPort,
|
|
134
|
+
harnessRoot: string,
|
|
135
|
+
projectRoot: string,
|
|
136
|
+
targetRoot: string,
|
|
137
|
+
skills: readonly string[],
|
|
138
|
+
): Promise<boolean> {
|
|
139
|
+
const modelDelegationPolicy = await modelDelegation.readPolicy(projectRoot);
|
|
140
|
+
for (const skill of skills) {
|
|
141
|
+
const current = await readTextOrNull(join(targetRoot, skill, "SKILL.md"));
|
|
142
|
+
if (current === null) return true;
|
|
143
|
+
const source = await readTextOrNull(join(harnessRoot, "skills", skill, "SKILL.md"));
|
|
144
|
+
if (source === null) continue;
|
|
145
|
+
const expected = modelDelegation.renderSkill(source, modelDelegationPolicy);
|
|
146
|
+
if (current !== expected) return true;
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
118
151
|
function normalizeJsonEntry(value: unknown): string {
|
|
119
152
|
return JSON.stringify(value);
|
|
120
153
|
}
|
|
@@ -234,6 +267,7 @@ export class RunReconcileUseCase {
|
|
|
234
267
|
constructor(
|
|
235
268
|
private readonly manifestRepository: ManifestRepositoryPort,
|
|
236
269
|
private readonly hashCalculator: HashCalculatorPort,
|
|
270
|
+
private readonly modelDelegation: ModelDelegationPort,
|
|
237
271
|
) {}
|
|
238
272
|
|
|
239
273
|
async execute(input: RunReconcileInput): Promise<RunReconcileResult> {
|
|
@@ -491,7 +525,14 @@ export class RunReconcileUseCase {
|
|
|
491
525
|
}
|
|
492
526
|
const missingManifest = manifest.findEntry(SHARED_SKILLS_VERSION_PATH) === null
|
|
493
527
|
|| skills.some((skill) => manifest.findEntry(`skills/${skill}`) === null);
|
|
494
|
-
const
|
|
528
|
+
const renderedSkillDrift = await hasRenderedSkillDrift(
|
|
529
|
+
this.modelDelegation,
|
|
530
|
+
input.harnessRoot,
|
|
531
|
+
input.projectRoot,
|
|
532
|
+
this.resolveProjectPath(input.projectRoot, "skills"),
|
|
533
|
+
skills,
|
|
534
|
+
);
|
|
535
|
+
const changed = versionContent === null || !versionContent.includes(expectedVersion) || missingSkill || missingManifest || renderedSkillDrift;
|
|
495
536
|
return {
|
|
496
537
|
item: this.item(
|
|
497
538
|
"skills",
|
|
@@ -514,7 +555,7 @@ export class RunReconcileUseCase {
|
|
|
514
555
|
|
|
515
556
|
private async deploySharedSkills(input: RunReconcileInput, skills: readonly string[], skillSet: SkillSet): Promise<void> {
|
|
516
557
|
const targetRoot = this.resolveProjectPath(input.projectRoot, "skills");
|
|
517
|
-
await copySelectedSkillDirectories(input.harnessRoot, targetRoot, skills);
|
|
558
|
+
await copySelectedSkillDirectories(this.modelDelegation, input.harnessRoot, input.projectRoot, targetRoot, skills);
|
|
518
559
|
await writeFile(
|
|
519
560
|
join(targetRoot, ".harness-version"),
|
|
520
561
|
`${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
|
|
@@ -541,7 +582,14 @@ export class RunReconcileUseCase {
|
|
|
541
582
|
}
|
|
542
583
|
const missingManifest = manifest.findEntry(`${relativePath}/.harness-version`) === null
|
|
543
584
|
|| skills.some((skill) => manifest.findEntry(`${relativePath}/${skill}`) === null);
|
|
544
|
-
const
|
|
585
|
+
const renderedSkillDrift = await hasRenderedSkillDrift(
|
|
586
|
+
this.modelDelegation,
|
|
587
|
+
input.harnessRoot,
|
|
588
|
+
input.projectRoot,
|
|
589
|
+
this.resolveProjectPath(input.projectRoot, relativePath),
|
|
590
|
+
skills,
|
|
591
|
+
);
|
|
592
|
+
const changed = versionContent === null || !versionContent.includes(expectedVersion) || missingSkill || missingManifest || renderedSkillDrift;
|
|
545
593
|
return {
|
|
546
594
|
item: this.item(
|
|
547
595
|
relativePath,
|
|
@@ -557,7 +605,7 @@ export class RunReconcileUseCase {
|
|
|
557
605
|
apply: async () => {
|
|
558
606
|
if (!changed) return null;
|
|
559
607
|
const targetRoot = this.resolveProjectPath(input.projectRoot, relativePath);
|
|
560
|
-
await copySelectedSkillDirectories(input.harnessRoot, targetRoot, skills);
|
|
608
|
+
await copySelectedSkillDirectories(this.modelDelegation, input.harnessRoot, input.projectRoot, targetRoot, skills);
|
|
561
609
|
await writeFile(
|
|
562
610
|
join(targetRoot, ".harness-version"),
|
|
563
611
|
`${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
|
|
@@ -26,6 +26,7 @@ import type { UninstallReverseStrategy } from "./domain/ports/uninstall-reverse-
|
|
|
26
26
|
import { FileSystemManifestRepositoryAdapter } from "./infrastructure/adapters/file-system-manifest-repository-adapter.js";
|
|
27
27
|
import { NodeCryptoHashAdapter } from "./infrastructure/adapters/node-crypto-hash-adapter.js";
|
|
28
28
|
import { NodeFsFileInspectorAdapter } from "./infrastructure/adapters/node-fs-file-inspector-adapter.js";
|
|
29
|
+
import { SkillDeployerModelDelegationAdapter } from "./infrastructure/adapters/skill-deployer-model-delegation-adapter.js";
|
|
29
30
|
import { DoctorHandler } from "./presentation/cli/doctor-handler.js";
|
|
30
31
|
import { InstallHandler } from "./presentation/cli/install-handler.js";
|
|
31
32
|
import { ReconcileHandler } from "./presentation/cli/reconcile-handler.js";
|
|
@@ -43,6 +44,7 @@ export function createInstallationModule() {
|
|
|
43
44
|
const inspector = new NodeFsFileInspectorAdapter();
|
|
44
45
|
const manifestRepository = new FileSystemManifestRepositoryAdapter();
|
|
45
46
|
const hashCalculator = new NodeCryptoHashAdapter();
|
|
47
|
+
const modelDelegation = new SkillDeployerModelDelegationAdapter();
|
|
46
48
|
const checks = [
|
|
47
49
|
new ClaudeHookMissingCheck(),
|
|
48
50
|
new ClaudeContextMissingCheck(),
|
|
@@ -58,8 +60,8 @@ export function createInstallationModule() {
|
|
|
58
60
|
new WiWorkflowDriftCheck(),
|
|
59
61
|
];
|
|
60
62
|
const runDoctorDiagnosticsUseCase = new RunDoctorDiagnosticsUseCase(checks, inspector, manifestRepository);
|
|
61
|
-
const runInstallUseCase = new RunInstallUseCase(manifestRepository, hashCalculator);
|
|
62
|
-
const runReconcileUseCase = new RunReconcileUseCase(manifestRepository, hashCalculator);
|
|
63
|
+
const runInstallUseCase = new RunInstallUseCase(manifestRepository, hashCalculator, modelDelegation);
|
|
64
|
+
const runReconcileUseCase = new RunReconcileUseCase(manifestRepository, hashCalculator, modelDelegation);
|
|
63
65
|
const runUninstallUseCase = new RunUninstallUseCase(manifestRepository, hashCalculator);
|
|
64
66
|
return {
|
|
65
67
|
manifestRepository,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @unit installation
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
// @work-item-id WI-219
|
|
4
|
+
|
|
5
|
+
import { readModelDelegationPolicy, renderSkillForModelDelegation } from "../../../setup/skill-deployer.js";
|
|
6
|
+
import type { ModelDelegationPolicy, ModelDelegationPort } from "../../application/ports/model-delegation-port.js";
|
|
7
|
+
|
|
8
|
+
export class SkillDeployerModelDelegationAdapter implements ModelDelegationPort {
|
|
9
|
+
readPolicy(projectRoot: string): Promise<ModelDelegationPolicy> {
|
|
10
|
+
return readModelDelegationPolicy(projectRoot);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
renderSkill(content: string, policy: ModelDelegationPolicy): string {
|
|
14
|
+
return renderSkillForModelDelegation(content, policy);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -44,7 +44,8 @@ export class InstallHandler {
|
|
|
44
44
|
input.apply ? `phasegate install${input.personal ? " --personal" : ""} apply` : `phasegate install${input.personal ? " --personal" : ""} dry-run`,
|
|
45
45
|
...result.plan.map((item) => {
|
|
46
46
|
const hint = item.skillHint ? `; hint: ${item.skillHint}` : "";
|
|
47
|
-
|
|
47
|
+
const warn = item.warning ? `\n WARNING: ${item.warning}` : "";
|
|
48
|
+
return `- ${item.path}: ${item.action} (${item.repairMode}, ${item.strategy}); diff: ${item.diff}${hint}${warn}`;
|
|
48
49
|
}),
|
|
49
50
|
];
|
|
50
51
|
if (result.backupDir !== null) lines.push(`backups: ${result.backupDir}`);
|
package/scripts/harness/main.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* @work-item-id WI-189
|
|
11
11
|
* @work-item-id WI-191
|
|
12
12
|
* @work-item-id WI-195
|
|
13
|
+
* @work-item-id WI-212
|
|
13
14
|
* @work-item-id WI-196
|
|
14
15
|
* @work-item-id WI-197
|
|
15
16
|
* @work-item-id WI-200
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
* @work-item-id WI-206
|
|
20
21
|
* @work-item-id WI-213
|
|
21
22
|
* @work-item-id WI-217
|
|
23
|
+
* @work-item-id WI-219
|
|
22
24
|
*
|
|
23
25
|
* Phasegate CLI エントリポイント。
|
|
24
26
|
* 各Unitの Composition Root からハンドラーを取得し、コマンドに応じてディスパッチする。
|
|
@@ -36,6 +38,7 @@ import {
|
|
|
36
38
|
writeFile as fsWriteFile,
|
|
37
39
|
} from "node:fs/promises";
|
|
38
40
|
import { dirname, join, resolve } from "node:path";
|
|
41
|
+
import { fileURLToPath } from "node:url";
|
|
39
42
|
import { createAdrFoundationModule } from "./adr-foundation/composition-root.js";
|
|
40
43
|
import { createBiomeAstEngineModule } from "./biome-ast-engine/composition-root.js";
|
|
41
44
|
import { buildCiGovernance } from "./ci-governance/composition-root.js";
|
|
@@ -122,6 +125,19 @@ function isJsonRecord(value: unknown): value is Record<string, unknown> {
|
|
|
122
125
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
async function isModelDelegationDisabled(rootDir: string): Promise<boolean> {
|
|
129
|
+
try {
|
|
130
|
+
const raw = await fsReadFile(join(rootDir, "phasegate.config.json"), "utf-8");
|
|
131
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
132
|
+
if (!isJsonRecord(parsed) || !isJsonRecord(parsed.modelRouting)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
return parsed.modelRouting.delegation === "none";
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
125
141
|
async function ensurePhasegatePackageDependency(rootDir: string, version: string): Promise<PackageDependencyResult> {
|
|
126
142
|
const packageJsonPath = join(rootDir, "package.json");
|
|
127
143
|
let pkg: PackageJsonDocument = {};
|
|
@@ -167,7 +183,7 @@ Setup:
|
|
|
167
183
|
init Initialize project: deploy skills + design docs + phasegate.config.json
|
|
168
184
|
(--name <project-name>, --preset <full|standard|minimal|custom>,
|
|
169
185
|
--skills <core|all>, --agent <claude|codex|both>, --workflow <standard|strict>,
|
|
170
|
-
--with-husky, --with-ci, --yes)
|
|
186
|
+
--language <language>, --with-husky, --with-ci, --yes)
|
|
171
187
|
update-skills Alias for reconcile (kept for compatibility)
|
|
172
188
|
doctor Diagnose silent installation failures (--json, --strict, --personal, --agent <claude|codex|both>, --report-out <path>)
|
|
173
189
|
scaffold-wi <unit|_cross> <story|issue|fix|refactor|chore>
|
|
@@ -320,21 +336,42 @@ async function listFilesRecursive(root: string): Promise<string[]> {
|
|
|
320
336
|
}
|
|
321
337
|
}
|
|
322
338
|
|
|
323
|
-
|
|
324
|
-
|
|
339
|
+
// Normalize path separators so scanning works on Windows (`\`) as well as POSIX.
|
|
340
|
+
export function toPosixPath(filePath: string): string {
|
|
341
|
+
return filePath.replace(/\\/g, "/");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Derive the next WI id from an already-collected list of file paths. Exposed
|
|
345
|
+
// (with the file list injected) so the numbering rules — Windows separator
|
|
346
|
+
// handling and dynamic zero-pad width past WI-999 — can be unit tested without
|
|
347
|
+
// touching the filesystem.
|
|
348
|
+
export function computeNextWorkItemId(files: readonly string[]): string {
|
|
325
349
|
let max = 0;
|
|
350
|
+
let maxWidth = 3;
|
|
326
351
|
for (const file of files) {
|
|
327
|
-
const match = file.match(/\/WI-(\d
|
|
328
|
-
if (match)
|
|
352
|
+
const match = toPosixPath(file).match(/\/WI-(\d+)\/description\.md$/);
|
|
353
|
+
if (match) {
|
|
354
|
+
max = Math.max(max, Number(match[1]));
|
|
355
|
+
maxWidth = Math.max(maxWidth, match[1].length);
|
|
356
|
+
}
|
|
329
357
|
}
|
|
330
|
-
|
|
358
|
+
const next = max + 1;
|
|
359
|
+
return `WI-${String(next).padStart(maxWidth, "0")}`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function nextWorkItemId(rootDir: string, inceptionRoot = "docs/inception"): Promise<string> {
|
|
363
|
+
const files = await listFilesRecursive(join(rootDir, inceptionRoot));
|
|
364
|
+
return computeNextWorkItemId(files);
|
|
331
365
|
}
|
|
332
366
|
|
|
333
367
|
async function countLegacyPlansWithoutWorkItems(rootDir: string): Promise<number> {
|
|
334
368
|
const files = await listFilesRecursive(join(rootDir, "docs", "inception"));
|
|
335
|
-
const hasWorkItem = files.some((file) => /\/WI-\d
|
|
369
|
+
const hasWorkItem = files.some((file) => /\/WI-\d+\/description\.md$/.test(toPosixPath(file)));
|
|
336
370
|
if (hasWorkItem) return 0;
|
|
337
|
-
return files.filter((file) =>
|
|
371
|
+
return files.filter((file) => {
|
|
372
|
+
const posix = toPosixPath(file);
|
|
373
|
+
return posix.includes("/codding_plan/") || posix.endsWith("_plan.md");
|
|
374
|
+
}).length;
|
|
338
375
|
}
|
|
339
376
|
|
|
340
377
|
async function scaffoldInceptionRoots(rootDir: string, unit: string | null = null, inceptionRoot = "docs/inception"): Promise<void> {
|
|
@@ -605,6 +642,7 @@ Options:
|
|
|
605
642
|
--skills <core|all> Skill set to deploy (default: "all")
|
|
606
643
|
--agent <claude|codex|both> Agent integration target (default: "claude")
|
|
607
644
|
--workflow <standard|strict> Workflow enforcement defaults (default: "standard")
|
|
645
|
+
--language <language> Project language declaration (default: "typescript")
|
|
608
646
|
--with-husky Install Husky pre-commit hooks
|
|
609
647
|
--with-ci Install GitHub Actions workflows
|
|
610
648
|
--yes Skip confirmation prompts
|
|
@@ -1362,11 +1400,11 @@ function buildConfigPatchPreview(intent: ConfigChangeIntent, before: unknown | n
|
|
|
1362
1400
|
const configIntents: Record<ConfigChangeIntent, readonly { readonly pointer: string; readonly path: readonly string[]; readonly value: unknown }[]> = {
|
|
1363
1401
|
"l4-strict": [
|
|
1364
1402
|
{ pointer: "/layers/L4/enabled", path: ["layers", "L4", "enabled"], value: true },
|
|
1365
|
-
{ pointer: "/
|
|
1403
|
+
{ pointer: "/validate/failOnWarning", path: ["validate", "failOnWarning"], value: true },
|
|
1366
1404
|
],
|
|
1367
1405
|
"ci-fail-on-warning": [
|
|
1368
1406
|
{ pointer: "/ci/enabled", path: ["ci", "enabled"], value: true },
|
|
1369
|
-
{ pointer: "/
|
|
1407
|
+
{ pointer: "/validate/failOnWarning", path: ["validate", "failOnWarning"], value: true },
|
|
1370
1408
|
],
|
|
1371
1409
|
"quick-mode-strict": [
|
|
1372
1410
|
{ pointer: "/quickMode/allowedCategories", path: ["quickMode", "allowedCategories"], value: ["bugfix"] },
|
|
@@ -1467,7 +1505,7 @@ async function buildConfigChangePlan(rootDir: string, intent: ConfigChangeIntent
|
|
|
1467
1505
|
readonly risks: readonly string[];
|
|
1468
1506
|
}> = {
|
|
1469
1507
|
"l4-strict": {
|
|
1470
|
-
targets: ["phasegate.config.json: layers.L4.enabled", "phasegate.config.json:
|
|
1508
|
+
targets: ["phasegate.config.json: layers.L4.enabled", "phasegate.config.json: validate.failOnWarning"],
|
|
1471
1509
|
managedTargets: ["phasegate.config.json"],
|
|
1472
1510
|
externalActions: [],
|
|
1473
1511
|
commands: ["phasegate config:plan --intent l4-strict --apply --json", "phasegate validate --layer L4 --fail-on-warning --format human"],
|
|
@@ -1740,6 +1778,7 @@ async function main(): Promise<void> {
|
|
|
1740
1778
|
"--skills",
|
|
1741
1779
|
"--agent",
|
|
1742
1780
|
"--workflow",
|
|
1781
|
+
"--language",
|
|
1743
1782
|
"--with-husky",
|
|
1744
1783
|
"--with-ci",
|
|
1745
1784
|
"--yes",
|
|
@@ -1780,6 +1819,11 @@ async function main(): Promise<void> {
|
|
|
1780
1819
|
process.exit(2);
|
|
1781
1820
|
}
|
|
1782
1821
|
const workflow = parseWorkflowMode(workflowRaw);
|
|
1822
|
+
const language = parseFlag(args, "--language") ?? "typescript";
|
|
1823
|
+
if (language.trim() === "") {
|
|
1824
|
+
console.error("Invalid --language value: value must not be empty.");
|
|
1825
|
+
process.exit(2);
|
|
1826
|
+
}
|
|
1783
1827
|
const deployClaude = agent === "claude" || agent === "both";
|
|
1784
1828
|
const deployCodex = agent === "codex" || agent === "both";
|
|
1785
1829
|
const result = await deploySkills(harnessRoot, rootDir, skillSet);
|
|
@@ -1792,6 +1836,7 @@ async function main(): Promise<void> {
|
|
|
1792
1836
|
const configResult = await initHarnessConfig(rootDir, projectName, phasePreset, {
|
|
1793
1837
|
ciEnabled: withCi,
|
|
1794
1838
|
workflow,
|
|
1839
|
+
language,
|
|
1795
1840
|
});
|
|
1796
1841
|
if (workflow === "strict") {
|
|
1797
1842
|
await scaffoldInceptionRoots(rootDir);
|
|
@@ -2743,9 +2788,28 @@ async function main(): Promise<void> {
|
|
|
2743
2788
|
}
|
|
2744
2789
|
|
|
2745
2790
|
case "phasegate:generate-matrix": {
|
|
2791
|
+
// REAL registry: 有効 storyId 一覧を traceability-model の StoryCatalog から取得する。
|
|
2792
|
+
// 空スタブだと生成マトリクスの storyId 整合性検査が事実上 no-op になるため配線する。
|
|
2793
|
+
// config・catalog が読めない環境(config 不在など)では空配列にフォールバックし、
|
|
2794
|
+
// マトリクス生成自体は継続する。
|
|
2795
|
+
const loadCatalogStoryIds = async (): Promise<readonly string[]> => {
|
|
2796
|
+
try {
|
|
2797
|
+
const { createConfigFoundationModule } = await import("./config-foundation/composition-root.js");
|
|
2798
|
+
const configModule = createConfigFoundationModule();
|
|
2799
|
+
const resolvedConfig = await configModule.usecases.loadResolvedConfigUseCase.execute();
|
|
2800
|
+
const { createTraceabilityModelModule } = await import("./traceability-model/composition-root.js");
|
|
2801
|
+
const traceModule = createTraceabilityModelModule(process.cwd(), {
|
|
2802
|
+
pathRoots: { designDocsRoot: resolvedConfig.config.paths.designDocs },
|
|
2803
|
+
});
|
|
2804
|
+
const catalogStoryIds = await traceModule.storyCatalog.getAllStoryIds();
|
|
2805
|
+
return catalogStoryIds.map((s) => s.value);
|
|
2806
|
+
} catch {
|
|
2807
|
+
return [];
|
|
2808
|
+
}
|
|
2809
|
+
};
|
|
2746
2810
|
const { createNyquistValidationModule } = await import("./nyquist-validation/composition-root.js");
|
|
2747
2811
|
const mod = createNyquistValidationModule({
|
|
2748
|
-
getStoryIds:
|
|
2812
|
+
getStoryIds: loadCatalogStoryIds,
|
|
2749
2813
|
});
|
|
2750
2814
|
const flags: Record<string, boolean | string> = {};
|
|
2751
2815
|
if (json) flags.json = true;
|
|
@@ -3133,6 +3197,13 @@ Examples:
|
|
|
3133
3197
|
printSubcommandHelp("delegate-sonnet");
|
|
3134
3198
|
process.exit(0);
|
|
3135
3199
|
}
|
|
3200
|
+
if (!hasFlag(args, "--dry-run") && await isModelDelegationDisabled(rootDir)) {
|
|
3201
|
+
console.error(JSON.stringify({
|
|
3202
|
+
code: "MODEL_DELEGATION_DISABLED",
|
|
3203
|
+
message: "phasegate delegate-sonnet is disabled by modelRouting.delegation=none",
|
|
3204
|
+
}));
|
|
3205
|
+
process.exit(1);
|
|
3206
|
+
}
|
|
3136
3207
|
const { spawn } = await import("node:child_process");
|
|
3137
3208
|
const scriptPath = join(harnessRoot, "scripts/delegate-sonnet.sh");
|
|
3138
3209
|
const forwardArgs = args.slice(1);
|
|
@@ -3220,4 +3291,19 @@ Examples:
|
|
|
3220
3291
|
}
|
|
3221
3292
|
}
|
|
3222
3293
|
|
|
3223
|
-
|
|
3294
|
+
// Only run the CLI when this module is the process entry point. Guarding this
|
|
3295
|
+
// keeps `main()` from executing (and calling process.exit) when the module is
|
|
3296
|
+
// imported for unit testing of exported helpers such as computeNextWorkItemId.
|
|
3297
|
+
function isMainModule(): boolean {
|
|
3298
|
+
const entry = process.argv[1];
|
|
3299
|
+
if (!entry) return false;
|
|
3300
|
+
try {
|
|
3301
|
+
return resolve(entry) === fileURLToPath(import.meta.url);
|
|
3302
|
+
} catch {
|
|
3303
|
+
return false;
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
if (isMainModule()) {
|
|
3308
|
+
main();
|
|
3309
|
+
}
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
// @work-item-id WI-125
|
|
4
4
|
// @work-item-id WI-131
|
|
5
5
|
|
|
6
|
+
// Intent coverage 系の型は domain 層(value-objects/intent-coverage)を正準とし、
|
|
7
|
+
// application 層はそれを参照・再エクスポートする(domain ← application の正しい依存方向)。
|
|
8
|
+
import type {
|
|
9
|
+
IntentCoverageStatus as DomainIntentCoverageStatus,
|
|
10
|
+
IntentCoverageItem as DomainIntentCoverageItem,
|
|
11
|
+
} from '../../domain/value-objects/intent-coverage.js';
|
|
12
|
+
|
|
6
13
|
export interface RequirementSourceDto {
|
|
7
14
|
readonly storyId: string;
|
|
8
15
|
readonly acIds: readonly string[];
|
|
@@ -48,14 +55,9 @@ export interface OrphanTestDto {
|
|
|
48
55
|
readonly testName?: string;
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
export type IntentCoverageStatus =
|
|
58
|
+
export type IntentCoverageStatus = DomainIntentCoverageStatus;
|
|
52
59
|
|
|
53
|
-
export
|
|
54
|
-
readonly storyId: string;
|
|
55
|
-
readonly acId: string;
|
|
56
|
-
readonly status: IntentCoverageStatus;
|
|
57
|
-
readonly warnings: readonly string[];
|
|
58
|
-
}
|
|
60
|
+
export type IntentCoverageItemDto = DomainIntentCoverageItem;
|
|
59
61
|
|
|
60
62
|
export interface MatrixGenerationReportDto {
|
|
61
63
|
readonly missingTests: readonly MissingTestDto[];
|