markback 0.1.1__py3-none-any.whl → 0.1.3__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.
- markback/linter.py +64 -0
- markback/parser.py +6 -2
- markback/types.py +69 -4
- markback/writer.py +10 -3
- {markback-0.1.1.dist-info → markback-0.1.3.dist-info}/METADATA +1 -1
- markback-0.1.3.dist-info/RECORD +14 -0
- markback-0.1.1.dist-info/RECORD +0 -14
- {markback-0.1.1.dist-info → markback-0.1.3.dist-info}/WHEEL +0 -0
- {markback-0.1.1.dist-info → markback-0.1.3.dist-info}/entry_points.txt +0 -0
- {markback-0.1.1.dist-info → markback-0.1.3.dist-info}/licenses/LICENSE +0 -0
markback/linter.py
CHANGED
|
@@ -137,6 +137,67 @@ def lint_prior_exists(
|
|
|
137
137
|
return diagnostics
|
|
138
138
|
|
|
139
139
|
|
|
140
|
+
def _is_position_invalid(source_ref) -> tuple[bool, str]:
|
|
141
|
+
"""Check if a SourceRef has an invalid position range.
|
|
142
|
+
|
|
143
|
+
Returns (is_invalid, error_message).
|
|
144
|
+
Position is invalid if:
|
|
145
|
+
- end_line < start_line
|
|
146
|
+
- end_line == start_line and end_column < start_column
|
|
147
|
+
"""
|
|
148
|
+
if source_ref.start_line is None or source_ref.end_line is None:
|
|
149
|
+
return False, ""
|
|
150
|
+
|
|
151
|
+
if source_ref.end_line < source_ref.start_line:
|
|
152
|
+
return True, f"end line {source_ref.end_line} is less than start line {source_ref.start_line}"
|
|
153
|
+
|
|
154
|
+
if source_ref.end_line == source_ref.start_line:
|
|
155
|
+
if (source_ref.start_column is not None and
|
|
156
|
+
source_ref.end_column is not None and
|
|
157
|
+
source_ref.end_column < source_ref.start_column):
|
|
158
|
+
return True, f"end column {source_ref.end_column} is less than start column {source_ref.start_column} on line {source_ref.start_line}"
|
|
159
|
+
|
|
160
|
+
return False, ""
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def lint_line_range(
|
|
164
|
+
record: Record,
|
|
165
|
+
record_idx: int,
|
|
166
|
+
) -> list[Diagnostic]:
|
|
167
|
+
"""Check if line/character ranges are valid (end position >= start position)."""
|
|
168
|
+
diagnostics: list[Diagnostic] = []
|
|
169
|
+
|
|
170
|
+
# Check @source range
|
|
171
|
+
if record.source and record.source.start_line is not None:
|
|
172
|
+
is_invalid, error_msg = _is_position_invalid(record.source)
|
|
173
|
+
if is_invalid:
|
|
174
|
+
diagnostics.append(Diagnostic(
|
|
175
|
+
file=record._source_file,
|
|
176
|
+
line=record._start_line,
|
|
177
|
+
column=None,
|
|
178
|
+
severity=Severity.ERROR,
|
|
179
|
+
code=ErrorCode.E011,
|
|
180
|
+
message=f"Invalid range in @source: {error_msg}",
|
|
181
|
+
record_index=record_idx,
|
|
182
|
+
))
|
|
183
|
+
|
|
184
|
+
# Check @prior range
|
|
185
|
+
if record.prior and record.prior.start_line is not None:
|
|
186
|
+
is_invalid, error_msg = _is_position_invalid(record.prior)
|
|
187
|
+
if is_invalid:
|
|
188
|
+
diagnostics.append(Diagnostic(
|
|
189
|
+
file=record._source_file,
|
|
190
|
+
line=record._start_line,
|
|
191
|
+
column=None,
|
|
192
|
+
severity=Severity.ERROR,
|
|
193
|
+
code=ErrorCode.E011,
|
|
194
|
+
message=f"Invalid range in @prior: {error_msg}",
|
|
195
|
+
record_index=record_idx,
|
|
196
|
+
))
|
|
197
|
+
|
|
198
|
+
return diagnostics
|
|
199
|
+
|
|
200
|
+
|
|
140
201
|
def lint_canonical_format(
|
|
141
202
|
records: list[Record],
|
|
142
203
|
original_text: str,
|
|
@@ -206,6 +267,9 @@ def lint_string(
|
|
|
206
267
|
result.diagnostics.extend(lint_source_exists(record, base_path, idx))
|
|
207
268
|
result.diagnostics.extend(lint_prior_exists(record, base_path, idx))
|
|
208
269
|
|
|
270
|
+
# Check line range validity
|
|
271
|
+
result.diagnostics.extend(lint_line_range(record, idx))
|
|
272
|
+
|
|
209
273
|
# Check canonical format
|
|
210
274
|
if check_canonical and result.records and not result.has_errors:
|
|
211
275
|
result.diagnostics.extend(lint_canonical_format(
|
markback/parser.py
CHANGED
|
@@ -17,7 +17,7 @@ from .types import (
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
# Known header keywords
|
|
20
|
-
KNOWN_HEADERS = {"uri", "source", "prior"}
|
|
20
|
+
KNOWN_HEADERS = {"uri", "by", "source", "prior"}
|
|
21
21
|
|
|
22
22
|
# Patterns
|
|
23
23
|
HEADER_PATTERN = re.compile(r"^@([a-z]+)\s+(.+)$")
|
|
@@ -145,6 +145,7 @@ def parse_string(
|
|
|
145
145
|
nonlocal pending_uri, in_content, had_blank_line
|
|
146
146
|
|
|
147
147
|
uri = current_headers.get("uri") or pending_uri
|
|
148
|
+
by = current_headers.get("by")
|
|
148
149
|
source_str = current_headers.get("source")
|
|
149
150
|
source = SourceRef(source_str) if source_str else None
|
|
150
151
|
prior_str = current_headers.get("prior")
|
|
@@ -164,6 +165,7 @@ def parse_string(
|
|
|
164
165
|
record = Record(
|
|
165
166
|
feedback=feedback,
|
|
166
167
|
uri=uri,
|
|
168
|
+
by=by,
|
|
167
169
|
source=source,
|
|
168
170
|
prior=prior,
|
|
169
171
|
content=content,
|
|
@@ -242,14 +244,16 @@ def parse_string(
|
|
|
242
244
|
line_num,
|
|
243
245
|
)
|
|
244
246
|
|
|
245
|
-
# Use any pending @uri from previous line and @prior if present
|
|
247
|
+
# Use any pending @uri from previous line and @by, @prior if present
|
|
246
248
|
uri = pending_uri or current_headers.get("uri")
|
|
249
|
+
by = current_headers.get("by")
|
|
247
250
|
prior_str = current_headers.get("prior")
|
|
248
251
|
prior = SourceRef(prior_str) if prior_str else None
|
|
249
252
|
|
|
250
253
|
record = Record(
|
|
251
254
|
feedback=feedback or "",
|
|
252
255
|
uri=uri,
|
|
256
|
+
by=by,
|
|
253
257
|
source=source,
|
|
254
258
|
prior=prior,
|
|
255
259
|
content=None,
|
markback/types.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Core types for MarkBack format."""
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from dataclasses import dataclass, field
|
|
4
5
|
from enum import Enum
|
|
5
6
|
from pathlib import Path
|
|
@@ -25,6 +26,7 @@ class ErrorCode(Enum):
|
|
|
25
26
|
E008 = "E008" # Unclosed quote in structured attribute value
|
|
26
27
|
E009 = "E009" # Empty feedback (nothing after <<< )
|
|
27
28
|
E010 = "E010" # Missing blank line before inline content
|
|
29
|
+
E011 = "E011" # Invalid line range (end < start)
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
class WarningCode(Enum):
|
|
@@ -76,29 +78,90 @@ class Diagnostic:
|
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
|
|
81
|
+
# Regex to parse line/character range from a path
|
|
82
|
+
# Supports: path:line, path:line:col, path:line-line, path:line:col-line:col
|
|
83
|
+
_LINE_RANGE_PATTERN = re.compile(r'^(.+?):(\d+)(?::(\d+))?(?:-(\d+)(?::(\d+))?)?$')
|
|
84
|
+
|
|
85
|
+
|
|
79
86
|
@dataclass
|
|
80
87
|
class SourceRef:
|
|
81
88
|
"""Reference to external content (file path or URI)."""
|
|
82
89
|
value: str
|
|
83
90
|
is_uri: bool = False
|
|
91
|
+
start_line: Optional[int] = None
|
|
92
|
+
end_line: Optional[int] = None
|
|
93
|
+
start_column: Optional[int] = None
|
|
94
|
+
end_column: Optional[int] = None
|
|
95
|
+
_path_only: str = ""
|
|
84
96
|
|
|
85
97
|
def __post_init__(self):
|
|
86
|
-
#
|
|
98
|
+
# Parse line range if present
|
|
99
|
+
self._parse_line_range()
|
|
100
|
+
|
|
101
|
+
# Determine if this is a URI or file path (using path without line range)
|
|
87
102
|
if not self.is_uri:
|
|
88
|
-
parsed = urlparse(self.
|
|
103
|
+
parsed = urlparse(self._path_only)
|
|
89
104
|
# Consider it a URI if it has a scheme that's not a Windows drive letter
|
|
90
105
|
self.is_uri = bool(parsed.scheme) and len(parsed.scheme) > 1
|
|
91
106
|
|
|
107
|
+
def _parse_line_range(self):
|
|
108
|
+
"""Parse optional line/character range from value."""
|
|
109
|
+
match = _LINE_RANGE_PATTERN.match(self.value)
|
|
110
|
+
if match:
|
|
111
|
+
self._path_only = match.group(1)
|
|
112
|
+
self.start_line = int(match.group(2))
|
|
113
|
+
if match.group(3):
|
|
114
|
+
self.start_column = int(match.group(3))
|
|
115
|
+
if match.group(4):
|
|
116
|
+
self.end_line = int(match.group(4))
|
|
117
|
+
if match.group(5):
|
|
118
|
+
self.end_column = int(match.group(5))
|
|
119
|
+
else:
|
|
120
|
+
# Single line/position reference: start and end are the same
|
|
121
|
+
self.end_line = self.start_line
|
|
122
|
+
self.end_column = self.start_column
|
|
123
|
+
else:
|
|
124
|
+
self._path_only = self.value
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def path(self) -> str:
|
|
128
|
+
"""Return path without line range."""
|
|
129
|
+
return self._path_only
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def line_range_str(self) -> Optional[str]:
|
|
133
|
+
"""Return formatted line/character range string, or None if no range."""
|
|
134
|
+
if self.start_line is None:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
# Build start position
|
|
138
|
+
if self.start_column is not None:
|
|
139
|
+
start = f":{self.start_line}:{self.start_column}"
|
|
140
|
+
else:
|
|
141
|
+
start = f":{self.start_line}"
|
|
142
|
+
|
|
143
|
+
# Check if end is the same as start (single position)
|
|
144
|
+
if self.start_line == self.end_line and self.start_column == self.end_column:
|
|
145
|
+
return start
|
|
146
|
+
|
|
147
|
+
# Build end position
|
|
148
|
+
if self.end_column is not None:
|
|
149
|
+
end = f"-{self.end_line}:{self.end_column}"
|
|
150
|
+
else:
|
|
151
|
+
end = f"-{self.end_line}"
|
|
152
|
+
|
|
153
|
+
return f"{start}{end}"
|
|
154
|
+
|
|
92
155
|
def resolve(self, base_path: Optional[Path] = None) -> Path:
|
|
93
156
|
"""Resolve to a file path (relative paths resolved against base_path)."""
|
|
94
157
|
if self.is_uri:
|
|
95
|
-
parsed = urlparse(self.
|
|
158
|
+
parsed = urlparse(self._path_only)
|
|
96
159
|
if parsed.scheme == "file":
|
|
97
160
|
# file:// URI
|
|
98
161
|
return Path(parsed.path)
|
|
99
162
|
raise ValueError(f"Cannot resolve non-file URI to path: {self.value}")
|
|
100
163
|
|
|
101
|
-
path = Path(self.
|
|
164
|
+
path = Path(self._path_only)
|
|
102
165
|
if path.is_absolute():
|
|
103
166
|
return path
|
|
104
167
|
if base_path:
|
|
@@ -122,6 +185,7 @@ class Record:
|
|
|
122
185
|
"""A MarkBack record containing content and feedback."""
|
|
123
186
|
feedback: str
|
|
124
187
|
uri: Optional[str] = None
|
|
188
|
+
by: Optional[str] = None
|
|
125
189
|
source: Optional[SourceRef] = None
|
|
126
190
|
prior: Optional[SourceRef] = None
|
|
127
191
|
content: Optional[str] = None
|
|
@@ -155,6 +219,7 @@ class Record:
|
|
|
155
219
|
"""Convert to JSON-serializable dict."""
|
|
156
220
|
return {
|
|
157
221
|
"uri": self.uri,
|
|
222
|
+
"by": self.by,
|
|
158
223
|
"source": str(self.source) if self.source else None,
|
|
159
224
|
"prior": str(self.prior) if self.prior else None,
|
|
160
225
|
"content": self.content,
|
markback/writer.py
CHANGED
|
@@ -38,17 +38,21 @@ def write_record_canonical(
|
|
|
38
38
|
)
|
|
39
39
|
|
|
40
40
|
if use_compact:
|
|
41
|
-
# Compact format: @uri on
|
|
41
|
+
# Compact format: @uri, @by, @prior on own lines (if present), then @source ... <<<
|
|
42
42
|
if record.uri:
|
|
43
43
|
lines.append(f"@uri {record.uri}")
|
|
44
|
+
if record.by:
|
|
45
|
+
lines.append(f"@by {record.by}")
|
|
44
46
|
if record.prior:
|
|
45
47
|
lines.append(f"@prior {record.prior}")
|
|
46
48
|
lines.append(f"@source {record.source} <<< {record.feedback}")
|
|
47
49
|
else:
|
|
48
50
|
# Full format
|
|
49
|
-
# Headers: @uri first, then @prior, then @source
|
|
51
|
+
# Headers: @uri first, then @by, then @prior, then @source
|
|
50
52
|
if record.uri:
|
|
51
53
|
lines.append(f"@uri {record.uri}")
|
|
54
|
+
if record.by:
|
|
55
|
+
lines.append(f"@by {record.by}")
|
|
52
56
|
if record.prior:
|
|
53
57
|
lines.append(f"@prior {record.prior}")
|
|
54
58
|
if record.source:
|
|
@@ -151,7 +155,10 @@ def write_label_file(record: Record) -> str:
|
|
|
151
155
|
|
|
152
156
|
if record.uri:
|
|
153
157
|
lines.append(f"@uri {record.uri}")
|
|
154
|
-
|
|
158
|
+
|
|
159
|
+
if record.by:
|
|
160
|
+
lines.append(f"@by {record.by}")
|
|
161
|
+
|
|
155
162
|
if record.prior:
|
|
156
163
|
lines.append(f"@prior {record.prior}")
|
|
157
164
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markback
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A compact, human-writable format for storing content paired with feedback/labels
|
|
5
5
|
Project-URL: Homepage, https://github.com/dandriscoll/markback
|
|
6
6
|
Project-URL: Repository, https://github.com/dandriscoll/markback
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
markback/__init__.py,sha256=B0-2dpUu5nkbnUI0hPz-x7PHiOl7M-tiRi6s3UCYJFk,1540
|
|
2
|
+
markback/cli.py,sha256=5wMk1OUG7W_voS9DxeFxRJrBTMabEdOK_s_o3Irxuu0,13639
|
|
3
|
+
markback/config.py,sha256=eTVhb7UwDER9FRYo8QUAvneLHSqXD2ZtLUgtBtnljUs,5455
|
|
4
|
+
markback/linter.py,sha256=vKy5JHTEGspF4-lWRL9o4zDWieghqQWkah-bP_n8-kM,11977
|
|
5
|
+
markback/llm.py,sha256=ON5_2C6v4KIk7_aIceulfWjEEI6hmallaPlLv-1-s_o,4692
|
|
6
|
+
markback/parser.py,sha256=KHC1QKmN1wSnYgcobC36zXUqL6cdNsZajC6fHlhETZc,19016
|
|
7
|
+
markback/types.py,sha256=t6HdFIWgBTlCfC2KRKKhSO4VBzfxZcP3Uq_zoh-bXZ4,11006
|
|
8
|
+
markback/workflow.py,sha256=zC1RUm1i1wgiciFDqUilJKJ0-bgInvctxhQ0h5WSdoQ,10485
|
|
9
|
+
markback/writer.py,sha256=HY_RYnEk27cIsodo9gGf_MvUb_Bvdmkz05S9kyq6Tdo,8093
|
|
10
|
+
markback-0.1.3.dist-info/METADATA,sha256=ZvzRC2PHMoowUUjoJFzpPzw2425HD7OM-_lr09BOJho,5133
|
|
11
|
+
markback-0.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
+
markback-0.1.3.dist-info/entry_points.txt,sha256=Bc9aXvtlPxVPuOJ9BWGngAVrkx5dMvRgujjVzXC-V5U,46
|
|
13
|
+
markback-0.1.3.dist-info/licenses/LICENSE,sha256=lLK1n13C_CXb0M10O-6itEIDY6dsXKutZYQH-09n6s0,1068
|
|
14
|
+
markback-0.1.3.dist-info/RECORD,,
|
markback-0.1.1.dist-info/RECORD
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
markback/__init__.py,sha256=B0-2dpUu5nkbnUI0hPz-x7PHiOl7M-tiRi6s3UCYJFk,1540
|
|
2
|
-
markback/cli.py,sha256=5wMk1OUG7W_voS9DxeFxRJrBTMabEdOK_s_o3Irxuu0,13639
|
|
3
|
-
markback/config.py,sha256=eTVhb7UwDER9FRYo8QUAvneLHSqXD2ZtLUgtBtnljUs,5455
|
|
4
|
-
markback/linter.py,sha256=JfhQV9R6K6Rm1I0Tu9ugU5d19MegQ9eZHlI_wyjk55k,9653
|
|
5
|
-
markback/llm.py,sha256=ON5_2C6v4KIk7_aIceulfWjEEI6hmallaPlLv-1-s_o,4692
|
|
6
|
-
markback/parser.py,sha256=P7GRjlwhy8j6Tnub7XAqILtZ4pFdkfdHhB-aIjLVRYU,18881
|
|
7
|
-
markback/types.py,sha256=6-aO1tkn9JZ1B47IFi7N9lf5_V8Y0LFrNQMoPgkp8Kc,8707
|
|
8
|
-
markback/workflow.py,sha256=zC1RUm1i1wgiciFDqUilJKJ0-bgInvctxhQ0h5WSdoQ,10485
|
|
9
|
-
markback/writer.py,sha256=3-LeupyuruGv4WZH9pV65hU4YDKuC5HgIIZ8YZ2SZnM,7896
|
|
10
|
-
markback-0.1.1.dist-info/METADATA,sha256=KsRmvJPyZLbvHGcNIeMiemLDTH2z2znLrs9U1SPCUVE,5133
|
|
11
|
-
markback-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
-
markback-0.1.1.dist-info/entry_points.txt,sha256=Bc9aXvtlPxVPuOJ9BWGngAVrkx5dMvRgujjVzXC-V5U,46
|
|
13
|
-
markback-0.1.1.dist-info/licenses/LICENSE,sha256=lLK1n13C_CXb0M10O-6itEIDY6dsXKutZYQH-09n6s0,1068
|
|
14
|
-
markback-0.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|