@sema-agent/settings-schema 1.0.0
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 +728 -0
- package/LICENSE +103 -0
- package/README.md +129 -0
- package/dist/api/auth-bridge.d.ts +331 -0
- package/dist/api/auth-bridge.js +210 -0
- package/dist/api/auth.d.ts +216 -0
- package/dist/api/auth.js +138 -0
- package/dist/api/scopes.d.ts +344 -0
- package/dist/api/scopes.js +222 -0
- package/dist/api/wire.d.ts +60 -0
- package/dist/api/wire.js +89 -0
- package/dist/bundle.d.ts +13 -0
- package/dist/bundle.js +67 -0
- package/dist/config-fns.d.ts +318 -0
- package/dist/config-fns.js +472 -0
- package/dist/cross-domain.d.ts +34 -0
- package/dist/cross-domain.js +118 -0
- package/dist/file-edit.d.ts +36 -0
- package/dist/file-edit.js +125 -0
- package/dist/file-store.d.ts +89 -0
- package/dist/file-store.js +238 -0
- package/dist/fleet.d.ts +498 -0
- package/dist/fleet.js +317 -0
- package/dist/hash.d.ts +32 -0
- package/dist/hash.js +59 -0
- package/dist/hooks.d.ts +5477 -0
- package/dist/hooks.js +627 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +26 -0
- package/dist/local-load.d.ts +42 -0
- package/dist/local-load.js +172 -0
- package/dist/migrate.d.ts +255 -0
- package/dist/migrate.js +542 -0
- package/dist/node.d.ts +11 -0
- package/dist/node.js +11 -0
- package/dist/reader.d.ts +18 -0
- package/dist/reader.js +1 -0
- package/dist/remote-exec.d.ts +274 -0
- package/dist/remote-exec.js +182 -0
- package/dist/resolve-roster.d.ts +28 -0
- package/dist/resolve-roster.js +108 -0
- package/dist/safety-merge-spec.d.ts +327 -0
- package/dist/safety-merge-spec.js +70 -0
- package/dist/scheduler-store-node.d.ts +72 -0
- package/dist/scheduler-store-node.js +119 -0
- package/dist/scheduler-store.d.ts +67 -0
- package/dist/scheduler-store.js +89 -0
- package/dist/secret-refs.d.ts +33 -0
- package/dist/secret-refs.js +48 -0
- package/dist/sha256.d.ts +16 -0
- package/dist/sha256.js +114 -0
- package/dist/skills-manifest.d.ts +12 -0
- package/dist/skills-manifest.js +54 -0
- package/dist/types.d.ts +13560 -0
- package/dist/types.js +2118 -0
- package/package.json +138 -0
package/dist/migrate.js
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import { RESERVED_WORKER_ENV } from "./types.js";
|
|
2
|
+
/** 存量 hosts grandfathering(§2-2):对既有条目批量回填 `approved:true` 并剥掉 `capacity`。幂等——
|
|
3
|
+
* 已带 approved 的条目保持原值(重跑不覆盖 admin 手动置回待批的机器)。 */
|
|
4
|
+
export function grandfatherHosts(rawHosts) {
|
|
5
|
+
if (rawHosts === null || typeof rawHosts !== "object" || !Array.isArray(rawHosts.hosts)) {
|
|
6
|
+
return { hosts: undefined, approvedBackfilled: [], capacityStripped: [] };
|
|
7
|
+
}
|
|
8
|
+
const approvedBackfilled = [];
|
|
9
|
+
const capacityStripped = [];
|
|
10
|
+
const hosts = (rawHosts.hosts).map((h) => {
|
|
11
|
+
if (h === null || typeof h !== "object")
|
|
12
|
+
return h;
|
|
13
|
+
const entry = { ...h };
|
|
14
|
+
const name = typeof entry.name === "string" ? entry.name : "(unnamed)";
|
|
15
|
+
if (entry.approved === undefined) {
|
|
16
|
+
entry.approved = true; // grandfathering:存量机器已在编,不因契约升级退回待批
|
|
17
|
+
approvedBackfilled.push(name);
|
|
18
|
+
}
|
|
19
|
+
if ("capacity" in entry) {
|
|
20
|
+
delete entry.capacity; // 0.6.0 契约已删;留着也会被 zod strip,主动剥干净便于 diff 审计
|
|
21
|
+
capacityStripped.push(name);
|
|
22
|
+
}
|
|
23
|
+
return entry;
|
|
24
|
+
});
|
|
25
|
+
return { hosts: { ...rawHosts, hosts }, approvedBackfilled, capacityStripped };
|
|
26
|
+
}
|
|
27
|
+
const catalogName = (modelId) => {
|
|
28
|
+
const n = modelId.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+/, "");
|
|
29
|
+
return /^[a-z0-9]/.test(n) ? n : `m-${n || "model"}`;
|
|
30
|
+
};
|
|
31
|
+
/** 规范容器内 env 名:MODEL_KEY_<NAME>(§3 apiKeyEnv 命名空间转换)。 */
|
|
32
|
+
export const canonicalModelKeyEnv = (name) => `MODEL_KEY_${name.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "MODEL"}`;
|
|
33
|
+
const signatureOf = (m, hostKeyEnv) => JSON.stringify({ provider: m.provider ?? "gateway", baseUrl: m.gatewayBaseUrl ?? "", key: hostKeyEnv, cost: m.cost ?? null });
|
|
34
|
+
/** 扫描存量 workers 的 model 七字段,产出 models 目录条目草稿 + 冲突报告 + cheapId→roster 计划。
|
|
35
|
+
* `existingModelNames` = 现 models 目录已占用的 name(避让后缀)。PURE——不写任何东西。
|
|
36
|
+
* 0.6.3:`opts.existingModels` 给出现目录条目时做内容级去重——worker.model 与既有条目 (id, provider,
|
|
37
|
+
* baseUrl) 相同 → 复用(进 `reuses`,不出新草稿;缺 apiKeyEnv 的条目给回填建议)。 */
|
|
38
|
+
export function planWorkerModelMigration(workers, existingModelNames = [], opts = {}) {
|
|
39
|
+
const drafts = [];
|
|
40
|
+
const conflicts = [];
|
|
41
|
+
const reuses = [];
|
|
42
|
+
const rosterActions = [];
|
|
43
|
+
const report = [];
|
|
44
|
+
const existing = opts.existingModels ?? [];
|
|
45
|
+
const usedNames = new Set([...existingModelNames, ...existing.map((e) => e.name)]);
|
|
46
|
+
// modelId → (signature → draft)
|
|
47
|
+
const byId = new Map();
|
|
48
|
+
// 复用聚合:entryName → reuse(同条目多 worker 归并;宿主名不一致=真冲突,进 conflicts)
|
|
49
|
+
const reuseByEntry = new Map();
|
|
50
|
+
const claimName = (base) => {
|
|
51
|
+
let name = base;
|
|
52
|
+
for (let i = 2; usedNames.has(name); i++)
|
|
53
|
+
name = `${base}-${i}`;
|
|
54
|
+
usedNames.add(name);
|
|
55
|
+
return name;
|
|
56
|
+
};
|
|
57
|
+
const upsert = (worker, m, hostKeyEnv, modelId, opts = {}) => {
|
|
58
|
+
// 0.6.4(W3/MINOR-1):cheap 条目身份与**主模型价格无关**(cheap 草稿 cost 恒 undefined,E1 裁定不照搬
|
|
59
|
+
// 主模型价)——cheap 签名剔除 cost,两 worker 同 cheapId 仅主模型价不同不再拆条目误报冲突(误报会连带
|
|
60
|
+
// 阻断整条 rosterAction,爆炸半径大于表面)。主模型签名仍含 cost(§3:cost 不同=拆条目,契约不变)。
|
|
61
|
+
const sig = signatureOf(opts.cheapOf ? { provider: m.provider, gatewayBaseUrl: m.gatewayBaseUrl } : m, hostKeyEnv);
|
|
62
|
+
const sigs = byId.get(modelId) ?? new Map();
|
|
63
|
+
byId.set(modelId, sigs);
|
|
64
|
+
let d = sigs.get(sig);
|
|
65
|
+
// cheap 挂靠:忽略 cost 维度找同 (provider, baseUrl, 宿主 key) 的既有草稿(如该 modelId 已被别的
|
|
66
|
+
// worker 作主模型合成过)——挂靠主形草稿定价真实,优于另造无价 cheap 条目。
|
|
67
|
+
if (!d && opts.cheapOf) {
|
|
68
|
+
d = [...sigs.values()].find((x) => x.entry.provider === (m.provider ?? "gateway") && (x.entry.baseUrl ?? "") === (m.gatewayBaseUrl ?? "") && x.hostApiKeyEnv === hostKeyEnv);
|
|
69
|
+
if (d) {
|
|
70
|
+
if (!d.sourceWorkers.includes(worker))
|
|
71
|
+
d.sourceWorkers.push(worker);
|
|
72
|
+
return d;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!d) {
|
|
76
|
+
const name = claimName(catalogName(modelId));
|
|
77
|
+
const provider = m.provider ?? "gateway";
|
|
78
|
+
d = {
|
|
79
|
+
entry: {
|
|
80
|
+
name,
|
|
81
|
+
id: modelId,
|
|
82
|
+
provider,
|
|
83
|
+
api: provider === "anthropic" ? "anthropic-messages" : "openai-completions",
|
|
84
|
+
baseUrl: m.gatewayBaseUrl || undefined,
|
|
85
|
+
apiKeyEnv: canonicalModelKeyEnv(name),
|
|
86
|
+
reasoning: opts.cheapOf ? undefined : m.reasoning || undefined,
|
|
87
|
+
maxTokens: opts.cheapOf ? undefined : m.maxTokens,
|
|
88
|
+
contextWindow: opts.cheapOf ? undefined : m.contextWindow,
|
|
89
|
+
// cheap 草稿不照搬主模型 cost:worker.model 只有主模型单一价,copy 给 cheap 条目=错误定价喂给
|
|
90
|
+
// prefer:cheapest/quota-weight 消费方;留空更安全(types.ts 口径 missing cost ≠ cheapest),
|
|
91
|
+
// E3 人工确认时补价(E1 评审 MINOR)。
|
|
92
|
+
cost: opts.cheapOf ? undefined : m.cost,
|
|
93
|
+
notes: opts.cheapOf ? `migrated from worker.model.cheapId (worker "${opts.cheapOf}")` : `migrated from worker.model (worker "${worker}")`,
|
|
94
|
+
},
|
|
95
|
+
sourceWorkers: [],
|
|
96
|
+
hostApiKeyEnv: hostKeyEnv,
|
|
97
|
+
};
|
|
98
|
+
sigs.set(sig, d);
|
|
99
|
+
drafts.push(d);
|
|
100
|
+
if (sigs.size > 1) {
|
|
101
|
+
const names = [...sigs.values()].map((x) => x.entry.name);
|
|
102
|
+
conflicts.push({ modelId, entryNames: names, reason: `same modelId "${modelId}" with differing gatewayBaseUrl/apiKeyEnv/cost/provider — split into ${names.length} entries (人工确认后再执行)` });
|
|
103
|
+
report.push(`⚠️ conflict: modelId "${modelId}" → entries [${names.join(", ")}] (signatures differ)`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!d.sourceWorkers.includes(worker))
|
|
107
|
+
d.sourceWorkers.push(worker);
|
|
108
|
+
return d;
|
|
109
|
+
};
|
|
110
|
+
/** 0.6.3 内容级去重:worker.model 与既有条目 (id, provider, baseUrl) 相同 → 复用。返回 undefined=无匹配走草稿。
|
|
111
|
+
* 0.6.4(W1/C1,PM 裁定):`compareFields`(仅主模型路径——cheapId 路径的 m 是主模型对象,四字段不属于
|
|
112
|
+
* cheap)时对 cost/maxTokens/contextWindow/reasoning 逐字段比对:worker.model 侧有值且与条目不一致 →
|
|
113
|
+
* **降级为冲突**(仍记 reuse 占位防重复草稿,但 apply 按 conflictedEntryNames 整条跳过进人工确认段;
|
|
114
|
+
* 不静默采用条目侧)。worker.model 侧缺值=退化存量,不算冲突(目录为前进方向真值)。 */
|
|
115
|
+
const tryReuse = (worker, m, hostKeyEnv, modelId, compareFields = false) => {
|
|
116
|
+
const e = existing.find((x) => x.id === modelId && (x.provider ?? "gateway") === (m.provider ?? "gateway") && (x.baseUrl ?? "") === (m.gatewayBaseUrl ?? ""));
|
|
117
|
+
if (!e)
|
|
118
|
+
return undefined;
|
|
119
|
+
if (compareFields) {
|
|
120
|
+
const diffs = [];
|
|
121
|
+
const fmt = (v) => (v === undefined ? "(unset)" : JSON.stringify(v));
|
|
122
|
+
if (m.cost !== undefined && JSON.stringify(m.cost) !== JSON.stringify(e.cost))
|
|
123
|
+
diffs.push(`cost ${fmt(m.cost)} ≠ ${fmt(e.cost)}`);
|
|
124
|
+
if (m.maxTokens !== undefined && m.maxTokens !== e.maxTokens)
|
|
125
|
+
diffs.push(`maxTokens ${fmt(m.maxTokens)} ≠ ${fmt(e.maxTokens)}`);
|
|
126
|
+
if (m.contextWindow !== undefined && m.contextWindow !== e.contextWindow)
|
|
127
|
+
diffs.push(`contextWindow ${fmt(m.contextWindow)} ≠ ${fmt(e.contextWindow)}`);
|
|
128
|
+
if (m.reasoning !== undefined && !!m.reasoning !== !!e.reasoning)
|
|
129
|
+
diffs.push(`reasoning ${fmt(!!m.reasoning)} ≠ ${fmt(!!e.reasoning)}`);
|
|
130
|
+
if (diffs.length) {
|
|
131
|
+
conflicts.push({ modelId, entryNames: [e.name], reason: `worker "${worker}" model 与既有条目 "${e.name}" 字段不一致(${diffs.join("; ")})— REUSE 降级为冲突,不静默采用,人工确认后再执行` });
|
|
132
|
+
report.push(`⚠️ conflict: worker "${worker}" model "${modelId}" vs catalog "${e.name}" — ${diffs.join("; ")}(REUSE 降级为冲突,人工确认)`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
let r = reuseByEntry.get(e.name);
|
|
136
|
+
if (!r) {
|
|
137
|
+
const backfill = e.apiKeyEnv ? undefined : canonicalModelKeyEnv(e.name);
|
|
138
|
+
r = { entryName: e.name, modelId, sourceWorkers: [], hostApiKeyEnv: hostKeyEnv, apiKeyEnvBackfill: backfill, containerApiKeyEnv: e.apiKeyEnv ?? backfill };
|
|
139
|
+
reuseByEntry.set(e.name, r);
|
|
140
|
+
reuses.push(r);
|
|
141
|
+
}
|
|
142
|
+
else if (r.hostApiKeyEnv !== hostKeyEnv) {
|
|
143
|
+
// 同一条目、不同宿主 key——接线单二义,人工裁决(不静默择一)。
|
|
144
|
+
conflicts.push({ modelId, entryNames: [e.name], reason: `existing entry "${e.name}" reused by workers with differing host apiKeyEnv ("${r.hostApiKeyEnv}" vs "${hostKeyEnv}") — wiring ambiguous, 人工确认后再执行` });
|
|
145
|
+
report.push(`⚠️ conflict: entry "${e.name}" host apiKeyEnv differs across workers ("${r.hostApiKeyEnv}" vs "${hostKeyEnv}" @ ${worker})`);
|
|
146
|
+
}
|
|
147
|
+
if (!r.sourceWorkers.includes(worker))
|
|
148
|
+
r.sourceWorkers.push(worker);
|
|
149
|
+
return r;
|
|
150
|
+
};
|
|
151
|
+
for (const w of workers) {
|
|
152
|
+
const m = w.model;
|
|
153
|
+
if (!m || !m.modelId)
|
|
154
|
+
continue;
|
|
155
|
+
const hostKeyEnv = w.secretRefs?.apiKeyEnv ?? "";
|
|
156
|
+
const mainReuse = tryReuse(w.name, m, hostKeyEnv, m.modelId, true);
|
|
157
|
+
const main = mainReuse ?? upsert(w.name, m, hostKeyEnv, m.modelId);
|
|
158
|
+
const mainName = mainReuse ? mainReuse.entryName : main.entry.name;
|
|
159
|
+
const mainKey = mainReuse ? mainReuse.containerApiKeyEnv : main.entry.apiKeyEnv;
|
|
160
|
+
report.push(mainReuse
|
|
161
|
+
? `worker "${w.name}": model "${m.modelId}" → REUSE existing catalog "${mainName}" (apiKeyEnv ${hostKeyEnv || "(unset)"} → ${mainKey}${mainReuse.apiKeyEnvBackfill ? ", 回填条目 apiKeyEnv" : ""})`
|
|
162
|
+
: `worker "${w.name}": model "${m.modelId}" → catalog "${mainName}" (apiKeyEnv ${hostKeyEnv || "(unset)"} → ${mainKey})`);
|
|
163
|
+
let cheapName;
|
|
164
|
+
if (m.cheapId) {
|
|
165
|
+
const cheapReuse = tryReuse(w.name, m, hostKeyEnv, m.cheapId);
|
|
166
|
+
if (cheapReuse) {
|
|
167
|
+
cheapName = cheapReuse.entryName;
|
|
168
|
+
report.push(`worker "${w.name}": cheapId "${m.cheapId}" → REUSE existing catalog "${cheapName}" + roster cheapModel 标记(0.6.3)`);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const cheap = upsert(w.name, m, hostKeyEnv, m.cheapId, { cheapOf: w.name });
|
|
172
|
+
cheapName = cheap.entry.name;
|
|
173
|
+
// 挂靠到主形草稿(定价真实)时不再误报"未定价";只有真·无价 cheap 草稿才提示人工补价。
|
|
174
|
+
report.push(cheap.entry.cost !== undefined
|
|
175
|
+
? `worker "${w.name}": cheapId "${m.cheapId}" → catalog "${cheapName}"(挂靠既有草稿)+ roster cheapModel 标记(0.6.3)`
|
|
176
|
+
: `worker "${w.name}": cheapId "${m.cheapId}" → catalog "${cheapName}" + roster cheapModel 标记(0.6.3)⚠️ cheap 条目未定价(源数据无 cheap 独立价,不沿用主模型 cost),E3 落库前请人工核对补价`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
rosterActions.push({ worker: w.name, mainModel: mainName, cheapModel: cheapName });
|
|
180
|
+
}
|
|
181
|
+
// 0.6.4(C2a):容器内 env 名全局碰撞检测——canonicalModelKeyEnv 把非字母数字折叠成 "_"(foo-bar/foo_bar/
|
|
182
|
+
// foo.bar 同名),不同条目分到同一容器 env 名且**宿主 key 不同**时接线互踩(后写者被静默跳过)→ 冲突进
|
|
183
|
+
// 人工段。宿主 key 相同的碰撞无歧义(同值幂等,两模型共享一把 key 属合法形态),放行。
|
|
184
|
+
{
|
|
185
|
+
const byEnv = new Map();
|
|
186
|
+
const claim = (env, u) => {
|
|
187
|
+
const arr = byEnv.get(env);
|
|
188
|
+
if (arr)
|
|
189
|
+
arr.push(u);
|
|
190
|
+
else
|
|
191
|
+
byEnv.set(env, [u]);
|
|
192
|
+
};
|
|
193
|
+
for (const d of drafts)
|
|
194
|
+
claim(d.entry.apiKeyEnv, { entryName: d.entry.name, modelId: d.entry.id, hostKey: d.hostApiKeyEnv });
|
|
195
|
+
for (const r of reuses)
|
|
196
|
+
claim(r.containerApiKeyEnv, { entryName: r.entryName, modelId: r.modelId, hostKey: r.hostApiKeyEnv });
|
|
197
|
+
for (const [env, users] of byEnv) {
|
|
198
|
+
const names = [...new Set(users.map((u) => u.entryName))];
|
|
199
|
+
const hostKeys = [...new Set(users.map((u) => u.hostKey))];
|
|
200
|
+
if (names.length > 1 && hostKeys.length > 1) {
|
|
201
|
+
conflicts.push({ modelId: [...new Set(users.map((u) => u.modelId))].join(" / "), entryNames: names, reason: `container apiKeyEnv "${env}" 被多个条目占用 [${names.join(", ")}] 且宿主 key 不一致 [${hostKeys.join(", ")}] — 接线互踩,人工改条目名/apiKeyEnv 后再执行` });
|
|
202
|
+
report.push(`⚠️ conflict: container env "${env}" collision across entries [${names.join(", ")}] with differing host keys [${hostKeys.join(", ")}]`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
report.push(`plan: ${drafts.length} catalog draft(s), ${reuses.length} reuse(s), ${conflicts.length} conflict(s), ${rosterActions.length} worker roster action(s) — REPORT ONLY, nothing written (E3 executes after 人工确认).`);
|
|
207
|
+
return { drafts, conflicts, reuses, rosterActions, report };
|
|
208
|
+
}
|
|
209
|
+
/** roster 的"生效主模型"(标记优先,退回显式 roles.default 的 {model} 形)——find-or-assign 的匹配键。 */
|
|
210
|
+
const rosterEffPrimary = (r) => r.primaryModel ?? (r.roles?.default?.model);
|
|
211
|
+
const rosterEffCheap = (r) => r.cheapModel ?? (r.roles?.subagent?.model);
|
|
212
|
+
/**
|
|
213
|
+
* 执行迁移计划(§3②③;**不删 worker.model**——真删=0.7):
|
|
214
|
+
* 1. models:追加非冲突草稿(同名已存在=幂等跳过);复用条目缺 apiKeyEnv 时回填规范名。
|
|
215
|
+
* 2. rosters:有 roster 的 worker → 该 roster 落 primaryModel/cheapModel 标记(已设且不同=skip 报冲突;
|
|
216
|
+
* 标记模型不在 roster.models 子集时补进子集);无 roster 的 worker → 指认「生效主/cheap 相同」的既有
|
|
217
|
+
* roster,否则按主模型名新建。
|
|
218
|
+
* 3. workers:secretEnv 接线 {容器内 apiKeyEnv ← 宿主 secretRefs.apiKeyEnv}(已接/宿主名缺失=跳过)+
|
|
219
|
+
* 无 roster 的 worker 写入指认结果。
|
|
220
|
+
* 幂等:对自身输出重跑 changes=[]。冲突 modelId 关联的 rosterAction/接线整条跳过(人工确认段)。
|
|
221
|
+
*/
|
|
222
|
+
export function applyModelMigrationPlan(plan, docs) {
|
|
223
|
+
const changes = [];
|
|
224
|
+
const skipped = [];
|
|
225
|
+
const models = { ...docs.models, models: docs.models.models.map((m) => ({ ...m })) };
|
|
226
|
+
const rosters = { ...docs.rosters, rosters: docs.rosters.rosters.map((r) => ({ ...r, models: r.models ? [...r.models] : r.models })) };
|
|
227
|
+
const workers = { ...docs.workers, workers: docs.workers.workers.map((w) => ({ ...w, secretEnv: w.secretEnv ? { ...w.secretEnv } : w.secretEnv })) };
|
|
228
|
+
const conflictedEntryNames = new Set(plan.conflicts.flatMap((c) => c.entryNames));
|
|
229
|
+
// 1a. 追加非冲突草稿
|
|
230
|
+
for (const d of plan.drafts) {
|
|
231
|
+
if (conflictedEntryNames.has(d.entry.name)) {
|
|
232
|
+
skipped.push(`draft "${d.entry.name}" (modelId "${d.entry.id}") — conflicted, 人工确认后再执行`);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (models.models.some((m) => m.name === d.entry.name))
|
|
236
|
+
continue; // 幂等
|
|
237
|
+
models.models.push({ ...d.entry });
|
|
238
|
+
changes.push(`models: +"${d.entry.name}" (id ${d.entry.id}, apiKeyEnv ${d.entry.apiKeyEnv})`);
|
|
239
|
+
}
|
|
240
|
+
// 1b. 复用条目 apiKeyEnv 回填
|
|
241
|
+
for (const r of plan.reuses) {
|
|
242
|
+
if (conflictedEntryNames.has(r.entryName)) {
|
|
243
|
+
skipped.push(`reuse "${r.entryName}" — conflicted(字段不一致 / host apiKeyEnv 二义 / env 碰撞), 人工确认后再执行`);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const e = models.models.find((m) => m.name === r.entryName);
|
|
247
|
+
if (!e) {
|
|
248
|
+
skipped.push(`reuse "${r.entryName}" — entry no longer in catalog`);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (r.apiKeyEnvBackfill && !e.apiKeyEnv) {
|
|
252
|
+
e.apiKeyEnv = r.apiKeyEnvBackfill;
|
|
253
|
+
changes.push(`models: "${r.entryName}" apiKeyEnv 回填 → ${r.apiKeyEnvBackfill}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const entryKey = (name) => {
|
|
257
|
+
const reuse = plan.reuses.find((x) => x.entryName === name);
|
|
258
|
+
if (reuse)
|
|
259
|
+
return reuse.containerApiKeyEnv;
|
|
260
|
+
const d = plan.drafts.find((x) => x.entry.name === name);
|
|
261
|
+
if (d)
|
|
262
|
+
return d.entry.apiKeyEnv;
|
|
263
|
+
return models.models.find((m) => m.name === name)?.apiKeyEnv;
|
|
264
|
+
};
|
|
265
|
+
const entryHostKey = (name) => plan.reuses.find((x) => x.entryName === name)?.hostApiKeyEnv ?? plan.drafts.find((x) => x.entry.name === name)?.hostApiKeyEnv;
|
|
266
|
+
const usedRosterNames = new Set(rosters.rosters.map((r) => r.name));
|
|
267
|
+
const claimRosterName = (base) => {
|
|
268
|
+
const b = base.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+/, "") || "roster";
|
|
269
|
+
let name = /^[a-z0-9]/.test(b) ? b : `r-${b}`;
|
|
270
|
+
for (let i = 2; usedRosterNames.has(name); i++)
|
|
271
|
+
name = `${b}-${i}`;
|
|
272
|
+
usedRosterNames.add(name);
|
|
273
|
+
return name;
|
|
274
|
+
};
|
|
275
|
+
/** 标记落到 roster;若 roster.models 是非空子集且缺该模型名,补进子集(否则 PUT 校验拒)。 */
|
|
276
|
+
const setMarker = (r, field, model) => {
|
|
277
|
+
if (r[field] === model)
|
|
278
|
+
return;
|
|
279
|
+
if (r[field] && r[field] !== model) {
|
|
280
|
+
skipped.push(`roster "${r.name}" ${field} already "${r[field]}" ≠ "${model}" — 不覆盖,人工裁决`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
r[field] = model;
|
|
284
|
+
if (r.models && r.models.length && !r.models.includes(model)) {
|
|
285
|
+
r.models.push(model);
|
|
286
|
+
changes.push(`rosters: "${r.name}" models += "${model}"(标记子集一致性)`);
|
|
287
|
+
}
|
|
288
|
+
changes.push(`rosters: "${r.name}" ${field} = "${model}"`);
|
|
289
|
+
};
|
|
290
|
+
// 2. roster 标记与指认:先处理已有 roster 的 worker(落标记),再给无 roster 的 worker 指认/新建
|
|
291
|
+
const actionable = plan.rosterActions.filter((a) => {
|
|
292
|
+
const bad = [a.mainModel, a.cheapModel].filter((n) => !!n).filter((n) => conflictedEntryNames.has(n));
|
|
293
|
+
if (bad.length) {
|
|
294
|
+
skipped.push(`worker "${a.worker}" roster action — 冲突条目 [${bad.join(", ")}], 人工确认后再执行`);
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
});
|
|
299
|
+
for (const a of actionable) {
|
|
300
|
+
const w = workers.workers.find((x) => x.name === a.worker);
|
|
301
|
+
if (!w?.roster)
|
|
302
|
+
continue;
|
|
303
|
+
const r = rosters.rosters.find((x) => x.name === w.roster);
|
|
304
|
+
if (!r) {
|
|
305
|
+
skipped.push(`worker "${a.worker}" → roster "${w.roster}" not found(dangling ref)`);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
setMarker(r, "primaryModel", a.mainModel);
|
|
309
|
+
if (a.cheapModel)
|
|
310
|
+
setMarker(r, "cheapModel", a.cheapModel);
|
|
311
|
+
}
|
|
312
|
+
for (const a of actionable) {
|
|
313
|
+
const w = workers.workers.find((x) => x.name === a.worker);
|
|
314
|
+
if (!w || w.roster)
|
|
315
|
+
continue;
|
|
316
|
+
let target = rosters.rosters.find((r) => rosterEffPrimary(r) === a.mainModel && (a.cheapModel ? rosterEffCheap(r) === a.cheapModel : rosterEffCheap(r) === undefined));
|
|
317
|
+
// 次选:主模型相同、cheap 不冲突(目标无 cheap 而 roster 有)也可指认——worker 原本无 cheapId,roster 的 cheap 只多不少
|
|
318
|
+
target ??= a.cheapModel ? undefined : rosters.rosters.find((r) => rosterEffPrimary(r) === a.mainModel);
|
|
319
|
+
if (!target) {
|
|
320
|
+
const name = claimRosterName(a.mainModel);
|
|
321
|
+
target = { name, models: a.cheapModel ? [a.mainModel, a.cheapModel] : [a.mainModel], primaryModel: a.mainModel, ...(a.cheapModel ? { cheapModel: a.cheapModel } : {}) };
|
|
322
|
+
rosters.rosters.push(target);
|
|
323
|
+
changes.push(`rosters: +"${name}" (primaryModel "${a.mainModel}"${a.cheapModel ? `, cheapModel "${a.cheapModel}"` : ""})`);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
setMarker(target, "primaryModel", a.mainModel);
|
|
327
|
+
if (a.cheapModel)
|
|
328
|
+
setMarker(target, "cheapModel", a.cheapModel);
|
|
329
|
+
}
|
|
330
|
+
w.roster = target.name;
|
|
331
|
+
changes.push(`workers: "${w.name}" roster = "${target.name}"`);
|
|
332
|
+
}
|
|
333
|
+
// 3. secretEnv 接线:容器内 apiKeyEnv ← 宿主名(不动 worker.model;specHash 变化=滚动重建的触发点)
|
|
334
|
+
for (const a of actionable) {
|
|
335
|
+
const w = workers.workers.find((x) => x.name === a.worker);
|
|
336
|
+
if (!w)
|
|
337
|
+
continue;
|
|
338
|
+
for (const entryName of [a.mainModel, a.cheapModel].filter((n) => !!n)) {
|
|
339
|
+
const container = entryKey(entryName);
|
|
340
|
+
const host = entryHostKey(entryName);
|
|
341
|
+
if (!container) {
|
|
342
|
+
skipped.push(`worker "${a.worker}" wiring "${entryName}" — 条目无 apiKeyEnv,跳过`);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (!host) {
|
|
346
|
+
skipped.push(`worker "${a.worker}" wiring "${entryName}" — 宿主 apiKeyEnv 缺失(secretRefs.apiKeyEnv 未设),跳过`);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (RESERVED_WORKER_ENV.has(container)) {
|
|
350
|
+
skipped.push(`worker "${a.worker}" wiring "${entryName}" — 容器名 "${container}" 是保留键(schema 拒 secretEnv 设置),人工改条目 apiKeyEnv`);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const cur = w.secretEnv?.[container];
|
|
354
|
+
if (cur === host)
|
|
355
|
+
continue; // 已接同值(幂等)
|
|
356
|
+
// 0.6.4(C2b):现值≠期望宿主名 → 不再静默接受过时/错误映射,进人工确认段(也不覆盖——覆盖可能打断在用接线)。
|
|
357
|
+
if (cur !== undefined) {
|
|
358
|
+
skipped.push(`worker "${a.worker}" wiring "${entryName}" — secretEnv["${container}"] 现值 "${cur}" ≠ 期望宿主名 "${host}",不覆盖,人工确认`);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
w.secretEnv = { ...(w.secretEnv ?? {}), [container]: host };
|
|
362
|
+
changes.push(`workers: "${w.name}" secretEnv["${container}"] = "${host}"(宿主名)`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return { models, rosters, workers, changes, skipped };
|
|
366
|
+
}
|
|
367
|
+
// ── 4. 迁移 diff 白名单门(0.6.4, E3 双评审 C3)—— PURE:apply 前后三域对比,只放行迁移的预期改动 ──────
|
|
368
|
+
const sameJson = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
369
|
+
/**
|
|
370
|
+
* 迁移执行的**行为等价门**(C3,与 refIntegrityIssues 双门同点位):对 {@link applyModelMigrationPlan}
|
|
371
|
+
* 的前后三域做白名单式结构 diff——只允许迁移语义内的预期变更,任何意外漂移返回违规行(调用方 fail-closed
|
|
372
|
+
* 400,不落库)。白名单:
|
|
373
|
+
* - models:只许**新增**条目、既有条目只许 apiKeyEnv 从缺 → 有(回填);不许删、不许改其它字段。
|
|
374
|
+
* - rosters:只许新增 roster(且只带 name/models/primaryModel/cheapModel 四键);既有 roster 只许
|
|
375
|
+
* primaryModel/cheapModel 从缺 → 有、models 子集**追加**(且追加项必须是该 roster 的主/cheap 标记)。
|
|
376
|
+
* - workers:不许增删;每个 worker 只许 roster 从缺 → 有(指认)、secretEnv **添键且值严格匹配计划内
|
|
377
|
+
* (容器名 → 宿主名) 接线映射**(既有键不许动);其余键(含 model——真删=0.7)逐键必须不变。
|
|
378
|
+
* 空数组 = 通过。PURE,无 I/O。
|
|
379
|
+
*/
|
|
380
|
+
export function migrationDiffViolations(plan, before, after) {
|
|
381
|
+
const v = [];
|
|
382
|
+
// 计划内接线映射:容器 env 名 → 允许的宿主名集合(drafts + reuses)。新增 secretEnv 键必须命中。
|
|
383
|
+
const expectedWiring = new Map();
|
|
384
|
+
const allow = (container, host) => {
|
|
385
|
+
const s = expectedWiring.get(container);
|
|
386
|
+
if (s)
|
|
387
|
+
s.add(host);
|
|
388
|
+
else
|
|
389
|
+
expectedWiring.set(container, new Set([host]));
|
|
390
|
+
};
|
|
391
|
+
for (const d of plan.drafts)
|
|
392
|
+
allow(d.entry.apiKeyEnv, d.hostApiKeyEnv);
|
|
393
|
+
for (const r of plan.reuses)
|
|
394
|
+
allow(r.containerApiKeyEnv, r.hostApiKeyEnv);
|
|
395
|
+
// models
|
|
396
|
+
{
|
|
397
|
+
const afterBy = new Map(after.models.models.map((m) => [m.name, m]));
|
|
398
|
+
for (const b of before.models.models) {
|
|
399
|
+
const a = afterBy.get(b.name);
|
|
400
|
+
if (!a) {
|
|
401
|
+
v.push(`models: 条目 "${b.name}" 被删除(迁移不许删)`);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
for (const k of new Set([...Object.keys(b), ...Object.keys(a)])) {
|
|
405
|
+
if (k === "apiKeyEnv" && b.apiKeyEnv === undefined && typeof a.apiKeyEnv === "string")
|
|
406
|
+
continue; // 回填
|
|
407
|
+
if (!sameJson(b[k], a[k]))
|
|
408
|
+
v.push(`models: "${b.name}".${k} 意外变更(${JSON.stringify(b[k])} → ${JSON.stringify(a[k])})`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// rosters
|
|
413
|
+
{
|
|
414
|
+
const beforeBy = new Map(before.rosters.rosters.map((r) => [r.name, r]));
|
|
415
|
+
const afterNames = new Set(after.rosters.rosters.map((r) => r.name));
|
|
416
|
+
for (const b of before.rosters.rosters)
|
|
417
|
+
if (!afterNames.has(b.name))
|
|
418
|
+
v.push(`rosters: "${b.name}" 被删除(迁移不许删)`);
|
|
419
|
+
for (const a of after.rosters.rosters) {
|
|
420
|
+
const b = beforeBy.get(a.name);
|
|
421
|
+
if (!b) {
|
|
422
|
+
const extra = Object.keys(a).filter((k) => !["name", "models", "primaryModel", "cheapModel"].includes(k));
|
|
423
|
+
if (extra.length)
|
|
424
|
+
v.push(`rosters: 新建 "${a.name}" 带白名单外键 [${extra.join(", ")}]`);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
for (const k of new Set([...Object.keys(b), ...Object.keys(a)])) {
|
|
428
|
+
if ((k === "primaryModel" || k === "cheapModel") && b[k] === undefined && typeof a[k] === "string")
|
|
429
|
+
continue; // 落标记
|
|
430
|
+
if (k === "models" && Array.isArray(b.models) && Array.isArray(a.models)) {
|
|
431
|
+
const added = a.models.filter((m) => !b.models.includes(m));
|
|
432
|
+
const removed = b.models.filter((m) => !a.models.includes(m));
|
|
433
|
+
if (removed.length)
|
|
434
|
+
v.push(`rosters: "${a.name}".models 移除了 [${removed.join(", ")}](迁移只许追加)`);
|
|
435
|
+
for (const m of added)
|
|
436
|
+
if (m !== a.primaryModel && m !== a.cheapModel)
|
|
437
|
+
v.push(`rosters: "${a.name}".models 追加了非主/cheap 标记项 "${m}"`);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (!sameJson(b[k], a[k]))
|
|
441
|
+
v.push(`rosters: "${a.name}".${k} 意外变更(${JSON.stringify(b[k])} → ${JSON.stringify(a[k])})`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// workers
|
|
446
|
+
{
|
|
447
|
+
const afterBy = new Map(after.workers.workers.map((w) => [w.name, w]));
|
|
448
|
+
for (const a of after.workers.workers)
|
|
449
|
+
if (!before.workers.workers.some((b) => b.name === a.name))
|
|
450
|
+
v.push(`workers: 出现新增 worker "${a.name}"(迁移不建 worker)`);
|
|
451
|
+
for (const b of before.workers.workers) {
|
|
452
|
+
const a = afterBy.get(b.name);
|
|
453
|
+
if (!a) {
|
|
454
|
+
v.push(`workers: "${b.name}" 被删除(迁移不许删)`);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
for (const k of new Set([...Object.keys(b), ...Object.keys(a)])) {
|
|
458
|
+
if (k === "roster" && b.roster === undefined && typeof a.roster === "string") {
|
|
459
|
+
if (!after.rosters.rosters.some((r) => r.name === a.roster))
|
|
460
|
+
v.push(`workers: "${b.name}".roster 指认了不存在的 roster "${a.roster}"`);
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (k === "secretEnv") {
|
|
464
|
+
const bs = b.secretEnv ?? {};
|
|
465
|
+
const as = a.secretEnv ?? {};
|
|
466
|
+
for (const key of Object.keys(bs))
|
|
467
|
+
if (as[key] !== bs[key])
|
|
468
|
+
v.push(`workers: "${b.name}".secretEnv["${key}"] 被改/删(${JSON.stringify(bs[key])} → ${JSON.stringify(as[key])};迁移只许添键)`);
|
|
469
|
+
for (const key of Object.keys(as)) {
|
|
470
|
+
if (key in bs)
|
|
471
|
+
continue;
|
|
472
|
+
if (!expectedWiring.get(key)?.has(as[key]))
|
|
473
|
+
v.push(`workers: "${b.name}".secretEnv["${key}"] = ${JSON.stringify(as[key])} 不在计划内接线映射(计划外漂移)`);
|
|
474
|
+
}
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (!sameJson(b[k], a[k]))
|
|
478
|
+
v.push(`workers: "${b.name}".${k} 意外变更(${JSON.stringify(b[k])} → ${JSON.stringify(a[k])})`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return v;
|
|
483
|
+
}
|
|
484
|
+
/** 迁移合成模板的默认安全闸(拍板:多模型烧钱必须有闸——老 TeamDef 没有 budget 概念,迁移必须给一个;
|
|
485
|
+
* 调用方/admin 可再调)。 */
|
|
486
|
+
export const DEFAULT_COLLAB_BUDGET_TOKENS = 1_000_000;
|
|
487
|
+
/**
|
|
488
|
+
* teams → collab 存量迁移(EXPERT-REDESIGN §8-5):每个 TeamDef → star 模板。
|
|
489
|
+
* - members → "reviewer" 槽(cardinality "N")members[]:member.model(目录 name)→ modelRef;
|
|
490
|
+
* member.systemPrompt(+role 前缀)→ 成员级 rolePrompt。
|
|
491
|
+
* - `modelRole` 机制不入新形状(T1:只存目录引用或空)——带 modelRole 无 model 的成员迁为 modelRef 空
|
|
492
|
+
* (=跟随用户当前模型),丢弃记录进 report。
|
|
493
|
+
* - synthesizer → 独立 "synthesizer" 槽 + finalizer {type:"summarize", slot};无 synthesizer →
|
|
494
|
+
* finalizer {type:"summarize"}(runTeamDiscussion 的缺省综合行为)。
|
|
495
|
+
* - rounds 保留为 rounds.max;scenario → 迁移报告(工具展开需 scenario 定义,人工在槽位 tools 落地);
|
|
496
|
+
* maxTranscriptTokens/无 budget → budget.maxTokens 兜底 {@link DEFAULT_COLLAB_BUDGET_TOKENS}。
|
|
497
|
+
* 幂等:纯函数,同输入同输出;name 冲突由调用方 parseDomain 的 id 去重校验兜底(team name 本就唯一)。
|
|
498
|
+
*/
|
|
499
|
+
export function migrateTeamsToCollab(rawTeams) {
|
|
500
|
+
const report = [];
|
|
501
|
+
const teams = (rawTeams !== null && typeof rawTeams === "object" && Array.isArray(rawTeams.teams))
|
|
502
|
+
? (rawTeams.teams)
|
|
503
|
+
: [];
|
|
504
|
+
const templates = teams.map((t) => {
|
|
505
|
+
const members = (t.members ?? []).map((m) => {
|
|
506
|
+
if (!m.model && m.modelRole)
|
|
507
|
+
report.push(`team "${t.name}" member "${m.role}": modelRole "${m.modelRole}" 不入新形状(T1)→ modelRef 空=跟随用户当前模型`);
|
|
508
|
+
return {
|
|
509
|
+
...(m.model ? { modelRef: m.model } : {}),
|
|
510
|
+
...(m.systemPrompt ? { rolePrompt: `[${m.role}] ${m.systemPrompt}`.slice(0, 2000) } : { rolePrompt: `[${m.role}]` }),
|
|
511
|
+
};
|
|
512
|
+
});
|
|
513
|
+
const slots = [{ id: "reviewer", cardinality: "N", members }];
|
|
514
|
+
let finalizer = { type: "summarize" };
|
|
515
|
+
if (t.synthesizer) {
|
|
516
|
+
const s = t.synthesizer;
|
|
517
|
+
if (!s.model && s.modelRole)
|
|
518
|
+
report.push(`team "${t.name}" synthesizer: modelRole "${s.modelRole}" 不入新形状(T1)→ modelRef 空`);
|
|
519
|
+
slots.push({ id: "synthesizer", cardinality: 1, members: [{ ...(s.model ? { modelRef: s.model } : {}), ...(s.systemPrompt ? { rolePrompt: s.systemPrompt.slice(0, 2000) } : {}) }] });
|
|
520
|
+
finalizer = { type: "summarize", slot: "synthesizer" };
|
|
521
|
+
}
|
|
522
|
+
if (t.scenario)
|
|
523
|
+
report.push(`team "${t.name}": scenario "${t.scenario}" → 槽位 tools 需人工展开(工具集定义在 scenarios 域,迁移不解析)`);
|
|
524
|
+
report.push(`team "${t.name}" → collab star 模板 "${t.name}"(${members.length} member(s), rounds.max=${t.rounds ?? 2}, budget.maxTokens=${t.maxTranscriptTokens ?? DEFAULT_COLLAB_BUDGET_TOKENS})`);
|
|
525
|
+
return {
|
|
526
|
+
id: t.name,
|
|
527
|
+
name: t.name,
|
|
528
|
+
description: t.notes,
|
|
529
|
+
topology: "star",
|
|
530
|
+
slots,
|
|
531
|
+
rounds: { max: t.rounds ?? 2 },
|
|
532
|
+
finalizer,
|
|
533
|
+
sharedContext: { kind: "transcript", persistence: "session" },
|
|
534
|
+
budget: { maxTokens: t.maxTranscriptTokens ?? DEFAULT_COLLAB_BUDGET_TOKENS },
|
|
535
|
+
enabled: t.enabled ?? true,
|
|
536
|
+
...(t.notes ? { notes: t.notes } : {}),
|
|
537
|
+
};
|
|
538
|
+
});
|
|
539
|
+
if (!teams.length)
|
|
540
|
+
report.push("no legacy teams — nothing to migrate.");
|
|
541
|
+
return { collab: { templates }, report };
|
|
542
|
+
}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sema-agent/settings-schema/node — the Node-only entry. These helpers read/write `config.d/<domain>.json`
|
|
3
|
+
* over the real filesystem (`node:fs`/`node:path`), so they are split out of the main barrel: a browser or
|
|
4
|
+
* client bundle that only needs the pure contract (zod types, hash, roster resolution, ref-integrity) must
|
|
5
|
+
* not transitively pull `node:fs`. The TOC desktop/CLI + any server-side file source imports them here.
|
|
6
|
+
*/
|
|
7
|
+
export * from "./file-store.js";
|
|
8
|
+
export * from "./file-edit.js";
|
|
9
|
+
export * from "./bundle.js";
|
|
10
|
+
export * from "./local-load.js";
|
|
11
|
+
export * from "./scheduler-store-node.js";
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sema-agent/settings-schema/node — the Node-only entry. These helpers read/write `config.d/<domain>.json`
|
|
3
|
+
* over the real filesystem (`node:fs`/`node:path`), so they are split out of the main barrel: a browser or
|
|
4
|
+
* client bundle that only needs the pure contract (zod types, hash, roster resolution, ref-integrity) must
|
|
5
|
+
* not transitively pull `node:fs`. The TOC desktop/CLI + any server-side file source imports them here.
|
|
6
|
+
*/
|
|
7
|
+
export * from "./file-store.js";
|
|
8
|
+
export * from "./file-edit.js";
|
|
9
|
+
export * from "./bundle.js";
|
|
10
|
+
export * from "./local-load.js";
|
|
11
|
+
export * from "./scheduler-store-node.js"; // R7 self-wake scheduler store — fs binding of the ./scheduler-store contract
|
package/dist/reader.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DomainConfig, DomainName, EffectiveConfig } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The MINIMAL read surface a consumer needs to resolve config — a local file source (FileConfigStore), the
|
|
4
|
+
* TOC desktop, or a test fixture implements this WITHOUT stubbing the ~32 publish/RBAC/worker-status/lifecycle
|
|
5
|
+
* methods of sema-registry's full ConfigStore.
|
|
6
|
+
*
|
|
7
|
+
* sema-registry's fat `ConfigStore` should `extends ConfigReader` (every store impl already has all three
|
|
8
|
+
* methods, so the extension is satisfied automatically). The signatures here MUST match store.ts exactly
|
|
9
|
+
* (incl. the `<K extends DomainName>` generic and the `| undefined` on getDomain) or that `extends` fails.
|
|
10
|
+
*/
|
|
11
|
+
export interface ConfigReader {
|
|
12
|
+
/** The stored payload for a domain, validated; `undefined` if never set. */
|
|
13
|
+
getDomain<K extends DomainName>(domain: K): Promise<DomainConfig[K] | undefined>;
|
|
14
|
+
/** All domains merged (defaults for unset) + a monotonic version — what a consumer pulls. */
|
|
15
|
+
getEffective(): Promise<EffectiveConfig>;
|
|
16
|
+
/** The current monotonic version ALONE — a cheap read so a conditional poll can short-circuit. */
|
|
17
|
+
getVersion(): Promise<number>;
|
|
18
|
+
}
|
package/dist/reader.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|