get-tbd 0.1.21 → 0.1.23

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.
@@ -1,4 +1,4 @@
1
- import { O as IssueSchema, a as stringifyYaml, i as sortKeys, o as stringifyYamlCompact, w as ISSUE_FIELD_ORDER } from "./yaml-utils-x_kr2IId.mjs";
1
+ import { O as IssueSchema, a as stringifyYaml, i as sortKeys, o as stringifyYamlCompact, w as ISSUE_FIELD_ORDER } from "./yaml-utils-U7l9hhkh.mjs";
2
2
  import matter from "gray-matter";
3
3
  import { parse } from "yaml";
4
4
 
@@ -182,8 +182,8 @@ function serializeIssue(issue) {
182
182
  * Package version, derived from git at build time.
183
183
  * Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.
184
184
  */
185
- const VERSION = "0.1.21";
185
+ const VERSION = "0.1.23";
186
186
 
187
187
  //#endregion
188
188
  export { insertAfterFrontmatter as a, noopLogger as c, serializeIssue as i, parseIssue as n, parseMarkdown as o, parseMarkdownWithFrontmatter as r, stripFrontmatter as s, VERSION as t };
189
- //# sourceMappingURL=src-BjMRpmMh.mjs.map
189
+ //# sourceMappingURL=src-7qUDeWJf.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"src-BjMRpmMh.mjs","names":["parseYaml"],"sources":["../src/lib/types.ts","../src/utils/markdown-utils.ts","../src/file/parser.ts","../src/index.ts"],"sourcesContent":["/**\n * TypeScript types derived from Zod schemas.\n *\n * These types are the canonical TypeScript interface for tbd entities.\n */\n\nimport type { z } from 'zod';\n\nimport type {\n IssueSchema,\n IssueStatus,\n IssueKind,\n Priority,\n Dependency,\n ConfigSchema,\n MetaSchema,\n LocalStateSchema,\n AtticEntrySchema,\n} from './schemas.js';\n\n// =============================================================================\n// Entity Types\n// =============================================================================\n\n/**\n * A tbd issue entity.\n */\nexport type Issue = z.infer<typeof IssueSchema>;\n\n/**\n * Issue status enum values.\n */\nexport type IssueStatusType = z.infer<typeof IssueStatus>;\n\n/**\n * Issue kind enum values.\n */\nexport type IssueKindType = z.infer<typeof IssueKind>;\n\n/**\n * Priority level (0-4).\n */\nexport type PriorityType = z.infer<typeof Priority>;\n\n/**\n * A dependency relationship.\n */\nexport type DependencyType = z.infer<typeof Dependency>;\n\n// =============================================================================\n// Configuration Types\n// =============================================================================\n\n/**\n * Project configuration.\n */\nexport type Config = z.infer<typeof ConfigSchema>;\n\n/**\n * Shared metadata.\n */\nexport type Meta = z.infer<typeof MetaSchema>;\n\n/**\n * Per-node local state.\n */\nexport type LocalState = z.infer<typeof LocalStateSchema>;\n\n/**\n * Attic entry for conflict losers.\n */\nexport type AtticEntry = z.infer<typeof AtticEntrySchema>;\n\n// =============================================================================\n// Input Types for Commands\n// =============================================================================\n\n/**\n * Options for creating an issue.\n */\nexport interface CreateIssueOptions {\n title: string;\n description?: string;\n kind?: IssueKindType;\n priority?: PriorityType;\n assignee?: string;\n labels?: string[];\n parent_id?: string;\n due_date?: string;\n deferred_until?: string;\n}\n\n/**\n * Options for updating an issue.\n */\nexport interface UpdateIssueOptions {\n title?: string;\n description?: string;\n notes?: string;\n kind?: IssueKindType;\n status?: IssueStatusType;\n priority?: PriorityType;\n assignee?: string | null;\n addLabels?: string[];\n removeLabels?: string[];\n parent_id?: string | null;\n due_date?: string | null;\n deferred_until?: string | null;\n}\n\n/**\n * Options for listing issues.\n */\nexport interface ListIssuesOptions {\n status?: IssueStatusType | IssueStatusType[];\n kind?: IssueKindType | IssueKindType[];\n priority?: PriorityType;\n assignee?: string;\n labels?: string[];\n parent?: string;\n all?: boolean;\n sort?: 'priority' | 'created' | 'updated';\n limit?: number;\n}\n\n/**\n * Options for searching issues.\n */\nexport interface SearchIssuesOptions {\n query: string;\n status?: IssueStatusType | IssueStatusType[];\n limit?: number;\n}\n\n// =============================================================================\n// CLI Utility Types\n// =============================================================================\n\n/**\n * A documentation section with title and slug.\n * Used by docs and design commands.\n */\nexport interface DocSection {\n title: string;\n slug: string;\n}\n\n/**\n * Logger interface for long-running operations in non-CLI layers.\n *\n * Allows core logic (file/, lib/) to report progress without depending on\n * the CLI output layer. CLI commands create an OperationLogger via\n * `OutputManager.logger(spinner)` and pass it to core functions.\n *\n * All methods are required. Use `noopLogger` when no logging is needed.\n */\nexport interface OperationLogger {\n /** Key milestones — drives the spinner in CLI context */\n progress: (message: string) => void;\n /** Operational detail (shown with --verbose or --debug) */\n info: (message: string) => void;\n /** Non-fatal warnings */\n warn: (message: string) => void;\n /** Internal state for troubleshooting (shown with --debug only) */\n debug: (message: string) => void;\n}\n\n/**\n * No-op logger for when no logging is needed.\n * Analogous to noopSpinner in the CLI layer.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-function\nconst noop = () => {};\nexport const noopLogger: OperationLogger = {\n progress: noop,\n info: noop,\n warn: noop,\n debug: noop,\n};\n","/**\n * Markdown utilities for processing markdown content.\n *\n * Uses gray-matter for parsing and centralized yaml-utils for stringify to ensure\n * proper handling of special YAML characters (colons, quotes, etc.).\n */\n\nimport matter from 'gray-matter';\n\nimport { stringifyYamlCompact } from './yaml-utils.js';\n\nexport interface ParsedMarkdown {\n /** Raw frontmatter string (without --- delimiters), or null if no frontmatter */\n frontmatter: string | null;\n /** Body content after frontmatter, with leading newlines trimmed */\n body: string;\n}\n\n/**\n * Normalize line endings to LF.\n */\nexport function normalizeLineEndings(content: string): string {\n return content.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n}\n\n/**\n * Parse markdown content into frontmatter and body.\n * Handles both LF and CRLF line endings.\n *\n * @returns Object with frontmatter (null if none) and body\n */\nexport function parseMarkdown(content: string): ParsedMarkdown {\n const normalized = normalizeLineEndings(content);\n\n if (!matter.test(normalized)) {\n return { frontmatter: null, body: content };\n }\n\n try {\n const parsed = matter(normalized);\n\n // Extract frontmatter from parsed.data by stringifying back to YAML\n // The matter property is unreliable, so we reconstruct from data\n const data = parsed.data;\n let frontmatter: string | null = null;\n\n if (data && Object.keys(data).length > 0) {\n // Use centralized yaml-utils for proper handling of special characters\n // (colons, quotes, multiline strings, etc.)\n frontmatter = stringifyYamlCompact(data).trimEnd();\n } else {\n // Empty frontmatter (just --- followed by ---)\n frontmatter = '';\n }\n\n // Body with leading newlines trimmed\n const body = parsed.content.replace(/^\\n+/, '');\n\n return { frontmatter, body };\n } catch {\n // Invalid/unclosed frontmatter - treat as no frontmatter\n return { frontmatter: null, body: content };\n }\n}\n\n/**\n * Parse YAML frontmatter from markdown content.\n * Returns the frontmatter content (without delimiters) or null if no valid frontmatter.\n * Handles both LF and CRLF line endings.\n */\nexport function parseFrontmatter(content: string): string | null {\n return parseMarkdown(content).frontmatter;\n}\n\n/**\n * Strip YAML frontmatter from markdown content.\n * Returns the body content without frontmatter, with leading newlines trimmed.\n * Handles both LF and CRLF line endings.\n */\nexport function stripFrontmatter(content: string): string {\n return parseMarkdown(content).body;\n}\n\n/**\n * Insert content after YAML frontmatter.\n * If no frontmatter exists, prepends the content.\n * Content is inserted directly after ---. Include leading newlines in toInsert if needed.\n */\nexport function insertAfterFrontmatter(content: string, toInsert: string): string {\n const { frontmatter, body } = parseMarkdown(content);\n\n if (frontmatter === null) {\n return toInsert + content;\n }\n\n const frontmatterBlock = frontmatter ? `---\\n${frontmatter}\\n---` : '---\\n---';\n return `${frontmatterBlock}\\n${toInsert}\\n\\n${body}`;\n}\n","/**\n * YAML front matter parser and serializer for issue files.\n *\n * Issues are stored as Markdown files with YAML front matter:\n * ---\n * type: is\n * id: is-a1b2c3\n * ...\n * ---\n *\n * Description body here.\n *\n * ## Notes\n *\n * Working notes here.\n *\n * See: tbd-design.md §2.1 Markdown + YAML Front Matter Format\n */\n\nimport matter from 'gray-matter';\nimport { parse as parseYaml } from 'yaml';\n\nimport { normalizeLineEndings } from '../utils/markdown-utils.js';\nimport { sortKeys, stringifyYaml } from '../utils/yaml-utils.js';\nimport type { Issue } from '../lib/types.js';\nimport { IssueSchema, ISSUE_FIELD_ORDER } from '../lib/schemas.js';\n\n/**\n * gray-matter options using the 'yaml' package as engine.\n * This preserves date strings instead of converting them to Date objects.\n */\nexport const matterOptions = {\n engines: {\n yaml: {\n parse: (str: string): object => parseYaml(str) as object,\n stringify: (obj: object): string => stringifyYaml(obj),\n },\n },\n};\n\n/**\n * Parsed issue file content.\n */\nexport interface ParsedIssueFile {\n frontmatter: Record<string, unknown>;\n description: string;\n notes: string;\n}\n\n/**\n * Parse a Markdown file with YAML front matter.\n * Uses gray-matter for consistent frontmatter parsing.\n * Handles both LF and CRLF line endings.\n */\nexport function parseMarkdownWithFrontmatter(content: string): ParsedIssueFile {\n // Normalize CRLF to LF before parsing\n const normalizedContent = normalizeLineEndings(content);\n\n // Check for valid frontmatter\n if (!matter.test(normalizedContent)) {\n throw new Error('Invalid format: missing front matter opening delimiter');\n }\n\n const parsed = matter(normalizedContent, matterOptions);\n\n // gray-matter returns empty object if no closing delimiter found\n // but the raw matter string will be empty if parsing failed\n if (parsed.matter === '' && !normalizedContent.includes('---\\n---')) {\n // Check if there's actually a closing delimiter\n const lines = normalizedContent.split('\\n');\n let hasClosing = false;\n for (let i = 1; i < lines.length; i++) {\n if (lines[i]?.trim() === '---') {\n hasClosing = true;\n break;\n }\n }\n if (!hasClosing) {\n throw new Error('Invalid format: missing front matter closing delimiter');\n }\n }\n\n const frontmatter = parsed.data as Record<string, unknown>;\n\n // Parse body - split into description and notes\n const body = parsed.content.trim();\n\n // Find notes section\n const notesMatch = /\\n## Notes\\n/i.exec(body);\n let description = body;\n let notes = '';\n\n if (notesMatch?.index !== undefined) {\n description = body.slice(0, notesMatch.index).trim();\n notes = body.slice(notesMatch.index + notesMatch[0].length).trim();\n }\n\n return { frontmatter, description, notes };\n}\n\n/**\n * Parse an issue from Markdown file content.\n */\nexport function parseIssue(content: string): Issue {\n const { frontmatter, description, notes } = parseMarkdownWithFrontmatter(content);\n\n // Merge body content into frontmatter\n const data = {\n ...frontmatter,\n description: description || undefined,\n notes: notes || undefined,\n };\n\n // Validate and parse with Zod\n return IssueSchema.parse(data);\n}\n\n/**\n * Serialize an issue to Markdown file content.\n * Uses canonical serialization for deterministic output.\n */\nexport function serializeIssue(issue: Issue): string {\n // Extract body fields\n const { description, notes, ...metadata } = issue;\n\n // Sort keys using canonical field order (not alphabetical)\n const sortedMetadata = sortKeys(metadata, ISSUE_FIELD_ORDER);\n\n // Serialize YAML with compact output for frontmatter.\n // sortMapEntries: false preserves our manual ordering.\n const yaml = stringifyYaml(sortedMetadata, {\n lineWidth: 0,\n nullStr: 'null',\n sortMapEntries: false,\n });\n\n // Build the file content\n // Note: No blank line between closing --- and body content\n const parts = ['---', yaml.trim(), '---'];\n\n if (description) {\n parts.push(description.trim());\n }\n\n if (notes) {\n parts.push('');\n parts.push('## Notes');\n parts.push('');\n parts.push(notes.trim());\n }\n\n // Single newline at end\n return parts.join('\\n') + '\\n';\n}\n","/**\n * tbd: Git-native issue tracking for AI agents and humans\n *\n * This is the library entry point. All exports here should be node-free\n * to support browser/edge runtime usage. CLI-specific code is in ./cli/.\n */\n\n// Version injected at build time\ndeclare const __TBD_VERSION__: string;\n\n/**\n * Package version, derived from git at build time.\n * Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.\n */\nexport const VERSION: string =\n typeof __TBD_VERSION__ !== 'undefined' ? __TBD_VERSION__ : 'development';\n\n// Re-export schemas for library consumers\nexport * from './lib/schemas.js';\nexport * from './lib/types.js';\n\n// Re-export core operations (these should be node-free)\nexport { parseIssue, serializeIssue } from './file/parser.js';\n"],"mappings":"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"}
1
+ {"version":3,"file":"src-7qUDeWJf.mjs","names":["parseYaml"],"sources":["../src/lib/types.ts","../src/utils/markdown-utils.ts","../src/file/parser.ts","../src/index.ts"],"sourcesContent":["/**\n * TypeScript types derived from Zod schemas.\n *\n * These types are the canonical TypeScript interface for tbd entities.\n */\n\nimport type { z } from 'zod';\n\nimport type {\n IssueSchema,\n IssueStatus,\n IssueKind,\n Priority,\n Dependency,\n ConfigSchema,\n MetaSchema,\n LocalStateSchema,\n AtticEntrySchema,\n} from './schemas.js';\n\n// =============================================================================\n// Entity Types\n// =============================================================================\n\n/**\n * A tbd issue entity.\n */\nexport type Issue = z.infer<typeof IssueSchema>;\n\n/**\n * Issue status enum values.\n */\nexport type IssueStatusType = z.infer<typeof IssueStatus>;\n\n/**\n * Issue kind enum values.\n */\nexport type IssueKindType = z.infer<typeof IssueKind>;\n\n/**\n * Priority level (0-4).\n */\nexport type PriorityType = z.infer<typeof Priority>;\n\n/**\n * A dependency relationship.\n */\nexport type DependencyType = z.infer<typeof Dependency>;\n\n// =============================================================================\n// Configuration Types\n// =============================================================================\n\n/**\n * Project configuration.\n */\nexport type Config = z.infer<typeof ConfigSchema>;\n\n/**\n * Shared metadata.\n */\nexport type Meta = z.infer<typeof MetaSchema>;\n\n/**\n * Per-node local state.\n */\nexport type LocalState = z.infer<typeof LocalStateSchema>;\n\n/**\n * Attic entry for conflict losers.\n */\nexport type AtticEntry = z.infer<typeof AtticEntrySchema>;\n\n// =============================================================================\n// Input Types for Commands\n// =============================================================================\n\n/**\n * Options for creating an issue.\n */\nexport interface CreateIssueOptions {\n title: string;\n description?: string;\n kind?: IssueKindType;\n priority?: PriorityType;\n assignee?: string;\n labels?: string[];\n parent_id?: string;\n due_date?: string;\n deferred_until?: string;\n}\n\n/**\n * Options for updating an issue.\n */\nexport interface UpdateIssueOptions {\n title?: string;\n description?: string;\n notes?: string;\n kind?: IssueKindType;\n status?: IssueStatusType;\n priority?: PriorityType;\n assignee?: string | null;\n addLabels?: string[];\n removeLabels?: string[];\n parent_id?: string | null;\n due_date?: string | null;\n deferred_until?: string | null;\n}\n\n/**\n * Options for listing issues.\n */\nexport interface ListIssuesOptions {\n status?: IssueStatusType | IssueStatusType[];\n kind?: IssueKindType | IssueKindType[];\n priority?: PriorityType;\n assignee?: string;\n labels?: string[];\n parent?: string;\n all?: boolean;\n sort?: 'priority' | 'created' | 'updated';\n limit?: number;\n}\n\n/**\n * Options for searching issues.\n */\nexport interface SearchIssuesOptions {\n query: string;\n status?: IssueStatusType | IssueStatusType[];\n limit?: number;\n}\n\n// =============================================================================\n// CLI Utility Types\n// =============================================================================\n\n/**\n * A documentation section with title and slug.\n * Used by docs and design commands.\n */\nexport interface DocSection {\n title: string;\n slug: string;\n}\n\n/**\n * Logger interface for long-running operations in non-CLI layers.\n *\n * Allows core logic (file/, lib/) to report progress without depending on\n * the CLI output layer. CLI commands create an OperationLogger via\n * `OutputManager.logger(spinner)` and pass it to core functions.\n *\n * All methods are required. Use `noopLogger` when no logging is needed.\n */\nexport interface OperationLogger {\n /** Key milestones — drives the spinner in CLI context */\n progress: (message: string) => void;\n /** Operational detail (shown with --verbose or --debug) */\n info: (message: string) => void;\n /** Non-fatal warnings */\n warn: (message: string) => void;\n /** Internal state for troubleshooting (shown with --debug only) */\n debug: (message: string) => void;\n}\n\n/**\n * No-op logger for when no logging is needed.\n * Analogous to noopSpinner in the CLI layer.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-function\nconst noop = () => {};\nexport const noopLogger: OperationLogger = {\n progress: noop,\n info: noop,\n warn: noop,\n debug: noop,\n};\n","/**\n * Markdown utilities for processing markdown content.\n *\n * Uses gray-matter for parsing and centralized yaml-utils for stringify to ensure\n * proper handling of special YAML characters (colons, quotes, etc.).\n */\n\nimport matter from 'gray-matter';\n\nimport { stringifyYamlCompact } from './yaml-utils.js';\n\nexport interface ParsedMarkdown {\n /** Raw frontmatter string (without --- delimiters), or null if no frontmatter */\n frontmatter: string | null;\n /** Body content after frontmatter, with leading newlines trimmed */\n body: string;\n}\n\n/**\n * Normalize line endings to LF.\n */\nexport function normalizeLineEndings(content: string): string {\n return content.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n}\n\n/**\n * Parse markdown content into frontmatter and body.\n * Handles both LF and CRLF line endings.\n *\n * @returns Object with frontmatter (null if none) and body\n */\nexport function parseMarkdown(content: string): ParsedMarkdown {\n const normalized = normalizeLineEndings(content);\n\n if (!matter.test(normalized)) {\n return { frontmatter: null, body: content };\n }\n\n try {\n const parsed = matter(normalized);\n\n // Extract frontmatter from parsed.data by stringifying back to YAML\n // The matter property is unreliable, so we reconstruct from data\n const data = parsed.data;\n let frontmatter: string | null = null;\n\n if (data && Object.keys(data).length > 0) {\n // Use centralized yaml-utils for proper handling of special characters\n // (colons, quotes, multiline strings, etc.)\n frontmatter = stringifyYamlCompact(data).trimEnd();\n } else {\n // Empty frontmatter (just --- followed by ---)\n frontmatter = '';\n }\n\n // Body with leading newlines trimmed\n const body = parsed.content.replace(/^\\n+/, '');\n\n return { frontmatter, body };\n } catch {\n // Invalid/unclosed frontmatter - treat as no frontmatter\n return { frontmatter: null, body: content };\n }\n}\n\n/**\n * Parse YAML frontmatter from markdown content.\n * Returns the frontmatter content (without delimiters) or null if no valid frontmatter.\n * Handles both LF and CRLF line endings.\n */\nexport function parseFrontmatter(content: string): string | null {\n return parseMarkdown(content).frontmatter;\n}\n\n/**\n * Strip YAML frontmatter from markdown content.\n * Returns the body content without frontmatter, with leading newlines trimmed.\n * Handles both LF and CRLF line endings.\n */\nexport function stripFrontmatter(content: string): string {\n return parseMarkdown(content).body;\n}\n\n/**\n * Insert content after YAML frontmatter.\n * If no frontmatter exists, prepends the content.\n * Content is inserted directly after ---. Include leading newlines in toInsert if needed.\n */\nexport function insertAfterFrontmatter(content: string, toInsert: string): string {\n const { frontmatter, body } = parseMarkdown(content);\n\n if (frontmatter === null) {\n return toInsert + content;\n }\n\n const frontmatterBlock = frontmatter ? `---\\n${frontmatter}\\n---` : '---\\n---';\n return `${frontmatterBlock}\\n${toInsert}\\n\\n${body}`;\n}\n","/**\n * YAML front matter parser and serializer for issue files.\n *\n * Issues are stored as Markdown files with YAML front matter:\n * ---\n * type: is\n * id: is-a1b2c3\n * ...\n * ---\n *\n * Description body here.\n *\n * ## Notes\n *\n * Working notes here.\n *\n * See: tbd-design.md §2.1 Markdown + YAML Front Matter Format\n */\n\nimport matter from 'gray-matter';\nimport { parse as parseYaml } from 'yaml';\n\nimport { normalizeLineEndings } from '../utils/markdown-utils.js';\nimport { sortKeys, stringifyYaml } from '../utils/yaml-utils.js';\nimport type { Issue } from '../lib/types.js';\nimport { IssueSchema, ISSUE_FIELD_ORDER } from '../lib/schemas.js';\n\n/**\n * gray-matter options using the 'yaml' package as engine.\n * This preserves date strings instead of converting them to Date objects.\n */\nexport const matterOptions = {\n engines: {\n yaml: {\n parse: (str: string): object => parseYaml(str) as object,\n stringify: (obj: object): string => stringifyYaml(obj),\n },\n },\n};\n\n/**\n * Parsed issue file content.\n */\nexport interface ParsedIssueFile {\n frontmatter: Record<string, unknown>;\n description: string;\n notes: string;\n}\n\n/**\n * Parse a Markdown file with YAML front matter.\n * Uses gray-matter for consistent frontmatter parsing.\n * Handles both LF and CRLF line endings.\n */\nexport function parseMarkdownWithFrontmatter(content: string): ParsedIssueFile {\n // Normalize CRLF to LF before parsing\n const normalizedContent = normalizeLineEndings(content);\n\n // Check for valid frontmatter\n if (!matter.test(normalizedContent)) {\n throw new Error('Invalid format: missing front matter opening delimiter');\n }\n\n const parsed = matter(normalizedContent, matterOptions);\n\n // gray-matter returns empty object if no closing delimiter found\n // but the raw matter string will be empty if parsing failed\n if (parsed.matter === '' && !normalizedContent.includes('---\\n---')) {\n // Check if there's actually a closing delimiter\n const lines = normalizedContent.split('\\n');\n let hasClosing = false;\n for (let i = 1; i < lines.length; i++) {\n if (lines[i]?.trim() === '---') {\n hasClosing = true;\n break;\n }\n }\n if (!hasClosing) {\n throw new Error('Invalid format: missing front matter closing delimiter');\n }\n }\n\n const frontmatter = parsed.data as Record<string, unknown>;\n\n // Parse body - split into description and notes\n const body = parsed.content.trim();\n\n // Find notes section\n const notesMatch = /\\n## Notes\\n/i.exec(body);\n let description = body;\n let notes = '';\n\n if (notesMatch?.index !== undefined) {\n description = body.slice(0, notesMatch.index).trim();\n notes = body.slice(notesMatch.index + notesMatch[0].length).trim();\n }\n\n return { frontmatter, description, notes };\n}\n\n/**\n * Parse an issue from Markdown file content.\n */\nexport function parseIssue(content: string): Issue {\n const { frontmatter, description, notes } = parseMarkdownWithFrontmatter(content);\n\n // Merge body content into frontmatter\n const data = {\n ...frontmatter,\n description: description || undefined,\n notes: notes || undefined,\n };\n\n // Validate and parse with Zod\n return IssueSchema.parse(data);\n}\n\n/**\n * Serialize an issue to Markdown file content.\n * Uses canonical serialization for deterministic output.\n */\nexport function serializeIssue(issue: Issue): string {\n // Extract body fields\n const { description, notes, ...metadata } = issue;\n\n // Sort keys using canonical field order (not alphabetical)\n const sortedMetadata = sortKeys(metadata, ISSUE_FIELD_ORDER);\n\n // Serialize YAML with compact output for frontmatter.\n // sortMapEntries: false preserves our manual ordering.\n const yaml = stringifyYaml(sortedMetadata, {\n lineWidth: 0,\n nullStr: 'null',\n sortMapEntries: false,\n });\n\n // Build the file content\n // Note: No blank line between closing --- and body content\n const parts = ['---', yaml.trim(), '---'];\n\n if (description) {\n parts.push(description.trim());\n }\n\n if (notes) {\n parts.push('');\n parts.push('## Notes');\n parts.push('');\n parts.push(notes.trim());\n }\n\n // Single newline at end\n return parts.join('\\n') + '\\n';\n}\n","/**\n * tbd: Git-native issue tracking for AI agents and humans\n *\n * This is the library entry point. All exports here should be node-free\n * to support browser/edge runtime usage. CLI-specific code is in ./cli/.\n */\n\n// Version injected at build time\ndeclare const __TBD_VERSION__: string;\n\n/**\n * Package version, derived from git at build time.\n * Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.\n */\nexport const VERSION: string =\n typeof __TBD_VERSION__ !== 'undefined' ? __TBD_VERSION__ : 'development';\n\n// Re-export schemas for library consumers\nexport * from './lib/schemas.js';\nexport * from './lib/types.js';\n\n// Re-export core operations (these should be node-free)\nexport { parseIssue, serializeIssue } from './file/parser.js';\n"],"mappings":"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"}
package/dist/tbd CHANGED
@@ -6734,6 +6734,12 @@ const Dependency = objectType({
6734
6734
  *
6735
6735
  * Note: Fields use .nullable() in addition to .optional() because
6736
6736
  * YAML parses `field: null` as JavaScript null, not undefined.
6737
+ *
6738
+ * Design note: We could add the short ID to this schema. We didn't originally
6739
+ * because it's one more field to maintain consistency around across files.
6740
+ * Having it here might make recovery of lost ID mappings far easier, but for
6741
+ * now we have more reliable management of the mappings file (ids.yml) and
6742
+ * consider it authoritative. See IdMappingYamlSchema (§2.6.8).
6737
6743
  */
6738
6744
  const IssueSchema = BaseEntity.extend({
6739
6745
  type: literalType("is"),
@@ -14027,7 +14033,7 @@ function serializeIssue(issue) {
14027
14033
  * Package version, derived from git at build time.
14028
14034
  * Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.
14029
14035
  */
14030
- const VERSION$1 = "0.1.21";
14036
+ const VERSION$1 = "0.1.23";
14031
14037
 
14032
14038
  //#endregion
14033
14039
  //#region src/cli/lib/version.ts
@@ -96557,15 +96563,12 @@ function sanitizeTab(tab, fallbackTab) {
96557
96563
  */
96558
96564
  function getCommandContext(command) {
96559
96565
  const opts = command.optsWithGlobals();
96560
- const isCI = Boolean(process.env.CI);
96561
96566
  return {
96562
96567
  dryRun: opts.dryRun ?? false,
96563
96568
  verbose: opts.verbose ?? false,
96564
96569
  quiet: opts.quiet ?? false,
96565
96570
  json: opts.json ?? false,
96566
96571
  color: opts.color ?? "auto",
96567
- nonInteractive: opts.nonInteractive ?? (!process.stdin.isTTY || isCI),
96568
- yes: opts.yes ?? false,
96569
96572
  sync: opts.sync !== false,
96570
96573
  debug: opts.debug ?? false
96571
96574
  };
@@ -97854,6 +97857,20 @@ function isCompatibleFormat(format) {
97854
97857
  *
97855
97858
  * See: tbd-design.md §2.2.2 Config File
97856
97859
  */
97860
+ var config_exports = /* @__PURE__ */ __exportAll({
97861
+ IncompatibleFormatError: () => IncompatibleFormatError,
97862
+ findTbdRoot: () => findTbdRoot,
97863
+ hasSeenWelcome: () => hasSeenWelcome,
97864
+ initConfig: () => initConfig,
97865
+ isInitialized: () => isInitialized,
97866
+ markWelcomeSeen: () => markWelcomeSeen,
97867
+ readConfig: () => readConfig,
97868
+ readConfigWithMigration: () => readConfigWithMigration,
97869
+ readLocalState: () => readLocalState,
97870
+ updateLocalState: () => updateLocalState,
97871
+ writeConfig: () => writeConfig,
97872
+ writeLocalState: () => writeLocalState
97873
+ });
97857
97874
  /**
97858
97875
  * Error thrown when the config format version is from a newer tbd version.
97859
97876
  * This prevents older tbd versions from silently stripping new config fields.
@@ -99852,11 +99869,13 @@ function naturalSort(arr) {
99852
99869
  var id_mapping_exports = /* @__PURE__ */ __exportAll({
99853
99870
  addIdMapping: () => addIdMapping,
99854
99871
  calculateOptimalLength: () => calculateOptimalLength,
99872
+ createShortIdMapping: () => createShortIdMapping,
99855
99873
  generateUniqueShortId: () => generateUniqueShortId,
99856
99874
  hasShortId: () => hasShortId,
99857
99875
  loadIdMapping: () => loadIdMapping,
99858
99876
  mergeIdMappings: () => mergeIdMappings,
99859
99877
  parseIdMappingFromYaml: () => parseIdMappingFromYaml,
99878
+ reconcileMappings: () => reconcileMappings,
99860
99879
  resolveToInternalId: () => resolveToInternalId,
99861
99880
  saveIdMapping: () => saveIdMapping
99862
99881
  });
@@ -99955,6 +99974,22 @@ function hasShortId(mapping, shortId) {
99955
99974
  return mapping.shortToUlid.has(shortId);
99956
99975
  }
99957
99976
  /**
99977
+ * Create a short ID mapping for a new internal ID.
99978
+ * Generates a unique short ID and registers it in the mapping.
99979
+ *
99980
+ * @param internalId - The internal ID (is-{ulid})
99981
+ * @param mapping - The ID mapping to update
99982
+ * @returns The generated short ID
99983
+ */
99984
+ function createShortIdMapping(internalId, mapping) {
99985
+ const ulid = extractUlidFromInternalId(internalId);
99986
+ const existing = mapping.ulidToShort.get(ulid);
99987
+ if (existing) return existing;
99988
+ const shortId = generateUniqueShortId(mapping);
99989
+ addIdMapping(mapping, ulid, shortId);
99990
+ return shortId;
99991
+ }
99992
+ /**
99958
99993
  * Resolve any ID input to an internal ID ({prefix}-{ulid}).
99959
99994
  *
99960
99995
  * Handles:
@@ -100001,6 +100036,44 @@ function parseIdMappingFromYaml(content) {
100001
100036
  };
100002
100037
  }
100003
100038
  /**
100039
+ * Ensure all given internal IDs have short ID mappings.
100040
+ * Creates missing mappings for any IDs without entries.
100041
+ *
100042
+ * This repairs state after git merges that may add issue files
100043
+ * without corresponding mapping entries (e.g., when outbox issues
100044
+ * are merged from a feature branch but ids.yml doesn't include them).
100045
+ *
100046
+ * When a `historicalMapping` is provided, the function will try to recover
100047
+ * the original short ID from that mapping before generating a new random one.
100048
+ * This preserves ID stability so that existing references (in docs, PRs,
100049
+ * conversations) remain valid.
100050
+ *
100051
+ * @param internalIds - Array of internal IDs (is-{ulid}) to reconcile
100052
+ * @param mapping - The ID mapping to update (mutated in-place)
100053
+ * @param historicalMapping - Optional mapping from prior state (e.g., git history) to recover original short IDs
100054
+ * @returns Object with `created` (IDs that got new random short IDs) and `recovered` (IDs restored from history)
100055
+ */
100056
+ function reconcileMappings(internalIds, mapping, historicalMapping) {
100057
+ const created = [];
100058
+ const recovered = [];
100059
+ for (const id of internalIds) {
100060
+ const ulid = extractUlidFromInternalId(id);
100061
+ if (mapping.ulidToShort.has(ulid)) continue;
100062
+ const historicalShortId = historicalMapping?.ulidToShort.get(ulid);
100063
+ if (historicalShortId && !mapping.shortToUlid.has(historicalShortId)) {
100064
+ addIdMapping(mapping, ulid, historicalShortId);
100065
+ recovered.push(id);
100066
+ } else {
100067
+ createShortIdMapping(id, mapping);
100068
+ created.push(id);
100069
+ }
100070
+ }
100071
+ return {
100072
+ created,
100073
+ recovered
100074
+ };
100075
+ }
100076
+ /**
100004
100077
  * Merge two ID mappings by combining all entries from both.
100005
100078
  * ID mappings are always additive (new IDs are only added, never removed),
100006
100079
  * so merging simply unions all key-value pairs.
@@ -102768,6 +102841,9 @@ async function importFromWorkspace(tbdRoot, dataSyncDir, options) {
102768
102841
  const sourceMapping = await loadIdMapping(sourceDir);
102769
102842
  const targetMapping = await loadIdMapping(dataSyncDir);
102770
102843
  for (const [shortId, ulid] of sourceMapping.shortToUlid) if (!targetMapping.shortToUlid.has(shortId)) addIdMapping(targetMapping, ulid, shortId);
102844
+ const reconcileResult = reconcileMappings(sourceIssues.map((i) => i.id), targetMapping, sourceMapping);
102845
+ if (reconcileResult.recovered.length > 0) log.info(`Recovered ${reconcileResult.recovered.length} ID mapping(s) from workspace`);
102846
+ if (reconcileResult.created.length > 0) log.info(`Created ${reconcileResult.created.length} new ID mapping(s) for imported issues`);
102771
102847
  await saveIdMapping(dataSyncDir, targetMapping);
102772
102848
  let cleared = false;
102773
102849
  if (shouldClear && imported > 0) {
@@ -102922,8 +102998,8 @@ var SyncHandler = class extends BaseCommand {
102922
102998
  else if (options.push) await this.pushChanges(syncBranch, remote);
102923
102999
  else await this.fullSync(syncBranch, remote, {
102924
103000
  force: options.force,
102925
- noAutoSave: options.noAutoSave,
102926
- noOutbox: options.noOutbox
103001
+ autoSave: options.autoSave,
103002
+ outbox: options.outbox
102927
103003
  });
102928
103004
  }
102929
103005
  /**
@@ -103226,6 +103302,24 @@ var SyncHandler = class extends BaseCommand {
103226
103302
  await git("-C", worktreePath, "merge", `${remote}/${syncBranch}`, "-m", "tbd sync: merge remote changes");
103227
103303
  this.output.debug(`Merged ${behindCommits} commit(s) from remote`);
103228
103304
  if (headBeforeMerge) await this.showGitLogDebug("Commits received", `${headBeforeMerge}..${syncBranch}`);
103305
+ const postMergeIssues = await listIssues(this.dataSyncDir);
103306
+ const postMergeMapping = await loadIdMapping(this.dataSyncDir);
103307
+ let historicalMapping;
103308
+ try {
103309
+ const remoteIdsContent = await git("show", `${remote}/${syncBranch}:${DATA_SYNC_DIR}/mappings/ids.yml`);
103310
+ if (remoteIdsContent) historicalMapping = parseIdMappingFromYaml(remoteIdsContent);
103311
+ } catch {}
103312
+ const reconcileResult = reconcileMappings(postMergeIssues.map((i) => i.id), postMergeMapping, historicalMapping);
103313
+ const totalReconciled = reconcileResult.created.length + reconcileResult.recovered.length;
103314
+ if (totalReconciled > 0) {
103315
+ await saveIdMapping(this.dataSyncDir, postMergeMapping);
103316
+ await git("-C", worktreePath, "add", "-A");
103317
+ try {
103318
+ await git("-C", worktreePath, "commit", "--no-verify", "-m", `tbd sync: reconcile ${totalReconciled} missing ID mapping(s)`);
103319
+ } catch {}
103320
+ if (reconcileResult.recovered.length > 0) this.output.debug(`Recovered ${reconcileResult.recovered.length} ID mapping(s) from history`);
103321
+ if (reconcileResult.created.length > 0) this.output.debug(`Created ${reconcileResult.created.length} new ID mapping(s) (no history available)`);
103322
+ }
103229
103323
  } catch {
103230
103324
  this.output.info(`Merge conflict, attempting file-level resolution`);
103231
103325
  const localIssues = await listIssues(this.dataSyncDir);
@@ -103238,18 +103332,29 @@ var SyncHandler = class extends BaseCommand {
103238
103332
  } catch {
103239
103333
  this.output.debug(`Issue ${localIssue.id} not on remote, keeping local`);
103240
103334
  }
103335
+ let conflictRemoteMapping;
103241
103336
  try {
103242
103337
  const remoteIdsContent = await git("show", `${remote}/${syncBranch}:${DATA_SYNC_DIR}/mappings/ids.yml`);
103243
103338
  if (remoteIdsContent) {
103339
+ conflictRemoteMapping = parseIdMappingFromYaml(remoteIdsContent);
103244
103340
  const localMapping = await loadIdMapping(this.dataSyncDir);
103245
- const remoteMapping = parseIdMappingFromYaml(remoteIdsContent);
103246
- const mergedMapping = mergeIdMappings(localMapping, remoteMapping);
103341
+ const mergedMapping = mergeIdMappings(localMapping, conflictRemoteMapping);
103247
103342
  await saveIdMapping(this.dataSyncDir, mergedMapping);
103248
- this.output.debug(`Merged ID mappings: ${localMapping.shortToUlid.size} local + ${remoteMapping.shortToUlid.size} remote = ${mergedMapping.shortToUlid.size} total`);
103343
+ this.output.debug(`Merged ID mappings: ${localMapping.shortToUlid.size} local + ${conflictRemoteMapping.shortToUlid.size} remote = ${mergedMapping.shortToUlid.size} total`);
103249
103344
  }
103250
103345
  } catch (error) {
103251
103346
  this.output.debug(`Could not merge ids.yml: ${error.message}`);
103252
103347
  }
103348
+ {
103349
+ const allIssues = await listIssues(this.dataSyncDir);
103350
+ const currentMapping = await loadIdMapping(this.dataSyncDir);
103351
+ const reconcileResult = reconcileMappings(allIssues.map((i) => i.id), currentMapping, conflictRemoteMapping);
103352
+ if (reconcileResult.created.length + reconcileResult.recovered.length > 0) {
103353
+ await saveIdMapping(this.dataSyncDir, currentMapping);
103354
+ if (reconcileResult.recovered.length > 0) this.output.debug(`Recovered ${reconcileResult.recovered.length} ID mapping(s) from remote`);
103355
+ if (reconcileResult.created.length > 0) this.output.debug(`Created ${reconcileResult.created.length} new ID mapping(s) after conflict resolution`);
103356
+ }
103357
+ }
103253
103358
  await git("-C", worktreePath, "add", "-A");
103254
103359
  const conflictCheck = await git("-C", worktreePath, "diff", "--cached", "-S<<<<<<< ", "--name-only");
103255
103360
  if (conflictCheck.trim()) {
@@ -103313,7 +103418,7 @@ var SyncHandler = class extends BaseCommand {
103313
103418
  this.output.error(`Push failed: ${displayError}`);
103314
103419
  console.log(` ${aheadCommits} commit(s) not pushed to remote.`);
103315
103420
  });
103316
- if (errorType === "permanent" && !options.noAutoSave) await this.handlePermanentFailure();
103421
+ if (errorType === "permanent" && options.autoSave !== false) await this.handlePermanentFailure();
103317
103422
  else if (!this.ctx.json) if (errorType === "transient") {
103318
103423
  console.log("");
103319
103424
  console.log(" This appears to be a temporary issue. Options:");
@@ -103328,7 +103433,7 @@ var SyncHandler = class extends BaseCommand {
103328
103433
  }
103329
103434
  return;
103330
103435
  }
103331
- if (!options.noOutbox) await this.maybeImportOutbox(syncBranch, remote);
103436
+ if (options.outbox !== false) await this.maybeImportOutbox(syncBranch, remote);
103332
103437
  this.output.data({
103333
103438
  summary,
103334
103439
  conflicts: conflicts.length
@@ -104351,6 +104456,7 @@ var DoctorHandler = class extends BaseCommand {
104351
104456
  healthChecks.push(await this.checkIdMappingDuplicates(options.fix));
104352
104457
  healthChecks.push(await this.checkTempFiles(options.fix));
104353
104458
  healthChecks.push(this.checkIssueValidity(this.issues));
104459
+ healthChecks.push(await this.checkMissingMappings(options.fix));
104354
104460
  healthChecks.push(await this.checkWorktree(options.fix));
104355
104461
  healthChecks.push(await this.checkDataLocation(options.fix));
104356
104462
  healthChecks.push(await this.checkLocalSyncBranch());
@@ -104694,6 +104800,63 @@ var DoctorHandler = class extends BaseCommand {
104694
104800
  suggestion: "Manually fix or delete invalid issue files"
104695
104801
  };
104696
104802
  }
104803
+ /**
104804
+ * Check for issues that have no short ID mapping in ids.yml.
104805
+ *
104806
+ * This can happen when a git merge brings in issue files (e.g., from
104807
+ * a feature branch with outbox issues) without the corresponding
104808
+ * ids.yml entries. Without a mapping, any command that tries to
104809
+ * display the issue ID will crash.
104810
+ *
104811
+ * With --fix, creates missing mappings automatically.
104812
+ */
104813
+ async checkMissingMappings(fix) {
104814
+ if (this.issues.length === 0) return {
104815
+ name: "ID mapping coverage",
104816
+ status: "ok"
104817
+ };
104818
+ const { loadIdMapping, saveIdMapping, reconcileMappings } = await Promise.resolve().then(() => id_mapping_exports);
104819
+ const mapping = await loadIdMapping(this.dataSyncDir);
104820
+ const missingIds = [];
104821
+ for (const issue of this.issues) {
104822
+ const ulid = extractUlidFromInternalId(issue.id);
104823
+ if (!mapping.ulidToShort.has(ulid)) missingIds.push(issue.id);
104824
+ }
104825
+ if (missingIds.length === 0) return {
104826
+ name: "ID mapping coverage",
104827
+ status: "ok"
104828
+ };
104829
+ if (fix && !this.checkDryRun("Create missing ID mappings")) {
104830
+ const { parseIdMappingFromYaml } = await Promise.resolve().then(() => id_mapping_exports);
104831
+ let historicalMapping;
104832
+ try {
104833
+ const syncBranch = (await Promise.resolve().then(() => config_exports).then((m) => m.readConfig(this.cwd))).sync.branch;
104834
+ const priorContent = await git("log", "-1", "--format=%H", syncBranch, "--", `${DATA_SYNC_DIR}/mappings/ids.yml`);
104835
+ if (priorContent.trim()) {
104836
+ const idsContent = await git("show", `${priorContent.trim()}:${DATA_SYNC_DIR}/mappings/ids.yml`);
104837
+ if (idsContent) historicalMapping = parseIdMappingFromYaml(idsContent);
104838
+ }
104839
+ } catch {}
104840
+ const result = reconcileMappings(missingIds, mapping, historicalMapping);
104841
+ await saveIdMapping(this.dataSyncDir, mapping);
104842
+ const parts = [];
104843
+ if (result.recovered.length > 0) parts.push(`recovered ${result.recovered.length} from git history`);
104844
+ if (result.created.length > 0) parts.push(`created ${result.created.length} new`);
104845
+ return {
104846
+ name: "ID mapping coverage",
104847
+ status: "ok",
104848
+ message: parts.join(", ")
104849
+ };
104850
+ }
104851
+ return {
104852
+ name: "ID mapping coverage",
104853
+ status: "error",
104854
+ message: `${missingIds.length} issue(s) without short ID mapping`,
104855
+ details: missingIds.map((id) => `${id} (no short ID)`),
104856
+ fixable: true,
104857
+ suggestion: "Run: tbd doctor --fix to create missing mappings"
104858
+ };
104859
+ }
104697
104860
  async checkClaudeSkill() {
104698
104861
  const claudePaths = getClaudePaths(this.cwd);
104699
104862
  try {
@@ -108468,6 +108631,9 @@ var SetupDefaultHandler = class extends BaseCommand {
108468
108631
  ]);
108469
108632
  if (tbdGitignoreResult.created) console.log(` ${colors.success("✓")} Created .tbd/.gitignore`);
108470
108633
  else if (tbdGitignoreResult.added.length > 0) console.log(` ${colors.success("✓")} Updated .tbd/.gitignore with new patterns`);
108634
+ const gitattributesResult = await ensureGitignorePatterns(join(projectDir, TBD_DIR, ".gitattributes"), ["# Protect ID mappings from merge deletion (always keep all rows)", "**/mappings/ids.yml merge=union"]);
108635
+ if (gitattributesResult.created) console.log(` ${colors.success("✓")} Created .tbd/.gitattributes (merge protection)`);
108636
+ else if (gitattributesResult.added.length > 0) console.log(` ${colors.success("✓")} Updated .tbd/.gitattributes (merge protection)`);
108471
108637
  console.log("Checking integrations...");
108472
108638
  await new SetupAutoHandler(this.cmd).run(projectDir);
108473
108639
  console.log("");
@@ -108598,6 +108764,9 @@ Example:
108598
108764
  ]);
108599
108765
  if (tbdGitignoreResult.created) console.log(` ${colors.success("✓")} Created .tbd/.gitignore`);
108600
108766
  else if (tbdGitignoreResult.added.length > 0) console.log(` ${colors.success("✓")} Updated .tbd/.gitignore`);
108767
+ const gitattributesResult = await ensureGitignorePatterns(join(cwd, TBD_DIR, ".gitattributes"), ["# Protect ID mappings from merge deletion (always keep all rows)", "**/mappings/ids.yml merge=union"]);
108768
+ if (gitattributesResult.created) console.log(` ${colors.success("✓")} Created .tbd/.gitattributes (merge protection)`);
108769
+ else if (gitattributesResult.added.length > 0) console.log(` ${colors.success("✓")} Updated .tbd/.gitattributes (merge protection)`);
108601
108770
  try {
108602
108771
  await initWorktree(cwd);
108603
108772
  const health = await checkWorktreeHealth(cwd);
@@ -108970,7 +109139,7 @@ const workspaceCommand = new Command("workspace").description("Manage workspaces
108970
109139
  function createProgram() {
108971
109140
  const program = new Command().name("tbd").description("Git-native issue tracking for AI agents and humans").version(VERSION, "--version", "Show version number").helpOption("--help", "Display help for command").showHelpAfterError("(add --help for additional information)");
108972
109141
  configureColoredHelp(program);
108973
- program.option("--dry-run", "Show what would be done without making changes").option("--verbose", "Enable verbose output").option("--quiet", "Suppress non-essential output").option("--json", "Output as JSON").option("--color <when>", "Colorize output: auto, always, never", "auto").option("--non-interactive", "Disable all prompts, fail if input required").option("--yes", "Assume yes to confirmation prompts").option("--no-sync", "Skip automatic sync after write operations").option("--debug", "Show internal IDs alongside public IDs for debugging");
109142
+ program.option("--dry-run", "Show what would be done without making changes").option("--verbose", "Enable verbose output").option("--quiet", "Suppress non-essential output").option("--json", "Output as JSON").option("--color <when>", "Colorize output: auto, always, never", "auto").option("--no-sync", "Skip automatic sync after write operations").option("--debug", "Show internal IDs alongside public IDs for debugging");
108974
109143
  program.commandsGroup("Documentation:");
108975
109144
  program.addCommand(readmeCommand);
108976
109145
  program.addCommand(primeCommand);
@@ -105,6 +105,12 @@ const Dependency = z.object({
105
105
  *
106
106
  * Note: Fields use .nullable() in addition to .optional() because
107
107
  * YAML parses `field: null` as JavaScript null, not undefined.
108
+ *
109
+ * Design note: We could add the short ID to this schema. We didn't originally
110
+ * because it's one more field to maintain consistency around across files.
111
+ * Having it here might make recovery of lost ID mappings far easier, but for
112
+ * now we have more reliable management of the mappings file (ids.yml) and
113
+ * consider it authoritative. See IdMappingYamlSchema (§2.6.8).
108
114
  */
109
115
  const IssueSchema = BaseEntity.extend({
110
116
  type: z.literal("is"),
@@ -572,4 +578,4 @@ function parseYamlToleratingDuplicateKeys(content, filePath) {
572
578
 
573
579
  //#endregion
574
580
  export { LOCAL_STATE_FIELD_ORDER as A, GitRemoteName as C, IssueKind as D, IssueId as E, ShortId as F, Timestamp as I, Ulid as L, META_FIELD_ORDER as M, MetaSchema as N, IssueSchema as O, Priority as P, Version as R, GitBranchName as S, IdMappingYamlSchema as T, DependencyRelationType as _, stringifyYaml as a, EntityType as b, ordering as c, ATTIC_ENTRY_FIELD_ORDER as d, AtticEntrySchema as f, Dependency as g, ConfigSchema as h, sortKeys as i, LocalStateSchema as j, IssueStatus as k, PAGINATION_LINE_THRESHOLD as l, CONFIG_FIELD_ORDER as m, parseYamlToleratingDuplicateKeys as n, stringifyYamlCompact as o, BaseEntity as p, parseYamlWithConflictDetection as r, comparisonChain as s, detectDuplicateYamlKeys as t, PARENT_CONTEXT_MAX_LINES as u, DocCacheConfigSchema as v, ISSUE_FIELD_ORDER as w, ExternalIssueIdInput as x, DocsCacheSchema as y };
575
- //# sourceMappingURL=yaml-utils-x_kr2IId.mjs.map
581
+ //# sourceMappingURL=yaml-utils-U7l9hhkh.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"yaml-utils-U7l9hhkh.mjs","names":["parseYaml"],"sources":["../src/lib/schemas.ts","../src/lib/settings.ts","../src/lib/comparison-chain.ts","../src/utils/yaml-utils.ts"],"sourcesContent":["/**\n * Zod schemas for tbd entities.\n *\n * These schemas are the normative specification for the file format.\n * See: tbd-design.md §2.6 Schemas\n */\n\nimport { z } from 'zod';\n\n// =============================================================================\n// Common Types (§2.6.1)\n// =============================================================================\n\n/**\n * ISO8601 timestamp with Z suffix (UTC).\n */\nexport const Timestamp = z.string().datetime();\n\n/**\n * Issue ID: prefix + 26 lowercase alphanumeric characters (ULID format).\n * Format: is-{ulid} where ulid is 26 chars (a-z, 0-9).\n * Example: is-01hx5zzkbkactav9wevgemmvrz\n */\nexport const IssueId = z.string().regex(/^is-[0-9a-z]{26}$/);\n\n/**\n * Short ID: 1+ base36 characters used for external/display IDs.\n * Typically 4 chars for new IDs (e.g., a7k2, b3m9).\n * Imports may preserve longer numeric IDs (e.g., \"100\" from \"tbd-100\").\n * Legacy imports may include dots (e.g., \"208.1\" from hierarchical numbering).\n * Imports may include dashes/underscores (e.g., \"stat-in_progress\" from JSONL).\n */\nexport const ShortId = z.string().regex(/^[0-9a-z._-]+$/);\n\n/**\n * ULID: 26 lowercase alphanumeric characters.\n * Used in internal IDs and ID mappings.\n * Example: 01hx5zzkbkactav9wevgemmvrz\n */\nexport const Ulid = z.string().regex(/^[0-9a-z]{26}$/);\n\n/**\n * External Issue ID input: accepts {prefix}-{short} or just {short}.\n * Examples: bd-a7k2, a7k2, bd-100, 100\n */\nexport const ExternalIssueIdInput = z.string().regex(/^([a-z]+-)?[0-9a-z]+$/);\n\n/**\n * Edit counter - incremented on every local change.\n * NOTE: Version is NOT used for conflict detection (Git push rejection is used).\n * Content hash is used as tiebreaker during merge resolution.\n * Version is informational only - set to max(local, remote) + 1 after merges.\n */\nexport const Version = z.number().int().nonnegative();\n\n/**\n * Entity type discriminator.\n */\nexport const EntityType = z.literal('is');\n\n// =============================================================================\n// BaseEntity (§2.6.2)\n// =============================================================================\n\n/**\n * All entities share common fields.\n */\nexport const BaseEntity = z.object({\n type: EntityType,\n id: IssueId,\n version: Version,\n created_at: Timestamp,\n updated_at: Timestamp,\n\n // Extensibility namespace for third-party data\n extensions: z.record(z.string(), z.unknown()).optional(),\n});\n\n// =============================================================================\n// Issue Schema (§2.6.3)\n// =============================================================================\n\n/**\n * Issue status values matching Beads.\n */\nexport const IssueStatus = z.enum(['open', 'in_progress', 'blocked', 'deferred', 'closed']);\n\n/**\n * Issue kind/type values matching Beads.\n * Note: CLI uses --type flag, which maps to this `kind` field.\n */\nexport const IssueKind = z.enum(['bug', 'feature', 'task', 'epic', 'chore']);\n\n/**\n * Priority: 0 (highest/critical) to 4 (lowest).\n */\nexport const Priority = z.number().int().min(0).max(4);\n\n/**\n * Dependency types - only \"blocks\" supported initially.\n */\nexport const DependencyRelationType = z.enum(['blocks']);\n\n/**\n * A dependency relationship.\n */\nexport const Dependency = z.object({\n type: DependencyRelationType,\n target: IssueId,\n});\n\n/**\n * Full issue schema.\n *\n * Field order is canonical and mirrored by ISSUE_FIELD_ORDER below:\n * type, id, title, kind, status, priority, version (the \"header seven\"),\n * then linkages, assignment, hierarchy, scheduling, provenance,\n * timestamps, lifecycle, and extensions.\n *\n * Note: Fields use .nullable() in addition to .optional() because\n * YAML parses `field: null` as JavaScript null, not undefined.\n *\n * Design note: We could add the short ID to this schema. We didn't originally\n * because it's one more field to maintain consistency around across files.\n * Having it here might make recovery of lost ID mappings far easier, but for\n * now we have more reliable management of the mappings file (ids.yml) and\n * consider it authoritative. See IdMappingYamlSchema (§2.6.8).\n */\nexport const IssueSchema = BaseEntity.extend({\n // Header seven: the fields you always want to see at a glance\n type: z.literal('is'),\n // id, version inherited from BaseEntity\n title: z.string().min(1).max(500),\n kind: IssueKind.default('task'),\n status: IssueStatus.default('open'),\n priority: Priority.default(2),\n\n // Body content (serialized outside frontmatter)\n description: z.string().max(50000).nullable().optional(),\n notes: z.string().max(50000).nullable().optional(),\n\n // Linkages\n spec_path: z.string().nullable().optional(),\n\n // Assignment and categorization\n assignee: z.string().nullable().optional(),\n labels: z.array(z.string()).default([]),\n dependencies: z.array(Dependency).default([]),\n\n // Hierarchical issues\n parent_id: IssueId.nullable().optional(),\n\n // Child ordering hints - soft ordering for children under this parent.\n // Array of internal IssueIds in preferred display order.\n // May contain stale IDs; display logic filters for actual children.\n child_order_hints: z.array(IssueId).nullable().optional(),\n\n // Scheduling\n due_date: Timestamp.nullable().optional(),\n deferred_until: Timestamp.nullable().optional(),\n\n // Provenance and lifecycle\n created_by: z.string().nullable().optional(),\n closed_at: Timestamp.nullable().optional(),\n close_reason: z.string().nullable().optional(),\n});\n\n// =============================================================================\n// Config Schema (§2.6.4)\n// =============================================================================\n\n/**\n * Git branch name - restricted to safe characters.\n * Allows: alphanumeric, hyphens, underscores, forward slashes, and dots.\n * Prevents shell injection in git commands.\n */\nexport const GitBranchName = z\n .string()\n .min(1)\n .max(255)\n .regex(\n /^[a-zA-Z0-9._/-]+$/,\n 'Invalid branch name: only alphanumeric, dots, underscores, hyphens, and slashes allowed',\n );\n\n/**\n * Git remote name - restricted to safe characters.\n * Allows: alphanumeric, hyphens, underscores, and dots.\n * Prevents shell injection in git commands.\n */\nexport const GitRemoteName = z\n .string()\n .min(1)\n .max(255)\n .regex(\n /^[a-zA-Z0-9._-]+$/,\n 'Invalid remote name: only alphanumeric, dots, underscores, and hyphens allowed',\n );\n\n/**\n * Doc cache configuration - maps destination paths to source locations.\n *\n * Keys are destination paths relative to .tbd/docs/ (e.g., \"shortcuts/standard/code-review-and-commit.md\")\n * Values are source locations:\n * - internal: prefix for bundled docs (e.g., \"internal:shortcuts/standard/code-review-and-commit.md\")\n * - Full URL for external docs (e.g., \"https://raw.githubusercontent.com/org/repo/main/file.md\")\n *\n * Example:\n * ```yaml\n * doc_cache:\n * shortcuts/standard/code-review-and-commit.md: internal:shortcuts/standard/code-review-and-commit.md\n * shortcuts/custom/my-shortcut.md: https://raw.githubusercontent.com/org/repo/main/shortcuts/my-shortcut.md\n * ```\n */\nexport const DocCacheConfigSchema = z.record(z.string(), z.string());\n\n/**\n * Documentation cache configuration (consolidated structure).\n *\n * Combines file sync mappings and lookup paths into a single config block.\n * See: docs/project/specs/active/plan-2026-01-26-docs-cache-config-restructure.md\n */\nexport const DocsCacheSchema = z.object({\n /**\n * Files to sync: maps destination paths to source locations.\n * Keys are destination paths relative to .tbd/docs/\n * Values are source locations:\n * - internal: prefix for bundled docs (e.g., \"internal:shortcuts/standard/code-review-and-commit.md\")\n * - Full URL for external docs (e.g., \"https://raw.githubusercontent.com/org/repo/main/file.md\")\n */\n files: z.record(z.string(), z.string()).optional(),\n /**\n * Search paths for doc lookup (like shell $PATH).\n * Earlier paths take precedence when names conflict.\n */\n lookup_path: z\n .array(z.string())\n .default(['.tbd/docs/shortcuts/system', '.tbd/docs/shortcuts/standard']),\n});\n\n/**\n * Project configuration stored in .tbd/config.yml\n *\n * ⚠️ FORMAT VERSIONING: See tbd-format.ts for version history and migration rules.\n * The tbd_format field tracks breaking changes to this schema.\n *\n * ⚠️ FORWARD COMPATIBILITY POLICY:\n * This schema uses Zod's default strip() mode, which discards unknown fields.\n * To prevent data loss when users mix tbd versions:\n *\n * 1. **When changing config schema (adding, removing, or modifying fields), ALWAYS\n * bump the format version** (e.g., f03 → f04)\n * 2. Older tbd versions will error when they see an unknown format version\n * 3. The error message tells users to upgrade tbd\n *\n * This ensures older versions fail fast rather than silently corrupting config.\n * The format version check happens in config.ts via isCompatibleFormat().\n *\n * See tbd-format.ts for format version history and migration rules.\n */\nexport const ConfigSchema = z.object({\n /**\n * Format version for the .tbd/ directory structure.\n * See tbd-format.ts for version history and migration rules.\n * Only bumped for breaking changes that require migration.\n */\n tbd_format: z.string().default('f01'),\n\n tbd_version: z.string(),\n sync: z\n .object({\n branch: GitBranchName.default('tbd-sync'),\n remote: GitRemoteName.default('origin'),\n })\n .default({}),\n display: z.object({\n id_prefix: z.string().min(1).max(20), // Required: set during init --prefix or import\n }),\n settings: z\n .object({\n auto_sync: z.boolean().default(false),\n /**\n * How often to automatically sync documentation cache (in hours).\n * - Default: 24 (sync once per day when actively using tbd)\n * - Set to 0 to disable auto-sync\n * - Only triggers when accessing docs (shortcut, guidelines, template commands)\n */\n doc_auto_sync_hours: z.number().default(24),\n /**\n * Whether to install the ensure-gh-cli.sh hook script during setup.\n * When true (default), `tbd setup` installs a SessionStart hook that\n * ensures the GitHub CLI is available in agent sessions.\n * Set to false or use `tbd setup --no-gh-cli` to disable.\n */\n use_gh_cli: z.boolean().default(true),\n })\n .default({}),\n /**\n * Documentation cache configuration (consolidated).\n * Contains files to sync and lookup paths.\n * See DocsCacheSchema for structure details.\n */\n docs_cache: DocsCacheSchema.optional(),\n});\n\n// =============================================================================\n// Meta Schema (§2.6.5)\n// =============================================================================\n\n/**\n * Shared metadata stored in .tbd/data-sync/meta.yml\n */\nexport const MetaSchema = z.object({\n schema_version: z.number().int(),\n created_at: Timestamp,\n});\n\n// =============================================================================\n// Local State Schema (§2.6.6)\n// =============================================================================\n\n/**\n * Per-node state stored in .tbd/state.yml (gitignored).\n * Tracks local timing information that shouldn't be shared across nodes.\n */\nexport const LocalStateSchema = z.object({\n /** When this node last synced issues successfully */\n last_sync_at: Timestamp.optional(),\n /** When this node last synced the doc cache successfully */\n last_doc_sync_at: Timestamp.optional(),\n /** Whether the user has seen the welcome message */\n welcome_seen: z.boolean().optional(),\n});\n\n// =============================================================================\n// Attic Entry Schema (§2.6.7)\n// =============================================================================\n\n/**\n * Preserved conflict losers.\n */\nexport const AtticEntrySchema = z.object({\n entity_id: IssueId,\n timestamp: Timestamp,\n field: z.string(),\n lost_value: z.string(),\n winner_source: z.enum(['local', 'remote']),\n loser_source: z.enum(['local', 'remote']),\n context: z.object({\n local_version: Version,\n remote_version: Version,\n local_updated_at: Timestamp,\n remote_updated_at: Timestamp,\n }),\n});\n\n// =============================================================================\n// ID Mapping Schema (§2.6.8)\n// =============================================================================\n\n/**\n * ID mapping YAML file schema for ids.yml.\n * Maps short IDs to ULIDs.\n * Format: { \"a7k2\": \"01hx5zzkbkactav9wevgemmvrz\", ... }\n */\nexport const IdMappingYamlSchema = z.record(ShortId, Ulid);\n\n// =============================================================================\n// Field Order Constants for YAML Serialization\n// =============================================================================\n//\n// Each array defines the canonical field order for YAML output.\n// Order mirrors the Zod schema definition above: identity first,\n// human-relevant fields next, bookkeeping last.\n//\n// Used with sortKeys() from yaml-utils.ts and ordering.manual()\n// from comparison-chain.ts. Fields not listed sort to the end.\n\n/**\n * Canonical field order for issue YAML frontmatter.\n * (description and notes are body content, not frontmatter)\n */\nexport const ISSUE_FIELD_ORDER = [\n // Header seven: the fields you always want to see at a glance\n 'type',\n 'id',\n 'title',\n 'kind',\n 'status',\n 'priority',\n 'version',\n\n // Linkages\n 'spec_path',\n\n // Assignment and categorization\n 'assignee',\n 'labels',\n 'dependencies',\n\n // Hierarchy\n 'parent_id',\n 'child_order_hints',\n\n // Scheduling\n 'due_date',\n 'deferred_until',\n\n // Provenance\n 'created_by',\n\n // Timestamps\n 'created_at',\n 'updated_at',\n\n // Lifecycle (closure)\n 'closed_at',\n 'close_reason',\n\n // Extensibility\n 'extensions',\n] as const;\n\n/**\n * Canonical field order for config YAML.\n */\nexport const CONFIG_FIELD_ORDER = [\n 'tbd_format',\n 'tbd_version',\n 'display',\n 'sync',\n 'settings',\n 'docs_cache',\n] as const;\n\n/**\n * Canonical field order for attic entry YAML.\n */\nexport const ATTIC_ENTRY_FIELD_ORDER = [\n 'entity_id',\n 'timestamp',\n 'field',\n 'lost_value',\n 'winner_source',\n 'loser_source',\n 'context',\n] as const;\n\n/**\n * Canonical field order for meta YAML.\n */\nexport const META_FIELD_ORDER = ['schema_version', 'created_at'] as const;\n\n/**\n * Canonical field order for local state YAML.\n */\nexport const LOCAL_STATE_FIELD_ORDER = [\n 'last_sync_at',\n 'last_doc_sync_at',\n 'welcome_seen',\n] as const;\n","/**\n * Global settings and configuration constants.\n *\n * Centralized location for project-wide defaults that may need tuning.\n * These are compile-time constants, not runtime configuration.\n */\n\nimport type { DocumentOptions, SchemaOptions, ToStringOptions } from 'yaml';\n\n// =============================================================================\n// YAML Formatting\n// =============================================================================\n\n/**\n * Default line width for YAML serialization.\n * 88 characters is a good balance between readability and avoiding excessive wrapping.\n * (Matches Python's Black formatter default.)\n */\nexport const YAML_LINE_WIDTH = 88;\n\n/**\n * Default string type for YAML serialization.\n * 'PLAIN' means no forced quoting - YAML only quotes when necessary.\n */\nexport const YAML_DEFAULT_STRING_TYPE = 'PLAIN' as const;\n\n/**\n * Default key type for YAML serialization.\n * 'PLAIN' means object keys are unquoted unless required.\n */\nexport const YAML_DEFAULT_KEY_TYPE = 'PLAIN' as const;\n\n/**\n * Combined options type for YAML stringify.\n */\nexport type YamlStringifyOptions = DocumentOptions & SchemaOptions & ToStringOptions;\n\n/**\n * Default YAML serialization options for readable output.\n *\n * Design principles:\n * - No forced quoting: YAML only quotes when necessary for special characters\n * - Reasonable line wrapping: 88 chars prevents overly long lines\n * - Plain keys: No unnecessary quotes around object keys\n * - Sorted keys: Deterministic output for diffs and version control\n */\nexport const YAML_STRINGIFY_OPTIONS: YamlStringifyOptions = {\n lineWidth: YAML_LINE_WIDTH,\n defaultStringType: YAML_DEFAULT_STRING_TYPE,\n defaultKeyType: YAML_DEFAULT_KEY_TYPE,\n sortMapEntries: true,\n};\n\n/**\n * YAML serialization options for compact output (e.g., frontmatter).\n * Uses lineWidth: 0 to prevent wrapping within values.\n */\nexport const YAML_STRINGIFY_OPTIONS_COMPACT: YamlStringifyOptions = {\n lineWidth: 0, // No wrapping\n defaultStringType: YAML_DEFAULT_STRING_TYPE,\n defaultKeyType: YAML_DEFAULT_KEY_TYPE,\n sortMapEntries: true,\n};\n\n// =============================================================================\n// Text Formatting\n// =============================================================================\n\n/**\n * Default line width for text output (terminal, markdown).\n * Same as YAML for consistency.\n */\nexport const TEXT_LINE_WIDTH = 88;\n\n// =============================================================================\n// CLI Output\n// =============================================================================\n\n/**\n * Minimum content length (in lines) before pagination is triggered.\n * ~50 lines is slightly more than one terminal screen.\n */\nexport const PAGINATION_LINE_THRESHOLD = 50;\n\n/**\n * Maximum lines to display when showing parent bead context in `tbd show`.\n * The parent's full serialized output is truncated to this many lines,\n * with an ellipsis and omitted-line count appended if exceeded.\n */\nexport const PARENT_CONTEXT_MAX_LINES = 50;\n","/**\n * Comparison chain utilities for complex multi-field sorting.\n *\n * Inspired by Google Guava's ComparisonChain, this provides a fluent API\n * for building comparators with multiple sort keys, null handling, and\n * custom orderings.\n *\n * Example:\n *\n * items.sort(comparisonChain<Item>()\n * .compare(item => item.title, ordering.nullsLast)\n * .compare(item => item.url)\n * .result());\n */\n\nexport type Selector<T, K> = (item: T) => K;\nexport type Comparator<T> = (a: T, b: T) => number;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst defaultCompare: Comparator<any> = (a, b) => {\n if (typeof a === 'string' && typeof b === 'string') {\n return a.localeCompare(b);\n }\n if (a < b) return -1;\n if (a > b) return 1;\n return 0;\n};\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst nullLastCompare: Comparator<any> = (a, b) => {\n if (a == null) return b == null ? 0 : 1;\n if (b == null) return -1;\n return defaultCompare(a, b);\n};\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst nullFirstCompare: Comparator<any> = (a, b) => {\n if (a == null) return b == null ? 0 : -1;\n if (b == null) return 1;\n return defaultCompare(a, b);\n};\n\n/**\n * A Google Guava-style comparison chain to make complex sorting, such as secondary\n * sorts, significantly easier.\n *\n * For example:\n *\n * items.sort(comparisonChain<Item>()\n * .compare(item => item.title, ordering.nullsLast)\n * .compare(item => item.url)\n * .result());\n */\nexport const comparisonChain = <T>() => {\n let compare: Comparator<T> = () => 0;\n\n const chain: {\n compare: <K>(selector: Selector<T, K>, comparator?: Comparator<K>) => typeof chain;\n result: () => Comparator<T>;\n } = {\n compare: <K>(selector: Selector<T, K>, comparator: Comparator<K> = defaultCompare) => {\n const prevCompare = compare;\n compare = (a, b) => prevCompare(a, b) || comparator(selector(a), selector(b));\n return chain;\n },\n result: () => compare,\n };\n\n return chain;\n};\n\n/**\n * Reverse a comparator's ordering.\n */\nexport const reverse =\n <T>(comparator: Comparator<T>) =>\n (a: T, b: T) =>\n comparator(b, a);\n\n/**\n * Create a comparator that sorts values in a manually specified order.\n * Values not in the order array are placed at the end.\n */\nconst manualOrderComparator = <T>(order: readonly T[]): Comparator<T> => {\n const orderMap = new Map(order.map((value, index) => [value, index]));\n\n return (a: T, b: T): number => {\n const indexA = orderMap.get(a);\n const indexB = orderMap.get(b);\n\n // Values not in the manually ordered array go at the end.\n if (indexA === undefined) return indexB === undefined ? 0 : 1;\n if (indexB === undefined) return -1;\n\n return indexA - indexB;\n };\n};\n\n/**\n * Common ordering strategies for use with comparisonChain.\n */\nexport const ordering = {\n nullsLast: nullLastCompare,\n nullsFirst: nullFirstCompare,\n default: defaultCompare,\n reversed: reverse(defaultCompare),\n manual: manualOrderComparator,\n};\n","/**\n * YAML utility functions.\n *\n * Provides centralized YAML parsing and serialization with:\n * - Merge conflict detection for user-editable files\n * - Consistent, readable formatting defaults\n * - Proper handling of special characters (colons, quotes, etc.)\n *\n * IMPORTANT: Always use these utilities instead of raw yaml package functions.\n * This ensures consistent formatting and proper error handling across the codebase.\n */\n\nimport { parse as parseYaml, stringify, parseDocument } from 'yaml';\n\nimport {\n YAML_LINE_WIDTH,\n YAML_STRINGIFY_OPTIONS,\n YAML_STRINGIFY_OPTIONS_COMPACT,\n type YamlStringifyOptions,\n} from '../lib/settings.js';\nimport { ordering } from '../lib/comparison-chain.js';\n\n// Re-export for convenience\nexport { YAML_LINE_WIDTH, YAML_STRINGIFY_OPTIONS, YAML_STRINGIFY_OPTIONS_COMPACT };\n\n// =============================================================================\n// Serialization Functions\n// =============================================================================\n\n/**\n * Serialize data to YAML with readable formatting.\n *\n * Uses consistent defaults:\n * - No forced quoting (YAML only quotes when necessary)\n * - lineWidth of 88 provides reasonable wrapping for long strings\n * - Plain keys without quotes\n * - Sorted keys for deterministic output\n *\n * @param data - Data to serialize\n * @param options - Optional overrides for default options\n * @returns YAML string\n */\nexport function stringifyYaml(data: unknown, options?: Partial<YamlStringifyOptions>): string {\n return stringify(data, { ...YAML_STRINGIFY_OPTIONS, ...options });\n}\n\n/**\n * Serialize data to YAML without line wrapping (compact mode).\n * Useful for frontmatter where values should stay on single lines.\n *\n * @param data - Data to serialize\n * @returns YAML string with no line wrapping\n */\nexport function stringifyYamlCompact(data: unknown): string {\n return stringify(data, YAML_STRINGIFY_OPTIONS_COMPACT);\n}\n\n/**\n * Create a new object with keys sorted according to a manual field ordering.\n * Uses ordering.manual() from comparison-chain.ts, so fields not in the\n * order array are placed at the end.\n */\nexport function sortKeys<T extends Record<string, unknown>>(\n obj: T,\n fieldOrder: readonly string[],\n): Record<string, unknown> {\n const keyComparator = ordering.manual(fieldOrder);\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(obj).sort(keyComparator)) {\n sorted[key] = obj[key];\n }\n return sorted;\n}\n\n/**\n * Error thrown when YAML content contains unresolved merge conflict markers.\n */\nexport class MergeConflictError extends Error {\n constructor(\n message: string,\n public readonly filePath?: string,\n ) {\n super(message);\n this.name = 'MergeConflictError';\n }\n}\n\n/**\n * Regex patterns for git merge conflict markers.\n */\nconst CONFLICT_PATTERNS = {\n start: /^<<<<<<< /m,\n separator: /^=======/m,\n end: /^>>>>>>> /m,\n};\n\n/**\n * Check if content contains git merge conflict markers.\n */\nexport function hasMergeConflictMarkers(content: string): boolean {\n return (\n CONFLICT_PATTERNS.start.test(content) ||\n CONFLICT_PATTERNS.separator.test(content) ||\n CONFLICT_PATTERNS.end.test(content)\n );\n}\n\n/**\n * Parse YAML content with merge conflict detection.\n *\n * If the content contains merge conflict markers, throws a MergeConflictError\n * with a helpful message instead of a cryptic YAML parse error.\n *\n * @param content - The YAML content to parse\n * @param filePath - Optional file path for error messages\n * @returns Parsed YAML data\n * @throws MergeConflictError if content has conflict markers\n * @throws Error if YAML is invalid for other reasons\n */\nexport function parseYamlWithConflictDetection<T = unknown>(content: string, filePath?: string): T {\n // Check for merge conflict markers first\n if (hasMergeConflictMarkers(content)) {\n const location = filePath ? ` in ${filePath}` : '';\n throw new MergeConflictError(\n `File${location} contains unresolved git merge conflict markers.\\n` +\n `This usually happens when 'tbd sync' encountered conflicts that weren't properly resolved.\\n` +\n `To fix: manually edit the file to resolve conflicts, or run 'tbd doctor --fix'.`,\n filePath,\n );\n }\n\n return parseYaml(content) as T;\n}\n\n/**\n * Result from parsing YAML with duplicate key handling.\n */\nexport interface ParseWithDuplicatesResult<T> {\n data: T;\n /** Keys that appeared more than once (empty if no duplicates). */\n duplicateKeys: string[];\n}\n\n/**\n * Detect duplicate top-level keys in YAML content.\n *\n * Scans lines for `key: value` patterns and finds keys that appear more than once.\n * Only checks top-level keys (no indentation).\n */\nexport function detectDuplicateYamlKeys(content: string): string[] {\n const seen = new Set<string>();\n const duplicates = new Set<string>();\n\n for (const line of content.split('\\n')) {\n // Match top-level keys: no leading whitespace, key followed by colon\n const match = /^([^\\s#][^:]*?):\\s/.exec(line);\n if (match?.[1]) {\n const key = match[1];\n if (seen.has(key)) {\n duplicates.add(key);\n }\n seen.add(key);\n }\n }\n\n return Array.from(duplicates);\n}\n\n/**\n * Parse YAML content tolerating duplicate keys.\n *\n * This is specifically designed for files like ids.yml that may end up with\n * duplicate keys after a git merge conflict resolution that kept entries from\n * both sides. Instead of crashing with \"Map keys must be unique\", this function:\n *\n * 1. Checks for merge conflict markers (throws MergeConflictError)\n * 2. Detects duplicate keys in the raw content\n * 3. Parses with `uniqueKeys: false` so the last occurrence wins\n * 4. Returns both the parsed data and the list of duplicate keys found\n *\n * Callers should warn about duplicates and re-save the file to fix it.\n */\nexport function parseYamlToleratingDuplicateKeys<T = unknown>(\n content: string,\n filePath?: string,\n): ParseWithDuplicatesResult<T> {\n // Check for merge conflict markers first\n if (hasMergeConflictMarkers(content)) {\n const location = filePath ? ` in ${filePath}` : '';\n throw new MergeConflictError(\n `File${location} contains unresolved git merge conflict markers.\\n` +\n `This usually happens when 'tbd sync' encountered conflicts that weren't properly resolved.\\n` +\n `To fix: manually edit the file to resolve conflicts, or run 'tbd doctor --fix'.`,\n filePath,\n );\n }\n\n // Detect duplicate keys before parsing\n const duplicateKeys = detectDuplicateYamlKeys(content);\n\n if (duplicateKeys.length === 0) {\n // No duplicates — use standard parse for maximum safety\n return {\n data: parseYaml(content) as T,\n duplicateKeys: [],\n };\n }\n\n // Parse with uniqueKeys: false to tolerate duplicates (last occurrence wins)\n const doc = parseDocument(content, { uniqueKeys: false });\n\n // Check for other (non-duplicate-key) parse errors\n if (doc.errors.length > 0) {\n const location = filePath ? ` in ${filePath}` : '';\n throw new Error(`YAML parse error${location}: ${doc.errors.map((e) => e.message).join('; ')}`);\n }\n\n return {\n data: doc.toJSON() as T,\n duplicateKeys,\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "get-tbd",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "Git-native issue tracking for AI agents and humans",
5
5
  "license": "MIT",
6
6
  "author": "Joshua Levy <joshua@cal.berkeley.edu> (https://github.com/jlevy)",
@@ -62,15 +62,15 @@
62
62
  },
63
63
  "devDependencies": {
64
64
  "@types/marked-terminal": "^6.1.1",
65
- "@types/node": "^22.19.7",
66
- "@vitest/coverage-v8": "^2.1.9",
65
+ "@types/node": "^24.0.0",
66
+ "@vitest/coverage-v8": "^4.0.0",
67
67
  "c8": "^10.1.3",
68
68
  "monocart-coverage-reports": "^2.12.9",
69
69
  "publint": "^0.3.17",
70
70
  "tryscript": "^0.1.6",
71
71
  "tsdown": "^0.20.1",
72
72
  "tsx": "^4.21.0",
73
- "vitest": "^2.1.9"
73
+ "vitest": "^4.0.0"
74
74
  },
75
75
  "scripts": {
76
76
  "prebuild": "node scripts/copy-docs.mjs prebuild",
@@ -1,3 +0,0 @@
1
- import { a as loadIdMapping, c as resolveToInternalId, i as hasShortId, l as saveIdMapping, n as calculateOptimalLength, o as mergeIdMappings, r as generateUniqueShortId, s as parseIdMappingFromYaml, t as addIdMapping } from "./id-mapping-CD5c_ZVA.mjs";
2
-
3
- export { loadIdMapping, saveIdMapping };