Skip to content

Sharing-service hardening: rule-name length bound, legacy rows without plan, reconcile branch coverage #5

Open

Description

@baozhoutao

业务场景(设计方案 V1.0 第 9 章「本部门 / 分管范围由系统按方案自动授权」;PR #4 评审记录)

按方案动态生成共享规则是数据范围的唯一落法,src/services/sharing-service.ts 需要在真实规模与升级场景下稳定。PR #4 评审留下六条记录,细化如下(调度员 2026-09-02 细化;原始记录见本 issue 首次正文与 PR #4 评审评论)。

触发操作

方案发布 / 关闭 / 归档;审批通过、加减分裁定、调整落地时的规则重申;应用启动。

输入与字段

  1. 规则名长度上界:sys_sharing_rule.name 最长 100,而派生规则名最坏约 104(长方案 slug)。在名称派生处对 slug 截断并追加 8 位稳定哈希后缀,保证任何方案名下规则名 ≤ 100;同一方案多次发布派生结果稳定(幂等)。
  2. 历史行回填:升级前已存在的 kpi_check_task / kpi_adjustment 行 plan 为空,落不进按方案的规则条件。在应用启动时(kernel:bootstrapped,与 src/data/align-demo-units.ts 同一挂点)做一次幂等回填:plan 为空且能从其填报单取到方案的行,写入 plan;记录回填条数到日志;无可回填行时零写入。
  3. 对账分支测试:reconcilePlanSharing 中停用旧式 kpi_share_* 规则的分支补单元测试(存在旧规则 → 停用;不存在 → 不动)。
  4. 日志区分:skipped_inactive 日志分成两个标记——管理员手工停用(规则 active=false 且不是本应用对账所写)与应用自身对账停用;实现上以规则描述或名称前缀区分,不新增字段。
  5. 空组织策略(裁定:维持现状):KPI_SHARING_NO_ORG 保持拒绝;单租户部署仍有组织(管理员建组织时即有),不放开。只需在该拒绝处补一句注释说明裁定与日期。
  6. 重申扇出(记录,不做):审批 / 加减分 / 调整落地时对结果规则逐条重申,当前规模可接受;大规模优化另立单。本项只在代码注释里标明该已知取舍。

校验规则

  1. 单测:方案名 200 个汉字时所有派生规则名 ≤ 100 且两次派生相同;
  2. 单测:回填——三种行(plan 为空可推导 / plan 为空不可推导 / plan 已有)只有第一种被写,第二次运行零写入;
  3. 单测:对账停用旧规则分支两个方向;
  4. pnpm verify、pnpm e2e 绿(T39 规则数不变);
  5. 文案红线不涉及(无用户可见文案变化);日志不含异常原文。

输出 / 反馈

依赖

无(main bdc87ff 已含 #10、#13)。

验收标准

  1. 校验规则 1~3 的单测存在且通过 → 通过;
  2. 用一个 plan 为空的核对任务行启动 dev,启动日志出现回填条数 1,该行 plan 已填 → 通过;再次启动零写入 → 通过;
  3. pnpm verify 与 pnpm e2e 原样输出绿 → 通过。

Activity

  1. baozhoutao commented on Sep 2, 2026

    @baozhoutao
    ContributorAuthor

    Refined into a dispatchable work item (body rewritten; original six findings kept as the basis). Rulings: item 5 (null-organization refusal) stays strict; item 6 (re-assert fan-out) is recorded, not implemented here.

    Claimed by dispatcher session session_01VR2khJ3Me96btawVsfG6jb. Branch: issue-5-sharing-hardening. Worktree ../kpi-issue-5, baseline main @ bdc87ff, dev port 3105, database .objectstack/issue-5.db. Developer subagent dispatched; no parallel item.


    Generated by Claude Code

  2. baozhoutao commented on Sep 2, 2026

    @baozhoutao
    ContributorAuthor

    Self-test report — work item #5, branch issue-5-sharing-hardening, PR #16 (draft).

    一、开测前三级核对

    项 读数
    进程 cwd /home/user/kpi-issue-5
    HEAD / 分支 b41648ce099a5e5397dcb64521b6efefe9152753 / issue-5-sharing-hardening(基线 main @ bdc87ff)
    端口 3105,开测前空闲;数据库 file:./.objectstack/issue-5.db(启动前 rm -rf dist)

    二、单元测试与元数据校验(pnpm verify,最终 commit b41648c)

      ✓ Validation passed (711ms)
    > tsc --noEmit
     Test Files  6 passed (6)
          Tests  86 passed (86)
    VERIFY_EXIT=0
    

    test/sharing.test.ts 单文件 44 passed,其中本次新增 19 条,按校验规则逐条:

    • 校验规则 1(规则名 ≤ 100 且幂等):方案名 200 个汉字:所有派生规则名 ≤ 100,且两次派生完全相同(幂等) ✓;
      最坏形态:方案 / 单元 / 领导三段 id 都超长时,最长的规则名恰好 99 ✓;
      修复前真正越界的形态:三段都是 32 位合规 id(旧代码原样保留,拼出 117) ✓;
      UUID 形态的 id(平台记录 id)仍走既有折法:头部 12 位 + 稳定哈希 ✓(升级兼容的钉子);
      合规但超长的 id 折成「截断 + 8 位稳定哈希」,长度恒为片段上界 ✓。
    • 校验规则 2(回填三种行 + 第二次零写入):三种行只写第一种;第二次运行零写入 ✓
      (断言 filled:1 / unresolved:1 / failed:0、写载荷键恰好 ['plan']、写上下文 {isSystem:true}、
      第二次 writes 长度 0);另有两对象都回填、被 hook 拒写计入 failed 不阻断其余行、
      无填报单的行计入 unresolved 而不写空、挂点是 kernel:bootstrapped 且无事可做时零日志。
    • 校验规则 3(对账停用旧规则两个方向):存在旧规则 → 停用(并盖上对账标记,行保留不删) ✓;
      不存在旧规则 → 一条都不动 ✓;附加 别的方案的规则与被管理员改过的规则都不在对账射程内 ✓。
    • 第 4 项日志区分:管理员手工停用的规则记 skipped_inactive_admin ✓;
      本应用上次对账停用的规则记 skipped_inactive_reconciled ✓;两种情形都不改写 active ✓。

    三、端到端(KPI_BASE_URL=http://localhost:3105 node scripts/e2e-flow.mjs,空库)

    {"passed":73,"failed":0}
    PASS T39 发布按方案配置写入动态共享规则,条数与方案配置精确相符,元数据零改动 — rules=29 expected=29
    PASS T39b 规则条件按方案隔离:每条规则的条件里都带本方案 id — 29 条
    PASS T54 换人后新方案只为新任分管领导建规则,老领导没有新方案的规则 — 新方案 27 条规则
    PASS T57 方案关闭后填报单 / 核对任务 / 调整的共享降为只读(留读、去写) — 29 条规则,其中可编辑 0 条
    

    校验规则 4 满足:T39 规则数不变(29 = 29),73 项全绿、零失败。

    四、验收标准 2:回填的真实运行

    制造历史行的方式:先按分配表用管理员经 REST PATCH /data/kpi_check_task/{id} 送 plan: null
    —— 平台返回 200 但静默忽略(plan 是 readonly 字段),读回仍是原值,REST 这条路走不通。
    改用分配表许可的回退:停 dev 后用 node:sqlite 直改本实例库文件 .objectstack/issue-5.db
    的该行(update kpi_check_task set plan = NULL where id = '-Mo-qaYenOTtciFY',changes = 1),
    不碰任何其它库、任何其它行。

    回填前(sqlite 读数):

    {"id":"-Mo-qaYenOTtciFY","sheet":"Kb3ZGjVNjmnbYZBp","plan":null}
    rows with empty plan = 1
    

    重启后的启动日志原样片段:

    2026-09-02T19:18:44.428Z INFO [kpi] position bindings ensured {"created":0,"total":6}
    2026-09-02T19:18:44.457Z INFO [kpi] 历史行方案回填完成 {"filled":1,"unresolved":0,"failed":0,"byObject":[{"object":"kpi_check_task","filled":1,"unresolved":0,"failed":0},{"object":"kpi_adjustment","filled":0,"unresolved":0,"failed":0}]}
    

    回填后经 REST 读回该行:

    REST readback: {"id":"-Mo-qaYenOTtciFY","sheet":"Kb3ZGjVNjmnbYZBp","plan":"B8US3PPR544FyxOR","branch":"bu_north","status":"confirmed"}
    

    第三次启动(幂等复核):position bindings ensured 照常出现(证明引导钩子确实跑了),
    回填 相关日志 0 行,plan 为空的行数 0,该行读回不变 —— 零写入。

    ⚠️ 一处需要知道的读数:ctx.logger.info 在 dev 的默认日志级别下不打印(align-demo-units
    自己的 info 行同样不出现)。上面的回填日志是用 pnpm dev --log-level info 采到的;
    默认级别下回填照常执行(第二次启动就是默认级别,该行的 plan 确实被补上了),只是那一行看不见。
    这是平台日志级别问题、不是本次改动的问题,记入遗留问题。

    五、反向验证(消融)

    三条,每条都先 commit 再变异,变异腿与还原腿都在磁盘上核对(注入串 / 被删串各 grep -c
    一次,并比对 blob 哈希与 HEAD),脚本带 trap ... EXIT INT TERM 还原;
    本仓 vitest 直接从 src/*.ts 解析,不经 dist/,无产物过期风险。

    消融 预期方向 实测
    把 ruleSlug 退回原来的 32 位直通、删掉截断分支 转红 3 failed | 41 passed
    去掉对账停用时盖的描述标记 转红 1 failed | 42 passed
    去掉回填的「推不出方案就跳过」守卫(改成照写空值) 转红 1 failed | 42 passed

    三次还原后 blob 哈希均等于 HEAD、git diff HEAD 为空。

    补一条诚实记录:第一版「最坏形态」用例取的是 64 位 id,而旧代码对超过 32 位的 id 反而会折短
    —— 那条用例在消融下并不会因越界而红。发现后补了「三段 32 位合规 id」这一条(旧代码拼出 117),
    消融红数从 2 升到 3,这才真正盯住了修复前的越界入口。

    六、自测结论

    通过。 校验规则 15 与验收标准 13 全部满足;文案红线不涉及(无用户可见文案变化,
    新增的只有服务端日志与规则描述前缀,不含异常原文)。


    Generated by Claude Code

  3. baozhoutao commented on Sep 2, 2026

    @baozhoutao
    ContributorAuthor

    Requirements conformance checklist — work item #5, PR #16, commit b41648c.

    三态:✅ 已实现并验证 / ⚠️ 有条件满足(出口链接) / ❌ 未实现。

    输入与字段(正文六条)

    # 条目 三态 证据 / 出口
    1 规则名长度上界:截断 + 稳定哈希后缀,任何方案名下 ≤ 100,多次发布派生稳定 ✅ RULE_NAME_MAX=100、RULE_SLUG_MAX=26,最坏形态恰好 99;5 条单测(含「修复前真正越界的 32 位形态」与幂等)
    2 历史行回填:kernel:bootstrapped 挂点、幂等、记录条数、无可回填行零写入 ✅ 新增 src/data/backfill-plan-links.ts,objectstack.config.ts 的 onEnable 注册;真实运行 filled:1,第三次启动零回填、零写入
    3 对账分支测试:存在旧规则 → 停用;不存在 → 不动 ✅ test/sharing.test.ts 两个方向 + 别的方案 / 管理员改过的规则不在射程内
    4 日志区分:管理员手工停用 vs 应用对账停用,以描述或名称前缀区分,不新增字段 ✅ skipped_inactive_admin / skipped_inactive_reconciled,以 RECONCILED_DESCRIPTION_MARK 描述前缀区分;3 条单测
    5 空组织策略维持拒绝,拒绝处补注释(裁定 + 日期 2026-09-02) ✅ src/services/sharing-service.ts 的 KPI_SHARING_NO_ORG 拒绝处
    6 重申扇出:只在注释里标明取舍,不做 ✅ provisionPlanSharing 文档注释新增「已知取舍:重申的扇出」一节

    校验规则

    # 条目 三态 证据 / 出口
    1 方案名 200 汉字时所有派生规则名 ≤ 100 且两次派生相同 ✅ 单测通过;消融该守卫后 3 条转红
    2 回填三种行只写第一种,第二次运行零写入 ✅ 单测断言 filled:1 / unresolved:1 / failed:0、写载荷键恰好 ['plan'];第二次 writes 为 0
    3 对账停用旧规则两个方向 ✅ 单测两条
    4 pnpm verify、pnpm e2e 绿(T39 规则数不变) ✅ verify 86 passed / exit 0;e2e {"passed":73,"failed":0};T39 rules=29 expected=29
    5 文案红线不涉及;日志不含异常原文 ✅ 无用户可见文案变化;新增日志只有键值(rule / object / 计数),回填拒写那一条记 error 摘要用于定位,不面向终端用户

    验收标准

    # 条目 三态 证据 / 出口
    1 校验规则 1~3 的单测存在且通过 ✅ test/sharing.test.ts 新增 19 条,单文件 44 passed
    2 plan 为空的核对任务行启动后回填条数 1、该行 plan 已填;再次启动零写入 ⚠️ 行为全部满足(日志 filled:1、REST 读回已填、第三次启动零回填零写入)。出口:ctx.logger.info 在 dev 默认日志级别下不打印(align-demo-units 的 info 行同样不出现),该行日志需 pnpm dev --log-level info 才可见;回填本身在默认级别下照常执行。见遗留问题 R1
    3 pnpm verify 与 pnpm e2e 原样输出绿 ✅ 见测试报告第二、三节的原样输出

    输出 / 反馈约定

    条目 三态 说明
    代码落点:src/services/sharing-service.ts、src/data/ 下新增回填模块并在 objectstack.config.ts 挂接、test/sharing.test.ts 增测 ✅ 四处与正文指定一致
    不改 docs/00~03、README、CLAUDE.md;docs/04 #18 备注可补一句 ✅ 仅 docs/04-需求符合度清单.md #18 备注加了第 ⑤ 点「历史行回填」;.github/ 未触碰

    遗留问题

    • R1:ctx.logger.info 在 dev 默认日志级别下不打印(平台日志级别行为,非本次改动引入;
      align-demo-units 既有的对齐日志同样不可见)。影响的是可观测性,不是回填行为本身。
      是否要把这类启动期一次性摘要提到 warn 级、或在平台侧调整 dev 默认级别,建议由验收侧定夺后另立工作项。

    Generated by Claude Code

  4. baozhoutao commented on Sep 2, 2026

    @baozhoutao
    ContributorAuthor

    Dispatcher collection check for PR #16 (head b41648c, base bdc87ff): passed — 5 files (+612 / −8), none of the forbidden files, no binaries; git merge-tree clean; pnpm verify re-run: validation passed, typecheck clean, 86/86 tests; CI verify green; trailers present on both commits, no model identifiers. Next: independent review gate, then merge.


    Generated by Claude Code

  5. baozhoutao commented on Sep 2, 2026

    @baozhoutao
    ContributorAuthor

    Review gate (independent reviewer, PR #16 @ b41648c): NOT MERGEABLE — 0 blocking, 1 should-fix, 5 notes. Returned to the developer on the same branch.

    Should-fix: the boot-time backfill's entry-sheet lookup (planOfSheets) has no error guard while every other query in the module does; a failed read there propagates out of the kernel:bootstrapped handler as an unhandled rejection and skips the adjustment pass. Fix: guard the lookup (warn, return empty map → rows stay unresolved for the next start) and wrap run like align-demo-units does; add a unit case.

    Folded in from the notes: ruleSlug comment precision (which id shapes actually change), a guard for the batch loop stalling when a whole batch is unresolved, comment wording about hooks and system context, and the backfill summary log at warn level when rows were filled (so acceptance criterion 2 is observable at the default level).

    Verified by the reviewer: the rule-name bound holds (max derived name 99) and UUID-shaped ids keep their pre-fix slug byte-for-byte (differential over 3,500 ids: zero drift), so no existing rule loses reconciliation after upgrade.


    Generated by Claude Code

  6. baozhoutao commented on Sep 2, 2026

    @baozhoutao
    ContributorAuthor

    Requirements conformance checklist (revision 2) — supersedes the checklist in comment 5515077347. Work item #5, PR #16, commit 888e642.

    本评论订正上一版的验收标准 2(⚠️ → ✅),并补录评审闸门 Review round 1 的五项。
    工具不能编辑旧评论,故以新评论订正;上一版其余条目结论不变,原文保留可查。

    三态:✅ 已实现并验证 / ⚠️ 有条件满足(出口链接) / ❌ 未实现。

    一、订正条目

    # 条目 上一版 本版 依据
    验收标准 2 plan 为空的核对任务行启动后回填条数 1、该行 plan 已填;再次启动零写入 ⚠️ ✅ F6 落地后摘要日志改 warn 级,dev 默认日志级别下可见,不再需要 --log-level info。已用默认级别重新采证一遍(下方第三节)

    原 ⚠️ 的出口(遗留问题 R1「ctx.logger.info 在 dev 默认级别下不打印」)对本工作项已关闭 ——
    回填摘要不再走 info。平台该行为本身仍然存在,只是不再影响本项验收,降级为观察项(下方第五节)。

    二、Review round 1 五项

    项 条目 三态 证据 / 出口
    F1 planOfSheets 包 try/catch,失败 warn 并返回空 Map;run 外再包一层 catch;补单测「读填报单失败不抛、不阻断另一个对象的回填」 ✅ 两条单测:读填报单失败:不抛、不阻断另一个对象的回填,本批延到下次启动(两对象都跑到、各落 unresolved、writes 为 0)、注册入口即便内部整体失败也不抛。消融该 try/catch → `1 failed
    F2 ruleSlug 注释「越界的只有它」改为两段说明 ✅ 注释已改:27–32 位合规 id(唯一越界段)与 33 位以上合规 id(折法改变但未越界);并注明两段在本应用都不产生。提交信息 888e642 同步了这段更正(旧提交信息不可改写,以新提交为准)
    F3 分批循环整批无进展时不静默 ✅ 选「补 warn」这一支:fresh.length === 0 && batch.length === BATCH 时记 a full batch made no progress 并带三个计数。未选 $nin —— 它会把 seen 整个塞进查询条件,批量一大就撞上与 F1 同一个参数上限。单测 整批都没写动时留下一条 warn
    F4 写被拒注释改措辞 ✅ 改为「本仓 hook 对系统上下文一律放行,这条路径今天走不到;留着是为了将来某个 hook 不放行时计数跳过而不中断」
    F5 不要动 ✅ 未触碰
    F6 filled > 0 时摘要用 warn 级,零回填仍不打印;用默认级别重启采证 ✅ 单测 有回填发生时摘要走 warn 级;默认级别实测见下方第三节

    三、验收标准 2 的重新采证(默认日志级别,888e642)

    rm -rf dist + 删库 → OS_PORT=3105 pnpm dev(不加 --log-level)→ pnpm e2e →
    停 dev → 用 node:sqlite 把一条核对任务的 plan 清空(REST PATCH 送 plan: null 返回 200
    但静默忽略,plan 是 readonly 字段,这条通路走不通;只改本实例库 .objectstack/issue-5.db 这一行)→ 重启。

    清空后(sqlite 读数):

    picked row (before) = {"id":"SQhRAqVX1b4ynOLw","sheet":"S4JRgqy0-EqulfR2","plan":"6kFVQ2smhXEJwJ9J"}
    UPDATE changes = 1
    after clear = {"id":"SQhRAqVX1b4ynOLw","sheet":"S4JRgqy0-EqulfR2","plan":null}
    empty-plan rows = 1
    

    重启后的启动日志原样片段(默认级别):

    2026-09-02T19:36:15.888Z WARN [kpi] 历史行方案回填完成 {"filled":1,"unresolved":0,"failed":0,"byObject":[{"object":"kpi_check_task","filled":1,"unresolved":0,"failed":0},{"object":"kpi_adjustment","filled":0,"unresolved":0,"failed":0}]}
    

    回填后经 REST 读回:

    REST readback: {"id":"SQhRAqVX1b4ynOLw","sheet":"S4JRgqy0-EqulfR2","plan":"6kFVQ2smhXEJwJ9J","branch":"bu_south","status":"confirmed"}
    

    再次启动(仍是默认级别):回填 相关日志 0 行,empty-plan check_task rows = 0,该行读回不变 —— 零写入。

    四、本轮全量验证

      ✓ Validation passed (714ms)
    > tsc --noEmit
     Test Files  6 passed (6)
          Tests  90 passed (90)
    VERIFY_EXIT=0
    
    E2E_EXIT=0
    {"passed":73,"failed":0}
    PASS T39 发布按方案配置写入动态共享规则,条数与方案配置精确相符,元数据零改动 — rules=29 expected=29
    PASS T39b 规则条件按方案隔离:每条规则的条件里都带本方案 id — 29 条
    

    校验规则 4(pnpm verify 与 pnpm e2e 绿、T39 规则数不变)在本轮 commit 上重新满足。
    上一版清单的其余条目(输入与字段 16、校验规则 13 与 5、验收标准 1 与 3、输出/反馈约定)结论不变。

    五、遗留问题(修订)

    • R1(降级为观察项):ctx.logger.info 在 dev 默认日志级别下不打印(平台行为,非本次引入;
      align-demo-units 既有的对齐日志同样不可见)。回填摘要已改走 warn,本项验收不再受影响;
      该平台行为是否需要处理,建议由验收侧另行判断。
    • R2(新增,过程记录):PR 正文经 GitHub 编辑接口(PATCH)更新时,末尾的 --- + 署名页脚
      会被稳定剥除(两次尝试均如此,创建时不会)。PR Sharing-service hardening: rule-name length bound, legacy plan backfill, reconcile branch coverage #16 正文因此当前无页脚;评论通路的页脚正常存活。
      这是平台正文处理行为,不影响代码与验收。

    Generated by Claude Code

  7. added a commit that references this issue on Sep 2, 2026
  8. baozhoutao commented on Sep 2, 2026

    @baozhoutao
    ContributorAuthor

    Merged PR #16 into main as fd268d1 (collection check passed at both rounds; review gate MERGEABLE at 888e642; CI verify green). Status → PM验收中; handed to the work-item creator for acceptance. Rulings recorded in the body: null-organization refusal stays strict; re-assert fan-out is recorded only. Reviewer note left open (not blocking): the "registration entry never throws" unit case does not pin the outer catch on its own.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions