repowiki-cli 0.6.1__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/PKG-INFO +1 -1
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/pyproject.toml +1 -1
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/coverage.py +51 -2
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/dispatch.py +4 -2
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/i18n.py +2 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/skills/repowiki/SKILL.en.md +5 -3
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/skills/repowiki/SKILL.md +2 -2
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/en/STYLE.md +1 -1
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/en/page_task.md +1 -1
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/en/update_task.md +1 -1
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/zh/STYLE.md +1 -1
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/zh/page_task.md +1 -1
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/zh/update_task.md +1 -1
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/validate.py +78 -17
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki_cli.egg-info/PKG-INFO +1 -1
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/tests/test_coverage.py +25 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/tests/test_i18n.py +3 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/tests/test_validate.py +50 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/LICENSE +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/README.md +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/setup.cfg +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/__init__.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/catalog.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/cli.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/errors.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/gitutil.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/knowledge.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/llms.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/metadata.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/output.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/paths.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/plan.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/scanner.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/site.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/skill_install.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/state.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/tasks.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/en/catalog_task.md +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/en/flow_template.md +6 -6
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/en/knowledge_card_task.md +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/en/knowledge_card_update_task.md +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/en/knowledge_module_task.md +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/en/knowledge_module_update_task.md +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/en/knowledge_task.md +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/en/overview_task.md +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/en/overview_update_task.md +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/en/page_template.md +6 -6
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/site/app.js +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/site.html +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/zh/catalog_task.md +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/zh/flow_template.md +6 -6
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/zh/knowledge_card_task.md +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/zh/knowledge_card_update_task.md +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/zh/knowledge_module_task.md +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/zh/knowledge_module_update_task.md +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/zh/knowledge_task.md +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/zh/overview_task.md +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/zh/overview_update_task.md +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/zh/page_template.md +6 -6
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/updater.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/vendor/marked.min.js +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/vendor/mermaid.min.js +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki_cli.egg-info/SOURCES.txt +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki_cli.egg-info/dependency_links.txt +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki_cli.egg-info/entry_points.txt +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki_cli.egg-info/requires.txt +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki_cli.egg-info/top_level.txt +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/tests/test_flow.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/tests/test_output.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/tests/test_paths_catalog.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/tests/test_robustness.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/tests/test_scanner.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/tests/test_site.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/tests/test_skill_install.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/tests/test_stale.py +0 -0
- {repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/tests/test_state.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "repowiki-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
8
8
|
description = "Deterministic repo wiki build system, driven by any coding agent. Output language follows the repository (zh/en)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -63,12 +63,16 @@ def run_coverage(paths: WikiPaths, as_json: bool) -> int:
|
|
|
63
63
|
uncited = sorted(known - cited)
|
|
64
64
|
total = len(known)
|
|
65
65
|
covered = total - len(uncited)
|
|
66
|
+
breakdown = _classify_uncited(uncited, cited, paths.locale)
|
|
67
|
+
eff_total = total - len(breakdown["vendor"]) - len(breakdown["locale_mirror"])
|
|
66
68
|
result = {
|
|
67
69
|
"ok": True,
|
|
68
70
|
"repo_files": total,
|
|
69
71
|
"cited_files": covered,
|
|
70
72
|
"coverage": round(covered / total, 4) if total else 1.0,
|
|
73
|
+
"effective_coverage": round(covered / eff_total, 4) if eff_total else 1.0,
|
|
71
74
|
"uncited_files": uncited,
|
|
75
|
+
"uncited_breakdown": breakdown,
|
|
72
76
|
"knowledge_files": sorted(knowledge_files),
|
|
73
77
|
"pages": pages,
|
|
74
78
|
}
|
|
@@ -76,15 +80,60 @@ def run_coverage(paths: WikiPaths, as_json: bool) -> int:
|
|
|
76
80
|
return 0
|
|
77
81
|
|
|
78
82
|
|
|
83
|
+
def _locale_mirrors(p: str, locale: str) -> set[str]:
|
|
84
|
+
"""Candidate paths of the other-locale twin of ``p`` (deterministic)."""
|
|
85
|
+
out: set[str] = set()
|
|
86
|
+
for old, new in (
|
|
87
|
+
(f"/{locale}/", "/en/"), ("/en/", f"/{locale}/"),
|
|
88
|
+
(f".{locale}.", ".en."), (".en.", f".{locale}."), (".en.", "."),
|
|
89
|
+
):
|
|
90
|
+
if old in p:
|
|
91
|
+
out.add(p.replace(old, new, 1))
|
|
92
|
+
return out
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _classify_uncited(uncited: list[str], cited: set[str], locale: str) -> dict[str, list[str]]:
|
|
96
|
+
"""Bucket uncited files: vendored third-party code, other-locale mirrors
|
|
97
|
+
of already-cited files, and the rest (the actionable residue)."""
|
|
98
|
+
vendor: list[str] = []
|
|
99
|
+
mirror: list[str] = []
|
|
100
|
+
other: list[str] = []
|
|
101
|
+
for p in uncited:
|
|
102
|
+
if "/vendor/" in f"/{p}":
|
|
103
|
+
vendor.append(p)
|
|
104
|
+
elif _locale_mirrors(p, locale) & cited:
|
|
105
|
+
mirror.append(p)
|
|
106
|
+
else:
|
|
107
|
+
other.append(p)
|
|
108
|
+
return {"vendor": sorted(vendor), "locale_mirror": sorted(mirror), "other": sorted(other)}
|
|
109
|
+
|
|
110
|
+
|
|
79
111
|
def _coverage_human(r: dict) -> str:
|
|
80
112
|
lines = [
|
|
81
113
|
f"覆盖率 {r['cited_files']}/{r['repo_files']}({r['coverage'] * 100:.1f}%)"
|
|
82
114
|
"——被 wiki 页面/总览/知识卡片引用过的仓库文件占比"
|
|
83
115
|
]
|
|
116
|
+
if r.get("effective_coverage") != r["coverage"]:
|
|
117
|
+
lines.append(
|
|
118
|
+
f"有效覆盖率 {r['effective_coverage'] * 100:.1f}%"
|
|
119
|
+
"(剔除 vendor 与其他语言镜像后的口径)"
|
|
120
|
+
)
|
|
84
121
|
uncited = r["uncited_files"]
|
|
85
122
|
if uncited:
|
|
86
|
-
|
|
87
|
-
|
|
123
|
+
bd = r.get("uncited_breakdown") or {}
|
|
124
|
+
mirror = bd.get("locale_mirror", [])
|
|
125
|
+
vendored = bd.get("vendor", [])
|
|
126
|
+
actionable = bd.get("other", uncited)
|
|
127
|
+
lines.append(f"未被引用 {len(uncited)} 个(JSON 输出含全量分组):")
|
|
128
|
+
if mirror:
|
|
129
|
+
lines.append(f" 其他语言镜像(无需引用): {', '.join(mirror[:5])}"
|
|
130
|
+
+ (f" 等 {len(mirror)} 个" if len(mirror) > 5 else ""))
|
|
131
|
+
if vendored:
|
|
132
|
+
lines.append(f" vendor 第三方(无需引用): {', '.join(vendored[:5])}"
|
|
133
|
+
+ (f" 等 {len(vendored)} 个" if len(vendored) > 5 else ""))
|
|
134
|
+
show = actionable[:MAX_LISTING]
|
|
135
|
+
lines.append(f" 值得补引用 {len(actionable)} 个(至多列出 {MAX_LISTING} 个):")
|
|
136
|
+
lines += [f" → {p}" for p in show]
|
|
88
137
|
else:
|
|
89
138
|
lines.append("仓库全部文件都被引用 ✓")
|
|
90
139
|
zero = [p for p in r["pages"] if p["cited_files"] == 0]
|
|
@@ -292,7 +292,8 @@ def _check_readonly(paths: WikiPaths, task: dict, inv) -> dict:
|
|
|
292
292
|
node_id = task["id"][:-len("-update")] if task["id"].endswith("-update") else task["id"]
|
|
293
293
|
res = check_page(raw, task["title"].replace("(增量更新)", ""), paths.repo_root,
|
|
294
294
|
is_update=(task["kind"] == "page_update"), locale=paths.locale,
|
|
295
|
-
archetype=_node_archetype(paths, node_id)
|
|
295
|
+
archetype=_node_archetype(paths, node_id),
|
|
296
|
+
known_paths=inv.known_paths())
|
|
296
297
|
return {**base, "ok": res.ok, "readonly": True, "errors": res.errors,
|
|
297
298
|
"fixed": [], "warnings": res.warnings,
|
|
298
299
|
"note": "done 为终态,此结果仅供参考,状态未改变"}
|
|
@@ -352,7 +353,8 @@ def _check_one(paths: WikiPaths, store: TaskStore, task: dict, inv) -> dict:
|
|
|
352
353
|
node_id = tid[:-len("-update")] if tid.endswith("-update") else tid
|
|
353
354
|
res = check_page(raw, task["title"].replace("(增量更新)", ""), paths.repo_root,
|
|
354
355
|
is_update=(kind == "page_update"), locale=paths.locale,
|
|
355
|
-
archetype=_node_archetype(paths, node_id)
|
|
356
|
+
archetype=_node_archetype(paths, node_id),
|
|
357
|
+
known_paths=inv.known_paths())
|
|
356
358
|
if res.fixed and res.text != raw:
|
|
357
359
|
out_file.write_text(res.text, encoding="utf-8")
|
|
358
360
|
status = "done" if res.ok else "failed"
|
|
@@ -32,6 +32,7 @@ STRINGS: dict[str, dict] = {
|
|
|
32
32
|
("结论", "exact"),
|
|
33
33
|
],
|
|
34
34
|
"update_extra": ("更新摘要", "exact"),
|
|
35
|
+
"section_sources": "章节来源",
|
|
35
36
|
"flow_sections": [
|
|
36
37
|
("简介", "exact"),
|
|
37
38
|
("流程总览", "exact"),
|
|
@@ -74,6 +75,7 @@ STRINGS: dict[str, dict] = {
|
|
|
74
75
|
("Conclusion", "exact"),
|
|
75
76
|
],
|
|
76
77
|
"update_extra": ("Update Summary", "exact"),
|
|
78
|
+
"section_sources": "Section sources",
|
|
77
79
|
"flow_sections": [
|
|
78
80
|
("Introduction", "exact"),
|
|
79
81
|
("Flow Overview", "exact"),
|
|
@@ -131,10 +131,12 @@ code 3 (it created the overview task — normal progress).
|
|
|
131
131
|
source code.
|
|
132
132
|
- Pages follow the template and STYLE guide embedded in the spec: all required sections
|
|
133
133
|
present, "Section sources" at the end of every section, "Diagram sources" after every
|
|
134
|
-
mermaid diagram, `[path:Lx-Ly](file://path#Lx-Ly)` format, line numbers within bounds
|
|
134
|
+
mermaid diagram, `[path:Lx-Ly](file://path#Lx-Ly)` format, line numbers within bounds
|
|
135
|
+
(a start past EOF or an inverted range is rejected; only an overhanging end is clamped),
|
|
135
136
|
zero cross-page links, no emoji/tables.
|
|
136
|
-
- Deterministic defects caught by `check` (anchors/line
|
|
137
|
-
no manual handling needed; only fix the semantic issues listed in
|
|
137
|
+
- Deterministic defects caught by `check` (anchors/H1/overhanging line-range ends) are
|
|
138
|
+
auto-repaired — no manual handling needed; only fix the semantic issues listed in
|
|
139
|
+
`errors`.
|
|
138
140
|
- Output lives in `<repo>/.repowiki/` (`<locale>/content` pages, `<locale>/meta`
|
|
139
141
|
metadata, `knowledge/<locale>/` knowledge cards, `<locale>/wiki.html` single-file
|
|
140
142
|
viewer; locale was fixed at plan time).
|
|
@@ -102,6 +102,6 @@ loop:
|
|
|
102
102
|
## 硬性规则
|
|
103
103
|
|
|
104
104
|
- **只写任务规格指定的 output 文件**,绝不改动仓库源码。
|
|
105
|
-
- 页面遵循规格内嵌的模板与 STYLE 规范:必备小节齐全、每节末尾「Section sources/章节来源」、每个 mermaid 图后「Diagram sources/图表来源」、`[path:Lx-Ly](file://path#Lx-Ly)`
|
|
106
|
-
- `check`
|
|
105
|
+
- 页面遵循规格内嵌的模板与 STYLE 规范:必备小节齐全、每节末尾「Section sources/章节来源」、每个 mermaid 图后「Diagram sources/图表来源」、`[path:Lx-Ly](file://path#Lx-Ly)` 格式、行号不越界(起点越界/区间倒置会被打回,仅终点越界自动钳制)、页间零链接、不用 emoji/表格。
|
|
106
|
+
- `check` 的确定性缺陷(锚点/H1/越界的行区间终点)会被自动修复,无需手动处理;只需修复 `errors` 列出的语义问题。
|
|
107
107
|
- 输出位于 `<repo>/.repowiki/`(`<locale>/content` 页面、`<locale>/meta` 元数据、`knowledge/<locale>/` 知识卡片、`<locale>/wiki.html` 单文件查看站点;locale 已在 plan 时确定)。
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## Section Sources & Diagram Sources (citation format, mandatory)
|
|
8
8
|
- Every body section ends with a "Section sources" list; every mermaid diagram is followed by a "Diagram sources" list.
|
|
9
|
-
- Link format (paths relative to the repo root, forward slashes, line ranges must not exceed the file's real length):
|
|
9
|
+
- Link format (paths relative to the repo root, forward slashes, line ranges must not exceed the file's real length; only an overhanging end is auto-clamped, while a start past EOF or an inverted range is rejected by `check`):
|
|
10
10
|
- `[README.md:1-120](file://README.md#L1-L120)`
|
|
11
11
|
- `src/graphiti/graphiti.py:146-283` → `[graphiti/graphiti.py:146-283](file://graphiti/graphiti.py#L146-L283)`
|
|
12
12
|
- Whole-file references may use the shorter form `[nodes.py](file://graphiti_core/nodes.py)` (inside the `<cite>` block).
|
|
@@ -35,7 +35,7 @@ Write the finished page to: <b>{{OUTPUT_ABS}}</b> (repo-relative: {{OUTPUT}}).
|
|
|
35
35
|
1. Exactly one H1, equal to "{{TITLE}}".
|
|
36
36
|
2. The `<cite>` block lists the files this page actually references (`[file name](file://repo-relative path)` format, 3~15 files).
|
|
37
37
|
3. The "Contents" anchors must match the real section headings (GitHub-style anchors: lowercase, punctuation dropped, spaces → `-`).
|
|
38
|
-
4. After the Introduction, every section ends with "Section sources"; every mermaid diagram is followed by "Diagram sources"; link format `[path:Lx-Ly](file://path#Lx-Ly)` with line numbers within the file's real length.
|
|
38
|
+
4. After the Introduction, every section ends with "Section sources"; every mermaid diagram is followed by "Diagram sources"; link format `[path:Lx-Ly](file://path#Lx-Ly)` with line numbers within the file's real length (a start past EOF or an inverted range is rejected; only an overhanging end is auto-clamped).
|
|
39
39
|
5. At least 2 mermaid diagrams (a structure diagram + a sequence/dependency diagram).
|
|
40
40
|
6. Beyond the required sections above you may add "Appendix: <topic>" sections as needed.
|
|
41
41
|
7. Never link any other page under `.repowiki/`.
|
|
@@ -25,7 +25,7 @@ Rewrite the full updated page to: <b>{{OUTPUT_ABS}}</b> (repo-relative: {{OUTPUT
|
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
## Update rules
|
|
28
|
-
1. Right after the
|
|
28
|
+
1. Right after the H1 and before the table of contents, insert one section:
|
|
29
29
|
|
|
30
30
|
```markdown
|
|
31
31
|
## Update Summary
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
## 章节来源与图表来源(引用格式,强制)
|
|
9
9
|
- 每个正文章节末尾附「章节来源」列表;每个 mermaid 图后附「图表来源」列表。
|
|
10
|
-
-
|
|
10
|
+
- 链接格式(路径相对仓库根目录,统一正斜杠,行号区间不得超出文件实际行数;仅终点越界会被自动钳制到文件末尾,起点越界或区间倒置会被 `check` 打回重写):
|
|
11
11
|
- `[README.md:1-120](file://README.md#L1-L120)`
|
|
12
12
|
- `[graphiti_core/graphiti.py:146-283](file://graphiti_core/graphiti.py#L146-L283)`
|
|
13
13
|
- 仅供整文件引用时可用 `[nodes.py](file://graphiti_core/nodes.py)` 形式(`<cite>` 块内)。
|
|
@@ -35,7 +35,7 @@ hint_files:
|
|
|
35
35
|
1. H1 必须且只能是「{{TITLE}}」。
|
|
36
36
|
2. `<cite>` 块内列出本文实际引用的文件(`[文件名](file://仓库相对路径)` 格式,3~15 个)。
|
|
37
37
|
3. 「目录」的锚点必须与实际章节标题对应(GitHub 中文锚点:去 `:` 等标点、空格转 `-`)。
|
|
38
|
-
4. 简介之后每个小节末尾都有「章节来源」;每个 mermaid 图后都有「图表来源」;链接格式 `[path:Lx-Ly](file://path#Lx-Ly)
|
|
38
|
+
4. 简介之后每个小节末尾都有「章节来源」;每个 mermaid 图后都有「图表来源」;链接格式 `[path:Lx-Ly](file://path#Lx-Ly)`,行号不得超出文件实际行数(起点越界/区间倒置会被打回,仅终点越界自动钳制)。
|
|
39
39
|
5. 至少 2 个 mermaid 图(结构图 + 时序/依赖图)。
|
|
40
40
|
6. 除模板列出的必备小节外,可按需增加「附录:<主题>」小节。
|
|
41
41
|
7. 禁止链接任何 `.repowiki/` 下的其他页面。
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"""Output validation: rule engine with deterministic auto-fix.
|
|
2
2
|
|
|
3
|
-
Anything computable (anchors,
|
|
4
|
-
silently and reported under ``fixed``;
|
|
5
|
-
|
|
3
|
+
Anything computable (anchors, H1, path separators, overhanging range ends)
|
|
4
|
+
is fixed silently and reported under ``fixed``; structural defects —
|
|
5
|
+
impossible line ranges (start past EOF, inverted), blank cited slices,
|
|
6
|
+
missing/empty sections, dangling file references, broken fences — fail the
|
|
7
|
+
task. Whether a citation's *content* supports the claim it anchors remains
|
|
8
|
+
the writer's responsibility; these rules only catch what is computable.
|
|
6
9
|
"""
|
|
7
10
|
|
|
8
11
|
from __future__ import annotations
|
|
@@ -75,7 +78,8 @@ def _strip_code(text: str) -> str:
|
|
|
75
78
|
return re.sub(r"`[^`\n]*`", " ", "".join(out))
|
|
76
79
|
|
|
77
80
|
|
|
78
|
-
def _file_loc(repo_root: Path, rel: str) -> int | None:
|
|
81
|
+
def _file_loc(repo_root: Path, rel: str) -> tuple[int, list[str]] | None:
|
|
82
|
+
"""Line count and decoded lines of a repo file; None if missing/binary."""
|
|
79
83
|
p = repo_root / rel
|
|
80
84
|
try:
|
|
81
85
|
if not p.is_file():
|
|
@@ -83,13 +87,22 @@ def _file_loc(repo_root: Path, rel: str) -> int | None:
|
|
|
83
87
|
data = p.read_bytes()
|
|
84
88
|
if b"\0" in data[:8192]:
|
|
85
89
|
return None
|
|
86
|
-
|
|
90
|
+
lines = data.decode("utf-8", errors="replace").splitlines()
|
|
91
|
+
return max(len(lines), 1), lines
|
|
87
92
|
except OSError:
|
|
88
93
|
return None
|
|
89
94
|
|
|
90
95
|
|
|
96
|
+
# file-looking tokens inside mermaid node labels (checked against the inventory)
|
|
97
|
+
_MERMAID_BLOCK_RE = re.compile(r"```mermaid\n(.*?)```", re.S)
|
|
98
|
+
_FILEISH_TOKEN_RE = re.compile(
|
|
99
|
+
r"[A-Za-z0-9_][\w./\\-]*\.(?:py|md|toml|ya?ml|json|js|jsx?|tsx?|sh|html|txt)\b"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
91
103
|
def check_page(raw: str, title: str, repo_root: Path, is_update: bool = False,
|
|
92
|
-
locale: str = "zh", archetype: str = "module"
|
|
104
|
+
locale: str = "zh", archetype: str = "module",
|
|
105
|
+
known_paths: set[str] | None = None) -> CheckResult:
|
|
93
106
|
res = CheckResult(text=raw)
|
|
94
107
|
text = raw
|
|
95
108
|
lang = strings(locale)
|
|
@@ -128,7 +141,7 @@ def check_page(raw: str, title: str, repo_root: Path, is_update: bool = False,
|
|
|
128
141
|
# <cite> block
|
|
129
142
|
cite = _CITE_RE.search(text)
|
|
130
143
|
if not cite:
|
|
131
|
-
res.fail("缺少 <cite>…</cite>
|
|
144
|
+
res.fail("缺少 <cite>…</cite> 引用块(应位于页面末尾)")
|
|
132
145
|
else:
|
|
133
146
|
cite_links = _FILE_LINK_RE.findall(cite.group("body"))
|
|
134
147
|
if not cite_links:
|
|
@@ -142,7 +155,7 @@ def check_page(raw: str, title: str, repo_root: Path, is_update: bool = False,
|
|
|
142
155
|
if len(mermaid_blocks) < MIN_MERMAID:
|
|
143
156
|
res.fail(f"mermaid 图数量 {len(mermaid_blocks)} < {MIN_MERMAID}(至少需要结构图与时序/依赖图各一)")
|
|
144
157
|
|
|
145
|
-
# file:// links: existence
|
|
158
|
+
# file:// links: existence + line-range sanity (errors) / clamp + separator fix (auto-fix)
|
|
146
159
|
cited_paths: list[str] = []
|
|
147
160
|
missing: set[str] = set()
|
|
148
161
|
|
|
@@ -151,24 +164,39 @@ def check_page(raw: str, title: str, repo_root: Path, is_update: bool = False,
|
|
|
151
164
|
path = orig_path.replace("\\", "/")
|
|
152
165
|
label = m.group("text").replace("\\", "/")
|
|
153
166
|
start, end = m.group("start"), m.group("end")
|
|
154
|
-
|
|
155
|
-
if
|
|
167
|
+
located = _file_loc(repo_root, path)
|
|
168
|
+
if located is None:
|
|
156
169
|
missing.add(orig_path)
|
|
157
170
|
return m.group(0)
|
|
171
|
+
loc, lines = located
|
|
158
172
|
changed = False
|
|
159
173
|
if path != orig_path or label != m.group("text"):
|
|
160
174
|
changed = True
|
|
161
175
|
res.fixed.append(f"链接路径分隔符归一:{orig_path} → {path}")
|
|
162
176
|
if start:
|
|
163
|
-
s = max(1,
|
|
164
|
-
e = max(1,
|
|
165
|
-
if
|
|
166
|
-
|
|
177
|
+
s = max(1, int(start))
|
|
178
|
+
e = max(1, int(end or start))
|
|
179
|
+
if s > loc:
|
|
180
|
+
# anchor points past EOF — almost certainly a hallucinated line number
|
|
181
|
+
res.fail(f"引用行号起点越界:{path} 共 {loc} 行,引用却从 L{s} 开始,请核对行号")
|
|
182
|
+
cited_paths.append(path)
|
|
183
|
+
return f"[{label}](file://{path})" if changed else m.group(0)
|
|
184
|
+
if e < s:
|
|
185
|
+
res.fail(f"行区间倒置:{path}#L{s}-L{e}(起点大于终点),请核对行号")
|
|
186
|
+
cited_paths.append(path)
|
|
187
|
+
return f"[{label}](file://{path})" if changed else m.group(0)
|
|
188
|
+
cited_paths.append(path)
|
|
189
|
+
e2 = min(e, loc)
|
|
190
|
+
if not any(ln.strip() for ln in lines[s - 1:e2]):
|
|
191
|
+
res.fail(f"引用区间全为空行:{path}#L{s}-{e2} 无法支撑任何论断,请核对行号")
|
|
192
|
+
return f"[{label}](file://{path})" if changed else m.group(0)
|
|
193
|
+
clamped = e2 != e or str(s) != start or str(e2) != (end or start)
|
|
194
|
+
if clamped:
|
|
167
195
|
res.fixed.append(
|
|
168
|
-
f"行区间钳制:{path}:{start}-{end or start} → {s}-{
|
|
196
|
+
f"行区间钳制:{path}:{start}-{end or start} → {s}-{e2}(文件共 {loc} 行)"
|
|
169
197
|
)
|
|
170
|
-
|
|
171
|
-
return f"[{path}:{s}-{
|
|
198
|
+
return f"[{path}:{s}-{e2}](file://{path}#L{s}-L{e2})"
|
|
199
|
+
return f"[{path}:{s}-{e2}](file://{path}#L{s}-L{e2})" if changed else m.group(0)
|
|
172
200
|
cited_paths.append(path)
|
|
173
201
|
return f"[{label}](file://{path})" if changed else m.group(0)
|
|
174
202
|
|
|
@@ -178,6 +206,39 @@ def check_page(raw: str, title: str, repo_root: Path, is_update: bool = False,
|
|
|
178
206
|
if len(set(cited_paths)) > MAX_CITED_FILES:
|
|
179
207
|
res.warnings.append(f"单页引用文件 {len(set(cited_paths))} 个 > {MAX_CITED_FILES}")
|
|
180
208
|
|
|
209
|
+
# mermaid node labels naming a file should reference real repo files.
|
|
210
|
+
# Label shorthand ("main.py") matches by basename; conceptual labels
|
|
211
|
+
# without a file extension never trigger this. Warn only.
|
|
212
|
+
if known_paths:
|
|
213
|
+
suspicious: set[str] = set()
|
|
214
|
+
for block in _MERMAID_BLOCK_RE.findall(text):
|
|
215
|
+
for token in _FILEISH_TOKEN_RE.findall(block):
|
|
216
|
+
norm = token.replace("\\", "/")
|
|
217
|
+
if norm not in known_paths and not any(
|
|
218
|
+
k == norm or k.endswith("/" + norm) for k in known_paths
|
|
219
|
+
):
|
|
220
|
+
suspicious.add(norm)
|
|
221
|
+
if suspicious:
|
|
222
|
+
res.warnings.append(
|
|
223
|
+
"mermaid 图节点引用了仓库中不存在的文件名(概念命名可忽略): "
|
|
224
|
+
+ ", ".join(sorted(suspicious)[:5])
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# per-section rules: every ## section (except the TOC / update summary)
|
|
228
|
+
# must be non-empty and carry a sources marker (auto-fix never applies)
|
|
229
|
+
src_marker = lang.get("section_sources", "章节来源")
|
|
230
|
+
skip_sections = {lang["toc"]}
|
|
231
|
+
if is_update:
|
|
232
|
+
skip_sections.add(lang["update_extra"][0])
|
|
233
|
+
parts = re.split(r"^## (.+?)\s*$", text, flags=re.M)
|
|
234
|
+
for h, body in zip(parts[1::2], parts[2::2]):
|
|
235
|
+
if h.strip() in skip_sections:
|
|
236
|
+
continue
|
|
237
|
+
if not _strip_code(body).strip():
|
|
238
|
+
res.fail(f"小节「{h.strip()}」内容为空")
|
|
239
|
+
elif src_marker not in body:
|
|
240
|
+
res.fail(f"小节「{h.strip()}」缺少「{src_marker}」来源列表")
|
|
241
|
+
|
|
181
242
|
# wiki-to-wiki links (warn only)
|
|
182
243
|
wiki_refs = [m.group(1) for m in _WIKI_LINK_RE.finditer(text)]
|
|
183
244
|
if wiki_refs:
|
|
@@ -87,3 +87,28 @@ def test_coverage_ignores_citations_to_deleted_files(repo, capsys):
|
|
|
87
87
|
assert run("coverage", str(repo), "--json") == 0
|
|
88
88
|
data = json.loads(capsys.readouterr().out)
|
|
89
89
|
assert data["cited_files"] == 2 # api.py no longer in the inventory
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_coverage_breakdown_and_effective_rate(repo, capsys):
|
|
93
|
+
paths = setup(repo)
|
|
94
|
+
(repo / "src/demo/vendor").mkdir()
|
|
95
|
+
(repo / "src/demo/vendor/lib.py").write_text("x = 1\n", encoding="utf-8")
|
|
96
|
+
(repo / "docs/zh").mkdir(parents=True)
|
|
97
|
+
(repo / "docs/zh/CONTEXT.md").write_text("背景\n", encoding="utf-8")
|
|
98
|
+
(repo / "docs/en").mkdir(parents=True)
|
|
99
|
+
(repo / "docs/en/CONTEXT.md").write_text("context\n", encoding="utf-8")
|
|
100
|
+
# cite the zh twin so the en file classifies as a locale mirror
|
|
101
|
+
page = paths.root / "zh/content/快速开始.md"
|
|
102
|
+
page.write_text(
|
|
103
|
+
valid_page("快速开始").replace(
|
|
104
|
+
"</cite>", "- [docs/zh/CONTEXT.md](file://docs/zh/CONTEXT.md)\n</cite>"
|
|
105
|
+
),
|
|
106
|
+
encoding="utf-8",
|
|
107
|
+
)
|
|
108
|
+
assert run("coverage", str(repo), "--json") == 0
|
|
109
|
+
data = json.loads(capsys.readouterr().out)
|
|
110
|
+
assert "src/demo/vendor/lib.py" in data["uncited_breakdown"]["vendor"]
|
|
111
|
+
assert "docs/en/CONTEXT.md" in data["uncited_breakdown"]["locale_mirror"]
|
|
112
|
+
assert "docs/zh/CONTEXT.md" not in data["uncited_files"]
|
|
113
|
+
# effective coverage excludes vendor + mirrors from the denominator
|
|
114
|
+
assert data["effective_coverage"] > data["coverage"]
|
|
@@ -69,6 +69,56 @@ class TestPageRules:
|
|
|
69
69
|
# README.md is 3 lines in the fixture; the out-of-range 9999 gets clamped
|
|
70
70
|
assert "README.md:1-3" in res.text
|
|
71
71
|
|
|
72
|
+
def test_start_beyond_eof_fails(self, repo):
|
|
73
|
+
# start past EOF is a hallucinated anchor: error, not a silent clamp
|
|
74
|
+
text = valid_page().replace(
|
|
75
|
+
"[README.md:1-2](file://README.md#L1-L2)", "[README.md:50-60](file://README.md#L50-L60)", 1
|
|
76
|
+
)
|
|
77
|
+
res = check_page(text, "项目概述", repo)
|
|
78
|
+
assert not res.ok and any("起点越界" in e for e in res.errors)
|
|
79
|
+
|
|
80
|
+
def test_inverted_range_fails(self, repo):
|
|
81
|
+
text = valid_page().replace(
|
|
82
|
+
"[src/demo/main.py:1-7](file://src/demo/main.py#L1-L7)",
|
|
83
|
+
"[src/demo/main.py:7-1](file://src/demo/main.py#L7-L1)", 1
|
|
84
|
+
)
|
|
85
|
+
res = check_page(text, "项目概述", repo)
|
|
86
|
+
assert not res.ok and any("倒置" in e for e in res.errors)
|
|
87
|
+
|
|
88
|
+
def test_blank_cited_slice_fails(self, repo):
|
|
89
|
+
# line 2 of the fixture README.md is blank: nothing to anchor a claim to
|
|
90
|
+
text = valid_page().replace(
|
|
91
|
+
"[README.md:1-2](file://README.md#L1-L2)", "[README.md:2-2](file://README.md#L2-L2)", 1
|
|
92
|
+
)
|
|
93
|
+
res = check_page(text, "项目概述", repo)
|
|
94
|
+
assert not res.ok and any("空行" in e for e in res.errors)
|
|
95
|
+
|
|
96
|
+
def test_mermaid_unknown_filename_warns(self, repo):
|
|
97
|
+
text = valid_page().replace('A["api.py"] --> B["models.py"]', 'A["engine.py"] --> B["models.py"]', 1)
|
|
98
|
+
known = {"src/demo/api.py", "src/demo/models.py", "src/demo/main.py"}
|
|
99
|
+
res = check_page(text, "项目概述", repo, known_paths=known)
|
|
100
|
+
assert res.ok and any("engine.py" in w for w in res.warnings)
|
|
101
|
+
|
|
102
|
+
def test_mermaid_known_filenames_no_warning(self, repo):
|
|
103
|
+
# label shorthand ("api.py") matches by basename against the inventory
|
|
104
|
+
known = {"src/demo/api.py", "src/demo/models.py", "src/demo/main.py"}
|
|
105
|
+
res = check_page(valid_page(), "项目概述", repo, known_paths=known)
|
|
106
|
+
assert res.ok and not any("不存在" in w for w in res.warnings)
|
|
107
|
+
|
|
108
|
+
def test_section_without_sources_fails(self, repo):
|
|
109
|
+
text = valid_page().replace("- 启动失败:检查依赖\n\n章节来源", "- 启动失败:检查依赖", 1)
|
|
110
|
+
res = check_page(text, "项目概述", repo)
|
|
111
|
+
assert not res.ok and any("章节来源" in e for e in res.errors)
|
|
112
|
+
|
|
113
|
+
def test_empty_section_fails(self, repo):
|
|
114
|
+
text = valid_page().replace(
|
|
115
|
+
"## 性能与一致性考量\n- 单进程内存列表,无并发保障\n\n章节来源"
|
|
116
|
+
"\n- [src/demo/api.py:1-8](file://src/demo/api.py#L1-L8)",
|
|
117
|
+
"## 性能与一致性考量", 1,
|
|
118
|
+
)
|
|
119
|
+
res = check_page(text, "项目概述", repo)
|
|
120
|
+
assert not res.ok and any("内容为空" in e for e in res.errors)
|
|
121
|
+
|
|
72
122
|
def test_backslash_path_normalized(self, repo):
|
|
73
123
|
text = valid_page().replace("file://src/demo/main.py", "file://src\\demo\\main.py")
|
|
74
124
|
res = check_page(text, "项目概述", repo)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
# {{TITLE}}
|
|
2
2
|
|
|
3
|
-
<cite>
|
|
4
|
-
**Files referenced**
|
|
5
|
-
- [<file name>](file://<repo-relative path>)
|
|
6
|
-
- [<file name>](file://<repo-relative path>)
|
|
7
|
-
</cite>
|
|
8
|
-
|
|
9
3
|
## Contents
|
|
10
4
|
1. [Introduction](#introduction)
|
|
11
5
|
2. [Flow Overview](#flow-overview)
|
|
@@ -85,3 +79,9 @@ Section sources
|
|
|
85
79
|
|
|
86
80
|
Section sources
|
|
87
81
|
- [<path>:<from>-<to>](file://<path>#L<from>-L<to>)
|
|
82
|
+
|
|
83
|
+
<cite>
|
|
84
|
+
**Files referenced**
|
|
85
|
+
- [<file name>](file://<repo-relative path>)
|
|
86
|
+
- [<file name>](file://<repo-relative path>)
|
|
87
|
+
</cite>
|
|
File without changes
|
{repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/en/knowledge_card_update_task.md
RENAMED
|
File without changes
|
{repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/en/knowledge_module_task.md
RENAMED
|
File without changes
|
{repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/en/knowledge_module_update_task.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
# {{TITLE}}
|
|
2
2
|
|
|
3
|
-
<cite>
|
|
4
|
-
**Files referenced by this page**
|
|
5
|
-
- [<file name>](file://<repo-relative path>)
|
|
6
|
-
- [<file name>](file://<repo-relative path>)
|
|
7
|
-
</cite>
|
|
8
|
-
|
|
9
3
|
## Contents
|
|
10
4
|
1. [Introduction](#introduction)
|
|
11
5
|
2. [Project Structure](#project-structure)
|
|
@@ -111,3 +105,9 @@ Section sources
|
|
|
111
105
|
|
|
112
106
|
Section sources
|
|
113
107
|
- [<path>:<start>-<end>](file://<path>#L<start>-L<end>)
|
|
108
|
+
|
|
109
|
+
<cite>
|
|
110
|
+
**Files referenced by this page**
|
|
111
|
+
- [<file name>](file://<repo-relative path>)
|
|
112
|
+
- [<file name>](file://<repo-relative path>)
|
|
113
|
+
</cite>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
# {{TITLE}}
|
|
2
2
|
|
|
3
|
-
<cite>
|
|
4
|
-
**本文引用的文件**
|
|
5
|
-
- [<文件名>](file://<仓库相对路径>)
|
|
6
|
-
- [<文件名>](file://<仓库相对路径>)
|
|
7
|
-
</cite>
|
|
8
|
-
|
|
9
3
|
## 目录
|
|
10
4
|
1. [简介](#简介)
|
|
11
5
|
2. [流程总览](#流程总览)
|
|
@@ -85,3 +79,9 @@ B -->|"完成"| C["终态"]
|
|
|
85
79
|
|
|
86
80
|
章节来源
|
|
87
81
|
- [<path>:<起>-<止>](file://<path>#L<起>-L<止>)
|
|
82
|
+
|
|
83
|
+
<cite>
|
|
84
|
+
**本文引用的文件**
|
|
85
|
+
- [<文件名>](file://<仓库相对路径>)
|
|
86
|
+
- [<文件名>](file://<仓库相对路径>)
|
|
87
|
+
</cite>
|
|
File without changes
|
{repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/zh/knowledge_card_update_task.md
RENAMED
|
File without changes
|
{repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/zh/knowledge_module_task.md
RENAMED
|
File without changes
|
{repowiki_cli-0.6.1 → repowiki_cli-0.7.0}/src/repowiki/templates/zh/knowledge_module_update_task.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
# {{TITLE}}
|
|
2
2
|
|
|
3
|
-
<cite>
|
|
4
|
-
**本文引用的文件**
|
|
5
|
-
- [<文件名>](file://<仓库相对路径>)
|
|
6
|
-
- [<文件名>](file://<仓库相对路径>)
|
|
7
|
-
</cite>
|
|
8
|
-
|
|
9
3
|
## 目录
|
|
10
4
|
1. [简介](#简介)
|
|
11
5
|
2. [项目结构](#项目结构)
|
|
@@ -111,3 +105,9 @@ B --> C["外部依赖"]
|
|
|
111
105
|
|
|
112
106
|
章节来源
|
|
113
107
|
- [<path>:<起>-<止>](file://<path>#L<起>-L<止>)
|
|
108
|
+
|
|
109
|
+
<cite>
|
|
110
|
+
**本文引用的文件**
|
|
111
|
+
- [<文件名>](file://<仓库相对路径>)
|
|
112
|
+
- [<文件名>](file://<仓库相对路径>)
|
|
113
|
+
</cite>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|