@asobacloud/nehanda 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE.md +57 -0
  3. package/README.md +404 -2
  4. package/bin/agent.mjs +1260 -0
  5. package/bin/nehanda-ui.mjs +824 -0
  6. package/lib/anthropicHeaders.mjs +8 -0
  7. package/lib/app.mjs +313 -0
  8. package/lib/auth.mjs +255 -0
  9. package/lib/bashguard.mjs +159 -0
  10. package/lib/compact.mjs +306 -0
  11. package/lib/energy-middleware.mjs +318 -0
  12. package/lib/hookShapeOrder.mjs +30 -0
  13. package/lib/hookplane.mjs +228 -0
  14. package/lib/instructions.mjs +23 -0
  15. package/lib/jev_read.mjs +216 -0
  16. package/lib/jev_write.mjs +253 -0
  17. package/lib/laya.mjs +296 -0
  18. package/lib/laya_sidecar.mjs +198 -0
  19. package/lib/mcp-config.mjs +189 -0
  20. package/lib/mcp.mjs +169 -0
  21. package/lib/modelConfig.mjs +152 -0
  22. package/lib/modelDiscovery.mjs +205 -0
  23. package/lib/openaiCompat.mjs +147 -0
  24. package/lib/orchestrate.mjs +1163 -0
  25. package/lib/paths.mjs +20 -0
  26. package/lib/permissions.mjs +44 -0
  27. package/lib/scripts/__pycache__/laya_worker.cpython-314.pyc +0 -0
  28. package/lib/scripts/audit-code-integrity.py +437 -0
  29. package/lib/scripts/ensure-laya-env.sh +55 -0
  30. package/lib/scripts/js-safety-checker.cjs +308 -0
  31. package/lib/scripts/laya_worker.py +171 -0
  32. package/lib/scripts/odse-transform.py +97 -0
  33. package/lib/scripts/python-safety-checker.py +197 -0
  34. package/lib/scripts/shell-safety-checker.sh +306 -0
  35. package/lib/settings.mjs +147 -0
  36. package/lib/store.mjs +278 -0
  37. package/lib/team.mjs +69 -0
  38. package/lib/testgen.mjs +146 -0
  39. package/lib/tools/AuditCodeIntegrity.json +30 -0
  40. package/lib/tools/JsSafetyChecker.json +29 -0
  41. package/lib/tools/PythonSafetyChecker.json +29 -0
  42. package/lib/tools/ShellSafetyChecker.json +29 -0
  43. package/lib/tools.mjs +907 -0
  44. package/lib/transcript.mjs +223 -0
  45. package/lib/ui.mjs +286 -0
  46. package/lib/workflow.mjs +91 -0
  47. package/lib/worktree.mjs +56 -0
  48. package/package.json +50 -4
  49. package/schema.sql +181 -0
  50. package/scripts/postinstall.mjs +62 -0
  51. package/scripts/sdlc-acceptance.sh +477 -0
  52. package/scripts/verify-sdlc-hook-order.mjs +26 -0
  53. package/templates/test_e2e.sh +32 -0
  54. package/templates/test_file.sh +14 -0
  55. package/templates/test_hook.sh +27 -0
  56. package/templates/test_http.sh +17 -0
  57. package/templates/test_phase.sh +23 -0
  58. package/templates/test_process.sh +14 -0
  59. package/templates/test_tool.sh +24 -0
  60. package/templates/validate_test.sh +55 -0
@@ -0,0 +1,216 @@
1
+ import {
2
+ routeQuery,
3
+ assessEvidence,
4
+ scoreCandidate,
5
+ truncateForLaya,
6
+ } from './laya.mjs'
7
+
8
+ /** Activation threshold θ_act for a relation view. */
9
+ export const THETA_ACT = 0.45
10
+ /** Sufficiency / continue thresholds. */
11
+ export const THETA_SUFF = 0.6
12
+ export const THETA_CONT = 0.45
13
+ /** Total graph expansion budget B and per-round beam W. */
14
+ export const DEFAULT_BUDGET = 12
15
+ export const BEAM_W = 4
16
+ export const MAX_DEPTH = 3
17
+ export const TOP_K_EVIDENCE = 6
18
+
19
+ /**
20
+ * Reciprocal rank fusion of lexical (FTS/LIKE) rankings — vector index optional later.
21
+ */
22
+ export function rrfAnchors(db, query, { sessionId, conversationId, limit = 8 } = {}) {
23
+ const tokens = String(query).toLowerCase().split(/[^a-z0-9_./-]+/).filter(t => t.length > 2).slice(0, 12)
24
+ const ranks = new Map() // id -> { node, rankSum }
25
+
26
+ const bump = (id, node, rank) => {
27
+ const prev = ranks.get(id) || { node, score: 0 }
28
+ prev.score += 1 / (60 + rank)
29
+ prev.node = node
30
+ ranks.set(id, prev)
31
+ }
32
+
33
+ // Lexical list via FTS
34
+ let ftsRows = []
35
+ try {
36
+ const q = tokens.join(' OR ')
37
+ if (q) {
38
+ ftsRows = db.prepare(`
39
+ SELECT m.* FROM memories m
40
+ JOIN memories_fts f ON f.rowid = m.rowid
41
+ WHERE f MATCH ?
42
+ LIMIT ?
43
+ `).all(q, limit * 2)
44
+ }
45
+ } catch { ftsRows = [] }
46
+
47
+ ftsRows.forEach((n, i) => bump(n.id, n, i + 1))
48
+
49
+ // Recency list as second "ranking"
50
+ let recent = []
51
+ try {
52
+ recent = db.prepare(`
53
+ SELECT * FROM memories
54
+ WHERE (? IS NULL OR session_id = ? OR conversation_id = ?)
55
+ ORDER BY observation_ts DESC
56
+ LIMIT ?
57
+ `).all(sessionId || null, sessionId || null, conversationId || null, limit * 2)
58
+ } catch { recent = [] }
59
+ recent.forEach((n, i) => bump(n.id, n, i + 1))
60
+
61
+ // Keyword LIKE fallback if still empty
62
+ if (!ranks.size && tokens.length) {
63
+ const like = `%${tokens[0]}%`
64
+ const rows = db.prepare(`
65
+ SELECT * FROM memories WHERE content LIKE ? OR title LIKE ? LIMIT ?
66
+ `).all(like, like, limit)
67
+ rows.forEach((n, i) => bump(n.id, n, i + 1))
68
+ }
69
+
70
+ return [...ranks.values()].sort((a, b) => b.score - a.score).slice(0, limit)
71
+ }
72
+
73
+ function neighbors(db, nodeId, relation) {
74
+ return db.prepare(`
75
+ SELECT e.*, m.* FROM memory_edges e
76
+ JOIN memories m ON m.id = CASE WHEN e.src_id = ? THEN e.dst_id ELSE e.src_id END
77
+ WHERE (e.src_id = ? OR e.dst_id = ?) AND e.relation = ?
78
+ ORDER BY e.weight DESC
79
+ LIMIT 20
80
+ `).all(nodeId, nodeId, nodeId, relation)
81
+ }
82
+
83
+ /**
84
+ * Adaptive System-1 retrieval closed loop.
85
+ * Returns { evidence, routing, rounds, stoppedReason }.
86
+ */
87
+ export async function retrieveEvidence(db, query, {
88
+ sessionId = null,
89
+ conversationId = null,
90
+ budget = DEFAULT_BUDGET,
91
+ layaOpts = {},
92
+ topK = TOP_K_EVIDENCE,
93
+ } = {}) {
94
+ const routing = await routeQuery(query, layaOpts)
95
+ const graphs = ['semantic', 'temporal', 'causal', 'entity']
96
+ const active = graphs.filter(g => (routing[g] ?? 0) >= THETA_ACT)
97
+ if (!active.length) active.push('semantic', 'temporal')
98
+
99
+ // Softmax-ish budget weights
100
+ const gamma = 1.5
101
+ const raw = active.map(g => Math.pow(Math.max(routing[g], 1e-3), gamma))
102
+ const sum = raw.reduce((a, b) => a + b, 0) || 1
103
+ const minShare = 1
104
+ let remaining = Math.max(0, budget - minShare * active.length)
105
+ const budgets = {}
106
+ active.forEach((g, i) => {
107
+ budgets[g] = minShare + Math.round(remaining * (raw[i] / sum))
108
+ })
109
+
110
+ const depthMax = Math.min(MAX_DEPTH, Math.max(1, Math.ceil(MAX_DEPTH * (routing.multi_hop || 0.3))))
111
+
112
+ const anchors = rrfAnchors(db, query, { sessionId, conversationId, limit: 8 })
113
+ const visited = new Set()
114
+ const evidence = [] // { node, score }
115
+ let frontier = anchors.map(a => ({ node: a.node, score: a.score, via: 'anchor' }))
116
+
117
+ for (const f of frontier) {
118
+ if (visited.has(f.node.id)) continue
119
+ visited.add(f.node.id)
120
+ evidence.push(f)
121
+ }
122
+
123
+ let stoppedReason = 'budget_exhausted'
124
+ let rounds = 0
125
+
126
+ for (let d = 0; d < depthMax; d++) {
127
+ rounds++
128
+ const evidenceText = truncateForLaya(
129
+ `Query: ${query}\n\nEvidence:\n` +
130
+ evidence.slice(0, 8).map(e => `- ${e.node.title}: ${String(e.node.content || '').slice(0, 120)}`).join('\n'),
131
+ )
132
+ const assessment = await assessEvidence(evidenceText, layaOpts)
133
+
134
+ if (assessment.sufficient >= THETA_SUFF &&
135
+ assessment.missing_required < THETA_CONT &&
136
+ assessment.contradiction < THETA_CONT) {
137
+ stoppedReason = 'sufficient'
138
+ break
139
+ }
140
+ if (assessment.further_utility < THETA_CONT) {
141
+ stoppedReason = 'low_utility'
142
+ break
143
+ }
144
+
145
+ const candidates = []
146
+ for (const g of active) {
147
+ if ((budgets[g] || 0) <= 0) continue
148
+ for (const e of evidence.slice(-BEAM_W * 2)) {
149
+ const neigh = neighbors(db, e.node.id, g)
150
+ for (const n of neigh) {
151
+ if (visited.has(n.id)) continue
152
+ candidates.push({ node: n, via: g, edgeWeight: n.weight ?? 0.5 })
153
+ }
154
+ }
155
+ }
156
+
157
+ if (!candidates.length) {
158
+ stoppedReason = 'no_candidates'
159
+ break
160
+ }
161
+
162
+ // Score candidates (bounded)
163
+ const scored = []
164
+ for (const c of candidates.slice(0, budget)) {
165
+ const ctext = truncateForLaya(
166
+ `Query: ${query}\nCandidate (${c.via}): ${c.node.title}\n${String(c.node.content || '').slice(0, 300)}`,
167
+ )
168
+ const sc = await scoreCandidate(ctext, layaOpts)
169
+ const s =
170
+ 0.35 * sc.relevance +
171
+ 0.2 * sc.relation_useful * (routing[c.via] || 0.5) +
172
+ 0.2 * sc.novelty +
173
+ 0.15 * sc.supports_evidence +
174
+ 0.1 * (c.edgeWeight || 0)
175
+ scored.push({ node: c.node, score: s, via: c.via })
176
+ budgets[c.via] = (budgets[c.via] || 0) - 1
177
+ }
178
+
179
+ scored.sort((a, b) => b.score - a.score)
180
+ const next = scored.slice(0, BEAM_W)
181
+ if (!next.length) {
182
+ stoppedReason = 'no_candidates'
183
+ break
184
+ }
185
+ for (const n of next) {
186
+ visited.add(n.node.id)
187
+ evidence.push(n)
188
+ }
189
+ }
190
+
191
+ evidence.sort((a, b) => b.score - a.score)
192
+ const top = evidence.slice(0, topK)
193
+
194
+ // Touch access stats
195
+ for (const e of top) {
196
+ try {
197
+ db.prepare(`
198
+ UPDATE memories SET access_count = COALESCE(access_count,0)+1, last_accessed = ? WHERE id = ?
199
+ `).run(Date.now(), e.node.id)
200
+ } catch { /* */ }
201
+ }
202
+
203
+ return {
204
+ evidence: top.map(e => ({
205
+ id: e.node.id,
206
+ title: e.node.title,
207
+ content: e.node.content,
208
+ score: e.score,
209
+ via: e.via,
210
+ })),
211
+ routing,
212
+ rounds,
213
+ stoppedReason,
214
+ activeGraphs: active,
215
+ }
216
+ }
@@ -0,0 +1,253 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import {
3
+ typeMemoryObservation,
4
+ judgeRelations,
5
+ extractCanonicalState,
6
+ truncateForLaya,
7
+ } from './laya.mjs'
8
+ import {
9
+ upsertMemoryNode,
10
+ upsertMemoryEdge,
11
+ upsertLayaJevState,
12
+ withTransaction,
13
+ } from './store.mjs'
14
+
15
+ /** Max candidates for System-1 relation judgment (paper K_w). */
16
+ export const KW_CANDIDATES = 8
17
+ /** Edge insertion threshold θ_rel. */
18
+ export const THETA_REL = 0.55
19
+
20
+ /**
21
+ * Deterministic candidate discovery: lexical overlap + shared entities + temporal proximity.
22
+ * Does NOT call Laya — keeps controller cost O(K_w), not O(|V|).
23
+ */
24
+ export function discoverCandidates(db, node, limit = KW_CANDIDATES) {
25
+ const sessionId = node.session_id
26
+ const entities = parseEntities(node.entities_json)
27
+ const entitySet = new Set(entities.map(e => String(e).toLowerCase()))
28
+ const tokens = tokenize(node.content || node.title || '')
29
+
30
+ let rows = []
31
+ try {
32
+ // Prefer FTS when available
33
+ const q = tokens.slice(0, 8).join(' OR ')
34
+ if (q) {
35
+ rows = db.prepare(`
36
+ SELECT m.* FROM memories m
37
+ JOIN memories_fts f ON f.rowid = m.rowid
38
+ WHERE f MATCH ? AND m.id != ?
39
+ LIMIT ?
40
+ `).all(q, node.id, limit * 3)
41
+ }
42
+ } catch {
43
+ rows = []
44
+ }
45
+
46
+ if (!rows.length) {
47
+ rows = db.prepare(`
48
+ SELECT * FROM memories
49
+ WHERE id != ? AND (session_id = ? OR conversation_id = ?)
50
+ ORDER BY observation_ts DESC
51
+ LIMIT ?
52
+ `).all(node.id, sessionId, node.conversation_id, limit * 4)
53
+ }
54
+
55
+ const scored = rows.map(u => {
56
+ let s = 0
57
+ const uEnt = parseEntities(u.entities_json).map(e => String(e).toLowerCase())
58
+ for (const e of uEnt) if (entitySet.has(e)) s += 2
59
+ const uTok = new Set(tokenize(u.content || u.title || ''))
60
+ let overlap = 0
61
+ for (const t of tokens) if (uTok.has(t)) overlap++
62
+ s += Math.min(overlap, 10) * 0.3
63
+ if (node.observation_ts && u.observation_ts) {
64
+ const dt = Math.abs(node.observation_ts - u.observation_ts)
65
+ if (dt < 60_000) s += 1.5
66
+ else if (dt < 600_000) s += 0.5
67
+ }
68
+ return { node: u, score: s }
69
+ })
70
+
71
+ scored.sort((a, b) => b.score - a.score)
72
+ return scored.slice(0, limit).map(x => x.node)
73
+ }
74
+
75
+ function tokenize(text) {
76
+ return String(text).toLowerCase().split(/[^a-z0-9_./-]+/).filter(t => t.length > 2).slice(0, 40)
77
+ }
78
+
79
+ function parseEntities(raw) {
80
+ if (!raw) return []
81
+ if (Array.isArray(raw)) return raw
82
+ try {
83
+ const v = JSON.parse(raw)
84
+ return Array.isArray(v) ? v : []
85
+ } catch {
86
+ return []
87
+ }
88
+ }
89
+
90
+ /** Extract path-like entities from observation text / tool metadata. */
91
+ export function extractEntities(text, extra = []) {
92
+ const found = new Set(extra.filter(Boolean))
93
+ const re = /(?:^|[\s`"'(])((?:\.\/|\/)?(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\.[A-Za-z0-9]+)/g
94
+ const s = String(text || '')
95
+ let m
96
+ while ((m = re.exec(s))) found.add(m[1])
97
+ return [...found]
98
+ }
99
+
100
+ /**
101
+ * Write path: observation → type → candidates → relations → memory plane + session state.
102
+ * Observations are always preserved (no discard-on-write).
103
+ */
104
+ export async function ingestObservation(db, {
105
+ sessionId,
106
+ conversationId,
107
+ text,
108
+ title,
109
+ provenance = 'transcript',
110
+ sourceEntryId = null,
111
+ filePaths = [],
112
+ observationTs = Date.now(),
113
+ layaOpts = {},
114
+ updateSessionState = true,
115
+ }) {
116
+ const content = String(text || '')
117
+ const entities = extractEntities(content, filePaths)
118
+ const typeScores = await typeMemoryObservation(truncateForLaya(content), layaOpts)
119
+
120
+ const id = randomUUID()
121
+ const node = {
122
+ id,
123
+ type: 'jev_observation',
124
+ title: title || content.slice(0, 80),
125
+ content,
126
+ keywords: entities.join(','),
127
+ session_id: sessionId,
128
+ conversation_id: conversationId,
129
+ provenance,
130
+ entities_json: entities,
131
+ t_episodic: typeScores.t_episodic,
132
+ t_semantic: typeScores.t_semantic,
133
+ t_procedural: typeScores.t_procedural,
134
+ t_preference: typeScores.t_preference,
135
+ source_entry_id: sourceEntryId,
136
+ observation_ts: observationTs,
137
+ created_at: observationTs,
138
+ updated_at: observationTs,
139
+ }
140
+
141
+ withTransaction(db, () => {
142
+ upsertMemoryNode(db, node)
143
+ })
144
+
145
+ const candidates = discoverCandidates(db, { ...node, entities_json: JSON.stringify(entities) })
146
+
147
+ // Structural temporal edges (no Laya): newer → older as temporal with weight 1
148
+ for (const u of candidates) {
149
+ if (node.observation_ts && u.observation_ts && node.observation_ts !== u.observation_ts) {
150
+ const [src, dst] = node.observation_ts >= u.observation_ts ? [node.id, u.id] : [u.id, node.id]
151
+ upsertMemoryEdge(db, {
152
+ src_id: src,
153
+ dst_id: dst,
154
+ relation: 'temporal',
155
+ weight: 1,
156
+ meta_json: { kind: 'timestamp_order' },
157
+ })
158
+ }
159
+ // Exact shared entity → entity edge without Laya
160
+ const uEnt = new Set(parseEntities(u.entities_json).map(e => e.toLowerCase()))
161
+ const shared = entities.filter(e => uEnt.has(String(e).toLowerCase()))
162
+ if (shared.length) {
163
+ upsertMemoryEdge(db, {
164
+ src_id: node.id,
165
+ dst_id: u.id,
166
+ relation: 'entity',
167
+ weight: Math.min(1, 0.5 + shared.length * 0.2),
168
+ meta_json: { shared },
169
+ })
170
+ }
171
+ }
172
+
173
+ // Laya relation judgments for TopK pairs (semantic / causal); entity/same_episode when needed
174
+ for (const u of candidates) {
175
+ const pair = truncateForLaya(
176
+ `Memory A:\n${content.slice(0, 400)}\n\nMemory B:\n${String(u.content || '').slice(0, 400)}`,
177
+ )
178
+ const rel = await judgeRelations(pair, layaOpts)
179
+ if (rel.semantic >= THETA_REL) {
180
+ upsertMemoryEdge(db, { src_id: node.id, dst_id: u.id, relation: 'semantic', weight: rel.semantic })
181
+ }
182
+ if (rel.causal >= THETA_REL) {
183
+ upsertMemoryEdge(db, { src_id: node.id, dst_id: u.id, relation: 'causal', weight: rel.causal })
184
+ }
185
+ // entity via Laya only if we didn't already create a structural entity edge
186
+ if (rel.entity >= THETA_REL) {
187
+ upsertMemoryEdge(db, { src_id: node.id, dst_id: u.id, relation: 'entity', weight: rel.entity })
188
+ }
189
+ }
190
+
191
+ let sessionState = null
192
+ if (updateSessionState) {
193
+ const extracted = await extractCanonicalState(content, filePaths, layaOpts)
194
+ sessionState = upsertLayaJevState(db, sessionId, extracted)
195
+ }
196
+
197
+ return { node: db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id), sessionState, candidateCount: candidates.length }
198
+ }
199
+
200
+ /**
201
+ * Extract recent file paths from transcript tool uses (Read/Edit/Write).
202
+ */
203
+ export function extractRecentFilePaths(db, sessionId, limit = 20) {
204
+ const rows = db.prepare(`
205
+ SELECT entry_type, payload_json FROM transcript_entries
206
+ WHERE session_id = ? ORDER BY sequence ASC
207
+ `).all(sessionId)
208
+
209
+ const paths = []
210
+ for (const row of rows) {
211
+ try {
212
+ const payload = JSON.parse(row.payload_json)
213
+ if (row.entry_type !== 'assistant') continue
214
+ for (const b of payload.content || []) {
215
+ if (b?.type !== 'tool_use') continue
216
+ if (!['Read', 'Edit', 'Write', 'MultiEdit'].includes(b.name)) continue
217
+ const p = b.input?.path || b.input?.file_path
218
+ if (p) paths.push(p)
219
+ }
220
+ } catch { /* */ }
221
+ }
222
+ return paths.slice(-limit)
223
+ }
224
+
225
+ /**
226
+ * Build a short text tail from recent transcript entries for Laya state extraction.
227
+ */
228
+ export function transcriptTailText(db, sessionId, maxChars = 1200) {
229
+ const rows = db.prepare(`
230
+ SELECT entry_type, payload_json FROM transcript_entries
231
+ WHERE session_id = ? ORDER BY sequence DESC LIMIT 30
232
+ `).all(sessionId)
233
+
234
+ const parts = []
235
+ for (const row of rows) {
236
+ try {
237
+ const p = JSON.parse(row.payload_json)
238
+ if (row.entry_type === 'user') {
239
+ const t = (p.content || []).filter(b => b.type === 'text').map(b => b.text).join('\n')
240
+ if (t) parts.push(`User: ${t}`)
241
+ } else if (row.entry_type === 'assistant') {
242
+ const t = (p.content || []).filter(b => b.type === 'text').map(b => b.text).join('\n')
243
+ if (t) parts.push(`Assistant: ${t.slice(0, 200)}`)
244
+ } else if (row.entry_type === 'tool_result') {
245
+ const c = typeof p.content === 'string' ? p.content : JSON.stringify(p.content ?? '')
246
+ const flag = p.is_error ? 'ERROR' : 'ok'
247
+ parts.push(`Tool(${flag}): ${c.slice(0, 200)}`)
248
+ }
249
+ } catch { /* */ }
250
+ }
251
+ parts.reverse()
252
+ return truncateForLaya(parts.join('\n'), maxChars)
253
+ }