markback 0.1.2__tar.gz → 0.1.3__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 → markback-0.1.3}/.claude/settings.local.json +5 -1
- {markback-0.1.2 → markback-0.1.3}/PKG-INFO +1 -1
- {markback-0.1.2 → markback-0.1.3}/SPEC.md +85 -20
- {markback-0.1.2 → markback-0.1.3}/markback/linter.py +32 -7
- {markback-0.1.2 → markback-0.1.3}/markback/parser.py +6 -2
- {markback-0.1.2 → markback-0.1.3}/markback/types.py +34 -9
- {markback-0.1.2 → markback-0.1.3}/markback/writer.py +10 -3
- {markback-0.1.2 → markback-0.1.3}/packages/markbackjs/package.json +1 -1
- {markback-0.1.2 → markback-0.1.3}/packages/markbackjs/src/linter.ts +31 -6
- {markback-0.1.2 → markback-0.1.3}/packages/markbackjs/src/parser.ts +6 -2
- {markback-0.1.2 → markback-0.1.3}/packages/markbackjs/src/types.ts +42 -7
- {markback-0.1.2 → markback-0.1.3}/packages/markbackjs/src/writer.ts +6 -0
- {markback-0.1.2 → markback-0.1.3}/packages/markbackjs/test/linter.test.js +104 -0
- {markback-0.1.2 → markback-0.1.3}/pyproject.toml +1 -1
- markback-0.1.3/scripts/publish-npm.sh +11 -0
- markback-0.1.3/scripts/publish-pypi.sh +14 -0
- markback-0.1.3/scripts/publish.sh +16 -0
- {markback-0.1.2 → markback-0.1.3}/tests/test_linter.py +127 -0
- {markback-0.1.2 → markback-0.1.3}/.gitignore +0 -0
- {markback-0.1.2 → markback-0.1.3}/.ishipped/card.md +0 -0
- {markback-0.1.2 → markback-0.1.3}/IMPLEMENTATION_NOTES.md +0 -0
- {markback-0.1.2 → markback-0.1.3}/LICENSE +0 -0
- {markback-0.1.2 → markback-0.1.3}/README.md +0 -0
- {markback-0.1.2 → markback-0.1.3}/markback/__init__.py +0 -0
- {markback-0.1.2 → markback-0.1.3}/markback/cli.py +0 -0
- {markback-0.1.2 → markback-0.1.3}/markback/config.py +0 -0
- {markback-0.1.2 → markback-0.1.3}/markback/llm.py +0 -0
- {markback-0.1.2 → markback-0.1.3}/markback/workflow.py +0 -0
- {markback-0.1.2 → markback-0.1.3}/packages/markbackjs/LICENSE +0 -0
- {markback-0.1.2 → markback-0.1.3}/packages/markbackjs/README.md +0 -0
- {markback-0.1.2 → markback-0.1.3}/packages/markbackjs/package-lock.json +0 -0
- {markback-0.1.2 → markback-0.1.3}/packages/markbackjs/src/index.ts +0 -0
- {markback-0.1.2 → markback-0.1.3}/packages/markbackjs/tsconfig.json +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/__init__.py +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/fixtures/compact_source.mb +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/fixtures/errors/content_with_source.mb +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/fixtures/errors/empty_feedback.mb +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/fixtures/errors/malformed_uri.mb +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/fixtures/errors/missing_feedback.mb +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/fixtures/errors/multiple_feedback.mb +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/fixtures/essay.label.txt +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/fixtures/essay.txt +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/fixtures/external_source.mb +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/fixtures/freeform_feedback.mb +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/fixtures/json_feedback.mb +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/fixtures/label_list.mb +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/fixtures/minimal.mb +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/fixtures/multi_record.mb +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/fixtures/with_uri.mb +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/test_cli.py +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/test_config.py +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/test_parser.py +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/test_types.py +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/test_workflow.py +0 -0
- {markback-0.1.2 → markback-0.1.3}/tests/test_writer.py +0 -0
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
"Bash(npm run build:*)",
|
|
8
8
|
"Bash(echo:*)",
|
|
9
9
|
"Bash(python -m markback lint:*)",
|
|
10
|
-
"Bash(python:*)"
|
|
10
|
+
"Bash(python:*)",
|
|
11
|
+
"Bash(python3 -m pytest:*)",
|
|
12
|
+
"Bash(pip3 install:*)",
|
|
13
|
+
"Bash(.venv/bin/python -m pytest:*)",
|
|
14
|
+
"Bash(chmod:*)"
|
|
11
15
|
]
|
|
12
16
|
}
|
|
13
17
|
}
|
|
@@ -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
|
|
@@ -27,6 +27,7 @@ A MarkBack **record** is the fundamental unit. Every record has:
|
|
|
27
27
|
| `content` | Yes* | The content being labeled (inline or referenced) |
|
|
28
28
|
| `feedback` | Yes | Text after the `<<<` delimiter (always one line) |
|
|
29
29
|
| `uri` | No | Unique identifier for the item |
|
|
30
|
+
| `by` | No | Freeform identifier for who provided the feedback |
|
|
30
31
|
| `source` | No | Reference to external content (when content is not inline) |
|
|
31
32
|
| `prior` | No | Reference to an item that precedes the source (e.g., a prompt that generated the content) |
|
|
32
33
|
|
|
@@ -66,6 +67,7 @@ Header lines appear at the start of a record and begin with `@`. They define met
|
|
|
66
67
|
|
|
67
68
|
```
|
|
68
69
|
@uri <uri-value>
|
|
70
|
+
@by <freeform-text>
|
|
69
71
|
@source <path-or-uri>
|
|
70
72
|
@prior <path-or-uri>
|
|
71
73
|
```
|
|
@@ -95,7 +97,23 @@ Defines the unique identifier for this record.
|
|
|
95
97
|
|
|
96
98
|
**Validation:** URI MUST be valid per RFC 3986. Parsers MUST reject malformed URIs as errors.
|
|
97
99
|
|
|
98
|
-
#### 3.1.2 `@
|
|
100
|
+
#### 3.1.2 `@by` Header
|
|
101
|
+
|
|
102
|
+
Identifies who provided the feedback. The value is freeform text.
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
@by dan@example.com
|
|
106
|
+
@by Dan Driscoll
|
|
107
|
+
@by reviewer-42
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Rules:**
|
|
111
|
+
- Value is freeform text extending to end of line (trailing whitespace trimmed)
|
|
112
|
+
- Can contain any characters including spaces, special characters, etc.
|
|
113
|
+
- Commonly used for email addresses, usernames, or full names
|
|
114
|
+
- Optional - records without `@by` are valid
|
|
115
|
+
|
|
116
|
+
#### 3.1.3 `@source` Header
|
|
99
117
|
|
|
100
118
|
References external content instead of inline content.
|
|
101
119
|
|
|
@@ -111,7 +129,7 @@ References external content instead of inline content.
|
|
|
111
129
|
- When `@source` is present, inline content MUST be empty (or contain only whitespace)
|
|
112
130
|
- Parsers MUST verify referenced files exist (warning if missing)
|
|
113
131
|
|
|
114
|
-
#### 3.1.
|
|
132
|
+
#### 3.1.4 `@prior` Header
|
|
115
133
|
|
|
116
134
|
References an item that precedes the source material. For example, if the source is an image generated by an LLM, the prior could be the prompt that was used to create it.
|
|
117
135
|
|
|
@@ -127,26 +145,36 @@ References an item that precedes the source material. For example, if the source
|
|
|
127
145
|
- `@prior` does not affect content handling (inline content or `@source` rules still apply)
|
|
128
146
|
- Parsers SHOULD verify referenced files exist (warning if missing)
|
|
129
147
|
|
|
130
|
-
#### 3.1.
|
|
148
|
+
#### 3.1.5 Line and Character Range Specification
|
|
131
149
|
|
|
132
|
-
Both `@source` and `@prior` headers support optional line range specifications using colon notation. This allows referencing specific
|
|
150
|
+
Both `@source` and `@prior` headers support optional line and character range specifications using colon notation. This allows referencing specific positions within a file.
|
|
133
151
|
|
|
134
|
-
**Syntax:**
|
|
152
|
+
**Syntax:**
|
|
153
|
+
- Line only: `<path-or-uri>:<line>` or `<path-or-uri>:<start-line>-<end-line>`
|
|
154
|
+
- With columns: `<path-or-uri>:<line>:<col>` or `<path-or-uri>:<start-line>:<start-col>-<end-line>:<end-col>`
|
|
135
155
|
|
|
136
156
|
```
|
|
137
157
|
@source ./code.py:42
|
|
138
158
|
@source ./code.py:42-50
|
|
159
|
+
@source ./code.py:42:10
|
|
160
|
+
@source ./code.py:42:10-42:25
|
|
161
|
+
@source ./code.py:10:5-15:20
|
|
139
162
|
@prior ./prompts/template.txt:1-20
|
|
140
163
|
@source https://example.com/file.txt:100-150
|
|
141
164
|
```
|
|
142
165
|
|
|
143
166
|
**Rules:**
|
|
144
|
-
- Line numbers are 1-indexed (first line is
|
|
167
|
+
- Line and column numbers are 1-indexed (first line/column is 1)
|
|
145
168
|
- Single line: `:N` references line N only
|
|
146
169
|
- Line range: `:N-M` references lines N through M (inclusive)
|
|
147
|
-
-
|
|
148
|
-
-
|
|
170
|
+
- Single position: `:N:C` references line N, column C
|
|
171
|
+
- Character range: `:N:C-M:D` references from line N column C to line M column D (inclusive)
|
|
172
|
+
- End position must be greater than or equal to start position (E011 error otherwise)
|
|
173
|
+
- If on same line: end column must be >= start column
|
|
174
|
+
- If on different lines: end line must be >= start line
|
|
175
|
+
- Ranges are informational metadata; parsers do not validate that referenced positions exist in the file
|
|
149
176
|
- Windows drive letters (e.g., `C:\path`) are not confused with line ranges because scheme detection requires length > 1
|
|
177
|
+
- Column specification is optional; you can specify `:10:5-20` (start with column, end without)
|
|
150
178
|
|
|
151
179
|
### 3.2 Content Block
|
|
152
180
|
|
|
@@ -481,7 +509,7 @@ Canonical form ensures consistent output for comparison and version control.
|
|
|
481
509
|
### 5.2 Canonicalization Rules
|
|
482
510
|
|
|
483
511
|
1. **Line endings:** Normalize to `\n` (LF)
|
|
484
|
-
2. **Header order:** `@uri` before `@prior` before `@source` before unknown headers (alphabetical)
|
|
512
|
+
2. **Header order:** `@uri` before `@by` before `@prior` before `@source` before unknown headers (alphabetical)
|
|
485
513
|
3. **Header spacing:** Exactly one space after keyword
|
|
486
514
|
4. **Trailing whitespace:** Remove from all lines
|
|
487
515
|
5. **Content whitespace:** Preserve internal whitespace; trim leading/trailing blank lines
|
|
@@ -678,7 +706,42 @@ Spring whispers goodbye.
|
|
|
678
706
|
<<< creative; follows haiku structure; quality=excellent
|
|
679
707
|
```
|
|
680
708
|
|
|
681
|
-
### 8.5
|
|
709
|
+
### 8.5 Record with Attribution
|
|
710
|
+
|
|
711
|
+
```
|
|
712
|
+
@uri local:review-001
|
|
713
|
+
@by dan@example.com
|
|
714
|
+
|
|
715
|
+
This code needs better error handling.
|
|
716
|
+
<<< actionable; priority=high
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
Or with a full name:
|
|
720
|
+
```
|
|
721
|
+
@uri local:review-002
|
|
722
|
+
@by Dan Driscoll
|
|
723
|
+
@source ./src/app.py
|
|
724
|
+
<<< approved; good code quality
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### 8.6 Character-Level References
|
|
728
|
+
|
|
729
|
+
Reference a specific position in a file:
|
|
730
|
+
```
|
|
731
|
+
@source ./code.py:42:10 <<< potential bug at this position
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
Reference a character range on a single line:
|
|
735
|
+
```
|
|
736
|
+
@source ./code.py:42:10-42:25 <<< consider renaming this variable
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
Reference a multi-line character range:
|
|
740
|
+
```
|
|
741
|
+
@source ./code.py:10:5-15:20 <<< this function needs refactoring
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
### 8.7 Single-File Example
|
|
682
745
|
|
|
683
746
|
**File:** `question.mb`
|
|
684
747
|
```
|
|
@@ -688,7 +751,7 @@ Explain quantum entanglement in simple terms.
|
|
|
688
751
|
<<< quality=excellent; accuracy=high; clarity=good
|
|
689
752
|
```
|
|
690
753
|
|
|
691
|
-
### 8.
|
|
754
|
+
### 8.8 Label List Example (Compact Format)
|
|
692
755
|
|
|
693
756
|
**File:** `image-annotations.mb`
|
|
694
757
|
```
|
|
@@ -720,7 +783,7 @@ Explain quantum entanglement in simple terms.
|
|
|
720
783
|
@source ./batch1/item3.txt <<< positive; excellent clarity
|
|
721
784
|
```
|
|
722
785
|
|
|
723
|
-
### 8.
|
|
786
|
+
### 8.9 Multi-Record Example (Mixed Freeform and Structured)
|
|
724
787
|
|
|
725
788
|
**File:** `training-data.mb`
|
|
726
789
|
```
|
|
@@ -751,7 +814,7 @@ Please write a formal letter requesting a meeting.
|
|
|
751
814
|
@source ./audio/sample-005.wav <<< transcription="Hello world"; quality=clear; language=en
|
|
752
815
|
```
|
|
753
816
|
|
|
754
|
-
### 8.
|
|
817
|
+
### 8.10 Paired-File Example
|
|
755
818
|
|
|
756
819
|
**Content file:** `essay.txt`
|
|
757
820
|
```
|
|
@@ -767,7 +830,7 @@ agriculture, manufacturing, mining, and transport.
|
|
|
767
830
|
<<< good; grade=B+; well structured but needs more specific examples
|
|
768
831
|
```
|
|
769
832
|
|
|
770
|
-
### 8.
|
|
833
|
+
### 8.11 Freeform Feedback Examples
|
|
771
834
|
|
|
772
835
|
Various styles of freeform feedback:
|
|
773
836
|
|
|
@@ -790,7 +853,7 @@ Explain machine learning to a child.
|
|
|
790
853
|
<<< needs work; the explanation assumes too much prior knowledge
|
|
791
854
|
```
|
|
792
855
|
|
|
793
|
-
### 8.
|
|
856
|
+
### 8.12 Complex Structured Feedback (JSON)
|
|
794
857
|
|
|
795
858
|
```
|
|
796
859
|
@uri local:complex-example
|
|
@@ -799,7 +862,7 @@ Multi-attribute content with special characters.
|
|
|
799
862
|
<<< json:{"rating":4.5,"tags":["important","review"],"notes":"Contains \"quoted\" text and; semicolons","scores":{"accuracy":0.9,"relevance":0.85}}
|
|
800
863
|
```
|
|
801
864
|
|
|
802
|
-
### 8.
|
|
865
|
+
### 8.13 Image with MarkBack Sidecar
|
|
803
866
|
|
|
804
867
|
**Content file:** `diagram.png` (binary)
|
|
805
868
|
|
|
@@ -882,13 +945,15 @@ feedback = "<<<" SP feedback-content LF
|
|
|
882
945
|
feedback-content = *VCHAR ; no LF allowed
|
|
883
946
|
|
|
884
947
|
; Compact record (single line, external source only)
|
|
885
|
-
compact-record = [uri-line] source-feedback-line
|
|
948
|
+
compact-record = [uri-line] [by-line] [prior-line] source-feedback-line
|
|
886
949
|
compact-list = compact-record *(1*blank-line compact-record)
|
|
887
950
|
uri-line = "@uri" SP value LF
|
|
951
|
+
by-line = "@by" SP value LF
|
|
952
|
+
prior-line = "@prior" SP path-with-range LF
|
|
888
953
|
source-feedback-line = "@source" SP path-with-range SP "<<<" SP feedback-content LF
|
|
889
|
-
path-with-range = path [
|
|
890
|
-
path = 1*VCHAR ; ends at space before <<< or
|
|
891
|
-
|
|
954
|
+
path-with-range = path [position-range] ; path with optional position range
|
|
955
|
+
path = 1*VCHAR ; ends at space before <<< or position-range
|
|
956
|
+
position-range = ":" 1*DIGIT [":" 1*DIGIT] ["-" 1*DIGIT [":" 1*DIGIT]]
|
|
892
957
|
|
|
893
958
|
LOWER = %x61-7A ; a-z
|
|
894
959
|
SP = %x20 ; space
|
|
@@ -137,36 +137,61 @@ 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
|
+
|
|
140
163
|
def lint_line_range(
|
|
141
164
|
record: Record,
|
|
142
165
|
record_idx: int,
|
|
143
166
|
) -> list[Diagnostic]:
|
|
144
|
-
"""Check if line ranges are valid (end >= start)."""
|
|
167
|
+
"""Check if line/character ranges are valid (end position >= start position)."""
|
|
145
168
|
diagnostics: list[Diagnostic] = []
|
|
146
169
|
|
|
147
|
-
# Check @source
|
|
170
|
+
# Check @source range
|
|
148
171
|
if record.source and record.source.start_line is not None:
|
|
149
|
-
|
|
172
|
+
is_invalid, error_msg = _is_position_invalid(record.source)
|
|
173
|
+
if is_invalid:
|
|
150
174
|
diagnostics.append(Diagnostic(
|
|
151
175
|
file=record._source_file,
|
|
152
176
|
line=record._start_line,
|
|
153
177
|
column=None,
|
|
154
178
|
severity=Severity.ERROR,
|
|
155
179
|
code=ErrorCode.E011,
|
|
156
|
-
message=f"Invalid
|
|
180
|
+
message=f"Invalid range in @source: {error_msg}",
|
|
157
181
|
record_index=record_idx,
|
|
158
182
|
))
|
|
159
183
|
|
|
160
|
-
# Check @prior
|
|
184
|
+
# Check @prior range
|
|
161
185
|
if record.prior and record.prior.start_line is not None:
|
|
162
|
-
|
|
186
|
+
is_invalid, error_msg = _is_position_invalid(record.prior)
|
|
187
|
+
if is_invalid:
|
|
163
188
|
diagnostics.append(Diagnostic(
|
|
164
189
|
file=record._source_file,
|
|
165
190
|
line=record._start_line,
|
|
166
191
|
column=None,
|
|
167
192
|
severity=Severity.ERROR,
|
|
168
193
|
code=ErrorCode.E011,
|
|
169
|
-
message=f"Invalid
|
|
194
|
+
message=f"Invalid range in @prior: {error_msg}",
|
|
170
195
|
record_index=record_idx,
|
|
171
196
|
))
|
|
172
197
|
|
|
@@ -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,
|
|
@@ -78,8 +78,9 @@ class Diagnostic:
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
# Regex to parse line range from a path
|
|
82
|
-
|
|
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+))?)?$')
|
|
83
84
|
|
|
84
85
|
|
|
85
86
|
@dataclass
|
|
@@ -89,6 +90,8 @@ class SourceRef:
|
|
|
89
90
|
is_uri: bool = False
|
|
90
91
|
start_line: Optional[int] = None
|
|
91
92
|
end_line: Optional[int] = None
|
|
93
|
+
start_column: Optional[int] = None
|
|
94
|
+
end_column: Optional[int] = None
|
|
92
95
|
_path_only: str = ""
|
|
93
96
|
|
|
94
97
|
def __post_init__(self):
|
|
@@ -102,16 +105,21 @@ class SourceRef:
|
|
|
102
105
|
self.is_uri = bool(parsed.scheme) and len(parsed.scheme) > 1
|
|
103
106
|
|
|
104
107
|
def _parse_line_range(self):
|
|
105
|
-
"""Parse optional line range from value."""
|
|
108
|
+
"""Parse optional line/character range from value."""
|
|
106
109
|
match = _LINE_RANGE_PATTERN.match(self.value)
|
|
107
110
|
if match:
|
|
108
111
|
self._path_only = match.group(1)
|
|
109
112
|
self.start_line = int(match.group(2))
|
|
110
113
|
if match.group(3):
|
|
111
|
-
self.
|
|
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))
|
|
112
119
|
else:
|
|
113
|
-
# Single line reference: start and end are the same
|
|
120
|
+
# Single line/position reference: start and end are the same
|
|
114
121
|
self.end_line = self.start_line
|
|
122
|
+
self.end_column = self.start_column
|
|
115
123
|
else:
|
|
116
124
|
self._path_only = self.value
|
|
117
125
|
|
|
@@ -122,12 +130,27 @@ class SourceRef:
|
|
|
122
130
|
|
|
123
131
|
@property
|
|
124
132
|
def line_range_str(self) -> Optional[str]:
|
|
125
|
-
"""Return formatted line range string, or None if no range."""
|
|
133
|
+
"""Return formatted line/character range string, or None if no range."""
|
|
126
134
|
if self.start_line is None:
|
|
127
135
|
return None
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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}"
|
|
131
154
|
|
|
132
155
|
def resolve(self, base_path: Optional[Path] = None) -> Path:
|
|
133
156
|
"""Resolve to a file path (relative paths resolved against base_path)."""
|
|
@@ -162,6 +185,7 @@ class Record:
|
|
|
162
185
|
"""A MarkBack record containing content and feedback."""
|
|
163
186
|
feedback: str
|
|
164
187
|
uri: Optional[str] = None
|
|
188
|
+
by: Optional[str] = None
|
|
165
189
|
source: Optional[SourceRef] = None
|
|
166
190
|
prior: Optional[SourceRef] = None
|
|
167
191
|
content: Optional[str] = None
|
|
@@ -195,6 +219,7 @@ class Record:
|
|
|
195
219
|
"""Convert to JSON-serializable dict."""
|
|
196
220
|
return {
|
|
197
221
|
"uri": self.uri,
|
|
222
|
+
"by": self.by,
|
|
198
223
|
"source": str(self.source) if self.source else None,
|
|
199
224
|
"prior": str(self.prior) if self.prior else None,
|
|
200
225
|
"content": self.content,
|
|
@@ -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
|
|
|
@@ -138,12 +138,36 @@ function lintPriorExists(record: MarkbackRecord, basePath: string | null, record
|
|
|
138
138
|
return diagnostics;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
interface PositionCheck {
|
|
142
|
+
isInvalid: boolean;
|
|
143
|
+
errorMsg: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isPositionInvalid(sourceRef: { startLine: number | null; endLine: number | null; startColumn: number | null; endColumn: number | null }): PositionCheck {
|
|
147
|
+
if (sourceRef.startLine === null || sourceRef.endLine === null) {
|
|
148
|
+
return { isInvalid: false, errorMsg: "" };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (sourceRef.endLine < sourceRef.startLine) {
|
|
152
|
+
return { isInvalid: true, errorMsg: `end line ${sourceRef.endLine} is less than start line ${sourceRef.startLine}` };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (sourceRef.endLine === sourceRef.startLine) {
|
|
156
|
+
if (sourceRef.startColumn !== null && sourceRef.endColumn !== null && sourceRef.endColumn < sourceRef.startColumn) {
|
|
157
|
+
return { isInvalid: true, errorMsg: `end column ${sourceRef.endColumn} is less than start column ${sourceRef.startColumn} on line ${sourceRef.startLine}` };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { isInvalid: false, errorMsg: "" };
|
|
162
|
+
}
|
|
163
|
+
|
|
141
164
|
function lintLineRange(record: MarkbackRecord, recordIdx: number): Diagnostic[] {
|
|
142
165
|
const diagnostics: Diagnostic[] = [];
|
|
143
166
|
|
|
144
|
-
// Check @source
|
|
167
|
+
// Check @source range
|
|
145
168
|
if (record.source && record.source.startLine !== null) {
|
|
146
|
-
|
|
169
|
+
const { isInvalid, errorMsg } = isPositionInvalid(record.source);
|
|
170
|
+
if (isInvalid) {
|
|
147
171
|
diagnostics.push(
|
|
148
172
|
new Diagnostic({
|
|
149
173
|
file: record._sourceFile ?? null,
|
|
@@ -151,16 +175,17 @@ function lintLineRange(record: MarkbackRecord, recordIdx: number): Diagnostic[]
|
|
|
151
175
|
column: null,
|
|
152
176
|
severity: Severity.ERROR,
|
|
153
177
|
code: ErrorCode.E011,
|
|
154
|
-
message: `Invalid
|
|
178
|
+
message: `Invalid range in @source: ${errorMsg}`,
|
|
155
179
|
recordIndex: recordIdx,
|
|
156
180
|
}),
|
|
157
181
|
);
|
|
158
182
|
}
|
|
159
183
|
}
|
|
160
184
|
|
|
161
|
-
// Check @prior
|
|
185
|
+
// Check @prior range
|
|
162
186
|
if (record.prior && record.prior.startLine !== null) {
|
|
163
|
-
|
|
187
|
+
const { isInvalid, errorMsg } = isPositionInvalid(record.prior);
|
|
188
|
+
if (isInvalid) {
|
|
164
189
|
diagnostics.push(
|
|
165
190
|
new Diagnostic({
|
|
166
191
|
file: record._sourceFile ?? null,
|
|
@@ -168,7 +193,7 @@ function lintLineRange(record: MarkbackRecord, recordIdx: number): Diagnostic[]
|
|
|
168
193
|
column: null,
|
|
169
194
|
severity: Severity.ERROR,
|
|
170
195
|
code: ErrorCode.E011,
|
|
171
|
-
message: `Invalid
|
|
196
|
+
message: `Invalid range in @prior: ${errorMsg}`,
|
|
172
197
|
recordIndex: recordIdx,
|
|
173
198
|
}),
|
|
174
199
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Diagnostic, ErrorCode, ParseResult, Record as MarkbackRecord, Severity, SourceRef, WarningCode } from "./types";
|
|
2
2
|
|
|
3
|
-
const KNOWN_HEADERS = new Set(["uri", "source", "prior"]);
|
|
3
|
+
const KNOWN_HEADERS = new Set(["uri", "by", "source", "prior"]);
|
|
4
4
|
|
|
5
5
|
const HEADER_PATTERN = /^@([a-z]+)\s+(.+)$/;
|
|
6
6
|
const FEEDBACK_DELIMITER = "<<<";
|
|
@@ -114,6 +114,7 @@ export function parseString(text: string, sourceFile?: string | null): ParseResu
|
|
|
114
114
|
|
|
115
115
|
const finalizeRecord = (feedback: string, endLine: number, isCompact = false) => {
|
|
116
116
|
const uri = currentHeaders.uri ?? pendingUri;
|
|
117
|
+
const by = currentHeaders.by ?? null;
|
|
117
118
|
const sourceStr = currentHeaders.source;
|
|
118
119
|
const source = sourceStr ? new SourceRef(sourceStr) : null;
|
|
119
120
|
const priorStr = currentHeaders.prior;
|
|
@@ -135,6 +136,7 @@ export function parseString(text: string, sourceFile?: string | null): ParseResu
|
|
|
135
136
|
new MarkbackRecord({
|
|
136
137
|
feedback,
|
|
137
138
|
uri: uri ?? null,
|
|
139
|
+
by,
|
|
138
140
|
source,
|
|
139
141
|
prior,
|
|
140
142
|
content,
|
|
@@ -202,13 +204,15 @@ export function parseString(text: string, sourceFile?: string | null): ParseResu
|
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
const uri = pendingUri ?? currentHeaders.uri ?? null;
|
|
207
|
+
const by = currentHeaders.by ?? null;
|
|
205
208
|
const priorStr = currentHeaders.prior;
|
|
206
209
|
const prior = priorStr ? new SourceRef(priorStr) : null;
|
|
207
210
|
|
|
208
211
|
records.push(
|
|
209
|
-
|
|
212
|
+
new MarkbackRecord({
|
|
210
213
|
feedback: feedback ?? "",
|
|
211
214
|
uri,
|
|
215
|
+
by,
|
|
212
216
|
source,
|
|
213
217
|
prior,
|
|
214
218
|
content: null,
|
|
@@ -100,20 +100,25 @@ function extractScheme(value: string): string | null {
|
|
|
100
100
|
return match ? match[1] : null;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
// Regex to parse line range from a path
|
|
104
|
-
|
|
103
|
+
// Regex to parse line/character range from a path
|
|
104
|
+
// Supports: path:line, path:line:col, path:line-line, path:line:col-line:col
|
|
105
|
+
const LINE_RANGE_PATTERN = /^(.+?):(\d+)(?::(\d+))?(?:-(\d+)(?::(\d+))?)?$/;
|
|
105
106
|
|
|
106
107
|
export class SourceRef {
|
|
107
108
|
value: string;
|
|
108
109
|
isUri: boolean;
|
|
109
110
|
startLine: number | null;
|
|
110
111
|
endLine: number | null;
|
|
112
|
+
startColumn: number | null;
|
|
113
|
+
endColumn: number | null;
|
|
111
114
|
private _pathOnly: string;
|
|
112
115
|
|
|
113
116
|
constructor(value: string, isUri = false) {
|
|
114
117
|
this.value = value;
|
|
115
118
|
this.startLine = null;
|
|
116
119
|
this.endLine = null;
|
|
120
|
+
this.startColumn = null;
|
|
121
|
+
this.endColumn = null;
|
|
117
122
|
this._pathOnly = value;
|
|
118
123
|
|
|
119
124
|
// Parse line range if present
|
|
@@ -135,10 +140,17 @@ export class SourceRef {
|
|
|
135
140
|
this._pathOnly = match[1];
|
|
136
141
|
this.startLine = parseInt(match[2], 10);
|
|
137
142
|
if (match[3]) {
|
|
138
|
-
this.
|
|
143
|
+
this.startColumn = parseInt(match[3], 10);
|
|
144
|
+
}
|
|
145
|
+
if (match[4]) {
|
|
146
|
+
this.endLine = parseInt(match[4], 10);
|
|
147
|
+
if (match[5]) {
|
|
148
|
+
this.endColumn = parseInt(match[5], 10);
|
|
149
|
+
}
|
|
139
150
|
} else {
|
|
140
|
-
// Single line reference: start and end are the same
|
|
151
|
+
// Single line/position reference: start and end are the same
|
|
141
152
|
this.endLine = this.startLine;
|
|
153
|
+
this.endColumn = this.startColumn;
|
|
142
154
|
}
|
|
143
155
|
}
|
|
144
156
|
}
|
|
@@ -151,10 +163,29 @@ export class SourceRef {
|
|
|
151
163
|
if (this.startLine === null) {
|
|
152
164
|
return null;
|
|
153
165
|
}
|
|
154
|
-
|
|
155
|
-
|
|
166
|
+
|
|
167
|
+
// Build start position
|
|
168
|
+
let start: string;
|
|
169
|
+
if (this.startColumn !== null) {
|
|
170
|
+
start = `:${this.startLine}:${this.startColumn}`;
|
|
171
|
+
} else {
|
|
172
|
+
start = `:${this.startLine}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check if end is the same as start (single position)
|
|
176
|
+
if (this.startLine === this.endLine && this.startColumn === this.endColumn) {
|
|
177
|
+
return start;
|
|
156
178
|
}
|
|
157
|
-
|
|
179
|
+
|
|
180
|
+
// Build end position
|
|
181
|
+
let end: string;
|
|
182
|
+
if (this.endColumn !== null) {
|
|
183
|
+
end = `-${this.endLine}:${this.endColumn}`;
|
|
184
|
+
} else {
|
|
185
|
+
end = `-${this.endLine}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return `${start}${end}`;
|
|
158
189
|
}
|
|
159
190
|
|
|
160
191
|
resolve(basePath?: string | null): string {
|
|
@@ -185,6 +216,7 @@ export class SourceRef {
|
|
|
185
216
|
export interface RecordInit {
|
|
186
217
|
feedback: string;
|
|
187
218
|
uri?: string | null;
|
|
219
|
+
by?: string | null;
|
|
188
220
|
source?: SourceRef | null;
|
|
189
221
|
prior?: SourceRef | null;
|
|
190
222
|
content?: string | null;
|
|
@@ -198,6 +230,7 @@ export interface RecordInit {
|
|
|
198
230
|
export class Record {
|
|
199
231
|
feedback: string;
|
|
200
232
|
uri: string | null;
|
|
233
|
+
by: string | null;
|
|
201
234
|
source: SourceRef | null;
|
|
202
235
|
prior: SourceRef | null;
|
|
203
236
|
content: string | null;
|
|
@@ -210,6 +243,7 @@ export class Record {
|
|
|
210
243
|
constructor(init: RecordInit) {
|
|
211
244
|
this.feedback = init.feedback;
|
|
212
245
|
this.uri = init.uri ?? null;
|
|
246
|
+
this.by = init.by ?? null;
|
|
213
247
|
this.source = init.source ?? null;
|
|
214
248
|
this.prior = init.prior ?? null;
|
|
215
249
|
this.content = init.content ?? null;
|
|
@@ -237,6 +271,7 @@ export class Record {
|
|
|
237
271
|
toDict(): UnknownMap {
|
|
238
272
|
return {
|
|
239
273
|
uri: this.uri,
|
|
274
|
+
by: this.by,
|
|
240
275
|
source: this.source ? this.source.toString() : null,
|
|
241
276
|
prior: this.prior ? this.prior.toString() : null,
|
|
242
277
|
content: this.content,
|
|
@@ -20,6 +20,9 @@ export function writeRecordCanonical(record: Record, preferCompact = true): stri
|
|
|
20
20
|
if (record.uri) {
|
|
21
21
|
lines.push(`@uri ${record.uri}`);
|
|
22
22
|
}
|
|
23
|
+
if (record.by) {
|
|
24
|
+
lines.push(`@by ${record.by}`);
|
|
25
|
+
}
|
|
23
26
|
if (record.prior) {
|
|
24
27
|
lines.push(`@prior ${record.prior}`);
|
|
25
28
|
}
|
|
@@ -28,6 +31,9 @@ export function writeRecordCanonical(record: Record, preferCompact = true): stri
|
|
|
28
31
|
if (record.uri) {
|
|
29
32
|
lines.push(`@uri ${record.uri}`);
|
|
30
33
|
}
|
|
34
|
+
if (record.by) {
|
|
35
|
+
lines.push(`@by ${record.by}`);
|
|
36
|
+
}
|
|
31
37
|
if (record.prior) {
|
|
32
38
|
lines.push(`@prior ${record.prior}`);
|
|
33
39
|
}
|
|
@@ -159,3 +159,107 @@ test("lintString: source without line range still works", () => {
|
|
|
159
159
|
assert.equal(result.records[0].source.startLine, null);
|
|
160
160
|
assert.equal(result.records[0].source.endLine, null);
|
|
161
161
|
});
|
|
162
|
+
|
|
163
|
+
// @by header tests
|
|
164
|
+
|
|
165
|
+
test("lintString: @by header basic", () => {
|
|
166
|
+
const text = "@uri local:example\n@by dan@example.com\n\nContent.\n<<< good\n";
|
|
167
|
+
const result = lintString(text, { checkSources: false, checkCanonical: false });
|
|
168
|
+
assert.equal(result.hasErrors, false);
|
|
169
|
+
assert.equal(result.records[0].by, "dan@example.com");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("lintString: @by header with spaces", () => {
|
|
173
|
+
const text = "@uri local:example\n@by Dan Driscoll\n\nContent.\n<<< good\n";
|
|
174
|
+
const result = lintString(text, { checkSources: false, checkCanonical: false });
|
|
175
|
+
assert.equal(result.hasErrors, false);
|
|
176
|
+
assert.equal(result.records[0].by, "Dan Driscoll");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("lintString: @by header with compact record", () => {
|
|
180
|
+
const text = "@uri local:item-001\n@by reviewer@example.com\n@source ./file.txt <<< feedback\n";
|
|
181
|
+
const result = lintString(text, { checkSources: false, checkCanonical: false });
|
|
182
|
+
assert.equal(result.hasErrors, false);
|
|
183
|
+
assert.equal(result.records[0].by, "reviewer@example.com");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("lintString: @by header with @prior", () => {
|
|
187
|
+
const text = "@uri local:gen-001\n@by ai-trainer@example.com\n@prior ./prompts/prompt.txt\n@source ./output.txt\n<<< good\n";
|
|
188
|
+
const result = lintString(text, { checkSources: false, checkCanonical: false });
|
|
189
|
+
assert.equal(result.hasErrors, false);
|
|
190
|
+
assert.equal(result.records[0].by, "ai-trainer@example.com");
|
|
191
|
+
assert.ok(result.records[0].prior !== null);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Character-level referencing tests
|
|
195
|
+
|
|
196
|
+
test("lintString: @source with single position (line:col)", () => {
|
|
197
|
+
const text = "@source ./code.py:42:10 <<< good\n";
|
|
198
|
+
const result = lintString(text, { checkSources: false, checkCanonical: false });
|
|
199
|
+
assert.equal(result.hasErrors, false);
|
|
200
|
+
assert.equal(result.records[0].source.path, "./code.py");
|
|
201
|
+
assert.equal(result.records[0].source.startLine, 42);
|
|
202
|
+
assert.equal(result.records[0].source.startColumn, 10);
|
|
203
|
+
assert.equal(result.records[0].source.endLine, 42);
|
|
204
|
+
assert.equal(result.records[0].source.endColumn, 10);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("lintString: @source with character range same line", () => {
|
|
208
|
+
const text = "@source ./code.py:42:10-42:25 <<< good\n";
|
|
209
|
+
const result = lintString(text, { checkSources: false, checkCanonical: false });
|
|
210
|
+
assert.equal(result.hasErrors, false);
|
|
211
|
+
assert.equal(result.records[0].source.path, "./code.py");
|
|
212
|
+
assert.equal(result.records[0].source.startLine, 42);
|
|
213
|
+
assert.equal(result.records[0].source.startColumn, 10);
|
|
214
|
+
assert.equal(result.records[0].source.endLine, 42);
|
|
215
|
+
assert.equal(result.records[0].source.endColumn, 25);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("lintString: @source with character range multi-line", () => {
|
|
219
|
+
const text = "@source ./code.py:10:5-15:20 <<< good\n";
|
|
220
|
+
const result = lintString(text, { checkSources: false, checkCanonical: false });
|
|
221
|
+
assert.equal(result.hasErrors, false);
|
|
222
|
+
assert.equal(result.records[0].source.path, "./code.py");
|
|
223
|
+
assert.equal(result.records[0].source.startLine, 10);
|
|
224
|
+
assert.equal(result.records[0].source.startColumn, 5);
|
|
225
|
+
assert.equal(result.records[0].source.endLine, 15);
|
|
226
|
+
assert.equal(result.records[0].source.endColumn, 20);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("lintString: @prior with character range", () => {
|
|
230
|
+
const text = "@prior ./prompts/template.txt:1:1-20:50\n@source ./output.txt\n<<< good\n";
|
|
231
|
+
const result = lintString(text, { checkSources: false, checkCanonical: false });
|
|
232
|
+
assert.equal(result.hasErrors, false);
|
|
233
|
+
assert.equal(result.records[0].prior.path, "./prompts/template.txt");
|
|
234
|
+
assert.equal(result.records[0].prior.startLine, 1);
|
|
235
|
+
assert.equal(result.records[0].prior.startColumn, 1);
|
|
236
|
+
assert.equal(result.records[0].prior.endLine, 20);
|
|
237
|
+
assert.equal(result.records[0].prior.endColumn, 50);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("lintString: invalid character range end col < start col", () => {
|
|
241
|
+
const text = "@source ./code.py:42:25-42:10 <<< good\n";
|
|
242
|
+
const result = lintString(text, { checkSources: false, checkCanonical: false });
|
|
243
|
+
assert.equal(result.hasErrors, true);
|
|
244
|
+
assert.equal(findCode(result.diagnostics, ErrorCode.E011).length, 1);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("lintString: line range without columns still works", () => {
|
|
248
|
+
const text = "@source ./code.py:10-20 <<< good\n";
|
|
249
|
+
const result = lintString(text, { checkSources: false, checkCanonical: false });
|
|
250
|
+
assert.equal(result.hasErrors, false);
|
|
251
|
+
assert.equal(result.records[0].source.startLine, 10);
|
|
252
|
+
assert.equal(result.records[0].source.startColumn, null);
|
|
253
|
+
assert.equal(result.records[0].source.endLine, 20);
|
|
254
|
+
assert.equal(result.records[0].source.endColumn, null);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("lintString: mixed column specification", () => {
|
|
258
|
+
const text = "@source ./code.py:10:5-20 <<< good\n";
|
|
259
|
+
const result = lintString(text, { checkSources: false, checkCanonical: false });
|
|
260
|
+
assert.equal(result.hasErrors, false);
|
|
261
|
+
assert.equal(result.records[0].source.startLine, 10);
|
|
262
|
+
assert.equal(result.records[0].source.startColumn, 5);
|
|
263
|
+
assert.equal(result.records[0].source.endLine, 20);
|
|
264
|
+
assert.equal(result.records[0].source.endColumn, null);
|
|
265
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Publish Python package to PyPI
|
|
5
|
+
|
|
6
|
+
cd "$(dirname "$0")/.."
|
|
7
|
+
|
|
8
|
+
echo "Building Python package..."
|
|
9
|
+
python -m build
|
|
10
|
+
|
|
11
|
+
echo "Uploading to PyPI..."
|
|
12
|
+
python -m twine upload dist/*
|
|
13
|
+
|
|
14
|
+
echo "Done! Package published to PyPI."
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Publish both Python and JavaScript packages
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(dirname "$0")"
|
|
7
|
+
|
|
8
|
+
echo "=== Publishing to PyPI ==="
|
|
9
|
+
"$SCRIPT_DIR/publish-pypi.sh"
|
|
10
|
+
|
|
11
|
+
echo ""
|
|
12
|
+
echo "=== Publishing to npm ==="
|
|
13
|
+
"$SCRIPT_DIR/publish-npm.sh"
|
|
14
|
+
|
|
15
|
+
echo ""
|
|
16
|
+
echo "All packages published successfully!"
|
|
@@ -326,3 +326,130 @@ class TestLineRangeSupport:
|
|
|
326
326
|
assert result.records[0].source.path == "./code.py"
|
|
327
327
|
assert result.records[0].source.start_line is None
|
|
328
328
|
assert result.records[0].source.end_line is None
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class TestByHeader:
|
|
332
|
+
"""Tests for @by header support."""
|
|
333
|
+
|
|
334
|
+
def test_by_header_basic(self):
|
|
335
|
+
"""Test basic @by header parsing."""
|
|
336
|
+
text = "@uri local:example\n@by dan@example.com\n\nContent.\n<<< good\n"
|
|
337
|
+
result = lint_string(text, check_sources=False, check_canonical=False)
|
|
338
|
+
|
|
339
|
+
assert not result.has_errors
|
|
340
|
+
assert result.records[0].by == "dan@example.com"
|
|
341
|
+
|
|
342
|
+
def test_by_header_with_spaces(self):
|
|
343
|
+
"""Test @by header with freeform text."""
|
|
344
|
+
text = "@uri local:example\n@by Dan Driscoll\n\nContent.\n<<< good\n"
|
|
345
|
+
result = lint_string(text, check_sources=False, check_canonical=False)
|
|
346
|
+
|
|
347
|
+
assert not result.has_errors
|
|
348
|
+
assert result.records[0].by == "Dan Driscoll"
|
|
349
|
+
|
|
350
|
+
def test_by_header_compact_record(self):
|
|
351
|
+
"""Test @by header with compact record."""
|
|
352
|
+
text = "@uri local:item-001\n@by reviewer@example.com\n@source ./file.txt <<< feedback\n"
|
|
353
|
+
result = lint_string(text, check_sources=False, check_canonical=False)
|
|
354
|
+
|
|
355
|
+
assert not result.has_errors
|
|
356
|
+
assert result.records[0].by == "reviewer@example.com"
|
|
357
|
+
|
|
358
|
+
def test_by_header_with_prior(self):
|
|
359
|
+
"""Test @by header combined with @prior."""
|
|
360
|
+
text = "@uri local:gen-001\n@by ai-trainer@example.com\n@prior ./prompts/prompt.txt\n@source ./output.txt\n<<< good\n"
|
|
361
|
+
result = lint_string(text, check_sources=False, check_canonical=False)
|
|
362
|
+
|
|
363
|
+
assert not result.has_errors
|
|
364
|
+
assert result.records[0].by == "ai-trainer@example.com"
|
|
365
|
+
assert result.records[0].prior is not None
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class TestCharacterLevelReferencing:
|
|
369
|
+
"""Tests for character-level referencing syntax."""
|
|
370
|
+
|
|
371
|
+
def test_source_with_single_position(self):
|
|
372
|
+
"""Test @source with single position (line:col)."""
|
|
373
|
+
text = "@source ./code.py:42:10 <<< good\n"
|
|
374
|
+
result = lint_string(text, check_sources=False, check_canonical=False)
|
|
375
|
+
|
|
376
|
+
assert not result.has_errors
|
|
377
|
+
assert result.records[0].source is not None
|
|
378
|
+
assert result.records[0].source.path == "./code.py"
|
|
379
|
+
assert result.records[0].source.start_line == 42
|
|
380
|
+
assert result.records[0].source.start_column == 10
|
|
381
|
+
assert result.records[0].source.end_line == 42
|
|
382
|
+
assert result.records[0].source.end_column == 10
|
|
383
|
+
|
|
384
|
+
def test_source_with_character_range_same_line(self):
|
|
385
|
+
"""Test @source with character range on same line."""
|
|
386
|
+
text = "@source ./code.py:42:10-42:25 <<< good\n"
|
|
387
|
+
result = lint_string(text, check_sources=False, check_canonical=False)
|
|
388
|
+
|
|
389
|
+
assert not result.has_errors
|
|
390
|
+
assert result.records[0].source is not None
|
|
391
|
+
assert result.records[0].source.path == "./code.py"
|
|
392
|
+
assert result.records[0].source.start_line == 42
|
|
393
|
+
assert result.records[0].source.start_column == 10
|
|
394
|
+
assert result.records[0].source.end_line == 42
|
|
395
|
+
assert result.records[0].source.end_column == 25
|
|
396
|
+
|
|
397
|
+
def test_source_with_character_range_multi_line(self):
|
|
398
|
+
"""Test @source with character range spanning multiple lines."""
|
|
399
|
+
text = "@source ./code.py:10:5-15:20 <<< good\n"
|
|
400
|
+
result = lint_string(text, check_sources=False, check_canonical=False)
|
|
401
|
+
|
|
402
|
+
assert not result.has_errors
|
|
403
|
+
assert result.records[0].source is not None
|
|
404
|
+
assert result.records[0].source.path == "./code.py"
|
|
405
|
+
assert result.records[0].source.start_line == 10
|
|
406
|
+
assert result.records[0].source.start_column == 5
|
|
407
|
+
assert result.records[0].source.end_line == 15
|
|
408
|
+
assert result.records[0].source.end_column == 20
|
|
409
|
+
|
|
410
|
+
def test_prior_with_character_range(self):
|
|
411
|
+
"""Test @prior with character range."""
|
|
412
|
+
text = "@prior ./prompts/template.txt:1:1-20:50\n@source ./output.txt\n<<< good\n"
|
|
413
|
+
result = lint_string(text, check_sources=False, check_canonical=False)
|
|
414
|
+
|
|
415
|
+
assert not result.has_errors
|
|
416
|
+
assert result.records[0].prior is not None
|
|
417
|
+
assert result.records[0].prior.path == "./prompts/template.txt"
|
|
418
|
+
assert result.records[0].prior.start_line == 1
|
|
419
|
+
assert result.records[0].prior.start_column == 1
|
|
420
|
+
assert result.records[0].prior.end_line == 20
|
|
421
|
+
assert result.records[0].prior.end_column == 50
|
|
422
|
+
|
|
423
|
+
def test_invalid_character_range_end_column_less_than_start(self):
|
|
424
|
+
"""Test E011: Invalid character range (end col < start col on same line)."""
|
|
425
|
+
text = "@source ./code.py:42:25-42:10 <<< good\n"
|
|
426
|
+
result = lint_string(text, check_sources=False, check_canonical=False)
|
|
427
|
+
|
|
428
|
+
assert result.has_errors
|
|
429
|
+
errors = [d for d in result.diagnostics if d.code == ErrorCode.E011]
|
|
430
|
+
assert len(errors) == 1
|
|
431
|
+
assert "end column 10 is less than start column 25" in errors[0].message
|
|
432
|
+
|
|
433
|
+
def test_line_range_without_columns_still_works(self):
|
|
434
|
+
"""Test that line ranges without columns still work."""
|
|
435
|
+
text = "@source ./code.py:10-20 <<< good\n"
|
|
436
|
+
result = lint_string(text, check_sources=False, check_canonical=False)
|
|
437
|
+
|
|
438
|
+
assert not result.has_errors
|
|
439
|
+
assert result.records[0].source is not None
|
|
440
|
+
assert result.records[0].source.start_line == 10
|
|
441
|
+
assert result.records[0].source.start_column is None
|
|
442
|
+
assert result.records[0].source.end_line == 20
|
|
443
|
+
assert result.records[0].source.end_column is None
|
|
444
|
+
|
|
445
|
+
def test_mixed_column_specification(self):
|
|
446
|
+
"""Test range with start column but no end column."""
|
|
447
|
+
text = "@source ./code.py:10:5-20 <<< good\n"
|
|
448
|
+
result = lint_string(text, check_sources=False, check_canonical=False)
|
|
449
|
+
|
|
450
|
+
assert not result.has_errors
|
|
451
|
+
assert result.records[0].source is not None
|
|
452
|
+
assert result.records[0].source.start_line == 10
|
|
453
|
+
assert result.records[0].source.start_column == 5
|
|
454
|
+
assert result.records[0].source.end_line == 20
|
|
455
|
+
assert result.records[0].source.end_column 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
|