openxiangda 1.0.174 → 1.0.176
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/README.md +35 -2
- package/lib/cli.js +1118 -98
- package/lib/content-cache.js +97 -0
- package/lib/dependency-capsule.js +246 -0
- package/lib/integration-bundle.js +204 -0
- package/lib/policy.js +97 -0
- package/lib/release-explain.js +75 -0
- package/lib/release-plan.js +65 -2
- package/lib/sdd.js +163 -13
- package/lib/task-status.js +267 -0
- package/lib/worktree-owner.js +7 -1
- package/openxiangda-skills/SKILL.md +3 -2
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +2 -2
- package/package.json +19 -3
- package/policy/openxiangda-policy.json +51 -0
- package/templates/openxiangda-react-spa/AGENTS.md +2 -1
- package/templates/openxiangda-react-spa/app-workspace.config.ts +1 -0
- package/templates/sy-lowcode-app-workspace/AGENTS.md +2 -1
- package/templates/sy-lowcode-app-workspace/app-workspace.config.ts +1 -0
- package/templates/sy-lowcode-app-workspace/src/types/app-workspace.types.ts +1 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const CACHE_SCHEMA_VERSION = 'openxiangda_content_cache_v1';
|
|
6
|
+
const MAX_CACHE_BYTES = 2 * 1024 * 1024;
|
|
7
|
+
|
|
8
|
+
function resolveCacheRoot(cwd) {
|
|
9
|
+
const root = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
10
|
+
cwd,
|
|
11
|
+
encoding: 'utf8',
|
|
12
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
13
|
+
});
|
|
14
|
+
const common = spawnSync('git', ['rev-parse', '--git-common-dir'], {
|
|
15
|
+
cwd,
|
|
16
|
+
encoding: 'utf8',
|
|
17
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
18
|
+
});
|
|
19
|
+
if (root.status !== 0 || common.status !== 0) return null;
|
|
20
|
+
const workspaceRoot = String(root.stdout || '').trim();
|
|
21
|
+
const commonPath = String(common.stdout || '').trim();
|
|
22
|
+
if (!workspaceRoot || !commonPath) return null;
|
|
23
|
+
const commonDir = path.isAbsolute(commonPath)
|
|
24
|
+
? commonPath
|
|
25
|
+
: path.resolve(workspaceRoot, commonPath);
|
|
26
|
+
return path.join(commonDir, 'openxiangda', 'cache', 'v1');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeSegment(value, label) {
|
|
30
|
+
const segment = String(value || '').trim();
|
|
31
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(segment)) {
|
|
32
|
+
throw new Error(`invalid content cache ${label}`);
|
|
33
|
+
}
|
|
34
|
+
return segment;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function cacheFile(cwd, namespace, key) {
|
|
38
|
+
const root = resolveCacheRoot(cwd);
|
|
39
|
+
if (!root) return null;
|
|
40
|
+
return path.join(
|
|
41
|
+
root,
|
|
42
|
+
normalizeSegment(namespace, 'namespace'),
|
|
43
|
+
`${normalizeSegment(key, 'key')}.json`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readContentCache(options = {}) {
|
|
48
|
+
const file = cacheFile(
|
|
49
|
+
path.resolve(options.cwd || process.cwd()),
|
|
50
|
+
options.namespace,
|
|
51
|
+
options.key
|
|
52
|
+
);
|
|
53
|
+
if (!file || !fs.existsSync(file)) return null;
|
|
54
|
+
try {
|
|
55
|
+
const stat = fs.statSync(file);
|
|
56
|
+
if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_CACHE_BYTES) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const record = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
60
|
+
if (
|
|
61
|
+
record?.schemaVersion !== CACHE_SCHEMA_VERSION ||
|
|
62
|
+
record?.namespace !== options.namespace ||
|
|
63
|
+
record?.key !== options.key
|
|
64
|
+
) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return record.value;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeContentCache(options = {}) {
|
|
74
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
75
|
+
const file = cacheFile(cwd, options.namespace, options.key);
|
|
76
|
+
if (!file) return null;
|
|
77
|
+
const record = {
|
|
78
|
+
schemaVersion: CACHE_SCHEMA_VERSION,
|
|
79
|
+
namespace: options.namespace,
|
|
80
|
+
key: options.key,
|
|
81
|
+
value: options.value,
|
|
82
|
+
createdAt: new Date().toISOString(),
|
|
83
|
+
};
|
|
84
|
+
const content = `${JSON.stringify(record)}\n`;
|
|
85
|
+
if (Buffer.byteLength(content, 'utf8') > MAX_CACHE_BYTES) return null;
|
|
86
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
87
|
+
const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
88
|
+
fs.writeFileSync(temporary, content, { mode: 0o600 });
|
|
89
|
+
fs.renameSync(temporary, file);
|
|
90
|
+
return file;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
CACHE_SCHEMA_VERSION,
|
|
95
|
+
readContentCache,
|
|
96
|
+
writeContentCache,
|
|
97
|
+
};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
const CAPSULE_SCHEMA = 'openxiangda_dependency_capsule_v1';
|
|
7
|
+
const LOCKFILES = [
|
|
8
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
9
|
+
['package-lock.json', 'npm'],
|
|
10
|
+
['npm-shrinkwrap.json', 'npm'],
|
|
11
|
+
['yarn.lock', 'yarn'],
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
function sha256(value) {
|
|
15
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function gitCommonDir(cwd) {
|
|
19
|
+
const result = spawnSync('git', ['rev-parse', '--git-common-dir'], {
|
|
20
|
+
cwd,
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
});
|
|
23
|
+
if (result.status !== 0) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`DEPENDENCY_CAPSULE_GIT_REQUIRED: ${String(result.stderr || result.stdout).trim()}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const value = String(result.stdout || '').trim();
|
|
29
|
+
return path.resolve(cwd, value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveDependencyContract(cwd) {
|
|
33
|
+
const packageFile = path.join(cwd, 'package.json');
|
|
34
|
+
if (!fs.existsSync(packageFile)) {
|
|
35
|
+
throw new Error('DEPENDENCY_CAPSULE_PACKAGE_REQUIRED: 缺少 package.json');
|
|
36
|
+
}
|
|
37
|
+
const lock = LOCKFILES.find(([name]) => fs.existsSync(path.join(cwd, name)));
|
|
38
|
+
if (!lock) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
'DEPENDENCY_CAPSULE_LOCKFILE_REQUIRED: 缺少 pnpm-lock.yaml/package-lock.json/yarn.lock'
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const [lockfile, packageManager] = lock;
|
|
44
|
+
const packageJson = fs.readFileSync(packageFile);
|
|
45
|
+
const lockContent = fs.readFileSync(path.join(cwd, lockfile));
|
|
46
|
+
const fingerprint = sha256(
|
|
47
|
+
Buffer.concat([
|
|
48
|
+
Buffer.from(`${CAPSULE_SCHEMA}\0${process.platform}\0${process.arch}\0${process.versions.node}\0`),
|
|
49
|
+
packageJson,
|
|
50
|
+
Buffer.from('\0'),
|
|
51
|
+
lockContent,
|
|
52
|
+
])
|
|
53
|
+
);
|
|
54
|
+
return {
|
|
55
|
+
schemaVersion: CAPSULE_SCHEMA,
|
|
56
|
+
fingerprint,
|
|
57
|
+
packageManager,
|
|
58
|
+
lockfile,
|
|
59
|
+
packageFile,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function installCommand(contract) {
|
|
64
|
+
if (contract.packageManager === 'pnpm') {
|
|
65
|
+
return ['pnpm', ['install', '--frozen-lockfile', '--ignore-scripts=false']];
|
|
66
|
+
}
|
|
67
|
+
if (contract.packageManager === 'yarn') {
|
|
68
|
+
return ['yarn', ['install', '--immutable']];
|
|
69
|
+
}
|
|
70
|
+
return ['npm', ['ci', '--no-audit', '--no-fund']];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildDependencyCapsulePlan(options = {}) {
|
|
74
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
75
|
+
const contract = resolveDependencyContract(cwd);
|
|
76
|
+
const commonDir = gitCommonDir(cwd);
|
|
77
|
+
const capsuleDir = path.join(
|
|
78
|
+
commonDir,
|
|
79
|
+
'openxiangda',
|
|
80
|
+
'dependencies',
|
|
81
|
+
contract.fingerprint
|
|
82
|
+
);
|
|
83
|
+
const capsuleNodeModules = path.join(capsuleDir, 'node_modules');
|
|
84
|
+
const markerFile = path.join(capsuleDir, 'capsule.json');
|
|
85
|
+
const workspaceNodeModules = path.join(cwd, 'node_modules');
|
|
86
|
+
let marker = null;
|
|
87
|
+
try {
|
|
88
|
+
marker = JSON.parse(fs.readFileSync(markerFile, 'utf8'));
|
|
89
|
+
} catch {
|
|
90
|
+
marker = null;
|
|
91
|
+
}
|
|
92
|
+
const capsuleReady = Boolean(
|
|
93
|
+
marker?.schemaVersion === CAPSULE_SCHEMA &&
|
|
94
|
+
marker?.fingerprint === contract.fingerprint &&
|
|
95
|
+
fs.existsSync(capsuleNodeModules)
|
|
96
|
+
);
|
|
97
|
+
const currentTarget = fs.existsSync(workspaceNodeModules)
|
|
98
|
+
? fs.realpathSync(workspaceNodeModules)
|
|
99
|
+
: null;
|
|
100
|
+
const linked = Boolean(
|
|
101
|
+
capsuleReady &&
|
|
102
|
+
currentTarget &&
|
|
103
|
+
currentTarget === fs.realpathSync(capsuleNodeModules)
|
|
104
|
+
);
|
|
105
|
+
const [command, args] = installCommand(contract);
|
|
106
|
+
return {
|
|
107
|
+
...contract,
|
|
108
|
+
cwd,
|
|
109
|
+
commonDir,
|
|
110
|
+
capsuleDir,
|
|
111
|
+
capsuleNodeModules,
|
|
112
|
+
markerFile,
|
|
113
|
+
workspaceNodeModules,
|
|
114
|
+
capsuleReady,
|
|
115
|
+
linked,
|
|
116
|
+
command,
|
|
117
|
+
args,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function copyDependencyInputs(plan) {
|
|
122
|
+
fs.mkdirSync(plan.capsuleDir, { recursive: true, mode: 0o700 });
|
|
123
|
+
fs.copyFileSync(plan.packageFile, path.join(plan.capsuleDir, 'package.json'));
|
|
124
|
+
fs.copyFileSync(
|
|
125
|
+
path.join(plan.cwd, plan.lockfile),
|
|
126
|
+
path.join(plan.capsuleDir, plan.lockfile)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function acquireInstallLock(plan) {
|
|
131
|
+
const lockDir = `${plan.capsuleDir}.lock`;
|
|
132
|
+
try {
|
|
133
|
+
fs.mkdirSync(lockDir, { recursive: false, mode: 0o700 });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error.code === 'EEXIST') {
|
|
136
|
+
const busy = new Error(
|
|
137
|
+
`DEPENDENCY_CAPSULE_BUSY: 另一个任务正在准备 ${plan.fingerprint.slice(0, 12)};稍后重试`
|
|
138
|
+
);
|
|
139
|
+
busy.code = 'DEPENDENCY_CAPSULE_BUSY';
|
|
140
|
+
throw busy;
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
return () => fs.rmSync(lockDir, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function writeMarker(plan) {
|
|
148
|
+
const marker = {
|
|
149
|
+
schemaVersion: CAPSULE_SCHEMA,
|
|
150
|
+
fingerprint: plan.fingerprint,
|
|
151
|
+
packageManager: plan.packageManager,
|
|
152
|
+
lockfile: plan.lockfile,
|
|
153
|
+
node: process.versions.node,
|
|
154
|
+
platform: process.platform,
|
|
155
|
+
arch: process.arch,
|
|
156
|
+
preparedAt: new Date().toISOString(),
|
|
157
|
+
};
|
|
158
|
+
const temporary = `${plan.markerFile}.${process.pid}.tmp`;
|
|
159
|
+
fs.writeFileSync(temporary, `${JSON.stringify(marker, null, 2)}\n`, {
|
|
160
|
+
mode: 0o600,
|
|
161
|
+
});
|
|
162
|
+
fs.renameSync(temporary, plan.markerFile);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function linkWorkspaceNodeModules(plan, options = {}) {
|
|
166
|
+
if (plan.linked) return 'already-linked';
|
|
167
|
+
if (fs.existsSync(plan.workspaceNodeModules)) {
|
|
168
|
+
const stats = fs.lstatSync(plan.workspaceNodeModules);
|
|
169
|
+
if (!stats.isSymbolicLink() && !options.force) {
|
|
170
|
+
const error = new Error(
|
|
171
|
+
'DEPENDENCY_CAPSULE_LOCAL_NODE_MODULES: 当前 node_modules 不是 capsule 链接;使用 --force 前请确认没有正在运行的构建'
|
|
172
|
+
);
|
|
173
|
+
error.code = 'DEPENDENCY_CAPSULE_LOCAL_NODE_MODULES';
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
fs.rmSync(plan.workspaceNodeModules, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
fs.symlinkSync(plan.capsuleNodeModules, plan.workspaceNodeModules, 'junction');
|
|
179
|
+
return 'linked';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function prepareDependencyCapsule(options = {}) {
|
|
183
|
+
let plan = buildDependencyCapsulePlan(options);
|
|
184
|
+
if (options.check) {
|
|
185
|
+
return {
|
|
186
|
+
ready: plan.capsuleReady && plan.linked,
|
|
187
|
+
capsuleReady: plan.capsuleReady,
|
|
188
|
+
linked: plan.linked,
|
|
189
|
+
fingerprint: plan.fingerprint,
|
|
190
|
+
packageManager: plan.packageManager,
|
|
191
|
+
lockfile: plan.lockfile,
|
|
192
|
+
recommendedCommand: plan.capsuleReady
|
|
193
|
+
? 'openxiangda workspace prepare --force'
|
|
194
|
+
: 'openxiangda workspace prepare',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (!plan.capsuleReady) {
|
|
198
|
+
fs.mkdirSync(path.dirname(plan.capsuleDir), { recursive: true, mode: 0o700 });
|
|
199
|
+
const releaseLock = acquireInstallLock(plan);
|
|
200
|
+
try {
|
|
201
|
+
copyDependencyInputs(plan);
|
|
202
|
+
const result = spawnSync(plan.command, plan.args, {
|
|
203
|
+
cwd: plan.capsuleDir,
|
|
204
|
+
encoding: 'utf8',
|
|
205
|
+
stdio: options.quiet ? 'pipe' : 'inherit',
|
|
206
|
+
env: {
|
|
207
|
+
...process.env,
|
|
208
|
+
CI: process.env.CI || 'true',
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
if (result.status !== 0) {
|
|
212
|
+
fs.rmSync(plan.capsuleDir, { recursive: true, force: true });
|
|
213
|
+
const error = new Error(
|
|
214
|
+
`DEPENDENCY_CAPSULE_INSTALL_FAILED: ${plan.command} ${plan.args.join(' ')}`
|
|
215
|
+
);
|
|
216
|
+
error.code = 'DEPENDENCY_CAPSULE_INSTALL_FAILED';
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
if (!fs.existsSync(plan.capsuleNodeModules)) {
|
|
220
|
+
throw new Error('DEPENDENCY_CAPSULE_INSTALL_EMPTY: 安装完成但 node_modules 不存在');
|
|
221
|
+
}
|
|
222
|
+
writeMarker(plan);
|
|
223
|
+
} finally {
|
|
224
|
+
releaseLock();
|
|
225
|
+
}
|
|
226
|
+
plan = buildDependencyCapsulePlan(options);
|
|
227
|
+
}
|
|
228
|
+
const action = linkWorkspaceNodeModules(plan, options);
|
|
229
|
+
return {
|
|
230
|
+
ready: true,
|
|
231
|
+
capsuleReady: true,
|
|
232
|
+
linked: true,
|
|
233
|
+
action,
|
|
234
|
+
fingerprint: plan.fingerprint,
|
|
235
|
+
packageManager: plan.packageManager,
|
|
236
|
+
lockfile: plan.lockfile,
|
|
237
|
+
capsuleDir: plan.capsuleDir,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = {
|
|
242
|
+
CAPSULE_SCHEMA,
|
|
243
|
+
buildDependencyCapsulePlan,
|
|
244
|
+
prepareDependencyCapsule,
|
|
245
|
+
resolveDependencyContract,
|
|
246
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const TASK_RESULT_SCHEMA_VERSION = 'openxiangda_task_result_v1';
|
|
6
|
+
const INTEGRATION_BUNDLE_SCHEMA_VERSION = 'openxiangda_integration_bundle_v1';
|
|
7
|
+
|
|
8
|
+
function runGit(cwd, args, options = {}) {
|
|
9
|
+
const result = spawnSync('git', args, {
|
|
10
|
+
cwd,
|
|
11
|
+
encoding: 'utf8',
|
|
12
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
13
|
+
});
|
|
14
|
+
if (result.status !== 0 && !options.allowFailure) {
|
|
15
|
+
const error = new Error(
|
|
16
|
+
String(result.stderr || result.stdout || `git ${args.join(' ')} failed`).trim()
|
|
17
|
+
);
|
|
18
|
+
error.code = 'INTEGRATION_GIT_FAILED';
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
ok: result.status === 0,
|
|
23
|
+
stdout: String(result.stdout || '').trim(),
|
|
24
|
+
stderr: String(result.stderr || '').trim(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function gitCommonStateDir(cwd) {
|
|
29
|
+
const root = runGit(cwd, ['rev-parse', '--show-toplevel']).stdout;
|
|
30
|
+
const common = runGit(cwd, ['rev-parse', '--git-common-dir']).stdout;
|
|
31
|
+
const commonDir = path.isAbsolute(common) ? common : path.resolve(root, common);
|
|
32
|
+
return path.join(commonDir, 'openxiangda');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function taskResultFile(cwd, changeId) {
|
|
36
|
+
return path.join(
|
|
37
|
+
gitCommonStateDir(cwd),
|
|
38
|
+
'task-results',
|
|
39
|
+
`${normalizeChangeId(changeId)}.json`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeChangeId(value) {
|
|
44
|
+
const changeId = String(value || '').trim();
|
|
45
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(changeId)) {
|
|
46
|
+
const error = new Error(`INTEGRATION_CHANGE_ID_INVALID: ${value || '(empty)'}`);
|
|
47
|
+
error.code = 'INTEGRATION_CHANGE_ID_INVALID';
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
return changeId.toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function recordIntegrationTaskResult(options = {}) {
|
|
54
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
55
|
+
const changeId = normalizeChangeId(options.changeId);
|
|
56
|
+
const dirty = runGit(cwd, ['status', '--porcelain']).stdout;
|
|
57
|
+
if (dirty) {
|
|
58
|
+
const error = new Error(
|
|
59
|
+
'INTEGRATION_TASK_RESULT_DIRTY: 请先提交该任务的实现、测试和 SDD 记录,再生成 task result'
|
|
60
|
+
);
|
|
61
|
+
error.code = 'INTEGRATION_TASK_RESULT_DIRTY';
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
const commit = runGit(cwd, ['rev-parse', 'HEAD']).stdout;
|
|
65
|
+
const treeHash = runGit(cwd, ['rev-parse', 'HEAD^{tree}']).stdout;
|
|
66
|
+
const branch = runGit(cwd, ['branch', '--show-current']).stdout;
|
|
67
|
+
if (!branch) {
|
|
68
|
+
const error = new Error(
|
|
69
|
+
'INTEGRATION_TASK_BRANCH_REQUIRED: detached HEAD 不能生成可合并任务结果'
|
|
70
|
+
);
|
|
71
|
+
error.code = 'INTEGRATION_TASK_BRANCH_REQUIRED';
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
const result = {
|
|
75
|
+
schemaVersion: TASK_RESULT_SCHEMA_VERSION,
|
|
76
|
+
changeId,
|
|
77
|
+
commit,
|
|
78
|
+
treeHash,
|
|
79
|
+
branch,
|
|
80
|
+
targets: options.targets || {},
|
|
81
|
+
changedFiles: Array.from(
|
|
82
|
+
new Set((options.changedFiles || []).map(String).filter(Boolean))
|
|
83
|
+
).sort(),
|
|
84
|
+
recordedAt: new Date().toISOString(),
|
|
85
|
+
};
|
|
86
|
+
const file = taskResultFile(cwd, changeId);
|
|
87
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
88
|
+
fs.writeFileSync(file, `${JSON.stringify(result, null, 2)}\n`, {
|
|
89
|
+
mode: 0o600,
|
|
90
|
+
});
|
|
91
|
+
return { ...result, file };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readIntegrationTaskResult(cwd, changeId) {
|
|
95
|
+
const file = taskResultFile(cwd, changeId);
|
|
96
|
+
if (!fs.existsSync(file)) return null;
|
|
97
|
+
try {
|
|
98
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
99
|
+
return value?.schemaVersion === TASK_RESULT_SCHEMA_VERSION
|
|
100
|
+
? { ...value, file }
|
|
101
|
+
: null;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function commitIsAncestor(cwd, commit, descendant = 'HEAD') {
|
|
108
|
+
return runGit(
|
|
109
|
+
cwd,
|
|
110
|
+
['merge-base', '--is-ancestor', String(commit), String(descendant)],
|
|
111
|
+
{ allowFailure: true }
|
|
112
|
+
).ok;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function collectIntegrationTaskResults(options = {}) {
|
|
116
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
117
|
+
const changeIds = Array.from(
|
|
118
|
+
new Set((options.changeIds || []).map(normalizeChangeId))
|
|
119
|
+
).sort();
|
|
120
|
+
const results = changeIds.map(changeId => {
|
|
121
|
+
const result = readIntegrationTaskResult(cwd, changeId);
|
|
122
|
+
if (!result) {
|
|
123
|
+
const error = new Error(
|
|
124
|
+
`INTEGRATION_TASK_RESULT_REQUIRED: ${changeId} 缺少 task result;请在任务提交后运行 openxiangda sdd ready ${changeId}`
|
|
125
|
+
);
|
|
126
|
+
error.code = 'INTEGRATION_TASK_RESULT_REQUIRED';
|
|
127
|
+
error.changeId = changeId;
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
if (!commitIsAncestor(cwd, result.commit, 'HEAD')) {
|
|
131
|
+
const error = new Error(
|
|
132
|
+
`INTEGRATION_TASK_COMMIT_NOT_MERGED: ${changeId}@${String(result.commit).slice(0, 12)} 尚未合并到当前主线 HEAD`
|
|
133
|
+
);
|
|
134
|
+
error.code = 'INTEGRATION_TASK_COMMIT_NOT_MERGED';
|
|
135
|
+
error.changeId = changeId;
|
|
136
|
+
error.commit = result.commit;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
schemaVersion: INTEGRATION_BUNDLE_SCHEMA_VERSION,
|
|
143
|
+
requiredChanges: changeIds,
|
|
144
|
+
requiredCommits: results.map(result => ({
|
|
145
|
+
changeId: result.changeId,
|
|
146
|
+
commit: result.commit,
|
|
147
|
+
treeHash: result.treeHash,
|
|
148
|
+
sourceBranch: result.branch,
|
|
149
|
+
})),
|
|
150
|
+
verifiedAgainst: runGit(cwd, ['rev-parse', 'HEAD']).stdout,
|
|
151
|
+
createdAt: new Date().toISOString(),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function inspectIntegrationBundleCommits(integration, options = {}) {
|
|
156
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
157
|
+
const required = Array.isArray(integration?.requiredCommits)
|
|
158
|
+
? integration.requiredCommits
|
|
159
|
+
: [];
|
|
160
|
+
return {
|
|
161
|
+
schemaVersion:
|
|
162
|
+
integration?.schemaVersion || INTEGRATION_BUNDLE_SCHEMA_VERSION,
|
|
163
|
+
valid: required.length > 0,
|
|
164
|
+
requiredCommits: required.map(item => ({
|
|
165
|
+
...item,
|
|
166
|
+
mergedIntoHead: commitIsAncestor(cwd, item.commit, 'HEAD'),
|
|
167
|
+
})),
|
|
168
|
+
checkedHead: runGit(cwd, ['rev-parse', 'HEAD']).stdout,
|
|
169
|
+
checkedAt: new Date().toISOString(),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function assertIntegrationBundleCommits(integration, options = {}) {
|
|
174
|
+
const inspection = inspectIntegrationBundleCommits(integration, options);
|
|
175
|
+
if (!inspection.valid) {
|
|
176
|
+
const error = new Error(
|
|
177
|
+
'INTEGRATION_BUNDLE_REQUIRED: mainline bundle 缺少 requiredCommits,禁止发布可能遗漏任务的主线'
|
|
178
|
+
);
|
|
179
|
+
error.code = 'INTEGRATION_BUNDLE_REQUIRED';
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
const missing = inspection.requiredCommits.filter(item => !item.mergedIntoHead);
|
|
183
|
+
if (missing.length > 0) {
|
|
184
|
+
const error = new Error(
|
|
185
|
+
`INTEGRATION_TASK_COMMIT_NOT_MERGED: ${missing
|
|
186
|
+
.map(item => `${item.changeId}@${String(item.commit).slice(0, 12)}`)
|
|
187
|
+
.join(', ')} 尚未合并到当前主线 HEAD`
|
|
188
|
+
);
|
|
189
|
+
error.code = 'INTEGRATION_TASK_COMMIT_NOT_MERGED';
|
|
190
|
+
error.missing = missing;
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
return inspection;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
INTEGRATION_BUNDLE_SCHEMA_VERSION,
|
|
198
|
+
TASK_RESULT_SCHEMA_VERSION,
|
|
199
|
+
assertIntegrationBundleCommits,
|
|
200
|
+
collectIntegrationTaskResults,
|
|
201
|
+
inspectIntegrationBundleCommits,
|
|
202
|
+
readIntegrationTaskResult,
|
|
203
|
+
recordIntegrationTaskResult,
|
|
204
|
+
};
|
package/lib/policy.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const POLICY_FILE = path.join(
|
|
5
|
+
__dirname,
|
|
6
|
+
'..',
|
|
7
|
+
'policy',
|
|
8
|
+
'openxiangda-policy.json'
|
|
9
|
+
);
|
|
10
|
+
const POLICY_MARKER_PREFIX = 'OpenXiangda-Policy-Version:';
|
|
11
|
+
const POLICY_MARKER_FILES = [
|
|
12
|
+
'README.md',
|
|
13
|
+
'openxiangda-skills/SKILL.md',
|
|
14
|
+
'templates/openxiangda-react-spa/AGENTS.md',
|
|
15
|
+
'templates/sy-lowcode-app-workspace/AGENTS.md',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function loadEngineeringPolicy() {
|
|
19
|
+
const policy = JSON.parse(fs.readFileSync(POLICY_FILE, 'utf8'));
|
|
20
|
+
validateEngineeringPolicy(policy);
|
|
21
|
+
return policy;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function validateEngineeringPolicy(policy) {
|
|
25
|
+
const errors = [];
|
|
26
|
+
if (policy?.schemaVersion !== 'openxiangda_engineering_policy_v1') {
|
|
27
|
+
errors.push('schemaVersion 必须是 openxiangda_engineering_policy_v1');
|
|
28
|
+
}
|
|
29
|
+
if (!Number.isSafeInteger(policy?.policyVersion) || policy.policyVersion < 1) {
|
|
30
|
+
errors.push('policyVersion 必须是正整数');
|
|
31
|
+
}
|
|
32
|
+
if (policy?.sdd?.verifyIsReadOnly !== true) {
|
|
33
|
+
errors.push('sdd.verifyIsReadOnly 必须为 true');
|
|
34
|
+
}
|
|
35
|
+
if (policy?.git?.requireRemoteMainlineContainment !== true) {
|
|
36
|
+
errors.push('git.requireRemoteMainlineContainment 必须为 true');
|
|
37
|
+
}
|
|
38
|
+
if (policy?.release?.unsupportedGenericMode !== 'fail-closed') {
|
|
39
|
+
errors.push('release.unsupportedGenericMode 必须为 fail-closed');
|
|
40
|
+
}
|
|
41
|
+
const ttl = Number(policy?.ownership?.defaultTtlSeconds);
|
|
42
|
+
if (!Number.isSafeInteger(ttl) || ttl < 300 || ttl > 86400) {
|
|
43
|
+
errors.push('ownership.defaultTtlSeconds 必须在 300-86400 之间');
|
|
44
|
+
}
|
|
45
|
+
if (errors.length > 0) {
|
|
46
|
+
const error = new Error(`OPENXIANGDA_POLICY_INVALID: ${errors.join('; ')}`);
|
|
47
|
+
error.code = 'OPENXIANGDA_POLICY_INVALID';
|
|
48
|
+
error.errors = errors;
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
return policy;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function checkEngineeringPolicyMarkers(options = {}) {
|
|
55
|
+
const root = path.resolve(options.root || path.join(__dirname, '..'));
|
|
56
|
+
const policy = loadEngineeringPolicy();
|
|
57
|
+
const expected = `${POLICY_MARKER_PREFIX} ${policy.policyVersion}`;
|
|
58
|
+
const files = POLICY_MARKER_FILES.map(relativePath => {
|
|
59
|
+
const file = path.join(root, relativePath);
|
|
60
|
+
const exists = fs.existsSync(file);
|
|
61
|
+
const content = exists ? fs.readFileSync(file, 'utf8') : '';
|
|
62
|
+
return {
|
|
63
|
+
file: relativePath,
|
|
64
|
+
exists,
|
|
65
|
+
synchronized: exists && content.includes(expected),
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
valid: files.every(item => item.synchronized),
|
|
70
|
+
policyVersion: policy.policyVersion,
|
|
71
|
+
expectedMarker: expected,
|
|
72
|
+
files,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function renderEngineeringPolicyMarkdown(policy = loadEngineeringPolicy()) {
|
|
77
|
+
return [
|
|
78
|
+
`<!-- ${POLICY_MARKER_PREFIX} ${policy.policyVersion} -->`,
|
|
79
|
+
`Policy v${policy.policyVersion}:`,
|
|
80
|
+
'',
|
|
81
|
+
`- 风险按 ${policy.risk.classifyBy.join('、')} 分级;目录只作信号。`,
|
|
82
|
+
'- SDD verify 只读;Markdown 是按需派生视图。',
|
|
83
|
+
'- 平台仓库直接开发权威 master;应用任务可用独立 worktree,但只从干净、已推送的权威主线发布。',
|
|
84
|
+
`- 原子 child:${policy.release.atomicChildren.join(', ')}。`,
|
|
85
|
+
`- 尚无 staged child 的通用资源必须 ${policy.release.unsupportedGenericMode};维护直发必须显式选择并展示影响范围。`,
|
|
86
|
+
`- 激活后的副作用进入 ${policy.release.postCommitMode}。`,
|
|
87
|
+
].join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
POLICY_FILE,
|
|
92
|
+
POLICY_MARKER_FILES,
|
|
93
|
+
checkEngineeringPolicyMarkers,
|
|
94
|
+
loadEngineeringPolicy,
|
|
95
|
+
renderEngineeringPolicyMarkdown,
|
|
96
|
+
validateEngineeringPolicy,
|
|
97
|
+
};
|