langchef 0.1.0__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.
- langchef-0.1.0/.github/ISSUE_TEMPLATE/config.yml +8 -0
- langchef-0.1.0/.github/ISSUE_TEMPLATE/decision.yml +51 -0
- langchef-0.1.0/.github/ISSUE_TEMPLATE/defect.yml +52 -0
- langchef-0.1.0/.github/ISSUE_TEMPLATE/feature.yml +66 -0
- langchef-0.1.0/.github/pull_request_template.md +18 -0
- langchef-0.1.0/.github/workflows/ci.yml +38 -0
- langchef-0.1.0/.github/workflows/claim.yml +97 -0
- langchef-0.1.0/.github/workflows/pr-guard.yml +119 -0
- langchef-0.1.0/.github/workflows/release.yml +106 -0
- langchef-0.1.0/.gitignore +14 -0
- langchef-0.1.0/.python-version +1 -0
- langchef-0.1.0/AGENTS.md +270 -0
- langchef-0.1.0/CONTRIBUTING.md +65 -0
- langchef-0.1.0/DECISIONS.md +192 -0
- langchef-0.1.0/LICENSE +202 -0
- langchef-0.1.0/NON-GOALS.md +77 -0
- langchef-0.1.0/PKG-INFO +446 -0
- langchef-0.1.0/README.md +422 -0
- langchef-0.1.0/TRACKER.md +124 -0
- langchef-0.1.0/adapters/README.md +9 -0
- langchef-0.1.0/adapters/claude-code/.claude-plugin/plugin.json +10 -0
- langchef-0.1.0/adapters/claude-code/README.md +37 -0
- langchef-0.1.0/adapters/claude-code/commands/langchef-calibrate.md +25 -0
- langchef-0.1.0/adapters/claude-code/commands/langchef-design.md +20 -0
- langchef-0.1.0/adapters/claude-code/commands/langchef-experiment.md +17 -0
- langchef-0.1.0/adapters/claude-code/skills/langchef-eval/SKILL.md +141 -0
- langchef-0.1.0/docs/.nojekyll +0 -0
- langchef-0.1.0/docs/AGENT-CONTRACT.md +91 -0
- langchef-0.1.0/docs/RESERVE-NAMES.md +76 -0
- langchef-0.1.0/docs/byod.html +188 -0
- langchef-0.1.0/docs/cli.html +182 -0
- langchef-0.1.0/docs/concepts.html +219 -0
- langchef-0.1.0/docs/index.html +280 -0
- langchef-0.1.0/docs/integrations.html +147 -0
- langchef-0.1.0/docs/numbers.html +400 -0
- langchef-0.1.0/docs/quickstart.html +23 -0
- langchef-0.1.0/docs/ref-agreement.html +387 -0
- langchef-0.1.0/docs/ref-compare.html +368 -0
- langchef-0.1.0/docs/ref-design.html +413 -0
- langchef-0.1.0/docs/ref-judging.html +271 -0
- langchef-0.1.0/docs/ref-sampling.html +227 -0
- langchef-0.1.0/docs/ref-taxonomy.html +257 -0
- langchef-0.1.0/docs/search-index.json +944 -0
- langchef-0.1.0/docs/search.js +59 -0
- langchef-0.1.0/docs/start.html +387 -0
- langchef-0.1.0/docs/style.css +277 -0
- langchef-0.1.0/dogfood/README.md +237 -0
- langchef-0.1.0/dogfood/__init__.py +6 -0
- langchef-0.1.0/dogfood/app.py +446 -0
- langchef-0.1.0/dogfood/build.py +102 -0
- langchef-0.1.0/dogfood/corpus.py +409 -0
- langchef-0.1.0/dogfood/label.py +51 -0
- langchef-0.1.0/packs/classification/README.md +43 -0
- langchef-0.1.0/packs/classification/metrics.py +143 -0
- langchef-0.1.0/packs/classification/pack.toml +37 -0
- langchef-0.1.0/packs/classification/rubrics/README.md +28 -0
- langchef-0.1.0/packs/genai-rag/pack.toml +39 -0
- langchef-0.1.0/packs/genai-rag/rubrics/README.md +15 -0
- langchef-0.1.0/pyproject.toml +85 -0
- langchef-0.1.0/scripts/assert_no_credentials.py +27 -0
- langchef-0.1.0/scripts/build_docs.py +3255 -0
- langchef-0.1.0/scripts/render_contract.py +120 -0
- langchef-0.1.0/scripts/verify.sh +61 -0
- langchef-0.1.0/src/langchef/__init__.py +7 -0
- langchef-0.1.0/src/langchef/cli/__init__.py +1 -0
- langchef-0.1.0/src/langchef/cli/calibrate_cmd.py +476 -0
- langchef-0.1.0/src/langchef/cli/common.py +112 -0
- langchef-0.1.0/src/langchef/cli/design_cmd.py +468 -0
- langchef-0.1.0/src/langchef/cli/experiment_cmd.py +314 -0
- langchef-0.1.0/src/langchef/cli/judge_cmd.py +149 -0
- langchef-0.1.0/src/langchef/cli/main.py +209 -0
- langchef-0.1.0/src/langchef/cli/memo_cmd.py +126 -0
- langchef-0.1.0/src/langchef/cli/power_cmd.py +122 -0
- langchef-0.1.0/src/langchef/cli/workspace_cmd.py +90 -0
- langchef-0.1.0/src/langchef/connect/__init__.py +1 -0
- langchef-0.1.0/src/langchef/core/__init__.py +7 -0
- langchef-0.1.0/src/langchef/core/agreement.py +246 -0
- langchef-0.1.0/src/langchef/core/compare.py +568 -0
- langchef-0.1.0/src/langchef/core/contract.py +200 -0
- langchef-0.1.0/src/langchef/core/credentials.py +27 -0
- langchef-0.1.0/src/langchef/core/delta.py +548 -0
- langchef-0.1.0/src/langchef/core/design.py +411 -0
- langchef-0.1.0/src/langchef/core/emit.py +32 -0
- langchef-0.1.0/src/langchef/core/exits.py +29 -0
- langchef-0.1.0/src/langchef/core/gates.py +93 -0
- langchef-0.1.0/src/langchef/core/retrieval.py +95 -0
- langchef-0.1.0/src/langchef/core/sampling.py +140 -0
- langchef-0.1.0/src/langchef/core/taxonomy.py +213 -0
- langchef-0.1.0/src/langchef/judge/__init__.py +1 -0
- langchef-0.1.0/src/langchef/judge/cache.py +100 -0
- langchef-0.1.0/src/langchef/judge/example.py +41 -0
- langchef-0.1.0/src/langchef/judge/providers.py +534 -0
- langchef-0.1.0/src/langchef/judge/rubric.py +62 -0
- langchef-0.1.0/src/langchef/judge/runner.py +181 -0
- langchef-0.1.0/src/langchef/packs/__init__.py +39 -0
- langchef-0.1.0/src/langchef/packs/loader.py +152 -0
- langchef-0.1.0/src/langchef/packs/manifest.py +243 -0
- langchef-0.1.0/src/langchef/render/__init__.py +1 -0
- langchef-0.1.0/src/langchef/render/memo.py +189 -0
- langchef-0.1.0/src/langchef/workspace/__init__.py +1 -0
- langchef-0.1.0/src/langchef/workspace/config.py +117 -0
- langchef-0.1.0/src/langchef/workspace/dataset.py +149 -0
- langchef-0.1.0/src/langchef/workspace/experiments.py +181 -0
- langchef-0.1.0/src/langchef/workspace/formats.py +106 -0
- langchef-0.1.0/src/langchef/workspace/ledger.py +52 -0
- langchef-0.1.0/src/langchef/workspace/paths.py +111 -0
- langchef-0.1.0/src/langchef/workspace/runs.py +142 -0
- langchef-0.1.0/src/langchef/workspace/scaffold.py +117 -0
- langchef-0.1.0/tests/cassettes/answer-quality.replay.json +3 -0
- langchef-0.1.0/tests/cassettes/openai-chat-completions.json +160 -0
- langchef-0.1.0/tests/conftest.py +50 -0
- langchef-0.1.0/tests/test_agreement.py +172 -0
- langchef-0.1.0/tests/test_boundaries.py +317 -0
- langchef-0.1.0/tests/test_cli.py +91 -0
- langchef-0.1.0/tests/test_compare.py +428 -0
- langchef-0.1.0/tests/test_dataset.py +155 -0
- langchef-0.1.0/tests/test_delta.py +469 -0
- langchef-0.1.0/tests/test_design.py +214 -0
- langchef-0.1.0/tests/test_docs_in_sync.py +46 -0
- langchef-0.1.0/tests/test_dogfood.py +364 -0
- langchef-0.1.0/tests/test_exits.py +23 -0
- langchef-0.1.0/tests/test_flow.py +675 -0
- langchef-0.1.0/tests/test_judge.py +358 -0
- langchef-0.1.0/tests/test_litellm_path.py +384 -0
- langchef-0.1.0/tests/test_no_credentials.py +43 -0
- langchef-0.1.0/tests/test_pack_classification.py +227 -0
- langchef-0.1.0/tests/test_packs.py +310 -0
- langchef-0.1.0/tests/test_power.py +133 -0
- langchef-0.1.0/tests/test_retrieval.py +181 -0
- langchef-0.1.0/tests/test_sampling.py +154 -0
- langchef-0.1.0/tests/test_taxonomy.py +120 -0
- langchef-0.1.0/tests/test_waiter.py +608 -0
- langchef-0.1.0/tests/test_workspace.py +164 -0
- langchef-0.1.0/uv.lock +1503 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
blank_issues_enabled: false
|
|
2
|
+
contact_links:
|
|
3
|
+
- name: The agent contract
|
|
4
|
+
url: https://deepskandpal.github.io/LangChef/cli.html
|
|
5
|
+
about: What each command does, what it writes, and what the exit codes mean. Read this before filing about CLI behaviour.
|
|
6
|
+
- name: How the numbers work
|
|
7
|
+
url: https://deepskandpal.github.io/LangChef/numbers.html
|
|
8
|
+
about: What a verdict, an interval and a detection limit actually mean here.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Decision
|
|
2
|
+
description: A call to make, not code to write
|
|
3
|
+
labels: ["type:decision", "lifecycle:spec", "agent:needs-human"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Decisions are left open on purpose, with the evidence that closes them named up front, so
|
|
9
|
+
that none of them becomes a standing debate. Settled ones go into `DECISIONS.md` with a
|
|
10
|
+
date and are not reopened.
|
|
11
|
+
- type: textarea
|
|
12
|
+
id: description
|
|
13
|
+
attributes:
|
|
14
|
+
label: Description
|
|
15
|
+
description: The question, in one line.
|
|
16
|
+
validations: {required: true}
|
|
17
|
+
- type: textarea
|
|
18
|
+
id: background
|
|
19
|
+
attributes:
|
|
20
|
+
label: Background
|
|
21
|
+
description: Why it is open, and what it costs to get wrong.
|
|
22
|
+
validations: {required: true}
|
|
23
|
+
- type: textarea
|
|
24
|
+
id: options
|
|
25
|
+
attributes:
|
|
26
|
+
label: Options
|
|
27
|
+
value: |
|
|
28
|
+
### (a)
|
|
29
|
+
- **For:**
|
|
30
|
+
- **Against:**
|
|
31
|
+
|
|
32
|
+
### (b)
|
|
33
|
+
- **For:**
|
|
34
|
+
- **Against:**
|
|
35
|
+
validations: {required: true}
|
|
36
|
+
- type: textarea
|
|
37
|
+
id: default
|
|
38
|
+
attributes:
|
|
39
|
+
label: The default if nobody decides
|
|
40
|
+
description: |
|
|
41
|
+
There is always one, and it is usually the bad one — a threshold hard-coded where nobody
|
|
42
|
+
reviews it. Name it, so that drifting is a choice rather than an accident.
|
|
43
|
+
validations: {required: true}
|
|
44
|
+
- type: textarea
|
|
45
|
+
id: acceptance
|
|
46
|
+
attributes:
|
|
47
|
+
label: Acceptance criteria (closes when)
|
|
48
|
+
description: The evidence, the date, or the trigger. Not "when we have time".
|
|
49
|
+
value: |
|
|
50
|
+
- [ ] The call is written into `DECISIONS.md` as a new dated entry
|
|
51
|
+
validations: {required: true}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Defect
|
|
2
|
+
description: Behaviour that is wrong, not merely absent
|
|
3
|
+
labels: ["type:defect", "lifecycle:ready"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
**A wrong number is worse than a crash here, because nothing complains.** If a figure looks
|
|
9
|
+
wrong, this is the most valuable issue you can file.
|
|
10
|
+
- type: textarea
|
|
11
|
+
id: description
|
|
12
|
+
attributes:
|
|
13
|
+
label: Description
|
|
14
|
+
description: The behaviour, and what it should be instead.
|
|
15
|
+
validations: {required: true}
|
|
16
|
+
- type: textarea
|
|
17
|
+
id: background
|
|
18
|
+
attributes:
|
|
19
|
+
label: Background
|
|
20
|
+
description: How it was found, and why it matters more or less than it looks like it does.
|
|
21
|
+
validations: {required: true}
|
|
22
|
+
- type: textarea
|
|
23
|
+
id: repro
|
|
24
|
+
attributes:
|
|
25
|
+
label: How to see it
|
|
26
|
+
description: The commands. Prefer the dogfood workspace, which needs no API key.
|
|
27
|
+
render: shell
|
|
28
|
+
validations: {required: true}
|
|
29
|
+
- type: dropdown
|
|
30
|
+
id: kind
|
|
31
|
+
attributes:
|
|
32
|
+
label: What kind of wrong
|
|
33
|
+
options:
|
|
34
|
+
- A wrong number — silent, and looks like a right number
|
|
35
|
+
- A wrong verdict — the arithmetic is right, the conclusion is not
|
|
36
|
+
- An integrity gap — the result can be manufactured
|
|
37
|
+
- A crash or an error
|
|
38
|
+
- An inconvenience
|
|
39
|
+
validations: {required: true}
|
|
40
|
+
- type: textarea
|
|
41
|
+
id: acceptance
|
|
42
|
+
attributes:
|
|
43
|
+
label: Acceptance criteria
|
|
44
|
+
value: |
|
|
45
|
+
- [ ] A test that fails before the fix and passes after
|
|
46
|
+
- [ ] `./scripts/verify.sh` stays green (10/10)
|
|
47
|
+
validations: {required: true}
|
|
48
|
+
- type: input
|
|
49
|
+
id: start
|
|
50
|
+
attributes:
|
|
51
|
+
label: Start at
|
|
52
|
+
placeholder: src/langchef/judge/runner.py
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
name: Feature
|
|
2
|
+
description: A capability that does not exist yet
|
|
3
|
+
labels: ["type:feature", "lifecycle:spec"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Write this so somebody — or something — can pick it up cold, months from now, with no
|
|
9
|
+
memory of this conversation. That is the whole standard. See any existing issue for the
|
|
10
|
+
shape; [#28](https://github.com/deepskandpal/LangChef/issues/28) is a good example.
|
|
11
|
+
- type: textarea
|
|
12
|
+
id: description
|
|
13
|
+
attributes:
|
|
14
|
+
label: Description
|
|
15
|
+
description: One or two sentences, imperative. The capability, not the implementation.
|
|
16
|
+
validations: {required: true}
|
|
17
|
+
- type: textarea
|
|
18
|
+
id: background
|
|
19
|
+
attributes:
|
|
20
|
+
label: Background
|
|
21
|
+
description: |
|
|
22
|
+
Why this exists and what is worse without it. Where it came from — a build-order section,
|
|
23
|
+
a defect, a conversation. Be concrete: "it would be nice" is not a reason.
|
|
24
|
+
validations: {required: true}
|
|
25
|
+
- type: textarea
|
|
26
|
+
id: dependencies
|
|
27
|
+
attributes:
|
|
28
|
+
label: Dependencies
|
|
29
|
+
value: |
|
|
30
|
+
- **Blocked by:** #
|
|
31
|
+
- **Blocks:** #
|
|
32
|
+
- **Related:** #
|
|
33
|
+
- **Needs a person:**
|
|
34
|
+
validations: {required: true}
|
|
35
|
+
- type: textarea
|
|
36
|
+
id: acceptance
|
|
37
|
+
attributes:
|
|
38
|
+
label: Acceptance criteria
|
|
39
|
+
description: |
|
|
40
|
+
Observable and checkable. Include the ways this could be built *wrongly* — a silently
|
|
41
|
+
dropped row, a threshold that stops being declared, an interval that stops matching its
|
|
42
|
+
point estimate. Those are the criteria that earn their place.
|
|
43
|
+
value: |
|
|
44
|
+
- [ ]
|
|
45
|
+
- [ ] `./scripts/verify.sh` stays green (10/10)
|
|
46
|
+
validations: {required: true}
|
|
47
|
+
- type: textarea
|
|
48
|
+
id: notes
|
|
49
|
+
attributes:
|
|
50
|
+
label: Implementation notes
|
|
51
|
+
description: Start at which file, what constrains it, what has already gone wrong nearby.
|
|
52
|
+
value: |
|
|
53
|
+
**Start at** `src/langchef/`
|
|
54
|
+
validations: {required: true}
|
|
55
|
+
- type: textarea
|
|
56
|
+
id: scope
|
|
57
|
+
attributes:
|
|
58
|
+
label: Out of scope
|
|
59
|
+
description: What this is not. Build order §10 lists things that are never built — check it.
|
|
60
|
+
- type: dropdown
|
|
61
|
+
id: area
|
|
62
|
+
attributes:
|
|
63
|
+
label: Area
|
|
64
|
+
description: Area labels are collision boundaries. Two agents must not hold the same one.
|
|
65
|
+
options: [core, judge, workspace, cli, connect, packs, adapters, dogfood, docs, ci]
|
|
66
|
+
validations: {required: true}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Closes #
|
|
2
|
+
|
|
3
|
+
## What changed
|
|
4
|
+
|
|
5
|
+
<!-- One paragraph. What a reviewer needs to know before reading the diff. -->
|
|
6
|
+
|
|
7
|
+
## Checks
|
|
8
|
+
|
|
9
|
+
- [ ] `./scripts/verify.sh` — all 10 steps pass locally
|
|
10
|
+
- [ ] Any new statistic has a known-answer test against an independent implementation
|
|
11
|
+
- [ ] Generated files were regenerated, not hand-edited (`docs/*.html`, `docs/AGENT-CONTRACT.md`)
|
|
12
|
+
- [ ] No provider SDK is imported outside `src/langchef/judge/providers.py`
|
|
13
|
+
- [ ] If a rubric-scoring check changed, `VERSION` in `providers.py` was bumped
|
|
14
|
+
- [ ] If this settles a decision, `DECISIONS.md` has a new dated entry
|
|
15
|
+
|
|
16
|
+
## What I did not do
|
|
17
|
+
|
|
18
|
+
<!-- Scope left out, and why. An honest gap here is worth more than a tidy diff. -->
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
name: verify (python ${{ matrix.python }})
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
strategy:
|
|
18
|
+
# Both legs always report. A break on one version should not hide the
|
|
19
|
+
# result on the other, which is the whole reason for testing two.
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
# The range pyproject declares: requires-python = ">=3.12,<3.14".
|
|
23
|
+
# Advertising support for a version nothing runs is a claim, not a fact.
|
|
24
|
+
python: ["3.12", "3.13"]
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
|
|
28
|
+
- name: Install uv
|
|
29
|
+
uses: astral-sh/setup-uv@v5
|
|
30
|
+
with:
|
|
31
|
+
enable-cache: true
|
|
32
|
+
|
|
33
|
+
# Every check lives in scripts/verify.sh so that what runs here and what
|
|
34
|
+
# a person runs locally cannot drift apart. See that file for the list.
|
|
35
|
+
- name: Verify
|
|
36
|
+
run: ./scripts/verify.sh
|
|
37
|
+
env:
|
|
38
|
+
UV_PYTHON: ${{ matrix.python }}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Claiming an issue, done by the repository rather than by the claimant.
|
|
2
|
+
#
|
|
3
|
+
# AGENTS.md used to ask a claimant to comment and then move the issue to
|
|
4
|
+
# `lifecycle:doing`. Outside contributors cannot set labels, so step two was
|
|
5
|
+
# impossible for exactly the people the instruction was written for, and for
|
|
6
|
+
# three hours on 27 August #25 looked free after it had been claimed. Two
|
|
7
|
+
# contributors wrote the same fix.
|
|
8
|
+
#
|
|
9
|
+
# The first version of this workflow matched claim phrasings with a regex. On
|
|
10
|
+
# 28 August @raappo wrote "I'd love to" where the pattern expected "I'd like
|
|
11
|
+
# to", #64 stayed `lifecycle:ready` for thirteen hours with a PR already open
|
|
12
|
+
# against it, and a second contributor duplicated the work overnight. The
|
|
13
|
+
# symptom of a missed phrase is silence, which looks exactly like no claim.
|
|
14
|
+
#
|
|
15
|
+
# So there is no phrase list any more. **Any comment from a person on a
|
|
16
|
+
# `lifecycle:ready` issue claims it.** On a board this specific a comment is
|
|
17
|
+
# almost never anything else, and the asymmetry is decisive: a false positive
|
|
18
|
+
# costs one label and is undone by replying, while a false negative costs
|
|
19
|
+
# somebody a night of duplicated work. The confirmation comment says how to
|
|
20
|
+
# undo it, which is what makes the trade safe.
|
|
21
|
+
|
|
22
|
+
name: Claim
|
|
23
|
+
|
|
24
|
+
on:
|
|
25
|
+
issue_comment:
|
|
26
|
+
types: [created]
|
|
27
|
+
|
|
28
|
+
permissions:
|
|
29
|
+
issues: write
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
claim:
|
|
33
|
+
if: ${{ !github.event.issue.pull_request && github.event.comment.user.type != 'Bot' }}
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/github-script@v7
|
|
37
|
+
with:
|
|
38
|
+
script: |
|
|
39
|
+
const owner = context.repo.owner;
|
|
40
|
+
const repo = context.repo.repo;
|
|
41
|
+
const issue_number = context.issue.number;
|
|
42
|
+
const claimant = context.payload.comment.user.login;
|
|
43
|
+
|
|
44
|
+
if (claimant === owner) return; // the maintainer is not claiming
|
|
45
|
+
|
|
46
|
+
const labels = context.payload.issue.labels.map((l) => l.name);
|
|
47
|
+
const ready = labels.includes("lifecycle:ready");
|
|
48
|
+
const doing = labels.includes("lifecycle:doing");
|
|
49
|
+
if (!ready && !doing) return; // spec, blocked or done: not claimable
|
|
50
|
+
|
|
51
|
+
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
52
|
+
owner, repo, issue_number, per_page: 100,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// The first person who was not the maintainer or a bot. Under this
|
|
56
|
+
// rule that comment is the claim, whatever it said.
|
|
57
|
+
const first = comments.find(
|
|
58
|
+
(c) =>
|
|
59
|
+
c.id !== context.payload.comment.id &&
|
|
60
|
+
c.user.login !== owner &&
|
|
61
|
+
c.user.type !== "Bot",
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (doing && first && first.user.login !== claimant) {
|
|
65
|
+
await github.rest.issues.createComment({
|
|
66
|
+
owner, repo, issue_number,
|
|
67
|
+
body:
|
|
68
|
+
`@${claimant} before you spend time on this: ` +
|
|
69
|
+
`@${first.user.login} claimed it on ${first.created_at.slice(0, 10)} ` +
|
|
70
|
+
`(${first.html_url}).\n\n` +
|
|
71
|
+
"Please check with them before continuing, or take something else from " +
|
|
72
|
+
"[`lifecycle:ready`](../../issues?q=is%3Aopen+label%3Alifecycle%3Aready). " +
|
|
73
|
+
"If they have gone quiet, say so here and a maintainer will reassign it.",
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!ready) return; // already doing, and it is theirs
|
|
79
|
+
|
|
80
|
+
await github.rest.issues.addLabels({
|
|
81
|
+
owner, repo, issue_number, labels: ["lifecycle:doing"],
|
|
82
|
+
});
|
|
83
|
+
await github.rest.issues
|
|
84
|
+
.removeLabel({ owner, repo, issue_number, name: "lifecycle:ready" })
|
|
85
|
+
.catch(() => {});
|
|
86
|
+
await github.rest.issues.createComment({
|
|
87
|
+
owner, repo, issue_number,
|
|
88
|
+
body:
|
|
89
|
+
`Marked \`lifecycle:doing\` for @${claimant}.\n\n` +
|
|
90
|
+
"You cannot set labels on a repository you do not have write access to, " +
|
|
91
|
+
"so the repository does it for you. Branch as " +
|
|
92
|
+
`\`${issue_number}-short-slug\` and open a PR when ready.\n\n` +
|
|
93
|
+
"**If you were only asking a question and did not mean to claim this**, " +
|
|
94
|
+
"say so and a maintainer will move it back to `lifecycle:ready`. " +
|
|
95
|
+
"We would rather label one question by mistake than let two people " +
|
|
96
|
+
"write the same patch, which has happened twice here.",
|
|
97
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# The side door.
|
|
2
|
+
#
|
|
3
|
+
# claim.yml guards the front door: someone comments on an issue, the repository
|
|
4
|
+
# moves the label and says who holds it. PR #59 walked straight past it by
|
|
5
|
+
# opening a pull request without ever commenting, which is a door claim.yml
|
|
6
|
+
# cannot see. By then the work was already written, which is the cost this is
|
|
7
|
+
# meant to avoid.
|
|
8
|
+
#
|
|
9
|
+
# Two checks, both disclosures rather than refusals. Neither fails the build:
|
|
10
|
+
# a maintainer decides, and a bot that blocks a good contribution is worse than
|
|
11
|
+
# one that says something a maintainer can ignore.
|
|
12
|
+
#
|
|
13
|
+
# SECURITY: pull_request_target runs with the base repository's token, which is
|
|
14
|
+
# what makes commenting on a fork PR possible at all. It is only safe because
|
|
15
|
+
# nothing here checks out or executes the pull request's code. Do not add
|
|
16
|
+
# actions/checkout to this file.
|
|
17
|
+
|
|
18
|
+
name: PR guard
|
|
19
|
+
|
|
20
|
+
on:
|
|
21
|
+
pull_request_target:
|
|
22
|
+
types: [opened, reopened]
|
|
23
|
+
|
|
24
|
+
permissions:
|
|
25
|
+
pull-requests: write
|
|
26
|
+
issues: read
|
|
27
|
+
contents: read
|
|
28
|
+
|
|
29
|
+
jobs:
|
|
30
|
+
guard:
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/github-script@v7
|
|
34
|
+
with:
|
|
35
|
+
script: |
|
|
36
|
+
const owner = context.repo.owner;
|
|
37
|
+
const repo = context.repo.repo;
|
|
38
|
+
const pr = context.payload.pull_request;
|
|
39
|
+
const author = pr.user.login;
|
|
40
|
+
const number = pr.number;
|
|
41
|
+
const notes = [];
|
|
42
|
+
|
|
43
|
+
// ---- 1. is the issue this closes already held by somebody else?
|
|
44
|
+
const text = `${pr.title}\n${pr.body || ""}`;
|
|
45
|
+
const refs = new Set();
|
|
46
|
+
const KEYWORD =
|
|
47
|
+
/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?|implements?|part of)\s+#(\d+)/gi;
|
|
48
|
+
for (const m of text.matchAll(KEYWORD)) refs.add(Number(m[1]));
|
|
49
|
+
// A bare #N in the title counts too: #59 said "(#25)" and nothing else.
|
|
50
|
+
for (const m of (pr.title || "").matchAll(/#(\d+)/g)) refs.add(Number(m[1]));
|
|
51
|
+
|
|
52
|
+
const CLAIM = new RegExp(
|
|
53
|
+
[
|
|
54
|
+
"starting\\s+(on|work)", "i'?m\\s+starting", "i\\s+am\\s+starting",
|
|
55
|
+
"i\\s+am\\s+ready\\s+to\\s+start", "claiming\\s+this", "taking\\s+this",
|
|
56
|
+
"working\\s+on\\s+this", "i'?d\\s+like\\s+to\\s+(work\\s+on|take)",
|
|
57
|
+
"i\\s+would\\s+like\\s+to\\s+work\\s+on",
|
|
58
|
+
].join("|"),
|
|
59
|
+
"i",
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
for (const n of refs) {
|
|
63
|
+
if (n === number) continue;
|
|
64
|
+
let issue;
|
|
65
|
+
try {
|
|
66
|
+
issue = (await github.rest.issues.get({owner, repo, issue_number: n})).data;
|
|
67
|
+
} catch { continue; }
|
|
68
|
+
if (issue.pull_request) continue;
|
|
69
|
+
|
|
70
|
+
const labels = issue.labels.map((l) => (typeof l === "string" ? l : l.name));
|
|
71
|
+
if (!labels.includes("lifecycle:doing")) continue;
|
|
72
|
+
|
|
73
|
+
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
74
|
+
owner, repo, issue_number: n, per_page: 100,
|
|
75
|
+
});
|
|
76
|
+
const prior = comments.filter(
|
|
77
|
+
(c) =>
|
|
78
|
+
c.user.login !== author &&
|
|
79
|
+
c.user.login !== owner &&
|
|
80
|
+
c.user.type !== "Bot" &&
|
|
81
|
+
CLAIM.test(c.body || ""),
|
|
82
|
+
);
|
|
83
|
+
if (!prior.length) continue;
|
|
84
|
+
|
|
85
|
+
const first = prior[0];
|
|
86
|
+
notes.push(
|
|
87
|
+
`**#${n} is already held.** @${first.user.login} claimed it on ` +
|
|
88
|
+
`${first.created_at.slice(0, 10)} (${first.html_url}), and it is labelled ` +
|
|
89
|
+
"`lifecycle:doing`.\n\n" +
|
|
90
|
+
"This is worth checking before you spend more time on it. Our claim " +
|
|
91
|
+
"automation only watches for a comment on the issue, so opening a pull " +
|
|
92
|
+
"request directly walks past it, which is our gap rather than yours.",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---- 2. does it change behaviour without changing a test?
|
|
97
|
+
const files = await github.paginate(github.rest.pulls.listFiles, {
|
|
98
|
+
owner, repo, pull_number: number, per_page: 100,
|
|
99
|
+
});
|
|
100
|
+
const paths = files.map((f) => f.filename);
|
|
101
|
+
const touchesSrc = paths.some((p) => p.startsWith("src/"));
|
|
102
|
+
const touchesTests = paths.some((p) => p.startsWith("tests/"));
|
|
103
|
+
if (touchesSrc && !touchesTests) {
|
|
104
|
+
notes.push(
|
|
105
|
+
"**This changes `src/` without changing `tests/`.**\n\n" +
|
|
106
|
+
"Most issues here list the tests they expect in their acceptance criteria, " +
|
|
107
|
+
"and `scripts/verify.sh` runs the suite as step 8. If the change is genuinely " +
|
|
108
|
+
"untestable, say so in the description and it will be taken at face value.",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!notes.length) return;
|
|
113
|
+
await github.rest.issues.createComment({
|
|
114
|
+
owner, repo, issue_number: number,
|
|
115
|
+
body:
|
|
116
|
+
notes.join("\n\n---\n\n") +
|
|
117
|
+
"\n\n<sub>Posted automatically by `pr-guard.yml`. Neither of these fails the " +
|
|
118
|
+
"build; a maintainer decides.</sub>",
|
|
119
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Publishing to PyPI, with no credential anywhere in this repository.
|
|
2
|
+
#
|
|
3
|
+
# Trusted Publishing (OIDC): PyPI is configured to trust this exact repository,
|
|
4
|
+
# workflow filename and environment, and GitHub mints a short-lived token per
|
|
5
|
+
# run. There is no API token to create, share, paste into a secret, rotate, or
|
|
6
|
+
# leak. If this file is renamed, or the environment below changes, PyPI stops
|
|
7
|
+
# trusting it and the upload fails closed.
|
|
8
|
+
#
|
|
9
|
+
# The build job runs the same verify.sh that guards every push, on both
|
|
10
|
+
# supported interpreters, before anything is uploaded. A release that skipped
|
|
11
|
+
# the checks would defeat the point of having them.
|
|
12
|
+
|
|
13
|
+
name: release
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
push:
|
|
17
|
+
tags: ["v*"]
|
|
18
|
+
workflow_dispatch:
|
|
19
|
+
inputs:
|
|
20
|
+
target:
|
|
21
|
+
description: "Where to publish"
|
|
22
|
+
required: true
|
|
23
|
+
default: testpypi
|
|
24
|
+
type: choice
|
|
25
|
+
options: [testpypi, pypi]
|
|
26
|
+
|
|
27
|
+
jobs:
|
|
28
|
+
verify:
|
|
29
|
+
name: verify (python ${{ matrix.python }})
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
strategy:
|
|
32
|
+
fail-fast: false
|
|
33
|
+
matrix:
|
|
34
|
+
python: ["3.12", "3.13"]
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
- uses: astral-sh/setup-uv@v5
|
|
38
|
+
with:
|
|
39
|
+
enable-cache: true
|
|
40
|
+
- run: ./scripts/verify.sh
|
|
41
|
+
env:
|
|
42
|
+
UV_PYTHON: ${{ matrix.python }}
|
|
43
|
+
|
|
44
|
+
build:
|
|
45
|
+
needs: verify
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
- uses: astral-sh/setup-uv@v5
|
|
50
|
+
|
|
51
|
+
# The tag is the source of truth for the version. A tag that disagrees
|
|
52
|
+
# with pyproject.toml means one of the two is a typo, and guessing which
|
|
53
|
+
# is how a wrong version reaches PyPI permanently: uploads cannot be
|
|
54
|
+
# replaced, only yanked.
|
|
55
|
+
- name: Tag matches pyproject version
|
|
56
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
57
|
+
run: |
|
|
58
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
59
|
+
pkg=$(grep -m1 '^version' pyproject.toml | cut -d'"' -f2)
|
|
60
|
+
if [ "$tag" != "$pkg" ]; then
|
|
61
|
+
echo "tag v$tag does not match pyproject version $pkg" >&2
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
echo "v$tag matches pyproject"
|
|
65
|
+
|
|
66
|
+
- run: uv build
|
|
67
|
+
|
|
68
|
+
# Catches the metadata problems that make a page look broken on PyPI,
|
|
69
|
+
# before the upload rather than after it.
|
|
70
|
+
- name: Check the distribution metadata
|
|
71
|
+
run: uv run --isolated --no-project --with twine twine check --strict dist/*
|
|
72
|
+
|
|
73
|
+
- uses: actions/upload-artifact@v4
|
|
74
|
+
with:
|
|
75
|
+
name: dist
|
|
76
|
+
path: dist/
|
|
77
|
+
|
|
78
|
+
testpypi:
|
|
79
|
+
needs: build
|
|
80
|
+
if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
|
|
81
|
+
runs-on: ubuntu-latest
|
|
82
|
+
environment: testpypi
|
|
83
|
+
permissions:
|
|
84
|
+
id-token: write # the whole credential story: a short-lived OIDC token
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/download-artifact@v4
|
|
87
|
+
with:
|
|
88
|
+
name: dist
|
|
89
|
+
path: dist/
|
|
90
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
91
|
+
with:
|
|
92
|
+
repository-url: https://test.pypi.org/legacy/
|
|
93
|
+
|
|
94
|
+
pypi:
|
|
95
|
+
needs: build
|
|
96
|
+
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.target == 'pypi')
|
|
97
|
+
runs-on: ubuntu-latest
|
|
98
|
+
environment: pypi
|
|
99
|
+
permissions:
|
|
100
|
+
id-token: write
|
|
101
|
+
steps:
|
|
102
|
+
- uses: actions/download-artifact@v4
|
|
103
|
+
with:
|
|
104
|
+
name: dist
|
|
105
|
+
path: dist/
|
|
106
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|