@awak-app/simy-cli 0.7.37 → 0.7.39
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 +55 -5
- package/package.json +2 -1
- package/src/auto-update.js +25 -13
- package/src/claude-desktop/bridge.js +156 -0
- package/src/claude-desktop/extension.js +84 -0
- package/src/claude-desktop/server.js +86 -0
- package/src/cli-contract.js +1 -1
- package/src/cli-observability.js +2 -2
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +240 -24
- package/src/codex-plugins/simy/hooks/mention-router.mjs +42 -0
- package/src/codex-plugins/simy/hooks/purpose-workspace.mjs +4 -1
- package/src/codex-plugins/simy/hooks/registered-requirement-checker.mjs +201 -0
- package/src/codex-plugins/simy/hooks/requirement-acceptance.mjs +69 -0
- package/src/codex-plugins/simy/skills/continue/SKILL.md +8 -3
- package/src/delivery-hooks/supervisor-watchdog.js +3 -2
- package/src/desktop-host.js +48 -18
- package/src/index.js +10 -0
- package/src/runner.js +28 -26
- package/src/session-supervision/acceptance-reads.js +11 -2
- package/src/session-supervision/analysis-manager.js +9 -2
- package/src/session-supervision/catalog-refresh.js +16 -0
- package/src/session-supervision/claude-native-agent-contract.js +62 -0
- package/src/session-supervision/claude-outcome-review.js +7 -6
- package/src/session-supervision/claude-sessions.js +137 -76
- package/src/session-supervision/codex-app-server.js +3 -3
- package/src/session-supervision/codex-children.js +2 -2
- package/src/session-supervision/conversation-context.js +58 -26
- package/src/session-supervision/discovery-window.js +8 -2
- package/src/session-supervision/index.js +25 -12
- package/src/session-supervision/purpose-verification.js +12 -3
- package/src/sqm/advisory-guidance.js +1 -0
- package/src/sqm/checkers.js +39 -1
- package/src/sqm/claude-plugin.js +15 -1
- package/src/sqm/claude-simy-agent.md +11 -0
- package/src/sqm/delivery-assessment.js +78 -0
- package/src/sqm/hypothesis-knowledge.js +3 -3
- package/src/sqm/knowledge-recall.js +34 -12
- package/src/sqm/reasoned-assessment.js +27 -12
- package/src/sqm/workflow-onboarding.js +2 -1
- package/src/update-detachment/runtime.js +2 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Only human/assistant prose crosses the analysis boundary. Tool payloads,
|
|
2
2
|
// attachments, paths and provider envelopes are deliberately excluded.
|
|
3
3
|
export const CONVERSATION_LIMITS = Object.freeze({ messages: 32, text: 4_000, total: 24_000, bytes: 512 * 1024 });
|
|
4
|
+
// Independent review preserves complete original requests within its own budget.
|
|
5
|
+
export const REVIEW_USER_CONTEXT_LIMITS = Object.freeze({ messages: 32, text: 64 * 1024, total: 128 * 1024 });
|
|
4
6
|
const CREDENTIAL_KEY = 'api[_ -]?key|access[_ -]?token|password|authorization';
|
|
5
7
|
const QUOTED_CREDENTIAL = new RegExp(String.raw`(["'])(${CREDENTIAL_KEY})\1(\s*:\s*)(["'])(?:\\.|(?!\4)[^\\])*\4`, 'gi');
|
|
6
8
|
const PLAIN_CREDENTIAL = new RegExp(String.raw`\b(${CREDENTIAL_KEY})\s*[:=]\s*(?:Bearer\s+)?[^\s,;]+`, 'gi');
|
|
@@ -41,32 +43,62 @@ export function boundProviderGoal(goal) {
|
|
|
41
43
|
...(goal.textTruncated === true || objective.length > 4000 ? { textTruncated: true } : {}) };
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
// Streaming readers and array callers share one prose-selection policy. Keep
|
|
47
|
+
// the first goal, latest messages and latest-user identity without retaining
|
|
48
|
+
// an entire transcript or sanitizing/trimming an already clipped message twice.
|
|
49
|
+
export function createConversationCollector({ reviewUserContext = false } = {}) {
|
|
50
|
+
const limits = reviewUserContext ? REVIEW_USER_CONTEXT_LIMITS : CONVERSATION_LIMITS;
|
|
51
|
+
const prose = [];
|
|
52
|
+
let count = 0, clipped = false, latestUser = null, foundRedacted = false;
|
|
53
|
+
return {
|
|
54
|
+
add(message) {
|
|
55
|
+
if (!['user', 'assistant'].includes(message?.role) || reviewUserContext && message.role !== 'user') return;
|
|
56
|
+
const text = sanitizedText(message.text);
|
|
57
|
+
if (reviewUserContext) {
|
|
58
|
+
foundRedacted ||= text.includes('[REDACTED]');
|
|
59
|
+
clipped ||= message.textTruncated === true;
|
|
60
|
+
}
|
|
61
|
+
if (!text) return;
|
|
62
|
+
if (text.length > limits.text) clipped = true;
|
|
63
|
+
const normalized = { role: message.role, turnRef: sanitizedText(message.turnRef).slice(0, 160) || `message:${count}`,
|
|
64
|
+
textTruncated: message.textTruncated === true || text.length > limits.text,
|
|
65
|
+
text: text.slice(0, limits.text) };
|
|
66
|
+
count++; prose.push(normalized);
|
|
67
|
+
if (normalized.role === 'user') latestUser = normalized;
|
|
68
|
+
if (prose.length > limits.messages + 1) prose.splice(1, 1);
|
|
69
|
+
},
|
|
70
|
+
finish({ truncated = false, incomplete = false, redacted = false } = {}) {
|
|
71
|
+
redacted ||= foundRedacted;
|
|
72
|
+
incomplete ||= reviewUserContext && redacted;
|
|
73
|
+
truncated ||= clipped || prose.length > limits.messages;
|
|
74
|
+
// Preserve goal provenance as well as the latest user/assistant exchange.
|
|
75
|
+
const selected = prose.length > limits.messages ? [prose[0], ...prose.slice(-(limits.messages - 1))] : prose;
|
|
76
|
+
let total = 0;
|
|
77
|
+
const result = [];
|
|
78
|
+
// Spend the bounded budget from newest to oldest; never trim a latest
|
|
79
|
+
// request to make room for early goal provenance.
|
|
80
|
+
for (const message of selected.toReversed()) {
|
|
81
|
+
if (total + message.text.length > limits.total) { truncated = true; continue; }
|
|
82
|
+
result.unshift(message);
|
|
83
|
+
total += message.text.length;
|
|
84
|
+
}
|
|
85
|
+
const finalMessage = prose.at(-1);
|
|
86
|
+
if (!latestUser || !result.includes(latestUser) || latestUser.textTruncated || finalMessage?.textTruncated) incomplete = true;
|
|
87
|
+
return { messages: result.map(({ textTruncated, ...message }) => ({ ...message, ...(textTruncated ? { textTruncated: true } : {}) })), truncated, incomplete, ...(reviewUserContext ? { redacted } : {}) };
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
44
92
|
export function boundConversation(messages, { truncated = false, incomplete = false } = {}) {
|
|
45
93
|
if (!Array.isArray(messages)) return { messages: [], truncated: false, incomplete: true };
|
|
46
|
-
const
|
|
47
|
-
for (const message of messages)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
if (prose.length > CONVERSATION_LIMITS.messages) truncated = true;
|
|
57
|
-
// Preserve goal provenance as well as the latest user/assistant exchange.
|
|
58
|
-
const selected = prose.length > CONVERSATION_LIMITS.messages ? [prose[0], ...prose.slice(-(CONVERSATION_LIMITS.messages - 1))] : prose;
|
|
59
|
-
let total = 0;
|
|
60
|
-
const result = [];
|
|
61
|
-
// Spend the bounded budget from newest to oldest; never trim a latest
|
|
62
|
-
// request to make room for early goal provenance.
|
|
63
|
-
for (const message of selected.toReversed()) {
|
|
64
|
-
if (total + message.text.length > CONVERSATION_LIMITS.total) { truncated = true; continue; }
|
|
65
|
-
result.unshift(message);
|
|
66
|
-
total += message.text.length;
|
|
67
|
-
}
|
|
68
|
-
const latestUser = prose.findLast(message => message.role === 'user');
|
|
69
|
-
const finalMessage = prose.at(-1);
|
|
70
|
-
if (!latestUser || !result.includes(latestUser) || latestUser.textTruncated || finalMessage?.textTruncated) incomplete = true;
|
|
71
|
-
return { messages: result.map(({ textTruncated, ...message }) => ({ ...message, ...(textTruncated ? { textTruncated: true } : {}) })), truncated, incomplete };
|
|
94
|
+
const collector = createConversationCollector();
|
|
95
|
+
for (const message of messages) collector.add(message);
|
|
96
|
+
return collector.finish({ truncated, incomplete });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function boundReviewUserContext(messages, { truncated = false, incomplete = false, redacted = false } = {}) {
|
|
100
|
+
if (!Array.isArray(messages)) return { messages: [], truncated: false, incomplete: true, redacted };
|
|
101
|
+
const collector = createConversationCollector({ reviewUserContext: true });
|
|
102
|
+
for (const message of messages) collector.add(message);
|
|
103
|
+
return collector.finish({ truncated, incomplete, redacted });
|
|
72
104
|
}
|
|
@@ -4,14 +4,16 @@ export const RECENT_SESSION_LIMIT = 200;
|
|
|
4
4
|
|
|
5
5
|
// Explicit tracking and unsettled work remain visible outside recent history.
|
|
6
6
|
// Completed, untracked records are history, not an unlimited extra catalog.
|
|
7
|
-
export const
|
|
7
|
+
export const supervisedSession = record => Boolean(record?.autoContinue || record?.monitor || record?.tracked ||
|
|
8
8
|
record?.run && !TERMINAL.has(record.run.state));
|
|
9
|
+
export const retainedSession = record => Boolean(record?.listPinned || supervisedSession(record));
|
|
9
10
|
|
|
10
|
-
export function recentSessionWindow(items, { limit = RECENT_SESSION_LIMIT, priority = () => false } = {}) {
|
|
11
|
+
export function recentSessionWindow(items, { limit = RECENT_SESSION_LIMIT, priority = () => false, includeAncestors = true } = {}) {
|
|
11
12
|
const recent = [...items].sort((a, b) => (b.lastActivityAt || 0) - (a.lastActivityAt || 0) || a.session.localeCompare(b.session));
|
|
12
13
|
const retained = [], history = [];
|
|
13
14
|
for (const item of recent) (priority(item) ? retained : history).push(item);
|
|
14
15
|
const selected = [...retained, ...history.slice(0, Math.max(0, limit - retained.length))];
|
|
16
|
+
if (!includeAncestors) return selected;
|
|
15
17
|
const byKey = new Map(items.map(item => [`${item.provider}:${item.session}`, item]));
|
|
16
18
|
const included = new Set(selected.map(item => `${item.provider}:${item.session}`));
|
|
17
19
|
// Keep observed ancestors so a recent child does not silently become a root.
|
|
@@ -25,3 +27,7 @@ export function recentSessionWindow(items, { limit = RECENT_SESSION_LIMIT, prior
|
|
|
25
27
|
}
|
|
26
28
|
return selected;
|
|
27
29
|
}
|
|
30
|
+
|
|
31
|
+
// Tree visibility and navigation pins do not subscribe all ancestors to live
|
|
32
|
+
// work. Explicit supervision and owned jobs are supplied by each caller.
|
|
33
|
+
export const liveSessionWindow = (items, options = {}) => recentSessionWindow(items, { ...options, includeAncestors: false });
|
|
@@ -16,7 +16,7 @@ import { loadPurposeRuleSet } from './purpose-instruction.js';
|
|
|
16
16
|
import { originalSessionTarget } from './original-session-target.js';
|
|
17
17
|
import { readCodexChildSessions } from './codex-children.js';
|
|
18
18
|
import { readCodexTitles, sessionTitle } from './session-titles.js';
|
|
19
|
-
import {
|
|
19
|
+
import { RECENT_SESSION_LIMIT, liveSessionWindow, retainedSession, supervisedSession } from './discovery-window.js';
|
|
20
20
|
import { requireProviderLink } from '../provider-links.js';
|
|
21
21
|
import { readDispatchGate, rootFromDelivery, sessionDispatchReadiness } from '../codex-plugins/simy/hooks/update-dispatch-gate.mjs';
|
|
22
22
|
|
|
@@ -165,13 +165,13 @@ export async function discoverSessions(options = {}) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
async function discoverSessionCatalog({ root, openClient = openCodexAppServer, listClaude = listClaudeSessions,
|
|
168
|
-
inspectProcess = processIdentity, now: requestedNow, signal, listCodexTitles = readCodexTitles, listCodexChildren = readCodexChildSessions, codexHome, connectedProviders = ['codex', 'claude'], includeArchived = false, includeSelectionCandidates = false, catalogSnapshot, recordSnapshot, archiveReconciliations = new Map(), readRecords = listRecords, onProgress } = {}) {
|
|
168
|
+
inspectProcess = processIdentity, now: requestedNow, signal, listCodexTitles = readCodexTitles, listCodexChildren = readCodexChildSessions, codexHome, connectedProviders = ['codex', 'claude'], includeArchived = false, includeSelectionCandidates = false, recentOnly = false, catalogSnapshot, recordSnapshot, archiveReconciliations = new Map(), readRecords = listRecords, onProgress } = {}) {
|
|
169
169
|
const now = requestedNow ?? Date.now();
|
|
170
170
|
const assertActive = () => { if (signal?.aborted) throw new CodexAppServerError('cancelled', 'Conversation discovery was cancelled.'); };
|
|
171
171
|
assertActive();
|
|
172
172
|
const connected = new Set(connectedProviders);
|
|
173
173
|
const records = (recordSnapshot || await readRecords(root)).filter(record => connected.has(record.provider));
|
|
174
|
-
const priorities = records.filter(
|
|
174
|
+
const priorities = records.filter(supervisedSession);
|
|
175
175
|
let archivedCodex = [];
|
|
176
176
|
let discovered = [], codexError = null, codexReasonCode = null, claude = [], claudeError = null;
|
|
177
177
|
let archiveStates = new Map();
|
|
@@ -192,7 +192,7 @@ async function discoverSessionCatalog({ root, openClient = openCodexAppServer, l
|
|
|
192
192
|
} else await Promise.all([
|
|
193
193
|
(async () => {
|
|
194
194
|
try { if (connected.has('codex')) discovered = await withClient(openClient, async client => {
|
|
195
|
-
const active = await listCodexSessions({ client, all:
|
|
195
|
+
const active = await listCodexSessions({ client, all: !recentOnly, signal, inspectState: true, codexHome,
|
|
196
196
|
onPage: async items => { discovered = [...items]; await progress({ discovered, archivedCodex }); },
|
|
197
197
|
prioritySessions: priorities.filter(record => record.provider === 'codex').map(record => record.session) });
|
|
198
198
|
if (includeArchived) {
|
|
@@ -207,7 +207,7 @@ async function discoverSessionCatalog({ root, openClient = openCodexAppServer, l
|
|
|
207
207
|
}
|
|
208
208
|
return response;
|
|
209
209
|
} };
|
|
210
|
-
try { await listCodexSessions({ client: archiveClient, all:
|
|
210
|
+
try { await listCodexSessions({ client: archiveClient, all: !recentOnly, signal, archived: true,
|
|
211
211
|
onPage: async () => { archivedCodex = [...observedArchives.values()]; await progress({ discovered, archivedCodex }); } }); }
|
|
212
212
|
catch (error) {
|
|
213
213
|
codexError = 'Codex archive discovery incomplete. Refresh to retry.';
|
|
@@ -216,7 +216,7 @@ async function discoverSessionCatalog({ root, openClient = openCodexAppServer, l
|
|
|
216
216
|
archivedCodex = [...observedArchives.values()];
|
|
217
217
|
}
|
|
218
218
|
const ids = new Set(active.map(item => item.session));
|
|
219
|
-
archiveStates = await readCodexArchiveStates(client, records.filter(record => record.provider === 'codex' && !ids.has(record.session)), { codexHome });
|
|
219
|
+
archiveStates = await readCodexArchiveStates(client, records.filter(record => record.provider === 'codex' && !ids.has(record.session) && (!recentOnly || supervisedSession(record))), { codexHome });
|
|
220
220
|
for (const item of archivedCodex) archiveStates.set(item.session, true);
|
|
221
221
|
return active;
|
|
222
222
|
}, signal); }
|
|
@@ -226,17 +226,22 @@ async function discoverSessionCatalog({ root, openClient = openCodexAppServer, l
|
|
|
226
226
|
}
|
|
227
227
|
})(),
|
|
228
228
|
(async () => {
|
|
229
|
-
try { if (connected.has('claude')) claude = await listClaude({ signal, all:
|
|
229
|
+
try { if (connected.has('claude')) claude = await listClaude({ signal, all: !recentOnly, limit: RECENT_SESSION_LIMIT, onProgress: items => { claude = items; return progress({ claude }); },
|
|
230
230
|
prioritySessions: priorities.filter(record => record.provider === 'claude').map(record => record.session) }); }
|
|
231
231
|
catch { claudeError = 'Claude conversation discovery unavailable.'; }
|
|
232
232
|
})(),
|
|
233
233
|
]);
|
|
234
234
|
assertActive();
|
|
235
235
|
const activeCodex = new Set(discovered.map(item => item.session));
|
|
236
|
+
const claudeByIdentity = new Map();
|
|
237
|
+
for (const item of claude) {
|
|
238
|
+
const key = JSON.stringify([item.session, item.cwd]);
|
|
239
|
+
if (!claudeByIdentity.has(key)) claudeByIdentity.set(key, item);
|
|
240
|
+
}
|
|
236
241
|
for (let index = 0; index < records.length; index++) {
|
|
237
242
|
assertActive();
|
|
238
243
|
const record = records[index];
|
|
239
|
-
const claudeItem = record.provider === 'claude' ?
|
|
244
|
+
const claudeItem = record.provider === 'claude' ? claudeByIdentity.get(JSON.stringify([record.session, record.cwd])) : null;
|
|
240
245
|
const archiveState = record.provider === 'codex' ? archiveStates.get(record.session) ?? (activeCodex.has(record.session) ? false : undefined) : claudeItem?.archived;
|
|
241
246
|
if (typeof archiveState !== 'boolean') continue;
|
|
242
247
|
const key = `${record.provider}:${record.session}`;
|
|
@@ -325,9 +330,10 @@ async function discoverSessionCatalog({ root, openClient = openCodexAppServer, l
|
|
|
325
330
|
const parentSession = navigationParent(item, record);
|
|
326
331
|
return { ...item, localHidden: record?.listHidden === true, ...(parentSession ? { parentSession } : {}) };
|
|
327
332
|
});
|
|
328
|
-
const liveItems =
|
|
329
|
-
|
|
330
|
-
|
|
333
|
+
const liveItems = liveSessionWindow(catalog.filter(item => item.archived !== true && (!item.localHidden ||
|
|
334
|
+
supervisedSession(recordsByKey.get(`${item.provider}:${item.session}`)))), {
|
|
335
|
+
priority: item => supervisedSession(recordsByKey.get(`${item.provider}:${item.session}`)),
|
|
336
|
+
});
|
|
331
337
|
const liveKeys = new Set(liveItems.map(item => `${item.provider}:${item.session}`));
|
|
332
338
|
const items = [...catalog];
|
|
333
339
|
// Include archived ancestors of active rows even when archive visibility is off.
|
|
@@ -357,6 +363,13 @@ async function discoverSessionCatalog({ root, openClient = openCodexAppServer, l
|
|
|
357
363
|
const selectionCandidates = includeSelectionCandidates ? [...codexTitles].map(([session, title]) => ({ provider: 'codex', session, title })) : undefined;
|
|
358
364
|
assertActive();
|
|
359
365
|
let nextItem = 0;
|
|
366
|
+
// Many conversations share a workspace. Resolve it once in this attempt,
|
|
367
|
+
// never across attempts or in the exact-session execution authority reader.
|
|
368
|
+
const workspaces = new Map();
|
|
369
|
+
const resolveWorkspace = cwd => {
|
|
370
|
+
if (!workspaces.has(cwd)) workspaces.set(cwd, realpath(cwd));
|
|
371
|
+
return workspaces.get(cwd);
|
|
372
|
+
};
|
|
360
373
|
const project = async item => {
|
|
361
374
|
if (item.delegated === true) return { ...item,
|
|
362
375
|
tracked: false, autoContinue: false, binding: { mode: 'unmanaged', taskId: null },
|
|
@@ -379,7 +392,7 @@ async function discoverSessionCatalog({ root, openClient = openCodexAppServer, l
|
|
|
379
392
|
if (supported || liveHook) {
|
|
380
393
|
try {
|
|
381
394
|
continuationAdmission = { provider: item.provider, session: item.session,
|
|
382
|
-
cwd: await
|
|
395
|
+
cwd: await resolveWorkspace(item.cwd), source: item.observation.source, model: item.model, validatedAt: now };
|
|
383
396
|
} catch { /* Missing workspaces cannot grant an admission. */ }
|
|
384
397
|
}
|
|
385
398
|
const availability = archived ? { canEnable: false, reasonCode: 'session_archived',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { boundConversation } from './conversation-context.js';
|
|
1
|
+
import { boundConversation, boundReviewUserContext } from './conversation-context.js';
|
|
2
2
|
import { currentAcceptanceRead } from './acceptance-reads.js';
|
|
3
3
|
import { normalizeOutcomeReview } from '../codex-plugins/simy/hooks/loop-kernel.mjs';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
@@ -11,19 +11,28 @@ export { workspaceIdentity } from '../codex-plugins/simy/hooks/purpose-workspace
|
|
|
11
11
|
// Filter before bounding so worker prose cannot crowd out user corrections.
|
|
12
12
|
// Untyped legacy snapshots cannot establish human provenance.
|
|
13
13
|
export function independentRequestContext(...snapshots) {
|
|
14
|
+
return requestContext(boundConversation, snapshots);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function independentReviewRequestContext(...snapshots) {
|
|
18
|
+
return requestContext(boundReviewUserContext, snapshots);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function requestContext(bound, snapshots) {
|
|
14
22
|
const messages = [], seen = new Set();
|
|
15
|
-
let incomplete = false, truncated = false;
|
|
23
|
+
let incomplete = false, truncated = false, redacted = false;
|
|
16
24
|
for (const snapshot of snapshots) {
|
|
17
25
|
if (!snapshot || !Array.isArray(snapshot.messages)) { incomplete = true; continue; }
|
|
18
26
|
incomplete ||= snapshot.incomplete === true;
|
|
19
27
|
truncated ||= snapshot.truncated === true;
|
|
28
|
+
redacted ||= snapshot.redacted === true;
|
|
20
29
|
for (const message of snapshot.messages) {
|
|
21
30
|
if (message?.role !== 'user') continue;
|
|
22
31
|
const key = JSON.stringify([message.turnRef, message.text]);
|
|
23
32
|
if (!seen.has(key)) { seen.add(key); messages.push(message); }
|
|
24
33
|
}
|
|
25
34
|
}
|
|
26
|
-
return
|
|
35
|
+
return bound(messages, { incomplete, truncated, redacted });
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
export function verificationInstruction(record, currentEvidence = null, sourceContext = null, observations = []) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Signed guidance is scoped reference data, never executable checks or authority.
|
|
2
|
+
export const MECHANISM_GUIDANCE_CAPABILITY = 'sqm-mechanism-guidance.v1';
|
|
2
3
|
export const ADVISORY_GUIDANCE_CAPABILITY = 'sqm-advisory-guidance.v1';
|
|
3
4
|
export const isAdvisoryGuidance = module => module?.schema_version === '1.2.0' && module?.kind === 'advisory_guidance';
|
|
4
5
|
|
package/src/sqm/checkers.js
CHANGED
|
@@ -45,7 +45,8 @@ export async function evaluateKnowledgeBundle(bundle, repository, options = {})
|
|
|
45
45
|
if (rule.type === 'reasoned_assessment') {
|
|
46
46
|
const assess = semanticCalls < (options.maxSemanticCalls ?? 4) ? options.assess : undefined;
|
|
47
47
|
if (assess) semanticCalls++;
|
|
48
|
-
|
|
48
|
+
const contextRepository = assess ? await withAssessmentContext(module, rule, repository) : repository;
|
|
49
|
+
assessment = await evaluateReasonedAssessment(module, rule, contextRepository, { assess, signal: options.signal });
|
|
49
50
|
if (!assess && options.assess) assessment.reason = 'semantic_call_budget_exhausted';
|
|
50
51
|
}
|
|
51
52
|
const violations = assessment
|
|
@@ -96,6 +97,43 @@ export async function evaluateKnowledgeBundle(bundle, repository, options = {})
|
|
|
96
97
|
return { findings, modules, matcherDiagnostics, advisoryGuidance: advisoryGuidanceDescriptions(bundle, repository.repository) };
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
// Supply bounded, line-numbered current code around changed hunks. Historical
|
|
101
|
+
// references cannot supply repository context or broaden the rule's routing.
|
|
102
|
+
async function withAssessmentContext(module, rule, repository) {
|
|
103
|
+
const routed = module.detection.matchers.flatMap(matcher => matcher.paths || []);
|
|
104
|
+
const files = routed.length ? repository.diff.filter(file => routed.some(pattern => globMatches(pattern, file.path))) : repository.diff;
|
|
105
|
+
// Scope only this assessment's code input. Repository identity, the full
|
|
106
|
+
// changed-path catalog and the signed proof continue to bind the entire diff.
|
|
107
|
+
const scoped = routed.length ? { ...repository, assessment_diff: files,
|
|
108
|
+
assessment_diff_scope: { matcher_paths: routed, omitted_unrelated_files: repository.diff.length - files.length } } : repository;
|
|
109
|
+
if (!rule.assessment.inputs.some(input => input.source.type === 'runtime_context' && input.source.key === 'repository.context')) return scoped;
|
|
110
|
+
const excerpts = []; let characters = 0;
|
|
111
|
+
for (const file of files.slice(0, 8)) {
|
|
112
|
+
try {
|
|
113
|
+
const lines = (await readText(repository.root, file.path)).split(/\r?\n/);
|
|
114
|
+
const selected = new Set();
|
|
115
|
+
for (const change of file.lines.filter(line => line.side === 'added')) {
|
|
116
|
+
for (let index = Math.max(0, change.line - 21); index < Math.min(lines.length, change.line + 20); index++) selected.add(index);
|
|
117
|
+
}
|
|
118
|
+
const text = [...selected].sort((a, b) => a - b).map(index => `${index + 1}: ${lines[index]}`).join('\n');
|
|
119
|
+
if (!text) continue;
|
|
120
|
+
const size = JSON.stringify({ file: file.path, excerpt: text }).length;
|
|
121
|
+
if (characters + size > 24_000) break;
|
|
122
|
+
excerpts.push({ file: file.path, excerpt: text }); characters += size;
|
|
123
|
+
} catch {
|
|
124
|
+
excerpts.push({ file: file.path, status: 'unavailable' });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const omitted = files.filter(file => !excerpts.some(item => item.file === file.path)).map(file => file.path);
|
|
128
|
+
// Keep code literal: serializing excerpts into nested JSON makes otherwise
|
|
129
|
+
// correct multiline quotations fail exact evidence validation.
|
|
130
|
+
return { ...scoped, assessment_context: [
|
|
131
|
+
'selection: changed_hunks_with_20_surrounding_lines; scope: bounded_context_only',
|
|
132
|
+
...excerpts.map(item => `File: ${item.file}\n${item.excerpt || 'unavailable'}`),
|
|
133
|
+
`Omitted files: ${JSON.stringify(omitted)}`,
|
|
134
|
+
].join('\n\n') };
|
|
135
|
+
}
|
|
136
|
+
|
|
99
137
|
function moduleDiagnostic(module, reason, matchers = []) {
|
|
100
138
|
return {
|
|
101
139
|
module_id: module.id,
|
package/src/sqm/claude-plugin.js
CHANGED
|
@@ -14,6 +14,7 @@ const AUTHOR = "Specialist Doctors Inc.";
|
|
|
14
14
|
// Generate both providers from the maintained workflow source. Only provider
|
|
15
15
|
// terminology, manifest schema, and hook environment names differ.
|
|
16
16
|
export async function claudePluginFiles() {
|
|
17
|
+
const packageInfo = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"));
|
|
17
18
|
const sourceManifest = JSON.parse(await readFile(path.join(SOURCE, ".codex-plugin/plugin.json"), "utf8"));
|
|
18
19
|
const files = new Map();
|
|
19
20
|
files.set('agents/outcome-review.md', '---\nname: outcome-review\ndescription: Independent tool-free outcome review under the original Claude budget\ntools: []\n---\n' + CLAUDE_NATIVE_REVIEW_PROMPT + '\n');
|
|
@@ -34,7 +35,10 @@ export async function claudePluginFiles() {
|
|
|
34
35
|
files.set(`hooks/${name}`, await readFile(path.join(SOURCE, "hooks", name), "utf8"));
|
|
35
36
|
}
|
|
36
37
|
const collector = await readFile(new URL("../cli-observability.js", import.meta.url), "utf8");
|
|
37
|
-
|
|
38
|
+
// Standalone hooks do not ship the CLI capability dependency graph. Freeze the
|
|
39
|
+
// maintained package version into their generated collector instead.
|
|
40
|
+
files.set("hooks/cli-observability.mjs", collector.replace("./web-api.js", "./web-api.mjs")
|
|
41
|
+
.replace("import { CLI_VERSION } from './cli-contract.js';", `const CLI_VERSION = ${JSON.stringify(packageInfo.version)};`));
|
|
38
42
|
files.set("hooks/web-api.mjs", await readFile(new URL("../web-api.js", import.meta.url), "utf8"));
|
|
39
43
|
const deliveryLoop = await readFile(path.join(SOURCE, "hooks/delivery-loop.mjs"), "utf8");
|
|
40
44
|
files.set("hooks/delivery-loop.mjs", deliveryLoop.replace(
|
|
@@ -61,10 +65,14 @@ export async function claudePluginFiles() {
|
|
|
61
65
|
files.set("hooks/purpose-cycle.mjs", await readFile(path.join(SOURCE, "hooks/purpose-cycle.mjs"), "utf8"));
|
|
62
66
|
files.set("hooks/purpose-rule-policy.mjs", await readFile(path.join(SOURCE, "hooks/purpose-rule-policy.mjs"), "utf8"));
|
|
63
67
|
files.set("hooks/acceptance-policy.mjs", await readFile(path.join(SOURCE, "hooks/acceptance-policy.mjs"), "utf8"));
|
|
68
|
+
files.set("hooks/requirement-acceptance.mjs", await readFile(path.join(SOURCE, "hooks/requirement-acceptance.mjs"), "utf8"));
|
|
69
|
+
files.set("hooks/registered-requirement-checker.mjs", await readFile(path.join(SOURCE, "hooks/registered-requirement-checker.mjs"), "utf8"));
|
|
64
70
|
files.set("hooks/purpose-rule-review.mjs", await readFile(path.join(SOURCE, "hooks/purpose-rule-review.mjs"), "utf8"));
|
|
65
71
|
files.set("hooks/workspace-lease.mjs", await readFile(path.join(SOURCE, "hooks/workspace-lease.mjs"), "utf8"));
|
|
66
72
|
for (const name of ["update-state-store.mjs", "update-dispatch-gate.mjs", "update-dispatch-identity.mjs"]) files.set(`hooks/${name}`, await readFile(path.join(SOURCE, "hooks", name), "utf8"));
|
|
67
73
|
files.set("hooks/connection-reminder.mjs", await readFile(path.join(SOURCE, "hooks/connection-reminder.mjs"), "utf8"));
|
|
74
|
+
files.set("hooks/mention-router.mjs", await readFile(path.join(SOURCE, "hooks/mention-router.mjs"), "utf8"));
|
|
75
|
+
files.set("agents/simy.md", await readFile(new URL("./claude-simy-agent.md", import.meta.url), "utf8"));
|
|
68
76
|
files.set("hooks/dev-work.mjs", await readFile(path.join(SOURCE, "hooks/dev-work.mjs"), "utf8"));
|
|
69
77
|
const hooks = JSON.parse(await readFile(path.join(SOURCE, "hooks/hooks.json"), "utf8"));
|
|
70
78
|
delete hooks.hooks.Interrupt; // Codex-only event; Claude interruptions cannot be vetoed.
|
|
@@ -92,6 +100,12 @@ export async function claudePluginFiles() {
|
|
|
92
100
|
}
|
|
93
101
|
}
|
|
94
102
|
}
|
|
103
|
+
hooks.hooks.UserPromptSubmit.unshift({ hooks: [{
|
|
104
|
+
type: "command",
|
|
105
|
+
command: hookRuntimeCommand("node --input-type=module -e \"import { pathToFileURL } from 'node:url'; import { join } from 'node:path'; const hook = await import(pathToFileURL(join(process.env.CLAUDE_PLUGIN_ROOT, 'hooks', 'mention-router.mjs')).href); await hook.main();\""),
|
|
106
|
+
timeout: 3,
|
|
107
|
+
statusMessage: "SIMY request routing",
|
|
108
|
+
}] });
|
|
95
109
|
files.set("hooks/hooks.json", json(hooks));
|
|
96
110
|
const hash = createHash("sha256");
|
|
97
111
|
for (const [name, contents] of [...files].sort(([a], [b]) => a.localeCompare(b))) hash.update(name).update("\0").update(contents);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: simy
|
|
3
|
+
description: SIMY — interpret @simy requests for continuation, diagnosis, quality checks, PR review, status or stopping work. Return guidance to the original Claude conversation.
|
|
4
|
+
tools: []
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are SIMY's intent guide, not an execution owner. Interpret the user's entire request in context, in Japanese or English. Return the intended action and which installed SIMY skill the parent Claude should read: continue (continuation, status, Auto Continue off or stopping), diagnose (why work stopped or explicitly authorized repair), sqm-loop (explicit quality check), or pr-review (explicit PR review). Answer questions about SIMY directly. For combined requests preserve each scope.
|
|
8
|
+
|
|
9
|
+
Do not start, resume, stop, review, or certify work yourself. This agent has no tools. The parent conversation must use its own verified original provider/session and the existing SIMY controls, and report their actual receipts. Never use this child agent's session identity. The parent's prompt hook supplies the skill paths.
|
|
10
|
+
|
|
11
|
+
A mention, selected agent, quote, code example, hypothetical, negation or request to wait is not permission to start work. Diagnosis does not authorize repair/continuation; continuation does not imply SQM; PR review does not authorize fixes. Clarify ambiguous requests. Preserve stops, approvals, opt-outs and permissions. Do not infer completion from an accepted setting or missing evidence.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { canStartProvider, refreshBudgetState } from '../orchestrator/budget.js';
|
|
4
|
+
import { createProviderStreamDecoder, completeProviderUsage } from '../provider-stream.js';
|
|
5
|
+
import { captureDispatchOwner, assertTaskAdmission, dispatchWithFence, observeDispatchEvent } from '../update-detachment/dispatch-gate.js';
|
|
6
|
+
import { captureDispatchProvenance } from '../update-detachment/provenance.js';
|
|
7
|
+
import { createLocalAssessmentRunner } from './reasoned-assessment.js';
|
|
8
|
+
|
|
9
|
+
// A semantic check is another reviewer call under the existing task authority
|
|
10
|
+
// and token ceiling, not an unaccounted provider invocation.
|
|
11
|
+
export function createDeliveryAssessmentRunner(run, { repositoryPath, authorize, persist, ...options }) {
|
|
12
|
+
const provider = run.snapshot.charter.audit_backend || run.snapshot.charter.backend;
|
|
13
|
+
return async (request, context = {}) => {
|
|
14
|
+
const owner = captureDispatchOwner(run.sqmHistoryRoot || (run.localStateRoot ? path.dirname(run.localStateRoot) : undefined));
|
|
15
|
+
const invocationId = randomUUID();
|
|
16
|
+
const attemptId = `${run.snapshot.id}:reviewer:sqm:${invocationId}`;
|
|
17
|
+
const provenance = captureDispatchProvenance({ root: owner.root, provider, worktree: repositoryPath,
|
|
18
|
+
accountSession: run.session, origin: run.apiOrigin || undefined, taskId: run.snapshot.id,
|
|
19
|
+
recordRef: run.localStateRoot ? path.join(run.localStateRoot, `${run.snapshot.id}.json`) : `coding-run:${run.snapshot.id}` });
|
|
20
|
+
const records = [], writes = [];
|
|
21
|
+
let terminalCount = 0, incomplete = false, persistenceError;
|
|
22
|
+
const checkpoint = status => {
|
|
23
|
+
run.snapshot.metadata.provider_usage_checkpoints ??= {};
|
|
24
|
+
run.snapshot.metadata.provider_usage_checkpoints[invocationId] = {
|
|
25
|
+
invocation_id: invocationId, phase: 'reviewer', purpose: 'sqm_semantic', backend: provider,
|
|
26
|
+
usage_status: status, token_usage: { records: structuredClone(records) },
|
|
27
|
+
};
|
|
28
|
+
refreshBudgetState(run.snapshot);
|
|
29
|
+
writes.push(Promise.resolve().then(persist).catch(error => { persistenceError ||= error; }));
|
|
30
|
+
};
|
|
31
|
+
const assess = createLocalAssessmentRunner(provider, {
|
|
32
|
+
...options,
|
|
33
|
+
beforeDispatch: async () => {
|
|
34
|
+
await authorize();
|
|
35
|
+
context.signal?.throwIfAborted();
|
|
36
|
+
assertTaskAdmission(owner, { taskId: run.snapshot.id, provenance });
|
|
37
|
+
const permission = canStartProvider(run.snapshot, { phase: 'reviewer' });
|
|
38
|
+
if (!permission.allowed) throw Object.assign(new Error(`SQM reviewer is held: ${permission.reason}`), { code: permission.reason });
|
|
39
|
+
},
|
|
40
|
+
dispatchTransport: initiate => {
|
|
41
|
+
// Recheck immediately at the synchronous spawn boundary as well.
|
|
42
|
+
authorize();
|
|
43
|
+
const permission = canStartProvider(run.snapshot, { phase: 'reviewer' });
|
|
44
|
+
if (!permission.allowed) throw Object.assign(new Error(`SQM reviewer is held: ${permission.reason}`), { code: permission.reason });
|
|
45
|
+
if (run.stopRequested) throw Object.assign(new Error('SQM review cancelled'), { code: 'analysis_cancelled' });
|
|
46
|
+
const child = dispatchWithFence(owner, { taskId: run.snapshot.id, attemptId, provenance }, initiate);
|
|
47
|
+
run.child = child;
|
|
48
|
+
checkpoint('pending');
|
|
49
|
+
const decoder = createProviderStreamDecoder({ backend: provider, onLine() {},
|
|
50
|
+
onUsage: usage => { records.push({ ...usage, backend: provider, phase: 'reviewer', invocation_id: invocationId }); checkpoint('pending'); },
|
|
51
|
+
onEvent: event => {
|
|
52
|
+
if (provider === 'claude' && event.type === 'result' || provider === 'codex' && event.type === 'turn.completed') {
|
|
53
|
+
terminalCount++; incomplete ||= !completeProviderUsage(provider, event);
|
|
54
|
+
}
|
|
55
|
+
observeDispatchEvent({ root: owner.root, attemptId, provider, event,
|
|
56
|
+
onError: error => { run.snapshot.metadata.dispatch_recovery_required = error.code || 'provenance_persistence_failed'; } });
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
child.stdout?.on('data', chunk => decoder.push(chunk.toString('utf8')));
|
|
60
|
+
child.once('close', () => {
|
|
61
|
+
decoder.flush();
|
|
62
|
+
if (run.child === child) run.child = null;
|
|
63
|
+
checkpoint(terminalCount > 0 && !incomplete && (provider !== 'claude' || terminalCount === 1) && records.length ? 'observed' : 'unknown');
|
|
64
|
+
});
|
|
65
|
+
return child;
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
try { return await assess(request, context); }
|
|
69
|
+
finally {
|
|
70
|
+
await Promise.all(writes);
|
|
71
|
+
if (persistenceError) {
|
|
72
|
+
run.snapshot.metadata.provider_usage_checkpoints[invocationId].usage_status = 'unknown';
|
|
73
|
+
refreshBudgetState(run.snapshot);
|
|
74
|
+
throw persistenceError;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ADVISORY_GUIDANCE_CAPABILITY, isAdvisoryGuidance, advisoryGuidanceDescriptions } from './advisory-guidance.js';
|
|
1
|
+
import { ADVISORY_GUIDANCE_CAPABILITY, MECHANISM_GUIDANCE_CAPABILITY, isAdvisoryGuidance, advisoryGuidanceDescriptions } from './advisory-guidance.js';
|
|
2
2
|
import { CLI_VERSION } from '../cli-contract.js';
|
|
3
3
|
import { isSessionValid, readSession } from '../session-store.js';
|
|
4
4
|
import { resolveWebOrigin } from '../web-origin.js';
|
|
@@ -39,7 +39,7 @@ export async function loadHypothesisKnowledge({ cwd, webOrigin, allowNetwork = f
|
|
|
39
39
|
const credentials = explicit ? { organizationId: env.SIMY_SQM_ORGANIZATION_ID, accountId: env.SIMY_SQM_ACCOUNT_ID, token: env.SIMY_SQM_TOKEN || env.SIMY_ACCESS_TOKEN, endpoint: env.SIMY_SQM_BUNDLE_URL } : { organizationId: session.organization_id || session.org_id, accountId: session.account_id || session.auth_user_id || session.device_id, token: session.token, endpoint: savedEndpoint };
|
|
40
40
|
if (!credentials.accountId || !credentials.organizationId || !credentials.token) return unavailable('existing_sqm_identity_incomplete');
|
|
41
41
|
const repository = await repositoryIdentityImpl(cwd, { signal });
|
|
42
|
-
const loaded = await loadBundleImpl({ repository, capabilities: [ADVISORY_GUIDANCE_CAPABILITY], cliVersion: CLI_VERSION,
|
|
42
|
+
const loaded = await loadBundleImpl({ repository, capabilities: [ADVISORY_GUIDANCE_CAPABILITY, MECHANISM_GUIDANCE_CAPABILITY], cliVersion: CLI_VERSION,
|
|
43
43
|
...credentials,
|
|
44
44
|
fetchImpl: async (...args) => {
|
|
45
45
|
if (!allowNetwork) throw Error('Network knowledge retrieval is unavailable.');
|
|
@@ -73,7 +73,7 @@ export async function loadHypothesisKnowledge({ cwd, webOrigin, allowNetwork = f
|
|
|
73
73
|
return { status: 'available', repository, bundleDigest: loaded.bundle.integrity.digest,
|
|
74
74
|
...recalled, changeContextStatus,
|
|
75
75
|
searchCommand: ['simy', 'sqm', 'knowledge', '--host', origin, ...(!allowNetwork ? ['--offline'] : []), '--json'],
|
|
76
|
-
advisoryGuidance: advisoryGuidanceDescriptions(loaded.bundle, repository), source: loaded.source, fetchedFor: 'hypothesis_reasoning_only', executedTests: 0 };
|
|
76
|
+
advisoryGuidance: advisoryGuidanceDescriptions(loaded.bundle, repository).filter(item => recalled.modules.some(module => module.moduleId === item.module_id)), source: loaded.source, fetchedFor: 'hypothesis_reasoning_only', executedTests: 0 };
|
|
77
77
|
} catch (error) {
|
|
78
78
|
if (signal?.aborted) throw signal.reason;
|
|
79
79
|
const diagnostic = sqmDiagnostic(error, 'knowledge_loading');
|
|
@@ -5,8 +5,14 @@ const MAX_QUERY_CHARACTERS = 12_000;
|
|
|
5
5
|
const PAGE_SIZE = 20;
|
|
6
6
|
|
|
7
7
|
function terms(value) {
|
|
8
|
-
const
|
|
8
|
+
const normalized = String(value).normalize('NFKC');
|
|
9
|
+
const text = normalized.toLowerCase();
|
|
9
10
|
const result = new Set(text.match(/[\p{L}\p{N}_-]{2,}/gu) || []);
|
|
11
|
+
// Keep exact identifiers searchable while also finding their natural-language
|
|
12
|
+
// components. A change need not repeat the original camelCase or snake_case.
|
|
13
|
+
const components = normalized.replace(/([\p{Ll}\p{N}])(\p{Lu})/gu, '$1 $2')
|
|
14
|
+
.replace(/(\p{Lu})(\p{Lu}\p{Ll})/gu, '$1 $2').toLowerCase();
|
|
15
|
+
for (const term of components.match(/[\p{L}\p{N}]{2,}/gu) || []) result.add(term);
|
|
10
16
|
for (const [run] of text.matchAll(/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]+/gu)) {
|
|
11
17
|
for (let i = 0; i + 1 < run.length; i++) result.add(run.slice(i, i + 2));
|
|
12
18
|
}
|
|
@@ -21,26 +27,42 @@ export function selectReasoningKnowledge(bundle, repository, { query = '', offse
|
|
|
21
27
|
? scope.repositories.map(repo => repo.toLowerCase()) : [target];
|
|
22
28
|
const queryText = String(query).slice(0, MAX_QUERY_CHARACTERS);
|
|
23
29
|
const queryTerms = terms(queryText);
|
|
24
|
-
const eligible = bundle.modules.filter(module =>
|
|
30
|
+
const eligible = bundle.modules.filter(module =>
|
|
25
31
|
!['candidate', 'retired'].includes(module.lifecycle.status) &&
|
|
26
32
|
(!module.scope.repositories?.length || module.scope.repositories.some(repo => repositories.includes(repo.toLowerCase()))));
|
|
27
|
-
const
|
|
33
|
+
const documents = eligible.map(module => {
|
|
28
34
|
// Keep all bounded invariant fields together, particularly applicability and
|
|
29
35
|
// counter-evidence. A short title alone cannot support causal transfer.
|
|
30
36
|
const context = {
|
|
31
37
|
moduleId: module.id, version: module.version, title: module.title,
|
|
32
|
-
invariant: module.
|
|
33
|
-
|
|
38
|
+
invariant: isAdvisoryGuidance(module) ? module.guidance.statement : module.invariant.statement,
|
|
39
|
+
failureCondition: isAdvisoryGuidance(module) ? module.guidance.rationale : module.invariant.failure_condition || null,
|
|
40
|
+
remediation: isAdvisoryGuidance(module) ? null : module.invariant.remediation || null,
|
|
41
|
+
...(isAdvisoryGuidance(module) ? { kind: module.kind, applicability: module.guidance.applicability,
|
|
42
|
+
limitations: module.guidance.limitations, sources: module.sources } : {}),
|
|
34
43
|
scope: module.scope, lifecycle: module.lifecycle.status,
|
|
35
|
-
ruleCount: module.detection.checks.length, scenarioDefinitionCount: module.assurance?.scenarios?.length || 0,
|
|
36
|
-
incidentRefs: module.incident_refs,
|
|
44
|
+
ruleCount: isAdvisoryGuidance(module) ? 0 : module.detection.checks.length, scenarioDefinitionCount: module.assurance?.scenarios?.length || 0,
|
|
45
|
+
incidentRefs: module.incident_refs || [],
|
|
37
46
|
transferStatus: module.scope.repositories?.some(repo => repo.toLowerCase() === target) || !module.scope.repositories?.length
|
|
38
47
|
? 'target_not_evaluated' : 'cross_repository_hypothesis',
|
|
39
48
|
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
// Schema property names and shared repository identity are transport data,
|
|
50
|
+
// not evidence of mechanism relevance.
|
|
51
|
+
const vocabulary = terms([context.moduleId, context.title, context.invariant,
|
|
52
|
+
context.failureCondition, context.remediation, ...(context.applicability || []), ...(context.limitations || []), ...(module.scope.boundaries || []),
|
|
53
|
+
...(module.scope.languages || []), ...(module.scope.frameworks || [])].filter(Boolean).join(' '));
|
|
54
|
+
return { context, vocabulary };
|
|
55
|
+
});
|
|
56
|
+
const frequencies = new Map();
|
|
57
|
+
for (const { vocabulary } of documents) {
|
|
58
|
+
for (const term of queryTerms) if (vocabulary.has(term)) frequencies.set(term, (frequencies.get(term) || 0) + 1);
|
|
59
|
+
}
|
|
60
|
+
const scored = documents.map(({ context, vocabulary }) => ({ context,
|
|
61
|
+
// Inverse document frequency keeps a rare causal term ahead of many shared
|
|
62
|
+
// framework words. Scores rank context only; they never authorize checks.
|
|
63
|
+
score: [...queryTerms].reduce((sum, term) => vocabulary.has(term)
|
|
64
|
+
? sum + Math.log(1 + (documents.length - frequencies.get(term) + 0.5) / (frequencies.get(term) + 0.5)) : sum, 0),
|
|
65
|
+
})).sort((a, b) => b.score - a.score || a.context.moduleId.localeCompare(b.context.moduleId) || a.context.version.localeCompare(b.context.version));
|
|
44
66
|
const matches = queryTerms.size ? scored.filter(item => item.score > 0) : scored;
|
|
45
67
|
const modules = []; let characters = 0; let consumed = 0; let oversizedCount = 0;
|
|
46
68
|
for (const item of matches.slice(offset, offset + PAGE_SIZE)) {
|
|
@@ -54,7 +76,7 @@ export function selectReasoningKnowledge(bundle, repository, { query = '', offse
|
|
|
54
76
|
modules, availableCount: eligible.length, matchedCount: matches.length,
|
|
55
77
|
omittedCount: eligible.length - modules.length,
|
|
56
78
|
nextOffset: next < matches.length && consumed ? next : null,
|
|
57
|
-
retrieval: { policyVersion: 'sqm-reasoning-recall.
|
|
79
|
+
retrieval: { policyVersion: 'sqm-reasoning-recall.v5', mode: 'unicode_lexical',
|
|
58
80
|
queryTruncated: String(query).length > queryText.length, contextCharacters: characters,
|
|
59
81
|
oversizedCount,
|
|
60
82
|
incomplete: matches.length > modules.length || queryText.length < String(query).length,
|