codex-overleaf-link 1.3.0 → 1.3.6

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.
@@ -7,6 +7,10 @@
7
7
  })(typeof globalThis !== 'undefined' ? globalThis : window, function sessionStateFactory() {
8
8
  'use strict';
9
9
 
10
+ const i18n = (typeof module === 'object' && module.exports)
11
+ ? require('./i18n')
12
+ : (typeof globalThis !== 'undefined' ? globalThis : window).CodexOverleafI18n;
13
+
10
14
  const DEFAULT_PANEL_STATE = {
11
15
  mode: 'confirm',
12
16
  model: 'gpt-5.4',
@@ -17,6 +21,7 @@
17
21
  autoOpen: true,
18
22
  loadCodexLocalSkills: true,
19
23
  loadCodexOverleafSkills: true,
24
+ codexOverleafSkillEnabled: {},
20
25
  panelWidth: 380,
21
26
  task: '',
22
27
  focusFiles: [],
@@ -105,12 +110,14 @@
105
110
  state.autoOpen = state.autoOpen !== false;
106
111
  state.loadCodexLocalSkills = state.loadCodexLocalSkills !== false;
107
112
  state.loadCodexOverleafSkills = state.loadCodexOverleafSkills !== false;
113
+ state.codexOverleafSkillEnabled = normalizeCodexOverleafSkillEnabled(state.codexOverleafSkillEnabled);
108
114
  state.panelWidth = normalizePanelWidth(state.panelWidth);
109
115
  state.task = typeof state.task === 'string' ? state.task : '';
110
116
  state.model = typeof state.model === 'string' && state.model ? state.model : DEFAULT_PANEL_STATE.model;
111
117
  state.customInstructionsByProject = normalizeCustomInstructionsByProject(state.customInstructionsByProject);
112
- state.runs = normalizeRuns(state.runs, options);
113
- state.sessions = normalizeSessions(state, input, options);
118
+ const localizedOptions = { ...options, locale: state.locale };
119
+ state.runs = normalizeRuns(state.runs, localizedOptions);
120
+ state.sessions = normalizeSessions(state, input, localizedOptions);
114
121
  state.activeSessionId = resolveActiveSessionId(state.sessions, input.activeSessionId);
115
122
 
116
123
  return mirrorActiveSession(state);
@@ -497,12 +504,13 @@
497
504
 
498
505
  function normalizeRun(run, options = {}) {
499
506
  const shouldStopRestoredRun = options.restoreRunningRuns === true && run.status === 'running';
507
+ const locale = options.locale || i18n.DEFAULT_LOCALE;
500
508
  const events = normalizeRunEvents(run.events);
501
509
  if (shouldStopRestoredRun) {
502
510
  events.push({
503
- title: '页面刷新后已停止跟踪这轮任务',
511
+ title: i18n.t(locale, 'restoredRunStoppedTitle'),
504
512
  status: 'failed',
505
- detail: '插件重新加载时发现这轮任务还标记为处理中。为了避免继续显示过期状态,已把它标记为中断;可以重新运行任务。',
513
+ detail: i18n.t(locale, 'restoredRunStoppedDetail'),
506
514
  timestamp: new Date().toISOString()
507
515
  });
508
516
  }
@@ -515,7 +523,7 @@
515
523
  reasoningEffort: typeof run.reasoningEffort === 'string' ? run.reasoningEffort : '',
516
524
  speedTier: typeof run.speedTier === 'string' ? run.speedTier : '',
517
525
  status: shouldStopRestoredRun ? 'failed' : normalizeRunStatus(run.status),
518
- statusText: shouldStopRestoredRun ? '页面刷新后已停止跟踪' : sanitizeAssistantVisibleText(run.statusText),
526
+ statusText: shouldStopRestoredRun ? i18n.t(locale, 'restoredRunStoppedStatus') : sanitizeAssistantVisibleText(run.statusText),
519
527
  startedAt: typeof run.startedAt === 'string' ? run.startedAt : '',
520
528
  finishedAt: shouldStopRestoredRun ? new Date().toISOString() : typeof run.finishedAt === 'string' ? run.finishedAt : '',
521
529
  events: events.slice(-MAX_RUN_EVENTS),
@@ -669,6 +677,23 @@
669
677
  return result;
670
678
  }
671
679
 
680
+ function normalizeCodexOverleafSkillEnabled(value) {
681
+ const result = {};
682
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
683
+ return result;
684
+ }
685
+ for (const key of Object.keys(value)) {
686
+ if (typeof key !== 'string' || !key) {
687
+ continue;
688
+ }
689
+ if (typeof value[key] !== 'boolean') {
690
+ continue;
691
+ }
692
+ result[key] = value[key];
693
+ }
694
+ return result;
695
+ }
696
+
672
697
  function normalizeProjectPrefKey(value) {
673
698
  const key = typeof value === 'string' ? value.trim() : '';
674
699
  if (!key) {
@@ -714,6 +739,7 @@
714
739
  autoOpen: source.autoOpen !== false,
715
740
  loadCodexLocalSkills: source.loadCodexLocalSkills !== false,
716
741
  loadCodexOverleafSkills: source.loadCodexOverleafSkills !== false,
742
+ codexOverleafSkillEnabled: normalizeCodexOverleafSkillEnabled(source.codexOverleafSkillEnabled),
717
743
  panelWidth: normalizePanelWidth(source.panelWidth),
718
744
  task: summarizeTextForStorage(active?.task || source.task || '', 'task'),
719
745
  focusFiles: normalizeFocusFiles(active?.focusFiles || source.focusFiles),
@@ -342,11 +342,31 @@
342
342
  experimentalOtByProject: normalizeBooleanMap(state.experimentalOtByProject),
343
343
  customInstructionsByProject: normalizeStringMap(state.customInstructionsByProject),
344
344
  governanceRulesByProject: normalizeGovernanceRulesMap(state.governanceRulesByProject),
345
- selectedLocalSkillIdsByProject: normalizeStringListMap(state.selectedLocalSkillIdsByProject)
345
+ selectedLocalSkillIdsByProject: normalizeStringListMap(state.selectedLocalSkillIdsByProject),
346
+ codexOverleafSkillEnabled: normalizeCodexOverleafSkillEnabledMap(state.codexOverleafSkillEnabled)
346
347
  };
347
348
  return prefs;
348
349
  }
349
350
 
351
+ function normalizeCodexOverleafSkillEnabledMap(value) {
352
+ var result = {};
353
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
354
+ return result;
355
+ }
356
+ var keys = Object.keys(value);
357
+ for (var i = 0; i < keys.length; i++) {
358
+ var key = keys[i];
359
+ if (typeof key !== 'string' || !key) {
360
+ continue;
361
+ }
362
+ if (typeof value[key] !== 'boolean') {
363
+ continue;
364
+ }
365
+ result[key] = value[key];
366
+ }
367
+ return result;
368
+ }
369
+
350
370
  function normalizeBooleanMap(value) {
351
371
  var result = {};
352
372
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -223,7 +223,15 @@
223
223
  if (typeof current !== 'string') {
224
224
  return;
225
225
  }
226
- if (typeof operation.replaceAll === 'string') {
226
+ // The writeback already verified the editor content it produced. When
227
+ // that authoritative post-write content is available, trust it instead
228
+ // of re-deriving the result by re-applying patches: a wide patch's
229
+ // `expected` spans a whole paragraph, so it silently fails to re-apply
230
+ // against any base that drifted even slightly from the patch's base,
231
+ // and the result would otherwise collapse to the un-patched content.
232
+ if (typeof operation.verifiedContent === 'string') {
233
+ filesByPath.set(operation.path, operation.verifiedContent);
234
+ } else if (typeof operation.replaceAll === 'string') {
227
235
  filesByPath.set(operation.path, operation.replaceAll);
228
236
  } else if (Array.isArray(operation.patches) && operation.patches.length) {
229
237
  const patched = applyTextPatches(current, operation.patches);
@@ -7,25 +7,31 @@ const {
7
7
  getNativeHostPlatform
8
8
  } = require('./nativeHostPlatform');
9
9
  const {
10
+ ensureCodexOverleafSkillInstalled,
10
11
  getCodexOverleafSkillsRoot,
11
- materializeProjectSkillsAsCodexSkills
12
+ materializeProjectSkillsAsCodexSkills,
13
+ OFFICIAL_CODEX_OVERLEAF_SKILL_IDS
12
14
  } = require('./localSkills');
13
15
 
14
16
  const COPIED_USER_CODEX_FILES = [
15
17
  'auth.json',
16
18
  'config.toml',
17
- 'AGENTS.md',
18
19
  'installation_id',
19
20
  'models_cache.json',
20
21
  'version.json'
21
22
  ];
22
23
 
23
24
  const LINKED_USER_CODEX_DIRS = [
24
- 'rules',
25
- 'memories',
26
25
  'vendor_imports'
27
26
  ];
28
27
 
28
+ // User-global Codex instruction/memory entries that must never enter the plugin
29
+ // Codex home. The extension supplies its own per-project personalization via the
30
+ // prompt; inheriting the user's global Codex guidance here is a leak. The plugin
31
+ // home is reused across runs, so these are removed every prepare to also clear
32
+ // entries left by earlier extension versions.
33
+ const ISOLATED_USER_INSTRUCTION_ENTRIES = ['AGENTS.md', 'rules', 'memories'];
34
+
29
35
  const LOCAL_SKILL_USER_CODEX_DIRS = [
30
36
  'plugins',
31
37
  'superpowers'
@@ -93,6 +99,18 @@ function preparePluginCodexHome(env = process.env, options = {}) {
93
99
  copied.push(fileName);
94
100
  }
95
101
 
102
+ // Personalization isolation: the plugin Codex home must never inherit the
103
+ // user's global Codex instructions/memory. Remove them every run — this also
104
+ // clears stale entries left by earlier extension versions. This runs after
105
+ // the samePath early-return above, so it never touches the user's real
106
+ // ~/.codex when the plugin home and user home are the same directory.
107
+ for (const entryName of ISOLATED_USER_INSTRUCTION_ENTRIES) {
108
+ removePluginHomeEntry(pluginHome, entryName, skippedLinks);
109
+ }
110
+ if (!isRegularFile(path.join(userHome, 'config.toml'))) {
111
+ removePluginHomeEntry(pluginHome, 'config.toml', skippedLinks);
112
+ }
113
+
96
114
  if (!loadCodexLocalSkills) {
97
115
  for (const entryName of LOCAL_SKILL_PLUGIN_HOME_ENTRIES) {
98
116
  removePluginHomeEntry(pluginHome, entryName, skippedLinks);
@@ -119,6 +137,8 @@ function preparePluginCodexHome(env = process.env, options = {}) {
119
137
  linked.push(dirName);
120
138
  }
121
139
 
140
+ ensureDefaultCodexOverleafSkills({ env });
141
+
122
142
  const skillsResult = composePluginSkillsDirectory({
123
143
  userHome,
124
144
  pluginHome,
@@ -137,13 +157,68 @@ function preparePluginCodexHome(env = process.env, options = {}) {
137
157
  return { userHome, pluginHome, copied, linked, skippedLinks };
138
158
  }
139
159
 
160
+ function ensureDefaultCodexOverleafSkills({ env = process.env } = {}) {
161
+ for (const id of OFFICIAL_CODEX_OVERLEAF_SKILL_IDS) {
162
+ const src = path.resolve(__dirname, 'skills', id, 'SKILL.md');
163
+ const content = fs.readFileSync(src, 'utf8');
164
+ ensureCodexOverleafSkillInstalled({ skillId: id, content, env });
165
+ }
166
+ }
167
+
140
168
  function copyUserCodexFile(source, target, fileName, options = {}) {
141
- if (fileName !== 'config.toml' || options.loadCodexLocalSkills !== false) {
169
+ if (fileName !== 'config.toml') {
142
170
  fs.copyFileSync(source, target);
143
171
  return;
144
172
  }
145
- const content = fs.readFileSync(source, 'utf8');
146
- fs.writeFileSync(target, sanitizeCodexConfigForLocalSkillIsolation(content), 'utf8');
173
+ let content = stripPersonalizationFromCodexConfig(fs.readFileSync(source, 'utf8'));
174
+ if (options.loadCodexLocalSkills === false) {
175
+ content = sanitizeCodexConfigForLocalSkillIsolation(content);
176
+ }
177
+ fs.writeFileSync(target, content, 'utf8');
178
+ }
179
+
180
+ // Removes the top-level `personality` key (Codex's built-in "personality"
181
+ // feature) so the plugin Codex home never inherits the user's global
182
+ // personalization. Only the top-level key is removed — a `personality` key
183
+ // inside a [section] is a different key and is preserved. Handles single-line
184
+ // values and both multi-line string forms (""" basic and ''' literal). All
185
+ // other lines pass through unchanged, aside from line endings being normalized to LF.
186
+ function stripPersonalizationFromCodexConfig(content) {
187
+ const lines = String(content || '').split(/\r?\n/);
188
+ const output = [];
189
+ let beforeFirstSection = true;
190
+ let closingDelimiter = '';
191
+
192
+ for (const line of lines) {
193
+ if (closingDelimiter) {
194
+ if (line.includes(closingDelimiter)) {
195
+ closingDelimiter = '';
196
+ }
197
+ // Drop every line of the multi-line value, including the one that closes it.
198
+ continue;
199
+ }
200
+ if (parseTomlSectionName(line)) {
201
+ beforeFirstSection = false;
202
+ output.push(line);
203
+ continue;
204
+ }
205
+ if (beforeFirstSection) {
206
+ const match = line.match(/^\s*personality\s*=\s*(.*)$/);
207
+ if (match) {
208
+ const value = match[1];
209
+ const opener = value.startsWith('"""') ? '"""'
210
+ : value.startsWith("'''") ? "'''"
211
+ : '';
212
+ if (opener && value.indexOf(opener, opener.length) === -1) {
213
+ closingDelimiter = opener;
214
+ }
215
+ continue;
216
+ }
217
+ }
218
+ output.push(line);
219
+ }
220
+
221
+ return output.join('\n');
147
222
  }
148
223
 
149
224
  function sanitizeCodexConfigForLocalSkillIsolation(content) {
@@ -532,6 +607,7 @@ module.exports = {
532
607
  buildCodexHomeEnv,
533
608
  clearPluginCodexHistory,
534
609
  composePluginSkillsDirectory,
610
+ ensureDefaultCodexOverleafSkills,
535
611
  getPluginCodexHome,
536
612
  getUserCodexHome,
537
613
  preparePluginCodexHome
@@ -68,6 +68,7 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
68
68
  const codexSkillInvocationContext = loadCodexSkillInvocationContext({
69
69
  skillInvocation,
70
70
  loadCodexOverleafSkills: skillLoading.loadCodexOverleafSkills,
71
+ enabledCodexOverleafSkillIds: params.enabledCodexOverleafSkillIds,
71
72
  env,
72
73
  emit
73
74
  });
@@ -385,6 +386,7 @@ function normalizeSkillLoadingSettings(params = {}) {
385
386
  function loadCodexSkillInvocationContext({
386
387
  skillInvocation,
387
388
  loadCodexOverleafSkills = true,
389
+ enabledCodexOverleafSkillIds,
388
390
  env = process.env,
389
391
  emit = () => {}
390
392
  } = {}) {
@@ -399,6 +401,7 @@ function loadCodexSkillInvocationContext({
399
401
  const result = loadSelectedCodexOverleafSkill({
400
402
  skillId: invocation.id,
401
403
  loadCodexOverleafSkills,
404
+ enabledCodexOverleafSkillIds,
402
405
  env
403
406
  });
404
407
  if (result.missing.length) {
@@ -13,6 +13,7 @@ const MAX_SKILL_CONTENT_CHARS = MAX_SKILL_CONTENT_BYTES;
13
13
  const MAX_SKILL_PREVIEW_CHARS = 240;
14
14
  const PROJECT_SKILL_SCOPE = 'project';
15
15
  const CODEX_OVERLEAF_SKILL_SCOPE = 'codex-overleaf';
16
+ const OFFICIAL_CODEX_OVERLEAF_SKILL_IDS = ['annotated-rewrite'];
16
17
 
17
18
  function listProjectSkills({ projectId, rootDir } = {}) {
18
19
  const skillsDir = getProjectSkillsDir(projectId, { rootDir });
@@ -110,13 +111,18 @@ function listCodexOverleafSkills({ env = process.env, skillsRoot } = {}) {
110
111
  continue;
111
112
  }
112
113
  const content = fs.readFileSync(filePath, 'utf8');
113
- skills.push(buildSkillMetadata({
114
- id: entry.name,
115
- content,
116
- size: stat.size,
117
- updatedAt: stat.mtime.toISOString(),
118
- scope: CODEX_OVERLEAF_SKILL_SCOPE
119
- }));
114
+ const isOfficial = OFFICIAL_CODEX_OVERLEAF_SKILL_IDS.includes(entry.name);
115
+ skills.push({
116
+ ...buildSkillMetadata({
117
+ id: entry.name,
118
+ content,
119
+ size: stat.size,
120
+ updatedAt: stat.mtime.toISOString(),
121
+ scope: CODEX_OVERLEAF_SKILL_SCOPE
122
+ }),
123
+ official: isOfficial,
124
+ removable: !isOfficial
125
+ });
120
126
  }
121
127
 
122
128
  return { skills: skills.sort((left, right) => left.id.localeCompare(right.id)) };
@@ -147,6 +153,9 @@ function installCodexOverleafSkill({ skillId, content, env = process.env, skills
147
153
 
148
154
  function removeCodexOverleafSkill({ skillId, env = process.env, skillsRoot } = {}) {
149
155
  const id = validateSkillId(skillId);
156
+ if (OFFICIAL_CODEX_OVERLEAF_SKILL_IDS.includes(id)) {
157
+ throw new Error(`Cannot remove official skill: ${id}`);
158
+ }
150
159
  const root = getCodexOverleafSkillsRoot({ env, skillsRoot });
151
160
  const skillDir = resolveInside(root, id, 'Unsafe Codex Overleaf skill path');
152
161
  const removed = fs.existsSync(skillDir);
@@ -156,6 +165,21 @@ function removeCodexOverleafSkill({ skillId, env = process.env, skillsRoot } = {
156
165
  return { id, scope: CODEX_OVERLEAF_SKILL_SCOPE, removed };
157
166
  }
158
167
 
168
+ function ensureCodexOverleafSkillInstalled({ skillId, content, env = process.env } = {}) {
169
+ const root = getCodexOverleafSkillsRoot({ env });
170
+ const skillPath = resolveCodexOverleafSkillPath(skillId, { env });
171
+ if (fs.existsSync(skillPath)) {
172
+ assertNoSymlinkEscape(skillPath, root, 'Existing official skill path is unsafe');
173
+ const stat = fs.lstatSync(skillPath);
174
+ if (stat.isFile()) {
175
+ return;
176
+ }
177
+ // symlink, directory, or other — remove and reinstall
178
+ fs.rmSync(skillPath, { recursive: true, force: true });
179
+ }
180
+ installCodexOverleafSkill({ skillId, content, env });
181
+ }
182
+
159
183
  function loadSelectedProjectSkills({ projectId, selectedSkillIds, rootDir, projectRoot } = {}) {
160
184
  const ids = normalizeSelectedSkillIds(selectedSkillIds);
161
185
  const skills = [];
@@ -230,6 +254,7 @@ function materializeProjectSkillsAsCodexSkills({
230
254
  function loadSelectedCodexOverleafSkill({
231
255
  skillId,
232
256
  loadCodexOverleafSkills = true,
257
+ enabledCodexOverleafSkillIds,
233
258
  env = process.env,
234
259
  skillsRoot
235
260
  } = {}) {
@@ -248,6 +273,18 @@ function loadSelectedCodexOverleafSkill({
248
273
  }]
249
274
  };
250
275
  }
276
+ // Per-skill enable check: if enabledCodexOverleafSkillIds is provided and does not include
277
+ // this skill, treat it as not available. Absent param = all enabled (backward-safe).
278
+ if (Array.isArray(enabledCodexOverleafSkillIds) && !enabledCodexOverleafSkillIds.includes(id)) {
279
+ return {
280
+ skill: null,
281
+ missing: [],
282
+ ignored: [{
283
+ id,
284
+ reason: 'codex_overleaf_skill_disabled'
285
+ }]
286
+ };
287
+ }
251
288
 
252
289
  const filePath = resolveCodexOverleafSkillPath(id, { env, skillsRoot });
253
290
  if (!fs.existsSync(filePath)) {
@@ -461,7 +498,9 @@ module.exports = {
461
498
  CODEX_OVERLEAF_SKILL_SCOPE,
462
499
  MAX_SKILL_CONTENT_BYTES,
463
500
  MAX_SKILL_CONTENT_CHARS,
501
+ OFFICIAL_CODEX_OVERLEAF_SKILL_IDS,
464
502
  PROJECT_SKILL_SCOPE,
503
+ ensureCodexOverleafSkillInstalled,
465
504
  getDefaultCodexOverleafSkillsRoot,
466
505
  getCodexOverleafSkillsRoot,
467
506
  installCodexOverleafSkill,
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: annotated-rewrite
3
+ description: >
4
+ When editing `.tex` files, use this skill as often as possible. When
5
+ rewriting `.tex` content spanning more than a few words, comment out the
6
+ original and write the replacement below it so the user can review the
7
+ before/after diff directly in the source.
8
+ ---
9
+
10
+ # Annotated Rewrite
11
+
12
+ When rewriting `.tex` file content (paragraphs, sentences, equations, list
13
+ bodies, or any block larger than a few words), preserve the original by
14
+ commenting it out rather than deleting it, then write the replacement below.
15
+ This lets the user review exactly what changed before accepting the edit.
16
+
17
+ ## When to apply
18
+
19
+ Default to this pattern whenever the replaced content is **more than a short inline phrase** (see the word-level exception below), regardless of content type — prose, equations, lists, environment bodies, or any contiguous block. Use it as often as possible — when in doubt, use it.
20
+
21
+ A useful guide: if the replaced content is **3 or more source lines**, always apply it. For shorter replacements (1-2 source lines), lean toward applying it unless the change is purely **word-level**: replacing a single word, fixing a typo, or changing a run of 1-5 words within an unbroken clause.
22
+
23
+ Word-level edits are the only case where skipping the annotated format is clearly appropriate (subject to the structural exceptions in Rule 8).
24
+
25
+ ## Edit format
26
+
27
+ ```tex
28
+ % [original]
29
+ % The proposed method encodes user history as a fixed-length vector,
30
+ % then applies a linear projection before computing dot-product scores.
31
+ % Training uses binary cross-entropy loss over sampled negatives.
32
+ %
33
+ % \begin{equation}
34
+ % \hat{y}_{ui} = \mathbf{u}_i^\top \mathbf{v}_j
35
+ % \end{equation}
36
+
37
+ % [revised]
38
+ We encode user history with a transformer encoder, producing a
39
+ context-aware representation that is projected into the item space.
40
+ Training minimises a softmax loss over in-batch negatives.
41
+
42
+ \begin{equation}
43
+ \hat{y}_{ui} = \mathrm{softmax}(\mathbf{U}\mathbf{V}^\top)_{ij}
44
+ \end{equation}
45
+ ```
46
+
47
+ ## Rules
48
+
49
+ 1. Copy the original lines **verbatim** into the comment block — do not alter them
50
+ 2. Include every line of the replaced content verbatim with `%` prepended;
51
+ blank lines within the block become `%` empty-comment lines (a line
52
+ containing only `%`). Include `\begin{...}` / `\end{...}` markers only when
53
+ they are part of the replaced content — do not comment out enclosing
54
+ environment markers that are not themselves changing
55
+ 3. No blank line between `% [original]` and the first commented line
56
+ 4. One blank line between the last commented line and `% [revised]`; one blank
57
+ line before `% [original]` and after the last line of the replacement (to
58
+ separate the annotated construct from surrounding document text)
59
+ 5. The new content after `% [revised]` is normal LaTeX (not commented)
60
+ 6. For multiple non-adjacent replaced blocks, apply the pattern independently
61
+ to each block
62
+ 7. **Idempotency**: if the content being edited already contains a
63
+ `% [original]` / `% [revised]` block, edit only the active content after
64
+ `% [revised]` — do not re-wrap the existing annotated block or create a new
65
+ outer annotation around it
66
+ 8. **Structure safety**: only apply this pattern at **block level** (paragraph,
67
+ sentence, equation block, list body, or environment body). Do NOT insert
68
+ `% [original]` / `% [revised]` inside
69
+ macro arguments, table rows, math expressions, `\caption{...}`,
70
+ `\item[...]`, or preamble commands — make minimal direct edits there instead,
71
+ or ask for confirmation before changing structurally sensitive content