@try-works/dsh-memory 0.1.19 → 0.1.21

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 (55) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +585 -585
  3. package/STATUS.md +294 -287
  4. package/cordis.patch.yml +69 -69
  5. package/package.json +1 -1
  6. package/scripts/check-package.mjs +76 -76
  7. package/scripts/inspect-log.ts +40 -0
  8. package/scripts/repair-ignorable.ts +130 -0
  9. package/scripts/scan-sessions.ts +43 -0
  10. package/scripts/verify-log.ts +26 -0
  11. package/src/SYSTEM.template.md +311 -311
  12. package/src/capture.js +242 -242
  13. package/src/client/index.js +886 -886
  14. package/src/config.js +57 -57
  15. package/src/consolidate-run.js +132 -132
  16. package/src/consolidate.js +330 -330
  17. package/src/context.js +88 -88
  18. package/src/decisions.js +106 -106
  19. package/src/defaults.js +93 -93
  20. package/src/digest.js +255 -255
  21. package/src/entry.js +179 -179
  22. package/src/eval.js +72 -72
  23. package/src/files.js +60 -60
  24. package/src/fixture.js +56 -56
  25. package/src/forget.js +71 -71
  26. package/src/freshness.js +65 -65
  27. package/src/fts.js +166 -166
  28. package/src/git.js +53 -53
  29. package/src/history.js +226 -226
  30. package/src/host.js +884 -866
  31. package/src/index.js +115 -94
  32. package/src/inject.js +108 -108
  33. package/src/layers.js +178 -178
  34. package/src/ledger.js +19 -19
  35. package/src/map.js +184 -184
  36. package/src/models.js +136 -136
  37. package/src/queue.js +602 -602
  38. package/src/rank.js +31 -31
  39. package/src/recover.js +91 -91
  40. package/src/refresh.js +80 -80
  41. package/src/remember.js +58 -58
  42. package/src/rerank.js +90 -90
  43. package/src/search.js +75 -75
  44. package/src/select.js +57 -57
  45. package/src/session.js +60 -60
  46. package/src/state.js +240 -240
  47. package/src/store.js +128 -128
  48. package/src/summarize.js +195 -195
  49. package/src/system-doc.js +171 -171
  50. package/src/tasks.js +617 -617
  51. package/src/telemetry.js +122 -122
  52. package/src/tools.js +828 -818
  53. package/src/working.js +288 -288
  54. package/src/workspace-key.js +68 -68
  55. 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
+ }