dsh-punky-swarm 0.5.0 → 0.5.1
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 +31 -1
- package/lib/assembly/schema.js +71 -2
- package/lib/comms/inject-budget.js +194 -0
- package/lib/engine/dispatch.js +191 -7
- package/lib/engine/replay-verify.js +151 -0
- package/lib/engine/suite.js +22 -0
- package/lib/hot/config-watch.js +12 -1
- package/lib/index.js +19 -3
- package/lib/sig-fingerprint.js +1 -1
- package/lib/state/event-types.js +16 -0
- package/lib/state/gates/command.d.ts +220 -1
- package/lib/state/gates/command.js +132 -8
- package/lib/state/gates/command.ts +121 -7
- package/lib/state/gates/detect.d.ts +9 -0
- package/lib/state/gates/detect.js +70 -0
- package/lib/state/gates/detect.ts +62 -0
- package/lib/state/gates/handoff.js +11 -3
- package/lib/state/gates/handoff.ts +11 -3
- package/lib/state/gates/helpers.d.ts +8 -0
- package/lib/state/gates/helpers.js +43 -5
- package/lib/state/gates/helpers.ts +43 -5
- package/lib/state/gates.d.ts +228 -1
- package/lib/state/gates.js +5 -1
- package/lib/state/gates.ts +5 -1
- package/lib/state/machine-rules.d.ts +6 -0
- package/lib/state/machine-rules.js +47 -3
- package/lib/state/machine-rules.ts +56 -4
- package/lib/state/rule-propagation.d.ts +22 -0
- package/lib/state/rule-propagation.js +119 -0
- package/lib/state/rule-propagation.ts +137 -0
- package/lib/state/store/handoff-accept.js +173 -0
- package/lib/state/store/member-gates.js +160 -1
- package/lib/state/store/plan-mutate.js +9 -1
- package/lib/state/store.js +139 -6
- package/lib/tools/asset-tools.js +5 -2
- package/lib/tools/assign-tools.js +135 -46
- package/lib/tools/handoff-tools.js +150 -8
- package/lib/tools/lane-tools.js +24 -14
- package/lib/tools/log-tools.js +2 -2
- package/lib/tools/mailbox-tools.js +4 -4
- package/lib/tools/member-tools.js +11 -5
- package/lib/tools/query-tools.js +11 -3
- package/lib/tools/register.js +17 -1
- package/lib/tools/shared.js +21 -2
- package/lib/tools/swarm-comms.js +44 -5
- package/lib/tools/swarm-tools.js +35 -5
- package/lib/tools/task-tools.js +6 -1
- package/lib/tools/tier-gate.js +35 -1
- package/lib/tools/wave-plan-tools.js +19 -8
- package/lib/types/contracts.d.ts +1 -1
- package/lib/types/contracts.ts +8 -0
- package/package.json +2 -2
- package/presets/punky-preset/agent.cordis.yml +1 -2
- package/presets/punky-preset/references/discipline.md +94 -13
- package/skills/video-team/SKILL.md +307 -0
- package/skills/video-team/references/constitution.md +74 -0
- package/skills/video-team/references/roles/audio-engineer.md +81 -0
- package/skills/video-team/references/roles/captioner.md +80 -0
- package/skills/video-team/references/roles/coordinator.md +77 -0
- package/skills/video-team/references/roles/designer.md +80 -0
- package/skills/video-team/references/roles/motion-designer.md +80 -0
- package/skills/video-team/references/roles/producer.md +83 -0
- package/skills/video-team/references/roles/supervisor.md +86 -0
- package/skills/video-team/references/templates/final-acceptance-template.md +62 -0
- package/skills/video-team/references/templates/gap-list.json +12 -0
- package/skills/video-team/references/templates/leader-decision-pack.md +62 -0
- package/skills/video-team/references/templates/scene-spec-template.md +73 -0
- package/skills/video-team/references/templates/storyboard-template.md +41 -0
- package/skills/video-team/references/templates/success-pattern-seeds.md +42 -0
- package/skills/video-team/references/workflow.md +132 -0
- package/skills/video-team/scripts/check-video-team-density.mjs +497 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,34 @@
|
|
|
1
|
-
##
|
|
1
|
+
## 0.5.0(2026-10-12)
|
|
2
|
+
## 0.5.1(2026-10-14)
|
|
3
|
+
|
|
4
|
+
### 抄规格体系 P0/P1/P2 三波次 + 三个已验收外部批(`a4a2779`,91 件 / +9407 −236)
|
|
5
|
+
|
|
6
|
+
- **范围**:一次收口提交,含三批主交付(P0 抄规格 L1 事件重放校验腿 + L2 上下文注入预算;P1 抄规格 L3 测试证据自证 / L5 隔离可判定 / L6 宿主兼容第三维;P2 抄规格 L7 写路径原子性 / L4 规则变更传播 / L8 判据脚本信任模型 / L9 记忆溯源 / C-5 坏配置 fail-closed)与**三个已验收外部批**(各自 `verdict = approve`、`blocking = 0`)。
|
|
7
|
+
- **要点**:新增 1 枚拒码(`GATE_SCRIPT_CHANGED`,语义独立于只读黑名单命中,以保审计可辨);`lib/comms/inject-budget.js` 新建(四层结构 + 显式省略标记);`lib/state/rule-propagation.ts` 新建(规则变更传播,随 `scripts/copy-ts-built.mjs` 回拷清单同步)。
|
|
8
|
+
- **读数**:`91 files changed, 9407 insertions(+), 236 deletions(-)`。
|
|
9
|
+
|
|
10
|
+
### 指引同步改造 —— 10 项 + 2 附带(`431b873`,2 件 / +77 −7)
|
|
11
|
+
|
|
12
|
+
- **范围**:把 L1~L9 的规格口径落进指引(正文 `references/discipline.md`)+**移除注入面 `0i` 行**(挂起内容不得在注入面出现)。
|
|
13
|
+
- **要点**:判据类落 `### 15.16`–`### 15.20`、操作类入各自同族 §;**不新增 `## §X`**(故不影响注入面↔正文双向检查);收口以 `node scripts/check-guidance-sync.mjs` **GREEN(exit 0)** 为准。
|
|
14
|
+
- **读数**:`2 files changed, 77 insertions(+), 7 deletions(-)`。
|
|
15
|
+
|
|
16
|
+
### 未发布补录:D 档偏离语义修复 + 引擎自派 worker 批归属索引填充面修复(2026-10-12,批 `session-bind-index-fix-20261012`;同段补录前批 `d-override-fix-20261011`)
|
|
17
|
+
|
|
18
|
+
- **形态**:本批 `session-bind-index-fix-20261012` = 5 lane(`p1-spec` →(`e1-fix` → `e2-assert`)∥ `e3-changelog` → `a1-audit`);前批 `d-override-fix-20261011` = 9 lane(`p1-spec` → `e1-engine` ∥ `e2-guidance` ∥ `e3-tests` ∥ `e4-session-bind` ∥ `e5-c1-schema-fix` ∥ `e6-index-wiring` ∥ `e7-assert-align` → `a1-audit`),`audit verdict = approve`(AC-1..AC-20 **100% 有结论** / **blocking 0** / followup 12)· **带 gap 收口**(非 clean-pass);前批全量读数(a1 独立复跑,非采信 lane 自述):`npm test` tests **1765 / pass 1761 / fail 0** exit 0 · `npm run check` exit 0 · `node scripts/check-guidance-sync.mjs` exit 0 · `ac19-host-smoke.mjs` **15/15**。
|
|
19
|
+
- **前批 ① · 双轴判据(D 档偏离语义修复的根)**:原语义「声明 D ⇒ 恒 `override:true`」把合规 D 误判为偏离。改为**两轴独立推导、互不耦合**——**难度轴** `derived` = 五布尔 + 两个新布尔的机械推导(任一 C 判据 ∥ `fixedRole` ∥ `durableSeats` → `C`;否则 `needIsolation` → `B`;否则 `A`;**`D` 不入难度轴**,因通道不属难度);**通道轴** `derivedChannel` = `durableSeats === true ? 'team' : 'dispatch'`。落点 `lib/tools/assign-tools.js:107-109`;两实参入 schema 且**均非必填**(缺省零破坏:不传时退化为原 `derived` 且恒走 tier 支)。
|
|
20
|
+
- **前批 ② · D 的正当形态 ⇒ `override:false`**:`difficulty:'D'` **且** 通道轴为 `team`(即 `durableSeats:true`)⇒ **回归常规路径**(`override:false` / `overrideKind:null`),rationale **不需要**引用 A/B/C 特征词 —— 这正是「D 被机械事实覆盖 ⇒ 不记偏离」的修复点。落点 `lib/tools/assign-tools.js:121-122`;真宿主读数(`ac19-host-smoke.mjs` ⇒ `total=15 pass=15 fail=0`):`{"derived":"C","derivedChannel":"team","override":false,"overrideKind":null}`,工具行**无** `[override(...)]` 标记,`governance.json#lastAssign` 逐键一致。
|
|
21
|
+
- **前批 ③ · D 通道变体 ⇒ 通道三要素**:`difficulty:'D'` 但通道轴为 `dispatch`(`durableSeats` 未声明 ⇒ 通道面未落实)⇒ **唯一保留的偏离**:`override:true` / `overrideKind:'channel'`,rationale 须含**通道三要素**(**① 引用通道/特征面 ② 为何 durable 而非 subagent ③ 通道方向与上限:不泛化为全批、team 面缺失须显式降级**),缺任一即拒 `GATE_DIFFICULTY_MISMATCH`(文案说「**通道偏离**」并逐项列缺,附两条正范式);**不**复用原三要素(其 ① 强制 A/B/C 特征词,正是要绕开的误拒源)。非 D 偏离**逐字保留**原路径(`overrideKind:'tier'` + 原三要素)。落点 `lib/tools/assign-tools.js:124-138`。
|
|
22
|
+
- **前批 ④ · 建批面 D×通道硬拒 + guard 硬消费**:`lib/tools/wave-plan-tools.js:96` 在 `createBatch` **之前** throw `GATE_DIFFICULTY_CHANNEL_UNRESOLVED`(本会话声明 D 档而本批 `channel:'dispatch'` ⇒ **D 与通道面不一致,拒建批**,**零批次 JSON 落盘**、`pendingBatch` 保留可重试);执行型工具面由 `lib/tools/tier-gate.js:221-234` **同源硬消费**同一码(只读壳调用判定**先于**该门 ⇒ 侦察面零阻断)。真宿主读数(前批 `G-01` 运行时半):拒态逐字命中 + 零落盘。
|
|
23
|
+
- **前批 ⑤ · C↔D 同档豁免**:会话镜像的**跳过判据**由强度序 `tierRank` 改用**准入档单点** `accessTierRank` —— `C` 与 `D` **同档** ⇒ 「D 会话被镜像 C」**不再跳过**(照常镜像),仅**跨档降档**才跳过;修掉「活跃批期间 D→C 必被 `GATE_DIFFICULTY_DOWNGRADE_BLOCKED` 误拦」的实测缺陷。落点 `lib/tools/assign-tools.js:215-225`(禁两处各写一套判据);真宿主读数:放行、无该码。
|
|
24
|
+
- **前批 ⑥ · C-1(`overrideKind` schema 可空)**:`assign_check` 的 `output.schema` 中 `overrideKind` 由 `{"type":"string"}` 改为 `oneOf:[{type:'string',enum:['channel','tier']},{type:'null'}]`(前批 lane `e5-c1-schema-fix`)——常规路径回 `null` 不再被**宿主同款**校验判违例(原 schema 使合规 D 的 `null` 读数被判违反输出契约 = 阻断缺陷 C-1)。落点 `lib/tools/assign-tools.js:60`;宿主同款校验**零违例**。
|
|
25
|
+
- **前批 ⑦ · session 绑定形态落地 + 指引同步**:批归属解析**单点** `lib/engine/dispatch.js#batchSessionIdOfWorker`(`worker 会话 → 批归属会话` 的**唯一**反查点,只 `get`、取不到即 `null` 不猜兜底)+ `lib/tools/shared.js#sessionOf` **四级回落**(`args.session` → 注入 resolver 命中 ⇒ 批归属会话 → `exec.agent.session.id` → `'cli'`;`shared.js` 守**零 import** 红线、resolver 只收**注入的函数**、抛错/非字符串视为不命中);装配面 `lib/index.js:307` 把 `dispatchIndex` 声明**前移** + `:313` 闭包注入 `resolveBatchSession`(捕获**引用**而非快照值),并完成批内治理工具(`lane-tools` / `query-tools` / `task-tools` / `member-tools` / `mailbox-tools` / `handoff-tools` / `asset-tools` / `log-tools`)的三参化接线(前批 lane `e6-index-wiring`)。**指引同步**:`presets/punky-preset/references/discipline.md:25` 写入 D 档偏离口径(双轴 / 正当形态 / 通道三要素 / 建批面仍须落实 `channel:'team'`,否则拒 `GATE_DIFFICULTY_CHANNEL_UNRESOLVED`),`:393` 后续项同步;`node scripts/check-guidance-sync.mjs` **GREEN**(repo == live,前批 a1 独立复跑)。
|
|
26
|
+
- **本批 · 现象与根因**:引擎**自派**的 worker(子会话)**不传 `session`** 调治理工具时一律回落自身子会话 ⇒ `Error: batch not found: <batchId> @<workerSessionId>`;`lane_checkpoint`(写)与只读 `batch_status` **同形失败**,lane worktree **零提交**。根因 = **索引填充面缺口**(**不是**接线逻辑错误,前批已逐条排除「宿主未加载新码」「闭包 Map ≠ 登记 Map」两候选):内存索引 `dispatchIndex` 的三条既有填充路径在自派场景**全部缺席** —— ① 启动重建(`lib/index.js:545-563`)**只跑一次**且早于批创建;② 热登记(`lib/bridge/dispatch-register.js:108`)只订阅宿主 `tools/post-execute` 且名单 `DEFAULT_DISPATCH_TOOLS = ['subagent','subagent_fork']`;③ **引擎自派写点**(`lib/engine/dispatch.js:884`)虽为「唯一写路径」写 `member.dispatch` 事件,却**不写 `dispatchIndex`** ⇒ 「**事件在盘上、索引里没有**」,resolver 恒未命中。旁证:同一份绑定数据在**磁盘扫描路命中**(`laneBindingOf`,`swarm_report` 回执自证)而**内存索引路未命中** ⇒ 失败点收敛在**填充面**,非绑定数据缺失。
|
|
27
|
+
- **本批 · 修复(三处)**:① **自派同步写(主路,确定性根源修复)**:自派成功写 `member.dispatch` 事件的**同一段同步代码内**同步 `dispatchIndex.set(...)`,消除「事件有、索引无」的分裂(`lib/engine/dispatch.js:884` 侧);② **未命中回填(兜底)**:resolver **未命中内存索引**时回落**扫事件流反查**(复用同文件既有 `laneBindingOf` 的 `member.dispatch` 判据),命中后**回填同一索引**(经 `dispatchIndexEntryOf` **单点构造**,保持既有 3 键形状与非枚举 `ownerSessionId`);③ **登记名单(防御性)**:`DEFAULT_DISPATCH_TOOLS` 补 `lane_dispatch` —— 引擎自派路径**直接调 `rt.start`**、**不经** `tools/post-execute` ⇒ 本条**不产生覆盖面**,仅为防御性登记,**不得**作为本缺陷的补救依据。**索引填充五路时序**(本批核心关系):① 启动重建(存量回溯,仅 t0)→ ② 热登记(宿主工具面增量)→ ③ **自派同步写(本批新增主路)** → ④ 结算路惰性重建(既有先例,本批**不动**)→ ⑤ **未命中回填(本批新增兜底)**;③ **先于** ⑤(自派写点在派发返回前同步落 Map ⇒ worker 首个治理调用即命中,⑤ 不触发);③⑤ **共用同一值构造单点**(冷/热读数逐字相同);⑤ **复用** ①④ 的同一事件事实源、**不新建第二套索引**,且是 ① 的**运行期补位**(① 时窗仅启动,由 ⑤ 在运行期补齐,两者判据同源、幂等同形)。
|
|
28
|
+
- **兼容性(红线,逐条钉死)**:`lib/tools/shared.js#sessionOf` 四级回落**顺序逐字不变**、`'cli'` 兜底取值式不变;**未命中且无反查数据源**(无 `store` / 反查无命中 / 反查抛错)⇒ **逐字**保持既有回落,Leader 自身行为**零变化**;`batchSessionIdOfWorker` **纯函数契约不动**(只 `get`、只认 `Map`);worker→批 反查仍**单点**(无第二套索引、**无负缓存**);索引值形状**冻结**(`Object.keys(entry)` 恰 `['sessionId','batchId','lane']`);回填**幂等**(同键覆盖 ⇒ 键数不增、不产生重复事件);导出面 / 事件常量 / `GATE_*` 码**净增 0**。⚠ 前批教训(已写入前批记录):**接线形态 ≠ 运行时连通** —— 不得以「接线在源码面在位」或「手塞 deps 的探针通过」代替数据面读数。
|
|
29
|
+
- **验证面**:单元判据(冷索引反查命中 + Map 事后含键可核 / 自派返回后**无需任何反查**即可查 / 索引形状与单点结构锁 / 回填幂等)交本批 `e2-assert` 承接并留读数;**宿主级 DoD 尚未执行**(见下「未闭合项」)。
|
|
30
|
+
- **⚠ 未闭合项(如实登记,不美化)**:**worker 侧自动会话解析的宿主级验证,在前批 `d-override-fix-20261011` 批末未通过** —— 前批 `e4-AC-8` 实拒 `Error: batch not found: smoke-session-bind-20261012 @b6d520d3-7d5a-443a-a652-7db7cc1a678f`,只读 `batch_status` 同形失败、lane worktree 零提交;**根因 = 上述索引填充面缺口**(前批按用户裁决**另开本批修**、以**带 gap** 形态收口,未冒认通过)。⇒ **本批修复后需重启宿主复验**:一次重启后真宿主演一遍 —— 引擎**自派** worker **刻意不传 `session`** 调 `lane_checkpoint` **成功提交**(事件流出现 `worktree.checkpoint`、`git log` 可见提交),并附**只读工具同形复现**(`batch_status` 不传 `session` 应可达)作为**数据面被真实填充**的证据;**补 `session` 参数重试的那次调用不属于判据**。前批另有一项未闭合(一并如实登记):建批面**非 dispatch 通道**(team 系)的**放行**半**未取运行时读数**(前批为 dispatch 通道,同批无法验)。
|
|
31
|
+
- **不升版声明 + 生效面**:本段两条批**均不升版** —— `package.json` 的 `version` **逐字保持 `0.5.0`**(Q-F:补条目但**不升版**;本批**未**改写版本号、**未**打 tag、**未**发布)。引擎代码(`lib/**`)与数据面(`presets/punky-preset/references/discipline.md`)⇒ 随宿主重启一次生效。
|
|
2
32
|
|
|
3
33
|
### 清理波:构建面收口(TD-09 + TD-21/N-13 + G-2)与长尾清理并行(2026-09-18,批 `cleanup-wave-20260918`)
|
|
4
34
|
|
package/lib/assembly/schema.js
CHANGED
|
@@ -24,8 +24,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
24
24
|
import { WATCH_DEFAULTS, TRAJECTORY_DEFAULTS, VERIFY_DEFAULTS, DISCOVERY_DEFAULTS, ACPS_DEFAULTS } from '../schema.js';
|
|
25
25
|
import { AUDITLOG_DEFAULTS } from '../auditlog/config.js';
|
|
26
26
|
|
|
27
|
-
// ── 能力注册表(
|
|
28
|
-
// path = config 取值路径;default = 缺省值(
|
|
27
|
+
// ── 能力注册表(13 键:aip/identity/discovery/verify/watch/worktree/budget/injectBudget/trajectory/acps/auditlog/logs/topic)──
|
|
28
|
+
// path = config 取值路径;default = 缺省值(9 键默认开——aip/discovery/verify/watch/worktree/budget/injectBudget/trajectory/auditlog;
|
|
29
29
|
// 显式 enabled:false 可逐键关闭;4 键默认关——identity/acps/logs/topic,需显式开启);
|
|
30
30
|
// consumers = 既有消费点(键路径一致性依据)
|
|
31
31
|
// identity 键:身份体系默认关——config.aip.identity.enabled === true 时
|
|
@@ -43,6 +43,12 @@ export const CAPABILITY_REGISTRY = [
|
|
|
43
43
|
{ key: 'watch', path: ['capabilities', 'watch'], default: WATCH_DEFAULTS, consumers: ['index.js watchdog + watch/lane-heartbeat.js'] },
|
|
44
44
|
{ key: 'worktree', path: ['capabilities', 'worktree'], default: { enabled: true, mergeAgent: { enabled: false, model: null, timeoutMs: 600000 } }, consumers: ['tools/lane-tools.js 三工具注册'] },
|
|
45
45
|
{ key: 'budget', path: ['capabilities', 'budget'], default: { enabled: true, maxChainHops: 4, maxChainRoundTrips: 2 }, consumers: ['comms/budget.js + mailbox-tools.js 发送检查'] },
|
|
46
|
+
// injectBudget 键:注入面字节预算(四层:每源软上限 / 总量硬上限 / head-tail 截断 / 显式省略标记)。
|
|
47
|
+
// 与上一行 budget 键**语义正交、互不复用**:budget = mailbox 链环防护(hop/round-trip),本键 = 注入文本字节上限。
|
|
48
|
+
// 默认开 ⇒ 与「零回归靠未超限不动作、不靠默认关」的取值口径一致(无需进测试 OFF_KEYS)。
|
|
49
|
+
// 阈值取值唯一源 = 本条目;实现侧禁止散落字面量。位宽对齐宿主日志行物理上限(auditlog 的 MAX_LINE_BYTES)。
|
|
50
|
+
// 消费点:engine/dispatch.js 任务包、tools/handoff-tools.js 交接注入、tools/swarm-comms.js lane 回报。
|
|
51
|
+
{ key: 'injectBudget', path: ['capabilities', 'injectBudget'], default: { enabled: true, perItemBytes: 3072, perBlockBytes: 12288, totalBytes: 32768, headBytes: 2048, tailBytes: 1024 }, consumers: ['comms/inject-budget.js + engine/dispatch.js + tools/handoff-tools.js + tools/swarm-comms.js'] },
|
|
46
52
|
{ key: 'trajectory', path: ['capabilities', 'trajectory'], default: TRAJECTORY_DEFAULTS, consumers: ['index.js 桥接订阅'] },
|
|
47
53
|
// logs 键:日志导出工具注册开关——默认关(与 cordis.patch.yml 显式 logs.enabled:true 相合);
|
|
48
54
|
// 消费点经 readCapability(config,'logs') 缺省读取,显式 enabled:false 可关。
|
|
@@ -93,6 +99,55 @@ export function readCapability(config, key) {
|
|
|
93
99
|
return deepMerge(entry.default, val);
|
|
94
100
|
}
|
|
95
101
|
|
|
102
|
+
// ── C-5 · 坏配置加载期 fail-closed(消除静默降级)────────────────────────────
|
|
103
|
+
// 存在理由:`capabilities.injectBudget` 的五个阈值此前**无任何校验** ⇒ `runtime.json` 写
|
|
104
|
+
// `{enabled:true,totalBytes:-1}` 会**通过白名单后静默降级**(`inject-budget.js` 的「无可用阈值 ⇒ 零动作」
|
|
105
|
+
// 是**契约**,故不会报错,只会静默地把整条预算关掉)⇒ 本组校验把该形态改为**可见的拒绝**。
|
|
106
|
+
// 语义边界(**不得混淆**):本组只管「**坏配置**」;`inject-budget.js` 中「未超限 / 开关关 / 无可用阈值 ⇒
|
|
107
|
+
// 返回入参原值本身、零省略」是**文档化契约**(`:165` 逐字),**不是**本组要消除的对象 —— 该件一字不动。
|
|
108
|
+
//
|
|
109
|
+
// 五个阈值字段(**唯一字段清单**;禁第二套字面量表)。
|
|
110
|
+
export const INJECT_BUDGET_LIMIT_FIELDS = Object.freeze([
|
|
111
|
+
'perItemBytes',
|
|
112
|
+
'perBlockBytes',
|
|
113
|
+
'totalBytes',
|
|
114
|
+
'headBytes',
|
|
115
|
+
'tailBytes',
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* **单一判定点**(`DEP-N` 值校验):`injectBudget` 是否为**可用的有限正数阈值**。
|
|
120
|
+
* 两个调用方共用本函数 ⇒ 启动期与热更通道**同判、文案同源、零漂移**:
|
|
121
|
+
* · `validateCapabilities`(本文件;经 `lib/index.js` 装配期 warn)
|
|
122
|
+
* · `validateOverlay`(`lib/hot/config-watch.js`;**热更通道**,拒则整份 overlay 不采用)
|
|
123
|
+
*
|
|
124
|
+
* 判定口径:
|
|
125
|
+
* · `enabled !== true` ⇒ **零校验**(与 DEP-1..DEP-5 同形:禁用能力零校验零开销);
|
|
126
|
+
* · 五字段**逐个**:`!== undefined` 才查(缺省不误伤),须为**有限正数**
|
|
127
|
+
* (`0` / 负 / `NaN` / `Infinity` / 字符串 全拒);文案含**字段名**与**实得值**。
|
|
128
|
+
* @param {unknown} block `capabilities.injectBudget` 显式声明的值
|
|
129
|
+
* @returns {string[]} 错误文案(空数组 = 合法)
|
|
130
|
+
*/
|
|
131
|
+
export function injectBudgetValueErrors(block) {
|
|
132
|
+
const errors = [];
|
|
133
|
+
if (block === undefined || block === null) return errors;
|
|
134
|
+
if (typeof block !== 'object' || Array.isArray(block)) {
|
|
135
|
+
// 非对象(`5` / `"on"` / `true`):`?.enabled` 取不到 ⇒ 预算会被**静默禁用且无痕**(同族静默面)。
|
|
136
|
+
errors.push('DEP-6: capabilities.injectBudget must be an object (got ' + JSON.stringify(block) + ')');
|
|
137
|
+
return errors;
|
|
138
|
+
}
|
|
139
|
+
if (block.enabled !== true) return errors;
|
|
140
|
+
for (const field of INJECT_BUDGET_LIMIT_FIELDS) {
|
|
141
|
+
const v = block[field];
|
|
142
|
+
if (v === undefined) continue;
|
|
143
|
+
if (typeof v !== 'number' || !Number.isFinite(v) || v <= 0) {
|
|
144
|
+
errors.push('DEP-6: capabilities.injectBudget.' + field
|
|
145
|
+
+ ' must be a finite positive number (got ' + JSON.stringify(v) + ')');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return errors;
|
|
149
|
+
}
|
|
150
|
+
|
|
96
151
|
// ── 校验:仅对显式 enabled 的非法组合报错;禁用能力零校验零开销 ──
|
|
97
152
|
export function validateCapabilities(config = {}) {
|
|
98
153
|
const errors = [];
|
|
@@ -135,6 +190,20 @@ export function validateCapabilities(config = {}) {
|
|
|
135
190
|
}
|
|
136
191
|
}
|
|
137
192
|
|
|
193
|
+
// DEP-6(包内写作 `DEP-N`):injectBudget 五阈值在 `enabled === true` 时须为**有限正数**。
|
|
194
|
+
// 落点理由:该键此前**零校验** ⇒ `totalBytes:-1` 一类坏值通过白名单后被**静默降级**(无任何报错)。
|
|
195
|
+
// 与 DEP-1..DEP-5 **同形**:仅显式 `enabled === true` 时校验值、字段缺省不误伤、文案 `DEP-6: …(got …)`。
|
|
196
|
+
//
|
|
197
|
+
// ⚠ **调用门不得写成 `c.injectBudget?.enabled === true`**(本 lane 自测抓出的真实缺陷,见 progress 03):
|
|
198
|
+
// 非对象形态(`injectBudget: 5`)取 `?.enabled` 得 `undefined` ⇒ 门为假 ⇒ **判定点根本看不到它**
|
|
199
|
+
// ⇒ 「坏配置被静默吞掉」的原病照旧(`5` 经 `deepMerge` 后同样让预算静默失效)。
|
|
200
|
+
// ⇒ 门只按「**是否显式声明**」开:`!== undefined` 即交判定点;`enabled !== true` 的**零校验**语义
|
|
201
|
+
// 由判定点自己实现(`injectBudgetValueErrors`),**既不误伤禁用态、也不漏放非对象**。
|
|
202
|
+
// `null` 不入判定点:`deepMerge(default, null)` 回落到注册表默认(预算照常生效)⇒ 无静默危害,不拒。
|
|
203
|
+
if (c.injectBudget !== undefined) {
|
|
204
|
+
errors.push(...injectBudgetValueErrors(c.injectBudget));
|
|
205
|
+
}
|
|
206
|
+
|
|
138
207
|
// MUTEX:互斥表(当前空表,无天然互斥;心跳/预算/worktree/verify/trajectory/aip 两两可叠加)
|
|
139
208
|
for (const ex of EXCLUSIONS) {
|
|
140
209
|
const on = ex.keys.filter((k) => readCapability(config, k)?.enabled === true);
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (C) 2025-2026 Punky
|
|
3
|
+
|
|
4
|
+
This program is free software: you can redistribute it and/or modify
|
|
5
|
+
it under the terms of the GNU Affero General Public License as
|
|
6
|
+
published by the Free Software Foundation, either version 3 of the
|
|
7
|
+
License, or (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
This program is distributed in the hope that it will be useful,
|
|
10
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
GNU Affero General Public License for more details.
|
|
13
|
+
|
|
14
|
+
You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// 文件 inject-budget:注入面字节预算(四层结构 + 显式省略标记)
|
|
19
|
+
// 作用面 = 注入给 worker / Leader 的**文本**(任务包骨架、交接断言块、lane 回报唤醒消息)。
|
|
20
|
+
// 刻意**不**作用于产物文件本体:截断结果只回传给调用方拼接注入文本,绝不回写任何文件(AC-10)。
|
|
21
|
+
//
|
|
22
|
+
// 纯函数模块(对齐同目录 budget.js 先例):**零 config import** —— 阈值由调用方从单一取值源
|
|
23
|
+
// capabilities.injectBudget 读出后传入;本模块内**不出现任何阈值字面量**(AC-8 / C-7)。
|
|
24
|
+
//
|
|
25
|
+
// 四层结构(层号只在本注释与返回值中体现,实现不重复编号):
|
|
26
|
+
// 层① 每源软上限 —— 由调用方选择 limit 种类:单条目 或 单块(两种 kind 各是一层,各自一条记录)
|
|
27
|
+
// 层② 总量硬上限 —— 对拼接后的整面文本施加(kind='total')
|
|
28
|
+
// 层③ head-tail 截断 —— 保结构头 + 结论尾(headBytes / tailBytes),中段挖除
|
|
29
|
+
// 层④ 显式省略标记 —— 被截断文本**自带** `…[omitted <N>B @ <surface>]`,消费方当场可辨
|
|
30
|
+
// 「没有」vs「被省略」;`<N>` = 十进制省略字节数,与 omissions[].omittedBytes 相等
|
|
31
|
+
//
|
|
32
|
+
// 分层施加顺序**固定**:单条目 → 单块 → 总量。每层各自产**一条** omissions 记录(不合并),
|
|
33
|
+
// 使 audit 可分辨「哪个面省略了多少」。调用方按序调用本函数即得该顺序(本模块不自行编排多面)。
|
|
34
|
+
//
|
|
35
|
+
// ── 调用契约(**务必先读**:签名比「面名 + 阈值」多一个层选择位,按声明面臆测会静默降级)──────
|
|
36
|
+
// `applyInjectionBudget(text, { surface, thresholds, kind })`
|
|
37
|
+
// ① `kind` ∈ `'perItem' | 'perBlock' | 'total'`,**缺省 `'total'`**。
|
|
38
|
+
// ⇒ 只施**该一层**,不施其它层;缺省时**只施总量层**、**不施单条目 / 单块层**。
|
|
39
|
+
// ⇒ 想施加「单条目层」或「单块层」必须**显式**传对应 `kind`,否则该层**静默不生效**
|
|
40
|
+
// (症状是「省略标记少了一层」这类**难定位**的偏差,不报错)。
|
|
41
|
+
// ② **分层不合并 = 调用方编排责任**:本模块**一次调用只施一层**。要四层齐备,调用方须
|
|
42
|
+
// **按固定顺序各呼一次**:`kind:'perItem'`(单条目)→ `kind:'perBlock'`(单块)→
|
|
43
|
+
// `kind:'total'`(总量);每次调用的返回值**串联**(把上一次的 `.text` 作为下一次的 `text`)。
|
|
44
|
+
// ③ 每条路径**恰产生一条 omission**(同面跨层 = 多条,**不得**合并为一条)——这正是 ② 的读端期望。
|
|
45
|
+
// ④ `thresholds` 必须由调用方**从单一取值源 `capabilities.injectBudget` 读出后传入**;
|
|
46
|
+
// 本模块不读配置、不内置默认阈值、不设兜底字面量。
|
|
47
|
+
//
|
|
48
|
+
// 触发口径:**严格大于**阈值才截断(恰等于阈值 ⇒ 不动作、零省略),并**返回入参原值本身**
|
|
49
|
+
// (同一引用,不重新拼接)⇒ 未超限零回归(AC-13)。
|
|
50
|
+
//
|
|
51
|
+
// 字节安全:一切长度按 Buffer 字节计,切点按**字节**取(禁按 String.length / 码元切,防 UTF-8 破字)。
|
|
52
|
+
// 实现自带该原语、**不触** lib/auditlog/sink.js —— 后者的字节截断函数为模块私有(无 export),
|
|
53
|
+
// 强行导出会把改动面推出本批写域边界;且两者语义本就不同(该处 = 尾部收敛 + 单标记;
|
|
54
|
+
// 本件 = head+tail + 结构化带字节数与面名的标记)。后续波次可抽公共原语。
|
|
55
|
+
|
|
56
|
+
// 省略标记模板:`…[omitted <N>B @ <surface>]`(省略号 U+2026)
|
|
57
|
+
const OMISSION_MARKER_TEMPLATE = '…[omitted {N}B @ {surface}]';
|
|
58
|
+
|
|
59
|
+
// 标记文案(本模块唯一构造点;`<surface>` 为空时退化为 `unknown`,不产半截标记)
|
|
60
|
+
export function omissionMarkerOf(omittedBytes, surface) {
|
|
61
|
+
return OMISSION_MARKER_TEMPLATE
|
|
62
|
+
.replace('{N}', String(omittedBytes))
|
|
63
|
+
.replace('{surface}', typeof surface === 'string' && surface.length > 0 ? surface : 'unknown');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 字节视图:按 byteBudget 自 text[0] 起取「不破字前缀」的**码元下标**(可与 String.prototype.slice 直配)
|
|
67
|
+
// 倒退最多 3 个码元(UTF-8 续字节前缀 0b10)保证切在码点边界上
|
|
68
|
+
function utf8PrefixEndIndex(text, byteBudget) {
|
|
69
|
+
if (!Number.isFinite(byteBudget) || byteBudget <= 0) return 0;
|
|
70
|
+
if (byteBudget >= Buffer.byteLength(text, 'utf8')) return text.length;
|
|
71
|
+
let n = 0;
|
|
72
|
+
let used = 0;
|
|
73
|
+
while (n < text.length) {
|
|
74
|
+
const w = Buffer.byteLength(text[n], 'utf8');
|
|
75
|
+
if (used + w > byteBudget) break;
|
|
76
|
+
used += w;
|
|
77
|
+
n += 1;
|
|
78
|
+
}
|
|
79
|
+
return n;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 字节视图:按 byteBudget 自 text 末尾起取「不破字后缀」的**起始码元下标**
|
|
83
|
+
function utf8SuffixStartIndex(text, byteBudget) {
|
|
84
|
+
if (!Number.isFinite(byteBudget) || byteBudget <= 0) return text.length;
|
|
85
|
+
if (byteBudget >= Buffer.byteLength(text, 'utf8')) return 0;
|
|
86
|
+
let start = text.length;
|
|
87
|
+
let used = 0;
|
|
88
|
+
while (start > 0) {
|
|
89
|
+
const w = Buffer.byteLength(text[start - 1], 'utf8');
|
|
90
|
+
if (used + w > byteBudget) break;
|
|
91
|
+
used += w;
|
|
92
|
+
start -= 1;
|
|
93
|
+
}
|
|
94
|
+
return start;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 内部:构造「head + 标记 + tail」并求不超过 limitBytes 的最大可用切法
|
|
98
|
+
// 取法:**在不超过 limitBytes 的前提下最大化保留量** ——
|
|
99
|
+
// ① head 取「结构头目标」headBytes(硬上限,不越界);
|
|
100
|
+
// ② tail 按目标占比 headBytes:tailBytes 放大,吃满本层剩余限额(tailBytes 同时是**下限**,
|
|
101
|
+
// 预算紧时仍保结论尾);
|
|
102
|
+
// ③ 结构头本身放不下时,对 head 预算做**二分边界搜索**取最大可行值(0 = 纯 tail)。
|
|
103
|
+
// 返回 null:连标记都放不下(预算过小)⇒ 调用方保持原值,不产「无信息的空壳」。
|
|
104
|
+
function headTailCut(text, limitBytes, headBytes, tailBytes, surface) {
|
|
105
|
+
const total = Buffer.byteLength(text, 'utf8');
|
|
106
|
+
// 标记占字节数按**上界省略值**自举(标记宽度随 N 的十进制位数量级变化)
|
|
107
|
+
const markerBytes = Buffer.byteLength(omissionMarkerOf(total, surface), 'utf8');
|
|
108
|
+
// available = 可供 head+tail 使用的字节(扣除标记)
|
|
109
|
+
const available = limitBytes - markerBytes;
|
|
110
|
+
if (available <= 0) return null;
|
|
111
|
+
|
|
112
|
+
// tryAssemble(headBudget):按给定 head 字节预算组装;返回 null 表示这一档放不下
|
|
113
|
+
const tryAssemble = (headBudget) => {
|
|
114
|
+
const headEnd = utf8PrefixEndIndex(text, headBudget);
|
|
115
|
+
const headBytesTaken = Buffer.byteLength(text.slice(0, headEnd), 'utf8');
|
|
116
|
+
// 余量给 tail:headBytes = head 硬上限;tailBytes = tail **下限**(预算紧时仍保结论)。
|
|
117
|
+
// 本层上限宽裕时 tail 吃满剩余(available - headTaken),避免「上限 12288 却只留 3112」的浪费;
|
|
118
|
+
// 剩余不足时退到 tailBytes 下限;head 只在自身预算内截。
|
|
119
|
+
const available = limitBytes - markerBytes;
|
|
120
|
+
const remaining = Math.max(0, available - headBytesTaken);
|
|
121
|
+
const tailBudget = Math.max(Math.min(tailBytes, available - headBytesTaken), remaining);
|
|
122
|
+
const tailStart = utf8SuffixStartIndex(text, tailBudget);
|
|
123
|
+
const usableTailStart = tailStart > headEnd ? tailStart : text.length;
|
|
124
|
+
const tailBytesTaken = usableTailStart < text.length ? Buffer.byteLength(text.slice(usableTailStart), 'utf8') : 0;
|
|
125
|
+
const kept = headBytesTaken + tailBytesTaken;
|
|
126
|
+
if (kept <= 0 || kept >= total) return null;
|
|
127
|
+
const omittedBytes = total - kept;
|
|
128
|
+
const rendered = text.slice(0, headEnd) + omissionMarkerOf(omittedBytes, surface) + text.slice(usableTailStart);
|
|
129
|
+
const renderedBytes = Buffer.byteLength(rendered, 'utf8');
|
|
130
|
+
if (renderedBytes > limitBytes) return null;
|
|
131
|
+
return { text: rendered, omittedBytes, keptBytes: renderedBytes };
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const headCap = Math.min(headBytes, total);
|
|
135
|
+
const widest = tryAssemble(headCap);
|
|
136
|
+
if (widest) return widest;
|
|
137
|
+
|
|
138
|
+
// 结构头放不下 ⇒ 二分找**最大可行的 head 预算**(0 表示纯 tail)
|
|
139
|
+
let lo = 0;
|
|
140
|
+
let hi = headCap;
|
|
141
|
+
let best = null;
|
|
142
|
+
while (lo <= hi) {
|
|
143
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
144
|
+
const candidate = tryAssemble(mid);
|
|
145
|
+
if (candidate) {
|
|
146
|
+
best = candidate;
|
|
147
|
+
lo = mid + 1;
|
|
148
|
+
} else {
|
|
149
|
+
hi = mid - 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return best;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// applyInjectionBudget:对**一个注入面**施加**一层**字节上限
|
|
156
|
+
// text:注入面文本(非字符串 ⇒ 原值返回、零动作,不抛)
|
|
157
|
+
// { surface, thresholds, kind } ——
|
|
158
|
+
// surface:面名(取自冻结面名集合;本模块**不**自造面名,亦不校验集合,校验归调用方/测试)
|
|
159
|
+
// thresholds:由调用方从 capabilities.injectBudget 读出({enabled, perItemBytes, perBlockBytes,
|
|
160
|
+
// totalBytes, headBytes, tailBytes});本模块**不**读配置、**不**内置默认阈值
|
|
161
|
+
// kind:层选择位 ∈ 'perItem' | 'perBlock' | 'total',**缺省 'total'**
|
|
162
|
+
// ⇒ **缺省只施总量层**,不施单条目/单块层;要那两层必须**显式**传(详见文件头「调用契约」)。
|
|
163
|
+
// ⇒ 一次调用只施一层;四层齐备由**调用方按序各呼一次**(perItem → perBlock → total)实现。
|
|
164
|
+
// 返回 { text, omissions }:omissions = [{ surface, omittedBytes, keptBytes }]
|
|
165
|
+
// · 未超限 / 开关关 / 无可用阈值 ⇒ text **原值本身**(同一引用)、omissions = [](零回归)
|
|
166
|
+
// · 超限 ⇒ head + 标记 + tail,**恰一条** omission(同面跨层 = 多条,不合并)
|
|
167
|
+
export function applyInjectionBudget(text, { surface, thresholds, kind = 'total' } = {}) {
|
|
168
|
+
const untouched = { text, omissions: [] };
|
|
169
|
+
if (typeof text !== 'string' || text.length === 0) return untouched;
|
|
170
|
+
if (thresholds?.enabled !== true) return untouched;
|
|
171
|
+
|
|
172
|
+
const limitOf = {
|
|
173
|
+
perItem: thresholds.perItemBytes,
|
|
174
|
+
perBlock: thresholds.perBlockBytes,
|
|
175
|
+
total: thresholds.totalBytes,
|
|
176
|
+
};
|
|
177
|
+
const limitBytes = limitOf[kind] ?? thresholds.totalBytes;
|
|
178
|
+
if (!Number.isFinite(limitBytes) || limitBytes <= 0) return untouched;
|
|
179
|
+
|
|
180
|
+
// 层①/② 触发判定:**严格大于**才截断;恰等于阈值 ⇒ 零动作(边界口径,防空实现)
|
|
181
|
+
if (Buffer.byteLength(text, 'utf8') <= limitBytes) return untouched;
|
|
182
|
+
|
|
183
|
+
const headBytes = Number.isFinite(thresholds.headBytes) ? thresholds.headBytes : 0;
|
|
184
|
+
const tailBytes = Number.isFinite(thresholds.tailBytes) ? thresholds.tailBytes : 0;
|
|
185
|
+
|
|
186
|
+
// 层③ head-tail 截断 + 层④ 显式省略标记
|
|
187
|
+
const cut = headTailCut(text, limitBytes, headBytes, tailBytes, surface);
|
|
188
|
+
if (cut === null) return untouched; // 标记都放不下 ⇒ 宁可不截断,也不产「无信息的空壳」
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
text: cut.text,
|
|
192
|
+
omissions: [{ surface, omittedBytes: cut.omittedBytes, keptBytes: cut.keptBytes }],
|
|
193
|
+
};
|
|
194
|
+
}
|