@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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urielsh/prodify",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "license": "Apache-2.0",
5
5
  "private": false,
6
6
  "type": "module",
@@ -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
  }
@@ -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}`,