get-tbd 0.1.20 → 0.1.21
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 +4 -0
- package/dist/bin.mjs +250 -108
- package/dist/bin.mjs.map +1 -1
- package/dist/cli.mjs +64 -97
- package/dist/cli.mjs.map +1 -1
- package/dist/docs/README.md +4 -0
- package/dist/docs/guidelines/tbd-sync-troubleshooting.md +29 -0
- package/dist/docs/tbd-design.md +8 -0
- package/dist/docs/tbd-docs.md +2 -1
- package/dist/{id-mapping-tbhnzNON.mjs → id-mapping-BqSnxlxk.mjs} +1 -1
- package/dist/{id-mapping-D6mJ1bjS.mjs → id-mapping-CD5c_ZVA.mjs} +5 -4
- package/dist/id-mapping-CD5c_ZVA.mjs.map +1 -0
- package/dist/index.d.mts +34 -8
- package/dist/index.mjs +3 -3
- package/dist/{src-BiKxOaNe.mjs → src-BjMRpmMh.mjs} +6 -7
- package/dist/{src-BiKxOaNe.mjs.map → src-BjMRpmMh.mjs.map} +1 -1
- package/dist/tbd +250 -108
- package/dist/{yaml-utils-pmgpOtBk.mjs → yaml-utils-x_kr2IId.mjs} +154 -6
- package/dist/yaml-utils-x_kr2IId.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/id-mapping-D6mJ1bjS.mjs.map +0 -1
- package/dist/yaml-utils-pmgpOtBk.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# tbd
|
|
2
2
|
|
|
3
|
+
[](https://x.com/ojoshe)
|
|
4
|
+
[](https://github.com/jlevy/tbd/actions/workflows/ci.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/get-tbd)
|
|
6
|
+
|
|
3
7
|
**Task tracking, spec-driven planning, and knowledge injection for AI coding agents.**
|
|
4
8
|
|
|
5
9
|
**tbd** (short for “To Be Done,” or “TypeScript beads” if you prefer) combines four
|
package/dist/bin.mjs
CHANGED
|
@@ -6727,17 +6727,23 @@ const Dependency = objectType({
|
|
|
6727
6727
|
/**
|
|
6728
6728
|
* Full issue schema.
|
|
6729
6729
|
*
|
|
6730
|
+
* Field order is canonical and mirrored by ISSUE_FIELD_ORDER below:
|
|
6731
|
+
* type, id, title, kind, status, priority, version (the "header seven"),
|
|
6732
|
+
* then linkages, assignment, hierarchy, scheduling, provenance,
|
|
6733
|
+
* timestamps, lifecycle, and extensions.
|
|
6734
|
+
*
|
|
6730
6735
|
* Note: Fields use .nullable() in addition to .optional() because
|
|
6731
6736
|
* YAML parses `field: null` as JavaScript null, not undefined.
|
|
6732
6737
|
*/
|
|
6733
6738
|
const IssueSchema = BaseEntity.extend({
|
|
6734
6739
|
type: literalType("is"),
|
|
6735
6740
|
title: stringType().min(1).max(500),
|
|
6736
|
-
description: stringType().max(5e4).nullable().optional(),
|
|
6737
|
-
notes: stringType().max(5e4).nullable().optional(),
|
|
6738
6741
|
kind: IssueKind.default("task"),
|
|
6739
6742
|
status: IssueStatus.default("open"),
|
|
6740
6743
|
priority: Priority.default(2),
|
|
6744
|
+
description: stringType().max(5e4).nullable().optional(),
|
|
6745
|
+
notes: stringType().max(5e4).nullable().optional(),
|
|
6746
|
+
spec_path: stringType().nullable().optional(),
|
|
6741
6747
|
assignee: stringType().nullable().optional(),
|
|
6742
6748
|
labels: arrayType(stringType()).default([]),
|
|
6743
6749
|
dependencies: arrayType(Dependency).default([]),
|
|
@@ -6747,8 +6753,7 @@ const IssueSchema = BaseEntity.extend({
|
|
|
6747
6753
|
deferred_until: Timestamp.nullable().optional(),
|
|
6748
6754
|
created_by: stringType().nullable().optional(),
|
|
6749
6755
|
closed_at: Timestamp.nullable().optional(),
|
|
6750
|
-
close_reason: stringType().nullable().optional()
|
|
6751
|
-
spec_path: stringType().nullable().optional()
|
|
6756
|
+
close_reason: stringType().nullable().optional()
|
|
6752
6757
|
});
|
|
6753
6758
|
/**
|
|
6754
6759
|
* Git branch name - restricted to safe characters.
|
|
@@ -6862,6 +6867,64 @@ const AtticEntrySchema = objectType({
|
|
|
6862
6867
|
* Format: { "a7k2": "01hx5zzkbkactav9wevgemmvrz", ... }
|
|
6863
6868
|
*/
|
|
6864
6869
|
const IdMappingYamlSchema = recordType(ShortId, Ulid);
|
|
6870
|
+
/**
|
|
6871
|
+
* Canonical field order for issue YAML frontmatter.
|
|
6872
|
+
* (description and notes are body content, not frontmatter)
|
|
6873
|
+
*/
|
|
6874
|
+
const ISSUE_FIELD_ORDER = [
|
|
6875
|
+
"type",
|
|
6876
|
+
"id",
|
|
6877
|
+
"title",
|
|
6878
|
+
"kind",
|
|
6879
|
+
"status",
|
|
6880
|
+
"priority",
|
|
6881
|
+
"version",
|
|
6882
|
+
"spec_path",
|
|
6883
|
+
"assignee",
|
|
6884
|
+
"labels",
|
|
6885
|
+
"dependencies",
|
|
6886
|
+
"parent_id",
|
|
6887
|
+
"child_order_hints",
|
|
6888
|
+
"due_date",
|
|
6889
|
+
"deferred_until",
|
|
6890
|
+
"created_by",
|
|
6891
|
+
"created_at",
|
|
6892
|
+
"updated_at",
|
|
6893
|
+
"closed_at",
|
|
6894
|
+
"close_reason",
|
|
6895
|
+
"extensions"
|
|
6896
|
+
];
|
|
6897
|
+
/**
|
|
6898
|
+
* Canonical field order for config YAML.
|
|
6899
|
+
*/
|
|
6900
|
+
const CONFIG_FIELD_ORDER = [
|
|
6901
|
+
"tbd_format",
|
|
6902
|
+
"tbd_version",
|
|
6903
|
+
"display",
|
|
6904
|
+
"sync",
|
|
6905
|
+
"settings",
|
|
6906
|
+
"docs_cache"
|
|
6907
|
+
];
|
|
6908
|
+
/**
|
|
6909
|
+
* Canonical field order for attic entry YAML.
|
|
6910
|
+
*/
|
|
6911
|
+
const ATTIC_ENTRY_FIELD_ORDER = [
|
|
6912
|
+
"entity_id",
|
|
6913
|
+
"timestamp",
|
|
6914
|
+
"field",
|
|
6915
|
+
"lost_value",
|
|
6916
|
+
"winner_source",
|
|
6917
|
+
"loser_source",
|
|
6918
|
+
"context"
|
|
6919
|
+
];
|
|
6920
|
+
/**
|
|
6921
|
+
* Canonical field order for local state YAML.
|
|
6922
|
+
*/
|
|
6923
|
+
const LOCAL_STATE_FIELD_ORDER = [
|
|
6924
|
+
"last_sync_at",
|
|
6925
|
+
"last_doc_sync_at",
|
|
6926
|
+
"welcome_seen"
|
|
6927
|
+
];
|
|
6865
6928
|
|
|
6866
6929
|
//#endregion
|
|
6867
6930
|
//#region src/lib/types.ts
|
|
@@ -13583,6 +13646,76 @@ const PAGINATION_LINE_THRESHOLD = 50;
|
|
|
13583
13646
|
*/
|
|
13584
13647
|
const PARENT_CONTEXT_MAX_LINES = 50;
|
|
13585
13648
|
|
|
13649
|
+
//#endregion
|
|
13650
|
+
//#region src/lib/comparison-chain.ts
|
|
13651
|
+
const defaultCompare = (a, b) => {
|
|
13652
|
+
if (typeof a === "string" && typeof b === "string") return a.localeCompare(b);
|
|
13653
|
+
if (a < b) return -1;
|
|
13654
|
+
if (a > b) return 1;
|
|
13655
|
+
return 0;
|
|
13656
|
+
};
|
|
13657
|
+
const nullLastCompare = (a, b) => {
|
|
13658
|
+
if (a == null) return b == null ? 0 : 1;
|
|
13659
|
+
if (b == null) return -1;
|
|
13660
|
+
return defaultCompare(a, b);
|
|
13661
|
+
};
|
|
13662
|
+
const nullFirstCompare = (a, b) => {
|
|
13663
|
+
if (a == null) return b == null ? 0 : -1;
|
|
13664
|
+
if (b == null) return 1;
|
|
13665
|
+
return defaultCompare(a, b);
|
|
13666
|
+
};
|
|
13667
|
+
/**
|
|
13668
|
+
* A Google Guava-style comparison chain to make complex sorting, such as secondary
|
|
13669
|
+
* sorts, significantly easier.
|
|
13670
|
+
*
|
|
13671
|
+
* For example:
|
|
13672
|
+
*
|
|
13673
|
+
* items.sort(comparisonChain<Item>()
|
|
13674
|
+
* .compare(item => item.title, ordering.nullsLast)
|
|
13675
|
+
* .compare(item => item.url)
|
|
13676
|
+
* .result());
|
|
13677
|
+
*/
|
|
13678
|
+
const comparisonChain = () => {
|
|
13679
|
+
let compare = () => 0;
|
|
13680
|
+
const chain = {
|
|
13681
|
+
compare: (selector, comparator = defaultCompare) => {
|
|
13682
|
+
const prevCompare = compare;
|
|
13683
|
+
compare = (a, b) => prevCompare(a, b) || comparator(selector(a), selector(b));
|
|
13684
|
+
return chain;
|
|
13685
|
+
},
|
|
13686
|
+
result: () => compare
|
|
13687
|
+
};
|
|
13688
|
+
return chain;
|
|
13689
|
+
};
|
|
13690
|
+
/**
|
|
13691
|
+
* Reverse a comparator's ordering.
|
|
13692
|
+
*/
|
|
13693
|
+
const reverse = (comparator) => (a, b) => comparator(b, a);
|
|
13694
|
+
/**
|
|
13695
|
+
* Create a comparator that sorts values in a manually specified order.
|
|
13696
|
+
* Values not in the order array are placed at the end.
|
|
13697
|
+
*/
|
|
13698
|
+
const manualOrderComparator = (order) => {
|
|
13699
|
+
const orderMap = new Map(order.map((value, index) => [value, index]));
|
|
13700
|
+
return (a, b) => {
|
|
13701
|
+
const indexA = orderMap.get(a);
|
|
13702
|
+
const indexB = orderMap.get(b);
|
|
13703
|
+
if (indexA === void 0) return indexB === void 0 ? 0 : 1;
|
|
13704
|
+
if (indexB === void 0) return -1;
|
|
13705
|
+
return indexA - indexB;
|
|
13706
|
+
};
|
|
13707
|
+
};
|
|
13708
|
+
/**
|
|
13709
|
+
* Common ordering strategies for use with comparisonChain.
|
|
13710
|
+
*/
|
|
13711
|
+
const ordering = {
|
|
13712
|
+
nullsLast: nullLastCompare,
|
|
13713
|
+
nullsFirst: nullFirstCompare,
|
|
13714
|
+
default: defaultCompare,
|
|
13715
|
+
reversed: reverse(defaultCompare),
|
|
13716
|
+
manual: manualOrderComparator
|
|
13717
|
+
};
|
|
13718
|
+
|
|
13586
13719
|
//#endregion
|
|
13587
13720
|
//#region src/utils/yaml-utils.ts
|
|
13588
13721
|
/**
|
|
@@ -13626,6 +13759,17 @@ function stringifyYamlCompact(data) {
|
|
|
13626
13759
|
return (0, import_dist$2.stringify)(data, YAML_STRINGIFY_OPTIONS_COMPACT);
|
|
13627
13760
|
}
|
|
13628
13761
|
/**
|
|
13762
|
+
* Create a new object with keys sorted according to a manual field ordering.
|
|
13763
|
+
* Uses ordering.manual() from comparison-chain.ts, so fields not in the
|
|
13764
|
+
* order array are placed at the end.
|
|
13765
|
+
*/
|
|
13766
|
+
function sortKeys(obj, fieldOrder) {
|
|
13767
|
+
const keyComparator = ordering.manual(fieldOrder);
|
|
13768
|
+
const sorted = {};
|
|
13769
|
+
for (const key of Object.keys(obj).sort(keyComparator)) sorted[key] = obj[key];
|
|
13770
|
+
return sorted;
|
|
13771
|
+
}
|
|
13772
|
+
/**
|
|
13629
13773
|
* Error thrown when YAML content contains unresolved merge conflict markers.
|
|
13630
13774
|
*/
|
|
13631
13775
|
var MergeConflictError = class extends Error {
|
|
@@ -13858,13 +14002,12 @@ function parseIssue(content) {
|
|
|
13858
14002
|
*/
|
|
13859
14003
|
function serializeIssue(issue) {
|
|
13860
14004
|
const { description, notes, ...metadata } = issue;
|
|
13861
|
-
const sortedMetadata = {};
|
|
13862
|
-
for (const key of Object.keys(metadata).sort()) sortedMetadata[key] = metadata[key];
|
|
13863
14005
|
const parts = [
|
|
13864
14006
|
"---",
|
|
13865
|
-
stringifyYaml(
|
|
14007
|
+
stringifyYaml(sortKeys(metadata, ISSUE_FIELD_ORDER), {
|
|
13866
14008
|
lineWidth: 0,
|
|
13867
|
-
nullStr: "null"
|
|
14009
|
+
nullStr: "null",
|
|
14010
|
+
sortMapEntries: false
|
|
13868
14011
|
}).trim(),
|
|
13869
14012
|
"---"
|
|
13870
14013
|
];
|
|
@@ -13884,7 +14027,7 @@ function serializeIssue(issue) {
|
|
|
13884
14027
|
* Package version, derived from git at build time.
|
|
13885
14028
|
* Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.
|
|
13886
14029
|
*/
|
|
13887
|
-
const VERSION$1 = "0.1.
|
|
14030
|
+
const VERSION$1 = "0.1.21";
|
|
13888
14031
|
|
|
13889
14032
|
//#endregion
|
|
13890
14033
|
//#region src/cli/lib/version.ts
|
|
@@ -97806,7 +97949,10 @@ async function readConfigWithMigration(baseDir) {
|
|
|
97806
97949
|
*/
|
|
97807
97950
|
async function writeConfig(baseDir, config) {
|
|
97808
97951
|
const configPath = join(baseDir, CONFIG_FILE);
|
|
97809
|
-
let content = stringifyYaml(config, {
|
|
97952
|
+
let content = stringifyYaml(sortKeys(config, CONFIG_FIELD_ORDER), {
|
|
97953
|
+
lineWidth: 0,
|
|
97954
|
+
sortMapEntries: false
|
|
97955
|
+
});
|
|
97810
97956
|
if (config.docs_cache && Object.keys(config.docs_cache).length > 0) content = content.replace("docs_cache:", "# Documentation cache configuration.\n# files: Maps destination paths (relative to .tbd/docs/) to source locations.\n# Sources can be:\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# lookup_path: Search paths for doc lookup (like shell $PATH). Earlier paths take precedence.\n#\n# To sync docs: tbd sync --docs\n# To check status: tbd sync --status\n#\n# Auto-sync: Docs are automatically synced when stale (default: every 24 hours).\n# Configure with settings.doc_auto_sync_hours (0 = disabled).\ndocs_cache:");
|
|
97811
97957
|
await writeFile(configPath, content);
|
|
97812
97958
|
}
|
|
@@ -97879,7 +98025,10 @@ async function writeLocalState(baseDir, state) {
|
|
|
97879
98025
|
} catch {
|
|
97880
98026
|
throw new Error(`Cannot write state: .tbd/ directory does not exist at ${baseDir}. Run 'tbd init' first or ensure the correct tbd root is being used.`);
|
|
97881
98027
|
}
|
|
97882
|
-
await writeFile(join(baseDir, STATE_FILE), stringifyYaml(state, {
|
|
98028
|
+
await writeFile(join(baseDir, STATE_FILE), stringifyYaml(sortKeys(state, LOCAL_STATE_FIELD_ORDER), {
|
|
98029
|
+
lineWidth: 0,
|
|
98030
|
+
sortMapEntries: false
|
|
98031
|
+
}));
|
|
97883
98032
|
}
|
|
97884
98033
|
/**
|
|
97885
98034
|
* Update specific fields in local state (merge with existing).
|
|
@@ -98489,6 +98638,26 @@ const FIELD_STRATEGIES = {
|
|
|
98489
98638
|
extensions: "lww"
|
|
98490
98639
|
};
|
|
98491
98640
|
/**
|
|
98641
|
+
* Fields that are metadata-only and should be ignored when checking
|
|
98642
|
+
* for substantive changes between issues. These fields change on every
|
|
98643
|
+
* merge operation and don't represent meaningful content changes.
|
|
98644
|
+
*/
|
|
98645
|
+
const METADATA_ONLY_FIELDS = new Set(["version", "updated_at"]);
|
|
98646
|
+
/**
|
|
98647
|
+
* Check if two issues are substantively equal, ignoring metadata fields
|
|
98648
|
+
* (version, updated_at) that change on every merge.
|
|
98649
|
+
*
|
|
98650
|
+
* This prevents trivial timestamp/version bumps from being treated as
|
|
98651
|
+
* real changes during outbox saves and sync operations.
|
|
98652
|
+
*/
|
|
98653
|
+
function issuesSubstantivelyEqual(a, b) {
|
|
98654
|
+
for (const key of Object.keys(FIELD_STRATEGIES)) {
|
|
98655
|
+
if (METADATA_ONLY_FIELDS.has(key)) continue;
|
|
98656
|
+
if (!deepEqual(a[key], b[key])) return false;
|
|
98657
|
+
}
|
|
98658
|
+
return true;
|
|
98659
|
+
}
|
|
98660
|
+
/**
|
|
98492
98661
|
* Deep equality check for values.
|
|
98493
98662
|
*/
|
|
98494
98663
|
function deepEqual(a, b) {
|
|
@@ -98596,6 +98765,11 @@ function mergeIssues(base, local, remote) {
|
|
|
98596
98765
|
break;
|
|
98597
98766
|
}
|
|
98598
98767
|
}
|
|
98768
|
+
const latest = local.version >= remote.version ? local : remote;
|
|
98769
|
+
if (issuesSubstantivelyEqual(merged, latest)) return {
|
|
98770
|
+
merged: { ...latest },
|
|
98771
|
+
conflicts
|
|
98772
|
+
};
|
|
98599
98773
|
merged.version = Math.max(local.version, remote.version) + 1;
|
|
98600
98774
|
merged.updated_at = now();
|
|
98601
98775
|
return {
|
|
@@ -99233,6 +99407,26 @@ function randomChar(prng) {
|
|
|
99233
99407
|
const randomPosition = Math.floor(prng() * ENCODING_LEN) % ENCODING_LEN;
|
|
99234
99408
|
return ENCODING.charAt(randomPosition);
|
|
99235
99409
|
}
|
|
99410
|
+
function replaceCharAt(str, index, char) {
|
|
99411
|
+
if (index > str.length - 1) return str;
|
|
99412
|
+
return str.substr(0, index) + char + str.substr(index + 1);
|
|
99413
|
+
}
|
|
99414
|
+
function incrementBase32(str) {
|
|
99415
|
+
let done = void 0, index = str.length, char, charIndex, output = str;
|
|
99416
|
+
const maxCharIndex = ENCODING_LEN - 1;
|
|
99417
|
+
while (!done && index-- >= 0) {
|
|
99418
|
+
char = output[index];
|
|
99419
|
+
charIndex = ENCODING.indexOf(char);
|
|
99420
|
+
if (charIndex === -1) throw new ULIDError(ULIDErrorCode.Base32IncorrectEncoding, "Incorrectly encoded string");
|
|
99421
|
+
if (charIndex === maxCharIndex) {
|
|
99422
|
+
output = replaceCharAt(output, index, ENCODING[0]);
|
|
99423
|
+
continue;
|
|
99424
|
+
}
|
|
99425
|
+
done = replaceCharAt(output, index, ENCODING[charIndex + 1]);
|
|
99426
|
+
}
|
|
99427
|
+
if (typeof done === "string") return done;
|
|
99428
|
+
throw new ULIDError(ULIDErrorCode.Base32IncorrectEncoding, "Failed incrementing string");
|
|
99429
|
+
}
|
|
99236
99430
|
/**
|
|
99237
99431
|
* Detect the best PRNG (pseudo-random number generator)
|
|
99238
99432
|
* @param root The root to check from (global/window)
|
|
@@ -99285,16 +99479,27 @@ function inWebWorker() {
|
|
|
99285
99479
|
return typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope;
|
|
99286
99480
|
}
|
|
99287
99481
|
/**
|
|
99288
|
-
*
|
|
99289
|
-
*
|
|
99290
|
-
* @param prng
|
|
99291
|
-
* @returns A
|
|
99482
|
+
* Create a ULID factory to generate monotonically-increasing
|
|
99483
|
+
* ULIDs
|
|
99484
|
+
* @param prng The PRNG to use
|
|
99485
|
+
* @returns A ulid factory
|
|
99292
99486
|
* @example
|
|
99487
|
+
* const ulid = monotonicFactory();
|
|
99293
99488
|
* ulid(); // "01HNZXD07M5CEN5XA66EMZSRZW"
|
|
99294
99489
|
*/
|
|
99295
|
-
function
|
|
99490
|
+
function monotonicFactory(prng) {
|
|
99296
99491
|
const currentPRNG = prng || detectPRNG();
|
|
99297
|
-
|
|
99492
|
+
let lastTime = 0, lastRandom;
|
|
99493
|
+
return function _ulid(seedTime) {
|
|
99494
|
+
const seed = !seedTime || isNaN(seedTime) ? Date.now() : seedTime;
|
|
99495
|
+
if (seed <= lastTime) {
|
|
99496
|
+
const incrementedRandom = lastRandom = incrementBase32(lastRandom);
|
|
99497
|
+
return encodeTime(lastTime, TIME_LEN) + incrementedRandom;
|
|
99498
|
+
}
|
|
99499
|
+
lastTime = seed;
|
|
99500
|
+
const newRandom = lastRandom = encodeRandom(RANDOM_LEN, currentPRNG);
|
|
99501
|
+
return encodeTime(seed, TIME_LEN) + newRandom;
|
|
99502
|
+
};
|
|
99298
99503
|
}
|
|
99299
99504
|
|
|
99300
99505
|
//#endregion
|
|
@@ -99310,6 +99515,7 @@ function ulid(seedTime, prng) {
|
|
|
99310
99515
|
*
|
|
99311
99516
|
* See: tbd-design.md §2.5 ID Generation
|
|
99312
99517
|
*/
|
|
99518
|
+
const ulid = monotonicFactory();
|
|
99313
99519
|
/**
|
|
99314
99520
|
* Cast a string to InternalIssueId after validation.
|
|
99315
99521
|
* Use this when you've validated that a string is a valid internal ID.
|
|
@@ -100214,76 +100420,6 @@ async function loadFullContext(command) {
|
|
|
100214
100420
|
};
|
|
100215
100421
|
}
|
|
100216
100422
|
|
|
100217
|
-
//#endregion
|
|
100218
|
-
//#region src/lib/comparison-chain.ts
|
|
100219
|
-
const defaultCompare = (a, b) => {
|
|
100220
|
-
if (typeof a === "string" && typeof b === "string") return a.localeCompare(b);
|
|
100221
|
-
if (a < b) return -1;
|
|
100222
|
-
if (a > b) return 1;
|
|
100223
|
-
return 0;
|
|
100224
|
-
};
|
|
100225
|
-
const nullLastCompare = (a, b) => {
|
|
100226
|
-
if (a == null) return b == null ? 0 : 1;
|
|
100227
|
-
if (b == null) return -1;
|
|
100228
|
-
return defaultCompare(a, b);
|
|
100229
|
-
};
|
|
100230
|
-
const nullFirstCompare = (a, b) => {
|
|
100231
|
-
if (a == null) return b == null ? 0 : -1;
|
|
100232
|
-
if (b == null) return 1;
|
|
100233
|
-
return defaultCompare(a, b);
|
|
100234
|
-
};
|
|
100235
|
-
/**
|
|
100236
|
-
* A Google Guava-style comparison chain to make complex sorting, such as secondary
|
|
100237
|
-
* sorts, significantly easier.
|
|
100238
|
-
*
|
|
100239
|
-
* For example:
|
|
100240
|
-
*
|
|
100241
|
-
* items.sort(comparisonChain<Item>()
|
|
100242
|
-
* .compare(item => item.title, ordering.nullsLast)
|
|
100243
|
-
* .compare(item => item.url)
|
|
100244
|
-
* .result());
|
|
100245
|
-
*/
|
|
100246
|
-
const comparisonChain = () => {
|
|
100247
|
-
let compare = () => 0;
|
|
100248
|
-
const chain = {
|
|
100249
|
-
compare: (selector, comparator = defaultCompare) => {
|
|
100250
|
-
const prevCompare = compare;
|
|
100251
|
-
compare = (a, b) => prevCompare(a, b) || comparator(selector(a), selector(b));
|
|
100252
|
-
return chain;
|
|
100253
|
-
},
|
|
100254
|
-
result: () => compare
|
|
100255
|
-
};
|
|
100256
|
-
return chain;
|
|
100257
|
-
};
|
|
100258
|
-
/**
|
|
100259
|
-
* Reverse a comparator's ordering.
|
|
100260
|
-
*/
|
|
100261
|
-
const reverse = (comparator) => (a, b) => comparator(b, a);
|
|
100262
|
-
/**
|
|
100263
|
-
* Create a comparator that sorts values in a manually specified order.
|
|
100264
|
-
* Values not in the order array are placed at the end.
|
|
100265
|
-
*/
|
|
100266
|
-
const manualOrderComparator = (order) => {
|
|
100267
|
-
const orderMap = new Map(order.map((value, index) => [value, index]));
|
|
100268
|
-
return (a, b) => {
|
|
100269
|
-
const indexA = orderMap.get(a);
|
|
100270
|
-
const indexB = orderMap.get(b);
|
|
100271
|
-
if (indexA === void 0) return indexB === void 0 ? 0 : 1;
|
|
100272
|
-
if (indexB === void 0) return -1;
|
|
100273
|
-
return indexA - indexB;
|
|
100274
|
-
};
|
|
100275
|
-
};
|
|
100276
|
-
/**
|
|
100277
|
-
* Common ordering strategies for use with comparisonChain.
|
|
100278
|
-
*/
|
|
100279
|
-
const ordering = {
|
|
100280
|
-
nullsLast: nullLastCompare,
|
|
100281
|
-
nullsFirst: nullFirstCompare,
|
|
100282
|
-
default: defaultCompare,
|
|
100283
|
-
reversed: reverse(defaultCompare),
|
|
100284
|
-
manual: manualOrderComparator
|
|
100285
|
-
};
|
|
100286
|
-
|
|
100287
100423
|
//#endregion
|
|
100288
100424
|
//#region src/lib/status.ts
|
|
100289
100425
|
/**
|
|
@@ -100788,14 +100924,10 @@ var ListHandler = class extends BaseCommand {
|
|
|
100788
100924
|
return true;
|
|
100789
100925
|
});
|
|
100790
100926
|
}
|
|
100791
|
-
sortIssues(issues, sortField,
|
|
100792
|
-
const getShortId = (issue) => {
|
|
100793
|
-
const ulid = extractUlidFromInternalId(issue.id);
|
|
100794
|
-
return mapping.ulidToShort.get(ulid) ?? ulid;
|
|
100795
|
-
};
|
|
100927
|
+
sortIssues(issues, sortField, _mapping) {
|
|
100796
100928
|
const primarySelector = sortField === "created" ? (i) => new Date(i.created_at).getTime() : sortField === "updated" ? (i) => new Date(i.updated_at).getTime() : (i) => i.priority;
|
|
100797
100929
|
const primaryOrdering = sortField === "created" || sortField === "updated" ? ordering.reversed : ordering.default;
|
|
100798
|
-
return [...issues].sort(comparisonChain().compare(primarySelector, primaryOrdering).compare(
|
|
100930
|
+
return [...issues].sort(comparisonChain().compare(primarySelector, primaryOrdering).compare((i) => extractUlidFromInternalId(i.id)).result());
|
|
100799
100931
|
}
|
|
100800
100932
|
};
|
|
100801
100933
|
const listCommand = new Command("list").description("List issues").option("--status <status>", "Filter: open, in_progress, blocked, deferred, closed").option("--all", "Include closed issues").option("--type <type>", "Filter: bug, feature, task, epic").option("--priority <0-4>", "Filter by priority").option("--assignee <name>", "Filter by assignee").option("--label <label>", "Filter by label (repeatable)", (val, prev = []) => [...prev, val]).option("--parent <id>", "List children of parent").option("--spec <path>", "Filter by spec path (matches full path, partial path suffix, or filename)").option("--deferred", "Show only deferred issues").option("--defer-before <date>", "Deferred before date").option("--sort <field>", "Sort by: priority, created, updated", "priority").option("--limit <n>", "Limit results").option("--count", "Output only the count of matching issues").option("--long", "Show descriptions").option("--pretty", "Show tree view with parent-child relationships").option("--specs", "Group output by linked spec").action(async (options, command) => {
|
|
@@ -102380,11 +102512,15 @@ async function syncDocsWithDefaults(tbdRoot, options = {}) {
|
|
|
102380
102512
|
*
|
|
102381
102513
|
* An issue is considered "updated" if:
|
|
102382
102514
|
* - It doesn't exist in the remote (new issue)
|
|
102383
|
-
* - Its content differs from the remote version (modified issue)
|
|
102515
|
+
* - Its substantive content differs from the remote version (modified issue)
|
|
102516
|
+
*
|
|
102517
|
+
* Uses issuesSubstantivelyEqual to ignore metadata-only changes (version, updated_at)
|
|
102518
|
+
* that don't represent meaningful content changes. This prevents trivial timestamp
|
|
102519
|
+
* bumps from causing thousands of issues to be saved to the outbox.
|
|
102384
102520
|
*
|
|
102385
102521
|
* @param localIssues - Issues from the local worktree
|
|
102386
102522
|
* @param remoteIssues - Issues from the remote tbd-sync branch
|
|
102387
|
-
* @returns Issues that are new or modified compared to remote
|
|
102523
|
+
* @returns Issues that are new or substantively modified compared to remote
|
|
102388
102524
|
*/
|
|
102389
102525
|
function getUpdatedIssues(localIssues, remoteIssues) {
|
|
102390
102526
|
const remoteById = /* @__PURE__ */ new Map();
|
|
@@ -102392,7 +102528,7 @@ function getUpdatedIssues(localIssues, remoteIssues) {
|
|
|
102392
102528
|
return localIssues.filter((local) => {
|
|
102393
102529
|
const remote = remoteById.get(local.id);
|
|
102394
102530
|
if (!remote) return true;
|
|
102395
|
-
return !
|
|
102531
|
+
return !issuesSubstantivelyEqual(local, remote);
|
|
102396
102532
|
});
|
|
102397
102533
|
}
|
|
102398
102534
|
/**
|
|
@@ -102447,7 +102583,7 @@ async function saveConflictToAttic(atticDir, conflict, winnerSource) {
|
|
|
102447
102583
|
}
|
|
102448
102584
|
};
|
|
102449
102585
|
const safeTimestamp = timestamp.replace(/:/g, "-");
|
|
102450
|
-
await writeFile(join(atticDir, `${conflict.issue_id}_${safeTimestamp}_${conflict.field}.yml`), stringifyYaml(entry));
|
|
102586
|
+
await writeFile(join(atticDir, `${conflict.issue_id}_${safeTimestamp}_${conflict.field}.yml`), stringifyYaml(sortKeys(entry, ATTIC_ENTRY_FIELD_ORDER), { sortMapEntries: false }));
|
|
102451
102587
|
}
|
|
102452
102588
|
/**
|
|
102453
102589
|
* Get the target/source directory for workspace operations.
|
|
@@ -102493,17 +102629,23 @@ async function saveToWorkspace(tbdRoot, dataSyncDir, options) {
|
|
|
102493
102629
|
log.info(`Loaded ${totalSource} issue(s) from worktree`);
|
|
102494
102630
|
let sourceIssues = allSourceIssues;
|
|
102495
102631
|
const isUpdatesOnly = options.updatesOnly ?? options.outbox;
|
|
102496
|
-
if (isUpdatesOnly)
|
|
102497
|
-
|
|
102498
|
-
|
|
102499
|
-
|
|
102500
|
-
|
|
102501
|
-
|
|
102502
|
-
|
|
102503
|
-
|
|
102504
|
-
|
|
102505
|
-
|
|
102506
|
-
|
|
102632
|
+
if (isUpdatesOnly) {
|
|
102633
|
+
try {
|
|
102634
|
+
log.progress("Fetching remote for comparison...");
|
|
102635
|
+
await git("-C", tbdRoot, "fetch", "origin", "tbd-sync");
|
|
102636
|
+
log.debug("Fetch succeeded");
|
|
102637
|
+
} catch (fetchError) {
|
|
102638
|
+
const fetchMsg = fetchError instanceof Error ? fetchError.message : String(fetchError);
|
|
102639
|
+
log.info(`Fetch failed (${fetchMsg}), using cached remote state for comparison`);
|
|
102640
|
+
}
|
|
102641
|
+
try {
|
|
102642
|
+
const remoteIssues = await readRemoteIssues(tbdRoot, "origin", "tbd-sync");
|
|
102643
|
+
log.info(`Loaded ${remoteIssues.length} remote issue(s) for comparison`);
|
|
102644
|
+
sourceIssues = getUpdatedIssues(allSourceIssues, remoteIssues);
|
|
102645
|
+
log.info(`Filtered to ${sourceIssues.length} updated issue(s) (of ${totalSource} total)`);
|
|
102646
|
+
} catch {
|
|
102647
|
+
log.info(`No remote state available, falling back to saving all ${totalSource} issues`);
|
|
102648
|
+
}
|
|
102507
102649
|
}
|
|
102508
102650
|
let saved = 0;
|
|
102509
102651
|
let conflicts = 0;
|