Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
48 changes: 45 additions & 3 deletions .github/workflows/mega-linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,68 @@ concurrency:
group: ${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true

permissions:
contents: read
permissions: {}

jobs:
megalinter:
name: MegaLinter
runs-on: ubuntu-latest
permissions:
# write is required so the auto-fix step can push linter fixes back to the
# branch. Scoped to this job rather than the workflow. Fixes are only ever
# pushed to branches in this repository — see FIX_BRANCH below.
contents: write
env:
# Branch that fixes are checked out from and pushed to. Empty for pull
# requests from forks, whose token is read-only and whose head branch does
# not exist here; empty makes actions/checkout use its default ref and
# disables the commit step, so a fork PR still gets linted, just not fixed.
FIX_BRANCH: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && (github.head_ref || github.ref_name) || '' }}
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ env.FIX_BRANCH }}
# Credentials are deliberately persisted so the auto-fix step can push.
# This is not the usual hardening default; it is required for
# APPLY_FIXES_MODE: commit to work.
persist-credentials: true # zizmor: ignore[artipacked]

- name: MegaLinter
id: ml
uses: oxsecurity/megalinter/flavors/javascript@ef3e84b8b836d76db562d0f3ed7da61e8fd538bc # v9.6.0
env:
VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Apply fixes on every event and commit them straight to the branch.
# Which linters may fix is controlled by APPLY_FIXES in .mega-linter.yml.
APPLY_FIXES_EVENT: all
APPLY_FIXES_MODE: commit

- name: Commit and push applied linter fixes
# !cancelled() is load-bearing: the MegaLinter step exits non-zero
# whenever any linter reports an error (here, the security scanners
# always do), and by default that skips every later step — so without
# this the fixes would never be pushed.
# has_updated_sources is 1 only when a linter actually rewrote a file.
# The follow-up push re-runs this workflow once; that run finds nothing
# left to fix, so it converges rather than looping.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Re-run checks on the auto-fix commit

When a same-repository PR or main run applies fixes, this push uses the persisted repository GITHUB_TOKEN, but events generated by that token do not trigger another workflow run. The generated commit therefore receives no MegaLinter validation, contrary to this convergence assumption; on a PR it may also become the new head without required checks. Use a GitHub App/PAT that can trigger workflows, or explicitly validate the rewritten tree before pushing. See GitHub's GITHUB_TOKEN documentation.

Useful? React with 👍 / 👎.

if: >
!cancelled() &&
steps.ml.outputs.has_updated_sources == 1 &&
env.FIX_BRANCH != ''
uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0
with:
branch: ${{ env.FIX_BRANCH }}
commit_message: 'style: apply MegaLinter auto-fixes'
commit_user_name: megalinter-bot
commit_user_email: megalinter-bot@users.noreply.github.com
# Without this the author defaults to github.actor, which attributes
# a machine-generated commit to whoever happened to push.
commit_author: megalinter-bot <megalinter-bot@users.noreply.github.com>
# Only ever commit real source fixes — never MegaLinter's own output.
file_pattern: ':!megalinter-reports'

- name: Archive MegaLinter reports
if: always()
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ dist/
# Test coverage
coverage/

# MegaLinter run output (regenerated every CI run, uploaded as an artifact)
megalinter-reports/

# Logs
*.log
npm-debug.log*
Expand Down
26 changes: 25 additions & 1 deletion AGENT.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
# Agent Guide: Media Clusterer

## Tech Stack

- **Frontend**: Vite (v8+), TypeScript, Vanilla CSS
- **AI**: Transformers.js (v4.2.0, Multimodal Nomic embeddings)
- **AI**: Three interchangeable embedding backends selected by `settings.modelVariant`:
- `sapiens2-*` (**default** `sapiens2-fp16`) — `src/sapiens2.ts`, raw onnxruntime-web
- `nomic` — Transformers.js (v4.2.0) multimodal Nomic embeddings
- `chrome-ai` — `src/chromeAI.ts`, Chrome Prompt API caption → `nomic-embed-text` vector

All three produce 768-dim L2-normalized vectors.
- **Projections**: DruidJS (UMAP, t-SNE, PCA, Isomap, LLE, MDS, Sammon, TriMap)
- **Database**: IndexedDB (for vector caching)
- **Deployment**: Cloudflare Pages

## Documentation

- **Architecture decisions** live in `docs/adr/` — see `docs/adr/README.md` for the convention.
Write an ADR when a change is hard to reverse or constrains later work.
- **Feature plans** live in root-level `*_PLAN.md` files.

## Core Workflows

### Build & Deploy

- Build: `npm run build`
- Deploy: `npm run deploy` (requires `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` in `.env`)
- Type-check: `npm run type-check`

### Testing

- Run all tests: `npm test`
- Coverage: `npm run test:coverage`

## Coding Standards

- **Types**: Always use strict TypeScript. Define interfaces in `src/types.ts`. Avoid `any` and `unknown` in 99% of circumstances; prefer precise types or generics.
- **Logic**: Use the `IProjection` interface for dimensionality reduction algorithms.
- **State**: Centralized in the `state` object in `src/app.ts`.
Expand All @@ -27,22 +42,26 @@
## Architecture

### Application Modes

1. **AI Mode** (default): Loads embeddings, runs projections, enables semantic search
2. **Viewer-Only Mode**: Skips AI, arranges by folder/date grid, no search

### State Management

- All state in `state` object (phase, files, vectors, points, clusters, thumbnails, settings)
- URL state managed via History API (`#folder:...`, `#dt:...`)
- Session persistence via localStorage (resume capability)

### Resource Management

- Object URLs created lazily via `URL.createObjectURL(file)` in `lazyDecodeThumbnail`
- Resources MUST be cleaned up before processing new files:
- Close ImageBitmaps: `bmp?.close()`
- Revoke object URLs: `URL.revokeObjectURL(url)`
- Clear caches: `thumbDecoding`, `thumbnailLRU`

### Navigation

- **Folder breadcrumbs**: Click path segments → `navigateToFolder(path)`
- **Datetime breadcrumbs**: Click datetime parts → `filterByDateTime(...)`
- **Back/Forward**: Handled via `popstate` event listener
Expand All @@ -51,28 +70,33 @@
## Key Functions

### File Processing

- `collectImages(dirHandle, sampleSize, basePath)` - Walk directory tree, apply reservoir sampling if sampleSize > 0
- `processFiles(files)` - Main entry point, handles both AI and viewer modes
- `lazyDecodeThumbnail(idx)` - Create object URL and decode thumbnail lazily

### Navigation

- `run(dirHandle, basePath)` - Load and process files from directory
- `navigateToFolder(targetPath)` - Navigate to subfolder (reuses currentDirHandle)
- `filterByDateTime(granularity, year, month, day, hour, minute)` - Filter and rescan full folder

### Modal

- `openFileModal(index)` - Show media with metadata in footer
- `closeModal()` - Hide modal, pause video
- `navigateModal(direction)` - Arrow buttons for grid navigation

## Deployment Details

- **Platform**: Cloudflare Pages
- **Project Name**: `media-clusterer`
- **Output Dir**: `dist/`
- **Production Branch**: `main`
- **Preview Branches**: PR branches get `https://<branch>.media-clusterer.pages.dev`

## GitHub Actions

- Runs on push to `main` and PRs to `main`
- Type-check, tests, build, deploy to Cloudflare Pages
- Comments on PR with preview URL and commit info
4 changes: 3 additions & 1 deletion IMPROVEMENT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ Secondary main-thread costs found alongside:
batchSize=1 that is one IndexedDB transaction **per image**. `cacheStats` cursor-scans
the whole store on every refresh (`src/db.ts:144`).
- Chrome-AI captions write `localStorage` synchronously per image
(`src/app.ts:1390,1424`).
(`src/app.ts:1390,1424`). **Addressed by `VIDEO_LM_PLAN.md` milestone M5**, which moves
captions into an IndexedDB `captions` store; multi-frame video captions are longer and
must not land on this path.
- Render-loop churn: a fresh `Int32Array(pts.length)` and a `Set` are allocated **every
frame** (`src/app.ts:677,690`); `new Image()` is constructed inside the draw loop on
cache miss (`src/app.ts:758`); the O(n) nearest-point-to-center scan is duplicated three
Expand Down
Loading
Loading