feat: IMAP mail sync with thread-to-contact matching (PRD §6.1)
Real IMAP connection via imapflow library: - Connects to IMAP server (IMAPFlow, TLS on port 993) - Fetches recent messages (configurable limit, default 20) - Extracts: from, fromEmail, subject, date, snippet (200 chars) - Thread-to-contact matching (PRD §6.1): 1. Match sender email against persons table 2. If no person match, extract domain and match against company.website 3. Returns matchedCompany + matchedPerson for each email - Audit-logged (action: crm.mailsync:imap) - Zod validated (host, port, user, password, folder, limit) - 502 on IMAP connection failure with detail - 400 on validation failure Tested: invalid creds → 502 'IMAP connection failed', missing host → 400
This commit is contained in:
1 parent
7acc0c5451
commit
e45b68bb8b
1089 files changed
+260614
No files matched your search
@@ -0,0 +1,20 @@
|
||||
# Figure Contract
|
||||
|
||||
Core conclusion:
|
||||
Figure archetype:
|
||||
Target output:
|
||||
Final size:
|
||||
|
||||
Panel map:
|
||||
- a:
|
||||
- b:
|
||||
- c:
|
||||
|
||||
Evidence hierarchy:
|
||||
- hero evidence:
|
||||
- validation evidence:
|
||||
- controls / robustness:
|
||||
|
||||
Statistics needed:
|
||||
Source data needed:
|
||||
Reviewer risk:
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Figure Examples
|
||||
|
||||
Reference matplotlib / seaborn scripts producing publication-grade figures
|
||||
for AI4S research papers. Reproduce with:
|
||||
|
||||
```bash
|
||||
pip install matplotlib seaborn
|
||||
cd <where-you-want-the-pdf>
|
||||
python make_fig_02_horizon_sweep.py
|
||||
python make_fig_03_heatmap.py
|
||||
python make_fig_04_ablation.py
|
||||
```
|
||||
|
||||
These are illustrative templates — copy and adapt for your data. They were
|
||||
extracted from a successful enrichment run of the `paper-writer` skill on
|
||||
the topic *Transformer-based time-series forecasting* (May 2026); the data
|
||||
embedded inline are simulated and meant only to illustrate the formatting.
|
||||
|
||||
Start by filling `FIGURE_CONTRACT_TEMPLATE.md` for the figure you are about to make.
|
||||
Then reuse `style_kit.py` so exports, palette, panel labels, and simulated
|
||||
watermarks stay consistent across the experiment package.
|
||||
|
||||
## Style summary
|
||||
- Shared `style_kit.py` with editable SVG text and PDF font embedding
|
||||
- Explicit color palette (no default cycle)
|
||||
- Removed top/right spines
|
||||
- `axes.grid` on at low alpha
|
||||
- Figure sizes targeted at column-width (3.4-5.0 inches wide)
|
||||
- "simulated" watermark in italic gray for runs from the simulator path
|
||||
- Save PDF + SVG + TIFF together when the environment allows it
|
||||
|
||||
See the `paper-writer` skill's `references/02-figures-publication-grade.md`
|
||||
for the full taste guide.
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Generate fig_02_horizon_sweep.pdf — MSE vs forecast horizon (publication style)."""
|
||||
import os
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from style_kit import (
|
||||
DEFAULT_COLORS,
|
||||
add_panel_label,
|
||||
add_simulated_watermark,
|
||||
apply_publication_style,
|
||||
save_publication_bundle,
|
||||
)
|
||||
|
||||
apply_publication_style()
|
||||
MARKERS = ["o", "s", "^", "D"]
|
||||
|
||||
horizons = np.array([96, 192, 336, 720])
|
||||
methods = {
|
||||
"DLinear": np.array([0.345, 0.380, 0.420, 0.498]),
|
||||
"PatchTST": np.array([0.330, 0.358, 0.395, 0.469]),
|
||||
"iTransformer": np.array([0.328, 0.354, 0.392, 0.466]),
|
||||
"Ours": np.array([0.319, 0.346, 0.385, 0.460]),
|
||||
}
|
||||
|
||||
fig, ax = plt.subplots(figsize=(5.0, 3.2))
|
||||
for (name, vals), color, marker in zip(methods.items(), DEFAULT_COLORS, MARKERS):
|
||||
ax.plot(horizons, vals, marker=marker, color=color, label=name)
|
||||
|
||||
ax.set_xlabel(r"Forecast horizon $H$")
|
||||
ax.set_ylabel("MSE on ETTm1 (lower is better)")
|
||||
ax.set_xscale("log", base=2)
|
||||
ax.set_xticks(horizons)
|
||||
ax.set_xticklabels([str(h) for h in horizons])
|
||||
ax.legend(frameon=False, loc="upper left")
|
||||
add_panel_label(ax, "(a)")
|
||||
add_simulated_watermark(ax)
|
||||
|
||||
plt.tight_layout(pad=0.4)
|
||||
out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fig_02_horizon_sweep")
|
||||
save_publication_bundle(fig, out)
|
||||
plt.close(fig)
|
||||
print(f"Wrote {out}.pdf/.svg/.tiff")
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Generate fig_03_method_x_dataset_heatmap.pdf — MSE heatmap across methods × datasets."""
|
||||
import os
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
import numpy as np
|
||||
from style_kit import add_panel_label, add_simulated_watermark, apply_publication_style, save_publication_bundle
|
||||
|
||||
apply_publication_style()
|
||||
|
||||
datasets = ["ETTh1", "ETTh2", "ETTm1", "ETTm2", "ECL", "Traffic", "Weather"]
|
||||
methods = ["DLinear", "Informer", "Autoformer", "FEDformer",
|
||||
"PatchTST", "iTrans.", "TimeMixer", "Ours"]
|
||||
|
||||
mse = np.array([
|
||||
[0.394, 0.495, 0.439, 0.411, 0.371, 0.364, 0.358, 0.355],
|
||||
[0.310, 0.452, 0.388, 0.358, 0.301, 0.294, 0.290, 0.287],
|
||||
[0.382, 0.512, 0.421, 0.401, 0.366, 0.359, 0.357, 0.354],
|
||||
[0.279, 0.401, 0.336, 0.318, 0.262, 0.258, 0.254, 0.252],
|
||||
[0.166, 0.219, 0.187, 0.179, 0.158, 0.151, 0.150, 0.149],
|
||||
[0.434, 0.608, 0.545, 0.508, 0.412, 0.406, 0.405, 0.402],
|
||||
[0.241, 0.318, 0.281, 0.265, 0.227, 0.224, 0.223, 0.220],
|
||||
])
|
||||
|
||||
fig, ax = plt.subplots(figsize=(6.4, 3.4))
|
||||
sns.heatmap(
|
||||
mse, annot=True, fmt=".3f",
|
||||
xticklabels=methods, yticklabels=datasets,
|
||||
cmap="RdYlGn_r",
|
||||
cbar_kws={"label": "MSE", "shrink": 0.7, "pad": 0.02},
|
||||
annot_kws={"size": 7.5},
|
||||
linewidths=0.4, linecolor="white",
|
||||
ax=ax,
|
||||
)
|
||||
ax.set_xticklabels(ax.get_xticklabels(), rotation=30, ha="right")
|
||||
ax.set_yticklabels(ax.get_yticklabels(), rotation=0)
|
||||
add_panel_label(ax, "(b)")
|
||||
add_simulated_watermark(ax, y=-0.16)
|
||||
|
||||
plt.tight_layout(pad=0.4)
|
||||
out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fig_03_method_x_dataset_heatmap")
|
||||
save_publication_bundle(fig, out)
|
||||
plt.close(fig)
|
||||
print(f"Wrote {out}.pdf/.svg/.tiff")
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Generate fig_04_ablation_panels.pdf — 2x2 ablation panel figure."""
|
||||
import os
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from style_kit import PALETTE, add_simulated_watermark, apply_publication_style, save_publication_bundle
|
||||
|
||||
apply_publication_style()
|
||||
|
||||
PRIMARY = PALETTE["blue_main"]
|
||||
ABLATION = PALETTE["red_strong"]
|
||||
DATASETS = ["ETTm1", "Electricity", "Traffic", "Weather"]
|
||||
|
||||
horizons = np.array([96, 192, 336, 720])
|
||||
full = {
|
||||
"ETTm1": np.array([0.319, 0.346, 0.385, 0.460]),
|
||||
"Electricity": np.array([0.131, 0.142, 0.149, 0.171]),
|
||||
"Traffic": np.array([0.352, 0.378, 0.402, 0.451]),
|
||||
"Weather": np.array([0.179, 0.198, 0.220, 0.272]),
|
||||
}
|
||||
no_patch = {
|
||||
"ETTm1": np.array([0.354, 0.382, 0.422, 0.499]),
|
||||
"Electricity": np.array([0.143, 0.155, 0.164, 0.186]),
|
||||
"Traffic": np.array([0.378, 0.408, 0.434, 0.486]),
|
||||
"Weather": np.array([0.198, 0.221, 0.247, 0.301]),
|
||||
}
|
||||
|
||||
fig, axes = plt.subplots(2, 2, figsize=(6.6, 4.4), sharex=True)
|
||||
for ax, name in zip(axes.flat, DATASETS):
|
||||
ax.plot(horizons, full[name], marker="o", color=PRIMARY, label="Full")
|
||||
ax.plot(horizons, no_patch[name], marker="s", linestyle="--",
|
||||
color=ABLATION, label=r"$-$ patching")
|
||||
ax.set_title(name)
|
||||
ax.set_xscale("log", base=2)
|
||||
ax.set_xticks(horizons)
|
||||
ax.set_xticklabels([str(h) for h in horizons])
|
||||
ax.grid(True, alpha=0.25, linewidth=0.4)
|
||||
ax.spines["top"].set_visible(False)
|
||||
ax.spines["right"].set_visible(False)
|
||||
|
||||
for ax in axes[:, 0]:
|
||||
ax.set_ylabel("MSE")
|
||||
for ax in axes[-1, :]:
|
||||
ax.set_xlabel(r"Horizon $H$")
|
||||
|
||||
handles, labels = axes[0, 0].get_legend_handles_labels()
|
||||
fig.legend(handles, labels, loc="upper center", bbox_to_anchor=(0.5, 1.02),
|
||||
ncol=2, frameon=False)
|
||||
|
||||
for ax, label in zip(axes.flat, "abcd"):
|
||||
ax.text(-0.15, 1.05, f"({label})", transform=ax.transAxes,
|
||||
fontweight="bold", fontsize=10)
|
||||
|
||||
add_simulated_watermark(fig.axes[-1], y=-0.22)
|
||||
|
||||
plt.tight_layout(rect=(0.0, 0.01, 1.0, 0.95))
|
||||
out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fig_04_ablation_panels")
|
||||
save_publication_bundle(fig, out)
|
||||
plt.close(fig)
|
||||
print(f"Wrote {out}.pdf/.svg/.tiff")
|
||||
@@ -0,0 +1,5 @@
|
||||
# Dependencies for the publication-figure example scripts.
|
||||
# Install: pip install -r requirements.txt
|
||||
matplotlib>=3.7
|
||||
numpy>=1.24
|
||||
seaborn>=0.13
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
"""Shared publication-style helpers for experiment-suite figures."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
PALETTE = {
|
||||
"blue_main": "#0F4D92",
|
||||
"blue_secondary": "#3775BA",
|
||||
"green_1": "#DDF3DE",
|
||||
"green_2": "#AADCA9",
|
||||
"green_3": "#8BCF8B",
|
||||
"red_1": "#F6CFCB",
|
||||
"red_2": "#E9A6A1",
|
||||
"red_strong": "#B64342",
|
||||
"neutral_light": "#CFCECE",
|
||||
"neutral_mid": "#767676",
|
||||
"neutral_dark": "#4D4D4D",
|
||||
"neutral_black": "#272727",
|
||||
"teal": "#42949E",
|
||||
"violet": "#9A4D8E",
|
||||
}
|
||||
|
||||
DEFAULT_COLORS = [
|
||||
PALETTE["blue_main"],
|
||||
PALETTE["green_3"],
|
||||
PALETTE["red_strong"],
|
||||
PALETTE["teal"],
|
||||
PALETTE["violet"],
|
||||
PALETTE["neutral_mid"],
|
||||
]
|
||||
|
||||
PALETTE_NMI_PASTEL = [
|
||||
"#484878",
|
||||
"#7884B4",
|
||||
"#B4C0E4",
|
||||
"#E4E4F0",
|
||||
"#E4CCD8",
|
||||
"#F0C0CC",
|
||||
]
|
||||
|
||||
|
||||
def apply_publication_style(font_size: int = 9, axes_linewidth: float = 0.6) -> None:
|
||||
"""Apply a shared journal-style matplotlib configuration."""
|
||||
plt.rcParams.update({
|
||||
"font.family": "sans-serif",
|
||||
"font.sans-serif": ["Arial", "DejaVu Sans", "Liberation Sans"],
|
||||
"font.size": font_size,
|
||||
"axes.labelsize": font_size,
|
||||
"axes.titlesize": font_size + 1,
|
||||
"xtick.labelsize": max(font_size - 1, 6),
|
||||
"ytick.labelsize": max(font_size - 1, 6),
|
||||
"legend.fontsize": max(font_size - 1, 6),
|
||||
"axes.spines.right": False,
|
||||
"axes.spines.top": False,
|
||||
"axes.linewidth": axes_linewidth,
|
||||
"axes.grid": True,
|
||||
"grid.alpha": 0.25,
|
||||
"grid.linewidth": 0.4,
|
||||
"lines.linewidth": 1.4,
|
||||
"lines.markersize": 4.5,
|
||||
"savefig.dpi": 300,
|
||||
"savefig.bbox": "tight",
|
||||
"svg.fonttype": "none",
|
||||
"pdf.fonttype": 42,
|
||||
"ps.fonttype": 42,
|
||||
})
|
||||
|
||||
|
||||
def add_panel_label(ax, label: str, x: float = -0.12, y: float = 1.03) -> None:
|
||||
ax.text(
|
||||
x, y, label,
|
||||
transform=ax.transAxes,
|
||||
fontsize=10,
|
||||
fontweight="bold",
|
||||
ha="left",
|
||||
va="bottom",
|
||||
)
|
||||
|
||||
|
||||
def add_simulated_watermark(ax, x: float = 0.99, y: float = 0.02) -> None:
|
||||
ax.text(
|
||||
x, y, "simulated",
|
||||
transform=ax.transAxes,
|
||||
ha="right",
|
||||
va="bottom",
|
||||
fontsize=7,
|
||||
color="gray",
|
||||
alpha=0.7,
|
||||
style="italic",
|
||||
)
|
||||
|
||||
|
||||
def save_publication_bundle(fig, out_base: str, dpi: int = 600) -> None:
|
||||
"""Save PDF/SVG/TIFF together when possible."""
|
||||
out_path = Path(out_base)
|
||||
fig.savefig(out_path.with_suffix(".pdf"))
|
||||
fig.savefig(out_path.with_suffix(".svg"))
|
||||
fig.savefig(out_path.with_suffix(".tiff"), dpi=dpi)
|
||||
Reference in new issue
Block a user