@bbigbang/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.js +380 -0
- package/dist/execution/executionDispatcher.js +3810 -0
- package/dist/main.js +90 -0
- package/dist/nodeEventHistory.js +206 -0
- package/dist/scheduler/dreamLogic.js +50 -0
- package/dist/scheduler/dreamScheduler.js +65 -0
- package/dist/services/agentFileAccessService.js +1913 -0
- package/dist/services/agentRuntimeCleanupBroker.js +62 -0
- package/dist/services/agentSkillsBroker.js +118 -0
- package/dist/services/agentSkillsService.js +83 -0
- package/dist/services/agentWorkspaceBroker.js +937 -0
- package/dist/services/agentWorkspaceService.js +70 -0
- package/dist/services/appVersion.js +14 -0
- package/dist/services/auth.js +586 -0
- package/dist/services/claudeControlBroker.js +154 -0
- package/dist/services/claudeTranscriptBroker.js +100 -0
- package/dist/services/claudeTranscriptService.js +359 -0
- package/dist/services/codexAppServerBroker.js +155 -0
- package/dist/services/codexTranscriptBroker.js +98 -0
- package/dist/services/codexTranscriptService.js +961 -0
- package/dist/services/droidMissionBroker.js +124 -0
- package/dist/services/droidMissionImporter.js +630 -0
- package/dist/services/droidModelOptions.js +165 -0
- package/dist/services/hubServerRegistrationService.js +268 -0
- package/dist/services/libraryManifest.js +43 -0
- package/dist/services/libraryScaffold.js +26 -0
- package/dist/services/libraryService.js +2263 -0
- package/dist/services/memoryService.js +386 -0
- package/dist/services/missionEvidence.js +377 -0
- package/dist/services/missionService.js +2361 -0
- package/dist/services/missionTrace.js +158 -0
- package/dist/services/nativeMissionBriefParser.js +120 -0
- package/dist/services/nativeMissionOrchestrator.js +2045 -0
- package/dist/services/nativeMissionReportGenerator.js +227 -0
- package/dist/services/nativeMissionValidationRunner.js +452 -0
- package/dist/services/nativeMissionWorkerBroker.js +190 -0
- package/dist/services/nodeRegistry.js +34 -0
- package/dist/services/nodeStateReconciler.js +97 -0
- package/dist/services/panelMediaScanner.js +119 -0
- package/dist/services/persistentRuntimeJsonlClient.js +153 -0
- package/dist/services/platformAgentPolicy.js +180 -0
- package/dist/services/platformAgentService.js +2041 -0
- package/dist/services/projectAccessResolver.js +93 -0
- package/dist/services/projectService.js +392 -0
- package/dist/services/resourceSpaceService.js +140 -0
- package/dist/services/scenarioRuntimeService.js +1130 -0
- package/dist/services/suggestedPlannerService.js +868 -0
- package/dist/services/workbenchGitBroker.js +161 -0
- package/dist/services/workbenchGitService.js +69 -0
- package/dist/services/workbenchInspectBroker.js +65 -0
- package/dist/services/workbenchNodePathService.js +79 -0
- package/dist/services/workbenchRegistryService.js +240 -0
- package/dist/services/workbenchRootService.js +181 -0
- package/dist/services/workbenchTerminalBroker.js +378 -0
- package/dist/services/workspaceRunOwnership.js +60 -0
- package/dist/services/workspaceScaffold.js +105 -0
- package/dist/services/workspaceSessionRuntimeService.js +576 -0
- package/dist/services/workspaceSessionService.js +245 -0
- package/dist/services/workspaceToolActionRunner.js +1582 -0
- package/dist/services/workspaceToolErrors.js +10 -0
- package/dist/services/workspaceToolExecutionUtils.js +895 -0
- package/dist/services/workspaceToolLatestStateProjector.js +91 -0
- package/dist/services/workspaceToolManifest.js +572 -0
- package/dist/services/workspaceToolMutationQueue.js +43 -0
- package/dist/services/workspaceToolPanelProjection.js +460 -0
- package/dist/services/workspaceToolPromotion.js +255 -0
- package/dist/services/workspaceToolPromotionState.js +224 -0
- package/dist/services/workspaceToolPublishDiagnostics.js +189 -0
- package/dist/services/workspaceToolPublishIdentityResolver.js +146 -0
- package/dist/services/workspaceToolReadModel.js +378 -0
- package/dist/services/workspaceToolRunLedger.js +239 -0
- package/dist/services/workspaceToolService.js +3067 -0
- package/dist/services/workspaceToolSnapshotPanelSync.js +293 -0
- package/dist/services/workspaceToolTerminalLifecycle.js +283 -0
- package/dist/services/workspaceToolTypes.js +1 -0
- package/dist/services/workspaceToolUploadMaterializer.js +228 -0
- package/dist/web/actionCardRoutes.js +129 -0
- package/dist/web/actionCards.js +469 -0
- package/dist/web/activationContext.js +684 -0
- package/dist/web/agentChannelGuards.js +48 -0
- package/dist/web/agentMentionCooldowns.js +32 -0
- package/dist/web/agentReminders.js +1668 -0
- package/dist/web/agentRuntimePresence.js +197 -0
- package/dist/web/agentSelfState.js +494 -0
- package/dist/web/agentTaskLinks.js +26 -0
- package/dist/web/agentVisibility.js +79 -0
- package/dist/web/assets.js +95 -0
- package/dist/web/channelActivationPrompt.js +395 -0
- package/dist/web/channelMemoryNotes.js +127 -0
- package/dist/web/channelMentions.js +10 -0
- package/dist/web/channelMessageSequences.js +19 -0
- package/dist/web/channelSubscriptions.js +26 -0
- package/dist/web/clearedTaskRoots.js +10 -0
- package/dist/web/collaborationPromptGuidance.js +36 -0
- package/dist/web/collaborationSurfaceState.js +140 -0
- package/dist/web/contextBundleRanking.js +154 -0
- package/dist/web/contextBundleResolver.js +488 -0
- package/dist/web/conversationBuiltinSkillRoots.js +50 -0
- package/dist/web/conversationControls.js +232 -0
- package/dist/web/conversationHandoffs.js +612 -0
- package/dist/web/conversationManager.js +2511 -0
- package/dist/web/conversationSummaries.js +876 -0
- package/dist/web/conversationSurfaceKinds.js +17 -0
- package/dist/web/conversationTargets.js +173 -0
- package/dist/web/directActivationPrompt.js +122 -0
- package/dist/web/directReplyTargets.js +69 -0
- package/dist/web/directThreadResolver.js +129 -0
- package/dist/web/dmTaskHandoffPrompt.js +120 -0
- package/dist/web/dmTaskThreadStatusProjection.js +229 -0
- package/dist/web/ftsQuery.js +33 -0
- package/dist/web/internalAgentRouter.js +11341 -0
- package/dist/web/libraryCuratorScheduler.js +58 -0
- package/dist/web/libraryDocumentPromptGuidance.js +8 -0
- package/dist/web/messageCheckpoints.js +19 -0
- package/dist/web/nodeWsHandler.js +2495 -0
- package/dist/web/notificationRounds.js +1061 -0
- package/dist/web/panelActionMessages.js +108 -0
- package/dist/web/panelActivationPrompt.js +18 -0
- package/dist/web/panelAudit.js +273 -0
- package/dist/web/panelLifecycle.js +222 -0
- package/dist/web/panelMediaPolicy.js +43 -0
- package/dist/web/panelPathPolicy.js +63 -0
- package/dist/web/panelPreviews.js +175 -0
- package/dist/web/panelQueryHandles.js +2749 -0
- package/dist/web/panelRoutes.js +2147 -0
- package/dist/web/panels.js +904 -0
- package/dist/web/peerInboxAggregates.js +1247 -0
- package/dist/web/planApprovalState.js +92 -0
- package/dist/web/platformAgentScheduler.js +66 -0
- package/dist/web/proactiveOpportunities.js +452 -0
- package/dist/web/promptContextSections.js +242 -0
- package/dist/web/promptHistorySanitizer.js +26 -0
- package/dist/web/promptSlashCommands.js +158 -0
- package/dist/web/rollingConversationSummary.js +453 -0
- package/dist/web/routeHelpers.js +11 -0
- package/dist/web/routes/handoff.js +288 -0
- package/dist/web/routes/history.js +345 -0
- package/dist/web/routes/memory.js +258 -0
- package/dist/web/routes/selfState.js +171 -0
- package/dist/web/routes/workspace.js +154 -0
- package/dist/web/runSurfaceWatermarks.js +431 -0
- package/dist/web/runtimeCapabilities.js +48 -0
- package/dist/web/sameAgentHandoffs.js +494 -0
- package/dist/web/server.js +15567 -0
- package/dist/web/sharedCollaborationCapsules.js +163 -0
- package/dist/web/soloSessionRelay.js +42 -0
- package/dist/web/soloWsHandler.js +138 -0
- package/dist/web/suggestedPlannerScheduler.js +56 -0
- package/dist/web/surfaceActivationPolicy.js +108 -0
- package/dist/web/surfaceCollaborators.js +61 -0
- package/dist/web/surfaceSystemStatus.js +263 -0
- package/dist/web/targetParticipants.js +77 -0
- package/dist/web/taskEvents.js +49 -0
- package/dist/web/taskLifecycleMessages.js +165 -0
- package/dist/web/taskLoops.js +732 -0
- package/dist/web/taskMemoryNotes.js +224 -0
- package/dist/web/taskNumbers.js +16 -0
- package/dist/web/taskOwnerGuards.js +49 -0
- package/dist/web/taskParticipantResolver.js +42 -0
- package/dist/web/taskParticipants.js +97 -0
- package/dist/web/taskSourceDetails.js +20 -0
- package/dist/web/taskStateViews.js +210 -0
- package/dist/web/taskStatusTransitions.js +9 -0
- package/dist/web/taskThreadFollowups.js +599 -0
- package/dist/web/taskThreadRuntimeClosure.js +685 -0
- package/dist/web/taskUpdateDelivery.js +104 -0
- package/dist/web/threadReplyContentHeuristics.js +30 -0
- package/dist/web/threadRoots.js +61 -0
- package/dist/web/threadTaskBindings.js +365 -0
- package/dist/web/uiPanelPromptGuidance.js +27 -0
- package/dist/web/workspaceMemoryHints.js +143 -0
- package/dist/web/workspaceToolPromptGuidance.js +30 -0
- package/dist/web/wsHandler.js +397 -0
- package/dist/web/wsSink.js +116 -0
- package/package.json +54 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export const NATIVE_MISSION_REPORT_FILE_NAME = 'mission-report.md';
|
|
4
|
+
/**
|
|
5
|
+
* Generates a Markdown mission report from native mission data and writes it
|
|
6
|
+
* to the mission directory. The report is intended to be human-readable and
|
|
7
|
+
* includes a mission summary, feature list, validation results, handoffs, and
|
|
8
|
+
* a timeline of key events.
|
|
9
|
+
*/
|
|
10
|
+
export function generateNativeMissionReport(missionDir, input) {
|
|
11
|
+
if (!existsSync(missionDir)) {
|
|
12
|
+
mkdirSync(missionDir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
const reportText = renderNativeMissionReport(input);
|
|
15
|
+
const reportPath = join(missionDir, NATIVE_MISSION_REPORT_FILE_NAME);
|
|
16
|
+
writeFileSync(reportPath, reportText, 'utf8');
|
|
17
|
+
return { reportPath, sizeBytes: Buffer.byteLength(reportText, 'utf8') };
|
|
18
|
+
}
|
|
19
|
+
function renderNativeMissionReport(input) {
|
|
20
|
+
const { mission, features, assertions, events } = input;
|
|
21
|
+
const featureStatusCounts = countBy(features.map((f) => f.status));
|
|
22
|
+
const assertionStatusCounts = countBy(assertions.map((a) => a.status));
|
|
23
|
+
const handoffFeatures = features.filter((f) => f.handoff != null);
|
|
24
|
+
const lines = [];
|
|
25
|
+
lines.push(`# Mission Report: ${mission.title}`);
|
|
26
|
+
lines.push('');
|
|
27
|
+
lines.push(`- **Mission ID:** \`${mission.missionId}\``);
|
|
28
|
+
lines.push(`- **State:** ${mission.state}`);
|
|
29
|
+
lines.push(`- **Runtime Provider:** ${mission.runtimeProvider}`);
|
|
30
|
+
lines.push(`- **Workspace:** \`${mission.workspacePath}\``);
|
|
31
|
+
lines.push(`- **Model Mode:** ${mission.modelMode}`);
|
|
32
|
+
if (mission.startedAt) {
|
|
33
|
+
lines.push(`- **Started At:** ${formatTimestamp(mission.startedAt)}`);
|
|
34
|
+
}
|
|
35
|
+
if (mission.completedAt) {
|
|
36
|
+
lines.push(`- **Completed At:** ${formatTimestamp(mission.completedAt)}`);
|
|
37
|
+
}
|
|
38
|
+
lines.push('');
|
|
39
|
+
lines.push('## Summary');
|
|
40
|
+
lines.push('');
|
|
41
|
+
lines.push(`- **Features:** ${features.length} total`);
|
|
42
|
+
lines.push(` - Completed: ${featureStatusCounts.completed ?? 0}`);
|
|
43
|
+
lines.push(` - Failed: ${featureStatusCounts.failed ?? 0}`);
|
|
44
|
+
lines.push(` - Skipped: ${featureStatusCounts.skipped ?? 0}`);
|
|
45
|
+
lines.push(` - Cancelled: ${featureStatusCounts.cancelled ?? 0}`);
|
|
46
|
+
lines.push(` - Pending: ${featureStatusCounts.pending ?? 0}`);
|
|
47
|
+
lines.push(`- **Validation Assertions:** ${assertions.length} total`);
|
|
48
|
+
lines.push(` - Passed: ${assertionStatusCounts.passed ?? 0}`);
|
|
49
|
+
lines.push(` - Failed: ${assertionStatusCounts.failed ?? 0}`);
|
|
50
|
+
lines.push(` - Blocked: ${assertionStatusCounts.blocked ?? 0}`);
|
|
51
|
+
lines.push(` - Pending: ${assertionStatusCounts.pending ?? 0}`);
|
|
52
|
+
lines.push(`- **Handoffs:** ${handoffFeatures.length}`);
|
|
53
|
+
lines.push('');
|
|
54
|
+
if (mission.description) {
|
|
55
|
+
lines.push('## Mission Brief');
|
|
56
|
+
lines.push('');
|
|
57
|
+
lines.push(mission.description);
|
|
58
|
+
lines.push('');
|
|
59
|
+
}
|
|
60
|
+
lines.push('## Features');
|
|
61
|
+
lines.push('');
|
|
62
|
+
if (features.length === 0) {
|
|
63
|
+
lines.push('*No features were parsed from the mission brief.*');
|
|
64
|
+
lines.push('');
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const featuresByMilestone = groupBy(features, (f) => f.milestone);
|
|
68
|
+
for (const [milestone, milestoneFeatures] of featuresByMilestone) {
|
|
69
|
+
lines.push(`### ${milestone}`);
|
|
70
|
+
lines.push('');
|
|
71
|
+
for (const feature of milestoneFeatures.sort((a, b) => a.ordering - b.ordering)) {
|
|
72
|
+
lines.push(`#### ${feature.description.split(/\r?\n/)[0] ?? feature.description}`);
|
|
73
|
+
lines.push('');
|
|
74
|
+
lines.push(`- **Status:** ${feature.status}`);
|
|
75
|
+
lines.push(`- **Feature ID:** \`${feature.featureId}\``);
|
|
76
|
+
if (feature.skillName) {
|
|
77
|
+
lines.push(`- **Skill:** ${feature.skillName}`);
|
|
78
|
+
}
|
|
79
|
+
if (feature.preconditions.length > 0) {
|
|
80
|
+
lines.push(`- **Preconditions:**`);
|
|
81
|
+
for (const precondition of feature.preconditions) {
|
|
82
|
+
lines.push(` - ${precondition}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (feature.expectedBehavior.length > 0) {
|
|
86
|
+
lines.push(`- **Expected Behavior:**`);
|
|
87
|
+
for (const expected of feature.expectedBehavior) {
|
|
88
|
+
lines.push(` - ${expected}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
lines.push('');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
lines.push('## Validation Results');
|
|
96
|
+
lines.push('');
|
|
97
|
+
if (assertions.length === 0) {
|
|
98
|
+
lines.push('*No validation assertions were generated.*');
|
|
99
|
+
lines.push('');
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const assertionsByFeature = groupBy(assertions, (a) => a.featureId ?? 'unknown');
|
|
103
|
+
for (const [featureId, featureAssertions] of assertionsByFeature) {
|
|
104
|
+
const feature = features.find((f) => f.featureId === featureId);
|
|
105
|
+
if (feature) {
|
|
106
|
+
lines.push(`### ${feature.description.split(/\r?\n/)[0] ?? feature.description}`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
lines.push(`### Feature ID: \`${featureId}\``);
|
|
110
|
+
}
|
|
111
|
+
lines.push('');
|
|
112
|
+
for (const assertion of featureAssertions) {
|
|
113
|
+
lines.push(`- **${assertion.status.toUpperCase()}:** ${assertion.assertionText}`);
|
|
114
|
+
if (assertion.note) {
|
|
115
|
+
lines.push(` - *Note:* ${assertion.note}`);
|
|
116
|
+
}
|
|
117
|
+
if (assertion.evidence) {
|
|
118
|
+
lines.push(` - *Evidence:* ${assertion.evidence}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
lines.push('');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
lines.push('## Handoffs');
|
|
125
|
+
lines.push('');
|
|
126
|
+
if (handoffFeatures.length === 0) {
|
|
127
|
+
lines.push('*No worker handoffs were recorded.*');
|
|
128
|
+
lines.push('');
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
for (const feature of handoffFeatures) {
|
|
132
|
+
const handoff = feature.handoff ?? {};
|
|
133
|
+
lines.push(`### ${feature.description.split(/\r?\n/)[0] ?? feature.description}`);
|
|
134
|
+
lines.push('');
|
|
135
|
+
const summary = typeof handoff.salientSummary === 'string' ? handoff.salientSummary : null;
|
|
136
|
+
if (summary) {
|
|
137
|
+
lines.push(summary);
|
|
138
|
+
lines.push('');
|
|
139
|
+
}
|
|
140
|
+
if (handoff.verification && typeof handoff.verification === 'object') {
|
|
141
|
+
lines.push(`- **Verification:** \`${JSON.stringify(handoff.verification)}\``);
|
|
142
|
+
}
|
|
143
|
+
if (Array.isArray(handoff.discoveredIssues) && handoff.discoveredIssues.length > 0) {
|
|
144
|
+
lines.push('- **Discovered Issues:**');
|
|
145
|
+
for (const issue of handoff.discoveredIssues) {
|
|
146
|
+
lines.push(` - ${String(issue)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
lines.push('');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
lines.push('## Timeline');
|
|
153
|
+
lines.push('');
|
|
154
|
+
const lifecycleEvents = events.filter((e) => isLifecycleEventType(e.eventType));
|
|
155
|
+
if (lifecycleEvents.length === 0) {
|
|
156
|
+
lines.push('*No lifecycle events recorded.*');
|
|
157
|
+
lines.push('');
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
for (const event of lifecycleEvents) {
|
|
161
|
+
const time = formatTimestamp(event.eventTime);
|
|
162
|
+
lines.push(`- **${time}** \`${event.eventType}\` (${event.source})`);
|
|
163
|
+
const payloadSummary = summarizePayload(event.payload);
|
|
164
|
+
if (payloadSummary) {
|
|
165
|
+
lines.push(` - ${payloadSummary}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
lines.push('');
|
|
169
|
+
}
|
|
170
|
+
lines.push('---');
|
|
171
|
+
lines.push('');
|
|
172
|
+
lines.push(`*Report generated by Bigbang Native Mission Runtime.*`);
|
|
173
|
+
lines.push('');
|
|
174
|
+
return lines.join('\n');
|
|
175
|
+
}
|
|
176
|
+
function isLifecycleEventType(eventType) {
|
|
177
|
+
return (eventType.startsWith('mission_') ||
|
|
178
|
+
eventType.startsWith('feature_') ||
|
|
179
|
+
eventType === 'validation_started' ||
|
|
180
|
+
eventType === 'validation_completed');
|
|
181
|
+
}
|
|
182
|
+
function summarizePayload(payload) {
|
|
183
|
+
const parts = [];
|
|
184
|
+
if (typeof payload.featureId === 'string') {
|
|
185
|
+
parts.push(`featureId=${payload.featureId}`);
|
|
186
|
+
}
|
|
187
|
+
if (typeof payload.milestone === 'string') {
|
|
188
|
+
parts.push(`milestone=${payload.milestone}`);
|
|
189
|
+
}
|
|
190
|
+
if (typeof payload.exitCode === 'number') {
|
|
191
|
+
parts.push(`exitCode=${payload.exitCode}`);
|
|
192
|
+
}
|
|
193
|
+
if (typeof payload.error === 'string') {
|
|
194
|
+
parts.push(`error=${payload.error}`);
|
|
195
|
+
}
|
|
196
|
+
if (typeof payload.reason === 'string') {
|
|
197
|
+
parts.push(`reason=${payload.reason}`);
|
|
198
|
+
}
|
|
199
|
+
if (parts.length === 0)
|
|
200
|
+
return null;
|
|
201
|
+
return parts.join(', ');
|
|
202
|
+
}
|
|
203
|
+
function formatTimestamp(timestamp) {
|
|
204
|
+
try {
|
|
205
|
+
return new Date(timestamp).toISOString();
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return String(timestamp);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function countBy(items) {
|
|
212
|
+
const counts = {};
|
|
213
|
+
for (const item of items) {
|
|
214
|
+
counts[item] = (counts[item] ?? 0) + 1;
|
|
215
|
+
}
|
|
216
|
+
return counts;
|
|
217
|
+
}
|
|
218
|
+
function groupBy(items, keyFn) {
|
|
219
|
+
const groups = new Map();
|
|
220
|
+
for (const item of items) {
|
|
221
|
+
const key = keyFn(item);
|
|
222
|
+
const list = groups.get(key) ?? [];
|
|
223
|
+
list.push(item);
|
|
224
|
+
groups.set(key, list);
|
|
225
|
+
}
|
|
226
|
+
return groups;
|
|
227
|
+
}
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
2
|
+
const DEFAULT_POLICY = 'continueOnWarning';
|
|
3
|
+
export class NativeMissionValidationRunner {
|
|
4
|
+
missionService;
|
|
5
|
+
defaultTimeoutMs;
|
|
6
|
+
defaultPolicy;
|
|
7
|
+
evaluator;
|
|
8
|
+
constructor(params) {
|
|
9
|
+
this.missionService = params.missionService;
|
|
10
|
+
this.defaultTimeoutMs = params.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
11
|
+
this.defaultPolicy = params.defaultPolicy ?? DEFAULT_POLICY;
|
|
12
|
+
this.evaluator = params.evaluator ?? defaultAssertionEvaluator;
|
|
13
|
+
}
|
|
14
|
+
async runValidation(input) {
|
|
15
|
+
const { missionId, milestone } = input;
|
|
16
|
+
const policy = input.policy ?? this.defaultPolicy;
|
|
17
|
+
const timeoutMs = input.timeoutMs ?? this.defaultTimeoutMs;
|
|
18
|
+
const forceInferredConfidence = input.forceInferredConfidence ?? false;
|
|
19
|
+
const features = this.missionService.listMissionFeatures({
|
|
20
|
+
missionId,
|
|
21
|
+
userId: '',
|
|
22
|
+
isAdmin: true,
|
|
23
|
+
});
|
|
24
|
+
const milestoneFeatureIds = new Set(features.filter((feature) => feature.milestone === milestone).map((feature) => feature.featureId));
|
|
25
|
+
const allAssertions = this.missionService.listMissionValidationAssertions({
|
|
26
|
+
missionId,
|
|
27
|
+
userId: '',
|
|
28
|
+
isAdmin: true,
|
|
29
|
+
});
|
|
30
|
+
const assertions = allAssertions.filter((assertion) => assertion.featureId && milestoneFeatureIds.has(assertion.featureId));
|
|
31
|
+
this.missionService.recordMissionRuntimeEvent({
|
|
32
|
+
missionId,
|
|
33
|
+
eventType: 'validation_started',
|
|
34
|
+
source: 'platform',
|
|
35
|
+
eventTime: Date.now(),
|
|
36
|
+
payload: {
|
|
37
|
+
milestone,
|
|
38
|
+
assertionCount: assertions.length,
|
|
39
|
+
assertionIds: assertions.map((assertion) => assertion.assertionId),
|
|
40
|
+
policy,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const featureById = new Map(features.map((feature) => [feature.featureId, feature]));
|
|
44
|
+
const runWithTimeout = async () => {
|
|
45
|
+
const result = {
|
|
46
|
+
milestone,
|
|
47
|
+
assertionCount: assertions.length,
|
|
48
|
+
passed: 0,
|
|
49
|
+
failed: 0,
|
|
50
|
+
blocked: 0,
|
|
51
|
+
failedAssertionIds: [],
|
|
52
|
+
shouldHalt: false,
|
|
53
|
+
timedOut: false,
|
|
54
|
+
};
|
|
55
|
+
for (const assertion of assertions) {
|
|
56
|
+
const feature = assertion.featureId ? featureById.get(assertion.featureId) ?? null : null;
|
|
57
|
+
const workerOutput = feature?.workerOutput ?? null;
|
|
58
|
+
const evaluation = await this.evaluator({ assertion, feature, workerOutput });
|
|
59
|
+
const confidence = forceInferredConfidence && evaluation.confidence !== 'unknown'
|
|
60
|
+
? 'inferred'
|
|
61
|
+
: evaluation.confidence;
|
|
62
|
+
this.missionService.updateMissionValidationAssertion({
|
|
63
|
+
missionId,
|
|
64
|
+
assertionId: assertion.assertionId,
|
|
65
|
+
status: evaluation.status,
|
|
66
|
+
note: evaluation.note,
|
|
67
|
+
evidence: evaluation.evidence,
|
|
68
|
+
confidence,
|
|
69
|
+
});
|
|
70
|
+
if (evaluation.status === 'passed') {
|
|
71
|
+
result.passed += 1;
|
|
72
|
+
}
|
|
73
|
+
else if (evaluation.status === 'failed') {
|
|
74
|
+
result.failed += 1;
|
|
75
|
+
result.failedAssertionIds.push(assertion.assertionId);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
result.blocked += 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
result.shouldHalt = policy === 'failOnFirst' && result.failed > 0;
|
|
82
|
+
return result;
|
|
83
|
+
};
|
|
84
|
+
let result;
|
|
85
|
+
let timedOut = false;
|
|
86
|
+
try {
|
|
87
|
+
result = await runWithTimeoutBound(runWithTimeout, timeoutMs, () => {
|
|
88
|
+
timedOut = true;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
const message = String(error?.message ?? error);
|
|
93
|
+
result = {
|
|
94
|
+
milestone,
|
|
95
|
+
assertionCount: assertions.length,
|
|
96
|
+
passed: 0,
|
|
97
|
+
failed: 0,
|
|
98
|
+
blocked: 0,
|
|
99
|
+
failedAssertionIds: [],
|
|
100
|
+
shouldHalt: policy === 'failOnFirst',
|
|
101
|
+
timedOut,
|
|
102
|
+
error: message,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
result.timedOut = timedOut || result.timedOut;
|
|
106
|
+
this.missionService.recordMissionRuntimeEvent({
|
|
107
|
+
missionId,
|
|
108
|
+
eventType: 'validation_completed',
|
|
109
|
+
source: 'platform',
|
|
110
|
+
eventTime: Date.now(),
|
|
111
|
+
payload: {
|
|
112
|
+
milestone,
|
|
113
|
+
assertionCount: result.assertionCount,
|
|
114
|
+
assertionIds: assertions.map((assertion) => assertion.assertionId),
|
|
115
|
+
failedAssertionIds: result.failedAssertionIds,
|
|
116
|
+
passed: result.passed,
|
|
117
|
+
failed: result.failed,
|
|
118
|
+
blocked: result.blocked,
|
|
119
|
+
policy,
|
|
120
|
+
shouldHalt: result.shouldHalt,
|
|
121
|
+
timedOut: result.timedOut,
|
|
122
|
+
error: result.error ?? null,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
async evaluateAssertion(input) {
|
|
128
|
+
const { missionId, assertionId, forceInferredConfidence } = input;
|
|
129
|
+
const assertion = this.missionService
|
|
130
|
+
.listMissionValidationAssertions({ missionId, userId: '', isAdmin: true })
|
|
131
|
+
.find((a) => a.assertionId === assertionId);
|
|
132
|
+
if (!assertion) {
|
|
133
|
+
throw new Error(`Assertion ${assertionId} not found`);
|
|
134
|
+
}
|
|
135
|
+
const features = this.missionService.listMissionFeatures({ missionId, userId: '', isAdmin: true });
|
|
136
|
+
const feature = assertion.featureId
|
|
137
|
+
? features.find((f) => f.featureId === assertion.featureId) ?? null
|
|
138
|
+
: null;
|
|
139
|
+
const workerOutput = feature?.workerOutput ?? null;
|
|
140
|
+
const evaluation = await this.evaluator({ assertion, feature, workerOutput });
|
|
141
|
+
const confidence = forceInferredConfidence && evaluation.confidence !== 'unknown'
|
|
142
|
+
? 'inferred'
|
|
143
|
+
: evaluation.confidence;
|
|
144
|
+
this.missionService.updateMissionValidationAssertion({
|
|
145
|
+
missionId,
|
|
146
|
+
assertionId,
|
|
147
|
+
status: evaluation.status,
|
|
148
|
+
note: evaluation.note,
|
|
149
|
+
evidence: evaluation.evidence,
|
|
150
|
+
confidence,
|
|
151
|
+
});
|
|
152
|
+
return { ...evaluation, confidence };
|
|
153
|
+
}
|
|
154
|
+
resolvePolicy(config) {
|
|
155
|
+
const value = config?.validationPolicy;
|
|
156
|
+
if (value === 'failOnFirst' || value === 'continueOnWarning')
|
|
157
|
+
return value;
|
|
158
|
+
return this.defaultPolicy;
|
|
159
|
+
}
|
|
160
|
+
resolveTimeoutMs(config) {
|
|
161
|
+
const value = config?.validationTimeoutMs;
|
|
162
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0)
|
|
163
|
+
return Math.floor(value);
|
|
164
|
+
return this.defaultTimeoutMs;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function runWithTimeoutBound(fn, timeoutMs, onTimeout) {
|
|
168
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
169
|
+
return fn();
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const timer = setTimeout(() => {
|
|
172
|
+
onTimeout();
|
|
173
|
+
reject(new Error(`Validation timed out after ${timeoutMs}ms`));
|
|
174
|
+
}, timeoutMs);
|
|
175
|
+
fn()
|
|
176
|
+
.then((value) => {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
resolve(value);
|
|
179
|
+
})
|
|
180
|
+
.catch((error) => {
|
|
181
|
+
clearTimeout(timer);
|
|
182
|
+
reject(error);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
export function defaultAssertionEvaluator(input) {
|
|
187
|
+
const { assertion, feature, workerOutput } = input;
|
|
188
|
+
if (!feature) {
|
|
189
|
+
return {
|
|
190
|
+
status: 'blocked',
|
|
191
|
+
note: 'Source feature not found for assertion.',
|
|
192
|
+
evidence: `assertionId=${assertion.assertionId}`,
|
|
193
|
+
confidence: 'unknown',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const status = feature.status;
|
|
197
|
+
if (status === 'completed') {
|
|
198
|
+
return evaluateCompletedFeature(assertion, feature, workerOutput);
|
|
199
|
+
}
|
|
200
|
+
if (status === 'failed') {
|
|
201
|
+
return evaluateFailedFeature(assertion, feature, workerOutput);
|
|
202
|
+
}
|
|
203
|
+
if (status === 'skipped') {
|
|
204
|
+
return {
|
|
205
|
+
status: 'blocked',
|
|
206
|
+
note: 'Source feature was skipped.',
|
|
207
|
+
evidence: buildFeatureStatusEvidence(feature),
|
|
208
|
+
confidence: 'inferred',
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (status === 'cancelled') {
|
|
212
|
+
return {
|
|
213
|
+
status: 'blocked',
|
|
214
|
+
note: 'Source feature was cancelled.',
|
|
215
|
+
evidence: buildFeatureStatusEvidence(feature),
|
|
216
|
+
confidence: 'inferred',
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
status: 'blocked',
|
|
221
|
+
note: `Source feature has unresolved status: ${status}.`,
|
|
222
|
+
evidence: buildFeatureStatusEvidence(feature),
|
|
223
|
+
confidence: 'unknown',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function evaluateCompletedFeature(assertion, feature, workerOutput) {
|
|
227
|
+
const commandsRun = extractCommandsRun(workerOutput);
|
|
228
|
+
if (commandsRun.length > 0) {
|
|
229
|
+
const failed = commandsRun.find((entry) => entry.exitCode !== 0);
|
|
230
|
+
if (failed) {
|
|
231
|
+
return {
|
|
232
|
+
status: 'failed',
|
|
233
|
+
note: `Verification command failed: ${failed.command}`,
|
|
234
|
+
evidence: `exitCode=${failed.exitCode}; observation=${truncateCitation(failed.observation)}`,
|
|
235
|
+
confidence: 'observed',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
status: 'passed',
|
|
240
|
+
note: 'Feature completed and verification commands ran successfully.',
|
|
241
|
+
evidence: commandsRun
|
|
242
|
+
.map((entry) => `${entry.command} (exitCode=${entry.exitCode})`)
|
|
243
|
+
.join('; '),
|
|
244
|
+
confidence: 'observed',
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const evidence = buildEvidenceCorpus(workerOutput);
|
|
248
|
+
if (!evidence.hasAnyEvidence) {
|
|
249
|
+
return {
|
|
250
|
+
status: 'passed',
|
|
251
|
+
note: 'Feature completed; no worker output evidence was captured.',
|
|
252
|
+
evidence: buildFeatureStatusEvidence(feature),
|
|
253
|
+
confidence: 'inferred',
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const match = findAssertionEvidenceMatch(assertion.assertionText, evidence);
|
|
257
|
+
if (match) {
|
|
258
|
+
return {
|
|
259
|
+
status: 'passed',
|
|
260
|
+
note: `Feature completed and worker output supports this assertion via ${match.source}.`,
|
|
261
|
+
evidence: match.citation,
|
|
262
|
+
confidence: 'observed',
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
status: 'passed',
|
|
267
|
+
note: 'Feature completed with worker output; no direct assertion keyword match, so result is derived from feature success.',
|
|
268
|
+
evidence: buildEvidenceSummary(evidence),
|
|
269
|
+
confidence: 'derived',
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function extractCommandsRun(workerOutput) {
|
|
273
|
+
const handoff = isPlainObject(workerOutput?.handoff) ? workerOutput.handoff : {};
|
|
274
|
+
const verification = isPlainObject(handoff.verification) ? handoff.verification : {};
|
|
275
|
+
const commandsRun = Array.isArray(verification.commandsRun) ? verification.commandsRun : [];
|
|
276
|
+
return commandsRun.filter((entry) => isPlainObject(entry) &&
|
|
277
|
+
typeof entry.command === 'string' &&
|
|
278
|
+
typeof entry.exitCode === 'number' &&
|
|
279
|
+
typeof entry.observation === 'string');
|
|
280
|
+
}
|
|
281
|
+
function evaluateFailedFeature(assertion, feature, workerOutput) {
|
|
282
|
+
const errorEvidence = buildErrorEvidence(workerOutput);
|
|
283
|
+
if (errorEvidence) {
|
|
284
|
+
return {
|
|
285
|
+
status: 'failed',
|
|
286
|
+
note: `Source feature ${feature.featureId} failed with worker-reported error.`,
|
|
287
|
+
evidence: errorEvidence,
|
|
288
|
+
confidence: 'observed',
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
status: 'failed',
|
|
293
|
+
note: `Source feature ${feature.featureId} failed.`,
|
|
294
|
+
evidence: buildFeatureStatusEvidence(feature),
|
|
295
|
+
confidence: 'derived',
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function buildEvidenceCorpus(workerOutput) {
|
|
299
|
+
const handoff = isPlainObject(workerOutput?.handoff) ? workerOutput.handoff : {};
|
|
300
|
+
const stdout = typeof workerOutput?.stdout === 'string' ? workerOutput.stdout : '';
|
|
301
|
+
const stderr = typeof workerOutput?.stderr === 'string' ? workerOutput.stderr : '';
|
|
302
|
+
const implementedFiles = Array.isArray(workerOutput?.implementedFiles)
|
|
303
|
+
? workerOutput.implementedFiles.filter((item) => typeof item === 'string')
|
|
304
|
+
: [];
|
|
305
|
+
return {
|
|
306
|
+
hasAnyEvidence: Boolean(stdout
|
|
307
|
+
|| stderr
|
|
308
|
+
|| implementedFiles.length > 0
|
|
309
|
+
|| handoff.salientSummary
|
|
310
|
+
|| handoff.verification
|
|
311
|
+
|| handoff.llmResponsePreview
|
|
312
|
+
|| (Array.isArray(handoff.discoveredIssues) && handoff.discoveredIssues.length > 0)),
|
|
313
|
+
handoffSummary: typeof handoff.salientSummary === 'string' ? handoff.salientSummary : '',
|
|
314
|
+
handoffVerification: isPlainObject(handoff.verification)
|
|
315
|
+
? JSON.stringify(handoff.verification)
|
|
316
|
+
: typeof handoff.verification === 'string'
|
|
317
|
+
? handoff.verification
|
|
318
|
+
: '',
|
|
319
|
+
handoffLlmPreview: typeof handoff.llmResponsePreview === 'string' ? handoff.llmResponsePreview : '',
|
|
320
|
+
discoveredIssues: Array.isArray(handoff.discoveredIssues)
|
|
321
|
+
? handoff.discoveredIssues
|
|
322
|
+
.map((item) => {
|
|
323
|
+
if (typeof item === 'string')
|
|
324
|
+
return item;
|
|
325
|
+
if (isPlainObject(item) && typeof item.description === 'string')
|
|
326
|
+
return item.description;
|
|
327
|
+
return JSON.stringify(item);
|
|
328
|
+
})
|
|
329
|
+
.join('\n')
|
|
330
|
+
: '',
|
|
331
|
+
stdout,
|
|
332
|
+
stderr,
|
|
333
|
+
implementedFiles,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function findAssertionEvidenceMatch(assertionText, evidence) {
|
|
337
|
+
const keywords = extractAssertionKeywords(assertionText);
|
|
338
|
+
if (keywords.length === 0)
|
|
339
|
+
return null;
|
|
340
|
+
// Prefer concrete file evidence first when a keyword names a changed file.
|
|
341
|
+
for (const filePath of evidence.implementedFiles) {
|
|
342
|
+
const normalizedPath = normalizeEvidenceText(filePath);
|
|
343
|
+
if (keywords.some((keyword) => normalizedPath.includes(keyword))) {
|
|
344
|
+
return {
|
|
345
|
+
source: 'implementedFiles',
|
|
346
|
+
citation: `implementedFiles: ${evidence.implementedFiles.join(', ')}`,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const searchableFields = [
|
|
351
|
+
{ key: 'handoff.salientSummary', text: evidence.handoffSummary },
|
|
352
|
+
{ key: 'handoff.verification', text: evidence.handoffVerification },
|
|
353
|
+
{ key: 'handoff.llmResponsePreview', text: evidence.handoffLlmPreview },
|
|
354
|
+
{ key: 'handoff.discoveredIssues', text: evidence.discoveredIssues },
|
|
355
|
+
{ key: 'stdout', text: evidence.stdout },
|
|
356
|
+
];
|
|
357
|
+
for (const field of searchableFields) {
|
|
358
|
+
const normalized = normalizeEvidenceText(field.text);
|
|
359
|
+
if (keywords.some((keyword) => normalized.includes(keyword))) {
|
|
360
|
+
return {
|
|
361
|
+
source: field.key,
|
|
362
|
+
citation: `${field.key}: ${truncateCitation(field.text)}`,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
function buildErrorEvidence(workerOutput) {
|
|
369
|
+
const parts = [];
|
|
370
|
+
if (typeof workerOutput?.error === 'string' && workerOutput.error) {
|
|
371
|
+
parts.push(`error: ${truncateCitation(workerOutput.error)}`);
|
|
372
|
+
}
|
|
373
|
+
if (typeof workerOutput?.stderr === 'string' && workerOutput.stderr) {
|
|
374
|
+
parts.push(`stderr: ${truncateCitation(workerOutput.stderr)}`);
|
|
375
|
+
}
|
|
376
|
+
const handoff = isPlainObject(workerOutput?.handoff) ? workerOutput.handoff : {};
|
|
377
|
+
if (Array.isArray(handoff.discoveredIssues) && handoff.discoveredIssues.length > 0) {
|
|
378
|
+
const issues = handoff.discoveredIssues
|
|
379
|
+
.map((item) => {
|
|
380
|
+
if (typeof item === 'string')
|
|
381
|
+
return item;
|
|
382
|
+
if (isPlainObject(item) && typeof item.description === 'string')
|
|
383
|
+
return item.description;
|
|
384
|
+
return JSON.stringify(item);
|
|
385
|
+
})
|
|
386
|
+
.join('; ');
|
|
387
|
+
parts.push(`discoveredIssues: ${truncateCitation(issues)}`);
|
|
388
|
+
}
|
|
389
|
+
return parts.length > 0 ? parts.join('; ') : null;
|
|
390
|
+
}
|
|
391
|
+
function buildEvidenceSummary(evidence) {
|
|
392
|
+
const parts = [];
|
|
393
|
+
if (evidence.handoffSummary)
|
|
394
|
+
parts.push(`handoff.salientSummary: ${truncateCitation(evidence.handoffSummary)}`);
|
|
395
|
+
if (evidence.implementedFiles.length > 0) {
|
|
396
|
+
parts.push(`implementedFiles: ${evidence.implementedFiles.join(', ')}`);
|
|
397
|
+
}
|
|
398
|
+
if (evidence.stdout)
|
|
399
|
+
parts.push(`stdout: ${truncateCitation(evidence.stdout)}`);
|
|
400
|
+
if (evidence.stderr)
|
|
401
|
+
parts.push(`stderr: ${truncateCitation(evidence.stderr)}`);
|
|
402
|
+
return parts.length > 0 ? parts.join('; ') : 'Worker output present but no summary fields were extracted.';
|
|
403
|
+
}
|
|
404
|
+
function buildFeatureStatusEvidence(feature) {
|
|
405
|
+
const parts = [`featureId=${feature.featureId}`, `status=${feature.status}`];
|
|
406
|
+
if (feature.description) {
|
|
407
|
+
parts.push(`description=${feature.description.slice(0, 200)}`);
|
|
408
|
+
}
|
|
409
|
+
return parts.join('; ');
|
|
410
|
+
}
|
|
411
|
+
function extractAssertionKeywords(assertionText) {
|
|
412
|
+
const normalized = normalizeEvidenceText(assertionText);
|
|
413
|
+
const stopWords = new Set([
|
|
414
|
+
// Generic modals and auxiliaries
|
|
415
|
+
'should', 'must', 'will', 'would', 'could', 'shall', 'need', 'needs', 'have', 'having',
|
|
416
|
+
'with', 'without', 'from', 'into', 'through', 'during', 'before', 'after', 'above',
|
|
417
|
+
'below', 'between', 'among', 'within', 'across', 'around', 'under', 'over', 'again',
|
|
418
|
+
'further', 'then', 'once', 'here', 'there', 'when', 'where', 'what', 'which', 'while',
|
|
419
|
+
'because', 'until', 'although', 'whether', 'unless', 'since', 'than', 'that', 'this',
|
|
420
|
+
'these', 'those', 'they', 'them', 'their', 'such', 'only', 'also', 'just', 'about',
|
|
421
|
+
'above', 'according', 'accordingly', 'across', 'actually', 'afterwards', 'able',
|
|
422
|
+
// Native mission domain generics that appear in boilerplate
|
|
423
|
+
'worker', 'workers', 'feature', 'features', 'mission', 'missions', 'native', 'natives',
|
|
424
|
+
'complete', 'completed', 'completes', 'completion', 'run', 'runs', 'running', 'execute',
|
|
425
|
+
'executes', 'executed', 'perform', 'performs', 'performed', 'generate', 'generates',
|
|
426
|
+
'generated', 'create', 'creates', 'created', 'write', 'writes', 'written', 'make',
|
|
427
|
+
'makes', 'made', 'implement', 'implements', 'implemented', 'build', 'builds', 'built',
|
|
428
|
+
]);
|
|
429
|
+
const tokens = normalized
|
|
430
|
+
.split(/\s+/)
|
|
431
|
+
.filter((token) => token.length >= 5 && !stopWords.has(token));
|
|
432
|
+
return [...new Set(tokens)];
|
|
433
|
+
}
|
|
434
|
+
function normalizeEvidenceText(value) {
|
|
435
|
+
return value
|
|
436
|
+
.toLowerCase()
|
|
437
|
+
.replace(/[^a-z0-9\/._-]+/g, ' ')
|
|
438
|
+
.replace(/\s+/g, ' ')
|
|
439
|
+
.trim();
|
|
440
|
+
}
|
|
441
|
+
function truncateCitation(value, limit = 300) {
|
|
442
|
+
const trimmed = value.trim();
|
|
443
|
+
if (trimmed.length <= limit)
|
|
444
|
+
return trimmed;
|
|
445
|
+
return `${trimmed.slice(0, limit)}...`;
|
|
446
|
+
}
|
|
447
|
+
function isPlainObject(value) {
|
|
448
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
449
|
+
return false;
|
|
450
|
+
const prototype = Object.getPrototypeOf(value);
|
|
451
|
+
return prototype === Object.prototype || prototype === null;
|
|
452
|
+
}
|