@polymorphism-tech/morph-spec 4.8.6 → 4.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/morph-spec.js +22 -1
- package/bin/task-manager.cjs +120 -16
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents.json +1854 -1815
- package/framework/hooks/claude-code/pre-compact/save-morph-context.js +141 -23
- package/framework/hooks/claude-code/statusline.py +304 -280
- package/framework/hooks/claude-code/statusline.sh +6 -2
- package/framework/hooks/claude-code/stop/validate-completion.js +70 -23
- package/framework/hooks/dev/guard-version-numbers.js +1 -1
- package/framework/skills/level-0-meta/morph-init/SKILL.md +44 -6
- package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +67 -16
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +77 -7
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +114 -50
- package/framework/skills/level-1-workflows/phase-implement/SKILL.md +139 -1
- package/framework/skills/level-1-workflows/phase-setup/SKILL.md +29 -6
- package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +4 -3
- package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +944 -933
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +429 -0
- package/framework/templates/REGISTRY.json +1909 -1888
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +282 -0
- package/package.json +1 -1
- package/src/commands/agents/dispatch-agents.js +430 -0
- package/src/commands/agents/index.js +2 -1
- package/src/commands/project/doctor.js +137 -2
- package/src/commands/state/state.js +20 -4
- package/src/commands/templates/generate-contracts.js +445 -0
- package/src/commands/templates/index.js +1 -0
- package/src/lib/validators/validation-runner.js +19 -7
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MORPH-SPEC Generate Contracts Command
|
|
3
|
+
*
|
|
4
|
+
* Reads schema-analysis.md and generates contracts.cs with REAL field names and types.
|
|
5
|
+
* Bridges the gap between schema analysis and contract template rendering (Problem 5 fix).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* morph-spec generate contracts <feature>
|
|
9
|
+
* morph-spec generate contracts <feature> --dry-run
|
|
10
|
+
* morph-spec generate contracts <feature> --output <path>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
14
|
+
import { join, dirname } from 'path';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import { logger } from '../../utils/logger.js';
|
|
17
|
+
import { markOutput } from '../../core/state/state-manager.js';
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Type Mapping — SQL/TypeScript → C#
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
const TYPE_MAP = {
|
|
24
|
+
// String
|
|
25
|
+
'varchar': 'string', 'text': 'string', 'character varying': 'string',
|
|
26
|
+
'char': 'string', 'nvarchar': 'string', 'ntext': 'string', 'string': 'string',
|
|
27
|
+
'character': 'string', 'name': 'string', 'citext': 'string',
|
|
28
|
+
// UUID
|
|
29
|
+
'uuid': 'Guid', 'uniqueidentifier': 'Guid',
|
|
30
|
+
// Integer
|
|
31
|
+
'int': 'int', 'integer': 'int', 'int4': 'int', 'number': 'int',
|
|
32
|
+
'bigint': 'long', 'int8': 'long', 'smallint': 'short', 'int2': 'short',
|
|
33
|
+
'serial': 'int', 'bigserial': 'long', 'oid': 'int',
|
|
34
|
+
// Decimal
|
|
35
|
+
'decimal': 'decimal', 'numeric': 'decimal', 'money': 'decimal',
|
|
36
|
+
'float': 'double', 'double precision': 'double', 'real': 'float',
|
|
37
|
+
'float4': 'float', 'float8': 'double',
|
|
38
|
+
// Boolean
|
|
39
|
+
'boolean': 'bool', 'bool': 'bool', 'bit': 'bool',
|
|
40
|
+
// DateTime
|
|
41
|
+
'timestamp': 'DateTime', 'timestamp with time zone': 'DateTimeOffset',
|
|
42
|
+
'timestamp without time zone': 'DateTime', 'timestamptz': 'DateTimeOffset',
|
|
43
|
+
'datetime': 'DateTime', 'datetime2': 'DateTime', 'datetimeoffset': 'DateTimeOffset',
|
|
44
|
+
'date': 'DateOnly', 'time': 'TimeOnly', 'timetz': 'TimeOnly',
|
|
45
|
+
// JSON
|
|
46
|
+
'jsonb': 'JsonElement', 'json': 'JsonElement',
|
|
47
|
+
// Binary
|
|
48
|
+
'bytea': 'byte[]', 'binary': 'byte[]', 'varbinary': 'byte[]',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Map a raw SQL/TS type string to a C# type.
|
|
53
|
+
* Non-nullable value types get ? suffix when nullable is true.
|
|
54
|
+
*/
|
|
55
|
+
function toCSharpType(rawType, nullable) {
|
|
56
|
+
const lower = rawType.toLowerCase().trim();
|
|
57
|
+
const csType = TYPE_MAP[lower] ?? 'string';
|
|
58
|
+
const isReferenceType = csType === 'string' || csType === 'byte[]' || csType === 'JsonElement';
|
|
59
|
+
const suffix = nullable && !isReferenceType ? '?' : '';
|
|
60
|
+
return csType + suffix;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Convert snake_case / kebab-case / space-separated to PascalCase.
|
|
65
|
+
*/
|
|
66
|
+
function toPascalCase(str) {
|
|
67
|
+
return str
|
|
68
|
+
.split(/[_\s-]+/)
|
|
69
|
+
.map(w => (w ? w[0].toUpperCase() + w.slice(1).toLowerCase() : ''))
|
|
70
|
+
.join('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Derive a C# entity class name from a table name.
|
|
75
|
+
* Strips a trailing plural 's' where safe (leads → Lead, users → User).
|
|
76
|
+
*/
|
|
77
|
+
function toEntityName(tableName) {
|
|
78
|
+
const pascal = toPascalCase(tableName);
|
|
79
|
+
// Simple singularization: strip trailing s unless double-s, -us, -ss pattern
|
|
80
|
+
if (pascal.endsWith('s') && !pascal.endsWith('ss') && !pascal.endsWith('us') && pascal.length > 3) {
|
|
81
|
+
return pascal.slice(0, -1);
|
|
82
|
+
}
|
|
83
|
+
return pascal;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// Schema-Analysis Parser
|
|
88
|
+
// ============================================================================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse schema-analysis.md to extract table definitions with column metadata.
|
|
92
|
+
*
|
|
93
|
+
* Handles the standard MORPH-SPEC template output:
|
|
94
|
+
* ### Table: <name>
|
|
95
|
+
* **Columns:**
|
|
96
|
+
* | Column Name | Type | Nullable | Default | Notes |
|
|
97
|
+
* |------------|------|----------|---------|-------|
|
|
98
|
+
* | id | uuid | NO | gen..() | PK |
|
|
99
|
+
*
|
|
100
|
+
* @param {string} content - Raw file content of schema-analysis.md
|
|
101
|
+
* @returns {Array<{name: string, columns: Array<{name, type, nullable, notes}>}>}
|
|
102
|
+
*/
|
|
103
|
+
export function parseSchemaAnalysisMd(content) {
|
|
104
|
+
const tables = [];
|
|
105
|
+
|
|
106
|
+
// Locate all "### Table:" headings and slice between them
|
|
107
|
+
const headingRe = /^### Table:/gm;
|
|
108
|
+
const positions = [];
|
|
109
|
+
let m;
|
|
110
|
+
while ((m = headingRe.exec(content)) !== null) {
|
|
111
|
+
positions.push(m.index);
|
|
112
|
+
}
|
|
113
|
+
if (positions.length === 0) return tables;
|
|
114
|
+
|
|
115
|
+
const sections = positions.map((pos, i) => {
|
|
116
|
+
const end = positions[i + 1] ?? content.length;
|
|
117
|
+
return content.slice(pos, end);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
for (const section of sections) {
|
|
121
|
+
const nameMatch = section.match(/^### Table:\s*(\S+)/);
|
|
122
|
+
if (!nameMatch) continue;
|
|
123
|
+
const tableName = nameMatch[1].replace(/[*`]/g, ''); // strip markdown decoration (preserve underscores)
|
|
124
|
+
|
|
125
|
+
// Find **Columns:** subsection
|
|
126
|
+
const colIdx = section.indexOf('**Columns:**');
|
|
127
|
+
if (colIdx === -1) continue;
|
|
128
|
+
|
|
129
|
+
const afterColumns = section.slice(colIdx + '**Columns:**'.length);
|
|
130
|
+
const columns = [];
|
|
131
|
+
let headerSeen = false;
|
|
132
|
+
|
|
133
|
+
for (const line of afterColumns.split('\n')) {
|
|
134
|
+
const trimmed = line.trim();
|
|
135
|
+
|
|
136
|
+
// Stop at next sub-section (**Relationships:**, **Indexes:**, etc.)
|
|
137
|
+
if (trimmed.startsWith('**') && trimmed.endsWith('**')) break;
|
|
138
|
+
if (!trimmed.startsWith('|')) continue;
|
|
139
|
+
|
|
140
|
+
// Split markdown table row into cells
|
|
141
|
+
const cells = trimmed.split('|').slice(1, -1).map(c => c.trim());
|
|
142
|
+
if (cells.length < 2) continue;
|
|
143
|
+
|
|
144
|
+
// Skip header row
|
|
145
|
+
if (!headerSeen && cells[0] === 'Column Name') {
|
|
146
|
+
headerSeen = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Skip separator rows (---|---| etc.)
|
|
151
|
+
if (cells[0].match(/^-+:?$/) || cells[0].match(/^:?-+$/)) continue;
|
|
152
|
+
|
|
153
|
+
if (cells[0]) {
|
|
154
|
+
columns.push({
|
|
155
|
+
name: cells[0],
|
|
156
|
+
type: cells[1] || 'string',
|
|
157
|
+
nullable: (cells[2] || 'NO').toUpperCase() === 'YES',
|
|
158
|
+
default: cells[3] && cells[3] !== '-' ? cells[3] : null,
|
|
159
|
+
notes: cells[4] || '',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (columns.length > 0) {
|
|
165
|
+
tables.push({ name: tableName, columns });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return tables;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// Contract Generator
|
|
174
|
+
// ============================================================================
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Determine whether a column is a primary key or auto-managed field
|
|
178
|
+
* (should be excluded from Create/Update requests).
|
|
179
|
+
*/
|
|
180
|
+
function isAutoManagedField(col) {
|
|
181
|
+
const name = col.name.toLowerCase();
|
|
182
|
+
const notes = col.notes.toLowerCase();
|
|
183
|
+
return (
|
|
184
|
+
notes.includes('pk') ||
|
|
185
|
+
name === 'id' ||
|
|
186
|
+
name === 'created_at' || name === 'createdat' ||
|
|
187
|
+
name === 'updated_at' || name === 'updatedat' ||
|
|
188
|
+
notes.includes('auto') || notes.includes('generated') || notes.includes('computed')
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Generate C# contracts.cs content from parsed table definitions.
|
|
194
|
+
*
|
|
195
|
+
* Produces:
|
|
196
|
+
* - One Dto record per table (all columns)
|
|
197
|
+
* - One CreateRequest record per table (non-auto fields, required)
|
|
198
|
+
* - One UpdateRequest record per table (non-auto fields, nullable/patch)
|
|
199
|
+
* - Service interface for the primary table
|
|
200
|
+
* - NotFoundException for the primary entity
|
|
201
|
+
*
|
|
202
|
+
* @param {Array} tables - Parsed table definitions
|
|
203
|
+
* @param {string} featureName - Feature name (used for namespace/class names)
|
|
204
|
+
* @param {string} namespace - C# namespace
|
|
205
|
+
* @returns {string} Generated C# source
|
|
206
|
+
*/
|
|
207
|
+
function generateContracts(tables, featureName, namespace) {
|
|
208
|
+
const entityName = toPascalCase(featureName);
|
|
209
|
+
const today = new Date().toISOString().split('T')[0];
|
|
210
|
+
|
|
211
|
+
const lines = [
|
|
212
|
+
`// ============================================================`,
|
|
213
|
+
`// CONTRACTS: ${entityName} — Generated from real schema-analysis.md`,
|
|
214
|
+
`// Generated by MORPH-SPEC generate contracts | Date: ${today}`,
|
|
215
|
+
`// Source: .morph/features/${featureName}/1-design/schema-analysis.md`,
|
|
216
|
+
`// Review: rename types, add validation attributes, adjust access modifiers.`,
|
|
217
|
+
`// ============================================================`,
|
|
218
|
+
``,
|
|
219
|
+
`using System;`,
|
|
220
|
+
`using System.Text.Json;`,
|
|
221
|
+
`using System.Collections.Generic;`,
|
|
222
|
+
`using System.Threading;`,
|
|
223
|
+
`using System.Threading.Tasks;`,
|
|
224
|
+
``,
|
|
225
|
+
`namespace ${namespace}.Application.Features.${entityName};`,
|
|
226
|
+
``,
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
for (const table of tables) {
|
|
230
|
+
const dtoPascal = toEntityName(table.name);
|
|
231
|
+
const userFields = table.columns.filter(c => !isAutoManagedField(c));
|
|
232
|
+
|
|
233
|
+
lines.push(`#region ${dtoPascal}`, ``);
|
|
234
|
+
|
|
235
|
+
// ── DTO ──────────────────────────────────────────────────────────────────
|
|
236
|
+
lines.push(`/// <summary>Read DTO — maps to the "${table.name}" table.</summary>`);
|
|
237
|
+
lines.push(`public record ${dtoPascal}Dto(`);
|
|
238
|
+
table.columns.forEach((col, i) => {
|
|
239
|
+
const csType = toCSharpType(col.type, col.nullable);
|
|
240
|
+
const propName = toPascalCase(col.name);
|
|
241
|
+
const comment = col.notes && col.notes !== '-' ? ` // ${table.name}.${col.name} (${col.type})` : '';
|
|
242
|
+
const comma = i < table.columns.length - 1 ? ',' : '';
|
|
243
|
+
lines.push(` ${csType} ${propName}${comma}${comment}`);
|
|
244
|
+
});
|
|
245
|
+
lines.push(`);`, ``);
|
|
246
|
+
|
|
247
|
+
// ── Create Request ────────────────────────────────────────────────────────
|
|
248
|
+
lines.push(`/// <summary>Command to create a new ${dtoPascal}.</summary>`);
|
|
249
|
+
lines.push(`public record Create${dtoPascal}Request(`);
|
|
250
|
+
if (userFields.length > 0) {
|
|
251
|
+
userFields.forEach((col, i) => {
|
|
252
|
+
const csType = toCSharpType(col.type, false); // Required for create
|
|
253
|
+
const propName = toPascalCase(col.name);
|
|
254
|
+
const comma = i < userFields.length - 1 ? ',' : '';
|
|
255
|
+
lines.push(` ${csType} ${propName}${comma}`);
|
|
256
|
+
});
|
|
257
|
+
} else {
|
|
258
|
+
lines.push(` // TODO: Add required fields — all columns are auto-managed`);
|
|
259
|
+
}
|
|
260
|
+
lines.push(`);`, ``);
|
|
261
|
+
|
|
262
|
+
// ── Update Request ────────────────────────────────────────────────────────
|
|
263
|
+
lines.push(`/// <summary>Command to update an existing ${dtoPascal} (patch semantics).</summary>`);
|
|
264
|
+
lines.push(`public record Update${dtoPascal}Request(`);
|
|
265
|
+
if (userFields.length > 0) {
|
|
266
|
+
userFields.forEach((col, i) => {
|
|
267
|
+
const csType = toCSharpType(col.type, true); // All nullable for patch
|
|
268
|
+
const propName = toPascalCase(col.name);
|
|
269
|
+
const comma = i < userFields.length - 1 ? ',' : '';
|
|
270
|
+
lines.push(` ${csType} ${propName}${comma}`);
|
|
271
|
+
});
|
|
272
|
+
} else {
|
|
273
|
+
lines.push(` // TODO: Add updatable fields`);
|
|
274
|
+
}
|
|
275
|
+
lines.push(`);`, ``);
|
|
276
|
+
lines.push(`#endregion`, ``);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── Service Interface for primary entity ──────────────────────────────────
|
|
280
|
+
const primaryTable = tables[0];
|
|
281
|
+
const primaryEntity = primaryTable ? toEntityName(primaryTable.name) : entityName;
|
|
282
|
+
|
|
283
|
+
lines.push(
|
|
284
|
+
`#region Service Interface`,
|
|
285
|
+
``,
|
|
286
|
+
`public interface I${entityName}Service`,
|
|
287
|
+
`{`,
|
|
288
|
+
` Task<${primaryEntity}Dto?> GetByIdAsync(Guid id, CancellationToken ct = default);`,
|
|
289
|
+
` Task<List<${primaryEntity}Dto>> GetAllAsync(CancellationToken ct = default);`,
|
|
290
|
+
` Task<${primaryEntity}Dto> CreateAsync(Create${primaryEntity}Request request, CancellationToken ct = default);`,
|
|
291
|
+
` Task UpdateAsync(Guid id, Update${primaryEntity}Request request, CancellationToken ct = default);`,
|
|
292
|
+
` Task DeleteAsync(Guid id, CancellationToken ct = default);`,
|
|
293
|
+
`}`,
|
|
294
|
+
``,
|
|
295
|
+
`#endregion`,
|
|
296
|
+
``,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// ── Exception ─────────────────────────────────────────────────────────────
|
|
300
|
+
lines.push(
|
|
301
|
+
`#region Exceptions`,
|
|
302
|
+
``,
|
|
303
|
+
`public class ${primaryEntity}NotFoundException(Guid id)`,
|
|
304
|
+
` : Exception($"${primaryEntity} '{{id}}' not found.");`,
|
|
305
|
+
``,
|
|
306
|
+
`#endregion`,
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
return lines.join('\n') + '\n';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ============================================================================
|
|
313
|
+
// Namespace Detection
|
|
314
|
+
// ============================================================================
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Attempt to derive the C# namespace from project config or .csproj file.
|
|
318
|
+
*/
|
|
319
|
+
async function detectNamespace(projectPath) {
|
|
320
|
+
const configPath = join(projectPath, '.morph/config/config.json');
|
|
321
|
+
try {
|
|
322
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
323
|
+
if (config.project?.namespace) return config.project.namespace;
|
|
324
|
+
if (config.project?.name) {
|
|
325
|
+
return config.project.name
|
|
326
|
+
.split(/[\s\-_]+/)
|
|
327
|
+
.map(w => (w ? w[0].toUpperCase() + w.slice(1) : ''))
|
|
328
|
+
.join('');
|
|
329
|
+
}
|
|
330
|
+
} catch { /* ignore */ }
|
|
331
|
+
|
|
332
|
+
// Try to find a .csproj via git ls-files
|
|
333
|
+
try {
|
|
334
|
+
const { execSync } = await import('child_process');
|
|
335
|
+
const csprojList = execSync('git ls-files "*.csproj"', {
|
|
336
|
+
cwd: projectPath,
|
|
337
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
338
|
+
encoding: 'utf-8',
|
|
339
|
+
}).trim().split('\n').filter(Boolean);
|
|
340
|
+
if (csprojList.length > 0) {
|
|
341
|
+
const name = csprojList[0].split('/').pop().replace('.csproj', '');
|
|
342
|
+
return name.replace(/\.|-/g, '');
|
|
343
|
+
}
|
|
344
|
+
} catch { /* not a git repo or no .csproj */ }
|
|
345
|
+
|
|
346
|
+
return 'YourProject';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ============================================================================
|
|
350
|
+
// CLI Entry Point
|
|
351
|
+
// ============================================================================
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Main handler for: morph-spec generate contracts <feature>
|
|
355
|
+
*
|
|
356
|
+
* @param {string} featureName - Feature slug (e.g. "email-marketing")
|
|
357
|
+
* @param {Object} options
|
|
358
|
+
* @param {boolean} options.dryRun - Print to stdout instead of writing
|
|
359
|
+
* @param {string} options.output - Override output path
|
|
360
|
+
*/
|
|
361
|
+
export async function generateContractsCommand(featureName, options = {}) {
|
|
362
|
+
logger.header(`Generate Contracts: ${featureName}`);
|
|
363
|
+
logger.blank();
|
|
364
|
+
|
|
365
|
+
const projectPath = options.projectPath || process.cwd();
|
|
366
|
+
|
|
367
|
+
// Locate schema-analysis.md
|
|
368
|
+
const schemaPath = join(projectPath, `.morph/features/${featureName}/1-design/schema-analysis.md`);
|
|
369
|
+
if (!existsSync(schemaPath)) {
|
|
370
|
+
logger.error(`schema-analysis.md not found at: ${schemaPath}`);
|
|
371
|
+
logger.dim(' Run the Design phase schema analysis first:');
|
|
372
|
+
logger.dim(' npx morph-spec phase advance ' + featureName);
|
|
373
|
+
logger.dim(' then execute /phase-codebase-analysis in Claude Code');
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
logger.dim(`Reading: ${schemaPath}`);
|
|
378
|
+
const content = readFileSync(schemaPath, 'utf-8');
|
|
379
|
+
|
|
380
|
+
// Parse tables
|
|
381
|
+
const tables = parseSchemaAnalysisMd(content);
|
|
382
|
+
|
|
383
|
+
if (tables.length === 0) {
|
|
384
|
+
logger.error('No tables found in schema-analysis.md');
|
|
385
|
+
logger.blank();
|
|
386
|
+
logger.dim(' Ensure the file uses the standard MORPH-SPEC format:');
|
|
387
|
+
logger.dim(' ### Table: your_table_name');
|
|
388
|
+
logger.dim(' **Columns:**');
|
|
389
|
+
logger.dim(' | Column Name | Type | Nullable | Default | Notes |');
|
|
390
|
+
logger.dim(' |------------|------|----------|---------|-------|');
|
|
391
|
+
logger.dim(' | id | uuid | NO | - | PK |');
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
logger.info(`Tables found: ${chalk.cyan(tables.map(t => t.name).join(', '))}`);
|
|
396
|
+
tables.forEach(t => {
|
|
397
|
+
const colSummary = t.columns
|
|
398
|
+
.slice(0, 4)
|
|
399
|
+
.map(c => `${c.name}:${c.type}`)
|
|
400
|
+
.join(', ');
|
|
401
|
+
const more = t.columns.length > 4 ? ` +${t.columns.length - 4} more` : '';
|
|
402
|
+
logger.dim(` ${t.name}: ${colSummary}${more}`);
|
|
403
|
+
});
|
|
404
|
+
logger.blank();
|
|
405
|
+
|
|
406
|
+
// Detect namespace
|
|
407
|
+
const namespace = await detectNamespace(projectPath);
|
|
408
|
+
logger.dim(`Namespace: ${namespace}`);
|
|
409
|
+
logger.blank();
|
|
410
|
+
|
|
411
|
+
// Generate content
|
|
412
|
+
const contractsContent = generateContracts(tables, featureName, namespace);
|
|
413
|
+
|
|
414
|
+
// Output path
|
|
415
|
+
const outputPath = options.output
|
|
416
|
+
|| join(projectPath, `.morph/features/${featureName}/1-design/contracts.cs`);
|
|
417
|
+
|
|
418
|
+
if (options.dryRun) {
|
|
419
|
+
logger.info(chalk.cyan('📋 DRY RUN — Preview (nothing written):'));
|
|
420
|
+
logger.blank();
|
|
421
|
+
console.log(contractsContent);
|
|
422
|
+
logger.dim(`Would be written to: ${outputPath}`);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
427
|
+
writeFileSync(outputPath, contractsContent, 'utf-8');
|
|
428
|
+
|
|
429
|
+
logger.success('✅ contracts.cs generated from real schema');
|
|
430
|
+
logger.dim(` Output: ${chalk.cyan(outputPath)}`);
|
|
431
|
+
const totalCols = tables.reduce((n, t) => n + t.columns.length, 0);
|
|
432
|
+
logger.dim(` ${tables.length} table(s) → ${totalCols} columns mapped`);
|
|
433
|
+
logger.blank();
|
|
434
|
+
|
|
435
|
+
// Mark output in state (non-blocking)
|
|
436
|
+
try {
|
|
437
|
+
markOutput(featureName, 'contracts');
|
|
438
|
+
logger.dim(' State: contracts output marked ✓');
|
|
439
|
+
} catch {
|
|
440
|
+
// Feature might not be in state yet — non-blocking
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
logger.blank();
|
|
444
|
+
logger.dim('Next step: review contracts.cs for type accuracy, then proceed with /phase-design');
|
|
445
|
+
}
|
|
@@ -71,15 +71,25 @@ export async function runValidation(projectPath, featureName, options = {}) {
|
|
|
71
71
|
validatorIds.push(...constraints.enabledValidators);
|
|
72
72
|
|
|
73
73
|
if (validatorIds.length === 0) {
|
|
74
|
-
|
|
74
|
+
// Distinguish: no agents vs. no validators mapped
|
|
75
|
+
const state = loadState(false);
|
|
76
|
+
const activeAgents = state?.features?.[featureName]?.activeAgents || [];
|
|
77
|
+
if (activeAgents.length === 0) {
|
|
78
|
+
result.summary = 'No validators ran — feature has no active agents';
|
|
79
|
+
console.log(chalk.gray('\n ℹ️ No validators configured (no active agents in state.json)'));
|
|
80
|
+
console.log(chalk.gray(' Add agents: morph-spec state add-agent ' + featureName + ' <agent-id>'));
|
|
81
|
+
} else {
|
|
82
|
+
result.summary = `No validators mapped for active agents: ${activeAgents.join(', ')}`;
|
|
83
|
+
console.log(chalk.gray(`\n ℹ️ Agents [${activeAgents.join(', ')}] have no validators in agents.json`));
|
|
84
|
+
}
|
|
85
|
+
result._skipReason = result.summary;
|
|
75
86
|
return result;
|
|
76
87
|
}
|
|
77
88
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
89
|
+
// Always show what's running (even non-verbose) — helps confirm validators are active
|
|
90
|
+
console.log(chalk.gray(`\n Running ${validatorIds.length} validator(s): ${validatorIds.join(', ')}`));
|
|
91
|
+
if (constraints.enabledValidators.length > 0) {
|
|
92
|
+
console.log(chalk.gray(` (+ ${constraints.enabledValidators.length} from decisions)`));
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
// Always run contract compliance if contracts.cs exists
|
|
@@ -234,7 +244,9 @@ async function runSingleValidator(validatorId, projectPath, featureName, options
|
|
|
234
244
|
*/
|
|
235
245
|
export function formatValidationResults(result) {
|
|
236
246
|
if (result.passed && result.warnings === 0) {
|
|
237
|
-
|
|
247
|
+
const ranCount = Object.keys(result.results || {}).length;
|
|
248
|
+
const countLabel = ranCount > 0 ? ` (${ranCount} validator${ranCount !== 1 ? 's' : ''})` : '';
|
|
249
|
+
console.log(chalk.green(`\n ✅ All validations passed${countLabel}!\n`));
|
|
238
250
|
return;
|
|
239
251
|
}
|
|
240
252
|
|