Procházet zdrojové kódy

Merge pull request #3529 from deepseek-harness/turtle/redesign-issue-pr-templates-policy

Redesign issue and pull request templates and simplify policy
Tianyi Cui před 3 týdny
rodič
revize
cd4a23c5df

+ 6 - 0
.agents/notes/implemented/process/2026-09-03-semantic-issue-templates-and-policy.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-03-semantic-issue-templates-and-policy.md
+2026-09-03-semantic-issue-templates-and-policy.md: 96b1981b21e901841d88b09d68a0e7e31ca140a2
+2026-09-03-semantic-issue-templates-and-policy.zh.md: 706cd59047fe5121964183bbc27efc8961a493ed

+ 39 - 0
.agents/notes/implemented/process/2026-09-03-semantic-issue-templates-and-policy.md

@@ -0,0 +1,39 @@
+# Agent Note: Semantic Issue templates and presentation-neutral policy
+
+Status: implemented
+
+English | [中文](2026-09-03-semantic-issue-templates-and-policy.zh.md)
+
+## Problem
+
+Issue and pull-request templates mixed intake questions with review evidence and hid their complete contents in `details` elements. Unused frontmatter and separate Idea and Research templates added choices without changing how the repository planned the work.
+
+Issue policy also treated Markdown presentation as repository metadata. Requirements for `details` elements, a 50-unit visible body, Chinese titles, title metadata prefixes, and an `Owner:` body line produced failures without identifying a missing semantic decision.
+
+## Decision
+
+Issue templates cover Bug, Feature, and Task. Bug asks for a summary, reproduction, current behavior, expected behavior, and environment. Feature asks for motivation and behavior. Task asks for a summary and deliverables. Idea and Research belong in Task unless a future decision gives them distinct lifecycle behavior.
+
+Issue-template frontmatter contains only `name`, `about`, and `type`. Markdown headings define the hierarchy, and HTML comments explain what belongs under each heading.
+
+The pull-request template contains `Motivation`; a `Changes` section with adjacent placeholders for public-interface and behavior changes; and `Testing` entries that show each method directly and place its proof in a local `details` element.
+
+Issue policy does not inspect `details` presentation, visible-body length, title language, title prefixes, or body ownership lines. Every other policy check, warning, lifecycle operation, workflow trigger, and pull-request enforcement exemption retains its existing behavior. This decision adds no metadata repair, Issue classification, data migration, or workflow capability.
+
+## Verification
+
+[Issue-management tests](../../../../.github/issue-management/policy.test.mjs) pin the template inventory and headings, the pull-request testing structure, and acceptance of titles, bodies, and assignee states that differ only in presentation.
+
+## Alternatives considered
+
+**Keep Idea and Research templates.** Their forms did not establish lifecycle or policy behavior distinct from Task, so separate entry points increased choice without preserving a meaningful type distinction.
+
+**Keep presentation rules as warnings.** These rules could fail otherwise actionable Issues and could not establish whether the requested work, expected behavior, or deliverables were clear.
+
+**Add automatic metadata repair or Issue classification.** Those behaviors require new mutation rules, permissions, failure handling, and operational evidence. They remain separate decisions rather than accompanying a policy simplification.
+
+## Consequences
+
+Contributors see shorter forms whose headings match the information needed at Issue intake and pull-request review. Policy failures remain focused on the existing semantic metadata checks.
+
+The policy does not rewrite legacy labels, choose missing Issue Types, synchronize Priority, or migrate existing repository data. Any future automation for those operations needs its own decision and review scope.

+ 39 - 0
.agents/notes/implemented/process/2026-09-03-semantic-issue-templates-and-policy.zh.md

@@ -0,0 +1,39 @@
+# Agent Note: 语义化 Issue template 与不检查展示形式的 policy
+
+Status: implemented
+
+[English](2026-09-03-semantic-issue-templates-and-policy.md) | 中文
+
+## 问题
+
+Issue 与拉取请求(Pull Request,PR)template 把信息收集问题与评审证据混在一起,并用 `details` 元素折叠全部内容。未使用的 frontmatter 以及独立的 Idea 和 Research template 增加了选择,却没有改变仓库规划这些工作的方式。
+
+Issue policy 还把 Markdown 展示方式当成仓库 metadata。对 `details` 元素、50 单位可见正文、中文标题、标题 metadata 前缀和正文 `Owner:` 行的要求会产生失败,却不能指出缺少了哪项语义决策。
+
+## 决策
+
+Issue template 只覆盖 Bug、Feature 和 Task。Bug 收集摘要、复现方式、当前行为、预期行为和环境。Feature 收集动机和行为。Task 收集摘要和交付物。除非未来的决策为 Idea 与 Research 定义不同的生命周期行为,否则它们属于 Task。
+
+Issue template frontmatter 只含 `name`、`about` 和 `type`。Markdown 标题定义信息层级,HTML 注释说明每个标题下应填写的内容。
+
+PR template 包含 `Motivation`、在 `Changes` 中相邻排列的公共接口与行为变化占位说明,以及直接展示每项测试方法并在局部 `details` 元素中放置对应证明的 `Testing` 条目。
+
+Issue policy 不检查 `details` 展示形式、可见正文长度、标题语言、标题前缀或正文 ownership 行。其他所有 policy 检查、警告、生命周期操作、workflow 触发条件和 PR 强制范围豁免均保持既有行为。这项决策不增加 metadata 自动修复、Issue 分类、数据迁移或 workflow 能力。
+
+## 验证
+
+[Issue 管理测试](../../../../.github/issue-management/policy.test.mjs)固定 template 清单与标题、PR 测试结构,并验证仅展示形式不同的标题、正文和 assignee 状态可以通过。
+
+## 考虑过的替代方案
+
+**保留 Idea 和 Research template。** 它们的表单没有建立区别于 Task 的生命周期或 policy 行为,因此独立入口只会增加选择,而不能保留有意义的 Type 区别。
+
+**把展示规则保留为警告。** 这些规则会让原本可执行的 Issue 失败,也不能确定需求工作、预期行为或交付物是否清晰。
+
+**增加 metadata 自动修复或 Issue 分类。** 这些行为需要新的修改规则、权限、失败处理和运行证据。它们应作为独立决策,而不随 policy 简化一同引入。
+
+## 后果
+
+贡献者会看到更短的表单,其标题分别对应 Issue 信息收集和 PR 评审所需的信息。Policy 失败会继续聚焦现有的语义 metadata 检查。
+
+Policy 不会改写旧标签、选择缺失的 Issue Type、同步 Priority 或迁移现有仓库数据。这些操作未来如需自动化,必须单独决策和评审。

+ 16 - 13
.github/ISSUE_TEMPLATE/bug.md

@@ -1,22 +1,25 @@
 ---
 name: Bug
 about: 记录现有预期行为的失效
-title: ''
-labels: ''
-assignees: ''
 type: Bug
 ---
 
-<!-- 标题写中文行动或结果句;外露正文不超过 50 单位。 -->
-一句话说明错误结果。
+## Summary
 
-<details>
-<summary>复现、预期与验收</summary>
+<!-- 简要说明发生了什么错误,以及受影响的用户或场景。 -->
 
-- 复现步骤:
-- 实际结果:
-- 预期结果:
-- 环境:
-- 验收条件:
+## Reproduction
 
-</details>
+<!-- 列出能稳定触发问题的最小步骤、输入或代码。 -->
+
+## Current behavior
+
+<!-- 说明实际结果,并附上必要的错误信息、日志或截图。 -->
+
+## Expected behavior
+
+<!-- 说明正确结果。 -->
+
+## Environment
+
+<!-- 说明相关版本、平台、配置或运行条件。 -->

+ 0 - 1
.github/ISSUE_TEMPLATE/config.yml

@@ -1,2 +1 @@
 blank_issues_enabled: false
-contact_links: []

+ 4 - 11
.github/ISSUE_TEMPLATE/feature.md

@@ -1,20 +1,13 @@
 ---
 name: Feature
 about: 新增或有意改变可观察行为
-title: ''
-labels: ''
-assignees: ''
 type: Feature
 ---
 
-<!-- 标题写中文行动或结果句;外露正文不超过 50 单位。 -->
-一句话说明预期结果。
+## Motivation
 
-<details>
-<summary>验收与细节</summary>
+<!-- 说明当前问题、受影响的用户,以及为什么需要这项变化。 -->
 
-- 验收条件:
-- 用户或模型可见变化:
-- 测试证据:
+## Behavior
 
-</details>
+<!-- 说明预期的用户、模型或系统可观察行为。 -->

+ 0 - 20
.github/ISSUE_TEMPLATE/idea.md

@@ -1,20 +0,0 @@
----
-name: Idea
-about: 记录尚未承诺实施、但具有行动可能的想法
-title: ''
-labels: ''
-assignees: ''
-type: Idea
----
-
-<!-- 标题写中文行动或结果句;外露正文不超过 50 单位。 -->
-一句话说明价值假设。
-
-<details>
-<summary>价值与细节</summary>
-
-- 价值假设:
-- 需要验证:
-- 可能的后续工作:
-
-</details>

+ 0 - 21
.github/ISSUE_TEMPLATE/research.md

@@ -1,21 +0,0 @@
----
-name: Research
-about: 形成结论、证据或决策
-title: ''
-labels: ''
-assignees: ''
-type: Research
----
-
-<!-- 标题写中文行动或结果句;外露正文不超过 50 单位。 -->
-一句话说明待回答的问题。
-
-<details>
-<summary>问题与证据标准</summary>
-
-- 核心问题:
-- 证据标准:
-- 交付结论:
-- 可能的后续工作:
-
-</details>

+ 4 - 11
.github/ISSUE_TEMPLATE/task.md

@@ -1,20 +1,13 @@
 ---
 name: Task
 about: 明确的非 Feature、非 Bug 工作
-title: ''
-labels: ''
-assignees: ''
 type: Task
 ---
 
-<!-- 标题写中文行动或结果句;外露正文不超过 50 单位。 -->
-一句话说明要完成的工作。
+## Summary
 
-<details>
-<summary>验收与细节</summary>
+<!-- 简要说明需要完成的工作。 -->
 
-- 验收条件:
-- 交付物:
-- 测试证据:
+## Deliverables
 
-</details>
+<!-- 列出需要提交或更新的代码、文档、配置或结论。 -->

+ 0 - 1
.github/issue-management/config.json

@@ -7,7 +7,6 @@
   "priorityField": "Priority",
   "startDateField": "Start Date",
   "projectTimeZone": "Asia/Shanghai",
-  "allowUnassignedOwner": true,
   "statuses": [
     "Inbox",
     "Backlog",

+ 2 - 116
.github/issue-management/policy.mjs

@@ -7,9 +7,7 @@ import { pathToFileURL } from 'node:url'
 import config from './config.json' with { type: 'json' }
 
 const API_VERSION = '2026-03-10'
-const BODY_LIMIT = 50
 const AUDIT_MARKER = '<!-- dsh-issue-policy -->'
-const OWNER_LINE = /^Owner: @([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?)$/
 const TYPES = new Set(['Idea', 'Feature', 'Bug', 'Research', 'Task'])
 const PRIORITIES = ['p0', 'p1', 'p2', 'p3']
 const PR_KINDS = new Set([
@@ -63,107 +61,6 @@ if (typeof config.projectTimeZone !== 'string' || !config.projectTimeZone) {
 }
 Intl.DateTimeFormat('en-US', { timeZone: config.projectTimeZone })
 
-/**
- * Return Markdown outside balanced details elements.
- * @param {string} body Markdown body.
- * @returns {{text: string, balanced: boolean, detailsCount: number, allCollapsed: boolean}} Visible source and details shape.
- */
-export function extractOutsideDetails(body) {
-  const source = body.replace(/<!--[\s\S]*?-->/g, '')
-  const tag = /<\/?details\b[^>]*>/gi
-  let depth = 0
-  let cursor = 0
-  let balanced = true
-  let text = ''
-  let detailsCount = 0
-  let allCollapsed = true
-
-  for (const match of source.matchAll(tag)) {
-    const index = match.index ?? 0
-    if (depth === 0) text += source.slice(cursor, index)
-    if (/^<\//.test(match[0])) {
-      if (depth === 0) balanced = false
-      else depth -= 1
-    } else {
-      depth += 1
-      detailsCount += 1
-      if (/\sopen(?:\s|=|>)/i.test(match[0])) allCollapsed = false
-    }
-    cursor = index + match[0].length
-  }
-
-  if (depth === 0) text += source.slice(cursor)
-  if (depth !== 0) balanced = false
-  return { text, balanced, detailsCount, allCollapsed }
-}
-
-/**
- * Count Chinese characters and contiguous Latin, numeric, or code tokens.
- * @param {string} body Markdown body.
- * @returns {{units: number, balanced: boolean, detailsCount: number, allCollapsed: boolean}} Visible unit count and details shape.
- */
-export function countVisibleUnits(body) {
-  const outside = extractOutsideDetails(body)
-  const visible = outside.text
-    .replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
-    .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
-    .replace(/\[([^\]]+)\]\[[^\]]*\]/g, '$1')
-    .replace(/<((?:https?:\/\/|mailto:)[^>]+)>/gi, '$1')
-    .replace(/<[^>]+>/g, ' ')
-    .replace(/&(?:[A-Za-z]+|#\d+|#x[0-9A-Fa-f]+);/g, ' ')
-    .replace(/[\u0060*~\[\]{}()<>#!|]/g, ' ')
-  const han = visible.match(/\p{Script=Han}/gu)?.length ?? 0
-  const tokens = visible.match(/[\p{Script=Latin}\p{Number}_./:@+-]+/gu)?.length ?? 0
-  return {
-    units: han + tokens,
-    balanced: outside.balanced,
-    detailsCount: outside.detailsCount,
-    allCollapsed: outside.allCollapsed,
-  }
-}
-
-function firstNonblankLine(body) {
-  return body
-    .split(/\r?\n/)
-    .map((line) => line.trim())
-    .find(Boolean)
-}
-
-/**
- * Validate required body sections and check Owner against assignees.
- * @param {{body: string, assignees: string[], allowUnassignedOwner?: boolean}} input Body input.
- * @returns {string[]} Validation errors.
- */
-export function validateBody({
-  body,
-  assignees,
-  allowUnassignedOwner = config.allowUnassignedOwner ?? false,
-}) {
-  const errors = []
-  const count = countVisibleUnits(body)
-  const owner = firstNonblankLine(body)?.match(OWNER_LINE)?.[1] ?? null
-  const normalized = [...new Set(assignees.map((login) => login.toLowerCase()))]
-
-  if (!count.balanced) errors.push('details 标签必须成对闭合')
-  if (count.detailsCount === 0) errors.push('正文必须包含默认收起的 <details> 区域')
-  if (!count.allCollapsed) errors.push('details 必须默认收起,不得设置 open')
-  if (count.units > BODY_LIMIT) {
-    errors.push(`正文外露部分为 ${count.units} 单位,超过 50 单位`)
-  }
-  if (normalized.length >= 2 && !owner) {
-    errors.push('多个 Assignees 时首个非空行必须是 Owner: @login')
-  } else if (normalized.length >= 2 && !normalized.includes(owner.toLowerCase())) {
-    errors.push('Owner 必须属于 Assignees')
-  } else if (
-    normalized.length < 2 &&
-    owner &&
-    !(normalized.length === 0 && allowUnassignedOwner)
-  ) {
-    errors.push('零或一个 Assignee 时不得写 Owner 行')
-  }
-  return errors
-}
-
 /**
  * Decide whether the human-review policy applies to a PR.
  * @param {{isDraft: boolean, authorType: string, reviewRequestCount: number, reviewCount: number}} input PR state.
@@ -315,27 +212,19 @@ export function retainIssueReferences(references, issues) {
 
 /**
  * Validate one Issue with its Project status.
- * @param {{title: string, body: string, assignees: string[], labels: string[], type: string|null, priority: string|null, status: string|null, state: string, stateReason: string|null}} issue Issue snapshot.
+ * @param {{labels: string[], type: string|null, priority: string|null, status: string|null, state: string, stateReason: string|null}} issue Issue snapshot.
  * @returns {string[]} Validation errors.
  */
 export function validateIssue(issue) {
-  const errors = validateBody(issue)
+  const errors = []
   const status = issue.status
   const invalidLabels = issue.labels.filter(
     (label) => label.startsWith('kind/') || LEGACY_LABELS.has(label),
   )
 
-  if (!/\p{Script=Han}/u.test(issue.title)) errors.push('Issue 标题必须包含中文')
   if (invalidLabels.length > 0) {
     errors.push(`Issue 不得使用 PR kind 或旧版标签:${invalidLabels.join(', ')}`)
   }
-  if (
-    /^\s*(?:\[(?:Idea|Feature|Bug|Research|Task|P[0-3]|Inbox|Backlog|Ready|In progress|In review|Done|No action|Owner|area\/[^\]]+)[^\]]*\]|(?:Idea|Feature|Bug|Research|Task|P[0-3]|Inbox|Backlog|Ready|In progress|In review|Done|No action|Owner|area\/[^:: ]+)\s*[::-])/iu.test(
-      issue.title,
-    )
-  ) {
-    errors.push('Issue 标题不得带 Type、Priority、Status、area 或 Owner 前缀')
-  }
   if (!TYPES.has(issue.type ?? '')) errors.push('Type 必须是五种原生英文 Type 之一')
   if (!status || !config.statuses.includes(status)) errors.push('Issue 必须在 Project 中且具有合法 Status')
   if (issue.priority !== null && !PRIORITIES.includes(issue.priority.toLowerCase())) {
@@ -468,9 +357,6 @@ export async function issueSnapshot(number, status = undefined) {
   return {
     number,
     nodeId: issue.node_id,
-    title: issue.title,
-    body: issue.body ?? '',
-    assignees: issue.assignees.map((assignee) => assignee.login),
     labels: issue.labels.map((label) => label.name),
     type: issue.type?.name ?? null,
     priority: context.item?.priorityValue?.name ?? null,

+ 60 - 48
.github/issue-management/policy.test.mjs

@@ -1,8 +1,8 @@
 import assert from 'node:assert/strict'
+import { readFileSync, readdirSync } from 'node:fs'
 import test from 'node:test'
 
 import {
-  countVisibleUnits,
   initializeIssueStartDate,
   initializePullRequestStartDates,
   issueSnapshot,
@@ -12,7 +12,6 @@ import {
   retainIssueReferences,
   resolvingIssueStatusCommand,
   requiresPullRequestPolicy,
-  validateBody,
   validateIssue,
   validatePullRequest,
 } from './policy.mjs'
@@ -105,13 +104,7 @@ const mockGraphql = (t, resolve) => {
   return requests
 }
 
-const withDetails = (summary) =>
-  `${summary}\n\n<details><summary>验收与细节</summary>待补充。</details>`
-
 const legalIssue = {
-  title: '完成议题管理校验',
-  body: withDetails('完成议题管理校验。'),
-  assignees: [],
   labels: [],
   type: 'Idea',
   priority: null,
@@ -155,56 +148,80 @@ const reviewedPull = (labels) => ({
   issues: new Map([[2, { priority: null }]]),
 })
 
-test('counts only text outside details', () => {
-  assert.deepEqual(countVisibleUnits('支持 GitHub Project。<details>隐藏文字</details>'), {
-    units: 4,
-    balanced: true,
-    detailsCount: 1,
-    allCollapsed: true,
-  })
+test('keeps only Bug, Feature, and Task Issue templates with used frontmatter', () => {
+  const directory = new URL('../ISSUE_TEMPLATE/', import.meta.url)
+  assert.deepEqual(readdirSync(directory).sort(), ['bug.md', 'config.yml', 'feature.md', 'task.md'])
+
+  for (const file of ['bug.md', 'feature.md', 'task.md']) {
+    const source = readFileSync(new URL(file, directory), 'utf8')
+    const frontmatter = source.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? ''
+    const keys = frontmatter
+      .split('\n')
+      .filter(Boolean)
+      .map((line) => line.slice(0, line.indexOf(':')))
+      .sort()
+    assert.deepEqual(keys, ['about', 'name', 'type'], file)
+    assert.match(source, /^## /m, file)
+    assert.match(source, /<!-- [^\n]+ -->/, file)
+    assert.doesNotMatch(source, /<details\b/i, file)
+  }
+  assert.equal(
+    readFileSync(new URL('config.yml', directory), 'utf8'),
+    'blank_issues_enabled: false\n',
+  )
 })
 
-test('requires a balanced default-collapsed details region', () => {
-  assert.deepEqual(validateBody({ body: '完成工作。', assignees: [] }), [
-    '正文必须包含默认收起的 <details> 区域',
-  ])
-  assert.deepEqual(
-    validateBody({
-      body: '完成工作。\n\n<details open><summary>细节</summary>待补充。</details>',
-      assignees: [],
-    }),
-    ['details 必须默认收起,不得设置 open'],
+test('keeps Feature Issues limited to motivation and behavior', () => {
+  const source = readFileSync(
+    new URL('../ISSUE_TEMPLATE/feature.md', import.meta.url),
+    'utf8',
   )
   assert.deepEqual(
-    validateBody({ body: '完成工作。\n\n<details><summary>细节</summary>', assignees: [] }),
-    ['details 标签必须成对闭合'],
+    [...source.matchAll(/^## .+$/gm)].map(([heading]) => heading),
+    ['## Motivation', '## Behavior'],
   )
 })
 
-test('requires Owner for multiple assignees', () => {
+test('keeps Bug Issues limited to the problem report', () => {
+  const source = readFileSync(new URL('../ISSUE_TEMPLATE/bug.md', import.meta.url), 'utf8')
   assert.deepEqual(
-    validateBody({
-      body: withDetails('完成工作。'),
-      assignees: ['tianyicui', 'tianyicui-bot'],
-    }),
-    ['多个 Assignees 时首个非空行必须是 Owner: @login'],
+    [...source.matchAll(/^## .+$/gm)].map(([heading]) => heading),
+    ['## Summary', '## Reproduction', '## Current behavior', '## Expected behavior', '## Environment'],
   )
 })
 
-test('accepts an intended Owner while assignment permission is pending', () => {
+test('keeps Task Issues limited to summary and deliverables', () => {
+  const source = readFileSync(new URL('../ISSUE_TEMPLATE/task.md', import.meta.url), 'utf8')
   assert.deepEqual(
-    validateBody({
-      body: withDetails('Owner: @octocat\n\n完成工作。'),
-      assignees: [],
-    }),
-    [],
+    [...source.matchAll(/^## .+$/gm)].map(([heading]) => heading),
+    ['## Summary', '## Deliverables'],
   )
+})
+
+test('structures the pull request template around motivation, changes, and testing', () => {
+  const source = readFileSync(new URL('../pull_request_template.md', import.meta.url), 'utf8')
+  for (const heading of ['## Motivation', '## Changes', '## Testing']) {
+    assert.match(source, new RegExp(`^${heading.replaceAll('#', '\\#')}$`, 'm'))
+  }
+  assert.doesNotMatch(source, /^### /m)
+  assert.match(
+    source,
+    /<!-- 高层次说明命令[^\n]+ -->\n<!-- 高层次说明用户[^\n]+ -->/,
+  )
+  assert.match(source, /- <!-- [^\n]+ -->\n\n  <details>\n  <summary>Proof<\/summary>/)
+  assert.equal(source.match(/<details>/g)?.length, 1)
+  assert.equal(source.match(/<\/details>/g)?.length, 1)
+})
+
+test('ignores Issue title, body presentation, and assignee ownership', () => {
   assert.deepEqual(
-    validateBody({
-      body: withDetails('Owner: @octocat\n\n完成工作。'),
-      assignees: ['hubot'],
+    validateIssue({
+      ...legalIssue,
+      title: '[Bug] English title',
+      body: `Owner: @octocat\n\n${'visible '.repeat(60)}<details open>unclosed`,
+      assignees: ['octocat', 'hubot'],
     }),
-    ['零或一个 Assignee 时不得写 Owner 行'],
+    [],
   )
 })
 
@@ -215,11 +232,6 @@ test('allows optional metadata in every open Status', () => {
   }
 })
 
-test('rejects metadata prefixes in an Issue title', () => {
-  const errors = validateIssue({ ...legalIssue, title: '[Bug] 修复恢复错误' })
-  assert.ok(errors.includes('Issue 标题不得带 Type、Priority、Status、area 或 Owner 前缀'))
-})
-
 test('reserves PR kind and legacy labels for pull requests', () => {
   for (const label of [
     ...canonicalKinds,

+ 17 - 9
.github/pull_request_template.md

@@ -1,13 +1,21 @@
-<!-- 写 Fixes #NN 表示解决并自动关闭;写 Related to #NN 仅关联。 -->
-<!-- 进入评审的非 Draft 人类 PR 至少引用一个同仓库 Issue。 -->
-<!-- 解决型 PR 与 Issue 同步 Priority;解决多个 Issue 时取最高值。 -->
+## Motivation
 
-关联 Issue:
+<!-- 用一句话说明需要解决的问题,并以 Fixes #NN 或 Related #NN 引用同仓库 Issue。 -->
 
-<details>
-<summary>变更与验证</summary>
+## Changes
 
-- 变更:
-- 验证:
+<!-- 高层次说明命令、配置、API、协议或持久化格式的变化;没有则写 None。 -->
+<!-- 高层次说明用户、模型或系统可观察行为的变化;没有则写 None。 -->
 
-</details>
+## Testing
+
+<!-- 每种测试方法添加一个条目。方法保持可见,将可复核证据放进对应的 Proof 区域。 -->
+
+- <!-- 说明命令或步骤,以及它覆盖的行为。 -->
+
+  <details>
+  <summary>Proof</summary>
+
+  <!-- 提供测试输出、截图、录屏、日志或其他可复核证据。 -->
+
+  </details>