markback 0.1.1__tar.gz → 0.1.2__tar.gz
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-0.1.2/.claude/settings.local.json +13 -0
- {markback-0.1.1 → markback-0.1.2}/PKG-INFO +1 -1
- {markback-0.1.1 → markback-0.1.2}/SPEC.md +26 -2
- {markback-0.1.1 → markback-0.1.2}/markback/linter.py +39 -0
- {markback-0.1.1 → markback-0.1.2}/markback/types.py +44 -4
- {markback-0.1.1 → markback-0.1.2}/packages/markbackjs/src/linter.ts +43 -0
- {markback-0.1.1 → markback-0.1.2}/packages/markbackjs/src/types.ts +50 -7
- {markback-0.1.1 → markback-0.1.2}/packages/markbackjs/test/linter.test.js +54 -0
- {markback-0.1.1 → markback-0.1.2}/pyproject.toml +1 -1
- {markback-0.1.1 → markback-0.1.2}/tests/test_linter.py +81 -0
- markback-0.1.1/.claude/settings.local.json +0 -9
- {markback-0.1.1 → markback-0.1.2}/.gitignore +0 -0
- {markback-0.1.1 → markback-0.1.2}/.ishipped/card.md +0 -0
- {markback-0.1.1 → markback-0.1.2}/IMPLEMENTATION_NOTES.md +0 -0
- {markback-0.1.1 → markback-0.1.2}/LICENSE +0 -0
- {markback-0.1.1 → markback-0.1.2}/README.md +0 -0
- {markback-0.1.1 → markback-0.1.2}/markback/__init__.py +0 -0
- {markback-0.1.1 → markback-0.1.2}/markback/cli.py +0 -0
- {markback-0.1.1 → markback-0.1.2}/markback/config.py +0 -0
- {markback-0.1.1 → markback-0.1.2}/markback/llm.py +0 -0
- {markback-0.1.1 → markback-0.1.2}/markback/parser.py +0 -0
- {markback-0.1.1 → markback-0.1.2}/markback/workflow.py +0 -0
- {markback-0.1.1 → markback-0.1.2}/markback/writer.py +0 -0
- {markback-0.1.1 → markback-0.1.2}/packages/markbackjs/LICENSE +0 -0
- {markback-0.1.1 → markback-0.1.2}/packages/markbackjs/README.md +0 -0
- {markback-0.1.1 → markback-0.1.2}/packages/markbackjs/package-lock.json +0 -0
- {markback-0.1.1 → markback-0.1.2}/packages/markbackjs/package.json +0 -0
- {markback-0.1.1 → markback-0.1.2}/packages/markbackjs/src/index.ts +0 -0
- {markback-0.1.1 → markback-0.1.2}/packages/markbackjs/src/parser.ts +0 -0
- {markback-0.1.1 → markback-0.1.2}/packages/markbackjs/src/writer.ts +0 -0
- {markback-0.1.1 → markback-0.1.2}/packages/markbackjs/tsconfig.json +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/__init__.py +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/fixtures/compact_source.mb +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/fixtures/errors/content_with_source.mb +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/fixtures/errors/empty_feedback.mb +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/fixtures/errors/malformed_uri.mb +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/fixtures/errors/missing_feedback.mb +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/fixtures/errors/multiple_feedback.mb +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/fixtures/essay.label.txt +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/fixtures/essay.txt +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/fixtures/external_source.mb +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/fixtures/freeform_feedback.mb +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/fixtures/json_feedback.mb +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/fixtures/label_list.mb +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/fixtures/minimal.mb +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/fixtures/multi_record.mb +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/fixtures/with_uri.mb +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/test_cli.py +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/test_config.py +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/test_parser.py +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/test_types.py +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/test_workflow.py +0 -0
- {markback-0.1.1 → markback-0.1.2}/tests/test_writer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markback
|
|
3
|
-
Version: 0.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
|
|
@@ -127,6 +127,27 @@ References an item that precedes the source material. For example, if the source
|
|
|
127
127
|
- `@prior` does not affect content handling (inline content or `@source` rules still apply)
|
|
128
128
|
- Parsers SHOULD verify referenced files exist (warning if missing)
|
|
129
129
|
|
|
130
|
+
#### 3.1.4 Line Range Specification
|
|
131
|
+
|
|
132
|
+
Both `@source` and `@prior` headers support optional line range specifications using colon notation. This allows referencing specific lines within a file.
|
|
133
|
+
|
|
134
|
+
**Syntax:** `<path-or-uri>:<start>` or `<path-or-uri>:<start>-<end>`
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
@source ./code.py:42
|
|
138
|
+
@source ./code.py:42-50
|
|
139
|
+
@prior ./prompts/template.txt:1-20
|
|
140
|
+
@source https://example.com/file.txt:100-150
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Rules:**
|
|
144
|
+
- Line numbers are 1-indexed (first line is line 1)
|
|
145
|
+
- Single line: `:N` references line N only
|
|
146
|
+
- Line range: `:N-M` references lines N through M (inclusive)
|
|
147
|
+
- End line must be greater than or equal to start line (E011 error otherwise)
|
|
148
|
+
- Line ranges are informational metadata; parsers do not validate that referenced lines exist in the file
|
|
149
|
+
- Windows drive letters (e.g., `C:\path`) are not confused with line ranges because scheme detection requires length > 1
|
|
150
|
+
|
|
130
151
|
### 3.2 Content Block
|
|
131
152
|
|
|
132
153
|
Content is everything between headers and the `<<<` feedback delimiter.
|
|
@@ -575,6 +596,7 @@ Each line is classified as one of:
|
|
|
575
596
|
| E008 | Unclosed quote in structured attribute value (only in `structured` parse mode) |
|
|
576
597
|
| E009 | Empty feedback (nothing after `<<< `) |
|
|
577
598
|
| E010 | Missing blank line before inline content (content starts with `@`) |
|
|
599
|
+
| E011 | Invalid line range (end line less than start line) |
|
|
578
600
|
|
|
579
601
|
### 7.2 Warnings (SHOULD fix)
|
|
580
602
|
|
|
@@ -863,8 +885,10 @@ feedback-content = *VCHAR ; no LF allowed
|
|
|
863
885
|
compact-record = [uri-line] source-feedback-line
|
|
864
886
|
compact-list = compact-record *(1*blank-line compact-record)
|
|
865
887
|
uri-line = "@uri" SP value LF
|
|
866
|
-
source-feedback-line = "@source" SP path SP "<<<" SP feedback-content LF
|
|
867
|
-
path
|
|
888
|
+
source-feedback-line = "@source" SP path-with-range SP "<<<" SP feedback-content LF
|
|
889
|
+
path-with-range = path [line-range] ; path with optional line range
|
|
890
|
+
path = 1*VCHAR ; ends at space before <<< or line-range
|
|
891
|
+
line-range = ":" 1*DIGIT ["-" 1*DIGIT]
|
|
868
892
|
|
|
869
893
|
LOWER = %x61-7A ; a-z
|
|
870
894
|
SP = %x20 ; space
|
|
@@ -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(
|
|
@@ -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
|
-
#
|
|
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.
|
|
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.
|
|
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.
|
|
141
|
+
path = Path(self._path_only)
|
|
102
142
|
if path.is_absolute():
|
|
103
143
|
return path
|
|
104
144
|
if base_path:
|
|
@@ -138,6 +138,46 @@ function lintPriorExists(record: MarkbackRecord, basePath: string | null, record
|
|
|
138
138
|
return diagnostics;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
function lintLineRange(record: MarkbackRecord, recordIdx: number): Diagnostic[] {
|
|
142
|
+
const diagnostics: Diagnostic[] = [];
|
|
143
|
+
|
|
144
|
+
// Check @source line range
|
|
145
|
+
if (record.source && record.source.startLine !== null) {
|
|
146
|
+
if (record.source.endLine !== null && record.source.endLine < record.source.startLine) {
|
|
147
|
+
diagnostics.push(
|
|
148
|
+
new Diagnostic({
|
|
149
|
+
file: record._sourceFile ?? null,
|
|
150
|
+
line: record._startLine ?? null,
|
|
151
|
+
column: null,
|
|
152
|
+
severity: Severity.ERROR,
|
|
153
|
+
code: ErrorCode.E011,
|
|
154
|
+
message: `Invalid line range in @source: end line ${record.source.endLine} is less than start line ${record.source.startLine}`,
|
|
155
|
+
recordIndex: recordIdx,
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check @prior line range
|
|
162
|
+
if (record.prior && record.prior.startLine !== null) {
|
|
163
|
+
if (record.prior.endLine !== null && record.prior.endLine < record.prior.startLine) {
|
|
164
|
+
diagnostics.push(
|
|
165
|
+
new Diagnostic({
|
|
166
|
+
file: record._sourceFile ?? null,
|
|
167
|
+
line: record._startLine ?? null,
|
|
168
|
+
column: null,
|
|
169
|
+
severity: Severity.ERROR,
|
|
170
|
+
code: ErrorCode.E011,
|
|
171
|
+
message: `Invalid line range in @prior: end line ${record.prior.endLine} is less than start line ${record.prior.startLine}`,
|
|
172
|
+
recordIndex: recordIdx,
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return diagnostics;
|
|
179
|
+
}
|
|
180
|
+
|
|
141
181
|
function lintCanonicalFormat(records: MarkbackRecord[], originalText: string, file?: string | null): Diagnostic[] {
|
|
142
182
|
const diagnostics: Diagnostic[] = [];
|
|
143
183
|
|
|
@@ -199,6 +239,9 @@ export function lintString(text: string, options: LintOptions = {}): ParseResult
|
|
|
199
239
|
result.diagnostics.push(...lintSourceExists(record, basePath, idx));
|
|
200
240
|
result.diagnostics.push(...lintPriorExists(record, basePath, idx));
|
|
201
241
|
}
|
|
242
|
+
|
|
243
|
+
// Check line range validity
|
|
244
|
+
result.diagnostics.push(...lintLineRange(record, idx));
|
|
202
245
|
});
|
|
203
246
|
|
|
204
247
|
if (checkCanonical && result.records.length > 0 && !result.hasErrors) {
|
|
@@ -17,6 +17,7 @@ export enum ErrorCode {
|
|
|
17
17
|
E008 = "E008",
|
|
18
18
|
E009 = "E009",
|
|
19
19
|
E010 = "E010",
|
|
20
|
+
E011 = "E011",
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export enum WarningCode {
|
|
@@ -99,39 +100,81 @@ function extractScheme(value: string): string | null {
|
|
|
99
100
|
return match ? match[1] : null;
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
// Regex to parse line range from a path: path:start or path:start-end
|
|
104
|
+
const LINE_RANGE_PATTERN = /^(.+?):(\d+)(?:-(\d+))?$/;
|
|
105
|
+
|
|
102
106
|
export class SourceRef {
|
|
103
107
|
value: string;
|
|
104
108
|
isUri: boolean;
|
|
109
|
+
startLine: number | null;
|
|
110
|
+
endLine: number | null;
|
|
111
|
+
private _pathOnly: string;
|
|
105
112
|
|
|
106
113
|
constructor(value: string, isUri = false) {
|
|
107
114
|
this.value = value;
|
|
115
|
+
this.startLine = null;
|
|
116
|
+
this.endLine = null;
|
|
117
|
+
this._pathOnly = value;
|
|
118
|
+
|
|
119
|
+
// Parse line range if present
|
|
120
|
+
this._parseLineRange();
|
|
121
|
+
|
|
108
122
|
if (isUri) {
|
|
109
123
|
this.isUri = true;
|
|
110
124
|
return;
|
|
111
125
|
}
|
|
112
126
|
|
|
113
|
-
|
|
127
|
+
// Determine if this is a URI (using path without line range)
|
|
128
|
+
const scheme = extractScheme(this._pathOnly);
|
|
114
129
|
this.isUri = !!scheme && scheme.length > 1;
|
|
115
130
|
}
|
|
116
131
|
|
|
132
|
+
private _parseLineRange(): void {
|
|
133
|
+
const match = LINE_RANGE_PATTERN.exec(this.value);
|
|
134
|
+
if (match) {
|
|
135
|
+
this._pathOnly = match[1];
|
|
136
|
+
this.startLine = parseInt(match[2], 10);
|
|
137
|
+
if (match[3]) {
|
|
138
|
+
this.endLine = parseInt(match[3], 10);
|
|
139
|
+
} else {
|
|
140
|
+
// Single line reference: start and end are the same
|
|
141
|
+
this.endLine = this.startLine;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get path(): string {
|
|
147
|
+
return this._pathOnly;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get lineRangeStr(): string | null {
|
|
151
|
+
if (this.startLine === null) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
if (this.startLine === this.endLine) {
|
|
155
|
+
return `:${this.startLine}`;
|
|
156
|
+
}
|
|
157
|
+
return `:${this.startLine}-${this.endLine}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
117
160
|
resolve(basePath?: string | null): string {
|
|
118
161
|
if (this.isUri) {
|
|
119
|
-
const scheme = extractScheme(this.
|
|
162
|
+
const scheme = extractScheme(this._pathOnly);
|
|
120
163
|
if (scheme && scheme.toLowerCase() === "file") {
|
|
121
|
-
return fileURLToPath(new URL(this.
|
|
164
|
+
return fileURLToPath(new URL(this._pathOnly));
|
|
122
165
|
}
|
|
123
166
|
throw new Error(`Cannot resolve non-file URI to path: ${this.value}`);
|
|
124
167
|
}
|
|
125
168
|
|
|
126
|
-
if (path.isAbsolute(this.
|
|
127
|
-
return this.
|
|
169
|
+
if (path.isAbsolute(this._pathOnly)) {
|
|
170
|
+
return this._pathOnly;
|
|
128
171
|
}
|
|
129
172
|
|
|
130
173
|
if (basePath) {
|
|
131
|
-
return path.join(basePath, this.
|
|
174
|
+
return path.join(basePath, this._pathOnly);
|
|
132
175
|
}
|
|
133
176
|
|
|
134
|
-
return this.
|
|
177
|
+
return this._pathOnly;
|
|
135
178
|
}
|
|
136
179
|
|
|
137
180
|
toString(): string {
|
|
@@ -105,3 +105,57 @@ test("lintString: @prior URI not checked", () => {
|
|
|
105
105
|
// Should not have W009 for URI-based @prior
|
|
106
106
|
assert.equal(findCode(result.diagnostics, WarningCode.W009).length, 0);
|
|
107
107
|
});
|
|
108
|
+
|
|
109
|
+
// Line range support tests
|
|
110
|
+
|
|
111
|
+
test("lintString: @source with single line", () => {
|
|
112
|
+
const text = "@source ./code.py:42 <<< good\n";
|
|
113
|
+
const result = lintString(text, { checkSources: false, checkCanonical: false });
|
|
114
|
+
assert.equal(result.hasErrors, false);
|
|
115
|
+
assert.equal(result.records[0].source.path, "./code.py");
|
|
116
|
+
assert.equal(result.records[0].source.startLine, 42);
|
|
117
|
+
assert.equal(result.records[0].source.endLine, 42);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("lintString: @source with line range", () => {
|
|
121
|
+
const text = "@source ./code.py:10-20 <<< good\n";
|
|
122
|
+
const result = lintString(text, { checkSources: false, checkCanonical: false });
|
|
123
|
+
assert.equal(result.hasErrors, false);
|
|
124
|
+
assert.equal(result.records[0].source.path, "./code.py");
|
|
125
|
+
assert.equal(result.records[0].source.startLine, 10);
|
|
126
|
+
assert.equal(result.records[0].source.endLine, 20);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("lintString: @prior with line range", () => {
|
|
130
|
+
const text = "@prior ./prompts/template.txt:1-20\n@source ./output.txt\n<<< good\n";
|
|
131
|
+
const result = lintString(text, { checkSources: false, checkCanonical: false });
|
|
132
|
+
assert.equal(result.hasErrors, false);
|
|
133
|
+
assert.equal(result.records[0].prior.path, "./prompts/template.txt");
|
|
134
|
+
assert.equal(result.records[0].prior.startLine, 1);
|
|
135
|
+
assert.equal(result.records[0].prior.endLine, 20);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("lintString: compact record with line range", () => {
|
|
139
|
+
const text = "@uri local:item-001\n@source ./file.txt:100-150 <<< feedback\n";
|
|
140
|
+
const result = lintString(text, { checkSources: false, checkCanonical: false });
|
|
141
|
+
assert.equal(result.hasErrors, false);
|
|
142
|
+
assert.equal(result.records[0].source.path, "./file.txt");
|
|
143
|
+
assert.equal(result.records[0].source.startLine, 100);
|
|
144
|
+
assert.equal(result.records[0].source.endLine, 150);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("lintString: invalid line range end < start", () => {
|
|
148
|
+
const text = "@source ./code.py:50-10 <<< good\n";
|
|
149
|
+
const result = lintString(text, { checkSources: false, checkCanonical: false });
|
|
150
|
+
assert.equal(result.hasErrors, true);
|
|
151
|
+
assert.equal(findCode(result.diagnostics, ErrorCode.E011).length, 1);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("lintString: source without line range still works", () => {
|
|
155
|
+
const text = "@source ./code.py <<< good\n";
|
|
156
|
+
const result = lintString(text, { checkSources: false, checkCanonical: false });
|
|
157
|
+
assert.equal(result.hasErrors, false);
|
|
158
|
+
assert.equal(result.records[0].source.path, "./code.py");
|
|
159
|
+
assert.equal(result.records[0].source.startLine, null);
|
|
160
|
+
assert.equal(result.records[0].source.endLine, null);
|
|
161
|
+
});
|
|
@@ -245,3 +245,84 @@ class TestSummarizeResults:
|
|
|
245
245
|
assert "records" in summary
|
|
246
246
|
assert "errors" in summary
|
|
247
247
|
assert "warnings" in summary
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class TestLineRangeSupport:
|
|
251
|
+
"""Tests for line range support in @source and @prior."""
|
|
252
|
+
|
|
253
|
+
def test_source_with_single_line(self):
|
|
254
|
+
"""Test @source with single line reference."""
|
|
255
|
+
text = "@source ./code.py:42 <<< good\n"
|
|
256
|
+
result = lint_string(text, check_sources=False, check_canonical=False)
|
|
257
|
+
|
|
258
|
+
assert not result.has_errors
|
|
259
|
+
assert result.records[0].source is not None
|
|
260
|
+
assert result.records[0].source.path == "./code.py"
|
|
261
|
+
assert result.records[0].source.start_line == 42
|
|
262
|
+
assert result.records[0].source.end_line == 42
|
|
263
|
+
|
|
264
|
+
def test_source_with_line_range(self):
|
|
265
|
+
"""Test @source with line range reference."""
|
|
266
|
+
text = "@source ./code.py:10-20 <<< good\n"
|
|
267
|
+
result = lint_string(text, check_sources=False, check_canonical=False)
|
|
268
|
+
|
|
269
|
+
assert not result.has_errors
|
|
270
|
+
assert result.records[0].source is not None
|
|
271
|
+
assert result.records[0].source.path == "./code.py"
|
|
272
|
+
assert result.records[0].source.start_line == 10
|
|
273
|
+
assert result.records[0].source.end_line == 20
|
|
274
|
+
|
|
275
|
+
def test_prior_with_line_range(self):
|
|
276
|
+
"""Test @prior with line range reference."""
|
|
277
|
+
text = "@prior ./prompts/template.txt:1-20\n@source ./output.txt\n<<< good\n"
|
|
278
|
+
result = lint_string(text, check_sources=False, check_canonical=False)
|
|
279
|
+
|
|
280
|
+
assert not result.has_errors
|
|
281
|
+
assert result.records[0].prior is not None
|
|
282
|
+
assert result.records[0].prior.path == "./prompts/template.txt"
|
|
283
|
+
assert result.records[0].prior.start_line == 1
|
|
284
|
+
assert result.records[0].prior.end_line == 20
|
|
285
|
+
|
|
286
|
+
def test_compact_record_with_line_range(self):
|
|
287
|
+
"""Test compact record with line range in @source."""
|
|
288
|
+
text = "@uri local:item-001\n@source ./file.txt:100-150 <<< feedback\n"
|
|
289
|
+
result = lint_string(text, check_sources=False, check_canonical=False)
|
|
290
|
+
|
|
291
|
+
assert not result.has_errors
|
|
292
|
+
assert result.records[0].source is not None
|
|
293
|
+
assert result.records[0].source.path == "./file.txt"
|
|
294
|
+
assert result.records[0].source.start_line == 100
|
|
295
|
+
assert result.records[0].source.end_line == 150
|
|
296
|
+
|
|
297
|
+
def test_uri_with_line_range(self):
|
|
298
|
+
"""Test URI source with line range."""
|
|
299
|
+
text = "@source https://example.com/file.txt:100-150 <<< good\n"
|
|
300
|
+
result = lint_string(text, check_sources=False, check_canonical=False)
|
|
301
|
+
|
|
302
|
+
assert not result.has_errors
|
|
303
|
+
assert result.records[0].source is not None
|
|
304
|
+
assert result.records[0].source.path == "https://example.com/file.txt"
|
|
305
|
+
assert result.records[0].source.start_line == 100
|
|
306
|
+
assert result.records[0].source.end_line == 150
|
|
307
|
+
assert result.records[0].source.is_uri
|
|
308
|
+
|
|
309
|
+
def test_invalid_line_range_end_less_than_start(self):
|
|
310
|
+
"""Test E011: Invalid line range (end < start)."""
|
|
311
|
+
text = "@source ./code.py:50-10 <<< good\n"
|
|
312
|
+
result = lint_string(text, check_sources=False, check_canonical=False)
|
|
313
|
+
|
|
314
|
+
assert result.has_errors
|
|
315
|
+
errors = [d for d in result.diagnostics if d.code == ErrorCode.E011]
|
|
316
|
+
assert len(errors) == 1
|
|
317
|
+
assert "end line 10 is less than start line 50" in errors[0].message
|
|
318
|
+
|
|
319
|
+
def test_source_without_line_range(self):
|
|
320
|
+
"""Test @source without line range still works."""
|
|
321
|
+
text = "@source ./code.py <<< good\n"
|
|
322
|
+
result = lint_string(text, check_sources=False, check_canonical=False)
|
|
323
|
+
|
|
324
|
+
assert not result.has_errors
|
|
325
|
+
assert result.records[0].source is not None
|
|
326
|
+
assert result.records[0].source.path == "./code.py"
|
|
327
|
+
assert result.records[0].source.start_line is None
|
|
328
|
+
assert result.records[0].source.end_line is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|