Skip to content

Commit 5645926

Browse files
OriNachumclaude
andcommitted
feat: M4 polish — pip-resolvable /simple/, llms.txt, catalog.json, md twins
Completes the machine-usability story for tools.culture.dev. pip-resolvable /simple/: - `index build` now emits a Cloudflare `_redirects` file mapping each certified tool's `/simple/<name>/` (both trailing-slash forms) to its PyPI simple page, so `pip install --index-url https://tools.culture.dev/simple/ <tool>` resolves a certified tool's real files from PyPI. Only listed tools get a rule → the index stays curated; uncertified names 404. - Dropped the static per-tool `/simple/<name>/` pages: a static asset shadows the redirect (Cloudflare applies `_redirects` only after an exact asset miss), so a page there would break pip-resolvability. New `render_redirects()` replaces `render_project`/`pypi_links`. sync-catalog.sh + catalog-refresh CI distribute the file to public/_redirects. Agent affordances on the site: - `/llms.txt` — the agent-facing index, built from the catalog. - `/catalog.json` — the machine-readable catalog as a stable endpoint. - markdown twins — `/index.md` and `/tools/<name>.md` (append `.md` to any page). - Footer links the new endpoints. 48 pytest (render_redirects + build assertions updated); astro check 0 errors, build green (new routes: llms.txt, catalog.json, *.md); lint + rubric + markdownlint clean. Version 0.5.1 -> 0.6.0; catalog regenerated to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LdsNvR24i1fGosuQ9AgtVG
1 parent 1ab673b commit 5645926

19 files changed

Lines changed: 265 additions & 121 deletions

File tree

.github/workflows/catalog-refresh.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ jobs:
9999
cp _stage/catalog.json site-astro/src/data/catalog.json
100100
rm -rf site-astro/public/simple
101101
cp -r _stage/simple site-astro/public/simple
102+
cp _stage/_redirects site-astro/public/_redirects
102103
rm -rf _siblings _stage
103104
104105
- name: Open a PR if the catalog changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
55
Format follows [Keep a Changelog](https://keepachangelog.com/). This project
66
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.6.0] - 2026-06-22
9+
10+
### Added
11+
12+
- **pip-resolvable `/simple/` (M4).** `index build` now emits a Cloudflare
13+
`_redirects` file mapping each certified tool's `/simple/<name>/` to its PyPI
14+
simple page, so `pip install --index-url https://tools.culture.dev/simple/
15+
<tool>` resolves a certified tool's real files from PyPI. Only listed tools get
16+
a rule, so the index stays curated; uncertified names fall through to 404. The
17+
static per-tool `/simple/<name>/` pages are gone — a static asset would shadow
18+
the redirect (Cloudflare applies `_redirects` only after an exact asset miss).
19+
New `render_redirects()` in `culture_tools.index._simple`; `sync-catalog.sh`
20+
and the catalog-refresh CI distribute the file to `public/_redirects`.
21+
- **Agent affordances on the site (M4):** `/llms.txt` (the agent-facing index,
22+
built from the catalog), `/catalog.json` (the machine-readable catalog as a
23+
stable endpoint), and **markdown twins** — append `.md` to any page
24+
(`/index.md`, `/tools/<name>.md`) for raw-markdown versions. Footer links the
25+
new endpoints.
26+
827
## [0.5.1] - 2026-06-22
928

1029
### Added

culture_tools/index/_build.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,7 @@
2121
from culture_tools.index._conformance import Runner, gate
2222
from culture_tools.index._introspect import introspect
2323
from culture_tools.index._manifest import Tool, candidates, default_repos_dir
24-
from culture_tools.index._simple import normalize, pypi_links, render_project, render_root
25-
26-
_SIMPLE_NOTE = (
27-
"Distribution files are served by PyPI; tools.culture.dev certifies the AgentFront contract."
28-
)
24+
from culture_tools.index._simple import render_redirects, render_root
2925

3026

3127
def build(
@@ -58,23 +54,25 @@ def build(
5854
catalog_path = out_dir / "catalog.json"
5955
catalog_path.write_text(json.dumps(catalog, indent=2) + "\n", encoding="utf-8")
6056

57+
# PEP 503 is keyed by the installable (PyPI) name, and so is pip's request
58+
# path — so the root listing and the redirect rules both key on tool.pypi.
59+
pypi_names = [t.pypi for t in listed_tools]
60+
6161
simple_dir = out_dir / "simple"
6262
simple_dir.mkdir(parents=True, exist_ok=True)
63-
(simple_dir / "index.html").write_text(
64-
render_root([t.name for t in listed_tools]), encoding="utf-8"
65-
)
66-
for tool in listed_tools:
67-
project_dir = simple_dir / normalize(tool.name)
68-
project_dir.mkdir(parents=True, exist_ok=True)
69-
(project_dir / "index.html").write_text(
70-
render_project(tool.name, pypi_links(tool.pypi), note=_SIMPLE_NOTE),
71-
encoding="utf-8",
72-
)
63+
(simple_dir / "index.html").write_text(render_root(pypi_names), encoding="utf-8")
64+
65+
# pip-resolvability: /simple/<name>/ → PyPI, as a Cloudflare _redirects file at
66+
# the site root. No static per-tool page is written — a static asset would
67+
# shadow the redirect (Cloudflare applies _redirects only after an asset miss).
68+
redirects_path = out_dir / "_redirects"
69+
redirects_path.write_text(render_redirects(pypi_names), encoding="utf-8")
7370

7471
return {
7572
"out": str(out_dir),
7673
"catalog": str(catalog_path),
7774
"simple": str(simple_dir),
75+
"redirects": str(redirects_path),
7876
"listed": len(entries),
7977
"excluded": len(excluded),
8078
"candidates": len(manifest),

culture_tools/index/_simple.py

Lines changed: 30 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,34 @@
1-
"""Static PEP 503 ``/simple/`` emitter.
1+
"""Static PEP 503 ``/simple/`` emitter + the pip-resolvability ``_redirects``.
22
33
Pure string builders — no I/O, no deps — so they unit-test cleanly. Ported from
4-
``../auntiepypi``'s index logic (PEP 503 name normalization + the root/project
5-
HTML), adapted to emit *static* files at build time rather than serve them from a
4+
``../auntiepypi``'s index logic (PEP 503 name normalization + the root HTML),
5+
adapted to emit *static* files at build time rather than serve them from a
66
running ``http.server``.
77
8-
v1 does not host wheels: each project page links out to the tool's canonical PyPI
9-
distribution page. The pip-resolvable ``/simple/<name>/`` → PyPI proxy is a
10-
deploy-layer concern (a Cloudflare ``_redirects`` rule), handled in M3.
8+
The index does not host wheels. To make ``/simple/`` **pip-resolvable** without
9+
hosting files, each certified tool's ``/simple/<name>/`` is a Cloudflare
10+
``_redirects`` rule pointing at the tool's real PyPI simple page. Only listed
11+
tools get a rule, so the index stays curated — ``pip install --index-url
12+
https://tools.culture.dev/simple/ <tool>`` resolves a certified tool's files from
13+
PyPI, and anything uncertified falls through to 404. There is deliberately **no
14+
static page** at ``/simple/<name>/``: a static asset would shadow the redirect
15+
(Cloudflare applies ``_redirects`` only after an exact static-asset miss).
1116
"""
1217

1318
from __future__ import annotations
1419

1520
import html
1621
import re
17-
from dataclasses import dataclass
1822

1923
_NORMALIZE_RE = re.compile(r"[-_.]+")
24+
_PYPI_SIMPLE = "https://pypi.org/simple"
2025

2126

2227
def normalize(name: str) -> str:
2328
"""PEP 503 normalized project name: lowercase, runs of ``[-_.]`` → single ``-``."""
2429
return _NORMALIZE_RE.sub("-", name).lower()
2530

2631

27-
@dataclass(frozen=True)
28-
class DistLink:
29-
"""One anchor on a project page (a distribution file, or a PyPI pointer)."""
30-
31-
label: str
32-
href: str
33-
34-
3532
def render_root(project_names: list[str]) -> str:
3633
"""The ``/simple/index.html`` root: one anchor per project, sorted, normalized."""
3734
rows = [
@@ -54,37 +51,23 @@ def render_root(project_names: list[str]) -> str:
5451
)
5552

5653

57-
def render_project(name: str, links: list[DistLink], *, note: str = "") -> str:
58-
"""A ``/simple/<name>/index.html`` page listing distribution anchors."""
59-
rows = [
60-
f' <a href="{html.escape(link.href)}">{html.escape(link.label)}</a><br/>'
61-
for link in links
62-
]
63-
body = "\n".join(rows) if rows else " <!-- no distributions -->"
64-
note_html = f" <!-- {html.escape(note)} -->\n" if note else ""
65-
return (
66-
"<!DOCTYPE html>\n"
67-
'<html lang="en">\n'
68-
" <head>\n"
69-
' <meta charset="utf-8" />\n'
70-
' <meta name="pypi:repository-version" content="1.0" />\n'
71-
f" <title>Links for {html.escape(name)}</title>\n"
72-
" </head>\n"
73-
" <body>\n"
74-
f" <h1>Links for {html.escape(name)}</h1>\n"
75-
f"{note_html}"
76-
f"{body}\n"
77-
" </body>\n"
78-
"</html>\n"
79-
)
80-
54+
def render_redirects(project_names: list[str]) -> str:
55+
"""The Cloudflare ``_redirects`` file making ``/simple/`` pip-resolvable.
8156
82-
def pypi_links(pypi_name: str) -> list[DistLink]:
83-
"""v1 project-page links: point at the tool's canonical PyPI home."""
84-
return [
85-
DistLink(f"{pypi_name} on PyPI", f"https://pypi.org/project/{pypi_name}/"),
86-
DistLink(
87-
"PyPI simple index (installable files)",
88-
f"https://pypi.org/simple/{normalize(pypi_name)}/",
89-
),
57+
One rule per certified tool, mapping ``/simple/<name>/`` (and the
58+
trailing-slash-less form pip may also request) to the tool's PyPI simple page.
59+
Sorted + normalized; only listed tools resolve.
60+
"""
61+
lines = [
62+
"# Generated by `culture-tools index build` — do not edit by hand.",
63+
"# pip-resolvable /simple/: each AgentFront-certified tool redirects to its",
64+
"# PyPI simple page (the real distribution files). Only listed tools resolve,",
65+
"# so the index stays curated; uncertified names fall through to 404.",
9066
]
67+
for name in sorted(project_names, key=normalize):
68+
norm = normalize(name)
69+
target = f"{_PYPI_SIMPLE}/{norm}/"
70+
lines.append(f"/simple/{norm}/ {target} 302")
71+
lines.append(f"/simple/{norm} {target} 302")
72+
lines.append("")
73+
return "\n".join(lines)

docs/design/tools-culture-dev.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,12 @@ Each milestone is one PR (this repo bumps the version every PR; the
114114
live conformance via `index build --repos-dir <cloned siblings>` and opens a PR
115115
(human-reviewed before a merge triggers the rebuild). Still open: `_redirects`
116116
for a pip-resolvable `/simple/`.
117-
- **M4 — Polish.** Agent affordances: `llms.txt`, markdown twins, sitemap,
118-
conformance badges, SEO. (S3 durable tier can land here or later.)
117+
- **M4 — Polish.** *In progress.* Done: **pip-resolvable `/simple/`** (a
118+
Cloudflare `_redirects` file maps each certified tool's `/simple/<name>/` to its
119+
PyPI simple page — curated, so only listed tools resolve), **`/llms.txt`**, a
120+
**`/catalog.json`** endpoint, and **markdown twins** (`/index.md`,
121+
`/tools/<name>.md`). Sitemap shipped in M2. Remaining: conformance badge SVGs,
122+
richer SEO, and the S3 durable tier.
119123

120124
## Open items (deferred, not blocking M1)
121125

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "culture-tools"
3-
version = "0.5.1"
3+
version = "0.6.0"
44
description = "tools.culture.dev — the package index for agent-first CLI tools that conform to the agentfront contract."
55
readme = "README.md"
66
license = "Apache-2.0"

site-astro/public/_redirects

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Generated by `culture-tools index build` — do not edit by hand.
2+
# pip-resolvable /simple/: each AgentFront-certified tool redirects to its
3+
# PyPI simple page (the real distribution files). Only listed tools resolve,
4+
# so the index stays curated; uncertified names fall through to 404.
5+
/simple/agentfront/ https://pypi.org/simple/agentfront/ 302
6+
/simple/agentfront https://pypi.org/simple/agentfront/ 302
7+
/simple/colleague/ https://pypi.org/simple/colleague/ 302
8+
/simple/colleague https://pypi.org/simple/colleague/ 302
9+
/simple/culture-tools/ https://pypi.org/simple/culture-tools/ 302
10+
/simple/culture-tools https://pypi.org/simple/culture-tools/ 302

site-astro/public/simple/agentfront/index.html

Lines changed: 0 additions & 14 deletions
This file was deleted.

site-astro/public/simple/colleague/index.html

Lines changed: 0 additions & 14 deletions
This file was deleted.

site-astro/public/simple/culture-tools/index.html

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)