gitifact 0.8.1 → 0.8.3

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 (129) hide show
  1. package/README.md +45 -37
  2. package/dist/THIRD_PARTY_NOTICES.txt +26 -0
  3. package/dist/browser/assets/{BottomSheet-7C5eiOji.js → BottomSheet-BFoM6syr.js} +1 -1
  4. package/dist/browser/assets/Grid-DCCm_m5g.js +1 -0
  5. package/dist/browser/assets/HgiRefresh-UIdBFt6N.js +1 -0
  6. package/dist/browser/assets/{HoverCard-D6keobP0.js → HoverCard-Bpt8LVFT.js} +1 -1
  7. package/dist/browser/assets/{Icon-EsJf4C7A.js → Icon-DPaLmsmh.js} +3 -3
  8. package/dist/browser/assets/{IconButton-CgOx1Jog.js → IconButton-Br-4XhQr.js} +1 -1
  9. package/dist/browser/assets/{Markdown-Droxhk-G.js → Markdown-CR_AHfxJ.js} +4 -4
  10. package/dist/browser/assets/{MenuBottomSheet-BOGxMa05.js → MenuBottomSheet-Dgz4h1S7.js} +1 -1
  11. package/dist/browser/assets/{MetadataListItem-C3SqHyk0.js → MetadataListItem-BEwPg27H.js} +1 -1
  12. package/dist/browser/assets/ProgressBarMarkTooltip-BxGSd9KS.js +1 -0
  13. package/dist/browser/assets/{Section-BEbhTpxd.js → Section-Z2PuhE6c.js} +1 -1
  14. package/dist/browser/assets/Selector-CPFOT7Wj.js +2 -0
  15. package/dist/browser/assets/Tab-DL11wx1W.js +1 -0
  16. package/dist/browser/assets/{Table-D4v8xCrw.js → Table-BK5793GP.js} +3 -3
  17. package/dist/browser/assets/{TimestampHoverCard-qfGbCkoP.js → TimestampHoverCard-B5JBaQDZ.js} +1 -1
  18. package/dist/browser/assets/Token-BnQGlhSv.js +2 -0
  19. package/dist/browser/assets/Token-Bqkc8eyB.css +1 -0
  20. package/dist/browser/assets/{Tooltip-QZg2pB5o.js → Tooltip-MoU3alsv.js} +1 -1
  21. package/dist/browser/assets/about-3BiGgi3V.js +3 -0
  22. package/dist/browser/assets/activity-timeline-CCrOFWbb.js +2 -0
  23. package/dist/browser/assets/{appearance-CDQ4fLdE.js → appearance-DyJA06C4.js} +1 -1
  24. package/dist/browser/assets/changelog-DMiOCVuF.js +1 -0
  25. package/dist/browser/assets/commit-CMw0y1Oj.js +4 -0
  26. package/dist/browser/assets/{container.stylex-6OsSOpKw.js → container.stylex-rwI38dw9.js} +1 -1
  27. package/dist/browser/assets/contributor-Cg_cxulE.js +1 -0
  28. package/dist/browser/assets/contributors-ol5bZm_T.js +1 -0
  29. package/dist/browser/assets/contributors._email-BoTYVUTP.js +1 -0
  30. package/dist/browser/assets/contributors.index-DrdKDOYt.js +1 -0
  31. package/dist/browser/assets/dashboard.index-BUzgcF91.js +1 -0
  32. package/dist/browser/assets/document-C2htw6u5.js +2 -0
  33. package/dist/browser/assets/document-Cmx0e6kz.css +1 -0
  34. package/dist/browser/assets/document-CteWuasx.js +19 -0
  35. package/dist/browser/assets/features-DUHxz0_L.css +1 -0
  36. package/dist/browser/assets/features-haTu80rD.js +2 -0
  37. package/dist/browser/assets/features._featureId-CHZA4Y9C.js +1 -0
  38. package/dist/browser/assets/features.index-BY8ScP1y.js +1 -0
  39. package/dist/browser/assets/getting-started-GU6t7LF-.js +1 -0
  40. package/dist/browser/assets/git-D2zVLj_w.js +1 -0
  41. package/dist/browser/assets/index-D1JeDnJc.css +1 -0
  42. package/dist/browser/assets/index-DaKOj2qL.js +49 -0
  43. package/dist/browser/assets/instructions-mU0nkIyN.js +1 -0
  44. package/dist/browser/assets/instructions._instructionId-D-pztPg5.js +1 -0
  45. package/dist/browser/assets/instructions.agents-WcmyZITb.js +1 -0
  46. package/dist/browser/assets/instructions.index-BEJLVyZD.js +1 -0
  47. package/dist/browser/assets/lazyRouteComponent-DcCLQOpn.js +1 -0
  48. package/dist/browser/assets/load-more-YVDkQmcp.js +1 -0
  49. package/dist/browser/assets/lottie_light-DSIvQuvH.js +2 -0
  50. package/dist/browser/assets/page-header-DTEyUWBS.css +1 -0
  51. package/dist/browser/assets/page-header-rHElc4Gv.js +246 -0
  52. package/dist/browser/assets/records._recordId-CQAy7eOv.js +1 -0
  53. package/dist/browser/assets/records.commits._commit-BP1KDLvt.js +1 -0
  54. package/dist/browser/assets/records.index-CZ6iNU57.js +1 -0
  55. package/dist/browser/assets/records.working-Bv9veUIm.js +1 -0
  56. package/dist/browser/assets/rocket-loader-CMSP4Jri.js +1 -0
  57. package/dist/browser/assets/search-CSSuO-jR.js +1 -0
  58. package/dist/browser/assets/settings-D2ZdL-Vu.js +1 -0
  59. package/dist/browser/assets/{themeProps-CfjLB1h4.js → themeProps-CkLe5zXY.js} +1 -1
  60. package/dist/browser/assets/{useClipboard-CTfBnLlg.js → useClipboard-Cqx6Upy7.js} +1 -1
  61. package/dist/browser/assets/useDevWarning-Dudfg3qF.js +1 -0
  62. package/dist/browser/assets/useInfiniteQuery-guh_cfSb.js +1 -0
  63. package/dist/browser/assets/{useIsomorphicLayoutEffect-DByj3svz.js → useIsomorphicLayoutEffect-BiEJxDg_.js} +1 -1
  64. package/dist/browser/assets/useKeyboardHint-BD_Jlpwp.js +1 -0
  65. package/dist/browser/assets/{useMediaQuery-CqJr5z44.js → useMediaQuery-twbHF0pO.js} +1 -1
  66. package/dist/browser/assets/{useScrollLock-CmZKJxOo.js → useScrollLock-D-Y-zHSV.js} +1 -1
  67. package/dist/browser/index.html +46 -25
  68. package/dist/browser/licenses/rocket-in-space-lottie.txt +29 -0
  69. package/dist/i18n/en/block.md +4 -6
  70. package/dist/i18n/en/changelog.md +27 -0
  71. package/dist/i18n/en/docs/commit.md +1 -1
  72. package/dist/i18n/en/docs/workflow.md +7 -7
  73. package/dist/i18n/ko/block.md +4 -6
  74. package/dist/i18n/ko/changelog.md +27 -0
  75. package/dist/i18n/ko/docs/commit.md +1 -1
  76. package/dist/i18n/ko/docs/workflow.md +5 -5
  77. package/dist/main.js +1279 -559
  78. package/package.json +1 -1
  79. package/dist/browser/assets/Banner-c1hFCLs8.js +0 -1
  80. package/dist/browser/assets/Grid-BrUBBmhu.js +0 -1
  81. package/dist/browser/assets/ProgressBarMarkTooltip-C5Moep_o.js +0 -1
  82. package/dist/browser/assets/Selector-D_xYW7R_.js +0 -2
  83. package/dist/browser/assets/Tab-VVHagF2Z.js +0 -1
  84. package/dist/browser/assets/Token-BLd8-jfF.js +0 -1
  85. package/dist/browser/assets/about-DeUi-_2z.js +0 -3
  86. package/dist/browser/assets/activity-timeline-bFoqFwVu.js +0 -2
  87. package/dist/browser/assets/changelog-Brd1Ninm.js +0 -2
  88. package/dist/browser/assets/changelog-CxmMo2wk.css +0 -1
  89. package/dist/browser/assets/commit-CiMtFlZl.js +0 -4
  90. package/dist/browser/assets/contributor-C_IskSHq.js +0 -1
  91. package/dist/browser/assets/contributors-CbwXLc7M.js +0 -1
  92. package/dist/browser/assets/contributors._email-BRfre3dw.js +0 -1
  93. package/dist/browser/assets/contributors.index-CV84owW7.js +0 -1
  94. package/dist/browser/assets/dashboard-CMSj2wu3.css +0 -1
  95. package/dist/browser/assets/dashboard.index-CQdr7hsf.js +0 -1
  96. package/dist/browser/assets/document-BZmiLx-e.js +0 -2
  97. package/dist/browser/assets/document-DEGaf2yT.css +0 -1
  98. package/dist/browser/assets/document-JLY2-z8S.js +0 -19
  99. package/dist/browser/assets/features-Cj41kcXf.js +0 -4
  100. package/dist/browser/assets/features-g3j2elTj.css +0 -1
  101. package/dist/browser/assets/features._featureId-BgFHsRcy.js +0 -1
  102. package/dist/browser/assets/features.index-Ca0rzFW6.js +0 -1
  103. package/dist/browser/assets/getting-started-CgL3Vi9o.js +0 -1
  104. package/dist/browser/assets/git-B7oxggGB.js +0 -1
  105. package/dist/browser/assets/git-D_wcK2vC.css +0 -1
  106. package/dist/browser/assets/index-CmU7dwFQ.css +0 -1
  107. package/dist/browser/assets/index-DWRGm9YO.js +0 -48
  108. package/dist/browser/assets/instructions-D3cdAPD3.js +0 -1
  109. package/dist/browser/assets/instructions._instructionId-BkT14646.js +0 -1
  110. package/dist/browser/assets/instructions.agents-B3jLRl1u.js +0 -1
  111. package/dist/browser/assets/instructions.index-8qlhSXoy.js +0 -1
  112. package/dist/browser/assets/lazyRouteComponent-JkRa2CHo.js +0 -1
  113. package/dist/browser/assets/page-header-CPL7myjo.js +0 -1
  114. package/dist/browser/assets/page-header-atX8Nsmd.css +0 -1
  115. package/dist/browser/assets/records-Dk3shbYl.css +0 -1
  116. package/dist/browser/assets/records-page-B4Al3fIQ.js +0 -1
  117. package/dist/browser/assets/records-page-D2F1WJrh.css +0 -1
  118. package/dist/browser/assets/records._recordId-Chn1SO5R.js +0 -1
  119. package/dist/browser/assets/records.commits._commit-D4Wk4i_m.js +0 -1
  120. package/dist/browser/assets/records.index-CViTeD08.js +0 -1
  121. package/dist/browser/assets/records.working-BsHKMIz-.js +0 -1
  122. package/dist/browser/assets/request-state-oiWyP9c-.js +0 -1
  123. package/dist/browser/assets/search-BCYi0CBz.js +0 -1
  124. package/dist/browser/assets/search-palette-D0ctICJy.js +0 -561
  125. package/dist/browser/assets/search-palette-DpmGOAIg.css +0 -1
  126. package/dist/browser/assets/settings-BDVKHrS8.js +0 -1
  127. package/dist/browser/assets/useDevWarning-gW8_bNZ_.js +0 -1
  128. package/dist/browser/assets/useInfiniteQuery-Bn_R13Ih.js +0 -1
  129. package/dist/browser/assets/useKeyboardHint-lSxUw5Qj.js +0 -1
package/dist/main.js CHANGED
@@ -3407,6 +3407,8 @@ var messages_default = {
3407
3407
  "config.invalidFields": "\uD504\uB85C\uC81D\uD2B8 \uC124\uC815\uC758 \uD544\uB4DC\uB098 \uAC12\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
3408
3408
  "config.jsonUnreadableShort": "\uC124\uC815 JSON\uC744 \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
3409
3409
  "config.schemaFields": "schemaVersion\uACFC baseline \uC124\uC815 \uD544\uB4DC\uB97C \uD655\uC778\uD558\uC138\uC694.",
3410
+ "config.cliVersion": "\uC124\uC815\uC758 cli\uB294 0.8.3 \uAC19\uC740 \uBC30\uD3EC \uBC84\uC804\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4.",
3411
+ "config.language": "\uC124\uC815\uC758 language\uB294 {languages} \uC911 \uD558\uB098\uC5EC\uC57C \uD569\uB2C8\uB2E4.",
3410
3412
  "document.pathTooLong": "\uC704\uD0A4 \uD398\uC774\uC9C0 \uACBD\uB85C \uAE4A\uC774 \uB610\uB294 \uAE38\uC774 \uD55C\uB3C4\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4: {path}",
3411
3413
  "document.fileName": "\uC704\uD0A4 \uD398\uC774\uC9C0 \uD30C\uC77C \uC774\uB984\uC740 \uC18C\uBB38\uC790\xB7\uC22B\uC790\xB7\uD558\uC774\uD508\uACFC .md\uC5EC\uC57C \uD569\uB2C8\uB2E4(\uB8E8\uD2B8\uC758 README.md\xB7ARCHITECTURE.md \uAC19\uC740 \uB300\uBB38\uC790 \uC774\uB984 \uC608\uC678): {path}",
3412
3414
  "document.folderName": "\uC704\uD0A4 \uD3F4\uB354 \uC774\uB984\uC740 \uC18C\uBB38\uC790\xB7\uC22B\uC790\xB7\uD558\uC774\uD508\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4: {path}",
@@ -3524,6 +3526,8 @@ var messages_default2 = {
3524
3526
  "config.invalidFields": "Invalid project configuration fields or values.",
3525
3527
  "config.jsonUnreadableShort": "Could not read configuration JSON.",
3526
3528
  "config.schemaFields": "Check the schemaVersion and baseline fields.",
3529
+ "config.cliVersion": "The config field cli must be a release version such as 0.8.3.",
3530
+ "config.language": "The config field language must be one of {languages}.",
3527
3531
  "document.pathTooLong": "Wiki page path exceeds the depth or length limit: {path}",
3528
3532
  "document.fileName": "Wiki filenames must use lowercase letters, digits, hyphens and .md; uppercase root names such as README.md and ARCHITECTURE.md are allowed: {path}",
3529
3533
  "document.folderName": "Wiki folder names must use lowercase letters, digits and hyphens: {path}",
@@ -3682,6 +3686,8 @@ var InitError = class extends Error {
3682
3686
  };
3683
3687
  var SCHEMA_VERSION = 3;
3684
3688
  var PREVIOUS_SCHEMA_VERSION = 2;
3689
+ var PROJECT_LANGUAGES = ["ko", "en"];
3690
+ var CLI_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
3685
3691
  var object = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3686
3692
  var keys = (value, expected) => Object.keys(value).sort().join(",") === expected.sort().join(",");
3687
3693
  function parseBaseline(value) {
@@ -3714,7 +3720,7 @@ function parseManagedConfig(text4) {
3714
3720
  }
3715
3721
  if (!object(value) || !("schemaVersion" in value))
3716
3722
  throw new InitError("UNSUPPORTED_FORMAT", t("config.unsupportedFormat"));
3717
- if (!keys(value, ["baseline", "schemaVersion"]))
3723
+ if (!("baseline" in value))
3718
3724
  throw new InitError("INVALID_CONFIG", t("config.schemaFields"));
3719
3725
  if (value.schemaVersion !== SCHEMA_VERSION) {
3720
3726
  const found = value.schemaVersion;
@@ -3723,7 +3729,35 @@ function parseManagedConfig(text4) {
3723
3729
  throw new InitError("UNSUPPORTED_SCHEMA", t("config.migrationRequired"));
3724
3730
  throw new InitError("UNSUPPORTED_SCHEMA", newer ? t("config.newerSchema", { version: String(found) }) : t("config.unsupportedSchema", { version: String(found) }));
3725
3731
  }
3726
- return { schemaVersion: SCHEMA_VERSION, baseline: parseBaseline(value.baseline) };
3732
+ const config2 = { schemaVersion: SCHEMA_VERSION, baseline: parseBaseline(value.baseline) };
3733
+ if ("cli" in value) {
3734
+ if (typeof value.cli !== "string" || !CLI_VERSION_PATTERN.test(value.cli))
3735
+ throw new InitError("INVALID_CONFIG", t("config.cliVersion"));
3736
+ config2.cli = value.cli;
3737
+ }
3738
+ if ("language" in value) {
3739
+ if (!PROJECT_LANGUAGES.includes(value.language))
3740
+ throw new InitError("INVALID_CONFIG", t("config.language", { languages: PROJECT_LANGUAGES.join(", ") }));
3741
+ config2.language = value.language;
3742
+ }
3743
+ return config2;
3744
+ }
3745
+ function formatManagedConfig(config2, previous) {
3746
+ let unknown2 = {};
3747
+ if (previous !== void 0) {
3748
+ const value = JSON.parse(previous);
3749
+ if (object(value)) {
3750
+ const { schemaVersion: _s, baseline: _b, cli: _c, language: _l, ...rest } = value;
3751
+ unknown2 = rest;
3752
+ }
3753
+ }
3754
+ const known = {
3755
+ schemaVersion: config2.schemaVersion,
3756
+ baseline: config2.baseline,
3757
+ ...config2.cli === void 0 ? {} : { cli: config2.cli },
3758
+ ...config2.language === void 0 ? {} : { language: config2.language }
3759
+ };
3760
+ return JSON.stringify({ ...known, ...unknown2 }, null, 2) + "\n";
3727
3761
  }
3728
3762
 
3729
3763
  // ../../packages/core/dist/formats/links.js
@@ -10413,15 +10447,15 @@ function bucketFor(state2, inst) {
10413
10447
  var handoff;
10414
10448
  var open = [];
10415
10449
  var memo = {
10416
- alloc(_inst, payload, empty) {
10450
+ alloc(_inst, payload, empty2) {
10417
10451
  const bucket = handoff;
10418
10452
  if (!bucket)
10419
- return empty;
10453
+ return empty2;
10420
10454
  handoff = void 0;
10421
- const entry2 = { value: empty, issues: null };
10455
+ const entry2 = { value: empty2, issues: null };
10422
10456
  bucket.set(payload.value, entry2);
10423
10457
  open.push(entry2);
10424
- return empty;
10458
+ return empty2;
10425
10459
  },
10426
10460
  guard(inst) {
10427
10461
  var _a3;
@@ -13092,8 +13126,8 @@ var error20 = () => {
13092
13126
  case "invalid_union":
13093
13127
  return "\u05E7\u05DC\u05D8 \u05DC\u05D0 \u05EA\u05E7\u05D9\u05DF";
13094
13128
  case "invalid_element": {
13095
- const place2 = withDefinite(issue2.origin ?? "array");
13096
- return `\u05E2\u05E8\u05DA \u05DC\u05D0 \u05EA\u05E7\u05D9\u05DF \u05D1${place2}`;
13129
+ const place3 = withDefinite(issue2.origin ?? "array");
13130
+ return `\u05E2\u05E8\u05DA \u05DC\u05D0 \u05EA\u05E7\u05D9\u05DF \u05D1${place3}`;
13097
13131
  }
13098
13132
  default:
13099
13133
  return `\u05E7\u05DC\u05D8 \u05DC\u05D0 \u05EA\u05E7\u05D9\u05DF`;
@@ -25064,20 +25098,73 @@ var instruction = external_exports.strictObject({
25064
25098
  var agents = external_exports.strictObject({ path: external_exports.string(), body: external_exports.string(), updatedAt: external_exports.string().nullable() });
25065
25099
  var problem = external_exports.strictObject({ code: external_exports.string(), path: external_exports.string(), message: external_exports.string() });
25066
25100
  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),
25101
+ var indexFeature = external_exports.strictObject({
25102
+ id: external_exports.string(),
25103
+ path: external_exports.string(),
25104
+ title: external_exports.string(),
25105
+ state,
25106
+ requirements: external_exports.array(external_exports.strictObject({ id: external_exports.string(), path: external_exports.string(), title: external_exports.string(), description: external_exports.string() })),
25107
+ designs: external_exports.array(external_exports.strictObject({ id: external_exports.string(), path: external_exports.string(), title: external_exports.string() }))
25108
+ });
25109
+ var indexInstruction = external_exports.strictObject({ id: external_exports.string(), name: external_exports.string(), path: external_exports.string(), title: external_exports.string() });
25110
+ var browserCheckoutV1 = external_exports.strictObject({
25111
+ contract: external_exports.literal("browser-checkout"),
25112
+ version: external_exports.literal(1),
25070
25113
  sessionId: external_exports.string(),
25071
25114
  head: oid2.nullable(),
25072
25115
  observedAt: external_exports.string(),
25073
25116
  working: external_exports.boolean(),
25074
25117
  stamp: external_exports.string(),
25075
- features: external_exports.array(feature),
25076
- instructions: external_exports.array(instruction),
25077
- agents: agents.nullable(),
25078
25118
  problems: external_exports.array(problem),
25119
+ 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() })) })
25120
+ });
25121
+ var featureRow = external_exports.strictObject({
25122
+ id: external_exports.string(),
25123
+ path: external_exports.string(),
25124
+ title: external_exports.string(),
25125
+ description: external_exports.string(),
25126
+ state,
25127
+ designs: external_exports.number().int().nonnegative(),
25079
25128
  contributors: external_exports.array(contributor),
25080
- contributorsLimited: external_exports.boolean()
25129
+ updatedAt: external_exports.string().nullable(),
25130
+ requirementCount: external_exports.number().int().nonnegative(),
25131
+ // `acceptance`: the numbered items under the acceptance heading, null without one. `designed`: a design names it.
25132
+ 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() })),
25133
+ hidden: external_exports.number().int().nonnegative()
25134
+ });
25135
+ var browserFeaturesV1 = external_exports.strictObject({
25136
+ contract: external_exports.literal("browser-features"),
25137
+ version: external_exports.literal(1),
25138
+ sessionId: external_exports.string(),
25139
+ total: external_exports.number().int().nonnegative(),
25140
+ all: external_exports.number().int().nonnegative(),
25141
+ requirements: external_exports.number().int().nonnegative(),
25142
+ mostRequirements: external_exports.number().int().nonnegative(),
25143
+ next: external_exports.string().nullable(),
25144
+ features: external_exports.array(featureRow)
25145
+ });
25146
+ var browserFeatureV1 = external_exports.strictObject({ contract: external_exports.literal("browser-feature"), version: external_exports.literal(1), sessionId: external_exports.string(), feature });
25147
+ var browserInstructionsV1 = external_exports.strictObject({
25148
+ contract: external_exports.literal("browser-instructions"),
25149
+ version: external_exports.literal(1),
25150
+ sessionId: external_exports.string(),
25151
+ instructions: external_exports.array(instruction),
25152
+ agents: agents.nullable()
25153
+ });
25154
+ var browserContributorsV1 = external_exports.strictObject({
25155
+ contract: external_exports.literal("browser-contributors"),
25156
+ version: external_exports.literal(1),
25157
+ sessionId: external_exports.string(),
25158
+ total: external_exports.number().int().nonnegative(),
25159
+ next: external_exports.string().nullable(),
25160
+ 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() }))
25161
+ });
25162
+ var browserContributorV1 = external_exports.strictObject({
25163
+ contract: external_exports.literal("browser-contributor"),
25164
+ version: external_exports.literal(1),
25165
+ sessionId: external_exports.string(),
25166
+ person: contributor,
25167
+ 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
25168
  });
25082
25169
  var kind = external_exports.enum(["feature", "requirement", "design", "wiki", "instruction"]);
25083
25170
  var reference = external_exports.strictObject({ id: external_exports.string(), title: external_exports.string(), specId: external_exports.string(), path: external_exports.string() }).nullable();
@@ -25122,11 +25209,13 @@ var browserHistoryV6 = external_exports.strictObject({
25122
25209
  next: oid2.nullable(),
25123
25210
  events: external_exports.array(event)
25124
25211
  });
25125
- var browserHistorySummaryV4 = external_exports.strictObject({
25212
+ var browserHistorySummaryV5 = external_exports.strictObject({
25126
25213
  contract: external_exports.literal("browser-history-summary"),
25127
- version: external_exports.literal(4),
25214
+ version: external_exports.literal(5),
25128
25215
  sessionId: external_exports.string(),
25129
25216
  head: oid2,
25217
+ // The three who committed most, and how many others there are and how many commits they made between them.
25218
+ 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
25219
  total: external_exports.number().int().nonnegative(),
25131
25220
  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
25221
  // One entry per commit within three weeks of the newest change, so the reader can count by its own calendar day.
@@ -25134,22 +25223,23 @@ var browserHistorySummaryV4 = external_exports.strictObject({
25134
25223
  // The newest commits: a few of their changes each and how many there are in all.
25135
25224
  recent: external_exports.array(external_exports.strictObject({ commit: oid2, count: external_exports.number().int().positive(), events: external_exports.array(event) }))
25136
25225
  });
25137
- var browserSearchV2 = external_exports.strictObject({
25226
+ var searchKind = external_exports.enum(["feature", "requirement", "design", "instruction", "record", "commit"]);
25227
+ var searchHit = external_exports.strictObject({
25228
+ id: external_exports.string(),
25229
+ kind: searchKind,
25230
+ title: external_exports.string(),
25231
+ where: external_exports.string(),
25232
+ line: external_exports.string(),
25233
+ // What the hit opens: a feature (with the requirement or design tab), an instruction, a record, or a commit.
25234
+ featureId: external_exports.string().optional(),
25235
+ commit: oid2.optional()
25236
+ });
25237
+ var browserSearchV3 = external_exports.strictObject({
25138
25238
  contract: external_exports.literal("browser-search"),
25139
- version: external_exports.literal(2),
25239
+ version: external_exports.literal(3),
25140
25240
  sessionId: external_exports.string(),
25141
25241
  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
- }))
25242
+ 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
25243
  });
25154
25244
  var headQuery = oid2;
25155
25245
  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 +25254,26 @@ var browserHistoryQueryV4 = external_exports.strictObject({
25164
25254
  q: external_exports.string().max(200).optional()
25165
25255
  });
25166
25256
  var browserHistorySummaryQueryV1 = external_exports.strictObject({ head: headQuery });
25167
- var browserSearchQueryV1 = external_exports.strictObject({ q: external_exports.string().min(1).max(200), head: headQuery.optional() });
25257
+ var featureId = external_exports.string().regex(/^S-[a-z2-7]{10}$/);
25258
+ var browserFeaturesQueryV1 = external_exports.strictObject({
25259
+ q: external_exports.string().max(200).optional(),
25260
+ design: external_exports.enum(["yes", "no"]).optional(),
25261
+ author: external_exports.string().min(1).max(320).optional(),
25262
+ sort: external_exports.enum(["title", "requirements", "updated"]).optional(),
25263
+ dir: external_exports.enum(["asc", "desc"]).optional(),
25264
+ after: featureId.optional(),
25265
+ limit: count(50).pipe(external_exports.number().min(1)).optional()
25266
+ });
25267
+ var browserFeatureQueryV1 = external_exports.strictObject({ id: featureId });
25268
+ 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() });
25269
+ var browserContributorQueryV1 = external_exports.strictObject({ email: external_exports.string().min(1).max(320) });
25270
+ var browserSearchQueryV2 = external_exports.strictObject({
25271
+ q: external_exports.string().max(200),
25272
+ head: headQuery.optional(),
25273
+ group: searchKind.optional(),
25274
+ after: external_exports.string().min(1).max(200).optional(),
25275
+ limit: count(50).pipe(external_exports.number().min(1)).optional()
25276
+ });
25168
25277
  var browserInstructionFileQueryV1 = external_exports.strictObject({ id: external_exports.string().regex(/^I-[a-z2-7]{10}$/), path: external_exports.string().min(1).max(1e3) });
25169
25278
  var changelogQueryV1 = external_exports.strictObject({ lang: external_exports.string().regex(/^[a-z]{2}(?:-[A-Z]{2})?$/).optional() });
25170
25279
  var fileStatus = external_exports.enum(["added", "modified", "deleted", "renamed"]);
@@ -25176,12 +25285,13 @@ var commitFile = external_exports.strictObject({
25176
25285
  additions: external_exports.number().int().nonnegative().nullable(),
25177
25286
  deletions: external_exports.number().int().nonnegative().nullable()
25178
25287
  });
25179
- var browserCommitFilesV1 = external_exports.strictObject({
25288
+ var browserCommitFilesV2 = external_exports.strictObject({
25180
25289
  contract: external_exports.literal("browser-commit-files"),
25181
- version: external_exports.literal(1),
25290
+ version: external_exports.literal(2),
25182
25291
  sessionId: external_exports.string(),
25183
25292
  commit: oid2,
25184
25293
  total: external_exports.number().int().nonnegative(),
25294
+ next: external_exports.string().nullable(),
25185
25295
  files: external_exports.array(commitFile)
25186
25296
  });
25187
25297
  var browserCommitFileV1 = external_exports.strictObject({
@@ -25196,7 +25306,7 @@ var browserCommitFileV1 = external_exports.strictObject({
25196
25306
  binary: external_exports.boolean(),
25197
25307
  tooLarge: external_exports.boolean()
25198
25308
  });
25199
- var browserCommitFilesQueryV1 = external_exports.strictObject({ commit: oid2 });
25309
+ 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
25310
  var browserCommitFileQueryV1 = external_exports.strictObject({ commit: oid2, path: external_exports.string().min(1).max(4096) });
25201
25311
  var browserCommitV4 = external_exports.strictObject({
25202
25312
  contract: external_exports.literal("browser-commit"),
@@ -25212,7 +25322,8 @@ var browserCommitV4 = external_exports.strictObject({
25212
25322
  next: external_exports.string().nullable(),
25213
25323
  changes: external_exports.array(event)
25214
25324
  });
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() });
25325
+ var recordId = external_exports.string().regex(/^(?:DR|H)-[A-Za-z0-9_-]{1,64}$/);
25326
+ 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
25327
  var browserCommitChangeV1 = external_exports.strictObject({
25217
25328
  contract: external_exports.literal("browser-commit-change"),
25218
25329
  version: external_exports.literal(1),
@@ -25223,14 +25334,15 @@ var browserCommitChangeV1 = external_exports.strictObject({
25223
25334
  after: snapshot
25224
25335
  });
25225
25336
  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({
25337
+ var browserRecordV2 = external_exports.strictObject({
25228
25338
  contract: external_exports.literal("browser-record"),
25229
- version: external_exports.literal(1),
25339
+ version: external_exports.literal(2),
25230
25340
  sessionId: external_exports.string(),
25231
25341
  head: oid2,
25232
25342
  id: recordId,
25233
- commit: oid2
25343
+ commit: oid2,
25344
+ record: record2,
25345
+ others: external_exports.number().int().nonnegative()
25234
25346
  });
25235
25347
  var browserRecordQueryV1 = external_exports.strictObject({ head: oid2, id: recordId });
25236
25348
  var browserInstructionFileV1 = external_exports.strictObject({
@@ -25322,9 +25434,9 @@ var projectInitV8 = external_exports.discriminatedUnion("ok", [
25322
25434
  external_exports.strictObject({ contract: external_exports.literal("project-init"), version: external_exports.literal(8), ok: external_exports.literal(false), error: external_exports.strictObject({ code: external_exports.string(), message: external_exports.string() }) })
25323
25435
  ]);
25324
25436
 
25325
- // ../../packages/contracts/dist/versions/update-v5.js
25437
+ // ../../packages/contracts/dist/versions/update-v6.js
25326
25438
  var commitReason = external_exports.enum(["untracked", "other-changes", "commit-failed"]);
25327
- var agentDocsCommit = external_exports.strictObject({
25439
+ var updateCommit = external_exports.strictObject({
25328
25440
  state: external_exports.enum(["not-requested", "nothing", "committed", "skipped"]),
25329
25441
  commit: external_exports.string().nullable(),
25330
25442
  paths: external_exports.array(external_exports.string()),
@@ -25332,27 +25444,40 @@ var agentDocsCommit = external_exports.strictObject({
25332
25444
  reason: commitReason.nullable(),
25333
25445
  detail: external_exports.string().nullable()
25334
25446
  }).refine((value) => value.state === "committed" === (value.commit !== null) && value.state === "skipped" === (value.reason !== null), "commit is set exactly when committed, reason exactly when skipped.");
25335
- var updateV5 = external_exports.discriminatedUnion("ok", [
25447
+ var updateV6 = external_exports.discriminatedUnion("ok", [
25336
25448
  external_exports.strictObject({
25337
25449
  contract: external_exports.literal("update"),
25338
- version: external_exports.literal(5),
25450
+ version: external_exports.literal(6),
25339
25451
  ok: external_exports.literal(true),
25340
25452
  cliVersion: external_exports.string().min(1),
25341
25453
  update: updateStateV1,
25342
- // Present only when a newer release exists. npx refreshes the project blocks without a global install; npmGlobal remains an alternative.
25343
- install: external_exports.strictObject({ npx: external_exports.string().min(1), npmGlobal: external_exports.string().min(1) }).nullable(),
25344
- // refreshed: blocks whose version line differed were rewritten. current: every block already matched.
25454
+ // Present only when a newer release exists; after installing it, `gitifact update` sets the project to it.
25455
+ install: external_exports.strictObject({ npmGlobal: external_exports.string().min(1) }).nullable(),
25456
+ // refreshed: blocks whose text differed were rewritten. current: every block already matched. kept: the project is on a
25457
+ // release newer than this CLI, so its blocks were left as they are.
25345
25458
  // not-initialized: no .gitifact config here, so only the version check ran. no-block: no file carries a block.
25346
25459
  // missing: files `gitifact init` would add and update does not create, e.g. CLAUDE.md when only AGENTS.md has the block.
25347
- agentDocs: external_exports.strictObject({ state: external_exports.enum(["refreshed", "current", "not-initialized", "no-block"]), paths: external_exports.array(external_exports.string()), missing: external_exports.array(external_exports.string()) }),
25348
- commit: agentDocsCommit,
25460
+ agentDocs: external_exports.strictObject({ state: external_exports.enum(["refreshed", "current", "kept", "not-initialized", "no-block"]), paths: external_exports.array(external_exports.string()), missing: external_exports.array(external_exports.string()) }),
25461
+ // written: .gitifact/config.json now names this release as `cli` and the block language. current: it already did.
25462
+ // cli is the project's release afterwards; one newer than cliVersion was kept, since an older CLI does not lower it.
25463
+ project: external_exports.strictObject({ state: external_exports.enum(["written", "current", "not-initialized"]), cli: external_exports.string().min(1).nullable() }),
25464
+ commit: updateCommit,
25349
25465
  // The project still uses the 0.7 storage convention (schemaVersion 2): the blocks now describe commands that will refuse it
25350
25466
  // until the documents are moved with `gitifact guide show migrate`.
25351
25467
  migrationRequired: external_exports.boolean()
25352
25468
  }),
25353
- external_exports.strictObject({ contract: external_exports.literal("update"), version: external_exports.literal(5), ok: external_exports.literal(false), error: external_exports.strictObject({ code: external_exports.string(), message: external_exports.string() }) })
25469
+ external_exports.strictObject({ contract: external_exports.literal("update"), version: external_exports.literal(6), ok: external_exports.literal(false), error: external_exports.strictObject({ code: external_exports.string(), message: external_exports.string() }) })
25354
25470
  ]);
25355
25471
 
25472
+ // ../../packages/contracts/dist/versions/update-later-v1.js
25473
+ var updateLaterV1 = external_exports.strictObject({
25474
+ contract: external_exports.literal("update-later"),
25475
+ version: external_exports.literal(1),
25476
+ ok: external_exports.literal(true),
25477
+ cliVersion: external_exports.string().min(1),
25478
+ later: external_exports.strictObject({ version: external_exports.string().min(1), until: external_exports.iso.datetime() }).nullable()
25479
+ });
25480
+
25356
25481
  // ../../packages/contracts/dist/versions/browser-session-v3.js
25357
25482
  var browserSessionV3 = external_exports.strictObject({
25358
25483
  contract: external_exports.literal("browser-session"),
@@ -25505,6 +25630,8 @@ var messages_default3 = {
25505
25630
  "server.sessionChanged": "\uC11C\uBC84 \uC138\uC158\uC774 \uBCC0\uACBD\uB410\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC5F0\uACB0\uD558\uC138\uC694.",
25506
25631
  "server.badRange": "\uC870\uD68C \uBC94\uC704\uB97C \uD655\uC778\uD558\uC138\uC694.",
25507
25632
  "server.commitNotFound": "\uC774 \uC800\uC7A5\uC18C\uC5D0 \uADF8 \uCEE4\uBC0B\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
25633
+ "server.featureNotFound": "\uCCB4\uD06C\uC544\uC6C3\uC5D0 \uADF8 \uAE30\uB2A5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
25634
+ "server.contributorNotFound": "HEAD\uC5D0 \uADF8 \uC774\uBA54\uC77C\uB85C \uCEE4\uBC0B\uD55C \uC0AC\uB78C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
25508
25635
  "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
25636
  "server.workingChangeNotFound": "\uCEE4\uBC0B \uC804 \uBCC0\uACBD\uC5D0 \uADF8 \uBB38\uC11C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
25510
25637
  "server.recordNotFound": "\uC774 \uC774\uB825\uC5D0 \uADF8 \uACB0\uC815\uAE30\uB85D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
@@ -25551,7 +25678,7 @@ var messages_default3 = {
25551
25678
  "init.text.storage": "\uC800\uC7A5 \uADDC\uC57D",
25552
25679
  "init.text.agentDocs": "\uC5D0\uC774\uC804\uD2B8 \uC9C0\uCE68",
25553
25680
  "init.text.lineEndings": "\uC904\uBC14\uAFC8: {path}\uB85C \uBB38\uC11C\uB97C LF\uB85C \uACE0\uC815\uD588\uC2B5\uB2C8\uB2E4. \uC774\uBBF8 \uC788\uB358 \uBB38\uC11C\uAC00 \uBCC0\uACBD\uC73C\uB85C \uBCF4\uC774\uBA74 \uC904\uBC14\uAFC8\uB9CC \uBC14\uB010 \uAC83\uC774\uB2C8 \uADF8 \uBB38\uC11C\uB4E4\uC744 \uD55C \uBC88 \uCEE4\uBC0B\uD574 \uC815\uB9AC\uD558\uC138\uC694.",
25554
- "init.text.updateAvailable": "\uC0C8 \uBC84\uC804 {version}\uC774 \uC788\uC2B5\uB2C8\uB2E4. {command}\uB85C \uC0C8 \uBC84\uC804\uC744 \uC2E4\uD589\uD558\uACE0 \uD504\uB85C\uC81D\uD2B8 \uC9C0\uCE68\uC744 \uAC31\uC2E0\uD558\uC138\uC694.",
25681
+ "init.text.updateAvailable": "\uC0C8 \uBC84\uC804 {version}\uC774 \uC788\uC2B5\uB2C8\uB2E4. {command}\uB85C \uC124\uCE58\uD55C \uB4A4 gitifact update\uB85C \uD504\uB85C\uC81D\uD2B8\uB97C \uC0C8 \uBC84\uC804\uC5D0 \uB9DE\uCD94\uC138\uC694.",
25555
25682
  "init.failed": "\uCD08\uAE30\uD654\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uD30C\uC77C \uC811\uADFC \uAD8C\uD55C\uACFC \uC0DD\uC131\uB41C \uC124\uC815\xB7\uC784\uC2DC \uD30C\uC77C\uC744 \uD655\uC778\uD558\uC138\uC694.",
25556
25683
  "config.storeNotDirectory": ".gitifact\uB294 \uB9C1\uD06C\uAC00 \uC544\uB2CC \uC77C\uBC18 \uB514\uB809\uD130\uB9AC\uC5EC\uC57C \uD569\uB2C8\uB2E4.",
25557
25684
  "config.fileShape": "config.json\uC740 64 KiB \uC774\uD558\uC758 \uC77C\uBC18 \uD30C\uC77C\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4.",
@@ -25571,7 +25698,6 @@ var messages_default3 = {
25571
25698
  "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
25699
  "specReader.headUnreadable": "HEAD\uB97C \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
25573
25700
  "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
25701
  "specReader.historyUnreadable": "Git \uC774\uB825 \uD615\uC2DD\uC744 \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
25576
25702
  "specReader.projectChanged": "\uC870\uD68C \uC911 \uD504\uB85C\uC81D\uD2B8\uAC00 \uBC14\uB00C\uC5C8\uC2B5\uB2C8\uB2E4. \uC0C8\uB85C\uACE0\uCE68\uD558\uC138\uC694.",
25577
25703
  "browser.invalidPort": "\uD3EC\uD2B8\uB294 0\uBD80\uD130 65535 \uC0AC\uC774\uC758 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4.",
@@ -25587,13 +25713,13 @@ var messages_default3 = {
25587
25713
  "update.text.upToDate": "\uCD5C\uC2E0 \uBC84\uC804\uC744 \uC0AC\uC6A9 \uC911\uC785\uB2C8\uB2E4.",
25588
25714
  "update.text.disabled": "\uC0C8 \uBC84\uC804 \uD655\uC778\uC774 \uAEBC\uC838 \uC788\uC2B5\uB2C8\uB2E4.",
25589
25715
  "update.text.unavailable": "\uC0C8 \uBC84\uC804\uC744 \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uB124\uD2B8\uC6CC\uD06C \uC5F0\uACB0\uC744 \uD655\uC778\uD558\uC138\uC694.",
25590
- "update.text.install": "\uC5C5\uB370\uC774\uD2B8 \uBA85\uB839: {command}",
25591
- "update.text.afterInstall": "\uC704 \uBA85\uB839\uC740 \uC804\uC5ED \uC124\uCE58 \uC5C6\uC774 \uC0C8 \uBC84\uC804\uC73C\uB85C \uC9C0\uCE68\uC744 \uAC31\uC2E0\uD569\uB2C8\uB2E4. \uD504\uB85C\uC81D\uD2B8 \uC758\uC874\uC131\uC73C\uB85C \uAD00\uB9AC\uD55C\uB2E4\uBA74 \uD574\uB2F9 \uBC84\uC804\uC744 \uBA3C\uC800 \uAC31\uC2E0\uD558\uC138\uC694. \uBE0C\uB77C\uC6B0\uC800\uB3C4 \uC0C8 \uBC84\uC804\uC73C\uB85C \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
25716
+ "update.text.install": "\uC124\uCE58 \uBA85\uB839: {command}",
25717
+ "update.text.afterInstall": "\uC124\uCE58\uD55C \uB4A4 gitifact update\uB97C \uB2E4\uC2DC \uC2E4\uD589\uD558\uBA74 \uD504\uB85C\uC81D\uD2B8 \uAE30\uC900\uACFC \uC9C0\uCE68\uC774 \uC0C8 \uBC84\uC804\uC73C\uB85C \uBC14\uB01D\uB2C8\uB2E4. \uBE0C\uB77C\uC6B0\uC800\uB3C4 \uC0C8 \uBC84\uC804\uC73C\uB85C \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
25592
25718
  "update.text.blockRefreshed": "\uC9C0\uCE68 \uBE14\uB85D \uAC31\uC2E0: {paths}",
25593
25719
  "update.text.blockCurrent": "\uC9C0\uCE68 \uBE14\uB85D\uC740 \uC774\uBBF8 \uD604\uC7AC \uBC84\uC804\uC785\uB2C8\uB2E4: {paths}",
25594
- "update.text.noBlock": "GITIFACT \uBE14\uB85D\uC774 \uC788\uB294 \uC9C0\uCE68 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. npx --yes gitifact@{version} init\uC73C\uB85C \uC124\uCE58\uD558\uC138\uC694.",
25720
+ "update.text.noBlock": "GITIFACT \uBE14\uB85D\uC774 \uC788\uB294 \uC9C0\uCE68 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. gitifact init\uC73C\uB85C \uC124\uCE58\uD558\uC138\uC694.",
25595
25721
  "update.text.notInitialized": "\uCD08\uAE30\uD654\uB41C \uD504\uB85C\uC81D\uD2B8\uAC00 \uC544\uB2C8\uC5B4\uC11C \uBC84\uC804\uB9CC \uD655\uC778\uD588\uC2B5\uB2C8\uB2E4.",
25596
- "update.text.missing": "{paths} \uD30C\uC77C\uC774 \uC5C6\uC5B4 Claude Code\uAC00 AGENTS.md\uC758 \uC9C0\uCE68\uC744 \uC77D\uC9C0 \uBABB\uD569\uB2C8\uB2E4. npx --yes gitifact@{version} init\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uBA74 AGENTS.md\uB97C \uAC00\uC838\uC624\uB294 \uD30C\uC77C\uC744 \uB9CC\uB4ED\uB2C8\uB2E4.",
25722
+ "update.text.missing": "{paths} \uD30C\uC77C\uC774 \uC5C6\uC5B4 Claude Code\uAC00 AGENTS.md\uC758 \uC9C0\uCE68\uC744 \uC77D\uC9C0 \uBABB\uD569\uB2C8\uB2E4. gitifact init\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uBA74 AGENTS.md\uB97C \uAC00\uC838\uC624\uB294 \uD30C\uC77C\uC744 \uB9CC\uB4ED\uB2C8\uB2E4.",
25597
25723
  "update.text.migrationRequired": "\uC774 \uD504\uB85C\uC81D\uD2B8\uC758 \uBB38\uC11C\uB294 \uC544\uC9C1 0.7 \uC800\uC7A5 \uADDC\uC57D(schemaVersion 2)\uC785\uB2C8\uB2E4. \uC9C0\uCE68 \uBE14\uB85D\uC740 \uC0C8 \uBA85\uB839\uC744 \uC548\uB0B4\uD558\uC9C0\uB9CC \uC804\uD658 \uC804\uC5D0\uB294 specs\xB7instructions\xB7records\xB7check\xB7changes \uBA85\uB839\uC774 \uB3D9\uC791\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. npx --yes gitifact@{version} guide show migrate\uC758 \uC808\uCC28\uB85C \uBB38\uC11C\uB97C \uC804\uD658\uD558\uC138\uC694.",
25598
25724
  "update.text.committed": "\uC9C0\uCE68 \uBE14\uB85D \uCEE4\uBC0B: {commit} ({paths})",
25599
25725
  "update.text.commitNothing": "\uCEE4\uBC0B\uD560 \uC9C0\uCE68 \uBE14\uB85D \uBCC0\uACBD\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
@@ -25605,7 +25731,7 @@ var messages_default3 = {
25605
25731
  "server.assetNotFound": "\uC5D0\uC14B\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
25606
25732
  "help.lang": "\uCD9C\uB825 \uC5B8\uC5B4 (ko \uB610\uB294 en)",
25607
25733
  "help.updateCheck": "\uD30C\uC77C\uC744 \uBC14\uAFB8\uC9C0 \uC54A\uACE0 \uC0C8 \uBC84\uC804\uB9CC \uD655\uC778",
25608
- "commitFiles.notRecord": ".gitifact \uC548\uC5D0\uC11C\uB294 \uAE30\uB85D\xB7\uC124\uC815\xB7\uC5D0\uC14B\uB9CC \uCEE4\uBC0B\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4: {path}",
25734
+ "commitFiles.notRecord": ".gitifact \uC548\uC5D0\uC11C\uB294 \uAE30\uB85D\xB7\uC124\uC815(config.json\xB7.gitattributes)\xB7\uC5D0\uC14B\uB9CC \uCEE4\uBC0B\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4: {path}",
25609
25735
  "help.changes": "\uCEE4\uBC0B\uD558\uC9C0 \uC54A\uC740 \uBB38\uC11C \uBCC0\uACBD \uC870\uD68C\uC640 \uCEE4\uBC0B",
25610
25736
  "help.changesList": "HEAD \uB300\uBE44 \uBC14\uB010 \uBB38\uC11C, \uCEE4\uBC0B\uD558\uC9C0 \uC54A\uC740 \uACB0\uC815\uAE30\uB85D, \uAE30\uB85D\uC774 \uC5C6\uB294 \uBCC0\uACBD, \uCEE4\uBC0B \uC785\uB825 \uD30C\uC77C \uACBD\uB85C",
25611
25737
  "help.changesCommit": "check\uC640 \uAC19\uC740 \uAC80\uC0AC \uB4A4 \uC120\uD0DD\uD55C \uD30C\uC77C\uACFC \uACB0\uC815\uAE30\uB85D\uC744 \uCEE4\uBC0B",
@@ -25758,7 +25884,16 @@ var messages_default3 = {
25758
25884
  "feedback.openUrl": "gh\uB97C \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC544\uB798 \uC8FC\uC18C\uB97C \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uC5B4 \uC774\uC288\uB97C \uC81C\uCD9C\uD558\uC138\uC694.",
25759
25885
  "feedback.ghFailed": "gh\uB85C \uC774\uC288\uB97C \uB9CC\uB4E4\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uC544\uB798 \uC8FC\uC18C\uB97C \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uC5B4 \uC774\uC288\uB97C \uC81C\uCD9C\uD558\uC138\uC694.",
25760
25886
  "feedback.truncated": "\uBCF8\uBB38\uC774 \uAE38\uC5B4 \uC8FC\uC18C\uC5D0\uB294 \uC55E\uBD80\uBD84\uB9CC \uB2F4\uACBC\uC2B5\uB2C8\uB2E4. \uC544\uB798 \uC804\uCCB4 \uBCF8\uBB38\uC744 \uC791\uC131 \uD398\uC774\uC9C0\uC5D0 \uBD99\uC5EC \uB123\uC73C\uC138\uC694.",
25761
- "feedback.title": "\uC81C\uBAA9: {title}"
25887
+ "feedback.title": "\uC81C\uBAA9: {title}",
25888
+ "update.text.projectWritten": "{path}\uC758 \uD504\uB85C\uC81D\uD2B8 \uAE30\uC900\uC744 gitifact {version}\uC73C\uB85C \uC801\uC5C8\uC2B5\uB2C8\uB2E4.",
25889
+ "update.text.projectAhead": "\uD504\uB85C\uC81D\uD2B8 \uAE30\uC900\uC740 gitifact {project}\uC785\uB2C8\uB2E4. \uC774 CLI({version})\uAC00 \uB354 \uC624\uB798\uB418\uC5B4 \uB0AE\uCD94\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. {command}\uB85C \uC62C\uB9AC\uC138\uC694.",
25890
+ "update.later.postponed": "{latest} \uC548\uB0B4\uB97C {until}\uAE4C\uC9C0 \uC27D\uB2C8\uB2E4. \uB354 \uC0C8 \uBC84\uC804\uC774 \uB098\uC624\uBA74 \uBC14\uB85C \uB2E4\uC2DC \uC548\uB0B4\uD569\uB2C8\uB2E4.",
25891
+ "update.later.nothing": "\uBBF8\uB8F0 \uC0C8 \uBC84\uC804 \uC548\uB0B4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4(\uC9C0\uAE08 {version}).",
25892
+ "notice.release": "\uC0C8 \uBC84\uC804 {latest}\uAC00 \uC788\uC2B5\uB2C8\uB2E4(\uC9C0\uAE08 {version}). \uC5C5\uB370\uC774\uD2B8: {command} \uD6C4 gitifact update \xB7 \uB098\uC911\uC5D0: gitifact update --later",
25893
+ "notice.projectNewer": "\uC774 \uD504\uB85C\uC81D\uD2B8\uB294 gitifact {project} \uAE30\uC900\uC785\uB2C8\uB2E4(\uC2E4\uD589 \uC911 {version}). {command}\uB85C \uC62C\uB9AC\uC138\uC694.",
25894
+ "notice.projectOlder": "\uC774 CLI({version})\uAC00 \uD504\uB85C\uC81D\uD2B8 \uAE30\uC900({project})\uBCF4\uB2E4 \uC0C8 \uBC84\uC804\uC785\uB2C8\uB2E4. gitifact update\uB85C \uD504\uB85C\uC81D\uD2B8 \uAE30\uC900\uC744 \uC62C\uB9B4 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
25895
+ "help.updateLater": "\uC9C0\uAE08 \uC548\uB0B4\uB41C \uC0C8 \uBC84\uC804\uC744 24\uC2DC\uAC04 \uB3D9\uC548 \uC548\uB0B4\uD558\uC9C0 \uC54A\uC74C. \uB354 \uC0C8 \uBC84\uC804\uC740 \uBC14\uB85C \uC548\uB0B4",
25896
+ "update.text.blockKept": "\uD504\uB85C\uC81D\uD2B8\uAC00 \uC774 CLI\uBCF4\uB2E4 \uC0C8 \uBC84\uC804 \uAE30\uC900\uC774\uB77C \uC9C0\uCE68 \uBE14\uB85D\uC744 \uADF8\uB300\uB85C \uB450\uC5C8\uC2B5\uB2C8\uB2E4: {paths}"
25762
25897
  };
25763
25898
 
25764
25899
  // src/shared/i18n/en/messages.json
@@ -25809,6 +25944,8 @@ var messages_default4 = {
25809
25944
  "server.sessionChanged": "The server session changed. Reconnect to continue.",
25810
25945
  "server.badRange": "Check the requested range.",
25811
25946
  "server.commitNotFound": "This repository has no such commit.",
25947
+ "server.featureNotFound": "The checkout has no such feature.",
25948
+ "server.contributorNotFound": "Nobody with this email committed to HEAD.",
25812
25949
  "server.cursorNotFound": "The place to read on from is no longer in the list. Load it again from the start.",
25813
25950
  "server.workingChangeNotFound": "No uncommitted change of that document.",
25814
25951
  "server.recordNotFound": "This history has no such decision record.",
@@ -25855,7 +25992,7 @@ var messages_default4 = {
25855
25992
  "init.text.storage": "Storage convention",
25856
25993
  "init.text.agentDocs": "Agent instructions",
25857
25994
  "init.text.lineEndings": "Line endings: {path} keeps documents in LF. Documents that now show as changed differ only in line endings; commit them once to settle it.",
25858
- "init.text.updateAvailable": "Version {version} is available. Run {command} to use it and refresh the project instructions.",
25995
+ "init.text.updateAvailable": "Version {version} is available. Install it with {command}, then run gitifact update to move the project to it.",
25859
25996
  "init.failed": "Initialization failed. Check file permissions and the generated configuration and temporary files.",
25860
25997
  "config.storeNotDirectory": ".gitifact must be a regular directory, not a link.",
25861
25998
  "config.fileShape": "config.json must be a regular file no larger than 64 KiB.",
@@ -25875,7 +26012,6 @@ var messages_default4 = {
25875
26012
  "agentBlock.duplicateStart": "Multiple GITIFACT start markers found. Remove duplicate blocks before retrying.",
25876
26013
  "specReader.headUnreadable": "Could not read HEAD.",
25877
26014
  "specReader.schemaRequired": "A project using the supported schema is required.",
25878
- "specReader.authorUnreadable": "Could not read Git authors.",
25879
26015
  "specReader.historyUnreadable": "Could not read Git history.",
25880
26016
  "specReader.projectChanged": "The project changed while reading. Refresh to continue.",
25881
26017
  "browser.invalidPort": "Port must be an integer between 0 and 65535.",
@@ -25891,13 +26027,13 @@ var messages_default4 = {
25891
26027
  "update.text.upToDate": "You are using the latest version.",
25892
26028
  "update.text.disabled": "Update checks are disabled.",
25893
26029
  "update.text.unavailable": "Could not check for updates. Check your network connection.",
25894
- "update.text.install": "Update command: {command}",
25895
- "update.text.afterInstall": "This command refreshes the instructions with the new version without a global installation. If you manage Gitifact as a project dependency, update that dependency first. Restart the browser with the new version.",
26030
+ "update.text.install": "Install command: {command}",
26031
+ "update.text.afterInstall": "After installing, run gitifact update again to move the project and its instructions to the new version. Restart the browser with the new version too.",
25896
26032
  "update.text.blockRefreshed": "Instruction blocks refreshed: {paths}",
25897
26033
  "update.text.blockCurrent": "Instruction blocks are already current: {paths}",
25898
- "update.text.noBlock": "No instruction files contain a GITIFACT block. Run npx --yes gitifact@{version} init to install one.",
26034
+ "update.text.noBlock": "No instruction files contain a GITIFACT block. Run gitifact init to install one.",
25899
26035
  "update.text.notInitialized": "Only the version was checked because this project is not initialized.",
25900
- "update.text.missing": "{paths} is missing, so Claude Code cannot load the AGENTS.md instructions. Rerun npx --yes gitifact@{version} init to create the import file.",
26036
+ "update.text.missing": "{paths} is missing, so Claude Code cannot load the AGENTS.md instructions. Rerun gitifact init to create the import file.",
25901
26037
  "update.text.migrationRequired": "This project's documents still use the 0.7 storage convention (schemaVersion 2). The blocks describe the new commands, but the specs, instructions, records, check and changes commands refuse to run until the documents are moved. Follow npx --yes gitifact@{version} guide show migrate to move them.",
25902
26038
  "update.text.committed": "Instruction blocks committed: {commit} ({paths})",
25903
26039
  "update.text.commitNothing": "No instruction block changes to commit.",
@@ -25909,7 +26045,7 @@ var messages_default4 = {
25909
26045
  "server.assetNotFound": "Asset not found.",
25910
26046
  "help.lang": "Output language (ko or en)",
25911
26047
  "help.updateCheck": "Check for a newer version without changing files",
25912
- "commitFiles.notRecord": "Inside .gitifact only records, the configuration and assets can be committed: {path}",
26048
+ "commitFiles.notRecord": "Inside .gitifact only records, the configuration (config.json, .gitattributes) and assets can be committed: {path}",
25913
26049
  "help.changes": "Inspect and commit uncommitted document changes",
25914
26050
  "help.changesList": "Documents changed since HEAD, uncommitted records, changes without a record, and the commit input path",
25915
26051
  "help.changesCommit": "Check like the check command, then commit the selected files and records",
@@ -26062,7 +26198,16 @@ var messages_default4 = {
26062
26198
  "feedback.openUrl": "gh is not available. Open the address below in a browser and submit the issue.",
26063
26199
  "feedback.ghFailed": "gh could not create the issue. Open the address below in a browser and submit the issue.",
26064
26200
  "feedback.truncated": "The body is long, so the address holds only its beginning. Paste the full body below into the page.",
26065
- "feedback.title": "Title: {title}"
26201
+ "feedback.title": "Title: {title}",
26202
+ "update.text.projectWritten": "Set the project to gitifact {version} in {path}.",
26203
+ "update.text.projectAhead": "The project is set to gitifact {project}. This CLI ({version}) is older, so it was not lowered. Install it with {command}.",
26204
+ "update.later.postponed": "The notice for {latest} is quiet until {until}. A newer release is announced right away.",
26205
+ "update.later.nothing": "No newer release to postpone (running {version}).",
26206
+ "notice.release": "Version {latest} is available (running {version}). Update: {command}, then gitifact update \xB7 Later: gitifact update --later",
26207
+ "notice.projectNewer": "This project is set to gitifact {project} (running {version}). Install it with {command}.",
26208
+ "notice.projectOlder": "This CLI ({version}) is newer than the project ({project}). Run gitifact update to move the project to it.",
26209
+ "help.updateLater": "Quiet the announced newer release for 24 hours; a release newer than it is announced right away",
26210
+ "update.text.blockKept": "The project is set to a newer release than this CLI, so the instruction blocks were left as they are: {paths}"
26066
26211
  };
26067
26212
 
26068
26213
  // src/shared/i18n/index.ts
@@ -26414,7 +26559,7 @@ function storeReader(cwd) {
26414
26559
  import { DatabaseSync } from "node:sqlite";
26415
26560
  import { lstat, mkdir, rm, writeFile } from "node:fs/promises";
26416
26561
  import { join as join2 } from "node:path";
26417
- var CACHE_FORMAT = 9;
26562
+ var CACHE_FORMAT = 11;
26418
26563
  var CACHE_DIR = ".gitifact/cache";
26419
26564
  var SCHEMA = `
26420
26565
  -- Working-tree files already read: the modification time and size decide whether a file is read again.
@@ -26444,7 +26589,17 @@ var SCHEMA = `
26444
26589
  CREATE TABLE lineage (head TEXT NOT NULL, pos INTEGER NOT NULL, oid TEXT NOT NULL, PRIMARY KEY (head, pos));
26445
26590
  CREATE INDEX lineage_by_commit ON lineage (head, oid);
26446
26591
  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').
26592
+ -- Every commit the log has met, code commits too: who made it and when, and whether it is a format migration.
26593
+ -- 'reach' is 1 for the commits of the HEAD the log follows now; contributors are counted over those.
26594
+ CREATE TABLE log (oid TEXT PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL, time INTEGER NOT NULL, date TEXT NOT NULL,
26595
+ subject TEXT NOT NULL, migration INTEGER NOT NULL, reach INTEGER NOT NULL);
26596
+ CREATE INDEX log_by_reach ON log (reach, email);
26597
+ -- The store folders a commit touched: a feature folder, an instruction folder, or AGENTS.md.
26598
+ CREATE TABLE touches (oid TEXT NOT NULL, folder TEXT NOT NULL, PRIMARY KEY (oid, folder));
26599
+ CREATE INDEX touches_by_folder ON touches (folder);
26600
+ -- What the log follows: the HEAD ('head') and the fingerprint of the .mailmap its names were read with ('mailmap').
26601
+ CREATE TABLE state (key TEXT PRIMARY KEY, value TEXT NOT NULL);
26602
+ -- What the search box finds among the working tree's documents (scope 'checkout'); records are searched in their table.
26448
26603
  -- The searched columns hold lower-cased text; trigram lets LIKE '%...%' use the index from three characters on.
26449
26604
  CREATE VIRTUAL TABLE search USING fts5(
26450
26605
  scope UNINDEXED, kind UNINDEXED, ref UNINDEXED, oid UNINDEXED, payload UNINDEXED,
@@ -26728,6 +26883,40 @@ function createDocumentCache(root, database) {
26728
26883
  }));
26729
26884
  });
26730
26885
  },
26886
+ /**
26887
+ * The working documents of one kind whose title, place or text holds the query, a page at a time: a title that
26888
+ * starts with it first, then a title, a place, and the text; by title within each. `after` is the ID of the last
26889
+ * hit of the page before; one that no longer matches answers undefined. Each hit names its feature's ID.
26890
+ */
26891
+ async searchKind(kind2, raw, after, limit) {
26892
+ const query = raw.trim().toLowerCase();
26893
+ await sync();
26894
+ return database.with((db) => {
26895
+ const literal2 = /[\\%_]/.test(query);
26896
+ const match = (column) => literal2 ? `instr(${column}, :q) > 0` : `${column} LIKE :like`;
26897
+ const ordered = `WITH hits AS (
26898
+ SELECT json_extract(payload, '$.id') AS id, payload, title, ref,
26899
+ 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
26900
+ FROM search WHERE scope = 'checkout' AND kind = :kind AND (${match("title")} OR ${match("place")} OR ${match("body")})),
26901
+ ordered AS (SELECT *, row_number() OVER (ORDER BY rank, title, ref) AS n FROM hits)`;
26902
+ const params = { q: query, kind: kind2, ...literal2 ? {} : { like: containing(query) } };
26903
+ const total = Number(db.prepare(`${ordered} SELECT count(*) AS n FROM ordered`).get(params).n);
26904
+ let start = 0;
26905
+ if (after !== void 0) {
26906
+ const at = db.prepare(`${ordered} SELECT n FROM ordered WHERE id = :after`).get({ ...params, after });
26907
+ if (!at) return void 0;
26908
+ start = Number(at.n);
26909
+ }
26910
+ const rows = db.prepare(`${ordered} SELECT payload FROM ordered WHERE n > :start ORDER BY n LIMIT :limit`).all({ ...params, start, limit: limit + 1 });
26911
+ const features = new Map(db.prepare("SELECT feature, id FROM documents WHERE kind = 'feature'").all().map((r) => [r.feature, r.id]));
26912
+ const hits = rows.slice(0, limit).map((r) => {
26913
+ const p = JSON.parse(r.payload);
26914
+ const featureId2 = p.feature ? features.get(p.feature) : void 0;
26915
+ return { id: p.id, kind: kind2, title: p.title, where: p.where, line: snippet(p.body, query), ...featureId2 ? { featureId: featureId2 } : {} };
26916
+ });
26917
+ return { total, next: rows.length > limit ? hits[hits.length - 1].id : null, hits };
26918
+ });
26919
+ },
26731
26920
  /** Documents that refer to `id` from their frontmatter: designs naming a requirement or a source. */
26732
26921
  async referencing(id) {
26733
26922
  await sync();
@@ -27096,20 +27285,19 @@ var listed = (e) => {
27096
27285
  return { ...event2, before: reference2(e.before), after: reference2(e.after) };
27097
27286
  };
27098
27287
  var sideOf3 = (snapshot5, rev) => !snapshot5 ? null : rev ? { rev, path: snapshot5.path } : snapshot5;
27099
- var titleOf = (e) => (e.after ?? e.before)?.title ?? e.id;
27100
27288
  var DAY = 864e5;
27101
27289
  var KEPT_HEADS = 4;
27102
27290
  var RECENT_COMMITS = 3;
27103
27291
  var RECENT_CHANGES = 12;
27104
- function createHistory(database, git) {
27292
+ function createHistory(database, git, originals) {
27105
27293
  const changes2 = createCommitChanges(git);
27294
+ const texts = originals ?? changes2;
27106
27295
  const building = /* @__PURE__ */ new Map();
27107
27296
  const built = /* @__PURE__ */ new Set();
27108
27297
  function insert(db, commits, readers) {
27109
27298
  const commit = db.prepare("INSERT OR IGNORE INTO commits (oid, reader) VALUES (?, ?)");
27110
27299
  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
27300
  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
27301
  transaction(db, () => {
27114
27302
  for (const c of commits) {
27115
27303
  if (!Number(commit.run(c.commit, readers.get(c.commit) ?? "current").changes)) continue;
@@ -27132,23 +27320,19 @@ function createHistory(database, git) {
27132
27320
  JSON.stringify(row),
27133
27321
  JSON.stringify(detail)
27134
27322
  );
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
27323
  });
27148
27324
  }
27149
27325
  });
27150
27326
  }
27151
27327
  async function build(head) {
27328
+ const known = await database.with((db) => {
27329
+ const seen = !!db.prepare("SELECT 1 FROM heads WHERE head = ?").get(head);
27330
+ if (seen) db.prepare("UPDATE heads SET seen = ? WHERE head = ?").run(Date.now(), head);
27331
+ return seen;
27332
+ });
27333
+ if (!known) await read(head);
27334
+ }
27335
+ async function read(head) {
27152
27336
  const lineage = await changes2.lineage(head);
27153
27337
  const readers = await changes2.readers(head, lineage);
27154
27338
  const known = await database.with((db) => new Map(db.prepare("SELECT oid, reader FROM commits").all().map((r) => [r.oid, r.reader])));
@@ -27158,13 +27342,14 @@ function createHistory(database, git) {
27158
27342
  db.prepare("DELETE FROM commits WHERE oid = ?").run(oid3);
27159
27343
  db.prepare("DELETE FROM changes WHERE oid = ?").run(oid3);
27160
27344
  db.prepare("DELETE FROM records WHERE oid = ?").run(oid3);
27161
- db.prepare("DELETE FROM search WHERE scope = 'history' AND oid = ?").run(oid3);
27162
27345
  }
27346
+ db.prepare("DELETE FROM lineage WHERE head != ?").run(head);
27347
+ db.prepare("DELETE FROM heads WHERE head != ?").run(head);
27163
27348
  }));
27164
27349
  const missing2 = lineage.filter((oid3) => !known.has(oid3) || stale.includes(oid3));
27165
27350
  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));
27351
+ const read2 = await changes2.of(missing2.slice(i, i + 100), readers);
27352
+ await database.with((db) => insert(db, read2, readers));
27168
27353
  }
27169
27354
  await database.with((db) => transaction(db, () => {
27170
27355
  db.prepare("DELETE FROM lineage WHERE head = ?").run(head);
@@ -27191,15 +27376,21 @@ function createHistory(database, git) {
27191
27376
  async function readCommit(commit) {
27192
27377
  const known = await database.with((db) => !!db.prepare("SELECT 1 FROM commits WHERE oid = ?").get(commit));
27193
27378
  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()));
27379
+ const read2 = await changes2.of([commit]).catch(() => void 0);
27380
+ if (read2) await database.with((db) => insert(db, read2, /* @__PURE__ */ new Map()));
27381
+ }
27382
+ function sameRecord(db, commit, id) {
27383
+ if (!id.startsWith("H-")) return [id];
27384
+ const own2 = db.prepare("SELECT title, sections FROM records WHERE oid = ? AND id = ?").get(commit, id);
27385
+ if (!own2) return [id];
27386
+ 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
27387
  }
27197
27388
  function hydrate(db, rows) {
27198
27389
  const events = rows.map((row) => JSON.parse(row));
27199
- const read = db.prepare("SELECT id, title, sections FROM records WHERE oid = ?");
27390
+ const read2 = db.prepare("SELECT id, title, sections FROM records WHERE oid = ?");
27200
27391
  const byCommit = /* @__PURE__ */ new Map();
27201
27392
  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) }])));
27393
+ 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
27394
  }
27204
27395
  return events.map((e) => ({ ...e, records: e.records.flatMap((id) => {
27205
27396
  const r = byCommit.get(e.commit)?.get(id);
@@ -27216,8 +27407,8 @@ function createHistory(database, git) {
27216
27407
  if (value && "rev" in value) places.push({ rev: value.rev, path: value.path, specId: events[i][key]?.specId ?? "" });
27217
27408
  }
27218
27409
  });
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]));
27410
+ const read2 = places.length ? await texts.sides(places) : [];
27411
+ const text4 = new Map(places.map((p, i) => [p.rev + ":" + p.path, read2[i] ?? null]));
27221
27412
  const resolve = (value) => !value ? null : "rev" in value ? text4.get(value.rev + ":" + value.path) ?? null : value;
27222
27413
  return events.map((event2, i) => ({ event: event2, before: resolve(sides[i].before), after: resolve(sides[i].after) }));
27223
27414
  }
@@ -27249,7 +27440,7 @@ function createHistory(database, git) {
27249
27440
  return {
27250
27441
  ensure,
27251
27442
  /** The document and reason files of one commit, read from Git and not kept. */
27252
- filesAt: (rev) => changes2.tree(rev),
27443
+ filesAt: (rev) => texts.tree(rev),
27253
27444
  /**
27254
27445
  * The changes that match, newest first, a page of whole commits: `limit` commits after the commit `after`, each
27255
27446
  * with all of its matching changes, and how many changes and commits match in all. `next` is the last commit of the
@@ -27278,11 +27469,15 @@ function createHistory(database, git) {
27278
27469
  };
27279
27470
  });
27280
27471
  },
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) {
27472
+ /**
27473
+ * The changes of one commit without their text, a page at a time after the change `after` (a key): the commit page's
27474
+ * list. With `record`, only the changes that record explains: the record page's documents.
27475
+ */
27476
+ async commitChanges(commit, after, limit, record4) {
27283
27477
  await readCommit(commit);
27284
27478
  return database.with((db) => {
27285
- const all = db.prepare("SELECT key, row FROM changes WHERE oid = ? ORDER BY ord").all(commit);
27479
+ const ids = record4 === void 0 ? void 0 : JSON.stringify(sameRecord(db, commit, record4));
27480
+ 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
27481
  const start = after === void 0 ? 0 : all.findIndex((r) => r.key === after) + 1;
27287
27482
  if (after !== void 0 && !start) return void 0;
27288
27483
  const shown = all.slice(start, start + limit);
@@ -27372,86 +27567,226 @@ function createHistory(database, git) {
27372
27567
  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
27568
  const newest = Math.max(0, ...commits.map((c) => Date.parse(c.date)).filter(Number.isFinite));
27374
27569
  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) => ({
27570
+ const recent2 = commits.slice(0, RECENT_COMMITS).map((c) => ({
27376
27571
  commit: c.oid,
27377
27572
  count: Number(c.n),
27378
27573
  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
27574
  }));
27380
- return { total, byType: { created: count2("created"), modified: count2("modified"), moved: count2("moved"), deleted: count2("deleted") }, pulse, recent };
27575
+ return { total, byType: { created: count2("created"), modified: count2("modified"), moved: count2("moved"), deleted: count2("deleted") }, pulse, recent: recent2 };
27381
27576
  });
27382
27577
  },
27383
- /** The commit of `head`'s history that added a record, found through the records its changes carry. */
27384
- async commitOfRecord(head, id) {
27578
+ /**
27579
+ * The record of `head`'s history with the commit that added it, and how many other records that commit holds (a
27580
+ * 0.7 reason written once over several documents counts once). Undefined when the history has no such record.
27581
+ */
27582
+ async recordOf(head, id) {
27385
27583
  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);
27584
+ return database.with((db) => {
27585
+ 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);
27586
+ if (!found) return void 0;
27587
+ const all = db.prepare("SELECT id, title, sections FROM records WHERE oid = ?").all(found.oid);
27588
+ const groups = new Set(all.map((r) => r.id.startsWith("H-") ? "H\0" + r.title + "\0" + r.sections : r.id));
27589
+ return { commit: found.oid, record: { id, title: found.title, sections: JSON.parse(found.sections) }, others: groups.size - 1 };
27590
+ });
27387
27591
  },
27388
27592
  /**
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.
27593
+ * The records of `head`'s history whose title, ID or sections hold the query, one hit per record file, a page at a
27594
+ * time: a title that starts with it first, then a title, the ID, and the sections; newest first within each. `after`
27595
+ * is the ID of the last hit of the page before; one that no longer matches answers undefined.
27391
27596
  */
27392
- async search(head, raw) {
27597
+ async searchRecords(head, raw, after, limit) {
27393
27598
  const query = raw.trim().toLowerCase();
27394
- if (!query) return [];
27395
- if (head) await ensure(head);
27599
+ await ensure(head);
27396
27600
  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 };
27601
+ const ordered = `WITH own AS (
27602
+ SELECT r.id AS id, r.title AS title, r.oid AS oid, min(l.pos) AS pos, r.sections AS sections,
27603
+ lower((SELECT group_concat(json_extract(s.value, '$.body'), ' ') FROM json_each(r.sections) s)) AS body
27604
+ FROM lineage l JOIN records r ON r.oid = l.oid WHERE l.head = :head GROUP BY r.id),
27605
+ hits AS (SELECT *, CASE WHEN substr(lower(title), 1, length(:q)) = :q THEN 0 WHEN instr(lower(title), :q) > 0 THEN 1
27606
+ WHEN instr(lower(id), :q) > 0 THEN 2 ELSE 3 END AS rank
27607
+ FROM own WHERE instr(lower(title), :q) > 0 OR instr(lower(id), :q) > 0 OR instr(body, :q) > 0),
27608
+ ordered AS (SELECT *, row_number() OVER (ORDER BY rank, pos, id) AS n FROM hits)`;
27609
+ const params = { head, q: query };
27610
+ const total = Number(db.prepare(`${ordered} SELECT count(*) AS n FROM ordered`).get(params).n);
27611
+ let start = 0;
27612
+ if (after !== void 0) {
27613
+ const at = db.prepare(`${ordered} SELECT n FROM ordered WHERE id = :after`).get({ ...params, after });
27614
+ if (!at) return void 0;
27615
+ start = Number(at.n);
27616
+ }
27617
+ 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 });
27618
+ const hits = rows.slice(0, limit).map((r) => {
27619
+ const text4 = JSON.parse(r.sections).map((s) => s.body).join(" ");
27620
+ 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
27621
  });
27422
- return [...documents, ...past];
27622
+ return { total, next: rows.length > limit ? hits[hits.length - 1].id : null, hits };
27623
+ });
27624
+ }
27625
+ };
27626
+ }
27627
+
27628
+ // src/adapters/cache/commit-log.ts
27629
+ import { lstat as lstat3 } from "node:fs/promises";
27630
+ import { join as join4 } from "node:path";
27631
+ var FORMAT2 = `%x1e%H%x00%aN%x00%aE%x00%at%x00%aI%x00%s%x00%(trailers:key=${MIGRATION_TRAILER},valueonly,separator=%x2C)`;
27632
+ var CHUNK = 1e5;
27633
+ var folderOf2 = (path3) => path3 === "AGENTS.md" ? path3 : new RegExp(`^(${SPEC_ROOT.replace(/\./g, "\\.")}/[^/]+|${INSTRUCTIONS_ROOT.replace(/\./g, "\\.")}/[^/]+)/`).exec(path3)?.[1];
27634
+ function createCommitLog(database, git, root) {
27635
+ let following;
27636
+ let followingHead;
27637
+ const read = async (args) => git.decode(await git.run(args));
27638
+ async function commits(range) {
27639
+ const out = [];
27640
+ for (let skip = 0; ; skip += CHUNK) {
27641
+ const text4 = await read(["log", `--format=${FORMAT2}`, `--max-count=${CHUNK}`, `--skip=${skip}`, ...range, "--"]);
27642
+ const chunk = text4.split("").filter(Boolean).map((entry2) => {
27643
+ const [oid3, name2, email3, time3, date5, subject, migration] = entry2.split("\0");
27644
+ return { oid: oid3, name: name2 ?? "", email: email3 ?? "", time: Number(time3), date: date5 ?? "", subject: subject ?? "", migration: !!migration?.trim() };
27645
+ });
27646
+ out.push(...chunk);
27647
+ if (chunk.length < CHUNK) return out;
27648
+ }
27649
+ }
27650
+ async function touches(range) {
27651
+ const text4 = await read(["log", "--format=%x1e%H", "-z", "--name-only", "--no-renames", ...range, "--", SPEC_ROOT, INSTRUCTIONS_ROOT, "AGENTS.md"]);
27652
+ const out = [];
27653
+ for (const entry2 of text4.split("").filter(Boolean)) {
27654
+ const [oid3, ...paths2] = entry2.split("\0");
27655
+ const folders = new Set(paths2.map((p) => folderOf2(p.replace(/^\n/, ""))).filter((f) => !!f));
27656
+ for (const folder of folders) out.push([oid3.trim(), folder]);
27657
+ }
27658
+ return out;
27659
+ }
27660
+ async function mailmap() {
27661
+ const info5 = await lstat3(join4(root, ".mailmap")).catch(() => void 0);
27662
+ return info5 ? info5.mtimeMs + ":" + info5.size : "-";
27663
+ }
27664
+ const stateOf = (db, key) => db.prepare("SELECT value FROM state WHERE key = ?").get(key)?.value;
27665
+ async function follow(head, attempts = 3) {
27666
+ const names = await mailmap();
27667
+ const [before, named] = await database.with((db) => [stateOf(db, "head"), stateOf(db, "mailmap")]);
27668
+ if (before === head && named === names) return;
27669
+ let change2;
27670
+ if (before && named === names) {
27671
+ try {
27672
+ const range = [head, "^" + before];
27673
+ const [added2, removed2, touched2] = await Promise.all([commits(range), read(["rev-list", before, "^" + head, "--"]), touches(range)]);
27674
+ change2 = { added: added2, removed: removed2.split("\n").filter(Boolean), touched: touched2, whole: false };
27675
+ } catch {
27676
+ change2 = void 0;
27677
+ }
27678
+ }
27679
+ if (!change2) {
27680
+ const [added2, touched2] = await Promise.all([commits([head]), touches([head])]);
27681
+ change2 = { added: added2, removed: [], touched: touched2, whole: true };
27682
+ }
27683
+ const { added, removed, touched, whole } = change2;
27684
+ const written = await database.with((db) => transaction(db, () => {
27685
+ if (stateOf(db, "head") !== before || stateOf(db, "mailmap") !== named) return false;
27686
+ if (whole) {
27687
+ db.exec("DELETE FROM log");
27688
+ db.exec("DELETE FROM touches");
27689
+ }
27690
+ const put = db.prepare(`INSERT INTO log (oid, name, email, time, date, subject, migration, reach) VALUES (?, ?, ?, ?, ?, ?, ?, 1)
27691
+ ON CONFLICT (oid) DO UPDATE SET name = excluded.name, email = excluded.email, reach = 1`);
27692
+ for (const c of added) put.run(c.oid, c.name, c.email, c.time, c.date, c.subject, c.migration ? 1 : 0);
27693
+ const leave = db.prepare("UPDATE log SET reach = 0 WHERE oid = ?");
27694
+ for (const oid3 of removed) leave.run(oid3);
27695
+ const touch = db.prepare("INSERT OR IGNORE INTO touches (oid, folder) VALUES (?, ?)");
27696
+ for (const [oid3, folder] of touched) touch.run(oid3, folder);
27697
+ const set2 = db.prepare("INSERT INTO state (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value");
27698
+ set2.run("head", head);
27699
+ set2.run("mailmap", names);
27700
+ return true;
27701
+ }));
27702
+ if (!written && attempts > 1) return follow(head, attempts - 1);
27703
+ }
27704
+ async function ensure(head) {
27705
+ while (following && followingHead !== head) await following.catch(() => void 0);
27706
+ if (!following) {
27707
+ followingHead = head;
27708
+ following = follow(head).finally(() => {
27709
+ following = void 0;
27710
+ followingHead = void 0;
27711
+ });
27712
+ }
27713
+ return following;
27714
+ }
27715
+ return {
27716
+ ensure,
27717
+ /** Everyone who committed to `head`, most commits first, format migrations left out. `latest` is their newest commit. */
27718
+ async contributors(head) {
27719
+ await ensure(head);
27720
+ return database.with((db) => db.prepare(`SELECT email, name, count(*) AS commits, max(time) AS time, date AS latest FROM log
27721
+ 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 })));
27722
+ },
27723
+ /**
27724
+ * The commits of `head` a hash names: the whole hash, or its start from seven characters on, as `git log --oneline`
27725
+ * prints it. Anything shorter or not a hash names none. Newest first, a page after the commit `after`.
27726
+ */
27727
+ async commitsNamed(head, raw, after, limit) {
27728
+ const prefix = raw.trim().toLowerCase();
27729
+ if (!/^[0-9a-f]{7,64}$/.test(prefix)) return { total: 0, next: null, hits: [] };
27730
+ await ensure(head);
27731
+ return database.with((db) => {
27732
+ 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);
27733
+ const start = after === void 0 ? 0 : rows.findIndex((r) => r.oid === after) + 1;
27734
+ if (after !== void 0 && !start) return void 0;
27735
+ const shown = rows.slice(start, start + limit);
27736
+ return {
27737
+ total: rows.length,
27738
+ next: start + shown.length < rows.length ? shown[shown.length - 1].oid : null,
27739
+ 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 }))
27740
+ };
27741
+ });
27742
+ },
27743
+ /** Each store folder's authors, most commits first, and when it was last touched, format migrations left out. */
27744
+ async folders(head) {
27745
+ await ensure(head);
27746
+ return database.with((db) => {
27747
+ 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
27748
+ FROM touches t JOIN log l ON l.oid = t.oid WHERE l.reach = 1 AND l.migration = 0
27749
+ GROUP BY t.folder, l.email ORDER BY t.folder, commits DESC, name, email`).all();
27750
+ const out = /* @__PURE__ */ new Map();
27751
+ for (const r of rows) {
27752
+ let entry2 = out.get(r.folder);
27753
+ if (!entry2) out.set(r.folder, entry2 = { people: [], latest: r.latest, time: r.time });
27754
+ entry2.people.push({ name: r.name, email: r.email, commits: Number(r.commits), latest: r.latest });
27755
+ if (r.time > entry2.time) {
27756
+ entry2.time = r.time;
27757
+ entry2.latest = r.latest;
27758
+ }
27759
+ }
27760
+ return new Map([...out].map(([folder, { people, latest }]) => [folder, { people, latest }]));
27423
27761
  });
27424
27762
  }
27425
27763
  };
27426
27764
  }
27427
27765
 
27428
27766
  // src/adapters/cache/index.ts
27429
- function openCache(root, git) {
27767
+ function openCache(root, git, originals) {
27430
27768
  const database = createCacheDatabase(root);
27431
27769
  const documents = createDocumentCache(root, database);
27432
- const history = createHistory(database, git);
27770
+ const history = createHistory(database, git, originals);
27771
+ const log = createCommitLog(database, git, root);
27433
27772
  return {
27434
27773
  documents,
27435
27774
  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
- }
27775
+ log,
27776
+ location: database.location
27442
27777
  };
27443
27778
  }
27444
27779
 
27445
27780
  // 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";
27781
+ import { lstat as lstat4, readdir as readdir3, readFile as readFile3 } from "node:fs/promises";
27782
+ import { join as join5 } from "node:path";
27448
27783
  var decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
27449
- var info = (path3) => lstat3(path3).catch((error62) => {
27784
+ var info = (path3) => lstat4(path3).catch((error62) => {
27450
27785
  if (error62.code === "ENOENT") return void 0;
27451
27786
  throw error62;
27452
27787
  });
27453
27788
  async function recordFolders(root) {
27454
- return (await readdir3(join4(root, ...RECORDS_ROOT.split("/"))).catch((error62) => {
27789
+ return (await readdir3(join5(root, ...RECORDS_ROOT.split("/"))).catch((error62) => {
27455
27790
  if (error62.code === "ENOENT") return [];
27456
27791
  throw error62;
27457
27792
  })).filter(isRecordFolder);
@@ -27459,7 +27794,7 @@ async function recordFolders(root) {
27459
27794
  async function findRecord(root, id) {
27460
27795
  for (const folder of await recordFolders(root)) {
27461
27796
  const path3 = `${RECORDS_ROOT}/${folder}/${id}.md`;
27462
- if ((await info(join4(root, ...path3.split("/"))))?.isFile()) return path3;
27797
+ if ((await info(join5(root, ...path3.split("/"))))?.isFile()) return path3;
27463
27798
  }
27464
27799
  return void 0;
27465
27800
  }
@@ -27475,7 +27810,7 @@ async function readPendingRecords(root, run) {
27475
27810
  altered.push(path3);
27476
27811
  continue;
27477
27812
  }
27478
- const file2 = join4(root, ...path3.split("/"));
27813
+ const file2 = join5(root, ...path3.split("/"));
27479
27814
  const stat3 = await info(file2);
27480
27815
  if (!stat3?.isFile() || stat3.isSymbolicLink()) {
27481
27816
  problems.push(docProblem("PATH_UNSUPPORTED", path3));
@@ -27497,30 +27832,30 @@ async function readPendingRecords(root, run) {
27497
27832
  if (!id) continue;
27498
27833
  for (const folder of folders) {
27499
27834
  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 }));
27835
+ if (other !== path3 && !files.has(other) && (await info(join5(root, ...other.split("/"))))?.isFile()) problems.push(docProblem("DUPLICATE_RECORD_ID", path3, { id, other }));
27501
27836
  }
27502
27837
  }
27503
27838
  return { files, altered: altered.sort(), problems };
27504
27839
  }
27505
27840
 
27506
27841
  // 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";
27842
+ import { lstat as lstat5, readFile as readFile4, realpath, mkdir as mkdir2, open as open2, link, rename, unlink } from "node:fs/promises";
27843
+ import { join as join6 } from "node:path";
27509
27844
  import { randomUUID as randomUUID2 } from "node:crypto";
27510
27845
  var missing = (error62) => error62.code === "ENOENT";
27511
- var fileInfo = (path3) => lstat4(path3).catch((error62) => {
27846
+ var fileInfo = (path3) => lstat5(path3).catch((error62) => {
27512
27847
  if (missing(error62)) return void 0;
27513
27848
  throw error62;
27514
27849
  });
27515
27850
  async function configDirectory(root) {
27516
- const path3 = join5(root, ".gitifact");
27851
+ const path3 = join6(root, ".gitifact");
27517
27852
  const info5 = await fileInfo(path3);
27518
27853
  if (info5 && (!info5.isDirectory() || info5.isSymbolicLink())) throw new InitError("PATH_CONFLICT", t2("config.storeNotDirectory"));
27519
27854
  return info5;
27520
27855
  }
27521
27856
  async function readConfigFile(root) {
27522
27857
  await configDirectory(root);
27523
- const path3 = join5(root, ".gitifact", "config.json");
27858
+ const path3 = join6(root, ".gitifact", "config.json");
27524
27859
  const info5 = await fileInfo(path3);
27525
27860
  if (!info5) return void 0;
27526
27861
  if (!info5.isFile() || info5.isSymbolicLink() || info5.size > 65536) throw new InitError("PATH_CONFLICT", t2("config.fileShape"));
@@ -27537,7 +27872,7 @@ async function readConfigFile(root) {
27537
27872
  }
27538
27873
  }
27539
27874
  async function publishConfig(root, text4, recheck, beforePublish, replace = false) {
27540
- const directory = join5(root, ".gitifact");
27875
+ const directory = join6(root, ".gitifact");
27541
27876
  await configDirectory(root);
27542
27877
  await mkdir2(directory).catch((error62) => {
27543
27878
  if (error62.code !== "EEXIST") throw error62;
@@ -27550,7 +27885,7 @@ async function publishConfig(root, text4, recheck, beforePublish, replace = fals
27550
27885
  throw new InitError("INPUT_CHANGED", t2("config.directoryChanged"));
27551
27886
  }
27552
27887
  };
27553
- const temporary = join5(directory, ".init-" + randomUUID2() + ".tmp");
27888
+ const temporary = join6(directory, ".init-" + randomUUID2() + ".tmp");
27554
27889
  const handle = await open2(temporary, "wx", 384);
27555
27890
  const tempOwner = await handle.stat();
27556
27891
  try {
@@ -27567,8 +27902,8 @@ async function publishConfig(root, text4, recheck, beforePublish, replace = fals
27567
27902
  const bytes = await readFile4(temporary, "utf8");
27568
27903
  if (bytes !== text4) throw new InitError("INPUT_CHANGED", t2("config.temporaryChanged"));
27569
27904
  parseManagedConfig(bytes);
27570
- if (replace) await rename(temporary, join5(directory, "config.json"));
27571
- else await link(temporary, join5(directory, "config.json"));
27905
+ if (replace) await rename(temporary, join6(directory, "config.json"));
27906
+ else await link(temporary, join6(directory, "config.json"));
27572
27907
  } finally {
27573
27908
  await unchanged();
27574
27909
  const current2 = await fileInfo(temporary);
@@ -27577,11 +27912,11 @@ async function publishConfig(root, text4, recheck, beforePublish, replace = fals
27577
27912
  }
27578
27913
 
27579
27914
  // 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";
27915
+ import { lstat as lstat6, open as open3, readdir as readdir4, readFile as readFile5 } from "node:fs/promises";
27916
+ import { join as join7 } from "node:path";
27582
27917
  var INSTRUCTION_FILE_LIMIT = 500;
27583
27918
  var INSTRUCTION_TEXT_LIMIT = 512 * 1024;
27584
- var folderOf2 = (root, instructionPath) => join6(root, ...instructionPath.split("/").slice(0, -1));
27919
+ var folderOf3 = (root, instructionPath) => join7(root, ...instructionPath.split("/").slice(0, -1));
27585
27920
  async function listInstructionFiles(root, instructionPath) {
27586
27921
  const files = [];
27587
27922
  let limited = false;
@@ -27594,21 +27929,21 @@ async function listInstructionFiles(root, instructionPath) {
27594
27929
  return;
27595
27930
  }
27596
27931
  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 });
27932
+ if (entry2.isDirectory()) await visit2(join7(folder, entry2.name), path3 + "/", depth + 1);
27933
+ else if (entry2.isFile() && path3 !== INSTRUCTION_FILE) files.push({ path: path3, size: (await lstat6(join7(folder, entry2.name))).size });
27599
27934
  }
27600
27935
  }
27601
- await visit2(folderOf2(root, instructionPath), "", 0);
27936
+ await visit2(folderOf3(root, instructionPath), "", 0);
27602
27937
  return { files, limited };
27603
27938
  }
27604
27939
  var FRONTMATTER_WINDOW = 8 * 1024;
27605
27940
  async function describeFiles(root, instructionPath, files) {
27606
- const folder = folderOf2(root, instructionPath);
27941
+ const folder = folderOf3(root, instructionPath);
27607
27942
  return Promise.all(files.map(async (file2) => {
27608
27943
  if (!isInstructionReference(file2.path)) return file2;
27609
27944
  let handle;
27610
27945
  try {
27611
- handle = await open3(join6(folder, ...file2.path.split("/")), "r");
27946
+ handle = await open3(join7(folder, ...file2.path.split("/")), "r");
27612
27947
  const buffer = Buffer.alloc(Math.min(FRONTMATTER_WINDOW, file2.size));
27613
27948
  const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
27614
27949
  return { ...file2, ...parseInstructionFile(file2.path, buffer.subarray(0, bytesRead).toString("utf8")) };
@@ -27626,13 +27961,13 @@ async function readInstructionFiles(root, instructionPath) {
27626
27961
  async function readInstructionFile(root, instructionPath, path3) {
27627
27962
  const parts = path3.split("/");
27628
27963
  if (parts.some((p) => !p || p === "." || p === ".." || /[\\:\0]/.test(p))) return void 0;
27629
- let full = folderOf2(root, instructionPath);
27964
+ let full = folderOf3(root, instructionPath);
27630
27965
  for (const [index, part] of parts.entries()) {
27631
- full = join6(full, part);
27632
- const info5 = await lstat5(full).catch(() => void 0);
27966
+ full = join7(full, part);
27967
+ const info5 = await lstat6(full).catch(() => void 0);
27633
27968
  if (!info5 || info5.isSymbolicLink() || (index < parts.length - 1 ? !info5.isDirectory() : !info5.isFile())) return void 0;
27634
27969
  }
27635
- const size = (await lstat5(full)).size;
27970
+ const size = (await lstat6(full)).size;
27636
27971
  if (size > INSTRUCTION_TEXT_LIMIT) return { text: null, size, binary: false, tooLarge: true };
27637
27972
  const bytes = await readFile5(full);
27638
27973
  try {
@@ -27644,12 +27979,12 @@ async function readInstructionFile(root, instructionPath, path3) {
27644
27979
  }
27645
27980
 
27646
27981
  // 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";
27982
+ import { lstat as lstat7, readFile as readFile6 } from "node:fs/promises";
27983
+ import { join as join8 } from "node:path";
27649
27984
  var LIMIT = 1024 * 1024;
27650
27985
  async function readAgentsFile(root) {
27651
- const file2 = join7(root, "AGENTS.md");
27652
- const info5 = await lstat6(file2).catch(() => void 0);
27986
+ const file2 = join8(root, "AGENTS.md");
27987
+ const info5 = await lstat7(file2).catch(() => void 0);
27653
27988
  if (!info5?.isFile() || info5.size > LIMIT) return null;
27654
27989
  try {
27655
27990
  return new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(await readFile6(file2));
@@ -27726,8 +28061,8 @@ async function documentsAtHead(project, head, paths2) {
27726
28061
 
27727
28062
  // src/server/checkout/stamp.ts
27728
28063
  import { createHash as createHash2 } from "node:crypto";
27729
- import { lstat as lstat7 } from "node:fs/promises";
27730
- import { join as join8 } from "node:path";
28064
+ import { lstat as lstat8 } from "node:fs/promises";
28065
+ import { join as join9 } from "node:path";
27731
28066
  function createStampReader(root, inherited = process.env) {
27732
28067
  const runner = createGitRunner();
27733
28068
  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 +28074,7 @@ function createStampReader(root, inherited = process.env) {
27739
28074
  ]);
27740
28075
  const paths2 = status.split("\0").filter(Boolean).map((entry2) => entry2.slice(3)).filter((path3) => !path3.startsWith(".gitifact/cache/"));
27741
28076
  const seen = await Promise.all(paths2.map(async (path3) => {
27742
- const info5 = await lstat7(join8(root, ...path3.split("/"))).catch(() => void 0);
28077
+ const info5 = await lstat8(join9(root, ...path3.split("/"))).catch(() => void 0);
27743
28078
  return path3 + " " + (info5 ? info5.mtimeMs + ":" + info5.size : "-");
27744
28079
  }));
27745
28080
  return createHash2("sha256").update(head.trim() + "\n" + seen.sort().join("\n")).digest("hex").slice(0, 20);
@@ -27747,19 +28082,14 @@ function createStampReader(root, inherited = process.env) {
27747
28082
  }
27748
28083
 
27749
28084
  // 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
28085
  async function settled(reads) {
27756
28086
  const results = await Promise.allSettled(reads);
27757
28087
  const failure2 = results.find((r) => r.status === "rejected");
27758
28088
  if (failure2) throw failure2.reason;
27759
28089
  return results.map((r) => r.value);
27760
28090
  }
27761
- var folderOf3 = (path3) => path3.split("/").slice(0, 3).join("/");
27762
- function checkoutDocuments(states) {
28091
+ var folderOf4 = (path3) => path3.split("/").slice(0, 3).join("/");
28092
+ function checkoutDocuments(states, authors) {
27763
28093
  const documents = states.documents;
27764
28094
  const standing2 = (id) => {
27765
28095
  const previousPath = states.previousPath(id);
@@ -27772,30 +28102,45 @@ function checkoutDocuments(states) {
27772
28102
  return { id: s.id, ...target ? { title: target.title, path: target.path } : {}, ...s.note ? { note: s.note } : {} };
27773
28103
  };
27774
28104
  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
- }))
28105
+ const features = arranged.features.map(({ index, requirements, designs }) => {
28106
+ const entry2 = authors.get(folderOf4(index.path));
28107
+ return {
28108
+ id: index.id,
28109
+ path: index.path,
28110
+ title: index.title,
28111
+ description: index.description,
28112
+ body: index.body,
28113
+ ...standing2(index.id),
28114
+ 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) })),
28115
+ designs: designs.map((d) => ({
28116
+ id: d.id,
28117
+ path: d.path,
28118
+ title: d.title,
28119
+ description: d.description,
28120
+ order: d.order,
28121
+ body: d.body,
28122
+ requirements: d.requirements,
28123
+ sources: d.sources.map(source2),
28124
+ ...standing2(d.id)
28125
+ })),
28126
+ // Who committed to the feature folder, most first, and when it was last touched; nobody until first committed.
28127
+ contributors: entry2?.people ?? [],
28128
+ updatedAt: entry2?.latest ?? null
28129
+ };
28130
+ });
28131
+ const instructions2 = arranged.instructions.map((k) => ({
28132
+ id: k.id,
28133
+ name: k.name,
28134
+ path: k.path,
28135
+ title: k.title,
28136
+ description: k.description,
28137
+ body: k.body,
28138
+ ...standing2(k.id),
28139
+ updatedAt: authors.get(folderOf4(k.path))?.latest ?? null
27794
28140
  }));
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
28141
  return { features, instructions: instructions2, orphans: arranged.orphans };
27797
28142
  }
27798
- function createCheckoutReader(root, sessionId, cache, inherited = process.env) {
28143
+ function createCheckoutReader(root, cache, inherited = process.env) {
27799
28144
  const reader = storeReader(root);
27800
28145
  const runner = createGitRunner();
27801
28146
  const env = { ...inherited, GIT_OPTIONAL_LOCKS: "0", GIT_NO_REPLACE_OBJECTS: "1", GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" };
@@ -27808,87 +28153,36 @@ function createCheckoutReader(root, sessionId, cache, inherited = process.env) {
27808
28153
  };
27809
28154
  const pending = /* @__PURE__ */ new Map();
27810
28155
  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
28156
  async function read() {
27841
28157
  const raw = await readConfigFile(root);
27842
28158
  if (!raw) throw new StoreError(t2("specReader.schemaRequired"));
27843
28159
  parseManagedConfig(raw);
27844
28160
  const head = await readHead();
27845
28161
  const stamp = await readStamp();
27846
- const [current2, dirty, authors, everyone] = await settled([
28162
+ const [current2, dirty, authors, people] = await settled([
27847
28163
  cache.documents.list(),
27848
28164
  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("")
28165
+ // Who touched each folder and who committed at all, counted over the commit log in the cache.
28166
+ head ? cache.log.folders(head) : Promise.resolve(/* @__PURE__ */ new Map()),
28167
+ head ? cache.log.contributors(head) : Promise.resolve([])
27852
28168
  ]);
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
28169
  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 }))];
28170
+ const { features, instructions: instructions2, orphans } = checkoutDocuments(states, authors);
28171
+ const problems = [...current2.problems, ...orphans.map((feature2) => docProblem("FEATURE_INDEX_REQUIRED", `.gitifact/spec/${feature2}/index.md`, { feature: feature2 }))];
27873
28172
  if (await readHead() !== head) throw new StoreError(t2("specReader.projectChanged"));
27874
- const checkout = browserSpecsV7.parse({
27875
- contract: "browser-specs",
27876
- version: 7,
27877
- sessionId,
28173
+ return {
27878
28174
  head,
27879
- observedAt: (/* @__PURE__ */ new Date()).toISOString(),
27880
28175
  stamp,
28176
+ observedAt: (/* @__PURE__ */ new Date()).toISOString(),
27881
28177
  working: head ? !!dirty.trim() : features.length > 0 || instructions2.length > 0,
27882
28178
  features,
27883
28179
  instructions: instructions2,
27884
- agents: agents2,
27885
28180
  problems,
27886
- contributors: [...people.values()].sort((a, b) => b.commits - a.commits),
27887
- contributorsLimited: lines.length > 1e4
27888
- });
27889
- return { checkout, head };
28181
+ people,
28182
+ folders: authors
28183
+ };
27890
28184
  }
27891
- return () => {
28185
+ const base = () => {
27892
28186
  const language = getLanguage();
27893
28187
  let value = pending.get(language);
27894
28188
  if (!value) {
@@ -27897,10 +28191,37 @@ function createCheckoutReader(root, sessionId, cache, inherited = process.env) {
27897
28191
  }
27898
28192
  return value;
27899
28193
  };
28194
+ return {
28195
+ base,
28196
+ /** Every instruction by name with the other files of its folder, and AGENTS.md: few enough to read whole. */
28197
+ async instructions() {
28198
+ const checkout = await base();
28199
+ const instructions2 = await Promise.all([...checkout.instructions].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map(async (instruction2) => {
28200
+ const { files, limited } = instruction2.state === "deleted" ? { files: [], limited: false } : await readInstructionFiles(root, instruction2.path);
28201
+ return { ...instruction2, files, filesLimited: limited };
28202
+ }));
28203
+ const agentsText = await readAgentsFile(root);
28204
+ return { instructions: instructions2, agents: agentsText === null ? null : { path: "AGENTS.md", body: agentsText, updatedAt: checkout.folders.get("AGENTS.md")?.latest ?? null } };
28205
+ }
28206
+ };
28207
+ }
28208
+
28209
+ // src/queries/paging.ts
28210
+ var PAGE_SIZE = 20;
28211
+ function pageOf(rows, keyOf, request) {
28212
+ let start = 0;
28213
+ if (request.after !== void 0) {
28214
+ const at = rows.findIndex((row) => keyOf(row) === request.after);
28215
+ if (at < 0) return void 0;
28216
+ start = at + 1;
28217
+ }
28218
+ const size = request.all ? rows.length : request.limit ?? PAGE_SIZE;
28219
+ const shown = rows.slice(start, start + size);
28220
+ const more = start + shown.length < rows.length && shown.length > 0;
28221
+ return { rows: shown, total: rows.length, next: more ? keyOf(shown[shown.length - 1]) : null };
27900
28222
  }
27901
28223
 
27902
28224
  // src/server/commit/commit-files.ts
27903
- var LIST_LIMIT = 500;
27904
28225
  var TEXT_LIMIT = 512 * 1024;
27905
28226
  var KEPT = 200;
27906
28227
  var statusOf = (code) => code.startsWith("R") ? "renamed" : code === "A" ? "added" : code === "D" ? "deleted" : "modified";
@@ -27956,10 +28277,15 @@ function createCommitFiles(git) {
27956
28277
  return git.run(["cat-file", "blob", `${rev}:${path3}`]);
27957
28278
  }
27958
28279
  return {
27959
- /** The first `LIST_LIMIT` source files and how many there are; undefined for a commit the repository lacks. */
27960
- async files(commit) {
28280
+ /**
28281
+ * A page of the source files after the file `after` (a path) and how many there are in all: undefined for a commit
28282
+ * the repository lacks, null for a cursor the commit does not have.
28283
+ */
28284
+ async files(commit, request) {
27961
28285
  const all = await list(commit);
27962
- return all && { ...all, files: all.files.slice(0, LIST_LIMIT) };
28286
+ if (!all) return void 0;
28287
+ const page = pageOf(all.files, (f) => f.path, request);
28288
+ return page ? { ...all, files: page.rows, next: page.next } : null;
27963
28289
  },
27964
28290
  /** One listed file on both sides; undefined when the commit did not change that path. */
27965
28291
  async file(commit, path3) {
@@ -28041,31 +28367,200 @@ async function workingChange2(source2, id) {
28041
28367
  };
28042
28368
  }
28043
28369
 
28370
+ // src/queries/search.ts
28371
+ var SEARCH_GROUPS = ["feature", "requirement", "design", "instruction", "record", "commit"];
28372
+ var FIRST = 5;
28373
+ var RECENT = 6;
28374
+ var place2 = (path3) => path3.replace(/^\.gitifact\//, "");
28375
+ var folderOf5 = (path3) => path3.split("/").slice(0, 3).join("/");
28376
+ async function searchRecords(source2, request) {
28377
+ const head = request.head ?? await source2.head();
28378
+ const query = request.q.trim().toLowerCase();
28379
+ if (!query) return [await recent(source2, head)];
28380
+ const size = request.group ? request.limit ?? 20 : FIRST;
28381
+ const one = async (group) => {
28382
+ 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);
28383
+ return found && { group, ...found };
28384
+ };
28385
+ const groups = await Promise.all((request.group ? [request.group] : SEARCH_GROUPS).map(one));
28386
+ if (groups.some((g) => !g)) return void 0;
28387
+ return groups.filter((g) => request.group || g.total > 0);
28388
+ }
28389
+ var empty = () => ({ total: 0, next: null, hits: [] });
28390
+ async function recent(source2, head) {
28391
+ const [{ documents }, folders] = await Promise.all([source2.cache.documents.list(), head ? source2.cache.log.folders(head) : /* @__PURE__ */ new Map()]);
28392
+ 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 }) => ({
28393
+ id: doc.id,
28394
+ kind: doc.kind,
28395
+ title: doc.title,
28396
+ where: place2(doc.path),
28397
+ line: doc.description,
28398
+ ...doc.kind === "feature" ? { featureId: doc.id } : {}
28399
+ }));
28400
+ return { group: "recent", total: hits.length, next: null, hits };
28401
+ }
28402
+
28403
+ // src/queries/checkout.ts
28404
+ var SHOWN_REQUIREMENTS = 12;
28405
+ function checkoutIndex(base) {
28406
+ return {
28407
+ features: base.features.map((f) => ({
28408
+ id: f.id,
28409
+ path: f.path,
28410
+ title: f.title,
28411
+ state: f.state,
28412
+ requirements: f.requirements.map((r) => ({ id: r.id, path: r.path, title: r.title, description: r.description })),
28413
+ designs: f.designs.map((d) => ({ id: d.id, path: d.path, title: d.title }))
28414
+ })),
28415
+ instructions: base.instructions.map((k) => ({ id: k.id, name: k.name, path: k.path, title: k.title })),
28416
+ people: base.people.map((p) => ({ name: p.name, email: p.email }))
28417
+ };
28418
+ }
28419
+ var acceptanceHeading = /^###\s+(?:수용 조건|acceptance criteria)\s*$/i;
28420
+ function acceptanceCount(body2) {
28421
+ const lines = body2.replace(/\r\n/g, "\n").split("\n");
28422
+ const start = lines.findIndex((line2) => acceptanceHeading.test(line2.trim()));
28423
+ if (start < 0) return null;
28424
+ let count2 = 0;
28425
+ for (const line2 of lines.slice(start + 1)) {
28426
+ if (/^#{1,6}\s/.test(line2)) break;
28427
+ if (/^\d+\.\s/.test(line2)) count2++;
28428
+ }
28429
+ return count2 || null;
28430
+ }
28431
+ function listFeatures(base, filter, request) {
28432
+ const query = filter.q?.trim().toLowerCase();
28433
+ const named = (f) => !query || (f.title + " " + f.id).toLowerCase().includes(query);
28434
+ const key = filter.sort ?? "updated";
28435
+ const opens = { title: "asc", requirements: "desc", updated: "desc" };
28436
+ const way = (filter.dir ?? opens[key]) === "asc" ? 1 : -1;
28437
+ 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));
28438
+ const page = pageOf(groups, (g) => g.feature.id, request);
28439
+ if (!page) return void 0;
28440
+ const features = page.rows.map(({ feature: f, requirements }) => {
28441
+ const designed = new Set(f.designs.flatMap((d) => d.requirements));
28442
+ return {
28443
+ id: f.id,
28444
+ path: f.path,
28445
+ title: f.title,
28446
+ description: f.description,
28447
+ state: f.state,
28448
+ designs: f.designs.length,
28449
+ contributors: f.contributors,
28450
+ updatedAt: f.updatedAt,
28451
+ requirementCount: f.requirements.length,
28452
+ requirements: requirements.slice(0, SHOWN_REQUIREMENTS).map((r) => ({
28453
+ id: r.id,
28454
+ title: r.title,
28455
+ description: r.description,
28456
+ state: r.state,
28457
+ acceptance: acceptanceCount(r.body),
28458
+ designed: designed.has(r.id)
28459
+ })),
28460
+ hidden: Math.max(0, requirements.length - SHOWN_REQUIREMENTS)
28461
+ };
28462
+ });
28463
+ return {
28464
+ total: page.total,
28465
+ all: base.features.length,
28466
+ requirements: groups.reduce((sum, g) => sum + g.requirements.length, 0),
28467
+ mostRequirements: Math.max(0, ...base.features.map((f) => f.requirements.length)),
28468
+ next: page.next,
28469
+ features
28470
+ };
28471
+ }
28472
+ var featureOf = (base, id) => base.features.find((f) => f.id === id);
28473
+ function listContributors(base, q, request) {
28474
+ const query = q?.trim().toLowerCase();
28475
+ const people = base.people.filter((p) => !query || (p.name + " " + p.email).toLowerCase().includes(query));
28476
+ const page = pageOf(people, (p) => p.email, request);
28477
+ if (!page) return void 0;
28478
+ const touched = (email3) => base.features.filter((f) => f.contributors.some((c) => c.email === email3)).length;
28479
+ return { total: page.total, next: page.next, people: page.rows.map((p) => ({ ...p, features: touched(p.email) })) };
28480
+ }
28481
+ function contributorOf(base, email3) {
28482
+ const person = base.people.find((p) => p.email === email3);
28483
+ if (!person) return void 0;
28484
+ const features = base.features.flatMap((f) => {
28485
+ const share = f.contributors.find((c) => c.email === email3);
28486
+ return share ? [{ id: f.id, title: f.title, commits: share.commits, requirements: f.requirements.length }] : [];
28487
+ });
28488
+ return { person, features };
28489
+ }
28490
+ function topContributors(people) {
28491
+ const rest = people.slice(3);
28492
+ return { total: people.length, top: people.slice(0, 3), rest: { count: rest.length, commits: rest.reduce((sum, p) => sum + p.commits, 0) } };
28493
+ }
28494
+
28044
28495
  // src/server/routes/record-routes.ts
28045
28496
  var PAGE = 20;
28046
28497
  function recordRoutes(root, sessionId, env) {
28047
28498
  const git = storeReader(root);
28048
28499
  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);
28500
+ const checkout = createCheckoutReader(root, cache, env);
28050
28501
  const readStamp = createStampReader(root, env);
28051
28502
  const commitFiles = createCommitFiles(git);
28503
+ const source2 = { head: async () => (await git.baseline()).head, reader: git, cache };
28052
28504
  const working2 = { head: async () => (await git.baseline()).head, cache, pendingRecords: () => readPendingRecords(root, (args) => git.run(args)) };
28053
28505
  const unreadable2 = () => t2("server.specsUnreadable");
28054
- const cursorGone = () => new HttpError(404, "NOT_FOUND", t2("server.cursorNotFound"));
28506
+ const cursorGone2 = () => new HttpError(404, "NOT_FOUND", t2("server.cursorNotFound"));
28055
28507
  return [
28056
- route({ method: "GET", path: "/api/v1/specs", session: true, unreadable: unreadable2, handle: async () => ok((await readCheckout()).checkout) }),
28508
+ // The checkout a part at a time, each shaped by the query layer (`queries/checkout.ts`): the frame every screen reads
28509
+ // first, the feature list and one feature, the instructions, and the contributors.
28510
+ route({ method: "GET", path: "/api/v1/checkout", session: true, unreadable: unreadable2, handle: async () => {
28511
+ const base = await checkout.base();
28512
+ return ok(browserCheckoutV1.parse({
28513
+ contract: "browser-checkout",
28514
+ version: 1,
28515
+ sessionId,
28516
+ head: base.head,
28517
+ observedAt: base.observedAt,
28518
+ stamp: base.stamp,
28519
+ working: base.working,
28520
+ problems: base.problems,
28521
+ index: checkoutIndex(base)
28522
+ }));
28523
+ } }),
28524
+ route({ method: "GET", path: "/api/v1/features", session: true, query: browserFeaturesQueryV1, unreadable: unreadable2, handle: async ({ query }) => {
28525
+ const page = listFeatures(await checkout.base(), query, { after: query.after, limit: query.limit ?? PAGE });
28526
+ if (!page) throw cursorGone2();
28527
+ return ok(browserFeaturesV1.parse({ contract: "browser-features", version: 1, sessionId, ...page }));
28528
+ } }),
28529
+ route({ method: "GET", path: "/api/v1/feature", session: true, query: browserFeatureQueryV1, unreadable: unreadable2, handle: async ({ query }) => {
28530
+ const feature2 = featureOf(await checkout.base(), query.id);
28531
+ if (!feature2) throw new HttpError(404, "NOT_FOUND", t2("server.featureNotFound"));
28532
+ return ok(browserFeatureV1.parse({ contract: "browser-feature", version: 1, sessionId, feature: feature2 }));
28533
+ } }),
28534
+ 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() })) }),
28535
+ route({ method: "GET", path: "/api/v1/contributors", session: true, query: browserContributorsQueryV1, unreadable: unreadable2, handle: async ({ query }) => {
28536
+ const page = listContributors(await checkout.base(), query.q, { after: query.after, limit: query.limit ?? PAGE });
28537
+ if (!page) throw cursorGone2();
28538
+ return ok(browserContributorsV1.parse({ contract: "browser-contributors", version: 1, sessionId, ...page }));
28539
+ } }),
28540
+ route({ method: "GET", path: "/api/v1/contributor", session: true, query: browserContributorQueryV1, unreadable: unreadable2, handle: async ({ query }) => {
28541
+ const found = contributorOf(await checkout.base(), query.email);
28542
+ if (!found) throw new HttpError(404, "NOT_FOUND", t2("server.contributorNotFound"));
28543
+ return ok(browserContributorV1.parse({ contract: "browser-contributor", version: 1, sessionId, ...found }));
28544
+ } }),
28057
28545
  // Whether the screen is behind, asked when the reader comes back to the tab; cheap enough to ask every time.
28058
28546
  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
28547
  route({ method: "GET", path: "/api/v1/history", session: true, query: browserHistoryQueryV4, unreadable: unreadable2, handle: async ({ query }) => {
28060
28548
  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();
28549
+ if (!page) throw cursorGone2();
28062
28550
  return ok(browserHistoryV6.parse({ contract: "browser-history", version: 6, sessionId, head: query.head, ...page }));
28063
28551
  } }),
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) })) }),
28552
+ route({ method: "GET", path: "/api/v1/history/summary", session: true, query: browserHistorySummaryQueryV1, unreadable: unreadable2, handle: async ({ query }) => ok(browserHistorySummaryV5.parse({
28553
+ contract: "browser-history-summary",
28554
+ version: 5,
28555
+ sessionId,
28556
+ head: query.head,
28557
+ ...await cache.history.summary(query.head),
28558
+ people: topContributors(await cache.log.contributors(query.head))
28559
+ })) }),
28065
28560
  // 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();
28561
+ route({ method: "GET", path: "/api/v1/commit", session: true, query: browserCommitQueryV3, unreadable: unreadable2, handle: async ({ query }) => {
28562
+ const page = await cache.history.commitChanges(query.commit, query.after, query.limit ?? PAGE, query.record);
28563
+ if (!page) throw cursorGone2();
28069
28564
  const first = page.events[0];
28070
28565
  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
28566
  if (!first && line2.length < 5) throw new HttpError(404, "NOT_FOUND", t2("server.commitNotFound"));
@@ -28093,9 +28588,9 @@ function recordRoutes(root, sessionId, env) {
28093
28588
  } }),
28094
28589
  // A record's page names the record; which commit added it is read from the history of the HEAD the reader is on.
28095
28590
  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 }));
28591
+ const found = await cache.history.recordOf(query.head, query.id);
28592
+ if (!found) throw new HttpError(404, "NOT_FOUND", t2("server.recordNotFound"));
28593
+ return ok(browserRecordV2.parse({ contract: "browser-record", version: 2, sessionId, head: query.head, id: query.id, ...found }));
28099
28594
  } }),
28100
28595
  // What is not committed yet, worked out again on every request: the uncommitted entry of the records list.
28101
28596
  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 +28600,11 @@ function recordRoutes(root, sessionId, env) {
28105
28600
  return ok(browserWorkingChangeV1.parse({ contract: "browser-working-change", version: 1, sessionId, ...change2 }));
28106
28601
  } }),
28107
28602
  // 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 }));
28603
+ route({ method: "GET", path: "/api/v1/commit/files", session: true, query: browserCommitFilesQueryV2, unreadable: unreadable2, handle: async ({ query }) => {
28604
+ const found = await commitFiles.files(query.commit, { after: query.after, limit: query.limit ?? PAGE });
28605
+ if (found === void 0) throw new HttpError(404, "NOT_FOUND", t2("server.commitNotFound"));
28606
+ if (found === null) throw cursorGone2();
28607
+ return ok(browserCommitFilesV2.parse({ contract: "browser-commit-files", version: 2, sessionId, commit: found.commit, total: found.total, next: found.next, files: found.files }));
28112
28608
  } }),
28113
28609
  route({ method: "GET", path: "/api/v1/commit/file", session: true, query: browserCommitFileQueryV1, unreadable: unreadable2, handle: async ({ query }) => {
28114
28610
  const found = await commitFiles.file(query.commit, query.path);
@@ -28122,21 +28618,26 @@ function recordRoutes(root, sessionId, env) {
28122
28618
  if (!found) throw new HttpError(404, "NOT_FOUND", t2("server.instructionFileNotFound"));
28123
28619
  return ok(browserInstructionFileV1.parse({ contract: "browser-instruction-file", version: 1, sessionId, id: query.id, path: query.path, ...found }));
28124
28620
  } }),
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) })) })
28621
+ // The search box's groups as the query layer works them out (`queries/search.ts`); the browser only draws them.
28622
+ route({ method: "GET", path: "/api/v1/search", session: true, query: browserSearchQueryV2, unreadable: unreadable2, handle: async ({ query }) => {
28623
+ const groups = await searchRecords(source2, query);
28624
+ if (!groups) throw cursorGone2();
28625
+ return ok(browserSearchV3.parse({ contract: "browser-search", version: 3, sessionId, query: query.q, groups }));
28626
+ } })
28126
28627
  ];
28127
28628
  }
28128
28629
 
28129
28630
  // 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";
28631
+ import { lstat as lstat9, readFile as readFile7 } from "node:fs/promises";
28632
+ import { join as join10 } from "node:path";
28132
28633
  function assetRoutes(root) {
28133
28634
  return [
28134
28635
  route({ method: "GET", path: "/api/v1/assets/*", session: false, handle: async ({ path: path3, response }) => {
28135
28636
  const relative = path3.slice("/api/v1/assets/".length);
28136
28637
  const assetPath = ASSETS_DIR + "/" + relative;
28137
28638
  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);
28639
+ const file2 = join10(root, ...assetPath.split("/"));
28640
+ const stat3 = await lstat9(file2).catch(() => void 0);
28140
28641
  if (!stat3?.isFile() || stat3.isSymbolicLink() || stat3.size > 20 * 1024 * 1024) throw new HttpError(404, "NOT_FOUND", t2("server.assetNotFound"));
28141
28642
  const bytes = await readFile7(file2);
28142
28643
  const extension = assetExtension(relative);
@@ -28286,13 +28787,13 @@ async function runBrowser(options, version2) {
28286
28787
 
28287
28788
  // src/commands/spec-init.ts
28288
28789
  import { readdir as readdir5 } from "node:fs/promises";
28289
- import { join as join13 } from "node:path";
28790
+ import { join as join14 } from "node:path";
28290
28791
 
28291
28792
  // src/adapters/git/init-repository.ts
28292
28793
  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) => {
28794
+ import { lstat as lstat10, readFile as readFile9 } from "node:fs/promises";
28795
+ import { join as join11 } from "node:path";
28796
+ var markerPresent = (path3) => lstat10(path3).then(() => true, (error62) => {
28296
28797
  if (error62.code === "ENOENT") return false;
28297
28798
  throw error62;
28298
28799
  });
@@ -28315,7 +28816,7 @@ function initRepository(cwd, inherited = process.env) {
28315
28816
  const gitDir = locations.length === 2 ? locations[0] : decodeGitLine(await git(["rev-parse", "--absolute-git-dir"], root));
28316
28817
  const indexPath = locations.length === 2 ? locations[1] : decodeGitLine(await git(["rev-parse", "--path-format=absolute", "--git-path", "index"], root));
28317
28818
  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 }));
28819
+ if (await markerPresent(join11(gitDir, marker2))) throw new InitError("GIT_OPERATION_IN_PROGRESS", t2("git.operationInProgress", { marker: marker2 }));
28319
28820
  }
28320
28821
  if (state2.changes.some((change2) => change2.kind === "unmerged")) throw new InitError("GIT_OPERATION_IN_PROGRESS", t2("git.resolveConflicts"));
28321
28822
  const index = await readFile9(indexPath).catch((error62) => {
@@ -28353,14 +28854,14 @@ function initRepository(cwd, inherited = process.env) {
28353
28854
 
28354
28855
  // src/adapters/filesystem/document-file.ts
28355
28856
  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";
28857
+ import { lstat as lstat11, mkdir as mkdir3, writeFile as writeFile2 } from "node:fs/promises";
28858
+ import { join as join12 } from "node:path";
28358
28859
  var generateId = (prefix) => prefix + "-" + [...randomBytes(10)].map((n) => "abcdefghijklmnopqrstuvwxyz234567"[n & 31]).join("");
28359
28860
  async function createDocumentFile(root, path3, text4) {
28360
28861
  const parts = path3.split("/");
28361
28862
  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) => {
28863
+ const folder = join12(root, ...parts.slice(0, i));
28864
+ const info5 = await lstat11(folder).catch((error62) => {
28364
28865
  if (error62.code === "ENOENT") return void 0;
28365
28866
  throw error62;
28366
28867
  });
@@ -28370,7 +28871,7 @@ async function createDocumentFile(root, path3, text4) {
28370
28871
  }
28371
28872
  if (!info5.isDirectory() || info5.isSymbolicLink()) throw Object.assign(new Error(folder), { code: "ENOTDIR" });
28372
28873
  }
28373
- await writeFile2(join11(root, ...parts), text4, { flag: "wx" });
28874
+ await writeFile2(join12(root, ...parts), text4, { flag: "wx" });
28374
28875
  }
28375
28876
  var LINE_ENDINGS_PATH = ".gitifact/.gitattributes";
28376
28877
  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 +28887,14 @@ async function writeLineEndings(root) {
28386
28887
 
28387
28888
  // src/adapters/filesystem/managed-file.ts
28388
28889
  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";
28890
+ import { join as join13, dirname } from "node:path";
28390
28891
  import { randomUUID as randomUUID4 } from "node:crypto";
28391
28892
  var DEFAULT_LIMIT = 1024 * 1024;
28392
28893
  async function managedPath(root, path3, create = false) {
28393
28894
  let full = root;
28394
28895
  const parts = path3.split("/");
28395
28896
  for (let i = 0; i < parts.length; i++) {
28396
- full = join12(full, parts[i]);
28897
+ full = join13(full, parts[i]);
28397
28898
  let info5 = await fileInfo(full);
28398
28899
  if (!info5 && create && i < parts.length - 1) {
28399
28900
  await mkdir4(full).catch((error62) => {
@@ -28441,7 +28942,7 @@ async function managedWrite(root, path3, previous, next, recheck) {
28441
28942
  if (previous !== null) await unlink2(full);
28442
28943
  return;
28443
28944
  }
28444
- const temp = join12(directory, ".gitifact-managed-" + randomUUID4() + ".tmp");
28945
+ const temp = join13(directory, ".gitifact-managed-" + randomUUID4() + ".tmp");
28445
28946
  const handle = await open4(temp, "wx", 384);
28446
28947
  const tempOwner = await handle.stat();
28447
28948
  try {
@@ -28546,15 +29047,15 @@ function boilerplateFor(path3) {
28546
29047
  return "# " + name2 + "\n\nProject-specific guidance for AI coding agents.\n";
28547
29048
  }
28548
29049
  var readBundledBlock = (lang) => readFile12(new URL("./i18n/" + lang + "/block.md", import.meta.url), "utf8");
28549
- async function renderAgentBlock(version2, controls = {}, lang = getLanguage()) {
29050
+ async function renderAgentBlock(controls = {}, lang = getLanguage()) {
28550
29051
  const body2 = await (controls.readBlock ?? readBundledBlock)(lang);
28551
- const filled = body2.trimEnd().replace(/\{(version|language|topics)\}/g, (_whole, name2) => name2 === "version" ? version2 : name2 === "language" ? lang : guideTopics.join(", "));
28552
- return AGENT_START + "\n" + filled + "\n" + AGENT_END;
29052
+ return AGENT_START + "\n" + body2.trimEnd().replace(/\{topics\}/g, guideTopics.join(", ")) + "\n" + AGENT_END;
28553
29053
  }
28554
- function parseAgentBlock(text4) {
28555
- const match = /^gitifact v(\S+)(?: · ([a-z][a-z-]*))? · .*schemaVersion (\d+)$/m.exec(text4);
28556
- if (!match) return null;
28557
- return { version: match[1], language: match[2] ?? "ko", schemaVersion: Number(match[3]) };
29054
+ function legacyBlockLanguage(text4) {
29055
+ const match = /^gitifact v\S+(?: · ([a-z][a-z-]*))? · .*schemaVersion \d+$/m.exec(text4);
29056
+ if (!match) return void 0;
29057
+ const language = match[1] ?? "ko";
29058
+ return language === "ko" || language === "en" ? language : void 0;
28558
29059
  }
28559
29060
  function findBlock(text4) {
28560
29061
  const start = text4.indexOf(AGENT_START);
@@ -28616,14 +29117,26 @@ function removeBlock(text4, boilerplate) {
28616
29117
  }
28617
29118
 
28618
29119
  // src/commands/agent-docs.ts
28619
- var skippedAgentDocs = { mode: "skip", paths: [], writes: [], missing: [] };
29120
+ var skippedAgentDocs = (language = explicitLanguage() ?? getLanguage()) => ({ mode: "skip", paths: [], writes: [], missing: [], language });
29121
+ var blockOf = (text4) => {
29122
+ try {
29123
+ const found = findBlock(text4);
29124
+ return found ? text4.slice(found.start, found.end) : void 0;
29125
+ } catch {
29126
+ return void 0;
29127
+ }
29128
+ };
28620
29129
  async function planAgentDocs(root, options) {
28621
- if (options.skip) return skippedAgentDocs;
29130
+ if (options.skip) return skippedAgentDocs(explicitLanguage() ?? options.language ?? getLanguage());
28622
29131
  const existing = /* @__PURE__ */ new Map();
28623
29132
  for (const path3 of candidatePaths) {
28624
29133
  const text4 = await managedRead(root, path3);
28625
29134
  if (text4 !== null) existing.set(path3, text4);
28626
29135
  }
29136
+ const language = explicitLanguage() ?? options.language ?? [...existing.values()].map((text4) => {
29137
+ const block2 = blockOf(text4);
29138
+ return block2 === void 0 ? void 0 : legacyBlockLanguage(block2);
29139
+ }).find(Boolean) ?? getLanguage();
28627
29140
  const writes = [];
28628
29141
  const paths2 = [];
28629
29142
  if (options.remove) {
@@ -28639,19 +29152,14 @@ async function planAgentDocs(root, options) {
28639
29152
  paths2.push(CLAUDE_WRAPPER_PATH);
28640
29153
  writes.push({ path: CLAUDE_WRAPPER_PATH, previous: wrapper, next: null });
28641
29154
  }
28642
- return { mode: "remove", paths: paths2, writes, missing: [] };
29155
+ return { mode: "remove", paths: paths2, writes, missing: [], language };
28643
29156
  }
28644
- const blocks = /* @__PURE__ */ new Map();
28645
- const blockFor = async (previous) => {
28646
- const stored = previous ? parseAgentBlock(previous)?.language : void 0;
28647
- const language = explicitLanguage() ?? (stored === "ko" || stored === "en" ? stored : getLanguage());
28648
- if (!blocks.has(language)) blocks.set(language, await renderAgentBlock(options.version, options, language));
28649
- return blocks.get(language);
28650
- };
29157
+ let block;
29158
+ const blockFor = async () => block ??= await renderAgentBlock(options, language);
28651
29159
  const { inject, create } = options.onlyExisting ? { inject: candidatePaths.filter((path3) => existing.has(path3) && findBlock(existing.get(path3)) && !isWrapperFile(existing.get(path3))), create: null } : resolveAgentPaths(options.agent, existing);
28652
29160
  for (const path3 of inject) {
28653
29161
  const previous = existing.get(path3);
28654
- const next = injectBlock(previous, await blockFor(previous), boilerplateFor(path3));
29162
+ const next = injectBlock(previous, await blockFor(), boilerplateFor(path3));
28655
29163
  paths2.push(path3);
28656
29164
  if (next !== previous) writes.push({ path: path3, previous, next });
28657
29165
  }
@@ -28671,7 +29179,7 @@ async function planAgentDocs(root, options) {
28671
29179
  if (paths2.includes(path3) || !isWrapperFile(text4) || !findBlock(text4)) continue;
28672
29180
  writes.push({ path: path3, previous: text4, next: removeBlock(text4, boilerplateFor(path3)) });
28673
29181
  }
28674
- return { mode: "install", paths: paths2, writes, missing: missing2 };
29182
+ return { mode: "install", paths: paths2, writes, missing: missing2, language };
28675
29183
  }
28676
29184
  async function applyAgentDocs(root, plan, recheck) {
28677
29185
  for (const write of plan.writes) await managedWrite(root, write.path, write.previous, write.next, recheck);
@@ -28720,12 +29228,32 @@ async function resolveUpdate(current2, fetchLatest, signal, timeoutMs = updateCh
28720
29228
  var npmGlobalInstall = (version2) => "npm install -g gitifact@" + version2;
28721
29229
  var npxUpdate = (version2) => "npx --yes gitifact@" + version2 + " update";
28722
29230
 
29231
+ // src/commands/project-settings.ts
29232
+ function settleConfig(config2, version2, language) {
29233
+ const cli = !CLI_VERSION_PATTERN.test(version2) || config2.cli !== void 0 && !isNewerRelease(version2, config2.cli) ? config2.cli : version2;
29234
+ const next = { ...config2, ...cli === void 0 ? {} : { cli }, language };
29235
+ return { config: next, changed: next.cli !== config2.cli || next.language !== config2.language };
29236
+ }
29237
+ function projectSettings(text4) {
29238
+ if (text4 === void 0) return {};
29239
+ try {
29240
+ const value = JSON.parse(text4);
29241
+ return {
29242
+ ...typeof value.cli === "string" && CLI_VERSION_PATTERN.test(value.cli) ? { cli: value.cli } : {},
29243
+ ...value.language === "ko" || value.language === "en" ? { language: value.language } : {}
29244
+ };
29245
+ } catch {
29246
+ return {};
29247
+ }
29248
+ }
29249
+
28723
29250
  // src/commands/spec-init.ts
28724
29251
  async function initializeSpecProject(cwd, dryRun = false, env = process.env, beforePublish, agentDocs, update = disabledUpdate) {
28725
29252
  const repo = initRepository(cwd, env);
28726
29253
  const first = await repo.inspect();
28727
29254
  const root = first.state.repository.rootPath;
28728
- const docsPlan = agentDocs ? await planAgentDocs(root, agentDocs) : skippedAgentDocs;
29255
+ const language = projectSettings(await readConfigFile(root).catch(() => void 0)).language;
29256
+ let docsPlan = agentDocs ? await planAgentDocs(root, { ...agentDocs, language }) : skippedAgentDocs(language);
28729
29257
  const result = async (config3, outcome, lineEndings = false) => {
28730
29258
  const checked = await update;
28731
29259
  return projectInitV8.parse({
@@ -28746,16 +29274,25 @@ async function initializeSpecProject(cwd, dryRun = false, env = process.env, bef
28746
29274
  const existing = async (text5) => {
28747
29275
  const config3 = parseManagedConfig(text5);
28748
29276
  await repo.validateBaseline(config3, root, first.state.head.commit, first.state.repository.objectFormat);
29277
+ let current2 = text5;
28749
29278
  const unchanged = async () => {
28750
- if ((await repo.inspect()).stamp !== first.stamp || await readConfigFile(root) !== text5) throw new InitError("INPUT_CHANGED", t2("init.inputChanged"));
29279
+ if ((await repo.inspect()).stamp !== first.stamp || await readConfigFile(root) !== current2) throw new InitError("INPUT_CHANGED", t2("init.inputChanged"));
28751
29280
  };
28752
29281
  await unchanged();
29282
+ if (agentDocs && config3.cli && isNewerRelease(config3.cli, agentDocs.version) && docsPlan.mode === "install") docsPlan = skippedAgentDocs(docsPlan.language);
28753
29283
  if (dryRun) return result(config3, "already-initialized");
29284
+ const settled2 = agentDocs ? settleConfig(config3, agentDocs.version, docsPlan.language) : { config: config3, changed: false };
29285
+ if (settled2.changed) {
29286
+ const next = formatManagedConfig(settled2.config, text5);
29287
+ await publishConfig(root, next, unchanged, void 0, true);
29288
+ current2 = next;
29289
+ }
28754
29290
  await applyAgentDocs(root, docsPlan, unchanged);
28755
- return result(config3, "already-initialized", await writeLineEndings(root));
29291
+ return result(settled2.config, "already-initialized", await writeLineEndings(root));
28756
29292
  };
28757
- const config2 = { schemaVersion: SCHEMA_VERSION, baseline: first.state.head.commit ? { kind: "commit", objectFormat: first.state.repository.objectFormat, commit: first.state.head.commit } : { kind: "empty" } };
28758
- const text4 = JSON.stringify(config2, null, 2) + "\n";
29293
+ const base = { schemaVersion: SCHEMA_VERSION, baseline: first.state.head.commit ? { kind: "commit", objectFormat: first.state.repository.objectFormat, commit: first.state.head.commit } : { kind: "empty" } };
29294
+ const config2 = agentDocs ? settleConfig(base, agentDocs.version, docsPlan.language).config : base;
29295
+ const text4 = formatManagedConfig(config2);
28759
29296
  const finish = async (outcome) => {
28760
29297
  const published = async () => {
28761
29298
  if ((await repo.inspect()).stamp !== first.stamp || await readConfigFile(root) !== text4) throw new InitError("INPUT_CHANGED_AFTER_WRITE", t2("init.changedAfterWrite"));
@@ -28768,7 +29305,7 @@ async function initializeSpecProject(cwd, dryRun = false, env = process.env, bef
28768
29305
  if (old !== void 0) return existing(old);
28769
29306
  if (first.trackedConfig) throw new InitError("CONFIG_DELETED", t2("init.configDeleted"));
28770
29307
  const checkRecords = async () => {
28771
- const entries = await readdir5(join13(root, ".gitifact")).catch((e) => {
29308
+ const entries = await readdir5(join14(root, ".gitifact")).catch((e) => {
28772
29309
  if (e.code === "ENOENT") return [];
28773
29310
  throw e;
28774
29311
  });
@@ -28805,6 +29342,53 @@ var fetchLatestVersion = async (signal) => {
28805
29342
  return latest;
28806
29343
  };
28807
29344
 
29345
+ // src/adapters/filesystem/user-cache.ts
29346
+ import { mkdir as mkdir5, readFile as readFile13, rename as rename3, unlink as unlink3, writeFile as writeFile3 } from "node:fs/promises";
29347
+ import { homedir } from "node:os";
29348
+ import { join as join15 } from "node:path";
29349
+ import { randomUUID as randomUUID5 } from "node:crypto";
29350
+ function userCacheDirectory(env = process.env, platform = process.platform, home = homedir()) {
29351
+ if (env.GITIFACT_CACHE_DIR) return env.GITIFACT_CACHE_DIR;
29352
+ if (platform === "win32") return join15(env.LOCALAPPDATA || join15(home, "AppData", "Local"), "gitifact");
29353
+ if (platform === "darwin") return join15(home, "Library", "Caches", "gitifact");
29354
+ return join15(env.XDG_CACHE_HOME || join15(home, ".cache"), "gitifact");
29355
+ }
29356
+ var cacheFile = (env) => join15(userCacheDirectory(env), "update.json");
29357
+ var isTime = (value) => typeof value === "string" && !Number.isNaN(Date.parse(value));
29358
+ async function readUpdateCache(env = process.env) {
29359
+ let value;
29360
+ try {
29361
+ value = JSON.parse(await readFile13(cacheFile(env), "utf8"));
29362
+ } catch {
29363
+ return {};
29364
+ }
29365
+ if (typeof value !== "object" || value === null) return {};
29366
+ const later = value.later;
29367
+ return {
29368
+ ...isTime(value.checkedAt) ? { checkedAt: value.checkedAt } : {},
29369
+ ...typeof value.latest === "string" ? { latest: value.latest } : {},
29370
+ ...isTime(value.checkingSince) ? { checkingSince: value.checkingSince } : {},
29371
+ ...later && typeof later.version === "string" && isTime(later.until) ? { later: { version: later.version, until: later.until } } : {}
29372
+ };
29373
+ }
29374
+ async function changeUpdateCache(env, change2) {
29375
+ const directory = userCacheDirectory(env);
29376
+ const temporary = join15(directory, "update-" + randomUUID5() + ".tmp");
29377
+ try {
29378
+ const next = change2(await readUpdateCache(env));
29379
+ await mkdir5(directory, { recursive: true });
29380
+ await writeFile3(temporary, JSON.stringify(next) + "\n", "utf8");
29381
+ await rename3(temporary, cacheFile(env));
29382
+ } catch {
29383
+ await unlink3(temporary).catch(() => {
29384
+ });
29385
+ }
29386
+ }
29387
+ async function recordUpdateCheck(env, update, now = Date.now()) {
29388
+ if (update.status === "disabled" || update.status === "checking") return;
29389
+ await changeUpdateCache(env, ({ checkingSince: _done, ...cache }) => ({ ...cache, checkedAt: new Date(now).toISOString(), ...update.latestVersion ? { latest: update.latestVersion } : {} }));
29390
+ }
29391
+
28808
29392
  // src/commands/init.ts
28809
29393
  async function runInit(options, version2) {
28810
29394
  const cancel = new AbortController();
@@ -28819,9 +29403,10 @@ async function runInit(options, version2) {
28819
29403
  update
28820
29404
  );
28821
29405
  if (!dto.ok) throw new Error("unreachable");
29406
+ await recordUpdateCheck(process.env, dto.update);
28822
29407
  const docs = dto.agentDocs.mode === "skip" ? t2("init.text.skipped") : dto.agentDocs.paths.join(", ") + (dto.agentDocs.mode === "remove" ? " (" + t2("init.text.blockRemoved") + ")" : dto.outcome === "planned" ? " (" + t2("init.text.blockPlanned") + ")" : " (" + t2("init.text.blockUpdated") + ")");
28823
29408
  const lineEndings = dto.lineEndings.created ? t2("init.text.lineEndings", { path: dto.lineEndings.path }) + "\n" : "";
28824
- const newer = dto.install ? t2("init.text.updateAvailable", { version: dto.update.latestVersion, command: dto.install.npx }) + "\n" : "";
29409
+ const newer = dto.install ? t2("init.text.updateAvailable", { version: dto.update.latestVersion, command: dto.install.npmGlobal }) + "\n" : "";
28825
29410
  process.stdout.write(options.format === "text" ? `${dto.outcome}: ${dto.rootPath}/.gitifact/config.json
28826
29411
  ${t2("init.text.storage")}: schemaVersion ${dto.schemaVersion}
28827
29412
  ${t2("init.text.agentDocs")}: ${docs}
@@ -28836,17 +29421,17 @@ ${lineEndings}${newer}` : JSON.stringify(dto) + "\n");
28836
29421
  }
28837
29422
 
28838
29423
  // src/commands/documents.ts
28839
- import { readFile as readFile13 } from "node:fs/promises";
28840
- import { join as join15 } from "node:path";
29424
+ import { readFile as readFile14 } from "node:fs/promises";
29425
+ import { join as join17 } from "node:path";
28841
29426
 
28842
29427
  // src/commands/project.ts
28843
- import { join as join14 } from "node:path";
29428
+ import { join as join16 } from "node:path";
28844
29429
  var operations = ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "rebase-merge", "rebase-apply", "sequencer", "BISECT_START", "index.lock"];
28845
29430
  async function openProject(cwd, options = {}) {
28846
29431
  const reader = storeReader(cwd);
28847
29432
  const location = await reader.location();
28848
29433
  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 }));
29434
+ for (const marker2 of operations) if (await fileInfo(join16(location.gitDir, marker2))) throw new InitError("GIT_OPERATION_IN_PROGRESS", t2("git.operationInProgress", { marker: marker2 }));
28850
29435
  if (await reader.hasUnmerged()) throw new InitError("GIT_OPERATION_IN_PROGRESS", t2("git.resolveConflicts"));
28851
29436
  }
28852
29437
  const raw = await readConfigFile(location.root);
@@ -28877,6 +29462,125 @@ function documentsOf(files) {
28877
29462
  return docs;
28878
29463
  }
28879
29464
 
29465
+ // src/queries/filters.ts
29466
+ function byAuthor(author) {
29467
+ if (author === void 0) return () => true;
29468
+ const wanted = author.trim().toLowerCase();
29469
+ return (change2) => change2.author.toLowerCase() === wanted || change2.email.toLowerCase() === wanted;
29470
+ }
29471
+ async function since(project, value, head) {
29472
+ if (value === void 0) return () => true;
29473
+ if (/^\d{4}-\d\d-\d\d/.test(value)) {
29474
+ const from = Date.parse(value);
29475
+ if (!Number.isFinite(from)) throw new CommandError("INVALID_VALUE", t2("list.since", { value }));
29476
+ return (change2) => Date.parse(change2.date) >= from;
29477
+ }
29478
+ let commit;
29479
+ try {
29480
+ commit = await project.reader.resolve(value);
29481
+ } catch {
29482
+ throw new CommandError("INVALID_VALUE", t2("list.since", { value }));
29483
+ }
29484
+ if (!head) return () => false;
29485
+ const after = new Set(project.reader.decode(await project.reader.run(["rev-list", `${commit}..${head}`, "--"])).split("\n").filter(Boolean));
29486
+ return (change2) => after.has(change2.commit);
29487
+ }
29488
+ async function sinceRange(project, value, head) {
29489
+ if (value === void 0) return {};
29490
+ if (/^\d{4}-\d\d-\d\d/.test(value)) {
29491
+ const from = Date.parse(value);
29492
+ if (!Number.isFinite(from)) throw new CommandError("INVALID_VALUE", t2("list.since", { value }));
29493
+ return { from };
29494
+ }
29495
+ let commit;
29496
+ try {
29497
+ commit = await project.reader.resolve(value);
29498
+ } catch {
29499
+ throw new CommandError("INVALID_VALUE", t2("list.since", { value }));
29500
+ }
29501
+ if (!head) return { commits: [] };
29502
+ return { commits: project.reader.decode(await project.reader.run(["rev-list", `${commit}..${head}`, "--"])).split("\n").filter(Boolean) };
29503
+ }
29504
+ var contains = (query) => {
29505
+ const wanted = query?.trim().toLowerCase();
29506
+ return (...texts) => !wanted || texts.some((value) => value?.toLowerCase().includes(wanted));
29507
+ };
29508
+
29509
+ // src/queries/specs.ts
29510
+ 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);
29511
+ async function listSpecs(source2, filter, request) {
29512
+ const listed3 = await source2.cache.documents.list();
29513
+ const problems = listed3.problems;
29514
+ const states = await documentStates(source2, listed3.documents);
29515
+ const documents = states.documents;
29516
+ const byId = new Map(documents.map((d) => [d.id, d]));
29517
+ const arranged = arrangeDocuments(documents);
29518
+ let features = arranged.features;
29519
+ if (filter.feature !== void 0) {
29520
+ features = features.filter((f) => f.index.feature === filter.feature);
29521
+ if (!features.length) throw new CommandError("UNKNOWN_FEATURE", t2("docs.unknownFeature", { feature: filter.feature }));
29522
+ }
29523
+ const titled = (id) => ({ id, title: byId.get(id)?.title ?? null });
29524
+ const designsOf = /* @__PURE__ */ new Map();
29525
+ 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)]);
29526
+ const historyNeeded = filter.sort === "updated" || filter.changedSince !== void 0 || filter.author !== void 0 || !!filter.withUpdated;
29527
+ const latest = /* @__PURE__ */ new Map();
29528
+ const newest = /* @__PURE__ */ new Map();
29529
+ let touched;
29530
+ if (historyNeeded) {
29531
+ const head = await source2.head();
29532
+ const changes2 = head ? await source2.cache.history.changesOf(head) : [];
29533
+ changes2.forEach((c, rank) => {
29534
+ if (!latest.has(c.id)) {
29535
+ latest.set(c.id, { commit: c.commit, date: c.date, author: c.author, email: c.email });
29536
+ newest.set(c.id, rank);
29537
+ }
29538
+ });
29539
+ if (filter.changedSince !== void 0 || filter.author !== void 0) {
29540
+ const after = await since(source2, filter.changedSince, head);
29541
+ const by = byAuthor(filter.author);
29542
+ touched = new Set(changes2.filter((c) => after(c) && by(c)).map((c) => c.id));
29543
+ }
29544
+ }
29545
+ const matched = filter.q !== void 0 ? await source2.cache.documents.matching(filter.q) : void 0;
29546
+ const rows = features.flatMap((f) => [f.index, ...f.requirements, ...f.designs].filter(
29547
+ (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))
29548
+ ).map((doc) => ({
29549
+ id: doc.id,
29550
+ kind: doc.kind,
29551
+ feature: doc.feature,
29552
+ path: doc.path,
29553
+ title: doc.title,
29554
+ description: doc.description,
29555
+ ...doc.kind === "requirement" || doc.kind === "design" ? { order: doc.order } : {},
29556
+ ...doc.draft ? { draft: true } : {},
29557
+ state: states.state(doc.id),
29558
+ ...states.previousPath(doc.id) ? { previousPath: states.previousPath(doc.id) } : {},
29559
+ ...doc.kind === "design" ? { requirements: doc.requirements.map(titled), sources: titledSources(doc, byId) } : {},
29560
+ ...doc.kind === "requirement" ? { designs: designsOf.get(doc.id) ?? [] } : {},
29561
+ ...historyNeeded ? { updated: latest.get(doc.id) ?? null } : {},
29562
+ ...matched ? { line: matched.get(doc.id) } : {}
29563
+ })));
29564
+ if (filter.sort === "title") rows.sort((a, b) => a.title.localeCompare(b.title));
29565
+ if (filter.sort === "updated") rows.sort((a, b) => (newest.get(a.id) ?? Infinity) - (newest.get(b.id) ?? Infinity));
29566
+ const grouped = filter.sort === "order";
29567
+ const groups = features.map((f) => ({ id: f.index.id, rows: rows.filter((r) => r.feature === f.index.feature) })).filter((g) => g.rows.length);
29568
+ const page = grouped ? pageOf(groups, (g) => g.id, request) : pageOf(rows, (r) => r.id, request);
29569
+ if (!page) return void 0;
29570
+ const shown = grouped ? page.rows.flatMap((g) => g.rows) : page.rows;
29571
+ return {
29572
+ rows: shown,
29573
+ features,
29574
+ byId,
29575
+ states,
29576
+ grouped,
29577
+ problems,
29578
+ page: { rows: page.rows, total: page.total, next: page.next },
29579
+ unit: grouped ? "feature" : "document",
29580
+ orphans: filter.feature === void 0 ? arranged.orphans : []
29581
+ };
29582
+ }
29583
+
28880
29584
  // src/commands/documents.ts
28881
29585
  var draftMark = (doc) => doc.draft ? " (" + t2("docs.draft") + ")" : "";
28882
29586
  var stateLabel = {
@@ -28887,12 +29591,11 @@ var stateLabel = {
28887
29591
  var stateMark = (state2) => state2 === "committed" ? "" : " (" + stateLabel[state2]() + ")";
28888
29592
  var line = (doc) => `${doc.id} ${doc.title}${draftMark(doc)} \u2014 ${doc.description}`;
28889
29593
  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
29594
  async function working(project) {
28892
29595
  const { documents } = await project.cache.documents.list();
28893
29596
  return {
28894
29597
  byId: new Map(documents.map((d) => [d.id, d])),
28895
- read: (path3) => readFile13(join15(project.root, ...path3.split("/")), "utf8"),
29598
+ read: (path3) => readFile14(join17(project.root, ...path3.split("/")), "utf8"),
28896
29599
  referencing: async (id) => [...new Set((await project.cache.documents.referencing(id)).map((r) => r.from))]
28897
29600
  };
28898
29601
  }
@@ -29001,48 +29704,6 @@ function checkFields(fields2, columns3) {
29001
29704
  if (unknown2.length || !fields2.length) throw new CommandError("INVALID_VALUE", t2("list.unknownFields", { fields: unknown2.join(", "), columns: columns3.join(", ") }));
29002
29705
  return [...new Set(fields2)];
29003
29706
  }
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
29707
  function cell(value) {
29047
29708
  if (value === void 0 || value === null || value === false) return "";
29048
29709
  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 +29722,14 @@ function selected(rows, fields2) {
29061
29722
  text: rows.length ? text2(rows.map((row) => fields2.map((f) => cell(row[f])).join(" "))) : ""
29062
29723
  };
29063
29724
  }
29064
- var PAGE_SIZE = 20;
29065
29725
  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 };
29726
+ return pageOr(pageOf(rows, keyOf, options), options);
29727
+ }
29728
+ function pageOr(page, options) {
29729
+ return page ?? cursorGone(options);
29730
+ }
29731
+ function cursorGone(options) {
29732
+ throw new CommandError("INVALID_VALUE", t2("list.after", { cursor: options.after ?? "" }));
29076
29733
  }
29077
29734
  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
29735
  function aside(format2, lines) {
@@ -29088,67 +29745,10 @@ var kindLabel = { feature: () => t2("docs.feature"), requirement: () => t2("docs
29088
29745
  var runSpecsList = (options) => runCommand("specs", options.format, async () => {
29089
29746
  const fields2 = checkFields(options.fields, columns);
29090
29747
  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;
29748
+ const listed3 = await listSpecs(project, { ...options, withUpdated: !!fields2?.includes("updated") }, options) ?? cursorGone(options);
29749
+ const { rows: shown, features, byId, states, grouped, problems, orphans, page } = listed3;
29149
29750
  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 : [];
29751
+ const paging = { total: page.total, next: page.next, unit: listed3.unit };
29152
29752
  if (fields2) {
29153
29753
  const picked = selected(shown, fields2);
29154
29754
  return { json: { documents: picked.json, orphans, problems, page: paging }, text: picked.text + aside(options.format, more) };
@@ -29192,27 +29792,23 @@ var runSpecsShow = (ids, options) => runCommand("specs", options.format, async (
29192
29792
  });
29193
29793
  var runSpecsNew = (kind2, path3, options) => runCommand("specs", options.format, async () => createDocument(await openProject(process.cwd()), kind2, path3, options));
29194
29794
 
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"];
29795
+ // src/queries/instructions.ts
29796
+ import { lstat as lstat12 } from "node:fs/promises";
29797
+ import { join as join18 } from "node:path";
29200
29798
  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();
29799
+ async function listInstructions(source2, options, request) {
29800
+ const listed3 = await source2.cache.documents.list();
29205
29801
  const problems = listed3.problems;
29206
- const states = await documentStates(project, listed3.documents);
29802
+ const states = await documentStates(source2, listed3.documents);
29207
29803
  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");
29804
+ const agents2 = { path: AGENTS, exists: !!(await lstat12(join18(source2.root, AGENTS)).catch(() => void 0))?.isFile() };
29805
+ const historyNeeded = options.sort === "updated" || options.author !== void 0 || !!options.withUpdated;
29210
29806
  const latest = /* @__PURE__ */ new Map();
29211
29807
  const newest = /* @__PURE__ */ new Map();
29212
29808
  let touched;
29213
29809
  if (historyNeeded) {
29214
- const head = await project.head();
29215
- const changes2 = head ? await project.cache.history.changesOf(head) : [];
29810
+ const head = await source2.head();
29811
+ const changes2 = head ? await source2.cache.history.changesOf(head) : [];
29216
29812
  changes2.forEach((c, rank) => {
29217
29813
  if (!latest.has(c.id)) {
29218
29814
  latest.set(c.id, { commit: c.commit, date: c.date, author: c.author, email: c.email });
@@ -29224,11 +29820,11 @@ var runInstructionsList = (options) => runCommand("instructions", options.format
29224
29820
  touched = new Set(changes2.filter(by).map((c) => c.id));
29225
29821
  }
29226
29822
  }
29227
- const matched = options.q !== void 0 ? await project.cache.documents.matching(options.q) : void 0;
29823
+ const matched = options.q !== void 0 ? await source2.cache.documents.matching(options.q) : void 0;
29228
29824
  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
29825
  const rows = await Promise.all(instructions2.map(async (d) => {
29230
29826
  const state2 = states.state(d.id);
29231
- const { files, limited } = state2 === "deleted" ? { files: [], limited: false } : await readInstructionFiles(project.root, d.path);
29827
+ const { files, limited } = state2 === "deleted" ? { files: [], limited: false } : await readInstructionFiles(source2.root, d.path);
29232
29828
  return {
29233
29829
  id: d.id,
29234
29830
  name: d.name,
@@ -29245,7 +29841,19 @@ var runInstructionsList = (options) => runCommand("instructions", options.format
29245
29841
  };
29246
29842
  }));
29247
29843
  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);
29844
+ const page = pageOf(rows, (r) => r.id, request);
29845
+ if (!page) return void 0;
29846
+ return { agents: agents2, problems, page };
29847
+ }
29848
+
29849
+ // src/commands/instructions.ts
29850
+ var instructionSorts = ["name", "updated"];
29851
+ var columns2 = ["id", "name", "path", "title", "description", "draft", "state", "previousPath", "files", "updated", "line"];
29852
+ var runInstructionsList = (options) => runCommand("instructions", options.format, async () => {
29853
+ const fields2 = checkFields(options.fields, columns2);
29854
+ const project = await openProject(process.cwd());
29855
+ const listed3 = await listInstructions(project, { ...options, withUpdated: !!fields2?.includes("updated") }, options) ?? cursorGone(options);
29856
+ const { agents: agents2, problems, page } = listed3;
29249
29857
  const shown = page.rows;
29250
29858
  const paging = { total: page.total, next: page.next, unit: "instruction" };
29251
29859
  const unreadable2 = problems.filter((p) => p.path.startsWith(INSTRUCTIONS_ROOT + "/"));
@@ -29291,9 +29899,9 @@ var runInstructionsShow = (targets, options) => runCommand("instructions", optio
29291
29899
  var runInstructionsNew = (name2, options) => runCommand("instructions", options.format, async () => createDocument(await openProject(process.cwd()), "instruction", name2, options));
29292
29900
 
29293
29901
  // 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) => {
29902
+ import { lstat as lstat13, readdir as readdir6 } from "node:fs/promises";
29903
+ import { join as join19 } from "node:path";
29904
+ var info2 = async (path3) => lstat13(path3).catch((e) => {
29297
29905
  if (e.code === "ENOENT") return void 0;
29298
29906
  throw e;
29299
29907
  });
@@ -29301,11 +29909,11 @@ var posix = (path3) => path3.split("\\").join("/");
29301
29909
  async function listAssets(root) {
29302
29910
  const out = [];
29303
29911
  async function visit2(relative, depth) {
29304
- const stat3 = await info2(join17(root, relative));
29912
+ const stat3 = await info2(join19(root, relative));
29305
29913
  if (!stat3 || stat3.isSymbolicLink()) return;
29306
29914
  if (stat3.isDirectory()) {
29307
29915
  if (depth > 8 || out.length > 4e3) return;
29308
- for (const name2 of (await readdir6(join17(root, relative))).sort()) await visit2(relative + "/" + name2, depth + 1);
29916
+ for (const name2 of (await readdir6(join19(root, relative))).sort()) await visit2(relative + "/" + name2, depth + 1);
29309
29917
  } else if (stat3.isFile()) out.push({ path: posix(relative), bytes: stat3.size });
29310
29918
  }
29311
29919
  await visit2(ASSETS_DIR, 0);
@@ -29314,7 +29922,7 @@ async function listAssets(root) {
29314
29922
  async function readDocumentWarnings(root, documents) {
29315
29923
  const outside = [...new Set(documentBodyLinks(documents).map((l) => l.target).filter((t3) => !t3.startsWith(".gitifact/") || t3.startsWith(INSTRUCTIONS_ROOT + "/")))];
29316
29924
  const existing = /* @__PURE__ */ new Set();
29317
- for (const target of outside) if ((await info2(join17(root, ...target.split("/"))))?.isFile()) existing.add(target);
29925
+ for (const target of outside) if ((await info2(join19(root, ...target.split("/"))))?.isFile()) existing.add(target);
29318
29926
  return documentWarnings(documents, await listAssets(root), existing);
29319
29927
  }
29320
29928
 
@@ -29337,33 +29945,33 @@ var runCheck = (options) => runCommand("check", options.format, async () => {
29337
29945
  });
29338
29946
 
29339
29947
  // src/commands/changes.ts
29340
- import { randomUUID as randomUUID5 } from "node:crypto";
29341
- 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";
29948
+ import { randomUUID as randomUUID6 } from "node:crypto";
29949
+ import { mkdir as mkdir7, open as open5, readFile as readFile17, rename as rename4, rmdir, unlink as unlink5, writeFile as writeFile5 } from "node:fs/promises";
29950
+ import { join as join22 } from "node:path";
29343
29951
 
29344
29952
  // src/adapters/filesystem/agent-inputs.ts
29345
29953
  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";
29954
+ import { lstat as lstat14, mkdir as mkdir6, readdir as readdir7, realpath as realpath3, unlink as unlink4, writeFile as writeFile4 } from "node:fs/promises";
29347
29955
  import { tmpdir } from "node:os";
29348
- import { basename, dirname as dirname2, join as join18 } from "node:path";
29956
+ import { basename, dirname as dirname2, join as join20 } from "node:path";
29349
29957
  var maxAge = 7 * 24 * 60 * 60 * 1e3;
29350
- var info3 = (path3) => lstat13(path3).catch((e) => {
29958
+ var info3 = (path3) => lstat14(path3).catch((e) => {
29351
29959
  if (e.code === "ENOENT") return void 0;
29352
29960
  throw e;
29353
29961
  });
29354
29962
  var comparable = (path3) => process.platform === "win32" ? path3.toLowerCase() : path3;
29355
29963
  async function candidates(root, controls) {
29356
29964
  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") };
29965
+ return { system: join20((controls.tmpdir ?? tmpdir)(), "gitifact", key), project: join20(root, ".gitifact", "tmp") };
29358
29966
  }
29359
29967
  async function writable(directory) {
29360
29968
  try {
29361
- await mkdir5(directory, { recursive: true });
29969
+ await mkdir6(directory, { recursive: true });
29362
29970
  const stat3 = await info3(directory);
29363
29971
  if (!stat3?.isDirectory() || stat3.isSymbolicLink()) return false;
29364
- const probe = join18(directory, ".probe-" + randomBytes2(8).toString("hex"));
29365
- await writeFile3(probe, "", { flag: "wx" });
29366
- await unlink3(probe);
29972
+ const probe = join20(directory, ".probe-" + randomBytes2(8).toString("hex"));
29973
+ await writeFile4(probe, "", { flag: "wx" });
29974
+ await unlink4(probe);
29367
29975
  return true;
29368
29976
  } catch {
29369
29977
  return false;
@@ -29375,7 +29983,7 @@ async function prepareAgentInputs(root, controls = {}) {
29375
29983
  if (await writable(system)) directory = system;
29376
29984
  else if (await writable(project)) {
29377
29985
  directory = project;
29378
- await writeFile3(join18(project, ".gitignore"), "*\n", { flag: "wx" }).catch((e) => {
29986
+ await writeFile4(join20(project, ".gitignore"), "*\n", { flag: "wx" }).catch((e) => {
29379
29987
  if (e.code !== "EEXIST") throw e;
29380
29988
  });
29381
29989
  }
@@ -29383,10 +29991,10 @@ async function prepareAgentInputs(root, controls = {}) {
29383
29991
  const now = (controls.now ?? Date.now)();
29384
29992
  for (const name2 of await readdir7(directory).catch(() => [])) {
29385
29993
  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);
29994
+ const stat3 = await info3(join20(directory, name2)).catch(() => void 0);
29995
+ if (stat3?.isFile() && now - stat3.mtimeMs > maxAge) await unlink4(join20(directory, name2)).catch(() => void 0);
29388
29996
  }
29389
- return { commit: join18(directory, "commit.json") };
29997
+ return { commit: join20(directory, "commit.json") };
29390
29998
  }
29391
29999
  async function discardAgentInput(root, file2, controls = {}) {
29392
30000
  try {
@@ -29399,7 +30007,7 @@ async function discardAgentInput(root, file2, controls = {}) {
29399
30007
  if (await info3(directory) && comparable(await realpath3(directory)) === parent) managed = true;
29400
30008
  }
29401
30009
  if (!managed || basename(file2) === ".gitignore") return false;
29402
- await unlink3(file2);
30010
+ await unlink4(file2);
29403
30011
  return true;
29404
30012
  } catch {
29405
30013
  return false;
@@ -29408,17 +30016,17 @@ async function discardAgentInput(root, file2, controls = {}) {
29408
30016
 
29409
30017
  // src/commands/commit-files.ts
29410
30018
  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";
30019
+ import { lstat as lstat15, readFile as readFile15 } from "node:fs/promises";
30020
+ import { join as join21 } from "node:path";
29413
30021
  var fail3 = (message) => {
29414
30022
  throw new CommandError("INVALID_COMMIT", message);
29415
30023
  };
29416
30024
  var hash2 = (value) => createHash5("sha256").update(value).digest("hex");
29417
- var optional2 = (path3) => readFile14(path3).catch((e) => {
30025
+ var optional2 = (path3) => readFile15(path3).catch((e) => {
29418
30026
  if (e.code === "ENOENT") return null;
29419
30027
  throw e;
29420
30028
  });
29421
- var info4 = (path3) => lstat14(path3).catch((e) => {
30029
+ var info4 = (path3) => lstat15(path3).catch((e) => {
29422
30030
  if (e.code === "ENOENT") return void 0;
29423
30031
  throw e;
29424
30032
  });
@@ -29444,14 +30052,14 @@ async function fingerprint(root, path3) {
29444
30052
  validPath2(path3);
29445
30053
  const parts = path3.split("/");
29446
30054
  for (let i = 1; i <= parts.length; i++) {
29447
- const target = join19(root, ...parts.slice(0, i));
30055
+ const target = join21(root, ...parts.slice(0, i));
29448
30056
  const stat3 = await info4(target);
29449
30057
  if (!stat3) return null;
29450
30058
  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"));
30059
+ if (i < parts.length && await info4(join21(target, ".git"))) fail3(t2("commitFiles.nestedRepository"));
29452
30060
  if (i === parts.length) {
29453
30061
  if (stat3.nlink !== 1 || stat3.size > 16 * 1024 * 1024) fail3(t2("commitFiles.hardLinkOrSize"));
29454
- return hash2(Buffer.concat([Buffer.from(String(stat3.mode & 73) + "\0"), await readFile14(target)]));
30062
+ return hash2(Buffer.concat([Buffer.from(String(stat3.mode & 73) + "\0"), await readFile15(target)]));
29455
30063
  }
29456
30064
  }
29457
30065
  return null;
@@ -29478,14 +30086,15 @@ function policyPaths(files) {
29478
30086
  }
29479
30087
  return [...all].sort();
29480
30088
  }
30089
+ var storeFiles = /* @__PURE__ */ new Set([".gitifact/config.json", LINE_ENDINGS_PATH]);
29481
30090
  function checkStoreSelection(selected2) {
29482
- for (const p of selected2.filter((p2) => p2.startsWith(".gitifact/") && !record3(p2) && p2 !== ".gitifact/config.json")) {
30091
+ for (const p of selected2.filter((p2) => p2.startsWith(".gitifact/") && !record3(p2) && !storeFiles.has(p2))) {
29483
30092
  if (!isAssetPath(p) && !instructionFile2(p)) fail3(t2("commitFiles.notRecord", { path: p }));
29484
30093
  }
29485
30094
  }
29486
30095
 
29487
30096
  // src/commands/input.ts
29488
- import { readFile as readFile15, stat as stat2 } from "node:fs/promises";
30097
+ import { readFile as readFile16, stat as stat2 } from "node:fs/promises";
29489
30098
  var LIMIT2 = 1024 * 1024;
29490
30099
  async function readJsonInput(file2, stdin = process.stdin) {
29491
30100
  const bytes = file2 === "-" ? await readStdin(stdin) : await readInputFile(file2);
@@ -29498,7 +30107,7 @@ async function readJsonInput(file2, stdin = process.stdin) {
29498
30107
  async function readInputFile(path3) {
29499
30108
  const source2 = await stat2(path3).catch(() => void 0);
29500
30109
  if (!source2?.isFile() || source2.size > LIMIT2) throw new CommandError("INVALID_INPUT", t2("input.file"));
29501
- return readFile15(path3);
30110
+ return readFile16(path3);
29502
30111
  }
29503
30112
  async function readStdin(stream) {
29504
30113
  const chunks = [];
@@ -29598,7 +30207,7 @@ async function commitChanges(cwd, input2, dryRun) {
29598
30207
  ...stdin ? { input: stdin } : {}
29599
30208
  });
29600
30209
  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");
30210
+ const busy = join22(gitDir, "gitifact-changes-commit.lock");
29602
30211
  if (await info4(busy)) fail3(t2("commit.busy", { path: busy }));
29603
30212
  if ((await staged()).length) fail3(t2("commit.existingStaging"));
29604
30213
  const indexHash = hash2(await optional2(indexPath) ?? Buffer.alloc(0));
@@ -29637,7 +30246,7 @@ async function commitChanges(cwd, input2, dryRun) {
29637
30246
  if (!same2(base, await project.reader.baseline())) fail3(t2("commit.headChanged"));
29638
30247
  return { outcome: "dry-run", committed: false, ...summary, commit: void 0 };
29639
30248
  }
29640
- const temporary = join20(gitDir, "gitifact-commit-index-" + randomUUID5());
30249
+ const temporary = join22(gitDir, "gitifact-commit-index-" + randomUUID6());
29641
30250
  let owned = false;
29642
30251
  let uncertain = false;
29643
30252
  let commitStarted = false;
@@ -29645,7 +30254,7 @@ async function commitChanges(cwd, input2, dryRun) {
29645
30254
  let committed2;
29646
30255
  try {
29647
30256
  try {
29648
- await mkdir6(busy);
30257
+ await mkdir7(busy);
29649
30258
  owned = true;
29650
30259
  } catch (e) {
29651
30260
  if (e.code === "EEXIST") fail3(t2("commit.busy", { path: busy }));
@@ -29656,31 +30265,31 @@ async function commitChanges(cwd, input2, dryRun) {
29656
30265
  if (hash2(original ?? Buffer.alloc(0)) !== indexHash || (await staged()).length) fail3(t2("commit.indexChanged"));
29657
30266
  const before = await hashes([...started.keys()]);
29658
30267
  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 }));
30268
+ await writeFile5(join22(busy, "recovery.json"), JSON.stringify({ before: base, originalIndex: original?.toString("base64") ?? null, temporary, paths: selected2, message }));
29660
30269
  const locked = await hashes([.../* @__PURE__ */ new Set([...selected2, ...contextPaths])]);
29661
- if (original) await writeFile4(temporary, original, { flag: "wx" });
30270
+ if (original) await writeFile5(temporary, original, { flag: "wx" });
29662
30271
  else await git(["read-tree", "--empty"], temporary);
29663
30272
  await git(["add", "--pathspec-from-file=-", "--pathspec-file-nul"], temporary, Buffer.from(stageable.join("\0")));
29664
30273
  const actual = await staged(temporary);
29665
30274
  if (!actual.length || actual.some((p) => !selected2.includes(p))) fail3(t2("commit.unexpectedFiles"));
29666
- const storeFiles = selected2.filter((p) => record3(p) && locked.get(p) !== null);
29667
- if (storeFiles.length) {
29668
- const output2 = await git(["cat-file", "--batch"], temporary, Buffer.from(storeFiles.map((p) => ":" + p).join("\n") + "\n"));
30275
+ const storeFiles2 = selected2.filter((p) => record3(p) && locked.get(p) !== null);
30276
+ if (storeFiles2.length) {
30277
+ const output2 = await git(["cat-file", "--batch"], temporary, Buffer.from(storeFiles2.map((p) => ":" + p).join("\n") + "\n"));
29669
30278
  let offset = 0;
29670
- for (const path3 of storeFiles) {
30279
+ for (const path3 of storeFiles2) {
29671
30280
  const end = output2.indexOf(10, offset);
29672
30281
  const header = output2.subarray(offset, end).toString("utf8").split(" ");
29673
30282
  const size = Number(header[2]);
29674
30283
  if (end < 0 || header[1] !== "blob" || !Number.isSafeInteger(size) || output2[end + 1 + size] !== 10) fail3(t2("commit.stagedUnreadable", { path: path3 }));
29675
30284
  const blob = output2.subarray(end + 1, end + 1 + size);
29676
- const file2 = await readFile16(join20(root, path3));
30285
+ const file2 = await readFile17(join22(root, path3));
29677
30286
  if (!blob.equals(file2) && !blob.equals(Buffer.from(file2.toString("latin1").replace(/\r\n/g, "\n"), "latin1"))) fail3(t2("commit.filterChanged"));
29678
30287
  offset = end + size + 2;
29679
30288
  }
29680
30289
  }
29681
30290
  const again = await hashes([...locked.keys()]);
29682
30291
  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"));
30292
+ for (const name2 of integration) if (await info4(join22(gitDir, name2))) fail3(t2("commit.integrationStarted"));
29684
30293
  if (!same2(base, await project.reader.baseline())) fail3(t2("commit.headChanged"));
29685
30294
  const tree = (await git(["write-tree"], temporary)).toString("utf8").trim();
29686
30295
  commitStarted = true;
@@ -29695,10 +30304,10 @@ async function commitChanges(cwd, input2, dryRun) {
29695
30304
  if (headers[0] !== "tree " + tree || after.branch !== base.branch || !same2(parents, base.head ? [base.head] : []) || trailers.some((line2) => !body2.split(/\r?\n/).includes(line2))) fail3(t2("commit.resultAltered"));
29696
30305
  if (hash2(await optional2(indexPath) ?? Buffer.alloc(0)) !== indexHash) fail3(t2("commit.indexChangedAfter"));
29697
30306
  if ((await git(["write-tree"], temporary)).toString("utf8").trim() !== tree) fail3(t2("commit.hookChangedStaging"));
29698
- await lock.writeFile(await readFile16(temporary));
30307
+ await lock.writeFile(await readFile17(temporary));
29699
30308
  await lock.sync();
29700
30309
  await lock.close();
29701
- await rename3(indexPath + ".lock", indexPath);
30310
+ await rename4(indexPath + ".lock", indexPath);
29702
30311
  indexLock = void 0;
29703
30312
  committed2 = { commit: after.head, paths: actual };
29704
30313
  } catch (error62) {
@@ -29715,12 +30324,12 @@ async function commitChanges(cwd, input2, dryRun) {
29715
30324
  await indexLock?.close().catch(() => {
29716
30325
  });
29717
30326
  if (!uncertain) {
29718
- if (indexLock) await unlink4(indexPath + ".lock").catch(() => {
30327
+ if (indexLock) await unlink5(indexPath + ".lock").catch(() => {
29719
30328
  });
29720
- await unlink4(temporary).catch(() => {
30329
+ await unlink5(temporary).catch(() => {
29721
30330
  });
29722
30331
  if (owned) {
29723
- await unlink4(join20(busy, "recovery.json")).catch(() => {
30332
+ await unlink5(join22(busy, "recovery.json")).catch(() => {
29724
30333
  });
29725
30334
  await rmdir(busy);
29726
30335
  }
@@ -29730,8 +30339,8 @@ async function commitChanges(cwd, input2, dryRun) {
29730
30339
  }
29731
30340
 
29732
30341
  // src/commands/records.ts
29733
- import { readFile as readFile17 } from "node:fs/promises";
29734
- import { join as join21 } from "node:path";
30342
+ import { readFile as readFile18 } from "node:fs/promises";
30343
+ import { join as join23 } from "node:path";
29735
30344
  var runRecordsNew = (options) => runCommand("records", options.format, async () => {
29736
30345
  if (/[\r\n]/.test(options.title)) throw new CommandError("INVALID_VALUE", t2("docs.singleLine", { field: "title" }));
29737
30346
  const docs = [...new Set(options.docs.flatMap((d) => d.split(",")).map((d) => d.trim()).filter(Boolean))];
@@ -29767,7 +30376,7 @@ var runRecordsShow = (ids, options) => runCommand("records", options.format, asy
29767
30376
  const shown = await Promise.all([...new Set(ids)].map(async (id) => {
29768
30377
  const path3 = await findRecord(project.root, id);
29769
30378
  if (!path3) return { id, missing: true };
29770
- const source2 = await readFile17(join21(project.root, ...path3.split("/")), "utf8");
30379
+ const source2 = await readFile18(join23(project.root, ...path3.split("/")), "utf8");
29771
30380
  const log = project.reader.decode(await project.reader.run(["log", "-1", "--diff-filter=A", "--format=%H%x00%aN%x00%aI", "--", path3])).trim();
29772
30381
  const [commit, author, date5] = log.split("\0");
29773
30382
  return { id, path: path3, text: source2, commit: commit ? { commit, author, date: date5 } : null };
@@ -29875,9 +30484,7 @@ async function readThrough(project, head, filter, id) {
29875
30484
  }
29876
30485
 
29877
30486
  // src/commands/feedback.ts
29878
- import { readFile as readFile18 } from "node:fs/promises";
29879
30487
  import { release as release3 } from "node:os";
29880
- import { dirname as dirname3, join as join22 } from "node:path";
29881
30488
 
29882
30489
  // src/adapters/github/issue.ts
29883
30490
  import { spawn as spawn2 } from "node:child_process";
@@ -29931,6 +30538,17 @@ function newIssueUrl(title, body2, marker2) {
29931
30538
  return { url: address(characters.slice(0, low).join("") + marker2), truncated: true };
29932
30539
  }
29933
30540
 
30541
+ // src/shared/project-config.ts
30542
+ import { readFile as readFile19 } from "node:fs/promises";
30543
+ import { dirname as dirname3, join as join24 } from "node:path";
30544
+ async function findProjectConfig(from) {
30545
+ for (let folder = from; ; folder = dirname3(folder)) {
30546
+ const text4 = await readFile19(join24(folder, ".gitifact", "config.json"), "utf8").catch(() => void 0);
30547
+ if (text4 !== void 0) return { root: folder, text: text4 };
30548
+ if (dirname3(folder) === folder) return void 0;
30549
+ }
30550
+ }
30551
+
29934
30552
  // src/commands/feedback.ts
29935
30553
  var types = ["bug", "idea"];
29936
30554
  function parseFeedback(input2) {
@@ -29943,17 +30561,13 @@ function parseFeedback(input2) {
29943
30561
  return { type, title: title.trim(), body: body2.trim() };
29944
30562
  }
29945
30563
  async function schemaVersion(from) {
29946
- for (let folder = from; ; folder = dirname3(folder)) {
29947
- const config2 = await readFile18(join22(folder, ".gitifact", "config.json"), "utf8").catch(() => void 0);
29948
- if (config2 !== void 0) {
29949
- try {
29950
- const value = JSON.parse(config2).schemaVersion;
29951
- return typeof value === "number" ? String(value) : "none";
29952
- } catch {
29953
- return "none";
29954
- }
29955
- }
29956
- if (dirname3(folder) === folder) return "none";
30564
+ const config2 = (await findProjectConfig(from))?.text;
30565
+ if (config2 === void 0) return "none";
30566
+ try {
30567
+ const value = JSON.parse(config2).schemaVersion;
30568
+ return typeof value === "number" ? String(value) : "none";
30569
+ } catch {
30570
+ return "none";
29957
30571
  }
29958
30572
  }
29959
30573
  async function issueBody(feedback, version2, cwd) {
@@ -29990,6 +30604,11 @@ var runFeedback = (options, version2, run) => runCommand("feedback", options.for
29990
30604
  return { json: { ...sent, title: feedback.title, body: body2 }, text: text2(out) };
29991
30605
  });
29992
30606
 
30607
+ // src/commands/update.ts
30608
+ import { readFile as readFile20 } from "node:fs/promises";
30609
+ import { join as join25 } from "node:path";
30610
+ import { isDeepStrictEqual } from "node:util";
30611
+
29993
30612
  // src/adapters/git/agent-docs-commit.ts
29994
30613
  function agentDocsGit(root, inherited = process.env) {
29995
30614
  const env = { ...inherited, GIT_OPTIONAL_LOCKS: "0", GIT_NO_LAZY_FETCH: "1", GIT_TERMINAL_PROMPT: "0", GIT_LITERAL_PATHSPECS: "1", LC_ALL: "C" };
@@ -30021,9 +30640,11 @@ function agentDocsGit(root, inherited = process.env) {
30021
30640
  }
30022
30641
 
30023
30642
  // src/commands/update.ts
30024
- var blockCommitMessage = (version2) => "chore(gitifact): refresh GITIFACT block to v" + version2;
30643
+ var CONFIG_PATH = ".gitifact/config.json";
30644
+ var updateCommitMessage = (version2) => "chore(gitifact): update project to gitifact v" + version2;
30025
30645
  var commitState = (state2, extra = {}) => ({ state: state2, commit: null, paths: [], message: null, reason: null, detail: null, ...extra });
30026
- async function refreshBlocks(cwd, version2, env, controls) {
30646
+ var notInitialized = { docs: { state: "not-initialized", paths: [], missing: [] }, project: { state: "not-initialized", cli: null } };
30647
+ async function refreshProject(cwd, version2, env, controls) {
30027
30648
  const repo = initRepository(cwd, env);
30028
30649
  let first;
30029
30650
  let config2;
@@ -30031,20 +30652,28 @@ async function refreshBlocks(cwd, version2, env, controls) {
30031
30652
  first = await repo.inspect();
30032
30653
  config2 = await readConfigFile(first.state.repository.rootPath);
30033
30654
  } catch (error62) {
30034
- if (error62 instanceof RepositoryReadError && error62.code === "NOT_A_REPOSITORY") return { state: "not-initialized", paths: [], missing: [] };
30655
+ if (error62 instanceof RepositoryReadError && error62.code === "NOT_A_REPOSITORY") return notInitialized;
30035
30656
  throw error62;
30036
30657
  }
30037
- if (config2 === void 0) return { state: "not-initialized", paths: [], missing: [] };
30658
+ if (config2 === void 0) return notInitialized;
30038
30659
  const root = first.state.repository.rootPath;
30039
- const plan = await planAgentDocs(root, { version: version2, onlyExisting: true, ...controls });
30660
+ const parsed2 = needsMigration(config2) ? void 0 : parseManagedConfig(config2);
30661
+ const plan = await planAgentDocs(root, { version: version2, onlyExisting: true, language: parsed2?.language, ...controls });
30040
30662
  const { missing: missing2 } = plan;
30041
- if (plan.paths.length === 0) return { state: "no-block", paths: [], missing: missing2, root };
30042
- if (plan.writes.length === 0) return { state: "current", paths: plan.paths, missing: missing2, root, candidates: plan.paths };
30663
+ if (parsed2?.cli && isNewerRelease(parsed2.cli, version2)) return { docs: { state: "kept", paths: plan.paths, missing: missing2, root }, project: { state: "current", cli: parsed2.cli } };
30664
+ let current2 = config2;
30043
30665
  const unchanged = async () => {
30044
- if ((await repo.inspect()).stamp !== first.stamp || await readConfigFile(root) !== config2) throw new InitError("INPUT_CHANGED", t2("init.inputChanged"));
30666
+ if ((await repo.inspect()).stamp !== first.stamp || await readConfigFile(root) !== current2) throw new InitError("INPUT_CHANGED", t2("init.inputChanged"));
30045
30667
  };
30046
- const applied = await applyAgentDocs(root, plan, unchanged);
30047
- return { state: "refreshed", paths: applied, missing: missing2, root, candidates: plan.paths };
30668
+ const docs = plan.paths.length === 0 ? { state: "no-block", paths: [], missing: missing2, root } : plan.writes.length === 0 ? { state: "current", paths: plan.paths, missing: missing2, root, candidates: plan.paths } : { state: "refreshed", paths: await applyAgentDocs(root, plan, unchanged), missing: missing2, root, candidates: plan.paths };
30669
+ if (!parsed2) return { docs, project: { state: "current", cli: null } };
30670
+ const settled2 = settleConfig(parsed2, version2, plan.language);
30671
+ if (settled2.changed) {
30672
+ const next = formatManagedConfig(settled2.config, config2);
30673
+ await publishConfig(root, next, unchanged, void 0, true);
30674
+ current2 = next;
30675
+ }
30676
+ return { docs: { ...docs, root }, project: { state: settled2.changed ? "written" : "current", cli: settled2.config.cli ?? null } };
30048
30677
  }
30049
30678
  function outsideBlock(text4) {
30050
30679
  const normalized2 = text4.replace(/\r\n/g, "\n");
@@ -30055,12 +30684,20 @@ function outsideBlock(text4) {
30055
30684
  return null;
30056
30685
  }
30057
30686
  }
30058
- async function commitBlocks(docs, version2, env) {
30059
- if (!docs.root || !docs.candidates?.length) return commitState("nothing");
30687
+ function withoutSettings(text4) {
30688
+ try {
30689
+ const { cli: _c, language: _l, ...rest } = JSON.parse(text4);
30690
+ return rest;
30691
+ } catch {
30692
+ return null;
30693
+ }
30694
+ }
30695
+ async function commitUpdate(docs, version2, env) {
30696
+ if (!docs.root) return commitState("nothing");
30060
30697
  const git = agentDocsGit(docs.root, env);
30061
30698
  const head = await git.head();
30062
30699
  const changed = [];
30063
- for (const path3 of docs.candidates) {
30700
+ for (const path3 of docs.candidates ?? []) {
30064
30701
  const working2 = await managedRead(docs.root, path3);
30065
30702
  const committed2 = head ? await git.committed(head, path3) : null;
30066
30703
  if (working2 === null) continue;
@@ -30070,8 +30707,16 @@ async function commitBlocks(docs, version2, env) {
30070
30707
  if (before === null || before !== outsideBlock(working2)) return commitState("skipped", { paths: [path3], reason: "other-changes" });
30071
30708
  changed.push(path3);
30072
30709
  }
30710
+ const workingConfig = await readFile20(join25(docs.root, CONFIG_PATH), "utf8").catch(() => null);
30711
+ const committedConfig = head ? await git.committed(head, CONFIG_PATH) : null;
30712
+ if (workingConfig !== null && committedConfig?.replace(/\r\n/g, "\n") !== workingConfig.replace(/\r\n/g, "\n")) {
30713
+ if (committedConfig === null) return commitState("skipped", { paths: [CONFIG_PATH], reason: "untracked" });
30714
+ const before = withoutSettings(committedConfig);
30715
+ if (before === null || !isDeepStrictEqual(before, withoutSettings(workingConfig))) return commitState("skipped", { paths: [CONFIG_PATH], reason: "other-changes" });
30716
+ changed.push(CONFIG_PATH);
30717
+ }
30073
30718
  if (changed.length === 0) return commitState("nothing");
30074
- const message = blockCommitMessage(version2);
30719
+ const message = updateCommitMessage(version2);
30075
30720
  let failure2;
30076
30721
  try {
30077
30722
  await git.commitOnly(changed, message);
@@ -30085,20 +30730,23 @@ async function commitBlocks(docs, version2, env) {
30085
30730
  return commitState("committed", { commit: after, paths: await git.changedPaths(after), message });
30086
30731
  }
30087
30732
  async function updateCommand(cwd, version2, env = process.env, controls = {}) {
30088
- const [update, agentDocs] = await Promise.all([
30733
+ const [update, refreshed] = await Promise.all([
30089
30734
  updateCheckDisabled(env) ? disabledUpdate : resolveUpdate(version2, controls.fetchLatest ?? fetchLatestVersion, void 0, controls.timeoutMs),
30090
- refreshBlocks(cwd, version2, env, controls)
30735
+ refreshProject(cwd, version2, env, controls)
30091
30736
  ]);
30092
- const commit = controls.commit ? await commitBlocks(agentDocs, version2, env) : commitState("not-requested");
30093
- const config2 = await readConfigFile(agentDocs.root ?? cwd).catch(() => void 0);
30094
- return updateV5.parse({
30737
+ await recordUpdateCheck(env, update, controls.now?.());
30738
+ const { docs, project } = refreshed;
30739
+ const commit = controls.commit ? await commitUpdate(docs, version2, env) : commitState("not-requested");
30740
+ const config2 = await readConfigFile(docs.root ?? cwd).catch(() => void 0);
30741
+ return updateV6.parse({
30095
30742
  contract: "update",
30096
- version: 5,
30743
+ version: 6,
30097
30744
  ok: true,
30098
30745
  cliVersion: version2,
30099
30746
  update,
30100
- install: update.status === "available" ? { npx: npxUpdate(update.latestVersion), npmGlobal: npmGlobalInstall(update.latestVersion) } : null,
30101
- agentDocs: { state: agentDocs.state, paths: agentDocs.paths, missing: agentDocs.missing },
30747
+ install: update.status === "available" ? { npmGlobal: npmGlobalInstall(update.latestVersion) } : null,
30748
+ agentDocs: { state: docs.state, paths: docs.paths, missing: docs.missing },
30749
+ project,
30102
30750
  commit,
30103
30751
  migrationRequired: config2 !== void 0 && needsMigration(config2)
30104
30752
  });
@@ -30113,9 +30761,11 @@ async function runUpdate(options, version2) {
30113
30761
  }
30114
30762
  const lines = [t2("update.text.current", { version: dto.cliVersion })];
30115
30763
  lines.push(dto.update.status === "available" ? t2("update.text.available", { version: dto.update.latestVersion }) : dto.update.status === "up-to-date" ? t2("update.text.upToDate") : dto.update.status === "disabled" ? t2("update.text.disabled") : t2("update.text.unavailable"));
30116
- if (dto.install) lines.push(t2("update.text.install", { command: dto.install.npx }), t2("update.text.afterInstall"));
30117
- lines.push(dto.agentDocs.state === "refreshed" ? t2("update.text.blockRefreshed", { paths: dto.agentDocs.paths.join(", ") }) : dto.agentDocs.state === "current" ? t2("update.text.blockCurrent", { paths: dto.agentDocs.paths.join(", ") }) : dto.agentDocs.state === "no-block" ? t2("update.text.noBlock", { version: dto.cliVersion }) : t2("update.text.notInitialized"));
30118
- if (dto.agentDocs.missing.length) lines.push(t2("update.text.missing", { paths: dto.agentDocs.missing.join(", "), version: dto.cliVersion }));
30764
+ if (dto.install) lines.push(t2("update.text.install", { command: dto.install.npmGlobal }), t2("update.text.afterInstall"));
30765
+ lines.push(dto.agentDocs.state === "refreshed" ? t2("update.text.blockRefreshed", { paths: dto.agentDocs.paths.join(", ") }) : dto.agentDocs.state === "current" ? t2("update.text.blockCurrent", { paths: dto.agentDocs.paths.join(", ") }) : dto.agentDocs.state === "kept" ? t2("update.text.blockKept", { paths: dto.agentDocs.paths.join(", ") || "-" }) : dto.agentDocs.state === "no-block" ? t2("update.text.noBlock") : t2("update.text.notInitialized"));
30766
+ if (dto.agentDocs.missing.length) lines.push(t2("update.text.missing", { paths: dto.agentDocs.missing.join(", ") }));
30767
+ if (dto.project.state === "written") lines.push(t2("update.text.projectWritten", { path: CONFIG_PATH, version: dto.project.cli ?? dto.cliVersion }));
30768
+ if (dto.project.cli && isNewerRelease(dto.project.cli, dto.cliVersion)) lines.push(t2("update.text.projectAhead", { project: dto.project.cli, version: dto.cliVersion, command: npmGlobalInstall(dto.project.cli) }));
30119
30769
  if (dto.migrationRequired) lines.push(t2("update.text.migrationRequired", { version: dto.cliVersion }));
30120
30770
  const { commit } = dto;
30121
30771
  if (commit.state === "committed") lines.push(t2("update.text.committed", { commit: commit.commit.slice(0, 12), paths: commit.paths.join(", ") }));
@@ -30129,7 +30779,7 @@ async function runUpdate(options, version2) {
30129
30779
  } catch (error62) {
30130
30780
  const known = error62 instanceof InitError || error62 instanceof RepositoryReadError;
30131
30781
  const failure2 = { code: known ? error62.code : "UPDATE_FAILED", message: known ? error62.message : t2("update.failed") };
30132
- process.stderr.write(options.format === "text" ? failure2.code + ": " + failure2.message + "\n" : JSON.stringify(updateV5.parse({ contract: "update", version: 5, ok: false, error: failure2 })) + "\n");
30782
+ process.stderr.write(options.format === "text" ? failure2.code + ": " + failure2.message + "\n" : JSON.stringify(updateV6.parse({ contract: "update", version: 6, ok: false, error: failure2 })) + "\n");
30133
30783
  process.exitCode = 1;
30134
30784
  }
30135
30785
  }
@@ -30137,6 +30787,7 @@ async function runUpdate(options, version2) {
30137
30787
  // src/commands/update-check.ts
30138
30788
  async function checkUpdateCommand(version2, env = process.env, controls = {}) {
30139
30789
  const update = updateCheckDisabled(env) ? disabledUpdate : await resolveUpdate(version2, controls.fetchLatest ?? fetchLatestVersion, void 0, controls.timeoutMs);
30790
+ await recordUpdateCheck(env, update, controls.now?.());
30140
30791
  return updateCheckV1.parse({
30141
30792
  contract: "update-check",
30142
30793
  version: 1,
@@ -30157,10 +30808,73 @@ async function runUpdateCheck(format2, version2) {
30157
30808
  t2("update.text.current", { version: dto.cliVersion }),
30158
30809
  state2.status === "available" ? t2("update.text.available", { version: state2.latestVersion }) : state2.status === "up-to-date" ? t2("update.text.upToDate") : state2.status === "disabled" ? t2("update.text.disabled") : t2("update.text.unavailable")
30159
30810
  ];
30160
- if (dto.command) lines.push(t2("update.text.install", { command: dto.command }));
30811
+ if (state2.status === "available") lines.push(t2("update.text.install", { command: npmGlobalInstall(state2.latestVersion) }), t2("update.text.afterInstall"));
30161
30812
  process.stdout.write(lines.join("\n") + "\n");
30162
30813
  }
30163
30814
 
30815
+ // src/commands/version-notice.ts
30816
+ import { spawn as spawn3 } from "node:child_process";
30817
+ import { homedir as homedir2 } from "node:os";
30818
+ var REFRESH_AFTER_MS = 60 * 60 * 1e3;
30819
+ var CHECKING_WINDOW_MS = 5 * 60 * 1e3;
30820
+ var LATER_MS = 24 * 60 * 60 * 1e3;
30821
+ var REFRESH_COMMAND = "__refresh-update";
30822
+ var noticeScope = (command) => ({ release: command !== "init" && command !== "update", projectOlder: command !== "update" });
30823
+ function chooseNotice(running2, project, cache, now, scope = { release: true, projectOlder: true }) {
30824
+ if (project && isNewerRelease(project, running2)) return { kind: "project-newer", project };
30825
+ const latest = cache.latest;
30826
+ const postponed = cache.later !== void 0 && cache.later.version === latest && now < Date.parse(cache.later.until);
30827
+ if (scope.release && latest && isNewerRelease(latest, running2) && !postponed) return { kind: "release", latest };
30828
+ if (scope.projectOlder && project && isNewerRelease(running2, project)) return { kind: "project-older", project };
30829
+ return null;
30830
+ }
30831
+ function noticeText(notice, running2) {
30832
+ if (notice.kind === "project-newer") return t2("notice.projectNewer", { project: notice.project, version: running2, command: npmGlobalInstall(notice.project) });
30833
+ if (notice.kind === "release") return t2("notice.release", { latest: notice.latest, version: running2, command: npmGlobalInstall(notice.latest) });
30834
+ return t2("notice.projectOlder", { project: notice.project, version: running2 });
30835
+ }
30836
+ var needsRefresh = (cache, now) => !(cache.checkedAt && now - Date.parse(cache.checkedAt) < REFRESH_AFTER_MS) && !(cache.checkingSince && now - Date.parse(cache.checkingSince) < CHECKING_WINDOW_MS);
30837
+ var spawnRefresh = () => {
30838
+ const child = spawn3(process.execPath, [...process.execArgv, process.argv[1], REFRESH_COMMAND], { cwd: homedir2(), detached: true, stdio: "ignore", windowsHide: true });
30839
+ child.on("error", () => {
30840
+ });
30841
+ child.unref();
30842
+ };
30843
+ async function noticeVersion(version2, command, controls = {}) {
30844
+ const env = controls.env ?? process.env;
30845
+ const now = controls.now ?? Date.now();
30846
+ try {
30847
+ const project = projectSettings((await findProjectConfig(controls.cwd ?? process.cwd()))?.text).cli;
30848
+ const scope = noticeScope(command);
30849
+ let cache = {};
30850
+ if (!updateCheckDisabled(env)) {
30851
+ cache = await readUpdateCache(env);
30852
+ if (scope.release && needsRefresh(cache, now)) {
30853
+ await changeUpdateCache(env, (current2) => ({ ...current2, checkingSince: new Date(now).toISOString() }));
30854
+ (controls.refresh ?? spawnRefresh)();
30855
+ }
30856
+ }
30857
+ const notice = chooseNotice(version2, project, cache, now, scope);
30858
+ if (notice) (controls.write ?? ((line2) => process.stderr.write(line2 + "\n")))(noticeText(notice, version2));
30859
+ } catch {
30860
+ }
30861
+ }
30862
+ async function refreshUpdateCache(version2, env = process.env, fetchLatest = fetchLatestVersion, now) {
30863
+ if (updateCheckDisabled(env)) return;
30864
+ const update = await resolveUpdate(version2, fetchLatest);
30865
+ await recordUpdateCheck(env, update, now?.());
30866
+ }
30867
+ async function postponeRelease(version2, env = process.env, now = Date.now()) {
30868
+ const cache = await readUpdateCache(env);
30869
+ const later = cache.latest && isNewerRelease(cache.latest, version2) ? { version: cache.latest, until: new Date(now + LATER_MS).toISOString() } : null;
30870
+ if (later) await changeUpdateCache(env, (current2) => ({ ...current2, later }));
30871
+ return updateLaterV1.parse({ contract: "update-later", version: 1, ok: true, cliVersion: version2, later });
30872
+ }
30873
+ async function runUpdateLater(format2, version2) {
30874
+ const dto = await postponeRelease(version2);
30875
+ process.stdout.write(format2 === "json" ? JSON.stringify(dto) + "\n" : (dto.later ? t2("update.later.postponed", { latest: dto.later.version, until: new Date(dto.later.until).toLocaleString() }) : t2("update.later.nothing", { version: version2 })) + "\n");
30876
+ }
30877
+
30164
30878
  // src/main.ts
30165
30879
  var languageArgs = process.argv.slice(2).slice(0, process.argv.slice(2).indexOf("--") < 0 ? void 0 : process.argv.slice(2).indexOf("--"));
30166
30880
  var selectedLanguage;
@@ -30170,11 +30884,17 @@ for (let i = 0; i < languageArgs.length; i++) {
30170
30884
  if (value === "ko" || value === "en") selectedLanguage = value;
30171
30885
  }
30172
30886
  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());
30887
+ var program2 = new Command().name("gitifact").addOption(new Option("--lang <language>", t2("help.lang")).choices(["ko", "en"])).description(t2("help.program")).version("0.8.3").allowExcessArguments(false).addHelpText("after", "\n" + t2("help.notYet")).action(() => program2.outputHelp());
30888
+ program2.hook("preAction", async (_program, action) => {
30889
+ let top = action;
30890
+ while (top.parent && top.parent !== program2) top = top.parent;
30891
+ if (top.name() !== REFRESH_COMMAND) await noticeVersion("0.8.3", top.name());
30892
+ });
30893
+ program2.command(REFRESH_COMMAND, { hidden: true }).allowExcessArguments(false).action(() => refreshUpdateCache("0.8.3"));
30174
30894
  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"));
30895
+ 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.3"));
30896
+ 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.3"));
30897
+ 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")).addOption(new Option("--later", t2("help.updateLater")).conflicts(["check", "commit"])).action((options) => options.later ? runUpdateLater(options.format, "0.8.3") : options.check ? runUpdateCheck(options.format, "0.8.3") : runUpdate(options, "0.8.3"));
30178
30898
  var listed2 = (command, sorts) => {
30179
30899
  command.allowExcessArguments(false).option("--q <words>", t2("help.listQ")).option("--author <name>", t2("help.listAuthor"));
30180
30900
  if (sorts) command.addOption(new Option("--sort <key>", t2("help.listSort")).choices([...sorts]).default(sorts[0]));
@@ -30196,7 +30916,7 @@ program2.command("check").description(t2("help.check")).allowExcessArguments(fal
30196
30916
  var changes = program2.command("changes").description(t2("help.changes"));
30197
30917
  changes.command("list").description(t2("help.changesList")).allowExcessArguments(false).addOption(format()).action((o) => runChangesList(o));
30198
30918
  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"));
30919
+ 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.3"));
30200
30920
  var guide = program2.command("guide").description(t2("help.guide"));
30201
30921
  guide.command("list").description(t2("help.guideList")).allowExcessArguments(false).addOption(format()).action((o) => runGuideList(o));
30202
30922
  guide.command("show").description(t2("help.guideShow")).argument("<topic>", t2("help.guideTopic")).allowExcessArguments(false).addOption(format()).action((topic, o) => runGuideShow(topic, o));