get-tbd 0.1.24 → 0.1.26
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 +2 -2
- package/dist/bin.mjs +195 -49
- package/dist/bin.mjs.map +1 -1
- package/dist/cli.mjs +126 -44
- package/dist/cli.mjs.map +1 -1
- package/dist/{config-CB1tcqTZ.mjs → config-BZte2m3w.mjs} +1 -1
- package/dist/{config-CmEAGaxz.mjs → config-b20Kf5pW.mjs} +3 -2
- package/dist/config-b20Kf5pW.mjs.map +1 -0
- package/dist/docs/README.md +2 -2
- package/dist/docs/SKILL.md +31 -31
- package/dist/docs/guidelines/cli-agent-skill-patterns.md +1 -1
- package/dist/docs/guidelines/convex-limits-best-practices.md +16 -16
- package/dist/docs/guidelines/convex-rules.md +3 -3
- package/dist/docs/guidelines/electron-app-development-patterns.md +1 -1
- package/dist/docs/guidelines/error-handling-rules.md +2 -2
- package/dist/docs/guidelines/general-coding-rules.md +2 -2
- package/dist/docs/guidelines/general-comment-rules.md +2 -2
- package/dist/docs/guidelines/general-eng-assistant-rules.md +2 -2
- package/dist/docs/guidelines/python-rules.md +4 -4
- package/dist/docs/guidelines/typescript-rules.md +17 -17
- package/dist/docs/guidelines/typescript-yaml-handling-rules.md +8 -8
- package/dist/docs/shortcuts/standard/new-guideline.md +4 -4
- package/dist/docs/shortcuts/standard/new-validation-plan.md +13 -13
- package/dist/docs/shortcuts/standard/revise-all-architecture-docs.md +1 -1
- package/dist/docs/shortcuts/standard/setup-github-cli.md +1 -1
- package/dist/docs/shortcuts/standard/welcome-user.md +12 -12
- package/dist/docs/shortcuts/system/skill-baseline.md +31 -31
- package/dist/id-mapping-BA_xn516.mjs +3 -0
- package/dist/{id-mapping-DjVJIO4M.mjs → id-mapping-BtBwq5nG.mjs} +68 -15
- package/dist/id-mapping-BtBwq5nG.mjs.map +1 -0
- package/dist/index.mjs +2 -2
- package/dist/schemas-BQYmDnkv.mjs +311 -0
- package/dist/schemas-BQYmDnkv.mjs.map +1 -0
- package/dist/{src-BrM6xcdG.mjs → src-DQcOQnFp.mjs} +4 -3
- package/dist/{src-BrM6xcdG.mjs.map → src-DQcOQnFp.mjs.map} +1 -1
- package/dist/tbd +195 -49
- package/dist/yaml-utils-BPy991by.mjs +273 -0
- package/dist/yaml-utils-BPy991by.mjs.map +1 -0
- package/dist/yaml-utils-swV780m5.mjs +3 -0
- package/package.json +1 -1
- package/dist/config-CmEAGaxz.mjs.map +0 -1
- package/dist/id-mapping-DjVJIO4M.mjs.map +0 -1
- package/dist/id-mapping-LjnDSEhN.mjs +0 -3
- package/dist/yaml-utils-U7l9hhkh.mjs +0 -581
- package/dist/yaml-utils-U7l9hhkh.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -279,7 +279,7 @@ npm install -g get-tbd@latest
|
|
|
279
279
|
### Setup
|
|
280
280
|
|
|
281
281
|
```bash
|
|
282
|
-
# Fresh project (--prefix is REQUIRED—
|
|
282
|
+
# Fresh project (--prefix is REQUIRED—a short alphabetic name used as an issue ID prefix, e.g. myapp → issues like myapp-a1b2)
|
|
283
283
|
tbd setup --auto --prefix=myapp
|
|
284
284
|
|
|
285
285
|
# Joining an existing tbd project (no prefix needed—reads existing config)
|
|
@@ -299,7 +299,7 @@ tbd setup --from-beads
|
|
|
299
299
|
**First contributor:**
|
|
300
300
|
```bash
|
|
301
301
|
npm install -g get-tbd@latest
|
|
302
|
-
tbd setup --auto --prefix=
|
|
302
|
+
tbd setup --auto --prefix=proj # Short alphabetic prefix for issue IDs
|
|
303
303
|
git add .tbd/ .claude/ && git commit -m "Initialize tbd"
|
|
304
304
|
git push
|
|
305
305
|
```
|
package/dist/bin.mjs
CHANGED
|
@@ -13735,6 +13735,18 @@ const ordering = {
|
|
|
13735
13735
|
* IMPORTANT: Always use these utilities instead of raw yaml package functions.
|
|
13736
13736
|
* This ensures consistent formatting and proper error handling across the codebase.
|
|
13737
13737
|
*/
|
|
13738
|
+
var yaml_utils_exports = /* @__PURE__ */ __exportAll({
|
|
13739
|
+
MergeConflictError: () => MergeConflictError,
|
|
13740
|
+
YAML_STRINGIFY_OPTIONS: () => YAML_STRINGIFY_OPTIONS,
|
|
13741
|
+
YAML_STRINGIFY_OPTIONS_COMPACT: () => YAML_STRINGIFY_OPTIONS_COMPACT,
|
|
13742
|
+
detectDuplicateYamlKeys: () => detectDuplicateYamlKeys,
|
|
13743
|
+
hasMergeConflictMarkers: () => hasMergeConflictMarkers,
|
|
13744
|
+
parseYamlToleratingDuplicateKeys: () => parseYamlToleratingDuplicateKeys,
|
|
13745
|
+
parseYamlWithConflictDetection: () => parseYamlWithConflictDetection,
|
|
13746
|
+
sortKeys: () => sortKeys,
|
|
13747
|
+
stringifyYaml: () => stringifyYaml,
|
|
13748
|
+
stringifyYamlCompact: () => stringifyYamlCompact
|
|
13749
|
+
});
|
|
13738
13750
|
/**
|
|
13739
13751
|
* Serialize data to YAML with readable formatting.
|
|
13740
13752
|
*
|
|
@@ -14033,7 +14045,7 @@ function serializeIssue(issue) {
|
|
|
14033
14045
|
* Package version, derived from git at build time.
|
|
14034
14046
|
* Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.
|
|
14035
14047
|
*/
|
|
14036
|
-
const VERSION$1 = "0.1.
|
|
14048
|
+
const VERSION$1 = "0.1.26";
|
|
14037
14049
|
|
|
14038
14050
|
//#endregion
|
|
14039
14051
|
//#region src/cli/lib/version.ts
|
|
@@ -99037,6 +99049,7 @@ async function initWorktree(baseDir, remote = "origin", syncBranch = SYNC_BRANCH
|
|
|
99037
99049
|
await writeFile(join(dataSyncPath, "meta.yml"), "schema_version: 1\n");
|
|
99038
99050
|
await writeFile(join(dataSyncPath, "issues", ".gitkeep"), "");
|
|
99039
99051
|
await writeFile(join(dataSyncPath, "mappings", ".gitkeep"), "");
|
|
99052
|
+
await writeFile(join(dataSyncPath, "mappings", ".gitattributes"), "ids.yml merge=union\n");
|
|
99040
99053
|
await git("-C", worktreePath, "add", ".");
|
|
99041
99054
|
await git("-C", worktreePath, "commit", "--no-verify", "-m", "Initialize tbd-sync branch");
|
|
99042
99055
|
return {
|
|
@@ -99274,9 +99287,16 @@ async function migrateDataToWorktree(baseDir, removeSource = false) {
|
|
|
99274
99287
|
await mkdir(correctMappingsPath, { recursive: true });
|
|
99275
99288
|
for (const file of issueFiles) await cp(join(wrongIssuesPath, file), join(correctIssuesPath, file));
|
|
99276
99289
|
for (const file of mappingFiles) if (file === "ids.yml") {
|
|
99277
|
-
const {
|
|
99278
|
-
const
|
|
99279
|
-
|
|
99290
|
+
const { readFile } = await import("node:fs/promises");
|
|
99291
|
+
const { loadIdMapping, mergeIdMappings, saveIdMapping, resolveIdMappingConflicts } = await Promise.resolve().then(() => id_mapping_exports);
|
|
99292
|
+
const sourceMapping = resolveIdMappingConflicts(await readFile(join(wrongMappingsPath, file), "utf-8"));
|
|
99293
|
+
let targetMapping;
|
|
99294
|
+
try {
|
|
99295
|
+
targetMapping = resolveIdMappingConflicts(await readFile(join(correctMappingsPath, file), "utf-8"));
|
|
99296
|
+
} catch {
|
|
99297
|
+
targetMapping = await loadIdMapping(correctPath);
|
|
99298
|
+
}
|
|
99299
|
+
await saveIdMapping(correctPath, mergeIdMappings(targetMapping, sourceMapping));
|
|
99280
99300
|
} else await cp(join(wrongMappingsPath, file), join(correctMappingsPath, file));
|
|
99281
99301
|
const totalFiles = issueFiles.length + mappingFiles.length;
|
|
99282
99302
|
await git("-C", worktreePath, "add", "-A");
|
|
@@ -99751,28 +99771,32 @@ async function listIssues(baseDir) {
|
|
|
99751
99771
|
return [];
|
|
99752
99772
|
}
|
|
99753
99773
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
99754
|
-
const
|
|
99755
|
-
const filePath = join(issuesDir, file);
|
|
99756
|
-
try {
|
|
99757
|
-
return {
|
|
99758
|
-
file,
|
|
99759
|
-
content: await readFile(filePath, "utf-8")
|
|
99760
|
-
};
|
|
99761
|
-
} catch {
|
|
99762
|
-
return {
|
|
99763
|
-
file,
|
|
99764
|
-
content: null
|
|
99765
|
-
};
|
|
99766
|
-
}
|
|
99767
|
-
}));
|
|
99774
|
+
const BATCH_SIZE = 200;
|
|
99768
99775
|
const issues = [];
|
|
99769
|
-
for (
|
|
99770
|
-
|
|
99771
|
-
|
|
99772
|
-
const
|
|
99773
|
-
|
|
99774
|
-
|
|
99775
|
-
|
|
99776
|
+
for (let i = 0; i < mdFiles.length; i += BATCH_SIZE) {
|
|
99777
|
+
const batch = mdFiles.slice(i, i + BATCH_SIZE);
|
|
99778
|
+
const fileContents = await Promise.all(batch.map(async (file) => {
|
|
99779
|
+
const filePath = join(issuesDir, file);
|
|
99780
|
+
try {
|
|
99781
|
+
return {
|
|
99782
|
+
file,
|
|
99783
|
+
content: await readFile(filePath, "utf-8")
|
|
99784
|
+
};
|
|
99785
|
+
} catch {
|
|
99786
|
+
return {
|
|
99787
|
+
file,
|
|
99788
|
+
content: null
|
|
99789
|
+
};
|
|
99790
|
+
}
|
|
99791
|
+
}));
|
|
99792
|
+
for (const { file, content } of fileContents) {
|
|
99793
|
+
if (content === null) continue;
|
|
99794
|
+
try {
|
|
99795
|
+
const issue = parseIssue(content);
|
|
99796
|
+
issues.push(issue);
|
|
99797
|
+
} catch (error) {
|
|
99798
|
+
console.warn(`Skipping invalid issue file: ${file}`, error);
|
|
99799
|
+
}
|
|
99776
99800
|
}
|
|
99777
99801
|
}
|
|
99778
99802
|
return issues;
|
|
@@ -99814,30 +99838,44 @@ async function listIssues(baseDir) {
|
|
|
99814
99838
|
* crashed and break the lock. This is a heuristic — safe when the critical
|
|
99815
99839
|
* section is short-lived (sub-second for file I/O).
|
|
99816
99840
|
*
|
|
99817
|
-
* ##
|
|
99841
|
+
* ## Failure on timeout
|
|
99842
|
+
*
|
|
99843
|
+
* If the lock cannot be acquired within the timeout, a LockAcquisitionError is
|
|
99844
|
+
* thrown. This prevents the dangerous "degraded mode" where the critical section
|
|
99845
|
+
* runs without mutual exclusion, which can cause data loss (e.g., lost ID
|
|
99846
|
+
* mappings during concurrent `tbd create`).
|
|
99818
99847
|
*
|
|
99819
|
-
*
|
|
99820
|
-
*
|
|
99821
|
-
* Callers should design their critical sections to be safe without the lock
|
|
99822
|
-
* (e.g., using read-merge-write for append-only data).
|
|
99848
|
+
* IMPORTANT: `timeoutMs` must be greater than `staleMs` so stale locks from
|
|
99849
|
+
* crashed processes are always detected and broken before the timeout expires.
|
|
99823
99850
|
*/
|
|
99824
|
-
const DEFAULT_TIMEOUT_MS =
|
|
99851
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
99825
99852
|
const DEFAULT_POLL_MS = 50;
|
|
99826
99853
|
const DEFAULT_STALE_MS = 5e3;
|
|
99827
99854
|
/**
|
|
99855
|
+
* Error thrown when the lock cannot be acquired within the timeout.
|
|
99856
|
+
*/
|
|
99857
|
+
var LockAcquisitionError = class extends Error {
|
|
99858
|
+
constructor(lockPath, timeoutMs) {
|
|
99859
|
+
super(`Failed to acquire lock at ${lockPath} within ${timeoutMs}ms. Another process may be holding the lock. If this persists, delete the lock directory manually and retry.`);
|
|
99860
|
+
this.name = "LockAcquisitionError";
|
|
99861
|
+
}
|
|
99862
|
+
};
|
|
99863
|
+
/**
|
|
99828
99864
|
* Execute `fn` while holding a lockfile.
|
|
99829
99865
|
*
|
|
99830
99866
|
* The lock is a directory at `lockPath` (typically `<target-file>.lock`).
|
|
99831
99867
|
* Concurrent callers will wait up to `timeoutMs` for the lock, polling
|
|
99832
99868
|
* every `pollMs`. Stale locks older than `staleMs` are broken automatically.
|
|
99833
99869
|
*
|
|
99834
|
-
* If the lock cannot be acquired
|
|
99835
|
-
* This ensures
|
|
99870
|
+
* If the lock cannot be acquired within the timeout, a LockAcquisitionError
|
|
99871
|
+
* is thrown. This ensures mutual exclusion is never silently bypassed, which
|
|
99872
|
+
* prevents data loss from concurrent writes.
|
|
99836
99873
|
*
|
|
99837
99874
|
* @param lockPath - Path to use as the lock directory (e.g., "/path/to/ids.yml.lock")
|
|
99838
99875
|
* @param fn - Critical section to execute under the lock
|
|
99839
99876
|
* @param options - Timing parameters for lock acquisition
|
|
99840
99877
|
* @returns The return value of `fn`
|
|
99878
|
+
* @throws LockAcquisitionError if the lock cannot be acquired within the timeout
|
|
99841
99879
|
*
|
|
99842
99880
|
* @example
|
|
99843
99881
|
* ```ts
|
|
@@ -99859,7 +99897,7 @@ async function withLockfile(lockPath, fn, options) {
|
|
|
99859
99897
|
acquired = true;
|
|
99860
99898
|
break;
|
|
99861
99899
|
} catch (error) {
|
|
99862
|
-
if (error.code !== "EEXIST")
|
|
99900
|
+
if (error.code !== "EEXIST") throw error;
|
|
99863
99901
|
try {
|
|
99864
99902
|
const lockStat = await stat(lockPath);
|
|
99865
99903
|
if (Date.now() - lockStat.mtimeMs > staleMs) {
|
|
@@ -99873,10 +99911,11 @@ async function withLockfile(lockPath, fn, options) {
|
|
|
99873
99911
|
}
|
|
99874
99912
|
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
99875
99913
|
}
|
|
99914
|
+
if (!acquired) throw new LockAcquisitionError(lockPath, timeoutMs);
|
|
99876
99915
|
try {
|
|
99877
99916
|
return await fn();
|
|
99878
99917
|
} finally {
|
|
99879
|
-
|
|
99918
|
+
try {
|
|
99880
99919
|
await rmdir(lockPath);
|
|
99881
99920
|
} catch {}
|
|
99882
99921
|
}
|
|
@@ -99984,6 +100023,7 @@ var id_mapping_exports = /* @__PURE__ */ __exportAll({
|
|
|
99984
100023
|
mergeIdMappings: () => mergeIdMappings,
|
|
99985
100024
|
parseIdMappingFromYaml: () => parseIdMappingFromYaml,
|
|
99986
100025
|
reconcileMappings: () => reconcileMappings,
|
|
100026
|
+
resolveIdMappingConflicts: () => resolveIdMappingConflicts,
|
|
99987
100027
|
resolveToInternalId: () => resolveToInternalId,
|
|
99988
100028
|
saveIdMapping: () => saveIdMapping
|
|
99989
100029
|
});
|
|
@@ -100033,8 +100073,8 @@ async function loadIdMapping(baseDir) {
|
|
|
100033
100073
|
* commands run in parallel.
|
|
100034
100074
|
*
|
|
100035
100075
|
* The merge is safe because ID mappings are append-only — entries are never
|
|
100036
|
-
* intentionally removed.
|
|
100037
|
-
*
|
|
100076
|
+
* intentionally removed. If the lock cannot be acquired within the timeout,
|
|
100077
|
+
* a LockAcquisitionError is thrown rather than proceeding without protection.
|
|
100038
100078
|
*/
|
|
100039
100079
|
async function saveIdMapping(baseDir, mapping) {
|
|
100040
100080
|
const filePath = getMappingPath(baseDir);
|
|
@@ -100247,6 +100287,43 @@ function mergeIdMappings(local, remote) {
|
|
|
100247
100287
|
}
|
|
100248
100288
|
return merged;
|
|
100249
100289
|
}
|
|
100290
|
+
/**
|
|
100291
|
+
* Resolve merge conflicts in ids.yml content by extracting both sides and merging.
|
|
100292
|
+
*
|
|
100293
|
+
* ids.yml is a sorted key-value YAML map where entries are append-only.
|
|
100294
|
+
* The most common merge conflict is both sides adding non-overlapping keys,
|
|
100295
|
+
* which is trivially auto-resolvable by keeping all entries from both sides.
|
|
100296
|
+
*
|
|
100297
|
+
* @param content - Raw file content that may contain git merge conflict markers
|
|
100298
|
+
* @returns Merged IdMapping with entries from both sides
|
|
100299
|
+
*/
|
|
100300
|
+
function resolveIdMappingConflicts(content) {
|
|
100301
|
+
if (!hasMergeConflictMarkers(content)) return parseIdMappingFromYaml(content);
|
|
100302
|
+
const lines = content.split("\n");
|
|
100303
|
+
const oursLines = [];
|
|
100304
|
+
const theirsLines = [];
|
|
100305
|
+
let inConflict = "none";
|
|
100306
|
+
for (const line of lines) {
|
|
100307
|
+
if (line.startsWith("<<<<<<< ")) {
|
|
100308
|
+
inConflict = "ours";
|
|
100309
|
+
continue;
|
|
100310
|
+
}
|
|
100311
|
+
if (line === "=======" && inConflict === "ours") {
|
|
100312
|
+
inConflict = "theirs";
|
|
100313
|
+
continue;
|
|
100314
|
+
}
|
|
100315
|
+
if (line.startsWith(">>>>>>> ") && inConflict === "theirs") {
|
|
100316
|
+
inConflict = "none";
|
|
100317
|
+
continue;
|
|
100318
|
+
}
|
|
100319
|
+
if (inConflict === "none") {
|
|
100320
|
+
oursLines.push(line);
|
|
100321
|
+
theirsLines.push(line);
|
|
100322
|
+
} else if (inConflict === "ours") oursLines.push(line);
|
|
100323
|
+
else theirsLines.push(line);
|
|
100324
|
+
}
|
|
100325
|
+
return mergeIdMappings(parseIdMappingFromYaml(oursLines.join("\n")), parseIdMappingFromYaml(theirsLines.join("\n")));
|
|
100326
|
+
}
|
|
100250
100327
|
|
|
100251
100328
|
//#endregion
|
|
100252
100329
|
//#region src/lib/priority.ts
|
|
@@ -101017,10 +101094,7 @@ function matchesSpecPath(storedPath, queryPath) {
|
|
|
101017
101094
|
if (!normalizedStored || !normalizedQuery) return false;
|
|
101018
101095
|
if (normalizedStored === normalizedQuery) return true;
|
|
101019
101096
|
if (normalizedStored.endsWith("/" + normalizedQuery)) return true;
|
|
101020
|
-
|
|
101021
|
-
const queryFilename = basename(normalizedQuery);
|
|
101022
|
-
if (!normalizedQuery.includes("/") && storedFilename === normalizedQuery) return true;
|
|
101023
|
-
if (!normalizedQuery.includes("/") && storedFilename === queryFilename) return true;
|
|
101097
|
+
if (!normalizedQuery.includes("/") && basename(normalizedStored) === normalizedQuery) return true;
|
|
101024
101098
|
return false;
|
|
101025
101099
|
}
|
|
101026
101100
|
/**
|
|
@@ -102365,7 +102439,7 @@ async function fetchWithGhFallback(url, options) {
|
|
|
102365
102439
|
* See: docs/project/specs/active/plan-2026-01-26-configurable-doc-cache-sync.md
|
|
102366
102440
|
*/
|
|
102367
102441
|
/** Prefix for internal bundled doc sources */
|
|
102368
|
-
const
|
|
102442
|
+
const INTERNAL_SOURCE_PREFIX = "internal:";
|
|
102369
102443
|
/**
|
|
102370
102444
|
* Syncs documentation files from configured sources.
|
|
102371
102445
|
*
|
|
@@ -102398,7 +102472,7 @@ var DocSync = class {
|
|
|
102398
102472
|
* // => { type: 'url', location: 'https://...' }
|
|
102399
102473
|
*/
|
|
102400
102474
|
parseSource(source) {
|
|
102401
|
-
if (source.startsWith(
|
|
102475
|
+
if (source.startsWith(INTERNAL_SOURCE_PREFIX)) return {
|
|
102402
102476
|
type: "internal",
|
|
102403
102477
|
location: source.slice(9)
|
|
102404
102478
|
};
|
|
@@ -102577,7 +102651,7 @@ async function generateDefaultDocCacheConfig() {
|
|
|
102577
102651
|
const entries = await readdir(fullDir, { withFileTypes: true });
|
|
102578
102652
|
for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
102579
102653
|
const relativePath = `${prefix}/${entry.name}`;
|
|
102580
|
-
config[relativePath] = `${
|
|
102654
|
+
config[relativePath] = `${INTERNAL_SOURCE_PREFIX}${relativePath}`;
|
|
102581
102655
|
}
|
|
102582
102656
|
} catch {}
|
|
102583
102657
|
}
|
|
@@ -102614,8 +102688,6 @@ function isDocsStale(lastSyncAt, autoSyncHours) {
|
|
|
102614
102688
|
const lastSync = new Date(lastSyncAt).getTime();
|
|
102615
102689
|
return (Date.now() - lastSync) / (1e3 * 60 * 60) >= autoSyncHours;
|
|
102616
102690
|
}
|
|
102617
|
-
/** Prefix for internal bundled doc sources */
|
|
102618
|
-
const INTERNAL_SOURCE_PREFIX = "internal:";
|
|
102619
102691
|
/**
|
|
102620
102692
|
* Check if an internal bundled doc exists.
|
|
102621
102693
|
*
|
|
@@ -103440,6 +103512,19 @@ var SyncHandler = class extends BaseCommand {
|
|
|
103440
103512
|
} catch {
|
|
103441
103513
|
this.output.debug("Remote sync branch does not exist yet");
|
|
103442
103514
|
}
|
|
103515
|
+
{
|
|
103516
|
+
const { access, writeFile } = await import("node:fs/promises");
|
|
103517
|
+
const attrPath = join(this.dataSyncDir, "mappings", ".gitattributes");
|
|
103518
|
+
try {
|
|
103519
|
+
await access(attrPath);
|
|
103520
|
+
} catch {
|
|
103521
|
+
await writeFile(attrPath, "ids.yml merge=union\n");
|
|
103522
|
+
await git("-C", worktreePath, "add", attrPath);
|
|
103523
|
+
try {
|
|
103524
|
+
await git("-C", worktreePath, "commit", "--no-verify", "-m", "chore: add merge=union for ids.yml");
|
|
103525
|
+
} catch {}
|
|
103526
|
+
}
|
|
103527
|
+
}
|
|
103443
103528
|
if (behindCommits > 0) {
|
|
103444
103529
|
let headBeforeMerge = "";
|
|
103445
103530
|
try {
|
|
@@ -103484,7 +103569,8 @@ var SyncHandler = class extends BaseCommand {
|
|
|
103484
103569
|
const remoteIdsContent = await git("show", `${remote}/${syncBranch}:${DATA_SYNC_DIR}/mappings/ids.yml`);
|
|
103485
103570
|
if (remoteIdsContent) {
|
|
103486
103571
|
conflictRemoteMapping = parseIdMappingFromYaml(remoteIdsContent);
|
|
103487
|
-
const
|
|
103572
|
+
const { readFile } = await import("node:fs/promises");
|
|
103573
|
+
const localMapping = resolveIdMappingConflicts(await readFile(join(this.dataSyncDir, "mappings", "ids.yml"), "utf-8"));
|
|
103488
103574
|
const mergedMapping = mergeIdMappings(localMapping, conflictRemoteMapping);
|
|
103489
103575
|
await saveIdMapping(this.dataSyncDir, mergedMapping);
|
|
103490
103576
|
this.output.debug(`Merged ID mappings: ${localMapping.shortToUlid.size} local + ${conflictRemoteMapping.shortToUlid.size} remote = ${mergedMapping.shortToUlid.size} total`);
|
|
@@ -104600,14 +104686,22 @@ var DoctorHandler = class extends BaseCommand {
|
|
|
104600
104686
|
healthChecks.push(await this.checkIssuesDirectory());
|
|
104601
104687
|
healthChecks.push(this.checkOrphanedDependencies(this.issues));
|
|
104602
104688
|
healthChecks.push(this.checkDuplicateIds(this.issues));
|
|
104689
|
+
healthChecks.push(await this.checkIdMappingConflicts(options.fix));
|
|
104603
104690
|
healthChecks.push(await this.checkIdMappingDuplicates(options.fix));
|
|
104604
104691
|
healthChecks.push(await this.checkTempFiles(options.fix));
|
|
104605
104692
|
healthChecks.push(this.checkIssueValidity(this.issues));
|
|
104693
|
+
healthChecks.push(await this.checkWorktree(options.fix));
|
|
104694
|
+
const dataLocationResult = await this.checkDataLocation(options.fix);
|
|
104695
|
+
healthChecks.push(dataLocationResult);
|
|
104696
|
+
if (dataLocationResult.status === "ok" && dataLocationResult.message?.includes("migrated")) {
|
|
104697
|
+
this.dataSyncDir = await resolveDataSyncDir(this.cwd);
|
|
104698
|
+
try {
|
|
104699
|
+
this.issues = await listIssues(this.dataSyncDir);
|
|
104700
|
+
} catch {}
|
|
104701
|
+
}
|
|
104606
104702
|
const parsedMaxHistory = options.maxHistory ? parseInt(options.maxHistory, 10) : 50;
|
|
104607
104703
|
const maxHistory = Number.isNaN(parsedMaxHistory) || parsedMaxHistory < 0 ? 50 : parsedMaxHistory;
|
|
104608
104704
|
healthChecks.push(await this.checkMissingMappings(options.fix, maxHistory));
|
|
104609
|
-
healthChecks.push(await this.checkWorktree(options.fix));
|
|
104610
|
-
healthChecks.push(await this.checkDataLocation(options.fix));
|
|
104611
104705
|
healthChecks.push(await this.checkLocalSyncBranch());
|
|
104612
104706
|
healthChecks.push(await this.checkRemoteSyncBranch());
|
|
104613
104707
|
healthChecks.push(await this.checkLocalVsRemoteData());
|
|
@@ -104805,6 +104899,58 @@ var DoctorHandler = class extends BaseCommand {
|
|
|
104805
104899
|
};
|
|
104806
104900
|
}
|
|
104807
104901
|
/**
|
|
104902
|
+
* Check 5b: Merge conflict markers in ids.yml.
|
|
104903
|
+
*
|
|
104904
|
+
* After a failed git merge during sync, ids.yml may retain unresolved
|
|
104905
|
+
* conflict markers (<<<<<<< / ======= / >>>>>>>). This blocks all tbd
|
|
104906
|
+
* commands since YAML parsing throws MergeConflictError.
|
|
104907
|
+
*
|
|
104908
|
+
* For ids.yml, both sides are simple key-value pairs that are append-only,
|
|
104909
|
+
* so the resolution is trivial: keep all entries from both sides.
|
|
104910
|
+
*
|
|
104911
|
+
* With --fix, extracts both sides, merges them, and re-saves.
|
|
104912
|
+
*/
|
|
104913
|
+
async checkIdMappingConflicts(fix) {
|
|
104914
|
+
const mappingPath = join(this.dataSyncDir, "mappings", "ids.yml");
|
|
104915
|
+
let content;
|
|
104916
|
+
try {
|
|
104917
|
+
content = await readFile(mappingPath, "utf-8");
|
|
104918
|
+
} catch {
|
|
104919
|
+
return {
|
|
104920
|
+
name: "ID mapping conflicts",
|
|
104921
|
+
status: "ok"
|
|
104922
|
+
};
|
|
104923
|
+
}
|
|
104924
|
+
const { hasMergeConflictMarkers } = await Promise.resolve().then(() => yaml_utils_exports);
|
|
104925
|
+
if (!hasMergeConflictMarkers(content)) return {
|
|
104926
|
+
name: "ID mapping conflicts",
|
|
104927
|
+
status: "ok"
|
|
104928
|
+
};
|
|
104929
|
+
if (fix && !this.checkDryRun("Resolve merge conflicts in ids.yml")) try {
|
|
104930
|
+
const { resolveIdMappingConflicts, saveIdMapping } = await Promise.resolve().then(() => id_mapping_exports);
|
|
104931
|
+
const resolved = resolveIdMappingConflicts(content);
|
|
104932
|
+
await saveIdMapping(this.dataSyncDir, resolved);
|
|
104933
|
+
return {
|
|
104934
|
+
name: "ID mapping conflicts",
|
|
104935
|
+
status: "ok",
|
|
104936
|
+
message: `resolved merge conflicts (${resolved.shortToUlid.size} entries)`
|
|
104937
|
+
};
|
|
104938
|
+
} catch (error) {
|
|
104939
|
+
return {
|
|
104940
|
+
name: "ID mapping conflicts",
|
|
104941
|
+
status: "error",
|
|
104942
|
+
message: `failed to resolve conflicts: ${error instanceof Error ? error.message : String(error)}`
|
|
104943
|
+
};
|
|
104944
|
+
}
|
|
104945
|
+
return {
|
|
104946
|
+
name: "ID mapping conflicts",
|
|
104947
|
+
status: "error",
|
|
104948
|
+
message: "ids.yml contains unresolved merge conflict markers",
|
|
104949
|
+
fixable: true,
|
|
104950
|
+
suggestion: "Run: tbd doctor --fix to auto-resolve"
|
|
104951
|
+
};
|
|
104952
|
+
}
|
|
104953
|
+
/**
|
|
104808
104954
|
* Check for duplicate keys in the ID mapping file (ids.yml).
|
|
104809
104955
|
*
|
|
104810
104956
|
* After a git merge conflict resolution that keeps entries from both sides,
|