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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/contribute-directive.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,17 @@ consultants, retroactive credit corrections.
/contribute @<github-login> weight:<float>
```

- `@<github-login>` — GitHub username, must match `github:` field of
- `@<github-login>` — GitHub username, must match the `github:` field of
someone in `.edpa/config/people.yaml`. Detect resolves the login to
the canonical person id.
- `@<person-id>` — alternatively, the canonical EDPA person id from
`people.yaml` (e.g. `@bob-pm`). An id always resolves to itself and takes
precedence over a github-handle collision. **Use this for multi-contract
people who share a github handle** (e.g. `bob-arch` and `bob-pm` both
`github: bob-arch-e2e`): the shared handle is ambiguous and credits only
one contract, so address the intended one by id. A token matching neither
a github handle nor a person id is credited as-is and earns 0h —
`detect_contributors` warns on stderr so typos don't vanish silently.
- `weight:<float>` — non-negative number; this becomes the signal's
contribution to that person's `contribution_score` on the item.
Recommended range: 0.5 to 5.0 (in line with auto-detected signal
Expand Down
20 changes: 20 additions & 0 deletions plugin/edpa/scripts/detect_contributors.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,15 @@ def load_people_map(edpa_root: Path) -> dict[str, str]:
mapping[p["email"].lower()] = pid
if p.get("name"):
mapping[p["name"].lower()] = pid
# Canonical id always resolves to itself and takes precedence over any
# github/email/name collision. Lets `/contribute @<id>` target a specific
# contract for multi-contract people who share a github handle (R-2:
# bob-arch + bob-pm share one login, so the shared handle is ambiguous —
# address them by id). ids are unique, so this pass is collision-free.
for p in data.get("people", []) or []:
pid = p.get("id", "")
if pid:
mapping[pid.lower()] = pid
return mapping


Expand Down Expand Up @@ -520,14 +529,25 @@ def aggregate_signals(signals: list[dict],
# Resolve every signal's login → person_id, normalising case.
by_person: dict[str, list[dict]] = defaultdict(list)
total_score = 0.0
unknown: set[str] = set()
for sig in signals:
login = sig["login"]
person_id = people_map.get(login.lower(), login)
if login.lower() not in people_map:
unknown.add(login)
# Preserve a clean signal record for YAML — drop the working
# `login` field, add resolved person id later in contributor entry.
clean = {k: v for k, v in sig.items() if k != "login"}
by_person[person_id].append(clean)
total_score += sig["weight"]
# Surface tokens that resolved to neither a known github handle nor a
# person id — these are credited as-is and the engine awards 0h, so a
# silent typo would otherwise vanish without a trace.
for tok in sorted(unknown):
print(f"WARNING: contribution token '{tok}' matches no github handle "
f"or person id in people.yaml — credited as-is (engine awards 0h "
f"unless it is a real person id; typo or external contributor?).",
file=sys.stderr)

if total_score <= 0:
return None
Expand Down
38 changes: 38 additions & 0 deletions tests/test_detect_contributors.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,44 @@
import detect_contributors as dc # noqa: E402


# ─── R-2: multi-contract addressable by id ──────────────────────────────────


def _people_root(tmp_path, people):
cfg = tmp_path / ".edpa" / "config"
cfg.mkdir(parents=True)
(cfg / "people.yaml").write_text(yaml.safe_dump({"people": people}))
return tmp_path / ".edpa"


def test_load_people_map_resolves_id_to_itself(tmp_path):
"""R-2: two contracts sharing one github handle are each addressable by
their unique id via `/contribute @<id>` (the shared handle is ambiguous)."""
root = _people_root(tmp_path, [
{"id": "bob-arch", "github": "bob-shared", "email": "ba@x"},
{"id": "bob-pm", "github": "bob-shared", "email": "bp@x"},
])
m = dc.load_people_map(root)
assert m["bob-arch"] == "bob-arch"
assert m["bob-pm"] == "bob-pm"
assert m["bob-shared"] in ("bob-arch", "bob-pm") # shared handle: one contract


def test_aggregate_credits_contract_by_id(tmp_path):
"""A /contribute signal addressed to an id credits that exact id even when
the github handle is shared with another contract."""
root = _people_root(tmp_path, [
{"id": "bob-arch", "github": "bob-shared"},
{"id": "bob-pm", "github": "bob-shared"},
])
out = dc.aggregate_signals(
[{"type": "manual:commit_message", "login": "bob-pm",
"weight": 3.0, "ref": "commit/x/contrib/bob-pm"}],
dc.load_people_map(root),
)
assert out is not None and out[0]["person"] == "bob-pm"


# ─── parse_contribute_directives ────────────────────────────────────────────


Expand Down
Loading