@thxgg/steward 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.
Files changed (203) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/{Bc2V3wPK.js → B2ow85x_.js} +2 -2
  3. package/.output/public/_nuxt/{U78rMDmo.js → B6CbIr08.js} +1 -1
  4. package/.output/public/_nuxt/{BknRrWsw.js → BLQAF8wp.js} +1 -1
  5. package/.output/public/_nuxt/BXuwtOqb.js +1 -0
  6. package/.output/public/_nuxt/CAMiEhze.js +1 -0
  7. package/.output/public/_nuxt/{BRQ9Cxaw.js → CIBCqZF5.js} +1 -1
  8. package/.output/public/_nuxt/Ce0-nlm9.js +1 -0
  9. package/.output/public/_nuxt/{T11EuTtn.js → ConzneVY.js} +1 -1
  10. package/.output/public/_nuxt/D0qxz_Pn.js +1310 -0
  11. package/.output/public/_nuxt/D3PDtLSF.js +3 -0
  12. package/.output/public/_nuxt/{C73kduX-.js → DdKC0UAK.js} +1 -1
  13. package/.output/public/_nuxt/Detail.BGdvrJGh.css +1 -0
  14. package/.output/public/_nuxt/{C53_p0K1.js → Dkh9ic1y.js} +1 -1
  15. package/.output/public/_nuxt/LEjJTR7-.js +1 -0
  16. package/.output/public/_nuxt/{BTmXUZ_s.js → UqZfMfrZ.js} +1 -1
  17. package/.output/public/_nuxt/builds/latest.json +1 -1
  18. package/.output/public/_nuxt/builds/meta/25438e34-19a2-421d-aede-53fd18f1ccd4.json +1 -0
  19. package/.output/public/_nuxt/dckrK0oj.js +1 -0
  20. package/.output/public/_nuxt/entry.DT4p6_uW.css +1 -0
  21. package/.output/public/_nuxt/pIWeVmPw.js +1 -0
  22. package/.output/public/_nuxt/xrHaPo1U.js +60 -0
  23. package/.output/server/chunks/_/prd-service.mjs.map +1 -1
  24. package/.output/server/chunks/build/{Detail-DMMUwTWr.mjs → Detail-rpcemNXe.mjs} +674 -481
  25. package/.output/server/chunks/build/Detail-rpcemNXe.mjs.map +1 -0
  26. package/.output/server/chunks/build/DiffViewer-styles.B1FB5NJj.mjs +8 -0
  27. package/.output/server/chunks/build/DiffViewer-styles.B1FB5NJj.mjs.map +1 -0
  28. package/.output/server/chunks/build/{_prd_-ByugK4Yi.mjs → _prd_-CeibvZOH.mjs} +67 -233
  29. package/.output/server/chunks/build/_prd_-CeibvZOH.mjs.map +1 -0
  30. package/.output/server/chunks/build/client.precomputed.mjs +1 -1
  31. package/.output/server/chunks/build/{default-BKKgG7HJ.mjs → default-iq8SaDDN.mjs} +3 -3
  32. package/.output/server/chunks/build/default-iq8SaDDN.mjs.map +1 -0
  33. package/.output/server/chunks/build/{error-404-Bf6kdO80.mjs → error-404-DFale9A5.mjs} +2 -2
  34. package/.output/server/chunks/build/error-404-DFale9A5.mjs.map +1 -0
  35. package/.output/server/chunks/build/{index-DE1tjHAd.mjs → index-Po00RvHm.mjs} +2 -2
  36. package/.output/server/chunks/build/index-Po00RvHm.mjs.map +1 -0
  37. package/.output/server/chunks/build/{nuxt-link-SvT1nf8Z.mjs → nuxt-link-B4oWFn7n.mjs} +2 -2
  38. package/.output/server/chunks/build/nuxt-link-B4oWFn7n.mjs.map +1 -0
  39. package/.output/server/chunks/build/{repo-graph-DzT45gSB.mjs → repo-graph-BQVFpA-w.mjs} +5 -4
  40. package/.output/server/chunks/build/repo-graph-BQVFpA-w.mjs.map +1 -0
  41. package/.output/server/chunks/build/server.mjs +7 -7
  42. package/.output/server/chunks/build/styles.mjs +4 -5
  43. package/.output/server/chunks/build/{usePrd-hXZOmvAv.mjs → usePrd-Bb6jlnNZ.mjs} +2 -2
  44. package/.output/server/chunks/build/usePrd-Bb6jlnNZ.mjs.map +1 -0
  45. package/.output/server/chunks/nitro/nitro.mjs +983 -678
  46. package/.output/server/chunks/nitro/nitro.mjs.map +1 -1
  47. package/.output/server/node_modules/@pierre/diffs/dist/components/File.js +324 -0
  48. package/.output/server/node_modules/@pierre/diffs/dist/components/FileDiff.js +395 -0
  49. package/.output/server/node_modules/@pierre/diffs/dist/components/FileStream.js +161 -0
  50. package/.output/server/node_modules/@pierre/diffs/dist/components/web-components.js +25 -0
  51. package/.output/server/node_modules/@pierre/diffs/dist/constants.js +23 -0
  52. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/languages/areLanguagesAttached.js +11 -0
  53. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/languages/attachResolvedLanguages.js +20 -0
  54. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/languages/cleanUpResolvedLanguages.js +11 -0
  55. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/languages/constants.js +8 -0
  56. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/languages/getResolvedLanguages.js +16 -0
  57. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/languages/getResolvedOrResolveLanguage.js +11 -0
  58. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/languages/hasResolvedLanguages.js +11 -0
  59. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/languages/resolveLanguage.js +30 -0
  60. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/languages/resolveLanguages.js +25 -0
  61. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/shared_highlighter.js +71 -0
  62. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/themes/areThemesAttached.js +12 -0
  63. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/themes/attachResolvedThemes.js +24 -0
  64. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/themes/cleanUpResolvedThemes.js +11 -0
  65. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/themes/constants.js +9 -0
  66. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/themes/getResolvedOrResolveTheme.js +11 -0
  67. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/themes/getResolvedThemes.js +16 -0
  68. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/themes/hasResolvedThemes.js +11 -0
  69. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/themes/registerCustomCSSVariableTheme.js +18 -0
  70. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/themes/registerCustomTheme.js +14 -0
  71. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/themes/resolveTheme.js +35 -0
  72. package/.output/server/node_modules/@pierre/diffs/dist/highlighter/themes/resolveThemes.js +21 -0
  73. package/.output/server/node_modules/@pierre/diffs/dist/index.js +84 -0
  74. package/.output/server/node_modules/@pierre/diffs/dist/managers/LineSelectionManager.js +282 -0
  75. package/.output/server/node_modules/@pierre/diffs/dist/managers/MouseEventManager.js +244 -0
  76. package/.output/server/node_modules/@pierre/diffs/dist/managers/ResizeManager.js +132 -0
  77. package/.output/server/node_modules/@pierre/diffs/dist/managers/ScrollSyncManager.js +62 -0
  78. package/.output/server/node_modules/@pierre/diffs/dist/managers/UniversalRenderingManager.js +32 -0
  79. package/.output/server/node_modules/@pierre/diffs/dist/renderers/DiffHunksRenderer.js +655 -0
  80. package/.output/server/node_modules/@pierre/diffs/dist/renderers/FileRenderer.js +243 -0
  81. package/.output/server/node_modules/@pierre/diffs/dist/shiki-stream/stream.js +32 -0
  82. package/.output/server/node_modules/@pierre/diffs/dist/shiki-stream/tokenizer.js +71 -0
  83. package/.output/server/node_modules/@pierre/diffs/dist/sprite.js +55 -0
  84. package/.output/server/node_modules/@pierre/diffs/dist/style.js +6 -0
  85. package/.output/server/node_modules/@pierre/diffs/dist/themes/pierre-dark.js +1328 -0
  86. package/.output/server/node_modules/@pierre/diffs/dist/themes/pierre-light.js +1328 -0
  87. package/.output/server/node_modules/@pierre/diffs/dist/utils/areFilesEqual.js +8 -0
  88. package/.output/server/node_modules/@pierre/diffs/dist/utils/areObjectsEqual.js +18 -0
  89. package/.output/server/node_modules/@pierre/diffs/dist/utils/areOptionsEqual.js +12 -0
  90. package/.output/server/node_modules/@pierre/diffs/dist/utils/areSelectionsEqual.js +8 -0
  91. package/.output/server/node_modules/@pierre/diffs/dist/utils/areThemesEqual.js +9 -0
  92. package/.output/server/node_modules/@pierre/diffs/dist/utils/cleanLastNewline.js +8 -0
  93. package/.output/server/node_modules/@pierre/diffs/dist/utils/createAnnotationElement.js +21 -0
  94. package/.output/server/node_modules/@pierre/diffs/dist/utils/createAnnotationWrapperNode.js +12 -0
  95. package/.output/server/node_modules/@pierre/diffs/dist/utils/createCodeNode.js +12 -0
  96. package/.output/server/node_modules/@pierre/diffs/dist/utils/createEmptyRowBuffer.js +16 -0
  97. package/.output/server/node_modules/@pierre/diffs/dist/utils/createFileHeaderElement.js +84 -0
  98. package/.output/server/node_modules/@pierre/diffs/dist/utils/createHoverContentNode.js +15 -0
  99. package/.output/server/node_modules/@pierre/diffs/dist/utils/createNoNewlineElement.js +24 -0
  100. package/.output/server/node_modules/@pierre/diffs/dist/utils/createPreElement.js +28 -0
  101. package/.output/server/node_modules/@pierre/diffs/dist/utils/createRowNodes.js +20 -0
  102. package/.output/server/node_modules/@pierre/diffs/dist/utils/createSeparator.js +69 -0
  103. package/.output/server/node_modules/@pierre/diffs/dist/utils/createSpanNodeFromToken.js +13 -0
  104. package/.output/server/node_modules/@pierre/diffs/dist/utils/createStyleElement.js +19 -0
  105. package/.output/server/node_modules/@pierre/diffs/dist/utils/createTransformerWithState.js +56 -0
  106. package/.output/server/node_modules/@pierre/diffs/dist/utils/createUnsafeCSSStyleNode.js +12 -0
  107. package/.output/server/node_modules/@pierre/diffs/dist/utils/cssWrappers.js +21 -0
  108. package/.output/server/node_modules/@pierre/diffs/dist/utils/diffAcceptRejectHunk.js +82 -0
  109. package/.output/server/node_modules/@pierre/diffs/dist/utils/formatCSSVariablePrefix.js +8 -0
  110. package/.output/server/node_modules/@pierre/diffs/dist/utils/getFiletypeFromFileName.js +343 -0
  111. package/.output/server/node_modules/@pierre/diffs/dist/utils/getHighlighterOptions.js +13 -0
  112. package/.output/server/node_modules/@pierre/diffs/dist/utils/getHighlighterThemeStyles.js +40 -0
  113. package/.output/server/node_modules/@pierre/diffs/dist/utils/getHunkSeparatorSlotName.js +8 -0
  114. package/.output/server/node_modules/@pierre/diffs/dist/utils/getIconForType.js +15 -0
  115. package/.output/server/node_modules/@pierre/diffs/dist/utils/getLineAnnotationName.js +8 -0
  116. package/.output/server/node_modules/@pierre/diffs/dist/utils/getLineEndingType.js +11 -0
  117. package/.output/server/node_modules/@pierre/diffs/dist/utils/getLineNodes.js +15 -0
  118. package/.output/server/node_modules/@pierre/diffs/dist/utils/getSingularPatch.js +20 -0
  119. package/.output/server/node_modules/@pierre/diffs/dist/utils/getThemes.js +16 -0
  120. package/.output/server/node_modules/@pierre/diffs/dist/utils/getTotalLineCountFromHunks.js +10 -0
  121. package/.output/server/node_modules/@pierre/diffs/dist/utils/hast_utils.js +42 -0
  122. package/.output/server/node_modules/@pierre/diffs/dist/utils/isWorkerContext.js +8 -0
  123. package/.output/server/node_modules/@pierre/diffs/dist/utils/parseDiffDecorations.js +34 -0
  124. package/.output/server/node_modules/@pierre/diffs/dist/utils/parseDiffFromFile.js +23 -0
  125. package/.output/server/node_modules/@pierre/diffs/dist/utils/parseLineType.js +17 -0
  126. package/.output/server/node_modules/@pierre/diffs/dist/utils/parsePatchFiles.js +211 -0
  127. package/.output/server/node_modules/@pierre/diffs/dist/utils/prerenderHTMLIfNecessary.js +10 -0
  128. package/.output/server/node_modules/@pierre/diffs/dist/utils/processLine.js +42 -0
  129. package/.output/server/node_modules/@pierre/diffs/dist/utils/renderDiffWithHighlighter.js +339 -0
  130. package/.output/server/node_modules/@pierre/diffs/dist/utils/renderFileWithHighlighter.js +52 -0
  131. package/.output/server/node_modules/@pierre/diffs/dist/utils/setLanguageOverride.js +11 -0
  132. package/.output/server/node_modules/@pierre/diffs/dist/utils/setWrapperNodeProps.js +29 -0
  133. package/.output/server/node_modules/@pierre/diffs/package.json +89 -0
  134. package/.output/server/node_modules/@shikijs/transformers/dist/index.mjs +831 -0
  135. package/.output/server/node_modules/@shikijs/transformers/package.json +37 -0
  136. package/.output/server/node_modules/diff/libesm/convert/dmp.js +21 -0
  137. package/.output/server/node_modules/diff/libesm/convert/xml.js +31 -0
  138. package/.output/server/node_modules/diff/libesm/diff/array.js +16 -0
  139. package/.output/server/node_modules/diff/libesm/diff/base.js +253 -0
  140. package/.output/server/node_modules/diff/libesm/diff/character.js +7 -0
  141. package/.output/server/node_modules/diff/libesm/diff/css.js +10 -0
  142. package/.output/server/node_modules/diff/libesm/diff/json.js +78 -0
  143. package/.output/server/node_modules/diff/libesm/diff/line.js +65 -0
  144. package/.output/server/node_modules/diff/libesm/diff/sentence.js +43 -0
  145. package/.output/server/node_modules/diff/libesm/diff/word.js +296 -0
  146. package/.output/server/node_modules/diff/libesm/index.js +30 -0
  147. package/.output/server/node_modules/diff/libesm/package.json +1 -0
  148. package/.output/server/node_modules/diff/libesm/patch/apply.js +257 -0
  149. package/.output/server/node_modules/diff/libesm/patch/create.js +228 -0
  150. package/.output/server/node_modules/diff/libesm/patch/line-endings.js +44 -0
  151. package/.output/server/node_modules/diff/libesm/patch/parse.js +147 -0
  152. package/.output/server/node_modules/diff/libesm/patch/reverse.js +23 -0
  153. package/.output/server/node_modules/diff/libesm/util/distance-iterator.js +37 -0
  154. package/.output/server/node_modules/diff/libesm/util/params.js +14 -0
  155. package/.output/server/node_modules/diff/libesm/util/string.js +128 -0
  156. package/.output/server/node_modules/diff/package.json +132 -0
  157. package/.output/server/package.json +4 -1
  158. package/README.md +41 -0
  159. package/bin/prd +1 -1
  160. package/dist/host/src/index.js +10 -0
  161. package/dist/host/src/sync.js +201 -0
  162. package/dist/server/utils/db.js +64 -0
  163. package/dist/server/utils/git.js +8 -6
  164. package/dist/server/utils/prd-state.js +24 -2
  165. package/dist/server/utils/repos.js +12 -2
  166. package/dist/server/utils/state-migration.js +4 -3
  167. package/dist/server/utils/sync-apply.js +380 -0
  168. package/dist/server/utils/sync-export.js +183 -0
  169. package/dist/server/utils/sync-identity.js +231 -0
  170. package/dist/server/utils/sync-inspect.js +103 -0
  171. package/dist/server/utils/sync-merge.js +579 -0
  172. package/dist/server/utils/sync-schema.js +100 -0
  173. package/package.json +2 -1
  174. package/.output/public/_nuxt/6tINjQEd.js +0 -141
  175. package/.output/public/_nuxt/B2mIQf5X.js +0 -3
  176. package/.output/public/_nuxt/C0BBSDJ7.js +0 -1
  177. package/.output/public/_nuxt/CN46Bgts.js +0 -1
  178. package/.output/public/_nuxt/CTJgb0zb.js +0 -1
  179. package/.output/public/_nuxt/Cce168lk.js +0 -30
  180. package/.output/public/_nuxt/CyVSeLw5.js +0 -1
  181. package/.output/public/_nuxt/Detail.CYc96mGf.css +0 -1
  182. package/.output/public/_nuxt/ZNypZshD.js +0 -13
  183. package/.output/public/_nuxt/builds/meta/8c342d49-fe70-4f67-a987-821c16f86125.json +0 -1
  184. package/.output/public/_nuxt/entry.Bw0CE6Iz.css +0 -1
  185. package/.output/public/_nuxt/pYJYAY-W.js +0 -60
  186. package/.output/server/chunks/build/Detail-DMMUwTWr.mjs.map +0 -1
  187. package/.output/server/chunks/build/DiffViewer-styles-1.mjs-d2dQvARr.mjs +0 -4
  188. package/.output/server/chunks/build/DiffViewer-styles-1.mjs-d2dQvARr.mjs.map +0 -1
  189. package/.output/server/chunks/build/DiffViewer-styles-2.mjs-X6QKNjM0.mjs +0 -4
  190. package/.output/server/chunks/build/DiffViewer-styles-2.mjs-X6QKNjM0.mjs.map +0 -1
  191. package/.output/server/chunks/build/DiffViewer-styles.0AbHFl6N.mjs +0 -8
  192. package/.output/server/chunks/build/DiffViewer-styles.0AbHFl6N.mjs.map +0 -1
  193. package/.output/server/chunks/build/DiffViewer-styles.BDwAqkTk.mjs +0 -8
  194. package/.output/server/chunks/build/DiffViewer-styles.BDwAqkTk.mjs.map +0 -1
  195. package/.output/server/chunks/build/DiffViewer-styles.DRJh5Ui4.mjs +0 -10
  196. package/.output/server/chunks/build/DiffViewer-styles.DRJh5Ui4.mjs.map +0 -1
  197. package/.output/server/chunks/build/_prd_-ByugK4Yi.mjs.map +0 -1
  198. package/.output/server/chunks/build/default-BKKgG7HJ.mjs.map +0 -1
  199. package/.output/server/chunks/build/error-404-Bf6kdO80.mjs.map +0 -1
  200. package/.output/server/chunks/build/index-DE1tjHAd.mjs.map +0 -1
  201. package/.output/server/chunks/build/nuxt-link-SvT1nf8Z.mjs.map +0 -1
  202. package/.output/server/chunks/build/repo-graph-DzT45gSB.mjs.map +0 -1
  203. package/.output/server/chunks/build/usePrd-hXZOmvAv.mjs.map +0 -1
@@ -0,0 +1,579 @@
1
+ import { resolve } from 'node:path';
2
+ import { dbAll } from './db.js';
3
+ import { getRepos } from './repos.js';
4
+ import { parseStoredProgressFile, parseTasksFile } from './state-schema.js';
5
+ import { createSyncFieldHashes, parseSyncBundle } from './sync-schema.js';
6
+ import { ensureRepoSyncMetaForRepos } from './sync-identity.js';
7
+ function normalizePath(path) {
8
+ return resolve(path);
9
+ }
10
+ function createRepoIndex(localRepos) {
11
+ const byRepoId = new Map();
12
+ const byPath = new Map();
13
+ const bySyncKey = new Map();
14
+ const byFingerprint = new Map();
15
+ for (const repo of localRepos) {
16
+ byRepoId.set(repo.repoId, repo);
17
+ byPath.set(normalizePath(repo.repoPath), repo);
18
+ bySyncKey.set(repo.syncKey, repo);
19
+ const fingerprintKey = `${repo.fingerprintKind}:${repo.fingerprint}`;
20
+ const current = byFingerprint.get(fingerprintKey);
21
+ if (current) {
22
+ current.push(repo);
23
+ }
24
+ else {
25
+ byFingerprint.set(fingerprintKey, [repo]);
26
+ }
27
+ }
28
+ return { byRepoId, byPath, bySyncKey, byFingerprint };
29
+ }
30
+ function parseLocalJson(rawValue, parseValue, fallback) {
31
+ if (rawValue === null) {
32
+ return null;
33
+ }
34
+ try {
35
+ const parsed = JSON.parse(rawValue);
36
+ try {
37
+ return parseValue(parsed);
38
+ }
39
+ catch {
40
+ return fallback(parsed);
41
+ }
42
+ }
43
+ catch {
44
+ return rawValue;
45
+ }
46
+ }
47
+ function toClock(primary, fallback, hasValue) {
48
+ if (primary) {
49
+ return primary;
50
+ }
51
+ if (hasValue) {
52
+ return fallback;
53
+ }
54
+ return null;
55
+ }
56
+ function compareIsoTimestamps(a, b) {
57
+ if (a === b) {
58
+ return 0;
59
+ }
60
+ if (!a && !b) {
61
+ return 0;
62
+ }
63
+ if (!a) {
64
+ return -1;
65
+ }
66
+ if (!b) {
67
+ return 1;
68
+ }
69
+ const aEpoch = Date.parse(a);
70
+ const bEpoch = Date.parse(b);
71
+ if (Number.isFinite(aEpoch) && Number.isFinite(bEpoch) && aEpoch !== bEpoch) {
72
+ return aEpoch > bEpoch ? 1 : -1;
73
+ }
74
+ return a.localeCompare(b);
75
+ }
76
+ function compareHashes(a, b) {
77
+ const left = a || '';
78
+ const right = b || '';
79
+ return left.localeCompare(right);
80
+ }
81
+ function decideFieldMerge(params) {
82
+ const { localClock, incomingClock, localHash, incomingHash } = params;
83
+ const valueChanged = (localHash || '') !== (incomingHash || '');
84
+ const clockChanged = localClock !== incomingClock;
85
+ const conflict = valueChanged && localHash !== null && incomingHash !== null;
86
+ if (!valueChanged && !clockChanged) {
87
+ return {
88
+ winner: 'local',
89
+ reason: 'equal_value',
90
+ localClock,
91
+ incomingClock,
92
+ localHash,
93
+ incomingHash,
94
+ changed: false,
95
+ valueChanged,
96
+ clockChanged,
97
+ conflict
98
+ };
99
+ }
100
+ if (incomingClock && localClock) {
101
+ const timestampComparison = compareIsoTimestamps(incomingClock, localClock);
102
+ if (timestampComparison > 0) {
103
+ return {
104
+ winner: 'incoming',
105
+ reason: 'incoming_newer_clock',
106
+ localClock,
107
+ incomingClock,
108
+ localHash,
109
+ incomingHash,
110
+ changed: valueChanged || clockChanged,
111
+ valueChanged,
112
+ clockChanged,
113
+ conflict
114
+ };
115
+ }
116
+ if (timestampComparison < 0) {
117
+ return {
118
+ winner: 'local',
119
+ reason: 'local_newer_clock',
120
+ localClock,
121
+ incomingClock,
122
+ localHash,
123
+ incomingHash,
124
+ changed: false,
125
+ valueChanged,
126
+ clockChanged,
127
+ conflict
128
+ };
129
+ }
130
+ }
131
+ else if (incomingClock && !localClock) {
132
+ return {
133
+ winner: 'incoming',
134
+ reason: 'incoming_has_clock',
135
+ localClock,
136
+ incomingClock,
137
+ localHash,
138
+ incomingHash,
139
+ changed: valueChanged || clockChanged,
140
+ valueChanged,
141
+ clockChanged,
142
+ conflict
143
+ };
144
+ }
145
+ else if (!incomingClock && localClock) {
146
+ return {
147
+ winner: 'local',
148
+ reason: 'local_has_clock',
149
+ localClock,
150
+ incomingClock,
151
+ localHash,
152
+ incomingHash,
153
+ changed: false,
154
+ valueChanged,
155
+ clockChanged,
156
+ conflict
157
+ };
158
+ }
159
+ const hashComparison = compareHashes(incomingHash, localHash);
160
+ if (hashComparison > 0) {
161
+ return {
162
+ winner: 'incoming',
163
+ reason: 'incoming_hash_tiebreak',
164
+ localClock,
165
+ incomingClock,
166
+ localHash,
167
+ incomingHash,
168
+ changed: valueChanged || clockChanged,
169
+ valueChanged,
170
+ clockChanged,
171
+ conflict
172
+ };
173
+ }
174
+ if (hashComparison < 0) {
175
+ return {
176
+ winner: 'local',
177
+ reason: 'local_hash_tiebreak',
178
+ localClock,
179
+ incomingClock,
180
+ localHash,
181
+ incomingHash,
182
+ changed: false,
183
+ valueChanged,
184
+ clockChanged,
185
+ conflict
186
+ };
187
+ }
188
+ return {
189
+ winner: 'local',
190
+ reason: 'equal_value',
191
+ localClock,
192
+ incomingClock,
193
+ localHash,
194
+ incomingHash,
195
+ changed: false,
196
+ valueChanged,
197
+ clockChanged,
198
+ conflict
199
+ };
200
+ }
201
+ function buildStateKey(repoId, slug) {
202
+ return `${repoId}:${slug}`;
203
+ }
204
+ function resolveMappedRepo(incomingRepoSyncKey, incomingRepoMeta, repoIndex, repoMap) {
205
+ const mappedValue = repoMap[incomingRepoSyncKey];
206
+ if (typeof mappedValue === 'string' && mappedValue.trim().length > 0) {
207
+ const target = mappedValue.trim();
208
+ const byId = repoIndex.byRepoId.get(target)
209
+ || repoIndex.byPath.get(normalizePath(target))
210
+ || repoIndex.bySyncKey.get(target);
211
+ if (!byId) {
212
+ return {
213
+ incomingRepoSyncKey,
214
+ ...(incomingRepoMeta?.name ? { incomingRepoName: incomingRepoMeta.name } : {}),
215
+ source: 'unresolved',
216
+ reason: 'map_target_not_found'
217
+ };
218
+ }
219
+ return {
220
+ incomingRepoSyncKey,
221
+ ...(incomingRepoMeta?.name ? { incomingRepoName: incomingRepoMeta.name } : {}),
222
+ localRepoId: byId.repoId,
223
+ localRepoPath: byId.repoPath,
224
+ localRepoSyncKey: byId.syncKey,
225
+ source: 'map'
226
+ };
227
+ }
228
+ const bySyncKey = repoIndex.bySyncKey.get(incomingRepoSyncKey);
229
+ if (bySyncKey) {
230
+ return {
231
+ incomingRepoSyncKey,
232
+ ...(incomingRepoMeta?.name ? { incomingRepoName: incomingRepoMeta.name } : {}),
233
+ localRepoId: bySyncKey.repoId,
234
+ localRepoPath: bySyncKey.repoPath,
235
+ localRepoSyncKey: bySyncKey.syncKey,
236
+ source: 'sync_key'
237
+ };
238
+ }
239
+ if (!incomingRepoMeta?.fingerprint || !incomingRepoMeta.fingerprintKind) {
240
+ return {
241
+ incomingRepoSyncKey,
242
+ ...(incomingRepoMeta?.name ? { incomingRepoName: incomingRepoMeta.name } : {}),
243
+ source: 'unresolved',
244
+ reason: incomingRepoMeta ? 'no_match' : 'unknown_repo_metadata'
245
+ };
246
+ }
247
+ const fingerprintKey = `${incomingRepoMeta.fingerprintKind}:${incomingRepoMeta.fingerprint}`;
248
+ const matches = repoIndex.byFingerprint.get(fingerprintKey) || [];
249
+ if (matches.length === 1) {
250
+ const matchedRepo = matches[0];
251
+ return {
252
+ incomingRepoSyncKey,
253
+ ...(incomingRepoMeta?.name ? { incomingRepoName: incomingRepoMeta.name } : {}),
254
+ localRepoId: matchedRepo.repoId,
255
+ localRepoPath: matchedRepo.repoPath,
256
+ localRepoSyncKey: matchedRepo.syncKey,
257
+ source: 'fingerprint'
258
+ };
259
+ }
260
+ if (matches.length > 1) {
261
+ return {
262
+ incomingRepoSyncKey,
263
+ ...(incomingRepoMeta?.name ? { incomingRepoName: incomingRepoMeta.name } : {}),
264
+ source: 'unresolved',
265
+ reason: 'fingerprint_ambiguous'
266
+ };
267
+ }
268
+ return {
269
+ incomingRepoSyncKey,
270
+ ...(incomingRepoMeta?.name ? { incomingRepoName: incomingRepoMeta.name } : {}),
271
+ source: 'unresolved',
272
+ reason: 'no_match'
273
+ };
274
+ }
275
+ async function loadLocalStateRows(repoIds) {
276
+ if (repoIds.length === 0) {
277
+ return new Map();
278
+ }
279
+ const placeholders = repoIds.map(() => '?').join(', ');
280
+ const rows = await dbAll(`
281
+ SELECT
282
+ repo_id,
283
+ slug,
284
+ tasks_json,
285
+ progress_json,
286
+ notes_md,
287
+ updated_at,
288
+ tasks_updated_at,
289
+ progress_updated_at,
290
+ notes_updated_at
291
+ FROM prd_states
292
+ WHERE repo_id IN (${placeholders})
293
+ `, repoIds);
294
+ const byKey = new Map();
295
+ for (const row of rows) {
296
+ byKey.set(buildStateKey(row.repo_id, row.slug), row);
297
+ }
298
+ return byKey;
299
+ }
300
+ async function loadLocalArchiveRows(repoIds) {
301
+ if (repoIds.length === 0) {
302
+ return new Map();
303
+ }
304
+ const placeholders = repoIds.map(() => '?').join(', ');
305
+ const rows = await dbAll(`
306
+ SELECT repo_id, slug, archived_at
307
+ FROM prd_archives
308
+ WHERE repo_id IN (${placeholders})
309
+ `, repoIds);
310
+ const byKey = new Map();
311
+ for (const row of rows) {
312
+ byKey.set(buildStateKey(row.repo_id, row.slug), row);
313
+ }
314
+ return byKey;
315
+ }
316
+ function getIncomingRepoMeta(bundle, repoSyncKey) {
317
+ const repo = bundle.repos.find((entry) => entry.repoSyncKey === repoSyncKey);
318
+ if (!repo) {
319
+ return null;
320
+ }
321
+ return {
322
+ name: repo.name,
323
+ fingerprint: repo.fingerprint,
324
+ fingerprintKind: repo.fingerprintKind
325
+ };
326
+ }
327
+ function parseLocalTasks(row) {
328
+ return parseLocalJson(row.tasks_json, parseTasksFile, (value) => value);
329
+ }
330
+ function parseLocalProgress(row, tasks) {
331
+ return parseLocalJson(row.progress_json, (value) => parseStoredProgressFile(value, {
332
+ totalTasksHint: tasks && typeof tasks === 'object' && !Array.isArray(tasks)
333
+ ? Array.isArray(tasks.tasks)
334
+ ? (tasks.tasks.length)
335
+ : undefined
336
+ : undefined,
337
+ prdNameFallback: row.slug
338
+ }), (value) => value);
339
+ }
340
+ function buildStateRowPlan(params) {
341
+ const { row, mapping, localStateRows } = params;
342
+ if (!mapping.localRepoId || mapping.source === 'unresolved') {
343
+ return {
344
+ repoSyncKey: row.repoSyncKey,
345
+ slug: row.slug,
346
+ action: 'unresolved',
347
+ mappingSource: mapping.source,
348
+ ...(mapping.reason ? { reason: mapping.reason } : {}),
349
+ updateFields: [],
350
+ conflictFields: []
351
+ };
352
+ }
353
+ const stateKey = buildStateKey(mapping.localRepoId, row.slug);
354
+ const localRow = localStateRows.get(stateKey);
355
+ if (!localRow) {
356
+ return {
357
+ repoSyncKey: row.repoSyncKey,
358
+ slug: row.slug,
359
+ action: 'insert',
360
+ localRepoId: mapping.localRepoId,
361
+ ...(mapping.localRepoPath ? { localRepoPath: mapping.localRepoPath } : {}),
362
+ mappingSource: mapping.source,
363
+ updateFields: ['tasks', 'progress', 'notes'],
364
+ conflictFields: []
365
+ };
366
+ }
367
+ const localTasks = parseLocalTasks(localRow);
368
+ const localProgress = parseLocalProgress(localRow, localTasks);
369
+ const localNotes = localRow.notes_md;
370
+ const localClocks = {
371
+ tasksUpdatedAt: toClock(localRow.tasks_updated_at, localRow.updated_at, localTasks !== null),
372
+ progressUpdatedAt: toClock(localRow.progress_updated_at, localRow.updated_at, localProgress !== null),
373
+ notesUpdatedAt: toClock(localRow.notes_updated_at, localRow.updated_at, localNotes !== null)
374
+ };
375
+ const localHashes = createSyncFieldHashes({
376
+ tasks: localTasks,
377
+ progress: localProgress,
378
+ notes: localNotes
379
+ });
380
+ const incomingHashes = createSyncFieldHashes({
381
+ tasks: row.tasks,
382
+ progress: row.progress,
383
+ notes: row.notes
384
+ });
385
+ const tasksDecision = decideFieldMerge({
386
+ localClock: localClocks.tasksUpdatedAt,
387
+ incomingClock: row.clocks.tasksUpdatedAt,
388
+ localHash: localHashes.tasksHash,
389
+ incomingHash: incomingHashes.tasksHash
390
+ });
391
+ const progressDecision = decideFieldMerge({
392
+ localClock: localClocks.progressUpdatedAt,
393
+ incomingClock: row.clocks.progressUpdatedAt,
394
+ localHash: localHashes.progressHash,
395
+ incomingHash: incomingHashes.progressHash
396
+ });
397
+ const notesDecision = decideFieldMerge({
398
+ localClock: localClocks.notesUpdatedAt,
399
+ incomingClock: row.clocks.notesUpdatedAt,
400
+ localHash: localHashes.notesHash,
401
+ incomingHash: incomingHashes.notesHash
402
+ });
403
+ const updateFields = [];
404
+ const conflictFields = [];
405
+ if (tasksDecision.changed) {
406
+ updateFields.push('tasks');
407
+ }
408
+ if (progressDecision.changed) {
409
+ updateFields.push('progress');
410
+ }
411
+ if (notesDecision.changed) {
412
+ updateFields.push('notes');
413
+ }
414
+ if (tasksDecision.conflict) {
415
+ conflictFields.push('tasks');
416
+ }
417
+ if (progressDecision.conflict) {
418
+ conflictFields.push('progress');
419
+ }
420
+ if (notesDecision.conflict) {
421
+ conflictFields.push('notes');
422
+ }
423
+ return {
424
+ repoSyncKey: row.repoSyncKey,
425
+ slug: row.slug,
426
+ action: updateFields.length > 0 ? 'update' : 'skip',
427
+ localRepoId: mapping.localRepoId,
428
+ ...(mapping.localRepoPath ? { localRepoPath: mapping.localRepoPath } : {}),
429
+ mappingSource: mapping.source,
430
+ updateFields,
431
+ conflictFields,
432
+ fieldDecisions: {
433
+ tasks: tasksDecision,
434
+ progress: progressDecision,
435
+ notes: notesDecision
436
+ }
437
+ };
438
+ }
439
+ function buildArchiveRowPlan(params) {
440
+ const { row, mapping, localArchiveRows } = params;
441
+ if (!mapping.localRepoId || mapping.source === 'unresolved') {
442
+ return {
443
+ repoSyncKey: row.repoSyncKey,
444
+ slug: row.slug,
445
+ action: 'unresolved',
446
+ mappingSource: mapping.source,
447
+ ...(mapping.reason ? { reason: mapping.reason } : {})
448
+ };
449
+ }
450
+ const archiveKey = buildStateKey(mapping.localRepoId, row.slug);
451
+ const localRow = localArchiveRows.get(archiveKey);
452
+ if (!localRow) {
453
+ return {
454
+ repoSyncKey: row.repoSyncKey,
455
+ slug: row.slug,
456
+ action: 'insert',
457
+ localRepoId: mapping.localRepoId,
458
+ ...(mapping.localRepoPath ? { localRepoPath: mapping.localRepoPath } : {}),
459
+ mappingSource: mapping.source
460
+ };
461
+ }
462
+ const incomingIsNewer = compareIsoTimestamps(row.archivedAt, localRow.archived_at) > 0;
463
+ return {
464
+ repoSyncKey: row.repoSyncKey,
465
+ slug: row.slug,
466
+ action: incomingIsNewer ? 'update' : 'skip',
467
+ localRepoId: mapping.localRepoId,
468
+ ...(mapping.localRepoPath ? { localRepoPath: mapping.localRepoPath } : {}),
469
+ mappingSource: mapping.source
470
+ };
471
+ }
472
+ export async function planSyncMerge(bundleInput, options = {}) {
473
+ const bundle = parseSyncBundle(bundleInput);
474
+ const localRepos = await getRepos();
475
+ const repoMetaById = await ensureRepoSyncMetaForRepos(localRepos);
476
+ const identities = localRepos.map((repo) => {
477
+ const meta = repoMetaById.get(repo.id);
478
+ if (!meta) {
479
+ throw new Error(`Missing sync metadata for local repository ${repo.id}`);
480
+ }
481
+ return {
482
+ repoId: repo.id,
483
+ repoPath: repo.path,
484
+ syncKey: meta.syncKey,
485
+ fingerprint: meta.fingerprint,
486
+ fingerprintKind: meta.fingerprintKind
487
+ };
488
+ });
489
+ const repoIndex = createRepoIndex(identities);
490
+ const incomingRepoKeys = new Set();
491
+ for (const repo of bundle.repos) {
492
+ incomingRepoKeys.add(repo.repoSyncKey);
493
+ }
494
+ for (const row of bundle.states) {
495
+ incomingRepoKeys.add(row.repoSyncKey);
496
+ }
497
+ for (const row of bundle.archives) {
498
+ incomingRepoKeys.add(row.repoSyncKey);
499
+ }
500
+ const mappingResults = Array.from(incomingRepoKeys)
501
+ .sort((a, b) => a.localeCompare(b))
502
+ .map((repoSyncKey) => {
503
+ return resolveMappedRepo(repoSyncKey, getIncomingRepoMeta(bundle, repoSyncKey), repoIndex, options.repoMap || {});
504
+ });
505
+ const mappingByIncomingKey = new Map(mappingResults.map((result) => [result.incomingRepoSyncKey, result]));
506
+ const mappedRepoIds = Array.from(new Set(mappingResults
507
+ .map((result) => result.localRepoId)
508
+ .filter((value) => typeof value === 'string' && value.length > 0)));
509
+ const [localStateRows, localArchiveRows] = await Promise.all([
510
+ loadLocalStateRows(mappedRepoIds),
511
+ loadLocalArchiveRows(mappedRepoIds)
512
+ ]);
513
+ const statePlans = bundle.states.map((row) => {
514
+ const mapping = mappingByIncomingKey.get(row.repoSyncKey) || {
515
+ incomingRepoSyncKey: row.repoSyncKey,
516
+ source: 'unresolved',
517
+ reason: 'unknown_repo_metadata'
518
+ };
519
+ return buildStateRowPlan({
520
+ row,
521
+ mapping,
522
+ localStateRows
523
+ });
524
+ });
525
+ const archivePlans = bundle.archives.map((row) => {
526
+ const mapping = mappingByIncomingKey.get(row.repoSyncKey) || {
527
+ incomingRepoSyncKey: row.repoSyncKey,
528
+ source: 'unresolved',
529
+ reason: 'unknown_repo_metadata'
530
+ };
531
+ return buildArchiveRowPlan({
532
+ row,
533
+ mapping,
534
+ localArchiveRows
535
+ });
536
+ });
537
+ const summary = {
538
+ repos: {
539
+ mapped: mappingResults.filter((result) => result.source !== 'unresolved').length,
540
+ unresolved: mappingResults.filter((result) => result.source === 'unresolved').length
541
+ },
542
+ states: {
543
+ insert: statePlans.filter((row) => row.action === 'insert').length,
544
+ update: statePlans.filter((row) => row.action === 'update').length,
545
+ skip: statePlans.filter((row) => row.action === 'skip').length,
546
+ unresolved: statePlans.filter((row) => row.action === 'unresolved').length,
547
+ conflicts: statePlans.reduce((sum, row) => sum + row.conflictFields.length, 0)
548
+ },
549
+ archives: {
550
+ insert: archivePlans.filter((row) => row.action === 'insert').length,
551
+ update: archivePlans.filter((row) => row.action === 'update').length,
552
+ skip: archivePlans.filter((row) => row.action === 'skip').length,
553
+ unresolved: archivePlans.filter((row) => row.action === 'unresolved').length
554
+ }
555
+ };
556
+ return {
557
+ bundle: {
558
+ bundleId: bundle.bundleId,
559
+ formatVersion: bundle.formatVersion,
560
+ sourceDeviceId: bundle.sourceDeviceId,
561
+ createdAt: bundle.createdAt
562
+ },
563
+ mappings: mappingResults,
564
+ states: statePlans,
565
+ archives: archivePlans,
566
+ summary
567
+ };
568
+ }
569
+ export async function planSyncMergeJson(jsonPayload, options = {}) {
570
+ let parsed;
571
+ try {
572
+ parsed = JSON.parse(jsonPayload);
573
+ }
574
+ catch (error) {
575
+ const message = error instanceof Error ? error.message : String(error);
576
+ throw new Error(`Invalid bundle JSON: ${message}`);
577
+ }
578
+ return await planSyncMerge(parsed, options);
579
+ }
@@ -0,0 +1,100 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ export const SYNC_BUNDLE_TYPE = 'steward-sync-bundle';
4
+ export const SYNC_BUNDLE_FORMAT_VERSION = 1;
5
+ const prdSlugSchema = z.string().trim().regex(/^[A-Za-z0-9][A-Za-z0-9-]*$/);
6
+ const syncRepoSchema = z.object({
7
+ repoSyncKey: z.string().trim().min(1),
8
+ name: z.string().trim().min(1),
9
+ pathHint: z.string().trim().min(1).optional(),
10
+ fingerprint: z.string().trim().min(1),
11
+ fingerprintKind: z.string().trim().min(1)
12
+ });
13
+ const syncFieldClocksSchema = z.object({
14
+ tasksUpdatedAt: z.string().nullable(),
15
+ progressUpdatedAt: z.string().nullable(),
16
+ notesUpdatedAt: z.string().nullable()
17
+ });
18
+ const syncFieldHashesSchema = z.object({
19
+ tasksHash: z.string().nullable(),
20
+ progressHash: z.string().nullable(),
21
+ notesHash: z.string().nullable()
22
+ });
23
+ const syncStateRowSchema = z.object({
24
+ repoSyncKey: z.string().trim().min(1),
25
+ slug: prdSlugSchema,
26
+ tasks: z.unknown().nullable(),
27
+ progress: z.unknown().nullable(),
28
+ notes: z.string().nullable(),
29
+ clocks: syncFieldClocksSchema,
30
+ hashes: syncFieldHashesSchema
31
+ });
32
+ const syncArchiveRowSchema = z.object({
33
+ repoSyncKey: z.string().trim().min(1),
34
+ slug: prdSlugSchema,
35
+ archivedAt: z.string().trim().min(1)
36
+ });
37
+ const syncBundleSchema = z.object({
38
+ type: z.literal(SYNC_BUNDLE_TYPE),
39
+ formatVersion: z.literal(SYNC_BUNDLE_FORMAT_VERSION),
40
+ bundleId: z.string().trim().min(1),
41
+ createdAt: z.string().trim().min(1),
42
+ sourceDeviceId: z.string().trim().min(1),
43
+ stewardVersion: z.string().trim().min(1),
44
+ repos: z.array(syncRepoSchema),
45
+ states: z.array(syncStateRowSchema),
46
+ archives: z.array(syncArchiveRowSchema)
47
+ });
48
+ function canonicalize(value) {
49
+ if (Array.isArray(value)) {
50
+ return value.map((entry) => canonicalize(entry));
51
+ }
52
+ if (!value || typeof value !== 'object') {
53
+ return value;
54
+ }
55
+ const objectValue = value;
56
+ const keys = Object.keys(objectValue).sort((a, b) => a.localeCompare(b));
57
+ const result = {};
58
+ for (const key of keys) {
59
+ result[key] = canonicalize(objectValue[key]);
60
+ }
61
+ return result;
62
+ }
63
+ export function toCanonicalJson(value) {
64
+ return JSON.stringify(canonicalize(value));
65
+ }
66
+ export function hashCanonicalValue(value) {
67
+ return createHash('sha256').update(toCanonicalJson(value)).digest('hex');
68
+ }
69
+ export function hashNullableCanonicalValue(value) {
70
+ if (value === null || value === undefined) {
71
+ return null;
72
+ }
73
+ return hashCanonicalValue(value);
74
+ }
75
+ export function createSyncFieldHashes(fields) {
76
+ return {
77
+ tasksHash: hashNullableCanonicalValue(fields.tasks),
78
+ progressHash: hashNullableCanonicalValue(fields.progress),
79
+ notesHash: hashNullableCanonicalValue(fields.notes)
80
+ };
81
+ }
82
+ export function parseSyncBundle(value) {
83
+ return syncBundleSchema.parse(value);
84
+ }
85
+ export function validateSyncBundle(value) {
86
+ const parsed = syncBundleSchema.safeParse(value);
87
+ if (parsed.success) {
88
+ return { success: true };
89
+ }
90
+ const issue = parsed.error.issues[0];
91
+ if (!issue) {
92
+ return { success: false, error: 'Invalid sync bundle payload' };
93
+ }
94
+ return {
95
+ success: false,
96
+ error: issue.path.length > 0
97
+ ? `${issue.path.join('.')}: ${issue.message}`
98
+ : issue.message
99
+ };
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thxgg/steward",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Local-first PRD workflow steward with codemode MCP and web UI.",
5
5
  "type": "module",
6
6
  "author": "thxgg",
@@ -57,6 +57,7 @@
57
57
  "dependencies": {
58
58
  "@dagrejs/dagre": "^2.0.4",
59
59
  "@modelcontextprotocol/sdk": "^1.26.0",
60
+ "@pierre/diffs": "^1.0.11",
60
61
  "@vue-flow/background": "^1.3.2",
61
62
  "@vue-flow/controls": "^1.1.3",
62
63
  "@vue-flow/core": "^1.48.2",