@miketromba/issy-core 0.3.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miketromba/issy-core",
3
- "version": "0.3.0",
3
+ "version": "0.5.1",
4
4
  "description": "Issue storage, search, and parsing for issy",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,6 +29,7 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "date-fns": "^4.1.0",
32
+ "fractional-indexing": "^3.2.0",
32
33
  "fuse.js": "^7.1.0"
33
34
  }
34
35
  }
@@ -37,7 +37,7 @@ const QUALIFIER_VALUES: Record<string, readonly string[]> = {
37
37
  priority: ['high', 'medium', 'low'] as const,
38
38
  scope: ['small', 'medium', 'large'] as const,
39
39
  type: ['bug', 'improvement'] as const,
40
- sort: ['priority', 'scope', 'created', 'updated', 'id'] as const,
40
+ sort: ['roadmap', 'priority', 'scope', 'created', 'updated', 'id'] as const,
41
41
  // label values are dynamic and provided via existingLabels parameter
42
42
  }
43
43
 
package/src/lib/index.ts CHANGED
@@ -14,24 +14,35 @@ export { formatDisplayDate, formatFullDate } from './formatDate'
14
14
  export {
15
15
  autoDetectIssuesDir,
16
16
  closeIssue,
17
+ computeOrderKey,
17
18
  createIssue,
18
19
  createSlug,
19
20
  deleteIssue,
20
21
  ensureIssuesDir,
21
22
  findGitRoot,
22
23
  findIssuesDirUpward,
24
+ findIssyDirUpward,
25
+ findLegacyIssuesDirUpward,
23
26
  formatDate,
27
+ generateBatchOrderKeys,
24
28
  generateFrontmatter,
25
29
  getAllIssues,
26
30
  getIssue,
27
31
  getIssueFiles,
28
32
  getIssueIdFromFilename,
29
33
  getIssuesDir,
34
+ getIssyDir,
35
+ getNextIssue,
30
36
  getNextIssueNumber,
37
+ getOnCloseContent,
38
+ getOpenIssuesByOrder,
39
+ hasLegacyIssuesDir,
31
40
  parseFrontmatter,
32
41
  reopenIssue,
33
42
  resolveIssuesDir,
43
+ resolveIssyDir,
34
44
  setIssuesDir,
45
+ setIssyDir,
35
46
  updateIssue,
36
47
  } from './issues'
37
48
  // Query parser
package/src/lib/issues.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  import { existsSync } from 'node:fs'
7
7
  import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
8
8
  import { dirname, join, resolve } from 'node:path'
9
+ import { generateKeyBetween, generateNKeysBetween } from 'fractional-indexing'
9
10
  import type {
10
11
  CreateIssueInput,
11
12
  Issue,
@@ -13,58 +14,85 @@ import type {
13
14
  UpdateIssueInput,
14
15
  } from './types'
15
16
 
16
- // Default issues directory - can be overridden
17
+ let issyDir: string | null = null
17
18
  let issuesDir: string | null = null
18
19
 
20
+ export function setIssyDir(dir: string) {
21
+ issyDir = dir
22
+ issuesDir = join(dir, 'issues')
23
+ }
24
+
25
+ export function getIssyDir(): string {
26
+ if (!issyDir) {
27
+ throw new Error(
28
+ 'Issy directory not initialized. Call resolveIssyDir() first.',
29
+ )
30
+ }
31
+ return issyDir
32
+ }
33
+
19
34
  /**
20
- * Initialize the issues directory path
35
+ * @deprecated Use setIssyDir() instead
21
36
  */
22
37
  export function setIssuesDir(dir: string) {
23
38
  issuesDir = dir
24
39
  }
25
40
 
26
- /**
27
- * Get the issues directory path
28
- */
29
41
  export function getIssuesDir(): string {
30
42
  if (!issuesDir) {
31
43
  throw new Error(
32
- 'Issues directory not initialized. Call setIssuesDir() or resolveIssuesDir() first.',
44
+ 'Issues directory not initialized. Call resolveIssyDir() first.',
33
45
  )
34
46
  }
35
47
  return issuesDir
36
48
  }
37
49
 
38
- /**
39
- * Ensure issues directory exists
40
- */
41
50
  export async function ensureIssuesDir(): Promise<void> {
42
51
  await mkdir(getIssuesDir(), { recursive: true })
43
52
  }
44
53
 
45
54
  /**
46
- * Try to find .issues directory by walking up from the given path.
47
- * Returns the path if found, or null if not found.
55
+ * Try to find .issy directory by walking up from the given path.
48
56
  */
49
- export function findIssuesDirUpward(fromPath: string): string | null {
57
+ export function findIssyDirUpward(fromPath: string): string | null {
58
+ let current = resolve(fromPath)
59
+ for (let i = 0; i < 20; i++) {
60
+ const candidate = join(current, '.issy')
61
+ if (existsSync(candidate)) {
62
+ return candidate
63
+ }
64
+ const parent = dirname(current)
65
+ if (parent === current) break
66
+ current = parent
67
+ }
68
+ return null
69
+ }
70
+
71
+ /**
72
+ * Try to find legacy .issues directory by walking up from the given path.
73
+ * Used for migration detection.
74
+ */
75
+ export function findLegacyIssuesDirUpward(fromPath: string): string | null {
50
76
  let current = resolve(fromPath)
51
- // Walk up to 20 levels (reasonable for most project structures)
52
77
  for (let i = 0; i < 20; i++) {
53
78
  const candidate = join(current, '.issues')
54
79
  if (existsSync(candidate)) {
55
80
  return candidate
56
81
  }
57
82
  const parent = dirname(current)
58
- if (parent === current) break // reached filesystem root
83
+ if (parent === current) break
59
84
  current = parent
60
85
  }
61
86
  return null
62
87
  }
63
88
 
64
89
  /**
65
- * Find the git repository root by walking up from the given path.
66
- * Returns the directory containing .git, or null if not in a git repo.
90
+ * @deprecated Use findIssyDirUpward() instead
67
91
  */
92
+ export function findIssuesDirUpward(fromPath: string): string | null {
93
+ return findIssyDirUpward(fromPath) ?? findLegacyIssuesDirUpward(fromPath)
94
+ }
95
+
68
96
  export function findGitRoot(fromPath: string): string | null {
69
97
  let current = resolve(fromPath)
70
98
  for (let i = 0; i < 20; i++) {
@@ -73,64 +101,78 @@ export function findGitRoot(fromPath: string): string | null {
73
101
  return current
74
102
  }
75
103
  const parent = dirname(current)
76
- if (parent === current) break // reached filesystem root
104
+ if (parent === current) break
77
105
  current = parent
78
106
  }
79
107
  return null
80
108
  }
81
109
 
82
110
  /**
83
- * Resolve the issues directory using the following priority:
84
- * 1. ISSUES_DIR env var (explicit override)
85
- * 2. Walk up from ISSUES_ROOT or cwd to find existing .issues directory
86
- * 3. If in a git repo, use .issues at the repo root
87
- * 4. Fall back to creating .issues in ISSUES_ROOT or cwd
111
+ * Resolve the .issy directory using the following priority:
112
+ * 1. ISSY_DIR env var (explicit override)
113
+ * 2. Walk up from cwd to find existing .issy directory
114
+ * 3. If in a git repo, use .issy at the repo root
115
+ * 4. Fall back to cwd/.issy
88
116
  *
89
- * This also sets the internal issuesDir variable.
117
+ * Also detects legacy .issues/ directories and warns.
90
118
  */
91
- export function resolveIssuesDir(): string {
92
- // 1. Explicit override via ISSUES_DIR
93
- if (process.env.ISSUES_DIR) {
94
- const dir = resolve(process.env.ISSUES_DIR)
95
- setIssuesDir(dir)
119
+ export function resolveIssyDir(): string {
120
+ if (process.env.ISSY_DIR) {
121
+ const dir = resolve(process.env.ISSY_DIR)
122
+ setIssyDir(dir)
96
123
  return dir
97
124
  }
98
125
 
99
- // 2. Try to find existing .issues by walking up from start directory
100
- const startDir = process.env.ISSUES_ROOT || process.cwd()
101
- const found = findIssuesDirUpward(startDir)
126
+ const startDir = process.env.ISSY_ROOT || process.cwd()
127
+
128
+ const found = findIssyDirUpward(startDir)
102
129
  if (found) {
103
- setIssuesDir(found)
130
+ setIssyDir(found)
104
131
  return found
105
132
  }
106
133
 
107
- // 3. If in a git repo, use .issues at the repo root
108
134
  const gitRoot = findGitRoot(startDir)
109
135
  if (gitRoot) {
110
- const gitIssuesDir = join(gitRoot, '.issues')
111
- setIssuesDir(gitIssuesDir)
112
- return gitIssuesDir
136
+ const gitIssyDir = join(gitRoot, '.issy')
137
+ setIssyDir(gitIssyDir)
138
+ return gitIssyDir
113
139
  }
114
140
 
115
- // 4. Fall back to creating .issues in the start directory
116
- const fallback = join(resolve(startDir), '.issues')
117
- setIssuesDir(fallback)
141
+ const fallback = join(resolve(startDir), '.issy')
142
+ setIssyDir(fallback)
118
143
  return fallback
119
144
  }
120
145
 
121
146
  /**
122
- * Auto-detect issues directory from common locations
123
- * @deprecated Use resolveIssuesDir() instead, which handles all cases
147
+ * @deprecated Use resolveIssyDir() instead
148
+ */
149
+ export function resolveIssuesDir(): string {
150
+ resolveIssyDir()
151
+ return getIssuesDir()
152
+ }
153
+
154
+ /**
155
+ * @deprecated Use resolveIssyDir() instead
124
156
  */
125
157
  export function autoDetectIssuesDir(fromPath: string): string {
126
- const found = findIssuesDirUpward(fromPath)
127
- if (found) return found
128
- throw new Error('Could not find .issues directory')
158
+ const found = findIssyDirUpward(fromPath)
159
+ if (found) {
160
+ setIssyDir(found)
161
+ return getIssuesDir()
162
+ }
163
+ throw new Error('Could not find .issy directory')
129
164
  }
130
165
 
131
166
  /**
132
- * Parse YAML front matter from issue content
167
+ * Check if a legacy .issues/ directory exists (not inside .issy/)
133
168
  */
169
+ export function hasLegacyIssuesDir(): string | null {
170
+ const startDir = process.env.ISSY_ROOT || process.cwd()
171
+ return findLegacyIssuesDirUpward(startDir)
172
+ }
173
+
174
+ // --- Frontmatter ---
175
+
134
176
  export function parseFrontmatter(content: string): {
135
177
  frontmatter: Partial<IssueFrontmatter>
136
178
  body: string
@@ -155,9 +197,6 @@ export function parseFrontmatter(content: string): {
155
197
  return { frontmatter, body }
156
198
  }
157
199
 
158
- /**
159
- * Generate YAML front matter string from issue data
160
- */
161
200
  export function generateFrontmatter(data: IssueFrontmatter): string {
162
201
  const lines = ['---']
163
202
  lines.push(`title: ${data.title}`)
@@ -171,6 +210,9 @@ export function generateFrontmatter(data: IssueFrontmatter): string {
171
210
  lines.push(`labels: ${data.labels}`)
172
211
  }
173
212
  lines.push(`status: ${data.status}`)
213
+ if (data.order) {
214
+ lines.push(`order: ${data.order}`)
215
+ }
174
216
  lines.push(`created: ${data.created}`)
175
217
  if (data.updated) {
176
218
  lines.push(`updated: ${data.updated}`)
@@ -179,17 +221,13 @@ export function generateFrontmatter(data: IssueFrontmatter): string {
179
221
  return lines.join('\n')
180
222
  }
181
223
 
182
- /**
183
- * Get issue ID from filename (e.g., "0001-fix-bug.md" -> "0001")
184
- */
224
+ // --- File operations ---
225
+
185
226
  export function getIssueIdFromFilename(filename: string): string {
186
227
  const match = filename.match(/^(\d+)-/)
187
228
  return match ? match[1] : filename.replace('.md', '')
188
229
  }
189
230
 
190
- /**
191
- * Create URL-friendly slug from title
192
- */
193
231
  export function createSlug(title: string): string {
194
232
  return title
195
233
  .toLowerCase()
@@ -199,17 +237,10 @@ export function createSlug(title: string): string {
199
237
  .slice(0, 50)
200
238
  }
201
239
 
202
- /**
203
- * Format date as ISO 8601 timestamp (YYYY-MM-DDTHH:mm:ss)
204
- * This provides second-level precision for better sorting
205
- */
206
240
  export function formatDate(date: Date = new Date()): string {
207
241
  return date.toISOString().slice(0, 19)
208
242
  }
209
243
 
210
- /**
211
- * Get all issue filenames from the issues directory
212
- */
213
244
  export async function getIssueFiles(): Promise<string[]> {
214
245
  try {
215
246
  const files = await readdir(getIssuesDir())
@@ -219,9 +250,6 @@ export async function getIssueFiles(): Promise<string[]> {
219
250
  }
220
251
  }
221
252
 
222
- /**
223
- * Get the next available issue number
224
- */
225
253
  export async function getNextIssueNumber(): Promise<string> {
226
254
  const files = await getIssueFiles()
227
255
  if (files.length === 0) return '0001'
@@ -234,9 +262,6 @@ export async function getNextIssueNumber(): Promise<string> {
234
262
  return String(max + 1).padStart(4, '0')
235
263
  }
236
264
 
237
- /**
238
- * Load a single issue by ID
239
- */
240
265
  export async function getIssue(id: string): Promise<Issue | null> {
241
266
  const files = await getIssueFiles()
242
267
  const paddedId = id.padStart(4, '0')
@@ -259,9 +284,6 @@ export async function getIssue(id: string): Promise<Issue | null> {
259
284
  }
260
285
  }
261
286
 
262
- /**
263
- * Load all issues
264
- */
265
287
  export async function getAllIssues(): Promise<Issue[]> {
266
288
  const files = await getIssueFiles()
267
289
  const issues: Issue[] = []
@@ -279,23 +301,102 @@ export async function getAllIssues(): Promise<Issue[]> {
279
301
  })
280
302
  }
281
303
 
282
- // Sort by priority (high medium low), then by ID (newest first) within each priority
283
- const priorityOrder: Record<string, number> = { high: 0, medium: 1, low: 2 }
304
+ // Default sort: roadmap order for issues that have it, then by ID
284
305
  return issues.sort((a, b) => {
285
- const priorityA = priorityOrder[a.frontmatter.priority] ?? 999
286
- const priorityB = priorityOrder[b.frontmatter.priority] ?? 999
287
-
288
- if (priorityA !== priorityB) {
289
- return priorityA - priorityB
290
- }
291
- // Within same priority, sort by ID descending (newest first)
292
- return b.id.localeCompare(a.id)
306
+ const orderA = a.frontmatter.order
307
+ const orderB = b.frontmatter.order
308
+ if (orderA && orderB) return orderA < orderB ? -1 : orderA > orderB ? 1 : 0
309
+ if (orderA && !orderB) return -1
310
+ if (!orderA && orderB) return 1
311
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
293
312
  })
294
313
  }
295
314
 
315
+ // --- Roadmap ordering ---
316
+
317
+ /**
318
+ * Get all open issues sorted by roadmap order.
319
+ */
320
+ export async function getOpenIssuesByOrder(): Promise<Issue[]> {
321
+ const allIssues = await getAllIssues()
322
+ return allIssues.filter((i) => i.frontmatter.status === 'open')
323
+ }
324
+
296
325
  /**
297
- * Create a new issue
326
+ * Compute a fractional index key for inserting relative to existing issues.
327
+ *
328
+ * @param openIssues - Open issues already sorted by order
329
+ * @param options - positioning: before/after target ID, or first/last boolean
330
+ * @param excludeId - Exclude this issue from consideration (for repositioning)
298
331
  */
332
+ export function computeOrderKey(
333
+ openIssues: Issue[],
334
+ options: {
335
+ before?: string
336
+ after?: string
337
+ first?: boolean
338
+ last?: boolean
339
+ },
340
+ excludeId?: string,
341
+ ): string {
342
+ const issues = excludeId
343
+ ? openIssues.filter((i) => i.id !== excludeId.padStart(4, '0'))
344
+ : openIssues
345
+
346
+ if (options.first) {
347
+ if (issues.length === 0) return generateKeyBetween(null, null)
348
+ const firstOrder = issues[0].frontmatter.order || null
349
+ return generateKeyBetween(null, firstOrder)
350
+ }
351
+
352
+ if (options.last) {
353
+ if (issues.length === 0) return generateKeyBetween(null, null)
354
+ const lastOrder = issues[issues.length - 1].frontmatter.order || null
355
+ return generateKeyBetween(lastOrder, null)
356
+ }
357
+
358
+ if (options.after) {
359
+ const targetId = options.after.padStart(4, '0')
360
+ const idx = issues.findIndex((i) => i.id === targetId)
361
+ if (idx === -1)
362
+ throw new Error(
363
+ `Issue #${options.after} not found among open issues. The --after target must be an open issue.`,
364
+ )
365
+ const afterOrder = issues[idx].frontmatter.order || null
366
+ const nextOrder =
367
+ idx + 1 < issues.length ? issues[idx + 1].frontmatter.order || null : null
368
+ return generateKeyBetween(afterOrder, nextOrder)
369
+ }
370
+
371
+ if (options.before) {
372
+ const targetId = options.before.padStart(4, '0')
373
+ const idx = issues.findIndex((i) => i.id === targetId)
374
+ if (idx === -1)
375
+ throw new Error(
376
+ `Issue #${options.before} not found among open issues. The --before target must be an open issue.`,
377
+ )
378
+ const beforeOrder = issues[idx].frontmatter.order || null
379
+ const prevOrder = idx > 0 ? issues[idx - 1].frontmatter.order || null : null
380
+ return generateKeyBetween(prevOrder, beforeOrder)
381
+ }
382
+
383
+ // No before/after: append at end (used for first issue or migration)
384
+ if (issues.length === 0) {
385
+ return generateKeyBetween(null, null)
386
+ }
387
+ const lastOrder = issues[issues.length - 1].frontmatter.order || null
388
+ return generateKeyBetween(lastOrder, null)
389
+ }
390
+
391
+ /**
392
+ * Generate evenly-spaced order keys for a batch of items (used during migration).
393
+ */
394
+ export function generateBatchOrderKeys(count: number): string[] {
395
+ return generateNKeysBetween(null, null, count)
396
+ }
397
+
398
+ // --- CRUD ---
399
+
299
400
  export async function createIssue(input: CreateIssueInput): Promise<Issue> {
300
401
  await ensureIssuesDir()
301
402
  if (!input.title) {
@@ -330,6 +431,7 @@ export async function createIssue(input: CreateIssueInput): Promise<Issue> {
330
431
  type,
331
432
  labels: input.labels || undefined,
332
433
  status: 'open',
434
+ order: input.order || undefined,
333
435
  created: formatDate(),
334
436
  }
335
437
 
@@ -351,9 +453,6 @@ export async function createIssue(input: CreateIssueInput): Promise<Issue> {
351
453
  }
352
454
  }
353
455
 
354
- /**
355
- * Update an existing issue
356
- */
357
456
  export async function updateIssue(
358
457
  id: string,
359
458
  input: UpdateIssueInput,
@@ -364,7 +463,6 @@ export async function updateIssue(
364
463
  throw new Error(`Issue not found: ${id}`)
365
464
  }
366
465
 
367
- // Update fields
368
466
  const updatedFrontmatter: IssueFrontmatter = {
369
467
  ...issue.frontmatter,
370
468
  ...(input.title && { title: input.title }),
@@ -376,6 +474,7 @@ export async function updateIssue(
376
474
  labels: input.labels || undefined,
377
475
  }),
378
476
  ...(input.status && { status: input.status }),
477
+ ...(input.order && { order: input.order }),
379
478
  updated: formatDate(),
380
479
  }
381
480
 
@@ -390,23 +489,14 @@ ${issue.content}`
390
489
  }
391
490
  }
392
491
 
393
- /**
394
- * Close an issue
395
- */
396
492
  export async function closeIssue(id: string): Promise<Issue> {
397
493
  return updateIssue(id, { status: 'closed' })
398
494
  }
399
495
 
400
- /**
401
- * Reopen an issue
402
- */
403
- export async function reopenIssue(id: string): Promise<Issue> {
404
- return updateIssue(id, { status: 'open' })
496
+ export async function reopenIssue(id: string, order?: string): Promise<Issue> {
497
+ return updateIssue(id, { status: 'open', order })
405
498
  }
406
499
 
407
- /**
408
- * Delete an issue permanently
409
- */
410
500
  export async function deleteIssue(id: string): Promise<void> {
411
501
  const issue = await getIssue(id)
412
502
 
@@ -417,3 +507,27 @@ export async function deleteIssue(id: string): Promise<void> {
417
507
  const { unlink } = await import('node:fs/promises')
418
508
  await unlink(join(getIssuesDir(), issue.filename))
419
509
  }
510
+
511
+ // --- Hooks ---
512
+
513
+ /**
514
+ * Read the on_close.md hook content if it exists.
515
+ */
516
+ export async function getOnCloseContent(): Promise<string | null> {
517
+ try {
518
+ const onClosePath = join(getIssyDir(), 'on_close.md')
519
+ return await readFile(onClosePath, 'utf-8')
520
+ } catch {
521
+ return null
522
+ }
523
+ }
524
+
525
+ // --- Next issue ---
526
+
527
+ /**
528
+ * Get the next issue to work on: the first open issue in roadmap order.
529
+ */
530
+ export async function getNextIssue(): Promise<Issue | null> {
531
+ const openIssues = await getOpenIssuesByOrder()
532
+ return openIssues.length > 0 ? openIssues[0] : null
533
+ }
package/src/lib/search.ts CHANGED
@@ -124,12 +124,22 @@ export function filterAndSearchIssues(
124
124
  * Sort issues by the specified sort option
125
125
  *
126
126
  * @param issues - Array of issues to sort (modified in place)
127
- * @param sortBy - Sort option: "priority", "scope", "created", "updated", or "id"
127
+ * @param sortBy - Sort option: "roadmap", "priority", "scope", "created", "updated", or "id"
128
128
  */
129
129
  function sortIssues(issues: Issue[], sortBy: string): void {
130
130
  const sortOption = sortBy.toLowerCase()
131
131
 
132
- if (sortOption === 'priority') {
132
+ if (sortOption === 'roadmap') {
133
+ issues.sort((a, b) => {
134
+ const orderA = a.frontmatter.order
135
+ const orderB = b.frontmatter.order
136
+ if (orderA && orderB)
137
+ return orderA < orderB ? -1 : orderA > orderB ? 1 : 0
138
+ if (orderA && !orderB) return -1
139
+ if (!orderA && orderB) return 1
140
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
141
+ })
142
+ } else if (sortOption === 'priority') {
133
143
  // Sort by priority (high → medium → low), then by ID (newest first)
134
144
  const priorityOrder: Record<string, number> = {
135
145
  high: 0,
@@ -188,17 +198,15 @@ function sortIssues(issues: Issue[], sortBy: string): void {
188
198
  // Sort by issue ID (newest first)
189
199
  issues.sort((a, b) => b.id.localeCompare(a.id))
190
200
  } else {
191
- // Invalid sort option - default to priority sort
192
- const priorityOrder: Record<string, number> = {
193
- high: 0,
194
- medium: 1,
195
- low: 2,
196
- }
201
+ // Invalid sort option - default to roadmap order
197
202
  issues.sort((a, b) => {
198
- const priorityA = priorityOrder[a.frontmatter.priority] ?? 999
199
- const priorityB = priorityOrder[b.frontmatter.priority] ?? 999
200
- if (priorityA !== priorityB) return priorityA - priorityB
201
- return b.id.localeCompare(a.id) // newest first within priority
203
+ const orderA = a.frontmatter.order
204
+ const orderB = b.frontmatter.order
205
+ if (orderA && orderB)
206
+ return orderA < orderB ? -1 : orderA > orderB ? 1 : 0
207
+ if (orderA && !orderB) return -1
208
+ if (!orderA && orderB) return 1
209
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
202
210
  })
203
211
  }
204
212
  }
@@ -211,12 +219,12 @@ function sortIssues(issues: Issue[], sortBy: string): void {
211
219
  * - `priority:high` / `priority:medium` / `priority:low` - filters by priority
212
220
  * - `type:bug` / `type:improvement` - filters by type
213
221
  * - `label:x` - filters by label (case-insensitive partial match)
214
- * - `sort:priority` / `sort:created` / `sort:created-asc` / `sort:updated` / `sort:id` - sorts results
222
+ * - `sort:roadmap` / `sort:priority` / `sort:created` / `sort:created-asc` / `sort:updated` / `sort:id` - sorts results
215
223
  *
216
224
  * Any remaining free text after qualifiers triggers fuzzy search across title,
217
225
  * description, labels, and content. Results are sorted by relevance when search
218
226
  * text is present. When no search text is provided, results are sorted by the
219
- * `sort:` qualifier (defaults to priority if not specified). ID prefix matching
227
+ * `sort:` qualifier (defaults to roadmap if not specified). ID prefix matching
220
228
  * is supported (e.g., "1" matches #0001).
221
229
  *
222
230
  * Invalid qualifier values are ignored (issue passes filter).
@@ -326,7 +334,7 @@ export function filterByQuery(issues: Issue[], query: string): Issue[] {
326
334
 
327
335
  // Apply sorting if no search text (search text uses relevance sorting)
328
336
  if (!parsed.searchText.trim()) {
329
- const sortBy = parsed.qualifiers.sort?.toLowerCase() || 'priority'
337
+ const sortBy = parsed.qualifiers.sort?.toLowerCase() || 'roadmap'
330
338
  sortIssues(result, sortBy)
331
339
  }
332
340
 
package/src/lib/types.ts CHANGED
@@ -10,6 +10,7 @@ export interface IssueFrontmatter {
10
10
  type: 'bug' | 'improvement'
11
11
  labels?: string
12
12
  status: 'open' | 'closed'
13
+ order?: string
13
14
  created: string
14
15
  updated?: string
15
16
  }
@@ -36,6 +37,7 @@ export interface CreateIssueInput {
36
37
  scope?: 'small' | 'medium' | 'large'
37
38
  type?: 'bug' | 'improvement'
38
39
  labels?: string
40
+ order?: string
39
41
  }
40
42
 
41
43
  export interface UpdateIssueInput {
@@ -46,4 +48,5 @@ export interface UpdateIssueInput {
46
48
  type?: 'bug' | 'improvement'
47
49
  labels?: string
48
50
  status?: 'open' | 'closed'
51
+ order?: string
49
52
  }