speclock 4.5.7 → 5.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.
- package/README.md +145 -9
- package/package.json +2 -2
- package/src/cli/index.js +1 -1
- package/src/core/code-graph.js +635 -0
- package/src/core/compliance.js +1 -1
- package/src/core/conflict.js +1 -0
- package/src/core/engine.js +32 -2
- package/src/core/llm-checker.js +3 -156
- package/src/core/llm-provider.js +208 -0
- package/src/core/memory.js +115 -0
- package/src/core/spec-compiler.js +315 -0
- package/src/core/typed-constraints.js +408 -0
- package/src/dashboard/index.html +5 -4
- package/src/mcp/http-server.js +598 -7
- package/src/mcp/server.js +383 -1
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpecLock Typed Constraints Module
|
|
3
|
+
* Universal constraint types for autonomous systems governance.
|
|
4
|
+
* Handles numerical, range, state, and temporal constraints.
|
|
5
|
+
*
|
|
6
|
+
* Existing text-based locks continue using the semantic engine.
|
|
7
|
+
* This module adds typed constraint checking for real-world systems:
|
|
8
|
+
* - Robotics: motor speed limits, joint angles, zone restrictions
|
|
9
|
+
* - Vehicles: speed limits, temperature ranges, safety states
|
|
10
|
+
* - Trading: position limits, risk thresholds, order rates
|
|
11
|
+
* - Medical: dosage limits, vital sign ranges, protocol states
|
|
12
|
+
*
|
|
13
|
+
* Developed by Sandeep Roy (https://github.com/sgroy10)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Supported constraint types.
|
|
18
|
+
* "text" is the existing semantic type (handled by semantics.js, not here).
|
|
19
|
+
*/
|
|
20
|
+
export const CONSTRAINT_TYPES = ["numerical", "range", "state", "temporal"];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Supported operators for numerical and temporal constraints.
|
|
24
|
+
*/
|
|
25
|
+
export const OPERATORS = ["<", "<=", "==", "!=", ">=", ">"];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validate a typed lock definition before storing it.
|
|
29
|
+
* Returns { valid: true } or { valid: false, error: "reason" }.
|
|
30
|
+
*/
|
|
31
|
+
export function validateTypedLock(lock) {
|
|
32
|
+
if (!lock || typeof lock !== "object") {
|
|
33
|
+
return { valid: false, error: "Lock must be an object" };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { constraintType } = lock;
|
|
37
|
+
if (!CONSTRAINT_TYPES.includes(constraintType)) {
|
|
38
|
+
return { valid: false, error: `Invalid constraintType: ${constraintType}. Must be one of: ${CONSTRAINT_TYPES.join(", ")}` };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
switch (constraintType) {
|
|
42
|
+
case "numerical":
|
|
43
|
+
return validateNumerical(lock);
|
|
44
|
+
case "range":
|
|
45
|
+
return validateRange(lock);
|
|
46
|
+
case "state":
|
|
47
|
+
return validateState(lock);
|
|
48
|
+
case "temporal":
|
|
49
|
+
return validateTemporal(lock);
|
|
50
|
+
default:
|
|
51
|
+
return { valid: false, error: `Unknown constraintType: ${constraintType}` };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function validateNumerical(lock) {
|
|
56
|
+
if (!lock.metric || typeof lock.metric !== "string") {
|
|
57
|
+
return { valid: false, error: "numerical constraint requires 'metric' (string)" };
|
|
58
|
+
}
|
|
59
|
+
if (!OPERATORS.includes(lock.operator)) {
|
|
60
|
+
return { valid: false, error: `Invalid operator: ${lock.operator}. Must be one of: ${OPERATORS.join(", ")}` };
|
|
61
|
+
}
|
|
62
|
+
if (typeof lock.value !== "number" || isNaN(lock.value)) {
|
|
63
|
+
return { valid: false, error: "numerical constraint requires 'value' (number)" };
|
|
64
|
+
}
|
|
65
|
+
return { valid: true };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function validateRange(lock) {
|
|
69
|
+
if (!lock.metric || typeof lock.metric !== "string") {
|
|
70
|
+
return { valid: false, error: "range constraint requires 'metric' (string)" };
|
|
71
|
+
}
|
|
72
|
+
if (typeof lock.min !== "number" || isNaN(lock.min)) {
|
|
73
|
+
return { valid: false, error: "range constraint requires 'min' (number)" };
|
|
74
|
+
}
|
|
75
|
+
if (typeof lock.max !== "number" || isNaN(lock.max)) {
|
|
76
|
+
return { valid: false, error: "range constraint requires 'max' (number)" };
|
|
77
|
+
}
|
|
78
|
+
if (lock.min >= lock.max) {
|
|
79
|
+
return { valid: false, error: `'min' (${lock.min}) must be less than 'max' (${lock.max})` };
|
|
80
|
+
}
|
|
81
|
+
return { valid: true };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function validateState(lock) {
|
|
85
|
+
if (!lock.entity || typeof lock.entity !== "string") {
|
|
86
|
+
return { valid: false, error: "state constraint requires 'entity' (string)" };
|
|
87
|
+
}
|
|
88
|
+
if (!Array.isArray(lock.forbidden) || lock.forbidden.length === 0) {
|
|
89
|
+
return { valid: false, error: "state constraint requires 'forbidden' (non-empty array of { from, to } transitions)" };
|
|
90
|
+
}
|
|
91
|
+
for (let i = 0; i < lock.forbidden.length; i++) {
|
|
92
|
+
const t = lock.forbidden[i];
|
|
93
|
+
if (!t || typeof t.from !== "string" || typeof t.to !== "string") {
|
|
94
|
+
return { valid: false, error: `forbidden[${i}] must have 'from' and 'to' strings` };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { valid: true };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function validateTemporal(lock) {
|
|
101
|
+
if (!lock.metric || typeof lock.metric !== "string") {
|
|
102
|
+
return { valid: false, error: "temporal constraint requires 'metric' (string)" };
|
|
103
|
+
}
|
|
104
|
+
if (!OPERATORS.includes(lock.operator)) {
|
|
105
|
+
return { valid: false, error: `Invalid operator: ${lock.operator}. Must be one of: ${OPERATORS.join(", ")}` };
|
|
106
|
+
}
|
|
107
|
+
if (typeof lock.value !== "number" || isNaN(lock.value)) {
|
|
108
|
+
return { valid: false, error: "temporal constraint requires 'value' (number)" };
|
|
109
|
+
}
|
|
110
|
+
return { valid: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check a proposed value/action against a single typed lock.
|
|
115
|
+
* Returns { hasConflict, confidence, level, reasons[] }.
|
|
116
|
+
*
|
|
117
|
+
* @param {Object} lock - The typed lock from brain.json
|
|
118
|
+
* @param {Object} proposed - What's being proposed. Shape depends on constraintType:
|
|
119
|
+
* numerical: { value: number }
|
|
120
|
+
* range: { value: number }
|
|
121
|
+
* state: { from: string, to: string }
|
|
122
|
+
* temporal: { value: number }
|
|
123
|
+
*/
|
|
124
|
+
export function checkTypedConstraint(lock, proposed) {
|
|
125
|
+
if (!lock || !proposed) {
|
|
126
|
+
return { hasConflict: false, confidence: 0, level: "SAFE", reasons: ["Missing lock or proposed value"] };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
switch (lock.constraintType) {
|
|
130
|
+
case "numerical":
|
|
131
|
+
return checkNumerical(lock, proposed);
|
|
132
|
+
case "range":
|
|
133
|
+
return checkRange(lock, proposed);
|
|
134
|
+
case "state":
|
|
135
|
+
return checkState(lock, proposed);
|
|
136
|
+
case "temporal":
|
|
137
|
+
return checkTemporal(lock, proposed);
|
|
138
|
+
default:
|
|
139
|
+
return { hasConflict: false, confidence: 0, level: "SAFE", reasons: ["Unknown constraint type"] };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check a numerical constraint.
|
|
145
|
+
* Lock: { metric, operator, value, unit }
|
|
146
|
+
* Proposed: { value }
|
|
147
|
+
*/
|
|
148
|
+
function checkNumerical(lock, proposed) {
|
|
149
|
+
if (typeof proposed.value !== "number") {
|
|
150
|
+
return { hasConflict: false, confidence: 0, level: "SAFE", reasons: ["Proposed value is not a number"] };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const passes = evaluateOperator(proposed.value, lock.operator, lock.value);
|
|
154
|
+
if (passes) {
|
|
155
|
+
return {
|
|
156
|
+
hasConflict: false,
|
|
157
|
+
confidence: 0,
|
|
158
|
+
level: "SAFE",
|
|
159
|
+
reasons: [`${lock.metric}: ${proposed.value} ${lock.operator} ${lock.value}${lock.unit ? " " + lock.unit : ""} — within limit`],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Calculate how far the violation is (for confidence scoring)
|
|
164
|
+
const distance = Math.abs(proposed.value - lock.value);
|
|
165
|
+
const scale = Math.abs(lock.value) || 1;
|
|
166
|
+
const overagePercent = (distance / scale) * 100;
|
|
167
|
+
const confidence = Math.min(100, Math.round(70 + overagePercent * 0.3));
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
hasConflict: true,
|
|
171
|
+
confidence,
|
|
172
|
+
level: confidence >= 90 ? "HIGH" : "MEDIUM",
|
|
173
|
+
reasons: [
|
|
174
|
+
`${lock.metric}: proposed ${proposed.value} violates constraint ${lock.operator} ${lock.value}${lock.unit ? " " + lock.unit : ""}`,
|
|
175
|
+
`Overage: ${distance.toFixed(2)}${lock.unit ? " " + lock.unit : ""} beyond limit`,
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Check a range constraint.
|
|
182
|
+
* Lock: { metric, min, max, unit }
|
|
183
|
+
* Proposed: { value }
|
|
184
|
+
*/
|
|
185
|
+
function checkRange(lock, proposed) {
|
|
186
|
+
if (typeof proposed.value !== "number") {
|
|
187
|
+
return { hasConflict: false, confidence: 0, level: "SAFE", reasons: ["Proposed value is not a number"] };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (proposed.value >= lock.min && proposed.value <= lock.max) {
|
|
191
|
+
return {
|
|
192
|
+
hasConflict: false,
|
|
193
|
+
confidence: 0,
|
|
194
|
+
level: "SAFE",
|
|
195
|
+
reasons: [`${lock.metric}: ${proposed.value} is within range [${lock.min}, ${lock.max}]${lock.unit ? " " + lock.unit : ""}`],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const distance = proposed.value < lock.min
|
|
200
|
+
? lock.min - proposed.value
|
|
201
|
+
: proposed.value - lock.max;
|
|
202
|
+
const rangeSize = lock.max - lock.min;
|
|
203
|
+
const overagePercent = (distance / (rangeSize || 1)) * 100;
|
|
204
|
+
const confidence = Math.min(100, Math.round(70 + overagePercent * 0.3));
|
|
205
|
+
|
|
206
|
+
const direction = proposed.value < lock.min ? "below minimum" : "above maximum";
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
hasConflict: true,
|
|
210
|
+
confidence,
|
|
211
|
+
level: confidence >= 90 ? "HIGH" : "MEDIUM",
|
|
212
|
+
reasons: [
|
|
213
|
+
`${lock.metric}: proposed ${proposed.value} is ${direction} [${lock.min}, ${lock.max}]${lock.unit ? " " + lock.unit : ""}`,
|
|
214
|
+
`Out of range by ${distance.toFixed(2)}${lock.unit ? " " + lock.unit : ""}`,
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Check a state transition constraint.
|
|
221
|
+
* Lock: { entity, forbidden: [{ from, to }], requireApproval }
|
|
222
|
+
* Proposed: { from, to }
|
|
223
|
+
*/
|
|
224
|
+
function checkState(lock, proposed) {
|
|
225
|
+
if (!proposed.from || !proposed.to) {
|
|
226
|
+
return { hasConflict: false, confidence: 0, level: "SAFE", reasons: ["Missing from/to state"] };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const fromNorm = proposed.from.toUpperCase().trim();
|
|
230
|
+
const toNorm = proposed.to.toUpperCase().trim();
|
|
231
|
+
|
|
232
|
+
for (const forbidden of lock.forbidden) {
|
|
233
|
+
const forbidFrom = forbidden.from.toUpperCase().trim();
|
|
234
|
+
const forbidTo = forbidden.to.toUpperCase().trim();
|
|
235
|
+
|
|
236
|
+
// Wildcard support: "*" matches any state
|
|
237
|
+
const fromMatch = forbidFrom === "*" || forbidFrom === fromNorm;
|
|
238
|
+
const toMatch = forbidTo === "*" || forbidTo === toNorm;
|
|
239
|
+
|
|
240
|
+
if (fromMatch && toMatch) {
|
|
241
|
+
return {
|
|
242
|
+
hasConflict: true,
|
|
243
|
+
confidence: 100,
|
|
244
|
+
level: "HIGH",
|
|
245
|
+
reasons: [
|
|
246
|
+
`${lock.entity}: transition ${proposed.from} -> ${proposed.to} is forbidden`,
|
|
247
|
+
lock.requireApproval
|
|
248
|
+
? "This transition requires explicit human approval"
|
|
249
|
+
: "This state transition is not allowed",
|
|
250
|
+
],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
hasConflict: false,
|
|
257
|
+
confidence: 0,
|
|
258
|
+
level: "SAFE",
|
|
259
|
+
reasons: [`${lock.entity}: transition ${proposed.from} -> ${proposed.to} is allowed`],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Check a temporal constraint.
|
|
265
|
+
* Lock: { metric, operator, value, unit }
|
|
266
|
+
* Proposed: { value }
|
|
267
|
+
*
|
|
268
|
+
* Same logic as numerical, but semantically for time intervals/frequencies.
|
|
269
|
+
*/
|
|
270
|
+
function checkTemporal(lock, proposed) {
|
|
271
|
+
if (typeof proposed.value !== "number") {
|
|
272
|
+
return { hasConflict: false, confidence: 0, level: "SAFE", reasons: ["Proposed value is not a number"] };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const passes = evaluateOperator(proposed.value, lock.operator, lock.value);
|
|
276
|
+
if (passes) {
|
|
277
|
+
return {
|
|
278
|
+
hasConflict: false,
|
|
279
|
+
confidence: 0,
|
|
280
|
+
level: "SAFE",
|
|
281
|
+
reasons: [`${lock.metric}: ${proposed.value}${lock.unit ? lock.unit : ""} ${lock.operator} ${lock.value}${lock.unit ? lock.unit : ""} — within limit`],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const distance = Math.abs(proposed.value - lock.value);
|
|
286
|
+
const scale = Math.abs(lock.value) || 1;
|
|
287
|
+
const overagePercent = (distance / scale) * 100;
|
|
288
|
+
const confidence = Math.min(100, Math.round(80 + overagePercent * 0.2));
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
hasConflict: true,
|
|
292
|
+
confidence,
|
|
293
|
+
level: confidence >= 90 ? "HIGH" : "MEDIUM",
|
|
294
|
+
reasons: [
|
|
295
|
+
`${lock.metric}: proposed ${proposed.value}${lock.unit ? lock.unit : ""} violates constraint ${lock.operator} ${lock.value}${lock.unit ? lock.unit : ""}`,
|
|
296
|
+
`Timing violation: off by ${distance.toFixed(2)}${lock.unit ? lock.unit : ""}`,
|
|
297
|
+
],
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Evaluate a comparison operator.
|
|
303
|
+
*/
|
|
304
|
+
function evaluateOperator(proposed, operator, threshold) {
|
|
305
|
+
switch (operator) {
|
|
306
|
+
case "<": return proposed < threshold;
|
|
307
|
+
case "<=": return proposed <= threshold;
|
|
308
|
+
case "==": return proposed === threshold;
|
|
309
|
+
case "!=": return proposed !== threshold;
|
|
310
|
+
case ">=": return proposed >= threshold;
|
|
311
|
+
case ">": return proposed > threshold;
|
|
312
|
+
default: return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Check ALL typed locks in brain against a proposed value/action.
|
|
318
|
+
* Filters locks by metric/entity to only check relevant ones.
|
|
319
|
+
*
|
|
320
|
+
* @param {Array} locks - All locks from brain.specLock.items
|
|
321
|
+
* @param {Object} proposed - { metric?, entity?, value?, from?, to? }
|
|
322
|
+
* @returns {Object} { hasConflict, conflictingLocks[], analysis }
|
|
323
|
+
*/
|
|
324
|
+
export function checkAllTypedConstraints(locks, proposed) {
|
|
325
|
+
if (!locks || !Array.isArray(locks)) {
|
|
326
|
+
return { hasConflict: false, conflictingLocks: [], analysis: "No locks to check." };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const typedLocks = locks.filter(l =>
|
|
330
|
+
l.active !== false && CONSTRAINT_TYPES.includes(l.constraintType)
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
if (typedLocks.length === 0) {
|
|
334
|
+
return { hasConflict: false, conflictingLocks: [], analysis: "No typed constraints to check against." };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Filter relevant locks: match by metric or entity
|
|
338
|
+
const relevant = typedLocks.filter(l => {
|
|
339
|
+
if (proposed.metric && (l.metric === proposed.metric)) return true;
|
|
340
|
+
if (proposed.entity && (l.entity === proposed.entity)) return true;
|
|
341
|
+
// If no metric/entity filter, check all typed locks
|
|
342
|
+
if (!proposed.metric && !proposed.entity) return true;
|
|
343
|
+
return false;
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
if (relevant.length === 0) {
|
|
347
|
+
return {
|
|
348
|
+
hasConflict: false,
|
|
349
|
+
conflictingLocks: [],
|
|
350
|
+
analysis: `No typed constraints found for ${proposed.metric || proposed.entity || "unknown"}. ${typedLocks.length} typed lock(s) exist for other metrics/entities.`,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const conflicting = [];
|
|
355
|
+
for (const lock of relevant) {
|
|
356
|
+
const result = checkTypedConstraint(lock, proposed);
|
|
357
|
+
if (result.hasConflict) {
|
|
358
|
+
conflicting.push({
|
|
359
|
+
id: lock.id,
|
|
360
|
+
text: lock.text || formatTypedLockText(lock),
|
|
361
|
+
constraintType: lock.constraintType,
|
|
362
|
+
metric: lock.metric,
|
|
363
|
+
entity: lock.entity,
|
|
364
|
+
confidence: result.confidence,
|
|
365
|
+
level: result.level,
|
|
366
|
+
reasons: result.reasons,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (conflicting.length === 0) {
|
|
372
|
+
return {
|
|
373
|
+
hasConflict: false,
|
|
374
|
+
conflictingLocks: [],
|
|
375
|
+
analysis: `Checked ${relevant.length} typed constraint(s). All within limits.`,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
conflicting.sort((a, b) => b.confidence - a.confidence);
|
|
380
|
+
const details = conflicting
|
|
381
|
+
.map(c => `- [${c.level}] ${c.constraintType}/${c.metric || c.entity}: ${c.reasons[0]} (${c.confidence}%)`)
|
|
382
|
+
.join("\n");
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
hasConflict: true,
|
|
386
|
+
conflictingLocks: conflicting,
|
|
387
|
+
analysis: `VIOLATION: ${conflicting.length} typed constraint(s) violated:\n${details}`,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Generate human-readable text for a typed lock (used as fallback for lock.text).
|
|
393
|
+
*/
|
|
394
|
+
export function formatTypedLockText(lock) {
|
|
395
|
+
switch (lock.constraintType) {
|
|
396
|
+
case "numerical":
|
|
397
|
+
return `${lock.metric} must be ${lock.operator} ${lock.value}${lock.unit ? " " + lock.unit : ""}`;
|
|
398
|
+
case "range":
|
|
399
|
+
return `${lock.metric} must stay between ${lock.min} and ${lock.max}${lock.unit ? " " + lock.unit : ""}`;
|
|
400
|
+
case "state":
|
|
401
|
+
const transitions = lock.forbidden.map(f => `${f.from} -> ${f.to}`).join(", ");
|
|
402
|
+
return `${lock.entity}: forbidden transitions: ${transitions}${lock.requireApproval ? " (requires human approval)" : ""}`;
|
|
403
|
+
case "temporal":
|
|
404
|
+
return `${lock.metric} must be ${lock.operator} ${lock.value}${lock.unit ? " " + lock.unit : ""}`;
|
|
405
|
+
default:
|
|
406
|
+
return "Unknown typed constraint";
|
|
407
|
+
}
|
|
408
|
+
}
|
package/src/dashboard/index.html
CHANGED
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
<div class="header">
|
|
90
90
|
<div>
|
|
91
91
|
<h1><span>SpecLock</span> Dashboard</h1>
|
|
92
|
-
<div class="meta">
|
|
92
|
+
<div class="meta">v5.0.0 — AI Constraint Engine</div>
|
|
93
93
|
</div>
|
|
94
94
|
<div style="display:flex;align-items:center;gap:12px;">
|
|
95
95
|
<span id="health-badge" class="status-badge healthy">Loading...</span>
|
|
@@ -154,7 +154,7 @@
|
|
|
154
154
|
<div class="grid">
|
|
155
155
|
<div class="card table-card">
|
|
156
156
|
<table>
|
|
157
|
-
<thead><tr><th>ID</th><th>Constraint</th><th>Source</th><th>Tags</th><th>Created</th></tr></thead>
|
|
157
|
+
<thead><tr><th>ID</th><th>Type</th><th>Constraint</th><th>Source</th><th>Tags</th><th>Created</th></tr></thead>
|
|
158
158
|
<tbody id="locks-table"><tr><td colspan="5" class="loading">Loading...</td></tr></tbody>
|
|
159
159
|
</table>
|
|
160
160
|
</div>
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
</div>
|
|
183
183
|
|
|
184
184
|
<div style="text-align:center;padding:24px;color:var(--muted);font-size:12px;">
|
|
185
|
-
SpecLock
|
|
185
|
+
SpecLock v5.0.0 — Developed by Sandeep Roy — <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
|
|
186
186
|
</div>
|
|
187
187
|
|
|
188
188
|
<script>
|
|
@@ -266,11 +266,12 @@ async function loadContext() {
|
|
|
266
266
|
// Locks table
|
|
267
267
|
const locksBody = document.getElementById('locks-table');
|
|
268
268
|
if (activeLocks.length === 0) {
|
|
269
|
-
locksBody.innerHTML = '<tr><td colspan="
|
|
269
|
+
locksBody.innerHTML = '<tr><td colspan="6" style="color:var(--muted)">No active locks</td></tr>';
|
|
270
270
|
} else {
|
|
271
271
|
locksBody.innerHTML = activeLocks.map(l => `
|
|
272
272
|
<tr>
|
|
273
273
|
<td><code>${l.id}</code></td>
|
|
274
|
+
<td><span class="badge ${l.constraintType ? 'medium' : 'low'}">${l.constraintType || 'text'}</span></td>
|
|
274
275
|
<td>${escHtml(l.text)}</td>
|
|
275
276
|
<td><span class="badge active">${l.source || 'agent'}</span></td>
|
|
276
277
|
<td>${(l.tags || []).map(t => `<span class="badge low">${t}</span>`).join(' ') || '-'}</td>
|