matchlab 0.1.0.dev1__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.
- matchlab-0.1.0.dev1/.github/actions/setup-toolchain/action.yml +29 -0
- matchlab-0.1.0.dev1/.github/dependabot.yml +19 -0
- matchlab-0.1.0.dev1/.github/pull_request_template.md +25 -0
- matchlab-0.1.0.dev1/.github/skills/lucid-docs/README.md +63 -0
- matchlab-0.1.0.dev1/.github/skills/lucid-docs/SKILL.md +153 -0
- matchlab-0.1.0.dev1/.github/skills/lucid-docs/readability.py +310 -0
- matchlab-0.1.0.dev1/.github/workflows/cd.yml +74 -0
- matchlab-0.1.0.dev1/.github/workflows/ci.yml +75 -0
- matchlab-0.1.0.dev1/.github/workflows/codeql.yml +49 -0
- matchlab-0.1.0.dev1/.gitignore +204 -0
- matchlab-0.1.0.dev1/.pre-commit-config.yaml +32 -0
- matchlab-0.1.0.dev1/.vscode/launch.json +29 -0
- matchlab-0.1.0.dev1/.vscode/settings.json +14 -0
- matchlab-0.1.0.dev1/CODEOWNERS +1 -0
- matchlab-0.1.0.dev1/LICENSE +21 -0
- matchlab-0.1.0.dev1/PKG-INFO +88 -0
- matchlab-0.1.0.dev1/README.md +53 -0
- matchlab-0.1.0.dev1/benchmarks/README.md +70 -0
- matchlab-0.1.0.dev1/benchmarks/__init__.py +1 -0
- matchlab-0.1.0.dev1/benchmarks/__main__.py +244 -0
- matchlab-0.1.0.dev1/benchmarks/cases.py +207 -0
- matchlab-0.1.0.dev1/benchmarks/data.py +284 -0
- matchlab-0.1.0.dev1/benchmarks/measure.py +129 -0
- matchlab-0.1.0.dev1/benchmarks/measurements.py +158 -0
- matchlab-0.1.0.dev1/benchmarks/report.py +400 -0
- matchlab-0.1.0.dev1/benchmarks/results/.gitkeep +0 -0
- matchlab-0.1.0.dev1/benchmarks/runner.py +168 -0
- matchlab-0.1.0.dev1/benchmarks/topologies.py +245 -0
- matchlab-0.1.0.dev1/docs/api/adapters.md +12 -0
- matchlab-0.1.0.dev1/docs/api/core/exceptions.md +12 -0
- matchlab-0.1.0.dev1/docs/api/core/hash.md +12 -0
- matchlab-0.1.0.dev1/docs/api/core/resolver_output.md +5 -0
- matchlab-0.1.0.dev1/docs/api/core/schemas.md +12 -0
- matchlab-0.1.0.dev1/docs/api/core/transform.md +12 -0
- matchlab-0.1.0.dev1/docs/api/eval-judgements.md +12 -0
- matchlab-0.1.0.dev1/docs/api/eval-metrics.md +12 -0
- matchlab-0.1.0.dev1/docs/api/eval-sampling.md +12 -0
- matchlab-0.1.0.dev1/docs/api/eval-tui.md +11 -0
- matchlab-0.1.0.dev1/docs/api/lineage.md +12 -0
- matchlab-0.1.0.dev1/docs/api/locations.md +12 -0
- matchlab-0.1.0.dev1/docs/api/models.md +12 -0
- matchlab-0.1.0.dev1/docs/api/progress.md +12 -0
- matchlab-0.1.0.dev1/docs/api/recordstep.md +5 -0
- matchlab-0.1.0.dev1/docs/api/resolvers.md +12 -0
- matchlab-0.1.0.dev1/docs/api/sources.md +12 -0
- matchlab-0.1.0.dev1/docs/api/specs.md +12 -0
- matchlab-0.1.0.dev1/docs/api/steps.md +12 -0
- matchlab-0.1.0.dev1/docs/api/testkit.md +109 -0
- matchlab-0.1.0.dev1/docs/api/transformers.md +12 -0
- matchlab-0.1.0.dev1/docs/assets/matchbox-icon-dark.png +0 -0
- matchlab-0.1.0.dev1/docs/assets/matchbox-icon.svg +9 -0
- matchlab-0.1.0.dev1/docs/assets/matchbox-logo-dark.svg +17 -0
- matchlab-0.1.0.dev1/docs/assets/matchbox-logo-light.svg +17 -0
- matchlab-0.1.0.dev1/docs/contributing.md +130 -0
- matchlab-0.1.0.dev1/docs/glossary.md +161 -0
- matchlab-0.1.0.dev1/docs/guide/build-a-plan.md +446 -0
- matchlab-0.1.0.dev1/docs/guide/evaluate.md +138 -0
- matchlab-0.1.0.dev1/docs/guide/install.md +52 -0
- matchlab-0.1.0.dev1/docs/guide/query.md +65 -0
- matchlab-0.1.0.dev1/docs/index.md +66 -0
- matchlab-0.1.0.dev1/docs/migration/matchbox-to-matchlab.md +291 -0
- matchlab-0.1.0.dev1/docs/stylesheets/extra.css +30 -0
- matchlab-0.1.0.dev1/docs/use-cases.md +26 -0
- matchlab-0.1.0.dev1/docs/why-matchlab.md +45 -0
- matchlab-0.1.0.dev1/justfile +36 -0
- matchlab-0.1.0.dev1/mkdocs.yml +150 -0
- matchlab-0.1.0.dev1/personal-data-exclusions.txt +3 -0
- matchlab-0.1.0.dev1/pyproject.toml +150 -0
- matchlab-0.1.0.dev1/scripts/test_eval.py +13 -0
- matchlab-0.1.0.dev1/setup.cfg +4 -0
- matchlab-0.1.0.dev1/src/matchlab/__init__.py +55 -0
- matchlab-0.1.0.dev1/src/matchlab/adapters/__init__.py +24 -0
- matchlab-0.1.0.dev1/src/matchlab/adapters/base.py +446 -0
- matchlab-0.1.0.dev1/src/matchlab/adapters/duckdb.py +659 -0
- matchlab-0.1.0.dev1/src/matchlab/cli.py +177 -0
- matchlab-0.1.0.dev1/src/matchlab/core/__init__.py +1 -0
- matchlab-0.1.0.dev1/src/matchlab/core/dataframes.py +254 -0
- matchlab-0.1.0.dev1/src/matchlab/core/dsu.py +62 -0
- matchlab-0.1.0.dev1/src/matchlab/core/exceptions.py +40 -0
- matchlab-0.1.0.dev1/src/matchlab/core/hash.py +141 -0
- matchlab-0.1.0.dev1/src/matchlab/core/kinds.py +12 -0
- matchlab-0.1.0.dev1/src/matchlab/core/logging.py +190 -0
- matchlab-0.1.0.dev1/src/matchlab/core/resolver_output.py +173 -0
- matchlab-0.1.0.dev1/src/matchlab/core/schemas.py +82 -0
- matchlab-0.1.0.dev1/src/matchlab/core/sql.py +12 -0
- matchlab-0.1.0.dev1/src/matchlab/document.py +329 -0
- matchlab-0.1.0.dev1/src/matchlab/eval/__init__.py +11 -0
- matchlab-0.1.0.dev1/src/matchlab/eval/judgements.py +41 -0
- matchlab-0.1.0.dev1/src/matchlab/eval/metrics.py +244 -0
- matchlab-0.1.0.dev1/src/matchlab/eval/samples.py +415 -0
- matchlab-0.1.0.dev1/src/matchlab/eval/tui/__init__.py +66 -0
- matchlab-0.1.0.dev1/src/matchlab/eval/tui/app.py +418 -0
- matchlab-0.1.0.dev1/src/matchlab/eval/tui/modals.py +84 -0
- matchlab-0.1.0.dev1/src/matchlab/eval/tui/styles.tcss +184 -0
- matchlab-0.1.0.dev1/src/matchlab/eval/tui/widgets/__init__.py +1 -0
- matchlab-0.1.0.dev1/src/matchlab/eval/tui/widgets/assignment.py +72 -0
- matchlab-0.1.0.dev1/src/matchlab/eval/tui/widgets/styling.py +75 -0
- matchlab-0.1.0.dev1/src/matchlab/eval/tui/widgets/table.py +285 -0
- matchlab-0.1.0.dev1/src/matchlab/lineage.py +239 -0
- matchlab-0.1.0.dev1/src/matchlab/locations.py +347 -0
- matchlab-0.1.0.dev1/src/matchlab/models/__init__.py +10 -0
- matchlab-0.1.0.dev1/src/matchlab/models/comparison.py +67 -0
- matchlab-0.1.0.dev1/src/matchlab/models/dedupers/__init__.py +5 -0
- matchlab-0.1.0.dev1/src/matchlab/models/dedupers/base.py +38 -0
- matchlab-0.1.0.dev1/src/matchlab/models/dedupers/naive.py +77 -0
- matchlab-0.1.0.dev1/src/matchlab/models/linkers/__init__.py +9 -0
- matchlab-0.1.0.dev1/src/matchlab/models/linkers/base.py +41 -0
- matchlab-0.1.0.dev1/src/matchlab/models/linkers/deterministic.py +229 -0
- matchlab-0.1.0.dev1/src/matchlab/models/linkers/splinklinker.py +262 -0
- matchlab-0.1.0.dev1/src/matchlab/models/linkers/weighteddeterministic.py +143 -0
- matchlab-0.1.0.dev1/src/matchlab/models/models.py +198 -0
- matchlab-0.1.0.dev1/src/matchlab/progress.py +455 -0
- matchlab-0.1.0.dev1/src/matchlab/recordstep.py +181 -0
- matchlab-0.1.0.dev1/src/matchlab/resolvers/__init__.py +10 -0
- matchlab-0.1.0.dev1/src/matchlab/resolvers/base.py +63 -0
- matchlab-0.1.0.dev1/src/matchlab/resolvers/components.py +66 -0
- matchlab-0.1.0.dev1/src/matchlab/resolvers/resolvers.py +409 -0
- matchlab-0.1.0.dev1/src/matchlab/sources.py +291 -0
- matchlab-0.1.0.dev1/src/matchlab/specs.py +129 -0
- matchlab-0.1.0.dev1/src/matchlab/steps.py +284 -0
- matchlab-0.1.0.dev1/src/matchlab/testkit/__init__.py +104 -0
- matchlab-0.1.0.dev1/src/matchlab/testkit/_generate.py +240 -0
- matchlab-0.1.0.dev1/src/matchlab/testkit/compare.py +178 -0
- matchlab-0.1.0.dev1/src/matchlab/testkit/entities.py +290 -0
- matchlab-0.1.0.dev1/src/matchlab/testkit/features.py +146 -0
- matchlab-0.1.0.dev1/src/matchlab/testkit/linked.py +527 -0
- matchlab-0.1.0.dev1/src/matchlab/testkit/matchers.py +233 -0
- matchlab-0.1.0.dev1/src/matchlab/testkit/models.py +165 -0
- matchlab-0.1.0.dev1/src/matchlab/testkit/sources.py +327 -0
- matchlab-0.1.0.dev1/src/matchlab/transformers/__init__.py +24 -0
- matchlab-0.1.0.dev1/src/matchlab/transformers/base.py +41 -0
- matchlab-0.1.0.dev1/src/matchlab/transformers/clean.py +48 -0
- matchlab-0.1.0.dev1/src/matchlab/transformers/explode.py +67 -0
- matchlab-0.1.0.dev1/src/matchlab/transformers/group.py +56 -0
- matchlab-0.1.0.dev1/src/matchlab/transformers/select.py +31 -0
- matchlab-0.1.0.dev1/src/matchlab/transformers/transform.py +101 -0
- matchlab-0.1.0.dev1/src/matchlab.egg-info/PKG-INFO +88 -0
- matchlab-0.1.0.dev1/src/matchlab.egg-info/SOURCES.txt +194 -0
- matchlab-0.1.0.dev1/src/matchlab.egg-info/dependency_links.txt +1 -0
- matchlab-0.1.0.dev1/src/matchlab.egg-info/entry_points.txt +2 -0
- matchlab-0.1.0.dev1/src/matchlab.egg-info/requires.txt +15 -0
- matchlab-0.1.0.dev1/src/matchlab.egg-info/scm_file_list.json +190 -0
- matchlab-0.1.0.dev1/src/matchlab.egg-info/scm_version.json +8 -0
- matchlab-0.1.0.dev1/src/matchlab.egg-info/top_level.txt +1 -0
- matchlab-0.1.0.dev1/test/README.md +60 -0
- matchlab-0.1.0.dev1/test/__init__.py +1 -0
- matchlab-0.1.0.dev1/test/adapters/__init__.py +1 -0
- matchlab-0.1.0.dev1/test/adapters/conftest.py +134 -0
- matchlab-0.1.0.dev1/test/adapters/test_base.py +49 -0
- matchlab-0.1.0.dev1/test/adapters/test_contract.py +433 -0
- matchlab-0.1.0.dev1/test/adapters/test_duckdb.py +152 -0
- matchlab-0.1.0.dev1/test/conftest.py +125 -0
- matchlab-0.1.0.dev1/test/core/__init__.py +1 -0
- matchlab-0.1.0.dev1/test/core/test_dsu.py +86 -0
- matchlab-0.1.0.dev1/test/core/test_eval.py +233 -0
- matchlab-0.1.0.dev1/test/core/test_hash.py +229 -0
- matchlab-0.1.0.dev1/test/core/test_resolver_output.py +190 -0
- matchlab-0.1.0.dev1/test/eval/__init__.py +1 -0
- matchlab-0.1.0.dev1/test/eval/test_samples.py +209 -0
- matchlab-0.1.0.dev1/test/eval/tui/__init__.py +1 -0
- matchlab-0.1.0.dev1/test/eval/tui/test_app.py +180 -0
- matchlab-0.1.0.dev1/test/eval/tui/test_widgets.py +100 -0
- matchlab-0.1.0.dev1/test/locations/__init__.py +1 -0
- matchlab-0.1.0.dev1/test/locations/conftest.py +88 -0
- matchlab-0.1.0.dev1/test/locations/test_locations.py +294 -0
- matchlab-0.1.0.dev1/test/models/__init__.py +1 -0
- matchlab-0.1.0.dev1/test/models/test_comparison.py +53 -0
- matchlab-0.1.0.dev1/test/models/test_dedupers_deterministic.py +88 -0
- matchlab-0.1.0.dev1/test/models/test_linkers_deterministic.py +456 -0
- matchlab-0.1.0.dev1/test/models/test_linkers_probabilistic.py +234 -0
- matchlab-0.1.0.dev1/test/models/test_models.py +124 -0
- matchlab-0.1.0.dev1/test/plan/__init__.py +1 -0
- matchlab-0.1.0.dev1/test/plan/test_ground_truth.py +127 -0
- matchlab-0.1.0.dev1/test/plan/test_plan.py +1033 -0
- matchlab-0.1.0.dev1/test/resolvers/__init__.py +1 -0
- matchlab-0.1.0.dev1/test/resolvers/test_components.py +105 -0
- matchlab-0.1.0.dev1/test/resolvers/test_resolvers.py +257 -0
- matchlab-0.1.0.dev1/test/test_benchmarks.py +399 -0
- matchlab-0.1.0.dev1/test/test_cli.py +187 -0
- matchlab-0.1.0.dev1/test/test_document.py +431 -0
- matchlab-0.1.0.dev1/test/test_lineage.py +156 -0
- matchlab-0.1.0.dev1/test/test_progress.py +906 -0
- matchlab-0.1.0.dev1/test/testkit/__init__.py +1 -0
- matchlab-0.1.0.dev1/test/testkit/test_entities.py +517 -0
- matchlab-0.1.0.dev1/test/testkit/test_linked.py +381 -0
- matchlab-0.1.0.dev1/test/testkit/test_models.py +437 -0
- matchlab-0.1.0.dev1/test/testkit/test_scores.py +367 -0
- matchlab-0.1.0.dev1/test/testkit/test_sources.py +545 -0
- matchlab-0.1.0.dev1/test/transformers/__init__.py +1 -0
- matchlab-0.1.0.dev1/test/transformers/test_clean.py +59 -0
- matchlab-0.1.0.dev1/test/transformers/test_explode.py +64 -0
- matchlab-0.1.0.dev1/test/transformers/test_group.py +49 -0
- matchlab-0.1.0.dev1/test/transformers/test_select.py +34 -0
- matchlab-0.1.0.dev1/test/transformers/test_transform.py +96 -0
- matchlab-0.1.0.dev1/trufflehog-exclude.txt +1 -0
- matchlab-0.1.0.dev1/uv.lock +2147 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Setup toolchain
|
|
2
|
+
description: Install uv with Python and optionally just
|
|
3
|
+
|
|
4
|
+
inputs:
|
|
5
|
+
python-version:
|
|
6
|
+
description: Python version to install
|
|
7
|
+
required: false
|
|
8
|
+
default: "3.11"
|
|
9
|
+
install-just:
|
|
10
|
+
description: Whether to install just
|
|
11
|
+
required: false
|
|
12
|
+
default: "true"
|
|
13
|
+
|
|
14
|
+
runs:
|
|
15
|
+
using: composite
|
|
16
|
+
steps:
|
|
17
|
+
- name: Install uv and Python
|
|
18
|
+
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
|
19
|
+
with:
|
|
20
|
+
enable-cache: true
|
|
21
|
+
cache-dependency-glob: "uv.lock"
|
|
22
|
+
python-version: ${{ inputs.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install just
|
|
25
|
+
if: ${{ inputs.install-just == 'true' }}
|
|
26
|
+
uses: extractions/setup-crate@7577c1bdf2d95e6d65d532788f35ed79d4b1dda2 # v2.0.1
|
|
27
|
+
with:
|
|
28
|
+
repo: casey/just
|
|
29
|
+
version: 1.55.1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
version: 2
|
|
3
|
+
updates:
|
|
4
|
+
- package-ecosystem: "github-actions"
|
|
5
|
+
directory: "/"
|
|
6
|
+
schedule:
|
|
7
|
+
interval: "weekly"
|
|
8
|
+
groups:
|
|
9
|
+
all-dependencies:
|
|
10
|
+
patterns:
|
|
11
|
+
- "*"
|
|
12
|
+
- package-ecosystem: uv
|
|
13
|
+
directory: "/"
|
|
14
|
+
schedule:
|
|
15
|
+
interval: "weekly"
|
|
16
|
+
groups:
|
|
17
|
+
all-dependencies:
|
|
18
|
+
patterns:
|
|
19
|
+
- "*"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<!-- Give high level context to this PR -->
|
|
2
|
+
|
|
3
|
+
## 🛠️ Changes proposed in this pull request
|
|
4
|
+
|
|
5
|
+
* <!-- List the main changes you've made -->
|
|
6
|
+
|
|
7
|
+
## 👀 Guidance to review
|
|
8
|
+
|
|
9
|
+
<!-- Help reviewers know where to focus their efforts, or flag controversial decisions -->
|
|
10
|
+
<!-- Add links and diagrams to explain decisions -->
|
|
11
|
+
<!-- Point out any **breaking changes** -->
|
|
12
|
+
|
|
13
|
+
## 🤖 AI declaration
|
|
14
|
+
|
|
15
|
+
<!-- Declare code where AI was used, and how you ensured its quality -->
|
|
16
|
+
|
|
17
|
+
## ✅ Checklist:
|
|
18
|
+
|
|
19
|
+
- [ ] This is the smallest, simplest solution to the problem
|
|
20
|
+
- [ ] I've read [our code standards](https://uktrade.github.io/matchlab/contributing/) and this code follows them
|
|
21
|
+
- [ ] All new code is tested
|
|
22
|
+
- I've updated all relevant documentation (select all that apply)
|
|
23
|
+
- [ ] API documentation (docstrings and indexes)
|
|
24
|
+
- [ ] Tutorials
|
|
25
|
+
- [ ] Developer docs
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Lucid docs v0.1
|
|
2
|
+
|
|
3
|
+
Plain syntax. Precise terms. Necessary detail.
|
|
4
|
+
|
|
5
|
+
Reduce reading effort without reducing technical depth.
|
|
6
|
+
|
|
7
|
+
## Reader
|
|
8
|
+
|
|
9
|
+
The skill defines a mid-level Python reader by assumed knowledge.
|
|
10
|
+
|
|
11
|
+
It assumes comfort with idiomatic Python, common language features, standard library conventions, type hints, testing, and ordinary object-oriented design. It does not explain standard Python or established dependencies unless their local use is surprising. It explains repository-specific concepts, hidden constraints, surprising behaviour, and non-obvious interactions.
|
|
12
|
+
|
|
13
|
+
The voice is direct when the repository supports a clear claim. Uncertainty is surfaced.
|
|
14
|
+
|
|
15
|
+
## Repository glossary
|
|
16
|
+
|
|
17
|
+
`docs/glossary.md` is the canonical source for repository-specific terminology. It is user-facing documentation and shared context for coding agents.
|
|
18
|
+
|
|
19
|
+
The skill reads the glossary before editing documentation. It adds or updates terms that developers must understand as concepts, while excluding ordinary Python terms, obvious identifiers, and incidental implementation details.
|
|
20
|
+
|
|
21
|
+
The glossary owns each canonical definition. Individual documents still provide enough local context to remain readable. Ambiguous or conflicting uses are reported rather than silently resolved.
|
|
22
|
+
|
|
23
|
+
## Documentation locality
|
|
24
|
+
|
|
25
|
+
Each explanation belongs at the narrowest scope that contains everyone who needs it:
|
|
26
|
+
|
|
27
|
+
- module docstrings hold cross-cutting decisions, boundaries, and a short map
|
|
28
|
+
- class and function docstrings hold the unit's contract and local why
|
|
29
|
+
- inline comments hold the smallest-scope reason or constraint
|
|
30
|
+
|
|
31
|
+
Shared reasoning is stated once and referred to by name. Shared contracts live at the abstraction that owns them. Implementations and overrides document only their differences.
|
|
32
|
+
|
|
33
|
+
## Proportionality and comments
|
|
34
|
+
|
|
35
|
+
Documentation depth follows conceptual weight, not line count. Simple units stay brief. `Args` and `Returns` sections appear only when they add information beyond the signature and type hints.
|
|
36
|
+
|
|
37
|
+
A comment earns its place when it names a reasonable wrong turn, hidden constraint, or non-obvious consequence. The code remains responsible for routine mechanics.
|
|
38
|
+
|
|
39
|
+
## Our style
|
|
40
|
+
|
|
41
|
+
We want to establish a simple term to encapsulate our style as succinctly as possible.
|
|
42
|
+
|
|
43
|
+
The established umbrella term is **plain language**, or **plain English** in a UK house style.
|
|
44
|
+
|
|
45
|
+
Plain language is defined for an intended audience. It does not require replacing terms that the audience needs and understands. For developer documentation, this means simple sentence structure around exact technical vocabulary.
|
|
46
|
+
|
|
47
|
+
The narrower linguistic operation is **syntactic simplification**: making sentence structure easier to parse while preserving meaning.
|
|
48
|
+
|
|
49
|
+
The closest formal engineering analogue is **ASD-STE100 Simplified Technical English**. It constrains grammar and general vocabulary while permitting project-specific technical nouns and verbs. This skill borrows that distinction, but it does not claim ASD-STE100 compliance. The full standard is more restrictive and uses American English.
|
|
50
|
+
|
|
51
|
+
For this project, the practical label is:
|
|
52
|
+
|
|
53
|
+
> Plain English for technical readers.
|
|
54
|
+
|
|
55
|
+
## Research links
|
|
56
|
+
|
|
57
|
+
- ISO 24495-1 plain language standard: https://www.iso.org/obp/ui/#iso:std:iso:24495:-1:ed-1:v1:en
|
|
58
|
+
- GOV.UK guidance on clear language for specialists: https://guidance.publishing.service.gov.uk/writing-to-gov-uk-standards/writing-guidelines/clear-language/
|
|
59
|
+
- IEEE guidance on plain language for engineers: https://procomm.ieee.org/communication-resources-for-engineers/other-topics/plain-language/
|
|
60
|
+
- ASD-STE100 official overview: https://www.asd-ste100.org/about_STE.html
|
|
61
|
+
- Google guidance on short technical sentences: https://developers.google.com/tech-writing/one/short-sentences
|
|
62
|
+
- Matt Pocock's domain-modelling skill: https://github.com/mattpocock/skills/tree/main/skills/engineering/domain-modeling
|
|
63
|
+
- Martin Fowler on ubiquitous language: https://martinfowler.com/bliki/UbiquitousLanguage.html
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lucid-docs
|
|
3
|
+
description: Edit Python documentation, docstrings, comments, and repository terminology in plain English for technical readers. Reduce reading effort while preserving precise terms, contracts, design reasons, warnings, and non-obvious behaviour.
|
|
4
|
+
license: CC0-1.0
|
|
5
|
+
compatibility: Designed for coding agents that can inspect a repository and edit text files.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Lucid docs
|
|
9
|
+
|
|
10
|
+
## Goal
|
|
11
|
+
|
|
12
|
+
Rewrite the specified documentation to reduce reading effort. Do not cut technical depth.
|
|
13
|
+
|
|
14
|
+
Write in plain English for technical readers:
|
|
15
|
+
|
|
16
|
+
> Plain syntax. Precise terms. Necessary detail.
|
|
17
|
+
|
|
18
|
+
Fluent, sophisticated prose is NOT plain English for technical readers. Rewrite sentence structure ruthlessly. Use short sentences and direct verbs. Reduce ideas per sentence, but keep precise technical terms.
|
|
19
|
+
|
|
20
|
+
Pay particular attention to the glossary.
|
|
21
|
+
|
|
22
|
+
Ensure you run all three final checks.
|
|
23
|
+
|
|
24
|
+
## Reader
|
|
25
|
+
|
|
26
|
+
The reader is a mid-level Python developer, with a B2 CEFR reading level (Flesch-Kincaid Grade 8-9).
|
|
27
|
+
|
|
28
|
+
Assume the reader can read idiomatic Python and understands common language features, standard library conventions, type hints, testing, and ordinary object-oriented design.
|
|
29
|
+
|
|
30
|
+
Do not explain standard Python or established dependencies unless their use here differs from normal expectations. Explain repository-specific concepts, hidden constraints, surprising behaviour, and non-obvious interactions.
|
|
31
|
+
|
|
32
|
+
State verified behaviour directly. Surface genuine uncertainty instead of weakening every claim.
|
|
33
|
+
|
|
34
|
+
## Repository language
|
|
35
|
+
|
|
36
|
+
Treat `docs/glossary.md` as the source of truth for repository-specific terms. Read it before editing documentation.
|
|
37
|
+
|
|
38
|
+
Add or update a term when the reader must understand it as a concept. Include domain concepts, named workflows, architectural concepts, and important local distinctions. Exclude ordinary Python terms, obvious identifiers, and incidental implementation details.
|
|
39
|
+
|
|
40
|
+
Write the glossary for developers as well as agents. Use the same plain, precise style as the rest of the documentation. Let the glossary own the canonical definition, while each document supplies enough local context to remain readable.
|
|
41
|
+
|
|
42
|
+
Do not invent a definition or silently resolve conflicting uses. Report ambiguity when the repository does not support one clear meaning.
|
|
43
|
+
|
|
44
|
+
## Editing
|
|
45
|
+
|
|
46
|
+
Read enough code, tests, types, nearby documentation, and glossary entries to preserve the meaning.
|
|
47
|
+
|
|
48
|
+
Keep information that helps a developer:
|
|
49
|
+
|
|
50
|
+
- use the code correctly
|
|
51
|
+
- understand a design choice
|
|
52
|
+
- preserve an invariant
|
|
53
|
+
- predict a side effect or failure
|
|
54
|
+
- follow a non-obvious interaction
|
|
55
|
+
- avoid a footgun
|
|
56
|
+
|
|
57
|
+
Then tighten the prose:
|
|
58
|
+
|
|
59
|
+
1. Put the main point first.
|
|
60
|
+
2. Give each sentence one job, but clarity always outranks brevity.
|
|
61
|
+
3. Put the subject and verb early.
|
|
62
|
+
4. Prefer direct verbs to noun-heavy phrases.
|
|
63
|
+
5. Remove words that add no meaning.
|
|
64
|
+
6. Keep exact technical terms.
|
|
65
|
+
7. Check that no condition, warning, or consequence was lost.
|
|
66
|
+
|
|
67
|
+
Do not mistake prose that reads fluently and is technically sophisticated for plain English for technical readers.
|
|
68
|
+
|
|
69
|
+
Do not minimise the diff on the documentation you've been asked to review. Assume it has been written maximally, and your job is to rewrite minimally. Plain syntax. Precise terms. Necessary detail.
|
|
70
|
+
|
|
71
|
+
## House style
|
|
72
|
+
|
|
73
|
+
- Use British English.
|
|
74
|
+
- Use sentence case headings.
|
|
75
|
+
- Do not use semicolons.
|
|
76
|
+
- Remove colons and dashes unless they truly simplify.
|
|
77
|
+
- Do not start sentences with the FANBOYS set (for, and, nor, but, or, yet, so) unless the sentence stands alone, with its own subject, verb, and stated reason.
|
|
78
|
+
- Use present tense for current behaviour.
|
|
79
|
+
- Use one term for one concept.
|
|
80
|
+
- Describe earlier or planned code only in migration guides.
|
|
81
|
+
|
|
82
|
+
## Python documentation
|
|
83
|
+
|
|
84
|
+
Put each explanation at the narrowest scope that contains everyone who needs it. State shared reasoning once, then refer to it by name instead of repeating it.
|
|
85
|
+
|
|
86
|
+
Use module docstrings for cross-cutting design decisions, boundaries, and a short map of what the module contains. Explain why the module has its current shape, without reference to past or future code states outside of migration guides. Do not restate its implementation.
|
|
87
|
+
|
|
88
|
+
Use class and function docstrings for the unit's contract and local why. Record the invariant it relies on, the footgun it avoids, or the important alternative it rejects. Let names, signatures, and type hints carry what they already express.
|
|
89
|
+
|
|
90
|
+
Use inline comments for the smallest-scope fact or reason. Place them beside the lines they explain. A comment earns its place when it names a reasonable wrong turn, hidden constraint, or non-obvious consequence. Let the code express routine mechanics.
|
|
91
|
+
|
|
92
|
+
Match documentation to conceptual weight. Keep simple units brief. Do not add `Args` or `Returns` sections merely because a function has parameters or returns a value. Give fuller treatment to contracts, decisions, and risks that need it.
|
|
93
|
+
|
|
94
|
+
Document a shared contract at the highest abstraction that owns it. Overrides and implementations document only their differences, added constraints, and surprising behaviour.
|
|
95
|
+
|
|
96
|
+
Mention another part of the codebase only when the interaction affects correct use or safe maintenance.
|
|
97
|
+
|
|
98
|
+
## Final check
|
|
99
|
+
|
|
100
|
+
The result should be easier to read and no less exact.
|
|
101
|
+
|
|
102
|
+
### Phase 1
|
|
103
|
+
|
|
104
|
+
Confirm that:
|
|
105
|
+
|
|
106
|
+
- the first sentence carries the main point
|
|
107
|
+
- each sentence is easy to parse
|
|
108
|
+
- precise technical vocabulary remains
|
|
109
|
+
- repository-specific terms match `docs/glossary.md`
|
|
110
|
+
- new or changed concepts are reflected in the glossary
|
|
111
|
+
- each explanation sits at the narrowest useful scope
|
|
112
|
+
- documentation depth matches conceptual weight
|
|
113
|
+
- shared contracts are stated once at the abstraction that owns them
|
|
114
|
+
- comments add reasoning the code cannot show
|
|
115
|
+
- design reasons and warnings remain
|
|
116
|
+
- every claim is supported by the repository
|
|
117
|
+
- the prose describes the code that exists now, not past or future states
|
|
118
|
+
|
|
119
|
+
### Phase 2
|
|
120
|
+
|
|
121
|
+
Perform a final check of the readability score and house style:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
uv run .github/skills/lucid-docs/readability.py <path>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
This takes the following options:
|
|
128
|
+
|
|
129
|
+
* `--top <int>`: only show the N hardest-to-read blocks per file
|
|
130
|
+
* `--rules`: comma-separated rule ids to run (default: all)
|
|
131
|
+
* `pleng001` readability score
|
|
132
|
+
* `pleng002` colon/semicolon/dash
|
|
133
|
+
* `pleng003` FANBOYS sentence openers
|
|
134
|
+
|
|
135
|
+
Ensure an acceptable reading level for the reader. You may make allowances for the requirements of technical writing.
|
|
136
|
+
|
|
137
|
+
Colons and dashes are permissable only if they truly simplify.
|
|
138
|
+
|
|
139
|
+
This script is a spot-check, not the target. It cannot detect banned words or fluent-but-wordy prose.
|
|
140
|
+
|
|
141
|
+
### Phase 3
|
|
142
|
+
|
|
143
|
+
Reread each file in its entirety, assuming it does not yet read as plain English.
|
|
144
|
+
|
|
145
|
+
For each paragraph, find the sentence a plain English reader would stumble on, state why to yourself, then change it. Only move on once you cannot find one.
|
|
146
|
+
|
|
147
|
+
Do not attempt to minimise churn.
|
|
148
|
+
|
|
149
|
+
## Output
|
|
150
|
+
|
|
151
|
+
Edit the requested documentation and `docs/glossary.md` in place when tools allow it.
|
|
152
|
+
|
|
153
|
+
Report the files changed, unresolved terminology, and any claim that could not be verified. Keep the report brief.
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = ["textstat", "typer"]
|
|
5
|
+
# ///
|
|
6
|
+
"""Run plain-English rules over .py or .md files, one line per finding.
|
|
7
|
+
|
|
8
|
+
Rules (run all by default, or pick with --rules):
|
|
9
|
+
|
|
10
|
+
pleng001 Flesch-Kincaid readability score per block
|
|
11
|
+
pleng002 sentences containing a colon, semicolon, or dash
|
|
12
|
+
pleng003 sentences opening with a FANBOYS conjunction
|
|
13
|
+
|
|
14
|
+
A "block" is a docstring, comment run (for .py) or a paragraph (for .md).
|
|
15
|
+
Blocks below TRIVIAL_WORDS are skipped from scoring entirely (the maths is
|
|
16
|
+
meaningless on 1-3 word fragments). Blocks below MIN_WORDS are still scored
|
|
17
|
+
but flagged low_confidence.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
python readability.py <path> [<path> ...]
|
|
21
|
+
uv run readability.py <path> [<path> ...] [--top N] [--rules pleng002]
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import ast
|
|
25
|
+
import json
|
|
26
|
+
import re
|
|
27
|
+
import tokenize
|
|
28
|
+
from collections.abc import Iterator
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from enum import StrEnum
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
import textstat
|
|
34
|
+
import typer
|
|
35
|
+
|
|
36
|
+
MIN_WORDS = 30
|
|
37
|
+
TRIVIAL_WORDS = 5
|
|
38
|
+
|
|
39
|
+
FANBOYS = {"for", "and", "nor", "but", "or", "yet", "so"}
|
|
40
|
+
|
|
41
|
+
SENTENCE_SPLIT = re.compile(r"(?<=[.!?])\s+")
|
|
42
|
+
PUNCT_MARKS = re.compile(r"[;:]|—|–|(?<=\s)-(?=\s)")
|
|
43
|
+
LEADING_WORD = re.compile(r"^[\"'“‘(]*([A-Za-z]+)")
|
|
44
|
+
INLINE_CODE = re.compile(r"`([^`]*)`")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Rule(StrEnum):
|
|
48
|
+
"""Plain-English rule ids, in the order they run and print."""
|
|
49
|
+
|
|
50
|
+
READABILITY = "pleng001"
|
|
51
|
+
PUNCTUATION = "pleng002"
|
|
52
|
+
FANBOYS = "pleng003"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
RULE_DESCRIPTIONS = {
|
|
56
|
+
Rule.READABILITY: "Flesch-Kincaid readability score per block",
|
|
57
|
+
Rule.PUNCTUATION: "sentences containing a colon, semicolon, or dash",
|
|
58
|
+
Rule.FANBOYS: "sentences opening with a FANBOYS conjunction",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def strip_inline_code(text: str) -> str:
|
|
63
|
+
"""Drop backticks but keep the token inside, so identifiers stay readable."""
|
|
64
|
+
return INLINE_CODE.sub(r"\1", text)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def sentences_of(text: str) -> list[str]:
|
|
68
|
+
"""Split a block into sentences (naive, on . ! ?), dropping empty fragments."""
|
|
69
|
+
parts = SENTENCE_SPLIT.split(text.replace("\n", " "))
|
|
70
|
+
return [s.strip() for s in parts if s.strip()]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class BlockScore:
|
|
75
|
+
"""The readability result for one block, or the reason it wasn't scored."""
|
|
76
|
+
|
|
77
|
+
label: str
|
|
78
|
+
words: int
|
|
79
|
+
status: str # "", "low_confidence", "trivial"
|
|
80
|
+
grade: float | None
|
|
81
|
+
ease: float | None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def score_block(label: str, text: str) -> BlockScore:
|
|
85
|
+
"""Score a block, or mark it trivial instead of scoring meaningless text."""
|
|
86
|
+
word_count = len(text.split())
|
|
87
|
+
if word_count < TRIVIAL_WORDS:
|
|
88
|
+
return BlockScore(label, word_count, "trivial", None, None)
|
|
89
|
+
grade = textstat.flesch_kincaid_grade(text)
|
|
90
|
+
ease = textstat.flesch_reading_ease(text)
|
|
91
|
+
status = "low_confidence" if word_count < MIN_WORDS else ""
|
|
92
|
+
return BlockScore(label, word_count, status, grade, ease)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def format_score(score: BlockScore) -> str:
|
|
96
|
+
"""Format a scored block as a single key=value line."""
|
|
97
|
+
status = f" status={score.status}" if score.status else ""
|
|
98
|
+
return (
|
|
99
|
+
f"{score.label} words={score.words}{status} "
|
|
100
|
+
f"flesch_kincaid_grade={score.grade} flesch_reading_ease={score.ease}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def punctuation_sentences(text: str) -> list[str]:
|
|
105
|
+
"""Return each sentence in text that contains a colon, semicolon, or dash."""
|
|
106
|
+
return [s for s in sentences_of(text) if PUNCT_MARKS.search(s)]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def fanboys_sentences(text: str) -> list[tuple[str, str]]:
|
|
110
|
+
"""Return (word, sentence) pairs for sentences opening with a FANBOYS word."""
|
|
111
|
+
found = []
|
|
112
|
+
for s in sentences_of(text):
|
|
113
|
+
m = LEADING_WORD.match(s)
|
|
114
|
+
if m and m.group(1).lower() in FANBOYS:
|
|
115
|
+
found.append((m.group(1).lower(), s))
|
|
116
|
+
return found
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def blocks_from_comments(path: Path) -> Iterator[tuple[str, str]]:
|
|
120
|
+
"""Yield (label, text) pairs for each run of consecutive whole-line comments."""
|
|
121
|
+
with open(path, "rb") as f:
|
|
122
|
+
run: list = []
|
|
123
|
+
start_line = None
|
|
124
|
+
prev_line = None
|
|
125
|
+
for tok in tokenize.tokenize(f.readline):
|
|
126
|
+
if tok.type != tokenize.COMMENT:
|
|
127
|
+
continue
|
|
128
|
+
text = tok.string.lstrip("#").strip()
|
|
129
|
+
if run and prev_line == tok.start[0] - 1:
|
|
130
|
+
run.append(text)
|
|
131
|
+
else:
|
|
132
|
+
if run:
|
|
133
|
+
yield f"line={start_line} block=comment", " ".join(run)
|
|
134
|
+
run = [text]
|
|
135
|
+
start_line = tok.start[0]
|
|
136
|
+
prev_line = tok.start[0]
|
|
137
|
+
if run:
|
|
138
|
+
yield f"line={start_line} block=comment", " ".join(run)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def blocks_from_py(path: Path) -> Iterator[tuple[str, str]]:
|
|
142
|
+
"""Yield (label, text) pairs for each docstring and comment block in a .py file."""
|
|
143
|
+
tree = ast.parse(path.read_text())
|
|
144
|
+
doc_nodes = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Module)
|
|
145
|
+
for node in ast.walk(tree):
|
|
146
|
+
if not isinstance(node, doc_nodes):
|
|
147
|
+
continue
|
|
148
|
+
doc = ast.get_docstring(node)
|
|
149
|
+
if not doc:
|
|
150
|
+
continue
|
|
151
|
+
name = getattr(node, "name", "module")
|
|
152
|
+
line = getattr(node, "lineno", 1)
|
|
153
|
+
yield f"line={line} block={name}", strip_inline_code(doc)
|
|
154
|
+
for label, text in blocks_from_comments(path):
|
|
155
|
+
yield label, strip_inline_code(text)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def blocks_from_md(path: Path) -> Iterator[tuple[str, str]]:
|
|
159
|
+
"""Yield (label, paragraph) pairs for each paragraph, with markup stripped."""
|
|
160
|
+
text = path.read_text()
|
|
161
|
+
# Blank out code blocks but keep their newlines, so line numbers stay accurate.
|
|
162
|
+
text = re.sub(
|
|
163
|
+
r"```.*?```", lambda m: "\n" * m.group(0).count("\n"), text, flags=re.DOTALL
|
|
164
|
+
)
|
|
165
|
+
text = strip_inline_code(text)
|
|
166
|
+
text = re.sub(r"!\[.*?\]\(.*?\)", "", text) # images
|
|
167
|
+
text = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", text) # links
|
|
168
|
+
text = re.sub(r"^#{1,6}\s*", "", text, flags=re.MULTILINE) # headings
|
|
169
|
+
text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text) # bold
|
|
170
|
+
text = re.sub(r"\*([^*]+)\*", r"\1", text) # italic
|
|
171
|
+
# admonition marker lines (mkdocs-style), e.g. !!! warning "title"
|
|
172
|
+
text = re.sub(r'^!!!\s+\S+(\s+".*?")?\s*$', "", text, flags=re.MULTILINE)
|
|
173
|
+
|
|
174
|
+
table_row = re.compile(r"^\s*\|.*\|\s*$")
|
|
175
|
+
para_lines: list = []
|
|
176
|
+
start_line = None
|
|
177
|
+
for i, raw_line in enumerate(text.split("\n"), start=1):
|
|
178
|
+
if table_row.match(raw_line):
|
|
179
|
+
if para_lines:
|
|
180
|
+
yield f"line={start_line} block=paragraph", "\n".join(para_lines)
|
|
181
|
+
para_lines, start_line = [], None
|
|
182
|
+
continue
|
|
183
|
+
if raw_line.strip():
|
|
184
|
+
if start_line is None:
|
|
185
|
+
start_line = i
|
|
186
|
+
para_lines.append(raw_line)
|
|
187
|
+
else:
|
|
188
|
+
if para_lines:
|
|
189
|
+
yield f"line={start_line} block=paragraph", "\n".join(para_lines)
|
|
190
|
+
para_lines, start_line = [], None
|
|
191
|
+
if para_lines:
|
|
192
|
+
yield f"line={start_line} block=paragraph", "\n".join(para_lines)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def score_file(path: Path, top: int | None, rules: list[Rule]) -> None:
|
|
196
|
+
"""Extract blocks from a file, print a summary, then each active rule's findings."""
|
|
197
|
+
if path.suffix == ".py":
|
|
198
|
+
blocks = list(blocks_from_py(path))
|
|
199
|
+
elif path.suffix == ".md":
|
|
200
|
+
blocks = list(blocks_from_md(path))
|
|
201
|
+
else:
|
|
202
|
+
typer.echo(f"Unsupported file type: {path.suffix}", err=True)
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
if not blocks:
|
|
206
|
+
print(f"file={path} status=no_prose_found")
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
summary = [f"file={path}"]
|
|
210
|
+
|
|
211
|
+
scored: list[BlockScore] = []
|
|
212
|
+
if Rule.READABILITY in rules:
|
|
213
|
+
scores = [score_block(label, text) for label, text in blocks]
|
|
214
|
+
scored = [s for s in scores if s.grade is not None]
|
|
215
|
+
trivial = sum(1 for s in scores if s.status == "trivial")
|
|
216
|
+
low_conf = sum(1 for s in scores if s.status == "low_confidence")
|
|
217
|
+
worst = max((s.grade for s in scored), default=None)
|
|
218
|
+
worst_str = f"{worst:.1f}" if worst is not None else "none"
|
|
219
|
+
summary.append(f"blocks={len(blocks)}")
|
|
220
|
+
summary.append(f"trivial={trivial}")
|
|
221
|
+
summary.append(f"low_confidence={low_conf}")
|
|
222
|
+
summary.append(f"worst_grade={worst_str}")
|
|
223
|
+
|
|
224
|
+
punct_flags: list[tuple[str, str]] = []
|
|
225
|
+
if Rule.PUNCTUATION in rules:
|
|
226
|
+
punct_flags = [
|
|
227
|
+
(label, s) for label, text in blocks for s in punctuation_sentences(text)
|
|
228
|
+
]
|
|
229
|
+
summary.append(f"punctuation_flags={len(punct_flags)}")
|
|
230
|
+
|
|
231
|
+
fanboys_flags: list[tuple[str, str, str]] = []
|
|
232
|
+
if Rule.FANBOYS in rules:
|
|
233
|
+
fanboys_flags = [
|
|
234
|
+
(label, word, s)
|
|
235
|
+
for label, text in blocks
|
|
236
|
+
for word, s in fanboys_sentences(text)
|
|
237
|
+
]
|
|
238
|
+
summary.append(f"fanboys_flags={len(fanboys_flags)}")
|
|
239
|
+
|
|
240
|
+
print(" ".join(summary))
|
|
241
|
+
|
|
242
|
+
if Rule.READABILITY in rules:
|
|
243
|
+
shown = scored
|
|
244
|
+
if top is not None:
|
|
245
|
+
shown = sorted(scored, key=lambda s: s.grade, reverse=True)[:top]
|
|
246
|
+
for score in shown:
|
|
247
|
+
print(format_score(score))
|
|
248
|
+
|
|
249
|
+
for label, sentence in punct_flags:
|
|
250
|
+
print(f"{label} sentence={json.dumps(sentence)}")
|
|
251
|
+
|
|
252
|
+
for label, word, sentence in fanboys_flags:
|
|
253
|
+
print(f"{label} word={word} sentence={json.dumps(sentence)}")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def parse_rules(value: str | None) -> list[Rule]:
|
|
257
|
+
"""Parse a comma-separated --rules value, defaulting to every rule."""
|
|
258
|
+
if not value:
|
|
259
|
+
return list(Rule)
|
|
260
|
+
selected: list[Rule] = []
|
|
261
|
+
for token in value.split(","):
|
|
262
|
+
token = token.strip()
|
|
263
|
+
if not token:
|
|
264
|
+
continue
|
|
265
|
+
try:
|
|
266
|
+
selected.append(Rule(token))
|
|
267
|
+
except ValueError:
|
|
268
|
+
valid = ", ".join(r.value for r in Rule)
|
|
269
|
+
typer.echo(f"Unknown rule: {token}. Valid: {valid}", err=True)
|
|
270
|
+
raise typer.Exit(1) from None
|
|
271
|
+
return selected
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def main(
|
|
275
|
+
paths: list[Path] = typer.Argument( # noqa: B008
|
|
276
|
+
..., help="One or more .py/.md files or directories to score."
|
|
277
|
+
),
|
|
278
|
+
top: int | None = typer.Option(
|
|
279
|
+
None, "--top", help="Show only the N worst-scoring blocks per file."
|
|
280
|
+
),
|
|
281
|
+
rules: str | None = typer.Option(
|
|
282
|
+
None,
|
|
283
|
+
"--rules",
|
|
284
|
+
help="Comma-separated rule ids to run (default: all). "
|
|
285
|
+
+ "; ".join(f"{r.value}={d}" for r, d in RULE_DESCRIPTIONS.items()),
|
|
286
|
+
),
|
|
287
|
+
) -> None:
|
|
288
|
+
"""Score .py/.md files or directories against one or more plain-English rules."""
|
|
289
|
+
selected_rules = parse_rules(rules)
|
|
290
|
+
|
|
291
|
+
files: list = []
|
|
292
|
+
for path in paths:
|
|
293
|
+
if not path.exists():
|
|
294
|
+
typer.echo(f"Path not found: {path}", err=True)
|
|
295
|
+
raise typer.Exit(1)
|
|
296
|
+
if path.is_dir():
|
|
297
|
+
found = sorted(p for p in path.rglob("*") if p.suffix in (".py", ".md"))
|
|
298
|
+
if not found:
|
|
299
|
+
typer.echo(f"No .py or .md files found under {path}", err=True)
|
|
300
|
+
raise typer.Exit(1)
|
|
301
|
+
files.extend(found)
|
|
302
|
+
else:
|
|
303
|
+
files.append(path)
|
|
304
|
+
|
|
305
|
+
for file in files:
|
|
306
|
+
score_file(file, top=top, rules=selected_rules)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
if __name__ == "__main__":
|
|
310
|
+
typer.run(main)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
name: CD
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout repository
|
|
15
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
16
|
+
with:
|
|
17
|
+
fetch-depth: 0
|
|
18
|
+
|
|
19
|
+
- name: Setup toolchain
|
|
20
|
+
uses: ./.github/actions/setup-toolchain
|
|
21
|
+
with:
|
|
22
|
+
install-just: "false"
|
|
23
|
+
|
|
24
|
+
- name: Build package
|
|
25
|
+
run: uv build
|
|
26
|
+
|
|
27
|
+
- name: Upload package artifacts
|
|
28
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
29
|
+
with:
|
|
30
|
+
name: package-dist
|
|
31
|
+
path: ./dist
|
|
32
|
+
|
|
33
|
+
- name: Build documentation
|
|
34
|
+
run: uv run mkdocs build
|
|
35
|
+
|
|
36
|
+
- name: Upload documentation artifact
|
|
37
|
+
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
|
38
|
+
with:
|
|
39
|
+
path: site/
|
|
40
|
+
|
|
41
|
+
deploy-package:
|
|
42
|
+
needs: build
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
permissions:
|
|
45
|
+
id-token: write
|
|
46
|
+
|
|
47
|
+
environment: pypi
|
|
48
|
+
|
|
49
|
+
steps:
|
|
50
|
+
- name: Download package artifacts
|
|
51
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
52
|
+
with:
|
|
53
|
+
name: package-dist
|
|
54
|
+
path: ./dist
|
|
55
|
+
|
|
56
|
+
- name: Publish package to PyPI
|
|
57
|
+
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
|
58
|
+
|
|
59
|
+
deploy-docs:
|
|
60
|
+
needs: build
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
permissions:
|
|
63
|
+
pages: write
|
|
64
|
+
id-token: write
|
|
65
|
+
contents: read
|
|
66
|
+
|
|
67
|
+
environment:
|
|
68
|
+
name: github-pages
|
|
69
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
70
|
+
|
|
71
|
+
steps:
|
|
72
|
+
- name: Deploy documentation to GitHub Pages
|
|
73
|
+
id: deployment
|
|
74
|
+
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|