@bevel-software/platform-shared 0.20.0 → 0.21.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/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/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/protected.ts +112 -38
- package/src/workspace/kb-layout.ts +66 -43
- package/src/workspace/platform-files.ts +22 -20
package/src/git/protected.ts
CHANGED
|
@@ -3,20 +3,29 @@
|
|
|
3
3
|
* their user-visible display names. Single source of truth for BOTH sides of
|
|
4
4
|
* the app, so backend enforcement and frontend affordances can never drift.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* time and throw when it found nothing — which made the branch model something
|
|
8
|
-
* a deployment had to know before any code could load. On the frontend that
|
|
9
|
-
* meant baking it into the bundle at build time (Vite `define`), so the same
|
|
10
|
-
* artifact could not serve two deployments and changing a branch name meant a
|
|
11
|
-
* rebuild. Both sides now CALL {@link configureBranchModel} during boot:
|
|
12
|
-
* the backend from its environment, the browser from `GET /api/config`.
|
|
6
|
+
* TWO SHAPES, ONE RULE SET.
|
|
13
7
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
8
|
+
* A VALUE ({@link BranchModelValue}), resolved by {@link resolveBranchModel}
|
|
9
|
+
* from the shape `/api/config` serves and the setup screen collects. The pure
|
|
10
|
+
* helpers below (`isProtectedBranch`, `protectedBranchDisplayName`) take the
|
|
11
|
+
* value they judge against, so a caller holding two deployments' models — a
|
|
12
|
+
* server hosting several knowledge bases in one process — asks about each by
|
|
13
|
+
* name. The backend uses ONLY this shape: every service is handed its
|
|
14
|
+
* knowledge base's model at construction and never reads a process-wide one.
|
|
15
|
+
*
|
|
16
|
+
* The BROWSER'S live bindings (`DEFAULT_BRANCH`, `PROTECTED_BRANCHES`, …),
|
|
17
|
+
* applied once by {@link configureBranchModel} from `GET /api/config` before
|
|
18
|
+
* React renders. A page shows one deployment, so one process-wide value is
|
|
19
|
+
* the right shape there, and the bindings let a component read the model
|
|
20
|
+
* without threading it through props. They are for the browser: the backend
|
|
21
|
+
* package forbids importing them (an ESLint rule names each one), and a
|
|
22
|
+
* server that configured them would be setting a value for every knowledge
|
|
23
|
+
* base it serves at once.
|
|
24
|
+
*
|
|
25
|
+
* Reading a live binding works inside a function body, which sees whatever
|
|
26
|
+
* configuration has been applied by the time it runs. What does NOT work is
|
|
27
|
+
* capturing one at module scope (`const X = DEFAULT_BRANCH` in a file's top
|
|
28
|
+
* level), which snapshots the value at import — before configuration.
|
|
20
29
|
*/
|
|
21
30
|
|
|
22
31
|
declare const process: { env: Record<string, string | undefined> };
|
|
@@ -39,26 +48,66 @@ function deriveDisplayName(slug: string): string {
|
|
|
39
48
|
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
40
49
|
}
|
|
41
50
|
|
|
51
|
+
/** The shape both sides configure from, and the shape `/api/config` serves. */
|
|
52
|
+
export interface BranchModel {
|
|
53
|
+
defaultBranch: string;
|
|
54
|
+
/** Slugs; a comma/space-separated string is accepted for env convenience. */
|
|
55
|
+
protectedBranches: string[] | string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A branch model as a VALUE: the resolved, validated form of {@link BranchModel}
|
|
60
|
+
* that everything judging a branch is handed.
|
|
61
|
+
*
|
|
62
|
+
* `defaultBranch` is the branch a logged-in user lands on when they don't
|
|
63
|
+
* explicitly pick one, and the default destination for shared drafts. It is
|
|
64
|
+
* the empty string on the one model that is not configured yet
|
|
65
|
+
* ({@link UNCONFIGURED_BRANCH_MODEL}): a fresh deployment has no branch model
|
|
66
|
+
* until its setup screen is answered, and everything behind that gate reads
|
|
67
|
+
* the model only once it is.
|
|
68
|
+
*/
|
|
69
|
+
export interface BranchModelValue {
|
|
70
|
+
readonly defaultBranch: string;
|
|
71
|
+
readonly protectedBranches: ReadonlySet<string>;
|
|
72
|
+
/** Slug → human-readable name, for every protected branch. */
|
|
73
|
+
readonly displayNames: Readonly<Record<string, string>>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The model a deployment has before setup has answered for one: no default
|
|
78
|
+
* branch, nothing protected. {@link isBranchModelConfigured} is false for
|
|
79
|
+
* exactly this value.
|
|
80
|
+
*/
|
|
81
|
+
export const UNCONFIGURED_BRANCH_MODEL: BranchModelValue = Object.freeze({
|
|
82
|
+
defaultBranch: '',
|
|
83
|
+
protectedBranches: new Set<string>(),
|
|
84
|
+
displayNames: Object.freeze({}),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Whether a model has been configured — the frontend gates render on this, and
|
|
89
|
+
* the backend's setup gate keeps the app shut until it holds.
|
|
90
|
+
*/
|
|
91
|
+
export function isBranchModelConfigured(model: BranchModelValue): boolean {
|
|
92
|
+
return model.defaultBranch !== '';
|
|
93
|
+
}
|
|
94
|
+
|
|
42
95
|
/**
|
|
43
96
|
* The branch a logged-in user lands on when they don't explicitly pick one, and
|
|
44
|
-
* the default destination for shared drafts.
|
|
97
|
+
* the default destination for shared drafts. BROWSER-SIDE live binding — see
|
|
98
|
+
* this file's header; the backend reads its `BranchModelValue` instead.
|
|
45
99
|
*
|
|
46
100
|
* Empty until {@link configureBranchModel} runs. Read it inside a function, not
|
|
47
|
-
* at module scope
|
|
101
|
+
* at module scope.
|
|
48
102
|
*/
|
|
49
103
|
export let DEFAULT_BRANCH: string = '';
|
|
50
104
|
|
|
105
|
+
/** Browser-side live binding — see this file's header. */
|
|
51
106
|
export let PROTECTED_BRANCH_DISPLAY_NAMES: Readonly<Record<string, string>> = Object.freeze({});
|
|
52
107
|
|
|
108
|
+
/** Browser-side live binding — see this file's header. */
|
|
53
109
|
export let PROTECTED_BRANCHES: ReadonlySet<string> = new Set<string>();
|
|
54
110
|
|
|
55
|
-
/** The shape both sides configure from, and the shape `/api/config` serves. */
|
|
56
|
-
export interface BranchModel {
|
|
57
|
-
defaultBranch: string;
|
|
58
|
-
/** Slugs; a comma/space-separated string is accepted for env convenience. */
|
|
59
|
-
protectedBranches: string[] | string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
111
|
/**
|
|
63
112
|
* Apply the branch model. Called once during boot on each side, and validated
|
|
64
113
|
* here rather than at either call site so the two cannot disagree about what
|
|
@@ -96,22 +145,46 @@ export function validateBranchModel(model: BranchModel): string | null {
|
|
|
96
145
|
return null;
|
|
97
146
|
}
|
|
98
147
|
|
|
99
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Resolve a model into the value everything judges against. Throws on an
|
|
150
|
+
* invalid pair — the same rule {@link validateBranchModel} states — so a
|
|
151
|
+
* value, once held, is known to be a usable one.
|
|
152
|
+
*/
|
|
153
|
+
export function resolveBranchModel(model: BranchModel): BranchModelValue {
|
|
100
154
|
const problem = validateBranchModel(model);
|
|
101
155
|
if (problem) throw new Error(problem);
|
|
102
|
-
const defaultBranch = model.defaultBranch.trim();
|
|
103
156
|
const list = branchListOf(model);
|
|
157
|
+
return Object.freeze({
|
|
158
|
+
defaultBranch: model.defaultBranch.trim(),
|
|
159
|
+
protectedBranches: new Set(list),
|
|
160
|
+
displayNames: Object.freeze(Object.fromEntries(list.map((slug) => [slug, deriveDisplayName(slug)]))),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
104
163
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Apply the model to the BROWSER'S live bindings. Called once during boot,
|
|
166
|
+
* from `GET /api/config`, before anything reads them. Validated by
|
|
167
|
+
* {@link resolveBranchModel}, so the bindings and a value resolved from the
|
|
168
|
+
* same model can never disagree.
|
|
169
|
+
*/
|
|
170
|
+
export function configureBranchModel(model: BranchModel): void {
|
|
171
|
+
const value = resolveBranchModel(model);
|
|
172
|
+
DEFAULT_BRANCH = value.defaultBranch;
|
|
173
|
+
PROTECTED_BRANCH_DISPLAY_NAMES = value.displayNames;
|
|
174
|
+
PROTECTED_BRANCHES = value.protectedBranches;
|
|
110
175
|
}
|
|
111
176
|
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
|
|
177
|
+
/**
|
|
178
|
+
* The browser's model as a value — for the code paths that take a
|
|
179
|
+
* {@link BranchModelValue} and run in the browser, where the one model in
|
|
180
|
+
* effect is the configured one.
|
|
181
|
+
*/
|
|
182
|
+
export function currentBranchModel(): BranchModelValue {
|
|
183
|
+
return {
|
|
184
|
+
defaultBranch: DEFAULT_BRANCH,
|
|
185
|
+
protectedBranches: PROTECTED_BRANCHES,
|
|
186
|
+
displayNames: PROTECTED_BRANCH_DISPLAY_NAMES,
|
|
187
|
+
};
|
|
115
188
|
}
|
|
116
189
|
|
|
117
190
|
/**
|
|
@@ -125,11 +198,9 @@ export function branchModelFromEnv(): BranchModel {
|
|
|
125
198
|
};
|
|
126
199
|
}
|
|
127
200
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
name,
|
|
132
|
-
);
|
|
201
|
+
/** Whether `name` is one of `model`'s protected branches. */
|
|
202
|
+
export function isProtectedBranch(model: BranchModelValue, name: string | null | undefined): boolean {
|
|
203
|
+
return !!name && model.protectedBranches.has(name);
|
|
133
204
|
}
|
|
134
205
|
|
|
135
206
|
/**
|
|
@@ -145,8 +216,11 @@ export function isProtectedBranch(name: string | null | undefined): boolean {
|
|
|
145
216
|
* whether to fall back to the raw string or hide the affordance.
|
|
146
217
|
*/
|
|
147
218
|
export function protectedBranchDisplayName(
|
|
219
|
+
model: BranchModelValue,
|
|
148
220
|
name: string | null | undefined,
|
|
149
221
|
): string | null {
|
|
150
222
|
if (!name) return null;
|
|
151
|
-
return
|
|
223
|
+
return Object.prototype.hasOwnProperty.call(model.displayNames, name)
|
|
224
|
+
? model.displayNames[name]!
|
|
225
|
+
: null;
|
|
152
226
|
}
|
|
@@ -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
|
}
|