gabriel / muse public
set_ops.py python
95 lines 2.9 KB
Raw
sha256:e6465e8a9b7fa8e6223ed4a3576e96c568c913ae2caeb9c31f15e7a81b250b40 docs: add | jq convention to --json section of agent-guide Sonnet 4.6 1 day ago
1 """Hash-set algebra diff for unordered collections.
2
3 Computes the symmetric difference between two ``frozenset[str]`` collections
4 of content IDs. The result is a ``StructuredDelta`` containing:
5
6 - ``InsertOp`` entries for content IDs present in *target* but not *base*.
7 - ``DeleteOp`` entries for content IDs present in *base* but not *target*.
8
9 No ``MoveOp`` or ``ReplaceOp`` is ever produced: unordered sets have no
10 positional semantics, so every element is either added or removed.
11
12 This is the algorithm the MIDI plugin uses for file-level diffing (which set
13 of POSIX paths changed). Plugins with a ``SetSchema`` in their ``DomainSchema``
14 get this algorithm for free via :func:`~muse.core.diff_algorithms.diff_by_schema`.
15
16 Public API
17 ----------
18 - :func:`diff` — ``frozenset[str]`` × ``frozenset[str]`` → ``StructuredDelta``.
19 """
20
21 import logging
22
23 from muse.core.schema import SetSchema
24 from muse.domain import DeleteOp, DomainOp, InsertOp, StructuredDelta
25
26 logger = logging.getLogger(__name__)
27
28 def diff(
29 schema: SetSchema,
30 base: frozenset[str],
31 target: frozenset[str],
32 *,
33 domain: str,
34 address: str = "",
35 ) -> StructuredDelta:
36 """Diff two unordered sets of content IDs under the given ``SetSchema``.
37
38 All insertions and deletions have ``position=None`` because the collection
39 is unordered — position has no meaning for set elements.
40
41 Args:
42 schema: The ``SetSchema`` declaring element type and identity.
43 base: Base (ancestor) set of content IDs.
44 target: Target (newer) set of content IDs.
45 domain: Domain tag for the returned ``StructuredDelta``.
46 address: Address prefix for generated op entries.
47
48 Returns:
49 A ``StructuredDelta`` with ``InsertOp`` and ``DeleteOp`` entries.
50 """
51 added = sorted(target - base)
52 removed = sorted(base - target)
53 elem = schema["element_type"]
54
55 ops: list[DomainOp] = []
56
57 for cid in removed:
58 ops.append(
59 DeleteOp(
60 op="delete",
61 address=address,
62 position=None,
63 content_id=cid,
64 content_summary=f"{elem} removed: {cid}",
65 )
66 )
67
68 for cid in added:
69 ops.append(
70 InsertOp(
71 op="insert",
72 address=address,
73 position=None,
74 content_id=cid,
75 content_summary=f"{elem} added: {cid}",
76 )
77 )
78
79 n_add = len(added)
80 n_del = len(removed)
81 parts: list[str] = []
82 if n_add:
83 parts.append(f"{n_add} {elem}{'s' if n_add != 1 else ''} added")
84 if n_del:
85 parts.append(f"{n_del} {elem}{'s' if n_del != 1 else ''} removed")
86 summary = ", ".join(parts) if parts else f"no {elem} changes"
87
88 logger.debug(
89 "set_ops.diff %r: +%d -%d elements",
90 address,
91 n_add,
92 n_del,
93 )
94
95 return StructuredDelta(domain=domain, ops=ops, summary=summary)
File History 1 commit
sha256:e6465e8a9b7fa8e6223ed4a3576e96c568c913ae2caeb9c31f15e7a81b250b40 docs: add | jq convention to --json section of agent-guide Sonnet 4.6 1 day ago