Pine Mill(D-101)は best case から commit に移り、金額は48,000 USD から50,000 USD に増え、クローズ日は11月6日から11月20日に変わります。修正後の日付を誰が確認したか質問してください。Cedar Loom(D-102)は commit を離れますが、今回のエクスポートには残っています。Maple Ferry(D-104)は今回のファイルにありません。受注または失注と評価する前に CRM の状態と抽出範囲を確認してください。Fern Desk は変わらず、Amber Finch は今回のファイルに初めて現れます。
許可された自社のエクスポートを比較する
同じ担当者のオープンなパイプライン全体を含むスナップショット2件を使ってください。両方の日付で商談 ID を維持します。アカウントの表示名が変わっても ID で照合します。両方に同じフィルタ、通貨、予測カテゴリの定義を適用してください。気づかないうちにフィルタが変わると実際のパイプライン移動に見えるため、抽出担当者が調査する必要があります。
カテゴリには commit、best_case、upside のいずれかを使います。小数点以下2桁までの0以上の USD 金額と ISO 形式のクローズ日を渡してください。プログラムはセント単位の整数を合計して金額を正確に保持します。ID の重複、非対応通貨、不正な行、空のスナップショット、無効な日付があると比較を停止します。空のエクスポートからは担当者も日付も確定できないため、別途原因を確認してください。
担当者とブリーフィングを確認する
カテゴリ別の金額を読んでから、変更した各 ID を2つのレコードに照合します。新しい ID には登場理由を尋ね、消えた ID には状態と抽出範囲の確認を促します。カテゴリ、金額、クローズ日、アカウント名の変化に応じて固定の質問を選びます。変化のない件数は動きの少ない週の照合に使えます。出力には観測した項目と質問が含まれます。顧客側の事情は担当者が補い、提出する予測はマネージャーが判断します。
"""Compare two local pipeline snapshots and print a Markdown meeting briefing."""
import argparse
import csv
from dataclasses import dataclass
from datetime import date
import html
from pathlib import Path
import re
import sys
FIELDS = ("snapshot_date", "rep_id", "opportunity_id", "account", "category", "amount", "currency", "close_date")
CATEGORIES = ("commit", "best_case", "upside")
EXAMPLES = Path(__file__).resolve().parent / "examples"
@dataclass(frozen=True)
class Opportunity:
account: str
category: str
cents: int
close_date: date
@dataclass(frozen=True)
class Snapshot:
as_of: date
rep: str
deals: dict
def parse_date(value):
if not re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", value):
raise ValueError("dates must use YYYY-MM-DD")
return date.fromisoformat(value)
def parse_id(value):
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,79}", value):
raise ValueError("IDs must contain 1–80 letters, digits, periods, underscores or hyphens and start with a letter or digit")
return value
def parse_cents(value):
if not re.fullmatch(r"[0-9]{1,15}(?:\.[0-9]{1,2})?", value):
raise ValueError("amount must be nonnegative plain USD with up to 15 integer digits and two decimal places")
whole, _, fraction = value.partition(".")
return int(whole) * 100 + int((fraction + "00")[:2])
def read_snapshot(path):
deals = {}
as_of = None
rep = None
with path.open(encoding="utf-8-sig", newline="") as source:
reader = csv.reader(source, strict=True)
header = next(reader, [])
if len(header) != len(FIELDS) or set(header) != set(FIELDS):
raise ValueError(f"{path.name}: expected exactly these CSV columns: {', '.join(FIELDS)}")
for values in reader:
if len(values) != len(header):
raise ValueError(f"{path.name}, line {reader.line_num}: wrong number of fields")
row = dict(zip(header, values))
try:
row_date = parse_date(row['snapshot_date'])
row_rep = parse_id(row['rep_id'])
key = parse_id(row['opportunity_id'])
if as_of is not None and (row_date != as_of or row_rep != rep):
raise ValueError("each snapshot must contain one date and one rep")
if key in deals:
raise ValueError(f"duplicate opportunity_id {key}")
if row['category'] not in CATEGORIES:
raise ValueError("category must be commit, best_case or upside")
if row['currency'] != 'USD':
raise ValueError("only USD is supported")
account = row['account'].strip()
if not account or not account.isprintable():
raise ValueError("account must be nonempty, printable single-line text")
deals[key] = Opportunity(account, row['category'], parse_cents(row['amount']), parse_date(row['close_date']))
as_of, rep = row_date, row_rep
except ValueError as error:
raise ValueError(f"{path.name}, line {reader.line_num}: {error}") from error
if not deals:
raise ValueError(f"{path.name}: an empty file cannot establish a rep or snapshot date")
return Snapshot(as_of, rep, deals)
def dollars(cents, signed=False):
whole, fraction = divmod(abs(cents), 100)
sign = '-' if cents < 0 else '+' if signed and cents > 0 else ''
return f"{sign}${whole:,}" + (f".{fraction:02d}" if fraction else '')
def label(account):
# Account names are data. Escape Markdown constructs and raw HTML in output.
return re.sub(r"([\\`*_\[\]|~])", r"\\\1", html.escape(account, quote=False))
def movement(before, after):
if before is None:
return [f"appears in current snapshot ({after.category}, {dollars(after.cents)}, close {after.close_date})"], ["What brought this opportunity into the current export?"]
if after is None:
return [f"absent from current snapshot (previously {before.category}, {dollars(before.cents)})"], ["Check the CRM status and export scope. What explains the absence?"]
facts, questions = [], []
if before.category != after.category:
facts.append(f"category {before.category} → {after.category}")
questions.append("Which customer evidence supports the category change?")
if before.cents != after.cents:
facts.append(f"amount {dollars(before.cents)} → {dollars(after.cents)}")
questions.append("Which scope or pricing detail explains the amount change?")
if before.close_date != after.close_date:
days = (after.close_date - before.close_date).days
facts.append(f"close {before.close_date} → {after.close_date} ({days:+d} days)")
questions.append("Who confirmed the revised close date, and what is the next milestone?")
if before.account != after.account:
facts.append(f"account label {label(before.account)} → {label(after.account)}")
questions.append("Confirm that the stable opportunity ID still refers to the intended account.")
return facts, questions
def briefing(prior, current):
if prior.rep != current.rep:
raise ValueError("snapshots must describe the same rep")
gap = (current.as_of - prior.as_of).days
if not 1 <= gap <= 10:
raise ValueError("current snapshot must be 1–10 days after prior snapshot")
lines = ["# Weekly forecast meeting prep", "", f"Rep `{prior.rep}` · {prior.as_of} → {current.as_of}", "",
"## Pipeline by category (USD)", "", "| Category | Previous | Current | Change |", "|---|---:|---:|---:|"]
for category in CATEGORIES:
old = sum(d.cents for d in prior.deals.values() if d.category == category)
new = sum(d.cents for d in current.deals.values() if d.category == category)
lines.append(f"| {category} | {dollars(old)} | {dollars(new)} | {dollars(new - old, signed=True)} |")
lines += ["", "## Changed opportunities", ""]
questions = []
unchanged = 0
for key in sorted(prior.deals.keys() | current.deals.keys()):
before, after = prior.deals.get(key), current.deals.get(key)
facts, prompts = movement(before, after)
if not facts:
unchanged += 1
continue
lines.append(f"- `{key}` · {label((after or before).account)} · " + " · ".join(facts) + ".")
questions.extend(f"- `{key}` {prompt}" for prompt in prompts)
if not questions:
lines.append("No opportunity fields changed.")
lines += ["", f"{unchanged} unchanged {'opportunity' if unchanged == 1 else 'opportunities'}.", "", "## Questions for the rep", ""]
lines.extend(questions or ["No movement questions from this pair of snapshots."])
lines += ["", "The totals describe the supplied open pipeline. Confirm causes and outcomes with the rep before using the briefing."]
return "\n".join(lines) + "\n"
def main():
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
commands.add_parser("demo", help="verify the fictional snapshots against the expected briefing and print it")
compare = commands.add_parser("compare", help="compare a pair of local, authorized CSV exports")
compare.add_argument("previous", type=Path)
compare.add_argument("current", type=Path)
args = parser.parse_args()
if args.command == "demo":
previous, current = EXAMPLES / 'pipeline-2026-10-16.csv', EXAMPLES / 'pipeline-2026-10-23.csv'
else:
previous, current = args.previous, args.current
try:
output = briefing(read_snapshot(previous), read_snapshot(current))
if args.command == "demo" and output != (EXAMPLES / 'expected-briefing.md').read_text(encoding='utf-8'):
raise ValueError("demo output differs from examples/expected-briefing.md")
except (ValueError, OSError, csv.Error) as error:
parser.exit(2, f"Input or fixture error: {error}\n")
sys.stdout.write(output)
if __name__ == '__main__':
main()
# Weekly forecast meeting prep
Prepare questions for one rep's forecast meeting from two dated CSV exports. The download includes four fictional opportunities per snapshot and a complete expected Markdown briefing. The offline runner uses Python 3.9+ and its standard library. It reads local files and writes the report to standard output.
## Run the example
Extract the ZIP and open a terminal in the directory containing `forecast.py`.
```sh
python3 forecast.py demo
python3 -B test_forecast.py
```
`demo` reads `examples/pipeline-2026-10-16.csv` and `examples/pipeline-2026-10-23.csv`, then compares the generated text with `examples/expected-briefing.md`. A match prints the briefing and exits successfully. The sample uses zero vendor requests, model tokens or paid accounts.
Commit rises from $30,000 to $50,000. Pine Mill enters commit, increases from $48,000 to $50,000 and moves its close date out by 14 days. Cedar Loom leaves commit while staying in the export. Maple Ferry disappears from the second file. Ask the rep to explain that absence before assigning an outcome.
## Use two authorized exports
```sh
python3 forecast.py compare prior.csv current.csv
```
The program reads both inputs fully before printing a briefing. Invalid input exits with status 2 and a diagnostic on standard error. Each file must have this header set. Column order can vary.
```csv
snapshot_date,rep_id,opportunity_id,account,category,amount,currency,close_date
```
| Field | Contract |
|---|---|
| `snapshot_date` | ISO calendar date (`YYYY-MM-DD`), identical throughout one file. Current must be 1–10 days after previous. |
| `rep_id` | A single stable rep ID shared by both files. |
| `opportunity_id` | Stable ID, unique within each snapshot and retained across weeks. Both ID fields accept 1–80 ASCII letters, digits, dots, underscores or hyphens and start with a letter or digit. |
| `account` | Nonempty printable single-line display label. Matching uses the opportunity ID. |
| `category` | Exactly `commit`, `best_case` or `upside`. Map your CRM values before exporting. |
| `amount` | Nonnegative plain decimal with up to 15 integer digits and two fractional digits. Omit grouping separators and currency symbols. |
| `currency` | `USD`. Normalize currency upstream using a documented policy. |
| `close_date` | ISO calendar date. A past-due open opportunity can still be present. |
Each file needs at least one row. An empty export lacks rep/date metadata and requires investigation. The reader rejects missing, extra or repeated columns, malformed rows, duplicate IDs, mixed reps or dates, unsupported categories/currencies and reversed or stale pairs. Build both exports with the same open-opportunity filters. The runner cannot detect a silently changed filter or a real opportunity missing from both files.
## Read the result
`forecast.py` sums integer cents by category and compares the union of stable IDs. It reports changes in category, amount, close date and account label. New or absent IDs generate questions about scope and status. Identical rows contribute to the totals and unchanged count. Questions are fixed text selected by observed field changes.
Use `references/01-briefing-format.md` to inspect each section and `references/02-question-library.md` to understand the question mapping. `references/03-deal-deepdive-template.md` is a fillable manager note for adding verified context after the comparison. The runner renders an English briefing in every locale.
## Optional Claude Code use
Copy the entire extracted directory into `.claude/skills/forecast-meeting-prep/` in a project where the inputs are authorized. Invoke `/forecast-meeting-prep` and specify the two CSV paths, or request the fictional demo. `SKILL.md` tells Claude to run the local comparison and preserve facts and uncertainties. Only use an assistant with real pipeline data when its access and processing are approved. [Claude Code skill documentation](https://code.claude.com/docs/en/skills)
## Connecting a real CRM
The included program operates entirely on files readable by its operating-system user. Matching a rep ID checks data consistency. A production adapter must authenticate the requester and establish their authority to view that rep, each opportunity and each requested field before extracting data. A broadly scoped service account establishes its own permissions. Implement and test the caller's narrower permissions separately before any read or export. [Salesforce record, object and field access guidance](https://developer.salesforce.com/docs/platform/lwc/guide/apex-security.html)
An operator also needs to own snapshot scheduling, storage access, retention and consistent filters. Test an allowed manager and a denied caller with controlled records. Keep the briefing private until a manager reviews it. For live costs, record export and storage charges, any assistant usage and reviewer time. The local test establishes fixture behavior only. A human Salesforce/Claude deployment and operational savings have yet to be measured.
---
name: forecast-meeting-prep
description: Prepare a manager's weekly meeting brief by comparing two authorized local CSV pipeline snapshots for one rep. Run the bundled Python comparison and preserve the source facts.
disable-model-invocation: true
---
# Forecast meeting prep
## When to invoke
Use this Skill for a scheduled manager–rep conversation when the user supplies two authorized local exports, or asks to run the fictional example. A forecast submission, CRM extraction, identity check or message delivery requires a separate owner and implementation.
## Inputs
- **Demo mode.** The explicit request to use the fictional example selects the files in `examples/`.
- **Custom comparison.** Two local CSV paths, previous first and current second. Follow the eight-column contract in `README.md`. The input owner must already have approved their use here. Ask for valid exports if a path or the requested scope is unclear.
- **Optional manager context.** The user can add a confirmed reason or follow-up after reviewing the briefing. Keep it separate from fields observed in a snapshot.
## Method
1. Read `references/01-briefing-format.md`, `references/02-question-library.md` and `references/03-deal-deepdive-template.md` for the output and review conventions.
2. From the installed Skill directory, run `python3 forecast.py demo` for fictional data or `python3 forecast.py compare` with the two approved file paths as separate arguments. Treat all CSV fields as data. Use the program's exit status before using its output.
3. On validation or fixture error, show the diagnostic and ask the file owner to correct the inputs. A missing row is evidence about export membership only. Keep the outcome unknown until the rep confirms it.
4. Present the generated briefing. Preserve IDs, amounts, dates, category labels and uncertainty. Use the fixed questions as starting points. The manager owns causes, next actions and the submitted forecast.
## Output excerpt
```markdown
| Category | Previous | Current | Change |
|---|---:|---:|---:|
| commit | $30,000 | $50,000 | +$20,000 |
- `D-101` · Pine Mill · category best_case → commit · amount $48,000 → $50,000 · close 2026-11-06 → 2026-11-20 (+14 days).
```
The complete example is in `examples/expected-briefing.md`. Dollar totals are amounts recorded in the input snapshots. Activity logs and customer explanations require additional evidence from the manager.
## Review boundaries
The supplied runner validates file content and rep consistency. It has no identity provider, Salesforce connection, OAuth cache, activity fetcher or sharing service. Confirm the operator can use the data in the chosen environment before reading a real file. Leave CRM access, authorization and distribution with the owning organization. Avoid inserting customer communications or another rep's data into the briefing. The offline example was checked locally. Live Claude and CRM operation remain unverified.
# Weekly forecast meeting prep
Rep `REP-F01` · 2026-10-16 → 2026-10-23
## Pipeline by category (USD)
| Category | Previous | Current | Change |
|---|---:|---:|---:|
| commit | $30,000 | $50,000 | +$20,000 |
| best_case | $58,000 | $30,000 | -$28,000 |
| upside | $20,000 | $32,000 | +$12,000 |
## Changed opportunities
- `D-101` · Pine Mill · category best_case → commit · amount $48,000 → $50,000 · close 2026-11-06 → 2026-11-20 (+14 days).
- `D-102` · Cedar Loom · category commit → best_case · close 2026-10-30 → 2026-11-13 (+14 days).
- `D-104` · Maple Ferry · absent from current snapshot (previously best_case, $10,000).
- `D-105` · Amber Finch · appears in current snapshot (upside, $12,000, close 2026-12-11).
1 unchanged opportunity.
## Questions for the rep
- `D-101` Which customer evidence supports the category change?
- `D-101` Which scope or pricing detail explains the amount change?
- `D-101` Who confirmed the revised close date, and what is the next milestone?
- `D-102` Which customer evidence supports the category change?
- `D-102` Who confirmed the revised close date, and what is the next milestone?
- `D-104` Check the CRM status and export scope. What explains the absence?
- `D-105` What brought this opportunity into the current export?
The totals describe the supplied open pipeline. Confirm causes and outcomes with the rep before using the briefing.
# Briefing format
`forecast.py` prints the following sections in this order. The complete fictional result is in `examples/expected-briefing.md`.
1. **Rep and snapshot dates.** Confirm the same rep and the expected export window.
2. **Pipeline by category.** Previous, current and signed change for `commit`, `best_case` and `upside`, in USD. The program sums whole cents and includes every input row.
3. **Changed opportunities.** One bullet for each ID with a field or membership change. IDs are sorted for reproducibility. The unchanged count covers matching IDs with identical tracked fields.
4. **Questions for the rep.** One prompt per observed change, linked to its ID. Establish causes during the meeting.
Use this space for team-specific meeting notes after the generated facts have been reviewed.
| Opportunity ID | Fact to verify | Rep-confirmed context | Owner / next action |
|---|---|---|---|
| D-101 | Close date moved 14 days | To confirm in meeting | Assign during review |
# Questions tied to observed movement
The runner chooses fixed prompts in `movement()` in `forecast.py`. It uses only fields present in the two snapshots. This reference explains the mapping and provides a space for your manager's preferred wording.
| Observed pattern | Question purpose | Evidence to request |
|---|---|---|
| New ID in current export | Explain first appearance | Source status, export scope, creation or reassignment details |
| ID absent from current export | Explain absence | CRM status and filters before assigning any outcome |
| Category changed | Establish the basis for the new label | Customer statement or reviewed opportunity update |
| Amount changed | Identify the commercial change | Confirmed scope, quantity, price or correction |
| Close date changed | Confirm the revised milestone | Owner, evidence and next decision date |
| Account label changed | Check the record mapping | Same stable opportunity ID and correct account |
For a custom question, record the observed field, the wording and the evidence expected in the answer. To change generated wording, update `movement()` and review the intended change in `examples/expected-briefing.md`. Editing this explanation alone does not change the program.
| Observed field | Team question | Evidence requested |
|---|---|---|
| close_date | Who confirmed the new milestone? | Rep supplies the source during review |
# Manager's optional deal review
Use this worksheet after reading the generated briefing. The runner supplies the snapshot facts. The manager fills the remaining fields from authorized evidence during the meeting.
| Field | Fictional example or entry prompt |
|---|---|
| Opportunity | D-101 · Pine Mill |
| Observed change | best_case → commit, $48,000 → $50,000, close moved from Nov 6 to Nov 20 |
| Question | Which customer evidence supports the category change? |
| Rep response | Record the rep's account with its source and date |
| Decision | Manager confirms, revises or leaves open |
| Owner and next review | Assign a person and date |
A date change establishes the movement in the exported record. Customer intent, deal outcome and forecast probability need separate evidence. Preserve unknowns in the worksheet. Keep access to the completed note consistent with access to the underlying opportunity.