Skip to content

Commit 20d6afc

Browse files
Copilotdevlux76
andcommitted
Add P0-X subtask creation, milestone enforcement, and project board automation workflows
Co-authored-by: devlux76 <86517969+devlux76@users.noreply.github.com>
1 parent 7d20faa commit 20d6afc

5 files changed

Lines changed: 420 additions & 2 deletions

File tree

.github/workflows/close-legacy-issues.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ jobs:
2121
const repo = context.repo.repo;
2222
2323
// Exact issue numbers created by the old sync-github-project.mjs script.
24+
// NOTE: #56 (P0-X: Fix Architectural Naming Drift) is intentionally excluded —
25+
// it is a live parent tracking issue with real subtasks (P0-X1..P0-X7).
2426
const LEGACY_ISSUES = [
2527
21, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
26-
36, 37, 38, 39, 40, 41, 42, 56, 57, 58, 59, 60, 61, 62, 63
28+
36, 37, 38, 39, 40, 41, 42, 57, 58, 59, 60, 61, 62, 63
2729
];
2830
2931
// Ensure the "wontfix" label exists
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
name: Create P0-X Subtask Issues
2+
3+
# One-shot workflow: creates the seven individually tracked subtasks for
4+
# Issue #56 (P0-X: Fix Architectural Naming Drift) and links each one back
5+
# to the parent with a "Part of #56" reference.
6+
# Run this once after the branch is merged; re-running is idempotent
7+
# (skips any subtask whose title already exists).
8+
9+
on:
10+
workflow_dispatch:
11+
inputs:
12+
dry_run:
13+
description: "Print actions without creating any issues (true/false)"
14+
required: false
15+
default: "false"
16+
type: choice
17+
options:
18+
- "false"
19+
- "true"
20+
21+
permissions:
22+
issues: write
23+
24+
jobs:
25+
create-subtasks:
26+
runs-on: ubuntu-latest
27+
steps:
28+
- name: Create P0-X1 through P0-X7 subtask issues
29+
uses: actions/github-script@v7
30+
with:
31+
script: |
32+
const owner = context.repo.owner;
33+
const repo = context.repo.repo;
34+
const PARENT = 56;
35+
const DRY_RUN = '${{ inputs.dry_run }}' === 'true';
36+
// Must match the GitHub milestone title exactly (character-for-character).
37+
// The em dash (—) is U+2014. Verify in repository Settings → Milestones.
38+
const MILESTONE_TITLE = 'v0.1 \u2014 Minimal Viable';
39+
40+
// ------------------------------------------------------------------
41+
// Subtask definitions (one entry per P0-X checklist item in #56)
42+
// ------------------------------------------------------------------
43+
const subtasks = [
44+
{
45+
id: 'P0-X1',
46+
title: '[P0-X1] Rename MetroidNeighbor → SemanticNeighbor',
47+
labels: ['P0: critical', 'layer: foundation', 'layer: storage', 'task'],
48+
body: `**Parent issue:** #${PARENT}
49+
50+
**Objective:**
51+
Rename the \`MetroidNeighbor\` type to \`SemanticNeighbor\` throughout the codebase.
52+
53+
**Files to modify:**
54+
- \`core/types.ts\` — rename type declaration
55+
- \`storage/IndexedDbMetadataStore.ts\` — update all references
56+
- All test files that reference \`MetroidNeighbor\`
57+
- JSDoc and inline comments
58+
59+
**Exit criteria:**
60+
- [ ] No occurrence of \`MetroidNeighbor\` remains in source files
61+
- [ ] All tests pass
62+
- [ ] Lint and typecheck clean
63+
`,
64+
},
65+
{
66+
id: 'P0-X2',
67+
title: '[P0-X2] Rename MetroidSubgraph → SemanticNeighborSubgraph',
68+
labels: ['P0: critical', 'layer: foundation', 'layer: storage', 'layer: cortex', 'task'],
69+
body: `**Parent issue:** #${PARENT}
70+
71+
**Objective:**
72+
Rename the \`MetroidSubgraph\` type to \`SemanticNeighborSubgraph\`.
73+
74+
**Files to modify:**
75+
- \`core/types.ts\` — rename type declaration
76+
- \`storage/IndexedDbMetadataStore.ts\` — update all references
77+
- \`cortex/Query.ts\` — update all references
78+
- JSDoc and inline comments
79+
80+
**Exit criteria:**
81+
- [ ] No occurrence of \`MetroidSubgraph\` remains in source files
82+
- [ ] All tests pass
83+
- [ ] Lint and typecheck clean
84+
`,
85+
},
86+
{
87+
id: 'P0-X3',
88+
title: '[P0-X3] Rename MetadataStore proximity-graph methods',
89+
labels: ['P0: critical', 'layer: foundation', 'layer: storage', 'layer: cortex', 'task'],
90+
body: `**Parent issue:** #${PARENT}
91+
92+
**Objective:**
93+
Rename all MetadataStore methods that refer to the proximity graph:
94+
95+
| Old name | New name |
96+
|---|---|
97+
| \`putMetroidNeighbors\` | \`putSemanticNeighbors\` |
98+
| \`getMetroidNeighbors\` | \`getSemanticNeighbors\` |
99+
| \`getInducedMetroidSubgraph\` | \`getInducedNeighborSubgraph\` |
100+
| \`needsMetroidRecalc\` | \`needsNeighborRecalc\` |
101+
| \`flagVolumeForMetroidRecalc\` | \`flagVolumeForNeighborRecalc\` |
102+
| \`clearMetroidRecalcFlag\` | \`clearNeighborRecalcFlag\` |
103+
104+
**Files to modify:**
105+
- \`core/types.ts\` — MetadataStore interface
106+
- \`storage/IndexedDbMetadataStore.ts\` — implementation + all callers
107+
- \`cortex/Query.ts\` — all callers
108+
- All test files that call these methods
109+
110+
**Exit criteria:**
111+
- [ ] No method named \`*Metroid*\` exists on MetadataStore
112+
- [ ] All tests pass
113+
- [ ] Lint and typecheck clean
114+
`,
115+
},
116+
{
117+
id: 'P0-X4',
118+
title: '[P0-X4] Rename planned file FastMetroidInsert → FastNeighborInsert',
119+
labels: ['P0: critical', 'layer: hippocampus', 'task'],
120+
body: `**Parent issue:** #${PARENT}
121+
122+
**Objective:**
123+
Rename the planned Hippocampus file and class for inserting proximity neighbors.
124+
125+
**Files to modify/create:**
126+
- Rename \`hippocampus/FastMetroidInsert.ts\` → \`hippocampus/FastNeighborInsert.ts\`
127+
- Rename class/function to \`FastNeighborInsert\` / \`insertSemanticNeighbors\`
128+
- Update any imports that reference the old path
129+
130+
**Exit criteria:**
131+
- [ ] No file named \`FastMetroidInsert.ts\` exists
132+
- [ ] No symbol named \`FastMetroidInsert\` exists in source
133+
- [ ] Lint and typecheck clean
134+
`,
135+
},
136+
{
137+
id: 'P0-X5',
138+
title: '[P0-X5] Rename planned file FullMetroidRecalc → FullNeighborRecalc',
139+
labels: ['P0: critical', 'layer: daydreamer', 'task'],
140+
body: `**Parent issue:** #${PARENT}
141+
142+
**Objective:**
143+
Rename the planned Daydreamer file and class for full neighbor recalculation.
144+
145+
**Files to modify/create:**
146+
- Rename \`daydreamer/FullMetroidRecalc.ts\` → \`daydreamer/FullNeighborRecalc.ts\`
147+
- Rename class/function to \`FullNeighborRecalc\` / \`runNeighborRecalc\`
148+
- Update any imports that reference the old path
149+
150+
**Exit criteria:**
151+
- [ ] No file named \`FullMetroidRecalc.ts\` exists
152+
- [ ] No symbol named \`FullMetroidRecalc\` exists in source
153+
- [ ] Lint and typecheck clean
154+
`,
155+
},
156+
{
157+
id: 'P0-X6',
158+
title: '[P0-X6] Rename IndexedDB object store metroid_neighbors → neighbor_graph',
159+
labels: ['P0: critical', 'layer: storage', 'task'],
160+
body: `**Parent issue:** #${PARENT}
161+
162+
**Objective:**
163+
Rename the IndexedDB object store used for the proximity graph.
164+
165+
**Files to modify:**
166+
- \`storage/IndexedDbMetadataStore.ts\`
167+
- Change store name from \`metroid_neighbors\` to \`neighbor_graph\`
168+
- Increment \`DB_VERSION\`
169+
- Add migration in \`applyUpgrade\`: copy data from old store to new store, then delete old store
170+
171+
**Exit criteria:**
172+
- [ ] No reference to the string \`metroid_neighbors\` in source
173+
- [ ] DB_VERSION incremented
174+
- [ ] Migration tested (open old DB, upgrade, verify data preserved)
175+
- [ ] All tests pass
176+
`,
177+
},
178+
{
179+
id: 'P0-X7',
180+
title: '[P0-X7] Update all docs and JSDoc: "Metroid neighbor" → "semantic neighbor"',
181+
labels: ['P0: critical', 'layer: documentation', 'task'],
182+
body: `**Parent issue:** #${PARENT}
183+
184+
**Objective:**
185+
Remove all remaining uses of "Metroid" from documentation and code comments
186+
where it refers to the proximity graph (not to the \`{ m1, m2, c }\` probe type).
187+
188+
**Files to review:**
189+
- \`DESIGN.md\`
190+
- All \`*.ts\` files — JSDoc blocks and inline comments
191+
- \`README.md\`
192+
- Any other \`.md\` files
193+
194+
**Exit criteria:**
195+
- [ ] The word "Metroid" does not appear in any doc comment where it describes the neighbor/proximity graph
196+
- [ ] "Metroid" is reserved exclusively for the \`{ m1, m2, c }\` dialectical probe
197+
- [ ] All tests pass
198+
`,
199+
},
200+
];
201+
202+
// ------------------------------------------------------------------
203+
// Resolve milestone number
204+
// ------------------------------------------------------------------
205+
let milestoneNumber = null;
206+
try {
207+
// Use per_page: 100 (API max) to handle repositories with many milestones.
208+
const { data: milestones } = await github.rest.issues.listMilestones({
209+
owner, repo, state: 'open', per_page: 100,
210+
});
211+
const m = milestones.find(x => x.title === MILESTONE_TITLE);
212+
if (m) {
213+
milestoneNumber = m.number;
214+
console.log(`Resolved milestone "${MILESTONE_TITLE}" → #${milestoneNumber}`);
215+
} else {
216+
console.log(`Milestone "${MILESTONE_TITLE}" not found — will create issues without milestone.`);
217+
console.log('Available milestones: ' + milestones.map(x => x.title).join(', '));
218+
}
219+
} catch (err) {
220+
console.log(`Could not fetch milestones: ${err.message}`);
221+
}
222+
223+
// ------------------------------------------------------------------
224+
// Fetch existing issue titles to allow idempotent re-runs
225+
// ------------------------------------------------------------------
226+
const existing = new Set();
227+
for await (const page of github.paginate.iterator(
228+
github.rest.issues.listForRepo,
229+
{ owner, repo, state: 'all', per_page: 100 },
230+
)) {
231+
for (const issue of page.data) {
232+
existing.add(issue.title);
233+
}
234+
}
235+
236+
// ------------------------------------------------------------------
237+
// Ensure required labels exist
238+
// ------------------------------------------------------------------
239+
const requiredLabels = [
240+
{ name: 'task', color: '0075ca', description: 'Implementation task' },
241+
{ name: 'P0: critical', color: 'e11d48', description: 'Blocks dependent work' },
242+
];
243+
for (const lbl of requiredLabels) {
244+
try {
245+
await github.rest.issues.getLabel({ owner, repo, name: lbl.name });
246+
} catch (err) {
247+
if (err.status === 404) {
248+
if (!DRY_RUN) {
249+
await github.rest.issues.createLabel({ owner, repo, ...lbl });
250+
console.log(`Created label: ${lbl.name}`);
251+
}
252+
} else {
253+
console.log(`Unexpected error checking label "${lbl.name}": ${err.message}`);
254+
}
255+
}
256+
}
257+
258+
// ------------------------------------------------------------------
259+
// Create subtask issues
260+
// ------------------------------------------------------------------
261+
const created = [];
262+
for (const task of subtasks) {
263+
if (existing.has(task.title)) {
264+
console.log(`SKIP (already exists): ${task.title}`);
265+
continue;
266+
}
267+
if (DRY_RUN) {
268+
console.log(`DRY RUN — would create: ${task.title}`);
269+
continue;
270+
}
271+
const payload = {
272+
owner, repo,
273+
title: task.title,
274+
body: task.body,
275+
labels: task.labels,
276+
};
277+
if (milestoneNumber) payload.milestone = milestoneNumber;
278+
279+
const { data: issue } = await github.rest.issues.create(payload);
280+
console.log(`Created #${issue.number}: ${issue.title}`);
281+
created.push(issue.number);
282+
}
283+
284+
// ------------------------------------------------------------------
285+
// Post a summary comment on the parent issue
286+
// ------------------------------------------------------------------
287+
if (created.length > 0) {
288+
const lines = created.map(n => `- #${n}`).join('\n');
289+
await github.rest.issues.createComment({
290+
owner, repo,
291+
issue_number: PARENT,
292+
body: `The following individually tracked subtask issues have been created:\n\n${lines}\n\nEach issue carries the \`P0: critical\` label, is linked to the **v0.1 — Minimal Viable** milestone, and references this parent issue in its description.`,
293+
});
294+
console.log(`Posted summary comment on #${PARENT}`);
295+
} else if (!DRY_RUN) {
296+
console.log('All subtasks already exist — nothing created.');
297+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Enforce Milestone on Issues
2+
3+
# Automatically assigns a milestone to any newly opened issue (or an issue
4+
# that just received a priority label) based on the priority label present:
5+
#
6+
# P0: critical → v0.1 — Minimal Viable
7+
# P1: high → v0.5 — Alpha
8+
# P2: medium → v1.0 — Production
9+
# P3: low → v1.0 — Production
10+
#
11+
# If the issue already has a milestone the workflow is a no-op.
12+
13+
on:
14+
issues:
15+
types: [opened, labeled]
16+
17+
permissions:
18+
issues: write
19+
20+
jobs:
21+
assign-milestone:
22+
runs-on: ubuntu-latest
23+
steps:
24+
- name: Assign milestone based on priority label
25+
uses: actions/github-script@v7
26+
with:
27+
script: |
28+
const owner = context.repo.owner;
29+
const repo = context.repo.repo;
30+
31+
const issue = context.payload.issue;
32+
33+
// Skip if milestone is already assigned
34+
if (issue.milestone) {
35+
console.log(`Issue #${issue.number} already has milestone "${issue.milestone.title}" — skipping.`);
36+
return;
37+
}
38+
39+
// Map priority labels to milestone titles.
40+
// The em dash (—) is U+2014. Verify titles match exactly in
41+
// repository Settings → Milestones.
42+
const PRIORITY_MAP = {
43+
'P0: critical': 'v0.1 \u2014 Minimal Viable',
44+
'P1: high': 'v0.5 \u2014 Alpha',
45+
'P2: medium': 'v1.0 \u2014 Production',
46+
'P3: low': 'v1.0 \u2014 Production',
47+
};
48+
49+
const labelNames = (issue.labels || []).map(l => l.name);
50+
let targetMilestoneTitle = null;
51+
for (const [label, milestoneTitle] of Object.entries(PRIORITY_MAP)) {
52+
if (labelNames.includes(label)) {
53+
targetMilestoneTitle = milestoneTitle;
54+
break;
55+
}
56+
}
57+
58+
if (!targetMilestoneTitle) {
59+
console.log(`Issue #${issue.number} has no recognized priority label — skipping.`);
60+
return;
61+
}
62+
63+
// Look up the milestone number
64+
// Use per_page: 100 (API max) to avoid missing milestones on
65+
// repositories that have more than 50 open milestones.
66+
const { data: milestones } = await github.rest.issues.listMilestones({
67+
owner, repo, state: 'open', per_page: 100,
68+
});
69+
const milestone = milestones.find(m => m.title === targetMilestoneTitle);
70+
71+
if (!milestone) {
72+
console.log(`Milestone "${targetMilestoneTitle}" not found — cannot auto-assign.`);
73+
return;
74+
}
75+
76+
await github.rest.issues.update({
77+
owner, repo,
78+
issue_number: issue.number,
79+
milestone: milestone.number,
80+
});
81+
console.log(`Assigned milestone "${milestone.title}" to issue #${issue.number}.`);

0 commit comments

Comments
 (0)