lee-spec-kit 0.6.30 → 0.6.31

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.
package/dist/index.js CHANGED
@@ -514,8 +514,8 @@ var ko = {
514
514
  reviewFixCommitIssueGuidance: "PR \uB9AC\uBDF0 \uC218\uC815 \uCEE4\uBC0B\uC744 \uC9C4\uD589\uD558\uC138\uC694. \uB9AC\uBDF0 \uBC18\uC601 \uD30C\uC77C\uB9CC \uC2A4\uD14C\uC774\uC9D5\uD55C \uB4A4 `fix(#{issueNumber}): <review-fix-summary>` \uD615\uC2DD\uC73C\uB85C \uCEE4\uBC0B\uD558\uC138\uC694. `<review-fix-summary>`\uC5D0\uB294 \uC774\uBC88 \uCEE4\uBC0B\uC5D0\uC11C \uC2E4\uC81C\uB85C \uD574\uACB0\uD55C \uB9AC\uBDF0 \uD56D\uBAA9 \uC694\uC57D\uC744 \uC791\uC131\uD558\uC138\uC694. (\uD0DC\uC2A4\uD06C \uC81C\uBAA9 \uC7AC\uC0AC\uC6A9 \uAE08\uC9C0)",
515
515
  reviewFixCommitGuidance: "PR \uB9AC\uBDF0 \uC218\uC815 \uCEE4\uBC0B\uC744 \uC9C4\uD589\uD558\uC138\uC694. \uB9AC\uBDF0 \uBC18\uC601 \uD30C\uC77C\uB9CC \uC2A4\uD14C\uC774\uC9D5\uD55C \uB4A4 `fix(review): <review-fix-summary>` \uD615\uC2DD\uC73C\uB85C \uCEE4\uBC0B\uD558\uC138\uC694. `<review-fix-summary>`\uC5D0\uB294 \uC774\uBC88 \uCEE4\uBC0B\uC5D0\uC11C \uC2E4\uC81C\uB85C \uD574\uACB0\uD55C \uB9AC\uBDF0 \uD56D\uBAA9 \uC694\uC57D\uC744 \uC791\uC131\uD558\uC138\uC694. (\uD0DC\uC2A4\uD06C \uC81C\uBAA9 \uC7AC\uC0AC\uC6A9 \uAE08\uC9C0)",
516
516
  standaloneNeedsProjectRoot: "standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot \uC124\uC815\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)",
517
- createBranch: 'cd "{projectGitCwd}" && mkdir -p .worktrees && (git worktree add ".worktrees/feat-{issueNumber}-{slug}" "feat/{issueNumber}-{slug}" || git worktree add -b "feat/{issueNumber}-{slug}" ".worktrees/feat-{issueNumber}-{slug}") && echo "worktree: {projectGitCwd}/.worktrees/feat-{issueNumber}-{slug}"',
518
- worktreeCleanupCommand: 'cd "{projectGitCwd}" && git worktree remove "{worktreePath}" && git worktree prune && CURRENT_BRANCH=$(git branch --show-current) && DEFAULT_BRANCH=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | cut -d/ -f2-) && TARGET_BRANCH="${DEFAULT_BRANCH:-$CURRENT_BRANCH}" && if [ -n "$TARGET_BRANCH" ]; then git checkout "$TARGET_BRANCH" >/dev/null 2>&1 || true; fi && if git rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1 && [ -z "$(git status --porcelain)" ]; then git pull --ff-only || true; fi',
517
+ createBranch: 'cd "{projectGitCwd}" && mkdir -p .worktrees && (git worktree add ".worktrees/feat-{issueNumber}-{slug}" "feat/{issueNumber}-{slug}" || git worktree add -b "feat/{issueNumber}-{slug}" ".worktrees/feat-{issueNumber}-{slug}") && WT="{projectGitCwd}/.worktrees/feat-{issueNumber}-{slug}" && for f in .env .env.local .env.development .env.development.local .env.test .env.test.local .env.production .env.production.local; do [ -f "{projectGitCwd}/$f" ] && [ ! -e "$WT/$f" ] && cp "{projectGitCwd}/$f" "$WT/$f" || true; done && echo "worktree: {projectGitCwd}/.worktrees/feat-{issueNumber}-{slug}"',
518
+ worktreeCleanupCommand: 'cd "{projectGitCwd}" && WT="{worktreePath}" && ROOT="$(pwd)" && case "$WT" in "$ROOT"/.worktrees/*) if git worktree list --porcelain | grep -Fxq "worktree $WT"; then git worktree remove --force "$WT" || true; fi; [ -d "$WT" ] && rm -rf "$WT" || true ;; *) echo "skip unsafe worktree path: $WT" ;; esac && git worktree prune && CURRENT_BRANCH=$(git branch --show-current) && DEFAULT_BRANCH=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | cut -d/ -f2-) && TARGET_BRANCH="${DEFAULT_BRANCH:-$CURRENT_BRANCH}" && if [ -n "$TARGET_BRANCH" ]; then git checkout "$TARGET_BRANCH" >/dev/null 2>&1 || true; fi && if git rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1 && [ -z "$(git status --porcelain)" ]; then git pull --ff-only || true; fi',
519
519
  tasksAllDoneButNoChecklist: '\uC644\uB8CC \uC870\uAC74 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8\uB97C \uC791\uC131\uD558\uC138\uC694. tasks.md\uC758 "\uC644\uB8CC \uC870\uAC74" \uC139\uC158\uC5D0 \uAC80\uC99D \uD56D\uBAA9\uC744 \uCD94\uAC00\uD558\uACE0, \uC0AC\uC6A9\uC790\uC640 \uD655\uC778 \uD6C4 \uCDA9\uC871 \uD56D\uBAA9\uC744 [x]\uB85C \uCCB4\uD06C\uD558\uC138\uC694. \uCD5C\uC885 \uC2B9\uC778(OK)\uB3C4 \uBC18\uC601\uD558\uC138\uC694.',
520
520
  tasksAllDoneButChecklist: "\uC644\uB8CC \uC870\uAC74 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8\uC758 \uB0A8\uC740 \uD56D\uBAA9\uC744 \uC9C4\uD589\uD558\uC138\uC694. \uD604\uC7AC \uC9C4\uD589: ({checked}/{total}) \uC0AC\uC6A9\uC790\uC640 \uD655\uC778 \uD6C4 \uCDA9\uC871 \uD56D\uBAA9\uC744 [x]\uB85C \uCCB4\uD06C\uD558\uACE0 \uCD5C\uC885 \uC2B9\uC778(OK)\uC744 \uBC18\uC601\uD558\uC138\uC694.",
521
521
  finishDoingTask: '\uD604\uC7AC DOING/REVIEW \uD0DC\uC2A4\uD06C\uB97C \uC218\uD589\uD558\uC138\uC694: "{title}" ({done}/{total}) \uC644\uB8CC \uC2DC \uACB0\uACFC/\uAC80\uC99D\uC744 \uACF5\uC720\uD558\uACE0 DONE \uCC98\uB9AC',
@@ -1025,8 +1025,8 @@ var en = {
1025
1025
  reviewFixCommitIssueGuidance: "Commit PR review fixes. Stage only review-fix files, then commit with `fix(#{issueNumber}): <review-fix-summary>`. `<review-fix-summary>` must describe review comments resolved in this commit (do not reuse task titles).",
1026
1026
  reviewFixCommitGuidance: "Commit PR review fixes. Stage only review-fix files, then commit with `fix(review): <review-fix-summary>`. `<review-fix-summary>` must describe review comments resolved in this commit (do not reuse task titles).",
1027
1027
  standaloneNeedsProjectRoot: "Standalone mode requires projectRoot. (npx lee-spec-kit config --project-root ...)",
1028
- createBranch: 'cd "{projectGitCwd}" && mkdir -p .worktrees && (git worktree add ".worktrees/feat-{issueNumber}-{slug}" "feat/{issueNumber}-{slug}" || git worktree add -b "feat/{issueNumber}-{slug}" ".worktrees/feat-{issueNumber}-{slug}") && echo "worktree: {projectGitCwd}/.worktrees/feat-{issueNumber}-{slug}"',
1029
- worktreeCleanupCommand: 'cd "{projectGitCwd}" && git worktree remove "{worktreePath}" && git worktree prune && CURRENT_BRANCH=$(git branch --show-current) && DEFAULT_BRANCH=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | cut -d/ -f2-) && TARGET_BRANCH="${DEFAULT_BRANCH:-$CURRENT_BRANCH}" && if [ -n "$TARGET_BRANCH" ]; then git checkout "$TARGET_BRANCH" >/dev/null 2>&1 || true; fi && if git rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1 && [ -z "$(git status --porcelain)" ]; then git pull --ff-only || true; fi',
1028
+ createBranch: 'cd "{projectGitCwd}" && mkdir -p .worktrees && (git worktree add ".worktrees/feat-{issueNumber}-{slug}" "feat/{issueNumber}-{slug}" || git worktree add -b "feat/{issueNumber}-{slug}" ".worktrees/feat-{issueNumber}-{slug}") && WT="{projectGitCwd}/.worktrees/feat-{issueNumber}-{slug}" && for f in .env .env.local .env.development .env.development.local .env.test .env.test.local .env.production .env.production.local; do [ -f "{projectGitCwd}/$f" ] && [ ! -e "$WT/$f" ] && cp "{projectGitCwd}/$f" "$WT/$f" || true; done && echo "worktree: {projectGitCwd}/.worktrees/feat-{issueNumber}-{slug}"',
1029
+ worktreeCleanupCommand: 'cd "{projectGitCwd}" && WT="{worktreePath}" && ROOT="$(pwd)" && case "$WT" in "$ROOT"/.worktrees/*) if git worktree list --porcelain | grep -Fxq "worktree $WT"; then git worktree remove --force "$WT" || true; fi; [ -d "$WT" ] && rm -rf "$WT" || true ;; *) echo "skip unsafe worktree path: $WT" ;; esac && git worktree prune && CURRENT_BRANCH=$(git branch --show-current) && DEFAULT_BRANCH=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | cut -d/ -f2-) && TARGET_BRANCH="${DEFAULT_BRANCH:-$CURRENT_BRANCH}" && if [ -n "$TARGET_BRANCH" ]; then git checkout "$TARGET_BRANCH" >/dev/null 2>&1 || true; fi && if git rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1 && [ -z "$(git status --porcelain)" ]; then git pull --ff-only || true; fi',
1030
1030
  tasksAllDoneButNoChecklist: 'Create the completion checklist. Add verification items to the tasks.md "Completion Criteria" section, then mark satisfied items as [x] after user confirmation. Record final approval (OK) as well.',
1031
1031
  tasksAllDoneButChecklist: "Proceed with remaining completion checklist items. Current progress: ({checked}/{total}) Mark items as [x] only after user confirmation and real verification. Record final approval (OK) as well.",
1032
1032
  finishDoingTask: 'Continue working on the current DOING/REVIEW task: "{title}" ({done}/{total}) After it is complete, share outcome/verification and mark it DONE',
@@ -2951,13 +2951,15 @@ function resolveWorkflowPolicy(workflow) {
2951
2951
  requireIssue: false,
2952
2952
  requireBranch: false,
2953
2953
  requirePr: false,
2954
- requireReview: false
2954
+ requireReview: false,
2955
+ requireMerge: false
2955
2956
  } : {
2956
2957
  mode,
2957
2958
  requireIssue: true,
2958
2959
  requireBranch: true,
2959
2960
  requirePr: true,
2960
- requireReview: true
2961
+ requireReview: true,
2962
+ requireMerge: true
2961
2963
  };
2962
2964
  if (typeof workflow?.requireIssue === "boolean") {
2963
2965
  policy.requireIssue = workflow.requireIssue;
@@ -2971,13 +2973,20 @@ function resolveWorkflowPolicy(workflow) {
2971
2973
  if (typeof workflow?.requireReview === "boolean") {
2972
2974
  policy.requireReview = workflow.requireReview;
2973
2975
  }
2976
+ if (typeof workflow?.requireMerge === "boolean") {
2977
+ policy.requireMerge = workflow.requireMerge;
2978
+ }
2974
2979
  if (!policy.requireIssue) {
2975
2980
  policy.requireBranch = false;
2976
2981
  }
2977
2982
  if (!policy.requirePr) {
2978
2983
  policy.requireReview = false;
2984
+ policy.requireMerge = false;
2979
2985
  } else if (policy.requireReview) {
2980
2986
  policy.requirePr = true;
2987
+ policy.requireMerge = true;
2988
+ } else if (policy.requireMerge) {
2989
+ policy.requirePr = true;
2981
2990
  }
2982
2991
  return policy;
2983
2992
  }
@@ -3076,7 +3085,7 @@ function isPrePrReviewSatisfied(feature, prePrReviewPolicy) {
3076
3085
  return true;
3077
3086
  }
3078
3087
  function isFeatureDone(feature, workflowPolicy, prePrReviewPolicy) {
3079
- return feature.specStatus === "Approved" && feature.planStatus === "Approved" && !feature.git.docsHasUncommittedChanges && !feature.git.projectHasUncommittedChanges && feature.docs.tasksExists && feature.tasks.total > 0 && feature.tasks.total === feature.tasks.done && isCompletionChecklistDone(feature) && isTasksDocApproved(feature) && (!workflowPolicy.requireIssue || !!feature.issueNumber) && (!workflowPolicy.requirePr || isPrMetadataConfigured(feature) && !!feature.pr.link) && (!workflowPolicy.requireReview || feature.pr.status === "Approved") && isPrePrReviewSatisfied(feature, prePrReviewPolicy);
3088
+ return feature.specStatus === "Approved" && feature.planStatus === "Approved" && !feature.git.docsHasUncommittedChanges && !feature.git.projectHasUncommittedChanges && feature.docs.tasksExists && feature.tasks.total > 0 && feature.tasks.total === feature.tasks.done && isCompletionChecklistDone(feature) && isTasksDocApproved(feature) && (!workflowPolicy.requireIssue || !!feature.issueNumber) && (!workflowPolicy.requirePr || isPrMetadataConfigured(feature) && !!feature.pr.link) && (!workflowPolicy.requireMerge || feature.pr.status === "Approved") && isPrePrReviewSatisfied(feature, prePrReviewPolicy);
3080
3089
  }
3081
3090
  function getPrReviewRemoteBlockReasons(feature, lang) {
3082
3091
  const remote = feature.pr.remote;
@@ -3981,10 +3990,10 @@ ${tr(lang, "messages", "taskCommitGateWarnProceed", {
3981
3990
  step: 14,
3982
3991
  name: tr(lang, "steps", "codeReview"),
3983
3992
  checklist: {
3984
- done: (f) => !workflowPolicy.requireReview || isPrMetadataConfigured(f) && f.pr.status === "Approved"
3993
+ done: (f) => !workflowPolicy.requireMerge || isPrMetadataConfigured(f) && f.pr.status === "Approved"
3985
3994
  },
3986
3995
  current: {
3987
- when: (f) => workflowPolicy.requireReview && isPrMetadataConfigured(f) && !!f.pr.link && f.pr.status !== "Approved",
3996
+ when: (f) => workflowPolicy.requireMerge && isPrMetadataConfigured(f) && !!f.pr.link && f.pr.status !== "Approved",
3988
3997
  actions: (f) => {
3989
3998
  if (!f.pr.status) {
3990
3999
  return [
@@ -3998,7 +4007,7 @@ ${tr(lang, "messages", "taskCommitGateWarnProceed", {
3998
4007
  ];
3999
4008
  }
4000
4009
  if (f.pr.status === "Review") {
4001
- if (f.pr.remote?.available && f.pr.remote.isMerged) {
4010
+ if (workflowPolicy.requireReview && f.pr.remote?.available && f.pr.remote.isMerged) {
4002
4011
  return [
4003
4012
  {
4004
4013
  type: "instruction",
@@ -4009,7 +4018,7 @@ ${tr(lang, "messages", "taskCommitGateWarnProceed", {
4009
4018
  }
4010
4019
  ];
4011
4020
  }
4012
- if (!f.docs.prReviewEvidenceFieldExists) {
4021
+ if (workflowPolicy.requireReview && !f.docs.prReviewEvidenceFieldExists) {
4013
4022
  return [
4014
4023
  {
4015
4024
  type: "instruction",
@@ -4020,7 +4029,7 @@ ${tr(lang, "messages", "taskCommitGateWarnProceed", {
4020
4029
  }
4021
4030
  ];
4022
4031
  }
4023
- if (!f.prReview.evidenceProvided) {
4032
+ if (workflowPolicy.requireReview && !f.prReview.evidenceProvided) {
4024
4033
  return [
4025
4034
  {
4026
4035
  type: "instruction",
@@ -4031,7 +4040,7 @@ ${tr(lang, "messages", "taskCommitGateWarnProceed", {
4031
4040
  }
4032
4041
  ];
4033
4042
  }
4034
- if (!f.docs.prReviewDecisionFieldExists) {
4043
+ if (workflowPolicy.requireReview && !f.docs.prReviewDecisionFieldExists) {
4035
4044
  return [
4036
4045
  {
4037
4046
  type: "instruction",
@@ -4042,7 +4051,7 @@ ${tr(lang, "messages", "taskCommitGateWarnProceed", {
4042
4051
  }
4043
4052
  ];
4044
4053
  }
4045
- if (!f.prReview.decisionProvided) {
4054
+ if (workflowPolicy.requireReview && !f.prReview.decisionProvided) {
4046
4055
  return [
4047
4056
  {
4048
4057
  type: "instruction",
@@ -4055,15 +4064,16 @@ ${tr(lang, "messages", "taskCommitGateWarnProceed", {
4055
4064
  }
4056
4065
  const remoteBlockReasons = getPrReviewRemoteBlockReasons(f, lang);
4057
4066
  const remoteUnavailable = workflowPolicy.mode === "github" && !!f.pr.link && (!f.pr.remote || !f.pr.remote.available);
4058
- const actions = [
4059
- {
4067
+ const actions = [];
4068
+ if (workflowPolicy.requireReview) {
4069
+ actions.push({
4060
4070
  type: "instruction",
4061
4071
  category: "code_review",
4062
4072
  requiresUserCheck: true,
4063
4073
  uiDetailKey: "context.actionDetail.codeReviewResolve",
4064
4074
  message: tr(lang, "messages", "prReviewResolve")
4065
- }
4066
- ];
4075
+ });
4076
+ }
4067
4077
  if (!f.git.projectGitCwd) {
4068
4078
  actions.push({
4069
4079
  type: "instruction",
@@ -4121,7 +4131,18 @@ ${tr(lang, "messages", "taskCommitGateWarnProceed", {
4121
4131
  })
4122
4132
  });
4123
4133
  }
4124
- return actions;
4134
+ if (actions.length > 0) return actions;
4135
+ return [
4136
+ {
4137
+ type: "instruction",
4138
+ category: "code_review",
4139
+ requiresUserCheck: true,
4140
+ uiDetailKey: "context.actionDetail.codeReviewMergeAfterOk",
4141
+ message: tr(lang, "messages", "prReviewMerge", {
4142
+ featureRef: f.id || f.folderName
4143
+ })
4144
+ }
4145
+ ];
4125
4146
  }
4126
4147
  return [
4127
4148
  {
@@ -5331,7 +5352,7 @@ async function parseFeature(featurePath, type, context, options) {
5331
5352
  prDocPrFieldExists = hasAnySpecKey(content, ["PR", "Pull Request"]);
5332
5353
  prDocReviewStatusFieldExists = hasAnySpecKey(content, ["PR \uC0C1\uD0DC", "PR Status"]);
5333
5354
  }
5334
- if (workflowPolicy.requireReview && prStatus === "Review" && prLink && effectiveProjectGitCwd) {
5355
+ if (workflowPolicy.requireMerge && prStatus === "Review" && prLink && effectiveProjectGitCwd) {
5335
5356
  prRemote = resolvePrRemoteStatus(prLink, effectiveProjectGitCwd) || void 0;
5336
5357
  }
5337
5358
  const warnings = [];
@@ -5473,7 +5494,7 @@ async function parseFeature(featurePath, type, context, options) {
5473
5494
  }
5474
5495
  const tasksDocApproved = !tasksDocStatusFieldExists || tasksDocStatus === "Approved";
5475
5496
  const implementationDone = tasksExists && tasksSummary.total > 0 && tasksSummary.total === tasksSummary.done && isCompletionChecklistDone2({ completionChecklist }) && tasksDocApproved;
5476
- const workflowDone = implementationDone && !docsHasUncommittedChanges && !projectHasUncommittedChanges && specStatus === "Approved" && planStatus === "Approved" && (!workflowPolicy.requireIssue || !!issueNumber) && (!workflowPolicy.requirePr || isPrMetadataConfigured2({ docs: { prFieldExists, prStatusFieldExists } }) && !!prLink) && (!workflowPolicy.requireReview || prStatus === "Approved") && isPrePrReviewSatisfied2(
5497
+ const workflowDone = implementationDone && !docsHasUncommittedChanges && !projectHasUncommittedChanges && specStatus === "Approved" && planStatus === "Approved" && (!workflowPolicy.requireIssue || !!issueNumber) && (!workflowPolicy.requirePr || isPrMetadataConfigured2({ docs: { prFieldExists, prStatusFieldExists } }) && !!prLink) && (!workflowPolicy.requireMerge || prStatus === "Approved") && isPrePrReviewSatisfied2(
5477
5498
  {
5478
5499
  docs: {
5479
5500
  prePrReviewFieldExists,
@@ -5504,11 +5525,11 @@ async function parseFeature(featurePath, type, context, options) {
5504
5525
  }
5505
5526
  if (workflowPolicy.requirePr && prFieldExists && prStatusFieldExists) {
5506
5527
  if (!prLink) warnings.push(tr(lang, "warnings", "workflowPrLinkMissing"));
5507
- if (workflowPolicy.requireReview) {
5528
+ if (workflowPolicy.requireMerge) {
5508
5529
  if (!prStatus) warnings.push(tr(lang, "warnings", "workflowPrStatusMissing"));
5509
5530
  if (prStatus && prStatus !== "Approved") {
5510
5531
  warnings.push(tr(lang, "warnings", "workflowPrStatusNotApproved"));
5511
- if (prStatus === "Review") {
5532
+ if (workflowPolicy.requireReview && prStatus === "Review") {
5512
5533
  if (!prReviewEvidenceFieldExists || !prReviewEvidenceProvided) {
5513
5534
  warnings.push(tr(lang, "warnings", "workflowPrReviewEvidenceMissing"));
5514
5535
  }
@@ -10808,8 +10829,14 @@ function runGhJson2(args, cwd, lang) {
10808
10829
  }
10809
10830
  function ensureSections(body, sections, kind, lang) {
10810
10831
  const hasHeading = (sectionHeading) => {
10811
- const re = new RegExp(`^##\\s+${escapeRegExp3(sectionHeading)}\\s*$`, "m");
10812
- return re.test(body);
10832
+ const target = normalizeHeading(sectionHeading);
10833
+ const lines = body.split("\n");
10834
+ for (const line of lines) {
10835
+ const match = line.match(/^\s*##\s+(.+?)\s*$/);
10836
+ if (!match) continue;
10837
+ if (normalizeHeading(match[1]) === target) return true;
10838
+ }
10839
+ return false;
10813
10840
  };
10814
10841
  const hasMetadataField = (field) => {
10815
10842
  const re = new RegExp(`^\\s*-\\s*\\*\\*${escapeRegExp3(field)}\\*\\*\\s*:`, "m");
@@ -10970,7 +10997,13 @@ function getFeatureDocPaths(feature) {
10970
10997
  };
10971
10998
  }
10972
10999
  function normalizeHeading(value) {
10973
- return value.trim().replace(/\s+/g, " ").toLowerCase();
11000
+ let normalized = value.trim();
11001
+ for (; ; ) {
11002
+ const next = normalized.replace(/\s*\([^)]*\)\s*$/, "").replace(/\s*([^)]*)\s*$/, "").trim();
11003
+ if (next === normalized) break;
11004
+ normalized = next;
11005
+ }
11006
+ return normalized.replace(/\s+/g, " ").toLowerCase();
10974
11007
  }
10975
11008
  function extractMarkdownByHeadings(content, headings, levels) {
10976
11009
  const targets = new Set(headings.map((heading) => normalizeHeading(heading)));