scene-capability-engine 3.6.65 → 3.6.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +17 -6
  3. package/README.zh.md +18 -6
  4. package/bin/scene-capability-engine.js +4 -0
  5. package/docs/README.md +2 -2
  6. package/docs/command-reference.md +382 -6
  7. package/docs/document-governance.md +3 -2
  8. package/docs/integration-modes.md +62 -478
  9. package/docs/integration-philosophy.md +56 -263
  10. package/docs/magicball-project-portfolio-contract.md +114 -2
  11. package/docs/project-management/README.md +14 -0
  12. package/docs/project-management/assurance/backup.md +3 -0
  13. package/docs/project-management/assurance/config.md +3 -0
  14. package/docs/project-management/assurance/evidence/README.md +3 -0
  15. package/docs/project-management/assurance/incidents/README.md +3 -0
  16. package/docs/project-management/assurance/logs.md +3 -0
  17. package/docs/project-management/assurance/overview.md +3 -0
  18. package/docs/project-management/assurance/recovery/README.md +3 -0
  19. package/docs/project-management/assurance/resource.md +3 -0
  20. package/docs/project-management/assurance/runbooks/README.md +3 -0
  21. package/docs/project-management/delivery/acceptance/README.md +3 -0
  22. package/docs/project-management/delivery/acceptance/evidence/README.md +3 -0
  23. package/docs/project-management/delivery/acceptance/exceptions/README.md +3 -0
  24. package/docs/project-management/delivery/acceptance/reports/README.md +3 -0
  25. package/docs/project-management/delivery/documents/changes.md +3 -0
  26. package/docs/project-management/delivery/documents/issues.md +3 -0
  27. package/docs/project-management/delivery/documents/overview.md +3 -0
  28. package/docs/project-management/delivery/documents/planning.md +3 -0
  29. package/docs/project-management/delivery/documents/requirements.md +3 -0
  30. package/docs/project-management/delivery/documents/tracking.md +3 -0
  31. package/docs/project-management/delivery/handoffs/README.md +3 -0
  32. package/docs/project-management/delivery/handoffs/evidence/README.md +3 -0
  33. package/docs/project-management/delivery/handoffs/records/README.md +3 -0
  34. package/docs/project-management/delivery/overview.md +10 -0
  35. package/docs/project-management/delivery/releases/README.md +3 -0
  36. package/docs/project-management/delivery/releases/baselines/README.md +3 -0
  37. package/docs/project-management/delivery/releases/evidence/README.md +3 -0
  38. package/docs/project-management/delivery/tables/changes.md +3 -0
  39. package/docs/project-management/delivery/tables/issues.md +3 -0
  40. package/docs/project-management/delivery/tables/planning.md +3 -0
  41. package/docs/project-management/delivery/tables/requirements.md +3 -0
  42. package/docs/project-management/delivery/tables/tracking.md +3 -0
  43. package/docs/project-management/environment/agent-discovery.md +3 -0
  44. package/docs/project-management/environment/development.md +3 -0
  45. package/docs/project-management/environment/overview.md +10 -0
  46. package/docs/project-management/environment/testing.md +3 -0
  47. package/docs/project-management/environment/version-alignment.md +3 -0
  48. package/docs/quick-start-with-ai-tools.md +68 -308
  49. package/docs/releases/README.md +2 -0
  50. package/docs/releases/v3.6.66.md +23 -0
  51. package/docs/releases/v3.6.67.md +23 -0
  52. package/docs/steering-governance.md +64 -2
  53. package/docs/zh/README.md +2 -2
  54. package/docs/zh/releases/README.md +2 -0
  55. package/docs/zh/releases/v3.6.66.md +23 -0
  56. package/docs/zh/releases/v3.6.67.md +23 -0
  57. package/lib/commands/adopt.js +24 -0
  58. package/lib/commands/native.js +158 -0
  59. package/lib/commands/project.js +95 -0
  60. package/lib/commands/semantic.js +1459 -0
  61. package/lib/commands/session.js +74 -3
  62. package/lib/commands/spec-bootstrap.js +10 -1
  63. package/lib/commands/spec-gate.js +10 -1
  64. package/lib/commands/spec-pipeline.js +10 -1
  65. package/lib/commands/studio.js +405 -30
  66. package/lib/commands/task.js +141 -7
  67. package/lib/governance/supreme-principles.js +530 -0
  68. package/lib/problem/problem-evaluator.js +4 -0
  69. package/lib/project/candidate-inspection-service.js +24 -1
  70. package/lib/project/portfolio-projection-service.js +315 -5
  71. package/lib/project/project-channel-output.js +94 -0
  72. package/lib/project/project-channel-projection.js +181 -0
  73. package/lib/project/root-onboarding-service.js +60 -8
  74. package/lib/project/semantic-shared-source-projection.js +150 -0
  75. package/lib/project/supervision-action-model.js +277 -0
  76. package/lib/project/supervision-projection-service.js +305 -5
  77. package/lib/project/target-resolution-service.js +70 -5
  78. package/lib/project/visibility-policy.js +93 -0
  79. package/lib/runtime/multi-spec-scene-session.js +8 -1
  80. package/lib/runtime/project-channel-context-store.js +387 -0
  81. package/lib/runtime/project-channel-context.js +406 -0
  82. package/lib/runtime/scene-session-binding.js +46 -0
  83. package/lib/runtime/session-store.js +186 -0
  84. package/lib/runtime/steering-contract.js +7 -1
  85. package/lib/semantic/archive-report.js +283 -0
  86. package/lib/semantic/archive-routing.js +67 -0
  87. package/lib/semantic/backflow-report.js +245 -0
  88. package/lib/semantic/capability-contract.js +30 -0
  89. package/lib/semantic/delta-export.js +145 -0
  90. package/lib/semantic/interaction-observer.js +254 -0
  91. package/lib/semantic/kernel-loader.js +881 -0
  92. package/lib/semantic/native-runtime.js +359 -0
  93. package/lib/semantic/progress-ledger.js +433 -0
  94. package/lib/semantic/replay-evaluator.js +382 -0
  95. package/lib/semantic/shared-publication.js +592 -0
  96. package/lib/semantic/shared-source-config.js +183 -0
  97. package/lib/semantic/shared-source-connect.js +139 -0
  98. package/lib/semantic/shared-source-discovery.js +98 -0
  99. package/lib/semantic/shared-sync-export.js +413 -0
  100. package/lib/semantic/shared-sync-intake.js +592 -0
  101. package/lib/semantic/shared-sync-merge.js +547 -0
  102. package/lib/semantic/shared-sync-release.js +463 -0
  103. package/lib/semantic/supreme-intent-report.js +300 -0
  104. package/lib/state/sce-state-store.js +1360 -0
  105. package/lib/steering/context-sync-manager.js +276 -25
  106. package/lib/studio/spec-intake-governor.js +39 -3
  107. package/lib/studio/task-envelope.js +35 -2
  108. package/lib/workspace/takeover-baseline.js +342 -83
  109. package/package.json +7 -2
  110. package/scripts/agent-governance-baseline-audit.js +395 -0
  111. package/scripts/clarification-first-audit.js +9 -9
  112. package/scripts/deprecated-entry-audit.js +240 -0
  113. package/scripts/release-posture-report.js +262 -0
  114. package/template/.sce/README.md +62 -228
  115. package/template/.sce/config/semantic-shared-sources.json +5 -0
  116. package/template/.sce/config/supreme-principles-policy.json +105 -0
  117. package/template/.sce/config/takeover-baseline.json +7 -0
  118. package/template/.sce/steering/CORE_PRINCIPLES.md +23 -63
  119. package/template/.sce/steering/CURRENT_CONTEXT.md +4 -0
  120. package/template/.sce/steering/RULES_GUIDE.md +17 -9
  121. package/template/README.md +32 -96
@@ -0,0 +1,181 @@
1
+ const fs = require('fs-extra');
2
+ const { ProjectChannelContextStore } = require('../runtime/project-channel-context-store');
3
+
4
+ function normalizeString(value) {
5
+ if (typeof value !== 'string') {
6
+ return '';
7
+ }
8
+ return value.trim();
9
+ }
10
+
11
+ function safeIsoAt(value) {
12
+ const normalized = normalizeString(value);
13
+ if (!normalized) {
14
+ return null;
15
+ }
16
+ const time = Date.parse(normalized);
17
+ return Number.isFinite(time) ? new Date(time).toISOString() : null;
18
+ }
19
+
20
+ function buildEmptyProjectChannelProjection() {
21
+ return {
22
+ summary: {
23
+ available: false,
24
+ contextProjectId: null,
25
+ canonicalProjectIdMatched: false,
26
+ focusedChannelId: null,
27
+ channelCount: 0,
28
+ storageMode: 'none'
29
+ },
30
+ resolvedChannel: null,
31
+ channels: [],
32
+ partial: false,
33
+ partialReasons: []
34
+ };
35
+ }
36
+
37
+ function sortChannels(channels = [], focusedChannelId = '') {
38
+ const normalizedFocusedChannelId = normalizeString(focusedChannelId);
39
+ return [...channels].sort((left, right) => {
40
+ const leftFocused = normalizeString(left && left.channelId) === normalizedFocusedChannelId ? 0 : 1;
41
+ const rightFocused = normalizeString(right && right.channelId) === normalizedFocusedChannelId ? 0 : 1;
42
+ if (leftFocused !== rightFocused) {
43
+ return leftFocused - rightFocused;
44
+ }
45
+ return `${left && left.channelId ? left.channelId : ''}`.localeCompare(`${right && right.channelId ? right.channelId : ''}`);
46
+ });
47
+ }
48
+
49
+ function buildChannelSummary(channel = {}, focusedChannelId = '') {
50
+ const channelId = normalizeString(channel.channelId);
51
+ return {
52
+ channelId,
53
+ focused: channelId === normalizeString(focusedChannelId),
54
+ activeScene: normalizeString(channel.activeScene) || null,
55
+ activeSpecId: normalizeString(channel.activeSpecId) || null,
56
+ activeDoc: normalizeString(channel.activeDoc) || null,
57
+ activeSessionPath: normalizeString(channel.activeSessionPath) || null,
58
+ runState: normalizeString(channel.runState) || null,
59
+ updatedAt: safeIsoAt(channel.updatedAt),
60
+ ...(channel.agentRuntime && typeof channel.agentRuntime === 'object'
61
+ ? {
62
+ agentRuntime: {
63
+ agentId: normalizeString(channel.agentRuntime.agentId) || null,
64
+ backendProfile: normalizeString(channel.agentRuntime.backendProfile) || null,
65
+ runtimeSessionId: normalizeString(channel.agentRuntime.runtimeSessionId) || null
66
+ }
67
+ }
68
+ : {})
69
+ };
70
+ }
71
+
72
+ async function listPersistedProjectChannelIds(projectRoot, fileSystem = fs) {
73
+ const store = new ProjectChannelContextStore(projectRoot, fileSystem);
74
+ const contexts = await store.listPersistedContexts();
75
+ return contexts.map((item) => item.projectId);
76
+ }
77
+
78
+ function chooseContextProjectId(projectIds = [], preferredProjectIds = []) {
79
+ const preferred = preferredProjectIds
80
+ .map((value) => normalizeString(value))
81
+ .filter(Boolean);
82
+ for (const preferredProjectId of preferred) {
83
+ if (projectIds.includes(preferredProjectId)) {
84
+ return {
85
+ projectId: preferredProjectId,
86
+ ambiguous: false
87
+ };
88
+ }
89
+ }
90
+ if (projectIds.length === 1) {
91
+ return {
92
+ projectId: projectIds[0],
93
+ ambiguous: false
94
+ };
95
+ }
96
+ return {
97
+ projectId: null,
98
+ ambiguous: projectIds.length > 1
99
+ };
100
+ }
101
+
102
+ async function buildProjectChannelProjection(projectRoot, options = {}, dependencies = {}) {
103
+ const fileSystem = dependencies.fileSystem || fs;
104
+ const store = dependencies.projectChannelContextStore || new ProjectChannelContextStore(projectRoot, fileSystem);
105
+ const preferredProjectIds = Array.isArray(options.preferredProjectIds)
106
+ ? options.preferredProjectIds
107
+ : [];
108
+ const requestedChannelId = normalizeString(options.channelId);
109
+ const partialReasons = [];
110
+
111
+ let projectIds = [];
112
+ let persistedContexts = [];
113
+ try {
114
+ persistedContexts = await store.listPersistedContexts();
115
+ projectIds = persistedContexts.map((item) => item.projectId);
116
+ } catch (_error) {
117
+ partialReasons.push('project_channel_context_unavailable');
118
+ }
119
+
120
+ const resolution = chooseContextProjectId(projectIds, preferredProjectIds);
121
+ if (resolution.ambiguous) {
122
+ partialReasons.push('project_channel_context_ambiguous');
123
+ }
124
+ if (!resolution.projectId) {
125
+ return {
126
+ ...buildEmptyProjectChannelProjection(),
127
+ partial: partialReasons.length > 0,
128
+ partialReasons
129
+ };
130
+ }
131
+
132
+ let context;
133
+ try {
134
+ context = await store.load(resolution.projectId, {
135
+ allowDefaultChannelSynthesis: false
136
+ });
137
+ } catch (_error) {
138
+ return {
139
+ ...buildEmptyProjectChannelProjection(),
140
+ partial: true,
141
+ partialReasons: [...partialReasons, 'project_channel_context_unavailable']
142
+ };
143
+ }
144
+
145
+ const focusedChannelId = normalizeString(context.focusedChannelId) || null;
146
+ const channels = sortChannels(
147
+ Object.values(context.channels || {}).map((channel) => buildChannelSummary(channel, focusedChannelId)),
148
+ focusedChannelId
149
+ );
150
+ if (channels.length === 0) {
151
+ partialReasons.push('project_channel_context_empty');
152
+ }
153
+ const resolvedChannel = channels.find((channel) => channel.channelId === requestedChannelId)
154
+ || channels.find((channel) => channel.focused)
155
+ || null;
156
+ const contextProjectId = normalizeString(context.projectId) || resolution.projectId;
157
+ const canonicalProjectIdMatched = preferredProjectIds.includes(contextProjectId);
158
+ const storageRecord = persistedContexts.find((item) => item.projectId === contextProjectId)
159
+ || persistedContexts.find((item) => item.projectId === resolution.projectId)
160
+ || null;
161
+
162
+ return {
163
+ summary: {
164
+ available: true,
165
+ contextProjectId,
166
+ canonicalProjectIdMatched,
167
+ focusedChannelId,
168
+ channelCount: channels.length,
169
+ storageMode: storageRecord ? storageRecord.storageMode : 'unknown'
170
+ },
171
+ resolvedChannel,
172
+ channels,
173
+ partial: partialReasons.length > 0,
174
+ partialReasons
175
+ };
176
+ }
177
+
178
+ module.exports = {
179
+ buildEmptyProjectChannelProjection,
180
+ buildProjectChannelProjection
181
+ };
@@ -3,6 +3,9 @@ const fs = require('fs-extra');
3
3
  const WorkspaceStateManager = require('../workspace/multi/workspace-state-manager');
4
4
  const SmartOrchestrator = require('../adoption/smart-orchestrator');
5
5
  const { applyTakeoverBaseline } = require('../workspace/takeover-baseline');
6
+ const { discoverSemanticSharedSourceDescriptors } = require('../semantic/shared-source-discovery');
7
+ const { buildProjectChannelProjection } = require('./project-channel-projection');
8
+ const { buildProjectChannelOutputFromProjection } = require('./project-channel-output');
6
9
  const {
7
10
  PROJECT_CANDIDATE_REASON_CODES,
8
11
  inspectProjectCandidate
@@ -52,6 +55,19 @@ function buildPublication(preview = {}, options = {}) {
52
55
  };
53
56
  }
54
57
 
58
+ function buildSemanticSharedSourceDiscoverySummary(discovery = {}) {
59
+ const summary = discovery && typeof discovery.summary === 'object'
60
+ ? discovery.summary
61
+ : {};
62
+ return {
63
+ totalDescriptors: Number.isFinite(summary.total) ? summary.total : 0,
64
+ approvedDescriptors: Number.isFinite(summary.approved) ? summary.approved : 0,
65
+ blockedDescriptors: Number.isFinite(summary.blocked) ? summary.blocked : 0,
66
+ items: Array.isArray(discovery.items) ? discovery.items : [],
67
+ blocked: Array.isArray(discovery.blocked) ? discovery.blocked : []
68
+ };
69
+ }
70
+
55
71
  function buildWorkspaceNameCandidate(rootDir) {
56
72
  const base = path.basename(rootDir).trim().toLowerCase();
57
73
  const normalized = base.replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
@@ -129,29 +145,43 @@ async function runProjectRootOnboardingImport(options = {}, dependencies = {}) {
129
145
  const steps = [];
130
146
 
131
147
  if (rootInspection.kind === 'invalid') {
148
+ const blockedReasonCode = rootInspection.reasonCodes.includes(PROJECT_CANDIDATE_REASON_CODES.EPHEMERAL_ROOT)
149
+ ? PROJECT_CANDIDATE_REASON_CODES.EPHEMERAL_ROOT
150
+ : PROJECT_ONBOARDING_REASON_CODES.BLOCKED_BY_CANDIDATE;
151
+ const attachDetail = rootInspection.reasonCodes.includes(PROJECT_CANDIDATE_REASON_CODES.EPHEMERAL_ROOT)
152
+ ? 'Local root is reserved for ephemeral probe/import flow use and cannot become a managed project.'
153
+ : 'Local root is not accessible as a project directory.';
154
+ const publishDetail = rootInspection.reasonCodes.includes(PROJECT_CANDIDATE_REASON_CODES.EPHEMERAL_ROOT)
155
+ ? 'Canonical portfolio publication is blocked for ephemeral probe roots.'
156
+ : 'Canonical portfolio publication is blocked until the root is valid.';
157
+ const failureDetail = rootInspection.reasonCodes.includes(PROJECT_CANDIDATE_REASON_CODES.EPHEMERAL_ROOT)
158
+ ? 'Root inspection reported an ephemeral probe root that must not be imported.'
159
+ : 'Root inspection reported an invalid candidate state.';
132
160
  steps.push(buildStep(
133
161
  'register',
134
162
  'failed',
135
163
  'Root directory cannot be onboarded.',
136
- PROJECT_ONBOARDING_REASON_CODES.BLOCKED_BY_CANDIDATE
164
+ blockedReasonCode
137
165
  ));
138
166
  steps.push(buildStep(
139
167
  'attach',
140
168
  'failed',
141
- 'Local root is not accessible as a project directory.',
142
- PROJECT_CANDIDATE_REASON_CODES.ROOT_INACCESSIBLE
169
+ attachDetail,
170
+ rootInspection.reasonCodes.includes(PROJECT_CANDIDATE_REASON_CODES.EPHEMERAL_ROOT)
171
+ ? PROJECT_CANDIDATE_REASON_CODES.EPHEMERAL_ROOT
172
+ : PROJECT_CANDIDATE_REASON_CODES.ROOT_INACCESSIBLE
143
173
  ));
144
174
  steps.push(buildStep(
145
175
  'hydrate',
146
176
  'failed',
147
177
  'Onboarding cannot continue until the root is valid.',
148
- PROJECT_ONBOARDING_REASON_CODES.BLOCKED_BY_CANDIDATE
178
+ blockedReasonCode
149
179
  ));
150
180
  steps.push(buildStep(
151
181
  'publish',
152
182
  'skipped',
153
- 'Canonical portfolio publication is blocked until the root is valid.',
154
- PROJECT_ONBOARDING_REASON_CODES.BLOCKED_BY_CANDIDATE
183
+ publishDetail,
184
+ blockedReasonCode
155
185
  ));
156
186
  steps.push(buildStep(
157
187
  'activate',
@@ -168,8 +198,8 @@ async function runProjectRootOnboardingImport(options = {}, dependencies = {}) {
168
198
  return buildFailureEnvelope(
169
199
  rootInspection,
170
200
  steps,
171
- 'Root inspection reported an invalid candidate state.',
172
- PROJECT_ONBOARDING_REASON_CODES.BLOCKED_BY_CANDIDATE
201
+ failureDetail,
202
+ blockedReasonCode
173
203
  );
174
204
  }
175
205
 
@@ -358,6 +388,17 @@ async function runProjectRootOnboardingImport(options = {}, dependencies = {}) {
358
388
  : PROJECT_ONBOARDING_REASON_CODES.SCAFFOLD_REUSED
359
389
  ));
360
390
 
391
+ const semanticSharedSourceDiscovery = buildSemanticSharedSourceDiscoverySummary(
392
+ await discoverSemanticSharedSourceDescriptors(rootInspection.rootDir, {
393
+ fileSystem
394
+ })
395
+ );
396
+ const projectChannelProjection = await buildProjectChannelProjection(rootInspection.rootDir, {
397
+ preferredProjectIds: [onboardingPreview.projectId, onboardingPreview.workspaceId]
398
+ }, {
399
+ fileSystem
400
+ });
401
+
361
402
  return {
362
403
  mode: 'import',
363
404
  generated_at: publishedAt,
@@ -369,6 +410,17 @@ async function runProjectRootOnboardingImport(options = {}, dependencies = {}) {
369
410
  visibleInPortfolio: true,
370
411
  publishedAt
371
412
  }),
413
+ semanticSharedSourceDiscovery,
414
+ projectChannelContext: {
415
+ ...projectChannelProjection.summary
416
+ },
417
+ ...(projectChannelProjection.resolvedChannel
418
+ ? {
419
+ projectChannel: buildProjectChannelOutputFromProjection(projectChannelProjection, {
420
+ canonicalProjectId: onboardingPreview.projectId || null
421
+ })
422
+ }
423
+ : {}),
372
424
  steps,
373
425
  result: {
374
426
  rootDir: onboardingPreview.rootDir,
@@ -0,0 +1,150 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const { listSemanticSharedSources } = require('../semantic/shared-source-config');
4
+ const { discoverSemanticSharedSourceDescriptors } = require('../semantic/shared-source-discovery');
5
+
6
+ function normalizeString(value) {
7
+ if (typeof value !== 'string') {
8
+ return '';
9
+ }
10
+ return value.trim();
11
+ }
12
+
13
+ function buildSourceKey(name = '', bundle = '') {
14
+ const normalizedName = normalizeString(name) || 'semantic-shared';
15
+ const normalizedBundle = normalizeString(bundle);
16
+ return `${normalizedName}::${normalizedBundle}`;
17
+ }
18
+
19
+ function buildConfiguredSourceIndexes(items = []) {
20
+ const byKey = new Set();
21
+ const byName = new Set();
22
+ for (const item of items) {
23
+ const name = normalizeString(item && item.name) || 'semantic-shared';
24
+ const bundle = normalizeString(item && item.bundle);
25
+ byName.add(name);
26
+ if (bundle) {
27
+ byKey.add(buildSourceKey(name, bundle));
28
+ }
29
+ }
30
+ return {
31
+ byKey,
32
+ byName
33
+ };
34
+ }
35
+
36
+ function isDescriptorConnected(item = {}, configuredIndexes = {}) {
37
+ if (!item.approved) {
38
+ return false;
39
+ }
40
+ const name = normalizeString(item.source_name) || 'semantic-shared';
41
+ const bundle = normalizeString(item.bundle);
42
+ if (bundle && configuredIndexes.byKey instanceof Set && configuredIndexes.byKey.has(buildSourceKey(name, bundle))) {
43
+ return true;
44
+ }
45
+ return !bundle
46
+ && configuredIndexes.byName instanceof Set
47
+ && configuredIndexes.byName.has(name);
48
+ }
49
+
50
+ function buildEmptySummary() {
51
+ return {
52
+ totalDescriptors: 0,
53
+ approvedDescriptors: 0,
54
+ blockedDescriptors: 0,
55
+ configuredSources: 0,
56
+ connectedApprovedDescriptors: 0,
57
+ pendingApprovedDescriptors: 0,
58
+ hasApprovedDescriptors: false,
59
+ hasPendingApprovedDescriptors: false
60
+ };
61
+ }
62
+
63
+ async function readDescriptorUpdatedAt(projectRoot, descriptorFile, fileSystem = fs) {
64
+ const normalizedDescriptorFile = normalizeString(descriptorFile);
65
+ if (!normalizedDescriptorFile) {
66
+ return null;
67
+ }
68
+ const absolutePath = path.join(projectRoot, normalizedDescriptorFile);
69
+ try {
70
+ const stats = await fileSystem.stat(absolutePath);
71
+ if (stats && stats.mtime instanceof Date && Number.isFinite(stats.mtime.getTime())) {
72
+ return stats.mtime.toISOString();
73
+ }
74
+ } catch (_error) {
75
+ return null;
76
+ }
77
+ return null;
78
+ }
79
+
80
+ async function buildSemanticSharedSourceProjection(projectRoot, dependencies = {}) {
81
+ const fileSystem = dependencies.fileSystem || fs;
82
+ const partialReasons = [];
83
+ let discovery = {
84
+ summary: {
85
+ total: 0,
86
+ approved: 0,
87
+ blocked: 0
88
+ },
89
+ items: [],
90
+ blocked: []
91
+ };
92
+ let configuredSources = {
93
+ total: 0,
94
+ items: []
95
+ };
96
+
97
+ try {
98
+ discovery = await discoverSemanticSharedSourceDescriptors(projectRoot, {
99
+ fileSystem
100
+ });
101
+ } catch (_error) {
102
+ partialReasons.push('semantic_shared_source_discovery_unavailable');
103
+ }
104
+
105
+ try {
106
+ configuredSources = await listSemanticSharedSources({}, {
107
+ projectPath: projectRoot,
108
+ fileSystem
109
+ });
110
+ } catch (_error) {
111
+ partialReasons.push('semantic_shared_source_config_unavailable');
112
+ }
113
+
114
+ const configuredItems = Array.isArray(configuredSources.items) ? configuredSources.items : [];
115
+ const configuredIndexes = buildConfiguredSourceIndexes(configuredItems);
116
+ const sourceItems = Array.isArray(discovery.items) ? discovery.items : [];
117
+ const items = [];
118
+ for (const item of sourceItems) {
119
+ items.push({
120
+ ...item,
121
+ connected: isDescriptorConnected(item, configuredIndexes),
122
+ updated_at: await readDescriptorUpdatedAt(projectRoot, item.descriptor_file, fileSystem)
123
+ });
124
+ }
125
+ const approvedItems = items.filter((item) => item.approved);
126
+ const connectedApprovedItems = approvedItems.filter((item) => item.connected);
127
+ const pendingApprovedItems = approvedItems.filter((item) => !item.connected);
128
+
129
+ return {
130
+ summary: {
131
+ totalDescriptors: items.length,
132
+ approvedDescriptors: approvedItems.length,
133
+ blockedDescriptors: Array.isArray(discovery.blocked) ? discovery.blocked.length : 0,
134
+ configuredSources: configuredItems.length,
135
+ connectedApprovedDescriptors: connectedApprovedItems.length,
136
+ pendingApprovedDescriptors: pendingApprovedItems.length,
137
+ hasApprovedDescriptors: approvedItems.length > 0,
138
+ hasPendingApprovedDescriptors: pendingApprovedItems.length > 0
139
+ },
140
+ items,
141
+ blocked: Array.isArray(discovery.blocked) ? discovery.blocked : [],
142
+ partial: partialReasons.length > 0,
143
+ partialReasons
144
+ };
145
+ }
146
+
147
+ module.exports = {
148
+ buildEmptySummary,
149
+ buildSemanticSharedSourceProjection
150
+ };