@lazyingart/agintiflow 0.20.301 → 0.20.302

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
@@ -39,6 +39,7 @@ The two screenshots below match the current website hero: terminal-first launch
39
39
  | Website | [https://flow.lazying.art](https://flow.lazying.art) |
40
40
  | GitHub | [https://github.com/lazyingart/AgInTiFlow](https://github.com/lazyingart/AgInTiFlow) |
41
41
  | npm | [https://www.npmjs.com/package/@lazyingart/agintiflow](https://www.npmjs.com/package/@lazyingart/agintiflow) |
42
+ | npm package chart | [https://npm.chart.dev/@lazyingart/agintiflow](https://npm.chart.dev/@lazyingart/agintiflow) |
42
43
  | AAPS npm | [https://www.npmjs.com/package/@lazyingart/aaps](https://www.npmjs.com/package/@lazyingart/aaps) |
43
44
  | Product positioning | [references/agintiflow-product-positioning.md](references/agintiflow-product-positioning.md) |
44
45
  | Full archived README reference | [references/notes/readme-full-reference-2026-05-05.md](references/notes/readme-full-reference-2026-05-05.md) |
@@ -588,3 +588,65 @@ fallback derives a concise public answer from the successful bounded command
588
588
  output. JSON status snapshots summarize visible delivered/retrying schedule
589
589
  states, ingress reachability, queue counts, and top-level health while keeping
590
590
  the original SCS checks for genuinely unfinished agent work.
591
+
592
+ ### Retained tmux Git completion evidence
593
+
594
+ `labcanvas-mix2s-recovery-review-061` exposed a restart boundary in supervised
595
+ project work. The retained AgInTi session had sent a marked commit command to a
596
+ project-owned tmux pane, captured the canonical successful commit output, and
597
+ left the target worktree clean at `90d61bb`. SCS still rejected completion
598
+ after resume because the durable event ledger did not reconstruct that tmux
599
+ sequence as a consequential Git action.
600
+
601
+ AgInTiFlow now binds an explicit uppercase start marker and requested Git
602
+ action to the exact tmux target, accepts a commit only when the same pane later
603
+ contains canonical commit output plus a zero exit marker, and preserves the
604
+ tail of truncated captures. Restart reconciliation replays the ordered
605
+ `tool.started`, successful `tmux_send_keys`, and successful same-pane capture
606
+ events to recover goal- and mutation-revision-scoped evidence. Failed sends,
607
+ wrong panes, stale markers, and nonzero exits remain rejected.
608
+
609
+ The real retained session then finished without repeating the commit or any
610
+ external side effect. The complete npm suite, focused evidence regressions, and
611
+ registry installation passed. The fix is released as AgInTiFlow `0.20.301`.
612
+
613
+ ### Context-complete document repair and semantic validation
614
+
615
+ Memo retests `082` through `084` exercised a retained LocalLLM document task
616
+ against a realistic mixed-language chat history. The task required a compiled
617
+ PDF, faithful current-state reconciliation, and a committed artifact. The
618
+ campaign exposed three independent AgInTiFlow runtime gaps rather than a
619
+ LabCanvas-specific routing problem.
620
+
621
+ In `082`, the TeX compiler repeatedly identified one exact line containing an
622
+ unescaped underscore. After several valid repairs, the provider ignored the
623
+ remaining narrow patch schema and attempted to replace the whole document.
624
+ AgInTiFlow now derives the mechanically safe replacement from the compiler
625
+ diagnostic and current source, constrains the trusted patch contract to that
626
+ replacement, and can recover an invalid broad model patch into the exact
627
+ evidence-derived edit. The recovery is limited to one active
628
+ revision-bound producer diagnostic and cannot generalize into an arbitrary
629
+ rewrite.
630
+
631
+ In `083`, the provider repaired a quality defect by deleting the remainder of
632
+ the TeX document. Patch anchoring now recognizes LaTeX section boundaries and
633
+ binds a defect to its containing section. A replacement is rejected when it
634
+ changes the net balance of revision-bound LaTeX environments, preventing a
635
+ local list repair from dropping later sections or environment terminators.
636
+
637
+ In `084`, the provider produced and committed a syntactically valid PDF that
638
+ described the document-generation process but omitted the substantive source
639
+ topics. Document validation now extracts bounded salient topic anchors from a
640
+ complete-context source and requires representative coverage when the request
641
+ explicitly asks for complete, reconciled, or context-complete output. It also
642
+ preserves unverified source status, rejects contradictory completion claims,
643
+ validates LaTeX source structure, and keeps decimal measurements such as
644
+ `0.25 mm` intact during status parsing.
645
+
646
+ The release also preserves requested artifact and project contracts across
647
+ compaction, replays failed requested PDF producers without repeating successful
648
+ side effects, retains exact compiler lines through bounded retries, rejects
649
+ append-only repairs when replacement is required, and caps repair schemas so a
650
+ small local model receives only the actionable tool surface. Focused coding,
651
+ truthful-completion, writing-routing, progressive-tool, document-quality, and
652
+ dynamic-budget suites pass. These fixes are prepared for AgInTiFlow `0.20.302`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.301",
3
+ "version": "0.20.302",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
6
6
  "license": "Apache-2.0",
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
7
7
  import {
8
8
  buildModelTimeoutRetryMessages,
9
9
  genericArtifactFilenameBlock,
10
+ goalClearlyAllowsOverwrite,
10
11
  modelTimeoutExhaustionRoute,
11
12
  modelTimeoutRetryRoute,
12
13
  normalizeNoMatchQueryResult,
@@ -917,6 +918,14 @@ try {
917
918
  exactPdfHeaderAuditPolicy.writesWorkspace === false,
918
919
  "a bounded workspace-local PDF header audit incorrectly required destructive host permission"
919
920
  );
921
+ const pdfinfoPolicy = evaluateCommandPolicy("pdfinfo report.pdf", hostWorkspacePolicy);
922
+ assert(
923
+ pdfinfoPolicy.allowed &&
924
+ pdfinfoPolicy.category === "read-only" &&
925
+ pdfinfoPolicy.needsNetwork === false &&
926
+ pdfinfoPolicy.writesWorkspace === false,
927
+ "plain workspace-local pdfinfo inspection incorrectly required host permission"
928
+ );
920
929
  for (const unsafeXxdCommand of [
921
930
  "xxd -r - output.bin",
922
931
  "xxd report.pdf output.hex",
@@ -1181,12 +1190,40 @@ try {
1181
1190
  );
1182
1191
  assert(pdflatexCompilePolicy.allowed, "workspace-local pdflatex compile should be allowed without package installs");
1183
1192
  assert(pdflatexCompilePolicy.category === "toolchain", "workspace-local pdflatex compile should be classified as toolchain");
1193
+ for (const engine of ["xelatex", "lualatex"]) {
1194
+ const unicodeLatexCompilePolicy = evaluateCommandPolicy(
1195
+ `${engine} -interaction=nonstopmode -halt-on-error main.tex`,
1196
+ hostWorkspacePolicy
1197
+ );
1198
+ assert(unicodeLatexCompilePolicy.allowed, `workspace-local ${engine} compile should be allowed without destructive host access`);
1199
+ assert(unicodeLatexCompilePolicy.category === "toolchain", `workspace-local ${engine} compile should be classified as toolchain`);
1200
+ }
1184
1201
  const latexmkCompilePolicy = evaluateCommandPolicy(
1185
1202
  'cd profile-latex-20260506 && latexmk -pdf main.tex 2>&1; echo "LATEXMK_EXIT:$?"',
1186
1203
  dockerWorkspaceNoInstallsPolicy
1187
1204
  );
1188
1205
  assert(latexmkCompilePolicy.allowed, "workspace-local latexmk compile should be allowed without package installs");
1189
1206
  assert(latexmkCompilePolicy.category === "toolchain", "workspace-local latexmk compile should be classified as toolchain");
1207
+ const latexmkSynctexCompilePolicy = evaluateCommandPolicy(
1208
+ "latexmk -synctex=1 -interaction=nonstopmode main.tex",
1209
+ dockerWorkspaceNoInstallsPolicy
1210
+ );
1211
+ assert(
1212
+ latexmkSynctexCompilePolicy.allowed,
1213
+ "bounded workspace-local latexmk synctex compile should be allowed"
1214
+ );
1215
+ assert(
1216
+ latexmkSynctexCompilePolicy.category === "toolchain",
1217
+ "bounded workspace-local latexmk synctex compile should be classified as toolchain"
1218
+ );
1219
+ assert(
1220
+ goalClearlyAllowsOverwrite("Remove the unrelated article and produce the requested memo."),
1221
+ "explicit removal of a wrong artifact did not authorize its coherent replacement"
1222
+ );
1223
+ assert(
1224
+ goalClearlyAllowsOverwrite("删除错误文件后生成正确报告。"),
1225
+ "explicit Chinese deletion request did not authorize coherent replacement"
1226
+ );
1190
1227
  const pythonUnittestPolicy = evaluateCommandPolicy(
1191
1228
  "python3 -m unittest test_data_helper.py 2>&1",
1192
1229
  dockerWorkspaceNoInstallsPolicy
@@ -1,11 +1,20 @@
1
1
  import assert from "node:assert/strict";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
2
5
 
3
6
  import {
7
+ collectDocumentSourceDocuments,
8
+ currentStateRequested,
9
+ documentArtifactVersioningDefect,
4
10
  evaluateCurrentStateText,
5
11
  evaluateDocumentConsistency,
6
12
  evaluateExtractedDocumentText,
13
+ evaluateLatexSourceStructure,
7
14
  evaluatePdfPageBalance,
8
15
  evaluatePdfTextBounds,
16
+ evaluateSourceTopicCoverage,
17
+ evaluateUnverifiedSourceCompletionClaims,
9
18
  extractSupersededLiterals,
10
19
  } from "../src/document-artifact-quality.js";
11
20
  import { evaluateSpreadsheetStructure } from "../src/spreadsheet-artifact-quality.js";
@@ -82,6 +91,121 @@ const currentDocument = evaluateCurrentStateText({
82
91
  });
83
92
  assert.equal(currentDocument.ok, true, "authoritative current-state prose was rejected");
84
93
 
94
+ const multilingualUnverifiedSource = [
95
+ "09:40 Lachlan: 我把一本显微成像的 PDF 发到群里了。请保存到 Downloads/Books,并同步 Nutstore 私人备份。",
96
+ "17:40 Lachlan: 今日真正完成:JLC 已付款;adapter 已验证;书籍归档请求已收到。书是否真的同步成功需要查证后才能写完成。",
97
+ ].join("\n");
98
+ const unsupportedBookCompletion = evaluateUnverifiedSourceCompletionClaims({
99
+ sourceText: multilingualUnverifiedSource,
100
+ outputText: "Completed Today\nBook on microscopy archived to Downloads/Books and synced to Nutstore.",
101
+ });
102
+ assert.equal(unsupportedBookCompletion.ok, false, "an unverified cross-language backup was reported as completed");
103
+ assert(
104
+ unsupportedBookCompletion.defects.some((item) => item.code === "source-unverified-completion-claim"),
105
+ "an unsupported completion claim did not produce a precise source-status defect"
106
+ );
107
+ const preservedBookUncertainty = evaluateUnverifiedSourceCompletionClaims({
108
+ sourceText: multilingualUnverifiedSource,
109
+ outputText: "Pending\nVerify whether the microscopy book reached Nutstore; the archive request was received only.",
110
+ });
111
+ assert.equal(preservedBookUncertainty.ok, true, "a correctly preserved unverified source state was rejected");
112
+
113
+ const contextCompleteChatSource = [
114
+ "08:07 Lachlan: Instagram retry must reuse the existing LazyEdit output.",
115
+ "09:02 Sunny: Dataset publication permission remains unconfirmed.",
116
+ "09:40 Lachlan: Archive the microscopy PDF and verify the Nutstore backup.",
117
+ "10:15 Lachlan: Record the 5V 400mA PCB idea only; do not manufacture it.",
118
+ "14:04 Lachlan: The M10-to-M6 adapter is assembled and verified.",
119
+ "17:40 Lachlan: JLC is paid, but the book backup is still unverified.",
120
+ ].join("\n");
121
+ const genericProcessOnlyReport = evaluateSourceTopicCoverage({
122
+ goal: "Read the complete chat history and produce a context-complete report.",
123
+ sourceText: contextCompleteChatSource,
124
+ outputText:
125
+ "This report read the source and created a structured LaTeX document. The PDF is ready for verification.",
126
+ });
127
+ assert.equal(
128
+ genericProcessOnlyReport.ok,
129
+ false,
130
+ "a process-only report passed despite omitting every salient source topic"
131
+ );
132
+ assert(
133
+ genericProcessOnlyReport.defects.some(
134
+ (item) => item.code === "source-topic-coverage-incomplete"
135
+ ),
136
+ "missing source topic coverage did not produce a precise semantic defect"
137
+ );
138
+ const contextGroundedReport = evaluateSourceTopicCoverage({
139
+ goal: "Read the complete chat history and produce a context-complete report.",
140
+ sourceText: contextCompleteChatSource,
141
+ outputText: [
142
+ "Retry Instagram from the existing LazyEdit result only.",
143
+ "Wait for Sunny before dataset publication and verify the Nutstore book backup.",
144
+ "JLC is already paid and the M10-to-M6 adapter is verified.",
145
+ "Keep the 5V 400mA PCB item as a note; do not manufacture it.",
146
+ ].join(" "),
147
+ });
148
+ assert.equal(
149
+ contextGroundedReport.ok,
150
+ true,
151
+ `a report grounded in the source topics was rejected: ${JSON.stringify(contextGroundedReport)}`
152
+ );
153
+ const contradictoryBookCompletion = evaluateUnverifiedSourceCompletionClaims({
154
+ sourceText: multilingualUnverifiedSource,
155
+ outputText: "Completed Today\nMicroscopy PDF archived to Downloads/Books and synced to Nutstore (verification pending).",
156
+ });
157
+ assert.equal(
158
+ contradictoryBookCompletion.ok,
159
+ false,
160
+ "a positive completion claim escaped validation merely by appending a pending-verification qualifier"
161
+ );
162
+ const unrelatedVerifiedCompletion = evaluateUnverifiedSourceCompletionClaims({
163
+ sourceText: multilingualUnverifiedSource,
164
+ outputText: "Completed Today\nJLC order paid and adapter verified.",
165
+ });
166
+ assert.equal(unrelatedVerifiedCompletion.ok, true, "a separately verified completion was tied to an unrelated source uncertainty");
167
+ const repeatedSpeakerDoesNotJoinStatuses = evaluateUnverifiedSourceCompletionClaims({
168
+ sourceText: [
169
+ "09:06 Lachlan: 在 Sunny 明确确认许可前不要上传数据。",
170
+ "10:43 Lachlan: 已支付的 JLC 订单不需要再操作。",
171
+ ].join("\n"),
172
+ outputText: "Completed Today\nJLC order paid and confirmed.",
173
+ });
174
+ assert.equal(
175
+ repeatedSpeakerDoesNotJoinStatuses.ok,
176
+ true,
177
+ "transport sender labels connected an unresolved item to an unrelated verified completion"
178
+ );
179
+ const transitiveChatTermsDoNotJoinStatuses = evaluateUnverifiedSourceCompletionClaims({
180
+ sourceText: [
181
+ "09:06 Lachlan: 在 Sunny 明确确认许可前不要上传数据。",
182
+ "10:43 Lachlan: 已支付的 JLC 订单不需要再操作。",
183
+ "14:35 Lachlan: 每日 memo 要整理完成事项、等待事项和明确边界。",
184
+ "16:28 Lachlan: 数据上传任务状态是等待 Sunny,明天中午检查。",
185
+ ].join("\n"),
186
+ outputText: "Daily Memo\nCompleted Today\nJLC order paid and confirmed.",
187
+ });
188
+ assert.equal(
189
+ transitiveChatTermsDoNotJoinStatuses.ok,
190
+ true,
191
+ "transitive chat vocabulary connected an unresolved dataset to an unrelated JLC completion"
192
+ );
193
+ const adjacentPronounCompletion = evaluateUnverifiedSourceCompletionClaims({
194
+ sourceText: [
195
+ "13:12 System: Instagram automation reports login required; no upload started.",
196
+ "13:18 Lachlan: 这是阻塞,不是失败。等登录恢复后只重试 Instagram。",
197
+ ].join("\n"),
198
+ outputText: [
199
+ "Instagram upload is blocked by login; retry only after login is restored.",
200
+ "Do not re-publish the bird video on Instagram; it has already been posted.",
201
+ ].join("\n"),
202
+ });
203
+ assert.equal(
204
+ adjacentPronounCompletion.ok,
205
+ false,
206
+ "an adjacent pronoun completion claim contradicted a source-verified login blocker"
207
+ );
208
+
85
209
  const supersedesHistory = evaluateCurrentStateText({
86
210
  sourceText: source,
87
211
  outputText: "This document supersedes earlier planning notes. The current owner is Mei.",
@@ -117,6 +241,164 @@ const consistentActions = evaluateDocumentConsistency([
117
241
  ].join("\n"));
118
242
  assert.equal(consistentActions.ok, true, "matching declared and section action counts were rejected");
119
243
 
244
+ const duplicatedProse = evaluateDocumentConsistency(
245
+ "Ideas: the concept of the concept of an instrument as agent."
246
+ );
247
+ assert.equal(duplicatedProse.ok, false, "an adjacent duplicated prose fragment was accepted");
248
+ assert(
249
+ duplicatedProse.defects.some((item) => item.code === "duplicated-prose-fragment"),
250
+ "an adjacent duplicated prose fragment did not produce its quality defect"
251
+ );
252
+ const duplicatedWrappedBullet = evaluateDocumentConsistency([
253
+ "Future Ideas",
254
+ "\u0088 Develop a conceptual article titled Turning Instruments into Collaborators, using case studies",
255
+ "from closed-loop microscopy, active spectroscopy, and autonomous experimentation.",
256
+ "\u0088 Explore another bounded idea for the roadmap and preserve it for later review.",
257
+ "\u0088 Develop a conceptual article titled Turning Instruments into Collaborators, using case studies",
258
+ "from closed-loop microscopy, active spectroscopy, and autonomous experimentation.",
259
+ ].join("\n"));
260
+ assert.equal(
261
+ duplicatedWrappedBullet.ok,
262
+ false,
263
+ "a repeated non-adjacent wrapped PDF bullet was accepted"
264
+ );
265
+ assert(
266
+ duplicatedWrappedBullet.defects.some(
267
+ (item) => item.code === "duplicated-prose-fragment"
268
+ ),
269
+ "a repeated wrapped PDF bullet did not produce its quality defect"
270
+ );
271
+
272
+ const unverifiedCompletedSection = evaluateDocumentConsistency([
273
+ "Completed Tasks",
274
+ "\u0088 A microscopy book was received and archived to the designated",
275
+ "local and cloud storage locations; verification is pending.",
276
+ "Pending Actions",
277
+ "\u0088 Verify the private backup evidence.",
278
+ ].join("\n"));
279
+ assert.equal(
280
+ unverifiedCompletedSection.ok,
281
+ false,
282
+ "wrapped unverified work inside a Completed section was accepted"
283
+ );
284
+ assert(
285
+ unverifiedCompletedSection.defects.some(
286
+ (item) => item.code === "completed-section-contains-unverified-work"
287
+ ),
288
+ "a wrapped completed/pending contradiction did not produce its section defect"
289
+ );
290
+
291
+ const contradictoryStatus = evaluateDocumentConsistency([
292
+ "Completed: JLC paid; book archive synchronized.",
293
+ "Pending: book archive evidence remains unverified.",
294
+ ].join("\n"));
295
+ assert.equal(contradictoryStatus.ok, false, "completed and unverified status for one subject was accepted");
296
+ assert(
297
+ contradictoryStatus.defects.some((item) => item.code === "completed-unverified-status-conflict"),
298
+ "a completed/unverified status conflict did not produce its quality defect"
299
+ );
300
+
301
+ const separateStatusBullets = evaluateDocumentConsistency([
302
+ "Executive Summary",
303
+ "This report outlines the current work without assigning status.",
304
+ "- Completed: authentication tests passed.",
305
+ "- Pending: deployment verification is waiting.",
306
+ ].join("\n"));
307
+ assert.equal(
308
+ separateStatusBullets.defects.some((item) => item.code === "completed-unverified-status-conflict"),
309
+ false,
310
+ "the status parser treated report preamble text as part of a wrapped status bullet"
311
+ );
312
+
313
+ const malformedLatexSource = evaluateLatexSourceStructure([
314
+ "\\documentclass{article}",
315
+ "\\begin{document}",
316
+ "Current report.",
317
+ "\\end{document}",
318
+ "Duplicated report suffix.",
319
+ "\\end{document}",
320
+ ].join("\n"));
321
+ assert.equal(malformedLatexSource.ok, false, "duplicated LaTeX document suffix was accepted");
322
+ assert(
323
+ malformedLatexSource.defects.some((item) => item.code === "latex-content-after-document-end") &&
324
+ malformedLatexSource.defects.some((item) => item.code === "latex-duplicate-document-end"),
325
+ "malformed LaTeX source did not report both trailing content and duplicate document termination"
326
+ );
327
+ const validLatexSource = evaluateLatexSourceStructure([
328
+ "\\documentclass{article}",
329
+ "\\begin{document}",
330
+ "Current report with an escaped \\% sign.",
331
+ "\\end{document}",
332
+ "% \\end{document} in a comment is harmless.",
333
+ ].join("\n"));
334
+ assert.equal(validLatexSource.ok, true, "valid LaTeX source with trailing comments was rejected");
335
+ const duplicateLatexDate = evaluateLatexSourceStructure([
336
+ "\\documentclass{article}",
337
+ "\\date{2026-08-23}",
338
+ "% \\date{ignored comment}",
339
+ "\\date{2026-09-01}",
340
+ "\\begin{document}",
341
+ "Current report.",
342
+ "\\end{document}",
343
+ ].join("\n"));
344
+ assert.equal(duplicateLatexDate.ok, false, "duplicate active LaTeX date declarations were accepted");
345
+ assert(
346
+ duplicateLatexDate.defects.some(
347
+ (item) => item.code === "latex-duplicate-singleton-command" && item.command === "date"
348
+ ),
349
+ "duplicate LaTeX date declarations did not produce a precise source defect"
350
+ );
351
+
352
+ assert(
353
+ currentStateRequested(
354
+ "Read the complete chat history and reconcile corrections into the current memo.",
355
+ "更正:巴黎视频今天不要发布,上面这句取消。"
356
+ ),
357
+ "multilingual current-state correction language did not activate reconciliation checks"
358
+ );
359
+
360
+ const exactSourceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "aginti-document-source-"));
361
+ try {
362
+ await fs.writeFile(
363
+ path.join(exactSourceRoot, "chat_history.md"),
364
+ "Later correction: keep the publication blocked until login is restored.\n",
365
+ "utf8"
366
+ );
367
+ const exactSources = await collectDocumentSourceDocuments(
368
+ exactSourceRoot,
369
+ ["chat_history.md"]
370
+ );
371
+ assert.deepEqual(
372
+ exactSources.map((item) => item.path),
373
+ ["chat_history.md"],
374
+ "an exact root-level source path was omitted from document validation"
375
+ );
376
+ } finally {
377
+ await fs.rm(exactSourceRoot, { recursive: true, force: true });
378
+ }
379
+
380
+ const versioningRoot = await fs.mkdtemp(path.join(os.tmpdir(), "aginti-document-versioning-"));
381
+ try {
382
+ const { execFile } = await import("node:child_process");
383
+ const { promisify } = await import("node:util");
384
+ const run = promisify(execFile);
385
+ await run("git", ["init", "-q"], { cwd: versioningRoot });
386
+ await fs.writeFile(path.join(versioningRoot, "report.pdf"), "fixture", "utf8");
387
+ assert.equal(
388
+ (await documentArtifactVersioningDefect(versioningRoot, "report.pdf"))?.code,
389
+ "document-artifact-not-versioned",
390
+ "an untracked final document artifact passed commit-aware validation"
391
+ );
392
+ await run("git", ["add", "--", "report.pdf"], { cwd: versioningRoot });
393
+ assert.equal(
394
+ await documentArtifactVersioningDefect(versioningRoot, "report.pdf"),
395
+ null,
396
+ "a staged document artifact was reported as unversioned"
397
+ );
398
+ } finally {
399
+ await fs.rm(versioningRoot, { recursive: true, force: true });
400
+ }
401
+
120
402
  function page(
121
403
  words,
122
404
  { height = 842, startY = 60, endY = 700, heading = "Section", wordHeight = 10 } = {}
@@ -169,6 +451,17 @@ assert.equal(corruptExtractedText.ok, false, "control and replacement glyphs wer
169
451
  assert.deepEqual(corruptExtractedText.codePoints, [0x16, 0xfffd]);
170
452
  assert.equal(corruptExtractedText.defects[0]?.code, "corrupt-extracted-text");
171
453
 
454
+ const latexBulletExtraction = evaluateExtractedDocumentText(
455
+ "Completed tasks\n\u0088 First item\n\u0088 Second item\n"
456
+ );
457
+ assert.equal(
458
+ latexBulletExtraction.ok,
459
+ true,
460
+ "the bounded pdflatex list-marker extraction glyph was treated as document corruption"
461
+ );
462
+ const embeddedControl = evaluateExtractedDocumentText("broken\u0088inside");
463
+ assert.equal(embeddedControl.ok, false, "an embedded C1 control byte was accepted as a list marker");
464
+
172
465
  const boundedText = evaluatePdfTextBounds(`<doc>${page(20)}</doc>`);
173
466
  assert.equal(boundedText.ok, true, "text inside readable margins was rejected");
174
467