champollion 0.3.3
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 +133 -0
- package/README.md +387 -0
- package/bin/cli.js +278 -0
- package/index.js +135 -0
- package/lib/api-key.js +127 -0
- package/lib/autofix.js +432 -0
- package/lib/bridge/method_bridge.py +430 -0
- package/lib/card-source-resolution.mjs +284 -0
- package/lib/cards/cache.js +169 -0
- package/lib/cards/env.js +82 -0
- package/lib/cards/fetch-card-child.js +38 -0
- package/lib/cards/reader.js +435 -0
- package/lib/cards/refresh.js +111 -0
- package/lib/cards/remote.js +387 -0
- package/lib/cldf-export.mjs +540 -0
- package/lib/cldf-terms.mjs +62 -0
- package/lib/command-help.js +790 -0
- package/lib/commands/audit.js +49 -0
- package/lib/commands/card.js +454 -0
- package/lib/commands/doctor.js +559 -0
- package/lib/commands/fonts.js +489 -0
- package/lib/commands/help.js +91 -0
- package/lib/commands/init.js +1259 -0
- package/lib/commands/integrity.js +148 -0
- package/lib/commands/leaderboard.js +478 -0
- package/lib/commands/lint.js +30 -0
- package/lib/commands/models.js +177 -0
- package/lib/commands/plugin.js +103 -0
- package/lib/commands/provenance.js +45 -0
- package/lib/commands/recommend.js +75 -0
- package/lib/commands/register-corpus.js +678 -0
- package/lib/commands/repair-script.js +42 -0
- package/lib/commands/seal-corpus.js +355 -0
- package/lib/commands/seo.js +72 -0
- package/lib/commands/serve.js +147 -0
- package/lib/commands/status.js +265 -0
- package/lib/commands/submit.js +332 -0
- package/lib/commands/sync.js +89 -0
- package/lib/commands/tm.js +573 -0
- package/lib/commands/verify.js +39 -0
- package/lib/commands/watch.js +20 -0
- package/lib/commands/wrap.js +138 -0
- package/lib/commands/xliff.js +327 -0
- package/lib/commercial-eligibility.js +235 -0
- package/lib/concurrent.js +87 -0
- package/lib/config.js +523 -0
- package/lib/contamination-lane.js +76 -0
- package/lib/content-sync.js +731 -0
- package/lib/content.js +733 -0
- package/lib/corpus-registration.mjs +608 -0
- package/lib/cost-report.js +346 -0
- package/lib/diff.js +155 -0
- package/lib/docusaurus-sync.js +1256 -0
- package/lib/flatten.js +55 -0
- package/lib/format.js +954 -0
- package/lib/hash.js +159 -0
- package/lib/icu.js +473 -0
- package/lib/integrity.js +689 -0
- package/lib/license-gate.mjs +478 -0
- package/lib/license-identify.mjs +229 -0
- package/lib/lint.js +629 -0
- package/lib/method-manifest.js +60 -0
- package/lib/methods/anthropic.js +140 -0
- package/lib/methods/apertium.js +163 -0
- package/lib/methods/api.js +316 -0
- package/lib/methods/base.js +184 -0
- package/lib/methods/content-separator.js +45 -0
- package/lib/methods/deepl.js +426 -0
- package/lib/methods/direct-llm.js +586 -0
- package/lib/methods/external.js +332 -0
- package/lib/methods/fetch-with-retry.js +124 -0
- package/lib/methods/gemini.js +147 -0
- package/lib/methods/google-translate.js +402 -0
- package/lib/methods/http-utils.js +122 -0
- package/lib/methods/libretranslate.js +314 -0
- package/lib/methods/llm-coached.js +670 -0
- package/lib/methods/llm.js +592 -0
- package/lib/methods/local.js +76 -0
- package/lib/methods/microsoft-translator.js +331 -0
- package/lib/methods/openai.js +131 -0
- package/lib/methods/openrouter-client.js +327 -0
- package/lib/methods/openrouter-pricing.js +156 -0
- package/lib/methods/provider-env.js +115 -0
- package/lib/methods/provider-pricing.js +310 -0
- package/lib/methods/tilde.js +150 -0
- package/lib/methods/translated.js +229 -0
- package/lib/methods/translation-error.js +80 -0
- package/lib/models.js +258 -0
- package/lib/no-translate.js +233 -0
- package/lib/output.js +238 -0
- package/lib/pairs.js +547 -0
- package/lib/plugins.js +447 -0
- package/lib/provenance.js +323 -0
- package/lib/recommend.js +648 -0
- package/lib/registers.js +1185 -0
- package/lib/repair-script.js +266 -0
- package/lib/scripts.js +994 -0
- package/lib/seal.mjs +464 -0
- package/lib/sealed-qualifier.mjs +211 -0
- package/lib/security.js +59 -0
- package/lib/segment.js +369 -0
- package/lib/seo.js +275 -0
- package/lib/serve.js +854 -0
- package/lib/string-classify.js +85 -0
- package/lib/submit.mjs +344 -0
- package/lib/sync.js +969 -0
- package/lib/tags/bcp47.js +202 -0
- package/lib/tags/resolve.js +314 -0
- package/lib/terminology.js +111 -0
- package/lib/tm-seed.js +294 -0
- package/lib/tm.js +515 -0
- package/lib/translate-pair.js +197 -0
- package/lib/translate.js +203 -0
- package/lib/types.js +230 -0
- package/lib/validate.js +510 -0
- package/lib/verify.js +451 -0
- package/lib/watch.js +145 -0
- package/lib/xliff.js +184 -0
- package/package.json +93 -0
- package/shared/ATTRIBUTION.md +145 -0
- package/shared/CORPORA-CARDS.md +288 -0
- package/shared/DATA-SOVEREIGNTY.md +500 -0
- package/shared/LANGUAGE-CARD-FIELDS.md +532 -0
- package/shared/card-lint-baseline.json +3189 -0
- package/shared/cards-fallback.json +1 -0
- package/shared/catalogue/card-config.json +6091 -0
- package/shared/catalogue/external-results.json +3888 -0
- package/shared/catalogue/gender-guidance.json +1038 -0
- package/shared/catalogue/method-coverage.json +1751 -0
- package/shared/catalogue/metric-coverage.json +170 -0
- package/shared/catalogue/metric-reliability.json +1 -0
- package/shared/catalogue/register-presets.json +3180 -0
- package/shared/catalogue/vitality-scales.json +55 -0
- package/shared/cldr-index.json +1115 -0
- package/shared/code-bridge.json +253 -0
- package/shared/corpora-cards-v1-reference.md +281 -0
- package/shared/curated-dictionary-flags.json +35 -0
- package/shared/curated-endonyms.json +35 -0
- package/shared/curated-fsts.json +51 -0
- package/shared/curated-orthography-conventions.json +26 -0
- package/shared/curated-sil-resources.json +374 -0
- package/shared/curated-tools.json +41 -0
- package/shared/docent/corpus.json +11333 -0
- package/shared/docent/faq.en.json +564 -0
- package/shared/docent/register-blocks.json +60 -0
- package/shared/docent/system-prompt.md +144 -0
- package/shared/domain-taxonomy.json +35 -0
- package/shared/explainers/glossary.json +2975 -0
- package/shared/explainers/tc-features.json +20112 -0
- package/shared/explainers/term-watchlist.json +147 -0
- package/shared/human-services.json +59 -0
- package/shared/license-corrections.json +261 -0
- package/shared/license-evidence.json +13452 -0
- package/shared/licenses.json +6781 -0
- package/shared/method-registry.json +236 -0
- package/shared/metric-registry.json +620 -0
- package/shared/model-aliases.json +7 -0
- package/shared/schemas/champollion-plugin.schema.json +206 -0
- package/shared/schemas/corpora-card.schema.json +957 -0
- package/shared/schemas/domain-taxonomy.schema.json +64 -0
- package/shared/schemas/external-results.schema.json +314 -0
- package/shared/schemas/human-services.schema.json +90 -0
- package/shared/schemas/language-card.schema.json +1308 -0
- package/shared/schemas/licenses.schema.json +155 -0
- package/shared/schemas/method-card.schema.json +412 -0
- package/shared/schemas/method-registry.schema.json +85 -0
- package/shared/schemas/metric-registry.schema.json +96 -0
- package/shared/schemas/metric-reliability.schema.json +178 -0
- package/shared/schemas/model-aliases.schema.json +27 -0
- package/shared/schemas/source-snapshot.schema.json +96 -0
package/lib/tm-seed.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tm-seed.js — seed the Translation Memory from EXISTING translations.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS:
|
|
5
|
+
* Block-level TM only gains entries as files are (re-)translated. A
|
|
6
|
+
* project that upgraded with a full set of already-translated content
|
|
7
|
+
* files has an empty TM for all of them — so a lost or clobbered
|
|
8
|
+
* .champollion-content.lock re-bills every file through the API even
|
|
9
|
+
* though perfectly good translations sit on disk. `champollion tm seed`
|
|
10
|
+
* walks those existing translations and back-fills the TM: front-matter
|
|
11
|
+
* fields (aligned by field NAME), body blocks (aligned 1:1 by POSITION
|
|
12
|
+
* via segment.js splitBlocks), and the whole body — each keyed with
|
|
13
|
+
* tmMethodKey(pairConfig), exactly as a live sync would store them.
|
|
14
|
+
*
|
|
15
|
+
* SAFETY RULES (never guess alignment):
|
|
16
|
+
* - A (file × locale) is seeded ONLY when the lock manifest entry says
|
|
17
|
+
* the target is up to date (stored hash === SHA-256 of the current
|
|
18
|
+
* source). A stale or missing entry means the target may correspond
|
|
19
|
+
* to an older source — skip.
|
|
20
|
+
* - Body blocks are seeded ONLY when source and target contain the SAME
|
|
21
|
+
* number of translatable blocks. Any mismatch skips the entire file
|
|
22
|
+
* with a warning; a positional guess would poison the TM.
|
|
23
|
+
* - Targets containing the legacy '[EN] ' fallback marker are never
|
|
24
|
+
* seeded — they are untranslated English, not translations.
|
|
25
|
+
*
|
|
26
|
+
* Seeding is additive and idempotent: entries whose cached value already
|
|
27
|
+
* matches the on-disk translation are left untouched; disk is treated as
|
|
28
|
+
* truth when they differ (a hand-polished file wins over an old cache).
|
|
29
|
+
*
|
|
30
|
+
* CACHE-UNIT PARITY (must mirror the sync engines exactly):
|
|
31
|
+
* Block keys/values replicate content-sync/docusaurus-sync block mode —
|
|
32
|
+
* segment the PROTECTED body (protectBlocks → splitBlocks), keep only
|
|
33
|
+
* 'translatable' segments (separators/passthrough are copied verbatim by
|
|
34
|
+
* sync, never cached), and key each on its RESTORED source text with the
|
|
35
|
+
* RESTORED target text as the value. Placeholder numbering is positional
|
|
36
|
+
* per file, so only restored text is stable across files and edits.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import fs from 'node:fs';
|
|
40
|
+
import path from 'node:path';
|
|
41
|
+
import crypto from 'node:crypto';
|
|
42
|
+
import { loadTM, saveTM, lookupTM, storeTM, isTMDirty, tmSize, tmMethodKey } from './tm.js';
|
|
43
|
+
import { readContentManifest } from './content-sync.js';
|
|
44
|
+
import { splitBlocks } from './segment.js';
|
|
45
|
+
import { isPathContained } from './security.js';
|
|
46
|
+
import { output } from './output.js';
|
|
47
|
+
import {
|
|
48
|
+
discoverContentFiles,
|
|
49
|
+
getTargetContentPath,
|
|
50
|
+
discoverDocusaurusContentFiles,
|
|
51
|
+
getDocusaurusTargetPath,
|
|
52
|
+
parseContentFile,
|
|
53
|
+
protectBlocks,
|
|
54
|
+
restoreBlocks,
|
|
55
|
+
DEFAULT_TRANSLATABLE_FIELDS,
|
|
56
|
+
} from './content.js';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extract the restored source texts of a body's translatable blocks —
|
|
60
|
+
* the exact TM key unit the sync engines use in block mode.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} body - Raw markdown body (front matter stripped)
|
|
63
|
+
* @returns {string[]} Restored translatable block texts, in document order
|
|
64
|
+
*/
|
|
65
|
+
function translatableBlockTexts(body) {
|
|
66
|
+
const { protectedBody, blocks } = protectBlocks(body);
|
|
67
|
+
return splitBlocks(protectedBody)
|
|
68
|
+
.filter(seg => seg.type === 'translatable')
|
|
69
|
+
.map(seg => restoreBlocks(seg.text, blocks));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Seed the TM from existing translated content files.
|
|
74
|
+
*
|
|
75
|
+
* Walks the same (source file × pair) space as the two content sync
|
|
76
|
+
* engines, using the same manifest keys, so what gets seeded is exactly
|
|
77
|
+
* what a future sync would look up:
|
|
78
|
+
* - Hugo lane (contentDir): "relPath:code"
|
|
79
|
+
* - Docusaurus lane: "docusaurus:dirName/relPath:code"
|
|
80
|
+
*
|
|
81
|
+
* @param {object} options
|
|
82
|
+
* @param {string} options.cwd - Project root (lock file + TM location)
|
|
83
|
+
* @param {Map<string, object>} options.pairs - Resolved pair graph (pairKey → pairConfig)
|
|
84
|
+
* @param {string} options.sourceLocale - Source language code
|
|
85
|
+
* @param {string[]|null} [options.translatableFields] - Hugo-lane front matter
|
|
86
|
+
* fields (null → DEFAULT_TRANSLATABLE_FIELDS). The Docusaurus lane always
|
|
87
|
+
* uses the defaults, mirroring runDocusaurusSync.
|
|
88
|
+
* @param {string|null} [options.contentDir] - Hugo content directory (null = lane off)
|
|
89
|
+
* @param {{ localesDir: string, docsDir: string, blogDir: string }|null} [options.docusaurus]
|
|
90
|
+
* Docusaurus lane directories (null = lane off)
|
|
91
|
+
* @param {boolean} [options.dryRun] - Report what would be seeded, write nothing
|
|
92
|
+
* @param {string|null} [options.localeFilter] - Only seed one target locale
|
|
93
|
+
* @returns {{
|
|
94
|
+
* files: Array<{ label: string, code: string, status: 'seeded'|'skipped',
|
|
95
|
+
* reason?: string, fields: number, blocks: number, body: boolean,
|
|
96
|
+
* added: number, existing: number }>,
|
|
97
|
+
* seededFiles: number, skippedFiles: number,
|
|
98
|
+
* entriesAdded: number, entriesExisting: number,
|
|
99
|
+
* tmSizeBefore: number, tmSizeAfter: number,
|
|
100
|
+
* dryRun: boolean, saved: boolean,
|
|
101
|
+
* }}
|
|
102
|
+
*/
|
|
103
|
+
function seedTMFromExisting(options) {
|
|
104
|
+
const {
|
|
105
|
+
cwd,
|
|
106
|
+
pairs,
|
|
107
|
+
sourceLocale,
|
|
108
|
+
translatableFields = null,
|
|
109
|
+
contentDir = null,
|
|
110
|
+
docusaurus = null,
|
|
111
|
+
dryRun = false,
|
|
112
|
+
localeFilter = null,
|
|
113
|
+
} = options;
|
|
114
|
+
|
|
115
|
+
const manifest = readContentManifest(cwd);
|
|
116
|
+
const tm = loadTM(cwd);
|
|
117
|
+
const tmSizeBefore = tmSize(tm);
|
|
118
|
+
|
|
119
|
+
const pairEntries = [...pairs.entries()]
|
|
120
|
+
.filter(([, pairConfig]) => !localeFilter || pairConfig.target === localeFilter)
|
|
121
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
122
|
+
|
|
123
|
+
// ── Collect candidates from both lanes ─────────────────────────────
|
|
124
|
+
// Each candidate mirrors one (source file × pair) unit of sync work.
|
|
125
|
+
const candidates = [];
|
|
126
|
+
|
|
127
|
+
if (contentDir && fs.existsSync(contentDir)) {
|
|
128
|
+
const fieldsList = translatableFields || DEFAULT_TRANSLATABLE_FIELDS;
|
|
129
|
+
for (const sourcePath of discoverContentFiles(contentDir, sourceLocale)) {
|
|
130
|
+
const relPath = path.relative(contentDir, sourcePath);
|
|
131
|
+
for (const [, pairConfig] of pairEntries) {
|
|
132
|
+
const code = pairConfig.target;
|
|
133
|
+
const targetPath = getTargetContentPath(sourcePath, code, sourceLocale);
|
|
134
|
+
if (!isPathContained(targetPath, contentDir)) continue;
|
|
135
|
+
candidates.push({
|
|
136
|
+
sourcePath, targetPath, pairConfig, code, fieldsList,
|
|
137
|
+
label: relPath,
|
|
138
|
+
manifestKey: `${relPath}:${code}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (docusaurus) {
|
|
145
|
+
const { localesDir, docsDir, blogDir } = docusaurus;
|
|
146
|
+
const contentSources = [];
|
|
147
|
+
if (docsDir && fs.existsSync(docsDir)) {
|
|
148
|
+
contentSources.push({ dir: docsDir, plugin: 'docusaurus-plugin-content-docs' });
|
|
149
|
+
}
|
|
150
|
+
if (blogDir && fs.existsSync(blogDir)) {
|
|
151
|
+
contentSources.push({ dir: blogDir, plugin: 'docusaurus-plugin-content-blog' });
|
|
152
|
+
}
|
|
153
|
+
for (const { dir, plugin } of contentSources) {
|
|
154
|
+
const dirName = path.basename(dir);
|
|
155
|
+
for (const sourcePath of discoverDocusaurusContentFiles(dir)) {
|
|
156
|
+
const relPath = path.relative(dir, sourcePath);
|
|
157
|
+
for (const [, pairConfig] of pairEntries) {
|
|
158
|
+
const code = pairConfig.target;
|
|
159
|
+
const targetPath = getDocusaurusTargetPath(sourcePath, dir, code, localesDir, plugin);
|
|
160
|
+
if (!isPathContained(targetPath, localesDir)) continue;
|
|
161
|
+
candidates.push({
|
|
162
|
+
sourcePath, targetPath, pairConfig, code,
|
|
163
|
+
fieldsList: DEFAULT_TRANSLATABLE_FIELDS,
|
|
164
|
+
label: `${dirName}/${relPath}`,
|
|
165
|
+
manifestKey: `docusaurus:${dirName}/${relPath}:${code}`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Seed each candidate behind the safety gates ─────────────────────
|
|
173
|
+
const files = [];
|
|
174
|
+
let seededFiles = 0;
|
|
175
|
+
let skippedFiles = 0;
|
|
176
|
+
let entriesAdded = 0;
|
|
177
|
+
let entriesExisting = 0;
|
|
178
|
+
|
|
179
|
+
const sourceCache = new Map(); // sourcePath → { raw, hash }
|
|
180
|
+
|
|
181
|
+
for (const cand of candidates) {
|
|
182
|
+
const record = {
|
|
183
|
+
label: cand.label, code: cand.code, status: 'skipped',
|
|
184
|
+
fields: 0, blocks: 0, body: false, added: 0, existing: 0,
|
|
185
|
+
};
|
|
186
|
+
files.push(record);
|
|
187
|
+
|
|
188
|
+
if (!fs.existsSync(cand.targetPath)) {
|
|
189
|
+
record.reason = 'no translated file';
|
|
190
|
+
skippedFiles++;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!sourceCache.has(cand.sourcePath)) {
|
|
195
|
+
const raw = fs.readFileSync(cand.sourcePath, 'utf-8');
|
|
196
|
+
const hash = crypto.createHash('sha256').update(raw, 'utf-8').digest('hex');
|
|
197
|
+
sourceCache.set(cand.sourcePath, { raw, hash });
|
|
198
|
+
}
|
|
199
|
+
const { raw, hash } = sourceCache.get(cand.sourcePath);
|
|
200
|
+
|
|
201
|
+
// Lock-manifest gate: only an entry that matches the CURRENT source
|
|
202
|
+
// hash proves the on-disk translation corresponds to this source.
|
|
203
|
+
const storedHash = cand.manifestKey in manifest ? manifest[cand.manifestKey] : null;
|
|
204
|
+
if (storedHash === null) {
|
|
205
|
+
record.reason = 'no lock entry — cannot prove the translation matches this source';
|
|
206
|
+
skippedFiles++;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (storedHash !== hash) {
|
|
210
|
+
record.reason = 'lock entry is stale (source changed since last sync)';
|
|
211
|
+
skippedFiles++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const targetRaw = fs.readFileSync(cand.targetPath, 'utf-8');
|
|
216
|
+
if (targetRaw.includes('[EN] ')) {
|
|
217
|
+
record.reason = 'target is a legacy [EN] fallback, not a translation';
|
|
218
|
+
skippedFiles++;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const src = parseContentFile(raw);
|
|
223
|
+
const tgt = parseContentFile(targetRaw);
|
|
224
|
+
const srcBlocks = translatableBlockTexts(src.body);
|
|
225
|
+
const tgtBlocks = translatableBlockTexts(tgt.body);
|
|
226
|
+
|
|
227
|
+
// Positional alignment is only trustworthy when the counts agree —
|
|
228
|
+
// skip the WHOLE file otherwise, never seed a guessed pairing.
|
|
229
|
+
if (srcBlocks.length !== tgtBlocks.length) {
|
|
230
|
+
record.reason = `translatable block count mismatch (source ${srcBlocks.length} vs target ${tgtBlocks.length})`;
|
|
231
|
+
skippedFiles++;
|
|
232
|
+
output.warn(`tm seed: ${cand.label} → ${cand.code} skipped — ${record.reason}`);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const tmKey = tmMethodKey(cand.pairConfig);
|
|
237
|
+
const seedEntry = (source, translation) => {
|
|
238
|
+
if (typeof source !== 'string' || source.trim() === '') return false;
|
|
239
|
+
if (typeof translation !== 'string' || translation.trim() === '') return false;
|
|
240
|
+
if (lookupTM(tm, source, cand.code, tmKey) === translation) {
|
|
241
|
+
record.existing++;
|
|
242
|
+
entriesExisting++;
|
|
243
|
+
} else {
|
|
244
|
+
if (!dryRun) storeTM(tm, source, cand.code, tmKey, translation);
|
|
245
|
+
record.added++;
|
|
246
|
+
entriesAdded++;
|
|
247
|
+
}
|
|
248
|
+
return true;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// Front-matter fields — aligned by NAME, the same per-field cache unit
|
|
252
|
+
// the sync path looks up via partitionByTM.
|
|
253
|
+
if (src.hasFrontMatter && tgt.hasFrontMatter) {
|
|
254
|
+
for (const field of cand.fieldsList) {
|
|
255
|
+
if (typeof src.frontMatter[field] === 'string' && typeof tgt.frontMatter[field] === 'string') {
|
|
256
|
+
if (seedEntry(src.frontMatter[field], tgt.frontMatter[field])) record.fields++;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Body blocks — aligned 1:1 by position (counts verified equal above).
|
|
262
|
+
for (let i = 0; i < srcBlocks.length; i++) {
|
|
263
|
+
if (seedEntry(srcBlocks[i], tgtBlocks[i])) record.blocks++;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Whole body — the cache unit the current sync path serves bodies from.
|
|
267
|
+
if (src.body.trim() && tgt.body.trim()) {
|
|
268
|
+
record.body = seedEntry(src.body, tgt.body);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
record.status = 'seeded';
|
|
272
|
+
seededFiles++;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let saved = false;
|
|
276
|
+
if (!dryRun && isTMDirty(tm)) {
|
|
277
|
+
saveTM(cwd, tm);
|
|
278
|
+
saved = true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
files,
|
|
283
|
+
seededFiles,
|
|
284
|
+
skippedFiles,
|
|
285
|
+
entriesAdded,
|
|
286
|
+
entriesExisting,
|
|
287
|
+
tmSizeBefore,
|
|
288
|
+
tmSizeAfter: tmSize(tm),
|
|
289
|
+
dryRun,
|
|
290
|
+
saved,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export { seedTMFromExisting };
|