{"version":3,"file":"feedback.mjs","names":[],"sources":["../src/feedback.ts"],"sourcesContent":["/**\n * Feedback submission builder shared by both agent surfaces.\n *\n * anonymize runs fully local and makes no network calls, so feedback is never\n * sent from here. Instead this module sanitizes the agent-authored title/body\n * and returns a prefilled GitHub new-issue URL (plus an equivalent `gh` command)\n * that a human opens and submits under their own account. The human approval and\n * the absence of any network call are the real controls; the sanitizer is a\n * coarse safety net. This module stays runtime-free.\n */\n\nimport {\n  sanitizeFeedbackText,\n  type SanitizeFeedbackResult,\n} from \"./feedback-sanitize\";\n\nexport const FEEDBACK_KINDS = [\n  \"bug\",\n  \"feature_request\",\n  \"docs\",\n  \"other\",\n] as const;\nexport type FeedbackKind = (typeof FEEDBACK_KINDS)[number];\n\nexport const MAX_FEEDBACK_TITLE_CHARS = 200;\nexport const MAX_FEEDBACK_BODY_CHARS = 8000;\n\nconst GITHUB_REPO = \"stella/anonymize\";\nconst GITHUB_ISSUE_LABEL = \"agent-feedback\";\nconst GITHUB_NEW_ISSUE_URL = `https://github.com/${GITHUB_REPO}/issues/new`;\n// A conservative cap: browsers accept much longer, but keeping the prefilled URL\n// small avoids client-side truncation surprises. The full sanitized body is\n// always returned separately, so nothing is lost.\nconst MAX_GITHUB_ISSUE_URL_CHARS = 7500;\nconst GITHUB_BODY_TRUNCATION_MARKER =\n  \"\\n\\n[body truncated — paste the rest manually]\";\nconst HIGH_SURROGATE_START = 55_296;\nconst HIGH_SURROGATE_END = 56_319;\n\ntype FeedbackInput = {\n  kind: FeedbackKind;\n  title: string;\n  body: string;\n};\n\nexport type FeedbackSubmission = {\n  title: string;\n  /** Fully sanitized body with a provenance footer; nothing is lost to URL truncation. */\n  sanitizedBody: string;\n  /** Total redactions across title and body, for the human to gauge stripping. */\n  redactions: number;\n  /** Prefilled new-issue URL the human opens and submits. */\n  issueUrl: string;\n  /** Equivalent `gh` command, safe to paste verbatim. */\n  ghCommand: string;\n};\n\nconst composeFeedbackBody = (body: string, kind: FeedbackKind): string =>\n  `${body}\\n\\n---\\n_Filed via stella-anonymize feedback (agent-assisted, sanitized). Kind: ${kind}._`;\n\nconst buildGithubIssueUrl = (title: string, body: string): string => {\n  const params = new URLSearchParams({\n    title,\n    body,\n    labels: GITHUB_ISSUE_LABEL,\n  });\n  return `${GITHUB_NEW_ISSUE_URL}?${params.toString()}`;\n};\n\nconst sliceWithoutDanglingHighSurrogate = (\n  value: string,\n  end: number,\n): string => {\n  const sliced = value.slice(0, end);\n  const last = sliced.codePointAt(sliced.length - 1);\n  return last !== undefined &&\n    last >= HIGH_SURROGATE_START &&\n    last <= HIGH_SURROGATE_END\n    ? sliced.slice(0, -1)\n    : sliced;\n};\n\n/**\n * Prefilled issue URL bounded to `MAX_GITHUB_ISSUE_URL_CHARS`. When the full\n * body overflows, the URL carries a truncated body with a paste-the-rest marker;\n * the caller still returns the full sanitized body separately.\n */\nconst buildBoundedGithubIssueUrl = (\n  title: string,\n  composedBody: string,\n): string => {\n  const full = buildGithubIssueUrl(title, composedBody);\n  if (full.length <= MAX_GITHUB_ISSUE_URL_CHARS) {\n    return full;\n  }\n  for (let keep = composedBody.length; keep > 0; keep -= 128) {\n    const candidate = buildGithubIssueUrl(\n      title,\n      sliceWithoutDanglingHighSurrogate(composedBody, keep) +\n        GITHUB_BODY_TRUNCATION_MARKER,\n    );\n    if (candidate.length <= MAX_GITHUB_ISSUE_URL_CHARS) {\n      return candidate;\n    }\n  }\n  // Even an empty body overflows (an outsized title): fall back to marker-only.\n  return buildGithubIssueUrl(title, GITHUB_BODY_TRUNCATION_MARKER);\n};\n\n/** POSIX single-quote escaping so the gh command is safe to paste verbatim. */\nconst shellSingleQuote = (value: string): string =>\n  `'${value.replaceAll(\"'\", \"'\\\\''\")}'`;\n\nconst buildGhCommand = (title: string, body: string): string =>\n  [\n    \"gh issue create\",\n    `--repo ${GITHUB_REPO}`,\n    `--label ${GITHUB_ISSUE_LABEL}`,\n    `--title ${shellSingleQuote(title)}`,\n    `--body ${shellSingleQuote(body)}`,\n  ].join(\" \");\n\n/**\n * Sanitize the title and body, then build the prefilled GitHub submission. Pure:\n * no I/O and no network. The caller presents the result for a human to submit.\n */\nexport const buildFeedbackSubmission = ({\n  body,\n  kind,\n  title,\n}: FeedbackInput): FeedbackSubmission => {\n  const cleanTitle: SanitizeFeedbackResult = sanitizeFeedbackText(title);\n  const cleanBody = sanitizeFeedbackText(body);\n  const composedBody = composeFeedbackBody(cleanBody.text, kind);\n  return {\n    title: cleanTitle.text,\n    sanitizedBody: composedBody,\n    redactions: cleanTitle.redactions + cleanBody.redactions,\n    issueUrl: buildBoundedGithubIssueUrl(cleanTitle.text, composedBody),\n    ghCommand: buildGhCommand(cleanTitle.text, composedBody),\n  };\n};\n"],"mappings":";;;;;;;;;;;;AAgBA,MAAa,iBAAiB;CAC5B;CACA;CACA;CACA;AACF;AAGA,MAAa,2BAA2B;AACxC,MAAa,0BAA0B;AAEvC,MAAM,cAAc;AACpB,MAAM,qBAAqB;AAC3B,MAAM,uBAAuB,sBAAsB,YAAY;AAI/D,MAAM,6BAA6B;AACnC,MAAM,gCACJ;AACF,MAAM,uBAAuB;AAC7B,MAAM,qBAAqB;AAoB3B,MAAM,uBAAuB,MAAc,SACzC,GAAG,KAAK,mFAAmF,KAAK;AAElG,MAAM,uBAAuB,OAAe,SAAyB;CACnE,MAAM,SAAS,IAAI,gBAAgB;EACjC;EACA;EACA,QAAQ;CACV,CAAC;CACD,OAAO,GAAG,qBAAqB,GAAG,OAAO,SAAS;AACpD;AAEA,MAAM,qCACJ,OACA,QACW;CACX,MAAM,SAAS,MAAM,MAAM,GAAG,GAAG;CACjC,MAAM,OAAO,OAAO,YAAY,OAAO,SAAS,CAAC;CACjD,OAAO,SAAS,KAAA,KACd,QAAQ,wBACR,QAAQ,qBACN,OAAO,MAAM,GAAG,EAAE,IAClB;AACN;;;;;;AAOA,MAAM,8BACJ,OACA,iBACW;CACX,MAAM,OAAO,oBAAoB,OAAO,YAAY;CACpD,IAAI,KAAK,UAAU,4BACjB,OAAO;CAET,KAAK,IAAI,OAAO,aAAa,QAAQ,OAAO,GAAG,QAAQ,KAAK;EAC1D,MAAM,YAAY,oBAChB,OACA,kCAAkC,cAAc,IAAI,IAClD,6BACJ;EACA,IAAI,UAAU,UAAU,4BACtB,OAAO;CAEX;CAEA,OAAO,oBAAoB,OAAO,6BAA6B;AACjE;;AAGA,MAAM,oBAAoB,UACxB,IAAI,MAAM,WAAW,KAAK,OAAO,EAAE;AAErC,MAAM,kBAAkB,OAAe,SACrC;CACE;CACA,UAAU;CACV,WAAW;CACX,WAAW,iBAAiB,KAAK;CACjC,UAAU,iBAAiB,IAAI;AACjC,CAAC,CAAC,KAAK,GAAG;;;;;AAMZ,MAAa,2BAA2B,EACtC,MACA,MACA,YACuC;CACvC,MAAM,aAAqC,qBAAqB,KAAK;CACrE,MAAM,YAAY,qBAAqB,IAAI;CAC3C,MAAM,eAAe,oBAAoB,UAAU,MAAM,IAAI;CAC7D,OAAO;EACL,OAAO,WAAW;EAClB,eAAe;EACf,YAAY,WAAW,aAAa,UAAU;EAC9C,UAAU,2BAA2B,WAAW,MAAM,YAAY;EAClE,WAAW,eAAe,WAAW,MAAM,YAAY;CACzD;AACF"}