From 60530fa23ec62afecc6de87ba37edfeaff57411c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 12:18:42 +0000 Subject: [PATCH 1/3] v2.0.3: product-rule checks (bank names, version bump, duplicate versions) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013Z8iBUNj6C627mxmm1CurV --- .github/bank-names.txt | 34 ++++++++ .github/scripts/check_rules.py | 123 ++++++++++++++++++++++++++++ .github/workflows/product-rules.yml | 33 ++++++++ CHANGELOG.md | 12 +++ CLAUDE.md | 4 +- README.md | 4 +- index.html | 2 +- js/views-settings.js | 2 +- 8 files changed, 208 insertions(+), 6 deletions(-) create mode 100644 .github/bank-names.txt create mode 100644 .github/scripts/check_rules.py create mode 100644 .github/workflows/product-rules.yml diff --git a/.github/bank-names.txt b/.github/bank-names.txt new file mode 100644 index 0000000..7eef539 --- /dev/null +++ b/.github/bank-names.txt @@ -0,0 +1,34 @@ +# Bank names that must not appear in FinLens's code, samples or current docs. +# One per line, matched case-insensitively as whole words. Lines starting with # are ignored. +# Use "bank" (or a neutral account kind) instead. See CLAUDE.md §0 / product rules. +# +# Names that are also everyday words (e.g. "Popular", "Discover", "Citizens") are +# listed only in a form that can't be an everyday word. +Ally +Chase +Banco Popular +Popular Bank +FirstBank +First Bank +Oriental +OFG +Santander +Wells Fargo +Bank of America +BofA +Citibank +Capital One +American Express +Amex +US Bank +U.S. Bank +PNC +Truist +TD Bank +Charles Schwab +Fidelity +SoFi +Marcus +Navy Federal +Synchrony +Barclays diff --git a/.github/scripts/check_rules.py b/.github/scripts/check_rules.py new file mode 100644 index 0000000..6fe1d3b --- /dev/null +++ b/.github/scripts/check_rules.py @@ -0,0 +1,123 @@ +"""FinLens product-rule checks. + +1. No bank names in tracked files (list in .github/bank-names.txt). + Past history is left as is, by Luis's rule: only the newest CHANGELOG entry is + checked, and published eval results / archived docs are skipped. +2. The version is the same in every place CLAUDE.md §17 lists. +3. On a pull request: the version is newer than the base branch's, and no other + open PR has already claimed it (a PR picks its version when it's opened). + +Env (set by the workflow on pull requests): BASE_REF, PR_NUMBER, GITHUB_REPOSITORY, GH_TOKEN. +""" +import json, os, pathlib, re, subprocess, sys + +root = pathlib.Path(__file__).resolve().parents[2] +errors = 0 + +def error(msg, file=None, line=None): + global errors + errors += 1 + loc = f" file={file}" + (f",line={line}" if line else "") if file else "" + print(f"::error{loc}::{msg}") + +# 1. Bank names --------------------------------------------------------------- +SKIP_PREFIXES = ("docs/eval-runs/", "docs/archive/", ".git/", "node_modules/") +SKIP_FILES = {".github/bank-names.txt", ".github/scripts/check_rules.py", + "docs/EVAL.md", "docs/eval-results.json", + # The v1.12.0 data migration has to recognize the old bank-named ids + # and account names in saved data to rename them. + "js/migrate.js"} + +names = [l.strip() for l in (root / ".github/bank-names.txt").read_text().splitlines() + if l.strip() and not l.startswith("#")] +pattern = re.compile(r"(? 1 else "" + +files = subprocess.run(["git", "ls-files"], cwd=root, capture_output=True, text=True, check=True).stdout.split("\n") +hits = 0 +for rel in filter(None, files): + if rel in SKIP_FILES or rel.startswith(SKIP_PREFIXES): + continue + for m in pattern.finditer(rel): # file and folder names count too + hits += 1 + error(f'Bank name "{m.group(1)}" in a file name', file=rel) + path = root / rel + try: + text = path.read_text(encoding="utf-8") + except (UnicodeDecodeError, IsADirectoryError, FileNotFoundError): + continue + if rel == "CHANGELOG.md": + entry = newest_changelog_entry(text) + offset = text[: text.find(entry)].count("\n") if entry else 0 + text_lines = entry.splitlines() + else: + offset, text_lines = 0, text.splitlines() + for i, line in enumerate(text_lines, 1): + for m in pattern.finditer(line): + hits += 1 + error(f'Bank name "{m.group(1)}" — use "bank" or a neutral account kind instead', file=rel, line=i + offset) +print(f"Bank names: {hits} found in {len([f for f in files if f])} tracked files") + +# 2. Version consistency ------------------------------------------------------ +def top_version(changelog): + m = re.search(r"(?m)^## \[(\d+)\.(\d+)\.(\d+)\]", changelog) + return tuple(map(int, m.groups())) if m else None + +def vstr(v): + return ".".join(map(str, v)) if v else "none" + +ver = top_version((root / "CHANGELOG.md").read_text(encoding="utf-8")) +if not ver: + error("No version entry found at the top of CHANGELOG.md", file="CHANGELOG.md") +else: + v = vstr(ver) + places = { + "index.html": rf"v{re.escape(v)} · FinLens", + "README.md": rf"\*\*Version:\*\* v{re.escape(v)}\b", + "js/views-settings.js": rf"FinLens v{re.escape(v)}(?![0-9.])", + "CLAUDE.md": rf"\*\*Current version:\*\* v{re.escape(v)}\b", + } + for rel, pat in places.items(): + if not re.search(pat, (root / rel).read_text(encoding="utf-8")): + error(f"Version v{v} (top of CHANGELOG.md) is missing from {rel} — see CLAUDE.md §17", file=rel) + if not re.search(rf"\*FinLens v{re.escape(v)} —", (root / "README.md").read_text(encoding="utf-8")): + error(f"Version v{v} is missing from the README footer line", file="README.md") + print(f"Version: v{v}") + +# 3. Pull requests: newer than base, and not claimed by another open PR ------- +base, pr, repo = os.environ.get("BASE_REF"), os.environ.get("PR_NUMBER"), os.environ.get("GITHUB_REPOSITORY") +if ver and base: + r = subprocess.run(["git", "show", f"origin/{base}:CHANGELOG.md"], cwd=root, capture_output=True, text=True) + base_ver = top_version(r.stdout) if r.returncode == 0 else None + if base_ver and ver <= base_ver: + error(f"Version v{vstr(ver)} is not newer than {base}'s v{vstr(base_ver)}. Add a new CHANGELOG entry and version (CLAUDE.md §17).", + file="CHANGELOG.md", line=1) + else: + print(f"Newer than {base} (v{vstr(base_ver)})") +if ver and pr and repo: + def gh(path): + out = subprocess.run(["gh", "api", "--paginate", path], capture_output=True, text=True) + return out.stdout if out.returncode == 0 else None + raw = gh(f"repos/{repo}/pulls?state=open&per_page=100") + if raw is None: + print("::warning::Could not list open pull requests; skipped the duplicate-version check.") + else: + prs = json.loads(raw.replace("][", ",")) + taken = [] + for p in prs: + if str(p["number"]) == str(pr): + continue + body = subprocess.run(["gh", "api", f"repos/{repo}/contents/CHANGELOG.md?ref={p['head']['sha']}", + "-H", "Accept: application/vnd.github.raw"], capture_output=True, text=True) + if body.returncode == 0 and top_version(body.stdout) == ver: + taken.append(f"#{p['number']}") + if taken: + error(f"Version v{vstr(ver)} is already used by open PR {', '.join(taken)}. Pick the next free version.", + file="CHANGELOG.md", line=1) + else: + print(f"Not used by any of the other {len(prs) - 1} open PRs") + +sys.exit(1 if errors else 0) diff --git a/.github/workflows/product-rules.yml b/.github/workflows/product-rules.yml new file mode 100644 index 0000000..be4c06b --- /dev/null +++ b/.github/workflows/product-rules.yml @@ -0,0 +1,33 @@ +name: Product rules + +# FinLens house rules that are easy to forget by hand: no bank names, the version +# bumped in every place, and no two open PRs claiming the same version. +on: + pull_request: + push: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + pull-requests: read + +concurrency: + group: product-rules-${{ github.ref }} + cancel-in-progress: true + +jobs: + rules: + name: Bank names and version + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # the base branch's CHANGELOG is compared on pull requests + - name: Check + env: + BASE_REF: ${{ github.base_ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} + GH_TOKEN: ${{ github.token }} + run: python3 .github/scripts/check_rules.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a41488d..678e448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). --- +## [2.0.3] — 2026-11-04 + +### Technical — CI +- **Product-rule checks on every pull request** (`.github/workflows/product-rules.yml`, `.github/scripts/check_rules.py`). Nothing in the app changes. + - **No bank names** in tracked files or file names, from the list in `.github/bank-names.txt` (whole words, any case). Past history is left as is: only the newest CHANGELOG entry is checked, and `docs/eval-runs/`, `docs/archive/`, `docs/EVAL.md` and `docs/eval-results.json` are skipped. `js/migrate.js` is skipped too, because the data migration has to recognize the old ids. + - **Version in every place** CLAUDE.md §17 lists: the top CHANGELOG version must also be in the `index.html` footer, both README lines, Settings → General and CLAUDE.md §1. + - **On pull requests:** the version must be newer than the base branch's, and no other open PR may already use it. This catches a taken version before merging instead of after. +- **Expected to fail on `master` until the redesign stack (#16–#51) merges**, which removes the bank names. On the top of the stack (#51) it passes. +- Part of the **v2.0 — Public launch** milestone. Left open on purpose until the rename and custom domain are ready. + +--- + ## [1.10.5] — 2026-11-02 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index d895c1b..c72cdef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,7 +59,7 @@ Until then, this section is the rule. FinLens is a **single-origin, zero-build, vanilla JavaScript personal finance dashboard** that runs entirely in the browser. There is no server, no npm, no bundler, no framework, and no TypeScript. The entry point is `index.html`; everything else is a plain `.js` file in the `js/` folder. -**Current version:** v1.10.5 (updated in `index.html` footer, `README.md`, and `CHANGELOG.md` on every release). +**Current version:** v2.0.3 (updated in `index.html` footer, `README.md`, and `CHANGELOG.md` on every release). **What it does:** Imports CSV exports from Ally Bank (Checking + Savings) and Chase credit cards, parses and normalizes them into a unified transaction model, and renders a multi-tab financial dashboard with budgeting, debt tracking, savings forecasting, investment planning, and Claude-powered AI analysis. @@ -798,7 +798,7 @@ Multiple Chase card files can be loaded simultaneously. Each gets a unique `acco --- -*Last updated: v1.10.5 — 2026-11-02 (no section changes: Import Logs race fix; v1.10.4: §8, §13 single `~` is literal; §21 excerpts escape `~`; v1.10.3: §13 refusals; §21 declined runs; v1.10.2: §13 summary states its dates; §21 per-month derivations; v1.10.1: §13 `AI_MAX_TOKENS`, thinking and cut-off answers; v1.10.0: §21 AI Eval added; §13 `callClaude()` raw mode; v1.9.6: §14 folder access lasts until the last tab closes, not one page load; v1.9.5: §7 chart guard; v1.9.4: views-db.js changelog reads CRLF checkouts; v1.9.3: §14 folder permission: re-request on click, Reconnect banner, browser copy wins; v1.9.2: §0 branch, PR and approval workflow added; v1.9.1: §4.5 rehydrate transactions loaded from JSON; v1.9.0: §20 Data & Reset added; v1.8.1: §10 re-upload dedup rewritten for the occurrence-suffix fix; v1.8.0: AI Analysis restored with API-key and output hardening; `renderMarkdown()` added; `samples/` added; §1–3, §13, §14, §17 corrected to match the code)* +*Last updated: v2.0.3 — 2026-11-04 (no section changes: product-rule checks on pull requests; v1.10.5 — 2026-11-02: no section changes: Import Logs race fix; v1.10.4: §8, §13 single `~` is literal; §21 excerpts escape `~`; v1.10.3: §13 refusals; §21 declined runs; v1.10.2: §13 summary states its dates; §21 per-month derivations; v1.10.1: §13 `AI_MAX_TOKENS`, thinking and cut-off answers; v1.10.0: §21 AI Eval added; §13 `callClaude()` raw mode; v1.9.6: §14 folder access lasts until the last tab closes, not one page load; v1.9.5: §7 chart guard; v1.9.4: views-db.js changelog reads CRLF checkouts; v1.9.3: §14 folder permission: re-request on click, Reconnect banner, browser copy wins; v1.9.2: §0 branch, PR and approval workflow added; v1.9.1: §4.5 rehydrate transactions loaded from JSON; v1.9.0: §20 Data & Reset added; v1.8.1: §10 re-upload dedup rewritten for the occurrence-suffix fix; v1.8.0: AI Analysis restored with API-key and output hardening; `renderMarkdown()` added; `samples/` added; §1–3, §13, §14, §17 corrected to match the code)* --- diff --git a/README.md b/README.md index 1cd3fc5..4201e68 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > A zero-install, browser-native finance dashboard. Drop in your bank CSVs, get instant clarity on your money. -**Version:** v1.10.5  ·  **Stack:** Vanilla JS, HTML, CSS  ·  **Install:** None +**Version:** v2.0.3  ·  **Stack:** Vanilla JS, HTML, CSS  ·  **Install:** None --- @@ -260,4 +260,4 @@ See [CHANGELOG.md](CHANGELOG.md) for the full release history. --- -*FinLens v1.10.5 — Built for one person's finances, designed to be useful for anyone's.* +*FinLens v2.0.3 — Built for one person's finances, designed to be useful for anyone's.* diff --git a/index.html b/index.html index ee48405..e5e2b5c 100644 --- a/index.html +++ b/index.html @@ -326,7 +326,7 @@
⚙️ Settings
diff --git a/js/views-settings.js b/js/views-settings.js index 6f7d389..548313e 100644 --- a/js/views-settings.js +++ b/js/views-settings.js @@ -49,7 +49,7 @@ function _renderGeneralSettings(root) { -
FinLens v1.10.5 ·${State.transactions.length} transactions loaded${State.lastRefresh?' · Refreshed '+dayjs(State.lastRefresh).format('MMM D, YYYY h:mm A'):''}
`; +
FinLens v2.0.3 ·${State.transactions.length} transactions loaded${State.lastRefresh?' · Refreshed '+dayjs(State.lastRefresh).format('MMM D, YYYY h:mm A'):''}
`; root.appendChild(displayCard); /* ---- Anthropic API Key ---- */ From 3918bde94b7c6b2fa3c9a6fa6aef0fa6ee2003fb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 12:19:19 +0000 Subject: [PATCH 2/3] Product rules: warn instead of passing when the base CHANGELOG can't be read Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013Z8iBUNj6C627mxmm1CurV --- .github/scripts/check_rules.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/scripts/check_rules.py b/.github/scripts/check_rules.py index 6fe1d3b..c364ec6 100644 --- a/.github/scripts/check_rules.py +++ b/.github/scripts/check_rules.py @@ -92,7 +92,9 @@ def vstr(v): if ver and base: r = subprocess.run(["git", "show", f"origin/{base}:CHANGELOG.md"], cwd=root, capture_output=True, text=True) base_ver = top_version(r.stdout) if r.returncode == 0 else None - if base_ver and ver <= base_ver: + if not base_ver: + print(f"::warning::Could not read CHANGELOG.md on origin/{base}; skipped the newer-version check.") + elif ver <= base_ver: error(f"Version v{vstr(ver)} is not newer than {base}'s v{vstr(base_ver)}. Add a new CHANGELOG entry and version (CLAUDE.md §17).", file="CHANGELOG.md", line=1) else: From 49889fed74bea88e94df9e7aa19f1076975bdd46 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 13:01:15 +0000 Subject: [PATCH 3/3] Renumber to v2.0.4: the rename PR (#58) takes v2.0.3, so versions follow the merge order Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013Z8iBUNj6C627mxmm1CurV --- CHANGELOG.md | 2 +- CLAUDE.md | 4 ++-- README.md | 4 ++-- index.html | 2 +- js/views-settings.js | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 678e448..2bbf595 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). --- -## [2.0.3] — 2026-11-04 +## [2.0.4] — 2026-11-04 ### Technical — CI - **Product-rule checks on every pull request** (`.github/workflows/product-rules.yml`, `.github/scripts/check_rules.py`). Nothing in the app changes. diff --git a/CLAUDE.md b/CLAUDE.md index c72cdef..cd4ebf5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,7 +59,7 @@ Until then, this section is the rule. FinLens is a **single-origin, zero-build, vanilla JavaScript personal finance dashboard** that runs entirely in the browser. There is no server, no npm, no bundler, no framework, and no TypeScript. The entry point is `index.html`; everything else is a plain `.js` file in the `js/` folder. -**Current version:** v2.0.3 (updated in `index.html` footer, `README.md`, and `CHANGELOG.md` on every release). +**Current version:** v2.0.4 (updated in `index.html` footer, `README.md`, and `CHANGELOG.md` on every release). **What it does:** Imports CSV exports from Ally Bank (Checking + Savings) and Chase credit cards, parses and normalizes them into a unified transaction model, and renders a multi-tab financial dashboard with budgeting, debt tracking, savings forecasting, investment planning, and Claude-powered AI analysis. @@ -798,7 +798,7 @@ Multiple Chase card files can be loaded simultaneously. Each gets a unique `acco --- -*Last updated: v2.0.3 — 2026-11-04 (no section changes: product-rule checks on pull requests; v1.10.5 — 2026-11-02: no section changes: Import Logs race fix; v1.10.4: §8, §13 single `~` is literal; §21 excerpts escape `~`; v1.10.3: §13 refusals; §21 declined runs; v1.10.2: §13 summary states its dates; §21 per-month derivations; v1.10.1: §13 `AI_MAX_TOKENS`, thinking and cut-off answers; v1.10.0: §21 AI Eval added; §13 `callClaude()` raw mode; v1.9.6: §14 folder access lasts until the last tab closes, not one page load; v1.9.5: §7 chart guard; v1.9.4: views-db.js changelog reads CRLF checkouts; v1.9.3: §14 folder permission: re-request on click, Reconnect banner, browser copy wins; v1.9.2: §0 branch, PR and approval workflow added; v1.9.1: §4.5 rehydrate transactions loaded from JSON; v1.9.0: §20 Data & Reset added; v1.8.1: §10 re-upload dedup rewritten for the occurrence-suffix fix; v1.8.0: AI Analysis restored with API-key and output hardening; `renderMarkdown()` added; `samples/` added; §1–3, §13, §14, §17 corrected to match the code)* +*Last updated: v2.0.4 — 2026-11-04 (no section changes: product-rule checks on pull requests; v1.10.5 — 2026-11-02: no section changes: Import Logs race fix; v1.10.4: §8, §13 single `~` is literal; §21 excerpts escape `~`; v1.10.3: §13 refusals; §21 declined runs; v1.10.2: §13 summary states its dates; §21 per-month derivations; v1.10.1: §13 `AI_MAX_TOKENS`, thinking and cut-off answers; v1.10.0: §21 AI Eval added; §13 `callClaude()` raw mode; v1.9.6: §14 folder access lasts until the last tab closes, not one page load; v1.9.5: §7 chart guard; v1.9.4: views-db.js changelog reads CRLF checkouts; v1.9.3: §14 folder permission: re-request on click, Reconnect banner, browser copy wins; v1.9.2: §0 branch, PR and approval workflow added; v1.9.1: §4.5 rehydrate transactions loaded from JSON; v1.9.0: §20 Data & Reset added; v1.8.1: §10 re-upload dedup rewritten for the occurrence-suffix fix; v1.8.0: AI Analysis restored with API-key and output hardening; `renderMarkdown()` added; `samples/` added; §1–3, §13, §14, §17 corrected to match the code)* --- diff --git a/README.md b/README.md index 4201e68..6d2f61c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > A zero-install, browser-native finance dashboard. Drop in your bank CSVs, get instant clarity on your money. -**Version:** v2.0.3  ·  **Stack:** Vanilla JS, HTML, CSS  ·  **Install:** None +**Version:** v2.0.4  ·  **Stack:** Vanilla JS, HTML, CSS  ·  **Install:** None --- @@ -260,4 +260,4 @@ See [CHANGELOG.md](CHANGELOG.md) for the full release history. --- -*FinLens v2.0.3 — Built for one person's finances, designed to be useful for anyone's.* +*FinLens v2.0.4 — Built for one person's finances, designed to be useful for anyone's.* diff --git a/index.html b/index.html index e5e2b5c..5bc910f 100644 --- a/index.html +++ b/index.html @@ -326,7 +326,7 @@
⚙️ Settings
diff --git a/js/views-settings.js b/js/views-settings.js index 548313e..1bfa804 100644 --- a/js/views-settings.js +++ b/js/views-settings.js @@ -49,7 +49,7 @@ function _renderGeneralSettings(root) { -
FinLens v2.0.3 ·${State.transactions.length} transactions loaded${State.lastRefresh?' · Refreshed '+dayjs(State.lastRefresh).format('MMM D, YYYY h:mm A'):''}
`; +
FinLens v2.0.4 ·${State.transactions.length} transactions loaded${State.lastRefresh?' · Refreshed '+dayjs(State.lastRefresh).format('MMM D, YYYY h:mm A'):''}
`; root.appendChild(displayCard); /* ---- Anthropic API Key ---- */