psyclaw 0.28.1 → 0.28.3
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/README.md +5 -5
- package/agents/recommended/catalog.json +1 -1
- package/dist/src/adapters/pi/extension.js +79 -25
- package/dist/src/adapters/pi/extension.js.map +1 -1
- package/dist/src/ars/keybindings.d.ts +13 -0
- package/dist/src/ars/keybindings.js +50 -0
- package/dist/src/ars/keybindings.js.map +1 -0
- package/dist/src/ars/mode-editor.d.ts +25 -0
- package/dist/src/ars/mode-editor.js +78 -0
- package/dist/src/ars/mode-editor.js.map +1 -0
- package/dist/src/ars/profile.d.ts +4 -1
- package/dist/src/ars/profile.js +23 -6
- package/dist/src/ars/profile.js.map +1 -1
- package/dist/src/chat.js +3 -0
- package/dist/src/chat.js.map +1 -1
- package/dist/src/panel/server.js +1 -1
- package/dist/src/panel/server.js.map +1 -1
- package/package.json +10 -3
- package/vendor/academic-paper-skills/LICENSE +21 -0
- package/vendor/academic-paper-skills/NOTICE.md +7 -0
- package/vendor/academic-paper-skills/PSYCLAW_SOURCE.json +14 -0
- package/vendor/academic-paper-skills/academic-paper-composer/SKILL.md +840 -0
- package/vendor/academic-paper-skills/academic-paper-composer/references/section_guides.md +675 -0
- package/vendor/academic-paper-skills/academic-paper-composer/references/writing_standards.md +629 -0
- package/vendor/academic-paper-skills/academic-paper-composer/scripts/chapter_quality_check.py +470 -0
- package/vendor/academic-paper-skills/academic-paper-composer/scripts/final_evaluation.py +550 -0
- package/vendor/academic-paper-skills/academic-paper-strategist/SKILL.md +670 -0
- package/vendor/academic-paper-skills/academic-paper-strategist/references/quality_standards.md +336 -0
- package/vendor/academic-paper-skills/academic-paper-strategist/references/search_strategy.md +459 -0
- package/vendor/academic-paper-skills/academic-paper-strategist/scripts/evaluate_samples.py +300 -0
- package/vendor/academic-paper-skills/academic-paper-strategist/scripts/gap_analysis.py +399 -0
- package/vendor/ars/pi/wrapper.js +9 -0
- package/vendor/nature-skills/LICENSE +201 -0
- package/vendor/nature-skills/NOTICE.md +7 -0
- package/vendor/nature-skills/PSYCLAW_SOURCE.json +19 -0
- package/vendor/nature-skills/skills/nature-figure/README.md +107 -0
- package/vendor/nature-skills/skills/nature-figure/README_EN.md +107 -0
- package/vendor/nature-skills/skills/nature-figure/SKILL.md +154 -0
- package/vendor/nature-skills/skills/nature-figure/agents/openai.yaml +4 -0
- package/vendor/nature-skills/skills/nature-figure/evals/evals.json +325 -0
- package/vendor/nature-skills/skills/nature-figure/manifest.yaml +103 -0
- package/vendor/nature-skills/skills/nature-figure/references/ai-graphical-abstract-workflow.md +129 -0
- package/vendor/nature-skills/skills/nature-figure/references/api.md +576 -0
- package/vendor/nature-skills/skills/nature-figure/references/asset-adaptation.md +72 -0
- package/vendor/nature-skills/skills/nature-figure/references/backend-selection.md +118 -0
- package/vendor/nature-skills/skills/nature-figure/references/chart-types.md +348 -0
- package/vendor/nature-skills/skills/nature-figure/references/common-patterns.md +371 -0
- package/vendor/nature-skills/skills/nature-figure/references/demos.md +58 -0
- package/vendor/nature-skills/skills/nature-figure/references/design-theory.md +467 -0
- package/vendor/nature-skills/skills/nature-figure/references/figure-contract.md +122 -0
- package/vendor/nature-skills/skills/nature-figure/references/figure-legend-conventions.md +90 -0
- package/vendor/nature-skills/skills/nature-figure/references/multipanel-evidence-architecture.md +268 -0
- package/vendor/nature-skills/skills/nature-figure/references/nature-2026-observations.md +124 -0
- package/vendor/nature-skills/skills/nature-figure/references/nature-article-requirements.md +149 -0
- package/vendor/nature-skills/skills/nature-figure/references/openrouter-image-generation.md +141 -0
- package/vendor/nature-skills/skills/nature-figure/references/qa-contract.md +344 -0
- package/vendor/nature-skills/skills/nature-figure/references/r-template-index.md +66 -0
- package/vendor/nature-skills/skills/nature-figure/references/r-workflow.md +234 -0
- package/vendor/nature-skills/skills/nature-figure/references/template-catalog.md +33 -0
- package/vendor/nature-skills/skills/nature-figure/references/tutorials.md +260 -0
- package/vendor/nature-skills/skills/nature-figure/requirements.txt +1 -0
- package/vendor/nature-skills/skills/nature-figure/scripts/audit_figure_collisions.py +742 -0
- package/vendor/nature-skills/skills/nature-figure/scripts/audit_panel_alignment.py +933 -0
- package/vendor/nature-skills/skills/nature-figure/scripts/audit_pdf_text.py +152 -0
- package/vendor/nature-skills/skills/nature-figure/scripts/figure_safety.py +50 -0
- package/vendor/nature-skills/skills/nature-figure/scripts/generate_openrouter_schematic.py +260 -0
- package/vendor/nature-skills/skills/nature-figure/scripts/nature_figure_backend.py +93 -0
- package/vendor/nature-skills/skills/nature-figure/scripts/panel_alignment.R +188 -0
- package/vendor/nature-skills/skills/nature-figure/scripts/plot_templates.py +604 -0
- package/vendor/nature-skills/skills/nature-figure/scripts/validate_figure.py +835 -0
- package/vendor/nature-skills/skills/nature-figure/static/core/contract.md +55 -0
- package/vendor/nature-skills/skills/nature-figure/static/core/stance.md +41 -0
- package/vendor/nature-skills/skills/nature-figure/static/fragments/backend/python.md +56 -0
- package/vendor/nature-skills/skills/nature-figure/static/fragments/backend/r.md +63 -0
- package/vendor/nature-skills/skills/nature-figure/tests/test_figure_safety.py +209 -0
- package/vendor/nature-skills/skills/nature-polishing/README.md +58 -0
- package/vendor/nature-skills/skills/nature-polishing/README_EN.md +58 -0
- package/vendor/nature-skills/skills/nature-polishing/SKILL.md +111 -0
- package/vendor/nature-skills/skills/nature-polishing/agents/openai.yaml +4 -0
- package/vendor/nature-skills/skills/nature-polishing/manifest.yaml +101 -0
- package/vendor/nature-skills/skills/nature-polishing/references/latex-layout.md +211 -0
- package/vendor/nature-skills/skills/nature-polishing/references/nat-comms-2025-diction.md +73 -0
- package/vendor/nature-skills/skills/nature-polishing/references/phrasebank-playbook.md +175 -0
- package/vendor/nature-skills/skills/nature-polishing/references/published-article-patterns.md +126 -0
- package/vendor/nature-skills/skills/nature-polishing/references/section-moves.md +252 -0
- package/vendor/nature-skills/skills/nature-polishing/references/style-guardrails.md +94 -0
- package/vendor/nature-skills/skills/nature-polishing/references/writing-strategy.md +160 -0
- package/vendor/nature-skills/skills/nature-polishing/static/core/failure-modes.md +29 -0
- package/vendor/nature-skills/skills/nature-polishing/static/core/output-format.md +21 -0
- package/vendor/nature-skills/skills/nature-polishing/static/core/stance.md +26 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/journal/generic.md +17 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/journal/nat-comms.md +38 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/journal/nat-mach-intell.md +102 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/journal/nature.md +19 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/language/en.md +19 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/language/zh-to-en.md +19 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/paper_type/algorithmic.md +18 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/paper_type/hypothesis.md +17 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/paper_type/methods.md +35 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/paper_type/research.md +29 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/paper_type/review.md +17 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/section/abstract.md +26 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/section/conclusion.md +15 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/section/discussion.md +48 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/section/intro.md +33 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/section/methods.md +26 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/section/results.md +61 -0
- package/vendor/nature-skills/skills/nature-polishing/static/fragments/section/title.md +17 -0
- package/vendor/nature-skills/skills/nature-ref-verifier/README.md +44 -0
- package/vendor/nature-skills/skills/nature-ref-verifier/README_EN.md +44 -0
- package/vendor/nature-skills/skills/nature-ref-verifier/SKILL.md +196 -0
- package/vendor/nature-skills/skills/nature-ref-verifier/agents/openai.yaml +4 -0
- package/vendor/nature-skills/skills/nature-ref-verifier/manifest.yaml +19 -0
- package/vendor/nature-skills/skills/nature-ref-verifier/references/common-patterns.md +96 -0
- package/vendor/nature-skills/skills/nature-shared/README.md +43 -0
- package/vendor/nature-skills/skills/nature-shared/README_EN.md +43 -0
- package/vendor/nature-skills/skills/nature-shared/SKILL.md +33 -0
- package/vendor/nature-skills/skills/nature-shared/agents/openai.yaml +6 -0
- package/vendor/nature-skills/skills/nature-shared/core/consistency-sweep.md +122 -0
- package/vendor/nature-skills/skills/nature-shared/core/discussion-argument-language.md +192 -0
- package/vendor/nature-skills/skills/nature-shared/core/ethics.md +88 -0
- package/vendor/nature-skills/skills/nature-shared/core/main-text-discipline.md +179 -0
- package/vendor/nature-skills/skills/nature-shared/core/nature-abstract.md +171 -0
- package/vendor/nature-skills/skills/nature-shared/core/nature-introduction.md +164 -0
- package/vendor/nature-skills/skills/nature-shared/core/nature-results-discussion.md +215 -0
- package/vendor/nature-skills/skills/nature-shared/core/paper-type-taxonomy.md +41 -0
- package/vendor/nature-skills/skills/nature-shared/core/reader-workflow.md +21 -0
- package/vendor/nature-skills/skills/nature-shared/core/research-compliance.md +192 -0
- package/vendor/nature-skills/skills/nature-shared/core/terminology-ledger.md +58 -0
- package/vendor/nature-skills/skills/nature-shared/journal-formats/nat-comms.md +139 -0
- package/vendor/nature-skills/skills/nature-shared/journal-formats/nature-machine-intelligence.md +431 -0
- package/vendor/nature-skills/skills/nature-shared/journal-formats/nature.md +313 -0
- package/vendor/nature-skills/skills/nature-shared/manifest.yaml +52 -0
- package/vendor/nature-skills/skills/nature-shared/scripts/check_consistency.py +273 -0
- package/vendor/nature-skills/skills/nature-shared/tests/test_check_consistency.py +63 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Sample Paper Evaluation Script
|
|
4
|
+
|
|
5
|
+
This script helps evaluate collected sample papers against quality standards.
|
|
6
|
+
It provides structured prompts and scoring templates for manual evaluation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import List, Dict
|
|
12
|
+
|
|
13
|
+
def create_evaluation_template() -> Dict:
|
|
14
|
+
"""Create an empty evaluation template for a sample paper."""
|
|
15
|
+
return {
|
|
16
|
+
"title": "",
|
|
17
|
+
"authors": "",
|
|
18
|
+
"publication_date": "",
|
|
19
|
+
"doi_or_url": "",
|
|
20
|
+
"relevance_score": 0, # 0-10
|
|
21
|
+
"selection_reason": "",
|
|
22
|
+
"time_category": "", # recent/current/classic
|
|
23
|
+
"quality_indicators": {
|
|
24
|
+
"has_doi": False,
|
|
25
|
+
"complete_abstract": False,
|
|
26
|
+
"proper_citations": False,
|
|
27
|
+
"platform_match": False
|
|
28
|
+
},
|
|
29
|
+
"extracted_features": {
|
|
30
|
+
"abstract_structure": "",
|
|
31
|
+
"chapter_organization": "",
|
|
32
|
+
"citation_style": "",
|
|
33
|
+
"language_style": "", # first-person/passive
|
|
34
|
+
"average_section_length": ""
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def evaluate_time_distribution(papers: List[Dict]) -> Dict:
|
|
39
|
+
"""
|
|
40
|
+
Evaluate whether time distribution meets quality standards.
|
|
41
|
+
|
|
42
|
+
Standards:
|
|
43
|
+
- Recent (last 6 months): 3 papers
|
|
44
|
+
- Current (1-2 years): 3 papers
|
|
45
|
+
- Classic (highly cited): 2 papers
|
|
46
|
+
"""
|
|
47
|
+
from datetime import datetime, timedelta
|
|
48
|
+
from dateutil.parser import parse
|
|
49
|
+
|
|
50
|
+
now = datetime.now()
|
|
51
|
+
six_months_ago = now - timedelta(days=180)
|
|
52
|
+
two_years_ago = now - timedelta(days=730)
|
|
53
|
+
|
|
54
|
+
recent = []
|
|
55
|
+
current = []
|
|
56
|
+
classic = []
|
|
57
|
+
|
|
58
|
+
for paper in papers:
|
|
59
|
+
try:
|
|
60
|
+
pub_date = parse(paper.get("publication_date", ""))
|
|
61
|
+
if pub_date >= six_months_ago:
|
|
62
|
+
recent.append(paper)
|
|
63
|
+
elif pub_date >= two_years_ago:
|
|
64
|
+
current.append(paper)
|
|
65
|
+
else:
|
|
66
|
+
classic.append(paper)
|
|
67
|
+
except:
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
"recent_count": len(recent),
|
|
72
|
+
"current_count": len(current),
|
|
73
|
+
"classic_count": len(classic),
|
|
74
|
+
"meets_standard": len(recent) >= 3 and len(current) >= 3 and len(classic) >= 2,
|
|
75
|
+
"recommendation": _get_time_distribution_recommendation(
|
|
76
|
+
len(recent), len(current), len(classic)
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def _get_time_distribution_recommendation(recent: int, current: int, classic: int) -> str:
|
|
81
|
+
"""Generate recommendation based on time distribution."""
|
|
82
|
+
issues = []
|
|
83
|
+
if recent < 3:
|
|
84
|
+
issues.append(f"Need {3-recent} more recent papers (last 6 months)")
|
|
85
|
+
if current < 3:
|
|
86
|
+
issues.append(f"Need {3-current} more current papers (1-2 years)")
|
|
87
|
+
if classic < 2:
|
|
88
|
+
issues.append(f"Need {2-classic} more classic papers (highly cited)")
|
|
89
|
+
|
|
90
|
+
if not issues:
|
|
91
|
+
return "✓ Time distribution meets standards"
|
|
92
|
+
return "⚠ " + "; ".join(issues)
|
|
93
|
+
|
|
94
|
+
def calculate_average_relevance(papers: List[Dict]) -> Dict:
|
|
95
|
+
"""Calculate average relevance score and check if meets standard (≥8.0)."""
|
|
96
|
+
scores = [p.get("relevance_score", 0) for p in papers if p.get("relevance_score", 0) > 0]
|
|
97
|
+
|
|
98
|
+
if not scores:
|
|
99
|
+
return {
|
|
100
|
+
"average": 0,
|
|
101
|
+
"count": 0,
|
|
102
|
+
"meets_standard": False,
|
|
103
|
+
"recommendation": "⚠ No relevance scores provided"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
avg = sum(scores) / len(scores)
|
|
107
|
+
meets = avg >= 8.0
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
"average": round(avg, 2),
|
|
111
|
+
"count": len(scores),
|
|
112
|
+
"meets_standard": meets,
|
|
113
|
+
"recommendation": (
|
|
114
|
+
"✓ Average relevance meets standard"
|
|
115
|
+
if meets
|
|
116
|
+
else f"⚠ Average relevance ({avg:.2f}) below 8.0 threshold"
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
def check_diversity(papers: List[Dict]) -> Dict:
|
|
121
|
+
"""Check author and perspective diversity."""
|
|
122
|
+
authors = set()
|
|
123
|
+
for paper in papers:
|
|
124
|
+
paper_authors = paper.get("authors", "").split(";")
|
|
125
|
+
authors.update([a.strip() for a in paper_authors if a.strip()])
|
|
126
|
+
|
|
127
|
+
unique_author_count = len(authors)
|
|
128
|
+
|
|
129
|
+
# Note: Perspective diversity requires manual assessment
|
|
130
|
+
return {
|
|
131
|
+
"unique_authors": unique_author_count,
|
|
132
|
+
"meets_author_diversity": unique_author_count >= 5,
|
|
133
|
+
"recommendation": (
|
|
134
|
+
"✓ Author diversity meets standard (≥5 different authors)"
|
|
135
|
+
if unique_author_count >= 5
|
|
136
|
+
else f"⚠ Need {5 - unique_author_count} more unique authors"
|
|
137
|
+
),
|
|
138
|
+
"note": "Perspective diversity (≥3 different schools/approaches) requires manual verification"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
def generate_evaluation_report(papers: List[Dict]) -> str:
|
|
142
|
+
"""Generate comprehensive evaluation report."""
|
|
143
|
+
time_dist = evaluate_time_distribution(papers)
|
|
144
|
+
relevance = calculate_average_relevance(papers)
|
|
145
|
+
diversity = check_diversity(papers)
|
|
146
|
+
|
|
147
|
+
report = f"""
|
|
148
|
+
# Sample Paper Evaluation Report
|
|
149
|
+
|
|
150
|
+
Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}
|
|
151
|
+
|
|
152
|
+
## Executive Summary
|
|
153
|
+
|
|
154
|
+
- **Papers analyzed**: {len(papers)}
|
|
155
|
+
- **Average relevance**: {relevance['average']}/10
|
|
156
|
+
- **Time distribution**: {"✓ Balanced" if time_dist['meets_standard'] else "⚠ Needs adjustment"}
|
|
157
|
+
- **Author diversity**: {"✓ Sufficient" if diversity['meets_author_diversity'] else "⚠ Insufficient"}
|
|
158
|
+
- **Overall quality**: {"✓ PASS" if all([time_dist['meets_standard'], relevance['meets_standard'], diversity['meets_author_diversity']]) else "⚠ NEEDS IMPROVEMENT"}
|
|
159
|
+
|
|
160
|
+
## Time Distribution
|
|
161
|
+
|
|
162
|
+
{time_dist['recommendation']}
|
|
163
|
+
|
|
164
|
+
- Recent (last 6 months): {time_dist['recent_count']}/3 required
|
|
165
|
+
- Current (1-2 years): {time_dist['current_count']}/3 required
|
|
166
|
+
- Classic (highly cited): {time_dist['classic_count']}/2 required
|
|
167
|
+
|
|
168
|
+
## Relevance Analysis
|
|
169
|
+
|
|
170
|
+
{relevance['recommendation']}
|
|
171
|
+
|
|
172
|
+
- Average score: {relevance['average']}/10
|
|
173
|
+
- Papers scored: {relevance['count']}
|
|
174
|
+
- Threshold: 8.0/10
|
|
175
|
+
|
|
176
|
+
## Diversity Check
|
|
177
|
+
|
|
178
|
+
{diversity['recommendation']}
|
|
179
|
+
|
|
180
|
+
- Unique authors: {diversity['unique_authors']}
|
|
181
|
+
- Minimum required: 5
|
|
182
|
+
|
|
183
|
+
{diversity['note']}
|
|
184
|
+
|
|
185
|
+
## Detailed Paper List
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
# Add table of papers
|
|
190
|
+
report += "| # | Title | Authors | Date | Relevance | Category |\n"
|
|
191
|
+
report += "|---|-------|---------|------|-----------|----------|\n"
|
|
192
|
+
|
|
193
|
+
for i, paper in enumerate(papers, 1):
|
|
194
|
+
title = paper.get("title", "")[:50] + "..." if len(paper.get("title", "")) > 50 else paper.get("title", "")
|
|
195
|
+
authors = paper.get("authors", "")[:30] + "..." if len(paper.get("authors", "")) > 30 else paper.get("authors", "")
|
|
196
|
+
date = paper.get("publication_date", "")
|
|
197
|
+
relevance_score = paper.get("relevance_score", "N/A")
|
|
198
|
+
category = paper.get("time_category", "")
|
|
199
|
+
|
|
200
|
+
report += f"| {i} | {title} | {authors} | {date} | {relevance_score}/10 | {category} |\n"
|
|
201
|
+
|
|
202
|
+
report += "\n## Quality Indicators Summary\n\n"
|
|
203
|
+
|
|
204
|
+
has_doi_count = sum(1 for p in papers if p.get("quality_indicators", {}).get("has_doi", False))
|
|
205
|
+
complete_abstract_count = sum(1 for p in papers if p.get("quality_indicators", {}).get("complete_abstract", False))
|
|
206
|
+
|
|
207
|
+
report += f"- Papers with DOI: {has_doi_count}/{len(papers)}\n"
|
|
208
|
+
report += f"- Papers with complete abstract: {complete_abstract_count}/{len(papers)}\n"
|
|
209
|
+
|
|
210
|
+
report += "\n## Recommendations\n\n"
|
|
211
|
+
|
|
212
|
+
if time_dist['meets_standard'] and relevance['meets_standard'] and diversity['meets_author_diversity']:
|
|
213
|
+
report += "✓ **PROCEED**: Sample quality meets all standards. Extract writing guidelines and proceed to Phase 2.\n"
|
|
214
|
+
else:
|
|
215
|
+
report += "⚠ **RE-SEARCH**: Address the following issues before proceeding:\n\n"
|
|
216
|
+
if not time_dist['meets_standard']:
|
|
217
|
+
report += f"- {time_dist['recommendation']}\n"
|
|
218
|
+
if not relevance['meets_standard']:
|
|
219
|
+
report += f"- {relevance['recommendation']}\n"
|
|
220
|
+
if not diversity['meets_author_diversity']:
|
|
221
|
+
report += f"- {diversity['recommendation']}\n"
|
|
222
|
+
|
|
223
|
+
return report
|
|
224
|
+
|
|
225
|
+
def main():
|
|
226
|
+
"""Interactive evaluation interface."""
|
|
227
|
+
print("=" * 60)
|
|
228
|
+
print("Sample Paper Evaluation Tool")
|
|
229
|
+
print("=" * 60)
|
|
230
|
+
print()
|
|
231
|
+
print("This tool helps evaluate sample papers against quality standards.")
|
|
232
|
+
print()
|
|
233
|
+
|
|
234
|
+
# Option 1: Load from JSON
|
|
235
|
+
print("Options:")
|
|
236
|
+
print("1. Load papers from JSON file")
|
|
237
|
+
print("2. Create evaluation template")
|
|
238
|
+
print("3. Generate report from JSON")
|
|
239
|
+
print()
|
|
240
|
+
|
|
241
|
+
choice = input("Select option (1-3): ").strip()
|
|
242
|
+
|
|
243
|
+
if choice == "1":
|
|
244
|
+
# Load and evaluate
|
|
245
|
+
filename = input("Enter JSON filename: ").strip()
|
|
246
|
+
try:
|
|
247
|
+
with open(filename, 'r') as f:
|
|
248
|
+
papers = json.load(f)
|
|
249
|
+
print(f"\nLoaded {len(papers)} papers from {filename}")
|
|
250
|
+
print("\nGenerating evaluation report...\n")
|
|
251
|
+
report = generate_evaluation_report(papers)
|
|
252
|
+
print(report)
|
|
253
|
+
|
|
254
|
+
# Save report
|
|
255
|
+
save = input("\nSave report to file? (y/n): ").strip().lower()
|
|
256
|
+
if save == 'y':
|
|
257
|
+
report_filename = filename.replace('.json', '_evaluation_report.md')
|
|
258
|
+
with open(report_filename, 'w') as f:
|
|
259
|
+
f.write(report)
|
|
260
|
+
print(f"Report saved to {report_filename}")
|
|
261
|
+
except FileNotFoundError:
|
|
262
|
+
print(f"Error: File '{filename}' not found")
|
|
263
|
+
except json.JSONDecodeError:
|
|
264
|
+
print(f"Error: '{filename}' is not valid JSON")
|
|
265
|
+
|
|
266
|
+
elif choice == "2":
|
|
267
|
+
# Create template
|
|
268
|
+
num_papers = int(input("Number of papers to evaluate: ").strip())
|
|
269
|
+
papers = [create_evaluation_template() for _ in range(num_papers)]
|
|
270
|
+
|
|
271
|
+
filename = input("Save template to (filename.json): ").strip()
|
|
272
|
+
with open(filename, 'w') as f:
|
|
273
|
+
json.dump(papers, f, indent=2)
|
|
274
|
+
|
|
275
|
+
print(f"\nTemplate saved to {filename}")
|
|
276
|
+
print("Fill in the template with paper details, then use option 3 to generate report.")
|
|
277
|
+
|
|
278
|
+
elif choice == "3":
|
|
279
|
+
# Generate report only
|
|
280
|
+
filename = input("Enter JSON filename: ").strip()
|
|
281
|
+
try:
|
|
282
|
+
with open(filename, 'r') as f:
|
|
283
|
+
papers = json.load(f)
|
|
284
|
+
report = generate_evaluation_report(papers)
|
|
285
|
+
print("\n" + report)
|
|
286
|
+
|
|
287
|
+
report_filename = filename.replace('.json', '_evaluation_report.md')
|
|
288
|
+
with open(report_filename, 'w') as f:
|
|
289
|
+
f.write(report)
|
|
290
|
+
print(f"\nReport saved to {report_filename}")
|
|
291
|
+
except FileNotFoundError:
|
|
292
|
+
print(f"Error: File '{filename}' not found")
|
|
293
|
+
except json.JSONDecodeError:
|
|
294
|
+
print(f"Error: '{filename}' is not valid JSON")
|
|
295
|
+
|
|
296
|
+
else:
|
|
297
|
+
print("Invalid option")
|
|
298
|
+
|
|
299
|
+
if __name__ == "__main__":
|
|
300
|
+
main()
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Research Gap Analysis Script
|
|
4
|
+
|
|
5
|
+
This script helps structure and evaluate identified research gaps.
|
|
6
|
+
It provides templates and validation for gap documentation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import List, Dict
|
|
12
|
+
|
|
13
|
+
def create_gap_template() -> Dict:
|
|
14
|
+
"""Create template for documenting a research gap."""
|
|
15
|
+
return {
|
|
16
|
+
"gap_id": "", # e.g., "GAP-001"
|
|
17
|
+
"title": "",
|
|
18
|
+
"type": "", # complete/partial/controversy
|
|
19
|
+
"definition": "",
|
|
20
|
+
"evidence": [
|
|
21
|
+
{
|
|
22
|
+
"citation": "", # "Author (Year)"
|
|
23
|
+
"quote": "",
|
|
24
|
+
"context": ""
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"significance": "", # high/medium/low
|
|
28
|
+
"feasibility": "",
|
|
29
|
+
"why_matters": "",
|
|
30
|
+
"user_approach": ""
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def validate_gap(gap: Dict) -> Dict:
|
|
34
|
+
"""
|
|
35
|
+
Validate a research gap against quality standards.
|
|
36
|
+
|
|
37
|
+
Requirements:
|
|
38
|
+
- Specific definition
|
|
39
|
+
- At least 3 pieces of evidence
|
|
40
|
+
- Clear significance and feasibility assessment
|
|
41
|
+
"""
|
|
42
|
+
issues = []
|
|
43
|
+
warnings = []
|
|
44
|
+
|
|
45
|
+
# Check required fields
|
|
46
|
+
required_fields = ["title", "type", "definition", "significance", "feasibility", "why_matters"]
|
|
47
|
+
for field in required_fields:
|
|
48
|
+
if not gap.get(field):
|
|
49
|
+
issues.append(f"Missing required field: {field}")
|
|
50
|
+
|
|
51
|
+
# Check gap type
|
|
52
|
+
valid_types = ["complete", "partial", "controversy"]
|
|
53
|
+
if gap.get("type") and gap.get("type") not in valid_types:
|
|
54
|
+
issues.append(f"Invalid type: '{gap.get('type')}'. Must be one of: {', '.join(valid_types)}")
|
|
55
|
+
|
|
56
|
+
# Check evidence count
|
|
57
|
+
evidence_count = len(gap.get("evidence", []))
|
|
58
|
+
if evidence_count < 3:
|
|
59
|
+
issues.append(f"Insufficient evidence: {evidence_count}/3 required")
|
|
60
|
+
elif evidence_count < 5:
|
|
61
|
+
warnings.append(f"Evidence count ({evidence_count}) is acceptable but 5+ recommended")
|
|
62
|
+
|
|
63
|
+
# Check evidence quality
|
|
64
|
+
for i, ev in enumerate(gap.get("evidence", []), 1):
|
|
65
|
+
if not ev.get("citation"):
|
|
66
|
+
issues.append(f"Evidence #{i}: Missing citation")
|
|
67
|
+
if not ev.get("quote"):
|
|
68
|
+
issues.append(f"Evidence #{i}: Missing quote")
|
|
69
|
+
if not ev.get("context"):
|
|
70
|
+
warnings.append(f"Evidence #{i}: Missing context (recommended)")
|
|
71
|
+
|
|
72
|
+
# Check significance
|
|
73
|
+
valid_significance = ["high", "medium", "low"]
|
|
74
|
+
if gap.get("significance") and gap.get("significance") not in valid_significance:
|
|
75
|
+
issues.append(f"Invalid significance: '{gap.get('significance')}'. Must be one of: {', '.join(valid_significance)}")
|
|
76
|
+
|
|
77
|
+
# Check definition length
|
|
78
|
+
definition_length = len(gap.get("definition", "").split())
|
|
79
|
+
if definition_length < 20:
|
|
80
|
+
warnings.append(f"Definition too short ({definition_length} words). Aim for 50-100 words.")
|
|
81
|
+
elif definition_length > 150:
|
|
82
|
+
warnings.append(f"Definition too long ({definition_length} words). Aim for 50-100 words.")
|
|
83
|
+
|
|
84
|
+
# Check why_matters length
|
|
85
|
+
why_matters_length = len(gap.get("why_matters", "").split())
|
|
86
|
+
if why_matters_length < 50:
|
|
87
|
+
warnings.append(f"'Why matters' too short ({why_matters_length} words). Aim for 100-200 words.")
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
"valid": len(issues) == 0,
|
|
91
|
+
"issues": issues,
|
|
92
|
+
"warnings": warnings,
|
|
93
|
+
"evidence_count": evidence_count,
|
|
94
|
+
"definition_length": definition_length,
|
|
95
|
+
"why_matters_length": why_matters_length
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
def analyze_gap_portfolio(gaps: List[Dict]) -> Dict:
|
|
99
|
+
"""Analyze the overall gap portfolio quality."""
|
|
100
|
+
if not gaps:
|
|
101
|
+
return {
|
|
102
|
+
"meets_standard": False,
|
|
103
|
+
"recommendation": "⚠ No gaps identified. Minimum 3 gaps required."
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
gap_count = len(gaps)
|
|
107
|
+
valid_gaps = [g for g in gaps if validate_gap(g)["valid"]]
|
|
108
|
+
valid_count = len(valid_gaps)
|
|
109
|
+
|
|
110
|
+
# Type distribution
|
|
111
|
+
types = [g.get("type", "") for g in gaps]
|
|
112
|
+
type_distribution = {
|
|
113
|
+
"complete": types.count("complete"),
|
|
114
|
+
"partial": types.count("partial"),
|
|
115
|
+
"controversy": types.count("controversy")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Significance distribution
|
|
119
|
+
significance = [g.get("significance", "") for g in gaps]
|
|
120
|
+
high_sig_count = significance.count("high")
|
|
121
|
+
|
|
122
|
+
# Average evidence per gap
|
|
123
|
+
total_evidence = sum(len(g.get("evidence", [])) for g in gaps)
|
|
124
|
+
avg_evidence = total_evidence / gap_count if gap_count > 0 else 0
|
|
125
|
+
|
|
126
|
+
# Meets standards check
|
|
127
|
+
meets_standard = (
|
|
128
|
+
gap_count >= 3 and
|
|
129
|
+
valid_count >= 3 and
|
|
130
|
+
high_sig_count >= 1 and
|
|
131
|
+
avg_evidence >= 3
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
"total_gaps": gap_count,
|
|
136
|
+
"valid_gaps": valid_count,
|
|
137
|
+
"type_distribution": type_distribution,
|
|
138
|
+
"high_significance_count": high_sig_count,
|
|
139
|
+
"average_evidence": round(avg_evidence, 1),
|
|
140
|
+
"meets_standard": meets_standard,
|
|
141
|
+
"recommendation": _get_portfolio_recommendation(
|
|
142
|
+
gap_count, valid_count, high_sig_count, avg_evidence
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def _get_portfolio_recommendation(total: int, valid: int, high_sig: int, avg_evidence: float) -> str:
|
|
147
|
+
"""Generate recommendation for gap portfolio."""
|
|
148
|
+
if total < 3:
|
|
149
|
+
return f"⚠ Insufficient gaps: {total}/3 minimum required"
|
|
150
|
+
|
|
151
|
+
if valid < 3:
|
|
152
|
+
return f"⚠ Only {valid}/{total} gaps are valid. Fix validation issues."
|
|
153
|
+
|
|
154
|
+
if high_sig < 1:
|
|
155
|
+
return "⚠ No high-significance gaps. At least 1 required."
|
|
156
|
+
|
|
157
|
+
if avg_evidence < 3:
|
|
158
|
+
return f"⚠ Average evidence ({avg_evidence:.1f}) below 3.0 minimum"
|
|
159
|
+
|
|
160
|
+
return "✓ Gap portfolio meets quality standards"
|
|
161
|
+
|
|
162
|
+
def generate_gap_report(gaps: List[Dict]) -> str:
|
|
163
|
+
"""Generate comprehensive gap analysis report."""
|
|
164
|
+
portfolio = analyze_gap_portfolio(gaps)
|
|
165
|
+
|
|
166
|
+
report = f"""
|
|
167
|
+
# Research Gap Analysis Report
|
|
168
|
+
|
|
169
|
+
Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}
|
|
170
|
+
|
|
171
|
+
## Portfolio Summary
|
|
172
|
+
|
|
173
|
+
- **Total gaps identified**: {portfolio['total_gaps']}
|
|
174
|
+
- **Valid gaps**: {portfolio['valid_gaps']}/{portfolio['total_gaps']}
|
|
175
|
+
- **High-significance gaps**: {portfolio['high_significance_count']}
|
|
176
|
+
- **Average evidence per gap**: {portfolio['average_evidence']}
|
|
177
|
+
- **Quality**: {"✓ PASS" if portfolio['meets_standard'] else "⚠ NEEDS IMPROVEMENT"}
|
|
178
|
+
|
|
179
|
+
## Gap Type Distribution
|
|
180
|
+
|
|
181
|
+
- Complete gaps (unexplored): {portfolio['type_distribution']['complete']}
|
|
182
|
+
- Partial gaps (understudied): {portfolio['type_distribution']['partial']}
|
|
183
|
+
- Controversy gaps (unresolved): {portfolio['type_distribution']['controversy']}
|
|
184
|
+
|
|
185
|
+
## Portfolio Assessment
|
|
186
|
+
|
|
187
|
+
{portfolio['recommendation']}
|
|
188
|
+
|
|
189
|
+
## Detailed Gap Analysis
|
|
190
|
+
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
for i, gap in enumerate(gaps, 1):
|
|
194
|
+
validation = validate_gap(gap)
|
|
195
|
+
|
|
196
|
+
status_icon = "✓" if validation['valid'] else "⚠"
|
|
197
|
+
report += f"### {status_icon} Gap {i}: {gap.get('title', 'Untitled')}\n\n"
|
|
198
|
+
|
|
199
|
+
report += f"**ID**: {gap.get('gap_id', 'N/A')}\n\n"
|
|
200
|
+
report += f"**Type**: {gap.get('type', 'N/A')}\n\n"
|
|
201
|
+
report += f"**Significance**: {gap.get('significance', 'N/A')}\n\n"
|
|
202
|
+
report += f"**Feasibility**: {gap.get('feasibility', 'N/A')}\n\n"
|
|
203
|
+
|
|
204
|
+
report += "**Definition**:\n"
|
|
205
|
+
report += f"{gap.get('definition', 'N/A')}\n\n"
|
|
206
|
+
|
|
207
|
+
report += f"**Evidence** ({validation['evidence_count']} pieces):\n\n"
|
|
208
|
+
for j, evidence in enumerate(gap.get('evidence', []), 1):
|
|
209
|
+
report += f"{j}. **{evidence.get('citation', 'No citation')}**\n"
|
|
210
|
+
report += f" > \"{evidence.get('quote', 'No quote')}\"\n"
|
|
211
|
+
if evidence.get('context'):
|
|
212
|
+
report += f" - Context: {evidence.get('context')}\n"
|
|
213
|
+
report += "\n"
|
|
214
|
+
|
|
215
|
+
report += "**Why This Matters**:\n"
|
|
216
|
+
report += f"{gap.get('why_matters', 'N/A')}\n\n"
|
|
217
|
+
|
|
218
|
+
# Validation results
|
|
219
|
+
if not validation['valid']:
|
|
220
|
+
report += "**⚠ Validation Issues**:\n"
|
|
221
|
+
for issue in validation['issues']:
|
|
222
|
+
report += f"- {issue}\n"
|
|
223
|
+
report += "\n"
|
|
224
|
+
|
|
225
|
+
if validation['warnings']:
|
|
226
|
+
report += "**ℹ️ Warnings**:\n"
|
|
227
|
+
for warning in validation['warnings']:
|
|
228
|
+
report += f"- {warning}\n"
|
|
229
|
+
report += "\n"
|
|
230
|
+
|
|
231
|
+
report += "---\n\n"
|
|
232
|
+
|
|
233
|
+
report += "## Recommendations\n\n"
|
|
234
|
+
|
|
235
|
+
if portfolio['meets_standard']:
|
|
236
|
+
report += "✓ **PROCEED**: Gap portfolio meets quality standards.\n\n"
|
|
237
|
+
report += "Next steps:\n"
|
|
238
|
+
report += "1. Proceed to originality assessment (Phase 2.2)\n"
|
|
239
|
+
report += "2. Discuss core concepts with user (Phase 2.3)\n"
|
|
240
|
+
report += "3. Begin outline design (Phase 3)\n"
|
|
241
|
+
else:
|
|
242
|
+
report += "⚠ **REVISE**: Address the following before proceeding:\n\n"
|
|
243
|
+
report += f"- {portfolio['recommendation']}\n"
|
|
244
|
+
|
|
245
|
+
# Specific actionable items
|
|
246
|
+
for i, gap in enumerate(gaps, 1):
|
|
247
|
+
validation = validate_gap(gap)
|
|
248
|
+
if not validation['valid']:
|
|
249
|
+
report += f"\n**Gap {i} ({gap.get('title', 'Untitled')})**:\n"
|
|
250
|
+
for issue in validation['issues']:
|
|
251
|
+
report += f" - Fix: {issue}\n"
|
|
252
|
+
|
|
253
|
+
return report
|
|
254
|
+
|
|
255
|
+
def generate_gap_evidence_package(gap: Dict) -> str:
|
|
256
|
+
"""Generate evidence package for a single gap (for documentation)."""
|
|
257
|
+
package = f"""
|
|
258
|
+
# Gap Evidence Package: {gap.get('title', 'Untitled')}
|
|
259
|
+
|
|
260
|
+
**Gap ID**: {gap.get('gap_id', 'N/A')}
|
|
261
|
+
|
|
262
|
+
## Definition
|
|
263
|
+
|
|
264
|
+
{gap.get('definition', 'No definition provided')}
|
|
265
|
+
|
|
266
|
+
## Evidence from Literature
|
|
267
|
+
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
for i, evidence in enumerate(gap.get('evidence', []), 1):
|
|
271
|
+
package += f"### Evidence {i}\n\n"
|
|
272
|
+
package += f"**Citation**: {evidence.get('citation', 'No citation')}\n\n"
|
|
273
|
+
package += f"**Quote**: \"{evidence.get('quote', 'No quote')}\"\n\n"
|
|
274
|
+
package += f"**Context**: {evidence.get('context', 'No context provided')}\n\n"
|
|
275
|
+
|
|
276
|
+
package += f"""
|
|
277
|
+
## Why This Gap Matters
|
|
278
|
+
|
|
279
|
+
{gap.get('why_matters', 'No justification provided')}
|
|
280
|
+
|
|
281
|
+
## Feasibility Assessment
|
|
282
|
+
|
|
283
|
+
{gap.get('feasibility', 'No assessment provided')}
|
|
284
|
+
|
|
285
|
+
## User's Approach
|
|
286
|
+
|
|
287
|
+
{gap.get('user_approach', 'No approach specified')}
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
return package
|
|
291
|
+
|
|
292
|
+
def main():
|
|
293
|
+
"""Interactive gap analysis interface."""
|
|
294
|
+
print("=" * 60)
|
|
295
|
+
print("Research Gap Analysis Tool")
|
|
296
|
+
print("=" * 60)
|
|
297
|
+
print()
|
|
298
|
+
|
|
299
|
+
print("Options:")
|
|
300
|
+
print("1. Create gap template")
|
|
301
|
+
print("2. Validate gap from JSON")
|
|
302
|
+
print("3. Generate portfolio report")
|
|
303
|
+
print("4. Generate evidence package for single gap")
|
|
304
|
+
print()
|
|
305
|
+
|
|
306
|
+
choice = input("Select option (1-4): ").strip()
|
|
307
|
+
|
|
308
|
+
if choice == "1":
|
|
309
|
+
# Create template
|
|
310
|
+
num_gaps = int(input("Number of gaps to document: ").strip())
|
|
311
|
+
gaps = [create_gap_template() for _ in range(num_gaps)]
|
|
312
|
+
|
|
313
|
+
# Assign IDs
|
|
314
|
+
for i, gap in enumerate(gaps, 1):
|
|
315
|
+
gap["gap_id"] = f"GAP-{i:03d}"
|
|
316
|
+
|
|
317
|
+
filename = input("Save template to (filename.json): ").strip()
|
|
318
|
+
with open(filename, 'w') as f:
|
|
319
|
+
json.dump(gaps, f, indent=2)
|
|
320
|
+
|
|
321
|
+
print(f"\nTemplate saved to {filename}")
|
|
322
|
+
print("Fill in the template, then use option 3 to validate.")
|
|
323
|
+
|
|
324
|
+
elif choice == "2":
|
|
325
|
+
# Validate single gap
|
|
326
|
+
filename = input("Enter gap JSON filename: ").strip()
|
|
327
|
+
try:
|
|
328
|
+
with open(filename, 'r') as f:
|
|
329
|
+
gap = json.load(f)
|
|
330
|
+
|
|
331
|
+
validation = validate_gap(gap)
|
|
332
|
+
|
|
333
|
+
print(f"\nValidation Results for: {gap.get('title', 'Untitled')}")
|
|
334
|
+
print(f"Status: {"✓ VALID" if validation['valid'] else "⚠ INVALID"}")
|
|
335
|
+
print(f"Evidence count: {validation['evidence_count']}")
|
|
336
|
+
print(f"Definition length: {validation['definition_length']} words")
|
|
337
|
+
print(f"'Why matters' length: {validation['why_matters_length']} words")
|
|
338
|
+
|
|
339
|
+
if validation['issues']:
|
|
340
|
+
print("\n⚠ Issues:")
|
|
341
|
+
for issue in validation['issues']:
|
|
342
|
+
print(f" - {issue}")
|
|
343
|
+
|
|
344
|
+
if validation['warnings']:
|
|
345
|
+
print("\nℹ️ Warnings:")
|
|
346
|
+
for warning in validation['warnings']:
|
|
347
|
+
print(f" - {warning}")
|
|
348
|
+
|
|
349
|
+
except FileNotFoundError:
|
|
350
|
+
print(f"Error: File '{filename}' not found")
|
|
351
|
+
except json.JSONDecodeError:
|
|
352
|
+
print(f"Error: '{filename}' is not valid JSON")
|
|
353
|
+
|
|
354
|
+
elif choice == "3":
|
|
355
|
+
# Generate portfolio report
|
|
356
|
+
filename = input("Enter gaps JSON filename: ").strip()
|
|
357
|
+
try:
|
|
358
|
+
with open(filename, 'r') as f:
|
|
359
|
+
gaps = json.load(f)
|
|
360
|
+
|
|
361
|
+
print(f"\nAnalyzing {len(gaps)} gaps...")
|
|
362
|
+
report = generate_gap_report(gaps)
|
|
363
|
+
print("\n" + report)
|
|
364
|
+
|
|
365
|
+
report_filename = filename.replace('.json', '_gap_report.md')
|
|
366
|
+
with open(report_filename, 'w') as f:
|
|
367
|
+
f.write(report)
|
|
368
|
+
print(f"\nReport saved to {report_filename}")
|
|
369
|
+
|
|
370
|
+
except FileNotFoundError:
|
|
371
|
+
print(f"Error: File '{filename}' not found")
|
|
372
|
+
except json.JSONDecodeError:
|
|
373
|
+
print(f"Error: '{filename}' is not valid JSON")
|
|
374
|
+
|
|
375
|
+
elif choice == "4":
|
|
376
|
+
# Generate evidence package
|
|
377
|
+
filename = input("Enter single gap JSON filename: ").strip()
|
|
378
|
+
try:
|
|
379
|
+
with open(filename, 'r') as f:
|
|
380
|
+
gap = json.load(f)
|
|
381
|
+
|
|
382
|
+
package = generate_gap_evidence_package(gap)
|
|
383
|
+
print("\n" + package)
|
|
384
|
+
|
|
385
|
+
package_filename = f"evidence_package_{gap.get('gap_id', 'gap')}.md"
|
|
386
|
+
with open(package_filename, 'w') as f:
|
|
387
|
+
f.write(package)
|
|
388
|
+
print(f"\nEvidence package saved to {package_filename}")
|
|
389
|
+
|
|
390
|
+
except FileNotFoundError:
|
|
391
|
+
print(f"Error: File '{filename}' not found")
|
|
392
|
+
except json.JSONDecodeError:
|
|
393
|
+
print(f"Error: '{filename}' is not valid JSON")
|
|
394
|
+
|
|
395
|
+
else:
|
|
396
|
+
print("Invalid option")
|
|
397
|
+
|
|
398
|
+
if __name__ == "__main__":
|
|
399
|
+
main()
|