@bevel-software/platform-shared 0.20.0 → 0.22.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/dist/git/pr.types.d.ts +53 -2
- package/dist/git/pr.types.d.ts.map +1 -1
- package/dist/git/protected.d.ts +82 -25
- package/dist/git/protected.d.ts.map +1 -1
- package/dist/git/protected.js +85 -27
- package/dist/git/protected.js.map +1 -1
- package/dist/git/types.d.ts +21 -1
- package/dist/git/types.d.ts.map +1 -1
- package/dist/workflow/interface.d.ts +23 -2
- package/dist/workflow/interface.d.ts.map +1 -1
- package/dist/workflow/types.d.ts +18 -0
- package/dist/workflow/types.d.ts.map +1 -1
- package/dist/workspace/kb-layout.d.ts +47 -33
- package/dist/workspace/kb-layout.d.ts.map +1 -1
- package/dist/workspace/kb-layout.js +65 -43
- package/dist/workspace/kb-layout.js.map +1 -1
- package/dist/workspace/platform-files.d.ts +13 -12
- package/dist/workspace/platform-files.d.ts.map +1 -1
- package/dist/workspace/platform-files.js +23 -22
- package/dist/workspace/platform-files.js.map +1 -1
- package/package.json +1 -1
- package/src/git/pr.types.ts +53 -2
- package/src/git/protected.ts +112 -38
- package/src/git/types.ts +29 -1
- package/src/workflow/interface.ts +27 -1
- package/src/workflow/types.ts +16 -0
- package/src/workspace/kb-layout.ts +66 -43
- package/src/workspace/platform-files.ts +22 -20
|
@@ -36,12 +36,19 @@ import { validateFilename } from './filename.js';
|
|
|
36
36
|
* Don't hard-code these strings elsewhere — import them from here.
|
|
37
37
|
*
|
|
38
38
|
* CONFIGURABLE, WITH DEFAULTS. The three roots a deployment may rename
|
|
39
|
-
* (`KnowledgeBase/`, `Skills/`, `Plugins/`)
|
|
40
|
-
* {@link
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
39
|
+
* (`KnowledgeBase/`, `Skills/`, `Plugins/`) and the guide's file name travel
|
|
40
|
+
* as a {@link KbLayout} VALUE: every helper below that depends on a name takes
|
|
41
|
+
* the layout it judges against, and the backend hands each knowledge base's
|
|
42
|
+
* layout to its services at construction — a server hosting several knowledge
|
|
43
|
+
* bases in one process holds one per knowledge base, never a process-wide one.
|
|
44
|
+
*
|
|
45
|
+
* The `let` bindings (`KNOWLEDGE_BASE_DIR`, …) are the BROWSER'S copy, applied
|
|
46
|
+
* once by {@link configureKbLayout} from `GET /api/config`, the same shape as
|
|
47
|
+
* the branch model in `git/protected.ts`: a page shows one deployment, so one
|
|
48
|
+
* process-wide value is right there. The backend package forbids importing
|
|
49
|
+
* them (an ESLint rule names each one). They carry defaults, so nothing waits
|
|
50
|
+
* for configuration; but read them inside a function body, never capture one
|
|
51
|
+
* at module scope.
|
|
45
52
|
*/
|
|
46
53
|
|
|
47
54
|
/** Folder under the repo root that contains all team ontologies. */
|
|
@@ -218,7 +225,7 @@ export function gitignoreLiteral(name: string): string {
|
|
|
218
225
|
* whether this sentence is present by searching for the RAW name — see
|
|
219
226
|
* {@link mentionsAgentsFile}, which is how the startup step asks.
|
|
220
227
|
*/
|
|
221
|
-
export function agentsFilePointerSentence(agentsFile: string
|
|
228
|
+
export function agentsFilePointerSentence(agentsFile: string): string {
|
|
222
229
|
return `Read [${markdownLinkLabel(agentsFile)}](${agentsFileLinkPath(agentsFile)})${POINTER_SENTENCE_TAIL}`;
|
|
223
230
|
}
|
|
224
231
|
|
|
@@ -266,7 +273,7 @@ function escapeRegExp(text: string): string {
|
|
|
266
273
|
* answers, and only the caller knows that the second one means "consider
|
|
267
274
|
* appending".
|
|
268
275
|
*/
|
|
269
|
-
export function retargetAgentsFilePointer(text: string, agentsFile: string
|
|
276
|
+
export function retargetAgentsFilePointer(text: string, agentsFile: string): string | null {
|
|
270
277
|
let found = false;
|
|
271
278
|
const wanted = agentsFilePointerSentence(agentsFile);
|
|
272
279
|
const updated = text.replace(POINTER_SENTENCE_PATTERN, () => {
|
|
@@ -315,7 +322,7 @@ function agentsFileLinkPath(agentsFile: string): string {
|
|
|
315
322
|
* that one would append a second copy on the next boot, and a third on the
|
|
316
323
|
* one after — the exact failure this feature exists to prevent.
|
|
317
324
|
*/
|
|
318
|
-
export function mentionsAgentsFile(text: string, agentsFile: string
|
|
325
|
+
export function mentionsAgentsFile(text: string, agentsFile: string): boolean {
|
|
319
326
|
return (
|
|
320
327
|
text.includes(agentsFile) ||
|
|
321
328
|
text.includes(markdownLinkLabel(agentsFile)) ||
|
|
@@ -369,7 +376,7 @@ export function validateKbRootName(name: string): string | null {
|
|
|
369
376
|
*/
|
|
370
377
|
export function validateAgentsFileName(
|
|
371
378
|
name: string,
|
|
372
|
-
roots: Pick<KbLayout, 'knowledgeBaseDir' | 'skillsDir' | 'pluginsDir'
|
|
379
|
+
roots: Pick<KbLayout, 'knowledgeBaseDir' | 'skillsDir' | 'pluginsDir'>,
|
|
373
380
|
): string | null {
|
|
374
381
|
const v = name.trim();
|
|
375
382
|
if (!v) return 'A file name is required.';
|
|
@@ -466,21 +473,37 @@ export function onKbLayoutApplied(listener: () => void): void {
|
|
|
466
473
|
}
|
|
467
474
|
|
|
468
475
|
/**
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
476
|
+
* A layout as the helpers hold it: validated, every name trimmed, the guide's
|
|
477
|
+
* name filled in from the default when absent. Throws on an invalid one so a
|
|
478
|
+
* bad deployment setting fails beside the rest of the wiring rather than
|
|
479
|
+
* scattering a half-renamed tree.
|
|
472
480
|
*/
|
|
473
|
-
export function
|
|
481
|
+
export function resolveKbLayout(layout: KbLayout): Required<KbLayout> {
|
|
474
482
|
const problem = validateKbLayout(layout);
|
|
475
483
|
if (problem) throw new Error(problem);
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
484
|
+
return Object.freeze({
|
|
485
|
+
knowledgeBaseDir: layout.knowledgeBaseDir.trim(),
|
|
486
|
+
skillsDir: layout.skillsDir.trim(),
|
|
487
|
+
pluginsDir: layout.pluginsDir.trim(),
|
|
488
|
+
agentsFile: agentsFileOf(layout),
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Apply the layout to the BROWSER'S live bindings. Called once during boot
|
|
494
|
+
* from `GET /api/config`; validated by {@link resolveKbLayout}. Applying the
|
|
495
|
+
* defaults is a no-op.
|
|
496
|
+
*/
|
|
497
|
+
export function configureKbLayout(layout: KbLayout): void {
|
|
498
|
+
const resolved = resolveKbLayout(layout);
|
|
499
|
+
KNOWLEDGE_BASE_DIR = resolved.knowledgeBaseDir;
|
|
500
|
+
SKILLS_DIR = resolved.skillsDir;
|
|
501
|
+
PLUGINS_DIR = resolved.pluginsDir;
|
|
502
|
+
AGENTS_FILE = resolved.agentsFile;
|
|
480
503
|
for (const listener of layoutListeners) listener();
|
|
481
504
|
}
|
|
482
505
|
|
|
483
|
-
/** The layout currently
|
|
506
|
+
/** The browser's layout as a value — what the live bindings currently hold. */
|
|
484
507
|
export function currentKbLayout(): Required<KbLayout> {
|
|
485
508
|
return {
|
|
486
509
|
knowledgeBaseDir: KNOWLEDGE_BASE_DIR,
|
|
@@ -491,14 +514,14 @@ export function currentKbLayout(): Required<KbLayout> {
|
|
|
491
514
|
}
|
|
492
515
|
|
|
493
516
|
/**
|
|
494
|
-
* Whether a layout
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
517
|
+
* Whether a layout is the default one. The setup-completing save applies the
|
|
518
|
+
* stored names while this holds of the one in effect, the same "only from
|
|
519
|
+
* none to some" rule the branch model follows — and also on a retry after its
|
|
520
|
+
* own failed initialization run, when the process holds names setup applied
|
|
521
|
+
* but the app never opened (see `setup.routes.ts`). A layout the process
|
|
522
|
+
* booted with is never replaced there.
|
|
500
523
|
*/
|
|
501
|
-
export function isDefaultKbLayout(layout: KbLayout
|
|
524
|
+
export function isDefaultKbLayout(layout: KbLayout): boolean {
|
|
502
525
|
return (
|
|
503
526
|
layout.knowledgeBaseDir === DEFAULT_KB_LAYOUT.knowledgeBaseDir &&
|
|
504
527
|
layout.skillsDir === DEFAULT_KB_LAYOUT.skillsDir &&
|
|
@@ -516,7 +539,7 @@ export function isDefaultKbLayout(layout: KbLayout = currentKbLayout()): boolean
|
|
|
516
539
|
* renamed the guide gets a guide naming the file it lives in. Text without
|
|
517
540
|
* placeholders passes through unchanged.
|
|
518
541
|
*/
|
|
519
|
-
export function renderKbLayoutPlaceholders(text: string, layout: KbLayout
|
|
542
|
+
export function renderKbLayoutPlaceholders(text: string, layout: KbLayout): string {
|
|
520
543
|
// Replacer FUNCTIONS: a string replacement would interpret `$&`, `$$` and
|
|
521
544
|
// friends inside a folder name, and `$` is a legal character in one.
|
|
522
545
|
return text
|
|
@@ -790,9 +813,9 @@ export function isPersonalPluginFolder(folderName: string): boolean {
|
|
|
790
813
|
* to start with the prefix is a plugin — discovery, the principal picker and
|
|
791
814
|
* the item pages all ask this one question of the FOLDER.
|
|
792
815
|
*/
|
|
793
|
-
export function isPersonalPluginDir(repoRelDir: string): boolean {
|
|
816
|
+
export function isPersonalPluginDir(repoRelDir: string, layout: KbLayout): boolean {
|
|
794
817
|
const segments = repoRelDir.split('/').filter(Boolean);
|
|
795
|
-
return segments.length === 2 && segments[0] ===
|
|
818
|
+
return segments.length === 2 && segments[0] === layout.pluginsDir && isPersonalPluginFolder(segments[1]!);
|
|
796
819
|
}
|
|
797
820
|
|
|
798
821
|
/**
|
|
@@ -808,9 +831,9 @@ export function isPersonalPluginDir(repoRelDir: string): boolean {
|
|
|
808
831
|
* have. Callers bucket by it; nothing requires it. A plugin-less skill is a
|
|
809
832
|
* real, supported state — the prototype calls those "yours alone".
|
|
810
833
|
*/
|
|
811
|
-
export function pluginOfPath(repoRelativePath: string): string | null {
|
|
834
|
+
export function pluginOfPath(repoRelativePath: string, layout: KbLayout): string | null {
|
|
812
835
|
const segments = repoRelativePath.split('/').filter(Boolean);
|
|
813
|
-
if (segments[0] !==
|
|
836
|
+
if (segments[0] !== layout.pluginsDir) return null;
|
|
814
837
|
// Needs a segment for the plugin AND at least one below it, otherwise
|
|
815
838
|
// `Plugins/GTM` (the folder itself) would report itself as being in a plugin,
|
|
816
839
|
// and a loose `Plugins/slack.tool` would report a plugin named "slack.tool".
|
|
@@ -833,19 +856,19 @@ export const PIPELINES_DIR = 'Pipelines';
|
|
|
833
856
|
/**
|
|
834
857
|
* The roots whose subfolders are discovered as ontologies by the graph parser
|
|
835
858
|
* (each subfolder with both `NodeTypes/` and `Knowledge/` is an ontology).
|
|
836
|
-
* A function, not a constant:
|
|
837
|
-
*
|
|
859
|
+
* A function of the layout, not a constant: the knowledge-base root is
|
|
860
|
+
* configurable per deployment.
|
|
838
861
|
*/
|
|
839
|
-
export function ontologyRoots(): readonly string[] {
|
|
840
|
-
return [
|
|
862
|
+
export function ontologyRoots(layout: KbLayout): readonly string[] {
|
|
863
|
+
return [layout.knowledgeBaseDir, DATA_DIR];
|
|
841
864
|
}
|
|
842
865
|
|
|
843
866
|
/**
|
|
844
|
-
* Every reserved root name
|
|
845
|
-
*
|
|
867
|
+
* Every reserved root name under `layout` — the set the file tree renders as
|
|
868
|
+
* its own sections rather than folding into Knowledge.
|
|
846
869
|
*/
|
|
847
|
-
export function reservedRootDirNames(): ReadonlySet<string> {
|
|
848
|
-
return new Set([
|
|
870
|
+
export function reservedRootDirNames(layout: KbLayout): ReadonlySet<string> {
|
|
871
|
+
return new Set([layout.knowledgeBaseDir, layout.skillsDir, layout.pluginsDir, DATA_DIR, AGENTS_DIR, PIPELINES_DIR]);
|
|
849
872
|
}
|
|
850
873
|
|
|
851
874
|
/**
|
|
@@ -854,11 +877,11 @@ export function reservedRootDirNames(): ReadonlySet<string> {
|
|
|
854
877
|
* needs read access to where it lands (the "read before write" rule); a new
|
|
855
878
|
* folder directly under one of these three is the one place that rule does
|
|
856
879
|
* not apply, because the new folder carries its creator's own grant. A
|
|
857
|
-
* function, like {@link reservedRootDirNames}, because the
|
|
858
|
-
* configurable.
|
|
880
|
+
* function of the layout, like {@link reservedRootDirNames}, because the
|
|
881
|
+
* names are configurable.
|
|
859
882
|
*/
|
|
860
|
-
export function creatableRootDirNames(): ReadonlySet<string> {
|
|
861
|
-
return new Set([
|
|
883
|
+
export function creatableRootDirNames(layout: KbLayout): ReadonlySet<string> {
|
|
884
|
+
return new Set([layout.knowledgeBaseDir, layout.skillsDir, layout.pluginsDir]);
|
|
862
885
|
}
|
|
863
886
|
|
|
864
887
|
/** The `Knowledge/` marker subfolder of an ontology (holds the graph nodes). */
|
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
DEFAULT_KB_LAYOUT,
|
|
3
3
|
FIXED_PLATFORM_FILE_NAMES,
|
|
4
4
|
agentsFileOf,
|
|
5
|
-
currentKbLayout,
|
|
6
5
|
reservedRootDirNames,
|
|
7
6
|
type KbLayout,
|
|
8
7
|
} from './kb-layout.js';
|
|
@@ -14,14 +13,15 @@ import {
|
|
|
14
13
|
* the repository root only, so a nested file of either name is ordinary
|
|
15
14
|
* content. Moving one changes what the platform enforces, so moves refuse them.
|
|
16
15
|
*
|
|
17
|
-
* A FUNCTION, not a constant, and that is the whole of the
|
|
18
|
-
* change on this side: the guide's name is a deployment
|
|
19
|
-
* platform file is `HEXIS.md` on one deployment and
|
|
20
|
-
* and on the first, a root `AGENTS.md` is the
|
|
21
|
-
* which has to move and delete like any page.
|
|
22
|
-
*
|
|
16
|
+
* A FUNCTION OF THE LAYOUT, not a constant, and that is the whole of the
|
|
17
|
+
* configurable-guide change on this side: the guide's name is a deployment
|
|
18
|
+
* setting, so the fourth platform file is `HEXIS.md` on one deployment and
|
|
19
|
+
* `AGENTS.md` on the next — and on the first, a root `AGENTS.md` is the
|
|
20
|
+
* CUSTOMER'S own conventions file, which has to move and delete like any page.
|
|
21
|
+
* Every gate asks this with the layout it serves rather than reading a list
|
|
22
|
+
* captured at module load.
|
|
23
23
|
*/
|
|
24
|
-
export function platformFileNames(layout: KbLayout
|
|
24
|
+
export function platformFileNames(layout: KbLayout): readonly string[] {
|
|
25
25
|
return [...FIXED_PLATFORM_FILE_NAMES, agentsFileOf(layout)];
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -38,8 +38,8 @@ export const PLATFORM_FILE_NAMES: readonly string[] = Object.freeze(
|
|
|
38
38
|
/** The platform files that are read wherever they sit, not only at the root. */
|
|
39
39
|
const PLATFORM_FILES_AT_ANY_DEPTH = new Set(['access.md', '.bevelignore']);
|
|
40
40
|
|
|
41
|
-
/** The names
|
|
42
|
-
const platformFiles = (): ReadonlySet<string> => new Set(platformFileNames());
|
|
41
|
+
/** The names under `layout`, as a set — rebuilt per call, because the guide's is configurable. */
|
|
42
|
+
const platformFiles = (layout: KbLayout): ReadonlySet<string> => new Set(platformFileNames(layout));
|
|
43
43
|
|
|
44
44
|
const normalize = (path: string): string => path.replace(/^\.?\/+/, '').replace(/\/+$/, '');
|
|
45
45
|
|
|
@@ -65,10 +65,10 @@ const hasTraversal = (path: string): boolean =>
|
|
|
65
65
|
* Exact spelling, as the platform reads it: `Access.md` is content. A caller
|
|
66
66
|
* on a case-insensitive disk passes the path's on-disk spelling.
|
|
67
67
|
*/
|
|
68
|
-
export function isPlatformFile(repoRelativePath: string): boolean {
|
|
68
|
+
export function isPlatformFile(repoRelativePath: string, layout: KbLayout): boolean {
|
|
69
69
|
const norm = normalize(repoRelativePath);
|
|
70
70
|
const name = baseName(norm);
|
|
71
|
-
if (!platformFiles().has(name)) return false;
|
|
71
|
+
if (!platformFiles(layout).has(name)) return false;
|
|
72
72
|
return PLATFORM_FILES_AT_ANY_DEPTH.has(name) || norm === name;
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -96,10 +96,10 @@ export function platformFileCreationRefusal(pathOrName: string): string {
|
|
|
96
96
|
* one takes a whole section of the knowledge base with it. Exact spelling; a
|
|
97
97
|
* caller on a case-insensitive disk passes the path's on-disk spelling.
|
|
98
98
|
*/
|
|
99
|
-
export function isPlatformFolder(repoRelativeDir: string): boolean {
|
|
99
|
+
export function isPlatformFolder(repoRelativeDir: string, layout: KbLayout): boolean {
|
|
100
100
|
const norm = normalize(repoRelativeDir);
|
|
101
101
|
if (norm === '') return true;
|
|
102
|
-
return !norm.includes('/') && reservedRootDirNames().has(norm);
|
|
102
|
+
return !norm.includes('/') && reservedRootDirNames(layout).has(norm);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/** The sentence a delete or move of a platform folder is refused with. */
|
|
@@ -118,8 +118,8 @@ export function platformFolderRefusal(repoRelativeDir: string): string {
|
|
|
118
118
|
* of the root's rather than standing in for it, which is why moving the
|
|
119
119
|
* ROOT's copy into a folder is a move out and not a restore.
|
|
120
120
|
*/
|
|
121
|
-
export function isRootPlatformFile(repoRelativePath: string): boolean {
|
|
122
|
-
return platformFiles().has(normalize(repoRelativePath));
|
|
121
|
+
export function isRootPlatformFile(repoRelativePath: string, layout: KbLayout): boolean {
|
|
122
|
+
return platformFiles(layout).has(normalize(repoRelativePath));
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/**
|
|
@@ -143,11 +143,12 @@ export type PlatformRestoreDestination =
|
|
|
143
143
|
|
|
144
144
|
export function platformRestoreDestination(
|
|
145
145
|
repoRelativeDestination: string,
|
|
146
|
+
layout: KbLayout,
|
|
146
147
|
): PlatformRestoreDestination | null {
|
|
147
148
|
const norm = normalize(repoRelativeDestination);
|
|
148
149
|
if (hasTraversal(norm)) return null;
|
|
149
150
|
const name = baseName(norm);
|
|
150
|
-
if (!platformFiles().has(name)) return null;
|
|
151
|
+
if (!platformFiles(layout).has(name)) return null;
|
|
151
152
|
if (name === 'access.md') {
|
|
152
153
|
const slash = norm.lastIndexOf('/');
|
|
153
154
|
return { name, kind: 'folder-without-access-md', dir: slash === -1 ? '' : norm.slice(0, slash) };
|
|
@@ -178,11 +179,12 @@ export function platformRestoreDestination(
|
|
|
178
179
|
export function isPlatformRestoreShape(
|
|
179
180
|
repoRelativeSource: string,
|
|
180
181
|
repoRelativeDestination: string,
|
|
182
|
+
layout: KbLayout,
|
|
181
183
|
): boolean {
|
|
182
184
|
if (hasTraversal(repoRelativeSource)) return false;
|
|
183
185
|
const name = baseName(repoRelativeSource);
|
|
184
|
-
if (!platformFiles().has(name)) return false;
|
|
185
|
-
if (isRootPlatformFile(repoRelativeSource)) return false;
|
|
186
|
-
const target = platformRestoreDestination(repoRelativeDestination);
|
|
186
|
+
if (!platformFiles(layout).has(name)) return false;
|
|
187
|
+
if (isRootPlatformFile(repoRelativeSource, layout)) return false;
|
|
188
|
+
const target = platformRestoreDestination(repoRelativeDestination, layout);
|
|
187
189
|
return target !== null && target.name === name;
|
|
188
190
|
}
|