markback 0.1.1__py3-none-any.whl → 0.1.2__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 CHANGED
@@ -137,6 +137,42 @@ def lint_prior_exists(
137
137
  return diagnostics
138
138
 
139
139
 
140
+ def lint_line_range(
141
+ record: Record,
142
+ record_idx: int,
143
+ ) -> list[Diagnostic]:
144
+ """Check if line ranges are valid (end >= start)."""
145
+ diagnostics: list[Diagnostic] = []
146
+
147
+ # Check @source line range
148
+ if record.source and record.source.start_line is not None:
149
+ if record.source.end_line is not None and record.source.end_line < record.source.start_line:
150
+ diagnostics.append(Diagnostic(
151
+ file=record._source_file,
152
+ line=record._start_line,
153
+ column=None,
154
+ severity=Severity.ERROR,
155
+ code=ErrorCode.E011,
156
+ message=f"Invalid line range in @source: end line {record.source.end_line} is less than start line {record.source.start_line}",
157
+ record_index=record_idx,
158
+ ))
159
+
160
+ # Check @prior line range
161
+ if record.prior and record.prior.start_line is not None:
162
+ if record.prior.end_line is not None and record.prior.end_line < record.prior.start_line:
163
+ diagnostics.append(Diagnostic(
164
+ file=record._source_file,
165
+ line=record._start_line,
166
+ column=None,
167
+ severity=Severity.ERROR,
168
+ code=ErrorCode.E011,
169
+ message=f"Invalid line range in @prior: end line {record.prior.end_line} is less than start line {record.prior.start_line}",
170
+ record_index=record_idx,
171
+ ))
172
+
173
+ return diagnostics
174
+
175
+
140
176
  def lint_canonical_format(
141
177
  records: list[Record],
142
178
  original_text: str,
@@ -206,6 +242,9 @@ def lint_string(
206
242
  result.diagnostics.extend(lint_source_exists(record, base_path, idx))
207
243
  result.diagnostics.extend(lint_prior_exists(record, base_path, idx))
208
244
 
245
+ # Check line range validity
246
+ result.diagnostics.extend(lint_line_range(record, idx))
247
+
209
248
  # Check canonical format
210
249
  if check_canonical and result.records and not result.has_errors:
211
250
  result.diagnostics.extend(lint_canonical_format(
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,67 @@ class Diagnostic:
76
78
  }
77
79
 
78
80
 
81
+ # Regex to parse line range from a path: path:start or path:start-end
82
+ _LINE_RANGE_PATTERN = re.compile(r'^(.+?):(\d+)(?:-(\d+))?$')
83
+
84
+
79
85
  @dataclass
80
86
  class SourceRef:
81
87
  """Reference to external content (file path or URI)."""
82
88
  value: str
83
89
  is_uri: bool = False
90
+ start_line: Optional[int] = None
91
+ end_line: Optional[int] = None
92
+ _path_only: str = ""
84
93
 
85
94
  def __post_init__(self):
86
- # Determine if this is a URI or file path
95
+ # Parse line range if present
96
+ self._parse_line_range()
97
+
98
+ # Determine if this is a URI or file path (using path without line range)
87
99
  if not self.is_uri:
88
- parsed = urlparse(self.value)
100
+ parsed = urlparse(self._path_only)
89
101
  # Consider it a URI if it has a scheme that's not a Windows drive letter
90
102
  self.is_uri = bool(parsed.scheme) and len(parsed.scheme) > 1
91
103
 
104
+ def _parse_line_range(self):
105
+ """Parse optional line range from value."""
106
+ match = _LINE_RANGE_PATTERN.match(self.value)
107
+ if match:
108
+ self._path_only = match.group(1)
109
+ self.start_line = int(match.group(2))
110
+ if match.group(3):
111
+ self.end_line = int(match.group(3))
112
+ else:
113
+ # Single line reference: start and end are the same
114
+ self.end_line = self.start_line
115
+ else:
116
+ self._path_only = self.value
117
+
118
+ @property
119
+ def path(self) -> str:
120
+ """Return path without line range."""
121
+ return self._path_only
122
+
123
+ @property
124
+ def line_range_str(self) -> Optional[str]:
125
+ """Return formatted line range string, or None if no range."""
126
+ if self.start_line is None:
127
+ return None
128
+ if self.start_line == self.end_line:
129
+ return f":{self.start_line}"
130
+ return f":{self.start_line}-{self.end_line}"
131
+
92
132
  def resolve(self, base_path: Optional[Path] = None) -> Path:
93
133
  """Resolve to a file path (relative paths resolved against base_path)."""
94
134
  if self.is_uri:
95
- parsed = urlparse(self.value)
135
+ parsed = urlparse(self._path_only)
96
136
  if parsed.scheme == "file":
97
137
  # file:// URI
98
138
  return Path(parsed.path)
99
139
  raise ValueError(f"Cannot resolve non-file URI to path: {self.value}")
100
140
 
101
- path = Path(self.value)
141
+ path = Path(self._path_only)
102
142
  if path.is_absolute():
103
143
  return path
104
144
  if base_path:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: markback
3
- Version: 0.1.1
3
+ Version: 0.1.2
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
@@ -1,14 +1,14 @@
1
1
  markback/__init__.py,sha256=B0-2dpUu5nkbnUI0hPz-x7PHiOl7M-tiRi6s3UCYJFk,1540
2
2
  markback/cli.py,sha256=5wMk1OUG7W_voS9DxeFxRJrBTMabEdOK_s_o3Irxuu0,13639
3
3
  markback/config.py,sha256=eTVhb7UwDER9FRYo8QUAvneLHSqXD2ZtLUgtBtnljUs,5455
4
- markback/linter.py,sha256=JfhQV9R6K6Rm1I0Tu9ugU5d19MegQ9eZHlI_wyjk55k,9653
4
+ markback/linter.py,sha256=IctgPEFNfGDo5DcELdo0Ni3d7Dp0bIWtlDw61ccWDOQ,11210
5
5
  markback/llm.py,sha256=ON5_2C6v4KIk7_aIceulfWjEEI6hmallaPlLv-1-s_o,4692
6
6
  markback/parser.py,sha256=P7GRjlwhy8j6Tnub7XAqILtZ4pFdkfdHhB-aIjLVRYU,18881
7
- markback/types.py,sha256=6-aO1tkn9JZ1B47IFi7N9lf5_V8Y0LFrNQMoPgkp8Kc,8707
7
+ markback/types.py,sha256=tFunBAoqUEVf9mi_4N1QwWmOsKt0nocZK7M7K_rybWg,10097
8
8
  markback/workflow.py,sha256=zC1RUm1i1wgiciFDqUilJKJ0-bgInvctxhQ0h5WSdoQ,10485
9
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,,
10
+ markback-0.1.2.dist-info/METADATA,sha256=d_xMmpicyEYeakJ4hA8SsrlGgj5Qxpd6fMv7tKf-eaI,5133
11
+ markback-0.1.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ markback-0.1.2.dist-info/entry_points.txt,sha256=Bc9aXvtlPxVPuOJ9BWGngAVrkx5dMvRgujjVzXC-V5U,46
13
+ markback-0.1.2.dist-info/licenses/LICENSE,sha256=lLK1n13C_CXb0M10O-6itEIDY6dsXKutZYQH-09n6s0,1068
14
+ markback-0.1.2.dist-info/RECORD,,