Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
f20b2cf
feat(classifier): add Gemini AI semantic email classifier and filter …
don-petry Sep 27, 2026
b91d8c3
feat(drive): add Google Drive AI ingestion with dual-layer hybrid tag…
don-petry Sep 27, 2026
a0bac8e
feat(security): add strict No-PII pre-commit hook and CI verification
don-petry Sep 27, 2026
4b4057a
fix(reviews): address review comments [skip ci-relay]
don-petry Sep 27, 2026
1063e91
chore(gas): add clasp configuration for gmail-ai-classifier
don-petry Sep 27, 2026
455f18c
feat(classifier): add daily classification audit trigger and anomaly …
don-petry Sep 27, 2026
1d7398c
feat(classifier): expand Rule 2 utility funding rules and label shield
don-petry Sep 28, 2026
124ffcb
feat(classifier): add Google Drive taxonomy attachment persistence an…
don-petry Sep 29, 2026
9f280f7
fix(classifier): resolve SonarCloud reliability bugs, cognitive compl…
don-petry Sep 29, 2026
451f9db
feat(classifier): add Rule 19 for web monitoring alerts and configura…
don-petry Sep 29, 2026
d276e07
feat(classifier): migrate prompt architecture to Ontological Knowledg…
don-petry Sep 29, 2026
962c003
feat(classifier): align default note routing with 7-Canonical Domain …
don-petry Sep 30, 2026
f67c1db
feat(classifier): add historical attachment audit and backfill tool w…
don-petry Oct 1, 2026
83221e2
feat(classifier): add zero-arg IDE runners for attachment audit and b…
don-petry Oct 1, 2026
c029497
fix(classifier): treat commercial TOS updates as non-canonical broadcast
don-petry Oct 1, 2026
1b83565
refactor(classifier): remove one-time remediation code
don-petry Oct 1, 2026
af8425b
perf(classifier): add folder caching and time-budget protection to at…
don-petry Oct 1, 2026
afde01d
feat(classifier): curate canonical attachments and add detailed audit…
don-petry Oct 1, 2026
d698b9e
feat(classifier): archive machine telemetry and route school updates …
don-petry Oct 1, 2026
017eae8
feat(classifier): add primary party attribution and dynamic sub-label…
don-petry Oct 1, 2026
55de2db
feat(classifier): add reclassifyThreadsByQuery and runRealignmentAudi…
don-petry Oct 1, 2026
b604e0e
feat(gmail-classifier): support attachment relocation and TLD trackin…
don-petry Oct 2, 2026
3a5c937
fix(classifier): dynamic sister label discovery and name change search
don-petry Oct 2, 2026
07b3221
fix(classifier): add deterministic fallback and quota protection
don-petry Oct 2, 2026
7214746
refactor(classifier): rely strictly on Gemini semantic model without …
don-petry Oct 2, 2026
2f75f42
fix(gmail-ai-classifier): align ontological prompt and sublabel mappi…
don-petry Oct 5, 2026
0fef216
fix(classifier): prevent root domain tagging and classify surveys as …
don-petry Oct 5, 2026
5789e86
fix(classifier): support canonical sub-label attachment persistence a…
don-petry Oct 5, 2026
fd64f5d
fix(classifier): classify enterprise daily activity digests and newsl…
don-petry Oct 7, 2026
3939635
feat(classifier): route non-profit mass newsletters and tech platform…
don-petry Oct 11, 2026
19aea72
feat(classifier): route school broadcasts/digests to Updates and archive
don-petry Oct 11, 2026
792c886
feat(classifier): enforce Category-Driven Inbox Retention Invariant a…
don-petry Oct 12, 2026
193e159
feat(classifier): add sweepAllUnreadInboxEmails and secondary unread …
don-petry Oct 12, 2026
9adbefc
feat(classifier): tune school newsletter routing and category archive…
don-petry Oct 12, 2026
609dff8
feat(classifier): add deterministic fast-path pre-classifier and no-p…
don-petry Oct 13, 2026
8957254
feat(classifier): expand deterministic fast-path for iot, benefits, a…
don-petry Oct 13, 2026
53f757f
fix(gmail-ai-classifier): remediate review findings with concurrency …
rachel-petry Oct 16, 2026
ddf6051
fix(gmail-ai-classifier): harden batch loop resilience, time budget, …
rachel-petry Oct 16, 2026
d622db2
fix(gmail-ai-classifier): align gitHubSync parity, payload check, and…
rachel-petry Oct 16, 2026
5459f3d
fix(classifier): enhance school domain coverage, audit sublabels, and…
rachel-petry Oct 16, 2026
7b7b390
fix(classifier): enhance realignment domain resolution, audit DI, and…
rachel-petry Oct 16, 2026
6424157
fix(classifier): export formatAuditDigest and ensureUserLabel alias
rachel-petry Oct 16, 2026
5cfdbca
feat(classifier): add setupFourHourAuditTrigger and expand config schema
rachel-petry Oct 16, 2026
d4663a7
feat(classifier): add response_mime_type json and dynamic model fallback
rachel-petry Oct 16, 2026
2f77092
fix(classifier): calibrate audit to exempt compliant non-canonical th…
rachel-petry Oct 16, 2026
3706162
feat(classifier): enrich classification snippet with eligible attachm…
rachel-petry Oct 16, 2026
2943860
feat(classifier): map ontology sublabels to canonical Drive folders a…
rachel-petry Oct 17, 2026
304c9a7
feat(classifier): add CacheService caching, model cascade, and JSON t…
rachel-petry Oct 17, 2026
2214ffa
fix(classifier): normalize domain and sublabel extraction in auditCla…
rachel-petry Oct 17, 2026
edad868
perf(classifier): bypass inter-thread rate-limit sleep on determinist…
rachel-petry Oct 17, 2026
ab688aa
fix(classifier): refine model cascade matrix and prevent false missin…
rachel-petry Oct 17, 2026
5ec87c7
fix(classifier): enhance listAvailableGeminiModels with DI and null s…
rachel-petry Oct 18, 2026
6f0caa6
fix(classifier): circuit-break consecutive remote API failures to gua…
rachel-petry Oct 18, 2026
f8cef71
feat(classifier): expand deterministic fast-path for developer monito…
rachel-petry Oct 18, 2026
15da9ea
feat(classifier): add direct carrier parcel tracking deterministic rules
rachel-petry Oct 18, 2026
f417297
feat(classifier): expand deterministic fast-paths for banking, receip…
rachel-petry Oct 18, 2026
e509d99
perf(classifier): optimize attachment deduplication with lazy MD5 has…
rachel-petry Oct 18, 2026
4df05a7
fix(classifier): harden message accessors and Utilities fallback in b…
rachel-petry Oct 19, 2026
19071dd
fix(classifier): harden ensureUserLabel with advanced fallback and er…
rachel-petry Oct 19, 2026
09e32f8
fix(config): harden PropertiesService and Session boundaries in getAi…
rachel-petry Oct 19, 2026
7dff76e
fix(classifier): harden message and thread property accessors across …
rachel-petry Oct 19, 2026
ddc09a2
feat(audit): add ARCHIVED_WITHOUT_ACTION telemetry to identify second…
rachel-petry Oct 19, 2026
6d1feb6
feat(classifier): route credit scores, smart reorders, school broadca…
rachel-petry Oct 19, 2026
c44de1a
feat(classifier): route apiary events and BroodMinder telemetry to sk…
rachel-petry Oct 19, 2026
161d8a2
feat(classifier): tune primary inbox rules, secondary sweep, and surv…
rachel-petry Oct 20, 2026
4b6b4cc
feat(classifier): route bank rate, travel, and saas plan alerts to sk…
rachel-petry Oct 23, 2026
037eb02
feat(classifier): shield sso alerts in primary; route pypi and charit…
rachel-petry Oct 23, 2026
5a85778
feat(classifier): route school tryouts, bible study digests, and heal…
rachel-petry Oct 23, 2026
854c48c
test(classifier): optimize in-memory caching benchmark loop
rachel-petry Oct 24, 2026
3220620
feat(classifier): add durable queueing, circuit breaker, and 06_Work_…
rachel-petry Oct 25, 2026
c305ed7
feat(classifier): add runMissedEmailsSweep runner for recent missed e…
rachel-petry Oct 25, 2026
6c97a3c
feat(classifier): auto-revert and queue missed career emails on trigger
rachel-petry Oct 25, 2026
b627ca2
fix(classifier): prioritize career sweep, fix label conflict, and upd…
rachel-petry Oct 25, 2026
eae5896
feat(classifier): add native auto-unzip for archive attachments in Drive
rachel-petry Oct 25, 2026
22a464c
fix(config): auto-sanitize deprecated gemini models to gemini-2.5-flash
rachel-petry Oct 25, 2026
cd00dba
chore: merge origin/main into feature/ontological-knowledge-graph-cla…
rachel-petry Oct 26, 2026
d8f2591
feat(classifier): add fast-paths for telemetry, receipts, insurance, …
rachel-petry Oct 26, 2026
563d368
fix(classifier): resolve PR #649 review comments and expand test cove…
rachel-petry Oct 26, 2026
c8666b1
refactor(classifier): deduplicate heuristic rules and address remaini…
rachel-petry Oct 26, 2026
29106df
fix(classifier): resolve follow-up review comments on PII scanner and…
rachel-petry Oct 26, 2026
f9d19db
fix(classifier): require promotional evidence before archiving retail…
rachel-petry Oct 26, 2026
5f2bf82
fix(classifier): address healthcare, signature, pagination, and CI wo…
rachel-petry Oct 26, 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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ jobs:
- name: Type check
run: npm run typecheck

- name: Strict No-PII Check
run: npm run check:pii

autofix:
needs: build-and-test
if: >
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -403,5 +403,6 @@ playwright-report/
# compliance-ci-trigger
# ci-trigger-298
.dev-lead/
.pii-rules.json
# compliance-ci-trigger
# ci-trigger-298
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/bin/sh
set -e
node scripts/check-no-pii.js --staged
npx lint-staged
npm run typecheck
26 changes: 13 additions & 13 deletions docs/proposals/proposal-001-ai-gemini-semantic-classifier.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,15 @@ The `Gmail.Users.Settings.Filters.create` call additionally requires the **Advan

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_School`** | 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 |
| 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 |

---

Expand Down Expand Up @@ -137,7 +137,7 @@ When Gemini classifies a new sender with high confidence (>= 95%), the script au
**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_School`) should be reviewed manually before trusting auto-generated filters in production.
- 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:**

Expand Down Expand Up @@ -188,6 +188,6 @@ src/gmail-ai-classifier/

## Discussion & Tracking

- Issue Tracked: [#491](https://github.com/open-source-projects/google-app-scripts/issues/491)
- Pull Request Tracked: [#492](https://github.com/open-source-projects/google-app-scripts/pull/492)
- Repository: `open-source-projects/google-app-scripts`
- 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`
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"lint": "eslint src",
"format": "prettier --write .",
"typecheck": "tsc --noEmit",
"check": "prettier --check . && npm run lint",
"check:pii": "node scripts/check-no-pii.js",
"check": "prettier --check . && npm run lint && npm run check:pii",
"prepare": "husky"
},
"lint-staged": {
Expand Down
321 changes: 321 additions & 0 deletions scripts/check-no-pii.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
#!/usr/bin/env node
// pii-scanner-definition

/**
* Strict No-PII Scanner for petry-projects/google-app-scripts
* Ensures that open-source code, docs, and tests remain completely generic
* and free of personal household identities, addresses, and private businesses.
*
* Additional household-specific rules can be supplied out-of-band via:
* - `PII_CUSTOM_RULES_JSON` environment variable (JSON array of { name, pattern, flags?, description, whitelist? })
* - `.pii-rules.json` in the repository root (gitignored)
*
* Usage:
* node scripts/check-no-pii.js # Scans repository files from repo root
* node scripts/check-no-pii.js --staged # Scans only git-staged index blobs
*/

const fs = require('fs')
const path = require('path')
const { execFileSync } = require('child_process')

const REPO_ROOT = path.resolve(__dirname, '..')

// Base PII patterns with human-readable descriptions
const BASE_PROHIBITED_RULES = [
{
name: 'Household Surnames',
pattern: /\b(Petry)\b/gi,
description:
'Household surname found outside of allowed open-source organizational context.',
whitelist: [
/petry-projects/i,
/github\.com\/don-petry/i,
/github\.com\/petry-projects/i,
/Copyright \(c\) \d{4} Don Petry/i,
/Don Petry \u2013/i,
/Petry-Projects \u2013/i,
],
},
{
name: 'Personal Gmail Addresses',
pattern: /\b[a-zA-Z0-9._%+-]+@gmail\.com\b/gi,
description:
'Personal Gmail addresses must not be in open-source files; use generic placeholders (e.g. user@example.com).',
whitelist: [
/^household-member@gmail\.com$/i,
/^your-email@gmail\.com$/i,
/^primary@gmail\.com$/i,
/^testuser@gmail\.com$/i,
],
},
{
name: 'US Social Security Numbers',
pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
description: 'Social Security Numbers must never be committed.',
whitelist: [/^000-00-0000$/, /^123-45-6789$/],
},
]

function loadCustomRules() {
const customRules = []
let rawJson = process.env.PII_CUSTOM_RULES_JSON || ''
if (!rawJson) {
const localRulesPath = path.join(REPO_ROOT, '.pii-rules.json')
if (fs.existsSync(localRulesPath)) {
rawJson = fs.readFileSync(localRulesPath, 'utf8')
}
}
if (!rawJson) return customRules

try {
const parsed = JSON.parse(rawJson)
if (Array.isArray(parsed)) {
for (const entry of parsed) {
if (entry && entry.name && entry.pattern) {
const flags = entry.flags
? entry.flags.includes('g')
? entry.flags
: `${entry.flags}g`
: 'gi'
customRules.push({
name: entry.name,
pattern: new RegExp(entry.pattern, flags),
description:
entry.description ||
'Prohibited custom PII pattern matched in repository.',
whitelist: Array.isArray(entry.whitelist)
? entry.whitelist.map((w) => new RegExp(w, 'i'))
: undefined,
})
}
}
}
} catch (err) {
console.error('Error parsing custom PII rules:', err.message)
process.exit(1)
}
return customRules
}

const PROHIBITED_RULES = [...BASE_PROHIBITED_RULES, ...loadCustomRules()]

// Extensions and directories to scan
const SCANNABLE_EXTENSIONS = new Set([
'.js',
'.ts',
'.gs',
'.json',
'.md',
'.yml',
'.yaml',
'.html',
])

const IGNORED_DIRS = new Set([
'node_modules',
'.git',
'coverage',
'dist',
'build',
'.wt-',
])

function toPosixPath(p) {
return p.split(path.sep).join('/')
}

function isIgnoredPath(relPath) {
const posixPath = toPosixPath(relPath)
const parts = posixPath.split('/')
for (const part of parts) {
if (IGNORED_DIRS.has(part) || part.startsWith('.wt-')) {
return true
}
}
if (
posixPath === 'scripts/check-no-pii.js' ||
posixPath === '.pii-rules.json'
) {
return true
}
return false
}

function getStagedFiles() {
try {
const output = execFileSync(
'git',
['diff', '--cached', '-z', '--name-only', '--diff-filter=ACMR'],
{
cwd: REPO_ROOT,
encoding: 'utf8',
}
)
return output
.split('\0')
.map((f) => f.trim())
.filter(Boolean)
} catch (err) {
console.error('Error fetching staged files from git:', err.message)
process.exit(1)
}
}

function getAllRepoFiles(dir = REPO_ROOT, base = '') {
let results = []
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const relPath = toPosixPath(path.join(base, entry.name))
if (isIgnoredPath(relPath)) continue

const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
results = results.concat(getAllRepoFiles(fullPath, relPath))
} else if (entry.isFile()) {
const ext = path.extname(entry.name).toLowerCase()
if (SCANNABLE_EXTENSIONS.has(ext)) {
results.push(relPath)
}
}
}
return results
}

function isMatchWhitelisted(line, match, whitelist) {
if (!whitelist || whitelist.length === 0) return false
const matchedText = match[0]
const matchStart = match.index
const matchEnd = matchStart + matchedText.length

for (const wl of whitelist) {
if (wl.test(matchedText)) {
return true
}
const globalFlags = wl.flags.includes('g') ? wl.flags : `${wl.flags}g`
const globalWl = new RegExp(wl.source, globalFlags)
for (const wlMatch of line.matchAll(globalWl)) {
const wlStart = wlMatch.index
const wlEnd = wlStart + wlMatch[0].length
if (matchStart >= wlStart && matchEnd <= wlEnd) {
return true
}
}
}
return false
}

function readTargetFileContent(relPath, isStagedOnly) {
const posixRelPath = toPosixPath(relPath)
if (isStagedOnly) {
try {
return execFileSync('git', ['show', `:${posixRelPath}`], {
cwd: REPO_ROOT,
encoding: 'utf8',
})
} catch {
return null
}
}

const fullPath = path.resolve(REPO_ROOT, relPath)
if (!fs.existsSync(fullPath)) return null
return fs.readFileSync(fullPath, 'utf8')
}

function scanFile(relPath, { isStagedOnly = false } = {}) {
if (isIgnoredPath(relPath)) return []
const content = readTargetFileContent(relPath, isStagedOnly)
if (!content) return []

const lines = content.split('\n')
const violations = []

lines.forEach((line, index) => {
// Support inline ignore: // pii-ignore or <!-- pii-ignore -->
if (line.includes('pii-ignore')) return

for (const rule of PROHIBITED_RULES) {
const flags = rule.pattern.flags.includes('g')
? rule.pattern.flags
: `${rule.pattern.flags}g`
const globalPattern = new RegExp(rule.pattern.source, flags)

for (const match of line.matchAll(globalPattern)) {
if (isMatchWhitelisted(line, match, rule.whitelist)) {
continue
}

violations.push({
file: toPosixPath(relPath),
line: index + 1,
rule: rule.name,
match: match[0],
description: rule.description,
})
}
}
})

return violations
}

function main() {
const isStagedOnly = process.argv.includes('--staged')
const filesToScan = isStagedOnly
? getStagedFiles().filter((f) => {
const ext = path.extname(f).toLowerCase()
return SCANNABLE_EXTENSIONS.has(ext) && !isIgnoredPath(f)
})
: getAllRepoFiles()

if (filesToScan.length === 0) {
console.log('✅ No scannable files found. PII check passed.')
process.exit(0)
}

let totalViolations = []
for (const file of filesToScan) {
const violations = scanFile(file, { isStagedOnly })
if (violations.length > 0) {
totalViolations = totalViolations.concat(violations)
}
}

if (totalViolations.length > 0) {
console.error(
'\n❌ PII CHECK FAILED — Prohibited Personal Information Detected:'
)
console.error(
'─────────────────────────────────────────────────────────────────'
)
for (const v of totalViolations) {
console.error(
`• [${v.rule}] ${v.file}:${v.line}\n Matched: "${v.match}"\n Remedy: ${v.description}\n`
)
}
console.error(
'─────────────────────────────────────────────────────────────────'
)
console.error(
'The petry-projects/google-app-scripts repository is a generic open-source'
)
console.error(
'harness. Personal student names, private businesses, and household addresses'
)
console.error(
'must be injected via ScriptProperties (CUSTOM_PROMPT_RULES), never committed.'
)
console.error(
'─────────────────────────────────────────────────────────────────\n'
)
process.exit(1)
}

console.log(
`✅ Strict No-PII check passed (${filesToScan.length} file(s) scanned).`
)
process.exit(0)
}

main()
Loading
Loading