@sema-agent/settings-schema 3.0.1 → 4.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 +303 -0
- package/README.md +6 -2
- package/conformance/VERSION +1 -1
- package/dist/api/auth.d.ts +21 -0
- package/dist/api/auth.js +21 -0
- package/dist/config-fns.d.ts +72 -7
- package/dist/config-fns.js +76 -21
- package/dist/conformance/contract-adapter.js +37 -0
- package/dist/conformance/entry-origin.d.ts +24 -0
- package/dist/conformance/entry-origin.js +86 -0
- package/dist/conformance/run.js +6 -6
- package/dist/conformance/types.d.ts +5 -1
- package/dist/conformance/warnings.d.ts +7 -48
- package/dist/conformance/warnings.js +17 -40
- package/dist/cross-domain.d.ts +26 -6
- package/dist/cross-domain.js +30 -10
- package/dist/node.d.ts +1 -0
- package/dist/node.js +1 -0
- package/dist/prompts-digest.d.ts +16 -0
- package/dist/prompts-digest.js +48 -0
- package/dist/scheduler-store-node.d.ts +7 -19
- package/dist/scheduler-store-node.js +8 -26
- package/package.json +3 -1
package/dist/config-fns.js
CHANGED
|
@@ -207,7 +207,7 @@ function judgeEntry(schema, value) {
|
|
|
207
207
|
* 一条粘错位置的凭据,所以路径里的键段过 {@link redactSecrets} 才出门(不变量①:secret 值永不随错误外流,
|
|
208
208
|
* 与 parseDomain 的 R13/R14 同一条);② 整段遍历包 try/catch —— 读字段、枚举 Record 都会触发访问器,
|
|
209
209
|
* 抛出来的文案可能带 secret。抢救是**尽力而为**的:抢不动就当没抢救(返回原引用),让既有的整域档位去裁,
|
|
210
|
-
* 绝不把解析拖下水、更不回显它的错误(与同文件
|
|
210
|
+
* 绝不把解析拖下水、更不回显它的错误(与同文件 {@link droppedKeys} 逐字同纪律)。
|
|
211
211
|
*
|
|
212
212
|
* 边界(如实登记):① 只看**顶层**字段(条目集合就在顶层,嵌套的坏形属于条目自身的问题);② raw 不是
|
|
213
213
|
* 纯 JSON 对象、字段值与 schema 形状对不上(schema 说数组、文档给对象)⇒ 原样返回,交给整域档位裁;
|
|
@@ -321,6 +321,58 @@ export function legacyGovernanceFromRuntime(rawRuntime) {
|
|
|
321
321
|
lifted[k] = r[k];
|
|
322
322
|
return Object.keys(lifted).length ? lifted : undefined;
|
|
323
323
|
}
|
|
324
|
+
/** {@link EffectiveReadWarning} 的 `kind` **闭集**(运行期值)。一致性 kit 的覆盖律按它计数,消费方要遍历
|
|
325
|
+
* 「所有告警种类」也用它 —— 属主在告警联合自己的模块里,`./conformance` 只再导出(4.0.0 前它住在 kit 里,
|
|
326
|
+
* 那边是联合的下游,却握着联合的闭集)。
|
|
327
|
+
*
|
|
328
|
+
* **这张表由谁咬住**(实测,2026-09-22,别照 `satisfies` 的样子推):`satisfies` 只咬「多一个联合里**没有**
|
|
329
|
+
* 的词」;**少登记一个** kind 它咬不住(少一员的数组照样可赋值)。真正咬「新 kind 必须被处置」的是三处
|
|
330
|
+
* `never` 穷举臂 —— {@link projectWarning}、{@link rebaseWarningPaths}、kit 的 `normalizeProjection`
|
|
331
|
+
* (给联合加一条新臂,实测这三处各红一条,`satisfies` 那行零诊断);而「新 kind 三处都处置了、却忘了登记
|
|
332
|
+
* 进本表」这半条缝,由 `test/warning-projection-derived-4.0.0.test.ts` 的闭集格看着(本表 == 从 AST 解出的
|
|
333
|
+
* 臂集,逐字同集同序)。 */
|
|
334
|
+
export const WARNING_KINDS = [
|
|
335
|
+
"hosts-grandfathered",
|
|
336
|
+
"domain-defaulted",
|
|
337
|
+
"unknown-keys-dropped",
|
|
338
|
+
"unknown-keys-carried",
|
|
339
|
+
"legacy-governance-lifted",
|
|
340
|
+
"entry-dropped",
|
|
341
|
+
"unread-config-file",
|
|
342
|
+
];
|
|
343
|
+
/**
|
|
344
|
+
* 一条告警 → 它的投影。**闭集穷举**:新 kind 不在这里表态就编译红(#157 的闭集纪律);而字段级的对齐由
|
|
345
|
+
* {@link WarningProjection} 的派生保证 —— 少铸一个字段,这里就是编译红。
|
|
346
|
+
*
|
|
347
|
+
* `truncatedFrom` 缺席时**不铸这个键**(不是铸 `undefined`)—— 规范化会把 `undefined` 值剔掉,两种写法
|
|
348
|
+
* 最终同形,但不铸更诚实:「没有截断」与「截断了 0 条」不该在中间形上分成两件事。
|
|
349
|
+
*/
|
|
350
|
+
export function projectWarning(w) {
|
|
351
|
+
switch (w.kind) {
|
|
352
|
+
case "hosts-grandfathered":
|
|
353
|
+
return { kind: w.kind, domain: w.domain, droppedNames: w.droppedNames, ...(w.truncatedFrom === undefined ? {} : { truncatedFrom: w.truncatedFrom }) };
|
|
354
|
+
case "domain-defaulted":
|
|
355
|
+
return { kind: w.kind, domain: w.domain };
|
|
356
|
+
// 两臂字段相同但**不合并**:合并后 `kind` 塌成两员的联合,TS 就再也认不出它属于投影联合的哪一员
|
|
357
|
+
// (真编译红过一次)。逐 kind 一臂,是这张表本来的样子。
|
|
358
|
+
case "unknown-keys-dropped":
|
|
359
|
+
return { kind: w.kind, domain: w.domain, keys: w.keys, ...(w.truncatedFrom === undefined ? {} : { truncatedFrom: w.truncatedFrom }) };
|
|
360
|
+
case "unknown-keys-carried":
|
|
361
|
+
return { kind: w.kind, domain: w.domain, keys: w.keys, ...(w.truncatedFrom === undefined ? {} : { truncatedFrom: w.truncatedFrom }) };
|
|
362
|
+
case "legacy-governance-lifted":
|
|
363
|
+
return { kind: w.kind, domain: w.domain, keys: w.keys };
|
|
364
|
+
case "entry-dropped":
|
|
365
|
+
return { kind: w.kind, domain: w.domain, path: w.path };
|
|
366
|
+
case "unread-config-file":
|
|
367
|
+
return { kind: w.kind, file: w.file, ...(w.domain === undefined ? {} : { domain: w.domain }), why: w.why };
|
|
368
|
+
default: {
|
|
369
|
+
const unhandled = w;
|
|
370
|
+
// 运行期到不了(本联合由本模块铸造);到了就让它红 —— 一条不认识的告警被静默投影成空,
|
|
371
|
+
// 正是「门看起来全绿」的那种假绿。
|
|
372
|
+
throw new Error(`settings-schema: unknown EffectiveReadWarning kind — ${JSON.stringify(unhandled)}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
324
376
|
/**
|
|
325
377
|
* 一条告警里的**段坐标**({@link ConfigPath})从「抢救压缩后的坐标」回写成「原文档的坐标」—— 纯函数,
|
|
326
378
|
* {@link EntryKeptIndex} 是它唯一的坐标账。
|
|
@@ -330,7 +382,7 @@ export function legacyGovernanceFromRuntime(rawRuntime) {
|
|
|
330
382
|
* `models.1` 指的是压缩后的第 1 条。运维照着这条告警去改原文档的第 1 条 —— 改的是别人的行,而且改完
|
|
331
383
|
* 告警还在(他以为改对了)。**告警必须说运维看得见的那份文档里的坐标**。
|
|
332
384
|
*
|
|
333
|
-
* 为什么回写在这里、而不是让扫描函数认识原文档:给
|
|
385
|
+
* 为什么回写在这里、而不是让扫描函数认识原文档:给 {@link droppedKeys} / {@link carriedUnknownKeys} 加一个「原始 raw」
|
|
334
386
|
* 第二入参,等于让扫描同时持有两套坐标,每条新告警都要自己决定用哪套。回写是**告警出门前**的一次
|
|
335
387
|
* 坐标换算,与诊断本身正交 —— 一个铸点,所有臂一次表态。
|
|
336
388
|
*
|
|
@@ -481,16 +533,6 @@ function collectDropped(domain, raw, parsed, prefix, depth, spared, acc) {
|
|
|
481
533
|
collectDropped(domain, raw[k], parsed[k], [...prefix, k], depth + 1, spared, acc);
|
|
482
534
|
}
|
|
483
535
|
}
|
|
484
|
-
function scanDropped(domain, raw, parsed) {
|
|
485
|
-
const acc = { keys: [], total: 0 };
|
|
486
|
-
try {
|
|
487
|
-
collectDropped(domain, raw, parsed, [], 0, domain === "runtime" ? GOVERNANCE_MIRROR_KEYS : [], acc);
|
|
488
|
-
}
|
|
489
|
-
catch {
|
|
490
|
-
return { keys: [], total: 0 }; // 反射/访问器抛错:告警是尽力而为的审计线,绝不能把解析拖下水,更不能回显其错误文案
|
|
491
|
-
}
|
|
492
|
-
return acc;
|
|
493
|
-
}
|
|
494
536
|
/**
|
|
495
537
|
* 一份域文档里被域 schema **丢掉的键**(zod 对象默认 `"strip"`),段坐标形({@link ConfigPath})。
|
|
496
538
|
*
|
|
@@ -506,13 +548,25 @@ function scanDropped(domain, raw, parsed) {
|
|
|
506
548
|
* `runtime` 域的治理三件({@link GOVERNANCE_MIRROR_KEYS})**不算被丢**:0.5 期 runtime 文档携带它们是
|
|
507
549
|
* 成文的抬升位(本文件的 governance 抬升函数会把它们抬进 governance 域),报出来是噪声。豁免只在顶层。
|
|
508
550
|
*
|
|
551
|
+
* 返回 {@link UnknownKeyScan}:`keys` 是**有界**的点名列表,`total` 是截断前的真实总数(4.0.0 BREAKING,
|
|
552
|
+
* 此前只回 `keys` —— 见 {@link UnknownKeyScan} 的成案)。本包自己的告警(`unknown-keys-dropped`)也是拿这两个
|
|
553
|
+
* 数拼出来的,**一个铸点**:消费方自铸的 `truncatedFrom` 与告警里那个恒等。
|
|
554
|
+
*
|
|
509
555
|
* 边界(如实登记):① 深度上限 {@link DROP_SCAN_MAX_DEPTH}、键数上限 {@link DROP_SCAN_MAX_KEYS}(告警
|
|
510
|
-
*
|
|
511
|
-
* JSON 对象——raw 若是类实例/Proxy 这类形状不扫;④ 读 raw 的值可能触发访问器,整段包
|
|
512
|
-
*
|
|
556
|
+
* 载荷有界,超出即少报**点名**——`total` 不少报);② 数组长度不同就整段跳过(元素级裁剪由各自的 clamp 告警
|
|
557
|
+
* 负责);③ 只比对纯 JSON 对象——raw 若是类实例/Proxy 这类形状不扫;④ 读 raw 的值可能触发访问器,整段包
|
|
558
|
+
* try/catch 且**绝不**让其错误外泄(错误文案可能带 secret,与 parseDomain 的 R13 同纪律),扫描失败即当作
|
|
559
|
+
* "没看见"(`{ keys: [], total: 0 }`),不影响解析。
|
|
513
560
|
*/
|
|
514
561
|
export function droppedKeys(domain, raw, parsed) {
|
|
515
|
-
|
|
562
|
+
const acc = { keys: [], total: 0 };
|
|
563
|
+
try {
|
|
564
|
+
collectDropped(domain, raw, parsed, [], 0, domain === "runtime" ? GOVERNANCE_MIRROR_KEYS : [], acc);
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
return { keys: [], total: 0 }; // 反射/访问器抛错:告警是尽力而为的审计线,绝不能把解析拖下水,更不能回显其错误文案
|
|
568
|
+
}
|
|
569
|
+
return acc;
|
|
516
570
|
}
|
|
517
571
|
/**
|
|
518
572
|
* open-world 域(`.passthrough()`)的**已知键表**:段坐标 → 该层已知键(`[]` = 顶层)。键集从 schema 的
|
|
@@ -535,13 +589,14 @@ const OPEN_WORLD_KNOWN_KEYS = {
|
|
|
535
589
|
* 一份已解析的 open-world 域文档里,**本包不认识但已承运**的键(段坐标)。非 open-world 域恒空
|
|
536
590
|
* (它们的未知键走 {@link droppedKeys} 那条更重的「被丢了」告警)。
|
|
537
591
|
*
|
|
592
|
+
* 返回 {@link UnknownKeyScan}(4.0.0 BREAKING,与 {@link droppedKeys} 同形同律:`keys` 有界、`total` 是
|
|
593
|
+
* 截断前的真实总数)。非 open-world 域返回的也是这个形(`{ keys: [], total: 0 }`),不是别的东西 ——
|
|
594
|
+
* 「这个域没有承运未知键」与「这个域不参与承运」在消费方那里是同一个处置。
|
|
595
|
+
*
|
|
538
596
|
* 边界(如实):只看 {@link OPEN_WORLD_KNOWN_KEYS} 登记了的层级 —— 未登记层级(如某个未知键自己的
|
|
539
597
|
* 子树)不再下探,那本就是消费方自定义的内容,逐层猜没有意义。
|
|
540
598
|
*/
|
|
541
599
|
export function carriedUnknownKeys(domain, parsed) {
|
|
542
|
-
return scanCarried(domain, parsed).keys;
|
|
543
|
-
}
|
|
544
|
-
function scanCarried(domain, parsed) {
|
|
545
600
|
const table = OPEN_WORLD_KNOWN_KEYS[domain];
|
|
546
601
|
const acc = { keys: [], total: 0 };
|
|
547
602
|
if (table === undefined)
|
|
@@ -571,10 +626,10 @@ function scanCarried(domain, parsed) {
|
|
|
571
626
|
export function parseDomainLoud(domain, raw, onWarning) {
|
|
572
627
|
const parsed = parseDomain(domain, raw);
|
|
573
628
|
if (onWarning) {
|
|
574
|
-
const dropped =
|
|
629
|
+
const dropped = droppedKeys(domain, raw, parsed);
|
|
575
630
|
if (dropped.keys.length > 0)
|
|
576
631
|
onWarning({ domain, kind: "unknown-keys-dropped", keys: dropped.keys, ...(dropped.total > dropped.keys.length ? { truncatedFrom: dropped.total } : {}) });
|
|
577
|
-
const carried =
|
|
632
|
+
const carried = carriedUnknownKeys(domain, parsed);
|
|
578
633
|
if (carried.keys.length > 0)
|
|
579
634
|
onWarning({ domain, kind: "unknown-keys-carried", keys: carried.keys, ...(carried.total > carried.keys.length ? { truncatedFrom: carried.total } : {}) });
|
|
580
635
|
}
|
|
@@ -6,11 +6,18 @@
|
|
|
6
6
|
* `buildEffective` / `readEffectiveWire` / `loadLocalConfig` **就是**它的生产入口);②historical 模式要对
|
|
7
7
|
* 一份旧版 dist 跑同样的三腿。
|
|
8
8
|
*
|
|
9
|
+
* 4.0.0 起这句自陈**有牙**:入口落在契约包自己身上 ⇒ 造适配器那一刻就**拒**(抛,不是告警 —— 一条告警
|
|
10
|
+
* 在 CI 日志里活不过第二天,而这道门一旦被同义反复接进去,它报的全绿是**假绿**)。判别式单铸在
|
|
11
|
+
* `./entry-origin.js`,`runConformance` 的 release 门用的是同一只(手写的适配器字面量走不到工厂,
|
|
12
|
+
* 那条路只有门看得见)。两条腿的拒绝句也同源:说清你落在哪儿、合法的是哪儿。
|
|
13
|
+
*
|
|
9
14
|
* 拒绝分层的判据(一条,不是一串特例):**ZodError 形的错 = `validate`**(某个域没通过它的 schema);
|
|
10
15
|
* 其余按腿各自的默认层 —— wire 腿是 `envelope`(它在解析任何域之前先判信封),local-root 腿是 `parse`
|
|
11
16
|
* (文件层先判:空文件 / 非 JSON / 非对象),domains 腿本来就只有校验这一层。
|
|
12
17
|
*/
|
|
13
18
|
import { materializeLocalRoot } from "./run.js";
|
|
19
|
+
import { contractOwnedEntry, contractOwnedEntryWhy } from "./entry-origin.js";
|
|
20
|
+
import { LEG_NAMES } from "./types.js";
|
|
14
21
|
/** ZodError 形(鸭子判,不用 `instanceof` —— historical 模式下旧版 dist 可能挂在另一份 zod 上)。 */
|
|
15
22
|
const isZodLike = (e) => e !== null && typeof e === "object" && Array.isArray(e.issues);
|
|
16
23
|
/** 内容哈希那一层拒绝的自述 —— `stableStringify` 的深度闸是**本包自己**抛的、也只有它会抛这一句;
|
|
@@ -21,7 +28,37 @@ const reject = (e, fallback) => {
|
|
|
21
28
|
const stage = isZodLike(e) ? "validate" : diagnostics.startsWith(HASH_STAGE_MARK) ? "hash" : fallback;
|
|
22
29
|
return { outcome: "rejected", stage, diagnostics };
|
|
23
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* 入口的**运行期拒**(fail-closed):三条腿的入口一条都不许落在**这份正在跑的**契约包自己的构建产物里,
|
|
33
|
+
* 缺席也不行。判别式与拒绝句来自 `./entry-origin.js`,这里只负责「拒」这个动作 —— 以及**按形分派**:
|
|
34
|
+
* 工厂看不见 `mode`,所以它只拒在任何模式下都错的那一形(`own-build-output` = 拿自己比自己);
|
|
35
|
+
* `another-installed-copy` 在 `historical` 模式下是正当的(那个模式就是拿旧版包比行为,而旧版包常常正好
|
|
36
|
+
* 装在 `node_modules/<包名>/` 布局下)⇒ 留给看得见 mode 的 release 门点名,工厂不越权。
|
|
37
|
+
* 形是闭集,`default` 那支是**编译期**守卫:新加一形而不在这里表态 ⇒ `never` 赋值红。
|
|
38
|
+
*/
|
|
39
|
+
function refuseContractOwnedEntries(entries) {
|
|
40
|
+
for (const leg of LEG_NAMES) {
|
|
41
|
+
const entry = entries[leg];
|
|
42
|
+
if (entry === undefined || typeof entry.module !== "string" || entry.module.length === 0) {
|
|
43
|
+
throw new Error(`conformance: leg \`${leg}\` 缺 entry.module —— 三条腿各自的**生产入口**坐标是必填的(来源律 §2d-2)`);
|
|
44
|
+
}
|
|
45
|
+
const owned = contractOwnedEntry(entry.module);
|
|
46
|
+
if (owned === undefined)
|
|
47
|
+
continue;
|
|
48
|
+
switch (owned.form) {
|
|
49
|
+
case "own-build-output":
|
|
50
|
+
throw new Error(`conformance: ${contractOwnedEntryWhy(leg, entry.module, owned)}`);
|
|
51
|
+
case "another-installed-copy":
|
|
52
|
+
continue; // 见上:historical 模式的正当形,归看得见 mode 的 release 门判
|
|
53
|
+
default: {
|
|
54
|
+
const unhandled = owned.form;
|
|
55
|
+
throw new Error(`conformance: 来源律判别式回了一个没人处置的形 ${JSON.stringify(unhandled)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
24
60
|
export function createContractAdapter(opts) {
|
|
61
|
+
refuseContractOwnedEntries(opts.entries);
|
|
25
62
|
const { mod } = opts;
|
|
26
63
|
const run = async (input) => {
|
|
27
64
|
const warnings = [];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** 契约包在盘上的两种样子(闭集):**我自己**,与**另一份**。 */
|
|
2
|
+
export declare const CONTRACT_OWNED_FORMS: readonly ["own-build-output", "another-installed-copy"];
|
|
3
|
+
export type ContractOwnedForm = (typeof CONTRACT_OWNED_FORMS)[number];
|
|
4
|
+
/** 判别式命中:哪一形、命中的是哪一段路径(拒绝句要说得出「你落在哪儿」)。 */
|
|
5
|
+
export interface ContractOwnedEntry {
|
|
6
|
+
form: ContractOwnedForm;
|
|
7
|
+
/** 命中的那段路径前缀(逐字,给人看的坐标)。 */
|
|
8
|
+
anchor: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 这个入口坐标落在契约包自己身上吗?命中回形与坐标,没命中回 `undefined`。
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ **两形的判定顺序是承重的**:消费方装好的那份 kit **自己就住在 `node_modules/<包名>/` 里**,它的
|
|
14
|
+
* `dist/` 同时命中两形。「是不是**我自己**」必须先判 —— 否则最该拒的那一形(消费方拿自己比自己)会被
|
|
15
|
+
* 判成「另一份」而被工厂放行。先 `dist` 后 `node_modules`,两形于是不相交:`another-installed-copy`
|
|
16
|
+
* 字面意思就是**另一份**装好的契约包(historical 模式的正当目标)。
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ **判之前先归一**(两侧都归):`<包根>/./dist/x.js` 与 `<包根>//dist/x.js` 与一条指向 `<包根>/dist`
|
|
19
|
+
* 的软链,指的都是同一个文件。release 门会先 `realpath` 再判,但 `historical` 模式根本不跑那道门 ——
|
|
20
|
+
* 工厂这一侧是那条路上唯一的闸,所以归一得在**判别式里**做,两处执法才咬同一个判据。
|
|
21
|
+
*/
|
|
22
|
+
export declare function contractOwnedEntry(modulePath: string): ContractOwnedEntry | undefined;
|
|
23
|
+
/** 拒绝句/违例句共用的那一段:你落在哪儿、合法的是哪儿。两处文案同源,消费方读到的是同一句话。 */
|
|
24
|
+
export declare function contractOwnedEntryWhy(leg: string, modulePath: string, owned: ContractOwnedEntry): string;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 来源律的**判别式**(§2d-2 同义反复护栏)——「这条腿的入口,其实就是契约包自己吗?」
|
|
3
|
+
*
|
|
4
|
+
* 成案:消费方登记的腿必须是**自己生产代码**的入口。把契约包的函数注册成腿,门证明的只是「包等于包」:
|
|
5
|
+
* 一次纯粹的同义反复,而且看起来全绿。判据落在**入口坐标**上,不在「你调了谁」上 —— 后者机器证不了
|
|
6
|
+
* (`docs/CONFORMANCE.md` 逐字写明了这条诚实边界)。
|
|
7
|
+
*
|
|
8
|
+
* **一只判别式,两处执法**(单铸,不许两处各写一遍「哪儿算契约包自己」)。两形是个**闭集**
|
|
9
|
+
* ({@link CONTRACT_OWNED_FORMS}),而**哪一形归哪一处**由一句话定:**工厂只拒在任何模式下都错的那一形**,
|
|
10
|
+
* 另一形归**看得见 `mode`** 的那道门。
|
|
11
|
+
* · `own-build-output` —— 入口落在**正在跑这段码的这份**契约包自己的构建产物 `<包根>/dist/` 下。
|
|
12
|
+
* 这是「拿自己比自己」,`release` 与 `historical` 两个模式下都是同义反复 ⇒ {@link createContractAdapter}
|
|
13
|
+
* 造适配器时**当场拒**(运行期拒,不是告警)。
|
|
14
|
+
* · `another-installed-copy` —— 入口落在**另一份**装好的契约包(`node_modules/<包名>/`)里。它在 `release`
|
|
15
|
+
* 下是同义反复,在 `historical` 下却**正当**(那个模式就是拿旧版包比行为,而旧版包常常正好装在
|
|
16
|
+
* `node_modules/<包名>/` 这个布局下)⇒ 工厂**不拒**(工厂看不见 `mode`,不越权),由 `runConformance`
|
|
17
|
+
* 的 release 门记成违例。
|
|
18
|
+
* 手写的适配器字面量(接入文档的默认配方)根本走不到工厂 ⇒ 那道门**两形都判**。
|
|
19
|
+
*
|
|
20
|
+
* 为什么 `own-build-output` 咬 `dist/` 而不是整个包根:契约仓**自证**时,腿的入口是本仓的 `src/*.ts`
|
|
21
|
+
* (那对契约仓来说就是它的生产入口,是这只工厂存在的两个理由之一),而 `src/` 不随包发 —— 消费方手上
|
|
22
|
+
* 只有 `dist/`。于是同一条判据既拦住了消费方的同义反复,又放行了契约仓自证;historical 模式拿
|
|
23
|
+
* **另一份**旧版包当入口,那份不是「自己」,照样放行。
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
26
|
+
import { join, resolve, sep } from "node:path";
|
|
27
|
+
import { PACKAGE_ROOT } from "./corpus.js";
|
|
28
|
+
/** 契约包在盘上的两种样子(闭集):**我自己**,与**另一份**。 */
|
|
29
|
+
export const CONTRACT_OWNED_FORMS = ["own-build-output", "another-installed-copy"];
|
|
30
|
+
let cachedName;
|
|
31
|
+
/** 本包的名字 —— 从**自己**的 package.json 读(不手抄;`kitIdentity()` 读的是同一个文件)。读不出来就抛:
|
|
32
|
+
* 一只判不了「哪儿是契约包自己」的判别式,静默放行等于把来源律整条关掉。 */
|
|
33
|
+
function contractPackageName() {
|
|
34
|
+
if (cachedName === undefined) {
|
|
35
|
+
const pkg = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
36
|
+
if (typeof pkg.name !== "string" || pkg.name.length === 0) {
|
|
37
|
+
throw new Error("conformance: 契约包自己的 package.json 没有 `name` —— 来源律判别式判不了「哪儿是契约包自己」");
|
|
38
|
+
}
|
|
39
|
+
cachedName = pkg.name;
|
|
40
|
+
}
|
|
41
|
+
return cachedName;
|
|
42
|
+
}
|
|
43
|
+
/** 一条路径的**归一形**(判据咬的是**文件**,不是字符串):点段 / 双斜杠先由 `resolve` 抹平,再尽力解一次
|
|
44
|
+
* 软链(`npm link` 出来的树就是软链)。文件不在盘上 ⇒ 解不了,那就只用字面归一形 —— 这不是放行:
|
|
45
|
+
* 「入口不存在」本来就由 release 门单独点名,这里少一个候选形不会让任何一形逃掉。 */
|
|
46
|
+
function canonicalForms(p) {
|
|
47
|
+
const lexical = resolve(p);
|
|
48
|
+
try {
|
|
49
|
+
const real = realpathSync(lexical);
|
|
50
|
+
return real === lexical ? [lexical] : [lexical, real];
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return [lexical];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 这个入口坐标落在契约包自己身上吗?命中回形与坐标,没命中回 `undefined`。
|
|
58
|
+
*
|
|
59
|
+
* ⚠️ **两形的判定顺序是承重的**:消费方装好的那份 kit **自己就住在 `node_modules/<包名>/` 里**,它的
|
|
60
|
+
* `dist/` 同时命中两形。「是不是**我自己**」必须先判 —— 否则最该拒的那一形(消费方拿自己比自己)会被
|
|
61
|
+
* 判成「另一份」而被工厂放行。先 `dist` 后 `node_modules`,两形于是不相交:`another-installed-copy`
|
|
62
|
+
* 字面意思就是**另一份**装好的契约包(historical 模式的正当目标)。
|
|
63
|
+
*
|
|
64
|
+
* ⚠️ **判之前先归一**(两侧都归):`<包根>/./dist/x.js` 与 `<包根>//dist/x.js` 与一条指向 `<包根>/dist`
|
|
65
|
+
* 的软链,指的都是同一个文件。release 门会先 `realpath` 再判,但 `historical` 模式根本不跑那道门 ——
|
|
66
|
+
* 工厂这一侧是那条路上唯一的闸,所以归一得在**判别式里**做,两处执法才咬同一个判据。
|
|
67
|
+
*/
|
|
68
|
+
export function contractOwnedEntry(modulePath) {
|
|
69
|
+
const candidates = canonicalForms(modulePath);
|
|
70
|
+
const ownDists = canonicalForms(join(PACKAGE_ROOT, "dist")).map((d) => d + sep);
|
|
71
|
+
for (const anchor of ownDists) {
|
|
72
|
+
if (candidates.some((c) => c.startsWith(anchor)))
|
|
73
|
+
return { form: "own-build-output", anchor };
|
|
74
|
+
}
|
|
75
|
+
const installed = `${sep}node_modules${sep}${contractPackageName().split("/").join(sep)}${sep}`;
|
|
76
|
+
if (candidates.some((c) => c.includes(installed)))
|
|
77
|
+
return { form: "another-installed-copy", anchor: installed };
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
/** 拒绝句/违例句共用的那一段:你落在哪儿、合法的是哪儿。两处文案同源,消费方读到的是同一句话。 */
|
|
81
|
+
export function contractOwnedEntryWhy(leg, modulePath, owned) {
|
|
82
|
+
return (`leg \`${leg}\` entry.module sits inside the contract package (${modulePath}) —— 命中 ${owned.form} @ ${owned.anchor};` +
|
|
83
|
+
`那条腿证明的是「包等于包」。合法的入口 = **你自己生产代码**里那个真入口文件的绝对路径` +
|
|
84
|
+
`(消费方:对着你自己的装配层求 \`fileURLToPath(import.meta.url)\`;契约仓自证:本仓 \`src/*.ts\`;` +
|
|
85
|
+
`historical 模式:另一份旧版包的 dist)。见 docs/CONFORMANCE.md 的来源律一节。`);
|
|
86
|
+
}
|
package/dist/conformance/run.js
CHANGED
|
@@ -18,12 +18,10 @@ import { canonicalize, deepEqual, diffJson, stableKey } from "./canonical.js";
|
|
|
18
18
|
import { diffWarningMultiset, findSecretLeaks, projectWarning } from "./warnings.js";
|
|
19
19
|
import { deriveLocalExpectation, deriveLocalInput, deriveWireExpectation, deriveWireInput, toManifestDoc } from "./derive.js";
|
|
20
20
|
import { kitIdentity, loadCorpus } from "./corpus.js";
|
|
21
|
+
import { contractOwnedEntry, contractOwnedEntryWhy } from "./entry-origin.js";
|
|
21
22
|
import { LEG_NAMES, } from "./types.js";
|
|
22
23
|
/** 一个**模块**文件的样子 —— 来源律用它挡住「随便找个存在的文件当入口」(`package.json` 也是文件)。 */
|
|
23
24
|
const MODULE_EXTENSIONS = [".ts", ".mts", ".cts", ".tsx", ".js", ".mjs", ".cjs", ".jsx"];
|
|
24
|
-
/** 契约包装在 `node_modules` 里的样子 —— 来源律用它判「这条腿其实就是契约包自己」。按**包名**拼,
|
|
25
|
-
* 不手抄(包名从 kit 自己的 package.json 来)。 */
|
|
26
|
-
const contractInNodeModules = (packageName) => `${sep}node_modules${sep}${packageName.split("/").join(sep)}${sep}`;
|
|
27
25
|
/**
|
|
28
26
|
* 把一份 `config.d/` 快照落成真目录 —— **kit 单铸**,消费方零复述(每个消费方各写一遍落盘逻辑,
|
|
29
27
|
* mtime 怎么设、目录怎么建就会各不相同,而 `updatedAt` 正是按 mtime 算的)。
|
|
@@ -330,9 +328,11 @@ async function releaseViolations(adapter, opts, corpus, kit) {
|
|
|
330
328
|
catch {
|
|
331
329
|
v.push(`leg \`${name}\` entry.module does not exist: ${mod}`);
|
|
332
330
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
331
|
+
// 来源律判别式 = `./entry-origin.js` 那一只(工厂当场拒的同一只)。点段 / 双斜杠 / 软链的归一在
|
|
332
|
+
// 判别式**里面**做(两处执法咬同一个判据),所以这里不再自己解一遍。
|
|
333
|
+
const owned = contractOwnedEntry(mod);
|
|
334
|
+
if (owned !== undefined)
|
|
335
|
+
v.push(contractOwnedEntryWhy(name, mod, owned));
|
|
336
336
|
if (leg.entry.export.trim().length === 0)
|
|
337
337
|
v.push(`leg \`${name}\` entry.export is empty`);
|
|
338
338
|
}
|
|
@@ -81,7 +81,11 @@ export interface ConformanceProvenance {
|
|
|
81
81
|
* 同义反复,而且看起来全绿。`module` 必须是消费方**生产代码**里那个真入口文件的绝对路径,`export`
|
|
82
82
|
* 是它导出的那个名字。
|
|
83
83
|
*
|
|
84
|
-
*
|
|
84
|
+
* 4.0.0 起这条律**有牙且两处执法**:判别式单铸在 `./entry-origin.js`(两形闭集 —— **我自己**的构建产物 /
|
|
85
|
+
* **另一份**装好的包)。`createContractAdapter` 看不见 `mode`,只拒在任何模式下都错的那一形(拿自己比自己),
|
|
86
|
+
* **当场拒**;另一形在 `historical` 下正当,连同手写的适配器字面量(走不到工厂)一起由 `release` 门记成违例。
|
|
87
|
+
*
|
|
88
|
+
* **门的诚实边界**:kit 能核的是「这个文件真的存在」「它不落在契约包自己身上」。它**不能**
|
|
85
89
|
* 证明这个文件内部没有直接转调契约函数 —— 那由消费方半场的**变异自证**负责(消费方对自己的折叠/投影层
|
|
86
90
|
* 注入一处变异 ⇒ kit 必红)。本字段是护栏,不是证明,docs/CONFORMANCE.md 逐字写明这一点。
|
|
87
91
|
*/
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 告警投影的**多重集比对** —— kit 的第二个单铸点。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* **投影本身不在这里**(4.0.0):`WarningProjection` / `projectWarning` / `WARNING_KINDS` 的属主是告警联合
|
|
5
|
+
* 自己的模块 `../config-fns`,本文件只再导出(kit 的进口逐字不变)。投影 = 每臂减掉 `error`,**从联合派生**;
|
|
6
|
+
* 为什么要剥 `error`:它带的是 zod 的错误对象/文案,可经脱敏、可随 zod 小版本改字 —— 比文案就是比实现。
|
|
7
|
+
* `error` 因此不进比对,但也不是就这么放过去了:{@link findSecretLeaks} 先断言它不含语料自带的秘密清单。
|
|
7
8
|
*
|
|
8
9
|
* 为什么是**多重集**而不是集合:同一个 kind 在同一个域上发两次(两条坏条目、两处未知键路径)是有信息的
|
|
9
10
|
* —— 去重会让「报了一条」与「报了两条」同形,而那恰是消费方最容易漏折的一类。
|
|
@@ -11,51 +12,9 @@
|
|
|
11
12
|
* 坐标({@link ConfigPath})按**结构**比:段数组进 JSON 编码,`["a.b"]` 与 `["a","b"]`、`["1"]` 与 `[1]`
|
|
12
13
|
* 在编码里都还分得开。kit 内**零** `formatConfigPath` 调用 —— 出门形是给人看的,不是比对用的。
|
|
13
14
|
*/
|
|
14
|
-
import type
|
|
15
|
+
import { type ConfigPath, type EffectiveReadWarning, type WarningProjection } from "../config-fns.js";
|
|
15
16
|
import type { ConformanceNormalizer } from "./types.js";
|
|
16
|
-
|
|
17
|
-
export declare const WARNING_KINDS: readonly ["hosts-grandfathered", "domain-defaulted", "unknown-keys-dropped", "unknown-keys-carried", "legacy-governance-lifted", "entry-dropped", "unread-config-file"];
|
|
18
|
-
export type WarningKind = (typeof WARNING_KINDS)[number];
|
|
19
|
-
/** 一条告警的投影 —— 每臂列全该 kind 的**非 `error`** 字段。 */
|
|
20
|
-
export type WarningProjection = {
|
|
21
|
-
kind: "hosts-grandfathered";
|
|
22
|
-
domain: "hosts";
|
|
23
|
-
droppedNames: readonly string[];
|
|
24
|
-
truncatedFrom?: number;
|
|
25
|
-
} | {
|
|
26
|
-
kind: "domain-defaulted";
|
|
27
|
-
domain: string;
|
|
28
|
-
} | {
|
|
29
|
-
kind: "unknown-keys-dropped";
|
|
30
|
-
domain: string;
|
|
31
|
-
keys: readonly ConfigPath[];
|
|
32
|
-
truncatedFrom?: number;
|
|
33
|
-
} | {
|
|
34
|
-
kind: "unknown-keys-carried";
|
|
35
|
-
domain: string;
|
|
36
|
-
keys: readonly ConfigPath[];
|
|
37
|
-
truncatedFrom?: number;
|
|
38
|
-
} | {
|
|
39
|
-
kind: "legacy-governance-lifted";
|
|
40
|
-
domain: "governance";
|
|
41
|
-
keys: readonly ConfigPath[];
|
|
42
|
-
} | {
|
|
43
|
-
kind: "entry-dropped";
|
|
44
|
-
domain: string;
|
|
45
|
-
path: ConfigPath;
|
|
46
|
-
} | {
|
|
47
|
-
kind: "unread-config-file";
|
|
48
|
-
file: string;
|
|
49
|
-
domain?: string;
|
|
50
|
-
why: string;
|
|
51
|
-
};
|
|
52
|
-
/**
|
|
53
|
-
* 一条告警 → 它的投影。**闭集穷举**:新 kind 不在这里表态就编译红(#157 的闭集纪律)。
|
|
54
|
-
*
|
|
55
|
-
* `truncatedFrom` 缺席时**不铸这个键**(不是铸 `undefined`)—— 规范化会把 `undefined` 值剔掉,两种写法
|
|
56
|
-
* 最终同形,但不铸更诚实:「没有截断」与「截断了 0 条」不该在中间形上分成两件事。
|
|
57
|
-
*/
|
|
58
|
-
export declare function projectWarning(w: EffectiveReadWarning): WarningProjection;
|
|
17
|
+
export { WARNING_KINDS, projectWarning, type WarningKind, type WarningProjection } from "../config-fns.js";
|
|
59
18
|
/**
|
|
60
19
|
* 一条投影的**比对归一形**:①坐标过一遍映射(historical 模式把段拼成旧版的点号串;无归一器时是恒等);
|
|
61
20
|
* ②`keys` / `droppedNames` 这两个**集合字段**按袋归一。第二处闭集穷举:哪些 kind 带坐标、带在哪个字段上,
|
|
@@ -1,46 +1,23 @@
|
|
|
1
|
-
import { canonicalize, stableKey } from "./canonical.js";
|
|
2
|
-
/** {@link EffectiveReadWarning} 的 `kind` 闭集(覆盖律按它计数;新 kind 不登记 ⇒ 下面的 switch 编译红)。 */
|
|
3
|
-
export const WARNING_KINDS = [
|
|
4
|
-
"hosts-grandfathered",
|
|
5
|
-
"domain-defaulted",
|
|
6
|
-
"unknown-keys-dropped",
|
|
7
|
-
"unknown-keys-carried",
|
|
8
|
-
"legacy-governance-lifted",
|
|
9
|
-
"entry-dropped",
|
|
10
|
-
"unread-config-file",
|
|
11
|
-
];
|
|
12
1
|
/**
|
|
13
|
-
*
|
|
2
|
+
* 告警投影的**多重集比对** —— kit 的第二个单铸点。
|
|
3
|
+
*
|
|
4
|
+
* **投影本身不在这里**(4.0.0):`WarningProjection` / `projectWarning` / `WARNING_KINDS` 的属主是告警联合
|
|
5
|
+
* 自己的模块 `../config-fns`,本文件只再导出(kit 的进口逐字不变)。投影 = 每臂减掉 `error`,**从联合派生**;
|
|
6
|
+
* 为什么要剥 `error`:它带的是 zod 的错误对象/文案,可经脱敏、可随 zod 小版本改字 —— 比文案就是比实现。
|
|
7
|
+
* `error` 因此不进比对,但也不是就这么放过去了:{@link findSecretLeaks} 先断言它不含语料自带的秘密清单。
|
|
14
8
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
9
|
+
* 为什么是**多重集**而不是集合:同一个 kind 在同一个域上发两次(两条坏条目、两处未知键路径)是有信息的
|
|
10
|
+
* —— 去重会让「报了一条」与「报了两条」同形,而那恰是消费方最容易漏折的一类。
|
|
11
|
+
*
|
|
12
|
+
* 坐标({@link ConfigPath})按**结构**比:段数组进 JSON 编码,`["a.b"]` 与 `["a","b"]`、`["1"]` 与 `[1]`
|
|
13
|
+
* 在编码里都还分得开。kit 内**零** `formatConfigPath` 调用 —— 出门形是给人看的,不是比对用的。
|
|
17
14
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// 两臂字段相同但**不合并**:合并后 `kind` 塌成两员的联合,TS 就再也认不出它属于投影联合的哪一员
|
|
25
|
-
// (真编译红过一次)。逐 kind 一臂,是这张表本来的样子。
|
|
26
|
-
case "unknown-keys-dropped":
|
|
27
|
-
return { kind: w.kind, domain: w.domain, keys: w.keys, ...(w.truncatedFrom === undefined ? {} : { truncatedFrom: w.truncatedFrom }) };
|
|
28
|
-
case "unknown-keys-carried":
|
|
29
|
-
return { kind: w.kind, domain: w.domain, keys: w.keys, ...(w.truncatedFrom === undefined ? {} : { truncatedFrom: w.truncatedFrom }) };
|
|
30
|
-
case "legacy-governance-lifted":
|
|
31
|
-
return { kind: w.kind, domain: w.domain, keys: w.keys };
|
|
32
|
-
case "entry-dropped":
|
|
33
|
-
return { kind: w.kind, domain: w.domain, path: w.path };
|
|
34
|
-
case "unread-config-file":
|
|
35
|
-
return { kind: w.kind, file: w.file, ...(w.domain === undefined ? {} : { domain: w.domain }), why: w.why };
|
|
36
|
-
default: {
|
|
37
|
-
const unhandled = w;
|
|
38
|
-
// 运行期到不了(本联合由契约包铸造);到了就让它红 —— 一条不认识的告警被静默投影成空,
|
|
39
|
-
// 正是「门看起来全绿」的那种假绿。
|
|
40
|
-
throw new Error(`conformance: unknown EffectiveReadWarning kind — ${JSON.stringify(unhandled)}`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
15
|
+
import { projectWarning } from "../config-fns.js";
|
|
16
|
+
import { canonicalize, stableKey } from "./canonical.js";
|
|
17
|
+
// 投影的**属主在 `../config-fns`**(告警联合自己的模块;4.0.0 起)—— kit 只再导出,进口不断。
|
|
18
|
+
// 4.0.0 之前这三件住在本文件里,于是「一条告警有哪些字段」在舰队里有两个属主:契约那边加一个字段,
|
|
19
|
+
// 这边的手写投影不动,而**没有一格会红**。现在投影从联合逐臂派生(`Omit<臂, "error">`),那条缝没了。
|
|
20
|
+
export { WARNING_KINDS, projectWarning } from "../config-fns.js";
|
|
44
21
|
/** 一只**袋**:元素按各自的稳定编码排序,重数保留。比对前归一用,不改变「报了几条」这件事。 */
|
|
45
22
|
const bag = (items) => [...items].sort((a, b) => (stableKey(a) < stableKey(b) ? -1 : stableKey(a) > stableKey(b) ? 1 : 0));
|
|
46
23
|
/**
|
package/dist/cross-domain.d.ts
CHANGED
|
@@ -29,10 +29,30 @@ export type SiblingResolver = <K extends keyof DomainConfig>(domain: K) => Promi
|
|
|
29
29
|
export declare function createSiblingResolver(store: ConfigReader, pending?: Partial<{
|
|
30
30
|
[K in keyof DomainConfig]: DomainConfig[K];
|
|
31
31
|
}>): SiblingResolver;
|
|
32
|
-
/** The domains whose writes carry cross-domain model/roster references we validate at save time
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
32
|
+
/** The domains whose writes carry cross-domain model/roster references we validate at save time — the closed
|
|
33
|
+
* set as a RUNTIME VALUE (4.0.0). A consumer that has to ask "is this domain ref-checked?" at runtime (a
|
|
34
|
+
* config-rollback path deciding whether to run the guard before writing) could only hand-copy the list
|
|
35
|
+
* before, and a hand copy drifts silently: two rollback routes in the admin console each carried a
|
|
36
|
+
* five-member copy that had lost `entitlement`, so an entitlement rollback skipped the guard entirely with
|
|
37
|
+
* nothing red anywhere. Ask this constant (or {@link isRefCheckedDomain}) instead of writing a list.
|
|
38
|
+
*
|
|
39
|
+
* `satisfies readonly (keyof DomainConfig)[]` keeps every member a real domain key; {@link RefCheckedDomain}
|
|
40
|
+
* is DERIVED from this value, so there is exactly one place the set is written down. */
|
|
41
|
+
export declare const REF_CHECKED_DOMAINS: readonly ["rosters", "workers", "models", "collab", "systems", "entitlement"];
|
|
42
|
+
/** The domains whose writes carry cross-domain model/roster references we validate at save time.
|
|
43
|
+
* DERIVED from {@link REF_CHECKED_DOMAINS} (4.0.0) — a hand-written union next to the value would be a
|
|
44
|
+
* second copy of the same set, free to disagree with it. */
|
|
45
|
+
export type RefCheckedDomain = (typeof REF_CHECKED_DOMAINS)[number];
|
|
46
|
+
/** Runtime narrowing to {@link RefCheckedDomain}, same idiom as `isDomainName`: use it instead of hard-coding
|
|
47
|
+
* the set when filtering an arbitrary domain key (a batch payload's keys, a rollback target). */
|
|
48
|
+
export declare function isRefCheckedDomain(d: unknown): d is RefCheckedDomain;
|
|
49
|
+
/** Cross-domain ref integrity for one {@link RefCheckedDomain} value. Every issue `path` starts with the
|
|
50
|
+
* DOMAIN KEY (e.g. `["rosters", i, "models", j]`, `["collab", "templates", i, …]`) so a caller can prefix a
|
|
51
|
+
* batch index and get a coordinate that is unambiguous across arms. Empty = ok. Runtime resolution stays
|
|
52
|
+
* tolerant (falls back); this is purely a save-time guard — a precise 400 at save time is friendlier than a
|
|
53
|
+
* silently-wrong roster.
|
|
54
|
+
*
|
|
55
|
+
* 4.0.0 made that first sentence TRUE: the `collab` arm used to start its paths at `"templates"` and the
|
|
56
|
+
* `entitlement` arm at `"tiers"`, so a caller could not read `path[0]` as the domain. See the CHANGELOG entry
|
|
57
|
+
* for the migration (BREAKING, value shape). */
|
|
38
58
|
export declare function refIntegrityIssues(domain: RefCheckedDomain, value: unknown, resolve: SiblingResolver): Promise<RefIssue[]>;
|
package/dist/cross-domain.js
CHANGED
|
@@ -12,10 +12,30 @@ export function createSiblingResolver(store, pending) {
|
|
|
12
12
|
return store.getDomain(domain);
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
/** The domains whose writes carry cross-domain model/roster references we validate at save time — the closed
|
|
16
|
+
* set as a RUNTIME VALUE (4.0.0). A consumer that has to ask "is this domain ref-checked?" at runtime (a
|
|
17
|
+
* config-rollback path deciding whether to run the guard before writing) could only hand-copy the list
|
|
18
|
+
* before, and a hand copy drifts silently: two rollback routes in the admin console each carried a
|
|
19
|
+
* five-member copy that had lost `entitlement`, so an entitlement rollback skipped the guard entirely with
|
|
20
|
+
* nothing red anywhere. Ask this constant (or {@link isRefCheckedDomain}) instead of writing a list.
|
|
21
|
+
*
|
|
22
|
+
* `satisfies readonly (keyof DomainConfig)[]` keeps every member a real domain key; {@link RefCheckedDomain}
|
|
23
|
+
* is DERIVED from this value, so there is exactly one place the set is written down. */
|
|
24
|
+
export const REF_CHECKED_DOMAINS = ["rosters", "workers", "models", "collab", "systems", "entitlement"];
|
|
25
|
+
/** Runtime narrowing to {@link RefCheckedDomain}, same idiom as `isDomainName`: use it instead of hard-coding
|
|
26
|
+
* the set when filtering an arbitrary domain key (a batch payload's keys, a rollback target). */
|
|
27
|
+
export function isRefCheckedDomain(d) {
|
|
28
|
+
return typeof d === "string" && REF_CHECKED_DOMAINS.includes(d);
|
|
29
|
+
}
|
|
30
|
+
/** Cross-domain ref integrity for one {@link RefCheckedDomain} value. Every issue `path` starts with the
|
|
31
|
+
* DOMAIN KEY (e.g. `["rosters", i, "models", j]`, `["collab", "templates", i, …]`) so a caller can prefix a
|
|
32
|
+
* batch index and get a coordinate that is unambiguous across arms. Empty = ok. Runtime resolution stays
|
|
33
|
+
* tolerant (falls back); this is purely a save-time guard — a precise 400 at save time is friendlier than a
|
|
34
|
+
* silently-wrong roster.
|
|
35
|
+
*
|
|
36
|
+
* 4.0.0 made that first sentence TRUE: the `collab` arm used to start its paths at `"templates"` and the
|
|
37
|
+
* `entitlement` arm at `"tiers"`, so a caller could not read `path[0]` as the domain. See the CHANGELOG entry
|
|
38
|
+
* for the migration (BREAKING, value shape). */
|
|
19
39
|
export async function refIntegrityIssues(domain, value, resolve) {
|
|
20
40
|
const issues = [];
|
|
21
41
|
if (domain === "rosters") {
|
|
@@ -73,7 +93,7 @@ export async function refIntegrityIssues(domain, value, resolve) {
|
|
|
73
93
|
t.slots.forEach((s, j) => {
|
|
74
94
|
s.members.forEach((mem, k) => {
|
|
75
95
|
if (mem.modelRef && !catalog.has(mem.modelRef))
|
|
76
|
-
issues.push({ path: ["templates", i, "slots", j, "members", k, "modelRef"], message: `collab template "${t.id}" slot "${s.id}" → unknown model "${mem.modelRef}" (not in the catalog)`, code: "custom" });
|
|
96
|
+
issues.push({ path: ["collab", "templates", i, "slots", j, "members", k, "modelRef"], message: `collab template "${t.id}" slot "${s.id}" → unknown model "${mem.modelRef}" (not in the catalog)`, code: "custom" });
|
|
77
97
|
});
|
|
78
98
|
});
|
|
79
99
|
});
|
|
@@ -101,15 +121,15 @@ export async function refIntegrityIssues(domain, value, resolve) {
|
|
|
101
121
|
const pluginNames = new Set((await resolve("plugins"))?.plugins.map((p) => p.name) ?? []);
|
|
102
122
|
value.tiers.forEach((t, i) => {
|
|
103
123
|
if (t.roster && !rosterNames.has(t.roster))
|
|
104
|
-
issues.push({ path: ["tiers", i, "roster"], message: `tier "${t.id}" references unknown roster "${t.roster}"`, code: "custom" });
|
|
124
|
+
issues.push({ path: ["entitlement", "tiers", i, "roster"], message: `tier "${t.id}" references unknown roster "${t.roster}"`, code: "custom" });
|
|
105
125
|
(t.models ?? []).forEach((name, j) => { if (!modelCatalog.has(name))
|
|
106
|
-
issues.push({ path: ["tiers", i, "models", j], message: `tier "${t.id}" model "${name}" not in the models catalog`, code: "custom" }); });
|
|
126
|
+
issues.push({ path: ["entitlement", "tiers", i, "models", j], message: `tier "${t.id}" model "${name}" not in the models catalog`, code: "custom" }); });
|
|
107
127
|
(t.visibleSkills ?? []).forEach((name, j) => { if (!skillNames.has(name))
|
|
108
|
-
issues.push({ path: ["tiers", i, "visibleSkills", j], message: `tier "${t.id}" visibleSkill "${name}" not in skills`, code: "custom" }); });
|
|
128
|
+
issues.push({ path: ["entitlement", "tiers", i, "visibleSkills", j], message: `tier "${t.id}" visibleSkill "${name}" not in skills`, code: "custom" }); });
|
|
109
129
|
(t.visibleMcp ?? []).forEach((name, j) => { if (!mcpNames.has(name))
|
|
110
|
-
issues.push({ path: ["tiers", i, "visibleMcp", j], message: `tier "${t.id}" visibleMcp "${name}" not in mcp`, code: "custom" }); });
|
|
130
|
+
issues.push({ path: ["entitlement", "tiers", i, "visibleMcp", j], message: `tier "${t.id}" visibleMcp "${name}" not in mcp`, code: "custom" }); });
|
|
111
131
|
(t.visiblePlugins ?? []).forEach((name, j) => { if (!pluginNames.has(name))
|
|
112
|
-
issues.push({ path: ["tiers", i, "visiblePlugins", j], message: `tier "${t.id}" visiblePlugin "${name}" not in plugins`, code: "custom" }); });
|
|
132
|
+
issues.push({ path: ["entitlement", "tiers", i, "visiblePlugins", j], message: `tier "${t.id}" visiblePlugin "${name}" not in plugins`, code: "custom" }); });
|
|
113
133
|
});
|
|
114
134
|
}
|
|
115
135
|
else {
|
package/dist/node.d.ts
CHANGED
package/dist/node.js
CHANGED
|
@@ -9,3 +9,4 @@ export * from "./file-edit.js";
|
|
|
9
9
|
export * from "./bundle.js";
|
|
10
10
|
export * from "./local-load.js";
|
|
11
11
|
export * from "./scheduler-store-node.js"; // R7 self-wake scheduler store — fs binding of the ./scheduler-store contract
|
|
12
|
+
export * from "./prompts-digest.js"; // prompts 下发包的内容摘要(4.0.0 从 scheduler-store-node 搬来 —— 与调度存储无关;出口路径不变)
|