gyojeong 0.2.0 → 0.3.0

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.
@@ -9,8 +9,8 @@ var __export = (target, all) => {
9
9
  // ../../scripts/review-cli.mjs
10
10
  import { spawn } from "node:child_process";
11
11
  import { randomUUID as randomUUID2 } from "node:crypto";
12
- import { closeSync, existsSync as existsSync3, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
13
- import path3 from "node:path";
12
+ import { chmodSync as chmodSync3, closeSync, existsSync as existsSync5, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
13
+ import path5 from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
15
 
16
16
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
@@ -256,10 +256,10 @@ function mergeDefs(...defs) {
256
256
  function cloneDef(schema) {
257
257
  return mergeDefs(schema._zod.def);
258
258
  }
259
- function getElementAtPath(obj, path4) {
260
- if (!path4)
259
+ function getElementAtPath(obj, path6) {
260
+ if (!path6)
261
261
  return obj;
262
- return path4.reduce((acc, key) => acc?.[key], obj);
262
+ return path6.reduce((acc, key) => acc?.[key], obj);
263
263
  }
264
264
  function promiseAllObject(promisesObj) {
265
265
  const keys = Object.keys(promisesObj);
@@ -668,11 +668,11 @@ function explicitlyAborted(x, startIndex = 0) {
668
668
  }
669
669
  return false;
670
670
  }
671
- function prefixIssues(path4, issues) {
671
+ function prefixIssues(path6, issues) {
672
672
  return issues.map((iss) => {
673
673
  var _a3;
674
674
  (_a3 = iss).path ?? (_a3.path = []);
675
- iss.path.unshift(path4);
675
+ iss.path.unshift(path6);
676
676
  return iss;
677
677
  });
678
678
  }
@@ -819,16 +819,16 @@ function flattenError(error2, mapper = (issue2) => issue2.message) {
819
819
  }
820
820
  function formatError(error2, mapper = (issue2) => issue2.message) {
821
821
  const fieldErrors = { _errors: [] };
822
- const processError = (error3, path4 = []) => {
822
+ const processError = (error3, path6 = []) => {
823
823
  for (const issue2 of error3.issues) {
824
824
  if (issue2.code === "invalid_union" && issue2.errors.length) {
825
- issue2.errors.map((issues) => processError({ issues }, [...path4, ...issue2.path]));
825
+ issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
826
826
  } else if (issue2.code === "invalid_key") {
827
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
827
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
828
828
  } else if (issue2.code === "invalid_element") {
829
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
829
+ processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
830
830
  } else {
831
- const fullpath = [...path4, ...issue2.path];
831
+ const fullpath = [...path6, ...issue2.path];
832
832
  if (fullpath.length === 0) {
833
833
  fieldErrors._errors.push(mapper(issue2));
834
834
  } else {
@@ -2072,7 +2072,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
2072
2072
  const _normalized = cached(() => normalizeDef(def));
2073
2073
  const generateFastpass = (shape) => {
2074
2074
  const doc = new Doc(["shape", "payload", "ctx"]);
2075
- const normalized = _normalized.value;
2075
+ const normalized2 = _normalized.value;
2076
2076
  const parseStr = (key) => {
2077
2077
  const k = esc(key);
2078
2078
  return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;
@@ -2080,11 +2080,11 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
2080
2080
  doc.write(`const input = payload.value;`);
2081
2081
  const ids = /* @__PURE__ */ Object.create(null);
2082
2082
  let counter = 0;
2083
- for (const key of normalized.keys) {
2083
+ for (const key of normalized2.keys) {
2084
2084
  ids[key] = `key_${counter++}`;
2085
2085
  }
2086
2086
  doc.write(`const newResult = {};`);
2087
- for (const key of normalized.keys) {
2087
+ for (const key of normalized2.keys) {
2088
2088
  const id = ids[key];
2089
2089
  const k = esc(key);
2090
2090
  const schema = shape[key];
@@ -5296,10 +5296,107 @@ function trimUrl(url3) {
5296
5296
  function maskCode(text) {
5297
5297
  return text.replace(CODE_SPAN_RE, " ");
5298
5298
  }
5299
- function stripComments(text) {
5299
+ var HASH_COMMENT_LANGUAGES = /* @__PURE__ */ new Set([
5300
+ "bash",
5301
+ "dockerfile",
5302
+ "make",
5303
+ "makefile",
5304
+ "perl",
5305
+ "php",
5306
+ "pl",
5307
+ "powershell",
5308
+ "ps1",
5309
+ "py",
5310
+ "python",
5311
+ "r",
5312
+ "rb",
5313
+ "ruby",
5314
+ "sh",
5315
+ "shell",
5316
+ "toml",
5317
+ "yaml",
5318
+ "yml",
5319
+ "zsh"
5320
+ ]);
5321
+ var SHELL_COMMENT_LANGUAGES = /* @__PURE__ */ new Set(["bash", "sh", "shell", "zsh"]);
5322
+ var WHITESPACE_HASH_COMMENT_LANGUAGES = /* @__PURE__ */ new Set(["yaml", "yml"]);
5323
+ var ANYWHERE_HASH_COMMENT_LANGUAGES = /* @__PURE__ */ new Set([
5324
+ "perl",
5325
+ "php",
5326
+ "pl",
5327
+ "powershell",
5328
+ "ps1",
5329
+ "rb",
5330
+ "ruby"
5331
+ ]);
5332
+ var MAKE_LANGUAGES = /* @__PURE__ */ new Set(["make", "makefile"]);
5333
+ var PHP_ATTRIBUTE_LANGUAGES = /* @__PURE__ */ new Set(["php"]);
5334
+ var LINE_HASH_COMMENT_LANGUAGES = /* @__PURE__ */ new Set(["dockerfile"]);
5335
+ var SLASH_COMMENT_LANGUAGES = /* @__PURE__ */ new Set([
5336
+ "c",
5337
+ "c++",
5338
+ "cc",
5339
+ "cpp",
5340
+ "cs",
5341
+ "csharp",
5342
+ "cxx",
5343
+ "go",
5344
+ "h",
5345
+ "hpp",
5346
+ "java",
5347
+ "javascript",
5348
+ "js",
5349
+ "jsonc",
5350
+ "jsx",
5351
+ "kotlin",
5352
+ "kt",
5353
+ "less",
5354
+ "php",
5355
+ "rs",
5356
+ "rust",
5357
+ "scala",
5358
+ "scss",
5359
+ "swift",
5360
+ "ts",
5361
+ "tsx",
5362
+ "typescript"
5363
+ ]);
5364
+ var C_STYLE_BLOCK_COMMENT_LANGUAGES = /* @__PURE__ */ new Set([...SLASH_COMMENT_LANGUAGES, "css"]);
5365
+ var HTML_COMMENT_LANGUAGES = /* @__PURE__ */ new Set(["html", "markdown", "md", "svelte", "svg", "vue", "xml"]);
5366
+ var PAIRED_DELIMITERS = /* @__PURE__ */ new Map([["(", ")"], ["{", "}"], ["[", "]"], ["<", ">"]]);
5367
+ var delimitedLiteralStart = (rest, previous, language) => {
5368
+ if (language === "ruby" || language === "rb") {
5369
+ return rest.match(/^%(?:[qQrwWixI])?([^\w\s])/u);
5370
+ }
5371
+ if ((language === "perl" || language === "pl") && !/[\w$@%]/u.test(previous)) {
5372
+ return rest.match(/^(?:qq|qw|qx|qr|q)([^\w\s])/u);
5373
+ }
5374
+ return null;
5375
+ };
5376
+ var slashLiteralStart = (text, index, language) => {
5377
+ if (!["perl", "pl", "rb", "ruby"].includes(language) || text[index] !== "/") return false;
5378
+ const beforeText = text.slice(0, index);
5379
+ const before = beforeText.match(/\S\s*$/u)?.[0].trim() ?? "";
5380
+ if (before === "" || /[=(:,!\[\{;~]/u.test(before.at(-1) ?? "")) return true;
5381
+ const word = beforeText.match(/([A-Za-z]+)\s*$/u)?.[1]?.toLowerCase();
5382
+ if (["rb", "ruby"].includes(language)) {
5383
+ return ["and", "if", "not", "or", "return", "then", "unless", "when", "while"].includes(word ?? "");
5384
+ }
5385
+ return ["m", "qr", "s"].includes(word ?? "");
5386
+ };
5387
+ function stripComments(text, language = "") {
5388
+ const normalizedLanguage = language.toLowerCase();
5389
+ const hashComments = HASH_COMMENT_LANGUAGES.has(normalizedLanguage);
5390
+ const slashComments = SLASH_COMMENT_LANGUAGES.has(normalizedLanguage);
5391
+ const cStyleBlockComments = C_STYLE_BLOCK_COMMENT_LANGUAGES.has(normalizedLanguage);
5392
+ const htmlComments = HTML_COMMENT_LANGUAGES.has(normalizedLanguage);
5393
+ const knownLanguage = hashComments || slashComments || cStyleBlockComments || htmlComments;
5300
5394
  let out = "";
5301
5395
  let quote = null;
5396
+ let quoteOpen = null;
5397
+ let quoteDepth = 0;
5302
5398
  let block = null;
5399
+ let lineStart = 0;
5303
5400
  for (let i = 0; i < text.length; i += 1) {
5304
5401
  const rest = text.slice(i);
5305
5402
  if (block) {
@@ -5307,48 +5404,92 @@ function stripComments(text) {
5307
5404
  i += block.length - 1;
5308
5405
  block = null;
5309
5406
  }
5407
+ if (text[i] === "\n") {
5408
+ lineStart = i + 1;
5409
+ }
5310
5410
  continue;
5311
5411
  }
5312
5412
  const char = text[i];
5413
+ const atLineStart = /^\s*$/.test(text.slice(lineStart, i));
5414
+ const genericSlashComment = !knownLanguage && atLineStart && rest.startsWith("//");
5415
+ const genericHashComment = !knownLanguage && atLineStart && char === "#" && !rest.startsWith("#!") && !/^#[\w-]+\s*\{/u.test(rest);
5416
+ const genericCStyleBlockComment = !knownLanguage && atLineStart && rest.startsWith("/*");
5417
+ const genericHtmlComment = !knownLanguage && atLineStart && rest.startsWith("<!--");
5418
+ const previous = text[i - 1] ?? "";
5419
+ const makeRecipe = MAKE_LANGUAGES.has(normalizedLanguage) && text[lineStart] === " ";
5420
+ const shellHashComment = (SHELL_COMMENT_LANGUAGES.has(normalizedLanguage) || makeRecipe) && char === "#" && (i === 0 || /[\s;&|()<>]/u.test(previous));
5421
+ const whitespaceHashComment = WHITESPACE_HASH_COMMENT_LANGUAGES.has(normalizedLanguage) && char === "#" && (i === 0 || /\s/u.test(previous)) && !(PHP_ATTRIBUTE_LANGUAGES.has(normalizedLanguage) && rest.startsWith("#["));
5422
+ const lineHashComment = LINE_HASH_COMMENT_LANGUAGES.has(normalizedLanguage) && char === "#" && atLineStart;
5423
+ const anywhereHashComment = ANYWHERE_HASH_COMMENT_LANGUAGES.has(normalizedLanguage) && char === "#" && !(PHP_ATTRIBUTE_LANGUAGES.has(normalizedLanguage) && rest.startsWith("#["));
5424
+ const makeHashComment = MAKE_LANGUAGES.has(normalizedLanguage) && char === "#" && (makeRecipe ? shellHashComment : true);
5425
+ const languageHashComment = hashComments && char === "#" && (SHELL_COMMENT_LANGUAGES.has(normalizedLanguage) || makeRecipe ? shellHashComment : WHITESPACE_HASH_COMMENT_LANGUAGES.has(normalizedLanguage) ? whitespaceHashComment : LINE_HASH_COMMENT_LANGUAGES.has(normalizedLanguage) ? lineHashComment : MAKE_LANGUAGES.has(normalizedLanguage) ? makeHashComment : anywhereHashComment);
5313
5426
  if (quote) {
5314
5427
  out += char;
5315
5428
  if (char === "\\") {
5316
5429
  out += text[i + 1] ?? "";
5317
5430
  i += 1;
5431
+ } else if (quoteOpen && char === quoteOpen) {
5432
+ quoteDepth += 1;
5318
5433
  } else if (char === quote) {
5319
- quote = null;
5434
+ if (quoteDepth > 0) {
5435
+ quoteDepth -= 1;
5436
+ } else {
5437
+ quote = null;
5438
+ quoteOpen = null;
5439
+ }
5320
5440
  }
5321
5441
  continue;
5322
5442
  }
5443
+ const delimitedLiteral = delimitedLiteralStart(rest, previous, normalizedLanguage);
5444
+ if (delimitedLiteral) {
5445
+ const opener = delimitedLiteral[1];
5446
+ const token = delimitedLiteral[0];
5447
+ quoteOpen = PAIRED_DELIMITERS.has(opener) ? opener : null;
5448
+ quote = PAIRED_DELIMITERS.get(opener) ?? opener;
5449
+ quoteDepth = 0;
5450
+ out += token;
5451
+ i += token.length - 1;
5452
+ continue;
5453
+ }
5323
5454
  if (char === '"' || char === "'" || char === "`") {
5324
5455
  quote = char;
5325
5456
  out += char;
5326
5457
  continue;
5327
5458
  }
5328
- if (rest.startsWith("/*")) {
5459
+ if (slashLiteralStart(text, i, normalizedLanguage)) {
5460
+ quote = "/";
5461
+ out += char;
5462
+ continue;
5463
+ }
5464
+ if (cStyleBlockComments && rest.startsWith("/*") || genericCStyleBlockComment) {
5329
5465
  block = "*/";
5330
5466
  continue;
5331
5467
  }
5332
- if (rest.startsWith("<!--")) {
5468
+ if (htmlComments && rest.startsWith("<!--") || genericHtmlComment) {
5333
5469
  block = "-->";
5334
5470
  continue;
5335
5471
  }
5336
- if (rest.startsWith("//") || char === "#") {
5472
+ if (slashComments && rest.startsWith("//") || languageHashComment || genericSlashComment || genericHashComment) {
5337
5473
  const end = text.indexOf("\n", i);
5338
5474
  if (end === -1) break;
5339
5475
  i = end - 1;
5476
+ lineStart = end;
5340
5477
  continue;
5341
5478
  }
5342
5479
  out += char;
5480
+ if (char === "\n") {
5481
+ lineStart = i + 1;
5482
+ }
5343
5483
  }
5344
5484
  return out;
5345
5485
  }
5346
5486
  function normalizeCodeBlock(text) {
5347
5487
  const raw = text.split("\n");
5348
5488
  const open = raw[0] ?? "";
5489
+ const language = open.match(/^\s*(?:`{3,}|~{3,})\s*([^\s{]+)/)?.[1] ?? "";
5349
5490
  const closes = raw.length > 1 && /^\s*(`{3,}|~{3,})\s*$/.test(raw[raw.length - 1] ?? "");
5350
5491
  const body = raw.slice(1, closes ? -1 : void 0).join("\n");
5351
- const lines = [open, ...stripComments(body).split("\n")];
5492
+ const lines = [open, ...stripComments(body, language).split("\n")];
5352
5493
  const indent = lines[0].match(/^\s*/)[0].length;
5353
5494
  return lines.map((line) => line.slice(Math.min(indent, line.match(/^\s*/)[0].length))).map((line) => line.replace(/\s+$/, "")).filter((line) => line.trim() !== "").join("\n");
5354
5495
  }
@@ -5463,25 +5604,18 @@ var ENGLISH_NUMBER_WORDS = {
5463
5604
  triple: 3,
5464
5605
  both: 2,
5465
5606
  dozen: 12,
5466
- half: 50,
5467
- hex: 16,
5468
- hexadecimal: 16,
5469
- octal: 8,
5470
- binary: 2,
5471
- decimal: 10,
5472
- january: 1,
5473
- february: 2,
5474
- march: 3,
5475
- april: 4,
5476
- may: 5,
5477
- june: 6,
5478
- july: 7,
5479
- august: 8,
5480
- september: 9,
5481
- october: 10,
5482
- november: 11,
5483
- december: 12
5607
+ half: 50
5484
5608
  };
5609
+ var MONTHS = { january: 1, february: 2, march: 3, april: 4, may: 5, june: 6, july: 7, august: 8, september: 9, october: 10, november: 11, december: 12 };
5610
+ var MONTH_DATE_RE = new RegExp(`\\b(${Object.keys(MONTHS).join("|")})\\s+\\d|\\d\\s+(${Object.keys(MONTHS).join("|")})\\b`, "g");
5611
+ var SECOND_UNIT_RE = /\b(?:\d+(?:\.\d+)?|a|an|one|per|each|every)\s+second\b|\bseconds\b/g;
5612
+ var KOREAN_BASE_RE = /(\d+)\s*진/g;
5613
+ var ENGLISH_BASES = { hex: 16, hexadecimal: 16, octal: 8, binary: 2, decimal: 10 };
5614
+ var ENGLISH_BASE_RE = new RegExp(`\\b(${Object.keys(ENGLISH_BASES).join("|")})\\b`, "g");
5615
+ function sourceBases(prose, language) {
5616
+ if (language === "en") return new Set([...prose.toLowerCase().matchAll(ENGLISH_BASE_RE)].map((m) => String(ENGLISH_BASES[m[1]])));
5617
+ return new Set([...prose.matchAll(KOREAN_BASE_RE)].map((m) => normalizeNumber(m[1])));
5618
+ }
5485
5619
  var KOREAN_NUMBER_WORDS = { \uD55C: 1, \uD558\uB098: 1, \uB450: 2, \uB458: 2, \uC138: 3, \uC14B: 3, \uB124: 4, \uB137: 4, \uB2E4\uC12F: 5, \uC5EC\uC12F: 6, \uC77C\uACF1: 7, \uC5EC\uB35F: 8, \uC544\uD649: 9, \uC5F4: 10 };
5486
5620
  var KOREAN_NUMERAL = "\uD55C|\uD558\uB098|\uB450|\uB458|\uC138|\uC14B|\uB124|\uB137|\uB2E4\uC12F|\uC5EC\uC12F|\uC77C\uACF1|\uC5EC\uB35F|\uC544\uD649|\uC5F4";
5487
5621
  var KOREAN_COUNTER = "\uAC1C|\uBC88|\uAC00\uC9C0|\uBA85|\uB300|\uC904|\uB2E8\uACC4|\uBC30|\uC2DC\uAC04|\uBD84|\uCD08|\uC77C|\uC8FC|\uB2EC|\uACF3|\uCABD|\uC7A5|\uCE78";
@@ -5500,13 +5634,15 @@ function normalizeNumber(raw) {
5500
5634
  return /^\d+(\.\d+)?$/.test(plain) ? String(Number(plain)) : plain;
5501
5635
  }
5502
5636
  function digitNumbers(prose) {
5503
- const normalized = prose.replace(KOREAN_DATE_RE, (_, y, m, d) => ` ${y} ${Number(m)} ${Number(d)} `).replace(KOREAN_UNIT_RE, (_, n, unit) => ` ${Number(n) * KOREAN_UNITS[unit]} `);
5504
- return new Set((normalized.match(NUM_RE) ?? []).map(normalizeNumber));
5637
+ const normalized2 = prose.replace(KOREAN_DATE_RE, (_, y, m, d) => ` ${y} ${Number(m)} ${Number(d)} `).replace(KOREAN_UNIT_RE, (_, n, unit) => ` ${Number(n) * KOREAN_UNITS[unit]} `);
5638
+ return new Set((normalized2.match(NUM_RE) ?? []).map(normalizeNumber));
5505
5639
  }
5506
5640
  function justifiedNumbers(prose, language) {
5507
5641
  const out = /* @__PURE__ */ new Set();
5508
5642
  if (language === "en") {
5509
- for (const m of prose.toLowerCase().matchAll(/\b[a-z]+\b/g)) {
5643
+ const lower = prose.toLowerCase();
5644
+ for (const m of lower.matchAll(MONTH_DATE_RE)) out.add(String(MONTHS[m[1] ?? m[2]]));
5645
+ for (const m of lower.replace(SECOND_UNIT_RE, " ").matchAll(/\b[a-z]+\b/g)) {
5510
5646
  if (Object.hasOwn(ENGLISH_NUMBER_WORDS, m[0])) out.add(String(ENGLISH_NUMBER_WORDS[m[0]]));
5511
5647
  }
5512
5648
  if (/\bnon-?negative\b|\bzero-based\b/i.test(prose)) out.add("0");
@@ -5521,14 +5657,17 @@ function justifiedNumbers(prose, language) {
5521
5657
  }
5522
5658
  function checkNumbers(sourceBlocks, draftBlocks, sourceLanguage, add) {
5523
5659
  const sourceProse = numberProse(sourceBlocks);
5524
- const draftProse = numberProse(draftBlocks);
5660
+ const draftText = numberProse(draftBlocks);
5661
+ const bases = sourceBases(sourceProse, sourceLanguage);
5662
+ const draftBases = new Set([...draftText.matchAll(KOREAN_BASE_RE)].map((m) => normalizeNumber(m[1])));
5663
+ const draftProse = draftText.replace(KOREAN_BASE_RE, " ");
5525
5664
  const sourceDigits = digitNumbers(sourceProse);
5526
5665
  const draftDigits = digitNumbers(draftProse);
5527
5666
  const justified = /* @__PURE__ */ new Set([...sourceDigits, ...justifiedNumbers(sourceProse, sourceLanguage)]);
5528
5667
  const draftWords = justifiedNumbers(draftProse, "ko");
5529
- const draftAll = /* @__PURE__ */ new Set([...draftDigits, ...draftWords]);
5668
+ const draftAll = /* @__PURE__ */ new Set([...draftDigits, ...draftWords, ...draftBases]);
5530
5669
  const addedWords = [...draftWords].filter((n) => Number(n) >= 3);
5531
- for (const n of [...draftDigits, ...addedWords]) {
5670
+ for (const n of [...draftDigits, ...addedWords, ...[...draftBases].filter((b) => !bases.has(b))]) {
5532
5671
  if (!justified.has(n)) add("fail", "number", `\uC6D0\uBB38\uC5D0 \uC5C6\uB294 \uC22B\uC790\uAC00 \uC0DD\uACBC\uC2B5\uB2C8\uB2E4: ${n}`, { token: n });
5533
5672
  }
5534
5673
  for (const n of sourceDigits) {
@@ -5539,7 +5678,7 @@ var EN_WEAK_PROHIBITION = [/\bavoid(?:s|ing)?\b/i, /\b(?:forbid|forbids|disallow
5539
5678
  var EN_PERMISSION = /\b(?:may|allowed|permitted|optional|optionally|free to|fine|okay|ok|need not|needn't|not required|no need|(?:do|does)n't need|(?:do|does) not need|(?:do|does)n't have to|(?:do|does) not have to|acceptable)\b/i;
5540
5679
  var EN = {
5541
5680
  prohibition: [
5542
- /(?:^|[.!?:]\s+|\n\s*(?:[-*+]|\d+[.)])?\s*|\*\*|>\s*)(?:do not|don't|never|avoid)\b(?!\s+(?:need|have to|require))/i,
5681
+ /(?:^|[.!?:;]\s+|\n\s*(?:[-*+]|\d+[.)])?\s*|\*\*|>\s*)(?:do not|don't|never|avoid)\b(?!\s+(?:need|have to|require))/i,
5543
5682
  // "should not", and "should, however, not"
5544
5683
  /\b(?:must|shall|should)(?:\s+not|n't|,[^,.;!?\n]{1,25},\s+not)\b/i,
5545
5684
  /\b(?:is|are|isn't|aren't)\s+not\s+(?:allowed|permitted)\b/i,
@@ -5913,30 +6052,156 @@ var occurrenceDelta = (beforeText, afterText, value) => ({
5913
6052
  before: countOccurrences(beforeText, value),
5914
6053
  after: countOccurrences(afterText, value)
5915
6054
  });
5916
- var exactReplacementStatus = (beforeText, afterText, selectedText, correction) => {
6055
+ var overlappingRanges = (text, value) => {
6056
+ const ranges = [];
6057
+ let cursor = 0;
6058
+ while (cursor <= text.length - value.length) {
6059
+ const start = text.indexOf(value, cursor);
6060
+ if (start === -1) {
6061
+ break;
6062
+ }
6063
+ ranges.push({ start, end: start + value.length });
6064
+ cursor = start + 1;
6065
+ }
6066
+ return ranges;
6067
+ };
6068
+ var sourceInsideReplacementStatus = (region, source, replacement) => {
6069
+ if (replacement === source || !replacement.includes(source)) {
6070
+ return { replaced: false, ambiguous: false };
6071
+ }
6072
+ const replacementRanges = overlappingRanges(region, replacement);
6073
+ const ambiguous = replacementRanges.some((range, index) => index > 0 && range.start < replacementRanges[index - 1].end);
6074
+ if (ambiguous) {
6075
+ return { replaced: false, ambiguous: true };
6076
+ }
6077
+ const sourceRanges = overlappingRanges(region, source);
6078
+ const replaced = sourceRanges.length > 0 && sourceRanges.every(
6079
+ (candidate) => replacementRanges.some((container) => candidate.start >= container.start && candidate.end <= container.end)
6080
+ );
6081
+ return { replaced, ambiguous: false };
6082
+ };
6083
+ var nearestIndexOf = (text, value, expected, from = 0) => {
6084
+ let best = -1;
6085
+ let bestDistance = Number.POSITIVE_INFINITY;
6086
+ let clusters = 0;
6087
+ let clusterEnd = -1;
6088
+ let tied = false;
6089
+ let cursor = from;
6090
+ while (cursor <= text.length - value.length) {
6091
+ const found = text.indexOf(value, cursor);
6092
+ if (found === -1) {
6093
+ break;
6094
+ }
6095
+ if (found >= clusterEnd) {
6096
+ clusters += 1;
6097
+ }
6098
+ clusterEnd = Math.max(clusterEnd, found + value.length);
6099
+ const distance = Math.abs(found - expected);
6100
+ if (distance < bestDistance) {
6101
+ best = found;
6102
+ bestDistance = distance;
6103
+ tied = false;
6104
+ } else if (distance === bestDistance) {
6105
+ tied = true;
6106
+ }
6107
+ cursor = found + 1;
6108
+ }
6109
+ return { index: best, ambiguous: tied || clusters > 1 };
6110
+ };
6111
+ var locateContext = (afterText, context, expected, side, from = 0) => {
6112
+ const points = Array.from(context);
6113
+ const minimum = Math.min(8, points.length);
6114
+ for (let length = points.length; length >= minimum; length -= 1) {
6115
+ const value = side === "prefix" ? points.slice(-length).join("") : points.slice(0, length).join("");
6116
+ const found = nearestIndexOf(afterText, value, expected, from);
6117
+ if (found.ambiguous) {
6118
+ return null;
6119
+ }
6120
+ if (found.index !== -1) {
6121
+ return side === "prefix" ? found.index + value.length : found.index;
6122
+ }
6123
+ }
6124
+ return null;
6125
+ };
6126
+ var revisedSelectionRegion = (beforeText, afterText, selection) => {
6127
+ const before = Array.from(beforeText);
6128
+ const unchangedSelectionRegion = () => {
6129
+ const selectedText = selection.selected_text;
6130
+ const selectedAt = afterText.indexOf(selectedText);
6131
+ if (selectedAt === -1 || afterText.indexOf(selectedText, selectedAt + selectedText.length) !== -1) {
6132
+ return null;
6133
+ }
6134
+ return afterText.slice(selectedAt, selectedAt + selectedText.length);
6135
+ };
6136
+ const context = 48;
6137
+ const prefix = before.slice(Math.max(0, selection.start - context), selection.start).join("");
6138
+ const suffix = before.slice(selection.end, Math.min(before.length, selection.end + context)).join("");
6139
+ const selectionStart = before.slice(0, selection.start).join("").length;
6140
+ const selectionEnd = before.slice(0, selection.end).join("").length;
6141
+ const start = prefix.length === 0 ? 0 : locateContext(afterText, prefix, selectionStart - prefix.length, "prefix");
6142
+ if (start === null) {
6143
+ return unchangedSelectionRegion();
6144
+ }
6145
+ const end = suffix.length === 0 ? afterText.length : locateContext(afterText, suffix, selectionEnd, "suffix", start);
6146
+ if (end === null || end < start) {
6147
+ return unchangedSelectionRegion();
6148
+ }
6149
+ return afterText.slice(start, end);
6150
+ };
6151
+ var exactReplacementStatus = (beforeText, afterText, feedback2, correction) => {
6152
+ const selectedText = feedback2.selection.selected_text;
6153
+ let region = revisedSelectionRegion(beforeText, afterText, feedback2.selection);
6154
+ if (region === null) {
6155
+ const selection = feedback2.selection;
6156
+ const beforePoints = Array.from(beforeText);
6157
+ const leading = beforePoints.slice(0, selection.start).join("");
6158
+ const trailing = beforePoints.slice(selection.end).join("");
6159
+ const boundaryOnly = (value) => /^[\s\p{P}]*$/u.test(value);
6160
+ const atStart = boundaryOnly(leading) && afterText.startsWith(leading + correction);
6161
+ const atEnd = boundaryOnly(trailing) && afterText.endsWith(correction + trailing);
6162
+ if (atStart || atEnd) {
6163
+ region = correction;
6164
+ }
6165
+ }
6166
+ if (region === null) {
6167
+ return { resolved: false, selectedReduced: false, notVerifiable: true, checks: ["selection_location_not_verifiable"] };
6168
+ }
5917
6169
  const selected = occurrenceDelta(beforeText, afterText, selectedText);
5918
6170
  const corrected = occurrenceDelta(beforeText, afterText, correction);
5919
- const correctionAdded = corrected.after > corrected.before;
6171
+ const correctionAdded = corrected.after > corrected.before && region.includes(correction);
5920
6172
  const selectedReduced = selected.after < selected.before;
5921
- const selectedSurvivesInsideCorrection = correction.includes(selectedText);
6173
+ const containment = correction.includes(selectedText) ? sourceInsideReplacementStatus(region, selectedText, correction) : null;
6174
+ if (containment?.ambiguous) {
6175
+ return { resolved: false, selectedReduced, notVerifiable: true, checks: ["overlapping_replacement_not_verifiable"] };
6176
+ }
6177
+ const selectedReplaced = containment ? containment.replaced : selectedReduced && !region.includes(selectedText);
5922
6178
  return {
5923
- resolved: correctionAdded && (selectedReduced || selectedSurvivesInsideCorrection),
6179
+ resolved: correctionAdded && selectedReplaced,
5924
6180
  selectedReduced,
5925
6181
  checks: [
5926
6182
  ...correctionAdded ? ["correction_occurrence_increased"] : ["correction_occurrence_not_increased"],
5927
- ...selectedReduced || selectedSurvivesInsideCorrection ? ["selected_occurrence_replaced"] : ["selected_occurrence_not_reduced"]
6183
+ ...selectedReplaced ? ["selected_occurrence_replaced"] : ["selected_occurrence_not_reduced"]
5928
6184
  ]
5929
6185
  };
5930
6186
  };
5931
- var inlineReplacementStatus = (beforeText, afterText, selectedText, correction) => {
6187
+ var inlineReplacementStatus = (beforeText, afterText, feedback2, correction) => {
6188
+ const selectedText = feedback2.selection.selected_text;
5932
6189
  const match = correction.match(/^\s*(.+?)\s*(?:->|→)\s*(.+?)\s*$/su);
5933
6190
  if (!match || !selectedText.includes(match[1])) {
5934
6191
  return null;
5935
6192
  }
6193
+ const region = revisedSelectionRegion(beforeText, afterText, feedback2.selection);
6194
+ if (region === null) {
6195
+ return { resolved: false, sourceReduced: false, notVerifiable: true, checks: ["selection_location_not_verifiable"] };
6196
+ }
5936
6197
  const source = occurrenceDelta(beforeText, afterText, match[1]);
5937
6198
  const target = occurrenceDelta(beforeText, afterText, match[2]);
5938
- const sourceReduced = source.after < source.before;
5939
- const targetAdded = target.after > target.before;
6199
+ const containment = match[2].includes(match[1]) ? sourceInsideReplacementStatus(region, match[1], match[2]) : null;
6200
+ if (containment?.ambiguous) {
6201
+ return { resolved: false, sourceReduced: false, notVerifiable: true, checks: ["overlapping_replacement_not_verifiable"] };
6202
+ }
6203
+ const sourceReduced = containment ? containment.replaced : source.after < source.before && !region.includes(match[1]);
6204
+ const targetAdded = target.after > target.before && region.includes(match[2]);
5940
6205
  return {
5941
6206
  resolved: sourceReduced && targetAdded,
5942
6207
  sourceReduced,
@@ -5953,8 +6218,20 @@ var classifyFeedback = (beforeText, afterText, feedback2, correction) => {
5953
6218
  }
5954
6219
  if (feedback2.category === "deletion") {
5955
6220
  const target = feedback2.term ?? selectedText;
6221
+ const region = revisedSelectionRegion(beforeText, afterText, feedback2.selection);
6222
+ if (region === null) {
6223
+ if (countOccurrences(beforeText, target) === 1) {
6224
+ const occurrences2 = occurrenceDelta(beforeText, afterText, target);
6225
+ const resolved2 = occurrences2.after < occurrences2.before;
6226
+ return {
6227
+ status: resolved2 ? "resolved" : "unresolved",
6228
+ checks: [resolved2 ? "unique_target_occurrence_decreased" : "unique_target_occurrence_not_decreased"]
6229
+ };
6230
+ }
6231
+ return { status: "not_verifiable", checks: ["selection_location_not_verifiable"] };
6232
+ }
5956
6233
  const occurrences = occurrenceDelta(beforeText, afterText, target);
5957
- const resolved = occurrences.after < occurrences.before;
6234
+ const resolved = occurrences.after < occurrences.before && !region.includes(target);
5958
6235
  return {
5959
6236
  status: resolved ? "resolved" : "unresolved",
5960
6237
  checks: [resolved ? "target_occurrence_decreased" : "target_occurrence_not_decreased"]
@@ -5969,7 +6246,10 @@ var classifyFeedback = (beforeText, afterText, feedback2, correction) => {
5969
6246
  checks: [selectedReduced ? "selected_occurrence_decreased" : "selected_occurrence_not_decreased"]
5970
6247
  };
5971
6248
  }
5972
- const exact2 = exactReplacementStatus(beforeText, afterText, selectedText, correction.correction);
6249
+ const exact2 = exactReplacementStatus(beforeText, afterText, feedback2, correction.correction);
6250
+ if (exact2.notVerifiable) {
6251
+ return { status: "not_verifiable", checks: exact2.checks };
6252
+ }
5973
6253
  return { status: exact2.resolved ? "resolved" : "unresolved", checks: exact2.checks };
5974
6254
  }
5975
6255
  if (!correction) {
@@ -5978,10 +6258,13 @@ var classifyFeedback = (beforeText, afterText, feedback2, correction) => {
5978
6258
  const inlineReplacement = inlineReplacementStatus(
5979
6259
  beforeText,
5980
6260
  afterText,
5981
- selectedText,
6261
+ feedback2,
5982
6262
  correction.correction
5983
6263
  );
5984
6264
  if (inlineReplacement) {
6265
+ if (inlineReplacement.notVerifiable) {
6266
+ return { status: "not_verifiable", checks: inlineReplacement.checks };
6267
+ }
5985
6268
  if (!inlineReplacement.resolved && feedback2.category === "unspecified" && inlineReplacement.sourceReduced) {
5986
6269
  return {
5987
6270
  status: "not_verifiable",
@@ -5994,8 +6277,12 @@ var classifyFeedback = (beforeText, afterText, feedback2, correction) => {
5994
6277
  };
5995
6278
  }
5996
6279
  if (feedback2.category === "omission") {
6280
+ const region = revisedSelectionRegion(beforeText, afterText, feedback2.selection);
6281
+ if (region === null) {
6282
+ return { status: "not_verifiable", checks: ["selection_location_not_verifiable"] };
6283
+ }
5997
6284
  const corrected = occurrenceDelta(beforeText, afterText, correction.correction);
5998
- const resolved = corrected.after > corrected.before;
6285
+ const resolved = corrected.after > corrected.before && region.includes(correction.correction);
5999
6286
  return {
6000
6287
  status: resolved ? "resolved" : "unresolved",
6001
6288
  checks: [resolved ? "correction_occurrence_increased" : "correction_occurrence_not_increased"]
@@ -6003,10 +6290,18 @@ var classifyFeedback = (beforeText, afterText, feedback2, correction) => {
6003
6290
  }
6004
6291
  if (feedback2.category === "terminology") {
6005
6292
  const target = feedback2.term ?? selectedText;
6293
+ const region = revisedSelectionRegion(beforeText, afterText, feedback2.selection);
6294
+ if (region === null) {
6295
+ return { status: "not_verifiable", checks: ["selection_location_not_verifiable"] };
6296
+ }
6006
6297
  const targetDelta = occurrenceDelta(beforeText, afterText, target);
6007
6298
  const correctionDelta = occurrenceDelta(beforeText, afterText, correction.correction);
6008
- const correctionAdded = correctionDelta.after > correctionDelta.before;
6009
- const targetReplaced = targetDelta.after < targetDelta.before || correction.correction.includes(target);
6299
+ const correctionAdded = correctionDelta.after > correctionDelta.before && region.includes(correction.correction);
6300
+ const containment = correction.correction.includes(target) ? sourceInsideReplacementStatus(region, target, correction.correction) : null;
6301
+ if (containment?.ambiguous) {
6302
+ return { status: "not_verifiable", checks: ["overlapping_replacement_not_verifiable"] };
6303
+ }
6304
+ const targetReplaced = containment ? containment.replaced : targetDelta.after < targetDelta.before && !region.includes(target);
6010
6305
  const resolved = correctionAdded && targetReplaced;
6011
6306
  return {
6012
6307
  status: resolved ? "resolved" : "unresolved",
@@ -6016,7 +6311,10 @@ var classifyFeedback = (beforeText, afterText, feedback2, correction) => {
6016
6311
  ]
6017
6312
  };
6018
6313
  }
6019
- const exact = exactReplacementStatus(beforeText, afterText, selectedText, correction.correction);
6314
+ const exact = exactReplacementStatus(beforeText, afterText, feedback2, correction.correction);
6315
+ if (exact.notVerifiable) {
6316
+ return { status: "not_verifiable", checks: exact.checks };
6317
+ }
6020
6318
  if (!exact.resolved && feedback2.category === "unspecified" && exact.selectedReduced) {
6021
6319
  return {
6022
6320
  status: "not_verifiable",
@@ -6067,21 +6365,777 @@ var loadIdleConfig = (env = process.env, configPath = path.join(homedir(), ".con
6067
6365
  };
6068
6366
  };
6069
6367
 
6368
+ // ../../scripts/gist-rules.mjs
6369
+ import { spawnSync } from "node:child_process";
6370
+ import { createHash as createHash2 } from "node:crypto";
6371
+ import { chmodSync, existsSync as existsSync2, mkdirSync, mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
6372
+ import { homedir as homedir2, hostname, tmpdir } from "node:os";
6373
+ import path2 from "node:path";
6374
+ var GIST_ID = /^[a-f0-9]{20,64}$/iu;
6375
+ var CONFIG_DIR = path2.join(homedir2(), ".config", "gyojeong");
6376
+ var CONFIG_PATH = path2.join(CONFIG_DIR, "config.json");
6377
+ var DOCUMENT_ONLY_CATEGORIES = /* @__PURE__ */ new Set(["deletion", "omission", "addition", "meaning_shift"]);
6378
+ var runGh = (args, input) => {
6379
+ const result = spawnSync("gh", args, { encoding: "utf8", input, maxBuffer: 32 * 1024 * 1024 });
6380
+ if (result.error?.code === "ENOENT") {
6381
+ throw new Error("gh_unavailable: install GitHub CLI from https://cli.github.com/ and run `gh auth login`");
6382
+ }
6383
+ if (result.error) throw new Error(`gh_unavailable: ${result.error.message}`);
6384
+ if (result.status !== 0) throw new Error(`gist_request_failed: ${(result.stderr || result.stdout).trim()}`);
6385
+ return result.stdout;
6386
+ };
6387
+ var runGit = (args, cwd) => {
6388
+ const result = spawnSync("git", [
6389
+ "-c",
6390
+ "credential.helper=",
6391
+ "-c",
6392
+ "credential.helper=!gh auth git-credential",
6393
+ ...args
6394
+ ], { cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
6395
+ if (result.error) throw new Error(`git_unavailable: ${result.error.message}`);
6396
+ if (result.status !== 0) throw new Error(`gist_git_failed: ${(result.stderr || result.stdout).trim()}`);
6397
+ return result.stdout;
6398
+ };
6399
+ var readLocalConfig = () => {
6400
+ if (!existsSync2(CONFIG_PATH)) return {};
6401
+ return JSON.parse(readFileSync2(CONFIG_PATH, "utf8"));
6402
+ };
6403
+ var readConfig = () => {
6404
+ const local = readLocalConfig();
6405
+ if (process.env.GYOJEONG_GIST_ID !== void 0) {
6406
+ const gistId = process.env.GYOJEONG_GIST_ID.trim();
6407
+ if (!GIST_ID.test(gistId)) throw new Error("invalid_GYOJEONG_GIST_ID");
6408
+ return { ...local, gist_index_id: gistId };
6409
+ }
6410
+ return local;
6411
+ };
6412
+ var writeConfig = (config2) => {
6413
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
6414
+ const current = existsSync2(CONFIG_PATH) ? JSON.parse(readFileSync2(CONFIG_PATH, "utf8")) : {};
6415
+ writeFileSync(CONFIG_PATH, `${JSON.stringify({ ...current, ...config2, storage: "gist" }, null, 2)}
6416
+ `, { mode: 384 });
6417
+ chmodSync(CONFIG_PATH, 384);
6418
+ };
6419
+ var assertPrivateOwnedGist = ({ isPublic, owner }, viewer) => {
6420
+ if (isPublic !== false) throw new Error("gist_must_be_secret");
6421
+ if (!owner || !viewer || owner.toLowerCase() !== viewer.toLowerCase()) {
6422
+ throw new Error("gist_must_be_owned_by_authenticated_user");
6423
+ }
6424
+ };
6425
+ var getVerifiedGist = (gistId) => {
6426
+ const gist = JSON.parse(runGh(["api", `gists/${gistId}`]));
6427
+ const viewer = runGh(["api", "user", "--jq", ".login"]).trim();
6428
+ assertPrivateOwnedGist({ isPublic: gist.public, owner: gist.owner?.login }, viewer);
6429
+ return gist;
6430
+ };
6431
+ var validateConfiguredGist = (gistId) => {
6432
+ getVerifiedGist(gistId);
6433
+ };
6434
+ var ensureRuleGist = () => {
6435
+ const found = readConfig();
6436
+ if (GIST_ID.test(found?.gist_index_id ?? "")) {
6437
+ validateConfiguredGist(found.gist_index_id);
6438
+ writeConfig({ gist_index_id: found.gist_index_id });
6439
+ return found.gist_index_id;
6440
+ }
6441
+ const state = { schema_version: 3, preferences: {} };
6442
+ const body = JSON.stringify({
6443
+ description: "gyojeong index (personal rules)",
6444
+ public: false,
6445
+ files: { "index.json": { content: JSON.stringify(state, null, 2) } }
6446
+ });
6447
+ const id = runGh(["api", "gists", "-X", "POST", "--input", "-", "--jq", ".id"], body).trim();
6448
+ if (!GIST_ID.test(id)) throw new Error("invalid_gist_id");
6449
+ writeConfig({ gist_index_id: id });
6450
+ return id;
6451
+ };
6452
+ var readIndex = (gistId) => {
6453
+ const directory = mkdtempSync(path2.join(tmpdir(), "gyojeong-rules-"));
6454
+ try {
6455
+ runGit(["clone", "--quiet", `https://gist.github.com/${gistId}.git`, directory]);
6456
+ } catch (error2) {
6457
+ rmSync(directory, { recursive: true, force: true });
6458
+ throw error2;
6459
+ }
6460
+ try {
6461
+ const indexPath = path2.join(directory, "index.json");
6462
+ if (!existsSync2(indexPath)) throw new Error("missing_gist_index");
6463
+ const state = JSON.parse(readFileSync2(indexPath, "utf8"));
6464
+ if (!state || typeof state !== "object" || Array.isArray(state)) throw new Error("invalid_gist_index");
6465
+ state.preferences ??= {};
6466
+ return { state, directory, indexPath, summaryPath: path2.join(directory, "rules.md") };
6467
+ } catch (error2) {
6468
+ rmSync(directory, { recursive: true, force: true });
6469
+ throw error2;
6470
+ }
6471
+ };
6472
+ var parseGistIndex = (gist) => {
6473
+ const file = gist?.files?.["index.json"];
6474
+ if (!file) throw new Error("missing_gist_index");
6475
+ if (file.truncated || typeof file.content !== "string") throw new Error("gist_index_truncated");
6476
+ const state = JSON.parse(file.content);
6477
+ if (!state || typeof state !== "object" || Array.isArray(state)) throw new Error("invalid_gist_index");
6478
+ state.preferences ??= {};
6479
+ return state;
6480
+ };
6481
+ var readGistRules = () => {
6482
+ const found = readConfig();
6483
+ let gistId;
6484
+ let gist;
6485
+ if (GIST_ID.test(found?.gist_index_id ?? "")) {
6486
+ gistId = found.gist_index_id;
6487
+ gist = getVerifiedGist(gistId);
6488
+ } else {
6489
+ gistId = ensureRuleGist();
6490
+ gist = getVerifiedGist(gistId);
6491
+ }
6492
+ let state;
6493
+ try {
6494
+ state = parseGistIndex(gist);
6495
+ } catch (error2) {
6496
+ if (error2.message !== "gist_index_truncated") throw error2;
6497
+ const cloned = readIndex(gistId);
6498
+ try {
6499
+ state = cloned.state;
6500
+ } finally {
6501
+ rmSync(cloned.directory, { recursive: true, force: true });
6502
+ }
6503
+ }
6504
+ const { state: current } = migrateRuleState(state);
6505
+ return {
6506
+ gist_id: gistId,
6507
+ onboarding_required: !isRulesOnboardingComplete(readConfig()),
6508
+ active_rules: appliedRulebook(current.preferences)
6509
+ };
6510
+ };
6511
+ var commitIndex = (state, directory, indexPath, summaryPath, message, removeOldHistory = false) => {
6512
+ writeFileSync(indexPath, `${JSON.stringify(state)}
6513
+ `, { mode: 384 });
6514
+ writeFileSync(summaryPath, renderRuleSummary(state.preferences), { mode: 384 });
6515
+ if (removeOldHistory) rmSync(path2.join(directory, "review-history.json"), { force: true });
6516
+ runGit(["config", "user.name", "gyojeong"], directory);
6517
+ runGit(["config", "user.email", "gyojeong@local"], directory);
6518
+ runGit(["add", "-A"], directory);
6519
+ runGit(["commit", "--quiet", "-m", message], directory);
6520
+ };
6521
+ var normalized = (value) => String(value ?? "").normalize("NFC").trim().replace(/\s+/gu, " ").toLowerCase();
6522
+ var rulesEnvironmentKey = ({
6523
+ machine = hostname(),
6524
+ platform = process.platform,
6525
+ home = homedir2(),
6526
+ codexHome = process.env.CODEX_HOME || path2.join(home, ".codex"),
6527
+ claudeHome = process.env.CLAUDE_CONFIG_DIR || path2.join(home, ".claude")
6528
+ } = {}) => createHash2("sha256").update(JSON.stringify([machine, platform, path2.resolve(home), path2.resolve(codexHome), path2.resolve(claudeHome)])).digest("hex").slice(0, 24);
6529
+ var isRulesOnboardingComplete = (config2, environmentKey = rulesEnvironmentKey()) => {
6530
+ return Boolean(config2?.rules_onboarding_environments?.[environmentKey]?.completed_at);
6531
+ };
6532
+ var preferenceKey = (preference) => {
6533
+ const category = preference?.structured_value?.category ?? preference?.category ?? "unspecified";
6534
+ const subject = preference?.term || preference?.selected_text || "";
6535
+ return `${category}\0${normalized(subject)}\0${normalized(preference?.correction)}`;
6536
+ };
6537
+ var distillRuleState = (state) => {
6538
+ const preferences = {};
6539
+ const groups = /* @__PURE__ */ new Map();
6540
+ const mappedRules = new Set(Object.values(state.preferences ?? {}).filter((value) => value.rule_status === "active" && (value.mappings?.length || value.term)).map((value) => value.rule));
6541
+ for (const [id, value] of Object.entries(state.preferences ?? {})) {
6542
+ if (!["active", "candidate"].includes(value.rule_status) || !value.rule) continue;
6543
+ if (value.rule_status === "candidate" && (!value.repeatable || !value.evidence?.length)) continue;
6544
+ if (value.rule_status === "active" && !value.term && !value.mappings?.length && !value.context && mappedRules.has(value.rule)) continue;
6545
+ const rawMappings = value.mappings ?? (value.term && value.correction ? [{ from: value.term, to: value.correction }] : void 0);
6546
+ const mappings = rawMappings?.map((mapping) => ({
6547
+ from: mapping.from,
6548
+ to: mapping.to,
6549
+ ...mapping.context || value.context ? { context: mapping.context ?? value.context } : {}
6550
+ }));
6551
+ const glossary = value.rule_status === "active" && mappings?.length;
6552
+ const rule = glossary ? "\uAC80\uC218\uB41C \uC6A9\uC5B4 \uD45C\uAE30\uB97C \uBB38\uB9E5\uC5D0 \uB9DE\uAC8C \uC77C\uAD00\uB418\uAC8C \uC0AC\uC6A9\uD55C\uB2E4." : value.rule;
6553
+ const groupKey = glossary ? "active-glossary" : `${value.rule_status}\0${normalized(rule)}\0${normalized(value.context)}\0${value.repeatable === true}`;
6554
+ const existingId = groups.get(groupKey);
6555
+ if (existingId) {
6556
+ const existing = preferences[existingId];
6557
+ for (const mapping of mappings ?? []) {
6558
+ if (!existing.mappings?.some((item) => item.from === mapping.from && item.to === mapping.to && item.context === mapping.context)) (existing.mappings ??= []).push(mapping);
6559
+ }
6560
+ for (const evidence of value.rule_status === "candidate" ? value.evidence ?? [] : []) {
6561
+ if (!existing.evidence?.some((item) => item.review_id === evidence.review_id && item.feedback_id === evidence.feedback_id)) {
6562
+ (existing.evidence ??= []).push(evidence);
6563
+ }
6564
+ }
6565
+ for (const sourceId of value.source_ids ?? (value.selected_text ? [id] : [])) {
6566
+ if (!existing.source_ids?.includes(sourceId)) (existing.source_ids ??= []).push(sourceId);
6567
+ }
6568
+ existing.occurrences = Math.max(
6569
+ existing.occurrences ?? 1,
6570
+ value.occurrences ?? 1,
6571
+ new Set((existing.evidence ?? []).map((item) => item.review_id)).size
6572
+ );
6573
+ continue;
6574
+ }
6575
+ groups.set(groupKey, id);
6576
+ preferences[id] = {
6577
+ category: value.category,
6578
+ rule,
6579
+ ...!glossary && value.context ? { context: value.context } : {},
6580
+ ...mappings ? { mappings } : {},
6581
+ ...value.repeatable ? { repeatable: true } : {},
6582
+ ...value.rule_status === "candidate" && value.evidence ? { evidence: value.evidence } : {},
6583
+ ...value.source_ids?.length || value.selected_text ? { source_ids: value.source_ids ?? [id] } : {},
6584
+ rule_status: value.rule_status,
6585
+ occurrences: value.occurrences,
6586
+ promotion_reason: value.promotion_reason,
6587
+ rule_updated_at: value.rule_updated_at
6588
+ };
6589
+ }
6590
+ return { schema_version: 3, preferences };
6591
+ };
6592
+ var LEGACY_FIELDS = /* @__PURE__ */ new Set([
6593
+ "owner_id",
6594
+ "project_id",
6595
+ "kind",
6596
+ "source_language",
6597
+ "target_language",
6598
+ "domain",
6599
+ "structured_value",
6600
+ "selected_text",
6601
+ "term",
6602
+ "correction",
6603
+ "created_at",
6604
+ "evidence",
6605
+ "occurrences",
6606
+ "promotion_reason",
6607
+ "rule_status",
6608
+ "rule_updated_at",
6609
+ "rule",
6610
+ "context",
6611
+ "category",
6612
+ "repeatable",
6613
+ "mappings",
6614
+ "source_ids"
6615
+ ]);
6616
+ var compactRuleState = (state) => {
6617
+ const allowedTop = /* @__PURE__ */ new Set(["schema_version", "runs", "job_index", "idempotency", "preferences", "rules"]);
6618
+ for (const key of Object.keys(state)) {
6619
+ if (!allowedTop.has(key)) throw new Error(`unknown_gist_index_field: ${key}`);
6620
+ }
6621
+ const preferences = {};
6622
+ for (const [id, value] of Object.entries(state.preferences ?? {})) {
6623
+ for (const key of Object.keys(value)) {
6624
+ if (!LEGACY_FIELDS.has(key)) throw new Error(`unknown_gist_preference_field: ${key}`);
6625
+ }
6626
+ const shadow = state.rules?.[id];
6627
+ if (shadow && (shadow.status !== value.rule_status || shadow.occurrences !== value.occurrences || shadow.promotion_reason !== value.promotion_reason)) {
6628
+ throw new Error(`conflicting_gist_rule: ${id}`);
6629
+ }
6630
+ preferences[id] = {
6631
+ category: value.category ?? value.structured_value?.category,
6632
+ selected_text: value.selected_text,
6633
+ ...value.term ? { term: value.term } : {},
6634
+ correction: value.correction,
6635
+ ...value.rule ? { rule: value.rule } : {},
6636
+ ...value.context ? { context: value.context } : {},
6637
+ ...value.mappings ? { mappings: value.mappings } : {},
6638
+ ...value.repeatable ? { repeatable: true } : {},
6639
+ ...value.source_ids ? { source_ids: value.source_ids } : {},
6640
+ created_at: value.created_at,
6641
+ ...value.evidence ? { evidence: value.evidence } : {},
6642
+ rule_status: value.rule_status,
6643
+ occurrences: value.occurrences,
6644
+ promotion_reason: value.promotion_reason,
6645
+ rule_updated_at: value.rule_updated_at
6646
+ };
6647
+ }
6648
+ if (state.rules && Object.keys(state.rules).some((id) => !preferences[id])) {
6649
+ throw new Error("orphan_gist_rule");
6650
+ }
6651
+ return { schema_version: 3, preferences };
6652
+ };
6653
+ var migrateRuleState = (state, now = (/* @__PURE__ */ new Date()).toISOString()) => {
6654
+ state.preferences ??= {};
6655
+ const entries = Object.entries(state.preferences ?? {});
6656
+ const counts = /* @__PURE__ */ new Map();
6657
+ for (const [, preference] of entries) {
6658
+ if (!preference.selected_text || !preference.correction) continue;
6659
+ const key = preferenceKey(preference);
6660
+ counts.set(key, (counts.get(key) ?? 0) + 1);
6661
+ }
6662
+ const semanticEvidence = /* @__PURE__ */ new Map();
6663
+ for (const [id, preference] of entries) {
6664
+ if (!preference.repeatable || !preference.rule || DOCUMENT_ONLY_CATEGORIES.has(preference.category)) continue;
6665
+ const key = `${normalized(preference.rule)}\0${normalized(preference.context)}`;
6666
+ const sources = semanticEvidence.get(key) ?? /* @__PURE__ */ new Set();
6667
+ for (const item of preference.evidence ?? []) sources.add(item.review_id ?? id);
6668
+ if (!preference.evidence?.length) sources.add(id);
6669
+ semanticEvidence.set(key, sources);
6670
+ }
6671
+ let active = 0;
6672
+ let candidates = 0;
6673
+ for (const [id, preference] of entries) {
6674
+ const category = preference?.structured_value?.category ?? preference?.category;
6675
+ const occurrences = preference.selected_text && preference.correction ? counts.get(preferenceKey(preference)) ?? 1 : preference.occurrences ?? 1;
6676
+ const excluded = DOCUMENT_ONLY_CATEGORIES.has(category);
6677
+ const semanticCount = semanticEvidence.get(`${normalized(preference.rule)}\0${normalized(preference.context)}`)?.size ?? 0;
6678
+ const repeatedSemanticRule = preference.repeatable && semanticCount >= 3;
6679
+ const status = excluded ? "excluded" : repeatedSemanticRule ? "active" : preference.rule_status ?? "candidate";
6680
+ preference.rule_status = status;
6681
+ preference.occurrences = Math.max(preference.occurrences ?? 1, occurrences, semanticCount);
6682
+ preference.rule_updated_at ??= preference.created_at ?? now;
6683
+ const automaticReason = excluded ? "document_only_category" : repeatedSemanticRule ? "repeated_semantic" : occurrences >= 2 ? "repeated_pending_agent" : "awaiting_repeat_or_agent";
6684
+ if (excluded || repeatedSemanticRule || !preference.promotion_reason || preference.promotion_reason === "repeated_pending_agent" || preference.promotion_reason === "awaiting_repeat_or_agent") {
6685
+ preference.promotion_reason = automaticReason;
6686
+ }
6687
+ if (status === "active") active += 1;
6688
+ else if (status === "candidate") candidates += 1;
6689
+ }
6690
+ return { state: compactRuleState(state), active, candidates };
6691
+ };
6692
+ var preferenceId = (reviewId, feedbackId) => createHash2("sha256").update(`gyojeong-rule:${reviewId}:${feedbackId}`).digest("hex").slice(0, 32);
6693
+ var ruleCurationPreview = (reviewId, feedback2) => feedback2.filter(isDurableFeedback).map((item) => ({
6694
+ id: preferenceId(reviewId, item.id),
6695
+ category: item.category,
6696
+ ...item.term ? { term: item.term } : {},
6697
+ selected_text: item.selection.selected_text,
6698
+ correction: item.correction
6699
+ }));
6700
+ var assertReviewCurations = (reviewId, feedback2, curations) => {
6701
+ const curatedIds = new Set(curations.map((item) => item.id));
6702
+ const missing = ruleCurationPreview(reviewId, feedback2).map((item) => item.id).filter((id) => !curatedIds.has(id));
6703
+ if (missing.length) throw new Error(`missing_rule_curations: ${missing.join(",")}`);
6704
+ };
6705
+ var retainedReviewFeedback = (reviewId, feedback2, curations) => {
6706
+ const decisions = new Map(curations.map((item) => [item.id, item.status]));
6707
+ return feedback2.filter((item) => decisions.get(preferenceId(reviewId, item.id)) !== "discard");
6708
+ };
6709
+ var isDurableFeedback = (item) => item?.scope === "personal" && Boolean(item?.correction) && !DOCUMENT_ONLY_CATEGORIES.has(item?.category);
6710
+ var syncReviewRules = (state, reviewId, feedback2, now = (/* @__PURE__ */ new Date()).toISOString()) => {
6711
+ state = migrateRuleState(state, now).state;
6712
+ const seen = new Set(Object.entries(state.preferences).flatMap(([id, value]) => [id, ...value.source_ids ?? []]));
6713
+ let added = 0;
6714
+ for (const item of feedback2) {
6715
+ if (!isDurableFeedback(item)) continue;
6716
+ const id = preferenceId(reviewId, item.id);
6717
+ if (seen.has(id)) continue;
6718
+ state.preferences[id] = {
6719
+ category: item.category,
6720
+ selected_text: item.selection.selected_text,
6721
+ ...item.term ? { term: item.term } : {},
6722
+ correction: item.correction,
6723
+ created_at: item.created_at ?? now,
6724
+ evidence: [{ review_id: reviewId, feedback_id: item.id }]
6725
+ };
6726
+ added += 1;
6727
+ }
6728
+ return { ...migrateRuleState(state, now), added };
6729
+ };
6730
+ var sessionFeedbackId = (entry) => createHash2("sha256").update(JSON.stringify([entry.source, entry.session_id, entry.selected_text, entry.correction, entry.term ?? ""])).digest("hex").slice(0, 32);
6731
+ var syncSessionCorrections = (state, entries, now = (/* @__PURE__ */ new Date()).toISOString()) => {
6732
+ if (!Array.isArray(entries) || entries.length === 0 || entries.length > 100) throw new Error("invalid_session_corrections");
6733
+ const ids = [];
6734
+ let added = 0;
6735
+ for (const raw of entries) {
6736
+ const entry = {
6737
+ source: String(raw?.source ?? "").trim(),
6738
+ session_id: String(raw?.session_id ?? "").trim(),
6739
+ category: String(raw?.category ?? "unspecified").trim(),
6740
+ selected_text: String(raw?.selected_text ?? "").normalize("NFC").trim(),
6741
+ correction: String(raw?.correction ?? "").normalize("NFC").trim(),
6742
+ term: raw?.term ? String(raw.term).normalize("NFC").trim() : void 0,
6743
+ rule: raw?.rule ? String(raw.rule).normalize("NFC").trim() : void 0,
6744
+ context: raw?.context ? String(raw.context).normalize("NFC").trim() : void 0,
6745
+ general: raw?.general === true,
6746
+ repeatable: raw?.repeatable === true
6747
+ };
6748
+ if (!["claude", "codex", "session"].includes(entry.source) || !/^[\w.:-]{1,200}$/u.test(entry.session_id) || !["unspecified", "awkward_expression", "terminology", "register"].includes(entry.category) || !entry.selected_text || !entry.correction || raw?.repeatable !== void 0 && typeof raw.repeatable !== "boolean" || entry.selected_text.length > 2e3 || entry.correction.length > 2e3 || entry.rule && entry.rule.length > 240 || entry.context && entry.context.length > 160 || /[\r\n]/u.test(entry.rule ?? "") || /[\r\n]/u.test(entry.context ?? "") || !entry.rule || entry.general && entry.repeatable) {
6749
+ throw new Error("invalid_session_correction");
6750
+ }
6751
+ const feedbackId = sessionFeedbackId(entry);
6752
+ const reviewId = `session:${entry.source}:${entry.session_id}`;
6753
+ const id = preferenceId(reviewId, feedbackId);
6754
+ const synced = syncReviewRules(state, reviewId, [{
6755
+ id: feedbackId,
6756
+ scope: "personal",
6757
+ category: entry.category,
6758
+ selection: { selected_text: entry.selected_text },
6759
+ correction: entry.correction,
6760
+ ...entry.term ? { term: entry.term } : {},
6761
+ created_at: now
6762
+ }], now);
6763
+ state = synced.state;
6764
+ added += synced.added;
6765
+ const preference = state.preferences[id];
6766
+ if (preference && synced.added > 0) {
6767
+ if (entry.rule) preference.rule = entry.rule;
6768
+ if (entry.context) preference.context = entry.context;
6769
+ if (entry.repeatable) preference.repeatable = true;
6770
+ preference.rule_status = entry.general ? "active" : "candidate";
6771
+ preference.promotion_reason = entry.general ? "agent_general" : entry.repeatable ? "semantic_pending" : "agent_contextual";
6772
+ preference.rule_updated_at = now;
6773
+ }
6774
+ ids.push(id);
6775
+ }
6776
+ return { ...migrateRuleState(state, now), added, ids };
6777
+ };
6778
+ var curateRules = (state, updates, now = (/* @__PURE__ */ new Date()).toISOString()) => {
6779
+ if (!Array.isArray(updates) || updates.length === 0 || updates.length > 100) throw new Error("invalid_rule_curations");
6780
+ state = migrateRuleState(state, now).state;
6781
+ for (const raw of updates) {
6782
+ const preference = state.preferences[raw?.id];
6783
+ const rule = typeof raw?.rule === "string" ? raw.rule.normalize("NFC").trim() : "";
6784
+ const context = typeof raw?.context === "string" ? raw.context.normalize("NFC").trim() : "";
6785
+ const mappings = raw?.mappings?.map((mapping) => ({
6786
+ from: typeof mapping?.from === "string" ? mapping.from.normalize("NFC").trim() : "",
6787
+ to: typeof mapping?.to === "string" ? mapping.to.normalize("NFC").trim() : ""
6788
+ }));
6789
+ if (!preference || raw?.status !== "discard" && !rule || rule.length > 240 || context.length > 160 || /[\r\n]/u.test(rule) || /[\r\n]/u.test(context) || !["active", "candidate", "discard"].includes(raw?.status) || raw.repeatable !== void 0 && typeof raw.repeatable !== "boolean" || raw.mappings !== void 0 && (!Array.isArray(raw.mappings) || !mappings.length || mappings.length > 8 || mappings.some(({ from, to }) => !from || !to || from.length > 80 || to.length > 80 || /[\r\n]/u.test(`${from}${to}`)))) {
6790
+ throw new Error("invalid_rule_curation");
6791
+ }
6792
+ if (DOCUMENT_ONLY_CATEGORIES.has(preference.category) && raw.status === "active") {
6793
+ throw new Error(`document_only_preference: ${raw.id}`);
6794
+ }
6795
+ if (raw.status === "discard") {
6796
+ delete state.preferences[raw.id];
6797
+ continue;
6798
+ }
6799
+ preference.rule = rule;
6800
+ if (mappings) preference.mappings = mappings;
6801
+ if (context) preference.context = context;
6802
+ else delete preference.context;
6803
+ preference.rule_status = raw.status;
6804
+ if (raw.repeatable === true && raw.status === "candidate") preference.repeatable = true;
6805
+ else delete preference.repeatable;
6806
+ preference.promotion_reason = raw.status === "active" ? "agent_general" : preference.repeatable ? "semantic_pending" : "agent_contextual";
6807
+ preference.rule_updated_at = now;
6808
+ }
6809
+ return migrateRuleState(state, now);
6810
+ };
6811
+ var presentActiveRules = (preferences) => {
6812
+ const rules2 = /* @__PURE__ */ new Map();
6813
+ const terms = /* @__PURE__ */ new Map();
6814
+ const termRules = /* @__PURE__ */ new Set();
6815
+ for (const [id, value] of Object.entries(preferences)) {
6816
+ if (value.rule_status !== "active") continue;
6817
+ const mappings = value.mappings ?? (value.term ? [{ from: value.term, to: value.correction }] : []);
6818
+ for (const mapping of mappings) {
6819
+ const context = mapping.context ?? value.context;
6820
+ const key = `${mapping.from}\0${mapping.to}\0${context ?? ""}`;
6821
+ const entry = terms.get(key) ?? { ...mapping, ...context ? { context } : {}, ids: [] };
6822
+ entry.ids.push(id);
6823
+ terms.set(key, entry);
6824
+ }
6825
+ if (mappings.length && value.rule) termRules.add(value.rule);
6826
+ }
6827
+ if (terms.size) rules2.set("glossary", {
6828
+ rule: "\uAC80\uC218\uB41C \uC6A9\uC5B4 \uD45C\uAE30\uB97C \uBB38\uB9E5\uC5D0 \uB9DE\uAC8C \uC77C\uAD00\uB418\uAC8C \uC0AC\uC6A9\uD55C\uB2E4.",
6829
+ terms: [...terms.values()]
6830
+ });
6831
+ for (const [id, value] of Object.entries(preferences)) {
6832
+ if (value.rule_status !== "active") continue;
6833
+ if (value.term || value.mappings || value.rule && !value.context && termRules.has(value.rule)) continue;
6834
+ if (!value.rule) {
6835
+ rules2.set(id, {
6836
+ id,
6837
+ category: value.category,
6838
+ term: value.term,
6839
+ selected_text: value.selected_text,
6840
+ correction: value.correction,
6841
+ occurrences: value.occurrences,
6842
+ reason: value.promotion_reason
6843
+ });
6844
+ continue;
6845
+ }
6846
+ const key = `${value.rule}\0${value.context ?? ""}`;
6847
+ const existing = rules2.get(key);
6848
+ if (existing) {
6849
+ existing.ids.push(id);
6850
+ } else {
6851
+ rules2.set(key, { ids: [id], rule: value.rule, ...value.context ? { context: value.context } : {} });
6852
+ }
6853
+ }
6854
+ return [...rules2.values()];
6855
+ };
6856
+ var renderRuleSummary = (preferences) => {
6857
+ const lines = ["# gyojeong active rules", "", "Apply these to Korean text you write, revise, or save. Candidate evidence is not a rule.", ""];
6858
+ for (const entry of presentActiveRules(preferences)) {
6859
+ if (entry.terms) {
6860
+ lines.push(`- ${entry.rule}`);
6861
+ lines.push(` ${entry.terms.map(({ from, to, context }) => `${from} -> ${to}${context ? ` (${context})` : ""}`).join(", ")}`);
6862
+ } else if (entry.rule) lines.push(`- ${entry.rule}${entry.context ? ` (${entry.context})` : ""}`);
6863
+ else lines.push(`- ${entry.term ?? entry.selected_text} -> ${entry.correction}`);
6864
+ }
6865
+ return `${lines.join("\n")}
6866
+ `;
6867
+ };
6868
+ var appliedRulebook = (preferences) => presentActiveRules(preferences).map((entry) => {
6869
+ if (entry.terms) return { rule: entry.rule, terms: entry.terms.map(({ ids: ids2, ...term }) => term) };
6870
+ const { id, ids, ...instruction } = entry;
6871
+ return instruction;
6872
+ });
6873
+ var updateGistRules = ({ reviewId, feedback: feedback2 = [], promoteIds = [], candidateIds = [], sessionCorrections = [], curations = [], completeOnboarding = false, includeCandidates = false, distill = false } = {}) => {
6874
+ if (reviewId) assertReviewCurations(reviewId, feedback2, curations);
6875
+ const gistId = ensureRuleGist();
6876
+ const environmentKey = rulesEnvironmentKey();
6877
+ for (let attempt = 0; attempt < 3; attempt += 1) {
6878
+ const { state, directory, indexPath, summaryPath } = readIndex(gistId);
6879
+ try {
6880
+ const before = JSON.stringify(state);
6881
+ const retainedFeedback = reviewId ? retainedReviewFeedback(reviewId, feedback2, curations) : [];
6882
+ let synced = reviewId ? syncReviewRules(state, reviewId, retainedFeedback) : migrateRuleState(state);
6883
+ let added = synced.added ?? 0;
6884
+ if (sessionCorrections.length) {
6885
+ synced = syncSessionCorrections(synced.state, sessionCorrections);
6886
+ added += synced.added;
6887
+ }
6888
+ const rawState = structuredClone(synced.state);
6889
+ if (curations.length) {
6890
+ const seen = new Set(Object.values(synced.state.preferences).flatMap((value) => value.source_ids ?? []));
6891
+ const pending = curations.filter((item) => (item.status !== "discard" || synced.state.preferences[item.id]) && (synced.state.preferences[item.id] || !seen.has(item.id)));
6892
+ if (pending.length) synced = curateRules(synced.state, pending);
6893
+ }
6894
+ for (const id of promoteIds) {
6895
+ const preference = synced.state.preferences[id];
6896
+ if (!preference) throw new Error(`unknown_preference: ${id}`);
6897
+ if (DOCUMENT_ONLY_CATEGORIES.has(preference?.structured_value?.category ?? preference?.category)) {
6898
+ throw new Error(`document_only_preference: ${id}`);
6899
+ }
6900
+ if (!preference.rule) throw new Error(`missing_rule_summary: ${id}`);
6901
+ preference.rule_status = "active";
6902
+ preference.promotion_reason = "agent_general";
6903
+ preference.rule_updated_at = (/* @__PURE__ */ new Date()).toISOString();
6904
+ }
6905
+ for (const id of candidateIds) {
6906
+ const preference = synced.state.preferences[id];
6907
+ if (!preference) throw new Error(`unknown_preference: ${id}`);
6908
+ if (DOCUMENT_ONLY_CATEGORIES.has(preference?.structured_value?.category ?? preference?.category)) {
6909
+ throw new Error(`document_only_preference: ${id}`);
6910
+ }
6911
+ preference.rule_status = "candidate";
6912
+ preference.promotion_reason = "agent_contextual";
6913
+ preference.rule_updated_at = (/* @__PURE__ */ new Date()).toISOString();
6914
+ }
6915
+ const shouldDistill = distill || added > 0 || curations.length > 0;
6916
+ if (shouldDistill) synced.state = distillRuleState(synced.state);
6917
+ const summary = renderRuleSummary(synced.state.preferences);
6918
+ const oldHistory = path2.join(directory, "review-history.json");
6919
+ const finalChanged = added > 0 || JSON.stringify(synced.state) !== before || !existsSync2(summaryPath) || readFileSync2(summaryPath, "utf8") !== summary || shouldDistill && existsSync2(oldHistory);
6920
+ if (finalChanged) {
6921
+ try {
6922
+ if (added > 0) commitIndex(rawState, directory, indexPath, summaryPath, "Archive correction evidence");
6923
+ commitIndex(synced.state, directory, indexPath, summaryPath, "Update distilled personal rules", shouldDistill);
6924
+ runGit(["push", "--quiet", "origin", "HEAD"], directory);
6925
+ } catch (error2) {
6926
+ if (/rejected|fetch first|non-fast-forward/iu.test(String(error2)) && attempt < 2) continue;
6927
+ throw error2;
6928
+ }
6929
+ }
6930
+ const preferences = Object.entries(synced.state.preferences);
6931
+ if (completeOnboarding) {
6932
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
6933
+ const localConfig = readLocalConfig();
6934
+ writeConfig({
6935
+ rules_onboarding_environments: {
6936
+ ...localConfig.rules_onboarding_environments ?? {},
6937
+ [environmentKey]: { completed_at: completedAt }
6938
+ }
6939
+ });
6940
+ }
6941
+ const currentConfig = readConfig();
6942
+ const onboardingComplete = isRulesOnboardingComplete(currentConfig, environmentKey);
6943
+ return {
6944
+ gist_id: gistId,
6945
+ storage: "gist",
6946
+ added,
6947
+ active: preferences.filter(([, value]) => value.rule_status === "active").length,
6948
+ candidates: preferences.filter(([, value]) => value.rule_status === "candidate").length,
6949
+ excluded: preferences.filter(([, value]) => value.rule_status === "excluded").length,
6950
+ rules_environment_id: environmentKey,
6951
+ onboarding_required: !onboardingComplete,
6952
+ session_rule_ids: synced.ids ?? [],
6953
+ active_rules: appliedRulebook(synced.state.preferences),
6954
+ ...includeCandidates || reviewId ? { recent: preferences.filter(([, value]) => value.rule_status === "candidate").filter(([, value]) => !reviewId || value.evidence?.some((item) => item.review_id === reviewId)).sort((a, b) => String(b[1].rule_updated_at ?? b[1].created_at).localeCompare(String(a[1].rule_updated_at ?? a[1].created_at))).slice(0, 20).map(([id, value]) => ({
6955
+ id,
6956
+ status: value.rule_status,
6957
+ category: value.category,
6958
+ rule: value.rule,
6959
+ context: value.context,
6960
+ mappings: value.mappings,
6961
+ occurrences: value.occurrences,
6962
+ evidence_count: value.evidence?.length ?? 0,
6963
+ reason: value.promotion_reason
6964
+ })) } : {}
6965
+ };
6966
+ } finally {
6967
+ rmSync(directory, { recursive: true, force: true });
6968
+ }
6969
+ }
6970
+ throw new Error("gist_write_conflict");
6971
+ };
6972
+
6973
+ // ../../scripts/session-history.mjs
6974
+ import { spawnSync as spawnSync2 } from "node:child_process";
6975
+ import { createHash as createHash3 } from "node:crypto";
6976
+ import { createReadStream, existsSync as existsSync3 } from "node:fs";
6977
+ import { opendir } from "node:fs/promises";
6978
+ import { homedir as homedir3 } from "node:os";
6979
+ import path3 from "node:path";
6980
+ import { createInterface } from "node:readline";
6981
+ var LANGUAGE_SIGNAL = /(영어|한글|원문|표현|단어|문구|말투|문장|번역|한국어|용어|윤문)/u;
6982
+ var CORRECTION_SIGNAL = /(해줘|써줘|바꿔|변경|고쳐|이상|어색|유지|그대로|하지\s*마|말고|통일|대신|풀어서)/u;
6983
+ var NON_LANGUAGE_SIGNAL = /(코드|구현|함수|클래스|파일|테스트|빌드|배포|서버|기능|버그|브랜치|커밋|프로젝트|api|ui|pr)/iu;
6984
+ var ARROW_REPLACEMENT = /.{1,80}\s*(?:->|=>|→)\s*.{1,80}/u;
6985
+ var EXPLICIT_REPLACEMENT = /(?:(?:대신에?|말고)\s*.{1,80}(?:써|사용|표현|바꿔|변경|고쳐))|(?:.{1,80}(?:로|으로)\s*(?:바꿔|변경|고쳐|써))/u;
6986
+ var SEARCH_SIGNAL = "(\uC601\uC5B4|\uD55C\uAE00|\uC6D0\uBB38|\uD45C\uD604|\uB2E8\uC5B4|\uBB38\uAD6C|\uB9D0\uD22C|\uBB38\uC7A5|\uBC88\uC5ED|\uD55C\uAD6D\uC5B4|\uC6A9\uC5B4|\uC724\uBB38|->|=>|\u2192|(\uB300\uC2E0\uC5D0?|\uB9D0\uACE0).{1,80}(\uC368|\uC0AC\uC6A9|\uD45C\uD604|\uBC14\uAFD4|\uBCC0\uACBD|\uACE0\uCCD0)|(\uB85C|\uC73C\uB85C)\\s*(\uBC14\uAFD4|\uBCC0\uACBD|\uACE0\uCCD0|\uC368))";
6987
+ var SEARCH_SIGNAL_JS = new RegExp(SEARCH_SIGNAL, "iu");
6988
+ var historyRecordPrefix = (source) => source === "codex" ? '^\\{(?=.{0,2048}$)(?=.*"type"\\s*:\\s*"response_item")(?=.*"role"\\s*:\\s*"user").*' : '^\\{(?=.{0,2048}$)(?=.*"type"\\s*:\\s*"user").*';
6989
+ var stringContent = (content) => {
6990
+ if (typeof content === "string") return content;
6991
+ if (!Array.isArray(content)) return "";
6992
+ return content.map((item) => typeof item === "string" ? item : item?.text ?? "").join("\n");
6993
+ };
6994
+ var sessionUserText = (record) => {
6995
+ if (record?.type === "response_item" && record?.payload?.role === "user") {
6996
+ return stringContent(record.payload.content);
6997
+ }
6998
+ if (record?.type === "user") return stringContent(record.content ?? record.message?.content);
6999
+ return "";
7000
+ };
7001
+ var isCorrectionCandidate = (text) => {
7002
+ const normalized2 = String(text ?? "").normalize("NFC").trim();
7003
+ return normalized2.length > 0 && normalized2.length <= 500 && !normalized2.startsWith("<") && (LANGUAGE_SIGNAL.test(normalized2) && CORRECTION_SIGNAL.test(normalized2) || ARROW_REPLACEMENT.test(normalized2) || !NON_LANGUAGE_SIGNAL.test(normalized2) && EXPLICIT_REPLACEMENT.test(normalized2));
7004
+ };
7005
+ var addRecord = (records, group, file, record) => {
7006
+ const text = sessionUserText(record).normalize("NFC").trim();
7007
+ if (!isCorrectionCandidate(text)) return;
7008
+ const sessionId = path3.basename(file, ".jsonl");
7009
+ const fingerprint = createHash3("sha256").update(text).digest("hex").slice(0, 16);
7010
+ const sessionKey = `${group.source}:${sessionId}`;
7011
+ const evidence = { source: group.source, session_id: sessionId, timestamp: record.timestamp ?? null };
7012
+ const existing = records.get(fingerprint);
7013
+ if (existing) {
7014
+ if (existing._session_keys.has(sessionKey)) return;
7015
+ existing._session_keys.add(sessionKey);
7016
+ existing.occurrences += 1;
7017
+ if (existing.evidence.length < 20) existing.evidence.push(evidence);
7018
+ if (String(record.timestamp ?? "") > String(existing.timestamp ?? "")) {
7019
+ existing.source = group.source;
7020
+ existing.session_id = sessionId;
7021
+ existing.timestamp = record.timestamp ?? null;
7022
+ }
7023
+ return;
7024
+ }
7025
+ records.set(fingerprint, {
7026
+ fingerprint,
7027
+ source: group.source,
7028
+ session_id: sessionId,
7029
+ timestamp: record.timestamp ?? null,
7030
+ text,
7031
+ occurrences: 1,
7032
+ evidence: [evidence],
7033
+ _session_keys: /* @__PURE__ */ new Set([sessionKey])
7034
+ });
7035
+ };
7036
+ async function* jsonlFiles(root) {
7037
+ let directory;
7038
+ try {
7039
+ directory = await opendir(root);
7040
+ } catch {
7041
+ return;
7042
+ }
7043
+ for await (const entry of directory) {
7044
+ const file = path3.join(root, entry.name);
7045
+ if (entry.isDirectory()) yield* jsonlFiles(file);
7046
+ else if (entry.isFile() && entry.name.endsWith(".jsonl")) yield file;
7047
+ }
7048
+ }
7049
+ var scanWithoutRipgrep = async (records, group, roots) => {
7050
+ for (const root of roots) {
7051
+ for await (const file of jsonlFiles(root)) {
7052
+ const lines = createInterface({ input: createReadStream(file), crlfDelay: Infinity });
7053
+ try {
7054
+ for await (const raw of lines) {
7055
+ if (raw.length > 2048 || !SEARCH_SIGNAL_JS.test(raw)) continue;
7056
+ let record;
7057
+ try {
7058
+ record = JSON.parse(raw);
7059
+ } catch {
7060
+ continue;
7061
+ }
7062
+ addRecord(records, group, file, record);
7063
+ }
7064
+ } catch {
7065
+ }
7066
+ }
7067
+ }
7068
+ };
7069
+ var scanSessionHistory = async (requested = "all") => {
7070
+ if (!["all", "claude", "codex"].includes(requested)) throw new Error("--history must be all, claude, or codex");
7071
+ const claudeHome = process.env.CLAUDE_CONFIG_DIR || path3.join(homedir3(), ".claude");
7072
+ const codexHome = process.env.CODEX_HOME || path3.join(homedir3(), ".codex");
7073
+ const sourceRoots = [];
7074
+ if (requested !== "codex") {
7075
+ sourceRoots.push({ source: "claude", roots: [path3.join(claudeHome, "transcripts"), path3.join(claudeHome, "projects")] });
7076
+ }
7077
+ if (requested !== "claude") sourceRoots.push({ source: "codex", roots: [path3.join(codexHome, "sessions")] });
7078
+ const available = sourceRoots.flatMap((group) => group.roots).filter(existsSync3);
7079
+ if (!available.length) return { scanned: [], count: 0, candidates: [] };
7080
+ const unique = /* @__PURE__ */ new Map();
7081
+ for (const group of sourceRoots) {
7082
+ const roots = group.roots.filter(existsSync3);
7083
+ if (!roots.length) continue;
7084
+ const prefix = historyRecordPrefix(group.source);
7085
+ const result = spawnSync2("rg", [
7086
+ "--json",
7087
+ "--pcre2",
7088
+ "-g",
7089
+ "*.jsonl",
7090
+ `${prefix}${SEARCH_SIGNAL}`,
7091
+ ...roots
7092
+ ], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
7093
+ if (result.error?.code === "ENOENT") {
7094
+ await scanWithoutRipgrep(unique, group, roots);
7095
+ continue;
7096
+ }
7097
+ if (result.error) throw new Error(`history_scan_unavailable: ${result.error.message}`);
7098
+ if (result.status !== 0 && result.status !== 1) throw new Error(`history_scan_failed: ${result.stderr.trim()}`);
7099
+ for (const line of result.stdout.split("\n")) {
7100
+ if (!line) continue;
7101
+ let event;
7102
+ try {
7103
+ event = JSON.parse(line);
7104
+ } catch {
7105
+ continue;
7106
+ }
7107
+ if (event.type !== "match") continue;
7108
+ const file = event.data?.path?.text;
7109
+ const raw = event.data?.lines?.text;
7110
+ if (!file || !raw) continue;
7111
+ let record;
7112
+ try {
7113
+ record = JSON.parse(raw);
7114
+ } catch {
7115
+ continue;
7116
+ }
7117
+ addRecord(unique, group, file, record);
7118
+ }
7119
+ }
7120
+ const candidates = [...unique.values()].sort((a, b) => String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? ""))).slice(0, 200).map(({ _session_keys, ...candidate }) => candidate);
7121
+ return { scanned: available, count: candidates.length, truncated: unique.size > candidates.length, candidates };
7122
+ };
7123
+
6070
7124
  // ../../scripts/review-store.mjs
6071
7125
  import { randomUUID } from "node:crypto";
6072
7126
  import {
6073
- chmodSync,
6074
- existsSync as existsSync2,
6075
- mkdirSync,
7127
+ chmodSync as chmodSync2,
7128
+ existsSync as existsSync4,
7129
+ mkdirSync as mkdirSync2,
6076
7130
  readdirSync,
6077
- readFileSync as readFileSync2,
7131
+ readFileSync as readFileSync3,
6078
7132
  renameSync,
6079
- rmSync,
7133
+ rmSync as rmSync2,
6080
7134
  statSync,
6081
- writeFileSync
7135
+ writeFileSync as writeFileSync2
6082
7136
  } from "node:fs";
6083
- import { hostname, homedir as homedir2 } from "node:os";
6084
- import path2 from "node:path";
7137
+ import { hostname as hostname2, homedir as homedir4 } from "node:os";
7138
+ import path4 from "node:path";
6085
7139
 
6086
7140
  // ../feedback-core/dist/index.js
6087
7141
  var createSelectionValidator = (text) => {
@@ -6111,29 +7165,29 @@ var StoreProblem = class extends Error {
6111
7165
  }
6112
7166
  };
6113
7167
  var problem = (status, code, message, nextAction) => new StoreProblem(status, code, message, nextAction);
6114
- var storeHome = () => path2.resolve(process.env.GYOJEONG_HOME || path2.join(homedir2(), ".local", "share", "gyojeong"));
6115
- var runsDir = (home) => path2.join(home, "runs");
7168
+ var storeHome = () => path4.resolve(process.env.GYOJEONG_HOME || path4.join(homedir4(), ".local", "share", "gyojeong"));
7169
+ var runsDir = (home) => path4.join(home, "runs");
6116
7170
  var runDir = (home, id) => {
6117
7171
  if (typeof id !== "string" || !REVIEW_ID.test(id)) {
6118
7172
  throw problem(400, "invalid_request", "\uAC80\uC218 ID \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.", "\uD604\uC7AC \uAC80\uC218 \uB9C1\uD06C\uB97C \uB2E4\uC2DC \uC5F4\uC5B4 \uC8FC\uC138\uC694.");
6119
7173
  }
6120
- return path2.join(runsDir(home), id);
7174
+ return path4.join(runsDir(home), id);
6121
7175
  };
6122
7176
  var ensurePrivateDir = (dir) => {
6123
- mkdirSync(dir, { recursive: true, mode: 448 });
6124
- chmodSync(dir, 448);
7177
+ mkdirSync2(dir, { recursive: true, mode: 448 });
7178
+ chmodSync2(dir, 448);
6125
7179
  };
6126
7180
  var writeAtomic = (file, text) => {
6127
7181
  const temp = `${file}.${process.pid}.${randomUUID()}.tmp`;
6128
- writeFileSync(temp, text, { mode: 384 });
7182
+ writeFileSync2(temp, text, { mode: 384 });
6129
7183
  renameSync(temp, file);
6130
7184
  };
6131
7185
  var readJson = (file, fallback) => {
6132
- if (!existsSync2(file)) return fallback;
7186
+ if (!existsSync4(file)) return fallback;
6133
7187
  try {
6134
- return JSON.parse(readFileSync2(file, "utf8"));
7188
+ return JSON.parse(readFileSync3(file, "utf8"));
6135
7189
  } catch {
6136
- throw problem(409, "storage_integrity", `${path2.basename(file)} \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`, "\uAC80\uC218 \uAE30\uB85D \uD30C\uC77C\uC744 \uD655\uC778\uD574 \uC8FC\uC138\uC694.");
7190
+ throw problem(409, "storage_integrity", `${path4.basename(file)} \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`, "\uAC80\uC218 \uAE30\uB85D \uD30C\uC77C\uC744 \uD655\uC778\uD574 \uC8FC\uC138\uC694.");
6137
7191
  }
6138
7192
  };
6139
7193
  var sleepSync = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
@@ -6147,19 +7201,19 @@ var processAlive = (pid) => {
6147
7201
  };
6148
7202
  var acquireLock = (lock) => {
6149
7203
  const token = randomUUID();
6150
- const owner = { pid: process.pid, host: hostname(), token, at: (/* @__PURE__ */ new Date()).toISOString() };
7204
+ const owner = { pid: process.pid, host: hostname2(), token, at: (/* @__PURE__ */ new Date()).toISOString() };
6151
7205
  const deadline = Date.now() + LOCK_WAIT_MS;
6152
7206
  const readOwner = () => {
6153
7207
  try {
6154
- return JSON.parse(readFileSync2(path2.join(lock, "owner.json"), "utf8"));
7208
+ return JSON.parse(readFileSync3(path4.join(lock, "owner.json"), "utf8"));
6155
7209
  } catch {
6156
7210
  return null;
6157
7211
  }
6158
7212
  };
6159
7213
  for (; ; ) {
6160
7214
  try {
6161
- mkdirSync(lock);
6162
- writeFileSync(path2.join(lock, "owner.json"), JSON.stringify(owner), { mode: 384 });
7215
+ mkdirSync2(lock);
7216
+ writeFileSync2(path4.join(lock, "owner.json"), JSON.stringify(owner), { mode: 384 });
6163
7217
  break;
6164
7218
  } catch (error2) {
6165
7219
  if (error2?.code === "ENOENT") {
@@ -6173,12 +7227,12 @@ var acquireLock = (lock) => {
6173
7227
  } catch {
6174
7228
  continue;
6175
7229
  }
6176
- const dead = held?.host === hostname() && Number.isInteger(held.pid) && !processAlive(held.pid);
7230
+ const dead = held?.host === hostname2() && Number.isInteger(held.pid) && !processAlive(held.pid);
6177
7231
  if (dead || age > LOCK_STALE_MS) {
6178
7232
  try {
6179
7233
  const broken = `${lock}.${token}.broken`;
6180
7234
  renameSync(lock, broken);
6181
- rmSync(broken, { recursive: true, force: true });
7235
+ rmSync2(broken, { recursive: true, force: true });
6182
7236
  continue;
6183
7237
  } catch {
6184
7238
  }
@@ -6190,7 +7244,7 @@ var acquireLock = (lock) => {
6190
7244
  }
6191
7245
  }
6192
7246
  return () => {
6193
- if (readOwner()?.token === token) rmSync(lock, { recursive: true, force: true });
7247
+ if (readOwner()?.token === token) rmSync2(lock, { recursive: true, force: true });
6194
7248
  };
6195
7249
  };
6196
7250
  var withLock = (lock, task) => {
@@ -6201,9 +7255,9 @@ var withLock = (lock, task) => {
6201
7255
  release();
6202
7256
  }
6203
7257
  };
6204
- var withRunLock = (dir, task) => withLock(path2.join(dir, ".lock"), task);
7258
+ var withRunLock = (dir, task) => withLock(path4.join(dir, ".lock"), task);
6205
7259
  var assertRunExists = (dir) => {
6206
- if (!existsSync2(path2.join(dir, "content.json"))) {
7260
+ if (!existsSync4(path4.join(dir, "content.json"))) {
6207
7261
  throw problem(404, "wrong_run", "\uAC80\uC218 \uAE30\uB85D\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.", "\uAC80\uC218\uAC00 \uC774\uBBF8 \uC885\uB8CC\uB410\uB294\uC9C0 \uD655\uC778\uD574 \uC8FC\uC138\uC694.");
6208
7262
  }
6209
7263
  };
@@ -6211,7 +7265,7 @@ var createRun = (home, input, { mode, check: check2 } = {}) => {
6211
7265
  const id = input.run_id;
6212
7266
  const dir = runDir(home, id);
6213
7267
  ensurePrivateDir(runsDir(home));
6214
- if (existsSync2(dir)) {
7268
+ if (existsSync4(dir)) {
6215
7269
  throw problem(409, "duplicate_run", "\uAC19\uC740 ID\uC758 \uAC80\uC218\uAC00 \uC774\uBBF8 \uC788\uC2B5\uB2C8\uB2E4.", "\uC0C8 ID\uB85C \uB2E4\uC2DC \uAC8C\uC2DC\uD574 \uC8FC\uC138\uC694.");
6216
7270
  }
6217
7271
  const job = input.jobs[0];
@@ -6243,15 +7297,15 @@ var createRun = (home, input, { mode, check: check2 } = {}) => {
6243
7297
  ensurePrivateDir(staging);
6244
7298
  try {
6245
7299
  (input.attachments ?? []).forEach((attachment, index) => {
6246
- writeFileSync(path2.join(staging, `attachment-${index}.b64`), attachment.content_base64, { mode: 384 });
7300
+ writeFileSync2(path4.join(staging, `attachment-${index}.b64`), attachment.content_base64, { mode: 384 });
6247
7301
  });
6248
- writeFileSync(path2.join(staging, "content.json"), serializeReviewContent(content), { mode: 384 });
6249
- writeFileSync(path2.join(staging, "feedback.json"), "[]", { mode: 384 });
6250
- writeFileSync(path2.join(staging, "state.json"), JSON.stringify({ processed_feedback_ids: [] }), { mode: 384 });
6251
- writeFileSync(path2.join(staging, "meta.json"), JSON.stringify({ created_at: createdAt }), { mode: 384 });
7302
+ writeFileSync2(path4.join(staging, "content.json"), serializeReviewContent(content), { mode: 384 });
7303
+ writeFileSync2(path4.join(staging, "feedback.json"), "[]", { mode: 384 });
7304
+ writeFileSync2(path4.join(staging, "state.json"), JSON.stringify({ processed_feedback_ids: [] }), { mode: 384 });
7305
+ writeFileSync2(path4.join(staging, "meta.json"), JSON.stringify({ created_at: createdAt }), { mode: 384 });
6252
7306
  renameSync(staging, dir);
6253
7307
  } catch (error2) {
6254
- rmSync(staging, { recursive: true, force: true });
7308
+ rmSync2(staging, { recursive: true, force: true });
6255
7309
  throw error2;
6256
7310
  }
6257
7311
  return { id, job_id: content.job.id, created_at: createdAt };
@@ -6259,14 +7313,14 @@ var createRun = (home, input, { mode, check: check2 } = {}) => {
6259
7313
  var readContent = (home, id) => {
6260
7314
  const dir = runDir(home, id);
6261
7315
  assertRunExists(dir);
6262
- return readJson(path2.join(dir, "content.json"), null);
7316
+ return readJson(path4.join(dir, "content.json"), null);
6263
7317
  };
6264
7318
  var readRun = (home, id) => {
6265
7319
  const dir = runDir(home, id);
6266
7320
  assertRunExists(dir);
6267
- const content = readJson(path2.join(dir, "content.json"), null);
6268
- const feedback2 = readJson(path2.join(dir, "feedback.json"), []);
6269
- const state = readJson(path2.join(dir, "state.json"), { processed_feedback_ids: [] });
7321
+ const content = readJson(path4.join(dir, "content.json"), null);
7322
+ const feedback2 = readJson(path4.join(dir, "feedback.json"), []);
7323
+ const state = readJson(path4.join(dir, "state.json"), { processed_feedback_ids: [] });
6270
7324
  const processed2 = new Set(state.processed_feedback_ids ?? []);
6271
7325
  return {
6272
7326
  content,
@@ -6318,30 +7372,30 @@ var pendingFeedbackByDraft = (home, id, run) => {
6318
7372
  };
6319
7373
  var readDraftVersion = (home, id, version2, content = readContent(home, id)) => {
6320
7374
  if (version2 === (content.draft_version ?? 0)) return content.draft_text;
6321
- const file = path2.join(runDir(home, id), `draft-${version2}.txt`);
6322
- if (!Number.isInteger(version2) || version2 < 0 || !existsSync2(file)) {
7375
+ const file = path4.join(runDir(home, id), `draft-${version2}.txt`);
7376
+ if (!Number.isInteger(version2) || version2 < 0 || !existsSync4(file)) {
6323
7377
  throw problem(409, "storage_integrity", "\uD53C\uB4DC\uBC31\uC744 \uB0A8\uAE34 \uCD08\uC548\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.", "\uAC80\uC218 \uAE30\uB85D\uC744 \uD655\uC778\uD574 \uC8FC\uC138\uC694.");
6324
7378
  }
6325
- return readFileSync2(file, "utf8");
7379
+ return readFileSync3(file, "utf8");
6326
7380
  };
6327
7381
  var markProcessed = (home, id, feedbackIds) => {
6328
7382
  const dir = runDir(home, id);
6329
7383
  assertRunExists(dir);
6330
7384
  return withRunLock(dir, () => {
6331
- const feedback2 = readJson(path2.join(dir, "feedback.json"), []);
7385
+ const feedback2 = readJson(path4.join(dir, "feedback.json"), []);
6332
7386
  const known = new Set(feedback2.map((item) => item.id));
6333
7387
  const unknown2 = feedbackIds.filter((fid) => !known.has(fid));
6334
7388
  if (unknown2.length) {
6335
7389
  throw problem(400, "unknown_feedback", `\uC774 \uAC80\uC218\uC5D0 \uC5C6\uB294 \uD53C\uB4DC\uBC31 ID\uC785\uB2C8\uB2E4: ${unknown2.join(", ")}`, "\uD53C\uB4DC\uBC31 \uBAA9\uB85D\uC744 \uB2E4\uC2DC \uC77D\uC5B4 \uC8FC\uC138\uC694.");
6336
7390
  }
6337
- const state = readJson(path2.join(dir, "state.json"), { processed_feedback_ids: [] });
7391
+ const state = readJson(path4.join(dir, "state.json"), { processed_feedback_ids: [] });
6338
7392
  const processed2 = [.../* @__PURE__ */ new Set([...state.processed_feedback_ids ?? [], ...feedbackIds])];
6339
- writeAtomic(path2.join(dir, "state.json"), JSON.stringify({ ...state, processed_feedback_ids: processed2 }));
7393
+ writeAtomic(path4.join(dir, "state.json"), JSON.stringify({ ...state, processed_feedback_ids: processed2 }));
6340
7394
  const done = new Set(processed2);
6341
7395
  const needed = new Set(feedback2.filter((item) => !done.has(item.id)).map((item) => item.draft_version));
6342
7396
  for (const file of readdirSync(dir)) {
6343
7397
  const match = /^draft-(\d+)\.txt$/u.exec(file);
6344
- if (match && !needed.has(Number(match[1]))) rmSync(path2.join(dir, file), { force: true });
7398
+ if (match && !needed.has(Number(match[1]))) rmSync2(path4.join(dir, file), { force: true });
6345
7399
  }
6346
7400
  return { processed: processed2.length, pending: feedback2.filter((item) => !done.has(item.id)).length };
6347
7401
  });
@@ -6350,7 +7404,7 @@ var reviseDraft = (home, id, draftText, { check: check2 } = {}) => {
6350
7404
  const dir = runDir(home, id);
6351
7405
  assertRunExists(dir);
6352
7406
  return withRunLock(dir, () => {
6353
- const content = readJson(path2.join(dir, "content.json"), null);
7407
+ const content = readJson(path4.join(dir, "content.json"), null);
6354
7408
  if (content.draft_text === draftText) {
6355
7409
  throw problem(409, "unchanged_draft", "\uC218\uC815\uBCF8\uC774 \uAE30\uC874 \uCD08\uC548\uACFC \uAC19\uC2B5\uB2C8\uB2E4.", "\uC218\uC815\uD55C \uD30C\uC77C\uC744 \uC9C0\uC815\uD588\uB294\uC9C0 \uD655\uC778\uD574 \uC8FC\uC138\uC694.");
6356
7410
  }
@@ -6367,18 +7421,18 @@ var reviseDraft = (home, id, draftText, { check: check2 } = {}) => {
6367
7421
  revised_at: (/* @__PURE__ */ new Date()).toISOString()
6368
7422
  };
6369
7423
  const serialized = serializeReviewContent(next);
6370
- const feedback2 = readJson(path2.join(dir, "feedback.json"), []);
7424
+ const feedback2 = readJson(path4.join(dir, "feedback.json"), []);
6371
7425
  const unversioned = feedback2.filter((item) => item.draft_version === void 0);
6372
7426
  if (unversioned.length) {
6373
7427
  const fitsCurrent = selectionChecker(content.draft_text);
6374
7428
  for (const item of unversioned) item.draft_version = feedbackDraftVersion(item, content, fitsCurrent);
6375
- writeAtomic(path2.join(dir, "feedback.json"), JSON.stringify(feedback2, null, 2));
7429
+ writeAtomic(path4.join(dir, "feedback.json"), JSON.stringify(feedback2, null, 2));
6376
7430
  }
6377
- const processed2 = new Set(readJson(path2.join(dir, "state.json"), { processed_feedback_ids: [] }).processed_feedback_ids ?? []);
7431
+ const processed2 = new Set(readJson(path4.join(dir, "state.json"), { processed_feedback_ids: [] }).processed_feedback_ids ?? []);
6378
7432
  if (feedback2.some((item) => !processed2.has(item.id) && item.draft_version === version2)) {
6379
- writeAtomic(path2.join(dir, `draft-${version2}.txt`), content.draft_text);
7433
+ writeAtomic(path4.join(dir, `draft-${version2}.txt`), content.draft_text);
6380
7434
  }
6381
- writeAtomic(path2.join(dir, "content.json"), serialized);
7435
+ writeAtomic(path4.join(dir, "content.json"), serialized);
6382
7436
  return { id, revised_at: next.revised_at };
6383
7437
  });
6384
7438
  };
@@ -6386,32 +7440,32 @@ var finishRun = (home, id, force = false) => {
6386
7440
  const dir = runDir(home, id);
6387
7441
  assertRunExists(dir);
6388
7442
  return withRunLock(dir, () => {
6389
- const feedback2 = readJson(path2.join(dir, "feedback.json"), []);
6390
- const state = readJson(path2.join(dir, "state.json"), { processed_feedback_ids: [] });
7443
+ const feedback2 = readJson(path4.join(dir, "feedback.json"), []);
7444
+ const state = readJson(path4.join(dir, "state.json"), { processed_feedback_ids: [] });
6391
7445
  const processed2 = new Set(state.processed_feedback_ids ?? []);
6392
7446
  const pending = feedback2.filter((item) => !processed2.has(item.id)).length;
6393
7447
  if (pending > 0 && force !== true) return { finished: false, pending };
6394
7448
  const trash = `${dir}.${process.pid}.deleting`;
6395
7449
  renameSync(dir, trash);
6396
- rmSync(trash, { recursive: true, force: true });
7450
+ rmSync2(trash, { recursive: true, force: true });
6397
7451
  return { finished: true, pending };
6398
7452
  });
6399
7453
  };
6400
7454
  var discardRun = (home, id) => finishRun(home, id, true);
6401
7455
  var listRuns = (home, now = Date.now()) => {
6402
- if (!existsSync2(runsDir(home))) return [];
7456
+ if (!existsSync4(runsDir(home))) return [];
6403
7457
  return readdirSync(runsDir(home)).filter((name) => REVIEW_ID.test(name)).flatMap((id) => {
6404
7458
  try {
6405
7459
  const dir = runDir(home, id);
6406
7460
  let meta2 = null;
6407
7461
  try {
6408
- meta2 = readJson(path2.join(dir, "meta.json"), null);
7462
+ meta2 = readJson(path4.join(dir, "meta.json"), null);
6409
7463
  } catch {
6410
7464
  meta2 = null;
6411
7465
  }
6412
- const createdAt = (meta2?.created_at ? meta2 : readJson(path2.join(dir, "content.json"), null)).created_at;
6413
- const feedback2 = readJson(path2.join(dir, "feedback.json"), []);
6414
- const processed2 = new Set(readJson(path2.join(dir, "state.json"), { processed_feedback_ids: [] }).processed_feedback_ids ?? []);
7466
+ const createdAt = (meta2?.created_at ? meta2 : readJson(path4.join(dir, "content.json"), null)).created_at;
7467
+ const feedback2 = readJson(path4.join(dir, "feedback.json"), []);
7468
+ const processed2 = new Set(readJson(path4.join(dir, "state.json"), { processed_feedback_ids: [] }).processed_feedback_ids ?? []);
6415
7469
  return [
6416
7470
  {
6417
7471
  review_id: id,
@@ -6428,8 +7482,8 @@ var listRuns = (home, now = Date.now()) => {
6428
7482
  };
6429
7483
 
6430
7484
  // ../../scripts/review-cli.mjs
6431
- var SCRIPT_DIR = path3.dirname(fileURLToPath(import.meta.url));
6432
- var SERVER_SCRIPT = path3.join(SCRIPT_DIR, "review-server.mjs");
7485
+ var SCRIPT_DIR = path5.dirname(fileURLToPath(import.meta.url));
7486
+ var SERVER_SCRIPT = path5.join(SCRIPT_DIR, "review-server.mjs");
6433
7487
  var MAX_TEXT_BYTES = 2 * 1024 * 1024;
6434
7488
  var STALE_DAYS = 14;
6435
7489
  var MEDIA_TYPES = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp" };
@@ -6452,15 +7506,15 @@ var readRaw = (file, maxBytes = MAX_TEXT_BYTES) => {
6452
7506
  const stat = statSync2(file, { throwIfNoEntry: false });
6453
7507
  if (!stat?.isFile()) throw new UsageError(`not a file: ${file}`);
6454
7508
  if (stat.size > maxBytes) throw new UsageError(`file larger than ${maxBytes / 1024 / 1024} MiB: ${file}`);
6455
- return readFileSync3(file, "utf8");
7509
+ return readFileSync4(file, "utf8");
6456
7510
  };
6457
7511
  var readText = (file) => readRaw(file).normalize("NFC");
6458
7512
  var requireOptions = (options, ...keys) => {
6459
7513
  for (const key of keys) if (!options[key]) throw new UsageError(`--${key} is required`);
6460
7514
  };
6461
7515
  var loadStandards = () => {
6462
- const file = path3.join(".gyojeong", "standards");
6463
- return existsSync3(file) ? parseStandards(readFileSync3(file, "utf8")) : { terms: [] };
7516
+ const file = path5.join(".gyojeong", "standards");
7517
+ return existsSync5(file) ? parseStandards(readFileSync4(file, "utf8")) : { terms: [] };
6464
7518
  };
6465
7519
  var processAlive2 = (pid) => {
6466
7520
  try {
@@ -6485,7 +7539,7 @@ var health = async (port, home) => {
6485
7539
  var runningServer = async (home) => {
6486
7540
  let info;
6487
7541
  try {
6488
- info = JSON.parse(readFileSync3(serverInfoPath(home), "utf8"));
7542
+ info = JSON.parse(readFileSync4(serverInfoPath(home), "utf8"));
6489
7543
  } catch {
6490
7544
  return null;
6491
7545
  }
@@ -6493,7 +7547,7 @@ var runningServer = async (home) => {
6493
7547
  return await health(info.port, home) ? info : null;
6494
7548
  };
6495
7549
  var spawnServer = async (home) => {
6496
- const log = openSync(path3.join(home, "server.log"), "a", 384);
7550
+ const log = openSync(path5.join(home, "server.log"), "a", 384);
6497
7551
  const child = spawn(process.execPath, [SERVER_SCRIPT], {
6498
7552
  detached: true,
6499
7553
  stdio: ["ignore", log, log],
@@ -6507,13 +7561,13 @@ var spawnServer = async (home) => {
6507
7561
  if (info) return { port: info.port, started: true };
6508
7562
  await new Promise((resolve) => setTimeout(resolve, 100));
6509
7563
  }
6510
- throw new Error(`review server did not start; see ${path3.join(home, "server.log")}`);
7564
+ throw new Error(`review server did not start; see ${path5.join(home, "server.log")}`);
6511
7565
  };
6512
7566
  var ensureServer = async (home) => {
6513
7567
  const existing = await runningServer(home);
6514
7568
  if (existing) return { port: existing.port, started: false };
6515
- mkdirSync2(home, { recursive: true, mode: 448 });
6516
- const release = acquireLock(path3.join(home, "server.lock"));
7569
+ mkdirSync3(home, { recursive: true, mode: 448 });
7570
+ const release = acquireLock(path5.join(home, "server.lock"));
6517
7571
  try {
6518
7572
  const started = await runningServer(home);
6519
7573
  return started ? { port: started.port, started: false } : await spawnServer(home);
@@ -6532,9 +7586,9 @@ var reviewUrls = (port, id, content, { publicOrigin } = loadIdleConfig()) => {
6532
7586
  };
6533
7587
  };
6534
7588
  var attachmentInput = (file) => {
6535
- const mediaType = MEDIA_TYPES[path3.extname(file).toLowerCase()];
7589
+ const mediaType = MEDIA_TYPES[path5.extname(file).toLowerCase()];
6536
7590
  if (!mediaType) throw new UsageError(`unsupported attachment type: ${file} (png, jpeg, webp)`);
6537
- return { filename: path3.basename(file), media_type: mediaType, content_base64: readFileSync3(file).toString("base64") };
7591
+ return { filename: path5.basename(file), media_type: mediaType, content_base64: readFileSync4(file).toString("base64") };
6538
7592
  };
6539
7593
  var publish = async (home, options) => {
6540
7594
  requireOptions(options, "source", "draft");
@@ -6678,7 +7732,16 @@ var verifyFeedback = (home, options) => {
6678
7732
  var processed = (home, options) => {
6679
7733
  requireOptions(options, "review", "ids");
6680
7734
  const ids = options.ids.split(",").map((id) => id.trim()).filter(Boolean);
6681
- return { review_id: options.review, ...markProcessed(home, options.review, ids) };
7735
+ const run = readRun(home, options.review);
7736
+ const selected = run.feedback.filter((item) => ids.includes(item.id));
7737
+ const unknown2 = ids.filter((id) => !selected.some((item) => item.id === id));
7738
+ if (unknown2.length) throw new UsageError(`unknown feedback ids: ${unknown2.join(",")}`);
7739
+ const durable = ruleCurationPreview(options.review, selected);
7740
+ return {
7741
+ review_id: options.review,
7742
+ ...markProcessed(home, options.review, ids),
7743
+ rules: { pending_curation: durable.length }
7744
+ };
6682
7745
  };
6683
7746
  var url2 = async (home, options) => {
6684
7747
  requireOptions(options, "review");
@@ -6695,7 +7758,55 @@ var discard = (home, options) => {
6695
7758
  discardRun(home, options.review);
6696
7759
  return { review_id: options.review, discarded: true };
6697
7760
  };
6698
- var COMMANDS = { publish, revise, feedback, "verify-feedback": verifyFeedback, processed, url: url2, list, discard };
7761
+ var rules = async (home, options) => {
7762
+ if (options.compact === "true" && (options.attach.length > 0 || Object.keys(options).some((key) => key !== "compact" && key !== "attach"))) {
7763
+ throw new UsageError("compact rules cannot be combined with other options");
7764
+ }
7765
+ if (options.compact === "true") return readGistRules();
7766
+ if (options.history) {
7767
+ const history = await scanSessionHistory(options.history);
7768
+ if (!options.export) return history;
7769
+ const target = path5.resolve(options.export);
7770
+ if (existsSync5(target)) throw new UsageError(`history export already exists: ${target}`);
7771
+ const payload = {
7772
+ schema_version: 1,
7773
+ exported_at: (/* @__PURE__ */ new Date()).toISOString(),
7774
+ count: history.count,
7775
+ truncated: history.truncated,
7776
+ candidates: history.candidates
7777
+ };
7778
+ writeFileSync3(target, `${JSON.stringify(payload, null, 2)}
7779
+ `, { flag: "wx", mode: 384 });
7780
+ chmodSync3(target, 384);
7781
+ return { exported: target, count: history.count, truncated: history.truncated };
7782
+ }
7783
+ if (options["history-import"]) {
7784
+ const payload = JSON.parse(readRaw(options["history-import"]));
7785
+ if (payload?.schema_version !== 1 || !Array.isArray(payload?.candidates) || payload.candidates.length > 200 || payload.candidates.some((item) => typeof item?.text !== "string" || item.text.length > 500)) {
7786
+ throw new UsageError("invalid history export");
7787
+ }
7788
+ return { count: payload.candidates.length, truncated: payload.truncated === true, candidates: payload.candidates };
7789
+ }
7790
+ const run = options.review ? readRun(home, options.review) : null;
7791
+ const promoteIds = options.ids ? options.ids.split(",").map((id) => id.trim()).filter(Boolean) : [];
7792
+ const candidateIds = options["candidate-ids"] ? options["candidate-ids"].split(",").map((id) => id.trim()).filter(Boolean) : [];
7793
+ const sessionCorrections = options.session ? JSON.parse(readRaw(options.session)) : [];
7794
+ const curations = options.curate ? JSON.parse(readRaw(options.curate)) : [];
7795
+ const processedFeedback = run?.feedback.filter((item) => run.processed_feedback_ids.includes(item.id)) ?? [];
7796
+ if (run && !options.curate) return { review_id: options.review, candidates: ruleCurationPreview(options.review, processedFeedback) };
7797
+ const result = updateGistRules({
7798
+ ...run ? { reviewId: options.review, feedback: processedFeedback } : {},
7799
+ promoteIds,
7800
+ candidateIds,
7801
+ sessionCorrections: Array.isArray(sessionCorrections) ? sessionCorrections : [sessionCorrections],
7802
+ curations: Array.isArray(curations) ? curations : [curations],
7803
+ completeOnboarding: options["complete-onboarding"] === "true",
7804
+ includeCandidates: options.candidates === "true",
7805
+ distill: options.distill === "true"
7806
+ });
7807
+ return result;
7808
+ };
7809
+ var COMMANDS = { publish, revise, feedback, "verify-feedback": verifyFeedback, processed, url: url2, list, discard, rules };
6699
7810
  var main = async (argv = process.argv.slice(2)) => {
6700
7811
  const [command, ...rest] = argv;
6701
7812
  const run = COMMANDS[command];
@@ -6705,8 +7816,9 @@ var main = async (argv = process.argv.slice(2)) => {
6705
7816
  return 64;
6706
7817
  }
6707
7818
  try {
6708
- const result = await run(storeHome(), parseArgs(rest));
6709
- process.stdout.write(`${JSON.stringify(result, null, 2)}
7819
+ const options = parseArgs(rest);
7820
+ const result = await run(storeHome(), options);
7821
+ process.stdout.write(`${JSON.stringify(result, null, command === "rules" && options.compact === "true" ? 0 : 2)}
6710
7822
  `);
6711
7823
  return 0;
6712
7824
  } catch (error2) {
@@ -6721,7 +7833,7 @@ var main = async (argv = process.argv.slice(2)) => {
6721
7833
  return 1;
6722
7834
  }
6723
7835
  };
6724
- if (process.argv[1] && path3.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
7836
+ if (process.argv[1] && path5.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
6725
7837
  process.exitCode = await main();
6726
7838
  }
6727
7839
  export {