@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,105 @@
1
+ import { type KbLayout } from './kb-layout.js';
2
+ /**
3
+ * The files the platform reads as configuration, not content. `access.md`
4
+ * governs the folder it sits in and `.bevelignore` layers like `.gitignore`,
5
+ * so both count at any depth; `roles.yaml` and the agent guide are read from
6
+ * the repository root only, so a nested file of either name is ordinary
7
+ * content. Moving one changes what the platform enforces, so moves refuse them.
8
+ *
9
+ * A FUNCTION, not a constant, and that is the whole of the configurable-guide
10
+ * change on this side: the guide's name is a deployment setting, so the fourth
11
+ * platform file is `HEXIS.md` on one deployment and `AGENTS.md` on the next —
12
+ * and on the first, a root `AGENTS.md` is the CUSTOMER'S own conventions file,
13
+ * which has to move and delete like any page. Every gate asks this rather than
14
+ * reading a list captured at module load.
15
+ */
16
+ export declare function platformFileNames(layout?: KbLayout): readonly string[];
17
+ /**
18
+ * The platform file names under the DEFAULT layout — what they were before the
19
+ * guide could be renamed. Kept for callers that want the default answer rather
20
+ * than this deployment's; anything judging a real path asks
21
+ * {@link platformFileNames}, which knows what this deployment called its guide.
22
+ */
23
+ export declare const PLATFORM_FILE_NAMES: readonly string[];
24
+ /**
25
+ * Whether the repository-relative `repoRelativePath` is a platform file.
26
+ * Exact spelling, as the platform reads it: `Access.md` is content. A caller
27
+ * on a case-insensitive disk passes the path's on-disk spelling.
28
+ */
29
+ export declare function isPlatformFile(repoRelativePath: string): boolean;
30
+ /** The one sentence every surface refuses a platform-file move with. */
31
+ export declare function platformFileRefusal(pathOrName: string): string;
32
+ /**
33
+ * The sentence a move is refused with when the DESTINATION would be a platform
34
+ * file — a note renamed to `access.md`, or dragged onto the one that is there.
35
+ * The other refusal keeps a platform file where the platform reads it; this one
36
+ * keeps everything else from becoming one, which a rename on disk would
37
+ * otherwise do silently: the folder would come back governed by rules nobody
38
+ * wrote as rules.
39
+ */
40
+ export declare function platformFileCreationRefusal(pathOrName: string): string;
41
+ /**
42
+ * Whether `repoRelativeDir` is a folder the platform owns rather than content:
43
+ * the repository root itself, or one of its reserved top-level folders
44
+ * (`KnowledgeBase/`, `Skills/`, `Plugins/`, `Data/`, …). Deleting or moving
45
+ * one takes a whole section of the knowledge base with it. Exact spelling; a
46
+ * caller on a case-insensitive disk passes the path's on-disk spelling.
47
+ */
48
+ export declare function isPlatformFolder(repoRelativeDir: string): boolean;
49
+ /** The sentence a delete or move of a platform folder is refused with. */
50
+ export declare function platformFolderRefusal(repoRelativeDir: string): string;
51
+ /**
52
+ * Whether the platform file at `repoRelativePath` sits directly in the
53
+ * repository root — the copy every one of the four is read from there, and so
54
+ * never the misplaced one: it is the copy a restore puts back. A nested
55
+ * `access.md` or `.bevelignore` is a platform file too, but it layers on top
56
+ * of the root's rather than standing in for it, which is why moving the
57
+ * ROOT's copy into a folder is a move out and not a restore.
58
+ */
59
+ export declare function isRootPlatformFile(repoRelativePath: string): boolean;
60
+ /**
61
+ * The place a misplaced platform file is allowed to be put back, when
62
+ * `repoRelativeDestination` names one, and null when it does not.
63
+ *
64
+ * `roles.yaml` and the agent guide are read from the repository root and
65
+ * nowhere else, so their one required location is the root. A nested `.bevelignore` is
66
+ * read too (it layers, see `BevelIgnoreStack`), yet a restore of one lands at
67
+ * the root only — a deliberate narrowing of the exception, not a claim about
68
+ * where the file is read: the root's copy is the one whose absence breaks the
69
+ * workspace, and a folder that never had a `.bevelignore` is not missing one.
70
+ * `access.md` governs whatever folder it sits in, so a folder that has none
71
+ * is a place one is missing from — WHETHER the folder has one is a fact about
72
+ * the disk, which this pure predicate does not know and the caller checks
73
+ * (see `AccessControlService.canRestorePlatformFile`).
74
+ */
75
+ export type PlatformRestoreDestination = {
76
+ name: string;
77
+ kind: 'root';
78
+ } | {
79
+ name: string;
80
+ kind: 'folder-without-access-md';
81
+ dir: string;
82
+ };
83
+ export declare function platformRestoreDestination(repoRelativeDestination: string): PlatformRestoreDestination | null;
84
+ /**
85
+ * Whether moving `repoRelativeSource` to `repoRelativeDestination` has the
86
+ * SHAPE of a platform-file restore — an admin putting a misplaced copy back.
87
+ * Who is asking is not part of the shape; `canRestorePlatformFile` answers
88
+ * that, and the state of the disk with it.
89
+ *
90
+ * Three things make the shape, and all three are about the move rather than
91
+ * about the source's current standing:
92
+ *
93
+ * - the source is NAMED like a platform file. A nested `roles.yaml`, or a
94
+ * nested copy of the agent guide, is ordinary content where it sits
95
+ * (`isPlatformFile` says so, and moving it needs no exception), but it is
96
+ * still the copy a restore carries back to the root — judging the shape on
97
+ * `isPlatformFile` would skip the exception for exactly the two files the
98
+ * root can lose;
99
+ * - the source is not the root's own copy, which is the copy a restore puts
100
+ * back, never the one it takes out;
101
+ * - the destination is a required location for that same name, so the file
102
+ * lands under the name the platform reads rather than beside it.
103
+ */
104
+ export declare function isPlatformRestoreShape(repoRelativeSource: string, repoRelativeDestination: string): boolean;
105
+ //# sourceMappingURL=platform-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-files.d.ts","sourceRoot":"","sources":["../../src/workspace/platform-files.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,QAAQ,EACd,MAAM,gBAAgB,CAAC;AAExB;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,QAA4B,GAAG,SAAS,MAAM,EAAE,CAEzF;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAEhD,CAAC;AA2BF;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAKhE;AAED,wEAAwE;AACxE,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAIjE;AAED,0EAA0E;AAC1E,wBAAgB,qBAAqB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAKrE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAEpE;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,0BAA0B,GAClC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,0BAA0B,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE,wBAAgB,0BAA0B,CACxC,uBAAuB,EAAE,MAAM,GAC9B,0BAA0B,GAAG,IAAI,CAUnC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,sBAAsB,CACpC,kBAAkB,EAAE,MAAM,EAC1B,uBAAuB,EAAE,MAAM,GAC9B,OAAO,CAOT"}
@@ -0,0 +1,147 @@
1
+ import { DEFAULT_KB_LAYOUT, FIXED_PLATFORM_FILE_NAMES, agentsFileOf, currentKbLayout, reservedRootDirNames, } from './kb-layout.js';
2
+ /**
3
+ * The files the platform reads as configuration, not content. `access.md`
4
+ * governs the folder it sits in and `.bevelignore` layers like `.gitignore`,
5
+ * so both count at any depth; `roles.yaml` and the agent guide are read from
6
+ * the repository root only, so a nested file of either name is ordinary
7
+ * content. Moving one changes what the platform enforces, so moves refuse them.
8
+ *
9
+ * A FUNCTION, not a constant, and that is the whole of the configurable-guide
10
+ * change on this side: the guide's name is a deployment setting, so the fourth
11
+ * platform file is `HEXIS.md` on one deployment and `AGENTS.md` on the next —
12
+ * and on the first, a root `AGENTS.md` is the CUSTOMER'S own conventions file,
13
+ * which has to move and delete like any page. Every gate asks this rather than
14
+ * reading a list captured at module load.
15
+ */
16
+ export function platformFileNames(layout = currentKbLayout()) {
17
+ return [...FIXED_PLATFORM_FILE_NAMES, agentsFileOf(layout)];
18
+ }
19
+ /**
20
+ * The platform file names under the DEFAULT layout — what they were before the
21
+ * guide could be renamed. Kept for callers that want the default answer rather
22
+ * than this deployment's; anything judging a real path asks
23
+ * {@link platformFileNames}, which knows what this deployment called its guide.
24
+ */
25
+ export const PLATFORM_FILE_NAMES = Object.freeze(platformFileNames(DEFAULT_KB_LAYOUT));
26
+ /** The platform files that are read wherever they sit, not only at the root. */
27
+ const PLATFORM_FILES_AT_ANY_DEPTH = new Set(['access.md', '.bevelignore']);
28
+ /** The names in effect, as a set — rebuilt per call, because the guide's is configurable. */
29
+ const platformFiles = () => new Set(platformFileNames());
30
+ const normalize = (path) => path.replace(/^\.?\/+/, '').replace(/\/+$/, '');
31
+ const baseName = (path) => {
32
+ const trimmed = normalize(path);
33
+ return trimmed.slice(trimmed.lastIndexOf('/') + 1);
34
+ };
35
+ /**
36
+ * A path that walks out of the repository (`..`) or stands still (`.`). The
37
+ * restore exception is the one write allowed past a destination that denies
38
+ * it, so it answers on the spelling it was handed and refuses anything whose
39
+ * meaning depends on resolving it: `KnowledgeBase/../access.md` names the
40
+ * root's file to a resolver and a nested one to a reader of segments. The
41
+ * move's own path-safety check refuses these too — this gate does not lean on
42
+ * that one.
43
+ */
44
+ const hasTraversal = (path) => normalize(path).split('/').some((segment) => segment === '..' || segment === '.');
45
+ /**
46
+ * Whether the repository-relative `repoRelativePath` is a platform file.
47
+ * Exact spelling, as the platform reads it: `Access.md` is content. A caller
48
+ * on a case-insensitive disk passes the path's on-disk spelling.
49
+ */
50
+ export function isPlatformFile(repoRelativePath) {
51
+ const norm = normalize(repoRelativePath);
52
+ const name = baseName(norm);
53
+ if (!platformFiles().has(name))
54
+ return false;
55
+ return PLATFORM_FILES_AT_ANY_DEPTH.has(name) || norm === name;
56
+ }
57
+ /** The one sentence every surface refuses a platform-file move with. */
58
+ export function platformFileRefusal(pathOrName) {
59
+ return `${baseName(pathOrName)} is a platform file and stays in its folder.`;
60
+ }
61
+ /**
62
+ * The sentence a move is refused with when the DESTINATION would be a platform
63
+ * file — a note renamed to `access.md`, or dragged onto the one that is there.
64
+ * The other refusal keeps a platform file where the platform reads it; this one
65
+ * keeps everything else from becoming one, which a rename on disk would
66
+ * otherwise do silently: the folder would come back governed by rules nobody
67
+ * wrote as rules.
68
+ */
69
+ export function platformFileCreationRefusal(pathOrName) {
70
+ return `${baseName(pathOrName)} is a platform file name; a move cannot create a platform file.`;
71
+ }
72
+ /**
73
+ * Whether `repoRelativeDir` is a folder the platform owns rather than content:
74
+ * the repository root itself, or one of its reserved top-level folders
75
+ * (`KnowledgeBase/`, `Skills/`, `Plugins/`, `Data/`, …). Deleting or moving
76
+ * one takes a whole section of the knowledge base with it. Exact spelling; a
77
+ * caller on a case-insensitive disk passes the path's on-disk spelling.
78
+ */
79
+ export function isPlatformFolder(repoRelativeDir) {
80
+ const norm = normalize(repoRelativeDir);
81
+ if (norm === '')
82
+ return true;
83
+ return !norm.includes('/') && reservedRootDirNames().has(norm);
84
+ }
85
+ /** The sentence a delete or move of a platform folder is refused with. */
86
+ export function platformFolderRefusal(repoRelativeDir) {
87
+ const norm = normalize(repoRelativeDir);
88
+ return norm === ''
89
+ ? 'The repository root is a platform folder and cannot be moved or deleted.'
90
+ : `${norm}/ is a platform folder and cannot be moved or deleted.`;
91
+ }
92
+ /**
93
+ * Whether the platform file at `repoRelativePath` sits directly in the
94
+ * repository root — the copy every one of the four is read from there, and so
95
+ * never the misplaced one: it is the copy a restore puts back. A nested
96
+ * `access.md` or `.bevelignore` is a platform file too, but it layers on top
97
+ * of the root's rather than standing in for it, which is why moving the
98
+ * ROOT's copy into a folder is a move out and not a restore.
99
+ */
100
+ export function isRootPlatformFile(repoRelativePath) {
101
+ return platformFiles().has(normalize(repoRelativePath));
102
+ }
103
+ export function platformRestoreDestination(repoRelativeDestination) {
104
+ const norm = normalize(repoRelativeDestination);
105
+ if (hasTraversal(norm))
106
+ return null;
107
+ const name = baseName(norm);
108
+ if (!platformFiles().has(name))
109
+ return null;
110
+ if (name === 'access.md') {
111
+ const slash = norm.lastIndexOf('/');
112
+ return { name, kind: 'folder-without-access-md', dir: slash === -1 ? '' : norm.slice(0, slash) };
113
+ }
114
+ return norm === name ? { name, kind: 'root' } : null;
115
+ }
116
+ /**
117
+ * Whether moving `repoRelativeSource` to `repoRelativeDestination` has the
118
+ * SHAPE of a platform-file restore — an admin putting a misplaced copy back.
119
+ * Who is asking is not part of the shape; `canRestorePlatformFile` answers
120
+ * that, and the state of the disk with it.
121
+ *
122
+ * Three things make the shape, and all three are about the move rather than
123
+ * about the source's current standing:
124
+ *
125
+ * - the source is NAMED like a platform file. A nested `roles.yaml`, or a
126
+ * nested copy of the agent guide, is ordinary content where it sits
127
+ * (`isPlatformFile` says so, and moving it needs no exception), but it is
128
+ * still the copy a restore carries back to the root — judging the shape on
129
+ * `isPlatformFile` would skip the exception for exactly the two files the
130
+ * root can lose;
131
+ * - the source is not the root's own copy, which is the copy a restore puts
132
+ * back, never the one it takes out;
133
+ * - the destination is a required location for that same name, so the file
134
+ * lands under the name the platform reads rather than beside it.
135
+ */
136
+ export function isPlatformRestoreShape(repoRelativeSource, repoRelativeDestination) {
137
+ if (hasTraversal(repoRelativeSource))
138
+ return false;
139
+ const name = baseName(repoRelativeSource);
140
+ if (!platformFiles().has(name))
141
+ return false;
142
+ if (isRootPlatformFile(repoRelativeSource))
143
+ return false;
144
+ const target = platformRestoreDestination(repoRelativeDestination);
145
+ return target !== null && target.name === name;
146
+ }
147
+ //# sourceMappingURL=platform-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-files.js","sourceRoot":"","sources":["../../src/workspace/platform-files.ts"],"names":[],"mappings":"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"}
@@ -25,6 +25,14 @@ export interface FileTreeEntry {
25
25
  relativePath: string;
26
26
  type: 'file' | 'directory';
27
27
  children?: FileTreeEntry[];
28
+ /**
29
+ * Set on a DIRECTORY of a read-filtered listing, and only when non-zero:
30
+ * how many entries the caller's read rules kept out of that directory's
31
+ * subtree — on the root, out of the whole listing. A number, never names —
32
+ * it lets the explorer tell "nothing is shared with you" apart from "this
33
+ * folder is empty" without saying what exists.
34
+ */
35
+ withheld?: number;
28
36
  }
29
37
  import type { AuthUser } from '../auth/types.js';
30
38
  export interface IWorkspaceService {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/workspace/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,EAAE,EAAE,MAAM,CAAC;IACX,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,YAAY,EAAE,MAAM,CAAC;IACrB,4FAA4F;IAC5F,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;CAC5B;AAED,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IAC7D;;;;;OAKG;IACH,kBAAkB,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5E,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAChE,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACvD,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrE,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrF,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5F,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChG,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvD,oFAAoF;IACpF,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9C"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/workspace/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,EAAE,EAAE,MAAM,CAAC;IACX,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,YAAY,EAAE,MAAM,CAAC;IACrB,4FAA4F;IAC5F,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IAC7D;;;;;OAKG;IACH,kBAAkB,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5E,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAChE,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACvD,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrE,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrF,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5F,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChG,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvD,oFAAoF;IACpF,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bevel-software/platform-shared",
3
- "version": "0.15.1",
3
+ "version": "0.19.0",
4
4
  "description": "Shared types and pure domain utilities of the Bevel core platform (auth, workspace, git/workflow contracts, branch registry).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/auth/types.ts CHANGED
@@ -11,6 +11,18 @@ export interface AuthUser {
11
11
  * absent field must never resurrect the welcome flow.
12
12
  */
13
13
  onboardingDone?: boolean;
14
+ /**
15
+ * Is this the deployment admin — the account whose password is set in the
16
+ * deployment environment (`ADMIN_EMAIL` while `ADMIN_PASSWORD` is set)
17
+ * rather than stored as a hash? That credential is the platform's rescue
18
+ * path into a deployment, so this account's password cannot be changed from
19
+ * the Account page. Derived from configuration on every read rather than
20
+ * stored, and it carries no part of the credential itself. Optional for the
21
+ * same reason as `onboardingDone` above — pre-existing fixtures and cached
22
+ * user objects stay valid — and only an explicit `true` means "deployment
23
+ * admin".
24
+ */
25
+ isEnvAdmin?: boolean;
14
26
  }
15
27
 
16
28
  export interface LoginRequest {
@@ -44,7 +44,39 @@ export interface PullRequestSummary {
44
44
  /** Relative paths within `knowledge-base/`. Empty if not yet computed. */
45
45
  touchedNodePaths: string[];
46
46
  review: PullRequestReviewStatus;
47
+ /**
48
+ * Link to the change request: absolute (`<public frontend address>/change-requests/<number>`)
49
+ * when the deployment has a public address configured, else the in-app relative path.
50
+ */
47
51
  url: string;
52
+ /** Present when `url` is relative — says how to get absolute links. */
53
+ urlNote?: string;
54
+ /**
55
+ * The most recent apply attempt that did not land, while the request is
56
+ * still open — so its author and every other viewer see the refusal the
57
+ * person who clicked Apply saw. Replaced by a newer refusal; null or
58
+ * absent when there is nothing to report.
59
+ */
60
+ lastApplyFailure?: ChangeRequestApplyFailure | null;
61
+ }
62
+
63
+ /**
64
+ * What refused an apply: the merge gate (approvals it still waits on), git
65
+ * (conflicts with the target), or anything else (a push, the roles.yaml guard,
66
+ * an internal error). Decides which later change makes the refusal obsolete.
67
+ */
68
+ export type ChangeRequestApplyFailureKind = 'gate' | 'conflicts' | 'error';
69
+
70
+ /** Why the last apply of a change request failed, as persisted on the request. */
71
+ export interface ChangeRequestApplyFailure {
72
+ /** Human-readable reason, credentials already redacted. */
73
+ reason: string;
74
+ /** True when git refused the merge on conflicts with the target. */
75
+ conflicts: boolean;
76
+ /** ISO timestamp of the failed attempt. */
77
+ at: string;
78
+ /** Display name of whoever attempted the apply. */
79
+ byName: string;
48
80
  }
49
81
 
50
82
  export type PrFileStatus =
@@ -145,16 +177,31 @@ export interface FileApprovalState {
145
177
  users: { name: string; email: string }[];
146
178
  };
147
179
  approvedBy: FileApprovalEntry[];
180
+ /**
181
+ * True only when `eligibleApprovers` is the access tree's actual answer for
182
+ * this file. False (or absent) when it could not be resolved — no workspace,
183
+ * no usable access config on the base, a failed lookup — in which case the
184
+ * empty approver set means "unknown", not "outside the gate", and anything
185
+ * granted on the strength of that emptiness must fail closed.
186
+ */
187
+ eligibilityResolved?: boolean;
148
188
  /**
149
189
  * True iff at least one eligible approver has submitted a non-stale
150
190
  * approval. Always `false` when `eligibleApprovers` is empty — with no
151
191
  * eligible set, nobody can satisfy the check.
152
192
  * Whether the gate *cares* about this file is a separate concern handled
153
- * by the merge-gate logic: non-md files and files with no eligible
154
- * approvers are silently excluded from the gate, so `isApproved: false`
193
+ * by the merge-gate logic: files with no eligible approvers, whatever
194
+ * their type, are silently excluded from the gate, so `isApproved: false`
155
195
  * on one of them does not block merge.
156
196
  */
157
197
  isApproved: boolean;
198
+ /**
199
+ * Whether the merge gate binds this file at all — the backend's one
200
+ * relevance rule, stamped per file so clients read the verdict instead of
201
+ * re-deriving it. False files neither warn nor block a merge, so no surface
202
+ * should count them as pending or name anyone to wait on.
203
+ */
204
+ inMergeGate: boolean;
158
205
  /**
159
206
  * Pre-computed for the requesting viewer: would `approveFile` accept their
160
207
  * click? True iff their email resolves to `write` on this path under the
@@ -184,21 +231,21 @@ export interface PullRequestDetail extends PullRequestSummary {
184
231
  */
185
232
  approvals: FileApprovalState[];
186
233
  /**
187
- * True iff no *hard* block applies — the PR is open, has files, and isn't
188
- * merged/closed. Soft warnings (missing owner approvals on md files) do
189
- * **not** set this to false; the UI handles them via the bypass dialog.
190
- * The button is disabled only when this is false.
234
+ * True iff no blocking reason remains — the PR is open, has files, isn't
235
+ * merged/closed, and every file with an eligible approver (of any file
236
+ * type) holds a current approval. False while any approval is missing; an
237
+ * admin may still merge past missing approvals with the bypass flag.
191
238
  */
192
239
  mergeableInBevel: boolean;
193
240
  /**
194
- * Hard-block reasons — merging is impossible until these resolve (PR state,
195
- * no files, etc.). Empty when the PR can be merged (possibly after bypass).
241
+ * Blocking reasons — hard blocks (PR state, no files) followed by each
242
+ * missing approval. Empty exactly when `mergeableInBevel` is true.
196
243
  */
197
244
  mergeBlockedReasons: string[];
198
245
  /**
199
- * Soft warnings — md files with an owner who hasn't approved (or whose
200
- * approval is stale). Merging is allowed but the UI asks for an explicit
201
- * bypass confirmation first. Non-md files and ownerless md files are silent.
246
+ * The missing approvals alone — files of any type with an eligible approver
247
+ * who hasn't approved (or whose approval is stale). An admin bypass merges
248
+ * past exactly these and records them. Files nobody can approve are silent.
202
249
  */
203
250
  mergeWarnings: string[];
204
251
  /**
@@ -221,6 +268,25 @@ export interface PullRequestDetail extends PullRequestSummary {
221
268
  * tree can't be resolved.
222
269
  */
223
270
  viewerCanCancel: boolean;
271
+ /**
272
+ * The commit this request forked from its target (merge base of `headSha`
273
+ * and `baseSha`). Every file diff in the request reads its "before" side
274
+ * here, never at the target tip — an edit made on the target after the
275
+ * proposal must not look like something the proposal deletes. `null` when
276
+ * the branches share no history (or no workspace could resolve them).
277
+ */
278
+ mergeBaseSha: string | null;
279
+ /**
280
+ * True iff the target holds commits the proposal does not contain — the
281
+ * request needs updating. False for anything not open.
282
+ */
283
+ behind: boolean;
284
+ /**
285
+ * True iff the viewer may Update the request (merge its target into it):
286
+ * the request is open AND the viewer is its author or may apply it. A UX
287
+ * hint — the update route re-checks the same predicate server-side.
288
+ */
289
+ viewerCanUpdate: boolean;
224
290
  }
225
291
 
226
292
  /**
package/src/git/types.ts CHANGED
@@ -249,6 +249,40 @@ export interface IGitService {
249
249
  * Just the repo-relative paths a change request touches (three-dot diff,
250
250
  * no statuses, no patches): the cheap form behind change-request list
251
251
  * summaries and owner routing.
252
+ *
253
+ * `fetch: false` skips the per-request fetch of the two refs — for a caller
254
+ * that has just refreshed the whole clone's remote-tracking refs in one
255
+ * round trip, which is what a LIST does rather than paying one fetch per
256
+ * request. Pass it only when that is true; otherwise the diff can describe
257
+ * a stale head. It is a skip, not a promise: a branch the clone does not
258
+ * have yet is fetched anyway, since there is nothing to diff without it —
259
+ * so a list's first sight of a new request still costs one round trip.
260
+ */
261
+ changedPathsForPr(
262
+ workspaceId: string,
263
+ baseBranch: string,
264
+ headBranch: string,
265
+ opts?: { fetch?: boolean },
266
+ ): Promise<string[]>;
267
+
268
+ /**
269
+ * A change request's fork point (merge base of the two resolved commits)
270
+ * and whether the target has commits the proposal does not contain. No
271
+ * fetch: `at` is what `resolvePrShas` just returned.
252
272
  */
253
- changedPathsForPr(workspaceId: string, baseBranch: string, headBranch: string): Promise<string[]>;
273
+ forkPointForPr(
274
+ workspaceId: string,
275
+ at: { baseSha: string; headSha: string },
276
+ ): Promise<{ mergeBaseSha: string | null; behind: boolean }>;
277
+
278
+ /**
279
+ * A file's content at a change request's fork point — a commit that must
280
+ * be on `baseBranch`'s history. `null` when the path did not exist there.
281
+ */
282
+ readFileAtForkPoint(
283
+ workspaceId: string,
284
+ baseBranch: string,
285
+ sha: string,
286
+ relativePath: string,
287
+ ): Promise<string | null>;
254
288
  }
package/src/index.ts CHANGED
@@ -6,10 +6,16 @@ export * from './chat/types.js';
6
6
 
7
7
  // Workspace
8
8
  export * from './workspace/types.js';
9
+ export * from './workspace/agent-preamble.js';
9
10
  export * from './workspace/filename.js';
11
+ export * from './workspace/entry-exists.js';
10
12
  export * from './workspace/kb-layout.js';
11
13
  export * from './workspace/join-request.js';
12
14
  export * from './workspace/frontmatter.js';
15
+ export * from './workspace/placeholder.js';
16
+ export * from './workspace/frontmatter-carriers.js';
17
+ export * from './workspace/platform-files.js';
18
+ export * from './workspace/access-verbs.js';
13
19
 
14
20
  // Git
15
21
  export * from './git/types.js';
@@ -221,6 +221,28 @@ export interface ChangeRequestRejectedEvent {
221
221
  number: number;
222
222
  }
223
223
 
224
+ /**
225
+ * An apply did NOT land, announced to EVERY session — the counterpart of the
226
+ * user-scoped `change-request-merge-failed` below, which only the clicker gets.
227
+ *
228
+ * The request stays open, so everyone who can see it (its author waiting on
229
+ * the verdict, another owner, another admin) needs to learn that the attempt
230
+ * failed and why. The reason is deliberately NOT carried here: it is persisted
231
+ * on the request (`PullRequestSummary.lastApplyFailure`) and read back through
232
+ * the list and detail endpoints the viewer already uses, so the broadcast puts
233
+ * no error text in front of every session, and a session that missed the event
234
+ * reads the same answer on its next fetch.
235
+ *
236
+ * Also sent when a recorded refusal is CLEARED because a change made it
237
+ * obsolete (an approval for a gate refusal, a moved source head for any):
238
+ * either way the request's `lastApplyFailure` changed, and every viewer
239
+ * re-reads it.
240
+ */
241
+ export interface ChangeRequestApplyFailedEvent {
242
+ kind: 'change-request-apply-failed';
243
+ number: number;
244
+ }
245
+
224
246
  /**
225
247
  * A merge the caller triggered did NOT land — either the gate refused it, the
226
248
  * branch needs conflict resolution, or `gh pr merge` failed. The merge route
@@ -228,9 +250,10 @@ export interface ChangeRequestRejectedEvent {
228
250
  * gateway timeout), so this is how the failure reaches the UI that kicked it
229
251
  * off. Success travels on `change-request-merged` instead.
230
252
  *
231
- * User-scoped (`forUserId`, no `workspaceId`) — a failed merge changes no
232
- * shared state; only the user who clicked needs the reason, so we don't
233
- * broadcast the error string to every session.
253
+ * User-scoped (`forUserId`, no `workspaceId`) — the clicker's immediate
254
+ * answer, which also routes a conflict into the resolution flow. Everyone else
255
+ * learns of the failure from `change-request-apply-failed`, which carries no
256
+ * error string; the reason itself is persisted on the request.
234
257
  */
235
258
  export interface ChangeRequestMergeFailedEvent {
236
259
  kind: 'change-request-merge-failed';
@@ -243,6 +266,12 @@ export interface ChangeRequestMergeFailedEvent {
243
266
  * The UI routes to the agent resolution flow instead of showing an error.
244
267
  */
245
268
  conflicts: boolean;
269
+ /**
270
+ * When the attempt failed (ISO) — the same instant persisted as
271
+ * `lastApplyFailure.at` when the refusal is recorded, so the clicker's tab
272
+ * can tell its own refusal from a later one somebody else's attempt made.
273
+ */
274
+ at?: string;
246
275
  }
247
276
 
248
277
  /**
@@ -296,6 +325,7 @@ export type WorkflowEventPayload =
296
325
  | ChangeRequestMergedEvent
297
326
  | ChangeRequestRejectedEvent
298
327
  | ChangeRequestMergeFailedEvent
328
+ | ChangeRequestApplyFailedEvent
299
329
  | ApprovalChangedEvent
300
330
  | HeartbeatEvent
301
331
  | ResyncEvent;