@lazyingart/agintiflow 0.20.231 → 0.20.232
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/package.json +4 -2
- package/scripts/smoke-document-artifact-quality.js +70 -0
- package/scripts/smoke-truthful-completion.js +30 -0
- package/scripts/smoke-web-api.js +1 -0
- package/scripts/smoke-web-port-fallback.js +1 -0
- package/scripts/smoke-web-ui.js +8 -3
- package/scripts/smoke-webapp-command.js +1 -0
- package/src/agent-runner.js +68 -0
- package/src/document-artifact-quality.js +478 -0
- package/src/task-profiles.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.232",
|
|
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",
|
|
@@ -102,6 +102,7 @@
|
|
|
102
102
|
"scripts/smoke-localllm-provider.js",
|
|
103
103
|
"scripts/smoke-progressive-tool-selection.js",
|
|
104
104
|
"scripts/smoke-truthful-completion.js",
|
|
105
|
+
"scripts/smoke-document-artifact-quality.js",
|
|
105
106
|
"scripts/smoke-writing-specialist-routing.js",
|
|
106
107
|
"scripts/eval-provider-attribution.js",
|
|
107
108
|
"scripts/local-first-agent-eval.mjs",
|
|
@@ -174,6 +175,7 @@
|
|
|
174
175
|
"smoke:localllm-provider": "node scripts/smoke-localllm-provider.js",
|
|
175
176
|
"smoke:progressive-tools": "node scripts/smoke-progressive-tool-selection.js",
|
|
176
177
|
"smoke:truthful-completion": "node scripts/smoke-truthful-completion.js",
|
|
178
|
+
"smoke:document-artifact-quality": "node scripts/smoke-document-artifact-quality.js",
|
|
177
179
|
"smoke:writing-specialist-routing": "node scripts/smoke-writing-specialist-routing.js",
|
|
178
180
|
"smoke:mcp": "node scripts/smoke-mcp.js",
|
|
179
181
|
"smoke:model-roles": "node scripts/smoke-model-roles.js",
|
|
@@ -217,7 +219,7 @@
|
|
|
217
219
|
"storage:migrate": "node bin/aginti-cli.js storage migrate",
|
|
218
220
|
"publish:env": "node scripts/npm-publish-from-env.js publish --access public",
|
|
219
221
|
"publish:env:whoami": "node scripts/npm-publish-from-env.js whoami",
|
|
220
|
-
"test": "npm run check && npm run smoke:integration-production-mount && npm run smoke:integration-storage-authority && npm run smoke:integration-retained-durable-common && npm run smoke:integration-retained-file-lock && npm run smoke:integration-retained-event-ledger && npm run smoke:integration-retained-event-ledger-bundle && npm run smoke:integration-retained-repository-kernel && npm run smoke:integration-retained-session-state-store && npm run smoke:integration-retained-native-session-repository-state && npm run smoke:integration-retained-native-execution-evidence && npm run smoke:integration-retained-runtime-repository-surface && npm run smoke:integration-retained-runtime-repository-phase-b && npm run smoke:integration-runtime-repository-contract && npm run smoke:integration-runtime-authority && npm run smoke:localllm-provider && npm run smoke:localllm-model-tiers && npm run smoke:localllm-code-routing && npm run smoke:local-failure-recovery && npm run smoke:localllm-auto-max && npm run smoke:local-resource-policy && npm run smoke:context-budget-recovery && npm run smoke:session-runtime && npm run smoke:runtime-core && npm run smoke:progressive-tools && npm run smoke:truthful-completion && npm run smoke:writing-specialist-routing && npm run eval:local-first-agent && npm run eval:provider-attribution && npm run smoke:runtime-compat && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:math-rendering && npm run smoke:web-ui && npm run smoke:web-autostart && npm run smoke:webapp-command && npm run smoke:web-port-fallback && npm run smoke:docker-command && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:execution-policy && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:deep-research && npm run smoke:public-research && npm run smoke:safe-chat && npm run smoke:auth && npm run smoke:agentlink && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:mcp && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:supervision-ledger && npm run smoke:tmux-tools && npm run smoke:long-jobs && npm run smoke:run-stdin && npm run smoke:cli-chat && npm run smoke:inbox",
|
|
222
|
+
"test": "npm run check && npm run smoke:integration-production-mount && npm run smoke:integration-storage-authority && npm run smoke:integration-retained-durable-common && npm run smoke:integration-retained-file-lock && npm run smoke:integration-retained-event-ledger && npm run smoke:integration-retained-event-ledger-bundle && npm run smoke:integration-retained-repository-kernel && npm run smoke:integration-retained-session-state-store && npm run smoke:integration-retained-native-session-repository-state && npm run smoke:integration-retained-native-execution-evidence && npm run smoke:integration-retained-runtime-repository-surface && npm run smoke:integration-retained-runtime-repository-phase-b && npm run smoke:integration-runtime-repository-contract && npm run smoke:integration-runtime-authority && npm run smoke:localllm-provider && npm run smoke:localllm-model-tiers && npm run smoke:localllm-code-routing && npm run smoke:local-failure-recovery && npm run smoke:localllm-auto-max && npm run smoke:local-resource-policy && npm run smoke:context-budget-recovery && npm run smoke:session-runtime && npm run smoke:runtime-core && npm run smoke:progressive-tools && npm run smoke:truthful-completion && npm run smoke:document-artifact-quality && npm run smoke:writing-specialist-routing && npm run eval:local-first-agent && npm run eval:provider-attribution && npm run smoke:runtime-compat && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:math-rendering && npm run smoke:web-ui && npm run smoke:web-autostart && npm run smoke:webapp-command && npm run smoke:web-port-fallback && npm run smoke:docker-command && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:execution-policy && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:deep-research && npm run smoke:public-research && npm run smoke:safe-chat && npm run smoke:auth && npm run smoke:agentlink && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:mcp && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:supervision-ledger && npm run smoke:tmux-tools && npm run smoke:long-jobs && npm run smoke:run-stdin && npm run smoke:cli-chat && npm run smoke:inbox",
|
|
221
223
|
"pack:dry-run": "npm pack --dry-run",
|
|
222
224
|
"smoke:capabilities": "node scripts/smoke-capabilities.js"
|
|
223
225
|
},
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
evaluateCurrentStateText,
|
|
5
|
+
evaluatePdfPageBalance,
|
|
6
|
+
extractSupersededLiterals,
|
|
7
|
+
} from "../src/document-artifact-quality.js";
|
|
8
|
+
|
|
9
|
+
const source = [
|
|
10
|
+
"Initial plan: the demonstration date was September 12.",
|
|
11
|
+
"Preliminary total budget was HKD 18,000.",
|
|
12
|
+
"Correction: the demonstration is September 19, 2026, not September 12.",
|
|
13
|
+
"Mei is the integration owner, replacing Ana.",
|
|
14
|
+
"Use Vendor C; Vendor A is no longer selected.",
|
|
15
|
+
].join("\n");
|
|
16
|
+
|
|
17
|
+
assert.deepEqual(
|
|
18
|
+
extractSupersededLiterals(source),
|
|
19
|
+
["HKD 18,000", "September 12", "Ana", "Vendor A"],
|
|
20
|
+
"explicit corrections did not produce a stable superseded-fact set"
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const staleDocument = evaluateCurrentStateText({
|
|
24
|
+
sourceText: source,
|
|
25
|
+
outputText: "Mei owns integration, replacing Ana. The approved cap is HKD 16,500, not HKD 18,000. Vendor A is no longer selected.",
|
|
26
|
+
currentStateRequired: true,
|
|
27
|
+
});
|
|
28
|
+
assert.equal(staleDocument.ok, false, "current-state validation accepted superseded history");
|
|
29
|
+
assert(
|
|
30
|
+
staleDocument.defects.some((item) => item.code === "superseded-facts-present"),
|
|
31
|
+
"superseded literals were not reported"
|
|
32
|
+
);
|
|
33
|
+
assert(
|
|
34
|
+
staleDocument.defects.some((item) => item.code === "historical-transition-prose"),
|
|
35
|
+
"historical transition prose was not reported"
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const currentDocument = evaluateCurrentStateText({
|
|
39
|
+
sourceText: source,
|
|
40
|
+
outputText: "The demonstration is September 19, 2026. Mei owns integration. The approved cap is HKD 16,500. Vendor C supplies the filter.",
|
|
41
|
+
currentStateRequired: true,
|
|
42
|
+
});
|
|
43
|
+
assert.equal(currentDocument.ok, true, "authoritative current-state prose was rejected");
|
|
44
|
+
|
|
45
|
+
function page(words, { height = 842, startY = 60, endY = 700, heading = "Section" } = {}) {
|
|
46
|
+
const items = [];
|
|
47
|
+
const count = Math.max(1, words);
|
|
48
|
+
for (let index = 0; index < count; index += 1) {
|
|
49
|
+
const ratio = count === 1 ? 0 : index / (count - 1);
|
|
50
|
+
const y = startY + (endY - startY) * ratio;
|
|
51
|
+
const text = index === 0 ? heading : `word${index}`;
|
|
52
|
+
items.push(`<word xMin="60" yMin="${y.toFixed(2)}" xMax="100" yMax="${(y + 10).toFixed(2)}">${text}</word>`);
|
|
53
|
+
}
|
|
54
|
+
items.push(`<word xMin="250" yMin="810" xMax="300" yMax="820">footer</word>`);
|
|
55
|
+
return `<page width="595" height="${height}">${items.join("")}</page>`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sparse = evaluatePdfPageBalance(`<doc>${page(240)}${page(55, { endY: 190, heading: "Risks" })}</doc>`);
|
|
59
|
+
assert.equal(sparse.ok, false, "a sparse trailing spill page was accepted");
|
|
60
|
+
assert.equal(sparse.defects[0]?.code, "sparse-trailing-page");
|
|
61
|
+
|
|
62
|
+
const balanced = evaluatePdfPageBalance(`<doc>${page(220)}${page(160, { endY: 620, heading: "Risks" })}</doc>`);
|
|
63
|
+
assert.equal(balanced.ok, true, "a balanced two-page document was rejected");
|
|
64
|
+
|
|
65
|
+
const intentionalAppendix = evaluatePdfPageBalance(
|
|
66
|
+
`<doc>${page(220)}${page(35, { endY: 170, heading: "Appendix" })}</doc>`
|
|
67
|
+
);
|
|
68
|
+
assert.equal(intentionalAppendix.ok, true, "an intentional sparse appendix page was rejected");
|
|
69
|
+
|
|
70
|
+
console.log("document artifact quality smoke test passed");
|
|
@@ -503,6 +503,36 @@ try {
|
|
|
503
503
|
assert.match(futureWorkFinish.result.result, /verified the working directory/i);
|
|
504
504
|
assert(futureWorkFinish.events.some((event) => event.type === "completion.evidence_rejected"));
|
|
505
505
|
|
|
506
|
+
const wordCompletionWithoutArtifact = await runCase({
|
|
507
|
+
id: "word-completion-without-artifact",
|
|
508
|
+
goal: "Create an editable, phone-friendly project handoff from this folder.",
|
|
509
|
+
taskProfile: "word",
|
|
510
|
+
allowFileTools: true,
|
|
511
|
+
responses: [
|
|
512
|
+
assistant("", [toolCall("word-finish-without-file-1", "finish", { result: "The handoff is complete." })]),
|
|
513
|
+
assistant("", [toolCall("word-finish-without-file-2", "finish", { result: "The handoff is complete." })]),
|
|
514
|
+
],
|
|
515
|
+
});
|
|
516
|
+
assert.equal(wordCompletionWithoutArtifact.result.stopped, true);
|
|
517
|
+
assert.equal(wordCompletionWithoutArtifact.result.reason, "model_did_not_execute");
|
|
518
|
+
assert(
|
|
519
|
+
wordCompletionWithoutArtifact.events.some(
|
|
520
|
+
(event) =>
|
|
521
|
+
event.type === "document.quality_assessed" &&
|
|
522
|
+
event.data?.ok === false &&
|
|
523
|
+
/no readable DOCX or PDF/i.test(String(event.data?.reason || ""))
|
|
524
|
+
),
|
|
525
|
+
"Word completion without a document artifact bypassed the independent quality gate"
|
|
526
|
+
);
|
|
527
|
+
assert(
|
|
528
|
+
wordCompletionWithoutArtifact.events.some(
|
|
529
|
+
(event) =>
|
|
530
|
+
event.type === "completion.evidence_rejected" &&
|
|
531
|
+
/no readable DOCX or PDF/i.test(String(event.data?.reason || ""))
|
|
532
|
+
),
|
|
533
|
+
"missing Word artifacts did not produce an actionable completion repair"
|
|
534
|
+
);
|
|
535
|
+
|
|
506
536
|
const verifiedAction = await runCase({
|
|
507
537
|
id: "verified-action",
|
|
508
538
|
goal: "Execute the shell command pwd and report the output.",
|
package/scripts/smoke-web-api.js
CHANGED
|
@@ -63,6 +63,7 @@ const child = spawn(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"),
|
|
|
63
63
|
cwd: runtimeDir,
|
|
64
64
|
env: {
|
|
65
65
|
...process.env,
|
|
66
|
+
AGINTIFLOW_NO_AUTO_UPDATE: "1",
|
|
66
67
|
AGINTIFLOW_RUNTIME_DIR: runtimeDir,
|
|
67
68
|
AGINTIFLOW_HOME: path.join(runtimeDir, ".agintiflow-home"),
|
|
68
69
|
},
|
package/scripts/smoke-web-ui.js
CHANGED
|
@@ -15,6 +15,7 @@ const server = spawn(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js")
|
|
|
15
15
|
cwd: runtimeDir,
|
|
16
16
|
env: {
|
|
17
17
|
...process.env,
|
|
18
|
+
AGINTIFLOW_NO_AUTO_UPDATE: "1",
|
|
18
19
|
AGINTIFLOW_HOME: agintiflowHome,
|
|
19
20
|
AGINTIFLOW_RUNTIME_DIR: runtimeDir,
|
|
20
21
|
},
|
|
@@ -35,17 +36,21 @@ function delay(ms) {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
async function waitForHealth() {
|
|
38
|
-
const deadline = Date.now() +
|
|
39
|
+
const deadline = Date.now() + 30000;
|
|
40
|
+
let lastError = "";
|
|
39
41
|
while (Date.now() < deadline) {
|
|
40
42
|
if (server.exitCode !== null) break;
|
|
41
43
|
try {
|
|
42
44
|
const health = await fetch(`${baseUrl}/health`).then((response) => response.json());
|
|
43
45
|
if (health.ok) return health;
|
|
44
|
-
} catch {
|
|
46
|
+
} catch (error) {
|
|
47
|
+
lastError = String(error?.message || error);
|
|
45
48
|
await delay(250);
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
|
-
throw new Error(
|
|
51
|
+
throw new Error(
|
|
52
|
+
`web server did not become healthy. lastError=${lastError} stdout=${stdout.slice(-500)} stderr=${stderr.slice(-500)}`
|
|
53
|
+
);
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
async function waitForRunState(page, status, timeout = 20000) {
|
|
@@ -77,6 +77,7 @@ async function runCase({ port, env = {}, expectHeader, label }) {
|
|
|
77
77
|
cwd: runtimeDir,
|
|
78
78
|
env: {
|
|
79
79
|
...process.env,
|
|
80
|
+
AGINTIFLOW_NO_AUTO_UPDATE: "1",
|
|
80
81
|
AGINTIFLOW_NO_ANIMATION: "1",
|
|
81
82
|
AGINTIFLOW_HOME: path.join(runtimeDir, `.ignored-cli-home-${label}`),
|
|
82
83
|
AGINTIFLOW_WEB_HOME: path.join(runtimeDir, `.agintiflow-web-home-${label}`),
|
package/src/agent-runner.js
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
} from "./workspace-tools.js";
|
|
52
52
|
import { normalizeCanvasPayload, persistCanvasPayloadFile } from "./artifact-tunnel.js";
|
|
53
53
|
import { getTaskProfile } from "./task-profiles.js";
|
|
54
|
+
import { validateWordDocumentArtifacts } from "./document-artifact-quality.js";
|
|
54
55
|
import { generateImage, listAuxiliarySkills } from "./auxiliary-tools.js";
|
|
55
56
|
import {
|
|
56
57
|
engineeringGuidanceForTask,
|
|
@@ -9122,6 +9123,66 @@ async function completionEvidenceDecision({ config, state, store, observers, ste
|
|
|
9122
9123
|
},
|
|
9123
9124
|
};
|
|
9124
9125
|
}
|
|
9126
|
+
let documentQuality = null;
|
|
9127
|
+
if (String(config.taskProfile || state.meta?.taskProfile || "").toLowerCase() === "word") {
|
|
9128
|
+
try {
|
|
9129
|
+
documentQuality = await validateWordDocumentArtifacts({
|
|
9130
|
+
commandCwd: config.commandCwd || state.commandCwd || process.cwd(),
|
|
9131
|
+
candidateResult,
|
|
9132
|
+
goal: completionContractGoal(config, state),
|
|
9133
|
+
exactOutputPaths: [
|
|
9134
|
+
...(assessment.contract?.exactOutputPaths || []),
|
|
9135
|
+
...exactOutputPathsForState(state),
|
|
9136
|
+
],
|
|
9137
|
+
});
|
|
9138
|
+
} catch (error) {
|
|
9139
|
+
documentQuality = {
|
|
9140
|
+
ok: false,
|
|
9141
|
+
checked: true,
|
|
9142
|
+
artifacts: [],
|
|
9143
|
+
defects: [{
|
|
9144
|
+
code: "document-quality-check-failed",
|
|
9145
|
+
message: String(error?.message || error),
|
|
9146
|
+
}],
|
|
9147
|
+
reason: `Independent document-quality validation failed: ${String(error?.message || error)}`,
|
|
9148
|
+
};
|
|
9149
|
+
}
|
|
9150
|
+
state.meta = state.meta || {};
|
|
9151
|
+
state.meta.documentArtifactQuality = documentQuality;
|
|
9152
|
+
const qualityEvent = {
|
|
9153
|
+
step,
|
|
9154
|
+
mode,
|
|
9155
|
+
ok: documentQuality.ok,
|
|
9156
|
+
reason: documentQuality.reason,
|
|
9157
|
+
artifacts: documentQuality.artifacts || [],
|
|
9158
|
+
defects: documentQuality.defects || [],
|
|
9159
|
+
sourcePaths: documentQuality.sourcePaths || [],
|
|
9160
|
+
};
|
|
9161
|
+
await store.appendEvent("document.quality_assessed", qualityEvent);
|
|
9162
|
+
observers.event("document.quality_assessed", qualityEvent);
|
|
9163
|
+
if (!documentQuality.ok) {
|
|
9164
|
+
const priorSemanticReason = assessment.semantic?.checked && !assessment.semantic?.ok
|
|
9165
|
+
? String(assessment.semantic.reason || "")
|
|
9166
|
+
: "";
|
|
9167
|
+
const qualityReason = String(documentQuality.reason || "The document artifact failed independent quality checks.");
|
|
9168
|
+
assessment = {
|
|
9169
|
+
...assessment,
|
|
9170
|
+
ok: false,
|
|
9171
|
+
documentQuality,
|
|
9172
|
+
evaluation: {
|
|
9173
|
+
...assessment.evaluation,
|
|
9174
|
+
ok: false,
|
|
9175
|
+
reason: qualityReason,
|
|
9176
|
+
},
|
|
9177
|
+
semantic: {
|
|
9178
|
+
...assessment.semantic,
|
|
9179
|
+
checked: true,
|
|
9180
|
+
ok: false,
|
|
9181
|
+
reason: [priorSemanticReason, qualityReason].filter(Boolean).join(" "),
|
|
9182
|
+
},
|
|
9183
|
+
};
|
|
9184
|
+
}
|
|
9185
|
+
}
|
|
9125
9186
|
const hasRealBlocker = finishResultClaimsBlocker(candidateResult) && hasScsBlockerEvidence(assessment.ledger);
|
|
9126
9187
|
if (assessment.ok && !claimsIncompleteWork) return { action: "accept", assessment };
|
|
9127
9188
|
if (claimsIncompleteWork) {
|
|
@@ -9168,6 +9229,13 @@ async function completionEvidenceDecision({ config, state, store, observers, ste
|
|
|
9168
9229
|
ok: Boolean(assessment.semantic.ok),
|
|
9169
9230
|
reason: assessment.semantic.reason || "",
|
|
9170
9231
|
},
|
|
9232
|
+
documentQuality: documentQuality
|
|
9233
|
+
? {
|
|
9234
|
+
ok: Boolean(documentQuality.ok),
|
|
9235
|
+
reason: documentQuality.reason || "",
|
|
9236
|
+
defects: documentQuality.defects || [],
|
|
9237
|
+
}
|
|
9238
|
+
: null,
|
|
9171
9239
|
progressCount,
|
|
9172
9240
|
};
|
|
9173
9241
|
state.meta = state.meta || {};
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
const DOCUMENT_EXTENSIONS = new Set([".csv", ".json", ".md", ".rst", ".tsv", ".txt", ".yaml", ".yml"]);
|
|
9
|
+
const SOURCE_DIRECTORY_NAMES = new Set(["input", "inputs", "material", "materials", "notes", "reference", "references", "source", "sources"]);
|
|
10
|
+
const ROOT_SOURCE_NAMES = /^(?:agents?|brief|project[-_ ]?notes?|readme|requirements?|style[-_ ]?notes?|task)(?:\.[^.]+)?$/i;
|
|
11
|
+
const EXCLUDED_DIRECTORY_NAMES = new Set([
|
|
12
|
+
".aginti",
|
|
13
|
+
".aginti-preview",
|
|
14
|
+
".aginti-sessions",
|
|
15
|
+
".git",
|
|
16
|
+
".venv",
|
|
17
|
+
"__pycache__",
|
|
18
|
+
"artifacts",
|
|
19
|
+
"build",
|
|
20
|
+
"coverage",
|
|
21
|
+
"dist",
|
|
22
|
+
"node_modules",
|
|
23
|
+
"output",
|
|
24
|
+
"outputs",
|
|
25
|
+
"temp",
|
|
26
|
+
"tmp",
|
|
27
|
+
]);
|
|
28
|
+
const INTENTIONAL_SPARSE_PAGE_PATTERN =
|
|
29
|
+
/^(?:appendix|approval|approvals|acknowledgements?|back cover|contact|notes|references|sign[- ]?off|signatures?)\b/i;
|
|
30
|
+
const HISTORICAL_TRANSITION_PATTERN =
|
|
31
|
+
/\b(?:formerly|no longer|previously|replac(?:ed|ing)|superseded|used to be)\b/i;
|
|
32
|
+
|
|
33
|
+
function portablePath(value = "") {
|
|
34
|
+
return String(value || "").replace(/\\/g, "/");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isInsideRoot(root, candidate) {
|
|
38
|
+
const relative = path.relative(root, candidate);
|
|
39
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function decodeXml(value = "") {
|
|
43
|
+
return String(value || "")
|
|
44
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16)))
|
|
45
|
+
.replace(/&#([0-9]+);/g, (_, decimal) => String.fromCodePoint(Number.parseInt(decimal, 10)))
|
|
46
|
+
.replace(/</g, "<")
|
|
47
|
+
.replace(/>/g, ">")
|
|
48
|
+
.replace(/"/g, '"')
|
|
49
|
+
.replace(/'/g, "'")
|
|
50
|
+
.replace(/&/g, "&");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function textFromDocumentXml(xml = "") {
|
|
54
|
+
return decodeXml(
|
|
55
|
+
String(xml || "")
|
|
56
|
+
.replace(/<w:tab\b[^>]*\/>/g, "\t")
|
|
57
|
+
.replace(/<w:br\b[^>]*\/>/g, "\n")
|
|
58
|
+
.replace(/<\/w:p>/g, "\n")
|
|
59
|
+
.replace(/<\/w:tr>/g, "\n")
|
|
60
|
+
.replace(/<[^>]+>/g, "")
|
|
61
|
+
)
|
|
62
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
63
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
64
|
+
.trim();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizedComparableText(value = "") {
|
|
68
|
+
return String(value || "")
|
|
69
|
+
.normalize("NFKC")
|
|
70
|
+
.replace(/[\u2018\u2019]/g, "'")
|
|
71
|
+
.replace(/[\u2013\u2014]/g, "-")
|
|
72
|
+
.replace(/\s+/g, " ")
|
|
73
|
+
.trim()
|
|
74
|
+
.toLowerCase();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function containsLiteral(text = "", literal = "") {
|
|
78
|
+
const normalizedText = normalizedComparableText(text);
|
|
79
|
+
const normalizedLiteral = normalizedComparableText(literal);
|
|
80
|
+
if (!normalizedText || !normalizedLiteral) return false;
|
|
81
|
+
const escaped = normalizedLiteral.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
82
|
+
return new RegExp(`(?:^|[^\\p{L}\\p{N}])${escaped}(?=$|[^\\p{L}\\p{N}])`, "iu").test(
|
|
83
|
+
normalizedText
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function cleanedSupersededLiteral(value = "") {
|
|
88
|
+
return String(value || "")
|
|
89
|
+
.replace(/^[\s:;,()\[\]-]+|[\s:;,()\[\].-]+$/g, "")
|
|
90
|
+
.replace(/\s+/g, " ")
|
|
91
|
+
.trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function extractSupersededLiterals(sourceText = "") {
|
|
95
|
+
const text = String(sourceText || "");
|
|
96
|
+
const values = [];
|
|
97
|
+
const add = (value) => {
|
|
98
|
+
const cleaned = cleanedSupersededLiteral(value);
|
|
99
|
+
if (cleaned.length < 2 || cleaned.length > 80) return;
|
|
100
|
+
if (!/[\p{L}\p{N}]/u.test(cleaned)) return;
|
|
101
|
+
values.push(cleaned);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
for (const sentence of text.split(/(?<=[.!?])\s+|\n+/u)) {
|
|
105
|
+
if (/\b(?:correction|corrected|revis(?:ed|ion)|update(?:d)?)\b/i.test(sentence)) {
|
|
106
|
+
for (const match of sentence.matchAll(
|
|
107
|
+
/\bnot\s+((?:[A-Z][\p{L}.'-]+(?:\s+\d{1,2}(?:,\s*\d{4})?)?)|(?:(?:HKD|USD|EUR|GBP|JPY|CNY|RMB)\s*[\d,.]+)|(?:[A-Z][\p{L}\p{N}&.'/-]*(?:\s+[A-Z][\p{L}\p{N}&.'/-]*){0,4}))/gu
|
|
108
|
+
)) {
|
|
109
|
+
add(match[1]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const match of sentence.matchAll(
|
|
113
|
+
/\breplac(?:ed|ing)\s+([A-Z][\p{L}\p{N}&.'/-]*(?:\s+[A-Z][\p{L}\p{N}&.'/-]*){0,4})/gu
|
|
114
|
+
)) {
|
|
115
|
+
add(match[1]);
|
|
116
|
+
}
|
|
117
|
+
for (const match of sentence.matchAll(
|
|
118
|
+
/([A-Z][\p{L}\p{N}&.'/-]*(?:\s+[A-Z][\p{L}\p{N}&.'/-]*){0,4})\s+(?:is|was)\s+no longer\b/gu
|
|
119
|
+
)) {
|
|
120
|
+
add(match[1]);
|
|
121
|
+
}
|
|
122
|
+
if (/\bpreliminary\b/i.test(sentence)) {
|
|
123
|
+
for (const match of sentence.matchAll(
|
|
124
|
+
/(?:(?:HKD|USD|EUR|GBP|JPY|CNY|RMB)\s*[\d,.]+|[$€£¥]\s*[\d,.]+)/gu
|
|
125
|
+
)) {
|
|
126
|
+
add(match[0]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const seen = new Set();
|
|
132
|
+
return values.filter((value) => {
|
|
133
|
+
const key = normalizedComparableText(value);
|
|
134
|
+
if (!key || seen.has(key)) return false;
|
|
135
|
+
seen.add(key);
|
|
136
|
+
return true;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parsePdfBboxPages(bboxXml = "") {
|
|
141
|
+
const pages = [];
|
|
142
|
+
const pagePattern = /<page\b[^>]*width="([^"]+)"[^>]*height="([^"]+)"[^>]*>([\s\S]*?)<\/page>/gi;
|
|
143
|
+
for (const pageMatch of String(bboxXml || "").matchAll(pagePattern)) {
|
|
144
|
+
const width = Number(pageMatch[1]);
|
|
145
|
+
const height = Number(pageMatch[2]);
|
|
146
|
+
const words = [];
|
|
147
|
+
const wordPattern =
|
|
148
|
+
/<word\b[^>]*xMin="([^"]+)"[^>]*yMin="([^"]+)"[^>]*xMax="([^"]+)"[^>]*yMax="([^"]+)"[^>]*>([\s\S]*?)<\/word>/gi;
|
|
149
|
+
for (const wordMatch of pageMatch[3].matchAll(wordPattern)) {
|
|
150
|
+
words.push({
|
|
151
|
+
xMin: Number(wordMatch[1]),
|
|
152
|
+
yMin: Number(wordMatch[2]),
|
|
153
|
+
xMax: Number(wordMatch[3]),
|
|
154
|
+
yMax: Number(wordMatch[4]),
|
|
155
|
+
text: decodeXml(wordMatch[5]).replace(/<[^>]+>/g, "").trim(),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
pages.push({ width, height, words });
|
|
159
|
+
}
|
|
160
|
+
return pages;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function median(values = []) {
|
|
164
|
+
const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
|
|
165
|
+
if (!sorted.length) return 0;
|
|
166
|
+
const middle = Math.floor(sorted.length / 2);
|
|
167
|
+
return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function evaluatePdfPageBalance(bboxXml = "") {
|
|
171
|
+
const pages = parsePdfBboxPages(bboxXml);
|
|
172
|
+
const metrics = pages.map((page, index) => {
|
|
173
|
+
const footerCutoff = Number.isFinite(page.height) ? page.height - 60 : Number.POSITIVE_INFINITY;
|
|
174
|
+
const contentWords = page.words.filter((word) => word.text && word.yMin < footerCutoff);
|
|
175
|
+
const yValues = contentWords.flatMap((word) => [word.yMin, word.yMax]).filter(Number.isFinite);
|
|
176
|
+
const usableHeight = Math.max(1, Number(page.height || 0) - 120);
|
|
177
|
+
const occupiedHeight = yValues.length ? Math.max(...yValues) - Math.min(...yValues) : 0;
|
|
178
|
+
return {
|
|
179
|
+
page: index + 1,
|
|
180
|
+
wordCount: contentWords.length,
|
|
181
|
+
occupiedRatio: occupiedHeight / usableHeight,
|
|
182
|
+
leadingText: contentWords.slice(0, 16).map((word) => word.text).join(" ").trim(),
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
const defects = [];
|
|
186
|
+
if (metrics.length > 1) {
|
|
187
|
+
const priorWordCounts = metrics.slice(0, -1).map((item) => item.wordCount).filter((value) => value > 0);
|
|
188
|
+
const last = metrics.at(-1);
|
|
189
|
+
const comparisonCount = Math.max(80, median(priorWordCounts) * 0.45);
|
|
190
|
+
const intentionalSparsePage = INTENTIONAL_SPARSE_PAGE_PATTERN.test(last.leadingText);
|
|
191
|
+
if (
|
|
192
|
+
!intentionalSparsePage &&
|
|
193
|
+
last.wordCount > 0 &&
|
|
194
|
+
last.wordCount < comparisonCount &&
|
|
195
|
+
last.occupiedRatio < 0.35
|
|
196
|
+
) {
|
|
197
|
+
defects.push({
|
|
198
|
+
code: "sparse-trailing-page",
|
|
199
|
+
message: `Page ${last.page} is a sparse spill page (${last.wordCount} words; ${(last.occupiedRatio * 100).toFixed(0)}% usable-height occupancy). Reflow the preceding content or rebalance sections so the final page is intentional and useful.`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
ok: pages.length > 0 && defects.length === 0,
|
|
205
|
+
checked: pages.length > 0,
|
|
206
|
+
pages: metrics,
|
|
207
|
+
defects,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function evaluateCurrentStateText({ sourceText = "", outputText = "", currentStateRequired = false } = {}) {
|
|
212
|
+
const supersededLiterals = extractSupersededLiterals(sourceText);
|
|
213
|
+
const presentSupersededLiterals = supersededLiterals.filter((literal) => containsLiteral(outputText, literal));
|
|
214
|
+
const defects = [];
|
|
215
|
+
if (presentSupersededLiterals.length) {
|
|
216
|
+
defects.push({
|
|
217
|
+
code: "superseded-facts-present",
|
|
218
|
+
message: `The reader-facing document still contains superseded source values: ${presentSupersededLiterals.join(", ")}. State only the authoritative current values unless history was explicitly requested.`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (currentStateRequired) {
|
|
222
|
+
const historicalMarkers = [...new Set(
|
|
223
|
+
(String(outputText || "").match(new RegExp(HISTORICAL_TRANSITION_PATTERN.source, "gi")) || [])
|
|
224
|
+
.map((value) => value.toLowerCase())
|
|
225
|
+
)];
|
|
226
|
+
if (historicalMarkers.length) {
|
|
227
|
+
defects.push({
|
|
228
|
+
code: "historical-transition-prose",
|
|
229
|
+
message: `This is a current-state document, but it narrates superseded history (${historicalMarkers.join(", ")}). Remove transition commentary and retain only current decisions.`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return { ok: defects.length === 0, defects, supersededLiterals, presentSupersededLiterals };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function collectSourceDocuments(commandCwd) {
|
|
237
|
+
const documents = [];
|
|
238
|
+
let totalBytes = 0;
|
|
239
|
+
const maxFiles = 128;
|
|
240
|
+
const maxTotalBytes = 4 * 1024 * 1024;
|
|
241
|
+
const maxFileBytes = 512 * 1024;
|
|
242
|
+
|
|
243
|
+
async function visit(directory, sourceRoot = false, depth = 0) {
|
|
244
|
+
if (documents.length >= maxFiles || totalBytes >= maxTotalBytes || depth > 5) return;
|
|
245
|
+
let entries;
|
|
246
|
+
try {
|
|
247
|
+
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
248
|
+
} catch {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
252
|
+
for (const entry of entries) {
|
|
253
|
+
if (documents.length >= maxFiles || totalBytes >= maxTotalBytes) break;
|
|
254
|
+
const absolutePath = path.join(directory, entry.name);
|
|
255
|
+
const lowerName = entry.name.toLowerCase();
|
|
256
|
+
if (entry.isDirectory()) {
|
|
257
|
+
if (EXCLUDED_DIRECTORY_NAMES.has(lowerName)) continue;
|
|
258
|
+
const nextSourceRoot = sourceRoot || SOURCE_DIRECTORY_NAMES.has(lowerName);
|
|
259
|
+
if (nextSourceRoot) await visit(absolutePath, true, depth + 1);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (!entry.isFile()) continue;
|
|
263
|
+
const extension = path.extname(lowerName);
|
|
264
|
+
const rootCandidate = directory === commandCwd && ROOT_SOURCE_NAMES.test(entry.name);
|
|
265
|
+
if (!(sourceRoot && DOCUMENT_EXTENSIONS.has(extension)) && !rootCandidate) continue;
|
|
266
|
+
try {
|
|
267
|
+
const stat = await fs.stat(absolutePath);
|
|
268
|
+
if (stat.size <= 0 || stat.size > maxFileBytes || totalBytes + stat.size > maxTotalBytes) continue;
|
|
269
|
+
const text = await fs.readFile(absolutePath, "utf8");
|
|
270
|
+
documents.push({ path: portablePath(path.relative(commandCwd, absolutePath)), text });
|
|
271
|
+
totalBytes += stat.size;
|
|
272
|
+
} catch {
|
|
273
|
+
// Unreadable source material is left to the existing source-coverage gate.
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
await visit(commandCwd, false, 0);
|
|
279
|
+
return documents;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function artifactCandidatesFromText(text = "") {
|
|
283
|
+
const candidates = [];
|
|
284
|
+
const pattern = /(?:`([^`\n]+\.(?:docx|pdf))`|((?:\.?\.?\/[\w .()\/-]+|[\w.-]+(?:\/[\w .()\/-]+)*)\.(?:docx|pdf)))/gi;
|
|
285
|
+
for (const match of String(text || "").matchAll(pattern)) {
|
|
286
|
+
const candidate = String(match[1] || match[2] || "").trim().replace(/[),.;:]+$/g, "");
|
|
287
|
+
if (candidate) candidates.push(candidate);
|
|
288
|
+
}
|
|
289
|
+
return candidates;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function collectOutputDirectoryCandidates(commandCwd) {
|
|
293
|
+
const candidates = [];
|
|
294
|
+
for (const name of ["output", "outputs"]) {
|
|
295
|
+
const root = path.join(commandCwd, name);
|
|
296
|
+
let entries;
|
|
297
|
+
try {
|
|
298
|
+
entries = await fs.readdir(root, { withFileTypes: true });
|
|
299
|
+
} catch {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
for (const entry of entries.slice(0, 80)) {
|
|
303
|
+
if (!entry.isFile() || !/\.(?:docx|pdf)$/i.test(entry.name)) continue;
|
|
304
|
+
candidates.push(portablePath(path.join(name, entry.name)));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return candidates;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function resolveExistingArtifacts(commandCwd, values = []) {
|
|
311
|
+
const artifacts = [];
|
|
312
|
+
const seen = new Set();
|
|
313
|
+
for (const value of values) {
|
|
314
|
+
const absolutePath = path.resolve(commandCwd, String(value || ""));
|
|
315
|
+
if (!isInsideRoot(commandCwd, absolutePath) || seen.has(absolutePath)) continue;
|
|
316
|
+
seen.add(absolutePath);
|
|
317
|
+
try {
|
|
318
|
+
const stat = await fs.stat(absolutePath);
|
|
319
|
+
if (!stat.isFile() || stat.size <= 0) continue;
|
|
320
|
+
artifacts.push({
|
|
321
|
+
path: portablePath(path.relative(commandCwd, absolutePath)),
|
|
322
|
+
absolutePath,
|
|
323
|
+
extension: path.extname(absolutePath).toLowerCase(),
|
|
324
|
+
size: stat.size,
|
|
325
|
+
});
|
|
326
|
+
} catch {
|
|
327
|
+
// Missing candidates are reported only when no document artifact exists.
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return artifacts;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function extractPdf(artifact) {
|
|
334
|
+
const [textResult, bboxResult] = await Promise.all([
|
|
335
|
+
execFileAsync("pdftotext", [artifact.absolutePath, "-"], {
|
|
336
|
+
encoding: "utf8",
|
|
337
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
338
|
+
timeout: 20_000,
|
|
339
|
+
}),
|
|
340
|
+
execFileAsync("pdftotext", ["-bbox", artifact.absolutePath, "-"], {
|
|
341
|
+
encoding: "utf8",
|
|
342
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
343
|
+
timeout: 20_000,
|
|
344
|
+
}),
|
|
345
|
+
]);
|
|
346
|
+
return { text: textResult.stdout, bbox: bboxResult.stdout };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function extractDocxText(artifact) {
|
|
350
|
+
const script = [
|
|
351
|
+
"import sys, zipfile",
|
|
352
|
+
"with zipfile.ZipFile(sys.argv[1]) as archive:",
|
|
353
|
+
" sys.stdout.buffer.write(archive.read('word/document.xml'))",
|
|
354
|
+
].join("\n");
|
|
355
|
+
const result = await execFileAsync("python3", ["-c", script, artifact.absolutePath], {
|
|
356
|
+
encoding: "utf8",
|
|
357
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
358
|
+
timeout: 20_000,
|
|
359
|
+
});
|
|
360
|
+
return textFromDocumentXml(result.stdout);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function currentStateRequested(goal = "", sourceText = "") {
|
|
364
|
+
const contractText = `${goal}\n${sourceText}`;
|
|
365
|
+
return /\b(?:authoritative current|current state|latest explicit correction|latest correction|superseded|use the latest)\b/i.test(
|
|
366
|
+
contractText
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export async function validateWordDocumentArtifacts({
|
|
371
|
+
commandCwd = process.cwd(),
|
|
372
|
+
candidateResult = "",
|
|
373
|
+
goal = "",
|
|
374
|
+
exactOutputPaths = [],
|
|
375
|
+
} = {}) {
|
|
376
|
+
const workspace = path.resolve(commandCwd || process.cwd());
|
|
377
|
+
const outputCandidates = [
|
|
378
|
+
...(Array.isArray(exactOutputPaths) ? exactOutputPaths : []),
|
|
379
|
+
...artifactCandidatesFromText(candidateResult),
|
|
380
|
+
...(await collectOutputDirectoryCandidates(workspace)),
|
|
381
|
+
];
|
|
382
|
+
const artifacts = await resolveExistingArtifacts(workspace, outputCandidates);
|
|
383
|
+
const relevantArtifacts = artifacts.filter((item) => [".docx", ".pdf"].includes(item.extension));
|
|
384
|
+
const defects = [];
|
|
385
|
+
if (!relevantArtifacts.length) {
|
|
386
|
+
return {
|
|
387
|
+
ok: false,
|
|
388
|
+
checked: true,
|
|
389
|
+
artifacts: [],
|
|
390
|
+
defects: [{
|
|
391
|
+
code: "missing-document-artifact",
|
|
392
|
+
message: "The Word/document task claimed completion without a readable DOCX or PDF artifact in the declared output paths or output directory.",
|
|
393
|
+
}],
|
|
394
|
+
reason: "No readable DOCX or PDF artifact was found for the completed document task.",
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const sourceDocuments = await collectSourceDocuments(workspace);
|
|
399
|
+
const sourceText = sourceDocuments.map((item) => item.text).join("\n\n");
|
|
400
|
+
const currentStateRequired = currentStateRequested(goal, sourceText);
|
|
401
|
+
const artifactReports = [];
|
|
402
|
+
for (const artifact of relevantArtifacts) {
|
|
403
|
+
try {
|
|
404
|
+
if (artifact.extension === ".pdf") {
|
|
405
|
+
const extracted = await extractPdf(artifact);
|
|
406
|
+
const semantic = evaluateCurrentStateText({ sourceText, outputText: extracted.text, currentStateRequired });
|
|
407
|
+
const pageBalance = evaluatePdfPageBalance(extracted.bbox);
|
|
408
|
+
if (!String(extracted.text || "").trim()) {
|
|
409
|
+
defects.push({
|
|
410
|
+
code: "empty-pdf-text",
|
|
411
|
+
path: artifact.path,
|
|
412
|
+
message: "The PDF has no independently extractable reader text.",
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
if (!pageBalance.checked) {
|
|
416
|
+
defects.push({
|
|
417
|
+
code: "pdf-page-geometry-unavailable",
|
|
418
|
+
path: artifact.path,
|
|
419
|
+
message: "The PDF page geometry could not be extracted, so page balance and clipping cannot be verified.",
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
defects.push(...semantic.defects.map((item) => ({ ...item, path: artifact.path })));
|
|
423
|
+
defects.push(...pageBalance.defects.map((item) => ({ ...item, path: artifact.path })));
|
|
424
|
+
artifactReports.push({
|
|
425
|
+
path: artifact.path,
|
|
426
|
+
extension: artifact.extension,
|
|
427
|
+
textChars: extracted.text.length,
|
|
428
|
+
pageCount: pageBalance.pages.length,
|
|
429
|
+
pages: pageBalance.pages,
|
|
430
|
+
supersededLiterals: semantic.supersededLiterals,
|
|
431
|
+
});
|
|
432
|
+
} else {
|
|
433
|
+
const text = await extractDocxText(artifact);
|
|
434
|
+
const semantic = evaluateCurrentStateText({ sourceText, outputText: text, currentStateRequired });
|
|
435
|
+
if (!String(text || "").trim()) {
|
|
436
|
+
defects.push({
|
|
437
|
+
code: "empty-docx-text",
|
|
438
|
+
path: artifact.path,
|
|
439
|
+
message: "The DOCX has no independently extractable editable document text.",
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
defects.push(...semantic.defects.map((item) => ({ ...item, path: artifact.path })));
|
|
443
|
+
artifactReports.push({
|
|
444
|
+
path: artifact.path,
|
|
445
|
+
extension: artifact.extension,
|
|
446
|
+
textChars: text.length,
|
|
447
|
+
supersededLiterals: semantic.supersededLiterals,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
} catch (error) {
|
|
451
|
+
defects.push({
|
|
452
|
+
code: "document-extraction-failed",
|
|
453
|
+
path: artifact.path,
|
|
454
|
+
message: `Could not independently extract and inspect ${artifact.path}: ${String(error?.message || error).slice(0, 300)}.`,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const uniqueDefects = [];
|
|
460
|
+
const defectKeys = new Set();
|
|
461
|
+
for (const defect of defects) {
|
|
462
|
+
const key = `${defect.code}\n${defect.message}`;
|
|
463
|
+
if (defectKeys.has(key)) continue;
|
|
464
|
+
defectKeys.add(key);
|
|
465
|
+
uniqueDefects.push(defect);
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
ok: uniqueDefects.length === 0,
|
|
469
|
+
checked: true,
|
|
470
|
+
artifacts: artifactReports,
|
|
471
|
+
sourcePaths: sourceDocuments.map((item) => item.path),
|
|
472
|
+
currentStateRequired,
|
|
473
|
+
defects: uniqueDefects,
|
|
474
|
+
reason: uniqueDefects.length
|
|
475
|
+
? uniqueDefects.map((item) => `${item.path ? `${item.path}: ` : ""}${item.message}`).join(" ")
|
|
476
|
+
: `Independent document checks passed for ${artifactReports.map((item) => item.path).join(", ")}.`,
|
|
477
|
+
};
|
|
478
|
+
}
|
package/src/task-profiles.js
CHANGED
|
@@ -276,7 +276,7 @@ export const TASK_PROFILES = {
|
|
|
276
276
|
id: "word",
|
|
277
277
|
label: "Word documents",
|
|
278
278
|
prompt:
|
|
279
|
-
"Bias toward Word/docx/document workflows while still using writing, conversion, LaTeX, or scripts when useful. Preserve source material byte-for-byte and synthesize a reader-facing document from the authoritative current facts instead of concatenating notes, logs, schemas, task IDs, private paths, or delivery instructions. Reconcile conflicting or superseded source facts before drafting: values labeled corrected, replaced, earlier, old, prior, cancelled, or no longer selected identify what to discard, and those discarded literals must not appear in the final document unless the user explicitly requests history or the history is necessary to explain a live decision. Before finishing, extract the final text and search for every superseded literal found in the sources. Prefer mature editable-document tooling already available in the workspace, such as python-docx, pandoc, or LibreOffice, over hand-written OOXML; when direct OOXML is genuinely necessary, validate its package parts and openability. Keep one maintainable source of truth and a reproducible project-local build command. Verify the DOCX is structurally editable, compile the PDF, run pdftotext or an equivalent extraction check that rejects replacement characters and unexpected control glyphs, and verify PDF text bounding boxes remain inside a readable page margin. Render every PDF page to a separate image under an ignored build/verification directory. Inspect one rendered page per read_image call, never batch pages into one vision call, and repair orphaned headings, near-empty spill pages, awkward table or paragraph breaks, overlaps, clipping, excessive whitespace, weak hierarchy, and inconsistent number formatting before finishing. Retain ignored verification renders as evidence; optional cleanup must never block completion. Use clear descriptive filenames, exclude caches and generated debris from commits, inspect git status/diff before committing, and report success only after the editable source, reader-facing current-state content, visual layout, searchable text, and requested artifacts all pass.",
|
|
279
|
+
"Bias toward Word/docx/document workflows while still using writing, conversion, LaTeX, or scripts when useful. Preserve source material byte-for-byte and synthesize a reader-facing document from the authoritative current facts instead of concatenating notes, logs, schemas, task IDs, private paths, or delivery instructions. Reconcile conflicting or superseded source facts before drafting: values labeled corrected, replaced, earlier, old, prior, cancelled, or no longer selected identify what to discard, and those discarded literals must not appear in the final document unless the user explicitly requests history or the history is necessary to explain a live decision. Keep the reconciliation history private: a current-state handoff should say who owns the work, which value is approved, and which option is selected, not narrate who was replaced or which preliminary value is no longer used. Before finishing, extract the final text and search for every superseded literal found in the sources. Prefer mature editable-document tooling already available in the workspace, such as python-docx, pandoc, or LibreOffice, over hand-written OOXML; when direct OOXML is genuinely necessary, validate its package parts and openability. Keep one maintainable source of truth and a reproducible project-local build command. Verify the DOCX is structurally editable, compile the PDF, run pdftotext or an equivalent extraction check that rejects replacement characters and unexpected control glyphs, and verify PDF text bounding boxes remain inside a readable page margin. Render every PDF page to a separate image under an ignored build/verification directory. Inspect one rendered page per read_image call, never batch pages into one vision call, and repair orphaned headings, near-empty spill pages, awkward table or paragraph breaks, overlaps, clipping, excessive whitespace, weak hierarchy, and inconsistent number formatting before finishing. Prefer one balanced readable page over pushing one short section onto a mostly empty trailing page; preserve an intentionally sparse appendix, approval, signature, reference, or back-matter page when it serves a real purpose. Retain ignored verification renders as evidence; optional cleanup must never block completion. Use clear descriptive filenames, exclude caches and generated debris from commits, inspect git status/diff before committing, and report success only after the editable source, reader-facing current-state content, visual layout, searchable text, and requested artifacts all pass.",
|
|
280
280
|
tools: ["files", "shell", "canvas", "sandbox"],
|
|
281
281
|
},
|
|
282
282
|
latex: {
|