@urielsh/prodify 0.1.2 → 0.1.3
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/.prodify/contracts/architecture.contract.json +9 -1
- package/.prodify/contracts/diagnose.contract.json +9 -1
- package/.prodify/contracts/plan.contract.json +9 -1
- package/.prodify/contracts/refactor.contract.json +13 -2
- package/.prodify/contracts/understand.contract.json +9 -1
- package/.prodify/contracts/validate.contract.json +11 -2
- package/.prodify/contracts-src/refactor.contract.md +7 -0
- package/.prodify/contracts-src/validate.contract.md +2 -0
- package/assets/presets/default/canonical/contracts-src/refactor.contract.md +7 -0
- package/assets/presets/default/canonical/contracts-src/validate.contract.md +2 -0
- package/dist/contracts/compiled-schema.js +10 -1
- package/dist/contracts/source-schema.js +42 -1
- package/dist/core/diff-validator.js +183 -0
- package/dist/core/plan-units.js +64 -0
- package/dist/core/state.js +6 -0
- package/dist/core/status.js +14 -0
- package/dist/core/validation.js +87 -9
- package/dist/scoring/model.js +94 -213
- package/dist/scoring/scoring-engine.js +158 -0
- package/docs/diff-validator-design.md +44 -0
- package/docs/impact-scoring-design.md +38 -0
- package/package.json +1 -1
- package/src/contracts/compiled-schema.ts +10 -1
- package/src/contracts/source-schema.ts +51 -1
- package/src/core/diff-validator.ts +230 -0
- package/src/core/plan-units.ts +82 -0
- package/src/core/state.ts +6 -0
- package/src/core/status.ts +17 -0
- package/src/core/validation.ts +136 -15
- package/src/scoring/model.ts +101 -250
- package/src/scoring/scoring-engine.ts +194 -0
- package/src/types.ts +55 -0
- package/tests/integration/cli-flows.test.js +1 -0
- package/tests/unit/diff-validator.test.js +28 -0
- package/tests/unit/scoring.test.js +42 -1
- package/tests/unit/validation.test.js +79 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Diff Validator Design
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Prodify must reject refactor stages that only move whitespace, touch too few files, or fail to create meaningful structural change.
|
|
6
|
+
|
|
7
|
+
## Snapshot Model
|
|
8
|
+
|
|
9
|
+
- Capture a tracked repository snapshot before refactor.
|
|
10
|
+
- Compare the current tracked tree against that snapshot.
|
|
11
|
+
- Tracked roots:
|
|
12
|
+
- `src/`
|
|
13
|
+
- `tests/`
|
|
14
|
+
- `assets/`
|
|
15
|
+
|
|
16
|
+
## Deterministic Outputs
|
|
17
|
+
|
|
18
|
+
- `filesModified`
|
|
19
|
+
- `filesAdded`
|
|
20
|
+
- `filesDeleted`
|
|
21
|
+
- `linesAdded`
|
|
22
|
+
- `linesRemoved`
|
|
23
|
+
- `formattingOnlyPaths`
|
|
24
|
+
- `structuralChanges`
|
|
25
|
+
|
|
26
|
+
## Structural Change Flags
|
|
27
|
+
|
|
28
|
+
- `new-directories`
|
|
29
|
+
- `new-layer-directories`
|
|
30
|
+
- `new-modules`
|
|
31
|
+
- `module-boundary-created`
|
|
32
|
+
- `responsibility-reduced`
|
|
33
|
+
|
|
34
|
+
## Validation Rules
|
|
35
|
+
|
|
36
|
+
- minimum files modified
|
|
37
|
+
- minimum lines changed
|
|
38
|
+
- optional file-creation requirement
|
|
39
|
+
- required structural flags
|
|
40
|
+
- formatting-only changes fail
|
|
41
|
+
|
|
42
|
+
## Plan Coupling
|
|
43
|
+
|
|
44
|
+
Refactor validation also confirms that the selected step in `05-refactor.md` maps to a real plan unit from `04-plan.md`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Impact Scoring Design
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Prodify should measure whether a run actually improved repository quality instead of only checking that files changed.
|
|
6
|
+
|
|
7
|
+
## Breakdown
|
|
8
|
+
|
|
9
|
+
- `structure`
|
|
10
|
+
- `maintainability`
|
|
11
|
+
- `complexity`
|
|
12
|
+
- `testability`
|
|
13
|
+
|
|
14
|
+
## Formula
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
total =
|
|
18
|
+
structure * 0.30 +
|
|
19
|
+
maintainability * 0.30 +
|
|
20
|
+
complexity * 0.20 +
|
|
21
|
+
testability * 0.20
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Signals
|
|
25
|
+
|
|
26
|
+
- average function length
|
|
27
|
+
- module count
|
|
28
|
+
- average directory depth
|
|
29
|
+
- dependency depth
|
|
30
|
+
- average imports per module
|
|
31
|
+
- test file ratio
|
|
32
|
+
|
|
33
|
+
## Runtime Integration
|
|
34
|
+
|
|
35
|
+
- Write baseline score when a run is bootstrapped.
|
|
36
|
+
- Write final score and delta after validated completion.
|
|
37
|
+
- Enforce `min_impact_score` during validation.
|
|
38
|
+
- Show stored score delta in `prodify status`.
|
package/package.json
CHANGED
|
@@ -20,6 +20,9 @@ export function validateCompiledContractShape(contract: unknown): CompiledStageC
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const record = contract as Record<string, unknown>;
|
|
23
|
+
const diffValidation = typeof record.diff_validation_rules === 'object' && record.diff_validation_rules !== null
|
|
24
|
+
? record.diff_validation_rules as Record<string, unknown>
|
|
25
|
+
: {};
|
|
23
26
|
return normalizeSourceContractDocument({
|
|
24
27
|
document: {
|
|
25
28
|
frontmatter: {
|
|
@@ -32,7 +35,13 @@ export function validateCompiledContractShape(contract: unknown): CompiledStageC
|
|
|
32
35
|
forbidden_writes: record.forbidden_writes,
|
|
33
36
|
policy_rules: record.policy_rules,
|
|
34
37
|
success_criteria: record.success_criteria,
|
|
35
|
-
skill_routing: record.skill_routing
|
|
38
|
+
skill_routing: record.skill_routing,
|
|
39
|
+
minimum_files_modified: diffValidation.minimum_files_modified ?? record.minimum_files_modified,
|
|
40
|
+
minimum_lines_changed: diffValidation.minimum_lines_changed ?? record.minimum_lines_changed,
|
|
41
|
+
must_create_files: diffValidation.must_create_files ?? record.must_create_files,
|
|
42
|
+
required_structural_changes: diffValidation.required_structural_changes ?? record.required_structural_changes,
|
|
43
|
+
min_impact_score: record.min_impact_score,
|
|
44
|
+
enforce_plan_units: record.enforce_plan_units
|
|
36
45
|
},
|
|
37
46
|
body: 'compiled-contract'
|
|
38
47
|
},
|
|
@@ -40,6 +40,48 @@ function asOptionalStringArray(value: unknown, fieldName: string): string[] {
|
|
|
40
40
|
return asStringArray(value, fieldName);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function asOptionalNonNegativeInteger(value: unknown, fieldName: string, fallback = 0): number {
|
|
44
|
+
if (value === undefined || value === null || value === '') {
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
49
|
+
throw new ProdifyError(`Contract frontmatter field "${fieldName}" must be a non-negative integer.`, {
|
|
50
|
+
code: 'CONTRACT_SCHEMA_INVALID'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function asOptionalNonNegativeNumber(value: unknown, fieldName: string, fallback = 0): number {
|
|
58
|
+
if (value === undefined || value === null || value === '') {
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (typeof value !== 'number' || Number.isNaN(value) || value < 0) {
|
|
63
|
+
throw new ProdifyError(`Contract frontmatter field "${fieldName}" must be a non-negative number.`, {
|
|
64
|
+
code: 'CONTRACT_SCHEMA_INVALID'
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function asOptionalBoolean(value: unknown, fieldName: string, fallback = false): boolean {
|
|
72
|
+
if (value === undefined || value === null || value === '') {
|
|
73
|
+
return fallback;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (typeof value !== 'boolean') {
|
|
77
|
+
throw new ProdifyError(`Contract frontmatter field "${fieldName}" must be a boolean.`, {
|
|
78
|
+
code: 'CONTRACT_SCHEMA_INVALID'
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
|
|
43
85
|
function asStage(value: unknown): FlowStage {
|
|
44
86
|
const stage = asString(value, 'stage') as FlowStage;
|
|
45
87
|
if (!CONTRACT_STAGE_NAMES.includes(stage)) {
|
|
@@ -136,6 +178,14 @@ export function normalizeSourceContractDocument(options: {
|
|
|
136
178
|
: [],
|
|
137
179
|
policy_rules: asStringArray(document.frontmatter.policy_rules, 'policy_rules'),
|
|
138
180
|
success_criteria: asStringArray(document.frontmatter.success_criteria, 'success_criteria'),
|
|
139
|
-
skill_routing: normalizeStageSkillRouting(document.frontmatter.skill_routing)
|
|
181
|
+
skill_routing: normalizeStageSkillRouting(document.frontmatter.skill_routing),
|
|
182
|
+
diff_validation_rules: {
|
|
183
|
+
minimum_files_modified: asOptionalNonNegativeInteger(document.frontmatter.minimum_files_modified, 'minimum_files_modified'),
|
|
184
|
+
minimum_lines_changed: asOptionalNonNegativeInteger(document.frontmatter.minimum_lines_changed, 'minimum_lines_changed'),
|
|
185
|
+
must_create_files: asOptionalBoolean(document.frontmatter.must_create_files, 'must_create_files'),
|
|
186
|
+
required_structural_changes: asOptionalStringArray(document.frontmatter.required_structural_changes, 'required_structural_changes')
|
|
187
|
+
},
|
|
188
|
+
min_impact_score: asOptionalNonNegativeNumber(document.frontmatter.min_impact_score, 'min_impact_score'),
|
|
189
|
+
enforce_plan_units: asOptionalBoolean(document.frontmatter.enforce_plan_units, 'enforce_plan_units')
|
|
140
190
|
};
|
|
141
191
|
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { listFilesRecursive, pathExists, writeFileEnsuringDir } from './fs.js';
|
|
5
|
+
import { normalizeRepoRelativePath, resolveRepoPath } from './paths.js';
|
|
6
|
+
import type { DiffResult } from '../types.js';
|
|
7
|
+
|
|
8
|
+
const DIFF_SNAPSHOT_SCHEMA_VERSION = '1';
|
|
9
|
+
const BASELINE_SNAPSHOT_PATH = '.prodify/metrics/refactor-baseline.snapshot.json';
|
|
10
|
+
const TRACKED_PREFIXES = ['src/', 'tests/', 'assets/'] as const;
|
|
11
|
+
const TRACKED_FILE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json', '.md', '.py', '.cs', '.css', '.scss', '.html']);
|
|
12
|
+
const LAYER_DIRECTORY_NAMES = new Set(['application', 'domain', 'services', 'service', 'modules', 'module', 'adapters', 'infrastructure', 'core']);
|
|
13
|
+
|
|
14
|
+
interface SnapshotFile {
|
|
15
|
+
path: string;
|
|
16
|
+
content: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface RepoSnapshot {
|
|
20
|
+
schema_version: string;
|
|
21
|
+
files: SnapshotFile[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isTrackedPath(relativePath: string): boolean {
|
|
25
|
+
const normalized = normalizeRepoRelativePath(relativePath);
|
|
26
|
+
if (!TRACKED_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return TRACKED_FILE_EXTENSIONS.has(path.extname(normalized));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeWhitespace(content: string): string {
|
|
34
|
+
return content
|
|
35
|
+
.replace(/\s+/g, '')
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toMap(snapshot: RepoSnapshot): Map<string, SnapshotFile> {
|
|
40
|
+
return new Map(snapshot.files.map((file) => [file.path, file]));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function diffLines(before: string, after: string): { added: number; removed: number } {
|
|
44
|
+
const beforeLines = before.replace(/\r\n/g, '\n').split('\n');
|
|
45
|
+
const afterLines = after.replace(/\r\n/g, '\n').split('\n');
|
|
46
|
+
const rows = beforeLines.length;
|
|
47
|
+
const cols = afterLines.length;
|
|
48
|
+
const dp = Array.from({ length: rows + 1 }, () => Array<number>(cols + 1).fill(0));
|
|
49
|
+
|
|
50
|
+
for (let row = rows - 1; row >= 0; row -= 1) {
|
|
51
|
+
for (let col = cols - 1; col >= 0; col -= 1) {
|
|
52
|
+
if (beforeLines[row] === afterLines[col]) {
|
|
53
|
+
dp[row][col] = dp[row + 1][col + 1] + 1;
|
|
54
|
+
} else {
|
|
55
|
+
dp[row][col] = Math.max(dp[row + 1][col], dp[row][col + 1]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const common = dp[0][0];
|
|
61
|
+
return {
|
|
62
|
+
added: Math.max(0, afterLines.length - common),
|
|
63
|
+
removed: Math.max(0, beforeLines.length - common)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function collectDirectories(paths: string[]): string[] {
|
|
68
|
+
return [...new Set(paths
|
|
69
|
+
.map((relativePath) => path.posix.dirname(relativePath))
|
|
70
|
+
.filter((directory) => directory !== '.' && directory !== ''))]
|
|
71
|
+
.sort((left, right) => left.localeCompare(right));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function detectStructuralChanges(options: {
|
|
75
|
+
addedPaths: string[];
|
|
76
|
+
modifiedPaths: string[];
|
|
77
|
+
beforeMap: Map<string, SnapshotFile>;
|
|
78
|
+
afterMap: Map<string, SnapshotFile>;
|
|
79
|
+
removedLineCounts: Map<string, number>;
|
|
80
|
+
}): DiffResult['structuralChanges'] {
|
|
81
|
+
const newDirectories = collectDirectories(options.addedPaths);
|
|
82
|
+
const newLayers = newDirectories.filter((directory) => LAYER_DIRECTORY_NAMES.has(path.posix.basename(directory)));
|
|
83
|
+
const filesWithReducedResponsibility = options.modifiedPaths
|
|
84
|
+
.filter((relativePath) => (options.removedLineCounts.get(relativePath) ?? 0) > 0)
|
|
85
|
+
.sort((left, right) => left.localeCompare(right));
|
|
86
|
+
const newModules = options.addedPaths
|
|
87
|
+
.filter((relativePath) => relativePath.startsWith('src/'))
|
|
88
|
+
.sort((left, right) => left.localeCompare(right));
|
|
89
|
+
|
|
90
|
+
const flags = new Set<string>();
|
|
91
|
+
if (newDirectories.length > 0) {
|
|
92
|
+
flags.add('new-directories');
|
|
93
|
+
}
|
|
94
|
+
if (newLayers.length > 0) {
|
|
95
|
+
flags.add('new-layer-directories');
|
|
96
|
+
flags.add('module-boundary-created');
|
|
97
|
+
}
|
|
98
|
+
if (newModules.length > 0) {
|
|
99
|
+
flags.add('new-modules');
|
|
100
|
+
flags.add('module-boundary-created');
|
|
101
|
+
}
|
|
102
|
+
if (filesWithReducedResponsibility.length > 0) {
|
|
103
|
+
flags.add('responsibility-reduced');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
new_directories: newDirectories,
|
|
108
|
+
new_layer_directories: newLayers,
|
|
109
|
+
files_with_reduced_responsibility: filesWithReducedResponsibility,
|
|
110
|
+
new_modules: newModules,
|
|
111
|
+
structural_change_flags: [...flags].sort((left, right) => left.localeCompare(right))
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function serializeSnapshot(snapshot: RepoSnapshot): string {
|
|
116
|
+
return `${JSON.stringify(snapshot, null, 2)}\n`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function captureRepoSnapshot(repoRoot: string): Promise<RepoSnapshot> {
|
|
120
|
+
const repoFiles = await listFilesRecursive(repoRoot);
|
|
121
|
+
const files: SnapshotFile[] = [];
|
|
122
|
+
|
|
123
|
+
for (const file of repoFiles) {
|
|
124
|
+
if (!isTrackedPath(file.relativePath)) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
files.push({
|
|
129
|
+
path: normalizeRepoRelativePath(file.relativePath),
|
|
130
|
+
content: await fs.readFile(file.fullPath, 'utf8')
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
files.sort((left, right) => left.path.localeCompare(right.path));
|
|
135
|
+
return {
|
|
136
|
+
schema_version: DIFF_SNAPSHOT_SCHEMA_VERSION,
|
|
137
|
+
files
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function writeRefactorBaselineSnapshot(repoRoot: string): Promise<RepoSnapshot> {
|
|
142
|
+
const snapshot = await captureRepoSnapshot(repoRoot);
|
|
143
|
+
await writeFileEnsuringDir(resolveRepoPath(repoRoot, BASELINE_SNAPSHOT_PATH), serializeSnapshot(snapshot));
|
|
144
|
+
return snapshot;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function readRefactorBaselineSnapshot(repoRoot: string): Promise<RepoSnapshot | null> {
|
|
148
|
+
const baselinePath = resolveRepoPath(repoRoot, BASELINE_SNAPSHOT_PATH);
|
|
149
|
+
if (!(await pathExists(baselinePath))) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return JSON.parse(await fs.readFile(baselinePath, 'utf8')) as RepoSnapshot;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function diffSnapshots(before: RepoSnapshot, after: RepoSnapshot): DiffResult {
|
|
157
|
+
const beforeMap = toMap(before);
|
|
158
|
+
const afterMap = toMap(after);
|
|
159
|
+
const allPaths = [...new Set([...beforeMap.keys(), ...afterMap.keys()])].sort((left, right) => left.localeCompare(right));
|
|
160
|
+
const modifiedPaths: string[] = [];
|
|
161
|
+
const addedPaths: string[] = [];
|
|
162
|
+
const deletedPaths: string[] = [];
|
|
163
|
+
const formattingOnlyPaths: string[] = [];
|
|
164
|
+
let linesAdded = 0;
|
|
165
|
+
let linesRemoved = 0;
|
|
166
|
+
const removedLineCounts = new Map<string, number>();
|
|
167
|
+
|
|
168
|
+
for (const relativePath of allPaths) {
|
|
169
|
+
const beforeFile = beforeMap.get(relativePath);
|
|
170
|
+
const afterFile = afterMap.get(relativePath);
|
|
171
|
+
|
|
172
|
+
if (!beforeFile && afterFile) {
|
|
173
|
+
addedPaths.push(relativePath);
|
|
174
|
+
linesAdded += afterFile.content.replace(/\r\n/g, '\n').split('\n').length;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (beforeFile && !afterFile) {
|
|
179
|
+
deletedPaths.push(relativePath);
|
|
180
|
+
const removed = beforeFile.content.replace(/\r\n/g, '\n').split('\n').length;
|
|
181
|
+
linesRemoved += removed;
|
|
182
|
+
removedLineCounts.set(relativePath, removed);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!beforeFile || !afterFile || beforeFile.content === afterFile.content) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
modifiedPaths.push(relativePath);
|
|
191
|
+
if (normalizeWhitespace(beforeFile.content) === normalizeWhitespace(afterFile.content)) {
|
|
192
|
+
formattingOnlyPaths.push(relativePath);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const lineDiff = diffLines(beforeFile.content, afterFile.content);
|
|
197
|
+
linesAdded += lineDiff.added;
|
|
198
|
+
linesRemoved += lineDiff.removed;
|
|
199
|
+
removedLineCounts.set(relativePath, lineDiff.removed);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
filesModified: modifiedPaths.length,
|
|
204
|
+
filesAdded: addedPaths.length,
|
|
205
|
+
filesDeleted: deletedPaths.length,
|
|
206
|
+
linesAdded,
|
|
207
|
+
linesRemoved,
|
|
208
|
+
modifiedPaths,
|
|
209
|
+
addedPaths,
|
|
210
|
+
deletedPaths,
|
|
211
|
+
formattingOnlyPaths,
|
|
212
|
+
structuralChanges: detectStructuralChanges({
|
|
213
|
+
addedPaths,
|
|
214
|
+
modifiedPaths,
|
|
215
|
+
beforeMap,
|
|
216
|
+
afterMap,
|
|
217
|
+
removedLineCounts
|
|
218
|
+
})
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function diffAgainstRefactorBaseline(repoRoot: string): Promise<DiffResult | null> {
|
|
223
|
+
const baseline = await readRefactorBaselineSnapshot(repoRoot);
|
|
224
|
+
if (!baseline) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const current = await captureRepoSnapshot(repoRoot);
|
|
229
|
+
return diffSnapshots(baseline, current);
|
|
230
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
import { resolveCanonicalPath } from './paths.js';
|
|
4
|
+
|
|
5
|
+
export interface PlanUnit {
|
|
6
|
+
id: string;
|
|
7
|
+
description: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function extractSection(markdown: string, heading: string): string {
|
|
11
|
+
const normalized = markdown.replace(/\r\n/g, '\n');
|
|
12
|
+
const match = new RegExp(`^##\\s+${heading}\\s*$([\\s\\S]*?)(?=^##\\s+|\\Z)`, 'm').exec(normalized);
|
|
13
|
+
return match?.[1]?.trim() ?? '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizePlanUnits(section: string): PlanUnit[] {
|
|
17
|
+
const lines = section.split('\n');
|
|
18
|
+
const units: PlanUnit[] = [];
|
|
19
|
+
let currentId: string | null = null;
|
|
20
|
+
let currentDescription = '';
|
|
21
|
+
|
|
22
|
+
for (const rawLine of lines) {
|
|
23
|
+
const line = rawLine.trim();
|
|
24
|
+
const idMatch = /^-\s+Step ID:\s+(.+)$/.exec(line);
|
|
25
|
+
if (idMatch) {
|
|
26
|
+
if (currentId) {
|
|
27
|
+
units.push({
|
|
28
|
+
id: currentId,
|
|
29
|
+
description: currentDescription
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
currentId = idMatch[1].trim();
|
|
34
|
+
currentDescription = '';
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!currentId) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const descriptionMatch = /^-\s+Description:\s+(.+)$/.exec(line);
|
|
43
|
+
if (descriptionMatch) {
|
|
44
|
+
currentDescription = descriptionMatch[1].trim();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (currentId) {
|
|
49
|
+
units.push({
|
|
50
|
+
id: currentId,
|
|
51
|
+
description: currentDescription
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return units;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function readPlanUnits(repoRoot: string): Promise<PlanUnit[]> {
|
|
59
|
+
const planPath = resolveCanonicalPath(repoRoot, '.prodify/artifacts/04-plan.md');
|
|
60
|
+
const markdown = await fs.readFile(planPath, 'utf8');
|
|
61
|
+
return normalizePlanUnits(extractSection(markdown, 'Step Breakdown'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function readSelectedRefactorStep(repoRoot: string): Promise<PlanUnit | null> {
|
|
65
|
+
const refactorPath = resolveCanonicalPath(repoRoot, '.prodify/artifacts/05-refactor.md');
|
|
66
|
+
const markdown = await fs.readFile(refactorPath, 'utf8');
|
|
67
|
+
const section = extractSection(markdown, 'Selected Step');
|
|
68
|
+
if (!section) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const id = /-\s+Step ID:\s+(.+)/.exec(section)?.[1]?.trim() ?? null;
|
|
73
|
+
const description = /-\s+Description:\s+(.+)/.exec(section)?.[1]?.trim() ?? '';
|
|
74
|
+
if (!id) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
id,
|
|
80
|
+
description
|
|
81
|
+
};
|
|
82
|
+
}
|
package/src/core/state.ts
CHANGED
|
@@ -3,6 +3,8 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import { ProdifyError } from './errors.js';
|
|
4
4
|
import { pathExists, writeFileEnsuringDir } from './fs.js';
|
|
5
5
|
import { isRuntimeProfileName, resolveCanonicalPath } from './paths.js';
|
|
6
|
+
import { writeRefactorBaselineSnapshot } from './diff-validator.js';
|
|
7
|
+
import { syncScoreArtifactsForRuntimeState } from '../scoring/model.js';
|
|
6
8
|
import type {
|
|
7
9
|
ExecutionMode,
|
|
8
10
|
FlowStage,
|
|
@@ -261,4 +263,8 @@ export async function readRuntimeState(
|
|
|
261
263
|
export async function writeRuntimeState(repoRoot: string, state: ProdifyState): Promise<void> {
|
|
262
264
|
const statePath = resolveCanonicalPath(repoRoot, '.prodify/state.json');
|
|
263
265
|
await writeFileEnsuringDir(statePath, serializeRuntimeState(state));
|
|
266
|
+
if (state.runtime.current_state === 'refactor_pending') {
|
|
267
|
+
await writeRefactorBaselineSnapshot(repoRoot);
|
|
268
|
+
}
|
|
269
|
+
await syncScoreArtifactsForRuntimeState(repoRoot, state);
|
|
264
270
|
}
|
package/src/core/status.ts
CHANGED
|
@@ -12,8 +12,10 @@ import { readRuntimeState, RUNTIME_STATUS } from './state.js';
|
|
|
12
12
|
import { inspectVersionStatus } from './version-checks.js';
|
|
13
13
|
import { buildBootstrapPrompt, hasManualBootstrapGuidance } from './prompt-builder.js';
|
|
14
14
|
import { getRuntimeProfile } from './targets.js';
|
|
15
|
+
import { readScoreDelta } from '../scoring/model.js';
|
|
15
16
|
import type {
|
|
16
17
|
RuntimeProfileName,
|
|
18
|
+
ScoreDelta,
|
|
17
19
|
RuntimeStateBlock,
|
|
18
20
|
StatusReport,
|
|
19
21
|
VersionInspection,
|
|
@@ -164,6 +166,16 @@ function describeStageValidation(report: StatusReport): string {
|
|
|
164
166
|
: `failed at ${runtime.last_validation.stage}`;
|
|
165
167
|
}
|
|
166
168
|
|
|
169
|
+
function describeImpactScore(scoreDelta: ScoreDelta | null): string {
|
|
170
|
+
if (!scoreDelta) {
|
|
171
|
+
return 'not available';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const threshold = scoreDelta.min_impact_score !== undefined ? `, threshold=${scoreDelta.min_impact_score}` : '';
|
|
175
|
+
const verdict = scoreDelta.passed === undefined ? '' : `, passed=${scoreDelta.passed}`;
|
|
176
|
+
return `${scoreDelta.baseline_score} -> ${scoreDelta.final_score} (delta ${scoreDelta.delta}${threshold}${verdict})`;
|
|
177
|
+
}
|
|
178
|
+
|
|
167
179
|
async function checkManualBootstrapGuidance(repoRoot: string): Promise<boolean> {
|
|
168
180
|
const agentsPath = resolveCanonicalPath(repoRoot, '.prodify/AGENTS.md');
|
|
169
181
|
if (!(await pathExists(agentsPath))) {
|
|
@@ -264,6 +276,7 @@ export async function inspectRepositoryStatus(
|
|
|
264
276
|
bootstrapProfile,
|
|
265
277
|
bootstrapPrompt,
|
|
266
278
|
stageSkillResolution: null,
|
|
279
|
+
scoreDelta: null,
|
|
267
280
|
recommendedNextAction: 'prodify init',
|
|
268
281
|
presetMetadata: preset.metadata
|
|
269
282
|
};
|
|
@@ -281,6 +294,7 @@ export async function inspectRepositoryStatus(
|
|
|
281
294
|
let runtimeState = null;
|
|
282
295
|
let runtimeStateError = null;
|
|
283
296
|
let stageSkillResolution = null;
|
|
297
|
+
let scoreDelta = null;
|
|
284
298
|
|
|
285
299
|
try {
|
|
286
300
|
runtimeState = await readRuntimeState(repoRoot, {
|
|
@@ -291,6 +305,7 @@ export async function inspectRepositoryStatus(
|
|
|
291
305
|
}
|
|
292
306
|
|
|
293
307
|
const manualBootstrapReady = await checkManualBootstrapGuidance(repoRoot);
|
|
308
|
+
scoreDelta = await readScoreDelta(repoRoot);
|
|
294
309
|
const canonicalOk = missingPaths.length === 0;
|
|
295
310
|
if (canonicalOk && contractInventory.ok) {
|
|
296
311
|
const skillStage = runtimeState?.runtime.current_stage
|
|
@@ -328,6 +343,7 @@ export async function inspectRepositoryStatus(
|
|
|
328
343
|
bootstrapProfile,
|
|
329
344
|
bootstrapPrompt,
|
|
330
345
|
stageSkillResolution,
|
|
346
|
+
scoreDelta,
|
|
331
347
|
recommendedNextAction: deriveNextAction({
|
|
332
348
|
initialized,
|
|
333
349
|
canonicalOk,
|
|
@@ -357,6 +373,7 @@ export function renderStatusReport(report: StatusReport): string {
|
|
|
357
373
|
`Skills active: ${describeActiveSkills(report)}`,
|
|
358
374
|
`Execution state: ${describeRuntime(report.runtimeState?.runtime ?? null)}`,
|
|
359
375
|
`Stage validation: ${describeStageValidation(report)}`,
|
|
376
|
+
`Impact score: ${describeImpactScore(report.scoreDelta)}`,
|
|
360
377
|
`Manual bootstrap: ${report.manualBootstrapReady ? 'ready' : 'repair .prodify/AGENTS.md guidance'}`,
|
|
361
378
|
`Bootstrap profile: ${report.bootstrapProfile}`,
|
|
362
379
|
`Bootstrap prompt: ${report.bootstrapPrompt}`,
|