phasegate 0.315.0 → 0.340.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 +4 -0
- package/README.ja.md +22 -9
- package/README.md +28 -17
- package/docs/ADR/038-config-state-operation-permission-policy.md +5 -2
- package/docs/ADR/039-hook-observable-state-as-authorization-unit.md +74 -0
- package/docs/ADR/040-quick-mode-config-via-preset-resolution.md +66 -0
- package/docs/ADR/041-non-excludable-agent-trust-roots.md +68 -0
- package/docs/guide/antigravity-integration.md +26 -0
- package/docs/guide/cli-reference.md +93 -11
- package/docs/guide/codex-integration.md +44 -107
- package/docs/guide/configuration.md +43 -2
- package/docs/guide/grok-integration.md +26 -0
- package/docs/guide/hooks-integration.md +27 -4
- package/docs/guide/installation.md +4 -2
- package/docs/guide/quick-vs-full-mode.md +36 -1
- package/docs/guide/setup-artifacts.md +4 -4
- package/docs/guide/skills-overview.md +1 -1
- package/package.json +2 -2
- package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +6 -2
- package/scripts/harness/agent-integration/application/dto/normalized-pre-tool-use-request.ts +32 -0
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +17 -9
- package/scripts/harness/agent-integration/domain/ports/full-mode-requirement-query-port.ts +3 -1
- package/scripts/harness/agent-integration/domain/services/apply-patch-write-target-extractor.ts +98 -0
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +196 -54
- package/scripts/harness/agent-integration/domain/value-objects/hook-translation-result.ts +1 -1
- package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +39 -17
- package/scripts/harness/agent-integration/infrastructure/adapters/quick-mode-full-mode-requirement-adapter.ts +15 -1
- package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +1 -1
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +1 -0
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +129 -73
- package/scripts/harness/agent-integration/presentation/pre-tool-use-payload-normalizer.ts +228 -0
- package/scripts/harness/agent-integration/presentation/pre-tool-use-response-renderer.ts +38 -0
- package/scripts/harness/ci-governance/application/dto/scaffold-inception-input.ts +9 -0
- package/scripts/harness/ci-governance/application/dto/scaffold-inception-output.ts +13 -0
- package/scripts/harness/ci-governance/application/usecases/list-templates-usecase.ts +30 -0
- package/scripts/harness/ci-governance/application/usecases/scaffold-inception-usecase.ts +63 -0
- package/scripts/harness/ci-governance/application/usecases/show-template-usecase.ts +39 -0
- package/scripts/harness/ci-governance/composition-root.ts +55 -3
- package/scripts/harness/ci-governance/domain/ports/inception-doc-writer-port.ts +19 -0
- package/scripts/harness/ci-governance/domain/ports/inception-template-repository-port.ts +16 -0
- package/scripts/harness/ci-governance/domain/ports/template-catalog-port.ts +23 -0
- package/scripts/harness/ci-governance/domain/value-objects/inception-doc-kind.ts +109 -0
- package/scripts/harness/ci-governance/domain/value-objects/integrity-target.ts +2 -0
- package/scripts/harness/ci-governance/domain/value-objects/template-catalog-entry.ts +53 -0
- package/scripts/harness/ci-governance/domain/value-objects/template-name.ts +50 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-doc-writer-adapter.ts +42 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-template-repository-adapter.ts +32 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-template-catalog-adapter.ts +60 -0
- package/scripts/harness/ci-governance/presentation/handlers/scaffold-inception-handler.ts +101 -0
- package/scripts/harness/ci-governance/presentation/handlers/templates-handler.ts +103 -0
- package/scripts/harness/config-foundation/domain/harness-config.ts +12 -0
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +2 -2
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +2 -2
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +2 -2
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +44 -1
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +42 -1
- package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +3 -1
- package/scripts/harness/installation/application/checks/antigravity-hook-missing-check.ts +79 -0
- package/scripts/harness/installation/application/checks/codex-hook-missing-check.ts +70 -6
- package/scripts/harness/installation/application/checks/grok-hook-missing-check.ts +79 -0
- package/scripts/harness/installation/application/checks/husky-runtime-inactive-check.ts +33 -0
- package/scripts/harness/installation/application/named-hook-json.ts +17 -0
- package/scripts/harness/installation/application/operator-notice.ts +42 -0
- package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +18 -7
- package/scripts/harness/installation/application/usecases/run-install.ts +152 -52
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +86 -10
- package/scripts/harness/installation/application/usecases/run-uninstall.ts +149 -25
- package/scripts/harness/installation/composition-root.ts +10 -0
- package/scripts/harness/installation/domain/agent-target.ts +39 -0
- package/scripts/harness/installation/domain/check-id.ts +5 -0
- package/scripts/harness/installation/domain/husky-runtime-state.ts +34 -0
- package/scripts/harness/installation/domain/ports/git-hooks-runtime-probe.ts +9 -0
- package/scripts/harness/installation/domain/repair-table.ts +5 -0
- package/scripts/harness/installation/infrastructure/adapters/git-hooks-runtime-probe-adapter.ts +62 -0
- package/scripts/harness/installation/presentation/cli/install-handler.ts +23 -4
- package/scripts/harness/installation/presentation/cli/reconcile-handler.ts +7 -1
- package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +27 -0
- package/scripts/harness/main.ts +202 -51
- package/scripts/harness/phase-dependency-model/application/services/evidence-bundle-assembler.ts +2 -1
- package/scripts/harness/phase-dependency-model/domain/ports/plan-document-reader-port.ts +12 -0
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +4 -1
- package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts +1 -1
- package/scripts/harness/quick-mode/application/dto/quick-mode-eligibility-contract.ts +1 -1
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +3 -1
- package/scripts/harness/quick-mode/domain/errors/quick-mode-config-error.ts +19 -0
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +59 -19
- package/scripts/harness/quick-mode/domain/types/rejection-rule.ts +1 -1
- package/scripts/harness/quick-mode/domain/value-objects/category-override-rules.ts +147 -0
- package/scripts/harness/quick-mode/domain/value-objects/change-category.ts +33 -0
- package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +31 -10
- package/scripts/harness/quick-mode/index.ts +7 -1
- package/scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts +83 -32
- package/scripts/harness/regression-suite/infrastructure/adapters/vitest-test-runner-adapter.ts +13 -3
- package/skills/product-architect/SKILL.md +19 -0
- package/skills/story-mapper/SKILL.md +11 -0
- package/skills/story-writer/SKILL.md +11 -0
- package/skills/unit-designer/SKILL.md +11 -0
- package/templates/.agents/hooks.json +16 -0
- package/templates/.claude/scripts/analyze-errors-hook.sh +2 -2
- package/templates/.claude/settings.json +5 -3
- package/templates/.codex/hooks.json +2 -2
- package/templates/product_overview.template.md +85 -0
- package/templates/product_overview_plan.template.md +55 -0
- package/templates/story_mapping_plan.template.md +61 -0
- package/templates/story_writer_plan.template.md +61 -0
- package/templates/unit_design_plan.template.md +63 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-368
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* scaffold 可能な inception / product 文書の種別。
|
|
7
|
+
*
|
|
8
|
+
* `DesignPhase` と同型だが **unit 軸を持たない**。`DesignPhase` は `--unit` 必須を
|
|
9
|
+
* 前提とする書き込み先解決を持つため、`_shared` 配下や product 直下の文書を
|
|
10
|
+
* 混ぜると「unit 必須なのに unit を使わない phase」という不整合な不変条件になる。
|
|
11
|
+
*
|
|
12
|
+
* 収録範囲は L2 の Level-1 フェーズゲートを塞ぐ文書に限定する(GitHub issue #42)。
|
|
13
|
+
* `user_stories.md` / `user_story_mapping.md` / `units/*.md` は段階投入のため未収録。
|
|
14
|
+
*/
|
|
15
|
+
export const INCEPTION_DOC_KINDS = [
|
|
16
|
+
"product-overview-plan",
|
|
17
|
+
"product-overview",
|
|
18
|
+
"story-writer-plan",
|
|
19
|
+
"story-mapping-plan",
|
|
20
|
+
"unit-design-plan",
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export type InceptionDocKindValue = (typeof INCEPTION_DOC_KINDS)[number];
|
|
24
|
+
|
|
25
|
+
/** 書き込み先解決に使う、解決済みドキュメントルート */
|
|
26
|
+
export interface InceptionDocRoots {
|
|
27
|
+
/** `paths.inceptionDocs`(既定 `docs/inception`) */
|
|
28
|
+
readonly inceptionDocsRoot: string;
|
|
29
|
+
/** `paths.designDocs`(既定 `docs/product/construction`) */
|
|
30
|
+
readonly designDocsRoot: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_INCEPTION_DOC_ROOTS: InceptionDocRoots = Object.freeze({
|
|
34
|
+
inceptionDocsRoot: "docs/inception",
|
|
35
|
+
designDocsRoot: "docs/product/construction",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/** `docs/product/construction` -> `docs/product`(phase node の `{designDocsRoot}/..` と同一規則) */
|
|
39
|
+
function productRootOf(designDocsRoot: string): string {
|
|
40
|
+
const normalized = designDocsRoot.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
41
|
+
const lastSlash = normalized.lastIndexOf("/");
|
|
42
|
+
return lastSlash <= 0 ? "." : normalized.slice(0, lastSlash);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class InceptionDocKind {
|
|
46
|
+
private constructor(readonly value: InceptionDocKindValue) {
|
|
47
|
+
Object.freeze(this);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static create(input: string): InceptionDocKind {
|
|
51
|
+
if (!InceptionDocKind.isValid(input)) {
|
|
52
|
+
throw new Error(`未知の doc-kind: "${input}"。許容値: ${INCEPTION_DOC_KINDS.join(", ")}`);
|
|
53
|
+
}
|
|
54
|
+
return new InceptionDocKind(input);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static isValid(input: string): input is InceptionDocKindValue {
|
|
58
|
+
return (INCEPTION_DOC_KINDS as readonly string[]).includes(input);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
equals(other: InceptionDocKind): boolean {
|
|
62
|
+
return this.value === other.value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** この kind に対応するテンプレートファイル名(`templates/` 配下) */
|
|
66
|
+
get templateFileName(): string {
|
|
67
|
+
switch (this.value) {
|
|
68
|
+
case "product-overview-plan":
|
|
69
|
+
return "product_overview_plan.template.md";
|
|
70
|
+
case "product-overview":
|
|
71
|
+
return "product_overview.template.md";
|
|
72
|
+
case "story-writer-plan":
|
|
73
|
+
return "story_writer_plan.template.md";
|
|
74
|
+
case "story-mapping-plan":
|
|
75
|
+
return "story_mapping_plan.template.md";
|
|
76
|
+
case "unit-design-plan":
|
|
77
|
+
return "unit_design_plan.template.md";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** この kind が生成する文書のファイル名 */
|
|
82
|
+
get docFileName(): string {
|
|
83
|
+
switch (this.value) {
|
|
84
|
+
case "product-overview-plan":
|
|
85
|
+
return "product_overview_plan.md";
|
|
86
|
+
case "product-overview":
|
|
87
|
+
return "product_overview.md";
|
|
88
|
+
case "story-writer-plan":
|
|
89
|
+
return "story_writer_plan.md";
|
|
90
|
+
case "story-mapping-plan":
|
|
91
|
+
return "story_mapping_plan.md";
|
|
92
|
+
case "unit-design-plan":
|
|
93
|
+
return "unit_design_plan.md";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* プロジェクトルートからの相対パス(POSIX 区切り)。
|
|
99
|
+
* `_shared/*_plan.md` は `{inceptionDocsRoot}/_shared/` 直下、
|
|
100
|
+
* `product_overview.md` は `dirname({designDocsRoot})` 直下に置く。
|
|
101
|
+
*/
|
|
102
|
+
relativeTargetPath(roots: InceptionDocRoots = DEFAULT_INCEPTION_DOC_ROOTS): string {
|
|
103
|
+
if (this.value === "product-overview") {
|
|
104
|
+
return `${productRootOf(roots.designDocsRoot)}/${this.docFileName}`;
|
|
105
|
+
}
|
|
106
|
+
const inceptionRoot = roots.inceptionDocsRoot.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
107
|
+
return `${inceptionRoot}/_shared/${this.docFileName}`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @unit ci-governance
|
|
2
2
|
// @layer domain
|
|
3
|
+
// @work-item-id WI-384
|
|
3
4
|
|
|
4
5
|
export interface IntegrityTargetProps {
|
|
5
6
|
readonly include: readonly string[];
|
|
@@ -31,6 +32,7 @@ export class IntegrityTarget {
|
|
|
31
32
|
"skills/*/SKILL.md",
|
|
32
33
|
".claude/settings.json",
|
|
33
34
|
".claude/scripts/*.sh",
|
|
35
|
+
".codex/hooks.json",
|
|
34
36
|
".husky/*",
|
|
35
37
|
"docs/templates/agent-context/**",
|
|
36
38
|
],
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-367
|
|
4
|
+
|
|
5
|
+
import { TemplateName } from "./template-name.js";
|
|
6
|
+
|
|
7
|
+
/** `<name>.template.<ext>` 形式のテンプレートファイル名を分解する */
|
|
8
|
+
const TEMPLATE_FILE_PATTERN = /^(.+)\.template\.([a-z0-9]+)$/;
|
|
9
|
+
|
|
10
|
+
export interface TemplateCatalogEntryProps {
|
|
11
|
+
readonly name: TemplateName;
|
|
12
|
+
readonly fileName: string;
|
|
13
|
+
readonly extension: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `templates/` 配下に実在する 1 テンプレートの記述子。
|
|
18
|
+
*
|
|
19
|
+
* `fileName` は readdir が返した実ファイル名であり、ユーザー入力を含まない。
|
|
20
|
+
* `templates show` はこのエントリを `name` で完全一致検索し、
|
|
21
|
+
* 見つかった `fileName` のみをテンプレートディレクトリに join する。
|
|
22
|
+
*/
|
|
23
|
+
export class TemplateCatalogEntry {
|
|
24
|
+
readonly name: TemplateName;
|
|
25
|
+
readonly fileName: string;
|
|
26
|
+
readonly extension: string;
|
|
27
|
+
|
|
28
|
+
private constructor(props: TemplateCatalogEntryProps) {
|
|
29
|
+
this.name = props.name;
|
|
30
|
+
this.fileName = props.fileName;
|
|
31
|
+
this.extension = props.extension;
|
|
32
|
+
Object.freeze(this);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* テンプレートファイル名からエントリを生成する。
|
|
37
|
+
* `<name>.template.<ext>` 形式でない、または name 部分が
|
|
38
|
+
* `TemplateName` の不変条件を満たさない場合は null を返す(catalog から除外される)。
|
|
39
|
+
*/
|
|
40
|
+
static fromFileName(fileName: string): TemplateCatalogEntry | null {
|
|
41
|
+
const matched = TEMPLATE_FILE_PATTERN.exec(fileName);
|
|
42
|
+
if (matched === null) return null;
|
|
43
|
+
|
|
44
|
+
const [, rawName, extension] = matched;
|
|
45
|
+
if (!TemplateName.isValid(rawName)) return null;
|
|
46
|
+
|
|
47
|
+
return new TemplateCatalogEntry({
|
|
48
|
+
name: TemplateName.create(rawName),
|
|
49
|
+
fileName,
|
|
50
|
+
extension,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-367
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* テンプレート識別子。**ファイルパスではない**。
|
|
7
|
+
*
|
|
8
|
+
* `phasegate templates show <name>` の `<name>` を受け取る第一防壁。
|
|
9
|
+
* `..` / `/` / `\` を含む入力をここで弾くが、これは最終防壁ではない。
|
|
10
|
+
* 最終防壁は `TemplateCatalogPort` が readdir した実ファイル名との完全一致照合であり、
|
|
11
|
+
* ユーザー入力文字列が `path.join` の引数になる経路を持たないことで
|
|
12
|
+
* path traversal を構造的に不能にする。
|
|
13
|
+
*/
|
|
14
|
+
const TEMPLATE_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
15
|
+
|
|
16
|
+
export class InvalidTemplateNameError extends Error {
|
|
17
|
+
constructor(input: string) {
|
|
18
|
+
super(`テンプレート名が不正です: "${input}"(使用可能: 英小文字・数字・_ ・-)`);
|
|
19
|
+
this.name = "InvalidTemplateNameError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class TemplateName {
|
|
24
|
+
readonly value: string;
|
|
25
|
+
|
|
26
|
+
private constructor(value: string) {
|
|
27
|
+
this.value = value;
|
|
28
|
+
Object.freeze(this);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static create(input: string): TemplateName {
|
|
32
|
+
if (!TemplateName.isValid(input)) {
|
|
33
|
+
throw new InvalidTemplateNameError(input);
|
|
34
|
+
}
|
|
35
|
+
return new TemplateName(input);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static isValid(input: string): boolean {
|
|
39
|
+
if (typeof input !== "string") return false;
|
|
40
|
+
return TEMPLATE_NAME_PATTERN.test(input);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
equals(other: TemplateName): boolean {
|
|
44
|
+
return this.value === other.value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
toString(): string {
|
|
48
|
+
return this.value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
// @work-item-id WI-368
|
|
4
|
+
|
|
5
|
+
import * as fs from "node:fs/promises";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import type { InceptionDocWriterPort } from "../../domain/ports/inception-doc-writer-port.js";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_INCEPTION_DOC_ROOTS,
|
|
10
|
+
type InceptionDocKind,
|
|
11
|
+
type InceptionDocRoots,
|
|
12
|
+
} from "../../domain/value-objects/inception-doc-kind.js";
|
|
13
|
+
|
|
14
|
+
export class FileSystemInceptionDocWriterAdapter implements InceptionDocWriterPort {
|
|
15
|
+
private readonly baseDir: string;
|
|
16
|
+
private readonly roots: InceptionDocRoots;
|
|
17
|
+
|
|
18
|
+
constructor(baseDir: string, roots: InceptionDocRoots = DEFAULT_INCEPTION_DOC_ROOTS) {
|
|
19
|
+
this.baseDir = baseDir;
|
|
20
|
+
this.roots = roots;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
resolvePath(kind: InceptionDocKind): string {
|
|
24
|
+
return path.join(this.baseDir, kind.relativeTargetPath(this.roots));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async exists(kind: InceptionDocKind): Promise<boolean> {
|
|
28
|
+
try {
|
|
29
|
+
await fs.access(this.resolvePath(kind));
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async write(kind: InceptionDocKind, content: string): Promise<string> {
|
|
37
|
+
const targetPath = this.resolvePath(kind);
|
|
38
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
39
|
+
await fs.writeFile(targetPath, content, "utf-8");
|
|
40
|
+
return targetPath;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
// @work-item-id WI-368
|
|
4
|
+
|
|
5
|
+
import * as fs from "node:fs/promises";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import type { InceptionTemplateRepositoryPort } from "../../domain/ports/inception-template-repository-port.js";
|
|
8
|
+
import type { InceptionDocKind } from "../../domain/value-objects/inception-doc-kind.js";
|
|
9
|
+
|
|
10
|
+
export class FileSystemInceptionTemplateRepositoryAdapter implements InceptionTemplateRepositoryPort {
|
|
11
|
+
private readonly templatesDir: string;
|
|
12
|
+
|
|
13
|
+
constructor(harnessRoot: string, subDir: string = "templates") {
|
|
14
|
+
this.templatesDir = path.isAbsolute(subDir) ? subDir : path.join(harnessRoot, subDir);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
resolvePath(kind: InceptionDocKind): string {
|
|
18
|
+
return path.join(this.templatesDir, kind.templateFileName);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async read(kind: InceptionDocKind): Promise<string> {
|
|
22
|
+
const filePath = this.resolvePath(kind);
|
|
23
|
+
try {
|
|
24
|
+
return await fs.readFile(filePath, "utf-8");
|
|
25
|
+
} catch (err) {
|
|
26
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
27
|
+
throw new Error(`テンプレートが見つかりません: ${filePath} (kind=${kind.value})`);
|
|
28
|
+
}
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
// @work-item-id WI-367
|
|
4
|
+
|
|
5
|
+
import * as fs from "node:fs/promises";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import type { TemplateCatalogPort } from "../../domain/ports/template-catalog-port.js";
|
|
8
|
+
import { TemplateCatalogEntry } from "../../domain/value-objects/template-catalog-entry.js";
|
|
9
|
+
import type { TemplateName } from "../../domain/value-objects/template-name.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `harnessRoot/templates` を readdir して catalog を組み立てる。
|
|
13
|
+
*
|
|
14
|
+
* `read()` は catalog エントリの `fileName`(readdir 由来)だけを join する。
|
|
15
|
+
* 引数の `TemplateName` は照合キーとしてのみ使われ、パス構成要素にならないため
|
|
16
|
+
* `../` を含む入力があっても templatesDir の外へは出られない。
|
|
17
|
+
*/
|
|
18
|
+
export class FileSystemTemplateCatalogAdapter implements TemplateCatalogPort {
|
|
19
|
+
private readonly templatesDir: string;
|
|
20
|
+
|
|
21
|
+
constructor(harnessRoot: string, subDir: string = "templates") {
|
|
22
|
+
this.templatesDir = path.isAbsolute(subDir) ? subDir : path.join(harnessRoot, subDir);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
directoryPath(): string {
|
|
26
|
+
return this.templatesDir;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async list(): Promise<readonly TemplateCatalogEntry[]> {
|
|
30
|
+
let dirEntries: import("node:fs").Dirent[];
|
|
31
|
+
try {
|
|
32
|
+
dirEntries = await fs.readdir(this.templatesDir, { withFileTypes: true });
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const entries: TemplateCatalogEntry[] = [];
|
|
38
|
+
for (const dirEntry of dirEntries) {
|
|
39
|
+
if (!dirEntry.isFile()) continue;
|
|
40
|
+
const entry = TemplateCatalogEntry.fromFileName(dirEntry.name);
|
|
41
|
+
if (entry === null) continue;
|
|
42
|
+
entries.push(entry);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// name 昇順(同名は fileName で決定的に)。表示・照合ともに name が主キーであるため。
|
|
46
|
+
entries.sort((a, b) => {
|
|
47
|
+
if (a.name.value !== b.name.value) return a.name.value < b.name.value ? -1 : 1;
|
|
48
|
+
return a.fileName < b.fileName ? -1 : a.fileName > b.fileName ? 1 : 0;
|
|
49
|
+
});
|
|
50
|
+
return entries;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async read(name: TemplateName): Promise<string | null> {
|
|
54
|
+
const entries = await this.list();
|
|
55
|
+
const matched = entries.find((entry) => entry.name.value === name.value);
|
|
56
|
+
if (matched === undefined) return null;
|
|
57
|
+
|
|
58
|
+
return await fs.readFile(path.join(this.templatesDir, matched.fileName), "utf-8");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer presentation
|
|
3
|
+
// @work-item-id WI-368
|
|
4
|
+
|
|
5
|
+
import type { ScaffoldInceptionUseCase } from "../../application/usecases/scaffold-inception-usecase.js";
|
|
6
|
+
import { INCEPTION_DOC_KINDS, InceptionDocKind } from "../../domain/value-objects/inception-doc-kind.js";
|
|
7
|
+
|
|
8
|
+
export interface ScaffoldInceptionHandlerArgs {
|
|
9
|
+
readonly kind?: string;
|
|
10
|
+
readonly dryRun?: boolean;
|
|
11
|
+
readonly apply?: boolean;
|
|
12
|
+
readonly force?: boolean;
|
|
13
|
+
readonly format?: "human" | "json";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ScaffoldInceptionHandlerResult {
|
|
17
|
+
readonly exitCode: number;
|
|
18
|
+
readonly output: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ALLOWED = INCEPTION_DOC_KINDS.join(", ");
|
|
22
|
+
|
|
23
|
+
export class ScaffoldInceptionHandler {
|
|
24
|
+
constructor(private readonly useCase: ScaffoldInceptionUseCase) {}
|
|
25
|
+
|
|
26
|
+
async handle(args: ScaffoldInceptionHandlerArgs): Promise<ScaffoldInceptionHandlerResult> {
|
|
27
|
+
const format = args.format ?? "human";
|
|
28
|
+
const apply = args.apply === true;
|
|
29
|
+
const dryRun = args.dryRun === true || !apply;
|
|
30
|
+
|
|
31
|
+
if (args.dryRun === true && apply) {
|
|
32
|
+
return this.fail(format, "--dry-run と --apply は同時に指定できません", 2);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!args.kind || args.kind.trim().length === 0) {
|
|
36
|
+
return this.fail(format, `--kind <doc-kind> は必須です(許容値: ${ALLOWED})`, 2);
|
|
37
|
+
}
|
|
38
|
+
if (!InceptionDocKind.isValid(args.kind)) {
|
|
39
|
+
return this.fail(format, `未知の doc-kind: "${args.kind}"(許容値: ${ALLOWED})`, 2);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let result: Awaited<ReturnType<ScaffoldInceptionUseCase["execute"]>>;
|
|
43
|
+
try {
|
|
44
|
+
result = await this.useCase.execute({
|
|
45
|
+
kind: args.kind,
|
|
46
|
+
dryRun,
|
|
47
|
+
force: args.force,
|
|
48
|
+
});
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
51
|
+
return this.fail(format, `scaffold 失敗: ${msg}`, 2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (format === "json") {
|
|
55
|
+
return {
|
|
56
|
+
exitCode: result.alreadyExists && !result.written ? 2 : 0,
|
|
57
|
+
output: JSON.stringify(result, null, 2),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (result.alreadyExists && !result.written) {
|
|
62
|
+
return {
|
|
63
|
+
exitCode: result.dryRun ? 0 : 2,
|
|
64
|
+
output: [
|
|
65
|
+
result.dryRun ? `dry-run: 既に存在します: ${result.targetPath}` : `既に存在します: ${result.targetPath}`,
|
|
66
|
+
result.dryRun ? "書き込むには --apply を指定してください。" : "上書きするには --force を指定してください。",
|
|
67
|
+
].join("\n"),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (result.dryRun) {
|
|
72
|
+
return {
|
|
73
|
+
exitCode: 0,
|
|
74
|
+
output: [
|
|
75
|
+
`dry-run: 文書を生成予定: ${result.targetPath}`,
|
|
76
|
+
`テンプレ: ${result.templatePath}`,
|
|
77
|
+
`doc-kind: ${result.kind}`,
|
|
78
|
+
"書き込むには --apply を指定してください。",
|
|
79
|
+
].join("\n"),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const headerVerb = result.overwritten ? "上書き保存しました" : "生成しました";
|
|
84
|
+
return {
|
|
85
|
+
exitCode: 0,
|
|
86
|
+
output: [
|
|
87
|
+
`文書を${headerVerb}: ${result.targetPath}`,
|
|
88
|
+
`テンプレ: ${result.templatePath}`,
|
|
89
|
+
`doc-kind: ${result.kind}`,
|
|
90
|
+
"TODO プレースホルダを実体で埋め、QA の [Answer] は人間が記入してください。",
|
|
91
|
+
].join("\n"),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private fail(format: "human" | "json", message: string, exitCode: number): ScaffoldInceptionHandlerResult {
|
|
96
|
+
if (format === "json") {
|
|
97
|
+
return { exitCode, output: JSON.stringify({ error: message }, null, 2) };
|
|
98
|
+
}
|
|
99
|
+
return { exitCode, output: message };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer presentation
|
|
3
|
+
// @work-item-id WI-367
|
|
4
|
+
|
|
5
|
+
import type { ListTemplatesUseCase } from "../../application/usecases/list-templates-usecase.js";
|
|
6
|
+
import type { ShowTemplateUseCase } from "../../application/usecases/show-template-usecase.js";
|
|
7
|
+
|
|
8
|
+
export interface TemplatesHandlerResult {
|
|
9
|
+
readonly exitCode: number;
|
|
10
|
+
/** stdout に出す内容(空文字なら出力しない) */
|
|
11
|
+
readonly output: string;
|
|
12
|
+
/** stderr に出す内容(空文字なら出力しない) */
|
|
13
|
+
readonly errorOutput: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TemplatesListArgs {
|
|
17
|
+
readonly format?: "human" | "json";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TemplatesShowArgs {
|
|
21
|
+
readonly name?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const USAGE = "Usage: phasegate templates <list|show <name>>";
|
|
25
|
+
|
|
26
|
+
export class TemplatesHandler {
|
|
27
|
+
constructor(
|
|
28
|
+
private readonly listUseCase: ListTemplatesUseCase,
|
|
29
|
+
private readonly showUseCase: ShowTemplateUseCase,
|
|
30
|
+
) {}
|
|
31
|
+
|
|
32
|
+
async list(args: TemplatesListArgs = {}): Promise<TemplatesHandlerResult> {
|
|
33
|
+
const result = await this.listUseCase.execute();
|
|
34
|
+
|
|
35
|
+
if (args.format === "json") {
|
|
36
|
+
return {
|
|
37
|
+
exitCode: 0,
|
|
38
|
+
output: JSON.stringify(result, null, 2),
|
|
39
|
+
errorOutput: "",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (result.templates.length === 0) {
|
|
44
|
+
return {
|
|
45
|
+
exitCode: 0,
|
|
46
|
+
output: [
|
|
47
|
+
"Available templates (0):",
|
|
48
|
+
"",
|
|
49
|
+
` テンプレートディレクトリが見つかりません: ${result.directoryPath}`,
|
|
50
|
+
].join("\n"),
|
|
51
|
+
errorOutput: "",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const lines = [`Available templates (${result.templates.length}):`, ""];
|
|
56
|
+
for (const entry of result.templates) {
|
|
57
|
+
lines.push(` ${entry.name} (${entry.fileName})`);
|
|
58
|
+
}
|
|
59
|
+
lines.push("", "本文を表示: phasegate templates show <name>");
|
|
60
|
+
|
|
61
|
+
return { exitCode: 0, output: lines.join("\n"), errorOutput: "" };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async show(args: TemplatesShowArgs): Promise<TemplatesHandlerResult> {
|
|
65
|
+
const rawName = args.name ?? "";
|
|
66
|
+
if (rawName.length === 0) {
|
|
67
|
+
return {
|
|
68
|
+
exitCode: 2,
|
|
69
|
+
output: "",
|
|
70
|
+
errorOutput: "Usage: phasegate templates show <name>",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const result = await this.showUseCase.execute(rawName);
|
|
75
|
+
|
|
76
|
+
if (result.found) {
|
|
77
|
+
// テンプレート本文だけを stdout に出す。リダイレクトでそのまま文書として保存できる。
|
|
78
|
+
return { exitCode: 0, output: result.content, errorOutput: "" };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (result.reason === "invalid-name") {
|
|
82
|
+
return {
|
|
83
|
+
exitCode: 2,
|
|
84
|
+
output: "",
|
|
85
|
+
errorOutput: [
|
|
86
|
+
`Invalid template name: ${rawName}`,
|
|
87
|
+
"テンプレート名は英小文字・数字・_ ・- のみで指定してください(パスは指定できません)。",
|
|
88
|
+
USAGE,
|
|
89
|
+
].join("\n"),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
exitCode: 2,
|
|
95
|
+
output: "",
|
|
96
|
+
errorOutput: [`Template not found: ${rawName}`, `Available: ${result.availableNames.join(", ")}`].join("\n"),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
usage(): TemplatesHandlerResult {
|
|
101
|
+
return { exitCode: 2, output: "", errorOutput: USAGE };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -99,6 +99,18 @@ export interface HarnessConfigResolvedDocument {
|
|
|
99
99
|
allowedCategories: string[];
|
|
100
100
|
maintainedLayers: string[];
|
|
101
101
|
relaxedGates: string[];
|
|
102
|
+
// WI-377: JSON Schema (v2/v3) は当該キーを許容し preset 解決の deepMerge も
|
|
103
|
+
// 実行時には保持していたが、型宣言だけが欠けていた。preset 定義側は宣言しないため optional。
|
|
104
|
+
fullModeRequiredWhen?: {
|
|
105
|
+
mixedCategories?: boolean;
|
|
106
|
+
newDomainFile?: boolean;
|
|
107
|
+
apiContractChange?: boolean;
|
|
108
|
+
};
|
|
109
|
+
// WI-372: プロジェクト固有パスのカテゴリ割当(quick-mode unit が消費する)。
|
|
110
|
+
// preset には既定を持たせず、source config 側でのみ指定する任意項目。
|
|
111
|
+
// preset 解決の deepMerge は preset 側に無いキーを source からそのまま引き継ぐため、
|
|
112
|
+
// 明示宣言は WI-377 の preset 解決経路を通しても保持される。
|
|
113
|
+
categoryOverrides?: Record<string, string[]>;
|
|
102
114
|
};
|
|
103
115
|
phaseDependencies: {
|
|
104
116
|
preset: PhaseDependenciesPresetId;
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"quickMode": {
|
|
24
24
|
"allowedCategories": ["bugfix", "docs", "test", "config"],
|
|
25
|
-
"maintainedLayers": ["L1", "L2"],
|
|
26
|
-
"relaxedGates": []
|
|
25
|
+
"maintainedLayers": ["L1", "L2-002", "L2-003", "L2-014", "L3-001"],
|
|
26
|
+
"relaxedGates": ["L2-001", "L3-002", "L3-003", "L3-004", "L4"]
|
|
27
27
|
},
|
|
28
28
|
"phaseDependencies": {
|
|
29
29
|
"preset": "default",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"quickMode": {
|
|
24
24
|
"allowedCategories": ["bugfix", "docs", "test", "config"],
|
|
25
|
-
"maintainedLayers": ["L1", "L2"],
|
|
26
|
-
"relaxedGates": []
|
|
25
|
+
"maintainedLayers": ["L1", "L2-002", "L2-003", "L2-014", "L3-001"],
|
|
26
|
+
"relaxedGates": ["L2-001", "L3-002", "L3-003", "L3-004", "L4"]
|
|
27
27
|
},
|
|
28
28
|
"phaseDependencies": {
|
|
29
29
|
"preset": "default",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"quickMode": {
|
|
24
24
|
"allowedCategories": ["bugfix", "docs", "test", "config"],
|
|
25
|
-
"maintainedLayers": ["L1", "L2"],
|
|
26
|
-
"relaxedGates": []
|
|
25
|
+
"maintainedLayers": ["L1", "L2-002", "L2-003", "L2-014", "L3-001"],
|
|
26
|
+
"relaxedGates": ["L2-001", "L3-002", "L3-003", "L3-004", "L4"]
|
|
27
27
|
},
|
|
28
28
|
"phaseDependencies": {
|
|
29
29
|
"preset": "default",
|