@try-works/dsh-memory 0.1.18 → 0.1.20

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 (51) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +585 -585
  3. package/STATUS.md +287 -287
  4. package/cordis.patch.yml +69 -69
  5. package/package.json +1 -1
  6. package/scripts/check-package.mjs +76 -76
  7. package/src/SYSTEM.template.md +311 -311
  8. package/src/capture.js +242 -242
  9. package/src/client/index.js +886 -886
  10. package/src/config.js +57 -57
  11. package/src/consolidate-run.js +132 -132
  12. package/src/consolidate.js +330 -330
  13. package/src/context.js +88 -88
  14. package/src/decisions.js +106 -106
  15. package/src/defaults.js +93 -93
  16. package/src/digest.js +255 -255
  17. package/src/entry.js +179 -179
  18. package/src/eval.js +72 -72
  19. package/src/files.js +60 -60
  20. package/src/fixture.js +56 -56
  21. package/src/forget.js +71 -71
  22. package/src/freshness.js +65 -65
  23. package/src/fts.js +166 -166
  24. package/src/git.js +53 -53
  25. package/src/history.js +226 -226
  26. package/src/host.js +866 -857
  27. package/src/index.js +115 -94
  28. package/src/inject.js +108 -108
  29. package/src/layers.js +178 -178
  30. package/src/ledger.js +19 -19
  31. package/src/map.js +184 -184
  32. package/src/models.js +136 -136
  33. package/src/queue.js +602 -602
  34. package/src/rank.js +31 -31
  35. package/src/recover.js +91 -91
  36. package/src/refresh.js +80 -80
  37. package/src/remember.js +58 -58
  38. package/src/rerank.js +90 -90
  39. package/src/search.js +75 -75
  40. package/src/select.js +57 -57
  41. package/src/session.js +60 -60
  42. package/src/state.js +240 -240
  43. package/src/store.js +128 -128
  44. package/src/summarize.js +195 -195
  45. package/src/system-doc.js +171 -171
  46. package/src/tasks.js +617 -617
  47. package/src/telemetry.js +122 -122
  48. package/src/tools.js +828 -818
  49. package/src/working.js +288 -288
  50. package/src/workspace-key.js +68 -68
  51. package/src/workspace.js +87 -87
package/src/layers.js CHANGED
@@ -1,178 +1,178 @@
1
- // Layered storage: semantic/episodic/procedural/learnings are INDEX files that point at
2
- // one file per topic inside a sibling directory. A single append-only file per layer grows
3
- // without bound and becomes hard to read, diff and search; one file per topic keeps every
4
- // file small and greppable, and the index is regenerated from the topic files so it can
5
- // never drift from them.
6
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
7
- import { join } from 'node:path'
8
- import { layerDir } from './store.js'
9
- import { LAYER_DIRS, LAYERED, LAYER_FILES, layersForScope, parseEntries } from './entry.js'
10
- import { slugify } from './workspace-key.js'
11
-
12
- // Re-exported so callers can reach the layer vocabulary from either module.
13
- export { LAYER_DIRS, LAYERED }
14
- /** True when this layer stores its entries as per-topic files. */
15
- export function isLayered(layer) { return Object.hasOwn(LAYER_DIRS, layer) }
16
-
17
- /** Display title for a scope, matching the layer templates. */
18
- export function scopeTitle(scope, key) { return scope === 'global' ? 'Global' : (key ?? 'workspace') }
19
-
20
- export function layerTopicsDir(root, scope, key, layer) { return join(layerDir(root, scope, key), LAYER_DIRS[layer]) }
21
- export function layerIndexPath(root, scope, key, layer) { return join(layerDir(root, scope, key), LAYER_FILES[layer]) }
22
-
23
- /** Filesystem-safe slug for a topic heading; never empty. */
24
- export function topicSlug(topic) { return slugify(topic) || 'untitled' }
25
-
26
- /** The file one topic's entries live in. */
27
- export function topicFilePath(root, scope, key, layer, topic) {
28
- return join(layerTopicsDir(root, scope, key, layer), topicSlug(topic) + '.md')
29
- }
30
-
31
- /** Heading of a topic file, so a generated file explains itself. */
32
- export function topicFileTitle(scope, key, layer, topic) {
33
- return '# ' + scopeTitle(scope, key) + ' \u2014 ' + (LAYER_FILES[layer] ?? layer).replace(/\.md$/, '') + ': ' + topic
34
- }
35
-
36
- /**
37
- * Read every topic file of one layer.
38
- * @returns {{slug: string, file: string, text: string, entries: object[]}[]} sorted by slug.
39
- */
40
- export function readLayerTopics(root, scope, key, layer) {
41
- const dir = layerTopicsDir(root, scope, key, layer)
42
- if (!existsSync(dir)) return []
43
- return readdirSync(dir)
44
- .filter((name) => name.endsWith('.md'))
45
- .sort()
46
- .map((name) => {
47
- const file = join(dir, name)
48
- const text = readFileSync(file, 'utf8')
49
- return { slug: name.replace(/\.md$/, ''), file, text, entries: parseEntries(text) }
50
- })
51
- }
52
-
53
- /** Concatenated text of a layer, for id allocation across every topic file. */
54
- export function layerText(root, scope, key, layer) {
55
- return readLayerTopics(root, scope, key, layer).map((topic) => topic.text).join('\n')
56
- }
57
-
58
- /** The relative link target from the layer index to a topic file. */
59
- function topicLink(layer, slug) { return LAYER_DIRS[layer] + '/' + slug + '.md' }
60
-
61
- /** Render a layer index: a topic map pointing at the per-topic files. */
62
- export function renderLayerIndex(scope, key, layer, topics) {
63
- const title = '# ' + scopeTitle(scope, key) + ' \u2014 ' + (LAYER_FILES[layer] ?? layer).replace(/\.md$/, '')
64
- const intro = 'Topic index. Entries live in one file per topic under ' + LAYER_DIRS[layer] + '/.'
65
- if (topics.length === 0) return [title, '', intro, '', '(no topics yet)', ''].join('\n')
66
- const rows = topics.map((topic) => {
67
- // A history topic IS a day, so the day is the useful label; elsewhere the first
68
- // entry heading names the topic better than its slug.
69
- const heading = layer === 'history' ? topic.slug : (topic.entries[0] ? topic.entries[0].heading : topic.slug)
70
- const count = topic.entries.length
71
- const updated = topic.entries.reduce((latest, entry) => {
72
- const stamp = entry.updated ?? entry.date ?? ''
73
- return stamp > latest ? stamp : latest
74
- }, '')
75
- const when = updated ? ', updated ' + String(updated).slice(0, 10) : ''
76
- return '- [' + heading + '](' + topicLink(layer, topic.slug) + ') \u2014 ' + count
77
- + (count === 1 ? ' entry' : ' entries') + when
78
- })
79
- return [title, '', intro, '', ...rows, ''].join('\n')
80
- }
81
-
82
- /** Regenerate one layer's index from its topic files. */
83
- export function rebuildLayerIndex(root, scope, key, layer) {
84
- if (!isLayered(layer)) return
85
- const topics = readLayerTopics(root, scope, key, layer)
86
- mkdirSync(layerDir(root, scope, key), { recursive: true })
87
- writeFileSync(layerIndexPath(root, scope, key, layer), renderLayerIndex(scope, key, layer, topics), 'utf8')
88
- }
89
-
90
- /** Regenerate every layered index for one scope. */
91
- export function rebuildLayerIndexes(root, scope, key, layers) {
92
- for (const layer of layers ?? layersForScope(scope)) rebuildLayerIndex(root, scope, key, layer)
93
- }
94
-
95
- /** Append an entry block to its topic file, creating the file (and dir) as needed. */
96
- export function appendToTopic(root, scope, key, layer, topic, block) {
97
- mkdirSync(layerTopicsDir(root, scope, key, layer), { recursive: true })
98
- const file = topicFilePath(root, scope, key, layer, topic)
99
- if (!existsSync(file)) {
100
- writeFileSync(file, topicFileTitle(scope, key, layer, topic) + '\n\n' + block + '\n', 'utf8')
101
- return file
102
- }
103
- const existing = readFileSync(file, 'utf8')
104
- const sep = existing.endsWith('\n') ? '' : '\n'
105
- writeFileSync(file, existing + sep + block + '\n', 'utf8')
106
- return file
107
- }
108
-
109
- /** The topic file holding an entry id, or undefined. Searches every layered kind of one scope. */
110
- export function findEntryFile(root, scope, key, id, layers) {
111
- for (const layer of layers ?? layersForScope(scope)) {
112
- for (const topic of readLayerTopics(root, scope, key, layer)) {
113
- if (topic.entries.some((entry) => entry.id === id)) return topic.file
114
- }
115
- }
116
- return undefined
117
- }
118
-
119
- /** Scope and workspace key from a root-relative ledger path. */
120
- export function scopeOfFile(rel) {
121
- const parts = String(rel ?? '').split('/')
122
- if (parts[0] === 'global') return { scope: 'global', key: undefined }
123
- if (parts[0] === 'workspaces' && parts[1]) return { scope: 'workspace', key: parts[1] }
124
- return undefined
125
- }
126
-
127
- /**
128
- * Move entries out of a pre-layering layer file into per-topic files and rebuild the index
129
- * from them. Idempotent: a layer whose index already holds only pointers moves nothing.
130
- * @returns how many entries were moved.
131
- */
132
- export function migrateLayer(root, scope, key, layer) {
133
- const file = layerIndexPath(root, scope, key, layer)
134
- if (!existsSync(file)) return 0
135
- const text = readFileSync(file, 'utf8')
136
- const entries = parseEntries(text)
137
- if (entries.length === 0) return 0
138
- const lines = text.split('\n')
139
- let moved = 0
140
- for (const entry of entries) {
141
- const block = lines.slice(entry.startLine - 1, entry.endLine).join('\n').trimEnd()
142
- if (block === '') continue
143
- appendToTopic(root, scope, key, layer, entry.heading, block)
144
- moved += 1
145
- }
146
- rebuildLayerIndex(root, scope, key, layer)
147
- return moved
148
- }
149
-
150
- /**
151
- * Bring every scope onto layered storage: make sure each layered kind has its directory,
152
- * move any entries still sitting in a single layer file, and rebuild its index.
153
- * Runs on load and is idempotent.
154
- */
155
- export function migrateToLayered(root) {
156
- const wdir = join(root, 'workspaces')
157
- let keys = []
158
- try { keys = readdirSync(wdir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name) } catch { keys = [] }
159
- const scopes = [['global', undefined], ...keys.map((key) => ['workspace', key])]
160
- let moved = 0
161
- for (const [scope, key] of scopes) {
162
- for (const layer of layersForScope(scope)) {
163
- mkdirSync(layerTopicsDir(root, scope, key, layer), { recursive: true })
164
- moved += migrateLayer(root, scope, key, layer)
165
- // A scope that predates this layer still needs its index file. Rebuilding is safe
166
- // here because migrateLayer has already lifted any entries out of it.
167
- if (!existsSync(layerIndexPath(root, scope, key, layer))) rebuildLayerIndex(root, scope, key, layer)
168
- }
169
- }
170
- return moved
171
- }
172
-
173
- /** Every topic file of one scope, for search and the ledger. */
174
- export function listTopicFiles(root, scope, key, layers) {
175
- const out = []
176
- for (const layer of layers ?? layersForScope(scope)) for (const topic of readLayerTopics(root, scope, key, layer)) out.push(topic.file)
177
- return out
178
- }
1
+ // Layered storage: semantic/episodic/procedural/learnings are INDEX files that point at
2
+ // one file per topic inside a sibling directory. A single append-only file per layer grows
3
+ // without bound and becomes hard to read, diff and search; one file per topic keeps every
4
+ // file small and greppable, and the index is regenerated from the topic files so it can
5
+ // never drift from them.
6
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
7
+ import { join } from 'node:path'
8
+ import { layerDir } from './store.js'
9
+ import { LAYER_DIRS, LAYERED, LAYER_FILES, layersForScope, parseEntries } from './entry.js'
10
+ import { slugify } from './workspace-key.js'
11
+
12
+ // Re-exported so callers can reach the layer vocabulary from either module.
13
+ export { LAYER_DIRS, LAYERED }
14
+ /** True when this layer stores its entries as per-topic files. */
15
+ export function isLayered(layer) { return Object.hasOwn(LAYER_DIRS, layer) }
16
+
17
+ /** Display title for a scope, matching the layer templates. */
18
+ export function scopeTitle(scope, key) { return scope === 'global' ? 'Global' : (key ?? 'workspace') }
19
+
20
+ export function layerTopicsDir(root, scope, key, layer) { return join(layerDir(root, scope, key), LAYER_DIRS[layer]) }
21
+ export function layerIndexPath(root, scope, key, layer) { return join(layerDir(root, scope, key), LAYER_FILES[layer]) }
22
+
23
+ /** Filesystem-safe slug for a topic heading; never empty. */
24
+ export function topicSlug(topic) { return slugify(topic) || 'untitled' }
25
+
26
+ /** The file one topic's entries live in. */
27
+ export function topicFilePath(root, scope, key, layer, topic) {
28
+ return join(layerTopicsDir(root, scope, key, layer), topicSlug(topic) + '.md')
29
+ }
30
+
31
+ /** Heading of a topic file, so a generated file explains itself. */
32
+ export function topicFileTitle(scope, key, layer, topic) {
33
+ return '# ' + scopeTitle(scope, key) + ' \u2014 ' + (LAYER_FILES[layer] ?? layer).replace(/\.md$/, '') + ': ' + topic
34
+ }
35
+
36
+ /**
37
+ * Read every topic file of one layer.
38
+ * @returns {{slug: string, file: string, text: string, entries: object[]}[]} sorted by slug.
39
+ */
40
+ export function readLayerTopics(root, scope, key, layer) {
41
+ const dir = layerTopicsDir(root, scope, key, layer)
42
+ if (!existsSync(dir)) return []
43
+ return readdirSync(dir)
44
+ .filter((name) => name.endsWith('.md'))
45
+ .sort()
46
+ .map((name) => {
47
+ const file = join(dir, name)
48
+ const text = readFileSync(file, 'utf8')
49
+ return { slug: name.replace(/\.md$/, ''), file, text, entries: parseEntries(text) }
50
+ })
51
+ }
52
+
53
+ /** Concatenated text of a layer, for id allocation across every topic file. */
54
+ export function layerText(root, scope, key, layer) {
55
+ return readLayerTopics(root, scope, key, layer).map((topic) => topic.text).join('\n')
56
+ }
57
+
58
+ /** The relative link target from the layer index to a topic file. */
59
+ function topicLink(layer, slug) { return LAYER_DIRS[layer] + '/' + slug + '.md' }
60
+
61
+ /** Render a layer index: a topic map pointing at the per-topic files. */
62
+ export function renderLayerIndex(scope, key, layer, topics) {
63
+ const title = '# ' + scopeTitle(scope, key) + ' \u2014 ' + (LAYER_FILES[layer] ?? layer).replace(/\.md$/, '')
64
+ const intro = 'Topic index. Entries live in one file per topic under ' + LAYER_DIRS[layer] + '/.'
65
+ if (topics.length === 0) return [title, '', intro, '', '(no topics yet)', ''].join('\n')
66
+ const rows = topics.map((topic) => {
67
+ // A history topic IS a day, so the day is the useful label; elsewhere the first
68
+ // entry heading names the topic better than its slug.
69
+ const heading = layer === 'history' ? topic.slug : (topic.entries[0] ? topic.entries[0].heading : topic.slug)
70
+ const count = topic.entries.length
71
+ const updated = topic.entries.reduce((latest, entry) => {
72
+ const stamp = entry.updated ?? entry.date ?? ''
73
+ return stamp > latest ? stamp : latest
74
+ }, '')
75
+ const when = updated ? ', updated ' + String(updated).slice(0, 10) : ''
76
+ return '- [' + heading + '](' + topicLink(layer, topic.slug) + ') \u2014 ' + count
77
+ + (count === 1 ? ' entry' : ' entries') + when
78
+ })
79
+ return [title, '', intro, '', ...rows, ''].join('\n')
80
+ }
81
+
82
+ /** Regenerate one layer's index from its topic files. */
83
+ export function rebuildLayerIndex(root, scope, key, layer) {
84
+ if (!isLayered(layer)) return
85
+ const topics = readLayerTopics(root, scope, key, layer)
86
+ mkdirSync(layerDir(root, scope, key), { recursive: true })
87
+ writeFileSync(layerIndexPath(root, scope, key, layer), renderLayerIndex(scope, key, layer, topics), 'utf8')
88
+ }
89
+
90
+ /** Regenerate every layered index for one scope. */
91
+ export function rebuildLayerIndexes(root, scope, key, layers) {
92
+ for (const layer of layers ?? layersForScope(scope)) rebuildLayerIndex(root, scope, key, layer)
93
+ }
94
+
95
+ /** Append an entry block to its topic file, creating the file (and dir) as needed. */
96
+ export function appendToTopic(root, scope, key, layer, topic, block) {
97
+ mkdirSync(layerTopicsDir(root, scope, key, layer), { recursive: true })
98
+ const file = topicFilePath(root, scope, key, layer, topic)
99
+ if (!existsSync(file)) {
100
+ writeFileSync(file, topicFileTitle(scope, key, layer, topic) + '\n\n' + block + '\n', 'utf8')
101
+ return file
102
+ }
103
+ const existing = readFileSync(file, 'utf8')
104
+ const sep = existing.endsWith('\n') ? '' : '\n'
105
+ writeFileSync(file, existing + sep + block + '\n', 'utf8')
106
+ return file
107
+ }
108
+
109
+ /** The topic file holding an entry id, or undefined. Searches every layered kind of one scope. */
110
+ export function findEntryFile(root, scope, key, id, layers) {
111
+ for (const layer of layers ?? layersForScope(scope)) {
112
+ for (const topic of readLayerTopics(root, scope, key, layer)) {
113
+ if (topic.entries.some((entry) => entry.id === id)) return topic.file
114
+ }
115
+ }
116
+ return undefined
117
+ }
118
+
119
+ /** Scope and workspace key from a root-relative ledger path. */
120
+ export function scopeOfFile(rel) {
121
+ const parts = String(rel ?? '').split('/')
122
+ if (parts[0] === 'global') return { scope: 'global', key: undefined }
123
+ if (parts[0] === 'workspaces' && parts[1]) return { scope: 'workspace', key: parts[1] }
124
+ return undefined
125
+ }
126
+
127
+ /**
128
+ * Move entries out of a pre-layering layer file into per-topic files and rebuild the index
129
+ * from them. Idempotent: a layer whose index already holds only pointers moves nothing.
130
+ * @returns how many entries were moved.
131
+ */
132
+ export function migrateLayer(root, scope, key, layer) {
133
+ const file = layerIndexPath(root, scope, key, layer)
134
+ if (!existsSync(file)) return 0
135
+ const text = readFileSync(file, 'utf8')
136
+ const entries = parseEntries(text)
137
+ if (entries.length === 0) return 0
138
+ const lines = text.split('\n')
139
+ let moved = 0
140
+ for (const entry of entries) {
141
+ const block = lines.slice(entry.startLine - 1, entry.endLine).join('\n').trimEnd()
142
+ if (block === '') continue
143
+ appendToTopic(root, scope, key, layer, entry.heading, block)
144
+ moved += 1
145
+ }
146
+ rebuildLayerIndex(root, scope, key, layer)
147
+ return moved
148
+ }
149
+
150
+ /**
151
+ * Bring every scope onto layered storage: make sure each layered kind has its directory,
152
+ * move any entries still sitting in a single layer file, and rebuild its index.
153
+ * Runs on load and is idempotent.
154
+ */
155
+ export function migrateToLayered(root) {
156
+ const wdir = join(root, 'workspaces')
157
+ let keys = []
158
+ try { keys = readdirSync(wdir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name) } catch { keys = [] }
159
+ const scopes = [['global', undefined], ...keys.map((key) => ['workspace', key])]
160
+ let moved = 0
161
+ for (const [scope, key] of scopes) {
162
+ for (const layer of layersForScope(scope)) {
163
+ mkdirSync(layerTopicsDir(root, scope, key, layer), { recursive: true })
164
+ moved += migrateLayer(root, scope, key, layer)
165
+ // A scope that predates this layer still needs its index file. Rebuilding is safe
166
+ // here because migrateLayer has already lifted any entries out of it.
167
+ if (!existsSync(layerIndexPath(root, scope, key, layer))) rebuildLayerIndex(root, scope, key, layer)
168
+ }
169
+ }
170
+ return moved
171
+ }
172
+
173
+ /** Every topic file of one scope, for search and the ledger. */
174
+ export function listTopicFiles(root, scope, key, layers) {
175
+ const out = []
176
+ for (const layer of layers ?? layersForScope(scope)) for (const topic of readLayerTopics(root, scope, key, layer)) out.push(topic.file)
177
+ return out
178
+ }
package/src/ledger.js CHANGED
@@ -1,19 +1,19 @@
1
- // Derived entry ledger: per-entry metadata (file, layer, line span, importance,
2
- // dates) rebuilt from the markdown in one step. Feeds ranking + freshness.
3
- import { readFileSync } from 'node:fs'
4
- import { relative } from 'node:path'
5
- import { indexedFiles, layerOfFile } from './files.js'
6
- import { parseEntries } from './entry.js'
7
-
8
- export function buildLedger(root) {
9
- const entries = []
10
- for (const file of indexedFiles(root)) {
11
- const rel = relative(root, file).replace(/\\/g, '/')
12
- const layer = layerOfFile(root, file)
13
- if (!layer) continue
14
- for (const e of parseEntries(readFileSync(file, 'utf8'))) {
15
- entries.push({ ...e, file: rel, layer })
16
- }
17
- }
18
- return entries
19
- }
1
+ // Derived entry ledger: per-entry metadata (file, layer, line span, importance,
2
+ // dates) rebuilt from the markdown in one step. Feeds ranking + freshness.
3
+ import { readFileSync } from 'node:fs'
4
+ import { relative } from 'node:path'
5
+ import { indexedFiles, layerOfFile } from './files.js'
6
+ import { parseEntries } from './entry.js'
7
+
8
+ export function buildLedger(root) {
9
+ const entries = []
10
+ for (const file of indexedFiles(root)) {
11
+ const rel = relative(root, file).replace(/\\/g, '/')
12
+ const layer = layerOfFile(root, file)
13
+ if (!layer) continue
14
+ for (const e of parseEntries(readFileSync(file, 'utf8'))) {
15
+ entries.push({ ...e, file: rel, layer })
16
+ }
17
+ }
18
+ return entries
19
+ }