任务:客户端生成工作簿,Hub 回读核验并记摘要
依据:docs/hub-r4-device-run.md(上真实台架的设计,第 1–5 节和第 8 节的分工),决定 26、27(docs/hub-r4-progress.md),理论文档第 6、7 节;模型用 node tools/show-element.ts D02 D03 D04 D07 B05 B06 B07 V03 W05 P05 读。
- 分支和工作树:
hub-case-workbook,目录circle-hub-hub-case-workbook(依赖已装好) - 可以改的路径:packs/workflow/case-compile/(新的 functions/generate.ts、functions/g-syntax.ts、functions/workbook.ts、scripts/,以及 workflow.json、requirements.json、checks.json、SKILL.md、README.md、samples/)。核心文件不碰:xlsx@1 解析器和 upload.ts 的 xlsx 一行在
xlsx-parser.md那件里做。 - 规则:同前(不推送、不建 PR、不发布、不部署;不碰 7420、钥匙串、环境配置文件和任何真实系统;测试只用合成数据,地址只用 192.0.2.0/24 和 2001:db8::/32;
git commit -s)。所有者的规矩:未知只能是过渡状态,写明缺的事实和下一步动作,流程自动走这一步;Hub 只核验,不生成内容、不调用模型;枚举只来自 contracts/generated(运行时用按包生成的快照)。 - 依赖:hub-case-runtime 和 xlsx-parser 合并之后
要做的
- 核心 xlsx@1 解析器已拆成
xlsx-parser.md先做,这里只用它的模型。“只认生成器写的子集”(设计第 3 节第 1 条的后半)在第 6 条的核验检查里判:成员恰为生成器写的那几个、一张可见的表、没有隐藏的表和行、只用内联字符串、没有公式、没有以=开头的值。 - G 写法(functions/g-syntax.ts):按 README 里的格式说明实现运行器解析规则的 encode 和 decode(逗号、引号、反斜杠、关键字、数字、换行)。encode 之后 decode 必须原样回来,回不来的值生成器拒绝;读取器用同一套规则解析。
- 生成器(functions/generate.ts,零依赖,只用 Node 内置;包的检查和 Circle 脚本导入同一个模块):输入为槽位、条目字段(autoid、标题、步骤原文)、合约身份和配置;输出 xlsx 字节。zip 只用 STORE、固定时间戳、固定成员次序、自算 CRC32;只用内联字符串,C 列是数字,A 列是文本;行序为身份行、表头、说明行(C=0,E–I 空,D 写运行号和构建,不写人名)、用例各步(autoid 只在第一行且该行 E 不空,每条期望一行字面断言紧跟它的 after 步骤)、空行、收尾行;范围名是工作簿级、指向表头行。OOXML 的写法参照 circle-packs 里 10-13 在台架上跑通过的 xlsx.mjs(只读参照),差异逐条写进 README。
- Circle 脚本(scripts/):normalize-mindmap.ts 把本地导出文件规整成 kityminder 形状,打印两边的摘要;generate-workbook.ts 读本地保存的已满足提交、条目字段、合约身份和配置,写出 xlsx,打印 sha256 和生成器版本。只用 Node 内置,不联网。SKILL.md 写明这些输入从哪次工具调用的回答里取。
- 输入槽 workbook(角色待判定产物,接受 xlsx 媒体类型,选填)和要求项 workbook_verified(事实、阻断);没绑定时记未知(缺输入)。
- 核验检查(functions/workbook.ts,独立读取器,不调用生成器去解析):设计第 3 节第 1 到 8 条;再用生成器从本次提交的槽位重算,字节相等才支持。支持结论的 data 写 {sha256, autoids, bed, build, generator, plan_run}(任务 3 的接口),其中 bed 是 {id, manifest_sha256}:id 是台架清单里的台架名,manifest_sha256 是本次运行绑定的台架清单的字节摘要;plan_run 是工作簿据以生成的已满足计划运行,说明行的 D 和参数 plan_run 都要等于它(它和本运行的 predecessor 相等由派发核);不带工作簿内容;生成器版本与本运行的包版本不符时写明。
- Hub 侧生成计划的要求项(deterministic_plan)已在 hub-case-runtime 里随 2.0 移除(决定 26:Hub 不生成内容);这里在 README 和 SKILL.md 里写两次运行的步骤。
必须有的测试
- 子集检查:多一个成员、两张表、隐藏的表、用了共享字符串,各一份反例。
- G 写法:每条规则一对正反例;固定种子的随机值往返。
- 生成器:同一输入两次字节相同;改一个槽位字节就不同;zip 自检(CRC、偏移、成员次序);脚本和检查函数导入的是同一个模块。
- 读取器对生成器:生成,经核心解析,再经包读取,逐行等于槽位。
- 核验反例各一份:缺身份行、摘要不符、两个表头、范围名是表级或指错行、说明行带了 E、收尾行不是最后一行、autoid 行 E 空、autoid 位数不对、断言跟在等待之后、上传字节与重算差一个字节、出现公式或以 = 开头的值、有隐藏行、认证请求格非空、两个用例。
- 生产测试(真引擎、假外部):计划运行满足后工作簿一项是未知(缺输入);带工作簿的第二次运行全部满足,结论带 S。
- npx tsc –noEmit;全仓词表扫描零命中,不加例外(
packs/workflow/case-review/packs.test.ts里的 handWrittenAxes,core-fixes 合并后覆盖 hub/ 和全部包)。
完成后在终端里回报:提交号、测试数量、没做完或拿不准的地方。真实台架上的任何动作都由集成者做,并且要等所有者答复设计第 6 节的问题。
附:运行器怎么读行和 G 列(集成者 2026-10-23 照运行器源码只读整理)
运行器源码只作行为参照,不抄代码。表头文字、对象名和方法名都是部署数据,下面用“运行环境配置里的某某”代称,例子全是合成的。G 写法(第 2 条)的 encode 和 decode 照这一节实现;回不来的值由生成器拒绝,不猜。
读单元格
- 工作簿按缓存值读,公式不求值。
- 每格用 Python 的
str()变成文字:空格子是文字None,整数是十进制,小数带.0,布尔是True/False。生成器只写内联字符串,所以 G 就是写进去的那段文字。 - G 空着不等于“没有参数”:运行器会传一个位置参数
None。没有参数的步骤写不出来,生成器拒绝。
选行
- 表头行之前的行不执行。
- 表头之后:
- C 是
1的行是每个用例开头先跑的初始化行; - A 不空的行开始一个新用例;这一行 E 空着,整个用例都跳过。
- C 是
- 第一个 C 是
0的行,它的 D 是说明。 - 表的最后一行当作结束标记。
- E 写的对象在运行器里找不到时,这一行被悄悄跳过。所以 Hub 的检查要保证每个 E 都是运行环境配置里登记的座位。
断言行(字面断言角色)
- G 不切分、不去引号:整格文字原样作期望值,逗号、引号、换行都保留,和最近一次结果比。
- “最近一次结果”是最近一个 H 空着的非断言行的返回值。所以生成器写的所有行 H、I 都空着,断言行紧跟它 after 指向的那一步;同一步后面的几条断言比的是同一个结果。
- 期望值不能是空串:空格子会变成
None。
普通行的 G
- 带换行:文字里有换行(LF)时,整段原样作一个位置参数,不去空白、不去引号、不认关键字。
- 例外:运行环境配置标为“折叠换行”的方法,先去两端空白,再删掉全部 LF 和 CR(各行直接接上,没有分隔),然后照第 2 条切分。
- 切分:在引号外的逗号处切开。
- 引号段从
"或'开始,到下一个没有被反斜杠转义的同种引号结束;段内反斜杠只让下一个字符不算结束,反斜杠本身留在值里。 - 没有配对的引号当普通字符。
- 空的片段和只有空白的片段丢掉,所以连续逗号、结尾逗号不产生空参数。
- 引号段从
- 去引号:每个片段去两端空白(Python 的空白,含全角空格)。整段首尾是同一种引号时,去掉这两个引号,里面再去一次两端空白。段内的反斜杠不还原。
- 关键字:去引号以后,片段里有
=就在第一个=处分开,两边去空白。- 键里有半角空格:整段当位置参数;
- 否则是关键字参数,加了引号也一样;
- 值全是数字字符(
isdigit)时转成整数,否则是文字; - 同名的键后者覆盖前者。
- 位置参数一律是文字,不转数字,按次序传;关键字参数跟在后面。
- I 列不空时,第一个位置参数当格式串,用那个变量的值替换
{}。生成器不写 I。 - 等待角色:第一个位置参数用
int()转换。
由此定的 encode 规则
能写成的才写,写不成的拒绝并说明是哪一条:
- 位置参数:
- 首尾有空白、为空或含 CR 的值写不成;
- 含逗号或以引号开头的值用整段引号包起来:值里有
"就用',两种都有就写不成; - 以反斜杠结尾又需要加引号的值写不成;
- 第一个
=前面没有半角空格的值会被当成关键字,写不成; - 带换行的值只能是这一行唯一的参数,而且方法不在“折叠换行”名单里。
- 关键字参数:
- 键只能是合约签名里允许按关键字传的参数名,重复的写不成;
- 整数值写成没有前导零的 ASCII 数字;
- 全是数字字符的文字值(例如
007)会被转成整数,写不成,非 ASCII 数字也写不成。
- 等待的时长只写 ASCII 数字。
- 每个值 encode 以后按上面的读法 decode 一遍,必须原样回来。