context-keeper-mcp 0.1.0__tar.gz

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.
@@ -0,0 +1,2 @@
1
+ __pycache__/
2
+ .context/
@@ -0,0 +1,60 @@
1
+ # Context Keeper MCP Server
2
+
3
+ Context Keeper maintains project memory across Claude conversations: architectural decisions, pipeline flows, and constraints that must not be forgotten or violated.
4
+
5
+ ## When to Record
6
+
7
+ ### Record a Decision when:
8
+ - You and the user choose between multiple approaches
9
+ - A technical trade-off is made (e.g., "JSON over SQLite because human-editable")
10
+ - A library, pattern, or architecture is selected
11
+ - The user says "let's go with X" after discussing options
12
+
13
+ Call `record_decision` with summary, rationale, and alternatives considered. Always include constraints_created if the decision limits future choices.
14
+
15
+ ### Record a Pipeline when:
16
+ - A multi-step workflow is established (build, deploy, data processing)
17
+ - Steps have ordering dependencies (A must happen before B)
18
+ - The user describes "the flow" or "the process"
19
+
20
+ Call `record_pipeline` with ordered steps. Include constraints like "never skip step 2" or "step 3 requires output from step 1."
21
+
22
+ ### Record a Constraint when:
23
+ - The user says "never do X" or "always do Y"
24
+ - A gotcha or footgun is discovered ("running from source breaks the scheduler")
25
+ - A project convention is established ("all API responses use camelCase")
26
+ - An external requirement exists ("must support Python 3.12+")
27
+
28
+ Call `record_constraint` with the rule, reason, scope, and hardness. Use hardness=absolute for true invariants, advisory for preferences.
29
+
30
+ ## When to Retrieve
31
+
32
+ ### At conversation start:
33
+ 1. Call `get_compaction_report` first. If the report shows discrepancies (missing or modified entries), surface them to the user before doing anything else. Missing entries may need to be re-recorded.
34
+ 2. Then call `get_project_summary` to orient yourself on the project's decisions, pipelines, and constraints. This prevents you from suggesting changes that violate established patterns.
35
+
36
+ ### Before making architectural changes:
37
+ Call `get_context` with tags or a query describing what you're about to change. Check for conflicting decisions or constraints before proposing changes.
38
+
39
+ ### When the user asks "why did we...":
40
+ Call `get_context` with relevant tags to find the decision with its rationale.
41
+
42
+ ### Before modifying a pipeline:
43
+ Call `get_context` with the pipeline name or tags to see the current flow and its constraints.
44
+
45
+ ## When NOT to Record
46
+ - Trivial implementation details (variable names, formatting choices)
47
+ - Temporary workarounds that will be removed
48
+ - Information already in the code comments or README
49
+ - One-off debugging steps
50
+
51
+ ## Staleness Management
52
+ Periodically (every few sessions or when the user asks), call `prune_stale` to find entries that haven't been verified recently. Present stale entries to the user and ask: "Is this still accurate?" Then either:
53
+ - Call `update_entry` to refresh verified_at (confirming it's still valid)
54
+ - Call `deprecate_entry` if it's no longer relevant
55
+
56
+ ## Tags Convention
57
+ Use lowercase, hyphen-separated tags. Common categories:
58
+ - Component names: auth, api, database, ui, deployment
59
+ - Cross-cutting: architecture, security, performance, testing
60
+ - Project names: skillmatch, conductor (for cross-project references)
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonathan Armstrong
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.4
2
+ Name: context-keeper-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server that maintains project context (decisions, pipelines, constraints) across Claude conversations
5
+ Project-URL: Homepage, https://github.com/jarmstrong158/context-keeper
6
+ Project-URL: Repository, https://github.com/jarmstrong158/context-keeper
7
+ Project-URL: Issues, https://github.com/jarmstrong158/context-keeper/issues
8
+ Author-email: Jonathan Armstrong <jarmstrong158@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: claude,context,decisions,mcp,memory,model-context-protocol
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+
24
+ <!-- mcp-name: io.github.jarmstrong158/context-keeper -->
25
+
26
+ # Context Keeper
27
+
28
+ Project memory for Claude. Records design decisions, pipeline flows, and constraints so Claude maintains context across conversations.
29
+
30
+ ## The Problem
31
+
32
+ As conversations get long, Claude loses the "why" behind earlier decisions. New conversations start blank. This causes Claude to make changes that break established patterns — like rewriting a pipeline step it doesn't remember exists.
33
+
34
+ ## The Solution
35
+
36
+ Context Keeper gives Claude 8 tools to record and retrieve structured project context:
37
+
38
+ | Tool | Purpose |
39
+ |------|---------|
40
+ | `record_decision` | Save a decision with rationale and alternatives |
41
+ | `record_pipeline` | Save a multi-step workflow with ordering |
42
+ | `record_constraint` | Save a rule with scope and enforcement level |
43
+ | `get_context` | Retrieve relevant entries by query, tags, scope, or ID |
44
+ | `get_project_summary` | Compact overview for conversation start |
45
+ | `update_entry` | Update any entry by ID |
46
+ | `deprecate_entry` | Retire an entry with reason |
47
+ | `prune_stale` | Find entries not verified recently |
48
+ | `get_compaction_report` | Check if last compaction lost any context |
49
+
50
+ All data stored as human-editable JSON files in `.context/` inside your project directory. Zero external dependencies.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install context-keeper
56
+ ```
57
+
58
+ ### Claude Code
59
+
60
+ ```bash
61
+ claude mcp add --scope user context-keeper -- python /path/to/context-keeper/server.py
62
+ ```
63
+
64
+ ### Claude Desktop
65
+
66
+ Add to your `claude_desktop_config.json`:
67
+
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "context-keeper": {
72
+ "command": "python",
73
+ "args": ["/path/to/context-keeper/server.py"],
74
+ "env": {
75
+ "CONTEXT_KEEPER_PROJECT": "/path/to/your/project"
76
+ }
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ Set `CONTEXT_KEEPER_PROJECT` to the root of your project. If omitted, it uses the current working directory.
83
+
84
+ ## How It Works
85
+
86
+ ### Recording Context
87
+
88
+ When you make a design decision:
89
+ ```
90
+ You: Let's use JSON files instead of SQLite for storage.
91
+ Claude: [calls record_decision with summary, rationale, and alternatives]
92
+ ```
93
+
94
+ When you establish a workflow:
95
+ ```
96
+ You: The deploy pipeline is: run tests, build, push to registry, deploy.
97
+ Claude: [calls record_pipeline with ordered steps]
98
+ ```
99
+
100
+ When you set a rule:
101
+ ```
102
+ You: Never run Conductor from source. Always use the exe.
103
+ Claude: [calls record_constraint with rule, reason, and hardness=absolute]
104
+ ```
105
+
106
+ ### Retrieving Context
107
+
108
+ At conversation start, Claude calls `get_project_summary` to see all active decisions, pipelines, and constraints. Before making changes, it calls `get_context` with relevant tags to check for conflicts.
109
+
110
+ ### Relevance Scoring
111
+
112
+ Without embeddings or external services, Context Keeper scores entries using:
113
+ - **Tag match** — overlap between query and entry tags
114
+ - **Text match** — query words found in summary/rationale/rule text
115
+ - **Recency** — recently verified entries score higher
116
+ - **Status** — active entries prioritized over superseded
117
+
118
+ Results are capped by a configurable token budget (default: 4000 tokens).
119
+
120
+ ## Claude Code Hook Setup
121
+
122
+ Context Keeper includes hooks that snapshot your context before Claude Code compaction and detect if anything was lost afterward.
123
+
124
+ Add to your Claude Code hooks config (`~/.claude/settings.json`):
125
+
126
+ ```json
127
+ {
128
+ "hooks": {
129
+ "PreCompact": [
130
+ {
131
+ "matcher": "",
132
+ "hooks": [
133
+ {
134
+ "type": "command",
135
+ "command": "python /path/to/context-keeper/hooks/pre_compact.py"
136
+ }
137
+ ]
138
+ }
139
+ ],
140
+ "Stop": [
141
+ {
142
+ "matcher": "",
143
+ "hooks": [
144
+ {
145
+ "type": "command",
146
+ "command": "python /path/to/context-keeper/hooks/post_compact.py"
147
+ }
148
+ ]
149
+ }
150
+ ]
151
+ }
152
+ }
153
+ ```
154
+
155
+ Replace `/path/to/context-keeper` with the actual install path. Set `CONTEXT_KEEPER_PROJECT` env var if your project isn't in the current working directory.
156
+
157
+ At session start, Claude calls `get_compaction_report` to check if the last compaction lost any context entries. If discrepancies are found, they're surfaced before any work begins.
158
+
159
+ ## Data Storage
160
+
161
+ ```
162
+ your-project/
163
+ .context/
164
+ decisions.json # Design decisions with rationale
165
+ pipelines.json # Multi-step workflows
166
+ constraints.json # Rules and invariants
167
+ config.json # Token budget, stale threshold
168
+ compaction_snapshot.json # Pre-compaction snapshot (auto-generated)
169
+ compaction_report.json # Post-compaction diff report (auto-generated)
170
+ hook.log # Hook activity log
171
+ ```
172
+
173
+ All files are human-readable JSON. You can edit them directly. IDs are sequential and readable: `dec-001`, `pipe-001`, `con-001`.
174
+
175
+ ## Configuration
176
+
177
+ Create `.context/config.json` to customize:
178
+
179
+ ```json
180
+ {
181
+ "project_name": "my-project",
182
+ "token_budget": 4000,
183
+ "max_entry_tokens": 1000,
184
+ "stale_threshold_days": 30
185
+ }
186
+ ```
187
+
188
+ ## Cross-Project Context
189
+
190
+ Query another project's context by passing `project_dir`:
191
+
192
+ ```
193
+ Claude: [calls get_context with project_dir="/path/to/other-project"]
194
+ ```
195
+
196
+ Or tag entries with other project names for cross-referencing.
@@ -0,0 +1,173 @@
1
+ <!-- mcp-name: io.github.jarmstrong158/context-keeper -->
2
+
3
+ # Context Keeper
4
+
5
+ Project memory for Claude. Records design decisions, pipeline flows, and constraints so Claude maintains context across conversations.
6
+
7
+ ## The Problem
8
+
9
+ As conversations get long, Claude loses the "why" behind earlier decisions. New conversations start blank. This causes Claude to make changes that break established patterns — like rewriting a pipeline step it doesn't remember exists.
10
+
11
+ ## The Solution
12
+
13
+ Context Keeper gives Claude 8 tools to record and retrieve structured project context:
14
+
15
+ | Tool | Purpose |
16
+ |------|---------|
17
+ | `record_decision` | Save a decision with rationale and alternatives |
18
+ | `record_pipeline` | Save a multi-step workflow with ordering |
19
+ | `record_constraint` | Save a rule with scope and enforcement level |
20
+ | `get_context` | Retrieve relevant entries by query, tags, scope, or ID |
21
+ | `get_project_summary` | Compact overview for conversation start |
22
+ | `update_entry` | Update any entry by ID |
23
+ | `deprecate_entry` | Retire an entry with reason |
24
+ | `prune_stale` | Find entries not verified recently |
25
+ | `get_compaction_report` | Check if last compaction lost any context |
26
+
27
+ All data stored as human-editable JSON files in `.context/` inside your project directory. Zero external dependencies.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install context-keeper
33
+ ```
34
+
35
+ ### Claude Code
36
+
37
+ ```bash
38
+ claude mcp add --scope user context-keeper -- python /path/to/context-keeper/server.py
39
+ ```
40
+
41
+ ### Claude Desktop
42
+
43
+ Add to your `claude_desktop_config.json`:
44
+
45
+ ```json
46
+ {
47
+ "mcpServers": {
48
+ "context-keeper": {
49
+ "command": "python",
50
+ "args": ["/path/to/context-keeper/server.py"],
51
+ "env": {
52
+ "CONTEXT_KEEPER_PROJECT": "/path/to/your/project"
53
+ }
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ Set `CONTEXT_KEEPER_PROJECT` to the root of your project. If omitted, it uses the current working directory.
60
+
61
+ ## How It Works
62
+
63
+ ### Recording Context
64
+
65
+ When you make a design decision:
66
+ ```
67
+ You: Let's use JSON files instead of SQLite for storage.
68
+ Claude: [calls record_decision with summary, rationale, and alternatives]
69
+ ```
70
+
71
+ When you establish a workflow:
72
+ ```
73
+ You: The deploy pipeline is: run tests, build, push to registry, deploy.
74
+ Claude: [calls record_pipeline with ordered steps]
75
+ ```
76
+
77
+ When you set a rule:
78
+ ```
79
+ You: Never run Conductor from source. Always use the exe.
80
+ Claude: [calls record_constraint with rule, reason, and hardness=absolute]
81
+ ```
82
+
83
+ ### Retrieving Context
84
+
85
+ At conversation start, Claude calls `get_project_summary` to see all active decisions, pipelines, and constraints. Before making changes, it calls `get_context` with relevant tags to check for conflicts.
86
+
87
+ ### Relevance Scoring
88
+
89
+ Without embeddings or external services, Context Keeper scores entries using:
90
+ - **Tag match** — overlap between query and entry tags
91
+ - **Text match** — query words found in summary/rationale/rule text
92
+ - **Recency** — recently verified entries score higher
93
+ - **Status** — active entries prioritized over superseded
94
+
95
+ Results are capped by a configurable token budget (default: 4000 tokens).
96
+
97
+ ## Claude Code Hook Setup
98
+
99
+ Context Keeper includes hooks that snapshot your context before Claude Code compaction and detect if anything was lost afterward.
100
+
101
+ Add to your Claude Code hooks config (`~/.claude/settings.json`):
102
+
103
+ ```json
104
+ {
105
+ "hooks": {
106
+ "PreCompact": [
107
+ {
108
+ "matcher": "",
109
+ "hooks": [
110
+ {
111
+ "type": "command",
112
+ "command": "python /path/to/context-keeper/hooks/pre_compact.py"
113
+ }
114
+ ]
115
+ }
116
+ ],
117
+ "Stop": [
118
+ {
119
+ "matcher": "",
120
+ "hooks": [
121
+ {
122
+ "type": "command",
123
+ "command": "python /path/to/context-keeper/hooks/post_compact.py"
124
+ }
125
+ ]
126
+ }
127
+ ]
128
+ }
129
+ }
130
+ ```
131
+
132
+ Replace `/path/to/context-keeper` with the actual install path. Set `CONTEXT_KEEPER_PROJECT` env var if your project isn't in the current working directory.
133
+
134
+ At session start, Claude calls `get_compaction_report` to check if the last compaction lost any context entries. If discrepancies are found, they're surfaced before any work begins.
135
+
136
+ ## Data Storage
137
+
138
+ ```
139
+ your-project/
140
+ .context/
141
+ decisions.json # Design decisions with rationale
142
+ pipelines.json # Multi-step workflows
143
+ constraints.json # Rules and invariants
144
+ config.json # Token budget, stale threshold
145
+ compaction_snapshot.json # Pre-compaction snapshot (auto-generated)
146
+ compaction_report.json # Post-compaction diff report (auto-generated)
147
+ hook.log # Hook activity log
148
+ ```
149
+
150
+ All files are human-readable JSON. You can edit them directly. IDs are sequential and readable: `dec-001`, `pipe-001`, `con-001`.
151
+
152
+ ## Configuration
153
+
154
+ Create `.context/config.json` to customize:
155
+
156
+ ```json
157
+ {
158
+ "project_name": "my-project",
159
+ "token_budget": 4000,
160
+ "max_entry_tokens": 1000,
161
+ "stale_threshold_days": 30
162
+ }
163
+ ```
164
+
165
+ ## Cross-Project Context
166
+
167
+ Query another project's context by passing `project_dir`:
168
+
169
+ ```
170
+ Claude: [calls get_context with project_dir="/path/to/other-project"]
171
+ ```
172
+
173
+ Or tag entries with other project names for cross-referencing.
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env python3
2
+ """Context Keeper — PostCompact hook.
3
+
4
+ Fires after Claude Code compaction (via Stop hook). Compares current context
5
+ state against the pre-compaction snapshot and reports any discrepancies.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import sys
11
+ from datetime import datetime, timezone
12
+
13
+ CONTEXT_DIR_NAME = ".context"
14
+ PROJECT_DIR = os.environ.get("CONTEXT_KEEPER_PROJECT", os.getcwd())
15
+ CONTEXT_DIR = os.path.join(PROJECT_DIR, CONTEXT_DIR_NAME)
16
+ SNAPSHOT_PATH = os.path.join(CONTEXT_DIR, "compaction_snapshot.json")
17
+ REPORT_PATH = os.path.join(CONTEXT_DIR, "compaction_report.json")
18
+ LOG_PATH = os.path.join(CONTEXT_DIR, "hook.log")
19
+
20
+ FILES = {
21
+ "decisions": os.path.join(CONTEXT_DIR, "decisions.json"),
22
+ "pipelines": os.path.join(CONTEXT_DIR, "pipelines.json"),
23
+ "constraints": os.path.join(CONTEXT_DIR, "constraints.json"),
24
+ }
25
+
26
+
27
+ def read_json(path):
28
+ if not os.path.exists(path):
29
+ return []
30
+ try:
31
+ with open(path, "r", encoding="utf-8") as f:
32
+ data = json.load(f)
33
+ return data if isinstance(data, list) else []
34
+ except Exception:
35
+ return []
36
+
37
+
38
+ def log(message):
39
+ try:
40
+ os.makedirs(CONTEXT_DIR, exist_ok=True)
41
+ ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
42
+ with open(LOG_PATH, "a", encoding="utf-8") as f:
43
+ f.write(f"[{ts}] {message}\n")
44
+ except Exception:
45
+ pass
46
+
47
+
48
+ def diff_entries(before, after):
49
+ """Compare two entry dicts. Returns dict of changed fields."""
50
+ changes = {}
51
+ all_keys = set(before.keys()) | set(after.keys())
52
+ skip = {"verified_at", "updated_at"} # timestamps change naturally
53
+ for key in all_keys:
54
+ if key in skip:
55
+ continue
56
+ bval = before.get(key)
57
+ aval = after.get(key)
58
+ if bval != aval:
59
+ changes[key] = {"before": bval, "after": aval}
60
+ return changes
61
+
62
+
63
+ def main():
64
+ if not os.path.exists(SNAPSHOT_PATH):
65
+ return
66
+
67
+ try:
68
+ with open(SNAPSHOT_PATH, "r", encoding="utf-8") as f:
69
+ snapshot = json.load(f)
70
+ except Exception:
71
+ log("POST_COMPACT: Could not read snapshot file")
72
+ return
73
+
74
+ missing = []
75
+ modified = []
76
+
77
+ for type_name, before_entries in snapshot.get("entries", {}).items():
78
+ path = FILES.get(type_name)
79
+ if not path:
80
+ continue
81
+
82
+ after_entries = read_json(path)
83
+ after_by_id = {e.get("id"): e for e in after_entries}
84
+
85
+ for before_entry in before_entries:
86
+ eid = before_entry.get("id")
87
+ if not eid:
88
+ continue
89
+
90
+ after_entry = after_by_id.get(eid)
91
+ if after_entry is None:
92
+ missing.append({
93
+ "type": type_name,
94
+ "entry": before_entry,
95
+ })
96
+ else:
97
+ changes = diff_entries(before_entry, after_entry)
98
+ if changes:
99
+ modified.append({
100
+ "type": type_name,
101
+ "id": eid,
102
+ "changes": changes,
103
+ })
104
+
105
+ has_discrepancies = len(missing) > 0 or len(modified) > 0
106
+ status = "discrepancies_found" if has_discrepancies else "clean"
107
+
108
+ report = {
109
+ "timestamp": datetime.now(timezone.utc).isoformat(),
110
+ "snapshot_timestamp": snapshot.get("timestamp"),
111
+ "status": status,
112
+ "missing_entries": missing,
113
+ "modified_entries": modified,
114
+ "missing_count": len(missing),
115
+ "modified_count": len(modified),
116
+ }
117
+
118
+ os.makedirs(CONTEXT_DIR, exist_ok=True)
119
+ with open(REPORT_PATH, "w", encoding="utf-8") as f:
120
+ json.dump(report, f, indent=2)
121
+
122
+ if has_discrepancies:
123
+ print(f"[Context Keeper] WARNING: Compaction discrepancies detected!", file=sys.stderr)
124
+ print(f" Missing entries: {len(missing)}", file=sys.stderr)
125
+ print(f" Modified entries: {len(modified)}", file=sys.stderr)
126
+ print(f" Report: {REPORT_PATH}", file=sys.stderr)
127
+ print(f" Call get_compaction_report to review details.", file=sys.stderr)
128
+ log(f"POST_COMPACT: DISCREPANCIES — {len(missing)} missing, {len(modified)} modified")
129
+ else:
130
+ log("POST_COMPACT: clean — no discrepancies")
131
+
132
+
133
+ if __name__ == "__main__":
134
+ main()
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env python3
2
+ """Context Keeper — PreCompact hook.
3
+
4
+ Fires before Claude Code compaction. Snapshots all active context entries
5
+ so post_compact.py can detect if anything was lost.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import sys
11
+ from datetime import datetime, timezone
12
+
13
+ CONTEXT_DIR_NAME = ".context"
14
+ PROJECT_DIR = os.environ.get("CONTEXT_KEEPER_PROJECT", os.getcwd())
15
+ CONTEXT_DIR = os.path.join(PROJECT_DIR, CONTEXT_DIR_NAME)
16
+ SNAPSHOT_PATH = os.path.join(CONTEXT_DIR, "compaction_snapshot.json")
17
+ LOG_PATH = os.path.join(CONTEXT_DIR, "hook.log")
18
+
19
+ FILES = {
20
+ "decisions": os.path.join(CONTEXT_DIR, "decisions.json"),
21
+ "pipelines": os.path.join(CONTEXT_DIR, "pipelines.json"),
22
+ "constraints": os.path.join(CONTEXT_DIR, "constraints.json"),
23
+ }
24
+
25
+
26
+ def read_json(path):
27
+ if not os.path.exists(path):
28
+ return []
29
+ try:
30
+ with open(path, "r", encoding="utf-8") as f:
31
+ data = json.load(f)
32
+ return data if isinstance(data, list) else []
33
+ except Exception:
34
+ return []
35
+
36
+
37
+ def log(message):
38
+ try:
39
+ os.makedirs(CONTEXT_DIR, exist_ok=True)
40
+ ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
41
+ with open(LOG_PATH, "a", encoding="utf-8") as f:
42
+ f.write(f"[{ts}] {message}\n")
43
+ except Exception:
44
+ pass
45
+
46
+
47
+ def main():
48
+ if not os.path.exists(CONTEXT_DIR):
49
+ return
50
+
51
+ snapshot = {
52
+ "timestamp": datetime.now(timezone.utc).isoformat(),
53
+ "entries": {},
54
+ "counts": {},
55
+ }
56
+
57
+ for type_name, path in FILES.items():
58
+ entries = read_json(path)
59
+ active = [e for e in entries if e.get("status", "active") != "deprecated"]
60
+ snapshot["entries"][type_name] = active
61
+ snapshot["counts"][type_name] = len(active)
62
+
63
+ total = sum(snapshot["counts"].values())
64
+
65
+ os.makedirs(CONTEXT_DIR, exist_ok=True)
66
+ with open(SNAPSHOT_PATH, "w", encoding="utf-8") as f:
67
+ json.dump(snapshot, f, indent=2)
68
+
69
+ counts_str = ", ".join(f"{k}={v}" for k, v in snapshot["counts"].items())
70
+ log(f"PRE_COMPACT: {total} active entries ({counts_str})")
71
+
72
+
73
+ if __name__ == "__main__":
74
+ main()