Skip to content

Commit e6014f4

Browse files
authored
Merge pull request #72 from propublica/develop
V3
2 parents 75a7bbf + 4e8844b commit e6014f4

41 files changed

Lines changed: 4928 additions & 777 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.clasp.json

Lines changed: 0 additions & 4 deletions
This file was deleted.

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,7 @@ Thumbs.db
2424
.gemini/
2525

2626
# Git worktrees
27-
.worktrees/
27+
.worktrees/
28+
29+
# Clasp project file (contains project ID and rootDir)
30+
.clasp.json

CLAUDE.md

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,12 @@ HtmlService can only serve `.html` files — all JavaScript and CSS must be inli
8282
4. Emits `dist/Sidebar.html` as an asset
8383
5. Deletes the intermediate `.js` chunk so clasp never pushes it as a `.gs` file
8484

85-
The `src/Sidebar.html` template is also read at test time by `__tests__/helpers/sidebar-fixtures.ts` to keep DOM fixtures structurally in sync with the real template.
86-
8785
### Module Dependency Graph
8886

8987
**Server:**
9088
```
9189
src/server/index.ts (entry point — menu, 4 tool orchestrators, UI handlers, re-exports custom functions)
92-
├── src/server/config.ts (CONFIG object: API key property name, model, column names, limits)
90+
├── src/server/config.ts (CONFIG object: API key property name, model name, size limits)
9391
├── src/server/api.ts (callGeminiAPI, buildGeminiPayload, invokeGemini — pure HTTP adapter via UrlFetchApp;
9492
│ buildGeminiPayload resolves ToolId[] via TOOL_REGISTRY, splits grounding vs function tools)
9593
├── src/server/inference.ts (runInference — unified inference handler for menu-triggered AI calls; no SpreadsheetApp dep;
@@ -99,13 +97,14 @@ src/server/index.ts (entry point — menu, 4 tool orchestrators, UI han
9997
├── src/server/types.ts (server-only types: AppConfig, GeminiRequest, GeminiTool discriminated union,
10098
│ GeminiInlineData, GeminiFunctionDeclaration, DriveFileInfo; never imported by client)
10199
├── src/server/drive.ts (extractTextUniversal, fetchAndEncodeFile, checkDriveService)
102-
├── src/server/dialog.ts (HTML_TEMPLATE string for AI mode selection modal)
100+
├── src/server/rich-text.ts (CellContent, TextRange interfaces; buildRichInferenceCellContent, buildRichGroundingCellContent —
101+
│ pure layer between GeminiResponse and Sheets cell content; no GAS globals)
103102
├── src/server/customFunctions.ts (SSI — Sheets custom function; calls invokeGemini directly; always returns string,
104103
│ uses "[SSI Error: ...]" format)
105104
├── src/server/utils.ts (extractId, isValidDriveLink, createSeededRandom, getAllFilesRecursive, sampleRows,
106105
│ truncateText, findOrCreateColumn, writeColumn, flattenArg)
107-
└── src/shared/types.ts (RPC boundary ONLY — ToolId union, RunConfig, PrepRecipeParams, PrepRecipeResult;
108-
all with optional tools?: ToolId[])
106+
└── src/shared/types.ts (RPC boundary ONLY — ToolId union, RunConfig, PrepRecipeParams, PrepRecipeResult,
107+
ImportDriveLinksConfig, ExtractTextConfig; all with optional tools?: ToolId[])
109108
```
110109

111110
**Client:**
@@ -118,17 +117,22 @@ src/client/sidebar-entry.ts (thin init — instantiates all panels, creates Rou
118117
└── src/client/tools.ts (TOOL_CATALOG: ToolCatalogEntry[] — display metadata for sidebar TagList;
119118
│ hardcoded at build time, no RPC needed)
120119
└── src/client/recipes.ts (RECIPES registry — RecipeDefinition[] for all standard recipes)
120+
└── src/client/job-store.ts (JobStore class — tracks active jobs, polls getJobProgress, notifies subscribers)
121121
└── src/client/panels/
122122
│ ├── tool-list.ts (ToolListPanel — entry screen, dispatches to tool or recipes)
123123
│ ├── configure-ai-run.ts (ConfigureAIRunPanel — column mapping, row range, tool selection, AI run)
124+
│ ├── import-drive-links.ts (ImportDriveLinksPanel — Drive folder import UI; mime-type filter, output column)
125+
│ ├── extract-text.ts (ExtractTextPanel — text extraction UI; source/output column, row range)
124126
│ ├── recipes-list.ts (RecipesListPanel — browsable list of recipes)
125127
│ └── recipe.ts (RecipePanel — generic panel driven by RecipeParams; prep → cook flow)
126128
└── src/client/components/ (reusable UI components)
127129
├── tag-list.ts (TagList — multi-select tag chips; accepts string[] or {label,value}[] items)
128130
├── single-tag-list.ts (SingleTagList — exclusive-select tag chips)
129131
├── row-range.ts (RowRange — start/end row inputs)
130132
├── lockable-field.ts (LockableField — value + lock/unlock toggle; optional onUnlock callback)
131-
└── recipe-prep-cook.ts (RecipePrepCook — 4-state machine: idle/prepping/prep-complete/cooking)
133+
├── recipe-prep-cook.ts (RecipePrepCook — 4-state machine: idle/prepping/prep-complete/cooking)
134+
├── panel-loader.ts (PanelLoader — drives panel loading skeleton: progress bar, spinner, message)
135+
└── job-indicator.ts (JobIndicator — renders active/failed jobs to #job-strip; persists across navigation)
132136
└── src/shared/types.ts
133137
134138
src/client/google.d.ts (compile-time type stub for google.script.run — uses declare global{} pattern)
@@ -194,13 +198,6 @@ beforeEach(() => {
194198
// Then invoke capturedSuccess(...) / capturedFailure(...) to simulate GAS callbacks.
195199
```
196200

197-
**Shared DOM fixtures:** `__tests__/helpers/sidebar-fixtures.ts` exports:
198-
- `FULL_SIDEBAR_HTML` — the sidebar HTML template read from `src/Sidebar.html` at test time (placeholders stripped), so tests stay structurally in sync with the real template without manual drift.
199-
- `setupConfigPanel(headers?)` — sets `document.body.innerHTML` and populates all tag containers.
200-
- `setupWithSelections(opts)` — calls `setupConfigPanel` then pre-selects values via `applyPreset`.
201-
202-
The `__tests__/helpers/` directory is excluded from test discovery via `testPathIgnorePatterns` in `jest.config.cjs`.
203-
204201
**Coverage:** Run `npm run test:coverage` to collect coverage and enforce per-file thresholds. Coverage is opt-in — the pre-commit hook runs `jest --bail` without `--coverage`.
205202

206203
Two files are excluded from coverage collection entirely:
@@ -213,15 +210,7 @@ See `docs/plans/2026-02-18-testing-coverage-design.md` for full rationale.
213210

214211
### Tool 4 — Spreadsheet Column Requirements
215212

216-
`runBatchAI` maps column headers by name. The active sheet must contain these exact headers (case-sensitive):
217-
218-
| Config key | Column header |
219-
| --- | --- |
220-
| `SOURCE_DRIVE` | `source_drive` |
221-
| `SOURCE_TEXT` | `source_text` |
222-
| `SYS_PROMPT` | `system_prompt` |
223-
| `USER_PROMPT` | `user_prompt` |
224-
| `OUTPUT` | `ai_inference` |
213+
`runBatchAI` maps column headers by name via `RunConfig` (user-selected in the sidebar — no hardcoded column names). The user selects which columns serve as user prompt inputs, drive file inputs, system prompt, and output. `runBatchAI` calls `resolveColumns` to locate them by header string and `findOrCreateColumn` to create the output column if absent.
225214

226215
The Gemini API key must be set as a Script Property (`GEMINI_API_KEY`) in Apps Script > Project Settings > Script Properties before Tool 4 will run.
227216

CONTRIBUTING.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Contributing
2+
3+
## Branch Workflow
4+
5+
```
6+
feature-branch → develop (PR + code review)
7+
develop → main (PR = release gate)
8+
```
9+
10+
Feature work happens on branches, merged to `develop` via PR. When ready to ship, `develop` is merged to `main` via a PR containing manual QA instructions — that merge is the release gate.
11+
12+
## Adding Features
13+
14+
### Adding a new Gemini tool
15+
16+
See [Tool System](docs/architecture.md#tool-system) in the architecture docs — it's a three-file change.
17+
18+
### Adding a recipe
19+
20+
Recipes are defined in `src/client/recipes.ts` as entries in the `RECIPES` array. Each `RecipeDefinition` describes the recipe's display metadata, the form fields shown during prep, and how those fields map to a `RunConfig` passed to Run AI. Adding a recipe is entirely client-side and requires no server changes — it's one of the most accessible contributions to make.
21+
22+
### Exposing a new server function
23+
24+
See [Build Pipeline](docs/architecture.md#build-pipeline) in the architecture docs — you must both export from `index.ts` and add a footer stub in `rollup.config.js`. If the function is callable from the client, also update `src/client/google.d.ts`.
25+
26+
## Testing
27+
28+
Tests live in `__tests__/`. Run them with:
29+
30+
```bash
31+
npm test # all tests
32+
npm run test:watch # watch mode
33+
npm run test:coverage # with per-file coverage thresholds
34+
```
35+
36+
### Mocking GAS globals
37+
38+
Apps Script globals (`UrlFetchApp`, `DriveApp`, `SpreadsheetApp`, etc.) must be set on `globalThis` **before** importing the module under test, because imports execute immediately:
39+
40+
```ts
41+
(globalThis as any).UrlFetchApp = { fetch: jest.fn() };
42+
const { callGeminiAPI } = await import("../src/server/api");
43+
```
44+
45+
### Mocking `google.script.run`
46+
47+
Capture the success/failure handlers registered by the function under test, then invoke them manually to simulate GAS callbacks:
48+
49+
```ts
50+
const mockRun = {
51+
withSuccessHandler: jest.fn().mockReturnThis(),
52+
withFailureHandler: jest.fn().mockReturnThis(),
53+
myServerFunction: jest.fn(),
54+
};
55+
(globalThis as unknown as { google: unknown }).google = { script: { run: mockRun } };
56+
57+
let capturedSuccess: (v: unknown) => void;
58+
mockRun.withSuccessHandler.mockImplementation((fn) => {
59+
capturedSuccess = fn;
60+
return mockRun;
61+
});
62+
// Later: capturedSuccess(mockValue) to simulate a successful GAS response.
63+
```
64+
65+
### Coverage
66+
67+
Coverage is enforced per-file. Run `npm run test:coverage` to check thresholds. Two files are excluded from coverage collection:
68+
69+
- `src/server/index.ts` — deeply coupled to SpreadsheetApp UI globals, not unit-tested.
70+
- `src/client/sidebar-entry.ts` — calls `init()` immediately at module load time, before `beforeEach` can set up the DOM.
71+
72+
## Code Style
73+
74+
Follows the Google TypeScript Style Guide, enforced by ESLint + Prettier + pre-commit hooks:
75+
76+
- Named exports only (no default exports)
77+
- `const` by default; no `var`, no `namespace`
78+
- `===` always; avoid `any` (prefer `unknown`)
79+
- UpperCamelCase for types/interfaces, lowerCamelCase for functions/variables, CONSTANT_CASE for constants
80+
- Semicolons required, double quotes, trailing commas
81+
- Explicit return types on exported functions
82+
- Prefix unused parameters with `_`
83+
84+
Run `npm run lint:fix` and `npm run format` before pushing.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 ProPublica
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

0 commit comments

Comments
 (0)