Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions skills/shared/collect_and_upload_sightmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,20 +146,46 @@ def collect(root: str) -> list[dict]:
return result


def _normalize_memory(value) -> list[str]:
"""Coerce a memory field (string, list, or missing) to a list of non-empty strings."""
if isinstance(value, str):
return [value] if value else []
if isinstance(value, list):
return [str(m) for m in value if m]
return []


def collect_memory(root: str) -> list[str]:
"""Collect top-level memory entries from .sightmap/ YAML files."""
"""Collect memory entries from .sightmap/ YAML files per Sightmap v1 §Memory.

Picks up memory attached to file, view, and request scopes. Component memory
stays attached to each component entry via flatten_components and is uploaded
on the component itself, not flattened into this list.
"""
files = find_sightmap_files(root)
result: list[str] = []
for path in files:
with open(path) as f:
data = yaml.safe_load(f)
if not isinstance(data, dict):
continue
memory = data.get("memory", [])
if isinstance(memory, str):
memory = [memory]
if isinstance(memory, list):
result.extend(str(m) for m in memory if m)

# File-level memory
result.extend(_normalize_memory(data.get("memory")))

# File-level (global) requests
for req in data.get("requests") or []:
if isinstance(req, dict):
result.extend(_normalize_memory(req.get("memory")))

# View-scoped memory + view-scoped requests
for view in data.get("views") or []:
if not isinstance(view, dict):
continue
result.extend(_normalize_memory(view.get("memory")))
for req in view.get("requests") or []:
if isinstance(req, dict):
result.extend(_normalize_memory(req.get("memory")))
return result


Expand Down
86 changes: 86 additions & 0 deletions skills/shared/test_collect_sightmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from collect_and_upload_sightmap import (
collect,
collect_memory,
find_sightmap_files,
flatten_components,
parse_file,
Expand Down Expand Up @@ -253,3 +254,88 @@ def test_no_duplicates(self):
def test_empty_root(self):
with tempfile.TemporaryDirectory() as tmp:
assert collect(tmp) == []


# --- collect_memory (spec-conformance) ---


def _write_yaml(root: str, name: str, body: str) -> None:
sdir = os.path.join(root, ".sightmap")
os.makedirs(sdir, exist_ok=True)
with open(os.path.join(sdir, name), "w") as f:
f.write(body)


class TestCollectMemory:
"""Sightmap v1 §Memory: memory can attach to file, view, component, or request.

collect_memory flattens file/view/request memory into a single list.
Component memory stays attached to the component via flatten_components and
is uploaded with the component itself, not in the flat memory list.
"""

def test_file_level_memory(self):
with tempfile.TemporaryDirectory() as tmp:
_write_yaml(tmp, "f.yaml", "version: 1\nmemory:\n - file-fact-1\n - file-fact-2\n")
assert collect_memory(tmp) == ["file-fact-1", "file-fact-2"]

def test_view_level_memory(self):
with tempfile.TemporaryDirectory() as tmp:
_write_yaml(
tmp,
"f.yaml",
"version: 1\nviews:\n - name: search\n route: /search\n memory:\n - view-fact-1\n - view-fact-2\n",
)
assert collect_memory(tmp) == ["view-fact-1", "view-fact-2"]

def test_top_level_request_memory(self):
with tempfile.TemporaryDirectory() as tmp:
_write_yaml(
tmp,
"f.yaml",
"version: 1\nrequests:\n - name: Health\n method: GET\n path: /healthz\n memory:\n - req-fact\n",
)
assert collect_memory(tmp) == ["req-fact"]

def test_view_scoped_request_memory(self):
with tempfile.TemporaryDirectory() as tmp:
_write_yaml(
tmp,
"f.yaml",
"version: 1\nviews:\n - name: search\n route: /search\n requests:\n - name: SearchAPI\n method: GET\n path: /api/search\n memory:\n - view-req-fact\n",
)
assert collect_memory(tmp) == ["view-req-fact"]

def test_string_memory_normalized_to_list(self):
with tempfile.TemporaryDirectory() as tmp:
_write_yaml(tmp, "f.yaml", "version: 1\nmemory: lonely-fact\n")
assert collect_memory(tmp) == ["lonely-fact"]

def test_combines_all_scopes_from_fixture(self):
result = collect_memory(TESTDATA)
# file-level (2) + top-level request (1) + view-level (2) + view-scoped request (1) = 6
# Component memory is NOT in this list (attached to component entry instead).
assert "Dates throughout the app are ISO-8601 (YYYY-MM-DD)" in result
assert "All currency values are USD minor units (cents)" in result
assert "Returns 503 during deploys; clients should treat that as not-yet-ready" in result
assert "The search form lives inside a modal on mobile; selectors differ" in result
assert "Hitting Enter inside the date input submits without clicking Search" in result
assert "Rate-limited to 10 requests/min per user; returns 429 beyond that" in result
# Component memory must NOT appear in the flat list.
assert "Accepts typed YYYY-MM-DD — skips the calendar" not in result

def test_component_memory_attached_not_flattened(self):
components = parse_file(
os.path.join(TESTDATA, ".sightmap", "memory.yaml")
)
search_form = next(c for c in components if c["name"] == "SearchForm")
assert "Accepts typed YYYY-MM-DD — skips the calendar" in search_form["memory"]

def test_no_memory_anywhere_returns_empty(self):
with tempfile.TemporaryDirectory() as tmp:
_write_yaml(tmp, "f.yaml", "version: 1\nviews:\n - name: x\n route: /x\n")
assert collect_memory(tmp) == []

def test_missing_sightmap_dir_returns_empty(self):
with tempfile.TemporaryDirectory() as tmp:
assert collect_memory(tmp) == []
34 changes: 34 additions & 0 deletions skills/shared/testdata/.sightmap/memory.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Spec-conformance fixture: memory at file, view, component, and request scopes
# per Sightmap v1 spec §Memory.

version: 1

memory:
- Dates throughout the app are ISO-8601 (YYYY-MM-DD)
- All currency values are USD minor units (cents)

requests:
- name: Health
method: GET
path: /healthz
memory:
- Returns 503 during deploys; clients should treat that as not-yet-ready

views:
- name: search
route: /search
memory:
- The search form lives inside a modal on mobile; selectors differ
- Hitting Enter inside the date input submits without clicking Search
components:
- name: SearchForm
selector: form.search
source: src/Search.tsx
memory:
- Accepts typed YYYY-MM-DD — skips the calendar
requests:
- name: SearchAPI
method: GET
path: /api/search
memory:
- Rate-limited to 10 requests/min per user; returns 429 beyond that