@attentionform/astra 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -110,7 +110,7 @@ Renders one saved-file page to PNG/JPEG via headless LibreOffice (same renderer
110
110
  Lists recorded sidecar commits newest first; read-only; `--init` (alias `--track`) records the current file as the baseline when no sidecar exists yet.
111
111
 
112
112
  ### 13. Revert to Recorded State (`astra revert`)
113
- Restores the tracked package state `--steps <n>` commits back and records the restore as a new commit (append-only); `preview-revert` previews the same change read-only.
113
+ Restores the package state `--steps <n>` commits back (every package part, comment metadata and document properties included) and records the restore as a new commit (append-only). Commits recorded before 0.4.2 hold fewer parts, so restoring from one prints a warning; `preview-revert` previews the same change read-only.
114
114
 
115
115
  ### 14. Redo Reverted State (`astra redo`)
116
116
  Re-applies the state replaced by the most recent revert (one redo per revert head).
package/dist/cli.js CHANGED
@@ -6175,6 +6175,9 @@ var RSIDS_EMPTY_RE = /<w:rsids>\s*<\/w:rsids>/g;
6175
6175
  var RSIDS_SELF_CLOSING_RE = /<w:rsids\s*\/>/g;
6176
6176
  var AUTHOR_ATTR_RE = /\s+(w|w15):author="[^"]*"/g;
6177
6177
  var INITIALS_ATTR_RE = /\s+w:initials="[^"]*"/g;
6178
+ var PRESENCE_INFO_TAG_RE = /<w15:presenceInfo\b(?:[^>"']|"[^"]*"|'[^']*')*>/g;
6179
+ var PRESENCE_PROVIDER_NONE_RE = /\sw15:providerId=(["'])None\1/;
6180
+ var PRESENCE_USERID_RE = /(\sw15:userId=)(?:"[^"]*"|'[^']*')/;
6178
6181
  var EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
6179
6182
  var PHONE_RE = /(\+\d[\d\s\-().]{6,}\d|\(\d{3}\)\s*\d{3}[\s\-.]\d{4}|\b\d{3}[\s\-.]\d{3}[\s\-.]\d{4}\b)/g;
6180
6183
  var CLAUSE_ACRONYM_RE = /\b(dfars|far|cfr|nist)\b/gi;
@@ -6946,6 +6949,15 @@ function sanitizeDocumentXml(xml, opts, partName = "word XML part", hooks) {
6946
6949
  o.replacementAuthor.split(/[\s._-]+/).filter(Boolean).map((s) => s[0]).join("").slice(0, 4).toUpperCase() || "A"
6947
6950
  );
6948
6951
  out = out.replace(INITIALS_ATTR_RE, ` w:initials="${initials}"`);
6952
+ if (partName === "word/people.xml") {
6953
+ out = out.replace(PRESENCE_INFO_TAG_RE, (tag) => {
6954
+ if (!PRESENCE_PROVIDER_NONE_RE.test(tag)) return tag;
6955
+ return tag.replace(PRESENCE_USERID_RE, (_m, open) => {
6956
+ purgedAuthors += 1;
6957
+ return `${open}"${replacement === "" ? "Anonymous" : replacement}"`;
6958
+ });
6959
+ });
6960
+ }
6949
6961
  }
6950
6962
  let acceptedRevisions = 0;
6951
6963
  let unresolvedRevisions = [];
@@ -10579,9 +10591,10 @@ var DocxParser = class _DocxParser {
10579
10591
  /**
10580
10592
  * Restores the package to a previously captured snapshot: every entry is
10581
10593
  * restored (entries added after the snapshot are removed), then documentDom
10582
- * and stylesDom are reloaded and stable IDs re-assigned.
10594
+ * and stylesDom are reloaded and stable IDs re-assigned. The restored package is exactly the snapshot;
10595
+ * only a restore that mixes a pre-0.4.2 commit with live parts passes `registerCoreProperties`.
10583
10596
  */
10584
- restorePackageSnapshot(snapshot3) {
10597
+ restorePackageSnapshot(snapshot3, options) {
10585
10598
  for (const entry of [...this.zip.getEntries()]) {
10586
10599
  if (!snapshot3.has(entry.entryName)) {
10587
10600
  this.zip.deleteFile(entry);
@@ -10600,8 +10613,15 @@ var DocxParser = class _DocxParser {
10600
10613
  continue;
10601
10614
  }
10602
10615
  const buffer = Buffer.from(data);
10603
- if (this.zip.getEntry(entryName)) {
10604
- this.zip.updateFile(entryName, buffer);
10616
+ const liveEntry = this.zip.getEntry(entryName);
10617
+ if (liveEntry) {
10618
+ let unchanged = false;
10619
+ try {
10620
+ unchanged = !liveEntry.isDirectory && liveEntry.header.size === buffer.length && readPartData(liveEntry, entryName).equals(buffer);
10621
+ } catch {
10622
+ unchanged = false;
10623
+ }
10624
+ if (!unchanged) this.zip.updateFile(entryName, buffer);
10605
10625
  } else {
10606
10626
  this.zip.addFile(entryName, buffer);
10607
10627
  }
@@ -10626,7 +10646,7 @@ var DocxParser = class _DocxParser {
10626
10646
  } else {
10627
10647
  this.stylesDom = null;
10628
10648
  }
10629
- registerExistingCorePropertiesPart(this.getZipAdapter());
10649
+ if (options?.registerCoreProperties) registerExistingCorePropertiesPart(this.getZipAdapter());
10630
10650
  this.invalidateMediaCache();
10631
10651
  this.assignStableIds();
10632
10652
  this._bumpNodePagesRevision();
@@ -13886,7 +13906,55 @@ function getRepoDir(docPath) {
13886
13906
  if (root !== null) return import_path3.default.join(root, historyKey(import_path3.default.dirname(docPath)), import_path3.default.basename(docPath));
13887
13907
  return import_path3.default.join(import_path3.default.dirname(docPath), ".astra", import_path3.default.basename(docPath));
13888
13908
  }
13909
+ var SNAPSHOT_FORMAT_MARKER = ".astra-snapshot-format";
13910
+ var SNAPSHOT_FORMAT_VERSION = "2";
13911
+ function isCompleteSnapshotMarker(content) {
13912
+ if (content === void 0) return false;
13913
+ return Buffer.from(content).toString("utf8").trim() === SNAPSHOT_FORMAT_VERSION;
13914
+ }
13915
+ var ESCAPED_DIR = "__astra_escaped__";
13916
+ var ESCAPED_NAME_MAX = 255;
13917
+ function needsHistoryEscape(entryName) {
13918
+ const name2 = entryName.replace(/\\/g, "/");
13919
+ if (name2 === SNAPSHOT_FORMAT_MARKER || name2 === "document.xml") return true;
13920
+ if (name2 === ESCAPED_DIR || name2.startsWith(`${ESCAPED_DIR}/`)) return true;
13921
+ return name2.split("/").some((segment) => segment.toLowerCase().startsWith(".git"));
13922
+ }
13923
+ function isUnrecordableName(entryName) {
13924
+ const name2 = entryName.replace(/\\/g, "/");
13925
+ if (name2 === "" || name2.endsWith("/") || name2.startsWith("/") || /^[a-zA-Z]:/.test(name2)) return true;
13926
+ return name2.split("/").some((segment) => segment === "" || segment === "." || segment === "..");
13927
+ }
13889
13928
  function isTrackedPackageFile(entryName) {
13929
+ const name2 = entryName.replace(/\\/g, "/");
13930
+ if (name2 === "word/document.xml") return false;
13931
+ return !isUnrecordableName(name2);
13932
+ }
13933
+ function historyPathFor(entryName) {
13934
+ const name2 = entryName.replace(/\\/g, "/");
13935
+ if (!needsHistoryEscape(name2)) return name2;
13936
+ const encoded = Buffer.from(name2, "utf8").toString("base64url");
13937
+ if (encoded.length > ESCAPED_NAME_MAX) {
13938
+ throw Object.assign(
13939
+ new Error(`E_VALIDATION: the package entry name is too long to record in history (${name2.length} characters).`),
13940
+ {
13941
+ code: "E_VALIDATION",
13942
+ remedy: "Rename or remove that package part (for example with sanitize_document), then save again."
13943
+ }
13944
+ );
13945
+ }
13946
+ return `${ESCAPED_DIR}/${encoded}`;
13947
+ }
13948
+ function entryNameFor(repoPath) {
13949
+ const rel = repoPath.replace(/\\/g, "/");
13950
+ if (!rel.startsWith(`${ESCAPED_DIR}/`)) return rel;
13951
+ return Buffer.from(rel.slice(ESCAPED_DIR.length + 1), "base64url").toString("utf8");
13952
+ }
13953
+ function isRepoPartPath(repoPath) {
13954
+ const rel = repoPath.replace(/\\/g, "/");
13955
+ return rel !== "document.xml" && rel !== SNAPSHOT_FORMAT_MARKER && rel !== "" && !rel.startsWith(".git/");
13956
+ }
13957
+ function isLegacyTrackedPackageFile(entryName) {
13890
13958
  const name2 = entryName.replace(/\\/g, "/");
13891
13959
  if (name2 === "word/document.xml") return false;
13892
13960
  if (name2 === "word/styles.xml") return true;
@@ -13910,7 +13978,7 @@ function toRepoRelative(entryName) {
13910
13978
  }
13911
13979
  async function writeAndAdd(repoDir, relPath, content) {
13912
13980
  const safeRel = toRepoRelative(relPath);
13913
- if (safeRel.includes("..") || import_path3.default.isAbsolute(safeRel) || /^[a-zA-Z]:/.test(safeRel)) {
13981
+ if (isTraversalName(safeRel)) {
13914
13982
  throw new Error(`Security Violation: refusing to track package path '${relPath}'`);
13915
13983
  }
13916
13984
  const fullPath = import_path3.default.join(repoDir, safeRel);
@@ -13922,6 +13990,59 @@ async function writeAndAdd(repoDir, relPath, content) {
13922
13990
  await writeFileNoFollow(fullPath, content);
13923
13991
  await import_isomorphic_git.default.add({ fs: import_fs4.default, dir: repoDir, filepath: safeRel });
13924
13992
  }
13993
+ function isTraversalName(entryName) {
13994
+ const name2 = toRepoRelative(entryName);
13995
+ return name2.split("/").includes("..") || import_path3.default.isAbsolute(name2) || /^[a-zA-Z]:/.test(name2);
13996
+ }
13997
+ async function writeSnapshotParts(repoDir, extraFiles) {
13998
+ const written = /* @__PURE__ */ new Set();
13999
+ if (!extraFiles) return written;
14000
+ for (const [name2, content] of Object.entries(extraFiles)) {
14001
+ if (isTraversalName(name2)) {
14002
+ await writeAndAdd(repoDir, name2, content);
14003
+ continue;
14004
+ }
14005
+ if (!isTrackedPackageFile(name2)) continue;
14006
+ const repoPath = historyPathFor(name2);
14007
+ await writeAndAdd(repoDir, repoPath, content);
14008
+ written.add(repoPath);
14009
+ }
14010
+ return written;
14011
+ }
14012
+ async function indexedPartPaths(repoDir) {
14013
+ const tracked = await import_isomorphic_git.default.listFiles({ fs: import_fs4.default, dir: repoDir });
14014
+ return new Set(tracked.map((t) => toRepoRelative(t)).filter(isRepoPartPath));
14015
+ }
14016
+ async function snapshotIsProvenComplete(repoDir, extraFiles, written) {
14017
+ if (!extraFiles) return false;
14018
+ try {
14019
+ const indexed = await indexedPartPaths(repoDir);
14020
+ if (indexed.size !== written.size) return false;
14021
+ for (const p of written) if (!indexed.has(p)) return false;
14022
+ return true;
14023
+ } catch {
14024
+ return false;
14025
+ }
14026
+ }
14027
+ async function applySnapshotMarker(repoDir, complete) {
14028
+ if (complete) {
14029
+ await writeAndAdd(repoDir, SNAPSHOT_FORMAT_MARKER, `${SNAPSHOT_FORMAT_VERSION}
14030
+ `);
14031
+ return;
14032
+ }
14033
+ try {
14034
+ await import_promises2.default.unlink(import_path3.default.join(repoDir, SNAPSHOT_FORMAT_MARKER));
14035
+ } catch {
14036
+ }
14037
+ try {
14038
+ await import_isomorphic_git.default.remove({ fs: import_fs4.default, dir: repoDir, filepath: SNAPSHOT_FORMAT_MARKER });
14039
+ } catch {
14040
+ }
14041
+ const stillIndexed = (await import_isomorphic_git.default.listFiles({ fs: import_fs4.default, dir: repoDir })).some((t) => toRepoRelative(t) === SNAPSHOT_FORMAT_MARKER);
14042
+ if (stillIndexed) {
14043
+ throw new Error("History commit refused: could not clear the snapshot marker of an earlier commit.");
14044
+ }
14045
+ }
13925
14046
  async function chmod0700BestEffort(dir) {
13926
14047
  if (process.platform === "win32") return;
13927
14048
  try {
@@ -14297,11 +14418,8 @@ async function initAstraRepo(docPath, initialXml, extraFiles) {
14297
14418
  const repoDir = await ensureAstraGitRepo(docPath);
14298
14419
  await import_isomorphic_git.default.init({ fs: import_fs4.default, dir: repoDir });
14299
14420
  await writeAndAdd(repoDir, "document.xml", initialXml);
14300
- if (extraFiles) {
14301
- for (const [name2, content] of Object.entries(extraFiles)) {
14302
- await writeAndAdd(repoDir, name2, content);
14303
- }
14304
- }
14421
+ const writtenParts = await writeSnapshotParts(repoDir, extraFiles);
14422
+ await applySnapshotMarker(repoDir, await snapshotIsProvenComplete(repoDir, extraFiles, writtenParts));
14305
14423
  await import_isomorphic_git.default.commit({
14306
14424
  fs: import_fs4.default,
14307
14425
  dir: repoDir,
@@ -14357,41 +14475,35 @@ async function commitChange(docPath, xmlContent, message, extraFiles, deletedFil
14357
14475
  } catch (err) {
14358
14476
  if (err?.code === HISTORY_QUOTA_CODE) throw err;
14359
14477
  }
14360
- await writeAndAdd(repoDir, "document.xml", xmlContent);
14361
14478
  if (extraFiles) {
14362
- for (const [name2, content] of Object.entries(extraFiles)) {
14363
- await writeAndAdd(repoDir, name2, content);
14364
- }
14479
+ for (const name2 of Object.keys(extraFiles)) if (!isTraversalName(name2) && isTrackedPackageFile(name2)) historyPathFor(name2);
14365
14480
  }
14481
+ await writeAndAdd(repoDir, "document.xml", xmlContent);
14482
+ const writtenParts = await writeSnapshotParts(repoDir, extraFiles);
14366
14483
  try {
14367
14484
  const toDelete = /* @__PURE__ */ new Set();
14368
14485
  if (deletedFiles) {
14369
14486
  for (const name2 of deletedFiles) {
14370
- const rel = toRepoRelative(name2);
14371
- if (rel === "document.xml") continue;
14372
- toDelete.add(rel);
14487
+ if (!isTrackedPackageFile(name2) || isTraversalName(name2)) continue;
14488
+ toDelete.add(historyPathFor(name2));
14373
14489
  }
14374
14490
  }
14375
14491
  if (extraFiles) {
14376
- const newTracked = new Set(
14377
- Object.keys(extraFiles).map((k) => toRepoRelative(k))
14378
- );
14492
+ const newTracked = writtenParts;
14379
14493
  try {
14380
14494
  const tracked = await import_isomorphic_git.default.listFiles({ fs: import_fs4.default, dir: repoDir });
14381
14495
  for (const t of tracked) {
14382
14496
  const rel = toRepoRelative(t);
14383
- if (rel === "document.xml") continue;
14384
- if (isTrackedPackageFile(rel) && !newTracked.has(rel)) {
14497
+ if (isRepoPartPath(rel) && !newTracked.has(rel)) {
14385
14498
  toDelete.add(rel);
14386
14499
  }
14387
14500
  }
14388
14501
  } catch {
14389
14502
  }
14390
14503
  try {
14391
- const onDisk = await collectRepoFiles(repoDir);
14392
- for (const rel of onDisk.keys()) {
14393
- if (rel === "document.xml") continue;
14394
- if (isTrackedPackageFile(rel) && !newTracked.has(rel)) {
14504
+ const onDisk = await listRepoPaths(repoDir);
14505
+ for (const rel of onDisk) {
14506
+ if (isRepoPartPath(rel) && !newTracked.has(rel)) {
14395
14507
  toDelete.add(rel);
14396
14508
  }
14397
14509
  }
@@ -14399,7 +14511,7 @@ async function commitChange(docPath, xmlContent, message, extraFiles, deletedFil
14399
14511
  }
14400
14512
  }
14401
14513
  for (const rel of toDelete) {
14402
- if (rel.includes("..") || import_path3.default.isAbsolute(rel) || /^[a-zA-Z]:/.test(rel)) continue;
14514
+ if (isTraversalName(rel)) continue;
14403
14515
  const fullPath = import_path3.default.join(repoDir, rel);
14404
14516
  try {
14405
14517
  await import_promises2.default.unlink(fullPath);
@@ -14412,6 +14524,7 @@ async function commitChange(docPath, xmlContent, message, extraFiles, deletedFil
14412
14524
  }
14413
14525
  } catch {
14414
14526
  }
14527
+ await applySnapshotMarker(repoDir, await snapshotIsProvenComplete(repoDir, extraFiles, writtenParts));
14415
14528
  const sha = await import_isomorphic_git.default.commit({
14416
14529
  fs: import_fs4.default,
14417
14530
  dir: repoDir,
@@ -14463,8 +14576,8 @@ async function getHistory(docPath) {
14463
14576
  });
14464
14577
  return entries;
14465
14578
  }
14466
- async function collectRepoFiles(repoDir) {
14467
- const out = /* @__PURE__ */ new Map();
14579
+ async function listRepoPaths(repoDir) {
14580
+ const out = [];
14468
14581
  async function walk(dir, relBase) {
14469
14582
  let entries = [];
14470
14583
  try {
@@ -14474,21 +14587,18 @@ async function collectRepoFiles(repoDir) {
14474
14587
  }
14475
14588
  for (const entry of entries) {
14476
14589
  if (entry.name === ".git") continue;
14477
- const full = import_path3.default.join(dir, entry.name);
14478
14590
  const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
14479
- if (entry.isDirectory()) {
14480
- await walk(full, rel);
14481
- } else if (entry.isFile()) {
14482
- try {
14483
- out.set(rel, await import_promises2.default.readFile(full));
14484
- } catch {
14485
- }
14486
- }
14591
+ if (entry.isDirectory()) await walk(import_path3.default.join(dir, entry.name), rel);
14592
+ else if (entry.isFile()) out.push(rel);
14487
14593
  }
14488
14594
  }
14489
14595
  await walk(repoDir, "");
14490
14596
  return out;
14491
14597
  }
14598
+ function trackedPredicateFor(target) {
14599
+ return target.complete ? isTrackedPackageFile : isLegacyTrackedPackageFile;
14600
+ }
14601
+ var INCOMPLETE_SNAPSHOT_WARNING = "This commit was recorded by an older ASTra version and predates complete snapshots: parts it never recorded (for example comment metadata, document properties, custom XML) keep their current content in the file, so the restore is not exact: those parts can be left without the content-type or relationship entries they had before. Restores to versions saved from now on are exact.";
14492
14602
  async function checkoutCommit(docPath, commitOid) {
14493
14603
  if (typeof commitOid !== "string" || commitOid.length === 0) {
14494
14604
  throw new Error(`Cannot checkout: commit ${commitOid} not found in history.`);
@@ -14552,8 +14662,18 @@ async function checkoutCommit(docPath, commitOid) {
14552
14662
  );
14553
14663
  }
14554
14664
  const xml = Buffer.from(docBlob.blob).toString("utf8");
14665
+ let complete = false;
14666
+ const markerOid = blobs.get(SNAPSHOT_FORMAT_MARKER);
14667
+ if (markerOid) {
14668
+ try {
14669
+ const marker = await import_isomorphic_git.default.readBlob({ fs: import_fs4.default, dir: repoDir, oid: markerOid });
14670
+ complete = isCompleteSnapshotMarker(marker.blob);
14671
+ } catch {
14672
+ complete = false;
14673
+ }
14674
+ }
14555
14675
  const trackedEntries = [...blobs.entries()].filter(
14556
- ([rel]) => rel !== "document.xml" && isTrackedPackageFile(rel)
14676
+ ([rel]) => isRepoPartPath(rel) && (complete || isLegacyTrackedPackageFile(entryNameFor(rel)))
14557
14677
  );
14558
14678
  const extraBlobs = await Promise.all(
14559
14679
  trackedEntries.map(async ([rel, blobOid]) => {
@@ -14566,11 +14686,11 @@ async function checkoutCommit(docPath, commitOid) {
14566
14686
  `Checkout failed: cannot read blob '${rel}' (${blobOid}) at commit ${commitOid}: ${cause}`
14567
14687
  );
14568
14688
  }
14569
- return [rel, Buffer.from(blobResult.blob)];
14689
+ return [entryNameFor(rel), Buffer.from(blobResult.blob)];
14570
14690
  })
14571
14691
  );
14572
14692
  const extra = new Map(extraBlobs);
14573
- return { xml, extra };
14693
+ return { xml, extra, complete };
14574
14694
  }
14575
14695
  async function commitStateIfDiffersFromHead(docPath, xmlContent, message, extraFiles = {}, author) {
14576
14696
  try {
@@ -40718,7 +40838,7 @@ function summarizeStyleGuide(guide) {
40718
40838
  // package.json
40719
40839
  var package_default = {
40720
40840
  name: "@attentionform/astra",
40721
- version: "0.4.1",
40841
+ version: "0.4.2",
40722
40842
  description: "Command-line tool for OOXML (.docx) document engineering",
40723
40843
  main: "dist/cli.js",
40724
40844
  types: "dist/packages/cli/src/cli.d.ts",
@@ -40762,7 +40882,7 @@ var package_default = {
40762
40882
  },
40763
40883
  dependencies: {
40764
40884
  "@attentionform/astra-engine": "^0.4.0",
40765
- "@attentionform/ooxml-validator": "^0.4.1",
40885
+ "@attentionform/ooxml-validator": "^0.4.2",
40766
40886
  "@xmldom/xmldom": "0.8.15",
40767
40887
  "adm-zip": "0.6.1",
40768
40888
  "isomorphic-git": "^1.25.0",
@@ -41538,10 +41658,13 @@ function commandHelpText(cmd) {
41538
41658
  " working file was modified since the last recorded commit (E_CONFLICT: a revert would",
41539
41659
  " overwrite changes that were never recorded). The restored package is written only after",
41540
41660
  " it passes the OOXML validation gate; a commit failure restores the pre-command bytes.",
41541
- "Tracked package files only: document.xml, [Content_Types].xml,",
41542
- " word/{styles,numbering,settings,comments,footnotes,endnotes}.xml, header/footer parts and",
41543
- " their rels, the document rels, and word/{media,charts,embeddings,theme}/; parts outside",
41544
- " that set (e.g. docProps/) keep the working-file bytes.",
41661
+ "Every package part is recorded and restored (document.xml, [Content_Types].xml, styles, comments",
41662
+ " and their metadata, word/{media,charts,embeddings,theme}/, docProps/, custom XML, ...); a part",
41663
+ " the commit does not have is removed. Commits recorded by ASTra versions before 0.4.2 hold only",
41664
+ " document.xml, [Content_Types].xml, word/{styles,numbering,settings,comments,footnotes,endnotes}.xml,",
41665
+ " header/footer parts and their rels, the document rels, and word/{media,charts,embeddings,theme}/;",
41666
+ " restoring from one prints a warning, and parts outside that set (e.g. docProps/) keep the",
41667
+ " working-file bytes.",
41545
41668
  "Exit 0 ok \xB7 1 conflict/validation/IO failure (nothing written) \xB7 2 bad args/absent sidecar."
41546
41669
  ].join("\n"),
41547
41670
  redo: [
@@ -41601,10 +41724,10 @@ function sidecarGitState(file) {
41601
41724
  function hasHistorySidecar(file) {
41602
41725
  return sidecarGitState(file) === "repo";
41603
41726
  }
41604
- function trackedExtras(snapshot3) {
41727
+ function trackedExtras(snapshot3, isTracked = isTrackedPackageFile) {
41605
41728
  const out = /* @__PURE__ */ new Map();
41606
41729
  for (const [name2, buf] of snapshot3) {
41607
- if (name2 !== "word/document.xml" && isTrackedPackageFile(name2)) out.set(name2, buf);
41730
+ if (name2 !== "word/document.xml" && isTracked(name2)) out.set(name2, buf);
41608
41731
  }
41609
41732
  return out;
41610
41733
  }
@@ -45363,7 +45486,7 @@ async function divergedFiles(file, parser2, commitOid) {
45363
45486
  const recorded = await checkoutCommit(file, commitOid);
45364
45487
  const snapshot3 = parser2.capturePackageSnapshot();
45365
45488
  const liveXml = snapshot3.get("word/document.xml")?.toString("utf8") ?? "";
45366
- return packageDiff(liveXml, trackedExtras(snapshot3), recorded.xml, recorded.extra);
45489
+ return packageDiff(liveXml, trackedExtras(snapshot3, trackedPredicateFor(recorded)), recorded.xml, recorded.extra);
45367
45490
  }
45368
45491
  function sidecarWarning(commits) {
45369
45492
  const carried = commits.warning;
@@ -45458,12 +45581,13 @@ function parseDocStepsArgs(out, cmd, args) {
45458
45581
  async function restoreAndCommit(opts) {
45459
45582
  const { cmd, file, parser: parser2, prevBytes, target, targetOid, commitMessage } = opts;
45460
45583
  const snapshot3 = parser2.capturePackageSnapshot();
45584
+ const isTrackedForTarget = trackedPredicateFor(target);
45461
45585
  for (const key of [...snapshot3.keys()]) {
45462
- if (key !== "word/document.xml" && isTrackedPackageFile(key)) snapshot3.delete(key);
45586
+ if (key !== "word/document.xml" && isTrackedForTarget(key)) snapshot3.delete(key);
45463
45587
  }
45464
45588
  for (const [name2, data] of target.extra) snapshot3.set(name2, data);
45465
45589
  snapshot3.set("word/document.xml", Buffer.from(target.xml, "utf8"));
45466
- parser2.restorePackageSnapshot(snapshot3);
45590
+ parser2.restorePackageSnapshot(snapshot3, { registerCoreProperties: !target.complete });
45467
45591
  const buf = parser2.rezip();
45468
45592
  const candidate = await validateDocxBuffer(buf, { maxErrors: BASELINE_MAX_ERRORS });
45469
45593
  if (!candidate.ok && judgeAgainstBaseline(candidate, buildBaseline(candidate)).kind !== "tolerated") {
@@ -45635,7 +45759,7 @@ async function cmdPreviewRevert(out, args) {
45635
45759
  const target = await checkoutCommit(file, targetOid);
45636
45760
  const snapshot3 = parser2.capturePackageSnapshot();
45637
45761
  const liveXml = snapshot3.get("word/document.xml")?.toString("utf8") ?? "";
45638
- const affectedFiles = packageDiff(liveXml, trackedExtras(snapshot3), target.xml, target.extra);
45762
+ const affectedFiles = packageDiff(liveXml, trackedExtras(snapshot3, trackedPredicateFor(target)), target.xml, target.extra);
45639
45763
  const changed = await divergedFiles(file, parser2, commits[0].oid);
45640
45764
  const warning = sidecarWarning(commits);
45641
45765
  const head = commits[0].oid;
@@ -45653,6 +45777,7 @@ async function cmdPreviewRevert(out, args) {
45653
45777
  changed.length > 0 ? ` working file: DIVERGED from head ${shortOid(head)} \u2014 a real revert would refuse E_CONFLICT (changed: ${shownList(changed)})` : ` working file: clean at head ${shortOid(head)} \u2014 a real revert would proceed`
45654
45778
  );
45655
45779
  if (warning) printStderr(`${CLI_NAME}: warning: ${warning}`);
45780
+ if (!target.complete) printStderr(`${CLI_NAME}: warning: ${INCOMPLETE_SNAPSHOT_WARNING}`);
45656
45781
  printJson(out, {
45657
45782
  ok: true,
45658
45783
  command: "preview-revert",
@@ -45666,7 +45791,9 @@ async function cmdPreviewRevert(out, args) {
45666
45791
  totalCommits: commits.length,
45667
45792
  diverged: changed.length > 0,
45668
45793
  divergedFiles: changed,
45669
- warning
45794
+ warning,
45795
+ snapshotComplete: target.complete,
45796
+ ...target.complete ? {} : { restoreWarning: INCOMPLETE_SNAPSHOT_WARNING }
45670
45797
  });
45671
45798
  return EXIT_OK;
45672
45799
  } catch (err) {
@@ -45699,6 +45826,7 @@ async function cmdRevert(out, args) {
45699
45826
  out,
45700
45827
  `REVERT OK ${file}: restored ${shortOid(targetOid)} "${subject}" (steps=${steps}); new commit ${shortOid(result.commitOid)}`
45701
45828
  );
45829
+ if (!target.complete) printStderr(`${CLI_NAME}: warning: ${INCOMPLETE_SNAPSHOT_WARNING}`);
45702
45830
  printJson(out, {
45703
45831
  ok: true,
45704
45832
  command: "revert",
@@ -45706,7 +45834,9 @@ async function cmdRevert(out, args) {
45706
45834
  steps,
45707
45835
  revertedToCommit: targetOid,
45708
45836
  commitOid: result.commitOid,
45709
- commitMessage: result.commitMessage
45837
+ commitMessage: result.commitMessage,
45838
+ snapshotComplete: target.complete,
45839
+ ...target.complete ? {} : { restoreWarning: INCOMPLETE_SNAPSHOT_WARNING }
45710
45840
  });
45711
45841
  return EXIT_OK;
45712
45842
  } catch (err) {
@@ -45752,13 +45882,16 @@ async function cmdRedo(out, args) {
45752
45882
  out,
45753
45883
  `REDO OK ${file}: re-applied the state replaced by revert ${shortOid(headOid)}; new commit ${shortOid(result.commitOid)}`
45754
45884
  );
45885
+ if (!target.complete) printStderr(`${CLI_NAME}: warning: ${INCOMPLETE_SNAPSHOT_WARNING}`);
45755
45886
  printJson(out, {
45756
45887
  ok: true,
45757
45888
  command: "redo",
45758
45889
  file,
45759
45890
  restoredCommit: targetOid,
45760
45891
  commitOid: result.commitOid,
45761
- commitMessage: result.commitMessage
45892
+ commitMessage: result.commitMessage,
45893
+ snapshotComplete: target.complete,
45894
+ ...target.complete ? {} : { restoreWarning: INCOMPLETE_SNAPSHOT_WARNING }
45762
45895
  });
45763
45896
  return EXIT_OK;
45764
45897
  } catch (err) {
@@ -13,8 +13,12 @@ export type SidecarGitState = 'repo' | 'stray' | 'absent';
13
13
  export declare function sidecarGitState(file: string): SidecarGitState;
14
14
  /** Is a valid sidecar chain present for this document? */
15
15
  export declare function hasHistorySidecar(file: string): boolean;
16
- /** Tracked package files (everything but document.xml) from a live snapshot. */
17
- export declare function trackedExtras(snapshot: Map<string, Buffer>): Map<string, Buffer>;
16
+ /**
17
+ * Tracked package files (everything but document.xml) from a live snapshot. `isTracked` selects the
18
+ * rule: the default is the complete-snapshot rule; pass trackedPredicateFor(commit) to compare with a
19
+ * commit recorded by an older ASTra version.
20
+ */
21
+ export declare function trackedExtras(snapshot: Map<string, Buffer>, isTracked?: (name: string) => boolean): Map<string, Buffer>;
18
22
  export declare function extrasRecord(extras: Map<string, Buffer>): Record<string, Buffer>;
19
23
  export type BatchRevisionResult = {
20
24
  recorded: true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@attentionform/astra",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Command-line tool for OOXML (.docx) document engineering",
5
5
  "main": "dist/cli.js",
6
6
  "types": "dist/packages/cli/src/cli.d.ts",
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@attentionform/astra-engine": "^0.4.0",
47
- "@attentionform/ooxml-validator": "^0.4.1",
47
+ "@attentionform/ooxml-validator": "^0.4.2",
48
48
  "@xmldom/xmldom": "0.8.15",
49
49
  "adm-zip": "0.6.1",
50
50
  "isomorphic-git": "^1.25.0",