🐛 fix(tsl-syntax-reference): route derived rules and bound section size
This commit is contained in:
@@ -37,6 +37,9 @@ QUICKSTART_RULE_RE = re.compile(
|
||||
QUICKSTART_RULE_PREFIX_RE = re.compile(
|
||||
r"^<!--\s*quickstart-rule\s*:", re.IGNORECASE
|
||||
)
|
||||
OWNER_SECTION_RE = re.compile(
|
||||
r"^\s+Owner Section:`([a-z0-9][a-z0-9._-]*)`\s*$", re.IGNORECASE
|
||||
)
|
||||
STRUCTURAL_METADATA_RE = re.compile(
|
||||
r"<!--\s*(?:section-id|quickstart-rule)\s*:.*?-->",
|
||||
re.DOTALL | re.IGNORECASE,
|
||||
@@ -83,23 +86,24 @@ WRITE_PRELUDE_SECTIONS = (
|
||||
)
|
||||
QUICKSTART_PAGE = "01_quickstart.md"
|
||||
QUICKSTART_SUMMARY_HEADING = "语言核心事实速查"
|
||||
QUICKSTART_RULE_OWNER_PAGES = {
|
||||
"assignment": "06_expressions_and_operators.md",
|
||||
"file-choice": "02_core_model.md",
|
||||
"tsl-layout": "02_core_model.md",
|
||||
"tsf-layout": "02_core_model.md",
|
||||
"tsf-filename": "02_core_model.md",
|
||||
"function-default": "05_functions_and_calls.md",
|
||||
"procedure-explicit": "05_functions_and_calls.md",
|
||||
"class-shape": "08_objects_and_classes.md",
|
||||
"object-creation": "08_objects_and_classes.md",
|
||||
"unit-shape": "09_units_and_scope.md",
|
||||
"unit-default": "09_units_and_scope.md",
|
||||
"named-arguments": "05_functions_and_calls.md",
|
||||
"named-argument-order": "05_functions_and_calls.md",
|
||||
"string-literal-default": "03_values_and_literals.md",
|
||||
"string-prefix-by-type": "03_values_and_literals.md",
|
||||
"index-origins": "03_values_and_literals.md",
|
||||
MAX_LEAF_SECTION_LINES = 180
|
||||
QUICKSTART_RULE_OWNERS = {
|
||||
"assignment": ("06_expressions_and_operators.md", "syntax-06-004"),
|
||||
"file-choice": ("02_core_model.md", "syntax-02-002"),
|
||||
"tsl-layout": ("02_core_model.md", "syntax-02-002"),
|
||||
"tsf-layout": ("02_core_model.md", "syntax-02-002"),
|
||||
"tsf-filename": ("02_core_model.md", "syntax-02-002"),
|
||||
"function-default": ("05_functions_and_calls.md", "syntax-05-002"),
|
||||
"procedure-explicit": ("05_functions_and_calls.md", "syntax-05-002"),
|
||||
"class-shape": ("08_objects_and_classes.md", "syntax-08-002"),
|
||||
"object-creation": ("08_objects_and_classes.md", "syntax-08-002"),
|
||||
"unit-shape": ("09_units_and_scope.md", "syntax-09-002"),
|
||||
"unit-default": ("09_units_and_scope.md", "syntax-09-002"),
|
||||
"named-arguments": ("05_functions_and_calls.md", "syntax-05-002"),
|
||||
"named-argument-order": ("05_functions_and_calls.md", "syntax-05-002"),
|
||||
"string-literal-default": ("03_values_and_literals.md", "syntax-03-004"),
|
||||
"string-prefix-by-type": ("03_values_and_literals.md", "syntax-03-004"),
|
||||
"index-origins": ("03_values_and_literals.md", "syntax-03-002"),
|
||||
}
|
||||
|
||||
HEADING_TOKEN_SCORE = 12
|
||||
@@ -232,6 +236,9 @@ class QuickstartRule:
|
||||
bullet_line: int
|
||||
key: str
|
||||
text: str
|
||||
section_id: str | None
|
||||
owner_section: str | None
|
||||
owner_line: int | None
|
||||
|
||||
|
||||
class ReferenceInstallationError(RuntimeError):
|
||||
@@ -724,6 +731,7 @@ def _quickstart_rule_records(
|
||||
records: list[QuickstartRule] = []
|
||||
problems: list[ValidationProblem] = []
|
||||
in_fence = False
|
||||
current_section_id: str | None = None
|
||||
for index, line in enumerate(lines):
|
||||
if FENCE_RE.match(line):
|
||||
in_fence = not in_fence
|
||||
@@ -731,6 +739,14 @@ def _quickstart_rule_records(
|
||||
if in_fence:
|
||||
continue
|
||||
stripped = line.strip()
|
||||
heading = HEADING_RE.match(line)
|
||||
if heading is not None and len(heading.group(1)) in (2, 3, 4):
|
||||
metadata_line = _next_nonblank_line(lines, index + 1)
|
||||
metadata = lines[metadata_line].strip() if metadata_line is not None else ""
|
||||
section_id_match = SECTION_ID_RE.fullmatch(metadata)
|
||||
current_section_id = (
|
||||
section_id_match.group(1) if section_id_match is not None else None
|
||||
)
|
||||
if not QUICKSTART_RULE_PREFIX_RE.match(stripped):
|
||||
continue
|
||||
marker = QUICKSTART_RULE_RE.fullmatch(stripped)
|
||||
@@ -750,6 +766,8 @@ def _quickstart_rule_records(
|
||||
)
|
||||
continue
|
||||
text_parts = [lines[bullet_line].strip()]
|
||||
owner_section: str | None = None
|
||||
owner_line: int | None = None
|
||||
continuation = bullet_line + 1
|
||||
while continuation < len(lines):
|
||||
line = lines[continuation]
|
||||
@@ -757,7 +775,21 @@ def _quickstart_rule_records(
|
||||
break
|
||||
if not line.startswith((" ", "\t")):
|
||||
break
|
||||
text_parts.append(line.strip())
|
||||
owner_match = OWNER_SECTION_RE.fullmatch(line)
|
||||
if owner_match is not None:
|
||||
if owner_section is not None:
|
||||
problems.append(
|
||||
ValidationProblem(
|
||||
page,
|
||||
continuation + 1,
|
||||
f"quickstart-rule {marker.group(1)!r} 重复 Owner Section",
|
||||
)
|
||||
)
|
||||
else:
|
||||
owner_section = owner_match.group(1)
|
||||
owner_line = continuation + 1
|
||||
else:
|
||||
text_parts.append(line.strip())
|
||||
continuation += 1
|
||||
records.append(
|
||||
QuickstartRule(
|
||||
@@ -766,6 +798,9 @@ def _quickstart_rule_records(
|
||||
bullet_line=bullet_line + 1,
|
||||
key=marker.group(1),
|
||||
text=" ".join(text_parts),
|
||||
section_id=current_section_id,
|
||||
owner_section=owner_section,
|
||||
owner_line=owner_line,
|
||||
)
|
||||
)
|
||||
return records, problems
|
||||
@@ -829,7 +864,13 @@ def _quickstart_rule_problems(
|
||||
by_key: dict[str, list[QuickstartRule]] = {}
|
||||
for record in records:
|
||||
by_key.setdefault(record.key, []).append(record)
|
||||
expected_keys = set(QUICKSTART_RULE_OWNER_PAGES) if require_complete else set()
|
||||
expected_keys = set(QUICKSTART_RULE_OWNERS) if require_complete else set()
|
||||
declared_section_ids = {
|
||||
match.group(1)
|
||||
for lines in pages.values()
|
||||
for line in lines
|
||||
if (match := SECTION_ID_RE.fullmatch(line.strip())) is not None
|
||||
}
|
||||
for key in sorted(set(by_key) | expected_keys):
|
||||
key_records = by_key.get(key, [])
|
||||
summaries = [record for record in key_records if record.page == quickstart]
|
||||
@@ -843,20 +884,57 @@ def _quickstart_rule_problems(
|
||||
)
|
||||
)
|
||||
continue
|
||||
expected_page = QUICKSTART_RULE_OWNER_PAGES.get(key)
|
||||
if expected_page is not None and canonicals[0].page.name != expected_page:
|
||||
summary = summaries[0]
|
||||
canonical = canonicals[0]
|
||||
expected_owner = QUICKSTART_RULE_OWNERS.get(key)
|
||||
if expected_owner is not None and canonical.page.name != expected_owner[0]:
|
||||
problems.append(
|
||||
ValidationProblem(
|
||||
canonicals[0].page,
|
||||
canonicals[0].line,
|
||||
f"quickstart-rule {key!r} 的专题 owner 应为 {expected_page}",
|
||||
canonical.page,
|
||||
canonical.line,
|
||||
f"quickstart-rule {key!r} 的专题 owner 应为 {expected_owner[0]}",
|
||||
)
|
||||
)
|
||||
if summaries[0].text != canonicals[0].text:
|
||||
if expected_owner is not None and canonical.section_id != expected_owner[1]:
|
||||
problems.append(
|
||||
ValidationProblem(
|
||||
canonical.page,
|
||||
canonical.line,
|
||||
f"quickstart-rule {key!r} 的专题 owner Section 应为 "
|
||||
f"{expected_owner[1]}",
|
||||
)
|
||||
)
|
||||
if summary.owner_section is None:
|
||||
problems.append(
|
||||
ValidationProblem(
|
||||
quickstart,
|
||||
summaries[0].bullet_line,
|
||||
summary.bullet_line,
|
||||
f"quickstart-rule {key!r} 缺少 Owner Section",
|
||||
)
|
||||
)
|
||||
elif summary.owner_section not in declared_section_ids:
|
||||
problems.append(
|
||||
ValidationProblem(
|
||||
quickstart,
|
||||
summary.owner_line or summary.bullet_line,
|
||||
f"quickstart-rule {key!r} 的 Owner Section 不存在:"
|
||||
f"{summary.owner_section}",
|
||||
)
|
||||
)
|
||||
elif canonical.section_id is not None and summary.owner_section != canonical.section_id:
|
||||
problems.append(
|
||||
ValidationProblem(
|
||||
quickstart,
|
||||
summary.owner_line or summary.bullet_line,
|
||||
f"quickstart-rule {key!r} 的 Owner Section 与专题事实所在 Section "
|
||||
f"不一致:{summary.owner_section} != {canonical.section_id}",
|
||||
)
|
||||
)
|
||||
if summary.text != canonical.text:
|
||||
problems.append(
|
||||
ValidationProblem(
|
||||
quickstart,
|
||||
summary.bullet_line,
|
||||
f"派生摘要规则与专题事实不一致:{key}",
|
||||
)
|
||||
)
|
||||
@@ -867,6 +945,35 @@ def _quickstart_rule_problems(
|
||||
return problems
|
||||
|
||||
|
||||
def _leaf_section_granularity_problems(
|
||||
sections: list[Section],
|
||||
) -> list[ValidationProblem]:
|
||||
"""Reject exact-retrieval units whose body has grown beyond the safe budget."""
|
||||
problems: list[ValidationProblem] = []
|
||||
for section in sections:
|
||||
has_child = any(
|
||||
other.page == section.page
|
||||
and len(other.heading_path) > len(section.heading_path)
|
||||
and other.heading_path[: len(section.heading_path)]
|
||||
== section.heading_path
|
||||
for other in sections
|
||||
)
|
||||
if has_child:
|
||||
continue
|
||||
line_count = len(section.body.splitlines())
|
||||
if line_count <= MAX_LEAF_SECTION_LINES:
|
||||
continue
|
||||
problems.append(
|
||||
ValidationProblem(
|
||||
section.page,
|
||||
1,
|
||||
f"叶子 Section 正文超过 {MAX_LEAF_SECTION_LINES} 行:"
|
||||
f"{section.id}({line_count} 行)",
|
||||
)
|
||||
)
|
||||
return problems
|
||||
|
||||
|
||||
def validate_references(
|
||||
references_dir: Path = DEFAULT_REFERENCES_DIR,
|
||||
) -> list[ValidationProblem]:
|
||||
@@ -971,6 +1078,7 @@ def validate_references(
|
||||
return problems
|
||||
sections = load_sections(references_dir)
|
||||
problems.extend(_tag_problems(sections))
|
||||
problems.extend(_leaf_section_granularity_problems(sections))
|
||||
ids: dict[str, Section] = {}
|
||||
for section in sections:
|
||||
if section.id in ids:
|
||||
@@ -1534,9 +1642,24 @@ HELP_EPILOG = """\
|
||||
选定的 Section ID 可以合并成一次取回
|
||||
lookup.py --section "syntax-05-004" "syntax-02-002"
|
||||
|
||||
查询词无从下手时先 --map 把需求映射到 TSL 概念;改动参考页或 data/ 词表后用 --check 校验。
|
||||
一次 --query 只覆盖一个语法要素;从用户原话提取错误原文、标识符或要素名,
|
||||
保留原写法但不传完整用户句。多个要素分别查询,再批量 --section。
|
||||
|
||||
write 模式额外列出 Required: yes 前置章节,不占 --limit,同一任务只需取回一次。
|
||||
syntax-01-002 是派生速查;每条规则后的 Owner Section 才是该事实的专题来源。
|
||||
|
||||
查询词无从下手时先 --map 把需求映射到 TSL 概念;地图不含可照写事实。
|
||||
弱命中(候选标 Weak: yes)没有意图/标题/标识符/标签命中,只靠正文低分撞词;
|
||||
全部候选皆弱时视同无匹配并返回 rc=2,应改进查询词重试而不是从弱候选里挑。
|
||||
混合候选只从强命中里选择;全部候选皆弱时视同无匹配并返回 rc=2。
|
||||
没有 Weak 行也不保证相关,仍按 Summary 选定后取回正文核对。
|
||||
|
||||
--section 中任一 ID 不存在时整批不返回正文,并在 stderr 给出最近的 Section ID。
|
||||
改动参考页或 data/ 词表后用 --check;它不校验事实语义、排序或示例运行结果。
|
||||
|
||||
退出码:
|
||||
0 动作成功
|
||||
1 参考目录/结构/安装错误,或 --check 发现问题
|
||||
2 参数错误、无匹配、全弱命中或 Section ID 不存在
|
||||
"""
|
||||
|
||||
|
||||
@@ -1581,7 +1704,8 @@ def _parser() -> argparse.ArgumentParser:
|
||||
"--check",
|
||||
action="store_true",
|
||||
help="只校验参考页结构、显式 Section ID、代码块身份、本地链接、quickstart "
|
||||
"派生规则和词表页覆盖;检索排序由测试套件校验。发现问题时退出码为 1",
|
||||
"派生规则、词表页覆盖和叶子 Section 180 行粒度上限;检索排序由测试套件校验。"
|
||||
"发现问题时退出码为 1",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
|
||||
Reference in New Issue
Block a user