Spaces:
Running
Running
Step 4A-6-C-R3.2.8.5-R1.2 CAR Writing Quality + Nonfatal Word-Count Diagnostics + Content Sufficiency Gate
#24
by GuXiaopeng - opened
- .gitattributes +1 -0
- __pycache__/app.cpython-313.pyc +0 -0
- __pycache__/config.cpython-313.pyc +0 -0
- __pycache__/desktop.ini +2 -0
- __pycache__/i18n.cpython-313.pyc +0 -0
- __pycache__/managed_auth.cpython-313.pyc +0 -0
- __pycache__/task_executor.cpython-313.pyc +0 -0
- config.py +8 -2
- core/__pycache__/checkpoint_store.cpython-313.pyc +0 -0
- core/__pycache__/desktop.ini +2 -0
- core/__pycache__/generation_runner.cpython-313.pyc +0 -0
- core/__pycache__/meta_extractor.cpython-313.pyc +0 -0
- core/__pycache__/pipeline_agent.cpython-313.pyc +3 -0
- core/__pycache__/production_guard.cpython-313.pyc +0 -0
- core/__pycache__/qualitative_engine.cpython-313.pyc +0 -0
- core/__pycache__/quantitative_engine.cpython-313.pyc +0 -0
- core/__pycache__/rag_engine.cpython-313.pyc +0 -0
- core/generation_runner.py +99 -15
- core/pipeline_agent.py +45 -69
- core/production_guard.py +35 -0
- smartpaper_restore_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- smartpaper_restore_bridge/__pycache__/desktop.ini +2 -0
- utils/__pycache__/desktop.ini +2 -0
- utils/__pycache__/docx_builder.cpython-313.pyc +0 -0
- utils/__pycache__/file_parser.cpython-313.pyc +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
core/__pycache__/pipeline_agent.cpython-313.pyc filter=lfs diff=lfs merge=lfs -text
|
__pycache__/app.cpython-313.pyc
ADDED
|
Binary file (61.3 kB). View file
|
|
|
__pycache__/config.cpython-313.pyc
ADDED
|
Binary file (31.9 kB). View file
|
|
|
__pycache__/desktop.ini
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[.ShellClassInfo]
|
| 2 |
+
IconResource=C:\Program Files\Google\Drive File Stream\129.0.1.0\GoogleDriveFS.exe,27
|
__pycache__/i18n.cpython-313.pyc
ADDED
|
Binary file (76.6 kB). View file
|
|
|
__pycache__/managed_auth.cpython-313.pyc
ADDED
|
Binary file (31.1 kB). View file
|
|
|
__pycache__/task_executor.cpython-313.pyc
ADDED
|
Binary file (33.7 kB). View file
|
|
|
config.py
CHANGED
|
@@ -326,10 +326,16 @@ def load_config():
|
|
| 326 |
},
|
| 327 |
],
|
| 328 |
"manuscript_validation": {
|
|
|
|
|
|
|
| 329 |
"minimum_total_ratio": 0.97,
|
| 330 |
"maximum_total_ratio": 1.03,
|
|
|
|
|
|
|
| 331 |
"minimum_section_words": 120,
|
| 332 |
"minimum_conclusion_words": 120,
|
|
|
|
|
|
|
| 333 |
"minimum_qualitative_themes": 3,
|
| 334 |
"minimum_qualitative_quotes": 8,
|
| 335 |
},
|
|
@@ -427,7 +433,7 @@ Literature Metadata:
|
|
| 427 |
4. ACADEMIC HEDGING: Use strategic hedging (e.g., 'This suggests a tendency toward X, bounded by Y').
|
| 428 |
5. NO SWEEPING GENERALIZATIONS: Every sentence must carry specific theoretical weight or empirical fact.
|
| 429 |
6. MECHANISM WITHOUT OVERCLAIMING: Articulate the theoretical mechanism precisely, but use causal wording only when the research design and frozen evidence justify causal identification.
|
| 430 |
-
7. ACADEMIC REGISTER:
|
| 431 |
8. PROHIBITED RHETORIC: Never use metaphors, analogies, rhetorical questions, literary embellishment, or promotional claims.
|
| 432 |
9. ABSTRACT DISCIPLINE: In abstracts, report only the minimum numerical evidence needed to convey the main finding; do not crowd the abstract with coefficients or test statistics.
|
| 433 |
10. STRUCTURAL DISCIPLINE: Cover only the assigned section and subsection. Do not recreate earlier sections, invent alternative headings, or omit required analytical elements.
|
|
@@ -455,7 +461,7 @@ Literature Metadata:
|
|
| 455 |
3. ACTOR/PROCESS LOGIC: Follow interpretation, action, coordination, sequence, comparison, or change as the selected analytic design and evidence warrant. Do not force a sensemaking lens onto every qualitative study.
|
| 456 |
4. NO FORMAL HYPOTHESES: Do not use formal hypothesis testing. Use a research question, process/model explanation, or propositions only when the selected qualitative design and evidence warrant them.
|
| 457 |
5. NO AI CLICHES: Strictly avoid 'delve', 'tapestry', 'multifaceted', 'beacon', 'landscape'.
|
| 458 |
-
6. ACADEMIC REGISTER:
|
| 459 |
7. PROHIBITED RHETORIC: Never use metaphors, analogies, rhetorical questions, literary embellishment, or promotional claims. Use concrete evidence-based description rather than decorative prose.
|
| 460 |
8. ABSTRACT DISCIPLINE: Keep numerical details to the minimum needed to communicate the empirical puzzle, main finding, and contribution.
|
| 461 |
9. CLAIM-SPECIFIC CITATION: Each sentence may contain at most one [CITE-XX] tag. Never group multiple references in one parenthesis or sentence. Separate distinct source-supported claims into separate sentences; when sources support the same claim, retain only the single most direct source. Every cited sentence must express a claim directly supported by the cited source evidence.""",
|
|
|
|
| 326 |
},
|
| 327 |
],
|
| 328 |
"manuscript_validation": {
|
| 329 |
+
# Word counts are planning/quality diagnostics only. They must never
|
| 330 |
+
# independently fail an otherwise complete manuscript.
|
| 331 |
"minimum_total_ratio": 0.97,
|
| 332 |
"maximum_total_ratio": 1.03,
|
| 333 |
+
"total_short_notice_ratio": 0.70,
|
| 334 |
+
"total_topic_review_ratio": 0.50,
|
| 335 |
"minimum_section_words": 120,
|
| 336 |
"minimum_conclusion_words": 120,
|
| 337 |
+
"section_short_notice_ratio": 0.60,
|
| 338 |
+
"section_topic_review_ratio": 0.40,
|
| 339 |
"minimum_qualitative_themes": 3,
|
| 340 |
"minimum_qualitative_quotes": 8,
|
| 341 |
},
|
|
|
|
| 433 |
4. ACADEMIC HEDGING: Use strategic hedging (e.g., 'This suggests a tendency toward X, bounded by Y').
|
| 434 |
5. NO SWEEPING GENERALIZATIONS: Every sentence must carry specific theoretical weight or empirical fact.
|
| 435 |
6. MECHANISM WITHOUT OVERCLAIMING: Articulate the theoretical mechanism precisely, but use causal wording only when the research design and frozen evidence justify causal identification.
|
| 436 |
+
7. ACADEMIC REGISTER: Follow CAR: Conciseness, Accuracy, and Rigor. Concepts must be explicit, hierarchy clear, and reasoning logically traceable. Word-count allocations are planning targets, not content quotas: never add repetition, unsupported claims, generic filler, or unnecessary detail merely to hit a target.
|
| 437 |
8. PROHIBITED RHETORIC: Never use metaphors, analogies, rhetorical questions, literary embellishment, or promotional claims.
|
| 438 |
9. ABSTRACT DISCIPLINE: In abstracts, report only the minimum numerical evidence needed to convey the main finding; do not crowd the abstract with coefficients or test statistics.
|
| 439 |
10. STRUCTURAL DISCIPLINE: Cover only the assigned section and subsection. Do not recreate earlier sections, invent alternative headings, or omit required analytical elements.
|
|
|
|
| 461 |
3. ACTOR/PROCESS LOGIC: Follow interpretation, action, coordination, sequence, comparison, or change as the selected analytic design and evidence warrant. Do not force a sensemaking lens onto every qualitative study.
|
| 462 |
4. NO FORMAL HYPOTHESES: Do not use formal hypothesis testing. Use a research question, process/model explanation, or propositions only when the selected qualitative design and evidence warrant them.
|
| 463 |
5. NO AI CLICHES: Strictly avoid 'delve', 'tapestry', 'multifaceted', 'beacon', 'landscape'.
|
| 464 |
+
6. ACADEMIC REGISTER: Follow CAR: Conciseness, Accuracy, and Rigor, with explicit concepts and logically ordered claims. Word-count allocations are planning targets, not content quotas: never add repetition, unsupported claims, generic filler, or unnecessary detail merely to hit a target.
|
| 465 |
7. PROHIBITED RHETORIC: Never use metaphors, analogies, rhetorical questions, literary embellishment, or promotional claims. Use concrete evidence-based description rather than decorative prose.
|
| 466 |
8. ABSTRACT DISCIPLINE: Keep numerical details to the minimum needed to communicate the empirical puzzle, main finding, and contribution.
|
| 467 |
9. CLAIM-SPECIFIC CITATION: Each sentence may contain at most one [CITE-XX] tag. Never group multiple references in one parenthesis or sentence. Separate distinct source-supported claims into separate sentences; when sources support the same claim, retain only the single most direct source. Every cited sentence must express a claim directly supported by the cited source evidence.""",
|
core/__pycache__/checkpoint_store.cpython-313.pyc
ADDED
|
Binary file (4.64 kB). View file
|
|
|
core/__pycache__/desktop.ini
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[.ShellClassInfo]
|
| 2 |
+
IconResource=C:\Program Files\Google\Drive File Stream\129.0.1.0\GoogleDriveFS.exe,27
|
core/__pycache__/generation_runner.cpython-313.pyc
ADDED
|
Binary file (70.2 kB). View file
|
|
|
core/__pycache__/meta_extractor.cpython-313.pyc
ADDED
|
Binary file (3.44 kB). View file
|
|
|
core/__pycache__/pipeline_agent.cpython-313.pyc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5ff1e1c8ae14f93f19fb2d92015df442e858db9e6cf8deccff732c4e0c8b1973
|
| 3 |
+
size 273806
|
core/__pycache__/production_guard.cpython-313.pyc
ADDED
|
Binary file (13.9 kB). View file
|
|
|
core/__pycache__/qualitative_engine.cpython-313.pyc
ADDED
|
Binary file (15.3 kB). View file
|
|
|
core/__pycache__/quantitative_engine.cpython-313.pyc
ADDED
|
Binary file (24.4 kB). View file
|
|
|
core/__pycache__/rag_engine.cpython-313.pyc
ADDED
|
Binary file (4.37 kB). View file
|
|
|
core/generation_runner.py
CHANGED
|
@@ -332,16 +332,95 @@ class GenerationRunner:
|
|
| 332 |
# Markdown punctuation is ignored; contractions/hyphenated terms stay one token.
|
| 333 |
return len(re.findall(r"\b[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*\b", body))
|
| 334 |
|
| 335 |
-
def _validate_target_length(self, manuscript: str, target_words: int) ->
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
quality = APP_CONFIG.get("manuscript_validation", {})
|
| 337 |
minimum = int(target_words * float(quality.get("minimum_total_ratio", 0.97)))
|
| 338 |
maximum = int(target_words * float(quality.get("maximum_total_ratio", 1.03)))
|
|
|
|
|
|
|
| 339 |
actual = self._english_count_for_target(manuscript)
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
|
| 346 |
def _resolve_paths(self, items) -> list[str]:
|
| 347 |
output = []
|
|
@@ -383,6 +462,7 @@ class GenerationRunner:
|
|
| 383 |
qual_engine = get_qualitative_engine(agent)
|
| 384 |
|
| 385 |
state = self.checkpoints.load_latest() or {}
|
|
|
|
| 386 |
if state:
|
| 387 |
self._preview_english_text = str(state.get('full_draft_en') or '')
|
| 388 |
self._preview_target_text = str(state.get('full_draft_translated') or '')
|
|
@@ -646,7 +726,7 @@ class GenerationRunner:
|
|
| 646 |
raise RuntimeError("task_cancel_requested")
|
| 647 |
remaining = sum(1 for item in adaptive_structure if item["display_name"] not in completed_sections)
|
| 648 |
adjusted_target = self._bounded_target(
|
| 649 |
-
int(sec["target_words"]),
|
| 650 |
)
|
| 651 |
sec_title = sec["section"]
|
| 652 |
subsection = sec.get("subsection")
|
|
@@ -745,7 +825,7 @@ Section: {sec_title}
|
|
| 745 |
Subsection: {subsection or 'None'}
|
| 746 |
Section reasoning role: {section_role}
|
| 747 |
Additional mode directive: {mode_directive}
|
| 748 |
-
|
| 749 |
{heading_directive}
|
| 750 |
{citation_rule}
|
| 751 |
{table_rule}
|
|
@@ -849,6 +929,9 @@ Output only final academic prose. Do not report the word count."""
|
|
| 849 |
context_tail = " ".join(en_raw.split()[-200:])
|
| 850 |
actual_words = self._english_count_for_target(en_resolved)
|
| 851 |
accumulated_deviation += actual_words - adjusted_target
|
|
|
|
|
|
|
|
|
|
| 852 |
completed_sections.append(display_name)
|
| 853 |
section_key = str(sec.get('section_id') or sec_title)
|
| 854 |
if self._section_is_complete(adaptive_structure, completed_sections, section_key):
|
|
@@ -869,6 +952,7 @@ Output only final academic prose. Do not report the word count."""
|
|
| 869 |
"translation_glossary": translation_glossary,
|
| 870 |
"context_tail": context_tail,
|
| 871 |
"accumulated_deviation": accumulated_deviation,
|
|
|
|
| 872 |
"agent_state": agent.export_state(),
|
| 873 |
"data_metadata": data_metadata,
|
| 874 |
}
|
|
@@ -900,13 +984,9 @@ Output only final academic prose. Do not report the word count."""
|
|
| 900 |
)
|
| 901 |
full_draft_en = full_draft_en.rstrip() + "\n\n# References\n" + "\n\n".join(references)
|
| 902 |
actual_words = self._english_count_for_target(full_draft_en)
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
if actual_words < minimum_words or actual_words > maximum_words:
|
| 907 |
-
full_draft_en = agent.rebalance_discussion_length(
|
| 908 |
-
full_draft_en, target_words=target_words, mode=mode, actual_words=actual_words
|
| 909 |
-
)
|
| 910 |
body_only, _reference_tail = agent.validate_reference_tail(
|
| 911 |
full_draft_en, minimum_references=MIN_FINAL_REFERENCES
|
| 912 |
)
|
|
@@ -931,7 +1011,9 @@ Output only final academic prose. Do not report the word count."""
|
|
| 931 |
)
|
| 932 |
agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
|
| 933 |
agent.enforce_resolved_claim_specific_citations(body_only)
|
| 934 |
-
self.
|
|
|
|
|
|
|
| 935 |
|
| 936 |
# Final front matter has a different source hash from the Working
|
| 937 |
# front matter. Retranslate only __front_matter__; unchanged body
|
|
@@ -958,6 +1040,7 @@ Output only final academic prose. Do not report the word count."""
|
|
| 958 |
"final_abstract": final_abstract,
|
| 959 |
"final_keywords": final_keywords,
|
| 960 |
"front_matter_phase": "final",
|
|
|
|
| 961 |
"english_finalized": True,
|
| 962 |
"translation_complete": False,
|
| 963 |
"agent_state": agent.export_state(),
|
|
@@ -1041,6 +1124,7 @@ Output only final academic prose. Do not report the word count."""
|
|
| 1041 |
mode=mode,
|
| 1042 |
citation_audit=agent.citation_audit_log,
|
| 1043 |
literature_metrics=agent.get_source_metrics(),
|
|
|
|
| 1044 |
)
|
| 1045 |
output_filename = f"SmartPaper_{bj_time}.zip"
|
| 1046 |
pending = self.output_dir / (".pending_" + output_filename)
|
|
|
|
| 332 |
# Markdown punctuation is ignored; contractions/hyphenated terms stay one token.
|
| 333 |
return len(re.findall(r"\b[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*\b", body))
|
| 334 |
|
| 335 |
+
def _validate_target_length(self, manuscript: str, target_words: int) -> dict[str, Any]:
|
| 336 |
+
"""Return a nonfatal total-length quality diagnostic.
|
| 337 |
+
|
| 338 |
+
The user-selected target remains a planning input, not a completion gate.
|
| 339 |
+
Completeness is validated separately by structure, evidence, citation,
|
| 340 |
+
translation, DOCX, and ZIP checks.
|
| 341 |
+
"""
|
| 342 |
quality = APP_CONFIG.get("manuscript_validation", {})
|
| 343 |
minimum = int(target_words * float(quality.get("minimum_total_ratio", 0.97)))
|
| 344 |
maximum = int(target_words * float(quality.get("maximum_total_ratio", 1.03)))
|
| 345 |
+
short_notice = float(quality.get("total_short_notice_ratio", 0.70))
|
| 346 |
+
topic_review = float(quality.get("total_topic_review_ratio", 0.50))
|
| 347 |
actual = self._english_count_for_target(manuscript)
|
| 348 |
+
ratio = (actual / max(1, int(target_words)))
|
| 349 |
+
status = "normal"
|
| 350 |
+
recommendation = ""
|
| 351 |
+
if ratio < topic_review:
|
| 352 |
+
status = "very_short"
|
| 353 |
+
recommendation = (
|
| 354 |
+
"First verify that generation completed normally and no section/evidence is missing. "
|
| 355 |
+
"If the manuscript is complete, consider broadening the research scope or selecting a new research topic."
|
| 356 |
+
)
|
| 357 |
+
elif ratio < short_notice:
|
| 358 |
+
status = "short"
|
| 359 |
+
recommendation = (
|
| 360 |
+
"If the manuscript is structurally complete, consider broadening the theoretical scope, context, "
|
| 361 |
+
"research question, or research topic."
|
| 362 |
)
|
| 363 |
+
elif actual < minimum:
|
| 364 |
+
status = "slightly_short"
|
| 365 |
+
elif actual > maximum:
|
| 366 |
+
status = "long"
|
| 367 |
+
if status != "normal":
|
| 368 |
+
logging.warning(
|
| 369 |
+
"Manuscript length diagnostic | actual=%s | target=%s | ratio=%.3f | status=%s | nonfatal=true",
|
| 370 |
+
actual, target_words, ratio, status,
|
| 371 |
+
)
|
| 372 |
+
return {
|
| 373 |
+
"scope": "manuscript", "name": "English manuscript", "actual_words": actual,
|
| 374 |
+
"target_words": int(target_words), "ratio": round(ratio, 4), "status": status,
|
| 375 |
+
"recommendation": recommendation, "nonfatal": True,
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
@staticmethod
|
| 379 |
+
def _upsert_quality_diagnostic(items: list[dict[str, Any]], diagnostic: dict[str, Any]) -> None:
|
| 380 |
+
key = (str(diagnostic.get("scope") or ""), str(diagnostic.get("name") or ""))
|
| 381 |
+
items[:] = [
|
| 382 |
+
item for item in items
|
| 383 |
+
if (str(item.get("scope") or ""), str(item.get("name") or "")) != key
|
| 384 |
+
]
|
| 385 |
+
items.append(dict(diagnostic))
|
| 386 |
+
|
| 387 |
+
def _section_word_diagnostic(self, name: str, actual_words: int, target_words: int) -> dict[str, Any]:
|
| 388 |
+
validation = APP_CONFIG.get("manuscript_validation", {})
|
| 389 |
+
budget = APP_CONFIG.get("word_budget", {})
|
| 390 |
+
target = max(1, int(target_words))
|
| 391 |
+
actual = max(0, int(actual_words))
|
| 392 |
+
ratio = actual / target
|
| 393 |
+
short_notice = float(validation.get("section_short_notice_ratio", 0.60))
|
| 394 |
+
topic_review = float(validation.get("section_topic_review_ratio", 0.40))
|
| 395 |
+
preferred_min = float(budget.get("section_min_ratio", 0.92))
|
| 396 |
+
preferred_max = float(budget.get("section_max_ratio", 1.08))
|
| 397 |
+
status = "normal"
|
| 398 |
+
recommendation = ""
|
| 399 |
+
if ratio < topic_review:
|
| 400 |
+
status = "very_short"
|
| 401 |
+
recommendation = (
|
| 402 |
+
"First verify that this section completed normally and that evidence was available. "
|
| 403 |
+
"If it is complete, consider broadening the research scope or selecting a new research topic."
|
| 404 |
+
)
|
| 405 |
+
elif ratio < short_notice:
|
| 406 |
+
status = "short"
|
| 407 |
+
recommendation = (
|
| 408 |
+
"If the section is complete, consider broadening the theoretical scope, context, research question, "
|
| 409 |
+
"or research topic."
|
| 410 |
+
)
|
| 411 |
+
elif ratio < preferred_min:
|
| 412 |
+
status = "slightly_short"
|
| 413 |
+
elif ratio > preferred_max:
|
| 414 |
+
status = "long"
|
| 415 |
+
if status != "normal":
|
| 416 |
+
logging.warning(
|
| 417 |
+
"Section length diagnostic | section=%s | actual=%s | target=%s | ratio=%.3f | status=%s | nonfatal=true",
|
| 418 |
+
name, actual, target, ratio, status,
|
| 419 |
+
)
|
| 420 |
+
return {
|
| 421 |
+
"scope": "section", "name": str(name), "actual_words": actual, "target_words": target,
|
| 422 |
+
"ratio": round(ratio, 4), "status": status, "recommendation": recommendation, "nonfatal": True,
|
| 423 |
+
}
|
| 424 |
|
| 425 |
def _resolve_paths(self, items) -> list[str]:
|
| 426 |
output = []
|
|
|
|
| 462 |
qual_engine = get_qualitative_engine(agent)
|
| 463 |
|
| 464 |
state = self.checkpoints.load_latest() or {}
|
| 465 |
+
quality_diagnostics = list(state.get("quality_diagnostics") or [])
|
| 466 |
if state:
|
| 467 |
self._preview_english_text = str(state.get('full_draft_en') or '')
|
| 468 |
self._preview_target_text = str(state.get('full_draft_translated') or '')
|
|
|
|
| 726 |
raise RuntimeError("task_cancel_requested")
|
| 727 |
remaining = sum(1 for item in adaptive_structure if item["display_name"] not in completed_sections)
|
| 728 |
adjusted_target = self._bounded_target(
|
| 729 |
+
int(sec["target_words"]), 0.0, remaining, safe_chunk_words
|
| 730 |
)
|
| 731 |
sec_title = sec["section"]
|
| 732 |
subsection = sec.get("subsection")
|
|
|
|
| 825 |
Subsection: {subsection or 'None'}
|
| 826 |
Section reasoning role: {section_role}
|
| 827 |
Additional mode directive: {mode_directive}
|
| 828 |
+
Planning length: approximately {int(adjusted_target * min_ratio)}-{int(adjusted_target * max_ratio)} words. This is guidance, not a content quota. Prioritize Conciseness, Accuracy, and Rigor; never add filler or remove useful reasoning solely to hit the range.
|
| 829 |
{heading_directive}
|
| 830 |
{citation_rule}
|
| 831 |
{table_rule}
|
|
|
|
| 929 |
context_tail = " ".join(en_raw.split()[-200:])
|
| 930 |
actual_words = self._english_count_for_target(en_resolved)
|
| 931 |
accumulated_deviation += actual_words - adjusted_target
|
| 932 |
+
self._upsert_quality_diagnostic(
|
| 933 |
+
quality_diagnostics, self._section_word_diagnostic(display_name, actual_words, adjusted_target)
|
| 934 |
+
)
|
| 935 |
completed_sections.append(display_name)
|
| 936 |
section_key = str(sec.get('section_id') or sec_title)
|
| 937 |
if self._section_is_complete(adaptive_structure, completed_sections, section_key):
|
|
|
|
| 952 |
"translation_glossary": translation_glossary,
|
| 953 |
"context_tail": context_tail,
|
| 954 |
"accumulated_deviation": accumulated_deviation,
|
| 955 |
+
"quality_diagnostics": quality_diagnostics,
|
| 956 |
"agent_state": agent.export_state(),
|
| 957 |
"data_metadata": data_metadata,
|
| 958 |
}
|
|
|
|
| 984 |
)
|
| 985 |
full_draft_en = full_draft_en.rstrip() + "\n\n# References\n" + "\n\n".join(references)
|
| 986 |
actual_words = self._english_count_for_target(full_draft_en)
|
| 987 |
+
self._upsert_quality_diagnostic(
|
| 988 |
+
quality_diagnostics, self._validate_target_length(full_draft_en, target_words)
|
| 989 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 990 |
body_only, _reference_tail = agent.validate_reference_tail(
|
| 991 |
full_draft_en, minimum_references=MIN_FINAL_REFERENCES
|
| 992 |
)
|
|
|
|
| 1011 |
)
|
| 1012 |
agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
|
| 1013 |
agent.enforce_resolved_claim_specific_citations(body_only)
|
| 1014 |
+
self._upsert_quality_diagnostic(
|
| 1015 |
+
quality_diagnostics, self._validate_target_length(full_draft_en, target_words)
|
| 1016 |
+
)
|
| 1017 |
|
| 1018 |
# Final front matter has a different source hash from the Working
|
| 1019 |
# front matter. Retranslate only __front_matter__; unchanged body
|
|
|
|
| 1040 |
"final_abstract": final_abstract,
|
| 1041 |
"final_keywords": final_keywords,
|
| 1042 |
"front_matter_phase": "final",
|
| 1043 |
+
"quality_diagnostics": quality_diagnostics,
|
| 1044 |
"english_finalized": True,
|
| 1045 |
"translation_complete": False,
|
| 1046 |
"agent_state": agent.export_state(),
|
|
|
|
| 1124 |
mode=mode,
|
| 1125 |
citation_audit=agent.citation_audit_log,
|
| 1126 |
literature_metrics=agent.get_source_metrics(),
|
| 1127 |
+
quality_diagnostics=quality_diagnostics,
|
| 1128 |
)
|
| 1129 |
output_filename = f"SmartPaper_{bj_time}.zip"
|
| 1130 |
pending = self.output_dir / (".pending_" + output_filename)
|
core/pipeline_agent.py
CHANGED
|
@@ -1257,14 +1257,35 @@ Return a concise PROVISIONAL RESEARCH LOGIC CONTRACT in plain English prose. It
|
|
| 1257 |
or re.search(r'\([^()\n]*?(?:19|20)\d{2}[a-z]?[^()\n]*?\)', text, flags=re.I)
|
| 1258 |
)
|
| 1259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1260 |
@classmethod
|
| 1261 |
def _validate_working_abstract(cls, value):
|
|
|
|
| 1262 |
text = re.sub(r'(?im)^#+\s*abstract\s*', '', str(value or '')).strip()
|
| 1263 |
-
|
| 1264 |
-
|
| 1265 |
-
|
| 1266 |
-
f'Working Abstract must contain 80-180 English words; got {len(words)}.'
|
| 1267 |
-
)
|
| 1268 |
if cls._front_matter_has_citation(text):
|
| 1269 |
raise DeepSeekRequestError('Working Abstract must not contain citations.')
|
| 1270 |
forbidden = (
|
|
@@ -1277,6 +1298,11 @@ Return a concise PROVISIONAL RESEARCH LOGIC CONTRACT in plain English prose. It
|
|
| 1277 |
)
|
| 1278 |
if any(re.search(pattern, text, flags=re.I) for pattern in forbidden):
|
| 1279 |
raise DeepSeekRequestError('Working Abstract asserted a result that is not yet frozen.')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1280 |
return text
|
| 1281 |
|
| 1282 |
def generate_working_front_matter(
|
|
@@ -1288,7 +1314,7 @@ Return a concise PROVISIONAL RESEARCH LOGIC CONTRACT in plain English prose. It
|
|
| 1288 |
synthesis = self._truncate_context(str(synthesis_result or ''), 3500)
|
| 1289 |
prompt = f"""Create WORKING front matter for an empirical {str(mode).upper()}-oriented manuscript before body drafting.
|
| 1290 |
Return JSON only: {{\"abstract\":\"...\",\"keywords\":[\"...\"]}}.
|
| 1291 |
-
|
| 1292 |
Return 4-6 concise academic keywords. User keywords are seeds, not an authoritative final list.
|
| 1293 |
TOPIC: {topic}
|
| 1294 |
USER KEYWORD SEEDS: {keywords}
|
|
@@ -1318,7 +1344,7 @@ USER KEYWORD SEEDS: {keywords}
|
|
| 1318 |
body = self._abstract_manuscript_context(manuscript, 18000)
|
| 1319 |
prompt = f"""Write FINAL English front matter for this completed {str(mode).upper()} manuscript.
|
| 1320 |
Return JSON only: {{\"abstract\":\"...\",\"keywords\":[\"...\"]}}.
|
| 1321 |
-
|
| 1322 |
Return 4-6 concise final academic keywords derived from the completed manuscript. User keyword seeds are advisory only.
|
| 1323 |
TOPIC: {topic}
|
| 1324 |
USER KEYWORD SEEDS: {keywords}
|
|
@@ -1342,9 +1368,14 @@ USER KEYWORD SEEDS: {keywords}
|
|
| 1342 |
if self._front_matter_has_citation(abstract):
|
| 1343 |
raise DeepSeekRequestError('Final Abstract revision introduced a citation.')
|
| 1344 |
max_words = max(80, int(APP_CONFIG.get('manuscript_quality', {}).get('abstract_max_words', 200)))
|
| 1345 |
-
|
| 1346 |
-
|
| 1347 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1348 |
final_keywords = self._front_matter_keywords(parsed.get('keywords'))
|
| 1349 |
return {'abstract': abstract, 'keywords': final_keywords}
|
| 1350 |
|
|
@@ -1544,7 +1575,10 @@ USER KEYWORD SEEDS: {keywords}
|
|
| 1544 |
count = len(match.group(1).split())
|
| 1545 |
required = minimum_conclusion if title == "Conclusion" else minimum_words
|
| 1546 |
if count < required:
|
| 1547 |
-
|
|
|
|
|
|
|
|
|
|
| 1548 |
|
| 1549 |
if errors:
|
| 1550 |
raise DeepSeekAPIError(
|
|
@@ -3195,64 +3229,6 @@ Output only the repaired section."""
|
|
| 3195 |
logging.info('Top-journal local repair accepted: section=%s issue=%s', section_name, instruction[:180])
|
| 3196 |
return self._remove_ai_meta_traces(revised)
|
| 3197 |
|
| 3198 |
-
def rebalance_discussion_length(self, manuscript, *, target_words, mode, actual_words):
|
| 3199 |
-
"""Use Discussion as the bounded final word-budget correction surface."""
|
| 3200 |
-
value = str(manuscript or '')
|
| 3201 |
-
delta = int(target_words) - int(actual_words)
|
| 3202 |
-
if abs(delta) < 40:
|
| 3203 |
-
return value
|
| 3204 |
-
match = re.search(r'(?ms)^#\s+Discussion\s*$\n(.*?)(?=^#\s+[^#].*?$|\Z)', value)
|
| 3205 |
-
if not match:
|
| 3206 |
-
return value
|
| 3207 |
-
original = match.group(1).strip()
|
| 3208 |
-
if len(original.split()) < 200:
|
| 3209 |
-
return value
|
| 3210 |
-
action = 'expand' if delta > 0 else 'compress'
|
| 3211 |
-
requested = min(abs(delta), max(120, int(len(original.split()) * 0.28)))
|
| 3212 |
-
prompt = f"""{action.capitalize()} this Discussion section by approximately {requested} words so the completed manuscript approaches its requested total length.
|
| 3213 |
-
Preserve every heading, number, resolved author-year citation, quotation, hypothesis label, variable abbreviation, and conclusion direction. Do not add new empirical facts, sources, statistics, quotations, or methods. If expanding, deepen mechanism, theoretical boundary, and interpretation already supported by the section. If compressing, remove repetition and low-information phrasing without deleting a substantive finding or citation.
|
| 3214 |
-
MODE: {mode}
|
| 3215 |
-
TEXT:
|
| 3216 |
-
{original}
|
| 3217 |
-
Output only the revised Discussion body, without the '# Discussion' heading."""
|
| 3218 |
-
revised = self.call_deepseek(
|
| 3219 |
-
'You are a precise top-journal length editor.', prompt,
|
| 3220 |
-
max_tokens=max(1800, min(8000, self._count_tokens(original) + max(800, requested * 2))),
|
| 3221 |
-
temp=0.1, model_role='writing', thinking=False, allow_fallback=False,
|
| 3222 |
-
).strip()
|
| 3223 |
-
if self._numeric_tokens(original) != self._numeric_tokens(revised):
|
| 3224 |
-
logging.warning('Word-budget Discussion repair rejected because numbers changed.')
|
| 3225 |
-
return value
|
| 3226 |
-
cite_pattern = r'\([^()\n]*?(?:19|20)\d{2}[a-z]?[^()\n]*?\)'
|
| 3227 |
-
if Counter(re.findall(cite_pattern, original, re.I)) != Counter(re.findall(cite_pattern, revised, re.I)):
|
| 3228 |
-
logging.warning('Word-budget Discussion repair rejected because citations changed.')
|
| 3229 |
-
return value
|
| 3230 |
-
if self.find_multi_citation_violations(revised):
|
| 3231 |
-
logging.warning('Word-budget Discussion repair rejected because it stacked multiple sources in one sentence.')
|
| 3232 |
-
return value
|
| 3233 |
-
return value[:match.start(1)] + revised + '\n\n' + value[match.end(1):]
|
| 3234 |
-
|
| 3235 |
-
@staticmethod
|
| 3236 |
-
def _trim_at_sentence_boundary(value, max_words):
|
| 3237 |
-
text = str(value or '').strip()
|
| 3238 |
-
words = text.split()
|
| 3239 |
-
if len(words) <= max_words:
|
| 3240 |
-
return text
|
| 3241 |
-
sentences = re.split(r'(?<=[.!?])\s+', text)
|
| 3242 |
-
selected = []
|
| 3243 |
-
count = 0
|
| 3244 |
-
for sentence in sentences:
|
| 3245 |
-
sentence_words = sentence.split()
|
| 3246 |
-
if not sentence_words:
|
| 3247 |
-
continue
|
| 3248 |
-
if count + len(sentence_words) > max_words:
|
| 3249 |
-
break
|
| 3250 |
-
selected.append(sentence.strip())
|
| 3251 |
-
count += len(sentence_words)
|
| 3252 |
-
if selected:
|
| 3253 |
-
return ' '.join(selected).strip()
|
| 3254 |
-
return ' '.join(words[:max_words]).rstrip(' ,;:') + '.'
|
| 3255 |
-
|
| 3256 |
def _abstract_manuscript_context(self, manuscript, max_tokens=18000):
|
| 3257 |
value = str(manuscript or '').strip()
|
| 3258 |
if self._count_tokens(value) <= max_tokens:
|
|
|
|
| 1257 |
or re.search(r'\([^()\n]*?(?:19|20)\d{2}[a-z]?[^()\n]*?\)', text, flags=re.I)
|
| 1258 |
)
|
| 1259 |
|
| 1260 |
+
@staticmethod
|
| 1261 |
+
def _working_abstract_word_count(value):
|
| 1262 |
+
return len(re.findall(r"\b[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*\b", str(value or '')))
|
| 1263 |
+
|
| 1264 |
+
@classmethod
|
| 1265 |
+
def _trim_working_abstract_overflow(cls, text, maximum=180, minimum=80):
|
| 1266 |
+
"""Trim only a small overflow at a complete sentence boundary."""
|
| 1267 |
+
sentences = [part.strip() for part in re.split(r'(?<=[.!?])\s+', str(text or '').strip()) if part.strip()]
|
| 1268 |
+
kept = []
|
| 1269 |
+
for sentence in sentences:
|
| 1270 |
+
candidate = ' '.join(kept + [sentence]).strip()
|
| 1271 |
+
if cls._working_abstract_word_count(candidate) > maximum:
|
| 1272 |
+
break
|
| 1273 |
+
kept.append(sentence)
|
| 1274 |
+
trimmed = ' '.join(kept).strip()
|
| 1275 |
+
count = cls._working_abstract_word_count(trimmed)
|
| 1276 |
+
if minimum <= count <= maximum:
|
| 1277 |
+
return trimmed
|
| 1278 |
+
raise DeepSeekRequestError(
|
| 1279 |
+
f'Working Abstract overflow could not be safely trimmed to {minimum}-{maximum} English words at a sentence boundary.'
|
| 1280 |
+
)
|
| 1281 |
+
|
| 1282 |
@classmethod
|
| 1283 |
def _validate_working_abstract(cls, value):
|
| 1284 |
+
"""Validate semantic safety; word count is quality guidance, never a fatal gate."""
|
| 1285 |
text = re.sub(r'(?im)^#+\s*abstract\s*', '', str(value or '')).strip()
|
| 1286 |
+
if not text:
|
| 1287 |
+
raise DeepSeekRequestError('Working Abstract is empty.')
|
| 1288 |
+
word_count = cls._working_abstract_word_count(text)
|
|
|
|
|
|
|
| 1289 |
if cls._front_matter_has_citation(text):
|
| 1290 |
raise DeepSeekRequestError('Working Abstract must not contain citations.')
|
| 1291 |
forbidden = (
|
|
|
|
| 1298 |
)
|
| 1299 |
if any(re.search(pattern, text, flags=re.I) for pattern in forbidden):
|
| 1300 |
raise DeepSeekRequestError('Working Abstract asserted a result that is not yet frozen.')
|
| 1301 |
+
if word_count < 80 or word_count > 180:
|
| 1302 |
+
logging.warning(
|
| 1303 |
+
'Working Abstract length diagnostic | words=%s | preferred=80-180 | nonfatal=true',
|
| 1304 |
+
word_count,
|
| 1305 |
+
)
|
| 1306 |
return text
|
| 1307 |
|
| 1308 |
def generate_working_front_matter(
|
|
|
|
| 1314 |
synthesis = self._truncate_context(str(synthesis_result or ''), 3500)
|
| 1315 |
prompt = f"""Create WORKING front matter for an empirical {str(mode).upper()}-oriented manuscript before body drafting.
|
| 1316 |
Return JSON only: {{\"abstract\":\"...\",\"keywords\":[\"...\"]}}.
|
| 1317 |
+
Aim for approximately 120-180 English words, have no citations, and function only as a continuity anchor. Word count is guidance, not a failure condition; prioritize Conciseness, Accuracy, and Rigor over mechanical compliance. It may state the research problem, theoretical tension, focal concepts, question/hypothesis architecture, design or method class, and intended contribution domain. It MUST NOT state coefficients, p-values, fit statistics, sample statistics unless already indispensable to define the design, supported/unsupported hypotheses, result direction, substantive findings, or a conclusion that has not yet been frozen. Do not predict findings.
|
| 1318 |
Return 4-6 concise academic keywords. User keywords are seeds, not an authoritative final list.
|
| 1319 |
TOPIC: {topic}
|
| 1320 |
USER KEYWORD SEEDS: {keywords}
|
|
|
|
| 1344 |
body = self._abstract_manuscript_context(manuscript, 18000)
|
| 1345 |
prompt = f"""Write FINAL English front matter for this completed {str(mode).upper()} manuscript.
|
| 1346 |
Return JSON only: {{\"abstract\":\"...\",\"keywords\":[\"...\"]}}.
|
| 1347 |
+
Aim for a concise Abstract of roughly 120-200 words, use no citations, and accurately state the research problem, validated design, main frozen finding(s), and contribution. Word count is guidance, not a failure condition; prioritize Conciseness, Accuracy, and Rigor. Include only the minimum numerical evidence required; do not list coefficients, p-values, and fit statistics unless one value is indispensable. Do not infer beyond the frozen evidence or completed manuscript.
|
| 1348 |
Return 4-6 concise final academic keywords derived from the completed manuscript. User keyword seeds are advisory only.
|
| 1349 |
TOPIC: {topic}
|
| 1350 |
USER KEYWORD SEEDS: {keywords}
|
|
|
|
| 1368 |
if self._front_matter_has_citation(abstract):
|
| 1369 |
raise DeepSeekRequestError('Final Abstract revision introduced a citation.')
|
| 1370 |
max_words = max(80, int(APP_CONFIG.get('manuscript_quality', {}).get('abstract_max_words', 200)))
|
| 1371 |
+
if not abstract:
|
| 1372 |
+
raise DeepSeekRequestError('Final Abstract is empty after validation.')
|
| 1373 |
+
final_word_count = self._working_abstract_word_count(abstract)
|
| 1374 |
+
if final_word_count < 80 or final_word_count > max_words:
|
| 1375 |
+
logging.warning(
|
| 1376 |
+
'Final Abstract length diagnostic | words=%s | preferred=80-%s | nonfatal=true',
|
| 1377 |
+
final_word_count, max_words,
|
| 1378 |
+
)
|
| 1379 |
final_keywords = self._front_matter_keywords(parsed.get('keywords'))
|
| 1380 |
return {'abstract': abstract, 'keywords': final_keywords}
|
| 1381 |
|
|
|
|
| 1575 |
count = len(match.group(1).split())
|
| 1576 |
required = minimum_conclusion if title == "Conclusion" else minimum_words
|
| 1577 |
if count < required:
|
| 1578 |
+
logging.warning(
|
| 1579 |
+
"Manuscript section length diagnostic | section=%s | words=%s | reference_min=%s | nonfatal=true",
|
| 1580 |
+
title, count, required,
|
| 1581 |
+
)
|
| 1582 |
|
| 1583 |
if errors:
|
| 1584 |
raise DeepSeekAPIError(
|
|
|
|
| 3229 |
logging.info('Top-journal local repair accepted: section=%s issue=%s', section_name, instruction[:180])
|
| 3230 |
return self._remove_ai_meta_traces(revised)
|
| 3231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3232 |
def _abstract_manuscript_context(self, manuscript, max_tokens=18000):
|
| 3233 |
value = str(manuscript or '').strip()
|
| 3234 |
if self._count_tokens(value) <= max_tokens:
|
core/production_guard.py
CHANGED
|
@@ -116,6 +116,39 @@ def build_generation_notes(data_metadata: dict[str, Any], mode: str) -> str:
|
|
| 116 |
return "\n".join(lines).strip() + "\n"
|
| 117 |
|
| 118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
def _manifest(entries: dict[str, bytes]) -> str:
|
| 120 |
rows = []
|
| 121 |
for name in sorted(entries):
|
|
@@ -133,6 +166,7 @@ def build_validated_package(
|
|
| 133 |
mode: str,
|
| 134 |
citation_audit: list[dict[str, Any]],
|
| 135 |
literature_metrics: dict[str, Any],
|
|
|
|
| 136 |
) -> PackageResult:
|
| 137 |
validate_markdown_manuscript(english_markdown, language_name="English")
|
| 138 |
validate_markdown_manuscript(translated_markdown, language_name="Target-language")
|
|
@@ -151,6 +185,7 @@ def build_validated_package(
|
|
| 151 |
"GENERATION_NOTES.txt": build_generation_notes(data_metadata, mode).encode("utf-8"),
|
| 152 |
"CITATION_AUDIT.json": json.dumps(citation_audit or [], ensure_ascii=False, indent=2).encode("utf-8"),
|
| 153 |
"LITERATURE_METRICS.json": json.dumps(literature_metrics or {}, ensure_ascii=False, indent=2).encode("utf-8"),
|
|
|
|
| 154 |
}
|
| 155 |
manifest = _manifest(entries)
|
| 156 |
entries["FILE_MANIFEST_SHA256.txt"] = manifest.encode("utf-8")
|
|
|
|
| 116 |
return "\n".join(lines).strip() + "\n"
|
| 117 |
|
| 118 |
|
| 119 |
+
|
| 120 |
+
def build_word_count_quality_report(diagnostics: list[dict[str, Any]] | None) -> str:
|
| 121 |
+
"""Human-readable, nonfatal word-count guidance for the author."""
|
| 122 |
+
items = [dict(item) for item in (diagnostics or []) if isinstance(item, dict)]
|
| 123 |
+
lines = [
|
| 124 |
+
"SmartPaper Word-Count Quality Report",
|
| 125 |
+
"",
|
| 126 |
+
"Policy: Word-count targets are planning guidance, not success/failure gates.",
|
| 127 |
+
"Writing quality is governed by CAR: Conciseness, Accuracy, and Rigor.",
|
| 128 |
+
"Long sections are not truncated merely to satisfy a target. Short sections are diagnosed before any topic recommendation.",
|
| 129 |
+
"",
|
| 130 |
+
]
|
| 131 |
+
flagged = [item for item in items if str(item.get("status") or "normal") != "normal"]
|
| 132 |
+
if not flagged:
|
| 133 |
+
lines.append("No material word-count deviation was detected.")
|
| 134 |
+
else:
|
| 135 |
+
lines.append("Diagnostics:")
|
| 136 |
+
for item in flagged:
|
| 137 |
+
actual = item.get("actual_words")
|
| 138 |
+
target = item.get("target_words")
|
| 139 |
+
status = str(item.get("status") or "")
|
| 140 |
+
name = str(item.get("name") or item.get("scope") or "Manuscript")
|
| 141 |
+
lines.append(f"- {name}: actual={actual}, planning target={target}, status={status}. Generation continued normally.")
|
| 142 |
+
recommendation = str(item.get("recommendation") or "").strip()
|
| 143 |
+
if recommendation:
|
| 144 |
+
lines.append(f" Author guidance: {recommendation}")
|
| 145 |
+
lines.extend([
|
| 146 |
+
"",
|
| 147 |
+
"Interpretation note:",
|
| 148 |
+
"A short section does not by itself prove that the research topic is unsuitable. It may reflect a narrow topic, limited evidence, a deliberately concise argument, or an incomplete generation step. Verify completeness first. If multiple core sections remain substantially short despite complete generation and adequate evidence, broaden the scope or consider selecting a new research topic.",
|
| 149 |
+
])
|
| 150 |
+
return "\n".join(lines).strip() + "\n"
|
| 151 |
+
|
| 152 |
def _manifest(entries: dict[str, bytes]) -> str:
|
| 153 |
rows = []
|
| 154 |
for name in sorted(entries):
|
|
|
|
| 166 |
mode: str,
|
| 167 |
citation_audit: list[dict[str, Any]],
|
| 168 |
literature_metrics: dict[str, Any],
|
| 169 |
+
quality_diagnostics: list[dict[str, Any]] | None = None,
|
| 170 |
) -> PackageResult:
|
| 171 |
validate_markdown_manuscript(english_markdown, language_name="English")
|
| 172 |
validate_markdown_manuscript(translated_markdown, language_name="Target-language")
|
|
|
|
| 185 |
"GENERATION_NOTES.txt": build_generation_notes(data_metadata, mode).encode("utf-8"),
|
| 186 |
"CITATION_AUDIT.json": json.dumps(citation_audit or [], ensure_ascii=False, indent=2).encode("utf-8"),
|
| 187 |
"LITERATURE_METRICS.json": json.dumps(literature_metrics or {}, ensure_ascii=False, indent=2).encode("utf-8"),
|
| 188 |
+
"WORD_COUNT_QUALITY_REPORT.txt": build_word_count_quality_report(quality_diagnostics).encode("utf-8"),
|
| 189 |
}
|
| 190 |
manifest = _manifest(entries)
|
| 191 |
entries["FILE_MANIFEST_SHA256.txt"] = manifest.encode("utf-8")
|
smartpaper_restore_bridge/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (3.14 kB). View file
|
|
|
smartpaper_restore_bridge/__pycache__/desktop.ini
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[.ShellClassInfo]
|
| 2 |
+
IconResource=C:\Program Files\Google\Drive File Stream\129.0.1.0\GoogleDriveFS.exe,27
|
utils/__pycache__/desktop.ini
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[.ShellClassInfo]
|
| 2 |
+
IconResource=C:\Program Files\Google\Drive File Stream\129.0.1.0\GoogleDriveFS.exe,27
|
utils/__pycache__/docx_builder.cpython-313.pyc
ADDED
|
Binary file (9.4 kB). View file
|
|
|
utils/__pycache__/file_parser.cpython-313.pyc
ADDED
|
Binary file (8.03 kB). View file
|
|
|