devassist-agent 1.0.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +467 -0
  3. package/bin/devassist.js +220 -0
  4. package/package.json +44 -0
  5. package/src/ai/adapter.js +464 -0
  6. package/src/ai/providers/claude.js +80 -0
  7. package/src/ai/providers/hunyuan.js +87 -0
  8. package/src/ai/providers/openai.js +74 -0
  9. package/src/ai/providers/qwen.js +81 -0
  10. package/src/cli/commands/ai.js +944 -0
  11. package/src/cli/commands/ask.js +79 -0
  12. package/src/cli/commands/backup.js +30 -0
  13. package/src/cli/commands/check.js +327 -0
  14. package/src/cli/commands/clean.js +130 -0
  15. package/src/cli/commands/comparison-report.js +326 -0
  16. package/src/cli/commands/convention.js +91 -0
  17. package/src/cli/commands/debt.js +49 -0
  18. package/src/cli/commands/deploy.js +88 -0
  19. package/src/cli/commands/diff.js +193 -0
  20. package/src/cli/commands/doctor.js +186 -0
  21. package/src/cli/commands/fix.js +195 -0
  22. package/src/cli/commands/init.js +431 -0
  23. package/src/cli/commands/inject.js +254 -0
  24. package/src/cli/commands/report.js +310 -0
  25. package/src/cli/commands/restore.js +78 -0
  26. package/src/cli/commands/schema.js +93 -0
  27. package/src/cli/commands/watch.js +212 -0
  28. package/src/cli/shared/code-context.js +51 -0
  29. package/src/cli/shared/config-loader.js +89 -0
  30. package/src/cli/shared/file-collector.js +116 -0
  31. package/src/cli/shared/inline-ignore.js +142 -0
  32. package/src/cli/shared/watch-list.js +281 -0
  33. package/src/core/event-bus.js +83 -0
  34. package/src/core/fsm.js +103 -0
  35. package/src/core/logger.js +54 -0
  36. package/src/core/rule-engine.js +117 -0
  37. package/src/index.js +64 -0
  38. package/src/modules/dev-time/arch-risk-assessor.js +250 -0
  39. package/src/modules/dev-time/code-quality-guard.js +1340 -0
  40. package/src/modules/dev-time/convention-store.js +201 -0
  41. package/src/modules/dev-time/impact-analyzer.js +292 -0
  42. package/src/modules/dev-time/pre-deploy-guard.js +284 -0
  43. package/src/modules/dev-time/schema-registry.js +284 -0
  44. package/src/modules/dev-time/tech-debt-tracker.js +225 -0
  45. package/src/modules/dev-time/version-manager.js +280 -0
  46. package/src/modules/runtime/channel-agent.js +404 -0
  47. package/src/modules/runtime/channel-middleware.js +316 -0
  48. package/src/modules/runtime/index.js +64 -0
  49. package/src/modules/runtime/infrastructure-guard.js +582 -0
  50. package/src/modules/runtime/notification-center.js +443 -0
  51. package/src/modules/runtime/schema-gatekeeper.js +664 -0
  52. package/src/modules/runtime/tool-registry.js +329 -0
  53. package/templates/ci/github-actions.yml +60 -0
  54. package/templates/ci/gitlab-ci.yml +30 -0
  55. package/templates/ci/pre-commit-hook.sh +18 -0
  56. package/templates/git-hooks/pre-commit +61 -0
  57. package/tests/run-all.js +434 -0
  58. package/tests/test-layer2.js +461 -0
  59. package/tests/test-new-rules.js +157 -0
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Watch List — 待观察发现跟踪器
3
+ *
4
+ * 引擎判断不一致的发现会被标记为"待观察",不立即修复,
5
+ * 等实际运行验证后再决定是否处理。
6
+ *
7
+ * 文件位置: .devassist/watch-list.json
8
+ *
9
+ * 数据结构:
10
+ * {
11
+ * items: [
12
+ * {
13
+ * id: "app.js:2781:xss-innerhtml", // 唯一标识(file:line:ruleId)
14
+ * status: "pending" | "resolved" | "dismissed",
15
+ * finding: { severity, message, file, line, ruleId },
16
+ * aiAnalyses: [ { engineName, isFalsePositive, rootCause, confidence } ],
17
+ * note: "待观察:引擎判断不一致,等运行时验证",
18
+ * deferredAt: "2026-07-22T00:00:00.000Z",
19
+ * resolvedAt: null,
20
+ * resolution: null, // "fixed" | "no-issue" | null
21
+ * runtimeNotes: "" // 运行时观察到的现象
22
+ * }
23
+ * ]
24
+ * }
25
+ */
26
+
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+
30
+ const WATCH_LIST_FILE = 'watch-list.json';
31
+
32
+ /**
33
+ * Get the path to the watch list file.
34
+ */
35
+ function getWatchListPath(projectRoot) {
36
+ return path.join(projectRoot, '.devassist', WATCH_LIST_FILE);
37
+ }
38
+
39
+ /**
40
+ * Load the watch list from disk.
41
+ * Returns { items: [] } if file doesn't exist.
42
+ */
43
+ function loadWatchList(projectRoot) {
44
+ const filePath = getWatchListPath(projectRoot);
45
+ if (!fs.existsSync(filePath)) {
46
+ return { items: [] };
47
+ }
48
+ try {
49
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
50
+ if (!Array.isArray(data.items)) data.items = [];
51
+ return data;
52
+ } catch (_) {
53
+ return { items: [] };
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Save the watch list to disk.
59
+ */
60
+ function saveWatchList(projectRoot, watchList) {
61
+ const dir = path.join(projectRoot, '.devassist');
62
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
63
+ const filePath = getWatchListPath(projectRoot);
64
+ fs.writeFileSync(filePath, JSON.stringify(watchList, null, 2), 'utf-8');
65
+ }
66
+
67
+ /**
68
+ * Generate a unique ID for a finding.
69
+ */
70
+ function makeFindingId(finding) {
71
+ return `${finding.file || 'unknown'}:${finding.line || 0}:${finding.ruleId || 'no-rule'}`;
72
+ }
73
+
74
+ /**
75
+ * Determine if a finding's AI analyses show disagreement.
76
+ * Disagreement = at least 2 engines returned results but they disagree on isFalsePositive.
77
+ */
78
+ function isDisagreement(finding) {
79
+ if (!finding.aiAnalyses) return false;
80
+ const available = finding.aiAnalyses.filter(ea => ea.available && ea.analysis);
81
+ if (available.length < 2) return false;
82
+
83
+ const fpValues = available.map(ea => ea.analysis.isFalsePositive);
84
+ const allSame = fpValues.every(v => v === fpValues[0]);
85
+ return !allSame;
86
+ }
87
+
88
+ /**
89
+ * Add disagreement findings to the watch list as "pending".
90
+ * Only adds findings that aren't already tracked.
91
+ * Returns the number of newly added items.
92
+ */
93
+ function addDeferredFindings(projectRoot, findings, defaultNote) {
94
+ const watchList = loadWatchList(projectRoot);
95
+ let added = 0;
96
+
97
+ for (const f of findings) {
98
+ if (!isDisagreement(f)) continue;
99
+
100
+ const id = makeFindingId(f);
101
+ const existing = watchList.items.find(item => item.id === id);
102
+
103
+ if (existing) {
104
+ // Update finding info and analyses (keep status/note if already set)
105
+ existing.finding = {
106
+ severity: f.severity,
107
+ message: f.message,
108
+ file: f.file,
109
+ line: f.line,
110
+ ruleId: f.ruleId,
111
+ };
112
+ existing.aiAnalyses = (f.aiAnalyses || []).map(ea => ({
113
+ engineName: ea.engineName,
114
+ isFalsePositive: ea.analysis?.isFalsePositive,
115
+ rootCause: ea.analysis?.rootCause,
116
+ fix: ea.analysis?.fix,
117
+ confidence: ea.analysis?.confidence,
118
+ }));
119
+ continue;
120
+ }
121
+
122
+ watchList.items.push({
123
+ id,
124
+ status: 'pending',
125
+ finding: {
126
+ severity: f.severity,
127
+ message: f.message,
128
+ file: f.file,
129
+ line: f.line,
130
+ ruleId: f.ruleId,
131
+ },
132
+ aiAnalyses: (f.aiAnalyses || []).map(ea => ({
133
+ engineName: ea.engineName,
134
+ isFalsePositive: ea.analysis?.isFalsePositive,
135
+ rootCause: ea.analysis?.rootCause,
136
+ fix: ea.analysis?.fix,
137
+ confidence: ea.analysis?.confidence,
138
+ })),
139
+ note: defaultNote || '待观察:引擎判断不一致,等运行时验证',
140
+ deferredAt: new Date().toISOString(),
141
+ resolvedAt: null,
142
+ resolution: null,
143
+ runtimeNotes: '',
144
+ });
145
+ added++;
146
+ }
147
+
148
+ saveWatchList(projectRoot, watchList);
149
+ return added;
150
+ }
151
+
152
+ /**
153
+ * Get all pending (unresolved) watch list items.
154
+ */
155
+ function getPending(projectRoot) {
156
+ const watchList = loadWatchList(projectRoot);
157
+ return watchList.items.filter(item => item.status === 'pending');
158
+ }
159
+
160
+ /**
161
+ * Get all watch list items (including resolved/dismissed).
162
+ */
163
+ function getAll(projectRoot) {
164
+ const watchList = loadWatchList(projectRoot);
165
+ return watchList.items;
166
+ }
167
+
168
+ /**
169
+ * Resolve a watch list item — mark as fixed after runtime confirmed issue.
170
+ */
171
+ function resolveItem(projectRoot, idOrIndex, resolution, runtimeNotes) {
172
+ const watchList = loadWatchList(projectRoot);
173
+ const item = findItem(watchList, idOrIndex);
174
+ if (!item) return null;
175
+
176
+ item.status = 'resolved';
177
+ item.resolvedAt = new Date().toISOString();
178
+ item.resolution = resolution || 'fixed';
179
+ if (runtimeNotes) item.runtimeNotes = runtimeNotes;
180
+
181
+ saveWatchList(projectRoot, watchList);
182
+ return item;
183
+ }
184
+
185
+ /**
186
+ * Dismiss a watch list item — runtime confirmed no issue.
187
+ */
188
+ function dismissItem(projectRoot, idOrIndex, runtimeNotes) {
189
+ const watchList = loadWatchList(projectRoot);
190
+ const item = findItem(watchList, idOrIndex);
191
+ if (!item) return null;
192
+
193
+ item.status = 'dismissed';
194
+ item.resolvedAt = new Date().toISOString();
195
+ item.resolution = 'no-issue';
196
+ if (runtimeNotes) item.runtimeNotes = runtimeNotes;
197
+
198
+ saveWatchList(projectRoot, watchList);
199
+ return item;
200
+ }
201
+
202
+ /**
203
+ * Update the note on a watch list item.
204
+ */
205
+ function updateNote(projectRoot, idOrIndex, note) {
206
+ const watchList = loadWatchList(projectRoot);
207
+ const item = findItem(watchList, idOrIndex);
208
+ if (!item) return null;
209
+
210
+ item.note = note;
211
+ saveWatchList(projectRoot, watchList);
212
+ return item;
213
+ }
214
+
215
+ /**
216
+ * Remove a watch list item entirely.
217
+ */
218
+ function removeItem(projectRoot, idOrIndex) {
219
+ const watchList = loadWatchList(projectRoot);
220
+ const idx = findItemIndex(watchList, idOrIndex);
221
+ if (idx === -1) return false;
222
+
223
+ const removed = watchList.items.splice(idx, 1)[0];
224
+ saveWatchList(projectRoot, watchList);
225
+ return removed;
226
+ }
227
+
228
+ /**
229
+ * Find an item by ID string or numeric index.
230
+ */
231
+ function findItem(watchList, idOrIndex) {
232
+ return findItemIndex(watchList, idOrIndex) >= 0
233
+ ? watchList.items[findItemIndex(watchList, idOrIndex)]
234
+ : null;
235
+ }
236
+
237
+ function findItemIndex(watchList, idOrIndex) {
238
+ // Try numeric index (into pending items)
239
+ const idx = parseInt(idOrIndex, 10);
240
+ if (!isNaN(idx)) {
241
+ const pending = watchList.items.filter(item => item.status === 'pending');
242
+ if (idx >= 0 && idx < pending.length) {
243
+ return watchList.items.indexOf(pending[idx]);
244
+ }
245
+ // Also try as direct index into all items
246
+ if (idx >= 0 && idx < watchList.items.length) {
247
+ return idx;
248
+ }
249
+ }
250
+ // Try as ID string
251
+ return watchList.items.findIndex(item => item.id === idOrIndex);
252
+ }
253
+
254
+ /**
255
+ * Get summary stats.
256
+ */
257
+ function getStats(projectRoot) {
258
+ const items = getAll(projectRoot);
259
+ return {
260
+ total: items.length,
261
+ pending: items.filter(i => i.status === 'pending').length,
262
+ resolved: items.filter(i => i.status === 'resolved').length,
263
+ dismissed: items.filter(i => i.status === 'dismissed').length,
264
+ };
265
+ }
266
+
267
+ module.exports = {
268
+ loadWatchList,
269
+ saveWatchList,
270
+ addDeferredFindings,
271
+ isDisagreement,
272
+ makeFindingId,
273
+ getPending,
274
+ getAll,
275
+ resolveItem,
276
+ dismissItem,
277
+ updateNote,
278
+ removeItem,
279
+ getStats,
280
+ getWatchListPath,
281
+ };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * EventBus - lightweight pub/sub event bus
3
+ * Decouples modules: dev-time checks, runtime monitors, AI adapter all communicate via events.
4
+ *
5
+ * Design from the 88-failure report: the #1 cause of "local optimization" bugs was
6
+ * modules not knowing what other modules were doing. EventBus fixes this by making
7
+ * all check results broadcast events that any module can react to.
8
+ */
9
+ class EventBus {
10
+ constructor() {
11
+ this._handlers = new Map();
12
+ this._history = [];
13
+ this._maxHistory = 200;
14
+ }
15
+
16
+ on(event, handler) {
17
+ if (!this._handlers.has(event)) {
18
+ this._handlers.set(event, []);
19
+ }
20
+ this._handlers.get(event).push(handler);
21
+ return () => this.off(event, handler);
22
+ }
23
+
24
+ off(event, handler) {
25
+ const list = this._handlers.get(event);
26
+ if (!list) return;
27
+ const idx = list.indexOf(handler);
28
+ if (idx >= 0) list.splice(idx, 1);
29
+ }
30
+
31
+ async emit(event, payload) {
32
+ const entry = { event, payload, ts: Date.now() };
33
+ this._history.push(entry);
34
+ if (this._history.length > this._maxHistory) {
35
+ this._history.shift();
36
+ }
37
+
38
+ const list = this._handlers.get(event);
39
+ if (!list) return;
40
+
41
+ const errors = [];
42
+ for (const handler of list) {
43
+ try {
44
+ await handler(payload);
45
+ } catch (err) {
46
+ errors.push({ event, handler: handler.name || 'anonymous', err });
47
+ }
48
+ }
49
+ // Also emit a catch-all for logging/monitoring
50
+ const allList = this._handlers.get('*');
51
+ if (allList) {
52
+ for (const handler of allList) {
53
+ try { await handler(entry); } catch (_) {}
54
+ }
55
+ }
56
+ // Don't swallow errors silently — return them for the caller to decide
57
+ if (errors.length > 0) {
58
+ return errors;
59
+ }
60
+ }
61
+
62
+ emitSync(event, payload) {
63
+ const list = this._handlers.get(event);
64
+ if (!list) return;
65
+ for (const handler of list) {
66
+ try { handler(payload); } catch (_) {}
67
+ }
68
+ }
69
+
70
+ getHistory(filter) {
71
+ if (!filter) return this._history.slice();
72
+ return this._history.filter(e => e.event === filter);
73
+ }
74
+
75
+ clear() {
76
+ this._handlers.clear();
77
+ this._history = [];
78
+ }
79
+ }
80
+
81
+ // Singleton instance shared across all modules
82
+ const bus = new EventBus();
83
+ module.exports = { EventBus, bus };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * FSM - Finite State Machine for the DevAssist agent lifecycle.
3
+ *
4
+ * Borrowed from the ChannelAgent design in the 麦克纳姆 project.
5
+ * States: IDLE -> PERCEIVING -> ANALYZING -> DECIDING -> ACTING -> LEARNING -> IDLE
6
+ *
7
+ * The FSM ensures the agent never gets stuck in an infinite loop
8
+ * (failure #7 in the report: RecoveryEngine infinite loop).
9
+ */
10
+
11
+ const STATES = {
12
+ IDLE: 'IDLE',
13
+ PERCEIVING: 'PERCEIVING',
14
+ ANALYZING: 'ANALYZING',
15
+ DECIDING: 'DECIDING',
16
+ ACTING: 'ACTING',
17
+ LEARNING: 'LEARNING',
18
+ ERROR: 'ERROR',
19
+ };
20
+
21
+ const TRANSITIONS = {
22
+ IDLE: ['PERCEIVING', 'ERROR'],
23
+ PERCEIVING: ['ANALYZING', 'IDLE', 'ERROR'],
24
+ ANALYZING: ['DECIDING', 'IDLE', 'ERROR'],
25
+ DECIDING: ['ACTING', 'IDLE', 'ERROR'],
26
+ ACTING: ['LEARNING', 'IDLE', 'ERROR'],
27
+ LEARNING: ['IDLE', 'ERROR'],
28
+ ERROR: ['IDLE'],
29
+ };
30
+
31
+ class FSM {
32
+ constructor() {
33
+ this._state = STATES.IDLE;
34
+ this._history = [{ state: STATES.IDLE, ts: Date.now(), reason: 'init' }];
35
+ this._maxHistory = 50;
36
+ this._maxTransitions = 100; // hard limit to prevent infinite loops
37
+ this._transitionCount = 0;
38
+ this._listeners = new Map();
39
+ }
40
+
41
+ get state() {
42
+ return this._state;
43
+ }
44
+
45
+ get history() {
46
+ return this._history.slice();
47
+ }
48
+
49
+ canTransition(to) {
50
+ const allowed = TRANSITIONS[this._state];
51
+ return allowed ? allowed.includes(to) : false;
52
+ }
53
+
54
+ transition(to, reason) {
55
+ if (!STATES[to]) {
56
+ throw new Error(`Unknown state: ${to}`);
57
+ }
58
+ if (!this.canTransition(to)) {
59
+ throw new Error(`Invalid transition: ${this._state} -> ${to}. Allowed: ${(TRANSITIONS[this._state] || []).join(', ')}`);
60
+ }
61
+
62
+ // Infinite loop protection (failure #7 defense)
63
+ this._transitionCount++;
64
+ if (this._transitionCount > this._maxTransitions) {
65
+ this._state = STATES.ERROR;
66
+ this._history.push({ state: STATES.ERROR, ts: Date.now(), reason: 'max transitions exceeded' });
67
+ if (this._history.length > this._maxHistory) this._history.shift();
68
+ throw new Error(`FSM transition limit (${this._maxTransitions}) exceeded — possible infinite loop. Forcing ERROR state.`);
69
+ }
70
+
71
+ const from = this._state;
72
+ this._state = to;
73
+ this._history.push({ state: to, ts: Date.now(), reason: reason || '', from });
74
+ if (this._history.length > this._maxHistory) this._history.shift();
75
+
76
+ // Notify listeners
77
+ const listeners = this._listeners.get('*') || [];
78
+ const stateListeners = this._listeners.get(to) || [];
79
+ for (const fn of [...listeners, ...stateListeners]) {
80
+ try { fn({ from, to, reason }); } catch (_) {}
81
+ }
82
+ }
83
+
84
+ on(state, fn) {
85
+ if (!this._listeners.has(state)) {
86
+ this._listeners.set(state, []);
87
+ }
88
+ this._listeners.get(state).push(fn);
89
+ }
90
+
91
+ reset() {
92
+ this._state = STATES.IDLE;
93
+ this._transitionCount = 0;
94
+ this._history.push({ state: STATES.IDLE, ts: Date.now(), reason: 'reset' });
95
+ if (this._history.length > this._maxHistory) this._history.shift();
96
+ }
97
+
98
+ isTerminal() {
99
+ return this._state === STATES.IDLE || this._state === STATES.ERROR;
100
+ }
101
+ }
102
+
103
+ module.exports = { FSM, STATES };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Logger - structured logging with severity levels.
3
+ * Keeps a ring buffer for debugging and emits events to EventBus.
4
+ */
5
+ const { bus } = require('./event-bus');
6
+
7
+ const LEVELS = { debug: 10, info: 20, warn: 30, error: 40, fatal: 50 };
8
+ const LEVEL_NAMES = { 10: 'DEBUG', 20: 'INFO', 30: 'WARN', 40: 'ERROR', 50: 'FATAL' };
9
+
10
+ class Logger {
11
+ constructor(name) {
12
+ this.name = name;
13
+ this._level = LEVELS.info;
14
+ this._buffer = [];
15
+ this._maxBuffer = 500;
16
+ }
17
+
18
+ setLevel(level) {
19
+ this._level = LEVELS[level] || LEVELS.info;
20
+ }
21
+
22
+ _log(level, msg, meta) {
23
+ if (level < this._level) return;
24
+ const entry = {
25
+ ts: new Date().toISOString(),
26
+ level: LEVEL_NAMES[level],
27
+ module: this.name,
28
+ msg,
29
+ meta: meta || null,
30
+ };
31
+ this._buffer.push(entry);
32
+ if (this._buffer.length > this._maxBuffer) this._buffer.shift();
33
+
34
+ // Color output for console
35
+ const colors = { DEBUG: '\x1b[90m', INFO: '\x1b[36m', WARN: '\x1b[33m', ERROR: '\x1b[31m', FATAL: '\x1b[35m' };
36
+ const reset = '\x1b[0m';
37
+ const c = colors[entry.level] || '';
38
+ const metaStr = meta ? ' ' + JSON.stringify(meta) : '';
39
+ console.log(`${c}[${entry.level}]${reset} ${entry.module}: ${msg}${metaStr}`);
40
+
41
+ // Emit for EventBus subscribers (e.g. NotificationCenter)
42
+ bus.emitSync('log', entry);
43
+ }
44
+
45
+ debug(msg, meta) { this._log(LEVELS.debug, msg, meta); }
46
+ info(msg, meta) { this._log(LEVELS.info, msg, meta); }
47
+ warn(msg, meta) { this._log(LEVELS.warn, msg, meta); }
48
+ error(msg, meta) { this._log(LEVELS.error, msg, meta); }
49
+ fatal(msg, meta) { this._log(LEVELS.fatal, msg, meta); }
50
+
51
+ getBuffer() { return this._buffer.slice(); }
52
+ }
53
+
54
+ module.exports = { Logger };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * RuleEngine - the deterministic first-pass checker.
3
+ *
4
+ * Core principle (from the design doc): "Rules engine first, AI second."
5
+ * The rule engine runs with zero latency, predictable results, and catches
6
+ * 90% of issues. Only the remaining 10% of "suspect" findings go to the AI.
7
+ *
8
+ * Each rule has:
9
+ * id - unique identifier (e.g. "cq-null-check")
10
+ * severity - 'block' | 'warn' | 'info'
11
+ * category - which module owns this rule
12
+ * check - (ctx) => Finding | null (synchronous, deterministic)
13
+ */
14
+
15
+ /**
16
+ * @typedef {Object} Finding
17
+ * @property {string} ruleId - which rule triggered
18
+ * @property {string} severity - block | warn | info
19
+ * @property {string} category - module category
20
+ * @property {string} message - human-readable description
21
+ * @property {string} file - affected file path
22
+ * @property {number} [line] - line number if available
23
+ * @property {string} [suggestion]- quick fix suggestion
24
+ * @property {object} [context] - extra data for AI deep-analysis
25
+ */
26
+
27
+ class RuleEngine {
28
+ constructor() {
29
+ this._rules = new Map();
30
+ }
31
+
32
+ register(rule) {
33
+ if (!rule.id || !rule.check) {
34
+ throw new Error(`Invalid rule: missing id or check. Got: ${JSON.stringify(rule)}`);
35
+ }
36
+ this._rules.set(rule.id, rule);
37
+ }
38
+
39
+ registerBatch(rules) {
40
+ for (const r of rules) this.register(r);
41
+ }
42
+
43
+ unregister(ruleId) {
44
+ this._rules.delete(ruleId);
45
+ }
46
+
47
+ list() {
48
+ return Array.from(this._rules.values());
49
+ }
50
+
51
+ /**
52
+ * Run all rules (or filtered by category) against a context.
53
+ * @param {object} ctx - { files, config, projectRoot, gitDiff, ... }
54
+ * @param {string} [categoryFilter] - only run rules in this category
55
+ * @returns {{ findings: Finding[], stats: object }}
56
+ */
57
+ run(ctx, categoryFilter) {
58
+ const findings = [];
59
+ let passed = 0;
60
+ let failed = 0;
61
+
62
+ for (const rule of this._rules.values()) {
63
+ if (categoryFilter && rule.category !== categoryFilter) continue;
64
+ try {
65
+ const result = rule.check(ctx);
66
+ if (result) {
67
+ if (Array.isArray(result)) {
68
+ for (const f of result) {
69
+ findings.push({ ...f, ruleId: rule.id, severity: f.severity || rule.severity, category: rule.category });
70
+ }
71
+ } else {
72
+ findings.push({
73
+ ...result,
74
+ ruleId: rule.id,
75
+ severity: result.severity || rule.severity,
76
+ category: rule.category
77
+ });
78
+ }
79
+ failed++;
80
+ } else {
81
+ passed++;
82
+ }
83
+ } catch (err) {
84
+ // A broken rule should not crash the entire engine
85
+ findings.push({
86
+ ruleId: rule.id,
87
+ severity: 'info',
88
+ category: rule.category || 'unknown',
89
+ message: `Rule execution error: ${err.message}`,
90
+ file: ctx.file || '(unknown)',
91
+ context: { error: err.stack }
92
+ });
93
+ }
94
+ }
95
+
96
+ const stats = {
97
+ totalRules: categoryFilter
98
+ ? Array.from(this._rules.values()).filter(r => r.category === categoryFilter).length
99
+ : this._rules.size,
100
+ passed,
101
+ failed,
102
+ findings: findings.length,
103
+ blocks: findings.filter(f => f.severity === 'block').length,
104
+ warnings: findings.filter(f => f.severity === 'warn').length,
105
+ infos: findings.filter(f => f.severity === 'info').length,
106
+ };
107
+
108
+ // Sort: blocks first, then warnings, then info
109
+ const severityOrder = { block: 0, warn: 1, info: 2 };
110
+ findings.sort((a, b) => (severityOrder[a.severity] || 3) - (severityOrder[b.severity] || 3));
111
+
112
+ return { findings, stats };
113
+ }
114
+ }
115
+
116
+ const engine = new RuleEngine();
117
+ module.exports = { RuleEngine, engine };
package/src/index.js ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * DevAssist Agent - main entry point
3
+ *
4
+ * Layer 1 (Dev-time): ConventionStore, SchemaRegistry, ImpactAnalyzer,
5
+ * TechDebtTracker, CodeQualityGuard, ArchRiskAssessor, PreDeployGuard, VersionManager
6
+ *
7
+ * Layer 2 (Runtime base code): ChannelAgent, ChannelMiddleware, ToolRegistry,
8
+ * InfrastructureGuard, NotificationCenter, SchemaGatekeeper
9
+ * (Install into projects via: devassist init --base-code)
10
+ *
11
+ * Layer 3 (AI interface): Unified adapter for Qwen/OpenAI/Claude
12
+ */
13
+
14
+ // Core
15
+ const { EventBus, bus } = require('./core/event-bus');
16
+ const { RuleEngine, engine } = require('./core/rule-engine');
17
+ const { FSM, STATES } = require('./core/fsm');
18
+ const { Logger } = require('./core/logger');
19
+
20
+ // Dev-time modules (Layer 1)
21
+ const { ConventionStore } = require('./modules/dev-time/convention-store');
22
+ const { SchemaRegistry } = require('./modules/dev-time/schema-registry');
23
+ const { ImpactAnalyzer } = require('./modules/dev-time/impact-analyzer');
24
+ const { TechDebtTracker } = require('./modules/dev-time/tech-debt-tracker');
25
+ const { codeQualityRules } = require('./modules/dev-time/code-quality-guard');
26
+ const { ArchRiskAssessor } = require('./modules/dev-time/arch-risk-assessor');
27
+ const { PreDeployGuard } = require('./modules/dev-time/pre-deploy-guard');
28
+ const { VersionManager } = require('./modules/dev-time/version-manager');
29
+
30
+ // Runtime modules (Layer 2)
31
+ const runtime = require('./modules/runtime');
32
+
33
+ // AI adapter (Layer 3)
34
+ const ai = require('./ai/adapter');
35
+
36
+ // Register default rules
37
+ engine.registerBatch(codeQualityRules);
38
+
39
+ module.exports = {
40
+ // Core
41
+ EventBus,
42
+ bus,
43
+ RuleEngine,
44
+ engine,
45
+ FSM,
46
+ STATES,
47
+ Logger,
48
+
49
+ // Dev-time modules (Layer 1)
50
+ ConventionStore,
51
+ SchemaRegistry,
52
+ ImpactAnalyzer,
53
+ TechDebtTracker,
54
+ ArchRiskAssessor,
55
+ PreDeployGuard,
56
+ VersionManager,
57
+ codeQualityRules,
58
+
59
+ // Runtime modules (Layer 2)
60
+ ...runtime,
61
+
62
+ // AI (Layer 3)
63
+ ai,
64
+ };