Skip to content

fix(langselector): install missing language packages - #1280

Merged
mhduiy merged 1 commit into
linuxdeepin:masterfrom
mhduiy:fix/langselector-install-language-packages
Oct 16, 2026
Merged

mhduiy merged 1 commit into
linuxdeepin:masterfrom
mhduiy:fix/langselector-install-language-packages

Conversation

@mhduiy

@mhduiy mhduiy commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

问题

PMS: BUG-379697、BUG-379687、BUG-379685

控制中心切换系统语言后,locale 正常生成、界面语言正常切换,但本应自动安装的语言支持包(字体、拼写词典、应用语言包等)从不安装,org.deepin.dde.Lastore1 收不到 InstallPackage;开始时的通知也不会出现"安装"那一条。另外 GetLanguageSupportPackages 查询很慢,可能超过调用方超时。

原因

  1. 安装判断写反:getInstallLangSupportPackages() 调用 ByLocale(locale, false),false 表示只返回尚未安装的包,但 setLocale() 却写成 isInstalled := len(pkg) != 0,于是有包要装时被当成"已安装"而跳过,没有缺包时才进入安装流程(装一个空列表)。
  2. 联网判断永远为"未联网":isNetworkEnable() 读取会话总线 org.deepin.dde.Network1 的 State,而 dde-network-core 2.0 中该对象是兼容层,State 固定返回 0,因此即使已联网也判定为断网,进一步挡住安装。
  3. 查询逐包起进程:dde-api 的 language_support 对每个候选包各启动一次 dpkg-query / apt-cache policy;apt-cache 每次启动都要加载整个包缓存并计算 pin,候选包有几十个时耗时累积到数十秒。

修改

langselector1/locale.go:

  • 改用 needsInstall := len(pkg) != 0,有缺包且联网才安装。
  • isNetworkEnable() 改为读取系统总线上 org.freedesktop.NetworkManager 的 State 属性(经 go-dbus-factory 的 system 绑定),不再依赖会话 org.deepin.dde.Network1 兼容层。
  • 联网条件收紧为 NM_STATE_CONNECTED_GLOBAL (70):仅本地/站点连通无法访问软件源。
  • 没有缺包时也发送"正在切换语言"通知;installLangSupportPackages() 复用 getInstallLangSupportPackages(),避免重复查询。

查询侧的批量改造在 dde-api:linuxdeepin/dde-api#205。

验证

  • go test ./langselector1 通过。
  • 隔离验证 setLocale() 的四种组合(联网/断网 × 有缺包/无缺包):只有"联网且缺包"会发起安装,断网仍能正常完成语言切换。
  • 实测 NetworkManager State 返回 70,修改后的读取函数判定为已连接;0~60 判定为未连通。
  • 真实环境下切换语言功能由提交者验证通过(控制中心切换 + 自动补装包)。

dde-api 的批量查询修复(linuxdeepin/dde-api#205)需一并合入,否则查询仍会逐包起进程;打包时 dde-daemon 依赖已安装的 dde-api-dev 源码,需要先更新 dde-api。

@sourcery-ai

sourcery-ai Bot commented Oct 15, 2026

Copy link
Copy Markdown

Reviewer's Guide

Fixes language support installation by correctly detecting missing packages, using system-bus NetworkManager global connectivity, and preserving start notifications for all language switches; the separate dde-api batch-query change must also be integrated to address slow package discovery.

Sequence diagram for language switching and support package installation

sequenceDiagram
    participant User
    participant LangSelector
    participant LanguageSupport
    participant NetworkManager
    participant Lastore
    participant Notifications

    User->>LangSelector: setLocale(locale)
    LangSelector->>LanguageSupport: ByLocale(locale, false)
    LanguageSupport-->>LangSelector: missing packages
    LangSelector->>NetworkManager: PropState().Get(0)
    NetworkManager-->>LangSelector: State
    LangSelector->>Notifications: sendNotify(start notification)
    LangSelector->>LangSelector: generate locale
    alt missing packages and State == 70
        LangSelector->>LanguageSupport: ByLocale(locale, false)
        LanguageSupport-->>LangSelector: packages to install
        LangSelector->>Lastore: InstallPackage(packages)
    end
Loading

File-Level Changes

Change Details Files
Correct language-support package installation decisions and notifications.
  • Treat nonempty missing-package results as requiring installation.
  • Install only when NetworkManager reports CONNECTED_GLOBAL.
  • Always send the language-switch start notification, selecting the install variant only when applicable.
  • Reuse the package lookup during installation to avoid duplicate lookup logic.
langselector1/locale.go
Switch connectivity detection to the authoritative system-wide NetworkManager API.
  • Replace the session-bus dde-network compatibility object with the system-bus NetworkManager manager binding.
  • Require state 70 rather than accepting partial local or site connectivity.
langselector1/locale.go
Document the language-selector fixes in the unreleased changelog.
  • Record package installation, connectivity gating, and notification behavior changes.
CHANGELOG.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@mhduiy
mhduiy force-pushed the fix/langselector-install-language-packages branch from fda232b to 951afa5 Compare October 15, 2026 12:25
1. A nonempty ByLocale result means packages are missing, so the inverted
   check treated them as already installed and nothing was ever installed;
   decide with needsInstall instead.
2. Read State from NetworkManager on the system bus instead of the session
   org.deepin.dde.Network1, whose dde-network-core 2.0 compatibility stub
   always returns 0 and made the daemon believe it was offline.
3. Require NM_STATE_CONNECTED_GLOBAL (70) before installing, because local
   or site connectivity alone cannot reach the package mirrors.
4. Send the start notification even when no package needs installation.
5. Reuse getInstallLangSupportPackages in the install path, removing the
   duplicated query code.

Log: Switching the system language installs the missing language packages again.
Influence: Language packages are installed when the internet is reachable.

fix(langselector): 联网时安装缺失的语言支持包

1. ByLocale 返回非空即表示有包尚未安装,原判断写反导致被当成已安装,从不安装;改用 needsInstall 判断。
2. 改为读取系统总线上 NetworkManager 的 State,不再依赖 dde-network-core 2.0 中固定返回 0 的会话 org.deepin.dde.Network1 兼容层,该兼容层使程序始终认为断网。
3. 只有 NM_STATE_CONNECTED_GLOBAL(70)才安装,仅本地或站点连通无法访问软件源。
4. 没有缺包时也发送开始通知。
5. 安装流程复用 getInstallLangSupportPackages,去掉重复的查询代码。

Log: 切换系统语言后能够正常安装缺失的语言支持包。
PMS: BUG-379697
PMS: BUG-379687
PMS: BUG-379685
Influence: 联网可达时自动补装语言支持包。
@mhduiy
mhduiy force-pushed the fix/langselector-install-language-packages branch from 951afa5 to eaf0878 Compare October 15, 2026 12:26

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="langselector1/locale.go" line_range="464" />
<code_context>
-		} else {
-			sendNotify(localeIconStart, "", notifyTxtStart)
-		}
+	if needsInstall && networkEnabled {
+		sendNotify(localeIconStart, "", notifyTxtStartWithInstall)
+	} else {
</code_context>
<issue_to_address>
**Failed installs appear successful**

When lastore rejects the install request or reports a failed package job, `installLangSupportPackages` returns an error, but `setLocale` only logs it before sending the finished notification and marking the locale change complete. Users see success while the support packages remain absent.

Propagate the installation error so `setLocale` does not report the locale change as successfully completed.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread langselector1/locale.go
@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

🤖 AI 代码审查报告

总体评分: 75 分 (通过阈值: 70分)

Pass


📊 总体评价

项目 结果
审查结论 代码审查通过
评分详情 总体评分 75 分,大于 70 分通过阈值。代码修复了语言包安装的逻辑缺陷,但存在资源泄漏问题需要关注。

🔍 详细分析

1. 语法逻辑 ❌

评价: 需改进 ❌ 不通过

潜在问题:

  1. langselector1/locale.go:588 - getInstallLangSupportPackages() 函数中移除了 ls.Destroy() 调用,导致 LanguageSupport 对象资源泄漏。该函数仍创建 ls 对象但不再销毁,每次调用都会泄漏资源

建议: 在 getInstallLangSupportPackages() 函数中使用 defer ls.Destroy() 确保资源释放,或恢复原来的显式 Destroy 调用


2. 代码质量 ✅

评价: 优秀 ✅ 通过

潜在问题:
✅ 未发现明显问题

建议: 代码质量良好,变量命名从 isInstalled 改为 needsInstall 更准确,installLangSupportPackages 复用 getInstallLangSupportPackages 消除了重复代码


3. 代码性能 ✅

评价: 优秀 ✅ 通过

潜在问题:
✅ 未发现明显问题

建议: 性能良好,使用 SystemBus 访问 NetworkManager 更合理,网络状态检查 state==70(NM_STATE_CONNECTED_GLOBAL) 更精确


4. 代码安全 🔒

评价: 优秀 ✅ 通过

🔐 发现 0 个安全漏洞

安全漏洞详情:
✅ 未发现安全漏洞

建议: 无安全漏洞,代码安全合规


💡 改进建议代码示例

func (lang *LangSelector) getInstallLangSupportPackages(locale string) ([]string, error) {
    ls, err := language_support.NewLanguageSupport()
    if err != nil {
        return nil, dbusutil.ToError(err)
    }
    defer ls.Destroy() // 确保资源释放
    return ls.ByLocale(locale, false), nil
}

本报告由 AI 代码审查工具自动生成

@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: fly602, mhduiy

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@mhduiy

mhduiy commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

deepin pr auto review

🤖 AI 代码审查报告

总体评分: 75 分 (通过阈值: 70分)

Pass ### 📊 总体评价 项目 结果 **审查结论** 代码审查通过 **评分详情** 总体评分 75 分,大于 70 分通过阈值。代码修复了语言包安装的逻辑缺陷,但存在资源泄漏问题需要关注。 ### 🔍 详细分析 #### 1. 语法逻辑 ❌

评价: 需改进 ❌ 不通过

潜在问题:

1. `langselector1/locale.go:588` - getInstallLangSupportPackages() 函数中移除了 ls.Destroy() 调用,导致 LanguageSupport 对象资源泄漏。该函数仍创建 ls 对象但不再销毁,每次调用都会泄漏资源

建议: 在 getInstallLangSupportPackages() 函数中使用 defer ls.Destroy() 确保资源释放,或恢复原来的显式 Destroy 调用

2. 代码质量 ✅

评价: 优秀 ✅ 通过

潜在问题: ✅ 未发现明显问题

建议: 代码质量良好,变量命名从 isInstalled 改为 needsInstall 更准确,installLangSupportPackages 复用 getInstallLangSupportPackages 消除了重复代码

3. 代码性能 ✅

评价: 优秀 ✅ 通过

潜在问题: ✅ 未发现明显问题

建议: 性能良好,使用 SystemBus 访问 NetworkManager 更合理,网络状态检查 state==70(NM_STATE_CONNECTED_GLOBAL) 更精确

4. 代码安全 🔒

评价: 优秀 ✅ 通过

🔐 发现 0 个安全漏洞

安全漏洞详情: ✅ 未发现安全漏洞

建议: 无安全漏洞,代码安全合规

💡 改进建议代码示例

func (lang *LangSelector) getInstallLangSupportPackages(locale string) ([]string, error) {
    ls, err := language_support.NewLanguageSupport()
    if err != nil {
        return nil, dbusutil.ToError(err)
    }
    defer ls.Destroy() // 确保资源释放
    return ls.ByLocale(locale, false), nil
}

本报告由 AI 代码审查工具自动生成

Destroy 本身是空实现,调用也是空调用

@mhduiy
mhduiy merged commit 252b257 into linuxdeepin:master Oct 16, 2026
13 checks passed
KHAEntertainment added a commit to KHAEntertainment/showrunner that referenced this pull request Oct 16, 2026
…and Sourcery markers

B7: Gemini and Qodo have no verified producing check. Their apps grant
`checks: read` only and no published CheckRun or StatusContext was
found in the live search. The Gemini push-re-review default is also
UNVERIFIED here (Google's docs describe the initial review and a
manual `/gemini review` trigger, not an automatic new-push
re-review), so the only confirmed shape is comment-only.

Greptile and Sourcery checks ARE verified:
- Greptile: b-editor/beutl#3095 prints
  `{"name":"Greptile Review","app_slug":"greptile-apps"}`.
- Sourcery: linuxdeepin/dde-daemon#1280 prints
  `{"name":"Sourcery review","app_slug":"sourcery-ai"}`.

DEFAULT_CHECK_MARKERS now carries only verified producers. The Kilo
marker covers the historical `kilocode` and the current
`kilo-code-bot` app slugs (renamed 2025-10-28). Login-side identity
sets for Gemini and Qodo are unchanged: a future vendor that
publishes a check must add a marker with evidence, not borrow one.

Tests:
- `test_issue_82_new_check_markers_route_to_their_reviewer` now
  exercises only the verified Greptile and Sourcery markers.
- New `test_issue_82_gemini_and_qodo_check_markers_are_not_added`
  pins the absence: a check naming Gemini or Qodo does not
  auto-route.
KHAEntertainment added a commit to KHAEntertainment/showrunner that referenced this pull request Oct 19, 2026
…mary evidence (#119)

* [Issues #82 #117] widen external-reviewer identities and add Kilo summary evidence

Issues #82 and #117, one lane:

- inspect_external_reviews.py: add gemini-code-assist, greptile, sourcery,
  qodo to DEFAULT_IDENTITIES with verified DEFAULT_CHECK_MARKERS.
  github-actions[bot] stays out: shared by every workflow, not a stable
  identity. Kilo summary evidence path: a <!-- kilo-review --> PR issue
  comment from a trusted Kilo identity counts as head review evidence
  when the comment's createdAt is at or after the Kilo check's startedAt
  (chronological proxy for "after the head push") or the Kilo check
  has a completedAt (the safety valve, the PR #114 shape). "No Issues
  Found" puts Kilo in completed_on_head; any other status surfaces the
  summary as a current-head finding for the existing disposition path.
  A failed Kilo check whose description names the rate-limit phrase
  gets a distinct pending reason (not the generic 'trusted review check
  failed' error) so the #48 rate-limit breakpoint can count it across
  all built-in reviewers.

- 4 new fixtures under tests/fixtures/external_reviews/: kilo-summary-
  clear-at-head, kilo-summary-stale, kilo-summary-with-issues,
  kilo-rate-limited-failed-check.

- tests/test_external_review_inspector.py: 14 new tests pin each new
  login/marker, the kilo-review ordering rule, the four fixture cases,
  the github-actions[bot] exclusion, the ask-once surface, and the #48
  rate-limit breakpoint against a newly added reviewer (greptile).

- phases/external-review.md: list the new reviewers with comment-only /
  re-review notes; document the Kilo summary evidence path; add the
  ask-once rule and forbid re-asking a recorded login.

- PROJECT_CONTEXT_TEMPLATE.md: add Pending reviewer decisions line.

- PROJECT_CONTEXT.md External Review Policy only: kilo required,
  coderabbit ignored, Kilo trigger rules (PR opened, new commits
  pushed, PR reopened, draft marked ready) replace the coderabbit-only
  auto-request paragraph; closing sentence updated.

- CHANGELOG.custom.md: bullet under Unreleased (v2.1.2) noting the
  behavior change, the ask-once contract, the Kilo summary evidence
  path, and the new fixtures.

Refs: #82, #117, #48

* [#82] remove unsupported Gemini and Qodo check markers; add Greptile and Sourcery markers

B7: Gemini and Qodo have no verified producing check. Their apps grant
`checks: read` only and no published CheckRun or StatusContext was
found in the live search. The Gemini push-re-review default is also
UNVERIFIED here (Google's docs describe the initial review and a
manual `/gemini review` trigger, not an automatic new-push
re-review), so the only confirmed shape is comment-only.

Greptile and Sourcery checks ARE verified:
- Greptile: b-editor/beutl#3095 prints
  `{"name":"Greptile Review","app_slug":"greptile-apps"}`.
- Sourcery: linuxdeepin/dde-daemon#1280 prints
  `{"name":"Sourcery review","app_slug":"sourcery-ai"}`.

DEFAULT_CHECK_MARKERS now carries only verified producers. The Kilo
marker covers the historical `kilocode` and the current
`kilo-code-bot` app slugs (renamed 2025-10-28). Login-side identity
sets for Gemini and Qodo are unchanged: a future vendor that
publishes a check must add a marker with evidence, not borrow one.

Tests:
- `test_issue_82_new_check_markers_route_to_their_reviewer` now
  exercises only the verified Greptile and Sourcery markers.
- New `test_issue_82_gemini_and_qodo_check_markers_are_not_added`
  pins the absence: a check naming Gemini or Qodo does not
  auto-route.

* [#117] rebuild rate-limit fixture from real failed Kilo run; fetch check output via REST

B4: the rate-limit phrase lives in the head SHA's check run
`output.summary`, but `gh pr view --json statusCheckRollup` returns
a CheckRun shape that omits both `output` and the run id. The
previous fixture invented a `description` field and the
"Will retry on the next push" suffix; live data had neither.

- New `fetch_check_output` calls
  `repos/{owner}/{repo}/commits/{head_sha}/check-runs` for the
  named check and returns the most recent run's `output`
  (plus the canonical timing/app fields the rollup sometimes
  omits).
- `augment_checks_with_output` is called from `main` after the
  live `fetch_pr`. It only fills a check that lacks both
  `output` and `description`, so fixtures that deliberately
  model an absent output stay authoritative.
- `check_description` now reads `output.summary`,
  `output.title`, and `output.text`, in that order, plus
  the legacy `description`.
- Fixture `kilo-rate-limited-failed-check.json` is rebuilt
  from the real failed run on PR #114 (check run
  115580659602, head 91e1cb4): timing 22:48:19Z–22:59:16Z,
  app slug `kilo-code-bot` (not the historical `kilocode`),
  and `output: { title: "Kilo Code Review failed",
  summary: "Review failed: Assistant request was rate
  limited" }`. The invented "description" and the "Will
  retry on the next push" suffix are gone.

The inspector now reads the rate-limit phrase from a real
field, and a future vendor that names its failure mode
differently in `output.title` is caught too.

* [#117] bind Kilo summary to head's successful run; enforce Status+Recommendation; route summary findings through disposition

B1: the previous ordering rule admitted any non-empty
`completedAt`, so a year-old completed Kilo run cleared
against a fresh head. The new rule is:

  1. The Kilo check for the head SHA COMPLETED with
     `conclusion == "success"` (case-folded). A failed,
     still-running, or non-success run is not a head review.
  2. The summary comment's `updatedAt` (or `createdAt` when
     the comment was never edited) parses as ISO 8601 AND is
     at or after the run's `startedAt`. A malformed date
     fails closed rather than silently satisfying the
     compare.

The `parse_iso8601` helper gates the timestamp; a date that
fails to parse becomes `None` and the comment is treated as
not bound to this run. The B1 negative test exercises a
year-old summary paired with a newly-completed SUCCESS run;
the inspector stays `pending` and Kilo is not in
`completed_on_head`. The malformed-timestamp probe
(`createdAt = "zzzz"`) likewise stays `pending`.

B2: the previous implementation parsed only the `Status`
field. `kilo_summary_fields` now returns
`(status, recommendation)`, and the clear rule requires
BOTH `Status: No Issues Found` AND `Recommendation: Merge`.
Issue #117's scope comment is explicit on this. A `Status: No
Issues Found | Recommendation: Request Changes` summary is
surfaced as a current-head finding the lead triages.

B3: the summary helper now accepts the disposition map and
sets `disposition` on the finding it returns. A
`blocking` disposition routes the gate to `blocking`
exactly like a triaged review-thread finding; an
`advisory` or `false_positive` disposition releases the
seat the way other reviewers' findings do. The hard-coded
`None` is gone.

Kilo checks handled by the summary path are returned in a
new `consumed_check_ids` set, and the main `inspect()`
skips them in the generic `check_failed` path. Without
this, a failed Kilo check would add both a specific
rate-limit (or non-success) pending reason AND the generic
"trusted review check failed without readable current-head
findings" error, routing the gate to `incomplete` instead
of `pending`.

Tests added: `test_issue_117_kilo_summary_edited_in_place`
(PR #118 shape), `test_issue_117_old_summary_with_completed_head_check_does_not_clear`
(B1 negative), `test_issue_117_failed_check_does_not_clear_summary`
(all non-success conclusions), `test_issue_117_malformed_timestamp_does_not_clear`
(B1 timestamp probe), `test_issue_117_no_issues_found_with_request_changes_does_not_clear`
(B2), `test_issue_117_summary_finding_can_be_dispositioned_as_blocking`
(B3, all three dispositions).

* [#82 #48] rate-limited reviewers stay pending even when a review completed

B5: a current-head Greptile COMMENTED review plus a failed
rate-limited Greptile check was routing the gate to `clear`.
The review evidence was correctly recorded in
`completed_on_head`, but `pending_reviewers` did not include
the rate-limited reviewer, so the gate cleared. The
rate-limit is a separate signal that the lead's retry or
breakpoint path must see.

`pending_reviewers` now unions `rate_limited_reviewers` for
every reviewer, not just Kilo. A Greptile (or any built-in
reviewer) with a completed review AND a rate-limited check
ends up in both `completed_on_head` and
`pending_reviewers`, with the rate-limit reason in
`pending_reasons`. The state is `pending` (not `clear`)
because the rate-limit requires a lead decision.

Test `test_issue_82_b5_rate_limited_reviewer_with_completed_review_is_pending`
probes the Greptile case from the B5 review. `completed_on_head`
is still `["greptile"]` so the lead's audit trail sees
both signals; `pending_reviewers` is `["greptile"]` with
a rate-limit reason and zero errors.

* [#82 #117] M3: rate-limit only on FAILED checks; B1 doc; PR #118 fixture; app.slug fidelity

M3 (QA round 1): `is_rate_limited_check` returned True for any
check whose text payload mentioned a rate-limit, even SUCCESS
checks. A SUCCESS Kilo check whose description said
"previous attempt was rate limited; this run retried" was
misclassified as a rate-limit failure, discarding a legitimate
clear verdict (QA probe s8).

`is_rate_limited_check` now requires the check to be FAILED
(non-success conclusion, or status=failure/error) AND its
text payload to name the rate-limit phrase. A SUCCESS check
that merely *mentions* a past rate-limit is not a rate-limit
failure — vendor retry history is not actionable infrastructure
evidence.

A new `is_failed_check` helper carries the conclusion/status
check; the `KILO_RATE_LIMIT_PHRASE` clause is now structurally
redundant (a substring of `RATE_LIMIT_PHRASE`) and is
documented as such rather than removed, so the constants stay
load-bearing as named references in logs and tests.

B1 documentation: `external-review.md` previously documented
the rule as "the comment's `createdAt` is at or after the
Kilo check's `startedAt`". PR #118's real data shows that
`createdAt` is stale on every re-review (created 00:03:43Z
against a run that started 00:22:51Z), so the documented
branch would fail on the real #118 shape. The rule is now
documented as the comment's `updatedAt` (falling back to
`createdAt` when never edited) at or after the run's
`startedAt`, with both dates required to parse as ISO 8601
so a malformed value fails closed.

PR #118 fixture: `kilo-summary-edited-in-place.json`
captures the real shape: `createdAt` 00:03:43Z,
`updatedAt` 00:26:26Z, head run started 00:22:51Z completed
00:26:24Z, conclusion SUCCESS, comment No Issues Found +
Merge. The fixture clears via the edited-in-place path,
proving the B1 binding rule against the real re-review data.

App-slug fidelity: all four Kilo fixtures previously asserted
`app.slug = "kilocode"` (or `kilo-code-bot`) but live
`gh pr view --json statusCheckRollup` returns
`app: null` for Kilo checks. The fixtures now mirror the
real shape; the inspector matches on the check's `name`
("Kilo Code Review" contains the `kilo code` marker), so
the change is structural and the live data path is unchanged.

Tests:
- `test_issue_82_m3_success_check_mentioning_rate_limit_clears`
  pins the s8 fix.
- `test_issue_82_m3_failed_check_with_rate_limit_is_rate_limited`
  pins the positive direction.
- `test_issue_82_m3_unrelated_check_mentioning_rate_limit_does_not_route`
  pins the s7 case (a github-actions check with rate-limit
  text in output is not a rate-limit signal).
- `test_issue_117_kilo_summary_edited_in_place_fixture_clears`
  exercises the new PR #118 fixture end-to-end.

* [#82] B6 widened: pin Pending reviewer decisions in template and PROJECT_CONTEXT; CHANGELOG trim

QA round 1 (M1): the ask-once contract was pinned only in
`phases/external-review.md`. Mutations M4 (delete the line in
the template) and M5 (delete the line in the root
`PROJECT_CONTEXT.md`) passed both the validator and the full
unit suite. This commit widens the B6 pin to all three
recording sites:

  - `scripts/validate_skill.py`: new per-file policy for
    `templates/PROJECT_CONTEXT_TEMPLATE.md` (carries the
    `Pending reviewer decisions` line and the
    External Review Policy structural tokens).
  - `scripts/validate_skill.py`: a new check on the root
    `PROJECT_CONTEXT.md` that fails closed when the
    External Review Policy does not include
    `**Pending reviewer decisions**`. The check runs
    before the routing-section check so a missing line
    surfaces as a distinct error.

QA round 1 (M1 widening): the mutation test
`test_ask_once_mutation_each_outcome_fails_validation`
performs seven mutations, each in a scratch copy of the
worktree so the mutation stays out of the live tree:

  1. delete the entire `### Ask once` section;
  2. delete the trust outcome line;
  3. delete the ignore outcome line;
  4. delete the decide-later outcome line;
  5. delete the not-re-asked paragraph;
  6. delete the `Pending reviewer decisions` line in
     the template;
  7. delete the `Pending reviewer decisions` line in
     `PROJECT_CONTEXT.md`.

All seven must fail the validator. A negative control
(additive unrelated change) must NOT fail, proving the
assertions are scoped to the ask-once contract rather than
to the file in general. The test now copies the validator
script into the scratch dir so the validator's
`repo_root` is the scratch root and the
`PROJECT_CONTEXT.md` check is exercised against the
mutated copy.

CHANGELOG: the `Unreleased (v2.1.2)` Added section is
trimmed to ~25 lines (was ~75) and the Changed section
corrects the prior overclaim that the template carries
the new policy values — the template keeps its generic
defaults and gains only the new `Pending reviewer
decisions` line.

* [#82 #117] N1: GET method for REST fetches; N3: fix docstring; A1: dead code; QA-N1: latest Kilo run + consume all; A3: datetime parse; B3: summary IS completion

Round 3 blocking items. Six commits worth of changes, one per
item group, bundled here where the changes are tightly coupled.

N1: `fetch_check_output` and the new batch
`fetch_check_outputs_for_head` now use `--method GET` with
`check_name` and `filter=all` encoded into the URL's query
string. `gh api` defaults to POST whenever request parameters
are supplied (via `-f`/`-F`), and a POST to the check-runs
endpoint would silently create or mutate a check run on the
head SHA. A transport-contract test asserts the exact argv:
`--method GET` is present, `-f`/`-F` are absent, and the
URL carries `check_name=...&filter=all`. Both the single-check
and batch paths are tested.

N1 (error propagation): a failed REST call no longer silently
falls through to the rollup's `description` (which is `null`
on live Kilo checks). Instead, the caller keeps the check
not-clear with a named reason; the inspector's `check_failed`
path reports "trusted review check failed without readable
current-head findings" for the affected reviewer. This matches
the lead's instruction to not swallow the error silently.

QA-N1 (latest Kilo run): `kilo_summary_evidence_for_head` now
gathers ALL Kilo runs on the head SHA, sorts by `started_at`
DESC with `id` DESC as a deterministic tie-break, and picks
the LATEST. All Kilo runs are marked as consumed so an older
failed run does not fall through to the generic `check_failed`
path and produce a self-contradictory `incomplete`. The REST
listing now passes `filter=all` (the default `filter=latest`
hides older runs). Three new tests cover both orders
(SUCCESS-then-FAILED, FAILED-then-SUCCESS) and the
consume-all invariant.

B3 (summary IS review completion): a bound successful Kilo
summary now counts in `completed_on_head` REGARDLESS of what
findings it carries. Findings gate via the normal disposition
path: blocking -> `blocking`; advisory or false_positive on
every finding -> `clear` when nothing else is open; untriaged
-> `pending`. The `kilo-summary-with-issues` and
`no_issues_found_with_request_changes` tests are updated to
the new verdicts (they previously pinned the wrong outcome).

A3 (timestamp parsing): `parse_iso8601` now delegates to
`datetime.fromisoformat` with a trailing-`Z` normalization
and returns a UTC `datetime` (not a string). Impossible
calendar values like `2026-99-99T99:99:99Z` and the original
`zzzz` case are rejected. Ordering comparisons are now
instant comparisons, not lexical string compares.

A1 (dead code): `KILO_RATE_LIMIT_PHRASE` and
`kilo_summary_status` have no runtime or test readers and
are deleted. The `is_rate_limited_check` docstring is
updated to drop the now-nonexistent constant reference.

N3 (docstring): the `check_description` docstring referenced
`fetch_pr_with_check_output`, which does not exist. The
actual name is `fetch_check_output`. Fixed.

A1 (N+1 REST calls): `augment_checks_with_output` now
fetches the head's check runs ONCE per PR (filter=all) via
`fetch_check_outputs_for_head`, not once per rollup entry.
A REST failure yields an empty mapping so the caller falls
back to the rollup's `description` field.

* [#117] N2: fetch updatedAt over REST for Kilo summary binding

The Kilo summary path consumes `updatedAt` to bind an
edited-in-place summary to the head's run (PR #118: created
00:03:43Z, updated 00:26:26Z, run started 00:22:51Z). The
`gh pr view --json comments` shape returns `createdAt` but
NOT `updatedAt`, so a real edited-in-place summary (the shape
Kilo produces on every re-review) could not be bound and the
inspector reported `pending` where it should have reported
`clear`. Codex round 2 N2 confirmed this is a P1 integration
regression.

New `fetch_pr_comments_with_metadata` fetches the REST
`GET repos/{owner}/{repo}/issues/{pr}/comments` endpoint
(--paginate) and returns a mapping keyed by both the comment's
`id` and its `node_id`, so the caller can match a
`gh pr view` comment by either identifier. The transport uses
`--method GET` with the URL-encoded path (no `-f`/`-F`, so
`gh api` does not switch to POST).

New `augment_comments_with_metadata` merges REST
`created_at`/`updated_at` into the gh-shaped comments in
place, with a `_restMeta` provenance block that records the
source URL and the values. REST values take precedence; a
missing REST value falls back to the gh `createdAt`. On REST
failure the gh values remain (which fail closed for the edited
case, matching the B1 invariant).

Wired into the live path in `main()` after
`augment_checks_with_output`.

Three new tests:
- `test_issue_82_n2_fetch_path_supplies_updated_at` asserts
  the fetch path returns both `createdAt` and `updatedAt`
  keyed by id and node_id, and that the transport uses
  `--method GET` with `--paginate` against the
  `issues/118/comments` endpoint.
- `test_issue_82_n2_augment_merges_metadata_into_comments`
  asserts the merge and the `_restMeta` provenance block.
- `test_issue_82_n2_fallback_when_rest_fails` asserts that a
  REST failure leaves the gh values intact and no provenance
  block is written.

Live proof (quoted in the report-back):
- `--repo KHAEntertainment/showrunner --pr 118
  --required-reviewer kilo --ignored-reviewer coderabbit`
  returns `state: clear`, `completed_on_head: ["kilo"]`.
- Same command with `--pr 114` returns `state: clear`,
  `completed_on_head: ["kilo"]`.

* [#82 #117] Round 3 coverage pins + A2 rewrite (no suite recursion)

Incident note: the first round-3 attempt at coverage-pin tests
shipped an in-suite mutation harness that invoked
`python -m unittest discover` from inside the test suite. Each
child re-discovered the same file and spawned more, putting 200+
Python processes on a shared host. That harness is DROPPED.

This commit re-ships the same coverage guarantees with a
strictly bounded shape:

- Coverage pins (QA M6, M7, M8) are pure TEXT ASSERTIONS. No
  subprocess, no discovery, no side effects. They read the
  shipped file and assert tokens:
  - M6: the four new Known-identities bullets in
    `phases/external-review.md` (Gemini, Greptile, Sourcery,
    Qodo) plus the `github-actions[bot]` non-addition;
  - M8: the root `PROJECT_CONTEXT.md` External Review Policy
    values (`Required reviewers: kilo`,
    `Ignored reviewers: coderabbit`);
  - M7: the `CHANGELOG.custom.md` `Unreleased (v2.1.2)`
    entry tokens (four new reviewers, Ask-once contract,
    Kilo summary evidence, kilo required, coderabbit ignored).

- The validator's own pin for the root
  `PROJECT_CONTEXT.md` `Pending reviewer decisions` line
  is tightened to require the full `**: none` form (a
  bare `**Pending reviewer decisions**` substring was
  previously enough, so a mutation that rewrote `: none`
  to `: removed for QA` passed). The stricter check fails
  closed on any edit that drops or rewords the line.

- A2 (the B6 mutation test) is REWRITTEN:
  - each of the 7 cases starts from a PRISTINE copy of the
    Skill in a fresh `tempfile.TemporaryDirectory` (no
    cumulative state across mutations);
  - each mutation is a REAL line/section DELETION, not a
    substring replacement that leaves the validator's key
    intact;
  - the additive-control probe is applied to
    `CHANGELOG.custom.md` (which has no routing-section
    check) rather than to the mutated target, so the
    control cannot accidentally trip the validator's
    `PROJECT_CONTEXT.md` `## Execution Routing Policy`
    whole-section-equality check;
  - the test ONLY invokes `scripts/validate_skill.py` on
    the temp copy (the one mutation-proof mechanism the
    rules permit inside the suite). It never runs the test
    suite (`unittest discover`, `verify.sh`, `pytest`),
    so there is no recursion and no process fan-out.

Outside-the-suite mutation proof (validator + single-module
`tests.test_backend_contract`) runs ONE mutation at a time
from a scratch clone, per the lead's hard rules.

* [#117] rate-limit fixture: add run id 115580659602 + filter=all provenance

The lead verified with `gh api .../check-runs?filter=all` that
run 115580659602 on commit 91e1cb4
is a real Kilo failure (22:48:19-22:59:16Z,
`output.summary: "Review failed: Assistant request was rate
limited"`). The default `filter=latest` hides this older
run; the LATEST run on that SHA is the success 115591827668
(23:20:05-23:25:48).

The fixture now carries a `_provenance` block with:
- `run_id: 115580659602`
- `filter: "all"`
- `fetch_command`: the exact gh api invocation
- a note explaining the filter=all requirement and the
  second (success) run on the same SHA

The fixture's shape is unchanged; the inspector still reads
`output.summary` and produces the same `pending` +
distinct rate-limit reason. The provenance is metadata
only.

* [#82 #117] make CHANGELOG check tolerant of absence (fixture trees)

The round-3 gate run failed on 22 tests in
`tests/test_validate_skill.py` (`ERROR: missing required
file: CHANGELOG.custom.md`). The fixture tree those tests
build copies `skills/dev`, `scripts/validate_skill.py`,
and `PROJECT_CONTEXT.md` but NOT `CHANGELOG.custom.md`,
so the new M7 coverage pin's "missing required file"
check rejected every fixture run.

Fix: the CHANGELOG check is now CONDITIONAL on the file
existing, matching the same tolerance pattern the validator
already uses for `EXTRA_MARKDOWN_DIRS` (`en/`, `zh/`)
and for the root `*.md` home-path scan. A fixture or
archival tree that does not carry the CHANGELOG is not a
defect; the check applies only when the file is present,
which is the shape a real source tree has.

The M7 mutation (deleting the `## Unreleased (v2.1.2)`
section from the CHANGELOG) is still caught, because the
file remains present with the section missing. The check
for `**Pending reviewer decisions**: none` in
`PROJECT_CONTEXT.md` is unchanged and still required,
because the fixture DOES copy that file.

Verified after the fix (single-module runs only, no
discover):
- `python3 scripts/validate_skill.py` -> 0
- `python3 -m unittest tests.test_validate_skill` -> 27 OK
- `python3 -m unittest tests.test_backend_contract` -> 102 OK
- `python3 -m unittest tests.test_external_review_inspector` -> 100 OK

* [#82 #117] Bind summary and check-run evidence to one record; release-aware changelog pin

F1 (Codex round-3, blocking/Critical): REST edit metadata
promoted an old summary body to current-head evidence. The
first gh read supplied the comment body; the later REST read
supplied newer edit timestamps but DISCARDED the REST body.
If Kilo edited the comment in place between the two reads,
the inspector bound the OLD body to the NEW edit time — the
exact in-place edit behavior that motivated N2.

Fix: `fetch_pr_comments_with_metadata` now returns the REST
record's `body` alongside `created_at`/`updated_at`.
`augment_comments_with_metadata` replaces the comment's
body, createdAt, and updatedAt ALL from the same REST record
— one consistent comment version. If the REST record is
incomplete (missing body or missing timestamps), the comment
is marked `_incoherent: True` and the inspector treats it
as not evidence (pending), never clear. A fresh timestamp
must never license stale text.

F2 (Codex round-3, blocking): field-filling mixed the REST
latest run's id/output with the older gh SUCCESS conclusion
and times. If a new run appeared between the gh read and
the REST read, the merged record claimed the newer run's ID
and output but the older run's SUCCESS conclusion and
start/completion times.

Fix: `augment_checks_with_output` now uses the selected
latest REST run as ONE WHOLE record — id, status, conclusion,
started/completed, and output all from that run. No
field-by-field merging with gh rollup data for the same
check. The gh rollup and REST disagree only when a new run
was created between reads; the REST record is the one that
describes the head SHA's latest run and wins outright.

D1 (debt, per round-3): the M7 pin required the literal
`## Unreleased (v2.1.2)`, so a release rename to
`## v2.1.2 (YYYY-MM-DD)` broke both the validator and the
backend coverage test. Both now accept either heading form
(regex: `## (?:Unreleased \(v2\.1\.2\)|v2\.1\.2(?
: \(\d{4}-\d{2}-\d{2}\))?)`). The same 8 #82/#117
tokens are still required inside whichever heading is
present. Verified by hand in a scratch copy: rename to
`## v2.1.2 (2026-10-19)` → validator 0, tests 0; delete
the section → validator 1, tests 1.

Two in-process regression tests (no subprocess, no suite
launch):
- `test_f1_rest_edit_metadata_promotes_coherent_body_only`
  reproduces the gh-read-then-REST-read race and asserts the
  verdict is `pending` + 1 untriaged (the REST body is the
  Issues Found one), not `clear`.
- `test_f1_incomplete_rest_record_marks_comment_not_evidence`
  covers the unpairable case (REST record with `body: null`)
  and asserts the comment is marked incoherent and the
  inspector keeps the gate `pending`.
- `test_f2_rest_run_used_as_one_whole_record` reproduces
  the gh-SUCCESS-then-REST-FAILURE race and asserts the
  merged record is the REST record entirely (id 300,
  conclusion failure, started 23:30:00, completed 23:35:00,
  output summary with rate-limit) and the verdict is
  `pending` with the rate-limit reason, not `clear`.

Existing `test_issue_82_n2_augment_merges_metadata_into_comments`
updated to include `body` in its REST record (F1 pairing
requirement).

Codex round-3 repro probes (before → after):
- F1 `REST has new issues body, production enrichment keeps
  old clear body`: clear → pending (1 untriaged)
- F2 `new failure output/id mixed with older SUCCESS fields`:
  clear → pending (rate-limit reason)
- Coherent controls unchanged: year-old/absent summary stays
  pending; current summary clears.

Light local checks (one at a time, nice -n 10):
- `tests.test_external_review_inspector`: 103 OK
- `tests.test_backend_contract`: 102 OK
- `tests.test_validate_skill`: 27 OK
- `python3 scripts/validate_skill.py`: OK

Live: `--pr 118` and `--pr 114` both `state: clear`,
`completed_on_head: ["kilo"]`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants