phasegate 0.315.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.
Files changed (56) hide show
  1. package/docs/ADR/039-hook-observable-state-as-authorization-unit.md +74 -0
  2. package/docs/ADR/040-quick-mode-config-via-preset-resolution.md +66 -0
  3. package/docs/guide/cli-reference.md +89 -7
  4. package/docs/guide/configuration.md +43 -2
  5. package/docs/guide/quick-vs-full-mode.md +36 -1
  6. package/package.json +2 -2
  7. package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +5 -1
  8. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +16 -8
  9. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +190 -2
  10. package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +5 -0
  11. package/scripts/harness/agent-integration/infrastructure/adapters/quick-mode-full-mode-requirement-adapter.ts +15 -1
  12. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +6 -3
  13. package/scripts/harness/ci-governance/application/dto/scaffold-inception-input.ts +9 -0
  14. package/scripts/harness/ci-governance/application/dto/scaffold-inception-output.ts +13 -0
  15. package/scripts/harness/ci-governance/application/usecases/list-templates-usecase.ts +30 -0
  16. package/scripts/harness/ci-governance/application/usecases/scaffold-inception-usecase.ts +63 -0
  17. package/scripts/harness/ci-governance/application/usecases/show-template-usecase.ts +39 -0
  18. package/scripts/harness/ci-governance/composition-root.ts +55 -3
  19. package/scripts/harness/ci-governance/domain/ports/inception-doc-writer-port.ts +19 -0
  20. package/scripts/harness/ci-governance/domain/ports/inception-template-repository-port.ts +16 -0
  21. package/scripts/harness/ci-governance/domain/ports/template-catalog-port.ts +23 -0
  22. package/scripts/harness/ci-governance/domain/value-objects/inception-doc-kind.ts +109 -0
  23. package/scripts/harness/ci-governance/domain/value-objects/template-catalog-entry.ts +53 -0
  24. package/scripts/harness/ci-governance/domain/value-objects/template-name.ts +50 -0
  25. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-doc-writer-adapter.ts +42 -0
  26. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-template-repository-adapter.ts +32 -0
  27. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-template-catalog-adapter.ts +60 -0
  28. package/scripts/harness/ci-governance/presentation/handlers/scaffold-inception-handler.ts +101 -0
  29. package/scripts/harness/ci-governance/presentation/handlers/templates-handler.ts +103 -0
  30. package/scripts/harness/config-foundation/domain/harness-config.ts +12 -0
  31. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +2 -2
  32. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +2 -2
  33. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +2 -2
  34. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +44 -1
  35. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +42 -1
  36. package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +3 -1
  37. package/scripts/harness/main.ts +102 -12
  38. package/scripts/harness/phase-dependency-model/application/services/evidence-bundle-assembler.ts +2 -1
  39. package/scripts/harness/phase-dependency-model/domain/ports/plan-document-reader-port.ts +12 -0
  40. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +4 -1
  41. package/scripts/harness/quick-mode/domain/errors/quick-mode-config-error.ts +19 -0
  42. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +46 -17
  43. package/scripts/harness/quick-mode/domain/value-objects/category-override-rules.ts +147 -0
  44. package/scripts/harness/quick-mode/domain/value-objects/change-category.ts +33 -0
  45. package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +31 -10
  46. package/scripts/harness/quick-mode/index.ts +7 -1
  47. package/scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts +83 -32
  48. package/skills/product-architect/SKILL.md +19 -0
  49. package/skills/story-mapper/SKILL.md +11 -0
  50. package/skills/story-writer/SKILL.md +11 -0
  51. package/skills/unit-designer/SKILL.md +11 -0
  52. package/templates/product_overview.template.md +85 -0
  53. package/templates/product_overview_plan.template.md +55 -0
  54. package/templates/story_mapping_plan.template.md +61 -0
  55. package/templates/story_writer_plan.template.md +61 -0
  56. package/templates/unit_design_plan.template.md +63 -0
@@ -13,6 +13,18 @@
13
13
  "decisionSignal": {
14
14
  "type": "string",
15
15
  "enum": ["business-rule-branch", "validation-rule", "error-construction", "state-transition", "policy-selection"]
16
+ },
17
+ "changeCategory": {
18
+ "type": "string",
19
+ "enum": ["bugfix", "docs", "test", "config", "feature", "domain", "api"]
20
+ },
21
+ "categoryOverrideGlobs": {
22
+ "type": "array",
23
+ "items": {
24
+ "type": "string",
25
+ "minLength": 1
26
+ },
27
+ "uniqueItems": true
16
28
  }
17
29
  },
18
30
  "properties": {
@@ -170,9 +182,10 @@
170
182
  "additionalProperties": false,
171
183
  "properties": {
172
184
  "allowedCategories": {
185
+ "description": "Quick Mode を通過させる ChangeCategory。7 値の enum で、未知値は設定エラーとして拒否される。",
173
186
  "type": "array",
174
187
  "items": {
175
- "type": "string"
188
+ "$ref": "#/$defs/changeCategory"
176
189
  },
177
190
  "uniqueItems": true
178
191
  },
@@ -204,6 +217,34 @@
204
217
  "type": "boolean"
205
218
  }
206
219
  }
220
+ },
221
+ "categoryOverrides": {
222
+ "description": "プロジェクト固有パスを任意の ChangeCategory へ割り当てる glob ルール。組み込み分類より先に評価されるが、組み込みが domain / api のファイルは降格できない。",
223
+ "type": "object",
224
+ "additionalProperties": false,
225
+ "properties": {
226
+ "bugfix": {
227
+ "$ref": "#/$defs/categoryOverrideGlobs"
228
+ },
229
+ "docs": {
230
+ "$ref": "#/$defs/categoryOverrideGlobs"
231
+ },
232
+ "test": {
233
+ "$ref": "#/$defs/categoryOverrideGlobs"
234
+ },
235
+ "config": {
236
+ "$ref": "#/$defs/categoryOverrideGlobs"
237
+ },
238
+ "feature": {
239
+ "$ref": "#/$defs/categoryOverrideGlobs"
240
+ },
241
+ "domain": {
242
+ "$ref": "#/$defs/categoryOverrideGlobs"
243
+ },
244
+ "api": {
245
+ "$ref": "#/$defs/categoryOverrideGlobs"
246
+ }
247
+ }
207
248
  }
208
249
  }
209
250
  },
@@ -1,6 +1,6 @@
1
1
  // @unit harness-api
2
2
  // @layer domain
3
- // @work-item-id WI-250, WI-291, WI-296
3
+ // @work-item-id WI-250, WI-291, WI-296, WI-367, WI-368
4
4
  // known-harness-commands.ts — CLI 実サーフェスの canonical 既知コマンド一覧
5
5
  //
6
6
  // main.ts の CLI dispatch(`switch (command)`)が受理する全トップレベルコマンド名の
@@ -70,6 +70,7 @@ export const KNOWN_HARNESS_COMMANDS: readonly string[] = Object.freeze([
70
70
  "regression:run-k14-k15",
71
71
  "render-errors",
72
72
  "scaffold-design",
73
+ "scaffold-inception",
73
74
  "scaffold-wi",
74
75
  "session",
75
76
  "setup:agent",
@@ -80,6 +81,7 @@ export const KNOWN_HARNESS_COMMANDS: readonly string[] = Object.freeze([
80
81
  "skill:validate-structure",
81
82
  "skills",
82
83
  "status",
84
+ "templates",
83
85
  "uninstall",
84
86
  "update-skills",
85
87
  "validate",
@@ -45,7 +45,7 @@ import { basename, dirname, join, resolve } from "node:path";
45
45
  import { fileURLToPath } from "node:url";
46
46
  import { createAdrFoundationModule } from "./adr-foundation/composition-root.js";
47
47
  import { createBiomeAstEngineModule } from "./biome-ast-engine/composition-root.js";
48
- import { buildCiGovernance } from "./ci-governance/composition-root.js";
48
+ import { buildCiGovernance, type CiGovernanceDocPaths } from "./ci-governance/composition-root.js";
49
49
  import { toPhaseConfigSection } from "./config-foundation/application/mappers/phase-config-section-mapper.js";
50
50
  import { toValidatorSystemConfig } from "./config-foundation/application/mappers/validator-system-config-mapper.js";
51
51
  import { toWorldModelConfig } from "./config-foundation/application/mappers/world-model-config-mapper.js";
@@ -120,6 +120,19 @@ function toTraceabilityModelOptions(resolvedConfig: HarnessConfigV2 | undefined)
120
120
  return resolvedConfig ? { pathRoots: { designDocsRoot: resolvedConfig.paths.designDocs } } : undefined;
121
121
  }
122
122
 
123
+ /**
124
+ * WI-369: ci-governance の scaffold 系アダプタに解決済み paths を渡す。
125
+ * これが無いと `scaffold-design` の書き込み先が `docs/product/construction` に
126
+ * 固定され、`paths.designDocs` を移設した PJ で scaffold 先とゲート検査先がズレる。
127
+ */
128
+ function toCiGovernanceDocPaths(resolvedConfig: HarnessConfigV2 | undefined): CiGovernanceDocPaths {
129
+ if (!resolvedConfig) return {};
130
+ return {
131
+ designDocs: resolvedConfig.paths.designDocs,
132
+ inceptionDocs: resolvedConfig.paths.inceptionDocs,
133
+ };
134
+ }
135
+
123
136
  interface PackageJsonDocument {
124
137
  readonly [key: string]: unknown;
125
138
  readonly dependencies?: unknown;
@@ -263,6 +276,7 @@ Gate semantics:
263
276
  ci:check-repetition Check error repetition (--code <errorCode>, --reset, --json)
264
277
  baseline Create retrofit baseline snapshot (--dry-run, --force, --paths <glob,glob,...>, --json)
265
278
  scaffold-design Scaffold a design doc (--unit <id>, --phase <logical|domain|uiux|unit-test|it-test>, --dry-run|--apply, --force, --json)
279
+ scaffold-inception Scaffold an inception/product doc (--kind <product-overview-plan|product-overview|story-writer-plan|story-mapping-plan|unit-design-plan>, --dry-run|--apply, --force, --json)
266
280
  session begin Start a Full Mode session (--mode full, --unit, --work-item, --reason, --duration)
267
281
  session end End a Full Mode session (--work-item)
268
282
 
@@ -294,6 +308,10 @@ Skills:
294
308
  skills list List all available skills
295
309
  skills info <name> Show skill details (SKILL.md)
296
310
 
311
+ Templates:
312
+ templates list List bundled document templates
313
+ templates show <name> Print a bundled template to stdout
314
+
297
315
  Options:
298
316
  --help Show this help message
299
317
  --version Show version number
@@ -900,6 +918,33 @@ Options:
900
918
  --force Overwrite an existing target when applying.
901
919
  --json Output machine-readable JSON.
902
920
  --help, -h Show this help`,
921
+ "scaffold-inception": `Usage: phasegate scaffold-inception --kind <doc-kind> [options]
922
+
923
+ Scaffold an inception plan document or the product overview.
924
+ Targets follow paths.inceptionDocs / paths.designDocs from phasegate.config.json.
925
+
926
+ Options:
927
+ --kind <doc-kind> product-overview-plan, product-overview, story-writer-plan,
928
+ story-mapping-plan, or unit-design-plan.
929
+ --dry-run Preview target and template without writing (default).
930
+ --apply Write the scaffold.
931
+ --force Overwrite an existing target when applying.
932
+ --json Output machine-readable JSON.
933
+ --help, -h Show this help`,
934
+ templates: `Usage: phasegate templates <list|show <name>>
935
+
936
+ Print bundled document templates to stdout, without reading node_modules.
937
+
938
+ Subcommands:
939
+ list List available template names.
940
+ show <name> Print the template body to stdout.
941
+
942
+ Options:
943
+ --json Output machine-readable JSON (list only).
944
+ --help, -h Show this help
945
+
946
+ Notes:
947
+ <name> is matched against the bundled template catalog. Paths are rejected.`,
903
948
  session: `Usage: phasegate session <begin|end> [options]
904
949
 
905
950
  Manage hook-visible Full Mode sessions.
@@ -2965,7 +3010,10 @@ async function main(): Promise<void> {
2965
3010
  case "ci-check": {
2966
3011
  const quick = hasFlag(args, "--quick");
2967
3012
  if (quick) {
2968
- const mod = createQuickModeCompositionRoot();
3013
+ // WI-364: check-change-category (WI-351) と同様に解決済み configPath / rootDir を注入する。
3014
+ // 無指定だと quickMode 設定を cwd 基準でしか探せず、サブディレクトリ実行で
3015
+ // HarnessConfigNotFoundError → exit 2 になっていた。
3016
+ const mod = createQuickModeCompositionRoot(await resolveQuickModeCompositionOptions());
2969
3017
  const failOnReject = hasFlag(args, "--fail-on-reject");
2970
3018
  const dryRun = hasFlag(args, "--dry-run");
2971
3019
  const files = parseFlag(args, "--files");
@@ -3241,7 +3289,7 @@ Examples:
3241
3289
  console.error(flagError);
3242
3290
  process.exit(2);
3243
3291
  }
3244
- const mod = buildCiGovernance(rootDir, harnessRoot);
3292
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3245
3293
  const presetId = parseFlag(args, "--preset") ?? "standard";
3246
3294
  const templateType = parseFlag(args, "--type") ?? "aidlc-gate";
3247
3295
  const render = hasFlag(args, "--render");
@@ -3253,7 +3301,7 @@ Examples:
3253
3301
  }
3254
3302
 
3255
3303
  case "ci:migrate-agents-md": {
3256
- const mod = buildCiGovernance(rootDir, harnessRoot);
3304
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3257
3305
  const dryRun = hasFlag(args, "--dry-run");
3258
3306
  const validateOnly = hasFlag(args, "--validate-only");
3259
3307
  const format = json ? "json" : "human";
@@ -3264,7 +3312,7 @@ Examples:
3264
3312
  }
3265
3313
 
3266
3314
  case "ci:auto-refresh-agent-context": {
3267
- const mod = buildCiGovernance(rootDir, harnessRoot);
3315
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3268
3316
  const dryRun = hasFlag(args, "--dry-run");
3269
3317
  const apply = hasFlag(args, "--apply");
3270
3318
  const format = json ? "json" : "human";
@@ -3275,7 +3323,7 @@ Examples:
3275
3323
  }
3276
3324
 
3277
3325
  case "refresh-claude-md": {
3278
- const mod = buildCiGovernance(rootDir, harnessRoot);
3326
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3279
3327
  const dryRun = hasFlag(args, "--dry-run");
3280
3328
  const apply = hasFlag(args, "--apply");
3281
3329
  const format = json ? "json" : "human";
@@ -3286,7 +3334,7 @@ Examples:
3286
3334
  }
3287
3335
 
3288
3336
  case "p2:check-agent-context": {
3289
- const mod = buildCiGovernance(rootDir, harnessRoot);
3337
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3290
3338
  const thresholdRaw = parseFlag(args, "--threshold-days");
3291
3339
  const thresholdDays = thresholdRaw === undefined ? undefined : Number(thresholdRaw);
3292
3340
  const format = json ? "json" : "human";
@@ -3297,7 +3345,7 @@ Examples:
3297
3345
  }
3298
3346
 
3299
3347
  case "ci:check-repetition": {
3300
- const mod = buildCiGovernance(rootDir, harnessRoot);
3348
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3301
3349
  const errorCode = parseFlag(args, "--code") ?? "";
3302
3350
  const reset = hasFlag(args, "--reset");
3303
3351
  const format = json ? "json" : "human";
@@ -3308,7 +3356,7 @@ Examples:
3308
3356
  }
3309
3357
 
3310
3358
  case "baseline": {
3311
- const mod = buildCiGovernance(rootDir, harnessRoot);
3359
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3312
3360
  const dryRun = hasFlag(args, "--dry-run");
3313
3361
  const force = hasFlag(args, "--force");
3314
3362
  const pathsFlag = parseFlag(args, "--paths");
@@ -3331,7 +3379,7 @@ Examples:
3331
3379
  }
3332
3380
 
3333
3381
  case "scaffold-design": {
3334
- const mod = buildCiGovernance(rootDir, harnessRoot);
3382
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3335
3383
  const unit = parseFlag(args, "--unit") ?? "";
3336
3384
  const phase = parseFlag(args, "--phase") ?? "";
3337
3385
  const dryRun = hasFlag(args, "--dry-run");
@@ -3351,8 +3399,50 @@ Examples:
3351
3399
  return;
3352
3400
  }
3353
3401
 
3402
+ case "scaffold-inception": {
3403
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3404
+ const kind = parseFlag(args, "--kind") ?? "";
3405
+ const dryRun = hasFlag(args, "--dry-run");
3406
+ const apply = hasFlag(args, "--apply");
3407
+ const force = hasFlag(args, "--force");
3408
+ const format = json ? "json" : "human";
3409
+ const result = await mod.scaffoldInceptionHandler.handle({
3410
+ kind,
3411
+ dryRun,
3412
+ apply,
3413
+ force,
3414
+ format,
3415
+ });
3416
+ console.log(result.output);
3417
+ await finishCliExit(result.exitCode);
3418
+ return;
3419
+ }
3420
+
3421
+ case "templates": {
3422
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3423
+ const subCommand = args[1];
3424
+ const emit = async (result: { exitCode: number; output: string; errorOutput: string }): Promise<void> => {
3425
+ if (result.output.length > 0) console.log(result.output);
3426
+ if (result.errorOutput.length > 0) console.error(result.errorOutput);
3427
+ await finishCliExit(result.exitCode);
3428
+ };
3429
+
3430
+ if (subCommand === "list") {
3431
+ await emit(await mod.templatesHandler.list({ format: json ? "json" : "human" }));
3432
+ return;
3433
+ }
3434
+
3435
+ if (subCommand === "show") {
3436
+ await emit(await mod.templatesHandler.show({ name: args[2] }));
3437
+ return;
3438
+ }
3439
+
3440
+ await emit(mod.templatesHandler.usage());
3441
+ return;
3442
+ }
3443
+
3354
3444
  case "integrity:pin": {
3355
- const mod = buildCiGovernance(rootDir, harnessRoot);
3445
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3356
3446
  const dryRun = hasFlag(args, "--dry-run");
3357
3447
  const format = json ? "json" : "human";
3358
3448
  const result = await mod.integrityHandler.pin({ dryRun, format });
@@ -3362,7 +3452,7 @@ Examples:
3362
3452
  }
3363
3453
 
3364
3454
  case "integrity:verify": {
3365
- const mod = buildCiGovernance(rootDir, harnessRoot);
3455
+ const mod = buildCiGovernance(rootDir, harnessRoot, toCiGovernanceDocPaths(resolvedConfig));
3366
3456
  const format = json ? "json" : "human";
3367
3457
  const result = await mod.integrityHandler.verify({ format });
3368
3458
  console.log(result.output);
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer application
3
3
  * @unit phase-dependency-model
4
+ * @work-item-id WI-369
4
5
  */
5
6
 
6
7
  import type { ArtifactExistenceCheckerPort } from '../../domain/ports/artifact-existence-checker-port.js';
@@ -68,7 +69,7 @@ export class EvidenceBundleAssembler {
68
69
  continue;
69
70
  }
70
71
 
71
- const actual = await this.planDocumentReader.readEvidence(node, scope, planningMode);
72
+ const actual = await this.planDocumentReader.readEvidence(node, scope, planningMode, pathRoots);
72
73
  planEvidences.set(node.nodeKey(), actual);
73
74
  }
74
75
 
@@ -1,16 +1,28 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit phase-dependency-model
4
+ * @work-item-id WI-369
4
5
  */
5
6
 
7
+ import type { PathRoots } from '../values/artifact.js';
6
8
  import type { PhaseNode } from '../values/phase-node.js';
7
9
  import type { PlanEvidence } from '../values/plan-evidence.js';
8
10
  import type { PlanningMode } from '../values/planning-mode.js';
9
11
 
10
12
  export interface PlanDocumentReaderPort {
13
+ /**
14
+ * plan 文書の存在と QA evidence を読む。
15
+ *
16
+ * `pathRoots` は `phasegate.config.json` の `paths` を解決したもの。
17
+ * 省略時は既定ルート(`docs/inception` / `docs/product/construction`)を使う。
18
+ * WI-369: これを渡さないと `paths.inceptionDocs` を移設した PJ で
19
+ * plan 文書だけが既定パスから探され、成果物検査と plan evidence の
20
+ * 探索先がズレる(成果物は在るのに「plan文書が不足」で落ちる)。
21
+ */
11
22
  readEvidence(
12
23
  node: PhaseNode,
13
24
  scope: { unitId?: string; storyId?: string },
14
25
  expectedMode: PlanningMode,
26
+ pathRoots?: PathRoots,
15
27
  ): Promise<PlanEvidence>;
16
28
  }
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * @layer infrastructure
3
3
  * @unit phase-dependency-model
4
+ * @work-item-id WI-369
4
5
  */
5
6
 
6
7
  import { readFile } from "node:fs/promises";
7
8
  import * as path from "node:path";
8
9
  import type { PlanDocumentReaderPort } from "../../domain/ports/plan-document-reader-port.js";
10
+ import { DEFAULT_PATH_ROOTS, type PathRoots } from "../../domain/values/artifact.js";
9
11
  import type { PhaseNode } from "../../domain/values/phase-node.js";
10
12
  import { PlanEvidence } from "../../domain/values/plan-evidence.js";
11
13
  import type { PlanningMode } from "../../domain/values/planning-mode.js";
@@ -41,6 +43,7 @@ export class MarkdownPlanDocumentReader implements PlanDocumentReaderPort {
41
43
  node: PhaseNode,
42
44
  scope: { unitId?: string; storyId?: string },
43
45
  expectedMode: PlanningMode,
46
+ pathRoots: PathRoots = DEFAULT_PATH_ROOTS,
44
47
  ): Promise<PlanEvidence> {
45
48
  const planArtifacts = node.planArtifacts();
46
49
 
@@ -55,7 +58,7 @@ export class MarkdownPlanDocumentReader implements PlanDocumentReaderPort {
55
58
  const artifact = planArtifacts[0];
56
59
  let resolvedPath: string;
57
60
  try {
58
- resolvedPath = artifact.resolve(scope);
61
+ resolvedPath = artifact.resolve(scope, pathRoots);
59
62
  } catch {
60
63
  return PlanEvidence.create({
61
64
  exists: false,
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @layer domain
3
+ * @unit quick-mode
4
+ * @work-item-id WI-372
5
+ *
6
+ * QuickMode 設定の不変条件違反を表すエラー。
7
+ *
8
+ * `quick-mode-config.ts` と `category-override-rules.ts` の双方から参照されるため、
9
+ * 循環 import を避ける目的で独立ファイルに切り出している。
10
+ * 後方互換のため `quick-mode-config.ts` からも re-export される。
11
+ */
12
+
13
+ export class QuickModeConfigError extends Error {
14
+ constructor(message: string) {
15
+ super(message);
16
+ this.name = 'QuickModeConfigError';
17
+ Object.setPrototypeOf(this, new.target.prototype);
18
+ }
19
+ }
@@ -4,10 +4,12 @@
4
4
  * @work-item-id WI-204
5
5
  * @work-item-id WI-349
6
6
  * @work-item-id WI-352
7
+ * @work-item-id WI-372
7
8
  *
8
9
  * ChangedFile[]をChangeClassificationに変換し、3拒否ルールを評価してQuickModeEligibilityを返すドメインサービス
9
10
  */
10
11
 
12
+ import { CategoryOverrideRules } from "../value-objects/category-override-rules.js";
11
13
  import { ChangeCategory } from "../value-objects/change-category.js";
12
14
  import { ChangeClassification } from "../value-objects/change-classification.js";
13
15
  import type { ChangedFile } from "../value-objects/changed-file.js";
@@ -15,17 +17,6 @@ import type { QuickModeConfig } from "../value-objects/quick-mode-config.js";
15
17
  import { QuickModeEligibility } from "../value-objects/quick-mode-eligibility.js";
16
18
  import { isCommentOnlyDiff } from "./comment-only-diff-detector.js";
17
19
 
18
- // リスク順優先度(api > domain > feature > bugfix > test > config > docs)
19
- const RISK_PRIORITY: Record<string, number> = {
20
- api: 6,
21
- domain: 5,
22
- feature: 4,
23
- bugfix: 3,
24
- test: 2,
25
- config: 1,
26
- docs: 0,
27
- };
28
-
29
20
  // config: リポジトリ直下の bootstrap 設定ファイル(列挙型 allowlist)。
30
21
  // これらは unit を持たず、CREATE だと feature に落ちて allowedCategories に
31
22
  // 入れる手段がないため greenfield な初期セットアップが恒久的にブロックされていた。
@@ -57,7 +48,7 @@ function isRootBootstrapConfigFile(filePath: string): boolean {
57
48
  return filePath.startsWith(HUSKY_DIRECTORY_PREFIX) && filePath.slice(HUSKY_DIRECTORY_PREFIX.length).length > 0;
58
49
  }
59
50
 
60
- function categorizeFile(file: ChangedFile): ChangeCategory {
51
+ function categorizeFileByBuiltInRules(file: ChangedFile): ChangeCategory {
61
52
  const { filePath, changeKind } = file;
62
53
 
63
54
  // Config files must stay config even when Edit payload snippets look like
@@ -130,6 +121,43 @@ function categorizeFile(file: ChangedFile): ChangeCategory {
130
121
  return ChangeCategory.fromString("bugfix");
131
122
  }
132
123
 
124
+ // 組み込み判定が構造的に高リスク(domain / api)のカテゴリは override で降格できない。
125
+ // @work-item-id WI-372
126
+ const NON_DOWNGRADABLE_BUILT_IN_CATEGORIES: ReadonlySet<string> = new Set(["domain", "api"]);
127
+
128
+ /**
129
+ * WI-372: `quickMode.categoryOverrides` を反映したカテゴリ判定。
130
+ *
131
+ * override は「このパスはこの種類の変更である」という利用者の明示宣言なので、
132
+ * 組み込みルールより **先** に評価する(DD-1)。組み込みの後段に置くと
133
+ * `notes/x.config.json` のような偶発的な組み込みマッチに常に負け、
134
+ * 「設定したのに効かない」不可解な挙動になるため。
135
+ *
136
+ * ただし組み込み判定が `domain` / `api` のファイルは override で降格できない(DD-2)。
137
+ * `judge()` の NEW_DOMAIN は CREATE のみ、API_CONTRACT は port/adapter のみを見るため、
138
+ * ガードが無いと domain ファイルの MODIFY が `docs` として素通りしてしまう。
139
+ * 降格は禁じるが昇格(domain → api 等)は許す。
140
+ *
141
+ * override 未設定時は組み込み分類と完全に一致する(後方互換)。
142
+ */
143
+ function categorizeFile(file: ChangedFile, overrides: CategoryOverrideRules): ChangeCategory {
144
+ const builtIn = categorizeFileByBuiltInRules(file);
145
+ if (overrides.isEmpty()) {
146
+ return builtIn;
147
+ }
148
+
149
+ const override = overrides.resolve(file.filePath);
150
+ if (override === null) {
151
+ return builtIn;
152
+ }
153
+
154
+ if (NON_DOWNGRADABLE_BUILT_IN_CATEGORIES.has(builtIn.toString())) {
155
+ return override.riskPriority() > builtIn.riskPriority() ? override : builtIn;
156
+ }
157
+
158
+ return override;
159
+ }
160
+
133
161
  /**
134
162
  * WI-349: 遮断理由に判定根拠(分類カテゴリと変更種別)を含める。
135
163
  *
@@ -142,17 +170,18 @@ function describeChangedFile(file: ChangedFile, category: string): string {
142
170
  }
143
171
 
144
172
  export class QuickModeJudgmentEngine {
145
- classify(changedFiles: readonly ChangedFile[], _config?: QuickModeConfig): ChangeClassification {
173
+ classify(changedFiles: readonly ChangedFile[], config?: QuickModeConfig): ChangeClassification {
146
174
  if (changedFiles.length === 0) {
147
175
  return new ChangeClassification(null, new Map(), 0);
148
176
  }
149
177
 
178
+ const overrides = config?.categoryOverrides ?? CategoryOverrideRules.empty();
150
179
  const categorizedMap = new Map<string, ChangedFile[]>();
151
180
  let dominantCategory: ChangeCategory | null = null;
152
181
  let dominantPriority = -1;
153
182
 
154
183
  for (const file of changedFiles) {
155
- const category = categorizeFile(file);
184
+ const category = categorizeFile(file, overrides);
156
185
  const key = category.toString();
157
186
 
158
187
  if (!categorizedMap.has(key)) {
@@ -160,7 +189,7 @@ export class QuickModeJudgmentEngine {
160
189
  }
161
190
  categorizedMap.get(key)!.push(file);
162
191
 
163
- const priority = RISK_PRIORITY[key] ?? 0;
192
+ const priority = category.riskPriority();
164
193
  if (priority > dominantPriority) {
165
194
  dominantPriority = priority;
166
195
  dominantCategory = category;
@@ -206,7 +235,7 @@ export class QuickModeJudgmentEngine {
206
235
  "NEW_DOMAIN",
207
236
  newDomainFiles,
208
237
  `domain/配下に新規ファイルが追加されています: ${newDomainFiles
209
- .map((f) => describeChangedFile(f, categorizeFile(f).toString()))
238
+ .map((f) => describeChangedFile(f, categorizeFile(f, config.categoryOverrides).toString()))
210
239
  .join(", ")}`,
211
240
  );
212
241
  }
@@ -223,7 +252,7 @@ export class QuickModeJudgmentEngine {
223
252
  "API_CONTRACT",
224
253
  apiContractFiles,
225
254
  `Port/Adapterインターフェースファイルの変更が含まれています: ${apiContractFiles
226
- .map((f) => describeChangedFile(f, categorizeFile(f).toString()))
255
+ .map((f) => describeChangedFile(f, categorizeFile(f, config.categoryOverrides).toString()))
227
256
  .join(", ")}`,
228
257
  );
229
258
  }