@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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/SKILL.md +147 -0
  3. package/cordis.patch.yml +5 -0
  4. package/i18n/zh.json +10 -0
  5. package/lib/client.js +18308 -0
  6. package/lib/client.js.map +1 -0
  7. package/lib/index.js +6005 -0
  8. package/lib/types/api.d.ts +620 -0
  9. package/lib/types/client/KnowledgeAudit.d.ts +39 -0
  10. package/lib/types/client/KnowledgeContextMenu.d.ts +121 -0
  11. package/lib/types/client/KnowledgeCreateDialog.d.ts +134 -0
  12. package/lib/types/client/KnowledgeFileTree.d.ts +117 -0
  13. package/lib/types/client/KnowledgeHome.d.ts +102 -0
  14. package/lib/types/client/KnowledgeIndexing.d.ts +60 -0
  15. package/lib/types/client/KnowledgeListing.d.ts +174 -0
  16. package/lib/types/client/KnowledgeLoading.d.ts +41 -0
  17. package/lib/types/client/KnowledgeMenuIcon.d.ts +27 -0
  18. package/lib/types/client/KnowledgePanel.d.ts +58 -0
  19. package/lib/types/client/KnowledgePermissionOverview.d.ts +147 -0
  20. package/lib/types/client/KnowledgePermissions.d.ts +47 -0
  21. package/lib/types/client/KnowledgeProgressPanel.d.ts +53 -0
  22. package/lib/types/client/KnowledgeReferencePicker.d.ts +81 -0
  23. package/lib/types/client/KnowledgeSearch.d.ts +83 -0
  24. package/lib/types/client/KnowledgeSectionHeader.d.ts +71 -0
  25. package/lib/types/client/KnowledgeSelectionBar.d.ts +45 -0
  26. package/lib/types/client/KnowledgeSettings.d.ts +96 -0
  27. package/lib/types/client/KnowledgeSettingsSection.d.ts +21 -0
  28. package/lib/types/client/KnowledgeShareDialog.d.ts +70 -0
  29. package/lib/types/client/KnowledgeTemplates.d.ts +43 -0
  30. package/lib/types/client/KnowledgeTrash.d.ts +62 -0
  31. package/lib/types/client/MenuGlyphs.d.ts +37 -0
  32. package/lib/types/client/auditPaging.d.ts +40 -0
  33. package/lib/types/client/departmentFolders.d.ts +57 -0
  34. package/lib/types/client/deployTarget.d.ts +34 -0
  35. package/lib/types/client/index.d.ts +40 -0
  36. package/lib/types/client/insertReadiness.d.ts +60 -0
  37. package/lib/types/client/knowledgeMenuCopy.d.ts +23 -0
  38. package/lib/types/client/locales.d.ts +505 -0
  39. package/lib/types/client/openInSidebar.d.ts +62 -0
  40. package/lib/types/client/ownerLabel.d.ts +39 -0
  41. package/lib/types/client/paging.d.ts +69 -0
  42. package/lib/types/client/permissionRows.d.ts +265 -0
  43. package/lib/types/client/progressRules.d.ts +54 -0
  44. package/lib/types/client/referenceTargets.d.ts +60 -0
  45. package/lib/types/client/roleSummary.d.ts +47 -0
  46. package/lib/types/client/saveAudit.d.ts +87 -0
  47. package/lib/types/client/shareRelations.d.ts +81 -0
  48. package/lib/types/client/useFavorites.d.ts +49 -0
  49. package/lib/types/client/useFileDrop.d.ts +171 -0
  50. package/lib/types/client/useFileDropHook.d.ts +64 -0
  51. package/lib/types/client/usePinnedPaths.d.ts +39 -0
  52. package/lib/types/client/useTreeState.d.ts +46 -0
  53. package/lib/types/client/useViewMode.d.ts +39 -0
  54. package/lib/types/deploy.d.ts +209 -0
  55. package/lib/types/extract.d.ts +144 -0
  56. package/lib/types/extractionQueue.d.ts +154 -0
  57. package/lib/types/index.d.ts +899 -0
  58. package/lib/types/permissions.d.ts +237 -0
  59. package/lib/types/protocol.d.ts +229 -0
  60. package/lib/types/remote.d.ts +105 -0
  61. package/lib/types/trash.d.ts +142 -0
  62. package/lib/types/viewMetadata.d.ts +20 -0
  63. package/lib/types/views.d.ts +923 -0
  64. package/package.json +109 -4
  65. package/skills/kb-organize/SKILL.md +365 -0
  66. package/skills/kb-organize/i18n/zh.json +10 -0
  67. package/README.md +0 -3
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Open a knowledge document in the product's own right-hand file window.
3
+ *
4
+ * The knowledge page does not draw a document preview of its own. Opening a document
5
+ * hands it to the host's sidebar, which is the surface every other file in the
6
+ * workspace opens into: tab strip, path input, preview/edit toggle, save, refresh,
7
+ * outline. The knowledge page owns none of that chrome.
8
+ *
9
+ * Two host services are involved, both published before any panel mounts:
10
+ *
11
+ * - `sidebarRight` (`ui-sidebar-right`) owns the column. Its `openResource` claims the
12
+ * address, places it, and **reveals the column in the same step** -- the column
13
+ * expands as part of opening, so a document handed to it cannot land in a collapsed
14
+ * panel the reader never sees.
15
+ * - `sidebarRightTabs` is the tab-type registry, whose ranking decides which kind
16
+ * claims a file address. Asking it keeps this open on the host's normal path instead
17
+ * of hard-coding a kind.
18
+ *
19
+ * The address is built with the official `fileAddressFor`, so a document opened from
20
+ * here dedupes against the tab the chat file-link path already opened for the same
21
+ * file rather than opening a second tab onto it.
22
+ *
23
+ * @module @sparkelf/dsh-knowledge-base/client
24
+ */
25
+ /** Outcome of asking the host to show a document. */
26
+ export type OpenOutcome = 'opened' | 'no-context' | 'no-registry' | 'unclaimed' | 'no-column';
27
+ /**
28
+ * Ask the host to open a document in its right-hand file window.
29
+ *
30
+ * @param ctx - the client context the page was injected with.
31
+ * @param sessionId - the conversation the sidebar column is scoped to.
32
+ * @param cwd - the session's workspace root, used to shorten the address to a
33
+ * session-relative one so it dedupes against chat-opened tabs.
34
+ * @param absolutePath - the document's absolute path inside the workspace.
35
+ * @returns Why it did or did not open; the caller reports the failure rather than
36
+ * appearing to do nothing.
37
+ */
38
+ export declare function openInSidebar(ctx: unknown, sessionId: string | undefined, cwd: string | undefined, absolutePath: string): OpenOutcome;
39
+ /**
40
+ * Return the right column's width to the page.
41
+ *
42
+ * Called when the knowledge page stops showing a document. It is deliberately separate
43
+ * from {@link openInSidebar} rather than a mode flag: the two states need different
44
+ * frames, and closing the track is what restores the page's own width. A host without
45
+ * the layout service is left alone -- the page simply keeps whatever it had.
46
+ * @param ctx - the client context the page runs in.
47
+ */
48
+ /**
49
+ * Report whether the host's right column is currently expanded.
50
+ *
51
+ * The page uses this to decide whether it may keep its own content column: a document
52
+ * opened in the reader means the page's column is redundant, but only while the reader
53
+ * is actually on screen. Reading it live (instead of tracking it in React state) keeps
54
+ * the page and the frame from disagreeing when the reader closes the column themselves.
55
+ * A host without the column service reports `false`, which keeps the page's own layout.
56
+ * @param ctx - the client context the page runs in.
57
+ * @returns whether the column is expanded right now.
58
+ */
59
+ export declare function readColumnExpanded(ctx: unknown): boolean;
60
+ export declare function closeSidebar(ctx: unknown): void;
61
+ /** Whether a failure means the host's right column is simply not on this profile. */
62
+ export declare function isHostMissing(outcome: OpenOutcome): boolean;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Who a knowledge document belongs to, derived from where it is stored.
3
+ *
4
+ * The list route reports a path and a location, not an owner: ownership is a property of
5
+ * the bucket, and every rule below is a rule about that bucket. Keeping the derivation
6
+ * here (rather than in the row renderer) does two things. It stays testable without a
7
+ * DOM, and it keeps the answer honest — nothing is invented when the information is not
8
+ * there, because a wrong owner is worse than an unflattering one.
9
+ *
10
+ * @module @sparkelf/dsh-knowledge-base/client/ownerLabel
11
+ */
12
+ /** What the caller knows about itself, as far as ownership is concerned. */
13
+ export interface OwnerContext {
14
+ /** The caller's DataOps id, so `personal/<id>` can be recognised as one's own folder. */
15
+ callerId?: string;
16
+ /** The caller's own department code, for naming the department bucket. */
17
+ departmentCode?: string;
18
+ /** The caller's own department name; falls back to the code when absent. */
19
+ departmentName?: string;
20
+ }
21
+ /** One resolved owner cell. */
22
+ export interface OwnerLabel {
23
+ /** Which rule produced the label, so the renderer can style it without re-parsing. */
24
+ kind: 'global' | 'department' | 'self' | 'person' | 'unknown';
25
+ /** The text to show. */
26
+ label: string;
27
+ }
28
+ /**
29
+ * Resolve the owner of one document.
30
+ *
31
+ * `labels` carries the three strings this module cannot know: what to call the shared
32
+ * bucket, what to call the caller's own folder, and what to show when a path is outside
33
+ * every bucket. Passing them in keeps the module free of locale plumbing.
34
+ */
35
+ export declare function ownerLabelOf(relPath: string, context: OwnerContext, labels: {
36
+ global: string;
37
+ self: string;
38
+ unknown: string;
39
+ }): OwnerLabel;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Page slicing for every table in the knowledge surface.
3
+ *
4
+ * A pure module rather than a hook: the rule worth testing is arithmetic, and a hook
5
+ * would need a renderer to exercise. The component owns the page and size as state;
6
+ * this answers what that state should display.
7
+ *
8
+ * The one rule that matters is the clamp. A reader can be on page 7 of a table when a
9
+ * filter, a search word or a re-read shrinks it to two pages, and obeying "page 7"
10
+ * literally renders an empty table with a pager that still says there is data. So the
11
+ * requested page is clamped into range and the clamped value is returned, which is what
12
+ * the owner writes back into its own state.
13
+ *
14
+ * @module @sparkelf/dsh-knowledge-base/client/paging
15
+ */
16
+ /** Page sizes every table offers, in the order the control shows them. */
17
+ export declare const PAGE_SIZES: readonly [20, 50, 100];
18
+ /** The page size a fresh table starts on. */
19
+ export declare const DEFAULT_PAGE_SIZE = 20;
20
+ /** One page of rows plus the numbers a pager draws. */
21
+ export interface Page<T> {
22
+ /** The rows on this page, in order. */
23
+ rows: T[];
24
+ /** 1-based page actually shown, clamped into range. */
25
+ page: number;
26
+ /** Total number of pages; never zero, so no pager can say "page 1 of 0". */
27
+ pageCount: number;
28
+ /** Total row count across all pages. */
29
+ total: number;
30
+ }
31
+ /**
32
+ * Whether a table of this size needs a pager at all.
33
+ *
34
+ * A single page has nothing to page through, so the control row is not drawn: an
35
+ * always-visible pager on a three-row table is furniture that implies the reader is
36
+ * missing something. Empty is the same answer for the same reason.
37
+ * @param total - row count before slicing.
38
+ * @param pageSize - rows per page.
39
+ * @returns true when there is more than one page.
40
+ */
41
+ export declare function needsPager(total: number, pageSize: number): boolean;
42
+ /**
43
+ * Slice one page out of a list that is already in display order.
44
+ *
45
+ * Ordering is the caller's job and happens BEFORE this call: pagination must never
46
+ * reorder, or turning a page would appear to sort the table differently. The same
47
+ * applies to filtering -- a filtered list is what gets paged, so the count in the pager
48
+ * is the count of matches rather than of the whole table.
49
+ *
50
+ * @param rows - rows already filtered and ordered.
51
+ * @param page - 1-based page requested; clamped into range.
52
+ * @param pageSize - rows per page; a non-positive or non-integer value falls back.
53
+ * @returns The page and the numbers to draw.
54
+ */
55
+ export declare function paginate<T>(rows: readonly T[], page: number, pageSize: number): Page<T>;
56
+ /**
57
+ * The 1-based page a size change should land on.
58
+ *
59
+ * Keeping the first visible row in view is the point: a reader who switches from 20 to
60
+ * 100 rows is looking at row 41, so the page they want is the one containing it, not
61
+ * page 1 and not page 3 of the old numbering. Clamping in {@link paginate} then keeps
62
+ * that answer in range if the list is shorter than expected.
63
+ *
64
+ * @param currentPage - page shown before the change.
65
+ * @param previousSize - size in force before the change.
66
+ * @param nextSize - size being chosen.
67
+ * @returns The 1-based page to request.
68
+ */
69
+ export declare function pageAfterSizeChange(currentPage: number, previousSize: number, nextSize: number): number;
@@ -0,0 +1,265 @@
1
+ /**
2
+ * The pure half of the permissions overview: how one grant table reads two ways.
3
+ *
4
+ * Kept apart from the rendering module for a real reason, not for tidiness: a
5
+ * component module imports the primitives package, which imports CSS, and a Node
6
+ * test runner cannot load a `.css` file. The grouping rules are the part worth
7
+ * testing without a DOM, so they live somewhere a plain import can reach.
8
+ *
9
+ * @module @sparkelf/dsh-knowledge-base/client/permissionRows
10
+ */
11
+ import type { GrantView, KnowledgeGrantSubjectType } from '../api.ts';
12
+ /** One person's holdings, as the person view renders them. */
13
+ export interface PersonHoldings {
14
+ /** Principal id the grants name. */
15
+ principalId: string;
16
+ /** Principal kind, so the type column can name it. */
17
+ principalType: KnowledgeGrantSubjectType;
18
+ /** Display name when a directory entry resolved one, else the id. */
19
+ label: string;
20
+ /** Distinct paths this person may touch. */
21
+ paths: string[];
22
+ /** Which actions the person holds, collapsed so a full house reads `all`. */
23
+ actions: string[];
24
+ /**
25
+ * Roles the person reaches through a `role` grant they belong to.
26
+ *
27
+ * Empty when the directory does not know the membership: a role grant names the
28
+ * ROLE, not the people in it, so this is only populated when a directory entry
29
+ * declared the membership.
30
+ */
31
+ roles: string[];
32
+ /** Most recent grant instant across this person's rows, ISO-8601. */
33
+ latestAt: string;
34
+ /** The grants this row covers, so a revoke can name exactly what it removes. */
35
+ grants: GrantView[];
36
+ }
37
+ /** One resource and everyone who may reach it. */
38
+ export interface ResourceHoldings {
39
+ /** Path, relative to the mount point. */
40
+ relPath: string;
41
+ /** Whether it is a file or a directory. */
42
+ kind: 'file' | 'directory';
43
+ /** Everyone holding any action on it, grouped by principal kind. */
44
+ principals: Array<{
45
+ principalType: KnowledgeGrantSubjectType;
46
+ principalId: string;
47
+ label: string;
48
+ actions: string[];
49
+ latestAt: string;
50
+ grants: GrantView[];
51
+ }>;
52
+ }
53
+ /**
54
+ * The result of reading a set of grants as permissions.
55
+ *
56
+ * `known: false` carries a reason rather than an empty list, because "we could not
57
+ * work this out" and "this principal holds nothing" must not render the same way: the
58
+ * first is a fault the reader should see, and collapsing it into an empty set is how a
59
+ * denial gets invented. See {@link resolveActions}.
60
+ */
61
+ export interface ResolvedActions {
62
+ /** Actions held, in display order. Empty when `known` is false. */
63
+ actions: string[];
64
+ /**
65
+ * Whether the full set could be determined.
66
+ *
67
+ * False when part of the input could not be read -- an inheritance path that was
68
+ * unavailable, or a grant whose action is a word this build does not know.
69
+ */
70
+ known: boolean;
71
+ /** Why the set is incomplete, when it is. Shown to the reader beside the actions. */
72
+ reason?: string;
73
+ }
74
+ /**
75
+ * Union a principal's grants into the permission set it actually holds.
76
+ *
77
+ * UNION, never intersection and never "the first row": a principal reaches a resource
78
+ * through several grants (a direct one and one inherited from each role), and what
79
+ * they may do is everything any of those grants allows. Reading one row is what made
80
+ * an administrator display as read-only.
81
+ *
82
+ * An unrecognised action word makes the whole answer unknown rather than being
83
+ * skipped. Skipping it would under-report, and under-reporting a permission is the
84
+ * direction that misleads an operator into thinking access is safe to ignore.
85
+ *
86
+ * @param actions - every action string from every grant reaching this principal.
87
+ * @returns The unioned set, or an explicit unknown with the reason.
88
+ */
89
+ export declare function resolveActions(actions: readonly string[]): ResolvedActions;
90
+ /**
91
+ * Whether a resolved set confers the ability to change a document.
92
+ *
93
+ * Used by the page to check itself against what the reader can actually do. `manage`
94
+ * implies write in this model: it is the full-control level, and a principal holding
95
+ * it can move and delete.
96
+ * @param resolved - the unioned permission set.
97
+ * @returns true when writing is possible, or unknown.
98
+ */
99
+ export declare function canWriteWith(resolved: ResolvedActions): boolean | undefined;
100
+ /**
101
+ * Collapse a set of action strings into the shortest describing list.
102
+ *
103
+ * A thin view over {@link resolveActions} so there is ONE vocabulary and ONE union
104
+ * rule in the browser: this used to be a second implementation filtering on
105
+ * `create`/`update`/`delete`, which silently dropped every `write` and `manage` grant.
106
+ *
107
+ * Kept as an exported name because the older callers and their tests use it.
108
+ * @param actions - raw action strings from grants.
109
+ * @returns The unioned actions in display order; unknown words are dropped here,
110
+ * which is why a surface that must not under-report calls {@link resolveActions}.
111
+ */
112
+ export declare function summarizeActions(actions: readonly string[]): string[];
113
+ /**
114
+ * Group grants by the principal they name.
115
+ *
116
+ * A principal appearing in several grants is one row: "what does 张三 have" is a
117
+ * question about a person, and splitting them across rows because they hold two
118
+ * paths would answer it wrong.
119
+ *
120
+ * @param grants - the grant table.
121
+ * @param labels - display names by `principalType:principalId`, optional.
122
+ * @returns one entry per principal, ordered by label then id.
123
+ */
124
+ export declare function groupHoldingsByPrincipal(grants: readonly GrantView[], labels?: Readonly<Record<string, string>>): PersonHoldings[];
125
+ /**
126
+ * Group grants by the resource they cover.
127
+ *
128
+ * @param grants - the grant table.
129
+ * @param labels - display names by `principalType:principalId`, optional.
130
+ * @returns one entry per path, ordered by path.
131
+ */
132
+ export declare function groupHoldingsByResource(grants: readonly GrantView[], labels?: Readonly<Record<string, string>>): ResourceHoldings[];
133
+ /**
134
+ * Format a grant instant the way the reference table does.
135
+ *
136
+ * DataOps' permission preview prints `YYYY/MM/DD HH:mm` and a dash for a missing
137
+ * value, so a reader comparing the two surfaces sees the same date. The raw
138
+ * ISO-8601 string the API returns is not that.
139
+ * @param value - ISO-8601 instant, or an empty string when the backend sent none.
140
+ * @returns The formatted instant, or a dash when there is nothing to show.
141
+ */
142
+ export declare function formatGrantInstant(value: string): string;
143
+ /**
144
+ * One DataOps account as the user directory reports it, reduced to what grouping needs.
145
+ *
146
+ * `roles` is what makes inheritance visible: a role grant names the ROLE, so without
147
+ * the membership list the page can only show role grants as belonging to nobody. The
148
+ * directory is the only source for it, and it is administrator-only -- see
149
+ * {@link DirectoryState}.
150
+ */
151
+ export interface DirectoryUser {
152
+ /** DataOps user id, which is what a `user` grant names. */
153
+ userId: string;
154
+ /** Login name, shown beside the display name so two "张伟" can be told apart. */
155
+ username: string;
156
+ /** Display name, when the account has one. */
157
+ displayName: string;
158
+ /** Role codes this account belongs to. */
159
+ roleCodes: string[];
160
+ }
161
+ /**
162
+ * Whether the user directory could be read.
163
+ *
164
+ * Three states, not two. "Nobody holds a role" and "we could not look" produce the same
165
+ * empty membership map, and rendering them the same way is how a page ends up telling
166
+ * an operator that nobody has inherited access when the truth is that the read failed.
167
+ */
168
+ export type DirectoryState = {
169
+ kind: 'available';
170
+ users: readonly DirectoryUser[];
171
+ } | {
172
+ kind: 'unavailable';
173
+ reason: string;
174
+ };
175
+ /** One account's holdings, as the by-person view renders them. */
176
+ export interface AccountHoldings {
177
+ /** The account id. */
178
+ userId: string;
179
+ /** Login name, for disambiguating identical display names. */
180
+ username?: string;
181
+ /** Display name, or the id when the account is not in the directory. */
182
+ label: string;
183
+ /** Distinct paths this account reaches, directly or through a role. */
184
+ paths: string[];
185
+ /** The unioned permission set, never narrowed by a single grant. */
186
+ resolved: ResolvedActions;
187
+ /** Direct grants the account holds itself. */
188
+ directGrants: GrantView[];
189
+ /**
190
+ * Role codes the account inherits through.
191
+ *
192
+ * Empty when the directory could not be read; {@link inheritanceKnown} says which.
193
+ */
194
+ viaRoles: string[];
195
+ /** Whether the inheritance half of this row was actually determined. */
196
+ inheritanceKnown: boolean;
197
+ /** Why inheritance is unknown, when it is. */
198
+ inheritanceReason?: string;
199
+ /** Most recent grant instant across the rows this account covers. */
200
+ latestAt: string;
201
+ }
202
+ /** One role's holdings, as the by-role view renders them. */
203
+ export interface RoleHoldings {
204
+ /** Role code, which is what a `role` grant names. */
205
+ roleCode: string;
206
+ /** Role display name when the directory resolved one, else the code. */
207
+ label: string;
208
+ /** Distinct paths the role covers. */
209
+ paths: string[];
210
+ /** The unioned permission set for this role. */
211
+ resolved: ResolvedActions;
212
+ /** Grants that name this role. */
213
+ grants: GrantView[];
214
+ /** How many directory accounts belong to this role, when the directory is known. */
215
+ memberCount?: number;
216
+ /** Most recent grant instant across this role's rows. */
217
+ latestAt: string;
218
+ }
219
+ /**
220
+ * Group grants by the DataOps ACCOUNT that holds them.
221
+ *
222
+ * The account is the row grain because "who can see this" is a question about people.
223
+ * A role grant confers access on every member of that role, so its paths are attributed
224
+ * to each member here -- otherwise a reader whose access is entirely role-derived would
225
+ * appear on no row at all, and the page would say "nobody has access" to somebody who
226
+ * can write.
227
+ *
228
+ * Three sources are merged, and the merge is a UNION:
229
+ * 1. direct `user` grants;
230
+ * 2. grants held by any role the account belongs to (inherited);
231
+ * 3. nothing else -- the built-in administrator levels are already rows in the grant
232
+ * table (`source: 'seed'`), so hard-coding them here would create a second authority
233
+ * that could disagree with the backend.
234
+ *
235
+ * @param grants - the whole grant table.
236
+ * @param directory - the user directory, or the reason it could not be read.
237
+ * @param labels - display names by `principalType:principalId`, optional.
238
+ * @returns One row per account that holds or inherits at least one grant, sorted by label.
239
+ */
240
+ export declare function groupHoldingsByAccount(grants: readonly GrantView[], directory: DirectoryState, labels?: Readonly<Record<string, string>>): AccountHoldings[];
241
+ /**
242
+ * Group grants by the ROLE they name.
243
+ *
244
+ * The same grants the old "by person" table showed, now labelled as what they are: a
245
+ * role is not a person, and putting it under "by person" told the reader that the
246
+ * administrators were the only people with access.
247
+ *
248
+ * A role whose grants are all unreadable still gets a row with `known: false`: that is
249
+ * the safety net against rendering "no permission" for a read failure.
250
+ *
251
+ * @param grants - the whole grant table.
252
+ * @param labels - display names by `principalType:principalId`, optional.
253
+ * @param memberCounts - role code -> member count, when the directory is known.
254
+ * @returns One row per role named by at least one grant, sorted by label.
255
+ */
256
+ export declare function groupHoldingsByRole(grants: readonly GrantView[], labels?: Readonly<Record<string, string>>, memberCounts?: ReadonlyMap<string, number>): RoleHoldings[];
257
+ /**
258
+ * Build the role -> member-count map from a directory.
259
+ *
260
+ * Only meaningful when the directory is known; returns undefined otherwise so the row
261
+ * omits the count rather than claiming zero members.
262
+ * @param directory - the user directory state.
263
+ * @returns Counts by role code, or undefined when unavailable.
264
+ */
265
+ export declare function roleMemberCounts(directory: DirectoryState): Map<string, number> | undefined;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The progress strip's two decisions, kept out of the component.
3
+ *
4
+ * Both are answers about data rather than about React: whether a batch is finished, and
5
+ * whether a retry button would help. They live here because a `.tsx` module that imports
6
+ * the UI primitives cannot be loaded by the test runner (the primitives package imports
7
+ * CSS at module scope, which the runner cannot resolve), so a test that imported the
8
+ * component would fail before reaching any assertion.
9
+ *
10
+ * @module @sparkelf/dsh-knowledge-base/client
11
+ */
12
+ /**
13
+ * Whether the strip may fold now.
14
+ *
15
+ * Folded means "one summary line instead of a list". It folds when every task in both
16
+ * groups reached a terminal state AND the batch has not been folded before -- the
17
+ * second half is what stops it re-folding after a reader expanded a finished batch to
18
+ * read which file failed.
19
+ * @param uploads - the current batch's rows.
20
+ * @param extractions - the extraction tasks the Host reports.
21
+ * @param alreadyFolded - whether a fold already happened for this batch.
22
+ * @returns Whether to fold.
23
+ */
24
+ export declare function shouldFoldStrip(uploads: readonly {
25
+ state: string;
26
+ }[], extractions: readonly {
27
+ state: string;
28
+ }[], alreadyFolded: boolean): boolean;
29
+ /**
30
+ * Whether a retry button would help for this state.
31
+ *
32
+ * Only a failure is worth retrying. `unsupported` means no extractor exists for the
33
+ * type, so a retry fails identically and the button would be a lie; the pending and
34
+ * running states are already being handled.
35
+ * @param state - the extraction state.
36
+ * @returns Whether to offer a retry.
37
+ */
38
+ export declare function canRetryExtraction(state: string): boolean;
39
+ /**
40
+ * A failure reason that survives a response which is not JSON.
41
+ *
42
+ * A gateway answering 502 sends HTML, and `response.json()` rejects on it. If that
43
+ * rejection escaped, the row would report "failed" with no reason at all -- which is
44
+ * exactly the silence this strip exists to remove. So the body is attempted, and an
45
+ * unreadable body falls back to the status code, which still tells the reader whether
46
+ * the server refused them (4xx) or broke (5xx).
47
+ * @param body - the parsed body, when it parsed.
48
+ * @param status - the HTTP status.
49
+ * @returns A sentence a reader can act on.
50
+ */
51
+ export declare function failureReason(body: {
52
+ error?: unknown;
53
+ code?: unknown;
54
+ } | undefined, status: number): string;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Pure helpers for the reference picker and the batch menu.
3
+ *
4
+ * Kept out of the components because the parts worth testing are the decisions, not the
5
+ * rendering: how a selection becomes a target list, what the batch label says, and which
6
+ * entries a paste loop should attempt. The components are wired to these, so a change in
7
+ * behaviour here is what the tests below pin down; the real DOM interaction is covered by
8
+ * the browser walkthrough instead.
9
+ *
10
+ * @module @sparkelf/dsh-knowledge-base/client
11
+ */
12
+ /** One entry a reference will be inserted for, in absolute path form. */
13
+ export interface ReferenceTarget {
14
+ /** Absolute path as the tree and the routes speak it. */
15
+ path: string;
16
+ /** True for a directory (folder mention) rather than a document (file chip). */
17
+ isDir: boolean;
18
+ }
19
+ /** One row a batch action covers. */
20
+ export interface BatchRow {
21
+ /** Path relative to the knowledge root. */
22
+ relPath: string;
23
+ /** Display name, for menu copy. */
24
+ name: string;
25
+ /** Whether the row is a directory. */
26
+ isDir: boolean;
27
+ }
28
+ /**
29
+ * The entries a picker opened on `relPath` should insert.
30
+ *
31
+ * A multi-row selection wins when it contains the clicked row, because that is the set
32
+ * the reader could see highlighted. Otherwise the click is a single-row action on the row
33
+ * under the pointer -- which is also what happens when the clicked row sits outside the
34
+ * selection, since acting on a batch the reader did not point at would be a surprise.
35
+ * @param relPath - the row the `@` button was pressed on.
36
+ * @param selection - the rows the explorer reports as selected, if any.
37
+ * @returns One target, or every selected row when the click was inside the selection.
38
+ */
39
+ export declare function referenceTargetsFor(relPath: string, selection?: readonly BatchRow[]): ReferenceTarget[];
40
+ /**
41
+ * The rows a menu opened on one row should act on.
42
+ *
43
+ * Mirrors {@link referenceTargetsFor}: a right-click inside a multi-selection acts on the
44
+ * whole selection, and anywhere else acts on the one row. The menu and the picker must
45
+ * agree, or a "delete 3 items" dialog would reference four.
46
+ * @param target - the row under the pointer.
47
+ * @param selection - the rows marked selected, if any.
48
+ * @returns The rows the batch entries cover, or an empty list for a single-row menu.
49
+ */
50
+ export declare function batchRowsFor(target: BatchRow, selection?: readonly BatchRow[]): BatchRow[];
51
+ /**
52
+ * Fill a batch label's `{n}` placeholder.
53
+ *
54
+ * A separate function so the count is formatted in one place: a label that prints the
55
+ * rows it will touch must never print a different number from the rows it does touch.
56
+ * @param template - the locale string, containing `{n}`.
57
+ * @param count - how many rows.
58
+ * @returns The label with the count substituted.
59
+ */
60
+ export declare function batchLabel(template: string, count: number): string;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * What each role in the grant table actually amounts to.
3
+ *
4
+ * A role is not stored anywhere: it exists because grants name it. That is a good
5
+ * property to inherit — a role cannot outlive its resources or appear with none —
6
+ * but it means "list the roles" is a question about the grants, answered by
7
+ * reading them. This module is that reading, kept out of the component so it can
8
+ * be tested without a DOM.
9
+ *
10
+ * @module @sparkelf/dsh-knowledge-base/client/roleSummary
11
+ */
12
+ import type { GrantView } from '../api.ts';
13
+ /** One role as the overview shows it. */
14
+ export interface RoleSummary {
15
+ /** Role id, which is the `principalId` its grants carry. */
16
+ id: string;
17
+ /** Display name, falling back to the id when no locale entry matches. */
18
+ label: string;
19
+ /** Actions granted anywhere by this role, collapsed so a full house reads `all`. */
20
+ actions: string[];
21
+ /** Distinct paths it reaches. */
22
+ paths: string[];
23
+ /** Whether this is one of the two the product ships with. */
24
+ builtIn: boolean;
25
+ }
26
+ /**
27
+ * Collapse raw action strings into the shortest describing list.
28
+ *
29
+ * The client needs this independently of the server: the overview renders grants
30
+ * the server already wrote, so it summarizes what is there rather than what a
31
+ * request asked for.
32
+ *
33
+ * @param actions - raw action strings.
34
+ * @returns `['all']` when all four concrete actions are held, else the subset.
35
+ */
36
+ export declare function summarizeRoleActions(actions: readonly string[]): string[];
37
+ /**
38
+ * Read the roles out of a grant table.
39
+ *
40
+ * Only `role` grants contribute: a user grant is not a role, and inventing one
41
+ * per user would flood the list with rows that mean nothing.
42
+ *
43
+ * @param grants - the grant table.
44
+ * @param locale - `zh` or `en`, for the built-in role names.
45
+ * @returns one entry per role named by a grant, built-ins first then alphabetical.
46
+ */
47
+ export declare function buildRoleSummaries(grants: readonly GrantView[], locale?: 'zh' | 'en'): RoleSummary[];