From 08ab511e2da6ed08d63601e798fb6520911019a3 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Wed, 11 Nov 2026 01:14:29 +0100 Subject: [PATCH 1/2] fix(skills): link the module layout standard on the docs site, not a path the wheel lacks (#875) Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4 --- CHANGELOG.md | 1 + docs/module-layout.md | 15 +++++++++ .../specs/2026-11-11-skill-links-design.md | 15 +++++++++ framework/cli/tests/test_cli_skill_links.py | 31 +++++++++++++++++++ skills/simple-module-conventions/SKILL.md | 2 +- skills/simple-module-creating/SKILL.md | 2 +- 6 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 docs/superpowers/specs/2026-11-11-skill-links-design.md create mode 100644 framework/cli/tests/test_cli_skill_links.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b491466..f77fc98f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to this project are documented in this file. The format is b ### Fixed +- The `simple-module-creating` and `simple-module-conventions` skills, which ship inside `simple_module_cli`, linked the module layout standard as `../../docs/module-layout.md`. That path does not exist in an installed CLI. They now link https://docs.py.simplemodule.dev/module-layout, and a test fails if a shipped skill links a file outside `skills/`. `docs/module-layout.md` gains a "Downstream modules" section on copying this repo's root-layout test (`ALLOWED_ROOT`, `DOMAIN_AREAS`) into an app or a standalone module repo (#875). - `smpy new` warns when the CLI running it is older than the latest `simple_module_cli` on PyPI. A new app pins the framework to the CLI's own version, and `uvx --from simple_module_cli` quietly reuses a copy installed with `uv tool install`, so an old tool used to scaffold an outdated app without saying so. The warning names both fixes: `uv tool upgrade simple_module_cli`, or `uvx --from simple_module_cli@latest`. The check is best effort, with a 3-second timeout; offline or on any PyPI error it is skipped, and it never blocks the scaffold. A CLI that is itself a pre-release, dev or local build is never warned. The README, the CLI and hosting READMEs and the landing page's copy command now use `uvx --from simple_module_cli@latest`, and the installation and quickstart guides, the docs home and the make-commands reference say how to upgrade an installed CLI, with `pipx upgrade` as the alternative (#899). - The first-module guidance now agrees with itself. The guides (`docs/index.md`, quickstart, installation, Your first module) wire a new module with `smpy add ../modules/orders --path host`, the command `smpy create-module` prints, which adds it to `host/pyproject.toml` as a workspace source. They used to say `uv add ./modules/orders`, which in a `smpy new` workspace edits the root `pyproject.toml` instead. A flat app uses `smpy add ./modules/orders`. The landing page `smpy new` generates drops the `create-admin` step (the `/setup` wizard already creates the first administrator), lists the wiring and migration steps after `create-module`, and its "Read the docs" button opens the first-module guide on docs.py.simplemodule.dev instead of the GitHub repo. The first-module guide and the `simple-module-conventions` skill note that SQLModel's `Field(regex=...)` is accepted but not enforced on a DTO, and show a `field_validator` instead (#898). - A fresh app's `make dev` no longer prints Vite errors or warnings, and `npm audit` reports nothing. The `simple_module_settings` wheel no longer ships `ui/components/validation-errors.test.ts`: that test made the host's Vite dependency scan fail with "Failed to run dependency scan … vitest". It now lives in `modules/settings/tests-js/`, and `make ci-build-packages` fails if any wheel contains a JS or TS `*.test.*` or `*.spec.*` file, a `test-dom.tsx` or a `page-tests/` directory. The scaffold's `client_app/vite.config.ts` loads under Vite 8's native config loader: it uses `import.meta.dirname` and `.ts` import extensions, and the template `tsconfig.json` sets `allowImportingTsExtensions`. It also uses `optimizeDeps.rolldownOptions` instead of the deprecated `esbuildOptions`. New apps' root `package.json` overrides `postcss-selector-parser` to `^6.1.5`, which is what `@tailwindcss/typography` pulls in, as this repo does. An existing app can copy `host/client_app/vite.config.ts` and `tsconfig.json` from a new app, and add the override (#900). diff --git a/docs/module-layout.md b/docs/module-layout.md index 58eb845d..35b3c7f2 100644 --- a/docs/module-layout.md +++ b/docs/module-layout.md @@ -171,6 +171,21 @@ root-layout test too. A shim that is a plain re-export instead (not an alias) mu by hand to the expiry test's `_SHIM_MODULES` tuple and `_EXPECTED` set and to the root-layout test's `DEPRECATED_ROOT` map until removed. +## Downstream modules + +A module created with `smpy create-module` already follows this layout. The +check that keeps it that way lives in this repo's +[`host/tests/test_module_layout.py`](https://github.com/antosubash/simple_module_python/blob/main/host/tests/test_module_layout.py), +which an app or a standalone module repo does not have. To keep the same ratchet: + +1. Copy that test into your own tests, for example `tests/test_module_layout.py`. +2. Point `MODULES` at your modules directory (`modules/` in a `smpy new` app). +3. Replace `DOMAIN_AREAS` with your own modules' named areas, and empty + `DEPRECATED_ROOT` unless you keep root-level shims. + +`ALLOWED_ROOT` is the root lookup list in [Package root](#package-root-lookup-paths); +keep the two in step when you upgrade the framework. + ## Moving an existing module One module per PR; no logic changes in a move PR. diff --git a/docs/superpowers/specs/2026-11-11-skill-links-design.md b/docs/superpowers/specs/2026-11-11-skill-links-design.md new file mode 100644 index 00000000..d40e986f --- /dev/null +++ b/docs/superpowers/specs/2026-11-11-skill-links-design.md @@ -0,0 +1,15 @@ +# Shipped skills link to files the wheel contains (#875) + +`skills/` is installed as `simple_module_cli/skills/` (a symlink in the repo). Two skills +linked `../../docs/module-layout.md`, which points outside the installed package. + +## Decisions + +- Link the docs site, `https://docs.py.simplemodule.dev/module-layout` (returns 200), + instead of shipping `docs/` in the wheel. The site follows `main` and needs no new + packaging. A GitHub link at a tag would need a version stamp. +- Add `framework/cli/tests/test_cli_skill_links.py`: every relative link in a shipped + `SKILL.md` must resolve inside `skills/` and exist. It fails on `main` and passes now. +- Add a "Downstream modules" section to `docs/module-layout.md`: copy + `host/tests/test_module_layout.py`, point `MODULES` at the app's modules, and set your + own `DOMAIN_AREAS`. The issue asked for this. diff --git a/framework/cli/tests/test_cli_skill_links.py b/framework/cli/tests/test_cli_skill_links.py new file mode 100644 index 00000000..6cdea6d6 --- /dev/null +++ b/framework/cli/tests/test_cli_skill_links.py @@ -0,0 +1,31 @@ +"""Shipped skills link only to files the CLI wheel contains (GH #875). + +`skills/` is installed as `simple_module_cli/skills/`, so a relative link that +leaves it (`../../docs/...`) points nowhere in a downstream app. Repo docs are +linked through the docs site instead. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +_SKILLS = Path(__file__).resolve().parents[3] / "skills" +_LINK = re.compile(r"\]\(([^)\s]+)\)") + + +def _relative_links(skill_md: Path) -> list[str]: + targets = [m.group(1).split("#", 1)[0] for m in _LINK.finditer(skill_md.read_text())] + return [t for t in targets if t and "://" not in t and not t.startswith("mailto:")] + + +@pytest.mark.parametrize( + "skill_md", sorted(_SKILLS.glob("*/SKILL.md")), ids=lambda p: p.parent.name +) +def test_relative_links_stay_inside_the_shipped_skills(skill_md: Path) -> None: + for target in _relative_links(skill_md): + resolved = (skill_md.parent / target).resolve() + assert resolved.is_relative_to(_SKILLS.resolve()), f"{target} leaves skills/" + assert resolved.exists(), f"{target} does not exist" diff --git a/skills/simple-module-conventions/SKILL.md b/skills/simple-module-conventions/SKILL.md index 783798a3..bd4c632c 100644 --- a/skills/simple-module-conventions/SKILL.md +++ b/skills/simple-module-conventions/SKILL.md @@ -41,7 +41,7 @@ Treat 300 lines as the design pressure on every `.py`, `.ts`, and `.tsx` file (t New module settings and their app-state container live in `core/settings.py` and `core/services.py`; HTTP dependencies and services live in `web/`. Keep root lookup files in place and put non-page frontend code in `ui/`. See -[Module layout](../../docs/module-layout.md). +[Module layout](https://docs.py.simplemodule.dev/module-layout). ### 3. Per-module settings: `register_module_settings` → `app.state.` diff --git a/skills/simple-module-creating/SKILL.md b/skills/simple-module-creating/SKILL.md index 289bad1a..3537d0c1 100644 --- a/skills/simple-module-creating/SKILL.md +++ b/skills/simple-module-creating/SKILL.md @@ -66,7 +66,7 @@ Modules use this area layout. Area initializers hold only a docstring. Private helpers stay in their area. Tests have no `__init__.py`, keep `conftest.py` at their root and use globally unique `test_orders_*.py` basenames; JS tests stay in `tests-js/`. -See [Module layout](../../docs/module-layout.md) for the lookup consumers and shim policy. +See [Module layout](https://docs.py.simplemodule.dev/module-layout) for the lookup consumers and shim policy. ## The contract: pyproject.toml + module.py From 6ca320abfa30fb0e287690b08d647933d1a69263 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Wed, 11 Nov 2026 01:16:41 +0100 Subject: [PATCH 2/2] fix: address code review findings (round 1, pass 1) Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4 --- docs/module-layout.md | 6 +++++- docs/superpowers/specs/2026-11-11-skill-links-design.md | 5 +++-- framework/cli/tests/test_cli_skill_links.py | 9 +++++---- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/module-layout.md b/docs/module-layout.md index 35b3c7f2..3970052e 100644 --- a/docs/module-layout.md +++ b/docs/module-layout.md @@ -179,7 +179,11 @@ check that keeps it that way lives in this repo's which an app or a standalone module repo does not have. To keep the same ratchet: 1. Copy that test into your own tests, for example `tests/test_module_layout.py`. -2. Point `MODULES` at your modules directory (`modules/` in a `smpy new` app). +2. Point `MODULES` at your modules directory (`modules/` in a `smpy new` app). The + copy computes it with `parents[2]`, which is right for `host/tests/`; from + `tests/test_module_layout.py` use `parents[1]`. + A standalone module repo has the package at its root, with no `modules//` + level, so change the `package`, `pyproject.toml` and `tests/` paths in the test too. 3. Replace `DOMAIN_AREAS` with your own modules' named areas, and empty `DEPRECATED_ROOT` unless you keep root-level shims. diff --git a/docs/superpowers/specs/2026-11-11-skill-links-design.md b/docs/superpowers/specs/2026-11-11-skill-links-design.md index d40e986f..aaa5a66f 100644 --- a/docs/superpowers/specs/2026-11-11-skill-links-design.md +++ b/docs/superpowers/specs/2026-11-11-skill-links-design.md @@ -8,8 +8,9 @@ linked `../../docs/module-layout.md`, which points outside the installed package - Link the docs site, `https://docs.py.simplemodule.dev/module-layout` (returns 200), instead of shipping `docs/` in the wheel. The site follows `main` and needs no new packaging. A GitHub link at a tag would need a version stamp. -- Add `framework/cli/tests/test_cli_skill_links.py`: every relative link in a shipped - `SKILL.md` must resolve inside `skills/` and exist. It fails on `main` and passes now. +- Add `framework/cli/tests/test_cli_skill_links.py`: every relative link (inline, + titled, angle-bracket or reference-style, outside code fences) in any Markdown file + under `skills/` must resolve inside `skills/` and exist. It fails on `main` and passes now. - Add a "Downstream modules" section to `docs/module-layout.md`: copy `host/tests/test_module_layout.py`, point `MODULES` at the app's modules, and set your own `DOMAIN_AREAS`. The issue asked for this. diff --git a/framework/cli/tests/test_cli_skill_links.py b/framework/cli/tests/test_cli_skill_links.py index 6cdea6d6..32621775 100644 --- a/framework/cli/tests/test_cli_skill_links.py +++ b/framework/cli/tests/test_cli_skill_links.py @@ -13,16 +13,17 @@ import pytest _SKILLS = Path(__file__).resolve().parents[3] / "skills" -_LINK = re.compile(r"\]\(([^)\s]+)\)") +_LINK = re.compile(r"\]\(]+)>?(?:\s+\"[^\"]*\")?\)|^\s*\[[^\]]+\]:\s*?\s*$", re.M) def _relative_links(skill_md: Path) -> list[str]: - targets = [m.group(1).split("#", 1)[0] for m in _LINK.finditer(skill_md.read_text())] - return [t for t in targets if t and "://" not in t and not t.startswith("mailto:")] + text = re.sub(r"^(```|~~~).*?^\1", "", skill_md.read_text(), flags=re.M | re.S) + targets = [(m.group(1) or m.group(2)).split("#", 1)[0] for m in _LINK.finditer(text)] + return [t for t in targets if t and not re.match(r"[a-z][a-z0-9+.-]*:", t, re.I)] @pytest.mark.parametrize( - "skill_md", sorted(_SKILLS.glob("*/SKILL.md")), ids=lambda p: p.parent.name + "skill_md", sorted(_SKILLS.glob("**/*.md")), ids=lambda p: str(p.relative_to(_SKILLS)) ) def test_relative_links_stay_inside_the_shipped_skills(skill_md: Path) -> None: for target in _relative_links(skill_md):