@soleri/core 9.7.2 → 9.8.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 (41) hide show
  1. package/dist/enforcement/adapters/index.d.ts +15 -0
  2. package/dist/enforcement/adapters/index.d.ts.map +1 -1
  3. package/dist/enforcement/adapters/index.js +38 -0
  4. package/dist/enforcement/adapters/index.js.map +1 -1
  5. package/dist/enforcement/adapters/opencode.d.ts +21 -0
  6. package/dist/enforcement/adapters/opencode.d.ts.map +1 -0
  7. package/dist/enforcement/adapters/opencode.js +115 -0
  8. package/dist/enforcement/adapters/opencode.js.map +1 -0
  9. package/dist/planning/evidence-collector.d.ts +2 -0
  10. package/dist/planning/evidence-collector.d.ts.map +1 -1
  11. package/dist/planning/evidence-collector.js +7 -2
  12. package/dist/planning/evidence-collector.js.map +1 -1
  13. package/dist/planning/plan-lifecycle.d.ts.map +1 -1
  14. package/dist/planning/plan-lifecycle.js +5 -0
  15. package/dist/planning/plan-lifecycle.js.map +1 -1
  16. package/dist/planning/planner-types.d.ts +2 -0
  17. package/dist/planning/planner-types.d.ts.map +1 -1
  18. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  19. package/dist/runtime/orchestrate-ops.js +65 -1
  20. package/dist/runtime/orchestrate-ops.js.map +1 -1
  21. package/dist/runtime/quality-signals.d.ts +42 -0
  22. package/dist/runtime/quality-signals.d.ts.map +1 -0
  23. package/dist/runtime/quality-signals.js +124 -0
  24. package/dist/runtime/quality-signals.js.map +1 -0
  25. package/dist/skills/trust-classifier.js +1 -1
  26. package/dist/skills/trust-classifier.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/enforcement/adapters/index.ts +45 -0
  29. package/src/enforcement/adapters/opencode.test.ts +404 -0
  30. package/src/enforcement/adapters/opencode.ts +153 -0
  31. package/src/planning/evidence-collector.test.ts +95 -0
  32. package/src/planning/evidence-collector.ts +11 -0
  33. package/src/planning/plan-lifecycle.test.ts +49 -0
  34. package/src/planning/plan-lifecycle.ts +5 -0
  35. package/src/planning/planner-types.ts +2 -0
  36. package/src/runtime/orchestrate-ops.test.ts +78 -1
  37. package/src/runtime/orchestrate-ops.ts +91 -1
  38. package/src/runtime/orchestrate-status-readiness.test.ts +162 -0
  39. package/src/runtime/quality-signals.test.ts +312 -0
  40. package/src/runtime/quality-signals.ts +169 -0
  41. package/src/skills/trust-classifier.ts +1 -1
@@ -0,0 +1,312 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { analyzeQualitySignals, captureQualitySignals } from './quality-signals.js';
3
+ import type { EvidenceReport } from '../planning/evidence-collector.js';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Helpers
7
+ // ---------------------------------------------------------------------------
8
+
9
+ function makeReport(overrides: Partial<EvidenceReport> = {}): EvidenceReport {
10
+ return {
11
+ planId: 'plan-test',
12
+ planObjective: 'Test plan',
13
+ accuracy: 80,
14
+ evidenceSources: ['git'],
15
+ taskEvidence: [],
16
+ unplannedChanges: [],
17
+ missingWork: [],
18
+ verificationGaps: [],
19
+ summary: '',
20
+ ...overrides,
21
+ };
22
+ }
23
+
24
+ function makeVault() {
25
+ return {
26
+ search: vi.fn().mockReturnValue([]),
27
+ add: vi.fn(),
28
+ } as unknown as ReturnType<
29
+ (typeof import('../vault/vault.js'))['Vault']['prototype']['search']
30
+ > & { add: ReturnType<typeof vi.fn>; search: ReturnType<typeof vi.fn> };
31
+ }
32
+
33
+ function makeBrain() {
34
+ return {
35
+ recordFeedback: vi.fn(),
36
+ } as unknown as { recordFeedback: ReturnType<typeof vi.fn> };
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // analyzeQualitySignals
41
+ // ---------------------------------------------------------------------------
42
+
43
+ describe('analyzeQualitySignals', () => {
44
+ it('detects anti-pattern when fixIterations > 2', () => {
45
+ const report = makeReport({
46
+ taskEvidence: [
47
+ {
48
+ taskId: 't1',
49
+ taskTitle: 'Fix login bug',
50
+ plannedStatus: 'completed',
51
+ matchedFiles: [],
52
+ verdict: 'DONE',
53
+ fixIterations: 3,
54
+ },
55
+ ],
56
+ });
57
+
58
+ const result = analyzeQualitySignals(report);
59
+
60
+ expect(result.antiPatterns).toHaveLength(1);
61
+ expect(result.antiPatterns[0].taskId).toBe('t1');
62
+ expect(result.antiPatterns[0].kind).toBe('anti-pattern');
63
+ expect(result.antiPatterns[0].fixIterations).toBe(3);
64
+ });
65
+
66
+ it('detects clean task when fixIterations === 0 and verdict DONE', () => {
67
+ const report = makeReport({
68
+ taskEvidence: [
69
+ {
70
+ taskId: 't2',
71
+ taskTitle: 'Add feature',
72
+ plannedStatus: 'completed',
73
+ matchedFiles: [],
74
+ verdict: 'DONE',
75
+ fixIterations: 0,
76
+ },
77
+ ],
78
+ });
79
+
80
+ const result = analyzeQualitySignals(report);
81
+
82
+ expect(result.cleanTasks).toHaveLength(1);
83
+ expect(result.cleanTasks[0].taskId).toBe('t2');
84
+ expect(result.cleanTasks[0].kind).toBe('clean');
85
+ });
86
+
87
+ it('does not flag task with fixIterations === 0 but verdict PARTIAL', () => {
88
+ const report = makeReport({
89
+ taskEvidence: [
90
+ {
91
+ taskId: 't3',
92
+ taskTitle: 'Partial work',
93
+ plannedStatus: 'in_progress',
94
+ matchedFiles: [],
95
+ verdict: 'PARTIAL',
96
+ fixIterations: 0,
97
+ },
98
+ ],
99
+ });
100
+
101
+ const result = analyzeQualitySignals(report);
102
+
103
+ expect(result.cleanTasks).toHaveLength(0);
104
+ expect(result.antiPatterns).toHaveLength(0);
105
+ });
106
+
107
+ it('does not flag task with fixIterations === 2 (at threshold, not above)', () => {
108
+ const report = makeReport({
109
+ taskEvidence: [
110
+ {
111
+ taskId: 't4',
112
+ taskTitle: 'Borderline task',
113
+ plannedStatus: 'completed',
114
+ matchedFiles: [],
115
+ verdict: 'DONE',
116
+ fixIterations: 2,
117
+ },
118
+ ],
119
+ });
120
+
121
+ const result = analyzeQualitySignals(report);
122
+
123
+ expect(result.antiPatterns).toHaveLength(0);
124
+ });
125
+
126
+ it('detects scope creep from unplanned changes', () => {
127
+ const report = makeReport({
128
+ unplannedChanges: [
129
+ {
130
+ file: { path: 'src/extra.ts', status: 'added' },
131
+ possibleReason: 'unplanned scope',
132
+ },
133
+ ],
134
+ });
135
+
136
+ const result = analyzeQualitySignals(report);
137
+
138
+ expect(result.scopeCreep).toHaveLength(1);
139
+ expect(result.scopeCreep[0].kind).toBe('scope-creep');
140
+ });
141
+
142
+ it('handles undefined fixIterations as 0', () => {
143
+ const report = makeReport({
144
+ taskEvidence: [
145
+ {
146
+ taskId: 't5',
147
+ taskTitle: 'No iterations field',
148
+ plannedStatus: 'completed',
149
+ matchedFiles: [],
150
+ verdict: 'DONE',
151
+ // fixIterations omitted
152
+ },
153
+ ],
154
+ });
155
+
156
+ const result = analyzeQualitySignals(report);
157
+
158
+ expect(result.cleanTasks).toHaveLength(1);
159
+ expect(result.antiPatterns).toHaveLength(0);
160
+ });
161
+ });
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // captureQualitySignals
165
+ // ---------------------------------------------------------------------------
166
+
167
+ describe('captureQualitySignals', () => {
168
+ let vault: ReturnType<typeof makeVault>;
169
+ let brain: ReturnType<typeof makeBrain>;
170
+
171
+ beforeEach(() => {
172
+ vault = makeVault();
173
+ brain = makeBrain();
174
+ });
175
+
176
+ it('captures anti-pattern to vault and records negative brain feedback', () => {
177
+ const analysis = {
178
+ antiPatterns: [
179
+ {
180
+ taskId: 't1',
181
+ taskTitle: 'Fix login',
182
+ kind: 'anti-pattern' as const,
183
+ fixIterations: 3,
184
+ verdict: 'DONE',
185
+ },
186
+ ],
187
+ cleanTasks: [],
188
+ scopeCreep: [],
189
+ };
190
+
191
+ const result = captureQualitySignals(analysis, vault, brain, 'plan-1');
192
+
193
+ expect(vault.add).toHaveBeenCalledTimes(1);
194
+ const entry = vault.add.mock.calls[0][0];
195
+ expect(entry.type).toBe('anti-pattern');
196
+ expect(entry.severity).toBe('warning');
197
+ expect(entry.tags).toContain('rework');
198
+ expect(entry.tags).toContain('fix-trail');
199
+ expect(entry.tags).toContain('auto-captured');
200
+
201
+ expect(brain.recordFeedback).toHaveBeenCalledWith(
202
+ 'quality-signal:rework:Fix login',
203
+ 't1',
204
+ 'dismissed',
205
+ );
206
+
207
+ expect(result.captured).toBe(1);
208
+ expect(result.feedback).toBe(1);
209
+ });
210
+
211
+ it('records positive brain feedback for clean tasks', () => {
212
+ const analysis = {
213
+ antiPatterns: [],
214
+ cleanTasks: [
215
+ {
216
+ taskId: 't2',
217
+ taskTitle: 'Add feature',
218
+ kind: 'clean' as const,
219
+ fixIterations: 0,
220
+ verdict: 'DONE',
221
+ },
222
+ ],
223
+ scopeCreep: [],
224
+ };
225
+
226
+ const result = captureQualitySignals(analysis, vault, brain, 'plan-1');
227
+
228
+ expect(brain.recordFeedback).toHaveBeenCalledWith(
229
+ 'quality-signal:clean:Add feature',
230
+ 't2',
231
+ 'accepted',
232
+ );
233
+ expect(result.feedback).toBe(1);
234
+ expect(result.captured).toBe(0);
235
+ });
236
+
237
+ it('skips duplicate anti-patterns when vault search returns high-score match', () => {
238
+ vault.search.mockReturnValue([{ entry: { id: 'existing' }, score: 0.8 }]);
239
+
240
+ const analysis = {
241
+ antiPatterns: [
242
+ {
243
+ taskId: 't1',
244
+ taskTitle: 'Fix login',
245
+ kind: 'anti-pattern' as const,
246
+ fixIterations: 3,
247
+ verdict: 'DONE',
248
+ },
249
+ ],
250
+ cleanTasks: [],
251
+ scopeCreep: [],
252
+ };
253
+
254
+ const result = captureQualitySignals(analysis, vault, brain, 'plan-1');
255
+
256
+ expect(vault.add).not.toHaveBeenCalled();
257
+ expect(result.skipped).toBe(1);
258
+ expect(result.captured).toBe(0);
259
+ // Brain feedback still recorded even for deduplicated captures
260
+ expect(brain.recordFeedback).toHaveBeenCalledTimes(1);
261
+ });
262
+
263
+ it('assigns critical severity for fixIterations > 4', () => {
264
+ const analysis = {
265
+ antiPatterns: [
266
+ {
267
+ taskId: 't1',
268
+ taskTitle: 'Hard bug',
269
+ kind: 'anti-pattern' as const,
270
+ fixIterations: 5,
271
+ verdict: 'DONE',
272
+ },
273
+ ],
274
+ cleanTasks: [],
275
+ scopeCreep: [],
276
+ };
277
+
278
+ captureQualitySignals(analysis, vault, brain, 'plan-1');
279
+
280
+ const entry = vault.add.mock.calls[0][0];
281
+ expect(entry.severity).toBe('critical');
282
+ });
283
+
284
+ it('handles mixed signals correctly', () => {
285
+ const analysis = {
286
+ antiPatterns: [
287
+ {
288
+ taskId: 't1',
289
+ taskTitle: 'Rework task',
290
+ kind: 'anti-pattern' as const,
291
+ fixIterations: 4,
292
+ verdict: 'DONE',
293
+ },
294
+ ],
295
+ cleanTasks: [
296
+ {
297
+ taskId: 't2',
298
+ taskTitle: 'Clean task',
299
+ kind: 'clean' as const,
300
+ fixIterations: 0,
301
+ verdict: 'DONE',
302
+ },
303
+ ],
304
+ scopeCreep: [],
305
+ };
306
+
307
+ const result = captureQualitySignals(analysis, vault, brain, 'plan-1');
308
+
309
+ expect(result.captured).toBe(1);
310
+ expect(result.feedback).toBe(2); // 1 dismissed + 1 accepted
311
+ });
312
+ });
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Quality Signals — analyze evidence reports for rework patterns and clean execution.
3
+ *
4
+ * Extracts anti-patterns (high rework), clean tasks (first-pass success),
5
+ * and scope creep signals from evidence reports. Captures findings to vault
6
+ * and feeds brain feedback. Best-effort — never throws.
7
+ */
8
+
9
+ import type { EvidenceReport } from '../planning/evidence-collector.js';
10
+ import type { Plan } from '../planning/planner.js';
11
+ import type { Vault } from '../vault/vault.js';
12
+ import type { Brain } from '../brain/brain.js';
13
+ import type { IntelligenceEntry } from '../intelligence/types.js';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Types
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export interface QualitySignal {
20
+ taskId: string;
21
+ taskTitle: string;
22
+ kind: 'anti-pattern' | 'clean' | 'scope-creep';
23
+ fixIterations: number;
24
+ verdict: string;
25
+ }
26
+
27
+ export interface QualityAnalysis {
28
+ antiPatterns: QualitySignal[];
29
+ cleanTasks: QualitySignal[];
30
+ scopeCreep: QualitySignal[];
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Thresholds
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /** Tasks with more than this many fix iterations are flagged as anti-patterns. */
38
+ const REWORK_THRESHOLD = 2;
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Analysis
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /**
45
+ * Analyze an evidence report for quality signals.
46
+ *
47
+ * - fixIterations > 2 → anti-pattern (rework)
48
+ * - fixIterations === 0 + verdict DONE → clean (first-pass success)
49
+ * - unplannedChanges → scope-creep signals
50
+ */
51
+ export function analyzeQualitySignals(
52
+ report: EvidenceReport,
53
+ _plan?: Plan | null,
54
+ ): QualityAnalysis {
55
+ const antiPatterns: QualitySignal[] = [];
56
+ const cleanTasks: QualitySignal[] = [];
57
+ const scopeCreep: QualitySignal[] = [];
58
+
59
+ for (const te of report.taskEvidence) {
60
+ const iterations = te.fixIterations ?? 0;
61
+
62
+ if (iterations > REWORK_THRESHOLD) {
63
+ antiPatterns.push({
64
+ taskId: te.taskId,
65
+ taskTitle: te.taskTitle,
66
+ kind: 'anti-pattern',
67
+ fixIterations: iterations,
68
+ verdict: te.verdict,
69
+ });
70
+ } else if (iterations === 0 && te.verdict === 'DONE') {
71
+ cleanTasks.push({
72
+ taskId: te.taskId,
73
+ taskTitle: te.taskTitle,
74
+ kind: 'clean',
75
+ fixIterations: 0,
76
+ verdict: te.verdict,
77
+ });
78
+ }
79
+ }
80
+
81
+ // Unplanned changes signal scope creep
82
+ for (const uc of report.unplannedChanges) {
83
+ scopeCreep.push({
84
+ taskId: 'unplanned',
85
+ taskTitle: uc.file.path,
86
+ kind: 'scope-creep',
87
+ fixIterations: 0,
88
+ verdict: uc.possibleReason,
89
+ });
90
+ }
91
+
92
+ return { antiPatterns, cleanTasks, scopeCreep };
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Capture to vault + brain
97
+ // ---------------------------------------------------------------------------
98
+
99
+ /**
100
+ * Persist quality signals to vault (anti-patterns) and brain (feedback).
101
+ * Deduplicates anti-patterns via vault search before adding.
102
+ * Best-effort — swallows all errors.
103
+ */
104
+ export function captureQualitySignals(
105
+ analysis: QualityAnalysis,
106
+ vault: Vault,
107
+ brain: Brain,
108
+ planId: string,
109
+ ): { captured: number; skipped: number; feedback: number } {
110
+ let captured = 0;
111
+ let skipped = 0;
112
+ let feedback = 0;
113
+
114
+ // Capture anti-patterns to vault (dedup first)
115
+ for (const ap of analysis.antiPatterns) {
116
+ try {
117
+ const query = `rework fix-trail ${ap.taskTitle}`;
118
+ const existing = vault.search(query, { type: 'anti-pattern', limit: 3 });
119
+ const isDuplicate = existing.some((r) => r.score > 0.7);
120
+
121
+ if (isDuplicate) {
122
+ skipped++;
123
+ continue;
124
+ }
125
+
126
+ const severity = ap.fixIterations > 4 ? 'critical' : 'warning';
127
+ const entry: IntelligenceEntry = {
128
+ id: `qs-ap-${planId}-${ap.taskId}-${Date.now()}`,
129
+ type: 'anti-pattern',
130
+ domain: 'engineering',
131
+ title: `Rework detected: ${ap.taskTitle}`,
132
+ severity,
133
+ description:
134
+ `Task "${ap.taskTitle}" required ${ap.fixIterations} fix iterations ` +
135
+ `(threshold: ${REWORK_THRESHOLD}). Investigate root cause — ` +
136
+ `unclear requirements, missing tests, or incomplete understanding.`,
137
+ tags: ['rework', 'fix-trail', 'auto-captured'],
138
+ origin: 'agent',
139
+ };
140
+
141
+ vault.add(entry);
142
+ captured++;
143
+ } catch {
144
+ // Best-effort — skip this signal
145
+ }
146
+ }
147
+
148
+ // Record negative brain feedback for rework tasks
149
+ for (const ap of analysis.antiPatterns) {
150
+ try {
151
+ brain.recordFeedback(`quality-signal:rework:${ap.taskTitle}`, ap.taskId, 'dismissed');
152
+ feedback++;
153
+ } catch {
154
+ // Best-effort
155
+ }
156
+ }
157
+
158
+ // Record positive brain feedback for clean tasks
159
+ for (const ct of analysis.cleanTasks) {
160
+ try {
161
+ brain.recordFeedback(`quality-signal:clean:${ct.taskTitle}`, ct.taskId, 'accepted');
162
+ feedback++;
163
+ } catch {
164
+ // Best-effort
165
+ }
166
+ }
167
+
168
+ return { captured, skipped, feedback };
169
+ }
@@ -92,7 +92,7 @@ function walkDir(rootDir: string, currentDir: string, inventory: SkillInventoryI
92
92
 
93
93
  if (!stat.isFile()) continue;
94
94
 
95
- const relPath = relative(rootDir, fullPath);
95
+ const relPath = relative(rootDir, fullPath).replaceAll('\\', '/');
96
96
  const ext = extname(name).toLowerCase();
97
97
  const kind = classifyFile(name, ext);
98
98