@entelligentsia/forgecli 0.8.4 → 0.9.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/CHANGELOG.md +53 -0
- package/dist/bin/argv.d.ts +2 -2
- package/dist/bin/argv.js +17 -0
- package/dist/bin/argv.js.map +1 -1
- package/dist/bin/config.d.ts +69 -0
- package/dist/bin/config.js +315 -0
- package/dist/bin/config.js.map +1 -0
- package/dist/bin/doctor.d.ts +1 -0
- package/dist/bin/doctor.js +12 -0
- package/dist/bin/doctor.js.map +1 -1
- package/dist/bin/forge.js +7 -0
- package/dist/bin/forge.js.map +1 -1
- package/dist/extensions/forgecli/config-command.d.ts +8 -0
- package/dist/extensions/forgecli/config-command.js +66 -0
- package/dist/extensions/forgecli/config-command.js.map +1 -0
- package/dist/extensions/forgecli/config-layer.d.ts +38 -0
- package/dist/extensions/forgecli/config-layer.js +68 -0
- package/dist/extensions/forgecli/config-layer.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/component.d.ts +35 -0
- package/dist/extensions/forgecli/config-tui/component.js +236 -0
- package/dist/extensions/forgecli/config-tui/component.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/handler.d.ts +40 -0
- package/dist/extensions/forgecli/config-tui/handler.js +240 -0
- package/dist/extensions/forgecli/config-tui/handler.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/index.d.ts +5 -0
- package/dist/extensions/forgecli/config-tui/index.js +5 -0
- package/dist/extensions/forgecli/config-tui/index.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/keys.d.ts +26 -0
- package/dist/extensions/forgecli/config-tui/keys.js +33 -0
- package/dist/extensions/forgecli/config-tui/keys.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/plugin-config-reader.d.ts +23 -0
- package/dist/extensions/forgecli/config-tui/plugin-config-reader.js +58 -0
- package/dist/extensions/forgecli/config-tui/plugin-config-reader.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/advanced-menu.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js +83 -0
- package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/confirm-quit.d.ts +11 -0
- package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js +54 -0
- package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/override-editor.d.ts +11 -0
- package/dist/extensions/forgecli/config-tui/screens/override-editor.js +233 -0
- package/dist/extensions/forgecli/config-tui/screens/override-editor.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js +91 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list.js +71 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-editor.d.ts +10 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-editor.js +182 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-editor.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-picker.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-picker.js +76 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-picker.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/personas-list.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/personas-list.js +98 -0
- package/dist/extensions/forgecli/config-tui/screens/personas-list.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/shared.d.ts +29 -0
- package/dist/extensions/forgecli/config-tui/screens/shared.js +100 -0
- package/dist/extensions/forgecli/config-tui/screens/shared.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.d.ts +23 -0
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.js +128 -0
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-menu.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-menu.js +135 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-menu.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-picker.d.ts +9 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-picker.js +122 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-picker.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/types.d.ts +24 -0
- package/dist/extensions/forgecli/config-tui/screens/types.js +5 -0
- package/dist/extensions/forgecli/config-tui/screens/types.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens.d.ts +24 -0
- package/dist/extensions/forgecli/config-tui/screens.js +78 -0
- package/dist/extensions/forgecli/config-tui/screens.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/buffer.d.ts +11 -0
- package/dist/extensions/forgecli/config-tui/state/buffer.js +91 -0
- package/dist/extensions/forgecli/config-tui/state/buffer.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/constants.d.ts +4 -0
- package/dist/extensions/forgecli/config-tui/state/constants.js +14 -0
- package/dist/extensions/forgecli/config-tui/state/constants.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/index.d.ts +6 -0
- package/dist/extensions/forgecli/config-tui/state/index.js +9 -0
- package/dist/extensions/forgecli/config-tui/state/index.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/init.d.ts +2 -0
- package/dist/extensions/forgecli/config-tui/state/init.js +30 -0
- package/dist/extensions/forgecli/config-tui/state/init.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/model.d.ts +192 -0
- package/dist/extensions/forgecli/config-tui/state/model.js +4 -0
- package/dist/extensions/forgecli/config-tui/state/model.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/reducer.d.ts +2 -0
- package/dist/extensions/forgecli/config-tui/state/reducer.js +212 -0
- package/dist/extensions/forgecli/config-tui/state/reducer.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/selectors.d.ts +91 -0
- package/dist/extensions/forgecli/config-tui/state/selectors.js +231 -0
- package/dist/extensions/forgecli/config-tui/state/selectors.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state.d.ts +6 -0
- package/dist/extensions/forgecli/config-tui/state.js +11 -0
- package/dist/extensions/forgecli/config-tui/state.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/theme.d.ts +37 -0
- package/dist/extensions/forgecli/config-tui/theme.js +88 -0
- package/dist/extensions/forgecli/config-tui/theme.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/tier-meta.d.ts +28 -0
- package/dist/extensions/forgecli/config-tui/tier-meta.js +69 -0
- package/dist/extensions/forgecli/config-tui/tier-meta.js.map +1 -0
- package/dist/extensions/forgecli/config-writer.d.ts +16 -0
- package/dist/extensions/forgecli/config-writer.js +63 -0
- package/dist/extensions/forgecli/config-writer.js.map +1 -0
- package/dist/extensions/forgecli/fix-bug.js +85 -1
- package/dist/extensions/forgecli/fix-bug.js.map +1 -1
- package/dist/extensions/forgecli/forge-cli-schema.json +54 -0
- package/dist/extensions/forgecli/forge-commands.js +3 -8
- package/dist/extensions/forgecli/forge-commands.js.map +1 -1
- package/dist/extensions/forgecli/forge-subagent.d.ts +13 -0
- package/dist/extensions/forgecli/forge-subagent.js +19 -0
- package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
- package/dist/extensions/forgecli/index.js +16 -0
- package/dist/extensions/forgecli/index.js.map +1 -1
- package/dist/extensions/forgecli/input-router.d.ts +33 -0
- package/dist/extensions/forgecli/input-router.js +133 -0
- package/dist/extensions/forgecli/input-router.js.map +1 -0
- package/dist/extensions/forgecli/model-resolver.d.ts +32 -0
- package/dist/extensions/forgecli/model-resolver.js +65 -0
- package/dist/extensions/forgecli/model-resolver.js.map +1 -0
- package/dist/extensions/forgecli/model-validator.d.ts +29 -0
- package/dist/extensions/forgecli/model-validator.js +107 -0
- package/dist/extensions/forgecli/model-validator.js.map +1 -0
- package/dist/extensions/forgecli/run-sprint.js +59 -0
- package/dist/extensions/forgecli/run-sprint.js.map +1 -1
- package/dist/extensions/forgecli/run-task.js +93 -1
- package/dist/extensions/forgecli/run-task.js.map +1 -1
- package/dist/extensions/forgecli/thread-switcher.js +5 -2
- package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
- package/dist/extensions/forgecli/whats-new-widget.js +5 -2
- package/dist/extensions/forgecli/whats-new-widget.js.map +1 -1
- package/package.json +11 -3
- package/dist/extensions/forgecli/review-command.d.ts +0 -2
- package/dist/extensions/forgecli/review-command.js +0 -184
- package/dist/extensions/forgecli/review-command.js.map +0 -1
- package/dist/forge-payload/.tools/banners.cjs +0 -435
- package/dist/forge-payload/.tools/build-context-pack.cjs +0 -290
- package/dist/forge-payload/.tools/build-init-context.cjs +0 -322
- package/dist/forge-payload/.tools/build-overlay.cjs +0 -326
- package/dist/forge-payload/.tools/build-persona-pack.cjs +0 -226
- package/dist/forge-payload/.tools/collate.cjs +0 -1041
- package/dist/forge-payload/.tools/generation-manifest.cjs +0 -311
- package/dist/forge-payload/.tools/lib/forge-root.cjs +0 -59
- package/dist/forge-payload/.tools/lib/paths.cjs +0 -29
- package/dist/forge-payload/.tools/lib/pricing.cjs +0 -165
- package/dist/forge-payload/.tools/lib/project-root.cjs +0 -32
- package/dist/forge-payload/.tools/lib/result.js +0 -40
- package/dist/forge-payload/.tools/lib/store-facade.cjs +0 -162
- package/dist/forge-payload/.tools/lib/store-nlp.cjs +0 -250
- package/dist/forge-payload/.tools/lib/store-query-exec.cjs +0 -272
- package/dist/forge-payload/.tools/lib/validate.js +0 -141
- package/dist/forge-payload/.tools/manage-config.cjs +0 -340
- package/dist/forge-payload/.tools/manage-versions.cjs +0 -365
- package/dist/forge-payload/.tools/package.json +0 -3
- package/dist/forge-payload/.tools/parse-gates.cjs +0 -151
- package/dist/forge-payload/.tools/parse-verdict.cjs +0 -67
- package/dist/forge-payload/.tools/preflight-gate.cjs +0 -350
- package/dist/forge-payload/.tools/prompts/sprint-plan-prompt.md +0 -70
- package/dist/forge-payload/.tools/schemas/task-list.schema.json +0 -53
- package/dist/forge-payload/.tools/seed-store.cjs +0 -237
- package/dist/forge-payload/.tools/store-cli.cjs +0 -1226
- package/dist/forge-payload/.tools/store-query.cjs +0 -319
- package/dist/forge-payload/.tools/store.cjs +0 -315
- package/dist/forge-payload/.tools/substitute-placeholders.cjs +0 -625
- package/dist/forge-payload/.tools/validate-store.cjs +0 -593
|
@@ -1,593 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
// Forge tool: validate-store
|
|
5
|
-
// Check store integrity: required fields, types, enums, and referential integrity.
|
|
6
|
-
// Usage: validate-store [--dry-run] [--fix] [--json]
|
|
7
|
-
|
|
8
|
-
let _store;
|
|
9
|
-
function _getStore() { return _store || (_store = require('./store.cjs')); }
|
|
10
|
-
|
|
11
|
-
let _schemas;
|
|
12
|
-
function _getSchemas() {
|
|
13
|
-
if (_schemas) return _schemas;
|
|
14
|
-
const fs = require('fs');
|
|
15
|
-
const path = require('path');
|
|
16
|
-
|
|
17
|
-
const ENTITY_TYPES = ['sprint', 'task', 'bug', 'event', 'feature'];
|
|
18
|
-
|
|
19
|
-
const MINIMAL_REQUIRED = {
|
|
20
|
-
sprint: ['sprintId', 'title', 'status', 'taskIds', 'createdAt'],
|
|
21
|
-
task: ['taskId', 'sprintId', 'title', 'status', 'path'],
|
|
22
|
-
bug: ['bugId', 'title', 'severity', 'status', 'path', 'reportedAt'],
|
|
23
|
-
event: ['eventId', 'taskId', 'sprintId', 'role', 'action', 'phase', 'iteration', 'startTimestamp', 'endTimestamp', 'durationMinutes', 'model'],
|
|
24
|
-
feature: ['id', 'title', 'status', 'created_at']
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const schemas = {};
|
|
28
|
-
const projectDir = path.join('.forge', 'schemas');
|
|
29
|
-
const inTreeDir = path.join('forge', 'schemas');
|
|
30
|
-
const pluginDir = path.join(__dirname, '..', 'schemas');
|
|
31
|
-
|
|
32
|
-
for (const type of ENTITY_TYPES) {
|
|
33
|
-
const schemaFile = `${type}.schema.json`;
|
|
34
|
-
let schema = null;
|
|
35
|
-
|
|
36
|
-
// 1. Try project-installed schemas first
|
|
37
|
-
const projectPath = path.join(projectDir, schemaFile);
|
|
38
|
-
try {
|
|
39
|
-
if (fs.existsSync(projectPath)) {
|
|
40
|
-
schema = JSON.parse(fs.readFileSync(projectPath, 'utf8'));
|
|
41
|
-
}
|
|
42
|
-
} catch (e) {
|
|
43
|
-
console.error(`WARN: schema file ${projectPath} exists but could not be parsed: ${e.message}`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// 2. Fall back to in-tree source schemas (development mode)
|
|
47
|
-
if (!schema) {
|
|
48
|
-
const inTreePath = path.join(inTreeDir, schemaFile);
|
|
49
|
-
try {
|
|
50
|
-
if (fs.existsSync(inTreePath)) {
|
|
51
|
-
schema = JSON.parse(fs.readFileSync(inTreePath, 'utf8'));
|
|
52
|
-
}
|
|
53
|
-
} catch (e) {
|
|
54
|
-
console.error(`WARN: schema file ${inTreePath} exists but could not be parsed: ${e.message}`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// 3. Fall back to plugin-installed schemas (production mode)
|
|
59
|
-
// validate-store.cjs lives at $FORGE_ROOT/tools/, so __dirname/../schemas/
|
|
60
|
-
// resolves to $FORGE_ROOT/schemas/ — always correct for installed plugins.
|
|
61
|
-
if (!schema) {
|
|
62
|
-
const pluginPath = path.join(pluginDir, schemaFile);
|
|
63
|
-
try {
|
|
64
|
-
if (fs.existsSync(pluginPath)) {
|
|
65
|
-
schema = JSON.parse(fs.readFileSync(pluginPath, 'utf8'));
|
|
66
|
-
}
|
|
67
|
-
} catch (e) {
|
|
68
|
-
console.error(`WARN: schema file ${pluginPath} exists but could not be parsed: ${e.message}`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (schema) {
|
|
73
|
-
schemas[type] = schema;
|
|
74
|
-
} else {
|
|
75
|
-
console.error(`WARN: schema file ${schemaFile} not found in ${projectDir}, ${inTreeDir}, or ${pluginDir}, using minimal fallback`);
|
|
76
|
-
schemas[type] = { type: 'object', required: MINIMAL_REQUIRED[type] || [], properties: {} };
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
_schemas = schemas;
|
|
81
|
-
return _schemas;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
// Constants (exported for testing)
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
|
|
88
|
-
const ENTITY_TYPES = ['sprint', 'task', 'bug', 'event', 'feature'];
|
|
89
|
-
|
|
90
|
-
// Non-entity schemas: referenced and parsed for validity but not used for store record validation.
|
|
91
|
-
const ANCILLARY_SCHEMAS = ['project-overlay', 'project-context', 'structure-versions'];
|
|
92
|
-
|
|
93
|
-
const MINIMAL_REQUIRED = {
|
|
94
|
-
sprint: ['sprintId', 'title', 'status', 'taskIds', 'createdAt'],
|
|
95
|
-
task: ['taskId', 'sprintId', 'title', 'status', 'path'],
|
|
96
|
-
bug: ['bugId', 'title', 'severity', 'status', 'path', 'reportedAt'],
|
|
97
|
-
event: ['eventId', 'taskId', 'sprintId', 'role', 'action', 'phase', 'iteration', 'startTimestamp', 'endTimestamp', 'durationMinutes', 'model'],
|
|
98
|
-
feature: ['id', 'title', 'status', 'created_at']
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
// Fields that are legitimately null:
|
|
102
|
-
// sprintId / taskId — optional FK (e.g. standalone bug fix has no sprint)
|
|
103
|
-
// endTimestamp / durationMinutes — not recorded on "start" events (phase opened but never closed)
|
|
104
|
-
const NULLABLE_FK = new Set(['sprintId', 'taskId', 'endTimestamp', 'durationMinutes']);
|
|
105
|
-
|
|
106
|
-
// --- Validation ---
|
|
107
|
-
function validateRecord(record, schema) {
|
|
108
|
-
const errors = [];
|
|
109
|
-
const required = schema.required || [];
|
|
110
|
-
|
|
111
|
-
for (const field of required) {
|
|
112
|
-
if (record[field] === undefined || record[field] === '') {
|
|
113
|
-
errors.push({ category: 'missing-required', field, message: `missing required field: "${field}"`, value: record[field], expected: null });
|
|
114
|
-
} else if (record[field] === null && !NULLABLE_FK.has(field)) {
|
|
115
|
-
errors.push({ category: 'missing-required', field, message: `missing required field: "${field}"`, value: null, expected: null });
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
for (const [field, def] of Object.entries(schema.properties || {})) {
|
|
120
|
-
const val = record[field];
|
|
121
|
-
if (val === undefined) continue;
|
|
122
|
-
if (val === null) continue;
|
|
123
|
-
|
|
124
|
-
if (def.type) {
|
|
125
|
-
const typeMatches = (expected, actualVal) => {
|
|
126
|
-
return expected === 'integer' ? Number.isInteger(actualVal)
|
|
127
|
-
: expected === 'number' ? typeof actualVal === 'number'
|
|
128
|
-
: expected === 'array' ? Array.isArray(actualVal)
|
|
129
|
-
: typeof actualVal === expected;
|
|
130
|
-
};
|
|
131
|
-
const ok = Array.isArray(def.type)
|
|
132
|
-
? def.type.some(t => typeMatches(t, val))
|
|
133
|
-
: typeMatches(def.type, val);
|
|
134
|
-
if (!ok) errors.push({ category: 'type-mismatch', field, message: `field "${field}": expected ${def.type}, got ${Array.isArray(val) ? 'array' : typeof val}`, value: val, expected: String(def.type) });
|
|
135
|
-
}
|
|
136
|
-
if (def.enum && !def.enum.includes(val)) {
|
|
137
|
-
errors.push({ category: 'invalid-enum', field, message: `field "${field}": value "${val}" not in [${def.enum.join(', ')}]`, value: String(val), expected: def.enum });
|
|
138
|
-
}
|
|
139
|
-
if (def.minimum !== undefined && typeof val === 'number' && val < def.minimum) {
|
|
140
|
-
errors.push({ category: 'minimum-violation', field, message: `field "${field}": value ${val} is below minimum ${def.minimum}`, value: val, expected: String(def.minimum) });
|
|
141
|
-
}
|
|
142
|
-
// FORGE-S20-T01 — minimal `pattern` interpreter for string fields. Strictly
|
|
143
|
-
// additive: schemas without `pattern` see no behavior change. Used by the
|
|
144
|
-
// friction `subkind` slot to encode "frozen enum OR ^x_[a-z_]+$" as a
|
|
145
|
-
// single combined regex (neither validator supports `anyOf`).
|
|
146
|
-
if (def.pattern && typeof val === 'string') {
|
|
147
|
-
let re;
|
|
148
|
-
try { re = new RegExp(def.pattern); }
|
|
149
|
-
catch (_) {
|
|
150
|
-
errors.push({
|
|
151
|
-
category: 'pattern-invalid',
|
|
152
|
-
field,
|
|
153
|
-
message: `field "${field}": schema pattern "${def.pattern}" is not a valid regex`,
|
|
154
|
-
value: String(val),
|
|
155
|
-
expected: String(def.pattern),
|
|
156
|
-
});
|
|
157
|
-
re = null;
|
|
158
|
-
}
|
|
159
|
-
if (re && !re.test(val)) {
|
|
160
|
-
errors.push({
|
|
161
|
-
category: 'pattern-mismatch',
|
|
162
|
-
field,
|
|
163
|
-
message: `field "${field}": value "${val}" does not match pattern ${def.pattern}`,
|
|
164
|
-
value: String(val),
|
|
165
|
-
expected: String(def.pattern),
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Check for undeclared fields when additionalProperties is false
|
|
172
|
-
if (schema.additionalProperties === false) {
|
|
173
|
-
const allowed = new Set([...required, ...Object.keys(schema.properties || {})]);
|
|
174
|
-
for (const key of Object.keys(record)) {
|
|
175
|
-
if (!allowed.has(key)) {
|
|
176
|
-
errors.push({ category: 'undeclared-field', field: key, message: `undeclared field: "${key}"`, value: record[key], expected: null });
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Conditional required via JSON-Schema `allOf` with `if/then/required`.
|
|
182
|
-
// FORGE-S20-T00 — minimal interpreter: each clause may carry an `if` whose
|
|
183
|
-
// `properties.<field>.const` must equal `record[field]`, AND every field in
|
|
184
|
-
// `if.required` must be present on the record. When the clause matches,
|
|
185
|
-
// every name in `then.required` becomes an additional required field.
|
|
186
|
-
// No other JSON-Schema constructs are honored — this is intentional; the
|
|
187
|
-
// store schemas are thin and we don't want to drag in a full validator.
|
|
188
|
-
if (Array.isArray(schema.allOf)) {
|
|
189
|
-
for (const clause of schema.allOf) {
|
|
190
|
-
if (!clause || typeof clause !== 'object' || !clause.if || !clause.then) continue;
|
|
191
|
-
const condProps = (clause.if.properties && typeof clause.if.properties === 'object')
|
|
192
|
-
? clause.if.properties
|
|
193
|
-
: {};
|
|
194
|
-
const condRequired = Array.isArray(clause.if.required) ? clause.if.required : [];
|
|
195
|
-
|
|
196
|
-
// All `if.required` fields must be present on the record.
|
|
197
|
-
const allReqPresent = condRequired.every((f) => record[f] !== undefined && record[f] !== null && record[f] !== '');
|
|
198
|
-
if (!allReqPresent) continue;
|
|
199
|
-
|
|
200
|
-
// Every const predicate in `if.properties` must match the record.
|
|
201
|
-
let condMatches = true;
|
|
202
|
-
for (const [field, pred] of Object.entries(condProps)) {
|
|
203
|
-
if (pred && Object.prototype.hasOwnProperty.call(pred, 'const')) {
|
|
204
|
-
if (record[field] !== pred.const) { condMatches = false; break; }
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
if (!condMatches) continue;
|
|
208
|
-
|
|
209
|
-
// Clause fired — enforce `then.required`.
|
|
210
|
-
const thenRequired = Array.isArray(clause.then.required) ? clause.then.required : [];
|
|
211
|
-
for (const field of thenRequired) {
|
|
212
|
-
if (record[field] === undefined || record[field] === null || record[field] === '') {
|
|
213
|
-
errors.push({
|
|
214
|
-
category: 'missing-required',
|
|
215
|
-
field,
|
|
216
|
-
message: `missing required field: "${field}"`,
|
|
217
|
-
value: record[field],
|
|
218
|
-
expected: null,
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return errors;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// --- Backfill defaults for --fix mode ---
|
|
229
|
-
const BACKFILL = {
|
|
230
|
-
sprint: {
|
|
231
|
-
createdAt: (rec) => rec.completedAt || rec.startDate || rec.endDate || new Date().toISOString(),
|
|
232
|
-
},
|
|
233
|
-
bug: {
|
|
234
|
-
reportedAt: (rec) => rec.resolvedAt || new Date().toISOString(),
|
|
235
|
-
},
|
|
236
|
-
event: {
|
|
237
|
-
eventId: (_rec, id) => id,
|
|
238
|
-
role: (rec) => rec.agent || 'unknown',
|
|
239
|
-
action: (rec) => rec.phase || 'unknown',
|
|
240
|
-
phase: (rec) => rec.action || 'unknown',
|
|
241
|
-
iteration: () => 1,
|
|
242
|
-
startTimestamp: (rec) => rec.timestamp || new Date().toISOString(),
|
|
243
|
-
endTimestamp: (rec) => rec.timestamp || null,
|
|
244
|
-
durationMinutes: () => null,
|
|
245
|
-
model: (rec) => {
|
|
246
|
-
if (rec.actor && typeof rec.actor === 'string' && rec.actor.includes('claude')) return rec.actor;
|
|
247
|
-
return 'unknown';
|
|
248
|
-
},
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
module.exports = { validateRecord, MINIMAL_REQUIRED, NULLABLE_FK, BACKFILL, ENTITY_TYPES, ANCILLARY_SCHEMAS };
|
|
253
|
-
|
|
254
|
-
// ---------------------------------------------------------------------------
|
|
255
|
-
// CLI
|
|
256
|
-
// ---------------------------------------------------------------------------
|
|
257
|
-
if (require.main === module) {
|
|
258
|
-
|
|
259
|
-
process.on('uncaughtException', (error) => {
|
|
260
|
-
console.error('Fatal validate-store error:', error);
|
|
261
|
-
process.exit(1);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
const fs = require('fs');
|
|
265
|
-
const path = require('path');
|
|
266
|
-
const store = _getStore();
|
|
267
|
-
const schemas = _getSchemas();
|
|
268
|
-
|
|
269
|
-
const DRY_RUN = process.argv.includes('--dry-run');
|
|
270
|
-
const FIX_MODE = process.argv.includes('--fix');
|
|
271
|
-
const JSON_MODE = process.argv.includes('--json');
|
|
272
|
-
|
|
273
|
-
// Read engineering root and project prefix from config for filesystem consistency checks
|
|
274
|
-
const CONFIG_PATH = '.forge/config.json';
|
|
275
|
-
let engineeringRoot = 'engineering';
|
|
276
|
-
let projectPrefix = '[A-Z]+';
|
|
277
|
-
try {
|
|
278
|
-
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
279
|
-
if (cfg.paths && cfg.paths.engineering) engineeringRoot = cfg.paths.engineering;
|
|
280
|
-
if (cfg.project && cfg.project.prefix) projectPrefix = cfg.project.prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
281
|
-
} catch (_) {}
|
|
282
|
-
|
|
283
|
-
// Slug-aware directory discovery regexes
|
|
284
|
-
const SPRINT_DIR_RE = new RegExp(`^(${projectPrefix}-S\\d+)(-\\S+)?$`);
|
|
285
|
-
const TASK_FULL_RE = new RegExp(`^(${projectPrefix}-S\\d+-T\\d+)(-\\S+)?$`);
|
|
286
|
-
const TASK_SHORT_RE = /^(T\d+)(-\S+)?$/;
|
|
287
|
-
|
|
288
|
-
let errorsCount = 0;
|
|
289
|
-
let warningsCount = 0;
|
|
290
|
-
let fixesCount = 0;
|
|
291
|
-
|
|
292
|
-
// Structured collections for --json mode
|
|
293
|
-
const jsonErrors = [];
|
|
294
|
-
const jsonWarnings = [];
|
|
295
|
-
const jsonFixes = [];
|
|
296
|
-
|
|
297
|
-
function err(id, msg, category, field, value, expected) {
|
|
298
|
-
errorsCount++;
|
|
299
|
-
if (JSON_MODE) {
|
|
300
|
-
jsonErrors.push({ entity: id.split('/')[0].split('-')[0] || 'unknown', id, category: category || 'unknown', field: field || null, message: msg, value: value || null, expected: expected || null });
|
|
301
|
-
} else {
|
|
302
|
-
console.error(`ERROR ${id}: ${msg}`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function warn(id, msg, category, field) {
|
|
307
|
-
warningsCount++;
|
|
308
|
-
if (JSON_MODE) {
|
|
309
|
-
jsonWarnings.push({ entity: id.split('/')[0].split('-')[0] || 'unknown', id, category: category || 'unknown', field: field || null, message: msg });
|
|
310
|
-
} else {
|
|
311
|
-
console.log(`WARN ${id}: ${msg}`);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function fixMsg(id, msg, category, field) {
|
|
316
|
-
fixesCount++;
|
|
317
|
-
if (JSON_MODE) {
|
|
318
|
-
jsonFixes.push({ entity: id.split('/')[0].split('-')[0] || 'unknown', id, category: category || 'backfill', field: field || null, message: msg, applied: !DRY_RUN });
|
|
319
|
-
} else {
|
|
320
|
-
console.log(`FIXED ${id}: ${msg}`);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function backfillRecord(id, rec, type) {
|
|
325
|
-
const rules = BACKFILL[type];
|
|
326
|
-
if (!rules) return false;
|
|
327
|
-
let changed = false;
|
|
328
|
-
for (const [field, derive] of Object.entries(rules)) {
|
|
329
|
-
if (rec[field] === undefined || rec[field] === null || rec[field] === '') {
|
|
330
|
-
const val = derive(rec, id);
|
|
331
|
-
rec[field] = val;
|
|
332
|
-
fixMsg(id, `backfilled "${field}" = "${val}"`, 'backfill', field);
|
|
333
|
-
changed = true;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
if (changed) {
|
|
337
|
-
// In dry-run mode, preview the fix without writing
|
|
338
|
-
if (DRY_RUN) return changed;
|
|
339
|
-
// Facade write uses the logic in FSImpl to maintain formatting
|
|
340
|
-
if (type === 'sprint') store.writeSprint(rec);
|
|
341
|
-
else if (type === 'task') store.writeTask(rec);
|
|
342
|
-
else if (type === 'bug') store.writeBug(rec);
|
|
343
|
-
else if (type === 'feature') store.writeFeature(rec);
|
|
344
|
-
// Events are slightly different as they need sprintId
|
|
345
|
-
else if (type === 'event') {
|
|
346
|
-
// We'll handle event writing in the loop where sprintId is known
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
return changed;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Load all records up-front for referential integrity checks
|
|
353
|
-
const sprintIds = new Set();
|
|
354
|
-
const taskIds = new Set();
|
|
355
|
-
const bugIds = new Set();
|
|
356
|
-
const featureIds = new Set();
|
|
357
|
-
|
|
358
|
-
// --- Pass 1: validate structure, collect IDs ---
|
|
359
|
-
const sprints = store.listSprints();
|
|
360
|
-
for (const rec of sprints) {
|
|
361
|
-
if (!rec) continue;
|
|
362
|
-
if (FIX_MODE) backfillRecord(rec.sprintId, rec, 'sprint');
|
|
363
|
-
if (rec.sprintId) sprintIds.add(rec.sprintId);
|
|
364
|
-
for (const e of validateRecord(rec, schemas.sprint)) err(rec.sprintId, e.message, e.category, e.field, e.value, e.expected);
|
|
365
|
-
if (!rec.path) warn(rec.sprintId, 'missing optional field "path"', 'missing-optional', 'path');
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const tasks = store.listTasks();
|
|
369
|
-
for (const rec of tasks) {
|
|
370
|
-
if (!rec) continue;
|
|
371
|
-
if (rec.taskId) taskIds.add(rec.taskId);
|
|
372
|
-
for (const e of validateRecord(rec, schemas.task)) err(rec.taskId, e.message, e.category, e.field, e.value, e.expected);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const bugs = store.listBugs();
|
|
376
|
-
for (const rec of bugs) {
|
|
377
|
-
if (!rec) continue;
|
|
378
|
-
if (FIX_MODE) backfillRecord(rec.bugId, rec, 'bug');
|
|
379
|
-
if (rec.bugId) bugIds.add(rec.bugId);
|
|
380
|
-
for (const e of validateRecord(rec, schemas.bug)) err(rec.bugId, e.message, e.category, e.field, e.value, e.expected);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const features = store.listFeatures();
|
|
384
|
-
for (const rec of features) {
|
|
385
|
-
if (!rec) continue;
|
|
386
|
-
const featureId = rec.id || 'unknown';
|
|
387
|
-
featureIds.add(featureId);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// --- Pass 2: referential integrity ---
|
|
391
|
-
for (const rec of sprints) {
|
|
392
|
-
if (!rec) continue;
|
|
393
|
-
if (rec.feature_id && !featureIds.has(rec.feature_id)) {
|
|
394
|
-
if (FIX_MODE) {
|
|
395
|
-
rec.feature_id = null;
|
|
396
|
-
if (!DRY_RUN) store.writeSprint(rec);
|
|
397
|
-
fixMsg(rec.sprintId, `nullified orphaned feature_id "${rec.feature_id}"`, 'orphaned-fk', 'feature_id');
|
|
398
|
-
} else {
|
|
399
|
-
err(rec.sprintId, `feature_id "${rec.feature_id}" references unknown feature`, 'orphaned-fk', 'feature_id', rec.feature_id);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
for (const rec of tasks) {
|
|
405
|
-
if (!rec) continue;
|
|
406
|
-
if (rec.sprintId && !sprintIds.has(rec.sprintId))
|
|
407
|
-
err(rec.taskId, `sprintId "${rec.sprintId}" references unknown sprint`, 'orphaned-fk', 'sprintId', rec.sprintId);
|
|
408
|
-
|
|
409
|
-
if (rec.feature_id && !featureIds.has(rec.feature_id)) {
|
|
410
|
-
if (FIX_MODE) {
|
|
411
|
-
rec.feature_id = null;
|
|
412
|
-
if (!DRY_RUN) store.writeTask(rec);
|
|
413
|
-
fixMsg(rec.taskId, `nullified orphaned feature_id "${rec.feature_id}"`, 'orphaned-fk', 'feature_id');
|
|
414
|
-
} else {
|
|
415
|
-
err(rec.taskId, `feature_id "${rec.feature_id}" references unknown feature`, 'orphaned-fk', 'feature_id', rec.feature_id);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
for (const rec of bugs) {
|
|
421
|
-
if (!rec) continue;
|
|
422
|
-
for (const ref of (rec.similarBugs || [])) {
|
|
423
|
-
if (!bugIds.has(ref)) err(rec.bugId, `similarBugs references unknown bug "${ref}"`, 'orphaned-fk', 'similarBugs', ref);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// --- Events ---
|
|
428
|
-
const allSprints = store.listSprints();
|
|
429
|
-
for (const sprint of allSprints) {
|
|
430
|
-
if (!sprint) continue;
|
|
431
|
-
const sprintId = sprint.sprintId;
|
|
432
|
-
const eventFileEntries = store.listEventFilenames(sprintId)
|
|
433
|
-
.filter(entry => !entry.id.startsWith('_'));
|
|
434
|
-
|
|
435
|
-
for (const entry of eventFileEntries) {
|
|
436
|
-
const filename = entry.id; // filename without .json extension
|
|
437
|
-
const rec = store.getEvent(filename, sprintId);
|
|
438
|
-
if (!rec) continue;
|
|
439
|
-
|
|
440
|
-
const eventId = rec.eventId;
|
|
441
|
-
|
|
442
|
-
if (FIX_MODE) {
|
|
443
|
-
const rules = BACKFILL.event;
|
|
444
|
-
let changed = false;
|
|
445
|
-
for (const [field, derive] of Object.entries(rules)) {
|
|
446
|
-
if (rec[field] === undefined || rec[field] === null || rec[field] === '') {
|
|
447
|
-
const val = derive(rec, filename);
|
|
448
|
-
rec[field] = val;
|
|
449
|
-
fixMsg(`${sprintId}/${filename}`, `backfilled "${field}" = "${val}"`, 'backfill', field);
|
|
450
|
-
changed = true;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// If the filename doesn't match the canonical eventId, resolve the mismatch.
|
|
455
|
-
// When the eventId is a valid filename, rename the file to match it.
|
|
456
|
-
// When the eventId is invalid (contains /, is a placeholder like "temp",
|
|
457
|
-
// or cannot be a filename), backfill eventId to the filename instead.
|
|
458
|
-
if (filename !== rec.eventId) {
|
|
459
|
-
const isValidFilename = (id) => id && !id.includes('/') && !id.includes('\\') && id !== '.';
|
|
460
|
-
if (isValidFilename(rec.eventId)) {
|
|
461
|
-
if (!DRY_RUN) {
|
|
462
|
-
try {
|
|
463
|
-
store.renameEvent(sprintId, filename, rec.eventId);
|
|
464
|
-
} catch (renameErr) {
|
|
465
|
-
err(`${sprintId}/${filename}`, `cannot rename to ${rec.eventId}.json: ${renameErr.message}`, 'filename-mismatch', 'eventId');
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
fixMsg(`${sprintId}/${filename}`, `renamed to ${rec.eventId}.json`, 'filename-mismatch', 'eventId');
|
|
469
|
-
} else {
|
|
470
|
-
// eventId is invalid for a filename — backfill it to the current filename
|
|
471
|
-
fixMsg(`${sprintId}/${filename}`, `eventId "${rec.eventId}" is not a valid filename, resetting to "${filename}"`, 'filename-mismatch', 'eventId');
|
|
472
|
-
rec.eventId = filename;
|
|
473
|
-
changed = true;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Write the updated record (writeEvent now handles ghost detection internally)
|
|
478
|
-
if (changed && !DRY_RUN) store.writeEvent(sprintId, rec);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
for (const e of validateRecord(rec, schemas.event)) err(`${sprintId}/${eventId}`, e.message, e.category, e.field, e.value, e.expected);
|
|
482
|
-
if (rec.taskId && !taskIds.has(rec.taskId) && !bugIds.has(rec.taskId))
|
|
483
|
-
err(`${sprintId}/${eventId}`, `taskId "${rec.taskId}" references unknown task or bug`, 'orphaned-fk', 'taskId', rec.taskId);
|
|
484
|
-
if (rec.sprintId && !sprintIds.has(rec.sprintId) && rec.sprintId !== sprintId)
|
|
485
|
-
err(`${sprintId}/${eventId}`, `sprintId "${rec.sprintId}" references unknown sprint`, 'orphaned-fk', 'sprintId', rec.sprintId);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// --- Pass 3: Filesystem consistency ---
|
|
490
|
-
// Walk engineering/sprints/ to detect orphaned directories and dangling path fields.
|
|
491
|
-
// All checks here emit warnings (not errors) for backward compatibility.
|
|
492
|
-
const sprintsDir = path.join(engineeringRoot, 'sprints');
|
|
493
|
-
if (fs.existsSync(sprintsDir)) {
|
|
494
|
-
let sprintEntries;
|
|
495
|
-
try { sprintEntries = fs.readdirSync(sprintsDir); } catch (_) { sprintEntries = []; }
|
|
496
|
-
|
|
497
|
-
for (const entry of sprintEntries) {
|
|
498
|
-
const entryPath = path.join(sprintsDir, entry);
|
|
499
|
-
let isDir = false;
|
|
500
|
-
try { isDir = fs.statSync(entryPath).isDirectory(); } catch (_) {}
|
|
501
|
-
if (!isDir) continue;
|
|
502
|
-
|
|
503
|
-
const sprintMatch = SPRINT_DIR_RE.exec(entry);
|
|
504
|
-
if (!sprintMatch) continue; // not a recognised sprint directory pattern — skip silently
|
|
505
|
-
const dirSprintId = sprintMatch[1];
|
|
506
|
-
|
|
507
|
-
if (!sprintIds.has(dirSprintId)) {
|
|
508
|
-
warn(dirSprintId, `directory "${entry}" has no sprint record in store`, 'orphan-directory');
|
|
509
|
-
continue; // no point walking tasks for an unregistered sprint
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// Walk for task subdirectories
|
|
513
|
-
let taskEntries;
|
|
514
|
-
try { taskEntries = fs.readdirSync(entryPath); } catch (_) { taskEntries = []; }
|
|
515
|
-
|
|
516
|
-
for (const taskEntry of taskEntries) {
|
|
517
|
-
const taskEntryPath = path.join(entryPath, taskEntry);
|
|
518
|
-
let isTaskDir = false;
|
|
519
|
-
try { isTaskDir = fs.statSync(taskEntryPath).isDirectory(); } catch (_) {}
|
|
520
|
-
if (!isTaskDir) continue;
|
|
521
|
-
|
|
522
|
-
let dirTaskId = null;
|
|
523
|
-
|
|
524
|
-
const taskFullMatch = TASK_FULL_RE.exec(taskEntry);
|
|
525
|
-
if (taskFullMatch) {
|
|
526
|
-
dirTaskId = taskFullMatch[1];
|
|
527
|
-
} else {
|
|
528
|
-
const taskShortMatch = TASK_SHORT_RE.exec(taskEntry);
|
|
529
|
-
if (taskShortMatch) {
|
|
530
|
-
// Construct full task ID from sprint ID + short task number (e.g. T09 → FORGE-S06-T09)
|
|
531
|
-
dirTaskId = `${dirSprintId}-${taskShortMatch[1]}`;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
if (!dirTaskId) continue; // not a recognised task directory pattern — skip silently
|
|
536
|
-
|
|
537
|
-
if (!taskIds.has(dirTaskId)) {
|
|
538
|
-
warn(dirTaskId, `directory "${entry}/${taskEntry}" has no task record in store`, 'orphan-directory');
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// path field cross-check for sprints
|
|
545
|
-
for (const rec of sprints) {
|
|
546
|
-
if (!rec || !rec.path) continue;
|
|
547
|
-
if (!fs.existsSync(rec.path)) {
|
|
548
|
-
warn(rec.sprintId, `path "${rec.path}" does not exist on disk`, 'stale-path', 'path');
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// path field cross-check for tasks
|
|
553
|
-
for (const rec of tasks) {
|
|
554
|
-
if (!rec || !rec.path) continue;
|
|
555
|
-
if (!fs.existsSync(rec.path)) {
|
|
556
|
-
warn(rec.taskId, `path "${rec.path}" does not exist on disk`, 'stale-path', 'path');
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// --- Result ---
|
|
561
|
-
if (JSON_MODE) {
|
|
562
|
-
const result = {
|
|
563
|
-
ok: errorsCount === 0,
|
|
564
|
-
errors: jsonErrors,
|
|
565
|
-
warnings: jsonWarnings,
|
|
566
|
-
fixes: jsonFixes,
|
|
567
|
-
summary: {
|
|
568
|
-
sprints: sprintIds.size,
|
|
569
|
-
tasks: taskIds.size,
|
|
570
|
-
bugs: bugIds.size,
|
|
571
|
-
features: featureIds.size,
|
|
572
|
-
errors: errorsCount,
|
|
573
|
-
warnings: warningsCount,
|
|
574
|
-
fixes: fixesCount
|
|
575
|
-
}
|
|
576
|
-
};
|
|
577
|
-
console.log(JSON.stringify(result, null, 2));
|
|
578
|
-
process.exit(errorsCount === 0 ? 0 : 1);
|
|
579
|
-
} else {
|
|
580
|
-
if (fixesCount > 0) {
|
|
581
|
-
console.log(`${fixesCount} field(s) backfilled.`);
|
|
582
|
-
}
|
|
583
|
-
if (errorsCount === 0) {
|
|
584
|
-
console.log(`Store validation passed (${sprintIds.size} sprint(s), ${taskIds.size} task(s), ${bugIds.size} bug(s)).`);
|
|
585
|
-
if (warningsCount > 0) console.log(`${warningsCount} warning(s).`);
|
|
586
|
-
process.exit(0);
|
|
587
|
-
} else {
|
|
588
|
-
console.error(`\n${errorsCount} error(s) found.`);
|
|
589
|
-
process.exit(1);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
} // end if (require.main === module)
|