@sema-agent/client-core 0.68.0 → 0.68.2

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.
@@ -0,0 +1,41 @@
1
+ /**
2
+ * src/sdkWireTransit.ts — SDK **wire 面**的转口口(L-61 存量清零;clay 令 C-R43「所有欠账绝不延期」)。
3
+ *
4
+ * ── 为什么开这一只(与 `engineHttpTools.ts` 的分界)──────────────────────────────────────────
5
+ * `engineHttpTools.ts` 转的是两件**纯工具**(URL builder / 存活探针)。本文件转的是 wire 面的
6
+ * **客户端类**、**帧谓词**与**型面** —— 端至今只能直连 `@sema-agent/sdk` 去取它们
7
+ * (cli sdk-isolation 存量册 `sdk-value` 桶的剩余条目),而「壳对引擎 wire 的一切消费必须经本包」
8
+ * 这条公约对它们一直不成立。
9
+ *
10
+ * 🔴 **为什么 0.68.2 之前把 `AgentClient` 记成 declined,现在改成转口**(改口要给理由,不能只改结论):
11
+ * 此前的理由是「转口一个客户端类 = 本包为它的连接姿势/鉴权头/重试语义背书」。这条**顾虑仍然
12
+ * 成立**,但它指向的是「端该不该直接 new 一个 AgentClient」这个**设计问题**,而不是「这条 import
13
+ * 该不该经本包」这个**归层问题**——把两者绑在一起的代价是:归层这件事被一个未排期的设计件
14
+ * 无限期挡住(存量窗已因此过期两代)。⇒ 分开处理:
15
+ * · **归层现在做**(本文件,原样转口,零包装);
16
+ * · **设计件照旧欠着**:本包已有的自有路 `makeEngineWireClient` 才是推荐姿势,端上新码一律走
17
+ * 那一条;本转口口只给**存量**用,不是新码的入口。这一条写在这里,不靠人记得。
18
+ *
19
+ * 🔴 **原样转口,一个字节都不加工**(与 `engineHttpTools.ts` 同一条纪律):不包装、不改签名、
20
+ * 不补默认值 —— 包一层就是把「同一实现」这条唯一的抗漂移保证亲手拆掉。
21
+ *
22
+ * ── 可移植门为什么不受影响(实测,不是推断)────────────────────────────────────────────────
23
+ * · `@sema-agent/sdk` **本来就在** `EXPECTED_PACKAGES_INDEX` 等值集里(本包已有值级 SDK import)
24
+ * ⇒ 集合一个字节不动;
25
+ * · SDK 的 `dist/` 全树 **零 `node:` 内建**(实测:`grep -rl "node:" dist/*.js` 空),传输走全局
26
+ * `fetch`/`AbortController` = web 标准 ⇒ ③ 段的 `--platform=browser` 真打包面零风险;
27
+ * · 型面转口是 `export type`,编译后整段消失 ⇒ 闭包扫描的「剥 type-only」那一步本来就不看它。
28
+ */
29
+ export { AgentClient } from '@sema-agent/sdk';
30
+ export { isApprovalRequestFrameV1, isApprovalRevokeFrameV1 } from '@sema-agent/sdk';
31
+ /**
32
+ * ── ③ wire **型面**(零运行期,`export type`)────────────────────────────────────────────────
33
+ *
34
+ * 🔴 **射程写明**(免得下一棒把这里当成「SDK 型面的全量镜像」):本表**只**收端上存量直连实际用到
35
+ * 的那些名字,一个不多。全量镜像是另一件事(会把本包的 `.d.ts` 与 SDK 的每一次型面改动绑死),
36
+ * 与 `@sema-agent/agent-types` 的**CC 形**全量镜像不是一回事 —— 那边镜的是 CC SDK 的会话词汇,
37
+ * 这边是引擎 wire 的请求/事件/回执形。
38
+ * 🔴 **谁是权威**:权威恒是 `@sema-agent/sdk` 本身。本文件是**别名**,上游改名 ⇒ 本文件当场编译红,
39
+ * 端跟着红 —— 这正是型面转口相对「端各自直连」唯一多出来的那点好处(红在一处,不是散在九处)。
40
+ */
41
+ export type { TaskRequest, SemaSettings, SkillSpec, AgentEvent, RunReceipt, CancelAck, RunRecord, ToolApprovalRespondAck, FleetTaskRow, FleetWorkflowRow, ApprovalCard, ApprovalRequestFrame, ApprovalRiskAxes, AskDecisionAck, AskDecisionBody, } from '@sema-agent/sdk';
@@ -0,0 +1,32 @@
1
+ /**
2
+ * src/sdkWireTransit.ts — SDK **wire 面**的转口口(L-61 存量清零;clay 令 C-R43「所有欠账绝不延期」)。
3
+ *
4
+ * ── 为什么开这一只(与 `engineHttpTools.ts` 的分界)──────────────────────────────────────────
5
+ * `engineHttpTools.ts` 转的是两件**纯工具**(URL builder / 存活探针)。本文件转的是 wire 面的
6
+ * **客户端类**、**帧谓词**与**型面** —— 端至今只能直连 `@sema-agent/sdk` 去取它们
7
+ * (cli sdk-isolation 存量册 `sdk-value` 桶的剩余条目),而「壳对引擎 wire 的一切消费必须经本包」
8
+ * 这条公约对它们一直不成立。
9
+ *
10
+ * 🔴 **为什么 0.68.2 之前把 `AgentClient` 记成 declined,现在改成转口**(改口要给理由,不能只改结论):
11
+ * 此前的理由是「转口一个客户端类 = 本包为它的连接姿势/鉴权头/重试语义背书」。这条**顾虑仍然
12
+ * 成立**,但它指向的是「端该不该直接 new 一个 AgentClient」这个**设计问题**,而不是「这条 import
13
+ * 该不该经本包」这个**归层问题**——把两者绑在一起的代价是:归层这件事被一个未排期的设计件
14
+ * 无限期挡住(存量窗已因此过期两代)。⇒ 分开处理:
15
+ * · **归层现在做**(本文件,原样转口,零包装);
16
+ * · **设计件照旧欠着**:本包已有的自有路 `makeEngineWireClient` 才是推荐姿势,端上新码一律走
17
+ * 那一条;本转口口只给**存量**用,不是新码的入口。这一条写在这里,不靠人记得。
18
+ *
19
+ * 🔴 **原样转口,一个字节都不加工**(与 `engineHttpTools.ts` 同一条纪律):不包装、不改签名、
20
+ * 不补默认值 —— 包一层就是把「同一实现」这条唯一的抗漂移保证亲手拆掉。
21
+ *
22
+ * ── 可移植门为什么不受影响(实测,不是推断)────────────────────────────────────────────────
23
+ * · `@sema-agent/sdk` **本来就在** `EXPECTED_PACKAGES_INDEX` 等值集里(本包已有值级 SDK import)
24
+ * ⇒ 集合一个字节不动;
25
+ * · SDK 的 `dist/` 全树 **零 `node:` 内建**(实测:`grep -rl "node:" dist/*.js` 空),传输走全局
26
+ * `fetch`/`AbortController` = web 标准 ⇒ ③ 段的 `--platform=browser` 真打包面零风险;
27
+ * · 型面转口是 `export type`,编译后整段消失 ⇒ 闭包扫描的「剥 type-only」那一步本来就不看它。
28
+ */
29
+ // ── ① 客户端类(存量转口;新码走 `makeEngineWireClient`)────────────────────────────────────
30
+ export { AgentClient } from '@sema-agent/sdk';
31
+ // ── ② 流内审批帧的**信封谓词**(v1 窄化判据;端拿它判「这一帧是不是我认得的那一代」)──────────
32
+ export { isApprovalRequestFrameV1, isApprovalRevokeFrameV1 } from '@sema-agent/sdk';
package/dist/seam.d.ts CHANGED
@@ -600,11 +600,53 @@ export interface WiringManifestChromeEvent {
600
600
  * text content block。CC 对位:CC 在 provider 的 `content_block_stop` 上把每个写完的块当作一条
601
601
  * 独立 assistant 消息 —— 块结束**就是**分段信号,本臂把同一个边界搬到了 wire 上。
602
602
  *
603
- * 🔴 **宿主消费义务**(全部可选、fail-soft;本臂存在的第一价值 = 边界信号不再落 `unknown_arm`):
604
- * ① **它是信号,不是内容**。`content` = 那一段的**权威全文**,而这些字节**已经**以 `stream_delta`
605
- * 活体增量流过、并且会由 transcript 平面的 assistant 消息给出 committed 形。
606
- * 🔴 拿 `content` 再渲一行 = **同一段文字上屏两遍**。它的正当用途是**对账**(用引擎的权威全文
607
- * 校/换掉自己缝合出来的那一段)与**定界**(现在可以提交这一段了)。
603
+ * 🔴 **宿主消费义务**(② ③ ④ 可选、fail-soft;① 自 0.68.1 起是**安全面义务**,不实现 = 未脱敏
604
+ * 字节留在那个宿主的本地转录里):
605
+ * ① 🔴 **它是信号 + 权威内容**(0.68.1 改口;server ≥7.75.3)。`content` = 那一段的**权威全文**,
606
+ * 并且与活体增量的拼接**可以不相等** —— server 7.75.3 起它与 `result`/账本走**同一只脱敏器**,
607
+ * 而 `text_delta` 仍逐字(跨 chunk 的凭据无法就地判)。修前这里写的是「与增量拼接逐字节相等…
608
+ * 别拿它再渲一行」,那句话在 7.75.3 起**不再成立**;按它不渲 = 屏上与转录里留的是**未脱敏**
609
+ * 的那一份。
610
+ * · **`diverged` 在场**(never false,缺席 = 逐字节相同)⇒ 你按增量拼出来的那一段是**过期**的:
611
+ * 必须用 `content` **重渲该段**,并以它为该段的 committed 文本,**不许保留增量拼文**。
612
+ * · 🔴 **`committedPrefixDiverged` 在场**(never false)⇒ 这一段此前已经 committed 上屏的那一截
613
+ * **自己**就是过期的(凭据落在它里面),而它**撤不回**(那条 transcript 消息早已交给你)。
614
+ * 这一形下本包**一个字节都不再交**给 transcript 平面 —— 该段唯一算数的那一份就是本帧的
615
+ * `content`:宿主必须把 `committedPrefixLen` 指的那一截(见下)换成 `content`。
616
+ * 🔴 **不实现这一条 = 这一段在你的转录里只剩未脱敏的那半截**(而且缺了后半段)。
617
+ * 它**缺席**时(含压根没有已提交前缀)本包交的是**尾段** —— 前缀 + 尾段拼起来逐字节 =
618
+ * `content`,宿主**什么都不用丢**,照常把新到的 transcript 消息接在后面。
619
+ * · **`committedPrefixLen` 在场**(never 0)= 这一段已经 committed 上屏的**长度**,是上一条的
620
+ * **定位量**,照抄这一行就对:
621
+ * `已committed正文.slice(0, 已committed正文.length - committedPrefixLen) + content`。
622
+ * 🔴 **单位 = JS 字符串长度(UTF-16 代码单元),不是 UTF-8 字节** —— 按 UTF-8 字节去截会在
623
+ * 任何非 ASCII 正文上截错位置(实测 `sk-…` + `中文`×30 + 换行:UTF-16 长度 82 / UTF-8 字节 202,
624
+ * 按 202 截会把凭据**原样留在屏上**还附带乱码)。
625
+ * 🔴 **也不是「几条消息」** —— 本包在 idle-flush 那一形下会把「上一段的定稿 + 这一段的
626
+ * 半截」合并进**同一条** assistant 消息(分段行为零改动的代价),按**消息**去丢会把已经
627
+ * 定稿的上一段一起删掉,而本包不会再补发它。
628
+ * 🔴 它按**增量拼文**计长,**不是** `content` 上的偏移量 —— 前缀自己也可能被脱敏改过字节,
629
+ * 拿它去切 `content` 在 `committedPrefixDiverged` 那一形上会切出半截乱码。
630
+ * ⚠️ 🔴 **一条总不变量 + 一条如实留白**:
631
+ * **不变量** —— 本包只在「整段都在自己手里(**同一条** committed 消息内)」时改自己交的字节;
632
+ * 段一旦跨过包侧边界(工具卡 / 消息划界),就**只发这三个键、不动任何既有行为**:转录逐条、
633
+ * 终帧补差走向都与 0.68.0 **逐字相同**(常驻门按「补差臂走向」这个真正决定结果的量对照)。
634
+ * **留白** —— 因此跨消息那几形里,`committedPrefixLen` 指的那一截可能落在别条消息里、甚至
635
+ * 中间**夹着一张工具卡**,而终帧补差的基线也不会跟着换(它本来就不跟,与本批无关)。
636
+ * 两者的根因是同一个:**包按自己的边界切 committed 消息,引擎按 content block 切段**。
637
+ * 根治 = 「每个引擎段各自一条 committed 消息」(= CC 在 `content_block_stop` 上的原生做法),
638
+ * 那是**分段行为改动**、与撤 idle-flush 启发式同一件事,按宪法三问单独走;本批不做,
639
+ * 把边界如实写在这里而不是假装没有。
640
+ * · 三键**都缺席**(= 最常见的那一形)⇒ 一切照旧:`content` 只是**定界**信号,这些字节已经
641
+ * 以 `stream_delta` 流过、并由 transcript 平面给出 committed 形,**拿它再渲一行 = 同一段
642
+ * 文字上屏两遍**。
643
+ * ⚠️ 分工:`diverged` 那一条只有**自己拼 delta 上屏**的宿主要做(只渲 transcript 平面的宿主
644
+ * 自动正确 —— 本包已经把权威全文换进段缓冲);而 `committedPrefixDiverged` 那一条
645
+ * **所有宿主都要做**(包在那一形上交不出东西,只有你能修)。
646
+ * ⚠️ 🔴 **迟到的段边界**(引擎在工具执行之后才报这一段的边界 —— openai 车道的实测时序)同样
647
+ * 会带着这三个键到达:那一段的正文可能已经落在**上一条** assistant 消息里,`committedPrefixLen`
648
+ * 指的就是那一条。宿主按同一条规矩处置(丢掉 + 用 `content` 重渲),不要因为「这一段看起来
649
+ * 已经结束很久了」就跳过。
608
650
  * ② **诚实缺席,不可反推**(core 臂注逐字):只有会报块结束的 Brain 才发它 —— 三个一方 brain 都发,
609
651
  * 自定义 brain 可能整条流一帧都没有。⇒ 缺席 = 「**没报**」,**永远不等于**「这一段没结束」。
610
652
  * 消费方按**每条流**判「这条流带不带边界帧」,只在整条流一帧都没有时才回落自己的启发式;
@@ -618,14 +660,50 @@ export interface WiringManifestChromeEvent {
618
660
  * 要消费子流边界的宿主读 SDKMessage 平面的 `text_end` 内部臂,那一层原样带 `parentToolCallId`。
619
661
  *
620
662
  * ⚠️ **UNTRUSTED、仅展示**:`content` 是模型输出,契约与活体增量同 —— 渲染,绝不回喂模型。
621
- * 📋 如实留白:本包的 `takeAnswerSegmentOnIdle` 启发式**本批不撤**(撤它要按 ② 做一条 per-stream
622
- * 的状态化策略,属行为面改动,按宪法三问单独走)。本批只把边界送到宿主手上。
663
+ * 📋 如实留白:本包的 `takeAnswerSegmentOnIdle` 启发式**仍未撤**(撤它要按 ② 做一条 per-stream
664
+ * 的状态化策略,属行为面改动,按宪法三问单独走)。0.68.1 的权威段替换是**兼容**它的 ——
665
+ * 「半段已被 idle-flush 提交」那一形由 `committedPrefixLen` / `committedPrefixDiverged` 如实
666
+ * 交代,不是把启发式悄悄换掉了。
623
667
  */
624
668
  export interface TextSegmentEndChromeEvent {
625
669
  kind: 'text_segment_end';
626
670
  laneProof: LaneProof;
627
- /** 该段的**权威全文**(与那一段活体增量的拼接逐字节相等)。🔴 对账/定界用,别拿它再渲一行。 */
671
+ /**
672
+ * 该段的**权威全文**(server ≥7.75.3 起经脱敏器;**可以**与活体增量的拼接**不相等**)。
673
+ * 🔴 读法全在上面义务 ①:`diverged` 缺席时它只是对账/定界量(别拿它再渲一行);在场时它是
674
+ * 该段唯一算数的那一份。
675
+ */
628
676
  content: string;
677
+ /**
678
+ * **活体面**那份过期了(never false —— 缺席 = 与增量拼接逐字节相同)。
679
+ * 在场 ⇒ 按 {@link TextSegmentEndChromeEvent.content} 重渲该段,别保留增量拼文。
680
+ */
681
+ diverged?: true;
682
+ /**
683
+ * **转录面**已经 committed 的那一截**自己**也过期了(never false)。
684
+ * 🔴 在场 ⇒ 本包**一个字节都不再交**给转录面(撤不回的那截拼不出 `content`,再补一条就是同一段话
685
+ * 上屏两遍)⇒ 该段唯一算数的那一份就是 {@link TextSegmentEndChromeEvent.content},宿主把
686
+ * {@link TextSegmentEndChromeEvent.committedPrefixLen} 指的那一截换成它;
687
+ * 缺席 ⇒ 本包交的是**尾段**,前缀 + 尾段 = `content`,什么都不用丢。
688
+ */
689
+ committedPrefixDiverged?: true;
690
+ /**
691
+ * 这一段已经 committed 上屏的**长度**(never 0)——「要换的是哪一段」的定位量:
692
+ * `已committed正文.slice(0, 已committed正文.length - committedPrefixLen) + content`。
693
+ * 🔴 **单位 = JS 字符串长度(UTF-16 代码单元),不是 UTF-8 字节**(按字节截会在非 ASCII 正文上
694
+ * 截错位置,凭据会原样留在屏上);🔴 **也不是「几条消息」**(本包会把上一段的定稿与这一段的
695
+ * 半截合并进同一条消息);
696
+ * 🔴 按**增量拼文**计长,不是 `content` 上的偏移量(见义务 ①)。
697
+ */
698
+ committedPrefixLen?: number;
699
+ /**
700
+ * **段身份**(CC-01,0.68.2)—— 与本段已过境的 committed assistant 文本行顶层 `_sema_segment_id`
701
+ * **同值**(本包在 `adapt()` 出口盖;语义与铸法见 `SEMA_SEGMENT_ID_KEY` 头注)。
702
+ * 🔴 宿主按 {@link TextSegmentEndChromeEvent.committedPrefixLen} 换掉已提交那一截时,认行的钥匙是
703
+ * **两把**:行 `uuid` ∧ 行上的段身份 === 本键(按字节相等找行会被同后缀的独立行冒充)。
704
+ * 恒在场(本包每条 leader 段边界都带);它**不是** wire 键 —— `eventId` 才是引擎铸的事件身份。
705
+ */
706
+ segmentId: string;
629
707
  /** core 铸的事件身份(uuidv7 形);wire 未必带 ⇒ 缺席时本键不在场。 */
630
708
  eventId?: string;
631
709
  }
package/dist/seam.js CHANGED
@@ -110,8 +110,22 @@ const CHROME_ARM_TABLE = {
110
110
  '(撤帧可能整帧丢失)③已 DECIDED 的兄弟不在 askIds 里;真撤到本地卡时给一行归因(reason 消毒后呈现),零命中不要多说那一行',
111
111
  },
112
112
  text_segment_end: {
113
- required: false,
114
- duty: '可选:引擎明报的 assistant 散文段边界(#323/core #447)。🔴 content 是对账/定界用的权威全文,拿它再渲一行 = 同一段上屏两遍;缺席只表示「没报」,绝不等于「段没结束」——要退回自家启发式必须按整条流判、不按单帧判',
113
+ // 🔴 0.68.1 / L-310:`false` → `true`。改的理由不是「这一面更重要了」,是**后果类目变了**:
114
+ // 本表的 `false` 语义逐字是「不接 = 这条披露看不见,不属『已发生的行为丢失』」,而 server
115
+ // 7.75.3 起不接的后果是**未脱敏字节留在本地转录里**(b2 那一形上,那条明文消息**已经渲过**
116
+ // ⇒ 就是「已发生的行为」)。留 `false` = 把一条安全面义务标成可选,正是本仓点名要治的
117
+ // 「假 affordance / 悄悄的谎」。⚠️ 这一位翻面会让按本表自检覆盖率的端**当场显形**——那正是目的。
118
+ required: true,
119
+ duty: '🔴 引擎明报的 assistant 散文段边界 + **该段权威全文**(#323/core #447;server ≥7.75.3 起 content 经脱敏器、' +
120
+ 'text_delta 仍逐字 ⇒ 两者**可以不相等**)。①`diverged` 在场(never false)⇒ 你按增量拼出来的那一段是过期的,' +
121
+ '必须用 content **重渲该段**并以它为该段 committed 文本,不许保留增量拼文;②`committedPrefixDiverged` 在场' +
122
+ '(never false)⇒ 这一段此前已 committed 上屏的正文**也**过期了,🔴 本包在这一形上**一个字节都不再交**' +
123
+ '(撤不回的那截拼不出 content)⇒ 把 committedPrefixLen 指的那一截换成 content,这一条**所有宿主都要做**;' +
124
+ '缺席 ⇒ 本包交的是尾段,前缀+尾段=content,什么都不用丢;③`committedPrefixLen`(never 0)= 定位量,' +
125
+ '🔴 **单位 = JS 字符串长度(UTF-16 代码单元),不是 UTF-8 字节、也不是几条消息**' +
126
+ '(按 UTF-8 字节截会在非 ASCII 正文上截错位置把凭据留在屏上;按消息丢会连合并在同一条消息里的上一段定稿一起删掉),' +
127
+ '照抄 `已committed正文.slice(0, 长度 - committedPrefixLen) + content`;按**增量拼文**计长、不是 content 上的偏移;④三键全缺席 ⇒ 照旧只当对账/定界,拿 content 再渲一行 = 同一段上屏两遍;⑤`segmentId`(恒在场)= 段身份,与本段已过境的 committed 文本行顶层 `_sema_segment_id` 同值 —— 换掉已提交那截时认行的钥匙是**两把**:行 uuid ∧ 段身份(按字节相等找行会被同后缀的独立行冒充)。' +
128
+ '🔴 缺席只表示「没报」,绝不等于「段没结束」——要退回自家启发式必须按整条流判、不按单帧判',
115
129
  },
116
130
  };
117
131
  /**
@@ -28,8 +28,16 @@
28
28
  * 而且它把「位置从名册来、永远不从 summary 来」这条契约反过来用了。
29
29
  *
30
30
  * -- 开集读 -----------------------------------------------------------------------------------
31
- * `source`(七词)/ `effect` / `family` / `access` 等词表的属主都是引擎,一律**原样透传**,不窄读成
32
- * 枚举 —— 那会在引擎加词当天把一份真名册判没。
31
+ * `source`(七词)/ `effect` / `family` / `access` / 三个 `*Provenance` 等词表的属主都是引擎,一律
32
+ * **原样透传**,不窄读成枚举 —— 那会在引擎加词当天把一份真名册判没。
33
+ *
34
+ * -- 🔴 逐键窄读器 vs 上游的逐字透传(L-290 的病形,0.68.1)-----------------------------------
35
+ * server(`src/trace/project.ts` 的 `tools` 段)对名册的姿势是**整只判形 + 逐字透传**,理由逐字:
36
+ * 成员表由 core 以 typebox schema 单点持有,手抄一张挑键表 = 立刻多一份会漂的镜像。本读器**确实**
37
+ * 是那份镜像(本包要给三端一个窄化过的视图,不能把 30 余键的开集原样甩出去)—— 所以镜像必须有账:
38
+ * `scripts/run-tool-roster-projection-test.mjs` **G 段**拿**实装 core 的 schema 成员表**与本读器
39
+ * 真挑出来的键集逐键对账,core 的每一个成员要么被挑、要么在账上写明「为什么不挑」;上游加一个
40
+ * 成员而账上没有 ⇒ 当天红。没有这本账,「挑漏一个键 = 那个键在包边界上不存在」就会一直无声发生。
33
41
  */
34
42
  /** 一只工具的**路径目标**(引擎的 `pathTarget`;`base`/`absent`/`patternParam` 是 core 7.9.1 #635 加的)。 */
35
43
  export interface ToolRosterPathTargetView {
@@ -78,10 +86,34 @@ export interface ToolRosterEntryView {
78
86
  capabilityId?: string;
79
87
  /** `read` / `write` / `idempotent`;开集读。 */
80
88
  effect?: string;
89
+ /**
90
+ * `effect` 这根轴**是谁说的**(`declared` 定义自报 / `caller` 调用方声明 / `synthetic` 引擎合成 /
91
+ * `default` 谁都没说,落默认);**开集读**。见下面 {@link ToolRosterEntryView.contentOriginProvenance}
92
+ * 的头注 —— 三个出身键同生同灭。
93
+ */
94
+ effectProvenance?: string;
81
95
  egress?: boolean;
96
+ /** `egress` 这根轴的出身(`declared` / `caller` / `default`);**开集读**,读法同上。 */
97
+ egressProvenance?: string;
82
98
  /** `never` / `maybe` / `always`;开集读。 */
83
99
  irreversibility?: string;
84
100
  contentOrigin?: string;
101
+ /**
102
+ * `contentOrigin` 这根轴的出身(`declared` 定义自报 / `server` mcp·a2a 行由服务端定 /
103
+ * `exempted` 在受信工具豁免名单里 / `default` 谁都没说);**开集读**(L-290,0.68.1)。
104
+ *
105
+ * 🔴 **为什么要有这一位**:轴的**值**说的是「这只工具的产出算不算外来内容」,而一个
106
+ * `contentOrigin: "local"` 到底是**工具自己声明**的、还是**因为它在豁免名单里**才这样 ——
107
+ * 在信任面上是两件事。诊断行(壳 `/doctor` 的「Tools (engine leg)」)读的正是后者。
108
+ * 🔴 **族扫(同形存量清剿)**:server `src/trace/project.ts` 的 `tools` 段是**整只判形 + 逐字
109
+ * 透传**,而本读器是**逐键挑** —— 挑漏的键在包边界上等于不存在。三根轴各有一个出身键,漏的形
110
+ * 一模一样,所以三个一起挑,不是只补被点名的那一个;核过之后 core `ToolRosterEntry` 的**每一个**
111
+ * 成员在 `scripts/run-tool-roster-projection-test.mjs` G 段都有一行处置(挑 / 不挑 + 理由),
112
+ * 上游再加成员当天红。
113
+ * ⚠️ 出身键是**诊断面**,刻意**不进** {@link ToolShim}(那是渲染面:人话名 / 卡型 / 三根轴的
114
+ * **值**)—— 端渲一只工具时要的是「它是什么」,不是「这句话是谁说的」。
115
+ */
116
+ contentOriginProvenance?: string;
85
117
  family?: string;
86
118
  pathTarget?: ToolRosterPathTargetView;
87
119
  renderHints?: ToolRosterRenderHintsView;
@@ -28,8 +28,16 @@
28
28
  * 而且它把「位置从名册来、永远不从 summary 来」这条契约反过来用了。
29
29
  *
30
30
  * -- 开集读 -----------------------------------------------------------------------------------
31
- * `source`(七词)/ `effect` / `family` / `access` 等词表的属主都是引擎,一律**原样透传**,不窄读成
32
- * 枚举 —— 那会在引擎加词当天把一份真名册判没。
31
+ * `source`(七词)/ `effect` / `family` / `access` / 三个 `*Provenance` 等词表的属主都是引擎,一律
32
+ * **原样透传**,不窄读成枚举 —— 那会在引擎加词当天把一份真名册判没。
33
+ *
34
+ * -- 🔴 逐键窄读器 vs 上游的逐字透传(L-290 的病形,0.68.1)-----------------------------------
35
+ * server(`src/trace/project.ts` 的 `tools` 段)对名册的姿势是**整只判形 + 逐字透传**,理由逐字:
36
+ * 成员表由 core 以 typebox schema 单点持有,手抄一张挑键表 = 立刻多一份会漂的镜像。本读器**确实**
37
+ * 是那份镜像(本包要给三端一个窄化过的视图,不能把 30 余键的开集原样甩出去)—— 所以镜像必须有账:
38
+ * `scripts/run-tool-roster-projection-test.mjs` **G 段**拿**实装 core 的 schema 成员表**与本读器
39
+ * 真挑出来的键集逐键对账,core 的每一个成员要么被挑、要么在账上写明「为什么不挑」;上游加一个
40
+ * 成员而账上没有 ⇒ 当天红。没有这本账,「挑漏一个键 = 那个键在包边界上不存在」就会一直无声发生。
33
41
  */
34
42
  const str = (v) => typeof v === 'string' && v.length > 0 ? v : undefined;
35
43
  const strArr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === 'string') : undefined;
@@ -91,9 +99,15 @@ function readEntry(raw) {
91
99
  ...(str(o.cardId) !== undefined ? { cardId: str(o.cardId) } : {}),
92
100
  ...(str(o.capabilityId) !== undefined ? { capabilityId: str(o.capabilityId) } : {}),
93
101
  ...(str(o.effect) !== undefined ? { effect: str(o.effect) } : {}),
102
+ // L-290 族扫:三根轴各自的**出身**键(开集读,`str()` 同律;坏形只丢这一格,行还在 —— 出身不是身份)。
103
+ ...(str(o.effectProvenance) !== undefined ? { effectProvenance: str(o.effectProvenance) } : {}),
94
104
  ...(typeof o.egress === 'boolean' ? { egress: o.egress } : {}),
105
+ ...(str(o.egressProvenance) !== undefined ? { egressProvenance: str(o.egressProvenance) } : {}),
95
106
  ...(str(o.irreversibility) !== undefined ? { irreversibility: str(o.irreversibility) } : {}),
96
107
  ...(str(o.contentOrigin) !== undefined ? { contentOrigin: str(o.contentOrigin) } : {}),
108
+ ...(str(o.contentOriginProvenance) !== undefined
109
+ ? { contentOriginProvenance: str(o.contentOriginProvenance) }
110
+ : {}),
97
111
  ...(str(o.family) !== undefined ? { family: str(o.family) } : {}),
98
112
  ...(readPathTarget(o.pathTarget) !== undefined
99
113
  ? { pathTarget: readPathTarget(o.pathTarget) }