@feng0531/dsh-code-review 0.86.1 → 0.87.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 +16 -0
- package/CORE-ALIGNMENT-TASKS.md +17 -3
- package/CORE-SOURCE-INVENTORY.md +6 -6
- package/DEVELOPMENT.md +15 -1
- package/README.md +20 -3
- package/RELEASE-NOTES.md +10 -12
- package/index.mjs +1 -1
- package/package.json +1 -1
- package/src/candidate-verification.mjs +1 -1
- package/src/coverage-plan.mjs +20 -6
- package/src/cpp-migration-bindings.mjs +149 -0
- package/src/evidence-context.mjs +96 -0
- package/src/group-interaction.mjs +48 -0
- package/src/group-primary.mjs +14 -3
- package/src/interaction-risk.mjs +71 -0
- package/src/migration-contracts.mjs +160 -0
- package/src/migration-regression.mjs +103 -0
- package/src/model-contract.mjs +3 -3
- package/src/model-tools.mjs +6 -4
- package/src/primary-input.mjs +2 -14
- package/src/prompts.mjs +1 -0
- package/src/regression-assessment.mjs +8 -2
- package/src/report-summary.mjs +59 -0
- package/src/retrieval-loop.mjs +16 -3
- package/src/review-change-map.mjs +17 -0
- package/src/review-reader-allocation.mjs +46 -4
- package/src/review-resume.mjs +154 -3
- package/src/review-stage-state.mjs +148 -0
- package/src/review.mjs +187 -43
- package/src/service.mjs +7 -7
- package/src/stage-continuation.mjs +74 -0
- package/src/stage-generation-cache.mjs +22 -0
- package/src/verification-loop.mjs +9 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
本文件介绍对使用者有意义的变化。安装与参数见 [README](README.md),本次发布正文见 [发布说明](RELEASE-NOTES.md)。历史提交和安装包保留在 [GitHub Releases](https://github.com/f-e-n-g-0531/dsh-code-review/releases);本文不把开发日志当作功能介绍。
|
|
4
4
|
|
|
5
|
+
## 0.87.1 — 修复阶段续审缓存中断
|
|
6
|
+
|
|
7
|
+
- 主审、窗口归因和续审导入从同一批准原文重建包络坐标;包络仍不表示精确编辑或回归因果。
|
|
8
|
+
- 阶段生成先校验再进入缓存,单条拒绝或容量不足不污染已保存记录、不再在文件收尾中止整任务;当前发现与独立复核保留。
|
|
9
|
+
- 组成员、组级和宿主限制按组合后的有界数量保存,单次模型响应仍最多50条;续审不继承旧复核回执。
|
|
10
|
+
- 续审导出总容量不足时仅保存适配的完整记录并披露省略数量,不中止当前审查;非法导入、取消及真实落盘错误不被容量退化吞掉。
|
|
11
|
+
|
|
12
|
+
## 0.87.0 — 连续取证、迁移契约与阶段续审
|
|
13
|
+
|
|
14
|
+
- 当前阶段按批准来源重新投递有限相关正文,交互风险可内联;未送入模型不计证据,缓存仍扣字节,独立复核使用新回执。
|
|
15
|
+
- 报告先给结论摘要与发现,区分确认新增、未决、反驳和既有问题,完整覆盖与限制仍保留。
|
|
16
|
+
- 增加旧入口→新入口→调用方的迁移导航、保守 C++ 类/成员绑定,以及受宿主契约约束的跨文件旧新比较;均不作语义或因果保证。
|
|
17
|
+
- 精确完整组可返回可选交互通道;当前代真实读取和引用验收成功才替代匹配的专门生成,非法通道不抹主审,候选仍独立复核。
|
|
18
|
+
- 根额度内支持阶段有界让出/继续;schema2 仅保存可复用 primary 生成,未完成综合与复核重新进入安全边界,不继承旧回执。
|
|
19
|
+
- 六项策略在生产变更入口默认开启、底层 service 默认关闭,可显式回退;项目模式不启用迁移契约/迁移复核/组交互/阶段继续四项。不宣称完整上游对齐或真实模型提速。
|
|
20
|
+
|
|
5
21
|
## 0.86.1 — 修复取证额度分配、重复读取与工件容量
|
|
6
22
|
|
|
7
23
|
- 同快照精确操作复用,重复交付仍扣字节并重新签发复核回执;根预算内为主审、综合及复核预留最低份额,释放闲置额度。
|
package/CORE-ALIGNMENT-TASKS.md
CHANGED
|
@@ -9,14 +9,28 @@
|
|
|
9
9
|
| Git/SVN变更采集 | Git工作副本净变化/暂存区/未暂存范围、固定历史端点、同仓库SVN分支比较 | 任意SVN状态均可审查、完整提交逐一覆盖 |
|
|
10
10
|
| 范围与来源保护 | 先预览后确认,固定来源与哈希,执行前重新采集 | 普通源码一定没有秘密、模型提供方不保存数据 |
|
|
11
11
|
| 上下文 | 显式文件、旧侧文件、最多三层字面依赖、限定目录引用方 | 完整语义依赖图、调用图或动态执行可达性 |
|
|
12
|
-
| 输入与调度 | 独立业务分组、2~4
|
|
12
|
+
| 输入与调度 | 独立业务分组、2~4 精确完整文件组主审、差异窗口和发送前回退;原生工具/契约字节纳入共享输入预算 | 所有窗口/轮次均能组主审、大仓库必然完成或提速 |
|
|
13
|
+
| 连续取证与交互 | 当前阶段有限正文重读投递、交互风险内联;当前代完整读取且验收通过的组通道替代匹配专门生成 | 历史 audit 即新证据、缺失/非法通道也能跳过交互或复核 |
|
|
14
|
+
| 迁移导航与比较 | 宿主旧入口→新入口→调用方契约、保守 C++ 类/成员线索、契约绑定跨文件旧新证据 | 同名即迁移、完整 C++ 语义绑定、模型自报契约可扩大来源 |
|
|
15
|
+
| 报告摘要 | 结论和发现前置,区分确认新增/未决/反驳/既有问题 | supported 即确认新增、无发现即完整通过 |
|
|
13
16
|
| 结果传输 | 原生结果工具,同次文本兼容;仅原生模式接受完整 JSON 后明确尾注 | 修补损坏 JSON、猜字段/身份、无歧义解析所有输出 |
|
|
14
|
-
| 显式续审 |
|
|
17
|
+
| 显式续审 | 新预览绑定完整快照/配置/路由;schema2 可复用已接受 primary 生成,额度不足有界让出继续 | 恢复旧授权/回执、复用任意对话、综合或复核自动完成、根额度重置 |
|
|
15
18
|
| 私有模型工件 | 宿主任务目录外于业务工作区,有界保存文本/工具参数及失败工件,普通报告只给摘要 | 工件成为批准来源、失败草稿成为确认发现、Windows 模式位保证 ACL |
|
|
16
19
|
| 取证资源 | 同快照精确操作复用、每次交付计字节、阶段最低额度及闲置份额释放 | 根预算增加、每阶段一定有新读额度、缓存即本阶段证据 |
|
|
17
20
|
| 问题复核 | 原文位置检查、模型反证与状态记录、每次重新签发回执 | 缺陷必然成立、无漏报或无误报 |
|
|
18
21
|
| 文件指导 | 语言、协议、配置与模板的保守检查提示 | 编译器、类型检查器或真实运行结果 |
|
|
19
22
|
|
|
23
|
+
## 固定对照与尚未完成项
|
|
24
|
+
|
|
25
|
+
本轮参考固定为 Open Code Review [216dcf513593055a70165f793e96b52589d71b1b](https://github.com/alibaba/open-code-review/tree/216dcf513593055a70165f793e96b52589d71b1b)。0.87.0 已实现上述机制,但以下差距仍明确未完成,不据此声称整体对齐:
|
|
26
|
+
|
|
27
|
+
- [ ] 增量多轮审查与“无新发现”早停;当前显式轮次不是继承前轮推理的增量循环。
|
|
28
|
+
- [ ] 多轮/窗口化的组主审;当前合并只覆盖预算适配的精确完整正文组。
|
|
29
|
+
- [ ] 有界推理记忆压缩;当前仅有 host 证据坐标与受限正文重投递,不共享模型隐藏推理。
|
|
30
|
+
- [ ] 安全检索正则、大小写与路径模式能力;现有批准来源内的字面搜索不是这些能力。
|
|
31
|
+
|
|
32
|
+
批准快照、显式来源目录、有限读取及未完成状态是有意安全取舍;与参考的全仓实时范围差异本身不作为缺陷或待放开权限项。六项开关的生产/底层默认及项目模式边界见 [README](README.md)。
|
|
33
|
+
|
|
20
34
|
## 当前限制
|
|
21
35
|
|
|
22
36
|
- 继承、重载、宏、动态分派和复杂别名不做完整语义绑定。
|
|
@@ -24,7 +38,7 @@
|
|
|
24
38
|
- 替换旧侧可采用用户明确选择的固定仓库基线,但这不等于恢复了丢失的本地BASE。
|
|
25
39
|
- 所有测试和安装验证只证明对应机制的行为,不能外推真实模型质量。当前没有独立质量基准成绩;历史试跑见 [评测说明](evaluation/README.md)。
|
|
26
40
|
- 不执行项目代码,不自动修复、提交、发布业务项目,也不建设独立模型计费或管理平台。
|
|
27
|
-
-
|
|
41
|
+
- 续审只复用完整文件链或 schema2 校验过的 primary 生成,不复用任意未完成中间对话;旧发现保留 priorRun 来源,不能冒充本轮证据。工件可能含源码,默认限额和显式取回边界见 [README](README.md)。
|
|
28
42
|
- 本轮机制变更不等于完整上游能力/质量对齐;没有真实模型质量、速度或成本的新基准结论,也不以文档更新声明已发布。
|
|
29
43
|
|
|
30
44
|
## 如何查看版本证据
|
package/CORE-SOURCE-INVENTORY.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
供维护者核查实现依据。使用方法见 [README](README.md),开发优先级见 [能力清单](CORE-ALIGNMENT-TASKS.md)。
|
|
4
4
|
|
|
5
|
-
当前本轮对照固定为 alibaba/open-code-review 提交 `
|
|
5
|
+
当前本轮对照固定为 alibaba/open-code-review 提交 `216dcf513593055a70165f793e96b52589d71b1b`,不随上游分支变化。下方文件索引仍是旧提交 `2d67596c961f80436deb2afa643efa2b1725d34c` 的历史阅读记录;未把旧记录冒充新提交逐文件复审。
|
|
6
6
|
|
|
7
7
|
## 对照结论
|
|
8
8
|
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
| 参考模块 | 当前对应能力 | 仍需补齐 |
|
|
12
12
|
|---|---|---|
|
|
13
|
-
| agent、delegate |
|
|
14
|
-
| llmloop |
|
|
15
|
-
| tool |
|
|
13
|
+
| agent、delegate | 风险内联、迁移导航、精确全文组主审及有证据的组交互验收、失败回退 | 增量多轮早停、多轮/窗口组主审 |
|
|
14
|
+
| llmloop | 有限检索、候选反证、当前阶段正文重投递、有界让出继续与明确结束状态 | 推理记忆压缩、真实模型质量验证 |
|
|
15
|
+
| tool | 批准快照内的文件查找、字面搜索、读取和独立证据校验 | 安全正则/大小写/路径模式检索 |
|
|
16
16
|
| config/rules | 语言和文件用途检查提示、显式项目规则 | |
|
|
17
17
|
| diff、gitcmd、pathutil | 只读变更、固定历史对象、路径保护、命令限额 | 不照搬实时无界读取 |
|
|
18
18
|
| model、session/manifest | 覆盖状态、未完成原因、失败与取消区别 | |
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
- 默认一次审查,可选三次,不照搬参考默认次数和对话继承方式。
|
|
26
26
|
- 历史重命名按删除和新增表达;区间比较使用明确端点,不自动计算合并基点。
|
|
27
27
|
- 参考中的风格偏好、无条件性能建议和错误绝对规则不作为缺陷标准。
|
|
28
|
-
-
|
|
29
|
-
-
|
|
28
|
+
- 现已提供身份绑定的任务检查点与显式续审:新预览和新确认后,复用完整快照/配置/路由一致的已完成合格文件链,或 schema2 校验过的 primary 生成;未完成综合和复核重新进入安全边界,不继承旧回执。不是上游任意会话/覆盖集合恢复,也没有 waived 豁免机制。
|
|
29
|
+
- 生产模型传输使用原生结果工具和同次文本兼容;精确组通道仅在本代完整读取及交互验收后替代匹配专门生成,缺失/非法仍回退,候选独立复核保留;不宣称“整段 JSON 必须完全有效”本身优于上游。仅原生模式容忍完整 JSON 后明确自然语言尾注,歧义或损坏内容仍失败关闭。
|
|
30
30
|
- 本插件后台任务留存有界私有模型工件,普通诊断不公开原文。上游原始响应日志是通过 `OCR_RAW_LOGGING=1` 显式启用的选项,不能描述为无条件默认记录;两者的隐私和恢复边界不同。
|
|
31
31
|
- 不做 pre-run token 预估警告:用共享输入字节与调用预算在运行中约束,实际覆盖以报告终态为准。
|
|
32
32
|
- 未覆盖项保留 pending 并报 partial,不扫成 failed:诚实暴露未完成,不伪造终态。
|
package/DEVELOPMENT.md
CHANGED
|
@@ -36,6 +36,20 @@
|
|
|
36
36
|
|
|
37
37
|
JSON合同新增280字节传输提示防止原始换行/反斜线转义错误,不自动修补响应或替换身份。诊断聚合allowlist纠正原因:语法纠正仅对实际model-end计数,审查格式与检索协议在重试通过预算/取消准入后计数;被veto的请求不报已执行纠正。`primary-initial`也可能是复核/综合检索链初始调用。单阶段轮数与共享reader耗尽分别输出code;全部字段不含源码、原响应或未知提供方错误文本。
|
|
38
38
|
|
|
39
|
+
### 0.87.0 连续上下文、迁移与阶段恢复
|
|
40
|
+
|
|
41
|
+
六项策略 enableEvidenceContext、inlineInteractionRisks、enableMigrationContracts、enableMigrationRegression、enableGroupInteractions、enableStageContinuation 在插件变更入口默认 true;直接底层/service 默认 false,均绑定预览、执行与续审身份。项目专用入口不启用后四项。与上游 216dcf513593055a70165f793e96b52589d71b1b 的已实现/未完成边界见 [能力清单](CORE-ALIGNMENT-TASKS.md),历史文件索引不冒充本次逐文件复审。
|
|
42
|
+
|
|
43
|
+
连续正文投递的索引仅保存本次批准且实际发送的坐标;当前 stage reader 重新取原文并扣字节,不共享旧 receipt。可选初始正文事务延迟到调用预算准入后,预算否决/取消/输入不适配必须回滚,未送入模型不发布 audit。交互内联风险缺失/非法不抹合法 findings,不补发规划。
|
|
44
|
+
|
|
45
|
+
迁移元数据是宿主生成的 side-qualified 导航,不是模型授权。跨文件 regression 只用已批准 contract ID、来源/角色/精确编辑和独立已校验 receipt,默认旧同主文件协议仍严格。C++ 仅在字面 include、唯一类声明/类外定义和明确 receiver 下产生保守线索,重载/继承/虚分派/宏不能猜测唯一绑定。
|
|
46
|
+
|
|
47
|
+
精确完整组的根可选 interactions 通道独立于合法 primary 结果验收。仅本代读取覆盖全部成员编辑、source/snapshot/hash 及跨文件引用验收成功后替代匹配专门生成;非法或缺失不触发隐藏主审重跑,候选仍用独立复核。原生 schema、文本合同和可选开销进入真实 wire 预算,预览 minimumCalls 是有条件下限并披露 fallbackMinimumCalls。
|
|
48
|
+
|
|
49
|
+
额度让出不抢 live unit;其他阶段 release 后有限 FIFO 继续,共享原调用/输出总账和绝对截止。schema2 只保存身份校验过的 primary generation,不持久化成可复用的新证据 receipt;恢复只省生成,交互/综合及验证重新从安全边界执行。没有进展保留 pending/partial,取消不自动变为完成。
|
|
50
|
+
|
|
51
|
+
受控集成验证入口为 `test/ocr-refactor-integration.test.mjs`、`test/ocr-complete-policy.test.mjs` 及 group/migration/stage 专项测试;其中开关对照 7→5、组通道 5→3 是固定离线响应的模型调用数,不是提供方速度或召回保证。正式发布仍执行下文完整门禁,不把文档更新或本地通过当作已发布。
|
|
52
|
+
|
|
39
53
|
## 全项目并发与离线速度对照
|
|
40
54
|
|
|
41
55
|
插件配置 `projectConcurrency` 默认为 8,范围 1~8;1 为串行回退。仅独立批准批次并行,各批仍有独立检索来源和账本,所有调用与 JSON 纠正共享原执行计数。该上限按单次执行生效,不是全宿主 RPM/TPM 限流器;提供方不支持并发或遇到 429 时应调低。取消/致命检查点错误必须等待全部在途模型清理后释放会话锁,后台 checkpoint 继续使用原串行持久化队列。
|
|
@@ -52,7 +66,7 @@ JSON合同新增280字节传输提示防止原始换行/反斜线转义错误
|
|
|
52
66
|
|
|
53
67
|
## 模型响应失败与诊断
|
|
54
68
|
|
|
55
|
-
当前核验的 DSH `llm.stream` 接口没有原生 JSON mode/response schema 参数;插件不发送会被忽略的 `responseFormat`,也不把工具参数 schema
|
|
69
|
+
当前核验的 DSH `llm.stream` 接口没有原生 JSON mode/response schema 参数;插件不发送会被忽略的 `responseFormat`,也不把工具参数 schema 当作结构化输出保证。生产优先使用真实结果工具,保留同次文本兼容,再做本地解析和阶段 schema、证据校验;没有原生工具支持时不伪造结构化输出保证。
|
|
56
70
|
|
|
57
71
|
正常 stop 后的非法 JSON 最多重新生成两次,共享原调用预算与取消信号;纠正请求只带固定语法分类和可用的行列位置,不回放错误响应、不自动修补 JSON、不删减发现来换取通过。语法位置相对于去掉外层空白及完整代码围栏后的 JSON 正文,偏移单位为 UTF-16 code unit;未知运行时错误文本回退到通用分类,不推测位置。
|
|
58
72
|
|
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ dsh plugin add https://github.com/f-e-n-g-0531/dsh-code-review.git
|
|
|
61
61
|
|
|
62
62
|
### 5. 查看结果
|
|
63
63
|
|
|
64
|
-
执行工具会在对话中返回 **中文 Markdown 报告**,同时返回结构化 JSON
|
|
64
|
+
执行工具会在对话中返回 **中文 Markdown 报告**,同时返回结构化 JSON。“执行诊断”列出实际加载的插件版本、阶段调用和墙钟统计;累计调用耗时可能并发重叠,不代表纯提供方耗时或可相加的总耗时。先看前置的“结论摘要”和问题列表,再核对覆盖与限制;摘要区分已确认新增、未决、已反驳和既有问题,不把仅 supported 或缺少旧新对照的候选当作确认新增。
|
|
65
65
|
|
|
66
66
|
如需保存报告,可以另行告诉 Agent:“将本次报告保存到我指定的位置”。插件本身不会自动在业务仓库创建报告文件。
|
|
67
67
|
|
|
@@ -241,7 +241,7 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
|
|
|
241
241
|
|
|
242
242
|
### 显式续审与私有模型工件
|
|
243
243
|
|
|
244
|
-
需要补审时,在**新预览**中传入 `resumeFromJobId`,例如 `{"resumeFromJobId":"原执行任务的真实jobId"}
|
|
244
|
+
需要补审时,在**新预览**中传入 `resumeFromJobId`,例如 `{"resumeFromJobId":"原执行任务的真实jobId"}`;仍须展示新范围、重新确认并通过当前宿主权限。只复用同一会话、工作目录、模型路由/思考设置下,完整快照、配置和运行时身份均匹配的已完成且符合复用条件的文件链;项目模式还绑定对应批次。来源或配置变化不做猜测复用。旧发现标记 `priorRun`,不伪造本轮读取凭据、模型调用或覆盖。变更模式开启阶段续审时,schema2 检查点还可保存已接受但所在文件链未完成的 primary 生成;它只省去匹配的主审生成,综合、交互和复核在新安全边界执行,复核重新取得独立回执。预览分别显示可复用文件、生成及仍需的新鲜复核;不能把保存了正文或候选当作阶段完成。
|
|
245
245
|
|
|
246
246
|
后台审查会在上述宿主任务目录的 `artifacts` 子目录留存有界模型工件,包括失败/取消时已捕获的文本或工具参数;不含 reasoning。它们可能含源码,应按源码保护,不会自动加入提示词、当作已确认发现或在普通状态报告公开。默认单工件 1 MiB、每任务原文总量 8 MiB、最多 4096 条记录;截断由工件标记,存储超限明确返回遗漏标记和无源码诊断。读取绑定原会话、目录、提供方、模型和思考设置。需要查看时,Agent 必须先提示“可能把源码/模型原文带入当前对话”,取得你的明确同意,再调用 `code_review_artifacts({jobId,confirmed:true})`;可用 `offset`/`limit` 分页,默认一次 1 条、最多 100 条。返回内容标为 `sourceBearing:true`、`untrusted:true`,不是批准证据;普通 `status` 不返回工件原文。目录/文件使用 0700/0600 权限模式及原子写入;Windows 模式位不等于额外 ACL 保证。
|
|
247
247
|
|
|
@@ -330,7 +330,7 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
|
|
|
330
330
|
|
|
331
331
|
## 调用效率与预算
|
|
332
332
|
|
|
333
|
-
业务分组在文件链启动前独立完成,不以先跑完第一个文件作引导。适合的 2~4
|
|
333
|
+
业务分组在文件链启动前独立完成,不以先跑完第一个文件作引导。适合的 2~4 个文件可合并为一次组主审:必须是精确变更、完整正文且可容纳的非项目模式成员,响应逐个返回原文件身份并通过各自校验。窗口化、超预算或不适合的组在发送前回退单文件。开启组交互通道后,仅当前生成的真实读取覆盖全部成员编辑且交互引用校验通过,才替代完全匹配的专门交互生成;缺失或非法通道保留合法主审并回退,候选仍独立复核。预览显示条件性节省及回退成本,组主审不等于跨文件缺陷已证明。可合并的风险假设是可选信息,缺失/无效会记录限制,不为它单独追加调用。
|
|
334
334
|
|
|
335
335
|
变更模式默认优先保留主文件全文,仅附带有导入/调用等关系的完整上下文对象(序列化合计最多16KiB或输入上限四分之一,取较小值);其余批准来源保留版本目录按需读取,不表示不存在或已核查。完整正文,或经过精确新旧编辑、坐标和原文核对且覆盖全部编辑的单批窗口,可将风险问题合入首轮主审。多批窗口、包络差异和项目审查仍保留独立规划;保留独立候选复核和跨窗口/跨文件综合,不以丢弃窗口换取速度。报告中的合并问题不是独立风险规划或已验证条件。
|
|
336
336
|
|
|
@@ -340,6 +340,23 @@ Git / SVN 历史审查也能使用这些参数;依赖和调用方按对应版
|
|
|
340
340
|
|
|
341
341
|
诊断显示实际加载版本、阶段及调用目的计数、读取余量,并区分单阶段检索轮数和共享读取额度。新增无源码的JSON纠正原因、协议/审查格式纠正计数及未合并规划原因;纠正计数以实际纠正调用为准;这些诊断不含原响应、源码片段或提供方错误文本,与下面说明的私有模型工件分开。`primary-initial` 是检索链初始调用,不只包含主审阶段。累计调用耗时可能并发重叠,不能当作总耗时或纯提供方耗时。离线调用数减少不代表实际审核必然同比提速,不能用更快返回partial冒充审核完成。
|
|
342
342
|
|
|
343
|
+
### 0.87.0 的连续取证与策略开关
|
|
344
|
+
|
|
345
|
+
同次变更审查会按目标来源选择此前实际交付过的有限片段,经当前阶段 reader 重新读取并放入当前模型输入;未发送、超预算或取消的片段不计为本阶段证据,缓存命中仍扣投递字节,不继承历史回执。精确适配的交互单元可把可选风险问题内联到结果中;缺失或非法风险如实保留限制,不抹合法发现、不补发规划。
|
|
346
|
+
|
|
347
|
+
迁移契约把明确的旧入口、新入口和调用方组织为有界导航,旧新路径分别解析并保留跨组联系;不是同名/相似度推断的语义证明。跨文件旧新比较只接受宿主契约限定的来源、编辑和本次独立复核证据,缺少一侧仍未决。C++ 类/成员线索仅覆盖字面 include、唯一声明/类外定义和明确接收者;重载、继承、虚分派、宏等不猜测绑定。
|
|
348
|
+
|
|
349
|
+
| 插件配置(不是 preview 参数) | 变更模式用途 |
|
|
350
|
+
|---|---|
|
|
351
|
+
| `enableEvidenceContext` | 当前阶段有限正文重新投递 |
|
|
352
|
+
| `inlineInteractionRisks` | 适配的交互风险内联 |
|
|
353
|
+
| `enableMigrationContracts` | 旧入口→新入口→调用方契约导航 |
|
|
354
|
+
| `enableMigrationRegression` | 受宿主契约约束的跨文件旧新复核 |
|
|
355
|
+
| `enableGroupInteractions` | 通过真实读取验收后替代匹配交互生成 |
|
|
356
|
+
| `enableStageContinuation` | 有界让出/继续及 primary 生成检查点 |
|
|
357
|
+
|
|
358
|
+
上述六项在正式插件变更入口默认 true,可逐项设 false 回退;底层及直接创建的 service 默认 false。项目模式不启用后四项,仍采用自己的批次、预算和续审协议。额度让出只等待其他阶段释放后有限继续,不抢占活动阶段、不提高根预算、不重置截止或调用账;无进展、取消或超时保留真实 pending/partial。
|
|
359
|
+
|
|
343
360
|
## 规模限制
|
|
344
361
|
|
|
345
362
|
| 项目 | 默认上限 |
|
package/RELEASE-NOTES.md
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
|
-
# v0.
|
|
1
|
+
# v0.87.1 发布说明
|
|
2
2
|
|
|
3
3
|
使用方式见 [README](README.md),历史变化见 [变更记录](CHANGELOG.md),发布门禁见 [开发说明](DEVELOPMENT.md)。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 修复阶段续审中断
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
修复 v0.87.0 中主审已接受的结果被续审缓存再次拒绝,并在文件收尾扩大为整个任务中止的问题。主审、窗口归因及续审校验现从同一批准原文重建大文件包络坐标;包络仍只是保守区域,不代表精确编辑或回归因果。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
组成员与组级限制合并后按宿主有界结果保存;模型成员及组级响应各自最多50条的限制不变。阶段生成先校验再入缓存,拒绝或容量不足不污染此前有效记录,也不省略当前候选的独立复核。不能继续的候选明确保持未完成,不通过重新生成掩盖失败。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## 续审容量与安全边界
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
续审导出超过总字节或节点容量时,按完整记录保留适配部分,披露未保存数量;不截断发现或假装已保存全部进度。省略主审或完整文件链时不继承相关继续状态。未保存部分在后续新授权审查中重新执行。
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
批准快照、身份与原始坐标校验仍严格。非法导入、伪造窗口和属性、访问器等不安全数据继续拒绝;旧复核回执不作为新证据。真实落盘错误及用户取消不被容量退化吞掉。
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
## 验证与交付
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
新增实际执行回归覆盖包络窗口开启/关闭阶段继续、非空候选、51/101条合法合并限制、缓存拒绝后继续处理其他文件、断点复用主审但重新独立复核,以及容量、取消和落盘失败。修复冻结验收完整回归1249项:1245通过、0失败、4跳过;关键组合66/66通过,宿主离线smoke通过。这些不是业务项目重跑或真实模型质量、速度保证。
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
正式发布须通过冻结标签 Ubuntu/Windows × Node.js22/24 四矩阵及 Actions、npm、GitHub Release 原始包逐字节、校验清单和冻结发布说明核验。不会自动安装、升级或重启当前 DSH。
|
|
21
|
+
正式发布还须通过该版本冻结标签 Ubuntu/Windows × Node.js22/24 四矩阵,并核验 Actions、npm 与 GitHub Release 的同一原始安装包及校验清单。发布不会自动安装、升级或重启当前 DSH。
|
package/index.mjs
CHANGED
|
@@ -14,7 +14,7 @@ const EXECUTE_PARAMETERS = { type: 'object', additionalProperties: false, proper
|
|
|
14
14
|
export function apply(ctx, config = {}) {
|
|
15
15
|
// This entry runs only when the host invokes the registered tool.
|
|
16
16
|
// Host tool policy owns permission; do not request a second approval here.
|
|
17
|
-
const service = createReviewService(ctx.llm, { allowModelSending: true, nativeOutputTools: true, enableGroupPrimary: true, enableRiskPlanning: true, enableBusinessGrouping: true, enableAnchorCorrection: true, projectConcurrency: config.projectConcurrency ?? DEFAULT_PROJECT_CONCURRENCY, riskPlanningMode: config.riskPlanningMode ?? 'auto', readerBudgetPolicy: config.readerBudgetPolicy ?? 'scaled', optimizeReviewCalls: true, enablePrimarySeeds: true, focusPrimaryContext: config.focusPrimaryContext ?? true, enableSearchSeeds: config.enableSearchSeeds ?? true, changesConcurrency: config.changesConcurrency ?? 2, maxRetrievalRounds: config.maxRetrievalRounds ?? 3, authorize: async () => true });
|
|
17
|
+
const service = createReviewService(ctx.llm, { allowModelSending: true, nativeOutputTools: true, enableGroupPrimary: true, enableEvidenceContext: config.enableEvidenceContext ?? true, inlineInteractionRisks: config.inlineInteractionRisks ?? true, enableMigrationContracts: config.enableMigrationContracts ?? true, enableMigrationRegression: config.enableMigrationRegression ?? true, enableGroupInteractions: config.enableGroupInteractions ?? true, enableStageContinuation: config.enableStageContinuation ?? true, enableRiskPlanning: true, enableBusinessGrouping: true, enableAnchorCorrection: true, projectConcurrency: config.projectConcurrency ?? DEFAULT_PROJECT_CONCURRENCY, riskPlanningMode: config.riskPlanningMode ?? 'auto', readerBudgetPolicy: config.readerBudgetPolicy ?? 'scaled', optimizeReviewCalls: true, enablePrimarySeeds: true, focusPrimaryContext: config.focusPrimaryContext ?? true, enableSearchSeeds: config.enableSearchSeeds ?? true, changesConcurrency: config.changesConcurrency ?? 2, maxRetrievalRounds: config.maxRetrievalRounds ?? 3, authorize: async () => true });
|
|
18
18
|
ctx.tools.register({
|
|
19
19
|
name: 'code_review_preview', description: PREVIEW_DESCRIPTION, parameters: PREVIEW_PARAMETERS, output: reviewToolOutput,
|
|
20
20
|
async execute(args, exec) { const preview = await service.preview(args, exec); return { json: JSON.stringify(preview), markdown: '' }; },
|
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@ export function validateCandidateVerification(value, candidateIds, read, resolve
|
|
|
15
15
|
}
|
|
16
16
|
const results = new Map(value.verdicts.map(item => {
|
|
17
17
|
const evidence = item.evidence.map(ref => validateEvidenceReference(resolveEvidence(ref), read));
|
|
18
|
-
const regression = validateRegressionAssessment(item.regression, evidence, comparison.catalog ?? [], comparison.fileIds?.[item.candidateId]);
|
|
18
|
+
const regression = validateRegressionAssessment(item.regression, evidence, comparison.catalog ?? [], comparison.fileIds?.[item.candidateId], { ...comparison, candidateId: item.candidateId });
|
|
19
19
|
return [item.candidateId, { candidateId: item.candidateId, verdict: item.verdict, reason: item.reason, evidence, regression, causality: 'unverified' }];
|
|
20
20
|
}));
|
|
21
21
|
return candidateIds.map(id => results.get(id));
|
package/src/coverage-plan.mjs
CHANGED
|
@@ -7,11 +7,16 @@ import { prepareInteractionInput } from './interaction-input.mjs';
|
|
|
7
7
|
import { initialInputBudget } from './input-budget.mjs';
|
|
8
8
|
import { preparePrimaryInput } from './primary-input.mjs';
|
|
9
9
|
import { prepareGroupPrimary } from './group-primary.mjs';
|
|
10
|
+
import { prepareInteractionRisk } from './interaction-risk.mjs';
|
|
11
|
+
import { buildMigrationContracts } from './migration-contracts.mjs';
|
|
12
|
+
import { buildReviewGroups } from './review-groups.mjs';
|
|
13
|
+
import { inferFileRelations } from './file-relations.mjs';
|
|
10
14
|
|
|
11
15
|
// Metadata only. No retained payload, file reads, model calls, or approval grants.
|
|
12
16
|
export function buildCoveragePlan(input, options) {
|
|
13
17
|
const rounds=reviewRounds(options?.reviewRounds);
|
|
14
18
|
riskPlanningMode(options?.riskPlanningMode);
|
|
19
|
+
for (const key of ['enableEvidenceContext', 'inlineInteractionRisks', 'enableMigrationContracts', 'enableMigrationRegression', 'enableGroupInteractions', 'enableStageContinuation']) if (options?.[key] !== undefined && typeof options[key] !== 'boolean') throw new Error('Invalid review optimization option');
|
|
15
20
|
if (!input || typeof input.id !== 'string' || !input.id || !Array.isArray(input.files) || input.files.length > 200) throw new Error('Invalid planning snapshot');
|
|
16
21
|
if (!options || typeof options.instructions !== 'string' || !Number.isSafeInteger(options.maxInputBytes) || options.maxInputBytes < 1) throw new Error('Invalid planning budget');
|
|
17
22
|
const ids = new Set(), paths = new Set();
|
|
@@ -19,6 +24,8 @@ export function buildCoveragePlan(input, options) {
|
|
|
19
24
|
if (!file || typeof file.id !== 'string' || !file.id || ids.has(file.id) || typeof file.path !== 'string' || !file.path || paths.has(file.path) || !['reviewable','excluded','blocked'].includes(file.eligibility)) throw new Error('Invalid planning file');
|
|
20
25
|
ids.add(file.id); paths.add(file.path);
|
|
21
26
|
}
|
|
27
|
+
const migrationContracts = options.enableMigrationContracts === true && options.scope && !input.projectScope ? buildMigrationContracts(input) : null;
|
|
28
|
+
if (migrationContracts?.contracts.length) options = { ...options, grouping: buildReviewGroups(input.files, [...inferFileRelations(input.files), ...migrationContracts.links.map(link => ({ from: link.from, to: link.to, reason: link.reasons.join(',') }))]) };
|
|
22
29
|
const items = [...input.files].sort((a,b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0).map(file => {
|
|
23
30
|
options.signal?.throwIfAborted();
|
|
24
31
|
const item = { fileId: file.id, path: file.path, status: file.eligibility, minimumCalls: 0 };
|
|
@@ -35,20 +42,27 @@ export function buildCoveragePlan(input, options) {
|
|
|
35
42
|
const local = options.resumeState?.businessGrouping ?? (options.enableBusinessGrouping === true ? localBusinessGroups(input.files) : null);
|
|
36
43
|
const groups = [...(options.grouping?.groups ?? [])];
|
|
37
44
|
for(const group of local?.groups ?? [])if(!groups.some(g=>[...g.fileIds].sort().join(',')===[...group.fileIds].sort().join(',')))groups.push(group);
|
|
38
|
-
const interactions = options.scope && groups.length ? groups.filter(g => g.fileIds.length > 1).map(group => {
|
|
45
|
+
const interactions = options.scope && groups.length ? groups.filter(g => g.fileIds.length > 1).map(group => {
|
|
46
|
+
const prepared = prepareInteractionInput(input, group, payload => initialInputBudget({ instructions: options.instructions, input: payload }, options.scope, options.maxInputBytes));
|
|
47
|
+
const strategy = prepareInteractionRisk(input, group, prepared, options);
|
|
48
|
+
const riskPlanning = { ...strategy.decision, mode: strategy.mode };
|
|
49
|
+
return { groupId: group.id, fileIds: prepared.fileIds, riskPlanning, status: prepared.status, reason: prepared.reason, minimumCalls: prepared.status === 'ready' ? rounds + strategy.planningCalls : 0 };
|
|
50
|
+
}) : [];
|
|
39
51
|
const businessGrouping = options.enableBusinessGrouping === true && input.files.filter(f => f.eligibility === 'reviewable').length > 1 ? local ? {minimumCalls:0,strategy:local.strategy,churn:local.churn,notice:'本地调度无语义依赖证明。'} : { minimumCalls: 1, dynamicInteractionCalls: 'unknown-until-execution', maximumAdditionalInitialCalls: Math.floor(input.files.filter(f => f.eligibility === 'reviewable').length / 2) * (rounds + (options.enableRiskPlanning === true ? 1 : 0)), notice: '模型分组假设仅调度,额外综合动态且不保证预算或覆盖。' } : null;
|
|
40
52
|
for (const interaction of interactions) if (interaction.fileIds.every(id => options.resumeState?.completed?.some(row => row.fileId === id))) { interaction.minimumCalls = 0; interaction.reused = true; }
|
|
41
53
|
const primaryGroups = [];
|
|
42
54
|
if (options.enableGroupPrimary === true && rounds === 1 && options.scope) {
|
|
43
55
|
const seen = new Set();
|
|
44
|
-
for (const group of local?.groups ?? options.grouping?.groups ?? []) {
|
|
45
|
-
if (group.fileIds.some(id => seen.has(id) || options.resumeState?.completed?.some(row => row.fileId === id))) continue;
|
|
46
|
-
const prepared = prepareGroupPrimary(input, group, options);
|
|
56
|
+
for (const group of [...(migrationContracts?.groups ?? []), ...(local?.groups ?? options.grouping?.groups ?? [])]) {
|
|
57
|
+
if (group.fileIds.some(id => seen.has(id) || options.resumeState?.completed?.some(row => row.fileId === id) || options.resumeState?.stageGenerations?.some(row => row.fileId === id))) continue;
|
|
58
|
+
const prepared = prepareGroupPrimary(input, group, { ...options, migrationContracts: migrationContracts?.contracts ?? [] });
|
|
47
59
|
if (!prepared) continue;
|
|
48
|
-
primaryGroups.push({ groupId: group.id, fileIds: prepared.fileIds, minimumCalls: 1 });
|
|
60
|
+
primaryGroups.push({ groupId: group.id, fileIds: prepared.fileIds, minimumCalls: 1, ...(prepared.groupInteraction ? { interactionChannel: 'conditional', fallback: 'dedicated-stage' } : {}) });
|
|
61
|
+
if (prepared.groupInteraction) for (const interaction of interactions.filter(item => [...item.fileIds].sort().join(',') === [...prepared.fileIds].sort().join(','))) { interaction.fallbackMinimumCalls = interaction.minimumCalls; interaction.minimumCalls = 0; interaction.method = 'conditional-group-channel'; interaction.notice = 'Only current-generation fully read and host-validated group channel replaces the dedicated stage; missing/invalid channel retains its fallback cost and independent candidate verification.'; }
|
|
49
62
|
for (const id of group.fileIds) { seen.add(id); const item = items.find(item => item.fileId === id); item.primaryMethod = 'group-primary'; item.primaryGroupId = group.id; item.minimumCalls = id === group.fileIds[0] ? 1 : 0; }
|
|
50
63
|
}
|
|
51
64
|
}
|
|
52
65
|
for (const row of options.resumeState?.completed ?? []) { const item = items.find(item => item.fileId === row.fileId); if (item) { item.minimumCalls = 0; item.reused = true; } }
|
|
53
|
-
|
|
66
|
+
for (const file of input.files) { const records = options.resumeState?.stageGenerations?.filter(record => record.fileId === file.id && record.phase === 'primary') ?? []; if (!records.length) continue; const item = items.find(item => item.fileId === file.id); const prepared = preparePrimaryInput(input, file, options); const expected = (prepared.batches?.length ?? 1) * rounds; if (records.length === expected) { item.minimumCalls = prepared.synthesis?.status === 'ready' ? rounds : 0; item.reusedGenerations = records.length; item.freshVerificationRequired = true; } }
|
|
67
|
+
return { ...(migrationContracts ? { migrationContracts } : {}), ...(options.enableStageContinuation ? { continuation: { maxRetries: 2, ledger: 'same-approved-root', notice: 'Release-driven bounded retry of unfinished stages; accepted primary generations may resume under exact identity, verification reads/receipts are fresh. Pending remains pending if quota/deadline/dependency does not permit continuation.' } } : {}), ...(options.enableGroupPrimary ? { primaryGroups, primaryGroupNotice: businessGrouping?.dynamicInteractionCalls ? '执行时模型分组可能改变组主审单元,当前为静态估计而非确定调用承诺。' : options.enableGroupInteractions ? '相同完整正文单元可附带交互通道;仅当前代完整取证且host校验通过时替代对应专门阶段,否则回退;候选仍独立复核。' : '相同完整正文单元在预算内合并,专门交互证据阶段仍独立执行。' } : {}), ...(businessGrouping ? { businessGrouping } : {}), interactions, snapshotId: input.id, reviewRounds:rounds, maxInputBytes: options.maxInputBytes, items, minimumCalls: (businessGrouping?.minimumCalls ?? 0) + items.reduce((sum,item) => sum + item.minimumCalls, 0) + interactions.reduce((sum,item) => sum + item.minimumCalls, 0), initialRequestsOnly: true };
|
|
54
68
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
const cpp = name => /\.(?:c|cc|cpp|cxx|h|hh|hpp|hxx)$/.test(name);
|
|
4
|
+
const id = '[A-Za-z_][A-Za-z0-9_]*';
|
|
5
|
+
const signature = params => params.trim().replace(/\s+/g, ' ').replace(/\s*([*&])\s*/g, '$1');
|
|
6
|
+
|
|
7
|
+
function linesOf(text, header) {
|
|
8
|
+
if (typeof text !== 'string' || Buffer.byteLength(text) > 256 * 1024 || /\/\*|R"|\\\r?\n|[\x60]/.test(text)) return undefined;
|
|
9
|
+
const lines = text.split('\n').map(line => line.replace(/\r$/, ''));
|
|
10
|
+
const active = lines.map((line, i) => ({ line, i })).filter(row => row.line.trim() && !/^\s*\/\//.test(row.line));
|
|
11
|
+
const directives = active.filter(row => /^\s*#/.test(row.line) && !/^\s*#\s*include\s*"[^"\\]+"\s*$/.test(row.line));
|
|
12
|
+
if (directives.length) {
|
|
13
|
+
const first = active[0], second = active[1], last = active.at(-1), guard = /^\s*#\s*ifndef\s+([A-Za-z_]\w*)\s*$/.exec(first?.line ?? '');
|
|
14
|
+
if (!header || !guard || directives.length !== 3 || !new RegExp('^\\s*#\\s*define\\s+' + guard[1] + '\\s*$').test(second?.line ?? '') || !/^\s*#\s*endif\s*$/.test(last?.line ?? '')) return undefined;
|
|
15
|
+
for (const row of [first, second, last]) lines[row.i] = '';
|
|
16
|
+
}
|
|
17
|
+
if (lines.some(line => {
|
|
18
|
+
const plain = line.replace(/\/\/.*$/, '');
|
|
19
|
+
return /\b(?:namespace|template|typedef|using|virtual|override|final)\b/.test(plain) ||
|
|
20
|
+
!/^\s*#\s*include\s*"[^"\\]+"\s*$/.test(plain) && /['"]/.test(plain);
|
|
21
|
+
})) return undefined;
|
|
22
|
+
return lines;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function headers(entry) {
|
|
26
|
+
const lines = linesOf(entry.text, true);
|
|
27
|
+
if (!lines) return [];
|
|
28
|
+
const classes = [];
|
|
29
|
+
for (let i = 0; i < lines.length; i++) {
|
|
30
|
+
const start = /^\s*class\s+([A-Za-z_]\w*)\s*\{\s*$/.exec(lines[i]);
|
|
31
|
+
if (!start) continue;
|
|
32
|
+
let access = 'private', methods = [], end;
|
|
33
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
34
|
+
const plain = lines[j].replace(/\/\/.*$/, '').trim();
|
|
35
|
+
if (plain === '};') { end = j; break; }
|
|
36
|
+
if (!plain) continue;
|
|
37
|
+
if (/^(?:public|private|protected):$/.test(plain)) { access = plain.slice(0, -1); continue; }
|
|
38
|
+
const method = /^(void|bool|int)\s+([A-Za-z_]\w*)\s*\(([^;{}]*)\)\s*;\s*$/.exec(plain);
|
|
39
|
+
if (!method || /[=<>]|\bconst\b/.test(method[3])) { methods = undefined; break; }
|
|
40
|
+
methods.push({ name: method[2], returnType: method[1], params: signature(method[3]), line: j + 1, access });
|
|
41
|
+
}
|
|
42
|
+
if (end !== undefined && methods && methods.every(m => methods.filter(other => other.name === m.name).length === 1)) classes.push({ entry, name: start[1], line: i + 1, methods });
|
|
43
|
+
}
|
|
44
|
+
// Multiple/nested classes, unsupported inheritance or unmatched declaration stay unresolved.
|
|
45
|
+
const classTokens = lines.filter(line => /\bclass\b/.test(line.replace(/\/\/.*$/, ''))).length;
|
|
46
|
+
return classTokens === classes.length ? classes : [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function bodies(entry) {
|
|
50
|
+
const lines = linesOf(entry.text, false);
|
|
51
|
+
if (!lines) return [];
|
|
52
|
+
const output = [];
|
|
53
|
+
for (let i = 0; i < lines.length; i++) {
|
|
54
|
+
const match = /^\s*(void|bool|int)\s+([A-Za-z_]\w*)::([A-Za-z_]\w*)\s*\(([^;{}]*)\)\s*\{\s*$/.exec(lines[i]);
|
|
55
|
+
if (!match || /[=<>]|\bconst\b/.test(match[4])) continue;
|
|
56
|
+
let depth = 1, end;
|
|
57
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
58
|
+
const plain = lines[j].replace(/\/\/.*$/, '');
|
|
59
|
+
if (/['"\/]/.test(plain)) break;
|
|
60
|
+
for (const char of plain) { if (char === '{') depth++; else if (char === '}') depth--; }
|
|
61
|
+
if (depth === 0) { end = j + 1; break; }
|
|
62
|
+
if (depth < 0) break;
|
|
63
|
+
}
|
|
64
|
+
if (end) output.push({ entry, symbol: match[2] + '::' + match[3], className: match[2], method: match[3], returnType: match[1],
|
|
65
|
+
params: signature(match[4]), start: i + 1, end, lines });
|
|
66
|
+
}
|
|
67
|
+
return output;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Entries are a single already-approved side, with stable source identities.
|
|
71
|
+
* No source capture, semantic/type inference, inheritance or basename pairing.
|
|
72
|
+
*/
|
|
73
|
+
export function cppMigrationBindings(entries, { maxBindings = 2000 } = {}) {
|
|
74
|
+
if (!Array.isArray(entries) || entries.length > 200 || !Number.isSafeInteger(maxBindings) || maxBindings < 1 || maxBindings > 2000) throw new Error('Invalid C++ migration binding input');
|
|
75
|
+
const result = { definitions: [], bindings: [], limitations: [] }, seen = new Set();
|
|
76
|
+
for (const e of entries) {
|
|
77
|
+
if (!e || typeof e.id !== 'string' || seen.has(e.id) || typeof e.path !== 'string' || !e.source || typeof e.source.id !== 'string') throw new Error('Invalid approved C++ migration entry');
|
|
78
|
+
seen.add(e.id);
|
|
79
|
+
}
|
|
80
|
+
const all = entries.filter(e => e.eligibility === 'reviewable' && cpp(e.path)), declarations = all.flatMap(headers), implementations = all.flatMap(bodies);
|
|
81
|
+
if (declarations.length > 2000 || implementations.length > 2000) return { ...result, limitations: ['C++ migration definition budget exceeded; identities unresolved.'] };
|
|
82
|
+
const includeCache = new Map();
|
|
83
|
+
const includes = entry => {
|
|
84
|
+
if (includeCache.has(entry.id)) return includeCache.get(entry.id);
|
|
85
|
+
const lines = linesOf(entry.text, false); if (!lines) return [];
|
|
86
|
+
const edges = lines.flatMap((line, index) => {
|
|
87
|
+
const match = /^\s*#\s*include\s*"([^"\\]+)"\s*$/.exec(line); if (!match) return [];
|
|
88
|
+
const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(entry.path), match[1]));
|
|
89
|
+
if (resolved.startsWith('../') || resolved.startsWith('/') || resolved.includes(':')) return [];
|
|
90
|
+
const matches = entries.filter(e => e.path === resolved);
|
|
91
|
+
return matches.length === 1 && matches[0].eligibility === 'reviewable' ? [{ to: matches[0].id, line: index + 1 }] : [];
|
|
92
|
+
});
|
|
93
|
+
includeCache.set(entry.id, edges); return edges;
|
|
94
|
+
};
|
|
95
|
+
const rawClasses = entries.filter(e => cpp(e.path) && typeof e.text === 'string').flatMap(e =>
|
|
96
|
+
[...e.text.matchAll(/\bclass\s+([A-Za-z_]\w*)/g)].map(match => ({ name: match[1], entryId: e.id })));
|
|
97
|
+
const rawDefinitions = entries.filter(e => cpp(e.path) && typeof e.text === 'string').flatMap(e =>
|
|
98
|
+
[...e.text.matchAll(/^\s*(?:[A-Za-z_]\w*\s+)+([A-Za-z_]\w*)::([A-Za-z_]\w*)\s*\(/gm)]
|
|
99
|
+
.map(match => match[1] + '::' + match[2]));
|
|
100
|
+
const resolved = [];
|
|
101
|
+
for (const body of implementations) {
|
|
102
|
+
const candidates = declarations.filter(d => d.name === body.className && includes(body.entry).some(e => e.to === d.entry.id));
|
|
103
|
+
const uniqueClass = declarations.filter(d => d.name === body.className);
|
|
104
|
+
const duplicateBody = implementations.filter(d => d.symbol === body.symbol);
|
|
105
|
+
if (candidates.length !== 1 || uniqueClass.length !== 1 || rawClasses.filter(c => c.name === body.className).length !== 1 || duplicateBody.length !== 1 || rawDefinitions.filter(symbol => symbol === body.symbol).length !== 1) continue;
|
|
106
|
+
const declaration = candidates[0], method = declaration.methods.find(m => m.name === body.method && m.returnType === body.returnType && m.params === body.params);
|
|
107
|
+
if (!method) continue;
|
|
108
|
+
const navigation = { sourceId: declaration.entry.source.id, side: declaration.entry.source.side, path: declaration.entry.path, sourcePath: declaration.entry.source.path ?? declaration.entry.path,
|
|
109
|
+
line: method.line, className: declaration.name, symbol: body.symbol, confidence: 'exact-declaration-syntax-only' };
|
|
110
|
+
if (result.definitions.length >= 2000) { result.limitations = ['C++ migration definition limit reached; remaining identities unresolved.']; break; }
|
|
111
|
+
resolved.push({ ...body, declaration, method, navigation });
|
|
112
|
+
result.definitions.push({ fileId: body.entry.id, symbol: body.symbol, start: body.start, end: body.end, navigation });
|
|
113
|
+
}
|
|
114
|
+
for (const body of resolved) {
|
|
115
|
+
const params = body.params ? body.params.split(',').map(p => p.trim()) : [];
|
|
116
|
+
const receivers = params.map(param => new RegExp('^(' + id + ')([&*])(' + id + ')$').exec(param)).filter(Boolean);
|
|
117
|
+
for (const receiver of receivers) {
|
|
118
|
+
const [, className, pointer, local] = receiver, operator = pointer === '*' ? '->' : '.';
|
|
119
|
+
const visible = declarations.filter(d => d.name === className && includes(body.entry).some(e => e.to === d.entry.id));
|
|
120
|
+
if (visible.length !== 1 || declarations.filter(d => d.name === className).length !== 1 ||
|
|
121
|
+
rawClasses.filter(c => c.name === className).length !== 1 || local === 'this') continue;
|
|
122
|
+
const token = new RegExp('(^|[^A-Za-z0-9_])' + local + '(?=[^A-Za-z0-9_]|$)', 'g');
|
|
123
|
+
const callPattern = new RegExp('^\\s*' + local + (operator === '.' ? '\\.' : '->') + '(' + id + ')\\s*\\(([^;\"\'{}\\/]*)\\)\\s*;\\s*$');
|
|
124
|
+
const calls = []; let safe = params.filter(p => new RegExp('(^|[^A-Za-z0-9_])' + local + '(?=[^A-Za-z0-9_]|$)').test(p)).length === 1;
|
|
125
|
+
for (let i = body.start; i < body.end - 1; i++) {
|
|
126
|
+
const line = body.lines[i], occurrences = [...line.matchAll(token)]; if (!occurrences.length) continue;
|
|
127
|
+
const call = callPattern.exec(line);
|
|
128
|
+
if (!call || occurrences.length !== 1) { safe = false; break; }
|
|
129
|
+
calls.push({ method: call[1], args: call[2], line: i + 1 });
|
|
130
|
+
}
|
|
131
|
+
if (!safe) continue;
|
|
132
|
+
for (const call of calls) {
|
|
133
|
+
const method = visible[0].methods.find(m => m.name === call.method && m.access === 'public');
|
|
134
|
+
const targets = resolved.filter(d => d.className === className && d.method.name === call.method);
|
|
135
|
+
if (!method || targets.length !== 1) continue;
|
|
136
|
+
const expected = method.params ? method.params.split(',').length : 0, actual = call.args.trim() ? call.args.split(',').length : 0;
|
|
137
|
+
if (expected !== actual || !/^(?:\s*(?:[A-Za-z_]\w*|[0-9]+)(?:\s*,\s*(?:[A-Za-z_]\w*|[0-9]+))*\s*)?$/.test(call.args)) continue;
|
|
138
|
+
if (result.bindings.length >= maxBindings) { result.limitations = ['C++ migration binding limit reached; remaining identities unresolved.']; break; }
|
|
139
|
+
const target = targets[0], include = includes(body.entry).find(e => e.to === visible[0].entry.id);
|
|
140
|
+
result.bindings.push({ from: body.entry.id, to: target.entry.id, importLine: include.line, callLine: call.line,
|
|
141
|
+
symbol: target.symbol, declarationLine: target.start, navigationBindings: [body.navigation, target.navigation,
|
|
142
|
+
{ sourceId: body.entry.source.id, side: body.entry.source.side, path: body.entry.path, sourcePath: body.entry.source.path ?? body.entry.path, line: body.start,
|
|
143
|
+
className, receiver: local, receiverKind: pointer === '*' ? 'pointer-parameter' : 'reference-parameter', confidence: 'explicit-receiver-syntax-only' }] });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (resolved.length < implementations.length) result.limitations.push('Some C++ class declarations or out-of-line definitions remain ambiguous or unsupported.');
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { retrievalRequest } from './retrieval-loop.mjs';
|
|
2
|
+
import { modelWireBudget, nativeWireEnabled } from './model-wire-budget.mjs';
|
|
3
|
+
|
|
4
|
+
const positive = value => Number.isSafeInteger(value) && value > 0;
|
|
5
|
+
const contains = (outer, inner) => positive(outer?.start) && positive(outer?.count) && outer.start <= inner.start && outer.start + outer.count >= inner.start + inner.count;
|
|
6
|
+
const capacity = () => Object.assign(new Error('Optional evidence context unavailable'), { code: 'EVIDENCE_CONTEXT_CAPACITY' });
|
|
7
|
+
const optional = error => ['EVIDENCE_CONTEXT_CAPACITY', 'READER_CALL_BUDGET_EXCEEDED', 'READER_OUTPUT_BUDGET_EXCEEDED'].includes(error?.code);
|
|
8
|
+
|
|
9
|
+
function supplied(payload, source, range) {
|
|
10
|
+
if (source.side === 'context' || source.side === 'context-old') return (Array.isArray(payload.context) ? payload.context : []).some(item => item?.path === source.path && typeof item[source.side === 'context' ? 'text' : 'oldText'] === 'string');
|
|
11
|
+
if (!['old', 'new'].includes(source.side)) return false;
|
|
12
|
+
const files = [payload.file, ...(Array.isArray(payload.relatedFiles) ? payload.relatedFiles : []), ...(Array.isArray(payload.reviews) ? payload.reviews.flatMap(review => [review.file, ...(Array.isArray(review.relatedFiles) ? review.relatedFiles : [])]) : [])];
|
|
13
|
+
if (files.some(file => file?.id === source.fileId && file.path === source.path && typeof (source.side === 'old' ? file.left?.text : file.right?.text) === 'string')) return true;
|
|
14
|
+
return payload.file?.id === source.fileId && payload.file.path === source.path && payload.sourceMode === 'change-windows' && (payload.windows ?? []).some(window => typeof window?.[source.side]?.text === 'string' && contains(window[source.side], range));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// A review-lifetime navigation index, NOT evidence, authority, or a receipt store.
|
|
18
|
+
// remember() must be called only for reads admitted to an actual model input.
|
|
19
|
+
// seed() never audits or remembers its own results; the sender does that after
|
|
20
|
+
// its normal fit/beforeCall/cancellation checks, just like ordinary retrieval.
|
|
21
|
+
export function createEvidenceContext(snapshotId, { maxEntries = 128 } = {}) {
|
|
22
|
+
if (typeof snapshotId !== 'string' || !snapshotId || snapshotId.length > 500 || !positive(maxEntries) || maxEntries > 512) throw new Error('Invalid evidence context options');
|
|
23
|
+
const entries = new Map();
|
|
24
|
+
return Object.freeze({
|
|
25
|
+
size: () => entries.size,
|
|
26
|
+
remember(records) {
|
|
27
|
+
if (!Array.isArray(records) || records.length > 256) throw new Error('Invalid evidence context records');
|
|
28
|
+
for (const record of records) {
|
|
29
|
+
if (record?.kind !== 'read' || record.snapshotId !== snapshotId || record.omitted === true || typeof record.sourceId !== 'string' || !record.sourceId || record.sourceId.length > 200 || typeof record.hash !== 'string' || !/^[a-f0-9]{64}$/.test(record.hash) || !positive(record.start) || !positive(record.count) || record.count > 200 || !Number.isSafeInteger(record.start + record.count)) continue;
|
|
30
|
+
const item = { sourceId: record.sourceId, hash: record.hash, start: record.start, count: record.count };
|
|
31
|
+
const key = JSON.stringify(item);
|
|
32
|
+
if (entries.has(key)) continue;
|
|
33
|
+
if (entries.size >= maxEntries) entries.delete(entries.keys().next().value);
|
|
34
|
+
entries.set(key, item);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
seed(scope, request, { sourceIds = [], ranges = [], existing = [], maxReads = 4, maxLines = 80, maxBytes = 16384, maxInputBytes = 98304, reserveCalls = 2, reserveBytes = 4096, signal = request?.signal, buildRequest = records => retrievalRequest(request, scope, records) } = {}) {
|
|
38
|
+
if (!Array.isArray(sourceIds) || sourceIds.length > 200 || sourceIds.some(id => typeof id !== 'string' || !id || id.length > 200) || !Array.isArray(ranges) || ranges.length > 200 || ranges.some(range => typeof range?.sourceId !== 'string' || !positive(range.start) || !positive(range.count) || !Number.isSafeInteger(range.start + range.count)) || !Array.isArray(existing) || existing.length > 50 || !Number.isSafeInteger(maxReads) || maxReads < 0 || maxReads > 8 || !positive(maxLines) || maxLines > 200 || !positive(maxBytes) || maxBytes > 65536 || !positive(maxInputBytes) || !Number.isSafeInteger(reserveCalls) || reserveCalls < 0 || reserveCalls > 100 || !Number.isSafeInteger(reserveBytes) || reserveBytes < 0 || reserveBytes > 1048576 || typeof buildRequest !== 'function' || signal !== undefined && !(signal instanceof AbortSignal) || request?.signal !== undefined && !(request.signal instanceof AbortSignal)) throw new Error('Invalid evidence context seed options');
|
|
39
|
+
const check = () => { request?.signal?.throwIfAborted(); signal?.throwIfAborted(); };
|
|
40
|
+
check();
|
|
41
|
+
const seeds = [];
|
|
42
|
+
if (!maxReads || !entries.size || !sourceIds.length && !ranges.length) return seeds;
|
|
43
|
+
if (typeof request?.input !== 'string' || typeof request.instructions !== 'string') throw new Error('Invalid evidence context request');
|
|
44
|
+
const payload = JSON.parse(request.input);
|
|
45
|
+
if (payload?.snapshotId !== snapshotId || typeof scope?.transaction !== 'function' || typeof scope.preflight !== 'function' || typeof scope.catalog !== 'function' || typeof scope.read !== 'function' || typeof scope.budget !== 'function') return seeds;
|
|
46
|
+
const catalog = scope.catalog(), selected = new Set(sourceIds);
|
|
47
|
+
const attempted = new Set();
|
|
48
|
+
let attempts = 0;
|
|
49
|
+
try {
|
|
50
|
+
return scope.transaction(() => {
|
|
51
|
+
for (const entry of entries.values()) {
|
|
52
|
+
check();
|
|
53
|
+
if (attempts >= maxReads) break;
|
|
54
|
+
const sources = catalog.filter(source => source.id === entry.sourceId);
|
|
55
|
+
if (sources.length !== 1) continue;
|
|
56
|
+
const source = sources[0];
|
|
57
|
+
if (!positive(source.lineCount) || entry.start > source.lineCount || source.hash !== undefined && source.hash !== entry.hash) continue;
|
|
58
|
+
const overlap = ranges.find(range => range.sourceId === entry.sourceId && range.start < entry.start + entry.count && entry.start < range.start + range.count);
|
|
59
|
+
if (!selected.has(entry.sourceId) && !overlap) continue;
|
|
60
|
+
const start = overlap ? Math.max(entry.start, overlap.start) : entry.start;
|
|
61
|
+
const count = Math.min(maxLines, entry.start + entry.count - start, source.lineCount - start + 1, overlap ? overlap.start + overlap.count - start : Infinity);
|
|
62
|
+
const operation = { kind: 'read', id: source.id, start, count };
|
|
63
|
+
const key = JSON.stringify(operation);
|
|
64
|
+
if (attempted.has(key) || supplied(payload, source, operation)) continue;
|
|
65
|
+
if ([...existing, ...seeds].some(item => item?.request?.kind === 'read' && item.result?.snapshotId === snapshotId && item.result.sourceId === source.id && item.result.hash === entry.hash && typeof item.result.text === 'string' && contains(item.result, operation))) continue;
|
|
66
|
+
attempted.add(key); attempts++;
|
|
67
|
+
check();
|
|
68
|
+
const budget = scope.budget(), cost = scope.preflight([operation]);
|
|
69
|
+
const cached = budget.reusableResults === true && cost?.calls === 0;
|
|
70
|
+
if ((!cached && budget.remainingCalls <= reserveCalls) || budget.remainingOutputBytes <= reserveBytes) throw capacity();
|
|
71
|
+
const result = scope.read(operation);
|
|
72
|
+
check();
|
|
73
|
+
if (result.snapshotId !== snapshotId || result.sourceId !== source.id || result.hash !== entry.hash || result.start !== start || result.count !== count || typeof result.text !== 'string' || result.receiptId !== undefined || result.omitted === true) throw capacity();
|
|
74
|
+
const proposed = [...seeds, { request: operation, result }];
|
|
75
|
+
if (Buffer.byteLength(JSON.stringify(proposed)) > maxBytes) throw capacity();
|
|
76
|
+
const next = buildRequest([...existing, ...proposed]);
|
|
77
|
+
if (typeof next?.input !== 'string' || typeof next.instructions !== 'string') throw new Error('Invalid evidence context request builder');
|
|
78
|
+
const fits = nativeWireEnabled(request, scope) ? modelWireBudget({ ...next, nativeOutputTools: true }, maxInputBytes).fits : Buffer.byteLength(next.input) + Buffer.byteLength(next.instructions) <= maxInputBytes;
|
|
79
|
+
check();
|
|
80
|
+
const remaining = scope.budget();
|
|
81
|
+
if (!fits || (!cached && remaining.remainingCalls < reserveCalls) || remaining.remainingOutputBytes < reserveBytes) throw capacity();
|
|
82
|
+
seeds.push({ request: operation, result });
|
|
83
|
+
}
|
|
84
|
+
check();
|
|
85
|
+
return seeds;
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
check();
|
|
89
|
+
if (!optional(error)) throw error;
|
|
90
|
+
// One rejected item abandons the entire optional batch: previously read
|
|
91
|
+
// siblings must not consume bytes/calls when no seed array is returned.
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|