From 7da873da73a2b1534210ffc868dba92da86d45bf Mon Sep 17 00:00:00 2001 From: DJC1412 <108855841+DJC1412@users.noreply.github.com> Date: Sun, 20 Sep 2026 11:16:52 +0800 Subject: [PATCH 1/4] fix(repo): point live surfaces at the canonical repository The organization migration moved the project to loopx-project/loopx, and GitHub's redirect meant nothing failed loudly while shipped defaults, projected documentation links, monitored connector targets and copied command examples kept handing out the pre-transfer address. Dated citations (pull/issue numbers in prose) are left alone: they record the address the event happened under. Signed-off-by: DJC1412 <108855841+DJC1412@users.noreply.github.com> --- loopx/agent_onboarding.py | 2 +- .../capabilities/benchmark_toolkit/README.md | 4 ++-- .../manager_runtime/machine_profile.py | 2 +- .../steward_executor/machine_defaults.py | 2 +- .../capabilities/value_connectors/planner.py | 4 ++-- loopx/cli_commands/first_run_report.py | 2 +- loopx/configuration_catalog.py | 20 +++++++++---------- packages/dsh-loopx-plugin/README.md | 2 +- packages/dsh-loopx-plugin/package.json | 2 +- packages/loopx-community-discussion/README.md | 4 ++-- .../examples/request.json | 2 +- .../loopx-repo-health/examples/request.json | 2 +- 12 files changed, 24 insertions(+), 24 deletions(-) diff --git a/loopx/agent_onboarding.py b/loopx/agent_onboarding.py index a861db2fd7..2f90aaf530 100644 --- a/loopx/agent_onboarding.py +++ b/loopx/agent_onboarding.py @@ -242,7 +242,7 @@ def _skill_delivery_contract( else {} ) ), - "source_repository": "https://github.com/huangruiteng/loopx", + "source_repository": "https://github.com/loopx-project/loopx", "source_directories": [ f"skills/{skill_id}" for skill_id in required_skill_ids diff --git a/loopx/capabilities/benchmark_toolkit/README.md b/loopx/capabilities/benchmark_toolkit/README.md index bb0d1e271a..93cf6ac9f7 100644 --- a/loopx/capabilities/benchmark_toolkit/README.md +++ b/loopx/capabilities/benchmark_toolkit/README.md @@ -1360,8 +1360,8 @@ All commands are local and no-upload by default. `benchmark-toolkit` grants no m Docker, runner, upload, submission, publication, or production authority. The active benchmark research program and current public-safe practice live under -[`benchmark/`](https://github.com/huangruiteng/loopx/blob/main/benchmark/README.md). Retired implementations, superseded runners, and dated research -packets are retained under [`deprecate/benchmark-legacy/`](https://github.com/huangruiteng/loopx/blob/main/deprecate/benchmark-legacy/README.md) +[`benchmark/`](https://github.com/loopx-project/loopx/blob/main/benchmark/README.md). Retired implementations, superseded runners, and dated research +packets are retained under [`deprecate/benchmark-legacy/`](https://github.com/loopx-project/loopx/blob/main/deprecate/benchmark-legacy/README.md) for source archaeology only. Immutable experiment snapshots follow the canonical [archive placement rules](../../../benchmark/README.md#archive-placement), diff --git a/loopx/capabilities/manager_runtime/machine_profile.py b/loopx/capabilities/manager_runtime/machine_profile.py index a4c93c5201..a00ad9e9cf 100644 --- a/loopx/capabilities/manager_runtime/machine_profile.py +++ b/loopx/capabilities/manager_runtime/machine_profile.py @@ -54,7 +54,7 @@ def manager_runtime_machine_configuration_namespace() -> MachineConfigurationNam documentation={ "path": "docs/architecture/rfcs/manager-runtime-profile-v0.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "docs/architecture/rfcs/manager-runtime-profile-v0.md" ), }, diff --git a/loopx/capabilities/steward_executor/machine_defaults.py b/loopx/capabilities/steward_executor/machine_defaults.py index 35482c8a67..be373e8813 100644 --- a/loopx/capabilities/steward_executor/machine_defaults.py +++ b/loopx/capabilities/steward_executor/machine_defaults.py @@ -145,7 +145,7 @@ def steward_executor_machine_configuration_namespace() -> ( documentation={ "path": "docs/architecture/rfcs/harness-selection-dsh-pi-v0.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "docs/architecture/rfcs/harness-selection-dsh-pi-v0.md" ), }, diff --git a/loopx/capabilities/value_connectors/planner.py b/loopx/capabilities/value_connectors/planner.py index 2b6ab53249..c39ff811ce 100644 --- a/loopx/capabilities/value_connectors/planner.py +++ b/loopx/capabilities/value_connectors/planner.py @@ -271,7 +271,7 @@ def build_value_connector_plan_fixture( channel="GitHub issue", stage="monitor", target_ref="public workflow intake issue", - target_url="https://github.com/huangruiteng/loopx/issues/670", + target_url="https://github.com/loopx-project/loopx/issues/670", access_mode="public_metadata_only", external_reads_allowed=True, value_axis="demand", @@ -286,7 +286,7 @@ def build_value_connector_plan_fixture( channel="GitHub discussion", stage="monitor", target_ref="public workflow discussion", - target_url="https://github.com/huangruiteng/loopx/discussions/673", + target_url="https://github.com/loopx-project/loopx/discussions/673", access_mode="public_metadata_only", external_reads_allowed=True, value_axis="demand", diff --git a/loopx/cli_commands/first_run_report.py b/loopx/cli_commands/first_run_report.py index 4b0f10f5d6..1a4f8d1f66 100644 --- a/loopx/cli_commands/first_run_report.py +++ b/loopx/cli_commands/first_run_report.py @@ -15,7 +15,7 @@ None, ] -FIRST_RUN_ISSUE_URL = "https://github.com/huangruiteng/loopx/issues/new" +FIRST_RUN_ISSUE_URL = "https://github.com/loopx-project/loopx/issues/new" FIRST_RUN_ISSUE_TEMPLATE = "first_run.yml" diff --git a/loopx/configuration_catalog.py b/loopx/configuration_catalog.py index 7448b42093..ee93f988bb 100644 --- a/loopx/configuration_catalog.py +++ b/loopx/configuration_catalog.py @@ -167,7 +167,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "docs/quota-allocation.md#completed-todo-review-cadence", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "docs/quota-allocation.md#completed-todo-review-cadence" ), }, @@ -216,7 +216,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md" ), }, @@ -280,7 +280,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "docs/integrations/codex-subagent-orchestration.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "docs/integrations/codex-subagent-orchestration.md" ), }, @@ -341,7 +341,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "docs/integrations/codex-subagent-orchestration.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "docs/integrations/codex-subagent-orchestration.md" ), }, @@ -390,7 +390,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "loopx/capabilities/explore/README.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "loopx/capabilities/explore/README.md" ), }, @@ -443,7 +443,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "loopx/capabilities/explore/README.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "loopx/capabilities/explore/README.md" ), }, @@ -535,7 +535,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "loopx/capabilities/change_quality/README.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "loopx/capabilities/change_quality/README.md" ), }, @@ -630,7 +630,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "loopx/capabilities/reward_memory/README.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "loopx/capabilities/reward_memory/README.md" ), }, @@ -701,7 +701,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "loopx/extensions/lark/docs/lark-event-inbox.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "loopx/extensions/lark/docs/lark-event-inbox.md" ), }, @@ -758,7 +758,7 @@ def build_goal_configuration_catalog( "documentation": { "path": "docs/integrations/lark-kanban-control-plane-adapter.md", "url": ( - "https://github.com/huangruiteng/loopx/blob/main/" + "https://github.com/loopx-project/loopx/blob/main/" "docs/integrations/lark-kanban-control-plane-adapter.md" ), }, diff --git a/packages/dsh-loopx-plugin/README.md b/packages/dsh-loopx-plugin/README.md index 53a53d3b1a..75f9bd02de 100644 --- a/packages/dsh-loopx-plugin/README.md +++ b/packages/dsh-loopx-plugin/README.md @@ -58,7 +58,7 @@ Install the prebuilt release into the web profile: ```bash dsh plugin --profile web add \ - "https://github.com/huangruiteng/loopx/releases/download/dsh-loopx-plugin-v0.1.1-beta.5/dsh-loopx-plugin-0.1.1-beta.5.tgz" + "https://github.com/loopx-project/loopx/releases/download/dsh-loopx-plugin-v0.1.1-beta.5/dsh-loopx-plugin-0.1.1-beta.5.tgz" ``` The prebuilt release above retains its original DSH compatibility. This source diff --git a/packages/dsh-loopx-plugin/package.json b/packages/dsh-loopx-plugin/package.json index 64767d92ff..f70b9e9426 100644 --- a/packages/dsh-loopx-plugin/package.json +++ b/packages/dsh-loopx-plugin/package.json @@ -113,7 +113,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/huangruiteng/loopx.git", + "url": "git+https://github.com/loopx-project/loopx.git", "directory": "packages/dsh-loopx-plugin" }, "license": "Apache-2.0", diff --git a/packages/loopx-community-discussion/README.md b/packages/loopx-community-discussion/README.md index a28b5b2725..2bc90a832a 100644 --- a/packages/loopx-community-discussion/README.md +++ b/packages/loopx-community-discussion/README.md @@ -60,8 +60,8 @@ Direct CLI: ```bash loopx-community-discussion --doctor -loopx-community-discussion scan --owner huangruiteng --repo loopx --days 14 --format json -loopx-community-discussion scan --owner huangruiteng --repo loopx --days 14 --format md +loopx-community-discussion scan --owner loopx-project --repo loopx --days 14 --format json +loopx-community-discussion scan --owner loopx-project --repo loopx --days 14 --format md ``` `schemas/fact.schema.json`, `schemas/scan.schema.json`, and the request/response diff --git a/packages/loopx-community-discussion/examples/request.json b/packages/loopx-community-discussion/examples/request.json index 925c614f15..0fc240e206 100644 --- a/packages/loopx-community-discussion/examples/request.json +++ b/packages/loopx-community-discussion/examples/request.json @@ -1,6 +1,6 @@ { "schema_version": "loopx_community_discussion_request_v0", - "owner": "huangruiteng", + "owner": "loopx-project", "repo": "loopx", "days": 14 } diff --git a/packages/loopx-repo-health/examples/request.json b/packages/loopx-repo-health/examples/request.json index 77cef29374..a6b5095ecd 100644 --- a/packages/loopx-repo-health/examples/request.json +++ b/packages/loopx-repo-health/examples/request.json @@ -1,5 +1,5 @@ { - "owner": "huangruiteng", + "owner": "loopx-project", "repo": "loopx", "schema_version": "loopx_repo_health_request_v0" } From c75e84198ff07408b577f6fefcf5f82c522dbb1d Mon Sep 17 00:00:00 2001 From: DJC1412 <108855841+DJC1412@users.noreply.github.com> Date: Sun, 20 Sep 2026 11:16:55 +0800 Subject: [PATCH 2/4] fix(community-discussion): keep classifying the project after the move STRONG_TERMS named only the pre-transfer owner and repo URL, so a mention of the canonical loopx-project/loopx address was graded weak: the provider downgraded this project's own current pages. Both addresses are strong now, and the smoke pins the current form, the archived form and a noise term in one pass. Signed-off-by: DJC1412 <108855841+DJC1412@users.noreply.github.com> --- .../smoke/community_discussion_smoke.py | 51 +++++++++++++++++-- .../loopx_community_discussion/normalize.py | 4 ++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/packages/loopx-community-discussion/smoke/community_discussion_smoke.py b/packages/loopx-community-discussion/smoke/community_discussion_smoke.py index ab73a37528..f26942203d 100644 --- a/packages/loopx-community-discussion/smoke/community_discussion_smoke.py +++ b/packages/loopx-community-discussion/smoke/community_discussion_smoke.py @@ -24,7 +24,7 @@ def _fixture() -> dict: external = make_fact( fact_type="external_discussion", source="github", - source_url="https://github.com/huangruiteng/loopx/issues/1", + source_url="https://github.com/loopx-project/loopx/issues/1", title="Question: how do I set a stop condition?", author="external-user", published_at="2026-08-17T00:00:00Z", @@ -32,7 +32,7 @@ def _fixture() -> dict: maintainer = make_fact( fact_type="maintainer_signal", source="github", - source_url="https://github.com/huangruiteng/loopx/issues/2", + source_url="https://github.com/loopx-project/loopx/issues/2", title="RFC: typed handoff packets", author="huangruiteng", published_at="2026-08-16T00:00:00Z", @@ -58,7 +58,7 @@ def _fixture() -> dict: duplicate = make_fact( fact_type="external_discussion", source="github", - source_url="https://github.com/huangruiteng/loopx/issues/1", + source_url="https://github.com/loopx-project/loopx/issues/1", title="Question: how do I set a stop condition?", author="external-user", published_at="2026-08-17T00:00:00Z", @@ -68,7 +68,7 @@ def _fixture() -> dict: "schema_version": SCAN_SCHEMA_VERSION, "scan_at": "2026-08-17T00:00:00+00:00", "window_days": 14, - "repo": {"owner": "huangruiteng", "name": "loopx"}, + "repo": {"owner": "loopx-project", "name": "loopx"}, "stats": { "raw_facts": 5, "deduped_facts": len(facts), @@ -80,7 +80,48 @@ def _fixture() -> dict: } +def _pre_and_post_transfer_addresses_are_both_strong() -> int: + """The move must not downgrade the project's own canonical pages.""" + + current = make_fact( + fact_type="external_discussion", + source="github", + source_url="https://github.com/loopx-project/loopx/issues/7", + title="Question from a new user", + author="external-user", + published_at="2026-09-19T00:00:00Z", + ) + archived = make_fact( + fact_type="external_discussion", + source="github", + source_url="https://github.com/huangruiteng/loopx/issues/8", + title="Archived question that still cites the old address", + author="external-user", + published_at="2026-09-18T00:00:00Z", + ) + noise = make_fact( + fact_type="ecosystem_signal", + source="hacker_news", + source_url="https://news.ycombinator.com/item?id=9", + title="We loopear the backlog every sprint", + author="someone", + published_at="2026-09-17T00:00:00Z", + ) + for label, fact in (("current", current), ("archived", archived)): + if fact is None or fact["relevance"] != "strong": + print(f"FAIL: {label} project address should classify strong, got {fact}") + return 1 + if noise is not None: + print(f"FAIL: noise term must stay dropped, got {noise}") + return 1 + return 0 + + def _run_offline() -> int: + status = _pre_and_post_transfer_addresses_are_both_strong() + if status: + return status + fixture = _fixture() if len(fixture["facts"]) != 4: print(f"FAIL: dedupe should collapse duplicate facts, got {len(fixture['facts'])}", file=sys.stderr) @@ -131,7 +172,7 @@ def _run_offline() -> int: return 1 md = render_markdown(fixture) - if "## External discussions" not in md or "https://github.com/huangruiteng/loopx/issues/1" not in md: + if "## External discussions" not in md or "https://github.com/loopx-project/loopx/issues/1" not in md: print("FAIL: markdown digest missing external discussion section", file=sys.stderr) return 1 if "## Public adoption & recommendations" not in md or "we-standardize-on-loopx" not in md: diff --git a/packages/loopx-community-discussion/src/loopx_community_discussion/normalize.py b/packages/loopx-community-discussion/src/loopx_community_discussion/normalize.py index b98a8d8e62..79c63f1f36 100644 --- a/packages/loopx-community-discussion/src/loopx_community_discussion/normalize.py +++ b/packages/loopx-community-discussion/src/loopx_community_discussion/normalize.py @@ -17,12 +17,16 @@ "loopear", ) +# Both repository addresses are strong: the project moved to `loopx-project` and +# archived pages, release notes and citations still name the pre-transfer one. STRONG_TERMS = ( "huangruiteng", "loop engineering", "agent control plane", "local-first agent", "github.com/huangruiteng/loopx", + "loopx-project", + "github.com/loopx-project/loopx", "loopx project", "loopx repo", ) From 805dab5e22b0eb5148e4408a93f58836eb230c20 Mon Sep 17 00:00:00 2001 From: DJC1412 <108855841+DJC1412@users.noreply.github.com> Date: Sun, 20 Sep 2026 11:16:58 +0800 Subject: [PATCH 3/4] test(hygiene): fail when a live surface names the pre-transfer address Code, scripts, workflows and package surfaces must carry the canonical repository address; prose may still cite the historical one, the provider disambiguation list must match both, and the packaged chat bundle is excluded because it is a build product fixed by rebuilding. Signed-off-by: DJC1412 <108855841+DJC1412@users.noreply.github.com> --- examples/repository-hygiene-smoke.py | 81 ++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/examples/repository-hygiene-smoke.py b/examples/repository-hygiene-smoke.py index a2988a33fa..3168f17e64 100644 --- a/examples/repository-hygiene-smoke.py +++ b/examples/repository-hygiene-smoke.py @@ -26,6 +26,52 @@ FIRST_PUBLIC_RELEASE = (0, 1, 3) VERSION_TAG_RE = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$") +CANONICAL_REPO = "loopx-project/loopx" +PRE_TRANSFER_REPO_URL = "github.com/huangruiteng/loopx" +# Surfaces that hand an address to a user, a host or another tool at run time. +LIVE_CODE_PREFIXES = ("loopx/", "scripts/", ".github/workflows/") +LIVE_PACKAGE_MARKERS = ("/src/", "/package.json", "/examples/request.json", "/smoke/") +DOCUMENT_SUFFIXES = (".md", ".html", ".txt") +# Prose may cite the address an event happened under; a code or config default +# may not, because it is what a shipped release keeps handing out. +HISTORICAL_CITATION_RE = re.compile( + r"^/(pull|issues|commit|releases/download|archive)/[0-9A-Za-z]" +) +# The provider's project-disambiguation list and its smoke are the declared +# exceptions: both must keep matching archived pages that cite the old address. +DISAMBIGUATION_SOURCES = ( + "packages/loopx-community-discussion/src/loopx_community_discussion/normalize.py", + "packages/loopx-community-discussion/smoke/community_discussion_smoke.py", +) +DISAMBIGUATION_TERMS_SOURCE = DISAMBIGUATION_SOURCES[0] +# The packaged chat bundle is a build product, so a stale address inside it is +# fixed by rebuilding rather than by hand-editing minified output. +GENERATED_ASSET_PREFIXES = ("loopx/web/chat/assets/",) + + +def _is_checked_surface(name: str) -> bool: + if name in DISAMBIGUATION_SOURCES: + return False + if name.startswith(GENERATED_ASSET_PREFIXES): + return False + if name.startswith(LIVE_CODE_PREFIXES): + return True + return name.startswith("packages/") and any( + marker in name for marker in LIVE_PACKAGE_MARKERS + ) + + +def _stale_live_pointer(text: str) -> str | None: + """Return the first old-address use that is a live pointer, not a citation.""" + + prose = text.endswith(DOCUMENT_SUFFIXES) or "/README" in text + for match in re.finditer(re.escape(PRE_TRANSFER_REPO_URL), text): + tail = text[match.end() : match.end() + 32] + if prose and HISTORICAL_CITATION_RE.match(tail): + continue + return tail.split("\n", 1)[0][:40] + return None + def tracked_files() -> set[str]: completed = subprocess.run( @@ -101,6 +147,40 @@ def release_tags() -> list[str]: return tags + +def validate_canonical_repository_pointer() -> None: + """Fail when a live surface still hands out the pre-transfer address. + + The organization migration left GitHub redirecting the old URL, so nothing + fails loudly: a shipped first-run link, a projected documentation pointer or + a code default could keep naming the previous owner indefinitely. Prose may + still cite the address an event happened under, and the disambiguation list + must match both, so those are the declared exceptions rather than a widening + allowlist. + """ + offenders: list[str] = [] + for name in sorted(tracked_files()): + if not _is_checked_surface(name): + continue + stale = _stale_live_pointer( + (REPO_ROOT / name).read_text(encoding="utf-8", errors="replace") + ) + if stale is not None: + offenders.append(f"{name} ({stale})") + if offenders: + raise AssertionError( + f"live surfaces must name the canonical {CANONICAL_REPO}; " + f"{PRE_TRANSFER_REPO_URL} still appears in: {offenders}" + ) + terms = (REPO_ROOT / DISAMBIGUATION_TERMS_SOURCE).read_text(encoding="utf-8") + for address in (CANONICAL_REPO, PRE_TRANSFER_REPO_URL.removeprefix("github.com/")): + if f"github.com/{address}" not in terms: + raise AssertionError( + f"project disambiguation terms dropped {address}; current and archived " + "pages must both classify as this project" + ) + + def validate_release_timeline() -> None: if not RELEASE_TIMELINE.is_file(): raise AssertionError(f"missing release timeline: {RELEASE_TIMELINE.relative_to(REPO_ROOT)}") @@ -122,6 +202,7 @@ def main() -> int: files = tracked_files() validate_required_tracked_files(files) validate_public_private_boundary() + validate_canonical_repository_pointer() validate_release_timeline() print("repository-hygiene-smoke ok") return 0 From 38cf0b9afb9d83ee920764db365fb0ab1118d290 Mon Sep 17 00:00:00 2001 From: DJC1412 <108855841+DJC1412@users.noreply.github.com> Date: Sun, 20 Sep 2026 18:07:10 +0800 Subject: [PATCH 4/4] test(hygiene): decide live addresses by path and use, not by prose The first cut guessed whether a file was prose from its own contents, so an install command could be excused by unrelated text sharing the file, and the package READMEs whose install commands this branch fixed were not covered by the guard at all. Liveness now comes from where a file is, and each occurrence is classified by the path segment that follows the address: repository, issue_form, discussion, release_asset and main_pointer are live, a numbered issue or pull citation is historical only where that path and use are in the reviewed table. Five cases inside the smoke pin the classifier itself, including that a pull citation stays tolerated beside a line mentioning a README, and that the same shape is still a live address in a product module. Also pins a fact with no project identity as weak, so widening the disambiguation terms cannot silently promote unrelated discussion. Signed-off-by: DJC1412 <108855841+DJC1412@users.noreply.github.com> --- examples/repository-hygiene-smoke.py | 170 +++++++++++++----- .../smoke/community_discussion_smoke.py | 15 ++ 2 files changed, 139 insertions(+), 46 deletions(-) diff --git a/examples/repository-hygiene-smoke.py b/examples/repository-hygiene-smoke.py index 3168f17e64..76127ab427 100644 --- a/examples/repository-hygiene-smoke.py +++ b/examples/repository-hygiene-smoke.py @@ -28,49 +28,83 @@ CANONICAL_REPO = "loopx-project/loopx" PRE_TRANSFER_REPO_URL = "github.com/huangruiteng/loopx" -# Surfaces that hand an address to a user, a host or another tool at run time. -LIVE_CODE_PREFIXES = ("loopx/", "scripts/", ".github/workflows/") -LIVE_PACKAGE_MARKERS = ("/src/", "/package.json", "/examples/request.json", "/smoke/") -DOCUMENT_SUFFIXES = (".md", ".html", ".txt") -# Prose may cite the address an event happened under; a code or config default -# may not, because it is what a shipped release keeps handing out. -HISTORICAL_CITATION_RE = re.compile( - r"^/(pull|issues|commit|releases/download|archive)/[0-9A-Za-z]" +OLD_ADDRESS_RE = re.compile( + r'github\.com/huangruiteng/loopx((?:/[^\s"<>)\],]*)?)' ) -# The provider's project-disambiguation list and its smoke are the declared -# exceptions: both must keep matching archived pages that cite the old address. -DISAMBIGUATION_SOURCES = ( - "packages/loopx-community-discussion/src/loopx_community_discussion/normalize.py", - "packages/loopx-community-discussion/smoke/community_discussion_smoke.py", -) -DISAMBIGUATION_TERMS_SOURCE = DISAMBIGUATION_SOURCES[0] -# The packaged chat bundle is a build product, so a stale address inside it is -# fixed by rebuilding rather than by hand-editing minified output. +# A surface is live by where it is, never by what else its text happens to +# contain: it hands an address to a user, a host or another tool at run time, +# or it is the command someone copies. +LIVE_SURFACE_PREFIXES = ("loopx/", "scripts/", ".github/workflows/", "packages/") +# A built bundle is regenerated, not edited, so its baked-in address is fixed by +# the release that rebuilds it. This is the tracked-build-output cost #4677 names. GENERATED_ASSET_PREFIXES = ("loopx/web/chat/assets/",) +# Where the pre-transfer address is the reviewed-correct content, by path and by +# use: this project's own disambiguation terms must keep matching the archived +# address, and prose may cite the pull request an event happened under. +REVIEWED_ADDRESS_EXCEPTIONS: dict[str, frozenset[str]] = { + "packages/loopx-community-discussion/src/loopx_community_discussion/normalize.py": + frozenset({"repository", "issue"}), + "packages/loopx-community-discussion/smoke/community_discussion_smoke.py": + frozenset({"issue"}), + "loopx/capabilities/issue_fix/README.md": frozenset({"pull"}), + "loopx/capabilities/issue_fix/README.zh-CN.md": frozenset({"pull"}), + "packages/loopx-codex-provider-routing/RUNBOOK.md": frozenset({"pull"}), +} +DISAMBIGUATION_TERMS_SOURCE = ( + "packages/loopx-community-discussion/src/loopx_community_discussion/normalize.py" +) -def _is_checked_surface(name: str) -> bool: - if name in DISAMBIGUATION_SOURCES: - return False +def _is_live_surface(name: str) -> bool: if name.startswith(GENERATED_ASSET_PREFIXES): return False - if name.startswith(LIVE_CODE_PREFIXES): - return True - return name.startswith("packages/") and any( - marker in name for marker in LIVE_PACKAGE_MARKERS + return name.startswith(LIVE_SURFACE_PREFIXES) + + +def _address_use(raw_path: str) -> str: + """Classify one old-address occurrence by the path that follows it. + + Only the occurrence itself decides the use, so unrelated text in the same + file cannot turn an install command into a citation or the reverse. + """ + + segments = [part for part in raw_path.strip("/").split("/") if part] + if not segments: + return "repository" + lead = segments[0] + if lead == "issues": + return "issue" if len(segments) > 1 and segments[1].isdigit() else "issue_form" + if lead == "releases": + return "release_asset" + if lead == "discussions": + return "discussion" + if lead == "tree": + return "main_pointer" if len(segments) > 1 and segments[1] == "main" else "branch" + if lead.startswith("."): + return "repository" + return {"pull": "pull", "commit": "commit", "blob": "main_pointer"}.get( + lead, lead ) -def _stale_live_pointer(text: str) -> str | None: - """Return the first old-address use that is a live pointer, not a citation.""" +# A use is either a live pointer this project must own or a dated citation that +# may keep the address the event happened under. +LIVE_ADDRESS_USES = frozenset( + {"repository", "issue_form", "discussion", "release_asset", "main_pointer", "branch"}) - prose = text.endswith(DOCUMENT_SUFFIXES) or "/README" in text - for match in re.finditer(re.escape(PRE_TRANSFER_REPO_URL), text): - tail = text[match.end() : match.end() + 32] - if prose and HISTORICAL_CITATION_RE.match(tail): - continue - return tail.split("\n", 1)[0][:40] - return None + +def stale_address_uses(name: str, text: str) -> list[str]: + """Return the old-address uses in a live surface that were never reviewed.""" + + tolerated = REVIEWED_ADDRESS_EXCEPTIONS.get(name, frozenset()) + return [ + use + for use in ( + _address_use(match.group(1) or "") + for match in OLD_ADDRESS_RE.finditer(text) + ) + if use not in tolerated + ] def tracked_files() -> set[str]: @@ -149,24 +183,24 @@ def release_tags() -> list[str]: def validate_canonical_repository_pointer() -> None: - """Fail when a live surface still hands out the pre-transfer address. - - The organization migration left GitHub redirecting the old URL, so nothing - fails loudly: a shipped first-run link, a projected documentation pointer or - a code default could keep naming the previous owner indefinitely. Prose may - still cite the address an event happened under, and the disambiguation list - must match both, so those are the declared exceptions rather than a widening - allowlist. + """Fail when a live surface hands out the pre-transfer repository address. + + GitHub's redirect made the migration silent: a first-run link, a projected + documentation pointer, an install command or a provider's own relevance + terms could keep naming the previous owner while everything still resolved. + Exceptions are per path and per use, so a reviewed citation cannot be + reclassified by unrelated text in the same file, and a reviewed file cannot + hide an install command. """ offenders: list[str] = [] for name in sorted(tracked_files()): - if not _is_checked_surface(name): + if not _is_live_surface(name): continue - stale = _stale_live_pointer( - (REPO_ROOT / name).read_text(encoding="utf-8", errors="replace") + stale = stale_address_uses( + name, (REPO_ROOT / name).read_text(encoding="utf-8", errors="replace") ) - if stale is not None: - offenders.append(f"{name} ({stale})") + if stale: + offenders.append(f"{name} ({', '.join(sorted(set(stale)))})") if offenders: raise AssertionError( f"live surfaces must name the canonical {CANONICAL_REPO}; " @@ -179,6 +213,50 @@ def validate_canonical_repository_pointer() -> None: f"project disambiguation terms dropped {address}; current and archived " "pages must both classify as this project" ) + _validate_stale_address_classifier() + + +def _validate_stale_address_classifier() -> None: + """Prove the classifier keys on path and use, not on surrounding prose.""" + + install = ( + "curl -L https://github.com/huangruiteng/loopx/releases/download/" + "pkg-v1/pkg.tgz -o pkg.tgz\n" + ) + if stale_address_uses("packages/dsh-loopx-plugin/README.md", install) != [ + "release_asset" + ]: + raise AssertionError( + "an install command under a package README must be named as a live " + "pre-transfer address" + ) + cited = ( + "See the README notes at #12 (https://github.com/huangruiteng/loopx/pull/12)\n" + ) + if stale_address_uses("packages/loopx-codex-provider-routing/RUNBOOK.md", cited): + raise AssertionError( + "a reviewed pull-request citation must stay tolerated even where the " + "same file mentions a README" + ) + if stale_address_uses("loopx/configuration_catalog.py", cited) != ["pull"]: + raise AssertionError( + "a reviewed exception for one path must not tolerate the same shape " + "elsewhere: a pull citation in a product module is still a live address" + ) + pointer = "https://github.com/huangruiteng/loopx/blob/main/docs/x.md\n" + if stale_address_uses("packages/loopx-community-discussion/README.md", pointer) != [ + "main_pointer" + ]: + raise AssertionError("a documentation pointer must be named as a live address") + if _is_live_surface("loopx/web/chat/assets/index-abc123.js"): + raise AssertionError( + "a generated bundle is outside the guard: its address is fixed by the " + "release that rebuilds it, not by hand-editing minified output" + ) + if not _is_live_surface("packages/dsh-loopx-plugin/README.md"): + raise AssertionError( + "an install command under a package README is a live surface" + ) def validate_release_timeline() -> None: diff --git a/packages/loopx-community-discussion/smoke/community_discussion_smoke.py b/packages/loopx-community-discussion/smoke/community_discussion_smoke.py index f26942203d..dee12da70d 100644 --- a/packages/loopx-community-discussion/smoke/community_discussion_smoke.py +++ b/packages/loopx-community-discussion/smoke/community_discussion_smoke.py @@ -99,6 +99,15 @@ def _pre_and_post_transfer_addresses_are_both_strong() -> int: author="external-user", published_at="2026-09-18T00:00:00Z", ) + generic = make_fact( + fact_type="external_discussion", + source="github", + source_url="https://github.com/example/other-tool/issues/3", + title="How do we schedule background jobs", + author="someone", + published_at="2026-09-16T00:00:00Z", + text="a question with no project identity in it", + ) noise = make_fact( fact_type="ecosystem_signal", source="hacker_news", @@ -114,6 +123,12 @@ def _pre_and_post_transfer_addresses_are_both_strong() -> int: if noise is not None: print(f"FAIL: noise term must stay dropped, got {noise}") return 1 + if generic is None or generic["relevance"] != "weak": + print( + "FAIL: a fact with no project identity must stay weak rather than be " + f"promoted or dropped, got {generic}" + ) + return 1 return 0