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,267 @@
|
|
|
1
|
+
function buildTaskStatus(input = {}) {
|
|
2
|
+
const execution = object(input.execution);
|
|
3
|
+
const steps = Array.isArray(execution?.steps) ? execution.steps : [];
|
|
4
|
+
const completed = steps.filter(step => step.status === 'completed');
|
|
5
|
+
const failed = steps.find(step => step.status === 'failed') || null;
|
|
6
|
+
const running =
|
|
7
|
+
steps.find(step => ['running', 'write-started'].includes(step.status)) ||
|
|
8
|
+
null;
|
|
9
|
+
const pending = steps.filter(step => step.status === 'pending');
|
|
10
|
+
const releaseState = execution?.status || 'not-started';
|
|
11
|
+
const postCommit = object(input.postCommit);
|
|
12
|
+
const remoteLease = object(input.remoteLease);
|
|
13
|
+
const taskResult = object(input.taskResult);
|
|
14
|
+
const integration = object(input.integration);
|
|
15
|
+
const change = object(input.change);
|
|
16
|
+
const phase = inferPhase({
|
|
17
|
+
releaseState,
|
|
18
|
+
steps,
|
|
19
|
+
failed,
|
|
20
|
+
running,
|
|
21
|
+
postCommit,
|
|
22
|
+
taskResult,
|
|
23
|
+
integration,
|
|
24
|
+
change,
|
|
25
|
+
});
|
|
26
|
+
const blockingLayer = inferBlockingLayer({
|
|
27
|
+
failed,
|
|
28
|
+
releaseState,
|
|
29
|
+
remoteLease,
|
|
30
|
+
integration,
|
|
31
|
+
postCommit,
|
|
32
|
+
});
|
|
33
|
+
const startedAt =
|
|
34
|
+
execution?.createdAt ||
|
|
35
|
+
change?.createdAt ||
|
|
36
|
+
taskResult?.recordedAt ||
|
|
37
|
+
null;
|
|
38
|
+
const endedAt =
|
|
39
|
+
execution?.completedAt ||
|
|
40
|
+
(phase === 'integration-ready' ? taskResult?.recordedAt : null);
|
|
41
|
+
const now = Number.isFinite(Number(input.now))
|
|
42
|
+
? Number(input.now)
|
|
43
|
+
: Date.now();
|
|
44
|
+
const elapsedMs = durationBetween(startedAt, endedAt, now);
|
|
45
|
+
const estimatedRemainingMs = estimateRemainingMs(
|
|
46
|
+
steps,
|
|
47
|
+
completed,
|
|
48
|
+
pending,
|
|
49
|
+
running,
|
|
50
|
+
);
|
|
51
|
+
const wrotePlatform = Boolean(
|
|
52
|
+
execution?.writeAttempted ||
|
|
53
|
+
execution?.stagedWriteOccurred ||
|
|
54
|
+
completed.some(step => step.id !== 'lease-and-capture'),
|
|
55
|
+
);
|
|
56
|
+
const healthy =
|
|
57
|
+
releaseState === 'completed' &&
|
|
58
|
+
(!postCommit || postCommit.status === 'completed');
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
schemaVersion: 'openxiangda_task_status_v1',
|
|
62
|
+
changeId: input.changeId || change?.id || execution?.changeId || null,
|
|
63
|
+
phase,
|
|
64
|
+
healthy,
|
|
65
|
+
wrotePlatform,
|
|
66
|
+
elapsedMs,
|
|
67
|
+
estimatedRemainingMs,
|
|
68
|
+
progress: {
|
|
69
|
+
completed: completed.length,
|
|
70
|
+
pending: pending.length,
|
|
71
|
+
total: steps.length,
|
|
72
|
+
currentStep: running?.id || failed?.id || pending[0]?.id || null,
|
|
73
|
+
},
|
|
74
|
+
source: {
|
|
75
|
+
changeStatus: change?.status || null,
|
|
76
|
+
taskCommit: taskResult?.commit || null,
|
|
77
|
+
taskReady: Boolean(taskResult),
|
|
78
|
+
integrated: inferIntegrated(integration),
|
|
79
|
+
},
|
|
80
|
+
release: {
|
|
81
|
+
state: releaseState,
|
|
82
|
+
appReleaseId: findAppReleaseId(steps),
|
|
83
|
+
postCommitStatus: postCommit?.status || null,
|
|
84
|
+
leaseActive: Boolean(remoteLease?.active),
|
|
85
|
+
leaseHolder:
|
|
86
|
+
remoteLease?.holder?.changeId ||
|
|
87
|
+
remoteLease?.holder?.clientSessionId ||
|
|
88
|
+
remoteLease?.changeId ||
|
|
89
|
+
null,
|
|
90
|
+
},
|
|
91
|
+
blocker: blockingLayer
|
|
92
|
+
? {
|
|
93
|
+
layer: blockingLayer,
|
|
94
|
+
code:
|
|
95
|
+
failed?.error?.code ||
|
|
96
|
+
failed?.errorCode ||
|
|
97
|
+
(releaseState === 'write-review-required'
|
|
98
|
+
? 'RELEASE_WRITE_REVIEW_REQUIRED'
|
|
99
|
+
: null),
|
|
100
|
+
message:
|
|
101
|
+
failed?.error?.message ||
|
|
102
|
+
failed?.error ||
|
|
103
|
+
blockerMessage(blockingLayer),
|
|
104
|
+
}
|
|
105
|
+
: null,
|
|
106
|
+
nextAction: nextAction({
|
|
107
|
+
phase,
|
|
108
|
+
releaseState,
|
|
109
|
+
failed,
|
|
110
|
+
remoteLease,
|
|
111
|
+
postCommit,
|
|
112
|
+
taskResult,
|
|
113
|
+
integration,
|
|
114
|
+
}),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function inferPhase(input) {
|
|
119
|
+
if (
|
|
120
|
+
input.postCommit &&
|
|
121
|
+
input.postCommit.status &&
|
|
122
|
+
input.postCommit.status !== 'completed'
|
|
123
|
+
) {
|
|
124
|
+
return 'health';
|
|
125
|
+
}
|
|
126
|
+
if (input.releaseState === 'completed') return 'healthy';
|
|
127
|
+
if (input.releaseState === 'write-review-required') return 'write-review';
|
|
128
|
+
if (input.failed) return 'failed';
|
|
129
|
+
if (input.running) return phaseFromStep(input.running.id);
|
|
130
|
+
const firstPending = input.steps.find(step => step.status === 'pending');
|
|
131
|
+
if (firstPending && input.releaseState !== 'not-started') {
|
|
132
|
+
return phaseFromStep(firstPending.id);
|
|
133
|
+
}
|
|
134
|
+
if (input.taskResult && inferIntegrated(input.integration)) {
|
|
135
|
+
return 'integration-ready';
|
|
136
|
+
}
|
|
137
|
+
if (input.taskResult) return 'integration';
|
|
138
|
+
if (input.change?.status === 'approved') return 'coding';
|
|
139
|
+
if (input.change) return 'design';
|
|
140
|
+
return 'not-started';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function phaseFromStep(stepId) {
|
|
144
|
+
const value = String(stepId || '').toLowerCase();
|
|
145
|
+
if (value.includes('lease') || value.includes('capture')) return 'staging';
|
|
146
|
+
if (value.includes('stage')) return 'staging';
|
|
147
|
+
if (value.includes('finalize') || value.includes('activate')) {
|
|
148
|
+
return 'activation';
|
|
149
|
+
}
|
|
150
|
+
if (value.includes('health') || value.includes('post-commit')) {
|
|
151
|
+
return 'health';
|
|
152
|
+
}
|
|
153
|
+
return 'release';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function inferBlockingLayer(input) {
|
|
157
|
+
const code = String(
|
|
158
|
+
input.failed?.error?.code ||
|
|
159
|
+
input.failed?.errorCode ||
|
|
160
|
+
'',
|
|
161
|
+
);
|
|
162
|
+
if (input.releaseState === 'write-review-required') return 'write-result';
|
|
163
|
+
if (input.postCommit?.status && input.postCommit.status !== 'completed') {
|
|
164
|
+
return 'post-commit';
|
|
165
|
+
}
|
|
166
|
+
if (
|
|
167
|
+
input.remoteLease?.active &&
|
|
168
|
+
input.releaseState !== 'running' &&
|
|
169
|
+
input.releaseState !== 'staged-resumable'
|
|
170
|
+
) {
|
|
171
|
+
return 'lease';
|
|
172
|
+
}
|
|
173
|
+
if (input.integration && inferIntegrated(input.integration) === false) {
|
|
174
|
+
return 'git-mainline';
|
|
175
|
+
}
|
|
176
|
+
if (/DB|MIGRATION|CONSTRAINT/.test(code)) return 'database';
|
|
177
|
+
if (/HTTP|API|CAPABILIT|CONTRACT/.test(code)) return 'platform-api';
|
|
178
|
+
if (/GIT|SOURCE|MAINLINE|INTEGRATION/.test(code)) return 'git-mainline';
|
|
179
|
+
if (/DEPENDENCY|PACKAGE|INSTALL/.test(code)) return 'dependencies';
|
|
180
|
+
if (/BUILD|TYPECHECK|TEST/.test(code)) return 'build-test';
|
|
181
|
+
if (/LEASE/.test(code)) return 'lease';
|
|
182
|
+
if (input.failed) return 'release-step';
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function inferIntegrated(integration) {
|
|
187
|
+
if (!integration) return null;
|
|
188
|
+
if (typeof integration.integrated === 'boolean') {
|
|
189
|
+
return integration.integrated;
|
|
190
|
+
}
|
|
191
|
+
const required = Array.isArray(integration.requiredCommits)
|
|
192
|
+
? integration.requiredCommits
|
|
193
|
+
: [];
|
|
194
|
+
if (required.length === 0) return null;
|
|
195
|
+
return required.every(item => item.mergedIntoHead === true);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function findAppReleaseId(steps) {
|
|
199
|
+
const finalize = [...steps]
|
|
200
|
+
.reverse()
|
|
201
|
+
.find(step => String(step.id || '').includes('app-finalize'));
|
|
202
|
+
return finalize?.result?.releaseId || null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function estimateRemainingMs(steps, completed, pending, running) {
|
|
206
|
+
if (!steps.length || (!pending.length && !running)) return 0;
|
|
207
|
+
const durations = completed
|
|
208
|
+
.map(step => durationBetween(step.startedAt, step.completedAt))
|
|
209
|
+
.filter(value => Number.isFinite(value) && value > 0);
|
|
210
|
+
if (!durations.length) return null;
|
|
211
|
+
const average =
|
|
212
|
+
durations.reduce((sum, value) => sum + value, 0) / durations.length;
|
|
213
|
+
return Math.round(average * (pending.length + (running ? 0.5 : 0)));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function durationBetween(start, end, now = Date.now()) {
|
|
217
|
+
if (!start) return null;
|
|
218
|
+
const started = new Date(start).getTime();
|
|
219
|
+
const ended = end ? new Date(end).getTime() : now;
|
|
220
|
+
if (!Number.isFinite(started) || !Number.isFinite(ended)) return null;
|
|
221
|
+
return Math.max(0, ended - started);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function nextAction(input) {
|
|
225
|
+
if (input.releaseState === 'write-review-required') {
|
|
226
|
+
return '只读核对 staged child/App Release,确认安全后使用 --resume-after-review。';
|
|
227
|
+
}
|
|
228
|
+
if (input.postCommit?.status && input.postCommit.status !== 'completed') {
|
|
229
|
+
return '等待平台自动重试;需要立即重试时执行 release app-retry <releaseId>。';
|
|
230
|
+
}
|
|
231
|
+
if (input.failed) {
|
|
232
|
+
return `修复 ${input.failed.id || '失败步骤'} 后重跑同一条 release publish。`;
|
|
233
|
+
}
|
|
234
|
+
if (input.remoteLease?.active && input.releaseState === 'not-started') {
|
|
235
|
+
return '等待当前应用发布租约释放;开发与测试可继续并行。';
|
|
236
|
+
}
|
|
237
|
+
if (input.releaseState === 'staged-resumable') {
|
|
238
|
+
return '重跑同一条 release publish,从已验证的 staged child 继续。';
|
|
239
|
+
}
|
|
240
|
+
if (input.releaseState === 'completed') return '执行业务验收并归档 change。';
|
|
241
|
+
if (input.taskResult && inferIntegrated(input.integration) === false) {
|
|
242
|
+
return '把 task commit 合并并推送到权威主分支,再创建 mainline bundle。';
|
|
243
|
+
}
|
|
244
|
+
if (input.taskResult) return '在主分支创建/更新 Integration Bundle 并发布。';
|
|
245
|
+
return '完成实现和聚焦测试,提交后运行 sdd ready。';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function blockerMessage(layer) {
|
|
249
|
+
const messages = {
|
|
250
|
+
lease: '另一个发布任务持有应用租约。',
|
|
251
|
+
'git-mainline': '任务提交尚未完整进入权威主分支。',
|
|
252
|
+
'post-commit': '发布已激活,提交后动作仍在自动重试。',
|
|
253
|
+
'write-result': '上次写请求结果不确定,需要只读核对。',
|
|
254
|
+
};
|
|
255
|
+
return messages[layer] || '当前阶段需要处理失败后才能继续。';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function object(value) {
|
|
259
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
260
|
+
? value
|
|
261
|
+
: null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = {
|
|
265
|
+
buildTaskStatus,
|
|
266
|
+
findAppReleaseId,
|
|
267
|
+
};
|
package/lib/worktree-owner.js
CHANGED
|
@@ -2,9 +2,15 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
4
|
const { readWorkspaceIdentity } = require('./publish-lease');
|
|
5
|
+
const { loadEngineeringPolicy } = require('./policy');
|
|
5
6
|
|
|
6
7
|
const OWNER_RELATIVE_FILE = path.join('.openxiangda', 'worktree-owner.json');
|
|
7
|
-
|
|
8
|
+
// Ownership is a collision guard for an actively edited checkout, not a
|
|
9
|
+
// half-day reservation. Long releases have their own renewable platform
|
|
10
|
+
// lease; source ownership is released by `sdd ready` once the task is clean
|
|
11
|
+
// and committed.
|
|
12
|
+
const DEFAULT_OWNER_TTL_SECONDS =
|
|
13
|
+
loadEngineeringPolicy().ownership.defaultTtlSeconds;
|
|
8
14
|
const LOCK_STALE_MS = 30_000;
|
|
9
15
|
const LOCK_RETRIES = 80;
|
|
10
16
|
const LOCK_RETRY_MS = 25;
|
|
@@ -3,6 +3,7 @@ name: openxiangda
|
|
|
3
3
|
description: "Use OpenXiangda for private low-code platform work: app workspaces, forms, pages, resources, functions, automations, workflows, permissions, publishing, deployment, diagnosis, profiles, and the openxiangda CLI."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
<!-- OpenXiangda-Policy-Version: 3 -->
|
|
6
7
|
# OpenXiangda
|
|
7
8
|
|
|
8
9
|
OpenXiangda connects an AI coding workspace to the private low-code platform through a normal-user profile and `/openxiangda-api/v1`. External backends using AK/SK are a separate `openxiangda-open-api` flow.
|
|
@@ -62,7 +63,7 @@ Function quick changes require the complete `--risk-json` assessment; Automation
|
|
|
62
63
|
|
|
63
64
|
Quick changes use compact generated JSON/prose. Do not expand them into proposal/design essays; implement the exact covered code, run the focused check, and let the mainline bundle own final atomic promotion.
|
|
64
65
|
|
|
65
|
-
Keep development lightweight: structured approval plus exact resource/file scope are authoritative. By default,
|
|
66
|
+
Keep development lightweight: structured approval plus exact resource/file scope are authoritative. By default, only `change.json`, `coverage.json`, and `release.json` are created; use `openxiangda sdd render <change>` when prose is genuinely useful. Missing tasks/evidence/spec prose warns and does not block release; set `strictDocumentation: true` only for a workspace that intentionally wants prose as a gate. Actual publish argv, mainline identity, CAS, lease, destructive scope, and atomic activation remain hard gates.
|
|
66
67
|
|
|
67
68
|
The CLI regenerates one canonical command set from structured coverage instead of asking agents to maintain two command copies. A single Function uses `resource publish function --only <code>`; qualified selectors are used only when Function and Automation are mixed. Generated React SPA plans use exact Form bundles, one Backend `--stage-only` command, Runtime `--no-activate`, and Root `app-finalize`. Actual argv is validated again at write time.
|
|
68
69
|
|
|
@@ -91,7 +92,7 @@ openxiangda release publish --change <release-change> --profile <name>
|
|
|
91
92
|
|
|
92
93
|
`release publish` is the default promotion entrypoint. It verifies without rewriting reviewed `change.json`/`release.json`, waits for the app lease, freezes the App capture after ownership is acquired, executes deterministic exact staged steps, resumes from `.openxiangda/releases/<change>/execution.json`, atomically finalizes, verifies mainline integration, and releases the lease. `release begin` and child commands remain recovery/diagnostic primitives.
|
|
93
94
|
|
|
94
|
-
Reviewed bundle commands may retain `<profile>` as a template. The explicit real `release publish --profile <name>` value is bound to actual child argv without rewriting tracked SDD. React SPA page codes are logical coverage targets and activate through one Runtime child; they do not require PageRelease. `release app-head`
|
|
95
|
+
Reviewed bundle commands may retain `<profile>` as a template. The explicit real `release publish --profile <name>` value is bound to actual child argv without rewriting tracked SDD. React SPA page codes are logical coverage targets and activate through one Runtime child; they do not require PageRelease. `release app-head` and `runtime releases` are compact by default; use `--full` only when the complete manifest is required.
|
|
95
96
|
|
|
96
97
|
`release begin --change` freezes a clean committed `HEAD` only when the current branch is the authoritative default `main`/`master` and its commit exactly equals the live remote tip. Before any live write, the CLI preflights the complete target set and rejects source changes during the release. A feature worktree or unpushed main receives `RELEASE_SOURCE_MAINLINE_REQUIRED` / `RELEASE_SOURCE_MAINLINE_NOT_PUSHED`; merge, test, push, and start the one mainline release instead of forcing it. Optional `.git` remote suffix differences are aliases of the same repository; genuinely different remotes still fail closed.
|
|
97
98
|
|
|
@@ -26,7 +26,7 @@ Run `openxiangda update check --json` once per substantial task, on a suspected
|
|
|
26
26
|
|
|
27
27
|
Every write/release command must include `--profile <name>`. Parallel tasks develop in isolated Git worktrees/branches, but do not publish there. Merge approved commits into the authoritative remote default branch, create one `sdd bundle` for the intended changes, commit/push it, and publish once from a clean local `main`/`master` that exactly equals the remote tip.
|
|
28
28
|
|
|
29
|
-
SDD is
|
|
29
|
+
SDD is structured-first by default: only `change.json`, `coverage.json`, and `release.json` are created. Generate optional prose with `openxiangda sdd render <change>`; missing checklist/evidence/spec prose is a warning, while approval, exact structured scope, actual argv, mainline identity, child CAS, lease, and atomic activation remain hard gates. Set `strictDocumentation: true` only when prose completion must intentionally block a workspace.
|
|
30
30
|
|
|
31
31
|
First-class direct configuration writes (form/page/menu/role/permission/workflow/connector/notification/auth/route/public-access/scope/data-view/storage/settings) also require `--change <id>` or a complete stored session created by `openxiangda release begin --change <id>`. Missing context fails locally with `PUBLISH_CONTEXT_REQUIRED` before any HTTP write. Dry-run, schema planning, validation, query/invoke/export/submit/execution/upload, and other data-plane actions do not acquire a lease. Direct Workflow publish/unpublish/delete additionally requires the frozen `--expected-revision`; never fetch the latest revision merely to replay an older payload.
|
|
32
32
|
|
|
@@ -98,7 +98,7 @@ Reviewed bundle commands may retain `<profile>` as a template. The explicit real
|
|
|
98
98
|
|
|
99
99
|
`runtime deploy --no-activate` uploads an immutable preview release from a clean committed mainline `HEAD`. It reads a narrow Runtime head instead of the full app snapshot. Before acquiring a lease, the CLI confirms `package.json#scripts.build` and existing dependencies; it uses `npm run build` to execute the declared script so pnpm worktree symlinks do not trigger a reinstall. Immutable Git-base artifact hashes are cached under the Git common directory. `release begin` requires local main/master and the live remote default tip to be identical, so `integration-status` is already satisfied after activation and `release end` does not wait for a later merge.
|
|
100
100
|
|
|
101
|
-
`release app-head`
|
|
101
|
+
`release app-head` and `runtime releases` return compact summaries by default. Use `--full` only when the complete manifest is required.
|
|
102
102
|
|
|
103
103
|
`resource plan`, `resource publish`, and `runtime deploy` report coarse phases on stderr and emit a heartbeat after 15 seconds without completion. JSON stdout stays machine-readable and includes `timings`; use it to identify whether time is spent in plan, preflight, build, upload, write, or activate instead of rerunning an opaque command.
|
|
104
104
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openxiangda",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.176",
|
|
4
4
|
"description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"bin": {
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"bin/",
|
|
54
54
|
"lib/",
|
|
55
55
|
"openxiangda-skills/",
|
|
56
|
+
"policy/",
|
|
56
57
|
"templates/",
|
|
57
58
|
"packages/sdk/bin/",
|
|
58
59
|
"packages/sdk/dist/",
|
|
@@ -62,8 +63,20 @@
|
|
|
62
63
|
"scripts": {
|
|
63
64
|
"build:sdk": "tsup --config packages/sdk/tsup.config.ts",
|
|
64
65
|
"check": "node --check bin/openxiangda.js && node --check lib/*.js && node --check packages/sdk/src/build-source/src/cli.mjs && node --check packages/sdk/src/build-source/scripts/*.mjs && node --check packages/sdk/src/build-source/scripts/utils/*.mjs",
|
|
65
|
-
"test": "npm run
|
|
66
|
-
"
|
|
66
|
+
"test": "npm run test:fast",
|
|
67
|
+
"test:fast": "npm run check && node scripts/http-proxy-smoke.mjs && node scripts/form-export-cli-smoke.mjs && node scripts/open-api-cli-smoke.mjs && node scripts/release-plan-smoke.mjs && node scripts/typed-resource-plan-smoke.mjs && node scripts/integration-bundle-smoke.mjs && node scripts/content-cache-smoke.mjs && node scripts/dependency-capsule-smoke.mjs && node scripts/sdd-stages-smoke.mjs",
|
|
68
|
+
"test:changed": "npm run check && node scripts/run-test-suite.mjs --changed --evidence",
|
|
69
|
+
"test:contract": "npm run check && node scripts/run-test-suite.mjs --contract --evidence",
|
|
70
|
+
"test:release": "npm run check && node scripts/run-test-suite.mjs --release --evidence",
|
|
71
|
+
"test:critical": "npm run test:release",
|
|
72
|
+
"test:all": "node scripts/run-test-suite.mjs --all --evidence",
|
|
73
|
+
"test:full": "npm run test:all",
|
|
74
|
+
"test:ci": "npm run check && npm run test:all",
|
|
75
|
+
"test:content-cache": "node scripts/content-cache-smoke.mjs",
|
|
76
|
+
"test:dependency-capsule": "node scripts/dependency-capsule-smoke.mjs",
|
|
77
|
+
"test:typed-resource-plan": "node scripts/typed-resource-plan-smoke.mjs",
|
|
78
|
+
"test:integration-bundle": "node scripts/integration-bundle-smoke.mjs",
|
|
79
|
+
"prepublishOnly": "npm run test:critical && node scripts/release-mainline-guard.mjs",
|
|
67
80
|
"prepack": "npm run build:sdk",
|
|
68
81
|
"test:profile-isolation": "bash scripts/profile-isolation-smoke.sh",
|
|
69
82
|
"test:resource-plan": "node scripts/resource-plan-smoke.mjs",
|
|
@@ -86,6 +99,9 @@
|
|
|
86
99
|
"test:publish-lease-state": "node scripts/publish-lease-state-smoke.mjs",
|
|
87
100
|
"test:publish-lease-heartbeat": "node scripts/publish-lease-heartbeat-smoke.mjs",
|
|
88
101
|
"test:release-telemetry": "node scripts/release-telemetry-smoke.mjs",
|
|
102
|
+
"test:release-explain": "node scripts/release-explain-smoke.mjs",
|
|
103
|
+
"test:task-status": "node scripts/task-status-smoke.mjs",
|
|
104
|
+
"test:policy": "node scripts/policy-smoke.mjs",
|
|
89
105
|
"test:release-plan": "node scripts/release-plan-smoke.mjs",
|
|
90
106
|
"test:release-mainline": "node scripts/release-mainline-smoke.mjs",
|
|
91
107
|
"test:publish-lease-cli": "node scripts/publish-lease-cli-smoke.mjs",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "openxiangda_engineering_policy_v1",
|
|
3
|
+
"policyVersion": 3,
|
|
4
|
+
"risk": {
|
|
5
|
+
"levels": ["L0", "L1", "L2", "L3"],
|
|
6
|
+
"quickMaxLevel": "L1",
|
|
7
|
+
"classifyBy": [
|
|
8
|
+
"data-impact",
|
|
9
|
+
"permission-impact",
|
|
10
|
+
"contract-impact",
|
|
11
|
+
"reversibility",
|
|
12
|
+
"dependency-blast-radius"
|
|
13
|
+
],
|
|
14
|
+
"directoryIsOnlySignal": true
|
|
15
|
+
},
|
|
16
|
+
"sdd": {
|
|
17
|
+
"sourceOfTruth": "openspec/changes/<change>/change.json",
|
|
18
|
+
"verifyIsReadOnly": true,
|
|
19
|
+
"markdownIsDerived": true,
|
|
20
|
+
"explicitWriteCommands": ["sdd sync", "sdd record-result", "sdd render"]
|
|
21
|
+
},
|
|
22
|
+
"git": {
|
|
23
|
+
"platformRepositoryMode": "direct-authoritative-master",
|
|
24
|
+
"applicationTaskMode": "isolated-worktree",
|
|
25
|
+
"applicationPublishMode": "clean-authoritative-master",
|
|
26
|
+
"taskReadyCommand": "sdd ready",
|
|
27
|
+
"integrationCommand": "sdd bundle",
|
|
28
|
+
"requireRemoteMainlineContainment": true
|
|
29
|
+
},
|
|
30
|
+
"release": {
|
|
31
|
+
"planAndVerifyAreReadOnly": true,
|
|
32
|
+
"atomicChildren": [
|
|
33
|
+
"RuntimeRelease",
|
|
34
|
+
"PageRelease",
|
|
35
|
+
"BackendRelease",
|
|
36
|
+
"FormRelease",
|
|
37
|
+
"WorkflowRelease"
|
|
38
|
+
],
|
|
39
|
+
"unsupportedGenericMode": "fail-closed",
|
|
40
|
+
"explicitMaintenanceDirectMode": true,
|
|
41
|
+
"postCommitMode": "transactional-outbox"
|
|
42
|
+
},
|
|
43
|
+
"ownership": {
|
|
44
|
+
"defaultTtlSeconds": 7200,
|
|
45
|
+
"releaseOnTaskReady": true
|
|
46
|
+
},
|
|
47
|
+
"testing": {
|
|
48
|
+
"layers": ["changed", "contract", "release", "all"],
|
|
49
|
+
"evidenceDirectory": ".openxiangda/evidence/tests"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<!-- OpenXiangda-Policy-Version: 3 -->
|
|
1
2
|
# AGENTS.md — OpenXiangda React SPA 应用工作区
|
|
2
3
|
|
|
3
4
|
本工作区是标准 React 18 + Vite + React Router 应用。默认模板只提供应用壳、账号菜单和一个首页,不是开发验证控制台。
|
|
@@ -6,7 +7,7 @@
|
|
|
6
7
|
|
|
7
8
|
- 架构类需求默认只规划、不实现。新应用、复杂页面、登录注册、公开访问、权限数据范围、流程自动化、连接器/通知等需求,先运行 `openxiangda doctor --json` 和 `openxiangda design gates --topic <code> --json`,输出设计并等用户确认;确认前只允许读取、快照、dry-run、提问和写设计文档,不允许改源码、写平台、发布或发送通知。
|
|
8
9
|
- 先按风险分级:只读/文档/测试为 L0;纯文案样式或单一既有资源绑定等可逆窄改为 L1,可用受限的 `openxiangda sdd quick` 记录精确范围;表单结构、业务函数、自动化/流程、权限、登录/公开访问、数据写入和 runtime/config 为 L2;不可逆、生产迁移或应用级扩权为 L3。L2/L3 必须挂完整 SDD,并由 `coverage.json` 把需求与场景映射到精确的文件、表单、页面及工程资源范围。
|
|
9
|
-
- 一个开发任务只使用一个显式 change,并从 `openxiangda sdd context --change <change> --changed --json`
|
|
10
|
+
- 一个开发任务只使用一个显式 change,并从 `openxiangda sdd context --change <change> --changed --json` 开始;默认只维护 change/coverage/release 三份结构化事实源,需要文档时才执行 `openxiangda sdd render <change>`。结构化 approval、资源和文件范围是硬约束,任务/证据/规格文案默认只告警。只有明确配置 `strictDocumentation: true` 才把文案完成度恢复为门禁。
|
|
10
11
|
- 多会话开发使用独立 Git worktree/branch,但 feature worktree 不发布。把已批准提交合并并 push 到权威默认主分支后,用 `sdd bundle <release-change> --changes ...` 聚合范围;只从与远端 tip 完全一致的 clean `main`/`master` 一次发布。
|
|
11
12
|
- 账号、角色、权限、数据范围、组织账号、RBAC、查询参数授权需求必须先运行 `openxiangda design gates --topic permissions --json`,选择 `managed-platform-account` / `existing-platform-user-assignment` / `static-role-permission` / `query-param-context`,输出权限矩阵后再实现。
|
|
12
13
|
- 应用角色只读查询组织账号时声明 `app:organization:read`;创建、修改账号/部门或重置密码时声明 `app:organization:manage`。创建角色、分配成员或维护权限组也必须在角色资源的 `apiPermissionCodes` 声明对应的 `app:role:manage`、`app:page-permission-group:manage`、`app:form-permission-group:manage`。
|
|
@@ -42,6 +42,7 @@ export default defineAppWorkspaceConfig({
|
|
|
42
42
|
strictHighRisk:
|
|
43
43
|
process.env.OPENXIANGDA_SDD_STRICT_HIGH_RISK === "false" ? false : true,
|
|
44
44
|
strictDocumentation: false,
|
|
45
|
+
documentationMode: 'structured',
|
|
45
46
|
path: process.env.OPENXIANGDA_SDD_PATH || "openspec",
|
|
46
47
|
schemaVersion: "openxiangda-sdd-v2",
|
|
47
48
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<!-- OpenXiangda-Policy-Version: 3 -->
|
|
1
2
|
# AGENTS.md — OpenXiangda 工作区 AI 强约束
|
|
2
3
|
|
|
3
4
|
> 任何 AI(Qoder / Claude / Codex / Cursor / Copilot 等)在本工作区操作前 **必须先读完本文件**。
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
|
|
41
42
|
- ✅ 先 preflight:`openxiangda env --profile <name>` + `openxiangda auth status --profile <name>` + `openxiangda update check --json`。
|
|
42
43
|
- ✅ 架构类需求先 plan gate:`openxiangda doctor --profile <name> --json` + `openxiangda design gates --topic <code> --json`,用户确认后再实现。
|
|
43
|
-
- ✅ L2/L3 保留 context → propose → 用户确认 → approve
|
|
44
|
+
- ✅ L2/L3 保留 context → propose → 用户确认 → approve;默认只创建 change/coverage/release,完整文档由 `sdd render` 按需生成。approval、coverage 与实际发布范围为硬门禁,未完成的任务/证据/规格文案只告警。需要严格文档门禁时显式配置 `strictDocumentation: true`。
|
|
44
45
|
- ✅ 涉及账号、角色、权限、数据范围、组织账号、RBAC、查询参数授权时,先跑 `openxiangda design gates --topic permissions --json`,选择 `managed-platform-account` / `existing-platform-user-assignment` / `static-role-permission` / `query-param-context`,并输出权限矩阵后再改资源。
|
|
45
46
|
- ✅ 某个应用角色如果要查询组织账号,声明 `app:organization:read`;如需创建、修改账号/部门或重置密码,再声明 `app:organization:manage`。角色设置、页面/表单权限组等能力同样通过 `src/resources/roles/<code>.json` 的 `apiPermissionCodes` 显式授权,例如 `app:role:manage`、`app:page-permission-group:manage`、`app:form-permission-group:manage`。
|
|
46
47
|
- ✅ 单文件改动默认按 change 和逻辑资源增量发布:`workspace plan --profile <name> --change <change> --changed`,再 `workspace publish --profile <name> --change <change> --only pages/a,forms/b --dry-run` → 正式发布。
|
|
@@ -43,6 +43,7 @@ export default defineAppWorkspaceConfig({
|
|
|
43
43
|
strictHighRisk:
|
|
44
44
|
process.env.OPENXIANGDA_SDD_STRICT_HIGH_RISK === "false" ? false : true,
|
|
45
45
|
strictDocumentation: false,
|
|
46
|
+
documentationMode: 'structured',
|
|
46
47
|
path: process.env.OPENXIANGDA_SDD_PATH || "openspec",
|
|
47
48
|
schemaVersion: "openxiangda-sdd-v2",
|
|
48
49
|
},
|