@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
|
@@ -38,5 +38,13 @@
|
|
|
38
38
|
"default_skills": [],
|
|
39
39
|
"allowed_skills": [],
|
|
40
40
|
"conditional_skills": []
|
|
41
|
-
}
|
|
41
|
+
},
|
|
42
|
+
"diff_validation_rules": {
|
|
43
|
+
"minimum_files_modified": 0,
|
|
44
|
+
"minimum_lines_changed": 0,
|
|
45
|
+
"must_create_files": false,
|
|
46
|
+
"required_structural_changes": []
|
|
47
|
+
},
|
|
48
|
+
"min_impact_score": 0,
|
|
49
|
+
"enforce_plan_units": false
|
|
42
50
|
}
|
|
@@ -38,5 +38,13 @@
|
|
|
38
38
|
"default_skills": [],
|
|
39
39
|
"allowed_skills": [],
|
|
40
40
|
"conditional_skills": []
|
|
41
|
-
}
|
|
41
|
+
},
|
|
42
|
+
"diff_validation_rules": {
|
|
43
|
+
"minimum_files_modified": 0,
|
|
44
|
+
"minimum_lines_changed": 0,
|
|
45
|
+
"must_create_files": false,
|
|
46
|
+
"required_structural_changes": []
|
|
47
|
+
},
|
|
48
|
+
"min_impact_score": 0,
|
|
49
|
+
"enforce_plan_units": false
|
|
42
50
|
}
|
|
@@ -38,5 +38,13 @@
|
|
|
38
38
|
"default_skills": [],
|
|
39
39
|
"allowed_skills": [],
|
|
40
40
|
"conditional_skills": []
|
|
41
|
-
}
|
|
41
|
+
},
|
|
42
|
+
"diff_validation_rules": {
|
|
43
|
+
"minimum_files_modified": 0,
|
|
44
|
+
"minimum_lines_changed": 0,
|
|
45
|
+
"must_create_files": false,
|
|
46
|
+
"required_structural_changes": []
|
|
47
|
+
},
|
|
48
|
+
"min_impact_score": 0,
|
|
49
|
+
"enforce_plan_units": false
|
|
42
50
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"stage": "refactor",
|
|
5
5
|
"task_id": "05-refactor",
|
|
6
6
|
"source_path": ".prodify/contracts-src/refactor.contract.md",
|
|
7
|
-
"source_hash": "
|
|
7
|
+
"source_hash": "3e752b6d837af065cd8eb623a2cfdd3aa3eaf48e71ed4e4513058af2e8f86f81",
|
|
8
8
|
"required_artifacts": [
|
|
9
9
|
{
|
|
10
10
|
"path": ".prodify/artifacts/05-refactor.md",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"Keep the diff minimal and behavior-preserving unless the plan says otherwise."
|
|
35
35
|
],
|
|
36
36
|
"success_criteria": [
|
|
37
|
+
"The refactor introduces measurable structural improvement.",
|
|
37
38
|
"The selected plan step is implemented fully.",
|
|
38
39
|
"Unrelated files remain untouched."
|
|
39
40
|
],
|
|
@@ -41,5 +42,15 @@
|
|
|
41
42
|
"default_skills": [],
|
|
42
43
|
"allowed_skills": [],
|
|
43
44
|
"conditional_skills": []
|
|
44
|
-
}
|
|
45
|
+
},
|
|
46
|
+
"diff_validation_rules": {
|
|
47
|
+
"minimum_files_modified": 1,
|
|
48
|
+
"minimum_lines_changed": 10,
|
|
49
|
+
"must_create_files": false,
|
|
50
|
+
"required_structural_changes": [
|
|
51
|
+
"module-boundary-created"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"min_impact_score": 0,
|
|
55
|
+
"enforce_plan_units": true
|
|
45
56
|
}
|
|
@@ -38,5 +38,13 @@
|
|
|
38
38
|
"default_skills": [],
|
|
39
39
|
"allowed_skills": [],
|
|
40
40
|
"conditional_skills": []
|
|
41
|
-
}
|
|
41
|
+
},
|
|
42
|
+
"diff_validation_rules": {
|
|
43
|
+
"minimum_files_modified": 0,
|
|
44
|
+
"minimum_lines_changed": 0,
|
|
45
|
+
"must_create_files": false,
|
|
46
|
+
"required_structural_changes": []
|
|
47
|
+
},
|
|
48
|
+
"min_impact_score": 0,
|
|
49
|
+
"enforce_plan_units": false
|
|
42
50
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"stage": "validate",
|
|
5
5
|
"task_id": "06-validate",
|
|
6
6
|
"source_path": ".prodify/contracts-src/validate.contract.md",
|
|
7
|
-
"source_hash": "
|
|
7
|
+
"source_hash": "4e1d5d6b56b147cbd6e0c98d35604e04e96eae0c6ae38353b7181da5359b3469",
|
|
8
8
|
"required_artifacts": [
|
|
9
9
|
{
|
|
10
10
|
"path": ".prodify/artifacts/06-validate.md",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"Validation must follow every refactor step."
|
|
42
42
|
],
|
|
43
43
|
"success_criteria": [
|
|
44
|
+
"The measured impact score exceeds the minimum threshold.",
|
|
44
45
|
"The result is strong enough to gate the next runtime transition.",
|
|
45
46
|
"Validation records whether regressions were found."
|
|
46
47
|
],
|
|
@@ -48,5 +49,13 @@
|
|
|
48
49
|
"default_skills": [],
|
|
49
50
|
"allowed_skills": [],
|
|
50
51
|
"conditional_skills": []
|
|
51
|
-
}
|
|
52
|
+
},
|
|
53
|
+
"diff_validation_rules": {
|
|
54
|
+
"minimum_files_modified": 0,
|
|
55
|
+
"minimum_lines_changed": 0,
|
|
56
|
+
"must_create_files": false,
|
|
57
|
+
"required_structural_changes": []
|
|
58
|
+
},
|
|
59
|
+
"min_impact_score": 1,
|
|
60
|
+
"enforce_plan_units": false
|
|
52
61
|
}
|
|
@@ -20,12 +20,19 @@ allowed_write_roots:
|
|
|
20
20
|
- tests/
|
|
21
21
|
forbidden_writes:
|
|
22
22
|
- .prodify/contracts/
|
|
23
|
+
minimum_files_modified: 1
|
|
24
|
+
minimum_lines_changed: 10
|
|
25
|
+
must_create_files: false
|
|
26
|
+
required_structural_changes:
|
|
27
|
+
- module-boundary-created
|
|
28
|
+
enforce_plan_units: true
|
|
23
29
|
policy_rules:
|
|
24
30
|
- Execute exactly one selected step.
|
|
25
31
|
- Keep the diff minimal and behavior-preserving unless the plan says otherwise.
|
|
26
32
|
success_criteria:
|
|
27
33
|
- The selected plan step is implemented fully.
|
|
28
34
|
- Unrelated files remain untouched.
|
|
35
|
+
- The refactor introduces measurable structural improvement.
|
|
29
36
|
---
|
|
30
37
|
# Refactor Contract
|
|
31
38
|
|
|
@@ -23,12 +23,14 @@ allowed_write_roots:
|
|
|
23
23
|
- .prodify/metrics/
|
|
24
24
|
forbidden_writes:
|
|
25
25
|
- src/
|
|
26
|
+
min_impact_score: 1
|
|
26
27
|
policy_rules:
|
|
27
28
|
- Validation must follow every refactor step.
|
|
28
29
|
- Critical regressions block forward progress.
|
|
29
30
|
success_criteria:
|
|
30
31
|
- Validation records whether regressions were found.
|
|
31
32
|
- The result is strong enough to gate the next runtime transition.
|
|
33
|
+
- The measured impact score exceeds the minimum threshold.
|
|
32
34
|
---
|
|
33
35
|
# Validate Contract
|
|
34
36
|
|
|
@@ -20,12 +20,19 @@ allowed_write_roots:
|
|
|
20
20
|
- tests/
|
|
21
21
|
forbidden_writes:
|
|
22
22
|
- .prodify/contracts/
|
|
23
|
+
minimum_files_modified: 1
|
|
24
|
+
minimum_lines_changed: 10
|
|
25
|
+
must_create_files: false
|
|
26
|
+
required_structural_changes:
|
|
27
|
+
- module-boundary-created
|
|
28
|
+
enforce_plan_units: true
|
|
23
29
|
policy_rules:
|
|
24
30
|
- Execute exactly one selected step.
|
|
25
31
|
- Keep the diff minimal and behavior-preserving unless the plan says otherwise.
|
|
26
32
|
success_criteria:
|
|
27
33
|
- The selected plan step is implemented fully.
|
|
28
34
|
- Unrelated files remain untouched.
|
|
35
|
+
- The refactor introduces measurable structural improvement.
|
|
29
36
|
skill_routing:
|
|
30
37
|
default_skills:
|
|
31
38
|
- refactoring-method
|
|
@@ -23,12 +23,14 @@ allowed_write_roots:
|
|
|
23
23
|
- .prodify/metrics/
|
|
24
24
|
forbidden_writes:
|
|
25
25
|
- src/
|
|
26
|
+
min_impact_score: 1
|
|
26
27
|
policy_rules:
|
|
27
28
|
- Validation must follow every refactor step.
|
|
28
29
|
- Critical regressions block forward progress.
|
|
29
30
|
success_criteria:
|
|
30
31
|
- Validation records whether regressions were found.
|
|
31
32
|
- The result is strong enough to gate the next runtime transition.
|
|
33
|
+
- The measured impact score exceeds the minimum threshold.
|
|
32
34
|
skill_routing:
|
|
33
35
|
default_skills:
|
|
34
36
|
- test-hardening
|
|
@@ -15,6 +15,9 @@ export function validateCompiledContractShape(contract) {
|
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
const record = contract;
|
|
18
|
+
const diffValidation = typeof record.diff_validation_rules === 'object' && record.diff_validation_rules !== null
|
|
19
|
+
? record.diff_validation_rules
|
|
20
|
+
: {};
|
|
18
21
|
return normalizeSourceContractDocument({
|
|
19
22
|
document: {
|
|
20
23
|
frontmatter: {
|
|
@@ -27,7 +30,13 @@ export function validateCompiledContractShape(contract) {
|
|
|
27
30
|
forbidden_writes: record.forbidden_writes,
|
|
28
31
|
policy_rules: record.policy_rules,
|
|
29
32
|
success_criteria: record.success_criteria,
|
|
30
|
-
skill_routing: record.skill_routing
|
|
33
|
+
skill_routing: record.skill_routing,
|
|
34
|
+
minimum_files_modified: diffValidation.minimum_files_modified ?? record.minimum_files_modified,
|
|
35
|
+
minimum_lines_changed: diffValidation.minimum_lines_changed ?? record.minimum_lines_changed,
|
|
36
|
+
must_create_files: diffValidation.must_create_files ?? record.must_create_files,
|
|
37
|
+
required_structural_changes: diffValidation.required_structural_changes ?? record.required_structural_changes,
|
|
38
|
+
min_impact_score: record.min_impact_score,
|
|
39
|
+
enforce_plan_units: record.enforce_plan_units
|
|
31
40
|
},
|
|
32
41
|
body: 'compiled-contract'
|
|
33
42
|
},
|
|
@@ -26,6 +26,39 @@ function asOptionalStringArray(value, fieldName) {
|
|
|
26
26
|
}
|
|
27
27
|
return asStringArray(value, fieldName);
|
|
28
28
|
}
|
|
29
|
+
function asOptionalNonNegativeInteger(value, fieldName, fallback = 0) {
|
|
30
|
+
if (value === undefined || value === null || value === '') {
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
34
|
+
throw new ProdifyError(`Contract frontmatter field "${fieldName}" must be a non-negative integer.`, {
|
|
35
|
+
code: 'CONTRACT_SCHEMA_INVALID'
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function asOptionalNonNegativeNumber(value, fieldName, fallback = 0) {
|
|
41
|
+
if (value === undefined || value === null || value === '') {
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
if (typeof value !== 'number' || Number.isNaN(value) || value < 0) {
|
|
45
|
+
throw new ProdifyError(`Contract frontmatter field "${fieldName}" must be a non-negative number.`, {
|
|
46
|
+
code: 'CONTRACT_SCHEMA_INVALID'
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
function asOptionalBoolean(value, fieldName, fallback = false) {
|
|
52
|
+
if (value === undefined || value === null || value === '') {
|
|
53
|
+
return fallback;
|
|
54
|
+
}
|
|
55
|
+
if (typeof value !== 'boolean') {
|
|
56
|
+
throw new ProdifyError(`Contract frontmatter field "${fieldName}" must be a boolean.`, {
|
|
57
|
+
code: 'CONTRACT_SCHEMA_INVALID'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
29
62
|
function asStage(value) {
|
|
30
63
|
const stage = asString(value, 'stage');
|
|
31
64
|
if (!CONTRACT_STAGE_NAMES.includes(stage)) {
|
|
@@ -106,6 +139,14 @@ export function normalizeSourceContractDocument(options) {
|
|
|
106
139
|
: [],
|
|
107
140
|
policy_rules: asStringArray(document.frontmatter.policy_rules, 'policy_rules'),
|
|
108
141
|
success_criteria: asStringArray(document.frontmatter.success_criteria, 'success_criteria'),
|
|
109
|
-
skill_routing: normalizeStageSkillRouting(document.frontmatter.skill_routing)
|
|
142
|
+
skill_routing: normalizeStageSkillRouting(document.frontmatter.skill_routing),
|
|
143
|
+
diff_validation_rules: {
|
|
144
|
+
minimum_files_modified: asOptionalNonNegativeInteger(document.frontmatter.minimum_files_modified, 'minimum_files_modified'),
|
|
145
|
+
minimum_lines_changed: asOptionalNonNegativeInteger(document.frontmatter.minimum_lines_changed, 'minimum_lines_changed'),
|
|
146
|
+
must_create_files: asOptionalBoolean(document.frontmatter.must_create_files, 'must_create_files'),
|
|
147
|
+
required_structural_changes: asOptionalStringArray(document.frontmatter.required_structural_changes, 'required_structural_changes')
|
|
148
|
+
},
|
|
149
|
+
min_impact_score: asOptionalNonNegativeNumber(document.frontmatter.min_impact_score, 'min_impact_score'),
|
|
150
|
+
enforce_plan_units: asOptionalBoolean(document.frontmatter.enforce_plan_units, 'enforce_plan_units')
|
|
110
151
|
};
|
|
111
152
|
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { listFilesRecursive, pathExists, writeFileEnsuringDir } from './fs.js';
|
|
4
|
+
import { normalizeRepoRelativePath, resolveRepoPath } from './paths.js';
|
|
5
|
+
const DIFF_SNAPSHOT_SCHEMA_VERSION = '1';
|
|
6
|
+
const BASELINE_SNAPSHOT_PATH = '.prodify/metrics/refactor-baseline.snapshot.json';
|
|
7
|
+
const TRACKED_PREFIXES = ['src/', 'tests/', 'assets/'];
|
|
8
|
+
const TRACKED_FILE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json', '.md', '.py', '.cs', '.css', '.scss', '.html']);
|
|
9
|
+
const LAYER_DIRECTORY_NAMES = new Set(['application', 'domain', 'services', 'service', 'modules', 'module', 'adapters', 'infrastructure', 'core']);
|
|
10
|
+
function isTrackedPath(relativePath) {
|
|
11
|
+
const normalized = normalizeRepoRelativePath(relativePath);
|
|
12
|
+
if (!TRACKED_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return TRACKED_FILE_EXTENSIONS.has(path.extname(normalized));
|
|
16
|
+
}
|
|
17
|
+
function normalizeWhitespace(content) {
|
|
18
|
+
return content
|
|
19
|
+
.replace(/\s+/g, '')
|
|
20
|
+
.trim();
|
|
21
|
+
}
|
|
22
|
+
function toMap(snapshot) {
|
|
23
|
+
return new Map(snapshot.files.map((file) => [file.path, file]));
|
|
24
|
+
}
|
|
25
|
+
function diffLines(before, after) {
|
|
26
|
+
const beforeLines = before.replace(/\r\n/g, '\n').split('\n');
|
|
27
|
+
const afterLines = after.replace(/\r\n/g, '\n').split('\n');
|
|
28
|
+
const rows = beforeLines.length;
|
|
29
|
+
const cols = afterLines.length;
|
|
30
|
+
const dp = Array.from({ length: rows + 1 }, () => Array(cols + 1).fill(0));
|
|
31
|
+
for (let row = rows - 1; row >= 0; row -= 1) {
|
|
32
|
+
for (let col = cols - 1; col >= 0; col -= 1) {
|
|
33
|
+
if (beforeLines[row] === afterLines[col]) {
|
|
34
|
+
dp[row][col] = dp[row + 1][col + 1] + 1;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
dp[row][col] = Math.max(dp[row + 1][col], dp[row][col + 1]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const common = dp[0][0];
|
|
42
|
+
return {
|
|
43
|
+
added: Math.max(0, afterLines.length - common),
|
|
44
|
+
removed: Math.max(0, beforeLines.length - common)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function collectDirectories(paths) {
|
|
48
|
+
return [...new Set(paths
|
|
49
|
+
.map((relativePath) => path.posix.dirname(relativePath))
|
|
50
|
+
.filter((directory) => directory !== '.' && directory !== ''))]
|
|
51
|
+
.sort((left, right) => left.localeCompare(right));
|
|
52
|
+
}
|
|
53
|
+
function detectStructuralChanges(options) {
|
|
54
|
+
const newDirectories = collectDirectories(options.addedPaths);
|
|
55
|
+
const newLayers = newDirectories.filter((directory) => LAYER_DIRECTORY_NAMES.has(path.posix.basename(directory)));
|
|
56
|
+
const filesWithReducedResponsibility = options.modifiedPaths
|
|
57
|
+
.filter((relativePath) => (options.removedLineCounts.get(relativePath) ?? 0) > 0)
|
|
58
|
+
.sort((left, right) => left.localeCompare(right));
|
|
59
|
+
const newModules = options.addedPaths
|
|
60
|
+
.filter((relativePath) => relativePath.startsWith('src/'))
|
|
61
|
+
.sort((left, right) => left.localeCompare(right));
|
|
62
|
+
const flags = new Set();
|
|
63
|
+
if (newDirectories.length > 0) {
|
|
64
|
+
flags.add('new-directories');
|
|
65
|
+
}
|
|
66
|
+
if (newLayers.length > 0) {
|
|
67
|
+
flags.add('new-layer-directories');
|
|
68
|
+
flags.add('module-boundary-created');
|
|
69
|
+
}
|
|
70
|
+
if (newModules.length > 0) {
|
|
71
|
+
flags.add('new-modules');
|
|
72
|
+
flags.add('module-boundary-created');
|
|
73
|
+
}
|
|
74
|
+
if (filesWithReducedResponsibility.length > 0) {
|
|
75
|
+
flags.add('responsibility-reduced');
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
new_directories: newDirectories,
|
|
79
|
+
new_layer_directories: newLayers,
|
|
80
|
+
files_with_reduced_responsibility: filesWithReducedResponsibility,
|
|
81
|
+
new_modules: newModules,
|
|
82
|
+
structural_change_flags: [...flags].sort((left, right) => left.localeCompare(right))
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function serializeSnapshot(snapshot) {
|
|
86
|
+
return `${JSON.stringify(snapshot, null, 2)}\n`;
|
|
87
|
+
}
|
|
88
|
+
export async function captureRepoSnapshot(repoRoot) {
|
|
89
|
+
const repoFiles = await listFilesRecursive(repoRoot);
|
|
90
|
+
const files = [];
|
|
91
|
+
for (const file of repoFiles) {
|
|
92
|
+
if (!isTrackedPath(file.relativePath)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
files.push({
|
|
96
|
+
path: normalizeRepoRelativePath(file.relativePath),
|
|
97
|
+
content: await fs.readFile(file.fullPath, 'utf8')
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
files.sort((left, right) => left.path.localeCompare(right.path));
|
|
101
|
+
return {
|
|
102
|
+
schema_version: DIFF_SNAPSHOT_SCHEMA_VERSION,
|
|
103
|
+
files
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export async function writeRefactorBaselineSnapshot(repoRoot) {
|
|
107
|
+
const snapshot = await captureRepoSnapshot(repoRoot);
|
|
108
|
+
await writeFileEnsuringDir(resolveRepoPath(repoRoot, BASELINE_SNAPSHOT_PATH), serializeSnapshot(snapshot));
|
|
109
|
+
return snapshot;
|
|
110
|
+
}
|
|
111
|
+
export async function readRefactorBaselineSnapshot(repoRoot) {
|
|
112
|
+
const baselinePath = resolveRepoPath(repoRoot, BASELINE_SNAPSHOT_PATH);
|
|
113
|
+
if (!(await pathExists(baselinePath))) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return JSON.parse(await fs.readFile(baselinePath, 'utf8'));
|
|
117
|
+
}
|
|
118
|
+
export function diffSnapshots(before, after) {
|
|
119
|
+
const beforeMap = toMap(before);
|
|
120
|
+
const afterMap = toMap(after);
|
|
121
|
+
const allPaths = [...new Set([...beforeMap.keys(), ...afterMap.keys()])].sort((left, right) => left.localeCompare(right));
|
|
122
|
+
const modifiedPaths = [];
|
|
123
|
+
const addedPaths = [];
|
|
124
|
+
const deletedPaths = [];
|
|
125
|
+
const formattingOnlyPaths = [];
|
|
126
|
+
let linesAdded = 0;
|
|
127
|
+
let linesRemoved = 0;
|
|
128
|
+
const removedLineCounts = new Map();
|
|
129
|
+
for (const relativePath of allPaths) {
|
|
130
|
+
const beforeFile = beforeMap.get(relativePath);
|
|
131
|
+
const afterFile = afterMap.get(relativePath);
|
|
132
|
+
if (!beforeFile && afterFile) {
|
|
133
|
+
addedPaths.push(relativePath);
|
|
134
|
+
linesAdded += afterFile.content.replace(/\r\n/g, '\n').split('\n').length;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (beforeFile && !afterFile) {
|
|
138
|
+
deletedPaths.push(relativePath);
|
|
139
|
+
const removed = beforeFile.content.replace(/\r\n/g, '\n').split('\n').length;
|
|
140
|
+
linesRemoved += removed;
|
|
141
|
+
removedLineCounts.set(relativePath, removed);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!beforeFile || !afterFile || beforeFile.content === afterFile.content) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
modifiedPaths.push(relativePath);
|
|
148
|
+
if (normalizeWhitespace(beforeFile.content) === normalizeWhitespace(afterFile.content)) {
|
|
149
|
+
formattingOnlyPaths.push(relativePath);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const lineDiff = diffLines(beforeFile.content, afterFile.content);
|
|
153
|
+
linesAdded += lineDiff.added;
|
|
154
|
+
linesRemoved += lineDiff.removed;
|
|
155
|
+
removedLineCounts.set(relativePath, lineDiff.removed);
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
filesModified: modifiedPaths.length,
|
|
159
|
+
filesAdded: addedPaths.length,
|
|
160
|
+
filesDeleted: deletedPaths.length,
|
|
161
|
+
linesAdded,
|
|
162
|
+
linesRemoved,
|
|
163
|
+
modifiedPaths,
|
|
164
|
+
addedPaths,
|
|
165
|
+
deletedPaths,
|
|
166
|
+
formattingOnlyPaths,
|
|
167
|
+
structuralChanges: detectStructuralChanges({
|
|
168
|
+
addedPaths,
|
|
169
|
+
modifiedPaths,
|
|
170
|
+
beforeMap,
|
|
171
|
+
afterMap,
|
|
172
|
+
removedLineCounts
|
|
173
|
+
})
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export async function diffAgainstRefactorBaseline(repoRoot) {
|
|
177
|
+
const baseline = await readRefactorBaselineSnapshot(repoRoot);
|
|
178
|
+
if (!baseline) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const current = await captureRepoSnapshot(repoRoot);
|
|
182
|
+
return diffSnapshots(baseline, current);
|
|
183
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { resolveCanonicalPath } from './paths.js';
|
|
3
|
+
function extractSection(markdown, heading) {
|
|
4
|
+
const normalized = markdown.replace(/\r\n/g, '\n');
|
|
5
|
+
const match = new RegExp(`^##\\s+${heading}\\s*$([\\s\\S]*?)(?=^##\\s+|\\Z)`, 'm').exec(normalized);
|
|
6
|
+
return match?.[1]?.trim() ?? '';
|
|
7
|
+
}
|
|
8
|
+
function normalizePlanUnits(section) {
|
|
9
|
+
const lines = section.split('\n');
|
|
10
|
+
const units = [];
|
|
11
|
+
let currentId = null;
|
|
12
|
+
let currentDescription = '';
|
|
13
|
+
for (const rawLine of lines) {
|
|
14
|
+
const line = rawLine.trim();
|
|
15
|
+
const idMatch = /^-\s+Step ID:\s+(.+)$/.exec(line);
|
|
16
|
+
if (idMatch) {
|
|
17
|
+
if (currentId) {
|
|
18
|
+
units.push({
|
|
19
|
+
id: currentId,
|
|
20
|
+
description: currentDescription
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
currentId = idMatch[1].trim();
|
|
24
|
+
currentDescription = '';
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (!currentId) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const descriptionMatch = /^-\s+Description:\s+(.+)$/.exec(line);
|
|
31
|
+
if (descriptionMatch) {
|
|
32
|
+
currentDescription = descriptionMatch[1].trim();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (currentId) {
|
|
36
|
+
units.push({
|
|
37
|
+
id: currentId,
|
|
38
|
+
description: currentDescription
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return units;
|
|
42
|
+
}
|
|
43
|
+
export async function readPlanUnits(repoRoot) {
|
|
44
|
+
const planPath = resolveCanonicalPath(repoRoot, '.prodify/artifacts/04-plan.md');
|
|
45
|
+
const markdown = await fs.readFile(planPath, 'utf8');
|
|
46
|
+
return normalizePlanUnits(extractSection(markdown, 'Step Breakdown'));
|
|
47
|
+
}
|
|
48
|
+
export async function readSelectedRefactorStep(repoRoot) {
|
|
49
|
+
const refactorPath = resolveCanonicalPath(repoRoot, '.prodify/artifacts/05-refactor.md');
|
|
50
|
+
const markdown = await fs.readFile(refactorPath, 'utf8');
|
|
51
|
+
const section = extractSection(markdown, 'Selected Step');
|
|
52
|
+
if (!section) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const id = /-\s+Step ID:\s+(.+)/.exec(section)?.[1]?.trim() ?? null;
|
|
56
|
+
const description = /-\s+Description:\s+(.+)/.exec(section)?.[1]?.trim() ?? '';
|
|
57
|
+
if (!id) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
id,
|
|
62
|
+
description
|
|
63
|
+
};
|
|
64
|
+
}
|
package/dist/core/state.js
CHANGED
|
@@ -2,6 +2,8 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import { ProdifyError } from './errors.js';
|
|
3
3
|
import { pathExists, writeFileEnsuringDir } from './fs.js';
|
|
4
4
|
import { resolveCanonicalPath } from './paths.js';
|
|
5
|
+
import { writeRefactorBaselineSnapshot } from './diff-validator.js';
|
|
6
|
+
import { syncScoreArtifactsForRuntimeState } from '../scoring/model.js';
|
|
5
7
|
export const RUNTIME_STATE_SCHEMA_VERSION = '2';
|
|
6
8
|
export const RUNTIME_STATUS = {
|
|
7
9
|
NOT_BOOTSTRAPPED: 'not_bootstrapped',
|
|
@@ -217,4 +219,8 @@ export async function readRuntimeState(repoRoot, { allowMissing = false, presetM
|
|
|
217
219
|
export async function writeRuntimeState(repoRoot, state) {
|
|
218
220
|
const statePath = resolveCanonicalPath(repoRoot, '.prodify/state.json');
|
|
219
221
|
await writeFileEnsuringDir(statePath, serializeRuntimeState(state));
|
|
222
|
+
if (state.runtime.current_state === 'refactor_pending') {
|
|
223
|
+
await writeRefactorBaselineSnapshot(repoRoot);
|
|
224
|
+
}
|
|
225
|
+
await syncScoreArtifactsForRuntimeState(repoRoot, state);
|
|
220
226
|
}
|
package/dist/core/status.js
CHANGED
|
@@ -11,6 +11,7 @@ import { readRuntimeState, RUNTIME_STATUS } from './state.js';
|
|
|
11
11
|
import { inspectVersionStatus } from './version-checks.js';
|
|
12
12
|
import { buildBootstrapPrompt, hasManualBootstrapGuidance } from './prompt-builder.js';
|
|
13
13
|
import { getRuntimeProfile } from './targets.js';
|
|
14
|
+
import { readScoreDelta } from '../scoring/model.js';
|
|
14
15
|
function describeCanonicalHealth(missingPaths) {
|
|
15
16
|
if (missingPaths.length === 0) {
|
|
16
17
|
return 'healthy';
|
|
@@ -126,6 +127,14 @@ function describeStageValidation(report) {
|
|
|
126
127
|
? `last pass at ${runtime.last_validation.stage} (contract ${runtime.last_validation.contract_version})`
|
|
127
128
|
: `failed at ${runtime.last_validation.stage}`;
|
|
128
129
|
}
|
|
130
|
+
function describeImpactScore(scoreDelta) {
|
|
131
|
+
if (!scoreDelta) {
|
|
132
|
+
return 'not available';
|
|
133
|
+
}
|
|
134
|
+
const threshold = scoreDelta.min_impact_score !== undefined ? `, threshold=${scoreDelta.min_impact_score}` : '';
|
|
135
|
+
const verdict = scoreDelta.passed === undefined ? '' : `, passed=${scoreDelta.passed}`;
|
|
136
|
+
return `${scoreDelta.baseline_score} -> ${scoreDelta.final_score} (delta ${scoreDelta.delta}${threshold}${verdict})`;
|
|
137
|
+
}
|
|
129
138
|
async function checkManualBootstrapGuidance(repoRoot) {
|
|
130
139
|
const agentsPath = resolveCanonicalPath(repoRoot, '.prodify/AGENTS.md');
|
|
131
140
|
if (!(await pathExists(agentsPath))) {
|
|
@@ -192,6 +201,7 @@ export async function inspectRepositoryStatus(repoRoot, options = {}) {
|
|
|
192
201
|
bootstrapProfile,
|
|
193
202
|
bootstrapPrompt,
|
|
194
203
|
stageSkillResolution: null,
|
|
204
|
+
scoreDelta: null,
|
|
195
205
|
recommendedNextAction: 'prodify init',
|
|
196
206
|
presetMetadata: preset.metadata
|
|
197
207
|
};
|
|
@@ -207,6 +217,7 @@ export async function inspectRepositoryStatus(repoRoot, options = {}) {
|
|
|
207
217
|
let runtimeState = null;
|
|
208
218
|
let runtimeStateError = null;
|
|
209
219
|
let stageSkillResolution = null;
|
|
220
|
+
let scoreDelta = null;
|
|
210
221
|
try {
|
|
211
222
|
runtimeState = await readRuntimeState(repoRoot, {
|
|
212
223
|
presetMetadata: preset.metadata
|
|
@@ -216,6 +227,7 @@ export async function inspectRepositoryStatus(repoRoot, options = {}) {
|
|
|
216
227
|
runtimeStateError = error instanceof Error ? error : new Error(String(error));
|
|
217
228
|
}
|
|
218
229
|
const manualBootstrapReady = await checkManualBootstrapGuidance(repoRoot);
|
|
230
|
+
scoreDelta = await readScoreDelta(repoRoot);
|
|
219
231
|
const canonicalOk = missingPaths.length === 0;
|
|
220
232
|
if (canonicalOk && contractInventory.ok) {
|
|
221
233
|
const skillStage = runtimeState?.runtime.current_stage
|
|
@@ -252,6 +264,7 @@ export async function inspectRepositoryStatus(repoRoot, options = {}) {
|
|
|
252
264
|
bootstrapProfile,
|
|
253
265
|
bootstrapPrompt,
|
|
254
266
|
stageSkillResolution,
|
|
267
|
+
scoreDelta,
|
|
255
268
|
recommendedNextAction: deriveNextAction({
|
|
256
269
|
initialized,
|
|
257
270
|
canonicalOk,
|
|
@@ -280,6 +293,7 @@ export function renderStatusReport(report) {
|
|
|
280
293
|
`Skills active: ${describeActiveSkills(report)}`,
|
|
281
294
|
`Execution state: ${describeRuntime(report.runtimeState?.runtime ?? null)}`,
|
|
282
295
|
`Stage validation: ${describeStageValidation(report)}`,
|
|
296
|
+
`Impact score: ${describeImpactScore(report.scoreDelta)}`,
|
|
283
297
|
`Manual bootstrap: ${report.manualBootstrapReady ? 'ready' : 'repair .prodify/AGENTS.md guidance'}`,
|
|
284
298
|
`Bootstrap profile: ${report.bootstrapProfile}`,
|
|
285
299
|
`Bootstrap prompt: ${report.bootstrapPrompt}`,
|