Skip to content

Commit cc80191

Browse files
authored
feat: add lat.md support to the codebase (#279)
* feat: add lat.md support to the codebase * Fix ty errors for optional Rust backend and test typing * fix: tests * fix: python 3.15 alpha 7 * fix: make version latest
1 parent b7a8095 commit cc80191

18 files changed

Lines changed: 287 additions & 55 deletions

.coverage

0 Bytes
Binary file not shown.

.github/workflows/lint.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ jobs:
3737
uses: actions/setup-python@v6
3838
with:
3939
python-version: '3.13'
40-
- name: Install uv
41-
uses: astral-sh/setup-uv@v6
40+
- name: Install the latest version of uv
41+
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
4242
with:
4343
enable-cache: true
44+
version: "latest"
4445
- name: Install dependencies
4546
run: |
4647
uv venv

.github/workflows/pythonpackage.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
strategy:
2929
fail-fast: false
3030
matrix:
31-
python-version: [pypy-3.10, pypy-3.11, '3.10', '3.11', '3.12', '3.13', '3.14', '3.14t', '3.15.0-alpha.3']
31+
python-version: [pypy-3.10, pypy-3.11, '3.10', '3.11', '3.12', '3.13', '3.14', '3.14t', '3.15.0-alpha.7']
3232
os: [
3333
ubuntu-latest,
3434
windows-latest,

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,6 @@ rust/target/
135135
Cargo.lock
136136
*.rlib
137137
*.rmeta
138+
.codex
139+
.pi
140+
.cursor

AGENTS.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Before starting work
2+
3+
- Run `lat search` to find sections relevant to your task. Read them to understand the design intent before writing code.
4+
- Run `lat expand` on user prompts to expand any `[[refs]]` — this resolves section names to file locations and provides context.
5+
6+
# Post-task checklist (REQUIRED — do not skip)
7+
8+
After EVERY task, before responding to the user:
9+
10+
- [ ] Update `lat.md/` if you added or changed any functionality, architecture, tests, or behavior
11+
- [ ] Run `lat check` — all wiki links and code refs must pass
12+
- [ ] Do not skip these steps. Do not consider your task done until both are complete.
13+
14+
---
15+
16+
# What is lat.md?
17+
18+
This project uses [lat.md](https://www.npmjs.com/package/lat.md) to maintain a structured knowledge graph of its architecture, design decisions, and test specs in the `lat.md/` directory. It is a set of cross-linked markdown files that describe **what** this project does and **why** — the domain concepts, key design decisions, business logic, and test specifications. Use it to ground your work in the actual architecture rather than guessing.
19+
20+
# Commands
21+
22+
```bash
23+
lat locate "Section Name" # find a section by name (exact, fuzzy)
24+
lat refs "file#Section" # find what references a section
25+
lat search "natural language" # semantic search across all sections
26+
lat expand "user prompt text" # expand [[refs]] to resolved locations
27+
lat check # validate all links and code refs
28+
```
29+
30+
Run `lat --help` when in doubt about available commands or options.
31+
32+
If `lat search` fails because no API key is configured, explain to the user that semantic search requires a key provided via `LAT_LLM_KEY` (direct value), `LAT_LLM_KEY_FILE` (path to key file), or `LAT_LLM_KEY_HELPER` (command that prints the key). Supported key prefixes: `sk-...` (OpenAI) or `vck_...` (Vercel). If the user doesn't want to set it up, use `lat locate` for direct lookups instead.
33+
34+
# Syntax primer
35+
36+
- **Section ids**: `lat.md/path/to/file#Heading#SubHeading` — full form uses project-root-relative path (e.g. `lat.md/tests/search#RAG Replay Tests`). Short form uses bare file name when unique (e.g. `search#RAG Replay Tests`, `cli#search#Indexing`).
37+
- **Wiki links**: `[[target]]` or `[[target|alias]]` — cross-references between sections. Can also reference source code: `[[src/foo.ts#myFunction]]`.
38+
- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct). `lat check` validates these exist.
39+
- **Code refs**: `// @lat: [[section-id]]` (JS/TS/Rust/Go/C) or `# @lat: [[section-id]]` (Python) — ties source code to concepts
40+
41+
# Test specs
42+
43+
Key tests can be described as sections in `lat.md/` files (e.g. `tests.md`). Add frontmatter to require that every leaf section is referenced by a `// @lat:` or `# @lat:` comment in test code:
44+
45+
```markdown
46+
---
47+
lat:
48+
require-code-mention: true
49+
---
50+
# Tests
51+
52+
Authentication and authorization test specifications.
53+
54+
## User login
55+
56+
Verify credential validation and error handling for the login endpoint.
57+
58+
### Rejects expired tokens
59+
Tokens past their expiry timestamp are rejected with 401, even if otherwise valid.
60+
61+
### Handles missing password
62+
Login request without a password field returns 400 with a descriptive error.
63+
```
64+
65+
Every section MUST have a description — at least one sentence explaining what the test verifies and why. Empty sections with just a heading are not acceptable. (This is a specific case of the general leading paragraph rule below.)
66+
67+
Each test in code should reference its spec with exactly one comment placed next to the relevant test — not at the top of the file:
68+
69+
```python
70+
# @lat: [[tests#User login#Rejects expired tokens]]
71+
def test_rejects_expired_tokens():
72+
...
73+
74+
# @lat: [[tests#User login#Handles missing password]]
75+
def test_handles_missing_password():
76+
...
77+
```
78+
79+
Do not duplicate refs. One `@lat:` comment per spec section, placed at the test that covers it. `lat check` will flag any spec section not covered by a code reference, and any code reference pointing to a nonexistent section.
80+
81+
# Section structure
82+
83+
Every section in `lat.md/` **must** have a leading paragraph — at least one sentence immediately after the heading, before any child headings or other block content. The first paragraph must be ≤250 characters (excluding `[[wiki link]]` content). This paragraph serves as the section's overview and is used in search results, command output, and RAG context — keeping it concise guarantees the section's essence is always captured.
84+
85+
```markdown
86+
# Good Section
87+
88+
Brief overview of what this section documents and why it matters.
89+
90+
More detail can go in subsequent paragraphs, code blocks, or lists.
91+
92+
## Child heading
93+
94+
Details about this child topic.
95+
```
96+
97+
```markdown
98+
# Bad Section
99+
100+
## Child heading
101+
102+
Details about this child topic.
103+
```
104+
105+
The second example is invalid because `Bad Section` has no leading paragraph. `lat check` validates this rule and reports errors for missing or overly long leading paragraphs.

json2xml/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
EMAIL = "mail@vinitkumar.me"
6161

6262

63+
# @lat: [[architecture#CLI entrypoint]]
6364
def create_parser() -> argparse.ArgumentParser:
6465
"""Create and configure the argument parser."""
6566
parser = argparse.ArgumentParser(
@@ -228,6 +229,7 @@ def create_parser() -> argparse.ArgumentParser:
228229
return parser
229230

230231

232+
# @lat: [[behavior#Input readers]]
231233
def read_input(args: argparse.Namespace) -> dict[str, Any] | list[Any]:
232234
"""
233235
Read JSON input from the specified source.

json2xml/dicttoxml.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def get_unique_id(element: str) -> str:
7272
]
7373

7474

75-
def get_xml_type(val: ELEMENT) -> str:
75+
def get_xml_type(val: Any) -> str:
7676
"""
7777
Get the XML type of a given value.
7878
@@ -227,6 +227,7 @@ def get_xpath31_tag_name(val: Any) -> str:
227227
return "string"
228228

229229

230+
# @lat: [[behavior#XPath 3.1 format]]
230231
def convert_to_xpath31(obj: Any, parent_key: str | None = None) -> str:
231232
"""
232233
Convert a Python object to XPath 3.1 json-to-xml format.
@@ -267,7 +268,7 @@ def convert_to_xpath31(obj: Any, parent_key: str | None = None) -> str:
267268

268269

269270
def convert(
270-
obj: ELEMENT,
271+
obj: Any,
271272
ids: Any,
272273
attr_type: bool,
273274
item_func: Callable[[str], str],
@@ -631,6 +632,7 @@ def convert_none(
631632
return f"<{key}{attr_string}></{key}>"
632633

633634

635+
# @lat: [[architecture#Conversion engine]]
634636
def dicttoxml(
635637
obj: ELEMENT,
636638
root: bool = True,

json2xml/dicttoxml_fast.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,24 @@
1717
from collections.abc import Callable
1818
from typing import Any
1919

20+
RustStringTransform = Callable[[str], str]
21+
2022
LOG = logging.getLogger("dicttoxml_fast")
2123

2224
# Try to import the Rust implementation
2325
_USE_RUST = False
24-
_rust_dicttoxml = None
26+
_rust_dicttoxml: Callable[..., bytes] | None = None
27+
rust_escape_xml: RustStringTransform | None = None
28+
rust_wrap_cdata: RustStringTransform | None = None
2529

2630
try:
27-
from json2xml_rs import dicttoxml as _rust_dicttoxml # type: ignore[import-not-found] # pragma: no cover
28-
from json2xml_rs import escape_xml_py as rust_escape_xml # type: ignore[import-not-found] # pragma: no cover
29-
from json2xml_rs import wrap_cdata_py as rust_wrap_cdata # type: ignore[import-not-found] # pragma: no cover
31+
from json2xml_rs import dicttoxml as _rust_dicttoxml # pragma: no cover
32+
from json2xml_rs import escape_xml_py as rust_escape_xml # pragma: no cover
33+
from json2xml_rs import wrap_cdata_py as rust_wrap_cdata # pragma: no cover
3034
_USE_RUST = True # pragma: no cover
3135
LOG.debug("Using Rust backend for dicttoxml") # pragma: no cover
3236
except ImportError: # pragma: no cover
3337
LOG.debug("Rust backend not available, using pure Python")
34-
rust_escape_xml = None
35-
rust_wrap_cdata = None
3638

3739
# Import the pure Python implementation as fallback
3840
from json2xml import dicttoxml as _py_dicttoxml # noqa: E402
@@ -48,6 +50,7 @@ def get_backend() -> str:
4850
return "rust" if _USE_RUST else "python"
4951

5052

53+
# @lat: [[architecture#Backend selection]]
5154
def dicttoxml(
5255
obj: Any,
5356
root: bool = True,

json2xml/json2xml.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .utils import InvalidDataError
99

1010

11+
# @lat: [[architecture#Core pipeline]]
1112
class Json2xml:
1213
"""
1314
Wrapper class to convert the data to xml
@@ -34,6 +35,8 @@ def __init__(
3435
self.cdata = cdata
3536
self.list_headers = list_headers
3637

38+
# @lat: [[behavior#Conversion output]]
39+
# @lat: [[behavior#Invalid XML payloads]]
3740
def to_xml(self) -> Any | None:
3841
"""
3942
Convert to xml using dicttoxml.dicttoxml and then pretty print it.

json2xml/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class StringReadError(Exception):
2424
pass
2525

2626

27+
# @lat: [[behavior#Input readers]]
2728
def readfromjson(filename: str) -> dict[str, str]:
2829
"""Reads a JSON file and returns a dictionary."""
2930
try:
@@ -44,7 +45,7 @@ def readfromurl(url: str, params: dict[str, str] | None = None) -> dict[str, str
4445
raise URLReadError("URL is not returning correct response")
4546

4647

47-
def readfromstring(jsondata: str) -> dict[str, str]:
48+
def readfromstring(jsondata: object) -> dict[str, str]:
4849
"""Loads JSON data from a string and returns a dictionary."""
4950
if not isinstance(jsondata, str):
5051
raise StringReadError("Input is not a proper JSON string")

0 commit comments

Comments
 (0)