diff --git a/.github/workflows/close-legacy-issues.yml b/.github/workflows/close-legacy-issues.yml deleted file mode 100644 index 86c922b..0000000 --- a/.github/workflows/close-legacy-issues.yml +++ /dev/null @@ -1,69 +0,0 @@ -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/create-p0x-subtasks.yml b/.github/workflows/create-p0x-subtasks.yml new file mode 100644 index 0000000..e5552d8 --- /dev/null +++ b/.github/workflows/create-p0x-subtasks.yml @@ -0,0 +1,306 @@ +name: Create P0-X Subtask Issues + +# One-shot workflow: creates the seven individually tracked subtasks for +# Issue #56 (P0-X: Fix Architectural Naming Drift) and links each one back +# to the parent with a "Part of #56" reference. +# Run this once after the branch is merged; re-running is idempotent +# (skips any subtask whose title already exists). + +on: + workflow_dispatch: + inputs: + dry_run: + description: "Print actions without creating any issues (true/false)" + required: false + default: "false" + type: choice + options: + - "false" + - "true" + +permissions: + issues: write + +jobs: + create-subtasks: + runs-on: ubuntu-latest + steps: + - name: Create P0-X1 through P0-X7 subtask issues + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const PARENT = 56; + const DRY_RUN = '${{ inputs.dry_run }}' === 'true'; + // Must match the GitHub milestone title exactly (character-for-character). + // The em dash (—) is U+2014. Verify in repository Settings → Milestones. + const MILESTONE_TITLE = 'v0.1 \u2014 Minimal Viable'; + + // ------------------------------------------------------------------ + // Subtask definitions (one entry per P0-X checklist item in #56) + // ------------------------------------------------------------------ + const subtasks = [ + { + id: 'P0-X1', + title: '[P0-X1] Rename MetroidNeighbor → SemanticNeighbor', + labels: ['P0: critical', 'layer: foundation', 'layer: storage', 'task'], + body: `**Parent issue:** #${PARENT} + +**Objective:** +Rename the \`MetroidNeighbor\` type to \`SemanticNeighbor\` throughout the codebase. + +**Files to modify:** +- \`core/types.ts\` — rename type declaration +- \`storage/IndexedDbMetadataStore.ts\` — update all references +- All test files that reference \`MetroidNeighbor\` +- JSDoc and inline comments + +**Exit criteria:** +- [ ] No occurrence of \`MetroidNeighbor\` remains in source files +- [ ] All tests pass +- [ ] Lint and typecheck clean +`, + }, + { + id: 'P0-X2', + title: '[P0-X2] Rename MetroidSubgraph → SemanticNeighborSubgraph', + labels: ['P0: critical', 'layer: foundation', 'layer: storage', 'layer: cortex', 'task'], + body: `**Parent issue:** #${PARENT} + +**Objective:** +Rename the \`MetroidSubgraph\` type to \`SemanticNeighborSubgraph\`. + +**Files to modify:** +- \`core/types.ts\` — rename type declaration +- \`storage/IndexedDbMetadataStore.ts\` — update all references +- \`cortex/Query.ts\` — update all references +- JSDoc and inline comments + +**Exit criteria:** +- [ ] No occurrence of \`MetroidSubgraph\` remains in source files +- [ ] All tests pass +- [ ] Lint and typecheck clean +`, + }, + { + id: 'P0-X3', + title: '[P0-X3] Rename MetadataStore proximity-graph methods', + labels: ['P0: critical', 'layer: foundation', 'layer: storage', 'layer: cortex', 'task'], + body: `**Parent issue:** #${PARENT} + +**Objective:** +Rename all MetadataStore methods that refer to the proximity graph: + +| Old name | New name | +|---|---| +| \`putMetroidNeighbors\` | \`putSemanticNeighbors\` | +| \`getMetroidNeighbors\` | \`getSemanticNeighbors\` | +| \`getInducedMetroidSubgraph\` | \`getInducedNeighborSubgraph\` | +| \`needsMetroidRecalc\` | \`needsNeighborRecalc\` | +| \`flagVolumeForMetroidRecalc\` | \`flagVolumeForNeighborRecalc\` | +| \`clearMetroidRecalcFlag\` | \`clearNeighborRecalcFlag\` | + +**Files to modify:** +- \`core/types.ts\` — MetadataStore interface +- \`storage/IndexedDbMetadataStore.ts\` — implementation + all callers +- \`cortex/Query.ts\` — all callers +- All test files that call these methods + +**Exit criteria:** +- [ ] No method named \`*Metroid*\` exists on MetadataStore +- [ ] All tests pass +- [ ] Lint and typecheck clean +`, + }, + { + id: 'P0-X4', + title: '[P0-X4] Rename planned file FastMetroidInsert → FastNeighborInsert', + labels: ['P0: critical', 'layer: hippocampus', 'task'], + body: `**Parent issue:** #${PARENT} + +**Objective:** +Rename the planned Hippocampus file and class for inserting proximity neighbors. + +**Files to modify/create:** +- Rename \`hippocampus/FastMetroidInsert.ts\` → \`hippocampus/FastNeighborInsert.ts\` +- Rename class/function to \`FastNeighborInsert\` / \`insertSemanticNeighbors\` +- Update any imports that reference the old path + +**Exit criteria:** +- [ ] No file named \`FastMetroidInsert.ts\` exists +- [ ] No symbol named \`FastMetroidInsert\` exists in source +- [ ] Lint and typecheck clean +`, + }, + { + id: 'P0-X5', + title: '[P0-X5] Rename planned file FullMetroidRecalc → FullNeighborRecalc', + labels: ['P0: critical', 'layer: daydreamer', 'task'], + body: `**Parent issue:** #${PARENT} + +**Objective:** +Rename the planned Daydreamer file and class for full neighbor recalculation. + +**Files to modify/create:** +- Rename \`daydreamer/FullMetroidRecalc.ts\` → \`daydreamer/FullNeighborRecalc.ts\` +- Rename class/function to \`FullNeighborRecalc\` / \`runNeighborRecalc\` +- Update any imports that reference the old path + +**Exit criteria:** +- [ ] No file named \`FullMetroidRecalc.ts\` exists +- [ ] No symbol named \`FullMetroidRecalc\` exists in source +- [ ] Lint and typecheck clean +`, + }, + { + id: 'P0-X6', + title: '[P0-X6] Rename IndexedDB object store metroid_neighbors → neighbor_graph', + labels: ['P0: critical', 'layer: storage', 'task'], + body: `**Parent issue:** #${PARENT} + +**Objective:** +Rename the IndexedDB object store used for the proximity graph. + +**Files to modify:** +- \`storage/IndexedDbMetadataStore.ts\` + - Change store name from \`metroid_neighbors\` to \`neighbor_graph\` + - Increment \`DB_VERSION\` + - Add migration in \`applyUpgrade\`: copy data from old store to new store, then delete old store + +**Exit criteria:** +- [ ] No reference to the string \`metroid_neighbors\` in source +- [ ] DB_VERSION incremented +- [ ] Migration tested (open old DB, upgrade, verify data preserved) +- [ ] All tests pass +`, + }, + { + id: 'P0-X7', + title: '[P0-X7] Update all docs and JSDoc: "Metroid neighbor" → "semantic neighbor"', + labels: ['P0: critical', 'layer: documentation', 'task'], + body: `**Parent issue:** #${PARENT} + +**Objective:** +Remove all remaining uses of "Metroid" from documentation and code comments +where it refers to the proximity graph (not to the \`{ m1, m2, c }\` probe type). + +**Files to review:** +- \`DESIGN.md\` +- All \`*.ts\` files — JSDoc blocks and inline comments +- \`README.md\` +- Any other \`.md\` files + +**Exit criteria:** +- [ ] The word "Metroid" does not appear in any doc comment where it describes the neighbor/proximity graph +- [ ] "Metroid" is reserved exclusively for the \`{ m1, m2, c }\` dialectical probe +- [ ] All tests pass +`, + }, + ]; + + // ------------------------------------------------------------------ + // Resolve milestone number + // ------------------------------------------------------------------ + let milestoneNumber = null; + try { + // Use per_page: 100 (API max) to handle repositories with many milestones. + const { data: milestones } = await github.rest.issues.listMilestones({ + owner, repo, state: 'open', per_page: 100, + }); + const m = milestones.find(x => x.title === MILESTONE_TITLE); + if (m) { + milestoneNumber = m.number; + console.log(`Resolved milestone "${MILESTONE_TITLE}" → #${milestoneNumber}`); + } else { + console.log(`Milestone "${MILESTONE_TITLE}" not found — will create issues without milestone.`); + console.log('Available milestones: ' + milestones.map(x => x.title).join(', ')); + } + } catch (err) { + console.log(`Could not fetch milestones: ${err.message}`); + } + + // ------------------------------------------------------------------ + // Fetch existing issue titles to allow idempotent re-runs + // ------------------------------------------------------------------ + const existing = new Set(); + for await (const page of github.paginate.iterator( + github.rest.issues.listForRepo, + { owner, repo, state: 'all', per_page: 100 }, + )) { + for (const issue of page.data) { + // `issues.listForRepo` returns both issues and pull requests. + // Only track real issues here so idempotency isn't affected by PR titles. + if (!issue.pull_request) { + existing.add(issue.title); + } + } + } + + // ------------------------------------------------------------------ + // Ensure required labels exist + // ------------------------------------------------------------------ + const requiredLabels = [ + { name: 'task', color: '0075ca', description: 'Implementation task' }, + { name: 'P0: critical', color: 'e11d48', description: 'Blocks dependent work' }, + ]; + for (const lbl of requiredLabels) { + try { + await github.rest.issues.getLabel({ owner, repo, name: lbl.name }); + } catch (err) { + if (err.status === 404) { + if (!DRY_RUN) { + await github.rest.issues.createLabel({ owner, repo, ...lbl }); + console.log(`Created label: ${lbl.name}`); + } + } else { + console.log(`Unexpected error checking label "${lbl.name}": ${err.message}`); + } + } + } + + // ------------------------------------------------------------------ + // Create subtask issues + // ------------------------------------------------------------------ + const created = []; + for (const task of subtasks) { + if (existing.has(task.title)) { + console.log(`SKIP (already exists): ${task.title}`); + continue; + } + if (DRY_RUN) { + console.log(`DRY RUN — would create: ${task.title}`); + continue; + } + const payload = { + owner, repo, + title: task.title, + body: task.body, + labels: task.labels, + }; + if (milestoneNumber) payload.milestone = milestoneNumber; + + const { data: issue } = await github.rest.issues.create(payload); + console.log(`Created #${issue.number}: ${issue.title}`); + created.push(issue.number); + } + + // ------------------------------------------------------------------ + // Post a summary comment on the parent issue + // ------------------------------------------------------------------ + if (created.length > 0) { + const lines = created.map(n => `- #${n}`).join('\n'); + const summaryBody = + `The following individually tracked subtask issues have been created:\n\n${lines}\n\n` + + `Each issue carries the \`P0: critical\` label` + + (milestoneNumber ? ', is linked to the **v0.1 — Minimal Viable** milestone,' : ',') + + ' and references this parent issue in its description.'; + await github.rest.issues.createComment({ + owner, repo, + issue_number: PARENT, + body: summaryBody, + }); + console.log(`Posted summary comment on #${PARENT}`); + } else if (!DRY_RUN) { + console.log('All subtasks already exist — nothing created.'); + } diff --git a/.github/workflows/enforce-milestone.yml b/.github/workflows/enforce-milestone.yml new file mode 100644 index 0000000..7d91c09 --- /dev/null +++ b/.github/workflows/enforce-milestone.yml @@ -0,0 +1,81 @@ +name: Enforce Milestone on Issues + +# Automatically assigns a milestone to any newly opened issue (or an issue +# that just received a priority label) based on the priority label present: +# +# P0: critical → v0.1 — Minimal Viable +# P1: high → v0.5 — Alpha +# P2: medium → v1.0 — Production +# P3: low → v1.0 — Production +# +# If the issue already has a milestone the workflow is a no-op. + +on: + issues: + types: [opened, labeled] + +permissions: + issues: write + +jobs: + assign-milestone: + runs-on: ubuntu-latest + steps: + - name: Assign milestone based on priority label + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + + const issue = context.payload.issue; + + // Skip if milestone is already assigned + if (issue.milestone) { + console.log(`Issue #${issue.number} already has milestone "${issue.milestone.title}" — skipping.`); + return; + } + + // Map priority labels to milestone titles. + // The em dash (—) is U+2014. Verify titles match exactly in + // repository Settings → Milestones. + const PRIORITY_MAP = { + 'P0: critical': 'v0.1 \u2014 Minimal Viable', + 'P1: high': 'v0.5 \u2014 Alpha', + 'P2: medium': 'v1.0 \u2014 Production', + 'P3: low': 'v1.0 \u2014 Production', + }; + + const labelNames = (issue.labels || []).map(l => l.name); + let targetMilestoneTitle = null; + for (const [label, milestoneTitle] of Object.entries(PRIORITY_MAP)) { + if (labelNames.includes(label)) { + targetMilestoneTitle = milestoneTitle; + break; + } + } + + if (!targetMilestoneTitle) { + console.log(`Issue #${issue.number} has no recognized priority label — skipping.`); + return; + } + + // Look up the milestone number + // Use per_page: 100 (API max) to avoid missing milestones on + // repositories that have more than 50 open milestones. + const { data: milestones } = await github.rest.issues.listMilestones({ + owner, repo, state: 'open', per_page: 100, + }); + const milestone = milestones.find(m => m.title === targetMilestoneTitle); + + if (!milestone) { + console.log(`Milestone "${targetMilestoneTitle}" not found — cannot auto-assign.`); + return; + } + + await github.rest.issues.update({ + owner, repo, + issue_number: issue.number, + milestone: milestone.number, + }); + console.log(`Assigned milestone "${milestone.title}" to issue #${issue.number}.`); diff --git a/.github/workflows/project-board-automation.yml b/.github/workflows/project-board-automation.yml new file mode 100644 index 0000000..a2a11d0 --- /dev/null +++ b/.github/workflows/project-board-automation.yml @@ -0,0 +1,42 @@ +name: Project Board Automation + +# Automatically adds newly opened issues and pull requests to the GitHub +# Projects board. +# +# SETUP REQUIRED — before this workflow can run: +# 1. Create (or identify) a GitHub Projects v2 board for this repository. +# 2. Copy its URL, e.g. https://github.com/orgs/devlux76/projects/1 +# or https://github.com/users/devlux76/projects/1 +# 3. Store that URL as a repository Actions variable named PROJECT_URL: +# Repository → Settings → Secrets and variables → Actions → Variables +# Name: PROJECT_URL Value: +# 4. Create a fine-grained Personal Access Token (PAT) with the +# "Projects (read & write)" permission and store it as the repository +# secret PROJECT_TOKEN. +# (The default GITHUB_TOKEN does not have project write access.) +# +# Once both are set, this workflow will auto-populate the board with every +# new issue or pull request. + +on: + issues: + types: [opened] + pull_request: + types: [opened] + +jobs: + add-to-project: + name: Add to project board + runs-on: ubuntu-latest + permissions: {} + # Skip entirely if the repository variable or token have not been configured, + # or if this is a pull request from a fork (no secrets available). + if: vars.PROJECT_URL != '' && + secrets.PROJECT_TOKEN != '' && + (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + steps: + - name: Add issue or PR to project board + uses: actions/add-to-project@v1.0.2 + with: + project-url: ${{ vars.PROJECT_URL }} + github-token: ${{ secrets.PROJECT_TOKEN }} diff --git a/project_board.json b/project_board.json deleted file mode 100644 index f3e0b04..0000000 --- a/project_board.json +++ /dev/null @@ -1 +0,0 @@ -{\n "name": "Global Taskboard",\n "body": "Automatically link all open and future issues in the repository to this board. Use labels such as `in-progress`, `to-do`, or `done` to move cards between columns.",\n "columns": [\n {\n "name": "Backlog",\n "description": "For all ideas and tasks captured but not planned for immediate work."\n },\n {\n "name": "To Do",\n "description": "Tasks that are planned and ready to be started."\n },\n {\n "name": "In Progress",\n "description": "Work that is actively being worked on."\n },\n {\n "name": "Code Review",\n "description": "Work completed and awaiting pull request review."\n },\n {\n "name": "Done",\n "description": "Completed tasks."\n }\n ]\n} \ No newline at end of file