@bevel-software/platform-core-backend 0.7.4 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -202
- package/THIRD-PARTY-NOTICES.md +428 -454
- package/dist/core/core-ports.d.ts +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +51 -7
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +5 -3
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +38 -21
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +8 -7
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +1 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +6 -6
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-declarations.d.ts +5 -5
- package/dist/modules/access/access-declarations.js +3 -3
- package/dist/modules/access/access-mutation.service.d.ts +3 -3
- package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
- package/dist/modules/access/access-mutation.service.js +3 -3
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-splice.js +4 -4
- package/dist/modules/access/access-splice.js.map +1 -1
- package/dist/modules/access/access.routes.js +19 -19
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/creator-access.d.ts +2 -2
- package/dist/modules/access/creator-access.js +5 -5
- package/dist/modules/access/creator-access.js.map +1 -1
- package/dist/modules/access/roles-admin.service.d.ts +18 -4
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +14 -4
- package/dist/modules/access/roles-admin.service.js.map +1 -1
- package/dist/modules/code-mode/code-mode-names.d.ts +5 -13
- package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode-names.js +5 -27
- package/dist/modules/code-mode/code-mode-names.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +29 -7
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +11 -52
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +33 -395
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/plugins/index.d.ts +7 -0
- package/dist/modules/plugins/index.d.ts.map +1 -0
- package/dist/modules/plugins/index.js +6 -0
- package/dist/modules/plugins/index.js.map +1 -0
- package/dist/modules/plugins/join-proposals.d.ts +53 -0
- package/dist/modules/plugins/join-proposals.d.ts.map +1 -0
- package/dist/modules/plugins/join-proposals.js +67 -0
- package/dist/modules/plugins/join-proposals.js.map +1 -0
- package/dist/modules/plugins/join-requests.service.d.ts +81 -0
- package/dist/modules/plugins/join-requests.service.d.ts.map +1 -0
- package/dist/modules/plugins/join-requests.service.js +135 -0
- package/dist/modules/plugins/join-requests.service.js.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts +134 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.js +344 -0
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -0
- package/dist/modules/plugins/plugins.contract.d.ts +106 -0
- package/dist/modules/plugins/plugins.contract.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.contract.js +36 -0
- package/dist/modules/plugins/plugins.contract.js.map +1 -0
- package/dist/modules/plugins/plugins.routes.d.ts +42 -0
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.routes.js +379 -0
- package/dist/modules/plugins/plugins.routes.js.map +1 -0
- package/dist/modules/plugins/plugins.service.d.ts +60 -0
- package/dist/modules/plugins/plugins.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.service.js +172 -0
- package/dist/modules/plugins/plugins.service.js.map +1 -0
- package/dist/modules/skills/pending-skills.service.d.ts +2 -2
- package/dist/modules/skills/pending-skills.service.js +7 -7
- package/dist/modules/skills/pending-skills.service.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +4 -4
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.contract.js +1 -1
- package/dist/modules/skills/skills.service.js +5 -5
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +65 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.js +276 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +92 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.js +328 -0
- package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +38 -12
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +13 -2
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.js +233 -2
- package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +74 -37
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -3
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/workflow/git/git.service.js +2 -2
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +11 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +37 -9
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts +17 -4
- package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.js +63 -9
- package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +1 -0
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +6 -1
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/kb-seed.service.d.ts +2 -2
- package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
- package/dist/modules/workspace/kb-seed.service.js +43 -10
- package/dist/modules/workspace/kb-seed.service.js.map +1 -1
- package/dist/modules/workspace/plugins-migration.d.ts +50 -0
- package/dist/modules/workspace/plugins-migration.d.ts.map +1 -0
- package/dist/modules/workspace/plugins-migration.js +379 -0
- package/dist/modules/workspace/plugins-migration.js.map +1 -0
- package/dist/modules/workspace/workspace.routes.js +3 -3
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +26 -0
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +83 -12
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/shared/kb-layout.test.js +3 -3
- package/dist/shared/kb-layout.test.js.map +1 -1
- package/dist/shared/utcp-namespace.d.ts +6 -27
- package/dist/shared/utcp-namespace.d.ts.map +1 -1
- package/dist/shared/utcp-namespace.js +6 -63
- package/dist/shared/utcp-namespace.js.map +1 -1
- package/dist/shared/variable-refs.d.ts +42 -0
- package/dist/shared/variable-refs.d.ts.map +1 -0
- package/dist/shared/variable-refs.js +60 -0
- package/dist/shared/variable-refs.js.map +1 -0
- package/kb-template/.bevelignore +1 -1
- package/kb-template/AGENTS.md +88 -35
- package/kb-template/KnowledgeBase/How to get started.md +10 -10
- package/kb-template/access.md +36 -36
- package/migrations/meta/0000_snapshot.json +1479 -1479
- package/package.json +5 -4
- package/src/assets.ts +25 -25
- package/src/core/core-ports.ts +106 -106
- package/src/core/create-core-server.ts +55 -9
- package/src/core/create-core-services.ts +48 -24
- package/src/index.ts +69 -69
- package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -98
- package/src/modules/access/__tests__/access-declarations.test.ts +28 -28
- package/src/modules/access/__tests__/access-md-format.test.ts +18 -18
- package/src/modules/access/__tests__/access-mutation.service.test.ts +5 -5
- package/src/modules/access/__tests__/access-splice.test.ts +2 -2
- package/src/modules/access/__tests__/access.routes.overrides.test.ts +16 -16
- package/src/modules/access/__tests__/grant-sources.test.ts +12 -12
- package/src/modules/access/__tests__/roles-admin.service.test.ts +13 -1
- package/src/modules/access/access-control.interface.ts +8 -7
- package/src/modules/access/access-control.service.ts +7 -7
- package/src/modules/access/access-declarations.ts +5 -5
- package/src/modules/access/access-mutation.service.ts +3 -3
- package/src/modules/access/access-splice.ts +4 -4
- package/src/modules/access/access.routes.ts +20 -20
- package/src/modules/access/creator-access.ts +5 -5
- package/src/modules/access/roles-admin.service.ts +13 -2
- package/src/modules/admin/admin-access.routes.ts +29 -29
- package/src/modules/auth/__tests__/auth.routes.test.ts +91 -91
- package/src/modules/auth/__tests__/rate-limit.test.ts +36 -36
- package/src/modules/auth/rate-limit.ts +45 -45
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +67 -0
- package/src/modules/code-mode/code-mode-names.ts +10 -36
- package/src/modules/code-mode/code-mode.tool.ts +27 -7
- package/src/modules/database/connection.ts +15 -15
- package/src/modules/database/schema.ts +11 -11
- package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -150
- package/src/modules/mcp/mcp.service.ts +57 -435
- package/src/modules/{groups → plugins}/__tests__/join-proposals.test.ts +1 -1
- package/src/modules/{groups → plugins}/__tests__/join-requests.service.test.ts +7 -7
- package/src/modules/{groups/__tests__/group-index.service.test.ts → plugins/__tests__/plugin-index.service.test.ts} +41 -41
- package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +312 -0
- package/src/modules/{groups/__tests__/groups.routes.test.ts → plugins/__tests__/plugins.routes.test.ts} +100 -100
- package/src/modules/plugins/index.ts +17 -0
- package/src/modules/{groups → plugins}/join-proposals.ts +2 -2
- package/src/modules/{groups → plugins}/join-requests.service.ts +8 -8
- package/src/modules/{groups/group-provision.service.ts → plugins/plugin-provision.service.ts} +139 -69
- package/src/modules/{groups/groups.contract.ts → plugins/plugins.contract.ts} +26 -26
- package/src/modules/{groups/groups.routes.ts → plugins/plugins.routes.ts} +102 -102
- package/src/modules/{groups/groups.service.ts → plugins/plugins.service.ts} +43 -43
- package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -143
- package/src/modules/skills/__tests__/pending-skills.service.test.ts +14 -14
- package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
- package/src/modules/skills/pending-skills.service.ts +7 -7
- package/src/modules/skills/skills.contract.ts +4 -4
- package/src/modules/skills/skills.service.ts +5 -5
- package/src/modules/tool-auth/llm-usage-meter.ts +19 -19
- package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +198 -0
- package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +346 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +101 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +3 -3
- package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +27 -27
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
- package/src/modules/tool-manuals/mcp-json-discovery.ts +328 -0
- package/src/modules/tool-manuals/mcp-server-edit.service.ts +434 -0
- package/src/modules/tool-manuals/tool-manuals.contract.ts +35 -12
- package/src/modules/tool-manuals/tool-manuals.routes.ts +222 -1
- package/src/modules/tool-manuals/tool-manuals.service.ts +82 -42
- package/src/modules/tool-manuals/tool-manuals.tools.ts +6 -3
- package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +118 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/branch-name.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +13 -8
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
- package/src/modules/workflow/git/git.service.ts +2 -2
- package/src/modules/workflow/pending-commits.service.ts +48 -12
- package/src/modules/workflow/pending-commits.worker.ts +64 -8
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +1 -1
- package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
- package/src/modules/workflow/workflow-hooks.ts +101 -101
- package/src/modules/workflow/workflow.service.ts +13 -2
- package/src/modules/workspace/__tests__/kb-seed.service.test.ts +81 -13
- package/src/modules/workspace/__tests__/plugins-migration.test.ts +427 -0
- package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -237
- package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -236
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -179
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -320
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -337
- package/src/modules/workspace/__tests__/workspace.service.test.ts +116 -0
- package/src/modules/workspace/bevel-ignore.ts +66 -66
- package/src/modules/workspace/kb-seed.service.ts +38 -9
- package/src/modules/workspace/plugins-migration.ts +479 -0
- package/src/modules/workspace/session-sink.ts +25 -25
- package/src/modules/workspace/workspace.routes.ts +3 -3
- package/src/modules/workspace/workspace.service.ts +85 -14
- package/src/modules/workspace/workspace.tools.ts +922 -922
- package/src/shared/__tests__/join-request.test.ts +13 -13
- package/src/shared/__tests__/kb-layout.plugin.test.ts +45 -0
- package/src/shared/kb-layout.test.ts +3 -3
- package/src/shared/utcp-namespace.ts +10 -68
- package/src/shared/variable-refs.ts +64 -0
- package/src/modules/groups/__tests__/group-provision.service.test.ts +0 -247
- package/src/modules/groups/index.ts +0 -17
- package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
- /package/kb-template/{Groups → Plugins}/.gitkeep +0 -0
|
@@ -139,8 +139,10 @@ describe('GitService.commitFile — skipValidator', () => {
|
|
|
139
139
|
'knowledge-base',
|
|
140
140
|
);
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
// Shape mirrors the reported filename (bracketed status prefix carrying a
|
|
143
|
+
// date, underscores, spaces) without reproducing anyone's actual content.
|
|
144
|
+
const relPath = 'Handbook/[Updated 03.09.2025] Handbook_How to search_Detail page.md';
|
|
145
|
+
await fs.mkdir(path.join(repo, 'Handbook'), { recursive: true });
|
|
144
146
|
await fs.writeFile(path.join(repo, relPath), 'content\n');
|
|
145
147
|
|
|
146
148
|
const change = await svc.commitFile(workspaceId, USER, relPath);
|
|
@@ -168,15 +170,18 @@ describe('GitService.commitFile — skipValidator', () => {
|
|
|
168
170
|
'knowledge-base',
|
|
169
171
|
);
|
|
170
172
|
|
|
173
|
+
// Synthetic segments matching the SHAPE of real deep-KB naming (numbered
|
|
174
|
+
// prefixes, parenthesised abbreviations, kebab tails) — the property under
|
|
175
|
+
// test is only the total length, asserted below.
|
|
171
176
|
const deepDir = path.join(
|
|
172
|
-
'
|
|
177
|
+
'Handbook',
|
|
173
178
|
'Knowledge',
|
|
174
|
-
'1.
|
|
175
|
-
'1.1 In -
|
|
176
|
-
'0-
|
|
177
|
-
'0.5-
|
|
179
|
+
'1. First-Level Section With A Long Name',
|
|
180
|
+
'1.1 In - Second-Level Subsection (SL)',
|
|
181
|
+
'0-Reference',
|
|
182
|
+
'0.5-Third-Level-Reference-Naming-Convention',
|
|
178
183
|
);
|
|
179
|
-
const fileName = '0.5.5-
|
|
184
|
+
const fileName = '0.5.5-A-Deeply-Nested-Reference-Document-With-A-Long-Name.md';
|
|
180
185
|
const relPath = path.posix.join(deepDir.split(path.sep).join('/'), fileName);
|
|
181
186
|
expect(`Update ${relPath}`.length).toBeGreaterThan(200);
|
|
182
187
|
|
|
@@ -134,7 +134,7 @@ function fakeAccessControl(adminEmails: string[]): IAccessControl {
|
|
|
134
134
|
canOwner: async () => notImpl(),
|
|
135
135
|
invalidate: () => {},
|
|
136
136
|
findEmailByHash: async () => null,
|
|
137
|
-
kbPrincipals: async () => ({
|
|
137
|
+
kbPrincipals: async () => ({ plugins: [], people: [] }),
|
|
138
138
|
validateRolesYaml: () => ({ ok: true }),
|
|
139
139
|
referencesToRole: async () => [],
|
|
140
140
|
canWriteAtRef: async () => null,
|
|
@@ -64,7 +64,7 @@ function makeAccessControl(byRef: WritersByRefAndPath): IAccessControl {
|
|
|
64
64
|
eligibleWritersAtRef: async () => null,
|
|
65
65
|
eligibleWritersForPathsAtRef: async () => null,
|
|
66
66
|
findEmailByHash: async () => null,
|
|
67
|
-
kbPrincipals: async () => ({
|
|
67
|
+
kbPrincipals: async () => ({ plugins: [], people: [] }),
|
|
68
68
|
validateRolesYaml: () => ({ ok: true }),
|
|
69
69
|
referencesToRole: async () => [],
|
|
70
70
|
};
|
|
@@ -1047,12 +1047,12 @@ export class GitService implements IGitService {
|
|
|
1047
1047
|
// protected pushes; that's defence-in-depth against local rebases or
|
|
1048
1048
|
// `commit-tree` shenanigans that might bypass the commit-time gate.
|
|
1049
1049
|
// `systemAuthorized` skips the gate for flows whose ENDPOINT is the
|
|
1050
|
-
// authorization —
|
|
1050
|
+
// authorization — plugin provisioning commits an access.md into a
|
|
1051
1051
|
// folder that does not exist at origin yet, which this gate can only
|
|
1052
1052
|
// ever read as "write: Admin". The provisioning service has already
|
|
1053
1053
|
// decided the write is legitimate (unused name, exclusive create);
|
|
1054
1054
|
// gating it here again just refuses every non-admin the product
|
|
1055
|
-
// promised a
|
|
1055
|
+
// promised a plugin to.
|
|
1056
1056
|
if (isProtectedBranch(branch) && !opts?.systemAuthorized) {
|
|
1057
1057
|
const touched = await this.unpushedTouchedPaths(cwd);
|
|
1058
1058
|
await this.assertCanWriteAtRef(
|
|
@@ -276,18 +276,7 @@ export class PendingCommitsService {
|
|
|
276
276
|
async claimNext(rawWorkspaceId: string, now: Date): Promise<PendingCommit | null> {
|
|
277
277
|
// Match the canonical form `enqueue` stores (see `canonicalWorkspaceId`).
|
|
278
278
|
const workspaceId = canonicalWorkspaceId(rawWorkspaceId);
|
|
279
|
-
|
|
280
|
-
// of round-tripping the row to compute eligibility. Index in BACKOFF_MS
|
|
281
|
-
// matches `attempts` directly — covers `attempts == 0` (a recovery-reset
|
|
282
|
-
// row whose `lastAttemptedAt` is non-null; without this, it would fall
|
|
283
|
-
// into ELSE = max backoff and be stuck waiting 30s after every recovery).
|
|
284
|
-
const caseArms = BACKOFF_MS.map((ms, i) => sql`WHEN ${i} THEN ${ms}`);
|
|
285
|
-
const lastMs = BACKOFF_MS[BACKOFF_MS.length - 1];
|
|
286
|
-
const backoffMsExpr = sql`(CASE ${pendingCommits.attempts} ${sql.join(caseArms, sql` `)} ELSE ${lastMs} END)`;
|
|
287
|
-
const readyExpr = or(
|
|
288
|
-
isNull(pendingCommits.lastAttemptedAt),
|
|
289
|
-
sql`${pendingCommits.lastAttemptedAt} + (${backoffMsExpr} || ' milliseconds')::interval <= ${now}`,
|
|
290
|
-
);
|
|
279
|
+
const readyExpr = readyPredicate(now);
|
|
291
280
|
const claimed = await this.db
|
|
292
281
|
.update(pendingCommits)
|
|
293
282
|
.set({ status: 'running', lastAttemptedAt: now })
|
|
@@ -306,6 +295,32 @@ export class PendingCommitsService {
|
|
|
306
295
|
return claimed.length > 0 ? rowToPending(claimed[0]) : null;
|
|
307
296
|
}
|
|
308
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Whether another row is ready for `workspaceId` right now — a peek, with
|
|
300
|
+
* the same readiness rule `claimNext` uses but claiming nothing.
|
|
301
|
+
*
|
|
302
|
+
* The worker asks this AFTER claiming a row, to learn whether the one in
|
|
303
|
+
* hand is the last of a burst. Everything before the last can skip the
|
|
304
|
+
* advisory commit validator: it parses the whole KB to produce a report
|
|
305
|
+
* that is only logged, and re-parsing per file made a bulk change (a
|
|
306
|
+
* migration, an agent run) cost one full parse per commit.
|
|
307
|
+
*/
|
|
308
|
+
async hasReadyRow(rawWorkspaceId: string, now: Date): Promise<boolean> {
|
|
309
|
+
const workspaceId = canonicalWorkspaceId(rawWorkspaceId);
|
|
310
|
+
const rows = await this.db
|
|
311
|
+
.select({ id: pendingCommits.id })
|
|
312
|
+
.from(pendingCommits)
|
|
313
|
+
.where(
|
|
314
|
+
and(
|
|
315
|
+
eq(pendingCommits.workspaceId, workspaceId),
|
|
316
|
+
eq(pendingCommits.status, 'pending'),
|
|
317
|
+
readyPredicate(now),
|
|
318
|
+
),
|
|
319
|
+
)
|
|
320
|
+
.limit(1);
|
|
321
|
+
return rows.length > 0;
|
|
322
|
+
}
|
|
323
|
+
|
|
309
324
|
/**
|
|
310
325
|
* Commit + push landed. Drop the row.
|
|
311
326
|
*/
|
|
@@ -481,3 +496,24 @@ export class PendingCommitsService {
|
|
|
481
496
|
}
|
|
482
497
|
}
|
|
483
498
|
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* The "ready to attempt" gate, shared by `claimNext` and `hasReadyRow` so a
|
|
502
|
+
* peek can never disagree with the claim that follows it.
|
|
503
|
+
*
|
|
504
|
+
* Backoff is a SQL CASE so the gate runs server-side instead of round-tripping
|
|
505
|
+
* every row to compute eligibility. The index into `BACKOFF_MS` is `attempts`
|
|
506
|
+
* directly, not `attempts - 1`: that covers the recovery-reset case where
|
|
507
|
+
* `attempts` is 0 but `lastAttemptedAt` is non-null, which would otherwise
|
|
508
|
+
* fall into the default arm and idle for the maximum backoff after every
|
|
509
|
+
* recovery run.
|
|
510
|
+
*/
|
|
511
|
+
function readyPredicate(now: Date) {
|
|
512
|
+
const caseArms = BACKOFF_MS.map((ms, i) => sql`WHEN ${i} THEN ${ms}`);
|
|
513
|
+
const lastMs = BACKOFF_MS[BACKOFF_MS.length - 1];
|
|
514
|
+
const backoffMsExpr = sql`(CASE ${pendingCommits.attempts} ${sql.join(caseArms, sql` `)} ELSE ${lastMs} END)`;
|
|
515
|
+
return or(
|
|
516
|
+
isNull(pendingCommits.lastAttemptedAt),
|
|
517
|
+
sql`${pendingCommits.lastAttemptedAt} + (${backoffMsExpr} || ' milliseconds')::interval <= ${now}`,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
@@ -56,6 +56,16 @@ export const consoleSystemNoticeSink: ISystemNoticeSink = {
|
|
|
56
56
|
*/
|
|
57
57
|
const POLL_INTERVAL_MS = 500;
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Ceiling on rows drained from ONE workspace before the sweep moves on.
|
|
61
|
+
* Fairness, not throughput: the loop holds the single in-flight commit slot,
|
|
62
|
+
* so an unbounded drain would let one workspace's thousand-file migration
|
|
63
|
+
* stall every other user's save until it finished. At this size a normal
|
|
64
|
+
* bulk change (tens of files) still lands in one sweep, and a huge one gives
|
|
65
|
+
* way after a bounded stretch and resumes on the next pass.
|
|
66
|
+
*/
|
|
67
|
+
const MAX_BURST_PER_WORKSPACE = 50;
|
|
68
|
+
|
|
59
69
|
/**
|
|
60
70
|
* The recovery-agent dispatcher the worker calls when a row exhausts its
|
|
61
71
|
* transient budget. Pulled behind an interface so the unit tests can
|
|
@@ -99,6 +109,7 @@ export interface WorkflowCommitDriver {
|
|
|
99
109
|
branch: string,
|
|
100
110
|
path: string,
|
|
101
111
|
user: AuthUser,
|
|
112
|
+
opts?: { skipValidation?: boolean },
|
|
102
113
|
): Promise<void>;
|
|
103
114
|
}
|
|
104
115
|
|
|
@@ -176,18 +187,61 @@ export class PendingCommitsWorker {
|
|
|
176
187
|
}
|
|
177
188
|
|
|
178
189
|
/**
|
|
179
|
-
* Run one drain pass: rotate through known workspaces,
|
|
180
|
-
* one
|
|
181
|
-
* deterministically without driving `setTimeout`.
|
|
190
|
+
* Run one drain pass: rotate through known workspaces, draining each
|
|
191
|
+
* one's ready rows before moving on. Exposed publicly so tests can step
|
|
192
|
+
* the loop deterministically without driving `setTimeout`.
|
|
193
|
+
*
|
|
194
|
+
* Draining a BURST rather than a single row per sweep is what keeps a
|
|
195
|
+
* bulk change honest. One row per sweep, with a `POLL_INTERVAL_MS` sleep
|
|
196
|
+
* after every pass, capped a workspace at ~2 commits/second no matter how
|
|
197
|
+
* fast git was — so a migration or an agent run that touched a hundred
|
|
198
|
+
* files spent a minute of pure polling latency, and anything waiting on
|
|
199
|
+
* the queue to settle (a change request being applied) waited with it.
|
|
200
|
+
*
|
|
201
|
+
* A failing row cannot spin this loop: `markTransientFailure` leaves
|
|
202
|
+
* `lastAttemptedAt` set, so the backoff gate in `claimNext` refuses to
|
|
203
|
+
* hand the same row back within the same sweep.
|
|
182
204
|
*/
|
|
183
205
|
async drainOnce(): Promise<void> {
|
|
184
206
|
for (const workspace of this.deps.workspaces.knownWorkspaces()) {
|
|
185
207
|
// Bail mid-sweep if stop() fired — the in-flight workspace gets
|
|
186
208
|
// to finish but we don't start a new one.
|
|
187
209
|
if (!this.running) return;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
210
|
+
for (let drained = 0; drained < MAX_BURST_PER_WORKSPACE; drained += 1) {
|
|
211
|
+
if (!this.running) return;
|
|
212
|
+
const row = await this.deps.service.claimNext(workspace.id, this.deps.now());
|
|
213
|
+
if (!row) break;
|
|
214
|
+
// Is this the last commit of the burst? Everything before it can skip
|
|
215
|
+
// the advisory commit validator, which parses the whole KB to produce
|
|
216
|
+
// a report that is only logged. Running it per file made a bulk change
|
|
217
|
+
// pay one full parse per commit; running it on the last one reports
|
|
218
|
+
// the same end state once. Asked AFTER the claim, so the row in hand
|
|
219
|
+
// is already out of `pending` and cannot answer for itself.
|
|
220
|
+
//
|
|
221
|
+
// At the burst ceiling this IS the last commit of the pass, however
|
|
222
|
+
// many rows remain queued — so it must validate, or a backlog larger
|
|
223
|
+
// than the cap would end every sweep on a skipped validation. Known
|
|
224
|
+
// without asking, so the ceiling is checked BEFORE the peek rather
|
|
225
|
+
// than folded in after it: on a big backlog that would be one wasted
|
|
226
|
+
// round-trip per sweep, on the hot path this change exists to speed up.
|
|
227
|
+
let lastOfBurst = drained === MAX_BURST_PER_WORKSPACE - 1;
|
|
228
|
+
if (!lastOfBurst) {
|
|
229
|
+
// The peek is an OPTIMISATION, never a gate: the row is already
|
|
230
|
+
// claimed and `running`, and throwing here would abandon it in that
|
|
231
|
+
// state — nothing resets it, so the workspace would stop draining
|
|
232
|
+
// until the process restarted. A failed peek therefore means
|
|
233
|
+
// "assume last", costing one extra validation pass and nothing else.
|
|
234
|
+
try {
|
|
235
|
+
lastOfBurst = !(await this.deps.service.hasReadyRow(workspace.id, this.deps.now()));
|
|
236
|
+
} catch (peekErr) {
|
|
237
|
+
console.warn(
|
|
238
|
+
`[pending-commits] ready-row peek failed for ws=${workspace.id}; validating this commit: ${sanitizeError(peekErr)}`,
|
|
239
|
+
);
|
|
240
|
+
lastOfBurst = true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
await this.processRow(row, { skipValidation: !lastOfBurst });
|
|
244
|
+
}
|
|
191
245
|
}
|
|
192
246
|
}
|
|
193
247
|
|
|
@@ -226,7 +280,7 @@ export class PendingCommitsWorker {
|
|
|
226
280
|
* - throws, transient exhausted, recovery budget remains → spawn agent.
|
|
227
281
|
* - throws, recovery exhausted → markNeedsAttention + feedback notice.
|
|
228
282
|
*/
|
|
229
|
-
private async processRow(row: PendingCommit): Promise<void> {
|
|
283
|
+
private async processRow(row: PendingCommit, opts?: { skipValidation?: boolean }): Promise<void> {
|
|
230
284
|
const user: AuthUser = {
|
|
231
285
|
// The worker doesn't have a real `users.id` for the commit author —
|
|
232
286
|
// commit attribution flows through `git commit --author="Name <email>"`,
|
|
@@ -237,7 +291,9 @@ export class PendingCommitsWorker {
|
|
|
237
291
|
name: row.authorName,
|
|
238
292
|
};
|
|
239
293
|
try {
|
|
240
|
-
await this.deps.workflow.runPendingCommit(row.workspaceId, row.branch, row.path, user
|
|
294
|
+
await this.deps.workflow.runPendingCommit(row.workspaceId, row.branch, row.path, user, {
|
|
295
|
+
skipValidation: opts?.skipValidation,
|
|
296
|
+
});
|
|
241
297
|
await this.deps.service.markSucceeded(row.id);
|
|
242
298
|
return;
|
|
243
299
|
} catch (err) {
|
|
@@ -164,7 +164,7 @@ function makeAccessControl(byPath: EligibilityByPath): IAccessControl {
|
|
|
164
164
|
return result;
|
|
165
165
|
},
|
|
166
166
|
findEmailByHash: async () => null,
|
|
167
|
-
kbPrincipals: async () => ({
|
|
167
|
+
kbPrincipals: async () => ({ plugins: [], people: [] }),
|
|
168
168
|
validateRolesYaml: () => ({ ok: true }),
|
|
169
169
|
referencesToRole: async () => [],
|
|
170
170
|
};
|
|
@@ -53,7 +53,7 @@ function makeAccessControl(adminEmails: string[] = []): IAccessControl {
|
|
|
53
53
|
eligibleWritersAtRef: async () => null,
|
|
54
54
|
eligibleWritersForPathsAtRef: async () => null,
|
|
55
55
|
findEmailByHash: async () => null,
|
|
56
|
-
kbPrincipals: async () => ({
|
|
56
|
+
kbPrincipals: async () => ({ plugins: [], people: [] }),
|
|
57
57
|
validateRolesYaml: () => ({ ok: true }),
|
|
58
58
|
referencesToRole: async () => [],
|
|
59
59
|
};
|
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
import type { ValidationReport } from '@bevel-software/platform-shared';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Workflow lifecycle hooks — the generic seam that replaced the
|
|
5
|
-
* constructor-injected KB validator (and the core-owned ontology write block).
|
|
6
|
-
* Core code invokes the hooks at fixed lifecycle points; the modules that OWN
|
|
7
|
-
* the behavior (the enterprise kb/kb-graph modules today) register handlers in
|
|
8
|
-
* the composition root, right after `createCoreServices` returns. Core
|
|
9
|
-
* registers none, so a core-only deployment runs every hook point as a no-op.
|
|
10
|
-
*
|
|
11
|
-
* Two hook kinds with deliberately different failure semantics:
|
|
12
|
-
*
|
|
13
|
-
* - `commitValidation` — ADVISORY. Runs at commit time in `GitService`
|
|
14
|
-
* (`commit` / `commitFile`), exactly where the injected `IKbValidator`
|
|
15
|
-
* used to run. A returned report with `mustFix` entries is logged/surfaced
|
|
16
|
-
* but NEVER blocks the commit, and a hook that throws is caught and logged
|
|
17
|
-
* (see CLAUDE.md "Validation is advisory"). The signature mirrors the old
|
|
18
|
-
* `runValidation(workspaceId)` shape so `KbValidatorService` registers
|
|
19
|
-
* with a one-line adapter.
|
|
20
|
-
*
|
|
21
|
-
* - `preWrite` — BLOCKING. Runs before a gated agent write (file-write
|
|
22
|
-
* tools) or a write-capable shell command; a hook that throws REJECTS the
|
|
23
|
-
* operation (the error propagates to the tool caller). This is where the
|
|
24
|
-
* enterprise ontology-session block registers; core registers nothing, so
|
|
25
|
-
* core never blocks (touch TRACKING stays core — see
|
|
26
|
-
* `session-ontology.gate.ts`).
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
/** What `GitService` knows at the advisory commit-validation point. */
|
|
30
|
-
export interface CommitValidationContext {
|
|
31
|
-
workspaceId: string;
|
|
32
|
-
branch: string;
|
|
33
|
-
/**
|
|
34
|
-
* Repo-relative paths the commit will include — the full touched set for
|
|
35
|
-
* `commit()`, a single element for the one-file `commitFile()` path.
|
|
36
|
-
*/
|
|
37
|
-
paths: string[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Advisory commit-time validation. Return a report to have the caller log /
|
|
42
|
-
* surface it (or `void` for "nothing to say"). Throwing is tolerated — the
|
|
43
|
-
* caller catches and logs, never blocks the commit.
|
|
44
|
-
*/
|
|
45
|
-
export type CommitValidationHook = (
|
|
46
|
-
ctx: CommitValidationContext,
|
|
47
|
-
) => Promise<ValidationReport | void>;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* What the pre-write gate knows when it consults the blocking hooks. The core
|
|
51
|
-
* gate has already resolved all skip conditions (boundary flag off, neutral
|
|
52
|
-
* path, non-agent caller, recovery bot, missing session id — see
|
|
53
|
-
* `resolveGatedSession`), so by hook time there is always a session id.
|
|
54
|
-
*/
|
|
55
|
-
export type PreWriteContext =
|
|
56
|
-
/** A path-addressed content write (write_file/edit_file/mkdir/move/copy/unzip…). */
|
|
57
|
-
| { kind: 'file-write'; sessionId: string; wsPath: string }
|
|
58
|
-
/** A write-capable operation with no resolvable target path (execute_command). */
|
|
59
|
-
| { kind: 'shell'; sessionId: string };
|
|
60
|
-
|
|
61
|
-
/** Blocking pre-write hook: throw to reject the write; return to allow. */
|
|
62
|
-
export type PreWriteHook = (ctx: PreWriteContext) => Promise<void>;
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* The registry instance. ONE per composition (created in
|
|
66
|
-
* `createCoreServices`, shared by `GitService` and exposed as
|
|
67
|
-
* `WorkflowService.hooks` for the composition root to register against).
|
|
68
|
-
*/
|
|
69
|
-
export class WorkflowHooks {
|
|
70
|
-
private readonly commitValidation: CommitValidationHook[] = [];
|
|
71
|
-
private readonly preWrite: PreWriteHook[] = [];
|
|
72
|
-
|
|
73
|
-
/** Register an advisory commit-time validation hook. */
|
|
74
|
-
onCommitValidation(hook: CommitValidationHook): void {
|
|
75
|
-
this.commitValidation.push(hook);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Register a blocking pre-write hook. */
|
|
79
|
-
onPreWrite(hook: PreWriteHook): void {
|
|
80
|
-
this.preWrite.push(hook);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* The registered commit-validation hooks, for the caller (`GitService`) to
|
|
85
|
-
* iterate with its own advisory catch-and-log semantics.
|
|
86
|
-
*/
|
|
87
|
-
commitValidationHooks(): readonly CommitValidationHook[] {
|
|
88
|
-
return this.commitValidation;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Run every blocking pre-write hook in registration order. The first throw
|
|
93
|
-
* propagates and rejects the write; with no hooks registered (core-only)
|
|
94
|
-
* this resolves immediately.
|
|
95
|
-
*/
|
|
96
|
-
async runPreWrite(ctx: PreWriteContext): Promise<void> {
|
|
97
|
-
for (const hook of this.preWrite) {
|
|
98
|
-
await hook(ctx);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
1
|
+
import type { ValidationReport } from '@bevel-software/platform-shared';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Workflow lifecycle hooks — the generic seam that replaced the
|
|
5
|
+
* constructor-injected KB validator (and the core-owned ontology write block).
|
|
6
|
+
* Core code invokes the hooks at fixed lifecycle points; the modules that OWN
|
|
7
|
+
* the behavior (the enterprise kb/kb-graph modules today) register handlers in
|
|
8
|
+
* the composition root, right after `createCoreServices` returns. Core
|
|
9
|
+
* registers none, so a core-only deployment runs every hook point as a no-op.
|
|
10
|
+
*
|
|
11
|
+
* Two hook kinds with deliberately different failure semantics:
|
|
12
|
+
*
|
|
13
|
+
* - `commitValidation` — ADVISORY. Runs at commit time in `GitService`
|
|
14
|
+
* (`commit` / `commitFile`), exactly where the injected `IKbValidator`
|
|
15
|
+
* used to run. A returned report with `mustFix` entries is logged/surfaced
|
|
16
|
+
* but NEVER blocks the commit, and a hook that throws is caught and logged
|
|
17
|
+
* (see CLAUDE.md "Validation is advisory"). The signature mirrors the old
|
|
18
|
+
* `runValidation(workspaceId)` shape so `KbValidatorService` registers
|
|
19
|
+
* with a one-line adapter.
|
|
20
|
+
*
|
|
21
|
+
* - `preWrite` — BLOCKING. Runs before a gated agent write (file-write
|
|
22
|
+
* tools) or a write-capable shell command; a hook that throws REJECTS the
|
|
23
|
+
* operation (the error propagates to the tool caller). This is where the
|
|
24
|
+
* enterprise ontology-session block registers; core registers nothing, so
|
|
25
|
+
* core never blocks (touch TRACKING stays core — see
|
|
26
|
+
* `session-ontology.gate.ts`).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** What `GitService` knows at the advisory commit-validation point. */
|
|
30
|
+
export interface CommitValidationContext {
|
|
31
|
+
workspaceId: string;
|
|
32
|
+
branch: string;
|
|
33
|
+
/**
|
|
34
|
+
* Repo-relative paths the commit will include — the full touched set for
|
|
35
|
+
* `commit()`, a single element for the one-file `commitFile()` path.
|
|
36
|
+
*/
|
|
37
|
+
paths: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Advisory commit-time validation. Return a report to have the caller log /
|
|
42
|
+
* surface it (or `void` for "nothing to say"). Throwing is tolerated — the
|
|
43
|
+
* caller catches and logs, never blocks the commit.
|
|
44
|
+
*/
|
|
45
|
+
export type CommitValidationHook = (
|
|
46
|
+
ctx: CommitValidationContext,
|
|
47
|
+
) => Promise<ValidationReport | void>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* What the pre-write gate knows when it consults the blocking hooks. The core
|
|
51
|
+
* gate has already resolved all skip conditions (boundary flag off, neutral
|
|
52
|
+
* path, non-agent caller, recovery bot, missing session id — see
|
|
53
|
+
* `resolveGatedSession`), so by hook time there is always a session id.
|
|
54
|
+
*/
|
|
55
|
+
export type PreWriteContext =
|
|
56
|
+
/** A path-addressed content write (write_file/edit_file/mkdir/move/copy/unzip…). */
|
|
57
|
+
| { kind: 'file-write'; sessionId: string; wsPath: string }
|
|
58
|
+
/** A write-capable operation with no resolvable target path (execute_command). */
|
|
59
|
+
| { kind: 'shell'; sessionId: string };
|
|
60
|
+
|
|
61
|
+
/** Blocking pre-write hook: throw to reject the write; return to allow. */
|
|
62
|
+
export type PreWriteHook = (ctx: PreWriteContext) => Promise<void>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The registry instance. ONE per composition (created in
|
|
66
|
+
* `createCoreServices`, shared by `GitService` and exposed as
|
|
67
|
+
* `WorkflowService.hooks` for the composition root to register against).
|
|
68
|
+
*/
|
|
69
|
+
export class WorkflowHooks {
|
|
70
|
+
private readonly commitValidation: CommitValidationHook[] = [];
|
|
71
|
+
private readonly preWrite: PreWriteHook[] = [];
|
|
72
|
+
|
|
73
|
+
/** Register an advisory commit-time validation hook. */
|
|
74
|
+
onCommitValidation(hook: CommitValidationHook): void {
|
|
75
|
+
this.commitValidation.push(hook);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Register a blocking pre-write hook. */
|
|
79
|
+
onPreWrite(hook: PreWriteHook): void {
|
|
80
|
+
this.preWrite.push(hook);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The registered commit-validation hooks, for the caller (`GitService`) to
|
|
85
|
+
* iterate with its own advisory catch-and-log semantics.
|
|
86
|
+
*/
|
|
87
|
+
commitValidationHooks(): readonly CommitValidationHook[] {
|
|
88
|
+
return this.commitValidation;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Run every blocking pre-write hook in registration order. The first throw
|
|
93
|
+
* propagates and rejects the write; with no hooks registered (core-only)
|
|
94
|
+
* this resolves immediately.
|
|
95
|
+
*/
|
|
96
|
+
async runPreWrite(ctx: PreWriteContext): Promise<void> {
|
|
97
|
+
for (const hook of this.preWrite) {
|
|
98
|
+
await hook(ctx);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -808,9 +808,20 @@ export class WorkflowService implements IWorkflowService {
|
|
|
808
808
|
branch: string,
|
|
809
809
|
targetPath: string,
|
|
810
810
|
user: AuthUser,
|
|
811
|
-
opts?: { systemAuthorized?: boolean },
|
|
811
|
+
opts?: { systemAuthorized?: boolean; skipValidation?: boolean },
|
|
812
812
|
): Promise<void> {
|
|
813
|
-
|
|
813
|
+
// `skipValidation` is the worker telling us this commit is not the last of
|
|
814
|
+
// a burst. The validator is advisory — it parses the whole KB to produce a
|
|
815
|
+
// report nothing reads but the log — so running it once on the burst's
|
|
816
|
+
// final state says the same thing as running it per file, for a fraction
|
|
817
|
+
// of the work. Nothing downstream depends on it having run.
|
|
818
|
+
const change = await this.git.commitFile(
|
|
819
|
+
workspaceId,
|
|
820
|
+
user,
|
|
821
|
+
targetPath,
|
|
822
|
+
undefined,
|
|
823
|
+
opts?.skipValidation,
|
|
824
|
+
);
|
|
814
825
|
if (!change) {
|
|
815
826
|
// No-op commit — the path was already clean. Usually a double-enqueue
|
|
816
827
|
// or a save of bytes identical to HEAD, BUT a clean tree does NOT
|