@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.
- package/LICENSE +202 -0
- package/dist/auth/types.d.ts +15 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/chat/types.d.ts +39 -0
- package/dist/chat/types.d.ts.map +1 -0
- package/dist/chat/types.js +13 -0
- package/dist/chat/types.js.map +1 -0
- package/dist/git/branchAuthor.d.ts +28 -0
- package/dist/git/branchAuthor.d.ts.map +1 -0
- package/dist/git/branchAuthor.js +45 -0
- package/dist/git/branchAuthor.js.map +1 -0
- package/dist/git/pr.types.d.ts +264 -0
- package/dist/git/pr.types.d.ts.map +1 -0
- package/dist/git/pr.types.js +2 -0
- package/dist/git/pr.types.js.map +1 -0
- package/dist/git/protected.d.ts +52 -0
- package/dist/git/protected.d.ts.map +1 -0
- package/dist/git/protected.js +92 -0
- package/dist/git/protected.js.map +1 -0
- package/dist/git/review.types.d.ts +49 -0
- package/dist/git/review.types.d.ts.map +1 -0
- package/dist/git/review.types.js +2 -0
- package/dist/git/review.types.js.map +1 -0
- package/dist/git/types.d.ts +107 -0
- package/dist/git/types.d.ts.map +1 -0
- package/dist/git/types.js +2 -0
- package/dist/git/types.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/workflow/events.d.ts +218 -0
- package/dist/workflow/events.d.ts.map +1 -0
- package/dist/workflow/events.js +48 -0
- package/dist/workflow/events.js.map +1 -0
- package/dist/workflow/interface.d.ts +233 -0
- package/dist/workflow/interface.d.ts.map +1 -0
- package/dist/workflow/interface.js +20 -0
- package/dist/workflow/interface.js.map +1 -0
- package/dist/workflow/types.d.ts +131 -0
- package/dist/workflow/types.d.ts.map +1 -0
- package/dist/workflow/types.js +17 -0
- package/dist/workflow/types.js.map +1 -0
- package/dist/workspace/filename.d.ts +33 -0
- package/dist/workspace/filename.d.ts.map +1 -0
- package/dist/workspace/filename.js +98 -0
- package/dist/workspace/filename.js.map +1 -0
- package/dist/workspace/frontmatter.d.ts +23 -0
- package/dist/workspace/frontmatter.d.ts.map +1 -0
- package/dist/workspace/frontmatter.js +48 -0
- package/dist/workspace/frontmatter.js.map +1 -0
- package/dist/workspace/kb-layout.d.ts +66 -0
- package/dist/workspace/kb-layout.d.ts.map +1 -0
- package/dist/workspace/kb-layout.js +63 -0
- package/dist/workspace/kb-layout.js.map +1 -0
- package/dist/workspace/types.d.ts +57 -0
- package/dist/workspace/types.d.ts.map +1 -0
- package/dist/workspace/types.js +2 -0
- package/dist/workspace/types.js.map +1 -0
- package/package.json +41 -0
- package/src/auth/types.ts +16 -0
- package/src/chat/types.ts +53 -0
- package/src/git/branchAuthor.ts +46 -0
- package/src/git/pr.types.ts +274 -0
- package/src/git/protected.ts +121 -0
- package/src/git/review.types.ts +51 -0
- package/src/git/types.ts +170 -0
- package/src/index.ts +23 -0
- package/src/workflow/events.ts +271 -0
- package/src/workflow/interface.ts +357 -0
- package/src/workflow/types.ts +156 -0
- package/src/workspace/filename.ts +102 -0
- package/src/workspace/frontmatter.ts +45 -0
- package/src/workspace/kb-layout.ts +81 -0
- package/src/workspace/types.ts +59 -0
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
|
|
17
|
+
import type {
|
|
18
|
+
BranchInfo,
|
|
19
|
+
CommitAttribution,
|
|
20
|
+
ShareChangesRequest,
|
|
21
|
+
WorkingTreeStatus,
|
|
22
|
+
} from '../git/types.js';
|
|
23
|
+
import type {
|
|
24
|
+
CancelPrResult,
|
|
25
|
+
FileApprovalState,
|
|
26
|
+
MergePrResult,
|
|
27
|
+
PullRequestDetail,
|
|
28
|
+
PullRequestState,
|
|
29
|
+
PullRequestSummary,
|
|
30
|
+
PostPrCommentInput,
|
|
31
|
+
PrReviewComment,
|
|
32
|
+
PullRequestFile,
|
|
33
|
+
} from '../git/pr.types.js';
|
|
34
|
+
|
|
35
|
+
// ── Branches ─────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/** A variation of the ontology. Protected or not. */
|
|
38
|
+
export type Branch = BranchInfo;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Snapshot of the workspace tied to a branch — currently-checked-out branch,
|
|
42
|
+
* dirty bit, unshared commit count. Mirrors `WorkingTreeStatus` from the git
|
|
43
|
+
* module. Renames to `BranchWorkspaceStatus` in the workflow domain to signal
|
|
44
|
+
* "this describes the branch's workspace state", not raw git plumbing.
|
|
45
|
+
*/
|
|
46
|
+
export type BranchWorkspaceStatus = WorkingTreeStatus;
|
|
47
|
+
|
|
48
|
+
// ── Changes ──────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A change is the atomic unit of history. Each change affects exactly one file.
|
|
52
|
+
* Creating an empty folder is modeled as a change creating a `.gitkeep` in it.
|
|
53
|
+
* Renames are a single change touching one path with the old path removed in
|
|
54
|
+
* the same atomic operation. Aliases `CommitAttribution` because that's
|
|
55
|
+
* exactly the shape the backend already returns for a single commit.
|
|
56
|
+
*/
|
|
57
|
+
export type Change = CommitAttribution;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Input shape for committing a change. `summary` is required and single-line;
|
|
61
|
+
* `description` is optional and supports a multi-line body. Aliases
|
|
62
|
+
* `ShareChangesRequest` for now — once the workflow module owns the validator
|
|
63
|
+
* gate end-to-end, this shape may grow workflow-specific fields (lock token,
|
|
64
|
+
* forced single-path scope).
|
|
65
|
+
*/
|
|
66
|
+
export type ChangeInput = ShareChangesRequest;
|
|
67
|
+
|
|
68
|
+
// ── File locks (new — no git counterpart) ────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A live edit lock on one (branch, path). Per the workflow spec:
|
|
72
|
+
* - acquired on first edit in edit mode, or on agent write
|
|
73
|
+
* - per-(branch, path); the same file on another branch is independently lockable
|
|
74
|
+
* - autosave checkpoint timer extends the lock and produces intermediate changes
|
|
75
|
+
* - stale locks (no heartbeat past TTL) auto-release so a disconnected client
|
|
76
|
+
* can't hold a file hostage
|
|
77
|
+
*
|
|
78
|
+
* Holder identity carries both `userId` (the join key against the users table)
|
|
79
|
+
* and `holderName` (display name, denormalised so consumers can render a
|
|
80
|
+
* "locked by Alice" badge without an extra round-trip). For agent-driven
|
|
81
|
+
* writes the holder is the user driving the agent.
|
|
82
|
+
*/
|
|
83
|
+
export interface FileLock {
|
|
84
|
+
branch: string;
|
|
85
|
+
path: string;
|
|
86
|
+
holderUserId: string;
|
|
87
|
+
holderName: string;
|
|
88
|
+
/** ISO timestamp when the lock was first acquired. */
|
|
89
|
+
acquiredAt: string;
|
|
90
|
+
/** ISO timestamp of the most recent heartbeat — the autosave/keepalive ping. */
|
|
91
|
+
lastHeartbeatAt: string;
|
|
92
|
+
/** ISO timestamp after which a non-heartbeated lock is considered stale and may be reclaimed. */
|
|
93
|
+
expiresAt: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Result of attempting to acquire a lock. `acquired: false` carries the
|
|
98
|
+
* current holder so the UI can render "Locked by X" without a follow-up call.
|
|
99
|
+
*/
|
|
100
|
+
export type AcquireLockResult =
|
|
101
|
+
| { acquired: true; lock: FileLock }
|
|
102
|
+
| { acquired: false; lock: FileLock };
|
|
103
|
+
|
|
104
|
+
// ── Change Requests ──────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A change request is the reconciliation of two branches. In the current
|
|
108
|
+
* implementation this is backed by a GitHub pull request; the alias hides
|
|
109
|
+
* that from callers. `state` values map 1:1 (`open`/`merged`/`closed`).
|
|
110
|
+
*
|
|
111
|
+
* Opening a change request is defined in the spec as auto-merging target →
|
|
112
|
+
* source first; that part isn't yet implemented in the backing services, so
|
|
113
|
+
* `openChangeRequest` on `IWorkflowService` currently throws
|
|
114
|
+
* `NotImplementedWorkflowError`.
|
|
115
|
+
*/
|
|
116
|
+
export type ChangeRequest = PullRequestSummary;
|
|
117
|
+
export type ChangeRequestDetail = PullRequestDetail;
|
|
118
|
+
export type ChangeRequestState = PullRequestState;
|
|
119
|
+
export type ChangedFile = PullRequestFile;
|
|
120
|
+
|
|
121
|
+
export type FileApproval = FileApprovalState;
|
|
122
|
+
export type MergeChangeRequestResult = MergePrResult;
|
|
123
|
+
export type CancelChangeRequestResult = CancelPrResult;
|
|
124
|
+
export type ChangeRequestComment = PrReviewComment;
|
|
125
|
+
export type PostChangeRequestCommentInput = PostPrCommentInput;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Input for opening a new change request. The workflow auto-merges
|
|
129
|
+
* `targetBranch` into `sourceBranch` as part of opening, so callers don't
|
|
130
|
+
* pass a head SHA — the workflow resolves the head itself.
|
|
131
|
+
*
|
|
132
|
+
* `title` + `description` are the change request's metadata; the description
|
|
133
|
+
* is plain text (no GitHub-flavoured markdown affordances exposed at this
|
|
134
|
+
* layer). The implementation may still render this as the PR body when the
|
|
135
|
+
* underlying GitHub PR is created.
|
|
136
|
+
*/
|
|
137
|
+
export interface OpenChangeRequestInput {
|
|
138
|
+
sourceBranch: string;
|
|
139
|
+
targetBranch: string;
|
|
140
|
+
title: string;
|
|
141
|
+
description?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Result of asking the workflow to merge a change request. Mirrors
|
|
146
|
+
* `MergePrResult` — caller learns the resulting change (commit) sha and the
|
|
147
|
+
* timestamp the merge landed.
|
|
148
|
+
*
|
|
149
|
+
* When the merge attempt hits conflicts the workflow falls back to an
|
|
150
|
+
* agent-driven resolution path; the caller does NOT get a `MergeChangeRequestResult`
|
|
151
|
+
* back in that case — they get a `ConflictPending` shape instead so the UI
|
|
152
|
+
* can route into the resolution flow.
|
|
153
|
+
*/
|
|
154
|
+
export type MergeChangeRequestOutcome =
|
|
155
|
+
| { kind: 'merged'; result: MergeChangeRequestResult }
|
|
156
|
+
| { kind: 'conflicts-need-resolution'; conflictedPaths: string[] };
|
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
/** Windows-reserved device names. Case-insensitive; the bare name AND
|
|
17
|
+
* `<name>.<ext>` are both reserved (e.g. `NUL.txt` is invalid on Windows). */
|
|
18
|
+
const WINDOWS_RESERVED_NAMES = new Set([
|
|
19
|
+
'CON', 'PRN', 'AUX', 'NUL',
|
|
20
|
+
'COM0', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
|
|
21
|
+
'LPT0', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/** Characters Windows forbids in any filename component. `/` is the path
|
|
25
|
+
* separator on Unix, included for the same reason. `\` is also a path
|
|
26
|
+
* separator on Windows. Control chars (0x00–0x1F) are rejected separately. */
|
|
27
|
+
// eslint-disable-next-line no-control-regex
|
|
28
|
+
const FORBIDDEN_CHARS = /[<>:"/\\|?*\x00-\x1F]/;
|
|
29
|
+
|
|
30
|
+
/** Per-component byte limit: NTFS = 255 UTF-16 units, ext4 = 255 bytes,
|
|
31
|
+
* APFS = 255 UTF-8 bytes. Use UTF-8 bytes — the strictest of the three. */
|
|
32
|
+
const MAX_NAME_BYTES = 255;
|
|
33
|
+
|
|
34
|
+
const utf8 = new TextEncoder();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate a single path component (file or folder name — no `/`, no `\`).
|
|
38
|
+
* Returns `null` if valid, or a short reason string suitable for surfacing
|
|
39
|
+
* in a UI error or API 400 body.
|
|
40
|
+
*/
|
|
41
|
+
export function validateFilename(name: string): string | null {
|
|
42
|
+
if (typeof name !== 'string') return 'Name is required';
|
|
43
|
+
if (name.length === 0) return 'Name cannot be empty';
|
|
44
|
+
if (name !== name.trim()) return 'Name cannot start or end with whitespace';
|
|
45
|
+
if (name === '.' || name === '..') return 'Name cannot be "." or ".."';
|
|
46
|
+
|
|
47
|
+
if (FORBIDDEN_CHARS.test(name)) {
|
|
48
|
+
return 'Name cannot contain any of these characters: < > : " / \\ | ? *';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Windows trims trailing dots and spaces silently — a name ending in either
|
|
52
|
+
// becomes a different filename on disk, breaking sync between OSes.
|
|
53
|
+
if (name.endsWith('.') || name.endsWith(' ')) {
|
|
54
|
+
return 'Name cannot end with a "." or a space';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Windows reserved device names — match `NAME` and `NAME.ext`, case-insensitive.
|
|
58
|
+
const stem = name.split('.')[0].toUpperCase();
|
|
59
|
+
if (WINDOWS_RESERVED_NAMES.has(stem)) {
|
|
60
|
+
return `"${name}" is a reserved system name on Windows`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (utf8.encode(name).length > MAX_NAME_BYTES) {
|
|
64
|
+
return `Name is too long (max ${MAX_NAME_BYTES} bytes)`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validate a workspace-relative path: a `/`-separated string of components.
|
|
72
|
+
* Each component is checked with {@link validateFilename}. Backslashes are
|
|
73
|
+
* rejected outright — the workspace API uses `/` everywhere and accepting
|
|
74
|
+
* `\` would round-trip differently on Windows vs the Linux container.
|
|
75
|
+
*/
|
|
76
|
+
export function validateRelativePath(relativePath: string): string | null {
|
|
77
|
+
if (typeof relativePath !== 'string' || relativePath.length === 0) {
|
|
78
|
+
return 'Path is required';
|
|
79
|
+
}
|
|
80
|
+
if (relativePath.includes('\\')) {
|
|
81
|
+
return 'Path cannot contain "\\" — use "/" as the separator';
|
|
82
|
+
}
|
|
83
|
+
// Strip a single leading `./` so callers that build paths from a base dir
|
|
84
|
+
// (`./<name>`) don't get a spurious "." segment rejection.
|
|
85
|
+
const normalized = relativePath.replace(/^\.\//, '');
|
|
86
|
+
const segments = normalized.split('/').filter((s) => s.length > 0);
|
|
87
|
+
if (segments.length === 0) return 'Path is required';
|
|
88
|
+
for (const segment of segments) {
|
|
89
|
+
const reason = validateFilename(segment);
|
|
90
|
+
if (reason) return `Invalid path segment "${segment}": ${reason}`;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Throwing wrapper for the backend service layer — keeps call sites a single
|
|
97
|
+
* line and produces a clear `Error` the route handler can surface as a 400.
|
|
98
|
+
*/
|
|
99
|
+
export function assertValidRelativePath(relativePath: string): void {
|
|
100
|
+
const reason = validateRelativePath(relativePath);
|
|
101
|
+
if (reason) throw new Error(`Invalid path: ${reason}`);
|
|
102
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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 function extractFrontmatter(text: string): { frontmatter: string; body: string } | null {
|
|
9
|
+
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
10
|
+
if (!m) return null;
|
|
11
|
+
return { frontmatter: m[1], body: m[2] };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Set a top-level scalar key in a file's `---` frontmatter, LINE-based: replace an
|
|
16
|
+
* existing `<key>:` line's value, else insert `<key>: <value>` at the top of the
|
|
17
|
+
* block (creating a `---` block if the file has none). Deliberately NOT a
|
|
18
|
+
* parse-and-re-serialize — a KB node's `nodeType:` quoted-markdown-link and access
|
|
19
|
+
* lines are read by a hand-rolled regex parser that a full YAML re-emit would
|
|
20
|
+
* corrupt, so every other line is preserved byte-for-byte. `value` is emitted as a
|
|
21
|
+
* bare scalar; callers pass simple id/name strings (no quoting/escaping needed for
|
|
22
|
+
* the `[a-z0-9_-]` id grammar).
|
|
23
|
+
*/
|
|
24
|
+
export function setFrontmatterField(text: string, key: string, value: string): string {
|
|
25
|
+
const fm = extractFrontmatter(text);
|
|
26
|
+
const line = `${key}: ${value}`;
|
|
27
|
+
// Escape the key before interpolating — callers pass plain `id`/`name`, but a
|
|
28
|
+
// metacharacter must never silently change what the line-matcher matches.
|
|
29
|
+
const keyRe = new RegExp(`^(\\s*)${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:.*$`);
|
|
30
|
+
// Preserve the file's own line-ending convention: rebuilding a CRLF file with
|
|
31
|
+
// bare `\n` would dirty every frontmatter line, breaking the byte-for-byte
|
|
32
|
+
// guarantee this helper exists for. Judged from the FIRST line ending (the
|
|
33
|
+
// fence line's own), so a stray CRLF deep in the body can't flip the block.
|
|
34
|
+
const firstNl = text.indexOf('\n');
|
|
35
|
+
const eol = firstNl > 0 && text[firstNl - 1] === '\r' ? '\r\n' : '\n';
|
|
36
|
+
if (!fm) {
|
|
37
|
+
// No frontmatter — prepend a fresh block, keeping the original body intact.
|
|
38
|
+
return `---${eol}${line}${eol}---${eol}${text}`;
|
|
39
|
+
}
|
|
40
|
+
const fmLines = fm.frontmatter.split(/\r?\n/);
|
|
41
|
+
const idx = fmLines.findIndex((l) => keyRe.test(l));
|
|
42
|
+
if (idx >= 0) fmLines[idx] = line;
|
|
43
|
+
else fmLines.unshift(line);
|
|
44
|
+
return `---${eol}${fmLines.join(eol)}${eol}---${eol}${fm.body}`;
|
|
45
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level layout of the KB repo (inside the `KB_DIR_NAME` clone).
|
|
3
|
+
*
|
|
4
|
+
* The repo root holds a small, fixed set of special folders that the app
|
|
5
|
+
* treats distinctly:
|
|
6
|
+
*
|
|
7
|
+
* <kbDirName>/
|
|
8
|
+
* ├── KnowledgeBase/ ← all team ontologies live here (the knowledge graph)
|
|
9
|
+
* ├── Data/ ← agent-produced records; parsed like KnowledgeBase/
|
|
10
|
+
* ├── Agents/ ← .agent files — agent role configurations (not the graph)
|
|
11
|
+
* ├── Pipelines/ ← .pipeline files — execution-layer processes (not the graph)
|
|
12
|
+
* ├── Skills/ ← reusable agent skills (NOT part of the graph)
|
|
13
|
+
* ├── Tools/ ← user-authored tool manuals (*.tool; not the graph)
|
|
14
|
+
* ├── roles.yaml ← identity → role mapping
|
|
15
|
+
* └── access.md ← repo-root access-control rules
|
|
16
|
+
*
|
|
17
|
+
* These names are the single source of truth for both sides of the app:
|
|
18
|
+
* - Backend: the graph parser discovers ontologies under the
|
|
19
|
+
* {@link ONTOLOGY_ROOTS} (`KnowledgeBase/` and `Data/`); `Skills/`,
|
|
20
|
+
* `Tools/`, `Agents/`, `Pipelines/` (and anything else at the root) are
|
|
21
|
+
* ignored by parsing, validation, and the diagram.
|
|
22
|
+
* - Frontend: the file tree renders these root folders as distinct
|
|
23
|
+
* top-level sections.
|
|
24
|
+
*
|
|
25
|
+
* Don't hard-code these strings elsewhere — import them from here.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** Folder under the repo root that contains all team ontologies. */
|
|
29
|
+
export const KNOWLEDGE_BASE_DIR = 'KnowledgeBase';
|
|
30
|
+
|
|
31
|
+
/** Folder under the repo root that holds reusable agent skills (not graph nodes). */
|
|
32
|
+
export const SKILLS_DIR = 'Skills';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Folder under the repo root that holds user-authored tool manuals (`*.tool`
|
|
36
|
+
* files). Each `.tool` is a UTCP manual (inline / http / mcp) that the MCP/UTCP
|
|
37
|
+
* endpoint loads for any user who can read it (access-controlled like Skills).
|
|
38
|
+
* Not part of the knowledge graph.
|
|
39
|
+
*/
|
|
40
|
+
export const TOOLS_DIR = 'Tools';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Folder under the repo root for agent-produced records (pipeline instances,
|
|
44
|
+
* work items, intermediate outputs). Parsed exactly like `KnowledgeBase/`:
|
|
45
|
+
* its direct subfolders are self-contained ontologies.
|
|
46
|
+
*/
|
|
47
|
+
export const DATA_DIR = 'Data';
|
|
48
|
+
|
|
49
|
+
/** Folder under the repo root that holds `.agent` files — agent role configurations (not graph nodes). */
|
|
50
|
+
export const AGENTS_DIR = 'Agents';
|
|
51
|
+
|
|
52
|
+
/** Folder under the repo root that holds `.pipeline` files — execution-layer processes (not graph nodes). */
|
|
53
|
+
export const PIPELINES_DIR = 'Pipelines';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The roots whose subfolders are discovered as ontologies by the graph parser
|
|
57
|
+
* (each subfolder with both `NodeTypes/` and `Knowledge/` is an ontology).
|
|
58
|
+
*/
|
|
59
|
+
export const ONTOLOGY_ROOTS: readonly string[] = [KNOWLEDGE_BASE_DIR, DATA_DIR];
|
|
60
|
+
|
|
61
|
+
/** The `Knowledge/` marker subfolder of an ontology (holds the graph nodes). */
|
|
62
|
+
export const KNOWLEDGE_DIR = 'Knowledge';
|
|
63
|
+
|
|
64
|
+
/** The `NodeTypes/` marker subfolder of an ontology (holds the type definitions). */
|
|
65
|
+
export const NODETYPE_DIR = 'NodeTypes';
|
|
66
|
+
|
|
67
|
+
/** The marker subfolders that make a directory an ontology (it needs BOTH). */
|
|
68
|
+
export const ONTOLOGY_MARKERS = new Set([KNOWLEDGE_DIR, NODETYPE_DIR]);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A named ontology id, or `null` for the neutral bucket — content that belongs
|
|
72
|
+
* to no named ontology (root config, `Skills/`, root-level `Knowledge/`, etc.).
|
|
73
|
+
* The named id is the repo-root-relative path of the ontology directory,
|
|
74
|
+
* e.g. `KnowledgeBase/Product` or `KnowledgeBase/IT Architecture`.
|
|
75
|
+
*/
|
|
76
|
+
export type Ontology = string | null;
|
|
77
|
+
|
|
78
|
+
// The implementation that resolves a path to its `Ontology` (`ontologyOf`) is
|
|
79
|
+
// backend-only — it lives in `packages/backend/src/shared/kb-layout.ts`, built
|
|
80
|
+
// from the `KNOWLEDGE_BASE_DIR` / `ONTOLOGY_MARKERS` constants above. This
|
|
81
|
+
// package holds only the cross-cutting constants and types, not logic.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface WorkspaceInfo {
|
|
2
|
+
/**
|
|
3
|
+
* Workspace id = `encodeURIComponent(branch)`. Stable across users — a
|
|
4
|
+
* branch's workspace is shared by everyone editing it (PLAN §3).
|
|
5
|
+
* Decoding rule: callers can derive the branch via
|
|
6
|
+
* `decodeURIComponent(id)` without a backend round-trip.
|
|
7
|
+
*/
|
|
8
|
+
id: string;
|
|
9
|
+
/** Display name. Currently the branch name verbatim. */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Absolute path on the backend's filesystem (debug / log surface only). */
|
|
12
|
+
absolutePath: string;
|
|
13
|
+
/** ISO timestamp. Per-branch workspaces have no meaningful creation time; epoch is fine. */
|
|
14
|
+
createdAt: string;
|
|
15
|
+
/**
|
|
16
|
+
* Directory name (relative to `absolutePath`) where the KB clone lives.
|
|
17
|
+
* Configured via `KB_DIR_NAME` on the backend; surfaced to the frontend
|
|
18
|
+
* so renderers and chat cards can build correct KB-relative paths
|
|
19
|
+
* without hardcoding the repo name.
|
|
20
|
+
*/
|
|
21
|
+
kbDirName: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface FileTreeEntry {
|
|
25
|
+
name: string;
|
|
26
|
+
relativePath: string;
|
|
27
|
+
type: 'file' | 'directory';
|
|
28
|
+
children?: FileTreeEntry[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
import type { AuthUser } from '../auth/types.js';
|
|
32
|
+
|
|
33
|
+
export interface IWorkspaceService {
|
|
34
|
+
/**
|
|
35
|
+
* Get-or-create the workspace for a branch. Workspaces are per-branch
|
|
36
|
+
* (PLAN §3) — every user editing `branch` shares the same on-disk clone,
|
|
37
|
+
* coordinated by the file-lock service.
|
|
38
|
+
*/
|
|
39
|
+
getOrCreateForBranch(branch: string): Promise<WorkspaceInfo>;
|
|
40
|
+
/**
|
|
41
|
+
* Convenience wrapper that defaults to the configured default branch
|
|
42
|
+
* (`DEFAULT_BRANCH`) when no branch is provided. Kept on the interface so route handlers that
|
|
43
|
+
* receive a logged-in user but no explicit branch can still bootstrap a
|
|
44
|
+
* workspace.
|
|
45
|
+
*/
|
|
46
|
+
getOrCreateForUser(user: AuthUser, branch?: string): Promise<WorkspaceInfo>;
|
|
47
|
+
getMetadata(workspaceId: string): Promise<WorkspaceInfo | null>;
|
|
48
|
+
deleteWorkspace(workspaceId: string): Promise<void>;
|
|
49
|
+
listFiles(workspaceId: string): Promise<FileTreeEntry>;
|
|
50
|
+
readFile(workspaceId: string, relativePath: string): Promise<string>;
|
|
51
|
+
writeFile(workspaceId: string, relativePath: string, content: string): Promise<void>;
|
|
52
|
+
createDirectory(workspaceId: string, relativePath: string): Promise<void>;
|
|
53
|
+
writeFileBinary(workspaceId: string, relativePath: string, data: Uint8Array): Promise<void>;
|
|
54
|
+
deleteFile(workspaceId: string, relativePath: string): Promise<void>;
|
|
55
|
+
moveEntry(workspaceId: string, oldRelativePath: string, newRelativePath: string): Promise<void>;
|
|
56
|
+
getWorkspacePath(workspaceId: string): Promise<string>;
|
|
57
|
+
/** Remove the workspace's transient `tmp/` dir (staged docs, tool-chain spills). */
|
|
58
|
+
clearTmp(workspaceId: string): Promise<void>;
|
|
59
|
+
}
|