@bevel-software/platform-shared 0.15.1 → 0.19.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 (73) hide show
  1. package/dist/auth/types.d.ts +12 -0
  2. package/dist/auth/types.d.ts.map +1 -1
  3. package/dist/git/pr.types.d.ts +75 -11
  4. package/dist/git/pr.types.d.ts.map +1 -1
  5. package/dist/git/types.d.ts +28 -1
  6. package/dist/git/types.d.ts.map +1 -1
  7. package/dist/index.d.ts +6 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +6 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/workflow/events.d.ts +32 -4
  12. package/dist/workflow/events.d.ts.map +1 -1
  13. package/dist/workflow/events.js.map +1 -1
  14. package/dist/workflow/interface.d.ts +103 -4
  15. package/dist/workflow/interface.d.ts.map +1 -1
  16. package/dist/workflow/types.d.ts +49 -0
  17. package/dist/workflow/types.d.ts.map +1 -1
  18. package/dist/workspace/access-verbs.d.ts +83 -0
  19. package/dist/workspace/access-verbs.d.ts.map +1 -0
  20. package/dist/workspace/access-verbs.js +110 -0
  21. package/dist/workspace/access-verbs.js.map +1 -0
  22. package/dist/workspace/agent-preamble.d.ts +33 -0
  23. package/dist/workspace/agent-preamble.d.ts.map +1 -0
  24. package/dist/workspace/agent-preamble.js +45 -0
  25. package/dist/workspace/agent-preamble.js.map +1 -0
  26. package/dist/workspace/entry-exists.d.ts +27 -0
  27. package/dist/workspace/entry-exists.d.ts.map +1 -0
  28. package/dist/workspace/entry-exists.js +33 -0
  29. package/dist/workspace/entry-exists.js.map +1 -0
  30. package/dist/workspace/filename.d.ts +15 -0
  31. package/dist/workspace/filename.d.ts.map +1 -1
  32. package/dist/workspace/filename.js +30 -0
  33. package/dist/workspace/filename.js.map +1 -1
  34. package/dist/workspace/frontmatter-carriers.d.ts +44 -0
  35. package/dist/workspace/frontmatter-carriers.d.ts.map +1 -0
  36. package/dist/workspace/frontmatter-carriers.js +52 -0
  37. package/dist/workspace/frontmatter-carriers.js.map +1 -0
  38. package/dist/workspace/frontmatter.d.ts +23 -4
  39. package/dist/workspace/frontmatter.d.ts.map +1 -1
  40. package/dist/workspace/frontmatter.js +59 -9
  41. package/dist/workspace/frontmatter.js.map +1 -1
  42. package/dist/workspace/kb-layout.d.ts +226 -19
  43. package/dist/workspace/kb-layout.d.ts.map +1 -1
  44. package/dist/workspace/kb-layout.js +353 -23
  45. package/dist/workspace/kb-layout.js.map +1 -1
  46. package/dist/workspace/placeholder.d.ts +21 -0
  47. package/dist/workspace/placeholder.d.ts.map +1 -0
  48. package/dist/workspace/placeholder.js +28 -0
  49. package/dist/workspace/placeholder.js.map +1 -0
  50. package/dist/workspace/platform-files.d.ts +105 -0
  51. package/dist/workspace/platform-files.d.ts.map +1 -0
  52. package/dist/workspace/platform-files.js +147 -0
  53. package/dist/workspace/platform-files.js.map +1 -0
  54. package/dist/workspace/types.d.ts +8 -0
  55. package/dist/workspace/types.d.ts.map +1 -1
  56. package/package.json +1 -1
  57. package/src/auth/types.ts +12 -0
  58. package/src/git/pr.types.ts +77 -11
  59. package/src/git/types.ts +35 -1
  60. package/src/index.ts +6 -0
  61. package/src/workflow/events.ts +33 -3
  62. package/src/workflow/interface.ts +127 -4
  63. package/src/workflow/types.ts +43 -0
  64. package/src/workspace/access-verbs.ts +124 -0
  65. package/src/workspace/agent-preamble.ts +47 -0
  66. package/src/workspace/entry-exists.ts +36 -0
  67. package/src/workspace/filename.ts +31 -0
  68. package/src/workspace/frontmatter-carriers.ts +57 -0
  69. package/src/workspace/frontmatter.ts +57 -8
  70. package/src/workspace/kb-layout.ts +388 -25
  71. package/src/workspace/placeholder.ts +29 -0
  72. package/src/workspace/platform-files.ts +188 -0
  73. package/src/workspace/types.ts +8 -0
@@ -0,0 +1,188 @@
1
+ import {
2
+ DEFAULT_KB_LAYOUT,
3
+ FIXED_PLATFORM_FILE_NAMES,
4
+ agentsFileOf,
5
+ currentKbLayout,
6
+ reservedRootDirNames,
7
+ type KbLayout,
8
+ } from './kb-layout.js';
9
+
10
+ /**
11
+ * The files the platform reads as configuration, not content. `access.md`
12
+ * governs the folder it sits in and `.bevelignore` layers like `.gitignore`,
13
+ * so both count at any depth; `roles.yaml` and the agent guide are read from
14
+ * the repository root only, so a nested file of either name is ordinary
15
+ * content. Moving one changes what the platform enforces, so moves refuse them.
16
+ *
17
+ * A FUNCTION, not a constant, and that is the whole of the configurable-guide
18
+ * change on this side: the guide's name is a deployment setting, so the fourth
19
+ * platform file is `HEXIS.md` on one deployment and `AGENTS.md` on the next —
20
+ * and on the first, a root `AGENTS.md` is the CUSTOMER'S own conventions file,
21
+ * which has to move and delete like any page. Every gate asks this rather than
22
+ * reading a list captured at module load.
23
+ */
24
+ export function platformFileNames(layout: KbLayout = currentKbLayout()): readonly string[] {
25
+ return [...FIXED_PLATFORM_FILE_NAMES, agentsFileOf(layout)];
26
+ }
27
+
28
+ /**
29
+ * The platform file names under the DEFAULT layout — what they were before the
30
+ * guide could be renamed. Kept for callers that want the default answer rather
31
+ * than this deployment's; anything judging a real path asks
32
+ * {@link platformFileNames}, which knows what this deployment called its guide.
33
+ */
34
+ export const PLATFORM_FILE_NAMES: readonly string[] = Object.freeze(
35
+ platformFileNames(DEFAULT_KB_LAYOUT),
36
+ );
37
+
38
+ /** The platform files that are read wherever they sit, not only at the root. */
39
+ const PLATFORM_FILES_AT_ANY_DEPTH = new Set(['access.md', '.bevelignore']);
40
+
41
+ /** The names in effect, as a set — rebuilt per call, because the guide's is configurable. */
42
+ const platformFiles = (): ReadonlySet<string> => new Set(platformFileNames());
43
+
44
+ const normalize = (path: string): string => path.replace(/^\.?\/+/, '').replace(/\/+$/, '');
45
+
46
+ const baseName = (path: string): string => {
47
+ const trimmed = normalize(path);
48
+ return trimmed.slice(trimmed.lastIndexOf('/') + 1);
49
+ };
50
+
51
+ /**
52
+ * A path that walks out of the repository (`..`) or stands still (`.`). The
53
+ * restore exception is the one write allowed past a destination that denies
54
+ * it, so it answers on the spelling it was handed and refuses anything whose
55
+ * meaning depends on resolving it: `KnowledgeBase/../access.md` names the
56
+ * root's file to a resolver and a nested one to a reader of segments. The
57
+ * move's own path-safety check refuses these too — this gate does not lean on
58
+ * that one.
59
+ */
60
+ const hasTraversal = (path: string): boolean =>
61
+ normalize(path).split('/').some((segment) => segment === '..' || segment === '.');
62
+
63
+ /**
64
+ * Whether the repository-relative `repoRelativePath` is a platform file.
65
+ * Exact spelling, as the platform reads it: `Access.md` is content. A caller
66
+ * on a case-insensitive disk passes the path's on-disk spelling.
67
+ */
68
+ export function isPlatformFile(repoRelativePath: string): boolean {
69
+ const norm = normalize(repoRelativePath);
70
+ const name = baseName(norm);
71
+ if (!platformFiles().has(name)) return false;
72
+ return PLATFORM_FILES_AT_ANY_DEPTH.has(name) || norm === name;
73
+ }
74
+
75
+ /** The one sentence every surface refuses a platform-file move with. */
76
+ export function platformFileRefusal(pathOrName: string): string {
77
+ return `${baseName(pathOrName)} is a platform file and stays in its folder.`;
78
+ }
79
+
80
+ /**
81
+ * The sentence a move is refused with when the DESTINATION would be a platform
82
+ * file — a note renamed to `access.md`, or dragged onto the one that is there.
83
+ * The other refusal keeps a platform file where the platform reads it; this one
84
+ * keeps everything else from becoming one, which a rename on disk would
85
+ * otherwise do silently: the folder would come back governed by rules nobody
86
+ * wrote as rules.
87
+ */
88
+ export function platformFileCreationRefusal(pathOrName: string): string {
89
+ return `${baseName(pathOrName)} is a platform file name; a move cannot create a platform file.`;
90
+ }
91
+
92
+ /**
93
+ * Whether `repoRelativeDir` is a folder the platform owns rather than content:
94
+ * the repository root itself, or one of its reserved top-level folders
95
+ * (`KnowledgeBase/`, `Skills/`, `Plugins/`, `Data/`, …). Deleting or moving
96
+ * one takes a whole section of the knowledge base with it. Exact spelling; a
97
+ * caller on a case-insensitive disk passes the path's on-disk spelling.
98
+ */
99
+ export function isPlatformFolder(repoRelativeDir: string): boolean {
100
+ const norm = normalize(repoRelativeDir);
101
+ if (norm === '') return true;
102
+ return !norm.includes('/') && reservedRootDirNames().has(norm);
103
+ }
104
+
105
+ /** The sentence a delete or move of a platform folder is refused with. */
106
+ export function platformFolderRefusal(repoRelativeDir: string): string {
107
+ const norm = normalize(repoRelativeDir);
108
+ return norm === ''
109
+ ? 'The repository root is a platform folder and cannot be moved or deleted.'
110
+ : `${norm}/ is a platform folder and cannot be moved or deleted.`;
111
+ }
112
+
113
+ /**
114
+ * Whether the platform file at `repoRelativePath` sits directly in the
115
+ * repository root — the copy every one of the four is read from there, and so
116
+ * never the misplaced one: it is the copy a restore puts back. A nested
117
+ * `access.md` or `.bevelignore` is a platform file too, but it layers on top
118
+ * of the root's rather than standing in for it, which is why moving the
119
+ * ROOT's copy into a folder is a move out and not a restore.
120
+ */
121
+ export function isRootPlatformFile(repoRelativePath: string): boolean {
122
+ return platformFiles().has(normalize(repoRelativePath));
123
+ }
124
+
125
+ /**
126
+ * The place a misplaced platform file is allowed to be put back, when
127
+ * `repoRelativeDestination` names one, and null when it does not.
128
+ *
129
+ * `roles.yaml` and the agent guide are read from the repository root and
130
+ * nowhere else, so their one required location is the root. A nested `.bevelignore` is
131
+ * read too (it layers, see `BevelIgnoreStack`), yet a restore of one lands at
132
+ * the root only — a deliberate narrowing of the exception, not a claim about
133
+ * where the file is read: the root's copy is the one whose absence breaks the
134
+ * workspace, and a folder that never had a `.bevelignore` is not missing one.
135
+ * `access.md` governs whatever folder it sits in, so a folder that has none
136
+ * is a place one is missing from — WHETHER the folder has one is a fact about
137
+ * the disk, which this pure predicate does not know and the caller checks
138
+ * (see `AccessControlService.canRestorePlatformFile`).
139
+ */
140
+ export type PlatformRestoreDestination =
141
+ | { name: string; kind: 'root' }
142
+ | { name: string; kind: 'folder-without-access-md'; dir: string };
143
+
144
+ export function platformRestoreDestination(
145
+ repoRelativeDestination: string,
146
+ ): PlatformRestoreDestination | null {
147
+ const norm = normalize(repoRelativeDestination);
148
+ if (hasTraversal(norm)) return null;
149
+ const name = baseName(norm);
150
+ if (!platformFiles().has(name)) return null;
151
+ if (name === 'access.md') {
152
+ const slash = norm.lastIndexOf('/');
153
+ return { name, kind: 'folder-without-access-md', dir: slash === -1 ? '' : norm.slice(0, slash) };
154
+ }
155
+ return norm === name ? { name, kind: 'root' } : null;
156
+ }
157
+
158
+ /**
159
+ * Whether moving `repoRelativeSource` to `repoRelativeDestination` has the
160
+ * SHAPE of a platform-file restore — an admin putting a misplaced copy back.
161
+ * Who is asking is not part of the shape; `canRestorePlatformFile` answers
162
+ * that, and the state of the disk with it.
163
+ *
164
+ * Three things make the shape, and all three are about the move rather than
165
+ * about the source's current standing:
166
+ *
167
+ * - the source is NAMED like a platform file. A nested `roles.yaml`, or a
168
+ * nested copy of the agent guide, is ordinary content where it sits
169
+ * (`isPlatformFile` says so, and moving it needs no exception), but it is
170
+ * still the copy a restore carries back to the root — judging the shape on
171
+ * `isPlatformFile` would skip the exception for exactly the two files the
172
+ * root can lose;
173
+ * - the source is not the root's own copy, which is the copy a restore puts
174
+ * back, never the one it takes out;
175
+ * - the destination is a required location for that same name, so the file
176
+ * lands under the name the platform reads rather than beside it.
177
+ */
178
+ export function isPlatformRestoreShape(
179
+ repoRelativeSource: string,
180
+ repoRelativeDestination: string,
181
+ ): boolean {
182
+ if (hasTraversal(repoRelativeSource)) return false;
183
+ const name = baseName(repoRelativeSource);
184
+ if (!platformFiles().has(name)) return false;
185
+ if (isRootPlatformFile(repoRelativeSource)) return false;
186
+ const target = platformRestoreDestination(repoRelativeDestination);
187
+ return target !== null && target.name === name;
188
+ }
@@ -26,6 +26,14 @@ export interface FileTreeEntry {
26
26
  relativePath: string;
27
27
  type: 'file' | 'directory';
28
28
  children?: FileTreeEntry[];
29
+ /**
30
+ * Set on a DIRECTORY of a read-filtered listing, and only when non-zero:
31
+ * how many entries the caller's read rules kept out of that directory's
32
+ * subtree — on the root, out of the whole listing. A number, never names —
33
+ * it lets the explorer tell "nothing is shared with you" apart from "this
34
+ * folder is empty" without saying what exists.
35
+ */
36
+ withheld?: number;
29
37
  }
30
38
 
31
39
  import type { AuthUser } from '../auth/types.js';