Step 4A-6-C-R3.2.8.5-R1.2 CAR Writing Quality + Nonfatal Word-Count Diagnostics + Content Sufficiency Gate

#24
.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: Use concise, accurate, and rigorous language. Concepts must be explicit, hierarchy clear, and reasoning logically traceable.
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: Use concise, accurate, and rigorous language with explicit concepts and logically ordered claims.
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) -> None:
 
 
 
 
 
 
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
- if actual < minimum or actual > maximum:
341
- raise RuntimeError(
342
- f"English manuscript length outside validated range: actual={actual}, "
343
- f"required={minimum}-{maximum}, target={target_words}."
 
 
 
 
 
 
 
 
 
 
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"]), accumulated_deviation, remaining, safe_chunk_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
- Target length: {int(adjusted_target * min_ratio)}-{int(adjusted_target * max_ratio)} words.
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
- quality = APP_CONFIG.get("manuscript_validation", {})
904
- minimum_words = int(target_words * float(quality.get("minimum_total_ratio", 0.97)))
905
- maximum_words = int(target_words * float(quality.get("maximum_total_ratio", 1.03)))
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._validate_target_length(full_draft_en, target_words)
 
 
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
- words = re.findall(r"\b[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*\b", text)
1264
- if len(words) < 80 or len(words) > 180:
1265
- raise DeepSeekRequestError(
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
- The Working Abstract must be 120-180 English words, have no citations, and function only as a continuity anchor. 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.
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
- The Abstract must be no more than 200 words, use no citations, and accurately state the research problem, validated design, main frozen finding(s), and contribution. 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.
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
- abstract = self._trim_at_sentence_boundary(abstract, max_words)
1346
- if len(re.findall(r"\b[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*\b", abstract)) < 80:
1347
- raise DeepSeekRequestError('Final Abstract is too short after validation.')
 
 
 
 
 
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
- errors.append(f"section {title!r} has {count} words; required>={required}")
 
 
 
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