@sparkelf/dsh-knowledge-base 0.1.0 → 0.1.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/lib/client.js +4302 -1970
- package/lib/client.js.map +1 -1
- package/lib/index.js +293 -12
- package/lib/types/api.d.ts +46 -0
- package/lib/types/client/KnowledgeContextMenu.d.ts +31 -6
- package/lib/types/client/KnowledgeFileTree.d.ts +29 -0
- package/lib/types/client/KnowledgeHome.d.ts +23 -0
- package/lib/types/client/KnowledgeListing.d.ts +14 -0
- package/lib/types/client/KnowledgePermissionOverview.d.ts +51 -3
- package/lib/types/client/KnowledgeProgressPanel.d.ts +53 -0
- package/lib/types/client/KnowledgeReferencePicker.d.ts +16 -0
- package/lib/types/client/auditPaging.d.ts +40 -0
- package/lib/types/client/index.d.ts +14 -1
- package/lib/types/client/insertReadiness.d.ts +60 -0
- package/lib/types/client/locales.d.ts +68 -1
- package/lib/types/client/paging.d.ts +69 -0
- package/lib/types/client/permissionRows.d.ts +176 -5
- package/lib/types/client/progressRules.d.ts +54 -0
- package/lib/types/client/referenceTargets.d.ts +60 -0
- package/lib/types/client/shareRelations.d.ts +81 -0
- package/lib/types/client/useFileDrop.d.ts +171 -0
- package/lib/types/client/useFileDropHook.d.ts +64 -0
- package/lib/types/extractionQueue.d.ts +154 -0
- package/lib/types/index.d.ts +53 -0
- package/package.json +1 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning grants into the two "share" groups the Home tab shows.
|
|
3
|
+
*
|
|
4
|
+
* The split the reader cares about is DIRECTION: what has been shared with me, and what
|
|
5
|
+
* I have shared out. The grant data has no grantor field (the backend's `toSummary`
|
|
6
|
+
* omits it and the table has no `created_by`), so "who gave me this" cannot be answered
|
|
7
|
+
* from it. What CAN be answered is the audience side: which principal a grant names.
|
|
8
|
+
*
|
|
9
|
+
* Everything here is a pure function of the grant list plus the caller, which is what
|
|
10
|
+
* makes the two-way split testable without a browser.
|
|
11
|
+
*
|
|
12
|
+
* @module @sparkelf/dsh-knowledge-base/client
|
|
13
|
+
*/
|
|
14
|
+
/** One grant, in the shape the Host relays. */
|
|
15
|
+
export interface ShareGrant {
|
|
16
|
+
/** Stable row id. */
|
|
17
|
+
id: string;
|
|
18
|
+
/** Path relative to the knowledge mount point. */
|
|
19
|
+
relPath: string;
|
|
20
|
+
/** Whether the path is a file or a directory. */
|
|
21
|
+
kind: 'file' | 'directory';
|
|
22
|
+
/** Principal kind the grant names. */
|
|
23
|
+
principalType: 'user' | 'role' | 'department' | 'group';
|
|
24
|
+
/** Principal id within its kind. */
|
|
25
|
+
principalId: string;
|
|
26
|
+
/** Permission the grant confers. */
|
|
27
|
+
action: string;
|
|
28
|
+
}
|
|
29
|
+
/** A principal name lookup: id per kind, falling back to the id itself. */
|
|
30
|
+
export type PrincipalNameOf = (kind: ShareGrant['principalType'], id: string) => string | undefined;
|
|
31
|
+
/** Which group one grant belongs to. */
|
|
32
|
+
export type ShareDirection = 'received' | 'given';
|
|
33
|
+
/** One row of either group. */
|
|
34
|
+
export interface ShareRow {
|
|
35
|
+
/** Stable row id, taken from the grant. */
|
|
36
|
+
id: string;
|
|
37
|
+
/** Path relative to the knowledge mount point. */
|
|
38
|
+
relPath: string;
|
|
39
|
+
/** File name alone, for the row's title. */
|
|
40
|
+
name: string;
|
|
41
|
+
/** Directory the path sits in, for the location column. */
|
|
42
|
+
location: string;
|
|
43
|
+
/** Principal kind the grant names. */
|
|
44
|
+
principalType: ShareGrant['principalType'];
|
|
45
|
+
/** The audience this grant names, resolved to a display name when possible. */
|
|
46
|
+
audience: string;
|
|
47
|
+
/** The permission the grant confers. */
|
|
48
|
+
action: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Whether a grant names the caller directly.
|
|
52
|
+
*
|
|
53
|
+
* Only a `user` grant can: a role, department or group is an audience the caller
|
|
54
|
+
* belongs to rather than the caller themselves, and treating it as "me" would let a
|
|
55
|
+
* department-wide grant read as a personal share.
|
|
56
|
+
* @param grant - the grant to test.
|
|
57
|
+
* @param callerId - the caller's own user id, when known.
|
|
58
|
+
* @returns True when the grant names this exact user.
|
|
59
|
+
*/
|
|
60
|
+
export declare function isOwnGrant(grant: ShareGrant, callerId: string | undefined): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Split grants into the two directions the Home tab shows.
|
|
63
|
+
*
|
|
64
|
+
* The rule is deliberately simple and says only what the data supports: a grant that
|
|
65
|
+
* names the caller is "shared with me"; every other grant is "shared out". It does NOT
|
|
66
|
+
* claim the caller created the latter -- the grantor is not in this data at all -- so
|
|
67
|
+
* callers must word that group as an audience ("I shared to X"), never as a fact about
|
|
68
|
+
* who granted it.
|
|
69
|
+
*
|
|
70
|
+
* A grant naming the caller is excluded from the outgoing group even when it also
|
|
71
|
+
* appears there, so one document cannot look like it was shared to the reader and by
|
|
72
|
+
* the reader at the same time.
|
|
73
|
+
* @param grants - every grant the Host relayed.
|
|
74
|
+
* @param callerId - the caller's own user id, when known.
|
|
75
|
+
* @param nameOf - the principal directory lookup.
|
|
76
|
+
* @returns Both groups, each in the order the grants arrived.
|
|
77
|
+
*/
|
|
78
|
+
export declare function splitShareRelations(grants: readonly ShareGrant[], callerId: string | undefined, nameOf: PrincipalNameOf): {
|
|
79
|
+
received: ShareRow[];
|
|
80
|
+
given: ShareRow[];
|
|
81
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drag-and-drop upload: the part that can be decided without a browser.
|
|
3
|
+
*
|
|
4
|
+
* A drop needs three things settled before anything is uploaded: which directory the
|
|
5
|
+
* pointer is over, whether that directory may receive a drop, and the list of files
|
|
6
|
+
* (with their paths inside a dropped folder) that the drop carried. All three are
|
|
7
|
+
* answers about *data*, not about React state, so they live here and are tested
|
|
8
|
+
* directly instead of through a rendered tree.
|
|
9
|
+
*
|
|
10
|
+
* The module deliberately does not touch `DataTransfer`, `FileList` or `document`.
|
|
11
|
+
* The test runner has no DOM (measured: `DataTransfer`, `FileList` and `document` are
|
|
12
|
+
* all undefined, only `File` exists), and a module that reached for them could only be
|
|
13
|
+
* tested by faking the entire browser. It describes the two shapes it needs instead,
|
|
14
|
+
* and the component adapts a real event to them at the boundary. That is also what
|
|
15
|
+
* keeps the traversal order and the collision rules honest: they are pure functions of
|
|
16
|
+
* their inputs, so a test can assert them without a browser.
|
|
17
|
+
*
|
|
18
|
+
* @module @sparkelf/dsh-knowledge-base/client
|
|
19
|
+
*/
|
|
20
|
+
/** A file plus the path it should occupy under the drop target. */
|
|
21
|
+
export interface DroppedFile {
|
|
22
|
+
/** The browser's `File`, or anything carrying a name and a byte source. */
|
|
23
|
+
file: unknown;
|
|
24
|
+
/**
|
|
25
|
+
* Path relative to the directory the reader dropped onto.
|
|
26
|
+
*
|
|
27
|
+
* A single dropped file carries only its name. A file from a dropped folder carries
|
|
28
|
+
* the folder's segments too (`specs/2026/api.md`), which is what preserves the shape
|
|
29
|
+
* the reader saw in the file manager instead of flattening it.
|
|
30
|
+
*/
|
|
31
|
+
relativePath: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* What a directory row knows about itself, as far as a drop is concerned.
|
|
35
|
+
*
|
|
36
|
+
* The two flags are separate on purpose. `isDirectory` says what the row IS; `drops`
|
|
37
|
+
* says whether this surface accepts drops AT ALL right now (an upload already running,
|
|
38
|
+
* or a read-only caller). A file row is a directory the drop must ignore, while a
|
|
39
|
+
* disabled directory row is one that must also ignore it -- the reason differs but the
|
|
40
|
+
* answer does not, so `acceptsDrop()` is the single place that combines them.
|
|
41
|
+
*/
|
|
42
|
+
export interface DropTargetLike {
|
|
43
|
+
/** The directory an accepted drop uploads into, relative to the knowledge root. */
|
|
44
|
+
relativePath: string;
|
|
45
|
+
/** Whether the row names a folder rather than a document. */
|
|
46
|
+
isDirectory: boolean;
|
|
47
|
+
/** Whether this surface accepts drops at the moment (false while busy/read-only). */
|
|
48
|
+
drops: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** The minimum a drop event must expose for this module to read it. */
|
|
51
|
+
export interface DropEventLike {
|
|
52
|
+
/** Whether the event carries file payload; a text drag must not upload anything. */
|
|
53
|
+
hasFiles: boolean;
|
|
54
|
+
/** The pending file list, or undefined when the browser exposes none. */
|
|
55
|
+
files: readonly DroppedFile[] | undefined;
|
|
56
|
+
}
|
|
57
|
+
/** Why a drop produced no upload. Each case is a different thing to tell the reader. */
|
|
58
|
+
export type DropRefusal =
|
|
59
|
+
/** The pointer was over a document, or outside any drop target. */
|
|
60
|
+
'not-a-directory'
|
|
61
|
+
/** A drop target was hit but it is not accepting drops right now. */
|
|
62
|
+
| 'disabled'
|
|
63
|
+
/** The target accepted, but the drop carried no files (a text or link drag). */
|
|
64
|
+
| 'no-files';
|
|
65
|
+
/** The verdict for one drop. */
|
|
66
|
+
export type DropPlan = {
|
|
67
|
+
kind: 'upload';
|
|
68
|
+
directory: string;
|
|
69
|
+
files: DroppedFile[];
|
|
70
|
+
} | {
|
|
71
|
+
kind: 'refuse';
|
|
72
|
+
reason: DropRefusal;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Whether this row may receive the drop that is being dragged over it.
|
|
76
|
+
*
|
|
77
|
+
* Used by `dragover` to decide `preventDefault` (which is what makes the browser show
|
|
78
|
+
* "copy" and allows the drop) and whether to paint the highlight. It answers `false`
|
|
79
|
+
* for a document row so a file dragged onto a file is refused rather than silently
|
|
80
|
+
* doing nothing, and `false` while the surface is busy so a second drop cannot start
|
|
81
|
+
* a competing upload.
|
|
82
|
+
* @param target - the row under the pointer, or undefined outside any row.
|
|
83
|
+
* @returns Whether the row would accept a drop.
|
|
84
|
+
*/
|
|
85
|
+
export declare function acceptsDrop(target: DropTargetLike | undefined): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Decide what one drop should do.
|
|
88
|
+
*
|
|
89
|
+
* Kept separate from the event so the refusal cases stay distinguishable: "you dropped
|
|
90
|
+
* it on a document" and "you dragged text, not files" need different sentences, and a
|
|
91
|
+
* single `undefined` return would force the caller to guess which happened.
|
|
92
|
+
* @param target - the row under the pointer, or undefined outside any row.
|
|
93
|
+
* @param event - the drop's payload.
|
|
94
|
+
* @returns The upload to perform, or why it was refused.
|
|
95
|
+
*/
|
|
96
|
+
export declare function planDrop(target: DropTargetLike | undefined, event: DropEventLike): DropPlan;
|
|
97
|
+
/**
|
|
98
|
+
* Normalise a path relative to the drop target.
|
|
99
|
+
*
|
|
100
|
+
* Browsers report a dropped folder's contents with either separator (the `File` API on
|
|
101
|
+
* Windows reports `\`, `webkitRelativePath` uses `/`), and a leading separator would
|
|
102
|
+
* make the joined path absolute and fail the server's containment check. Both are
|
|
103
|
+
* collapsed here so the server sees one form.
|
|
104
|
+
* @param raw - the path as the browser reported it.
|
|
105
|
+
* @returns The path with forward slashes and no leading separator.
|
|
106
|
+
*/
|
|
107
|
+
export declare function normalizeDropPath(raw: string): string;
|
|
108
|
+
/**
|
|
109
|
+
* Join a drop target with one file's own path.
|
|
110
|
+
* @param directory - the target directory relative to the knowledge root, `''` for the root.
|
|
111
|
+
* @param relativePath - the file's path inside the drop, already normalised or not.
|
|
112
|
+
* @returns The target path relative to the knowledge root.
|
|
113
|
+
*/
|
|
114
|
+
export declare function dropTargetPath(directory: string, relativePath: string): string;
|
|
115
|
+
/**
|
|
116
|
+
* Collapse a dropped list to one entry per destination.
|
|
117
|
+
*
|
|
118
|
+
* Dropping the same folder twice, or a folder plus one of its files, would otherwise
|
|
119
|
+
* send two writes to the same path: the first succeeds and the second is refused as
|
|
120
|
+
* "already exists", so the reader would be shown a failure for a file that is actually
|
|
121
|
+
* there. The FIRST occurrence wins because that is the one the browser enumerated
|
|
122
|
+
* first, and its bytes are the ones the reader saw at the top of the drag.
|
|
123
|
+
* @param files - the dropped files.
|
|
124
|
+
* @returns One entry per distinct destination path, in first-seen order.
|
|
125
|
+
*/
|
|
126
|
+
export declare function dedupeDropped(files: readonly DroppedFile[]): DroppedFile[];
|
|
127
|
+
/**
|
|
128
|
+
* One line describing the outcome of a whole drop, or undefined when there is nothing
|
|
129
|
+
* worth saying (all files landed).
|
|
130
|
+
*
|
|
131
|
+
* The reader dropped N files and gets one message: naming each failure is unreadable,
|
|
132
|
+
* while "some failed" without a count is not actionable. Partial success therefore
|
|
133
|
+
* states both numbers and names the first few failures, because the next step (retry
|
|
134
|
+
* that one, rename it, ask for access) depends on WHICH file was refused.
|
|
135
|
+
* @param results - one outcome per attempted file, in order.
|
|
136
|
+
* @param maxNames - how many failing names to spell out before summarising the rest.
|
|
137
|
+
* @returns The sentence to show, or undefined when every file landed.
|
|
138
|
+
*/
|
|
139
|
+
export declare function summarizeDrop(results: readonly {
|
|
140
|
+
relativePath: string;
|
|
141
|
+
ok: boolean;
|
|
142
|
+
}[], maxNames?: number): {
|
|
143
|
+
ok: number;
|
|
144
|
+
failed: string[];
|
|
145
|
+
} | undefined;
|
|
146
|
+
/**
|
|
147
|
+
* Whether a drag event carries files this surface could upload.
|
|
148
|
+
*
|
|
149
|
+
* `types` is the only part of a `DataTransfer` readable during `dragover`: the payload
|
|
150
|
+
* itself is protected until the drop, so this is what `dragover` must ask to decide
|
|
151
|
+
* whether to `preventDefault`. An internal row drag sets `text/plain` and the knowledge
|
|
152
|
+
* base's own MIME type but NOT `Files`, which is how an in-page move stays a move.
|
|
153
|
+
* @param types - `event.dataTransfer.types`.
|
|
154
|
+
* @returns Whether the drag is an external file payload.
|
|
155
|
+
*/
|
|
156
|
+
export declare function isFileDrag(types: readonly string[] | undefined): boolean;
|
|
157
|
+
/**
|
|
158
|
+
* Read the files of a drop as the shape {@link planDrop} takes.
|
|
159
|
+
*
|
|
160
|
+
* `webkitGetAsEntry` is preferred because it is the only way a DROPPED FOLDER arrives:
|
|
161
|
+
* folders never appear in `dataTransfer.files`, so a plain file list silently drops them
|
|
162
|
+
* and the reader sees "nothing happened". The entry API must be read synchronously from
|
|
163
|
+
* the drop handler (the payload enters protected mode once the event dispatch ends), so
|
|
164
|
+
* entries are captured here first and traversed afterwards.
|
|
165
|
+
*
|
|
166
|
+
* Falls back to the flat file list when the entry API is missing or yields nothing
|
|
167
|
+
* usable, which is also the correct behaviour for a browser that only reports files.
|
|
168
|
+
* @param transfer - the drop's `dataTransfer`.
|
|
169
|
+
* @returns One entry per file, with paths relative to the drop target.
|
|
170
|
+
*/
|
|
171
|
+
export declare function readDroppedFiles(transfer: DataTransfer | undefined | null): Promise<DroppedFile[]>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The React shell around the drag-and-drop decisions.
|
|
3
|
+
*
|
|
4
|
+
* Everything that can be decided without a browser lives in `useFileDrop.ts` next to
|
|
5
|
+
* this file -- the target rules, the path joining, the de-duplication, the summary.
|
|
6
|
+
* What is left is the one thing that genuinely needs a component: which row is
|
|
7
|
+
* currently painted as the drop target.
|
|
8
|
+
*
|
|
9
|
+
* Kept in its own module for two reasons. The pure module must stay importable from a
|
|
10
|
+
* test that has no DOM (measured: the runner has `File`, but no `DataTransfer`,
|
|
11
|
+
* `FileList` or `document`), and importing React into it would make that impossible.
|
|
12
|
+
* And the component wiring is the part that has to wait for the in-flight edits to
|
|
13
|
+
* `KnowledgeFileTree.tsx` / `KnowledgeHome.tsx` to land, so it is kept separate rather
|
|
14
|
+
* than half-merged.
|
|
15
|
+
*
|
|
16
|
+
* @module @sparkelf/dsh-knowledge-base/client
|
|
17
|
+
*/
|
|
18
|
+
import type { DropEventLike, DropPlan, DropTargetLike } from './useFileDrop.ts';
|
|
19
|
+
/** What a component gets back from {@link useFileDrop}. */
|
|
20
|
+
export interface FileDropHandle {
|
|
21
|
+
/** The path of the row that should paint its drop highlight, or undefined. */
|
|
22
|
+
highlighted: string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Call from a row's `onDragOver`.
|
|
25
|
+
*
|
|
26
|
+
* Returns whether the row accepts this drag, which the caller uses to decide
|
|
27
|
+
* `event.preventDefault()`: without it the browser refuses the drop outright.
|
|
28
|
+
* @param target - the row under the pointer.
|
|
29
|
+
* @returns Whether the row would accept the drop.
|
|
30
|
+
*/
|
|
31
|
+
onDragOver: (target: DropTargetLike | undefined) => boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Call from a row's `onDragEnter`.
|
|
34
|
+
*
|
|
35
|
+
* @param target - the row the pointer entered.
|
|
36
|
+
* @returns Whether the row would accept the drop.
|
|
37
|
+
*/
|
|
38
|
+
onDragEnter: (target: DropTargetLike | undefined) => boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Call from a row's `onDragLeave`, and from the surface's own `onDragLeave` for the
|
|
41
|
+
* case where the pointer left the panel entirely.
|
|
42
|
+
* @param target - the row the pointer left, if any.
|
|
43
|
+
*/
|
|
44
|
+
onDragLeave: (target?: DropTargetLike | undefined) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Call from a row's `onDrop`. Clears the highlight and returns what to upload.
|
|
47
|
+
* @param target - the row the pointer was over.
|
|
48
|
+
* @param event - the drop payload.
|
|
49
|
+
* @returns The upload to perform, or why nothing happens.
|
|
50
|
+
*/
|
|
51
|
+
onDrop: (target: DropTargetLike | undefined, event: DropEventLike) => DropPlan;
|
|
52
|
+
/** Clear the highlight without a drop (Escape, or the drag leaving the window). */
|
|
53
|
+
reset: () => void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Track the drag state for one drop surface.
|
|
57
|
+
*
|
|
58
|
+
* Note the enter/leave counter. A drag crossing from a row onto its own child fires
|
|
59
|
+
* `dragleave` on the parent BEFORE `dragenter` on the child, so clearing the highlight
|
|
60
|
+
* on every `dragleave` makes it flicker off and back on. Counting enter/leave pairs is
|
|
61
|
+
* what keeps it stable; the counter reaching zero is the real exit.
|
|
62
|
+
* @returns The handle a component wires into its rows.
|
|
63
|
+
*/
|
|
64
|
+
export declare function useFileDrop(): FileDropHandle;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background extraction of a document's Markdown sibling.
|
|
3
|
+
*
|
|
4
|
+
* Upload and extraction are two different promises. The bytes are the uploader's
|
|
5
|
+
* deliverable and must be visible the moment the write lands; the `.md` sibling is
|
|
6
|
+
* derived text that may take a minute (a scanned PDF goes through MinerU, a legacy
|
|
7
|
+
* `.doc` through LibreOffice). Doing both inside the upload request made a slow
|
|
8
|
+
* parse look like a slow upload -- and a failed parse look like a failed upload.
|
|
9
|
+
*
|
|
10
|
+
* This queue separates them. `enqueue` never blocks and never throws: a failed
|
|
11
|
+
* extraction is a status, not an upload error. The queue lives in this process only
|
|
12
|
+
* (see {@link MAX_FINISHED_TASKS}) because the file itself is already durable; a
|
|
13
|
+
* restart loses the bookkeeping, and the manual `extract.retry` route is the recovery
|
|
14
|
+
* path. Persisting a queue would add a second source of truth about files that the
|
|
15
|
+
* filesystem already answers for.
|
|
16
|
+
*
|
|
17
|
+
* @module @sparkelf/dsh-knowledge-base/extractionQueue
|
|
18
|
+
*/
|
|
19
|
+
/** Lifecycle of one extraction task. */
|
|
20
|
+
export type ExtractionState = 'pending' | 'running' | 'succeeded' | 'failed' | 'unsupported';
|
|
21
|
+
/** One extraction task, as a reader observes it. */
|
|
22
|
+
export interface ExtractionTask {
|
|
23
|
+
/** Knowledge-relative path of the SOURCE document. */
|
|
24
|
+
relPath: string;
|
|
25
|
+
/** Where the task is in its lifecycle. */
|
|
26
|
+
state: ExtractionState;
|
|
27
|
+
/** When the task was accepted, ISO-8601. */
|
|
28
|
+
queuedAt: string;
|
|
29
|
+
/** When the run started, ISO-8601. */
|
|
30
|
+
startedAt?: string;
|
|
31
|
+
/** When the run reached a terminal state, ISO-8601. */
|
|
32
|
+
finishedAt?: string;
|
|
33
|
+
/** The Markdown sibling that was written, when one was. */
|
|
34
|
+
markdown?: string;
|
|
35
|
+
/** Which authority produced the text (`mineru`, `libreoffice`, `python`, `native`). */
|
|
36
|
+
via?: string;
|
|
37
|
+
/** Why the extraction produced nothing, verbatim from the extractor. */
|
|
38
|
+
reason?: string;
|
|
39
|
+
}
|
|
40
|
+
/** What one run reports back to the queue. */
|
|
41
|
+
export interface ExtractionOutcome {
|
|
42
|
+
/** Terminal state for this run. */
|
|
43
|
+
state: Exclude<ExtractionState, 'pending' | 'running'>;
|
|
44
|
+
/** The written sibling, when the run produced one. */
|
|
45
|
+
markdown?: string;
|
|
46
|
+
/** The authority that produced the text. */
|
|
47
|
+
via?: string;
|
|
48
|
+
/** Why nothing was produced. */
|
|
49
|
+
reason?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* How many extractions may run at once.
|
|
53
|
+
*
|
|
54
|
+
* One, deliberately. Every authority this drives is expensive in a way a second
|
|
55
|
+
* concurrent run makes worse: LibreOffice spawns a headless process per conversion and
|
|
56
|
+
* MinerU parses on the same small box the workspace already shares. Serializing also
|
|
57
|
+
* makes the observable order the submission order, which is what a reader retrying one
|
|
58
|
+
* file expects.
|
|
59
|
+
*/
|
|
60
|
+
export declare const MAX_CONCURRENT_EXTRACTIONS = 1;
|
|
61
|
+
/**
|
|
62
|
+
* Most finished tasks remembered for status reads.
|
|
63
|
+
*
|
|
64
|
+
* The map is bookkeeping, not storage: an evicted entry loses only its status line,
|
|
65
|
+
* never a file. Bounded so a long-lived process does not accumulate one record per
|
|
66
|
+
* upload it ever saw. Running and pending tasks are never evicted.
|
|
67
|
+
*/
|
|
68
|
+
export declare const MAX_FINISHED_TASKS = 200;
|
|
69
|
+
/** What the queue needs from the surrounding plugin, injected so it stays testable. */
|
|
70
|
+
export interface ExtractionQueueOptions {
|
|
71
|
+
/**
|
|
72
|
+
* Run one extraction.
|
|
73
|
+
*
|
|
74
|
+
* Supplied by the plugin so this module owns scheduling only, not the extraction
|
|
75
|
+
* itself: the caller keeps the permission-checked `extractBeside` path in one place.
|
|
76
|
+
* @param relPath - the source document.
|
|
77
|
+
* @param mineruBaseUrl - the caller's own parser address, when set.
|
|
78
|
+
* @returns the terminal state of the run.
|
|
79
|
+
*/
|
|
80
|
+
run: (relPath: string, mineruBaseUrl: string | undefined) => Promise<ExtractionOutcome>;
|
|
81
|
+
/**
|
|
82
|
+
* Whether this path can be extracted at all.
|
|
83
|
+
*
|
|
84
|
+
* Asked before queueing so an unsupported file answers immediately rather than
|
|
85
|
+
* occupying the single worker slot to discover it has no reader.
|
|
86
|
+
*/
|
|
87
|
+
isExtractable: (relPath: string) => boolean;
|
|
88
|
+
}
|
|
89
|
+
/** One extraction queue. One instance per plugin registration. */
|
|
90
|
+
export interface ExtractionQueue {
|
|
91
|
+
/**
|
|
92
|
+
* Accept a source document for background extraction.
|
|
93
|
+
*
|
|
94
|
+
* Never throws and never waits: the upload that calls this has already written its
|
|
95
|
+
* bytes, and nothing about a parse may change that answer. A repeat for the same
|
|
96
|
+
* path while one is still pending or running returns the existing task instead of
|
|
97
|
+
* queueing a second run.
|
|
98
|
+
* @param relPath - the source document, relative to the knowledge root.
|
|
99
|
+
* @param mineruBaseUrl - the caller's own parser address, when set.
|
|
100
|
+
* @returns the task as it stands right after the call.
|
|
101
|
+
*/
|
|
102
|
+
enqueue: (relPath: string, mineruBaseUrl?: string) => ExtractionTask;
|
|
103
|
+
/**
|
|
104
|
+
* Read one task.
|
|
105
|
+
* @param relPath - the source document.
|
|
106
|
+
* @returns the task, or undefined when this process never saw it.
|
|
107
|
+
*/
|
|
108
|
+
get: (relPath: string) => ExtractionTask | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Snapshot the queue for a status surface.
|
|
111
|
+
* @returns every remembered task plus what is running.
|
|
112
|
+
*/
|
|
113
|
+
snapshot: () => {
|
|
114
|
+
running: string | undefined;
|
|
115
|
+
size: number;
|
|
116
|
+
tasks: ExtractionTask[];
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Wait for one path's task to reach a terminal state.
|
|
120
|
+
*
|
|
121
|
+
* The manual-retry route waits on its own document, and it must wait on the TASK
|
|
122
|
+
* rather than on `drain()`. Two reasons, both about there being one truth:
|
|
123
|
+
*
|
|
124
|
+
* 1. `drain()` also waits for unrelated uploads queued ahead, so a retry would
|
|
125
|
+
* report a delay that has nothing to do with this file.
|
|
126
|
+
* 2. The retry must NOT run its own extraction and then enqueue a second copy to
|
|
127
|
+
* keep the status surface current. That is two runs of one document, and an
|
|
128
|
+
* answer built from one run while `extract.status` reports the other -- the
|
|
129
|
+
* exact split this method exists to remove.
|
|
130
|
+
*
|
|
131
|
+
* Resolves with the terminal task, or immediately with `undefined` when this
|
|
132
|
+
* process never queued that path (a restart, or a path it has not seen). Never
|
|
133
|
+
* rejects: the caller is a route, and the queue is not allowed to become an error
|
|
134
|
+
* path for an upload.
|
|
135
|
+
* @param relPath - the source document.
|
|
136
|
+
* @returns the terminal task, or undefined when the path was never queued.
|
|
137
|
+
*/
|
|
138
|
+
settled: (relPath: string) => Promise<ExtractionTask | undefined>;
|
|
139
|
+
/**
|
|
140
|
+
* Wait for the queue to go idle.
|
|
141
|
+
*
|
|
142
|
+
* For tests and for a caller that asked to wait on one specific run; a reader never
|
|
143
|
+
* needs it. Resolves when nothing is pending or running.
|
|
144
|
+
* @returns when the worker has drained.
|
|
145
|
+
*/
|
|
146
|
+
drain: () => Promise<void>;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Create an extraction queue.
|
|
150
|
+
*
|
|
151
|
+
* @param options - the runner and the predicate that decides whether a path has a reader.
|
|
152
|
+
* @returns a queue that runs at most {@link MAX_CONCURRENT_EXTRACTIONS} task at a time.
|
|
153
|
+
*/
|
|
154
|
+
export declare function createExtractionQueue(options: ExtractionQueueOptions): ExtractionQueue;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { type KnowledgeConfig, type KnowledgeGrant, type KnowledgeGrantRequest,
|
|
|
24
24
|
import { type KnowledgeStorageSettings, type ViewDelta } from './views.ts';
|
|
25
25
|
import { type DeployPlanContext, type MineruBackend, type MineruTier } from './deploy.ts';
|
|
26
26
|
import { type RemoteHost, type RemoteRunner } from './remote.ts';
|
|
27
|
+
import { type ExtractionOutcome } from './extractionQueue.ts';
|
|
27
28
|
/** Cordis plugin name. */
|
|
28
29
|
export declare const name = "dsh-knowledge-base";
|
|
29
30
|
/**
|
|
@@ -363,6 +364,21 @@ export interface KnowledgeEntry {
|
|
|
363
364
|
readable?: boolean;
|
|
364
365
|
/** Number of direct children, for a directory row. */
|
|
365
366
|
childCount?: number;
|
|
367
|
+
/**
|
|
368
|
+
* Whether a reader exists for this file's format.
|
|
369
|
+
*
|
|
370
|
+
* Set on file rows only. The page uses it to decide whether "extract markdown" is a
|
|
371
|
+
* meaningful action for this row, so the menu does not have to know the extractor's
|
|
372
|
+
* extension list.
|
|
373
|
+
*/
|
|
374
|
+
canExtract?: boolean;
|
|
375
|
+
/**
|
|
376
|
+
* Whether this file's `.md` sibling already exists.
|
|
377
|
+
*
|
|
378
|
+
* Set on file rows only, and only for formats that have an extractor. Lets the page
|
|
379
|
+
* say "already extracted" rather than offering a retry that would be refused.
|
|
380
|
+
*/
|
|
381
|
+
hasMarkdown?: boolean;
|
|
366
382
|
}
|
|
367
383
|
/**
|
|
368
384
|
* List one directory level with each child's modification time.
|
|
@@ -445,6 +461,18 @@ export interface PrincipalOption {
|
|
|
445
461
|
id: string;
|
|
446
462
|
/** Human-facing name, falling back to the id when the backend sends none. */
|
|
447
463
|
label: string;
|
|
464
|
+
/** Login name, for users only: two accounts can share a display name. */
|
|
465
|
+
username?: string;
|
|
466
|
+
/**
|
|
467
|
+
* Role codes this principal belongs to.
|
|
468
|
+
*
|
|
469
|
+
* Users only, and only when the directory reported it. The permission overview needs
|
|
470
|
+
* this to answer "what does this PERSON have": a role grant confers access on every
|
|
471
|
+
* member, so without the membership a reader whose access is entirely role-derived
|
|
472
|
+
* would look like they hold nothing. Absent means the directory did not say -- which
|
|
473
|
+
* the page reports as unavailable inheritance rather than as no inheritance.
|
|
474
|
+
*/
|
|
475
|
+
roleCodes?: string[];
|
|
448
476
|
}
|
|
449
477
|
/**
|
|
450
478
|
* Read the principals of one kind from the backend.
|
|
@@ -666,6 +694,31 @@ export declare function extractBeside(config: Config, relPath: string, mineruUrl
|
|
|
666
694
|
via?: string;
|
|
667
695
|
reason?: string;
|
|
668
696
|
} | undefined>;
|
|
697
|
+
/**
|
|
698
|
+
* Translate {@link extractBeside}'s answer into a queue terminal state.
|
|
699
|
+
*
|
|
700
|
+
* The extractor reports three different outcomes through two fields, and they mean
|
|
701
|
+
* different things to a reader deciding what to do next:
|
|
702
|
+
*
|
|
703
|
+
* - `markdown` set, no `reason` — the sibling was written (`succeeded`).
|
|
704
|
+
* - `markdown` set WITH a `reason` — the text was produced but writing the sibling was
|
|
705
|
+
* refused, typically because it already exists. The reason is kept so a retry can
|
|
706
|
+
* report why nothing changed (`failed`, with the refusal verbatim).
|
|
707
|
+
* - `markdown` empty, `reason` set — no authority could read the document; this is the
|
|
708
|
+
* `no-extractor-succeeded: …` case a reader retries after an upgrade.
|
|
709
|
+
*
|
|
710
|
+
* An `undefined` answer means the format has no extractor at all, which the queue
|
|
711
|
+
* normally prevents by asking `isExtractable` first; it is mapped for the caller that
|
|
712
|
+
* bypassed that gate rather than left to throw.
|
|
713
|
+
*
|
|
714
|
+
* @param outcome - what {@link extractBeside} returned.
|
|
715
|
+
* @returns the state the queue records.
|
|
716
|
+
*/
|
|
717
|
+
export declare function extractionOutcomeOf(outcome: {
|
|
718
|
+
markdown: string;
|
|
719
|
+
via?: string;
|
|
720
|
+
reason?: string;
|
|
721
|
+
} | undefined): ExtractionOutcome;
|
|
669
722
|
/**
|
|
670
723
|
* MinerU's base URL for this deployment, or undefined when it is not configured.
|
|
671
724
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sparkelf/dsh-knowledge-base",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "DataOps-backed knowledge base for DeepSeek Harness: a left-sidebar destination, Settings configuration, and index tooling over a mounted NAS view.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|