@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.
Files changed (36) hide show
  1. package/.prodify/contracts/architecture.contract.json +9 -1
  2. package/.prodify/contracts/diagnose.contract.json +9 -1
  3. package/.prodify/contracts/plan.contract.json +9 -1
  4. package/.prodify/contracts/refactor.contract.json +13 -2
  5. package/.prodify/contracts/understand.contract.json +9 -1
  6. package/.prodify/contracts/validate.contract.json +11 -2
  7. package/.prodify/contracts-src/refactor.contract.md +7 -0
  8. package/.prodify/contracts-src/validate.contract.md +2 -0
  9. package/assets/presets/default/canonical/contracts-src/refactor.contract.md +7 -0
  10. package/assets/presets/default/canonical/contracts-src/validate.contract.md +2 -0
  11. package/dist/contracts/compiled-schema.js +10 -1
  12. package/dist/contracts/source-schema.js +42 -1
  13. package/dist/core/diff-validator.js +183 -0
  14. package/dist/core/plan-units.js +64 -0
  15. package/dist/core/state.js +6 -0
  16. package/dist/core/status.js +14 -0
  17. package/dist/core/validation.js +87 -9
  18. package/dist/scoring/model.js +94 -213
  19. package/dist/scoring/scoring-engine.js +158 -0
  20. package/docs/diff-validator-design.md +44 -0
  21. package/docs/impact-scoring-design.md +38 -0
  22. package/package.json +1 -1
  23. package/src/contracts/compiled-schema.ts +10 -1
  24. package/src/contracts/source-schema.ts +51 -1
  25. package/src/core/diff-validator.ts +230 -0
  26. package/src/core/plan-units.ts +82 -0
  27. package/src/core/state.ts +6 -0
  28. package/src/core/status.ts +17 -0
  29. package/src/core/validation.ts +136 -15
  30. package/src/scoring/model.ts +101 -250
  31. package/src/scoring/scoring-engine.ts +194 -0
  32. package/src/types.ts +55 -0
  33. package/tests/integration/cli-flows.test.js +1 -0
  34. package/tests/unit/diff-validator.test.js +28 -0
  35. package/tests/unit/scoring.test.js +42 -1
  36. 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": "0445b0239d888c292430f8fe5ddba557813f0e19a6bafdb9c7a7ad7d10c627b1",
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": "10c9f32b2bbed054884a3c9c5ad5f195954ca0c702f14a783835d79a07aca2ed",
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
+ }
@@ -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
  }
@@ -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}`,