@try-works/dsh-memory 0.1.19 → 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.
- package/LICENSE +21 -21
- package/README.md +585 -585
- package/STATUS.md +287 -287
- package/cordis.patch.yml +69 -69
- package/package.json +1 -1
- package/scripts/check-package.mjs +76 -76
- package/src/SYSTEM.template.md +311 -311
- package/src/capture.js +242 -242
- package/src/client/index.js +886 -886
- package/src/config.js +57 -57
- package/src/consolidate-run.js +132 -132
- package/src/consolidate.js +330 -330
- package/src/context.js +88 -88
- package/src/decisions.js +106 -106
- package/src/defaults.js +93 -93
- package/src/digest.js +255 -255
- package/src/entry.js +179 -179
- package/src/eval.js +72 -72
- package/src/files.js +60 -60
- package/src/fixture.js +56 -56
- package/src/forget.js +71 -71
- package/src/freshness.js +65 -65
- package/src/fts.js +166 -166
- package/src/git.js +53 -53
- package/src/history.js +226 -226
- package/src/host.js +866 -866
- package/src/index.js +115 -94
- package/src/inject.js +108 -108
- package/src/layers.js +178 -178
- package/src/ledger.js +19 -19
- package/src/map.js +184 -184
- package/src/models.js +136 -136
- package/src/queue.js +602 -602
- package/src/rank.js +31 -31
- package/src/recover.js +91 -91
- package/src/refresh.js +80 -80
- package/src/remember.js +58 -58
- package/src/rerank.js +90 -90
- package/src/search.js +75 -75
- package/src/select.js +57 -57
- package/src/session.js +60 -60
- package/src/state.js +240 -240
- package/src/store.js +128 -128
- package/src/summarize.js +195 -195
- package/src/system-doc.js +171 -171
- package/src/tasks.js +617 -617
- package/src/telemetry.js +122 -122
- package/src/tools.js +828 -818
- package/src/working.js +288 -288
- package/src/workspace-key.js +68 -68
- package/src/workspace.js +87 -87
package/src/system-doc.js
CHANGED
|
@@ -1,171 +1,171 @@
|
|
|
1
|
-
// The system document: the memory system describing itself, as built.
|
|
2
|
-
//
|
|
3
|
-
// This is not a memory layer - it is not a fact about the user's work, it is the manual for the
|
|
4
|
-
// machinery. It is rendered from SYSTEM.template.md on every boot so it can never drift from the
|
|
5
|
-
// build, and it lives at the store root (committed) so the same description travels with the
|
|
6
|
-
// memory to every machine.
|
|
7
|
-
//
|
|
8
|
-
// It exists because an agent that reads and writes memory needs to understand the intent behind
|
|
9
|
-
// it: which layer a fact belongs in, why writes are deterministic, and what is derived and
|
|
10
|
-
// therefore safe to lose. Without that, memory becomes a junk drawer.
|
|
11
|
-
import { readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
12
|
-
import { dirname, join } from 'node:path'
|
|
13
|
-
import { fileURLToPath } from 'node:url'
|
|
14
|
-
import { LAYER_CODES, LAYER_DIRS, LAYERED, VOLATILITY_DAYS, WORKSPACE_ONLY_LAYERS } from './entry.js'
|
|
15
|
-
|
|
16
|
-
/** Bumped whenever the described behaviour changes, so a stale on-disk copy is detectable. */
|
|
17
|
-
export const SYSTEM_DOC_VERSION = 2
|
|
18
|
-
|
|
19
|
-
/** The template ships beside this module; resolve it relative to the module, not the cwd. */
|
|
20
|
-
export function templatePath() {
|
|
21
|
-
return join(dirname(fileURLToPath(import.meta.url)), 'SYSTEM.template.md')
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Absolute path of the rendered document inside a store. */
|
|
25
|
-
export function systemDocPath(root) { return join(root, 'SYSTEM.md') }
|
|
26
|
-
|
|
27
|
-
/** The layer diagram, kept as data so it cannot break the surrounding code. */
|
|
28
|
-
const DIAGRAM = [
|
|
29
|
-
'global/ workspaces/<key>/',
|
|
30
|
-
' semantic.md semantic.md',
|
|
31
|
-
' semantic/<topic>.md semantic/<topic>.md',
|
|
32
|
-
' episodic.md + episodic/<topic>.md',
|
|
33
|
-
' procedural.md + procedural/<topic>.md',
|
|
34
|
-
' learnings.md + learnings/<topic>.md',
|
|
35
|
-
' working.md working.md (single file, never indexed)',
|
|
36
|
-
' junk.md junk.md (single file, never indexed)',
|
|
37
|
-
' codebase.md + codebase/<topic>.md (workspace only)',
|
|
38
|
-
' history.md + history/<day>.md (workspace only)',
|
|
39
|
-
' state.md state.md (repo state; replaced per section)',
|
|
40
|
-
' working.md working.md (YOUR task/activity head)',
|
|
41
|
-
].join(String.fromCharCode(10))
|
|
42
|
-
|
|
43
|
-
/** Count the markdown files inside one layer directory, or 0 when it does not exist. */
|
|
44
|
-
function countTopics(root, scopeDir, layer) {
|
|
45
|
-
try {
|
|
46
|
-
return readdirSync(join(root, scopeDir, LAYER_DIRS[layer])).filter((n) => n.endsWith('.md')).length
|
|
47
|
-
} catch { return 0 }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Summarise what the store currently holds, so the document reports reality rather than intent.
|
|
52
|
-
* Best-effort: an unreadable scope is reported as absent, never thrown.
|
|
53
|
-
*/
|
|
54
|
-
export function describeStore(root) {
|
|
55
|
-
const scopes = [{ label: 'global', dir: 'global' }]
|
|
56
|
-
try {
|
|
57
|
-
for (const name of readdirSync(join(root, 'workspaces'))) scopes.push({ label: name, dir: join('workspaces', name) })
|
|
58
|
-
} catch {}
|
|
59
|
-
return scopes.map((scope) => {
|
|
60
|
-
const counts = {}
|
|
61
|
-
for (const layer of LAYERED) counts[layer] = countTopics(root, scope.dir, layer)
|
|
62
|
-
return { scope: scope.label, counts, total: Object.values(counts).reduce((a, b) => a + b, 0) }
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Format a millisecond cadence for humans: 3600000 -> '1h'. */
|
|
67
|
-
export function humanDuration(ms) {
|
|
68
|
-
const n = Number(ms)
|
|
69
|
-
if (!Number.isFinite(n) || n <= 0) return String(ms)
|
|
70
|
-
if (n % 86400000 === 0) return (n / 86400000) + 'd'
|
|
71
|
-
if (n % 3600000 === 0) return (n / 3600000) + 'h'
|
|
72
|
-
if (n % 60000 === 0) return (n / 60000) + 'm'
|
|
73
|
-
if (n % 1000 === 0) return (n / 1000) + 's'
|
|
74
|
-
return n + 'ms'
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** What each layer is for, keyed by layer so the table cannot drift from the vocabulary. */
|
|
78
|
-
const LAYER_PURPOSE = {
|
|
79
|
-
semantic: 'Durable facts that stay true: how something works, what was decided, environment facts, versions, where things live.',
|
|
80
|
-
episodic: 'What happened: an incident, a migration, a change made on a date and why.',
|
|
81
|
-
procedural: 'How to do something: runbooks, commands, build and test steps, order of operations.',
|
|
82
|
-
learning: 'A lesson from experience - what you wish you had known an hour earlier.',
|
|
83
|
-
codebase: 'What a module, file or subsystem of THIS repository does. Workspace only.',
|
|
84
|
-
history: 'How the repository changed: one entry per commit, grouped by day. Workspace only.',
|
|
85
|
-
decision: 'A settled choice and why it won: dependencies, conventions, architecture. Available in both scopes.',
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* The per-layer table, generated from the layer vocabulary so a newly added layer appears here
|
|
90
|
-
* automatically. A layer with no purpose recorded still gets a row, flagged rather than omitted.
|
|
91
|
-
*/
|
|
92
|
-
function layerTable() {
|
|
93
|
-
const rows = ['| Layer | Code | Use it for |', '| --- | --- | --- |']
|
|
94
|
-
for (const layer of LAYERED) {
|
|
95
|
-
const purpose = LAYER_PURPOSE[layer] ?? '(undocumented)'
|
|
96
|
-
rows.push('| `' + layer + '` | `' + (LAYER_CODES[layer] ?? layer) + '` | ' + purpose + ' |')
|
|
97
|
-
}
|
|
98
|
-
return rows.join(String.fromCharCode(10))
|
|
99
|
-
}
|
|
100
|
-
/** The current-state table, so the document shows what this store actually holds. */
|
|
101
|
-
function storeState(root) {
|
|
102
|
-
if (!root) return '(store root unknown)'
|
|
103
|
-
const rows = describeStore(root)
|
|
104
|
-
const layers = LAYERED
|
|
105
|
-
const lines = [
|
|
106
|
-
'| Scope | ' + layers.join(' | ') + ' | total |',
|
|
107
|
-
'| --- | ' + layers.map(() => '---').join(' | ') + ' | --- |',
|
|
108
|
-
]
|
|
109
|
-
for (const row of rows) {
|
|
110
|
-
lines.push('| ' + row.scope + ' | ' + layers.map((l) => row.counts[l]).join(' | ') + ' | ' + row.total + ' |')
|
|
111
|
-
}
|
|
112
|
-
return lines.join(String.fromCharCode(10))
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Render the system document by filling the template.
|
|
117
|
-
* @param options.cfg - resolved plugin config, quoted so the document matches this build.
|
|
118
|
-
* @param options.version - plugin version.
|
|
119
|
-
* @param options.store - store root, used for the current-state table.
|
|
120
|
-
* @param options.now - ISO timestamp.
|
|
121
|
-
* @returns the markdown document.
|
|
122
|
-
*/
|
|
123
|
-
export function renderSystemDoc(options = {}) {
|
|
124
|
-
const cfg = options.cfg ?? {}
|
|
125
|
-
const template = options.template ?? readFileSync(templatePath(), 'utf8')
|
|
126
|
-
const values = {
|
|
127
|
-
documentVersion: String(SYSTEM_DOC_VERSION),
|
|
128
|
-
version: String(options.version ?? 'unknown'),
|
|
129
|
-
generated: options.now ?? new Date().toISOString(),
|
|
130
|
-
store: String(options.store ?? '(unknown)'),
|
|
131
|
-
diagram: DIAGRAM,
|
|
132
|
-
layerTable: layerTable(),
|
|
133
|
-
workspaceOnly: WORKSPACE_ONLY_LAYERS.map((l) => '`' + l + '`').join(' and '),
|
|
134
|
-
captureInterval: humanDuration(cfg.autoCaptureIntervalMs),
|
|
135
|
-
consolidateInterval: humanDuration(cfg.autoConsolidateIntervalMs),
|
|
136
|
-
refreshInterval: humanDuration(cfg.autoRefreshIntervalMs),
|
|
137
|
-
historyInterval: humanDuration(cfg.autoHistoryIntervalMs),
|
|
138
|
-
syncInterval: humanDuration(cfg.autoSyncIntervalMs),
|
|
139
|
-
commitDebounce: humanDuration(cfg.commitDebounceMs),
|
|
140
|
-
effort: String(cfg.memoryReasoningEffort ?? '(model default)'),
|
|
141
|
-
minSignalChars: String(cfg.summaryMinSignalChars ?? ''),
|
|
142
|
-
digestKeep: String(cfg.digestKeepCount ?? ''),
|
|
143
|
-
stableDays: String(VOLATILITY_DAYS.stable),
|
|
144
|
-
normalDays: String(VOLATILITY_DAYS.normal),
|
|
145
|
-
volatileDays: String(VOLATILITY_DAYS.volatile),
|
|
146
|
-
storeState: storeState(options.store),
|
|
147
|
-
}
|
|
148
|
-
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
149
|
-
return Object.hasOwn(values, key) ? String(values[key]) : match
|
|
150
|
-
})
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Write SYSTEM.md into a store. Always overwrites: the document is generated, never edited
|
|
155
|
-
* in place, so a stale copy must not survive a boot.
|
|
156
|
-
* @returns the path written.
|
|
157
|
-
*/
|
|
158
|
-
export function writeSystemDoc(root, options = {}) {
|
|
159
|
-
const target = systemDocPath(root)
|
|
160
|
-
writeFileSync(target, renderSystemDoc({ ...options, store: root }), 'utf8')
|
|
161
|
-
return target
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** True when the on-disk document is missing or was written by an older version. */
|
|
165
|
-
export function systemDocStale(root) {
|
|
166
|
-
try {
|
|
167
|
-
const text = readFileSync(systemDocPath(root), 'utf8')
|
|
168
|
-
const found = /Document version: (\d+)/.exec(text)
|
|
169
|
-
return found === null || Number(found[1]) !== SYSTEM_DOC_VERSION
|
|
170
|
-
} catch { return true }
|
|
171
|
-
}
|
|
1
|
+
// The system document: the memory system describing itself, as built.
|
|
2
|
+
//
|
|
3
|
+
// This is not a memory layer - it is not a fact about the user's work, it is the manual for the
|
|
4
|
+
// machinery. It is rendered from SYSTEM.template.md on every boot so it can never drift from the
|
|
5
|
+
// build, and it lives at the store root (committed) so the same description travels with the
|
|
6
|
+
// memory to every machine.
|
|
7
|
+
//
|
|
8
|
+
// It exists because an agent that reads and writes memory needs to understand the intent behind
|
|
9
|
+
// it: which layer a fact belongs in, why writes are deterministic, and what is derived and
|
|
10
|
+
// therefore safe to lose. Without that, memory becomes a junk drawer.
|
|
11
|
+
import { readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
12
|
+
import { dirname, join } from 'node:path'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import { LAYER_CODES, LAYER_DIRS, LAYERED, VOLATILITY_DAYS, WORKSPACE_ONLY_LAYERS } from './entry.js'
|
|
15
|
+
|
|
16
|
+
/** Bumped whenever the described behaviour changes, so a stale on-disk copy is detectable. */
|
|
17
|
+
export const SYSTEM_DOC_VERSION = 2
|
|
18
|
+
|
|
19
|
+
/** The template ships beside this module; resolve it relative to the module, not the cwd. */
|
|
20
|
+
export function templatePath() {
|
|
21
|
+
return join(dirname(fileURLToPath(import.meta.url)), 'SYSTEM.template.md')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Absolute path of the rendered document inside a store. */
|
|
25
|
+
export function systemDocPath(root) { return join(root, 'SYSTEM.md') }
|
|
26
|
+
|
|
27
|
+
/** The layer diagram, kept as data so it cannot break the surrounding code. */
|
|
28
|
+
const DIAGRAM = [
|
|
29
|
+
'global/ workspaces/<key>/',
|
|
30
|
+
' semantic.md semantic.md',
|
|
31
|
+
' semantic/<topic>.md semantic/<topic>.md',
|
|
32
|
+
' episodic.md + episodic/<topic>.md',
|
|
33
|
+
' procedural.md + procedural/<topic>.md',
|
|
34
|
+
' learnings.md + learnings/<topic>.md',
|
|
35
|
+
' working.md working.md (single file, never indexed)',
|
|
36
|
+
' junk.md junk.md (single file, never indexed)',
|
|
37
|
+
' codebase.md + codebase/<topic>.md (workspace only)',
|
|
38
|
+
' history.md + history/<day>.md (workspace only)',
|
|
39
|
+
' state.md state.md (repo state; replaced per section)',
|
|
40
|
+
' working.md working.md (YOUR task/activity head)',
|
|
41
|
+
].join(String.fromCharCode(10))
|
|
42
|
+
|
|
43
|
+
/** Count the markdown files inside one layer directory, or 0 when it does not exist. */
|
|
44
|
+
function countTopics(root, scopeDir, layer) {
|
|
45
|
+
try {
|
|
46
|
+
return readdirSync(join(root, scopeDir, LAYER_DIRS[layer])).filter((n) => n.endsWith('.md')).length
|
|
47
|
+
} catch { return 0 }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Summarise what the store currently holds, so the document reports reality rather than intent.
|
|
52
|
+
* Best-effort: an unreadable scope is reported as absent, never thrown.
|
|
53
|
+
*/
|
|
54
|
+
export function describeStore(root) {
|
|
55
|
+
const scopes = [{ label: 'global', dir: 'global' }]
|
|
56
|
+
try {
|
|
57
|
+
for (const name of readdirSync(join(root, 'workspaces'))) scopes.push({ label: name, dir: join('workspaces', name) })
|
|
58
|
+
} catch {}
|
|
59
|
+
return scopes.map((scope) => {
|
|
60
|
+
const counts = {}
|
|
61
|
+
for (const layer of LAYERED) counts[layer] = countTopics(root, scope.dir, layer)
|
|
62
|
+
return { scope: scope.label, counts, total: Object.values(counts).reduce((a, b) => a + b, 0) }
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Format a millisecond cadence for humans: 3600000 -> '1h'. */
|
|
67
|
+
export function humanDuration(ms) {
|
|
68
|
+
const n = Number(ms)
|
|
69
|
+
if (!Number.isFinite(n) || n <= 0) return String(ms)
|
|
70
|
+
if (n % 86400000 === 0) return (n / 86400000) + 'd'
|
|
71
|
+
if (n % 3600000 === 0) return (n / 3600000) + 'h'
|
|
72
|
+
if (n % 60000 === 0) return (n / 60000) + 'm'
|
|
73
|
+
if (n % 1000 === 0) return (n / 1000) + 's'
|
|
74
|
+
return n + 'ms'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** What each layer is for, keyed by layer so the table cannot drift from the vocabulary. */
|
|
78
|
+
const LAYER_PURPOSE = {
|
|
79
|
+
semantic: 'Durable facts that stay true: how something works, what was decided, environment facts, versions, where things live.',
|
|
80
|
+
episodic: 'What happened: an incident, a migration, a change made on a date and why.',
|
|
81
|
+
procedural: 'How to do something: runbooks, commands, build and test steps, order of operations.',
|
|
82
|
+
learning: 'A lesson from experience - what you wish you had known an hour earlier.',
|
|
83
|
+
codebase: 'What a module, file or subsystem of THIS repository does. Workspace only.',
|
|
84
|
+
history: 'How the repository changed: one entry per commit, grouped by day. Workspace only.',
|
|
85
|
+
decision: 'A settled choice and why it won: dependencies, conventions, architecture. Available in both scopes.',
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The per-layer table, generated from the layer vocabulary so a newly added layer appears here
|
|
90
|
+
* automatically. A layer with no purpose recorded still gets a row, flagged rather than omitted.
|
|
91
|
+
*/
|
|
92
|
+
function layerTable() {
|
|
93
|
+
const rows = ['| Layer | Code | Use it for |', '| --- | --- | --- |']
|
|
94
|
+
for (const layer of LAYERED) {
|
|
95
|
+
const purpose = LAYER_PURPOSE[layer] ?? '(undocumented)'
|
|
96
|
+
rows.push('| `' + layer + '` | `' + (LAYER_CODES[layer] ?? layer) + '` | ' + purpose + ' |')
|
|
97
|
+
}
|
|
98
|
+
return rows.join(String.fromCharCode(10))
|
|
99
|
+
}
|
|
100
|
+
/** The current-state table, so the document shows what this store actually holds. */
|
|
101
|
+
function storeState(root) {
|
|
102
|
+
if (!root) return '(store root unknown)'
|
|
103
|
+
const rows = describeStore(root)
|
|
104
|
+
const layers = LAYERED
|
|
105
|
+
const lines = [
|
|
106
|
+
'| Scope | ' + layers.join(' | ') + ' | total |',
|
|
107
|
+
'| --- | ' + layers.map(() => '---').join(' | ') + ' | --- |',
|
|
108
|
+
]
|
|
109
|
+
for (const row of rows) {
|
|
110
|
+
lines.push('| ' + row.scope + ' | ' + layers.map((l) => row.counts[l]).join(' | ') + ' | ' + row.total + ' |')
|
|
111
|
+
}
|
|
112
|
+
return lines.join(String.fromCharCode(10))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Render the system document by filling the template.
|
|
117
|
+
* @param options.cfg - resolved plugin config, quoted so the document matches this build.
|
|
118
|
+
* @param options.version - plugin version.
|
|
119
|
+
* @param options.store - store root, used for the current-state table.
|
|
120
|
+
* @param options.now - ISO timestamp.
|
|
121
|
+
* @returns the markdown document.
|
|
122
|
+
*/
|
|
123
|
+
export function renderSystemDoc(options = {}) {
|
|
124
|
+
const cfg = options.cfg ?? {}
|
|
125
|
+
const template = options.template ?? readFileSync(templatePath(), 'utf8')
|
|
126
|
+
const values = {
|
|
127
|
+
documentVersion: String(SYSTEM_DOC_VERSION),
|
|
128
|
+
version: String(options.version ?? 'unknown'),
|
|
129
|
+
generated: options.now ?? new Date().toISOString(),
|
|
130
|
+
store: String(options.store ?? '(unknown)'),
|
|
131
|
+
diagram: DIAGRAM,
|
|
132
|
+
layerTable: layerTable(),
|
|
133
|
+
workspaceOnly: WORKSPACE_ONLY_LAYERS.map((l) => '`' + l + '`').join(' and '),
|
|
134
|
+
captureInterval: humanDuration(cfg.autoCaptureIntervalMs),
|
|
135
|
+
consolidateInterval: humanDuration(cfg.autoConsolidateIntervalMs),
|
|
136
|
+
refreshInterval: humanDuration(cfg.autoRefreshIntervalMs),
|
|
137
|
+
historyInterval: humanDuration(cfg.autoHistoryIntervalMs),
|
|
138
|
+
syncInterval: humanDuration(cfg.autoSyncIntervalMs),
|
|
139
|
+
commitDebounce: humanDuration(cfg.commitDebounceMs),
|
|
140
|
+
effort: String(cfg.memoryReasoningEffort ?? '(model default)'),
|
|
141
|
+
minSignalChars: String(cfg.summaryMinSignalChars ?? ''),
|
|
142
|
+
digestKeep: String(cfg.digestKeepCount ?? ''),
|
|
143
|
+
stableDays: String(VOLATILITY_DAYS.stable),
|
|
144
|
+
normalDays: String(VOLATILITY_DAYS.normal),
|
|
145
|
+
volatileDays: String(VOLATILITY_DAYS.volatile),
|
|
146
|
+
storeState: storeState(options.store),
|
|
147
|
+
}
|
|
148
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
149
|
+
return Object.hasOwn(values, key) ? String(values[key]) : match
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Write SYSTEM.md into a store. Always overwrites: the document is generated, never edited
|
|
155
|
+
* in place, so a stale copy must not survive a boot.
|
|
156
|
+
* @returns the path written.
|
|
157
|
+
*/
|
|
158
|
+
export function writeSystemDoc(root, options = {}) {
|
|
159
|
+
const target = systemDocPath(root)
|
|
160
|
+
writeFileSync(target, renderSystemDoc({ ...options, store: root }), 'utf8')
|
|
161
|
+
return target
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** True when the on-disk document is missing or was written by an older version. */
|
|
165
|
+
export function systemDocStale(root) {
|
|
166
|
+
try {
|
|
167
|
+
const text = readFileSync(systemDocPath(root), 'utf8')
|
|
168
|
+
const found = /Document version: (\d+)/.exec(text)
|
|
169
|
+
return found === null || Number(found[1]) !== SYSTEM_DOC_VERSION
|
|
170
|
+
} catch { return true }
|
|
171
|
+
}
|