gitifact 0.8.1 → 0.8.2

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 (99) hide show
  1. package/dist/browser/assets/{Banner-c1hFCLs8.js → Banner-DQ1G6Jkv.js} +1 -1
  2. package/dist/browser/assets/{BottomSheet-7C5eiOji.js → BottomSheet-BFoM6syr.js} +1 -1
  3. package/dist/browser/assets/Grid-DCCm_m5g.js +1 -0
  4. package/dist/browser/assets/{HoverCard-D6keobP0.js → HoverCard-Bpt8LVFT.js} +1 -1
  5. package/dist/browser/assets/{Icon-EsJf4C7A.js → Icon-DPaLmsmh.js} +3 -3
  6. package/dist/browser/assets/{IconButton-CgOx1Jog.js → IconButton-Br-4XhQr.js} +1 -1
  7. package/dist/browser/assets/{Markdown-Droxhk-G.js → Markdown-B9QJ2Ao-.js} +4 -4
  8. package/dist/browser/assets/{MenuBottomSheet-BOGxMa05.js → MenuBottomSheet-Dgz4h1S7.js} +1 -1
  9. package/dist/browser/assets/{MetadataListItem-C3SqHyk0.js → MetadataListItem-BEwPg27H.js} +1 -1
  10. package/dist/browser/assets/ProgressBarMarkTooltip-BxGSd9KS.js +1 -0
  11. package/dist/browser/assets/{Section-BEbhTpxd.js → Section-Z2PuhE6c.js} +1 -1
  12. package/dist/browser/assets/Selector-BlDYK5Fb.js +2 -0
  13. package/dist/browser/assets/{Tab-VVHagF2Z.js → Tab-BoCyIuQw.js} +1 -1
  14. package/dist/browser/assets/{Table-D4v8xCrw.js → Table-LdzcI4YS.js} +3 -3
  15. package/dist/browser/assets/{TimestampHoverCard-qfGbCkoP.js → TimestampHoverCard-B5JBaQDZ.js} +1 -1
  16. package/dist/browser/assets/{Token-BLd8-jfF.js → Token-CYBSUyAu.js} +1 -1
  17. package/dist/browser/assets/{Tooltip-QZg2pB5o.js → Tooltip-MoU3alsv.js} +1 -1
  18. package/dist/browser/assets/{about-DeUi-_2z.js → about-ClxCgIMf.js} +2 -2
  19. package/dist/browser/assets/{activity-timeline-bFoqFwVu.js → activity-timeline-i0lmNZ1o.js} +2 -2
  20. package/dist/browser/assets/{appearance-CDQ4fLdE.js → appearance-DgDbwXAm.js} +1 -1
  21. package/dist/browser/assets/changelog-bGtJx397.js +2 -0
  22. package/dist/browser/assets/commit-5TIiQrYh.js +4 -0
  23. package/dist/browser/assets/{container.stylex-6OsSOpKw.js → container.stylex-rwI38dw9.js} +1 -1
  24. package/dist/browser/assets/contributor-C1R6Q471.js +1 -0
  25. package/dist/browser/assets/contributors-DjNETDIr.js +1 -0
  26. package/dist/browser/assets/{contributors._email-BRfre3dw.js → contributors._email-DXxCGNZx.js} +1 -1
  27. package/dist/browser/assets/contributors.index-ByVvGuYC.js +1 -0
  28. package/dist/browser/assets/dashboard.index-RoAwRn4H.js +1 -0
  29. package/dist/browser/assets/document-BwoITy_-.js +2 -0
  30. package/dist/browser/assets/{document-JLY2-z8S.js → document-CwA2dxtP.js} +5 -5
  31. package/dist/browser/assets/features-CttwIYBf.js +2 -0
  32. package/dist/browser/assets/features-ZYCEDoiE.css +1 -0
  33. package/dist/browser/assets/{features._featureId-BgFHsRcy.js → features._featureId-DPctVYNz.js} +1 -1
  34. package/dist/browser/assets/features.index-lcNtlMn8.js +1 -0
  35. package/dist/browser/assets/getting-started-Chwezama.js +1 -0
  36. package/dist/browser/assets/git-CtsTwkaq.js +1 -0
  37. package/dist/browser/assets/index-iSt1IRzY.js +48 -0
  38. package/dist/browser/assets/instructions-DULmclHB.js +1 -0
  39. package/dist/browser/assets/instructions._instructionId-CBz40hpF.js +1 -0
  40. package/dist/browser/assets/instructions.agents-CDpXylSu.js +1 -0
  41. package/dist/browser/assets/instructions.index-BZ5tCMZJ.js +1 -0
  42. package/dist/browser/assets/{lazyRouteComponent-JkRa2CHo.js → lazyRouteComponent-DhTs_UEV.js} +1 -1
  43. package/dist/browser/assets/load-more-DxscRoh6.js +1 -0
  44. package/dist/browser/assets/{page-header-CPL7myjo.js → page-header-B9LQWl7J.js} +1 -1
  45. package/dist/browser/assets/records-page-B4tDip2H.css +1 -0
  46. package/dist/browser/assets/records-page-BjyFUmMr.js +1 -0
  47. package/dist/browser/assets/records._recordId-vwjWdPRX.js +1 -0
  48. package/dist/browser/assets/records.commits._commit-CYYgrOxv.js +1 -0
  49. package/dist/browser/assets/records.index-N49ep6ay.js +1 -0
  50. package/dist/browser/assets/records.working-CcYY8eyI.js +1 -0
  51. package/dist/browser/assets/request-state-BeiYxf95.js +1 -0
  52. package/dist/browser/assets/search-CSSuO-jR.js +1 -0
  53. package/dist/browser/assets/search-palette-BCEUlRVv.js +561 -0
  54. package/dist/browser/assets/search-palette-CDmYAo36.css +1 -0
  55. package/dist/browser/assets/{settings-BDVKHrS8.js → settings-I5Mn86wq.js} +1 -1
  56. package/dist/browser/assets/{themeProps-CfjLB1h4.js → themeProps-CkLe5zXY.js} +1 -1
  57. package/dist/browser/assets/{useClipboard-CTfBnLlg.js → useClipboard-Cqx6Upy7.js} +1 -1
  58. package/dist/browser/assets/useDevWarning-Dudfg3qF.js +1 -0
  59. package/dist/browser/assets/{useInfiniteQuery-Bn_R13Ih.js → useInfiniteQuery-tqfloUvn.js} +1 -1
  60. package/dist/browser/assets/{useIsomorphicLayoutEffect-DByj3svz.js → useIsomorphicLayoutEffect-BiEJxDg_.js} +1 -1
  61. package/dist/browser/assets/{useKeyboardHint-lSxUw5Qj.js → useKeyboardHint-HHpn4XUs.js} +1 -1
  62. package/dist/browser/assets/{useMediaQuery-CqJr5z44.js → useMediaQuery-twbHF0pO.js} +1 -1
  63. package/dist/browser/assets/{useScrollLock-CmZKJxOo.js → useScrollLock-D-Y-zHSV.js} +1 -1
  64. package/dist/browser/index.html +24 -24
  65. package/dist/i18n/en/changelog.md +10 -0
  66. package/dist/i18n/ko/changelog.md +10 -0
  67. package/dist/main.js +906 -447
  68. package/package.json +1 -1
  69. package/dist/browser/assets/Grid-BrUBBmhu.js +0 -1
  70. package/dist/browser/assets/ProgressBarMarkTooltip-C5Moep_o.js +0 -1
  71. package/dist/browser/assets/Selector-D_xYW7R_.js +0 -2
  72. package/dist/browser/assets/changelog-Brd1Ninm.js +0 -2
  73. package/dist/browser/assets/commit-CiMtFlZl.js +0 -4
  74. package/dist/browser/assets/contributor-C_IskSHq.js +0 -1
  75. package/dist/browser/assets/contributors-CbwXLc7M.js +0 -1
  76. package/dist/browser/assets/contributors.index-CV84owW7.js +0 -1
  77. package/dist/browser/assets/dashboard.index-CQdr7hsf.js +0 -1
  78. package/dist/browser/assets/document-BZmiLx-e.js +0 -2
  79. package/dist/browser/assets/features-Cj41kcXf.js +0 -4
  80. package/dist/browser/assets/features-g3j2elTj.css +0 -1
  81. package/dist/browser/assets/features.index-Ca0rzFW6.js +0 -1
  82. package/dist/browser/assets/getting-started-CgL3Vi9o.js +0 -1
  83. package/dist/browser/assets/git-B7oxggGB.js +0 -1
  84. package/dist/browser/assets/index-DWRGm9YO.js +0 -48
  85. package/dist/browser/assets/instructions-D3cdAPD3.js +0 -1
  86. package/dist/browser/assets/instructions._instructionId-BkT14646.js +0 -1
  87. package/dist/browser/assets/instructions.agents-B3jLRl1u.js +0 -1
  88. package/dist/browser/assets/instructions.index-8qlhSXoy.js +0 -1
  89. package/dist/browser/assets/records-page-B4Al3fIQ.js +0 -1
  90. package/dist/browser/assets/records-page-D2F1WJrh.css +0 -1
  91. package/dist/browser/assets/records._recordId-Chn1SO5R.js +0 -1
  92. package/dist/browser/assets/records.commits._commit-D4Wk4i_m.js +0 -1
  93. package/dist/browser/assets/records.index-CViTeD08.js +0 -1
  94. package/dist/browser/assets/records.working-BsHKMIz-.js +0 -1
  95. package/dist/browser/assets/request-state-oiWyP9c-.js +0 -1
  96. package/dist/browser/assets/search-BCYi0CBz.js +0 -1
  97. package/dist/browser/assets/search-palette-D0ctICJy.js +0 -561
  98. package/dist/browser/assets/search-palette-DpmGOAIg.css +0 -1
  99. package/dist/browser/assets/useDevWarning-gW8_bNZ_.js +0 -1
package/dist/main.js CHANGED
@@ -10413,15 +10413,15 @@ function bucketFor(state2, inst) {
10413
10413
  var handoff;
10414
10414
  var open = [];
10415
10415
  var memo = {
10416
- alloc(_inst, payload, empty) {
10416
+ alloc(_inst, payload, empty2) {
10417
10417
  const bucket = handoff;
10418
10418
  if (!bucket)
10419
- return empty;
10419
+ return empty2;
10420
10420
  handoff = void 0;
10421
- const entry2 = { value: empty, issues: null };
10421
+ const entry2 = { value: empty2, issues: null };
10422
10422
  bucket.set(payload.value, entry2);
10423
10423
  open.push(entry2);
10424
- return empty;
10424
+ return empty2;
10425
10425
  },
10426
10426
  guard(inst) {
10427
10427
  var _a3;
@@ -13092,8 +13092,8 @@ var error20 = () => {
13092
13092
  case "invalid_union":
13093
13093
  return "\u05E7\u05DC\u05D8 \u05DC\u05D0 \u05EA\u05E7\u05D9\u05DF";
13094
13094
  case "invalid_element": {
13095
- const place2 = withDefinite(issue2.origin ?? "array");
13096
- return `\u05E2\u05E8\u05DA \u05DC\u05D0 \u05EA\u05E7\u05D9\u05DF \u05D1${place2}`;
13095
+ const place3 = withDefinite(issue2.origin ?? "array");
13096
+ return `\u05E2\u05E8\u05DA \u05DC\u05D0 \u05EA\u05E7\u05D9\u05DF \u05D1${place3}`;
13097
13097
  }
13098
13098
  default:
13099
13099
  return `\u05E7\u05DC\u05D8 \u05DC\u05D0 \u05EA\u05E7\u05D9\u05DF`;
@@ -25064,20 +25064,73 @@ var instruction = external_exports.strictObject({
25064
25064
  var agents = external_exports.strictObject({ path: external_exports.string(), body: external_exports.string(), updatedAt: external_exports.string().nullable() });
25065
25065
  var problem = external_exports.strictObject({ code: external_exports.string(), path: external_exports.string(), message: external_exports.string() });
25066
25066
  var oid2 = external_exports.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/);
25067
- var browserSpecsV7 = external_exports.strictObject({
25068
- contract: external_exports.literal("browser-specs"),
25069
- version: external_exports.literal(7),
25067
+ var indexFeature = external_exports.strictObject({
25068
+ id: external_exports.string(),
25069
+ path: external_exports.string(),
25070
+ title: external_exports.string(),
25071
+ state,
25072
+ requirements: external_exports.array(external_exports.strictObject({ id: external_exports.string(), path: external_exports.string(), title: external_exports.string(), description: external_exports.string() })),
25073
+ designs: external_exports.array(external_exports.strictObject({ id: external_exports.string(), path: external_exports.string(), title: external_exports.string() }))
25074
+ });
25075
+ var indexInstruction = external_exports.strictObject({ id: external_exports.string(), name: external_exports.string(), path: external_exports.string(), title: external_exports.string() });
25076
+ var browserCheckoutV1 = external_exports.strictObject({
25077
+ contract: external_exports.literal("browser-checkout"),
25078
+ version: external_exports.literal(1),
25070
25079
  sessionId: external_exports.string(),
25071
25080
  head: oid2.nullable(),
25072
25081
  observedAt: external_exports.string(),
25073
25082
  working: external_exports.boolean(),
25074
25083
  stamp: external_exports.string(),
25075
- features: external_exports.array(feature),
25076
- instructions: external_exports.array(instruction),
25077
- agents: agents.nullable(),
25078
25084
  problems: external_exports.array(problem),
25085
+ index: external_exports.strictObject({ features: external_exports.array(indexFeature), instructions: external_exports.array(indexInstruction), people: external_exports.array(external_exports.strictObject({ name: external_exports.string(), email: external_exports.string() })) })
25086
+ });
25087
+ var featureRow = external_exports.strictObject({
25088
+ id: external_exports.string(),
25089
+ path: external_exports.string(),
25090
+ title: external_exports.string(),
25091
+ description: external_exports.string(),
25092
+ state,
25093
+ designs: external_exports.number().int().nonnegative(),
25079
25094
  contributors: external_exports.array(contributor),
25080
- contributorsLimited: external_exports.boolean()
25095
+ updatedAt: external_exports.string().nullable(),
25096
+ requirementCount: external_exports.number().int().nonnegative(),
25097
+ // `acceptance`: the numbered items under the acceptance heading, null without one. `designed`: a design names it.
25098
+ requirements: external_exports.array(external_exports.strictObject({ id: external_exports.string(), title: external_exports.string(), description: external_exports.string(), state, acceptance: external_exports.number().int().positive().nullable(), designed: external_exports.boolean() })),
25099
+ hidden: external_exports.number().int().nonnegative()
25100
+ });
25101
+ var browserFeaturesV1 = external_exports.strictObject({
25102
+ contract: external_exports.literal("browser-features"),
25103
+ version: external_exports.literal(1),
25104
+ sessionId: external_exports.string(),
25105
+ total: external_exports.number().int().nonnegative(),
25106
+ all: external_exports.number().int().nonnegative(),
25107
+ requirements: external_exports.number().int().nonnegative(),
25108
+ mostRequirements: external_exports.number().int().nonnegative(),
25109
+ next: external_exports.string().nullable(),
25110
+ features: external_exports.array(featureRow)
25111
+ });
25112
+ var browserFeatureV1 = external_exports.strictObject({ contract: external_exports.literal("browser-feature"), version: external_exports.literal(1), sessionId: external_exports.string(), feature });
25113
+ var browserInstructionsV1 = external_exports.strictObject({
25114
+ contract: external_exports.literal("browser-instructions"),
25115
+ version: external_exports.literal(1),
25116
+ sessionId: external_exports.string(),
25117
+ instructions: external_exports.array(instruction),
25118
+ agents: agents.nullable()
25119
+ });
25120
+ var browserContributorsV1 = external_exports.strictObject({
25121
+ contract: external_exports.literal("browser-contributors"),
25122
+ version: external_exports.literal(1),
25123
+ sessionId: external_exports.string(),
25124
+ total: external_exports.number().int().nonnegative(),
25125
+ next: external_exports.string().nullable(),
25126
+ people: external_exports.array(external_exports.strictObject({ email: external_exports.string(), name: external_exports.string(), commits: external_exports.number().int().nonnegative(), latest: external_exports.string(), features: external_exports.number().int().nonnegative() }))
25127
+ });
25128
+ var browserContributorV1 = external_exports.strictObject({
25129
+ contract: external_exports.literal("browser-contributor"),
25130
+ version: external_exports.literal(1),
25131
+ sessionId: external_exports.string(),
25132
+ person: contributor,
25133
+ features: external_exports.array(external_exports.strictObject({ id: external_exports.string(), title: external_exports.string(), commits: external_exports.number().int().nonnegative(), requirements: external_exports.number().int().nonnegative() }))
25081
25134
  });
25082
25135
  var kind = external_exports.enum(["feature", "requirement", "design", "wiki", "instruction"]);
25083
25136
  var reference = external_exports.strictObject({ id: external_exports.string(), title: external_exports.string(), specId: external_exports.string(), path: external_exports.string() }).nullable();
@@ -25122,11 +25175,13 @@ var browserHistoryV6 = external_exports.strictObject({
25122
25175
  next: oid2.nullable(),
25123
25176
  events: external_exports.array(event)
25124
25177
  });
25125
- var browserHistorySummaryV4 = external_exports.strictObject({
25178
+ var browserHistorySummaryV5 = external_exports.strictObject({
25126
25179
  contract: external_exports.literal("browser-history-summary"),
25127
- version: external_exports.literal(4),
25180
+ version: external_exports.literal(5),
25128
25181
  sessionId: external_exports.string(),
25129
25182
  head: oid2,
25183
+ // The three who committed most, and how many others there are and how many commits they made between them.
25184
+ people: external_exports.strictObject({ total: external_exports.number().int().nonnegative(), top: external_exports.array(contributor), rest: external_exports.strictObject({ count: external_exports.number().int().nonnegative(), commits: external_exports.number().int().nonnegative() }) }),
25130
25185
  total: external_exports.number().int().nonnegative(),
25131
25186
  byType: external_exports.strictObject({ created: external_exports.number().int().nonnegative(), modified: external_exports.number().int().nonnegative(), moved: external_exports.number().int().nonnegative(), deleted: external_exports.number().int().nonnegative() }),
25132
25187
  // One entry per commit within three weeks of the newest change, so the reader can count by its own calendar day.
@@ -25134,22 +25189,23 @@ var browserHistorySummaryV4 = external_exports.strictObject({
25134
25189
  // The newest commits: a few of their changes each and how many there are in all.
25135
25190
  recent: external_exports.array(external_exports.strictObject({ commit: oid2, count: external_exports.number().int().positive(), events: external_exports.array(event) }))
25136
25191
  });
25137
- var browserSearchV2 = external_exports.strictObject({
25192
+ var searchKind = external_exports.enum(["feature", "requirement", "design", "instruction", "record", "commit"]);
25193
+ var searchHit = external_exports.strictObject({
25194
+ id: external_exports.string(),
25195
+ kind: searchKind,
25196
+ title: external_exports.string(),
25197
+ where: external_exports.string(),
25198
+ line: external_exports.string(),
25199
+ // What the hit opens: a feature (with the requirement or design tab), an instruction, a record, or a commit.
25200
+ featureId: external_exports.string().optional(),
25201
+ commit: oid2.optional()
25202
+ });
25203
+ var browserSearchV3 = external_exports.strictObject({
25138
25204
  contract: external_exports.literal("browser-search"),
25139
- version: external_exports.literal(2),
25205
+ version: external_exports.literal(3),
25140
25206
  sessionId: external_exports.string(),
25141
25207
  query: external_exports.string(),
25142
- hits: external_exports.array(external_exports.strictObject({
25143
- id: external_exports.string(),
25144
- kind: external_exports.enum(["feature", "requirement", "design", "instruction", "history"]),
25145
- title: external_exports.string(),
25146
- where: external_exports.string(),
25147
- line: external_exports.string(),
25148
- // What the hit opens: a feature (with the requirement or design tab), an instruction, or one change in the activity.
25149
- featureId: external_exports.string().optional(),
25150
- documentId: external_exports.string().optional(),
25151
- key: external_exports.string().optional()
25152
- }))
25208
+ groups: external_exports.array(external_exports.strictObject({ group: external_exports.union([searchKind, external_exports.literal("recent")]), total: external_exports.number().int().nonnegative(), next: external_exports.string().nullable(), hits: external_exports.array(searchHit) }))
25153
25209
  });
25154
25210
  var headQuery = oid2;
25155
25211
  var count = (max) => external_exports.string().regex(/^(0|[1-9]\d{0,6})$/).transform(Number).pipe(external_exports.number().int().min(0).max(max));
@@ -25164,7 +25220,26 @@ var browserHistoryQueryV4 = external_exports.strictObject({
25164
25220
  q: external_exports.string().max(200).optional()
25165
25221
  });
25166
25222
  var browserHistorySummaryQueryV1 = external_exports.strictObject({ head: headQuery });
25167
- var browserSearchQueryV1 = external_exports.strictObject({ q: external_exports.string().min(1).max(200), head: headQuery.optional() });
25223
+ var featureId = external_exports.string().regex(/^S-[a-z2-7]{10}$/);
25224
+ var browserFeaturesQueryV1 = external_exports.strictObject({
25225
+ q: external_exports.string().max(200).optional(),
25226
+ design: external_exports.enum(["yes", "no"]).optional(),
25227
+ author: external_exports.string().min(1).max(320).optional(),
25228
+ sort: external_exports.enum(["title", "requirements", "updated"]).optional(),
25229
+ dir: external_exports.enum(["asc", "desc"]).optional(),
25230
+ after: featureId.optional(),
25231
+ limit: count(50).pipe(external_exports.number().min(1)).optional()
25232
+ });
25233
+ var browserFeatureQueryV1 = external_exports.strictObject({ id: featureId });
25234
+ var browserContributorsQueryV1 = external_exports.strictObject({ q: external_exports.string().max(200).optional(), after: external_exports.string().min(1).max(320).optional(), limit: count(50).pipe(external_exports.number().min(1)).optional() });
25235
+ var browserContributorQueryV1 = external_exports.strictObject({ email: external_exports.string().min(1).max(320) });
25236
+ var browserSearchQueryV2 = external_exports.strictObject({
25237
+ q: external_exports.string().max(200),
25238
+ head: headQuery.optional(),
25239
+ group: searchKind.optional(),
25240
+ after: external_exports.string().min(1).max(200).optional(),
25241
+ limit: count(50).pipe(external_exports.number().min(1)).optional()
25242
+ });
25168
25243
  var browserInstructionFileQueryV1 = external_exports.strictObject({ id: external_exports.string().regex(/^I-[a-z2-7]{10}$/), path: external_exports.string().min(1).max(1e3) });
25169
25244
  var changelogQueryV1 = external_exports.strictObject({ lang: external_exports.string().regex(/^[a-z]{2}(?:-[A-Z]{2})?$/).optional() });
25170
25245
  var fileStatus = external_exports.enum(["added", "modified", "deleted", "renamed"]);
@@ -25176,12 +25251,13 @@ var commitFile = external_exports.strictObject({
25176
25251
  additions: external_exports.number().int().nonnegative().nullable(),
25177
25252
  deletions: external_exports.number().int().nonnegative().nullable()
25178
25253
  });
25179
- var browserCommitFilesV1 = external_exports.strictObject({
25254
+ var browserCommitFilesV2 = external_exports.strictObject({
25180
25255
  contract: external_exports.literal("browser-commit-files"),
25181
- version: external_exports.literal(1),
25256
+ version: external_exports.literal(2),
25182
25257
  sessionId: external_exports.string(),
25183
25258
  commit: oid2,
25184
25259
  total: external_exports.number().int().nonnegative(),
25260
+ next: external_exports.string().nullable(),
25185
25261
  files: external_exports.array(commitFile)
25186
25262
  });
25187
25263
  var browserCommitFileV1 = external_exports.strictObject({
@@ -25196,7 +25272,7 @@ var browserCommitFileV1 = external_exports.strictObject({
25196
25272
  binary: external_exports.boolean(),
25197
25273
  tooLarge: external_exports.boolean()
25198
25274
  });
25199
- var browserCommitFilesQueryV1 = external_exports.strictObject({ commit: oid2 });
25275
+ var browserCommitFilesQueryV2 = external_exports.strictObject({ commit: oid2, after: external_exports.string().min(1).max(4096).optional(), limit: count(100).pipe(external_exports.number().min(1)).optional() });
25200
25276
  var browserCommitFileQueryV1 = external_exports.strictObject({ commit: oid2, path: external_exports.string().min(1).max(4096) });
25201
25277
  var browserCommitV4 = external_exports.strictObject({
25202
25278
  contract: external_exports.literal("browser-commit"),
@@ -25212,7 +25288,8 @@ var browserCommitV4 = external_exports.strictObject({
25212
25288
  next: external_exports.string().nullable(),
25213
25289
  changes: external_exports.array(event)
25214
25290
  });
25215
- var browserCommitQueryV2 = external_exports.strictObject({ commit: oid2, after: external_exports.string().min(1).max(200).optional(), limit: count(500).pipe(external_exports.number().min(1)).optional() });
25291
+ var recordId = external_exports.string().regex(/^(?:DR|H)-[A-Za-z0-9_-]{1,64}$/);
25292
+ var browserCommitQueryV3 = external_exports.strictObject({ commit: oid2, after: external_exports.string().min(1).max(200).optional(), limit: count(100).pipe(external_exports.number().min(1)).optional(), record: recordId.optional() });
25216
25293
  var browserCommitChangeV1 = external_exports.strictObject({
25217
25294
  contract: external_exports.literal("browser-commit-change"),
25218
25295
  version: external_exports.literal(1),
@@ -25223,14 +25300,15 @@ var browserCommitChangeV1 = external_exports.strictObject({
25223
25300
  after: snapshot
25224
25301
  });
25225
25302
  var browserCommitChangeQueryV1 = external_exports.strictObject({ commit: oid2, id: external_exports.string().min(1).max(100) });
25226
- var recordId = external_exports.string().regex(/^(?:DR|H)-[A-Za-z0-9_-]{1,64}$/);
25227
- var browserRecordV1 = external_exports.strictObject({
25303
+ var browserRecordV2 = external_exports.strictObject({
25228
25304
  contract: external_exports.literal("browser-record"),
25229
- version: external_exports.literal(1),
25305
+ version: external_exports.literal(2),
25230
25306
  sessionId: external_exports.string(),
25231
25307
  head: oid2,
25232
25308
  id: recordId,
25233
- commit: oid2
25309
+ commit: oid2,
25310
+ record: record2,
25311
+ others: external_exports.number().int().nonnegative()
25234
25312
  });
25235
25313
  var browserRecordQueryV1 = external_exports.strictObject({ head: oid2, id: recordId });
25236
25314
  var browserInstructionFileV1 = external_exports.strictObject({
@@ -25505,6 +25583,8 @@ var messages_default3 = {
25505
25583
  "server.sessionChanged": "\uC11C\uBC84 \uC138\uC158\uC774 \uBCC0\uACBD\uB410\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC5F0\uACB0\uD558\uC138\uC694.",
25506
25584
  "server.badRange": "\uC870\uD68C \uBC94\uC704\uB97C \uD655\uC778\uD558\uC138\uC694.",
25507
25585
  "server.commitNotFound": "\uC774 \uC800\uC7A5\uC18C\uC5D0 \uADF8 \uCEE4\uBC0B\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
25586
+ "server.featureNotFound": "\uCCB4\uD06C\uC544\uC6C3\uC5D0 \uADF8 \uAE30\uB2A5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
25587
+ "server.contributorNotFound": "HEAD\uC5D0 \uADF8 \uC774\uBA54\uC77C\uB85C \uCEE4\uBC0B\uD55C \uC0AC\uB78C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
25508
25588
  "server.cursorNotFound": "\uC774\uC5B4 \uC77D\uC744 \uC704\uCE58\uB97C \uBAA9\uB85D\uC5D0\uC11C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uCC98\uC74C\uBD80\uD130 \uB2E4\uC2DC \uBD88\uB7EC\uC624\uC138\uC694.",
25509
25589
  "server.workingChangeNotFound": "\uCEE4\uBC0B \uC804 \uBCC0\uACBD\uC5D0 \uADF8 \uBB38\uC11C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
25510
25590
  "server.recordNotFound": "\uC774 \uC774\uB825\uC5D0 \uADF8 \uACB0\uC815\uAE30\uB85D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
@@ -25571,7 +25651,6 @@ var messages_default3 = {
25571
25651
  "agentBlock.duplicateStart": "GITIFACT \uC2DC\uC791 \uB9C8\uCEE4\uAC00 \uC5EC\uB7EC \uAC1C\uC785\uB2C8\uB2E4. \uC911\uBCF5 \uBE14\uB85D\uC744 \uC815\uB9AC\uD55C \uB4A4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
25572
25652
  "specReader.headUnreadable": "HEAD\uB97C \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
25573
25653
  "specReader.schemaRequired": "\uC0C8 \uC2A4\uD0A4\uB9C8 \uD504\uB85C\uC81D\uD2B8\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
25574
- "specReader.authorUnreadable": "Git \uC791\uC131\uC790\uB97C \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
25575
25654
  "specReader.historyUnreadable": "Git \uC774\uB825 \uD615\uC2DD\uC744 \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
25576
25655
  "specReader.projectChanged": "\uC870\uD68C \uC911 \uD504\uB85C\uC81D\uD2B8\uAC00 \uBC14\uB00C\uC5C8\uC2B5\uB2C8\uB2E4. \uC0C8\uB85C\uACE0\uCE68\uD558\uC138\uC694.",
25577
25656
  "browser.invalidPort": "\uD3EC\uD2B8\uB294 0\uBD80\uD130 65535 \uC0AC\uC774\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4.",
@@ -25809,6 +25888,8 @@ var messages_default4 = {
25809
25888
  "server.sessionChanged": "The server session changed. Reconnect to continue.",
25810
25889
  "server.badRange": "Check the requested range.",
25811
25890
  "server.commitNotFound": "This repository has no such commit.",
25891
+ "server.featureNotFound": "The checkout has no such feature.",
25892
+ "server.contributorNotFound": "Nobody with this email committed to HEAD.",
25812
25893
  "server.cursorNotFound": "The place to read on from is no longer in the list. Load it again from the start.",
25813
25894
  "server.workingChangeNotFound": "No uncommitted change of that document.",
25814
25895
  "server.recordNotFound": "This history has no such decision record.",
@@ -25875,7 +25956,6 @@ var messages_default4 = {
25875
25956
  "agentBlock.duplicateStart": "Multiple GITIFACT start markers found. Remove duplicate blocks before retrying.",
25876
25957
  "specReader.headUnreadable": "Could not read HEAD.",
25877
25958
  "specReader.schemaRequired": "A project using the supported schema is required.",
25878
- "specReader.authorUnreadable": "Could not read Git authors.",
25879
25959
  "specReader.historyUnreadable": "Could not read Git history.",
25880
25960
  "specReader.projectChanged": "The project changed while reading. Refresh to continue.",
25881
25961
  "browser.invalidPort": "Port must be an integer between 0 and 65535.",
@@ -26414,7 +26494,7 @@ function storeReader(cwd) {
26414
26494
  import { DatabaseSync } from "node:sqlite";
26415
26495
  import { lstat, mkdir, rm, writeFile } from "node:fs/promises";
26416
26496
  import { join as join2 } from "node:path";
26417
- var CACHE_FORMAT = 9;
26497
+ var CACHE_FORMAT = 11;
26418
26498
  var CACHE_DIR = ".gitifact/cache";
26419
26499
  var SCHEMA = `
26420
26500
  -- Working-tree files already read: the modification time and size decide whether a file is read again.
@@ -26444,7 +26524,17 @@ var SCHEMA = `
26444
26524
  CREATE TABLE lineage (head TEXT NOT NULL, pos INTEGER NOT NULL, oid TEXT NOT NULL, PRIMARY KEY (head, pos));
26445
26525
  CREATE INDEX lineage_by_commit ON lineage (head, oid);
26446
26526
  CREATE TABLE heads (head TEXT PRIMARY KEY, seen INTEGER NOT NULL);
26447
- -- Everything the search box finds: documents of the working tree (scope 'checkout') and past changes ('history').
26527
+ -- Every commit the log has met, code commits too: who made it and when, and whether it is a format migration.
26528
+ -- 'reach' is 1 for the commits of the HEAD the log follows now; contributors are counted over those.
26529
+ CREATE TABLE log (oid TEXT PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL, time INTEGER NOT NULL, date TEXT NOT NULL,
26530
+ subject TEXT NOT NULL, migration INTEGER NOT NULL, reach INTEGER NOT NULL);
26531
+ CREATE INDEX log_by_reach ON log (reach, email);
26532
+ -- The store folders a commit touched: a feature folder, an instruction folder, or AGENTS.md.
26533
+ CREATE TABLE touches (oid TEXT NOT NULL, folder TEXT NOT NULL, PRIMARY KEY (oid, folder));
26534
+ CREATE INDEX touches_by_folder ON touches (folder);
26535
+ -- What the log follows: the HEAD ('head') and the fingerprint of the .mailmap its names were read with ('mailmap').
26536
+ CREATE TABLE state (key TEXT PRIMARY KEY, value TEXT NOT NULL);
26537
+ -- What the search box finds among the working tree's documents (scope 'checkout'); records are searched in their table.
26448
26538
  -- The searched columns hold lower-cased text; trigram lets LIKE '%...%' use the index from three characters on.
26449
26539
  CREATE VIRTUAL TABLE search USING fts5(
26450
26540
  scope UNINDEXED, kind UNINDEXED, ref UNINDEXED, oid UNINDEXED, payload UNINDEXED,
@@ -26728,6 +26818,40 @@ function createDocumentCache(root, database) {
26728
26818
  }));
26729
26819
  });
26730
26820
  },
26821
+ /**
26822
+ * The working documents of one kind whose title, place or text holds the query, a page at a time: a title that
26823
+ * starts with it first, then a title, a place, and the text; by title within each. `after` is the ID of the last
26824
+ * hit of the page before; one that no longer matches answers undefined. Each hit names its feature's ID.
26825
+ */
26826
+ async searchKind(kind2, raw, after, limit) {
26827
+ const query = raw.trim().toLowerCase();
26828
+ await sync();
26829
+ return database.with((db) => {
26830
+ const literal2 = /[\\%_]/.test(query);
26831
+ const match = (column) => literal2 ? `instr(${column}, :q) > 0` : `${column} LIKE :like`;
26832
+ const ordered = `WITH hits AS (
26833
+ SELECT json_extract(payload, '$.id') AS id, payload, title, ref,
26834
+ CASE WHEN substr(title, 1, length(:q)) = :q THEN 0 WHEN instr(title, :q) > 0 THEN 1 WHEN instr(place, :q) > 0 THEN 2 ELSE 3 END AS rank
26835
+ FROM search WHERE scope = 'checkout' AND kind = :kind AND (${match("title")} OR ${match("place")} OR ${match("body")})),
26836
+ ordered AS (SELECT *, row_number() OVER (ORDER BY rank, title, ref) AS n FROM hits)`;
26837
+ const params = { q: query, kind: kind2, ...literal2 ? {} : { like: containing(query) } };
26838
+ const total = Number(db.prepare(`${ordered} SELECT count(*) AS n FROM ordered`).get(params).n);
26839
+ let start = 0;
26840
+ if (after !== void 0) {
26841
+ const at = db.prepare(`${ordered} SELECT n FROM ordered WHERE id = :after`).get({ ...params, after });
26842
+ if (!at) return void 0;
26843
+ start = Number(at.n);
26844
+ }
26845
+ const rows = db.prepare(`${ordered} SELECT payload FROM ordered WHERE n > :start ORDER BY n LIMIT :limit`).all({ ...params, start, limit: limit + 1 });
26846
+ const features = new Map(db.prepare("SELECT feature, id FROM documents WHERE kind = 'feature'").all().map((r) => [r.feature, r.id]));
26847
+ const hits = rows.slice(0, limit).map((r) => {
26848
+ const p = JSON.parse(r.payload);
26849
+ const featureId2 = p.feature ? features.get(p.feature) : void 0;
26850
+ return { id: p.id, kind: kind2, title: p.title, where: p.where, line: snippet(p.body, query), ...featureId2 ? { featureId: featureId2 } : {} };
26851
+ });
26852
+ return { total, next: rows.length > limit ? hits[hits.length - 1].id : null, hits };
26853
+ });
26854
+ },
26731
26855
  /** Documents that refer to `id` from their frontmatter: designs naming a requirement or a source. */
26732
26856
  async referencing(id) {
26733
26857
  await sync();
@@ -27096,20 +27220,19 @@ var listed = (e) => {
27096
27220
  return { ...event2, before: reference2(e.before), after: reference2(e.after) };
27097
27221
  };
27098
27222
  var sideOf3 = (snapshot5, rev) => !snapshot5 ? null : rev ? { rev, path: snapshot5.path } : snapshot5;
27099
- var titleOf = (e) => (e.after ?? e.before)?.title ?? e.id;
27100
27223
  var DAY = 864e5;
27101
27224
  var KEPT_HEADS = 4;
27102
27225
  var RECENT_COMMITS = 3;
27103
27226
  var RECENT_CHANGES = 12;
27104
- function createHistory(database, git) {
27227
+ function createHistory(database, git, originals) {
27105
27228
  const changes2 = createCommitChanges(git);
27229
+ const texts = originals ?? changes2;
27106
27230
  const building = /* @__PURE__ */ new Map();
27107
27231
  const built = /* @__PURE__ */ new Set();
27108
27232
  function insert(db, commits, readers) {
27109
27233
  const commit = db.prepare("INSERT OR IGNORE INTO commits (oid, reader) VALUES (?, ?)");
27110
27234
  const change2 = db.prepare("INSERT OR IGNORE INTO changes (key, oid, ord, id, kind, types, email, date, before_spec, after_spec, needle, row, detail) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)");
27111
27235
  const record4 = db.prepare("INSERT OR IGNORE INTO records (oid, id, title, sections) VALUES (?, ?, ?, ?)");
27112
- const search = db.prepare("INSERT INTO search (scope, kind, ref, oid, payload, title, place, body) VALUES (?,?,?,?,?,?,?,?)");
27113
27236
  transaction(db, () => {
27114
27237
  for (const c of commits) {
27115
27238
  if (!Number(commit.run(c.commit, readers.get(c.commit) ?? "current").changes)) continue;
@@ -27132,23 +27255,19 @@ function createHistory(database, git) {
27132
27255
  JSON.stringify(row),
27133
27256
  JSON.stringify(detail)
27134
27257
  );
27135
- const line2 = e.records.length ? e.records.map((r) => [r.title, ...r.sections.map((s) => s.body)].join(" ")).join(" \xB7 ") : e.message;
27136
- const where2 = e.commit.slice(0, 7) + " \xB7 " + e.author;
27137
- search.run(
27138
- "history",
27139
- "history",
27140
- e.key,
27141
- e.commit,
27142
- JSON.stringify({ title: titleOf(e), where: where2, line: line2 }),
27143
- titleOf(e).toLowerCase(),
27144
- where2.toLowerCase(),
27145
- [line2, e.message, e.id].join(" ").toLowerCase()
27146
- );
27147
27258
  });
27148
27259
  }
27149
27260
  });
27150
27261
  }
27151
27262
  async function build(head) {
27263
+ const known = await database.with((db) => {
27264
+ const seen = !!db.prepare("SELECT 1 FROM heads WHERE head = ?").get(head);
27265
+ if (seen) db.prepare("UPDATE heads SET seen = ? WHERE head = ?").run(Date.now(), head);
27266
+ return seen;
27267
+ });
27268
+ if (!known) await read(head);
27269
+ }
27270
+ async function read(head) {
27152
27271
  const lineage = await changes2.lineage(head);
27153
27272
  const readers = await changes2.readers(head, lineage);
27154
27273
  const known = await database.with((db) => new Map(db.prepare("SELECT oid, reader FROM commits").all().map((r) => [r.oid, r.reader])));
@@ -27158,13 +27277,14 @@ function createHistory(database, git) {
27158
27277
  db.prepare("DELETE FROM commits WHERE oid = ?").run(oid3);
27159
27278
  db.prepare("DELETE FROM changes WHERE oid = ?").run(oid3);
27160
27279
  db.prepare("DELETE FROM records WHERE oid = ?").run(oid3);
27161
- db.prepare("DELETE FROM search WHERE scope = 'history' AND oid = ?").run(oid3);
27162
27280
  }
27281
+ db.prepare("DELETE FROM lineage WHERE head != ?").run(head);
27282
+ db.prepare("DELETE FROM heads WHERE head != ?").run(head);
27163
27283
  }));
27164
27284
  const missing2 = lineage.filter((oid3) => !known.has(oid3) || stale.includes(oid3));
27165
27285
  for (let i = 0; i < missing2.length; i += 100) {
27166
- const read = await changes2.of(missing2.slice(i, i + 100), readers);
27167
- await database.with((db) => insert(db, read, readers));
27286
+ const read2 = await changes2.of(missing2.slice(i, i + 100), readers);
27287
+ await database.with((db) => insert(db, read2, readers));
27168
27288
  }
27169
27289
  await database.with((db) => transaction(db, () => {
27170
27290
  db.prepare("DELETE FROM lineage WHERE head = ?").run(head);
@@ -27191,15 +27311,21 @@ function createHistory(database, git) {
27191
27311
  async function readCommit(commit) {
27192
27312
  const known = await database.with((db) => !!db.prepare("SELECT 1 FROM commits WHERE oid = ?").get(commit));
27193
27313
  if (known) return;
27194
- const read = await changes2.of([commit]).catch(() => void 0);
27195
- if (read) await database.with((db) => insert(db, read, /* @__PURE__ */ new Map()));
27314
+ const read2 = await changes2.of([commit]).catch(() => void 0);
27315
+ if (read2) await database.with((db) => insert(db, read2, /* @__PURE__ */ new Map()));
27316
+ }
27317
+ function sameRecord(db, commit, id) {
27318
+ if (!id.startsWith("H-")) return [id];
27319
+ const own2 = db.prepare("SELECT title, sections FROM records WHERE oid = ? AND id = ?").get(commit, id);
27320
+ if (!own2) return [id];
27321
+ return db.prepare("SELECT id FROM records WHERE oid = ? AND id LIKE 'H-%' AND title = ? AND sections = ?").all(commit, own2.title, own2.sections).map((r) => r.id);
27196
27322
  }
27197
27323
  function hydrate(db, rows) {
27198
27324
  const events = rows.map((row) => JSON.parse(row));
27199
- const read = db.prepare("SELECT id, title, sections FROM records WHERE oid = ?");
27325
+ const read2 = db.prepare("SELECT id, title, sections FROM records WHERE oid = ?");
27200
27326
  const byCommit = /* @__PURE__ */ new Map();
27201
27327
  for (const e of events) if (e.records.length && !byCommit.has(e.commit)) {
27202
- byCommit.set(e.commit, new Map(read.all(e.commit).map((r) => [r.id, { id: r.id, title: r.title, sections: JSON.parse(r.sections) }])));
27328
+ byCommit.set(e.commit, new Map(read2.all(e.commit).map((r) => [r.id, { id: r.id, title: r.title, sections: JSON.parse(r.sections) }])));
27203
27329
  }
27204
27330
  return events.map((e) => ({ ...e, records: e.records.flatMap((id) => {
27205
27331
  const r = byCommit.get(e.commit)?.get(id);
@@ -27216,8 +27342,8 @@ function createHistory(database, git) {
27216
27342
  if (value && "rev" in value) places.push({ rev: value.rev, path: value.path, specId: events[i][key]?.specId ?? "" });
27217
27343
  }
27218
27344
  });
27219
- const read = places.length ? await changes2.sides(places) : [];
27220
- const text4 = new Map(places.map((p, i) => [p.rev + ":" + p.path, read[i] ?? null]));
27345
+ const read2 = places.length ? await texts.sides(places) : [];
27346
+ const text4 = new Map(places.map((p, i) => [p.rev + ":" + p.path, read2[i] ?? null]));
27221
27347
  const resolve = (value) => !value ? null : "rev" in value ? text4.get(value.rev + ":" + value.path) ?? null : value;
27222
27348
  return events.map((event2, i) => ({ event: event2, before: resolve(sides[i].before), after: resolve(sides[i].after) }));
27223
27349
  }
@@ -27249,7 +27375,7 @@ function createHistory(database, git) {
27249
27375
  return {
27250
27376
  ensure,
27251
27377
  /** The document and reason files of one commit, read from Git and not kept. */
27252
- filesAt: (rev) => changes2.tree(rev),
27378
+ filesAt: (rev) => texts.tree(rev),
27253
27379
  /**
27254
27380
  * The changes that match, newest first, a page of whole commits: `limit` commits after the commit `after`, each
27255
27381
  * with all of its matching changes, and how many changes and commits match in all. `next` is the last commit of the
@@ -27278,11 +27404,15 @@ function createHistory(database, git) {
27278
27404
  };
27279
27405
  });
27280
27406
  },
27281
- /** The changes of one commit without their text, a page at a time after the change `after` (a key): the commit page's list. */
27282
- async commitChanges(commit, after, limit) {
27407
+ /**
27408
+ * The changes of one commit without their text, a page at a time after the change `after` (a key): the commit page's
27409
+ * list. With `record`, only the changes that record explains: the record page's documents.
27410
+ */
27411
+ async commitChanges(commit, after, limit, record4) {
27283
27412
  await readCommit(commit);
27284
27413
  return database.with((db) => {
27285
- const all = db.prepare("SELECT key, row FROM changes WHERE oid = ? ORDER BY ord").all(commit);
27414
+ const ids = record4 === void 0 ? void 0 : JSON.stringify(sameRecord(db, commit, record4));
27415
+ const all = ids === void 0 ? db.prepare("SELECT key, row FROM changes WHERE oid = ? ORDER BY ord").all(commit) : db.prepare("SELECT key, row FROM changes c WHERE oid = ? AND EXISTS (SELECT 1 FROM json_each(c.row, '$.records') j WHERE j.value IN (SELECT value FROM json_each(?))) ORDER BY ord").all(commit, ids);
27286
27416
  const start = after === void 0 ? 0 : all.findIndex((r) => r.key === after) + 1;
27287
27417
  if (after !== void 0 && !start) return void 0;
27288
27418
  const shown = all.slice(start, start + limit);
@@ -27372,86 +27502,226 @@ function createHistory(database, git) {
27372
27502
  const commits = db.prepare(`SELECT l.oid AS oid, max(c.date) AS date, count(*) AS n ${from} GROUP BY l.pos, l.oid ORDER BY l.pos`).all(head);
27373
27503
  const newest = Math.max(0, ...commits.map((c) => Date.parse(c.date)).filter(Number.isFinite));
27374
27504
  const pulse = commits.filter((c) => Date.parse(c.date) >= newest - 22 * DAY).map((c) => ({ date: c.date, count: Number(c.n) }));
27375
- const recent = commits.slice(0, RECENT_COMMITS).map((c) => ({
27505
+ const recent2 = commits.slice(0, RECENT_COMMITS).map((c) => ({
27376
27506
  commit: c.oid,
27377
27507
  count: Number(c.n),
27378
27508
  events: hydrate(db, db.prepare("SELECT row FROM changes WHERE oid = ? ORDER BY ord LIMIT ?").all(c.oid, RECENT_CHANGES).map((r) => r.row))
27379
27509
  }));
27380
- return { total, byType: { created: count2("created"), modified: count2("modified"), moved: count2("moved"), deleted: count2("deleted") }, pulse, recent };
27510
+ return { total, byType: { created: count2("created"), modified: count2("modified"), moved: count2("moved"), deleted: count2("deleted") }, pulse, recent: recent2 };
27381
27511
  });
27382
27512
  },
27383
- /** The commit of `head`'s history that added a record, found through the records its changes carry. */
27384
- async commitOfRecord(head, id) {
27513
+ /**
27514
+ * The record of `head`'s history with the commit that added it, and how many other records that commit holds (a
27515
+ * 0.7 reason written once over several documents counts once). Undefined when the history has no such record.
27516
+ */
27517
+ async recordOf(head, id) {
27385
27518
  await ensure(head);
27386
- return database.with((db) => db.prepare("SELECT l.oid AS oid FROM lineage l JOIN records r ON r.oid = l.oid WHERE l.head = ? AND r.id = ? ORDER BY l.pos LIMIT 1").get(head, id)?.oid);
27519
+ return database.with((db) => {
27520
+ const found = db.prepare("SELECT r.oid AS oid, r.title AS title, r.sections AS sections FROM lineage l JOIN records r ON r.oid = l.oid WHERE l.head = ? AND r.id = ? ORDER BY l.pos LIMIT 1").get(head, id);
27521
+ if (!found) return void 0;
27522
+ const all = db.prepare("SELECT id, title, sections FROM records WHERE oid = ?").all(found.oid);
27523
+ const groups = new Set(all.map((r) => r.id.startsWith("H-") ? "H\0" + r.title + "\0" + r.sections : r.id));
27524
+ return { commit: found.oid, record: { id, title: found.title, sections: JSON.parse(found.sections) }, others: groups.size - 1 };
27525
+ });
27387
27526
  },
27388
27527
  /**
27389
- * Documents whose title, place or text holds the query — a title match before a place match before a text match —
27390
- * then past changes of `head`'s history, newest first. The caller syncs the working documents first.
27528
+ * The records of `head`'s history whose title, ID or sections hold the query, one hit per record file, a page at a
27529
+ * time: a title that starts with it first, then a title, the ID, and the sections; newest first within each. `after`
27530
+ * is the ID of the last hit of the page before; one that no longer matches answers undefined.
27391
27531
  */
27392
- async search(head, raw) {
27532
+ async searchRecords(head, raw, after, limit) {
27393
27533
  const query = raw.trim().toLowerCase();
27394
- if (!query) return [];
27395
- if (head) await ensure(head);
27534
+ await ensure(head);
27396
27535
  return database.with((db) => {
27397
- const literal2 = /[\\%_]/.test(query);
27398
- const match = (column) => literal2 ? `instr(${column}, ?) > 0` : `${column} LIKE ?`;
27399
- const needle = literal2 ? query : containing(query);
27400
- const features = new Map(db.prepare("SELECT feature, id FROM documents WHERE kind = 'feature'").all().map((r) => [r.feature, r.id]));
27401
- const found = db.prepare(`SELECT kind, payload, title, place FROM search WHERE scope = 'checkout' AND (${match("title")} OR ${match("place")} OR ${match("body")}) LIMIT 400`).all(needle, needle, needle);
27402
- const documents = found.map((r) => {
27403
- const at = r.title.indexOf(query);
27404
- const rank = at === 0 ? 0 : at > 0 ? 1 : r.place.includes(query) ? 2 : 3;
27405
- const p = JSON.parse(r.payload);
27406
- const featureId = p.feature ? features.get(p.feature) : void 0;
27407
- const hit = {
27408
- id: p.id,
27409
- kind: r.kind,
27410
- title: p.title,
27411
- where: p.where,
27412
- line: snippet(p.body, query),
27413
- ...featureId ? { featureId } : {}
27414
- };
27415
- return { rank, hit };
27416
- }).sort((a, b) => a.rank - b.rank || a.hit.title.localeCompare(b.hit.title)).slice(0, 24).map((r) => r.hit);
27417
- const past = !head ? [] : db.prepare(`SELECT s.ref AS ref, s.payload AS payload FROM search s JOIN lineage l ON l.head = ? AND l.oid = s.oid
27418
- WHERE s.scope = 'history' AND (${match("s.title")} OR ${match("s.body")}) ORDER BY l.pos LIMIT 8`).all(head, needle, needle).map((r) => {
27419
- const p = JSON.parse(r.payload);
27420
- return { id: r.ref, kind: "history", title: p.title, where: p.where, line: snippet(p.line, query), key: r.ref };
27536
+ const ordered = `WITH own AS (
27537
+ SELECT r.id AS id, r.title AS title, r.oid AS oid, min(l.pos) AS pos, r.sections AS sections,
27538
+ lower((SELECT group_concat(json_extract(s.value, '$.body'), ' ') FROM json_each(r.sections) s)) AS body
27539
+ FROM lineage l JOIN records r ON r.oid = l.oid WHERE l.head = :head GROUP BY r.id),
27540
+ hits AS (SELECT *, CASE WHEN substr(lower(title), 1, length(:q)) = :q THEN 0 WHEN instr(lower(title), :q) > 0 THEN 1
27541
+ WHEN instr(lower(id), :q) > 0 THEN 2 ELSE 3 END AS rank
27542
+ FROM own WHERE instr(lower(title), :q) > 0 OR instr(lower(id), :q) > 0 OR instr(body, :q) > 0),
27543
+ ordered AS (SELECT *, row_number() OVER (ORDER BY rank, pos, id) AS n FROM hits)`;
27544
+ const params = { head, q: query };
27545
+ const total = Number(db.prepare(`${ordered} SELECT count(*) AS n FROM ordered`).get(params).n);
27546
+ let start = 0;
27547
+ if (after !== void 0) {
27548
+ const at = db.prepare(`${ordered} SELECT n FROM ordered WHERE id = :after`).get({ ...params, after });
27549
+ if (!at) return void 0;
27550
+ start = Number(at.n);
27551
+ }
27552
+ const rows = db.prepare(`${ordered} SELECT id, title, oid, sections FROM ordered WHERE n > :start ORDER BY n LIMIT :limit`).all({ ...params, start, limit: limit + 1 });
27553
+ const hits = rows.slice(0, limit).map((r) => {
27554
+ const text4 = JSON.parse(r.sections).map((s) => s.body).join(" ");
27555
+ return { id: r.id, kind: "record", title: r.title, where: r.id + " \xB7 " + r.oid.slice(0, 7), line: snippet(text4, query), commit: r.oid };
27421
27556
  });
27422
- return [...documents, ...past];
27557
+ return { total, next: rows.length > limit ? hits[hits.length - 1].id : null, hits };
27558
+ });
27559
+ }
27560
+ };
27561
+ }
27562
+
27563
+ // src/adapters/cache/commit-log.ts
27564
+ import { lstat as lstat3 } from "node:fs/promises";
27565
+ import { join as join4 } from "node:path";
27566
+ var FORMAT2 = `%x1e%H%x00%aN%x00%aE%x00%at%x00%aI%x00%s%x00%(trailers:key=${MIGRATION_TRAILER},valueonly,separator=%x2C)`;
27567
+ var CHUNK = 1e5;
27568
+ var folderOf2 = (path3) => path3 === "AGENTS.md" ? path3 : new RegExp(`^(${SPEC_ROOT.replace(/\./g, "\\.")}/[^/]+|${INSTRUCTIONS_ROOT.replace(/\./g, "\\.")}/[^/]+)/`).exec(path3)?.[1];
27569
+ function createCommitLog(database, git, root) {
27570
+ let following;
27571
+ let followingHead;
27572
+ const read = async (args) => git.decode(await git.run(args));
27573
+ async function commits(range) {
27574
+ const out = [];
27575
+ for (let skip = 0; ; skip += CHUNK) {
27576
+ const text4 = await read(["log", `--format=${FORMAT2}`, `--max-count=${CHUNK}`, `--skip=${skip}`, ...range, "--"]);
27577
+ const chunk = text4.split("").filter(Boolean).map((entry2) => {
27578
+ const [oid3, name2, email3, time3, date5, subject, migration] = entry2.split("\0");
27579
+ return { oid: oid3, name: name2 ?? "", email: email3 ?? "", time: Number(time3), date: date5 ?? "", subject: subject ?? "", migration: !!migration?.trim() };
27580
+ });
27581
+ out.push(...chunk);
27582
+ if (chunk.length < CHUNK) return out;
27583
+ }
27584
+ }
27585
+ async function touches(range) {
27586
+ const text4 = await read(["log", "--format=%x1e%H", "-z", "--name-only", "--no-renames", ...range, "--", SPEC_ROOT, INSTRUCTIONS_ROOT, "AGENTS.md"]);
27587
+ const out = [];
27588
+ for (const entry2 of text4.split("").filter(Boolean)) {
27589
+ const [oid3, ...paths2] = entry2.split("\0");
27590
+ const folders = new Set(paths2.map((p) => folderOf2(p.replace(/^\n/, ""))).filter((f) => !!f));
27591
+ for (const folder of folders) out.push([oid3.trim(), folder]);
27592
+ }
27593
+ return out;
27594
+ }
27595
+ async function mailmap() {
27596
+ const info5 = await lstat3(join4(root, ".mailmap")).catch(() => void 0);
27597
+ return info5 ? info5.mtimeMs + ":" + info5.size : "-";
27598
+ }
27599
+ const stateOf = (db, key) => db.prepare("SELECT value FROM state WHERE key = ?").get(key)?.value;
27600
+ async function follow(head, attempts = 3) {
27601
+ const names = await mailmap();
27602
+ const [before, named] = await database.with((db) => [stateOf(db, "head"), stateOf(db, "mailmap")]);
27603
+ if (before === head && named === names) return;
27604
+ let change2;
27605
+ if (before && named === names) {
27606
+ try {
27607
+ const range = [head, "^" + before];
27608
+ const [added2, removed2, touched2] = await Promise.all([commits(range), read(["rev-list", before, "^" + head, "--"]), touches(range)]);
27609
+ change2 = { added: added2, removed: removed2.split("\n").filter(Boolean), touched: touched2, whole: false };
27610
+ } catch {
27611
+ change2 = void 0;
27612
+ }
27613
+ }
27614
+ if (!change2) {
27615
+ const [added2, touched2] = await Promise.all([commits([head]), touches([head])]);
27616
+ change2 = { added: added2, removed: [], touched: touched2, whole: true };
27617
+ }
27618
+ const { added, removed, touched, whole } = change2;
27619
+ const written = await database.with((db) => transaction(db, () => {
27620
+ if (stateOf(db, "head") !== before || stateOf(db, "mailmap") !== named) return false;
27621
+ if (whole) {
27622
+ db.exec("DELETE FROM log");
27623
+ db.exec("DELETE FROM touches");
27624
+ }
27625
+ const put = db.prepare(`INSERT INTO log (oid, name, email, time, date, subject, migration, reach) VALUES (?, ?, ?, ?, ?, ?, ?, 1)
27626
+ ON CONFLICT (oid) DO UPDATE SET name = excluded.name, email = excluded.email, reach = 1`);
27627
+ for (const c of added) put.run(c.oid, c.name, c.email, c.time, c.date, c.subject, c.migration ? 1 : 0);
27628
+ const leave = db.prepare("UPDATE log SET reach = 0 WHERE oid = ?");
27629
+ for (const oid3 of removed) leave.run(oid3);
27630
+ const touch = db.prepare("INSERT OR IGNORE INTO touches (oid, folder) VALUES (?, ?)");
27631
+ for (const [oid3, folder] of touched) touch.run(oid3, folder);
27632
+ const set2 = db.prepare("INSERT INTO state (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value");
27633
+ set2.run("head", head);
27634
+ set2.run("mailmap", names);
27635
+ return true;
27636
+ }));
27637
+ if (!written && attempts > 1) return follow(head, attempts - 1);
27638
+ }
27639
+ async function ensure(head) {
27640
+ while (following && followingHead !== head) await following.catch(() => void 0);
27641
+ if (!following) {
27642
+ followingHead = head;
27643
+ following = follow(head).finally(() => {
27644
+ following = void 0;
27645
+ followingHead = void 0;
27646
+ });
27647
+ }
27648
+ return following;
27649
+ }
27650
+ return {
27651
+ ensure,
27652
+ /** Everyone who committed to `head`, most commits first, format migrations left out. `latest` is their newest commit. */
27653
+ async contributors(head) {
27654
+ await ensure(head);
27655
+ return database.with((db) => db.prepare(`SELECT email, name, count(*) AS commits, max(time) AS time, date AS latest FROM log
27656
+ WHERE reach = 1 AND migration = 0 GROUP BY email ORDER BY commits DESC, name, email`).all().map(({ name: name2, email: email3, commits: commits2, latest }) => ({ name: name2, email: email3, commits: Number(commits2), latest })));
27657
+ },
27658
+ /**
27659
+ * The commits of `head` a hash names: the whole hash, or its start from seven characters on, as `git log --oneline`
27660
+ * prints it. Anything shorter or not a hash names none. Newest first, a page after the commit `after`.
27661
+ */
27662
+ async commitsNamed(head, raw, after, limit) {
27663
+ const prefix = raw.trim().toLowerCase();
27664
+ if (!/^[0-9a-f]{7,64}$/.test(prefix)) return { total: 0, next: null, hits: [] };
27665
+ await ensure(head);
27666
+ return database.with((db) => {
27667
+ const rows = db.prepare("SELECT oid, name, date, subject FROM log WHERE reach = 1 AND substr(oid, 1, length(?)) = ? ORDER BY time DESC, oid").all(prefix, prefix);
27668
+ const start = after === void 0 ? 0 : rows.findIndex((r) => r.oid === after) + 1;
27669
+ if (after !== void 0 && !start) return void 0;
27670
+ const shown = rows.slice(start, start + limit);
27671
+ return {
27672
+ total: rows.length,
27673
+ next: start + shown.length < rows.length ? shown[shown.length - 1].oid : null,
27674
+ hits: shown.map((r) => ({ id: r.oid, kind: "commit", title: r.subject, where: r.oid.slice(0, 7) + " \xB7 " + r.name, line: r.date.slice(0, 10), commit: r.oid }))
27675
+ };
27676
+ });
27677
+ },
27678
+ /** Each store folder's authors, most commits first, and when it was last touched, format migrations left out. */
27679
+ async folders(head) {
27680
+ await ensure(head);
27681
+ return database.with((db) => {
27682
+ const rows = db.prepare(`SELECT t.folder AS folder, l.email AS email, l.name AS name, count(*) AS commits, max(l.time) AS time, l.date AS latest
27683
+ FROM touches t JOIN log l ON l.oid = t.oid WHERE l.reach = 1 AND l.migration = 0
27684
+ GROUP BY t.folder, l.email ORDER BY t.folder, commits DESC, name, email`).all();
27685
+ const out = /* @__PURE__ */ new Map();
27686
+ for (const r of rows) {
27687
+ let entry2 = out.get(r.folder);
27688
+ if (!entry2) out.set(r.folder, entry2 = { people: [], latest: r.latest, time: r.time });
27689
+ entry2.people.push({ name: r.name, email: r.email, commits: Number(r.commits), latest: r.latest });
27690
+ if (r.time > entry2.time) {
27691
+ entry2.time = r.time;
27692
+ entry2.latest = r.latest;
27693
+ }
27694
+ }
27695
+ return new Map([...out].map(([folder, { people, latest }]) => [folder, { people, latest }]));
27423
27696
  });
27424
27697
  }
27425
27698
  };
27426
27699
  }
27427
27700
 
27428
27701
  // src/adapters/cache/index.ts
27429
- function openCache(root, git) {
27702
+ function openCache(root, git, originals) {
27430
27703
  const database = createCacheDatabase(root);
27431
27704
  const documents = createDocumentCache(root, database);
27432
- const history = createHistory(database, git);
27705
+ const history = createHistory(database, git, originals);
27706
+ const log = createCommitLog(database, git, root);
27433
27707
  return {
27434
27708
  documents,
27435
27709
  history,
27436
- location: database.location,
27437
- /** Documents first, so a search right after an edit finds the edited text. */
27438
- async search(head, query) {
27439
- await documents.sync();
27440
- return history.search(head, query);
27441
- }
27710
+ log,
27711
+ location: database.location
27442
27712
  };
27443
27713
  }
27444
27714
 
27445
27715
  // src/adapters/git/pending-records.ts
27446
- import { lstat as lstat3, readdir as readdir3, readFile as readFile3 } from "node:fs/promises";
27447
- import { join as join4 } from "node:path";
27716
+ import { lstat as lstat4, readdir as readdir3, readFile as readFile3 } from "node:fs/promises";
27717
+ import { join as join5 } from "node:path";
27448
27718
  var decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
27449
- var info = (path3) => lstat3(path3).catch((error62) => {
27719
+ var info = (path3) => lstat4(path3).catch((error62) => {
27450
27720
  if (error62.code === "ENOENT") return void 0;
27451
27721
  throw error62;
27452
27722
  });
27453
27723
  async function recordFolders(root) {
27454
- return (await readdir3(join4(root, ...RECORDS_ROOT.split("/"))).catch((error62) => {
27724
+ return (await readdir3(join5(root, ...RECORDS_ROOT.split("/"))).catch((error62) => {
27455
27725
  if (error62.code === "ENOENT") return [];
27456
27726
  throw error62;
27457
27727
  })).filter(isRecordFolder);
@@ -27459,7 +27729,7 @@ async function recordFolders(root) {
27459
27729
  async function findRecord(root, id) {
27460
27730
  for (const folder of await recordFolders(root)) {
27461
27731
  const path3 = `${RECORDS_ROOT}/${folder}/${id}.md`;
27462
- if ((await info(join4(root, ...path3.split("/"))))?.isFile()) return path3;
27732
+ if ((await info(join5(root, ...path3.split("/"))))?.isFile()) return path3;
27463
27733
  }
27464
27734
  return void 0;
27465
27735
  }
@@ -27475,7 +27745,7 @@ async function readPendingRecords(root, run) {
27475
27745
  altered.push(path3);
27476
27746
  continue;
27477
27747
  }
27478
- const file2 = join4(root, ...path3.split("/"));
27748
+ const file2 = join5(root, ...path3.split("/"));
27479
27749
  const stat3 = await info(file2);
27480
27750
  if (!stat3?.isFile() || stat3.isSymbolicLink()) {
27481
27751
  problems.push(docProblem("PATH_UNSUPPORTED", path3));
@@ -27497,30 +27767,30 @@ async function readPendingRecords(root, run) {
27497
27767
  if (!id) continue;
27498
27768
  for (const folder of folders) {
27499
27769
  const other = `${RECORDS_ROOT}/${folder}/${id}.md`;
27500
- if (other !== path3 && !files.has(other) && (await info(join4(root, ...other.split("/"))))?.isFile()) problems.push(docProblem("DUPLICATE_RECORD_ID", path3, { id, other }));
27770
+ if (other !== path3 && !files.has(other) && (await info(join5(root, ...other.split("/"))))?.isFile()) problems.push(docProblem("DUPLICATE_RECORD_ID", path3, { id, other }));
27501
27771
  }
27502
27772
  }
27503
27773
  return { files, altered: altered.sort(), problems };
27504
27774
  }
27505
27775
 
27506
27776
  // src/adapters/filesystem/config-file.ts
27507
- import { lstat as lstat4, readFile as readFile4, realpath, mkdir as mkdir2, open as open2, link, rename, unlink } from "node:fs/promises";
27508
- import { join as join5 } from "node:path";
27777
+ import { lstat as lstat5, readFile as readFile4, realpath, mkdir as mkdir2, open as open2, link, rename, unlink } from "node:fs/promises";
27778
+ import { join as join6 } from "node:path";
27509
27779
  import { randomUUID as randomUUID2 } from "node:crypto";
27510
27780
  var missing = (error62) => error62.code === "ENOENT";
27511
- var fileInfo = (path3) => lstat4(path3).catch((error62) => {
27781
+ var fileInfo = (path3) => lstat5(path3).catch((error62) => {
27512
27782
  if (missing(error62)) return void 0;
27513
27783
  throw error62;
27514
27784
  });
27515
27785
  async function configDirectory(root) {
27516
- const path3 = join5(root, ".gitifact");
27786
+ const path3 = join6(root, ".gitifact");
27517
27787
  const info5 = await fileInfo(path3);
27518
27788
  if (info5 && (!info5.isDirectory() || info5.isSymbolicLink())) throw new InitError("PATH_CONFLICT", t2("config.storeNotDirectory"));
27519
27789
  return info5;
27520
27790
  }
27521
27791
  async function readConfigFile(root) {
27522
27792
  await configDirectory(root);
27523
- const path3 = join5(root, ".gitifact", "config.json");
27793
+ const path3 = join6(root, ".gitifact", "config.json");
27524
27794
  const info5 = await fileInfo(path3);
27525
27795
  if (!info5) return void 0;
27526
27796
  if (!info5.isFile() || info5.isSymbolicLink() || info5.size > 65536) throw new InitError("PATH_CONFLICT", t2("config.fileShape"));
@@ -27537,7 +27807,7 @@ async function readConfigFile(root) {
27537
27807
  }
27538
27808
  }
27539
27809
  async function publishConfig(root, text4, recheck, beforePublish, replace = false) {
27540
- const directory = join5(root, ".gitifact");
27810
+ const directory = join6(root, ".gitifact");
27541
27811
  await configDirectory(root);
27542
27812
  await mkdir2(directory).catch((error62) => {
27543
27813
  if (error62.code !== "EEXIST") throw error62;
@@ -27550,7 +27820,7 @@ async function publishConfig(root, text4, recheck, beforePublish, replace = fals
27550
27820
  throw new InitError("INPUT_CHANGED", t2("config.directoryChanged"));
27551
27821
  }
27552
27822
  };
27553
- const temporary = join5(directory, ".init-" + randomUUID2() + ".tmp");
27823
+ const temporary = join6(directory, ".init-" + randomUUID2() + ".tmp");
27554
27824
  const handle = await open2(temporary, "wx", 384);
27555
27825
  const tempOwner = await handle.stat();
27556
27826
  try {
@@ -27567,8 +27837,8 @@ async function publishConfig(root, text4, recheck, beforePublish, replace = fals
27567
27837
  const bytes = await readFile4(temporary, "utf8");
27568
27838
  if (bytes !== text4) throw new InitError("INPUT_CHANGED", t2("config.temporaryChanged"));
27569
27839
  parseManagedConfig(bytes);
27570
- if (replace) await rename(temporary, join5(directory, "config.json"));
27571
- else await link(temporary, join5(directory, "config.json"));
27840
+ if (replace) await rename(temporary, join6(directory, "config.json"));
27841
+ else await link(temporary, join6(directory, "config.json"));
27572
27842
  } finally {
27573
27843
  await unchanged();
27574
27844
  const current2 = await fileInfo(temporary);
@@ -27577,11 +27847,11 @@ async function publishConfig(root, text4, recheck, beforePublish, replace = fals
27577
27847
  }
27578
27848
 
27579
27849
  // src/adapters/filesystem/instruction-folder.ts
27580
- import { lstat as lstat5, open as open3, readdir as readdir4, readFile as readFile5 } from "node:fs/promises";
27581
- import { join as join6 } from "node:path";
27850
+ import { lstat as lstat6, open as open3, readdir as readdir4, readFile as readFile5 } from "node:fs/promises";
27851
+ import { join as join7 } from "node:path";
27582
27852
  var INSTRUCTION_FILE_LIMIT = 500;
27583
27853
  var INSTRUCTION_TEXT_LIMIT = 512 * 1024;
27584
- var folderOf2 = (root, instructionPath) => join6(root, ...instructionPath.split("/").slice(0, -1));
27854
+ var folderOf3 = (root, instructionPath) => join7(root, ...instructionPath.split("/").slice(0, -1));
27585
27855
  async function listInstructionFiles(root, instructionPath) {
27586
27856
  const files = [];
27587
27857
  let limited = false;
@@ -27594,21 +27864,21 @@ async function listInstructionFiles(root, instructionPath) {
27594
27864
  return;
27595
27865
  }
27596
27866
  const path3 = prefix + entry2.name;
27597
- if (entry2.isDirectory()) await visit2(join6(folder, entry2.name), path3 + "/", depth + 1);
27598
- else if (entry2.isFile() && path3 !== INSTRUCTION_FILE) files.push({ path: path3, size: (await lstat5(join6(folder, entry2.name))).size });
27867
+ if (entry2.isDirectory()) await visit2(join7(folder, entry2.name), path3 + "/", depth + 1);
27868
+ else if (entry2.isFile() && path3 !== INSTRUCTION_FILE) files.push({ path: path3, size: (await lstat6(join7(folder, entry2.name))).size });
27599
27869
  }
27600
27870
  }
27601
- await visit2(folderOf2(root, instructionPath), "", 0);
27871
+ await visit2(folderOf3(root, instructionPath), "", 0);
27602
27872
  return { files, limited };
27603
27873
  }
27604
27874
  var FRONTMATTER_WINDOW = 8 * 1024;
27605
27875
  async function describeFiles(root, instructionPath, files) {
27606
- const folder = folderOf2(root, instructionPath);
27876
+ const folder = folderOf3(root, instructionPath);
27607
27877
  return Promise.all(files.map(async (file2) => {
27608
27878
  if (!isInstructionReference(file2.path)) return file2;
27609
27879
  let handle;
27610
27880
  try {
27611
- handle = await open3(join6(folder, ...file2.path.split("/")), "r");
27881
+ handle = await open3(join7(folder, ...file2.path.split("/")), "r");
27612
27882
  const buffer = Buffer.alloc(Math.min(FRONTMATTER_WINDOW, file2.size));
27613
27883
  const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
27614
27884
  return { ...file2, ...parseInstructionFile(file2.path, buffer.subarray(0, bytesRead).toString("utf8")) };
@@ -27626,13 +27896,13 @@ async function readInstructionFiles(root, instructionPath) {
27626
27896
  async function readInstructionFile(root, instructionPath, path3) {
27627
27897
  const parts = path3.split("/");
27628
27898
  if (parts.some((p) => !p || p === "." || p === ".." || /[\\:\0]/.test(p))) return void 0;
27629
- let full = folderOf2(root, instructionPath);
27899
+ let full = folderOf3(root, instructionPath);
27630
27900
  for (const [index, part] of parts.entries()) {
27631
- full = join6(full, part);
27632
- const info5 = await lstat5(full).catch(() => void 0);
27901
+ full = join7(full, part);
27902
+ const info5 = await lstat6(full).catch(() => void 0);
27633
27903
  if (!info5 || info5.isSymbolicLink() || (index < parts.length - 1 ? !info5.isDirectory() : !info5.isFile())) return void 0;
27634
27904
  }
27635
- const size = (await lstat5(full)).size;
27905
+ const size = (await lstat6(full)).size;
27636
27906
  if (size > INSTRUCTION_TEXT_LIMIT) return { text: null, size, binary: false, tooLarge: true };
27637
27907
  const bytes = await readFile5(full);
27638
27908
  try {
@@ -27644,12 +27914,12 @@ async function readInstructionFile(root, instructionPath, path3) {
27644
27914
  }
27645
27915
 
27646
27916
  // src/adapters/filesystem/agents-file.ts
27647
- import { lstat as lstat6, readFile as readFile6 } from "node:fs/promises";
27648
- import { join as join7 } from "node:path";
27917
+ import { lstat as lstat7, readFile as readFile6 } from "node:fs/promises";
27918
+ import { join as join8 } from "node:path";
27649
27919
  var LIMIT = 1024 * 1024;
27650
27920
  async function readAgentsFile(root) {
27651
- const file2 = join7(root, "AGENTS.md");
27652
- const info5 = await lstat6(file2).catch(() => void 0);
27921
+ const file2 = join8(root, "AGENTS.md");
27922
+ const info5 = await lstat7(file2).catch(() => void 0);
27653
27923
  if (!info5?.isFile() || info5.size > LIMIT) return null;
27654
27924
  try {
27655
27925
  return new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(await readFile6(file2));
@@ -27726,8 +27996,8 @@ async function documentsAtHead(project, head, paths2) {
27726
27996
 
27727
27997
  // src/server/checkout/stamp.ts
27728
27998
  import { createHash as createHash2 } from "node:crypto";
27729
- import { lstat as lstat7 } from "node:fs/promises";
27730
- import { join as join8 } from "node:path";
27999
+ import { lstat as lstat8 } from "node:fs/promises";
28000
+ import { join as join9 } from "node:path";
27731
28001
  function createStampReader(root, inherited = process.env) {
27732
28002
  const runner = createGitRunner();
27733
28003
  const env = { ...inherited, GIT_OPTIONAL_LOCKS: "0", GIT_NO_REPLACE_OBJECTS: "1", GIT_TERMINAL_PROMPT: "0", GIT_LITERAL_PATHSPECS: "0", LC_ALL: "C" };
@@ -27739,7 +28009,7 @@ function createStampReader(root, inherited = process.env) {
27739
28009
  ]);
27740
28010
  const paths2 = status.split("\0").filter(Boolean).map((entry2) => entry2.slice(3)).filter((path3) => !path3.startsWith(".gitifact/cache/"));
27741
28011
  const seen = await Promise.all(paths2.map(async (path3) => {
27742
- const info5 = await lstat7(join8(root, ...path3.split("/"))).catch(() => void 0);
28012
+ const info5 = await lstat8(join9(root, ...path3.split("/"))).catch(() => void 0);
27743
28013
  return path3 + " " + (info5 ? info5.mtimeMs + ":" + info5.size : "-");
27744
28014
  }));
27745
28015
  return createHash2("sha256").update(head.trim() + "\n" + seen.sort().join("\n")).digest("hex").slice(0, 20);
@@ -27747,19 +28017,14 @@ function createStampReader(root, inherited = process.env) {
27747
28017
  }
27748
28018
 
27749
28019
  // src/server/checkout/checkout-reader.ts
27750
- var tally = (people, name2, email3, latest) => {
27751
- const person = people.get(email3);
27752
- if (person) person.commits++;
27753
- else people.set(email3, { name: name2, email: email3, latest, commits: 1 });
27754
- };
27755
28020
  async function settled(reads) {
27756
28021
  const results = await Promise.allSettled(reads);
27757
28022
  const failure2 = results.find((r) => r.status === "rejected");
27758
28023
  if (failure2) throw failure2.reason;
27759
28024
  return results.map((r) => r.value);
27760
28025
  }
27761
- var folderOf3 = (path3) => path3.split("/").slice(0, 3).join("/");
27762
- function checkoutDocuments(states) {
28026
+ var folderOf4 = (path3) => path3.split("/").slice(0, 3).join("/");
28027
+ function checkoutDocuments(states, authors) {
27763
28028
  const documents = states.documents;
27764
28029
  const standing2 = (id) => {
27765
28030
  const previousPath = states.previousPath(id);
@@ -27772,30 +28037,45 @@ function checkoutDocuments(states) {
27772
28037
  return { id: s.id, ...target ? { title: target.title, path: target.path } : {}, ...s.note ? { note: s.note } : {} };
27773
28038
  };
27774
28039
  const arranged = arrangeDocuments(documents);
27775
- const features = arranged.features.map(({ index, requirements, designs }) => ({
27776
- id: index.id,
27777
- path: index.path,
27778
- title: index.title,
27779
- description: index.description,
27780
- body: index.body,
27781
- ...standing2(index.id),
27782
- requirements: requirements.map((r) => ({ id: r.id, path: r.path, title: r.title, description: r.description, order: r.order, body: r.body, ...standing2(r.id) })),
27783
- designs: designs.map((d) => ({
27784
- id: d.id,
27785
- path: d.path,
27786
- title: d.title,
27787
- description: d.description,
27788
- order: d.order,
27789
- body: d.body,
27790
- requirements: d.requirements,
27791
- sources: d.sources.map(source2),
27792
- ...standing2(d.id)
27793
- }))
28040
+ const features = arranged.features.map(({ index, requirements, designs }) => {
28041
+ const entry2 = authors.get(folderOf4(index.path));
28042
+ return {
28043
+ id: index.id,
28044
+ path: index.path,
28045
+ title: index.title,
28046
+ description: index.description,
28047
+ body: index.body,
28048
+ ...standing2(index.id),
28049
+ requirements: requirements.map((r) => ({ id: r.id, path: r.path, title: r.title, description: r.description, order: r.order, body: r.body, ...standing2(r.id) })),
28050
+ designs: designs.map((d) => ({
28051
+ id: d.id,
28052
+ path: d.path,
28053
+ title: d.title,
28054
+ description: d.description,
28055
+ order: d.order,
28056
+ body: d.body,
28057
+ requirements: d.requirements,
28058
+ sources: d.sources.map(source2),
28059
+ ...standing2(d.id)
28060
+ })),
28061
+ // Who committed to the feature folder, most first, and when it was last touched; nobody until first committed.
28062
+ contributors: entry2?.people ?? [],
28063
+ updatedAt: entry2?.latest ?? null
28064
+ };
28065
+ });
28066
+ const instructions2 = arranged.instructions.map((k) => ({
28067
+ id: k.id,
28068
+ name: k.name,
28069
+ path: k.path,
28070
+ title: k.title,
28071
+ description: k.description,
28072
+ body: k.body,
28073
+ ...standing2(k.id),
28074
+ updatedAt: authors.get(folderOf4(k.path))?.latest ?? null
27794
28075
  }));
27795
- const instructions2 = arranged.instructions.map((k) => ({ id: k.id, name: k.name, path: k.path, title: k.title, description: k.description, body: k.body, ...standing2(k.id) }));
27796
28076
  return { features, instructions: instructions2, orphans: arranged.orphans };
27797
28077
  }
27798
- function createCheckoutReader(root, sessionId, cache, inherited = process.env) {
28078
+ function createCheckoutReader(root, cache, inherited = process.env) {
27799
28079
  const reader = storeReader(root);
27800
28080
  const runner = createGitRunner();
27801
28081
  const env = { ...inherited, GIT_OPTIONAL_LOCKS: "0", GIT_NO_REPLACE_OBJECTS: "1", GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" };
@@ -27808,87 +28088,36 @@ function createCheckoutReader(root, sessionId, cache, inherited = process.env) {
27808
28088
  };
27809
28089
  const pending = /* @__PURE__ */ new Map();
27810
28090
  const readStamp = createStampReader(root, inherited);
27811
- const notMigration = ["-E", "--invert-grep", `--grep=^${MIGRATION_TRAILER}: `];
27812
- async function storeAuthors(head) {
27813
- const text4 = await git(["log", ...notMigration, "--format=%x1e%aN%x00%aE%x00%aI", "-z", "--name-only", "--no-renames", "--max-count=20000", head, "--", ".gitifact/spec", INSTRUCTIONS_ROOT, "AGENTS.md"]);
27814
- const folders = /* @__PURE__ */ new Map();
27815
- const instructions2 = /* @__PURE__ */ new Map();
27816
- for (const chunk of text4.split("")) {
27817
- if (!chunk) continue;
27818
- const [name2, email3, date5, ...paths2] = chunk.split("\0");
27819
- if (!name2 || !email3 || !date5) throw new StoreError(t2("specReader.authorUnreadable"));
27820
- const seen = /* @__PURE__ */ new Set();
27821
- for (const raw of paths2) {
27822
- const path3 = raw.replace(/^\n/, "");
27823
- if (!path3) continue;
27824
- const folder = /^(\.gitifact\/spec\/[^/]+)\//.exec(path3)?.[1];
27825
- if (folder && !seen.has(folder)) {
27826
- seen.add(folder);
27827
- let entry2 = folders.get(folder);
27828
- if (!entry2) folders.set(folder, entry2 = { people: /* @__PURE__ */ new Map(), count: 0, latest: date5 });
27829
- if (entry2.count < 2e3) {
27830
- entry2.count++;
27831
- tally(entry2.people, name2, email3, date5);
27832
- }
27833
- }
27834
- const instruction2 = path3 === "AGENTS.md" ? path3 : /^(\.gitifact\/instructions\/[^/]+)\//.exec(path3)?.[1];
27835
- if (instruction2 && !instructions2.has(instruction2)) instructions2.set(instruction2, date5);
27836
- }
27837
- }
27838
- return { folders, instructions: instructions2 };
27839
- }
27840
28091
  async function read() {
27841
28092
  const raw = await readConfigFile(root);
27842
28093
  if (!raw) throw new StoreError(t2("specReader.schemaRequired"));
27843
28094
  parseManagedConfig(raw);
27844
28095
  const head = await readHead();
27845
28096
  const stamp = await readStamp();
27846
- const [current2, dirty, authors, everyone] = await settled([
28097
+ const [current2, dirty, authors, people] = await settled([
27847
28098
  cache.documents.list(),
27848
28099
  head ? git(["status", "--porcelain=v1", "--", ".gitifact/spec", INSTRUCTIONS_ROOT]) : Promise.resolve(""),
27849
- head ? storeAuthors(head) : Promise.resolve(void 0),
27850
- // Git mailmap may change without a new HEAD; refresh names with every observation that carries them.
27851
- head ? git(["log", ...notMigration, "--format=%aN%x00%aE%x00%aI", "--max-count=10001", head]) : Promise.resolve("")
28100
+ // Who touched each folder and who committed at all, counted over the commit log in the cache.
28101
+ head ? cache.log.folders(head) : Promise.resolve(/* @__PURE__ */ new Map()),
28102
+ head ? cache.log.contributors(head) : Promise.resolve([])
27852
28103
  ]);
27853
- const people = /* @__PURE__ */ new Map();
27854
- const lines = everyone.trim().split("\n").filter(Boolean);
27855
- for (const line2 of lines.slice(0, 1e4)) {
27856
- const [name2, email3, latest] = line2.split("\0");
27857
- if (!name2 || !email3 || !latest) throw new StoreError(t2("specReader.authorUnreadable"));
27858
- tally(people, name2, email3, latest);
27859
- }
27860
28104
  const states = await documentStates({ head: async () => head, reader }, current2.documents);
27861
- const arranged = checkoutDocuments(states);
27862
- const features = arranged.features.map((feature2) => {
27863
- const entry2 = authors?.folders.get(folderOf3(feature2.path));
27864
- return { ...feature2, contributors: entry2 ? [...entry2.people.values()].sort((a, b) => b.commits - a.commits) : [], updatedAt: entry2?.latest ?? null };
27865
- });
27866
- const instructions2 = await Promise.all(arranged.instructions.map(async (instruction2) => {
27867
- const { files, limited } = instruction2.state === "deleted" ? { files: [], limited: false } : await readInstructionFiles(root, instruction2.path);
27868
- return { ...instruction2, files, filesLimited: limited, updatedAt: authors?.instructions.get(instruction2.path.split("/").slice(0, 3).join("/")) ?? null };
27869
- }));
27870
- const agentsText = await readAgentsFile(root);
27871
- const agents2 = agentsText === null ? null : { path: "AGENTS.md", body: agentsText, updatedAt: authors?.instructions.get("AGENTS.md") ?? null };
27872
- const problems = [...current2.problems, ...arranged.orphans.map((feature2) => docProblem("FEATURE_INDEX_REQUIRED", `.gitifact/spec/${feature2}/index.md`, { feature: feature2 }))];
28105
+ const { features, instructions: instructions2, orphans } = checkoutDocuments(states, authors);
28106
+ const problems = [...current2.problems, ...orphans.map((feature2) => docProblem("FEATURE_INDEX_REQUIRED", `.gitifact/spec/${feature2}/index.md`, { feature: feature2 }))];
27873
28107
  if (await readHead() !== head) throw new StoreError(t2("specReader.projectChanged"));
27874
- const checkout = browserSpecsV7.parse({
27875
- contract: "browser-specs",
27876
- version: 7,
27877
- sessionId,
28108
+ return {
27878
28109
  head,
27879
- observedAt: (/* @__PURE__ */ new Date()).toISOString(),
27880
28110
  stamp,
28111
+ observedAt: (/* @__PURE__ */ new Date()).toISOString(),
27881
28112
  working: head ? !!dirty.trim() : features.length > 0 || instructions2.length > 0,
27882
28113
  features,
27883
28114
  instructions: instructions2,
27884
- agents: agents2,
27885
28115
  problems,
27886
- contributors: [...people.values()].sort((a, b) => b.commits - a.commits),
27887
- contributorsLimited: lines.length > 1e4
27888
- });
27889
- return { checkout, head };
28116
+ people,
28117
+ folders: authors
28118
+ };
27890
28119
  }
27891
- return () => {
28120
+ const base = () => {
27892
28121
  const language = getLanguage();
27893
28122
  let value = pending.get(language);
27894
28123
  if (!value) {
@@ -27897,10 +28126,37 @@ function createCheckoutReader(root, sessionId, cache, inherited = process.env) {
27897
28126
  }
27898
28127
  return value;
27899
28128
  };
28129
+ return {
28130
+ base,
28131
+ /** Every instruction by name with the other files of its folder, and AGENTS.md: few enough to read whole. */
28132
+ async instructions() {
28133
+ const checkout = await base();
28134
+ const instructions2 = await Promise.all([...checkout.instructions].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map(async (instruction2) => {
28135
+ const { files, limited } = instruction2.state === "deleted" ? { files: [], limited: false } : await readInstructionFiles(root, instruction2.path);
28136
+ return { ...instruction2, files, filesLimited: limited };
28137
+ }));
28138
+ const agentsText = await readAgentsFile(root);
28139
+ return { instructions: instructions2, agents: agentsText === null ? null : { path: "AGENTS.md", body: agentsText, updatedAt: checkout.folders.get("AGENTS.md")?.latest ?? null } };
28140
+ }
28141
+ };
28142
+ }
28143
+
28144
+ // src/queries/paging.ts
28145
+ var PAGE_SIZE = 20;
28146
+ function pageOf(rows, keyOf, request) {
28147
+ let start = 0;
28148
+ if (request.after !== void 0) {
28149
+ const at = rows.findIndex((row) => keyOf(row) === request.after);
28150
+ if (at < 0) return void 0;
28151
+ start = at + 1;
28152
+ }
28153
+ const size = request.all ? rows.length : request.limit ?? PAGE_SIZE;
28154
+ const shown = rows.slice(start, start + size);
28155
+ const more = start + shown.length < rows.length && shown.length > 0;
28156
+ return { rows: shown, total: rows.length, next: more ? keyOf(shown[shown.length - 1]) : null };
27900
28157
  }
27901
28158
 
27902
28159
  // src/server/commit/commit-files.ts
27903
- var LIST_LIMIT = 500;
27904
28160
  var TEXT_LIMIT = 512 * 1024;
27905
28161
  var KEPT = 200;
27906
28162
  var statusOf = (code) => code.startsWith("R") ? "renamed" : code === "A" ? "added" : code === "D" ? "deleted" : "modified";
@@ -27956,10 +28212,15 @@ function createCommitFiles(git) {
27956
28212
  return git.run(["cat-file", "blob", `${rev}:${path3}`]);
27957
28213
  }
27958
28214
  return {
27959
- /** The first `LIST_LIMIT` source files and how many there are; undefined for a commit the repository lacks. */
27960
- async files(commit) {
28215
+ /**
28216
+ * A page of the source files after the file `after` (a path) and how many there are in all: undefined for a commit
28217
+ * the repository lacks, null for a cursor the commit does not have.
28218
+ */
28219
+ async files(commit, request) {
27961
28220
  const all = await list(commit);
27962
- return all && { ...all, files: all.files.slice(0, LIST_LIMIT) };
28221
+ if (!all) return void 0;
28222
+ const page = pageOf(all.files, (f) => f.path, request);
28223
+ return page ? { ...all, files: page.rows, next: page.next } : null;
27963
28224
  },
27964
28225
  /** One listed file on both sides; undefined when the commit did not change that path. */
27965
28226
  async file(commit, path3) {
@@ -28041,31 +28302,200 @@ async function workingChange2(source2, id) {
28041
28302
  };
28042
28303
  }
28043
28304
 
28305
+ // src/queries/search.ts
28306
+ var SEARCH_GROUPS = ["feature", "requirement", "design", "instruction", "record", "commit"];
28307
+ var FIRST = 5;
28308
+ var RECENT = 6;
28309
+ var place2 = (path3) => path3.replace(/^\.gitifact\//, "");
28310
+ var folderOf5 = (path3) => path3.split("/").slice(0, 3).join("/");
28311
+ async function searchRecords(source2, request) {
28312
+ const head = request.head ?? await source2.head();
28313
+ const query = request.q.trim().toLowerCase();
28314
+ if (!query) return [await recent(source2, head)];
28315
+ const size = request.group ? request.limit ?? 20 : FIRST;
28316
+ const one = async (group) => {
28317
+ const found = group === "record" ? head ? await source2.cache.history.searchRecords(head, query, request.after, size) : empty() : group === "commit" ? head ? await source2.cache.log.commitsNamed(head, query, request.after, size) : empty() : await source2.cache.documents.searchKind(group, query, request.after, size);
28318
+ return found && { group, ...found };
28319
+ };
28320
+ const groups = await Promise.all((request.group ? [request.group] : SEARCH_GROUPS).map(one));
28321
+ if (groups.some((g) => !g)) return void 0;
28322
+ return groups.filter((g) => request.group || g.total > 0);
28323
+ }
28324
+ var empty = () => ({ total: 0, next: null, hits: [] });
28325
+ async function recent(source2, head) {
28326
+ const [{ documents }, folders] = await Promise.all([source2.cache.documents.list(), head ? source2.cache.log.folders(head) : /* @__PURE__ */ new Map()]);
28327
+ const hits = documents.filter((d) => d.kind === "feature" || d.kind === "instruction").map((d) => ({ doc: d, latest: folders.get(folderOf5(d.path))?.latest })).filter((d) => !!d.latest).sort((a, b) => Date.parse(b.latest) - Date.parse(a.latest) || a.doc.id.localeCompare(b.doc.id)).slice(0, RECENT).map(({ doc }) => ({
28328
+ id: doc.id,
28329
+ kind: doc.kind,
28330
+ title: doc.title,
28331
+ where: place2(doc.path),
28332
+ line: doc.description,
28333
+ ...doc.kind === "feature" ? { featureId: doc.id } : {}
28334
+ }));
28335
+ return { group: "recent", total: hits.length, next: null, hits };
28336
+ }
28337
+
28338
+ // src/queries/checkout.ts
28339
+ var SHOWN_REQUIREMENTS = 12;
28340
+ function checkoutIndex(base) {
28341
+ return {
28342
+ features: base.features.map((f) => ({
28343
+ id: f.id,
28344
+ path: f.path,
28345
+ title: f.title,
28346
+ state: f.state,
28347
+ requirements: f.requirements.map((r) => ({ id: r.id, path: r.path, title: r.title, description: r.description })),
28348
+ designs: f.designs.map((d) => ({ id: d.id, path: d.path, title: d.title }))
28349
+ })),
28350
+ instructions: base.instructions.map((k) => ({ id: k.id, name: k.name, path: k.path, title: k.title })),
28351
+ people: base.people.map((p) => ({ name: p.name, email: p.email }))
28352
+ };
28353
+ }
28354
+ var acceptanceHeading = /^###\s+(?:수용 조건|acceptance criteria)\s*$/i;
28355
+ function acceptanceCount(body2) {
28356
+ const lines = body2.replace(/\r\n/g, "\n").split("\n");
28357
+ const start = lines.findIndex((line2) => acceptanceHeading.test(line2.trim()));
28358
+ if (start < 0) return null;
28359
+ let count2 = 0;
28360
+ for (const line2 of lines.slice(start + 1)) {
28361
+ if (/^#{1,6}\s/.test(line2)) break;
28362
+ if (/^\d+\.\s/.test(line2)) count2++;
28363
+ }
28364
+ return count2 || null;
28365
+ }
28366
+ function listFeatures(base, filter, request) {
28367
+ const query = filter.q?.trim().toLowerCase();
28368
+ const named = (f) => !query || (f.title + " " + f.id).toLowerCase().includes(query);
28369
+ const key = filter.sort ?? "updated";
28370
+ const opens = { title: "asc", requirements: "desc", updated: "desc" };
28371
+ const way = (filter.dir ?? opens[key]) === "asc" ? 1 : -1;
28372
+ const groups = base.features.filter((f) => !filter.design || filter.design === "yes" === f.designs.length > 0).filter((f) => !filter.author || f.contributors.some((p) => p.email === filter.author)).map((f) => ({ feature: f, requirements: named(f) ? f.requirements : f.requirements.filter((r) => (r.title + " " + r.id).toLowerCase().includes(query)) })).filter((g) => named(g.feature) || g.requirements.length).sort((a, b) => way * (key === "requirements" ? a.feature.requirements.length - b.feature.requirements.length : key === "title" ? a.feature.title.localeCompare(b.feature.title) : (a.feature.updatedAt ?? "").localeCompare(b.feature.updatedAt ?? "")) || a.feature.title.localeCompare(b.feature.title));
28373
+ const page = pageOf(groups, (g) => g.feature.id, request);
28374
+ if (!page) return void 0;
28375
+ const features = page.rows.map(({ feature: f, requirements }) => {
28376
+ const designed = new Set(f.designs.flatMap((d) => d.requirements));
28377
+ return {
28378
+ id: f.id,
28379
+ path: f.path,
28380
+ title: f.title,
28381
+ description: f.description,
28382
+ state: f.state,
28383
+ designs: f.designs.length,
28384
+ contributors: f.contributors,
28385
+ updatedAt: f.updatedAt,
28386
+ requirementCount: f.requirements.length,
28387
+ requirements: requirements.slice(0, SHOWN_REQUIREMENTS).map((r) => ({
28388
+ id: r.id,
28389
+ title: r.title,
28390
+ description: r.description,
28391
+ state: r.state,
28392
+ acceptance: acceptanceCount(r.body),
28393
+ designed: designed.has(r.id)
28394
+ })),
28395
+ hidden: Math.max(0, requirements.length - SHOWN_REQUIREMENTS)
28396
+ };
28397
+ });
28398
+ return {
28399
+ total: page.total,
28400
+ all: base.features.length,
28401
+ requirements: groups.reduce((sum, g) => sum + g.requirements.length, 0),
28402
+ mostRequirements: Math.max(0, ...base.features.map((f) => f.requirements.length)),
28403
+ next: page.next,
28404
+ features
28405
+ };
28406
+ }
28407
+ var featureOf = (base, id) => base.features.find((f) => f.id === id);
28408
+ function listContributors(base, q, request) {
28409
+ const query = q?.trim().toLowerCase();
28410
+ const people = base.people.filter((p) => !query || (p.name + " " + p.email).toLowerCase().includes(query));
28411
+ const page = pageOf(people, (p) => p.email, request);
28412
+ if (!page) return void 0;
28413
+ const touched = (email3) => base.features.filter((f) => f.contributors.some((c) => c.email === email3)).length;
28414
+ return { total: page.total, next: page.next, people: page.rows.map((p) => ({ ...p, features: touched(p.email) })) };
28415
+ }
28416
+ function contributorOf(base, email3) {
28417
+ const person = base.people.find((p) => p.email === email3);
28418
+ if (!person) return void 0;
28419
+ const features = base.features.flatMap((f) => {
28420
+ const share = f.contributors.find((c) => c.email === email3);
28421
+ return share ? [{ id: f.id, title: f.title, commits: share.commits, requirements: f.requirements.length }] : [];
28422
+ });
28423
+ return { person, features };
28424
+ }
28425
+ function topContributors(people) {
28426
+ const rest = people.slice(3);
28427
+ return { total: people.length, top: people.slice(0, 3), rest: { count: rest.length, commits: rest.reduce((sum, p) => sum + p.commits, 0) } };
28428
+ }
28429
+
28044
28430
  // src/server/routes/record-routes.ts
28045
28431
  var PAGE = 20;
28046
28432
  function recordRoutes(root, sessionId, env) {
28047
28433
  const git = storeReader(root);
28048
28434
  const cache = openCache(root, { run: (args, input2) => git.run(args, input2), decode: git.decode, legacyBundles: (oids) => git.readBundles(oids) });
28049
- const readCheckout = createCheckoutReader(root, sessionId, cache, env);
28435
+ const checkout = createCheckoutReader(root, cache, env);
28050
28436
  const readStamp = createStampReader(root, env);
28051
28437
  const commitFiles = createCommitFiles(git);
28438
+ const source2 = { head: async () => (await git.baseline()).head, reader: git, cache };
28052
28439
  const working2 = { head: async () => (await git.baseline()).head, cache, pendingRecords: () => readPendingRecords(root, (args) => git.run(args)) };
28053
28440
  const unreadable2 = () => t2("server.specsUnreadable");
28054
- const cursorGone = () => new HttpError(404, "NOT_FOUND", t2("server.cursorNotFound"));
28441
+ const cursorGone2 = () => new HttpError(404, "NOT_FOUND", t2("server.cursorNotFound"));
28055
28442
  return [
28056
- route({ method: "GET", path: "/api/v1/specs", session: true, unreadable: unreadable2, handle: async () => ok((await readCheckout()).checkout) }),
28443
+ // The checkout a part at a time, each shaped by the query layer (`queries/checkout.ts`): the frame every screen reads
28444
+ // first, the feature list and one feature, the instructions, and the contributors.
28445
+ route({ method: "GET", path: "/api/v1/checkout", session: true, unreadable: unreadable2, handle: async () => {
28446
+ const base = await checkout.base();
28447
+ return ok(browserCheckoutV1.parse({
28448
+ contract: "browser-checkout",
28449
+ version: 1,
28450
+ sessionId,
28451
+ head: base.head,
28452
+ observedAt: base.observedAt,
28453
+ stamp: base.stamp,
28454
+ working: base.working,
28455
+ problems: base.problems,
28456
+ index: checkoutIndex(base)
28457
+ }));
28458
+ } }),
28459
+ route({ method: "GET", path: "/api/v1/features", session: true, query: browserFeaturesQueryV1, unreadable: unreadable2, handle: async ({ query }) => {
28460
+ const page = listFeatures(await checkout.base(), query, { after: query.after, limit: query.limit ?? PAGE });
28461
+ if (!page) throw cursorGone2();
28462
+ return ok(browserFeaturesV1.parse({ contract: "browser-features", version: 1, sessionId, ...page }));
28463
+ } }),
28464
+ route({ method: "GET", path: "/api/v1/feature", session: true, query: browserFeatureQueryV1, unreadable: unreadable2, handle: async ({ query }) => {
28465
+ const feature2 = featureOf(await checkout.base(), query.id);
28466
+ if (!feature2) throw new HttpError(404, "NOT_FOUND", t2("server.featureNotFound"));
28467
+ return ok(browserFeatureV1.parse({ contract: "browser-feature", version: 1, sessionId, feature: feature2 }));
28468
+ } }),
28469
+ route({ method: "GET", path: "/api/v1/instructions", session: true, unreadable: unreadable2, handle: async () => ok(browserInstructionsV1.parse({ contract: "browser-instructions", version: 1, sessionId, ...await checkout.instructions() })) }),
28470
+ route({ method: "GET", path: "/api/v1/contributors", session: true, query: browserContributorsQueryV1, unreadable: unreadable2, handle: async ({ query }) => {
28471
+ const page = listContributors(await checkout.base(), query.q, { after: query.after, limit: query.limit ?? PAGE });
28472
+ if (!page) throw cursorGone2();
28473
+ return ok(browserContributorsV1.parse({ contract: "browser-contributors", version: 1, sessionId, ...page }));
28474
+ } }),
28475
+ route({ method: "GET", path: "/api/v1/contributor", session: true, query: browserContributorQueryV1, unreadable: unreadable2, handle: async ({ query }) => {
28476
+ const found = contributorOf(await checkout.base(), query.email);
28477
+ if (!found) throw new HttpError(404, "NOT_FOUND", t2("server.contributorNotFound"));
28478
+ return ok(browserContributorV1.parse({ contract: "browser-contributor", version: 1, sessionId, ...found }));
28479
+ } }),
28057
28480
  // Whether the screen is behind, asked when the reader comes back to the tab; cheap enough to ask every time.
28058
28481
  route({ method: "GET", path: "/api/v1/stamp", session: true, unreadable: unreadable2, handle: async () => ok(browserStampV1.parse({ contract: "browser-stamp", version: 1, sessionId, stamp: await readStamp() })) }),
28059
28482
  route({ method: "GET", path: "/api/v1/history", session: true, query: browserHistoryQueryV4, unreadable: unreadable2, handle: async ({ query }) => {
28060
28483
  const page = await cache.history.commits(query.head, { kind: query.kind, document: query.document, feature: query.feature, author: query.author, q: query.q }, query.after, query.limit ?? PAGE);
28061
- if (!page) throw cursorGone();
28484
+ if (!page) throw cursorGone2();
28062
28485
  return ok(browserHistoryV6.parse({ contract: "browser-history", version: 6, sessionId, head: query.head, ...page }));
28063
28486
  } }),
28064
- route({ method: "GET", path: "/api/v1/history/summary", session: true, query: browserHistorySummaryQueryV1, unreadable: unreadable2, handle: async ({ query }) => ok(browserHistorySummaryV4.parse({ contract: "browser-history-summary", version: 4, sessionId, head: query.head, ...await cache.history.summary(query.head) })) }),
28487
+ route({ method: "GET", path: "/api/v1/history/summary", session: true, query: browserHistorySummaryQueryV1, unreadable: unreadable2, handle: async ({ query }) => ok(browserHistorySummaryV5.parse({
28488
+ contract: "browser-history-summary",
28489
+ version: 5,
28490
+ sessionId,
28491
+ head: query.head,
28492
+ ...await cache.history.summary(query.head),
28493
+ people: topContributors(await cache.log.contributors(query.head))
28494
+ })) }),
28065
28495
  // One commit as its page reads it: who made it and a page of the documents it changed, even when it changed none.
28066
- route({ method: "GET", path: "/api/v1/commit", session: true, query: browserCommitQueryV2, unreadable: unreadable2, handle: async ({ query }) => {
28067
- const page = await cache.history.commitChanges(query.commit, query.after, query.limit ?? PAGE);
28068
- if (!page) throw cursorGone();
28496
+ route({ method: "GET", path: "/api/v1/commit", session: true, query: browserCommitQueryV3, unreadable: unreadable2, handle: async ({ query }) => {
28497
+ const page = await cache.history.commitChanges(query.commit, query.after, query.limit ?? PAGE, query.record);
28498
+ if (!page) throw cursorGone2();
28069
28499
  const first = page.events[0];
28070
28500
  const line2 = first ? [] : git.decode(await git.run(["log", "-1", "--format=%aN%x00%aE%x00%cN%x00%aI%x00%s", query.commit, "--"]).catch(() => Buffer.from(""))).split("\0");
28071
28501
  if (!first && line2.length < 5) throw new HttpError(404, "NOT_FOUND", t2("server.commitNotFound"));
@@ -28093,9 +28523,9 @@ function recordRoutes(root, sessionId, env) {
28093
28523
  } }),
28094
28524
  // A record's page names the record; which commit added it is read from the history of the HEAD the reader is on.
28095
28525
  route({ method: "GET", path: "/api/v1/record", session: true, query: browserRecordQueryV1, unreadable: unreadable2, handle: async ({ query }) => {
28096
- const commit = await cache.history.commitOfRecord(query.head, query.id);
28097
- if (!commit) throw new HttpError(404, "NOT_FOUND", t2("server.recordNotFound"));
28098
- return ok(browserRecordV1.parse({ contract: "browser-record", version: 1, sessionId, head: query.head, id: query.id, commit }));
28526
+ const found = await cache.history.recordOf(query.head, query.id);
28527
+ if (!found) throw new HttpError(404, "NOT_FOUND", t2("server.recordNotFound"));
28528
+ return ok(browserRecordV2.parse({ contract: "browser-record", version: 2, sessionId, head: query.head, id: query.id, ...found }));
28099
28529
  } }),
28100
28530
  // What is not committed yet, worked out again on every request: the uncommitted entry of the records list.
28101
28531
  route({ method: "GET", path: "/api/v1/working", session: true, unreadable: unreadable2, handle: async () => ok(browserWorkingV1.parse({ contract: "browser-working", version: 1, sessionId, ...await workingOverview(working2) })) }),
@@ -28105,10 +28535,11 @@ function recordRoutes(root, sessionId, env) {
28105
28535
  return ok(browserWorkingChangeV1.parse({ contract: "browser-working-change", version: 1, sessionId, ...change2 }));
28106
28536
  } }),
28107
28537
  // The source a commit changed beside its documents, read from Git when the commit or record page asks for it.
28108
- route({ method: "GET", path: "/api/v1/commit/files", session: true, query: browserCommitFilesQueryV1, unreadable: unreadable2, handle: async ({ query }) => {
28109
- const found = await commitFiles.files(query.commit);
28110
- if (!found) throw new HttpError(404, "NOT_FOUND", t2("server.commitNotFound"));
28111
- return ok(browserCommitFilesV1.parse({ contract: "browser-commit-files", version: 1, sessionId, commit: found.commit, total: found.total, files: found.files }));
28538
+ route({ method: "GET", path: "/api/v1/commit/files", session: true, query: browserCommitFilesQueryV2, unreadable: unreadable2, handle: async ({ query }) => {
28539
+ const found = await commitFiles.files(query.commit, { after: query.after, limit: query.limit ?? PAGE });
28540
+ if (found === void 0) throw new HttpError(404, "NOT_FOUND", t2("server.commitNotFound"));
28541
+ if (found === null) throw cursorGone2();
28542
+ return ok(browserCommitFilesV2.parse({ contract: "browser-commit-files", version: 2, sessionId, commit: found.commit, total: found.total, next: found.next, files: found.files }));
28112
28543
  } }),
28113
28544
  route({ method: "GET", path: "/api/v1/commit/file", session: true, query: browserCommitFileQueryV1, unreadable: unreadable2, handle: async ({ query }) => {
28114
28545
  const found = await commitFiles.file(query.commit, query.path);
@@ -28122,21 +28553,26 @@ function recordRoutes(root, sessionId, env) {
28122
28553
  if (!found) throw new HttpError(404, "NOT_FOUND", t2("server.instructionFileNotFound"));
28123
28554
  return ok(browserInstructionFileV1.parse({ contract: "browser-instruction-file", version: 1, sessionId, id: query.id, path: query.path, ...found }));
28124
28555
  } }),
28125
- route({ method: "GET", path: "/api/v1/search", session: true, query: browserSearchQueryV1, unreadable: unreadable2, handle: async ({ query }) => ok(browserSearchV2.parse({ contract: "browser-search", version: 2, sessionId, query: query.q, hits: await cache.search(query.head ?? null, query.q) })) })
28556
+ // The search box's groups as the query layer works them out (`queries/search.ts`); the browser only draws them.
28557
+ route({ method: "GET", path: "/api/v1/search", session: true, query: browserSearchQueryV2, unreadable: unreadable2, handle: async ({ query }) => {
28558
+ const groups = await searchRecords(source2, query);
28559
+ if (!groups) throw cursorGone2();
28560
+ return ok(browserSearchV3.parse({ contract: "browser-search", version: 3, sessionId, query: query.q, groups }));
28561
+ } })
28126
28562
  ];
28127
28563
  }
28128
28564
 
28129
28565
  // src/server/routes/asset-routes.ts
28130
- import { lstat as lstat8, readFile as readFile7 } from "node:fs/promises";
28131
- import { join as join9 } from "node:path";
28566
+ import { lstat as lstat9, readFile as readFile7 } from "node:fs/promises";
28567
+ import { join as join10 } from "node:path";
28132
28568
  function assetRoutes(root) {
28133
28569
  return [
28134
28570
  route({ method: "GET", path: "/api/v1/assets/*", session: false, handle: async ({ path: path3, response }) => {
28135
28571
  const relative = path3.slice("/api/v1/assets/".length);
28136
28572
  const assetPath = ASSETS_DIR + "/" + relative;
28137
28573
  if (!isAssetPath(assetPath)) throw new HttpError(404, "NOT_FOUND", t2("server.assetNotFound"));
28138
- const file2 = join9(root, ...assetPath.split("/"));
28139
- const stat3 = await lstat8(file2).catch(() => void 0);
28574
+ const file2 = join10(root, ...assetPath.split("/"));
28575
+ const stat3 = await lstat9(file2).catch(() => void 0);
28140
28576
  if (!stat3?.isFile() || stat3.isSymbolicLink() || stat3.size > 20 * 1024 * 1024) throw new HttpError(404, "NOT_FOUND", t2("server.assetNotFound"));
28141
28577
  const bytes = await readFile7(file2);
28142
28578
  const extension = assetExtension(relative);
@@ -28286,13 +28722,13 @@ async function runBrowser(options, version2) {
28286
28722
 
28287
28723
  // src/commands/spec-init.ts
28288
28724
  import { readdir as readdir5 } from "node:fs/promises";
28289
- import { join as join13 } from "node:path";
28725
+ import { join as join14 } from "node:path";
28290
28726
 
28291
28727
  // src/adapters/git/init-repository.ts
28292
28728
  import { createHash as createHash3 } from "node:crypto";
28293
- import { lstat as lstat9, readFile as readFile9 } from "node:fs/promises";
28294
- import { join as join10 } from "node:path";
28295
- var markerPresent = (path3) => lstat9(path3).then(() => true, (error62) => {
28729
+ import { lstat as lstat10, readFile as readFile9 } from "node:fs/promises";
28730
+ import { join as join11 } from "node:path";
28731
+ var markerPresent = (path3) => lstat10(path3).then(() => true, (error62) => {
28296
28732
  if (error62.code === "ENOENT") return false;
28297
28733
  throw error62;
28298
28734
  });
@@ -28315,7 +28751,7 @@ function initRepository(cwd, inherited = process.env) {
28315
28751
  const gitDir = locations.length === 2 ? locations[0] : decodeGitLine(await git(["rev-parse", "--absolute-git-dir"], root));
28316
28752
  const indexPath = locations.length === 2 ? locations[1] : decodeGitLine(await git(["rev-parse", "--path-format=absolute", "--git-path", "index"], root));
28317
28753
  for (const marker2 of ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "rebase-merge", "rebase-apply", "sequencer", "BISECT_START", "index.lock"]) {
28318
- if (await markerPresent(join10(gitDir, marker2))) throw new InitError("GIT_OPERATION_IN_PROGRESS", t2("git.operationInProgress", { marker: marker2 }));
28754
+ if (await markerPresent(join11(gitDir, marker2))) throw new InitError("GIT_OPERATION_IN_PROGRESS", t2("git.operationInProgress", { marker: marker2 }));
28319
28755
  }
28320
28756
  if (state2.changes.some((change2) => change2.kind === "unmerged")) throw new InitError("GIT_OPERATION_IN_PROGRESS", t2("git.resolveConflicts"));
28321
28757
  const index = await readFile9(indexPath).catch((error62) => {
@@ -28353,14 +28789,14 @@ function initRepository(cwd, inherited = process.env) {
28353
28789
 
28354
28790
  // src/adapters/filesystem/document-file.ts
28355
28791
  import { randomBytes } from "node:crypto";
28356
- import { lstat as lstat10, mkdir as mkdir3, writeFile as writeFile2 } from "node:fs/promises";
28357
- import { join as join11 } from "node:path";
28792
+ import { lstat as lstat11, mkdir as mkdir3, writeFile as writeFile2 } from "node:fs/promises";
28793
+ import { join as join12 } from "node:path";
28358
28794
  var generateId = (prefix) => prefix + "-" + [...randomBytes(10)].map((n) => "abcdefghijklmnopqrstuvwxyz234567"[n & 31]).join("");
28359
28795
  async function createDocumentFile(root, path3, text4) {
28360
28796
  const parts = path3.split("/");
28361
28797
  for (let i = 1; i < parts.length; i++) {
28362
- const folder = join11(root, ...parts.slice(0, i));
28363
- const info5 = await lstat10(folder).catch((error62) => {
28798
+ const folder = join12(root, ...parts.slice(0, i));
28799
+ const info5 = await lstat11(folder).catch((error62) => {
28364
28800
  if (error62.code === "ENOENT") return void 0;
28365
28801
  throw error62;
28366
28802
  });
@@ -28370,7 +28806,7 @@ async function createDocumentFile(root, path3, text4) {
28370
28806
  }
28371
28807
  if (!info5.isDirectory() || info5.isSymbolicLink()) throw Object.assign(new Error(folder), { code: "ENOTDIR" });
28372
28808
  }
28373
- await writeFile2(join11(root, ...parts), text4, { flag: "wx" });
28809
+ await writeFile2(join12(root, ...parts), text4, { flag: "wx" });
28374
28810
  }
28375
28811
  var LINE_ENDINGS_PATH = ".gitifact/.gitattributes";
28376
28812
  var LINE_ENDINGS = "# Written by gitifact init: documents keep LF on every OS, so what Git holds and the files read the same.\n* text=auto eol=lf\n";
@@ -28386,14 +28822,14 @@ async function writeLineEndings(root) {
28386
28822
 
28387
28823
  // src/adapters/filesystem/managed-file.ts
28388
28824
  import { open as open4, readFile as readFile10, mkdir as mkdir4, realpath as realpath2, rename as rename2, link as link2, unlink as unlink2 } from "node:fs/promises";
28389
- import { join as join12, dirname } from "node:path";
28825
+ import { join as join13, dirname } from "node:path";
28390
28826
  import { randomUUID as randomUUID4 } from "node:crypto";
28391
28827
  var DEFAULT_LIMIT = 1024 * 1024;
28392
28828
  async function managedPath(root, path3, create = false) {
28393
28829
  let full = root;
28394
28830
  const parts = path3.split("/");
28395
28831
  for (let i = 0; i < parts.length; i++) {
28396
- full = join12(full, parts[i]);
28832
+ full = join13(full, parts[i]);
28397
28833
  let info5 = await fileInfo(full);
28398
28834
  if (!info5 && create && i < parts.length - 1) {
28399
28835
  await mkdir4(full).catch((error62) => {
@@ -28441,7 +28877,7 @@ async function managedWrite(root, path3, previous, next, recheck) {
28441
28877
  if (previous !== null) await unlink2(full);
28442
28878
  return;
28443
28879
  }
28444
- const temp = join12(directory, ".gitifact-managed-" + randomUUID4() + ".tmp");
28880
+ const temp = join13(directory, ".gitifact-managed-" + randomUUID4() + ".tmp");
28445
28881
  const handle = await open4(temp, "wx", 384);
28446
28882
  const tempOwner = await handle.stat();
28447
28883
  try {
@@ -28768,7 +29204,7 @@ async function initializeSpecProject(cwd, dryRun = false, env = process.env, bef
28768
29204
  if (old !== void 0) return existing(old);
28769
29205
  if (first.trackedConfig) throw new InitError("CONFIG_DELETED", t2("init.configDeleted"));
28770
29206
  const checkRecords = async () => {
28771
- const entries = await readdir5(join13(root, ".gitifact")).catch((e) => {
29207
+ const entries = await readdir5(join14(root, ".gitifact")).catch((e) => {
28772
29208
  if (e.code === "ENOENT") return [];
28773
29209
  throw e;
28774
29210
  });
@@ -28837,16 +29273,16 @@ ${lineEndings}${newer}` : JSON.stringify(dto) + "\n");
28837
29273
 
28838
29274
  // src/commands/documents.ts
28839
29275
  import { readFile as readFile13 } from "node:fs/promises";
28840
- import { join as join15 } from "node:path";
29276
+ import { join as join16 } from "node:path";
28841
29277
 
28842
29278
  // src/commands/project.ts
28843
- import { join as join14 } from "node:path";
29279
+ import { join as join15 } from "node:path";
28844
29280
  var operations = ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "rebase-merge", "rebase-apply", "sequencer", "BISECT_START", "index.lock"];
28845
29281
  async function openProject(cwd, options = {}) {
28846
29282
  const reader = storeReader(cwd);
28847
29283
  const location = await reader.location();
28848
29284
  if (options.writing) {
28849
- for (const marker2 of operations) if (await fileInfo(join14(location.gitDir, marker2))) throw new InitError("GIT_OPERATION_IN_PROGRESS", t2("git.operationInProgress", { marker: marker2 }));
29285
+ for (const marker2 of operations) if (await fileInfo(join15(location.gitDir, marker2))) throw new InitError("GIT_OPERATION_IN_PROGRESS", t2("git.operationInProgress", { marker: marker2 }));
28850
29286
  if (await reader.hasUnmerged()) throw new InitError("GIT_OPERATION_IN_PROGRESS", t2("git.resolveConflicts"));
28851
29287
  }
28852
29288
  const raw = await readConfigFile(location.root);
@@ -28877,6 +29313,125 @@ function documentsOf(files) {
28877
29313
  return docs;
28878
29314
  }
28879
29315
 
29316
+ // src/queries/filters.ts
29317
+ function byAuthor(author) {
29318
+ if (author === void 0) return () => true;
29319
+ const wanted = author.trim().toLowerCase();
29320
+ return (change2) => change2.author.toLowerCase() === wanted || change2.email.toLowerCase() === wanted;
29321
+ }
29322
+ async function since(project, value, head) {
29323
+ if (value === void 0) return () => true;
29324
+ if (/^\d{4}-\d\d-\d\d/.test(value)) {
29325
+ const from = Date.parse(value);
29326
+ if (!Number.isFinite(from)) throw new CommandError("INVALID_VALUE", t2("list.since", { value }));
29327
+ return (change2) => Date.parse(change2.date) >= from;
29328
+ }
29329
+ let commit;
29330
+ try {
29331
+ commit = await project.reader.resolve(value);
29332
+ } catch {
29333
+ throw new CommandError("INVALID_VALUE", t2("list.since", { value }));
29334
+ }
29335
+ if (!head) return () => false;
29336
+ const after = new Set(project.reader.decode(await project.reader.run(["rev-list", `${commit}..${head}`, "--"])).split("\n").filter(Boolean));
29337
+ return (change2) => after.has(change2.commit);
29338
+ }
29339
+ async function sinceRange(project, value, head) {
29340
+ if (value === void 0) return {};
29341
+ if (/^\d{4}-\d\d-\d\d/.test(value)) {
29342
+ const from = Date.parse(value);
29343
+ if (!Number.isFinite(from)) throw new CommandError("INVALID_VALUE", t2("list.since", { value }));
29344
+ return { from };
29345
+ }
29346
+ let commit;
29347
+ try {
29348
+ commit = await project.reader.resolve(value);
29349
+ } catch {
29350
+ throw new CommandError("INVALID_VALUE", t2("list.since", { value }));
29351
+ }
29352
+ if (!head) return { commits: [] };
29353
+ return { commits: project.reader.decode(await project.reader.run(["rev-list", `${commit}..${head}`, "--"])).split("\n").filter(Boolean) };
29354
+ }
29355
+ var contains = (query) => {
29356
+ const wanted = query?.trim().toLowerCase();
29357
+ return (...texts) => !wanted || texts.some((value) => value?.toLowerCase().includes(wanted));
29358
+ };
29359
+
29360
+ // src/queries/specs.ts
29361
+ var titledSources = (d, byId) => d.sources.map((s) => "id" in s ? { id: s.id, title: byId.get(s.id)?.title ?? null, ...s.note ? { note: s.note } : {} } : s);
29362
+ async function listSpecs(source2, filter, request) {
29363
+ const listed3 = await source2.cache.documents.list();
29364
+ const problems = listed3.problems;
29365
+ const states = await documentStates(source2, listed3.documents);
29366
+ const documents = states.documents;
29367
+ const byId = new Map(documents.map((d) => [d.id, d]));
29368
+ const arranged = arrangeDocuments(documents);
29369
+ let features = arranged.features;
29370
+ if (filter.feature !== void 0) {
29371
+ features = features.filter((f) => f.index.feature === filter.feature);
29372
+ if (!features.length) throw new CommandError("UNKNOWN_FEATURE", t2("docs.unknownFeature", { feature: filter.feature }));
29373
+ }
29374
+ const titled = (id) => ({ id, title: byId.get(id)?.title ?? null });
29375
+ const designsOf = /* @__PURE__ */ new Map();
29376
+ for (const f of arranged.features) for (const d of f.designs) for (const r of d.requirements) designsOf.set(r, [...designsOf.get(r) ?? [], titled(d.id)]);
29377
+ const historyNeeded = filter.sort === "updated" || filter.changedSince !== void 0 || filter.author !== void 0 || !!filter.withUpdated;
29378
+ const latest = /* @__PURE__ */ new Map();
29379
+ const newest = /* @__PURE__ */ new Map();
29380
+ let touched;
29381
+ if (historyNeeded) {
29382
+ const head = await source2.head();
29383
+ const changes2 = head ? await source2.cache.history.changesOf(head) : [];
29384
+ changes2.forEach((c, rank) => {
29385
+ if (!latest.has(c.id)) {
29386
+ latest.set(c.id, { commit: c.commit, date: c.date, author: c.author, email: c.email });
29387
+ newest.set(c.id, rank);
29388
+ }
29389
+ });
29390
+ if (filter.changedSince !== void 0 || filter.author !== void 0) {
29391
+ const after = await since(source2, filter.changedSince, head);
29392
+ const by = byAuthor(filter.author);
29393
+ touched = new Set(changes2.filter((c) => after(c) && by(c)).map((c) => c.id));
29394
+ }
29395
+ }
29396
+ const matched = filter.q !== void 0 ? await source2.cache.documents.matching(filter.q) : void 0;
29397
+ const rows = features.flatMap((f) => [f.index, ...f.requirements, ...f.designs].filter(
29398
+ (doc) => (filter.type === void 0 || doc.kind === filter.type) && (!filter.withoutDesign || !f.designs.length) && (!filter.uncovered || doc.kind === "requirement" && !designsOf.has(doc.id)) && (!filter.draft || doc.draft) && (!touched || touched.has(doc.id)) && (!matched || matched.has(doc.id))
29399
+ ).map((doc) => ({
29400
+ id: doc.id,
29401
+ kind: doc.kind,
29402
+ feature: doc.feature,
29403
+ path: doc.path,
29404
+ title: doc.title,
29405
+ description: doc.description,
29406
+ ...doc.kind === "requirement" || doc.kind === "design" ? { order: doc.order } : {},
29407
+ ...doc.draft ? { draft: true } : {},
29408
+ state: states.state(doc.id),
29409
+ ...states.previousPath(doc.id) ? { previousPath: states.previousPath(doc.id) } : {},
29410
+ ...doc.kind === "design" ? { requirements: doc.requirements.map(titled), sources: titledSources(doc, byId) } : {},
29411
+ ...doc.kind === "requirement" ? { designs: designsOf.get(doc.id) ?? [] } : {},
29412
+ ...historyNeeded ? { updated: latest.get(doc.id) ?? null } : {},
29413
+ ...matched ? { line: matched.get(doc.id) } : {}
29414
+ })));
29415
+ if (filter.sort === "title") rows.sort((a, b) => a.title.localeCompare(b.title));
29416
+ if (filter.sort === "updated") rows.sort((a, b) => (newest.get(a.id) ?? Infinity) - (newest.get(b.id) ?? Infinity));
29417
+ const grouped = filter.sort === "order";
29418
+ const groups = features.map((f) => ({ id: f.index.id, rows: rows.filter((r) => r.feature === f.index.feature) })).filter((g) => g.rows.length);
29419
+ const page = grouped ? pageOf(groups, (g) => g.id, request) : pageOf(rows, (r) => r.id, request);
29420
+ if (!page) return void 0;
29421
+ const shown = grouped ? page.rows.flatMap((g) => g.rows) : page.rows;
29422
+ return {
29423
+ rows: shown,
29424
+ features,
29425
+ byId,
29426
+ states,
29427
+ grouped,
29428
+ problems,
29429
+ page: { rows: page.rows, total: page.total, next: page.next },
29430
+ unit: grouped ? "feature" : "document",
29431
+ orphans: filter.feature === void 0 ? arranged.orphans : []
29432
+ };
29433
+ }
29434
+
28880
29435
  // src/commands/documents.ts
28881
29436
  var draftMark = (doc) => doc.draft ? " (" + t2("docs.draft") + ")" : "";
28882
29437
  var stateLabel = {
@@ -28887,12 +29442,11 @@ var stateLabel = {
28887
29442
  var stateMark = (state2) => state2 === "committed" ? "" : " (" + stateLabel[state2]() + ")";
28888
29443
  var line = (doc) => `${doc.id} ${doc.title}${draftMark(doc)} \u2014 ${doc.description}`;
28889
29444
  var fileLine = (f) => f.title ? `${f.path} ${f.title} \u2014 ${f.description}` : f.path;
28890
- var titledSources = (d, byId) => d.sources.map((s) => "id" in s ? { id: s.id, title: byId.get(s.id)?.title ?? null, ...s.note ? { note: s.note } : {} } : s);
28891
29445
  async function working(project) {
28892
29446
  const { documents } = await project.cache.documents.list();
28893
29447
  return {
28894
29448
  byId: new Map(documents.map((d) => [d.id, d])),
28895
- read: (path3) => readFile13(join15(project.root, ...path3.split("/")), "utf8"),
29449
+ read: (path3) => readFile13(join16(project.root, ...path3.split("/")), "utf8"),
28896
29450
  referencing: async (id) => [...new Set((await project.cache.documents.referencing(id)).map((r) => r.from))]
28897
29451
  };
28898
29452
  }
@@ -29001,48 +29555,6 @@ function checkFields(fields2, columns3) {
29001
29555
  if (unknown2.length || !fields2.length) throw new CommandError("INVALID_VALUE", t2("list.unknownFields", { fields: unknown2.join(", "), columns: columns3.join(", ") }));
29002
29556
  return [...new Set(fields2)];
29003
29557
  }
29004
- function byAuthor(author) {
29005
- if (author === void 0) return () => true;
29006
- const wanted = author.trim().toLowerCase();
29007
- return (change2) => change2.author.toLowerCase() === wanted || change2.email.toLowerCase() === wanted;
29008
- }
29009
- async function since(project, value, head) {
29010
- if (value === void 0) return () => true;
29011
- if (/^\d{4}-\d\d-\d\d/.test(value)) {
29012
- const from = Date.parse(value);
29013
- if (!Number.isFinite(from)) throw new CommandError("INVALID_VALUE", t2("list.since", { value }));
29014
- return (change2) => Date.parse(change2.date) >= from;
29015
- }
29016
- let commit;
29017
- try {
29018
- commit = await project.reader.resolve(value);
29019
- } catch {
29020
- throw new CommandError("INVALID_VALUE", t2("list.since", { value }));
29021
- }
29022
- if (!head) return () => false;
29023
- const after = new Set(project.reader.decode(await project.reader.run(["rev-list", `${commit}..${head}`, "--"])).split("\n").filter(Boolean));
29024
- return (change2) => after.has(change2.commit);
29025
- }
29026
- async function sinceRange(project, value, head) {
29027
- if (value === void 0) return {};
29028
- if (/^\d{4}-\d\d-\d\d/.test(value)) {
29029
- const from = Date.parse(value);
29030
- if (!Number.isFinite(from)) throw new CommandError("INVALID_VALUE", t2("list.since", { value }));
29031
- return { from };
29032
- }
29033
- let commit;
29034
- try {
29035
- commit = await project.reader.resolve(value);
29036
- } catch {
29037
- throw new CommandError("INVALID_VALUE", t2("list.since", { value }));
29038
- }
29039
- if (!head) return { commits: [] };
29040
- return { commits: project.reader.decode(await project.reader.run(["rev-list", `${commit}..${head}`, "--"])).split("\n").filter(Boolean) };
29041
- }
29042
- var contains = (query) => {
29043
- const wanted = query?.trim().toLowerCase();
29044
- return (...texts) => !wanted || texts.some((value) => value?.toLowerCase().includes(wanted));
29045
- };
29046
29558
  function cell(value) {
29047
29559
  if (value === void 0 || value === null || value === false) return "";
29048
29560
  if (Array.isArray(value)) return value.map((v) => typeof v === "object" && v !== null ? "id" in v ? v.id : "title" in v ? v.title : JSON.stringify(v) : String(v)).join(",");
@@ -29061,18 +29573,14 @@ function selected(rows, fields2) {
29061
29573
  text: rows.length ? text2(rows.map((row) => fields2.map((f) => cell(row[f])).join(" "))) : ""
29062
29574
  };
29063
29575
  }
29064
- var PAGE_SIZE = 20;
29065
29576
  function paginate(rows, keyOf, options) {
29066
- let start = 0;
29067
- if (options.after !== void 0) {
29068
- const at = rows.findIndex((row) => keyOf(row) === options.after);
29069
- if (at < 0) throw new CommandError("INVALID_VALUE", t2("list.after", { cursor: options.after }));
29070
- start = at + 1;
29071
- }
29072
- const size = options.all ? rows.length : options.limit ?? PAGE_SIZE;
29073
- const shown = rows.slice(start, start + size);
29074
- const more = start + shown.length < rows.length && shown.length > 0;
29075
- return { rows: shown, total: rows.length, next: more ? keyOf(shown[shown.length - 1]) : null };
29577
+ return pageOr(pageOf(rows, keyOf, options), options);
29578
+ }
29579
+ function pageOr(page, options) {
29580
+ return page ?? cursorGone(options);
29581
+ }
29582
+ function cursorGone(options) {
29583
+ throw new CommandError("INVALID_VALUE", t2("list.after", { cursor: options.after ?? "" }));
29076
29584
  }
29077
29585
  var pageLine = (page, before = 0, unit = t2("list.items")) => page.next === null ? [] : [t2("list.more", { shown: before + page.rows.length, total: page.total, unit, cursor: page.next })];
29078
29586
  function aside(format2, lines) {
@@ -29088,67 +29596,10 @@ var kindLabel = { feature: () => t2("docs.feature"), requirement: () => t2("docs
29088
29596
  var runSpecsList = (options) => runCommand("specs", options.format, async () => {
29089
29597
  const fields2 = checkFields(options.fields, columns);
29090
29598
  const project = await openProject(process.cwd());
29091
- const listed3 = await project.cache.documents.list();
29092
- const problems = listed3.problems;
29093
- const states = await documentStates(project, listed3.documents);
29094
- const documents = states.documents;
29095
- const byId = new Map(documents.map((d) => [d.id, d]));
29096
- const arranged = arrangeDocuments(documents);
29097
- let features = arranged.features;
29098
- if (options.feature !== void 0) {
29099
- features = features.filter((f) => f.index.feature === options.feature);
29100
- if (!features.length) throw new CommandError("UNKNOWN_FEATURE", t2("docs.unknownFeature", { feature: options.feature }));
29101
- }
29102
- const titled = (id) => ({ id, title: byId.get(id)?.title ?? null });
29103
- const designsOf = /* @__PURE__ */ new Map();
29104
- for (const f of arranged.features) for (const d of f.designs) for (const r of d.requirements) designsOf.set(r, [...designsOf.get(r) ?? [], titled(d.id)]);
29105
- const historyNeeded = options.sort === "updated" || options.changedSince !== void 0 || options.author !== void 0 || !!fields2?.includes("updated");
29106
- const latest = /* @__PURE__ */ new Map();
29107
- const newest = /* @__PURE__ */ new Map();
29108
- let touched;
29109
- if (historyNeeded) {
29110
- const head = await project.head();
29111
- const changes2 = head ? await project.cache.history.changesOf(head) : [];
29112
- changes2.forEach((c, rank) => {
29113
- if (!latest.has(c.id)) {
29114
- latest.set(c.id, { commit: c.commit, date: c.date, author: c.author, email: c.email });
29115
- newest.set(c.id, rank);
29116
- }
29117
- });
29118
- if (options.changedSince !== void 0 || options.author !== void 0) {
29119
- const after = await since(project, options.changedSince, head);
29120
- const by = byAuthor(options.author);
29121
- touched = new Set(changes2.filter((c) => after(c) && by(c)).map((c) => c.id));
29122
- }
29123
- }
29124
- const matched = options.q !== void 0 ? await project.cache.documents.matching(options.q) : void 0;
29125
- const rows = features.flatMap((f) => [f.index, ...f.requirements, ...f.designs].filter(
29126
- (doc) => (options.type === void 0 || doc.kind === options.type) && (!options.withoutDesign || !f.designs.length) && (!options.uncovered || doc.kind === "requirement" && !designsOf.has(doc.id)) && (!options.draft || doc.draft) && (!touched || touched.has(doc.id)) && (!matched || matched.has(doc.id))
29127
- ).map((doc) => ({
29128
- id: doc.id,
29129
- kind: doc.kind,
29130
- feature: doc.feature,
29131
- path: doc.path,
29132
- title: doc.title,
29133
- description: doc.description,
29134
- ...doc.kind === "requirement" || doc.kind === "design" ? { order: doc.order } : {},
29135
- ...doc.draft ? { draft: true } : {},
29136
- state: states.state(doc.id),
29137
- ...states.previousPath(doc.id) ? { previousPath: states.previousPath(doc.id) } : {},
29138
- ...doc.kind === "design" ? { requirements: doc.requirements.map(titled), sources: titledSources(doc, byId) } : {},
29139
- ...doc.kind === "requirement" ? { designs: designsOf.get(doc.id) ?? [] } : {},
29140
- ...historyNeeded ? { updated: latest.get(doc.id) ?? null } : {},
29141
- ...matched ? { line: matched.get(doc.id) } : {}
29142
- })));
29143
- if (options.sort === "title") rows.sort((a, b) => a.title.localeCompare(b.title));
29144
- if (options.sort === "updated") rows.sort((a, b) => (newest.get(a.id) ?? Infinity) - (newest.get(b.id) ?? Infinity));
29145
- const grouped = options.sort === "order";
29146
- const groups = features.map((f) => ({ id: f.index.id, rows: rows.filter((r) => r.feature === f.index.feature) })).filter((g) => g.rows.length);
29147
- const page = grouped ? paginate(groups, (g) => g.id, options) : paginate(rows, (r) => r.id, options);
29148
- const shown = grouped ? page.rows.flatMap((g) => g.rows) : page.rows;
29599
+ const listed3 = await listSpecs(project, { ...options, withUpdated: !!fields2?.includes("updated") }, options) ?? cursorGone(options);
29600
+ const { rows: shown, features, byId, states, grouped, problems, orphans, page } = listed3;
29149
29601
  const more = pageLine(page, 0, grouped ? t2("list.features") : t2("list.items"));
29150
- const paging = { total: page.total, next: page.next, unit: grouped ? "feature" : "document" };
29151
- const orphans = options.feature === void 0 ? arranged.orphans : [];
29602
+ const paging = { total: page.total, next: page.next, unit: listed3.unit };
29152
29603
  if (fields2) {
29153
29604
  const picked = selected(shown, fields2);
29154
29605
  return { json: { documents: picked.json, orphans, problems, page: paging }, text: picked.text + aside(options.format, more) };
@@ -29192,27 +29643,23 @@ var runSpecsShow = (ids, options) => runCommand("specs", options.format, async (
29192
29643
  });
29193
29644
  var runSpecsNew = (kind2, path3, options) => runCommand("specs", options.format, async () => createDocument(await openProject(process.cwd()), kind2, path3, options));
29194
29645
 
29195
- // src/commands/instructions.ts
29196
- import { lstat as lstat11 } from "node:fs/promises";
29197
- import { join as join16 } from "node:path";
29198
- var instructionSorts = ["name", "updated"];
29199
- var columns2 = ["id", "name", "path", "title", "description", "draft", "state", "previousPath", "files", "updated", "line"];
29646
+ // src/queries/instructions.ts
29647
+ import { lstat as lstat12 } from "node:fs/promises";
29648
+ import { join as join17 } from "node:path";
29200
29649
  var AGENTS = "AGENTS.md";
29201
- var runInstructionsList = (options) => runCommand("instructions", options.format, async () => {
29202
- const fields2 = checkFields(options.fields, columns2);
29203
- const project = await openProject(process.cwd());
29204
- const listed3 = await project.cache.documents.list();
29650
+ async function listInstructions(source2, options, request) {
29651
+ const listed3 = await source2.cache.documents.list();
29205
29652
  const problems = listed3.problems;
29206
- const states = await documentStates(project, listed3.documents);
29653
+ const states = await documentStates(source2, listed3.documents);
29207
29654
  const documents = states.documents;
29208
- const agents2 = { path: AGENTS, exists: !!(await lstat11(join16(project.root, AGENTS)).catch(() => void 0))?.isFile() };
29209
- const historyNeeded = options.sort === "updated" || options.author !== void 0 || !!fields2?.includes("updated");
29655
+ const agents2 = { path: AGENTS, exists: !!(await lstat12(join17(source2.root, AGENTS)).catch(() => void 0))?.isFile() };
29656
+ const historyNeeded = options.sort === "updated" || options.author !== void 0 || !!options.withUpdated;
29210
29657
  const latest = /* @__PURE__ */ new Map();
29211
29658
  const newest = /* @__PURE__ */ new Map();
29212
29659
  let touched;
29213
29660
  if (historyNeeded) {
29214
- const head = await project.head();
29215
- const changes2 = head ? await project.cache.history.changesOf(head) : [];
29661
+ const head = await source2.head();
29662
+ const changes2 = head ? await source2.cache.history.changesOf(head) : [];
29216
29663
  changes2.forEach((c, rank) => {
29217
29664
  if (!latest.has(c.id)) {
29218
29665
  latest.set(c.id, { commit: c.commit, date: c.date, author: c.author, email: c.email });
@@ -29224,11 +29671,11 @@ var runInstructionsList = (options) => runCommand("instructions", options.format
29224
29671
  touched = new Set(changes2.filter(by).map((c) => c.id));
29225
29672
  }
29226
29673
  }
29227
- const matched = options.q !== void 0 ? await project.cache.documents.matching(options.q) : void 0;
29674
+ const matched = options.q !== void 0 ? await source2.cache.documents.matching(options.q) : void 0;
29228
29675
  const instructions2 = documents.filter((d) => d.kind === "instruction").filter((d) => (!touched || touched.has(d.id)) && (!matched || matched.has(d.id))).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
29229
29676
  const rows = await Promise.all(instructions2.map(async (d) => {
29230
29677
  const state2 = states.state(d.id);
29231
- const { files, limited } = state2 === "deleted" ? { files: [], limited: false } : await readInstructionFiles(project.root, d.path);
29678
+ const { files, limited } = state2 === "deleted" ? { files: [], limited: false } : await readInstructionFiles(source2.root, d.path);
29232
29679
  return {
29233
29680
  id: d.id,
29234
29681
  name: d.name,
@@ -29245,7 +29692,19 @@ var runInstructionsList = (options) => runCommand("instructions", options.format
29245
29692
  };
29246
29693
  }));
29247
29694
  if (options.sort === "updated") rows.sort((a, b) => (newest.get(a.id) ?? Infinity) - (newest.get(b.id) ?? Infinity));
29248
- const page = paginate(rows, (r) => r.id, options);
29695
+ const page = pageOf(rows, (r) => r.id, request);
29696
+ if (!page) return void 0;
29697
+ return { agents: agents2, problems, page };
29698
+ }
29699
+
29700
+ // src/commands/instructions.ts
29701
+ var instructionSorts = ["name", "updated"];
29702
+ var columns2 = ["id", "name", "path", "title", "description", "draft", "state", "previousPath", "files", "updated", "line"];
29703
+ var runInstructionsList = (options) => runCommand("instructions", options.format, async () => {
29704
+ const fields2 = checkFields(options.fields, columns2);
29705
+ const project = await openProject(process.cwd());
29706
+ const listed3 = await listInstructions(project, { ...options, withUpdated: !!fields2?.includes("updated") }, options) ?? cursorGone(options);
29707
+ const { agents: agents2, problems, page } = listed3;
29249
29708
  const shown = page.rows;
29250
29709
  const paging = { total: page.total, next: page.next, unit: "instruction" };
29251
29710
  const unreadable2 = problems.filter((p) => p.path.startsWith(INSTRUCTIONS_ROOT + "/"));
@@ -29291,9 +29750,9 @@ var runInstructionsShow = (targets, options) => runCommand("instructions", optio
29291
29750
  var runInstructionsNew = (name2, options) => runCommand("instructions", options.format, async () => createDocument(await openProject(process.cwd()), "instruction", name2, options));
29292
29751
 
29293
29752
  // src/adapters/filesystem/document-warnings.ts
29294
- import { lstat as lstat12, readdir as readdir6 } from "node:fs/promises";
29295
- import { join as join17 } from "node:path";
29296
- var info2 = async (path3) => lstat12(path3).catch((e) => {
29753
+ import { lstat as lstat13, readdir as readdir6 } from "node:fs/promises";
29754
+ import { join as join18 } from "node:path";
29755
+ var info2 = async (path3) => lstat13(path3).catch((e) => {
29297
29756
  if (e.code === "ENOENT") return void 0;
29298
29757
  throw e;
29299
29758
  });
@@ -29301,11 +29760,11 @@ var posix = (path3) => path3.split("\\").join("/");
29301
29760
  async function listAssets(root) {
29302
29761
  const out = [];
29303
29762
  async function visit2(relative, depth) {
29304
- const stat3 = await info2(join17(root, relative));
29763
+ const stat3 = await info2(join18(root, relative));
29305
29764
  if (!stat3 || stat3.isSymbolicLink()) return;
29306
29765
  if (stat3.isDirectory()) {
29307
29766
  if (depth > 8 || out.length > 4e3) return;
29308
- for (const name2 of (await readdir6(join17(root, relative))).sort()) await visit2(relative + "/" + name2, depth + 1);
29767
+ for (const name2 of (await readdir6(join18(root, relative))).sort()) await visit2(relative + "/" + name2, depth + 1);
29309
29768
  } else if (stat3.isFile()) out.push({ path: posix(relative), bytes: stat3.size });
29310
29769
  }
29311
29770
  await visit2(ASSETS_DIR, 0);
@@ -29314,7 +29773,7 @@ async function listAssets(root) {
29314
29773
  async function readDocumentWarnings(root, documents) {
29315
29774
  const outside = [...new Set(documentBodyLinks(documents).map((l) => l.target).filter((t3) => !t3.startsWith(".gitifact/") || t3.startsWith(INSTRUCTIONS_ROOT + "/")))];
29316
29775
  const existing = /* @__PURE__ */ new Set();
29317
- for (const target of outside) if ((await info2(join17(root, ...target.split("/"))))?.isFile()) existing.add(target);
29776
+ for (const target of outside) if ((await info2(join18(root, ...target.split("/"))))?.isFile()) existing.add(target);
29318
29777
  return documentWarnings(documents, await listAssets(root), existing);
29319
29778
  }
29320
29779
 
@@ -29339,29 +29798,29 @@ var runCheck = (options) => runCommand("check", options.format, async () => {
29339
29798
  // src/commands/changes.ts
29340
29799
  import { randomUUID as randomUUID5 } from "node:crypto";
29341
29800
  import { mkdir as mkdir6, open as open5, readFile as readFile16, rename as rename3, rmdir, unlink as unlink4, writeFile as writeFile4 } from "node:fs/promises";
29342
- import { join as join20 } from "node:path";
29801
+ import { join as join21 } from "node:path";
29343
29802
 
29344
29803
  // src/adapters/filesystem/agent-inputs.ts
29345
29804
  import { createHash as createHash4, randomBytes as randomBytes2 } from "node:crypto";
29346
- import { lstat as lstat13, mkdir as mkdir5, readdir as readdir7, realpath as realpath3, unlink as unlink3, writeFile as writeFile3 } from "node:fs/promises";
29805
+ import { lstat as lstat14, mkdir as mkdir5, readdir as readdir7, realpath as realpath3, unlink as unlink3, writeFile as writeFile3 } from "node:fs/promises";
29347
29806
  import { tmpdir } from "node:os";
29348
- import { basename, dirname as dirname2, join as join18 } from "node:path";
29807
+ import { basename, dirname as dirname2, join as join19 } from "node:path";
29349
29808
  var maxAge = 7 * 24 * 60 * 60 * 1e3;
29350
- var info3 = (path3) => lstat13(path3).catch((e) => {
29809
+ var info3 = (path3) => lstat14(path3).catch((e) => {
29351
29810
  if (e.code === "ENOENT") return void 0;
29352
29811
  throw e;
29353
29812
  });
29354
29813
  var comparable = (path3) => process.platform === "win32" ? path3.toLowerCase() : path3;
29355
29814
  async function candidates(root, controls) {
29356
29815
  const key = createHash4("sha256").update(comparable(await realpath3(root))).digest("hex").slice(0, 16);
29357
- return { system: join18((controls.tmpdir ?? tmpdir)(), "gitifact", key), project: join18(root, ".gitifact", "tmp") };
29816
+ return { system: join19((controls.tmpdir ?? tmpdir)(), "gitifact", key), project: join19(root, ".gitifact", "tmp") };
29358
29817
  }
29359
29818
  async function writable(directory) {
29360
29819
  try {
29361
29820
  await mkdir5(directory, { recursive: true });
29362
29821
  const stat3 = await info3(directory);
29363
29822
  if (!stat3?.isDirectory() || stat3.isSymbolicLink()) return false;
29364
- const probe = join18(directory, ".probe-" + randomBytes2(8).toString("hex"));
29823
+ const probe = join19(directory, ".probe-" + randomBytes2(8).toString("hex"));
29365
29824
  await writeFile3(probe, "", { flag: "wx" });
29366
29825
  await unlink3(probe);
29367
29826
  return true;
@@ -29375,7 +29834,7 @@ async function prepareAgentInputs(root, controls = {}) {
29375
29834
  if (await writable(system)) directory = system;
29376
29835
  else if (await writable(project)) {
29377
29836
  directory = project;
29378
- await writeFile3(join18(project, ".gitignore"), "*\n", { flag: "wx" }).catch((e) => {
29837
+ await writeFile3(join19(project, ".gitignore"), "*\n", { flag: "wx" }).catch((e) => {
29379
29838
  if (e.code !== "EEXIST") throw e;
29380
29839
  });
29381
29840
  }
@@ -29383,10 +29842,10 @@ async function prepareAgentInputs(root, controls = {}) {
29383
29842
  const now = (controls.now ?? Date.now)();
29384
29843
  for (const name2 of await readdir7(directory).catch(() => [])) {
29385
29844
  if (name2 === ".gitignore") continue;
29386
- const stat3 = await info3(join18(directory, name2)).catch(() => void 0);
29387
- if (stat3?.isFile() && now - stat3.mtimeMs > maxAge) await unlink3(join18(directory, name2)).catch(() => void 0);
29845
+ const stat3 = await info3(join19(directory, name2)).catch(() => void 0);
29846
+ if (stat3?.isFile() && now - stat3.mtimeMs > maxAge) await unlink3(join19(directory, name2)).catch(() => void 0);
29388
29847
  }
29389
- return { commit: join18(directory, "commit.json") };
29848
+ return { commit: join19(directory, "commit.json") };
29390
29849
  }
29391
29850
  async function discardAgentInput(root, file2, controls = {}) {
29392
29851
  try {
@@ -29408,8 +29867,8 @@ async function discardAgentInput(root, file2, controls = {}) {
29408
29867
 
29409
29868
  // src/commands/commit-files.ts
29410
29869
  import { createHash as createHash5 } from "node:crypto";
29411
- import { lstat as lstat14, readFile as readFile14 } from "node:fs/promises";
29412
- import { join as join19 } from "node:path";
29870
+ import { lstat as lstat15, readFile as readFile14 } from "node:fs/promises";
29871
+ import { join as join20 } from "node:path";
29413
29872
  var fail3 = (message) => {
29414
29873
  throw new CommandError("INVALID_COMMIT", message);
29415
29874
  };
@@ -29418,7 +29877,7 @@ var optional2 = (path3) => readFile14(path3).catch((e) => {
29418
29877
  if (e.code === "ENOENT") return null;
29419
29878
  throw e;
29420
29879
  });
29421
- var info4 = (path3) => lstat14(path3).catch((e) => {
29880
+ var info4 = (path3) => lstat15(path3).catch((e) => {
29422
29881
  if (e.code === "ENOENT") return void 0;
29423
29882
  throw e;
29424
29883
  });
@@ -29444,11 +29903,11 @@ async function fingerprint(root, path3) {
29444
29903
  validPath2(path3);
29445
29904
  const parts = path3.split("/");
29446
29905
  for (let i = 1; i <= parts.length; i++) {
29447
- const target = join19(root, ...parts.slice(0, i));
29906
+ const target = join20(root, ...parts.slice(0, i));
29448
29907
  const stat3 = await info4(target);
29449
29908
  if (!stat3) return null;
29450
29909
  if (stat3.isSymbolicLink() || (i < parts.length ? !stat3.isDirectory() : !stat3.isFile())) fail3(t2("commitFiles.linkOrDirectory"));
29451
- if (i < parts.length && await info4(join19(target, ".git"))) fail3(t2("commitFiles.nestedRepository"));
29910
+ if (i < parts.length && await info4(join20(target, ".git"))) fail3(t2("commitFiles.nestedRepository"));
29452
29911
  if (i === parts.length) {
29453
29912
  if (stat3.nlink !== 1 || stat3.size > 16 * 1024 * 1024) fail3(t2("commitFiles.hardLinkOrSize"));
29454
29913
  return hash2(Buffer.concat([Buffer.from(String(stat3.mode & 73) + "\0"), await readFile14(target)]));
@@ -29598,7 +30057,7 @@ async function commitChanges(cwd, input2, dryRun) {
29598
30057
  ...stdin ? { input: stdin } : {}
29599
30058
  });
29600
30059
  const staged = async (index) => (await git(["diff", "--cached", "--ita-visible-in-index", "--name-only", "--no-renames", "-z"], index)).toString("utf8").split("\0").filter(Boolean).sort();
29601
- const busy = join20(gitDir, "gitifact-changes-commit.lock");
30060
+ const busy = join21(gitDir, "gitifact-changes-commit.lock");
29602
30061
  if (await info4(busy)) fail3(t2("commit.busy", { path: busy }));
29603
30062
  if ((await staged()).length) fail3(t2("commit.existingStaging"));
29604
30063
  const indexHash = hash2(await optional2(indexPath) ?? Buffer.alloc(0));
@@ -29637,7 +30096,7 @@ async function commitChanges(cwd, input2, dryRun) {
29637
30096
  if (!same2(base, await project.reader.baseline())) fail3(t2("commit.headChanged"));
29638
30097
  return { outcome: "dry-run", committed: false, ...summary, commit: void 0 };
29639
30098
  }
29640
- const temporary = join20(gitDir, "gitifact-commit-index-" + randomUUID5());
30099
+ const temporary = join21(gitDir, "gitifact-commit-index-" + randomUUID5());
29641
30100
  let owned = false;
29642
30101
  let uncertain = false;
29643
30102
  let commitStarted = false;
@@ -29656,7 +30115,7 @@ async function commitChanges(cwd, input2, dryRun) {
29656
30115
  if (hash2(original ?? Buffer.alloc(0)) !== indexHash || (await staged()).length) fail3(t2("commit.indexChanged"));
29657
30116
  const before = await hashes([...started.keys()]);
29658
30117
  for (const [path3, value] of started) if (before.get(path3) !== value) fail3(t2("commit.fileChangedRunning", { path: path3 }));
29659
- await writeFile4(join20(busy, "recovery.json"), JSON.stringify({ before: base, originalIndex: original?.toString("base64") ?? null, temporary, paths: selected2, message }));
30118
+ await writeFile4(join21(busy, "recovery.json"), JSON.stringify({ before: base, originalIndex: original?.toString("base64") ?? null, temporary, paths: selected2, message }));
29660
30119
  const locked = await hashes([.../* @__PURE__ */ new Set([...selected2, ...contextPaths])]);
29661
30120
  if (original) await writeFile4(temporary, original, { flag: "wx" });
29662
30121
  else await git(["read-tree", "--empty"], temporary);
@@ -29673,14 +30132,14 @@ async function commitChanges(cwd, input2, dryRun) {
29673
30132
  const size = Number(header[2]);
29674
30133
  if (end < 0 || header[1] !== "blob" || !Number.isSafeInteger(size) || output2[end + 1 + size] !== 10) fail3(t2("commit.stagedUnreadable", { path: path3 }));
29675
30134
  const blob = output2.subarray(end + 1, end + 1 + size);
29676
- const file2 = await readFile16(join20(root, path3));
30135
+ const file2 = await readFile16(join21(root, path3));
29677
30136
  if (!blob.equals(file2) && !blob.equals(Buffer.from(file2.toString("latin1").replace(/\r\n/g, "\n"), "latin1"))) fail3(t2("commit.filterChanged"));
29678
30137
  offset = end + size + 2;
29679
30138
  }
29680
30139
  }
29681
30140
  const again = await hashes([...locked.keys()]);
29682
30141
  for (const [path3, value] of locked) if (again.get(path3) !== value) fail3(t2("commit.fileChangedPreparing", { path: path3 }));
29683
- for (const name2 of integration) if (await info4(join20(gitDir, name2))) fail3(t2("commit.integrationStarted"));
30142
+ for (const name2 of integration) if (await info4(join21(gitDir, name2))) fail3(t2("commit.integrationStarted"));
29684
30143
  if (!same2(base, await project.reader.baseline())) fail3(t2("commit.headChanged"));
29685
30144
  const tree = (await git(["write-tree"], temporary)).toString("utf8").trim();
29686
30145
  commitStarted = true;
@@ -29720,7 +30179,7 @@ async function commitChanges(cwd, input2, dryRun) {
29720
30179
  await unlink4(temporary).catch(() => {
29721
30180
  });
29722
30181
  if (owned) {
29723
- await unlink4(join20(busy, "recovery.json")).catch(() => {
30182
+ await unlink4(join21(busy, "recovery.json")).catch(() => {
29724
30183
  });
29725
30184
  await rmdir(busy);
29726
30185
  }
@@ -29731,7 +30190,7 @@ async function commitChanges(cwd, input2, dryRun) {
29731
30190
 
29732
30191
  // src/commands/records.ts
29733
30192
  import { readFile as readFile17 } from "node:fs/promises";
29734
- import { join as join21 } from "node:path";
30193
+ import { join as join22 } from "node:path";
29735
30194
  var runRecordsNew = (options) => runCommand("records", options.format, async () => {
29736
30195
  if (/[\r\n]/.test(options.title)) throw new CommandError("INVALID_VALUE", t2("docs.singleLine", { field: "title" }));
29737
30196
  const docs = [...new Set(options.docs.flatMap((d) => d.split(",")).map((d) => d.trim()).filter(Boolean))];
@@ -29767,7 +30226,7 @@ var runRecordsShow = (ids, options) => runCommand("records", options.format, asy
29767
30226
  const shown = await Promise.all([...new Set(ids)].map(async (id) => {
29768
30227
  const path3 = await findRecord(project.root, id);
29769
30228
  if (!path3) return { id, missing: true };
29770
- const source2 = await readFile17(join21(project.root, ...path3.split("/")), "utf8");
30229
+ const source2 = await readFile17(join22(project.root, ...path3.split("/")), "utf8");
29771
30230
  const log = project.reader.decode(await project.reader.run(["log", "-1", "--diff-filter=A", "--format=%H%x00%aN%x00%aI", "--", path3])).trim();
29772
30231
  const [commit, author, date5] = log.split("\0");
29773
30232
  return { id, path: path3, text: source2, commit: commit ? { commit, author, date: date5 } : null };
@@ -29877,7 +30336,7 @@ async function readThrough(project, head, filter, id) {
29877
30336
  // src/commands/feedback.ts
29878
30337
  import { readFile as readFile18 } from "node:fs/promises";
29879
30338
  import { release as release3 } from "node:os";
29880
- import { dirname as dirname3, join as join22 } from "node:path";
30339
+ import { dirname as dirname3, join as join23 } from "node:path";
29881
30340
 
29882
30341
  // src/adapters/github/issue.ts
29883
30342
  import { spawn as spawn2 } from "node:child_process";
@@ -29944,7 +30403,7 @@ function parseFeedback(input2) {
29944
30403
  }
29945
30404
  async function schemaVersion(from) {
29946
30405
  for (let folder = from; ; folder = dirname3(folder)) {
29947
- const config2 = await readFile18(join22(folder, ".gitifact", "config.json"), "utf8").catch(() => void 0);
30406
+ const config2 = await readFile18(join23(folder, ".gitifact", "config.json"), "utf8").catch(() => void 0);
29948
30407
  if (config2 !== void 0) {
29949
30408
  try {
29950
30409
  const value = JSON.parse(config2).schemaVersion;
@@ -30170,11 +30629,11 @@ for (let i = 0; i < languageArgs.length; i++) {
30170
30629
  if (value === "ko" || value === "en") selectedLanguage = value;
30171
30630
  }
30172
30631
  configureLanguage(selectedLanguage ?? environmentLanguage(), selectedLanguage);
30173
- var program2 = new Command().name("gitifact").addOption(new Option("--lang <language>", t2("help.lang")).choices(["ko", "en"])).description(t2("help.program")).version("0.8.1").allowExcessArguments(false).addHelpText("after", "\n" + t2("help.notYet")).action(() => program2.outputHelp());
30632
+ var program2 = new Command().name("gitifact").addOption(new Option("--lang <language>", t2("help.lang")).choices(["ko", "en"])).description(t2("help.program")).version("0.8.2").allowExcessArguments(false).addHelpText("after", "\n" + t2("help.notYet")).action(() => program2.outputHelp());
30174
30633
  var format = () => new Option("--format <format>", t2("help.format")).choices(formats).default("text");
30175
- program2.command("browser").description(t2("help.browser")).allowExcessArguments(false).option("--port <port>", t2("help.browserPort"), parsePort, 0).option("--dev", t2("help.browserDev")).action((options) => runBrowser(options, "0.8.1"));
30176
- program2.command("init").description(t2("help.init")).allowExcessArguments(false).option("--dry-run", t2("help.initDryRun")).addOption(new Option("--agent <agent>", t2("help.initAgent")).choices([...agentPresetNames])).addOption(new Option("--remove-agents", t2("help.initRemoveAgents")).conflicts("skipAgents")).option("--skip-agents", t2("help.initSkipAgents")).addOption(new Option("--format <format>", t2("help.format")).choices(["json", "text"]).default("json")).action((options) => runInit(options, "0.8.1"));
30177
- program2.command("update").description(t2("help.update")).allowExcessArguments(false).addOption(new Option("--format <format>", t2("help.format")).choices(["json", "text"]).default("json")).option("--commit", t2("help.updateCommit")).addOption(new Option("--check", t2("help.updateCheck")).conflicts("commit")).action((options) => options.check ? runUpdateCheck(options.format, "0.8.1") : runUpdate(options, "0.8.1"));
30634
+ program2.command("browser").description(t2("help.browser")).allowExcessArguments(false).option("--port <port>", t2("help.browserPort"), parsePort, 0).option("--dev", t2("help.browserDev")).action((options) => runBrowser(options, "0.8.2"));
30635
+ program2.command("init").description(t2("help.init")).allowExcessArguments(false).option("--dry-run", t2("help.initDryRun")).addOption(new Option("--agent <agent>", t2("help.initAgent")).choices([...agentPresetNames])).addOption(new Option("--remove-agents", t2("help.initRemoveAgents")).conflicts("skipAgents")).option("--skip-agents", t2("help.initSkipAgents")).addOption(new Option("--format <format>", t2("help.format")).choices(["json", "text"]).default("json")).action((options) => runInit(options, "0.8.2"));
30636
+ program2.command("update").description(t2("help.update")).allowExcessArguments(false).addOption(new Option("--format <format>", t2("help.format")).choices(["json", "text"]).default("json")).option("--commit", t2("help.updateCommit")).addOption(new Option("--check", t2("help.updateCheck")).conflicts("commit")).action((options) => options.check ? runUpdateCheck(options.format, "0.8.2") : runUpdate(options, "0.8.2"));
30178
30637
  var listed2 = (command, sorts) => {
30179
30638
  command.allowExcessArguments(false).option("--q <words>", t2("help.listQ")).option("--author <name>", t2("help.listAuthor"));
30180
30639
  if (sorts) command.addOption(new Option("--sort <key>", t2("help.listSort")).choices([...sorts]).default(sorts[0]));
@@ -30196,7 +30655,7 @@ program2.command("check").description(t2("help.check")).allowExcessArguments(fal
30196
30655
  var changes = program2.command("changes").description(t2("help.changes"));
30197
30656
  changes.command("list").description(t2("help.changesList")).allowExcessArguments(false).addOption(format()).action((o) => runChangesList(o));
30198
30657
  changes.command("commit").description(t2("help.changesCommit")).allowExcessArguments(false).requiredOption("--file <path>", t2("help.changesCommitFile")).option("--dry-run", t2("help.changesCommitDryRun")).addOption(format()).action((o) => runChangesCommit(o));
30199
- program2.command("feedback").description(t2("help.feedback")).allowExcessArguments(false).requiredOption("--file <path>", t2("help.feedbackFile")).option("--dry-run", t2("help.feedbackDryRun")).addOption(format()).action((o) => runFeedback(o, "0.8.1"));
30658
+ program2.command("feedback").description(t2("help.feedback")).allowExcessArguments(false).requiredOption("--file <path>", t2("help.feedbackFile")).option("--dry-run", t2("help.feedbackDryRun")).addOption(format()).action((o) => runFeedback(o, "0.8.2"));
30200
30659
  var guide = program2.command("guide").description(t2("help.guide"));
30201
30660
  guide.command("list").description(t2("help.guideList")).allowExcessArguments(false).addOption(format()).action((o) => runGuideList(o));
30202
30661
  guide.command("show").description(t2("help.guideShow")).argument("<topic>", t2("help.guideTopic")).allowExcessArguments(false).addOption(format()).action((topic, o) => runGuideShow(topic, o));