@try-works/dsh-memory 0.1.32 → 0.2.1
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/STATUS.md +29 -2
- package/package.json +1 -1
- package/src/assets/describe.js +181 -0
- package/src/assets/entry.js +169 -0
- package/src/assets/index.js +648 -0
- package/src/assets/lfs.js +169 -0
- package/src/assets/objects.js +199 -0
- package/src/assets/policy.js +259 -0
- package/src/config.js +12 -0
- package/src/defaults.js +11 -0
- package/src/entry.js +58 -13
- package/src/forget.js +7 -0
- package/src/host.js +202 -3
- package/src/refresh.js +7 -2
- package/src/search.js +47 -3
- package/src/tools.js +735 -10
package/STATUS.md
CHANGED
|
@@ -3,10 +3,37 @@
|
|
|
3
3
|
A tracker and planner: what exists, what is verified working, what is written but not
|
|
4
4
|
loaded, and what is left. Kept as a record of MEASURED state, not intention.
|
|
5
5
|
|
|
6
|
-
**Last measured: 2026-11-01** (
|
|
6
|
+
**Last measured: 2026-11-01** (522 tests passing; plugin at 0.2.0, published and installed
|
|
7
7
|
byte-identical on harness 0.2.1-rc.2; queue terminal-clean; the Memory tab reads the published
|
|
8
8
|
map under the new event name; the FTS index is 148.6 MB after a rebuild that used to be 436 MB,
|
|
9
|
-
search measures 46-274 ms and an injection 3.3 s).
|
|
9
|
+
search measures 46-274 ms and an injection 3.3 s; the asset store ships phase 1).
|
|
10
|
+
|
|
11
|
+
## Asset store, phase 1 (0.2.0)
|
|
12
|
+
|
|
13
|
+
Built from `docs/asset-store-proposal.md` and `docs/asset-api-contract.md` by three agents working
|
|
14
|
+
in parallel against one contract, with TDD throughout (failing test first; the acceptance suite is
|
|
15
|
+
independent of the implementation).
|
|
16
|
+
|
|
17
|
+
What exists: a new `assets` layer in both scopes; content-addressed blobs under
|
|
18
|
+
`assets/objects/<ab>/<sha256>.<ext>` carried by Git LFS (`.gitattributes`, `git lfs install
|
|
19
|
+
--local`, pointer detection); five tools (`memory_asset_put/get/list/forget/policy`); automatic
|
|
20
|
+
attachment capture on a human message when the class toggle is on, with no prompt; three class
|
|
21
|
+
toggles resolved as config (hard off) → workspace policy → global policy; 50 MB per file and 1 GB
|
|
22
|
+
per scope with refusals that surface the figures and ask the user through `ctx.userQuestions`
|
|
23
|
+
(forgive / raise / forget / cancel) instead of pruning; one model description from capture context
|
|
24
|
+
with a deterministic fallback; `memory_status` reporting asset bytes and quota notices; search
|
|
25
|
+
hits on asset entries carrying `{ sha256, mime, name, bytes, class }`.
|
|
26
|
+
|
|
27
|
+
What does NOT exist, by design: content indexing. The store documents a file ("What it is", "When
|
|
28
|
+
to read it"); it never extracts or indexes the file's contents, and a token that appears only
|
|
29
|
+
inside a PDF is deliberately not searchable (pinned by the acceptance suite).
|
|
30
|
+
|
|
31
|
+
Three integration decisions worth keeping: `refresh` skips the asset layer entirely (a supersede
|
|
32
|
+
would hide the entry while its inventory refcount stayed, orphaning the blob); the generic
|
|
33
|
+
`memory_forget` REFUSES an asset entry and names `memory_asset_forget`, which is the only path
|
|
34
|
+
that decrements the inventory; and an automatic capture over quota records a notice instead of
|
|
35
|
+
asking, because `session/event` is reentrancy-sensitive and has no answerer — only
|
|
36
|
+
`memory_asset_put` asks.
|
|
10
37
|
|
|
11
38
|
## Store performance, 2026-11-01
|
|
12
39
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// The one model call that documents a captured asset.
|
|
2
|
+
//
|
|
3
|
+
// An asset is stored as bytes plus an entry, and the entry is what search finds later. At capture
|
|
4
|
+
// time the agent still has the context that made the file relevant - the user's request, the tool
|
|
5
|
+
// call, the task - and a later reader does not. This module turns that context (and ONLY that
|
|
6
|
+
// context: the file itself is never opened) into the two sections of the entry body:
|
|
7
|
+
//
|
|
8
|
+
// ## What it is
|
|
9
|
+
// ## When to read it
|
|
10
|
+
//
|
|
11
|
+
// The model is advisory. Capture must never fail for want of a description, so every failure
|
|
12
|
+
// (no model routed, a throw, an empty reply, an unparseable one) falls back to a deterministic
|
|
13
|
+
// pair of lines derived from the same context. "Deterministic" is a promise: the same context
|
|
14
|
+
// yields byte-identical text, with no timestamps and no randomness.
|
|
15
|
+
|
|
16
|
+
/** Bound on each generated section, so a verbose model cannot bloat the entry and the index. */
|
|
17
|
+
export const MAX_SECTION_CHARS = 1200
|
|
18
|
+
/** Bound on the excerpt of the user message quoted in the deterministic fallback. */
|
|
19
|
+
export const MAX_EXCERPT_CHARS = 160
|
|
20
|
+
|
|
21
|
+
/** Normalize any context value to bounded, single-line text without throwing on weird input. */
|
|
22
|
+
function text(value, maxChars = MAX_SECTION_CHARS) {
|
|
23
|
+
if (value === undefined || value === null) return ''
|
|
24
|
+
let rendered
|
|
25
|
+
if (typeof value === 'string') rendered = value
|
|
26
|
+
else {
|
|
27
|
+
try { rendered = JSON.stringify(value) } catch { rendered = String(value) }
|
|
28
|
+
}
|
|
29
|
+
return String(rendered ?? '').replace(/\s+/g, ' ').trim().slice(0, maxChars)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The fields this module documents from. Anything else in the input - a path, bytes, a contents
|
|
34
|
+
* field, a count - is deliberately ignored: this module never reads the file, so it cannot
|
|
35
|
+
* describe it.
|
|
36
|
+
*
|
|
37
|
+
* putAsset calls the describe hook with a small META object ({ class, mime, name, bytes,
|
|
38
|
+
* context: {userMessage, toolCall, task} }), while a direct caller passes the bare context. Both
|
|
39
|
+
* shapes are accepted here so the hook and the unit-test seam are the same function.
|
|
40
|
+
*/
|
|
41
|
+
function captureFacts(context = {}) {
|
|
42
|
+
const source = context && typeof context === 'object' ? context : {}
|
|
43
|
+
const nested = source.context && typeof source.context === 'object' ? source.context : {}
|
|
44
|
+
return {
|
|
45
|
+
name: text(source.name, 240) || 'untitled file',
|
|
46
|
+
mime: text(source.mime, 120) || undefined,
|
|
47
|
+
class: text(source.class, 40) || undefined,
|
|
48
|
+
userMessage: text(source.userMessage ?? nested.userMessage, MAX_EXCERPT_CHARS) || undefined,
|
|
49
|
+
toolCall: text(source.toolCall ?? nested.toolCall, 120) || undefined,
|
|
50
|
+
task: text(source.task ?? nested.task, MAX_EXCERPT_CHARS) || undefined,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The deterministic `Asset-Used-For:` line.
|
|
56
|
+
*
|
|
57
|
+
* This is ALWAYS written to the entry, model or no model: it is the one sentence that survives a
|
|
58
|
+
* deployment with no routed model, and it records the situation in which the file was captured.
|
|
59
|
+
* Pure and deterministic for a given context.
|
|
60
|
+
*/
|
|
61
|
+
export function usedForFrom(context = {}) {
|
|
62
|
+
const facts = captureFacts(context)
|
|
63
|
+
const parts = []
|
|
64
|
+
if (facts.userMessage) parts.push('while answering "' + facts.userMessage + '"')
|
|
65
|
+
if (facts.task) parts.push('for the task "' + facts.task + '"')
|
|
66
|
+
if (facts.toolCall) parts.push('(via ' + facts.toolCall + ')')
|
|
67
|
+
if (parts.length === 0) {
|
|
68
|
+
return 'Captured from the session context; the description was written without reading the file.'
|
|
69
|
+
}
|
|
70
|
+
return 'Captured ' + parts.join(' ') + '; the description was written without reading the file.'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The deterministic fallback description. Public because tests (and any caller that wants to
|
|
75
|
+
* know what the fallback will say before spending a model call) must be able to pin it.
|
|
76
|
+
* @returns {{whatItIs: string, whenToRead: string, usedFor: string}}
|
|
77
|
+
*/
|
|
78
|
+
export function fallbackDescription(context = {}) {
|
|
79
|
+
const facts = captureFacts(context)
|
|
80
|
+
const kind = facts.class ? facts.class.replace(/s$/, '') + ' file' : 'file'
|
|
81
|
+
const type = facts.mime ? ' (' + facts.mime + ')' : ''
|
|
82
|
+
const whatItIs = 'A ' + kind + ' named "' + facts.name + '"' + type
|
|
83
|
+
+ ', stored from the context of this session. Its contents were not read, so this line is a pointer rather than a summary.'
|
|
84
|
+
const trigger = facts.userMessage ? 'the request "' + facts.userMessage + '"'
|
|
85
|
+
: facts.task ? 'the task "' + facts.task + '"'
|
|
86
|
+
: 'a later request in this area'
|
|
87
|
+
const whenToRead = 'Read this when a task returns to ' + trigger
|
|
88
|
+
+ ' or explicitly needs "' + facts.name + '". Open it with the appropriate reader; nothing from inside the file is indexed.'
|
|
89
|
+
return { whatItIs, whenToRead, usedFor: usedForFrom(context) }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The single prompt sent to the model. It spoils nothing beyond the capture context, and says so
|
|
94
|
+
* explicitly: a model asked to "describe this PDF" without being told it cannot see it will
|
|
95
|
+
* invent a plausible table of contents, which is exactly the failure this store can least afford.
|
|
96
|
+
*/
|
|
97
|
+
export function buildDescribePrompt(context = {}) {
|
|
98
|
+
const facts = captureFacts(context)
|
|
99
|
+
const lines = [
|
|
100
|
+
'Write the catalogue entry for a file being kept in a memory store.',
|
|
101
|
+
'You have NOT read the file and cannot see its contents. Describe the file only from the capture context below, and say nothing that would require opening it. Do not invent sections, tables, numbers, or page counts. Write plain sentences: no markdown headings and no bullet lists.',
|
|
102
|
+
'',
|
|
103
|
+
'Reply in exactly this form, with no preamble and no extra sections:',
|
|
104
|
+
'',
|
|
105
|
+
'WHAT IT IS:',
|
|
106
|
+
'<one or two sentences on what this file is, from the context>',
|
|
107
|
+
'',
|
|
108
|
+
'WHEN TO READ IT:',
|
|
109
|
+
'<one or two sentences on which future requests should open it, and which should not>',
|
|
110
|
+
'',
|
|
111
|
+
'Capture context:',
|
|
112
|
+
'- name: ' + facts.name,
|
|
113
|
+
]
|
|
114
|
+
if (facts.mime) lines.push('- mime type: ' + facts.mime)
|
|
115
|
+
if (facts.class) lines.push('- class: ' + facts.class)
|
|
116
|
+
if (facts.userMessage) lines.push('- what the user was doing: ' + facts.userMessage)
|
|
117
|
+
if (facts.task) lines.push('- task: ' + facts.task)
|
|
118
|
+
if (facts.toolCall) lines.push('- tool call: ' + facts.toolCall)
|
|
119
|
+
return lines.join('\n')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Parse the two sections out of a model reply.
|
|
124
|
+
*
|
|
125
|
+
* Tolerant of the forms a model actually produces: `WHAT IT IS:` / `When to read it:`, with or
|
|
126
|
+
* without markdown heading markers. Returns undefined unless BOTH sections yield non-empty text,
|
|
127
|
+
* because half an entry is worse than the deterministic fallback: a missing "When to read it"
|
|
128
|
+
* turns the asset invisible to the only query that matters. An optional NOTES section is carried
|
|
129
|
+
* through when the model volunteered one; the text itself never keeps its section heading.
|
|
130
|
+
* @returns {{whatItIs: string, whenToRead: string, notes?: string}|undefined}
|
|
131
|
+
*/
|
|
132
|
+
export function parseDescription(reply) {
|
|
133
|
+
const body = String(reply ?? '').trim()
|
|
134
|
+
if (body === '') return undefined
|
|
135
|
+
const section = (heading) => {
|
|
136
|
+
const pattern = new RegExp(
|
|
137
|
+
'(?:^|\\n)\\s*(?:#{1,6}\\s*)?' + heading + '\\s*:?\\s*\\n([\\s\\S]*?)(?=(?:\\n\\s*(?:#{1,6}\\s*)?(?:WHAT IT IS|WHEN TO READ IT|NOTES)\\s*:?\\s*(?:\\n|$))|$)',
|
|
138
|
+
'i',
|
|
139
|
+
)
|
|
140
|
+
const match = pattern.exec(body)
|
|
141
|
+
const value = match ? match[1].trim() : ''
|
|
142
|
+
return value === '' ? undefined : value.slice(0, MAX_SECTION_CHARS)
|
|
143
|
+
}
|
|
144
|
+
const whatItIs = section('WHAT IT IS')
|
|
145
|
+
const whenToRead = section('WHEN TO READ IT')
|
|
146
|
+
if (!whatItIs || !whenToRead) return undefined
|
|
147
|
+
const notes = section('NOTES')
|
|
148
|
+
return notes === undefined ? { whatItIs, whenToRead } : { whatItIs, whenToRead, notes }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Describe one captured asset: one model call, or the deterministic fallback.
|
|
153
|
+
*
|
|
154
|
+
* The returned text has NO heading markers of its own - the entry renderer owns the `### What it
|
|
155
|
+
* is` / `### When to read it` headings and demotes anything heading-like inside the text.
|
|
156
|
+
*
|
|
157
|
+
* @param context - capture context or putAsset's describe-hook meta. Only the documentation facts
|
|
158
|
+
* are used; a path or bytes in the input is ignored, because this function never reads the file.
|
|
159
|
+
* @param options.llm - optional async (prompt) => text. Called at most once.
|
|
160
|
+
* @returns {{whatItIs: string, whenToRead: string, usedFor: string, source: 'model'|'fallback', reason?: string, notes?: string}}
|
|
161
|
+
*/
|
|
162
|
+
export async function describeAsset(context = {}, { llm } = {}) {
|
|
163
|
+
const fallback = fallbackDescription(context)
|
|
164
|
+
if (typeof llm !== 'function') return { ...fallback, source: 'fallback', reason: 'no model available' }
|
|
165
|
+
let reply
|
|
166
|
+
try {
|
|
167
|
+
reply = await llm(buildDescribePrompt(context))
|
|
168
|
+
} catch (error) {
|
|
169
|
+
return { ...fallback, source: 'fallback', reason: 'the model call failed: ' + text(error && error.message ? error.message : error, 160) }
|
|
170
|
+
}
|
|
171
|
+
if (typeof reply !== 'string' || reply.trim() === '') {
|
|
172
|
+
return { ...fallback, source: 'fallback', reason: 'the model returned nothing' }
|
|
173
|
+
}
|
|
174
|
+
const parsed = parseDescription(reply)
|
|
175
|
+
if (!parsed) {
|
|
176
|
+
return { ...fallback, source: 'fallback', reason: 'the model reply did not contain both sections' }
|
|
177
|
+
}
|
|
178
|
+
return { ...parsed, usedFor: fallback.usedFor, source: 'model' }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export default describeAsset
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Asset entries: an ordinary Format 2 entry in the `asset` layer, plus Asset-* headers and the
|
|
2
|
+
// documentation sections search relies on.
|
|
3
|
+
//
|
|
4
|
+
// Rendering and parsing are pure, so the round trip is testable without a store. The bytes never
|
|
5
|
+
// enter the entry: it holds metadata and the documentation a later reader searches for.
|
|
6
|
+
import { formatEntry } from '../entry.js'
|
|
7
|
+
|
|
8
|
+
/** Body section titles. Search relies on exactly these headings. */
|
|
9
|
+
export const SECTION_WHAT = 'What it is'
|
|
10
|
+
export const SECTION_WHEN = 'When to read it'
|
|
11
|
+
export const SECTION_NOTES = 'Notes'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Body sections are H3, not H2: `## ` is Format 2's entry delimiter, so an H2 section would split
|
|
15
|
+
* an asset into an entry plus ignored pseudo-entries. The parser still accepts H2 section titles for
|
|
16
|
+
* entries written before that decision (and for hand-written ones), which is why parseEntries also
|
|
17
|
+
* carries the defensive H2-as-body rule for entries that already hold `Asset-Sha256`.
|
|
18
|
+
*/
|
|
19
|
+
const SECTION_PREFIX = '### '
|
|
20
|
+
|
|
21
|
+
/** Collapse a value onto one line: headers are one field per line, and a newline would end it. */
|
|
22
|
+
function oneLine(value) {
|
|
23
|
+
return String(value ?? '').replace(/\r?\n+/g, ' ').trim()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function optionalNumber(value) {
|
|
27
|
+
const number = Number(value)
|
|
28
|
+
return Number.isFinite(number) && number >= 0 ? number : 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function assetHeaderLines(asset = {}) {
|
|
32
|
+
const lines = [
|
|
33
|
+
'**Asset-Class:** ' + oneLine(asset.class),
|
|
34
|
+
'**Asset-Mime:** ' + oneLine(asset.mime),
|
|
35
|
+
'**Asset-Bytes:** ' + String(optionalNumber(asset.bytes)),
|
|
36
|
+
'**Asset-Sha256:** ' + oneLine(asset.sha256),
|
|
37
|
+
'**Asset-Name:** ' + oneLine(asset.name),
|
|
38
|
+
'**Asset-Origin:** ' + oneLine(asset.origin ?? 'inline'),
|
|
39
|
+
'**Asset-Used-For:** ' + oneLine(asset.usedFor),
|
|
40
|
+
]
|
|
41
|
+
const description = oneLine(asset.description)
|
|
42
|
+
if (description !== '') lines.push('**Asset-Description:** ' + description)
|
|
43
|
+
return lines
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Normalize a documentation body: demote any markdown heading inside it. A model-supplied
|
|
48
|
+
* description may contain `## something`; at column zero that would terminate the entry (and, for
|
|
49
|
+
* the three reserved titles, the section), so nested headings are lifted to `###`.
|
|
50
|
+
*/
|
|
51
|
+
function sectionText(value) {
|
|
52
|
+
return String(value ?? '')
|
|
53
|
+
.replace(/\r\n/g, '\n')
|
|
54
|
+
.split('\n')
|
|
55
|
+
.map((line) => line.replace(/^(\s*)#{1,6}\s+/, '$1### '))
|
|
56
|
+
.join('\n')
|
|
57
|
+
.trim()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function section(title, text) {
|
|
61
|
+
const body = sectionText(text)
|
|
62
|
+
return body === '' ? '' : SECTION_PREFIX + title + '\n\n' + body
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Render one asset entry block, heading included.
|
|
67
|
+
* @param input.id - Format 2 id (`global-ast-0001`).
|
|
68
|
+
* @param input.asset - { class, mime, bytes, sha256, name, origin, usedFor, description? }.
|
|
69
|
+
* @param input.documentation - { whatItIs, whenToRead, notes? }; the two named sections are the
|
|
70
|
+
* search surface and are always written by putAsset (which supplies a deterministic fallback).
|
|
71
|
+
* @returns the markdown block, ready for appendToTopic.
|
|
72
|
+
*/
|
|
73
|
+
export function renderAssetEntry({
|
|
74
|
+
id,
|
|
75
|
+
source,
|
|
76
|
+
date,
|
|
77
|
+
importance = 2,
|
|
78
|
+
status = 'active',
|
|
79
|
+
volatility,
|
|
80
|
+
verified,
|
|
81
|
+
until,
|
|
82
|
+
heading,
|
|
83
|
+
asset,
|
|
84
|
+
documentation = {},
|
|
85
|
+
} = {}) {
|
|
86
|
+
const title = oneLine(heading ?? asset?.name) || 'asset'
|
|
87
|
+
const sections = [
|
|
88
|
+
section(SECTION_WHAT, documentation.whatItIs),
|
|
89
|
+
section(SECTION_WHEN, documentation.whenToRead),
|
|
90
|
+
]
|
|
91
|
+
const notes = section(SECTION_NOTES, documentation.notes)
|
|
92
|
+
if (notes !== '') sections.push(notes)
|
|
93
|
+
const content = [assetHeaderLines(asset).join('\n'), ...sections.filter((part) => part !== '')]
|
|
94
|
+
.filter((part) => part !== '')
|
|
95
|
+
.join('\n\n')
|
|
96
|
+
const metadata = formatEntry({
|
|
97
|
+
id,
|
|
98
|
+
source,
|
|
99
|
+
date,
|
|
100
|
+
importance,
|
|
101
|
+
status,
|
|
102
|
+
volatility,
|
|
103
|
+
verified,
|
|
104
|
+
until,
|
|
105
|
+
content,
|
|
106
|
+
})
|
|
107
|
+
return '## ' + title + '\n\n' + metadata
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Split an entry body into the three documented sections. H3 is the format; H2 is tolerated. */
|
|
111
|
+
function parseSections(content) {
|
|
112
|
+
const sections = { whatItIs: undefined, whenToRead: undefined, notes: undefined }
|
|
113
|
+
const buffers = { whatItIs: [], whenToRead: [], notes: [] }
|
|
114
|
+
let current
|
|
115
|
+
const flush = () => {
|
|
116
|
+
if (current === undefined) return
|
|
117
|
+
const text = buffers[current].join('\n').trim()
|
|
118
|
+
if (text !== '') sections[current] = text
|
|
119
|
+
}
|
|
120
|
+
for (const line of String(content ?? '').split('\n')) {
|
|
121
|
+
const heading = line.match(/^#{2,3}\s+(.+)$/)
|
|
122
|
+
if (heading) {
|
|
123
|
+
flush()
|
|
124
|
+
const title = heading[1].trim().toLowerCase()
|
|
125
|
+
current = title === SECTION_WHAT.toLowerCase()
|
|
126
|
+
? 'whatItIs'
|
|
127
|
+
: title === SECTION_WHEN.toLowerCase()
|
|
128
|
+
? 'whenToRead'
|
|
129
|
+
: title === SECTION_NOTES.toLowerCase()
|
|
130
|
+
? 'notes'
|
|
131
|
+
: undefined
|
|
132
|
+
if (current !== undefined) buffers[current] = []
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
if (current !== undefined) buffers[current].push(line)
|
|
136
|
+
}
|
|
137
|
+
flush()
|
|
138
|
+
return sections
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Parse an asset entry from the projection parseEntries returns (raw `meta` + body).
|
|
143
|
+
* @returns the asset, or undefined when the entry is not an asset entry.
|
|
144
|
+
*/
|
|
145
|
+
export function parseAssetEntry(entry) {
|
|
146
|
+
if (entry === null || entry === undefined || entry.meta === null || entry.meta === undefined) return undefined
|
|
147
|
+
const meta = entry.meta
|
|
148
|
+
const sha256 = meta['Asset-Sha256']
|
|
149
|
+
if (!sha256) return undefined
|
|
150
|
+
const sections = parseSections(entry.content)
|
|
151
|
+
return {
|
|
152
|
+
id: entry.id,
|
|
153
|
+
heading: entry.heading,
|
|
154
|
+
date: entry.date,
|
|
155
|
+
updated: entry.updated,
|
|
156
|
+
status: entry.status,
|
|
157
|
+
name: meta['Asset-Name'],
|
|
158
|
+
class: meta['Asset-Class'],
|
|
159
|
+
mime: meta['Asset-Mime'],
|
|
160
|
+
bytes: optionalNumber(meta['Asset-Bytes']),
|
|
161
|
+
sha256,
|
|
162
|
+
origin: meta['Asset-Origin'],
|
|
163
|
+
usedFor: meta['Asset-Used-For'],
|
|
164
|
+
description: meta['Asset-Description'],
|
|
165
|
+
whatItIs: sections.whatItIs,
|
|
166
|
+
whenToRead: sections.whenToRead,
|
|
167
|
+
notes: sections.notes,
|
|
168
|
+
}
|
|
169
|
+
}
|