docupuncture 0.1.6 → 0.1.8

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/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  Treat the document you already have.
4
4
 
5
- Insert precise edits without changing the theme, format, comments, or history.
5
+ Make targeted edits in an existing Google Doc, Sheet, or Slides file using a reviewable Apps Script. DocuPuncture starts with a dry run and is designed to preserve surrounding structure for supported edit types.
6
6
 
7
- When an AI regenerates a Google Doc, Sheet, or deck, those things usually go with it. DocuPuncture writes a small Apps Script you paste and run on the live file.
7
+ When an AI regenerates a Google Doc, Sheet, or deck, theme, format, comments, and the existing revision thread usually go with it. DocuPuncture writes a small Apps Script you paste and run on the live file.
8
8
 
9
9
  ## The skills
10
10
 
@@ -13,7 +13,7 @@ Copy a folder into Cursor, Claude Code, or anything else that reads `SKILL.md`.
13
13
  | Skill | Surface | Fit |
14
14
  | --- | --- | --- |
15
15
  | [`docupuncture-docs`](skills/docupuncture-docs/SKILL.md) | Google Docs | Comments, tabs, lists, partial styling. |
16
- | [`docupuncture-sheets`](skills/docupuncture-sheets/SKILL.md) | Google Sheets | Grid and ranges. Notes and validation stay. |
16
+ | [`docupuncture-sheets`](skills/docupuncture-sheets/SKILL.md) | Google Sheets | Grid and ranges. Notes and validation stay on cells you do not rewrite. |
17
17
  | [`docupuncture-slides`](skills/docupuncture-slides/SKILL.md) | Google Slides | Text and content. Layout surgery is higher risk. |
18
18
 
19
19
  ## Install
@@ -33,12 +33,13 @@ Site: [docupuncture.dev](https://docupuncture.dev)
33
33
 
34
34
  ## How a run works
35
35
 
36
- 1. The agent reads the live file and confirms the edits.
37
- 2. It writes an Apps Script.
38
- 3. You open the file → Extensions → Apps Script paste → run `applyEdits`.
39
- 4. First run is a dry run (`DRY_RUN = true`). Read the log, flip the flag, run again.
36
+ 1. Name the target file (URL or ID) and the intended changes.
37
+ 2. Give the agent exact current text for anchors, or let a host Drive connector read the file if you already have one. The skill does not grant Google access.
38
+ 3. Review the Apps Script it writes.
39
+ 4. Open the file Extensions Apps Script paste run `applyEdits`.
40
+ 5. First run is a dry run (`DRY_RUN = true`). Read the log, flip the flag, then run again.
40
41
 
41
- The script is safe to re-run. Missing anchors are logged. It never invents a location.
42
+ The script runs as you. Missing or ambiguous anchors are logged. It never invents a location. For the showcased insert and update operations, a repeat run skips work that is already there.
42
43
 
43
44
  ## Why the name
44
45
 
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "docupuncture",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "private": false,
5
5
  "type": "module",
6
- "description": "Insert precise edits without changing the theme, format, comments, or history.",
6
+ "description": "Targeted, reviewable Apps Script edits in an existing Google Doc, Sheet, or Slides file.",
7
7
  "license": "MIT",
8
8
  "author": "Catalyst Forge LLC",
9
9
  "keywords": [
@@ -1,20 +1,20 @@
1
1
  ---
2
2
  name: docupuncture-docs
3
- description: Precise in-place edits to an existing Google Doc via paste-and-run Apps Script. Use when the user wants edits, tweaks, or iterations to a Google Doc (or says "DocuPuncture") — deliver a self-contained script that patches the live doc while preserving all formatting, instead of regenerating a new document. Prefer this over full regeneration for formatted docs. Part of the DocuPuncture family (Docs, Sheets, Slides).
3
+ description: Precise in-place edits to an existing Google Doc via paste-and-run Apps Script. Use when the user wants edits, tweaks, or iterations to a Google Doc (or says "DocuPuncture") — deliver a self-contained script that patches the live doc while preserving surrounding structure for supported edit types, instead of regenerating a new document. Prefer this over full regeneration for formatted docs. Part of the DocuPuncture family (Docs, Sheets, Slides).
4
4
  ---
5
5
 
6
6
  # DocuPuncture for Docs
7
7
 
8
- DocuPuncture = insert precise edits without changing the theme, format, comments, or history. This skill is the **Docs** implementation.
8
+ DocuPuncture writes a reviewable Apps Script that patches an existing Google Doc in place. Surrounding structure is a design goal for supported edit types. An edit adds a revision. This skill is the **Docs** implementation.
9
9
 
10
- Deliver edits as a self-contained Apps Script the user pastes into Extensions → Apps Script and runs, instead of regenerating a new document. In-place patching preserves all formatting (fonts, colors, images, tables, comments, theme) that API imports and full regenerations destroy. Use the name "DocuPuncture" when referring to this workflow with the user.
10
+ Deliver edits as a self-contained Apps Script the user pastes into Extensions → Apps Script and runs, instead of regenerating a new document. In-place patching is designed to keep theme, sibling formatting when copied, and comments on untouched text. It does not guarantee every format or comment anchor survives every edit. Use the name "DocuPuncture" when referring to this workflow with the user.
11
11
 
12
12
 
13
13
  ## When to offer
14
14
 
15
15
  Whenever the user asks for edits, tweaks, or iterations to an existing Google Doc — especially repeated iteration rounds — use AskUserQuestion to offer:
16
16
 
17
- 1. **DocuPuncture: Apps Script patch** (recommended for formatted docs) — I generate a script; you run it once. Formatting preserved. First run requires a one-time authorization prompt.
17
+ 1. **DocuPuncture: Apps Script patch** (recommended for formatted docs) — I generate a script; you run it once. Surrounding structure stays for supported edits. First run requires a one-time authorization prompt.
18
18
  2. **New generated doc** — full regeneration (warn: loses theme/logo/comments).
19
19
  3. **Manual edit checklist** — exact paste-ready text.
20
20
 
@@ -32,15 +32,15 @@ If the set of changes is high-risk, surface this explicitly to the user before w
32
32
 
33
33
  ## Workflow
34
34
 
35
- 1. **Read the doc first** via the Google Drive MCP `read_file_content` (extract the file ID from the URL). Get the EXACT current text anchors must match character-for-character. Confirm with the user which edits to make before writing the script.
35
+ 1. **Identify the file and the exact current text.** Ask for the Doc URL or ID and the intended changes. If the host already has Google Drive MCP `read_file_content` (or another Drive connector) connected, use it. Otherwise require the user to paste the relevant passages. This skill does not grant Google access. Anchors must match character-for-character. Confirm which edits to make before writing the script.
36
36
 
37
37
  2. **Write the script** (rules below) and deliver it in a code block with run instructions:
38
38
  - Open the doc → Extensions → Apps Script
39
39
  - Delete any existing code, paste the script, click Run (▶) on `applyEdits`
40
40
  - First run: authorize when prompted (it runs as you, on your doc)
41
- - Check the Execution log for the ✓/✗ summary
41
+ - Check the Execution log for the ✓/✗ summary. Keep `DRY_RUN = true` until that log matches the intended changes.
42
42
 
43
- 3. **Verify** after the user says they ran it: re-read the doc via MCP and confirm every edit landed.
43
+ 3. **Verify** after the user says they ran it: re-read the doc via MCP if available, or ask the user to confirm each edit.
44
44
 
45
45
  ## Script rules
46
46
 
@@ -52,8 +52,10 @@ If the set of changes is high-risk, surface this explicitly to the user before w
52
52
  - Prefer a short, highly unique literal that is unlikely to change (section titles, distinctive phrases near the edit point, or rare words).
53
53
  - `findText` treats its pattern as a regex, so always route anchor literals through the `escapeRegExp` helper (see skeleton) rather than passing raw strings — a stray `(` or `?` in a section title will silently break the match otherwise.
54
54
  - Use a distinctive substring, never an entire long paragraph (text may span multiple runs).
55
- - For higher robustness on medium/high-risk edits: make each edit function try a primary unique anchor, then a secondary nearby unique context string if the primary is missing. Log which strategy succeeded.
56
- - Never invent a location if no anchor is found.
55
+ - For higher robustness on medium/high-risk edits: make each edit function try a primary unique anchor, then a secondary nearby unique context string if the primary is missing. Log which strategy succeeded. The backup must also be unique.
56
+ - If a targeted edit's anchor matches more than once, log `✗ SKIPPED: <edit name> — anchor is ambiguous` and skip. Do not take the first match.
57
+ - Never invent a location if no unique anchor is found.
58
+ - `replaceText` for a simple swap applies to every match. Use it only when the user asked for every occurrence, and say so in the log.
57
59
 
58
60
  - **Inserting paragraphs**: `body.getChildIndex(anchorElement.getParent())` then `body.insertParagraph(idx + 1, text)`. Copy formatting from a sibling: `newPara.setAttributes(sibling.getAttributes())`.
59
61
 
@@ -65,11 +67,11 @@ If the set of changes is high-risk, surface this explicitly to the user before w
65
67
 
66
68
  - **Replacements**: prefer `body.replaceText(escapedPattern, replacement)` for simple text swaps; for structural changes (splitting a bullet, inserting between existing items), edit the existing element's text and insert new siblings.
67
69
 
68
- - **Idempotent**: before each edit, check whether the target state already exists (`findText` for new content; absence for deletions) and skip with a log message if so. Re-running must never duplicate content.
70
+ - **Repeat-run (showcased insert)**: before a targeted insert or swap, check whether the target state already exists (`findText` for new content, absence for deletions) and skip with a log message if so. The sample `edit_AddIntroParagraph` skips when the new paragraph is already there. Do not claim that guarantee for every script.
69
71
 
70
- - **Fail loudly**: if an anchor isn't found, log `✗ SKIPPED: <edit name> — anchor not found` and continue with the other edits. Never guess a location. End with a summary: `Logger.log('Done: X applied, Y skipped')`.
72
+ - **Fail loudly**: if an anchor isn't found, or a targeted edit's anchor matches more than once, log `✗ SKIPPED: <edit name> — anchor not found` or `anchor is ambiguous` and continue with the other edits. Never guess a location. End with a summary: `Logger.log('Done: X applied, Y skipped')`.
71
73
 
72
- - **Dry run**: include a `DRY_RUN` flag (default `true`) at the top of the script. When on, each edit function locates its anchor and logs `→ WOULD APPLY: <edit name>` without modifying the document. The user reads the log, flips the flag to `false`, and runs again. This is a stronger safety story than "test on a copy" for important documents.
74
+ - **Dry run**: include a `DRY_RUN` flag (default `true`) at the top of the script. When on, each edit function locates its anchor and logs `→ WOULD APPLY: <edit name>` without modifying the document. No write, insert, delete, or `replaceText` in this mode. The user reads the log, flips the flag to `false`, and runs again.
73
75
 
74
76
  - **Structure**: one `applyEdits()` entry function; each edit in its own small named function (or clearly-commented block) whose name matches how you described the edit to the user. See `references/sample-skeleton.js` for a recommended starting template.
75
77
 
@@ -78,6 +80,8 @@ If the set of changes is high-risk, surface this explicitly to the user before w
78
80
  ## Caveats to tell the user (once)
79
81
 
80
82
  - First run shows Google's authorization screen — the script runs entirely as them, on their doc.
83
+ - This skill does not grant Google access. Live reads depend on a host connector or user-provided text.
84
+ - An edit adds a revision. The existing file stays the same artifact.
81
85
  - Apps Script can't delete tabs or edit comments; those stay manual.
82
86
  - Comments are preserved in untouched regions, but an edit that deletes or replaces the exact text a comment is anchored to will orphan that comment. Flag this when an edit overlaps a commented range.
83
87
  - Suggest running on a copy the first time if the doc is important.
@@ -1,12 +1,18 @@
1
1
  /**
2
2
  * DocuPuncture script skeleton (Docs)
3
3
  *
4
+ * Protocol for this sample:
5
+ * - DRY_RUN = true locates anchors and logs. It must not mutate.
6
+ * - A missing or ambiguous targeted anchor is logged and skipped. Do not guess.
7
+ * - Repeat-run for edit_AddIntroParagraph: skip when the new paragraph
8
+ * text is already present. That skip is for this insert, not every script.
9
+ *
4
10
  * Usage:
5
11
  * 1. Open the target Google Doc → Extensions → Apps Script
6
12
  * 2. Replace all code with this script
7
13
  * 3. Click Run ▶ on applyEdits — with DRY_RUN = true it only logs what it
8
14
  * WOULD change, without touching the document
9
- * 4. Read the Execution log; if it looks right, set DRY_RUN = false and run again
15
+ * 4. Read the Execution log. If it looks right, set DRY_RUN = false and run again
10
16
  * 5. Authorize on first run
11
17
  *
12
18
  * Each edit lives in its own named function so the log is human-readable
@@ -34,29 +40,22 @@ function applyEdits() {
34
40
  }
35
41
 
36
42
  /**
37
- * Example edit function.
38
- * Name it after the human description of the change.
39
- * Always idempotent, always fail loudly, always escape anchors.
43
+ * Showcased insert: add a paragraph after a unique section heading.
44
+ * Repeat-run: skip when "Welcome to the new section" is already present.
40
45
  */
41
46
  function edit_AddIntroParagraph(body) {
42
47
  const EDIT_NAME = 'AddIntroParagraph';
43
48
 
44
- // 1. Check if the desired end state already exists (idempotency)
49
+ // 1. Check if the desired end state already exists (repeat-run for this insert)
45
50
  if (body.findText(escapeRegExp('Welcome to the new section'))) {
46
51
  Logger.log(`✓ SKIPPED (already present): ${EDIT_NAME}`);
47
52
  return false;
48
53
  }
49
54
 
50
- // 2. Locate a robust anchor (primary + optional secondary).
51
- // Always route anchor literals through escapeRegExp findText treats
52
- // its pattern as a regex, and a stray '(' or '?' silently breaks the match.
53
- let range = body.findText(escapeRegExp('Section 2: Overview')); // primary unique literal
54
- if (!range) {
55
- range = body.findText(escapeRegExp('Overview of the system')); // secondary nearby context
56
- }
57
-
58
- if (!range) {
59
- Logger.log(`✗ SKIPPED: ${EDIT_NAME} — anchor not found`);
55
+ // 2. Locate a unique anchor (primary, then nearby backup if the primary is missing).
56
+ const found = findUniqueText(body, 'Section 2: Overview', 'Overview of the system');
57
+ if (found.skipReason) {
58
+ Logger.log(`✗ SKIPPED: ${EDIT_NAME} — ${found.skipReason}`);
60
59
  return false;
61
60
  }
62
61
 
@@ -67,7 +66,7 @@ function edit_AddIntroParagraph(body) {
67
66
  }
68
67
 
69
68
  // 4. Perform the edit
70
- const element = range.getElement();
69
+ const element = found.range.getElement();
71
70
  const parent = element.getParent();
72
71
  const idx = body.getChildIndex(parent);
73
72
 
@@ -80,6 +79,43 @@ function edit_AddIntroParagraph(body) {
80
79
  return true;
81
80
  }
82
81
 
82
+ /**
83
+ * Find a unique literal. If `primary` is missing, try `secondary`.
84
+ * Ambiguous (more than one match) is a skip, not a first-match guess.
85
+ */
86
+ function findUniqueText(body, primary, secondary) {
87
+ const primaryResult = uniqueFindText(body, primary);
88
+ if (primaryResult.status === 'unique') {
89
+ return { range: primaryResult.range };
90
+ }
91
+ if (primaryResult.status === 'ambiguous') {
92
+ return { skipReason: 'anchor is ambiguous' };
93
+ }
94
+ if (secondary) {
95
+ const secondaryResult = uniqueFindText(body, secondary);
96
+ if (secondaryResult.status === 'unique') {
97
+ return { range: secondaryResult.range };
98
+ }
99
+ if (secondaryResult.status === 'ambiguous') {
100
+ return { skipReason: 'anchor is ambiguous' };
101
+ }
102
+ }
103
+ return { skipReason: 'anchor not found' };
104
+ }
105
+
106
+ function uniqueFindText(body, literal) {
107
+ const escaped = escapeRegExp(literal);
108
+ const first = body.findText(escaped);
109
+ if (!first) {
110
+ return { status: 'missing' };
111
+ }
112
+ const second = body.findText(escaped, first);
113
+ if (second) {
114
+ return { status: 'ambiguous' };
115
+ }
116
+ return { status: 'unique', range: first };
117
+ }
118
+
83
119
  /**
84
120
  * Helper: safely escape a string for use in findText / replaceText regex.
85
121
  * Use this on EVERY anchor literal — do not pass raw strings.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: docupuncture-sheets
3
- description: Precise in-place edits to an existing Google Sheet via paste-and-run Apps Script. Use when the user wants edits, tweaks, or iterations to a Google Sheet (or says "DocuPuncture" in a sheets context) — deliver a self-contained script that patches the live spreadsheet while preserving formatting, notes, data validation, and structure, instead of regenerating a new sheet.
3
+ description: Precise in-place edits to an existing Google Sheet via paste-and-run Apps Script. Use when the user wants edits, tweaks, or iterations to a Google Sheet (or says "DocuPuncture" in a sheets context) — deliver a self-contained script that patches the live spreadsheet while preserving surrounding structure for supported edit types, instead of regenerating a new sheet.
4
4
  ---
5
5
 
6
6
  # DocuPuncture for Sheets
7
7
 
8
- DocuPuncture applied to Google Sheets: insert precise edits without changing the theme, format, comments, or history. Notes, data validation, charts, and structure stay too. Deliver edits as a self-contained Apps Script the user pastes into Extensions → Apps Script and runs.
8
+ DocuPuncture writes a reviewable Apps Script that patches an existing Google Sheet in place. Surrounding structure is a design goal for supported edit types. An edit adds a revision. Notes, data validation, charts, and structure are designed to stay when you update values on existing cells. Deliver edits as a self-contained Apps Script the user pastes into Extensions → Apps Script and runs.
9
9
 
10
10
  Use the name "DocuPuncture" (or "DocuPuncture on this sheet") when referring to this workflow.
11
11
 
@@ -13,7 +13,7 @@ Use the name "DocuPuncture" (or "DocuPuncture on this sheet") when referring to
13
13
 
14
14
  Whenever the user asks for edits, tweaks, or iterations to an existing Google Sheet — especially repeated rounds — offer:
15
15
 
16
- 1. **DocuPuncture: Apps Script patch** (recommended) — I generate a script; you run it once. Formatting, notes, and validation preserved.
16
+ 1. **DocuPuncture: Apps Script patch** (recommended) — I generate a script; you run it once. Formatting, notes, and validation stay on cells you do not rewrite.
17
17
  2. **New generated sheet** — full regeneration (warn: loses most formatting, notes, validation, charts).
18
18
  3. **Manual edit checklist** — exact values/ranges to change.
19
19
 
@@ -27,28 +27,30 @@ Skip the question if the user already said "DocuPuncture" or established the pre
27
27
 
28
28
  ## Workflow
29
29
 
30
- 1. **Read the sheet first** (via available tools or user-provided ranges/values). Confirm the exact current state and the requested changes.
30
+ 1. **Identify the file and the exact current values.** Ask for the spreadsheet URL or ID and the intended changes. If the host already has a Drive connector, use it. Otherwise require the user to paste the relevant ranges or values. This skill does not grant Google access.
31
31
  2. **Write the script** and deliver it with clear run instructions:
32
32
  - Open the spreadsheet → Extensions → Apps Script
33
33
  - Paste the script, run `applyEdits`
34
34
  - Authorize on first run
35
- - Check the Execution log for the summary
36
- 3. **Verify** after the user runs it by re-checking key cells/ranges if possible.
35
+ - Check the Execution log. Keep `DRY_RUN = true` until that log matches the intended changes.
36
+ 3. **Verify** after the user runs it by re-checking key cells or ranges if a connector is available, or ask the user to confirm.
37
37
 
38
38
  ## Script rules
39
39
 
40
40
  - **Container-bound**: use `SpreadsheetApp.getActiveSpreadsheet()` (and `getActiveSheet()` or named sheets). Prefer this over `openById` for minimal friction.
41
- - **Anchors**: locate targets with `sheet.createTextFinder(text).findNext()` or by stable headers + relative offsets. Prefer unique header values or distinctive cell content as anchors. Escape regex when needed.
42
- - **Preserve formatting**: when updating values, prefer patterns that keep existing formatting (e.g. `setValue` on already-formatted cells, or capture/re-apply RichTextValue / `copyTo` with format options when necessary). Explicitly preserve notes and data validation where the edit should not touch them.
43
- - **Idempotent**: before changing a cell or range, check whether the desired end state already exists and skip if so. Re-running must never create duplicate rows or overwrite correct data unnecessarily.
44
- - **Fail loudly**: if an anchor/header is missing, log `✗ SKIPPED: <edit name> — anchor not found` and continue. Never invent row/column numbers.
45
- - **Dry run**: include a `DRY_RUN` flag (default `true`). When on, each edit locates its anchor and logs `→ WOULD APPLY` without writing anything. User reads the log, flips to `false`, runs again.
41
+ - **Anchors**: locate targets with `sheet.createTextFinder(text).findNext()` or by stable headers + relative offsets. Prefer unique header values or distinctive cell content as anchors. Escape regex when needed. If a targeted edit's finder matches more than once, log `✗ SKIPPED: <edit name> — anchor is ambiguous` and skip. Do not take the first match.
42
+ - **Preserve formatting**: when updating values, prefer patterns that keep existing formatting (e.g. `setValue` on already-formatted cells, or capture/re-apply RichTextValue / `copyTo` with format options when necessary). Explicitly preserve notes and data validation where the edit should not touch them. Notes and validation on a rewritten range are not guaranteed.
43
+ - **Repeat-run (showcased cell update)**: before changing a cell, check whether the desired end state already exists and skip if so. The sample `edit_UpdateStatusColumn` skips when the status cell is already `Completed`. Do not claim that guarantee for every script.
44
+ - **Fail loudly**: if an anchor or header is missing, or a targeted finder matches more than once, log `✗ SKIPPED` with `anchor not found` or `anchor is ambiguous` and continue. Never invent row or column numbers.
45
+ - **Dry run**: include a `DRY_RUN` flag (default `true`). When on, each edit locates its anchor and logs `→ WOULD APPLY` without writing anything. No `setValue`, `setValues`, insert, or delete in this mode. User reads the log, flips to `false`, runs again.
46
46
  - **Structure**: one `applyEdits()` entry point. Each logical change in its own named function that returns success/failure. End with a clear `Logger.log` summary.
47
47
  - **Never** hardcode credentials.
48
48
 
49
49
  ## Caveats to tell the user (once)
50
50
 
51
51
  - First run shows Google’s authorization screen — the script runs as them.
52
+ - This skill does not grant Google access. Live reads depend on a host connector or user-provided ranges.
53
+ - An edit adds a revision. The existing spreadsheet stays the same artifact.
52
54
  - Complex formula dependencies can still break if the structural change is large; prefer small, targeted needles.
53
55
  - Suggest testing on a copy first if the sheet is critical.
54
56
 
@@ -1,9 +1,15 @@
1
1
  /**
2
2
  * DocuPuncture for Sheets – sample skeleton
3
3
  *
4
+ * Protocol for this sample:
5
+ * - DRY_RUN = true locates anchors and logs. It must not mutate.
6
+ * - A missing or ambiguous targeted anchor is logged and skipped. Do not guess.
7
+ * - Repeat-run for edit_UpdateStatusColumn: skip when column E is already
8
+ * Completed. That skip is for this cell update, not every script.
9
+ *
4
10
  * Paste into the target spreadsheet's Extensions → Apps Script, then run
5
- * applyEdits. With DRY_RUN = true it only logs what it WOULD change;
6
- * read the log, set DRY_RUN = false, and run again to apply.
11
+ * applyEdits. With DRY_RUN = true it only logs what it WOULD change.
12
+ * Read the log, set DRY_RUN = false, and run again to apply.
7
13
  */
8
14
 
9
15
  const DRY_RUN = true; // ← set to false to actually apply the edits
@@ -23,8 +29,8 @@ function applyEdits() {
23
29
  }
24
30
 
25
31
  /**
26
- * Example: find a row by unique key and update a specific column.
27
- * Returns true if applied (or would apply in dry run), false if skipped.
32
+ * Showcased update: find a unique key and set its status cell.
33
+ * Repeat-run: skip when that status cell is already Completed.
28
34
  */
29
35
  function edit_UpdateStatusColumn(sheet) {
30
36
  const EDIT_NAME = 'UpdateStatusColumn';
@@ -38,11 +44,15 @@ function edit_UpdateStatusColumn(sheet) {
38
44
  Logger.log(`✗ SKIPPED: ${EDIT_NAME} — anchor not found`);
39
45
  return false;
40
46
  }
47
+ if (finder.findNext()) {
48
+ Logger.log(`✗ SKIPPED: ${EDIT_NAME} — anchor is ambiguous`);
49
+ return false;
50
+ }
41
51
 
42
52
  const row = cell.getRow();
43
53
  const statusCell = sheet.getRange(row, 5); // column E for status, adjust as needed
44
54
 
45
- // 2. Idempotency: skip if already correct
55
+ // 2. Repeat-run for this update: skip if already correct
46
56
  if (statusCell.getValue() === 'Completed') {
47
57
  Logger.log(`✓ SKIPPED (already present): ${EDIT_NAME}`);
48
58
  return false;
@@ -54,7 +64,7 @@ function edit_UpdateStatusColumn(sheet) {
54
64
  return true;
55
65
  }
56
66
 
57
- // 4. Apply the change (formatting, notes, and validation on the cell stay intact)
67
+ // 4. Apply the change (formatting, notes, and validation on this cell stay)
58
68
  statusCell.setValue('Completed');
59
69
  Logger.log(`✓ APPLIED: ${EDIT_NAME} (row ${row})`);
60
70
  return true;
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: docupuncture-slides
3
- description: Precise in-place edits to an existing Google Slides presentation via paste-and-run Apps Script. Use when the user wants edits, tweaks, or iterations to a Google Slides deck (or says "DocuPuncture" in a slides context) — deliver a self-contained script that patches the live presentation while preserving layout, styles, and structure, instead of regenerating a new deck.
3
+ description: Precise in-place edits to an existing Google Slides presentation via paste-and-run Apps Script. Use when the user wants edits, tweaks, or iterations to a Google Slides deck (or says "DocuPuncture" in a slides context) — deliver a self-contained script that patches the live presentation while preserving surrounding structure for supported edit types, instead of regenerating a new deck.
4
4
  ---
5
5
 
6
6
  # DocuPuncture for Slides
7
7
 
8
- DocuPuncture applied to Google Slides: insert precise edits without changing the theme, format, comments, or history. Layout and speaker notes stay too. Deliver edits as a self-contained Apps Script the user pastes into Extensions → Apps Script and runs.
8
+ DocuPuncture writes a reviewable Apps Script that patches an existing Google Slides deck in place. Surrounding structure is a design goal for supported text edits. An edit adds a revision. Theme, speaker notes, and the layout of unmoved objects are designed to stay. Deliver edits as a self-contained Apps Script the user pastes into Extensions → Apps Script and runs.
9
9
 
10
10
  Use the name "DocuPuncture" (or "DocuPuncture on these slides") when referring to this workflow.
11
11
 
@@ -13,7 +13,7 @@ Use the name "DocuPuncture" (or "DocuPuncture on these slides") when referring t
13
13
 
14
14
  Whenever the user asks for edits, tweaks, or iterations to an existing Google Slides presentation — especially repeated rounds — offer:
15
15
 
16
- 1. **DocuPuncture: Apps Script patch** (recommended for content/text changes) — I generate a script; you run it once. Layout and theme preserved.
16
+ 1. **DocuPuncture: Apps Script patch** (recommended for content/text changes) — I generate a script; you run it once. Theme and unmoved layout stay for supported text edits.
17
17
  2. **New generated deck** — full regeneration (warn: loses theme, precise positioning, speaker notes, etc.).
18
18
  3. **Manual edit checklist** — exact text or slide-by-slide changes.
19
19
 
@@ -27,22 +27,22 @@ Skip the question if the user already said "DocuPuncture" or established the pre
27
27
 
28
28
  ## Workflow
29
29
 
30
- 1. **Read / understand the presentation first**. Confirm the exact current text content and the requested changes (slide numbers or unique text anchors help).
30
+ 1. **Identify the file and the exact current text.** Ask for the presentation URL or ID and the intended changes. If the host already has a Drive connector, use it. Otherwise require the user to paste the relevant slide text. This skill does not grant Google access.
31
31
  2. **Write the script** and deliver it with clear run instructions:
32
32
  - Open the presentation → Extensions → Apps Script
33
33
  - Paste the script, run `applyEdits`
34
34
  - Authorize on first run
35
- - Check the Execution log
36
- 3. **Verify** after the user runs it if possible.
35
+ - Check the Execution log. Keep `DRY_RUN = true` until that log matches the intended changes.
36
+ 3. **Verify** after the user runs it if a connector is available, or ask the user to confirm.
37
37
 
38
38
  ## Script rules
39
39
 
40
40
  - **Container-bound**: use `SlidesApp.getActivePresentation()`. Prefer this over opening by ID.
41
- - **Anchors**: locate shapes by their text content (`shape.getText().asString()` or `find`). Prefer unique or distinctive phrases that are unlikely to change. For multi-slide decks, iterate slides and search each page’s shapes.
41
+ - **Anchors**: locate shapes by their text content (`shape.getText().asString()` or `find`). Prefer unique or distinctive phrases that are unlikely to change. For multi-slide decks, iterate slides and search each page’s shapes. If a single-shape edit matches more than one shape, log `✗ SKIPPED: <edit name> — anchor is ambiguous` and skip. Do not take the first match. `replaceAllText` may apply to every match when that is the requested operation.
42
42
  - **Text edits**: prefer `TextRange` methods. **Warning**: `getText().setText()` on a shape with mixed character styling (e.g. one bolded word, an inline color) flattens it to a single run. For shapes with mixed styling, prefer `replaceAllText` on the shape, or targeted insert/delete on the sub-`TextRange` around only the changed span, so untouched runs keep their styling. Full `setText` is fine only when the shape's text is uniformly styled.
43
- - **Idempotent**: check whether the target text or state already exists before changing it. Re-running must not duplicate shapes or text.
44
- - **Fail loudly**: if a text anchor / shape is not found, log `✗ SKIPPED: <edit name> — anchor not found` and continue. Never guess slide indices or object IDs.
45
- - **Dry run**: include a `DRY_RUN` flag (default `true`). When on, each edit locates its anchor/shape and logs `→ WOULD APPLY` without modifying the deck. User reads the log, flips to `false`, runs again.
43
+ - **Repeat-run (showcased title update)**: check whether the target text already exists before changing it. The sample `edit_UpdateTitleOnSlide1` skips when the shape already has the new title. Do not claim that guarantee for every script.
44
+ - **Fail loudly**: if a text anchor or shape is not found, or a single-shape edit matches more than one shape, log `✗ SKIPPED` with `anchor not found` or `anchor is ambiguous` and continue. Never guess slide indices or object IDs.
45
+ - **Dry run**: include a `DRY_RUN` flag (default `true`). When on, each edit locates its anchor or shape and logs `→ WOULD APPLY` without modifying the deck. Do not call `replaceAllText` or `setText` in this mode. Count matches by hand instead. User reads the log, flips to `false`, runs again.
46
46
  - **Structure**: one `applyEdits()` entry point. Each logical change in its own named function. End with a clear summary log.
47
47
  - **Scope note**: v1 focuses on text content and simple shape text updates. Complex layout surgery (moving/resizing many objects) is higher risk and should be flagged.
48
48
  - **Never** hardcode credentials.
@@ -50,6 +50,8 @@ Skip the question if the user already said "DocuPuncture" or established the pre
50
50
  ## Caveats to tell the user (once)
51
51
 
52
52
  - First run shows Google’s authorization screen — the script runs as them.
53
+ - This skill does not grant Google access. Live reads depend on a host connector or user-provided text.
54
+ - An edit adds a revision. The existing deck stays the same artifact.
53
55
  - Heavy layout changes are more fragile than pure text edits; prefer content-focused needles.
54
56
  - Suggest testing on a copy first if the presentation is important.
55
57
 
@@ -1,9 +1,18 @@
1
1
  /**
2
2
  * DocuPuncture for Slides – sample skeleton
3
3
  *
4
+ * Protocol for this sample:
5
+ * - DRY_RUN = true locates anchors and logs. It must not mutate.
6
+ * - A missing or ambiguous single-shape anchor is logged and skipped.
7
+ * Do not guess a slide index or object ID.
8
+ * - Repeat-run for edit_UpdateTitleOnSlide1: skip when the shape already
9
+ * has the new title. That skip is for this title update, not every script.
10
+ * - replaceAllText is a different operation. Multiple matches are intended.
11
+ * Do not call it during a dry run. Count matches by hand instead.
12
+ *
4
13
  * Paste into the target presentation's Extensions → Apps Script, then run
5
- * applyEdits. With DRY_RUN = true it only logs what it WOULD change;
6
- * read the log, set DRY_RUN = false, and run again to apply.
14
+ * applyEdits. With DRY_RUN = true it only logs what it WOULD change.
15
+ * Read the log, set DRY_RUN = false, and run again to apply.
7
16
  */
8
17
 
9
18
  const DRY_RUN = true; // ← set to false to actually apply the edits
@@ -23,7 +32,8 @@ function applyEdits() {
23
32
  }
24
33
 
25
34
  /**
26
- * Example: find a shape containing specific text and update it.
35
+ * Showcased title update: one unique shape, then set its text.
36
+ * Repeat-run: skip when the shape already says New Title Text.
27
37
  *
28
38
  * Styling note: getText().setText() flattens mixed character styling
29
39
  * (e.g. one bolded word) to a single run. It is safe here only because
@@ -36,21 +46,25 @@ function edit_UpdateTitleOnSlide1(slides) {
36
46
  const targetSlide = slides[0]; // or find by content
37
47
 
38
48
  const shapes = targetSlide.getShapes();
39
- let targetShape = null;
49
+ const matches = [];
40
50
 
41
51
  for (let i = 0; i < shapes.length; i++) {
42
52
  const text = shapes[i].getText().asString().trim();
43
53
  if (text.indexOf('Old Title Text') !== -1) {
44
- targetShape = shapes[i];
45
- break;
54
+ matches.push(shapes[i]);
46
55
  }
47
56
  }
48
57
 
49
- if (!targetShape) {
58
+ if (matches.length === 0) {
50
59
  Logger.log(`✗ SKIPPED: ${EDIT_NAME} — anchor not found`);
51
60
  return false;
52
61
  }
62
+ if (matches.length > 1) {
63
+ Logger.log(`✗ SKIPPED: ${EDIT_NAME} — anchor is ambiguous`);
64
+ return false;
65
+ }
53
66
 
67
+ const targetShape = matches[0];
54
68
  const current = targetShape.getText().asString().trim();
55
69
  if (current === 'New Title Text') {
56
70
  Logger.log(`✓ SKIPPED (already present): ${EDIT_NAME}`);
@@ -72,7 +86,8 @@ function edit_UpdateTitleOnSlide1(slides) {
72
86
  * replaceAllText preserves each match's run styling, so it is the
73
87
  * preferred tool for text swaps inside shapes with mixed styling.
74
88
  *
75
- * Note: replaceAllText has no read-only mode, so in a dry run we count
89
+ * Multiple matches are the operation here, not an ambiguity.
90
+ * replaceAllText has no read-only mode, so in a dry run we count
76
91
  * matches manually instead of calling it.
77
92
  */
78
93
  function edit_ReplacePlaceholderAcrossDeck(presentation) {