scene-capability-engine 3.6.28 → 3.6.32

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.
@@ -5,6 +5,12 @@ const {
5
5
  ProjectTimelineStore,
6
6
  captureTimelineCheckpoint
7
7
  } = require('../runtime/project-timeline');
8
+ const {
9
+ summarizeTimelineAttention,
10
+ buildTimelineEntryViewModel,
11
+ buildTimelineListViewModel,
12
+ buildTimelineShowViewModel
13
+ } = require('../magicball/timeline-view-model');
8
14
 
9
15
  function normalizeText(value) {
10
16
  if (typeof value !== 'string') {
@@ -44,88 +50,6 @@ function createStore(dependencies = {}) {
44
50
  return dependencies.timelineStore || new ProjectTimelineStore(projectPath, fileSystem);
45
51
  }
46
52
 
47
- function summarizeTimelineAttention(entry = {}) {
48
- const trigger = normalizeText(entry.trigger).toLowerCase();
49
- const git = entry && typeof entry.git === 'object' ? entry.git : {};
50
- if (trigger === 'restore') {
51
- return 'high';
52
- }
53
- if (trigger === 'push') {
54
- return 'medium';
55
- }
56
- if (Number(git.dirty_count || 0) > 0) {
57
- return 'medium';
58
- }
59
- return 'low';
60
- }
61
-
62
- function buildTimelineEntryViewModel(entry = {}) {
63
- const title = normalizeText(entry.summary) || ((normalizeText(entry.trigger) || 'timeline') + ' checkpoint');
64
- const subtitleParts = [
65
- normalizeText(entry.event),
66
- entry.scene_id ? ('scene=' + entry.scene_id) : '',
67
- Number.isFinite(Number(entry.file_count)) ? ('files=' + Number(entry.file_count)) : ''
68
- ].filter(Boolean);
69
- return {
70
- snapshot_id: normalizeText(entry.snapshot_id) || null,
71
- title,
72
- subtitle: subtitleParts.join(' | '),
73
- trigger: normalizeText(entry.trigger) || null,
74
- event: normalizeText(entry.event) || null,
75
- created_at: normalizeText(entry.created_at) || null,
76
- scene_id: normalizeText(entry.scene_id) || null,
77
- session_id: normalizeText(entry.session_id) || null,
78
- file_count: Number.isFinite(Number(entry.file_count)) ? Number(entry.file_count) : 0,
79
- branch: entry && entry.git ? normalizeText(entry.git.branch) || null : null,
80
- head: entry && entry.git ? normalizeText(entry.git.head) || null : null,
81
- dirty_count: entry && entry.git && Number.isFinite(Number(entry.git.dirty_count)) ? Number(entry.git.dirty_count) : 0,
82
- attention_level: summarizeTimelineAttention(entry),
83
- show_command: normalizeText(entry.snapshot_id) ? ('sce timeline show ' + entry.snapshot_id + ' --json') : null,
84
- restore_command: normalizeText(entry.snapshot_id) ? ('sce timeline restore ' + entry.snapshot_id + ' --json') : null
85
- };
86
- }
87
-
88
- function buildTimelineListViewModel(payload = {}) {
89
- const snapshots = Array.isArray(payload.snapshots) ? payload.snapshots : [];
90
- const trigger_counts = {};
91
- let dirty_snapshot_count = 0;
92
- const sceneIds = new Set();
93
- for (const item of snapshots) {
94
- const trigger = normalizeText(item.trigger) || 'unknown';
95
- trigger_counts[trigger] = Number(trigger_counts[trigger] || 0) + 1;
96
- if (item.scene_id) {
97
- sceneIds.add(item.scene_id);
98
- }
99
- if (item.git && item.git.dirty) {
100
- dirty_snapshot_count += 1;
101
- }
102
- }
103
- return {
104
- summary: {
105
- total: Number(payload.total || snapshots.length || 0),
106
- latest_snapshot_id: snapshots[0] ? normalizeText(snapshots[0].snapshot_id) || null : null,
107
- latest_created_at: snapshots[0] ? normalizeText(snapshots[0].created_at) || null : null,
108
- dirty_snapshot_count,
109
- scene_count: sceneIds.size,
110
- trigger_counts
111
- },
112
- entries: snapshots.map((item) => buildTimelineEntryViewModel(item))
113
- };
114
- }
115
-
116
- function buildTimelineShowViewModel(payload = {}) {
117
- const snapshot = payload.snapshot && typeof payload.snapshot === 'object' ? payload.snapshot : {};
118
- const filesPayload = payload.files && typeof payload.files === 'object' ? payload.files : {};
119
- const files = Array.isArray(filesPayload.files) ? filesPayload.files : [];
120
- return {
121
- snapshot: buildTimelineEntryViewModel(snapshot),
122
- files_preview: files.slice(0, 20),
123
- file_preview_count: Math.min(files.length, 20),
124
- file_total: Number(filesPayload.file_count || files.length || 0),
125
- restore_command: snapshot.snapshot_id ? ('sce timeline restore ' + snapshot.snapshot_id + ' --json') : null
126
- };
127
- }
128
-
129
53
  function printPayload(payload, asJson = false, title = 'Timeline') {
130
54
  if (asJson) {
131
55
  console.log(JSON.stringify(payload, null, 2));
@@ -0,0 +1,213 @@
1
+ const { buildMagicballStatusLanguage } = require('./status-language');
2
+
3
+ function normalizeText(value) {
4
+ if (typeof value !== 'string') {
5
+ return '';
6
+ }
7
+ return value.trim();
8
+ }
9
+
10
+ function buildCapabilityInventorySceneAdvice(entry = {}) {
11
+ const sceneId = String(entry && entry.scene_id || 'scene.unknown');
12
+ const releaseUi = entry && entry.release_readiness_ui ? entry.release_readiness_ui : { publish_ready: true, blocking_missing: [] };
13
+ const missing = Array.isArray(releaseUi.blocking_missing) ? releaseUi.blocking_missing : [];
14
+ const valueScore = Number(entry && entry.score_preview && entry.score_preview.value_score || 0);
15
+
16
+ let attentionLevel = 'low';
17
+ let recommendedAction = '可直接发布';
18
+ let blockingSummary = '已满足发布前置条件';
19
+ let nextAction = 'publish';
20
+
21
+ if (!releaseUi.publish_ready) {
22
+ if (missing.includes('decision_strategy')) {
23
+ attentionLevel = 'critical';
24
+ recommendedAction = '补齐决策策略';
25
+ blockingSummary = '缺决策策略,暂不可发布';
26
+ nextAction = 'fill_decision_strategy';
27
+ } else if (missing.includes('business_rules')) {
28
+ attentionLevel = 'high';
29
+ recommendedAction = '补齐业务规则';
30
+ blockingSummary = '缺业务规则,暂不可发布';
31
+ nextAction = 'fill_business_rules';
32
+ } else if (missing.includes('entity_relation')) {
33
+ attentionLevel = 'medium';
34
+ recommendedAction = '补齐实体关系';
35
+ blockingSummary = '缺实体关系,暂不可发布';
36
+ nextAction = 'fill_entity_relation';
37
+ } else {
38
+ attentionLevel = 'medium';
39
+ recommendedAction = '补齐本体能力';
40
+ blockingSummary = '本体能力不完整,暂不可发布';
41
+ nextAction = 'repair_ontology_core';
42
+ }
43
+ } else if (valueScore >= 70) {
44
+ attentionLevel = 'low';
45
+ recommendedAction = '进入模板构建';
46
+ blockingSummary = '能力成熟度较高,可进入模板构建/发布';
47
+ nextAction = 'build_template';
48
+ } else {
49
+ attentionLevel = 'medium';
50
+ recommendedAction = '继续补充任务证据';
51
+ blockingSummary = '已可发布,但建议先补强任务与验证证据';
52
+ nextAction = 'strengthen_evidence';
53
+ }
54
+
55
+ return {
56
+ attention_level: attentionLevel,
57
+ recommended_action: recommendedAction,
58
+ blocking_summary: blockingSummary,
59
+ next_action: nextAction,
60
+ next_command: 'sce capability extract --scene ' + sceneId + ' --json',
61
+ mb_status: buildMagicballStatusLanguage({
62
+ attention_level: attentionLevel,
63
+ status_label: releaseUi.publish_ready ? 'publish_ready' : 'blocked',
64
+ blocking_summary: blockingSummary,
65
+ recommended_action: recommendedAction
66
+ })
67
+ };
68
+ }
69
+
70
+ function buildCapabilityInventorySummaryStats(entries) {
71
+ const items = Array.isArray(entries) ? entries : [];
72
+ const summary = {
73
+ publish_ready_count: 0,
74
+ blocked_count: 0,
75
+ missing_triads: {
76
+ decision_strategy: 0,
77
+ business_rules: 0,
78
+ entity_relation: 0
79
+ }
80
+ };
81
+
82
+ for (const entry of items) {
83
+ const ready = Boolean(entry && entry.release_readiness_ui && entry.release_readiness_ui.publish_ready);
84
+ if (ready) {
85
+ summary.publish_ready_count += 1;
86
+ } else {
87
+ summary.blocked_count += 1;
88
+ }
89
+
90
+ const missing = Array.isArray(entry && entry.release_readiness_ui && entry.release_readiness_ui.blocking_missing)
91
+ ? entry.release_readiness_ui.blocking_missing
92
+ : [];
93
+ for (const triad of Object.keys(summary.missing_triads)) {
94
+ if (missing.includes(triad)) {
95
+ summary.missing_triads[triad] += 1;
96
+ }
97
+ }
98
+ }
99
+
100
+ return summary;
101
+ }
102
+
103
+ function buildCapabilityInventorySummaryRecommendations(entries) {
104
+ const items = Array.isArray(entries) ? entries : [];
105
+ const recommendations = [];
106
+ const blocked = items.filter((item) => !(item && item.release_readiness_ui && item.release_readiness_ui.publish_ready));
107
+ const missingDecision = blocked.filter((item) => Array.isArray(item.release_readiness_ui && item.release_readiness_ui.blocking_missing) && item.release_readiness_ui.blocking_missing.includes('decision_strategy'));
108
+ const missingRules = blocked.filter((item) => Array.isArray(item.release_readiness_ui && item.release_readiness_ui.blocking_missing) && item.release_readiness_ui.blocking_missing.includes('business_rules'));
109
+ const readyScenes = items.filter((item) => item && item.release_readiness_ui && item.release_readiness_ui.publish_ready);
110
+
111
+ if (missingDecision.length > 0) {
112
+ recommendations.push('优先处理缺决策策略的 scene(' + missingDecision.length + ')');
113
+ }
114
+ if (missingRules.length > 0) {
115
+ recommendations.push('其次处理缺业务规则的 scene(' + missingRules.length + ')');
116
+ }
117
+ if (readyScenes.length > 0) {
118
+ recommendations.push('可优先推进可发布 scene 进入模板构建(' + readyScenes.length + ')');
119
+ }
120
+ if (blocked.length === 0 && readyScenes.length === 0 && items.length > 0) {
121
+ recommendations.push('当前 scene 已基本稳定,可继续补强验证证据');
122
+ }
123
+
124
+ return recommendations;
125
+ }
126
+
127
+ function buildCapabilityInventoryQuickFilters(summaryStats) {
128
+ const stats = summaryStats || { blocked_count: 0, missing_triads: {} };
129
+ const filters = [];
130
+ if (Number(stats.blocked_count || 0) > 0) {
131
+ filters.push({ id: 'blocked', label: '不可发布', query: { release_ready: false, missing_triad: null } });
132
+ }
133
+ for (const triad of ['decision_strategy', 'business_rules', 'entity_relation']) {
134
+ if (Number(stats.missing_triads && stats.missing_triads[triad] || 0) > 0) {
135
+ filters.push({ id: 'missing_' + triad, label: '缺' + triad, query: { release_ready: false, missing_triad: triad } });
136
+ }
137
+ }
138
+ if (Number(stats.publish_ready_count || 0) > 0) {
139
+ filters.push({ id: 'ready', label: '可发布', query: { release_ready: true, missing_triad: null } });
140
+ }
141
+ return filters;
142
+ }
143
+
144
+ function resolveCapabilityTriadPriority(entry = {}) {
145
+ const missing = Array.isArray(entry && entry.release_readiness_ui && entry.release_readiness_ui.blocking_missing)
146
+ ? entry.release_readiness_ui.blocking_missing
147
+ : [];
148
+ if (missing.includes('decision_strategy')) {
149
+ return 0;
150
+ }
151
+ if (missing.includes('business_rules')) {
152
+ return 1;
153
+ }
154
+ if (missing.includes('entity_relation')) {
155
+ return 2;
156
+ }
157
+ return 3;
158
+ }
159
+
160
+ function sortCapabilityInventoryEntries(entries) {
161
+ return [...(Array.isArray(entries) ? entries : [])].sort((left, right) => {
162
+ const leftReady = Boolean(left && left.release_readiness_ui && left.release_readiness_ui.publish_ready);
163
+ const rightReady = Boolean(right && right.release_readiness_ui && right.release_readiness_ui.publish_ready);
164
+ if (leftReady !== rightReady) {
165
+ return leftReady ? 1 : -1;
166
+ }
167
+
168
+ const triadDelta = resolveCapabilityTriadPriority(left) - resolveCapabilityTriadPriority(right);
169
+ if (triadDelta !== 0) {
170
+ return triadDelta;
171
+ }
172
+
173
+ const leftValue = Number(left && left.score_preview && left.score_preview.value_score || 0);
174
+ const rightValue = Number(right && right.score_preview && right.score_preview.value_score || 0);
175
+ if (leftValue !== rightValue) {
176
+ return rightValue - leftValue;
177
+ }
178
+
179
+ return String(left && left.scene_id || '').localeCompare(String(right && right.scene_id || ''));
180
+ });
181
+ }
182
+
183
+ function filterCapabilityInventoryEntries(entries, options = {}) {
184
+ const normalizedMissingTriad = normalizeText(options.missingTriad || options.missing_triad).toLowerCase();
185
+ const releaseReadyFilter = normalizeText(options.releaseReady || options.release_ready).toLowerCase();
186
+ return (Array.isArray(entries) ? entries : []).filter((entry) => {
187
+ if (releaseReadyFilter) {
188
+ const expected = ['1', 'true', 'yes', 'ready'].includes(releaseReadyFilter);
189
+ if (Boolean(entry.release_readiness_ui && entry.release_readiness_ui.publish_ready) !== expected) {
190
+ return false;
191
+ }
192
+ }
193
+ if (normalizedMissingTriad) {
194
+ const missing = Array.isArray(entry.release_readiness_ui && entry.release_readiness_ui.blocking_missing)
195
+ ? entry.release_readiness_ui.blocking_missing
196
+ : [];
197
+ if (!missing.includes(normalizedMissingTriad)) {
198
+ return false;
199
+ }
200
+ }
201
+ return true;
202
+ });
203
+ }
204
+
205
+ module.exports = {
206
+ buildCapabilityInventorySceneAdvice,
207
+ buildCapabilityInventorySummaryStats,
208
+ buildCapabilityInventorySummaryRecommendations,
209
+ buildCapabilityInventoryQuickFilters,
210
+ resolveCapabilityTriadPriority,
211
+ sortCapabilityInventoryEntries,
212
+ filterCapabilityInventoryEntries
213
+ };
@@ -0,0 +1,29 @@
1
+ const TONE_BY_ATTENTION = Object.freeze({
2
+ critical: 'danger',
3
+ high: 'warning',
4
+ medium: 'info',
5
+ low: 'success'
6
+ });
7
+
8
+ function normalizeText(value) {
9
+ if (typeof value !== 'string') {
10
+ return '';
11
+ }
12
+ return value.trim();
13
+ }
14
+
15
+ function buildMagicballStatusLanguage(input = {}) {
16
+ const attention = normalizeText(input.attention_level) || 'medium';
17
+ return {
18
+ attention_level: attention,
19
+ status_tone: TONE_BY_ATTENTION[attention] || 'info',
20
+ status_label: normalizeText(input.status_label) || null,
21
+ blocking_summary: normalizeText(input.blocking_summary) || null,
22
+ recommended_action: normalizeText(input.recommended_action) || null
23
+ };
24
+ }
25
+
26
+ module.exports = {
27
+ buildMagicballStatusLanguage,
28
+ TONE_BY_ATTENTION
29
+ };
@@ -0,0 +1,113 @@
1
+ const { buildMagicballStatusLanguage } = require('./status-language');
2
+
3
+ function normalizeString(value) {
4
+ if (typeof value !== 'string') {
5
+ return '';
6
+ }
7
+ return value.trim();
8
+ }
9
+
10
+ function buildTaskFeedbackModel(payload = {}) {
11
+ const task = payload && payload.task && typeof payload.task === 'object' ? payload.task : {};
12
+ const handoff = task && typeof task.handoff === 'object' ? task.handoff : {};
13
+ const errors = Array.isArray(task.errors) ? task.errors : [];
14
+ const commands = Array.isArray(task.commands) ? task.commands : [];
15
+ const fileChanges = Array.isArray(task.file_changes) ? task.file_changes : [];
16
+ const evidence = Array.isArray(task.evidence) ? task.evidence : [];
17
+ const acceptance = Array.isArray(task.acceptance_criteria) ? task.acceptance_criteria : [];
18
+ const firstError = errors[0] || {};
19
+ const stage = normalizeString(handoff.stage) || 'task';
20
+ const status = normalizeString(task.status) || 'unknown';
21
+ const problemComponent = normalizeString(handoff.component) || normalizeString(payload.sceneId) || null;
22
+ const expected = normalizeString(acceptance[0]) || ('Complete ' + stage + ' stage successfully');
23
+ const actual = normalizeString(firstError.message) || ('Current status: ' + status);
24
+
25
+ let chainCheckpoint = 'task-envelope';
26
+ if (errors.length > 0 && commands.length > 0) {
27
+ chainCheckpoint = 'command-execution';
28
+ } else if (errors.length > 0) {
29
+ chainCheckpoint = 'stage-gate';
30
+ } else if (fileChanges.length > 0) {
31
+ chainCheckpoint = 'patch-applied';
32
+ } else if (evidence.length > 0) {
33
+ chainCheckpoint = 'evidence-collected';
34
+ }
35
+
36
+ let confidence = 'low';
37
+ if (errors.length > 0) {
38
+ confidence = 'medium';
39
+ } else if (status === 'completed') {
40
+ confidence = 'high';
41
+ }
42
+
43
+ let recommendedAction = '继续当前阶段';
44
+ if (errors.length > 0) {
45
+ recommendedAction = '处理阻断后重试';
46
+ } else if (status === 'completed' && normalizeString(task.next_action) && normalizeString(task.next_action) !== 'complete') {
47
+ recommendedAction = '执行下一阶段';
48
+ } else if (status === 'completed') {
49
+ recommendedAction = '任务完成';
50
+ }
51
+
52
+ const model = {
53
+ version: '1.0',
54
+ problem: {
55
+ component: problemComponent,
56
+ action: normalizeString(handoff.action) || stage,
57
+ expected,
58
+ actual
59
+ },
60
+ execution: {
61
+ stage,
62
+ status,
63
+ summary: Array.isArray(task.summary) ? task.summary.slice(0, 3) : [],
64
+ blocking_summary: normalizeString(handoff.blocking_summary) || normalizeString(firstError.message) || null
65
+ },
66
+ diagnosis: {
67
+ hypothesis: normalizeString(firstError.error_bundle) || normalizeString(firstError.message) || normalizeString(handoff.reason) || null,
68
+ chain_checkpoint: chainCheckpoint,
69
+ root_cause_confidence: confidence
70
+ },
71
+ evidence: {
72
+ file_count: fileChanges.length,
73
+ file_paths: fileChanges.slice(0, 5).map((item) => normalizeString(item && item.path)).filter(Boolean),
74
+ command_count: commands.length,
75
+ error_count: errors.length,
76
+ verification_result: status === 'completed' ? 'passed-or-advanced' : (errors.length > 0 ? 'blocked' : 'in-progress'),
77
+ regression_scope: Array.isArray(handoff.regression_scope) ? handoff.regression_scope : []
78
+ },
79
+ next_step: {
80
+ recommended_action: recommendedAction,
81
+ next_action: normalizeString(task.next_action) || null,
82
+ next_command: normalizeString(task.next_action) || null
83
+ }
84
+ };
85
+
86
+ model.mb_status = buildMagicballStatusLanguage({
87
+ status: model.execution.status,
88
+ attention_level: model.diagnosis.root_cause_confidence === 'high' ? 'high' : (model.execution.status === 'completed' ? 'low' : 'medium'),
89
+ status_label: model.execution.status,
90
+ blocking_summary: model.execution.blocking_summary,
91
+ recommended_action: model.next_step.recommended_action
92
+ });
93
+
94
+ return model;
95
+ }
96
+
97
+ function attachTaskFeedbackModel(payload = {}) {
98
+ if (!payload || typeof payload !== 'object' || !payload.task || typeof payload.task !== 'object') {
99
+ return payload;
100
+ }
101
+ return {
102
+ ...payload,
103
+ task: {
104
+ ...payload.task,
105
+ feedback_model: buildTaskFeedbackModel(payload)
106
+ }
107
+ };
108
+ }
109
+
110
+ module.exports = {
111
+ buildTaskFeedbackModel,
112
+ attachTaskFeedbackModel
113
+ };
@@ -0,0 +1,95 @@
1
+ function normalizeText(value) {
2
+ if (typeof value !== 'string') {
3
+ return '';
4
+ }
5
+ return value.trim();
6
+ }
7
+
8
+ function summarizeTimelineAttention(entry = {}) {
9
+ const trigger = normalizeText(entry.trigger).toLowerCase();
10
+ const git = entry && typeof entry.git === 'object' ? entry.git : {};
11
+ if (trigger === 'restore') {
12
+ return 'high';
13
+ }
14
+ if (trigger === 'push') {
15
+ return 'medium';
16
+ }
17
+ if (Number(git.dirty_count || 0) > 0) {
18
+ return 'medium';
19
+ }
20
+ return 'low';
21
+ }
22
+
23
+ function buildTimelineEntryViewModel(entry = {}) {
24
+ const title = normalizeText(entry.summary) || ((normalizeText(entry.trigger) || 'timeline') + ' checkpoint');
25
+ const subtitleParts = [
26
+ normalizeText(entry.event),
27
+ entry.scene_id ? ('scene=' + entry.scene_id) : '',
28
+ Number.isFinite(Number(entry.file_count)) ? ('files=' + Number(entry.file_count)) : ''
29
+ ].filter(Boolean);
30
+ return {
31
+ snapshot_id: normalizeText(entry.snapshot_id) || null,
32
+ title,
33
+ subtitle: subtitleParts.join(' | '),
34
+ trigger: normalizeText(entry.trigger) || null,
35
+ event: normalizeText(entry.event) || null,
36
+ created_at: normalizeText(entry.created_at) || null,
37
+ scene_id: normalizeText(entry.scene_id) || null,
38
+ session_id: normalizeText(entry.session_id) || null,
39
+ file_count: Number.isFinite(Number(entry.file_count)) ? Number(entry.file_count) : 0,
40
+ branch: entry && entry.git ? normalizeText(entry.git.branch) || null : null,
41
+ head: entry && entry.git ? normalizeText(entry.git.head) || null : null,
42
+ dirty_count: entry && entry.git && Number.isFinite(Number(entry.git.dirty_count)) ? Number(entry.git.dirty_count) : 0,
43
+ attention_level: summarizeTimelineAttention(entry),
44
+ show_command: normalizeText(entry.snapshot_id) ? ('sce timeline show ' + entry.snapshot_id + ' --json') : null,
45
+ restore_command: normalizeText(entry.snapshot_id) ? ('sce timeline restore ' + entry.snapshot_id + ' --json') : null
46
+ };
47
+ }
48
+
49
+ function buildTimelineListViewModel(payload = {}) {
50
+ const snapshots = Array.isArray(payload.snapshots) ? payload.snapshots : [];
51
+ const trigger_counts = {};
52
+ let dirty_snapshot_count = 0;
53
+ const sceneIds = new Set();
54
+ for (const item of snapshots) {
55
+ const trigger = normalizeText(item.trigger) || 'unknown';
56
+ trigger_counts[trigger] = Number(trigger_counts[trigger] || 0) + 1;
57
+ if (item.scene_id) {
58
+ sceneIds.add(item.scene_id);
59
+ }
60
+ if (item.git && item.git.dirty) {
61
+ dirty_snapshot_count += 1;
62
+ }
63
+ }
64
+ return {
65
+ summary: {
66
+ total: Number(payload.total || snapshots.length || 0),
67
+ latest_snapshot_id: snapshots[0] ? normalizeText(snapshots[0].snapshot_id) || null : null,
68
+ latest_created_at: snapshots[0] ? normalizeText(snapshots[0].created_at) || null : null,
69
+ dirty_snapshot_count,
70
+ scene_count: sceneIds.size,
71
+ trigger_counts
72
+ },
73
+ entries: snapshots.map((item) => buildTimelineEntryViewModel(item))
74
+ };
75
+ }
76
+
77
+ function buildTimelineShowViewModel(payload = {}) {
78
+ const snapshot = payload.snapshot && typeof payload.snapshot === 'object' ? payload.snapshot : {};
79
+ const filesPayload = payload.files && typeof payload.files === 'object' ? payload.files : {};
80
+ const files = Array.isArray(filesPayload.files) ? filesPayload.files : [];
81
+ return {
82
+ snapshot: buildTimelineEntryViewModel(snapshot),
83
+ files_preview: files.slice(0, 20),
84
+ file_preview_count: Math.min(files.length, 20),
85
+ file_total: Number(filesPayload.file_count || files.length || 0),
86
+ restore_command: snapshot.snapshot_id ? ('sce timeline restore ' + snapshot.snapshot_id + ' --json') : null
87
+ };
88
+ }
89
+
90
+ module.exports = {
91
+ summarizeTimelineAttention,
92
+ buildTimelineEntryViewModel,
93
+ buildTimelineListViewModel,
94
+ buildTimelineShowViewModel
95
+ };