Skip to content

【No.6】feat(metrics): add full-response repetition diagnostics - #332

Closed
bugkeep wants to merge 4 commits into
redai-studio:mainfrom
bugkeep:feat/full-response-repetition
Closed

bugkeep wants to merge 4 commits into
redai-studio:mainfrom
bugkeep:feat/full-response-repetition

Conversation

@bugkeep

@bugkeep bugkeep commented Sep 18, 2026 •

Copy link
Copy Markdown

What

Implement No.6 — 长响应全文重复检测与离线诊断 (full-response repetition detection and offline diagnostics) from the second Relax contributor program.

This detects highly repetitive text anywhere in Sample.response and helps locate suspicious regions in saved rollouts. Compression is used only as a statistical measurement; this feature does not summarize or compress context/memory, rewrite responses, stop generation, or change rewards. Tool observations remain part of the original response.

Deliverables:

  • Full-response overlapping-window detection, preserving has_repetition(...) -> bool and the existing repetition_frac sample-level metric.
  • Detailed results: hit status, suspicious character intervals with individual compression ratios, the maximum over every scanned window, and union-based covered character counts.
  • Offline CLI for existing training/evaluation JSONL summaries and Torch debug dumps, including record identities, console hit summaries, and an atomic JSON report.
  • Fixed regression fixture, unit and production-metric/dump integration tests, reproducible CPU benchmarks, and English/Chinese usage guides.

Why

Relates to #321, specifically task No.6. Claim: #321 (comment) (bugkeep / Shuan-xx). This PR covers No.6 only; the team's separately claimed No.8 is outside its scope. No RFC is required for No.6.

Previously, has_repetition only inspected the last 10,000 characters and only when the response was longer than 10,000 characters. A response with a severely repetitive middle and an ordinary ending therefore produced a false negative. The committed fixture tests/fixtures/repetition/middle_repetition.jsonl demonstrates this: the old suffix check stays below the threshold while the full scan finds the middle window.

Reference: radixark/miles#3115. Unlike that PR's later fixed-window-count optimization, this implementation never widens the stride or drops intermediate windows to cap cost.

How

Detection semantics

  • Default window 10,000 Unicode code points, stride 5,000. Add an end-aligned full suffix window when necessary, without duplicating aligned windows.
  • Ratio = UTF-8 original bytes / zlib-compressed bytes, compression level 9. A hit requires ratio > 10; equality is not a hit.
  • Offsets are zero-based Python string indices, [start, end); they are neither byte offsets nor token offsets. Intervals identify suspicious windows, not exact repetition boundaries.
  • Empty responses produce no hit and a JSON null maximum. Nonempty short responses and exactly-one-window responses are checked once. This intentionally expands the historical metric's coverage, including short responses.
  • The boolean API stops only after finding a hit. Detailed diagnostics scan every window to return all hits and the actual global maximum. Covered characters count the union of hit windows.

Integration and offline use

The existing production compute_metrics_from_samples implementation is moved unchanged into relax/utils/metrics/rollout_metrics.py and re-exported by the distributed rollout module. The Ray import in get_current_node_ip is deferred; misc.is_port_available remains a module-level export used by get_free_port. This lets CPU tests exercise the same production aggregation without starting inference engines; metric names and sample-level denominators are retained.

Training metrics scan every nonrepetitive response without a window limit. With fixed window/stride sizes, per-step CPU cost grows with the total response characters across the batch. Detector-only benchmarks do not establish whole-batch aggregation cost or training throughput.

On POSIX, atomic report replacement preserves existing read/write/execute permission bits. New reports default to private 0600; ownership, ACLs and extended attributes are not copied. These rules are documented in both usage guides.

python -m relax.entrypoints.repetition /path/to/rollout_result --output repetition-report.json
python -m relax.entrypoints.repetition /path/to/debug.pt --output debug-report.json

JSONL processing streams one record at a time and needs no Torch/Ray/SGLang dependencies. Torch loads on CPU with restricted deserialization by default; trusted legacy object dumps require explicit --trusted-torch. Records preserve physical source/position and available rollout/sample/group/dataset identifiers. Invalid records fail with a location instead of disappearing silently. Reports are published only after a successful complete scan.

Usage: docs/en/guide/repetition-diagnostics.md and docs/zh/guide/repetition-diagnostics.md.

Testing

Includes upstream main at 0651812.

Review-fix validation: 86 passed, 30 skipped on Windows, covering the three repetition suites plus real-socket port tests, POSIX report-permission tests, and the existing weight-sync port-hardening suite. Skips comprise 23 weight-sync tests and one logger test requiring unavailable training dependencies, plus six POSIX-only cases. WSL has no pytest installed; POSIX cases await Linux CI. Full-file pre-commit compatibility checks passed with Python 3.12 for docformatter. The prior GPU run at 961d092 had 2779 passes and exactly six failures involving the removed misc.is_port_available export; the new commit restores that export. Its GPU integration workflows passed both Qwen training cases. The new commit requires fresh CI results.

python -m pytest -q tests/utils/test_repetition.py tests/utils/test_repetition_dump.py tests/utils/test_repetition_metrics.py tests/utils/test_metric_utils_rloo.py tests/utils/test_multimodal_rollout_stats.py

91 passed, 2 skipped. Tests cover beginning/middle/end repetition, the fixed middle-repeat/normal-tail fixture, empty/short/exact-window responses, overlap boundaries, unaligned suffixes, Chinese/emoji/combining-mark offsets, controls, threshold equality, maximum calculation, union coverage, invalid input/configuration, and legacy compression compatibility. Integration tests call the real production sample aggregation and actual dump writers, reader, and CLI, without replacing the detector or aggregation with mocks.

The two skips require the unavailable Ray/SGLang/Megatron training environment (the optional distributed logger wiring check and an existing eval-logger test). No GPU/multinode training run or full pytest tests/ pass is claimed.

  • pre-commit run --all-files compatibility run passes (all hooks executed; details below)
  • Full test suite passes (pytest tests/) — targeted results above; full training dependencies unavailable locally
  • New tests added
  • Documentation updated

Local pre-commit was run over all files in an isolated Windows worktree. A temporary, uncommitted configuration selected existing Python 3.12 for the original pinned docformatter hook (Python 3.13 removed lib2to3). Windows checks out upstream symlinks as plain placeholder files; after the EOF hook normalized those placeholders, the all-file rerun passed. Those 12 unrelated placeholder-only changes were excluded and restored. All changed-file hooks also pass. No hooks were disabled or project hook configuration changed; upstream Linux CI remains authoritative.

Type of Change

  • Bug fix
  • New feature
  • Breaking API change
  • Documentation update
  • Refactoring of metric module placement (aggregation unchanged)
  • Performance improvement
  • CI/CD or build changes

Screenshots / Logs

Reproduce the actual detector benchmark:

python -m relax.tools.benchmark_repetition --output benchmarks/repetition/local.json --repeats 7

benchmarks/repetition/windows-python313.json records 24 cases: 10k/100k/1m characters, nonrepetitive control / middle repetition / full repetition / Chinese middle repetition, detailed and boolean APIs. Each case uses a fresh subprocess and seven timing batches. Full results and methodology: benchmarks/repetition/README.md.

Detailed scan of the nonrepetitive control on Windows 11, Python 3.13.9, zlib 1.3.1, 12 logical CPUs:

Characters All windows scanned Median CPU Median wall Peak process working set
10,000 1 0.053 ms 0.068 ms 24.06 MiB
100,000 19 1.645 ms 1.690 ms 25.69 MiB
1,000,000 199 18.229 ms 18.701 ms 37.19 MiB

Native process peaks include interpreter/imports, input creation, warmup and native zlib allocations, rather than claiming incremental scanner memory. Separate tracemalloc scan peaks, raw timings, and environment metadata are included. Timing excludes input generation/imports and memory tracing; these measurements are not end-to-end training throughput.

Required CI and rai-studio-bot review must pass before requesting the community's manual mentor review. This PR does not claim those pending checks are complete.

# ⭐ Feature

- Scan every response window with a 10,000-character window and 5,000-character
  stride, retaining the strict compression threshold and boolean metric API.
- Report suspicious intervals, per-window and maximum compression ratios, and
  union coverage without changing responses, generation, or rewards.
- Add streaming JSONL and CPU Torch dump diagnostics with atomic JSON reports.

# ♻️ Refactor

- Reuse the existing rollout metric aggregation in a CPU-importable module and
  preserve its distributed entry point; defer Ray-only utility imports.

# ✅ Tests

- Add fixed middle-repeat/normal-suffix regression and real dump/CLI roundtrips.
- Validate the production aggregation and boundary, Unicode, strict-threshold,
  overlap-union, and full-coverage cases.
- Record 24 reproducible CPU benchmark cases through one million characters.

# 📝 Documentation

- Add English and Chinese usage guides and benchmark methodology/results.

Relates to redai-studio#321 (contributor task No.6).
@SigureMo SigureMo changed the title feat(metrics): add full-response repetition diagnostics (No.6) 【No.6】feat(metrics): add full-response repetition diagnostics Sep 18, 2026
@SigureMo

Copy link
Copy Markdown
Member

辛苦 merge 下最新 main,新增了两条流水线

@bugkeep

bugkeep commented Sep 18, 2026

Copy link
Copy Markdown
Author

辛苦 merge 下最新 main,新增了两条流水线
好的

# 🔩 Chore

- Merge upstream main at 0651812 to incorporate the GPU integration workflow and local device-action references.
- Preserve the full-response repetition diagnostics changes.

# ✅ Tests

- Run the full pre-commit suite with Python 3.12 for the legacy docformatter hook.
- Run repetition detection, dump reader, CLI, and production metrics regression tests.
@rai-studio-bot

rai-studio-bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Nyanpasu 审查看板

审查状态: ✅ 已通过

审查版本: f2babd933350222d081cdddea9ac15e0620fc540

已针对修复提交 `f2babd93` 完成复审:上一轮的 1 项阻塞 + 2 项非阻塞全部修复并经证据核验,本轮未发现新的阻塞或非阻塞问题。新 head 上 7 项检查全绿,其中此前失败的 GPU Unit Tests 由 `6 failed, 2779 passed` 转为 `2794 passed, 18 skipped, 0 failed`;三位发现分别通过 CI、本地复现与新增回归测试确认关闭。

编号 问题 优先级 状态 规则来源
F1 misc.py 导入下沉移除模块属性,打断既有端口测试 P1 ✅ 已解决 —
F2 repetition_frac 无窗口上限,训练侧每步检测成本随响应长度线性上升 P2 ✅ 已解决 —
F3 离线报告权限固定 0600,且静默改写既有报告的权限 P3 ✅ 已解决 —
Powered by Nyanpasu with deepseek-v4.1-flash-ali medium, please check the suggestions carefully.

@rai-studio-bot rai-studio-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查结论:请求修改

已针对 961d092a 完成审查。相比上一版 cda2ff1d,本 PR 仅合入了上游 CI 变更,检测器、离线 CLI、dump 读取与指标模块代码逐字节一致,结论不受影响。

阻塞(1 项)

  • relax/utils/misc.py 把 is_port_available 的导入下沉到函数体内,移除了 relax.utils.misc 的模块属性,会让既有测试 tests/distributed/ray/test_weight_sync_port_hardening.py 的 6 处 patch.object(misc, "is_port_available", ...) 直接报 AttributeError;在 CI 训练镜像下该文件不会被 skipif 跳过,pytest tests/ 会失败。修复方式与验证命令见该线程。

非阻塞(2 项)

  • repetition_frac 依赖的 has_repetition 现在没有窗口上限,且非重复样本无法提前退出,训练侧每步的检测成本随响应长度线性上升(已给出实测数据)。
  • 离线报告由 NamedTemporaryFile + os.replace 发布,权限固定为 0600 且会静默改写既有报告的权限。

以上三项的证据、复现与建议都在对应行级线程中,此处不再重复。

已核实无问题

窗口覆盖完整(含未对齐末段、无重复窗口)、has_repetition 与 analyze_repetition 判定一致、阈值严格大于、covered_chars 并集计算正确、迁移后的聚合函数与旧实现逐行等价且指标名/分母保持、weights_only 默认安全且无静默回退到 pickle、CLI 在无 torch/numpy/ray 的解释器下可端到端跑通并保持原子发布、benchmarks/repetition/windows-python313.json 的 24 个用例判定可用本仓库检测器复现、中英文文档一致且链接可达。

P1 修复后(并确认 pytest tests/distributed/ray/test_weight_sync_port_hardening.py 通过)即可再次请求审查;P2/P3 可在本 PR 或后续跟进。

Powered by Nyanpasu with deepseek-v4.1-flash-ali medium, please check the suggestions carefully.

Comment thread relax/utils/misc.py Outdated


def get_free_port(start_port=10000, consecutive=1, max_port=None):
from relax.utils.http_utils import is_port_available

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 优先级:P1

把 is_port_available 的导入下沉到函数体内,会让 relax.utils.misc 不再具备该模块属性,直接打断既有测试 tests/distributed/ray/test_weight_sync_port_hardening.py(该文件第 125、137、150、155、166、183 行均为 patch.object(misc, "is_port_available", ...))。mock.patch.object 默认 create=False,目标属性不存在时在进入上下文时即抛错:

AttributeError: <module 'relax.utils.misc'> does not have the attribute 'is_port_available'

(已用等价的最小模块复现验证。)

该文件由 pytestmark = pytest.mark.skipif(not HAS_DEPS, ...) 保护,而 HAS_DEPS 取决于 relax.distributed.ray.rollout 能否导入——在 CI 训练镜像(ghcr.io/redai-studio/relaxrl:latest 配合 requirements.txt 中的 ray/sglang)下为真,因此不会被跳过。.github/workflows/unittest.yml:86 执行 pytest tests/,这会让必需检查变红。本 PR 描述写的是「完整测试套件未跑(缺少训练依赖)」,正是这类只在有 Ray/SGLang 环境里才暴露的问题。

还有两点需要注意:

  1. 只把模块级导入加回去、同时保留函数体内这一行,测试仍然会失败。函数体内的 from ... import 每次调用都会重新读取 relax.utils.http_utils,而 patch.object(misc, ...) 只修改 misc 的属性,get_free_port 依旧调用真实的端口探测,于是 assert port == 21000(第 139 行)之类的断言照样不成立。要让 patch 生效,函数体内必须改回读取全局名。
  2. 这处下沉的收益很有限:relax/utils/http_utils.py 只依赖 httpx 与 relax.utils.env,不引入 ray/torch;而 relax/utils/misc.py 仍在模块级 import torch,导入开销由 torch 主导。真正需要延迟的是同一改动里的 import ray(那处保留即可),is_port_available 这处可以还原。

建议:恢复 relax/utils/misc.py 模块级的 from relax.utils.http_utils import is_port_available,删除 get_free_port 内的同名导入,保留 get_current_node_ip 内的 import ray;改完用 pytest tests/distributed/ray/test_weight_sync_port_hardening.py(需 ray/sglang 环境)确认恢复。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI 已在本 head 上确认该问题:GPU Unit Tests(961d092a)在训练镜像内执行 pytest tests/,结果为

6 failed, 2779 passed, 18 skipped, 99 warnings in 383.96s (0:06:23)

失败的 6 条正是 tests/distributed/ray/test_weight_sync_port_hardening.py::TestBoundedPortWindow 的全部用例,报错原文(对应上文列出的 6 个 patch.object 调用点):

E   AttributeError: <module 'relax.utils.misc' from '/workspace/Relax/relax/utils/misc.py'> does not have the attribute 'is_port_available'
  • 运行:https://github.com/redai-studio/Relax/actions/runs/35319444050 (job 105526169816,即必需的 "Unit Tests (H20, 4 GPUs)" 检查,退出码 1)
  • 该 run 的 head_sha 就是 961d092a;同一 head 上 Pre-commit Checks、Tests (Python 3.10/3.11/3.12) 与两个 GPU 集成用例均通过,所以这是当前唯一的红灯。

补充两点实测结论:

  1. 本 PR 自带的 tests/utils/test_repetition*.py 在同一个训练镜像里 84 条用例全部 PASSED,检测器、dump 读取与 CLI 部分没有问题,红灯完全来自这一处导入下沉。
  2. 因此上文建议的修法不变:还原 relax/utils/misc.py 模块级的 from relax.utils.http_utils import is_port_available、删除 get_free_port 内的同名导入,保留 get_current_node_ip 内的 import ray。

改完重跑该 workflow(或本地 pytest tests/distributed/ray/test_weight_sync_port_hardening.py)确认转绿后,即可再次请求审查。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 f2babd93 验证修复,F1 关闭。

  • relax/utils/misc.py:13 恢复模块级 from relax.utils.http_utils import is_port_available,get_free_port 改回读取全局名(import ray 的延迟保留),与建议一致;http_utils 只依赖 env/logging_utils,不存在循环导入。
  • 此前失败的必需检查 GPU Unit Tests 在新 head 上通过:2794 passed, 18 skipped, 0 failed(run 35323216487),tests/distributed/ray/test_weight_sync_port_hardening.py 全部用例转绿。
  • 新增的 tests/utils/test_misc_ports.py 用真实 socket 覆盖了模块属性导出与端口占用两条路径,同样通过。

log_dict |= _compute_prefix_cache_metrics(args, samples)
log_dict |= _compute_reward_cat_metrics(args, reward_category_samples)
log_dict |= compute_mopd_metrics(args, rewarded_samples)
log_dict["repetition_frac"] = np.mean([int(has_repetition(s.response)) for s in samples]).item()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 优先级:P2

这一行在每个 rollout step 对整批样本各调用一次 has_repetition(调用链:_log_rollout_data → compute_metrics_from_samples,relax/distributed/ray/rollout.py:4774,4813)。新实现没有窗口上限,窗口数随响应长度线性增长(relax/utils/repetition.py:107-124,10k 窗口 / 5k 步长),且每个窗口都做一次 level 9 压缩;旧实现对每个样本固定压缩一次末尾 10k 窗口。关键在于:非重复样本无法提前退出,所以训练中最常见的路径从常数成本变成了 O(响应长度)。

用本仓库真实检测器实测(非重复文本,纯 CPU,200 次均值):

响应长度 旧实现 新实现 倍数
10,000 字符 不检测 0.19 ms 新增覆盖
60,000 字符 0.19 ms 2.48 ms 12.8×
120,000 字符 0.18 ms 5.04 ms 28.4×

绝对量级不大(PR 自带的 benchmarks/repetition/ 也显示 1M 字符约 18 ms),通常远小于一个训练 step,所以我把它标为 P2 而非阻塞项。但 benchmarks/ 只刻画了独立检测器,没有覆盖训练侧「每步 × 全批」的聚合开销;对 agentic rollout 这类响应可达十万字符量级的场景,建议在 PR 描述里明确这个权衡。

如果希望保留布尔路径的固定成本,可以给 has_repetition 单独设窗口数上限、或复用已扫描窗口的缓存;如果认为当前成本可以接受,也请把「指标路径不做窗口上限」这一点写进 PR 描述或 docs/*/guide/repetition-diagnostics.md,避免后续被当成回归。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已按建议在文档中显式说明该权衡,F2 关闭。

docs/en/guide/repetition-diagnostics.md 与 docs/zh/guide/repetition-diagnostics.md 的「快速开始」新增段落,说明训练指标同样使用无窗口上限的全文布尔扫描、非重复样本需压缩每个窗口、CPU 成本随批次内响应字符总数线性增长,并明确检测器基准不覆盖整批聚合与训练吞吐。

实现按 PR 原设计保留完整窗口覆盖,这一点现在对使用者是显式可见的,不再需要后续以「无上限」为由重新讨论。

Comment on lines +92 to +95
with tempfile.NamedTemporaryFile(
mode="w", encoding="utf-8", dir=output.parent, prefix=f".{output.name}.", suffix=".tmp", delete=False
) as stream:
temporary = Path(stream.name)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 优先级:P3

tempfile.NamedTemporaryFile 以固定的 0600 创建临时文件(不受 umask 影响),第 143 行的 os.replace 会把这个权限原样发布给最终报告。于是:

  • 在 umask 022 下生成的报告是 0600,与仓库其它 writer 的行为不一致(例如 relax/utils/training/train_dump_utils.py:233 的 open(path, "w") 遵循 umask);
  • 更值得注意的是,若已有报告是 0644,重跑一次会被静默改成 0600,作者不会收到任何提示。

本机复现(umask 022):

$ python -m relax.entrypoints.repetition tests/fixtures/repetition/middle_repetition.jsonl --output /tmp/perm.json
$ stat -c '%a' /tmp/perm.json   # 600
$ chmod 644 /tmp/perm.json
$ python -m relax.entrypoints.repetition tests/fixtures/repetition/middle_repetition.jsonl --output /tmp/perm.json
$ stat -c '%a' /tmp/perm.json   # 600(被静默改回)

该 CLI 的典型用法是把报告写进共享的 rollout 结果目录,其他账号或后续采集进程可能因此读不到报告。建议在 os.replace 之前显式设定权限,例如按 umask 计算、或在目标已存在时沿用其原有 mode:

current_umask = os.umask(0)
os.umask(current_umask)
os.chmod(temporary, 0o666 & ~current_umask)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 f2babd93 验证修复,F3 关闭。

relax/entrypoints/repetition.py:143-144 在 os.replace 前按既有报告的 st_mode & 0o777 设定临时文件权限。原始复现场景(umask 022):

$ python -m relax.entrypoints.repetition tests/fixtures/repetition/middle_repetition.jsonl --output /tmp/perm2.json
$ stat -c '%a' /tmp/perm2.json   # 600(新报告私有,文档已说明)
$ chmod 644 /tmp/perm2.json && python -m relax.entrypoints.repetition ... --output /tmp/perm2.json
$ stat -c '%a' /tmp/perm2.json   # 644(保留,不再是此前的静默降级)
  • 新增 tests/utils/test_repetition_report_permissions.py 覆盖 0600/0640/0644/0664 的保留、新报告私有、失败时原文件与权限不变且无残留临时文件;本地与 CI 均通过。
  • 中英文文档同步说明了新报告默认 0600、替换时保留读/写/执行位、以及所有者、ACL 与扩展属性不复制。

# 🐛 Bug Fix

- Restore the module-level is_port_available export used by get_free_port and weight-sync callers; retain the lazy Ray import.
- Preserve existing POSIX report permission bits during atomic replacement and document private new-report defaults.

# ✅ Tests

- Add CPU port tests using real sockets and POSIX report permission regression tests.
- Windows targeted validation: 86 passed, 30 skipped (24 training-environment tests and 6 POSIX cases).
- All-file pre-commit compatibility checks pass with Python 3.12 for docformatter.

# 📝 Documentation

- Explain per-batch linear CPU cost of full-response training metrics and retain complete window coverage.

@rai-studio-bot rai-studio-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查结论:通过

已针对 f2babd93 复审。上一轮的 3 项发现均已修复并经证据核验,本轮未发现新的阻塞或非阻塞问题。

  • F1(P1,已关闭):relax/utils/misc.py:13 恢复 is_port_available 的模块级导入、get_free_port 改回读取全局名,import ray 仍保持延迟;此前失败的 GPU Unit Tests 在新 head 上由 6 failed, 2779 passed 转为 2794 passed, 18 skipped, 0 failed,tests/distributed/ray/test_weight_sync_port_hardening.py 全部转绿。
  • F2(P2,已关闭):中英文文档说明了训练指标全文扫描的成本随批次字符数线性增长、检测器基准不覆盖整批聚合;实现按原设计保留完整窗口覆盖。
  • F3(P3,已关闭):relax/entrypoints/repetition.py:143-144 在 os.replace 前按既有报告的权限位设定临时文件;本地复现确认既有 0644 报告重跑后保持 644,新增回归测试覆盖多种权限与失败路径。

各发现的详细证据与复现命令已在对应行级线程中,此处不再重复;新 head 上 7 项检查全部通过。

Powered by Nyanpasu with deepseek-v4.1-flash-ali medium, please check the suggestions carefully.

@SigureMo

Copy link
Copy Markdown
Member

第六题已由 #329 完成,本 PR close,感谢参与~

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants