Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions autoassistant/audit_skill_apis.py
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,117 @@ def write_provenance(root: Path, only: Optional[list[Path]] = None) -> int:
return 0


# ---------------------------------------------------------------------------
# Citation-path check (--check-citations)
# ---------------------------------------------------------------------------
# The symbol audit and the citation paths are independent failure axes: a page can
# cite only live `al.*` symbols while its `Project:relative/path` source citations
# point at a pre-refactor file layout (module → package moves, deleted files). This
# check resolves every citation — inline backticked `<project>:<path>` in the body
# and each `sources[].paths[]` entry in wiki frontmatter — against a real checkout.


def _sources_projects(root: Path) -> list[str]:
"""Project names from sources.yaml — the canonical set a citation may reference."""
if yaml is None:
return list(PROJECT_IMPORT)
try:
data = yaml.safe_load((root / "sources.yaml").read_text(encoding="utf-8")) or {}
names = [p["name"] for p in data.get("projects", []) if isinstance(p, dict) and p.get("name")]
return names or list(PROJECT_IMPORT)
except Exception: # noqa: BLE001
return list(PROJECT_IMPORT)


def _project_tree(project: str, root: Path) -> Optional[Path]:
"""A directory holding `project`'s source tree: the installed checkout
(_project_repo), else `sources/<project>/`, else a sibling clone of this repo.
None when nothing is present — the caller downgrades to a warning."""
repo = _project_repo(project)
if repo is not None:
return repo
for candidate in (root / "sources" / project, root.parent / project):
if candidate.is_dir():
return candidate
return None


def check_citations(root: Path) -> int:
"""Resolve every `Project:path` citation in skills/, wiki/core/, AGENTS.md and
llms.txt against a checkout of the cited project. A path containing `...` is a
deliberate abbreviation — only its concrete prefix is required to exist. Exit 1
on any missing path; unresolvable project trees are warnings (packaged install
with no clone), missing paths are errors."""
projects = _sources_projects(root)
cite_re = re.compile(
r"`(" + "|".join(re.escape(p) for p in projects) + r"):([A-Za-z0-9_./\-]+?)`"
)
files = sorted((root / "skills").glob("*.md"))
files += sorted((root / "wiki" / "core").rglob("*.md"))
files += [p for p in (root / "AGENTS.md", root / "llms.txt") if p.exists()]

errors: list[str] = []
warns: list[str] = []
seen_missing_tree: set[str] = set()
n_citations = 0

def _check_one(rel_file: Path, project: str, cited: str) -> None:
nonlocal n_citations
n_citations += 1
if project == "autolens_assistant":
tree: Optional[Path] = root # self-citations resolve against this repo
else:
tree = _project_tree(project, root)
if tree is None:
if project not in seen_missing_tree:
seen_missing_tree.add(project)
warns.append(
f"{project}: no checkout resolvable (installed/sources/sibling) — "
f"its citations skipped."
)
return
concrete = cited.split("...")[0].rstrip("/")
if not concrete:
return
if not (tree / concrete).exists():
errors.append(f"{rel_file}: `{project}:{cited}` — {concrete} not in {tree}")

for f in files:
text = f.read_text(encoding="utf-8")
if IDIOM_SKIP_MARKER in text:
continue
rel = f.relative_to(root)
fm_text, body = split_frontmatter(text)
for m in cite_re.finditer(body if fm_text is not None else text):
_check_one(rel, m.group(1), m.group(2))
# Frontmatter sources[].paths[] make the same claim in structured form.
if fm_text is not None and yaml is not None:
try:
meta = yaml.safe_load(fm_text) or {}
except yaml.YAMLError:
meta = {}
if isinstance(meta, dict):
for source in meta.get("sources") or []:
if not isinstance(source, dict):
continue
project = source.get("project")
if project not in projects:
continue
for path in source.get("paths") or []:
_check_one(rel, project, str(path))

for w in warns:
print(f"[citations] warn {w}", file=sys.stderr)
for e in errors:
print(f"[citations] ERROR {e}", file=sys.stderr)
print(
f"[citations] scanned {len(files)} files, {n_citations} citation(s) — "
f"{len(errors)} missing path(s), {len(warns)} warning(s).",
file=sys.stderr,
)
return 1 if errors else 0


# ---------------------------------------------------------------------------
# Code gate (--code / --file)
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1289,6 +1400,13 @@ def main() -> int:
help="With --write-provenance, restrict stamping to this page (repeatable). The "
"honest default for a targeted refresh — only stamp what you re-validated.",
)
parser.add_argument(
"--check-citations",
action="store_true",
help="Resolve every `Project:path` source citation (inline + wiki frontmatter "
"sources paths) against a checkout of the cited project and exit (0 ok, 1 "
"missing). Catches pre-refactor path staleness the symbol audit is blind to.",
)
parser.add_argument(
"--strict",
action="store_true",
Expand Down Expand Up @@ -1330,6 +1448,8 @@ def main() -> int:
return write_provenance(root, only=only)
if args.check_provenance:
return check_provenance(root, strict=args.strict)
if args.check_citations:
return check_citations(root)

# Baseline actions short-circuit the (expensive) Markdown/script scan.
if args.check_version:
Expand Down
3 changes: 2 additions & 1 deletion llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ Read order: [AGENTS.md](./AGENTS.md) → [skills/README.md](./skills/README.md)

## Reference

- [wiki/](./wiki): curated PyAuto* reference (`wiki/core/`) plus the strong-lensing science wiki (`wiki/literature/`).
- [wiki/core/index.md](./wiki/core/index.md): curated PyAuto* reference — stack, lensing concepts, API catalogues, operations.
- [wiki/literature/index.md](./wiki/literature/index.md): strong-lensing science wiki — concepts, named entities (surveys, lenses), bibliography.

## Runnable examples & tutorials (elsewhere)

Expand Down
2 changes: 1 addition & 1 deletion skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ configured) via symlinks; the canonical files live here.
+ the understanding to evolve it. Project-workflow skills may instead drive `rsync`,
`cp`, or other repo-level operations.
- Source citations use the project-name + repo-relative-path form,
e.g. `PyAutoFit:autofit/non_linear/search/nest/nautilus.py`, resolved via
e.g. `PyAutoFit:autofit/non_linear/search/nest/nautilus/`, resolved via
[`../sources.yaml`](../sources.yaml).
- Wiki references use workspace-relative paths,
e.g. `wiki/core/concepts/non_linear_search.md`.
Expand Down
2 changes: 1 addition & 1 deletion skills/_style.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ project's repo root**, resolvable via [`../sources.yaml`](../sources.yaml).

Good:

> See `PyAutoFit:autofit/non_linear/search/nest/nautilus.py` for the search's default
> See `PyAutoFit:autofit/non_linear/search/nest/nautilus/` for the search's default
> settings, and `wiki/core/api/searches.md#nautilus` for when to pick it.

Bad:
Expand Down
21 changes: 21 additions & 0 deletions skills/al_audit_skill_apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,27 @@ honest re-pin, the partner of `al_update_wiki` step 4):
python autoassistant/audit_skill_apis.py --write-provenance --page wiki/core/api/<page>.md
```

### 6. Citation paths — the layout axis the symbol audit is blind to

A page can cite only live `al.*` symbols while its `` `Project:relative/path` ``
source citations point at a pre-refactor file layout (module → package moves like
`nest/nautilus.py` → `nest/nautilus/`, deleted files, renamed READMEs). The
2026-07-09 release audit found ~30 such stale paths on an otherwise
0-broken-symbols tree — the two are independent failure axes.

```bash
python autoassistant/audit_skill_apis.py --check-citations # 0 ok, 1 on missing path
```

It scans `skills/`, `wiki/core/`, `AGENTS.md` and `llms.txt` for inline
`` `<project>:<path>` `` citations (projects from `sources.yaml`) plus each wiki
page's frontmatter `sources[].paths[]`, and resolves every path against a checkout
of the cited project (installed → `sources/<project>/` → sibling clone). A path
containing `...` is a deliberate abbreviation — only its concrete prefix must
exist. Missing paths are ERRORs; a project with no resolvable checkout downgrades
to a warning. Run it alongside the symbol audit in every refresh
(`al_refresh_api_docs`) and before a release.

ERRORs fail the check; warnings (unpinned `main`, unstamped legacy pages) do not unless
`--strict`, so the release/PR check goes red on genuine forgery/staleness without nuking the
50+ legacy `main`-pinned pages that predate the discipline.
Expand Down
2 changes: 1 addition & 1 deletion skills/al_build_imaging_model.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ for g in gaussian_per_basis[1:]:
lens_bulge = af.Model(al.lp_basis.Basis, profile_list=gaussian_per_basis)
```

See `PyAutoGalaxy:autogalaxy/profiles/light_linear/` for the linear-profile classes.
See `PyAutoGalaxy:autogalaxy/profiles/light/linear/` for the linear-profile classes.

## Branch — pixelised source

Expand Down
3 changes: 2 additions & 1 deletion skills/al_cluster_csv_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ ranges.

> TODO: recipe. Pattern: `model = al.csv.model_from(mass_csv=Path("mass.csv"),
> light_csv=Path("light.csv"), point_csv=Path("point.csv"))` (verify
> exact API in `PyAutoLens:autolens/csv/...` — module name may differ).
> exact API in `PyAutoGalaxy:autogalaxy/galaxy/galaxy_model_csv.py`; the
> workspace walkthrough is `autolens_workspace:scripts/cluster/csv_api.py`).

## Branch — adding scaling-tier members

Expand Down
6 changes: 3 additions & 3 deletions skills/al_configure_search.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ search = af.Nautilus(
)
```

Source: `PyAutoFit:autofit/non_linear/search/nest/nautilus.py`.
Source: `PyAutoFit:autofit/non_linear/search/nest/nautilus/`.

Knobs to know:
- `n_live` — more = more accurate posterior, slower. Start at 200; go to 400+ only if
Expand All @@ -68,7 +68,7 @@ search = af.DynestyStatic(
)
```

Source: `PyAutoFit:autofit/non_linear/search/nest/dynesty.py`.
Source: `PyAutoFit:autofit/non_linear/search/nest/dynesty/`.

## Branch — Emcee (MCMC)

Expand All @@ -85,7 +85,7 @@ search = af.Emcee(
)
```

Source: `PyAutoFit:autofit/non_linear/search/mcmc/emcee.py`.
Source: `PyAutoFit:autofit/non_linear/search/mcmc/emcee/`.

## Branch — Other searches

Expand Down
2 changes: 1 addition & 1 deletion skills/al_custom_profile.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ class CustomNFW(ag.mp.MassProfile):
raise NotImplementedError("Wire up your deflection integral here.")
```

Source: `PyAutoGalaxy:autogalaxy/profiles/mass/abstract.py` — interface.
Source: `PyAutoGalaxy:autogalaxy/profiles/mass/abstract/` — interface.
`PyAutoGalaxy:autogalaxy/profiles/mass/dark/nfw.py` — canonical NFW for shape.

For the physics of what each method represents (convergence = surface mass density,
Expand Down
2 changes: 1 addition & 1 deletion skills/al_debug_fit_failure.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Symptoms: parameter posteriors are tight but at unphysical values (einstein radi
The inversion has over-fitted background noise into the source plane.

- **Regularisation too low.** Bump the regularisation coefficient range — see
`PyAutoLens:autolens/inversion/regularization/`. The `ConstantSplit` regularisation
`PyAutoArray:autoarray/inversion/regularization/`. The `ConstantSplit` regularisation
with sensible coefficients usually works.
- **No positions penalty.** Add a `PositionsLH` to prevent the inversion from
demagnifying the source into infinity.
Expand Down
2 changes: 1 addition & 1 deletion skills/al_prepare_imaging_data.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Source citations:
- `PyAutoArray:autoarray/dataset/imaging/dataset.py` — `Imaging.from_fits`, `apply_mask`,
`apply_over_sampling`.
- `PyAutoArray:autoarray/mask/mask_2d.py` — `Mask2D.circular`.
- `PyAutoArray:autoarray/operators/over_sampling.py` — `over_sample_size_via_radial_bins_from`.
- `PyAutoArray:autoarray/operators/over_sampling/over_sample_util.py` — `over_sample_size_via_radial_bins_from`.

Read [`wiki/core/concepts/grids_and_masks.md`](../wiki/core/concepts/grids_and_masks.md) for
*why* over-sampling matters (steep light profiles in pixels near the centre alias
Expand Down
5 changes: 4 additions & 1 deletion skills/al_refresh_api_docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,13 @@ script that skill would generate.

## Combine — what "complete" looks like

A full refresh pass is complete when all four are true:
A full refresh pass is complete when all five are true:

- the PyAuto* stack imports in the target environment
- `autoassistant/audit_skill_apis.py --scope <scope>` reports zero misses
- `autoassistant/audit_skill_apis.py --check-citations` reports zero missing paths
(symbols and citation paths are independent failure axes — see
`al_audit_skill_apis` §6)
- every touched wiki page has either an updated `pinned_commit` or an explicit decision
that the source diff was cosmetic
- any materially changed skill recipe has been smoke-tested with `PYAUTO_TEST_MODE=1`
Expand Down
2 changes: 1 addition & 1 deletion skills/al_sensitivity_mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Workspace path:
> TODO: recipe. The pattern: define a `SensitivityMapping` job that takes
> the base fit + grid of perturber parameters; per cell, simulate, fit
> base + fit perturbed, store Δlog-evidence. Aggregate into a map. See
> `PyAutoFit:autofit/non_linear/grid/sensitivity.py` for the framework
> `PyAutoFit:autofit/non_linear/grid/sensitivity/` for the framework
> and `PyAutoLens:autolens/lens/sensitivity.py` (if present) for the
> lensing wrapper.

Expand Down
2 changes: 1 addition & 1 deletion skills/al_subhalo_detect.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ The workspace path is
> a `Grid2D` of subhalo positions, refitting at each cell with an added
> `al.mp.NFW` (or similar). Compare per-cell log evidence to the base
> evidence; significant gains flag detections. See
> `PyAutoLens:autolens/lens/subhalo/...` for the subhalo helper module
> `PyAutoLens:autolens/lens/subhalo.py` for the subhalo helper module
> if one exists, otherwise compose by hand.

## Branch — followup on a detection candidate
Expand Down
12 changes: 6 additions & 6 deletions skills/al_update_wiki.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ title: Non-linear searches
sources:
- project: PyAutoFit
paths:
- autofit/non_linear/search/nest/nautilus.py
- autofit/non_linear/search/nest/dynesty.py
- autofit/non_linear/search/mcmc/emcee.py
- autofit/non_linear/search/mle/bfgs.py
- autofit/non_linear/search/mle/pyswarms.py
- autofit/non_linear/search/nest/nautilus/
- autofit/non_linear/search/nest/dynesty/
- autofit/non_linear/search/mcmc/emcee/
- autofit/non_linear/search/mle/bfgs/
- autofit/non_linear/search/mle/pyswarms/
pinned_commit: <sha-or-tag>
last_updated: 2026-05-22
---
Expand Down Expand Up @@ -93,7 +93,7 @@ unaffected prose just because the page was touched — small, reviewable diffs.

When rewriting:

- Cite source code as `<Project>:<path>` (e.g. `PyAutoFit:autofit/non_linear/search/nest/nautilus.py`).
- Cite source code as `<Project>:<path>` (e.g. `PyAutoFit:autofit/non_linear/search/nest/nautilus/`).
- Update tables of classes / functions / parameters to match what's actually exported.
- Add new entries when the source has new public items in `__all__` or top-level
imports.
Expand Down
4 changes: 2 additions & 2 deletions wiki/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ title: <Page title>
sources:
- project: PyAutoFit
paths:
- autofit/non_linear/search/nest/nautilus.py
- autofit/non_linear/search/nest/nautilus/
pinned_commit: <sha-or-tag>
last_updated: 2026-05-22
---
Expand All @@ -48,7 +48,7 @@ Inside a wiki page, code references use the **project name + path relative to th
project's repo root**, identical to the skill convention:

```
See `PyAutoFit:autofit/non_linear/search/nest/nautilus.py` for the implementation.
See `PyAutoFit:autofit/non_linear/search/nest/nautilus/` for the implementation.
```

Resolve project names via [`../../sources.yaml`](../../sources.yaml). Never embed
Expand Down
27 changes: 25 additions & 2 deletions wiki/core/api/analysis_objects.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ sources:
- autofit/graphical/declarative/collection.py
- autofit/graphical/declarative/abstract.py
pinned_commit: ce2baa2b6611de99922e04d44b272de1be3ceb8e
last_updated: 2026-06-22
content_sha256: 8c415d521a9a0e4789eeb9ef69cb9bc9371ae5e7131e4035506554ed443d765b
last_updated: 2026-07-09
content_sha256: 4d96296dca40cf01909f7426f1639417b9fc0af02124e6389e649bae26baa21b
---

# Analysis objects
Expand Down Expand Up @@ -144,6 +144,29 @@ loglike = analysis.log_likelihood_function(instance=instance)
This evaluates the model at a specific parameter vector and returns the
log-likelihood. Useful for prior-bound sanity checks.

## Custom `Analysis` subclasses

When a dataset doesn't fit any built-in analysis, write your own: subclass
`af.Analysis`, store the dataset in `__init__`, and implement
`log_likelihood_function(instance)` — the instance is the lens model at one
parameter vector (galaxies, profiles, all as concrete objects), and the return
value is the log likelihood the non-linear search samples. Everything else
(searches, priors, samples, the aggregator) works unchanged, because the
`Analysis` class is PyAutoFit's only contract between model and data.

The built-in analyses are the reference implementations — they live in the
`imaging/model/`, `interferometer/model/`, `point/model/` packages of
PyAutoGalaxy/PyAutoLens, and a shortened `AnalysisImaging` is walked through in
the workspace guide. Weak lensing is the instructive precedent: it began life
as exactly this kind of custom analysis before graduating into the built-in
`AnalysisWeak` (see [`../concepts/weak_lensing.md`](../concepts/weak_lensing.md)).

Workspace guide: `autolens_workspace:scripts/guides/advanced/custom_analysis.py`.
PyAutoFit's analysis cookbook
(https://pyautofit.readthedocs.io/en/latest/cookbooks/analysis.html) is the
concise API reference. This is the ground the
[`al_custom_analysis`](../../../skills/al_custom_analysis.md) skill covers.

## Visualisations during fit

Analyses also produce diagnostic plots that PyAutoFit writes to `output/.../image/`
Expand Down
Loading
Loading