@tiangong-ai/cli 0.0.49 → 0.0.51

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.
@@ -1,8 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
- import { chmod, link, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, } from "node:fs/promises";
4
- import { hostname, homedir, platform } from "node:os";
5
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { chmod, link, lstat, mkdtemp, mkdir, open, readFile, readdir, realpath, rename, rm, } from "node:fs/promises";
4
+ import { hostname, homedir, platform, tmpdir } from "node:os";
5
+ import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
6
  import { setTimeout as sleep } from "node:timers/promises";
7
7
  import { CliError } from "../../errors.js";
8
8
  import { loadCapabilityDeclarations } from "./capabilities.js";
@@ -1033,7 +1033,7 @@ async function containsUnmanagedPathCliFallback(root) {
1033
1033
  };
1034
1034
  return inspectDirectory(root, 0);
1035
1035
  }
1036
- async function findAmbientExecutable(environment, executable) {
1036
+ async function findAmbientExecutable(environment, executable, options = {}) {
1037
1037
  const pathValue = environment.PATH;
1038
1038
  if (!pathValue)
1039
1039
  return null;
@@ -1042,7 +1042,7 @@ async function findAmbientExecutable(environment, executable) {
1042
1042
  if (!directory)
1043
1043
  continue;
1044
1044
  for (const suffix of suffixes) {
1045
- const candidate = join(directory, `${executable}${suffix}`);
1045
+ const candidate = resolve(directory, `${executable}${suffix}`);
1046
1046
  const info = await lstat(candidate).catch(() => undefined);
1047
1047
  if (!info || (!info.isFile() && !info.isSymbolicLink()))
1048
1048
  continue;
@@ -1050,8 +1050,12 @@ async function findAmbientExecutable(environment, executable) {
1050
1050
  if (!resolved)
1051
1051
  continue;
1052
1052
  const resolvedInfo = await lstat(resolved).catch(() => undefined);
1053
- if (resolvedInfo?.isFile() && !resolvedInfo.isSymbolicLink())
1054
- return { path: resolved, ignoredByExactInvocation: true };
1053
+ if (resolvedInfo?.isFile() && !resolvedInfo.isSymbolicLink()) {
1054
+ const path = options.preserveLeafSymlink
1055
+ ? join(await realpath(dirname(candidate)), basename(candidate))
1056
+ : resolved;
1057
+ return { path, ignoredByExactInvocation: true };
1058
+ }
1055
1059
  }
1056
1060
  }
1057
1061
  return null;
@@ -1446,7 +1450,16 @@ export async function doctorResearchSetup(workspace, options = {}) {
1446
1450
  : `Run research setup credential set --id ${credential.id} --prompt --workspace ${root}.`,
1447
1451
  });
1448
1452
  }
1449
- await appendDependencyChecks(checks, plan, selected, runner, root, environment);
1453
+ const authoringRuntime = await resolveAuthoringRuntime(selected, environment);
1454
+ await appendDependencyChecks(checks, plan, selected, runner, root, environment, authoringRuntime);
1455
+ await appendAuthoringCanaryChecks(checks, {
1456
+ plan,
1457
+ selected,
1458
+ runner,
1459
+ root,
1460
+ environment,
1461
+ runtime: authoringRuntime,
1462
+ });
1450
1463
  let capabilityDoctor = null;
1451
1464
  const blockingBeforeLive = checks
1452
1465
  .map((check) => normalizeSetupDoctorCheck(check, selected))
@@ -3499,6 +3512,10 @@ function setupDomainReadiness(checks, scope) {
3499
3512
  return "NOT_REQUIRED";
3500
3513
  if (matching.some((check) => check.blocking && check.status === "fail"))
3501
3514
  return "BLOCKED";
3515
+ if (scope === "authoring" &&
3516
+ matching.some((check) => check.componentGate && check.status === "fail")) {
3517
+ return "BLOCKED";
3518
+ }
3502
3519
  return matching.some((check) => check.status !== "pass") ? "DEGRADED" : "READY";
3503
3520
  }
3504
3521
  function requireAbsoluteWorkspace(value) {
@@ -3749,6 +3766,7 @@ function installerEnvironment(source) {
3749
3766
  "SSL_CERT_FILE",
3750
3767
  "SSL_CERT_DIR",
3751
3768
  "NODE_EXTRA_CA_CERTS",
3769
+ "NODE_PATH",
3752
3770
  "CODEX_HOME",
3753
3771
  "CLAUDE_CONFIG_DIR",
3754
3772
  "VIRTUAL_ENV",
@@ -3917,7 +3935,99 @@ async function setupDoctorCheck(checks, id, category, callback, failureMinimumAc
3917
3935
  });
3918
3936
  }
3919
3937
  }
3920
- async function appendDependencyChecks(checks, plan, selected, runner, root, environment) {
3938
+ const AUTHORING_PYTHON_PROBES = {
3939
+ "authoring:defusedxml": {
3940
+ distribution: "defusedxml",
3941
+ module: "defusedxml",
3942
+ exactVersion: "0.7.1",
3943
+ },
3944
+ "authoring:lxml": { distribution: "lxml", module: "lxml.etree", exactVersion: null },
3945
+ "authoring:pillow": { distribution: "Pillow", module: "PIL", exactVersion: null },
3946
+ "authoring:python-pptx": {
3947
+ distribution: "python-pptx",
3948
+ module: "pptx",
3949
+ exactVersion: null,
3950
+ },
3951
+ "authoring:markitdown": {
3952
+ distribution: "markitdown",
3953
+ module: "markitdown",
3954
+ exactVersion: "0.1.7",
3955
+ },
3956
+ "authoring:pypdf": { distribution: "pypdf", module: "pypdf", exactVersion: null },
3957
+ "authoring:pdfplumber": {
3958
+ distribution: "pdfplumber",
3959
+ module: "pdfplumber",
3960
+ exactVersion: null,
3961
+ },
3962
+ "authoring:reportlab": {
3963
+ distribution: "reportlab",
3964
+ module: "reportlab",
3965
+ exactVersion: null,
3966
+ },
3967
+ "authoring:pdf2image": {
3968
+ distribution: "pdf2image",
3969
+ module: "pdf2image",
3970
+ exactVersion: null,
3971
+ },
3972
+ "authoring:openpyxl": {
3973
+ distribution: "openpyxl",
3974
+ module: "openpyxl",
3975
+ exactVersion: null,
3976
+ },
3977
+ "authoring:pandas": { distribution: "pandas", module: "pandas", exactVersion: null },
3978
+ };
3979
+ const AUTHORING_NODE_PROBES = {
3980
+ "authoring:node-docx": "docx",
3981
+ "authoring:node-pptxgenjs": "pptxgenjs",
3982
+ };
3983
+ const AUTHORING_COMMAND_PROBES = {
3984
+ "authoring:pandoc": "pandoc",
3985
+ "authoring:libreoffice": "soffice",
3986
+ "authoring:poppler": "pdftoppm",
3987
+ "authoring:zip": "zip",
3988
+ "authoring:unzip": "unzip",
3989
+ };
3990
+ async function resolveAuthoringRuntime(selected, environment) {
3991
+ if (!selected.some((skill) => skill.role === "post-closure-authoring"))
3992
+ return null;
3993
+ const resolveCommand = async (name) => (await findAmbientExecutable(environment, name, {
3994
+ preserveLeafSymlink: name === "python3",
3995
+ }))?.path ?? null;
3996
+ const [python, node, pandoc, soffice, pdftoppm, zip, unzip] = await Promise.all([
3997
+ resolveCommand("python3"),
3998
+ resolveCommand("node"),
3999
+ resolveCommand("pandoc"),
4000
+ resolveCommand("soffice"),
4001
+ resolveCommand("pdftoppm"),
4002
+ resolveCommand("zip"),
4003
+ resolveCommand("unzip"),
4004
+ ]);
4005
+ return { python, node, pandoc, soffice, pdftoppm, zip, unzip };
4006
+ }
4007
+ function requireAuthoringRuntimeExecutable(value, minimumAction) {
4008
+ if (!value)
4009
+ throw new Error(minimumAction);
4010
+ return value;
4011
+ }
4012
+ function authoringEnvironment(source, runtime) {
4013
+ const result = installerEnvironment(source);
4014
+ const commandDirectories = [
4015
+ runtime?.soffice,
4016
+ runtime?.pdftoppm,
4017
+ runtime?.pandoc,
4018
+ runtime?.zip,
4019
+ runtime?.unzip,
4020
+ ]
4021
+ .filter((value) => Boolean(value))
4022
+ .map(dirname);
4023
+ if (commandDirectories.length) {
4024
+ result.PATH = [...new Set(commandDirectories), result.PATH ?? ""]
4025
+ .filter(Boolean)
4026
+ .join(delimiter);
4027
+ }
4028
+ return result;
4029
+ }
4030
+ async function appendDependencyChecks(checks, plan, selected, runner, root, environment, authoringRuntime) {
3921
4031
  const dependencies = [
3922
4032
  ...new Map(selected
3923
4033
  .flatMap((skill) => skill.dependencies)
@@ -3937,7 +4047,7 @@ async function appendDependencyChecks(checks, plan, selected, runner, root, envi
3937
4047
  await setupDoctorCheck(checks, `dependency.${dependency.id}`, "dependency", async () => {
3938
4048
  if (dependency.id === "python-3.10") {
3939
4049
  const result = await runner({
3940
- command: "python3",
4050
+ command: authoringRuntime?.python ?? "python3",
3941
4051
  args: ["--version"],
3942
4052
  cwd: root,
3943
4053
  environment: installerEnvironment(environment),
@@ -3971,40 +4081,321 @@ async function appendDependencyChecks(checks, plan, selected, runner, root, envi
3971
4081
  }
3972
4082
  return `${dependency.requirement} is ready.`;
3973
4083
  }
3974
- if (dependency.id === "authoring:defusedxml") {
4084
+ const pythonProbe = AUTHORING_PYTHON_PROBES[dependency.id];
4085
+ if (pythonProbe) {
4086
+ const python = requireAuthoringRuntimeExecutable(authoringRuntime?.python ?? null, dependency.minimumAction);
3975
4087
  const result = await runner({
3976
- command: "python3",
4088
+ command: python,
3977
4089
  args: [
3978
4090
  "-c",
3979
- "import importlib.metadata as m, defusedxml; print(m.version('defusedxml'))",
4091
+ `import importlib.metadata as m; import ${pythonProbe.module}; print(m.version(${JSON.stringify(pythonProbe.distribution)}))`,
3980
4092
  ],
3981
4093
  cwd: root,
3982
4094
  environment: installerEnvironment(environment),
3983
4095
  timeoutMs: 15_000,
3984
4096
  });
3985
4097
  const observed = result.stdout.trim();
3986
- if (result.exitCode !== 0 || observed !== "0.7.1") {
3987
- throw new Error(`${dependency.requirement} is not active in the selected python3.`);
4098
+ if (result.exitCode !== 0 ||
4099
+ !observed ||
4100
+ (pythonProbe.exactVersion !== null && observed !== pythonProbe.exactVersion)) {
4101
+ throw new Error(`${dependency.requirement} is not active in the bound authoring Python.`);
3988
4102
  }
3989
- return `${dependency.requirement} is active.`;
4103
+ return `${dependency.requirement} is active (version ${observed}).`;
3990
4104
  }
3991
- if (dependency.id === "authoring:markitdown-pptx") {
4105
+ const nodeProbe = AUTHORING_NODE_PROBES[dependency.id];
4106
+ if (nodeProbe) {
4107
+ const node = requireAuthoringRuntimeExecutable(authoringRuntime?.node ?? null, dependency.minimumAction);
3992
4108
  const result = await runner({
3993
- command: "markitdown",
3994
- args: ["--version"],
4109
+ command: node,
4110
+ args: [
4111
+ "-e",
4112
+ `const resolved=require.resolve(${JSON.stringify(nodeProbe)}); process.stdout.write(resolved);`,
4113
+ ],
3995
4114
  cwd: root,
3996
- environment: installerEnvironment(environment),
4115
+ environment: authoringEnvironment(environment, authoringRuntime),
3997
4116
  timeoutMs: 15_000,
3998
4117
  });
3999
- if (result.exitCode !== 0 || !/\b0\.1\.7\b/.test(result.stdout)) {
4000
- throw new Error(`${dependency.requirement} is not available on PATH.`);
4118
+ if (result.exitCode !== 0 || !result.stdout.trim()) {
4119
+ throw new Error(`${dependency.requirement} is not resolvable by the bound authoring Node.`);
4001
4120
  }
4002
- return `${dependency.requirement} is available on PATH.`;
4121
+ return `${dependency.requirement} is resolvable.`;
4122
+ }
4123
+ const commandKey = AUTHORING_COMMAND_PROBES[dependency.id];
4124
+ if (commandKey) {
4125
+ const command = requireAuthoringRuntimeExecutable(authoringRuntime?.[commandKey] ?? null, dependency.minimumAction);
4126
+ const result = await runner({
4127
+ command,
4128
+ args: ["pdftoppm", "zip", "unzip"].includes(commandKey) ? ["-v"] : ["--version"],
4129
+ cwd: root,
4130
+ environment: authoringEnvironment(environment, authoringRuntime),
4131
+ timeoutMs: 15_000,
4132
+ });
4133
+ if (result.exitCode !== 0)
4134
+ throw new Error(`${dependency.requirement} is unavailable.`);
4135
+ return `${dependency.requirement} is executable.`;
4003
4136
  }
4004
4137
  throw new Error(`No automatic dependency check is declared for ${dependency.id}. ${dependency.minimumAction}`);
4005
4138
  }, dependency.minimumAction);
4006
4139
  }
4007
4140
  }
4141
+ async function appendAuthoringCanaryChecks(checks, input) {
4142
+ for (const skill of input.selected.filter((candidate) => candidate.role === "post-closure-authoring" &&
4143
+ ["anthropic.docx", "anthropic.pdf", "anthropic.pptx", "anthropic.xlsx"].includes(candidate.id))) {
4144
+ const failedPrerequisites = checks.filter((check) => check.id.startsWith("dependency.") &&
4145
+ check.status === "fail" &&
4146
+ setupCheckComponentIds(check, input.selected).includes(skill.id));
4147
+ if (failedPrerequisites.length) {
4148
+ checks.push({
4149
+ id: `authoring-canary.${skill.id}`,
4150
+ category: "authoring-canary",
4151
+ scope: "authoring",
4152
+ componentIds: [skill.id],
4153
+ status: "fail",
4154
+ detail: `Functional canary was not started because prerequisite checks failed: ${failedPrerequisites.map((check) => check.id).join(", ")}.`,
4155
+ minimumAction: "Resolve every declared runtime and dependency prerequisite, then rerun setup doctor; setup never installs them.",
4156
+ blocking: false,
4157
+ componentGate: true,
4158
+ requiredFor: [`component:${skill.id}`],
4159
+ skippedBecause: failedPrerequisites.map((check) => check.id).join(", "),
4160
+ });
4161
+ continue;
4162
+ }
4163
+ await setupDoctorCheck(checks, `authoring-canary.${skill.id}`, "authoring-canary", async () => {
4164
+ const temporary = await mkdtemp(join(tmpdir(), `tiangong-authoring-${skill.skillName}-`));
4165
+ try {
4166
+ await runAuthoringCanary({ ...input, skill, temporary });
4167
+ return `${skill.id} completed its exact-file synthetic functional canary.`;
4168
+ }
4169
+ finally {
4170
+ await rm(temporary, { recursive: true, force: true });
4171
+ }
4172
+ }, "Inspect the failed pinned helper/package/command step, repair the explicit authoring runtime outside this CLI, and rerun setup doctor.");
4173
+ Object.assign(checks.at(-1), {
4174
+ scope: "authoring",
4175
+ componentIds: [skill.id],
4176
+ blocking: false,
4177
+ componentGate: true,
4178
+ requiredFor: [`component:${skill.id}`],
4179
+ });
4180
+ }
4181
+ }
4182
+ async function runAuthoringCanary(input) {
4183
+ const environment = authoringEnvironment(input.environment, input.runtime);
4184
+ environment.PYTHONDONTWRITEBYTECODE = "1";
4185
+ if (input.skill.id === "anthropic.docx")
4186
+ return runDocxAuthoringCanary(input, environment);
4187
+ if (input.skill.id === "anthropic.pdf")
4188
+ return runPdfAuthoringCanary(input, environment);
4189
+ if (input.skill.id === "anthropic.pptx")
4190
+ return runPptxAuthoringCanary(input, environment);
4191
+ if (input.skill.id === "anthropic.xlsx")
4192
+ return runXlsxAuthoringCanary(input, environment);
4193
+ throw new Error("Unsupported authoring canary.");
4194
+ }
4195
+ async function runDocxAuthoringCanary(input, environment) {
4196
+ const node = requireAuthoringRuntimeExecutable(input.runtime?.node ?? null, "Node is missing.");
4197
+ const python = requireAuthoringRuntimeExecutable(input.runtime?.python ?? null, "Python is missing.");
4198
+ const pandoc = requireAuthoringRuntimeExecutable(input.runtime?.pandoc ?? null, "Pandoc is missing.");
4199
+ const pdftoppm = requireAuthoringRuntimeExecutable(input.runtime?.pdftoppm ?? null, "Poppler is missing.");
4200
+ const skillDirectory = await verifiedCompanionSkillDirectory(input.plan, input.skill);
4201
+ const validator = join(skillDirectory, "scripts", "office", "validate.py");
4202
+ const soffice = join(skillDirectory, "scripts", "office", "soffice.py");
4203
+ await Promise.all([
4204
+ requireRegularCompanionFile(input.root, validator, "DOCX validator"),
4205
+ requireRegularCompanionFile(input.root, soffice, "DOCX LibreOffice helper"),
4206
+ ]);
4207
+ const document = join(input.temporary, "docx-canary.docx");
4208
+ await authoringStep(input, environment, "docx.create", node, [
4209
+ "-e",
4210
+ DOCX_CANARY_SCRIPT,
4211
+ document,
4212
+ ]);
4213
+ await requireAuthoringArtifact(document, 128);
4214
+ await authoringStep(input, environment, "docx.validate", python, [validator, document]);
4215
+ const text = await authoringStep(input, environment, "docx.extract", pandoc, [
4216
+ "-t",
4217
+ "plain",
4218
+ document,
4219
+ ]);
4220
+ if (!text.stdout.includes("TIANGONG_DOCX_CANARY")) {
4221
+ throw new Error("DOCX text extraction did not return the exact canary sentinel.");
4222
+ }
4223
+ await authoringStep(input, environment, "docx.render", python, [soffice, "--headless", "--convert-to", "pdf", "--outdir", input.temporary, document], 60_000);
4224
+ const pdf = join(input.temporary, "docx-canary.pdf");
4225
+ await requireAuthoringArtifact(pdf, 128);
4226
+ const renderPrefix = join(input.temporary, "docx-page");
4227
+ await authoringStep(input, environment, "docx.rasterize", pdftoppm, [
4228
+ "-f",
4229
+ "1",
4230
+ "-singlefile",
4231
+ "-png",
4232
+ pdf,
4233
+ renderPrefix,
4234
+ ]);
4235
+ await requireAuthoringArtifact(`${renderPrefix}.png`, 64);
4236
+ }
4237
+ async function runPdfAuthoringCanary(input, environment) {
4238
+ const python = requireAuthoringRuntimeExecutable(input.runtime?.python ?? null, "Python is missing.");
4239
+ const pdftoppm = requireAuthoringRuntimeExecutable(input.runtime?.pdftoppm ?? null, "Poppler is missing.");
4240
+ const skillDirectory = await verifiedCompanionSkillDirectory(input.plan, input.skill);
4241
+ const converter = join(skillDirectory, "scripts", "convert_pdf_to_images.py");
4242
+ const validationImage = join(skillDirectory, "scripts", "create_validation_image.py");
4243
+ await Promise.all([
4244
+ requireRegularCompanionFile(input.root, converter, "PDF image converter"),
4245
+ requireRegularCompanionFile(input.root, validationImage, "PDF validation image helper"),
4246
+ ]);
4247
+ const pdf = join(input.temporary, "pdf-canary.pdf");
4248
+ await authoringStep(input, environment, "pdf.create", python, ["-c", PDF_CANARY_SCRIPT, pdf]);
4249
+ await requireAuthoringArtifact(pdf, 128);
4250
+ const parsed = await authoringStep(input, environment, "pdf.parse", python, [
4251
+ "-c",
4252
+ PDF_VERIFY_SCRIPT,
4253
+ pdf,
4254
+ ]);
4255
+ if (!parsed.stdout.includes("TIANGONG_PDF_CANARY")) {
4256
+ throw new Error("PDF parser did not return the exact canary sentinel.");
4257
+ }
4258
+ const renderPrefix = join(input.temporary, "pdf-page");
4259
+ await authoringStep(input, environment, "pdf.rasterize", pdftoppm, [
4260
+ "-f",
4261
+ "1",
4262
+ "-singlefile",
4263
+ "-png",
4264
+ pdf,
4265
+ renderPrefix,
4266
+ ]);
4267
+ await requireAuthoringArtifact(`${renderPrefix}.png`, 64);
4268
+ const pages = join(input.temporary, "pdf-pages");
4269
+ await mkdir(pages);
4270
+ await authoringStep(input, environment, "pdf.convert-images", python, [converter, pdf, pages]);
4271
+ const page = join(pages, "page_1.png");
4272
+ await requireAuthoringArtifact(page, 64);
4273
+ const fields = join(input.temporary, "pdf-fields.json");
4274
+ await writeJsonAtomic(fields, { form_fields: [] });
4275
+ const annotated = join(input.temporary, "pdf-validation.png");
4276
+ await authoringStep(input, environment, "pdf.validation-image", python, [
4277
+ validationImage,
4278
+ "1",
4279
+ fields,
4280
+ page,
4281
+ annotated,
4282
+ ]);
4283
+ await requireAuthoringArtifact(annotated, 64);
4284
+ }
4285
+ async function runPptxAuthoringCanary(input, environment) {
4286
+ const node = requireAuthoringRuntimeExecutable(input.runtime?.node ?? null, "Node is missing.");
4287
+ const python = requireAuthoringRuntimeExecutable(input.runtime?.python ?? null, "Python is missing.");
4288
+ const skillDirectory = await verifiedCompanionSkillDirectory(input.plan, input.skill);
4289
+ const validator = join(skillDirectory, "scripts", "office", "validate.py");
4290
+ const thumbnail = join(skillDirectory, "scripts", "thumbnail.py");
4291
+ await Promise.all([
4292
+ requireRegularCompanionFile(input.root, validator, "PPTX validator"),
4293
+ requireRegularCompanionFile(input.root, thumbnail, "PPTX thumbnail helper"),
4294
+ ]);
4295
+ const presentation = join(input.temporary, "pptx-canary.pptx");
4296
+ await authoringStep(input, environment, "pptx.create", node, [
4297
+ "-e",
4298
+ PPTX_CANARY_SCRIPT,
4299
+ presentation,
4300
+ ]);
4301
+ await requireAuthoringArtifact(presentation, 128);
4302
+ await authoringStep(input, environment, "pptx.validate", python, [validator, presentation]);
4303
+ const text = await authoringStep(input, environment, "pptx.extract", python, [
4304
+ "-m",
4305
+ "markitdown",
4306
+ presentation,
4307
+ ]);
4308
+ if (!text.stdout.includes("TIANGONG_PPTX_CANARY")) {
4309
+ throw new Error("PPTX MarkItDown did not return the exact canary sentinel.");
4310
+ }
4311
+ const thumbnailPrefix = join(input.temporary, "pptx-grid");
4312
+ await authoringStep(input, environment, "pptx.thumbnail", python, [thumbnail, presentation, thumbnailPrefix, "--cols", "1"], 60_000);
4313
+ await requireAuthoringArtifact(`${thumbnailPrefix}.jpg`, 64);
4314
+ }
4315
+ async function runXlsxAuthoringCanary(input, environment) {
4316
+ const python = requireAuthoringRuntimeExecutable(input.runtime?.python ?? null, "Python is missing.");
4317
+ const skillDirectory = await verifiedCompanionSkillDirectory(input.plan, input.skill);
4318
+ const recalc = join(skillDirectory, "scripts", "recalc.py");
4319
+ await requireRegularCompanionFile(input.root, recalc, "XLSX recalculation helper");
4320
+ const workbook = join(input.temporary, "xlsx-canary.xlsx");
4321
+ await authoringStep(input, environment, "xlsx.create", python, [
4322
+ "-c",
4323
+ XLSX_CANARY_SCRIPT,
4324
+ workbook,
4325
+ ]);
4326
+ await requireAuthoringArtifact(workbook, 128);
4327
+ const recalculated = await authoringStep(input, environment, "xlsx.recalculate", python, [recalc, workbook, "60"], 75_000);
4328
+ let result;
4329
+ try {
4330
+ result = JSON.parse(recalculated.stdout);
4331
+ }
4332
+ catch {
4333
+ throw new Error("XLSX recalculation helper did not return JSON.");
4334
+ }
4335
+ if (!isObject(result) || result.status !== "success" || result.total_errors !== 0) {
4336
+ throw new Error("XLSX recalculation helper did not complete without formula errors.");
4337
+ }
4338
+ const cached = await authoringStep(input, environment, "xlsx.verify-cache", python, [
4339
+ "-c",
4340
+ XLSX_VERIFY_SCRIPT,
4341
+ workbook,
4342
+ ]);
4343
+ if (cached.stdout.trim() !== "5") {
4344
+ throw new Error("XLSX recalculation did not persist the exact cached formula value.");
4345
+ }
4346
+ const markdown = await authoringStep(input, environment, "xlsx.extract", python, [
4347
+ "-m",
4348
+ "markitdown",
4349
+ workbook,
4350
+ ]);
4351
+ const normalizedMarkdown = markdown.stdout.replaceAll("\\_", "_");
4352
+ if (!normalizedMarkdown.includes("TIANGONG_XLSX_CANARY") ||
4353
+ !/(?:^|\D)5(?:\D|$)/u.test(normalizedMarkdown)) {
4354
+ throw new Error("XLSX MarkItDown did not return the exact canary sentinel and value.");
4355
+ }
4356
+ }
4357
+ async function authoringStep(input, environment, step, command, args, timeoutMs = 30_000) {
4358
+ const result = await input.runner({
4359
+ command,
4360
+ args,
4361
+ cwd: input.root,
4362
+ environment,
4363
+ timeoutMs,
4364
+ });
4365
+ if (result.exitCode !== 0) {
4366
+ throw new Error(`Authoring canary step failed (${step}).`);
4367
+ }
4368
+ return result;
4369
+ }
4370
+ async function requireAuthoringArtifact(path, minimumBytes) {
4371
+ const info = await lstat(path).catch(() => undefined);
4372
+ if (!info?.isFile() || info.isSymbolicLink() || info.size < minimumBytes) {
4373
+ throw new Error("Authoring canary did not create its exact expected regular artifact.");
4374
+ }
4375
+ }
4376
+ const DOCX_CANARY_SCRIPT = String.raw `
4377
+ const fs=require("node:fs"); const {Document,Packer,Paragraph}=require("docx");
4378
+ (async()=>{const out=process.argv[1]; const doc=new Document({sections:[{children:[new Paragraph("TIANGONG_DOCX_CANARY")]}]}); fs.writeFileSync(out,await Packer.toBuffer(doc));})().catch(()=>process.exit(1));`;
4379
+ const PDF_CANARY_SCRIPT = String.raw `
4380
+ import sys
4381
+ from reportlab.pdfgen import canvas
4382
+ c=canvas.Canvas(sys.argv[1]); c.drawString(72,720,"TIANGONG_PDF_CANARY"); c.save()`;
4383
+ const PDF_VERIFY_SCRIPT = String.raw `
4384
+ import sys, pdfplumber
4385
+ from pypdf import PdfReader
4386
+ assert len(PdfReader(sys.argv[1]).pages)==1
4387
+ with pdfplumber.open(sys.argv[1]) as p: print(p.pages[0].extract_text())`;
4388
+ const PPTX_CANARY_SCRIPT = String.raw `
4389
+ const pptxgen=require("pptxgenjs");
4390
+ (async()=>{const p=new pptxgen(); p.addSlide().addText("TIANGONG_PPTX_CANARY",{x:1,y:1,w:6,h:1}); await p.writeFile({fileName:process.argv[1]});})().catch(()=>process.exit(1));`;
4391
+ const XLSX_CANARY_SCRIPT = String.raw `
4392
+ import sys
4393
+ from openpyxl import Workbook
4394
+ w=Workbook(); s=w.active; s["A1"]="TIANGONG_XLSX_CANARY"; s["A2"]=2; s["A3"]=3; s["A4"]="=SUM(A2:A3)"; w.save(sys.argv[1])`;
4395
+ const XLSX_VERIFY_SCRIPT = String.raw `
4396
+ import sys
4397
+ from openpyxl import load_workbook
4398
+ w=load_workbook(sys.argv[1],data_only=True,read_only=True); print(w.active["A4"].value); w.close()`;
4008
4399
  async function appendCompanionLiveChecks(checks, input) {
4009
4400
  if (input.selected.some((skill) => skill.id === "tiangong.academic-paper-download")) {
4010
4401
  await appendSemanticScholarLiveCheck(checks, input);