From b2f11e89871a5d56596f99713daeed0570ce4963 Mon Sep 17 00:00:00 2001 From: Florian DAVID Date: Tue, 23 Jun 2026 08:28:28 +0200 Subject: [PATCH] fix(config): strip _-prefixed doc keys from merged config (#64) _comments/_purpose/_notes from example configs flowed through the jq merge into REMEMBER_CONFIG. Filter top-level _* keys post-merge so future schema/unknown-key validation isn't tripped. Convention: _* are user-facing docs, never runtime data. Co-Authored-By: Max --- scripts/lib-memory-dir.sh | 5 +++-- tests/test_layered_config.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/scripts/lib-memory-dir.sh b/scripts/lib-memory-dir.sh index b8f83f4..4978fd2 100644 --- a/scripts/lib-memory-dir.sh +++ b/scripts/lib-memory-dir.sh @@ -129,8 +129,9 @@ _cfg_sources=() [ -f "$_project_cfg" ] && _cfg_sources+=("$_project_cfg") if [ "${#_cfg_sources[@]}" -gt 0 ] && command -v jq >/dev/null 2>&1; then - # Deep-merge: later files override earlier ones. - jq -s 'reduce .[] as $x ({}; . * $x)' "${_cfg_sources[@]}" > "$_merged_cfg" 2>/dev/null \ + # Deep-merge: later files override earlier ones. Strip `_`-prefixed keys — + # convention: `_*` are user-facing docs (_comments/_purpose/_notes), never runtime data. + jq -s 'reduce .[] as $x ({}; . * $x) | with_entries(select(.key | startswith("_") | not))' "${_cfg_sources[@]}" > "$_merged_cfg" 2>/dev/null \ || cp "$_bundled_cfg" "$_merged_cfg" 2>/dev/null else # No jq, or no config files — fall back to the bundled defaults. diff --git a/tests/test_layered_config.py b/tests/test_layered_config.py index 2543c99..cb6e449 100644 --- a/tests/test_layered_config.py +++ b/tests/test_layered_config.py @@ -40,8 +40,10 @@ def _run_lib(project_dir: str, pipeline_dir: str, home_dir: str, env_extra: "dic if [ -f "$REMEMBER_CONFIG" ] && command -v jq >/dev/null 2>&1; then SAVE_SEC=$(jq -r '.cooldowns.save_seconds // "absent"' "$REMEMBER_CONFIG") DATA_DIR=$(jq -r '.data_dir // "absent"' "$REMEMBER_CONFIG") + UNDERSCORE_KEYS=$(jq -r '[keys[] | select(startswith("_"))] | length' "$REMEMBER_CONFIG") echo "MERGED_SAVE_SECONDS=$SAVE_SEC" echo "MERGED_DATA_DIR=$DATA_DIR" + echo "MERGED_UNDERSCORE_KEYS=$UNDERSCORE_KEYS" fi """ env = {**os.environ, **(env_extra or {})} @@ -162,6 +164,26 @@ def test_per_project_overrides_user_global(self, tmp_path): result = _run_lib(str(project), str(pipeline), str(home)) assert result.get("MERGED_SAVE_SECONDS") == "999" + def test_underscore_keys_stripped(self, tmp_path): + """`_`-prefixed doc keys (e.g. _comments, _purpose) never reach the merged config.""" + project = tmp_path / "proj" + project.mkdir() + pipeline = tmp_path / "plugin" + pipeline.mkdir() + home = tmp_path / "home" + (home / ".remember").mkdir(parents=True) + + (pipeline / "config.json").write_text( + json.dumps({"_comments": {"a": "doc"}, "cooldowns": {"save_seconds": 99}}) + ) + (home / ".remember" / "config.json").write_text( + json.dumps({"_purpose": "doc", "_notes": ["x"], "cooldowns": {"save_seconds": 200}}) + ) + + result = _run_lib(str(project), str(pipeline), str(home)) + assert result.get("MERGED_UNDERSCORE_KEYS") == "0" + assert result.get("MERGED_SAVE_SECONDS") == "200" + def test_missing_user_global_skipped(self, tmp_path): """Missing user-global file does not cause an error; bundled values are used.""" project = tmp_path / "proj"