futura-scion 0.2.2 → 0.2.3
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/bin/scion.js +50 -0
- package/config/scion.config.yaml +5 -1
- package/package.json +1 -1
- package/src/http.js +72 -1
- package/src/mind/brief.js +145 -6
- package/src/mind/nlu.js +18 -0
package/bin/scion.js
CHANGED
|
@@ -84,6 +84,33 @@ const oracle = realVerifiers ? { verifiers: realVerifiers } : { verifyFn };
|
|
|
84
84
|
*/
|
|
85
85
|
const CLARIFY_MAX_DEPTH = 3;
|
|
86
86
|
|
|
87
|
+
/** Human chat summary of a brief model (+ optional verification). */
|
|
88
|
+
function briefSummary(model, verification, intake) {
|
|
89
|
+
const reqs = model.requirements || [];
|
|
90
|
+
const lines = [];
|
|
91
|
+
if (intake) {
|
|
92
|
+
lines.push(intake.added > 0
|
|
93
|
+
? `Noted — ${intake.added} new requirement${intake.added > 1 ? 's' : ''} added to brief #${model.id} (${intake.total} total).`
|
|
94
|
+
: `Already captured — brief #${model.id} has ${intake.total} requirement${intake.total === 1 ? '' : 's'}.`);
|
|
95
|
+
} else {
|
|
96
|
+
lines.push(`Brief #${model.id}: ${reqs.length} requirement${reqs.length === 1 ? '' : 's'}.`);
|
|
97
|
+
}
|
|
98
|
+
for (const r of reqs.slice(0, 8)) {
|
|
99
|
+
const mark = r.status === 'pass' ? '✓' : r.status === 'fail' ? '✗' : r.checkable ? '◇' : '·';
|
|
100
|
+
lines.push(` ${mark} ${r.kind}: ${String(r.text).slice(0, 90)}${r.checkable ? '' : ' (judgment — needs a human or a model)'}`);
|
|
101
|
+
}
|
|
102
|
+
if (reqs.length > 8) lines.push(` …and ${reqs.length - 8} more`);
|
|
103
|
+
if (verification) {
|
|
104
|
+
lines.push(`Gate verification: ${verification.passed} passed, ${verification.failed} failed` +
|
|
105
|
+
(verification.coverage != null ? `, coverage ${(verification.coverage * 100).toFixed(0)}%` : '') +
|
|
106
|
+
(verification.failed > 0 ? ' — failing requirements block completion.' :
|
|
107
|
+
verification.passed > 0 ? ' — the contract holds.' : ''));
|
|
108
|
+
} else {
|
|
109
|
+
lines.push('Say "verify the brief" to run the checks against the tree.');
|
|
110
|
+
}
|
|
111
|
+
return lines.join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
87
114
|
async function oneShot(text, opts = {}, depth = 0) {
|
|
88
115
|
const { interpret, slotsToPayload, clarify, resolveClarification, resetLearnedTermsCache } = await import('../src/mind/nlu.js');
|
|
89
116
|
const { getProjectScopes, resolveScopes } = await import('../src/mind/scopes.js');
|
|
@@ -142,6 +169,29 @@ async function oneShot(text, opts = {}, depth = 0) {
|
|
|
142
169
|
const saved = save({ type: 'observation', title: content.slice(0, 80), content, tags: ['remembered'], confidence: 90, provenance: 'explicit' });
|
|
143
170
|
return { ok: true, intent: 'remember', result: saved };
|
|
144
171
|
}
|
|
172
|
+
// Brief intent — context arrives IN THE CHAT, not as a file. The message
|
|
173
|
+
// itself is ingested into the active brief (accumulated across turns);
|
|
174
|
+
// "verify"/"check" phrasing settles the active brief instead.
|
|
175
|
+
if (interp.intent === 'brief') {
|
|
176
|
+
const briefs = await import('../src/mind/brief.js');
|
|
177
|
+
if (arg === 'verify' || restArgs.includes('--verify') || /\b(verify|check|settle)\b/i.test(text)) {
|
|
178
|
+
const id = briefs.activeBriefId();
|
|
179
|
+
if (!id) return { ok: false, intent: 'brief', reply: 'no active brief — state a requirement first (e.g. "the report must contain totals").' };
|
|
180
|
+
const c = briefs.compileBrief(id);
|
|
181
|
+
const v = await briefs.verifyBrief(id);
|
|
182
|
+
return {
|
|
183
|
+
ok: true, intent: 'brief',
|
|
184
|
+
reply: briefSummary(briefs.getBrief(id), v),
|
|
185
|
+
result: { briefId: id, compiled: c.compiled, verification: v },
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const intake = await briefs.chatIntake(text);
|
|
189
|
+
return {
|
|
190
|
+
ok: true, intent: 'brief',
|
|
191
|
+
reply: briefSummary(intake.model, null, intake),
|
|
192
|
+
result: { briefId: intake.briefId, added: intake.added, total: intake.total },
|
|
193
|
+
};
|
|
194
|
+
}
|
|
145
195
|
|
|
146
196
|
// Ambiguous: ask ONE deterministic question with the top-N ranked
|
|
147
197
|
// interpretations instead of a cold escalation or a silent guess. Every
|
package/config/scion.config.yaml
CHANGED
|
@@ -11,8 +11,12 @@ ladder:
|
|
|
11
11
|
min_insight_confidence: 0.55 # reasoner rung admission floor
|
|
12
12
|
|
|
13
13
|
llm:
|
|
14
|
-
daily_tokens:
|
|
14
|
+
daily_tokens: 500
|
|
15
15
|
|
|
16
|
+
apiKey: null
|
|
17
|
+
model: "qwen2.5-coder:7b"
|
|
18
|
+
baseUrl: "http://localhost:11434/v1"
|
|
19
|
+
provider: "ollama"
|
|
16
20
|
bounds:
|
|
17
21
|
max_turns: 25
|
|
18
22
|
timeout_ms: 300000
|
package/package.json
CHANGED
package/src/http.js
CHANGED
|
@@ -275,7 +275,32 @@ export function createApiServer(opts = {}) {
|
|
|
275
275
|
|
|
276
276
|
let reply; let meta;
|
|
277
277
|
try {
|
|
278
|
-
|
|
278
|
+
// Brief intent: requirements arrive IN THE CHAT — ingest with
|
|
279
|
+
// streamed stage events (parsed → compiled), or verify on request.
|
|
280
|
+
if (interp.intent === 'brief') {
|
|
281
|
+
const briefs = await import('./mind/brief.js');
|
|
282
|
+
if (/\b(verify|check|settle)\b/i.test(body.text)) {
|
|
283
|
+
const id = briefs.activeBriefId();
|
|
284
|
+
if (!id) {
|
|
285
|
+
reply = 'No active brief yet — state a requirement first (e.g. "the report must contain a totals row").';
|
|
286
|
+
meta = { rung: 'brief' };
|
|
287
|
+
} else {
|
|
288
|
+
emit('progress', { stage: 'brief-compile', detail: `compiling brief ${id}` });
|
|
289
|
+
const c = briefs.compileBrief(id);
|
|
290
|
+
emit('progress', { stage: 'brief-verify', detail: `${c.compiled} checks through the Gate` });
|
|
291
|
+
const v = await briefs.verifyBrief(id);
|
|
292
|
+
briefs.setActiveBrief(null); // settled — matches chatIntake settle semantics
|
|
293
|
+
reply = briefChatSummary(briefs.getBrief(id), v);
|
|
294
|
+
meta = { rung: 'brief', detail: `brief ${id}: ${v.passed}✓ ${v.failed}✗` };
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
emit('progress', { stage: 'brief-parse', detail: 'parsing requirements from the message' });
|
|
298
|
+
const intake = await briefs.chatIntake(body.text);
|
|
299
|
+
emit('progress', { stage: 'brief-compiled', detail: `${intake.model.requirements.filter(r => r.checkable).length} machine-checkable` });
|
|
300
|
+
reply = briefChatSummary(intake.model, null, intake);
|
|
301
|
+
meta = { rung: 'brief', detail: `brief ${intake.briefId}: +${intake.added} (${intake.total} total)` };
|
|
302
|
+
}
|
|
303
|
+
} else if (interp.intent === 'show' || interp.intent === 'search' || interp.intent === 'remember' || mode === 'chat') {
|
|
279
304
|
reply = await chatAnswer(interp, body.text);
|
|
280
305
|
meta = { rung: 'nlu', detail: `intent: ${interp.intent} (${Math.round(interp.confidence * 100)}%)` };
|
|
281
306
|
} else {
|
|
@@ -325,6 +350,25 @@ export function createApiServer(opts = {}) {
|
|
|
325
350
|
const { interpret, slotsToPayload } = await nlu();
|
|
326
351
|
const interp = interpret(body.text);
|
|
327
352
|
const mode = body.mode === 'agent' || body.mode === 'chat' ? body.mode : 'chat';
|
|
353
|
+
// Brief intent: requirements arrive IN THE CHAT — ingest into the
|
|
354
|
+
// active brief (accumulated across turns), or verify on request.
|
|
355
|
+
if (interp.intent === 'brief') {
|
|
356
|
+
const briefs = await import('./mind/brief.js');
|
|
357
|
+
if (/\b(verify|check|settle)\b/i.test(body.text)) {
|
|
358
|
+
const id = briefs.activeBriefId();
|
|
359
|
+
if (!id) {
|
|
360
|
+
return send(res, 200, { reply: 'No active brief yet — state a requirement first (e.g. "the report must contain a totals row").', meta: { rung: 'brief' } });
|
|
361
|
+
}
|
|
362
|
+
const c = briefs.compileBrief(id);
|
|
363
|
+
const v = await briefs.verifyBrief(id);
|
|
364
|
+
return send(res, 200, { reply: briefChatSummary(briefs.getBrief(id), v), meta: { rung: 'brief', detail: `brief ${id}: ${v.passed}✓ ${v.failed}✗` } });
|
|
365
|
+
}
|
|
366
|
+
const intake = await briefs.chatIntake(body.text);
|
|
367
|
+
return send(res, 200, {
|
|
368
|
+
reply: briefChatSummary(intake.model, null, intake),
|
|
369
|
+
meta: { rung: 'brief', detail: `brief ${intake.briefId}: +${intake.added} (${intake.total} total)` },
|
|
370
|
+
});
|
|
371
|
+
}
|
|
328
372
|
if (interp.intent === 'show' || interp.intent === 'search' || interp.intent === 'remember' || mode === 'chat') {
|
|
329
373
|
const reply = await chatAnswer(interp, body.text);
|
|
330
374
|
return send(res, 200, { reply, meta: { rung: 'nlu', detail: `intent: ${interp.intent} (${Math.round(interp.confidence * 100)}%)` } });
|
|
@@ -489,6 +533,33 @@ async function nlu() {
|
|
|
489
533
|
}
|
|
490
534
|
|
|
491
535
|
/** Chat mode: answer from kernel state — never invents, never mutates. */
|
|
536
|
+
/** Human chat summary of a brief model (shared by /ui/act and stream). */
|
|
537
|
+
function briefChatSummary(model, verification, intake) {
|
|
538
|
+
const reqs = model.requirements || [];
|
|
539
|
+
const lines = [];
|
|
540
|
+
if (intake) {
|
|
541
|
+
lines.push(intake.added > 0
|
|
542
|
+
? `Noted — ${intake.added} new requirement${intake.added > 1 ? 's' : ''} added to brief #${model.id} (${intake.total} total).`
|
|
543
|
+
: `Already captured — brief #${model.id} has ${intake.total} requirement${intake.total === 1 ? '' : 's'}.`);
|
|
544
|
+
} else {
|
|
545
|
+
lines.push(`Brief #${model.id}: ${reqs.length} requirement${reqs.length === 1 ? '' : 's'}.`);
|
|
546
|
+
}
|
|
547
|
+
for (const r of reqs.slice(0, 8)) {
|
|
548
|
+
const mark = r.status === 'pass' ? '✓' : r.status === 'fail' ? '✗' : r.checkable ? '◇' : '·';
|
|
549
|
+
lines.push(`${mark} ${r.kind}: ${String(r.text).slice(0, 90)}${r.checkable ? '' : ' (judgment — needs a human or a model)'}`);
|
|
550
|
+
}
|
|
551
|
+
if (reqs.length > 8) lines.push(`…and ${reqs.length - 8} more`);
|
|
552
|
+
if (verification) {
|
|
553
|
+
lines.push(`Gate verification: ${verification.passed} passed, ${verification.failed} failed` +
|
|
554
|
+
(verification.coverage != null ? `, coverage ${(verification.coverage * 100).toFixed(0)}%` : '') +
|
|
555
|
+
(verification.failed > 0 ? ' — failing requirements block completion.' :
|
|
556
|
+
verification.passed > 0 ? ' — the contract holds.' : ''));
|
|
557
|
+
} else {
|
|
558
|
+
lines.push('Say "verify the brief" to run the checks against the tree.');
|
|
559
|
+
}
|
|
560
|
+
return lines.join('\n');
|
|
561
|
+
}
|
|
562
|
+
|
|
492
563
|
async function chatAnswer(interp, text) {
|
|
493
564
|
if (interp.intent === 'show') {
|
|
494
565
|
const what = interp.target || '';
|
package/src/mind/brief.js
CHANGED
|
@@ -119,12 +119,26 @@ export function ingest(opts) {
|
|
|
119
119
|
continue;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
// Rule sentences: MUST/SHALL/SHOULD/cannot/never/always…
|
|
122
|
+
// Rule sentences: MUST/SHALL/SHOULD/cannot/never/always… Compound
|
|
123
|
+
// chat lines ("X must exist and the customer wants a nicer UI") are
|
|
124
|
+
// split on conjunctions so each requirement stands alone — a checkable
|
|
125
|
+
// rule is never muddled with an adjacent wish.
|
|
123
126
|
if (RULE_MODAL_RE.test(line)) {
|
|
124
127
|
const modality = /must\s+not|shall\s+not|cannot|can't|can’t|never|forbidden|not\s+be\s+allowed/i.test(line) ? 'must-not'
|
|
125
128
|
: /must|shall|requires/i.test(line) ? 'must'
|
|
126
129
|
: /should/i.test(line) ? 'should' : 'always';
|
|
127
|
-
|
|
130
|
+
const conjuncts = line
|
|
131
|
+
// sentence boundaries are the primary split inside a chat line
|
|
132
|
+
.split(/(?<=[.!?])\s+/)
|
|
133
|
+
.flatMap(s => s.split(/\s+(?:and|also|plus)\s+(?=(?:the\s+|a\s+|an\s+|all\s+|every\s+)?\w+\s+(?:must|shall|should|cannot|never|wants?|needs?|is\s+not\s+allowed))/i))
|
|
134
|
+
.map(s => s.trim().replace(/[.]$/, '')).filter(Boolean);
|
|
135
|
+
for (const part of conjuncts) {
|
|
136
|
+
const m = /must\s+not|shall\s+not|cannot|can't|can’t|never|forbidden/i.test(part) ? 'must-not'
|
|
137
|
+
: /must|shall|requires/i.test(part) ? 'must'
|
|
138
|
+
: /should/i.test(part) ? 'should'
|
|
139
|
+
: /^(the\s+)?(customer|user|client|team)/i.test(part) ? 'wish' : modality;
|
|
140
|
+
requirements.push({ kind: m === 'wish' ? 'judgment' : 'rule', text: part, subject: part, modality: m });
|
|
141
|
+
}
|
|
128
142
|
continue;
|
|
129
143
|
}
|
|
130
144
|
}
|
|
@@ -171,6 +185,21 @@ export function ingest(opts) {
|
|
|
171
185
|
export function classify(req) {
|
|
172
186
|
const t = String(req?.text || '');
|
|
173
187
|
|
|
188
|
+
// Compound sentences split FIRST: chat lines carry several requirements
|
|
189
|
+
// joined by "and"/"also" — each conjunct is classified on its own, so
|
|
190
|
+
// "X must exist and the customer wants a nicer UI" yields one checkable
|
|
191
|
+
// rule and one judgment, not one muddled compound.
|
|
192
|
+
const conjuncts = t.split(/\s+(?:and|also|plus|;)\s+/i).map(s => s.trim()).filter(s => s.length >= 8);
|
|
193
|
+
if (conjuncts.length > 1) {
|
|
194
|
+
const parts = conjuncts.map(conj => classify({ ...req, text: conj }));
|
|
195
|
+
if (parts.some(p => p.checkable)) {
|
|
196
|
+
// Prefer the checkable part as THIS requirement's spec; the other
|
|
197
|
+
// conjuncts are re-emitted by the caller via splitConjuncts().
|
|
198
|
+
const hit = parts.find(p => p.checkable);
|
|
199
|
+
return { ...hit, conjuncts };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
174
203
|
// Explicit directive wins: "check: <shell command>" or a backticked
|
|
175
204
|
// command anchored to an exit-code phrase — the author says exactly how
|
|
176
205
|
// to verify; the Gate runs it verbatim.
|
|
@@ -184,12 +213,14 @@ export function classify(req) {
|
|
|
184
213
|
|| t.match(/\b(?:must|shall|should)\s+(?:exist|be\s+present)[^.]*\b([\w./\\-]+\.[a-z0-9]{1,6})\b/i);
|
|
185
214
|
if (exists) return { checkable: true, spec: { type: 'file-exists', target: exists[1] } };
|
|
186
215
|
|
|
187
|
-
// file-lacks / file-contains: "X must not contain/contain Y".
|
|
188
|
-
|
|
189
|
-
|
|
216
|
+
// file-lacks / file-contains: "X must not contain/contain Y". The needle
|
|
217
|
+
// may be bare (no quotes) in chat speech: "must not contain the word ZZZZ".
|
|
218
|
+
const contains = t.match(/\b([\w./\\-]+\.[a-z0-9]{1,6})\b[^.]*?\b(?:must|shall|should)\s+not\s+(?:contain|include|reference|log)\s+(?:the\s+)?(?:word\s+|string\s+|secret[s]?\s+)?(?:["'`]([^"'`\n]+)["'`]|([A-Za-z0-9_]{3,})\b)/i)
|
|
219
|
+
|| t.match(/\b([\w./\\-]+\.[a-z0-9]{1,6})\b[^.]*?\b(?:must|shall|should)\s+(?:contain|include|reference)\s+(?:the\s+)?(?:word\s+|string\s+)?(?:["'`]([^"'`\n]+)["'`]|([A-Za-z0-9_]{3,})\b)/i);
|
|
190
220
|
if (contains) {
|
|
191
221
|
const negative = /must\s+not|shall\s+not|never/i.test(contains.input);
|
|
192
|
-
|
|
222
|
+
const needle = contains[2] ?? contains[3];
|
|
223
|
+
return { checkable: true, spec: { type: negative ? 'file-lacks' : 'file-contains', target: contains[1], needle } };
|
|
193
224
|
}
|
|
194
225
|
|
|
195
226
|
// regex: "must match /re/"
|
|
@@ -377,9 +408,117 @@ export function listBriefs(limit = 20) {
|
|
|
377
408
|
).all(limit);
|
|
378
409
|
}
|
|
379
410
|
|
|
411
|
+
// ─── Chat accumulation: the ACTIVE brief ────────────────────────────
|
|
412
|
+
|
|
413
|
+
const ACTIVE_KEY = 'active_brief';
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* The active brief: requirements arriving in CHAT accumulate onto one
|
|
417
|
+
* brief (the conversation's working contract) until it is verified or
|
|
418
|
+
* explicitly replaced. Persisted in the brain meta table — restart-safe.
|
|
419
|
+
*
|
|
420
|
+
* @returns {number|null} brief id
|
|
421
|
+
*/
|
|
422
|
+
export function activeBriefId() {
|
|
423
|
+
try {
|
|
424
|
+
const row = getDb().prepare('SELECT value FROM meta WHERE key = ?').get(ACTIVE_KEY);
|
|
425
|
+
if (!row) return null;
|
|
426
|
+
const id = Number(JSON.parse(row.value));
|
|
427
|
+
return getDb().prepare('SELECT id FROM briefs WHERE id = ?').get(id) ? id : null;
|
|
428
|
+
} catch { return null; }
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Point the accumulator at a brief (or clear with null). */
|
|
432
|
+
export function setActiveBrief(briefId) {
|
|
433
|
+
const db = getDb();
|
|
434
|
+
if (briefId == null) {
|
|
435
|
+
db.prepare('DELETE FROM meta WHERE key = ?').run(ACTIVE_KEY);
|
|
436
|
+
return { active: null };
|
|
437
|
+
}
|
|
438
|
+
if (!db.prepare('SELECT id FROM briefs WHERE id = ?').get(briefId)) {
|
|
439
|
+
throw new Error(`brief.setActiveBrief: no brief ${briefId}`);
|
|
440
|
+
}
|
|
441
|
+
db.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value')
|
|
442
|
+
.run(ACTIVE_KEY, JSON.stringify(Number(briefId)));
|
|
443
|
+
return { active: Number(briefId) };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Chat intake: append a message to the active brief (creating one on
|
|
448
|
+
* first requirement), re-parse, and report the accumulated model. Short
|
|
449
|
+
* messages add requirements; the message itself is the context, not a
|
|
450
|
+
* file path.
|
|
451
|
+
*
|
|
452
|
+
* @param {string} message — the raw chat text
|
|
453
|
+
* @param {Object} [opts] — { title? }
|
|
454
|
+
* @returns {{ briefId, added: number, total: number, newRequirements: Array, model: Object }}
|
|
455
|
+
*/
|
|
456
|
+
const SETTLE_RE = /^(verify|check|settle|run)( the)? brief\b/i;
|
|
457
|
+
export async function chatIntake(message, opts = {}) {
|
|
458
|
+
const db = getDb();
|
|
459
|
+
let briefId = activeBriefId();
|
|
460
|
+
|
|
461
|
+
if (!briefId) {
|
|
462
|
+
const b = ingest({ text: message, title: opts.title || message.slice(0, 80), source: 'chat' });
|
|
463
|
+
briefId = b.brief.id;
|
|
464
|
+
setActiveBrief(briefId);
|
|
465
|
+
const model = getBrief(briefId);
|
|
466
|
+
trailJournal('brief.chat-intake', { brief_id: briefId, added: model.requirements.length, created: true });
|
|
467
|
+
return { briefId, added: model.requirements.length, total: model.requirements.length, newRequirements: model.requirements, model };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Append to the active brief WITHOUT re-ingesting the whole document —
|
|
471
|
+
// re-parse drifts (the goal paragraph mutates as text grows, old rules
|
|
472
|
+
// resurface as "new"). Only the NEW message is parsed; dedup by exact
|
|
473
|
+
// text keeps repeats honest.
|
|
474
|
+
const existing = new Set(
|
|
475
|
+
db.prepare('SELECT text FROM requirements WHERE brief_id = ?').all(briefId).map(r => r.text)
|
|
476
|
+
);
|
|
477
|
+
const parsed = ingest({ text: message, title: opts.title, source: 'chat', persist: false });
|
|
478
|
+
// Goals are only minted at brief creation (the goal paragraph is the
|
|
479
|
+
// whole accumulated text — re-adding it per turn duplicates noise).
|
|
480
|
+
// Settle phrases route to verification, never into the contract.
|
|
481
|
+
if (SETTLE_RE.test(message.trim())) {
|
|
482
|
+
const model = getBrief(briefId);
|
|
483
|
+
trailJournal('brief.chat-settle', { brief_id: briefId });
|
|
484
|
+
const verification = await verifyBrief(briefId);
|
|
485
|
+
setActiveBrief(null); // settled — the next context starts a fresh brief
|
|
486
|
+
return { briefId, verified: true, added: 0, total: model.requirements.length, newRequirements: [], verification, model };
|
|
487
|
+
}
|
|
488
|
+
const fresh = parsed.requirements.filter(r => r.kind !== 'goal' && !existing.has(r.text));
|
|
489
|
+
const ins = db.prepare('INSERT INTO requirements (brief_id, kind, text, subject) VALUES (?, ?, ?, ?)');
|
|
490
|
+
for (const r of fresh) ins.run(briefId, r.kind, r.text, r.subject ?? null);
|
|
491
|
+
const prev = db.prepare('SELECT text FROM briefs WHERE id = ?').get(briefId);
|
|
492
|
+
db.prepare('UPDATE briefs SET text = ? WHERE id = ?').run(`${prev.text}\n${message}`, briefId);
|
|
493
|
+
|
|
494
|
+
const model = getBrief(briefId);
|
|
495
|
+
// Auto-compile any newly checkable requirements.
|
|
496
|
+
let compiledNew = 0;
|
|
497
|
+
for (const row of model.requirements) {
|
|
498
|
+
if (row.kind !== 'goal' && !row.check_spec) {
|
|
499
|
+
const cls = classify(row);
|
|
500
|
+
const cmd = cls.checkable ? compileCheck(cls.spec) : null;
|
|
501
|
+
const spec = cls.checkable ? (cmd ? JSON.stringify(typeof cmd === 'string' ? { cmd } : cmd) : null) : null;
|
|
502
|
+
db.prepare('UPDATE requirements SET checkable = ?, check_spec = ?, compiled_cmd = ? WHERE id = ?')
|
|
503
|
+
.run(cls.checkable ? 1 : 0, cls.checkable ? JSON.stringify(cls.spec) : null, spec, row.id);
|
|
504
|
+
if (spec) compiledNew++;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
trailJournal('brief.chat-intake', { brief_id: briefId, added: fresh.length, compiled_new: compiledNew });
|
|
508
|
+
return {
|
|
509
|
+
briefId,
|
|
510
|
+
added: fresh.length,
|
|
511
|
+
total: model.requirements.length,
|
|
512
|
+
newRequirements: fresh,
|
|
513
|
+
compiledNew,
|
|
514
|
+
model: getBrief(briefId),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
380
518
|
/** Reset (tests). */
|
|
381
519
|
export function reset() {
|
|
382
520
|
const db = getDb();
|
|
383
521
|
db.prepare('DELETE FROM requirements').run();
|
|
384
522
|
db.prepare('DELETE FROM briefs').run();
|
|
523
|
+
db.prepare('DELETE FROM meta WHERE key = ?').run(ACTIVE_KEY);
|
|
385
524
|
}
|
package/src/mind/nlu.js
CHANGED
|
@@ -154,6 +154,12 @@ export const INTENT_LEXICON = Object.freeze([
|
|
|
154
154
|
{ intent: 'analyze', words: ['analyze', 'scan', 'inspect', 'audit', 'lint'] },
|
|
155
155
|
{ intent: 'fix', words: ['fix', 'repair', 'patch', 'resolve', 'clean', 'solve'] },
|
|
156
156
|
{ intent: 'run', words: ['run', 'execute', 'do', 'perform', 'start'] },
|
|
157
|
+
// The brief intent: a chat message that CARRIES requirements ("the report
|
|
158
|
+
// must contain totals", "users must not be deleted without audit") is a
|
|
159
|
+
// problem-understanding utterance, not a fix/run command. Routed to the
|
|
160
|
+
// brief organ: the requirements are ingested into the active brief and
|
|
161
|
+
// compiled to Gate checks — in the chat, no file needed.
|
|
162
|
+
{ intent: 'brief', words: ['must', 'must not', 'shall', 'should', 'requirement', 'requirements', 'acceptance', 'criteria', 'brief'] },
|
|
157
163
|
]);
|
|
158
164
|
|
|
159
165
|
/**
|
|
@@ -376,6 +382,18 @@ export function interpret(utterance, opts = {}) {
|
|
|
376
382
|
const specific = bestOf(tier1, 1);
|
|
377
383
|
if (specific) {
|
|
378
384
|
intent = specific.intent; matchedWord = specific.w;
|
|
385
|
+
// Brief-context precedence: when the utterance ALSO carries rule
|
|
386
|
+
// modality ("must"/"shall"/"never"), it is a requirements utterance
|
|
387
|
+
// even if a task verb appears in it — "fix the login bug, the report
|
|
388
|
+
// must contain totals" carries a requirement; the verb describes the
|
|
389
|
+
// work the requirements govern. The brief organ ingests the whole
|
|
390
|
+
// message; the verb is preserved as evidence.
|
|
391
|
+
const hasBrief = intentEvidence.some(e => e.intent === 'brief');
|
|
392
|
+
const hasActionVerb = intent !== 'brief' && ['fix', 'analyze', 'plan', 'run'].includes(intent);
|
|
393
|
+
if (hasBrief && hasActionVerb) {
|
|
394
|
+
intent = 'brief';
|
|
395
|
+
matchedWord = intentEvidence.find(e => e.intent === 'brief')?.word ?? matchedWord;
|
|
396
|
+
}
|
|
379
397
|
} else {
|
|
380
398
|
const generic = bestOf(tier2, 2);
|
|
381
399
|
if (generic) { intent = generic.intent; matchedWord = generic.w; }
|