chati-dev 4.3.1 → 4.4.1
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 +18 -13
- package/bin/chati.js +28 -4
- package/framework/agents/build/dev.md +5 -1
- package/framework/agents/discover/greenfield-wu.md +16 -1
- package/framework/agents/plan/tasks.md +31 -0
- package/framework/agents/plan/ux-brand-architect.md +21 -2
- package/framework/agents/plan/ux-component-engineer.md +10 -0
- package/framework/agents/quality/qa-implementation.md +1 -1
- package/framework/agents/quality/qa-planning.md +1 -1
- package/framework/agents/quality/qa-visual.md +24 -21
- package/framework/config.yaml +5 -4
- package/framework/constitution.md +60 -11
- package/framework/context/governance.md +12 -10
- package/framework/context/root.md +2 -2
- package/framework/data/entity-registry.yaml +12 -4
- package/framework/data/qa-rubrics.yaml +335 -0
- package/framework/domains/constitution.yaml +3 -3
- package/framework/executors/json-validate.js +80 -0
- package/framework/executors/npm-script.js +72 -0
- package/framework/executors/path-check.js +67 -0
- package/framework/executors/registry.yaml +36 -0
- package/framework/hooks/advance-trigger.js +47 -3
- package/framework/hooks/constitution-guard.js +9 -4
- package/framework/hooks/git-push-authority.js +113 -0
- package/framework/hooks/license-guard.js +59 -9
- package/framework/hooks/model-governance.js +40 -17
- package/framework/hooks/prism-engine.js +78 -11
- package/framework/hooks/session-digest.js +12 -3
- package/framework/hooks/settings.json +7 -3
- package/framework/hooks/style-guard.js +52 -3
- package/framework/hooks/team-quality-gate.js +43 -5
- package/framework/i18n/en.yaml +3 -3
- package/framework/i18n/es.yaml +3 -3
- package/framework/i18n/fr.yaml +3 -3
- package/framework/i18n/pt.yaml +3 -3
- package/framework/intelligence/confidence.yaml +85 -6
- package/framework/intelligence/context-engine.md +9 -5
- package/framework/intelligence/decision-engine.md +1 -1
- package/framework/manifest.json +1351 -0
- package/framework/manifest.sig +1 -0
- package/framework/orchestrator/chati-router.js +1 -1
- package/framework/orchestrator/chati-update.md +19 -3
- package/framework/orchestrator/chati.md +24 -3
- package/framework/quality-gates/planning-gate.md +2 -0
- package/framework/scaffold/motion-premium/scaffold.yaml +2 -1
- package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +1 -1
- package/framework/scaffold/saas-dashboard/README.md +58 -0
- package/framework/scaffold/saas-dashboard/app/dashboard/page.tsx.template +546 -0
- package/framework/scaffold/saas-dashboard/app/globals.css.template +746 -0
- package/framework/scaffold/saas-dashboard/app/layout.tsx.template +192 -0
- package/framework/scaffold/saas-dashboard/app/shell.css.template +1070 -0
- package/framework/scaffold/saas-dashboard/components/data/Chart.tsx.template +725 -0
- package/framework/scaffold/saas-dashboard/components/data/DataTable.tsx.template +623 -0
- package/framework/scaffold/saas-dashboard/components/data/DataTable.types.ts.template +104 -0
- package/framework/scaffold/saas-dashboard/components/data/DataTablePagination.tsx.template +234 -0
- package/framework/scaffold/saas-dashboard/components/data/SkeletonBlock.tsx.template +111 -0
- package/framework/scaffold/saas-dashboard/components/data/StatCard.tsx.template +393 -0
- package/framework/scaffold/saas-dashboard/components/data/StatCardGrid.tsx.template +121 -0
- package/framework/scaffold/saas-dashboard/components/feedback/EmptyState.tsx.template +281 -0
- package/framework/scaffold/saas-dashboard/components/interaction/CommandPalette.tsx.template +767 -0
- package/framework/scaffold/saas-dashboard/components/interaction/Field.tsx.template +351 -0
- package/framework/scaffold/saas-dashboard/components/interaction/Form.tsx.template +424 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Badge.tsx.template +193 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Button.tsx.template +261 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Card.tsx.template +287 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Input.tsx.template +310 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Spinner.tsx.template +135 -0
- package/framework/scaffold/saas-dashboard/components/primitives/index.ts.template +60 -0
- package/framework/scaffold/saas-dashboard/components/shell/AppShell.tsx.template +174 -0
- package/framework/scaffold/saas-dashboard/components/shell/Breadcrumb.tsx.template +186 -0
- package/framework/scaffold/saas-dashboard/components/shell/Container.tsx.template +86 -0
- package/framework/scaffold/saas-dashboard/components/shell/NavItem.tsx.template +147 -0
- package/framework/scaffold/saas-dashboard/components/shell/Sidebar.tsx.template +378 -0
- package/framework/scaffold/saas-dashboard/components/shell/SidebarContext.tsx.template +169 -0
- package/framework/scaffold/saas-dashboard/components/shell/ThemeToggle.tsx.template +128 -0
- package/framework/scaffold/saas-dashboard/components/shell/Topbar.tsx.template +301 -0
- package/framework/scaffold/saas-dashboard/lib/brand.ts.template +324 -0
- package/framework/scaffold/saas-dashboard/scaffold.yaml +225 -0
- package/framework/schemas/session.schema.json +5 -0
- package/framework/schemas/task.schema.json +9 -0
- package/framework/scripts/visual-qa.js +87 -1
- package/framework/tasks/orchestrator-health.md +4 -4
- package/framework/tasks/qa-impl-verdict.md +11 -5
- package/framework/templates/qa-gate-tmpl.yaml +24 -11
- package/package.json +4 -4
- package/src/config/claude-settings-generator.js +9 -6
- package/src/config/context-file-generator.js +0 -6
- package/src/dashboard/renderer.js +0 -36
- package/src/executors/runner.js +204 -0
- package/src/installer/core.js +35 -4
- package/src/installer/manifest.js +30 -4
- package/src/installer/signing-public-key.pem +1 -1
- package/src/installer/templates.js +5 -6
- package/src/installer/validator.js +2 -1
- package/src/intelligence/registry-manager.js +3 -2
- package/src/license/client.js +9 -3
- package/src/license/commands.js +12 -2
- package/src/license/machine-id.js +42 -1
- package/src/memory/gotchas.js +58 -40
- package/src/memory/magic-docs.js +7 -2
- package/src/memory/session-digest.js +9 -4
- package/src/orchestrator/cli.js +195 -23
- package/src/orchestrator/deviation-handler.js +5 -3
- package/src/orchestrator/doctor.js +98 -16
- package/src/orchestrator/pipeline-manager.js +44 -22
- package/src/orchestrator/session-manager.js +56 -8
- package/src/telemetry/sender.js +7 -19
- package/src/terminal/run-parallel.js +14 -14
- package/src/terminal/run-team.js +3 -3
- package/src/upgrade/backup.js +22 -44
- package/src/upgrade/migrator.js +51 -12
- package/src/upgrade/tracked-files-detector.js +34 -16
- package/src/utils/constitution-meta.js +12 -0
- package/src/utils/feature-flags.js +26 -2
- package/src/utils/flatten-entities.js +4 -40
- package/src/utils/schema-validator.js +0 -14
- package/src/wizard/i18n.js +3 -3
- package/src/api/index.js +0 -120
- package/src/autonomy/autonomous-gate.js +0 -294
- package/src/autonomy/build-loop.js +0 -281
- package/src/autonomy/build-state.js +0 -286
- package/src/autonomy/cause-analyzer.js +0 -177
- package/src/autonomy/escalation.js +0 -214
- package/src/autonomy/index.js +0 -51
- package/src/autonomy/mode-manager.js +0 -225
- package/src/autonomy/mode-suggester.js +0 -283
- package/src/autonomy/progress-reporter.js +0 -275
- package/src/autonomy/safety-net.js +0 -370
- package/src/config/agent-customizer.js +0 -231
- package/src/decision/analyzer.js +0 -291
- package/src/decision/engine.js +0 -250
- package/src/decision/index.js +0 -38
- package/src/decision/registry-healer.js +0 -468
- package/src/decision/registry-updater.js +0 -339
- package/src/extensions/loader.js +0 -145
- package/src/extensions/registry.js +0 -134
- package/src/gates/circuit-breaker.js +0 -151
- package/src/gates/g1-planning-complete.js +0 -154
- package/src/gates/g2-qa-planning.js +0 -156
- package/src/gates/g3-implementation.js +0 -215
- package/src/gates/g4-qa-implementation.js +0 -240
- package/src/gates/g5-deploy-ready.js +0 -181
- package/src/gates/gate-base.js +0 -185
- package/src/gates/index.js +0 -46
- package/src/health/auto-fix.js +0 -216
- package/src/health/engine.js +0 -246
- package/src/merger/semantic-merger.js +0 -292
- package/src/preview/detector.js +0 -238
- package/src/preview/index.js +0 -20
- package/src/preview/launcher.js +0 -235
- package/src/preview/log-buffer.js +0 -103
- package/src/quality/metrics-collector.js +0 -281
- package/src/quality/test-runner.js +0 -366
- package/src/tasks/executor.js +0 -195
- package/src/tasks/index.js +0 -4
- package/src/tasks/loader.js +0 -210
- package/src/tasks/router.js +0 -182
- package/src/utils/event-bus.js +0 -126
|
@@ -1,468 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Registry Healer - Self-healing for entity-registry.yaml
|
|
3
|
-
* Detects and fixes 6 types of registry issues
|
|
4
|
-
*
|
|
5
|
-
* @module decision/registry-healer
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { readFileSync, writeFileSync, existsSync, statSync, copyFileSync, mkdirSync } from 'fs';
|
|
9
|
-
import { join, isAbsolute } from 'path';
|
|
10
|
-
import yaml from 'js-yaml';
|
|
11
|
-
import { resolveFrameworkDir } from '../utils/framework-dir.js';
|
|
12
|
-
import { detectNewEntities, detectRemovedEntities, generateEntityMeta } from './registry-updater.js';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Load and parse the entity registry YAML once.
|
|
16
|
-
* @param {string} projectDir
|
|
17
|
-
* @returns {{ registry: object|null, registryPath: string }}
|
|
18
|
-
*/
|
|
19
|
-
function loadRegistry(projectDir) {
|
|
20
|
-
const registryPath = join(projectDir, resolveFrameworkDir(projectDir), 'entity-registry.yaml');
|
|
21
|
-
if (!existsSync(registryPath)) {
|
|
22
|
-
return { registry: null, registryPath };
|
|
23
|
-
}
|
|
24
|
-
const registry = yaml.load(readFileSync(registryPath, 'utf8'));
|
|
25
|
-
return { registry, registryPath };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Run all healing rules and return diagnosis.
|
|
30
|
-
* @param {string} projectDir
|
|
31
|
-
* @param {object} [options] - { autoFix: boolean }
|
|
32
|
-
* @returns {{ healthy: boolean, issues: object[], fixes: object[] }}
|
|
33
|
-
*/
|
|
34
|
-
export function healRegistry(projectDir, options = {}) {
|
|
35
|
-
const { autoFix = false } = options;
|
|
36
|
-
|
|
37
|
-
const issues = [];
|
|
38
|
-
const fixes = [];
|
|
39
|
-
|
|
40
|
-
// Load registry once for rules 3-6
|
|
41
|
-
const { registry } = loadRegistry(projectDir);
|
|
42
|
-
|
|
43
|
-
// Rule 1: Missing entities
|
|
44
|
-
const missingIssues = detectMissingEntities(projectDir);
|
|
45
|
-
issues.push(...missingIssues);
|
|
46
|
-
if (autoFix) {
|
|
47
|
-
missingIssues.forEach(issue => {
|
|
48
|
-
fixes.push({
|
|
49
|
-
rule: 'missing_entity',
|
|
50
|
-
action: 'add',
|
|
51
|
-
path: issue.path,
|
|
52
|
-
entity: issue.entity
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Rule 2: Orphaned entries
|
|
58
|
-
const orphanedIssues = detectOrphanedEntries(projectDir);
|
|
59
|
-
issues.push(...orphanedIssues);
|
|
60
|
-
if (autoFix) {
|
|
61
|
-
orphanedIssues.forEach(issue => {
|
|
62
|
-
fixes.push({
|
|
63
|
-
rule: 'orphaned_entry',
|
|
64
|
-
action: 'remove',
|
|
65
|
-
path: issue.path
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Rule 3: Stale metadata
|
|
71
|
-
const staleIssues = detectStaleMetadata(projectDir, registry);
|
|
72
|
-
issues.push(...staleIssues);
|
|
73
|
-
if (autoFix) {
|
|
74
|
-
staleIssues.forEach(issue => {
|
|
75
|
-
fixes.push({
|
|
76
|
-
rule: 'stale_metadata',
|
|
77
|
-
action: 'update',
|
|
78
|
-
path: issue.path,
|
|
79
|
-
newMeta: issue.newMeta
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Rule 4: Duplicates
|
|
85
|
-
const duplicateIssues = detectDuplicates(projectDir, registry);
|
|
86
|
-
issues.push(...duplicateIssues);
|
|
87
|
-
if (autoFix) {
|
|
88
|
-
duplicateIssues.forEach(issue => {
|
|
89
|
-
fixes.push({
|
|
90
|
-
rule: 'duplicate_entry',
|
|
91
|
-
action: 'deduplicate',
|
|
92
|
-
path: issue.path,
|
|
93
|
-
count: issue.count
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Rule 5: Invalid paths
|
|
99
|
-
const invalidPathIssues = detectInvalidPaths(projectDir, registry);
|
|
100
|
-
issues.push(...invalidPathIssues);
|
|
101
|
-
if (autoFix) {
|
|
102
|
-
invalidPathIssues.forEach(issue => {
|
|
103
|
-
fixes.push({
|
|
104
|
-
rule: 'invalid_path',
|
|
105
|
-
action: 'remove',
|
|
106
|
-
path: issue.path,
|
|
107
|
-
reason: issue.reason
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Rule 6: Count mismatch
|
|
113
|
-
const countIssues = detectCountMismatch(projectDir, registry);
|
|
114
|
-
issues.push(...countIssues);
|
|
115
|
-
if (autoFix) {
|
|
116
|
-
countIssues.forEach(issue => {
|
|
117
|
-
fixes.push({
|
|
118
|
-
rule: 'count_mismatch',
|
|
119
|
-
action: 'fix_count',
|
|
120
|
-
expected: issue.actual,
|
|
121
|
-
current: issue.recorded
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Apply fixes if requested
|
|
127
|
-
if (autoFix && fixes.length > 0) {
|
|
128
|
-
const result = applyFixes(projectDir, fixes);
|
|
129
|
-
return {
|
|
130
|
-
healthy: issues.length === 0,
|
|
131
|
-
issues,
|
|
132
|
-
fixes,
|
|
133
|
-
applied: result.applied,
|
|
134
|
-
backup: result.backup
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
healthy: issues.length === 0,
|
|
140
|
-
issues,
|
|
141
|
-
fixes
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Rule 1: Missing entities (file exists but not in registry).
|
|
147
|
-
* @param {string} projectDir
|
|
148
|
-
* @returns {object[]} Issues found
|
|
149
|
-
*/
|
|
150
|
-
export function detectMissingEntities(projectDir) {
|
|
151
|
-
const newEntities = detectNewEntities(projectDir);
|
|
152
|
-
return newEntities.map(entity => ({
|
|
153
|
-
rule: 'missing_entity',
|
|
154
|
-
severity: 'warning',
|
|
155
|
-
path: entity.path,
|
|
156
|
-
message: `File exists but not in registry: ${entity.path}`,
|
|
157
|
-
entity
|
|
158
|
-
}));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Rule 2: Orphaned entries (in registry but file doesn't exist).
|
|
163
|
-
* @param {string} projectDir
|
|
164
|
-
* @returns {object[]} Issues found
|
|
165
|
-
*/
|
|
166
|
-
export function detectOrphanedEntries(projectDir) {
|
|
167
|
-
const removedEntities = detectRemovedEntities(projectDir);
|
|
168
|
-
return removedEntities.map(entity => ({
|
|
169
|
-
rule: 'orphaned_entry',
|
|
170
|
-
severity: 'error',
|
|
171
|
-
path: entity.path,
|
|
172
|
-
message: `Registry entry exists but file not found: ${entity.path}`
|
|
173
|
-
}));
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Rule 3: Stale metadata (file modified after registry entry).
|
|
178
|
-
* @param {string} projectDir
|
|
179
|
-
* @param {object} [preloaded] - Pre-loaded registry object (avoids redundant file read)
|
|
180
|
-
* @returns {object[]} Issues found
|
|
181
|
-
*/
|
|
182
|
-
export function detectStaleMetadata(projectDir, preloaded) {
|
|
183
|
-
const registry = preloaded || loadRegistry(projectDir).registry;
|
|
184
|
-
if (!registry) {
|
|
185
|
-
return [];
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const lastUpdated = registry.metadata?.last_updated
|
|
189
|
-
? new Date(registry.metadata.last_updated)
|
|
190
|
-
: new Date(0);
|
|
191
|
-
|
|
192
|
-
const issues = [];
|
|
193
|
-
|
|
194
|
-
Object.entries(registry.entities || {}).forEach(([_type, entities]) => {
|
|
195
|
-
if (Array.isArray(entities)) {
|
|
196
|
-
entities.forEach(entity => {
|
|
197
|
-
if (entity.path) {
|
|
198
|
-
const fullPath = join(projectDir, entity.path);
|
|
199
|
-
if (existsSync(fullPath)) {
|
|
200
|
-
const stats = statSync(fullPath);
|
|
201
|
-
if (stats.mtime > lastUpdated) {
|
|
202
|
-
const newMeta = generateEntityMeta(projectDir, entity.path);
|
|
203
|
-
issues.push({
|
|
204
|
-
rule: 'stale_metadata',
|
|
205
|
-
severity: 'info',
|
|
206
|
-
path: entity.path,
|
|
207
|
-
message: `File modified after registry update: ${entity.path}`,
|
|
208
|
-
newMeta
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
return issues;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Rule 4: Duplicate entries (same path appears twice).
|
|
222
|
-
* @param {string} projectDir
|
|
223
|
-
* @param {object} [preloaded] - Pre-loaded registry object (avoids redundant file read)
|
|
224
|
-
* @returns {object[]} Issues found
|
|
225
|
-
*/
|
|
226
|
-
export function detectDuplicates(projectDir, preloaded) {
|
|
227
|
-
const registry = preloaded || loadRegistry(projectDir).registry;
|
|
228
|
-
if (!registry) {
|
|
229
|
-
return [];
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const pathCounts = new Map();
|
|
233
|
-
const issues = [];
|
|
234
|
-
|
|
235
|
-
Object.values(registry.entities || {}).forEach(entities => {
|
|
236
|
-
if (Array.isArray(entities)) {
|
|
237
|
-
entities.forEach(entity => {
|
|
238
|
-
if (entity.path) {
|
|
239
|
-
pathCounts.set(entity.path, (pathCounts.get(entity.path) || 0) + 1);
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
pathCounts.forEach((count, path) => {
|
|
246
|
-
if (count > 1) {
|
|
247
|
-
issues.push({
|
|
248
|
-
rule: 'duplicate_entry',
|
|
249
|
-
severity: 'error',
|
|
250
|
-
path,
|
|
251
|
-
count,
|
|
252
|
-
message: `Duplicate registry entries for path: ${path} (${count} times)`
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
return issues;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Rule 5: Invalid paths (malformed or absolute paths).
|
|
262
|
-
* @param {string} projectDir
|
|
263
|
-
* @param {object} [preloaded] - Pre-loaded registry object (avoids redundant file read)
|
|
264
|
-
* @returns {object[]} Issues found
|
|
265
|
-
*/
|
|
266
|
-
export function detectInvalidPaths(projectDir, preloaded) {
|
|
267
|
-
const registry = preloaded || loadRegistry(projectDir).registry;
|
|
268
|
-
if (!registry) {
|
|
269
|
-
return [];
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const issues = [];
|
|
273
|
-
|
|
274
|
-
Object.values(registry.entities || {}).forEach(entities => {
|
|
275
|
-
if (Array.isArray(entities)) {
|
|
276
|
-
entities.forEach(entity => {
|
|
277
|
-
if (!entity.path) {
|
|
278
|
-
issues.push({
|
|
279
|
-
rule: 'invalid_path',
|
|
280
|
-
severity: 'error',
|
|
281
|
-
path: '<missing>',
|
|
282
|
-
reason: 'Missing path field',
|
|
283
|
-
message: 'Entity missing path field'
|
|
284
|
-
});
|
|
285
|
-
} else if (isAbsolute(entity.path)) {
|
|
286
|
-
issues.push({
|
|
287
|
-
rule: 'invalid_path',
|
|
288
|
-
severity: 'error',
|
|
289
|
-
path: entity.path,
|
|
290
|
-
reason: 'Absolute path',
|
|
291
|
-
message: `Path should be relative: ${entity.path}`
|
|
292
|
-
});
|
|
293
|
-
} else if (entity.path.includes('..')) {
|
|
294
|
-
issues.push({
|
|
295
|
-
rule: 'invalid_path',
|
|
296
|
-
severity: 'error',
|
|
297
|
-
path: entity.path,
|
|
298
|
-
reason: 'Contains parent directory reference',
|
|
299
|
-
message: `Path contains ..: ${entity.path}`
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
return issues;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Rule 6: Entity count mismatch (metadata.entity_count != actual).
|
|
311
|
-
* @param {string} projectDir
|
|
312
|
-
* @param {object} [preloaded] - Pre-loaded registry object (avoids redundant file read)
|
|
313
|
-
* @returns {object[]} Issues found
|
|
314
|
-
*/
|
|
315
|
-
export function detectCountMismatch(projectDir, preloaded) {
|
|
316
|
-
const registry = preloaded || loadRegistry(projectDir).registry;
|
|
317
|
-
if (!registry) {
|
|
318
|
-
return [];
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
let actualCount = 0;
|
|
322
|
-
|
|
323
|
-
Object.values(registry.entities || {}).forEach(entities => {
|
|
324
|
-
if (Array.isArray(entities)) {
|
|
325
|
-
actualCount += entities.length;
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
const recordedCount = registry.metadata?.entity_count || 0;
|
|
330
|
-
|
|
331
|
-
if (actualCount !== recordedCount) {
|
|
332
|
-
return [{
|
|
333
|
-
rule: 'count_mismatch',
|
|
334
|
-
severity: 'warning',
|
|
335
|
-
actual: actualCount,
|
|
336
|
-
recorded: recordedCount,
|
|
337
|
-
message: `Entity count mismatch: metadata says ${recordedCount}, actual is ${actualCount}`
|
|
338
|
-
}];
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return [];
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Apply fixes and create backup before modifying.
|
|
346
|
-
* @param {string} projectDir
|
|
347
|
-
* @param {object[]} fixes
|
|
348
|
-
* @returns {{ applied: number, backup: string }}
|
|
349
|
-
*/
|
|
350
|
-
export function applyFixes(projectDir, fixes) {
|
|
351
|
-
const registryPath = join(projectDir, resolveFrameworkDir(projectDir), 'entity-registry.yaml');
|
|
352
|
-
|
|
353
|
-
if (!existsSync(registryPath)) {
|
|
354
|
-
return { applied: 0, backup: null };
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Create backup
|
|
358
|
-
const backupDir = join(projectDir, '.chati', 'backups');
|
|
359
|
-
if (!existsSync(backupDir)) {
|
|
360
|
-
mkdirSync(backupDir, { recursive: true });
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
364
|
-
const backupPath = join(backupDir, `entity-registry-${timestamp}.yaml`);
|
|
365
|
-
copyFileSync(registryPath, backupPath);
|
|
366
|
-
|
|
367
|
-
// Load registry (fresh read for modification — applyFixes is the write path)
|
|
368
|
-
const registry = yaml.load(readFileSync(registryPath, 'utf8'));
|
|
369
|
-
|
|
370
|
-
let applied = 0;
|
|
371
|
-
|
|
372
|
-
fixes.forEach(fix => {
|
|
373
|
-
try {
|
|
374
|
-
switch (fix.action) {
|
|
375
|
-
case 'add':
|
|
376
|
-
// Add missing entity
|
|
377
|
-
if (fix.entity) {
|
|
378
|
-
const type = fix.entity.type;
|
|
379
|
-
if (!registry.entities[type]) {
|
|
380
|
-
registry.entities[type] = [];
|
|
381
|
-
}
|
|
382
|
-
registry.entities[type].push(fix.entity);
|
|
383
|
-
applied++;
|
|
384
|
-
}
|
|
385
|
-
break;
|
|
386
|
-
|
|
387
|
-
case 'remove':
|
|
388
|
-
// Remove orphaned entry
|
|
389
|
-
Object.keys(registry.entities).forEach(type => {
|
|
390
|
-
if (Array.isArray(registry.entities[type])) {
|
|
391
|
-
registry.entities[type] = registry.entities[type].filter(
|
|
392
|
-
e => e.path !== fix.path
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
});
|
|
396
|
-
applied++;
|
|
397
|
-
break;
|
|
398
|
-
|
|
399
|
-
case 'update':
|
|
400
|
-
// Update stale metadata
|
|
401
|
-
Object.keys(registry.entities).forEach(type => {
|
|
402
|
-
if (Array.isArray(registry.entities[type])) {
|
|
403
|
-
const index = registry.entities[type].findIndex(
|
|
404
|
-
e => e.path === fix.path
|
|
405
|
-
);
|
|
406
|
-
if (index !== -1) {
|
|
407
|
-
registry.entities[type][index] = {
|
|
408
|
-
...registry.entities[type][index],
|
|
409
|
-
...fix.newMeta
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
});
|
|
414
|
-
applied++;
|
|
415
|
-
break;
|
|
416
|
-
|
|
417
|
-
case 'deduplicate':
|
|
418
|
-
// Remove duplicate entries (keep first occurrence)
|
|
419
|
-
Object.keys(registry.entities).forEach(type => {
|
|
420
|
-
if (Array.isArray(registry.entities[type])) {
|
|
421
|
-
const seen = new Set();
|
|
422
|
-
registry.entities[type] = registry.entities[type].filter(e => {
|
|
423
|
-
if (e.path === fix.path) {
|
|
424
|
-
if (seen.has(e.path)) {
|
|
425
|
-
return false; // Remove duplicate
|
|
426
|
-
}
|
|
427
|
-
seen.add(e.path);
|
|
428
|
-
}
|
|
429
|
-
return true;
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
applied++;
|
|
434
|
-
break;
|
|
435
|
-
|
|
436
|
-
case 'fix_count':
|
|
437
|
-
// Fix entity count
|
|
438
|
-
registry.metadata = registry.metadata || {};
|
|
439
|
-
registry.metadata.entity_count = fix.expected;
|
|
440
|
-
applied++;
|
|
441
|
-
break;
|
|
442
|
-
}
|
|
443
|
-
} catch { /* expected: operation may fail gracefully */
|
|
444
|
-
// Skip failed fixes
|
|
445
|
-
}
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
// Update metadata
|
|
449
|
-
registry.metadata = registry.metadata || {};
|
|
450
|
-
registry.metadata.last_updated = new Date().toISOString();
|
|
451
|
-
|
|
452
|
-
// Recalculate entity count
|
|
453
|
-
let totalCount = 0;
|
|
454
|
-
Object.values(registry.entities).forEach(entities => {
|
|
455
|
-
if (Array.isArray(entities)) {
|
|
456
|
-
totalCount += entities.length;
|
|
457
|
-
}
|
|
458
|
-
});
|
|
459
|
-
registry.metadata.entity_count = totalCount;
|
|
460
|
-
|
|
461
|
-
// Write updated registry
|
|
462
|
-
writeFileSync(registryPath, yaml.dump(registry), 'utf8');
|
|
463
|
-
|
|
464
|
-
return {
|
|
465
|
-
applied,
|
|
466
|
-
backup: backupPath
|
|
467
|
-
};
|
|
468
|
-
}
|