Repository navigation
v0.9.1
🆕 新增
-
M.O. 从「看得见」走到「跑得出账」:运行账落在项目里,执行窗口由人显式打开(beta) — v0.9.0 的 M.O. 只有一张图和候选,没有任何一条「跑过」的证据——读侧早就能把执行情况算成四态,可账本一直没人写,
runs恒为空数组、所有节点恒为candidate,跳过与绕开的退休提议永远发不出来。细节
这一版补上写侧:
src/pipeline-runs.js把一次 run 落到<项目>/.mneme/pipelines/<id>.runs.jsonl,与手写的.json分开,一次 run 不会冲掉人的 diff。开窗必须由人的显式动作划:工作流的trigger是人话,拿它去匹配用户消息只能猜,而猜错的代价不是少记一笔,是把一条从没跑过的流程记成跑过、还把节点染成可信——所以窗口边界是/mo-run start <id>与/mo-run stop,窗口之外发生的一切一个字都不记,漏记可接受、编造不可。四个取舍都有代价意识:窗口按会话开、一个会话同时只允许一个(再开会被拒而不是顶掉,顶掉会让前一次执行悄无声息地消失);事件先缓冲到 stop 才配对(工具的结果可能比调用晚很久,边到边配会把「还没等到结果」误判成「没有这个调用」);只在显式 stop 结账、不按回合关(一条流程常跨多个回合,按回合关会切成几条半截账);热路径永不抛(挂的是宿主会话事件通道,可选插件的可选能力把 agent 一步带崩,比丢一次抢救严重得多)。「账本坏了」与「从没跑过」严格分开:逐行收,好行照收、坏行进problems(带行号、标出是否只是一截尾巴),状态降partial/broken,并撤掉坏账本的 500——它会让客户端当成离线,还吞掉读得出来的那张图。账本目录自带.gitignore也是为这件事:.runs.jsonl一旦被提交,两条并行车道先后追加同一份账本就会产生冲突标记,而那些标记不是合法 JSON,一份流水污染会升级成工作流状态永久消失。独立旗标actionRunEnabled默认关且进LIGHT_MODE_OFF——看图与写账是两件事,混成一个键会让只想看图的人不知不觉获得写入能力。 -
会话事件翻成观察:只有跑在命令执行器里的调用才算工作流的一步 — 接线前的实测就把原方案否掉了——400 个会话里工具名是
pwsh(1399)/read(1709)/grep(1040)/glob(183),而工作流里人写的tool是npm test、git push <tag>这类命令行,两者不重叠…细节
直接拿 name 去比一条都对不上;唯一能对上的是「命令跑在哪个工具里」,
pwsh的arguments.command才是那条命令行。三条纪律都是「不许编造」的具体形态:非命令执行器一律不产观察(read/write/glob/grep是 agent 的日常动作,硬把文件路径拼成「命令行」去匹配只会制造噪声,而噪声的代价是把某一步凭空染色);只有拿到结果的调用才产观察(配对靠callId,「调了但没结果」既不是成功也不是失败,拿它当一次成功使用是编造,如实进dangling);成败是三态(拿不到就留null,把「不知道」折叠成「失败」会给这一步打上不该有的refuted标记)。 -
归属规则只出候选,绝不下结论 —
tool字段是人手写的自由文本——单命令、多命令并列、带占位符,还有 4 步是空的,拿这种文本做精确匹配注定命中率不高,而误匹配的代价远大于漏匹配(把「推 tag」记成「推分支」,deriveState就会把错误的那一步染成可信)。细节
所以这一层只产「候选 + 理由 + 三种匹配强度(
exact/weak/ 认不出来)」,认不出来是合法答案。弱匹配用词前缀而不是「第一个词相同」:真实工作流里git出现 5 次,按首词匹配会让一次git push报出六条候选,噪声盖过信号;压到 2–3 条之后人才愿意看。报告分三桶(有候选 / 认不出来 / 拼不出命令行),并把结构性无法归属的步骤单列出来——那是「为什么这一步永远没账」的答案。ponytail 复核随即砍掉四块替不存在的写者预留的代码(整块的attributionReport、at/actor两个没人读的字段、SEPARATOR_RE的三个从未出现的分支、PLACEHOLDER_RE的两种从未出现的形态),模块 189 → 165 行。 -
P1/P2/P3 三级落地:从「回执列出干了什么」到「人确认才写账」再到「观察旁挂与脱节检出」 — 三条实测把原方案一步步逼掉——单条命令匹配手写步骤表,唯一归属仅 14.2%(非发版会话 4.4%,区分力只有 3 倍);整段命令当分组键每会话中位 539 组、最多 7373 组,人扫不过来;换动词当键,密度最高的是 PowerShell 管道语法…
细节
说明**「命令 ≈ 动作」这个预设本身是错的**;只留业务命令(git/gh/npm/node/docker)后业务段占 6.5%,那才是人能扫的量级。于是 P1 只报「本次观察到哪些业务动作、各几次」由人对账(
stop在没归属到步骤时不再只说「没有落盘」,而是同时给出观察清单——清单才是这一版真正的交付物);P2 把写账从stop挪到/mo-run confirm——「没确认」≠「没发生」(人没提到的步骤什么都不写,若把「忘了勾」记成「被跳过」,RETIRE_AFTER_SKIPS就会据此发出错误的删步提议),unsure与notDone必须分开,「全部没做」也是一条合法且完整的记录;P3 把两样都有的东西对一遍——人说这步做了 + 声明的 tool 本次一次都没出现(实测背书:gh pr merge --rebase在 21723 次调用里出现 0 次),系统只把矛盾摆出来让人判,不改步骤表。观察走旁挂<memoryDir>/observations/<项目键>/<id>.jsonl而不是给账本加字段——账本是 v0.9.0 发出去的数据契约,为 P3 加字段是动已发布契约。左右各一道隐私闸:观察行只存{key, count, flags?},形状闸验原始输入再构造,任何带command/args/segments的输入大声拒(静默剥离会把上游的形状 bug 藏进一次成功写入);P1 那边同样只保留动词/子命令/旗标、位置参数一律丢弃——回执会显示在界面上、可能被截图,而完整命令行里几乎必然含绝对路径。端到端实测当场抓出四个真 bug(脚本路径被当子命令、git -C <path> log把值当子命令、带点的文件名通过校验、旗标被重复渲染),全部属于同一条红线。 -
步骤新增
assert与optional:机器能不能验完这一步,从此在数据里说得出口 —assert不是普通字段,它是「可断言节点 vs 判断节点」这整个划分的唯一载体——没有它,「哪些步骤能被机器自动盯着」在代码里就没有地方表达(评测报告的实测是 0/19 步,一条流程全靠人判,而那个事实当时没处可写)。细节
加它要一次改四处(校验 / 归一化 / 节点投影 / 首屏计数),漏任何一处症状都是「加了但面板看不见」且不报错,所以先装一道字段白名单锁再改:往步骤里塞
assert/onFailure/retries,三个全部静默消失,正是这类「声称改了但产物里查不到」的同型装置。两个语义决定:没写断言时投影成null而不是空数组(后者会让「作者没写」与「写了但一条都没通过」长得一样,而验收时这是两件事);首屏新增assertable计数与既有contrast并列(后者数「写了没有」、内容仍靠人判,前者数「机器能不能判」,0/19 是实话不是缺陷,但清单上必须看得见)。optional缺省false(=必经),显式落键而非缺省缺键——「这步可不可跳」是判据要读的语义,读侧拿到的一律是布尔;存量步骤表零改动。 -
R1/R2:跳过有了性质,前置违规与 tool 过时都有了提议 — 同一句「这一步总被跳」,在可选步骤上是不该提议删的(只发
optional-skip-ok信息项),在必经步骤上则同时意味着「删掉这一步」与「这条路不对」两种读法,两种提议并存、谁也不吞谁——第一版把drop-step挂在else if里永远到不了…细节
被自己的测试抓出来。R1 的前置违规跨整本账本逐 run 查(保住 run 边界,
tallyRuns已经把 run 抹掉了),按run|step|need去重、同一步对不同前置各报一条(修图的人要知道断在哪条边上),一次就报。retirementProposals的封闭问题表从两类穷举到五类——新类不配问法会落进「岔路」那条毫不相关的问法里。 -
沉淀链:从真实会话日志里把反复出现的手法挖成 draft 工作流,但判据不给结论 —
src/pipeline-nudge.js走「多帧 zstd 逐帧解 → 重复序列候选 → 能算的缺席对照 + 答不出的封闭问题 →status:"draft"草稿(过validatePipeline)」。细节
buildCounterfactual().verdict恒为null:机器只写观察到的,其余留「(待补)」,答案只能由人回答、经applyAnswers写进steps[].evidence[]。专家团复核抓出七处,最贵的一条是「空回答不算已答」——applyAnswers(c, {})与applyAnswers(c, null)曾返回ok:true并推进到answered,那是判据链路上唯一一处「没人回答也算答了」;另有一条把「机器不给结论」从产者的自觉变成边界的性质:校验层拒绝任何非null的counterfactual.verdict(缺键也拒)。其余是跨工作区不并账、unanswered补回出口、草稿与提议改原子落盘(一个半截文件曾能让整个项目层的工作流从面板上消失)、按层过滤后的相邻不许当真相邻、failures不再用null同时表示「未接线」与「确实没有失败记录」。 -
人格定义页 v1 → v1.2:从「一张卡」到「按 agent 寻址的多张卡」(beta,默认关) — v1 是存储/API/开关/注入/面板五件套(
user_settings加persona键、PERSONA_FIELDS白名单挑拣、坏 JSON 容错回空对象、GET+PUT /persona双面路由、inject.js里人格段前置而rules恒最硬、任一闸关时输出逐字节等于历史版本)。v1.2 按实测三反馈升级:设置键变成 `{default…细节
byAgent}
多卡形,**v1 的单对象读作default、零迁移**;注入按会话身份(session.header.agentPreset,与scope.js同源读法)寻址对应 agent 的卡,身份缺失/无卡/卡关一律落default;面板改多卡列表,补三个字段的可见标签(v1.1 反馈:aria-label看不见)与保存失败可见错误行(v1 的静默catch吞掉了 404,实测踩中)。**一次修好又回滚的教训**:卡标签删除的×复用了规则卡的mneme-set-del样式,而那个类只在.mneme-set-row:hover下解除opacity:0,卡标签不在该悬停容器里——功能在、UI 永不可达;修复(内联opacity:1`)后又因同一原因整体回滚(#459),留作已知问题。占位示例也从使用者自己的两个人设名换成了中性名。
🐛 修复
-
面板的检查更新把「成功」报成了「失败」,星星入口指向了「谁已经点过星」 — (#460):面板拿
updateInfo.status去比一组后端classify()根本不会返回的名字,于是成功的检查一律落进错误分支。细节
顺手补一道跨文件契约锁双向钉住(面板比较的每个状态必须是
classify能返回的值,取数路径必须对得上注册的路由),并把「重新注入错名字」作为判别验证——新锁会点名它。星星入口原先链到/stargazers,那是「已经点过星的人」列表、页面上没有 Star 按钮,改为仓库主页。 -
向量的「有向量」不再是一句空话:写入回执、行级模型出处与模型感知的回填选择器 — (#450):
embedFor/schedule返回结构化回执(ok/failed/skipped+ 原因)且schedule不再吞掉 promise,失败进 200 条环形账本(不接llm_audit_logs:高频回填会淹没 LLM 调用的追溯面)。细节
vector_meta.model_hash是「最后一次成功写入的模型」,换模型即被整表覆盖,无法回答「这一条向量是谁产的」——memories新增embedding_model列做行级出处,存量一律NULL(出身未知),不按时间推断模型名(猜出来的名字会被日后审计当作证据,而它不是);清空向量时连同出处一起清。回填与reindexMissing改走新选择器(空向量 OR 出身未知 OR 模型不匹配),换模型后旧向量才真正被重算,而不是永远占着「有向量」名额虚增覆盖率;maxTotal=0的语义定为一灌到底,启动期回填的三个写死数(500/10/200)改成配置项,返回体让capped与drained不再不可区分。 -
账本的 ids 跨块引用被
catch静默吞掉,fix-step-tool永远少发而界面无异常 — (CodeRabbit #458 四条里核实的三条):F1 账本ids原声明在if块内,观察旁挂那块跨块引用必ReferenceError,被外层catch吞成「没有观察」——提升为 handler 级runIds…细节
回归测试走真 handler + 真旁挂文件锁住「观察进了判据」这条正向信号(负向断言抓不到它,因为吞掉之后一切看起来都正常)。F2
config.memoryDir默认值是~/.dsh/memory,而observationDir不展开~,readdir永远落空,补expandHome与candidates.js同构。F3rework-path的两种成因(必经步骤被跳 / 被绕开成常态)共用一句问法,跳过场景答非所问,改为提议带cause字段、nudge按成因选封闭问法、旧形状退回原问法。 -
candidates.js的每一个换行都是「双 CR + LF」,从出生一路进了 main 又发到了 npm — 各 454 处。\r在 JS 里是合法空白,解析、加载、1869 条测试全绿——当时没有任何检查会红。细节
代价落在别处:任何改动这个文件的人 diff 都是整文件 908/908,噪声盖住真改动;同一份文件不同工具数出行数不同(
Get-Content908 行、编辑器 454 行);发布产物里带着畸形行尾。只动行尾、一个字符不碰(字节差 454 恰等于处数,剥掉全部 CR 后与 main 原版逐字节相同),并补一道扫字节序列本身的锁:既有的lib-smoke字节一致性锁抓不到它,那个锁只比 src↔lib 彼此是否一致,两边一起坏就一路放行;新锁直接查这个序列并报出文件与处数(注入 534 处双 CR 后精确变红、还原回绿)。 -
桌面版装的是 0.26.1,落在
dsh-better-sidebar的^0.18.0之外 — peer 是 optional 且消费端全防御式(服务注入 +typeof检查,issue #88 模式),范围只反映生态兼容面而非 API 锁,故放宽上界到<1而不是逐版追加。 -
合并人格页后
effective键计数锁少了一键 — 冲突时 git 把两边的加法串自动拼上,但只体现了一个+1——实际上两边各加一键(actionRunEnabled与personaEnabled),应为 99。细节
顺手把那条 26 项加法串收成一个字面量:那串每合并一次就要重算一遍,拼得越长越没人验算,换成 99 配一行注释反而可查。同步重新生成
lib/(合并时lib/config.js与lib/settings.js的冲突标记是脏的,sync后清零)。
🎨 面板
-
记忆页三栏改两栏、实体页去冗余、热力图改成按窗口分位染色 — (#448,三轮复检):记忆页左栏加宽到 264px 让四个重要性 chip 排成一行,重要性块跟在检索区之后而不是钉在底部;实体页搜索与计数吸顶、按类型分组的侧栏、画布 320 → 440、补「没有匹配」提示;设置页去掉自动弹出的版本横幅…
细节
改手动检查更新按钮(带行内结果)+ 星星入口 + 版本页脚,并把 M.O. 开关提到首屏(首屏预算 8 → 9,注释同步改);实体侧栏补
box-sizing: border-box——width:100%加水平内边距会把 224px 的栏撑破,overflow-x: hidden再把「×N」提及数裁掉。热力图撤掉写死的阈值(750k/1.5M/2.5M)改用每窗口分位分桶,低使用量的库也能铺满四档、空窗口降为 0 档,网格收到 GitHub 贡献图口径(11px 格 / 2px 间距 / 月份标签 / 一三五标注)。顺带补了一道反向 i18n 锁:既有的「每键恰好两行」计数锁看不见「零行」的键(它从计数里消失了),而Icon遇到未注册名字返回null,于是新增「每个字面量t()都得有词条、每个传给Icon的名字都得已注册」的锁(73 → 74),并做过「临时删掉某键两行词典 → 旧锁仍绿、新锁点名」的判别验证。
📚 文档
-
门面加「最近更新」、补四种安装来源、修掉一圈文档漂移 — (#466):根 README 新增 News 表(日期 | 版本 | 头条)与四种安装来源对照(npm 包名 / Git / tarball / 本地绝对路径),并写明
#path:不可省的理由——省掉装到的是仓库根那份壳(0.0.0、无dsh字段),宿主不会把它当插件…细节
github:slow-stack/mneme这种写法实测装出来就是个_pnpmPlaceholder空壳,正确写法是#path:/dsh-mneme。同时补 M.O. 章节、补路线图 v0.9 行、删掉两个 README 里早已不存在的幽灵键sessionLifecycleEnabled(src/全无命中,v0.7.11 已清理)。子 README 的「模型工具(9 个)」改成 10 个(漏了memory_register_document,而同一份文档的架构图里早就写着 10——自相矛盾比写错更容易被忽略)、「9 种记忆类型」改成 10 种(补document指针,issue #230)、设计文档 5 篇 → 11 项。 -
SECURITY.md支持版本表走到 0.9.x,并删掉一个不存在的安全供应商 — (#467):英中两张支持版本表同步到0.9.x,停止维护通知改成「尽力维护」而不是写死某个版本会终止;功能描述里写死的版本号(Standalone External API (v0.7.12, opt-in))全部去掉——功能描述不该绑版本。细节
删掉
OrbisAI Security六处:仓库里没有任何其他痕迹,它是 v0.7.1 一笔提交带进来的幽灵供应商,换成 CI 实况(gitleaks 全历史 + dependency-review 看 PR diff + OSV-Scanner 看 lockfile + Dependabot 告警 + secret scanning)。中文段此前整体落后英文段一个版本,这次一并补齐——包括「未配置 token 时远程端点实际处于禁用状态」这句,它正好是 fail-open 真相的反面。配套新增test/security-doc.test.js六条断言,其中两条把「开放读清单 / 受门控清单」与createApi真实装配出的route.policy双向比对——写进文档的路由清单从此不再可能悄悄漂移。
🔐 安全
-
外部访问 API 的鉴权从「每个 handler 自己记得写」变成「路由注册的必填字段」(#462 / #464) — 审计前的实际形状是 39 条路由里 20 条有
requireAuth,其中 6 条只写了写方法、读方法裸奔…细节
GET /export(导出整库,含archived/forgotten)与GET /external-api(明文回吐 Bearer,且会在未鉴权的请求里就地铸造 token)两条最贵重的路由都没有门。根因不是「忘了写」,而是漏写不会响:鉴权写在 handler 内部,少写一行的表现是「功能正常」。改法是把鉴权搬到结构上——新增export const ROUTE_POLICIES = Object.freeze(["open","token","token-write"])与export function createRouteRegistrar(webServer, apiToken, disposers),每条路由注册时必须声明 policy,漏声明或写错值 →createApi装配期直接抛,而不是运行时静默放行;token-write的语义是「只有非 GET/HEAD 才要 token」。39 条逐条归档为 open 19 / token-write 6 / token 14,23 处手写requireAuth与函数本体一并删除(grep 归零),routes: 36这个会腐化的字面量改成count()。唯一行为差异:少数「方法不对 + 未鉴权」的请求由 404 变 401(先判门再进 handler),方向是收紧。回归锁 6 条进新增的test/route-policy.test.js(漏声明必抛 / 重复kind:path必抛 / 三种 policy 语义 / 装配后全部合法 /count()与挂载数一致)。这组锁替代了原计划的权限矩阵测试:矩阵要抓的「新增路由忘了加鉴权」在结构上已经写不出来,测试只需锁住「结构本身还在」。 -
请求体上限从「没有」改成 4 MiB,超限回 413 而不是静默截断(#462 评审跟进) —
readBody原先无上限,且超限时返回空串——parseBody把空串解析成{},而PUT /profile、/rules、/persona把「字段缺失」当成「用户要清空」,于是一个 5 MiB 的请求可以静默清空画像 / 规则 / 人格。细节
现在超限
resolve(null)并由 21 处调用点统一早退回 413;错误分支同时改成resolve(overflow ? null : ""),避免 413 之后又被空串抹掉状态、让调用点拿空串继续写。两条既有测试原先是空断言(旧超限用例只断言notEqual(200),而/create成功返回 201),改为明确断言 413 + 库里没新增 + 画像/规则原样保留。 -
供应链三处收紧 —
security.yml的 OSV 扫描由fail-on-vuln: false改为true(有漏洞即红,不再只报告);release.yml的 npm 发布加id-token: write与--provenance(发布产物带可验证来源)…细节
仓库侧打开 secret scanning、push protection 与 Dependabot security updates。
main的保护规则 required checks 追加 gitleaks 与 dependency-review 两条(enforce_admins仍为 false,这是刻意留的:紧急修复通道要留着)。 -
secret scanning 告警 #1 结案:不是泄露,是语料漏改,修法给语料加锁(#465) — 告警说
dsh-mneme/test/helpers/write-admission-samples.js:76有明文 OpenAI Key,实际是刻意构造的形状样本(sk-proj-+ 26 个顺序字母 + 10 个顺序数字 +ABCD)。细节
该文件里 GitHub PAT / Slack / JWT / Stripe 四条凭据早已用
assemble()拆成两段拼接(注释写明理由:push protection 会以 GH013 拒推、gitleaks 全历史扫描会红),唯独第五条漏了——「逐条记得改」本身不可靠。改为同样走assemble(),并新增一条守卫用例:扫test/helpers/下所有.js,任何完整形状的凭据字面量都会让测试变红。守卫的规则前缀与生产src/sensitive-scan.js对齐(ghp|gho|ghu|ghs|ghr/xox[abprs]-/sk-(?:proj-)?),并加同源反查:每条规则给一个探针,断言探针同时被守卫自己的正则与生产scanSensitive命中——只查一边的话,任一侧收窄都不会红,正是这次漏掉的失效模式。双向变异验证过(守卫去ghs→ 红;生产去ghs→ 红)。
🧰 发版与文档
-
发版准备改成「机械的归机器、判断的归人」(Release Prep 工作流重做) — 原先的
scripts/release-prep.mjs只覆盖版本号、测试数与 CHANGELOG 占位节,而它对两个 README 的版本表有一半是静默失效的——根 README 的路线图与 News 表版本列写的是两段式**v0.9**…细节
旧正则只认三段式
**v0.9.0**,于是它打印「无版本表格行,跳过」,一个字节都没改。把「没做」说成了「不需要做」,比报错更坏:v0.9.0 那次发布准备(a9f8e35)只有 5 个文件变动,根 README 的路线图与 News 一字未动,就是这条路径留下的痕迹。现在版本表扫描拆成两段式与三段式两条独立正则(分开认是刻意的:给根 README 插**v0.9.1**会凭空造出一个不存在的小版本条目),syncReadmeTables()同时做三件事——三段式版本历史表插占位行、两段式路线图插 minor 占位行、把遗留的「🚧 准备中」回填成已发布。它刻意不把「✅ 已发布(v0.9.0)」改写成「(v0.9.1)」:脚本跑在打 tag 之前,改了等于在 PR 里提前宣布一个还没发出去的版本,PR 一旦放弃就成了谎话。返回的是 actions 数组而不是布尔,因为「没插」有三种原因(没有版本表 / 已是最新 / 插了),入口要能如实说明;actions 非空而文件没变则直接 exit 1,脚本自己的 bug 不许静默发布。 -
SECURITY.md的支持版本表只核对、不改写,落后时警告不中断 — 跨 minor 发版(0.9 → 0.10)时整张表要向下挪一格并同步改停止维护通知,那是维护策略的判断——机器写容易写出「0.9.x 已停止支持」这种真事故。细节
脚本只核对中英两张表的首行是否等于
${major}.${minor}.x并写明期望值。刻意不硬失败:表先改则package.json还旧、测试红;版本号先改则表落后、测试红——两头都堵死,人就会绕过这套流程手工发版,那才是最坏结果。代价是那个 PR 的 CI 一开始就是红的,直到人把表挪完,而这正是test/security-doc.test.js该红的时候。 -
根 README 的 News 表明确划在自动化之外 — 它是「日期 | 版本 | 头条」的编辑内容,脚本从不碰,改由工作流在 PR 正文与 commit message 里列进人写清单(连同 CHANGELOG 条目、占位行主题描述共三件)。
细节
CONTRIBUTING.md的发布流程由 6 步改为 7 步:第 1 步跑工作流、第 2 步补人写内容、第 3 步跨 minor 挪 SECURITY 表、第 5 步合并后打 tag、第 6 步说明 tag 触发release.yml的完整链路(三方一致校验 → 测试矩阵 →--provenance+publish-gate.mjs→ 从 CHANGELOG 抽正文建 Release),手工npm publish降级为第 7 步的「例外,不是流程」。工作流的 PR 正文改用--body-file(多行中文塞进--body需要一串$'\n'转义,读的人看不懂、改的人不敢动)。回归锁 8 条进新增的dsh-mneme/test/release-docs.test.js:两份真实 README 的版本表仍被认得出、两段式能插新 minor、三段式插完整 patch 号并带日期、已发布的行不被改写、插入不混入裸 LF、重复跑幂等、SECURITY 同 minor 通过跨 minor 报中英两条、节缺失或表格形状变了也必须报错。 -
Release 正文从「CHANGELOG 逐字搬运」改成「标题 + 一句导语 + 折叠细节」(新增
dsh-mneme/scripts/release-notes.mjs) — 此前release.yml用内联node -e把 CHANGELOG 该节原样写进 Release 说明,于是 v0.9.0 的 Release 页有 29921 字符、单条子弹最长 3318 字符——正文里全是决策理由与变异验证,读者要点开一屏才知道这条在说什么。细节
抽取搬到独立模块并改成分层渲染:每条子弹保留粗体标题与一句导语(默认可见),其余正文折进
<details>,CHANGELOG 本体一个字不动(那是含决策理由与审计线索的完整记录,压缩只发生在对外渲染这一步)。导语切分规则全由真实语料逼出:句末标点只认。!?而刻意不认半角句号(版本号里的点是同一字符,认了会切在版本号中间);软标点断点必须括号平衡(v0.9.0 实测切出过「GET /external-api(明文回吐 Bearer,」这种把从句切断的收尾);导语不得短于 24 字符(有条首句只有 8 个字的「两处水源在说谎。」,离开上下文没有信息量);正文本身比上限还短时整段就是导语、不再加省略号(否则凭空说「后面还有」)。回归锁 17 条进新增的test/release-notes.test.js(含真进程端到端:渲染后 CHANGELOG 字节不变、找不到小节必须非零退出且报出是哪一版),test/release-workflow.test.js补一条「正文抽取必须调模块、不许退回内联脚本」。两条评审意见都是真 bug,各补一条锁:一是「短正文保护」原先守在lede()末尾、排在软断点搜索之后,短正文里只要有逗号就会被加省略号,逗号恰是末字时省略号后面什么都没有、折叠块是空的——这条保护必须最先执行;二是渲染单位错成了「行」,而 CHANGELOG 里有 179 行缩进续行(0.8.6 / 0.8.7 / 0.8.12 等老版本集中出现,另有 59 行缩进子列表),逐行 trim 会把续行甩到条目的<details>外面并丢掉缩进,读者看到的是「正文只有一句,剩下的莫名其妙散在下面」——改为先splitBlocks()切出「一条子弹 + 它的全部续行」再按条目渲染,且导语只从条目首行取(把续行拼回正文再切,换行会混进可见行,续行又被推回 details 外面)。变异验证三轮共十处全部变红(删短正文保护 / 去掉括号平衡 / 导语上限归零 / 软断点consumed少记一位 / details 与正文之间去掉空行 / 删入口守卫 / 删版本格式校验 / 短正文保护挪回末尾 / 退回逐行渲染 / 导语改从整段取)。 -
发版准备把新版本节插在了
[Unreleased]之上,且把攒着的条目留在原地(第一次实跑暴露,随后按评审意见补两处) — 改造后的 Release Prep 工作流第一次在真实 main 上跑 v0.9.1,gh pr create因组织策略失败(GitHub Actions 不允许开 PR,属合理默认,不改),但脚本已经跑完并推了分支——于是它的产出能被直接审。细节
产出是错的,两个位置错误:新版本节被插在
# Changelog之后、也就是## [Unreleased]之上,得到「[0.9.1]排在[Unreleased]前面」的颠倒顺序;[Unreleased]里攒的 9 条条目原地不动,新节里只有一条「🐛 修复 /(待填)」。两处都有同一个历史痕迹作证:v0.9.0 那次发版(a9f8e35)之后靠一笔人工提交77df0fb「v0.9.0 版本节补上内容——把 [Unreleased] 攒的 27 条搬进来」才掰回来——机器算得对的事不该留给人补。现在先切出[Unreleased]到下一个## [之间的正文:非空则整段搬进新版本节、[Unreleased]留空;为空才插占位节。搬位置不搬内容,一个字不改,所以「新节里该写什么」仍然只由往[Unreleased]写条目的人做。两条评审意见都是真问题,各补一条锁:一是用moved(-条目数)当「有没有搬」的判据是错的——正文非空但一条-都没有时moved是 0,入口会打印「占位节([Unreleased]本来是空的)」,而它显然不空;改为独立字段movedContent决定日志分支、moved只用来把话说得更具体。二是body.trim()会吃掉首行的前导空格,[Unreleased]首行若是四空格缩进代码块就会降级成普通段落,与「原样搬」矛盾;改为只剥节边界空行(并以原文行尾拼回,免得 CRLF 文件退化成混合行尾)。回归测试 8 条 → 10 条。 -
发版准备工作流真正跑通:重跑不再撞分支,开 PR 失败会告诉人该去开哪个开关(两次实跑暴露的两个独立真因) — 2026-10-18 把 v0.9.1 在真实 main 上跑了两遍,八步里前七步全成功,每次都卡在最后一步,但两次的原因完全不同。
细节
①第一次(run 39123249758)报
GitHub Actions is not permitted to create or approve pull requests——工作流 job 里写着pull-requests: write,可它只是声明式权限,真正的总开关在仓库设置(Settings → Actions → General → Allow GitHub Actions to create and approve pull requests),实测gh api repos/slow-stack/mneme/actions/permissions/workflow返回can_approve_pull_request_reviews: false;更麻烦的是这个开关在组织层默认关闭时仓库层根本改不动(gh api -X PUT直接 409 Conflict:The organization does not allow GitHub Actions to create or approve pull requests),所以除了给报错补上「去哪个页面、打开叫什么名字的开关」,代码这边能做的只有把失败说清楚。②第二次(run 39126751114)在更早的位置就挂了:! [rejected] release-prep/0.9.1 -> release-prep/0.9.1 (fetch first)——上一次运行已经把同名分支推上去了,这条工作流原本不可重入。不可重入的代价不是「重跑失败」这么轻:人会以为流程坏了,转去手工发版,整套自动化就此被绕开,而那正是这套流程想避免的结果。修法是把推分支那一步改成三档守卫:分支上有进行中的 PR → 什么都不动、静默退出(分支上可能已经有人补了 CHANGELOG 条目,那正是留给人的那部分工作,覆盖掉比不能重跑更糟);没有 PR 且顶端是上次机器产出(提交者是modusensus、标题以chore: v<版本> 发布准备开头)→ 按重跑处理,用--force-with-lease=refs/heads/<分支>:<fetch 到的 SHA>覆盖(不是--force:远端在我们 fetch 之后被推过就拒绝);其余情况 → 停下并要人先确认那条分支。回归锁加在test/workflow-guards.test.js两条,五处变异验证(去掉 PR 查询 /--force退回无条件 / 删掉「顶端不是机器产出就停」的判定 / 报错文案去开关名 / 报错文案去具体报错)全部变红;其中后两条第一次是 GREEN——因为对全文 grep 会被注释里解释这件事的文字骗过,改用codeOnly()剥注释后才真正锁住。
📦 仓库结构
-
仓库根补一份「只是壳」的清单,让单仓多包探测器认得出可安装的子包(dshfind 显示「不可安装」的修复) — 本仓库一直是单仓多包——真正可发布的包在
dsh-mneme/,仓库根只有文档。细节
这在 npm 与
dsh plugin add眼里没有问题(用#path:或绝对路径指到子目录即可),但依赖「仓库根有清单」的工具会判错:dshfind.com 的探测器在probe-install.mjs:182返回no-manifest,页面直接写「Not an installable plugin package」,而这句判词与实际能不能装无关。同类项目(包名恰好等于仓库名)不会被误判,我们的包名@modusensus/dsh-mneme与仓库名mneme不同,探测器无从推断,只能看根清单。补的是一份刻意的空壳:private: true+version: "0.0.0"+ 不带dsh字段(带了探测器会把仓库根当插件本体,装出来是一个没有lib/index.js的空包)+ 不带type字段(带了会改变仓库根test/与scripts/下 CommonJS 的模块语义,scripts/release-prep.mjs与test/mcp-package.test.js的运行方式随之变化)。子包声明写进pnpm-workspace.yaml而不是package.json的workspaces:npm 一看到根workspaces就把 prefix 提到仓库根、在仓库根找 lockfile,于是dsh-mneme/里的npm ci报 EUSAGE 退出 1——ci.yml、release-prep.yml、release.yml三条工作流全在子目录跑npm ci,会一起炸掉;pnpm-workspace.yaml只有 pnpm 与探测器读,npm 不读,零副作用。同时改掉一圈因这件事而变假的表述:两份 README 的安装章节原本写「子目录包、仓库根没有package.json」,现在改说「单仓多包、根清单是仓库壳」;CONTRIBUTING.md的仓库布局与发布红线、AGENTS.md的仓库布局、ci.yml跨包平价腿的注释、release.yml的「勿把 link 指到仓库根」说明同步更新——文档说「根目录没有 package.json」而代码里有,比没有文档更坏。
🧪 内部与测试
-
注释里的裸行号改成「符号名 + 日期」 — 审计发现一批行号引用已经腐坏。判据刻意不取「本批改动使该行内容变了」——那会把早就腐坏的引用一起算进来;改为两个维度交叉:锚点法(把引用点名的符号写成正则,看它在基线里能否找到)与平移法(用
git diff --unified=0算净位移)…细节
结论 3 / 18 / 0——本批推歪 3 条已改、既有腐坏 18 条只登记不改、反常 0 条。单靠任一法都会判反:早期探针只跑锚点法、还把锚点抠成了引用正文里的字样,于是把本批推歪的一条误判成「既有腐坏」。修法照既有约定:不留裸行号,写成「<文件> 的 <符号名>,YYYY-MM-DD 在 :<行号>」——只把数字改对是治标,下次再插一行又错,而注释里的行号不会有人来测。
-
零消费者的代码与「替未来预留」的字段一次性清掉 —
runner里赋值后立刻void的死代码、只出现在 import 行的导入、confirm回执里三块零引用字段、BUSINESS_VERBS从 24 个砍到实测命中过的 6 个(21723 次调用只出现 git/gh/npm/node,其余是「猜未来会用到」的预留)、`lo…细节
oksLikeSubcommand
的五道if收成一个正则(字符集天然挡住旗标、含点文件名、$(…)片段与超长值)。**顺带修掉一处隐私回流**:hints[stepId]` 存的是完整命令行,而它会经提议流进命令回执——P1 的分桶层刚花力气把位置参数剥掉防路径泄露,这里等于从另一个口子把路径带回来,改成只存次数。实际净减 16 行(不是清单预估的 28,有几处加了说明注释抵消)——「删了 28 行」这种账要按实际算,不然它就是下一轮审计里的假话。