mergeset 0.0.2__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.
- mergeset-0.0.2/.claude/agents/mergeset-analyst.md +49 -0
- mergeset-0.0.2/.claude/skills/mergeset/SKILL.md +94 -0
- mergeset-0.0.2/.gitattributes +1 -0
- mergeset-0.0.2/.github/workflows/ci.yml +68 -0
- mergeset-0.0.2/.gitignore +120 -0
- mergeset-0.0.2/DECISIONS.md +120 -0
- mergeset-0.0.2/HANDOFF.md +335 -0
- mergeset-0.0.2/LICENSE +21 -0
- mergeset-0.0.2/PKG-INFO +115 -0
- mergeset-0.0.2/README.md +94 -0
- mergeset-0.0.2/RESEARCH.md +110 -0
- mergeset-0.0.2/mergeset/__init__.py +150 -0
- mergeset-0.0.2/mergeset/__main__.py +5 -0
- mergeset-0.0.2/mergeset/analysis.py +465 -0
- mergeset-0.0.2/mergeset/attribution.py +190 -0
- mergeset-0.0.2/mergeset/base.py +257 -0
- mergeset-0.0.2/mergeset/cli.py +338 -0
- mergeset-0.0.2/mergeset/gitops.py +406 -0
- mergeset-0.0.2/mergeset/log.py +249 -0
- mergeset-0.0.2/mergeset/misc/CHANGELOG.md +20 -0
- mergeset-0.0.2/mergeset/oracle.py +268 -0
- mergeset-0.0.2/mergeset/report.py +382 -0
- mergeset-0.0.2/mergeset/solve.py +475 -0
- mergeset-0.0.2/mergeset/sources.py +231 -0
- mergeset-0.0.2/mergeset/stacks.py +193 -0
- mergeset-0.0.2/mergeset/validation.py +401 -0
- mergeset-0.0.2/notes/fixtures/README.md +8 -0
- mergeset-0.0.2/notes/fixtures/vitest-577-587-import-crash.txt +36 -0
- mergeset-0.0.2/notes/fixtures/vitest-579-631-drift.txt +83 -0
- mergeset-0.0.2/pyproject.toml +162 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/.superseded-mergeset.py.bak +111 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/REPORT-1-adhoc.md +81 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/REPORT.md +215 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/conflicts.json +130 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/evaluate.sh +29 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/evaluations.jsonl +16 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/mkbranches.py +42 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/mkhtml.py +201 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/mss_driver.py +135 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/preoracle.json +1870 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/preoracle.py +142 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/prs.json +1 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/report.html +71 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/seqmerge.json +1263 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/seqmerge.py +117 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/show.py +10 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/tool-run/README.md +9 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/tool-run/evaluations.jsonl +11 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/tool-run/tool-REPORT.md +121 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/tool-run/tool-report.html +131 -0
- mergeset-0.0.2/reports/cosmograph-2026-09-08/tool-run/toolrun.py +47 -0
- mergeset-0.0.2/tests/conftest.py +47 -0
- mergeset-0.0.2/tests/test_attribution.py +131 -0
- mergeset-0.0.2/tests/test_git.py +172 -0
- mergeset-0.0.2/tests/test_safety.py +221 -0
- mergeset-0.0.2/tests/test_solve.py +196 -0
- mergeset-0.0.2/tests/test_stacks.py +128 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mergeset-analyst
|
|
3
|
+
description: Works out which of a repository's in-flight branches or PRs can be merged together, and explains the result. Use when asked "which of these PRs can land together", "what conflicts with what", "can I merge these branches", "why can't these two go together", or when several PRs are open on one repo and a landing order is needed. Also use to explain an existing mergeset report.
|
|
4
|
+
tools: Bash, Read, Grep, Glob
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You determine which subsets of a set of in-flight changes can be merged and validated together, using the `mergeset` package. You do not merge anything into a default branch, push to an existing branch, force-push, or delete a remote ref — ever, and not even when asked; say that you cannot and hand back the plan instead.
|
|
8
|
+
|
|
9
|
+
## The shape of the job
|
|
10
|
+
|
|
11
|
+
The tool is doing a search where every step is expensive (a merge plus a test run), so your job is mostly to *avoid* spending those steps. Work in this order and do not skip ahead.
|
|
12
|
+
|
|
13
|
+
**1. Establish the base, and say it out loud.** Every change must be measured against the same commit. If the candidates have different bases — stacked PRs, or a PR opened against a non-default branch — `analyze` falls back to their common merge base and tells you. Confirm that is what the user wants before spending anything.
|
|
14
|
+
|
|
15
|
+
**2. Run the free checks first.** Start with `--merge-only`. It costs seconds, finds every textual conflict, and on real branch sets that is most of them:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
python -m mergeset prs OWNER/REPO --repo . --author USER --updated-within-hours 48 \
|
|
19
|
+
--merge-only --report-dir reports/$(date +%F)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Read that report before running anything expensive. It gives you the conflict graph, the stack structure, and which changes cannot merge onto the base at all.
|
|
23
|
+
|
|
24
|
+
**3. Only then validate.** Give the project's *real* command — never assume pytest. Look at the repo first: `package.json` means a JS runner, `pyproject.toml` a Python one, and a build step is usually a prerequisite of the tests rather than part of them. If the project needs more than one step, use the library API with `staged_validation` so "failed to build" stays distinguishable from "tests failed", and pass `reuse_worktree=` so an expensive install is paid once.
|
|
25
|
+
|
|
26
|
+
Budget it: `--max-seconds`, `--max-evaluations`. Partial results are labelled partial and are still useful.
|
|
27
|
+
|
|
28
|
+
**4. Report the plan, not the search.** The user wants: merge these, in this order, and here is what you lose and why. Conflicts explained as *why* — which files clash textually, or which tests fail — not merely that they do.
|
|
29
|
+
|
|
30
|
+
## What must make you stop
|
|
31
|
+
|
|
32
|
+
- **The report says ABORTED, or a note says an evaluation could not be performed.** The results are not an answer. Find out what broke (usually a bad path or a wrong validation command), fix it, re-run. Do not present partial results from an aborted run as findings.
|
|
33
|
+
- **The base does not validate on its own.** `mergeset` refuses to start in this case. Do not pass `check_base=False` to get past it; fix the base or the command.
|
|
34
|
+
- **A monotonicity violation is reported.** Something failed that a passing set contains. Say so plainly: it means a flaky test or a change that fixes another, and the conclusions are not trustworthy until it is understood.
|
|
35
|
+
- **A plan recommends dropping something the user cares about.** Weights are heuristic. Offer to re-run with `weight=` favouring it, or pinned.
|
|
36
|
+
|
|
37
|
+
## Things that look like answers and are not
|
|
38
|
+
|
|
39
|
+
- A green CI badge on a PR is against *that PR's own base branch*. If its base is not the base you are merging onto, it says nothing, and `mergeset` will tell you it is ignoring it.
|
|
40
|
+
- A set marked "mergeable with assisted resolution" is not a clean merge. The resolution diff is saved; show it to the user and let them decide.
|
|
41
|
+
- File-overlap components are a way to find conflicts cheaply, not proof that two changes are independent. A whole-repo test run can fail on changes that share no file.
|
|
42
|
+
|
|
43
|
+
## Assisted merges
|
|
44
|
+
|
|
45
|
+
`--resolver claude` (or `resolver=claude_code_resolver()`) lets a subagent attempt a conflicted merge. The policy is narrow on purpose: mechanical conflicts only — both sides adding imports, both appending to a changelog, adjacent independent edits, regenerable lockfiles — and an explicit refusal whenever resolving would require deciding which behaviour is correct. Never widen it. Anything resolved this way is flagged in the report and its diff kept.
|
|
46
|
+
|
|
47
|
+
## Explaining an existing report
|
|
48
|
+
|
|
49
|
+
`python -m mergeset show-log --log-path <path>` prints what is already known without evaluating anything. The evaluation log is the source of truth and reports regenerate from it, so answer questions from the log rather than re-running.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mergeset
|
|
3
|
+
description: Work out which branches or pull requests can be merged together, and in what order. Use when several changes are in flight on one repository and someone asks which of them can land together, what conflicts with what and why, whether a set of branches can be merged, why two changes cannot go together, or for a landing plan and integration branches. Also use to read or explain an existing mergeset report or evaluation log.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# mergeset
|
|
7
|
+
|
|
8
|
+
Finds the maximal sets of changes that merge cleanly and still validate, and the minimal conflicts that stop the rest.
|
|
9
|
+
|
|
10
|
+
## Do the cheap thing first
|
|
11
|
+
|
|
12
|
+
Every expensive step is a merge plus a test run. The order below is the order of cost, and skipping ahead wastes minutes per step.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# 1. free: textual conflicts, stacks, changes that cannot merge onto base at all
|
|
16
|
+
python -m mergeset branches feat-a feat-b feat-c --base main --merge-only
|
|
17
|
+
|
|
18
|
+
# 2. the same for PRs, with the forge metadata
|
|
19
|
+
python -m mergeset prs OWNER/REPO --repo . --author USER --updated-within-hours 48 --merge-only
|
|
20
|
+
|
|
21
|
+
# 3. only now, with the project's real command and a budget
|
|
22
|
+
python -m mergeset prs OWNER/REPO --repo . --validate-command 'pnpm run test' \
|
|
23
|
+
--max-seconds 3600 --report-dir reports/$(date +%F) --integration-branches
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`--merge-only` costs seconds and usually finds most of the conflicts. Read that report before spending anything.
|
|
27
|
+
|
|
28
|
+
## Use the library when the project needs more than one command
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from mergeset import (
|
|
32
|
+
analyze,
|
|
33
|
+
fetch_pull_requests,
|
|
34
|
+
pr_changes,
|
|
35
|
+
staged_validation,
|
|
36
|
+
ValidationStage,
|
|
37
|
+
file_fingerprint,
|
|
38
|
+
markdown_report,
|
|
39
|
+
html_report,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
validate = staged_validation(
|
|
43
|
+
[
|
|
44
|
+
ValidationStage(
|
|
45
|
+
"setup",
|
|
46
|
+
"pnpm install --frozen-lockfile",
|
|
47
|
+
fingerprint=file_fingerprint("pnpm-lock.yaml"),
|
|
48
|
+
),
|
|
49
|
+
ValidationStage("build", "pnpm run build"), # a prerequisite, not a test
|
|
50
|
+
ValidationStage("test", "pnpm run test"),
|
|
51
|
+
ValidationStage("lint", "pnpm run lint", required=False),
|
|
52
|
+
]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
prs = fetch_pull_requests("owner/repo", author="someone")
|
|
56
|
+
analysis = analyze(
|
|
57
|
+
".",
|
|
58
|
+
list(pr_changes(".", prs)),
|
|
59
|
+
base="origin/main",
|
|
60
|
+
validate=validate,
|
|
61
|
+
reuse_worktree="/tmp/mergeset-wt", # install paid once, not per evaluation
|
|
62
|
+
log_path="reports/evaluations.jsonl",
|
|
63
|
+
max_seconds=3600,
|
|
64
|
+
)
|
|
65
|
+
print(markdown_report(analysis))
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`analysis.merge_plan()` gives, per set: `merge` (the refs to actually merge — stack tips only), `changes` (everything that lands), `dropped`, and the weights.
|
|
69
|
+
|
|
70
|
+
## Never do
|
|
71
|
+
|
|
72
|
+
Merge into a default branch, push to an existing branch, force-push, or delete a remote ref. `mergeset` only ever creates *new local* branches; keep it that way. Hand back the plan and let a human land it.
|
|
73
|
+
|
|
74
|
+
## Stop and say so when
|
|
75
|
+
|
|
76
|
+
- The report says **ABORTED**, or a note says an evaluation could not be performed — the run did not fail, it did not happen. Usually a bad `reuse_worktree` path or a wrong validation command.
|
|
77
|
+
- The **base does not validate on its own** — `analyze` refuses to start. Fix the base or the command; do not pass `check_base=False` to get past it.
|
|
78
|
+
- A **monotonicity violation** is reported — a flaky test, or a change that fixes another. The conclusions are not trustworthy until it is understood.
|
|
79
|
+
|
|
80
|
+
## Read, do not re-run
|
|
81
|
+
|
|
82
|
+
The evaluation log is the single source of truth and everything regenerates from it.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
python -m mergeset show-log --log-path reports/evaluations.jsonl
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Answer questions about an existing analysis from the log. A second run over the same sets costs nothing, but re-deriving what the log already says costs a reader's trust.
|
|
89
|
+
|
|
90
|
+
## What the output does not mean
|
|
91
|
+
|
|
92
|
+
- A green CI badge is against the PR's **own** base branch. If that is not the base you are merging onto, it says nothing — the report will say it is being ignored.
|
|
93
|
+
- "Mergeable with assisted resolution" is **not** a clean merge. Show the saved resolution diff and let a human decide.
|
|
94
|
+
- File-overlap components find conflicts cheaply; they do not prove two changes are independent. A whole-repo test run can fail on changes that share no file.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*.ipynb linguist-documentation
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# wads CI — calls the reusable workflow hosted in i2mint/wads.
|
|
2
|
+
#
|
|
3
|
+
# All configuration comes from this repo's pyproject.toml [tool.wads.ci.*].
|
|
4
|
+
# To customize the workflow itself (rare), replace this file with the
|
|
5
|
+
# full inline template `wads/data/github_ci_uv.yml` from i2mint/wads.
|
|
6
|
+
#
|
|
7
|
+
# Pinning: `@master` floats with wads. If you need version stability for
|
|
8
|
+
# a release-sensitive repo, change `@master` to a wads tag (e.g. `@0.2.15`;
|
|
9
|
+
# tags have no `v` prefix). A stub whose `secrets:` block passes the JSON
|
|
10
|
+
# transport (the default below) needs a tag from a release after 0.2.14 —
|
|
11
|
+
# older tags don't declare that secret and GitHub then rejects the
|
|
12
|
+
# workflow at parse time.
|
|
13
|
+
# CI failure does not block a published release — it blocks the publish
|
|
14
|
+
# step itself — so floating master is generally safe.
|
|
15
|
+
#
|
|
16
|
+
# Permissions: GitHub validates that the caller grants AT LEAST the
|
|
17
|
+
# permissions any job in the called workflow requests — at workflow-parse
|
|
18
|
+
# time, not at run-time, even if the job would be skipped via `if:`.
|
|
19
|
+
# The reusable workflow needs:
|
|
20
|
+
# contents: write for the publish job's version-bump push-back
|
|
21
|
+
# and for the github-pages job's gh-pages branch push
|
|
22
|
+
# pages: write for the github-pages job's REST API Pages config
|
|
23
|
+
# Both default to `write` on org-account GITHUB_TOKEN and need to be
|
|
24
|
+
# granted explicitly on personal-account callers (where the default is
|
|
25
|
+
# read-only). No `id-token: write` needed — the publish-github-pages
|
|
26
|
+
# action uses peaceiris/actions-gh-pages (branch-based) + REST API,
|
|
27
|
+
# not the OIDC `actions/deploy-pages` flow.
|
|
28
|
+
name: Continuous Integration
|
|
29
|
+
on: [push, pull_request]
|
|
30
|
+
jobs:
|
|
31
|
+
ci:
|
|
32
|
+
uses: i2mint/wads/.github/workflows/uv-ci.yml@master
|
|
33
|
+
permissions:
|
|
34
|
+
contents: write
|
|
35
|
+
pages: write
|
|
36
|
+
id-token: write
|
|
37
|
+
# Transport: named secrets, matching every other working i2mint repo. The
|
|
38
|
+
# generated default was the JSON transport (`toJSON(toJSON(secrets))`),
|
|
39
|
+
# and with it every run on this repo came back `action_required` with no
|
|
40
|
+
# jobs and could not even be re-run. Original comment follows.
|
|
41
|
+
#
|
|
42
|
+
# Transport: this repo's whole `secrets` context, serialized into the one
|
|
43
|
+
# secret the reusable workflow declares. Double-encoded (toJSON twice) so
|
|
44
|
+
# the value is a single line — a multiline secret would register its `{`
|
|
45
|
+
# and `}` lines as global log masks. Any secret name works; there is no
|
|
46
|
+
# fixed list to fall outside of. (Cross-owner `secrets: inherit` does not
|
|
47
|
+
# propagate secrets, so it cannot replace this.)
|
|
48
|
+
#
|
|
49
|
+
# Note this hands EVERY secret this repo can read — including org-level
|
|
50
|
+
# ones — to the called workflow. For a minimal secret surface (only the
|
|
51
|
+
# names you list), regenerate with
|
|
52
|
+
# wads-migrate ci-to-stub --transport named
|
|
53
|
+
#
|
|
54
|
+
# *Which* of these become job env vars — and which are required — is
|
|
55
|
+
# driven entirely by [tool.wads.ci.env] in pyproject.toml; nothing is
|
|
56
|
+
# exported unless declared there (`wads-secrets add VAR_NAME` declares
|
|
57
|
+
# one and can set its value). Non-sensitive values don't need a secret:
|
|
58
|
+
# use [tool.wads.ci.env].defaults (committed literals) or a repository
|
|
59
|
+
# *variable* (`gh variable set NAME`) — declared names fall back to
|
|
60
|
+
# repo variables automatically.
|
|
61
|
+
secrets:
|
|
62
|
+
PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
|
63
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
64
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
65
|
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
|
66
|
+
HUGGINGFACE_TOKEN: ${{ secrets.HUGGINGFACE_TOKEN }}
|
|
67
|
+
KAGGLE_USERNAME: ${{ secrets.KAGGLE_USERNAME }}
|
|
68
|
+
KAGGLE_KEY: ${{ secrets.KAGGLE_KEY }}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
.claude/handoffs/
|
|
2
|
+
.claude/scratch/
|
|
3
|
+
|
|
4
|
+
# Byte-compiled / optimized / DLL files
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
*$py.class
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
.DS_Store
|
|
11
|
+
# C extensions
|
|
12
|
+
*.so
|
|
13
|
+
|
|
14
|
+
# TLS certificates
|
|
15
|
+
## Ignore all PEM files anywhere
|
|
16
|
+
*.pem
|
|
17
|
+
## Also ignore any certs directory
|
|
18
|
+
certs/
|
|
19
|
+
|
|
20
|
+
# Distribution / packaging
|
|
21
|
+
.Python
|
|
22
|
+
build/
|
|
23
|
+
develop-eggs/
|
|
24
|
+
dist/
|
|
25
|
+
downloads/
|
|
26
|
+
eggs/
|
|
27
|
+
.eggs/
|
|
28
|
+
lib/
|
|
29
|
+
lib64/
|
|
30
|
+
parts/
|
|
31
|
+
sdist/
|
|
32
|
+
var/
|
|
33
|
+
wheels/
|
|
34
|
+
*.egg-info/
|
|
35
|
+
.installed.cfg
|
|
36
|
+
*.egg
|
|
37
|
+
MANIFEST
|
|
38
|
+
_build
|
|
39
|
+
|
|
40
|
+
# PyInstaller
|
|
41
|
+
# Usually these files are written by a python script from a template
|
|
42
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
43
|
+
*.manifest
|
|
44
|
+
*.spec
|
|
45
|
+
|
|
46
|
+
# Installer logs
|
|
47
|
+
pip-log.txt
|
|
48
|
+
pip-delete-this-directory.txt
|
|
49
|
+
|
|
50
|
+
# Unit test / coverage reports
|
|
51
|
+
htmlcov/
|
|
52
|
+
.tox/
|
|
53
|
+
.coverage
|
|
54
|
+
.coverage.*
|
|
55
|
+
.cache
|
|
56
|
+
nosetests.xml
|
|
57
|
+
coverage.xml
|
|
58
|
+
*.cover
|
|
59
|
+
.hypothesis/
|
|
60
|
+
.pytest_cache/
|
|
61
|
+
|
|
62
|
+
# Translations
|
|
63
|
+
*.mo
|
|
64
|
+
*.pot
|
|
65
|
+
|
|
66
|
+
# Django stuff:
|
|
67
|
+
*.log
|
|
68
|
+
local_settings.py
|
|
69
|
+
db.sqlite3
|
|
70
|
+
|
|
71
|
+
# Flask stuff:
|
|
72
|
+
instance/
|
|
73
|
+
.webassets-cache
|
|
74
|
+
|
|
75
|
+
# Scrapy stuff:
|
|
76
|
+
.scrapy
|
|
77
|
+
|
|
78
|
+
# Sphinx documentation
|
|
79
|
+
docs/_build/
|
|
80
|
+
docs/*
|
|
81
|
+
|
|
82
|
+
# PyBuilder
|
|
83
|
+
target/
|
|
84
|
+
|
|
85
|
+
# Jupyter Notebook
|
|
86
|
+
.ipynb_checkpoints
|
|
87
|
+
|
|
88
|
+
# pyenv
|
|
89
|
+
.python-version
|
|
90
|
+
|
|
91
|
+
# celery beat schedule file
|
|
92
|
+
celerybeat-schedule
|
|
93
|
+
|
|
94
|
+
# SageMath parsed files
|
|
95
|
+
*.sage.py
|
|
96
|
+
|
|
97
|
+
# Environments
|
|
98
|
+
.env
|
|
99
|
+
.venv
|
|
100
|
+
env/
|
|
101
|
+
venv/
|
|
102
|
+
ENV/
|
|
103
|
+
env.bak/
|
|
104
|
+
venv.bak/
|
|
105
|
+
|
|
106
|
+
# Spyder project settings
|
|
107
|
+
.spyderproject
|
|
108
|
+
.spyproject
|
|
109
|
+
|
|
110
|
+
# Rope project settings
|
|
111
|
+
.ropeproject
|
|
112
|
+
|
|
113
|
+
# mkdocs documentation
|
|
114
|
+
/site
|
|
115
|
+
|
|
116
|
+
# mypy
|
|
117
|
+
.mypy_cache/
|
|
118
|
+
|
|
119
|
+
# PyCharm
|
|
120
|
+
.idea
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Decisions
|
|
2
|
+
|
|
3
|
+
Every non-obvious choice, with the reason. Entries are append-only; when a decision is reversed, the old entry stays and the new one says what changed.
|
|
4
|
+
|
|
5
|
+
## D1 — The name is `mergeset`
|
|
6
|
+
|
|
7
|
+
Free on PyPI (404), `github.com/i2mint/mergeset` did not exist, and `import mergeset` shadows nothing in the stdlib or in any well-known package. It also names the exact object the tool computes: the maximal *merge sets*. Runners-up, all also free: `greenset`, `braidy`, `knitset`, `amalgo`, `lattix`, `maxmerge`. Rejected because taken on PyPI: `confluo`, `conflux`, `medley`, `plexus`.
|
|
8
|
+
|
|
9
|
+
## D2 — The unit is a **change**, not a branch or a PR
|
|
10
|
+
|
|
11
|
+
A change is a commit range `base..head`. Branches, pull requests and explicit commit lists are *sources* that produce changes; a PR source additionally carries metadata (number, author, title, CI status, base ref, draft flag, stack relationship) in `Change.meta`.
|
|
12
|
+
|
|
13
|
+
The reason to pick one word and hold it: the same analysis has to serve "these five local branches", "these PRs updated in the last 48h", and "these cherry-picks", and if the vocabulary tracks the source then every function ends up with three names for one idea. Only the *sources* module knows the word "PR"; the solver, the log and the report never say it. Metadata rides along for weighting and reporting but the solver never reads it — that keeps the search honest and testable without a repository.
|
|
14
|
+
|
|
15
|
+
## D3 — The CLI adapter is `cw`, not `argh`
|
|
16
|
+
|
|
17
|
+
The spec asked for `argh`. `argh` is LGPL-3.0-or-later and is being removed fleet-wide (per the `python-dispatching` skill), so this package uses `cw`, which is MIT with zero runtime dependencies. The durable part is `mergeset/cli.py::_dispatch_funcs` — an SSOT list of plain functions — and only the last line binds it to an adapter, so this decision costs one line to reverse and the same list is what a future HTTP or MCP surface would consume.
|
|
18
|
+
|
|
19
|
+
## D4 — The evaluation log is the single source of truth, and it is append-only JSONL
|
|
20
|
+
|
|
21
|
+
`.mergeset/evaluations.jsonl`, one JSON object per evaluated set. Everything else — the conflict set, the known-good/known-bad closure, the merge plan, both reports — is *derived* and re-derivable, so a report can be regenerated without re-running anything and a crashed run resumes for free.
|
|
22
|
+
|
|
23
|
+
Two consequences worth naming:
|
|
24
|
+
|
|
25
|
+
- The monotone closure is computed lazily from the rows, never materialized. Materializing it would mean writing `2**|S|` implied facts for every passing set.
|
|
26
|
+
- The persistence seam is one keyword (`store`): anything with `append(jdict)` and `__iter__` works, so an in-memory log (tests), a `dol` store, or a database table are all drop-ins. The default is a plain file because the default must work with no dependency at all.
|
|
27
|
+
|
|
28
|
+
## D5 — Monotonicity is a prior, not an invariant, and violations are reported rather than smoothed over
|
|
29
|
+
|
|
30
|
+
The theory says good sets are downward-closed. Reality disagrees in two ways: a change can contain the *fix* that makes another change work (so a superset of a bad set passes), and flaky tests break closure outright. `EvaluationLog.monotonicity_violations()` detects the contradiction and `analyze` surfaces it as a loud note on the report instead of silently trusting a wrong inference. `flake_tolerant(validate, retries=n)` is the cheap mitigation; the honest answer is telling the user their result is not trustworthy.
|
|
31
|
+
|
|
32
|
+
## D6 — Cost ordering is the architecture
|
|
33
|
+
|
|
34
|
+
The pipeline is ordered by price, and nothing expensive runs until everything cheap has had its say:
|
|
35
|
+
|
|
36
|
+
1. **free** — a change whose own CI is already red against its base is a conflict of size one;
|
|
37
|
+
2. **milliseconds** — pairwise `git merge-tree --write-tree` finds textual conflicts with no worktree and no checkout (this is why git >= 2.38 is a hard requirement, checked up front with a fixable error message);
|
|
38
|
+
3. **milliseconds** — file-overlap connected components split one `2**n` search into several small independent ones, whose answers combine by Cartesian product;
|
|
39
|
+
4. **minutes each** — only then the real merge-and-test oracle.
|
|
40
|
+
|
|
41
|
+
Each of 1–3 can be switched off by one keyword, because each embeds an assumption (see D7) that may not hold for a given repository.
|
|
42
|
+
|
|
43
|
+
## D7 — File-overlap decomposition is an assumption, and it is opt-out
|
|
44
|
+
|
|
45
|
+
Two changes touching disjoint file sets provably cannot conflict *textually*. They can still conflict *semantically* — change A adds a caller of a function change B deletes, in a file A never touched. The decomposition is on by default because the speedup is enormous and the failure mode is rare, and it is `decompose=False` away from being off. It is written down here rather than buried, because it is the one place where the tool can be confidently wrong.
|
|
46
|
+
|
|
47
|
+
## D8 — Assisted merges are always flagged, never silently clean
|
|
48
|
+
|
|
49
|
+
An AI resolver may attempt a conflicted merge, under a policy that is deliberately narrow: mechanical conflicts only (both sides added imports, both appended to a changelog, adjacent independent edits, regenerable lockfiles), and an explicit refusal whenever resolving would require guessing which behaviour is correct. Any set that merged this way is recorded with `merge.assisted = True` and the resolution diff saved, and it is marked in both reports. A merge that needed a machine's judgement is a different fact from a merge that was clean, and the report must never conflate them.
|
|
50
|
+
|
|
51
|
+
## D9 — Weights mean "cost of dropping this change"
|
|
52
|
+
|
|
53
|
+
The hitting set is weighted so that when something must be dropped, the tool drops the *cheapest* work. Default weight is `1 + log1p(lines changed)`: bigger changes are worth more, log-shaped so one huge branch cannot dominate. Overridable with any `Change -> float`, and PR metadata (labels, author, age) is right there in `change.meta` for a policy that wants it.
|
|
54
|
+
|
|
55
|
+
## D10 — The oracle returns a record, not a bit
|
|
56
|
+
|
|
57
|
+
Every evaluation returns which tests failed, which files they live in, the merge order used, the conflicting paths and the duration. The solver uses the merge outcome's attribution to aim `quickxplain` at the changes actually implicated instead of halving blindly, and the report can say *why* two changes conflict rather than only *that* they do. Returning a bit would have been simpler and would have thrown away the most useful thing an expensive run produces.
|
|
58
|
+
|
|
59
|
+
## D11 — Merge order is deterministic and recorded
|
|
60
|
+
|
|
61
|
+
Order should not matter when merges are clean. When it does matter, the run must still be reproducible, so `merge_order` sorts stacked changes parent-first and otherwise smallest-first (which surfaces cheap conflicts early), and the order used is stored on every log row.
|
|
62
|
+
|
|
63
|
+
## D12 — Only *new* local branches are ever created
|
|
64
|
+
|
|
65
|
+
`create_integration_branch` refuses to touch a branch that already exists unless explicitly forced, and nothing in the package pushes. This is a hard safety boundary, not a default: the tool is pointed at repositories where existing branches are other people's in-flight work.
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## D13 — `git merge-tree` is always chained through base (found by TEST)
|
|
69
|
+
|
|
70
|
+
`git merge-tree A B` merges at `merge-base(A, B)`. When two candidates were cut at different times — one is stale — that merge base is older than the base we actually care about, and the base branch's own commits are reported as conflicts. Measured by the TEST workstream on a real 15-PR set: the naive pairwise sweep found **13 conflicting pairs where only 2 were real**, eleven false positives from a single stale branch.
|
|
71
|
+
|
|
72
|
+
Every git operation now goes through `gitops.merge_sequence`, which merges onto base one change at a time in the object database (`merge-tree --write-tree acc head` → `commit-tree`). A pleasant consequence: a conflicted set costs milliseconds and never touches the filesystem, and a clean set yields a real commit, so the worktree step becomes a checkout rather than a sequence of merges.
|
|
73
|
+
|
|
74
|
+
## D14 — Validation is an ordered sequence of named stages (found by TEST)
|
|
75
|
+
|
|
76
|
+
Real projects do not have "the test command". On the repository TEST measured, `pnpm run build:cosmos` is a *prerequisite* of testing — without it seven test files fail to collect — so a validator that ran only the test command would report a false failure. And the expensive step is not the tests (9 s) but the install (33 s), which only needs to run when the lockfile moves.
|
|
77
|
+
|
|
78
|
+
So a validator is a list of `ValidationStage(name, command, fingerprint=, required=)`. The failing stage's name is prefixed onto every failure id, keeping "failed to build" distinguishable from "tests failed" in the log; a fingerprinted stage re-runs only when its inputs change; and `reuse_worktree=` keeps one tree for the whole run so the install is amortized. Lint is a non-required stage by default, matching how the repository's own CI gates.
|
|
79
|
+
|
|
80
|
+
## D15 — Stacks are a forest, and the constraint lives in the core (found by TEST)
|
|
81
|
+
|
|
82
|
+
TEST's framing, adopted verbatim: **a valid candidate set is downward-closed under the parent relation**. Not "a prefix of a chain" — stacks branch, so siblings share a parent. Four consequences, all in `mergeset/stacks.py`: close a set before evaluating it; drop a change's whole descendant cone when dropping it; merge only the *tips*, since a tip brings its ancestors (15 changes became 6 merges); and weight a change by its cone, or the hitting set will drop a stack root believing it dropped one small PR. On the observed forest this cut the search space from 32768 subsets to 576.
|
|
83
|
+
|
|
84
|
+
Shrinking must respect the closure too. QuickXplain proposes arbitrary subsets, and an arbitrary subset of a stack is a fiction: `{577, 587}` and `{575, 577, 587}` and `{575, 576, 577, 587}` all produce the same merged tree, so TEST's log recorded two duplicate evaluations under different labels. The solver now closes every subset before evaluating it, which both makes the log honest and collapses those onto one cache entry.
|
|
85
|
+
|
|
86
|
+
## D16 — A forge's CI verdict is only trusted when the bases agree (found by TEST)
|
|
87
|
+
|
|
88
|
+
GitHub reported PR #602 as `MERGEABLE` / `CLEAN` while it would not merge onto `main` at all — because GitHub was evaluating it against its own base branch, which had moved on. `analyze` compares each change's `base_ref` against the base being merged onto and ignores the forge's signal when they differ, saying so in the report. TEST rates this the single highest-value cheap check in the run: it excluded one change and its whole cone before any test.
|
|
89
|
+
|
|
90
|
+
## D17 — "Could not run the experiment" is not "the experiment failed" (found by TEST)
|
|
91
|
+
|
|
92
|
+
TEST passed a bad `reuse_worktree` and got a confident report: *Plan 1 — merge 0 of 15 · 24 expensive evaluations spent · complete*. Every checkout error had been recorded as a textual merge conflict. Three changes:
|
|
93
|
+
|
|
94
|
+
- `MergeOutcome.reason` is `'conflict'` or `'error'`, and only a conflict may enter the conflict set.
|
|
95
|
+
- An error produces `Verdict.ERROR`, and the search **aborts** on the first one. Continuing past an untestable evaluation manufactures conflicts out of infrastructure problems.
|
|
96
|
+
- `reuse_worktree` is validated (absolute path, real worktree) with an error message naming the fix — the original failure was passing `True` to an `Optional[str]`.
|
|
97
|
+
|
|
98
|
+
The tell in the bad reports was that `conflicting_files` was empty; a real textual conflict always names files.
|
|
99
|
+
|
|
100
|
+
## D18 — The base is evaluated before anything else
|
|
101
|
+
|
|
102
|
+
One evaluation of the empty set, on base alone. If the base does not validate, every subsequent failure is meaningless and "nothing can be merged" gets reported as a finding rather than as the misconfiguration it is. It would also have caught an earlier run where `detect_runner` classified a TypeScript repository as pytest — it checked for a `tests/` directory before looking at `package.json` — and ran pytest in it for twelve evaluations. `detect_runner` now ranks manifests above directory names, and the default validator refuses to guess rather than running the wrong command.
|
|
103
|
+
|
|
104
|
+
## D19 — File-overlap components are a search strategy, not a soundness claim (found by TEST)
|
|
105
|
+
|
|
106
|
+
The original version combined per-component results and presented the combination as an answer. TEST produced the counterexample: `{#579, #631}` is a real conflict spanning two components — #631's drift test asserts that committed schema artifacts match TypeScript sources, and #579 edits those sources while touching none of #631's files. No evaluated subset had ever contained both, so the tool recommended a 13-change plan that fails.
|
|
107
|
+
|
|
108
|
+
Decomposition is sound for *textual* conflicts and unsound for anything a whole-repo run can see: generated artifacts, barrel exports, snapshots, type checks, project-wide lint. It is kept, because finding a conflict inside a small component is finding it cheaply — but it now only *seeds*. The components are searched first, and then a global search runs with every conflict they found already known, so it starts nearly finished. A validator may opt out by declaring `validate.component_local = True` (as `merge_only_validation` does), and the "combine freely" claim was removed from the report unless that declaration is present.
|
|
109
|
+
|
|
110
|
+
## D20 — The oracle's failure output attributes blame (fixtures from TEST)
|
|
111
|
+
|
|
112
|
+
`mergeset/attribution.py` mines a failure for the changes it implicates, using three signals in increasing order of strength: every path in the failure block (source frames included, not just the failing test's file); the changes that touched those paths; and — decisively — the identifiers the output names, matched against each candidate's *added* diff lines.
|
|
113
|
+
|
|
114
|
+
The third signal is not a refinement. In TEST's `{#579, #631}` case the failing test lives in a file #631 added, while the culprit #579 shares no file with it, so file-level attribution accuses the innocent change; only matching `pointColorHopDirection` / `pointColorHopSeeds` / `TraversalDirectionType` against the diffs finds #579. In the `{#577, #587}` case four suites fail to *collect*, so there are no test ids at all — just a stack trace whose actionable frame is a source file. Both fixtures are committed under `notes/fixtures/` and are the tests.
|
|
115
|
+
|
|
116
|
+
Attribution is only ever a *hint*: it narrows the shrink, and a wrong hint costs one wasted check before falling back to unguided halving. It never decides a verdict.
|
|
117
|
+
|
|
118
|
+
## D21 — An empty log is still a log
|
|
119
|
+
|
|
120
|
+
`log = log or EvaluationLog(...)` silently discarded a caller-supplied log, because `__len__` makes an empty one falsy — on the first run, which is the run where it matters. `EvaluationLog.__bool__` now returns True, and the call site tests `is None`.
|