open-agreements 0.1.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.
- package/LICENSE +21 -0
- package/README.md +161 -0
- package/bin/open-agreements.js +2 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +102 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/commands/fill.d.ts +7 -0
- package/dist/commands/fill.d.ts.map +1 -0
- package/dist/commands/fill.js +84 -0
- package/dist/commands/fill.js.map +1 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +202 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/recipe.d.ts +21 -0
- package/dist/commands/recipe.d.ts.map +1 -0
- package/dist/commands/recipe.js +71 -0
- package/dist/commands/recipe.js.map +1 -0
- package/dist/commands/scan.d.ts +12 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +122 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/validate.d.ts +6 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +139 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/core/command-generation/adapters/claude.d.ts +11 -0
- package/dist/core/command-generation/adapters/claude.d.ts.map +1 -0
- package/dist/core/command-generation/adapters/claude.js +85 -0
- package/dist/core/command-generation/adapters/claude.js.map +1 -0
- package/dist/core/command-generation/types.d.ts +14 -0
- package/dist/core/command-generation/types.d.ts.map +1 -0
- package/dist/core/command-generation/types.js +2 -0
- package/dist/core/command-generation/types.js.map +1 -0
- package/dist/core/engine.d.ts +13 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +149 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/external/index.d.ts +8 -0
- package/dist/core/external/index.d.ts.map +1 -0
- package/dist/core/external/index.js +92 -0
- package/dist/core/external/index.js.map +1 -0
- package/dist/core/external/types.d.ts +18 -0
- package/dist/core/external/types.d.ts.map +1 -0
- package/dist/core/external/types.js +2 -0
- package/dist/core/external/types.js.map +1 -0
- package/dist/core/fill-pipeline.d.ts +61 -0
- package/dist/core/fill-pipeline.d.ts.map +1 -0
- package/dist/core/fill-pipeline.js +279 -0
- package/dist/core/fill-pipeline.js.map +1 -0
- package/dist/core/fill-utils.d.ts +39 -0
- package/dist/core/fill-utils.d.ts.map +1 -0
- package/dist/core/fill-utils.js +127 -0
- package/dist/core/fill-utils.js.map +1 -0
- package/dist/core/metadata.d.ts +396 -0
- package/dist/core/metadata.d.ts.map +1 -0
- package/dist/core/metadata.js +126 -0
- package/dist/core/metadata.js.map +1 -0
- package/dist/core/recipe/cleaner.d.ts +13 -0
- package/dist/core/recipe/cleaner.d.ts.map +1 -0
- package/dist/core/recipe/cleaner.js +106 -0
- package/dist/core/recipe/cleaner.js.map +1 -0
- package/dist/core/recipe/downloader.d.ts +8 -0
- package/dist/core/recipe/downloader.d.ts.map +1 -0
- package/dist/core/recipe/downloader.js +58 -0
- package/dist/core/recipe/downloader.js.map +1 -0
- package/dist/core/recipe/index.d.ts +14 -0
- package/dist/core/recipe/index.d.ts.map +1 -0
- package/dist/core/recipe/index.js +91 -0
- package/dist/core/recipe/index.js.map +1 -0
- package/dist/core/recipe/ooxml-parts.d.ts +21 -0
- package/dist/core/recipe/ooxml-parts.d.ts.map +1 -0
- package/dist/core/recipe/ooxml-parts.js +33 -0
- package/dist/core/recipe/ooxml-parts.js.map +1 -0
- package/dist/core/recipe/patcher.d.ts +17 -0
- package/dist/core/recipe/patcher.d.ts.map +1 -0
- package/dist/core/recipe/patcher.js +240 -0
- package/dist/core/recipe/patcher.js.map +1 -0
- package/dist/core/recipe/types.d.ts +28 -0
- package/dist/core/recipe/types.d.ts.map +1 -0
- package/dist/core/recipe/types.js +2 -0
- package/dist/core/recipe/types.js.map +1 -0
- package/dist/core/recipe/verifier.d.ts +24 -0
- package/dist/core/recipe/verifier.d.ts.map +1 -0
- package/dist/core/recipe/verifier.js +143 -0
- package/dist/core/recipe/verifier.js.map +1 -0
- package/dist/core/validation/external.d.ts +16 -0
- package/dist/core/validation/external.d.ts.map +1 -0
- package/dist/core/validation/external.js +106 -0
- package/dist/core/validation/external.js.map +1 -0
- package/dist/core/validation/license.d.ts +15 -0
- package/dist/core/validation/license.d.ts.map +1 -0
- package/dist/core/validation/license.js +30 -0
- package/dist/core/validation/license.js.map +1 -0
- package/dist/core/validation/output.d.ts +12 -0
- package/dist/core/validation/output.d.ts.map +1 -0
- package/dist/core/validation/output.js +47 -0
- package/dist/core/validation/output.js.map +1 -0
- package/dist/core/validation/recipe.d.ts +19 -0
- package/dist/core/validation/recipe.d.ts.map +1 -0
- package/dist/core/validation/recipe.js +148 -0
- package/dist/core/validation/recipe.js.map +1 -0
- package/dist/core/validation/template.d.ts +11 -0
- package/dist/core/validation/template.d.ts.map +1 -0
- package/dist/core/validation/template.js +159 -0
- package/dist/core/validation/template.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/paths.d.ts +15 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +43 -0
- package/dist/utils/paths.js.map +1 -0
- package/external/LICENSE +27 -0
- package/external/README.md +38 -0
- package/external/yc-safe-discount/README.md +16 -0
- package/external/yc-safe-discount/clean.json +4 -0
- package/external/yc-safe-discount/metadata.yaml +71 -0
- package/external/yc-safe-discount/replacements.json +13 -0
- package/external/yc-safe-discount/template.docx +0 -0
- package/external/yc-safe-mfn/README.md +16 -0
- package/external/yc-safe-mfn/clean.json +4 -0
- package/external/yc-safe-mfn/metadata.yaml +64 -0
- package/external/yc-safe-mfn/replacements.json +12 -0
- package/external/yc-safe-mfn/template.docx +0 -0
- package/external/yc-safe-pro-rata-side-letter/README.md +16 -0
- package/external/yc-safe-pro-rata-side-letter/clean.json +4 -0
- package/external/yc-safe-pro-rata-side-letter/metadata.yaml +49 -0
- package/external/yc-safe-pro-rata-side-letter/replacements.json +9 -0
- package/external/yc-safe-pro-rata-side-letter/template.docx +0 -0
- package/external/yc-safe-valuation-cap/README.md +16 -0
- package/external/yc-safe-valuation-cap/clean.json +4 -0
- package/external/yc-safe-valuation-cap/metadata.yaml +64 -0
- package/external/yc-safe-valuation-cap/replacements.json +12 -0
- package/external/yc-safe-valuation-cap/template.docx +0 -0
- package/package.json +77 -0
- package/recipes/nvca-certificate-of-incorporation/clean.json +8 -0
- package/recipes/nvca-certificate-of-incorporation/metadata.yaml +43 -0
- package/recipes/nvca-certificate-of-incorporation/replacements.json +9 -0
- package/recipes/nvca-certificate-of-incorporation/schema.json +11 -0
- package/recipes/nvca-indemnification-agreement/clean.json +7 -0
- package/recipes/nvca-indemnification-agreement/metadata.yaml +83 -0
- package/recipes/nvca-indemnification-agreement/replacements.json +17 -0
- package/recipes/nvca-indemnification-agreement/schema.json +19 -0
- package/recipes/nvca-investors-rights-agreement/clean.json +12 -0
- package/recipes/nvca-investors-rights-agreement/metadata.yaml +75 -0
- package/recipes/nvca-investors-rights-agreement/replacements.json +18 -0
- package/recipes/nvca-investors-rights-agreement/schema.json +18 -0
- package/recipes/nvca-management-rights-letter/clean.json +7 -0
- package/recipes/nvca-management-rights-letter/metadata.yaml +50 -0
- package/recipes/nvca-management-rights-letter/replacements.json +11 -0
- package/recipes/nvca-management-rights-letter/schema.json +13 -0
- package/recipes/nvca-rofr-co-sale-agreement/clean.json +7 -0
- package/recipes/nvca-rofr-co-sale-agreement/metadata.yaml +80 -0
- package/recipes/nvca-rofr-co-sale-agreement/replacements.json +17 -0
- package/recipes/nvca-rofr-co-sale-agreement/schema.json +19 -0
- package/recipes/nvca-stock-purchase-agreement/clean.json +10 -0
- package/recipes/nvca-stock-purchase-agreement/metadata.yaml +74 -0
- package/recipes/nvca-stock-purchase-agreement/replacements.json +20 -0
- package/recipes/nvca-stock-purchase-agreement/schema.json +19 -0
- package/recipes/nvca-voting-agreement/README.md +53 -0
- package/recipes/nvca-voting-agreement/clean.json +7 -0
- package/recipes/nvca-voting-agreement/metadata.yaml +70 -0
- package/recipes/nvca-voting-agreement/replacements.json +18 -0
- package/recipes/nvca-voting-agreement/schema.json +28 -0
- package/skills/open-agreements/SKILL.md +166 -0
- package/templates/bonterms-mutual-nda/README.md +27 -0
- package/templates/bonterms-mutual-nda/metadata.yaml +58 -0
- package/templates/bonterms-mutual-nda/template.docx +0 -0
- package/templates/bonterms-professional-services-agreement/README.md +24 -0
- package/templates/bonterms-professional-services-agreement/metadata.yaml +40 -0
- package/templates/bonterms-professional-services-agreement/template.docx +0 -0
- package/templates/common-paper-ai-addendum/README.md +23 -0
- package/templates/common-paper-ai-addendum/metadata.yaml +33 -0
- package/templates/common-paper-ai-addendum/template.docx +0 -0
- package/templates/common-paper-ai-addendum-in-app/README.md +21 -0
- package/templates/common-paper-ai-addendum-in-app/metadata.yaml +23 -0
- package/templates/common-paper-ai-addendum-in-app/template.docx +0 -0
- package/templates/common-paper-amendment/README.md +27 -0
- package/templates/common-paper-amendment/metadata.yaml +53 -0
- package/templates/common-paper-amendment/template.docx +0 -0
- package/templates/common-paper-business-associate-agreement/README.md +29 -0
- package/templates/common-paper-business-associate-agreement/metadata.yaml +63 -0
- package/templates/common-paper-business-associate-agreement/template.docx +0 -0
- package/templates/common-paper-cloud-service-agreement/README.md +32 -0
- package/templates/common-paper-cloud-service-agreement/metadata.yaml +488 -0
- package/templates/common-paper-cloud-service-agreement/template.docx +0 -0
- package/templates/common-paper-csa-click-through/README.md +33 -0
- package/templates/common-paper-csa-click-through/metadata.yaml +83 -0
- package/templates/common-paper-csa-click-through/template.docx +0 -0
- package/templates/common-paper-csa-with-ai/README.md +49 -0
- package/templates/common-paper-csa-with-ai/metadata.yaml +166 -0
- package/templates/common-paper-csa-with-ai/template.docx +0 -0
- package/templates/common-paper-csa-with-sla/README.md +53 -0
- package/templates/common-paper-csa-with-sla/metadata.yaml +185 -0
- package/templates/common-paper-csa-with-sla/template.docx +0 -0
- package/templates/common-paper-csa-without-sla/README.md +47 -0
- package/templates/common-paper-csa-without-sla/metadata.yaml +155 -0
- package/templates/common-paper-csa-without-sla/template.docx +0 -0
- package/templates/common-paper-data-processing-agreement/README.md +46 -0
- package/templates/common-paper-data-processing-agreement/metadata.yaml +149 -0
- package/templates/common-paper-data-processing-agreement/template.docx +0 -0
- package/templates/common-paper-design-partner-agreement/README.md +29 -0
- package/templates/common-paper-design-partner-agreement/metadata.yaml +65 -0
- package/templates/common-paper-design-partner-agreement/template.docx +0 -0
- package/templates/common-paper-independent-contractor-agreement/README.md +27 -0
- package/templates/common-paper-independent-contractor-agreement/metadata.yaml +55 -0
- package/templates/common-paper-independent-contractor-agreement/template.docx +0 -0
- package/templates/common-paper-letter-of-intent/README.md +25 -0
- package/templates/common-paper-letter-of-intent/metadata.yaml +43 -0
- package/templates/common-paper-letter-of-intent/template.docx +0 -0
- package/templates/common-paper-mutual-nda/README.md +29 -0
- package/templates/common-paper-mutual-nda/metadata.yaml +59 -0
- package/templates/common-paper-mutual-nda/template.docx +0 -0
- package/templates/common-paper-one-way-nda/README.md +27 -0
- package/templates/common-paper-one-way-nda/metadata.yaml +60 -0
- package/templates/common-paper-one-way-nda/template.docx +0 -0
- package/templates/common-paper-order-form/README.md +36 -0
- package/templates/common-paper-order-form/metadata.yaml +98 -0
- package/templates/common-paper-order-form/template.docx +0 -0
- package/templates/common-paper-order-form-with-sla/README.md +42 -0
- package/templates/common-paper-order-form-with-sla/metadata.yaml +129 -0
- package/templates/common-paper-order-form-with-sla/template.docx +0 -0
- package/templates/common-paper-partnership-agreement/README.md +34 -0
- package/templates/common-paper-partnership-agreement/metadata.yaml +90 -0
- package/templates/common-paper-partnership-agreement/template.docx +0 -0
- package/templates/common-paper-pilot-agreement/README.md +34 -0
- package/templates/common-paper-pilot-agreement/metadata.yaml +90 -0
- package/templates/common-paper-pilot-agreement/template.docx +0 -0
- package/templates/common-paper-professional-services-agreement/README.md +44 -0
- package/templates/common-paper-professional-services-agreement/metadata.yaml +141 -0
- package/templates/common-paper-professional-services-agreement/template.docx +0 -0
- package/templates/common-paper-software-license-agreement/README.md +18 -0
- package/templates/common-paper-software-license-agreement/metadata.yaml +13 -0
- package/templates/common-paper-software-license-agreement/template.docx +0 -0
- package/templates/common-paper-statement-of-work/README.md +32 -0
- package/templates/common-paper-statement-of-work/metadata.yaml +78 -0
- package/templates/common-paper-statement-of-work/template.docx +0 -0
- package/templates/common-paper-term-sheet/README.md +22 -0
- package/templates/common-paper-term-sheet/metadata.yaml +28 -0
- package/templates/common-paper-term-sheet/template.docx +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import AdmZip from 'adm-zip';
|
|
2
|
+
import { writeFileSync } from 'node:fs';
|
|
3
|
+
import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
|
|
4
|
+
import { enumerateTextParts, getGeneralTextPartNames } from './ooxml-parts.js';
|
|
5
|
+
const W_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
|
|
6
|
+
/** Maximum replacement iterations per key per paragraph to prevent infinite loops. */
|
|
7
|
+
const MAX_REPLACEMENTS_PER_KEY = 200;
|
|
8
|
+
/**
|
|
9
|
+
* Element local names that indicate a run is NOT safe to remove even if its text is empty.
|
|
10
|
+
* These are non-text children that carry visual or structural meaning.
|
|
11
|
+
*/
|
|
12
|
+
const UNSAFE_RUN_CHILDREN = new Set([
|
|
13
|
+
'drawing', 'pict', 'object', 'fldChar', 'instrText',
|
|
14
|
+
'br', 'cr', 'tab', 'footnoteReference', 'endnoteReference',
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* Patch a DOCX document by replacing bracketed placeholders with template tags.
|
|
18
|
+
* Uses a char_map algorithm to handle cross-run replacements where Word splits
|
|
19
|
+
* placeholder text across multiple XML run elements.
|
|
20
|
+
*
|
|
21
|
+
* Processes all general OOXML text parts (document, headers, footers, endnotes).
|
|
22
|
+
*/
|
|
23
|
+
export async function patchDocument(inputPath, outputPath, replacements) {
|
|
24
|
+
const zip = new AdmZip(inputPath);
|
|
25
|
+
const parser = new DOMParser();
|
|
26
|
+
const serializer = new XMLSerializer();
|
|
27
|
+
const parts = enumerateTextParts(zip);
|
|
28
|
+
const partNames = getGeneralTextPartNames(parts);
|
|
29
|
+
if (partNames.length === 0) {
|
|
30
|
+
throw new Error('No OOXML text parts found in DOCX');
|
|
31
|
+
}
|
|
32
|
+
// Sort keys longest-first to prevent partial matches
|
|
33
|
+
const sortedKeys = Object.keys(replacements).sort((a, b) => b.length - a.length);
|
|
34
|
+
// Track which parts we modify so we can rebuild the zip cleanly
|
|
35
|
+
const modifiedParts = new Map();
|
|
36
|
+
for (const partName of partNames) {
|
|
37
|
+
const entry = zip.getEntry(partName);
|
|
38
|
+
if (!entry)
|
|
39
|
+
continue;
|
|
40
|
+
const xml = entry.getData().toString('utf-8');
|
|
41
|
+
const doc = parser.parseFromString(xml, 'text/xml');
|
|
42
|
+
// Process all paragraphs (body + tables)
|
|
43
|
+
const allParagraphs = doc.getElementsByTagNameNS(W_NS, 'p');
|
|
44
|
+
for (let i = 0; i < allParagraphs.length; i++) {
|
|
45
|
+
replaceInParagraph(allParagraphs[i], replacements, sortedKeys);
|
|
46
|
+
}
|
|
47
|
+
modifiedParts.set(partName, Buffer.from(serializer.serializeToString(doc), 'utf-8'));
|
|
48
|
+
}
|
|
49
|
+
// Rebuild the zip from scratch using addFile() to avoid adm-zip data
|
|
50
|
+
// descriptor issues. Some DOCX files use streaming (bit 3) flags which
|
|
51
|
+
// adm-zip's updateFile/writeZip/toBuffer handle incorrectly.
|
|
52
|
+
const outZip = new AdmZip();
|
|
53
|
+
for (const entry of zip.getEntries()) {
|
|
54
|
+
const data = modifiedParts.get(entry.entryName) ?? entry.getData();
|
|
55
|
+
outZip.addFile(entry.entryName, data);
|
|
56
|
+
}
|
|
57
|
+
writeFileSync(outputPath, outZip.toBuffer());
|
|
58
|
+
return outputPath;
|
|
59
|
+
}
|
|
60
|
+
function buildCharMap(runs) {
|
|
61
|
+
const charMap = [];
|
|
62
|
+
let fullText = '';
|
|
63
|
+
for (let runIndex = 0; runIndex < runs.length; runIndex++) {
|
|
64
|
+
const text = getRunText(runs[runIndex]);
|
|
65
|
+
for (let offset = 0; offset < text.length; offset++) {
|
|
66
|
+
charMap.push({ runIndex, charOffset: offset });
|
|
67
|
+
}
|
|
68
|
+
fullText += text;
|
|
69
|
+
}
|
|
70
|
+
return { fullText, charMap };
|
|
71
|
+
}
|
|
72
|
+
export function getRunText(run) {
|
|
73
|
+
const tElements = run.getElementsByTagNameNS(W_NS, 't');
|
|
74
|
+
let text = '';
|
|
75
|
+
for (let i = 0; i < tElements.length; i++) {
|
|
76
|
+
text += tElements[i].textContent ?? '';
|
|
77
|
+
}
|
|
78
|
+
return text;
|
|
79
|
+
}
|
|
80
|
+
function setRunText(run, text) {
|
|
81
|
+
const tElements = run.getElementsByTagNameNS(W_NS, 't');
|
|
82
|
+
if (tElements.length === 0) {
|
|
83
|
+
const doc = run.ownerDocument;
|
|
84
|
+
const t = doc.createElementNS(W_NS, 'w:t');
|
|
85
|
+
t.setAttribute('xml:space', 'preserve');
|
|
86
|
+
t.textContent = text;
|
|
87
|
+
run.appendChild(t);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
tElements[0].textContent = text;
|
|
91
|
+
if (text.startsWith(' ') || text.endsWith(' ')) {
|
|
92
|
+
tElements[0].setAttribute('xml:space', 'preserve');
|
|
93
|
+
}
|
|
94
|
+
for (let i = 1; i < tElements.length; i++) {
|
|
95
|
+
tElements[i].textContent = '';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Collect all <w:r> elements in document order within a paragraph, including
|
|
100
|
+
* runs nested inside wrapper elements like <w:hyperlink>, <w:ins>, <w:del>, etc.
|
|
101
|
+
*/
|
|
102
|
+
function getRunElements(para) {
|
|
103
|
+
const runs = [];
|
|
104
|
+
collectRunsRecursive(para, runs);
|
|
105
|
+
return runs;
|
|
106
|
+
}
|
|
107
|
+
function collectRunsRecursive(node, runs) {
|
|
108
|
+
const children = node.childNodes;
|
|
109
|
+
if (!children)
|
|
110
|
+
return;
|
|
111
|
+
for (let i = 0; i < children.length; i++) {
|
|
112
|
+
const child = children[i];
|
|
113
|
+
if (child.nodeType !== 1)
|
|
114
|
+
continue;
|
|
115
|
+
const el = child;
|
|
116
|
+
if (el.localName === 'r' && el.namespaceURI === W_NS) {
|
|
117
|
+
runs.push(el);
|
|
118
|
+
}
|
|
119
|
+
else if (el.localName !== 'p') {
|
|
120
|
+
// Recurse into wrapper elements (hyperlink, ins, del, smartTag, etc.)
|
|
121
|
+
// but don't recurse into nested paragraphs
|
|
122
|
+
collectRunsRecursive(el, runs);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Check if a run is safe to remove when its text is empty.
|
|
128
|
+
* Returns true only if all element children are <w:rPr> or <w:t>.
|
|
129
|
+
* Returns false if the run contains drawings, fields, breaks, or other non-text elements.
|
|
130
|
+
*/
|
|
131
|
+
export function isRunSafeToRemove(run) {
|
|
132
|
+
const children = run.childNodes;
|
|
133
|
+
if (!children)
|
|
134
|
+
return true;
|
|
135
|
+
for (let i = 0; i < children.length; i++) {
|
|
136
|
+
const child = children[i];
|
|
137
|
+
if (child.nodeType !== 1)
|
|
138
|
+
continue; // skip text/comment nodes
|
|
139
|
+
const name = child.localName;
|
|
140
|
+
if (!name)
|
|
141
|
+
return false;
|
|
142
|
+
if (name === 'rPr' || name === 't')
|
|
143
|
+
continue;
|
|
144
|
+
if (UNSAFE_RUN_CHILDREN.has(name))
|
|
145
|
+
return false;
|
|
146
|
+
// Any other element child we don't recognize — not safe
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Compute the length of the common prefix between two strings.
|
|
153
|
+
*/
|
|
154
|
+
function commonPrefixLen(a, b) {
|
|
155
|
+
let i = 0;
|
|
156
|
+
while (i < a.length && i < b.length && a[i] === b[i])
|
|
157
|
+
i++;
|
|
158
|
+
return i;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Compute the length of the common suffix between two strings,
|
|
162
|
+
* not overlapping with a known common prefix.
|
|
163
|
+
*/
|
|
164
|
+
function commonSuffixLen(a, b, prefixLen) {
|
|
165
|
+
let i = 0;
|
|
166
|
+
while (i < a.length - prefixLen &&
|
|
167
|
+
i < b.length - prefixLen &&
|
|
168
|
+
a[a.length - 1 - i] === b[b.length - 1 - i])
|
|
169
|
+
i++;
|
|
170
|
+
return i;
|
|
171
|
+
}
|
|
172
|
+
function replaceInParagraph(para, replacements, sortedKeys) {
|
|
173
|
+
const runs = getRunElements(para);
|
|
174
|
+
if (runs.length === 0)
|
|
175
|
+
return;
|
|
176
|
+
const { fullText } = buildCharMap(runs);
|
|
177
|
+
if (!sortedKeys.some((key) => fullText.includes(key)))
|
|
178
|
+
return;
|
|
179
|
+
for (const key of sortedKeys) {
|
|
180
|
+
let rebuilt = buildCharMap(runs);
|
|
181
|
+
let iterations = 0;
|
|
182
|
+
while (rebuilt.fullText.includes(key)) {
|
|
183
|
+
iterations++;
|
|
184
|
+
if (iterations > MAX_REPLACEMENTS_PER_KEY) {
|
|
185
|
+
throw new Error(`Patcher: exceeded ${MAX_REPLACEMENTS_PER_KEY} replacements for key "${key}" ` +
|
|
186
|
+
`in a single paragraph. This usually means the replacement value contains ` +
|
|
187
|
+
`the search key, creating an infinite loop.`);
|
|
188
|
+
}
|
|
189
|
+
const prevText = rebuilt.fullText;
|
|
190
|
+
const matchStart = rebuilt.fullText.indexOf(key);
|
|
191
|
+
const replacement = replacements[key];
|
|
192
|
+
// Surgical replacement: compute common prefix/suffix between key and value
|
|
193
|
+
// so we only modify the differing middle, preserving formatting on context text.
|
|
194
|
+
const cpLen = commonPrefixLen(key, replacement);
|
|
195
|
+
const csLen = commonSuffixLen(key, replacement, cpLen);
|
|
196
|
+
let start;
|
|
197
|
+
let end;
|
|
198
|
+
let replText;
|
|
199
|
+
if (cpLen + csLen >= key.length || cpLen + csLen >= replacement.length) {
|
|
200
|
+
// No useful common prefix/suffix — full replacement (current behavior)
|
|
201
|
+
start = matchStart;
|
|
202
|
+
end = matchStart + key.length;
|
|
203
|
+
replText = replacement;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// Surgical: only replace the differing middle
|
|
207
|
+
start = matchStart + cpLen;
|
|
208
|
+
end = matchStart + key.length - csLen;
|
|
209
|
+
replText = replacement.slice(cpLen, replacement.length - csLen);
|
|
210
|
+
}
|
|
211
|
+
const firstEntry = rebuilt.charMap[start];
|
|
212
|
+
const lastEntry = rebuilt.charMap[end - 1];
|
|
213
|
+
if (firstEntry.runIndex === lastEntry.runIndex) {
|
|
214
|
+
const runText = getRunText(runs[firstEntry.runIndex]);
|
|
215
|
+
setRunText(runs[firstEntry.runIndex], runText.slice(0, firstEntry.charOffset) + replText + runText.slice(lastEntry.charOffset + 1));
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
const firstRunText = getRunText(runs[firstEntry.runIndex]);
|
|
219
|
+
setRunText(runs[firstEntry.runIndex], firstRunText.slice(0, firstEntry.charOffset) + replText);
|
|
220
|
+
const lastRunText = getRunText(runs[lastEntry.runIndex]);
|
|
221
|
+
setRunText(runs[lastEntry.runIndex], lastRunText.slice(lastEntry.charOffset + 1));
|
|
222
|
+
for (let mid = firstEntry.runIndex + 1; mid < lastEntry.runIndex; mid++) {
|
|
223
|
+
setRunText(runs[mid], '');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
rebuilt = buildCharMap(runs);
|
|
227
|
+
// Progress guard: if text didn't change, we're stuck
|
|
228
|
+
if (rebuilt.fullText === prevText) {
|
|
229
|
+
throw new Error(`Patcher: no progress replacing key "${key}" — replacement value may contain the search key.`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Sweep: remove empty runs that are safe to remove (no drawings, fields, etc.)
|
|
234
|
+
for (let i = runs.length - 1; i >= 0; i--) {
|
|
235
|
+
if (getRunText(runs[i]) === '' && isRunSafeToRemove(runs[i])) {
|
|
236
|
+
runs[i].parentNode?.removeChild(runs[i]);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=patcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patcher.js","sourceRoot":"","sources":["../../../src/core/recipe/patcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { RecipeMetadata } from '../metadata.js';
|
|
2
|
+
export interface RecipeRunOptions {
|
|
3
|
+
recipeId: string;
|
|
4
|
+
inputPath?: string;
|
|
5
|
+
outputPath: string;
|
|
6
|
+
values: Record<string, string>;
|
|
7
|
+
keepIntermediate?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface RecipeRunResult {
|
|
10
|
+
outputPath: string;
|
|
11
|
+
metadata: RecipeMetadata;
|
|
12
|
+
fieldsUsed: string[];
|
|
13
|
+
stages: {
|
|
14
|
+
clean: string;
|
|
15
|
+
patch: string;
|
|
16
|
+
fill: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface VerifyResult {
|
|
20
|
+
passed: boolean;
|
|
21
|
+
checks: VerifyCheck[];
|
|
22
|
+
}
|
|
23
|
+
export interface VerifyCheck {
|
|
24
|
+
name: string;
|
|
25
|
+
passed: boolean;
|
|
26
|
+
details?: string;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/recipe/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,cAAc,CAAC;IACzB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,EAAE;QACN,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/recipe/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { VerifyResult } from './types.js';
|
|
2
|
+
import type { CleanConfig } from '../metadata.js';
|
|
3
|
+
/**
|
|
4
|
+
* Normalize text for value comparison:
|
|
5
|
+
* - Convert non-breaking spaces to regular spaces
|
|
6
|
+
* - Normalize smart quotes to straight quotes
|
|
7
|
+
* - Collapse runs of spaces/tabs to single space (preserve newlines)
|
|
8
|
+
* - Trim
|
|
9
|
+
*/
|
|
10
|
+
export declare function normalizeText(text: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Verify a filled recipe output DOCX:
|
|
13
|
+
* - All context values appear in the document text
|
|
14
|
+
* - No unrendered {template_tags} remain
|
|
15
|
+
* - No leftover [bracketed placeholders] from the replacement map remain
|
|
16
|
+
* - Footnotes removed (if clean config specified)
|
|
17
|
+
* - Drafting note paragraphs removed (if clean config specified)
|
|
18
|
+
*/
|
|
19
|
+
export declare function verifyOutput(outputPath: string, values: Record<string, string>, replacements: Record<string, string>, cleanConfig?: CleanConfig): Promise<VerifyResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Extract all text from general OOXML text parts (document, headers, footers, endnotes).
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractAllText(docxPath: string): string;
|
|
24
|
+
//# sourceMappingURL=verifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../../src/core/recipe/verifier.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,YAAY,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAKlD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAWlD;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,WAAW,CAAC,EAAE,WAAW,GACxB,OAAO,CAAC,YAAY,CAAC,CA6EvB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA8BvD"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import AdmZip from 'adm-zip';
|
|
2
|
+
import { DOMParser } from '@xmldom/xmldom';
|
|
3
|
+
import { enumerateTextParts, getGeneralTextPartNames } from './ooxml-parts.js';
|
|
4
|
+
const W_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
|
|
5
|
+
/**
|
|
6
|
+
* Normalize text for value comparison:
|
|
7
|
+
* - Convert non-breaking spaces to regular spaces
|
|
8
|
+
* - Normalize smart quotes to straight quotes
|
|
9
|
+
* - Collapse runs of spaces/tabs to single space (preserve newlines)
|
|
10
|
+
* - Trim
|
|
11
|
+
*/
|
|
12
|
+
export function normalizeText(text) {
|
|
13
|
+
return text
|
|
14
|
+
// Non-breaking spaces
|
|
15
|
+
.replace(/[\u00A0\u2007\u202F]/g, ' ')
|
|
16
|
+
// Smart single quotes → straight
|
|
17
|
+
.replace(/[\u2018\u2019\u2039\u203A]/g, "'")
|
|
18
|
+
// Smart double quotes → straight
|
|
19
|
+
.replace(/[\u201C\u201D\u201A\u201E\u00AB\u00BB]/g, '"')
|
|
20
|
+
// Collapse horizontal whitespace (spaces/tabs) to single space, preserve newlines
|
|
21
|
+
.replace(/[^\S\n]+/g, ' ')
|
|
22
|
+
.trim();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Verify a filled recipe output DOCX:
|
|
26
|
+
* - All context values appear in the document text
|
|
27
|
+
* - No unrendered {template_tags} remain
|
|
28
|
+
* - No leftover [bracketed placeholders] from the replacement map remain
|
|
29
|
+
* - Footnotes removed (if clean config specified)
|
|
30
|
+
* - Drafting note paragraphs removed (if clean config specified)
|
|
31
|
+
*/
|
|
32
|
+
export async function verifyOutput(outputPath, values, replacements, cleanConfig) {
|
|
33
|
+
const checks = [];
|
|
34
|
+
const rawFullText = extractAllText(outputPath);
|
|
35
|
+
const normalizedFullText = normalizeText(rawFullText);
|
|
36
|
+
const xml = extractDocumentXml(outputPath);
|
|
37
|
+
// Check 1: All context values present (with normalization)
|
|
38
|
+
const missingValues = [];
|
|
39
|
+
for (const [key, value] of Object.entries(values)) {
|
|
40
|
+
if (!value || !value.trim())
|
|
41
|
+
continue; // skip empty/whitespace-only values
|
|
42
|
+
const normalizedValue = normalizeText(value);
|
|
43
|
+
if (!normalizedFullText.includes(normalizedValue)) {
|
|
44
|
+
missingValues.push(`${key}="${value}"`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
checks.push({
|
|
48
|
+
name: 'Context values present',
|
|
49
|
+
passed: missingValues.length === 0,
|
|
50
|
+
details: missingValues.length > 0 ? `Missing: ${missingValues.join(', ')}` : undefined,
|
|
51
|
+
});
|
|
52
|
+
// Check 2: No unrendered {template_tags}
|
|
53
|
+
const unrenderedTags = rawFullText.match(/\{[a-z_][a-z0-9_]*\}/gi) ?? [];
|
|
54
|
+
checks.push({
|
|
55
|
+
name: 'No unrendered template tags',
|
|
56
|
+
passed: unrenderedTags.length === 0,
|
|
57
|
+
details: unrenderedTags.length > 0 ? `Found: ${unrenderedTags.join(', ')}` : undefined,
|
|
58
|
+
});
|
|
59
|
+
// Check 3: No leftover [bracketed placeholders] from replacement map
|
|
60
|
+
const replacementKeys = Object.keys(replacements);
|
|
61
|
+
const leftoverBrackets = replacementKeys.filter((key) => rawFullText.includes(key));
|
|
62
|
+
checks.push({
|
|
63
|
+
name: 'No leftover source placeholders',
|
|
64
|
+
passed: leftoverBrackets.length === 0,
|
|
65
|
+
details: leftoverBrackets.length > 0 ? `Found: ${leftoverBrackets.join(', ')}` : undefined,
|
|
66
|
+
});
|
|
67
|
+
// Check 4: No footnote references (if removeFootnotes was set)
|
|
68
|
+
if (cleanConfig?.removeFootnotes) {
|
|
69
|
+
const footnoteRefs = (xml.match(/footnoteReference/g) ?? []).length;
|
|
70
|
+
checks.push({
|
|
71
|
+
name: 'Footnotes removed',
|
|
72
|
+
passed: footnoteRefs === 0,
|
|
73
|
+
details: footnoteRefs > 0 ? `${footnoteRefs} footnote reference(s) remain` : undefined,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// Check 5: No double dollar signs ($$ or $ $)
|
|
77
|
+
// This catches cases where the template already has a $ before a placeholder
|
|
78
|
+
// and the user also included a $ in their value (e.g. "$1,000,000")
|
|
79
|
+
const doubleDollarPattern = /\$[\s\u00A0\t]*\$/;
|
|
80
|
+
const doubleDollarLines = rawFullText.split('\n').filter((line) => doubleDollarPattern.test(line));
|
|
81
|
+
checks.push({
|
|
82
|
+
name: 'No double dollar signs',
|
|
83
|
+
passed: doubleDollarLines.length === 0,
|
|
84
|
+
details: doubleDollarLines.length > 0
|
|
85
|
+
? `Found ${doubleDollarLines.length} occurrence(s): "${doubleDollarLines[0].trim().slice(0, 80)}"`
|
|
86
|
+
: undefined,
|
|
87
|
+
});
|
|
88
|
+
// Check 6: No drafting note paragraphs (if patterns were set)
|
|
89
|
+
if (cleanConfig?.removeParagraphPatterns && cleanConfig.removeParagraphPatterns.length > 0) {
|
|
90
|
+
const regexes = cleanConfig.removeParagraphPatterns.map((p) => new RegExp(p, 'i'));
|
|
91
|
+
const lines = rawFullText.split('\n');
|
|
92
|
+
const matchingLines = lines.filter((line) => regexes.some((r) => r.test(line.trim())));
|
|
93
|
+
checks.push({
|
|
94
|
+
name: 'Drafting notes removed',
|
|
95
|
+
passed: matchingLines.length === 0,
|
|
96
|
+
details: matchingLines.length > 0 ? `Found: ${matchingLines[0].trim().slice(0, 80)}...` : undefined,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
passed: checks.every((c) => c.passed),
|
|
101
|
+
checks,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Extract all text from general OOXML text parts (document, headers, footers, endnotes).
|
|
106
|
+
*/
|
|
107
|
+
export function extractAllText(docxPath) {
|
|
108
|
+
const zip = new AdmZip(docxPath);
|
|
109
|
+
const parser = new DOMParser();
|
|
110
|
+
const parts = enumerateTextParts(zip);
|
|
111
|
+
const partNames = getGeneralTextPartNames(parts);
|
|
112
|
+
const allParagraphs = [];
|
|
113
|
+
for (const partName of partNames) {
|
|
114
|
+
const entry = zip.getEntry(partName);
|
|
115
|
+
if (!entry)
|
|
116
|
+
continue;
|
|
117
|
+
const xml = entry.getData().toString('utf-8');
|
|
118
|
+
const doc = parser.parseFromString(xml, 'text/xml');
|
|
119
|
+
const paras = doc.getElementsByTagNameNS(W_NS, 'p');
|
|
120
|
+
for (let i = 0; i < paras.length; i++) {
|
|
121
|
+
const tElements = paras[i].getElementsByTagNameNS(W_NS, 't');
|
|
122
|
+
const textParts = [];
|
|
123
|
+
for (let j = 0; j < tElements.length; j++) {
|
|
124
|
+
textParts.push(tElements[j].textContent ?? '');
|
|
125
|
+
}
|
|
126
|
+
if (textParts.length > 0) {
|
|
127
|
+
allParagraphs.push(textParts.join(''));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return allParagraphs.join('\n');
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Extract raw XML from word/document.xml only (for footnote ref counting).
|
|
135
|
+
*/
|
|
136
|
+
function extractDocumentXml(docxPath) {
|
|
137
|
+
const zip = new AdmZip(docxPath);
|
|
138
|
+
const entry = zip.getEntry('word/document.xml');
|
|
139
|
+
if (!entry)
|
|
140
|
+
return '';
|
|
141
|
+
return entry.getData().toString('utf-8');
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=verifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verifier.js","sourceRoot":"","sources":["../../../src/core/recipe/verifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface ExternalValidationResult {
|
|
2
|
+
externalId: string;
|
|
3
|
+
valid: boolean;
|
|
4
|
+
errors: string[];
|
|
5
|
+
warnings: string[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Validate an external template directory:
|
|
9
|
+
* - metadata.yaml validates against ExternalMetadataSchema
|
|
10
|
+
* - template.docx exists and SHA-256 matches source_sha256
|
|
11
|
+
* - replacements.json is present and valid
|
|
12
|
+
* - clean.json is valid if present
|
|
13
|
+
* - Replacement targets reference metadata fields
|
|
14
|
+
*/
|
|
15
|
+
export declare function validateExternal(externalDir: string, externalId: string): ExternalValidationResult;
|
|
16
|
+
//# sourceMappingURL=external.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external.d.ts","sourceRoot":"","sources":["../../../src/core/validation/external.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AASD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,GACjB,wBAAwB,CAsG1B"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { validateExternalMetadata, loadExternalMetadata, CleanConfigSchema } from '../metadata.js';
|
|
5
|
+
/** Pattern for valid template tags within replacement values */
|
|
6
|
+
const TAG_RE = /\{[a-z_][a-z0-9_]*\}/g;
|
|
7
|
+
/** Pattern for any curly-brace token */
|
|
8
|
+
const ANY_BRACE_RE = /\{[^}]+\}/g;
|
|
9
|
+
/** Only simple identifiers inside braces are allowed */
|
|
10
|
+
const SAFE_TAG_RE = /^\{[a-z_][a-z0-9_]*\}$/;
|
|
11
|
+
/**
|
|
12
|
+
* Validate an external template directory:
|
|
13
|
+
* - metadata.yaml validates against ExternalMetadataSchema
|
|
14
|
+
* - template.docx exists and SHA-256 matches source_sha256
|
|
15
|
+
* - replacements.json is present and valid
|
|
16
|
+
* - clean.json is valid if present
|
|
17
|
+
* - Replacement targets reference metadata fields
|
|
18
|
+
*/
|
|
19
|
+
export function validateExternal(externalDir, externalId) {
|
|
20
|
+
const errors = [];
|
|
21
|
+
const warnings = [];
|
|
22
|
+
// Validate metadata
|
|
23
|
+
const metaResult = validateExternalMetadata(externalDir);
|
|
24
|
+
if (!metaResult.valid) {
|
|
25
|
+
errors.push(...metaResult.errors.map((e) => `metadata: ${e}`));
|
|
26
|
+
return { externalId, valid: false, errors, warnings };
|
|
27
|
+
}
|
|
28
|
+
const metadata = loadExternalMetadata(externalDir);
|
|
29
|
+
// Validate template.docx exists
|
|
30
|
+
const docxPath = join(externalDir, 'template.docx');
|
|
31
|
+
if (!existsSync(docxPath)) {
|
|
32
|
+
errors.push('template.docx not found');
|
|
33
|
+
return { externalId, valid: false, errors, warnings };
|
|
34
|
+
}
|
|
35
|
+
// SHA-256 integrity check
|
|
36
|
+
const docxBuf = readFileSync(docxPath);
|
|
37
|
+
const actualHash = createHash('sha256').update(docxBuf).digest('hex');
|
|
38
|
+
if (actualHash !== metadata.source_sha256) {
|
|
39
|
+
errors.push(`DOCX integrity check failed:\n` +
|
|
40
|
+
` Expected SHA-256: ${metadata.source_sha256}\n` +
|
|
41
|
+
` Actual SHA-256: ${actualHash}`);
|
|
42
|
+
}
|
|
43
|
+
// Validate replacements.json
|
|
44
|
+
const replacementsPath = join(externalDir, 'replacements.json');
|
|
45
|
+
if (!existsSync(replacementsPath)) {
|
|
46
|
+
errors.push('replacements.json not found (required for external templates)');
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
try {
|
|
50
|
+
const raw = readFileSync(replacementsPath, 'utf-8');
|
|
51
|
+
const replacements = JSON.parse(raw);
|
|
52
|
+
if (typeof replacements !== 'object' || replacements === null) {
|
|
53
|
+
errors.push('replacements.json must be a JSON object');
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const metadataFieldNames = new Set(metadata.fields.map((f) => f.name));
|
|
57
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
58
|
+
if (typeof value !== 'string') {
|
|
59
|
+
errors.push(`replacements.json: value for "${key}" must be a string`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const tags = value.match(TAG_RE);
|
|
63
|
+
if (!tags || tags.length === 0) {
|
|
64
|
+
errors.push(`replacements.json: value for "${key}" must contain at least one {identifier} tag, got "${value}"`);
|
|
65
|
+
}
|
|
66
|
+
const allBraces = value.match(ANY_BRACE_RE);
|
|
67
|
+
if (allBraces) {
|
|
68
|
+
for (const token of allBraces) {
|
|
69
|
+
if (!SAFE_TAG_RE.test(token)) {
|
|
70
|
+
errors.push(`replacements.json: unsafe tag "${token}" in value for "${key}". Only {identifier} tags allowed.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (value.includes(key)) {
|
|
75
|
+
errors.push(`replacements.json: value for "${key}" contains the key itself (would cause infinite loop)`);
|
|
76
|
+
}
|
|
77
|
+
// Check field coverage
|
|
78
|
+
if (tags) {
|
|
79
|
+
for (const tag of tags) {
|
|
80
|
+
const fieldName = tag.slice(1, -1);
|
|
81
|
+
if (!metadataFieldNames.has(fieldName)) {
|
|
82
|
+
warnings.push(`Replacement target {${fieldName}} not found in metadata fields`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
errors.push(`replacements.json: ${err.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Validate clean.json if present
|
|
94
|
+
const cleanPath = join(externalDir, 'clean.json');
|
|
95
|
+
if (existsSync(cleanPath)) {
|
|
96
|
+
try {
|
|
97
|
+
const raw = readFileSync(cleanPath, 'utf-8');
|
|
98
|
+
CleanConfigSchema.parse(JSON.parse(raw));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
errors.push('clean.json: invalid format');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { externalId, valid: errors.length === 0, errors, warnings };
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=external.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external.js","sourceRoot":"","sources":["../../../src/core/validation/external.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface LicenseValidationResult {
|
|
2
|
+
templateId: string;
|
|
3
|
+
valid: boolean;
|
|
4
|
+
errors: string[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Validate a template's license metadata.
|
|
8
|
+
*
|
|
9
|
+
* The allow_derivatives field means "the committed source DOCX must not be
|
|
10
|
+
* modified" when false. It does NOT prevent the tool from rendering filled
|
|
11
|
+
* output — that decision is made by the fill command based on directory
|
|
12
|
+
* context (templates/ vs external/).
|
|
13
|
+
*/
|
|
14
|
+
export declare function validateLicense(templateDir: string, templateId: string): LicenseValidationResult;
|
|
15
|
+
//# sourceMappingURL=license.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"license.d.ts","sourceRoot":"","sources":["../../../src/core/validation/license.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,uBAAuB,CAyBhG"}
|