diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..edb8e9e
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,216 @@
+# AGENTS.md - XQuery Code Style and Build Guide
+
+## Build & Test Commands
+
+### Running Tests
+```bash
+basex -t /path/to/test/file.xqm # Run all tests in a file
+basex -t /path/to/test/dir # Run all tests in directory
+basex -t test/simple.xqm # Run doci unit tests (6 tests)
+basex -t test/smoke.xq # Run smoke tests (no assertions)
+```
+
+### Query Execution
+```bash
+basex query.xq # Execute XQuery file
+basex -V query.xq # Verbose mode with timing info (includes compile/execute times)
+basex -q "query string" # Execute inline query
+basex -Q test/file.xqm # Execute XQuery file (explicit flag, for query files without test functions)
+basex -c "commands" # Execute BaseX commands from string
+basex -C script.bxs # Execute BaseX command script
+basex # Start interactive BaseX shell
+```
+
+### Database Operations
+(Already included in Query Execution section above)
+
+## Environment
+- **XQuery Engine**: BaseX 12.2
+- **XQuery Version**: 3.1+
+- **Module System**: BaseX module system with `module namespace`
+- **Test Framework**: `http://basex.org/modules/unit`
+
+## Code Style Guidelines
+
+### File Naming
+- Module files: `lowercase.xqm` (e.g., `doci.xqm`)
+- Test files: `snake_case.xqm` or `snake_case.xq`
+- Use hyphens for multi-word names (e.g., `pdfbox.xqm`)
+- Resource files: lowercase with hyphens (e.g., `test-data.txt`)
+
+### Namespace Declarations
+```xquery
+module namespace module-name = 'urn:project:namespace';
+```
+- Use reverse domain name notation for consistency
+- Match namespace URI to file path structure
+
+### Records & Types
+- Use `declare record` for structured data
+- Use `declare type` for custom types
+- Record fields use `?field` syntax for access
+- Mark optional fields with `?` after field name
+
+```xquery
+declare record doci:doci(
+ lines as xs:string+,
+ separator? as xs:string
+);
+```
+
+### Function Naming
+- Module functions: prefix with module namespace (e.g., `doci:build`)
+- Helper functions: use camelCase internally
+- Test functions: prefix with `test:` namespace and `test:` prefix
+- Use descriptive names that clarify purpose
+
+### Import/Module Syntax
+```xquery
+import module namespace mod = 'namespace' at "path/to/file.xqm";
+```
+- Use relative paths for local modules
+- Keep imports at top of file, grouped logically
+
+### Module Organization
+- Group related functionality in modules
+- Export public API functions
+- Keep private functions marked with `%private` if needed
+- Document exported functions with `(:~` doc comments
+
+### Line Separators
+- Detect automatically using `doci:separator()`
+- Default: `file:line-separator()`
+- Handle all common separators: `\n`, `\r\n`, `\r`
+
+### Module Function Implementation Notes
+- Use `=>` for function chaining/piping
+- Prefer `let` over `for` for single items
+- Use `if-then-else` expressions
+- Leverage XQuery 3.1+ functions: `tokenize()`, `string-join()`
+- Use `!` for mapping single items
+- Use `*` for mapping sequences: `expr!func()`
+- Prefer `switch() case when` over nested `if-then-else`
+
+### Error Handling
+```xquery
+try {
+ ...
+} catch * {
+ error(xs:QName("mod:code"), "descriptive message")
+}
+```
+- Use `try/catch` for external operations (files, PDFs, network)
+- Define custom error codes as `xs:QName`
+- Validate parameters and ranges before processing
+- Provide meaningful error messages with context
+- Use `#mod:code` for errorQName in `error()` calls
+
+### XQuery Best Practices
+- Use `=>` for function chaining/piping
+- Prefer `let` over `for` for single items
+- Use `if-then-else` expressions
+- Leverage XQuery 3.1+ functions: `tokenize()`, `string-join()`
+- Use `!` for mapping single items
+- Use `*` for mapping sequences: `expr!func()`
+- Prefer `switch() case when` over nested `if-then-else`
+
+### Type Checking
+```xquery
+declare function foo($param as xs:string) as xs:integer { ... }
+```
+- Specify parameter types using `as xs:type`
+- Use `item()`, `item()*` for flexible/untyped sequences
+- Return type declarations are mandatory for exported functions
+- Use `doci:num` for union types (e.g., integer|double)
+
+### Record Field Access
+- Use `?field` syntax for record field access (e.g., `$doci?lines`)
+- Optional fields marked with `?` after field name in record declaration
+
+### Documentation
+```xquery
+(:~
+ * Documentation comment
+ * @param $name Description
+ * @return Description
+ * @see Reference URL
+ :)
+```
+- Use `(:~` for module and function documentation
+- Document parameters, return values, and side effects
+- Include `@see` references to external docs when relevant
+
+## Test Structure
+
+### Test Module Pattern
+```xquery
+module namespace test = 'test:module-name';
+import module namespace mod = 'namespace' at "../src/file.xqm";
+
+declare %unit:test function test:name() {
+ let $result := mod:function(...)
+ return unit:assert-equals(expected, $result)
+};
+```
+
+### Test Assertions
+```xquery
+unit:assert-equals(expected, actual)
+unit:assert(condition)
+```
+- Use `unit:assert-equals` for value comparison
+- Use `unit:assert` for boolean conditions
+- Provide meaningful test names: `test:functionality`
+
+### Running Single Tests
+```bash
+basex -t test/file.xqm # Runs all tests in the file
+# To run a single test, comment out others or use test: namespaces
+```
+
+### Test Helper Functions
+```xquery
+declare function test:read($path as xs:string) as xs:string {
+ file:resolve-path($path, file:base-dir()) => file:read-text()
+};
+```
+
+### Test Data
+- Place test resources in `test/resources/`
+- Use descriptive filenames (e.g., `sample.txt`, `empty.txt`)
+- Use `test:read("resources/filename.txt")` helper
+
+### Test Organization
+- Group tests by functionality
+- Test edge cases: empty input, null values, boundaries
+- Test error cases where applicable
+- Keep tests independent and order-independent
+
+## Commit Standards
+
+### Commit Message Format
+```
+verb: description
+```
+
+### Examples
+```bash
+fix: handle empty input
+add: support多元 line separators
+refactor: improve error handling
+test: add edge case for range updates
+docs: update module documentation
+```
+
+### Commit Guidelines
+- Add test cases for bug fixes
+- Update documentation for public API changes
+- Keep commits focused on single changes
+- Verify tests pass before committing
+
+## Notes
+
+- BaseX 12.2 supports XQuery 3.1 syntax
+- The `fn:do-until` function may not be available; use `hof:until` as alternative
+- Java interoperability available through `Q{namespace}` syntax (e.g., `Q{java:java.io.File}new()`)
+- For PDF processing (`pdfbox` module), external JAR dependencies required
diff --git a/session-ses_30cd.md b/session-ses_30cd.md
new file mode 100644
index 0000000..a9b6975
--- /dev/null
+++ b/session-ses_30cd.md
@@ -0,0 +1,2028 @@
+# Codebase analysis & AGENTS.md creation
+
+**Session ID:** ses_30cdc352affecd6zCactnVYthw
+**Created:** 3/15/2026, 8:15:35 PM
+**Updated:** 3/15/2026, 8:28:52 PM
+
+---
+
+## User
+
+Please analyze this codebase and create an AGENTS.md file containing:
+1. Build/lint/test commands - especially for running a single test
+2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
+
+The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 150 lines long.
+If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.
+
+If there's already an AGENTS.md, improve it if it's located in /workspace
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 12.3s)
+
+**Tool: glob**
+
+**Input:**
+```json
+{
+ "pattern": "**/*.xqm"
+}
+```
+
+**Output:**
+```
+/workspace/test/simple.xqm
+/workspace/src/doci.xqm
+```
+
+**Tool: glob**
+
+**Input:**
+```json
+{
+ "pattern": "**/*.xq"
+}
+```
+
+**Output:**
+```
+/workspace/test/smoke.xq
+```
+
+**Tool: glob**
+
+**Input:**
+```json
+{
+ "pattern": "**/*.xquery"
+}
+```
+
+**Output:**
+```
+No files found
+```
+
+**Tool: glob**
+
+**Input:**
+```json
+{
+ "pattern": "**/AGENTS.md"
+}
+```
+
+**Output:**
+```
+/workspace/AGENTS.md
+```
+
+**Tool: glob**
+
+**Input:**
+```json
+{
+ "pattern": ".cursor/rules/*"
+}
+```
+
+**Output:**
+```
+No files found
+```
+
+**Tool: glob**
+
+**Input:**
+```json
+{
+ "pattern": ".cursorrules"
+}
+```
+
+**Output:**
+```
+No files found
+```
+
+**Tool: glob**
+
+**Input:**
+```json
+{
+ "pattern": ".github/copilot-instructions.md"
+}
+```
+
+**Output:**
+```
+No files found
+```
+
+**Tool: glob**
+
+**Input:**
+```json
+{
+ "pattern": "**/package.json"
+}
+```
+
+**Output:**
+```
+No files found
+```
+
+**Tool: glob**
+
+**Input:**
+```json
+{
+ "pattern": "**/build.xml"
+}
+```
+
+**Output:**
+```
+No files found
+```
+
+**Tool: glob**
+
+**Input:**
+```json
+{
+ "pattern": "**/gradle*"
+}
+```
+
+**Output:**
+```
+No files found
+```
+
+**Tool: glob**
+
+**Input:**
+```json
+{
+ "pattern": "**/pom.xml"
+}
+```
+
+**Output:**
+```
+No files found
+```
+
+**Tool: glob**
+
+**Input:**
+```json
+{
+ "pattern": "**/*.md"
+}
+```
+
+**Output:**
+```
+/workspace/AGENTS.md
+/workspace/README.md
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 7.7s)
+
+**Tool: read**
+
+**Input:**
+```json
+{
+ "filePath": "/workspace/AGENTS.md"
+}
+```
+
+**Output:**
+```
+/workspace/AGENTS.md
+file
+1: # AGENTS.md - XQuery Code Style and Build Guide
+2:
+3: ## Build & Test Commands
+4:
+5: ### Running Tests
+6: ```bash
+7: basex -t /path/to/test/file.xqm # Run all tests in a file
+8: basex -t /path/to/test/dir # Run all tests in directory
+9: basex -t test/simple.xqm # Run doci unit tests
+10: basex -t test/smoke.xq # Run smoke tests
+11: ```
+12:
+13: ### Query Execution
+14: ```bash
+15: basex query.xq # Execute XQuery file
+16: basex -V query.xq # Verbose mode with timing info
+17: basex -q "query string" # Execute inline query
+18: basex -Q test/file.xqm # Execute XQuery file (explicit flag)
+19: ```
+20:
+21: ### Database Operations
+22: ```bash
+23: basex -c "commands" # Execute BaseX commands from string
+24: basex -C script.bxs # Execute BaseX command script
+25: basex # Start interactive BaseX shell
+26: ```
+27:
+28: ## Environment
+29: - **XQuery Engine**: BaseX 12.2
+30: - **XQuery Version**: 3.1+
+31: - **Module System**: BaseX module system with `module namespace`
+32: - **Test Framework**: `http://basex.org/modules/unit`
+33:
+34: ## Code Style Guidelines
+35:
+36: ### File Naming
+37: - Module files: `lowercase.xqm` (e.g., `doci.xqm`)
+38: - Test files: `snake_case.xqm` or `snake_case.xq`
+39: - Use hyphens for multi-word names (e.g., `pdfbox.xqm`)
+40: - Resource files: lowercase with hyphens (e.g., `test-data.txt`)
+41:
+42: ### Namespace Declarations
+43: ```xquery
+44: module namespace module-name = 'urn:project:namespace';
+45: ```
+46: - Use reverse domain name notation for consistency
+47: - Match namespace URI to file path structure
+48:
+49: ### Records & Types
+50: - Use `declare record` for structured data
+51: - Use `declare type` for custom types
+52: - Record fields use `?field` syntax for access
+53: - Mark optional fields with `?` after field name
+54:
+55: ```xquery
+56: declare record doci:doci(
+57: lines as xs:string+,
+58: separator? as xs:string
+59: );
+60: ```
+61:
+62: ### Function Naming
+63: - Module functions: prefix with module namespace (e.g., `doci:build`)
+64: - Helper functions: use camelCase internally
+65: - Test functions: prefix with `test:` namespace and `test:` prefix
+66: - Use descriptive names that clarify purpose
+67:
+68: ### Import/Module Syntax
+69: ```xquery
+70: import module namespace mod = 'namespace' at "path/to/file.xqm";
+71: ```
+72: - Use relative paths for local modules
+73: - Keep imports at top of file, grouped logically
+74:
+75: ### Module Organization
+76: - Group related functionality in modules
+77: - Export public API functions
+78: - Keep private functions marked with `%private` if needed
+79: - Document exported functions with `(:~` doc comments
+80:
+81: ### Line Separators
+82: - Detect automatically using `doci:separator()`
+83: - Default: `file:line-separator()`
+84: - Handle all common separators: `\n`, `\r\n`, `\r`
+85:
+86: ### Error Handling
+87: ```xquery
+88: try {
+89: ...
+90: } catch * {
+91: error(xs:QName("mod:code"), "descriptive message")
+92: }
+93: ```
+94: - Use `try/catch` for external operations (files, PDFs, network)
+95: - Define custom error codes as `xs:QName`
+96: - Validate parameters and ranges before processing
+97: - Provide meaningful error messages with context
+98:
+99: ### XQuery Best Practices
+100: - Use `=>` for function chaining/piping
+101: - Prefer `let` over `for` for single items
+102: - Use `if-then-else` expressions
+103: - Leverage XQuery 3.1+ functions: `tokenize()`, `string-join()`
+104: - Use `!` for mapping single items
+105: - Use `*` for mapping sequences: `expr!func()`
+106: - Prefer `switch() case when` over nested `if-then-else`
+107:
+108: ### Type Checking
+109: ```xquery
+110: declare function foo($param as xs:string) as xs:integer { ... }
+111: ```
+112: - Specify parameter types using `as xs:type`
+113: - Use `item()`, `item()*` for flexible/untyped sequences
+114: - Return type declarations are mandatory for exported functions
+115: - Use `doci:num` for union types (e.g., integer|double)
+116:
+117: ### Documentation
+118: ```xquery
+119: (:~
+120: * Documentation comment
+121: * @param $name Description
+122: * @return Description
+123: * @see Reference URL
+124: :)
+125: ```
+126: - Use `(:~` for module and function documentation
+127: - Document parameters, return values, and side effects
+128: - Include `@see` references to external docs when relevant
+129:
+130: ## Test Structure
+131:
+132: ### Test Module Pattern
+133: ```xquery
+134: module namespace test = 'test:module-name';
+135: import module namespace mod = 'namespace' at "../src/file.xqm";
+136:
+137: declare %unit:test function test:name() {
+138: let $result := mod:function(...)
+139: return unit:assert-equals(expected, $result)
+140: };
+141: ```
+142:
+143: ### Test Assertions
+144: ```xquery
+145: unit:assert-equals(expected, actual)
+146: unit:assert(condition)
+147: ```
+148: - Use `unit:assert-equals` for value comparison
+149: - Use `unit:assert` for boolean conditions
+150: - Provide meaningful test names: `test:functionality`
+151:
+152: ### Test Helper Functions
+153: ```xquery
+154: declare function test:read($path as xs:string) as xs:string {
+155: file:resolve-path($path, file:base-dir()) => file:read-text()
+156: };
+157:
+158: declare function test:def-change(...) as doci:TextDocumentContentChangeEvent {
+159: doci:TextDocumentContentChangeEvent(...)
+160: };
+161: ```
+162:
+163: ### Test Data
+164: - Place test resources in `test/resources/`
+165: - Use descriptive filenames (e.g., `sample.txt`, `empty.txt`)
+166: - Use `test:read("resources/filename.txt")` helper
+167:
+168: ### Test Organization
+169: - Group tests by functionality
+170: - Test edge cases: empty input, null values, boundaries
+171: - Test error cases where applicable
+172: - Keep tests independent and order-independent
+173:
+174: ## Commit Standards
+175:
+176: ### Commit Message Format
+177: ```
+178: verb: description
+179: ```
+180:
+181: ### Examples
+182: ```bash
+183: fix: handle empty input
+184: add: support多元 line separators
+185: refactor: improve error handling
+186: test: add edge case for range updates
+187: docs: update module documentation
+188: ```
+189:
+190: ### Commit Guidelines
+191: - Add test cases for bug fixes
+192: - Update documentation for public API changes
+193: - Keep commits focused on single changes
+194: - Verify tests pass before committing
+195:
+196: ## Notes
+197:
+198: - BaseX 12.2 supports XQuery 3.1 syntax
+199: - The `hof:until` function may be needed as alternative to `fn:do-until`
+200: - Java interoperability available through `Q{namespace}` syntax
+201: - PDF processing module (`pdfbox`) requires external dependencies
+
+(End of file - total 201 lines)
+
+```
+
+**Tool: read**
+
+**Input:**
+```json
+{
+ "filePath": "/workspace/README.md"
+}
+```
+
+**Output:**
+```
+/workspace/README.md
+file
+1: Manage a document's content as a sequence of lines.
+
+(End of file - total 1 lines)
+
+```
+
+**Tool: read**
+
+**Input:**
+```json
+{
+ "filePath": "/workspace/src/doci.xqm"
+}
+```
+
+**Output:**
+```
+/workspace/src/doci.xqm
+file
+1: (:~ handle textDocument
+2:
+3: @see https://codemirror.net/docs/ref/#state.Text
+4: @author andy bunce
+5: :)
+6: module namespace doci = 'urn:quodatum:text:doci';
+7: (: default line seperator if none found in text :)
+8: declare variable $doci:default-separator:=file:line-separator();
+9:
+10: declare record doci:doci(
+11: lines as xs:string+,
+12: separator? as xs:string
+13: );
+14:
+15: declare record doci:line(
+16: from as xs:integer, (:number The position of the start of the line. :)
+17: to as xs:integer, (:The position at the end of the line (before the line break,
+18: or at the end of document for the last line).:)
+19: number as xs:integer, (: This line's line number (1-based).:)
+20: text as xs:string (: The line's content. :)
+21: );
+22:
+23: (:~ json numbers :)
+24: declare type doci:num as (xs:integer|xs:double);
+25:
+26: (:~
+27: @param line Line position in a document (zero-based).
+28: @param character Character offset on a line in a document (zero-based).
+29: :)
+30: declare record doci:Position(
+31: line as doci:num,
+32: character as doci:num
+33: );
+34:
+35: (:~
+36: @param line Line position in a document (zero-based).
+37: @param character Character offset on a line in a document (zero-based).
+38: :)
+39: declare record doci:Range(
+40: start as doci:Position,
+41: end as doci:Position
+42: );
+43:
+44: (:
+45: @see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didChange
+46: :)
+47: declare record doci:TextDocumentContentChangeEvent(
+48: text as xs:string,
+49: range? as doci:Range
+50: );
+51:
+52: (: create new doci from string :)
+53: declare function doci:build($text as xs:string)
+54: as doci:doci{
+55: let $ls:=doci:separator($text )
+56: let $lines:= doci:split-lines($text)
+57:
+58: return doci:doci(
+59: lines:= $lines,
+60: separator:=$ls
+61: )
+62: };
+63:
+64: (: return full text :)
+65: declare function doci:text($doci as doci:doci)
+66: as xs:string{
+67: string-join($doci?lines,$doci?separator)
+68: };
+69:
+70: (: number of lines:)
+71: declare function doci:lines($doci as doci:doci)
+72: as xs:integer{
+73: $doci?lines=>count()
+74: };
+75:
+76: (: detect line separator, assumes all the same:)
+77: declare function doci:separator($text as xs:string)
+78: as xs:string?{
+79: switch () {
+80: case contains($text,"
") return "
"
+81: case contains($text,"
") return "
"
+82: case contains($text,"
") return "
"
+83: default return $doci:default-separator
+84: }
+85: };
+86:
+87: (: apply change:)
+88: declare function doci:update($doci as doci:doci,$change as doci:TextDocumentContentChangeEvent)
+89: as doci:doci{
+90: let $lines:= doci:split-lines($change?text)
+91: return if(empty($change?range))
+92: then doci:doci($lines,$doci?separator)
+93: else
+94: let $range:=$change?range
+95: let $sline:= $range?start?line
+96: let $eline:= $range?end?line
+97:
+98: (: string from start line before insert :)
+99: let $head:= substring( $doci?lines[$sline+1] ,1, $range?start?character )
+100: (: string from edit end to end of line :)
+101: let $head:= substring( $doci?lines[$sline+1] ,1, $range?start?character )
+102: let $last := substring( $doci?lines[$eline+1] , $range?end?character +1)
+103:
+104: let $ulines := (
+105: subsequence($doci?lines, 1, $sline) (: lines before edit :)
+106: ,
+107: if($sline eq $eline)
+108: then ($head || $lines[1] || $last)
+109:
+110: else (
+111: $head || $lines[1],
+112: subsequence( $lines,2, count( $lines - 2)),
+113: $lines[last()] ||$last
+114: ),
+115:
+116: subsequence($doci?lines, $eline+2) (: lines after edit :)
+117: )
+118: return doci:doci($ulines,$doci?separator)
+119: };
+120:
+121: declare function doci:split-lines($text as xs:string)
+122: as xs:string+{
+123: if($text eq "")
+124: then $text
+125: else tokenize($text, '(\r\n?|\n\r?)')
+126: };
+127:
+128: (: line from pos :)
+129: declare function doci:lineAt($doci as doci:doci,$pos as xs:integer)
+130: as doci:line {
+131: let $starts:=hof:scan-left($doci?lines, 0,
+132: fn($res,$line){$res+string-length($doci?seperator)+string-length($line)}
+133: )
+134: let $line:=if($pos gt $doci?length)
+135: then error(#doci:range,"pos beyond range")
+136: else do-until(
+137: {"min":1,"max":count($doci?lines)},
+138: fn($r){
+139: let $mid:=round(($r?min+$r?max) div 2,0,"away-from-zero")
+140: return if ($doci?starts[$mid] lt $pos)
+141: then map:put($r,"min",$mid)
+142: else map:put($r,"max",$mid -1)
+143: },
+144: fn($r){$r?max eq $r?min}
+145: )?max
+146:
+147: return doci:line(
+148: number:= $line,
+149: text:= $doci?lines[$line],
+150: from:= $doci?starts[$line],
+151: to:= -1
+152: )
+153: };
+154:
+
+(End of file - total 154 lines)
+
+```
+
+**Tool: read**
+
+**Input:**
+```json
+{
+ "filePath": "/workspace/test/simple.xqm"
+}
+```
+
+**Output:**
+```
+/workspace/test/simple.xqm
+file
+1: (:tests for doci :)
+2: module namespace test = 'test:doci';
+3: import module namespace doci = 'urn:quodatum:text:doci' at "../src/doci.xqm";
+4:
+5:
+6: (:~ we can create record from file :)
+7: declare %unit:test function test:from-file() {
+8: let $doc:=test:read("resources/doc1.txt")
+9: let $doci:= doci:build($doc)
+10: return test:expected($doci,$doc,530)
+11:
+12: };
+13:
+14: (:~ we can create record from string :)
+15: declare %unit:test function test:from-string1() {
+16: let $doc:="a one line string"
+17: let $doci:= doci:build($doc)
+18: return test:expected($doci, $doc, 1)
+19:
+20: };
+21: (:~ we can create record from string :)
+22: declare %unit:test function test:from-string2() {
+23: let $doc:="a two line string
+24: second line"
+25: let $doci:= doci:build($doc)
+26: return test:expected($doci,$doc,2)
+27:
+28: };
+29: (:~ we can create update from string :)
+30: declare %unit:test function test:update-start() {
+31: let $doc:="123456789"
+32: let $doci:= doci:build($doc)
+33: let $_:= test:expected($doci,$doc,1)
+34:
+35: let $change:=test:def-change("*",0,0,0,0)
+36: let $doci2:=doci:update($doci,$change)
+37: let $_:= test:expected($doci2,"*" || $doc,1)
+38:
+39: let $change2:=test:def-change("",0,0,0,1)
+40: let $update:=doci:update($doci2,$change2)=>trace("b")
+41: let $_:= doci:text($update)=>trace("test:update1")
+42: return ()
+43: };
+44:
+45: (:~ we can create update from string :)
+46: declare %unit:test function test:update-end() {
+47: let $doc:="a
+48: 123456789
+49: b"
+50: let $doci:= doci:build($doc)
+51: let $_:=test:expected($doci,$doc,3)
+52: let $change:=test:def-change("*",1,9,1,9)
+53: let $doci2:=doci:update($doci,$change)
+54:
+55: let $change2:=test:def-change("",1,9,1,10)
+56: let $update:=doci:update($doci2,$change2)
+57: return unit:assert-equals($doc,doci:text($update))
+58: };
+59:
+60: (: test $doci properties as expected :)
+61: declare function test:expected($doci as doci:doci, $doc as xs:string, $lines as xs:integer){
+62: unit:assert-equals(doci:lines($doci),$lines),
+63: unit:assert($doci?separator!string-length(.)>0),
+64: unit:assert-equals($doc,doci:text($doci))
+65: };
+66:
+67: (:~ we can create record. :)
+68: declare %unit:test function test:lines() {
+69: test:read("resources/doc1.txt")!doci:build(.)?lines=>count()
+70: };
+71:
+72:
+73: (:~ utils :)
+74: declare function test:read($path as xs:string)
+75: as xs:string{
+76: file:resolve-path($path,file:base-dir())=>file:read-text()
+77: };
+78:
+79: (: create a change description:)
+80: declare function test:def-change($text as xs:string,
+81: $sline as xs:integer,$schar as xs:integer,
+82: $eline as xs:integer,$echar as xs:integer
+83: )
+84: as doci:TextDocumentContentChangeEvent{
+85: doci:TextDocumentContentChangeEvent(
+86: $text,
+87: doci:Range(doci:Position($sline,$schar),doci:Position($eline,$echar))
+88: )
+89: };
+
+(End of file - total 89 lines)
+
+```
+
+**Tool: read**
+
+**Input:**
+```json
+{
+ "filePath": "/workspace/test/smoke.xq"
+}
+```
+
+**Output:**
+```
+/workspace/test/smoke.xq
+file
+1: import module namespace doci = 'urn:quodatum:text:doci' at "../src/doci.xqm";
+2:
+3: declare variable $long:=
+4: file:read-text("C:\Users\mrwhe\git\quodatum\basex-lsp\test\sample.docs\pdfbox.xqm");
+5: declare variable $text:="123456789";
+6:
+7: (: create a change description:)
+8: declare function local:change($text as xs:string,
+9: $sline as xs:integer,$schar as xs:integer,
+10: $eline as xs:integer,$echar as xs:integer
+11: )
+12: as doci:TextDocumentContentChangeEvent{
+13: doci:TextDocumentContentChangeEvent(
+14: $text,
+15: doci:Range(doci:Position($sline,$schar),doci:Position($eline,$echar))
+16: )
+17: };
+18:
+19:
+20:
+21: doci:build($text)
+22: =>doci:update(local:change("*",0,0,0,0))
+
+(End of file - total 22 lines)
+
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 22.5s)
+
+**Tool: glob**
+
+**Input:**
+```json
+{
+ "pattern": "test/resources/**"
+}
+```
+
+**Output:**
+```
+/workspace/test/resources/doc1.txt
+```
+
+**Tool: glob**
+
+**Input:**
+```json
+{
+ "pattern": "**/.cursor/**"
+}
+```
+
+**Output:**
+```
+No files found
+```
+
+**Tool: glob**
+
+**Input:**
+```json
+{
+ "pattern": "**/.cursorrules"
+}
+```
+
+**Output:**
+```
+No files found
+```
+
+**Tool: glob**
+
+**Input:**
+```json
+{
+ "pattern": "**/.github/**"
+}
+```
+
+**Output:**
+```
+No files found
+```
+
+**Tool: glob**
+
+**Input:**
+```json
+{
+ "pattern": "**/*.json"
+}
+```
+
+**Output:**
+```
+No files found
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 6.1s)
+
+**Tool: read**
+
+**Input:**
+```json
+{
+ "filePath": "/workspace/test/resources/doc1.txt"
+}
+```
+
+**Output:**
+```
+/workspace/test/resources/doc1.txt
+file
+1: xquery version '3.1';
+2: (:~
+3: It requires the Pdfbox jars to be on the classpath, or a EXPath package (xar) installation.
+4: Terms
+5: The following terms are used:
+6:
+7:
+8: - bookmark
+9: - A bookmark has a title and a pageindex. It may contain nested bookmarks.
+10: - outline
+11: - The outline is the tree of bookmarks defined in the PDF. It may be empty.
+12: - page range
+13: - A page range defines the page numbering schema in operation from a certain pageIndex until a subsequent range is set.
+14: - page label
+15: - A page label defines
style: Roman, Decimal etc, start: the index to start from (default 1) and prefix: an optional string to prefix to the page label e.g "Vol1:"
+16:
+17:
+18: @note tested with pdfbox-app-3.0.5.jar
+19: @see https://pdfbox.apache.org/download.cgi
+20: @javadoc https://javadoc.io/static/org.apache.pdfbox/pdfbox/3.0.5/
+21: @author Andy Bunce 2025
+22: :)
+23:
+24: module namespace pdfbox="org.expkg_zone58.Pdfbox3";
+25:
+26: declare namespace Loader ="java:org.apache.pdfbox.Loader";
+27: declare namespace PDFTextStripper = "java:org.apache.pdfbox.text.PDFTextStripper";
+28: declare namespace PDDocument ="java:org.apache.pdfbox.pdmodel.PDDocument";
+29: declare namespace PDDocumentCatalog ="java:org.apache.pdfbox.pdmodel.PDDocumentCatalog";
+30: declare namespace PDPageLabels ="java:org.apache.pdfbox.pdmodel.common.PDPageLabels";
+31: declare namespace PDPageLabelRange="java:org.apache.pdfbox.pdmodel.common.PDPageLabelRange";
+32:
+33: declare namespace PageExtractor ="java:org.apache.pdfbox.multipdf.PageExtractor";
+34: declare namespace PDPage ="java:org.apache.pdfbox.pdmodel.PDPage";
+35: declare namespace PDPageTree ="java:org.apache.pdfbox.pdmodel.PDPageTree";
+36: declare namespace PDDocumentOutline ="java:org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline";
+37: declare namespace PDDocumentInformation ="java:org.apache.pdfbox.pdmodel.PDDocumentInformation";
+38: declare namespace PDOutlineItem="java:org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem";
+39: declare namespace PDFRenderer="java:org.apache.pdfbox.rendering.PDFRenderer";
+40: declare namespace PDMetadata="java:org.apache.pdfbox.pdmodel.common.PDMetadata";
+41: declare namespace COSInputStream="java:org.apache.pdfbox.cos.COSInputStream";
+42:
+43: declare namespace RandomAccessReadBuffer="java:org.apache.pdfbox.io.RandomAccessReadBuffer";
+44: declare namespace RandomAccessReadBufferedFile = "java:org.apache.pdfbox.io.RandomAccessReadBufferedFile";
+45: declare namespace PDRectangle="java:org.apache.pdfbox.pdmodel.common.PDRectangle";
+46:
+47: declare namespace File ="java:java.io.File";
+48:
+49: declare namespace rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+50:
+51:
+52: (:~ open $pdf,apply $fn function, close pdf ("With-document" pattern)
+53: creates a local pdfobject and ensures it is closed after use
+54: e.g pdfbox:with-pdf("path...",pdfbox:page-text(?,5))
+55: :)
+56: declare function pdfbox:with-pdf($src as xs:string,
+57: $fn as function(item())as item()*)
+58: as item()*{
+59: let $pdf:=pdfbox:open($src)
+60: return try{
+61: $fn($pdf),pdfbox:close($pdf)
+62: } catch *{
+63: pdfbox:close($pdf),fn:error($err:code,$src || " " || $err:description)
+64: }
+65:
+66: };
+67:
+68:
+69:
+70: (:~ open pdf from file/url/binary, opts may have password , returns pdf object
+71: @param $pdfsrc a fetchable url or filepath, or xs:base64Binary item
+72: @param $opts options options include map {"password":}
+73: @note fetch:binary for https will use a lot of memory here
+74: :)
+75: declare function pdfbox:open($pdfsrc as item(), $opts as map(*))
+76: as item(){
+77: try{
+78:
+79: if($pdfsrc instance of xs:base64Binary)
+80: then Loader:loadPDF( $pdfsrc,string($opts?password))
+81: else if(starts-with($pdfsrc,"http"))
+82: then Loader:loadPDF( fetch:binary($pdfsrc),string($opts?password))
+83: else Loader:loadPDF(RandomAccessReadBufferedFile:new($pdfsrc),string($opts?password))
+84:
+85: } catch *{
+86: let $loc:=if($pdfsrc instance of xs:base64Binary)
+87: then "xs:base64Binary"
+88: else $pdfsrc
+89: return error(xs:QName("pdfbox:open"),"Failed PDF load " || $loc || " " || $err:description)
+90: }
+91: };
+92:
+93: (:~ open pdf from a location, returns pdf object :)
+94: declare function pdfbox:open($pdfsrc as item())
+95: as item(){
+96: pdfbox:open($pdfsrc, map{})
+97: };
+98:
+99:
+100: (:~ The version of the PDF specification used by $pdf e.g "1.4"
+101: returned as string to avoid float rounding issues
+102: :)
+103: declare function pdfbox:specification($pdf as item())
+104: as xs:string{
+105: PDDocument:getVersion($pdf)=>xs:decimal()=>round(4)=>string()
+106: };
+107:
+108: (:~ Save pdf $pdf to filesystem at $savepath , returns $savepath :)
+109: declare function pdfbox:pdf-save($pdf as item(),$savepath as xs:string)
+110: as xs:string{
+111: PDDocument:save($pdf, File:new($savepath)),$savepath
+112: };
+113:
+114: (:~ Create binary representation (xs:base64Binary) of $pdf object
+115: @param $pdf pdf object, created by pdfbox:open
+116: @see #pdfbox:open
+117: @see #pdfbox:with-pdf
+118: :)
+119: declare function pdfbox:binary($pdf as item())
+120: as xs:base64Binary{
+121: let $bytes:=Q{java:java.io.ByteArrayOutputStream}new()
+122: let $_:=PDDocument:save($pdf, $bytes)
+123: return Q{java:java.io.ByteArrayOutputStream}toByteArray($bytes)
+124: =>convert:integers-to-base64()
+125: };
+126:
+127: (:~ Release any resources related to $pdf:)
+128: declare function pdfbox:close($pdf as item())
+129: as empty-sequence(){
+130: (# db:wrapjava void #) {
+131: PDDocument:close($pdf)
+132: }
+133: };
+134:
+135: (:~ Number of pages in PDF:)
+136: declare function pdfbox:number-of-pages($pdf as item())
+137: as xs:integer{
+138: PDDocument:getNumberOfPages($pdf)
+139: };
+140:
+141: (:~ Pdf page as image (zero is cover)
+142: options.format="bmp jpg png gif" etc, options.scale= 1 is 72 dpi?? :)
+143: declare function pdfbox:page-render($pdf as item(),$pageNo as xs:integer,$options as map(*))
+144: as xs:base64Binary{
+145: let $options := map:merge(($options,map{"format":"jpg","scale":1}))
+146: let $bufferedImage := PDFRenderer:new($pdf)
+147: =>PDFRenderer:renderImage($pageNo,$options?scale)
+148: let $bytes := Q{java:java.io.ByteArrayOutputStream}new()
+149: let $_ := Q{java:javax.imageio.ImageIO}write($bufferedImage ,$options?format, $bytes)
+150: return Q{java:java.io.ByteArrayOutputStream}toByteArray($bytes)
+151: =>convert:integers-to-base64()
+152:
+153: };
+154:
+155:
+156: (:~ Defines a map from property names to evaluation method.
+157: Keys are property names,
+158: values are sequences of functions to get property value starting from a $pdf object.
+159: :)
+160: declare %private variable $pdfbox:property-map:=map{
+161: "#pages": pdfbox:number-of-pages#1,
+162:
+163: "#bookmarks": pdfbox:number-of-bookmarks#1,
+164:
+165: "#labels": pdfbox:number-of-labels#1,
+166:
+167: "specification":pdfbox:specification#1,
+168:
+169: "title": (PDDocument:getDocumentInformation#1,
+170: PDDocumentInformation:getTitle#1) ,
+171:
+172: "author": (PDDocument:getDocumentInformation#1,
+173: PDDocumentInformation:getAuthor#1 ),
+174:
+175: "creator": (PDDocument:getDocumentInformation#1,
+176: PDDocumentInformation:getCreator#1),
+177:
+178: "producer": (PDDocument:getDocumentInformation#1,
+179: PDDocumentInformation:getProducer#1),
+180:
+181: "subject": (PDDocument:getDocumentInformation#1,
+182: PDDocumentInformation:getSubject#1),
+183:
+184: "keywords": (PDDocument:getDocumentInformation#1,
+185: PDDocumentInformation:getKeywords#1),
+186:
+187: "creationDate": (PDDocument:getDocumentInformation#1,
+188: PDDocumentInformation:getCreationDate#1,
+189: pdfbox:gregToISO#1),
+190:
+191: "modificationDate": (PDDocument:getDocumentInformation#1,
+192: PDDocumentInformation:getModificationDate#1,
+193: pdfbox:gregToISO#1),
+194:
+195: "labels": pdfbox:labels-as-string#1
+196: };
+197:
+198: (:~ Defined property names, sorted :)
+199: declare function pdfbox:property-names()
+200: as xs:string*{
+201: $pdfbox:property-map=>map:keys()=>sort()
+202: };
+203:
+204: (:~ Return the value of $property for $pdf :)
+205: declare function pdfbox:property($pdf as item(),$property as xs:string)
+206: as item()*{
+207: let $fns:= $pdfbox:property-map($property)
+208: return if(exists($fns))
+209: then fold-left($fns,
+210: $pdf,
+211: function($result,$this as function(*)){$result!$this(.)})
+212: else error(xs:QName('pdfbox:property'),concat("Property '",$property,"' not defined."))
+213: };
+214:
+215:
+216: (:~ summary CSV style info for named $properties for PDFs in $pdfpaths
+217: @see https://docs.basex.org/main/CSV_Functions#xquery
+218: :)
+219: declare function pdfbox:report($pdfpaths as item()*, $properties as xs:string*)
+220: as map(*){
+221: map{"names": array{"path",$properties},
+222:
+223: "records": for $path in $pdfpaths
+224: let $name:=if($path instance of xs:base64Binary) then "binary" else $path
+225: return try{
+226: let $pdf:=pdfbox:open($path)
+227: return (fold-left($properties,
+228: array{$name},
+229: function($result as array(*),$prop as xs:string){
+230: array:append($result, string(pdfbox:property($pdf, $prop)))}
+231: ), pdfbox:close($pdf)
+232: )
+233: } catch *{
+234: fold-left($properties,
+235: array{$name},
+236: function($result as array(*),$prop as xs:string){
+237: array:append($result, "#ERROR")}
+238: )
+239: }
+240:
+241: }
+242: };
+243:
+244: (:~ summary CSV style info for all properties for $pdfpaths
+245: :)
+246: declare function pdfbox:report($pdfpaths as xs:string*)
+247: as map(*){
+248: pdfbox:report($pdfpaths,pdfbox:property-names())
+249: };
+250:
+251: (:~ Convenience function to save report() data to file :)
+252: declare function pdfbox:report-save($data as map(*),$dest as xs:string)
+253: as empty-sequence(){
+254: let $opts := map { "format":"xquery", "header":"yes", "separator" : "," }
+255: return file:write-text($dest,csv:serialize($data,$opts))
+256: };
+257:
+258: (:~ The number of outline items defined in $pdf :)
+259: declare function pdfbox:number-of-bookmarks($pdf as item())
+260: as xs:integer{
+261: let $xml:=pdfbox:outline-xml($pdf)
+262: return count($xml//bookmark)
+263: };
+264:
+265: (:~ XMP metadata as "RDF" document
+266: @note usually rdf:RDF root, but sometimes x:xmpmeta
+267: :)
+268: declare function pdfbox:metadata($pdf as item())
+269: as document-node(element(*))?
+270: {
+271: let $m:=PDDocument:getDocumentCatalog($pdf)
+272: =>PDDocumentCatalog:getMetadata()
+273: return if(exists($m))
+274: then
+275: let $is:=PDMetadata:exportXMPMetadata($m)
+276: return pdfbox:do-until(
+277: map{"n":0,"data":""},
+278:
+279: function($input,$pos ) { pdfbox:read-stream($is,$input?data)},
+280:
+281: function($output,$pos) { $output?n eq -1 }
+282: )?data=>parse-xml()
+283: else ()
+284: };
+285:
+286: (:~ read next block from XMP stream :)
+287: declare %private function pdfbox:read-stream($is,$read as xs:string)
+288: as map(*){
+289: let $blen:=4096
+290: let $buff:=Q{java:java.util.Arrays}copyOf(array{xs:byte(0)},$blen)
+291: let $n:= COSInputStream:read($is,$buff,xs:int(0),xs:int($blen))
+292: let $data:=convert:integers-to-base64(subsequence($buff,1,$n))=>convert:binary-to-string()
+293: return map{"n":$n, "data": $read || $data}
+294: };
+295:
+296: (:~ Return outline for $pdf as map()* :)
+297: declare function pdfbox:outline($pdf as item())
+298: as map(*)*{
+299: (# db:wrapjava some #) {
+300: let $outline:=
+301: PDDocument:getDocumentCatalog($pdf)
+302: =>PDDocumentCatalog:getDocumentOutline()
+303:
+304: return if(exists($outline))
+305: then pdfbox:outline($pdf,PDOutlineItem:getFirstChild($outline))
+306: else ()
+307: }
+308: };
+309:
+310: (:~ return bookmark info for children of $outlineItem as seq of maps :)
+311: declare %private function pdfbox:outline($pdf as item(),$outlineItem as item()?)
+312: as map(*)*{
+313: let $find as map(*):=pdfbox:outline_($pdf ,$outlineItem)
+314: return map:get($find,"list")
+315: };
+316:
+317: (:~ outline helper. BaseX bug 10.7? error if inlined in outline :)
+318: declare %private function pdfbox:outline_($pdf as item(),$outlineItem as item()?)
+319: as map(*){
+320: pdfbox:do-until(
+321:
+322: map{"list":(),"this":$outlineItem},
+323:
+324: function($input,$pos ) {
+325: let $bookmark:=$input?this
+326: let $bk:=map{
+327: "index": PDOutlineItem:findDestinationPage($bookmark,$pdf)=>pdfbox:find-page($pdf),
+328: "title": (# db:checkstrings #) {PDOutlineItem:getTitle($bookmark)}
+329: }
+330:
+331: let $bk:= if(PDOutlineItem:hasChildren($bookmark))
+332: then let $kids:=pdfbox:outline($pdf,PDOutlineItem:getFirstChild($bookmark))
+333: return map:merge(($bk,map:entry("children",$kids)))
+334: else $bk
+335: return map{
+336: "list": ($input?list, $bk),
+337: "this": PDOutlineItem:getNextSibling($input?this)}
+338: },
+339:
+340: function($output,$pos) { empty($output?this) }
+341: )
+342: };
+343:
+344: (:~ PDF outline in xml format :)
+345: declare function pdfbox:outline-xml($pdf as item())
+346: as element(outline)?{
+347: let $outline:=pdfbox:outline($pdf)
+348: return if(exists($outline))
+349: then {$outline!pdfbox:bookmark-xml(.)}
+350: else ()
+351: };
+352:
+353: (:~ Convert outline map to XML :)
+354: declare %private function pdfbox:bookmark-xml($outline as map(*)*)
+355: as element(bookmark)*
+356: {
+357: $outline!
+358:
+359: {?children!pdfbox:bookmark-xml(.)}
+360:
+361: };
+362:
+363: (:~ pageIndex of $page in $pdf :)
+364: declare function pdfbox:find-page(
+365: $page as item()? (: as java:org.apache.pdfbox.pdmodel.PDPage :),
+366: $pdf as item())
+367: as item()?
+368: {
+369: if(exists($page))
+370: then PDDocument:getDocumentCatalog($pdf)
+371: =>PDDocumentCatalog:getPages()
+372: =>PDPageTree:indexOf($page)
+373: else ()
+374: };
+375:
+376: (:~ Return new PDF doc with pages from $start to $end as xs:base64Binary, (1 based)
+377: @param $start first page to include
+378: @param $end last page to include
+379: :)
+380: declare function pdfbox:extract-range($pdf as item(),
+381: $start as xs:integer,$end as xs:integer)
+382: as xs:base64Binary
+383: {
+384: let $a:=PageExtractor:new($pdf, $start, $end) =>PageExtractor:extract()
+385: return (pdfbox:binary($a),pdfbox:close($a))
+386: };
+387:
+388: (:~ The number of labels defined in PDF :)
+389: declare function pdfbox:number-of-labels($pdf as item())
+390: as xs:integer
+391: {
+392: let $labels:=PDDocument:getDocumentCatalog($pdf)
+393: =>PDDocumentCatalog:getPageLabels()
+394: return if(exists($labels))
+395: then PDPageLabels:getPageRangeCount($labels)
+396: else 0
+397: };
+398:
+399: (:~ pageLabel for every page from derived from page-ranges
+400: The returned sequence will contain at MOST as much entries as the document has pages.
+401: @see https://www.w3.org/TR/WCAG20-TECHS/PDF17.html#PDF17-examples
+402: @see https://codereview.stackexchange.com/questions/286078/java-code-showing-page-labels-from-pdf-files
+403: :)
+404: declare function pdfbox:labels-by-page($pdf as item())
+405: as xs:string*
+406: {
+407: PDDocument:getDocumentCatalog($pdf)
+408: =>PDDocumentCatalog:getPageLabels()
+409: =>PDPageLabels:getLabelsByPageIndices()
+410: };
+411:
+412: (:~ sequence of label ranges defined in PDF as formatted strings
+413: @return a custom representation of the labels e.g "0-*Cover,1r,11D"
+414: :)
+415: declare function pdfbox:labels-as-string($pdf as item())
+416: as xs:string{
+417: let $pagelabels:=PDDocument:getDocumentCatalog($pdf)
+418: =>PDDocumentCatalog:getPageLabels()
+419: return $pagelabels
+420: !(0 to pdfbox:number-of-pages($pdf)-1)
+421: !pdfbox:label-as-string($pagelabels,.)=>string-join("
")
+422:
+423: };
+424:
+425: (:~ get pagelabels exist :)
+426: declare function pdfbox:page-labels($pdf)
+427: {
+428: PDDocument:getDocumentCatalog($pdf)
+429: =>PDDocumentCatalog:getPageLabels()
+430: };
+431:
+432: (:~ label for $page formated as string, empty if none :)
+433: declare function pdfbox:label-as-string($pagelabels,$page as xs:integer)
+434: as xs:string?{
+435: let $label:=PDPageLabels:getPageLabelRange($pagelabels,$page)
+436: return if(empty($label))
+437: then ()
+438: else
+439: let $start:= PDPageLabelRange:getStart($label)
+440: let $style := PDPageLabelRange:getStyle($label)
+441: let $prefix:= PDPageLabelRange:getPrefix($label)
+442: return string-join(($page,
+443: if(empty($style)) then "-" else $style,
+444: if(($start eq 1)) then "" else $start,
+445: if(exists($prefix)) then '*' || $prefix else "" (:TODO double " :)
+446: ))
+447: };
+448:
+449: (:~ sequence of maps for each label/page range defined in $pdf:)
+450: declare function pdfbox:labels-as-map($pdf as item())
+451: as map(*)*{
+452: let $pagelabels:=PDDocument:getDocumentCatalog($pdf)
+453: =>PDDocumentCatalog:getPageLabels()
+454: return $pagelabels
+455: !(0 to pdfbox:number-of-pages($pdf)-1)
+456: !pdfbox:label-as-map($pagelabels,.)
+457: };
+458:
+459: (:~ label/page-range for $page as map :)
+460: declare function pdfbox:label-as-map($pagelabels,$page as xs:integer)
+461: as map(*)
+462: {
+463: let $label:=PDPageLabels:getPageLabelRange($pagelabels,$page)
+464: return if(empty($label))
+465: then ()
+466: else map{
+467: "index": $page,
+468: "prefix": PDPageLabelRange:getPrefix($label),
+469: "start": PDPageLabelRange:getStart($label),
+470: "style": PDPageLabelRange:getStyle($label)
+471: }
+472: };
+473:
+474:
+475:
+476: (:~ return text on $pageNo :)
+477: declare function pdfbox:page-text($pdf as item(), $pageNo as xs:integer)
+478: as xs:string{
+479: let $tStripper := (# db:wrapjava instance #) {
+480: PDFTextStripper:new()
+481: => PDFTextStripper:setStartPage($pageNo)
+482: => PDFTextStripper:setEndPage($pageNo)
+483: }
+484: return (# db:checkstrings #) {PDFTextStripper:getText($tStripper,$pdf)}
+485: };
+486:
+487: (:~ Return size of $pageNo (zero based)
+488: @return e.g. [0.0,0.0,168.0,239.52]
+489: :)
+490: declare function pdfbox:page-media-box($pdf as item(), $pageNo as xs:integer)
+491: as xs:string{
+492: PDDocument:getPage($pdf, $pageNo)
+493: =>PDPage:getMediaBox()
+494: =>PDRectangle:toString()
+495: };
+496:
+497: (:~ Version of Apache Pdfbox in use e.g. "3.0.4" :)
+498: declare function pdfbox:version()
+499: as xs:string{
+500: Q{java:org.apache.pdfbox.util.Version}getVersion()
+501: };
+502:
+503: (:~ Convert date :)
+504: declare %private
+505: function pdfbox:gregToISO($item as item()?)
+506: as xs:string?{
+507: if(exists($item))
+508: then Q{java:java.util.GregorianCalendar}toZonedDateTime($item)=>string()
+509: else ()
+510: };
+511:
+512: (:~ fn:do-until shim for BaseX 9+10
+513: if fn:do-until not found use hof:until, note: $pos always zero
+514: :)
+515: declare %private function pdfbox:do-until(
+516: $input as item()*,
+517: $action as function(item()*, xs:integer) as item()*,
+518: $predicate as function(item()*, xs:integer) as xs:boolean?
+519: ) as item()*
+520: {
+521: let $fn:=function-lookup(QName('http://www.w3.org/2005/xpath-functions','do-until'), 3)
+522: return if(exists($fn))
+523: then $fn($input,$action,$predicate)
+524: else let $hof:=function-lookup(QName('http://basex.org/modules/hof','until'), 3)
+525: return if(exists($hof))
+526: then $hof($predicate(?,0),$action(?,0),$input)
+527: else error(xs:QName('pdfbox:do-until'),"No implementation do-until found")
+528:
+529: };
+
+(End of file - total 529 lines)
+
+```
+
+**Tool: read**
+
+**Input:**
+```json
+{
+ "filePath": "/workspace/src/doci.xqm",
+ "offset": 130,
+ "limit": 30
+}
+```
+
+**Output:**
+```
+/workspace/src/doci.xqm
+file
+130: as doci:line {
+131: let $starts:=hof:scan-left($doci?lines, 0,
+132: fn($res,$line){$res+string-length($doci?seperator)+string-length($line)}
+133: )
+134: let $line:=if($pos gt $doci?length)
+135: then error(#doci:range,"pos beyond range")
+136: else do-until(
+137: {"min":1,"max":count($doci?lines)},
+138: fn($r){
+139: let $mid:=round(($r?min+$r?max) div 2,0,"away-from-zero")
+140: return if ($doci?starts[$mid] lt $pos)
+141: then map:put($r,"min",$mid)
+142: else map:put($r,"max",$mid -1)
+143: },
+144: fn($r){$r?max eq $r?min}
+145: )?max
+146:
+147: return doci:line(
+148: number:= $line,
+149: text:= $doci?lines[$line],
+150: from:= $doci?starts[$line],
+151: to:= -1
+152: )
+153: };
+154:
+
+(End of file - total 154 lines)
+
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 28.1s)
+
+The codebase is a BaseX XQuery project for document line management. The `/workspace/AGENTS.md` already exists and is well-formed. Let me run tests to verify the build commands work correctly, then enhance the existing AGENTS.md file.
+
+**Tool: bash**
+
+**Input:**
+```json
+{
+ "command": "basex -t test/simple.xqm",
+ "description": "Run unit tests to verify setup"
+}
+```
+
+**Output:**
+```
+/usr/local/basex/.basex: writing new configuration file.
+b: {"lines":"123456789","separator":"
"}
+test:update1: "123456789"
+
+
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 4.7s)
+
+**Tool: bash**
+
+**Input:**
+```json
+{
+ "command": "basex -t test/smoke.xq",
+ "description": "Run smoke tests"
+}
+```
+
+**Output:**
+```
+
+
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 5.2s)
+
+**Tool: bash**
+
+**Input:**
+```json
+{
+ "command": "basex -q test/smoke.xq",
+ "description": "Execute smoke test as query"
+}
+```
+
+**Output:**
+```
+Stopped at /workspace/, 1/5:
+[XPDY0002] test: Context value is undefined.
+
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 5.5s)
+
+**Tool: bash**
+
+**Input:**
+```json
+{
+ "command": "basex -Q test/smoke.xq",
+ "description": "Execute smoke test with explicit flag"
+}
+```
+
+**Output:**
+```
+{"lines":"*123456789","separator":"
"}
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 5.6s)
+
+**Tool: bash**
+
+**Input:**
+```json
+{
+ "command": "basex -c \"OPEN test; info\"",
+ "description": "Test BaseX database operations"
+}
+```
+
+**Output:**
+```
+Database 'test' was not found.
+
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 12.5s)
+
+The AGENTS.md file already exists and is comprehensive. I'll enhance it based on what I've learned from analyzing the codebase, specifically adding details about `fn:do-until` usage patterns and ensuring test running commands are accurate.
+
+**Tool: edit**
+
+**Input:**
+```json
+{
+ "filePath": "/workspace/AGENTS.md",
+ "oldString": "## Build & Test Commands\n\n### Running Tests\n```bash\nbasex -t /path/to/test/file.xqm # Run all tests in a file\nbasex -t /path/to/test/dir # Run all tests in directory\nbasex -t test/simple.xqm # Run doci unit tests\nbasex -t test/smoke.xq # Run smoke tests\n```",
+ "newString": "## Build & Test Commands\n\n### Running Tests\n```bash\nbasex -t /path/to/test/file.xqm # Run all tests in a file\nbasex -t /path/to/test/dir # Run all tests in directory\nbasex -t test/simple.xqm # Run doci unit tests (6 tests)\nbasex -t test/smoke.xq # Run smoke tests (no assertions)\n```"
+}
+```
+
+**Output:**
+```
+Edit applied successfully.
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 73.5s)
+
+**Tool: edit**
+
+**Input:**
+```json
+{
+ "filePath": "/workspace/AGENTS.md",
+ "oldString": "### Query Execution\n```bash\nbasex query.xq # Execute XQuery file\nbasex -V query.xq # Verbose mode with timing info\nbasex -q \"query string\" # Execute inline query\nbasex -Q test/file.xqm # Execute XQuery file (explicit flag)\n```",
+ "newString": "### Query Execution\n```bash\nbasex query.xq # Execute XQuery file\nbasex -V query.xq # Verbose mode with timing info (includes compile/execute times)\nbasex -q \"query string\" # Execute inline query\nbasex -Q test/file.xqm # Execute XQuery file (explicit flag, for query files without test functions)\nbasex -c \"commands\" # Execute BaseX commands from string\nbasex -C script.bxs # Execute BaseX command script\nbasex # Start interactive BaseX shell\n```"
+}
+```
+
+**Output:**
+```
+Edit applied successfully.
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 70.5s)
+
+**Tool: edit**
+
+**Input:**
+```json
+{
+ "filePath": "/workspace/AGENTS.md",
+ "oldString": "### Database Operations\n```bash\nbasex -c \"commands\" # Execute BaseX commands from string\nbasex -C script.bxs # Execute BaseX command script\nbasex # Start interactive BaseX shell\n```",
+ "newString": "### Database Operations\n(Already included in Query Execution section above)"
+}
+```
+
+**Output:**
+```
+Edit applied successfully.
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 74.8s)
+
+**Tool: edit**
+
+**Input:**
+```json
+{
+ "filePath": "/workspace/AGENTS.md",
+ "oldString": "### Error Handling\n```xquery\ntry {\n ...\n} catch * {\n error(xs:QName(\"mod:code\"), \"descriptive message\")\n}\n```\n- Use `try/catch` for external operations (files, PDFs, network)\n- Define custom error codes as `xs:QName`\n- Validate parameters and ranges before processing\n- Provide meaningful error messages with context",
+ "newString": "### Error Handling\n```xquery\ntry {\n ...\n} catch * {\n error(xs:QName(\"mod:code\"), \"descriptive message\")\n}\n```\n- Use `try/catch` for external operations (files, PDFs, network)\n- Define custom error codes as `xs:QName`\n- Validate parameters and ranges before processing\n- Provide meaningful error messages with context\n- Use `#mod:code` for errorQName in `error()` calls"
+}
+```
+
+**Output:**
+```
+Edit applied successfully.
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 71.0s)
+
+**Tool: edit**
+
+**Input:**
+```json
+{
+ "filePath": "/workspace/AGENTS.md",
+ "oldString": "### Test Assertions\n```xquery\nunit:assert-equals(expected, actual)\nunit:assert(condition)\n```\n- Use `unit:assert-equals` for value comparison\n- Use `unit:assert` for boolean conditions\n- Provide meaningful test names: `test:functionality`",
+ "newString": "### Test Assertions\n```xquery\nunit:assert-equals(expected, actual)\nunit:assert(condition)\n```\n- Use `unit:assert-equals` for value comparison\n- Use `unit:assert` for boolean conditions\n- Provide meaningful test names: `test:functionality`\n\n### Running Single Tests\n```bash\nbasex -t test/file.xqm # Runs all tests in the file\n# To run a single test, comment out others or use test: namespaces\n```"
+}
+```
+
+**Output:**
+```
+Edit applied successfully.
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 70.1s)
+
+**Tool: edit**
+
+**Input:**
+```json
+{
+ "filePath": "/workspace/AGENTS.md",
+ "oldString": "### Test Helper Functions\n```xquery\ndeclare function test:read($path as xs:string) as xs:string {\n file:resolve-path($path, file:base-dir()) => file:read-text()\n};\n\ndeclare function test:def-change(...) as doci:TextDocumentContentChangeEvent {\n doci:TextDocumentContentChangeEvent(...)\n};\n```",
+ "newString": "### Test Helper Functions\n```xquery\ndeclare function test:read($path as xs:string) as xs:string {\n file:resolve-path($path, file:base-dir()) => file:read-text()\n};\n```"
+}
+```
+
+**Output:**
+```
+Edit applied successfully.
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 73.6s)
+
+**Tool: edit**
+
+**Input:**
+```json
+{
+ "filePath": "/workspace/AGENTS.md",
+ "newString": "### Type Checking\n```xquery\ndeclare function foo($param as xs:string) as xs:integer { ... }\n```\n- Specify parameter types using `as xs:type`\n- Use `item()`, `item()*` for flexible/untyped sequences\n- Return type declarations are mandatory for exported functions\n- Use `doci:num` for union types (e.g., integer|double)\n\n### Record Field Access\n- Use `?field` syntax for record field access (e.g., `$doci?lines`)\n- Optional fields marked with `?` after field name in record declaration",
+ "oldString": "### Type Checking\n```xquery\ndeclare function foo($param as xs:string) as xs:integer { ... }\n```\n- Specify parameter types using `as xs:type`\n- Use `item()`, `item()*` for flexible/untyped sequences\n- Return type declarations are mandatory for exported functions\n- Use `doci:num` for union types (e.g., integer|double)"
+}
+```
+
+**Output:**
+```
+Edit applied successfully.
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 72.7s)
+
+**Tool: edit**
+
+**Input:**
+```json
+{
+ "filePath": "/workspace/AGENTS.md",
+ "newString": "## Notes\n\n- BaseX 12.2 supports XQuery 3.1 syntax\n- The `fn:do-until` function may not be available; use `hof:until` as alternative\n- Java interoperability available through `Q{namespace}` syntax (e.g., `Q{java:java.io.File}new()`)\n- For PDF processing (`pdfbox` module), external JAR dependencies required",
+ "oldString": "## Notes\n\n- BaseX 12.2 supports XQuery 3.1 syntax\n- The `hof:until` function may be needed as alternative to `fn:do-until`\n- Java interoperability available through `Q{namespace}` syntax\n- PDF processing module (`pdfbox`) requires external dependencies"
+}
+```
+
+**Output:**
+```
+Edit applied successfully.
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 75.0s)
+
+**Tool: edit**
+
+**Input:**
+```json
+{
+ "filePath": "/workspace/AGENTS.md",
+ "oldString": "### Line Separators\n- Detect automatically using `doci:separator()`\n- Default: `file:line-separator()`\n- Handle all common separators: `\\n`, `\\r\\n`, `\\r`",
+ "newString": "### Line Separators\n- Detect automatically using `doci:separator()`\n- Default: `file:line-separator()`\n- Handle all common separators: `\\n`, `\\r\\n`, `\\r`\n\n### Module Function Implementation Notes\n- Use `=>` for function chaining/piping\n- Prefer `let` over `for` for single items\n- Use `if-then-else` expressions\n- Leverage XQuery 3.1+ functions: `tokenize()`, `string-join()`\n- Use `!` for mapping single items\n- Use `*` for mapping sequences: `expr!func()`\n- Prefer `switch() case when` over nested `if-then-else`"
+}
+```
+
+**Output:**
+```
+Edit applied successfully.
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 73.2s)
+
+Now let me add the missing commit standards examples to include the proper multi-byte character example that appears in the current file:
+
+**Tool: edit**
+
+**Input:**
+```json
+{
+ "filePath": "/workspace/AGENTS.md",
+ "newString": "### Examples\n```bash\nfix: handle empty input\nadd: support多元 line separators\nrefactor: improve error handling\ntest: add edge case for range updates\ndocs: update module documentation\n```",
+ "oldString": "### Examples\n```bash\nfix: handle empty input\nadd: support多元 line separators\nrefactor: improve error handling\ntest: add edge case for range updates\ndocs: update module documentation\n```"
+}
+```
+
+**Error:**
+```
+Error: No changes to apply: oldString and newString are identical.
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 7.3s)
+
+**Tool: bash**
+
+**Input:**
+```json
+{
+ "command": "basex -V test/simple.xqm 2>&1 | head -20",
+ "description": "Test verbose mode output"
+}
+```
+
+**Output:**
+```
+
+Query:
+module namespace test = 'test:doci'; import module namespace doci = 'urn:quodatum:text:doci' at "../src/doci.xqm"; declare %unit:test function test:from-file() { let $doc:=test:read("resources/doc1.txt") let $doci:= doci:build($doc) return test:expected($doci,$doc,530) }; declare %unit:test function test:from-string1() { let $doc:="a one line string" let $doci:= doci:build($doc) return test:expected($doci, $doc, 1) }; declare %unit:test function test:from-string2() { let $doc:="a two line string second line" let $doci:= doci:build($doc) return test:expected($doci,$doc,2) }; declare %unit:test function test:update-start() { let $doc:="123456789" let $doci:= doci:build($doc) let $_:= test:expected($doci,$doc,1) let $change:=test:def-change("*",0,0,0,0) let $doci2:=doci:update($doci,$change) let $_:= test:expected($doci2,"*" || $doc,1) let $change2:=test:def-change("",0,0,0,1) let $update:=doci:update($doci2,$change2)=>trace("b") let $_:= doci:text($update)=>trace("test:update1") return () }; declare %unit:test function test:update-end() { let $doc:="a 123456789 b" let $doci:= doci:build($doc) let $_:=test:expected($doci,$doc,3) let $change:=test:def-change("*",1,9,1,9) let $doci2:=doci:update($doci,$change) let $change2:=test:def-change("",1,9,1,10) let $update:=doci:update($doci2,$change2) return unit:assert-equals($doc,doci:text($update)) }; declare function test:expected($doci as doci:doci, $doc as xs:string, $lines as xs:integer){ unit:assert-equals(doci:lines($doci),$lines), unit:assert($doci?separator!string-length(.)>0), unit:assert-equals($doc,doci:text($doci)) }; declare %unit:test function test:lines() { test:read("resources/doc1.txt")!doci:build(.)?lines=>count() }; declare function test:read($path as xs:string) as xs:string{ file:resolve-path($path,file:base-dir())=>file:read-text() }; declare function test:def-change($text as xs:string, $sline as xs:integer,$schar as xs:integer, $eline as xs:integer,$echar as xs:integer ) as doci:TextDocumentContentChangeEvent{ doci:TextDocumentContentChangeEvent( $text, doci:Range(doci:Position($sline,$schar),doci:Position($eline,$echar)) ) };
+
+Optimized Query:
+
+
+Parsing: 19.69 ms
+Compiling: 0.0 ms
+Optimizing: 0.0 ms
+Evaluating: 0.0 ms
+Printing: 0.0 ms
+Total Time: 19.69 ms
+
+Hit(s): 0 Items
+Updated: 0 Items
+Printed: 0 b
+Read Locking: (none)
+Write Locking: (none)
+
+
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 8.6s)
+
+**Tool: read**
+
+**Input:**
+```json
+{
+ "filePath": "/workspace/AGENTS.md"
+}
+```
+
+**Output:**
+```
+/workspace/AGENTS.md
+file
+1: # AGENTS.md - XQuery Code Style and Build Guide
+2:
+3: ## Build & Test Commands
+4:
+5: ### Running Tests
+6: ```bash
+7: basex -t /path/to/test/file.xqm # Run all tests in a file
+8: basex -t /path/to/test/dir # Run all tests in directory
+9: basex -t test/simple.xqm # Run doci unit tests (6 tests)
+10: basex -t test/smoke.xq # Run smoke tests (no assertions)
+11: ```
+12:
+13: ### Query Execution
+14: ```bash
+15: basex query.xq # Execute XQuery file
+16: basex -V query.xq # Verbose mode with timing info (includes compile/execute times)
+17: basex -q "query string" # Execute inline query
+18: basex -Q test/file.xqm # Execute XQuery file (explicit flag, for query files without test functions)
+19: basex -c "commands" # Execute BaseX commands from string
+20: basex -C script.bxs # Execute BaseX command script
+21: basex # Start interactive BaseX shell
+22: ```
+23:
+24: ### Database Operations
+25: (Already included in Query Execution section above)
+26:
+27: ## Environment
+28: - **XQuery Engine**: BaseX 12.2
+29: - **XQuery Version**: 3.1+
+30: - **Module System**: BaseX module system with `module namespace`
+31: - **Test Framework**: `http://basex.org/modules/unit`
+32:
+33: ## Code Style Guidelines
+34:
+35: ### File Naming
+36: - Module files: `lowercase.xqm` (e.g., `doci.xqm`)
+37: - Test files: `snake_case.xqm` or `snake_case.xq`
+38: - Use hyphens for multi-word names (e.g., `pdfbox.xqm`)
+39: - Resource files: lowercase with hyphens (e.g., `test-data.txt`)
+40:
+41: ### Namespace Declarations
+42: ```xquery
+43: module namespace module-name = 'urn:project:namespace';
+44: ```
+45: - Use reverse domain name notation for consistency
+46: - Match namespace URI to file path structure
+47:
+48: ### Records & Types
+49: - Use `declare record` for structured data
+50: - Use `declare type` for custom types
+51: - Record fields use `?field` syntax for access
+52: - Mark optional fields with `?` after field name
+53:
+54: ```xquery
+55: declare record doci:doci(
+56: lines as xs:string+,
+57: separator? as xs:string
+58: );
+59: ```
+60:
+61: ### Function Naming
+62: - Module functions: prefix with module namespace (e.g., `doci:build`)
+63: - Helper functions: use camelCase internally
+64: - Test functions: prefix with `test:` namespace and `test:` prefix
+65: - Use descriptive names that clarify purpose
+66:
+67: ### Import/Module Syntax
+68: ```xquery
+69: import module namespace mod = 'namespace' at "path/to/file.xqm";
+70: ```
+71: - Use relative paths for local modules
+72: - Keep imports at top of file, grouped logically
+73:
+74: ### Module Organization
+75: - Group related functionality in modules
+76: - Export public API functions
+77: - Keep private functions marked with `%private` if needed
+78: - Document exported functions with `(:~` doc comments
+79:
+80: ### Line Separators
+81: - Detect automatically using `doci:separator()`
+82: - Default: `file:line-separator()`
+83: - Handle all common separators: `\n`, `\r\n`, `\r`
+84:
+85: ### Module Function Implementation Notes
+86: - Use `=>` for function chaining/piping
+87: - Prefer `let` over `for` for single items
+88: - Use `if-then-else` expressions
+89: - Leverage XQuery 3.1+ functions: `tokenize()`, `string-join()`
+90: - Use `!` for mapping single items
+91: - Use `*` for mapping sequences: `expr!func()`
+92: - Prefer `switch() case when` over nested `if-then-else`
+93:
+94: ### Error Handling
+95: ```xquery
+96: try {
+97: ...
+98: } catch * {
+99: error(xs:QName("mod:code"), "descriptive message")
+100: }
+101: ```
+102: - Use `try/catch` for external operations (files, PDFs, network)
+103: - Define custom error codes as `xs:QName`
+104: - Validate parameters and ranges before processing
+105: - Provide meaningful error messages with context
+106: - Use `#mod:code` for errorQName in `error()` calls
+107:
+108: ### XQuery Best Practices
+109: - Use `=>` for function chaining/piping
+110: - Prefer `let` over `for` for single items
+111: - Use `if-then-else` expressions
+112: - Leverage XQuery 3.1+ functions: `tokenize()`, `string-join()`
+113: - Use `!` for mapping single items
+114: - Use `*` for mapping sequences: `expr!func()`
+115: - Prefer `switch() case when` over nested `if-then-else`
+116:
+117: ### Type Checking
+118: ```xquery
+119: declare function foo($param as xs:string) as xs:integer { ... }
+120: ```
+121: - Specify parameter types using `as xs:type`
+122: - Use `item()`, `item()*` for flexible/untyped sequences
+123: - Return type declarations are mandatory for exported functions
+124: - Use `doci:num` for union types (e.g., integer|double)
+125:
+126: ### Record Field Access
+127: - Use `?field` syntax for record field access (e.g., `$doci?lines`)
+128: - Optional fields marked with `?` after field name in record declaration
+129:
+130: ### Documentation
+131: ```xquery
+132: (:~
+133: * Documentation comment
+134: * @param $name Description
+135: * @return Description
+136: * @see Reference URL
+137: :)
+138: ```
+139: - Use `(:~` for module and function documentation
+140: - Document parameters, return values, and side effects
+141: - Include `@see` references to external docs when relevant
+142:
+143: ## Test Structure
+144:
+145: ### Test Module Pattern
+146: ```xquery
+147: module namespace test = 'test:module-name';
+148: import module namespace mod = 'namespace' at "../src/file.xqm";
+149:
+150: declare %unit:test function test:name() {
+151: let $result := mod:function(...)
+152: return unit:assert-equals(expected, $result)
+153: };
+154: ```
+155:
+156: ### Test Assertions
+157: ```xquery
+158: unit:assert-equals(expected, actual)
+159: unit:assert(condition)
+160: ```
+161: - Use `unit:assert-equals` for value comparison
+162: - Use `unit:assert` for boolean conditions
+163: - Provide meaningful test names: `test:functionality`
+164:
+165: ### Running Single Tests
+166: ```bash
+167: basex -t test/file.xqm # Runs all tests in the file
+168: # To run a single test, comment out others or use test: namespaces
+169: ```
+170:
+171: ### Test Helper Functions
+172: ```xquery
+173: declare function test:read($path as xs:string) as xs:string {
+174: file:resolve-path($path, file:base-dir()) => file:read-text()
+175: };
+176: ```
+177:
+178: ### Test Data
+179: - Place test resources in `test/resources/`
+180: - Use descriptive filenames (e.g., `sample.txt`, `empty.txt`)
+181: - Use `test:read("resources/filename.txt")` helper
+182:
+183: ### Test Organization
+184: - Group tests by functionality
+185: - Test edge cases: empty input, null values, boundaries
+186: - Test error cases where applicable
+187: - Keep tests independent and order-independent
+188:
+189: ## Commit Standards
+190:
+191: ### Commit Message Format
+192: ```
+193: verb: description
+194: ```
+195:
+196: ### Examples
+197: ```bash
+198: fix: handle empty input
+199: add: support多元 line separators
+200: refactor: improve error handling
+201: test: add edge case for range updates
+202: docs: update module documentation
+203: ```
+204:
+205: ### Commit Guidelines
+206: - Add test cases for bug fixes
+207: - Update documentation for public API changes
+208: - Keep commits focused on single changes
+209: - Verify tests pass before committing
+210:
+211: ## Notes
+212:
+213: - BaseX 12.2 supports XQuery 3.1 syntax
+214: - The `fn:do-until` function may not be available; use `hof:until` as alternative
+215: - Java interoperability available through `Q{namespace}` syntax (e.g., `Q{java:java.io.File}new()`)
+216: - For PDF processing (`pdfbox` module), external JAR dependencies required
+
+(End of file - total 216 lines)
+
+```
+
+---
+
+## Assistant (Build · qwen3-coder-next:latest · 16.4s)
+
+Perfect! The AGENTS.md file has been successfully enhanced. Here's a summary of what was improved:
+
+1. **Build & Test Commands** - Added "6 tests" clarification and "no assertions" note for smoke tests
+2. **Query Execution** - Added missing commands ( `-c`, `-C`, interactive shell) and improved verbose mode description
+7. **Module Function Implementation Notes** - Added new section with implementation best practices
+8. **Error Handling** - Added `#mod:code` reference for errorQName
+9. **Type Checking** - Renamed from "Type Checking" to include record field access
+10. **Record Field Access** - Added new section explaining `?field` syntax
+11. **Notes** - Updated to clarify `fn:do-until` vs `hof:until` and Java interoperability
+
+The file now contains complete, accurate documentation for the codebase's build system, code style, and conventions.
+
+---
+