scene-capability-engine 3.6.39 → 3.6.45
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 +59 -0
- package/bin/scene-capability-engine.js +78 -4
- package/docs/command-reference.md +5 -0
- package/docs/developer-guide.md +1 -1
- package/docs/releases/README.md +6 -0
- package/docs/releases/v3.6.40.md +19 -0
- package/docs/releases/v3.6.41.md +20 -0
- package/docs/releases/v3.6.42.md +19 -0
- package/docs/releases/v3.6.43.md +17 -0
- package/docs/releases/v3.6.44.md +17 -0
- package/docs/releases/v3.6.45.md +18 -0
- package/docs/spec-collaboration-guide.md +1 -1
- package/docs/zh/releases/README.md +6 -0
- package/docs/zh/releases/v3.6.40.md +19 -0
- package/docs/zh/releases/v3.6.41.md +20 -0
- package/docs/zh/releases/v3.6.42.md +19 -0
- package/docs/zh/releases/v3.6.43.md +17 -0
- package/docs/zh/releases/v3.6.44.md +17 -0
- package/docs/zh/releases/v3.6.45.md +18 -0
- package/lib/adoption/adoption-logger.js +1 -1
- package/lib/adoption/adoption-strategy.js +29 -29
- package/lib/adoption/detection-engine.js +16 -13
- package/lib/adoption/smart-orchestrator.js +3 -3
- package/lib/adoption/strategy-selector.js +19 -15
- package/lib/adoption/template-sync.js +3 -3
- package/lib/auto/autonomous-engine.js +5 -5
- package/lib/auto/handoff-release-gate-history-loaders-service.js +24 -4
- package/lib/auto/handoff-run-service.js +37 -0
- package/lib/backup/backup-system.js +10 -10
- package/lib/collab/collab-manager.js +8 -5
- package/lib/collab/dependency-manager.js +1 -1
- package/lib/commands/adopt.js +2 -2
- package/lib/commands/auto.js +239 -97
- package/lib/commands/collab.js +10 -4
- package/lib/commands/status.js +3 -3
- package/lib/commands/studio.js +8 -0
- package/lib/repo/config-manager.js +2 -2
- package/lib/spec/bootstrap/context-collector.js +5 -4
- package/lib/spec-gate/rules/default-rules.js +8 -8
- package/lib/upgrade/migration-engine.js +5 -5
- package/lib/upgrade/migrations/1.0.0-to-1.1.0.js +3 -3
- package/lib/utils/tool-detector.js +4 -4
- package/lib/utils/validation.js +6 -6
- package/lib/workspace/collab-governance-audit.js +575 -0
- package/lib/workspace/multi/workspace-context-resolver.js +3 -3
- package/lib/workspace/multi/workspace-registry.js +3 -3
- package/lib/workspace/multi/workspace-state-manager.js +3 -3
- package/lib/workspace/spec-delivery-audit.js +553 -0
- package/package.json +1 -1
|
@@ -94,14 +94,14 @@ class WorkspaceContextResolver {
|
|
|
94
94
|
*/
|
|
95
95
|
async isValidSceDirectory(dirPath) {
|
|
96
96
|
try {
|
|
97
|
-
const
|
|
98
|
-
const exists = await fs.pathExists(
|
|
97
|
+
const scePath = path.join(dirPath, '.sce');
|
|
98
|
+
const exists = await fs.pathExists(scePath);
|
|
99
99
|
|
|
100
100
|
if (!exists) {
|
|
101
101
|
return false;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
const stats = await fs.stat(
|
|
104
|
+
const stats = await fs.stat(scePath);
|
|
105
105
|
return stats.isDirectory();
|
|
106
106
|
} catch (error) {
|
|
107
107
|
return false;
|
|
@@ -84,10 +84,10 @@ class WorkspaceRegistry {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
// Check if .sce directory exists
|
|
87
|
-
const
|
|
88
|
-
const
|
|
87
|
+
const scePath = path.join(workspacePath, '.sce');
|
|
88
|
+
const sceExists = await fs.pathExists(scePath);
|
|
89
89
|
|
|
90
|
-
return
|
|
90
|
+
return sceExists;
|
|
91
91
|
} catch (error) {
|
|
92
92
|
return false;
|
|
93
93
|
}
|
|
@@ -192,9 +192,9 @@ class WorkspaceStateManager {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
// Validate path (check for .sce directory)
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
if (!
|
|
195
|
+
const scePath = path.join(workspacePath, '.sce');
|
|
196
|
+
const sceExists = await fs.pathExists(scePath);
|
|
197
|
+
if (!sceExists) {
|
|
198
198
|
throw new Error(`Path "${workspacePath}" is not a valid sce project directory. ` +
|
|
199
199
|
`Ensure it exists and contains a .sce/ directory.`);
|
|
200
200
|
}
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs-extra');
|
|
5
|
+
const { minimatch } = require('minimatch');
|
|
6
|
+
const {
|
|
7
|
+
runGit,
|
|
8
|
+
parseRemotes,
|
|
9
|
+
parseAheadBehind
|
|
10
|
+
} = require('../../scripts/git-managed-gate');
|
|
11
|
+
|
|
12
|
+
const DELIVERY_MANIFEST_FILE = 'deliverables.json';
|
|
13
|
+
const DELIVERY_VERIFICATION_MODES = new Set(['blocking', 'advisory']);
|
|
14
|
+
|
|
15
|
+
function shouldAllowDetachedHeadSync(options = {}) {
|
|
16
|
+
if (typeof options.allowDetachedHead === 'boolean') {
|
|
17
|
+
return options.allowDetachedHead;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const githubActions = `${process.env.GITHUB_ACTIONS || ''}`.trim().toLowerCase() === 'true';
|
|
21
|
+
const githubRefType = `${process.env.GITHUB_REF_TYPE || ''}`.trim().toLowerCase();
|
|
22
|
+
const githubRef = `${process.env.GITHUB_REF || ''}`.trim();
|
|
23
|
+
return githubActions && (
|
|
24
|
+
githubRefType === 'tag' ||
|
|
25
|
+
githubRef.startsWith('refs/tags/')
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeRelativePath(projectRoot, candidate) {
|
|
30
|
+
if (typeof candidate !== 'string') {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const trimmed = candidate.trim();
|
|
35
|
+
if (!trimmed) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const absolutePath = path.isAbsolute(trimmed)
|
|
40
|
+
? trimmed
|
|
41
|
+
: path.join(projectRoot, trimmed);
|
|
42
|
+
const relativePath = path.relative(projectRoot, absolutePath);
|
|
43
|
+
const normalized = `${relativePath}`.replace(/\\/g, '/');
|
|
44
|
+
|
|
45
|
+
if (!normalized || normalized.startsWith('..')) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizePathList(projectRoot, values) {
|
|
53
|
+
if (!Array.isArray(values)) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
const results = [];
|
|
59
|
+
for (const value of values) {
|
|
60
|
+
const normalized = normalizeRelativePath(projectRoot, value);
|
|
61
|
+
if (!normalized || seen.has(normalized)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
seen.add(normalized);
|
|
65
|
+
results.push(normalized);
|
|
66
|
+
}
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizePatternList(values) {
|
|
71
|
+
if (!Array.isArray(values)) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
const results = [];
|
|
77
|
+
for (const value of values) {
|
|
78
|
+
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
79
|
+
if (!normalized || seen.has(normalized)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
seen.add(normalized);
|
|
83
|
+
results.push(normalized);
|
|
84
|
+
}
|
|
85
|
+
return results;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseManifest(projectRoot, specName, payload) {
|
|
89
|
+
const manifest = payload && typeof payload === 'object' && !Array.isArray(payload)
|
|
90
|
+
? payload
|
|
91
|
+
: {};
|
|
92
|
+
const verificationMode = DELIVERY_VERIFICATION_MODES.has(manifest.verification_mode)
|
|
93
|
+
? manifest.verification_mode
|
|
94
|
+
: 'blocking';
|
|
95
|
+
return {
|
|
96
|
+
spec: specName,
|
|
97
|
+
verification_mode: verificationMode,
|
|
98
|
+
declared_files: normalizePathList(projectRoot, manifest.declared_files),
|
|
99
|
+
optional_files: normalizePathList(projectRoot, manifest.optional_files),
|
|
100
|
+
ignored_patterns: normalizePatternList(manifest.ignored_patterns)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseStatusEntries(raw = '') {
|
|
105
|
+
const entries = new Map();
|
|
106
|
+
const lines = `${raw || ''}`.split(/\r?\n/).map((line) => line.trimEnd()).filter(Boolean);
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
if (line.length < 3) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const xy = line.slice(0, 2);
|
|
112
|
+
const payload = line.slice(3).trim();
|
|
113
|
+
const targetPath = payload.includes(' -> ') ? payload.split(' -> ').pop().trim() : payload;
|
|
114
|
+
const normalizedPath = `${targetPath}`.replace(/\\/g, '/');
|
|
115
|
+
entries.set(normalizedPath, {
|
|
116
|
+
raw: line,
|
|
117
|
+
x: xy[0],
|
|
118
|
+
y: xy[1]
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return entries;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resolveWorktreeStatus(statusEntry) {
|
|
125
|
+
if (!statusEntry) {
|
|
126
|
+
return 'unmodified';
|
|
127
|
+
}
|
|
128
|
+
if (statusEntry.x === '?' && statusEntry.y === '?') {
|
|
129
|
+
return 'untracked';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const statusToken = [statusEntry.x, statusEntry.y].find((value) => value && value !== ' ');
|
|
133
|
+
switch (statusToken) {
|
|
134
|
+
case 'M':
|
|
135
|
+
return 'modified';
|
|
136
|
+
case 'A':
|
|
137
|
+
return 'added';
|
|
138
|
+
case 'D':
|
|
139
|
+
return 'deleted';
|
|
140
|
+
case 'R':
|
|
141
|
+
return 'renamed';
|
|
142
|
+
case 'C':
|
|
143
|
+
return 'copied';
|
|
144
|
+
case 'U':
|
|
145
|
+
return 'unmerged';
|
|
146
|
+
default:
|
|
147
|
+
return 'unmodified';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function shouldIgnorePath(filePath, ignoredPatterns = []) {
|
|
152
|
+
return ignoredPatterns.some((pattern) => minimatch(filePath, pattern, { dot: true }));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function discoverDeliveryManifests(projectRoot, options = {}, dependencies = {}) {
|
|
156
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
157
|
+
const specRoot = path.join(projectRoot, '.sce', 'specs');
|
|
158
|
+
const specFilter = typeof options.spec === 'string' && options.spec.trim().length > 0
|
|
159
|
+
? options.spec.trim()
|
|
160
|
+
: null;
|
|
161
|
+
|
|
162
|
+
const exists = await fileSystem.pathExists(specRoot);
|
|
163
|
+
if (!exists) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const entries = await fileSystem.readdir(specRoot, { withFileTypes: true });
|
|
168
|
+
const manifests = [];
|
|
169
|
+
for (const entry of entries) {
|
|
170
|
+
if (!entry || !entry.isDirectory()) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (specFilter && entry.name !== specFilter) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const manifestPath = path.join(specRoot, entry.name, DELIVERY_MANIFEST_FILE);
|
|
177
|
+
if (!await fileSystem.pathExists(manifestPath)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
manifests.push({
|
|
181
|
+
spec: entry.name,
|
|
182
|
+
manifest_path: manifestPath
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return manifests.sort((left, right) => left.spec.localeCompare(right.spec));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function loadGitSnapshot(projectRoot, options = {}, dependencies = {}) {
|
|
189
|
+
const runGitCommand = dependencies.runGit || runGit;
|
|
190
|
+
const allowNoRemote = options.allowNoRemote !== false;
|
|
191
|
+
const allowDetachedHead = shouldAllowDetachedHeadSync(options);
|
|
192
|
+
const targetHosts = Array.isArray(options.targetHosts) && options.targetHosts.length > 0
|
|
193
|
+
? options.targetHosts
|
|
194
|
+
: ['github.com', 'gitlab.com'];
|
|
195
|
+
|
|
196
|
+
const inside = runGitCommand(projectRoot, ['rev-parse', '--is-inside-work-tree']);
|
|
197
|
+
if (inside.status !== 0 || `${inside.stdout || ''}`.trim().toLowerCase() !== 'true') {
|
|
198
|
+
return {
|
|
199
|
+
available: false,
|
|
200
|
+
passed: false,
|
|
201
|
+
reason: 'not-a-git-repository',
|
|
202
|
+
target_hosts: targetHosts,
|
|
203
|
+
tracked_files: new Set(),
|
|
204
|
+
status_entries: new Map(),
|
|
205
|
+
warnings: [],
|
|
206
|
+
violations: ['current directory is not a git repository'],
|
|
207
|
+
branch: null,
|
|
208
|
+
upstream: null,
|
|
209
|
+
ahead: null,
|
|
210
|
+
behind: null,
|
|
211
|
+
has_target_remote: false,
|
|
212
|
+
clean_worktree: null,
|
|
213
|
+
worktree_changes: {
|
|
214
|
+
tracked_count: 0,
|
|
215
|
+
untracked_count: 0
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const warnings = [];
|
|
221
|
+
const violations = [];
|
|
222
|
+
const trackedFilesResult = runGitCommand(projectRoot, ['ls-files']);
|
|
223
|
+
const trackedFiles = trackedFilesResult.status === 0
|
|
224
|
+
? new Set(
|
|
225
|
+
`${trackedFilesResult.stdout || ''}`
|
|
226
|
+
.split(/\r?\n/)
|
|
227
|
+
.map((item) => item.trim())
|
|
228
|
+
.filter(Boolean)
|
|
229
|
+
.map((item) => item.replace(/\\/g, '/'))
|
|
230
|
+
)
|
|
231
|
+
: new Set();
|
|
232
|
+
if (trackedFilesResult.status !== 0) {
|
|
233
|
+
violations.push(`failed to list tracked files: ${trackedFilesResult.stderr || 'unknown error'}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const statusResult = runGitCommand(projectRoot, ['status', '--porcelain']);
|
|
237
|
+
const statusEntries = statusResult.status === 0
|
|
238
|
+
? parseStatusEntries(statusResult.stdout)
|
|
239
|
+
: new Map();
|
|
240
|
+
|
|
241
|
+
let trackedChanges = 0;
|
|
242
|
+
let untrackedChanges = 0;
|
|
243
|
+
for (const entry of statusEntries.values()) {
|
|
244
|
+
if (entry.x === '?' && entry.y === '?') {
|
|
245
|
+
untrackedChanges += 1;
|
|
246
|
+
} else {
|
|
247
|
+
trackedChanges += 1;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const remotesResult = runGitCommand(projectRoot, ['remote', '-v']);
|
|
252
|
+
const remoteInfo = remotesResult.status === 0
|
|
253
|
+
? parseRemotes(remotesResult.stdout, targetHosts)
|
|
254
|
+
: { allRemotes: [], targetRemotes: [] };
|
|
255
|
+
const hasTargetRemote = remoteInfo.targetRemotes.length > 0;
|
|
256
|
+
if (remotesResult.status !== 0) {
|
|
257
|
+
warnings.push(`failed to read git remotes: ${remotesResult.stderr || 'unknown error'}`);
|
|
258
|
+
} else if (!hasTargetRemote) {
|
|
259
|
+
if (allowNoRemote) {
|
|
260
|
+
warnings.push('no GitHub/GitLab remote configured; sync proof is advisory only');
|
|
261
|
+
} else {
|
|
262
|
+
violations.push('no GitHub/GitLab remote configured');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const branchResult = runGitCommand(projectRoot, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
267
|
+
const branch = branchResult.status === 0 ? `${branchResult.stdout || ''}`.trim() : null;
|
|
268
|
+
if (branchResult.status !== 0) {
|
|
269
|
+
warnings.push(`failed to resolve branch: ${branchResult.stderr || 'unknown error'}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let upstream = null;
|
|
273
|
+
let ahead = null;
|
|
274
|
+
let behind = null;
|
|
275
|
+
if (hasTargetRemote) {
|
|
276
|
+
if (branch === 'HEAD' && allowDetachedHead) {
|
|
277
|
+
warnings.push('detached HEAD release checkout detected; upstream tracking check skipped');
|
|
278
|
+
} else {
|
|
279
|
+
const upstreamResult = runGitCommand(projectRoot, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
|
|
280
|
+
if (upstreamResult.status !== 0) {
|
|
281
|
+
violations.push('current branch has no upstream tracking branch');
|
|
282
|
+
} else {
|
|
283
|
+
upstream = `${upstreamResult.stdout || ''}`.trim();
|
|
284
|
+
const aheadBehindResult = runGitCommand(projectRoot, ['rev-list', '--left-right', '--count', 'HEAD...@{u}']);
|
|
285
|
+
if (aheadBehindResult.status !== 0) {
|
|
286
|
+
violations.push(`failed to compare with upstream: ${aheadBehindResult.stderr || 'unknown error'}`);
|
|
287
|
+
} else {
|
|
288
|
+
const parsed = parseAheadBehind(aheadBehindResult.stdout);
|
|
289
|
+
ahead = parsed.ahead;
|
|
290
|
+
behind = parsed.behind;
|
|
291
|
+
if (Number.isFinite(ahead) && ahead > 0) {
|
|
292
|
+
violations.push(`branch is ahead of upstream by ${ahead} commit(s); push required`);
|
|
293
|
+
}
|
|
294
|
+
if (Number.isFinite(behind) && behind > 0) {
|
|
295
|
+
violations.push(`branch is behind upstream by ${behind} commit(s); sync required`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
available: true,
|
|
304
|
+
passed: violations.length === 0,
|
|
305
|
+
reason: violations.length === 0 ? 'synced' : 'violations',
|
|
306
|
+
target_hosts: targetHosts,
|
|
307
|
+
remotes: remoteInfo.allRemotes,
|
|
308
|
+
target_remotes: remoteInfo.targetRemotes,
|
|
309
|
+
tracked_files: trackedFiles,
|
|
310
|
+
status_entries: statusEntries,
|
|
311
|
+
warnings,
|
|
312
|
+
violations,
|
|
313
|
+
branch,
|
|
314
|
+
upstream,
|
|
315
|
+
ahead,
|
|
316
|
+
behind,
|
|
317
|
+
has_target_remote: hasTargetRemote,
|
|
318
|
+
clean_worktree: trackedChanges === 0 && untrackedChanges === 0,
|
|
319
|
+
worktree_changes: {
|
|
320
|
+
tracked_count: trackedChanges,
|
|
321
|
+
untracked_count: untrackedChanges
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function auditSpecDeliverySync(projectRoot = process.cwd(), options = {}, dependencies = {}) {
|
|
327
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
328
|
+
const manifests = await discoverDeliveryManifests(projectRoot, options, dependencies);
|
|
329
|
+
const requireManifest = options.requireManifest === true;
|
|
330
|
+
const git = manifests.length > 0
|
|
331
|
+
? loadGitSnapshot(projectRoot, options, dependencies)
|
|
332
|
+
: {
|
|
333
|
+
available: false,
|
|
334
|
+
passed: true,
|
|
335
|
+
reason: 'not-required',
|
|
336
|
+
tracked_files: new Set(),
|
|
337
|
+
status_entries: new Map(),
|
|
338
|
+
warnings: [],
|
|
339
|
+
violations: [],
|
|
340
|
+
branch: null,
|
|
341
|
+
upstream: null,
|
|
342
|
+
ahead: null,
|
|
343
|
+
behind: null,
|
|
344
|
+
has_target_remote: false,
|
|
345
|
+
clean_worktree: null,
|
|
346
|
+
worktree_changes: {
|
|
347
|
+
tracked_count: 0,
|
|
348
|
+
untracked_count: 0
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const report = {
|
|
353
|
+
mode: 'spec-delivery-audit',
|
|
354
|
+
generated_at: new Date().toISOString(),
|
|
355
|
+
root: projectRoot,
|
|
356
|
+
spec: typeof options.spec === 'string' && options.spec.trim().length > 0 ? options.spec.trim() : null,
|
|
357
|
+
require_manifest: requireManifest,
|
|
358
|
+
manifests: [],
|
|
359
|
+
git: {
|
|
360
|
+
available: git.available === true,
|
|
361
|
+
passed: git.passed === true,
|
|
362
|
+
reason: git.reason,
|
|
363
|
+
warnings: git.warnings,
|
|
364
|
+
violations: git.violations,
|
|
365
|
+
branch: git.branch,
|
|
366
|
+
upstream: git.upstream,
|
|
367
|
+
ahead: git.ahead,
|
|
368
|
+
behind: git.behind,
|
|
369
|
+
has_target_remote: git.has_target_remote,
|
|
370
|
+
clean_worktree: git.clean_worktree,
|
|
371
|
+
worktree_changes: git.worktree_changes
|
|
372
|
+
},
|
|
373
|
+
summary: {
|
|
374
|
+
manifest_count: manifests.length,
|
|
375
|
+
blocking_manifest_count: 0,
|
|
376
|
+
advisory_manifest_count: 0,
|
|
377
|
+
passed_manifests: 0,
|
|
378
|
+
failed_manifests: 0,
|
|
379
|
+
declared_files: 0,
|
|
380
|
+
missing_declared_files: 0,
|
|
381
|
+
untracked_declared_files: 0,
|
|
382
|
+
dirty_declared_files: 0
|
|
383
|
+
},
|
|
384
|
+
warnings: [],
|
|
385
|
+
violations: [],
|
|
386
|
+
passed: true,
|
|
387
|
+
reason: 'passed'
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
if (manifests.length === 0) {
|
|
391
|
+
if (requireManifest) {
|
|
392
|
+
report.passed = false;
|
|
393
|
+
report.reason = 'missing-manifest';
|
|
394
|
+
report.violations.push(
|
|
395
|
+
report.spec
|
|
396
|
+
? `no delivery manifest found for spec "${report.spec}"`
|
|
397
|
+
: 'no delivery manifests found under .sce/specs'
|
|
398
|
+
);
|
|
399
|
+
} else {
|
|
400
|
+
report.reason = 'no-manifests';
|
|
401
|
+
report.warnings.push('no delivery manifests found; delivery sync audit is advisory only');
|
|
402
|
+
}
|
|
403
|
+
return report;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
for (const item of manifests) {
|
|
407
|
+
let manifestPayload;
|
|
408
|
+
try {
|
|
409
|
+
manifestPayload = await fileSystem.readJson(item.manifest_path);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
const manifestReport = {
|
|
412
|
+
spec: item.spec,
|
|
413
|
+
manifest_file: normalizeRelativePath(projectRoot, item.manifest_path),
|
|
414
|
+
verification_mode: 'blocking',
|
|
415
|
+
declared_files: [],
|
|
416
|
+
optional_files: [],
|
|
417
|
+
ignored_patterns: [],
|
|
418
|
+
files: [],
|
|
419
|
+
warnings: [],
|
|
420
|
+
violations: [`invalid delivery manifest: ${error.message}`],
|
|
421
|
+
passed: false,
|
|
422
|
+
summary: {
|
|
423
|
+
declared_count: 0,
|
|
424
|
+
missing_declared_files: 0,
|
|
425
|
+
untracked_declared_files: 0,
|
|
426
|
+
dirty_declared_files: 0
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
report.manifests.push(manifestReport);
|
|
430
|
+
report.summary.blocking_manifest_count += 1;
|
|
431
|
+
report.summary.failed_manifests += 1;
|
|
432
|
+
report.violations.push(`[${item.spec}] invalid delivery manifest: ${error.message}`);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const manifest = parseManifest(projectRoot, item.spec, manifestPayload);
|
|
437
|
+
const manifestReport = {
|
|
438
|
+
spec: item.spec,
|
|
439
|
+
manifest_file: normalizeRelativePath(projectRoot, item.manifest_path),
|
|
440
|
+
verification_mode: manifest.verification_mode,
|
|
441
|
+
declared_files: manifest.declared_files,
|
|
442
|
+
optional_files: manifest.optional_files,
|
|
443
|
+
ignored_patterns: manifest.ignored_patterns,
|
|
444
|
+
files: [],
|
|
445
|
+
warnings: [],
|
|
446
|
+
violations: [],
|
|
447
|
+
passed: true,
|
|
448
|
+
summary: {
|
|
449
|
+
declared_count: manifest.declared_files.length,
|
|
450
|
+
missing_declared_files: 0,
|
|
451
|
+
untracked_declared_files: 0,
|
|
452
|
+
dirty_declared_files: 0
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
if (manifest.verification_mode === 'blocking') {
|
|
457
|
+
report.summary.blocking_manifest_count += 1;
|
|
458
|
+
} else {
|
|
459
|
+
report.summary.advisory_manifest_count += 1;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const declaredFiles = manifest.declared_files.filter((filePath) => !shouldIgnorePath(filePath, manifest.ignored_patterns));
|
|
463
|
+
report.summary.declared_files += declaredFiles.length;
|
|
464
|
+
|
|
465
|
+
for (const filePath of declaredFiles) {
|
|
466
|
+
const absolutePath = path.join(projectRoot, filePath);
|
|
467
|
+
const exists = await fileSystem.pathExists(absolutePath);
|
|
468
|
+
const tracked = git.tracked_files.has(filePath);
|
|
469
|
+
const worktreeStatus = resolveWorktreeStatus(git.status_entries.get(filePath));
|
|
470
|
+
const fileResult = {
|
|
471
|
+
path: filePath,
|
|
472
|
+
exists,
|
|
473
|
+
tracked,
|
|
474
|
+
worktree_status: worktreeStatus,
|
|
475
|
+
issues: []
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
if (!exists) {
|
|
479
|
+
fileResult.issues.push('missing');
|
|
480
|
+
manifestReport.summary.missing_declared_files += 1;
|
|
481
|
+
report.summary.missing_declared_files += 1;
|
|
482
|
+
}
|
|
483
|
+
if (!tracked) {
|
|
484
|
+
fileResult.issues.push('not-tracked');
|
|
485
|
+
manifestReport.summary.untracked_declared_files += 1;
|
|
486
|
+
report.summary.untracked_declared_files += 1;
|
|
487
|
+
}
|
|
488
|
+
if (tracked && worktreeStatus !== 'unmodified') {
|
|
489
|
+
fileResult.issues.push(`dirty:${worktreeStatus}`);
|
|
490
|
+
manifestReport.summary.dirty_declared_files += 1;
|
|
491
|
+
report.summary.dirty_declared_files += 1;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (fileResult.issues.length > 0) {
|
|
495
|
+
const reason = `${filePath} => ${fileResult.issues.join(', ')}`;
|
|
496
|
+
if (manifest.verification_mode === 'blocking') {
|
|
497
|
+
manifestReport.violations.push(reason);
|
|
498
|
+
} else {
|
|
499
|
+
manifestReport.warnings.push(reason);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
manifestReport.files.push(fileResult);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (git.available !== true) {
|
|
507
|
+
const reason = git.violations[0] || 'git repository unavailable';
|
|
508
|
+
if (manifest.verification_mode === 'blocking') {
|
|
509
|
+
manifestReport.violations.push(reason);
|
|
510
|
+
} else {
|
|
511
|
+
manifestReport.warnings.push(reason);
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
for (const gitViolation of git.violations) {
|
|
515
|
+
if (gitViolation.includes('ahead of upstream') || gitViolation.includes('behind upstream') || gitViolation.includes('no upstream tracking branch')) {
|
|
516
|
+
if (manifest.verification_mode === 'blocking') {
|
|
517
|
+
manifestReport.violations.push(gitViolation);
|
|
518
|
+
} else {
|
|
519
|
+
manifestReport.warnings.push(gitViolation);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (git.has_target_remote !== true) {
|
|
524
|
+
manifestReport.warnings.push('no GitHub/GitLab remote configured; cannot prove cross-machine delivery sync');
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
manifestReport.passed = manifestReport.violations.length === 0;
|
|
529
|
+
if (manifestReport.passed) {
|
|
530
|
+
report.summary.passed_manifests += 1;
|
|
531
|
+
} else {
|
|
532
|
+
report.summary.failed_manifests += 1;
|
|
533
|
+
report.violations.push(...manifestReport.violations.map((value) => `[${item.spec}] ${value}`));
|
|
534
|
+
}
|
|
535
|
+
report.warnings.push(...manifestReport.warnings.map((value) => `[${item.spec}] ${value}`));
|
|
536
|
+
report.manifests.push(manifestReport);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
report.passed = report.violations.length === 0;
|
|
540
|
+
report.reason = report.passed ? 'passed' : 'violations';
|
|
541
|
+
return report;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
module.exports = {
|
|
545
|
+
DELIVERY_MANIFEST_FILE,
|
|
546
|
+
DELIVERY_VERIFICATION_MODES,
|
|
547
|
+
discoverDeliveryManifests,
|
|
548
|
+
auditSpecDeliverySync,
|
|
549
|
+
loadGitSnapshot,
|
|
550
|
+
parseManifest,
|
|
551
|
+
parseStatusEntries,
|
|
552
|
+
resolveWorktreeStatus
|
|
553
|
+
};
|
package/package.json
CHANGED