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.
@@ -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 ["summary output must be an object"]
550
+ return errors
550
551
  for field in ("decisions", "shared_decisions"):
551
- for index, item in enumerate(payload.get(field) or []):
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 not summary:
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
- for index, item in enumerate(payload.get(field) or []):
564
- if not isinstance(item, str) or not item.strip():
565
- errors.append(f"{field}[{index}] must be a non-empty string")
566
- elif _looks_like_placeholder_text(item):
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dev-memory-cli",
3
- "version": "0.26.0",
3
+ "version": "0.26.1",
4
4
  "description": "CLI for dev-memory hooks and repo-local setup (formerly @xluos/dev-assets-cli)",
5
5
  "license": "MIT",
6
6
  "repository": {