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.
- package/README.md +17 -19
- package/dist/bin.mjs +181 -12
- package/dist/bin.mjs.map +1 -1
- package/dist/cli.mjs +110 -644
- package/dist/cli.mjs.map +1 -1
- package/dist/config-CB1tcqTZ.mjs +3 -0
- package/dist/config-CmEAGaxz.mjs +637 -0
- package/dist/config-CmEAGaxz.mjs.map +1 -0
- package/dist/docs/README.md +17 -19
- package/dist/docs/guidelines/bun-monorepo-patterns.md +816 -80
- package/dist/docs/guidelines/pnpm-monorepo-patterns.md +586 -16
- package/dist/docs/guidelines/python-cli-patterns.md +2 -2
- package/dist/docs/guidelines/tbd-sync-troubleshooting.md +27 -0
- package/dist/docs/guidelines/typescript-cli-tool-rules.md +465 -196
- package/dist/docs/tbd-design.md +86 -46
- package/dist/docs/tbd-docs.md +0 -6
- package/dist/id-mapping-0-R0X8zb.mjs +3 -0
- package/dist/{id-mapping-CD5c_ZVA.mjs → id-mapping-JGow6Jk4.mjs} +57 -3
- package/dist/{id-mapping-CD5c_ZVA.mjs.map → id-mapping-JGow6Jk4.mjs.map} +1 -1
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +2 -2
- package/dist/{src-BjMRpmMh.mjs → src-7qUDeWJf.mjs} +3 -3
- package/dist/{src-BjMRpmMh.mjs.map → src-7qUDeWJf.mjs.map} +1 -1
- package/dist/tbd +181 -12
- package/dist/{yaml-utils-x_kr2IId.mjs → yaml-utils-U7l9hhkh.mjs} +7 -1
- package/dist/yaml-utils-U7l9hhkh.mjs.map +1 -0
- package/package.json +4 -4
- package/dist/id-mapping-BqSnxlxk.mjs +0 -3
- package/dist/yaml-utils-x_kr2IId.mjs.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"yaml-utils-x_kr2IId.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 */\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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"}
|