@sparkelf/dsh-knowledge-base 0.0.0-stage → 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/LICENSE +21 -0
- package/SKILL.md +147 -0
- package/cordis.patch.yml +5 -0
- package/i18n/zh.json +10 -0
- package/lib/client.js +18308 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +6005 -0
- package/lib/types/api.d.ts +620 -0
- package/lib/types/client/KnowledgeAudit.d.ts +39 -0
- package/lib/types/client/KnowledgeContextMenu.d.ts +121 -0
- package/lib/types/client/KnowledgeCreateDialog.d.ts +134 -0
- package/lib/types/client/KnowledgeFileTree.d.ts +117 -0
- package/lib/types/client/KnowledgeHome.d.ts +102 -0
- package/lib/types/client/KnowledgeIndexing.d.ts +60 -0
- package/lib/types/client/KnowledgeListing.d.ts +174 -0
- package/lib/types/client/KnowledgeLoading.d.ts +41 -0
- package/lib/types/client/KnowledgeMenuIcon.d.ts +27 -0
- package/lib/types/client/KnowledgePanel.d.ts +58 -0
- package/lib/types/client/KnowledgePermissionOverview.d.ts +147 -0
- package/lib/types/client/KnowledgePermissions.d.ts +47 -0
- package/lib/types/client/KnowledgeProgressPanel.d.ts +53 -0
- package/lib/types/client/KnowledgeReferencePicker.d.ts +81 -0
- package/lib/types/client/KnowledgeSearch.d.ts +83 -0
- package/lib/types/client/KnowledgeSectionHeader.d.ts +71 -0
- package/lib/types/client/KnowledgeSelectionBar.d.ts +45 -0
- package/lib/types/client/KnowledgeSettings.d.ts +96 -0
- package/lib/types/client/KnowledgeSettingsSection.d.ts +21 -0
- package/lib/types/client/KnowledgeShareDialog.d.ts +70 -0
- package/lib/types/client/KnowledgeTemplates.d.ts +43 -0
- package/lib/types/client/KnowledgeTrash.d.ts +62 -0
- package/lib/types/client/MenuGlyphs.d.ts +37 -0
- package/lib/types/client/auditPaging.d.ts +40 -0
- package/lib/types/client/departmentFolders.d.ts +57 -0
- package/lib/types/client/deployTarget.d.ts +34 -0
- package/lib/types/client/index.d.ts +40 -0
- package/lib/types/client/insertReadiness.d.ts +60 -0
- package/lib/types/client/knowledgeMenuCopy.d.ts +23 -0
- package/lib/types/client/locales.d.ts +505 -0
- package/lib/types/client/openInSidebar.d.ts +62 -0
- package/lib/types/client/ownerLabel.d.ts +39 -0
- package/lib/types/client/paging.d.ts +69 -0
- package/lib/types/client/permissionRows.d.ts +265 -0
- package/lib/types/client/progressRules.d.ts +54 -0
- package/lib/types/client/referenceTargets.d.ts +60 -0
- package/lib/types/client/roleSummary.d.ts +47 -0
- package/lib/types/client/saveAudit.d.ts +87 -0
- package/lib/types/client/shareRelations.d.ts +81 -0
- package/lib/types/client/useFavorites.d.ts +49 -0
- package/lib/types/client/useFileDrop.d.ts +171 -0
- package/lib/types/client/useFileDropHook.d.ts +64 -0
- package/lib/types/client/usePinnedPaths.d.ts +39 -0
- package/lib/types/client/useTreeState.d.ts +46 -0
- package/lib/types/client/useViewMode.d.ts +39 -0
- package/lib/types/deploy.d.ts +209 -0
- package/lib/types/extract.d.ts +144 -0
- package/lib/types/extractionQueue.d.ts +154 -0
- package/lib/types/index.d.ts +899 -0
- package/lib/types/permissions.d.ts +237 -0
- package/lib/types/protocol.d.ts +229 -0
- package/lib/types/remote.d.ts +105 -0
- package/lib/types/trash.d.ts +142 -0
- package/lib/types/viewMetadata.d.ts +20 -0
- package/lib/types/views.d.ts +923 -0
- package/package.json +109 -4
- package/skills/kb-organize/SKILL.md +365 -0
- package/skills/kb-organize/i18n/zh.json +10 -0
- package/README.md +0 -3
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materialize each user's visible knowledge tree as a hardlink view.
|
|
3
|
+
*
|
|
4
|
+
* A grant in the DataOps database is a statement about what a principal MAY read;
|
|
5
|
+
* it is not a filesystem fact. A workspace process that mounts the shared export
|
|
6
|
+
* sees every file in it, so the grant has to be turned into an actual directory
|
|
7
|
+
* tree before it can mean anything: one directory per user, holding hardlinks to
|
|
8
|
+
* exactly the files that user may read. Nothing crosses into a view whose grant
|
|
9
|
+
* was not resolved by DataOps, so this module never decides who may see what --
|
|
10
|
+
* it only asks, and links what the answer allows.
|
|
11
|
+
*
|
|
12
|
+
* Hardlinks rather than copies: the link shares the inode, so a revoke unlinks
|
|
13
|
+
* the view's entry and the source is untouched, and a re-grant costs one link
|
|
14
|
+
* rather than a copy of the file. That is what makes permission changes
|
|
15
|
+
* incremental, and it is why the NFS export has to be one filesystem (a hardlink
|
|
16
|
+
* cannot span mounts).
|
|
17
|
+
*
|
|
18
|
+
* @module @sparkelf/dsh-knowledge-base/views
|
|
19
|
+
*/
|
|
20
|
+
/** One path a user may read, as DataOps resolved it. */
|
|
21
|
+
export interface ResolvedPath {
|
|
22
|
+
/** Path relative to the authoritative root. */
|
|
23
|
+
relPath: string;
|
|
24
|
+
}
|
|
25
|
+
/** Which layout the listing uses for this caller. */
|
|
26
|
+
export type ViewMode = 'list' | 'grid' | 'explorer';
|
|
27
|
+
/** What one user's view currently holds. */
|
|
28
|
+
export interface ViewDelta {
|
|
29
|
+
/** Paths linked into the view during this run. */
|
|
30
|
+
linked: string[];
|
|
31
|
+
/** Paths unlinked from the view during this run. */
|
|
32
|
+
unlinked: string[];
|
|
33
|
+
/** Paths left alone because the view already matched. */
|
|
34
|
+
unchanged: number;
|
|
35
|
+
/**
|
|
36
|
+
* Directories removed because a revoke emptied them.
|
|
37
|
+
*
|
|
38
|
+
* Kept apart from `unlinked` because a leftover empty directory is not a stale
|
|
39
|
+
* file: it still discloses a folder name the user has lost access to.
|
|
40
|
+
*/
|
|
41
|
+
pruned: string[];
|
|
42
|
+
/**
|
|
43
|
+
* Set when the prune pass was deliberately skipped, with the reason.
|
|
44
|
+
*
|
|
45
|
+
* Reported rather than logged so a reader can see it: "the view is not being
|
|
46
|
+
* cleaned because nothing resolved this round" is a fact about the deployment,
|
|
47
|
+
* and the alternative -- silently keeping stale links -- would be indistinguishable
|
|
48
|
+
* from a view that is correct.
|
|
49
|
+
*/
|
|
50
|
+
pruneSkipped?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface ViewOptions {
|
|
53
|
+
/** Directory holding the authoritative files. */
|
|
54
|
+
actualRoot: string;
|
|
55
|
+
/** Directory holding one view per user. */
|
|
56
|
+
viewRoot: string;
|
|
57
|
+
/** Opaque user key; a directory name, so it must not contain a separator. */
|
|
58
|
+
userId: string;
|
|
59
|
+
/**
|
|
60
|
+
* Department code used to select the `department/` subtree, when the caller has one.
|
|
61
|
+
*
|
|
62
|
+
* Layout only: it decides which department bucket is linked, never whether a path
|
|
63
|
+
* may be read. Without it the view simply has no department bucket.
|
|
64
|
+
*/
|
|
65
|
+
departmentCode?: string;
|
|
66
|
+
/**
|
|
67
|
+
* The caller's organization path, root first, as DataOps reported it.
|
|
68
|
+
*
|
|
69
|
+
* This is what turns the department bucket into nested folders: the view mirrors
|
|
70
|
+
* `department/<ancestor>/.../<own>/file` so a reader opening 「部门」 walks their
|
|
71
|
+
* own reporting line instead of one flat pile of every department's documents.
|
|
72
|
+
*
|
|
73
|
+
* Absent from an older backend that reports only `departmentCode`. The view then
|
|
74
|
+
* falls back to the single-level layout, which is exactly what it used to be, so
|
|
75
|
+
* an un-upgraded backend degrades to the old shape rather than to an empty bucket.
|
|
76
|
+
*/
|
|
77
|
+
departmentPathCodes?: readonly string[];
|
|
78
|
+
/**
|
|
79
|
+
* Paths this caller has removed from their view, in view coordinates.
|
|
80
|
+
*
|
|
81
|
+
* A filter, not a permission: a hidden path keeps its grant, its inode and every
|
|
82
|
+
* other caller's link. It is checked while building the view because a view is
|
|
83
|
+
* hardlinks into one tree, so "remove" cannot mean "unlink" without also undoing
|
|
84
|
+
* a grant on the next sync.
|
|
85
|
+
*/
|
|
86
|
+
hidden?: readonly string[];
|
|
87
|
+
}
|
|
88
|
+
/** A resolved set linked into a user's view. */
|
|
89
|
+
export interface ViewSyncOptions extends ViewOptions {
|
|
90
|
+
/**
|
|
91
|
+
* Whether `resolved` is the complete answer for this caller.
|
|
92
|
+
*
|
|
93
|
+
* True when the caller enumerated the whole tree (the routine and full syncs), and
|
|
94
|
+
* false when it asked about specific paths (a write verifying its own result).
|
|
95
|
+
* Only a full scan may apply the prune-refusal rule below: a targeted sync that
|
|
96
|
+
* resolved nothing knows nothing about the rest of the tree, so it must never prune
|
|
97
|
+
* either -- but it also must not be mistaken for evidence that everything failed.
|
|
98
|
+
*/
|
|
99
|
+
fullScan?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Whether an empty `resolved` may be read as "nothing is readable".
|
|
102
|
+
*
|
|
103
|
+
* Default true: an empty set prunes, which is what a real revoke looks like and what
|
|
104
|
+
* every existing caller means. A caller that enumerated the tree and saw every
|
|
105
|
+
* resolve fail passes false, because an empty answer there is a failure rather than
|
|
106
|
+
* a permission decision, and must not delete the reader's links.
|
|
107
|
+
*/
|
|
108
|
+
trustResolvedSet?: boolean;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The three top-level buckets every view exposes, in display order.
|
|
112
|
+
*
|
|
113
|
+
* A curated shared store needs one visible answer to "where does this document
|
|
114
|
+
* live" before it can have a delete policy: what everyone may read, what the
|
|
115
|
+
* caller's department may read, and what only the caller may read are three
|
|
116
|
+
* different audiences, and mixing them puts a department document beside a
|
|
117
|
+
* company-wide one with no way to tell them apart.
|
|
118
|
+
*/
|
|
119
|
+
/**
|
|
120
|
+
* File name of a caller's trash record inside their view directory.
|
|
121
|
+
*
|
|
122
|
+
* Named here rather than imported from `trash.ts` so the view builder does not
|
|
123
|
+
* depend on the trash module: the builder only needs to know not to prune it.
|
|
124
|
+
*/
|
|
125
|
+
export declare const TRASH_RECORD_NAME = ".kb-trash.json";
|
|
126
|
+
/**
|
|
127
|
+
* Every bookkeeping file this plugin writes into a caller's view directory.
|
|
128
|
+
*
|
|
129
|
+
* The view directory is the directory the reader browses, so a state file left in it
|
|
130
|
+
* is a file the explorer will offer to open, rename or delete. The NAMES live in
|
|
131
|
+
* `viewMetadata.ts` because the client needs them too and cannot import this module
|
|
132
|
+
* (`node:fs/promises` is not available in a browser bundle).
|
|
133
|
+
*
|
|
134
|
+
* The list is cross-checked against the `*PathFor` helpers below at module load: a
|
|
135
|
+
* renamed state file then fails loudly here instead of quietly appearing in every
|
|
136
|
+
* reader's tree, which is how `.kb-favorites.json` reached it once.
|
|
137
|
+
*/
|
|
138
|
+
export declare const VIEW_METADATA_NAMES: readonly string[];
|
|
139
|
+
export declare const VIEW_BUCKETS: readonly ["global", "department", "personal"];
|
|
140
|
+
/** One bucket a view can hold. */
|
|
141
|
+
export type ViewBucket = typeof VIEW_BUCKETS[number];
|
|
142
|
+
/** Display name of each bucket, keyed for the client's locale files. */
|
|
143
|
+
export declare const VIEW_BUCKET_LABELS: Readonly<Record<ViewBucket, string>>;
|
|
144
|
+
/**
|
|
145
|
+
* The caller's organization path, cleaned and made safe to use as folder names.
|
|
146
|
+
*
|
|
147
|
+
* Three inputs can produce an empty answer, and all three mean "no hierarchy to
|
|
148
|
+
* render": no path at all (an older backend), an empty path, or a path whose codes
|
|
149
|
+
* are not ordinary path segments. The last one matters because the codes come from
|
|
150
|
+
* another system: a code carrying `/` or `..` would otherwise place a directory
|
|
151
|
+
* outside the view, which is the one thing this module must not allow. Rejecting
|
|
152
|
+
* the whole path rather than the offending segment keeps the layout all-or-nothing,
|
|
153
|
+
* so a caller never sees half a hierarchy that reads as a real one.
|
|
154
|
+
*
|
|
155
|
+
* @param codes - `departmentPathCodes` as DataOps reported it.
|
|
156
|
+
* @returns The usable codes, root first, or an empty array.
|
|
157
|
+
*/
|
|
158
|
+
export declare function usableDepartmentPath(codes: readonly string[] | undefined): string[];
|
|
159
|
+
/**
|
|
160
|
+
* Which bucket a resolved path belongs to, and where it lands inside that bucket.
|
|
161
|
+
*
|
|
162
|
+
* The authoritative root carries the same three names as ordinary directories, so
|
|
163
|
+
* ownership is a fact of where the file lives rather than a per-file label that
|
|
164
|
+
* could disagree with it. Legacy flat files (written before the split) answer
|
|
165
|
+
* `global`, which keeps a view populated while a migration is still in progress.
|
|
166
|
+
*
|
|
167
|
+
* `global` owns no second segment, so only its own name is dropped. `department`
|
|
168
|
+
* and `personal` name an OWNER and drop it: a caller opening 「部门」 expects their
|
|
169
|
+
* department's documents inside it, not nested under a second copy of the code.
|
|
170
|
+
*
|
|
171
|
+
* The department bucket's DEPTH depends on the caller, so this function takes the
|
|
172
|
+
* caller's organization path. Everything below the caller's own department keeps
|
|
173
|
+
* its sub-directories, which is what makes the bucket read as a reporting line
|
|
174
|
+
* rather than a flat pile.
|
|
175
|
+
*
|
|
176
|
+
* @param relPath - path relative to the authoritative root.
|
|
177
|
+
* @param departmentPathCodes - the caller's usable organization path, root first.
|
|
178
|
+
* Absent or empty keeps the legacy single-level department layout.
|
|
179
|
+
* @returns The bucket and the path to link inside it, or undefined when the path
|
|
180
|
+
* cannot be placed safely.
|
|
181
|
+
*/
|
|
182
|
+
export declare function bucketOf(relPath: string, departmentPathCodes?: readonly string[]): {
|
|
183
|
+
bucket: ViewBucket;
|
|
184
|
+
inner: string;
|
|
185
|
+
} | undefined;
|
|
186
|
+
/**
|
|
187
|
+
* Whether a `department/…` path belongs to the caller's own department.
|
|
188
|
+
*
|
|
189
|
+
* The department segment is compared exactly, so `OPS` does not match `OPS2`. A
|
|
190
|
+
* caller with no department matches nothing: their view gets no department bucket
|
|
191
|
+
* rather than every department a stray grant happens to mention.
|
|
192
|
+
*
|
|
193
|
+
* With an organization path there are two shapes a document of the caller's own
|
|
194
|
+
* department can have, and both are accepted because both exist on a live share:
|
|
195
|
+
*
|
|
196
|
+
* - **Nested**, `department/<ancestor>/.../<own>/<something>`: the whole chain must
|
|
197
|
+
* match in order. Comparing the chain rather than the leaf alone is what keeps a
|
|
198
|
+
* peer out, since a peer that merely shares the root (`A/B` vs `A/C`) would pass a
|
|
199
|
+
* leaf-only test.
|
|
200
|
+
* - **Legacy flat**, `department/<own>/<something>`: written before the hierarchy
|
|
201
|
+
* existed. The caller's own code is at the first position instead of the last, so it
|
|
202
|
+
* is checked separately; refusing it would make every pre-upgrade document vanish.
|
|
203
|
+
*
|
|
204
|
+
* Both shapes need at least one segment BELOW the department, because the department
|
|
205
|
+
* folder itself is a container, not a document.
|
|
206
|
+
*
|
|
207
|
+
* @param relPath - authoritative path, already known to start with `department/`.
|
|
208
|
+
* @param departmentCode - the caller's department code, when DataOps reported one.
|
|
209
|
+
* @param departmentPathCodes - the caller's organization path, root first, when the
|
|
210
|
+
* backend reported one. Falls back to {@link departmentCode} when absent.
|
|
211
|
+
* @returns true when the path sits under the caller's own department.
|
|
212
|
+
*/
|
|
213
|
+
export declare function departmentPrefixMatches(relPath: string, departmentCode: string | undefined, departmentPathCodes?: readonly string[]): boolean;
|
|
214
|
+
/**
|
|
215
|
+
* Whether a user key is safe to use as one directory name.
|
|
216
|
+
*
|
|
217
|
+
* The key comes from the identity provider rather than from this plugin, so it is
|
|
218
|
+
* validated before it reaches the filesystem: a value carrying a separator or a
|
|
219
|
+
* parent reference would place the view outside the view root, which is the one
|
|
220
|
+
* thing this module must not allow.
|
|
221
|
+
* @param userId - the candidate key.
|
|
222
|
+
* @returns true when it is a single ordinary path segment.
|
|
223
|
+
*/ export declare function isSafeUserId(userId: string): boolean;
|
|
224
|
+
/**
|
|
225
|
+
* The directory one user's view lives in.
|
|
226
|
+
* @param options - the roots and the user key.
|
|
227
|
+
* @returns The absolute view directory.
|
|
228
|
+
*/
|
|
229
|
+
/**
|
|
230
|
+
* Whether a path is a mount point.
|
|
231
|
+
*
|
|
232
|
+
* Read from `/proc/self/mountinfo` rather than inferred from `stat`: comparing a
|
|
233
|
+
* directory's device against its parent's reports a bind mount of the SAME
|
|
234
|
+
* filesystem as "not a mount point", and the whole point of this check is to catch
|
|
235
|
+
* the deployment where the share IS the path being retargeted. The file is the
|
|
236
|
+
* kernel's own list, so a path present in it is mounted by definition.
|
|
237
|
+
*
|
|
238
|
+
* Field 5 of each line is the mount point, escaped (`\040` for a space). A path
|
|
239
|
+
* that is not absolute, or a kernel without the file, answers false: the remaining
|
|
240
|
+
* guards in {@link pointMountAtView} still refuse a non-empty directory, so this
|
|
241
|
+
* check failing open cannot on its own authorize a destructive replace.
|
|
242
|
+
* @param path - absolute path to test.
|
|
243
|
+
* @returns true when the kernel lists it as a mount point.
|
|
244
|
+
*/
|
|
245
|
+
export declare function isMountPoint(path: string): Promise<boolean>;
|
|
246
|
+
/**
|
|
247
|
+
* Point the browsable mount at one user's view.
|
|
248
|
+
*
|
|
249
|
+
* The view is built beside the authoritative tree, but the page browses
|
|
250
|
+
* `mountPoint`. Without this step every user would browse the same directory and a
|
|
251
|
+
* grant would change nothing they can see: the view would be correct and
|
|
252
|
+
* unreachable at the same time.
|
|
253
|
+
*
|
|
254
|
+
* A symlink is the mechanism because the browsable path is a fixed path the caller
|
|
255
|
+
* configured, while the view is per user.
|
|
256
|
+
*
|
|
257
|
+
* This function REPLACES whatever sits at `mountPoint`, so it refuses two shapes
|
|
258
|
+
* outright rather than removing them:
|
|
259
|
+
*
|
|
260
|
+
* - a **mount point**, because removing it would delete the mounted filesystem's
|
|
261
|
+
* contents on the server. Measured on 2026-10-17: a deployment whose
|
|
262
|
+
* `mountPoint` was the NFS root had `actual/` and every user's `views/` deleted
|
|
263
|
+
* by the old unconditional `rm -rf`, and the share root replaced by a symlink.
|
|
264
|
+
* - a **non-empty real directory**, because its contents are someone else's data
|
|
265
|
+
* and a retarget is not a reason to delete them.
|
|
266
|
+
*
|
|
267
|
+
* What remains is exactly the set of shapes a pointer may legitimately occupy: an
|
|
268
|
+
* absent path (first sync), a symlink (a repeat sync), an empty directory (a
|
|
269
|
+
* leftover), or an already-correct link. A refusal throws rather than returning
|
|
270
|
+
* false, because false means "the view was not ready" while this means "the
|
|
271
|
+
* configuration is wrong" -- two different things for the caller to report.
|
|
272
|
+
* @param options - the roots the paths are resolved against.
|
|
273
|
+
* @param mountPoint - the browsable path to retarget.
|
|
274
|
+
* @returns True when the link now points at the view.
|
|
275
|
+
*/
|
|
276
|
+
export declare function pointMountAtView(options: ViewOptions, mountPoint: string): Promise<boolean>;
|
|
277
|
+
export declare function viewPathFor(options: ViewOptions): string;
|
|
278
|
+
/**
|
|
279
|
+
* The view-relative path one authoritative entry is expected to appear at.
|
|
280
|
+
*
|
|
281
|
+
* A write knows the path it just created; this answers "where should the reader see
|
|
282
|
+
* it". Used to VERIFY a refresh rather than trust its status: `syncCallerView` reports
|
|
283
|
+
* success as soon as it has walked the tree, and its per-path permission question
|
|
284
|
+
* fails closed, so a resolve that failed for every path still answers 200 with
|
|
285
|
+
* `resolved: 0`. Comparing the expected view path against the disk is what turns
|
|
286
|
+
* "the sync ran" into "the reader can see it".
|
|
287
|
+
*
|
|
288
|
+
* @param options - the roots and the user key, for the department layout.
|
|
289
|
+
* @param relPath - authoritative path of the entry.
|
|
290
|
+
* @returns The view-relative path, or undefined when the caller's layout excludes it.
|
|
291
|
+
*/
|
|
292
|
+
export declare function expectedViewPathFor(options: ViewOptions, relPath: string): string | undefined;
|
|
293
|
+
/**
|
|
294
|
+
* Reject a relative path that would escape the root it is joined to.
|
|
295
|
+
*
|
|
296
|
+
* The resolved paths come from the authorization service, but they name files in
|
|
297
|
+
* a shared export. A `..` segment would let a grant for one path reach another, so
|
|
298
|
+
* it is refused here rather than trusted: the service decides visibility, this
|
|
299
|
+
* module decides that a visible path still lands inside the tree.
|
|
300
|
+
* @param relPath - the candidate path.
|
|
301
|
+
* @returns true when the path is relative and stays under its root.
|
|
302
|
+
*/
|
|
303
|
+
export declare function isContainedRelPath(relPath: string): boolean;
|
|
304
|
+
/**
|
|
305
|
+
* Bring one user's view in line with the paths DataOps resolved for them.
|
|
306
|
+
*
|
|
307
|
+
* Only the difference is touched: a path already linked is left alone, a missing
|
|
308
|
+
* one is linked, and one that is no longer resolved is unlinked. A run over an
|
|
309
|
+
* unchanged grant set therefore performs no filesystem writes at all, which is
|
|
310
|
+
* what makes this safe to repeat after every permission change.
|
|
311
|
+
*
|
|
312
|
+
* A link whose source has gone stale is reported as unlinked rather than failing
|
|
313
|
+
* the run, so one deleted file cannot leave a user's whole view half-applied.
|
|
314
|
+
* @param options - the roots and the user key.
|
|
315
|
+
* @param resolved - the paths DataOps says this user may read.
|
|
316
|
+
* @returns What changed, so a caller can report or log the delta.
|
|
317
|
+
*/
|
|
318
|
+
export declare function syncView(options: ViewSyncOptions, resolved: readonly ResolvedPath[]): Promise<ViewDelta>;
|
|
319
|
+
/**
|
|
320
|
+
* Remove one user's whole view.
|
|
321
|
+
*
|
|
322
|
+
* Used when a user is removed rather than merely re-scoped: their grants are gone
|
|
323
|
+
* from DataOps, so the view directory has no reason to exist. The authoritative
|
|
324
|
+
* files are untouched, because every entry in the view is a link to them.
|
|
325
|
+
* @param options - the roots and the user key.
|
|
326
|
+
* @returns true when a directory was removed, false when there was none.
|
|
327
|
+
*/
|
|
328
|
+
export declare function dropView(options: ViewOptions): Promise<boolean>;
|
|
329
|
+
/**
|
|
330
|
+
* The dotfile holding one user's list/grid preference.
|
|
331
|
+
*
|
|
332
|
+
* Beside `.pinned.json` and for the same reasons: the view directory is already
|
|
333
|
+
* per-user, already on the share, and the choice should follow the reader rather
|
|
334
|
+
* than the browser profile. Written through {@link writeViewMode} and skipped by
|
|
335
|
+
* the sync walk, which is what keeps a preference from being deleted as if it
|
|
336
|
+
* were a stale grant.
|
|
337
|
+
* @param viewDir - the user's view directory.
|
|
338
|
+
* @returns The absolute path of the view-mode file.
|
|
339
|
+
*/
|
|
340
|
+
export declare function viewModePathFor(viewDir: string): string;
|
|
341
|
+
/**
|
|
342
|
+
* The dotfile holding this knowledge base's review settings.
|
|
343
|
+
*
|
|
344
|
+
* A third sibling of `.pinned.json` and `.view-mode.json`, for the same reasons: the
|
|
345
|
+
* view directory already exists per user, already lives on the share, and the setting
|
|
346
|
+
* belongs to the knowledge base rather than to one browser profile. It MUST be named in
|
|
347
|
+
* the sync walk's metadata set, or the next sync deletes it as if it were a stale grant.
|
|
348
|
+
* @param viewDir - the user's view directory.
|
|
349
|
+
* @returns The absolute path of the settings file.
|
|
350
|
+
*/
|
|
351
|
+
export declare function settingsPathFor(viewDir: string): string;
|
|
352
|
+
/**
|
|
353
|
+
* The append-only audit log.
|
|
354
|
+
*
|
|
355
|
+
* A `.jsonl` file rather than a JSON array: an audit entry is appended with a single
|
|
356
|
+
* `appendFile`, so recording one action never rewrites the history before it. A JSON
|
|
357
|
+
* array would have to be read, extended and rewritten in full, which both costs more and
|
|
358
|
+
* creates a window where a crash truncates the whole trail.
|
|
359
|
+
* @param viewDir - the user's view directory.
|
|
360
|
+
* @returns The absolute path of the audit log.
|
|
361
|
+
*/
|
|
362
|
+
export declare function auditLogPathFor(viewDir: string): string;
|
|
363
|
+
/**
|
|
364
|
+
* Where content waits while review is required.
|
|
365
|
+
*
|
|
366
|
+
* Deliberately NOT under a view: the sync walk unlinks every file in a view that the
|
|
367
|
+
* caller's resolved grants do not name, so pending content parked there would be
|
|
368
|
+
* deleted by the next sync, which is the same failure that made writing new documents
|
|
369
|
+
* into a view wrong. It lives in the authoritative root instead, in a directory whose
|
|
370
|
+
* name starts with a dot so the tree listing already hides it.
|
|
371
|
+
* @param actualRoot - the authoritative root.
|
|
372
|
+
* @returns The absolute path of the pending directory.
|
|
373
|
+
*/
|
|
374
|
+
export declare function pendingDirFor(actualRoot: string): string;
|
|
375
|
+
/**
|
|
376
|
+
* Build the stable filename one pending write is parked under.
|
|
377
|
+
*
|
|
378
|
+
* The caller's id is part of the name so the trail of who submitted what survives even
|
|
379
|
+
* when the same path is submitted twice by different people; the timestamp keeps the
|
|
380
|
+
* two submissions from overwriting each other.
|
|
381
|
+
* @param userId - the submitting caller.
|
|
382
|
+
* @param relPath - the path the caller wanted to write.
|
|
383
|
+
* @param at - when it was submitted.
|
|
384
|
+
* @returns A filename that is safe as one path segment.
|
|
385
|
+
*/
|
|
386
|
+
export declare function pendingFileNameFor(userId: string, relPath: string, at: string): string;
|
|
387
|
+
/**
|
|
388
|
+
* Park one write for review instead of applying it.
|
|
389
|
+
*
|
|
390
|
+
* The payload is a small JSON envelope beside a copy of the bytes, both inside the
|
|
391
|
+
* pending directory. Keeping the content rather than only the intent means an approver
|
|
392
|
+
* can read exactly what was submitted, which is the whole point of reviewing it.
|
|
393
|
+
* @param actualRoot - the authoritative root.
|
|
394
|
+
* @param submission - what was submitted.
|
|
395
|
+
* @returns The directory the submission was parked in.
|
|
396
|
+
*/
|
|
397
|
+
export declare function parkPendingWrite(actualRoot: string, submission: {
|
|
398
|
+
userId: string;
|
|
399
|
+
relPath: string;
|
|
400
|
+
content: string;
|
|
401
|
+
at: string;
|
|
402
|
+
}): Promise<string>;
|
|
403
|
+
/** One submission waiting for review. */
|
|
404
|
+
export interface PendingWrite {
|
|
405
|
+
/** The parking name, which is also the handle a reviewer acts on. */
|
|
406
|
+
name: string;
|
|
407
|
+
/** Who submitted it. */
|
|
408
|
+
userId: string;
|
|
409
|
+
/** The path it wants to occupy in the authoritative tree. */
|
|
410
|
+
relPath: string;
|
|
411
|
+
/** When it was submitted. */
|
|
412
|
+
at: string;
|
|
413
|
+
/** Size of the submitted bytes. */
|
|
414
|
+
bytes: number;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Decide whether a value is one parking name and not a path.
|
|
418
|
+
*
|
|
419
|
+
* A reviewer acts on a name that came back from {@link listPendingWrites}, but the
|
|
420
|
+
* request is still browser input. Refusing separators and dot segments here means a
|
|
421
|
+
* crafted name cannot reach outside the pending directory, whatever the caller sends.
|
|
422
|
+
* @param value - the candidate name.
|
|
423
|
+
* @returns Whether it is usable as one path segment.
|
|
424
|
+
*/
|
|
425
|
+
export declare function isPendingName(value: unknown): value is string;
|
|
426
|
+
/**
|
|
427
|
+
* Read every submission waiting for review.
|
|
428
|
+
*
|
|
429
|
+
* The envelope is the source of truth rather than the filename, because the name escapes
|
|
430
|
+
* characters out of `relPath` and that escaping is lossy: a reviewer must see the path
|
|
431
|
+
* the submitter actually asked for. A submission whose envelope cannot be read is
|
|
432
|
+
* skipped rather than aborting the queue.
|
|
433
|
+
* @param actualRoot - the authoritative root.
|
|
434
|
+
* @returns The pending submissions, oldest first.
|
|
435
|
+
*/
|
|
436
|
+
export declare function listPendingWrites(actualRoot: string): Promise<PendingWrite[]>;
|
|
437
|
+
/**
|
|
438
|
+
* Read one submission's bytes.
|
|
439
|
+
*
|
|
440
|
+
* The name is checked again rather than trusted: this is the function that returns
|
|
441
|
+
* content to be written into the shared tree, so it is the last place a traversal could
|
|
442
|
+
* turn into a write outside the pending directory.
|
|
443
|
+
* @param actualRoot - the authoritative root.
|
|
444
|
+
* @param name - the parking name.
|
|
445
|
+
* @returns The submitted content, or null when it is not there.
|
|
446
|
+
*/
|
|
447
|
+
export declare function readPendingContent(actualRoot: string, name: string): Promise<string | null>;
|
|
448
|
+
/**
|
|
449
|
+
* Forget one submission.
|
|
450
|
+
*
|
|
451
|
+
* Used by both the approve and the reject path: approving means the bytes now live in the
|
|
452
|
+
* authoritative tree, and rejecting means nobody wants them. Either way the parked copy
|
|
453
|
+
* must not linger, or the queue would re-offer a decision already made.
|
|
454
|
+
* @param actualRoot - the authoritative root.
|
|
455
|
+
* @param name - the parking name.
|
|
456
|
+
* @returns Whether anything was removed.
|
|
457
|
+
*/
|
|
458
|
+
export declare function discardPendingWrite(actualRoot: string, name: string): Promise<boolean>;
|
|
459
|
+
/**
|
|
460
|
+
* Storage choices the operator may change from the panel.
|
|
461
|
+
*
|
|
462
|
+
* A strict subset of the plugin's configuration, and every field here is one the panel
|
|
463
|
+
* can meaningfully write: which shape to run, which transport a remote deployment uses,
|
|
464
|
+
* and where that transport reaches. Everything omitted -- `mountPoint`, `actualRoot`,
|
|
465
|
+
* `viewRoot`, `permitted` -- is left to the deployment because those paths are bound into
|
|
466
|
+
* the container when it is created, so writing them here would only point the plugin at
|
|
467
|
+
* paths nothing serves.
|
|
468
|
+
*
|
|
469
|
+
* There is deliberately no private-key field. `sshKeyRef` names a credential; the secret
|
|
470
|
+
* lives in the credential store, which is the only place a key may live.
|
|
471
|
+
*/
|
|
472
|
+
export interface KnowledgeStorageSettings {
|
|
473
|
+
/** Which storage shape to run, when the operator has chosen one. */
|
|
474
|
+
storageMode?: 'local' | 'remote';
|
|
475
|
+
/** Transport a remote deployment reaches its share by. */
|
|
476
|
+
storageProtocol?: 'nfs' | 'sshfs';
|
|
477
|
+
/** NFS export host. */
|
|
478
|
+
nfsServer?: string;
|
|
479
|
+
/** SSHFS: remote host. */
|
|
480
|
+
sshHost?: string;
|
|
481
|
+
/** SSHFS: login user. */
|
|
482
|
+
sshUser?: string;
|
|
483
|
+
/** SSHFS: SSH daemon port. */
|
|
484
|
+
sshPort?: number;
|
|
485
|
+
/** SSHFS: absolute path of the shared directory on the remote host. */
|
|
486
|
+
sshRemotePath?: string;
|
|
487
|
+
/** SSHFS: extra `-o` options. */
|
|
488
|
+
sshMountOptions?: string;
|
|
489
|
+
/** SSHFS: credential reference holding the private key -- a name, never the key. */
|
|
490
|
+
sshKeyRef?: string;
|
|
491
|
+
}
|
|
492
|
+
/** Whether a write must be reviewed before it counts as published. */
|
|
493
|
+
export interface KnowledgeSettings extends KnowledgeStorageSettings {
|
|
494
|
+
/**
|
|
495
|
+
* When true, content a caller adds is recorded as pending instead of taking effect.
|
|
496
|
+
*
|
|
497
|
+
* Defaults to false: the product ships with review OFF and an audit trail only, which
|
|
498
|
+
* is the behaviour the operator asked for. Turning it on is a deliberate act.
|
|
499
|
+
*/
|
|
500
|
+
reviewRequired: boolean;
|
|
501
|
+
/**
|
|
502
|
+
* MinerU's full base address, or `''` to derive it from the port.
|
|
503
|
+
*
|
|
504
|
+
* The port alone cannot express anything but this machine: a service reached through
|
|
505
|
+
* a tunnel, a gateway or another host has no port to put here. An empty value means
|
|
506
|
+
* "derive it", which keeps every existing deployment working without a migration.
|
|
507
|
+
*/
|
|
508
|
+
mineruBaseUrl: string;
|
|
509
|
+
}
|
|
510
|
+
/** The settings a caller who has never opened the page gets. */
|
|
511
|
+
export declare const DEFAULT_KNOWLEDGE_SETTINGS: KnowledgeSettings;
|
|
512
|
+
/**
|
|
513
|
+
* Normalise a MinerU base address.
|
|
514
|
+
*
|
|
515
|
+
* Only an absolute http(s) address is storable. A bare host, a `file:` URL or a typo
|
|
516
|
+
* would be handed to a parser that can only fail on it, and the failure would surface
|
|
517
|
+
* as "extraction broke" rather than "this setting is wrong", so the value is checked
|
|
518
|
+
* where it is written instead.
|
|
519
|
+
* @param raw - the candidate value.
|
|
520
|
+
* @returns The trimmed address without a trailing slash, or `''`.
|
|
521
|
+
*/
|
|
522
|
+
export declare function normalizeMineruBaseUrl(raw: unknown): string;
|
|
523
|
+
/**
|
|
524
|
+
* Copy the storage fields out of a parsed settings object.
|
|
525
|
+
*
|
|
526
|
+
* Values are validated here rather than at the render site so a hand-edited or foreign file
|
|
527
|
+
* cannot put an unknown mode into the effective configuration: an unrecognized value is
|
|
528
|
+
* dropped and the deployment default stands.
|
|
529
|
+
* @param raw - the parsed settings object.
|
|
530
|
+
* @returns The recognized storage fields, omitting anything invalid.
|
|
531
|
+
*/
|
|
532
|
+
export declare function storageSettingsOf(raw: Record<string, unknown>): KnowledgeStorageSettings;
|
|
533
|
+
/**
|
|
534
|
+
* Read this knowledge base's settings.
|
|
535
|
+
*
|
|
536
|
+
* Anything unrecognized answers the defaults rather than an error: a corrupt settings
|
|
537
|
+
* file should cost the reader their preferences, not the page. `reviewRequired` is
|
|
538
|
+
* coerced to a real boolean so a hand-edited `"false"` string cannot switch review on.
|
|
539
|
+
* @param viewDir - the user's view directory.
|
|
540
|
+
* @returns The stored settings, or the defaults.
|
|
541
|
+
*/
|
|
542
|
+
export declare function readSettings(viewDir: string): Promise<KnowledgeSettings>;
|
|
543
|
+
/**
|
|
544
|
+
* Replace this knowledge base's settings.
|
|
545
|
+
*
|
|
546
|
+
* Atomic by rename for the same reason the pins and the view mode are: a reader must
|
|
547
|
+
* never observe a half-written settings file. The view directory is created because a
|
|
548
|
+
* setting can be recorded before the first sync has built one.
|
|
549
|
+
* @param viewDir - the user's view directory, created if absent.
|
|
550
|
+
* @param settings - the settings to store.
|
|
551
|
+
* @returns The settings actually written.
|
|
552
|
+
*/
|
|
553
|
+
export declare function writeSettings(viewDir: string, settings: KnowledgeSettings): Promise<KnowledgeSettings>;
|
|
554
|
+
/**
|
|
555
|
+
* The deployment-level settings file, addressed from the mounted knowledge tree.
|
|
556
|
+
*
|
|
557
|
+
* A separate accessor from {@link settingsPathFor} because the two files answer different
|
|
558
|
+
* questions and are read at different moments. The per-user file is only reachable once a
|
|
559
|
+
* caller is known, while this one must be readable on the very first `config` call -- before
|
|
560
|
+
* any identity has been resolved -- because the storage shape is what tells the page whether
|
|
561
|
+
* a caller is needed at all. `mountPoint` is the one path the panel knows without asking
|
|
562
|
+
* anyone, so the file hangs off it.
|
|
563
|
+
*
|
|
564
|
+
* Named `.kb-settings.json` so it lands in the same metadata set the sync walk already
|
|
565
|
+
* skips; a sync that deleted it would silently revert the operator's choice.
|
|
566
|
+
* @param mountPoint - the mounted knowledge tree.
|
|
567
|
+
* @returns The absolute path of the settings file.
|
|
568
|
+
*/
|
|
569
|
+
export declare function storageSettingsPathFor(mountPoint: string): string;
|
|
570
|
+
/**
|
|
571
|
+
* Read the stored storage settings, tolerating every ordinary absence.
|
|
572
|
+
*
|
|
573
|
+
* A missing mount point, a missing file, and a file written by an older build are all the
|
|
574
|
+
* same answer here: nothing has been chosen yet, so the deployment defaults stand. Only a
|
|
575
|
+
* value that IS present and unrecognized is dropped, and it is dropped on its own rather
|
|
576
|
+
* than invalidating its neighbours.
|
|
577
|
+
* @param mountPoint - the mounted knowledge tree.
|
|
578
|
+
* @returns The stored storage fields, empty when nothing usable is on disk.
|
|
579
|
+
*/
|
|
580
|
+
export declare function readStorageSettings(mountPoint: string): Promise<KnowledgeStorageSettings>;
|
|
581
|
+
/**
|
|
582
|
+
* Merge a patch into the stored storage settings and write the file atomically.
|
|
583
|
+
*
|
|
584
|
+
* A merge rather than a replace: the panel sends only the fields the operator touched, and a
|
|
585
|
+
* save that blanked the others would make editing one field erase the rest. Written by
|
|
586
|
+
* `rename` so a reader never sees a half-written file, for the same reason the per-user
|
|
587
|
+
* settings are.
|
|
588
|
+
* @param mountPoint - the mounted knowledge tree, created if absent.
|
|
589
|
+
* @param patch - the fields to change.
|
|
590
|
+
* @returns The settings now on disk.
|
|
591
|
+
*/
|
|
592
|
+
export declare function writeStorageSettings(mountPoint: string, patch: KnowledgeStorageSettings): Promise<KnowledgeStorageSettings>;
|
|
593
|
+
/**
|
|
594
|
+
* Read one user's list/grid preference.
|
|
595
|
+
*
|
|
596
|
+
* Anything unrecognized answers `list`, which is the shape the page was built
|
|
597
|
+
* around: a corrupt or hand-edited preference should cost the reader a layout
|
|
598
|
+
* choice, not the page. A missing file is that same ordinary first read.
|
|
599
|
+
* @param viewDir - the user's view directory.
|
|
600
|
+
* @returns The stored mode.
|
|
601
|
+
*/
|
|
602
|
+
export declare function readViewMode(viewDir: string): Promise<ViewMode>;
|
|
603
|
+
/**
|
|
604
|
+
* Replace one user's list/grid preference.
|
|
605
|
+
*
|
|
606
|
+
* Atomic by rename for the same reason the pins are: a reader must never observe
|
|
607
|
+
* a half-written file. The view directory is created because the preference can
|
|
608
|
+
* be recorded before the first sync has built one.
|
|
609
|
+
* @param viewDir - the user's view directory, created if absent.
|
|
610
|
+
* @param mode - the mode to store.
|
|
611
|
+
* @returns The mode actually written.
|
|
612
|
+
*/
|
|
613
|
+
export declare function writeViewMode(viewDir: string, mode: ViewMode): Promise<ViewMode>;
|
|
614
|
+
/**
|
|
615
|
+
* The dotfile holding one user's expanded folders in the explorer.
|
|
616
|
+
*
|
|
617
|
+
* A sibling of `.pinned.json` and `.view-mode.json`, for the same reasons: the view
|
|
618
|
+
* directory is already per-user and already on the share, so "the same person sees
|
|
619
|
+
* the same tree" needs no new storage of its own. It MUST be named in the sync
|
|
620
|
+
* walk's metadata set, or the next sync deletes it as if it were a stale grant --
|
|
621
|
+
* which reads as a preference that will not stick rather than as a bug in that walk.
|
|
622
|
+
* @param viewDir - the user's view directory.
|
|
623
|
+
* @returns The absolute path of the tree-state file.
|
|
624
|
+
*/
|
|
625
|
+
export declare function treeStatePathFor(viewDir: string): string;
|
|
626
|
+
/**
|
|
627
|
+
* Read one user's expanded folders.
|
|
628
|
+
*
|
|
629
|
+
* Every failure answers an empty list, which is also the correct default: the tree
|
|
630
|
+
* starts fully collapsed, so a missing file, an unreadable file, a truncated file,
|
|
631
|
+
* and a file holding something other than an array all resolve to the state a
|
|
632
|
+
* first visit has. Never throws -- the tree must render even when this file is
|
|
633
|
+
* missing or corrupt, and a preference is never worth failing a page for.
|
|
634
|
+
* @param viewDir - the user's view directory.
|
|
635
|
+
* @returns The stored expanded paths, deduplicated, junk entries dropped.
|
|
636
|
+
*/
|
|
637
|
+
export declare function readTreeState(viewDir: string): Promise<string[]>;
|
|
638
|
+
/**
|
|
639
|
+
* Replace one user's expanded folders.
|
|
640
|
+
*
|
|
641
|
+
* Atomic by rename for the same reason the pins and the view mode are: a reader
|
|
642
|
+
* must never observe a half-written file, and the file lives on a share where a
|
|
643
|
+
* crash mid-write would otherwise leave a truncated document that reads as "all
|
|
644
|
+
* collapsed". The view directory is created because the preference can be
|
|
645
|
+
* recorded before the first sync has built one.
|
|
646
|
+
*
|
|
647
|
+
* Junk is dropped rather than refused: unlike a pin, which names a document the
|
|
648
|
+
* reader explicitly asked to keep, this is a volume knob whose worst failure mode
|
|
649
|
+
* is a folder that comes back closed. Answering 400 here would make a broken
|
|
650
|
+
* client's save look like a page error for no reader-visible gain.
|
|
651
|
+
* @param viewDir - the user's view directory, created if absent.
|
|
652
|
+
* @param expanded - the absolute paths to store.
|
|
653
|
+
* @returns The paths actually written.
|
|
654
|
+
*/
|
|
655
|
+
export declare function writeTreeState(viewDir: string, expanded: readonly string[]): Promise<string[]>;
|
|
656
|
+
/**
|
|
657
|
+
* The dotfile holding one user's favourite paths.
|
|
658
|
+
*
|
|
659
|
+
* A fourth sibling of `.pinned.json`, `.view-mode.json` and `.kb-tree-state.json`,
|
|
660
|
+
* and for the same reason: a favourite is a view preference about the reader, not a
|
|
661
|
+
* property of the document, so the caller's own view directory is already the right
|
|
662
|
+
* owner and no backend table is involved. It MUST stay listed in the sync walk's
|
|
663
|
+
* metadata set or the next sync unlinks it, which shows up as a star that will not
|
|
664
|
+
* stick rather than as a bug in that walk.
|
|
665
|
+
*
|
|
666
|
+
* Kept separate from `pins` on purpose. A pin promotes a path into the rail's band
|
|
667
|
+
* and is addressed by the reader looking for a shortcut; a favourite only marks the
|
|
668
|
+
* row where it already sits, and the two are expected to differ -- pinning a folder
|
|
669
|
+
* you work in every day while still starring the report you are drafting.
|
|
670
|
+
* @param viewDir - the user's view directory.
|
|
671
|
+
* @returns The absolute path of the favourites file.
|
|
672
|
+
*/
|
|
673
|
+
export declare function favoritesPathFor(viewDir: string): string;
|
|
674
|
+
/**
|
|
675
|
+
* Read one user's favourites.
|
|
676
|
+
*
|
|
677
|
+
* A missing or unparsable file is the empty list, exactly as {@link readPins}
|
|
678
|
+
* reports it: no favourites is the ordinary first state, and a corrupt preference
|
|
679
|
+
* should cost the reader their stars rather than the page.
|
|
680
|
+
* @param viewDir - the user's view directory.
|
|
681
|
+
* @returns The favourited paths, in the order they were written.
|
|
682
|
+
*/
|
|
683
|
+
export declare function readFavorites(viewDir: string): Promise<string[]>;
|
|
684
|
+
/**
|
|
685
|
+
* Replace one user's favourites.
|
|
686
|
+
*
|
|
687
|
+
* Normalised and written by atomic rename, like every other preference file here:
|
|
688
|
+
* a reader must never observe a half-written list, and the temporary is a sibling
|
|
689
|
+
* because a rename cannot cross a filesystem.
|
|
690
|
+
* @param viewDir - the user's view directory, created if absent.
|
|
691
|
+
* @param relPaths - the paths to store.
|
|
692
|
+
* @returns The paths actually written, after normalisation.
|
|
693
|
+
*/
|
|
694
|
+
export declare function writeFavorites(viewDir: string, relPaths: readonly string[]): Promise<string[]>;
|
|
695
|
+
/**
|
|
696
|
+
* Move or rename one entry inside the authoritative tree.
|
|
697
|
+
*
|
|
698
|
+
* `rename(2)` rather than a copy followed by an unlink, and that choice is the
|
|
699
|
+
* whole point of this function: the authoritative tree is an NFS export, so a
|
|
700
|
+
* copy-and-delete would read and rewrite the entire file across the network for
|
|
701
|
+
* what the kernel can do as a single atomic directory operation. It also keeps
|
|
702
|
+
* the inode, which matters because every user's view holds HARDLINKS to these
|
|
703
|
+
* files: a copy would give the moved file a new inode and leave every hardlink in
|
|
704
|
+
* every view pointing at the old one. Callers must resynchronize the affected
|
|
705
|
+
* views afterwards ({@link syncView} relinks the new path and drops the old), and
|
|
706
|
+
* that is not optional -- without it the reader sees a dead entry.
|
|
707
|
+
*
|
|
708
|
+
* Both paths are required to be contained relative paths; the caller is
|
|
709
|
+
* responsible for having asked the authority whether the caller may write them.
|
|
710
|
+
* The destination's parent is created when it does not exist, and a destination
|
|
711
|
+
* that already holds an entry is refused rather than overwritten, because a move
|
|
712
|
+
* that silently replaces a document is data loss and no prompt can undo it.
|
|
713
|
+
* @param options - the roots; only `actualRoot` is used.
|
|
714
|
+
* @param fromRelPath - the entry's current path, relative to the root.
|
|
715
|
+
* @param toRelPath - the entry's new path, relative to the root.
|
|
716
|
+
* @returns The moved entry's new relative path.
|
|
717
|
+
*/
|
|
718
|
+
export declare function moveEntry(options: ViewOptions, fromRelPath: string, toRelPath: string): Promise<{
|
|
719
|
+
from: string;
|
|
720
|
+
to: string;
|
|
721
|
+
}>;
|
|
722
|
+
/**
|
|
723
|
+
* Copy one entry, with its subtree, to a new path inside the authoritative tree.
|
|
724
|
+
*
|
|
725
|
+
* Three rules that the caller cannot waive:
|
|
726
|
+
*
|
|
727
|
+
* - **No overwrite.** An existing destination is refused by name rather than replaced
|
|
728
|
+
* or auto-renamed: a silent overwrite destroys something the reader did not name, and
|
|
729
|
+
* an auto-rename produces a file under a name they never typed while the surface says
|
|
730
|
+
* the copy succeeded.
|
|
731
|
+
* - **Symlinks are copied, not followed.** See {@link planCopy}.
|
|
732
|
+
* - **All-or-nothing.** Everything is resolved and size-checked first; if the write
|
|
733
|
+
* phase still fails, the paths written so far are removed, so the tree never keeps a
|
|
734
|
+
* half-copied directory.
|
|
735
|
+
*
|
|
736
|
+
* `copyFile` + `mkdir` rather than a recursive helper: the plan already knows every
|
|
737
|
+
* path, and doing them one at a time is what makes rollback a list of paths to unlink
|
|
738
|
+
* instead of a second tree walk over an unknown half-written state.
|
|
739
|
+
* @param options - the caller's view options (only `actualRoot` is used).
|
|
740
|
+
* @param fromRelPath - the source, relative to the authoritative root.
|
|
741
|
+
* @param toRelPath - the destination, relative to the authoritative root.
|
|
742
|
+
* @returns The source and destination paths, on success.
|
|
743
|
+
*/
|
|
744
|
+
export declare function copyEntry(options: ViewOptions, fromRelPath: string, toRelPath: string): Promise<{
|
|
745
|
+
from: string;
|
|
746
|
+
to: string;
|
|
747
|
+
entries: number;
|
|
748
|
+
bytes: number;
|
|
749
|
+
}>;
|
|
750
|
+
/**
|
|
751
|
+
* The dotfile holding one user's pins.
|
|
752
|
+
*
|
|
753
|
+
* A dotfile inside the caller's own view directory, rather than a backend table:
|
|
754
|
+
* the view directory is already per-user, already on the shared export, and
|
|
755
|
+
* already rebuilt in place by {@link syncView}. Pins are therefore durable across
|
|
756
|
+
* a restart and need no new authority to store. The name is dotted to keep it out
|
|
757
|
+
* of the way of a reader scanning the tree, and the walk skips this exact path so
|
|
758
|
+
* a sync cannot delete it.
|
|
759
|
+
* @param viewDir - the user's view directory.
|
|
760
|
+
* @returns The absolute path of the pin file.
|
|
761
|
+
*/
|
|
762
|
+
export declare function pinsPathFor(viewDir: string): string;
|
|
763
|
+
/**
|
|
764
|
+
* Read one user's pins.
|
|
765
|
+
*
|
|
766
|
+
* A missing file is the empty list rather than an error: every user has no pins
|
|
767
|
+
* until they make one, so that is the ordinary first read. A file that exists but
|
|
768
|
+
* cannot be parsed is also reported empty, because a corrupt preference should
|
|
769
|
+
* cost the reader their pins rather than the whole page.
|
|
770
|
+
* @param viewDir - the user's view directory.
|
|
771
|
+
* @returns The pinned paths, in the order they were written.
|
|
772
|
+
*/
|
|
773
|
+
export declare function readPins(viewDir: string): Promise<string[]>;
|
|
774
|
+
/**
|
|
775
|
+
* Replace one user's pins.
|
|
776
|
+
*
|
|
777
|
+
* The list is normalized before it is written: every entry must be a contained
|
|
778
|
+
* relative path, duplicates collapse, and the order is the caller's. Writing is
|
|
779
|
+
* atomic by rename, so a reader never observes a half-written file, and the
|
|
780
|
+
* temporary name is a sibling because a rename cannot cross a filesystem.
|
|
781
|
+
* @param viewDir - the user's view directory, created if absent.
|
|
782
|
+
* @param relPaths - the paths to store, already filtered for readability.
|
|
783
|
+
* @returns The paths actually written, after normalization.
|
|
784
|
+
*/
|
|
785
|
+
export declare function writePins(viewDir: string, relPaths: readonly string[]): Promise<string[]>;
|
|
786
|
+
/** One recorded change to the knowledge base. */
|
|
787
|
+
export interface KnowledgeAuditEntry {
|
|
788
|
+
/** ISO timestamp of the action. */
|
|
789
|
+
at: string;
|
|
790
|
+
/** Who performed it: the DataOps user id the Host resolved. */
|
|
791
|
+
actor: string;
|
|
792
|
+
/**
|
|
793
|
+
* That user's display name at the time of the action, when the Host resolved one.
|
|
794
|
+
*
|
|
795
|
+
* Kept beside the id rather than replacing it: the id is the identity an audit is
|
|
796
|
+
* checked against, while this is only what the trail is read as. Optional because
|
|
797
|
+
* logs written before it existed carry no label, and a reader must show such an
|
|
798
|
+
* entry by its id rather than drop it.
|
|
799
|
+
*/
|
|
800
|
+
actorLabel?: string;
|
|
801
|
+
/** What they did. */
|
|
802
|
+
/**
|
|
803
|
+
* What they did.
|
|
804
|
+
*
|
|
805
|
+
* `edit` is a save of an existing document's bytes, kept distinct from `create`
|
|
806
|
+
* because the two answer different questions in a review: "who first put this
|
|
807
|
+
* here" and "who changed it last". A save through the host's own file editor
|
|
808
|
+
* reports here through a side channel (see the `audit.appended` route), which is
|
|
809
|
+
* what keeps the host's write path untouched.
|
|
810
|
+
*/
|
|
811
|
+
action: 'create' | 'upload' | 'move' | 'approve' | 'reject' | 'edit';
|
|
812
|
+
/** The path the action concerned, relative to the knowledge root. */
|
|
813
|
+
relPath: string;
|
|
814
|
+
/** For a move, the destination path. */
|
|
815
|
+
toRelPath?: string;
|
|
816
|
+
/**
|
|
817
|
+
* What became of it.
|
|
818
|
+
*
|
|
819
|
+
* `pending` means it was parked for review, `applied` means it took effect,
|
|
820
|
+
* `rejected` means a reviewer refused it. The three are kept distinct because
|
|
821
|
+
* "nothing happened yet" and "somebody said no" are different facts, and an audit
|
|
822
|
+
* trail that collapsed them would misreport why a document is missing.
|
|
823
|
+
*/
|
|
824
|
+
outcome: 'applied' | 'pending' | 'rejected';
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Append one audit entry.
|
|
828
|
+
*
|
|
829
|
+
* Deliberately append-only: no update and no delete path exists in this module, because
|
|
830
|
+
* an audit trail a caller can rewrite is not an audit trail. The file is opened with
|
|
831
|
+
* `'a'`, so the write is a single append at the end and can never overwrite an earlier
|
|
832
|
+
* entry, even with two writers racing.
|
|
833
|
+
*
|
|
834
|
+
* Failure to record is NOT allowed to fail the action that was recorded. The caller has
|
|
835
|
+
* already made its change, so throwing here would report a failure that did not happen;
|
|
836
|
+
* the caller is expected to treat this as best-effort and the reason is logged by the
|
|
837
|
+
* host. That trade is deliberate: an audit gap is bad, but a reported-but-not-applied
|
|
838
|
+
* write is worse.
|
|
839
|
+
* @param viewDir - the user's view directory, created if absent.
|
|
840
|
+
* @param entry - the entry to record.
|
|
841
|
+
*/
|
|
842
|
+
export declare function appendAuditEntry(viewDir: string, entry: KnowledgeAuditEntry): Promise<void>;
|
|
843
|
+
/**
|
|
844
|
+
* Read the newest audit entries, newest first.
|
|
845
|
+
*
|
|
846
|
+
* The whole file is read because it is a per-user, append-only log that grows by one
|
|
847
|
+
* short line per write; there is no rotation yet and pretending otherwise would add a
|
|
848
|
+
* seek that the format does not support. Unparseable lines are skipped rather than
|
|
849
|
+
* thrown: a half-written last line from a crash must not make the whole trail unreadable.
|
|
850
|
+
* @param viewDir - the user's view directory.
|
|
851
|
+
* @param limit - maximum entries to return, newest first.
|
|
852
|
+
* @returns The entries, newest first.
|
|
853
|
+
*/
|
|
854
|
+
export declare function readAuditEntries(viewDir: string, limit?: number): Promise<KnowledgeAuditEntry[]>;
|
|
855
|
+
/** One stored template, as the index records it. */
|
|
856
|
+
export interface TemplateRecord {
|
|
857
|
+
/** Derived from the stored document's name. */
|
|
858
|
+
id: string;
|
|
859
|
+
title: string;
|
|
860
|
+
description: string;
|
|
861
|
+
category: string;
|
|
862
|
+
tags: string[];
|
|
863
|
+
/** The document's stored file name, including its extension. */
|
|
864
|
+
fileName: string;
|
|
865
|
+
/** Last-modified instant, ISO-8601. */
|
|
866
|
+
updatedAt: string;
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* The template directory's absolute path.
|
|
870
|
+
* @param actualRoot - the authoritative tree's root.
|
|
871
|
+
* @returns The absolute directory.
|
|
872
|
+
*/
|
|
873
|
+
export declare function templateDirFor(actualRoot: string): string;
|
|
874
|
+
/**
|
|
875
|
+
* Read the template index.
|
|
876
|
+
*
|
|
877
|
+
* Every failure answers an empty library, which is also the correct first-run state:
|
|
878
|
+
* a missing directory, a truncated index, and an index holding the wrong shape all
|
|
879
|
+
* resolve to "no templates yet". Never throws -- the library must render.
|
|
880
|
+
* @param actualRoot - the authoritative tree's root.
|
|
881
|
+
* @returns The template records, newest first.
|
|
882
|
+
*/
|
|
883
|
+
export declare function readTemplates(actualRoot: string): Promise<TemplateRecord[]>;
|
|
884
|
+
/**
|
|
885
|
+
* Store one template: its document and its metadata.
|
|
886
|
+
*
|
|
887
|
+
* Replacing is keyed on the FILE NAME, not on a separate id, because the file name is
|
|
888
|
+
* the identity. A save that changes the name therefore writes a new document and
|
|
889
|
+
* removes the old one, which is what "rename this template" means; a save that keeps
|
|
890
|
+
* the name overwrites in place.
|
|
891
|
+
* @param actualRoot - the authoritative tree's root.
|
|
892
|
+
* @param input - the document text and metadata.
|
|
893
|
+
* @returns The stored record.
|
|
894
|
+
*/
|
|
895
|
+
export declare function saveTemplate(actualRoot: string, input: {
|
|
896
|
+
title: string;
|
|
897
|
+
description: string;
|
|
898
|
+
category: string;
|
|
899
|
+
tags: readonly string[];
|
|
900
|
+
fileName: string;
|
|
901
|
+
content?: string;
|
|
902
|
+
/** The template being edited, so its previous document is removed on a rename. */
|
|
903
|
+
id?: string;
|
|
904
|
+
}): Promise<TemplateRecord>;
|
|
905
|
+
/**
|
|
906
|
+
* Remove one template: its document and its record.
|
|
907
|
+
*
|
|
908
|
+
* Both halves go together, because a record without a document is a card that cannot
|
|
909
|
+
* be used and a document without a record is an invisible file. A missing document is
|
|
910
|
+
* not an error -- the record is what says the template exists, and removing it is the
|
|
911
|
+
* operation asked for.
|
|
912
|
+
* @param actualRoot - the authoritative tree's root.
|
|
913
|
+
* @param id - the template to remove.
|
|
914
|
+
* @returns Whether a template was removed.
|
|
915
|
+
*/
|
|
916
|
+
export declare function removeTemplate(actualRoot: string, id: string): Promise<boolean>;
|
|
917
|
+
/**
|
|
918
|
+
* Stat one stored template's document, so the card can report its real size.
|
|
919
|
+
* @param actualRoot - the authoritative tree's root.
|
|
920
|
+
* @param fileName - the record's stored name.
|
|
921
|
+
* @returns Byte size, or 0 when the document is gone.
|
|
922
|
+
*/
|
|
923
|
+
export declare function templateBytes(actualRoot: string, fileName: string): Promise<number>;
|