diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..67a9954 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,71 @@ +name: "šŸ› Bug Report" +description: Report a bug in CORTEX +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for reporting a bug. Please fill in the details below. + + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of the bug. + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Steps to Reproduce + description: Minimal steps to reproduce the behavior. + placeholder: | + 1. Call `ingestText(...)` with ... + 2. Then call `query(...)` with ... + 3. Observe ... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What you expected to happen. + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual Behavior + description: What actually happened. Include error messages or screenshots if applicable. + validations: + required: true + + - type: dropdown + id: layer + attributes: + label: Affected Layer + description: Which CORTEX layer does this bug affect? + multiple: true + options: + - Foundation (core/) + - Storage (storage/) + - Vector Compute (root backends) + - Embeddings (embeddings/) + - Hippocampus (hippocampus/) + - Cortex (cortex/) + - Daydreamer + - Runtime Harness (runtime/) + - CI / Build + - Documentation + validations: + required: true + + - type: input + id: browser + attributes: + label: Browser / Runtime + description: Browser version, Electron version, or Node/Bun version. + placeholder: "Chrome 120, Electron 28, Bun 1.3.10" diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ab8c2fc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: "šŸ“– Architecture Reference" + url: https://github.com/devlux76/cortex/blob/main/DESIGN.md + about: Review DESIGN.md before proposing architectural changes. diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..137c437 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,60 @@ +name: "✨ Feature Request" +description: Propose a new feature or enhancement for CORTEX +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Describe the feature you'd like to see in CORTEX. + + - type: textarea + id: summary + attributes: + label: Summary + description: A concise description of the proposed feature. + validations: + required: true + + - type: textarea + id: motivation + attributes: + label: Motivation + description: Why is this feature needed? What problem does it solve? + validations: + required: true + + - type: textarea + id: design + attributes: + label: Proposed Design + description: How should this feature work? Reference DESIGN.md sections if applicable. + + - type: dropdown + id: layer + attributes: + label: Target Layer + description: Which CORTEX layer would this feature primarily affect? + multiple: true + options: + - Foundation (core/) + - Storage (storage/) + - Vector Compute (root backends) + - Embeddings (embeddings/) + - Hippocampus (hippocampus/) + - Cortex (cortex/) + - Daydreamer + - Runtime Harness (runtime/) + - CI / Build + - Documentation + validations: + required: true + + - type: textarea + id: exit-criteria + attributes: + label: Exit Criteria + description: What must be true for this feature to be considered complete? + placeholder: | + - [ ] Unit tests pass + - [ ] Integration test covers end-to-end flow + - [ ] DESIGN.md updated if architecture changed diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml new file mode 100644 index 0000000..788955f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -0,0 +1,77 @@ +name: "šŸ”§ Implementation Task" +description: Define a specific implementation task +labels: ["task"] +body: + - type: markdown + attributes: + value: | + Define a concrete implementation task. Reference DESIGN.md for architectural context. + + - type: textarea + id: objective + attributes: + label: Objective + description: What needs to be built or changed? + validations: + required: true + + - type: dropdown + id: priority + attributes: + label: Priority + options: + - "P0: critical — blocks dependent work" + - "P1: high — targets next milestone" + - "P2: medium — important but not blocking" + - "P3: low — polish or nice-to-have" + validations: + required: true + + - type: dropdown + id: layer + attributes: + label: Target Layer + description: Which CORTEX layer does this task target? + multiple: true + options: + - Foundation (core/) + - Storage (storage/) + - Vector Compute (root backends) + - Embeddings (embeddings/) + - Hippocampus (hippocampus/) + - Cortex (cortex/) + - Daydreamer + - Runtime Harness (runtime/) + - CI / Build + - Documentation + validations: + required: true + + - type: textarea + id: files + attributes: + label: Files to Create or Modify + description: List the files this task will touch. + placeholder: | + - `cortex/MetroidBuilder.ts` (create) + - `tests/cortex/MetroidBuilder.test.ts` (create) + - `DESIGN.md` (update if architecture changes) + + - type: textarea + id: exit-criteria + attributes: + label: Exit Criteria + description: Checklist of conditions that must be met to close this task. + placeholder: | + - [ ] Implementation complete + - [ ] Unit tests passing + - [ ] Lint and typecheck clean + - [ ] guard:model-derived passes (if numerics changed) + validations: + required: true + + - type: textarea + id: dependencies + attributes: + label: Dependencies + description: List any issues or tasks that must be completed first. Use `#issue_number` references. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..acb20c5 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ +## Summary + + + +## Changes + + + +- + +## Checklist + +- [ ] Lint passes (`npm run lint`) +- [ ] Typecheck passes (`npm run build`) +- [ ] Unit tests pass (`npm run test:unit`) +- [ ] `guard:model-derived` passes (if numeric constants changed) +- [ ] DESIGN.md updated (if architecture changed) +- [ ] No hardcoded model-derived numbers — all sourced from `core/` +- [ ] No server-side dependencies, cloud calls, or telemetry added diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 094079e..9f08291 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -18,7 +18,19 @@ The engine models three biological brain regions: | `PLAN.md` | Module-by-module implementation status and development phases | | `TODO.md` | Prioritized actionable tasks to ship v1.0 | -Keep all documents synchronized with the real code state after every implementation pass. +Keep `DESIGN.md` synchronized with the real code state after every implementation pass. + +## Project Management + +All task tracking, prioritization, and status is managed through **GitHub-native features** — not markdown files: + +- **GitHub Issues** — Every task, bug, and feature request is a GitHub Issue (use the issue templates in `.github/ISSUE_TEMPLATE/`). +- **GitHub Projects** — Use project boards for Kanban-style lifecycle tracking (To Do → In Progress → Done). +- **Milestones** — Group issues by release phase (`v0.1`, `v0.5`, `v1.0`). +- **Labels** — Auto-applied on PRs by `.github/workflows/auto-label.yml` using path-based rules in `.github/labeler.yml`. Priority labels (`P0`–`P3`) and layer labels (`layer: foundation`, `layer: cortex`, etc.) are used for classification. +- **Issue linking** — Reference dependencies with `#issue_number`. Use `Closes #N` in PR descriptions to auto-close issues on merge. + +Agents with `gh` CLI access can create, update, and close issues directly. Do not create markdown files for task tracking. ## Directory Structure @@ -119,8 +131,10 @@ Run `npm run guard:model-derived` after any numeric change to verify compliance. - All CI checks must pass: `lint`, `build` (typecheck), `test:unit`. - `npm run guard:model-derived` must pass for any change that touches numeric constants. -- Keep `README.md`, `CORTEX-DESIGN-PLAN-TODO.md`, and `PROJECT-EXECUTION-PLAN.md` synchronized with any implementation state changes. -- Record blockers with file path, failure symptom, and next action. +- PRs are auto-labeled by layer based on changed files (`.github/labeler.yml`). +- Use the PR template (`.github/PULL_REQUEST_TEMPLATE.md`) — it is pre-populated on every PR. +- Reference the issue being addressed with `Closes #N` in the PR description. +- Keep `DESIGN.md` synchronized with any architectural changes. ## What NOT to Do diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..8bc2e27 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,60 @@ +# Maps file-path globs to labels applied automatically on PRs. +# Used by the auto-label.yml workflow (actions/labeler). + +"layer: foundation": + - changed-files: + - any-glob-to-any-file: "core/**" + +"layer: storage": + - changed-files: + - any-glob-to-any-file: "storage/**" + +"layer: compute": + - changed-files: + - any-glob-to-any-file: + - "VectorBackend.ts" + - "WebGPUVectorBackend.ts" + - "WebGLVectorBackend.ts" + - "WebNNVectorBackend.ts" + - "WasmVectorBackend.ts" + - "CreateVectorBackend.ts" + - "BackendKind.ts" + - "TopK.ts" + - "Vectors.glsl" + - "Vectors.wgsl" + - "Vectors.wat" + +"layer: embeddings": + - changed-files: + - any-glob-to-any-file: "embeddings/**" + +"layer: hippocampus": + - changed-files: + - any-glob-to-any-file: "hippocampus/**" + +"layer: cortex": + - changed-files: + - any-glob-to-any-file: "cortex/**" + +"layer: daydreamer": + - changed-files: + - any-glob-to-any-file: "daydreamer/**" + +"layer: testing": + - changed-files: + - any-glob-to-any-file: "tests/**" + +"layer: ci": + - changed-files: + - any-glob-to-any-file: + - ".github/workflows/**" + - ".github/labeler.yml" + +"layer: documentation": + - changed-files: + - any-glob-to-any-file: + - "*.md" + - "docs/**" + - ".github/copilot-instructions.md" + - ".github/ISSUE_TEMPLATE/**" + - ".github/PULL_REQUEST_TEMPLATE.md" diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml new file mode 100644 index 0000000..85f437d --- /dev/null +++ b/.github/workflows/auto-label.yml @@ -0,0 +1,19 @@ +name: Auto-Label PRs + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +jobs: + label: + runs-on: ubuntu-latest + steps: + - name: Label PR by changed files + uses: actions/labeler@v5 + with: + configuration-path: .github/labeler.yml + sync-labels: true diff --git a/.github/workflows/close-legacy-issues.yml b/.github/workflows/close-legacy-issues.yml new file mode 100644 index 0000000..86c922b --- /dev/null +++ b/.github/workflows/close-legacy-issues.yml @@ -0,0 +1,69 @@ +name: Close Legacy Issues + +# One-shot workflow: closes the 29 issues created by the removed +# sync-github-project.mjs script. Run manually once, then delete this file. + +on: + workflow_dispatch: + +permissions: + issues: write + +jobs: + close-legacy: + runs-on: ubuntu-latest + steps: + - name: Close legacy sync-generated issues as won't-fix + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + + // Exact issue numbers created by the old sync-github-project.mjs script. + const LEGACY_ISSUES = [ + 21, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, + 36, 37, 38, 39, 40, 41, 42, 56, 57, 58, 59, 60, 61, 62, 63 + ]; + + // Ensure the "wontfix" label exists + try { + await github.rest.issues.getLabel({ owner, repo, name: "wontfix" }); + } catch { + await github.rest.issues.createLabel({ + owner, repo, + name: "wontfix", + color: "ffffff", + description: "Closed during migration to GitHub-native project management" + }); + } + + let closed = 0; + for (const num of LEGACY_ISSUES) { + try { + const { data: issue } = await github.rest.issues.get({ + owner, repo, + issue_number: num, + }); + if (issue.state === "closed") { + console.log(`#${num} already closed — skipping`); + continue; + } + await github.rest.issues.addLabels({ + owner, repo, + issue_number: num, + labels: ["wontfix"], + }); + await github.rest.issues.update({ + owner, repo, + issue_number: num, + state: "closed", + state_reason: "not_planned", + }); + console.log(`Closed #${num}: ${issue.title}`); + closed++; + } catch (err) { + console.log(`#${num}: skipped (${err.message})`); + } + } + console.log(`\nDone — closed ${closed} legacy issues.`); diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..0d4760b --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,37 @@ +name: Stale Issue Management + +on: + schedule: + - cron: "30 2 * * 1" # Every Monday at 02:30 UTC + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - name: Mark and close stale issues and PRs + uses: actions/stale@v9 + with: + stale-issue-message: > + This issue has had no activity for 60 days and will be closed in 14 + days unless there is new activity. If this is still relevant, please + comment or remove the `stale` label. + close-issue-message: > + Closed due to inactivity. Reopen if this is still needed. + stale-pr-message: > + This PR has had no activity for 30 days and will be closed in 14 + days unless there is new activity. + close-pr-message: > + Closed due to inactivity. Reopen if this is still needed. + days-before-stale: 60 + days-before-close: 14 + days-before-pr-stale: 30 + days-before-pr-close: 14 + stale-issue-label: stale + stale-pr-label: stale + exempt-issue-labels: "P0: critical,pinned" + exempt-pr-labels: "pinned" diff --git a/.github/workflows/sync-github-project.yml b/.github/workflows/sync-github-project.yml deleted file mode 100644 index e09aa32..0000000 --- a/.github/workflows/sync-github-project.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Sync GitHub Project - -on: - push: - branches: - - main - paths: - - "TODO.md" - workflow_dispatch: - -concurrency: - group: sync-github-project - cancel-in-progress: false - -permissions: - issues: write - contents: read - -jobs: - sync: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: "latest" - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Sync GitHub Project (milestones, labels, issues) - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: bun run sync:github-project diff --git a/DESIGN.md b/DESIGN.md index 2f1dfe9..2501836 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -407,7 +407,7 @@ This keeps subgraph expansion cost sublinear in graph mass at scale while remain ### Policy Source of Truth -All hotpath constants — `c`, `α`, `β`, `γ`, `q_s`, `q_v`, `q_b`, `q_p` — live in `core/HotpathPolicy.ts` as a frozen default policy object. These are **policy-derived constants** (not model-derived) and are kept strictly separate from `core/ModelDefaults.ts`. A companion guard (or an extension to `guard:model-derived`) is planned (see TODO.md P3-E3) to prevent these constants from being hardcoded elsewhere; until that guard is in place, discipline is enforced by convention. +All hotpath constants — `c`, `α`, `β`, `γ`, `q_s`, `q_v`, `q_b`, `q_p` — live in `core/HotpathPolicy.ts` as a frozen default policy object. These are **policy-derived constants** (not model-derived) and are kept strictly separate from `core/ModelDefaults.ts`. A companion guard (or an extension to `guard:model-derived`) is planned to prevent these constants from being hardcoded elsewhere; until that guard is in place, discipline is enforced by convention. --- diff --git a/README.md b/README.md index 810472c..cf92341 100644 --- a/README.md +++ b/README.md @@ -119,3 +119,13 @@ bun run dev:harness # start the browser runtime harness at http://127.0.0.1:4173 | [`ARCHITECTURE-REVIEW.md`](ARCHITECTURE-REVIEW.md) | Repository-wide architectural drift report and correction tasks | | [`docs/api.md`](docs/api.md) | API reference for developers integrating with CORTEX | | [`docs/development.md`](docs/development.md) | Build, test, debug, and Docker workflow | + +### Project Management + +Task tracking, prioritization, and sprint planning use **GitHub-native features**: + +- **[Issues](../../issues)** — Every task, bug, and feature request. Use the structured templates. +- **[Projects](../../projects)** — Kanban boards for lifecycle tracking. +- **Milestones** — Group issues by release phase (`v0.1`, `v0.5`, `v1.0`). +- **Labels** — Auto-applied on PRs based on changed files. Priority (`P0`–`P3`) and layer labels for classification. +- **`gh` CLI** — Agents create, update, and close issues directly via `gh issue create`, `gh issue close`, etc. diff --git a/docs/development.md b/docs/development.md index c729d5f..e2a60e4 100644 --- a/docs/development.md +++ b/docs/development.md @@ -170,9 +170,9 @@ This command will fail the build if it detects numeric literals that are likely At the end of every implementation pass, update documents in this order: -1. **`PROJECT-EXECUTION-PLAN.md`** — append pass status delta and exact commands executed. -2. **`CORTEX-DESIGN-PLAN-TODO.md`** — update the design-to-code status matrix. -3. **`README.md`** — confirm the project description still reflects reality. -4. **`docs/api.md`** — update if new public APIs are added or existing ones change. +1. **`DESIGN.md`** — update if architecture changes. +2. **`README.md`** — confirm the project description still reflects reality. +3. **`docs/api.md`** — update if new public APIs are added or existing ones change. +4. **GitHub Issues** — close completed tasks, create new ones as needed via `gh` CLI or the web UI. > Numeric examples in design docs are illustrative unless explicitly sourced from model metadata. diff --git a/package.json b/package.json index d9855a1..81fc6bb 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,7 @@ "docker:electron:build": "docker compose -f docker-compose.electron-debug.yml build electron-debug", "docker:electron:up": "docker compose -f docker-compose.electron-debug.yml up --build --force-recreate electron-debug", "docker:electron:down": "docker compose -f docker-compose.electron-debug.yml down --remove-orphans", - "docker:electron:logs": "docker compose -f docker-compose.electron-debug.yml logs -f electron-debug", - "sync:github-project": "bun scripts/sync-github-project.mjs" + "docker:electron:logs": "docker compose -f docker-compose.electron-debug.yml logs -f electron-debug" }, "devDependencies": { "@eslint/js": "latest", diff --git a/scripts/sync-github-project.mjs b/scripts/sync-github-project.mjs deleted file mode 100644 index 2a2ccc9..0000000 --- a/scripts/sync-github-project.mjs +++ /dev/null @@ -1,495 +0,0 @@ -#!/usr/bin/env bun - -/** - * sync-github-project.mjs - * - * Reads PLAN.md and TODO.md and creates the corresponding GitHub structure: - * - Milestones (one per release phase, sourced from PLAN.md) - * - Labels (priority P0–P3 and layer labels) - * - Issues (one per ### task-group in TODO.md) - * - * Re-run safe: existing milestones, labels, and issues with matching titles - * are detected and skipped. Completed groups (āœ… COMPLETE) are created and - * immediately closed. - * - * Usage: - * bun scripts/sync-github-project.mjs [--dry-run] - * - * --dry-run Print every action that would be taken; make no API calls. - * - * Prerequisites: - * gh CLI installed and authenticated (run: gh auth status) - */ - -import { readFile } from "node:fs/promises"; -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; -import path from "node:path"; -import process from "node:process"; - -const execFileAsync = promisify(execFile); -const ROOT = process.cwd(); -const DRY_RUN = process.argv.includes("--dry-run"); - -// --------------------------------------------------------------------------- -// Milestone definitions — derived from the four phases in PLAN.md -// --------------------------------------------------------------------------- - -const MILESTONES = [ - { - phaseEmoji: "🚨", - title: "v0.1 — Minimal Viable", - description: - "Critical path: enable ingest and retrieval for a single user session. " + - "Exit criteria: user can call ingestText() and query() end-to-end.", - }, - { - phaseEmoji: "🟔", - title: "v0.5 — Hierarchical + Coherent", - description: - "Add hierarchical routing and coherent path ordering. " + - "Exit criteria: queries return ordered context chains, not just ranked pages.", - }, - { - phaseEmoji: "🟢", - title: "v1.0 — Background Consolidation", - description: - "Idle background maintenance keeps memory healthy. " + - "Exit criteria: system self-maintains over extended use without manual intervention.", - }, - { - phaseEmoji: "šŸ”µ", - title: "v1.0 — Polish & Ship", - description: - "Improve quality, performance, and developer experience. " + - "Exit criteria: all tests pass; benchmarks recorded; docs complete; ready for public use.", - }, -]; - -// --------------------------------------------------------------------------- -// Label definitions -// --------------------------------------------------------------------------- - -const PRIORITY_LABELS = [ - { name: "P0: critical", color: "D73A4A", description: "Critical path — blocks all dependent work" }, - { name: "P1: high", color: "E4E669", description: "High priority — targets v0.5" }, - { name: "P2: medium", color: "0075CA", description: "Medium priority — targets v1.0" }, - { name: "P3: low", color: "CFE2F3", description: "Lower priority — polish and release prep" }, -]; - -const LAYER_LABELS = [ - { name: "layer: foundation", color: "F9D0C4", description: "Core types, model profiles, crypto" }, - { name: "layer: storage", color: "FEF2C0", description: "OPFS vector store and IndexedDB metadata store" }, - { name: "layer: compute", color: "C2E0C6", description: "WebGPU / WebGL / WebNN / WASM vector backends" }, - { name: "layer: embeddings", color: "FBCA04", description: "Embedding providers and resolver" }, - { name: "layer: hippocampus", color: "5319E7", description: "Ingest orchestration (chunk → embed → persist)" }, - { name: "layer: cortex", color: "1D76DB", description: "Retrieval orchestration (rank → expand → order)" }, - { name: "layer: daydreamer", color: "0E8A16", description: "Background consolidation (LTP/LTD, recalc)" }, - { name: "layer: testing", color: "C5DEF5", description: "Test coverage and integration tests" }, - { name: "layer: ci", color: "BFD4F2", description: "CI/CD pipeline and build tooling" }, - { name: "layer: documentation",color: "BFDADC", description: "API docs, developer guide, architecture diagrams" }, -]; - -const ALL_LABELS = [...PRIORITY_LABELS, ...LAYER_LABELS]; - -// --------------------------------------------------------------------------- -// TODO.md parser -// --------------------------------------------------------------------------- - -/** - * @typedef {{ header: string, bodyLines: string[], isComplete: boolean }} IssueGroup - * @typedef {{ header: string, phaseEmoji: string, groups: IssueGroup[] }} Phase - */ - -/** - * Parse TODO.md into phases and task groups. - * @param {string} content - * @returns {Phase[]} - */ -function parseTodoMd(content) { - const lines = content.split(/\r?\n/); - /** @type {Phase[]} */ - const phases = []; - /** @type {Phase | null} */ - let currentPhase = null; - /** @type {IssueGroup | null} */ - let currentGroup = null; - - for (const line of lines) { - if (line.startsWith("## ")) { - const header = line.slice(3).trim(); - const emojiMatch = header.match(/^(\p{Emoji})/u); - currentPhase = { - header, - phaseEmoji: emojiMatch ? emojiMatch[1] : "", - groups: [], - }; - phases.push(currentPhase); - currentGroup = null; - } else if (line.startsWith("### ") && currentPhase) { - const header = line.slice(4).trim(); - currentGroup = { - header, - bodyLines: [], - isComplete: header.includes("āœ… COMPLETE"), - }; - currentPhase.groups.push(currentGroup); - } else if (line === "---") { - currentGroup = null; - } else if (currentGroup) { - currentGroup.bodyLines.push(line); - } - } - - return phases; -} - -// --------------------------------------------------------------------------- -// Label inference helpers -// --------------------------------------------------------------------------- - -const LAYER_KEYWORD_MAP = /** @type {[string, string[]][]} */ ([ - ["layer: hippocampus", ["hippocampus", "Hippocampus", "Chunker", "Ingest", "PageBuilder", "FastMetroid", "HierarchyBuilder"]], - ["layer: cortex", ["cortex/", "Ranking.ts", "Query.ts", "OpenTSPSolver", "QueryResult", "SeedSelection"]], - ["layer: daydreamer", ["daydreamer", "Daydreamer", "IdleScheduler", "HebbianUpdater", "FullMetroidRecalc", "PrototypeRecomputer", "ExperienceReplay", "ClusterStability"]], - ["layer: embeddings", ["embeddings/", "EmbeddingBackend", "OrtWebgl", "TransformersJs", "ProviderResolver"]], - ["layer: testing", ["tests/integration", "tests/benchmarks", "Integration", "Benchmark", "bench.ts"]], - ["layer: ci", ["CI", "GitHub Actions", ".github/workflows", "guard-model-derived", "ci.yml"]], - ["layer: documentation",["docs/", "documentation", "API reference", "architecture diagram"]], -]); - -/** - * Infer layer labels from a group's header and body. - * @param {string} header - * @param {string[]} bodyLines - * @returns {string[]} - */ -function inferLayerLabels(header, bodyLines) { - const haystack = [header, ...bodyLines].join("\n"); - const found = []; - for (const [label, keywords] of LAYER_KEYWORD_MAP) { - if (keywords.some((kw) => haystack.includes(kw))) { - found.push(label); - } - } - return found; -} - -/** - * Infer priority label from a group's header prefix (P0-X, P1-X …). - * @param {string} header - * @returns {string | null} - */ -function inferPriorityLabel(header) { - const m = header.match(/^P([0-3])-/); - if (!m) { - return null; - } - const map = { 0: "P0: critical", 1: "P1: high", 2: "P2: medium", 3: "P3: low" }; - return map[m[1]] ?? null; -} - -/** - * Map a phase's emoji to the matching milestone title. - * @param {string} emoji - * @returns {string | null} - */ -function phaseEmojiToMilestone(emoji) { - const m = MILESTONES.find((ms) => ms.phaseEmoji === emoji); - return m ? m.title : null; -} - -// --------------------------------------------------------------------------- -// GitHub API helpers (uses `gh auth token` for auth, native fetch for calls) -// --------------------------------------------------------------------------- - -/** @returns {Promise} */ -async function getGitHubToken() { - const { stdout } = await execFileAsync("gh", ["auth", "token"]); - return stdout.trim(); -} - -/** - * @returns {Promise<{ owner: string, repo: string }>} - */ -async function getRepoIdentity() { - const { stdout } = await execFileAsync("gh", [ - "repo", "view", "--json", "name,owner", - ]); - const { name, owner } = JSON.parse(stdout); - return { owner: owner.login, repo: name }; -} - -/** - * Fetch all pages of a GitHub API endpoint (link-header pagination). - * @param {string} token - * @param {string} url - * @returns {Promise} - */ -async function fetchAllPages(token, url) { - const results = []; - let nextUrl = url; - - while (nextUrl) { - const res = await fetch(nextUrl, { - headers: { - Authorization: `Bearer ${token}`, - Accept: "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - }, - }); - - if (!res.ok) { - const body = await res.text(); - throw new Error(`GET ${nextUrl} → ${res.status}: ${body}`); - } - - const page = await res.json(); - results.push(...page); - - const link = res.headers.get("link") ?? ""; - const match = link.match(/<([^>]+)>;\s*rel="next"/); - // null signals the end of pagination - nextUrl = match ? match[1] : null; - } - - return results; -} - -/** - * Call GitHub REST API with a JSON body. - * @param {string} token - * @param {"POST"|"PATCH"|"DELETE"} method - * @param {string} url - * @param {object} [body] - * @returns {Promise} - */ -async function githubApi(token, method, url, body) { - const res = await fetch(url, { - method, - headers: { - Authorization: `Bearer ${token}`, - Accept: "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - "Content-Type": "application/json", - }, - body: body ? JSON.stringify(body) : undefined, - }); - - const text = await res.text(); - if (!res.ok) { - throw new Error(`${method} ${url} → ${res.status}: ${text}`); - } - return text ? JSON.parse(text) : null; -} - -// --------------------------------------------------------------------------- -// Sync helpers -// --------------------------------------------------------------------------- - -/** - * Create any milestones from MILESTONES that do not yet exist. - * Returns a map of milestone title → milestone number. - * @param {string} token - * @param {string} owner - * @param {string} repo - * @returns {Promise>} - */ -async function syncMilestones(token, owner, repo) { - const base = `https://api.github.com/repos/${owner}/${repo}`; - - /** @type {Array<{ title: string, number: number }>} */ - const existing = /** @type {any[]} */ ( - await fetchAllPages(token, `${base}/milestones?state=all&per_page=100`) - ); - - const existingTitles = new Map(existing.map((m) => [m.title, m.number])); - const milestoneMap = new Map(existingTitles); - - for (const ms of MILESTONES) { - if (existingTitles.has(ms.title)) { - globalThis.console.log(` ā­ Milestone already exists: "${ms.title}"`); - continue; - } - - globalThis.console.log(` āž• Creating milestone: "${ms.title}"`); - if (!DRY_RUN) { - const created = /** @type {{ number: number }} */ ( - await githubApi(token, "POST", `${base}/milestones`, { - title: ms.title, - description: ms.description, - state: "open", - }) - ); - milestoneMap.set(ms.title, created.number); - } else { - milestoneMap.set(ms.title, -1); - } - } - - return milestoneMap; -} - -/** - * Create any labels in ALL_LABELS that do not yet exist. - * @param {string} token - * @param {string} owner - * @param {string} repo - */ -async function syncLabels(token, owner, repo) { - const base = `https://api.github.com/repos/${owner}/${repo}`; - - /** @type {Array<{ name: string }>} */ - const existing = /** @type {any[]} */ ( - await fetchAllPages(token, `${base}/labels?per_page=100`) - ); - - const existingNames = new Set(existing.map((l) => l.name)); - - for (const label of ALL_LABELS) { - if (existingNames.has(label.name)) { - globalThis.console.log(` ā­ Label already exists: "${label.name}"`); - continue; - } - - globalThis.console.log(` āž• Creating label: "${label.name}"`); - if (!DRY_RUN) { - await githubApi(token, "POST", `${base}/labels`, { - name: label.name, - color: label.color, - description: label.description, - }); - } - } -} - -/** - * Create GitHub issues for every task group in the parsed phases. - * Already-existing issues (same title) are skipped. Completed groups are - * created then immediately closed. - * - * @param {string} token - * @param {string} owner - * @param {string} repo - * @param {Phase[]} phases - * @param {Map} milestoneMap - */ -async function syncIssues(token, owner, repo, phases, milestoneMap) { - const base = `https://api.github.com/repos/${owner}/${repo}`; - - /** @type {Array<{ title: string, number: number, state: string }>} */ - const existingIssues = /** @type {any[]} */ ( - await fetchAllPages(token, `${base}/issues?state=all&per_page=100`) - ); - - const existingTitles = new Set(existingIssues.map((i) => i.title)); - - for (const phase of phases) { - if (phase.groups.length === 0) { - continue; - } - - const milestoneName = phaseEmojiToMilestone(phase.phaseEmoji); - const milestoneNumber = milestoneName ? milestoneMap.get(milestoneName) : null; - - for (const group of phase.groups) { - const title = group.header.replace(/\s*āœ…\s*COMPLETE\s*/g, "").trim(); - - if (existingTitles.has(title)) { - globalThis.console.log(` ā­ Issue already exists: "${title}"`); - continue; - } - - // Build issue body: preserve the markdown content as-is - const body = group.bodyLines.join("\n").trim(); - const priorityLabel = inferPriorityLabel(title); - const layerLabels = inferLayerLabels(title, group.bodyLines); - const labels = [priorityLabel, ...layerLabels].filter(Boolean); - - globalThis.console.log(` āž• Creating issue: "${title}"`); - if (labels.length > 0) { - globalThis.console.log(` Labels: ${labels.join(", ")}`); - } - if (milestoneName) { - globalThis.console.log(` Milestone: ${milestoneName}`); - } - - if (!DRY_RUN) { - /** @type {{ number: number }} */ - const created = /** @type {any} */ ( - await githubApi(token, "POST", `${base}/issues`, { - title, - body, - labels, - ...(milestoneNumber != null && milestoneNumber !== -1 - ? { milestone: milestoneNumber } - : {}), - }) - ); - - if (group.isComplete) { - await githubApi(token, "PATCH", `${base}/issues/${created.number}`, { - state: "closed", - state_reason: "completed", - }); - globalThis.console.log(` Closed (already complete)`); - } - } - } - } -} - -// --------------------------------------------------------------------------- -// Main -// --------------------------------------------------------------------------- - -async function main() { - if (DRY_RUN) { - globalThis.console.log("šŸ” Dry-run mode — no changes will be made.\n"); - } - - // Verify gh CLI is available - try { - await execFileAsync("gh", ["auth", "status"]); - } catch { - globalThis.console.error( - "āŒ gh CLI is not authenticated. Run: gh auth login", - ); - process.exit(1); - } - - const token = await getGitHubToken(); - const { owner, repo } = await getRepoIdentity(); - globalThis.console.log(`\nRepository: ${owner}/${repo}\n`); - - // Parse TODO.md - const todoPath = path.join(ROOT, "TODO.md"); - const todoContent = await readFile(todoPath, "utf8"); - const phases = parseTodoMd(todoContent); - - const totalGroups = phases.reduce((n, p) => n + p.groups.length, 0); - globalThis.console.log(`Parsed TODO.md: ${phases.length} phases, ${totalGroups} task groups\n`); - - // 1. Milestones - globalThis.console.log("── Milestones ──────────────────────────────────────────────"); - const milestoneMap = await syncMilestones(token, owner, repo); - - // 2. Labels - globalThis.console.log("\n── Labels ──────────────────────────────────────────────────"); - await syncLabels(token, owner, repo); - - // 3. Issues - globalThis.console.log("\n── Issues ──────────────────────────────────────────────────"); - await syncIssues(token, owner, repo, phases, milestoneMap); - - globalThis.console.log( - DRY_RUN - ? "\nāœ… Dry-run complete. Re-run without --dry-run to apply changes." - : "\nāœ… Sync complete.", - ); -} - -main().catch((error) => { - globalThis.console.error("sync-github-project crashed:", error); - process.exit(1); -});