get-tbd 0.1.25 → 0.1.27
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/dist/bin.mjs +253 -43
- package/dist/bin.mjs.map +1 -1
- package/dist/cli.mjs +196 -46
- package/dist/cli.mjs.map +1 -1
- package/dist/{config-CmEAGaxz.mjs → config-B38rbI9u.mjs} +3 -2
- package/dist/config-B38rbI9u.mjs.map +1 -0
- package/dist/{config-CB1tcqTZ.mjs → config-C0ITTrtc.mjs} +1 -1
- 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-BSNsaOCC.mjs → id-mapping-CqrrLgeX.mjs} +42 -4
- package/dist/{id-mapping-BSNsaOCC.mjs.map → id-mapping-CqrrLgeX.mjs.map} +1 -1
- package/dist/id-mapping-Ctfl_nc1.mjs +3 -0
- package/dist/index.d.mts +13 -1
- package/dist/index.mjs +3 -3
- package/dist/schemas-C8mOQykE.mjs +323 -0
- package/dist/schemas-C8mOQykE.mjs.map +1 -0
- package/dist/{src-DuXy2Uyd.mjs → src-BIE27KDA.mjs} +4 -3
- package/dist/{src-DuXy2Uyd.mjs.map → src-BIE27KDA.mjs.map} +1 -1
- package/dist/tbd +253 -43
- 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-DMMKwXZv.mjs +0 -3
- package/dist/yaml-utils-U7l9hhkh.mjs +0 -581
- package/dist/yaml-utils-U7l9hhkh.mjs.map +0 -1
package/dist/bin.mjs
CHANGED
|
@@ -6710,6 +6710,18 @@ const IssueKind = enumType([
|
|
|
6710
6710
|
"chore"
|
|
6711
6711
|
]);
|
|
6712
6712
|
/**
|
|
6713
|
+
* Maximum issue title length before detail belongs in the description body.
|
|
6714
|
+
*/
|
|
6715
|
+
const ISSUE_TITLE_MAX_LENGTH = 500;
|
|
6716
|
+
/**
|
|
6717
|
+
* Maximum issue body section length to keep issue files practical to review and sync.
|
|
6718
|
+
*/
|
|
6719
|
+
const ISSUE_BODY_MAX_LENGTH = 5e4;
|
|
6720
|
+
/**
|
|
6721
|
+
* Issue title text as persisted in issue files.
|
|
6722
|
+
*/
|
|
6723
|
+
const IssueTitle = stringType().min(1).max(ISSUE_TITLE_MAX_LENGTH);
|
|
6724
|
+
/**
|
|
6713
6725
|
* Priority: 0 (highest/critical) to 4 (lowest).
|
|
6714
6726
|
*/
|
|
6715
6727
|
const Priority = numberType().int().min(0).max(4);
|
|
@@ -6743,12 +6755,12 @@ const Dependency = objectType({
|
|
|
6743
6755
|
*/
|
|
6744
6756
|
const IssueSchema = BaseEntity.extend({
|
|
6745
6757
|
type: literalType("is"),
|
|
6746
|
-
title:
|
|
6758
|
+
title: IssueTitle,
|
|
6747
6759
|
kind: IssueKind.default("task"),
|
|
6748
6760
|
status: IssueStatus.default("open"),
|
|
6749
6761
|
priority: Priority.default(2),
|
|
6750
|
-
description: stringType().max(
|
|
6751
|
-
notes: stringType().max(
|
|
6762
|
+
description: stringType().max(ISSUE_BODY_MAX_LENGTH).nullable().optional(),
|
|
6763
|
+
notes: stringType().max(ISSUE_BODY_MAX_LENGTH).nullable().optional(),
|
|
6752
6764
|
spec_path: stringType().nullable().optional(),
|
|
6753
6765
|
assignee: stringType().nullable().optional(),
|
|
6754
6766
|
labels: arrayType(stringType()).default([]),
|
|
@@ -13735,6 +13747,18 @@ const ordering = {
|
|
|
13735
13747
|
* IMPORTANT: Always use these utilities instead of raw yaml package functions.
|
|
13736
13748
|
* This ensures consistent formatting and proper error handling across the codebase.
|
|
13737
13749
|
*/
|
|
13750
|
+
var yaml_utils_exports = /* @__PURE__ */ __exportAll({
|
|
13751
|
+
MergeConflictError: () => MergeConflictError,
|
|
13752
|
+
YAML_STRINGIFY_OPTIONS: () => YAML_STRINGIFY_OPTIONS,
|
|
13753
|
+
YAML_STRINGIFY_OPTIONS_COMPACT: () => YAML_STRINGIFY_OPTIONS_COMPACT,
|
|
13754
|
+
detectDuplicateYamlKeys: () => detectDuplicateYamlKeys,
|
|
13755
|
+
hasMergeConflictMarkers: () => hasMergeConflictMarkers,
|
|
13756
|
+
parseYamlToleratingDuplicateKeys: () => parseYamlToleratingDuplicateKeys,
|
|
13757
|
+
parseYamlWithConflictDetection: () => parseYamlWithConflictDetection,
|
|
13758
|
+
sortKeys: () => sortKeys,
|
|
13759
|
+
stringifyYaml: () => stringifyYaml,
|
|
13760
|
+
stringifyYamlCompact: () => stringifyYamlCompact
|
|
13761
|
+
});
|
|
13738
13762
|
/**
|
|
13739
13763
|
* Serialize data to YAML with readable formatting.
|
|
13740
13764
|
*
|
|
@@ -14033,7 +14057,7 @@ function serializeIssue(issue) {
|
|
|
14033
14057
|
* Package version, derived from git at build time.
|
|
14034
14058
|
* Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.
|
|
14035
14059
|
*/
|
|
14036
|
-
const VERSION$1 = "0.1.
|
|
14060
|
+
const VERSION$1 = "0.1.27";
|
|
14037
14061
|
|
|
14038
14062
|
//#endregion
|
|
14039
14063
|
//#region src/cli/lib/version.ts
|
|
@@ -99037,6 +99061,7 @@ async function initWorktree(baseDir, remote = "origin", syncBranch = SYNC_BRANCH
|
|
|
99037
99061
|
await writeFile(join(dataSyncPath, "meta.yml"), "schema_version: 1\n");
|
|
99038
99062
|
await writeFile(join(dataSyncPath, "issues", ".gitkeep"), "");
|
|
99039
99063
|
await writeFile(join(dataSyncPath, "mappings", ".gitkeep"), "");
|
|
99064
|
+
await writeFile(join(dataSyncPath, "mappings", ".gitattributes"), "ids.yml merge=union\n");
|
|
99040
99065
|
await git("-C", worktreePath, "add", ".");
|
|
99041
99066
|
await git("-C", worktreePath, "commit", "--no-verify", "-m", "Initialize tbd-sync branch");
|
|
99042
99067
|
return {
|
|
@@ -99274,9 +99299,16 @@ async function migrateDataToWorktree(baseDir, removeSource = false) {
|
|
|
99274
99299
|
await mkdir(correctMappingsPath, { recursive: true });
|
|
99275
99300
|
for (const file of issueFiles) await cp(join(wrongIssuesPath, file), join(correctIssuesPath, file));
|
|
99276
99301
|
for (const file of mappingFiles) if (file === "ids.yml") {
|
|
99277
|
-
const {
|
|
99278
|
-
const
|
|
99279
|
-
|
|
99302
|
+
const { readFile } = await import("node:fs/promises");
|
|
99303
|
+
const { loadIdMapping, mergeIdMappings, saveIdMapping, resolveIdMappingConflicts } = await Promise.resolve().then(() => id_mapping_exports);
|
|
99304
|
+
const sourceMapping = resolveIdMappingConflicts(await readFile(join(wrongMappingsPath, file), "utf-8"));
|
|
99305
|
+
let targetMapping;
|
|
99306
|
+
try {
|
|
99307
|
+
targetMapping = resolveIdMappingConflicts(await readFile(join(correctMappingsPath, file), "utf-8"));
|
|
99308
|
+
} catch {
|
|
99309
|
+
targetMapping = await loadIdMapping(correctPath);
|
|
99310
|
+
}
|
|
99311
|
+
await saveIdMapping(correctPath, mergeIdMappings(targetMapping, sourceMapping));
|
|
99280
99312
|
} else await cp(join(wrongMappingsPath, file), join(correctMappingsPath, file));
|
|
99281
99313
|
const totalFiles = issueFiles.length + mappingFiles.length;
|
|
99282
99314
|
await git("-C", worktreePath, "add", "-A");
|
|
@@ -99706,6 +99738,29 @@ function formatDebugId(internalId, mapping, prefix = "tbd") {
|
|
|
99706
99738
|
return `${formatDisplayId(internalId, mapping, prefix)} (${internalId})`;
|
|
99707
99739
|
}
|
|
99708
99740
|
|
|
99741
|
+
//#endregion
|
|
99742
|
+
//#region src/utils/zod-error-utils.ts
|
|
99743
|
+
/**
|
|
99744
|
+
* Helpers for rendering Zod errors without relying on object inspection.
|
|
99745
|
+
*/
|
|
99746
|
+
/**
|
|
99747
|
+
* Format a ZodError as concise path-qualified messages for CLI output.
|
|
99748
|
+
*/
|
|
99749
|
+
function formatZodError(error) {
|
|
99750
|
+
const messages = error.issues.map((issue) => {
|
|
99751
|
+
return `${issue.path.length > 0 ? issue.path.join(".") : "<root>"}: ${issue.message}`;
|
|
99752
|
+
});
|
|
99753
|
+
return messages.length > 0 ? messages.join("; ") : error.message;
|
|
99754
|
+
}
|
|
99755
|
+
/**
|
|
99756
|
+
* Format unknown thrown values as safe strings for warnings and diagnostics.
|
|
99757
|
+
*/
|
|
99758
|
+
function formatUnknownError(error) {
|
|
99759
|
+
if (error instanceof ZodError) return formatZodError(error);
|
|
99760
|
+
if (error instanceof Error) return error.message;
|
|
99761
|
+
return String(error);
|
|
99762
|
+
}
|
|
99763
|
+
|
|
99709
99764
|
//#endregion
|
|
99710
99765
|
//#region src/file/storage.ts
|
|
99711
99766
|
/**
|
|
@@ -99717,6 +99772,10 @@ function formatDebugId(internalId, mapping, prefix = "tbd") {
|
|
|
99717
99772
|
* See: tbd-design.md §3.2 Storage Layer
|
|
99718
99773
|
*/
|
|
99719
99774
|
/**
|
|
99775
|
+
* Maximum issue files read concurrently to avoid exhausting file descriptors in large repos.
|
|
99776
|
+
*/
|
|
99777
|
+
const ISSUE_READ_BATCH_SIZE = 200;
|
|
99778
|
+
/**
|
|
99720
99779
|
* Get the path to an issue file.
|
|
99721
99780
|
*/
|
|
99722
99781
|
function getIssuePath(baseDir, id) {
|
|
@@ -99734,7 +99793,8 @@ async function readIssue(baseDir, id) {
|
|
|
99734
99793
|
* Uses atomic write to prevent corruption.
|
|
99735
99794
|
*/
|
|
99736
99795
|
async function writeIssue(baseDir, issue) {
|
|
99737
|
-
|
|
99796
|
+
const validIssue = IssueSchema.parse(issue);
|
|
99797
|
+
await writeFile(getIssuePath(baseDir, validIssue.id), serializeIssue(validIssue));
|
|
99738
99798
|
}
|
|
99739
99799
|
/**
|
|
99740
99800
|
* List all issues in the worktree.
|
|
@@ -99742,7 +99802,8 @@ async function writeIssue(baseDir, issue) {
|
|
|
99742
99802
|
*
|
|
99743
99803
|
* Uses parallel file reading for better performance with many issues.
|
|
99744
99804
|
*/
|
|
99745
|
-
async function listIssues(baseDir) {
|
|
99805
|
+
async function listIssues(baseDir, options = {}) {
|
|
99806
|
+
const warnOnInvalid = options.warnOnInvalid ?? true;
|
|
99746
99807
|
const issuesDir = join(baseDir, "issues");
|
|
99747
99808
|
let files;
|
|
99748
99809
|
try {
|
|
@@ -99751,10 +99812,9 @@ async function listIssues(baseDir) {
|
|
|
99751
99812
|
return [];
|
|
99752
99813
|
}
|
|
99753
99814
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
99754
|
-
const BATCH_SIZE = 200;
|
|
99755
99815
|
const issues = [];
|
|
99756
|
-
for (let i = 0; i < mdFiles.length; i +=
|
|
99757
|
-
const batch = mdFiles.slice(i, i +
|
|
99816
|
+
for (let i = 0; i < mdFiles.length; i += ISSUE_READ_BATCH_SIZE) {
|
|
99817
|
+
const batch = mdFiles.slice(i, i + ISSUE_READ_BATCH_SIZE);
|
|
99758
99818
|
const fileContents = await Promise.all(batch.map(async (file) => {
|
|
99759
99819
|
const filePath = join(issuesDir, file);
|
|
99760
99820
|
try {
|
|
@@ -99762,25 +99822,38 @@ async function listIssues(baseDir) {
|
|
|
99762
99822
|
file,
|
|
99763
99823
|
content: await readFile(filePath, "utf-8")
|
|
99764
99824
|
};
|
|
99765
|
-
} catch {
|
|
99825
|
+
} catch (error) {
|
|
99766
99826
|
return {
|
|
99767
99827
|
file,
|
|
99768
|
-
|
|
99828
|
+
error: formatUnknownError(error)
|
|
99769
99829
|
};
|
|
99770
99830
|
}
|
|
99771
99831
|
}));
|
|
99772
|
-
for (const
|
|
99773
|
-
if (
|
|
99832
|
+
for (const result of fileContents) {
|
|
99833
|
+
if ("error" in result) {
|
|
99834
|
+
reportInvalidIssueFile({
|
|
99835
|
+
file: result.file,
|
|
99836
|
+
reason: `failed to read file: ${result.error}`
|
|
99837
|
+
}, warnOnInvalid, options.onInvalidIssue);
|
|
99838
|
+
continue;
|
|
99839
|
+
}
|
|
99774
99840
|
try {
|
|
99775
|
-
const issue = parseIssue(content);
|
|
99841
|
+
const issue = parseIssue(result.content);
|
|
99776
99842
|
issues.push(issue);
|
|
99777
99843
|
} catch (error) {
|
|
99778
|
-
|
|
99844
|
+
reportInvalidIssueFile({
|
|
99845
|
+
file: result.file,
|
|
99846
|
+
reason: formatUnknownError(error)
|
|
99847
|
+
}, warnOnInvalid, options.onInvalidIssue);
|
|
99779
99848
|
}
|
|
99780
99849
|
}
|
|
99781
99850
|
}
|
|
99782
99851
|
return issues;
|
|
99783
99852
|
}
|
|
99853
|
+
function reportInvalidIssueFile(invalidIssue, warnOnInvalid, onInvalidIssue) {
|
|
99854
|
+
onInvalidIssue?.(invalidIssue);
|
|
99855
|
+
if (warnOnInvalid) console.warn(`Skipping invalid issue file: ${invalidIssue.file}: ${invalidIssue.reason}`);
|
|
99856
|
+
}
|
|
99784
99857
|
|
|
99785
99858
|
//#endregion
|
|
99786
99859
|
//#region src/utils/lockfile.ts
|
|
@@ -99877,7 +99950,7 @@ async function withLockfile(lockPath, fn, options) {
|
|
|
99877
99950
|
acquired = true;
|
|
99878
99951
|
break;
|
|
99879
99952
|
} catch (error) {
|
|
99880
|
-
if (error.code !== "EEXIST")
|
|
99953
|
+
if (error.code !== "EEXIST") throw error;
|
|
99881
99954
|
try {
|
|
99882
99955
|
const lockStat = await stat(lockPath);
|
|
99883
99956
|
if (Date.now() - lockStat.mtimeMs > staleMs) {
|
|
@@ -100003,6 +100076,7 @@ var id_mapping_exports = /* @__PURE__ */ __exportAll({
|
|
|
100003
100076
|
mergeIdMappings: () => mergeIdMappings,
|
|
100004
100077
|
parseIdMappingFromYaml: () => parseIdMappingFromYaml,
|
|
100005
100078
|
reconcileMappings: () => reconcileMappings,
|
|
100079
|
+
resolveIdMappingConflicts: () => resolveIdMappingConflicts,
|
|
100006
100080
|
resolveToInternalId: () => resolveToInternalId,
|
|
100007
100081
|
saveIdMapping: () => saveIdMapping
|
|
100008
100082
|
});
|
|
@@ -100266,6 +100340,43 @@ function mergeIdMappings(local, remote) {
|
|
|
100266
100340
|
}
|
|
100267
100341
|
return merged;
|
|
100268
100342
|
}
|
|
100343
|
+
/**
|
|
100344
|
+
* Resolve merge conflicts in ids.yml content by extracting both sides and merging.
|
|
100345
|
+
*
|
|
100346
|
+
* ids.yml is a sorted key-value YAML map where entries are append-only.
|
|
100347
|
+
* The most common merge conflict is both sides adding non-overlapping keys,
|
|
100348
|
+
* which is trivially auto-resolvable by keeping all entries from both sides.
|
|
100349
|
+
*
|
|
100350
|
+
* @param content - Raw file content that may contain git merge conflict markers
|
|
100351
|
+
* @returns Merged IdMapping with entries from both sides
|
|
100352
|
+
*/
|
|
100353
|
+
function resolveIdMappingConflicts(content) {
|
|
100354
|
+
if (!hasMergeConflictMarkers(content)) return parseIdMappingFromYaml(content);
|
|
100355
|
+
const lines = content.split("\n");
|
|
100356
|
+
const oursLines = [];
|
|
100357
|
+
const theirsLines = [];
|
|
100358
|
+
let inConflict = "none";
|
|
100359
|
+
for (const line of lines) {
|
|
100360
|
+
if (line.startsWith("<<<<<<< ")) {
|
|
100361
|
+
inConflict = "ours";
|
|
100362
|
+
continue;
|
|
100363
|
+
}
|
|
100364
|
+
if (line === "=======" && inConflict === "ours") {
|
|
100365
|
+
inConflict = "theirs";
|
|
100366
|
+
continue;
|
|
100367
|
+
}
|
|
100368
|
+
if (line.startsWith(">>>>>>> ") && inConflict === "theirs") {
|
|
100369
|
+
inConflict = "none";
|
|
100370
|
+
continue;
|
|
100371
|
+
}
|
|
100372
|
+
if (inConflict === "none") {
|
|
100373
|
+
oursLines.push(line);
|
|
100374
|
+
theirsLines.push(line);
|
|
100375
|
+
} else if (inConflict === "ours") oursLines.push(line);
|
|
100376
|
+
else theirsLines.push(line);
|
|
100377
|
+
}
|
|
100378
|
+
return mergeIdMappings(parseIdMappingFromYaml(oursLines.join("\n")), parseIdMappingFromYaml(theirsLines.join("\n")));
|
|
100379
|
+
}
|
|
100269
100380
|
|
|
100270
100381
|
//#endregion
|
|
100271
100382
|
//#region src/lib/priority.ts
|
|
@@ -100458,6 +100569,22 @@ async function resolveAndValidatePath(inputPath, projectRoot, cwd) {
|
|
|
100458
100569
|
return resolved;
|
|
100459
100570
|
}
|
|
100460
100571
|
|
|
100572
|
+
//#endregion
|
|
100573
|
+
//#region src/cli/lib/issue-input-validation.ts
|
|
100574
|
+
/**
|
|
100575
|
+
* CLI validation helpers for user-provided issue fields.
|
|
100576
|
+
*/
|
|
100577
|
+
/**
|
|
100578
|
+
* Validate a CLI-provided issue title with actionable user-facing errors.
|
|
100579
|
+
*/
|
|
100580
|
+
function validateIssueTitle(title, options) {
|
|
100581
|
+
if (options.rejectBlank ? title.trim().length === 0 : title.length === 0) throw new ValidationError(options.emptyMessage);
|
|
100582
|
+
if (title.length > ISSUE_TITLE_MAX_LENGTH) throw new ValidationError(`Title is too long (${title.length} chars, max ${ISSUE_TITLE_MAX_LENGTH}). Move detail into the description body.`);
|
|
100583
|
+
const result = IssueTitle.safeParse(title);
|
|
100584
|
+
if (!result.success) throw new ValidationError(`Invalid title: ${formatZodError(result.error)}`);
|
|
100585
|
+
return title;
|
|
100586
|
+
}
|
|
100587
|
+
|
|
100461
100588
|
//#endregion
|
|
100462
100589
|
//#region src/cli/commands/create.ts
|
|
100463
100590
|
/**
|
|
@@ -100469,6 +100596,10 @@ var CreateHandler = class extends BaseCommand {
|
|
|
100469
100596
|
async run(title, options) {
|
|
100470
100597
|
const tbdRoot = await requireInit();
|
|
100471
100598
|
if (!title && !options.fromFile) throw new ValidationError("Title is required. Use: tbd create \"Issue title\"");
|
|
100599
|
+
const validatedTitle = title === void 0 ? void 0 : validateIssueTitle(title, {
|
|
100600
|
+
emptyMessage: "Title is required. Use: tbd create \"Issue title\"",
|
|
100601
|
+
rejectBlank: true
|
|
100602
|
+
});
|
|
100472
100603
|
const kind = this.parseKind(options.type ?? "task");
|
|
100473
100604
|
const priority = this.validatePriority(options.priority ?? "2");
|
|
100474
100605
|
let description = options.description;
|
|
@@ -100485,7 +100616,7 @@ var CreateHandler = class extends BaseCommand {
|
|
|
100485
100616
|
throw new ValidationError(getPathErrorMessage(error));
|
|
100486
100617
|
}
|
|
100487
100618
|
if (this.checkDryRun("Would create issue", {
|
|
100488
|
-
title,
|
|
100619
|
+
title: validatedTitle,
|
|
100489
100620
|
kind,
|
|
100490
100621
|
priority,
|
|
100491
100622
|
spec: specPath,
|
|
@@ -100517,7 +100648,7 @@ var CreateHandler = class extends BaseCommand {
|
|
|
100517
100648
|
type: "is",
|
|
100518
100649
|
id,
|
|
100519
100650
|
version: 1,
|
|
100520
|
-
title,
|
|
100651
|
+
title: validatedTitle,
|
|
100521
100652
|
kind,
|
|
100522
100653
|
status: "open",
|
|
100523
100654
|
priority,
|
|
@@ -100549,9 +100680,9 @@ var CreateHandler = class extends BaseCommand {
|
|
|
100549
100680
|
this.output.data({
|
|
100550
100681
|
id: displayId,
|
|
100551
100682
|
internalId: id,
|
|
100552
|
-
title
|
|
100683
|
+
title: validatedTitle
|
|
100553
100684
|
}, () => {
|
|
100554
|
-
this.output.success(`Created ${displayId}: ${
|
|
100685
|
+
this.output.success(`Created ${displayId}: ${validatedTitle}`);
|
|
100555
100686
|
});
|
|
100556
100687
|
}
|
|
100557
100688
|
parseKind(value) {
|
|
@@ -101036,10 +101167,7 @@ function matchesSpecPath(storedPath, queryPath) {
|
|
|
101036
101167
|
if (!normalizedStored || !normalizedQuery) return false;
|
|
101037
101168
|
if (normalizedStored === normalizedQuery) return true;
|
|
101038
101169
|
if (normalizedStored.endsWith("/" + normalizedQuery)) return true;
|
|
101039
|
-
|
|
101040
|
-
const queryFilename = basename(normalizedQuery);
|
|
101041
|
-
if (!normalizedQuery.includes("/") && storedFilename === normalizedQuery) return true;
|
|
101042
|
-
if (!normalizedQuery.includes("/") && storedFilename === queryFilename) return true;
|
|
101170
|
+
if (!normalizedQuery.includes("/") && basename(normalizedStored) === normalizedQuery) return true;
|
|
101043
101171
|
return false;
|
|
101044
101172
|
}
|
|
101045
101173
|
/**
|
|
@@ -101370,7 +101498,10 @@ var UpdateHandler = class extends BaseCommand {
|
|
|
101370
101498
|
}
|
|
101371
101499
|
try {
|
|
101372
101500
|
const { frontmatter, description, notes } = parseMarkdownWithFrontmatter(content);
|
|
101373
|
-
if (typeof frontmatter.title === "string") updates.title = frontmatter.title
|
|
101501
|
+
if (typeof frontmatter.title === "string") updates.title = validateIssueTitle(frontmatter.title, {
|
|
101502
|
+
emptyMessage: "Title cannot be empty",
|
|
101503
|
+
rejectBlank: true
|
|
101504
|
+
});
|
|
101374
101505
|
if (typeof frontmatter.status === "string") {
|
|
101375
101506
|
const result = IssueStatus.safeParse(frontmatter.status);
|
|
101376
101507
|
if (result.success) updates.status = result.data;
|
|
@@ -101401,10 +101532,10 @@ var UpdateHandler = class extends BaseCommand {
|
|
|
101401
101532
|
}
|
|
101402
101533
|
return updates;
|
|
101403
101534
|
}
|
|
101404
|
-
if (options.title !== void 0) {
|
|
101405
|
-
|
|
101406
|
-
|
|
101407
|
-
}
|
|
101535
|
+
if (options.title !== void 0) updates.title = validateIssueTitle(options.title, {
|
|
101536
|
+
emptyMessage: "Title cannot be empty",
|
|
101537
|
+
rejectBlank: true
|
|
101538
|
+
});
|
|
101408
101539
|
if (options.status) {
|
|
101409
101540
|
const result = IssueStatus.safeParse(options.status);
|
|
101410
101541
|
if (!result.success) throw new ValidationError(`Invalid status: ${options.status}`);
|
|
@@ -102384,7 +102515,7 @@ async function fetchWithGhFallback(url, options) {
|
|
|
102384
102515
|
* See: docs/project/specs/active/plan-2026-01-26-configurable-doc-cache-sync.md
|
|
102385
102516
|
*/
|
|
102386
102517
|
/** Prefix for internal bundled doc sources */
|
|
102387
|
-
const
|
|
102518
|
+
const INTERNAL_SOURCE_PREFIX = "internal:";
|
|
102388
102519
|
/**
|
|
102389
102520
|
* Syncs documentation files from configured sources.
|
|
102390
102521
|
*
|
|
@@ -102417,7 +102548,7 @@ var DocSync = class {
|
|
|
102417
102548
|
* // => { type: 'url', location: 'https://...' }
|
|
102418
102549
|
*/
|
|
102419
102550
|
parseSource(source) {
|
|
102420
|
-
if (source.startsWith(
|
|
102551
|
+
if (source.startsWith(INTERNAL_SOURCE_PREFIX)) return {
|
|
102421
102552
|
type: "internal",
|
|
102422
102553
|
location: source.slice(9)
|
|
102423
102554
|
};
|
|
@@ -102596,7 +102727,7 @@ async function generateDefaultDocCacheConfig() {
|
|
|
102596
102727
|
const entries = await readdir(fullDir, { withFileTypes: true });
|
|
102597
102728
|
for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
102598
102729
|
const relativePath = `${prefix}/${entry.name}`;
|
|
102599
|
-
config[relativePath] = `${
|
|
102730
|
+
config[relativePath] = `${INTERNAL_SOURCE_PREFIX}${relativePath}`;
|
|
102600
102731
|
}
|
|
102601
102732
|
} catch {}
|
|
102602
102733
|
}
|
|
@@ -102633,8 +102764,6 @@ function isDocsStale(lastSyncAt, autoSyncHours) {
|
|
|
102633
102764
|
const lastSync = new Date(lastSyncAt).getTime();
|
|
102634
102765
|
return (Date.now() - lastSync) / (1e3 * 60 * 60) >= autoSyncHours;
|
|
102635
102766
|
}
|
|
102636
|
-
/** Prefix for internal bundled doc sources */
|
|
102637
|
-
const INTERNAL_SOURCE_PREFIX = "internal:";
|
|
102638
102767
|
/**
|
|
102639
102768
|
* Check if an internal bundled doc exists.
|
|
102640
102769
|
*
|
|
@@ -103459,6 +103588,19 @@ var SyncHandler = class extends BaseCommand {
|
|
|
103459
103588
|
} catch {
|
|
103460
103589
|
this.output.debug("Remote sync branch does not exist yet");
|
|
103461
103590
|
}
|
|
103591
|
+
{
|
|
103592
|
+
const { access, writeFile } = await import("node:fs/promises");
|
|
103593
|
+
const attrPath = join(this.dataSyncDir, "mappings", ".gitattributes");
|
|
103594
|
+
try {
|
|
103595
|
+
await access(attrPath);
|
|
103596
|
+
} catch {
|
|
103597
|
+
await writeFile(attrPath, "ids.yml merge=union\n");
|
|
103598
|
+
await git("-C", worktreePath, "add", attrPath);
|
|
103599
|
+
try {
|
|
103600
|
+
await git("-C", worktreePath, "commit", "--no-verify", "-m", "chore: add merge=union for ids.yml");
|
|
103601
|
+
} catch {}
|
|
103602
|
+
}
|
|
103603
|
+
}
|
|
103462
103604
|
if (behindCommits > 0) {
|
|
103463
103605
|
let headBeforeMerge = "";
|
|
103464
103606
|
try {
|
|
@@ -103503,7 +103645,8 @@ var SyncHandler = class extends BaseCommand {
|
|
|
103503
103645
|
const remoteIdsContent = await git("show", `${remote}/${syncBranch}:${DATA_SYNC_DIR}/mappings/ids.yml`);
|
|
103504
103646
|
if (remoteIdsContent) {
|
|
103505
103647
|
conflictRemoteMapping = parseIdMappingFromYaml(remoteIdsContent);
|
|
103506
|
-
const
|
|
103648
|
+
const { readFile } = await import("node:fs/promises");
|
|
103649
|
+
const localMapping = resolveIdMappingConflicts(await readFile(join(this.dataSyncDir, "mappings", "ids.yml"), "utf-8"));
|
|
103507
103650
|
const mergedMapping = mergeIdMappings(localMapping, conflictRemoteMapping);
|
|
103508
103651
|
await saveIdMapping(this.dataSyncDir, mergedMapping);
|
|
103509
103652
|
this.output.debug(`Merged ID mappings: ${localMapping.shortToUlid.size} local + ${conflictRemoteMapping.shortToUlid.size} remote = ${mergedMapping.shortToUlid.size} total`);
|
|
@@ -104601,6 +104744,7 @@ var DoctorHandler = class extends BaseCommand {
|
|
|
104601
104744
|
cwd = "";
|
|
104602
104745
|
config = null;
|
|
104603
104746
|
issues = [];
|
|
104747
|
+
invalidIssueFiles = [];
|
|
104604
104748
|
async run(options) {
|
|
104605
104749
|
const tbdRoot = await requireInit();
|
|
104606
104750
|
this.cwd = tbdRoot;
|
|
@@ -104609,7 +104753,11 @@ var DoctorHandler = class extends BaseCommand {
|
|
|
104609
104753
|
this.config = await readConfig(this.cwd);
|
|
104610
104754
|
} catch {}
|
|
104611
104755
|
try {
|
|
104612
|
-
this.
|
|
104756
|
+
this.invalidIssueFiles = [];
|
|
104757
|
+
this.issues = await listIssues(this.dataSyncDir, {
|
|
104758
|
+
warnOnInvalid: false,
|
|
104759
|
+
onInvalidIssue: (invalidIssue) => this.invalidIssueFiles.push(invalidIssue)
|
|
104760
|
+
});
|
|
104613
104761
|
} catch {}
|
|
104614
104762
|
const statusInfo = await this.gatherStatusInfo();
|
|
104615
104763
|
const statsInfo = await this.gatherStatsInfo();
|
|
@@ -104619,16 +104767,21 @@ var DoctorHandler = class extends BaseCommand {
|
|
|
104619
104767
|
healthChecks.push(await this.checkIssuesDirectory());
|
|
104620
104768
|
healthChecks.push(this.checkOrphanedDependencies(this.issues));
|
|
104621
104769
|
healthChecks.push(this.checkDuplicateIds(this.issues));
|
|
104770
|
+
healthChecks.push(await this.checkIdMappingConflicts(options.fix));
|
|
104622
104771
|
healthChecks.push(await this.checkIdMappingDuplicates(options.fix));
|
|
104623
104772
|
healthChecks.push(await this.checkTempFiles(options.fix));
|
|
104624
|
-
healthChecks.push(this.checkIssueValidity(this.issues));
|
|
104773
|
+
healthChecks.push(this.checkIssueValidity(this.issues, this.invalidIssueFiles));
|
|
104625
104774
|
healthChecks.push(await this.checkWorktree(options.fix));
|
|
104626
104775
|
const dataLocationResult = await this.checkDataLocation(options.fix);
|
|
104627
104776
|
healthChecks.push(dataLocationResult);
|
|
104628
104777
|
if (dataLocationResult.status === "ok" && dataLocationResult.message?.includes("migrated")) {
|
|
104629
104778
|
this.dataSyncDir = await resolveDataSyncDir(this.cwd);
|
|
104630
104779
|
try {
|
|
104631
|
-
this.
|
|
104780
|
+
this.invalidIssueFiles = [];
|
|
104781
|
+
this.issues = await listIssues(this.dataSyncDir, {
|
|
104782
|
+
warnOnInvalid: false,
|
|
104783
|
+
onInvalidIssue: (invalidIssue) => this.invalidIssueFiles.push(invalidIssue)
|
|
104784
|
+
});
|
|
104632
104785
|
} catch {}
|
|
104633
104786
|
}
|
|
104634
104787
|
const parsedMaxHistory = options.maxHistory ? parseInt(options.maxHistory, 10) : 50;
|
|
@@ -104831,6 +104984,58 @@ var DoctorHandler = class extends BaseCommand {
|
|
|
104831
104984
|
};
|
|
104832
104985
|
}
|
|
104833
104986
|
/**
|
|
104987
|
+
* Check 5b: Merge conflict markers in ids.yml.
|
|
104988
|
+
*
|
|
104989
|
+
* After a failed git merge during sync, ids.yml may retain unresolved
|
|
104990
|
+
* conflict markers (<<<<<<< / ======= / >>>>>>>). This blocks all tbd
|
|
104991
|
+
* commands since YAML parsing throws MergeConflictError.
|
|
104992
|
+
*
|
|
104993
|
+
* For ids.yml, both sides are simple key-value pairs that are append-only,
|
|
104994
|
+
* so the resolution is trivial: keep all entries from both sides.
|
|
104995
|
+
*
|
|
104996
|
+
* With --fix, extracts both sides, merges them, and re-saves.
|
|
104997
|
+
*/
|
|
104998
|
+
async checkIdMappingConflicts(fix) {
|
|
104999
|
+
const mappingPath = join(this.dataSyncDir, "mappings", "ids.yml");
|
|
105000
|
+
let content;
|
|
105001
|
+
try {
|
|
105002
|
+
content = await readFile(mappingPath, "utf-8");
|
|
105003
|
+
} catch {
|
|
105004
|
+
return {
|
|
105005
|
+
name: "ID mapping conflicts",
|
|
105006
|
+
status: "ok"
|
|
105007
|
+
};
|
|
105008
|
+
}
|
|
105009
|
+
const { hasMergeConflictMarkers } = await Promise.resolve().then(() => yaml_utils_exports);
|
|
105010
|
+
if (!hasMergeConflictMarkers(content)) return {
|
|
105011
|
+
name: "ID mapping conflicts",
|
|
105012
|
+
status: "ok"
|
|
105013
|
+
};
|
|
105014
|
+
if (fix && !this.checkDryRun("Resolve merge conflicts in ids.yml")) try {
|
|
105015
|
+
const { resolveIdMappingConflicts, saveIdMapping } = await Promise.resolve().then(() => id_mapping_exports);
|
|
105016
|
+
const resolved = resolveIdMappingConflicts(content);
|
|
105017
|
+
await saveIdMapping(this.dataSyncDir, resolved);
|
|
105018
|
+
return {
|
|
105019
|
+
name: "ID mapping conflicts",
|
|
105020
|
+
status: "ok",
|
|
105021
|
+
message: `resolved merge conflicts (${resolved.shortToUlid.size} entries)`
|
|
105022
|
+
};
|
|
105023
|
+
} catch (error) {
|
|
105024
|
+
return {
|
|
105025
|
+
name: "ID mapping conflicts",
|
|
105026
|
+
status: "error",
|
|
105027
|
+
message: `failed to resolve conflicts: ${error instanceof Error ? error.message : String(error)}`
|
|
105028
|
+
};
|
|
105029
|
+
}
|
|
105030
|
+
return {
|
|
105031
|
+
name: "ID mapping conflicts",
|
|
105032
|
+
status: "error",
|
|
105033
|
+
message: "ids.yml contains unresolved merge conflict markers",
|
|
105034
|
+
fixable: true,
|
|
105035
|
+
suggestion: "Run: tbd doctor --fix to auto-resolve"
|
|
105036
|
+
};
|
|
105037
|
+
}
|
|
105038
|
+
/**
|
|
104834
105039
|
* Check for duplicate keys in the ID mapping file (ids.yml).
|
|
104835
105040
|
*
|
|
104836
105041
|
* After a git merge conflict resolution that keeps entries from both sides,
|
|
@@ -104919,8 +105124,12 @@ var DoctorHandler = class extends BaseCommand {
|
|
|
104919
105124
|
suggestion: "Run: tbd doctor --fix"
|
|
104920
105125
|
};
|
|
104921
105126
|
}
|
|
104922
|
-
checkIssueValidity(issues) {
|
|
105127
|
+
checkIssueValidity(issues, invalidIssueFiles) {
|
|
104923
105128
|
const invalid = [];
|
|
105129
|
+
for (const invalidIssueFile of invalidIssueFiles) invalid.push({
|
|
105130
|
+
id: invalidIssueFile.file,
|
|
105131
|
+
reason: invalidIssueFile.reason
|
|
105132
|
+
});
|
|
104924
105133
|
for (const issue of issues) {
|
|
104925
105134
|
const issueId = issue.id ?? "unknown";
|
|
104926
105135
|
if (!issue.id) {
|
|
@@ -104970,7 +105179,8 @@ var DoctorHandler = class extends BaseCommand {
|
|
|
104970
105179
|
return {
|
|
104971
105180
|
name: "Issue validity",
|
|
104972
105181
|
status: "error",
|
|
104973
|
-
message: `${invalid.length} invalid issue(s)`,
|
|
105182
|
+
message: `${invalid.length} invalid issue file(s)`,
|
|
105183
|
+
path: join(CONFIG_DIR, "issues"),
|
|
104974
105184
|
details: invalid.map((i) => `${i.id}: ${i.reason}`),
|
|
104975
105185
|
suggestion: "Manually fix or delete invalid issue files"
|
|
104976
105186
|
};
|