@bevel-software/platform-shared 0.1.0

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 (77) hide show
  1. package/LICENSE +202 -0
  2. package/dist/auth/types.d.ts +15 -0
  3. package/dist/auth/types.d.ts.map +1 -0
  4. package/dist/auth/types.js +2 -0
  5. package/dist/auth/types.js.map +1 -0
  6. package/dist/chat/types.d.ts +39 -0
  7. package/dist/chat/types.d.ts.map +1 -0
  8. package/dist/chat/types.js +13 -0
  9. package/dist/chat/types.js.map +1 -0
  10. package/dist/git/branchAuthor.d.ts +28 -0
  11. package/dist/git/branchAuthor.d.ts.map +1 -0
  12. package/dist/git/branchAuthor.js +45 -0
  13. package/dist/git/branchAuthor.js.map +1 -0
  14. package/dist/git/pr.types.d.ts +264 -0
  15. package/dist/git/pr.types.d.ts.map +1 -0
  16. package/dist/git/pr.types.js +2 -0
  17. package/dist/git/pr.types.js.map +1 -0
  18. package/dist/git/protected.d.ts +52 -0
  19. package/dist/git/protected.d.ts.map +1 -0
  20. package/dist/git/protected.js +92 -0
  21. package/dist/git/protected.js.map +1 -0
  22. package/dist/git/review.types.d.ts +49 -0
  23. package/dist/git/review.types.d.ts.map +1 -0
  24. package/dist/git/review.types.js +2 -0
  25. package/dist/git/review.types.js.map +1 -0
  26. package/dist/git/types.d.ts +107 -0
  27. package/dist/git/types.d.ts.map +1 -0
  28. package/dist/git/types.js +2 -0
  29. package/dist/git/types.js.map +1 -0
  30. package/dist/index.d.ts +15 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +20 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/workflow/events.d.ts +218 -0
  35. package/dist/workflow/events.d.ts.map +1 -0
  36. package/dist/workflow/events.js +48 -0
  37. package/dist/workflow/events.js.map +1 -0
  38. package/dist/workflow/interface.d.ts +233 -0
  39. package/dist/workflow/interface.d.ts.map +1 -0
  40. package/dist/workflow/interface.js +20 -0
  41. package/dist/workflow/interface.js.map +1 -0
  42. package/dist/workflow/types.d.ts +131 -0
  43. package/dist/workflow/types.d.ts.map +1 -0
  44. package/dist/workflow/types.js +17 -0
  45. package/dist/workflow/types.js.map +1 -0
  46. package/dist/workspace/filename.d.ts +33 -0
  47. package/dist/workspace/filename.d.ts.map +1 -0
  48. package/dist/workspace/filename.js +98 -0
  49. package/dist/workspace/filename.js.map +1 -0
  50. package/dist/workspace/frontmatter.d.ts +23 -0
  51. package/dist/workspace/frontmatter.d.ts.map +1 -0
  52. package/dist/workspace/frontmatter.js +48 -0
  53. package/dist/workspace/frontmatter.js.map +1 -0
  54. package/dist/workspace/kb-layout.d.ts +66 -0
  55. package/dist/workspace/kb-layout.d.ts.map +1 -0
  56. package/dist/workspace/kb-layout.js +63 -0
  57. package/dist/workspace/kb-layout.js.map +1 -0
  58. package/dist/workspace/types.d.ts +57 -0
  59. package/dist/workspace/types.d.ts.map +1 -0
  60. package/dist/workspace/types.js +2 -0
  61. package/dist/workspace/types.js.map +1 -0
  62. package/package.json +41 -0
  63. package/src/auth/types.ts +16 -0
  64. package/src/chat/types.ts +53 -0
  65. package/src/git/branchAuthor.ts +46 -0
  66. package/src/git/pr.types.ts +274 -0
  67. package/src/git/protected.ts +121 -0
  68. package/src/git/review.types.ts +51 -0
  69. package/src/git/types.ts +170 -0
  70. package/src/index.ts +23 -0
  71. package/src/workflow/events.ts +271 -0
  72. package/src/workflow/interface.ts +357 -0
  73. package/src/workflow/types.ts +156 -0
  74. package/src/workspace/filename.ts +102 -0
  75. package/src/workspace/frontmatter.ts +45 -0
  76. package/src/workspace/kb-layout.ts +81 -0
  77. package/src/workspace/types.ts +59 -0
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Workflow event bus types — what the backend pushes over SSE to keep the
3
+ * frontend in sync with state changes, including those triggered by other
4
+ * users on the same branch or by the agent acting server-side.
5
+ *
6
+ * **Scope model** — events have one of three scopes, set by the variant:
7
+ *
8
+ * - **Workspace-scoped** (`workspaceId` field). Pushed to every connected
9
+ * session whose current focus is `workspaceId`. Examples: a file changed
10
+ * on this branch, someone took the lock on a file. Anyone editing this
11
+ * branch cares.
12
+ *
13
+ * - **User-scoped** (`forUserId` field, no `workspaceId`). Pushed only to
14
+ * sessions opened by `forUserId`, regardless of their current focus.
15
+ * Examples: an agent turn called a tool on your behalf, your view should
16
+ * navigate to a different branch. Other users on the same branch don't
17
+ * care.
18
+ *
19
+ * - **Global** (no `workspaceId`, no `forUserId`). Pushed to every
20
+ * authenticated session. Examples: change requests opening / merging /
21
+ * being rejected — any user with access to the repo might care.
22
+ *
23
+ * **Identity + replay** — every event carries an `id` (monotonic int) and an
24
+ * ISO `ts`. Clients reconnecting with `Last-Event-ID: <id>` get the buffered
25
+ * tail replayed (server's ring buffer is finite — see `WorkflowEventBus`).
26
+ * If `id` is older than what the buffer still has, the server sends a
27
+ * `{ kind: 'resync' }` instead so the client refetches the world cleanly.
28
+ *
29
+ * **Idempotency over exactly-once** — clients tolerate duplicates and
30
+ * out-of-order delivery: every payload carries enough state for the UI to
31
+ * reach the right end state regardless of how many times it sees the event
32
+ * (e.g. `file-changed` includes the new SHA — UI only refreshes if the open
33
+ * tab's SHA differs).
34
+ */
35
+ /** True for events scoped to a workspace — the focus filter applies. */
36
+ export function isWorkspaceScoped(e) {
37
+ return (e.kind === 'file-changed' ||
38
+ e.kind === 'lock-acquired' ||
39
+ e.kind === 'lock-released' ||
40
+ e.kind === 'fs-tree-changed');
41
+ }
42
+ /** True for events scoped to a specific user — the user filter applies. */
43
+ export function isUserScoped(e) {
44
+ return (e.kind === 'agent-tool-call' ||
45
+ e.kind === 'branch-switched' ||
46
+ e.kind === 'change-request-merge-failed');
47
+ }
48
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/workflow/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAwNH,wEAAwE;AACxE,MAAM,UAAU,iBAAiB,CAC/B,CAAuB;IAEvB,OAAO,CACL,CAAC,CAAC,IAAI,KAAK,cAAc;QACzB,CAAC,CAAC,IAAI,KAAK,eAAe;QAC1B,CAAC,CAAC,IAAI,KAAK,eAAe;QAC1B,CAAC,CAAC,IAAI,KAAK,iBAAiB,CAC7B,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,YAAY,CAC1B,CAAuB;IAEvB,OAAO,CACL,CAAC,CAAC,IAAI,KAAK,iBAAiB;QAC5B,CAAC,CAAC,IAAI,KAAK,iBAAiB;QAC5B,CAAC,CAAC,IAAI,KAAK,6BAA6B,CACzC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,233 @@
1
+ /**
2
+ * `IWorkflowService` — the abstraction the rest of the app talks to instead
3
+ * of raw git/gh. See `bevel-platform/PLAN.md` for the motivation and the spec the
4
+ * methods here encode (protected branches, atomic per-file changes, file
5
+ * locks, change requests as branch-pair reconciliations).
6
+ *
7
+ * Naming convention: methods speak workflow vocabulary (`commitChange`,
8
+ * `openChangeRequest`, `acquireLock`), never git vocabulary (`commit`,
9
+ * `createPr`, no analogue for locks). Implementations may delegate to the
10
+ * existing git/PR/review-workflow services until those are folded into
11
+ * `modules/workflow`; callers should not depend on that fact.
12
+ *
13
+ * Some methods on this interface have no backing service yet — file locks,
14
+ * `openChangeRequest`, `updateFromTarget`. The facade implementation throws
15
+ * `NotImplementedWorkflowError` from those until the underlying support
16
+ * lands. The interface defines them up-front so consumers can plan against
17
+ * the final surface area.
18
+ */
19
+ import type { AuthUser } from '../auth/types.js';
20
+ import type { AcquireLockResult, Branch, BranchWorkspaceStatus, CancelChangeRequestResult, Change, ChangeInput, ChangeRequest, ChangeRequestComment, ChangeRequestDetail, ChangeRequestState, ChangedFile, FileApproval, FileLock, MergeChangeRequestOutcome, OpenChangeRequestInput, PostChangeRequestCommentInput } from './types.js';
21
+ export interface IWorkflowService {
22
+ listBranches(workspaceId: string, opts?: {
23
+ freshFetch?: boolean;
24
+ }): Promise<Branch[]>;
25
+ /**
26
+ * Create a new unprotected branch. Protected branches cannot be created
27
+ * via the workflow — the protected set is bootstrapped from the KB repo's
28
+ * initial state and never grown at runtime.
29
+ */
30
+ createBranch(workspaceId: string, name: string, fromBase?: string): Promise<Branch>;
31
+ /**
32
+ * Delete a branch. Removes both local and origin refs in one call. The
33
+ * branch's author (per `<email-localpart>/...` naming) OR an admin (per the
34
+ * workspace's `roles.yaml`) can delete it. Admins can additionally remove
35
+ * unprefixed CLI-created branches that have no recognisable author.
36
+ *
37
+ * `onlyIfNoRemote: true` is the legacy orphan-cleanup path: bypasses the
38
+ * author/admin check (callers prune any orphan they encounter) and refuses
39
+ * if origin still has the ref. Protected and current branches always reject.
40
+ */
41
+ deleteBranch(workspaceId: string, name: string, user: AuthUser, opts?: {
42
+ onlyIfNoRemote?: boolean;
43
+ }): Promise<void>;
44
+ /** Current branch's workspace status — currently just the branch name + upstream sync state. */
45
+ branchStatus(workspaceId: string): Promise<BranchWorkspaceStatus>;
46
+ /**
47
+ * Push the current branch upstream so other users (and change requests)
48
+ * can see the commits on it. Workflow term: "share current branch". The
49
+ * underlying implementation is `git push -u origin <branch>`.
50
+ */
51
+ shareCurrentBranch(workspaceId: string, user: AuthUser): Promise<void>;
52
+ /**
53
+ * Number of commits on `branch` that aren't on `baseBranch` — exactly
54
+ * what a CR from `branch` to `baseBranch` would propose for review.
55
+ * Implemented as `git rev-list --count <baseBranch>..<branch>` against
56
+ * the workspace's clone; returns 0 when the branch has no unmerged
57
+ * commits (a fresh fork that nobody wrote to) and when either ref
58
+ * cannot be resolved. Background runs use this to decide whether a
59
+ * routine branch deserves a change request — without it, a run where
60
+ * the agent commits files but forgets to call `open_change_request`
61
+ * would silently lose its work to the no-changes cleanup path.
62
+ */
63
+ countCommitsAhead(workspaceId: string, branch: string, baseBranch: string): Promise<number>;
64
+ /** Fetch remote refs without modifying the working tree. */
65
+ refreshRemotes(workspaceId: string): Promise<void>;
66
+ /** Pull the latest remote state into the current branch (rebase strategy). */
67
+ updateFromRemote(workspaceId: string): Promise<void>;
68
+ /**
69
+ * Hard-reset the workspace's checked-out branch to `origin/<branch>` (fetch
70
+ * first), discarding any local divergence. Break-glass primitive for
71
+ * roles.yaml recovery so the restoring commit can fast-forward on push.
72
+ */
73
+ resetToRemote(workspaceId: string, branch: string): Promise<void>;
74
+ /**
75
+ * Best-fit protected branch that `branch` was forked from — picks the
76
+ * protected branch on origin yielding the smallest "ahead" count.
77
+ * Returns null if no protected branch can be reached.
78
+ */
79
+ resolveForkBase(workspaceId: string, branch: string): Promise<string | null>;
80
+ /**
81
+ * Paths that the next `commitChange` would include — what `git add -A`
82
+ * would stage: modified + deleted + renamed + untracked (honouring
83
+ * `.gitignore`). Returns only paths.
84
+ */
85
+ listPendingChangePaths(workspaceId: string): Promise<string[]>;
86
+ /**
87
+ * Commit one atomic change. The current backing implementation packs every
88
+ * dirty path in the working tree into the commit; the workflow spec
89
+ * eventually constrains this to one file per change, enforced at the
90
+ * commit boundary. Until that lands, callers should already commit
91
+ * one-file-at-a-time by convention.
92
+ */
93
+ commitChange(workspaceId: string, user: AuthUser, input: ChangeInput): Promise<Change>;
94
+ /**
95
+ * Commit whatever the caller has already written to the working tree as ONE
96
+ * atomic change, then push. Unlike `commitChange` (one file at a time, guarded
97
+ * at the commit boundary) this commits the whole dirty set together — for the
98
+ * rare case where a group of files must land together or not at all (e.g. a
99
+ * role rename rewriting `roles.yaml` + every reference). Does NOT write
100
+ * content: the caller writes the files first (e.g. via the lock-aware
101
+ * filesystem) and is responsible for any validation. Returns null on a no-op
102
+ * (clean tree).
103
+ */
104
+ commitChanges(workspaceId: string, user: AuthUser, summary: string): Promise<Change | null>;
105
+ /** Per-file history. Newest first; clamps to ≤ 100 entries. */
106
+ listChangesForFile(workspaceId: string, path: string, limit?: number): Promise<Change[]>;
107
+ /**
108
+ * Revert a single change by creating a new change that undoes it.
109
+ * Refused on protected branches; refused without write permission on the
110
+ * touched paths.
111
+ */
112
+ revertChange(workspaceId: string, user: AuthUser, sha: string): Promise<Change>;
113
+ /**
114
+ * Diff of one file between two branches. Both names must resolve to a
115
+ * known branch on this workspace (local head or remote-tracking). Returns
116
+ * the raw unified diff body — empty when identical, includes the
117
+ * "new file" / "deleted file" headers when the file only exists on one
118
+ * side.
119
+ */
120
+ compareFile(workspaceId: string, path: string, fromBranch: string, toBranch: string): Promise<string>;
121
+ /**
122
+ * Diff of one file at a specific change (sha) — i.e. `git show <sha> -- <path>`.
123
+ * Used by the File viewer's History tab.
124
+ */
125
+ showFileAtChange(workspaceId: string, path: string, sha: string): Promise<string>;
126
+ /**
127
+ * Try to acquire an edit lock on `(branch, path)` for the caller. Returns
128
+ * `{ acquired: true, lock }` on success; `{ acquired: false, lock }` if
129
+ * someone else already holds it (the existing lock is included so the UI
130
+ * can render "Locked by X" without a follow-up call).
131
+ *
132
+ * Pessimistic per the spec. Auto-released when the lock expires past TTL
133
+ * without a heartbeat — stale locks must not block forever.
134
+ */
135
+ acquireLock(workspaceId: string, branch: string, path: string, user: AuthUser): Promise<AcquireLockResult>;
136
+ /** Heartbeat to keep an acquired lock alive past its current TTL. */
137
+ heartbeatLock(workspaceId: string, branch: string, path: string, user: AuthUser): Promise<FileLock>;
138
+ /**
139
+ * Commit the lock-held file's accumulated edits as a change *without*
140
+ * releasing the lock. Used by the client-side autosave checkpoint timer
141
+ * so the lock stays exclusively the caller's across many interim
142
+ * commits — releasing + reacquiring around every checkpoint would let
143
+ * another user grab the lock in the gap. Returns `null` when the file
144
+ * has no pending edits (idempotent). Refused when the lock isn't held
145
+ * by `user`.
146
+ */
147
+ commitFileWhileLocked(workspaceId: string, branch: string, path: string, user: AuthUser, summary?: string): Promise<Change | null>;
148
+ /**
149
+ * Release a held lock. The "I'm done editing this file" terminal signal.
150
+ *
151
+ * **Does not commit or push synchronously.** This method enqueues a row in
152
+ * `pending_commits` and drops the lock, then returns `void`. The background
153
+ * `PendingCommitsWorker` claims the row, synthesises the commit subject
154
+ * (callers do *not* supply one — the worker derives a default from the path
155
+ * and any worker-side metadata), runs `commitFile` against whatever bytes
156
+ * are on disk, and pushes the result. Decoupling the user-visible lock
157
+ * release from git means a transient push failure no longer wedges the
158
+ * editor, and a process crash mid-commit becomes resumable instead of
159
+ * leaving an orphan on disk. See `lock-decoupling-plan.md` for the full
160
+ * design.
161
+ *
162
+ * The autosave-checkpoint timer and explicit `commitChange` calls land
163
+ * commits directly (no queue); `releaseLock` is queue-only.
164
+ */
165
+ releaseLock(workspaceId: string, branch: string, path: string, user: AuthUser): Promise<void>;
166
+ /**
167
+ * Drop the lock **without committing**. Used on the failure path of a
168
+ * lock-aware write — when the underlying op threw, we don't want the
169
+ * normal `releaseLock` to commit + push whatever partial state happens
170
+ * to be on disk. The lock row goes away (so the next caller can edit
171
+ * the file); the working tree is left as-is. Idempotent like
172
+ * `releaseLock`: a no-op when the caller doesn't hold the lock.
173
+ */
174
+ releaseLockNoCommit(workspaceId: string, branch: string, path: string, user: AuthUser): Promise<void>;
175
+ /** Current lock holder for `(branch, path)`, or null when nobody holds it. */
176
+ getLock(workspaceId: string, branch: string, path: string): Promise<FileLock | null>;
177
+ listChangeRequests(opts?: {
178
+ fresh?: boolean;
179
+ }): Promise<ChangeRequest[]>;
180
+ /** Change requests authored by the caller (hash-matched against the body marker). */
181
+ listChangeRequestsAuthoredBy(emailOrLogin: string): Promise<ChangeRequest[]>;
182
+ /** Change requests touching files the user has write permission on. */
183
+ listChangeRequestsForUser(workspaceId: string, email: string, opts?: {
184
+ fresh?: boolean;
185
+ }): Promise<ChangeRequest[]>;
186
+ getChangeRequest(number: number): Promise<ChangeRequest | null>;
187
+ getChangeRequestDetail(number: number, opts?: {
188
+ fresh?: boolean;
189
+ workspaceId?: string;
190
+ viewerEmail?: string;
191
+ }): Promise<ChangeRequestDetail | null>;
192
+ /**
193
+ * Open a new change request. Per spec, auto-merges `targetBranch` into
194
+ * `sourceBranch` and surfaces the resulting diff as the changes to review.
195
+ * Throws `NotImplementedWorkflowError` until the backing create-PR path
196
+ * lands in the workflow module (today the agent shells `gh pr create`
197
+ * directly).
198
+ */
199
+ openChangeRequest(workspaceId: string, user: AuthUser, input: OpenChangeRequestInput): Promise<ChangeRequestDetail>;
200
+ /**
201
+ * Re-run `targetBranch → sourceBranch` merge on an existing change request.
202
+ * Used when the target has advanced since the change request was opened.
203
+ * Throws `NotImplementedWorkflowError` until the backing merge path lands.
204
+ */
205
+ updateFromTarget(workspaceId: string, user: AuthUser, number: number): Promise<ChangeRequestDetail>;
206
+ listComments(number: number): Promise<ChangeRequestComment[]>;
207
+ postComment(number: number, user: AuthUser, input: PostChangeRequestCommentInput, headSha: string): Promise<ChangeRequestComment>;
208
+ editComment(commentId: string, number: number, user: AuthUser, body: string): Promise<ChangeRequestComment>;
209
+ deleteComment(commentId: string, number: number, user: AuthUser): Promise<void>;
210
+ approveFile(number: number, path: string, user: AuthUser, files: ChangedFile[], headSha: string, baseBranch: string, authorIdHash: string | null, workspaceId: string): Promise<FileApproval[]>;
211
+ unapproveFile(number: number, path: string, user: AuthUser, files: ChangedFile[], headSha: string, baseBranch: string, authorIdHash: string | null, workspaceId: string): Promise<FileApproval[]>;
212
+ /**
213
+ * Reject a change request without merging. Authorized for the author or
214
+ * for users with write permission to every changed file on the target
215
+ * branch.
216
+ */
217
+ rejectChangeRequest(number: number, user: AuthUser, state: ChangeRequestState, authorIdHash: string | null, baseBranch: string, workspaceId: string): Promise<CancelChangeRequestResult>;
218
+ /**
219
+ * Merge a change request. The workflow tries a direct merge first; on
220
+ * conflicts it returns a `conflicts-need-resolution` outcome and the
221
+ * caller (typically the agent) is responsible for resolving them as new
222
+ * changes on the source branch. Once the source contains target cleanly,
223
+ * a subsequent `mergeChangeRequest` call lands the merge.
224
+ *
225
+ * The hard merge invariant — only this method may invoke `gh pr merge` —
226
+ * is preserved through the facade by delegating to
227
+ * `IReviewWorkflowService.mergePr`.
228
+ */
229
+ mergeChangeRequest(number: number, user: AuthUser, headSha: string, approvals: FileApproval[], state: ChangeRequestState, title: string, baseBranch: string, workspaceId: string, opts?: {
230
+ bypass?: boolean;
231
+ }): Promise<MergeChangeRequestOutcome>;
232
+ }
233
+ //# sourceMappingURL=interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../src/workflow/interface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EACV,iBAAiB,EACjB,MAAM,EACN,qBAAqB,EACrB,yBAAyB,EACzB,MAAM,EACN,WAAW,EACX,aAAa,EACb,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,EAClB,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,yBAAyB,EACzB,sBAAsB,EACtB,6BAA6B,EAC9B,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,gBAAgB;IAG/B,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACtF;;;;OAIG;IACH,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpF;;;;;;;;;OASG;IACH,YAAY,CACV,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,QAAQ,EACd,IAAI,CAAC,EAAE;QAAE,cAAc,CAAC,EAAE,OAAO,CAAA;KAAE,GAClC,OAAO,CAAC,IAAI,CAAC,CAAC;IAQjB,gGAAgG;IAChG,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAGlE;;;;OAIG;IACH,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5F,4DAA4D;IAC5D,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,8EAA8E;IAC9E,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD;;;;OAIG;IACH,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE;;;;OAIG;IACH,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7E;;;;OAIG;IACH,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAQ/D;;;;;;OAMG;IACH,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvF;;;;;;;;;OASG;IACH,aAAa,CACX,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,QAAQ,EACd,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1B,+DAA+D;IAC/D,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACzF;;;;OAIG;IACH,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAChF;;;;;;OAMG;IACH,WAAW,CACT,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB;;;OAGG;IACH,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAIlF;;;;;;;;OAQG;IACH,WAAW,CACT,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,QAAQ,GACb,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC9B,qEAAqE;IACrE,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpG;;;;;;;;OAQG;IACH,qBAAqB,CACnB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,QAAQ,EACd,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAE1B;;;;;;;;;;;;;;;;OAgBG;IACH,WAAW,CACT,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,QAAQ,GACb,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB;;;;;;;OAOG;IACH,mBAAmB,CACjB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,QAAQ,GACb,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,8EAA8E;IAC9E,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IAIrF,kBAAkB,CAAC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;IACzE,qFAAqF;IACrF,4BAA4B,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;IAC7E,uEAAuE;IACvE,yBAAyB,CACvB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GACzB,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;IAC5B,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAChE,sBAAsB,CACpB,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GACrE,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;IAEvC;;;;;;OAMG;IACH,iBAAiB,CACf,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,sBAAsB,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAEhC;;;;OAIG;IACH,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAGpG,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAC;IAC9D,WAAW,CACT,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,6BAA6B,EACpC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACjC,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC5G,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAIhF,WAAW,CACT,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,WAAW,EAAE,EACpB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAC3B,aAAa,CACX,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,WAAW,EAAE,EACpB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAE3B;;;;OAIG;IACH,mBAAmB,CACjB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,kBAAkB,EACzB,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,yBAAyB,CAAC,CAAC;IAEtC;;;;;;;;;;OAUG;IACH,kBAAkB,CAChB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,QAAQ,EACd,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,YAAY,EAAE,EACzB,KAAK,EAAE,kBAAkB,EACzB,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAC1B,OAAO,CAAC,yBAAyB,CAAC,CAAC;CACvC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `IWorkflowService` — the abstraction the rest of the app talks to instead
3
+ * of raw git/gh. See `bevel-platform/PLAN.md` for the motivation and the spec the
4
+ * methods here encode (protected branches, atomic per-file changes, file
5
+ * locks, change requests as branch-pair reconciliations).
6
+ *
7
+ * Naming convention: methods speak workflow vocabulary (`commitChange`,
8
+ * `openChangeRequest`, `acquireLock`), never git vocabulary (`commit`,
9
+ * `createPr`, no analogue for locks). Implementations may delegate to the
10
+ * existing git/PR/review-workflow services until those are folded into
11
+ * `modules/workflow`; callers should not depend on that fact.
12
+ *
13
+ * Some methods on this interface have no backing service yet — file locks,
14
+ * `openChangeRequest`, `updateFromTarget`. The facade implementation throws
15
+ * `NotImplementedWorkflowError` from those until the underlying support
16
+ * lands. The interface defines them up-front so consumers can plan against
17
+ * the final surface area.
18
+ */
19
+ export {};
20
+ //# sourceMappingURL=interface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interface.js","sourceRoot":"","sources":["../../src/workflow/interface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Workflow vocabulary — the abstraction our app speaks instead of raw git/gh.
3
+ *
4
+ * Most types here alias existing engineering types in `@bevel-software/platform-shared/git/*` so
5
+ * the workflow layer is a single source of truth without duplicating the
6
+ * payload shapes our backend already produces. The point of the aliases is the
7
+ * *name*: callers depend on `Change` / `ChangeRequest` / `Branch`, never on
8
+ * `Commit` / `PullRequest` / `Branch` from the git module. When we eventually
9
+ * collapse `modules/git` and `modules/review-workflow` into `modules/workflow`,
10
+ * the alias targets move with the implementation; the consumer-facing names
11
+ * don't need to.
12
+ *
13
+ * New types that have no git counterpart (file locks, change-request open
14
+ * inputs, conflict-resolution payloads) live here directly.
15
+ */
16
+ import type { BranchInfo, CommitAttribution, ShareChangesRequest, WorkingTreeStatus } from '../git/types.js';
17
+ import type { CancelPrResult, FileApprovalState, MergePrResult, PullRequestDetail, PullRequestState, PullRequestSummary, PostPrCommentInput, PrReviewComment, PullRequestFile } from '../git/pr.types.js';
18
+ /** A variation of the ontology. Protected or not. */
19
+ export type Branch = BranchInfo;
20
+ /**
21
+ * Snapshot of the workspace tied to a branch — currently-checked-out branch,
22
+ * dirty bit, unshared commit count. Mirrors `WorkingTreeStatus` from the git
23
+ * module. Renames to `BranchWorkspaceStatus` in the workflow domain to signal
24
+ * "this describes the branch's workspace state", not raw git plumbing.
25
+ */
26
+ export type BranchWorkspaceStatus = WorkingTreeStatus;
27
+ /**
28
+ * A change is the atomic unit of history. Each change affects exactly one file.
29
+ * Creating an empty folder is modeled as a change creating a `.gitkeep` in it.
30
+ * Renames are a single change touching one path with the old path removed in
31
+ * the same atomic operation. Aliases `CommitAttribution` because that's
32
+ * exactly the shape the backend already returns for a single commit.
33
+ */
34
+ export type Change = CommitAttribution;
35
+ /**
36
+ * Input shape for committing a change. `summary` is required and single-line;
37
+ * `description` is optional and supports a multi-line body. Aliases
38
+ * `ShareChangesRequest` for now — once the workflow module owns the validator
39
+ * gate end-to-end, this shape may grow workflow-specific fields (lock token,
40
+ * forced single-path scope).
41
+ */
42
+ export type ChangeInput = ShareChangesRequest;
43
+ /**
44
+ * A live edit lock on one (branch, path). Per the workflow spec:
45
+ * - acquired on first edit in edit mode, or on agent write
46
+ * - per-(branch, path); the same file on another branch is independently lockable
47
+ * - autosave checkpoint timer extends the lock and produces intermediate changes
48
+ * - stale locks (no heartbeat past TTL) auto-release so a disconnected client
49
+ * can't hold a file hostage
50
+ *
51
+ * Holder identity carries both `userId` (the join key against the users table)
52
+ * and `holderName` (display name, denormalised so consumers can render a
53
+ * "locked by Alice" badge without an extra round-trip). For agent-driven
54
+ * writes the holder is the user driving the agent.
55
+ */
56
+ export interface FileLock {
57
+ branch: string;
58
+ path: string;
59
+ holderUserId: string;
60
+ holderName: string;
61
+ /** ISO timestamp when the lock was first acquired. */
62
+ acquiredAt: string;
63
+ /** ISO timestamp of the most recent heartbeat — the autosave/keepalive ping. */
64
+ lastHeartbeatAt: string;
65
+ /** ISO timestamp after which a non-heartbeated lock is considered stale and may be reclaimed. */
66
+ expiresAt: string;
67
+ }
68
+ /**
69
+ * Result of attempting to acquire a lock. `acquired: false` carries the
70
+ * current holder so the UI can render "Locked by X" without a follow-up call.
71
+ */
72
+ export type AcquireLockResult = {
73
+ acquired: true;
74
+ lock: FileLock;
75
+ } | {
76
+ acquired: false;
77
+ lock: FileLock;
78
+ };
79
+ /**
80
+ * A change request is the reconciliation of two branches. In the current
81
+ * implementation this is backed by a GitHub pull request; the alias hides
82
+ * that from callers. `state` values map 1:1 (`open`/`merged`/`closed`).
83
+ *
84
+ * Opening a change request is defined in the spec as auto-merging target →
85
+ * source first; that part isn't yet implemented in the backing services, so
86
+ * `openChangeRequest` on `IWorkflowService` currently throws
87
+ * `NotImplementedWorkflowError`.
88
+ */
89
+ export type ChangeRequest = PullRequestSummary;
90
+ export type ChangeRequestDetail = PullRequestDetail;
91
+ export type ChangeRequestState = PullRequestState;
92
+ export type ChangedFile = PullRequestFile;
93
+ export type FileApproval = FileApprovalState;
94
+ export type MergeChangeRequestResult = MergePrResult;
95
+ export type CancelChangeRequestResult = CancelPrResult;
96
+ export type ChangeRequestComment = PrReviewComment;
97
+ export type PostChangeRequestCommentInput = PostPrCommentInput;
98
+ /**
99
+ * Input for opening a new change request. The workflow auto-merges
100
+ * `targetBranch` into `sourceBranch` as part of opening, so callers don't
101
+ * pass a head SHA — the workflow resolves the head itself.
102
+ *
103
+ * `title` + `description` are the change request's metadata; the description
104
+ * is plain text (no GitHub-flavoured markdown affordances exposed at this
105
+ * layer). The implementation may still render this as the PR body when the
106
+ * underlying GitHub PR is created.
107
+ */
108
+ export interface OpenChangeRequestInput {
109
+ sourceBranch: string;
110
+ targetBranch: string;
111
+ title: string;
112
+ description?: string;
113
+ }
114
+ /**
115
+ * Result of asking the workflow to merge a change request. Mirrors
116
+ * `MergePrResult` — caller learns the resulting change (commit) sha and the
117
+ * timestamp the merge landed.
118
+ *
119
+ * When the merge attempt hits conflicts the workflow falls back to an
120
+ * agent-driven resolution path; the caller does NOT get a `MergeChangeRequestResult`
121
+ * back in that case — they get a `ConflictPending` shape instead so the UI
122
+ * can route into the resolution flow.
123
+ */
124
+ export type MergeChangeRequestOutcome = {
125
+ kind: 'merged';
126
+ result: MergeChangeRequestResult;
127
+ } | {
128
+ kind: 'conflicts-need-resolution';
129
+ conflictedPaths: string[];
130
+ };
131
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/workflow/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,eAAe,EACf,eAAe,EAChB,MAAM,oBAAoB,CAAC;AAI5B,qDAAqD;AACrD,MAAM,MAAM,MAAM,GAAG,UAAU,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,MAAM,qBAAqB,GAAG,iBAAiB,CAAC;AAItD;;;;;;GAMG;AACH,MAAM,MAAM,MAAM,GAAG,iBAAiB,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG,mBAAmB,CAAC;AAI9C;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,eAAe,EAAE,MAAM,CAAC;IACxB,iGAAiG;IACjG,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClC;IAAE,QAAQ,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAIxC;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GAAG,kBAAkB,CAAC;AAC/C,MAAM,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AACpD,MAAM,MAAM,kBAAkB,GAAG,gBAAgB,CAAC;AAClD,MAAM,MAAM,WAAW,GAAG,eAAe,CAAC;AAE1C,MAAM,MAAM,YAAY,GAAG,iBAAiB,CAAC;AAC7C,MAAM,MAAM,wBAAwB,GAAG,aAAa,CAAC;AACrD,MAAM,MAAM,yBAAyB,GAAG,cAAc,CAAC;AACvD,MAAM,MAAM,oBAAoB,GAAG,eAAe,CAAC;AACnD,MAAM,MAAM,6BAA6B,GAAG,kBAAkB,CAAC;AAE/D;;;;;;;;;GASG;AACH,MAAM,WAAW,sBAAsB;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,yBAAyB,GACjC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,wBAAwB,CAAA;CAAE,GACpD;IAAE,IAAI,EAAE,2BAA2B,CAAC;IAAC,eAAe,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Workflow vocabulary — the abstraction our app speaks instead of raw git/gh.
3
+ *
4
+ * Most types here alias existing engineering types in `@bevel-software/platform-shared/git/*` so
5
+ * the workflow layer is a single source of truth without duplicating the
6
+ * payload shapes our backend already produces. The point of the aliases is the
7
+ * *name*: callers depend on `Change` / `ChangeRequest` / `Branch`, never on
8
+ * `Commit` / `PullRequest` / `Branch` from the git module. When we eventually
9
+ * collapse `modules/git` and `modules/review-workflow` into `modules/workflow`,
10
+ * the alias targets move with the implementation; the consumer-facing names
11
+ * don't need to.
12
+ *
13
+ * New types that have no git counterpart (file locks, change-request open
14
+ * inputs, conflict-resolution payloads) live here directly.
15
+ */
16
+ export {};
17
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/workflow/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Cross-platform filename validation. A name that passes here must be writable
3
+ * as-is on Windows (NTFS), macOS (APFS), and Linux (ext4) — so the same
4
+ * workspace can round-trip between dev machines, the backend container, and
5
+ * any user's local clone without `git checkout` failing on a forbidden name.
6
+ *
7
+ * Used by:
8
+ * - frontend: gate the create / rename inputs in FileExplorer
9
+ * - backend: reject API requests at the workspace service boundary
10
+ * - agent prompt: the same rules are described to the LLM so it picks valid
11
+ * names when authoring KB files
12
+ *
13
+ * Keep this file dependency-free — both bundle targets import it directly.
14
+ */
15
+ /**
16
+ * Validate a single path component (file or folder name — no `/`, no `\`).
17
+ * Returns `null` if valid, or a short reason string suitable for surfacing
18
+ * in a UI error or API 400 body.
19
+ */
20
+ export declare function validateFilename(name: string): string | null;
21
+ /**
22
+ * Validate a workspace-relative path: a `/`-separated string of components.
23
+ * Each component is checked with {@link validateFilename}. Backslashes are
24
+ * rejected outright — the workspace API uses `/` everywhere and accepting
25
+ * `\` would round-trip differently on Windows vs the Linux container.
26
+ */
27
+ export declare function validateRelativePath(relativePath: string): string | null;
28
+ /**
29
+ * Throwing wrapper for the backend service layer — keeps call sites a single
30
+ * line and produces a clear `Error` the route handler can surface as a 400.
31
+ */
32
+ export declare function assertValidRelativePath(relativePath: string): void;
33
+ //# sourceMappingURL=filename.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filename.d.ts","sourceRoot":"","sources":["../../src/workspace/filename.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAsBH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA2B5D;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBxE;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAGlE"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Cross-platform filename validation. A name that passes here must be writable
3
+ * as-is on Windows (NTFS), macOS (APFS), and Linux (ext4) — so the same
4
+ * workspace can round-trip between dev machines, the backend container, and
5
+ * any user's local clone without `git checkout` failing on a forbidden name.
6
+ *
7
+ * Used by:
8
+ * - frontend: gate the create / rename inputs in FileExplorer
9
+ * - backend: reject API requests at the workspace service boundary
10
+ * - agent prompt: the same rules are described to the LLM so it picks valid
11
+ * names when authoring KB files
12
+ *
13
+ * Keep this file dependency-free — both bundle targets import it directly.
14
+ */
15
+ /** Windows-reserved device names. Case-insensitive; the bare name AND
16
+ * `<name>.<ext>` are both reserved (e.g. `NUL.txt` is invalid on Windows). */
17
+ const WINDOWS_RESERVED_NAMES = new Set([
18
+ 'CON', 'PRN', 'AUX', 'NUL',
19
+ 'COM0', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
20
+ 'LPT0', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
21
+ ]);
22
+ /** Characters Windows forbids in any filename component. `/` is the path
23
+ * separator on Unix, included for the same reason. `\` is also a path
24
+ * separator on Windows. Control chars (0x00–0x1F) are rejected separately. */
25
+ // eslint-disable-next-line no-control-regex
26
+ const FORBIDDEN_CHARS = /[<>:"/\\|?*\x00-\x1F]/;
27
+ /** Per-component byte limit: NTFS = 255 UTF-16 units, ext4 = 255 bytes,
28
+ * APFS = 255 UTF-8 bytes. Use UTF-8 bytes — the strictest of the three. */
29
+ const MAX_NAME_BYTES = 255;
30
+ const utf8 = new TextEncoder();
31
+ /**
32
+ * Validate a single path component (file or folder name — no `/`, no `\`).
33
+ * Returns `null` if valid, or a short reason string suitable for surfacing
34
+ * in a UI error or API 400 body.
35
+ */
36
+ export function validateFilename(name) {
37
+ if (typeof name !== 'string')
38
+ return 'Name is required';
39
+ if (name.length === 0)
40
+ return 'Name cannot be empty';
41
+ if (name !== name.trim())
42
+ return 'Name cannot start or end with whitespace';
43
+ if (name === '.' || name === '..')
44
+ return 'Name cannot be "." or ".."';
45
+ if (FORBIDDEN_CHARS.test(name)) {
46
+ return 'Name cannot contain any of these characters: < > : " / \\ | ? *';
47
+ }
48
+ // Windows trims trailing dots and spaces silently — a name ending in either
49
+ // becomes a different filename on disk, breaking sync between OSes.
50
+ if (name.endsWith('.') || name.endsWith(' ')) {
51
+ return 'Name cannot end with a "." or a space';
52
+ }
53
+ // Windows reserved device names — match `NAME` and `NAME.ext`, case-insensitive.
54
+ const stem = name.split('.')[0].toUpperCase();
55
+ if (WINDOWS_RESERVED_NAMES.has(stem)) {
56
+ return `"${name}" is a reserved system name on Windows`;
57
+ }
58
+ if (utf8.encode(name).length > MAX_NAME_BYTES) {
59
+ return `Name is too long (max ${MAX_NAME_BYTES} bytes)`;
60
+ }
61
+ return null;
62
+ }
63
+ /**
64
+ * Validate a workspace-relative path: a `/`-separated string of components.
65
+ * Each component is checked with {@link validateFilename}. Backslashes are
66
+ * rejected outright — the workspace API uses `/` everywhere and accepting
67
+ * `\` would round-trip differently on Windows vs the Linux container.
68
+ */
69
+ export function validateRelativePath(relativePath) {
70
+ if (typeof relativePath !== 'string' || relativePath.length === 0) {
71
+ return 'Path is required';
72
+ }
73
+ if (relativePath.includes('\\')) {
74
+ return 'Path cannot contain "\\" — use "/" as the separator';
75
+ }
76
+ // Strip a single leading `./` so callers that build paths from a base dir
77
+ // (`./<name>`) don't get a spurious "." segment rejection.
78
+ const normalized = relativePath.replace(/^\.\//, '');
79
+ const segments = normalized.split('/').filter((s) => s.length > 0);
80
+ if (segments.length === 0)
81
+ return 'Path is required';
82
+ for (const segment of segments) {
83
+ const reason = validateFilename(segment);
84
+ if (reason)
85
+ return `Invalid path segment "${segment}": ${reason}`;
86
+ }
87
+ return null;
88
+ }
89
+ /**
90
+ * Throwing wrapper for the backend service layer — keeps call sites a single
91
+ * line and produces a clear `Error` the route handler can surface as a 400.
92
+ */
93
+ export function assertValidRelativePath(relativePath) {
94
+ const reason = validateRelativePath(relativePath);
95
+ if (reason)
96
+ throw new Error(`Invalid path: ${reason}`);
97
+ }
98
+ //# sourceMappingURL=filename.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filename.js","sourceRoot":"","sources":["../../src/workspace/filename.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The ONE `---` frontmatter splitter, shared by backend and frontend so no file
3
+ * type grows its own regex. Splits a leading `---`-fenced YAML block from the
4
+ * body; null when the text doesn't open with a fence. Parsing the YAML inside is
5
+ * the caller's concern (the access-control resolver deliberately keeps its own
6
+ * hardened line-based reader — see `modules/access/access-splice.ts`).
7
+ */
8
+ export declare function extractFrontmatter(text: string): {
9
+ frontmatter: string;
10
+ body: string;
11
+ } | null;
12
+ /**
13
+ * Set a top-level scalar key in a file's `---` frontmatter, LINE-based: replace an
14
+ * existing `<key>:` line's value, else insert `<key>: <value>` at the top of the
15
+ * block (creating a `---` block if the file has none). Deliberately NOT a
16
+ * parse-and-re-serialize — a KB node's `nodeType:` quoted-markdown-link and access
17
+ * lines are read by a hand-rolled regex parser that a full YAML re-emit would
18
+ * corrupt, so every other line is preserved byte-for-byte. `value` is emitted as a
19
+ * bare scalar; callers pass simple id/name strings (no quoting/escaping needed for
20
+ * the `[a-z0-9_-]` id grammar).
21
+ */
22
+ export declare function setFrontmatterField(text: string, key: string, value: string): string;
23
+ //# sourceMappingURL=frontmatter.d.ts.map