diff --git a/docs/MONETISATION.md b/docs/MONETISATION.md index 3f65a65..dc1989f 100644 --- a/docs/MONETISATION.md +++ b/docs/MONETISATION.md @@ -13,7 +13,7 @@ profitable business. Those remain separate questions to measure. Demand and utility activity reports use durable event history when SQLite is active. `/demand/feed` reports `demand-history-v2`, `/swarm/stats` and `/swarm/graph` report `swarm-activity-v2`, and `/funnel` reports -`conversion-activity-v2`. Each discloses coverage; a compacted JSON history or +`conversion-activity-v3`. Each discloses coverage; a compacted JSON history or known missing prefix is incomplete. Restored historical counts are not new demand and must not be compared as growth against older reports that used only the recent serving cache. Capability asks are not funded jobs, and a successful @@ -21,6 +21,15 @@ utility return is not evidence that the caller used or paid for its output. Serving/acquisition cost is unknown until an allocated cost ledger exists. The funnel's task-outcome stage splits the actual receipt states, including delivery, acceptance, rejection and neutral stops; it is not a success count. +Version 3 also applies the central exact-ID ownership corrections to historical +task parties and current account/agent ownership to event stages. Two July 13 +buyers are proved first-party by the committed `artifacts/market_evidence/` +`external_txn_helloworld.json` and `external_txn_paki.json` run records. They used +sandbox credits against external providers. Those observations remain intact; +they establish no outside customer payment. Similar buyer names are insufficient +for ownership attribution. Counts before and after this correction are not a +growth comparison, and the remaining external task bucket means registered and +not known-owned, not independently proved customer demand. The bounded discovery scout prioritises eligible pending requests before the historical ranking, preserving its per-run limit and all attribution gates. diff --git a/live/guild/app/attribution.py b/live/guild/app/attribution.py index 90e991c..fc2fdba 100644 --- a/live/guild/app/attribution.py +++ b/live/guild/app/attribution.py @@ -74,7 +74,11 @@ # was enforced. These exact production identities are public and auditable: # four TanukiTextStats registrations declare # ``operator=agent-guild (first-party demo supply)`` and the fifth is the -# operator-owned Codex Autonomous Worker. Their old account/event rows were +# operator-owned Codex Autonomous Worker. Two July 13 buyer identities are +# pinned by the committed first-party run artifacts in artifacts/market_evidence/ +# external_txn_{helloworld,paki}.json (steps/register_buyer). The providers in +# those runs are external; that does not make our buyer an external customer. +# Their old account/event rows were # written with ``first_party=false`` and therefore used to qualify as external # verified members at read time. Keep this a narrow exact-ID set: it may only # demote known-owned traffic and must never infer ownership from a name/domain. @@ -84,8 +88,23 @@ "agent_87bcabedf2c0", "agent_c7d2e902dc50", "agent_f75dd36ac192", + "agent_1e6cf5203b48", + "agent_42e6eb9716d5", }) + +def is_guild_operated_agent(agent_id: str | None, + agent: Mapping[str, Any] | None = None) -> bool: + """Ownership evidence for analytics, including exact legacy identities. + + This only demotes known-owned activity. Names, endpoints, counterparty + ownership and resemblance to our tests never establish ownership. It does + not mutate accounts, tasks, reputation evidence or payment attribution. + """ + return (agent_id in KNOWN_GUILD_OPERATED_AGENT_IDS + or bool((agent or {}).get("first_party"))) + + # Known first-party incidents: OUR OWN traffic that slipped past first-party # tagging (e.g. a maintainer test that forgot the X-Guild-Source header) and # would otherwise read as genuine external. Each entry is deliberately narrow — diff --git a/live/guild/app/store.py b/live/guild/app/store.py index 107aa5a..0cd9ce4 100644 --- a/live/guild/app/store.py +++ b/live/guild/app/store.py @@ -2708,11 +2708,8 @@ def record_event(self, key: Optional[str], etype: str, ua: str = "", **meta) -> owner_agent_id = ((acct or {}).get("owner_agent_id") or (acct or {}).get("agent_id")) owner_agent = self.agents.get(owner_agent_id or "") or {} - known_owned_agent = owner_agent_id in ( - _attr.KNOWN_GUILD_OPERATED_AGENT_IDS) fp = (bool(acct and acct.get("first_party")) - or bool(owner_agent.get("first_party")) - or known_owned_agent + or _attr.is_guild_operated_agent(owner_agent_id, owner_agent) or _attr.is_guild_internal_origin(meta)) event = {"key": key or "anon", "type": etype, "ua": ua or "", "fp": fp, "surface": self._surface_of(key, ua or ""), @@ -3562,8 +3559,8 @@ def _caller_class_for(self, ev: dict[str, Any]) -> str: # historical rows: marking an account/agent first-party (or pinning a # proven legacy Guild identity) must reclassify its immutable old # events without rewriting or deleting the event log. - if (acct.get("first_party") or agent.get("first_party") - or agent_id in _attr.KNOWN_GUILD_OPERATED_AGENT_IDS): + if (acct.get("first_party") + or _attr.is_guild_operated_agent(agent_id, agent)): ev = {**ev, "fp": True} member = bool(acct) verified = bool((agent.get("milestones") or {}).get("key_proof") @@ -4678,7 +4675,7 @@ def conversion_funnel(self) -> dict[str, Any]: def _class_of(e: dict[str, Any]) -> str: if e.get("demand_first_party"): return "first_party" - cls = attribution.caller_class(e) + cls = self._caller_class_for(e) if cls in ("AG_INTERNAL", "AG_TEST", "OPERATOR"): return "first_party" if (attribution.may_count_as_external_growth(cls) @@ -4727,9 +4724,12 @@ def _class_of(e: dict[str, Any]) -> str: state = t.get("outcome") if state not in by_outcome: continue - req = self.agents.get(t.get("requester_agent_id") or "") or {} - wrk = self.agents.get(t.get("worker_agent_id") or "") or {} - if req.get("first_party") or wrk.get("first_party"): + req_id = t.get("requester_agent_id") + wrk_id = t.get("worker_agent_id") + req = self.agents.get(req_id or "") or {} + wrk = self.agents.get(wrk_id or "") or {} + if (attribution.is_guild_operated_agent(req_id, req) + or attribution.is_guild_operated_agent(wrk_id, wrk)): classification = "first_party" elif req and wrk: classification = "external" @@ -4785,14 +4785,18 @@ def _flow(stage: str, what: str) -> dict[str, Any]: "shown separately, never merged")} return { - "measurement_version": "conversion-activity-v2", + "measurement_version": "conversion-activity-v3", "measurement_coverage": coverage, "interpretation": ( "Event stages use retained durable history; task outcomes, " "current endpoints and settlements use their separate stores. " "These are activity counts, not a linked buyer conversion " "rate. Restoring older events is not new adoption; prior " - "retained-tail snapshots are not comparable."), + "retained-tail snapshots are not comparable. Known-owned " + "legacy identities and current first-party account/agent " + "flags reclassify historical activity at read time without " + "rewriting records; v2 attribution counts are not directly " + "comparable."), "stages": [ _flow("demand_observed", "explicit capability_demand events"), @@ -4834,7 +4838,8 @@ def _flow(stage: str, what: str) -> dict[str, Any]: "by actual receipt state (including legacy success/" "failure). Delivery and neutral stops are not " "successful completion. external = both parties " - "registered and neither marked Guild-operated; " + "registered and neither flagged nor exact-pinned " + "as Guild-operated; " "this does not prove independent ownership or " "useful work. first-party = any Guild-operated " "party; unknown = a party record is missing"}, diff --git a/live/guild/tests/test_legacy_buyer_attribution.py b/live/guild/tests/test_legacy_buyer_attribution.py new file mode 100644 index 0000000..6d66cce --- /dev/null +++ b/live/guild/tests/test_legacy_buyer_attribution.py @@ -0,0 +1,127 @@ +"""Retained first-party buyer tests are evidence, never external demand.""" +from copy import deepcopy +import json +from pathlib import Path + +import pytest + +from app import attribution +from app.store import Store + + +@pytest.fixture(params=["json", "sqlite"]) +def store(request, monkeypatch, tmp_path): + monkeypatch.setenv("GUILD_STORE", request.param) + return Store(path=str(tmp_path / "guild.json")) + + +def stage(store, name): + return next(s for s in store.conversion_funnel()["stages"] + if s["stage"] == name) + + +def test_exact_buyer_ids_are_supported_by_committed_owned_run_evidence(): + repo = Path(__file__).resolve().parents[3] + expected = { + "external_txn_helloworld.json": "agent_1e6cf5203b48", + "external_txn_paki.json": "agent_42e6eb9716d5", + } + for filename, agent_id in expected.items(): + run = json.loads((repo / "artifacts/market_evidence" / filename).read_text()) + registered = next(s for s in run["steps"] if s["name"] == "register_buyer") + assert registered["agent_id"] == agent_id + assert run["settlement"]["currency"] == "credits_sandbox" + assert attribution.is_guild_operated_agent(agent_id, {}) + # The independently operated provider is not reclassified as ours. + assert not attribution.is_guild_operated_agent(run["provider"]["id"], {}) + + +def test_known_owned_either_party_reclassifies_without_rewriting_evidence(store): + stranger = "agent_unrelated" + store.agents[stranger] = {"id": stranger, "first_party": False} + for agent_id in attribution.KNOWN_GUILD_OPERATED_AGENT_IDS: + store.agents[agent_id] = {"id": agent_id, "first_party": False} + for role in ("requester", "worker"): + for state in ("accepted", "delivered"): + task_id = f"{agent_id}-{role}-{state}" + store.tasks[task_id] = { + "id": task_id, + "requester_agent_id": agent_id if role == "requester" else stranger, + "worker_agent_id": agent_id if role == "worker" else stranger, + "outcome": state, "payment": 3.0, + "deliverable_hash": "0x" + "12" * 32, + } + before = deepcopy((store.agents, store.tasks, store.billing_log)) + result = stage(store, "outcome") + count = len(attribution.KNOWN_GUILD_OPERATED_AGENT_IDS) * 2 + assert result["by_outcome"]["accepted"] == { + "external": 0, "first_party": count, "unknown": 0} + assert result["by_outcome"]["delivered"] == { + "external": 0, "first_party": count, "unknown": 0} + assert result["count"] == 0 + assert (store.agents, store.tasks, store.billing_log) == before + assert store.agents[stranger]["first_party"] is False + + +@pytest.mark.parametrize("owner_field", ["owner_agent_id", "agent_id"]) +def test_legacy_account_flow_uses_current_ownership_without_rewriting_events( + store, owner_field): + owned_id = "agent_1e6cf5203b48" + store.agents[owned_id] = {"id": owned_id, "first_party": False} + store.accounts["owned-test-key"] = {owner_field: owned_id, "first_party": False} + historical = { + "key": "owned-test-key", "type": "delegation", "fp": False, + "ua": "python-httpx/0.27", "at": "2026-07-13T14:23:19+00:00", + } + store.events.append(historical) + if store.backend is not None: + store.backend.append_event(historical) + assert stage(store, "delegation")["breakdown"] == { + "external": 0, "first_party": 1, "unknown": 0} + assert historical["fp"] is False + store.record_event("owned-test-key", "delegation", ua="python-httpx/0.27") + assert store.events[-1]["fp"] is True + + +def test_unproven_similar_buyers_and_external_providers_keep_their_attribution(store): + # The audit found similar greeting runs, but no exact owned-run artifacts + # for these two buyers. Similarity must not become an ownership rule. + provider = "agent_707735e598c5" + store.agents[provider] = {"id": provider, "first_party": False} + for agent_id in ("agent_4e44d05783a2", "agent_1864d45db456", + "agent_1e6cf5203b49"): + store.agents[agent_id] = { + "id": agent_id, "name": "ExternalBuyer-Py-test", "first_party": False} + store.tasks[agent_id] = {"requester_agent_id": agent_id, + "worker_agent_id": provider, "outcome": "accepted"} + result = stage(store, "outcome") + assert result["by_outcome"]["accepted"] == { + "external": 3, "first_party": 0, "unknown": 0} + assert "does not prove independent ownership" in result["source"] + + +@pytest.mark.parametrize("flag_on", ["account", "agent"]) +def test_current_ownership_flags_reclassify_old_flow_events(store, flag_on): + agent_id = "agent_flagged_later" + store.agents[agent_id] = {"id": agent_id, "first_party": flag_on == "agent"} + store.accounts["late-flag-key"] = { + "owner_agent_id": agent_id, "first_party": flag_on == "account"} + event = {"type": "query", "paid": True, "key": "late-flag-key", + "fp": False, "ua": "python-httpx/0.27", + "at": "2026-07-13T14:23:19+00:00"} + store.events.append(event) + if store.backend is not None: + store.backend.append_event(event) + assert stage(store, "paid_decision")["breakdown"] == { + "external": 0, "first_party": 1, "unknown": 0} + assert event["fp"] is False + + +def test_flagged_and_missing_records_remain_separate(store): + store.agents["flagged"] = {"id": "flagged", "first_party": True} + store.tasks["flagged"] = {"requester_agent_id": "flagged", + "worker_agent_id": "missing", "outcome": "accepted"} + store.tasks["unknown"] = {"requester_agent_id": "missing", + "worker_agent_id": "missing2", "outcome": "accepted"} + assert stage(store, "outcome")["by_outcome"]["accepted"] == { + "external": 0, "first_party": 1, "unknown": 1}