Skip to content

Commit 75cde00

Browse files
illeatmyhatclaude
andcommitted
feat(platform-integrations): stable native-to-entity id linkage for provenance
Closes the correlation-id gap that broke provenance on Claude. The adapter now derives the entity slug from the native memory's name field, so the entity id is a deterministic, derivable <type>/<name> on both the save and recall sides, and re-mirroring overwrites in place (idempotent, no -N suffix). The entity also stamps native_path as a back-reference. - entity_io.write_entity_file: optional filename/overwrite for deterministic in-place writes (default behavior unchanged for existing callers); native_path added to _FRONTMATTER_KEYS - adapt_memory.py: parse native name, write <type>/<name>.md, print the id - Claude EVOLVE.md recall-audit now logs the entity id <type>/<name> (not native paths), which provenance resolves to .evolve/entities/<type>/<name>.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2934513 commit 75cde00

13 files changed

Lines changed: 372 additions & 111 deletions

File tree

platform-integrations/bob/evolve-lite/lib/evolve-lite/entity_io.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def unique_filename(directory, slug):
154154
# Markdown <-> dict conversion
155155
# ---------------------------------------------------------------------------
156156

157-
_FRONTMATTER_KEYS = ("type", "trigger", "trajectory", "owner", "source", "visibility", "published_at")
157+
_FRONTMATTER_KEYS = ("type", "trigger", "trajectory", "owner", "source", "native_path", "visibility", "published_at")
158158

159159

160160
def entity_to_markdown(entity):
@@ -354,12 +354,24 @@ def load_all_entities(entities_dir):
354354
return entities
355355

356356

357-
def write_entity_file(directory, entity):
357+
def write_entity_file(directory, entity, filename=None, overwrite=False):
358358
"""Write a single entity as a markdown file under *directory*.
359359
360360
The file is placed in a ``{type}/`` subdirectory. Uses atomic
361361
write (write to ``.tmp``, then ``os.rename``).
362362
363+
Args:
364+
directory: Entities root directory.
365+
entity: The entity dict to serialize.
366+
filename: Optional explicit slug for the target file (without the
367+
``.md`` suffix). When omitted, the slug is derived from the
368+
entity content (the historical default).
369+
overwrite: When True, the entity is written to a deterministic
370+
``{type}/{filename}.md`` path, overwriting any existing file in
371+
place (stable id, idempotent re-mirroring). When False (the
372+
default), the historical collision-avoiding behavior is kept —
373+
a ``-2``/``-3`` suffix is appended on collision.
374+
363375
Returns:
364376
Path to the written file.
365377
"""
@@ -370,7 +382,7 @@ def write_entity_file(directory, entity):
370382
type_dir = Path(directory) / entity_type
371383
type_dir.mkdir(parents=True, exist_ok=True)
372384

373-
slug = slugify(entity.get("content", "entity"))
385+
slug = filename if filename else slugify(entity.get("content", "entity"))
374386
content = entity_to_markdown(entity)
375387

376388
# Write to a unique temp file first (avoids predictable .tmp collisions)
@@ -381,6 +393,13 @@ def write_entity_file(directory, entity):
381393
os.close(fd)
382394
fd = None
383395

396+
if overwrite:
397+
# Deterministic target: overwrite any existing file in place so
398+
# the entity id is stable across re-mirroring.
399+
target = type_dir / f"{slug}.md"
400+
os.replace(tmp_path, target)
401+
return target
402+
384403
# Atomically claim the target using O_EXCL; retry on race
385404
while True:
386405
target = unique_filename(type_dir, slug)

platform-integrations/bob/evolve-lite/skills/evolve-lite-adapt-memory/scripts/adapt_memory.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from entity_io import ( # noqa: E402
4949
find_entities_dir,
5050
get_default_entities_dir,
51+
slugify,
5152
write_entity_file,
5253
log as _log,
5354
)
@@ -58,14 +59,15 @@ def log(message):
5859

5960

6061
def parse_native_memory(text):
61-
"""Split a native memory file into (description, body).
62+
"""Split a native memory file into (name, description, body).
6263
6364
Native frontmatter is simple ``key: value`` lines plus a nested
64-
``metadata:`` block; we only need ``description`` and the body, so we
65-
parse the top-level ``description:`` line and treat everything after the
66-
closing ``---`` as the body. Missing frontmatter is tolerated — the whole
67-
text is then the body.
65+
``metadata:`` block; we parse the top-level ``name`` and ``description``
66+
lines and treat everything after the closing ``---`` as the body. The
67+
``name`` is the native slug we reuse as the stable entity id. Missing
68+
frontmatter is tolerated — the whole text is then the body.
6869
"""
70+
name = None
6971
description = None
7072
body = text
7173
if text.startswith("---"):
@@ -74,16 +76,17 @@ def parse_native_memory(text):
7476
frontmatter, body = parts[1], parts[2]
7577
for line in frontmatter.splitlines():
7678
# Only top-level keys (no leading indentation) — keeps the
77-
# nested metadata.* keys out of the description match.
79+
# nested metadata.* keys out of the top-level matches.
7880
if line[:1].isspace():
7981
continue
8082
key, _, value = line.partition(":")
81-
if key.strip() == "description":
82-
value = value.strip()
83-
if value:
84-
description = value
85-
break
86-
return description, body.strip()
83+
key = key.strip()
84+
value = value.strip()
85+
if key == "name" and value:
86+
name = value
87+
elif key == "description" and value:
88+
description = value
89+
return name, description, body.strip()
8790

8891

8992
def main():
@@ -112,7 +115,7 @@ def main():
112115
print(f"Error: cannot read {memory_path} - {exc}", file=sys.stderr)
113116
sys.exit(1)
114117

115-
description, body = parse_native_memory(text)
118+
name, description, body = parse_native_memory(text)
116119
if not body:
117120
print(f"Error: native memory {memory_path} has no body to mirror.", file=sys.stderr)
118121
sys.exit(1)
@@ -123,11 +126,18 @@ def main():
123126
if description and description not in body:
124127
content = f"{description}\n\n{body}"
125128

129+
# The native ``name`` becomes the stable, derivable entity slug so the
130+
# entity id is ``<type>/<name>`` on both sides — provenance can map an
131+
# audited native memory straight onto its mirror. Fall back to a
132+
# content-derived slug only when the native frontmatter has no name.
133+
slug = slugify(name) if name else slugify(content)
134+
126135
entity = {
127136
"type": args.type,
128137
"trigger": args.trigger,
129138
"content": content,
130139
"source": "native-memory",
140+
"native_path": args.memory_path,
131141
}
132142

133143
entities_dir = find_entities_dir()
@@ -138,9 +148,14 @@ def main():
138148
entities_dir = get_default_entities_dir()
139149
log(f"Created entities dir: {entities_dir}")
140150

141-
path = write_entity_file(entities_dir, entity)
142-
log(f"Mirrored {memory_path} -> {path}")
151+
# Deterministic, idempotent write: re-mirroring the same native memory
152+
# (same name + type) overwrites <type>/<name>.md in place rather than
153+
# creating <name>-2.md, keeping the entity id stable.
154+
path = write_entity_file(entities_dir, entity, filename=slug, overwrite=True)
155+
entity_id = f"{entity['type']}/{slug}"
156+
log(f"Mirrored {memory_path} -> {path} (id: {entity_id})")
143157
print(f"Mirrored native memory into evolve store: {path}")
158+
print(f"Entity id: {entity_id}")
144159

145160

146161
if __name__ == "__main__":

platform-integrations/claude/plugins/evolve-lite/EVOLVE.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,17 @@ once per memory you saved.
1515

1616
## Record what you consulted
1717

18-
After you read or consult memories this turn, log which ones you actually opened,
19-
so the value of this memory can be measured over time. Run:
18+
After you read or consult native memories this turn, log which ones you actually
19+
opened, so the value of this memory can be measured over time. Run:
2020

2121
```
22-
python3 ~/.claude/evolve-lite/audit_recall.py <file> [<file> ...]
22+
python3 ~/.claude/evolve-lite/audit_recall.py <id> [<id> ...]
2323
```
2424

25-
Pass the memory files you read this turn (space-separated paths, relative to the
26-
project root). Skip this step entirely if you consulted no memories. If the
27-
command prints a line beginning `evolve-session:`, include that line once,
28-
verbatim, somewhere in your reply — it lets later analysis tie this session to
29-
what you recalled.
25+
Pass the entity id `<type>/<name>` for each native memory you consulted, where
26+
`<type>` is the memory's `metadata.type` and `<name>` is its top-level `name`
27+
field — this is the id provenance resolves to `./.evolve/entities/<type>/<name>.md`
28+
(the same id /evolve-lite:adapt-memory mirrors to). Skip this step entirely
29+
if you consulted no memories. If the command prints a line beginning
30+
`evolve-session:`, include that line once, verbatim, somewhere in your reply — it
31+
lets later analysis tie this session to what you recalled.

platform-integrations/claude/plugins/evolve-lite/lib/evolve-lite/entity_io.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def unique_filename(directory, slug):
154154
# Markdown <-> dict conversion
155155
# ---------------------------------------------------------------------------
156156

157-
_FRONTMATTER_KEYS = ("type", "trigger", "trajectory", "owner", "source", "visibility", "published_at")
157+
_FRONTMATTER_KEYS = ("type", "trigger", "trajectory", "owner", "source", "native_path", "visibility", "published_at")
158158

159159

160160
def entity_to_markdown(entity):
@@ -354,12 +354,24 @@ def load_all_entities(entities_dir):
354354
return entities
355355

356356

357-
def write_entity_file(directory, entity):
357+
def write_entity_file(directory, entity, filename=None, overwrite=False):
358358
"""Write a single entity as a markdown file under *directory*.
359359
360360
The file is placed in a ``{type}/`` subdirectory. Uses atomic
361361
write (write to ``.tmp``, then ``os.rename``).
362362
363+
Args:
364+
directory: Entities root directory.
365+
entity: The entity dict to serialize.
366+
filename: Optional explicit slug for the target file (without the
367+
``.md`` suffix). When omitted, the slug is derived from the
368+
entity content (the historical default).
369+
overwrite: When True, the entity is written to a deterministic
370+
``{type}/{filename}.md`` path, overwriting any existing file in
371+
place (stable id, idempotent re-mirroring). When False (the
372+
default), the historical collision-avoiding behavior is kept —
373+
a ``-2``/``-3`` suffix is appended on collision.
374+
363375
Returns:
364376
Path to the written file.
365377
"""
@@ -370,7 +382,7 @@ def write_entity_file(directory, entity):
370382
type_dir = Path(directory) / entity_type
371383
type_dir.mkdir(parents=True, exist_ok=True)
372384

373-
slug = slugify(entity.get("content", "entity"))
385+
slug = filename if filename else slugify(entity.get("content", "entity"))
374386
content = entity_to_markdown(entity)
375387

376388
# Write to a unique temp file first (avoids predictable .tmp collisions)
@@ -381,6 +393,13 @@ def write_entity_file(directory, entity):
381393
os.close(fd)
382394
fd = None
383395

396+
if overwrite:
397+
# Deterministic target: overwrite any existing file in place so
398+
# the entity id is stable across re-mirroring.
399+
target = type_dir / f"{slug}.md"
400+
os.replace(tmp_path, target)
401+
return target
402+
384403
# Atomically claim the target using O_EXCL; retry on race
385404
while True:
386405
target = unique_filename(type_dir, slug)

platform-integrations/claude/plugins/evolve-lite/skills/evolve-lite/adapt-memory/scripts/adapt_memory.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from entity_io import ( # noqa: E402
4949
find_entities_dir,
5050
get_default_entities_dir,
51+
slugify,
5152
write_entity_file,
5253
log as _log,
5354
)
@@ -58,14 +59,15 @@ def log(message):
5859

5960

6061
def parse_native_memory(text):
61-
"""Split a native memory file into (description, body).
62+
"""Split a native memory file into (name, description, body).
6263
6364
Native frontmatter is simple ``key: value`` lines plus a nested
64-
``metadata:`` block; we only need ``description`` and the body, so we
65-
parse the top-level ``description:`` line and treat everything after the
66-
closing ``---`` as the body. Missing frontmatter is tolerated — the whole
67-
text is then the body.
65+
``metadata:`` block; we parse the top-level ``name`` and ``description``
66+
lines and treat everything after the closing ``---`` as the body. The
67+
``name`` is the native slug we reuse as the stable entity id. Missing
68+
frontmatter is tolerated — the whole text is then the body.
6869
"""
70+
name = None
6971
description = None
7072
body = text
7173
if text.startswith("---"):
@@ -74,16 +76,17 @@ def parse_native_memory(text):
7476
frontmatter, body = parts[1], parts[2]
7577
for line in frontmatter.splitlines():
7678
# Only top-level keys (no leading indentation) — keeps the
77-
# nested metadata.* keys out of the description match.
79+
# nested metadata.* keys out of the top-level matches.
7880
if line[:1].isspace():
7981
continue
8082
key, _, value = line.partition(":")
81-
if key.strip() == "description":
82-
value = value.strip()
83-
if value:
84-
description = value
85-
break
86-
return description, body.strip()
83+
key = key.strip()
84+
value = value.strip()
85+
if key == "name" and value:
86+
name = value
87+
elif key == "description" and value:
88+
description = value
89+
return name, description, body.strip()
8790

8891

8992
def main():
@@ -112,7 +115,7 @@ def main():
112115
print(f"Error: cannot read {memory_path} - {exc}", file=sys.stderr)
113116
sys.exit(1)
114117

115-
description, body = parse_native_memory(text)
118+
name, description, body = parse_native_memory(text)
116119
if not body:
117120
print(f"Error: native memory {memory_path} has no body to mirror.", file=sys.stderr)
118121
sys.exit(1)
@@ -123,11 +126,18 @@ def main():
123126
if description and description not in body:
124127
content = f"{description}\n\n{body}"
125128

129+
# The native ``name`` becomes the stable, derivable entity slug so the
130+
# entity id is ``<type>/<name>`` on both sides — provenance can map an
131+
# audited native memory straight onto its mirror. Fall back to a
132+
# content-derived slug only when the native frontmatter has no name.
133+
slug = slugify(name) if name else slugify(content)
134+
126135
entity = {
127136
"type": args.type,
128137
"trigger": args.trigger,
129138
"content": content,
130139
"source": "native-memory",
140+
"native_path": args.memory_path,
131141
}
132142

133143
entities_dir = find_entities_dir()
@@ -138,9 +148,14 @@ def main():
138148
entities_dir = get_default_entities_dir()
139149
log(f"Created entities dir: {entities_dir}")
140150

141-
path = write_entity_file(entities_dir, entity)
142-
log(f"Mirrored {memory_path} -> {path}")
151+
# Deterministic, idempotent write: re-mirroring the same native memory
152+
# (same name + type) overwrites <type>/<name>.md in place rather than
153+
# creating <name>-2.md, keeping the entity id stable.
154+
path = write_entity_file(entities_dir, entity, filename=slug, overwrite=True)
155+
entity_id = f"{entity['type']}/{slug}"
156+
log(f"Mirrored {memory_path} -> {path} (id: {entity_id})")
143157
print(f"Mirrored native memory into evolve store: {path}")
158+
print(f"Entity id: {entity_id}")
144159

145160

146161
if __name__ == "__main__":

platform-integrations/claw-code/plugins/evolve-lite/lib/evolve-lite/entity_io.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def unique_filename(directory, slug):
154154
# Markdown <-> dict conversion
155155
# ---------------------------------------------------------------------------
156156

157-
_FRONTMATTER_KEYS = ("type", "trigger", "trajectory", "owner", "source", "visibility", "published_at")
157+
_FRONTMATTER_KEYS = ("type", "trigger", "trajectory", "owner", "source", "native_path", "visibility", "published_at")
158158

159159

160160
def entity_to_markdown(entity):
@@ -354,12 +354,24 @@ def load_all_entities(entities_dir):
354354
return entities
355355

356356

357-
def write_entity_file(directory, entity):
357+
def write_entity_file(directory, entity, filename=None, overwrite=False):
358358
"""Write a single entity as a markdown file under *directory*.
359359
360360
The file is placed in a ``{type}/`` subdirectory. Uses atomic
361361
write (write to ``.tmp``, then ``os.rename``).
362362
363+
Args:
364+
directory: Entities root directory.
365+
entity: The entity dict to serialize.
366+
filename: Optional explicit slug for the target file (without the
367+
``.md`` suffix). When omitted, the slug is derived from the
368+
entity content (the historical default).
369+
overwrite: When True, the entity is written to a deterministic
370+
``{type}/{filename}.md`` path, overwriting any existing file in
371+
place (stable id, idempotent re-mirroring). When False (the
372+
default), the historical collision-avoiding behavior is kept —
373+
a ``-2``/``-3`` suffix is appended on collision.
374+
363375
Returns:
364376
Path to the written file.
365377
"""
@@ -370,7 +382,7 @@ def write_entity_file(directory, entity):
370382
type_dir = Path(directory) / entity_type
371383
type_dir.mkdir(parents=True, exist_ok=True)
372384

373-
slug = slugify(entity.get("content", "entity"))
385+
slug = filename if filename else slugify(entity.get("content", "entity"))
374386
content = entity_to_markdown(entity)
375387

376388
# Write to a unique temp file first (avoids predictable .tmp collisions)
@@ -381,6 +393,13 @@ def write_entity_file(directory, entity):
381393
os.close(fd)
382394
fd = None
383395

396+
if overwrite:
397+
# Deterministic target: overwrite any existing file in place so
398+
# the entity id is stable across re-mirroring.
399+
target = type_dir / f"{slug}.md"
400+
os.replace(tmp_path, target)
401+
return target
402+
384403
# Atomically claim the target using O_EXCL; retry on race
385404
while True:
386405
target = unique_filename(type_dir, slug)

0 commit comments

Comments
 (0)