Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
38ead62
docs: add AI-Powered Gemini Semantic Classifier feature proposal
don-petry Jul 28, 2026
06794e1
docs: update proposal with 7-Domain Standard Taxonomy Matrix & MECE V…
don-petry Jul 28, 2026
fa96d54
docs: add Inbox Preservation rule (processing to Drive does NOT remov…
don-petry Jul 28, 2026
2496323
docs: resolve merge conflict and add Inbox Preservation rule
don-petry Jul 28, 2026
19ae580
docs: use Single Global Processed Label marker to eliminate sidebar l…
don-petry Jul 28, 2026
b78ac1d
docs: detail Idempotency & Single-Click Reprocessing workflows
don-petry Jul 28, 2026
8021d5a
feat: add native Gemini AI classifier and auto-filter engine module
don-petry Jul 28, 2026
cd7e914
chore: dev-lead update (review-changes) [skip ci-relay]
donpetry-bot Jul 28, 2026
034b23c
chore: dev-lead update (review-changes) [skip ci-relay]
donpetry-bot Jul 28, 2026
f1a8dc9
chore: dev-lead update (review-changes) [skip ci-relay]
donpetry-bot Jul 28, 2026
15ee6da
fix(ci): auto-fix for SonarCloud Code Analysis [skip ci-relay]
donpetry-bot Jul 28, 2026
d8154d3
fix(ci): auto-fix for SonarCloud Code Analysis [skip ci-relay]
donpetry-bot Jul 28, 2026
6ffb452
fix(ci): auto-fix for SonarCloud Code Analysis [skip ci-relay]
donpetry-bot Jul 28, 2026
4996a6e
chore: dev-lead update (review-changes) [skip ci-relay]
donpetry-bot Jul 28, 2026
bae0762
fix(ci): auto-fix for SonarCloud Code Analysis [skip ci-relay]
donpetry-bot Jul 28, 2026
f1ae720
fix(reviews): address review comments [skip ci-relay]
donpetry-bot Jul 28, 2026
3e934e5
fix(ci): auto-fix for SonarCloud Code Analysis [skip ci-relay]
donpetry-bot Jul 28, 2026
05db839
fix(ci): auto-fix for SonarCloud Code Analysis [skip ci-relay]
donpetry-bot Jul 28, 2026
ccf9ad5
fix(ci): auto-fix for SonarCloud Code Analysis [skip ci-relay]
donpetry-bot Jul 28, 2026
257460e
fix(ci): auto-fix for SonarCloud Code Analysis [skip ci-relay]
donpetry-bot Jul 28, 2026
995b18a
docs: detail Unclassified & Edge-Case email handling safety nets
don-petry Jul 28, 2026
3fcd9cc
feat: add native Google Apps Script GitHub Sync module (UrlFetchApp -…
don-petry Jul 28, 2026
1d65a71
chore(ci): apply prettier/eslint auto-fixes
github-actions[bot] Jul 28, 2026
c1ab1ba
fix(bot): address bot feedback [skip ci-relay]
donpetry-bot Jul 28, 2026
899ad30
fix(bot): address bot feedback [skip ci-relay]
donpetry-bot Jul 28, 2026
d5f21ee
test: add performance and scalability benchmark test suite for long d…
don-petry Jul 28, 2026
ee1ae05
feat: enhance gitHubSync with targeted insertion and 409 SHA collisio…
don-petry Jul 28, 2026
f06010c
chore: dev-lead update (review-changes) [skip ci-relay]
donpetry-bot Jul 28, 2026
64b46ec
feat: add Option A multi-account user email resolution and deployment…
don-petry Jul 29, 2026
b41cdd1
chore(ci): apply prettier/eslint auto-fixes
github-actions[bot] Jul 29, 2026
48c8a3e
feat: add modelEndpoint to classifier config
donpetry-bot Jul 29, 2026
f1f8aa1
feat: add appsscript.json manifest for clasp deployment
don-petry Jul 29, 2026
581cc10
chore: add .claspignore to exclude Node.js Jest test files from Apps …
don-petry Jul 29, 2026
6f6185d
fix: export getAiClassifierConfig and standalone GAS logic
don-petry Jul 29, 2026
bcdde8f
fix: safe Gemini API HTTP status checking and response guards
don-petry Jul 29, 2026
ea2bf36
fix: update Gemini API endpoint to gemini-2.0-flash with automatic fa…
don-petry Jul 29, 2026
db9dad7
feat: add listAvailableGeminiModels helper and expanded model endpoints
don-petry Jul 29, 2026
3c76f60
feat: set gemini-3.1-flash-lite as 2nd fallback endpoint
don-petry Jul 29, 2026
ea307b3
feat: add 404 auto-create note functionality with Progressive Disclos…
don-petry Jul 29, 2026
e75dc95
feat: reorder endpoints to highest-level model first and add HTTP 429…
don-petry Jul 29, 2026
0cb6756
feat: set Gemini 3.5 Flash Lite as primary endpoint #1
don-petry Jul 29, 2026
184fb59
feat: set Gemini 3.5 Flash Lite as #1 and Gemini 3.1 Flash Lite as #2
don-petry Jul 29, 2026
8ecfaab
chore: remove deprecated 1.x and 2.x Gemini models, use 3.x models only
don-petry Jul 29, 2026
466fad3
chore: dev-lead update (review-changes) [skip ci-relay]
donpetry-bot Jul 29, 2026
683d5a9
feat: add Family/DJ-Rachel sub-label and strict prompt classification…
don-petry Jul 29, 2026
dff51b9
feat: exclude health newsletters and news bulletins from 04_Family_He…
don-petry Jul 29, 2026
b566b95
feat: add setupFiveMinuteTrigger and stopAllTriggers for automated ba…
don-petry Jul 29, 2026
1656f46
feat: map student college/career planning meetings to Family/School
don-petry Jul 29, 2026
d813331
feat: map MyBroodMinder alerts to 07_Community_NonProfit with sub-lab…
don-petry Jul 29, 2026
7037ee1
feat: map ParentSquare and MCAA school emails to Family/School-Toby
don-petry Jul 29, 2026
4befa97
fix: add 2-second rate limit backoff delay on HTTP 429 and stabilize …
don-petry Jul 30, 2026
c0408e0
feat: parse and honor Retry-After header and retryDelay on HTTP 429
don-petry Jul 30, 2026
ba84dfb
chore: strictly enforce Gemini 3.x models ONLY
don-petry Jul 30, 2026
28404ed
feat: add Gemma 4 31B (gemma-4-31b-it) as primary endpoint #1
don-petry Jul 30, 2026
57590f5
fix: add extractJsonSubstring helper to parse Gemma 4 31B markdown fo…
don-petry Jul 30, 2026
a763bd6
feat: exclude tech webinars and commercial store marketing from core …
don-petry Jul 30, 2026
1ab8558
feat: add automated action execution (trash, archive, mark_read) and …
don-petry Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/add-to-project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
add-to-project:
permissions:
contents: read
uses: petry-projects/.github/.github/workflows/add-to-project-reusable.yml@add-to-project/v1-stable # NOSONAR(githubactions:S7637) first-party channel ref
uses: petry-projects/.github/.github/workflows/add-to-project-reusable.yml@add-to-project/v1-stable # NOSONAR(githubactions:S7637) first-party channel ref
with:
project_id: PVT_kwDOD2inqs4BZq3-
project_url: https://github.com/orgs/petry-projects/projects/1
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/auto-rebase.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ permissions: {}
jobs:
auto-rebase:
permissions:
contents: write # update-branch via GITHUB_TOKEN (may touch .github/workflows/)
contents: write # update-branch via GITHUB_TOKEN (may touch .github/workflows/)
pull-requests: write # post comments on PRs
uses: petry-projects/.github/.github/workflows/auto-rebase-reusable.yml@auto-rebase/v2-stable # NOSONAR(githubactions:S7637) first-party channel ref
secrets: inherit # NOSONAR(githubactions:S7635) first-party trusted reusable
uses: petry-projects/.github/.github/workflows/auto-rebase-reusable.yml@auto-rebase/v2-stable # NOSONAR(githubactions:S7637) first-party channel ref
secrets: inherit # NOSONAR(githubactions:S7635) first-party trusted reusable
Comment thread
don-petry marked this conversation as resolved.
4 changes: 2 additions & 2 deletions .github/workflows/dependabot-automerge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@ jobs:
permissions:
contents: read
pull-requests: read
uses: petry-projects/.github/.github/workflows/dependabot-automerge-reusable.yml@dependabot-automerge/v2-stable # NOSONAR(githubactions:S7637) first-party channel ref
secrets: inherit # NOSONAR(githubactions:S7635) first-party trusted reusable
uses: petry-projects/.github/.github/workflows/dependabot-automerge-reusable.yml@dependabot-automerge/v2-stable # NOSONAR(githubactions:S7637) first-party channel ref
secrets: inherit # NOSONAR(githubactions:S7635) first-party trusted reusable
4 changes: 2 additions & 2 deletions .github/workflows/pr-auto-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ on:
# to catch CI turning green on a PR.
# TODO: replace "CI" with your repository's CI workflow name(s).
workflow_run:
workflows: ["CI"]
workflows: ['CI']
types: [completed]
# check_suite covers third-party CI checks (e.g. SonarCloud, external apps).
check_suite:
Expand All @@ -50,7 +50,7 @@ jobs:
pull-requests: read
checks: read
actions: read
uses: petry-projects/.github/.github/workflows/pr-auto-review-reusable.yml@pr-auto-review/v1-stable # NOSONAR(githubactions:S7637) first-party channel ref
uses: petry-projects/.github/.github/workflows/pr-auto-review-reusable.yml@pr-auto-review/v1-stable # NOSONAR(githubactions:S7637) first-party channel ref
secrets:
# Canonical-first fallback (.github-private#1326). The reusable resolves this
# secret BY NAME, so only the VALUE changes — the passed key stays named
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6
with:
node-version: '20'
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Generate coverage report
run: npm test -- --coverage
- name: SonarCloud Scan
if: ${{ env.SONAR_TOKEN != '' }}
uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1
193 changes: 193 additions & 0 deletions docs/proposals/proposal-001-ai-gemini-semantic-classifier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# Feature Proposal 001: Native Gemini AI Semantic Email Classifier & 7-Domain Taxonomy Engine

## Executive Summary

This proposal introduces a native **Gemini AI Semantic Email Classifier & Standard Taxonomy Engine** into the `google-app-scripts` suite.

By calling the Gemini REST API (`gemini-1.5-flash`) natively via Google Apps Script's `UrlFetchApp.fetch()`, incoming unclassified emails are semantically categorized into **7 Standard Canonical Domains**, and high-confidence classifications automatically generate permanent Gmail filter rules via the Advanced Gmail Service.

---

## Implementation Prerequisites

### 1. User Opt-In Configuration

The classifier requires explicit user opt-in before processing email content with an external AI API:

- A `GEMINI_CLASSIFIER_ENABLED` Script Property must be set to `true` to activate processing.
- Users must acknowledge that email metadata (sender, subject, body snippet) is transmitted to the Gemini API.

> **Not yet implemented:** The `GEMINI_CLASSIFIER_ENABLED` guard and user acknowledgment step are not present in the current version; `processEmailsWithAiClassifier()` runs unconditionally.

### 2. Data Minimization & Redaction Boundary

Before sending email content to the Gemini API, the implementation applies:

- **Sender**: Full sender address (required for filter generation accuracy).
- **Subject**: Full subject line.
- **Body snippet**: Maximum 1,200 characters of plain text; HTML tags and quoted-reply blocks are stripped via `getCleanBody()` before transmission. _(Not yet implemented: the current version uses `getPlainBody().substring(0, 1200)` without quoted-reply stripping.)_
- Sensitive patterns (e.g., SSN-like strings, credit card numbers) should be redacted from the body snippet before it is sent. _(Not yet implemented.)_

### 3. Gemini Provider Data-Retention

This implementation targets the Google Gemini API under a Google Workspace account. Administrators must verify the Gemini API data retention and logging settings for their organization before enabling this classifier on accounts containing sensitive or regulated data.
Comment thread
don-petry marked this conversation as resolved.

### 4. API Key Storage (User-Only Script Properties)

The Gemini API key **must** be stored in Google Apps Script **Script Properties** (per-script, not Document or User Properties):

```javascript
PropertiesService.getScriptProperties().setProperty(
'GEMINI_API_KEY',
'<your-key>'
)
```

Never hard-code the API key in `config.gs` or any committed file.

### 5. Required OAuth Scopes

Declare the following scopes in `appsscript.json`:

```json
{
"oauthScopes": [
"https://www.googleapis.com/auth/gmail.labels",
"https://www.googleapis.com/auth/gmail.modify",
"https://www.googleapis.com/auth/gmail.settings.basic",
"https://www.googleapis.com/auth/script.external_request"
]
}
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

The `Gmail.Users.Settings.Filters.create` call additionally requires the **Advanced Gmail Service** to be enabled in the Apps Script project under **Services**.

---

## Handling Unclassified & Edge-Case Mail

- **Transactional & 2FA Passcodes**: Verification codes, password resets, and 2FA emails are skipped by Drive ingestion to prevent clogging Drive with 1-line passcodes.
- **Inbox Safety Net**: Unclassified emails are **NEVER deleted or hidden**. They remain safely in the user's INBOX for standard reading and review.
- **Optional `Unclassified` Badge**: Ambiguous emails can be tagged with an optional `Unclassified` label for easy visual filtering.

---

## Idempotency & Reprocessing Workflows

- **Query Guard**: Ingestion routines process threads matching `label:Category -label:Processed`.
- **Deduplication Safety Nets**: MD5 native checksums (`file.getMd5Checksum()`) prevent duplicate attachment saves; thread markers (`[THREAD:id]`) prevent duplicate text logs.
- **Single-Click Reprocessing**: Removing the `Processed` label from an email thread in Gmail causes the script to re-evaluate, re-clean, and re-process the thread on its next run.

---

## Clean Ingestion Marker: Single Global `Processed` Label

- **Eliminating Duplicate Labels**: Rather than creating parallel `-Processed` labels (which doubles sidebar labels from 20 to 40+), the script applies a **single global `Processed` label** (or Green Star badge) to indicate Drive ingestion completion.
- **Category Preservation**: The thread retains its canonical category label.
- **Inbox Preservation**: Processing an email into Google Drive **MUST NOT remove the message from the user's INBOX**. The email thread remains visible in the Inbox for human review until explicitly archived or deleted by the user.

---

## Standard 7-Domain Taxonomy Matrix (PARA / ISO 15489 / MECE Aligned)

The classifier operates against a standardized, mutually exclusive, collectively exhaustive (MECE) 7-domain taxonomy:

| 7 Canonical Domains | Standard PARA Area | Scope & Contents |
| :--------------------------- | :---------------------------- | :------------------------------------------------------------------------------------- |
| **`01_Household`** | Real Estate & Living Space | Primary residence, secondary properties, rental properties, property archives |
| **`02_Finance_Legal`** | Financial & Legal Protections | Taxes, banking, insurance policies, court records, custody, estate planning |
| **`03_Vehicles`** | Transportation & Assets | Vehicle searches, vehicle titles, maintenance logs |
| **`04_Family_Health`** | Family Welfare & Health | Children, school records, medical appointments, therapy sessions, educational programs |
| **`05_Tech_Infrastructure`** | IT & Home Automation | Home automation, NAS backups, scripting tools, network gear, software licenses |
| **`06_Work_Career`** | Professional Life & Income | Employment records, resumes, interview materials, expense reports, admin notes |
| **`07_Community_NonProfit`** | Civic Duty & Service | Charity organizations, community board roles, faith outreach |

---

## Classifier Response Contract

The Gemini classifier prompt requests a response conforming to the following JSON schema:

```json
{
"canonical_label": "<one of the configured canonicalDomains strings>",
"confidence": 0.98,
"reasoning": "<one-sentence explanation>"
}
```

**Field definitions:**

- `canonical_label` (string, required): Must be one of the exact strings from `config.canonicalDomains`. The implementation validates this value and rejects responses that do not match a configured label.
- `confidence` (number, required): A float in the range `[0.0, 1.0]` representing the classifier's certainty. Values outside this range are treated as invalid.
- `reasoning` (string, required): A brief one-sentence explanation of the classification decision.

**Validation and fallback behavior:**

- If the Gemini API returns an HTTP error, malformed JSON, or a refused response, `classifyEmailWithGemini` returns `null` and the thread is skipped without modification (no label applied, no filter created).
- If the parsed response is missing required fields, or `canonical_label` does not exactly match one of the configured domains, the response is rejected and treated as `null`.
- An abstain/unknown outcome is implicit: any non-conforming response leaves the thread unlabeled and unprocessed, making it eligible for the next run.
Comment thread
don-petry marked this conversation as resolved.

---

## Self-Learning Gmail Filter Generation

When Gemini classifies a new sender with high confidence (>= 95%), the script automatically calls `Gmail.Users.Settings.Filters.create` to create a permanent static Gmail filter rule. Future emails from that sender are processed instantly by Gmail's fast native filter engine at 0 cost.

**Confidence calibration and staged approval:**

- Only classifications with `confidence >= autoFilterConfidenceThreshold` (default: `0.95`) trigger filter creation.
- Senders categorized under sensitive domains (e.g., `02_Finance_Legal`, `04_Family_Health`) should be reviewed manually before trusting auto-generated filters in production.

**Duplicate filter detection:**

- Before creating a new filter, the implementation should call `Gmail.Users.Settings.Filters.list` to check for an existing filter matching the same sender address and skip creation if one already exists.

**Label existence and resolution:**

- The `ensureGmailLabel()` helper creates the target label if it does not exist before the filter is created.
- If label creation fails, filter creation is skipped for that sender.

**Correction and rollback semantics:**

- If a manual label correction is detected (user re-labels a thread after auto-classification), the script should update or delete the corresponding filter rule on the next run.
- A `rollbackGmailFilter()` operation can retrieve and delete previously created filters via `Gmail.Users.Settings.Filters.list` + `Gmail.Users.Settings.Filters.delete`.

---

## Key Capabilities

1. **Zero-Configuration Semantic Classification**:
- Classifies incoming emails from unknown senders based on semantic intent, sender, subject, and body snippet into target canonical labels.
2. **Self-Learning Gmail Filter Generation**:
- When Gemini classifies a new sender with high confidence (>= 95%), the script automatically creates a permanent static Gmail filter rule.
3. **Single-Click Reprocessing**:
- Simply remove the `Processed` label from any thread to re-run and re-ingest.
4. **100% Cloud-Native & Serverless**:
- Executes natively inside Google Apps Script via `UrlFetchApp.fetch()`. Requires zero external servers, Docker containers, or Python infrastructure.

---

## Code Module Design

Target file structure in `google-app-scripts`:

```text
src/gmail-ai-classifier/
├── README.md
├── code.gs
├── config.gs
├── src/
│ └── index.js
└── tests/
├── code.test.js
└── integration.test.js
Comment thread
don-petry marked this conversation as resolved.
```

---

## Discussion & Tracking

- Issue Tracked: [#491](https://github.com/petry-projects/google-app-scripts/issues/491)
- Pull Request Tracked: [#492](https://github.com/petry-projects/google-app-scripts/pull/492)
- Repository: `petry-projects/google-app-scripts`
Loading
Loading