scene-capability-engine 3.6.64 → 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.
- package/CHANGELOG.md +26 -0
- package/README.md +17 -6
- package/README.zh.md +18 -6
- package/bin/scene-capability-engine.js +4 -0
- package/docs/README.md +2 -2
- package/docs/command-reference.md +385 -8
- package/docs/document-governance.md +3 -2
- package/docs/integration-modes.md +62 -478
- package/docs/integration-philosophy.md +56 -263
- package/docs/magicball-cli-invocation-examples.md +1 -0
- package/docs/magicball-project-portfolio-contract.md +125 -4
- package/docs/project-management/README.md +14 -0
- package/docs/project-management/assurance/backup.md +3 -0
- package/docs/project-management/assurance/config.md +3 -0
- package/docs/project-management/assurance/evidence/README.md +3 -0
- package/docs/project-management/assurance/incidents/README.md +3 -0
- package/docs/project-management/assurance/logs.md +3 -0
- package/docs/project-management/assurance/overview.md +3 -0
- package/docs/project-management/assurance/recovery/README.md +3 -0
- package/docs/project-management/assurance/resource.md +3 -0
- package/docs/project-management/assurance/runbooks/README.md +3 -0
- package/docs/project-management/delivery/acceptance/README.md +3 -0
- package/docs/project-management/delivery/acceptance/evidence/README.md +3 -0
- package/docs/project-management/delivery/acceptance/exceptions/README.md +3 -0
- package/docs/project-management/delivery/acceptance/reports/README.md +3 -0
- package/docs/project-management/delivery/documents/changes.md +3 -0
- package/docs/project-management/delivery/documents/issues.md +3 -0
- package/docs/project-management/delivery/documents/overview.md +3 -0
- package/docs/project-management/delivery/documents/planning.md +3 -0
- package/docs/project-management/delivery/documents/requirements.md +3 -0
- package/docs/project-management/delivery/documents/tracking.md +3 -0
- package/docs/project-management/delivery/handoffs/README.md +3 -0
- package/docs/project-management/delivery/handoffs/evidence/README.md +3 -0
- package/docs/project-management/delivery/handoffs/records/README.md +3 -0
- package/docs/project-management/delivery/overview.md +10 -0
- package/docs/project-management/delivery/releases/README.md +3 -0
- package/docs/project-management/delivery/releases/baselines/README.md +3 -0
- package/docs/project-management/delivery/releases/evidence/README.md +3 -0
- package/docs/project-management/delivery/tables/changes.md +3 -0
- package/docs/project-management/delivery/tables/issues.md +3 -0
- package/docs/project-management/delivery/tables/planning.md +3 -0
- package/docs/project-management/delivery/tables/requirements.md +3 -0
- package/docs/project-management/delivery/tables/tracking.md +3 -0
- package/docs/project-management/environment/agent-discovery.md +3 -0
- package/docs/project-management/environment/development.md +3 -0
- package/docs/project-management/environment/overview.md +10 -0
- package/docs/project-management/environment/testing.md +3 -0
- package/docs/project-management/environment/version-alignment.md +3 -0
- package/docs/quick-start-with-ai-tools.md +68 -308
- package/docs/releases/README.md +3 -0
- package/docs/releases/v3.6.65.md +25 -0
- package/docs/releases/v3.6.66.md +23 -0
- package/docs/releases/v3.6.67.md +23 -0
- package/docs/steering-governance.md +64 -2
- package/docs/zh/README.md +2 -2
- package/docs/zh/releases/README.md +3 -0
- package/docs/zh/releases/v3.6.65.md +25 -0
- package/docs/zh/releases/v3.6.66.md +23 -0
- package/docs/zh/releases/v3.6.67.md +23 -0
- package/lib/commands/adopt.js +24 -0
- package/lib/commands/native.js +158 -0
- package/lib/commands/project.js +96 -0
- package/lib/commands/semantic.js +1459 -0
- package/lib/commands/session.js +74 -3
- package/lib/commands/spec-bootstrap.js +10 -1
- package/lib/commands/spec-gate.js +10 -1
- package/lib/commands/spec-pipeline.js +10 -1
- package/lib/commands/studio.js +405 -30
- package/lib/commands/task.js +141 -7
- package/lib/governance/supreme-principles.js +530 -0
- package/lib/problem/problem-evaluator.js +4 -0
- package/lib/project/candidate-inspection-service.js +24 -1
- package/lib/project/portfolio-projection-service.js +315 -5
- package/lib/project/project-channel-output.js +94 -0
- package/lib/project/project-channel-projection.js +181 -0
- package/lib/project/root-onboarding-service.js +107 -7
- package/lib/project/semantic-shared-source-projection.js +150 -0
- package/lib/project/supervision-action-model.js +277 -0
- package/lib/project/supervision-projection-service.js +305 -5
- package/lib/project/target-resolution-service.js +70 -5
- package/lib/project/visibility-policy.js +93 -0
- package/lib/runtime/multi-spec-scene-session.js +8 -1
- package/lib/runtime/project-channel-context-store.js +387 -0
- package/lib/runtime/project-channel-context.js +406 -0
- package/lib/runtime/scene-session-binding.js +46 -0
- package/lib/runtime/session-store.js +186 -0
- package/lib/runtime/steering-contract.js +7 -1
- package/lib/semantic/archive-report.js +283 -0
- package/lib/semantic/archive-routing.js +67 -0
- package/lib/semantic/backflow-report.js +245 -0
- package/lib/semantic/capability-contract.js +30 -0
- package/lib/semantic/delta-export.js +145 -0
- package/lib/semantic/interaction-observer.js +254 -0
- package/lib/semantic/kernel-loader.js +881 -0
- package/lib/semantic/native-runtime.js +359 -0
- package/lib/semantic/progress-ledger.js +433 -0
- package/lib/semantic/replay-evaluator.js +382 -0
- package/lib/semantic/shared-publication.js +592 -0
- package/lib/semantic/shared-source-config.js +183 -0
- package/lib/semantic/shared-source-connect.js +139 -0
- package/lib/semantic/shared-source-discovery.js +98 -0
- package/lib/semantic/shared-sync-export.js +413 -0
- package/lib/semantic/shared-sync-intake.js +592 -0
- package/lib/semantic/shared-sync-merge.js +547 -0
- package/lib/semantic/shared-sync-release.js +463 -0
- package/lib/semantic/supreme-intent-report.js +300 -0
- package/lib/state/sce-state-store.js +1360 -0
- package/lib/steering/context-sync-manager.js +276 -25
- package/lib/studio/spec-intake-governor.js +39 -3
- package/lib/studio/task-envelope.js +35 -2
- package/lib/workspace/takeover-baseline.js +342 -83
- package/package.json +7 -2
- package/scripts/agent-governance-baseline-audit.js +395 -0
- package/scripts/clarification-first-audit.js +9 -9
- package/scripts/deprecated-entry-audit.js +240 -0
- package/scripts/release-doc-version-audit.js +24 -0
- package/scripts/release-posture-report.js +262 -0
- package/template/.sce/README.md +62 -228
- package/template/.sce/config/semantic-shared-sources.json +5 -0
- package/template/.sce/config/supreme-principles-policy.json +105 -0
- package/template/.sce/config/takeover-baseline.json +7 -0
- package/template/.sce/steering/CORE_PRINCIPLES.md +23 -63
- package/template/.sce/steering/CURRENT_CONTEXT.md +4 -0
- package/template/.sce/steering/RULES_GUIDE.md +17 -9
- package/template/README.md +32 -96
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const { buildProjectPortfolioProjection } = require('./portfolio-projection-service');
|
|
3
|
+
const { buildProjectChannelProjection } = require('./project-channel-projection');
|
|
4
|
+
const { buildProjectChannelOutput, buildProjectChannelOutputFromProjection } = require('./project-channel-output');
|
|
3
5
|
|
|
4
6
|
function normalizeString(value) {
|
|
5
7
|
if (typeof value !== 'string') {
|
|
@@ -67,28 +69,71 @@ function scoreProjectMatch(requestText, project = {}) {
|
|
|
67
69
|
workspaceId: project.workspaceId || null,
|
|
68
70
|
projectName: project.projectName || null,
|
|
69
71
|
appKey: project.appKey || null,
|
|
72
|
+
projectChannelContext: project.projectChannelContext || null,
|
|
70
73
|
confidence: Number(bestScore.toFixed(2)),
|
|
71
74
|
reasonCode
|
|
72
75
|
};
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
function
|
|
78
|
+
function buildCallerProjectChannelBinding(projectId, projectChannelProjection = {}) {
|
|
79
|
+
const channel = buildProjectChannelOutputFromProjection(projectChannelProjection, {
|
|
80
|
+
canonicalProjectId: projectId
|
|
81
|
+
});
|
|
82
|
+
if (!channel.context_available || !channel.channel_id) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return channel;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function buildResolutionCallerContext(options = {}, portfolio = {}, dependencies = {}) {
|
|
76
89
|
const portfolioContext = portfolio && portfolio.callerContext && typeof portfolio.callerContext === 'object'
|
|
77
90
|
? portfolio.callerContext
|
|
78
91
|
: {};
|
|
79
92
|
const explicitCurrentProject = normalizeString(options.currentProject);
|
|
80
93
|
const explicitDeviceId = normalizeString(options.device);
|
|
81
94
|
const explicitToolInstanceId = normalizeString(options.toolInstanceId);
|
|
95
|
+
const explicitChannelId = normalizeString(options.channel);
|
|
96
|
+
const currentProjectId = explicitCurrentProject || normalizeString(portfolioContext.projectId);
|
|
97
|
+
let projectChannelProjection = null;
|
|
98
|
+
|
|
99
|
+
if (currentProjectId) {
|
|
100
|
+
const currentProject = Array.isArray(portfolio.projects)
|
|
101
|
+
? portfolio.projects.find((project) => project.projectId === currentProjectId)
|
|
102
|
+
: null;
|
|
103
|
+
if (currentProject && normalizeString(currentProject.projectRoot)) {
|
|
104
|
+
projectChannelProjection = await buildProjectChannelProjection(currentProject.projectRoot, {
|
|
105
|
+
preferredProjectIds: [
|
|
106
|
+
currentProject.projectChannelContext && currentProject.projectChannelContext.contextProjectId,
|
|
107
|
+
currentProjectId,
|
|
108
|
+
currentProject.workspaceId
|
|
109
|
+
],
|
|
110
|
+
channelId: explicitChannelId
|
|
111
|
+
}, dependencies);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const callerProjectChannel = buildCallerProjectChannelBinding(currentProjectId, projectChannelProjection || {});
|
|
82
116
|
|
|
83
117
|
return {
|
|
84
|
-
...(
|
|
85
|
-
? { currentProjectId
|
|
118
|
+
...(currentProjectId
|
|
119
|
+
? { currentProjectId }
|
|
86
120
|
: {}),
|
|
87
121
|
...(portfolioContext.workspaceId ? { workspaceId: portfolioContext.workspaceId } : {}),
|
|
88
122
|
...(explicitDeviceId || portfolioContext.deviceId
|
|
89
123
|
? { deviceId: explicitDeviceId || portfolioContext.deviceId }
|
|
90
124
|
: {}),
|
|
91
|
-
...(explicitToolInstanceId ? { toolInstanceId: explicitToolInstanceId } : {})
|
|
125
|
+
...(explicitToolInstanceId ? { toolInstanceId: explicitToolInstanceId } : {}),
|
|
126
|
+
...(callerProjectChannel && callerProjectChannel.channel_id
|
|
127
|
+
? {
|
|
128
|
+
currentChannelId: callerProjectChannel.channel_id,
|
|
129
|
+
currentChannelContextAvailable: callerProjectChannel.context_available,
|
|
130
|
+
currentChannelStorageMode: callerProjectChannel.storage_mode,
|
|
131
|
+
requestedChannelId: callerProjectChannel.requested_channel_id,
|
|
132
|
+
focusedChannelId: callerProjectChannel.focused_channel_id,
|
|
133
|
+
activeScene: callerProjectChannel.active_scene,
|
|
134
|
+
activeSpecId: callerProjectChannel.active_spec_id
|
|
135
|
+
}
|
|
136
|
+
: {})
|
|
92
137
|
};
|
|
93
138
|
}
|
|
94
139
|
|
|
@@ -97,16 +142,32 @@ async function resolveProjectTarget(options = {}, dependencies = {}) {
|
|
|
97
142
|
const portfolio = await buildProjectPortfolioProjection({
|
|
98
143
|
workspace: options.workspace
|
|
99
144
|
}, dependencies);
|
|
100
|
-
const callerContext = buildResolutionCallerContext(options, portfolio);
|
|
145
|
+
const callerContext = await buildResolutionCallerContext(options, portfolio, dependencies);
|
|
101
146
|
const currentProjectId = normalizeString(callerContext.currentProjectId);
|
|
102
147
|
const visibleProjects = Array.isArray(portfolio.projects) ? portfolio.projects : [];
|
|
103
148
|
const currentProject = visibleProjects.find((project) => project.projectId === currentProjectId) || null;
|
|
149
|
+
const callerProjectChannel = callerContext.currentChannelId
|
|
150
|
+
? buildProjectChannelOutput({
|
|
151
|
+
projectId: currentProjectId || null,
|
|
152
|
+
canonicalProjectId: currentProjectId || null,
|
|
153
|
+
requestedChannelId: callerContext.requestedChannelId,
|
|
154
|
+
contextAvailable: callerContext.currentChannelContextAvailable === true,
|
|
155
|
+
storageMode: callerContext.currentChannelStorageMode,
|
|
156
|
+
fallback: {
|
|
157
|
+
focused_channel_id: callerContext.focusedChannelId,
|
|
158
|
+
channel_id: callerContext.currentChannelId,
|
|
159
|
+
active_scene: callerContext.activeScene,
|
|
160
|
+
active_spec_id: callerContext.activeSpecId
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
: null;
|
|
104
164
|
|
|
105
165
|
if (!requestText) {
|
|
106
166
|
if (currentProject) {
|
|
107
167
|
return {
|
|
108
168
|
resolvedAt: new Date().toISOString(),
|
|
109
169
|
callerContext,
|
|
170
|
+
...(callerProjectChannel ? { callerProjectChannel } : {}),
|
|
110
171
|
status: 'current-project',
|
|
111
172
|
currentProjectId,
|
|
112
173
|
resolvedProjectId: currentProjectId,
|
|
@@ -117,6 +178,7 @@ async function resolveProjectTarget(options = {}, dependencies = {}) {
|
|
|
117
178
|
return {
|
|
118
179
|
resolvedAt: new Date().toISOString(),
|
|
119
180
|
callerContext,
|
|
181
|
+
...(callerProjectChannel ? { callerProjectChannel } : {}),
|
|
120
182
|
status: 'unresolved',
|
|
121
183
|
...(currentProjectId ? { currentProjectId } : {}),
|
|
122
184
|
reasonCode: currentProjectId
|
|
@@ -139,6 +201,7 @@ async function resolveProjectTarget(options = {}, dependencies = {}) {
|
|
|
139
201
|
return {
|
|
140
202
|
resolvedAt: new Date().toISOString(),
|
|
141
203
|
callerContext,
|
|
204
|
+
...(callerProjectChannel ? { callerProjectChannel } : {}),
|
|
142
205
|
status: 'unresolved',
|
|
143
206
|
...(currentProjectId ? { currentProjectId } : {}),
|
|
144
207
|
reasonCode: 'target.no_match'
|
|
@@ -153,6 +216,7 @@ async function resolveProjectTarget(options = {}, dependencies = {}) {
|
|
|
153
216
|
return {
|
|
154
217
|
resolvedAt: new Date().toISOString(),
|
|
155
218
|
callerContext,
|
|
219
|
+
...(callerProjectChannel ? { callerProjectChannel } : {}),
|
|
156
220
|
status: 'ambiguous',
|
|
157
221
|
...(currentProjectId ? { currentProjectId } : {}),
|
|
158
222
|
confidence: best.confidence,
|
|
@@ -164,6 +228,7 @@ async function resolveProjectTarget(options = {}, dependencies = {}) {
|
|
|
164
228
|
return {
|
|
165
229
|
resolvedAt: new Date().toISOString(),
|
|
166
230
|
callerContext,
|
|
231
|
+
...(callerProjectChannel ? { callerProjectChannel } : {}),
|
|
167
232
|
status: currentProjectId && best.projectId === currentProjectId
|
|
168
233
|
? 'current-project'
|
|
169
234
|
: 'resolved-other-project',
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
const PROJECT_VISIBILITY_REASON_CODES = {
|
|
4
|
+
MANAGED: 'project.visibility.managed',
|
|
5
|
+
EPHEMERAL_ROOT: 'project.visibility.ephemeral_root'
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const EXPLICIT_PROBE_NAME_PATTERNS = [
|
|
9
|
+
/^_codex_import_probe(?:[-_.].*)?$/,
|
|
10
|
+
/^_mb_tmp_project_import_probe(?:[-_.].*)?$/,
|
|
11
|
+
/(?:^|[-_.])codex_import_probe(?:[-_.].*)?$/,
|
|
12
|
+
/(?:^|[-_.])mb_tmp_project_import_probe(?:[-_.].*)?$/,
|
|
13
|
+
/(?:^|[-_.])tmp_project_import_probe(?:[-_.].*)?$/,
|
|
14
|
+
/(?:^|[-_.])import_probe(?:[-_.].*)?$/
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const TEMP_PATH_PATTERNS = [
|
|
18
|
+
/\/tmp\//,
|
|
19
|
+
/\/temp\//,
|
|
20
|
+
/\/appdata\/local\/temp\//,
|
|
21
|
+
/\/var\/folders\//
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function normalizeString(value) {
|
|
25
|
+
if (typeof value !== 'string') {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
return value.trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeText(value) {
|
|
32
|
+
return normalizeString(value).toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizePath(value) {
|
|
36
|
+
return normalizeString(value).replace(/\\/g, '/');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function looksLikeExplicitProbeName(value) {
|
|
40
|
+
const normalized = normalizeText(value);
|
|
41
|
+
if (!normalized) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return EXPLICIT_PROBE_NAME_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function looksLikeTempProbePath(projectRoot) {
|
|
48
|
+
const normalizedRoot = normalizePath(projectRoot).toLowerCase();
|
|
49
|
+
if (!normalizedRoot) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const baseName = normalizeText(path.basename(normalizedRoot));
|
|
53
|
+
if (!baseName.includes('probe')) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return TEMP_PATH_PATTERNS.some((pattern) => pattern.test(normalizedRoot));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isEphemeralProjectRoot(input = {}) {
|
|
60
|
+
const workspaceId = normalizeString(input.workspaceId);
|
|
61
|
+
const projectRoot = normalizePath(input.projectRoot || input.rootDir);
|
|
62
|
+
const projectName = normalizeString(input.projectName) || (projectRoot ? path.basename(projectRoot) : '');
|
|
63
|
+
return looksLikeExplicitProbeName(workspaceId)
|
|
64
|
+
|| looksLikeExplicitProbeName(projectName)
|
|
65
|
+
|| looksLikeTempProbePath(projectRoot);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function classifyProjectVisibility(input = {}) {
|
|
69
|
+
if (isEphemeralProjectRoot(input)) {
|
|
70
|
+
return {
|
|
71
|
+
visibility: 'hidden',
|
|
72
|
+
lifecycle: 'ephemeral',
|
|
73
|
+
reasonCode: PROJECT_VISIBILITY_REASON_CODES.EPHEMERAL_ROOT
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
visibility: 'managed',
|
|
79
|
+
lifecycle: 'persistent',
|
|
80
|
+
reasonCode: PROJECT_VISIBILITY_REASON_CODES.MANAGED
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isCallerVisibleProject(input = {}) {
|
|
85
|
+
return classifyProjectVisibility(input).visibility === 'managed';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
PROJECT_VISIBILITY_REASON_CODES,
|
|
90
|
+
classifyProjectVisibility,
|
|
91
|
+
isCallerVisibleProject,
|
|
92
|
+
isEphemeralProjectRoot
|
|
93
|
+
};
|
|
@@ -40,6 +40,8 @@ async function bindMultiSpecSceneSession(options = {}, dependencies = {}) {
|
|
|
40
40
|
const {
|
|
41
41
|
specTargets = [],
|
|
42
42
|
sceneId = null,
|
|
43
|
+
projectId = null,
|
|
44
|
+
collabChannel = null,
|
|
43
45
|
commandName = 'spec-command',
|
|
44
46
|
commandLabel = 'spec-command',
|
|
45
47
|
commandOptions = {},
|
|
@@ -60,6 +62,8 @@ async function bindMultiSpecSceneSession(options = {}, dependencies = {}) {
|
|
|
60
62
|
|
|
61
63
|
const sceneBinding = await resolveSpecSceneBinding({
|
|
62
64
|
sceneId,
|
|
65
|
+
projectId,
|
|
66
|
+
collabChannel,
|
|
63
67
|
allowNoScene: false
|
|
64
68
|
}, {
|
|
65
69
|
projectPath,
|
|
@@ -115,7 +119,10 @@ async function bindMultiSpecSceneSession(options = {}, dependencies = {}) {
|
|
|
115
119
|
scene_session_id: sceneBinding.scene_session_id,
|
|
116
120
|
binding_source: sceneBinding.source,
|
|
117
121
|
multi_spec_child_sessions: specSessions
|
|
118
|
-
}
|
|
122
|
+
},
|
|
123
|
+
...(sceneBinding.project_channel
|
|
124
|
+
? { project_channel: sceneBinding.project_channel }
|
|
125
|
+
: {})
|
|
119
126
|
};
|
|
120
127
|
} catch (error) {
|
|
121
128
|
if (shouldTrackSessions) {
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const {
|
|
4
|
+
normalizeProjectChannelContext,
|
|
5
|
+
normalizeChannelState,
|
|
6
|
+
setFocusedChannel,
|
|
7
|
+
updateChannelState
|
|
8
|
+
} = require('./project-channel-context');
|
|
9
|
+
|
|
10
|
+
const PROJECT_CHANNEL_CONTEXT_DIR = path.join('.sce', 'state', 'project-channel-contexts');
|
|
11
|
+
const PROJECT_CHANNEL_INDEX_FILE = 'index.json';
|
|
12
|
+
const PROJECT_CHANNEL_CHANNELS_DIR = 'channels';
|
|
13
|
+
|
|
14
|
+
function normalizeString(value) {
|
|
15
|
+
if (typeof value !== 'string') {
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
return value.trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function safePathPart(value, fallback) {
|
|
22
|
+
const normalized = `${value || ''}`
|
|
23
|
+
.trim()
|
|
24
|
+
.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
25
|
+
.replace(/^-+|-+$/g, '');
|
|
26
|
+
return normalized || fallback;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class ProjectChannelContextStore {
|
|
30
|
+
constructor(projectPath = process.cwd(), fileSystem = fs, options = {}) {
|
|
31
|
+
this._projectPath = projectPath;
|
|
32
|
+
this._fileSystem = fileSystem;
|
|
33
|
+
this._contextDir = path.join(projectPath, PROJECT_CHANNEL_CONTEXT_DIR);
|
|
34
|
+
this._contextSyncManager = options.contextSyncManager || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getContextPath(projectId) {
|
|
38
|
+
return this.getProjectIndexPath(projectId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getLegacyContextPath(projectId) {
|
|
42
|
+
const normalizedProjectId = normalizeString(projectId);
|
|
43
|
+
if (!normalizedProjectId) {
|
|
44
|
+
throw new Error('projectId is required');
|
|
45
|
+
}
|
|
46
|
+
return path.join(this._contextDir, `${safePathPart(normalizedProjectId, 'project')}.json`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getProjectDirectoryPath(projectId) {
|
|
50
|
+
const normalizedProjectId = normalizeString(projectId);
|
|
51
|
+
if (!normalizedProjectId) {
|
|
52
|
+
throw new Error('projectId is required');
|
|
53
|
+
}
|
|
54
|
+
return path.join(this._contextDir, safePathPart(normalizedProjectId, 'project'));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getProjectIndexPath(projectId) {
|
|
58
|
+
return path.join(this.getProjectDirectoryPath(projectId), PROJECT_CHANNEL_INDEX_FILE);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getChannelContextPath(projectId, channelId) {
|
|
62
|
+
const normalizedChannelId = normalizeString(channelId);
|
|
63
|
+
if (!normalizedChannelId) {
|
|
64
|
+
throw new Error('channelId is required');
|
|
65
|
+
}
|
|
66
|
+
return path.join(
|
|
67
|
+
this.getProjectDirectoryPath(projectId),
|
|
68
|
+
PROJECT_CHANNEL_CHANNELS_DIR,
|
|
69
|
+
`${safePathPart(normalizedChannelId, 'channel-default')}.json`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async load(projectId, options = {}) {
|
|
74
|
+
const normalizedProjectId = normalizeString(projectId);
|
|
75
|
+
if (!normalizedProjectId) {
|
|
76
|
+
throw new Error('projectId is required');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const indexPayload = await this._readProjectIndex(normalizedProjectId);
|
|
80
|
+
const contextPayload = indexPayload
|
|
81
|
+
? await this._readSplitContext(normalizedProjectId, indexPayload)
|
|
82
|
+
: await this._readLegacyContext(normalizedProjectId);
|
|
83
|
+
|
|
84
|
+
return normalizeProjectChannelContext(contextPayload || { projectId: normalizedProjectId }, {
|
|
85
|
+
projectId: normalizedProjectId,
|
|
86
|
+
defaultChannelId: options.defaultChannelId,
|
|
87
|
+
now: options.now,
|
|
88
|
+
channelDefaults: options.channelDefaults,
|
|
89
|
+
allowDefaultChannelSynthesis: options.allowDefaultChannelSynthesis
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async hasPersistedContext(projectId) {
|
|
94
|
+
const normalizedProjectId = normalizeString(projectId);
|
|
95
|
+
if (!normalizedProjectId) {
|
|
96
|
+
throw new Error('projectId is required');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (await this.detectPersistedContext(normalizedProjectId)).available;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async detectPersistedContext(projectId) {
|
|
103
|
+
const normalizedProjectId = normalizeString(projectId);
|
|
104
|
+
if (!normalizedProjectId) {
|
|
105
|
+
throw new Error('projectId is required');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (await this._fileSystem.pathExists(this.getProjectIndexPath(normalizedProjectId))) {
|
|
109
|
+
return {
|
|
110
|
+
available: true,
|
|
111
|
+
storageMode: 'split'
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (await this._fileSystem.pathExists(this.getLegacyContextPath(normalizedProjectId))) {
|
|
116
|
+
return {
|
|
117
|
+
available: true,
|
|
118
|
+
storageMode: 'legacy'
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
available: false,
|
|
124
|
+
storageMode: 'none'
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async listPersistedContexts() {
|
|
129
|
+
if (!await this._fileSystem.pathExists(this._contextDir)) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let entries = [];
|
|
134
|
+
try {
|
|
135
|
+
entries = await this._fileSystem.readdir(this._contextDir);
|
|
136
|
+
} catch (_error) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const contexts = new Map();
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
const candidatePath = path.join(this._contextDir, entry);
|
|
143
|
+
let stats = null;
|
|
144
|
+
try {
|
|
145
|
+
stats = await this._fileSystem.stat(candidatePath);
|
|
146
|
+
} catch (_error) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (stats.isDirectory()) {
|
|
151
|
+
const indexPath = path.join(candidatePath, PROJECT_CHANNEL_INDEX_FILE);
|
|
152
|
+
if (!await this._fileSystem.pathExists(indexPath)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const payload = await this._fileSystem.readJson(indexPath);
|
|
157
|
+
const projectId = normalizeString(payload && payload.projectId);
|
|
158
|
+
if (projectId) {
|
|
159
|
+
contexts.set(projectId, {
|
|
160
|
+
projectId,
|
|
161
|
+
storageMode: 'split'
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
} catch (_error) {
|
|
165
|
+
// Ignore unreadable split contexts and let projection report partial elsewhere.
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!stats.isFile() || path.extname(entry).toLowerCase() !== '.json') {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const payload = await this._fileSystem.readJson(candidatePath);
|
|
176
|
+
const projectId = normalizeString(payload && payload.projectId);
|
|
177
|
+
if (projectId && !contexts.has(projectId)) {
|
|
178
|
+
contexts.set(projectId, {
|
|
179
|
+
projectId,
|
|
180
|
+
storageMode: 'legacy'
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
} catch (_error) {
|
|
184
|
+
// Ignore unreadable legacy contexts and let projection report partial elsewhere.
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return [...contexts.values()].sort((left, right) => left.projectId.localeCompare(right.projectId));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async save(context = {}, options = {}) {
|
|
192
|
+
const normalized = normalizeProjectChannelContext(context, {
|
|
193
|
+
projectId: options.projectId,
|
|
194
|
+
defaultChannelId: options.defaultChannelId,
|
|
195
|
+
now: options.now,
|
|
196
|
+
channelDefaults: options.channelDefaults
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await this._writeFullContext(normalized);
|
|
200
|
+
await this._cleanupLegacyContext(normalized.projectId);
|
|
201
|
+
|
|
202
|
+
if (options.syncCurrentContext === true || this._contextSyncManager) {
|
|
203
|
+
await this.syncCurrentContext(normalized.projectId, {
|
|
204
|
+
...options,
|
|
205
|
+
projectChannelContext: normalized
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return normalized;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async updateChannel(projectId, channelId, patch = {}, options = {}) {
|
|
213
|
+
const current = await this.load(projectId, options);
|
|
214
|
+
const next = updateChannelState(current, channelId, patch, options);
|
|
215
|
+
|
|
216
|
+
await this._writeProjectShell(next.projectId, next.focusedChannelId, Object.keys(next.channels));
|
|
217
|
+
await this._writeChannelState(next.projectId, next.channels[channelId]);
|
|
218
|
+
await this._cleanupLegacyContext(next.projectId);
|
|
219
|
+
|
|
220
|
+
if (options.syncCurrentContext === true || this._contextSyncManager) {
|
|
221
|
+
await this.syncCurrentContext(next.projectId, {
|
|
222
|
+
...options,
|
|
223
|
+
projectChannelContext: next
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return next;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async focusChannel(projectId, channelId, options = {}) {
|
|
231
|
+
const current = await this.load(projectId, options);
|
|
232
|
+
const next = setFocusedChannel(current, channelId, options);
|
|
233
|
+
|
|
234
|
+
await this._writeProjectShell(next.projectId, next.focusedChannelId, Object.keys(next.channels));
|
|
235
|
+
if (!current.channels[channelId] && next.channels[channelId]) {
|
|
236
|
+
await this._writeChannelState(next.projectId, next.channels[channelId]);
|
|
237
|
+
}
|
|
238
|
+
await this._cleanupLegacyContext(next.projectId);
|
|
239
|
+
|
|
240
|
+
if (options.syncCurrentContext === true || this._contextSyncManager) {
|
|
241
|
+
await this.syncCurrentContext(next.projectId, {
|
|
242
|
+
...options,
|
|
243
|
+
projectChannelContext: next
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return next;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async syncCurrentContext(projectId, options = {}) {
|
|
251
|
+
const manager = options.contextSyncManager
|
|
252
|
+
|| this._contextSyncManager
|
|
253
|
+
|| this._createContextSyncManager();
|
|
254
|
+
const projectChannelContext = options.projectChannelContext || await this.load(projectId, options);
|
|
255
|
+
await manager.updateProjectChannelSummary(projectId, projectChannelContext);
|
|
256
|
+
return { success: true };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async _readProjectIndex(projectId) {
|
|
260
|
+
const indexPath = this.getProjectIndexPath(projectId);
|
|
261
|
+
if (!await this._fileSystem.pathExists(indexPath)) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
return await this._fileSystem.readJson(indexPath);
|
|
267
|
+
} catch (_error) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async _readSplitContext(projectId, indexPayload = {}) {
|
|
273
|
+
const normalizedProjectId = normalizeString(projectId);
|
|
274
|
+
const indexChannels = Array.isArray(indexPayload.channelIds)
|
|
275
|
+
? indexPayload.channelIds
|
|
276
|
+
: [];
|
|
277
|
+
const channels = {};
|
|
278
|
+
|
|
279
|
+
for (const channelId of indexChannels) {
|
|
280
|
+
const normalizedChannelId = normalizeString(channelId);
|
|
281
|
+
if (!normalizedChannelId) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const channelPath = this.getChannelContextPath(normalizedProjectId, normalizedChannelId);
|
|
286
|
+
try {
|
|
287
|
+
const rawChannel = await this._fileSystem.readJson(channelPath);
|
|
288
|
+
const channelState = normalizeChannelState(normalizedChannelId, rawChannel, {
|
|
289
|
+
projectId: normalizedProjectId
|
|
290
|
+
});
|
|
291
|
+
channels[channelState.channelId] = channelState;
|
|
292
|
+
} catch (_error) {
|
|
293
|
+
// ignore unreadable channel file and let normalizeProjectChannelContext repair focus/defaults
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
projectId: normalizedProjectId,
|
|
299
|
+
focusedChannelId: normalizeString(indexPayload.focusedChannelId),
|
|
300
|
+
channels
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async _readLegacyContext(projectId) {
|
|
305
|
+
const legacyPath = this.getLegacyContextPath(projectId);
|
|
306
|
+
if (!await this._fileSystem.pathExists(legacyPath)) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
return await this._fileSystem.readJson(legacyPath);
|
|
312
|
+
} catch (_error) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async _writeFullContext(context) {
|
|
318
|
+
const channelIds = Object.keys(context.channels);
|
|
319
|
+
await this._writeProjectShell(context.projectId, context.focusedChannelId, channelIds);
|
|
320
|
+
|
|
321
|
+
for (const channelId of channelIds) {
|
|
322
|
+
await this._writeChannelState(context.projectId, context.channels[channelId]);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
await this._deleteRemovedChannelFiles(context.projectId, channelIds);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async _writeProjectShell(projectId, focusedChannelId, channelIds) {
|
|
329
|
+
const indexPath = this.getProjectIndexPath(projectId);
|
|
330
|
+
await this._fileSystem.ensureDir(path.dirname(indexPath));
|
|
331
|
+
await this._fileSystem.writeJson(indexPath, {
|
|
332
|
+
projectId,
|
|
333
|
+
focusedChannelId,
|
|
334
|
+
channelIds: [...channelIds].sort((left, right) => left.localeCompare(right))
|
|
335
|
+
}, { spaces: 2 });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async _writeChannelState(projectId, channelState) {
|
|
339
|
+
const normalizedChannelId = normalizeString(channelState && channelState.channelId);
|
|
340
|
+
if (!normalizedChannelId) {
|
|
341
|
+
throw new Error('channelState.channelId is required');
|
|
342
|
+
}
|
|
343
|
+
const channelPath = this.getChannelContextPath(projectId, normalizedChannelId);
|
|
344
|
+
await this._fileSystem.ensureDir(path.dirname(channelPath));
|
|
345
|
+
await this._fileSystem.writeJson(channelPath, channelState, { spaces: 2 });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async _deleteRemovedChannelFiles(projectId, retainedChannelIds = []) {
|
|
349
|
+
const channelsDir = path.join(this.getProjectDirectoryPath(projectId), PROJECT_CHANNEL_CHANNELS_DIR);
|
|
350
|
+
if (!await this._fileSystem.pathExists(channelsDir)) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const retained = new Set(
|
|
355
|
+
retainedChannelIds.map((channelId) => this.getChannelContextPath(projectId, channelId))
|
|
356
|
+
);
|
|
357
|
+
const entries = await this._fileSystem.readdir(channelsDir);
|
|
358
|
+
for (const entry of entries) {
|
|
359
|
+
if (!entry.endsWith('.json')) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const candidatePath = path.join(channelsDir, entry);
|
|
363
|
+
if (!retained.has(candidatePath)) {
|
|
364
|
+
await this._fileSystem.remove(candidatePath);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async _cleanupLegacyContext(projectId) {
|
|
370
|
+
const legacyPath = this.getLegacyContextPath(projectId);
|
|
371
|
+
if (await this._fileSystem.pathExists(legacyPath)) {
|
|
372
|
+
await this._fileSystem.remove(legacyPath);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
_createContextSyncManager() {
|
|
377
|
+
const { ContextSyncManager } = require('../steering/context-sync-manager');
|
|
378
|
+
return new ContextSyncManager(this._projectPath);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
module.exports = {
|
|
383
|
+
ProjectChannelContextStore,
|
|
384
|
+
PROJECT_CHANNEL_CONTEXT_DIR,
|
|
385
|
+
PROJECT_CHANNEL_INDEX_FILE,
|
|
386
|
+
PROJECT_CHANNEL_CHANNELS_DIR
|
|
387
|
+
};
|