pumuki-ast-hooks 5.5.60 → 5.6.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 +361 -1101
- package/bin/__tests__/check-version.spec.js +32 -57
- package/docs/ARCHITECTURE.md +66 -1
- package/docs/TODO.md +41 -0
- package/docs/images/ast_intelligence_01.svg +40 -0
- package/docs/images/ast_intelligence_02.svg +39 -0
- package/docs/images/ast_intelligence_03.svg +55 -0
- package/docs/images/ast_intelligence_04.svg +39 -0
- package/docs/images/ast_intelligence_05.svg +45 -0
- package/docs/images/logo.png +0 -0
- package/package.json +1 -1
- package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +20 -0
- package/scripts/hooks-system/application/DIValidationService.js +43 -0
- package/scripts/hooks-system/application/__tests__/DIValidationService.spec.js +81 -0
- package/scripts/hooks-system/bin/__tests__/check-version.spec.js +37 -57
- package/scripts/hooks-system/bin/cli.js +109 -0
- package/scripts/hooks-system/config/di-rules.json +42 -0
- package/scripts/hooks-system/domain/ports/FileSystemPort.js +19 -0
- package/scripts/hooks-system/domain/strategies/ConcreteDependencyStrategy.js +78 -0
- package/scripts/hooks-system/domain/strategies/DIStrategy.js +31 -0
- package/scripts/hooks-system/infrastructure/adapters/NodeFileSystemAdapter.js +28 -0
- package/scripts/hooks-system/infrastructure/ast/ast-core.js +124 -0
- package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +19 -1
- package/scripts/hooks-system/infrastructure/ast/backend/detectors/god-class-detector.js +28 -8
- package/scripts/hooks-system/infrastructure/ast/common/ast-common.js +133 -0
- package/scripts/hooks-system/infrastructure/ast/frontend/analyzers/__tests__/FrontendArchitectureDetector.spec.js +4 -1
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/__tests__/iOSASTIntelligentAnalyzer.spec.js +3 -1
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +3 -2
- package/scripts/hooks-system/infrastructure/ast/ios/ast-ios.js +1 -1
- package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-ast-intelligent-strategies.js +40 -46
- package/scripts/hooks-system/infrastructure/cascade-hooks/README.md +114 -0
- package/scripts/hooks-system/infrastructure/cascade-hooks/cascade-hooks-config.json +20 -0
- package/scripts/hooks-system/infrastructure/cascade-hooks/claude-code-hook.sh +127 -0
- package/scripts/hooks-system/infrastructure/cascade-hooks/post-write-code-hook.js +72 -0
- package/scripts/hooks-system/infrastructure/cascade-hooks/pre-write-code-hook.js +167 -0
- package/scripts/hooks-system/infrastructure/cascade-hooks/universal-hook-adapter.js +186 -0
- package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js +739 -24
- package/scripts/hooks-system/infrastructure/observability/MetricsCollector.js +221 -0
- package/scripts/hooks-system/infrastructure/observability/index.js +23 -0
- package/scripts/hooks-system/infrastructure/orchestration/__tests__/intelligent-audit.spec.js +177 -0
- package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +87 -1
- package/scripts/hooks-system/infrastructure/registry/StrategyRegistry.js +63 -0
- package/scripts/hooks-system/infrastructure/resilience/CircuitBreaker.js +229 -0
- package/scripts/hooks-system/infrastructure/resilience/RetryPolicy.js +141 -0
- package/scripts/hooks-system/infrastructure/resilience/index.js +34 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* MetricsCollector - Enterprise Observability for AST Intelligence
|
|
4
|
+
* =============================================================================
|
|
5
|
+
* Collects and exposes metrics in Prometheus format
|
|
6
|
+
* Supports: counters, gauges, histograms
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class MetricsCollector {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.prefix = options.prefix || 'ast_intelligence';
|
|
12
|
+
this.labels = options.defaultLabels || {};
|
|
13
|
+
this.metrics = new Map();
|
|
14
|
+
this.histogramBuckets = options.histogramBuckets || [0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
counter(name, help, labelNames = []) {
|
|
18
|
+
const key = `${this.prefix}_${name}`;
|
|
19
|
+
if (!this.metrics.has(key)) {
|
|
20
|
+
this.metrics.set(key, {
|
|
21
|
+
type: 'counter',
|
|
22
|
+
name: key,
|
|
23
|
+
help,
|
|
24
|
+
labelNames,
|
|
25
|
+
values: new Map()
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
inc: (labels = {}, value = 1) => this._incCounter(key, labels, value)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
gauge(name, help, labelNames = []) {
|
|
34
|
+
const key = `${this.prefix}_${name}`;
|
|
35
|
+
if (!this.metrics.has(key)) {
|
|
36
|
+
this.metrics.set(key, {
|
|
37
|
+
type: 'gauge',
|
|
38
|
+
name: key,
|
|
39
|
+
help,
|
|
40
|
+
labelNames,
|
|
41
|
+
values: new Map()
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
set: (labels = {}, value) => this._setGauge(key, labels, value),
|
|
46
|
+
inc: (labels = {}, value = 1) => this._incGauge(key, labels, value),
|
|
47
|
+
dec: (labels = {}, value = 1) => this._decGauge(key, labels, value)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
histogram(name, help, labelNames = [], buckets = null) {
|
|
52
|
+
const key = `${this.prefix}_${name}`;
|
|
53
|
+
if (!this.metrics.has(key)) {
|
|
54
|
+
this.metrics.set(key, {
|
|
55
|
+
type: 'histogram',
|
|
56
|
+
name: key,
|
|
57
|
+
help,
|
|
58
|
+
labelNames,
|
|
59
|
+
buckets: buckets || this.histogramBuckets,
|
|
60
|
+
values: new Map()
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
observe: (labels = {}, value) => this._observeHistogram(key, labels, value)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_labelKey(labels) {
|
|
69
|
+
return JSON.stringify(labels);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_incCounter(key, labels, value) {
|
|
73
|
+
const metric = this.metrics.get(key);
|
|
74
|
+
if (!metric) return;
|
|
75
|
+
const labelKey = this._labelKey(labels);
|
|
76
|
+
const current = metric.values.get(labelKey) || 0;
|
|
77
|
+
metric.values.set(labelKey, current + value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_setGauge(key, labels, value) {
|
|
81
|
+
const metric = this.metrics.get(key);
|
|
82
|
+
if (!metric) return;
|
|
83
|
+
const labelKey = this._labelKey(labels);
|
|
84
|
+
metric.values.set(labelKey, value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_incGauge(key, labels, value) {
|
|
88
|
+
const metric = this.metrics.get(key);
|
|
89
|
+
if (!metric) return;
|
|
90
|
+
const labelKey = this._labelKey(labels);
|
|
91
|
+
const current = metric.values.get(labelKey) || 0;
|
|
92
|
+
metric.values.set(labelKey, current + value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_decGauge(key, labels, value) {
|
|
96
|
+
const metric = this.metrics.get(key);
|
|
97
|
+
if (!metric) return;
|
|
98
|
+
const labelKey = this._labelKey(labels);
|
|
99
|
+
const current = metric.values.get(labelKey) || 0;
|
|
100
|
+
metric.values.set(labelKey, current - value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
_observeHistogram(key, labels, value) {
|
|
104
|
+
const metric = this.metrics.get(key);
|
|
105
|
+
if (!metric) return;
|
|
106
|
+
const labelKey = this._labelKey(labels);
|
|
107
|
+
|
|
108
|
+
if (!metric.values.has(labelKey)) {
|
|
109
|
+
metric.values.set(labelKey, {
|
|
110
|
+
sum: 0,
|
|
111
|
+
count: 0,
|
|
112
|
+
buckets: new Map(metric.buckets.map(b => [b, 0]))
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const data = metric.values.get(labelKey);
|
|
117
|
+
data.sum += value;
|
|
118
|
+
data.count += 1;
|
|
119
|
+
|
|
120
|
+
for (const bucket of metric.buckets) {
|
|
121
|
+
if (value <= bucket) {
|
|
122
|
+
data.buckets.set(bucket, data.buckets.get(bucket) + 1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
toPrometheusFormat() {
|
|
128
|
+
const lines = [];
|
|
129
|
+
|
|
130
|
+
for (const [, metric] of this.metrics) {
|
|
131
|
+
lines.push(`# HELP ${metric.name} ${metric.help}`);
|
|
132
|
+
lines.push(`# TYPE ${metric.name} ${metric.type}`);
|
|
133
|
+
|
|
134
|
+
for (const [labelKey, value] of metric.values) {
|
|
135
|
+
const labels = JSON.parse(labelKey);
|
|
136
|
+
const labelStr = Object.entries(labels)
|
|
137
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
138
|
+
.join(',');
|
|
139
|
+
|
|
140
|
+
if (metric.type === 'histogram') {
|
|
141
|
+
for (const [bucket, count] of value.buckets) {
|
|
142
|
+
const bucketLabels = labelStr ? `${labelStr},le="${bucket}"` : `le="${bucket}"`;
|
|
143
|
+
lines.push(`${metric.name}_bucket{${bucketLabels}} ${count}`);
|
|
144
|
+
}
|
|
145
|
+
const infLabels = labelStr ? `${labelStr},le="+Inf"` : `le="+Inf"`;
|
|
146
|
+
lines.push(`${metric.name}_bucket{${infLabels}} ${value.count}`);
|
|
147
|
+
lines.push(`${metric.name}_sum{${labelStr}} ${value.sum}`);
|
|
148
|
+
lines.push(`${metric.name}_count{${labelStr}} ${value.count}`);
|
|
149
|
+
} else {
|
|
150
|
+
if (labelStr) {
|
|
151
|
+
lines.push(`${metric.name}{${labelStr}} ${value}`);
|
|
152
|
+
} else {
|
|
153
|
+
lines.push(`${metric.name} ${value}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return lines.join('\n');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getMetricsJSON() {
|
|
163
|
+
const result = {};
|
|
164
|
+
for (const [key, metric] of this.metrics) {
|
|
165
|
+
result[key] = {
|
|
166
|
+
type: metric.type,
|
|
167
|
+
help: metric.help,
|
|
168
|
+
values: Object.fromEntries(metric.values)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
reset() {
|
|
175
|
+
for (const [, metric] of this.metrics) {
|
|
176
|
+
metric.values.clear();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const globalCollector = new MetricsCollector();
|
|
182
|
+
|
|
183
|
+
const gateCheckCounter = globalCollector.counter(
|
|
184
|
+
'gate_check_total',
|
|
185
|
+
'Total number of AI gate checks',
|
|
186
|
+
['status', 'branch_type']
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const gateCheckDuration = globalCollector.histogram(
|
|
190
|
+
'gate_check_duration_seconds',
|
|
191
|
+
'Duration of AI gate checks in seconds',
|
|
192
|
+
['status']
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const violationsGauge = globalCollector.gauge(
|
|
196
|
+
'violations_current',
|
|
197
|
+
'Current number of violations',
|
|
198
|
+
['severity']
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const mcpToolCallCounter = globalCollector.counter(
|
|
202
|
+
'mcp_tool_call_total',
|
|
203
|
+
'Total MCP tool calls',
|
|
204
|
+
['tool', 'success']
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const evidenceAgeGauge = globalCollector.gauge(
|
|
208
|
+
'evidence_age_seconds',
|
|
209
|
+
'Age of AI evidence in seconds',
|
|
210
|
+
[]
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
MetricsCollector,
|
|
215
|
+
globalCollector,
|
|
216
|
+
gateCheckCounter,
|
|
217
|
+
gateCheckDuration,
|
|
218
|
+
violationsGauge,
|
|
219
|
+
mcpToolCallCounter,
|
|
220
|
+
evidenceAgeGauge
|
|
221
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability Module - Metrics & Monitoring
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
MetricsCollector,
|
|
7
|
+
globalCollector,
|
|
8
|
+
gateCheckCounter,
|
|
9
|
+
gateCheckDuration,
|
|
10
|
+
violationsGauge,
|
|
11
|
+
mcpToolCallCounter,
|
|
12
|
+
evidenceAgeGauge
|
|
13
|
+
} = require('./MetricsCollector');
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
MetricsCollector,
|
|
17
|
+
globalCollector,
|
|
18
|
+
gateCheckCounter,
|
|
19
|
+
gateCheckDuration,
|
|
20
|
+
violationsGauge,
|
|
21
|
+
mcpToolCallCounter,
|
|
22
|
+
evidenceAgeGauge
|
|
23
|
+
};
|
package/scripts/hooks-system/infrastructure/orchestration/__tests__/intelligent-audit.spec.js
CHANGED
|
@@ -196,3 +196,180 @@ describe('AI_EVIDENCE.json structure validation', () => {
|
|
|
196
196
|
});
|
|
197
197
|
});
|
|
198
198
|
});
|
|
199
|
+
|
|
200
|
+
describe('Cognitive Memory Layers', () => {
|
|
201
|
+
const createMockEvidenceWithLayers = (humanIntentOverride = null) => ({
|
|
202
|
+
timestamp: new Date().toISOString(),
|
|
203
|
+
platforms: {
|
|
204
|
+
backend: { detected: true, violations: 0 },
|
|
205
|
+
frontend: { detected: false, violations: 0 },
|
|
206
|
+
ios: { detected: false, violations: 0 },
|
|
207
|
+
android: { detected: false, violations: 0 }
|
|
208
|
+
},
|
|
209
|
+
current_context: {
|
|
210
|
+
working_on: 'Test',
|
|
211
|
+
current_branch: 'feature/test',
|
|
212
|
+
base_branch: 'develop'
|
|
213
|
+
},
|
|
214
|
+
session_id: 'session-123-abc',
|
|
215
|
+
watchers: {
|
|
216
|
+
token_monitor: { enabled: true },
|
|
217
|
+
evidence_watcher: { auto_refresh: true }
|
|
218
|
+
},
|
|
219
|
+
protocol_3_questions: { answered: true },
|
|
220
|
+
human_intent: humanIntentOverride,
|
|
221
|
+
semantic_snapshot: null
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('human_intent layer (Intentional Memory)', () => {
|
|
225
|
+
it('should initialize human_intent with empty structure when not present', () => {
|
|
226
|
+
const { preserveOrInitHumanIntent } = jest.requireActual('../intelligent-audit');
|
|
227
|
+
|
|
228
|
+
if (typeof preserveOrInitHumanIntent !== 'function') {
|
|
229
|
+
const evidence = createMockEvidenceWithLayers(null);
|
|
230
|
+
expect(evidence.human_intent).toBeNull();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const evidence = createMockEvidenceWithLayers(null);
|
|
235
|
+
const result = preserveOrInitHumanIntent(evidence);
|
|
236
|
+
|
|
237
|
+
expect(result).toBeDefined();
|
|
238
|
+
expect(result.primary_goal).toBeNull();
|
|
239
|
+
expect(result.secondary_goals).toEqual([]);
|
|
240
|
+
expect(result.non_goals).toEqual([]);
|
|
241
|
+
expect(result.constraints).toEqual([]);
|
|
242
|
+
expect(result.confidence_level).toBe('unset');
|
|
243
|
+
expect(result._hint).toBeDefined();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should preserve existing human_intent if not expired', () => {
|
|
247
|
+
const futureDate = new Date(Date.now() + 86400000).toISOString();
|
|
248
|
+
const existingIntent = {
|
|
249
|
+
primary_goal: 'Implement feature X',
|
|
250
|
+
secondary_goals: ['Add tests'],
|
|
251
|
+
non_goals: ['Refactor unrelated code'],
|
|
252
|
+
constraints: ['No breaking changes'],
|
|
253
|
+
confidence_level: 'high',
|
|
254
|
+
set_by: 'user',
|
|
255
|
+
set_at: new Date().toISOString(),
|
|
256
|
+
expires_at: futureDate,
|
|
257
|
+
preservation_count: 2
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const evidence = createMockEvidenceWithLayers(existingIntent);
|
|
261
|
+
|
|
262
|
+
expect(evidence.human_intent).toBeDefined();
|
|
263
|
+
expect(evidence.human_intent.primary_goal).toBe('Implement feature X');
|
|
264
|
+
expect(evidence.human_intent.preservation_count).toBe(2);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should have required human_intent contract fields', () => {
|
|
268
|
+
const requiredFields = [
|
|
269
|
+
'primary_goal',
|
|
270
|
+
'secondary_goals',
|
|
271
|
+
'non_goals',
|
|
272
|
+
'constraints',
|
|
273
|
+
'confidence_level'
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
const emptyIntent = {
|
|
277
|
+
primary_goal: null,
|
|
278
|
+
secondary_goals: [],
|
|
279
|
+
non_goals: [],
|
|
280
|
+
constraints: [],
|
|
281
|
+
confidence_level: 'unset'
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
requiredFields.forEach(field => {
|
|
285
|
+
expect(emptyIntent).toHaveProperty(field);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('semantic_snapshot layer (Semantic Memory)', () => {
|
|
291
|
+
it('should have required semantic_snapshot contract fields when generated', () => {
|
|
292
|
+
const snapshot = {
|
|
293
|
+
generated_at: new Date().toISOString(),
|
|
294
|
+
derivation_source: 'auto:updateAIEvidence',
|
|
295
|
+
context_hash: 'ctx-abc123',
|
|
296
|
+
summary: {
|
|
297
|
+
health_score: 100,
|
|
298
|
+
gate_status: 'PASSED',
|
|
299
|
+
active_platforms: ['backend'],
|
|
300
|
+
violation_count: 0,
|
|
301
|
+
violation_preview: 'No violations',
|
|
302
|
+
branch: 'feature/test',
|
|
303
|
+
session_id: 'session-123-abc'
|
|
304
|
+
},
|
|
305
|
+
feature_state: {
|
|
306
|
+
ai_gate_enabled: true,
|
|
307
|
+
token_monitoring: true,
|
|
308
|
+
auto_refresh: true,
|
|
309
|
+
protocol_3_active: true
|
|
310
|
+
},
|
|
311
|
+
decisions: {
|
|
312
|
+
last_gate_decision: 'allow',
|
|
313
|
+
blocking_reason: null,
|
|
314
|
+
recommended_action: 'proceed_with_development'
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
expect(snapshot.generated_at).toBeDefined();
|
|
319
|
+
expect(snapshot.derivation_source).toBe('auto:updateAIEvidence');
|
|
320
|
+
expect(snapshot.summary).toBeDefined();
|
|
321
|
+
expect(snapshot.summary.health_score).toBeGreaterThanOrEqual(0);
|
|
322
|
+
expect(snapshot.summary.health_score).toBeLessThanOrEqual(100);
|
|
323
|
+
expect(snapshot.feature_state).toBeDefined();
|
|
324
|
+
expect(snapshot.decisions).toBeDefined();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should calculate health_score based on violations', () => {
|
|
328
|
+
const noViolationsScore = 100;
|
|
329
|
+
const withCriticalScore = Math.max(0, 100 - 20);
|
|
330
|
+
const withHighScore = Math.max(0, 100 - 10);
|
|
331
|
+
const withManyViolations = Math.max(0, 100 - (10 * 5));
|
|
332
|
+
|
|
333
|
+
expect(noViolationsScore).toBe(100);
|
|
334
|
+
expect(withCriticalScore).toBe(80);
|
|
335
|
+
expect(withHighScore).toBe(90);
|
|
336
|
+
expect(withManyViolations).toBe(50);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should derive recommended_action from gate status', () => {
|
|
340
|
+
const passedDecision = {
|
|
341
|
+
last_gate_decision: 'allow',
|
|
342
|
+
recommended_action: 'proceed_with_development'
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const blockedDecision = {
|
|
346
|
+
last_gate_decision: 'block',
|
|
347
|
+
recommended_action: 'fix_violations_before_commit'
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
expect(passedDecision.recommended_action).toBe('proceed_with_development');
|
|
351
|
+
expect(blockedDecision.recommended_action).toBe('fix_violations_before_commit');
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe('Layer integration', () => {
|
|
356
|
+
it('should include both layers in complete evidence structure', () => {
|
|
357
|
+
const completeEvidence = {
|
|
358
|
+
timestamp: new Date().toISOString(),
|
|
359
|
+
severity_metrics: { total_violations: 0 },
|
|
360
|
+
ai_gate: { status: 'ALLOWED' },
|
|
361
|
+
human_intent: {
|
|
362
|
+
primary_goal: null,
|
|
363
|
+
confidence_level: 'unset'
|
|
364
|
+
},
|
|
365
|
+
semantic_snapshot: {
|
|
366
|
+
generated_at: new Date().toISOString(),
|
|
367
|
+
summary: { health_score: 100 }
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
expect(completeEvidence.human_intent).toBeDefined();
|
|
372
|
+
expect(completeEvidence.semantic_snapshot).toBeDefined();
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
});
|
|
@@ -25,6 +25,88 @@ function deriveCategoryFromRuleId(ruleId) {
|
|
|
25
25
|
return parts[0] || 'unknown';
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Generate semantic_snapshot automatically from current evidence state.
|
|
30
|
+
* This is the Semantic Memory Layer - derived, never manually input.
|
|
31
|
+
*/
|
|
32
|
+
function generateSemanticSnapshot(evidence, violations, gateResult) {
|
|
33
|
+
const now = new Date();
|
|
34
|
+
const activePlatforms = Object.entries(evidence.platforms || {})
|
|
35
|
+
.filter(([, v]) => v.detected)
|
|
36
|
+
.map(([k]) => k);
|
|
37
|
+
|
|
38
|
+
const violationSummary = violations.length > 0
|
|
39
|
+
? violations.slice(0, 5).map(v => `${v.severity}: ${v.ruleId || v.rule || 'unknown'}`).join('; ')
|
|
40
|
+
: 'No violations';
|
|
41
|
+
|
|
42
|
+
const healthScore = Math.max(0, 100 - (violations.length * 5) -
|
|
43
|
+
(violations.filter(v => v.severity === 'CRITICAL').length * 20) -
|
|
44
|
+
(violations.filter(v => v.severity === 'HIGH').length * 10));
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
generated_at: now.toISOString(),
|
|
48
|
+
derivation_source: 'auto:updateAIEvidence',
|
|
49
|
+
context_hash: `ctx-${Date.now().toString(36)}`,
|
|
50
|
+
summary: {
|
|
51
|
+
health_score: healthScore,
|
|
52
|
+
gate_status: gateResult.passed ? 'PASSED' : 'FAILED',
|
|
53
|
+
active_platforms: activePlatforms,
|
|
54
|
+
violation_count: violations.length,
|
|
55
|
+
violation_preview: violationSummary,
|
|
56
|
+
branch: evidence.current_context?.current_branch || 'unknown',
|
|
57
|
+
session_id: evidence.session_id || 'unknown'
|
|
58
|
+
},
|
|
59
|
+
feature_state: {
|
|
60
|
+
ai_gate_enabled: true,
|
|
61
|
+
token_monitoring: evidence.watchers?.token_monitor?.enabled ?? true,
|
|
62
|
+
auto_refresh: evidence.watchers?.evidence_watcher?.auto_refresh ?? true,
|
|
63
|
+
protocol_3_active: evidence.protocol_3_questions?.answered ?? false
|
|
64
|
+
},
|
|
65
|
+
decisions: {
|
|
66
|
+
last_gate_decision: gateResult.passed ? 'allow' : 'block',
|
|
67
|
+
blocking_reason: gateResult.blockedBy || null,
|
|
68
|
+
recommended_action: gateResult.passed
|
|
69
|
+
? 'proceed_with_development'
|
|
70
|
+
: 'fix_violations_before_commit'
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Preserve or initialize human_intent layer.
|
|
77
|
+
* This is the Intentional Memory Layer - set by human, preserved across updates.
|
|
78
|
+
*/
|
|
79
|
+
function preserveOrInitHumanIntent(existingEvidence) {
|
|
80
|
+
const existing = existingEvidence.human_intent;
|
|
81
|
+
|
|
82
|
+
if (existing && typeof existing === 'object' && existing.primary_goal) {
|
|
83
|
+
const expiresAt = existing.expires_at ? new Date(existing.expires_at) : null;
|
|
84
|
+
const isExpired = expiresAt && expiresAt < new Date();
|
|
85
|
+
|
|
86
|
+
if (!isExpired) {
|
|
87
|
+
return {
|
|
88
|
+
...existing,
|
|
89
|
+
preserved_at: new Date().toISOString(),
|
|
90
|
+
preservation_count: (existing.preservation_count || 0) + 1
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
primary_goal: null,
|
|
97
|
+
secondary_goals: [],
|
|
98
|
+
non_goals: [],
|
|
99
|
+
constraints: [],
|
|
100
|
+
confidence_level: 'unset',
|
|
101
|
+
set_by: null,
|
|
102
|
+
set_at: null,
|
|
103
|
+
expires_at: null,
|
|
104
|
+
preserved_at: new Date().toISOString(),
|
|
105
|
+
preservation_count: 0,
|
|
106
|
+
_hint: 'Set via CLI: ast-hooks intent --goal "your goal" or manually edit this file'
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
28
110
|
function detectPlatformsFromStagedFiles(stagedFiles) {
|
|
29
111
|
const platforms = new Set();
|
|
30
112
|
const files = Array.isArray(stagedFiles) ? stagedFiles : [];
|
|
@@ -669,8 +751,12 @@ async function updateAIEvidence(violations, gateResult, tokenUsage) {
|
|
|
669
751
|
}
|
|
670
752
|
};
|
|
671
753
|
|
|
754
|
+
evidence.human_intent = preserveOrInitHumanIntent(evidence);
|
|
755
|
+
|
|
756
|
+
evidence.semantic_snapshot = generateSemanticSnapshot(evidence, violations, gateResult);
|
|
757
|
+
|
|
672
758
|
fs.writeFileSync(evidencePath, JSON.stringify(evidence, null, 2));
|
|
673
|
-
console.log('[Intelligent Audit] ✅ .AI_EVIDENCE.json updated with complete format (ai_gate, severity_metrics, token_usage, git_flow, watchers)');
|
|
759
|
+
console.log('[Intelligent Audit] ✅ .AI_EVIDENCE.json updated with complete format (ai_gate, severity_metrics, token_usage, git_flow, watchers, human_intent, semantic_snapshot)');
|
|
674
760
|
|
|
675
761
|
const MacNotificationSender = require('../../application/services/notification/MacNotificationSender');
|
|
676
762
|
const notificationSender = new MacNotificationSender(null);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
class StrategyRegistry {
|
|
4
|
+
constructor(fileSystemPort, configPath) {
|
|
5
|
+
this.strategies = new Map();
|
|
6
|
+
this.fileSystemPort = fileSystemPort;
|
|
7
|
+
this.configPath = configPath;
|
|
8
|
+
this.config = null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async loadStrategies() {
|
|
12
|
+
this.config = await this._loadConfig();
|
|
13
|
+
const strategiesDir = this.fileSystemPort.resolvePath(__dirname, '../..', 'domain', 'strategies');
|
|
14
|
+
|
|
15
|
+
const exists = await this.fileSystemPort.exists(strategiesDir);
|
|
16
|
+
if (!exists) {
|
|
17
|
+
throw new Error(`Strategies directory not found at ${strategiesDir}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const files = await this.fileSystemPort.readDir(strategiesDir);
|
|
21
|
+
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
if (file.endsWith('Strategy.js') && !file.includes('DIStrategy.js')) {
|
|
24
|
+
const strategyPath = path.join(strategiesDir, file);
|
|
25
|
+
const StrategyClass = require(strategyPath);
|
|
26
|
+
|
|
27
|
+
if (typeof StrategyClass === 'function') {
|
|
28
|
+
const strategy = new StrategyClass(this.config.dependencyInjection);
|
|
29
|
+
this.strategies.set(strategy.id, strategy);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getStrategy(id) {
|
|
36
|
+
return this.strategies.get(id);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getAllStrategies() {
|
|
40
|
+
return Array.from(this.strategies.values());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
findStrategiesForNode(node, context) {
|
|
44
|
+
return this.getAllStrategies().filter(strategy =>
|
|
45
|
+
strategy.canHandle(node, context)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async _loadConfig() {
|
|
50
|
+
try {
|
|
51
|
+
const configContent = await this.fileSystemPort.readFile(
|
|
52
|
+
this.configPath,
|
|
53
|
+
'utf8'
|
|
54
|
+
);
|
|
55
|
+
return JSON.parse(configContent);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(`Failed to load config from ${this.configPath}:`, error);
|
|
58
|
+
return { dependencyInjection: {} };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = StrategyRegistry;
|