From b923af2c0907679ba6b7acb16a0e0858cd3ab4be Mon Sep 17 00:00:00 2001 From: Steve Loeppky Date: Wed, 27 May 2026 11:51:20 -0700 Subject: [PATCH 1/5] docs(spec): add specification and implementation plan for OR filter on project boards Adds feature 007-project-board-or-filter targeting GitHub issue #8. Includes spec, research (DOM/API analysis of GitHub memex internals), implementation plan, data model, contracts, quickstart, and task breakdown. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../checklists/requirements.md | 36 ++++ .../contracts/memex-api.ts | 105 ++++++++++ .../contracts/or-query-parser.ts | 47 +++++ .../contracts/result-merger.ts | 24 +++ .../007-project-board-or-filter/data-model.md | 140 +++++++++++++ specs/007-project-board-or-filter/plan.md | 189 ++++++++++++++++++ .../007-project-board-or-filter/quickstart.md | 95 +++++++++ specs/007-project-board-or-filter/research.md | 126 ++++++++++++ specs/007-project-board-or-filter/spec.md | 109 ++++++++++ specs/007-project-board-or-filter/tasks.md | 165 +++++++++++++++ 10 files changed, 1036 insertions(+) create mode 100644 specs/007-project-board-or-filter/checklists/requirements.md create mode 100644 specs/007-project-board-or-filter/contracts/memex-api.ts create mode 100644 specs/007-project-board-or-filter/contracts/or-query-parser.ts create mode 100644 specs/007-project-board-or-filter/contracts/result-merger.ts create mode 100644 specs/007-project-board-or-filter/data-model.md create mode 100644 specs/007-project-board-or-filter/plan.md create mode 100644 specs/007-project-board-or-filter/quickstart.md create mode 100644 specs/007-project-board-or-filter/research.md create mode 100644 specs/007-project-board-or-filter/spec.md create mode 100644 specs/007-project-board-or-filter/tasks.md diff --git a/specs/007-project-board-or-filter/checklists/requirements.md b/specs/007-project-board-or-filter/checklists/requirements.md new file mode 100644 index 0000000..a38a509 --- /dev/null +++ b/specs/007-project-board-or-filter/checklists/requirements.md @@ -0,0 +1,36 @@ +# Specification Quality Checklist: Enable Project Board Filtering with OR Conditions + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-05-27 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- All items pass validation. Spec is ready for `/speckit.clarify` or `/speckit.plan`. +- The spec references "GitHub GraphQL API" and "GitHub CSS custom properties" in EXT-* requirements β€” these are platform constraints necessary for trust/data documentation, not implementation prescriptions. +- cSpell warnings for "biglep" (a username) and "standups" are expected domain terms, not errors. diff --git a/specs/007-project-board-or-filter/contracts/memex-api.ts b/specs/007-project-board-or-filter/contracts/memex-api.ts new file mode 100644 index 0000000..437ef6c --- /dev/null +++ b/specs/007-project-board-or-filter/contracts/memex-api.ts @@ -0,0 +1,105 @@ +/** + * Contract: Memex Paginated Items API Client + * + * Fetches project board items from GitHub's internal paginated items API. + * Handles cursor-based pagination to retrieve complete result sets. + */ + +/** Parameters for a paginated items API call */ +export interface PaginatedItemsParams { + /** The memex project ID (extracted from page) */ + memexId: string + /** Filter query string */ + query: string + /** Sort direction */ + sortDirection: 'asc' | 'desc' + /** Column ID to sort by */ + sortColumnId: string + /** Column ID to group by (optional) */ + groupColumnId?: string + /** Column ID to slice by (optional) */ + sliceColumnId?: string + /** Field IDs to include in response */ + fieldIds: string[] +} + +/** A single page response from the paginated items API (grouped) */ +export interface GroupedItemsResponse { + groups: { nodes: GroupNode[] } + groupedItems: Record + slices: Record + totalCount: { value: number; isApproximate: boolean } +} + +/** A single page response from the paginated items API (flat) */ +export interface FlatItemsResponse { + nodes: MemexItem[] + pageInfo: { hasNextPage: boolean; endCursor: string } + slices: Record + totalCount: { value: number; isApproximate: boolean } +} + +export interface GroupNode { + groupValue: string + groupId: string + groupMetadata: { + id: string + name: string + nameHtml: string + color: string + description: string + descriptionHtml: string + } + totalCount: { value: number; isApproximate: boolean } + fieldMetrics: unknown[] +} + +export interface GroupItems { + groupId: string + nodes: MemexItem[] + pageInfo: { hasNextPage: boolean; endCursor: string } +} + +export interface SliceInfo { + sliceId: string + sliceValue: string + totalCount: { value: number; isApproximate: boolean } +} + +export interface MemexItem { + id: number + contentId: number + contentType: 'Issue' | 'PullRequest' + contentRepositoryId: number + updatedAt: string + createdAt: string + state: string + memexProjectColumnValues: ColumnValue[] + content: unknown + [key: string]: unknown +} + +export interface ColumnValue { + memexProjectColumnId: string | number + value: unknown +} + +/** + * Fetch all items for a single filter query, following pagination cursors. + * + * @param params - API call parameters + * @returns Complete response with all items (pagination resolved) + */ +export declare function fetchAllItems( + params: PaginatedItemsParams +): Promise + +/** + * Extract memexId and view configuration from the current page's embedded data. + * + * @param viewNumber - The current view number (from URL) + * @returns Parameters needed for API calls + */ +export declare function extractViewParams( + viewNumber: number +): PaginatedItemsParams | null diff --git a/specs/007-project-board-or-filter/contracts/or-query-parser.ts b/specs/007-project-board-or-filter/contracts/or-query-parser.ts new file mode 100644 index 0000000..40317b0 --- /dev/null +++ b/specs/007-project-board-or-filter/contracts/or-query-parser.ts @@ -0,0 +1,47 @@ +/** + * Contract: OR Query Parser + * + * Parses a project board filter string for OR syntax and returns + * a structured representation of the query branches. + */ + +/** Result of parsing a filter string for OR syntax */ +export interface ORQuery { + /** Shared filter terms before the first parenthesized group */ + prefix: string + /** Filter conditions for each OR branch (contents of parenthesized groups) */ + branches: string[] + /** Original unparsed query string */ + raw: string +} + +/** Parse error when OR syntax is detected but malformed */ +export interface ORParseError { + type: 'nested_parens' | 'trailing_terms' | 'or_inside_parens' | 'single_branch' + message: string + raw: string +} + +/** Result of attempting to parse OR syntax */ +export type ORParseResult = + | { kind: 'or_query'; query: ORQuery } + | { kind: 'plain_query'; raw: string } // No OR syntax β€” pass through + | { kind: 'invalid_or'; error: ORParseError } // Malformed OR syntax + +/** + * Parse a filter string for OR syntax. + * + * Valid OR syntax: "shared-prefix (branch-1) OR (branch-2) [OR (branch-N)]" + * + * Rules: + * - At least 2 parenthesized groups separated by uppercase "OR" + * - Shared prefix (before first group) is applied to all branches + * - No nested parentheses + * - No filter terms after the final closing parenthesis + * - No "OR" keyword inside parentheses + * - Maximum 5 branches + * + * @param filterText - The raw filter string from the filter bar + * @returns Parsed result: OR query, plain query, or invalid OR error + */ +export declare function parseORQuery(filterText: string): ORParseResult diff --git a/specs/007-project-board-or-filter/contracts/result-merger.ts b/specs/007-project-board-or-filter/contracts/result-merger.ts new file mode 100644 index 0000000..cb236b8 --- /dev/null +++ b/specs/007-project-board-or-filter/contracts/result-merger.ts @@ -0,0 +1,24 @@ +/** + * Contract: Result Merger + * + * Merges multiple paginated items API responses into a single + * deduplicated response that the React app can render natively. + */ + +import type { GroupedItemsResponse } from './memex-api' + +/** + * Merge multiple API responses (one per OR branch) into a single response. + * + * - Deduplicates items by `item.id` (first occurrence wins) + * - Unions group metadata from all branches + * - Assigns items to correct groups based on their field values + * - Recalculates totalCount as the count of unique items + * - Merges slice data from all branches + * + * @param responses - Array of responses, one per OR branch + * @returns Single merged response compatible with the React app's expected format + */ +export declare function mergeResponses( + responses: GroupedItemsResponse[] +): GroupedItemsResponse diff --git a/specs/007-project-board-or-filter/data-model.md b/specs/007-project-board-or-filter/data-model.md new file mode 100644 index 0000000..2035afd --- /dev/null +++ b/specs/007-project-board-or-filter/data-model.md @@ -0,0 +1,140 @@ +# Data Model: Project Board OR Filter + +**Branch**: `007-project-board-or-filter` | **Date**: 2026-05-27 + +## Entities + +### ORQuery + +Parsed representation of a filter string containing OR syntax. + +| Field | Type | Description | +|-------|------|-------------| +| `prefix` | `string` | Shared filter terms before the first parenthesized group (may be empty) | +| `branches` | `string[]` | Array of filter conditions, one per OR branch (contents of each parenthesized group) | +| `raw` | `string` | Original unparsed query string | + +**Validation rules**: +- At least 2 branches required +- No nested parentheses within branches +- No filter terms after the final closing parenthesis +- No `OR` keyword inside parentheses +- Maximum 5 branches (practical limit from tpm-utils spec) + +**Examples**: +- `cycle:202605-2 biglep (-status:"πŸŽ‰ Done") OR (-last-updated:1days)` + β†’ `{ prefix: "cycle:202605-2 biglep", branches: ['-status:"πŸŽ‰ Done"', "-last-updated:1days"] }` +- `(-status:"πŸŽ‰ Done") OR (-last-updated:1days)` + β†’ `{ prefix: "", branches: ['-status:"πŸŽ‰ Done"', "-last-updated:1days"] }` + +### MemexPaginatedResponse (Grouped) + +API response from `/memexes/{id}/paginated_items` when `groupedBy` is specified. + +| Field | Type | Description | +|-------|------|-------------| +| `groups` | `{ nodes: GroupNode[] }` | Group definitions with metadata | +| `groupedItems` | `Record` | Items organized by group index | +| `slices` | `Record` | Slice/category breakdowns | +| `totalCount` | `{ value: number, isApproximate: boolean }` | Total items matching the filter | + +### MemexPaginatedResponse (Flat) + +API response for pagination continuation (no grouping). + +| Field | Type | Description | +|-------|------|-------------| +| `nodes` | `MemexItem[]` | Array of project items | +| `pageInfo` | `{ hasNextPage: boolean, endCursor: string }` | Pagination cursor | +| `slices` | `Record` | Slice/category breakdowns | +| `totalCount` | `{ value: number, isApproximate: boolean }` | Total items matching the filter | + +### GroupNode + +| Field | Type | Description | +|-------|------|-------------| +| `groupValue` | `string` | Display name (e.g., "🐱 Todo") | +| `groupId` | `string` | Base64-encoded unique identifier | +| `groupMetadata` | `{ id, name, nameHtml, color, description, descriptionHtml }` | Display metadata | +| `totalCount` | `{ value: number, isApproximate: boolean }` | Items in this group | +| `fieldMetrics` | `unknown[]` | Field-level aggregation metrics | + +### GroupItems + +| Field | Type | Description | +|-------|------|-------------| +| `groupId` | `string` | Matches `GroupNode.groupId` | +| `nodes` | `MemexItem[]` | Items in this group | +| `pageInfo` | `{ hasNextPage: boolean, endCursor: string }` | Pagination within group | + +### MemexItem + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `number` | **Deduplication key** β€” unique project item ID | +| `contentId` | `number` | ID of the underlying Issue or PR | +| `contentType` | `"Issue" \| "PullRequest"` | Type discriminator | +| `contentRepositoryId` | `number` | Repository ID | +| `priority` | `number \| null` | Item priority | +| `virtualPriority` | `string` | Sort key for priority ordering | +| `updatedAt` | `string` (ISO 8601) | Last update timestamp | +| `createdAt` | `string` (ISO 8601) | Creation timestamp | +| `state` | `string` | Issue/PR state (open, closed, merged) | +| `memexProjectColumnValues` | `ColumnValue[]` | Field values for this item | +| `content` | `object` | Nested issue/PR metadata (title, labels, assignees, etc.) | + +### ColumnValue + +| Field | Type | Description | +|-------|------|-------------| +| `memexProjectColumnId` | `string \| number` | Column identifier (e.g., "Title", "Status", or numeric ID) | +| `value` | `object` | Column-type-specific value object | + +### ViewConfig + +Extracted from `#memex-views` script tag. Defines the current view's parameters needed for API calls. + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `number` | View ID | +| `number` | `number` | View number (in URL) | +| `name` | `string` | Display name | +| `filter` | `string` | Default filter for this view | +| `layout` | `string` | Layout type (e.g., "table_layout") | +| `groupBy` | `number[]` | Column IDs to group by | +| `sortBy` | `[number, string][]` | Column ID + direction pairs | +| `visibleFields` | `number[]` | Column IDs to include in response | + +## State Transitions + +``` +IDLE β†’ OR_DETECTED β†’ FETCHING β†’ MERGING β†’ RENDERED + β”‚ β”‚ + β”‚ ← (user changes filter) β†β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + └── (no OR syntax) β†’ PASS_THROUGH (native filtering) +``` + +| State | Description | +|-------|-------------| +| `IDLE` | No active OR query; normal board behavior | +| `OR_DETECTED` | OR syntax parsed from filter input; preparing to intercept | +| `FETCHING` | Making paginated_items API calls for each branch | +| `MERGING` | All branches fetched; merging and deduplicating | +| `RENDERED` | Merged data returned to React app; board displays combined results | +| `PASS_THROUGH` | No OR syntax; fetch interception disabled, native behavior | + +## Relationships + +``` +ORQuery 1──* QueryBranch (prefix + branch = complete filter) + β”‚ + β–Ό (one API call per branch) +MemexPaginatedResponse *──* MemexItem (items from all branches) + β”‚ + β–Ό (merge + deduplicate by item.id) +MergedResponse 1──* MemexItem (union, no duplicates) + β”‚ + β–Ό (returned to React app) +Board UI renders naturally +``` diff --git a/specs/007-project-board-or-filter/plan.md b/specs/007-project-board-or-filter/plan.md new file mode 100644 index 0000000..03898d9 --- /dev/null +++ b/specs/007-project-board-or-filter/plan.md @@ -0,0 +1,189 @@ +# Implementation Plan: Enable Project Board Filtering with OR Conditions + +**Branch**: `007-project-board-or-filter` | **Date**: 2026-05-27 | **Spec**: [spec.md](spec.md) +**Input**: Feature specification from `/specs/007-project-board-or-filter/spec.md` + +## Summary + +Enable OR-condition filtering on GitHub project board views by intercepting filter submissions, parsing OR syntax, executing multiple calls to GitHub's internal paginated items API (one per branch), merging and deduplicating results, and rendering the combined data in the board's existing UI. The extension currently only targets issue/PR pages; this feature adds content scripts for project board pages. + +## Technical Context + +**Language/Version**: TypeScript (esbuild-compiled, Manifest V3 Chrome extension) +**Primary Dependencies**: Chrome Extensions API, GitHub internal memex API (`/memexes/{id}/paginated_items`) +**Storage**: `chrome.storage.local` (existing config β€” no new storage for this feature) +**Testing**: Manual verification (per constitution Principle IV); automated tests optional +**Target Platform**: Chromium browsers (Manifest V3) +**Project Type**: Browser extension (content scripts + service worker) +**Performance Goals**: OR query results within a few seconds for typical 2-3 branch queries +**Constraints**: Must not break existing extension features; must handle GitHub's internal API changes gracefully +**Scale/Scope**: Internal FilOzone team usage; 2-5 OR branches per query typical + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +Gates derived from `.specify/memory/constitution.md` (TPM Utils GitHub Extension): + +- **Least privilege**: New content script match `https://github.com/orgs/*/projects/*/views/*` is within existing `host_permissions` (`https://github.com/*`). No new permissions, OAuth scopes, or host permissions required. API calls use session cookies (same-origin) β€” no token access needed beyond existing auth. **PASS** +- **User credentials**: No new credential handling. Same-origin fetch to GitHub's internal API uses the user's existing session. No tokens stored or transmitted. **PASS** +- **API discipline**: Uses GitHub's internal `/memexes/{id}/paginated_items` API (same-origin, session-authenticated). Rate limiting handled by concurrent request throttling (max 5 branches). Partial failure shows successful branches + error notification. **PASS** +- **Verification (internal velocity)**: Manual verification steps documented below. No manifest/auth/host-permission changes beyond content_scripts match addition. Smoke checklist included for the new content script match. **PASS** +- **Incremental scope**: MVP is a single feature slice: OR query parsing + multi-fetch + merge + render. No speculative features. **PASS** +- **UI fidelity**: No custom UI injected β€” merged data is rendered by GitHub's own React app. No light/dark mode concerns. **PASS** + +## Project Structure + +### Documentation (this feature) + +```text +specs/007-project-board-or-filter/ +β”œβ”€β”€ plan.md # This file +β”œβ”€β”€ research.md # Phase 0 output β€” API and DOM research +β”œβ”€β”€ data-model.md # Phase 1 output β€” data structures +β”œβ”€β”€ quickstart.md # Phase 1 output β€” dev setup +β”œβ”€β”€ contracts/ # Phase 1 output β€” internal interfaces +└── tasks.md # Phase 2 output (/speckit.tasks) +``` + +### Source Code (repository root) + +```text +extension/ +β”œβ”€β”€ manifest.json # Add content_scripts match for project board pages +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ content/ +β”‚ β”‚ β”œβ”€β”€ board-filter/ # NEW β€” project board OR filter feature +β”‚ β”‚ β”‚ β”œβ”€β”€ board-filter-main.ts # Entry point: DOM observation, filter interception +β”‚ β”‚ β”‚ β”œβ”€β”€ or-query-parser.ts # Parse OR syntax into prefix + branches +β”‚ β”‚ β”‚ β”œβ”€β”€ memex-api.ts # Fetch paginated_items API, handle pagination +β”‚ β”‚ β”‚ β”œβ”€β”€ result-merger.ts # Merge + deduplicate items from multiple branches +β”‚ β”‚ β”‚ β”œβ”€β”€ board-data-injector.ts # Inject merged data into React app +β”‚ β”‚ └── ... (existing content scripts) +β”‚ β”œβ”€β”€ lib/ +β”‚ β”‚ └── messages.ts # Add message types if service worker relay needed +β”‚ └── background/ +β”‚ └── service-worker.ts # May need relay for API calls (if CORS issues) +``` + +**Structure Decision**: New code lives in `extension/src/content/board-filter/` as a self-contained module. The OR query parser is isolated for testability. The feature shares the existing service worker and message infrastructure but operates independently from the issue/PR sidebar features. + +## Complexity Tracking + +| Violation | Why Needed | Simpler Alternative Rejected Because | +|-----------|------------|-------------------------------------| +| Internal API dependency (`/memexes/{id}/paginated_items`) | GitHub's public API doesn't support board filter syntax (cycle, last-updated, keyword) | Public GraphQL API lacks filter support; HTML parsing is more fragile than JSON API | +| Main-world script injection (for fetch interception) | Need to intercept/modify the React app's API calls transparently | Content-script-only approach can't interact with page's JS context for seamless rendering | + +## Phase 0: Research (Complete) + +See [research.md](research.md) for full findings. Key decisions: + +1. **Data source**: GitHub's internal `GET /memexes/{memexId}/paginated_items?q={filter}` API +2. **Interception point**: Filter bar input (`#filter-bar-component-input`) Enter key event +3. **Rendering strategy**: Intercept the React app's fetch calls via main-world script to return merged data transparently (Option B from research) +4. **Pagination**: Cursor-based (`after` param); must follow all pages per branch before merging + +## Phase 1: Design + +### Architecture + +``` +User types OR query β†’ Enter key + β”‚ + β–Ό +[Content Script: board-filter-main.ts] + Observes filter bar, detects OR syntax + β”‚ + β–Ό +[or-query-parser.ts] + Parses: "prefix (branch1) OR (branch2)" β†’ { prefix, branches: ["branch1", "branch2"] } + β”‚ + β–Ό +[Main-world script: board-data-injector.ts] + Intercepts fetch("/memexes/{id}/paginated_items") + For each branch: + └── [memex-api.ts] fetches paginated_items?q=prefix+branch + └── Follows pagination cursors + β”‚ + β–Ό +[result-merger.ts] + Merges items from all branches + Deduplicates by item.id + Reconstructs groups, slices, totalCount + β”‚ + β–Ό + Returns merged response to React app + (React renders naturally β€” no DOM manipulation needed) +``` + +### Implementation Phases + +**Phase 1a β€” OR Query Parser (P1, standalone)** +- Parse the filter string for OR syntax +- Validate syntax rules (no nesting, no trailing terms, etc.) +- Return structured result: `{ prefix: string, branches: string[] }` or `null` (no OR detected) +- Pure function, easily testable + +**Phase 1b β€” Memex API Client (P1, standalone)** +- Fetch `GET /memexes/{id}/paginated_items` with given query params +- Handle cursor-based pagination (follow `after` until no more pages) +- Extract memexId from `#memex-item-get-api-data` script tag +- Extract view params (sort, group, slice, fields) from `#memex-views` script tag +- Return complete item set for a single filter query + +**Phase 1c β€” Result Merger (P1, standalone)** +- Take multiple API responses (one per branch) +- Deduplicate items by `id` +- Merge group metadata (union of groups from all branches) +- Reconstruct `groupedItems` with items assigned to correct groups +- Recalculate `totalCount` and `slices` + +**Phase 1d β€” Board Data Injector / Fetch Interceptor (P1, integration)** +- Main-world script that patches `window.fetch` +- Detects calls to `/memexes/{id}/paginated_items` +- When an OR query is active: intercepts the call, runs multi-branch fetch + merge, returns merged response +- When no OR query: passes through to original fetch unchanged +- Communication with content script via `CustomEvent` or `window.postMessage` + +**Phase 1e β€” Content Script Entry Point (P1, integration)** +- New content script registered in manifest for `https://github.com/orgs/*/projects/*/views/*` +- Observes `#filter-bar-component-input` for Enter key submission +- Parses filter text for OR syntax +- If OR detected: signals main-world script to activate fetch interception for the upcoming API call +- If no OR: does nothing (full pass-through) + +### Key Design Decisions + +1. **Fetch interception over DOM manipulation**: By intercepting the React app's API calls and returning merged data, we let GitHub's own rendering code handle display. This is more robust than trying to manipulate the DOM or React state directly. + +2. **Content script + main-world script**: The content script handles DOM observation and OR detection. A main-world script (injected like the existing `pr-expand-main-world.ts`) handles fetch interception, since content scripts can't access the page's JS context. + +3. **Same-origin fetch**: API calls to `/memexes/{id}/paginated_items` are same-origin (github.com β†’ github.com), so session cookies are sent automatically. No need to relay through the service worker. + +4. **Graceful degradation**: If the internal API changes format or the fetch interception fails, the extension falls back to native filtering (the unmodified API call goes through). + +### Manual Verification Plan + +**Smoke checklist** (required for new content script match): +1. Load extension, navigate to `https://github.com/orgs/FilOzone/projects/14/views/20` +2. Verify content script loads (check console for log marker) +3. Verify existing issue/PR sidebar features still work on issue pages +4. Verify no errors on project board pages without OR queries + +**Feature verification**: +1. Enter `cycle:202605-2 biglep (-status:"πŸŽ‰ Done") OR (-last-updated:1days)` in filter bar +2. Verify merged results appear (items from both branches) +3. Verify no duplicates in results +4. Test with invalid OR syntax: `((nested))`, `(a) OR (b) trailing` β€” verify native filtering still works +5. Test without OR syntax: verify normal filtering unchanged +6. Test on views with different layouts (table, board) if applicable + +**Test URLs** (for `docs/canonical-test-urls.md`): +- `https://github.com/orgs/FilOzone/projects/14/views/20` β€” "Current by Status" view (filtered, grouped) +- `https://github.com/orgs/FilOzone/projects/14/views/2` β€” "All" view (large item set, pagination) +- `https://github.com/orgs/FilOzone/projects/14/views/33` β€” "Recently Updated" view + +## Phase 2: Task Breakdown + +*To be generated by `/speckit.tasks`* diff --git a/specs/007-project-board-or-filter/quickstart.md b/specs/007-project-board-or-filter/quickstart.md new file mode 100644 index 0000000..9d8e6ea --- /dev/null +++ b/specs/007-project-board-or-filter/quickstart.md @@ -0,0 +1,95 @@ +# Quickstart: Project Board OR Filter Development + +**Branch**: `007-project-board-or-filter` + +## Prerequisites + +- Node.js installed +- Chrome browser +- GitHub account with access to FilOzone org projects + +## Setup + +```bash +# 1. Clone and switch to feature branch +git checkout 007-project-board-or-filter +npm install + +# 2. Build the extension +npm run build + +# 3. Launch Chrome with remote debugging (required for reload scripts) +/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \ + --remote-debugging-port=9222 \ + --remote-allow-origins='*' \ + --user-data-dir="$HOME/.chrome-dev-profile" + +# 4. Load the extension in Chrome +# - Navigate to chrome://extensions +# - Enable "Developer mode" +# - Click "Load unpacked" β†’ select the extension/dist/ directory + +# 5. Navigate to a project board to test +# https://github.com/orgs/FilOzone/projects/14/views/20 +``` + +## Development Loop + +```bash +# Make code changes, then: +npm run build && node scripts/cdp-reload.mjs + +# To stream service worker logs: +node scripts/sw-logs.mjs +``` + +## Key Files to Edit + +| File | Purpose | +|------|---------| +| `extension/manifest.json` | Add content_scripts match for project board pages | +| `extension/src/content/board-filter/board-filter-main.ts` | Entry point: observe filter bar, detect OR syntax | +| `extension/src/content/board-filter/or-query-parser.ts` | Parse OR syntax into structured query | +| `extension/src/content/board-filter/memex-api.ts` | Call paginated_items API with pagination handling | +| `extension/src/content/board-filter/result-merger.ts` | Merge + deduplicate items from multiple branches | +| `extension/src/content/board-filter/board-data-injector.ts` | Main-world fetch interception | + +## Test Queries + +**Valid OR queries**: +- `cycle:202605-2 biglep (-status:"πŸŽ‰ Done") OR (-last-updated:1days)` +- `(-status:"πŸŽ‰ Done") OR (-last-updated:1days)` + +**Invalid OR queries** (should fall back to native filter): +- `((nested))` β€” nested parentheses +- `(a) OR (b) trailing` β€” terms after final group +- `(a OR b)` β€” OR inside parentheses + +**Non-OR queries** (should pass through unchanged): +- `cycle:202605-2 -status:"πŸŽ‰ Done"` +- `biglep` + +## Key API Endpoint + +``` +GET /memexes/{memexId}/paginated_items + ?q={filter-string} + &sortedBy[direction]={asc|desc} + &sortedBy[columnId]={columnId} + &groupedBy[columnId]={columnId} + &sliceBy[columnId]={columnId} + &fieldIds=[...] + &after={cursor} # for pagination +``` + +- `memexId`: Extract from `#memex-item-get-api-data` script tag on the page +- View params: Extract from `#memex-views` script tag +- Auth: Session cookies (same-origin, automatic) + +## Browser Debugging Tips + +- Open DevTools on the project board page +- Network tab: filter by `paginated_items` to see API calls +- Console: look for `[FilOz]` prefix from extension logs +- Elements: search for `#memex-paginated-items-data` to see embedded data +- Sources: check content script is loaded under `chrome-extension://` sources diff --git a/specs/007-project-board-or-filter/research.md b/specs/007-project-board-or-filter/research.md new file mode 100644 index 0000000..9732b6a --- /dev/null +++ b/specs/007-project-board-or-filter/research.md @@ -0,0 +1,126 @@ +# Research: Enable Project Board Filtering with OR Conditions + +**Date**: 2026-05-27 +**Branch**: `007-project-board-or-filter` + +## Key Findings + +### 1. GitHub Project Board Data Architecture + +**Decision**: The extension must intercept filter submission and use GitHub's internal memex API to fetch multiple filtered item sets. + +**Rationale**: GitHub project boards (internally called "memex") use a two-phase data loading model: + +- **Initial page load**: Data is server-rendered into a `