phasegate 0.283.0 → 0.335.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 +16 -0
- package/docs/ADR/017-warning-severity-aggregation.md +17 -0
- package/docs/ADR/038-config-state-operation-permission-policy.md +78 -0
- 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/guide/cli-reference.md +89 -7
- package/docs/guide/configuration.md +43 -2
- package/docs/guide/layer-model.md +2 -0
- package/docs/guide/quick-vs-full-mode.md +63 -3
- package/docs/guide/troubleshooting.md +37 -0
- package/docs/templates/agent-context/CLAUDE.md.template.md +6 -6
- package/package.json +2 -2
- package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +5 -1
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +108 -20
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +215 -5
- package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +5 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/file-system-full-mode-session-query-adapter.ts +75 -23
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +18 -3
- package/scripts/harness/agent-integration/infrastructure/adapters/quick-mode-full-mode-requirement-adapter.ts +15 -1
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +57 -9
- 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 +57 -5
- 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/services/claude-md-composer.ts +20 -11
- package/scripts/harness/ci-governance/domain/value-objects/inception-doc-kind.ts +109 -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/check-repetition-handler.ts +10 -2
- 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 +3 -3
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +3 -3
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +3 -3
- 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/services/command-dispatch-service.ts +12 -3
- package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +2 -22
- package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +3 -1
- package/scripts/harness/harness-error/application/dto/create-harness-error-input.ts +3 -1
- package/scripts/harness/harness-error/application/dto/harness-error-contract.ts +3 -1
- package/scripts/harness/harness-error/application/mappers/harness-error-contract-mapper.ts +9 -17
- package/scripts/harness/harness-error/application/usecases/create-harness-error-use-case.ts +7 -7
- package/scripts/harness/harness-error/domain/services/harness-error-factory.ts +30 -33
- package/scripts/harness/harness-error/domain/value-objects/error-definition.ts +28 -17
- package/scripts/harness/harness-error/domain/value-objects/harness-error.ts +30 -9
- package/scripts/harness/harness-error/domain/value-objects/remediation-type.ts +30 -0
- package/scripts/harness/harness-error/infrastructure/registry/l2-error-definitions.ts +44 -29
- package/scripts/harness/harness-error/infrastructure/registry/l3-error-definitions.ts +44 -27
- package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +47 -32
- package/scripts/harness/installation/application/checks/claude-context-missing-check.ts +13 -7
- package/scripts/harness/installation/application/checks/config-status-check.ts +52 -0
- package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +6 -0
- package/scripts/harness/installation/application/ports/config-status-probe-port.ts +9 -0
- package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +30 -8
- package/scripts/harness/installation/application/usecases/run-install.ts +44 -11
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +44 -11
- package/scripts/harness/installation/composition-root.ts +13 -3
- package/scripts/harness/installation/domain/check-id.ts +2 -0
- package/scripts/harness/installation/domain/config-status.ts +17 -0
- package/scripts/harness/installation/domain/ports/heuristic-check.ts +10 -1
- package/scripts/harness/installation/infrastructure/adapters/config-status-probe-adapter.ts +79 -0
- package/scripts/harness/installation/presentation/cli/doctor-handler.ts +6 -1
- package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +19 -5
- package/scripts/harness/integrations/pre-commit.ts +17 -3
- package/scripts/harness/main.ts +159 -21
- 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 +64 -33
- package/scripts/harness/phase2-extensions/presentation/handlers/check-freshness-handler.ts +17 -9
- package/scripts/harness/quick-mode/application/ports/file-existence-port.ts +15 -0
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +60 -22
- package/scripts/harness/quick-mode/composition-root.ts +25 -15
- 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 +116 -18
- 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/fs-file-existence-adapter.ts +38 -0
- package/scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts +83 -32
- package/scripts/harness/skill-quality/infrastructure/adapters/file-system-requirement-test-matrix-adapter.ts +51 -8
- package/scripts/harness/skill-quality/presentation/handlers/check-coverage-handler.ts +13 -6
- package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +5 -1
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-identity-gateway.ts +6 -1
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +15 -2
- package/scripts/harness/validator-system/application/use-cases/aggregate-validation-results-usecase.ts +11 -14
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +8 -0
- package/scripts/harness/validator-system/domain/services/effective-severity-policy.ts +39 -0
- package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +6 -4
- package/scripts/harness/validator-system/domain/value-objects/drift-report.ts +12 -7
- package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +11 -3
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +20 -17
- package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +35 -14
- package/scripts/harness/validator-system/infrastructure/adapters/traceability-metadata-policy-adapter.ts +13 -8
- package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +19 -10
- package/skills/product-architect/SKILL.md +19 -0
- package/skills/quick-implementor/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/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,19 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-368
|
|
4
|
+
|
|
5
|
+
import type { InceptionDocKind } from "../value-objects/inception-doc-kind.js";
|
|
6
|
+
|
|
7
|
+
export interface InceptionDocWriterPort {
|
|
8
|
+
/** 書き込み先の絶対パスを返す(副作用なし) */
|
|
9
|
+
resolvePath(kind: InceptionDocKind): string;
|
|
10
|
+
|
|
11
|
+
/** 書き込み先が既に存在するか */
|
|
12
|
+
exists(kind: InceptionDocKind): Promise<boolean>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* inception / product 文書を書き込む(親ディレクトリが無ければ作成)。
|
|
16
|
+
* 書き込んだ絶対パスを返す。
|
|
17
|
+
*/
|
|
18
|
+
write(kind: InceptionDocKind, content: string): Promise<string>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-368
|
|
4
|
+
|
|
5
|
+
import type { InceptionDocKind } from "../value-objects/inception-doc-kind.js";
|
|
6
|
+
|
|
7
|
+
export interface InceptionTemplateRepositoryPort {
|
|
8
|
+
/** テンプレートの絶対パスを返す(UI 出力用) */
|
|
9
|
+
resolvePath(kind: InceptionDocKind): string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* kind に対応するテンプレートファイルを読み込む。
|
|
13
|
+
* 存在しない場合は例外を投げる。
|
|
14
|
+
*/
|
|
15
|
+
read(kind: InceptionDocKind): Promise<string>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-367
|
|
4
|
+
|
|
5
|
+
import type { TemplateCatalogEntry } from "../value-objects/template-catalog-entry.js";
|
|
6
|
+
import type { TemplateName } from "../value-objects/template-name.js";
|
|
7
|
+
|
|
8
|
+
export interface TemplateCatalogPort {
|
|
9
|
+
/** テンプレートディレクトリの絶対パス(案内表示用) */
|
|
10
|
+
directoryPath(): string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 実在するテンプレートを name 昇順で列挙する。
|
|
14
|
+
* ディレクトリが存在しない場合は空配列を返す(例外にしない)。
|
|
15
|
+
*/
|
|
16
|
+
list(): Promise<readonly TemplateCatalogEntry[]>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* catalog に name 完全一致するテンプレート本文を読む。
|
|
20
|
+
* 一致するエントリが無い場合は null を返す。
|
|
21
|
+
*/
|
|
22
|
+
read(name: TemplateName): Promise<string | null>;
|
|
23
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer domain
|
|
3
3
|
* @unit ci-governance
|
|
4
|
+
* @work-item-id WI-331
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
export interface ClaudeMdTemplateValues {
|
|
@@ -9,20 +10,28 @@ export interface ClaudeMdTemplateValues {
|
|
|
9
10
|
readonly presets: readonly string[];
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
const USER_SECTION_START =
|
|
13
|
-
const USER_SECTION_END =
|
|
14
|
-
const DEFAULT_USER_SECTION =
|
|
13
|
+
const USER_SECTION_START = "<!-- phasegate:user-section:start -->";
|
|
14
|
+
const USER_SECTION_END = "<!-- phasegate:user-section:end -->";
|
|
15
|
+
const DEFAULT_USER_SECTION = "Project-specific agent instructions go here.";
|
|
15
16
|
|
|
16
17
|
export class ClaudeMdComposer {
|
|
18
|
+
// Rebuilds CLAUDE.md from the current template, carrying over the existing
|
|
19
|
+
// user-section body. Because the whole file is re-rendered, a legacy file
|
|
20
|
+
// whose user-section still sits inside the managed block (pre-WI-331
|
|
21
|
+
// template shape) is migrated to the current outside-of-block structure.
|
|
17
22
|
compose(template: string, existing: string | null, values: ClaudeMdTemplateValues): string {
|
|
18
23
|
const userSection = this.extractUserSection(existing) ?? DEFAULT_USER_SECTION;
|
|
19
|
-
|
|
20
|
-
.replace(
|
|
21
|
-
.replace(
|
|
22
|
-
.replace(
|
|
23
|
-
|
|
24
|
-
.
|
|
25
|
-
.
|
|
24
|
+
const rendered = template
|
|
25
|
+
.replace("{{PHASEGATE_COMMANDS}}", this.toList(values.commands))
|
|
26
|
+
.replace("{{PHASEGATE_SKILLS}}", this.toList(values.skills))
|
|
27
|
+
.replace("{{PHASEGATE_PRESETS}}", this.toList(values.presets))
|
|
28
|
+
// Collapse blank-line runs before injecting the user body so
|
|
29
|
+
// user-authored spacing is preserved byte-for-byte.
|
|
30
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
31
|
+
// Replacer function keeps `$`-sequences in the user body from being
|
|
32
|
+
// interpreted as String.replace substitution patterns.
|
|
33
|
+
.replace("{{PHASEGATE_USER_SECTION}}", () => userSection);
|
|
34
|
+
return `${rendered.trimEnd()}\n`;
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
private extractUserSection(existing: string | null): string | null {
|
|
@@ -34,6 +43,6 @@ export class ClaudeMdComposer {
|
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
private toList(values: readonly string[]): string {
|
|
37
|
-
return values.map((value) => `- \`${value}\``).join(
|
|
46
|
+
return values.map((value) => `- \`${value}\``).join("\n");
|
|
38
47
|
}
|
|
39
48
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -29,6 +29,13 @@ export class CheckRepetitionHandler {
|
|
|
29
29
|
async handle(args: CheckRepetitionHandlerArgs): Promise<CheckRepetitionHandlerResult> {
|
|
30
30
|
const { errorCode, reset = false, format = 'human' } = args;
|
|
31
31
|
|
|
32
|
+
if (errorCode.trim().length === 0) {
|
|
33
|
+
return {
|
|
34
|
+
exitCode: 1,
|
|
35
|
+
output: 'Error: --code <errorCode> is required. Usage: phasegate ci:check-repetition --code <errorCode> [--reset] [--json]',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
if (reset && this.resetUseCase) {
|
|
33
40
|
const result = await this.resetUseCase.execute({ errorCode, confirmedResolution: true });
|
|
34
41
|
|
|
@@ -49,9 +56,10 @@ export class CheckRepetitionHandler {
|
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
const result = await this.checkUseCase.execute({ errorCode });
|
|
59
|
+
const exitCode = result.escalated === true ? 1 : 0;
|
|
52
60
|
|
|
53
61
|
if (format === 'json') {
|
|
54
|
-
return { exitCode
|
|
62
|
+
return { exitCode, output: JSON.stringify(result, null, 2) };
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
const lines: string[] = [];
|
|
@@ -63,6 +71,6 @@ export class CheckRepetitionHandler {
|
|
|
63
71
|
lines.push(` Escalated: ${result.escalated}`);
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
return { exitCode
|
|
74
|
+
return { exitCode, output: lines.join('\n') };
|
|
67
75
|
}
|
|
68
76
|
}
|
|
@@ -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
|
+
}
|