phasegate 0.44.0 → 0.63.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/README.ja.md +32 -0
- package/README.md +33 -0
- package/docs/guide/codex-integration.md +162 -0
- package/docs/guide/quick-vs-full-mode.md +141 -0
- package/package.json +1 -1
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +60 -0
- package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +299 -0
- package/scripts/harness/agent-integration/presentation/session-start-hook.ts +54 -0
- package/scripts/harness/agent-integration/presentation/user-prompt-submit-hook.ts +70 -0
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +15 -0
- package/scripts/harness/integrations/pre-commit.ts +128 -28
- package/scripts/harness/main.ts +87 -9
- package/scripts/harness/phase2-extensions/application/dto/check-initial-creation-expiration-input.ts +9 -0
- package/scripts/harness/phase2-extensions/application/dto/check-initial-creation-expiration-output.ts +17 -0
- package/scripts/harness/phase2-extensions/application/usecases/check-initial-creation-expiration-usecase.ts +103 -0
- package/scripts/harness/phase2-extensions/composition-root.ts +22 -0
- package/scripts/harness/phase2-extensions/domain/aggregates/initial-creation-expiration-rule.ts +103 -0
- package/scripts/harness/phase2-extensions/domain/ports/frontmatter-reader-port.ts +17 -0
- package/scripts/harness/phase2-extensions/domain/ports/initial-creation-age-port.ts +9 -0
- package/scripts/harness/phase2-extensions/domain/ports/initial-creation-expiration-config-port.ts +9 -0
- package/scripts/harness/phase2-extensions/domain/services/initial-creation-expiration-check-service.ts +54 -0
- package/scripts/harness/phase2-extensions/domain/value-objects/initial-creation-age.ts +54 -0
- package/scripts/harness/phase2-extensions/infrastructure/adapters/git-log-initial-creation-age-adapter.ts +77 -0
- package/scripts/harness/phase2-extensions/infrastructure/adapters/harness-config-initial-creation-expiration-adapter.ts +57 -0
- package/scripts/harness/phase2-extensions/infrastructure/adapters/markdown-frontmatter-reader-adapter.ts +56 -0
- package/scripts/harness/phase2-extensions/presentation/formatters/initial-creation-expiration-result-formatter.ts +23 -0
- package/scripts/harness/phase2-extensions/presentation/handlers/check-initial-creation-expiration-handler.ts +38 -0
- package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts +20 -0
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +83 -0
- package/scripts/harness/quick-mode/composition-root.ts +12 -0
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +38 -32
- package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +36 -4
- package/scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts +6 -0
- package/scripts/harness/quick-mode/presentation/formatters/change-category-formatter.ts +42 -0
- package/scripts/harness/quick-mode/presentation/handlers/check-change-category-handler.ts +50 -0
- package/scripts/harness/setup/skill-deployer.ts +30 -0
- package/scripts/harness/traceability-model/composition-root.ts +14 -0
- package/scripts/harness/traceability-model/domain/value-objects/project-relative-path.ts +3 -0
- package/scripts/harness/traceability-model/infrastructure/parsers/markdown-story-annotation-parser.ts +39 -4
- package/scripts/harness/traceability-model/presentation/cli/validate-metadata-command-handler.ts +103 -9
- package/skills/domain-designer/SKILL.md +34 -0
- package/skills/it-test-logic-designer/SKILL.md +16 -0
- package/skills/logical-designer/SKILL.md +34 -0
- package/skills/quick-implementor/SKILL.md +10 -1
- package/skills/scenario-test-logic-designer/SKILL.md +16 -0
- package/skills/story-implementor/SKILL.md +58 -0
- package/skills/unit-designer/SKILL.md +41 -0
- package/skills/unit-test-logic-designer/SKILL.md +18 -0
- package/templates/.codex/hooks.json +63 -0
- package/templates/logical_design.template.md +79 -0
- package/templates/source.template.ts +18 -0
- package/templates/test.template.ts +37 -0
package/scripts/harness/main.ts
CHANGED
|
@@ -29,7 +29,7 @@ import { buildCiGovernance } from './ci-governance/composition-root.js';
|
|
|
29
29
|
import { createSkillQualityHandlers } from './skill-quality/composition-root.js';
|
|
30
30
|
import { buildRegressionSuite } from './regression-suite/composition-root.js';
|
|
31
31
|
import { buildPhase2Extensions } from './phase2-extensions/composition-root.js';
|
|
32
|
-
import { deploySkills, deployHookScripts, getDeployedVersion, getHarnessVersion, initHarnessConfig, deployDesignDocs, deployHuskyHook, SKILL_CATEGORIES, getCategoryForSkill } from './setup/skill-deployer.js';
|
|
32
|
+
import { deploySkills, deployHookScripts, getDeployedVersion, getHarnessVersion, initHarnessConfig, deployDesignDocs, deployHuskyHook, deployCodexHooks, SKILL_CATEGORIES, getCategoryForSkill } from './setup/skill-deployer.js';
|
|
33
33
|
import type { SkillSet } from './setup/skill-deployer.js';
|
|
34
34
|
import type { HarnessConfigV2 } from './config-foundation/domain/harness-config.js';
|
|
35
35
|
import { ConfigValidationError } from './config-foundation/domain/errors/config-validation-error.js';
|
|
@@ -53,7 +53,7 @@ Usage: phasegate <command> [options]
|
|
|
53
53
|
|
|
54
54
|
Setup:
|
|
55
55
|
init Initialize project: deploy skills + design docs + phasegate.config.json
|
|
56
|
-
(--name <project-name>, --preset <full|standard|minimal|custom>, --with-husky)
|
|
56
|
+
(--name <project-name>, --preset <full|standard|minimal|custom>, --agent <claude|codex|both>, --with-husky)
|
|
57
57
|
update-skills Re-deploy skills from current harness version
|
|
58
58
|
|
|
59
59
|
Commands:
|
|
@@ -106,7 +106,8 @@ Commands:
|
|
|
106
106
|
p2:check-freshness Check doc freshness (--pattern <glob>, --dry-run, --format text|json)
|
|
107
107
|
p2:validate-pointers Validate doc pointers (--include-urls, --format text|json)
|
|
108
108
|
p2:generate-e2e-template Generate E2E test template (--phase <phase>, --output <path>)
|
|
109
|
-
|
|
109
|
+
p2:check-initial-creation Detect long-lived initial_creation:true docs (--pattern <glob>, --format text|json)
|
|
110
|
+
hook <pre-tool-use|post-tool-use|stop|session-start|user-prompt-submit> Run agent hook (reads JSON from stdin; writes JSON to stdout for session-start/user-prompt-submit)
|
|
110
111
|
pre-commit Run L2 pre-commit validators on staged files
|
|
111
112
|
delegate-sonnet [...args] Delegate task to Sonnet 4.6 (forwards args to scripts/delegate-sonnet.sh)
|
|
112
113
|
|
|
@@ -400,9 +401,22 @@ async function main(): Promise<void> {
|
|
|
400
401
|
process.exit(2);
|
|
401
402
|
}
|
|
402
403
|
const skillSet: SkillSet = skillSetRaw;
|
|
404
|
+
const agentRaw = parseFlag(args, '--agent') ?? 'claude';
|
|
405
|
+
if (agentRaw !== 'claude' && agentRaw !== 'codex' && agentRaw !== 'both') {
|
|
406
|
+
console.error(`Invalid --agent value: "${agentRaw}". Use "claude", "codex", or "both".`);
|
|
407
|
+
process.exit(2);
|
|
408
|
+
}
|
|
409
|
+
const agent = agentRaw;
|
|
410
|
+
const deployClaude = agent === 'claude' || agent === 'both';
|
|
411
|
+
const deployCodex = agent === 'codex' || agent === 'both';
|
|
403
412
|
const result = await deploySkills(harnessRoot, rootDir, skillSet);
|
|
404
413
|
const configResult = await initHarnessConfig(rootDir, projectName, phasePreset);
|
|
405
|
-
const hooksResult =
|
|
414
|
+
const hooksResult = deployClaude
|
|
415
|
+
? await deployHookScripts(harnessRoot, rootDir)
|
|
416
|
+
: { scriptsDeployed: 0, settingsCreated: false };
|
|
417
|
+
const codexResult = deployCodex
|
|
418
|
+
? await deployCodexHooks(harnessRoot, rootDir)
|
|
419
|
+
: null;
|
|
406
420
|
const designDocsResult = await deployDesignDocs(harnessRoot, rootDir);
|
|
407
421
|
const withHusky = hasFlag(args, '--with-husky');
|
|
408
422
|
const huskyResult = withHusky
|
|
@@ -422,6 +436,13 @@ async function main(): Promise<void> {
|
|
|
422
436
|
} else if (hooksResult.scriptsDeployed > 0) {
|
|
423
437
|
console.log(` .claude/settings.json already exists, skipped`);
|
|
424
438
|
}
|
|
439
|
+
if (codexResult !== null) {
|
|
440
|
+
if (codexResult.created) {
|
|
441
|
+
console.log(`✓ .codex/hooks.json deployed`);
|
|
442
|
+
} else {
|
|
443
|
+
console.log(` .codex/hooks.json already exists, skipped`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
425
446
|
if (designDocsResult.copiedFiles.length > 0) {
|
|
426
447
|
console.log(`✓ Design docs deployed (${designDocsResult.copiedFiles.length} files)`);
|
|
427
448
|
}
|
|
@@ -435,7 +456,7 @@ async function main(): Promise<void> {
|
|
|
435
456
|
console.log(` .husky/pre-commit already exists, skipped`);
|
|
436
457
|
}
|
|
437
458
|
}
|
|
438
|
-
console.log(`✓ Harness v${result.version} initialized`);
|
|
459
|
+
console.log(`✓ Harness v${result.version} initialized (agent: ${agent})`);
|
|
439
460
|
console.log('');
|
|
440
461
|
console.log('Next steps:');
|
|
441
462
|
if (skillSet === 'core') {
|
|
@@ -444,7 +465,14 @@ async function main(): Promise<void> {
|
|
|
444
465
|
console.log(' 1. Run the product-architect skill to start AIDLC');
|
|
445
466
|
}
|
|
446
467
|
console.log(' 2. Customize phasegate.config.json if needed');
|
|
447
|
-
|
|
468
|
+
if (deployClaude) {
|
|
469
|
+
console.log(' 3. Edit .claude/scripts/hook-config.json to set target directories');
|
|
470
|
+
}
|
|
471
|
+
if (deployCodex) {
|
|
472
|
+
console.log(` ${deployClaude ? '4' : '3'}. Enable Codex hooks: codex features enable codex_hooks`);
|
|
473
|
+
console.log(` ${deployClaude ? '5' : '4'}. (Recommended) Install pre-commit backstop: rerun with --with-husky or set up husky manually`);
|
|
474
|
+
console.log(` See docs/guide/codex-integration.md for the native apply_patch limitation.`);
|
|
475
|
+
}
|
|
448
476
|
process.exit(0);
|
|
449
477
|
break;
|
|
450
478
|
}
|
|
@@ -688,6 +716,41 @@ async function main(): Promise<void> {
|
|
|
688
716
|
break;
|
|
689
717
|
}
|
|
690
718
|
|
|
719
|
+
// ── quick-mode / check-change-category (H10-05) ──
|
|
720
|
+
case 'check-change-category': {
|
|
721
|
+
if (hasFlag(args, '--help')) {
|
|
722
|
+
process.stdout.write([
|
|
723
|
+
'Usage: phasegate check-change-category --paths <csv> [options]',
|
|
724
|
+
'',
|
|
725
|
+
'Classify changed file paths into quick-mode categories and report',
|
|
726
|
+
'whether Full Mode is required.',
|
|
727
|
+
'',
|
|
728
|
+
'Options:',
|
|
729
|
+
' --paths <csv> Comma-separated file paths to classify.',
|
|
730
|
+
' --format <human|json> Output format. Default: human.',
|
|
731
|
+
' --fail-on-full-required Exit with code 1 when Full Mode is required.',
|
|
732
|
+
' --help Show this help.',
|
|
733
|
+
'',
|
|
734
|
+
'Examples:',
|
|
735
|
+
' phasegate check-change-category --paths src/foo.ts,src/bar.ts',
|
|
736
|
+
' phasegate check-change-category --paths src/foo.ts --format json',
|
|
737
|
+
'',
|
|
738
|
+
].join('\n'));
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const mod = createQuickModeCompositionRoot();
|
|
742
|
+
const paths = parseFlag(args, '--paths');
|
|
743
|
+
const format = parseFlag(args, '--format') as 'human' | 'json' | undefined;
|
|
744
|
+
const failOnFullRequired = hasFlag(args, '--fail-on-full-required');
|
|
745
|
+
const result = await mod.checkChangeCategoryHandler.handle({
|
|
746
|
+
paths,
|
|
747
|
+
format,
|
|
748
|
+
failOnFullRequired,
|
|
749
|
+
});
|
|
750
|
+
process.exit(result.exitCode);
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
|
|
691
754
|
// ── harness-api ──
|
|
692
755
|
case 'phasegate:check-ready': {
|
|
693
756
|
const mod = createHarnessApiModule();
|
|
@@ -996,22 +1059,34 @@ Examples:
|
|
|
996
1059
|
break;
|
|
997
1060
|
}
|
|
998
1061
|
|
|
1062
|
+
case 'p2:check-initial-creation': {
|
|
1063
|
+
const mod = buildPhase2Extensions(rootDir, resolvedConfig ?? undefined);
|
|
1064
|
+
const p2args = args.slice(1);
|
|
1065
|
+
const result = await mod.checkInitialCreationExpirationHandler.handle(p2args);
|
|
1066
|
+
console.log(result.stdout);
|
|
1067
|
+
process.exit(result.exitCode);
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
999
1071
|
// ── agent integration / hooks ──
|
|
1000
1072
|
case 'hook': {
|
|
1001
1073
|
const subCommand = args[1];
|
|
1074
|
+
const usage = 'Usage: phasegate hook <pre-tool-use|post-tool-use|stop|session-start|user-prompt-submit>';
|
|
1002
1075
|
if (!subCommand) {
|
|
1003
|
-
console.error(
|
|
1076
|
+
console.error(usage);
|
|
1004
1077
|
process.exit(2);
|
|
1005
1078
|
}
|
|
1006
1079
|
const hookFileName: Record<string, string> = {
|
|
1007
1080
|
'pre-tool-use': 'pre-tool-use-hook.js',
|
|
1008
1081
|
'post-tool-use': 'post-tool-use-hook.js',
|
|
1009
1082
|
'stop': 'stop-hook.js',
|
|
1083
|
+
'session-start': 'session-start-hook.js',
|
|
1084
|
+
'user-prompt-submit': 'user-prompt-submit-hook.js',
|
|
1010
1085
|
};
|
|
1011
1086
|
const fileName = hookFileName[subCommand];
|
|
1012
1087
|
if (!fileName) {
|
|
1013
1088
|
console.error(`Unknown hook subcommand: ${subCommand}`);
|
|
1014
|
-
console.error(
|
|
1089
|
+
console.error(usage);
|
|
1015
1090
|
process.exit(2);
|
|
1016
1091
|
}
|
|
1017
1092
|
const hookPath = join(harnessRoot, 'scripts/harness/agent-integration/presentation', fileName);
|
|
@@ -1021,7 +1096,10 @@ Examples:
|
|
|
1021
1096
|
|
|
1022
1097
|
case 'pre-commit': {
|
|
1023
1098
|
const preCommitPath = join(harnessRoot, 'scripts/harness/integrations/pre-commit.js');
|
|
1024
|
-
await import(preCommitPath)
|
|
1099
|
+
const preCommitMod = (await import(preCommitPath)) as {
|
|
1100
|
+
runPreCommitCli: () => Promise<void>;
|
|
1101
|
+
};
|
|
1102
|
+
await preCommitMod.runPreCommitCli();
|
|
1025
1103
|
break;
|
|
1026
1104
|
}
|
|
1027
1105
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer application
|
|
3
|
+
* @unit phase2-extensions
|
|
4
|
+
*/
|
|
5
|
+
import type { HarnessErrorContract } from '../../../harness-error/application/dto/harness-error-contract.js';
|
|
6
|
+
import type { InitialCreationExpirationResult } from '../../domain/services/initial-creation-expiration-check-service.js';
|
|
7
|
+
|
|
8
|
+
export interface CheckInitialCreationExpirationOutput {
|
|
9
|
+
results: InitialCreationExpirationResult[];
|
|
10
|
+
summary: {
|
|
11
|
+
total: number;
|
|
12
|
+
ok: number;
|
|
13
|
+
warn: number;
|
|
14
|
+
};
|
|
15
|
+
warnings: HarnessErrorContract[];
|
|
16
|
+
errors: HarnessErrorContract[];
|
|
17
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer application
|
|
3
|
+
* @unit phase2-extensions
|
|
4
|
+
*/
|
|
5
|
+
import type { CheckInitialCreationExpirationInput } from '../dto/check-initial-creation-expiration-input.js';
|
|
6
|
+
import type { CheckInitialCreationExpirationOutput } from '../dto/check-initial-creation-expiration-output.js';
|
|
7
|
+
import type { InitialCreationExpirationConfigPort } from '../../domain/ports/initial-creation-expiration-config-port.js';
|
|
8
|
+
import type { DocumentScannerPort } from '../../domain/ports/document-scanner-port.js';
|
|
9
|
+
import type { FrontmatterReaderPort } from '../../domain/ports/frontmatter-reader-port.js';
|
|
10
|
+
import type { InitialCreationAgePort } from '../../domain/ports/initial-creation-age-port.js';
|
|
11
|
+
import type { InitialCreationExpirationCheckService } from '../../domain/services/initial-creation-expiration-check-service.js';
|
|
12
|
+
import type { HarnessErrorContract } from '../../../harness-error/application/dto/harness-error-contract.js';
|
|
13
|
+
import type { InitialCreationExpirationResult } from '../../domain/services/initial-creation-expiration-check-service.js';
|
|
14
|
+
|
|
15
|
+
export class CheckInitialCreationExpirationUseCase {
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly configPort: InitialCreationExpirationConfigPort,
|
|
18
|
+
private readonly scannerPort: DocumentScannerPort,
|
|
19
|
+
private readonly frontmatterReaderPort: FrontmatterReaderPort,
|
|
20
|
+
private readonly agePort: InitialCreationAgePort,
|
|
21
|
+
private readonly checkService: InitialCreationExpirationCheckService,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
async execute(input: CheckInitialCreationExpirationInput): Promise<CheckInitialCreationExpirationOutput> {
|
|
25
|
+
try {
|
|
26
|
+
const allRules = await this.configPort.loadRules();
|
|
27
|
+
const filteredRules = input.targetPattern
|
|
28
|
+
? allRules.filter((rule) => rule.documentPattern === input.targetPattern)
|
|
29
|
+
: allRules;
|
|
30
|
+
|
|
31
|
+
const results: InitialCreationExpirationResult[] = [];
|
|
32
|
+
const warnings: HarnessErrorContract[] = [];
|
|
33
|
+
|
|
34
|
+
for (const rule of filteredRules) {
|
|
35
|
+
if (!rule.isEnabled()) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const documentPaths = await this.scannerPort.scan(rule.documentPattern);
|
|
40
|
+
|
|
41
|
+
for (const documentPath of documentPaths) {
|
|
42
|
+
const fmResult = await this.frontmatterReaderPort.read(documentPath);
|
|
43
|
+
|
|
44
|
+
if (fmResult.parseError !== null) {
|
|
45
|
+
warnings.push({
|
|
46
|
+
code: 'L4-232',
|
|
47
|
+
severity: 'warning',
|
|
48
|
+
message: `frontmatter parse failed for ${documentPath}: ${fmResult.parseError}`,
|
|
49
|
+
suggestion: 'YAML 構文を確認してください',
|
|
50
|
+
});
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (fmResult.flags === null || fmResult.flags.initialCreation !== true) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const age = await this.agePort.getAge(documentPath);
|
|
59
|
+
const checkResult = this.checkService.check(rule, age, documentPath);
|
|
60
|
+
results.push(checkResult);
|
|
61
|
+
|
|
62
|
+
if (checkResult.level === 'warn') {
|
|
63
|
+
warnings.push({
|
|
64
|
+
code: 'L4-231',
|
|
65
|
+
severity: 'warning',
|
|
66
|
+
message: checkResult.message,
|
|
67
|
+
suggestion: 'frontmatter を削除し @story-id 注釈を付与してください',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const summary = {
|
|
74
|
+
total: results.length,
|
|
75
|
+
ok: results.filter((result) => result.level === 'ok').length,
|
|
76
|
+
warn: results.filter((result) => result.level === 'warn').length,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
results,
|
|
81
|
+
summary,
|
|
82
|
+
warnings,
|
|
83
|
+
errors: [],
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const message = error instanceof Error ? error.message : 'unknown error';
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
results: [],
|
|
90
|
+
summary: { total: 0, ok: 0, warn: 0 },
|
|
91
|
+
warnings: [],
|
|
92
|
+
errors: [
|
|
93
|
+
{
|
|
94
|
+
code: 'L4-299',
|
|
95
|
+
severity: 'error',
|
|
96
|
+
message,
|
|
97
|
+
suggestion: 'phasegate.config.json を確認してください',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -4,16 +4,22 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { HarnessConfigV2 } from '../config-foundation/domain/harness-config.js';
|
|
6
6
|
import { CheckDocFreshnessUseCase } from './application/usecases/check-doc-freshness-usecase.js';
|
|
7
|
+
import { CheckInitialCreationExpirationUseCase } from './application/usecases/check-initial-creation-expiration-usecase.js';
|
|
7
8
|
import { GenerateE2ETemplateUseCase } from './application/usecases/generate-e2e-template-usecase.js';
|
|
8
9
|
import { ValidateDocPointersUseCase } from './application/usecases/validate-doc-pointers-usecase.js';
|
|
9
10
|
import { FreshnessCheckService } from './domain/services/freshness-check-service.js';
|
|
11
|
+
import { InitialCreationExpirationCheckService } from './domain/services/initial-creation-expiration-check-service.js';
|
|
10
12
|
import { PointerResolutionService } from './domain/services/pointer-resolution-service.js';
|
|
11
13
|
import { FileSystemDocumentScannerAdapter } from './infrastructure/adapters/file-system-document-scanner-adapter.js';
|
|
12
14
|
import { FileSystemPointerResolverAdapter } from './infrastructure/adapters/file-system-pointer-resolver-adapter.js';
|
|
13
15
|
import { GitLogDocumentAgeAdapter } from './infrastructure/adapters/git-log-document-age-adapter.js';
|
|
16
|
+
import { GitLogInitialCreationAgeAdapter } from './infrastructure/adapters/git-log-initial-creation-age-adapter.js';
|
|
14
17
|
import { HarnessConfigFreshnessAdapter } from './infrastructure/adapters/harness-config-freshness-adapter.js';
|
|
18
|
+
import { HarnessConfigInitialCreationExpirationAdapter } from './infrastructure/adapters/harness-config-initial-creation-expiration-adapter.js';
|
|
19
|
+
import { MarkdownFrontmatterReaderAdapter } from './infrastructure/adapters/markdown-frontmatter-reader-adapter.js';
|
|
15
20
|
import { RegexPointerExtractorAdapter } from './infrastructure/adapters/regex-pointer-extractor-adapter.js';
|
|
16
21
|
import { CheckFreshnessHandler } from './presentation/handlers/check-freshness-handler.js';
|
|
22
|
+
import { CheckInitialCreationExpirationHandler } from './presentation/handlers/check-initial-creation-expiration-handler.js';
|
|
17
23
|
import { GenerateE2ETemplateHandler } from './presentation/handlers/generate-e2e-template-handler.js';
|
|
18
24
|
import { ValidatePointersHandler } from './presentation/handlers/validate-pointers-handler.js';
|
|
19
25
|
|
|
@@ -24,8 +30,13 @@ export function buildPhase2Extensions(projectRoot: string, config?: HarnessConfi
|
|
|
24
30
|
const pointerExtractor = new RegexPointerExtractorAdapter(projectRoot);
|
|
25
31
|
const pointerResolver = new FileSystemPointerResolverAdapter(projectRoot);
|
|
26
32
|
|
|
33
|
+
const initialCreationConfigAdapter = new HarnessConfigInitialCreationExpirationAdapter(config);
|
|
34
|
+
const frontmatterReader = new MarkdownFrontmatterReaderAdapter(projectRoot);
|
|
35
|
+
const initialCreationAge = new GitLogInitialCreationAgeAdapter(projectRoot);
|
|
36
|
+
|
|
27
37
|
const freshnessCheckService = new FreshnessCheckService();
|
|
28
38
|
const pointerResolutionService = new PointerResolutionService(pointerResolver);
|
|
39
|
+
const initialCreationExpirationCheckService = new InitialCreationExpirationCheckService();
|
|
29
40
|
|
|
30
41
|
const checkDocFreshnessUseCase = new CheckDocFreshnessUseCase(
|
|
31
42
|
configAdapter,
|
|
@@ -40,13 +51,24 @@ export function buildPhase2Extensions(projectRoot: string, config?: HarnessConfi
|
|
|
40
51
|
pointerResolutionService,
|
|
41
52
|
);
|
|
42
53
|
const generateE2ETemplateUseCase = new GenerateE2ETemplateUseCase();
|
|
54
|
+
const checkInitialCreationExpirationUseCase = new CheckInitialCreationExpirationUseCase(
|
|
55
|
+
initialCreationConfigAdapter,
|
|
56
|
+
documentScanner,
|
|
57
|
+
frontmatterReader,
|
|
58
|
+
initialCreationAge,
|
|
59
|
+
initialCreationExpirationCheckService,
|
|
60
|
+
);
|
|
43
61
|
|
|
44
62
|
return {
|
|
45
63
|
checkDocFreshnessUseCase,
|
|
46
64
|
validateDocPointersUseCase,
|
|
47
65
|
generateE2ETemplateUseCase,
|
|
66
|
+
checkInitialCreationExpirationUseCase,
|
|
48
67
|
checkFreshnessHandler: new CheckFreshnessHandler(checkDocFreshnessUseCase),
|
|
49
68
|
validatePointersHandler: new ValidatePointersHandler(validateDocPointersUseCase),
|
|
50
69
|
generateE2ETemplateHandler: new GenerateE2ETemplateHandler(generateE2ETemplateUseCase),
|
|
70
|
+
checkInitialCreationExpirationHandler: new CheckInitialCreationExpirationHandler(
|
|
71
|
+
checkInitialCreationExpirationUseCase,
|
|
72
|
+
),
|
|
51
73
|
};
|
|
52
74
|
}
|
package/scripts/harness/phase2-extensions/domain/aggregates/initial-creation-expiration-rule.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer domain
|
|
3
|
+
* @unit phase2-extensions
|
|
4
|
+
*/
|
|
5
|
+
import { Phase2ExtensionsDomainError } from '../errors/phase2-extensions-domain-error.js';
|
|
6
|
+
|
|
7
|
+
export type InitialCreationEvaluationMode = 'or' | 'and';
|
|
8
|
+
|
|
9
|
+
export interface InitialCreationExpirationRuleProps {
|
|
10
|
+
ruleId: string;
|
|
11
|
+
documentPattern: string;
|
|
12
|
+
daysThreshold: number;
|
|
13
|
+
commitCountThreshold: number;
|
|
14
|
+
evaluationMode: InitialCreationEvaluationMode;
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function escapeRegex(value: string): string {
|
|
19
|
+
return value.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function toPatternRegex(pattern: string): RegExp {
|
|
23
|
+
let regex = '^';
|
|
24
|
+
|
|
25
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
26
|
+
const current = pattern[index];
|
|
27
|
+
const next = pattern[index + 1];
|
|
28
|
+
const afterNext = pattern[index + 2];
|
|
29
|
+
|
|
30
|
+
if (current === '*' && next === '*') {
|
|
31
|
+
if (afterNext === '/') {
|
|
32
|
+
regex += '(?:.*/)?';
|
|
33
|
+
index += 2;
|
|
34
|
+
} else {
|
|
35
|
+
regex += '.*';
|
|
36
|
+
index += 1;
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (current === '*') {
|
|
42
|
+
regex += '[^/]*';
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
regex += escapeRegex(current);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return new RegExp(`${regex}$`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class InitialCreationExpirationRule {
|
|
53
|
+
readonly ruleId: string;
|
|
54
|
+
readonly documentPattern: string;
|
|
55
|
+
readonly daysThreshold: number;
|
|
56
|
+
readonly commitCountThreshold: number;
|
|
57
|
+
readonly evaluationMode: InitialCreationEvaluationMode;
|
|
58
|
+
readonly enabled: boolean;
|
|
59
|
+
private readonly patternRegex: RegExp;
|
|
60
|
+
|
|
61
|
+
private constructor(props: InitialCreationExpirationRuleProps) {
|
|
62
|
+
this.ruleId = props.ruleId;
|
|
63
|
+
this.documentPattern = props.documentPattern;
|
|
64
|
+
this.daysThreshold = props.daysThreshold;
|
|
65
|
+
this.commitCountThreshold = props.commitCountThreshold;
|
|
66
|
+
this.evaluationMode = props.evaluationMode;
|
|
67
|
+
this.enabled = props.enabled;
|
|
68
|
+
this.patternRegex = toPatternRegex(props.documentPattern);
|
|
69
|
+
Object.freeze(this);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static create(props: InitialCreationExpirationRuleProps): InitialCreationExpirationRule {
|
|
73
|
+
if (props.ruleId.trim().length === 0) {
|
|
74
|
+
throw new Phase2ExtensionsDomainError('L4-247', 'ruleId は空文字不可です');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (props.documentPattern.trim().length === 0) {
|
|
78
|
+
throw new Phase2ExtensionsDomainError('L4-248', 'documentPattern は空文字不可です');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!Number.isInteger(props.daysThreshold) || props.daysThreshold <= 0) {
|
|
82
|
+
throw new Phase2ExtensionsDomainError('L4-244', 'daysThreshold は正の整数 (0 超) である必要があります');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!Number.isInteger(props.commitCountThreshold) || props.commitCountThreshold <= 0) {
|
|
86
|
+
throw new Phase2ExtensionsDomainError('L4-245', 'commitCountThreshold は正の整数 (0 超) である必要があります');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (props.evaluationMode !== 'or' && props.evaluationMode !== 'and') {
|
|
90
|
+
throw new Phase2ExtensionsDomainError('L4-246', 'evaluationMode が不正です');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return new InitialCreationExpirationRule(props);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
isEnabled(): boolean {
|
|
97
|
+
return this.enabled;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
matchesDocument(documentPath: string): boolean {
|
|
101
|
+
return this.patternRegex.test(documentPath);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer domain
|
|
3
|
+
* @unit phase2-extensions
|
|
4
|
+
*/
|
|
5
|
+
export interface FrontmatterFlags {
|
|
6
|
+
initialCreation: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface FrontmatterReadResult {
|
|
10
|
+
filePath: string;
|
|
11
|
+
flags: FrontmatterFlags | null;
|
|
12
|
+
parseError: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FrontmatterReaderPort {
|
|
16
|
+
read(filePath: string): Promise<FrontmatterReadResult>;
|
|
17
|
+
}
|
package/scripts/harness/phase2-extensions/domain/ports/initial-creation-expiration-config-port.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer domain
|
|
3
|
+
* @unit phase2-extensions
|
|
4
|
+
*/
|
|
5
|
+
import type { InitialCreationExpirationRule } from '../aggregates/initial-creation-expiration-rule.js';
|
|
6
|
+
|
|
7
|
+
export interface InitialCreationExpirationConfigPort {
|
|
8
|
+
loadRules(): Promise<InitialCreationExpirationRule[]>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer domain
|
|
3
|
+
* @unit phase2-extensions
|
|
4
|
+
*/
|
|
5
|
+
import type { InitialCreationExpirationRule } from '../aggregates/initial-creation-expiration-rule.js';
|
|
6
|
+
import type { InitialCreationAge, InitialCreationAgeSource } from '../value-objects/initial-creation-age.js';
|
|
7
|
+
|
|
8
|
+
export type InitialCreationExpirationLevel = 'ok' | 'warn';
|
|
9
|
+
|
|
10
|
+
export interface InitialCreationExpirationResult {
|
|
11
|
+
ruleId: string;
|
|
12
|
+
documentPath: string;
|
|
13
|
+
ageInDays: number;
|
|
14
|
+
commitCount: number;
|
|
15
|
+
ageSource: InitialCreationAgeSource;
|
|
16
|
+
level: InitialCreationExpirationLevel;
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class InitialCreationExpirationCheckService {
|
|
21
|
+
check(
|
|
22
|
+
rule: InitialCreationExpirationRule,
|
|
23
|
+
age: InitialCreationAge,
|
|
24
|
+
documentPath: string,
|
|
25
|
+
): InitialCreationExpirationResult {
|
|
26
|
+
if (!rule.isEnabled()) {
|
|
27
|
+
return {
|
|
28
|
+
ruleId: rule.ruleId,
|
|
29
|
+
documentPath,
|
|
30
|
+
ageInDays: age.ageInDays,
|
|
31
|
+
commitCount: age.commitCount,
|
|
32
|
+
ageSource: age.source,
|
|
33
|
+
level: 'ok',
|
|
34
|
+
message: 'disabled rule skipped',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const daysHit = age.ageInDays >= rule.daysThreshold;
|
|
39
|
+
const commitHit = age.commitCount >= rule.commitCountThreshold;
|
|
40
|
+
const expired = rule.evaluationMode === 'or' ? daysHit || commitHit : daysHit && commitHit;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
ruleId: rule.ruleId,
|
|
44
|
+
documentPath,
|
|
45
|
+
ageInDays: age.ageInDays,
|
|
46
|
+
commitCount: age.commitCount,
|
|
47
|
+
ageSource: age.source,
|
|
48
|
+
level: expired ? 'warn' : 'ok',
|
|
49
|
+
message: expired
|
|
50
|
+
? `${documentPath} has been initial_creation:true for ${age.ageInDays} days (commits=${age.commitCount})`
|
|
51
|
+
: `${documentPath} within threshold`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer domain
|
|
3
|
+
* @unit phase2-extensions
|
|
4
|
+
*/
|
|
5
|
+
import { Phase2ExtensionsDomainError } from '../errors/phase2-extensions-domain-error.js';
|
|
6
|
+
|
|
7
|
+
export type InitialCreationAgeSource = 'git-log' | 'file-mtime';
|
|
8
|
+
|
|
9
|
+
export interface InitialCreationAgeProps {
|
|
10
|
+
ageInDays: number;
|
|
11
|
+
commitCount: number;
|
|
12
|
+
source: InitialCreationAgeSource;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class InitialCreationAge {
|
|
16
|
+
readonly ageInDays: number;
|
|
17
|
+
readonly commitCount: number;
|
|
18
|
+
readonly source: InitialCreationAgeSource;
|
|
19
|
+
|
|
20
|
+
private constructor(props: InitialCreationAgeProps) {
|
|
21
|
+
this.ageInDays = props.ageInDays;
|
|
22
|
+
this.commitCount = props.commitCount;
|
|
23
|
+
this.source = props.source;
|
|
24
|
+
Object.freeze(this);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static create(props: InitialCreationAgeProps): InitialCreationAge {
|
|
28
|
+
if (!Number.isFinite(props.ageInDays) || props.ageInDays < 0) {
|
|
29
|
+
throw new Phase2ExtensionsDomainError('L4-241', 'ageInDays は 0 以上である必要があります');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!Number.isFinite(props.commitCount) || props.commitCount < 1) {
|
|
33
|
+
throw new Phase2ExtensionsDomainError('L4-242', 'commitCount は 1 以上である必要があります');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (props.source !== 'git-log' && props.source !== 'file-mtime') {
|
|
37
|
+
throw new Phase2ExtensionsDomainError('L4-243', 'source が不正です');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return new InitialCreationAge({
|
|
41
|
+
ageInDays: Math.floor(props.ageInDays),
|
|
42
|
+
commitCount: Math.floor(props.commitCount),
|
|
43
|
+
source: props.source,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
equals(other: InitialCreationAge): boolean {
|
|
48
|
+
return (
|
|
49
|
+
this.ageInDays === other.ageInDays &&
|
|
50
|
+
this.commitCount === other.commitCount &&
|
|
51
|
+
this.source === other.source
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|