roadmapsmith 0.5.1 → 0.7.0
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/bin/cli.js +247 -211
- package/package.json +20 -4
- package/src/config.js +7 -0
- package/src/generator/index.js +563 -562
- package/src/model.js +1 -0
- package/src/renderer/professional.js +536 -476
- package/src/validator/index.js +464 -420
package/bin/cli.js
CHANGED
|
@@ -1,212 +1,248 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const { parseArgv } = require('../src/utils');
|
|
7
|
-
const { loadConfig, resolveRoadmapFile, resolveAgentsFile, loadPlugins } = require('../src/config');
|
|
8
|
-
const { readTextIfExists, writeText, printDryRunDiff } = require('../src/io');
|
|
9
|
-
const { renderRoadmapTemplate, renderAgentsTemplate } = require('../src/templates');
|
|
10
|
-
const { generateRoadmapDocument } = require('../src/generator');
|
|
11
|
-
const { parseRoadmap } = require('../src/parser');
|
|
12
|
-
const { buildValidationContext, validateTasks, auditValidation } = require('../src/validator');
|
|
13
|
-
const { applySync } = require('../src/sync');
|
|
14
|
-
|
|
15
|
-
function printHelp() {
|
|
16
|
-
console.log([
|
|
17
|
-
'Usage:',
|
|
18
|
-
' roadmapsmith init [--roadmap-file <path>] [--agents-file <path>] [--dry-run]',
|
|
19
|
-
' roadmapsmith generate [--project-root <path>] [--config <path>] [--roadmap-file <path>] [--dry-run] [--audit]',
|
|
20
|
-
' roadmapsmith sync [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run] [--audit]',
|
|
21
|
-
' roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json]'
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { parseArgv } = require('../src/utils');
|
|
7
|
+
const { loadConfig, resolveRoadmapFile, resolveAgentsFile, loadPlugins } = require('../src/config');
|
|
8
|
+
const { readTextIfExists, writeText, printDryRunDiff } = require('../src/io');
|
|
9
|
+
const { renderRoadmapTemplate, renderAgentsTemplate } = require('../src/templates');
|
|
10
|
+
const { generateRoadmapDocument } = require('../src/generator');
|
|
11
|
+
const { parseRoadmap } = require('../src/parser');
|
|
12
|
+
const { buildValidationContext, validateTasks, auditValidation, CONFIDENCE_RANK, applyMinimumConfidence } = require('../src/validator');
|
|
13
|
+
const { applySync } = require('../src/sync');
|
|
14
|
+
|
|
15
|
+
function printHelp() {
|
|
16
|
+
console.log([
|
|
17
|
+
'Usage:',
|
|
18
|
+
' roadmapsmith init [--roadmap-file <path>] [--agents-file <path>] [--dry-run]',
|
|
19
|
+
' roadmapsmith generate [--project-root <path>] [--config <path>] [--roadmap-file <path>] [--dry-run] [--audit]',
|
|
20
|
+
' roadmapsmith sync [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run] [--audit]',
|
|
21
|
+
' roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json]',
|
|
22
|
+
' roadmapsmith doctor [--roadmap-file <path>] [--project-root <path>] [--config <path>]'
|
|
23
|
+
].join('\n'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isEnabled(value) {
|
|
27
|
+
if (value === true) return true;
|
|
28
|
+
if (typeof value !== 'string') return false;
|
|
29
|
+
const normalized = value.toLowerCase();
|
|
30
|
+
return normalized === '1' || normalized === 'true' || normalized === 'yes';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatResultLine(task, result) {
|
|
34
|
+
const status = result.passed ? 'PASS' : 'FAIL';
|
|
35
|
+
const reason = result.reasons.length > 0 ? ` :: ${result.reasons.join('; ')}` : '';
|
|
36
|
+
return `${status} [${task.id}] ${task.text}${reason}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function maybeFilterTasks(tasks, filterValue) {
|
|
40
|
+
if (!filterValue) return tasks;
|
|
41
|
+
const normalized = String(filterValue).toLowerCase();
|
|
42
|
+
return tasks.filter((task) => {
|
|
43
|
+
return task.id.toLowerCase() === normalized || task.text.toLowerCase().includes(normalized);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function printAudit(audit) {
|
|
48
|
+
console.log(`Audit summary: ${audit.checkedWithoutEvidence.length} checked-without-evidence, ${audit.readyButUnchecked.length} ready-but-unchecked.`);
|
|
49
|
+
if (audit.checkedWithoutEvidence.length > 0) {
|
|
50
|
+
console.log('Checked without evidence:');
|
|
51
|
+
audit.checkedWithoutEvidence.forEach((item) => {
|
|
52
|
+
console.log(`- [${item.task.id}] ${item.task.text}`);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (audit.readyButUnchecked.length > 0) {
|
|
56
|
+
console.log('Ready but unchecked:');
|
|
57
|
+
audit.readyButUnchecked.forEach((item) => {
|
|
58
|
+
console.log(`- [${item.task.id}] ${item.task.text}`);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function run() {
|
|
64
|
+
const parsed = parseArgv(process.argv.slice(2));
|
|
65
|
+
const command = parsed.command;
|
|
66
|
+
const flags = parsed.flags;
|
|
67
|
+
|
|
68
|
+
if (!command || isEnabled(flags.help) || isEnabled(flags.h)) {
|
|
69
|
+
printHelp();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (command === 'init') {
|
|
74
|
+
const projectRoot = process.cwd();
|
|
75
|
+
const config = loadConfig({ projectRoot });
|
|
76
|
+
const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
|
|
77
|
+
const agentsFile = resolveAgentsFile(projectRoot, config, flags['agents-file']);
|
|
78
|
+
const dryRun = isEnabled(flags['dry-run']);
|
|
79
|
+
|
|
80
|
+
const roadmapExists = fs.existsSync(roadmapFile);
|
|
81
|
+
const agentsExists = fs.existsSync(agentsFile);
|
|
82
|
+
|
|
83
|
+
if (!roadmapExists) {
|
|
84
|
+
const roadmap = renderRoadmapTemplate();
|
|
85
|
+
const result = writeText(roadmapFile, roadmap, { dryRun });
|
|
86
|
+
if (dryRun && result.changed) {
|
|
87
|
+
printDryRunDiff(roadmapFile, result.before, result.after);
|
|
88
|
+
}
|
|
89
|
+
console.log(`${dryRun ? 'Would create' : 'Created'} ${roadmapFile}`);
|
|
90
|
+
} else {
|
|
91
|
+
console.log(`Skipped existing ${roadmapFile}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!agentsExists) {
|
|
95
|
+
const agents = renderAgentsTemplate({ roadmapPath: path.basename(roadmapFile) });
|
|
96
|
+
const result = writeText(agentsFile, agents, { dryRun });
|
|
97
|
+
if (dryRun && result.changed) {
|
|
98
|
+
printDryRunDiff(agentsFile, result.before, result.after);
|
|
99
|
+
}
|
|
100
|
+
console.log(`${dryRun ? 'Would create' : 'Created'} ${agentsFile}`);
|
|
101
|
+
} else {
|
|
102
|
+
console.log(`Skipped existing ${agentsFile}`);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (command === 'generate') {
|
|
108
|
+
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
109
|
+
const config = loadConfig({ projectRoot, configPath: flags.config });
|
|
110
|
+
const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
|
|
111
|
+
const plugins = loadPlugins(projectRoot, config.plugins);
|
|
112
|
+
const existingContent = readTextIfExists(roadmapFile) || '';
|
|
113
|
+
const dryRun = isEnabled(flags['dry-run']);
|
|
114
|
+
|
|
115
|
+
const document = generateRoadmapDocument({
|
|
116
|
+
projectRoot,
|
|
117
|
+
roadmapPath: roadmapFile,
|
|
118
|
+
existingContent,
|
|
119
|
+
config,
|
|
120
|
+
plugins
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const writeResult = writeText(roadmapFile, document, { dryRun });
|
|
124
|
+
if (dryRun) {
|
|
125
|
+
if (writeResult.changed) {
|
|
126
|
+
printDryRunDiff(roadmapFile, writeResult.before, writeResult.after);
|
|
127
|
+
} else {
|
|
128
|
+
console.log(`No changes for ${roadmapFile}`);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
console.log(writeResult.changed ? `Updated ${roadmapFile}` : `No changes for ${roadmapFile}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (isEnabled(flags.audit)) {
|
|
135
|
+
const parsedRoadmap = parseRoadmap(document);
|
|
136
|
+
const validationContext = buildValidationContext(projectRoot, config, plugins);
|
|
137
|
+
const results = validateTasks(parsedRoadmap.tasks, validationContext, config, plugins);
|
|
138
|
+
const audit = auditValidation(parsedRoadmap.tasks, results);
|
|
139
|
+
printAudit(audit);
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (command === 'sync') {
|
|
145
|
+
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
146
|
+
const config = loadConfig({ projectRoot, configPath: flags.config });
|
|
147
|
+
const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
|
|
148
|
+
const content = readTextIfExists(roadmapFile);
|
|
149
|
+
if (content == null) {
|
|
150
|
+
throw new Error(`Roadmap not found: ${roadmapFile}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const parsedRoadmap = parseRoadmap(content);
|
|
154
|
+
const validationContext = buildValidationContext(projectRoot, config, loadPlugins(projectRoot, config.plugins));
|
|
155
|
+
const results = validateTasks(parsedRoadmap.tasks, validationContext, config, validationContext.plugins);
|
|
156
|
+
applyMinimumConfidence(results, config.validation?.minimumConfidence);
|
|
157
|
+
const next = applySync(content, parsedRoadmap.tasks, results);
|
|
158
|
+
const dryRun = isEnabled(flags['dry-run']);
|
|
159
|
+
const writeResult = writeText(roadmapFile, next, { dryRun });
|
|
160
|
+
|
|
161
|
+
if (dryRun) {
|
|
162
|
+
if (writeResult.changed) {
|
|
163
|
+
printDryRunDiff(roadmapFile, writeResult.before, writeResult.after);
|
|
164
|
+
} else {
|
|
165
|
+
console.log(`No changes for ${roadmapFile}`);
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
console.log(writeResult.changed ? `Updated ${roadmapFile}` : `No changes for ${roadmapFile}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (isEnabled(flags.audit)) {
|
|
172
|
+
const audit = auditValidation(parsedRoadmap.tasks, results);
|
|
173
|
+
printAudit(audit);
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (command === 'validate') {
|
|
179
|
+
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
180
|
+
const config = loadConfig({ projectRoot, configPath: flags.config });
|
|
181
|
+
const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
|
|
182
|
+
const content = readTextIfExists(roadmapFile);
|
|
183
|
+
if (content == null) {
|
|
184
|
+
throw new Error(`Roadmap not found: ${roadmapFile}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const parsedRoadmap = parseRoadmap(content);
|
|
188
|
+
const tasks = maybeFilterTasks(parsedRoadmap.tasks, flags.task);
|
|
189
|
+
const validationContext = buildValidationContext(projectRoot, config, loadPlugins(projectRoot, config.plugins));
|
|
190
|
+
const results = validateTasks(tasks, validationContext, config, validationContext.plugins);
|
|
191
|
+
|
|
192
|
+
const minRank = CONFIDENCE_RANK[config.validation && config.validation.minimumConfidence] ?? 0;
|
|
193
|
+
const visibleTasks = tasks.filter((task) => (CONFIDENCE_RANK[results[task.id].confidence] ?? 0) >= minRank);
|
|
194
|
+
|
|
195
|
+
if (isEnabled(flags.json)) {
|
|
196
|
+
const payload = visibleTasks.map((task) => ({ task, result: results[task.id] }));
|
|
197
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
198
|
+
} else {
|
|
199
|
+
visibleTasks.forEach((task) => {
|
|
200
|
+
console.log(formatResultLine(task, results[task.id]));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const failed = visibleTasks.some((task) => !results[task.id].passed);
|
|
205
|
+
if (failed) {
|
|
206
|
+
process.exitCode = 1;
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (command === 'doctor') {
|
|
212
|
+
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
213
|
+
let ok = true;
|
|
214
|
+
|
|
215
|
+
let config;
|
|
216
|
+
try {
|
|
217
|
+
config = loadConfig({ projectRoot, configPath: flags.config });
|
|
218
|
+
console.log('[ok] Config loaded without errors');
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error(`[fail] Config error: ${error.message}`);
|
|
221
|
+
ok = false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (config) {
|
|
225
|
+
const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
|
|
226
|
+
if (fs.existsSync(roadmapFile)) {
|
|
227
|
+
console.log(`[ok] ROADMAP file found: ${roadmapFile}`);
|
|
228
|
+
} else {
|
|
229
|
+
console.error(`[fail] ROADMAP file not found: ${roadmapFile}`);
|
|
230
|
+
ok = false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!ok) {
|
|
235
|
+
process.exitCode = 1;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
console.log('doctor: all checks passed');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
throw new Error(`Unknown command: ${command}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
run().catch((error) => {
|
|
246
|
+
console.error(error.message);
|
|
247
|
+
process.exitCode = 1;
|
|
212
248
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roadmapsmith",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Evidence-backed ROADMAP.md generator and sync tool for AI coding agents.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"roadmapsmith": "bin/cli.js"
|
|
@@ -12,10 +12,26 @@
|
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
14
|
"roadmap",
|
|
15
|
-
"
|
|
15
|
+
"planning",
|
|
16
16
|
"agent",
|
|
17
17
|
"cli",
|
|
18
|
-
"
|
|
18
|
+
"validation",
|
|
19
|
+
"sync",
|
|
20
|
+
"task-tracking",
|
|
21
|
+
"evidence-based",
|
|
22
|
+
"deterministic",
|
|
23
|
+
"monorepo",
|
|
24
|
+
"claude-code",
|
|
25
|
+
"ai-agent",
|
|
26
|
+
"project-management",
|
|
27
|
+
"coding-agents",
|
|
28
|
+
"agent-skills",
|
|
29
|
+
"roadmap-generator",
|
|
30
|
+
"roadmap-sync",
|
|
31
|
+
"task-validation",
|
|
32
|
+
"developer-tools",
|
|
33
|
+
"markdown",
|
|
34
|
+
"agent-workflow"
|
|
19
35
|
],
|
|
20
36
|
"author": "PapiScholz",
|
|
21
37
|
"license": "MIT",
|
package/src/config.js
CHANGED
|
@@ -24,6 +24,9 @@ const DEFAULT_CONFIG = {
|
|
|
24
24
|
steps: [],
|
|
25
25
|
phases: []
|
|
26
26
|
},
|
|
27
|
+
validation: {
|
|
28
|
+
minimumConfidence: 'low'
|
|
29
|
+
},
|
|
27
30
|
milestones: [
|
|
28
31
|
{ version: 'v0.1', goal: 'Foundation baseline complete' },
|
|
29
32
|
{ version: 'v0.2', goal: 'Core feature coverage stabilized' },
|
|
@@ -73,6 +76,10 @@ function mergeConfig(userConfig) {
|
|
|
73
76
|
phases: (userConfig && userConfig.product && Array.isArray(userConfig.product.phases))
|
|
74
77
|
? userConfig.product.phases
|
|
75
78
|
: DEFAULT_CONFIG.product.phases
|
|
79
|
+
},
|
|
80
|
+
validation: {
|
|
81
|
+
...DEFAULT_CONFIG.validation,
|
|
82
|
+
...((userConfig && userConfig.validation) || {})
|
|
76
83
|
}
|
|
77
84
|
};
|
|
78
85
|
}
|