dev-memory-cli 0.26.0 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/dev_memory_capture.py +127 -3
- package/lib/dev_memory_session_scan.py +13 -14
- package/package.json +1 -1
|
@@ -836,6 +836,129 @@ KIND_MAP = {
|
|
|
836
836
|
"pending": {"file": "pending_promotion", "section": "候选条目", "default_mode": "append"},
|
|
837
837
|
}
|
|
838
838
|
|
|
839
|
+
SUMMARY_OUTPUT_ALLOWED_FIELDS = {
|
|
840
|
+
"title",
|
|
841
|
+
"file_map",
|
|
842
|
+
"decisions",
|
|
843
|
+
"risks",
|
|
844
|
+
"glossary",
|
|
845
|
+
"shared_decisions",
|
|
846
|
+
"shared_context",
|
|
847
|
+
"shared_sources",
|
|
848
|
+
"upserts",
|
|
849
|
+
"appends",
|
|
850
|
+
"rewrites",
|
|
851
|
+
"deletes",
|
|
852
|
+
"skip_reason",
|
|
853
|
+
}
|
|
854
|
+
SUMMARY_OUTPUT_ARRAY_FIELDS = (
|
|
855
|
+
"file_map",
|
|
856
|
+
"decisions",
|
|
857
|
+
"risks",
|
|
858
|
+
"glossary",
|
|
859
|
+
"shared_decisions",
|
|
860
|
+
"shared_context",
|
|
861
|
+
"shared_sources",
|
|
862
|
+
"upserts",
|
|
863
|
+
"appends",
|
|
864
|
+
"rewrites",
|
|
865
|
+
"deletes",
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def summary_output_schema_errors(payload):
|
|
870
|
+
"""Validate the complete summary payload before any memory write starts."""
|
|
871
|
+
if not isinstance(payload, dict):
|
|
872
|
+
return ["summary output must be an object"]
|
|
873
|
+
|
|
874
|
+
errors = []
|
|
875
|
+
unknown = sorted(set(payload) - SUMMARY_OUTPUT_ALLOWED_FIELDS)
|
|
876
|
+
if unknown:
|
|
877
|
+
errors.append(f"unsupported summary output fields: {', '.join(unknown)}")
|
|
878
|
+
|
|
879
|
+
for field in ("title", "skip_reason"):
|
|
880
|
+
if field in payload and (
|
|
881
|
+
not isinstance(payload[field], str) or not payload[field].strip()
|
|
882
|
+
):
|
|
883
|
+
errors.append(f"{field} must be a non-empty string")
|
|
884
|
+
|
|
885
|
+
arrays = {}
|
|
886
|
+
for field in SUMMARY_OUTPUT_ARRAY_FIELDS:
|
|
887
|
+
if field not in payload:
|
|
888
|
+
arrays[field] = []
|
|
889
|
+
elif not isinstance(payload[field], list):
|
|
890
|
+
errors.append(f"{field} must be an array")
|
|
891
|
+
arrays[field] = []
|
|
892
|
+
else:
|
|
893
|
+
arrays[field] = payload[field]
|
|
894
|
+
|
|
895
|
+
for field in ("decisions", "shared_decisions"):
|
|
896
|
+
for index, item in enumerate(arrays[field]):
|
|
897
|
+
if isinstance(item, str):
|
|
898
|
+
if not item.strip():
|
|
899
|
+
errors.append(f"{field}[{index}] requires a non-empty summary")
|
|
900
|
+
continue
|
|
901
|
+
if not isinstance(item, dict):
|
|
902
|
+
errors.append(f"{field}[{index}] must be an object or non-empty string")
|
|
903
|
+
continue
|
|
904
|
+
summary = item.get("summary", item.get("decision"))
|
|
905
|
+
if not isinstance(summary, str) or not summary.strip():
|
|
906
|
+
errors.append(f"{field}[{index}] requires a non-empty summary")
|
|
907
|
+
for key in ("reason", "impact"):
|
|
908
|
+
if key in item and not isinstance(item[key], str):
|
|
909
|
+
errors.append(f"{field}[{index}].{key} must be a string")
|
|
910
|
+
|
|
911
|
+
for field in ("risks", "glossary", "shared_context", "shared_sources"):
|
|
912
|
+
for index, item in enumerate(arrays[field]):
|
|
913
|
+
if not isinstance(item, str) or not item.strip():
|
|
914
|
+
errors.append(f"{field}[{index}] must be a non-empty string")
|
|
915
|
+
|
|
916
|
+
for index, item in enumerate(arrays["file_map"]):
|
|
917
|
+
if not isinstance(item, dict):
|
|
918
|
+
errors.append(f"file_map[{index}] must be an object")
|
|
919
|
+
continue
|
|
920
|
+
label = item.get("label")
|
|
921
|
+
paths = item.get("paths")
|
|
922
|
+
if not isinstance(label, str) or not label.strip():
|
|
923
|
+
errors.append(f"file_map[{index}].label must be a non-empty string")
|
|
924
|
+
if not isinstance(paths, list) or not paths:
|
|
925
|
+
errors.append(f"file_map[{index}].paths must be a non-empty string array")
|
|
926
|
+
elif any(not isinstance(path, str) or not path.strip() for path in paths):
|
|
927
|
+
errors.append(f"file_map[{index}].paths must contain only non-empty strings")
|
|
928
|
+
|
|
929
|
+
for field in ("upserts", "appends"):
|
|
930
|
+
for index, item in enumerate(arrays[field]):
|
|
931
|
+
if not isinstance(item, dict):
|
|
932
|
+
errors.append(f"{field}[{index}] must be an object")
|
|
933
|
+
continue
|
|
934
|
+
kind = item.get("kind")
|
|
935
|
+
content = item.get("content")
|
|
936
|
+
if not isinstance(kind, str) or kind not in KIND_MAP:
|
|
937
|
+
errors.append(f"{field}[{index}].kind must be one of: {', '.join(sorted(KIND_MAP))}")
|
|
938
|
+
if not isinstance(content, str) or not content.strip():
|
|
939
|
+
errors.append(f"{field}[{index}].content must be a non-empty string")
|
|
940
|
+
|
|
941
|
+
for index, item in enumerate(arrays["rewrites"]):
|
|
942
|
+
if not isinstance(item, dict):
|
|
943
|
+
errors.append(f"rewrites[{index}] must be an object")
|
|
944
|
+
continue
|
|
945
|
+
for key in ("id", "content", "reason"):
|
|
946
|
+
if not isinstance(item.get(key), str) or not item[key].strip():
|
|
947
|
+
errors.append(f"rewrites[{index}].{key} must be a non-empty string")
|
|
948
|
+
|
|
949
|
+
for index, item in enumerate(arrays["deletes"]):
|
|
950
|
+
if not isinstance(item, dict):
|
|
951
|
+
errors.append(f"deletes[{index}] must be an object")
|
|
952
|
+
continue
|
|
953
|
+
for key in ("id", "reason"):
|
|
954
|
+
if not isinstance(item.get(key), str) or not item[key].strip():
|
|
955
|
+
errors.append(f"deletes[{index}].{key} must be a non-empty string")
|
|
956
|
+
|
|
957
|
+
if isinstance(payload.get("skip_reason"), str) and payload["skip_reason"].strip():
|
|
958
|
+
if any(arrays[field] for field in SUMMARY_OUTPUT_ARRAY_FIELDS):
|
|
959
|
+
errors.append("skip_reason must not be set when memory mutations are present")
|
|
960
|
+
return errors
|
|
961
|
+
|
|
839
962
|
|
|
840
963
|
# Worktree write-back deliberately mirrors only append-style knowledge. Snapshot
|
|
841
964
|
# fields like progress/overview/next represent the current branch state and would
|
|
@@ -1848,13 +1971,14 @@ def command_delete_entry(args):
|
|
|
1848
1971
|
|
|
1849
1972
|
|
|
1850
1973
|
def command_apply_summary_output(args):
|
|
1974
|
+
payload = _load_json_payload(args.json, args.json_file)
|
|
1975
|
+
validation_errors = summary_output_schema_errors(payload)
|
|
1976
|
+
if validation_errors:
|
|
1977
|
+
raise RuntimeError("invalid summary output: " + "; ".join(validation_errors))
|
|
1851
1978
|
repo_root, branch_name, branch_key, storage_root, repo_key, repo_dir, branch_dir, paths = ensure_branch_paths_exist(
|
|
1852
1979
|
args.repo, args.context_dir, args.branch
|
|
1853
1980
|
)
|
|
1854
1981
|
worktree_writeback = _worktree_writeback_context(repo_root, repo_dir, paths, branch_name)
|
|
1855
|
-
payload = _load_json_payload(args.json, args.json_file)
|
|
1856
|
-
if not isinstance(payload, dict):
|
|
1857
|
-
raise RuntimeError("summary output must be a JSON object")
|
|
1858
1982
|
|
|
1859
1983
|
touched = []
|
|
1860
1984
|
actions = []
|
|
@@ -16,6 +16,7 @@ import time
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
18
|
from dev_memory_common import get_branch_paths, list_repos_in_workspace, now_iso
|
|
19
|
+
from dev_memory_capture import summary_output_schema_errors
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
SCHEMA_VERSION = 1
|
|
@@ -544,31 +545,29 @@ def _looks_like_placeholder_text(value):
|
|
|
544
545
|
|
|
545
546
|
|
|
546
547
|
def _summary_payload_validation_errors(payload):
|
|
547
|
-
errors =
|
|
548
|
+
errors = summary_output_schema_errors(payload)
|
|
548
549
|
if not isinstance(payload, dict):
|
|
549
|
-
return
|
|
550
|
+
return errors
|
|
550
551
|
for field in ("decisions", "shared_decisions"):
|
|
551
|
-
|
|
552
|
+
value = payload.get(field)
|
|
553
|
+
if value is not None and not isinstance(value, list):
|
|
554
|
+
continue
|
|
555
|
+
for index, item in enumerate(value or []):
|
|
552
556
|
if isinstance(item, str):
|
|
553
557
|
summary = item.strip()
|
|
554
558
|
elif isinstance(item, dict):
|
|
555
559
|
summary = str(item.get("summary") or item.get("decision") or "").strip()
|
|
556
560
|
else:
|
|
557
561
|
summary = ""
|
|
558
|
-
if
|
|
559
|
-
errors.append(f"{field}[{index}] requires a non-empty summary")
|
|
560
|
-
elif _looks_like_placeholder_text(summary):
|
|
562
|
+
if summary and _looks_like_placeholder_text(summary):
|
|
561
563
|
errors.append(f"{field}[{index}] contains schema placeholder text")
|
|
562
564
|
for field in ("risks", "glossary", "shared_context", "shared_sources"):
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
565
|
+
value = payload.get(field)
|
|
566
|
+
if value is not None and not isinstance(value, list):
|
|
567
|
+
continue
|
|
568
|
+
for index, item in enumerate(value or []):
|
|
569
|
+
if isinstance(item, str) and item.strip() and _looks_like_placeholder_text(item):
|
|
567
570
|
errors.append(f"{field}[{index}] contains schema placeholder text")
|
|
568
|
-
for index, item in enumerate(payload.get("file_map") or []):
|
|
569
|
-
paths = item.get("paths") if isinstance(item, dict) else None
|
|
570
|
-
if not isinstance(item, dict) or not str(item.get("label") or "").strip() or not paths:
|
|
571
|
-
errors.append(f"file_map[{index}] requires label and paths")
|
|
572
571
|
return errors
|
|
573
572
|
|
|
574
573
|
|