@feng0531/dsh-code-review 0.87.0 → 0.87.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.
- package/CHANGELOG.md +15 -0
- package/CORE-SOURCE-INVENTORY.md +1 -1
- package/DEVELOPMENT.md +3 -3
- package/README.md +1 -1
- package/RELEASE-NOTES.md +11 -15
- package/package.json +1 -1
- package/src/background-review.mjs +6 -1
- package/src/cpp-migration-bindings.mjs +30 -0
- package/src/group-primary.mjs +6 -2
- package/src/line-diff.mjs +57 -1
- package/src/migration-contracts.mjs +87 -3
- package/src/migration-regression.mjs +71 -7
- package/src/preview-validity.mjs +9 -0
- package/src/primary-input.mjs +2 -14
- package/src/project-review-service.mjs +6 -3
- package/src/prompts.mjs +1 -1
- package/src/retrieval-loop.mjs +26 -5
- package/src/review-change-map.mjs +17 -0
- package/src/review-resume.mjs +95 -9
- package/src/review-stage-state.mjs +7 -2
- package/src/review.mjs +84 -25
- package/src/service.mjs +8 -3
- package/src/stage-continuation.mjs +12 -2
- package/src/stage-generation-cache.mjs +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
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.2 — 导出案例回归修复
|
|
6
|
+
|
|
7
|
+
- 大文件稀疏、远距离编辑增加有界最短编辑回退,避免二维差异表超限后不必要地扩大为近全文包络;超预算仍明确受限。
|
|
8
|
+
- 文件/窗口主审在局部额度不足时保留同次检索链,等待其他阶段释放容量后有限续跑;不重置读取总账、轮数或截止时间,不重放已接受生成。
|
|
9
|
+
- 正常组主审仍合并调用;仅真实局部读取预算不足且尚未接受组成果时回退可续跑成员主审,保留已消费总账。风险规划开启时,组响应明确要求各成员提供 `risks`(允许空数组),消除可选提示与验收要求矛盾。
|
|
10
|
+
- 迁移对照保留旧公开校验入口与旧委托实现;新增删除导出/单例/不同名入口的有限词法导航,歧义与未证实因果仍为 uncertain。
|
|
11
|
+
- 预览返回有效截止时间,区分缺失、过期及模型/目录变化;后台摘要说明调用下限不是实际成本上限,阶段限制与全局余额分别展示。
|
|
12
|
+
|
|
13
|
+
## 0.87.1 — 修复阶段续审缓存中断
|
|
14
|
+
|
|
15
|
+
- 主审、窗口归因和续审导入从同一批准原文重建包络坐标;包络仍不表示精确编辑或回归因果。
|
|
16
|
+
- 阶段生成先校验再进入缓存,单条拒绝或容量不足不污染已保存记录、不再在文件收尾中止整任务;当前发现与独立复核保留。
|
|
17
|
+
- 组成员、组级和宿主限制按组合后的有界数量保存,单次模型响应仍最多50条;续审不继承旧复核回执。
|
|
18
|
+
- 续审导出总容量不足时仅保存适配的完整记录并披露省略数量,不中止当前审查;非法导入、取消及真实落盘错误不被容量退化吞掉。
|
|
19
|
+
|
|
5
20
|
## 0.87.0 — 连续取证、迁移契约与阶段续审
|
|
6
21
|
|
|
7
22
|
- 当前阶段按批准来源重新投递有限相关正文,交互风险可内联;未送入模型不计证据,缓存仍扣字节,独立复核使用新回执。
|
package/CORE-SOURCE-INVENTORY.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
供维护者核查实现依据。使用方法见 [README](README.md),开发优先级见 [能力清单](CORE-ALIGNMENT-TASKS.md)。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
上一轮完整对照固定为 alibaba/open-code-review 提交 `216dcf513593055a70165f793e96b52589d71b1b`。本轮导出案例修复另核实上游 HEAD `8f9c8b771f65bed63a193e873f12cdffb050b748`,只补读 `internal/tool/file_read_diff.go` 的缺失差异导航和 `internal/diff/git.go` 的 tracked/gitignored 范围修复,不宣称完成该版本全量对照。差异算法参考旧固定版本 `internal/suggestdiff/diff.go` 与 `internal/diff/hunk.go`,不照搬无界二维表或忽略大小写/空白的对齐。下方文件索引仍是旧提交 `2d67596c961f80436deb2afa643efa2b1725d34c` 的历史阅读记录;未把旧记录冒充新提交逐文件复审。
|
|
6
6
|
|
|
7
7
|
## 对照结论
|
|
8
8
|
|
package/DEVELOPMENT.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
npm test
|
|
11
11
|
node scripts/host-smoke.mjs <DSH-checkout>
|
|
12
12
|
|
|
13
|
-
宿主检查加载真实 DSH 工具运行时,但使用受控模型响应,不是 GUI
|
|
13
|
+
宿主检查加载真实 DSH 工具运行时,但使用受控模型响应,不是 GUI 安装或真实模型质量测试。若安装路径位于 Electron `app.asar` 内,普通 Node 不识别归档虚拟路径;PowerShell 可用 `$env:ELECTRON_RUN_AS_NODE="1"; & "<安装目录>/DeepSeek Harness.exe" scripts/host-smoke.mjs "<安装目录>/resources/app.asar/dsh"` 运行归档感知的同一检查,不解包或修改宿主。Windows SVN 需要支持 UTF-8 路径;CI 使用临时工具,不修改用户安装。
|
|
14
14
|
|
|
15
15
|
## 会话与模式
|
|
16
16
|
|
|
@@ -42,11 +42,11 @@ JSON合同新增280字节传输提示防止原始换行/反斜线转义错误
|
|
|
42
42
|
|
|
43
43
|
连续正文投递的索引仅保存本次批准且实际发送的坐标;当前 stage reader 重新取原文并扣字节,不共享旧 receipt。可选初始正文事务延迟到调用预算准入后,预算否决/取消/输入不适配必须回滚,未送入模型不发布 audit。交互内联风险缺失/非法不抹合法 findings,不补发规划。
|
|
44
44
|
|
|
45
|
-
迁移元数据是宿主生成的 side-qualified
|
|
45
|
+
迁移元数据是宿主生成的 side-qualified 导航,不是模型授权。`old-delegation` 保留旧公开入口、旧实现及新同源入口,非 uncertain 判定要求本代 receipt 完整覆盖三个函数。删除导出入口、单例调用与不同名新入口的 `deleted-export-route` 只交付词法候选导航,强制 uncertain;不证明类型、宏语义、实参等价或共享状态身份,复杂/歧义语法明确记录不可用。跨文件 regression 只用已批准 contract ID、来源/角色/精确编辑和独立已校验 receipt,默认旧同主文件协议仍严格。C++ 仅在字面 include、唯一类声明/类外定义和明确 receiver 下产生保守线索,重载/继承/虚分派/宏不能猜测唯一绑定。
|
|
46
46
|
|
|
47
47
|
精确完整组的根可选 interactions 通道独立于合法 primary 结果验收。仅本代读取覆盖全部成员编辑、source/snapshot/hash 及跨文件引用验收成功后替代匹配专门生成;非法或缺失不触发隐藏主审重跑,候选仍用独立复核。原生 schema、文本合同和可选开销进入真实 wire 预算,预览 minimumCalls 是有条件下限并披露 fallbackMinimumCalls。
|
|
48
48
|
|
|
49
|
-
额度让出不抢 live unit
|
|
49
|
+
额度让出不抢 live unit;文件/窗口 primary 在同次执行以内存闭包保留待执行请求、已送结果、检索轮数和单次纠正许可,其他阶段 release 后有限 FIFO 继续,共享原调用/输出总账和绝对截止。正常 group-primary 仍一次合并生成;仅尚未接受组生成且真实局部分配失败、root 可承接请求时,明确回退成员 primary。预留成员 fallback floor 会降低当时组可用额度,成功组立即释放 floor;不重放已接受组,不给新总账。schema2 只保存身份校验过的 primary generation,不持久化成可复用的新证据 receipt;恢复只省生成,交互/综合及验证重新从安全边界执行。没有进展保留 pending/partial,取消不自动变为完成。
|
|
50
50
|
|
|
51
51
|
受控集成验证入口为 `test/ocr-refactor-integration.test.mjs`、`test/ocr-complete-policy.test.mjs` 及 group/migration/stage 专项测试;其中开关对照 7→5、组通道 5→3 是固定离线响应的模型调用数,不是提供方速度或召回保证。正式发布仍执行下文完整门禁,不把文档更新或本地通过当作已发布。
|
|
52
52
|
|
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ dsh plugin add https://github.com/f-e-n-g-0531/dsh-code-review.git
|
|
|
53
53
|
确认刚才展示的预览范围和模型,使用该 previewId 执行只读审查。
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
插件使用预览返回的 ID 执行。默认预览有效期约 **5
|
|
56
|
+
插件使用预览返回的 ID 执行。默认预览有效期约 **5 分钟**,返回 `expiresAt` 与 `validForMs`;到期、文件、模型或会话目录变化后,需要重新预览和确认。计划最少调用只是初始下限,不是总调用数或耗时上限,后续取证、复核与综合会增加工作量。宿主权限不允许时,确认也不能绕过限制。
|
|
57
57
|
|
|
58
58
|
### 后台执行如何等待
|
|
59
59
|
|
package/RELEASE-NOTES.md
CHANGED
|
@@ -1,27 +1,23 @@
|
|
|
1
|
-
# v0.87.
|
|
1
|
+
# v0.87.2 发布说明
|
|
2
2
|
|
|
3
3
|
使用方式见 [README](README.md),历史变化见 [变更记录](CHANGELOG.md),发布门禁见 [开发说明](DEVELOPMENT.md)。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 差异交付与预算恢复
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
大文件稀疏、远距离编辑增加有界最短编辑回退,避免差异表超限后不必要地扩大为近全文包络;耗尽预算仍明确受限。文件/窗口主审保留同次检索链,等待其他阶段释放局部额度后有限续跑,不重置总账、检索轮数、纠正许可或超时。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
正常组主审仍合并调用;仅真实局部分配失败且尚未接受组结果时明确回退成员主审,保留已消费预算、不回放已接受生成。组风险规划明确要求各成员提供 risks(允许空数组);缺失或畸形仍报告限制,合法发现保留,不隐藏补发规划。
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## 迁移对照与安全边界
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
迁移导航保留旧公开入口、旧委托实现及新入口。删除导出、单例调用和不同名新入口仅提供有限词法候选导航,强制 uncertain,不证明类型、宏语义、共享状态身份或回归因果。不支持或歧义的旧入口明确报告限制。
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
全部宿主迁移合同从批准快照独立重建并匹配完整身份,防止删除种类、基线或来源字段降级绕过限制;拒绝不可信访问器、Proxy 和自定义序列化钩子。独立复核仍使用本代新证据回执。
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
预览返回有效截止时间并区分失效原因;后台分别显示阶段限制与全局余量,调用下限不是成本或耗时上限。批准等待中的取消或模型变化不能发送源码。
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
## 验证与交付
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
修复冻结本地完整回归1309项:1305通过、0失败、4跳过;最终独立复核定向65项通过。真实DSH ToolRuntime加载/工具注册、包内容与差异检查通过。这些不是业务项目真实模型重跑,也不保证召回率或实际耗时。
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
受控离线夹具覆盖开关实际调用对照、原生/文本预算、无效可选通道回退、新回执、取消回滚及续审配置绑定。它们验证机制,不是用户业务重跑,也不证明真实模型召回、速度或成本。增量多轮早停、多轮/窗口组主审、推理记忆压缩和安全检索模式仍未实现,见 [能力清单](CORE-ALIGNMENT-TASKS.md)。
|
|
26
|
-
|
|
27
|
-
正式交付仍须完成冻结标签 Ubuntu/Windows × Node.js22/24 四矩阵及 Actions、npm、GitHub Release 原始安装包逐字节与校验清单核验。本说明不是已发布或已安装声明;发布不会自动升级或重启当前 DSH。
|
|
23
|
+
正式发布须通过冻结标签 Ubuntu/Windows × Node.js22/24 四矩阵,并核验 Actions、npm 与 GitHub Release 的同一原始安装包及校验清单。发布不自动安装、升级或重启当前宿主。
|
package/package.json
CHANGED
|
@@ -36,7 +36,10 @@ function terminalSummary(value) {
|
|
|
36
36
|
}
|
|
37
37
|
const reasons = Array.isArray(d?.readerLimitReasons) ? d.readerLimitReasons : [];
|
|
38
38
|
const bounded = LIMIT_CODES.flatMap(code => { const row = reasons.find(item => item?.code === code); return row ? [code + ' ' + count(row.calls)] : []; });
|
|
39
|
-
if (bounded.length)
|
|
39
|
+
if (bounded.length) {
|
|
40
|
+
lines.push('读取限制原因:' + bounded.join(';'));
|
|
41
|
+
if (d?.readerBudget?.remainingCalls > 0 && d?.readerBudget?.remainingOutputBytes > 0) lines.push('全局读取预算仍有余额;阶段分配或检索轮次限制仍可能使范围未完成,不能据余额认定可以继续或已经通过。');
|
|
42
|
+
}
|
|
40
43
|
if (d?.readerLimits) lines.push('每阶段最大检索轮次:' + count(d.readerLimits.maxRounds));
|
|
41
44
|
if (d?.artifacts) lines.push('私有工件计数:留存 ' + count(d.artifacts.retained) + ',未留存 ' + count(d.artifacts.omitted) + '(不包含原始正文)');
|
|
42
45
|
if (value.resultPage) lines.push('当前页:文件 ' + (value.resultPage.files?.length ?? 0) + '/' + count(value.resultPage.totalFiles) + ',发现 ' + (value.resultPage.findings?.length ?? 0) + '/' + count(value.resultPage.totalFindings) + ';code_review_status 的 offset/limit 仅切换结果页。');
|
|
@@ -44,6 +47,8 @@ function terminalSummary(value) {
|
|
|
44
47
|
} else if (report?.previewId) {
|
|
45
48
|
lines.push('预览已完成,尚未执行审查;请展示批准范围并取得用户确认后再执行。');
|
|
46
49
|
lines.push('预览文件总量:' + count(report.filePage?.total ?? report.files?.length) + ';计划最少调用:' + count(report.plan?.minimumCalls));
|
|
50
|
+
lines.push('最少调用仅为初始计划下限,不是总调用数或耗时上限;后续取证、复核及综合会增加工作量。');
|
|
51
|
+
if (typeof report.expiresAt === 'string' && Number.isFinite(Date.parse(report.expiresAt))) lines.push('预览有效至:' + new Date(report.expiresAt).toISOString() + ';过期后须重新预览并确认。');
|
|
47
52
|
} else {
|
|
48
53
|
lines.push('未提供完整报告摘要;终态不会自动恢复或重新发送源码。请检查已保存检查点和未完成范围。');
|
|
49
54
|
}
|
|
@@ -4,6 +4,33 @@ const cpp = name => /\.(?:c|cc|cpp|cxx|h|hh|hpp|hxx)$/.test(name);
|
|
|
4
4
|
const id = '[A-Za-z_][A-Za-z0-9_]*';
|
|
5
5
|
const signature = params => params.trim().replace(/\s+/g, ' ').replace(/\s*([*&])\s*/g, '$1');
|
|
6
6
|
|
|
7
|
+
// Evidence coordinates only, never resolved C++ call or reachability identities.
|
|
8
|
+
export function cppExportRouteDefinitions(text) {
|
|
9
|
+
if (typeof text !== 'string' || Buffer.byteLength(text) > 256 * 1024 || /\/\*|R"|\\\r?\n|[\x60]/.test(text)) return [];
|
|
10
|
+
const lines = text.split('\n').map(line => line.replace(/\r$/, '').replace(/\/\/.*$/, ''));
|
|
11
|
+
if (lines.some(line => /^\s*#\s*(?!include\b)/.test(line) || /\b(?:namespace|template|using|typedef)\b/.test(line))) return [];
|
|
12
|
+
const output = [];
|
|
13
|
+
for (let i = 0; i < lines.length; i++) {
|
|
14
|
+
const match = /^\s*(?:(extern\s+"C")\s+(?:[A-Z_]\w*\s+)?)?(void|bool|int)\s+((?:[A-Za-z_]\w*::)?[A-Za-z_]\w*)\s*\(([^(){};]*)\)\s*(\{)?\s*$/.exec(lines[i]);
|
|
15
|
+
if (!match || !match[1] && !match[3].includes('::')) continue;
|
|
16
|
+
const params = match[4].split(',').map(p => p.trim());
|
|
17
|
+
if (!params.every(p => /^(?:const\s+)?[A-Za-z_]\w*\s*[*&]?\s+[A-Za-z_]\w*$/.test(p))) continue;
|
|
18
|
+
const brace = match[5] ? i : i + 1;
|
|
19
|
+
if (!match[5] && !/^\s*\{\s*$/.test(lines[brace] ?? '')) continue;
|
|
20
|
+
let depth = 1, end;
|
|
21
|
+
for (let j = brace + 1; j < lines.length; j++) {
|
|
22
|
+
if (/['"\\]/.test(lines[j])) break;
|
|
23
|
+
for (const c of lines[j]) { if (c === '{') depth++; else if (c === '}') depth--; }
|
|
24
|
+
if (depth === 0) { end = j + 1; break; }
|
|
25
|
+
if (depth < 0) break;
|
|
26
|
+
}
|
|
27
|
+
if (end) output.push({ symbol: match[3], exported: Boolean(match[1]), params: signature(match[4]),
|
|
28
|
+
names: params.map(p => /([A-Za-z_]\w*)$/.exec(p)[1]), start: i + 1, bodyStartLine: brace + 2, end, lines: lines.slice(brace + 1, end - 1) });
|
|
29
|
+
if (output.length > 100) return [];
|
|
30
|
+
}
|
|
31
|
+
return output;
|
|
32
|
+
}
|
|
33
|
+
|
|
7
34
|
function linesOf(text, header) {
|
|
8
35
|
if (typeof text !== 'string' || Buffer.byteLength(text) > 256 * 1024 || /\/\*|R"|\\\r?\n|[\x60]/.test(text)) return undefined;
|
|
9
36
|
const lines = text.split('\n').map(line => line.replace(/\r$/, ''));
|
|
@@ -36,6 +63,9 @@ function headers(entry) {
|
|
|
36
63
|
if (!plain) continue;
|
|
37
64
|
if (/^(?:public|private|protected):$/.test(plain)) { access = plain.slice(0, -1); continue; }
|
|
38
65
|
const method = /^(void|bool|int)\s+([A-Za-z_]\w*)\s*\(([^;{}]*)\)\s*;\s*$/.exec(plain);
|
|
66
|
+
// Plain fields do not bind calls; they only allow the containing declaration
|
|
67
|
+
// to remain navigable. Initializers, aliases and function pointers stay unsupported.
|
|
68
|
+
if (!method && /^(?:bool|int|float|double|[A-Za-z_]\w*\s*[*&]?)\s+[A-Za-z_]\w*\s*;$/.test(plain)) continue;
|
|
39
69
|
if (!method || /[=<>]|\bconst\b/.test(method[3])) { methods = undefined; break; }
|
|
40
70
|
methods.push({ name: method[2], returnType: method[1], params: signature(method[3]), line: j + 1, access });
|
|
41
71
|
}
|
package/src/group-primary.mjs
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { preparePrimaryInput } from './primary-input.mjs';
|
|
2
2
|
import { initialInputBudget } from './input-budget.mjs';
|
|
3
|
-
import { INLINE_RISK_INSTRUCTIONS } from './prompts.mjs';
|
|
4
3
|
import { validateRiskPlan } from './risk-plan.mjs';
|
|
5
4
|
import { GROUP_INTERACTION_INSTRUCTIONS } from './group-interaction.mjs';
|
|
6
5
|
|
|
7
6
|
export const GROUP_PRIMARY_INSTRUCTIONS = '批准的group-primary输入包含多个完整主文件。逐个审核所有reviews成员的新旧正文,并结合组内其他文件分析;不能仅审核一个文件。返回 {"reviews":[{"fileId":"原成员身份","findings":[],"limitations":[]}],"limitations":[]},每个成员恰好一项,包括没有发现的成员。各finding仍需原审查指令全部字段、精确原行anchor及该fileId本次变化归因;不能用其他成员的身份或坐标替代。缺证据写limitations,不声明跨文件综合已完成;专门交互阶段仍独立校验。可按批准检索协议先返回requests。';
|
|
8
7
|
|
|
8
|
+
// Group output has a different envelope from single-file inline review. Keep the
|
|
9
|
+
// required risk field on each member, without inheriting optional/omit guidance.
|
|
10
|
+
const GROUP_PRIMARY_RISK_INSTRUCTIONS = '每个reviews成员必须包含risks字段(数组,可为[]),禁止省略或放到顶层;没有真实风险或精确编辑时返回risks:[],不要为满足字段要求编造风险。组风险可引用本组所有成员的精确编辑,不增加规划调用。风险仅为待证假设,不是证据、缺陷或扩大范围/读取权限的授权;sourceIds不证明已读。先审核全部成员编辑与反证,再报告findings。中间可按批准检索协议返回requests,但不得与最终结果混用。Risk shape: {id:"r1",fileIds:["reviews[].fileId"],editRefs:[{fileId:"reviews[].fileId",editId:"changes.edits[].id"}],question:"Risk?",trigger:"Condition",impact:"Failure",checks:["Countercheck"],sourceIds:[]}. Exactly these 8 keys, all required. Each member risks: 0..20; serialized {risks} JSON UTF-8<=65536 bytes. id/question/trigger/impact and each check: nonblank string, JS length<=2000; id unique within each member risks. fileIds: 1..4 unique approved group member IDs. editRefs: 1..20 unique objects with ONLY string fileId/editId; fileId in this risk.fileIds and editId owned by that file. checks: 1..8 unique strings. sourceIds: 0..8 unique catalog IDs (no catalog: []), never paths. Copy real IDs, never guess.';
|
|
11
|
+
|
|
9
12
|
export function prepareGroupPrimary(snapshot, group, { instructions, scope, maxInputBytes, focusPrimaryContext = false, enableRiskPlanning = false, enableGroupInteractions = false, migrationContracts = [] } = {}) {
|
|
10
13
|
if (typeof enableGroupInteractions !== 'boolean') throw new Error('Invalid group interaction option');
|
|
11
14
|
if (!scope || snapshot.projectScope || !Array.isArray(group?.fileIds) || group.fileIds.length < 2 || group.fileIds.length > 4 || new Set(group.fileIds).size !== group.fileIds.length) return null;
|
|
@@ -18,7 +21,8 @@ export function prepareGroupPrimary(snapshot, group, { instructions, scope, maxI
|
|
|
18
21
|
for (const item of reviews) { delete item.context; delete item.rules; delete item.businessRequirement; delete item.requirementNotice; }
|
|
19
22
|
const value = { snapshotId: snapshot.id, sourceMode: 'group-primary', groupId: group.id, reviews, context, rules: snapshot.rules ?? [], ...(snapshot.businessRequirement !== undefined ? { businessRequirement: snapshot.businessRequirement } : {}) };
|
|
20
23
|
let payload = JSON.stringify(value);
|
|
21
|
-
|
|
24
|
+
const groupInstructions = enableRiskPlanning ? GROUP_PRIMARY_INSTRUCTIONS.replace('"findings":[],"limitations":[]}', '"findings":[],"limitations":[],"risks":[]}') + '\n' + GROUP_PRIMARY_RISK_INSTRUCTIONS : GROUP_PRIMARY_INSTRUCTIONS;
|
|
25
|
+
let combined = instructions + '\n' + groupInstructions;
|
|
22
26
|
if (!initialInputBudget({ input: payload, instructions: combined }, scope, maxInputBytes).fits) return null;
|
|
23
27
|
const contracts = migrationContracts.filter(contract => contract.candidateFileIds?.some(id => group.fileIds.includes(id)));
|
|
24
28
|
if (contracts.length) { const proposed = JSON.stringify({ ...JSON.parse(payload), migrationContracts: contracts, migrationNotice: 'Host navigation hypotheses only. Compare the old public gate, new entry and callers under the same trigger. Neither semantics nor causality are established; independent verification is required.' }); if (initialInputBudget({ input: proposed, instructions: combined }, scope, maxInputBytes).fits) payload = proposed; }
|
package/src/line-diff.mjs
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
import { changeRegion } from './change-region.mjs';
|
|
2
2
|
|
|
3
|
+
// Myers frontier search avoids quadratic rectangles for sparse, distant edits.
|
|
4
|
+
// Charge both retained trace cells and equality probes to the same hard budget;
|
|
5
|
+
// exhaustion discards the entire script, never promoting a partial alignment.
|
|
6
|
+
function boundedMatches(a, b, offset, n, m, budget) {
|
|
7
|
+
const trace = [];
|
|
8
|
+
let used = 0;
|
|
9
|
+
for (let d = 0; d <= n + m; d++) {
|
|
10
|
+
if (used + d + 1 > budget) return null;
|
|
11
|
+
used += d + 1;
|
|
12
|
+
const row = new Int32Array(d + 1), previous = trace.at(-1);
|
|
13
|
+
for (let p = 0; p <= d; p++) {
|
|
14
|
+
const k = 2 * p - d;
|
|
15
|
+
let x = d === 0 ? 0 : p === 0 ? previous[0] : p === d ? previous[p - 1] + 1
|
|
16
|
+
: previous[p - 1] < previous[p] ? previous[p] : previous[p - 1] + 1;
|
|
17
|
+
let y = x - k;
|
|
18
|
+
while (x < n && y < m) {
|
|
19
|
+
if (++used > budget) return null;
|
|
20
|
+
if (a[offset + x] !== b[offset + y]) break;
|
|
21
|
+
x++; y++;
|
|
22
|
+
}
|
|
23
|
+
row[p] = x;
|
|
24
|
+
if (x >= n && y >= m) {
|
|
25
|
+
const matches = [];
|
|
26
|
+
for (let depth = d; depth > 0; depth--) {
|
|
27
|
+
const prev = trace[depth - 1], index = (x - y + depth) / 2;
|
|
28
|
+
const down = index === 0 || (index !== depth && prev[index - 1] < prev[index]);
|
|
29
|
+
const prevX = prev[down ? index : index - 1];
|
|
30
|
+
const prevY = prevX - (x - y + (down ? 1 : -1));
|
|
31
|
+
const count = Math.min(x - prevX, y - prevY);
|
|
32
|
+
if (count > 0) matches.push([x - count, y - count, count]);
|
|
33
|
+
x = prevX; y = prevY;
|
|
34
|
+
}
|
|
35
|
+
if (x > 0) matches.push([0, 0, x]);
|
|
36
|
+
return matches.reverse();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
trace.push(row);
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
3
44
|
export function lineDiff(before, after, { maxCells = 200000, ...options } = {}) {
|
|
4
45
|
if (!Number.isSafeInteger(maxCells) || maxCells < 1 || maxCells > 2000000) throw new RangeError('Invalid cell limit');
|
|
5
46
|
const region = changeRegion(before, after, options);
|
|
@@ -7,7 +48,22 @@ export function lineDiff(before, after, { maxCells = 200000, ...options } = {})
|
|
|
7
48
|
const split = text => text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
8
49
|
const a = split(before), b = split(after);
|
|
9
50
|
const offset = region.old.start - 1, n = region.old.count, m = region.new.count;
|
|
10
|
-
if ((n + 1) * (m + 1) > maxCells)
|
|
51
|
+
if ((n + 1) * (m + 1) > maxCells) {
|
|
52
|
+
const matches = boundedMatches(a, b, offset, n, m, maxCells);
|
|
53
|
+
if (!matches) return { status: 'limited', reason: 'Diff cell limit exceeded', envelope: region, edits: [] };
|
|
54
|
+
const edits = [];
|
|
55
|
+
let i = 0, j = 0;
|
|
56
|
+
for (const [nextI, nextJ, count] of [...matches, [n, m, 0]]) {
|
|
57
|
+
if (nextI > i || nextJ > j) edits.push({
|
|
58
|
+
old: { start: offset + i + 1, count: nextI - i },
|
|
59
|
+
new: { start: offset + j + 1, count: nextJ - j },
|
|
60
|
+
removed: a.slice(offset + i, offset + nextI).join(''),
|
|
61
|
+
added: b.slice(offset + j, offset + nextJ).join(''),
|
|
62
|
+
});
|
|
63
|
+
i = nextI + count; j = nextJ + count;
|
|
64
|
+
}
|
|
65
|
+
return { status: 'changed', precision: 'exact', edits };
|
|
66
|
+
}
|
|
11
67
|
const width = m + 1, table = new Uint32Array((n + 1) * width);
|
|
12
68
|
for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) {
|
|
13
69
|
table[i * width + j] = a[offset + i] === b[offset + j] ? 1 + table[(i + 1) * width + j + 1] : Math.max(table[(i + 1) * width + j], table[i * width + j + 1]);
|
|
@@ -3,7 +3,7 @@ import { createRetrievalScope } from './retrieval-scope.mjs';
|
|
|
3
3
|
import { inferImportRelations } from './import-relations.mjs';
|
|
4
4
|
import { inferCppRelations } from './cpp-relations.mjs';
|
|
5
5
|
import { buildReviewGroups } from './review-groups.mjs';
|
|
6
|
-
import { cppMigrationBindings } from './cpp-migration-bindings.mjs';
|
|
6
|
+
import { cppMigrationBindings, cppExportRouteDefinitions } from './cpp-migration-bindings.mjs';
|
|
7
7
|
|
|
8
8
|
const NOTICE = 'Conservative syntax navigation only; not semantic migration, call reachability, authorization, read coverage or causality proof.';
|
|
9
9
|
const cmp = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -148,9 +148,93 @@ export function buildMigrationContracts(snapshot, { maxContracts = 32, maxGroupF
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
+
// An old public route may stop delegating altogether. Retain that old-side
|
|
152
|
+
// edge and compare only the same approved file/function coordinates on the
|
|
153
|
+
// new side; a matching name in another file is never a migration binding.
|
|
154
|
+
for (const bridge of graph.old) {
|
|
155
|
+
const enclosing = (definitions.get('old:' + bridge.from) ?? []).filter(d => bridge.callLine > d.start && bridge.callLine < d.end);
|
|
156
|
+
if (enclosing.length !== 1 || bridge.from === bridge.to) continue;
|
|
157
|
+
const previous = enclosing[0];
|
|
158
|
+
const oldText = byId.get(bridge.from)?.left?.text;
|
|
159
|
+
if (!oldText || !oldText.split('\n').slice(previous.start, bridge.callLine - 1).some(line => /^\s*if\s*\(/.test(line))) continue;
|
|
160
|
+
const next = (definitions.get('new:' + bridge.from) ?? []).filter(d => d.symbol === previous.symbol);
|
|
161
|
+
const target = (definitions.get('old:' + bridge.to) ?? []).find(d => d.start === bridge.declarationLine);
|
|
162
|
+
const oldRole = role(bridge.from, 'old', previous), newRole = next.length === 1 ? role(bridge.from, 'new', next[0]) : undefined;
|
|
163
|
+
const targetSource = source(bridge.to, 'old');
|
|
164
|
+
if (!oldRole || !newRole || !target || !targetSource) continue;
|
|
165
|
+
const key = JSON.stringify(['old-delegation', bridge.from, previous.symbol, bridge.to, bridge.symbol]);
|
|
166
|
+
if (seen.has(key)) continue;
|
|
167
|
+
seen.add(key);
|
|
168
|
+
if (result.contracts.length >= maxContracts) { unresolved.add('Migration contract limit reached; additional chains remain unresolved.'); continue; }
|
|
169
|
+
const baseline = { ...(byId.has(bridge.to) ? { fileId: bridge.to } : {}), sourceId: targetSource.id, side: targetSource.side,
|
|
170
|
+
path: byId.has(bridge.to) ? byId.get(bridge.to).oldPath ?? byId.get(bridge.to).path : targetSource.path,
|
|
171
|
+
sourcePath: targetSource.path, symbol: target.symbol, line: target.start, endLine: target.end, editIds: [] };
|
|
172
|
+
Object.assign(oldRole, { startLine: previous.start, endLine: previous.end });
|
|
173
|
+
Object.assign(newRole, { startLine: next[0].start, endLine: next[0].end });
|
|
174
|
+
baseline.startLine = target.start;
|
|
175
|
+
const callers = [caller(bridge, 'old')];
|
|
176
|
+
const navigationBindings = [...(previous.navigation ? [previous.navigation] : []), ...(next[0].navigation ? [next[0].navigation] : []),
|
|
177
|
+
...(target.navigation ? [target.navigation] : []), ...(bridge.navigationBindings ?? [])];
|
|
178
|
+
result.contracts.push({ id: 'mc' + (result.contracts.length + 1), kind: 'old-delegation', snapshotId: snapshot.id,
|
|
179
|
+
candidateFileIds: [bridge.from], old: [oldRole], new: [newRole], baseline: [baseline], callers, navigationBindings,
|
|
180
|
+
sourceIds: [...new Set([oldRole.sourceId, newRole.sourceId, baseline.sourceId, ...callers.map(c => c.sourceId)])],
|
|
181
|
+
status: 'navigation-only', causality: 'unverified',
|
|
182
|
+
unresolved: ['Same-file entry continuity is syntax-only; delegation removal, guard ordering and shared-state effects require independent evidence.'] });
|
|
183
|
+
}
|
|
184
|
+
// Deleted exports cannot be paired by spelling or filename. Retain a bounded
|
|
185
|
+
// candidate route only when explicit singleton spelling, full parameter signature,
|
|
186
|
+
// a shared assignment and forwarding syntax independently connect all three roles.
|
|
187
|
+
const routeDefs = { old: [], new: [] };
|
|
188
|
+
for (const side of ['old', 'new']) for (const file of snapshot.files.filter(f => f.eligibility === 'reviewable' && /\.(?:cpp|cc|cxx)$/.test(f.path))) {
|
|
189
|
+
const text = (side === 'old' ? file.left : file.right)?.text;
|
|
190
|
+
const parsed = cppExportRouteDefinitions(text);
|
|
191
|
+
routeDefs[side].push(...parsed.map(def => ({ ...def, fileId: file.id })));
|
|
192
|
+
if (side === 'old') {
|
|
193
|
+
const starts = new Set(parsed.filter(d => d.exported).map(d => d.start));
|
|
194
|
+
for (const [index, line] of (text ?? '').split('\n').entries()) if (/extern\s+"C"[^\n]*\(/.test(line) && !starts.has(index + 1))
|
|
195
|
+
unresolved.add('Old public export syntax unsupported at ' + (file.oldPath ?? file.path) + ':L' + (index + 1) + '; old entry navigation unavailable.');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (routeDefs.old.length > 2000 || routeDefs.new.length > 2000) {
|
|
199
|
+
unresolved.add('Deleted old public export route definition budget exceeded; candidate navigation unavailable.');
|
|
200
|
+
routeDefs.old = []; routeDefs.new = [];
|
|
201
|
+
}
|
|
202
|
+
for (const previous of routeDefs.old.filter(d => d.exported)) {
|
|
203
|
+
const oldRole = role(previous.fileId, 'old', previous);
|
|
204
|
+
if (!oldRole || routeDefs.new.some(d => d.symbol === previous.symbol && d.params === previous.params && d.exported)) continue;
|
|
205
|
+
const missing = 'Deleted old public entry ' + oldRole.path + ':L' + previous.start + ' (' + previous.symbol + '): ';
|
|
206
|
+
const calls = previous.lines.flatMap((line, index) => {
|
|
207
|
+
const m = /^\s*([A-Za-z_]\w*)::GetInstance\(\)->([A-Za-z_]\w*)\(([^;{}]*)\);\s*$/.exec(line);
|
|
208
|
+
return m ? [{ symbol: m[1] + '::' + m[2], args: m[3].split(',').map(a => a.trim()), line: previous.bodyStartLine + index }] : [];
|
|
209
|
+
});
|
|
210
|
+
if (calls.length !== 1) { unresolved.add(missing + 'singleton delegation unsupported or ambiguous.'); continue; }
|
|
211
|
+
const call = calls[0];
|
|
212
|
+
const targets = routeDefs.old.filter(d => d.symbol === call.symbol && d.params === previous.params && d.names.length === call.args.length);
|
|
213
|
+
if (targets.length !== 1 || !call.args.every(a => /^[A-Za-z_]\w*(?:\.c_str\(\))?$/.test(a))) {
|
|
214
|
+
unresolved.add(missing + 'old implementation identity/signature unresolved.'); continue;
|
|
215
|
+
}
|
|
216
|
+
const target = targets[0];
|
|
217
|
+
const assignments = target.lines.map(line => /^\s*([A-Za-z_]\w*)\s*=\s*([A-Za-z_]\w*)\s*;\s*$/.exec(line)).filter(Boolean)
|
|
218
|
+
.filter(m => target.names.includes(m[2]));
|
|
219
|
+
const next = routeDefs.new.filter(d => !d.exported && d.params === previous.params && assignments.some(m => d.lines.some(line => line.trim() === m[1] + ' = ' + m[2] + ';')) &&
|
|
220
|
+
d.lines.some(line => line.trim() === 'return ' + d.symbol.split('::').at(-1) + '(' + d.names.slice(0, -1).join(', ') + ');'));
|
|
221
|
+
if (next.length !== 1) { unresolved.add(missing + 'new entry candidate unsupported or ambiguous; no same-name fallback.'); continue; }
|
|
222
|
+
const current = next[0], newRole = role(current.fileId, 'new', current), src = source(target.fileId, 'old');
|
|
223
|
+
if (!newRole || !src) { unresolved.add(missing + 'changed candidate or historical implementation source unavailable.'); continue; }
|
|
224
|
+
if (result.contracts.length >= maxContracts) { unresolved.add(missing + 'contract budget exceeded.'); continue; }
|
|
225
|
+
Object.assign(oldRole, { startLine: previous.start, endLine: previous.end });
|
|
226
|
+
Object.assign(newRole, { startLine: current.start, endLine: current.end });
|
|
227
|
+
const baseline = { fileId: target.fileId, sourceId: src.id, side: 'old', path: byId.get(target.fileId).oldPath ?? byId.get(target.fileId).path,
|
|
228
|
+
sourcePath: src.path, symbol: target.symbol, line: target.start, startLine: target.start, endLine: target.end, editIds: [] };
|
|
229
|
+
result.contracts.push({ id: 'mc' + (result.contracts.length + 1), kind: 'deleted-export-route', snapshotId: snapshot.id,
|
|
230
|
+
old: [oldRole], new: [newRole], baseline: [baseline], candidateFileIds: [...new Set([previous.fileId, current.fileId])],
|
|
231
|
+
callers: [{ ...oldRole, line: call.line, callLine: call.line, editIds: [], targetFileId: target.fileId, targetSymbol: target.symbol }],
|
|
232
|
+
navigationBindings: [], sourceIds: [...new Set([oldRole.sourceId, newRole.sourceId, baseline.sourceId])], status: 'navigation-only', causality: 'unverified',
|
|
233
|
+
unresolved: ['Deleted export route is lexical candidate navigation only; singleton resolution, macros, transformed arguments, shared-state identity and cross-file causality are unsupported. Non-uncertain classification is not permitted.'] });
|
|
234
|
+
}
|
|
151
235
|
for (const contract of result.contracts) contract.sourceIds = [...new Set([...contract.sourceIds, ...contract.navigationBindings.map(row => row.sourceId)])];
|
|
152
|
-
const edges = result.contracts.flatMap(c => [{ from: c.old[0].fileId, to: c.new[0].fileId, reason: 'migration-contract:' + c.id },
|
|
153
|
-
...c.callers.filter(row => row.fileId && row.fileId !== row.targetFileId).map(row => ({ from: row.fileId, to: row.targetFileId, reason: 'migration-caller:' + c.id }))]);
|
|
236
|
+
const edges = result.contracts.flatMap(c => [...(c.old[0].fileId !== c.new[0].fileId ? [{ from: c.old[0].fileId, to: c.new[0].fileId, reason: 'migration-contract:' + c.id }] : []),
|
|
237
|
+
...c.callers.filter(row => row.fileId && byId.has(row.targetFileId) && row.fileId !== row.targetFileId).map(row => ({ from: row.fileId, to: row.targetFileId, reason: 'migration-caller:' + c.id }))]);
|
|
154
238
|
const involved = new Set(edges.flatMap(e => [e.from, e.to]));
|
|
155
239
|
const grouped = buildReviewGroups(snapshot.files.filter(f => involved.has(f.id)), edges, { maxFiles: maxGroupFiles });
|
|
156
240
|
result.groups = grouped.groups; result.links = grouped.links;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { isDeepStrictEqual, types } from 'node:util';
|
|
1
2
|
import { changeMap } from './change-map.mjs';
|
|
2
3
|
import { contextSources } from './context-identity.mjs';
|
|
3
4
|
import { hash } from './content.mjs';
|
|
5
|
+
import { buildMigrationContracts } from './migration-contracts.mjs';
|
|
4
6
|
|
|
5
7
|
const contexts = new WeakSet();
|
|
6
8
|
const positive = value => Number.isSafeInteger(value) && value > 0;
|
|
@@ -8,9 +10,42 @@ const overlap = (ref, range) => range?.count > 0 && ref.start < range.start + ra
|
|
|
8
10
|
const fail = () => { throw new Error('Invalid approved migration regression contract'); };
|
|
9
11
|
const freeze = value => { if (value && typeof value === 'object') { for (const item of Object.values(value)) freeze(item); Object.freeze(value); } return value; };
|
|
10
12
|
|
|
13
|
+
// Copy only inert own data. structuredClone/JSON.stringify would invoke getters
|
|
14
|
+
// before validation; custom prototypes, proxies and toJSON hooks are not input data.
|
|
15
|
+
function inertData(value, state = { remaining: 100000 }, depth = 0) {
|
|
16
|
+
if (--state.remaining < 0 || depth > 32) fail();
|
|
17
|
+
if (value === null || value === undefined || ['string', 'boolean', 'number'].includes(typeof value)) return value;
|
|
18
|
+
if (typeof value !== 'object' || types.isProxy(value)) fail();
|
|
19
|
+
const array = Array.isArray(value), prototype = Object.getPrototypeOf(value);
|
|
20
|
+
if (prototype !== (array ? Array.prototype : Object.prototype) && !(prototype === null && !array)) fail();
|
|
21
|
+
const copy = array ? [] : {};
|
|
22
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
23
|
+
if (typeof key !== 'string' || ['__proto__', 'constructor', 'toJSON'].includes(key)) fail();
|
|
24
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
25
|
+
if (!Object.hasOwn(descriptor, 'value')) fail();
|
|
26
|
+
if (array && key === 'length') continue;
|
|
27
|
+
if (!descriptor.enumerable) fail();
|
|
28
|
+
Object.defineProperty(copy, key, { value: inertData(descriptor.value, state, depth + 1), enumerable: true, writable: true, configurable: true });
|
|
29
|
+
}
|
|
30
|
+
if (array && copy.length !== value.length) fail();
|
|
31
|
+
return copy;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The original manually approved protocol has no host navigation coordinates.
|
|
35
|
+
// Do not infer host origin solely from kind: that tag is removable input data.
|
|
36
|
+
function legacyContract(contract) {
|
|
37
|
+
const only = (value, keys) => value && Object.keys(value).every(key => keys.includes(key));
|
|
38
|
+
const roleKeys = ['sourceId', 'fileId', 'path', 'side', 'line', 'symbol', 'editIds'];
|
|
39
|
+
return only(contract, ['id', 'snapshotId', 'status', 'causality', 'candidateFileIds', 'old', 'new', 'callers']) &&
|
|
40
|
+
[...contract.old, ...contract.new].every(role => only(role, roleKeys)) &&
|
|
41
|
+
contract.callers.every(role => only(role, [...roleKeys, 'importLine', 'targetFileId', 'targetSymbol']));
|
|
42
|
+
}
|
|
43
|
+
|
|
11
44
|
// Compile host navigation against the exact approved snapshot. No model-provided
|
|
12
45
|
// contract object or copied input metadata can replace this invocation capability.
|
|
13
46
|
export function prepareMigrationRegression(snapshot, scope, contracts, candidates) {
|
|
47
|
+
contracts = inertData(contracts);
|
|
48
|
+
candidates = inertData(candidates);
|
|
14
49
|
if (!snapshot || typeof snapshot.id !== 'string' || snapshot.projectScope || snapshot.files?.some(file => file.reviewMode === 'project') || !Array.isArray(snapshot.files) || !Array.isArray(contracts) || contracts.length > 100 || !Array.isArray(candidates)) fail();
|
|
15
50
|
const catalog = scope.catalog();
|
|
16
51
|
const role = (value, expectedSide) => {
|
|
@@ -42,27 +77,46 @@ export function prepareMigrationRegression(snapshot, scope, contracts, candidate
|
|
|
42
77
|
}
|
|
43
78
|
const line = value.line ?? value.callLine;
|
|
44
79
|
if (typeof body !== 'string' || !positive(line) || line > source.lineCount || value.importLine !== undefined && (!positive(value.importLine) || value.importLine > source.lineCount) || edits.length && !edits.some(edit => line >= edit.range.start && line < edit.range.start + edit.range.count)) fail();
|
|
45
|
-
|
|
80
|
+
if (value.startLine !== undefined && (!positive(value.startLine) || !positive(value.endLine) || value.startLine > line || value.endLine < line || value.endLine > source.lineCount)) fail();
|
|
81
|
+
return { sourceId: source.id, side: source.side, path: value.path, sourcePath: source.path, ...(source.fileId ? { fileId: source.fileId } : {}), line, ...(value.startLine !== undefined ? { startLine: value.startLine, endLine: value.endLine } : {}), hash: hash(body), edits };
|
|
46
82
|
};
|
|
47
83
|
const ids = new Set(), compiled = [];
|
|
84
|
+
const approvedContracts = contracts.length ? buildMigrationContracts(snapshot, { maxContracts: 100 }).contracts : [];
|
|
48
85
|
for (const contract of contracts) {
|
|
49
86
|
if (!contract || typeof contract.id !== 'string' || !contract.id || contract.id.length > 200 || ids.has(contract.id) || contract.snapshotId !== snapshot.id || contract.status !== 'navigation-only' || contract.causality !== 'unverified' || !Array.isArray(contract.callers) || contract.callers.length > 32) fail();
|
|
50
87
|
ids.add(contract.id);
|
|
51
88
|
if (!Array.isArray(contract.old) || contract.old.length !== 1 || !Array.isArray(contract.new) || contract.new.length !== 1 || !Array.isArray(contract.candidateFileIds)) fail();
|
|
52
89
|
const oldValue = contract.old[0], newValue = contract.new[0];
|
|
90
|
+
// Rebuild all host shapes (ordinary navigation included), comparing the complete
|
|
91
|
+
// identity, not just roles. Renaming an ID or deleting every origin marker must
|
|
92
|
+
// not turn an independently known host route into the legacy manual protocol.
|
|
93
|
+
const approved = approvedContracts.find(item => isDeepStrictEqual(item, contract));
|
|
94
|
+
const knownRoute = approvedContracts.some(item => item.id === contract.id ||
|
|
95
|
+
item.old[0].sourceId === oldValue?.sourceId && item.new[0].sourceId === newValue?.sourceId);
|
|
96
|
+
if (!approved && (knownRoute || !legacyContract(contract))) fail();
|
|
53
97
|
const old = role(oldValue, 'old'), current = role(newValue, 'new');
|
|
54
|
-
|
|
98
|
+
const exportRoute = contract.kind === 'deleted-export-route';
|
|
99
|
+
const delegation = contract.kind === 'old-delegation' || exportRoute;
|
|
100
|
+
if (contract.kind !== undefined && !delegation) fail();
|
|
101
|
+
let baseline = [];
|
|
102
|
+
if (delegation) {
|
|
103
|
+
// Only a complete independently rebuilt host contract can carry these roles.
|
|
104
|
+
if (!approved || (!exportRoute && old.fileId !== current.fileId) || !Array.isArray(contract.baseline) || contract.baseline.length !== 1) fail();
|
|
105
|
+
baseline = contract.baseline.map(value => role(value));
|
|
106
|
+
if (!['old', 'context-old'].includes(baseline[0].side)) fail();
|
|
107
|
+
}
|
|
108
|
+
if (!old.fileId || !current.fileId || !delegation && old.fileId === current.fileId || old.sourceId === current.sourceId || !old.edits.length || !current.edits.length) fail();
|
|
55
109
|
const callers = contract.callers.map(caller => {
|
|
56
110
|
const result = role(caller);
|
|
57
|
-
if (!['old', 'new', 'context-old', 'context'].includes(result.side) || caller.targetFileId !== oldValue.fileId && caller.targetFileId !== newValue.fileId) fail();
|
|
111
|
+
if (!['old', 'new', 'context-old', 'context'].includes(result.side) || !delegation && caller.targetFileId !== oldValue.fileId && caller.targetFileId !== newValue.fileId) fail();
|
|
58
112
|
return result;
|
|
59
113
|
});
|
|
60
114
|
// The navigation chain has an explicit old-entry caller; a new-role source
|
|
61
115
|
// alone cannot manufacture a migration relation between arbitrary files.
|
|
62
|
-
if (!contract.callers.some(caller => caller.targetFileId === oldValue.fileId && ['old', 'context-old'].includes(caller.side))) fail();
|
|
116
|
+
if (!contract.callers.some(caller => (delegation ? caller.sourceId === old.sourceId : caller.targetFileId === oldValue.fileId) && ['old', 'context-old'].includes(caller.side))) fail();
|
|
63
117
|
const fileIds = [...new Set([old.fileId, current.fileId].filter(Boolean))];
|
|
64
118
|
if (new Set(contract.candidateFileIds).size !== contract.candidateFileIds.length || contract.candidateFileIds.length !== fileIds.length || contract.candidateFileIds.some(id => !fileIds.includes(id))) fail();
|
|
65
|
-
compiled.push({ id: contract.id, snapshotId: snapshot.id, old, new: current, callers, candidateFileIds: fileIds });
|
|
119
|
+
compiled.push({ id: contract.id, snapshotId: snapshot.id, ...(delegation ? { kind: contract.kind, baseline } : {}), old, new: current, callers, candidateFileIds: fileIds });
|
|
66
120
|
}
|
|
67
121
|
const context = freeze({ snapshotId: snapshot.id, contracts: compiled, candidates: structuredClone(candidates) });
|
|
68
122
|
contexts.add(context);
|
|
@@ -81,6 +135,7 @@ export function validateMigrationRegression(value, evidence, fileId, context, ca
|
|
|
81
135
|
if (!contract || !candidate || candidate.attribution?.status !== 'references-validated' || !Array.isArray(candidate.attribution.editIds)) fail();
|
|
82
136
|
const ownRoles = [contract.old, contract.new].filter(role => role.fileId === fileId);
|
|
83
137
|
if (!ownRoles.some(role => role.edits.some(edit => candidate.attribution.editIds.includes(edit.editId) && candidate.anchor?.kind === 'line' && candidate.anchor.side === (role.side === 'old' ? 'old' : 'new') && overlap({ start: candidate.anchor.start, count: candidate.anchor.end - candidate.anchor.start + 1 }, edit.range)))) fail();
|
|
138
|
+
if (contract.kind === 'deleted-export-route' && value.classification !== 'uncertain') throw new Error('Deleted export candidate navigation permits uncertain classification only');
|
|
84
139
|
let actualEdit = false;
|
|
85
140
|
for (const [field, edge, sides] of [['oldEvidence', contract.old, ['old', 'context-old']], ['newEvidence', contract.new, ['new', 'context']]]) {
|
|
86
141
|
const indices = value[field];
|
|
@@ -89,8 +144,8 @@ export function validateMigrationRegression(value, evidence, fileId, context, ca
|
|
|
89
144
|
for (const index of indices) {
|
|
90
145
|
if (!Number.isSafeInteger(index) || index < 0 || index >= evidence.length) fail();
|
|
91
146
|
const ref = evidence[index];
|
|
92
|
-
const binding = [edge, ...contract.callers.filter(role => sides.includes(role.side))].find(role => role.sourceId === ref.sourceId && sides.includes(role.side));
|
|
93
|
-
if (!binding || ref.status !== 'references-validated' || ref.snapshotId !== context.snapshotId || ref.hash !== binding.hash || !positive(ref.start) || !positive(ref.count) || binding !== edge && !(ref.start <= binding.line && binding.line < ref.start + ref.count)) fail();
|
|
147
|
+
const binding = [edge, ...(contract.baseline ?? []), ...contract.callers.filter(role => sides.includes(role.side))].find(role => role.sourceId === ref.sourceId && sides.includes(role.side));
|
|
148
|
+
if (!binding || ref.status !== 'references-validated' || ref.snapshotId !== context.snapshotId || ref.hash !== binding.hash || !positive(ref.start) || !positive(ref.count) || binding !== edge && !(binding.startLine !== undefined ? ref.start <= binding.endLine && binding.startLine < ref.start + ref.count : ref.start <= binding.line && binding.line < ref.start + ref.count)) fail();
|
|
94
149
|
if (binding === edge && ref.start <= edge.line && edge.line < ref.start + ref.count) {
|
|
95
150
|
entry = true;
|
|
96
151
|
if (edge.edits.some(edit => overlap(ref, edit.range))) actualEdit = true;
|
|
@@ -98,6 +153,15 @@ export function validateMigrationRegression(value, evidence, fileId, context, ca
|
|
|
98
153
|
}
|
|
99
154
|
if (value.classification !== 'uncertain' && !entry) throw new Error('Migration comparison requires both explicit entry roles');
|
|
100
155
|
}
|
|
156
|
+
if (value.classification !== 'uncertain' && contract.kind === 'old-delegation') {
|
|
157
|
+
for (const required of [contract.old, contract.new, ...contract.baseline]) {
|
|
158
|
+
const indices = ['old', 'context-old'].includes(required.side) ? value.oldEvidence : value.newEvidence;
|
|
159
|
+
const ranges = indices.map(index => evidence[index]).filter(ref => ref.sourceId === required.sourceId).sort((a, b) => a.start - b.start);
|
|
160
|
+
let through = required.startLine;
|
|
161
|
+
for (const ref of ranges) { if (ref.start > through) break; through = Math.max(through, ref.start + ref.count); }
|
|
162
|
+
if (through <= required.endLine) throw new Error('Migration delegation comparison requires complete entry and baseline evidence');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
101
165
|
if (value.classification !== 'uncertain' && !actualEdit) throw new Error('Migration comparison requires actual changed-role evidence');
|
|
102
166
|
return { ...value, oldEvidence: [...value.oldEvidence], newEvidence: [...value.newEvidence], migrationContractId: contract.id, migrationSnapshotId: context.snapshotId, status: 'comparison-references-validated', causality: 'unverified' };
|
|
103
167
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Preview validity is diagnostic only: it never restores approval or bypasses recapture.
|
|
2
|
+
export function previewValidity(preview, current, now) {
|
|
3
|
+
const fail = (code, message) => { const error = new Error(message + '; preview again and obtain fresh confirmation'); error.code = code; throw error; };
|
|
4
|
+
if (!preview) fail('PREVIEW_MISSING', 'Preview missing or already consumed');
|
|
5
|
+
if (preview.expires <= now) fail('PREVIEW_EXPIRED', 'Preview expired');
|
|
6
|
+
if (preview.agent !== current.agent) fail('PREVIEW_OWNER_CHANGED', 'Preview owner changed');
|
|
7
|
+
if (preview.cwd !== current.cwd) fail('PREVIEW_DIRECTORY_CHANGED', 'Preview directory changed');
|
|
8
|
+
if (JSON.stringify(preview.route) !== JSON.stringify(current.route)) fail('PREVIEW_MODEL_CHANGED', 'Preview model changed');
|
|
9
|
+
}
|
package/src/primary-input.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { sourceSegments } from './source-segments.mjs';
|
|
2
2
|
import { primaryContextFocus } from './primary-context.mjs';
|
|
3
|
-
import { changeRegion } from './change-region.mjs';
|
|
4
3
|
import { CONTEXT_SIDE_NOTICE } from './context-identity.mjs';
|
|
5
4
|
import { approvedCallerBindings } from './caller-binding-input.mjs';
|
|
6
5
|
import { REQUIREMENT_NOTICE } from './business-requirement.mjs';
|
|
@@ -12,7 +11,7 @@ import { inferContextRelations } from './context-relations.mjs';
|
|
|
12
11
|
import { prepareWindowSynthesis } from './window-synthesis.mjs';
|
|
13
12
|
import { planWindowBatches } from './window-batches.mjs';
|
|
14
13
|
import { changeWindows } from './change-windows.mjs';
|
|
15
|
-
import {
|
|
14
|
+
import { reviewChangeMap } from './review-change-map.mjs';
|
|
16
15
|
import { initialInputBudget } from './input-budget.mjs';
|
|
17
16
|
import { changeScopeNotice } from './git-change-scope.mjs';
|
|
18
17
|
import { chooseRiskStrategy, riskPlanningMode as validateRiskPlanningMode } from './risk-strategy.mjs';
|
|
@@ -25,18 +24,7 @@ const optional = h => h.confidence === 'namespace-member-syntax-only' || h.confi
|
|
|
25
24
|
export function preparePrimaryInput(input, file, { instructions, scope, grouping, maxInputBytes, riskPlanningMode, enableRiskPlanning, skipSmallRiskPlans, round, focusPrimaryContext = false }) {
|
|
26
25
|
validateRiskPlanningMode(riskPlanningMode);
|
|
27
26
|
if (typeof focusPrimaryContext !== 'boolean') throw new Error('focusPrimaryContext must be boolean');
|
|
28
|
-
|
|
29
|
-
if (changes.status === 'limited') {
|
|
30
|
-
const region = changeRegion(file.left.text, file.right.text);
|
|
31
|
-
if (region.status === 'changed') {
|
|
32
|
-
changes = {
|
|
33
|
-
status: 'changed', precision: 'envelope',
|
|
34
|
-
reason: 'Exact diff unavailable; broad region includes potentially unchanged lines, not exact edits or causal proof.',
|
|
35
|
-
edits: [{ id: 'envelope1', old: region.old, new: region.new }],
|
|
36
|
-
hunks: [{ id: 'envelope-h1', old: region.old, new: region.new, editIds: ['envelope1'] }],
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
27
|
+
const changes = reviewChangeMap(file.left.text, file.right.text);
|
|
40
28
|
|
|
41
29
|
let base = {
|
|
42
30
|
...(input.projectScope ? { projectScope: input.projectScope, reviewNotice: 'Full current-file review; empty left is a sentinel, not a historical baseline. changes are transport coordinates only, do not infer introduced regression.' } : {}),
|