@neurcode-ai/cli 0.9.61 → 0.9.62
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 +201 -0
- package/dist/commands/control-plane.d.ts +3 -0
- package/dist/commands/control-plane.d.ts.map +1 -0
- package/dist/commands/control-plane.js +163 -0
- package/dist/commands/control-plane.js.map +1 -0
- package/dist/commands/export.d.ts +7 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +72 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/fix.d.ts +1 -0
- package/dist/commands/fix.d.ts.map +1 -1
- package/dist/commands/fix.js +3 -0
- package/dist/commands/fix.js.map +1 -1
- package/dist/commands/generate.d.ts +1 -0
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +63 -48
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/patch-apply.d.ts.map +1 -1
- package/dist/commands/patch-apply.js +1 -0
- package/dist/commands/patch-apply.js.map +1 -1
- package/dist/commands/replay.d.ts +3 -0
- package/dist/commands/replay.d.ts.map +1 -0
- package/dist/commands/replay.js +267 -0
- package/dist/commands/replay.js.map +1 -0
- package/dist/commands/verify.d.ts +7 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +281 -149
- package/dist/commands/verify.js.map +1 -1
- package/dist/commands/workspace.d.ts +3 -0
- package/dist/commands/workspace.d.ts.map +1 -0
- package/dist/commands/workspace.js +407 -0
- package/dist/commands/workspace.js.map +1 -0
- package/dist/daemon/server.d.ts +0 -17
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +1038 -53
- package/dist/daemon/server.js.map +1 -1
- package/dist/index.js +296 -12
- package/dist/index.js.map +1 -1
- package/dist/utils/cli-json.d.ts +1 -0
- package/dist/utils/cli-json.d.ts.map +1 -1
- package/dist/utils/cli-json.js +1 -0
- package/dist/utils/cli-json.js.map +1 -1
- package/dist/utils/control-plane.d.ts +171 -0
- package/dist/utils/control-plane.d.ts.map +1 -0
- package/dist/utils/control-plane.js +684 -0
- package/dist/utils/control-plane.js.map +1 -0
- package/dist/utils/execution-bus.d.ts +205 -0
- package/dist/utils/execution-bus.d.ts.map +1 -0
- package/dist/utils/execution-bus.js +1346 -0
- package/dist/utils/execution-bus.js.map +1 -0
- package/dist/utils/gitignore.d.ts +2 -2
- package/dist/utils/gitignore.d.ts.map +1 -1
- package/dist/utils/gitignore.js +27 -14
- package/dist/utils/gitignore.js.map +1 -1
- package/dist/utils/replay-runtime.d.ts +295 -0
- package/dist/utils/replay-runtime.d.ts.map +1 -0
- package/dist/utils/replay-runtime.js +1080 -0
- package/dist/utils/replay-runtime.js.map +1 -0
- package/dist/utils/runtime-events.d.ts +44 -0
- package/dist/utils/runtime-events.d.ts.map +1 -0
- package/dist/utils/runtime-events.js +213 -0
- package/dist/utils/runtime-events.js.map +1 -0
- package/dist/utils/verification-evidence.d.ts +22 -0
- package/dist/utils/verification-evidence.d.ts.map +1 -0
- package/dist/utils/verification-evidence.js +233 -0
- package/dist/utils/verification-evidence.js.map +1 -0
- package/dist/utils/workspace-runtime.d.ts +267 -0
- package/dist/utils/workspace-runtime.d.ts.map +1 -0
- package/dist/utils/workspace-runtime.js +1415 -0
- package/dist/utils/workspace-runtime.js.map +1 -0
- package/package.json +7 -8
|
@@ -0,0 +1,1415 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listWorkspaces = listWorkspaces;
|
|
4
|
+
exports.getWorkspaceById = getWorkspaceById;
|
|
5
|
+
exports.getActiveWorkspace = getActiveWorkspace;
|
|
6
|
+
exports.createWorkspace = createWorkspace;
|
|
7
|
+
exports.setActiveWorkspace = setActiveWorkspace;
|
|
8
|
+
exports.addWorkspaceRepository = addWorkspaceRepository;
|
|
9
|
+
exports.updateWorkspace = updateWorkspace;
|
|
10
|
+
exports.getWorkspaceRuntimeSnapshot = getWorkspaceRuntimeSnapshot;
|
|
11
|
+
exports.executeWorkspaceAction = executeWorkspaceAction;
|
|
12
|
+
const crypto_1 = require("crypto");
|
|
13
|
+
const fs_1 = require("fs");
|
|
14
|
+
const path_1 = require("path");
|
|
15
|
+
const execution_bus_1 = require("./execution-bus");
|
|
16
|
+
const control_plane_1 = require("./control-plane");
|
|
17
|
+
const runtime_events_1 = require("./runtime-events");
|
|
18
|
+
const replay_runtime_1 = require("./replay-runtime");
|
|
19
|
+
const WORKSPACE_SCHEMA = 'neurcode.workspace.v1';
|
|
20
|
+
const WORKSPACE_INDEX_SCHEMA = 'neurcode.workspaces.index.v1';
|
|
21
|
+
const WORKSPACE_RUNTIME_SCHEMA = 'neurcode.workspace-runtime.v1';
|
|
22
|
+
const DEFAULT_WORKSPACES_DIR = '.neurcode/workspaces';
|
|
23
|
+
const DEFAULT_EVIDENCE_LIMIT = 150;
|
|
24
|
+
const DEFAULT_EXECUTION_LIMIT = 200;
|
|
25
|
+
const DEFAULT_EVENT_LIMIT = 500;
|
|
26
|
+
const repoScanCache = new Map();
|
|
27
|
+
function nowIso() {
|
|
28
|
+
return new Date().toISOString();
|
|
29
|
+
}
|
|
30
|
+
function asObject(value) {
|
|
31
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
32
|
+
return null;
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function asString(value) {
|
|
36
|
+
if (typeof value !== 'string')
|
|
37
|
+
return null;
|
|
38
|
+
const trimmed = value.trim();
|
|
39
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
40
|
+
}
|
|
41
|
+
function asBoolean(value, fallback = false) {
|
|
42
|
+
if (typeof value === 'boolean')
|
|
43
|
+
return value;
|
|
44
|
+
if (typeof value === 'string') {
|
|
45
|
+
const normalized = value.trim().toLowerCase();
|
|
46
|
+
if (normalized === 'true' || normalized === '1' || normalized === 'yes')
|
|
47
|
+
return true;
|
|
48
|
+
if (normalized === 'false' || normalized === '0' || normalized === 'no')
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return fallback;
|
|
52
|
+
}
|
|
53
|
+
function asNumber(value) {
|
|
54
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
55
|
+
return value;
|
|
56
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
57
|
+
const parsed = Number(value);
|
|
58
|
+
if (Number.isFinite(parsed))
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function clampInt(value, fallback, min, max) {
|
|
64
|
+
const parsed = asNumber(value);
|
|
65
|
+
if (parsed === null)
|
|
66
|
+
return fallback;
|
|
67
|
+
return Math.max(min, Math.min(max, Math.floor(parsed)));
|
|
68
|
+
}
|
|
69
|
+
function normalizeList(values) {
|
|
70
|
+
if (!Array.isArray(values))
|
|
71
|
+
return [];
|
|
72
|
+
return Array.from(new Set(values
|
|
73
|
+
.map((entry) => asString(entry))
|
|
74
|
+
.filter((entry) => Boolean(entry)))).sort((left, right) => left.localeCompare(right));
|
|
75
|
+
}
|
|
76
|
+
function slugify(value) {
|
|
77
|
+
const slug = value
|
|
78
|
+
.toLowerCase()
|
|
79
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
80
|
+
.replace(/^-+|-+$/g, '');
|
|
81
|
+
return slug.length > 0 ? slug : 'workspace';
|
|
82
|
+
}
|
|
83
|
+
function clampPercent(value) {
|
|
84
|
+
return Math.max(0, Math.min(100, Math.round(value * 100) / 100));
|
|
85
|
+
}
|
|
86
|
+
function insideRoot(rootDir, target) {
|
|
87
|
+
const normalizedRoot = rootDir.endsWith(path_1.sep) ? rootDir : `${rootDir}${path_1.sep}`;
|
|
88
|
+
return target === rootDir || target.startsWith(normalizedRoot);
|
|
89
|
+
}
|
|
90
|
+
function resolveRepoRoot(startDir) {
|
|
91
|
+
let current = (0, path_1.resolve)(startDir);
|
|
92
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
93
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(current, '.git')) || (0, fs_1.existsSync)((0, path_1.join)(current, 'pnpm-workspace.yaml'))) {
|
|
94
|
+
return current;
|
|
95
|
+
}
|
|
96
|
+
const parent = (0, path_1.dirname)(current);
|
|
97
|
+
if (parent === current)
|
|
98
|
+
break;
|
|
99
|
+
current = parent;
|
|
100
|
+
}
|
|
101
|
+
return (0, path_1.resolve)(startDir);
|
|
102
|
+
}
|
|
103
|
+
function resolveWorkspaceRoot(cwd) {
|
|
104
|
+
return resolveRepoRoot(cwd);
|
|
105
|
+
}
|
|
106
|
+
function toWorkspacePaths(rootDir) {
|
|
107
|
+
const workspacesDir = (0, path_1.resolve)(rootDir, DEFAULT_WORKSPACES_DIR);
|
|
108
|
+
const indexFile = (0, path_1.join)(workspacesDir, 'index.json');
|
|
109
|
+
(0, fs_1.mkdirSync)(workspacesDir, { recursive: true });
|
|
110
|
+
(0, fs_1.mkdirSync)((0, path_1.join)(workspacesDir, 'definitions'), { recursive: true });
|
|
111
|
+
return {
|
|
112
|
+
rootDir,
|
|
113
|
+
workspacesDir,
|
|
114
|
+
indexFile,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function workspaceDefinitionFile(paths, workspaceId) {
|
|
118
|
+
return (0, path_1.join)(paths.workspacesDir, 'definitions', `${workspaceId}.json`);
|
|
119
|
+
}
|
|
120
|
+
function defaultWorkspaceIndex() {
|
|
121
|
+
return {
|
|
122
|
+
schemaVersion: WORKSPACE_INDEX_SCHEMA,
|
|
123
|
+
activeWorkspaceId: null,
|
|
124
|
+
updatedAt: nowIso(),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function readWorkspaceIndex(paths) {
|
|
128
|
+
if (!(0, fs_1.existsSync)(paths.indexFile)) {
|
|
129
|
+
return defaultWorkspaceIndex();
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(paths.indexFile, 'utf-8'));
|
|
133
|
+
const record = asObject(parsed);
|
|
134
|
+
if (!record || record.schemaVersion !== WORKSPACE_INDEX_SCHEMA) {
|
|
135
|
+
return defaultWorkspaceIndex();
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
schemaVersion: WORKSPACE_INDEX_SCHEMA,
|
|
139
|
+
activeWorkspaceId: asString(record.activeWorkspaceId),
|
|
140
|
+
updatedAt: asString(record.updatedAt) || nowIso(),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return defaultWorkspaceIndex();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function writeWorkspaceIndex(paths, index) {
|
|
148
|
+
(0, fs_1.writeFileSync)(paths.indexFile, `${JSON.stringify(index, null, 2)}\n`, 'utf-8');
|
|
149
|
+
}
|
|
150
|
+
function normalizeRepository(rootDir, raw, fallbackIdSeed) {
|
|
151
|
+
const displayName = asString(raw.name) || (0, path_1.basename)(asString(raw.rootPath) || fallbackIdSeed) || fallbackIdSeed;
|
|
152
|
+
const id = slugify(asString(raw.id) || displayName || fallbackIdSeed);
|
|
153
|
+
const rootInput = asString(raw.rootPath) || '.';
|
|
154
|
+
const resolved = (0, path_1.isAbsolute)(rootInput) ? (0, path_1.resolve)(rootInput) : (0, path_1.resolve)(rootDir, rootInput);
|
|
155
|
+
const safeResolved = insideRoot(rootDir, resolved) ? resolved : rootDir;
|
|
156
|
+
const relativePath = (0, path_1.relative)(rootDir, safeResolved).replace(/\\/g, '/');
|
|
157
|
+
const normalizedPath = relativePath.length > 0 ? relativePath : '.';
|
|
158
|
+
return {
|
|
159
|
+
id,
|
|
160
|
+
name: displayName,
|
|
161
|
+
rootPath: normalizedPath,
|
|
162
|
+
services: normalizeList(raw.services),
|
|
163
|
+
policyDomain: asString(raw.policyDomain),
|
|
164
|
+
tags: normalizeList(raw.tags),
|
|
165
|
+
enabled: asBoolean(raw.enabled, true),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function defaultGovernance() {
|
|
169
|
+
return {
|
|
170
|
+
posture: {
|
|
171
|
+
targetRisk: 'medium',
|
|
172
|
+
enforcement: 'strict',
|
|
173
|
+
notes: null,
|
|
174
|
+
},
|
|
175
|
+
controlPlane: {
|
|
176
|
+
inherit: true,
|
|
177
|
+
overrides: {},
|
|
178
|
+
},
|
|
179
|
+
policy: {
|
|
180
|
+
workspacePacks: [],
|
|
181
|
+
repositoryPackOverrides: {},
|
|
182
|
+
precedence: 'workspace-first',
|
|
183
|
+
},
|
|
184
|
+
evidence: {
|
|
185
|
+
retentionMaxArtifacts: 50,
|
|
186
|
+
indexLimit: 200,
|
|
187
|
+
},
|
|
188
|
+
remediation: {
|
|
189
|
+
autonomousApplySafe: true,
|
|
190
|
+
requireManualApprovalAtRisk: 'high',
|
|
191
|
+
},
|
|
192
|
+
runtime: {
|
|
193
|
+
executionRetention: 250,
|
|
194
|
+
eventRetention: 5000,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function normalizeGovernance(raw) {
|
|
199
|
+
const defaults = defaultGovernance();
|
|
200
|
+
const record = asObject(raw) || {};
|
|
201
|
+
const posture = asObject(record.posture) || {};
|
|
202
|
+
const controlPlane = asObject(record.controlPlane) || {};
|
|
203
|
+
const policy = asObject(record.policy) || {};
|
|
204
|
+
const evidence = asObject(record.evidence) || {};
|
|
205
|
+
const remediation = asObject(record.remediation) || {};
|
|
206
|
+
const runtime = asObject(record.runtime) || {};
|
|
207
|
+
const targetRisk = asString(posture.targetRisk);
|
|
208
|
+
const enforcement = asString(posture.enforcement);
|
|
209
|
+
const approvalRisk = asString(remediation.requireManualApprovalAtRisk);
|
|
210
|
+
const precedence = asString(policy.precedence);
|
|
211
|
+
const overrides = asObject(controlPlane.overrides) || {};
|
|
212
|
+
const repoOverrides = {};
|
|
213
|
+
const rawRepoOverrides = asObject(policy.repositoryPackOverrides) || {};
|
|
214
|
+
for (const [repoId, packs] of Object.entries(rawRepoOverrides)) {
|
|
215
|
+
repoOverrides[slugify(repoId)] = normalizeList(packs);
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
posture: {
|
|
219
|
+
targetRisk: targetRisk === 'low' || targetRisk === 'high' ? targetRisk : defaults.posture.targetRisk,
|
|
220
|
+
enforcement: enforcement === 'advisory' || enforcement === 'balanced' ? enforcement : defaults.posture.enforcement,
|
|
221
|
+
notes: asString(posture.notes),
|
|
222
|
+
},
|
|
223
|
+
controlPlane: {
|
|
224
|
+
inherit: asBoolean(controlPlane.inherit, defaults.controlPlane.inherit),
|
|
225
|
+
overrides: {
|
|
226
|
+
...(asObject(overrides.runtime) ? { runtime: asObject(overrides.runtime) || {} } : {}),
|
|
227
|
+
...(asObject(overrides.remediation) ? { remediation: asObject(overrides.remediation) || {} } : {}),
|
|
228
|
+
...(asObject(overrides.evidence) ? { evidence: asObject(overrides.evidence) || {} } : {}),
|
|
229
|
+
...(asObject(overrides.eventRuntime) ? { eventRuntime: asObject(overrides.eventRuntime) || {} } : {}),
|
|
230
|
+
...(asObject(overrides.ciGovernance) ? { ciGovernance: asObject(overrides.ciGovernance) || {} } : {}),
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
policy: {
|
|
234
|
+
workspacePacks: normalizeList(policy.workspacePacks),
|
|
235
|
+
repositoryPackOverrides: repoOverrides,
|
|
236
|
+
precedence: precedence === 'repo-first' ? 'repo-first' : defaults.policy.precedence,
|
|
237
|
+
},
|
|
238
|
+
evidence: {
|
|
239
|
+
retentionMaxArtifacts: clampInt(evidence.retentionMaxArtifacts, defaults.evidence.retentionMaxArtifacts, 10, 2000),
|
|
240
|
+
indexLimit: clampInt(evidence.indexLimit, defaults.evidence.indexLimit, 25, 3000),
|
|
241
|
+
},
|
|
242
|
+
remediation: {
|
|
243
|
+
autonomousApplySafe: asBoolean(remediation.autonomousApplySafe, defaults.remediation.autonomousApplySafe),
|
|
244
|
+
requireManualApprovalAtRisk: approvalRisk === 'none' || approvalRisk === 'critical'
|
|
245
|
+
? approvalRisk
|
|
246
|
+
: defaults.remediation.requireManualApprovalAtRisk,
|
|
247
|
+
},
|
|
248
|
+
runtime: {
|
|
249
|
+
executionRetention: clampInt(runtime.executionRetention, defaults.runtime.executionRetention, 25, 5000),
|
|
250
|
+
eventRetention: clampInt(runtime.eventRetention, defaults.runtime.eventRetention, 500, 100000),
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function normalizeAccess(raw) {
|
|
255
|
+
const record = asObject(raw) || {};
|
|
256
|
+
const membersRaw = Array.isArray(record.members) ? record.members : [];
|
|
257
|
+
const members = [];
|
|
258
|
+
const seen = new Set();
|
|
259
|
+
for (const entry of membersRaw) {
|
|
260
|
+
const item = asObject(entry);
|
|
261
|
+
if (!item)
|
|
262
|
+
continue;
|
|
263
|
+
const actor = asString(item.actor);
|
|
264
|
+
const role = asString(item.role);
|
|
265
|
+
if (!actor)
|
|
266
|
+
continue;
|
|
267
|
+
const normalizedRole = role === 'workspace_admin' || role === 'governance_admin' || role === 'engineer' || role === 'auditor'
|
|
268
|
+
? role
|
|
269
|
+
: 'engineer';
|
|
270
|
+
const key = `${actor.toLowerCase()}::${normalizedRole}`;
|
|
271
|
+
if (seen.has(key))
|
|
272
|
+
continue;
|
|
273
|
+
seen.add(key);
|
|
274
|
+
members.push({ actor, role: normalizedRole });
|
|
275
|
+
}
|
|
276
|
+
members.sort((left, right) => left.actor.localeCompare(right.actor));
|
|
277
|
+
const defaultRoleRaw = asString(record.defaultRole);
|
|
278
|
+
const defaultRole = defaultRoleRaw === 'workspace_admin'
|
|
279
|
+
|| defaultRoleRaw === 'governance_admin'
|
|
280
|
+
|| defaultRoleRaw === 'engineer'
|
|
281
|
+
|| defaultRoleRaw === 'auditor'
|
|
282
|
+
? defaultRoleRaw
|
|
283
|
+
: 'engineer';
|
|
284
|
+
return {
|
|
285
|
+
defaultRole,
|
|
286
|
+
members,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function normalizeWorkspaceDefinition(rootDir, raw, fallbackId) {
|
|
290
|
+
const record = asObject(raw) || {};
|
|
291
|
+
const name = asString(record.name) || 'Workspace';
|
|
292
|
+
const id = slugify(asString(record.id) || fallbackId || name);
|
|
293
|
+
const createdAt = asString(record.createdAt) || nowIso();
|
|
294
|
+
const updatedAt = asString(record.updatedAt) || nowIso();
|
|
295
|
+
const repositoriesRaw = Array.isArray(record.repositories) ? record.repositories : [];
|
|
296
|
+
const repositories = repositoriesRaw
|
|
297
|
+
.map((entry, idx) => normalizeRepository(rootDir, (asObject(entry) || {}), `${id}-repo-${idx + 1}`))
|
|
298
|
+
.reduce((acc, repo) => {
|
|
299
|
+
if (acc.some((existing) => existing.id === repo.id))
|
|
300
|
+
return acc;
|
|
301
|
+
acc.push(repo);
|
|
302
|
+
return acc;
|
|
303
|
+
}, [])
|
|
304
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
305
|
+
return {
|
|
306
|
+
schemaVersion: WORKSPACE_SCHEMA,
|
|
307
|
+
id,
|
|
308
|
+
name,
|
|
309
|
+
description: asString(record.description),
|
|
310
|
+
createdAt,
|
|
311
|
+
updatedAt,
|
|
312
|
+
repositories,
|
|
313
|
+
governance: normalizeGovernance(record.governance),
|
|
314
|
+
access: normalizeAccess(record.access),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function writeWorkspaceDefinition(paths, workspace) {
|
|
318
|
+
const filePath = workspaceDefinitionFile(paths, workspace.id);
|
|
319
|
+
(0, fs_1.writeFileSync)(filePath, `${JSON.stringify(workspace, null, 2)}\n`, 'utf-8');
|
|
320
|
+
}
|
|
321
|
+
function loadWorkspaceDefinition(paths, fileName) {
|
|
322
|
+
const filePath = (0, path_1.join)(paths.workspacesDir, 'definitions', fileName);
|
|
323
|
+
try {
|
|
324
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(filePath, 'utf-8'));
|
|
325
|
+
const fallbackId = (0, path_1.basename)(fileName, '.json');
|
|
326
|
+
return normalizeWorkspaceDefinition(paths.rootDir, parsed, fallbackId);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function listWorkspaceDefinitionsInternal(paths) {
|
|
333
|
+
const defsDir = (0, path_1.join)(paths.workspacesDir, 'definitions');
|
|
334
|
+
if (!(0, fs_1.existsSync)(defsDir))
|
|
335
|
+
return [];
|
|
336
|
+
const files = (0, fs_1.readdirSync)(defsDir)
|
|
337
|
+
.filter((name) => name.endsWith('.json'))
|
|
338
|
+
.sort((left, right) => left.localeCompare(right));
|
|
339
|
+
const workspaces = [];
|
|
340
|
+
for (const fileName of files) {
|
|
341
|
+
const workspace = loadWorkspaceDefinition(paths, fileName);
|
|
342
|
+
if (!workspace)
|
|
343
|
+
continue;
|
|
344
|
+
workspaces.push(workspace);
|
|
345
|
+
}
|
|
346
|
+
return workspaces;
|
|
347
|
+
}
|
|
348
|
+
function emitWorkspaceMutationEvent(rootDir, workspace, source, actor, action, payload) {
|
|
349
|
+
const synthetic = (0, execution_bus_1.recordSyntheticExecution)({
|
|
350
|
+
cwd: rootDir,
|
|
351
|
+
type: 'policy-sync',
|
|
352
|
+
source,
|
|
353
|
+
actor,
|
|
354
|
+
status: 'completed',
|
|
355
|
+
success: true,
|
|
356
|
+
message: `${action} workspace ${workspace.id}`,
|
|
357
|
+
payload: {
|
|
358
|
+
action,
|
|
359
|
+
workspaceId: workspace.id,
|
|
360
|
+
workspaceName: workspace.name,
|
|
361
|
+
...(payload || {}),
|
|
362
|
+
},
|
|
363
|
+
narrative: {
|
|
364
|
+
status: 'success',
|
|
365
|
+
summary: `Workspace ${workspace.name} ${action}`,
|
|
366
|
+
why: 'Workspace runtime orchestration updated deterministically.',
|
|
367
|
+
riskLevel: 'low',
|
|
368
|
+
recommendedAction: 'Run workspace posture to validate cross-repo governance health.',
|
|
369
|
+
expectedImprovement: 'Workspace-level governance state remains inspectable and replayable.',
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
try {
|
|
373
|
+
(0, runtime_events_1.emitRuntimeEvent)(rootDir, {
|
|
374
|
+
type: 'governance.config.updated',
|
|
375
|
+
executionId: synthetic.id,
|
|
376
|
+
source,
|
|
377
|
+
actor,
|
|
378
|
+
severity: 'low',
|
|
379
|
+
payload: {
|
|
380
|
+
action: `workspace.${action}`,
|
|
381
|
+
workspaceId: workspace.id,
|
|
382
|
+
workspaceName: workspace.name,
|
|
383
|
+
...(payload || {}),
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
// Best-effort event emission only.
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
const paths = toWorkspacePaths(rootDir);
|
|
392
|
+
const index = readWorkspaceIndex(paths);
|
|
393
|
+
(0, replay_runtime_1.writeWorkspaceReplaySnapshot)({
|
|
394
|
+
cwd: rootDir,
|
|
395
|
+
workspaceId: workspace.id,
|
|
396
|
+
workspaceName: workspace.name,
|
|
397
|
+
workspace: JSON.parse(JSON.stringify(workspace)),
|
|
398
|
+
posture: null,
|
|
399
|
+
source,
|
|
400
|
+
actor,
|
|
401
|
+
action: `workspace.${action}`,
|
|
402
|
+
executionId: synthetic.id,
|
|
403
|
+
activeWorkspaceId: index.activeWorkspaceId,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
// Snapshot persistence is best-effort.
|
|
408
|
+
}
|
|
409
|
+
return synthetic.id;
|
|
410
|
+
}
|
|
411
|
+
function cloneRecord(value) {
|
|
412
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
413
|
+
return {};
|
|
414
|
+
return JSON.parse(JSON.stringify(value));
|
|
415
|
+
}
|
|
416
|
+
function mergeRecords(base, patch) {
|
|
417
|
+
const merged = { ...base };
|
|
418
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
419
|
+
if (Array.isArray(value)) {
|
|
420
|
+
merged[key] = [...value];
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (value && typeof value === 'object') {
|
|
424
|
+
const baseValue = merged[key];
|
|
425
|
+
const nextBase = baseValue && typeof baseValue === 'object' && !Array.isArray(baseValue)
|
|
426
|
+
? baseValue
|
|
427
|
+
: {};
|
|
428
|
+
merged[key] = mergeRecords(nextBase, value);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
merged[key] = value;
|
|
432
|
+
}
|
|
433
|
+
return merged;
|
|
434
|
+
}
|
|
435
|
+
function resolveEffectiveControlPlane(workspace, rootDir) {
|
|
436
|
+
const controlPlane = (0, control_plane_1.readControlPlaneState)(rootDir);
|
|
437
|
+
const overrides = workspace.governance.controlPlane.overrides;
|
|
438
|
+
const inherited = workspace.governance.controlPlane.inherit;
|
|
439
|
+
const overrideKeys = [];
|
|
440
|
+
if (overrides.runtime && Object.keys(overrides.runtime).length > 0)
|
|
441
|
+
overrideKeys.push('runtime');
|
|
442
|
+
if (overrides.remediation && Object.keys(overrides.remediation).length > 0)
|
|
443
|
+
overrideKeys.push('remediation');
|
|
444
|
+
if (overrides.evidence && Object.keys(overrides.evidence).length > 0)
|
|
445
|
+
overrideKeys.push('evidence');
|
|
446
|
+
if (overrides.eventRuntime && Object.keys(overrides.eventRuntime).length > 0)
|
|
447
|
+
overrideKeys.push('eventRuntime');
|
|
448
|
+
if (overrides.ciGovernance && Object.keys(overrides.ciGovernance).length > 0)
|
|
449
|
+
overrideKeys.push('ciGovernance');
|
|
450
|
+
const baseRuntime = cloneRecord(controlPlane.runtime);
|
|
451
|
+
const baseRemediation = cloneRecord(controlPlane.remediation);
|
|
452
|
+
const baseEvidence = cloneRecord(controlPlane.evidence);
|
|
453
|
+
const baseEventRuntime = cloneRecord(controlPlane.eventRuntime);
|
|
454
|
+
const baseCiGovernance = cloneRecord(controlPlane.ciGovernance);
|
|
455
|
+
return {
|
|
456
|
+
inherited,
|
|
457
|
+
overridesApplied: overrideKeys,
|
|
458
|
+
runtime: inherited
|
|
459
|
+
? mergeRecords(baseRuntime, cloneRecord(overrides.runtime))
|
|
460
|
+
: mergeRecords(baseRuntime, cloneRecord(overrides.runtime)),
|
|
461
|
+
remediation: inherited
|
|
462
|
+
? mergeRecords(baseRemediation, cloneRecord(overrides.remediation))
|
|
463
|
+
: mergeRecords(baseRemediation, cloneRecord(overrides.remediation)),
|
|
464
|
+
evidence: inherited
|
|
465
|
+
? mergeRecords(baseEvidence, cloneRecord(overrides.evidence))
|
|
466
|
+
: mergeRecords(baseEvidence, cloneRecord(overrides.evidence)),
|
|
467
|
+
eventRuntime: inherited
|
|
468
|
+
? mergeRecords(baseEventRuntime, cloneRecord(overrides.eventRuntime))
|
|
469
|
+
: mergeRecords(baseEventRuntime, cloneRecord(overrides.eventRuntime)),
|
|
470
|
+
ciGovernance: inherited
|
|
471
|
+
? mergeRecords(baseCiGovernance, cloneRecord(overrides.ciGovernance))
|
|
472
|
+
: mergeRecords(baseCiGovernance, cloneRecord(overrides.ciGovernance)),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function pruneFilesByLimit(dirPath, filter, limit) {
|
|
476
|
+
if (!(0, fs_1.existsSync)(dirPath))
|
|
477
|
+
return;
|
|
478
|
+
const max = Math.max(1, Math.floor(limit));
|
|
479
|
+
if (max <= 0)
|
|
480
|
+
return;
|
|
481
|
+
const names = (0, fs_1.readdirSync)(dirPath)
|
|
482
|
+
.filter(filter)
|
|
483
|
+
.sort()
|
|
484
|
+
.reverse();
|
|
485
|
+
if (names.length <= max)
|
|
486
|
+
return;
|
|
487
|
+
for (const name of names.slice(max)) {
|
|
488
|
+
try {
|
|
489
|
+
(0, fs_1.unlinkSync)((0, path_1.join)(dirPath, name));
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
// best effort cleanup
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function pruneJsonlByLines(filePath, maxLines) {
|
|
497
|
+
if (!(0, fs_1.existsSync)(filePath))
|
|
498
|
+
return;
|
|
499
|
+
const max = Math.max(100, Math.floor(maxLines));
|
|
500
|
+
try {
|
|
501
|
+
const raw = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
502
|
+
const lines = raw
|
|
503
|
+
.split('\n')
|
|
504
|
+
.map((line) => line.trim())
|
|
505
|
+
.filter(Boolean);
|
|
506
|
+
if (lines.length <= max)
|
|
507
|
+
return;
|
|
508
|
+
const next = `${lines.slice(lines.length - max).join('\n')}\n`;
|
|
509
|
+
(0, fs_1.writeFileSync)(filePath, next, 'utf-8');
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
// best effort cleanup
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function enforceWorkspaceRetention(workspace, repoRoot) {
|
|
516
|
+
const evidenceLimit = workspace.governance.evidence.retentionMaxArtifacts;
|
|
517
|
+
const executionLimit = workspace.governance.runtime.executionRetention;
|
|
518
|
+
const eventLimit = workspace.governance.runtime.eventRetention;
|
|
519
|
+
pruneFilesByLimit((0, path_1.join)(repoRoot, '.neurcode/evidence'), (name) => name.startsWith('verification-') && name.endsWith('.json'), evidenceLimit);
|
|
520
|
+
pruneFilesByLimit((0, path_1.join)(repoRoot, '.neurcode/executions/records'), (name) => name.startsWith('execution-') && name.endsWith('.json'), executionLimit);
|
|
521
|
+
pruneJsonlByLines((0, path_1.join)(repoRoot, '.neurcode/runtime-events/events.jsonl'), eventLimit);
|
|
522
|
+
}
|
|
523
|
+
function readLatestJsonLines(pathValue, limit) {
|
|
524
|
+
if (!(0, fs_1.existsSync)(pathValue))
|
|
525
|
+
return [];
|
|
526
|
+
try {
|
|
527
|
+
const raw = (0, fs_1.readFileSync)(pathValue, 'utf-8');
|
|
528
|
+
const lines = raw
|
|
529
|
+
.split('\n')
|
|
530
|
+
.map((line) => line.trim())
|
|
531
|
+
.filter(Boolean);
|
|
532
|
+
const slice = lines.slice(Math.max(0, lines.length - limit));
|
|
533
|
+
const parsed = [];
|
|
534
|
+
for (const line of slice) {
|
|
535
|
+
try {
|
|
536
|
+
const value = JSON.parse(line);
|
|
537
|
+
const record = asObject(value);
|
|
538
|
+
if (record)
|
|
539
|
+
parsed.push(record);
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
// ignore invalid line
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return parsed;
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
return [];
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function evidenceFiles(repoRoot) {
|
|
552
|
+
const dir = (0, path_1.join)(repoRoot, '.neurcode/evidence');
|
|
553
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
554
|
+
return [];
|
|
555
|
+
return (0, fs_1.readdirSync)(dir)
|
|
556
|
+
.filter((name) => name.startsWith('verification-') && name.endsWith('.json'))
|
|
557
|
+
.sort()
|
|
558
|
+
.reverse();
|
|
559
|
+
}
|
|
560
|
+
function executionFiles(repoRoot) {
|
|
561
|
+
const dir = (0, path_1.join)(repoRoot, '.neurcode/executions/records');
|
|
562
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
563
|
+
return [];
|
|
564
|
+
return (0, fs_1.readdirSync)(dir)
|
|
565
|
+
.filter((name) => name.startsWith('execution-') && name.endsWith('.json'))
|
|
566
|
+
.sort()
|
|
567
|
+
.reverse();
|
|
568
|
+
}
|
|
569
|
+
function eventFile(repoRoot) {
|
|
570
|
+
return (0, path_1.join)(repoRoot, '.neurcode/runtime-events/events.jsonl');
|
|
571
|
+
}
|
|
572
|
+
function makeRepoFingerprint(repoRoot, evidenceLimit, executionLimit) {
|
|
573
|
+
const evidenceDir = (0, path_1.join)(repoRoot, '.neurcode/evidence');
|
|
574
|
+
const executionsDir = (0, path_1.join)(repoRoot, '.neurcode/executions/records');
|
|
575
|
+
const eventsPath = eventFile(repoRoot);
|
|
576
|
+
const parts = [repoRoot];
|
|
577
|
+
if ((0, fs_1.existsSync)(evidenceDir)) {
|
|
578
|
+
for (const fileName of evidenceFiles(repoRoot).slice(0, evidenceLimit)) {
|
|
579
|
+
const abs = (0, path_1.join)(evidenceDir, fileName);
|
|
580
|
+
try {
|
|
581
|
+
const stat = (0, fs_1.statSync)(abs);
|
|
582
|
+
parts.push(`e:${fileName}:${stat.size}:${Math.floor(stat.mtimeMs)}`);
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
// ignore
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if ((0, fs_1.existsSync)(executionsDir)) {
|
|
590
|
+
for (const fileName of executionFiles(repoRoot).slice(0, executionLimit)) {
|
|
591
|
+
const abs = (0, path_1.join)(executionsDir, fileName);
|
|
592
|
+
try {
|
|
593
|
+
const stat = (0, fs_1.statSync)(abs);
|
|
594
|
+
parts.push(`x:${fileName}:${stat.size}:${Math.floor(stat.mtimeMs)}`);
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
// ignore
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if ((0, fs_1.existsSync)(eventsPath)) {
|
|
602
|
+
try {
|
|
603
|
+
const stat = (0, fs_1.statSync)(eventsPath);
|
|
604
|
+
parts.push(`r:${stat.size}:${Math.floor(stat.mtimeMs)}`);
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
// ignore
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return (0, crypto_1.createHash)('sha256').update(parts.join('|'), 'utf-8').digest('hex');
|
|
611
|
+
}
|
|
612
|
+
function computeRiskLevel(score) {
|
|
613
|
+
if (score >= 70)
|
|
614
|
+
return 'high';
|
|
615
|
+
if (score >= 40)
|
|
616
|
+
return 'medium';
|
|
617
|
+
return 'low';
|
|
618
|
+
}
|
|
619
|
+
function computeRepositoryHealth(workspaceId, repository, workspace, rootDir) {
|
|
620
|
+
const repoRoot = (0, path_1.resolve)(rootDir, repository.rootPath);
|
|
621
|
+
const exists = insideRoot(rootDir, repoRoot) && (0, fs_1.existsSync)(repoRoot);
|
|
622
|
+
const fingerprint = makeRepoFingerprint(repoRoot, workspace.governance.evidence.indexLimit, DEFAULT_EXECUTION_LIMIT);
|
|
623
|
+
const cacheKey = `${workspaceId}:${repository.id}`;
|
|
624
|
+
const cached = repoScanCache.get(cacheKey);
|
|
625
|
+
if (cached && cached.fingerprint === fingerprint) {
|
|
626
|
+
return {
|
|
627
|
+
health: cached.health,
|
|
628
|
+
recentEvents: cached.events,
|
|
629
|
+
eventCounts: cached.eventCounts,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
const policyPacks = new Set(workspace.governance.policy.workspacePacks);
|
|
633
|
+
const overridePacks = workspace.governance.policy.repositoryPackOverrides[repository.id] || [];
|
|
634
|
+
if (workspace.governance.policy.precedence === 'repo-first' && overridePacks.length > 0) {
|
|
635
|
+
for (const pack of overridePacks)
|
|
636
|
+
policyPacks.add(pack);
|
|
637
|
+
}
|
|
638
|
+
let runs = 0;
|
|
639
|
+
let passRuns = 0;
|
|
640
|
+
let failRuns = 0;
|
|
641
|
+
let totalBlocking = 0;
|
|
642
|
+
let totalAdvisory = 0;
|
|
643
|
+
let regressionRuns = 0;
|
|
644
|
+
let coverageAccum = 0;
|
|
645
|
+
let coverageCount = 0;
|
|
646
|
+
let lastRunAt = null;
|
|
647
|
+
const policyCounts = new Map();
|
|
648
|
+
const fileCounts = new Map();
|
|
649
|
+
const directoryCounts = new Map();
|
|
650
|
+
if (exists) {
|
|
651
|
+
const evidenceDir = (0, path_1.join)(repoRoot, '.neurcode/evidence');
|
|
652
|
+
const evidenceNames = evidenceFiles(repoRoot).slice(0, workspace.governance.evidence.indexLimit);
|
|
653
|
+
for (const fileName of evidenceNames) {
|
|
654
|
+
const pathValue = (0, path_1.join)(evidenceDir, fileName);
|
|
655
|
+
try {
|
|
656
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(pathValue, 'utf-8'));
|
|
657
|
+
const record = asObject(parsed);
|
|
658
|
+
if (!record)
|
|
659
|
+
continue;
|
|
660
|
+
const timestamp = asString(record.timestamp);
|
|
661
|
+
if (timestamp && (!lastRunAt || Date.parse(timestamp) > Date.parse(lastRunAt))) {
|
|
662
|
+
lastRunAt = timestamp;
|
|
663
|
+
}
|
|
664
|
+
runs += 1;
|
|
665
|
+
const verdict = (asString(record.verdict) || '').toUpperCase();
|
|
666
|
+
if (verdict === 'PASS') {
|
|
667
|
+
passRuns += 1;
|
|
668
|
+
}
|
|
669
|
+
else if (verdict === 'FAIL') {
|
|
670
|
+
failRuns += 1;
|
|
671
|
+
}
|
|
672
|
+
const blocking = clampInt(record.blockingCount, 0, 0, 100000);
|
|
673
|
+
const advisory = clampInt(record.advisoryCount, 0, 0, 100000);
|
|
674
|
+
totalBlocking += blocking;
|
|
675
|
+
totalAdvisory += advisory;
|
|
676
|
+
const regressions = Array.isArray(record.regressions) ? record.regressions : [];
|
|
677
|
+
if (regressions.length > 0)
|
|
678
|
+
regressionRuns += 1;
|
|
679
|
+
const canonical = asObject(record.canonicalVerifyOutput) || {};
|
|
680
|
+
const coverage = asNumber(canonical.driftScore) ?? asNumber(canonical.score);
|
|
681
|
+
if (coverage !== null) {
|
|
682
|
+
coverageAccum += Math.max(0, Math.min(100, coverage));
|
|
683
|
+
coverageCount += 1;
|
|
684
|
+
}
|
|
685
|
+
const policySources = asObject(canonical.policySources);
|
|
686
|
+
const policyMode = asString(policySources?.mode);
|
|
687
|
+
if (policyMode && policyMode !== 'local' && policyMode !== 'merged' && policyMode !== 'org_only') {
|
|
688
|
+
policyCounts.set(`policy_source:${policyMode}`, (policyCounts.get(`policy_source:${policyMode}`) || 0) + 1);
|
|
689
|
+
}
|
|
690
|
+
const entries = [];
|
|
691
|
+
if (Array.isArray(canonical.violations))
|
|
692
|
+
entries.push(...canonical.violations);
|
|
693
|
+
if (Array.isArray(canonical.warnings))
|
|
694
|
+
entries.push(...canonical.warnings);
|
|
695
|
+
if (Array.isArray(canonical.blockingItems))
|
|
696
|
+
entries.push(...canonical.blockingItems);
|
|
697
|
+
if (Array.isArray(canonical.advisoryItems))
|
|
698
|
+
entries.push(...canonical.advisoryItems);
|
|
699
|
+
for (const entry of entries) {
|
|
700
|
+
const item = asObject(entry);
|
|
701
|
+
if (!item)
|
|
702
|
+
continue;
|
|
703
|
+
const policy = asString(item.policy) || asString(item.rule) || 'unknown_policy';
|
|
704
|
+
const file = asString(item.file) || 'unknown';
|
|
705
|
+
const normalizedFile = file.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
706
|
+
const directory = (0, path_1.dirname)(normalizedFile) === '.' ? '/' : (0, path_1.dirname)(normalizedFile);
|
|
707
|
+
policyCounts.set(policy, (policyCounts.get(policy) || 0) + 1);
|
|
708
|
+
fileCounts.set(normalizedFile, (fileCounts.get(normalizedFile) || 0) + 1);
|
|
709
|
+
directoryCounts.set(directory, (directoryCounts.get(directory) || 0) + 1);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
// ignore malformed evidence artifact
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (runs === 0) {
|
|
717
|
+
const recordsDir = (0, path_1.join)(repoRoot, '.neurcode/executions/records');
|
|
718
|
+
const recordNames = executionFiles(repoRoot).slice(0, DEFAULT_EXECUTION_LIMIT);
|
|
719
|
+
for (const fileName of recordNames) {
|
|
720
|
+
const pathValue = (0, path_1.join)(recordsDir, fileName);
|
|
721
|
+
try {
|
|
722
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(pathValue, 'utf-8'));
|
|
723
|
+
const record = asObject(parsed);
|
|
724
|
+
if (!record)
|
|
725
|
+
continue;
|
|
726
|
+
runs += 1;
|
|
727
|
+
const completedAt = asString(record.completedAt) || asString(record.createdAt);
|
|
728
|
+
if (completedAt && (!lastRunAt || Date.parse(completedAt) > Date.parse(lastRunAt))) {
|
|
729
|
+
lastRunAt = completedAt;
|
|
730
|
+
}
|
|
731
|
+
const result = asObject(record.result);
|
|
732
|
+
const success = asBoolean(result?.success, false);
|
|
733
|
+
if (success)
|
|
734
|
+
passRuns += 1;
|
|
735
|
+
else
|
|
736
|
+
failRuns += 1;
|
|
737
|
+
const verification = asObject(record.verification);
|
|
738
|
+
const diff = asObject(verification?.diff);
|
|
739
|
+
const after = asObject(diff?.after);
|
|
740
|
+
totalBlocking += clampInt(after?.blocking, 0, 0, 100000);
|
|
741
|
+
totalAdvisory += clampInt(after?.advisory, 0, 0, 100000);
|
|
742
|
+
const trend = asString(diff?.trend);
|
|
743
|
+
if (trend === 'regressed')
|
|
744
|
+
regressionRuns += 1;
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
// ignore malformed execution
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const passRate = runs > 0 ? clampPercent((passRuns / runs) * 100) : 0;
|
|
753
|
+
const blockRate = runs > 0 ? clampPercent((failRuns / runs) * 100) : 0;
|
|
754
|
+
const averageBlocking = runs > 0 ? clampPercent(totalBlocking / runs) : 0;
|
|
755
|
+
const averageAdvisory = runs > 0 ? clampPercent(totalAdvisory / runs) : 0;
|
|
756
|
+
const regressionRate = runs > 0 ? clampPercent((regressionRuns / runs) * 100) : 0;
|
|
757
|
+
const coverageScore = coverageCount > 0 ? clampPercent(coverageAccum / coverageCount) : null;
|
|
758
|
+
const policyDrift = workspace.governance.policy.workspacePacks.length > 0
|
|
759
|
+
&& overridePacks.length > 0
|
|
760
|
+
&& !overridePacks.every((pack) => workspace.governance.policy.workspacePacks.includes(pack));
|
|
761
|
+
let riskScore = (blockRate * 0.55)
|
|
762
|
+
+ (regressionRate * 0.5)
|
|
763
|
+
+ (averageBlocking * 4.8)
|
|
764
|
+
+ (averageAdvisory * 1.3)
|
|
765
|
+
+ (runs < 3 ? 8 : 0)
|
|
766
|
+
+ (policyDrift ? 10 : 0);
|
|
767
|
+
if (workspace.governance.posture.enforcement === 'advisory') {
|
|
768
|
+
riskScore *= 0.9;
|
|
769
|
+
}
|
|
770
|
+
else if (workspace.governance.posture.enforcement === 'strict') {
|
|
771
|
+
riskScore *= 1.05;
|
|
772
|
+
}
|
|
773
|
+
const boundedRisk = clampPercent(Math.max(0, Math.min(100, riskScore)));
|
|
774
|
+
const riskLevel = computeRiskLevel(boundedRisk);
|
|
775
|
+
const status = !exists
|
|
776
|
+
? 'unknown'
|
|
777
|
+
: riskLevel === 'high'
|
|
778
|
+
? 'critical'
|
|
779
|
+
: riskLevel === 'medium'
|
|
780
|
+
? 'degraded'
|
|
781
|
+
: 'healthy';
|
|
782
|
+
const topPolicies = [...policyCounts.entries()]
|
|
783
|
+
.sort((left, right) => right[1] - left[1])
|
|
784
|
+
.slice(0, 5)
|
|
785
|
+
.map(([policy, occurrences]) => ({ policy, occurrences }));
|
|
786
|
+
const topFiles = [...fileCounts.entries()]
|
|
787
|
+
.sort((left, right) => right[1] - left[1])
|
|
788
|
+
.slice(0, 5)
|
|
789
|
+
.map(([file, occurrences]) => ({ file, occurrences }));
|
|
790
|
+
const health = {
|
|
791
|
+
workspaceId,
|
|
792
|
+
repositoryId: repository.id,
|
|
793
|
+
repositoryName: repository.name,
|
|
794
|
+
rootPath: repository.rootPath,
|
|
795
|
+
exists,
|
|
796
|
+
status,
|
|
797
|
+
riskLevel,
|
|
798
|
+
riskScore: boundedRisk,
|
|
799
|
+
runs,
|
|
800
|
+
passRate,
|
|
801
|
+
blockRate,
|
|
802
|
+
averageBlocking,
|
|
803
|
+
averageAdvisory,
|
|
804
|
+
regressionRate,
|
|
805
|
+
coverageScore,
|
|
806
|
+
lastRunAt,
|
|
807
|
+
policyDrift,
|
|
808
|
+
topPolicies,
|
|
809
|
+
topFiles,
|
|
810
|
+
services: repository.services,
|
|
811
|
+
policyDomain: repository.policyDomain,
|
|
812
|
+
};
|
|
813
|
+
const eventCounts = {};
|
|
814
|
+
const recentEvents = [];
|
|
815
|
+
if (exists) {
|
|
816
|
+
const events = readLatestJsonLines(eventFile(repoRoot), DEFAULT_EVENT_LIMIT);
|
|
817
|
+
for (const event of events) {
|
|
818
|
+
const type = asString(event.type) || 'unknown';
|
|
819
|
+
eventCounts[type] = (eventCounts[type] || 0) + 1;
|
|
820
|
+
const timestamp = asString(event.timestamp) || nowIso();
|
|
821
|
+
const cursor = asString(event.cursor) || `${Date.parse(timestamp) || Date.now()}:${asString(event.id) || ''}`;
|
|
822
|
+
recentEvents.push({
|
|
823
|
+
cursor,
|
|
824
|
+
type,
|
|
825
|
+
timestamp,
|
|
826
|
+
source: asString(event.source) || 'unknown',
|
|
827
|
+
actor: asString(event.actor) || 'unknown',
|
|
828
|
+
severity: asString(event.severity) || 'low',
|
|
829
|
+
executionId: asString(event.executionId) || 'unknown',
|
|
830
|
+
repositoryId: repository.id,
|
|
831
|
+
repositoryName: repository.name,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
recentEvents.sort((left, right) => {
|
|
835
|
+
const leftMs = Date.parse(left.timestamp);
|
|
836
|
+
const rightMs = Date.parse(right.timestamp);
|
|
837
|
+
return rightMs - leftMs;
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
repoScanCache.set(cacheKey, {
|
|
841
|
+
fingerprint,
|
|
842
|
+
health,
|
|
843
|
+
events: recentEvents,
|
|
844
|
+
eventCounts,
|
|
845
|
+
});
|
|
846
|
+
return { health, recentEvents, eventCounts };
|
|
847
|
+
}
|
|
848
|
+
function aggregateHotspots(rows) {
|
|
849
|
+
const fileMap = new Map();
|
|
850
|
+
const policyMap = new Map();
|
|
851
|
+
const dirMap = new Map();
|
|
852
|
+
for (const row of rows) {
|
|
853
|
+
for (const item of row.topFiles) {
|
|
854
|
+
const current = fileMap.get(item.file) || { occurrences: 0, repositories: new Set(), score: 0 };
|
|
855
|
+
current.occurrences += item.occurrences;
|
|
856
|
+
current.repositories.add(row.repositoryId);
|
|
857
|
+
current.score += item.occurrences * (row.riskScore / 100);
|
|
858
|
+
fileMap.set(item.file, current);
|
|
859
|
+
const directory = (0, path_1.dirname)(item.file) === '.' ? '/' : (0, path_1.dirname)(item.file);
|
|
860
|
+
const dirCurrent = dirMap.get(directory) || { occurrences: 0, repositories: new Set(), score: 0 };
|
|
861
|
+
dirCurrent.occurrences += item.occurrences;
|
|
862
|
+
dirCurrent.repositories.add(row.repositoryId);
|
|
863
|
+
dirCurrent.score += item.occurrences * (row.riskScore / 100);
|
|
864
|
+
dirMap.set(directory, dirCurrent);
|
|
865
|
+
}
|
|
866
|
+
for (const item of row.topPolicies) {
|
|
867
|
+
const current = policyMap.get(item.policy) || { occurrences: 0, repositories: new Set(), score: 0 };
|
|
868
|
+
current.occurrences += item.occurrences;
|
|
869
|
+
current.repositories.add(row.repositoryId);
|
|
870
|
+
current.score += item.occurrences * (row.riskScore / 100);
|
|
871
|
+
policyMap.set(item.policy, current);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
const toHotspots = (source, kind) => {
|
|
875
|
+
return [...source.entries()]
|
|
876
|
+
.map(([key, value]) => ({
|
|
877
|
+
key,
|
|
878
|
+
kind,
|
|
879
|
+
score: clampPercent(value.score),
|
|
880
|
+
occurrences: value.occurrences,
|
|
881
|
+
repositoryCount: value.repositories.size,
|
|
882
|
+
}))
|
|
883
|
+
.sort((left, right) => right.score - left.score)
|
|
884
|
+
.slice(0, 15);
|
|
885
|
+
};
|
|
886
|
+
return {
|
|
887
|
+
files: toHotspots(fileMap, 'file'),
|
|
888
|
+
policies: toHotspots(policyMap, 'policy'),
|
|
889
|
+
directories: toHotspots(dirMap, 'directory'),
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function summarizePosture(workspace, rows) {
|
|
893
|
+
const repositoryCount = rows.length;
|
|
894
|
+
const healthyRepositories = rows.filter((row) => row.status === 'healthy').length;
|
|
895
|
+
const degradedRepositories = rows.filter((row) => row.status === 'degraded').length;
|
|
896
|
+
const criticalRepositories = rows.filter((row) => row.status === 'critical').length;
|
|
897
|
+
const combinedRuns = rows.reduce((sum, row) => sum + row.runs, 0);
|
|
898
|
+
const passRate = repositoryCount > 0
|
|
899
|
+
? clampPercent(rows.reduce((sum, row) => sum + row.passRate, 0) / repositoryCount)
|
|
900
|
+
: 0;
|
|
901
|
+
const blockRate = repositoryCount > 0
|
|
902
|
+
? clampPercent(rows.reduce((sum, row) => sum + row.blockRate, 0) / repositoryCount)
|
|
903
|
+
: 0;
|
|
904
|
+
const overallRiskScore = repositoryCount > 0
|
|
905
|
+
? clampPercent(rows.reduce((sum, row) => sum + row.riskScore, 0) / repositoryCount)
|
|
906
|
+
: 0;
|
|
907
|
+
const overallRiskLevel = computeRiskLevel(overallRiskScore);
|
|
908
|
+
const coverageRows = rows.filter((row) => typeof row.coverageScore === 'number');
|
|
909
|
+
const averageCoverageScore = coverageRows.length > 0
|
|
910
|
+
? clampPercent(coverageRows.reduce((sum, row) => sum + (row.coverageScore || 0), 0) / coverageRows.length)
|
|
911
|
+
: null;
|
|
912
|
+
const regressionConcentration = [...rows]
|
|
913
|
+
.sort((left, right) => right.regressionRate - left.regressionRate)
|
|
914
|
+
.slice(0, 5)
|
|
915
|
+
.map((row) => ({
|
|
916
|
+
repositoryId: row.repositoryId,
|
|
917
|
+
repositoryName: row.repositoryName,
|
|
918
|
+
regressionRate: row.regressionRate,
|
|
919
|
+
}));
|
|
920
|
+
const serviceScores = new Map();
|
|
921
|
+
for (const row of rows) {
|
|
922
|
+
for (const service of row.services) {
|
|
923
|
+
serviceScores.set(service, (serviceScores.get(service) || 0) + row.riskScore);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
const unstableServices = [...serviceScores.entries()]
|
|
927
|
+
.sort((left, right) => right[1] - left[1])
|
|
928
|
+
.slice(0, 8)
|
|
929
|
+
.map(([service]) => service);
|
|
930
|
+
return {
|
|
931
|
+
workspaceId: workspace.id,
|
|
932
|
+
workspaceName: workspace.name,
|
|
933
|
+
repositoryCount,
|
|
934
|
+
healthyRepositories,
|
|
935
|
+
degradedRepositories,
|
|
936
|
+
criticalRepositories,
|
|
937
|
+
overallRiskLevel,
|
|
938
|
+
overallRiskScore,
|
|
939
|
+
passRate,
|
|
940
|
+
blockRate,
|
|
941
|
+
averageCoverageScore,
|
|
942
|
+
regressionConcentration,
|
|
943
|
+
policyDriftRepositories: rows.filter((row) => row.policyDrift).length,
|
|
944
|
+
unstableServices,
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
function resolveActiveWorkspaceInternal(paths, workspaces) {
|
|
948
|
+
const index = readWorkspaceIndex(paths);
|
|
949
|
+
if (index.activeWorkspaceId) {
|
|
950
|
+
const found = workspaces.find((workspace) => workspace.id === index.activeWorkspaceId);
|
|
951
|
+
if (found)
|
|
952
|
+
return found;
|
|
953
|
+
}
|
|
954
|
+
if (workspaces.length > 0) {
|
|
955
|
+
return workspaces[0];
|
|
956
|
+
}
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
function actorRoleForWorkspace(workspace, actor) {
|
|
960
|
+
if (!workspace)
|
|
961
|
+
return 'workspace_admin';
|
|
962
|
+
if (!actor)
|
|
963
|
+
return workspace.access.defaultRole;
|
|
964
|
+
const found = workspace.access.members.find((member) => member.actor.toLowerCase() === actor.toLowerCase());
|
|
965
|
+
return found ? found.role : workspace.access.defaultRole;
|
|
966
|
+
}
|
|
967
|
+
function listWorkspaces(cwd = process.cwd()) {
|
|
968
|
+
const rootDir = resolveWorkspaceRoot(cwd);
|
|
969
|
+
const paths = toWorkspacePaths(rootDir);
|
|
970
|
+
const workspaces = listWorkspaceDefinitionsInternal(paths);
|
|
971
|
+
return workspaces.map((workspace) => ({
|
|
972
|
+
id: workspace.id,
|
|
973
|
+
name: workspace.name,
|
|
974
|
+
description: workspace.description,
|
|
975
|
+
updatedAt: workspace.updatedAt,
|
|
976
|
+
repositoryCount: workspace.repositories.length,
|
|
977
|
+
enabledRepositoryCount: workspace.repositories.filter((repo) => repo.enabled).length,
|
|
978
|
+
posture: workspace.governance.posture,
|
|
979
|
+
}));
|
|
980
|
+
}
|
|
981
|
+
function getWorkspaceById(workspaceId, cwd = process.cwd()) {
|
|
982
|
+
const rootDir = resolveWorkspaceRoot(cwd);
|
|
983
|
+
const paths = toWorkspacePaths(rootDir);
|
|
984
|
+
const workspaces = listWorkspaceDefinitionsInternal(paths);
|
|
985
|
+
return workspaces.find((workspace) => workspace.id === workspaceId) || null;
|
|
986
|
+
}
|
|
987
|
+
function getActiveWorkspace(cwd = process.cwd()) {
|
|
988
|
+
const rootDir = resolveWorkspaceRoot(cwd);
|
|
989
|
+
const paths = toWorkspacePaths(rootDir);
|
|
990
|
+
const workspaces = listWorkspaceDefinitionsInternal(paths);
|
|
991
|
+
return resolveActiveWorkspaceInternal(paths, workspaces);
|
|
992
|
+
}
|
|
993
|
+
function createWorkspace(input, options) {
|
|
994
|
+
const rootDir = resolveWorkspaceRoot(options?.cwd || process.cwd());
|
|
995
|
+
const paths = toWorkspacePaths(rootDir);
|
|
996
|
+
const existing = listWorkspaceDefinitionsInternal(paths);
|
|
997
|
+
const requestedId = asString(input.id) || `${slugify(input.name)}-${(0, crypto_1.createHash)('sha256').update(`${input.name}:${Date.now()}`).digest('hex').slice(0, 6)}`;
|
|
998
|
+
const workspaceId = slugify(requestedId);
|
|
999
|
+
if (existing.some((workspace) => workspace.id === workspaceId)) {
|
|
1000
|
+
throw new Error(`Workspace already exists: ${workspaceId}`);
|
|
1001
|
+
}
|
|
1002
|
+
const repositories = Array.isArray(input.repositories) && input.repositories.length > 0
|
|
1003
|
+
? input.repositories.map((repo) => ({ ...repo }))
|
|
1004
|
+
: [
|
|
1005
|
+
{
|
|
1006
|
+
name: (0, path_1.basename)(rootDir),
|
|
1007
|
+
rootPath: '.',
|
|
1008
|
+
enabled: true,
|
|
1009
|
+
},
|
|
1010
|
+
];
|
|
1011
|
+
const seed = {
|
|
1012
|
+
schemaVersion: WORKSPACE_SCHEMA,
|
|
1013
|
+
id: workspaceId,
|
|
1014
|
+
name: input.name,
|
|
1015
|
+
description: input.description || null,
|
|
1016
|
+
createdAt: nowIso(),
|
|
1017
|
+
updatedAt: nowIso(),
|
|
1018
|
+
repositories,
|
|
1019
|
+
governance: input.governance || defaultGovernance(),
|
|
1020
|
+
access: input.access || { defaultRole: 'workspace_admin', members: [] },
|
|
1021
|
+
};
|
|
1022
|
+
const workspace = normalizeWorkspaceDefinition(rootDir, seed, workspaceId);
|
|
1023
|
+
writeWorkspaceDefinition(paths, workspace);
|
|
1024
|
+
const setActive = options?.setActive !== false;
|
|
1025
|
+
if (setActive) {
|
|
1026
|
+
writeWorkspaceIndex(paths, {
|
|
1027
|
+
schemaVersion: WORKSPACE_INDEX_SCHEMA,
|
|
1028
|
+
activeWorkspaceId: workspace.id,
|
|
1029
|
+
updatedAt: nowIso(),
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
const source = options?.source || 'cli';
|
|
1033
|
+
const actor = options?.actor || 'workspace-admin';
|
|
1034
|
+
const executionId = emitWorkspaceMutationEvent(rootDir, workspace, source, actor, 'created', {
|
|
1035
|
+
setActive,
|
|
1036
|
+
});
|
|
1037
|
+
return { workspace, executionId };
|
|
1038
|
+
}
|
|
1039
|
+
function setActiveWorkspace(workspaceId, options) {
|
|
1040
|
+
const rootDir = resolveWorkspaceRoot(options?.cwd || process.cwd());
|
|
1041
|
+
const paths = toWorkspacePaths(rootDir);
|
|
1042
|
+
const workspaces = listWorkspaceDefinitionsInternal(paths);
|
|
1043
|
+
const workspace = workspaces.find((item) => item.id === workspaceId);
|
|
1044
|
+
if (!workspace) {
|
|
1045
|
+
throw new Error(`Workspace not found: ${workspaceId}`);
|
|
1046
|
+
}
|
|
1047
|
+
writeWorkspaceIndex(paths, {
|
|
1048
|
+
schemaVersion: WORKSPACE_INDEX_SCHEMA,
|
|
1049
|
+
activeWorkspaceId: workspace.id,
|
|
1050
|
+
updatedAt: nowIso(),
|
|
1051
|
+
});
|
|
1052
|
+
const source = options?.source || 'cli';
|
|
1053
|
+
const actor = options?.actor || 'workspace-admin';
|
|
1054
|
+
const executionId = emitWorkspaceMutationEvent(rootDir, workspace, source, actor, 'activated');
|
|
1055
|
+
return { workspace, executionId };
|
|
1056
|
+
}
|
|
1057
|
+
function addWorkspaceRepository(workspaceId, input, options) {
|
|
1058
|
+
const rootDir = resolveWorkspaceRoot(options?.cwd || process.cwd());
|
|
1059
|
+
const paths = toWorkspacePaths(rootDir);
|
|
1060
|
+
const workspaces = listWorkspaceDefinitionsInternal(paths);
|
|
1061
|
+
const target = workspaces.find((item) => item.id === workspaceId);
|
|
1062
|
+
if (!target) {
|
|
1063
|
+
throw new Error(`Workspace not found: ${workspaceId}`);
|
|
1064
|
+
}
|
|
1065
|
+
const repo = normalizeRepository(rootDir, {
|
|
1066
|
+
id: input.id,
|
|
1067
|
+
name: input.name,
|
|
1068
|
+
rootPath: input.rootPath,
|
|
1069
|
+
services: input.services,
|
|
1070
|
+
policyDomain: input.policyDomain,
|
|
1071
|
+
tags: input.tags,
|
|
1072
|
+
enabled: input.enabled,
|
|
1073
|
+
}, `${workspaceId}-repo-${target.repositories.length + 1}`);
|
|
1074
|
+
if (target.repositories.some((existing) => existing.id === repo.id)) {
|
|
1075
|
+
throw new Error(`Repository already exists in workspace: ${repo.id}`);
|
|
1076
|
+
}
|
|
1077
|
+
target.repositories = [...target.repositories, repo].sort((left, right) => left.name.localeCompare(right.name));
|
|
1078
|
+
target.updatedAt = nowIso();
|
|
1079
|
+
writeWorkspaceDefinition(paths, normalizeWorkspaceDefinition(rootDir, target, target.id));
|
|
1080
|
+
const source = options?.source || 'cli';
|
|
1081
|
+
const actor = options?.actor || 'workspace-admin';
|
|
1082
|
+
const executionId = emitWorkspaceMutationEvent(rootDir, target, source, actor, 'repository-added', {
|
|
1083
|
+
repositoryId: repo.id,
|
|
1084
|
+
repositoryName: repo.name,
|
|
1085
|
+
repositoryPath: repo.rootPath,
|
|
1086
|
+
});
|
|
1087
|
+
return {
|
|
1088
|
+
workspace: target,
|
|
1089
|
+
executionId,
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
function updateWorkspace(workspaceId, patch, options) {
|
|
1093
|
+
const rootDir = resolveWorkspaceRoot(options?.cwd || process.cwd());
|
|
1094
|
+
const paths = toWorkspacePaths(rootDir);
|
|
1095
|
+
const workspaces = listWorkspaceDefinitionsInternal(paths);
|
|
1096
|
+
const target = workspaces.find((item) => item.id === workspaceId);
|
|
1097
|
+
if (!target) {
|
|
1098
|
+
throw new Error(`Workspace not found: ${workspaceId}`);
|
|
1099
|
+
}
|
|
1100
|
+
const merged = {
|
|
1101
|
+
...target,
|
|
1102
|
+
...(patch.name ? { name: patch.name } : {}),
|
|
1103
|
+
...(typeof patch.description !== 'undefined' ? { description: patch.description } : {}),
|
|
1104
|
+
...(patch.repositories ? { repositories: patch.repositories } : {}),
|
|
1105
|
+
...(patch.governance ? { governance: patch.governance } : {}),
|
|
1106
|
+
...(patch.access ? { access: patch.access } : {}),
|
|
1107
|
+
updatedAt: nowIso(),
|
|
1108
|
+
};
|
|
1109
|
+
const workspace = normalizeWorkspaceDefinition(rootDir, merged, workspaceId);
|
|
1110
|
+
writeWorkspaceDefinition(paths, workspace);
|
|
1111
|
+
const source = options?.source || 'cli';
|
|
1112
|
+
const actor = options?.actor || 'workspace-admin';
|
|
1113
|
+
const executionId = emitWorkspaceMutationEvent(rootDir, workspace, source, actor, 'updated');
|
|
1114
|
+
return {
|
|
1115
|
+
workspace,
|
|
1116
|
+
executionId,
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
function getWorkspaceRuntimeSnapshot(options) {
|
|
1120
|
+
const rootDir = resolveWorkspaceRoot(options?.cwd || process.cwd());
|
|
1121
|
+
const paths = toWorkspacePaths(rootDir);
|
|
1122
|
+
const workspaces = listWorkspaceDefinitionsInternal(paths);
|
|
1123
|
+
const requested = options?.workspaceId ? workspaces.find((workspace) => workspace.id === options.workspaceId) || null : null;
|
|
1124
|
+
const active = requested || resolveActiveWorkspaceInternal(paths, workspaces);
|
|
1125
|
+
const role = actorRoleForWorkspace(active, options?.actor || null);
|
|
1126
|
+
const summaries = workspaces.map((workspace) => ({
|
|
1127
|
+
id: workspace.id,
|
|
1128
|
+
name: workspace.name,
|
|
1129
|
+
description: workspace.description,
|
|
1130
|
+
updatedAt: workspace.updatedAt,
|
|
1131
|
+
repositoryCount: workspace.repositories.length,
|
|
1132
|
+
enabledRepositoryCount: workspace.repositories.filter((repo) => repo.enabled).length,
|
|
1133
|
+
posture: workspace.governance.posture,
|
|
1134
|
+
}));
|
|
1135
|
+
if (!active) {
|
|
1136
|
+
return {
|
|
1137
|
+
schemaVersion: WORKSPACE_RUNTIME_SCHEMA,
|
|
1138
|
+
generatedAt: nowIso(),
|
|
1139
|
+
rootDir,
|
|
1140
|
+
activeWorkspaceId: null,
|
|
1141
|
+
activeWorkspaceRole: role,
|
|
1142
|
+
workspaces: summaries,
|
|
1143
|
+
workspace: null,
|
|
1144
|
+
effectiveControlPlane: null,
|
|
1145
|
+
repositoryHealthMatrix: [],
|
|
1146
|
+
hotspots: {
|
|
1147
|
+
files: [],
|
|
1148
|
+
policies: [],
|
|
1149
|
+
directories: [],
|
|
1150
|
+
},
|
|
1151
|
+
runtimeActivity: {
|
|
1152
|
+
eventCounts: {},
|
|
1153
|
+
recentEvents: [],
|
|
1154
|
+
},
|
|
1155
|
+
posture: null,
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
const rows = [];
|
|
1159
|
+
const allEvents = [];
|
|
1160
|
+
const eventCounts = {};
|
|
1161
|
+
const repositories = active.repositories
|
|
1162
|
+
.filter((repo) => repo.enabled)
|
|
1163
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
1164
|
+
for (const repository of repositories) {
|
|
1165
|
+
const repoRoot = (0, path_1.resolve)(rootDir, repository.rootPath);
|
|
1166
|
+
if (insideRoot(rootDir, repoRoot) && (0, fs_1.existsSync)(repoRoot)) {
|
|
1167
|
+
enforceWorkspaceRetention(active, repoRoot);
|
|
1168
|
+
}
|
|
1169
|
+
const scan = computeRepositoryHealth(active.id, repository, active, rootDir);
|
|
1170
|
+
rows.push(scan.health);
|
|
1171
|
+
allEvents.push(...scan.recentEvents.slice(0, 40));
|
|
1172
|
+
for (const [eventType, count] of Object.entries(scan.eventCounts)) {
|
|
1173
|
+
eventCounts[eventType] = (eventCounts[eventType] || 0) + count;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
allEvents.sort((left, right) => {
|
|
1177
|
+
const leftMs = Date.parse(left.timestamp);
|
|
1178
|
+
const rightMs = Date.parse(right.timestamp);
|
|
1179
|
+
return rightMs - leftMs;
|
|
1180
|
+
});
|
|
1181
|
+
const hotspots = aggregateHotspots(rows);
|
|
1182
|
+
const posture = summarizePosture(active, rows);
|
|
1183
|
+
return {
|
|
1184
|
+
schemaVersion: WORKSPACE_RUNTIME_SCHEMA,
|
|
1185
|
+
generatedAt: nowIso(),
|
|
1186
|
+
rootDir,
|
|
1187
|
+
activeWorkspaceId: active.id,
|
|
1188
|
+
activeWorkspaceRole: role,
|
|
1189
|
+
workspaces: summaries,
|
|
1190
|
+
workspace: active,
|
|
1191
|
+
effectiveControlPlane: resolveEffectiveControlPlane(active, rootDir),
|
|
1192
|
+
repositoryHealthMatrix: rows,
|
|
1193
|
+
hotspots,
|
|
1194
|
+
runtimeActivity: {
|
|
1195
|
+
eventCounts,
|
|
1196
|
+
recentEvents: allEvents.slice(0, 60),
|
|
1197
|
+
},
|
|
1198
|
+
posture,
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
function selectWorkspaceRepositories(workspace, repositoryIds) {
|
|
1202
|
+
const enabled = workspace.repositories.filter((repo) => repo.enabled);
|
|
1203
|
+
if (!repositoryIds || repositoryIds.length === 0) {
|
|
1204
|
+
return [...enabled].sort((left, right) => left.name.localeCompare(right.name));
|
|
1205
|
+
}
|
|
1206
|
+
const target = new Set(repositoryIds.map((id) => slugify(id)));
|
|
1207
|
+
return enabled
|
|
1208
|
+
.filter((repo) => target.has(repo.id))
|
|
1209
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
1210
|
+
}
|
|
1211
|
+
async function executeWorkspaceAction(request, options) {
|
|
1212
|
+
const rootDir = resolveWorkspaceRoot(options?.cwd || process.cwd());
|
|
1213
|
+
const paths = toWorkspacePaths(rootDir);
|
|
1214
|
+
const workspaces = listWorkspaceDefinitionsInternal(paths);
|
|
1215
|
+
const active = request.workspaceId
|
|
1216
|
+
? workspaces.find((workspace) => workspace.id === request.workspaceId) || null
|
|
1217
|
+
: resolveActiveWorkspaceInternal(paths, workspaces);
|
|
1218
|
+
if (!active) {
|
|
1219
|
+
throw new Error('No active workspace configured. Create or activate a workspace first.');
|
|
1220
|
+
}
|
|
1221
|
+
const repositories = selectWorkspaceRepositories(active, request.repositoryIds);
|
|
1222
|
+
const source = request.source || 'cli';
|
|
1223
|
+
const actor = request.actor || 'workspace-admin';
|
|
1224
|
+
const startedAt = nowIso();
|
|
1225
|
+
const effectiveControlPlane = resolveEffectiveControlPlane(active, rootDir);
|
|
1226
|
+
const runtimeConfig = asObject(effectiveControlPlane?.runtime) || {};
|
|
1227
|
+
const evidenceConfig = asObject(effectiveControlPlane?.evidence) || {};
|
|
1228
|
+
const executionConfig = asObject(runtimeConfig.execution) || {};
|
|
1229
|
+
const verificationConfig = asObject(runtimeConfig.verification) || {};
|
|
1230
|
+
const evidenceCollection = asObject(evidenceConfig.collection) || {};
|
|
1231
|
+
const workspaceDedupeWindow = asNumber(executionConfig.dedupeWindowMs);
|
|
1232
|
+
const workspaceCiMode = asBoolean(verificationConfig.deterministicOnlyInCi, true);
|
|
1233
|
+
const workspaceEvidenceDir = asString(evidenceCollection.directory);
|
|
1234
|
+
const items = [];
|
|
1235
|
+
let succeeded = 0;
|
|
1236
|
+
let failed = 0;
|
|
1237
|
+
for (const repository of repositories) {
|
|
1238
|
+
const repoRoot = (0, path_1.resolve)(rootDir, repository.rootPath);
|
|
1239
|
+
if (!insideRoot(rootDir, repoRoot) || !(0, fs_1.existsSync)(repoRoot)) {
|
|
1240
|
+
failed += 1;
|
|
1241
|
+
items.push({
|
|
1242
|
+
repositoryId: repository.id,
|
|
1243
|
+
repositoryName: repository.name,
|
|
1244
|
+
rootPath: repository.rootPath,
|
|
1245
|
+
ok: false,
|
|
1246
|
+
execution: null,
|
|
1247
|
+
primaryPayload: null,
|
|
1248
|
+
verificationPayload: null,
|
|
1249
|
+
error: 'Repository path missing or outside workspace root',
|
|
1250
|
+
});
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
enforceWorkspaceRetention(active, repoRoot);
|
|
1254
|
+
try {
|
|
1255
|
+
const run = await (0, execution_bus_1.runExecution)({
|
|
1256
|
+
type: request.type,
|
|
1257
|
+
source,
|
|
1258
|
+
actor,
|
|
1259
|
+
target: request.target || null,
|
|
1260
|
+
intentText: request.intentText || null,
|
|
1261
|
+
reverify: request.reverify,
|
|
1262
|
+
ciMode: typeof request.ciMode === 'boolean' ? request.ciMode : workspaceCiMode,
|
|
1263
|
+
evidenceDir: request.evidenceDir || workspaceEvidenceDir || undefined,
|
|
1264
|
+
dedupeWindowMs: typeof request.dedupeWindowMs === 'number' ? request.dedupeWindowMs : workspaceDedupeWindow ?? undefined,
|
|
1265
|
+
cwd: repoRoot,
|
|
1266
|
+
});
|
|
1267
|
+
const ok = run.execution.result?.success === true;
|
|
1268
|
+
if (ok)
|
|
1269
|
+
succeeded += 1;
|
|
1270
|
+
else
|
|
1271
|
+
failed += 1;
|
|
1272
|
+
items.push({
|
|
1273
|
+
repositoryId: repository.id,
|
|
1274
|
+
repositoryName: repository.name,
|
|
1275
|
+
rootPath: repository.rootPath,
|
|
1276
|
+
ok,
|
|
1277
|
+
execution: run.execution,
|
|
1278
|
+
primaryPayload: run.primaryPayload,
|
|
1279
|
+
verificationPayload: run.verificationPayload,
|
|
1280
|
+
error: ok ? null : run.execution.result?.message || 'Workspace execution failed',
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
catch (error) {
|
|
1284
|
+
failed += 1;
|
|
1285
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1286
|
+
items.push({
|
|
1287
|
+
repositoryId: repository.id,
|
|
1288
|
+
repositoryName: repository.name,
|
|
1289
|
+
rootPath: repository.rootPath,
|
|
1290
|
+
ok: false,
|
|
1291
|
+
execution: null,
|
|
1292
|
+
primaryPayload: null,
|
|
1293
|
+
verificationPayload: null,
|
|
1294
|
+
error: message,
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
finally {
|
|
1298
|
+
enforceWorkspaceRetention(active, repoRoot);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
const completedAt = nowIso();
|
|
1302
|
+
const synthetic = (0, execution_bus_1.recordSyntheticExecution)({
|
|
1303
|
+
cwd: rootDir,
|
|
1304
|
+
type: 'policy-sync',
|
|
1305
|
+
source,
|
|
1306
|
+
actor,
|
|
1307
|
+
status: failed > 0 ? 'failed' : 'completed',
|
|
1308
|
+
success: failed === 0,
|
|
1309
|
+
message: `Workspace action ${request.type} across ${repositories.length} repos`,
|
|
1310
|
+
payload: {
|
|
1311
|
+
action: 'workspace.execute',
|
|
1312
|
+
workspaceId: active.id,
|
|
1313
|
+
workspaceName: active.name,
|
|
1314
|
+
executionType: request.type,
|
|
1315
|
+
totals: {
|
|
1316
|
+
repositories: repositories.length,
|
|
1317
|
+
attempted: items.length,
|
|
1318
|
+
succeeded,
|
|
1319
|
+
failed,
|
|
1320
|
+
},
|
|
1321
|
+
repositories: items.map((item) => ({
|
|
1322
|
+
repositoryId: item.repositoryId,
|
|
1323
|
+
repositoryName: item.repositoryName,
|
|
1324
|
+
ok: item.ok,
|
|
1325
|
+
executionId: item.execution?.id || null,
|
|
1326
|
+
error: item.error,
|
|
1327
|
+
})),
|
|
1328
|
+
},
|
|
1329
|
+
verification: {
|
|
1330
|
+
verdict: failed > 0 ? 'WARN' : 'PASS',
|
|
1331
|
+
counts: {
|
|
1332
|
+
blocking: failed,
|
|
1333
|
+
advisory: Math.max(0, items.length - succeeded),
|
|
1334
|
+
},
|
|
1335
|
+
},
|
|
1336
|
+
narrative: {
|
|
1337
|
+
status: failed > 0 ? 'warning' : 'success',
|
|
1338
|
+
summary: `Workspace execution ${request.type} ${failed > 0 ? 'completed with failures' : 'completed successfully'}`,
|
|
1339
|
+
why: `Applied deterministic ${request.type} orchestration across ${repositories.length} repositories.`,
|
|
1340
|
+
riskLevel: failed > 0 ? 'high' : 'low',
|
|
1341
|
+
recommendedAction: failed > 0
|
|
1342
|
+
? 'Review failed repositories in workspace execution output and rerun targeted remediation.'
|
|
1343
|
+
: 'Review workspace posture to confirm cross-repo risk reduction.',
|
|
1344
|
+
expectedImprovement: 'Workspace execution history now includes cross-repo governance runs with provenance.',
|
|
1345
|
+
},
|
|
1346
|
+
});
|
|
1347
|
+
try {
|
|
1348
|
+
(0, runtime_events_1.emitRuntimeEvent)(rootDir, {
|
|
1349
|
+
type: failed > 0 ? 'execution.failed' : 'execution.completed',
|
|
1350
|
+
executionId: synthetic.id,
|
|
1351
|
+
source,
|
|
1352
|
+
actor,
|
|
1353
|
+
severity: failed > 0 ? 'high' : 'low',
|
|
1354
|
+
payload: {
|
|
1355
|
+
action: 'workspace.execute',
|
|
1356
|
+
workspaceId: active.id,
|
|
1357
|
+
workspaceName: active.name,
|
|
1358
|
+
executionType: request.type,
|
|
1359
|
+
repositories: repositories.length,
|
|
1360
|
+
succeeded,
|
|
1361
|
+
failed,
|
|
1362
|
+
},
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
catch {
|
|
1366
|
+
// best-effort
|
|
1367
|
+
}
|
|
1368
|
+
try {
|
|
1369
|
+
const paths = toWorkspacePaths(rootDir);
|
|
1370
|
+
const index = readWorkspaceIndex(paths);
|
|
1371
|
+
(0, replay_runtime_1.writeWorkspaceReplaySnapshot)({
|
|
1372
|
+
cwd: rootDir,
|
|
1373
|
+
workspaceId: active.id,
|
|
1374
|
+
workspaceName: active.name,
|
|
1375
|
+
workspace: JSON.parse(JSON.stringify(active)),
|
|
1376
|
+
posture: {
|
|
1377
|
+
totals: {
|
|
1378
|
+
repositories: repositories.length,
|
|
1379
|
+
attempted: items.length,
|
|
1380
|
+
succeeded,
|
|
1381
|
+
failed,
|
|
1382
|
+
},
|
|
1383
|
+
executionType: request.type,
|
|
1384
|
+
startedAt,
|
|
1385
|
+
completedAt,
|
|
1386
|
+
},
|
|
1387
|
+
source,
|
|
1388
|
+
actor,
|
|
1389
|
+
action: 'workspace.execute',
|
|
1390
|
+
executionId: synthetic.id,
|
|
1391
|
+
activeWorkspaceId: index.activeWorkspaceId,
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
catch {
|
|
1395
|
+
// Snapshot persistence is best-effort.
|
|
1396
|
+
}
|
|
1397
|
+
return {
|
|
1398
|
+
workspaceId: active.id,
|
|
1399
|
+
workspaceName: active.name,
|
|
1400
|
+
executionId: synthetic.id,
|
|
1401
|
+
source,
|
|
1402
|
+
actor,
|
|
1403
|
+
type: request.type,
|
|
1404
|
+
startedAt,
|
|
1405
|
+
completedAt,
|
|
1406
|
+
totals: {
|
|
1407
|
+
repositories: repositories.length,
|
|
1408
|
+
attempted: items.length,
|
|
1409
|
+
succeeded,
|
|
1410
|
+
failed,
|
|
1411
|
+
},
|
|
1412
|
+
items,
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
//# sourceMappingURL=workspace-runtime.js.map
|