chati-dev 4.3.0 → 4.4.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/README.md +18 -13
- package/bin/chati.js +32 -4
- package/framework/agents/build/dev.md +9 -5
- package/framework/agents/discover/brief.md +2 -0
- package/framework/agents/discover/brownfield-wu.md +2 -0
- package/framework/agents/discover/greenfield-wu.md +18 -1
- package/framework/agents/plan/detail.md +2 -0
- 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 +33 -21
- package/framework/config.yaml +5 -4
- package/framework/constitution.md +58 -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/orchestrator/chati-router.js +59 -5
- package/framework/orchestrator/chati-update.md +19 -3
- package/framework/orchestrator/chati.md +20 -1
- package/framework/quality-gates/planning-gate.md +2 -0
- package/framework/rules/human-writing-style.md +47 -0
- package/framework/scaffold/motion-premium/README.md +12 -2
- package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +1 -1
- package/framework/scaffold/motion-premium/scaffold.yaml +6 -1
- package/framework/scaffold/motion-premium-3d/README.md +10 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +1 -1
- package/framework/scaffold/motion-premium-3d/scaffold.yaml +3 -0
- 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 +101 -3
- package/framework/tasks/orchestrator-health.md +4 -4
- package/framework/tasks/qa-impl-verdict.md +11 -5
- package/framework/templates/brandbook-html-tmpl.md +1 -1
- package/framework/templates/qa-gate-tmpl.yaml +24 -11
- package/package.json +4 -3
- 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 +13 -3
- package/src/installer/templates.js +5 -6
- package/src/intelligence/registry-manager.js +1 -1
- 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 +1 -1
- package/src/memory/session-digest.js +9 -4
- package/src/orchestrator/cli.js +186 -15
- package/src/orchestrator/doctor.js +98 -16
- package/src/orchestrator/pipeline-manager.js +44 -22
- package/src/orchestrator/session-manager.js +64 -8
- package/src/telemetry/sender.js +7 -19
- package/src/terminal/run-team.js +3 -3
- package/src/upgrade/tracked-files-detector.js +34 -16
- package/src/utils/feature-flags.js +1 -1
- 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,339 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Registry Updater - Auto-updates entity-registry.yaml
|
|
3
|
-
* Scans project for new/changed/removed entities
|
|
4
|
-
*
|
|
5
|
-
* @module decision/registry-updater
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
9
|
-
import { join, basename, relative } from 'path';
|
|
10
|
-
import yaml from 'js-yaml';
|
|
11
|
-
import { resolveFrameworkDir } from '../utils/framework-dir.js';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Build KNOWN_DIRS using resolveFrameworkDir so paths are correct
|
|
15
|
-
* for both chati.dev/ and .chati.dev/ installations.
|
|
16
|
-
* @param {string} projectDir
|
|
17
|
-
* @returns {{ path: string, type: string }[]}
|
|
18
|
-
*/
|
|
19
|
-
function buildKnownDirs(projectDir) {
|
|
20
|
-
const fw = resolveFrameworkDir(projectDir);
|
|
21
|
-
return [
|
|
22
|
-
{ path: `${fw}/agents`, type: 'agent' },
|
|
23
|
-
{ path: `${fw}/tasks`, type: 'task' },
|
|
24
|
-
{ path: `${fw}/schemas`, type: 'schema' },
|
|
25
|
-
{ path: `${fw}/domains`, type: 'domain' },
|
|
26
|
-
{ path: `${fw}/hooks`, type: 'hook' },
|
|
27
|
-
{ path: `${fw}/workflows`, type: 'workflow' },
|
|
28
|
-
{ path: `${fw}/templates`, type: 'template' },
|
|
29
|
-
];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Scan project for new/changed files and update registry.
|
|
34
|
-
* @param {string} projectDir
|
|
35
|
-
* @param {object} [options] - { dryRun: boolean }
|
|
36
|
-
* @returns {{ added: string[], removed: string[], updated: string[], unchanged: number }}
|
|
37
|
-
*/
|
|
38
|
-
export function updateRegistry(projectDir, options = {}) {
|
|
39
|
-
const { dryRun = false } = options;
|
|
40
|
-
const registryPath = join(projectDir, resolveFrameworkDir(projectDir), 'entity-registry.yaml');
|
|
41
|
-
|
|
42
|
-
let registry = {
|
|
43
|
-
metadata: {
|
|
44
|
-
version: '1.0',
|
|
45
|
-
entity_count: 0,
|
|
46
|
-
last_updated: new Date().toISOString()
|
|
47
|
-
},
|
|
48
|
-
entities: {}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
// Load existing registry
|
|
52
|
-
if (existsSync(registryPath)) {
|
|
53
|
-
registry = yaml.load(readFileSync(registryPath, 'utf8'));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const existingPaths = new Set();
|
|
57
|
-
Object.values(registry.entities || {}).forEach(typeEntities => {
|
|
58
|
-
if (Array.isArray(typeEntities)) {
|
|
59
|
-
typeEntities.forEach(entity => {
|
|
60
|
-
if (entity.path) {
|
|
61
|
-
existingPaths.add(entity.path);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Detect new entities
|
|
68
|
-
const newEntities = detectNewEntities(projectDir);
|
|
69
|
-
const newPaths = new Set(newEntities.map(e => e.path));
|
|
70
|
-
|
|
71
|
-
// Detect removed entities
|
|
72
|
-
const removedEntities = detectRemovedEntities(projectDir);
|
|
73
|
-
const removedPaths = new Set(removedEntities.map(e => e.path));
|
|
74
|
-
|
|
75
|
-
// Track changes
|
|
76
|
-
const changes = {
|
|
77
|
-
added: [],
|
|
78
|
-
removed: [],
|
|
79
|
-
updated: [],
|
|
80
|
-
unchanged: 0
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// Add new entities
|
|
84
|
-
newEntities.forEach(entity => {
|
|
85
|
-
if (!existingPaths.has(entity.path)) {
|
|
86
|
-
changes.added.push(entity.path);
|
|
87
|
-
|
|
88
|
-
if (!dryRun) {
|
|
89
|
-
const type = entity.type;
|
|
90
|
-
if (!registry.entities[type]) {
|
|
91
|
-
registry.entities[type] = [];
|
|
92
|
-
}
|
|
93
|
-
registry.entities[type].push(entity);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// Remove deleted entities
|
|
99
|
-
removedEntities.forEach(entity => {
|
|
100
|
-
if (existingPaths.has(entity.path)) {
|
|
101
|
-
changes.removed.push(entity.path);
|
|
102
|
-
|
|
103
|
-
if (!dryRun) {
|
|
104
|
-
const type = entity.type;
|
|
105
|
-
if (registry.entities[type]) {
|
|
106
|
-
registry.entities[type] = registry.entities[type].filter(
|
|
107
|
-
e => e.path !== entity.path
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// Check for updates (files modified after last registry update)
|
|
115
|
-
existingPaths.forEach(path => {
|
|
116
|
-
if (!removedPaths.has(path) && !newPaths.has(path)) {
|
|
117
|
-
const fullPath = join(projectDir, path);
|
|
118
|
-
if (existsSync(fullPath)) {
|
|
119
|
-
const stats = statSync(fullPath);
|
|
120
|
-
const lastUpdated = registry.metadata?.last_updated
|
|
121
|
-
? new Date(registry.metadata.last_updated)
|
|
122
|
-
: new Date(0);
|
|
123
|
-
|
|
124
|
-
if (stats.mtime > lastUpdated) {
|
|
125
|
-
changes.updated.push(path);
|
|
126
|
-
|
|
127
|
-
if (!dryRun) {
|
|
128
|
-
// Regenerate metadata
|
|
129
|
-
const newMeta = generateEntityMeta(projectDir, path);
|
|
130
|
-
Object.entries(registry.entities).forEach(([type, entities]) => {
|
|
131
|
-
const index = entities.findIndex(e => e.path === path);
|
|
132
|
-
if (index !== -1) {
|
|
133
|
-
registry.entities[type][index] = { ...newMeta, path };
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
} else {
|
|
138
|
-
changes.unchanged++;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// Update metadata
|
|
145
|
-
if (!dryRun) {
|
|
146
|
-
let totalCount = 0;
|
|
147
|
-
Object.values(registry.entities).forEach(typeEntities => {
|
|
148
|
-
if (Array.isArray(typeEntities)) {
|
|
149
|
-
totalCount += typeEntities.length;
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
registry.metadata.entity_count = totalCount;
|
|
154
|
-
registry.metadata.last_updated = new Date().toISOString();
|
|
155
|
-
|
|
156
|
-
writeFileSync(registryPath, yaml.dump(registry), 'utf8');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return changes;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Detect new entities that aren't in the registry.
|
|
164
|
-
* Scans known directories (chati.dev/agents, chati.dev/tasks, etc).
|
|
165
|
-
* @param {string} projectDir
|
|
166
|
-
* @returns {object[]} New entity candidates
|
|
167
|
-
*/
|
|
168
|
-
export function detectNewEntities(projectDir) {
|
|
169
|
-
const registryPath = join(projectDir, resolveFrameworkDir(projectDir), 'entity-registry.yaml');
|
|
170
|
-
const existingPaths = new Set();
|
|
171
|
-
|
|
172
|
-
if (existsSync(registryPath)) {
|
|
173
|
-
const registry = yaml.load(readFileSync(registryPath, 'utf8'));
|
|
174
|
-
Object.values(registry.entities || {}).forEach(typeEntities => {
|
|
175
|
-
if (Array.isArray(typeEntities)) {
|
|
176
|
-
typeEntities.forEach(entity => {
|
|
177
|
-
if (entity.path) {
|
|
178
|
-
existingPaths.add(entity.path);
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const newEntities = [];
|
|
186
|
-
|
|
187
|
-
buildKnownDirs(projectDir).forEach(({ path: dirPath, type }) => {
|
|
188
|
-
const fullPath = join(projectDir, dirPath);
|
|
189
|
-
if (!existsSync(fullPath)) {
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const files = scanDirectory(fullPath);
|
|
194
|
-
files.forEach(file => {
|
|
195
|
-
const relativePath = relative(projectDir, file);
|
|
196
|
-
if (!existingPaths.has(relativePath)) {
|
|
197
|
-
const meta = generateEntityMeta(projectDir, relativePath);
|
|
198
|
-
newEntities.push({
|
|
199
|
-
...meta,
|
|
200
|
-
path: relativePath,
|
|
201
|
-
type
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
return newEntities;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Detect removed entities (in registry but not on disk).
|
|
212
|
-
* @param {string} projectDir
|
|
213
|
-
* @returns {object[]} Removed entity candidates
|
|
214
|
-
*/
|
|
215
|
-
export function detectRemovedEntities(projectDir) {
|
|
216
|
-
const registryPath = join(projectDir, resolveFrameworkDir(projectDir), 'entity-registry.yaml');
|
|
217
|
-
if (!existsSync(registryPath)) {
|
|
218
|
-
return [];
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const registry = yaml.load(readFileSync(registryPath, 'utf8'));
|
|
222
|
-
const removedEntities = [];
|
|
223
|
-
|
|
224
|
-
Object.entries(registry.entities || {}).forEach(([type, entities]) => {
|
|
225
|
-
if (Array.isArray(entities)) {
|
|
226
|
-
entities.forEach(entity => {
|
|
227
|
-
if (entity.path) {
|
|
228
|
-
const fullPath = join(projectDir, entity.path);
|
|
229
|
-
if (!existsSync(fullPath)) {
|
|
230
|
-
removedEntities.push({ ...entity, type });
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
return removedEntities;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Generate entity metadata from file.
|
|
242
|
-
* Extracts type, purpose, keywords from filename and content.
|
|
243
|
-
* @param {string} projectDir
|
|
244
|
-
* @param {string} filePath
|
|
245
|
-
* @returns {object} Entity metadata
|
|
246
|
-
*/
|
|
247
|
-
export function generateEntityMeta(projectDir, filePath) {
|
|
248
|
-
const fullPath = join(projectDir, filePath);
|
|
249
|
-
const fileName = basename(filePath, '.md');
|
|
250
|
-
|
|
251
|
-
const meta = {
|
|
252
|
-
name: fileName,
|
|
253
|
-
keywords: [],
|
|
254
|
-
purpose: ''
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
// Extract keywords from filename
|
|
258
|
-
const nameWords = fileName
|
|
259
|
-
.replace(/[._-]/g, ' ')
|
|
260
|
-
.toLowerCase()
|
|
261
|
-
.split(/\W+/)
|
|
262
|
-
.filter(w => w.length > 3);
|
|
263
|
-
meta.keywords.push(...nameWords);
|
|
264
|
-
|
|
265
|
-
// Try to extract purpose from file content
|
|
266
|
-
if (existsSync(fullPath)) {
|
|
267
|
-
try {
|
|
268
|
-
const content = readFileSync(fullPath, 'utf8');
|
|
269
|
-
const lines = content.split('\n').slice(0, 20); // First 20 lines
|
|
270
|
-
|
|
271
|
-
// Look for common purpose patterns
|
|
272
|
-
for (const line of lines) {
|
|
273
|
-
const purposeMatch = line.match(/^#+\s*(.+)$/); // Markdown heading
|
|
274
|
-
if (purposeMatch) {
|
|
275
|
-
meta.purpose = purposeMatch[1].trim();
|
|
276
|
-
break;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const descMatch = line.match(/^(?:description|purpose|summary):\s*(.+)$/i);
|
|
280
|
-
if (descMatch) {
|
|
281
|
-
meta.purpose = descMatch[1].trim();
|
|
282
|
-
break;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Extract additional keywords from content
|
|
287
|
-
const contentWords = content
|
|
288
|
-
.toLowerCase()
|
|
289
|
-
.split(/\W+/)
|
|
290
|
-
.filter(w => w.length > 5); // Longer words only
|
|
291
|
-
const wordFreq = new Map();
|
|
292
|
-
contentWords.forEach(w => {
|
|
293
|
-
wordFreq.set(w, (wordFreq.get(w) || 0) + 1);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
// Add top 5 frequent words as keywords
|
|
297
|
-
const topWords = [...wordFreq.entries()]
|
|
298
|
-
.sort((a, b) => b[1] - a[1])
|
|
299
|
-
.slice(0, 5)
|
|
300
|
-
.map(([word]) => word);
|
|
301
|
-
meta.keywords.push(...topWords);
|
|
302
|
-
} catch { /* expected: operation may fail gracefully */
|
|
303
|
-
// Ignore read errors
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Remove duplicates
|
|
308
|
-
meta.keywords = [...new Set(meta.keywords)];
|
|
309
|
-
|
|
310
|
-
return meta;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Recursively scan directory for files.
|
|
315
|
-
* @private
|
|
316
|
-
* @param {string} dirPath
|
|
317
|
-
* @returns {string[]} Array of file paths
|
|
318
|
-
*/
|
|
319
|
-
function scanDirectory(dirPath) {
|
|
320
|
-
const files = [];
|
|
321
|
-
|
|
322
|
-
try {
|
|
323
|
-
const entries = readdirSync(dirPath);
|
|
324
|
-
entries.forEach(entry => {
|
|
325
|
-
const fullPath = join(dirPath, entry);
|
|
326
|
-
const stats = statSync(fullPath);
|
|
327
|
-
|
|
328
|
-
if (stats.isDirectory()) {
|
|
329
|
-
files.push(...scanDirectory(fullPath));
|
|
330
|
-
} else if (stats.isFile() && entry.endsWith('.md')) {
|
|
331
|
-
files.push(fullPath);
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
} catch { /* expected: operation may fail gracefully */
|
|
335
|
-
// Ignore read errors
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return files;
|
|
339
|
-
}
|
package/src/extensions/loader.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Extension loader for chati.dev.
|
|
3
|
-
*
|
|
4
|
-
* Discovers and loads project-level extensions from
|
|
5
|
-
* `chati.dev/extensions/*.js`, validates them, and registers
|
|
6
|
-
* them in the extension registry.
|
|
7
|
-
*
|
|
8
|
-
* Constitution Article XIV — Framework Registry Governance.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { existsSync, readdirSync } from 'fs';
|
|
12
|
-
import { join, extname, basename } from 'path';
|
|
13
|
-
import { registerExtension, EXTENSION_POINTS } from './registry.js';
|
|
14
|
-
import { resolveFrameworkDir } from '../utils/framework-dir.js';
|
|
15
|
-
|
|
16
|
-
/** Required fields in an extension module's default export. */
|
|
17
|
-
const REQUIRED_FIELDS = ['name', 'point', 'handler'];
|
|
18
|
-
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// Public API
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Validate an extension object has the required shape.
|
|
25
|
-
*
|
|
26
|
-
* @param {object} ext - Extension object to validate
|
|
27
|
-
* @returns {{ valid: boolean, errors: string[] }}
|
|
28
|
-
*/
|
|
29
|
-
export function validateExtension(ext) {
|
|
30
|
-
const errors = [];
|
|
31
|
-
|
|
32
|
-
if (!ext || typeof ext !== 'object') {
|
|
33
|
-
errors.push('Extension must be a non-null object');
|
|
34
|
-
return { valid: false, errors };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
for (const field of REQUIRED_FIELDS) {
|
|
38
|
-
if (ext[field] === undefined || ext[field] === null) {
|
|
39
|
-
errors.push(`Missing required field: "${field}"`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (ext.name !== undefined && typeof ext.name !== 'string') {
|
|
44
|
-
errors.push('name must be a string');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (ext.point !== undefined) {
|
|
48
|
-
const validPoints = Object.values(EXTENSION_POINTS);
|
|
49
|
-
if (!validPoints.includes(ext.point)) {
|
|
50
|
-
errors.push(`Invalid extension point: "${ext.point}". Valid: ${validPoints.join(', ')}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (ext.handler !== undefined && typeof ext.handler !== 'function') {
|
|
55
|
-
errors.push('handler must be a function');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (ext.priority !== undefined && typeof ext.priority !== 'number') {
|
|
59
|
-
errors.push('priority must be a number');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return { valid: errors.length === 0, errors };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Discover extension files in the project extensions directory.
|
|
67
|
-
*
|
|
68
|
-
* @param {string} projectDir - Project root directory
|
|
69
|
-
* @returns {{ files: string[], dir: string }}
|
|
70
|
-
*/
|
|
71
|
-
export function discoverExtensions(projectDir) {
|
|
72
|
-
if (!projectDir) {
|
|
73
|
-
return { files: [], dir: '' };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const extDir = join(projectDir, resolveFrameworkDir(projectDir), 'extensions');
|
|
77
|
-
|
|
78
|
-
if (!existsSync(extDir)) {
|
|
79
|
-
return { files: [], dir: extDir };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const entries = readdirSync(extDir);
|
|
84
|
-
const jsFiles = entries
|
|
85
|
-
.filter(f => extname(f) === '.js')
|
|
86
|
-
.map(f => join(extDir, f));
|
|
87
|
-
|
|
88
|
-
return { files: jsFiles, dir: extDir };
|
|
89
|
-
} catch { /* expected: directory may not exist */
|
|
90
|
-
return { files: [], dir: extDir };
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Load and register all extensions from a project directory.
|
|
96
|
-
*
|
|
97
|
-
* @param {string} projectDir - Project root directory
|
|
98
|
-
* @returns {Promise<{ loaded: number, failed: number, errors: Array<{ file: string, error: string }> }>}
|
|
99
|
-
*/
|
|
100
|
-
export async function loadProjectExtensions(projectDir) {
|
|
101
|
-
const { files } = discoverExtensions(projectDir);
|
|
102
|
-
let loaded = 0;
|
|
103
|
-
let failed = 0;
|
|
104
|
-
const errors = [];
|
|
105
|
-
|
|
106
|
-
for (const filePath of files) {
|
|
107
|
-
try {
|
|
108
|
-
const mod = await import(filePath);
|
|
109
|
-
const ext = mod.default || mod;
|
|
110
|
-
|
|
111
|
-
const validation = validateExtension(ext);
|
|
112
|
-
if (!validation.valid) {
|
|
113
|
-
failed++;
|
|
114
|
-
errors.push({
|
|
115
|
-
file: basename(filePath),
|
|
116
|
-
error: validation.errors.join('; '),
|
|
117
|
-
});
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const result = registerExtension(ext.point, ext.handler, {
|
|
122
|
-
name: ext.name,
|
|
123
|
-
priority: ext.priority || 0,
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
if (result.registered) {
|
|
127
|
-
loaded++;
|
|
128
|
-
} else {
|
|
129
|
-
failed++;
|
|
130
|
-
errors.push({
|
|
131
|
-
file: basename(filePath),
|
|
132
|
-
error: result.error || 'Registration failed',
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
} catch (err) {
|
|
136
|
-
failed++;
|
|
137
|
-
errors.push({
|
|
138
|
-
file: basename(filePath),
|
|
139
|
-
error: err.message,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return { loaded, failed, errors };
|
|
145
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Extension point registry for chati.dev.
|
|
3
|
-
*
|
|
4
|
-
* Provides a mechanism for registering, retrieving, and
|
|
5
|
-
* executing extension handlers at defined extension points.
|
|
6
|
-
*
|
|
7
|
-
* Constitution Article XIV — Framework Registry Governance.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Constants
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
/** Available extension points in the pipeline lifecycle. */
|
|
15
|
-
export const EXTENSION_POINTS = {
|
|
16
|
-
PRE_AGENT: 'pre_agent',
|
|
17
|
-
POST_AGENT: 'post_agent',
|
|
18
|
-
PRE_BUILD: 'pre_build',
|
|
19
|
-
POST_BUILD: 'post_build',
|
|
20
|
-
HEALTH_CHECK: 'health_check',
|
|
21
|
-
CUSTOM_GATE: 'custom_gate',
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// Internal State
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
/** @type {Map<string, Array<{ handler: Function, name: string, priority: number }>>} */
|
|
29
|
-
const _registry = new Map();
|
|
30
|
-
|
|
31
|
-
// Initialize registry with empty arrays for each point
|
|
32
|
-
for (const point of Object.values(EXTENSION_POINTS)) {
|
|
33
|
-
_registry.set(point, []);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
// Public API
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Register an extension handler at a specific point.
|
|
42
|
-
*
|
|
43
|
-
* @param {string} point - Extension point (from EXTENSION_POINTS)
|
|
44
|
-
* @param {Function} handler - Handler function
|
|
45
|
-
* @param {{ name?: string, priority?: number }} [options={}]
|
|
46
|
-
* @returns {{ registered: boolean, error?: string }}
|
|
47
|
-
*/
|
|
48
|
-
export function registerExtension(point, handler, options = {}) {
|
|
49
|
-
if (!Object.values(EXTENSION_POINTS).includes(point)) {
|
|
50
|
-
return { registered: false, error: `Unknown extension point: ${point}` };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (typeof handler !== 'function') {
|
|
54
|
-
return { registered: false, error: 'Handler must be a function' };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const name = options.name || handler.name || 'anonymous';
|
|
58
|
-
const priority = options.priority || 0;
|
|
59
|
-
|
|
60
|
-
const extensions = _registry.get(point);
|
|
61
|
-
extensions.push({ handler, name, priority });
|
|
62
|
-
|
|
63
|
-
// Sort by priority (higher first)
|
|
64
|
-
extensions.sort((a, b) => b.priority - a.priority);
|
|
65
|
-
|
|
66
|
-
return { registered: true };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Get all extensions registered at a point.
|
|
71
|
-
*
|
|
72
|
-
* @param {string} point - Extension point
|
|
73
|
-
* @returns {Array<{ handler: Function, name: string, priority: number }>}
|
|
74
|
-
*/
|
|
75
|
-
export function getExtensions(point) {
|
|
76
|
-
return _registry.get(point) || [];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Execute all extensions at a point with given context.
|
|
81
|
-
*
|
|
82
|
-
* @param {string} point - Extension point
|
|
83
|
-
* @param {object} context - Context passed to each handler
|
|
84
|
-
* @returns {Promise<{ results: Array<{ name: string, result: any, error?: string }>, executed: number }>}
|
|
85
|
-
*/
|
|
86
|
-
export async function executeExtensions(point, context = {}) {
|
|
87
|
-
const extensions = getExtensions(point);
|
|
88
|
-
const results = [];
|
|
89
|
-
|
|
90
|
-
for (const ext of extensions) {
|
|
91
|
-
try {
|
|
92
|
-
const result = await ext.handler(context);
|
|
93
|
-
results.push({ name: ext.name, result });
|
|
94
|
-
} catch (err) {
|
|
95
|
-
results.push({ name: ext.name, result: null, error: err.message });
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return { results, executed: extensions.length };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Clear all extensions (or extensions at a specific point).
|
|
104
|
-
*
|
|
105
|
-
* @param {string} [point] - Optional point to clear (clears all if omitted)
|
|
106
|
-
*/
|
|
107
|
-
export function clearExtensions(point) {
|
|
108
|
-
if (point) {
|
|
109
|
-
if (_registry.has(point)) {
|
|
110
|
-
_registry.set(point, []);
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
for (const key of _registry.keys()) {
|
|
114
|
-
_registry.set(key, []);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Get count of registered extensions.
|
|
121
|
-
*
|
|
122
|
-
* @returns {{ total: number, byPoint: Record<string, number> }}
|
|
123
|
-
*/
|
|
124
|
-
export function getExtensionStats() {
|
|
125
|
-
const byPoint = {};
|
|
126
|
-
let total = 0;
|
|
127
|
-
|
|
128
|
-
for (const [point, extensions] of _registry.entries()) {
|
|
129
|
-
byPoint[point] = extensions.length;
|
|
130
|
-
total += extensions.length;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return { total, byPoint };
|
|
134
|
-
}
|