@open-agreements/open-agreements 0.3.1 → 0.5.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.
Files changed (170) hide show
  1. package/README.md +13 -1
  2. package/content/external/yc-safe-discount/metadata.yaml +1 -1
  3. package/content/external/yc-safe-mfn/metadata.yaml +1 -1
  4. package/content/external/yc-safe-pro-rata-side-letter/metadata.yaml +1 -1
  5. package/content/external/yc-safe-valuation-cap/metadata.yaml +1 -1
  6. package/content/recipes/QUALITY_TRACKER.md +35 -0
  7. package/content/recipes/nvca-certificate-of-incorporation/clean.json +14 -2
  8. package/content/recipes/nvca-certificate-of-incorporation/computed.json +78 -0
  9. package/content/recipes/nvca-certificate-of-incorporation/metadata.yaml +115 -2
  10. package/content/recipes/nvca-certificate-of-incorporation/replacements.json +50 -2
  11. package/content/recipes/nvca-certificate-of-incorporation/selections-roadmap.md +47 -0
  12. package/content/recipes/nvca-indemnification-agreement/metadata.yaml +5 -1
  13. package/content/recipes/nvca-indemnification-agreement/replacements.json +30 -5
  14. package/content/recipes/nvca-investors-rights-agreement/clean.json +9 -1
  15. package/content/recipes/nvca-investors-rights-agreement/metadata.yaml +1 -1
  16. package/content/recipes/nvca-investors-rights-agreement/replacements.json +4 -1
  17. package/content/recipes/nvca-management-rights-letter/metadata.yaml +1 -1
  18. package/content/recipes/nvca-rofr-co-sale-agreement/clean.json +11 -1
  19. package/content/recipes/nvca-rofr-co-sale-agreement/metadata.yaml +5 -1
  20. package/content/recipes/nvca-rofr-co-sale-agreement/replacements.json +62 -3
  21. package/content/recipes/nvca-stock-purchase-agreement/clean.json +8 -3
  22. package/content/recipes/nvca-stock-purchase-agreement/metadata.yaml +6 -1
  23. package/content/recipes/nvca-stock-purchase-agreement/normalize.json +0 -16
  24. package/content/recipes/nvca-stock-purchase-agreement/replacements.json +15 -12
  25. package/content/recipes/nvca-stock-purchase-agreement/selections.json +17 -0
  26. package/content/recipes/nvca-voting-agreement/clean.json +11 -1
  27. package/content/recipes/nvca-voting-agreement/metadata.yaml +1 -1
  28. package/content/recipes/nvca-voting-agreement/replacements.json +2 -1
  29. package/content/templates/bonterms-mutual-nda/metadata.yaml +1 -1
  30. package/content/templates/bonterms-professional-services-agreement/metadata.yaml +1 -1
  31. package/content/templates/closing-checklist/metadata.yaml +1 -1
  32. package/content/templates/closing-checklist/template.docx +0 -0
  33. package/content/templates/common-paper-ai-addendum/metadata.yaml +1 -1
  34. package/content/templates/common-paper-ai-addendum-in-app/metadata.yaml +1 -1
  35. package/content/templates/common-paper-amendment/metadata.yaml +1 -1
  36. package/content/templates/common-paper-business-associate-agreement/metadata.yaml +1 -1
  37. package/content/templates/common-paper-cloud-service-agreement/metadata.yaml +1 -1
  38. package/content/templates/common-paper-csa-click-through/metadata.yaml +1 -1
  39. package/content/templates/common-paper-csa-with-ai/metadata.yaml +1 -1
  40. package/content/templates/common-paper-csa-with-sla/metadata.yaml +1 -1
  41. package/content/templates/common-paper-csa-without-sla/metadata.yaml +1 -1
  42. package/content/templates/common-paper-data-processing-agreement/metadata.yaml +1 -1
  43. package/content/templates/common-paper-design-partner-agreement/metadata.yaml +1 -1
  44. package/content/templates/common-paper-independent-contractor-agreement/metadata.yaml +1 -1
  45. package/content/templates/common-paper-letter-of-intent/metadata.yaml +1 -1
  46. package/content/templates/common-paper-mutual-nda/metadata.yaml +1 -1
  47. package/content/templates/common-paper-one-way-nda/metadata.yaml +1 -1
  48. package/content/templates/common-paper-order-form/metadata.yaml +1 -1
  49. package/content/templates/common-paper-order-form-with-sla/metadata.yaml +1 -1
  50. package/content/templates/common-paper-partnership-agreement/metadata.yaml +1 -1
  51. package/content/templates/common-paper-pilot-agreement/metadata.yaml +1 -1
  52. package/content/templates/common-paper-professional-services-agreement/metadata.yaml +1 -1
  53. package/content/templates/common-paper-software-license-agreement/metadata.yaml +1 -1
  54. package/content/templates/common-paper-statement-of-work/metadata.yaml +1 -1
  55. package/content/templates/common-paper-term-sheet/metadata.yaml +1 -1
  56. package/content/templates/openagreements-employee-ip-inventions-assignment/metadata.yaml +1 -1
  57. package/content/templates/openagreements-employment-confidentiality-acknowledgement/metadata.yaml +1 -1
  58. package/content/templates/openagreements-employment-offer-letter/metadata.yaml +1 -1
  59. package/content/templates/working-group-list/metadata.yaml +1 -1
  60. package/dist/cli/index.d.ts.map +1 -1
  61. package/dist/cli/index.js +8 -4
  62. package/dist/cli/index.js.map +1 -1
  63. package/dist/commands/fill.js +3 -3
  64. package/dist/commands/fill.js.map +1 -1
  65. package/dist/commands/list.js +19 -15
  66. package/dist/commands/list.js.map +1 -1
  67. package/dist/core/checklist/docx-import.d.ts +50 -0
  68. package/dist/core/checklist/docx-import.d.ts.map +1 -0
  69. package/dist/core/checklist/docx-import.js +613 -0
  70. package/dist/core/checklist/docx-import.js.map +1 -0
  71. package/dist/core/checklist/docx-table-helpers.d.ts +33 -0
  72. package/dist/core/checklist/docx-table-helpers.d.ts.map +1 -0
  73. package/dist/core/checklist/docx-table-helpers.js +154 -0
  74. package/dist/core/checklist/docx-table-helpers.js.map +1 -0
  75. package/dist/core/checklist/format-checklist-docx.d.ts.map +1 -1
  76. package/dist/core/checklist/format-checklist-docx.js +37 -88
  77. package/dist/core/checklist/format-checklist-docx.js.map +1 -1
  78. package/dist/core/checklist/index.d.ts +15 -12
  79. package/dist/core/checklist/index.d.ts.map +1 -1
  80. package/dist/core/checklist/index.js +48 -30
  81. package/dist/core/checklist/index.js.map +1 -1
  82. package/dist/core/checklist/status-labels.d.ts +6 -0
  83. package/dist/core/checklist/status-labels.d.ts.map +1 -1
  84. package/dist/core/checklist/status-labels.js +8 -0
  85. package/dist/core/checklist/status-labels.js.map +1 -1
  86. package/dist/core/command-generation/adapters/claude.js +4 -4
  87. package/dist/core/employment/memo.js +3 -3
  88. package/dist/core/engine.js +1 -1
  89. package/dist/core/external/index.js +1 -1
  90. package/dist/core/fill-pipeline.d.ts +3 -3
  91. package/dist/core/fill-pipeline.js +6 -6
  92. package/dist/core/fill-pipeline.js.map +1 -1
  93. package/dist/core/metadata.d.ts +13 -3
  94. package/dist/core/metadata.d.ts.map +1 -1
  95. package/dist/core/metadata.js +19 -11
  96. package/dist/core/metadata.js.map +1 -1
  97. package/dist/core/recipe/bracket-normalizer.d.ts +4 -5
  98. package/dist/core/recipe/bracket-normalizer.d.ts.map +1 -1
  99. package/dist/core/recipe/bracket-normalizer.js +67 -149
  100. package/dist/core/recipe/bracket-normalizer.js.map +1 -1
  101. package/dist/core/recipe/cleaner.d.ts.map +1 -1
  102. package/dist/core/recipe/cleaner.js +91 -7
  103. package/dist/core/recipe/cleaner.js.map +1 -1
  104. package/dist/core/recipe/downloader.d.ts.map +1 -1
  105. package/dist/core/recipe/downloader.js +8 -3
  106. package/dist/core/recipe/downloader.js.map +1 -1
  107. package/dist/core/recipe/index.d.ts +2 -1
  108. package/dist/core/recipe/index.d.ts.map +1 -1
  109. package/dist/core/recipe/index.js +11 -4
  110. package/dist/core/recipe/index.js.map +1 -1
  111. package/dist/core/recipe/patcher.d.ts +9 -5
  112. package/dist/core/recipe/patcher.d.ts.map +1 -1
  113. package/dist/core/recipe/patcher.js +183 -136
  114. package/dist/core/recipe/patcher.js.map +1 -1
  115. package/dist/core/recipe/replacement-keys.d.ts +2 -7
  116. package/dist/core/recipe/replacement-keys.d.ts.map +1 -1
  117. package/dist/core/recipe/replacement-keys.js +3 -20
  118. package/dist/core/recipe/replacement-keys.js.map +1 -1
  119. package/dist/core/recipe/source-drift.js +1 -1
  120. package/dist/core/recipe/source-drift.js.map +1 -1
  121. package/dist/core/recipe/verifier.d.ts +7 -1
  122. package/dist/core/recipe/verifier.d.ts.map +1 -1
  123. package/dist/core/recipe/verifier.js +69 -3
  124. package/dist/core/recipe/verifier.js.map +1 -1
  125. package/dist/core/selector.d.ts +2 -0
  126. package/dist/core/selector.d.ts.map +1 -1
  127. package/dist/core/selector.js +97 -1
  128. package/dist/core/selector.js.map +1 -1
  129. package/dist/core/template-listing.d.ts +3 -1
  130. package/dist/core/template-listing.d.ts.map +1 -1
  131. package/dist/core/template-listing.js +4 -3
  132. package/dist/core/template-listing.js.map +1 -1
  133. package/dist/core/unified-pipeline.d.ts +1 -1
  134. package/dist/core/unified-pipeline.d.ts.map +1 -1
  135. package/dist/core/unified-pipeline.js +21 -3
  136. package/dist/core/unified-pipeline.js.map +1 -1
  137. package/dist/core/validation/external.d.ts.map +1 -1
  138. package/dist/core/validation/external.js +0 -2
  139. package/dist/core/validation/external.js.map +1 -1
  140. package/dist/core/validation/recipe.d.ts.map +1 -1
  141. package/dist/core/validation/recipe.js +0 -2
  142. package/dist/core/validation/recipe.js.map +1 -1
  143. package/dist/core/validation/scan-metadata.d.ts.map +1 -1
  144. package/dist/core/validation/scan-metadata.js +17 -1
  145. package/dist/core/validation/scan-metadata.js.map +1 -1
  146. package/dist/core/validation/template.js +5 -5
  147. package/dist/index.d.ts +1 -1
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +1 -1
  150. package/dist/index.js.map +1 -1
  151. package/package.json +11 -5
  152. package/server.json +31 -0
  153. package/skills/client-email/SKILL.md +143 -0
  154. package/skills/cloud-service-agreement/SKILL.md +8 -99
  155. package/skills/data-privacy-agreement/SKILL.md +10 -97
  156. package/skills/delaware-franchise-tax/SKILL.md +7 -1
  157. package/skills/edit-docx-agreement/SKILL.md +10 -3
  158. package/skills/employment-contract/SKILL.md +10 -95
  159. package/skills/iso-27001-evidence-collection/SKILL.md +7 -1
  160. package/skills/iso-27001-internal-audit/SKILL.md +6 -0
  161. package/skills/nda/SKILL.md +7 -99
  162. package/skills/open-agreements/CONNECTORS.md +1 -1
  163. package/skills/open-agreements/SKILL.md +17 -104
  164. package/skills/recipe-quality-audit/SKILL.md +116 -0
  165. package/skills/safe/SKILL.md +12 -97
  166. package/skills/services-agreement/SKILL.md +8 -99
  167. package/skills/shared/template-filling-execution.md +92 -0
  168. package/skills/soc2-readiness/SKILL.md +7 -1
  169. package/skills/unit-test-philosophy/SKILL.md +12 -1
  170. package/skills/venture-financing/SKILL.md +11 -96
@@ -2,9 +2,14 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { createHash } from 'node:crypto';
5
- const CACHE_ROOT = join(homedir(), '.open-agreements', 'cache');
5
+ // Lazy must be a function so ESM import hoisting doesn't evaluate before
6
+ // api/_shared.ts sets OPEN_AGREEMENTS_CACHE_ROOT for Vercel.
7
+ function getCacheRoot() {
8
+ return process.env['OPEN_AGREEMENTS_CACHE_ROOT']
9
+ ?? join(homedir(), '.open-agreements', 'cache');
10
+ }
6
11
  function getCachePath(recipeId) {
7
- return join(CACHE_ROOT, recipeId, 'source.docx');
12
+ return join(getCacheRoot(), recipeId, 'source.docx');
8
13
  }
9
14
  function verifyHash(buf, expected) {
10
15
  const actual = createHash('sha256').update(buf).digest('hex');
@@ -49,7 +54,7 @@ export async function ensureSourceDocx(recipeId, metadata) {
49
54
  }
50
55
  }
51
56
  // Write to cache
52
- const cacheDir = join(CACHE_ROOT, recipeId);
57
+ const cacheDir = join(getCacheRoot(), recipeId);
53
58
  mkdirSync(cacheDir, { recursive: true });
54
59
  writeFileSync(cachePath, buf);
55
60
  console.log(`Cached: ${cachePath}`);
@@ -1 +1 @@
1
- {"version":3,"file":"downloader.js","sourceRoot":"","sources":["../../../src/core/recipe/downloader.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"downloader.js","sourceRoot":"","sources":["../../../src/core/recipe/downloader.ts"],"names":[],"mappings":"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"}
@@ -7,7 +7,8 @@ export declare function runRecipe(options: RecipeRunOptions): Promise<RecipeRunR
7
7
  export { cleanDocument } from './cleaner.js';
8
8
  export type { CleanResult, CleanOptions } from './cleaner.js';
9
9
  export { patchDocument } from './patcher.js';
10
- export { verifyOutput, normalizeText, extractAllText } from './verifier.js';
10
+ export type { PatchResult } from './patcher.js';
11
+ export { verifyOutput, normalizeText, extractAllText, countFormattingAnomalies } from './verifier.js';
11
12
  export { ensureSourceDocx } from './downloader.js';
12
13
  export { checkRecipeSourceDrift, computeSourceStructureSignature } from './source-drift.js';
13
14
  export { enumerateTextParts, getGeneralTextPartNames } from './ooxml-parts.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/recipe/index.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEpE;;;GAGG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CA8EnF;AAED,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC5E,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,+BAA+B,EAAE,MAAM,mBAAmB,CAAC;AAC5F,OAAO,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC/E,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC/F,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/recipe/index.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEpE;;;GAGG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAqFnF;AAED,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AACtG,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,+BAA+B,EAAE,MAAM,mBAAmB,CAAC;AAC5F,OAAO,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC/E,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC/F,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC"}
@@ -1,6 +1,7 @@
1
- import { readFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { loadRecipeMetadata, loadCleanConfig, loadNormalizeConfig } from '../metadata.js';
4
+ import { loadSelectionsConfig } from '../selector.js';
4
5
  import { resolveRecipeDir } from '../../utils/paths.js';
5
6
  import { verifyOutput } from './verifier.js';
6
7
  import { ensureSourceDocx } from './downloader.js';
@@ -17,6 +18,11 @@ export async function runRecipe(options) {
17
18
  const metadata = loadRecipeMetadata(recipeDir);
18
19
  const cleanConfig = loadCleanConfig(recipeDir);
19
20
  const normalizeConfig = loadNormalizeConfig(recipeDir);
21
+ // Load selectionsConfig if selections.json exists (mirrors engine.ts template path)
22
+ const selectionsPath = join(recipeDir, 'selections.json');
23
+ const selectionsConfig = existsSync(selectionsPath)
24
+ ? loadSelectionsConfig(selectionsPath)
25
+ : undefined;
20
26
  // Resolve input: explicit path or auto-download
21
27
  const inputPath = options.inputPath ?? await ensureSourceDocx(recipeId, metadata);
22
28
  // Load replacements.json
@@ -53,14 +59,15 @@ export async function runRecipe(options) {
53
59
  if (computedOutPath && computedArtifact) {
54
60
  writeComputedArtifact(computedOutPath, computedArtifact);
55
61
  }
56
- const shouldNormalizeBracketArtifacts = options.normalizeBracketArtifacts ?? recipeId === 'nvca-stock-purchase-agreement';
62
+ const shouldNormalizeBracketArtifacts = options.normalizeBracketArtifacts ?? normalizeConfig.paragraph_rules.length > 0;
57
63
  const result = await runFillPipeline({
58
64
  inputPath,
59
65
  outputPath,
60
66
  values: effectiveValues,
61
67
  fields: metadata.fields,
62
- requiredFieldNames: metadata.required_fields,
68
+ priorityFieldNames: metadata.priority_fields,
63
69
  cleanPatch: { cleanConfig, replacements },
70
+ selectionsConfig,
64
71
  postProcess: shouldNormalizeBracketArtifacts
65
72
  ? async (outputDocPath) => {
66
73
  await normalizeBracketArtifacts(outputDocPath, outputDocPath, {
@@ -83,7 +90,7 @@ export async function runRecipe(options) {
83
90
  }
84
91
  export { cleanDocument } from './cleaner.js';
85
92
  export { patchDocument } from './patcher.js';
86
- export { verifyOutput, normalizeText, extractAllText } from './verifier.js';
93
+ export { verifyOutput, normalizeText, extractAllText, countFormattingAnomalies } from './verifier.js';
87
94
  export { ensureSourceDocx } from './downloader.js';
88
95
  export { checkRecipeSourceDrift, computeSourceStructureSignature } from './source-drift.js';
89
96
  export { enumerateTextParts, getGeneralTextPartNames } from './ooxml-parts.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/recipe/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/recipe/index.ts"],"names":[],"mappings":"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"}
@@ -1,15 +1,19 @@
1
1
  import type { Element } from '@xmldom/xmldom';
2
+ export interface PatchResult {
3
+ outputPath: string;
4
+ zeroMatchKeys: string[];
5
+ }
2
6
  /**
3
7
  * Patch a DOCX document by replacing bracketed placeholders with template tags.
4
8
  * Uses a char_map algorithm to handle cross-run replacements where Word splits
5
9
  * placeholder text across multiple XML run elements.
6
10
  *
7
- * Supports three key syntax types:
11
+ * Supports two key syntax types:
8
12
  * - Simple: "search text" → replaces all occurrences
9
- * - Context: "label > placeholder" → replaces placeholder only in matching table rows
10
- * - Nth: "text#N" replaces only the Nth occurrence across the document
13
+ * - Context: "context > placeholder" → in tables, replaces placeholder in rows
14
+ * where label matches; in paragraphs, replaces first placeholder after context
11
15
  *
12
- * Processing order: context keys first, nth keys second, simple keys last.
16
+ * Processing order: context keys first, simple keys last.
13
17
  * This ensures qualified rules claim their targets before simple rules sweep up the rest.
14
18
  */
15
19
  export interface PatchOptions {
@@ -18,7 +22,7 @@ export interface PatchOptions {
18
22
  * overriding any inherited placeholder styling (e.g. gray). */
19
23
  replacementColor?: string;
20
24
  }
21
- export declare function patchDocument(inputPath: string, outputPath: string, replacements: Record<string, string>, options?: PatchOptions): Promise<string>;
25
+ export declare function patchDocument(inputPath: string, outputPath: string, replacements: Record<string, string>, options?: PatchOptions): Promise<PatchResult>;
22
26
  /**
23
27
  * Get the text content of the first table cell (label cell) in the same row
24
28
  * as the paragraph. Returns null if the paragraph is not inside a table cell.
@@ -1 +1 @@
1
- {"version":3,"file":"patcher.d.ts","sourceRoot":"","sources":["../../../src/core/recipe/patcher.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAY,OAAO,EAAQ,MAAM,gBAAgB,CAAC;AAwB9D;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,YAAY;IAC3B;;oEAEgE;IAChE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,aAAa,CACjC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,MAAM,CAAC,CA+HjB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAwB/D;AA0BD,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAO/C;AAuFD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAcvD"}
1
+ {"version":3,"file":"patcher.d.ts","sourceRoot":"","sources":["../../../src/core/recipe/patcher.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAY,OAAO,EAAQ,MAAM,gBAAgB,CAAC;AA4B9D,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAcD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,YAAY;IAC3B;;oEAEgE;IAChE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,aAAa,CACjC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,WAAW,CAAC,CAsItB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAwB/D;AA0BD,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAO/C;AAuFD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAcvD"}
@@ -1,9 +1,20 @@
1
1
  import AdmZip from 'adm-zip';
2
2
  import { writeFileSync } from 'node:fs';
3
3
  import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
4
+ import { replaceParagraphTextRange, getParagraphText, SafeDocxError, } from '@usejunior/docx-core';
4
5
  import { enumerateTextParts, getGeneralTextPartNames } from './ooxml-parts.js';
5
6
  import { parseReplacementKey } from './replacement-keys.js';
6
7
  const W_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
8
+ /**
9
+ * Normalize smart/typographic quotes to ASCII equivalents for matching.
10
+ * Uses the same canonical quote set as verifier's normalizeText().
11
+ * This is a 1-to-1 character mapping so charMap positions remain valid.
12
+ */
13
+ function normalizeQuotes(text) {
14
+ return text
15
+ .replace(/[\u2018\u2019\u2039\u203A]/g, "'")
16
+ .replace(/[\u201C\u201D\u201A\u201E\u00AB\u00BB]/g, '"');
17
+ }
7
18
  /** Maximum replacement iterations per key per paragraph to prevent infinite loops. */
8
19
  const MAX_REPLACEMENTS_PER_KEY = 200;
9
20
  /**
@@ -23,30 +34,48 @@ export async function patchDocument(inputPath, outputPath, replacements, options
23
34
  if (partNames.length === 0) {
24
35
  throw new Error('No OOXML text parts found in DOCX');
25
36
  }
26
- // Parse and classify all keys
37
+ // Parse and classify all keys, normalizing smart quotes
27
38
  const parsedKeys = [];
28
39
  for (const [key, value] of Object.entries(replacements)) {
29
- parsedKeys.push(parseReplacementKey(key, value));
40
+ const parsed = parseReplacementKey(key, value);
41
+ // Normalize smart quotes in search text and context for matching
42
+ if (parsed.type === 'context') {
43
+ parsed.searchText = normalizeQuotes(parsed.searchText);
44
+ parsed.context = normalizeQuotes(parsed.context);
45
+ }
46
+ else {
47
+ parsed.searchText = normalizeQuotes(parsed.searchText);
48
+ }
49
+ parsedKeys.push(parsed);
50
+ }
51
+ // Deduplicate keys that collide after quote normalization
52
+ const seenSearchKeys = new Map();
53
+ const deduplicatedKeys = [];
54
+ for (const pk of parsedKeys) {
55
+ const dedupKey = pk.type === 'context' ? `${pk.context} > ${pk.searchText}` : pk.searchText;
56
+ const existing = seenSearchKeys.get(dedupKey);
57
+ if (existing) {
58
+ if (existing.value !== pk.value) {
59
+ console.warn(`Patcher: quote-normalized key collision for "${dedupKey}" with different values`);
60
+ }
61
+ // Skip duplicate
62
+ }
63
+ else {
64
+ seenSearchKeys.set(dedupKey, pk);
65
+ deduplicatedKeys.push(pk);
66
+ }
30
67
  }
31
68
  // Separate by type and sort
32
- const contextKeys = parsedKeys
69
+ const contextKeys = deduplicatedKeys
33
70
  .filter((k) => k.type === 'context')
34
71
  .sort((a, b) => b.searchText.length - a.searchText.length);
35
- const nthKeys = parsedKeys
36
- .filter((k) => k.type === 'nth')
37
- .sort((a, b) => {
38
- // Group by searchText, then by N ascending
39
- if (a.searchText !== b.searchText)
40
- return b.searchText.length - a.searchText.length;
41
- return a.n - b.n;
42
- });
43
- const simpleKeys = parsedKeys
72
+ const simpleKeys = deduplicatedKeys
44
73
  .filter((k) => k.type === 'simple')
45
74
  .sort((a, b) => b.searchText.length - a.searchText.length);
46
75
  // Track which parts we modify so we can rebuild the zip cleanly
47
76
  const modifiedParts = new Map();
48
- // Occurrence counter for nth keys (shared across all parts, reset per patchDocument call)
49
- const nthCounters = new Map();
77
+ // Collect all pre-patch paragraph text for zero-match detection
78
+ const preMatchTexts = [];
50
79
  for (const partName of partNames) {
51
80
  const entry = zip.getEntry(partName);
52
81
  if (!entry)
@@ -54,53 +83,38 @@ export async function patchDocument(inputPath, outputPath, replacements, options
54
83
  const xml = entry.getData().toString('utf-8');
55
84
  const doc = parser.parseFromString(xml, 'text/xml');
56
85
  const allParagraphs = doc.getElementsByTagNameNS(W_NS, 'p');
86
+ // Collect paragraph text before any replacements for zero-match detection
87
+ for (let i = 0; i < allParagraphs.length; i++) {
88
+ const text = getParagraphText(allParagraphs[i]);
89
+ if (!text)
90
+ continue;
91
+ preMatchTexts.push(normalizeQuotes(text));
92
+ }
57
93
  // Phase 1: Context keys
58
94
  for (const ck of contextKeys) {
59
95
  for (let i = 0; i < allParagraphs.length; i++) {
60
96
  const para = allParagraphs[i];
61
- const runs = getRunElements(para);
62
- if (runs.length === 0)
97
+ const paraText = getParagraphText(para);
98
+ if (!paraText)
63
99
  continue;
64
- const { fullText } = buildCharMap(runs);
65
- if (!fullText.includes(ck.searchText))
100
+ const normalizedText = normalizeQuotes(paraText);
101
+ if (!normalizedText.includes(ck.searchText))
66
102
  continue;
67
103
  const rowContext = getTableRowContext(para);
68
- if (rowContext === null || !rowContext.includes(ck.context))
69
- continue;
70
- replaceInParagraph(para, { [ck.searchText]: ck.value }, [ck.searchText], options?.replacementColor);
71
- }
72
- }
73
- // Phase 2: Nth occurrence keys — group by searchText for a single pass each
74
- const nthGroups = new Map();
75
- for (const nk of nthKeys) {
76
- const group = nthGroups.get(nk.searchText) ?? [];
77
- group.push(nk);
78
- nthGroups.set(nk.searchText, group);
79
- }
80
- for (const [searchText, group] of nthGroups) {
81
- // Build a map from occurrence number → value for this group
82
- const nthMap = new Map();
83
- for (const nk of group) {
84
- nthMap.set(nk.n, nk.value);
85
- }
86
- let counter = nthCounters.get(searchText) ?? 0;
87
- for (let i = 0; i < allParagraphs.length; i++) {
88
- const para = allParagraphs[i];
89
- const runs = getRunElements(para);
90
- if (runs.length === 0)
91
- continue;
92
- const { fullText } = buildCharMap(runs);
93
- if (!fullText.includes(searchText))
94
- continue;
95
- counter++;
96
- const value = nthMap.get(counter);
97
- if (value !== undefined) {
98
- replaceInParagraphOnce(para, searchText, value, options?.replacementColor);
104
+ if (rowContext !== null) {
105
+ const normalizedRowContext = normalizeQuotes(rowContext);
106
+ if (!normalizedRowContext.includes(ck.context))
107
+ continue;
108
+ replaceInParagraph(para, { [ck.searchText]: ck.value }, [ck.searchText], options?.replacementColor);
109
+ }
110
+ else {
111
+ if (!normalizedText.includes(ck.context))
112
+ continue;
113
+ replaceFirstAfterContext(para, ck.searchText, ck.context, ck.value, options?.replacementColor);
99
114
  }
100
115
  }
101
- nthCounters.set(searchText, counter);
102
116
  }
103
- // Phase 3: Simple keys
117
+ // Phase 2: Simple keys
104
118
  if (simpleKeys.length > 0) {
105
119
  const simpleReplacements = {};
106
120
  const simpleSortedKeys = [];
@@ -114,6 +128,15 @@ export async function patchDocument(inputPath, outputPath, replacements, options
114
128
  }
115
129
  modifiedParts.set(partName, Buffer.from(serializer.serializeToString(doc), 'utf-8'));
116
130
  }
131
+ // Compute zero-match keys by checking pre-patch paragraph text
132
+ const preMatchFullText = preMatchTexts.join('\n');
133
+ const zeroMatchKeys = [];
134
+ for (const pk of deduplicatedKeys) {
135
+ const keyLabel = pk.type === 'context' ? `${pk.context} > ${pk.searchText}` : pk.searchText;
136
+ if (!preMatchFullText.includes(pk.searchText)) {
137
+ zeroMatchKeys.push(keyLabel);
138
+ }
139
+ }
117
140
  // Rebuild the zip from scratch using addFile() to avoid adm-zip data
118
141
  // descriptor issues. Some DOCX files use streaming (bit 3) flags which
119
142
  // adm-zip's updateFile/writeZip/toBuffer handle incorrectly.
@@ -123,7 +146,7 @@ export async function patchDocument(inputPath, outputPath, replacements, options
123
146
  outZip.addFile(entry.entryName, data);
124
147
  }
125
148
  writeFileSync(outputPath, outZip.toBuffer());
126
- return outputPath;
149
+ return { outputPath, zeroMatchKeys };
127
150
  }
128
151
  /**
129
152
  * Get the text content of the first table cell (label cell) in the same row
@@ -288,44 +311,16 @@ export function isRunSafeToRemove(run) {
288
311
  return true;
289
312
  }
290
313
  /**
291
- * Compute the length of the common prefix between two strings.
292
- */
293
- function commonPrefixLen(a, b) {
294
- let i = 0;
295
- while (i < a.length && i < b.length && a[i] === b[i])
296
- i++;
297
- return i;
298
- }
299
- /**
300
- * Compute the length of the common suffix between two strings,
301
- * not overlapping with a known common prefix.
302
- */
303
- function commonSuffixLen(a, b, prefixLen) {
304
- let i = 0;
305
- while (i < a.length - prefixLen &&
306
- i < b.length - prefixLen &&
307
- a[a.length - 1 - i] === b[b.length - 1 - i])
308
- i++;
309
- return i;
310
- }
311
- /**
312
- * Perform a single replacement of searchText with value in the paragraph.
313
- * Does NOT loop — replaces only the first match found.
314
- * Used for nth-occurrence keys where the value may contain the searchText.
314
+ * Legacy charMap-based splice: replace `searchTextLength` characters starting at
315
+ * `matchStart` with `value`, cleaning up empty runs afterwards.
316
+ * Used as a fallback when replaceParagraphTextRange throws UNSAFE_CONTAINER_BOUNDARY
317
+ * (e.g. replacements spanning across hyperlinks or SDTs) or UNSUPPORTED_EDIT
318
+ * (e.g. replacement spans field results).
315
319
  */
316
- function replaceInParagraphOnce(para, searchText, value, replacementColor) {
317
- const runs = getRunElements(para);
318
- if (runs.length === 0)
319
- return;
320
- const rebuilt = buildCharMap(runs);
321
- const matchStart = rebuilt.fullText.indexOf(searchText);
322
- if (matchStart === -1)
323
- return;
324
- // Always use full replacement for single-shot mode
325
- const start = matchStart;
326
- const end = matchStart + searchText.length;
327
- const firstEntry = rebuilt.charMap[start];
328
- const lastEntry = rebuilt.charMap[end - 1];
320
+ function replaceAtPositionLegacy(runs, charMap, matchStart, searchTextLength, value, replacementColor) {
321
+ const end = matchStart + searchTextLength;
322
+ const firstEntry = charMap[matchStart];
323
+ const lastEntry = charMap[end - 1];
329
324
  if (firstEntry.runIndex === lastEntry.runIndex) {
330
325
  const runText = getRunText(runs[firstEntry.runIndex]);
331
326
  setRunText(runs[firstEntry.runIndex], runText.slice(0, firstEntry.charOffset) + value + runText.slice(lastEntry.charOffset + 1));
@@ -349,74 +344,126 @@ function replaceInParagraphOnce(para, searchText, value, replacementColor) {
349
344
  }
350
345
  }
351
346
  }
347
+ /**
348
+ * Perform a single replacement of searchText with value in the paragraph.
349
+ * Does NOT loop — replaces only the first match found.
350
+ * Uses docx-core's replaceParagraphTextRange for correct formatting preservation,
351
+ * with a legacy charMap fallback for container-boundary cases (hyperlinks, SDTs).
352
+ */
353
+ function replaceInParagraphOnce(para, searchText, value, replacementColor) {
354
+ const fullText = getParagraphText(para);
355
+ const matchStart = normalizeQuotes(fullText).indexOf(searchText);
356
+ if (matchStart === -1)
357
+ return;
358
+ try {
359
+ replaceParagraphTextRange(para, matchStart, matchStart + searchText.length, replacementColor
360
+ ? [{ text: value, addRunProps: { color: replacementColor } }]
361
+ : value);
362
+ }
363
+ catch (e) {
364
+ if (e instanceof SafeDocxError && (e.code === 'UNSAFE_CONTAINER_BOUNDARY' || e.code === 'UNSUPPORTED_EDIT')) {
365
+ const runs = getRunElements(para);
366
+ const { fullText: legacyText, charMap } = buildCharMap(runs);
367
+ const legacyPos = normalizeQuotes(legacyText).indexOf(searchText);
368
+ if (legacyPos === -1)
369
+ return;
370
+ replaceAtPositionLegacy(runs, charMap, legacyPos, searchText.length, value, replacementColor);
371
+ }
372
+ else {
373
+ throw e;
374
+ }
375
+ }
376
+ }
377
+ /**
378
+ * Replace the first occurrence of searchText that appears AFTER contextText
379
+ * in the same paragraph. Each call is self-contained — no chaining or order dependency.
380
+ * Uses docx-core's replaceParagraphTextRange with legacy charMap fallback.
381
+ */
382
+ function replaceFirstAfterContext(para, searchText, contextText, value, replacementColor) {
383
+ const fullText = getParagraphText(para);
384
+ const normalizedFull = normalizeQuotes(fullText);
385
+ const ctxPos = normalizedFull.indexOf(contextText);
386
+ if (ctxPos === -1)
387
+ return;
388
+ const matchStart = normalizedFull.indexOf(searchText, ctxPos + contextText.length);
389
+ if (matchStart === -1)
390
+ return;
391
+ try {
392
+ replaceParagraphTextRange(para, matchStart, matchStart + searchText.length, replacementColor
393
+ ? [{ text: value, addRunProps: { color: replacementColor } }]
394
+ : value);
395
+ }
396
+ catch (e) {
397
+ if (e instanceof SafeDocxError && (e.code === 'UNSAFE_CONTAINER_BOUNDARY' || e.code === 'UNSUPPORTED_EDIT')) {
398
+ const runs = getRunElements(para);
399
+ const { fullText: legacyText, charMap } = buildCharMap(runs);
400
+ const normalizedLegacy = normalizeQuotes(legacyText);
401
+ const legacyCtxPos = normalizedLegacy.indexOf(contextText);
402
+ if (legacyCtxPos === -1)
403
+ return;
404
+ const legacyPos = normalizedLegacy.indexOf(searchText, legacyCtxPos + contextText.length);
405
+ if (legacyPos === -1)
406
+ return;
407
+ replaceAtPositionLegacy(runs, charMap, legacyPos, searchText.length, value, replacementColor);
408
+ }
409
+ else {
410
+ throw e;
411
+ }
412
+ }
413
+ }
352
414
  function replaceInParagraph(para, replacements, sortedKeys, replacementColor) {
353
- const runs = getRunElements(para);
354
- if (runs.length === 0)
415
+ // Quick check: skip if no keys match
416
+ const initialText = getParagraphText(para);
417
+ if (!initialText)
355
418
  return;
356
- const { fullText } = buildCharMap(runs);
357
- if (!sortedKeys.some((key) => fullText.includes(key)))
419
+ const normalizedInitial = normalizeQuotes(initialText);
420
+ if (!sortedKeys.some((key) => normalizedInitial.includes(key)))
358
421
  return;
359
422
  for (const key of sortedKeys) {
360
- let rebuilt = buildCharMap(runs);
361
423
  let iterations = 0;
362
- while (rebuilt.fullText.includes(key)) {
424
+ let paraText = getParagraphText(para);
425
+ while (normalizeQuotes(paraText).includes(key)) {
363
426
  iterations++;
364
427
  if (iterations > MAX_REPLACEMENTS_PER_KEY) {
365
428
  throw new Error(`Patcher: exceeded ${MAX_REPLACEMENTS_PER_KEY} replacements for key "${key}" ` +
366
429
  `in a single paragraph. This usually means the replacement value contains ` +
367
430
  `the search key, creating an infinite loop.`);
368
431
  }
369
- const prevText = rebuilt.fullText;
370
- const matchStart = rebuilt.fullText.indexOf(key);
432
+ const prevText = paraText;
433
+ const matchStart = normalizeQuotes(paraText).indexOf(key);
371
434
  const replacement = replacements[key];
372
- // Surgical replacement: compute common prefix/suffix between key and value
373
- // so we only modify the differing middle, preserving formatting on context text.
374
- const cpLen = commonPrefixLen(key, replacement);
375
- const csLen = commonSuffixLen(key, replacement, cpLen);
376
- let start;
377
- let end;
378
- let replText;
379
- if (cpLen + csLen >= key.length || cpLen + csLen >= replacement.length) {
380
- // No useful common prefix/suffix — full replacement (current behavior)
381
- start = matchStart;
382
- end = matchStart + key.length;
383
- replText = replacement;
435
+ try {
436
+ replaceParagraphTextRange(para, matchStart, matchStart + key.length, replacementColor
437
+ ? [{ text: replacement, addRunProps: { color: replacementColor } }]
438
+ : replacement);
384
439
  }
385
- else {
386
- // Surgical: only replace the differing middle
387
- start = matchStart + cpLen;
388
- end = matchStart + key.length - csLen;
389
- replText = replacement.slice(cpLen, replacement.length - csLen);
390
- }
391
- const firstEntry = rebuilt.charMap[start];
392
- const lastEntry = rebuilt.charMap[end - 1];
393
- if (firstEntry.runIndex === lastEntry.runIndex) {
394
- const runText = getRunText(runs[firstEntry.runIndex]);
395
- setRunText(runs[firstEntry.runIndex], runText.slice(0, firstEntry.charOffset) + replText + runText.slice(lastEntry.charOffset + 1));
396
- }
397
- else {
398
- const firstRunText = getRunText(runs[firstEntry.runIndex]);
399
- setRunText(runs[firstEntry.runIndex], firstRunText.slice(0, firstEntry.charOffset) + replText);
400
- const lastRunText = getRunText(runs[lastEntry.runIndex]);
401
- setRunText(runs[lastEntry.runIndex], lastRunText.slice(lastEntry.charOffset + 1));
402
- for (let mid = firstEntry.runIndex + 1; mid < lastEntry.runIndex; mid++) {
403
- setRunText(runs[mid], '');
440
+ catch (e) {
441
+ if (e instanceof SafeDocxError && (e.code === 'UNSAFE_CONTAINER_BOUNDARY' || e.code === 'UNSUPPORTED_EDIT')) {
442
+ // Fallback: use legacy charMap splice for this specific match
443
+ const runs = getRunElements(para);
444
+ const { fullText: legacyText, charMap } = buildCharMap(runs);
445
+ const legacyPos = normalizeQuotes(legacyText).indexOf(key);
446
+ if (legacyPos === -1)
447
+ break;
448
+ replaceAtPositionLegacy(runs, charMap, legacyPos, key.length, replacement, replacementColor);
449
+ }
450
+ else {
451
+ throw e;
404
452
  }
405
453
  }
406
- if (replacementColor) {
407
- setRunColor(runs[firstEntry.runIndex], replacementColor);
408
- }
409
- rebuilt = buildCharMap(runs);
454
+ // Re-read paragraph text after DOM modification
455
+ paraText = getParagraphText(para);
410
456
  // Progress guard: if text didn't change, we're stuck
411
- if (rebuilt.fullText === prevText) {
457
+ if (paraText === prevText) {
412
458
  throw new Error(`Patcher: no progress replacing key "${key}" — replacement value may contain the search key.`);
413
459
  }
414
460
  }
415
461
  }
416
462
  // Sweep: remove empty runs that are safe to remove (no drawings, fields, etc.)
417
- for (let i = runs.length - 1; i >= 0; i--) {
418
- if (getRunText(runs[i]) === '' && isRunSafeToRemove(runs[i])) {
419
- runs[i].parentNode?.removeChild(runs[i]);
463
+ const finalRuns = getRunElements(para);
464
+ for (let i = finalRuns.length - 1; i >= 0; i--) {
465
+ if (getRunText(finalRuns[i]) === '' && isRunSafeToRemove(finalRuns[i])) {
466
+ finalRuns[i].parentNode?.removeChild(finalRuns[i]);
420
467
  }
421
468
  }
422
469
  }