logicdiff 0.1.0__py3-none-any.whl

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.
logicdiff/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """logicdiff — a whitespace- and reflow-blind diff. Zero dependencies."""
2
+
3
+ __version__ = "0.1.0"
logicdiff/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
logicdiff/cli.py ADDED
@@ -0,0 +1,167 @@
1
+ import json
2
+ import os
3
+ import re
4
+ import signal
5
+ import sys
6
+
7
+ from . import __version__
8
+ from . import core
9
+
10
+ DEFAULT_MAX_TOKENS = 2000000
11
+
12
+
13
+ def _color_on(mode):
14
+ if mode == "always":
15
+ return True
16
+ if mode == "never":
17
+ return False
18
+ return sys.stdout.isatty() and not os.environ.get("NO_COLOR")
19
+
20
+
21
+ def _mk_paint(on):
22
+ def col(c, s):
23
+ return f"\x1b[{c}m{s}\x1b[0m" if on else s
24
+ return {
25
+ "red": lambda s: col("31", s), "green": lambda s: col("32", s),
26
+ "yellow": lambda s: col("33", s), "dim": lambda s: col("2", s),
27
+ "bold": lambda s: col("1", s), "cyan": lambda s: col("36", s),
28
+ }
29
+
30
+
31
+ def _help():
32
+ b = _mk_paint(_color_on("auto"))["bold"]
33
+ return (
34
+ f"{b('logicdiff')} — a whitespace- and reflow-blind diff. Zero dependencies.\n"
35
+ "\n"
36
+ "Diffs two files but folds away pure formatting — respacing AND line reflow — and\n"
37
+ "shows only the logical changes. Where `git diff -w` still flags a re-wrapped block,\n"
38
+ "logicdiff says \"only formatting differs\". Language-agnostic, no parser.\n"
39
+ "\n"
40
+ f"{b('Usage')}\n"
41
+ " logicdiff <old> <new> Compare two files (use - for stdin)\n"
42
+ "\n"
43
+ f"{b('Options')}\n"
44
+ " --stat Print only the counts (machine-friendly key=value)\n"
45
+ " --json Machine-readable output (byte-identical across Node and Python)\n"
46
+ " -q, --quiet No output; exit code only\n"
47
+ " --color=auto|always|never Colorize (default auto)\n"
48
+ f" --max-tokens N Bail (exit 2) over N tokens total (default {DEFAULT_MAX_TOKENS})\n"
49
+ " --help | --version\n"
50
+ "\n"
51
+ f"{b('Exit')} 0 identical or formatting-only · 1 logical changes · 2 error\n"
52
+ )
53
+
54
+
55
+ class _Exit(Exception):
56
+ def __init__(self, code):
57
+ self.code = code
58
+
59
+
60
+ def _out(s):
61
+ sys.stdout.buffer.write(s.encode("latin-1"))
62
+
63
+
64
+ def main(argv=None):
65
+ try:
66
+ signal.signal(signal.SIGPIPE, signal.SIG_DFL)
67
+ except (AttributeError, ValueError):
68
+ pass
69
+
70
+ argv = list(sys.argv[1:] if argv is None else argv)
71
+
72
+ def die(msg):
73
+ sys.stderr.write(_mk_paint(_color_on("auto"))["red"](f"logicdiff: {msg}\n"))
74
+ raise _Exit(2)
75
+
76
+ try:
77
+ dd_idx = argv.index("--") if "--" in argv else len(argv)
78
+ pre = argv[:dd_idx]
79
+ if "-h" in pre or "--help" in pre:
80
+ sys.stdout.write(_help())
81
+ return 0
82
+ if "-v" in pre or "--version" in pre:
83
+ sys.stdout.write(__version__ + "\n")
84
+ return 0
85
+
86
+ as_stat = as_json = quiet = False
87
+ color = "auto"
88
+ max_tokens = DEFAULT_MAX_TOKENS
89
+ files = []
90
+ dd = False
91
+ i = 0
92
+ while i < len(argv):
93
+ a = argv[i]
94
+ if dd:
95
+ files.append(a); i += 1; continue
96
+ if a == "--":
97
+ dd = True; i += 1; continue
98
+ eq = a.find("=") if a.startswith("--") else -1
99
+ flag = a[:eq] if eq != -1 else a
100
+ inline = a[eq + 1:] if eq != -1 else None
101
+ if flag == "--stat":
102
+ as_stat = True
103
+ elif flag == "--json":
104
+ as_json = True
105
+ elif a == "-q" or flag == "--quiet":
106
+ quiet = True
107
+ elif flag == "--color":
108
+ color = inline if inline is not None else (argv[i + 1] if i + 1 < len(argv) else "")
109
+ if inline is None:
110
+ i += 1
111
+ if color not in ("auto", "always", "never"):
112
+ die("--color must be auto, always or never")
113
+ elif flag == "--max-tokens":
114
+ val = inline if inline is not None else (argv[i + 1] if i + 1 < len(argv) else "")
115
+ if inline is None:
116
+ i += 1
117
+ # Strict integer grammar (optional leading +, decimal digits only) so this
118
+ # parses byte-identically to the Node build. fullmatch (not $) avoids Python's
119
+ # trailing-newline match; int() alone would accept PEP-515 '1_000'.
120
+ if not re.fullmatch(r"\+?[0-9]+", val):
121
+ die("--max-tokens must be a positive integer")
122
+ max_tokens = int(val)
123
+ if max_tokens <= 0:
124
+ die("--max-tokens must be a positive integer")
125
+ elif a in ("-h", "--help", "-v", "--version"):
126
+ pass
127
+ elif a.startswith("-") and a != "-":
128
+ die(f"unknown option: {a} (use -- to end options)")
129
+ else:
130
+ files.append(a)
131
+ i += 1
132
+
133
+ if len(files) != 2:
134
+ die(f"expected exactly 2 files, got {len(files)} (usage: logicdiff <old> <new>)")
135
+
136
+ def read_input(p):
137
+ try:
138
+ buf = sys.stdin.buffer.read() if p == "-" else open(p, "rb").read()
139
+ except OSError:
140
+ die(f"cannot read '{p}'") # OS-specific detail omitted for Node/Python parity
141
+ if b"\x00" in buf:
142
+ return None
143
+ return buf.decode("latin-1")
144
+
145
+ a_text = read_input(files[0])
146
+ b_text = read_input(files[1])
147
+ if a_text is None or b_text is None:
148
+ if not quiet:
149
+ sys.stderr.write("logicdiff: binary file, not diffed\n")
150
+ return 2
151
+
152
+ try:
153
+ result = core.compare(a_text, b_text, {"maxTokens": max_tokens})
154
+ except ValueError as e:
155
+ die(str(e))
156
+
157
+ paint = _mk_paint(_color_on(color) and not as_json and not as_stat)
158
+ if as_json:
159
+ _out(json.dumps(core.to_json(result, files[0], files[1]), indent=2, ensure_ascii=False) + "\n")
160
+ elif as_stat:
161
+ _out(core.stat_line(result) + "\n")
162
+ elif not quiet:
163
+ _out(core.format_human(result, files[0], files[1], paint) + "\n")
164
+
165
+ return 0 if (result["identical"] or result["formattingOnly"]) else 1
166
+ except _Exit as ex:
167
+ return ex.code
logicdiff/core.py ADDED
@@ -0,0 +1,225 @@
1
+ """logicdiff core — pure whitespace-and-reflow-blind diff. No fs, no clock.
2
+
3
+ The trick: diff the two files as sequences of TOKENS (words + single punctuation
4
+ chars), with whitespace dropped, so respacing AND line-reflow are invisible. If the
5
+ token sequences are equal, the files differ only in formatting. Token comparison is
6
+ on TEXT ONLY (line numbers are metadata) — that's what makes reflow fold.
7
+
8
+ Byte-for-byte identical to the Node build: everything runs on a latin-1
9
+ (byte-faithful) string, the tokenizer uses explicit ASCII classes (never \\w/\\s), and
10
+ the diff is the canonical Myers O(ND) algorithm with a pinned tie-break.
11
+ """
12
+
13
+ WS = "\x20\x09\x0a\x0d\x0c\x0b" # space, tab, LF, CR, FF, VT
14
+
15
+
16
+ def is_word(code):
17
+ return (65 <= code <= 90) or (97 <= code <= 122) or (48 <= code <= 57) or code == 95
18
+
19
+
20
+ def tokenize(s):
21
+ """[{text,line}]; word=[A-Za-z0-9_] run, else single non-ws char; ws dropped;
22
+ only '\\n' increments the 1-based line (lone '\\r' does not -> CRLF == LF)."""
23
+ toks = []
24
+ line = 1
25
+ i = 0
26
+ n = len(s)
27
+ while i < n:
28
+ ch = s[i]
29
+ if ch == "\n":
30
+ line += 1
31
+ i += 1
32
+ continue
33
+ if ch in WS:
34
+ i += 1
35
+ continue
36
+ if is_word(ord(ch)):
37
+ j = i + 1
38
+ while j < n and is_word(ord(s[j])):
39
+ j += 1
40
+ toks.append({"text": s[i:j], "line": line})
41
+ i = j
42
+ else:
43
+ toks.append({"text": ch, "line": line})
44
+ i += 1
45
+ return toks
46
+
47
+
48
+ def split_lines(s):
49
+ return s.split("\n")
50
+
51
+
52
+ # ---- canonical Myers O(ND) diff over string arrays -------------------------
53
+ # Coglan reference variant. Tie-break: prefer "down" (insertion) only when
54
+ # k == -d OR (k != d AND V[k-1] < V[k+1]) with strict <. Snapshot V before each
55
+ # d-round. Implemented identically in the Node build.
56
+
57
+ # Bail before the O(D*(n+m)) trace exhausts memory (D = edit distance). ~1.2e8 ints keeps
58
+ # the Node build well under its default heap cap; this build bails at the same point.
59
+ MAX_TRACE_ELEMENTS = 120000000
60
+
61
+
62
+ def shortest_edit(a, b, max_trace=None):
63
+ n, m = len(a), len(b)
64
+ mx = n + m
65
+ cap = max_trace or MAX_TRACE_ELEMENTS
66
+ trace = []
67
+ if mx == 0:
68
+ return trace, n, m
69
+ v = [0] * (2 * mx + 1)
70
+ for d in range(0, mx + 1):
71
+ # The trace holds one full V-snapshot (length 2*mx+1) per d-round, so worst-case
72
+ # memory is O(D*(n+m)); bail deterministically (identically in both builds) before it
73
+ # exhausts the heap. Triggered only by very dissimilar inputs (large edit distance).
74
+ if (d + 1) * (2 * mx + 1) > cap:
75
+ raise ValueError("input too large to diff (too many differences between the inputs)")
76
+ trace.append(v[:])
77
+ for k in range(-d, d + 1, 2):
78
+ if k == -d or (k != d and v[k - 1 + mx] < v[k + 1 + mx]):
79
+ x = v[k + 1 + mx]
80
+ else:
81
+ x = v[k - 1 + mx] + 1
82
+ y = x - k
83
+ while x < n and y < m and a[x] == b[y]:
84
+ x += 1
85
+ y += 1
86
+ v[k + mx] = x
87
+ if x >= n and y >= m:
88
+ return trace, n, m
89
+ return trace, n, m
90
+
91
+
92
+ def diff_seq(a, b, max_trace=None):
93
+ n, m = len(a), len(b)
94
+ mx = n + m
95
+ trace, _, _ = shortest_edit(a, b, max_trace)
96
+ x, y = n, m
97
+ moves = []
98
+ for d in range(len(trace) - 1, -1, -1):
99
+ v = trace[d]
100
+ k = x - y
101
+ if k == -d or (k != d and v[k - 1 + mx] < v[k + 1 + mx]):
102
+ prev_k = k + 1
103
+ else:
104
+ prev_k = k - 1
105
+ prev_x = v[prev_k + mx]
106
+ prev_y = prev_x - prev_k
107
+ while x > prev_x and y > prev_y:
108
+ moves.append({"op": "keep", "a": x - 1, "b": y - 1})
109
+ x -= 1
110
+ y -= 1
111
+ if d > 0:
112
+ if x == prev_x:
113
+ moves.append({"op": "ins", "b": y - 1})
114
+ else:
115
+ moves.append({"op": "del", "a": x - 1})
116
+ x, y = prev_x, prev_y
117
+ moves.reverse()
118
+ return moves
119
+
120
+
121
+ # ---- top-level comparison --------------------------------------------------
122
+
123
+ def compare(a_str, b_str, opts=None):
124
+ opts = opts or {}
125
+ max_trace = opts.get("maxTrace")
126
+ a_toks, b_toks = tokenize(a_str), tokenize(b_str)
127
+ max_tokens = opts.get("maxTokens")
128
+ if max_tokens and len(a_toks) + len(b_toks) > max_tokens:
129
+ raise ValueError(f"input too large ({len(a_toks) + len(b_toks)} tokens > --max-tokens {max_tokens})")
130
+ moves = diff_seq([t["text"] for t in a_toks], [t["text"] for t in b_toks], max_trace)
131
+
132
+ tokens_added = tokens_removed = 0
133
+ del_lines, ins_lines = set(), set()
134
+ for mv in moves:
135
+ if mv["op"] == "del":
136
+ tokens_removed += 1
137
+ del_lines.add(a_toks[mv["a"]]["line"])
138
+ elif mv["op"] == "ins":
139
+ tokens_added += 1
140
+ ins_lines.add(b_toks[mv["b"]]["line"])
141
+ del_line_arr = sorted(del_lines)
142
+ ins_line_arr = sorted(ins_lines)
143
+
144
+ a_lines, b_lines = split_lines(a_str), split_lines(b_str)
145
+ git_would_show = sum(1 for mv in diff_seq(a_lines, b_lines, max_trace) if mv["op"] != "keep")
146
+ logical_lines_changed = len(del_line_arr) + len(ins_line_arr)
147
+
148
+ return {
149
+ "identical": a_str == b_str,
150
+ "formattingOnly": tokens_added == 0 and tokens_removed == 0,
151
+ "stats": {
152
+ "tokensAdded": tokens_added, "tokensRemoved": tokens_removed,
153
+ "logicalLinesChanged": logical_lines_changed,
154
+ "linesReflowed": max(0, git_would_show - logical_lines_changed),
155
+ "gitWouldShow": git_would_show,
156
+ },
157
+ "delLines": del_line_arr, "insLines": ins_line_arr,
158
+ "aLines": a_lines, "bLines": b_lines,
159
+ }
160
+
161
+
162
+ # ---- rendering -------------------------------------------------------------
163
+
164
+ PLAIN = {"red": lambda s: s, "green": lambda s: s, "yellow": lambda s: s,
165
+ "dim": lambda s: s, "bold": lambda s: s, "cyan": lambda s: s}
166
+
167
+
168
+ def strip_cr(s):
169
+ return s[:-1] if s and s[-1] == "\r" else s
170
+
171
+
172
+ def format_human(r, file_a, file_b, paint=None):
173
+ p = paint or PLAIN
174
+ # Output is written as latin-1 bytes (to round-trip file content faithfully),
175
+ # so all decoration here is ASCII-only.
176
+ if r["identical"]:
177
+ return p["dim"]("identical")
178
+ if r["formattingOnly"]:
179
+ n = r["stats"]["gitWouldShow"]
180
+ return p["green"]("only formatting differs") + p["dim"](
181
+ f" - no logical change (a line diff would show {n} changed line{'' if n == 1 else 's'})")
182
+ lines = [p["bold"]("--- " + file_a), p["bold"]("+++ " + file_b)]
183
+ for L in r["delLines"]:
184
+ lines.append(p["red"]("-" + str(L) + ": " + strip_cr(r["aLines"][L - 1])))
185
+ for L in r["insLines"]:
186
+ lines.append(p["green"]("+" + str(L) + ": " + strip_cr(r["bLines"][L - 1])))
187
+ lines.append("")
188
+ s = r["stats"]
189
+ summary = f"{s['tokensRemoved']} token{'' if s['tokensRemoved'] == 1 else 's'} removed, {s['tokensAdded']} added"
190
+ summary += f" across {s['logicalLinesChanged']} logical line{'' if s['logicalLinesChanged'] == 1 else 's'}"
191
+ if s["linesReflowed"] > 0:
192
+ summary += f" ({s['linesReflowed']} line{'' if s['linesReflowed'] == 1 else 's'} folded as reflow/whitespace)"
193
+ lines.append(p["red"](summary))
194
+ return "\n".join(lines)
195
+
196
+
197
+ def stat_line(r):
198
+ s = r["stats"]
199
+ return "\n".join([
200
+ "formatting_only=" + ("1" if r["formattingOnly"] else "0"),
201
+ "tokens_added=" + str(s["tokensAdded"]),
202
+ "tokens_removed=" + str(s["tokensRemoved"]),
203
+ "logical_lines_changed=" + str(s["logicalLinesChanged"]),
204
+ "lines_reflowed=" + str(s["linesReflowed"]),
205
+ "git_would_show_lines=" + str(s["gitWouldShow"]),
206
+ ])
207
+
208
+
209
+ def to_json(r, file_a, file_b):
210
+ s = r["stats"]
211
+ return {
212
+ "fileA": file_a, "fileB": file_b,
213
+ "identical": r["identical"],
214
+ "formatting_only": r["formattingOnly"],
215
+ "exit_code": 0 if (r["identical"] or r["formattingOnly"]) else 1,
216
+ "stats": {
217
+ "tokens_added": s["tokensAdded"],
218
+ "tokens_removed": s["tokensRemoved"],
219
+ "logical_lines_changed": s["logicalLinesChanged"],
220
+ "lines_reflowed": s["linesReflowed"],
221
+ "git_would_show_lines": s["gitWouldShow"],
222
+ },
223
+ "removed": [{"line": L, "text": strip_cr(r["aLines"][L - 1])} for L in r["delLines"]],
224
+ "added": [{"line": L, "text": strip_cr(r["bLines"][L - 1])} for L in r["insLines"]],
225
+ }
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: logicdiff
3
+ Version: 0.1.0
4
+ Summary: A whitespace- and reflow-blind diff: folds respacing AND line re-wrapping that git diff -w can't, and tells you if a change is logical or just formatting. Zero dependencies.
5
+ Author: yyfjj
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/jjdoor/logicdiff-py
8
+ Project-URL: Repository, https://github.com/jjdoor/logicdiff-py
9
+ Project-URL: Issues, https://github.com/jjdoor/logicdiff-py/issues
10
+ Keywords: diff,whitespace,reflow,format,git,code-review,cli,ci,devtools
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Software Development :: Version Control
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # logicdiff
25
+
26
+ **A whitespace- and reflow-blind diff.** A pull request reindents a file and
27
+ rewraps a few long lines, and now `git diff` shows 80 changed lines — but did
28
+ anything *actually* change? `logicdiff` answers that: it folds away pure
29
+ formatting (respacing **and** line reflow) and shows only the logical changes.
30
+
31
+ ```bash
32
+ logicdiff old.js new.js
33
+ # only formatting differs - no logical change (a line diff would show 80 changed lines)
34
+
35
+ logicdiff a.js b.js
36
+ # --- a.js
37
+ # +++ b.js
38
+ # -42: const total = price * qty;
39
+ # +51: const total = price + qty;
40
+ #
41
+ # 1 token removed, 1 added across 2 logical lines (78 lines folded as reflow/whitespace)
42
+ ```
43
+
44
+ Exit `0` when the change is formatting-only (or identical), `1` when there's a
45
+ real logical change — so CI can ask "is this PR just a reformat?" Zero
46
+ dependencies, language-agnostic, also on npm (`npx logicdiff`) — the two
47
+ builds produce **byte-for-byte identical** output.
48
+
49
+ ## Why not `git diff -w`?
50
+
51
+ `git diff -w` (ignore-all-space) folds *respacing* — but it is still
52
+ line-anchored, so it **cannot fold reflow**. Re-wrap a function signature across
53
+ three lines and `git diff -w` still shows 1 removed + 3 added, even though not a
54
+ single token changed. That exact gap is [GitHub discussion #20610]
55
+ ("Ignore Format Changes in Diff"), open and unanswered for years.
56
+
57
+ `difftastic` solves it beautifully with per-language tree-sitter parsing — but
58
+ it's a multi-megabyte binary, needs a grammar for each language (config/log/DSL
59
+ files fall back to text), and it's a *display* tool with no "is this
60
+ formatting-only?" exit code.
61
+
62
+ `logicdiff` is the lightweight middle ground: **zero-config, zero-dependency,
63
+ language-agnostic** (works on any text — code, YAML, logs, DSLs), folds *both*
64
+ whitespace and reflow, and gives a one-shot CLI answer plus a CI exit code.
65
+
66
+ ## How it works
67
+
68
+ It tokenizes each file into a sequence of tokens — a token is a run of
69
+ `[A-Za-z0-9_]` or a single punctuation character, and **whitespace is dropped**.
70
+ So `a+b`, `a + b`, and `a +\n b` all become the same token stream `[a, + , b]`:
71
+ respacing and line breaks become invisible. It then runs the canonical
72
+ [Myers diff] on the token streams. If the streams are equal, the change is
73
+ formatting-only. If not, the changed tokens are mapped back to their line
74
+ numbers and shown.
75
+
76
+ Because it has no language parser, whitespace **inside string literals** is also
77
+ ignored — `x = "a b"` and `x = "a b"` are "formatting only", exactly like
78
+ `git diff -w`. That's a deliberate, documented limitation, not a bug.
79
+
80
+ ## Usage
81
+
82
+ ```bash
83
+ logicdiff old new # human diff (or "only formatting differs")
84
+ logicdiff old new --stat # just the counts, machine-friendly key=value
85
+ logicdiff old new --json # structured output (byte-identical both builds)
86
+ logicdiff old new -q # no output, exit code only (the CI gate)
87
+ cat new | logicdiff old - # - reads stdin
88
+ ```
89
+
90
+ `--color=auto|always|never`, `--max-tokens N` (bail over N tokens, default 2,000,000).
91
+ Two wildly dissimilar inputs (a huge edit distance) also bail with exit `2` instead of
92
+ risking the heap — logicdiff is for spotting a real change inside a reformat, not for
93
+ diffing unrelated files.
94
+
95
+ Exit codes: `0` identical or formatting-only · `1` logical changes · `2` error.
96
+
97
+ ```yaml
98
+ # CI: warn when a PR is more than a reformat
99
+ - run: logicdiff "$BASE" "$HEAD" -q || echo "::warning::real code change, review carefully"
100
+ ```
101
+
102
+ ## Install
103
+
104
+ ```bash
105
+ pip install logicdiff # or pipx run logicdiff
106
+ npm i -g logicdiff # Node build, identical behaviour
107
+ ```
108
+
109
+ Python ≥ 3.8 or Node ≥ 18. No dependencies.
110
+
111
+ [GitHub discussion #20610]: https://github.com/orgs/community/discussions/20610
112
+ [Myers diff]: https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/
113
+
114
+ ## License
115
+
116
+ MIT
@@ -0,0 +1,10 @@
1
+ logicdiff/__init__.py,sha256=AfMUlnHl8u8rZjoIx5oPgdiXLPhM2PZX49rmVZ3y-7s,99
2
+ logicdiff/__main__.py,sha256=4JMK66Wj4uLZTKbF-sT3LAxOsr6buig77PmOkJCRRxw,83
3
+ logicdiff/cli.py,sha256=-_ReZ2oKRQGUkHJktc4AsY35pAEPFvsowmqwfGgru7o,5994
4
+ logicdiff/core.py,sha256=H2weFqKTw3kil8avR_TmQaq4dUaWy7hVZNHwkiW12LY,8437
5
+ logicdiff-0.1.0.dist-info/licenses/LICENSE,sha256=N6691UqSpfX5o4zAQqDPGXJbfI_AdM29qLeC5x0sCCs,1079
6
+ logicdiff-0.1.0.dist-info/METADATA,sha256=DykId1XrF-eluVQ_8NQ8Eilr5YLATEN5h_Qe3ceOVVo,4795
7
+ logicdiff-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ logicdiff-0.1.0.dist-info/entry_points.txt,sha256=ZLn9FyvhMwSOIDxvL2H_yLja2h5VyGWjUjkBhAwaGuo,49
9
+ logicdiff-0.1.0.dist-info/top_level.txt,sha256=rJ5O3iVEE-hB24K604G2VaZWdBPCe8kQYpD15C60bb0,10
10
+ logicdiff-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ logicdiff = logicdiff.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 logicdiff contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ logicdiff