scene-capability-engine 3.6.32 → 3.6.36

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 (83) hide show
  1. package/CHANGELOG.md +86 -1
  2. package/README.md +119 -122
  3. package/README.zh.md +123 -121
  4. package/bin/scene-capability-engine.js +11 -0
  5. package/docs/README.md +21 -32
  6. package/docs/auto-refactor-index.md +384 -0
  7. package/docs/command-reference.md +94 -2
  8. package/docs/magicball-adaptation-task-checklist-v1.md +385 -0
  9. package/docs/magicball-app-bundle-sqlite-and-command-draft.md +539 -0
  10. package/docs/magicball-capability-iteration-api.md +2 -0
  11. package/docs/magicball-capability-iteration-ui.md +2 -0
  12. package/docs/magicball-capability-library.md +2 -0
  13. package/docs/magicball-cli-invocation-examples.md +336 -0
  14. package/docs/magicball-frontend-state-and-command-mapping.md +244 -0
  15. package/docs/magicball-integration-doc-index.md +137 -0
  16. package/docs/magicball-integration-issue-tracker.md +218 -0
  17. package/docs/magicball-mode-home-and-ontology-empty-state-playbook.md +249 -0
  18. package/docs/magicball-sce-adaptation-guide.md +203 -0
  19. package/docs/magicball-three-mode-alignment-plan.md +551 -0
  20. package/docs/magicball-ui-surface-checklist.md +126 -0
  21. package/docs/magicball-write-auth-adaptation-guide.md +328 -0
  22. package/docs/refactor-completion-roadmap.md +116 -0
  23. package/docs/zh/README.md +27 -30
  24. package/docs/zh/refactor-completion-roadmap.md +116 -0
  25. package/lib/app/registry-config.js +73 -0
  26. package/lib/app/registry-sync-service.js +228 -0
  27. package/lib/auto/archive-schema-service.js +276 -0
  28. package/lib/auto/archive-summary.js +60 -0
  29. package/lib/auto/batch-goal-input-service.js +543 -0
  30. package/lib/auto/batch-output.js +201 -0
  31. package/lib/auto/batch-summary-storage-service.js +110 -0
  32. package/lib/auto/close-loop-batch-service.js +116 -0
  33. package/lib/auto/close-loop-controller-service.js +287 -0
  34. package/lib/auto/close-loop-program-service.js +283 -0
  35. package/lib/auto/close-loop-recovery-service.js +191 -0
  36. package/lib/auto/close-loop-session-storage-service.js +50 -0
  37. package/lib/auto/controller-lock-service.js +55 -0
  38. package/lib/auto/controller-output.js +32 -0
  39. package/lib/auto/controller-queue-service.js +127 -0
  40. package/lib/auto/controller-session-storage-service.js +105 -0
  41. package/lib/auto/governance-advisory-service.js +208 -0
  42. package/lib/auto/governance-close-loop-service.js +411 -0
  43. package/lib/auto/governance-maintenance-presenter.js +162 -0
  44. package/lib/auto/governance-maintenance-service.js +112 -0
  45. package/lib/auto/governance-session-presenter.js +70 -0
  46. package/lib/auto/governance-session-storage-service.js +198 -0
  47. package/lib/auto/governance-signals.js +139 -0
  48. package/lib/auto/governance-stats-presenter.js +337 -0
  49. package/lib/auto/governance-stats-service.js +115 -0
  50. package/lib/auto/governance-summary.js +703 -0
  51. package/lib/auto/handoff-capability-matrix-service.js +281 -0
  52. package/lib/auto/handoff-evidence-review-service.js +251 -0
  53. package/lib/auto/handoff-release-evidence-service.js +190 -0
  54. package/lib/auto/handoff-release-gate-history-loaders-service.js +502 -0
  55. package/lib/auto/handoff-release-gate-history-service.js +257 -0
  56. package/lib/auto/handoff-reporting-service.js +1407 -0
  57. package/lib/auto/handoff-run-service.js +486 -0
  58. package/lib/auto/handoff-snapshots-service.js +645 -0
  59. package/lib/auto/observability-service.js +132 -0
  60. package/lib/auto/output-writer.js +34 -0
  61. package/lib/auto/program-auto-remediation-service.js +130 -0
  62. package/lib/auto/program-diagnostics.js +138 -0
  63. package/lib/auto/program-governance-helpers.js +306 -0
  64. package/lib/auto/program-governance-loop-service.js +413 -0
  65. package/lib/auto/program-output.js +106 -0
  66. package/lib/auto/program-summary.js +183 -0
  67. package/lib/auto/recovery-memory-service.js +684 -0
  68. package/lib/auto/recovery-selection-service.js +52 -0
  69. package/lib/auto/retention-policy.js +98 -0
  70. package/lib/auto/session-persistence-service.js +106 -0
  71. package/lib/auto/session-presenter.js +105 -0
  72. package/lib/auto/session-prune-service.js +190 -0
  73. package/lib/auto/session-query-service.js +249 -0
  74. package/lib/auto/spec-protection.js +141 -0
  75. package/lib/commands/app.js +911 -0
  76. package/lib/commands/assurance.js +212 -0
  77. package/lib/commands/auto.js +1091 -11063
  78. package/lib/commands/mode.js +321 -0
  79. package/lib/commands/ontology.js +415 -0
  80. package/lib/commands/pm.js +422 -0
  81. package/lib/ontology/seed-profiles.js +160 -0
  82. package/lib/state/sce-state-store.js +3369 -1200
  83. package/package.json +1 -1
@@ -0,0 +1,73 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+
4
+ const DEFAULT_APP_REGISTRY_CONFIG_PATH = path.join('.sce', 'config', 'app-registries.json');
5
+
6
+ const DEFAULT_APP_REGISTRY_CONFIG = Object.freeze({
7
+ version: '1.0',
8
+ bundle_registry: {
9
+ repo_url: 'https://github.com/heguangyong/magicball-app-bundle-registry.git',
10
+ branch: 'main',
11
+ index_url: 'https://raw.githubusercontent.com/heguangyong/magicball-app-bundle-registry/main/bundles/index.json'
12
+ },
13
+ service_catalog: {
14
+ repo_url: 'https://github.com/heguangyong/magicball-app-service-catalog.git',
15
+ branch: 'main',
16
+ index_url: 'https://raw.githubusercontent.com/heguangyong/magicball-app-service-catalog/main/catalog/index.json'
17
+ }
18
+ });
19
+
20
+ function normalizeString(value) {
21
+ if (typeof value !== 'string') {
22
+ return '';
23
+ }
24
+ return value.trim();
25
+ }
26
+
27
+ function mergeSection(base = {}, patch = {}) {
28
+ return {
29
+ ...base,
30
+ ...Object.fromEntries(
31
+ Object.entries(patch || {}).filter(([, value]) => value !== undefined)
32
+ )
33
+ };
34
+ }
35
+
36
+ async function loadAppRegistryConfig(projectPath = process.cwd(), fileSystem = fs) {
37
+ const configPath = path.join(projectPath, DEFAULT_APP_REGISTRY_CONFIG_PATH);
38
+ let filePayload = {};
39
+ if (await fileSystem.pathExists(configPath)) {
40
+ filePayload = await fileSystem.readJson(configPath);
41
+ }
42
+ return {
43
+ config_path: DEFAULT_APP_REGISTRY_CONFIG_PATH,
44
+ config: {
45
+ version: normalizeString(filePayload.version) || DEFAULT_APP_REGISTRY_CONFIG.version,
46
+ bundle_registry: mergeSection(DEFAULT_APP_REGISTRY_CONFIG.bundle_registry, filePayload.bundle_registry),
47
+ service_catalog: mergeSection(DEFAULT_APP_REGISTRY_CONFIG.service_catalog, filePayload.service_catalog)
48
+ }
49
+ };
50
+ }
51
+
52
+ async function saveAppRegistryConfig(patch = {}, projectPath = process.cwd(), fileSystem = fs) {
53
+ const loaded = await loadAppRegistryConfig(projectPath, fileSystem);
54
+ const nextConfig = {
55
+ version: DEFAULT_APP_REGISTRY_CONFIG.version,
56
+ bundle_registry: mergeSection(loaded.config.bundle_registry, patch.bundle_registry),
57
+ service_catalog: mergeSection(loaded.config.service_catalog, patch.service_catalog)
58
+ };
59
+ const configPath = path.join(projectPath, DEFAULT_APP_REGISTRY_CONFIG_PATH);
60
+ await fileSystem.ensureDir(path.dirname(configPath));
61
+ await fileSystem.writeJson(configPath, nextConfig, { spaces: 2 });
62
+ return {
63
+ config_path: DEFAULT_APP_REGISTRY_CONFIG_PATH,
64
+ config: nextConfig
65
+ };
66
+ }
67
+
68
+ module.exports = {
69
+ DEFAULT_APP_REGISTRY_CONFIG_PATH,
70
+ DEFAULT_APP_REGISTRY_CONFIG,
71
+ loadAppRegistryConfig,
72
+ saveAppRegistryConfig
73
+ };
@@ -0,0 +1,228 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const { loadAppRegistryConfig } = require('./registry-config');
4
+
5
+ function normalizeString(value) {
6
+ if (typeof value !== 'string') {
7
+ return '';
8
+ }
9
+ return value.trim();
10
+ }
11
+
12
+ async function readJsonResource(resourceRef, dependencies = {}) {
13
+ const fileSystem = dependencies.fileSystem || fs;
14
+ const fetchImpl = dependencies.fetchImpl || global.fetch;
15
+ const normalized = normalizeString(resourceRef);
16
+ if (!normalized) {
17
+ throw new Error('resource reference is required');
18
+ }
19
+ if (/^https?:\/\//i.test(normalized)) {
20
+ if (typeof fetchImpl !== 'function') {
21
+ throw new Error(`fetch unavailable for remote resource: ${normalized}`);
22
+ }
23
+ const response = await fetchImpl(normalized);
24
+ if (!response || response.ok !== true) {
25
+ const status = response && typeof response.status !== 'undefined' ? response.status : 'unknown';
26
+ throw new Error(`failed to fetch ${normalized} (status=${status})`);
27
+ }
28
+ return response.json();
29
+ }
30
+ const resolvedPath = path.isAbsolute(normalized)
31
+ ? normalized
32
+ : path.join(dependencies.projectPath || process.cwd(), normalized);
33
+ return fileSystem.readJson(resolvedPath);
34
+ }
35
+
36
+ function resolveChildResource(parentRef, childRef) {
37
+ const normalizedParent = normalizeString(parentRef);
38
+ const normalizedChild = normalizeString(childRef);
39
+ if (!normalizedChild) {
40
+ return normalizedParent;
41
+ }
42
+ if (/^https?:\/\//i.test(normalizedChild) || path.isAbsolute(normalizedChild)) {
43
+ return normalizedChild;
44
+ }
45
+ if (/^https?:\/\//i.test(normalizedParent)) {
46
+ return new URL(normalizedChild, normalizedParent).toString();
47
+ }
48
+ return path.join(path.dirname(normalizedParent), normalizedChild);
49
+ }
50
+
51
+ function graphToRegisterPayload(graph = {}) {
52
+ const bundle = graph.bundle || {};
53
+ const runtimeRelease = graph.runtime_release || {};
54
+ const ontologyBundle = graph.ontology_bundle || {};
55
+ const engineeringProject = graph.engineering_project || {};
56
+ return {
57
+ app_id: bundle.app_id,
58
+ app_key: bundle.app_key,
59
+ app_name: bundle.app_name,
60
+ app_slug: bundle.app_slug || undefined,
61
+ workspace_id: bundle.workspace_id || undefined,
62
+ runtime_release_id: bundle.runtime_release_id || undefined,
63
+ ontology_bundle_id: bundle.ontology_bundle_id || undefined,
64
+ engineering_project_id: bundle.engineering_project_id || undefined,
65
+ default_scene_id: bundle.default_scene_id || undefined,
66
+ environment: bundle.environment || undefined,
67
+ status: bundle.status || undefined,
68
+ source_origin: bundle.source_origin || undefined,
69
+ tags: Array.isArray(bundle.tags) ? bundle.tags : [],
70
+ metadata: bundle.metadata && typeof bundle.metadata === 'object' ? { ...bundle.metadata } : {},
71
+ runtime: runtimeRelease && runtimeRelease.release_id ? {
72
+ release_id: runtimeRelease.release_id,
73
+ runtime_version: runtimeRelease.runtime_version,
74
+ release_channel: runtimeRelease.release_channel,
75
+ release_status: runtimeRelease.release_status,
76
+ entrypoint: runtimeRelease.entrypoint,
77
+ runtime_status: runtimeRelease.runtime_status,
78
+ release_notes_file: runtimeRelease.release_notes_file,
79
+ release_evidence_file: runtimeRelease.release_evidence_file,
80
+ published_at: runtimeRelease.published_at,
81
+ source_updated_at: runtimeRelease.source_updated_at,
82
+ metadata: runtimeRelease.metadata || {}
83
+ } : undefined,
84
+ ontology: ontologyBundle && ontologyBundle.ontology_bundle_id ? {
85
+ ontology_bundle_id: ontologyBundle.ontology_bundle_id,
86
+ ontology_version: ontologyBundle.ontology_version,
87
+ template_version: ontologyBundle.template_version,
88
+ capability_catalog_version: ontologyBundle.capability_catalog_version,
89
+ triad_revision: ontologyBundle.triad_revision,
90
+ triad_status: ontologyBundle.triad_status,
91
+ publish_readiness: ontologyBundle.publish_readiness,
92
+ template_source: ontologyBundle.template_source,
93
+ capability_set: Array.isArray(ontologyBundle.capability_set) ? ontologyBundle.capability_set : [],
94
+ summary: ontologyBundle.summary || {},
95
+ metadata: ontologyBundle.metadata || {}
96
+ } : undefined,
97
+ engineering: engineeringProject && engineeringProject.engineering_project_id ? {
98
+ engineering_project_id: engineeringProject.engineering_project_id,
99
+ project_key: engineeringProject.project_key,
100
+ project_name: engineeringProject.project_name,
101
+ repo_url: engineeringProject.repo_url,
102
+ repo_provider: engineeringProject.repo_provider,
103
+ default_branch: engineeringProject.default_branch,
104
+ current_branch: engineeringProject.current_branch,
105
+ commit_sha: engineeringProject.commit_sha,
106
+ workspace_path: engineeringProject.workspace_path,
107
+ code_version: engineeringProject.code_version,
108
+ synced_runtime_release_id: engineeringProject.synced_runtime_release_id,
109
+ dirty_state: engineeringProject.dirty_state === true,
110
+ auth_policy: engineeringProject.auth_policy || {},
111
+ metadata: engineeringProject.metadata || {}
112
+ } : undefined,
113
+ scene_bindings: Array.isArray(graph.scene_bindings)
114
+ ? graph.scene_bindings.map((item) => ({
115
+ scene_id: item.scene_id,
116
+ binding_role: item.binding_role,
117
+ source: item.source,
118
+ metadata: item.metadata || {}
119
+ }))
120
+ : []
121
+ };
122
+ }
123
+
124
+ async function syncBundleRegistry(options = {}, dependencies = {}) {
125
+ const projectPath = dependencies.projectPath || process.cwd();
126
+ const fileSystem = dependencies.fileSystem || fs;
127
+ const stateStore = dependencies.stateStore;
128
+ const loadedConfig = await loadAppRegistryConfig(projectPath, fileSystem);
129
+ const indexRef = normalizeString(options.indexUrl) || normalizeString(loadedConfig.config.bundle_registry.index_url);
130
+ const indexPayload = await readJsonResource(indexRef, dependencies);
131
+ const bundleItems = Array.isArray(indexPayload && indexPayload.bundles) ? indexPayload.bundles : [];
132
+ const synced = [];
133
+ for (const item of bundleItems) {
134
+ const resourceRef = normalizeString(item && item.url) || resolveChildResource(indexRef, item && item.file);
135
+ const bundlePayload = await readJsonResource(resourceRef, dependencies);
136
+ const graph = await stateStore.registerAppBundle(bundlePayload);
137
+ synced.push({
138
+ app_id: graph && graph.bundle ? graph.bundle.app_id : null,
139
+ app_key: graph && graph.bundle ? graph.bundle.app_key : null,
140
+ source: resourceRef
141
+ });
142
+ }
143
+ return {
144
+ mode: 'app-registry-sync-bundles',
145
+ index_url: indexRef,
146
+ source_generated_at: indexPayload && indexPayload.generated_at ? indexPayload.generated_at : null,
147
+ synced_count: synced.length,
148
+ items: synced
149
+ };
150
+ }
151
+
152
+ async function syncServiceCatalog(options = {}, dependencies = {}) {
153
+ const projectPath = dependencies.projectPath || process.cwd();
154
+ const fileSystem = dependencies.fileSystem || fs;
155
+ const stateStore = dependencies.stateStore;
156
+ const loadedConfig = await loadAppRegistryConfig(projectPath, fileSystem);
157
+ const indexRef = normalizeString(options.indexUrl) || normalizeString(loadedConfig.config.service_catalog.index_url);
158
+ const indexPayload = await readJsonResource(indexRef, dependencies);
159
+ const appItems = Array.isArray(indexPayload && indexPayload.apps) ? indexPayload.apps : [];
160
+ const synced = [];
161
+ for (const item of appItems) {
162
+ const resourceRef = normalizeString(item && item.url) || resolveChildResource(indexRef, item && item.file);
163
+ const appPayload = await readJsonResource(resourceRef, dependencies);
164
+ const appId = normalizeString(appPayload && appPayload.app_id);
165
+ const appKey = normalizeString(appPayload && appPayload.app_key);
166
+ const appName = normalizeString(appPayload && appPayload.app_name);
167
+ let graph = await stateStore.getAppBundleGraph(appId || appKey);
168
+ if (!graph) {
169
+ graph = await stateStore.registerAppBundle({
170
+ app_id: appId,
171
+ app_key: appKey,
172
+ app_name: appName || appKey || appId,
173
+ status: 'active',
174
+ environment: normalizeString(appPayload && appPayload.environment) || null,
175
+ source_origin: 'app-service-catalog-sync'
176
+ });
177
+ }
178
+ const nextPayload = graphToRegisterPayload(graph);
179
+ const releases = Array.isArray(appPayload && appPayload.releases) ? appPayload.releases : [];
180
+ const defaultReleaseId = normalizeString(appPayload && appPayload.default_release_id) || (releases[0] && normalizeString(releases[0].release_id)) || null;
181
+ const activeRelease = releases.find((entry) => normalizeString(entry && entry.release_id) === defaultReleaseId) || releases[0] || null;
182
+ nextPayload.metadata = nextPayload.metadata || {};
183
+ nextPayload.metadata.service_catalog = {
184
+ synced_at: new Date().toISOString(),
185
+ source: resourceRef,
186
+ app_name: appName || null,
187
+ default_release_id: defaultReleaseId,
188
+ releases
189
+ };
190
+ if (activeRelease) {
191
+ nextPayload.runtime_release_id = normalizeString(activeRelease.release_id);
192
+ nextPayload.runtime = {
193
+ release_id: normalizeString(activeRelease.release_id),
194
+ runtime_version: normalizeString(activeRelease.runtime_version),
195
+ release_channel: normalizeString(activeRelease.release_channel) || null,
196
+ release_status: normalizeString(activeRelease.release_status) || 'published',
197
+ entrypoint: normalizeString(activeRelease.entrypoint) || null,
198
+ runtime_status: normalizeString(activeRelease.runtime_status) || null,
199
+ release_notes_file: normalizeString(activeRelease.release_notes_file) || null,
200
+ release_evidence_file: normalizeString(activeRelease.release_evidence_file) || null,
201
+ published_at: normalizeString(activeRelease.published_at) || null,
202
+ metadata: activeRelease.metadata && typeof activeRelease.metadata === 'object' ? activeRelease.metadata : {}
203
+ };
204
+ }
205
+ const updated = await stateStore.registerAppBundle(nextPayload);
206
+ synced.push({
207
+ app_id: updated && updated.bundle ? updated.bundle.app_id : appId,
208
+ app_key: updated && updated.bundle ? updated.bundle.app_key : appKey,
209
+ source: resourceRef,
210
+ release_count: releases.length,
211
+ default_release_id: defaultReleaseId || null
212
+ });
213
+ }
214
+ return {
215
+ mode: 'app-registry-sync-service-catalog',
216
+ index_url: indexRef,
217
+ source_generated_at: indexPayload && indexPayload.generated_at ? indexPayload.generated_at : null,
218
+ synced_count: synced.length,
219
+ items: synced
220
+ };
221
+ }
222
+
223
+ module.exports = {
224
+ readJsonResource,
225
+ resolveChildResource,
226
+ syncBundleRegistry,
227
+ syncServiceCatalog
228
+ };
@@ -0,0 +1,276 @@
1
+ const path = require('path');
2
+
3
+ function normalizeSchemaScope(scopeCandidate) {
4
+ const allowed = new Set([
5
+ 'close-loop-session',
6
+ 'batch-session',
7
+ 'controller-session',
8
+ 'governance-session'
9
+ ]);
10
+ const raw = `${scopeCandidate || 'all'}`.trim().toLowerCase();
11
+ const tokens = raw.split(',').map((item) => item.trim()).filter(Boolean);
12
+ if (tokens.length === 0 || tokens.includes('all')) {
13
+ return [...allowed];
14
+ }
15
+ const normalized = [];
16
+ for (const token of tokens) {
17
+ if (!allowed.has(token)) {
18
+ throw new Error('--only must be one of: all, close-loop-session, batch-session, controller-session, governance-session');
19
+ }
20
+ if (!normalized.includes(token)) {
21
+ normalized.push(token);
22
+ }
23
+ }
24
+ return normalized;
25
+ }
26
+
27
+ function normalizeTargetSchemaVersion(targetVersionCandidate) {
28
+ const normalized = `${targetVersionCandidate || ''}`.trim();
29
+ if (!normalized) {
30
+ throw new Error('--target-version is required.');
31
+ }
32
+ if (normalized.length > 32) {
33
+ throw new Error('--target-version must be 32 characters or fewer.');
34
+ }
35
+ return normalized;
36
+ }
37
+
38
+ function getAutoArchiveSchemaTargets(projectPath, scope, dependencies = {}) {
39
+ const {
40
+ getCloseLoopSessionDir,
41
+ getCloseLoopBatchSummaryDir,
42
+ getCloseLoopControllerSessionDir,
43
+ getGovernanceCloseLoopSessionDir
44
+ } = dependencies;
45
+ const allTargets = [
46
+ { id: 'close-loop-session', directory: getCloseLoopSessionDir(projectPath) },
47
+ { id: 'batch-session', directory: getCloseLoopBatchSummaryDir(projectPath) },
48
+ { id: 'controller-session', directory: getCloseLoopControllerSessionDir(projectPath) },
49
+ { id: 'governance-session', directory: getGovernanceCloseLoopSessionDir(projectPath) }
50
+ ];
51
+ const scopeSet = new Set(scope);
52
+ return allTargets.filter((item) => scopeSet.has(item.id));
53
+ }
54
+
55
+ function classifyArchiveSchemaCompatibility(schemaVersion, supportedVersions) {
56
+ const normalized = typeof schemaVersion === 'string' ? schemaVersion.trim() : '';
57
+ if (!normalized) {
58
+ return 'missing_schema_version';
59
+ }
60
+ if (supportedVersions.has(normalized)) {
61
+ return 'compatible';
62
+ }
63
+ return 'incompatible';
64
+ }
65
+
66
+ async function checkAutoArchiveSchema(projectPath, options = {}, dependencies = {}) {
67
+ const {
68
+ fs,
69
+ calculatePercent,
70
+ getCloseLoopSessionDir,
71
+ getCloseLoopBatchSummaryDir,
72
+ getCloseLoopControllerSessionDir,
73
+ getGovernanceCloseLoopSessionDir,
74
+ supportedVersions,
75
+ now = () => new Date()
76
+ } = dependencies;
77
+ const scope = normalizeSchemaScope(options.only);
78
+ const targets = getAutoArchiveSchemaTargets(projectPath, scope, {
79
+ getCloseLoopSessionDir,
80
+ getCloseLoopBatchSummaryDir,
81
+ getCloseLoopControllerSessionDir,
82
+ getGovernanceCloseLoopSessionDir
83
+ });
84
+ const archives = [];
85
+
86
+ for (const target of targets) {
87
+ const archiveSummary = {
88
+ id: target.id,
89
+ directory: target.directory,
90
+ total_files: 0,
91
+ compatible_files: 0,
92
+ missing_schema_version_files: 0,
93
+ incompatible_files: 0,
94
+ parse_error_files: 0,
95
+ issues: []
96
+ };
97
+ if (!(await fs.pathExists(target.directory))) {
98
+ archives.push(archiveSummary);
99
+ continue;
100
+ }
101
+ const files = (await fs.readdir(target.directory))
102
+ .filter((item) => item.toLowerCase().endsWith('.json'))
103
+ .sort((left, right) => left.localeCompare(right));
104
+ archiveSummary.total_files = files.length;
105
+
106
+ for (const file of files) {
107
+ const filePath = path.join(target.directory, file);
108
+ let payload = null;
109
+ try {
110
+ payload = await fs.readJson(filePath);
111
+ } catch (error) {
112
+ archiveSummary.parse_error_files += 1;
113
+ archiveSummary.issues.push({ file: filePath, compatibility: 'parse_error', error: error.message });
114
+ continue;
115
+ }
116
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
117
+ archiveSummary.parse_error_files += 1;
118
+ archiveSummary.issues.push({ file: filePath, compatibility: 'parse_error', error: 'invalid JSON root type' });
119
+ continue;
120
+ }
121
+ const schemaVersion = typeof payload.schema_version === 'string' ? payload.schema_version.trim() : '';
122
+ const compatibility = classifyArchiveSchemaCompatibility(schemaVersion, supportedVersions);
123
+ if (compatibility === 'compatible') {
124
+ archiveSummary.compatible_files += 1;
125
+ } else if (compatibility === 'missing_schema_version') {
126
+ archiveSummary.missing_schema_version_files += 1;
127
+ archiveSummary.issues.push({ file: filePath, compatibility, schema_version: null });
128
+ } else {
129
+ archiveSummary.incompatible_files += 1;
130
+ archiveSummary.issues.push({ file: filePath, compatibility, schema_version: schemaVersion });
131
+ }
132
+ }
133
+ archives.push(archiveSummary);
134
+ }
135
+
136
+ const totals = archives.reduce((acc, item) => ({
137
+ total_files: acc.total_files + item.total_files,
138
+ compatible_files: acc.compatible_files + item.compatible_files,
139
+ missing_schema_version_files: acc.missing_schema_version_files + item.missing_schema_version_files,
140
+ incompatible_files: acc.incompatible_files + item.incompatible_files,
141
+ parse_error_files: acc.parse_error_files + item.parse_error_files
142
+ }), {
143
+ total_files: 0,
144
+ compatible_files: 0,
145
+ missing_schema_version_files: 0,
146
+ incompatible_files: 0,
147
+ parse_error_files: 0
148
+ });
149
+
150
+ const nowValue = now();
151
+ return {
152
+ mode: 'auto-schema-check',
153
+ generated_at: nowValue instanceof Date ? nowValue.toISOString() : new Date(nowValue).toISOString(),
154
+ supported_versions: [...supportedVersions],
155
+ scope,
156
+ summary: {
157
+ ...totals,
158
+ compatibility_rate_percent: calculatePercent(totals.compatible_files, totals.total_files)
159
+ },
160
+ archives
161
+ };
162
+ }
163
+
164
+ async function migrateAutoArchiveSchema(projectPath, options = {}, dependencies = {}) {
165
+ const {
166
+ fs,
167
+ getCloseLoopSessionDir,
168
+ getCloseLoopBatchSummaryDir,
169
+ getCloseLoopControllerSessionDir,
170
+ getGovernanceCloseLoopSessionDir,
171
+ defaultVersion,
172
+ now = () => new Date()
173
+ } = dependencies;
174
+ const scope = normalizeSchemaScope(options.only);
175
+ const targetVersion = normalizeTargetSchemaVersion(options.targetVersion || defaultVersion);
176
+ const dryRun = !options.apply;
177
+ const targets = getAutoArchiveSchemaTargets(projectPath, scope, {
178
+ getCloseLoopSessionDir,
179
+ getCloseLoopBatchSummaryDir,
180
+ getCloseLoopControllerSessionDir,
181
+ getGovernanceCloseLoopSessionDir
182
+ });
183
+ const archives = [];
184
+
185
+ for (const target of targets) {
186
+ const archiveSummary = {
187
+ id: target.id,
188
+ directory: target.directory,
189
+ total_files: 0,
190
+ candidate_files: 0,
191
+ updated_files: 0,
192
+ skipped_compatible_files: 0,
193
+ parse_error_files: 0,
194
+ updates: [],
195
+ errors: []
196
+ };
197
+ if (!(await fs.pathExists(target.directory))) {
198
+ archives.push(archiveSummary);
199
+ continue;
200
+ }
201
+
202
+ const files = (await fs.readdir(target.directory))
203
+ .filter((item) => item.toLowerCase().endsWith('.json'))
204
+ .sort((left, right) => left.localeCompare(right));
205
+ archiveSummary.total_files = files.length;
206
+
207
+ for (const file of files) {
208
+ const filePath = path.join(target.directory, file);
209
+ let payload = null;
210
+ try {
211
+ payload = await fs.readJson(filePath);
212
+ } catch (error) {
213
+ archiveSummary.parse_error_files += 1;
214
+ archiveSummary.errors.push({ file: filePath, error: error.message });
215
+ continue;
216
+ }
217
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
218
+ archiveSummary.parse_error_files += 1;
219
+ archiveSummary.errors.push({ file: filePath, error: 'invalid JSON root type' });
220
+ continue;
221
+ }
222
+
223
+ const previousVersion = typeof payload.schema_version === 'string' ? payload.schema_version.trim() : '';
224
+ if (previousVersion === targetVersion) {
225
+ archiveSummary.skipped_compatible_files += 1;
226
+ continue;
227
+ }
228
+
229
+ archiveSummary.candidate_files += 1;
230
+ if (dryRun) {
231
+ archiveSummary.updates.push({ file: filePath, from: previousVersion || null, to: targetVersion });
232
+ continue;
233
+ }
234
+
235
+ payload.schema_version = targetVersion;
236
+ await fs.writeJson(filePath, payload, { spaces: 2 });
237
+ archiveSummary.updated_files += 1;
238
+ archiveSummary.updates.push({ file: filePath, from: previousVersion || null, to: targetVersion });
239
+ }
240
+ archives.push(archiveSummary);
241
+ }
242
+
243
+ const totals = archives.reduce((acc, item) => ({
244
+ total_files: acc.total_files + item.total_files,
245
+ candidate_files: acc.candidate_files + item.candidate_files,
246
+ updated_files: acc.updated_files + item.updated_files,
247
+ skipped_compatible_files: acc.skipped_compatible_files + item.skipped_compatible_files,
248
+ parse_error_files: acc.parse_error_files + item.parse_error_files
249
+ }), {
250
+ total_files: 0,
251
+ candidate_files: 0,
252
+ updated_files: 0,
253
+ skipped_compatible_files: 0,
254
+ parse_error_files: 0
255
+ });
256
+
257
+ const nowValue = now();
258
+ return {
259
+ mode: 'auto-schema-migrate',
260
+ generated_at: nowValue instanceof Date ? nowValue.toISOString() : new Date(nowValue).toISOString(),
261
+ dry_run: dryRun,
262
+ target_version: targetVersion,
263
+ scope,
264
+ summary: totals,
265
+ archives
266
+ };
267
+ }
268
+
269
+ module.exports = {
270
+ normalizeSchemaScope,
271
+ normalizeTargetSchemaVersion,
272
+ getAutoArchiveSchemaTargets,
273
+ classifyArchiveSchemaCompatibility,
274
+ checkAutoArchiveSchema,
275
+ migrateAutoArchiveSchema
276
+ };
@@ -0,0 +1,60 @@
1
+ function normalizeStatusToken(statusCandidate) {
2
+ return String(statusCandidate || '').trim().toLowerCase();
3
+ }
4
+
5
+ function isCompletedStatus(statusCandidate) {
6
+ return normalizeStatusToken(statusCandidate) === 'completed';
7
+ }
8
+
9
+ function isFailedStatus(statusCandidate) {
10
+ return ['failed', 'partial-failed', 'error', 'invalid'].includes(normalizeStatusToken(statusCandidate));
11
+ }
12
+
13
+ function normalizeStatsWindowDays(daysCandidate) {
14
+ if (daysCandidate === undefined || daysCandidate === null) {
15
+ return null;
16
+ }
17
+ const parsed = Number(daysCandidate);
18
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 36500) {
19
+ throw new Error('--days must be an integer between 0 and 36500.');
20
+ }
21
+ return parsed;
22
+ }
23
+
24
+ function filterEntriesByStatus(entries, statusFilter = []) {
25
+ if (!Array.isArray(statusFilter) || statusFilter.length === 0) {
26
+ return Array.isArray(entries) ? entries : [];
27
+ }
28
+ const statusSet = new Set(statusFilter.map((item) => normalizeStatusToken(item)).filter(Boolean));
29
+ return (Array.isArray(entries) ? entries : []).filter((entry) => statusSet.has(normalizeStatusToken(entry && entry.status)));
30
+ }
31
+
32
+ function filterGovernanceEntriesByResumeMode(entries, resumeOnly = false) {
33
+ const safeEntries = Array.isArray(entries) ? entries : [];
34
+ if (!resumeOnly) {
35
+ return safeEntries;
36
+ }
37
+ return safeEntries.filter((entry) => (
38
+ typeof (entry && entry.resumed_from_governance_session_id) === 'string' &&
39
+ String(entry.resumed_from_governance_session_id).trim().length > 0
40
+ ));
41
+ }
42
+
43
+ function calculatePercent(numerator, denominator) {
44
+ const safeNumerator = Number(numerator) || 0;
45
+ const safeDenominator = Number(denominator) || 0;
46
+ if (safeDenominator <= 0) {
47
+ return 0;
48
+ }
49
+ return Number(((safeNumerator / safeDenominator) * 100).toFixed(2));
50
+ }
51
+
52
+ module.exports = {
53
+ normalizeStatusToken,
54
+ isCompletedStatus,
55
+ isFailedStatus,
56
+ normalizeStatsWindowDays,
57
+ filterEntriesByStatus,
58
+ filterGovernanceEntriesByResumeMode,
59
+ calculatePercent
60
+ };