@inventec/dsh-copilot-auth 1.2.17 → 1.2.19
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 +25 -21
- package/lib/client.js +305 -214
- package/package.json +2 -2
- package/src/auth-flow.mjs +11 -15
- package/src/auth-host.mjs +24 -41
- package/src/host.mjs +0 -18
- package/src/model-update.mjs +107 -82
- package/src/refresh-flow.mjs +115 -33
- package/src/refresh-service.mjs +28 -13
- package/src/shared.mjs +1 -2
package/src/refresh-flow.mjs
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
// refresh-flow.mjs —
|
|
1
|
+
// refresh-flow.mjs — 「管理模型列表」client 侧状态机(协议 v3,ADR 0005 / Q32–Q36)。
|
|
2
2
|
// reduce 为纯函数;runEffect/advance 只使用注入的 fetchImpl(node:test 可测)。
|
|
3
|
-
// 状态:idle → previewing → confirming(勾选→materialize
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// 状态:idle → previewing → confirming(勾选→materialize 循环;缓存证据+删除提案时
|
|
4
|
+
// 自动 live 升级:闩锁、硬失败回退 materialize、软降级置 staleNotice)→ applying →
|
|
5
|
+
// result / pendingRestart / checking / busy / resultUnknown;recovery-needed 经
|
|
6
|
+
// retireConfirm → retiring → 重新预览闭环。
|
|
6
7
|
// 关键规则:
|
|
7
|
-
// -
|
|
8
|
-
// -
|
|
8
|
+
// - 勾选集合即终态;选择变化经 basePreviewId 从原快照 materialize(不重新取数)
|
|
9
|
+
// - 确认门按实际 diff:removed>0 ∨ clearOverrides ∨ 空目标 → 二次确认;空目标另需 empty
|
|
10
|
+
// - 升级判定覆盖两条 preview-ok 迁移路径(初始进入与 confirming 内原位替换);
|
|
11
|
+
// 升级在途置 materializing(确认门拦截);硬失败回退保留勾选,软降级置 staleNotice
|
|
9
12
|
// - 423 转 status 查询;apply 网络不明先查同 operationId,绝不换 ID 重发
|
|
10
13
|
// - 预览过期/漂移(409 preview-stale/auth-changed/preview-invalid)→ 自动重新预览
|
|
11
14
|
import { routes, PROTOCOL_VERSION } from "./shared.mjs";
|
|
@@ -37,6 +40,49 @@ export function refreshBlocked(flags) {
|
|
|
37
40
|
return flags?.scopeAvailable === false || mode === "blocked" || mode === "unknown";
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
// lastSelection → effect 选择字段(逐字段在场性复制;空对象 → 不带任何选择字段)
|
|
44
|
+
function selectionFields(lastSelection = {}) {
|
|
45
|
+
const out = {};
|
|
46
|
+
if (lastSelection.selectedIds !== undefined) out.selectedIds = lastSelection.selectedIds;
|
|
47
|
+
if (lastSelection.clearOverrides !== undefined) out.clearOverrides = lastSelection.clearOverrides;
|
|
48
|
+
if (lastSelection.confirmEmpty !== undefined) out.confirmEmpty = lastSelection.confirmEmpty;
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 升级判定:删除提案 ∧ 有效缓存 ∧ latest 目录 ∧ 闩锁未用 → 发新鲜预览(不走 basePreviewId)。
|
|
53
|
+
// 初始默认预览的 lastSelection 为空对象 → 升级 effect 不带选择字段,服务端按 live 数据重算默认
|
|
54
|
+
// (全漂移 corner 因此不可能触发空目标确认,M01 弹窗必开)。
|
|
55
|
+
function upgradeCandidate(state) {
|
|
56
|
+
const p = state.preview;
|
|
57
|
+
if (!p || state.liveUpgradeUsed) return null;
|
|
58
|
+
if ((p.diff?.removed?.length ?? 0) === 0) return null;
|
|
59
|
+
if (p.evidence?.source !== "cache" || p.evidence?.stale === true) return null;
|
|
60
|
+
if (p.catalogSource !== "latest") return null;
|
|
61
|
+
return {
|
|
62
|
+
type: "preview",
|
|
63
|
+
operation: p.operation,
|
|
64
|
+
catalogSource: p.catalogSource,
|
|
65
|
+
...selectionFields(state.lastSelection),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 进入 confirming(两条 preview-ok 迁移路径共用):先落确认基态,再评估自动升级。
|
|
70
|
+
function enterConfirming({ flags, preview, staleNotice, lastSelection }) {
|
|
71
|
+
const state = {
|
|
72
|
+
name: "confirming",
|
|
73
|
+
flags: flags ?? initial.flags,
|
|
74
|
+
preview,
|
|
75
|
+
staleNotice: staleNotice === true,
|
|
76
|
+
materializing: false,
|
|
77
|
+
awaitingUpgrade: false,
|
|
78
|
+
liveUpgradeUsed: false,
|
|
79
|
+
lastSelection: lastSelection ?? {},
|
|
80
|
+
};
|
|
81
|
+
const effect = upgradeCandidate(state);
|
|
82
|
+
if (!effect) return [state, null];
|
|
83
|
+
return [{ ...state, liveUpgradeUsed: true, awaitingUpgrade: true, materializing: true }, effect];
|
|
84
|
+
}
|
|
85
|
+
|
|
40
86
|
export function reduce(state, event) {
|
|
41
87
|
switch (state.name) {
|
|
42
88
|
case "idle":
|
|
@@ -46,17 +92,14 @@ export function reduce(state, event) {
|
|
|
46
92
|
case "busy":
|
|
47
93
|
case "resultUnknown": {
|
|
48
94
|
if (event.type === "start") {
|
|
95
|
+
// v3 单一操作:恒为 manage(无 legacy 字面量分支)
|
|
49
96
|
return [{
|
|
50
97
|
name: "previewing",
|
|
51
98
|
flags: state.flags ?? initial.flags,
|
|
52
|
-
operation:
|
|
99
|
+
operation: "manage",
|
|
53
100
|
catalogSource: event.catalogSource,
|
|
54
101
|
stale: false,
|
|
55
|
-
}, {
|
|
56
|
-
type: "preview",
|
|
57
|
-
operation: event.operation === "rebuild" ? "rebuild" : "supplement",
|
|
58
|
-
catalogSource: event.catalogSource,
|
|
59
|
-
}];
|
|
102
|
+
}, { type: "preview", operation: "manage", catalogSource: event.catalogSource }];
|
|
60
103
|
}
|
|
61
104
|
if (event.type === "init" && state.name === "idle") return [state, { type: "status" }];
|
|
62
105
|
if (event.type === "check" && state.name === "resultUnknown") {
|
|
@@ -87,7 +130,7 @@ export function reduce(state, event) {
|
|
|
87
130
|
}
|
|
88
131
|
case "previewing": {
|
|
89
132
|
if (event.type === "preview-ok") {
|
|
90
|
-
return
|
|
133
|
+
return enterConfirming({ flags: state.flags ?? initial.flags, preview: event.preview, staleNotice: state.stale === true });
|
|
91
134
|
}
|
|
92
135
|
if (event.type === "preview-fail") return [{ name: "failed", flags: state.flags ?? initial.flags, error: event.error }, null];
|
|
93
136
|
return [state, null];
|
|
@@ -95,53 +138,89 @@ export function reduce(state, event) {
|
|
|
95
138
|
case "confirming": {
|
|
96
139
|
const p = state.preview;
|
|
97
140
|
if (event.type === "select") {
|
|
141
|
+
const lastSelection = { ...selectionFields({
|
|
142
|
+
selectedIds: event.selectedIds,
|
|
143
|
+
clearOverrides: event.clearOverrides,
|
|
144
|
+
confirmEmpty: event.confirmEmpty,
|
|
145
|
+
}) };
|
|
98
146
|
// 选择变化:从原快照 materialize(服务端重算,沿用原有效期,不重新取数)。
|
|
99
|
-
// 留在 confirming+materializing
|
|
100
|
-
// 历史实现切回 previewing 导致弹窗整体关闭再打开(勾选闪烁的根因,2026-10-05)。
|
|
147
|
+
// 留在 confirming+materializing——弹窗保持挂载、不卸载重挂。
|
|
101
148
|
return [{
|
|
102
149
|
name: "confirming",
|
|
103
150
|
flags: state.flags ?? initial.flags,
|
|
104
151
|
preview: p,
|
|
105
152
|
materializing: true,
|
|
153
|
+
awaitingUpgrade: false,
|
|
154
|
+
liveUpgradeUsed: state.liveUpgradeUsed ?? false,
|
|
155
|
+
lastSelection,
|
|
106
156
|
staleNotice: state.staleNotice,
|
|
107
157
|
}, {
|
|
108
158
|
type: "preview",
|
|
109
159
|
operation: p.operation,
|
|
110
160
|
catalogSource: p.catalogSource,
|
|
111
161
|
basePreviewId: p.previewId,
|
|
112
|
-
|
|
113
|
-
confirmEmpty: event.confirmEmpty === true,
|
|
162
|
+
...selectionFields(lastSelection),
|
|
114
163
|
}];
|
|
115
164
|
}
|
|
116
165
|
if (event.type === "preview-ok") {
|
|
117
|
-
// materialize
|
|
118
|
-
|
|
166
|
+
// materialize/切源/升级完成:预览原位替换(新 previewId 绑定新选择)。
|
|
167
|
+
// 升级软降级(R4-3):落地后证据仍非 live → staleNotice(UI 据此禁用 apply)。
|
|
168
|
+
const wasUpgrade = state.awaitingUpgrade === true;
|
|
169
|
+
const landed = {
|
|
170
|
+
name: "confirming",
|
|
171
|
+
flags: state.flags ?? initial.flags,
|
|
172
|
+
preview: event.preview,
|
|
173
|
+
materializing: false,
|
|
174
|
+
awaitingUpgrade: false,
|
|
175
|
+
liveUpgradeUsed: state.liveUpgradeUsed ?? false,
|
|
176
|
+
lastSelection: state.lastSelection ?? {},
|
|
177
|
+
staleNotice: (wasUpgrade && event.preview?.evidence?.source !== "live") || state.staleNotice === true,
|
|
178
|
+
};
|
|
179
|
+
const effect = upgradeCandidate(landed);
|
|
180
|
+
if (effect) return [{ ...landed, liveUpgradeUsed: true, awaitingUpgrade: true, materializing: true }, effect];
|
|
181
|
+
return [landed, null];
|
|
119
182
|
}
|
|
120
183
|
if (event.type === "preview-fail") {
|
|
184
|
+
if (state.awaitingUpgrade === true && p) {
|
|
185
|
+
// 升级硬失败回退:以升级前快照 materialize(逐字段复制 lastSelection,保留勾选);
|
|
186
|
+
// 弹窗保持挂载;回退后再失败才落 failed。
|
|
187
|
+
return [{
|
|
188
|
+
name: "confirming",
|
|
189
|
+
flags: state.flags ?? initial.flags,
|
|
190
|
+
preview: p,
|
|
191
|
+
materializing: true,
|
|
192
|
+
awaitingUpgrade: false,
|
|
193
|
+
liveUpgradeUsed: state.liveUpgradeUsed ?? true,
|
|
194
|
+
lastSelection: state.lastSelection ?? {},
|
|
195
|
+
staleNotice: true,
|
|
196
|
+
}, {
|
|
197
|
+
type: "preview",
|
|
198
|
+
operation: p.operation,
|
|
199
|
+
catalogSource: p.catalogSource,
|
|
200
|
+
basePreviewId: p.previewId,
|
|
201
|
+
...selectionFields(state.lastSelection),
|
|
202
|
+
}];
|
|
203
|
+
}
|
|
121
204
|
return [{ name: "failed", flags: state.flags ?? initial.flags, error: event.error }, null];
|
|
122
205
|
}
|
|
123
206
|
if (event.type === "start") {
|
|
124
|
-
// 显式切源(overlay/local
|
|
125
|
-
// 同 select:留在 confirming 保持弹窗挂载。
|
|
207
|
+
// 显式切源(overlay/local):生成全新预览(不隐式降级,Q7);留在 confirming 保持弹窗挂载
|
|
126
208
|
return [{
|
|
127
209
|
name: "confirming",
|
|
128
210
|
flags: state.flags ?? initial.flags,
|
|
129
211
|
preview: p,
|
|
130
212
|
materializing: true,
|
|
213
|
+
awaitingUpgrade: false,
|
|
214
|
+
liveUpgradeUsed: state.liveUpgradeUsed ?? false,
|
|
215
|
+
lastSelection: state.lastSelection ?? {},
|
|
131
216
|
staleNotice: state.staleNotice,
|
|
132
|
-
}, {
|
|
133
|
-
type: "preview",
|
|
134
|
-
operation: event.operation === "rebuild" ? "rebuild" : p.operation,
|
|
135
|
-
catalogSource: event.catalogSource,
|
|
136
|
-
}];
|
|
217
|
+
}, { type: "preview", operation: p.operation, catalogSource: event.catalogSource }];
|
|
137
218
|
}
|
|
138
219
|
if (event.type === "confirm") {
|
|
139
|
-
if (state.materializing) return [state, null]; //
|
|
140
|
-
const isRebuild = p.operation === "rebuild";
|
|
141
|
-
const emptyTarget = isRebuild && (p.diff?.targetView?.models ?? []).length === 0;
|
|
142
|
-
if (isRebuild && event.second !== true) return [state, null]; // 重建需二次确认
|
|
143
|
-
if (emptyTarget && event.empty !== true) return [state, null]; // 清空需单独确认
|
|
220
|
+
if (state.materializing) return [state, null]; // 物化/升级在途不得用旧 previewId 应用
|
|
144
221
|
if (p.evidence?.stale === true) return [state, null]; // stale 证据不可应用(Q7)
|
|
222
|
+
// 单次确认(Q37 用户裁决):破坏性意图已由固定底栏常驻风险线在操作前完整告知,
|
|
223
|
+
// 不再要求二次点击;证据门(removals-need-live)由服务端独立兜底
|
|
145
224
|
return [{ name: "applying", flags: state.flags ?? initial.flags, preview: p }, { type: "apply", previewId: p.previewId, operationId: p.operationId }];
|
|
146
225
|
}
|
|
147
226
|
if (event.type === "cancel") return [{ ...initial, flags: state.flags ?? initial.flags, lastResult: state.lastResult ?? null }, null];
|
|
@@ -216,11 +295,14 @@ export async function runEffect(effect, fetchImpl) {
|
|
|
216
295
|
body: JSON.stringify({ protocolVersion: PROTOCOL_VERSION, ...body }),
|
|
217
296
|
});
|
|
218
297
|
if (effect.type === "preview") {
|
|
298
|
+
// 选择字段按在场性组装(R1-9):初始预览不带;materialize/升级/回退按 effect 实际字段携带
|
|
219
299
|
const res = await post(r.refreshPreview, {
|
|
220
300
|
operation: effect.operation,
|
|
221
301
|
...(effect.catalogSource ? { catalogSource: effect.catalogSource } : {}),
|
|
222
|
-
...(effect.basePreviewId ? { basePreviewId: effect.basePreviewId
|
|
223
|
-
...(effect.
|
|
302
|
+
...(effect.basePreviewId !== undefined ? { basePreviewId: effect.basePreviewId } : {}),
|
|
303
|
+
...(effect.selectedIds !== undefined ? { selectedIds: effect.selectedIds } : {}),
|
|
304
|
+
...(effect.clearOverrides !== undefined ? { clearOverrides: effect.clearOverrides } : {}),
|
|
305
|
+
...(effect.confirmEmpty !== undefined ? { confirmEmpty: effect.confirmEmpty } : {}),
|
|
224
306
|
});
|
|
225
307
|
if (res.ok) return { type: "preview-ok", preview: res.body };
|
|
226
308
|
return { type: "preview-fail", error: res.error?.details?.errorCode ?? res.error?.messageKey };
|
package/src/refresh-service.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// refresh-service.mjs — 预览服务与账号证据(T8)。
|
|
2
2
|
// 职责:内存预览快照(10 分钟、选择从原快照重算、operationId 绑定选择)、
|
|
3
|
-
//
|
|
3
|
+
// 账号证据门禁(移除类变更需 live+latest;纯新增允许 ≤24h 同 intentVersion 缓存;stale 只参考不可应用)、
|
|
4
4
|
// apply 幂等(同 operationId 返回已知结果;activeOperation 未终结时新 apply 423)、
|
|
5
5
|
// boot/apply/retire 同一队列串行。
|
|
6
6
|
// 事务内核(T9)经 transaction 注入:{runApply({snapshot}), bootRecover(), retire({operationId})}。
|
|
@@ -26,8 +26,9 @@ function flowError(code, message, extra = {}) {
|
|
|
26
26
|
const iso = (ts) => new Date(ts).toISOString();
|
|
27
27
|
|
|
28
28
|
// 账号证据:live GET 成功才写缓存(fetchedAt+intentVersion);缓存仅 ≤24h 且同
|
|
29
|
-
// intentVersion
|
|
30
|
-
//
|
|
29
|
+
// intentVersion 时可作纯新增的应用依据;否则 stale——预览可展示参考,apply 被拒(Q7/Q16)。
|
|
30
|
+
// 移除类变更(removed>0)在 apply 时强制 live 证据+latest 目录(removals-need-live,Q35/R2);
|
|
31
|
+
// live 拉取失败回退有效缓存属软降级,由 client 端升级/禁用逻辑兜底(R4-3),服务端门最终兜底。
|
|
31
32
|
async function resolveAccountEvidence(ctx, { scope, readIntentVersion, clock, fetchImpl }) {
|
|
32
33
|
const iv = readIntentVersion();
|
|
33
34
|
const cacheFile = join(scope.dataDir, "account-model-cache.json");
|
|
@@ -83,14 +84,15 @@ export function createRefreshService(ctx, {
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
function diffFromInputs({ operation, evidence, catalog, config, selectedIds, confirmEmpty }) {
|
|
87
|
+
function diffFromInputs({ operation, evidence, catalog, config, selectedIds, confirmEmpty, clearOverrides }) {
|
|
87
88
|
const diff = buildModelChange({
|
|
88
89
|
operation,
|
|
89
90
|
rawView: config.view,
|
|
90
91
|
effectiveView: config.effectiveView,
|
|
91
92
|
accountIds: evidence.ids,
|
|
92
93
|
resolvableIds: catalog.resolvableIds,
|
|
93
|
-
selectedIds
|
|
94
|
+
selectedIds, // undefined → 服务端默认(在列∩(账号∩可解析));显式 [] → 用户空目标意图
|
|
95
|
+
clearOverrides: clearOverrides === true,
|
|
94
96
|
confirmEmpty: confirmEmpty === true,
|
|
95
97
|
catalogNewEntryCount: catalog.newEntryCount ?? 0,
|
|
96
98
|
});
|
|
@@ -98,6 +100,11 @@ export function createRefreshService(ctx, {
|
|
|
98
100
|
return diff;
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
const hadModelsOf = (config) => {
|
|
104
|
+
const models = config?.view?.modelsPresent && Array.isArray(config.view.models) ? config.view.models : [];
|
|
105
|
+
return models.some((m) => typeof m?.id === "string");
|
|
106
|
+
};
|
|
107
|
+
|
|
101
108
|
async function preview(body = {}) {
|
|
102
109
|
requireScope();
|
|
103
110
|
requireDeps();
|
|
@@ -106,8 +113,8 @@ export function createRefreshService(ctx, {
|
|
|
106
113
|
for (const [id, snap] of snapshots) {
|
|
107
114
|
if (nowTs > snap.expiresAt) snapshots.delete(id);
|
|
108
115
|
}
|
|
109
|
-
const { operation, catalogSource, basePreviewId, selectedIds, confirmEmpty } = body;
|
|
110
|
-
if (operation !== "
|
|
116
|
+
const { operation, catalogSource, basePreviewId, selectedIds, confirmEmpty, clearOverrides } = body;
|
|
117
|
+
if (operation !== "manage") {
|
|
111
118
|
throw flowError("INVALID_OPERATION", "invalid-operation");
|
|
112
119
|
}
|
|
113
120
|
// 选择变化:从原快照重算(不重新下载、不重新取数),沿用原 expiresAt
|
|
@@ -117,13 +124,14 @@ export function createRefreshService(ctx, {
|
|
|
117
124
|
if (clock.now() > base.expiresAt) throw flowError("PREVIEW_INVALID", "preview-invalid", { reason: "expired" });
|
|
118
125
|
const diff = diffFromInputs({
|
|
119
126
|
operation: base.operation, evidence: base.evidence, catalog: base.catalog,
|
|
120
|
-
config: base.config, selectedIds, confirmEmpty,
|
|
127
|
+
config: base.config, selectedIds, confirmEmpty, clearOverrides,
|
|
121
128
|
});
|
|
122
129
|
const snap = {
|
|
123
130
|
...base,
|
|
124
131
|
previewId: randomUUID(),
|
|
125
132
|
selectedIds: [...(selectedIds ?? [])],
|
|
126
133
|
confirmEmpty: confirmEmpty === true,
|
|
134
|
+
clearOverrides: clearOverrides === true,
|
|
127
135
|
diff,
|
|
128
136
|
operationId: randomUUID(),
|
|
129
137
|
basePreviewId,
|
|
@@ -133,13 +141,10 @@ export function createRefreshService(ctx, {
|
|
|
133
141
|
return projectPreview(snap);
|
|
134
142
|
}
|
|
135
143
|
const evidence = await resolveAccountEvidence(ctx, { scope, readIntentVersion, clock, fetchImpl: getFetch() });
|
|
136
|
-
if (operation === "rebuild" && evidence.source !== "live") {
|
|
137
|
-
throw flowError("EVIDENCE_UNAVAILABLE", "rebuild requires a live account fetch; cache is never used for rebuild");
|
|
138
|
-
}
|
|
139
144
|
const catalog = await resolveCatalog(catalogSource);
|
|
140
145
|
const config = await describeConfigView();
|
|
141
146
|
const intentVersion = readIntentVersion();
|
|
142
|
-
const diff = diffFromInputs({ operation, evidence, catalog, config, selectedIds, confirmEmpty });
|
|
147
|
+
const diff = diffFromInputs({ operation, evidence, catalog, config, selectedIds, confirmEmpty, clearOverrides });
|
|
143
148
|
const now = clock.now();
|
|
144
149
|
const snap = {
|
|
145
150
|
previewId: randomUUID(),
|
|
@@ -148,6 +153,8 @@ export function createRefreshService(ctx, {
|
|
|
148
153
|
operation,
|
|
149
154
|
selectedIds: [...(selectedIds ?? [])],
|
|
150
155
|
confirmEmpty: confirmEmpty === true,
|
|
156
|
+
clearOverrides: clearOverrides === true,
|
|
157
|
+
hadModels: hadModelsOf(config),
|
|
151
158
|
evidence,
|
|
152
159
|
catalog,
|
|
153
160
|
config,
|
|
@@ -172,9 +179,14 @@ export function createRefreshService(ctx, {
|
|
|
172
179
|
catalogError: snap.catalog.catalogError ?? null,
|
|
173
180
|
skipped: snap.catalog.skipped ?? [],
|
|
174
181
|
normalized: snap.catalog.normalized ?? null,
|
|
182
|
+
hadModels: snap.hadModels === true,
|
|
183
|
+
clearOverrides: snap.clearOverrides === true,
|
|
184
|
+
catalogNewEntries: snap.catalog.newEntryCount ?? 0,
|
|
185
|
+
overridesMeta: snap.diff.overridesMeta,
|
|
186
|
+
rows: snap.diff.rows,
|
|
175
187
|
diff: {
|
|
176
188
|
targetView: snap.diff.targetView,
|
|
177
|
-
|
|
189
|
+
selectedIds: snap.diff.selectedIds,
|
|
178
190
|
added: snap.diff.added,
|
|
179
191
|
removed: snap.diff.removed,
|
|
180
192
|
kept: snap.diff.kept,
|
|
@@ -220,6 +232,9 @@ export function createRefreshService(ctx, {
|
|
|
220
232
|
if (snap.evidence.stale) {
|
|
221
233
|
throw flowError("EVIDENCE_STALE", "account evidence is stale (no trusted timestamp, expired, or auth changed); live fetch required");
|
|
222
234
|
}
|
|
235
|
+
if ((snap.diff.removed ?? []).length > 0 && (snap.evidence.source !== "live" || snap.catalog.catalogSource !== "latest")) {
|
|
236
|
+
throw flowError("removals-need-live", "removals-need-live");
|
|
237
|
+
}
|
|
223
238
|
if (!transaction) throw flowError("DEPS_MISSING", "transaction kernel not wired");
|
|
224
239
|
snap.consumed = true;
|
|
225
240
|
return transaction.runApply({ snapshot: snap });
|
package/src/shared.mjs
CHANGED
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
// client 侧 fetch 硬编码同一路径,配置一旦漂移 client 即 404(见计划附录 B)。
|
|
3
3
|
export const CREDENTIAL_KEY = "llm-pi-ai/github-copilot";
|
|
4
4
|
export const ROUTE_PREFIX = "/copilot-auth";
|
|
5
|
-
export const PROTOCOL_VERSION =
|
|
5
|
+
export const PROTOCOL_VERSION = 3; // v3:manage 目标状态编辑(ADR 0005);v2 为 supplement/rebuild 双入口
|
|
6
6
|
export const routes = () => ({
|
|
7
7
|
start: `${ROUTE_PREFIX}/start`,
|
|
8
8
|
state: `${ROUTE_PREFIX}/state`,
|
|
9
9
|
status: `${ROUTE_PREFIX}/status`,
|
|
10
10
|
logout: `${ROUTE_PREFIX}/logout`,
|
|
11
|
-
cancel: `${ROUTE_PREFIX}/cancel`,
|
|
12
11
|
refreshPreview: `${ROUTE_PREFIX}/refresh/preview`,
|
|
13
12
|
refreshApply: `${ROUTE_PREFIX}/refresh/apply`,
|
|
14
13
|
refreshRetire: `${ROUTE_PREFIX}/refresh/retire`,
|