@litfamily/litclaude 1.0.19 → 1.0.21

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.
Files changed (101) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +14 -8
  3. package/README_ko-KR.md +13 -8
  4. package/docs/assets/readme/badge-version.svg +1 -1
  5. package/docs/hooks.md +3 -2
  6. package/package.json +1 -1
  7. package/plugins/litclaude/.claude-plugin/plugin.json +1 -1
  8. package/plugins/litclaude/bin/litclaude-hook.js +4 -4
  9. package/plugins/litclaude/bin/litclaude-mcp.js +1 -1
  10. package/plugins/litclaude/lib/canonical-skill-resources.mjs +91 -31
  11. package/plugins/litclaude/skills/frontend-ui-ux/ATTRIBUTION.md +3 -6
  12. package/plugins/litclaude/skills/lit-docx/SKILL.md +50 -19
  13. package/plugins/litclaude/skills/lit-docx/references/components.md +207 -0
  14. package/plugins/litclaude/skills/lit-docx/references/direction-step.md +277 -0
  15. package/plugins/litclaude/skills/lit-docx/references/examples/01-report-ko.md +166 -0
  16. package/plugins/litclaude/skills/lit-docx/references/examples/02-brief-ko.md +105 -0
  17. package/plugins/litclaude/skills/lit-docx/references/examples/03-manual-ko.md +164 -0
  18. package/plugins/litclaude/skills/lit-docx/references/examples/04-proposal-ko.md +138 -0
  19. package/plugins/litclaude/skills/lit-docx/references/examples/05-memo-en.md +55 -0
  20. package/plugins/litclaude/skills/lit-docx/references/examples/06-journal-en.md +130 -0
  21. package/plugins/litclaude/skills/lit-docx/references/examples/07-report-en.md +123 -0
  22. package/plugins/litclaude/skills/lit-docx/references/examples/08-proposal-en.md +123 -0
  23. package/plugins/litclaude/skills/lit-docx/references/examples/09-memo-ko.md +45 -0
  24. package/plugins/litclaude/skills/lit-docx/references/page-composition.md +221 -0
  25. package/plugins/litclaude/skills/lit-docx/references/tonalities/brief.md +143 -0
  26. package/plugins/litclaude/skills/lit-docx/references/tonalities/journal.md +142 -0
  27. package/plugins/litclaude/skills/lit-docx/references/tonalities/manual.md +148 -0
  28. package/plugins/litclaude/skills/lit-docx/references/tonalities/memo.md +137 -0
  29. package/plugins/litclaude/skills/lit-docx/references/tonalities/proposal.md +140 -0
  30. package/plugins/litclaude/skills/lit-docx/references/tonalities/report.md +156 -0
  31. package/plugins/litclaude/skills/lit-docx/scripts/convert_md_to_docx.py +274 -73
  32. package/plugins/litclaude/skills/lit-docx/scripts/docx_design.py +2449 -0
  33. package/plugins/litclaude/skills/lit-docx/scripts/docx_layout.py +959 -0
  34. package/plugins/litclaude/skills/lit-docx/scripts/qa_docx.py +66 -9
  35. package/plugins/litclaude/skills/lit-docx/scripts/slop_lint.py +18 -6
  36. package/plugins/litclaude/skills/lit-docx/templates/tonalities/brief.yaml +28 -0
  37. package/plugins/litclaude/skills/lit-docx/templates/tonalities/journal.yaml +25 -0
  38. package/plugins/litclaude/skills/lit-docx/templates/tonalities/manual.yaml +26 -0
  39. package/plugins/litclaude/skills/lit-docx/templates/tonalities/memo.yaml +26 -0
  40. package/plugins/litclaude/skills/lit-docx/templates/tonalities/proposal.yaml +26 -0
  41. package/plugins/litclaude/skills/lit-docx/templates/tonalities/report.yaml +26 -0
  42. package/plugins/litclaude/skills/lit-pptx/SKILL.md +105 -48
  43. package/plugins/litclaude/skills/lit-pptx/references/agents/analyst.md +1 -1
  44. package/plugins/litclaude/skills/lit-pptx/references/agents/thesis.md +1 -1
  45. package/plugins/litclaude/skills/lit-pptx/references/anti-slop-checklist.md +59 -30
  46. package/plugins/litclaude/skills/lit-pptx/references/authoring-guide.md +119 -42
  47. package/plugins/litclaude/skills/lit-pptx/references/consensus-protocol.md +1 -1
  48. package/plugins/litclaude/skills/lit-pptx/references/data-display-cookbook.md +13 -6
  49. package/plugins/litclaude/skills/lit-pptx/references/density-and-fill.md +164 -0
  50. package/plugins/litclaude/skills/lit-pptx/references/design-system-azure.md +7 -4
  51. package/plugins/litclaude/skills/lit-pptx/references/direction-step.md +186 -0
  52. package/plugins/litclaude/skills/lit-pptx/references/examples/01-pitch-signal-ko.md +210 -0
  53. package/plugins/litclaude/skills/lit-pptx/references/examples/02-pitch-studio-ko.md +147 -0
  54. package/plugins/litclaude/skills/lit-pptx/references/examples/03-business-review-ledger-ko.md +199 -0
  55. package/plugins/litclaude/skills/lit-pptx/references/examples/04-business-review-night-en.md +203 -0
  56. package/plugins/litclaude/skills/lit-pptx/references/examples/05-research-talk-paper-en.md +179 -0
  57. package/plugins/litclaude/skills/lit-pptx/references/examples/06-lecture-chalk-ko.md +199 -0
  58. package/plugins/litclaude/skills/lit-pptx/references/examples/07-data-review-night-en.md +194 -0
  59. package/plugins/litclaude/skills/lit-pptx/references/examples/08-image-led-atlas-ko.md +175 -0
  60. package/plugins/litclaude/skills/lit-pptx/references/examples/09-briefing-gazette-ko.md +204 -0
  61. package/plugins/litclaude/skills/lit-pptx/references/examples/10-status-update-ledger-ko.md +179 -0
  62. package/plugins/litclaude/skills/lit-pptx/references/examples/11-portfolio-studio-en.md +164 -0
  63. package/plugins/litclaude/skills/lit-pptx/references/examples/12-research-tutorial-chalk-en.md +174 -0
  64. package/plugins/litclaude/skills/lit-pptx/references/interview-protocol.md +1 -1
  65. package/plugins/litclaude/skills/lit-pptx/references/layout-families.md +198 -0
  66. package/plugins/litclaude/skills/lit-pptx/references/stages/autopilot.md +1 -1
  67. package/plugins/litclaude/skills/lit-pptx/references/stages/interview.md +2 -2
  68. package/plugins/litclaude/skills/lit-pptx/references/stages/plan.md +1 -1
  69. package/plugins/litclaude/skills/lit-pptx/references/title-treatments.md +109 -0
  70. package/plugins/litclaude/skills/lit-pptx/references/tonalities/atlas.md +139 -0
  71. package/plugins/litclaude/skills/lit-pptx/references/tonalities/chalk.md +148 -0
  72. package/plugins/litclaude/skills/lit-pptx/references/tonalities/gazette.md +149 -0
  73. package/plugins/litclaude/skills/lit-pptx/references/tonalities/ledger.md +149 -0
  74. package/plugins/litclaude/skills/lit-pptx/references/tonalities/night.md +147 -0
  75. package/plugins/litclaude/skills/lit-pptx/references/tonalities/paper.md +151 -0
  76. package/plugins/litclaude/skills/lit-pptx/references/tonalities/signal.md +150 -0
  77. package/plugins/litclaude/skills/lit-pptx/references/tonalities/studio.md +132 -0
  78. package/plugins/litclaude/skills/lit-pptx/scripts/compile-deck.js +64 -16
  79. package/plugins/litclaude/skills/lit-pptx/scripts/craft_extras.py +664 -3
  80. package/plugins/litclaude/skills/lit-pptx/scripts/deck_craft.py +48 -6
  81. package/plugins/litclaude/skills/lit-pptx/scripts/lib/grid-resolver.js +521 -0
  82. package/plugins/litclaude/skills/lit-pptx/scripts/lib/layout-resolver.js +362 -11
  83. package/plugins/litclaude/skills/lit-pptx/scripts/lib/markdown-loader.js +6 -4
  84. package/plugins/litclaude/skills/lit-pptx/scripts/lib/render-adapter-pptx.js +211 -65
  85. package/plugins/litclaude/skills/lit-pptx/scripts/lib/render-pack.js +3559 -0
  86. package/plugins/litclaude/skills/lit-pptx/scripts/lib/slide-ast.js +10 -6
  87. package/plugins/litclaude/skills/lit-pptx/scripts/lib/template-registry.js +286 -1
  88. package/plugins/litclaude/skills/lit-pptx/scripts/qa_deck.py +39 -10
  89. package/plugins/litclaude/skills/lit-pptx/scripts/validate_pptx.py +22 -7
  90. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-A2Z/layout-mapping.yaml +1 -1
  91. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-A2Z/template.yaml +4 -1
  92. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-PRO/layout-mapping.yaml +1 -1
  93. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-PRO/template.yaml +4 -1
  94. package/plugins/litclaude/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +92 -0
  95. package/plugins/litclaude/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +97 -0
  96. package/plugins/litclaude/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +93 -0
  97. package/plugins/litclaude/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +94 -0
  98. package/plugins/litclaude/skills/lit-pptx/templates/tonalities/night/pack.yaml +93 -0
  99. package/plugins/litclaude/skills/lit-pptx/templates/tonalities/paper/pack.yaml +98 -0
  100. package/plugins/litclaude/skills/lit-pptx/templates/tonalities/signal/pack.yaml +94 -0
  101. package/plugins/litclaude/skills/lit-pptx/templates/tonalities/studio/pack.yaml +92 -0
@@ -14,13 +14,21 @@ The document counterpart of lit-pptx's qa_deck.py. It combines:
14
14
  4. Content — the document has body text, every Markdown heading reached it, no
15
15
  unfilled blank ([제품명], [금액], XXX, TBD, ○○, {{…}}) is left, and
16
16
  no YAML frontmatter leaked into the body
17
-
18
- Rendering is separate (visual_audit.py) because it needs soffice; a clean gate
19
- is defect-absence, not a verdict on how the pages read.
17
+ 5. Output — docx_layout.py: headings, the title and the subtitle are noun-phrase
18
+ labels (heading.declarative), heading levels in order (heading.order),
19
+ components counted; with --layout the document is rendered through
20
+ soffice and its pages checked: fill.page, heading.stranded, table.split,
21
+ figure.split, component.variety and cover.block (FAILs on a tonality document);
22
+ the restraint checks of spec Amendments 4 always (color.accent-kinds, heading.ink,
23
+ heading.ratio, table.fill, component.budget, furniture.chip on a tonality document;
24
+ date.iso on every path); --compare OTHER.docx adds tonality.structure
25
+
26
+ Without --layout nothing is rendered; visual_audit.py writes the page images to look at.
27
+ A clean gate is defect-absence, not a verdict on how the pages read.
20
28
 
21
29
  Usage:
30
+ python3 qa_docx.py out.docx --source draft.md --layout [--kind report] # tonality or plain
22
31
  python3 qa_docx.py out.docx --source draft.md --publisher korean-generic [--kind report]
23
- python3 qa_docx.py out.docx --source draft.md # plain profile
24
32
 
25
33
  Prints JSON; exit 0 on PASS, 1 on FAIL.
26
34
  """
@@ -34,11 +42,13 @@ _sys.dont_write_bytecode = True # the installed skill directory stays read-only
34
42
  _sys.path.insert(0, _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "..", "..", "..", "lib"))
35
43
  from office_runtime_bootstrap import ensure_runtime # noqa: E402
36
44
 
37
- ensure_runtime(["docx", "markdown", "bs4", "yaml"])
45
+ ensure_runtime(["docx", "markdown", "bs4", "yaml", "pymupdf"])
38
46
 
39
47
  import argparse # noqa: E402
40
48
  import json # noqa: E402
41
49
  import re # noqa: E402
50
+ import subprocess # noqa: E402
51
+ import tempfile # noqa: E402
42
52
  from pathlib import Path # noqa: E402
43
53
 
44
54
  from docx import Document # noqa: E402
@@ -46,7 +56,10 @@ from ooxml_integrity import check_package # noqa: E402
46
56
 
47
57
  SCRIPT_DIR = Path(__file__).resolve().parent
48
58
  _sys.path.insert(0, str(SCRIPT_DIR))
59
+ import docx_layout # noqa: E402
49
60
  import slop_lint # noqa: E402
61
+ from convert_md_to_docx import parse_frontmatter # noqa: E402
62
+ from render_pages import to_pdf # noqa: E402
50
63
 
51
64
  HEADING = re.compile(r"^(#{1,6})\s+(.+?)\s*#*\s*$")
52
65
  # A bracket with a digit in it is a citation marker ([1], [S3]); a blank has none.
@@ -79,9 +92,9 @@ def check_content(docx_path: Path, markdown_text: str | None) -> dict:
79
92
  leaked = [p for p in paragraphs[:8] if FRONTMATTER_LEAK.match(p)]
80
93
  missing = []
81
94
  if markdown_text:
82
- text = "\n".join(paragraphs)
95
+ text = docx_layout.norm("".join(paragraphs + cells)) # a heading may stand in a side-by-side columns row
83
96
  for heading in source_headings(markdown_text):
84
- if heading not in text:
97
+ if docx_layout.norm(heading) not in text:
85
98
  missing.append(heading)
86
99
  return {
87
100
  "pass": body_chars > 0 and not missing and not blanks and not leaked,
@@ -94,7 +107,7 @@ def check_content(docx_path: Path, markdown_text: str | None) -> dict:
94
107
  }
95
108
 
96
109
 
97
- def lint(source: Path | None, docx_path: Path, publisher_name: str, kind: str) -> dict:
110
+ def lint(source: Path | None, docx_path: Path, publisher_name: str, kind: str, explicit_publisher: bool = True) -> dict:
98
111
  registry = SCRIPT_DIR.parent / "templates" / "registry.yaml"
99
112
  phrase_rules = slop_lint.load_phrase_rules(SCRIPT_DIR.parent / "references" / "slop_phrase_list.yaml")
100
113
  publisher = slop_lint.load_publisher(registry, publisher_name)
@@ -105,6 +118,9 @@ def lint(source: Path | None, docx_path: Path, publisher_name: str, kind: str) -
105
118
  findings.extend(found)
106
119
  if kind == "report":
107
120
  findings = [f for f in findings if f.rule_id not in slop_lint.MANUSCRIPT_ONLY_RULES]
121
+ elif not explicit_publisher:
122
+ # A tonality or plain manuscript has no publisher outline to follow.
123
+ findings = [f for f in findings if f.rule_id != "rule-08-structure-order"]
108
124
  design = slop_lint.audit_docx_design(docx_path, publisher)
109
125
  if locale in {"ko", "mixed"} or source is None:
110
126
  design.extend(slop_lint.audit_docx_cjk(docx_path))
@@ -120,6 +136,39 @@ def lint(source: Path | None, docx_path: Path, publisher_name: str, kind: str) -
120
136
  }
121
137
 
122
138
 
139
+ def output_checks(docx_path: Path, source: Path | None, args) -> dict:
140
+ """docx_layout's checks: structural always, the page checks when --layout renders the document."""
141
+ front = parse_frontmatter(source.read_text(encoding="utf-8"))[0] if source else {}
142
+ tonality = bool(args.tonality or front.get("tonality"))
143
+ info = docx_layout.read_docx(docx_path)
144
+ found = docx_layout.structural(info, front, args.kind == "manuscript", tonality)
145
+ found += docx_layout.restraint(docx_path, tonality)
146
+ result = {"components": info["components"], "rendered": False}
147
+ if args.compare:
148
+ mine, other = docx_layout.structure(docx_path), docx_layout.structure(args.compare.expanduser().resolve())
149
+ differ = docx_layout.structure_diff(mine, other)
150
+ result["structure"] = {"this": mine, "other": other, "differ": differ}
151
+ if len(differ) < 3:
152
+ found.append(docx_layout.finding("tonality.structure", "FAIL", None,
153
+ f"the two documents differ in {len(differ)} structural feature(s): {', '.join(differ) or 'none'} (at least 3)",
154
+ "Two tonalities differ in structure, not colour: title block, summary form, numbering, component set, running head, contents."))
155
+ if args.layout:
156
+ with tempfile.TemporaryDirectory(prefix="lit-docx-layout-") as tmp:
157
+ try:
158
+ pdf, how = to_pdf(docx_path, Path(tmp))
159
+ except (RuntimeError, subprocess.TimeoutExpired) as error:
160
+ pdf, how = None, f"render failed: {error}"
161
+ if pdf is None:
162
+ result["render"] = how # the reply must say the page checks did not run
163
+ else:
164
+ page_found, pages = docx_layout.paged(info, docx_layout.read_pdf(pdf), tonality, front)
165
+ found += page_found
166
+ result.update(pages, rendered=True)
167
+ result["findings"] = [f for f in found if f["severity"] == "FAIL"]
168
+ result["advisories"] = [f for f in found if f["severity"] != "FAIL"]
169
+ return result
170
+
171
+
123
172
  def main(argv: list[str] | None = None) -> int:
124
173
  parser = argparse.ArgumentParser(description="Pass/fail gate for a generated DOCX.")
125
174
  parser.add_argument("docx", type=Path)
@@ -130,6 +179,9 @@ def main(argv: list[str] | None = None) -> int:
130
179
  help="Profile the DOCX was built with (korean-generic, elsevier, acs, ieee, nature). Omit for the plain profile.",
131
180
  )
132
181
  parser.add_argument("--kind", choices=("report", "manuscript"), default="report")
182
+ parser.add_argument("--layout", action="store_true", help="render through soffice and check the pages (fill, stranded headings, splits, component variety)")
183
+ parser.add_argument("--tonality", default=None, help="the tonality the DOCX was built with, when the source frontmatter does not name it")
184
+ parser.add_argument("--compare", type=Path, default=None, help="the same source built in another tonality: fail when the two differ in fewer than three structural features")
133
185
  args = parser.parse_args(argv)
134
186
 
135
187
  docx_path = args.docx.expanduser().resolve()
@@ -139,12 +191,14 @@ def main(argv: list[str] | None = None) -> int:
139
191
  # The plain profile has no registry entry; its prose is still linted with the
140
192
  # korean-generic phrase rules, and the design audit is limited to what every
141
193
  # profile shares.
142
- lint_report = lint(source, docx_path, args.publisher or "korean-generic", args.kind) if integrity["pass"] else {"pass": False}
194
+ lint_report = lint(source, docx_path, args.publisher or "korean-generic", args.kind, args.publisher is not None) if integrity["pass"] else {"pass": False}
143
195
  if args.publisher is None and lint_report.get("design"):
144
196
  shared = {"rule-45-highlighted-text", "rule-46-tracked-changes", "rule-55-ellipsis", "rule-60-numeric-column-alignment"}
145
197
  lint_report["design"] = [f for f in lint_report["design"] if f["rule"] in shared]
146
198
  lint_report["pass"] = not lint_report["prose"] and not lint_report["design"]
147
199
 
200
+ output = output_checks(docx_path, source, args) if integrity["pass"] else {"findings": [], "advisories": []}
201
+
148
202
  reasons = []
149
203
  if not integrity["pass"]:
150
204
  reasons.append("integrity: " + "; ".join(integrity["problems"][:5]))
@@ -158,6 +212,8 @@ def main(argv: list[str] | None = None) -> int:
158
212
  reasons.append(f"prose lint: {len(lint_report['prose'])} finding(s)")
159
213
  if lint_report.get("design"):
160
214
  reasons.append(f"design audit: {len(lint_report['design'])} finding(s)")
215
+ if output["findings"]:
216
+ reasons.append(f"output checks: {', '.join(sorted({f['check'] for f in output['findings']}))}")
161
217
 
162
218
  report = {
163
219
  "file": str(docx_path),
@@ -168,6 +224,7 @@ def main(argv: list[str] | None = None) -> int:
168
224
  "integrity": {"pass": integrity["pass"], "problems": integrity["problems"][:20]},
169
225
  "content": content,
170
226
  "lint": lint_report,
227
+ "output": output,
171
228
  }
172
229
  print(json.dumps(report, ensure_ascii=False, indent=2))
173
230
  return 0 if report["pass"] else 1
@@ -102,6 +102,9 @@ def word_count(text: str) -> int:
102
102
  return len(re.findall(r"\b[\w’'-]+\b", text))
103
103
 
104
104
 
105
+ TITLE_MINOR = {"a", "an", "the", "and", "but", "or", "nor", "for", "of", "in", "on", "at", "to", "by", "as", "via", "with", "from"}
106
+
107
+
105
108
  def heading_case_ok(heading: str, expected: str) -> bool:
106
109
  bare = heading.strip()
107
110
  if not bare:
@@ -111,8 +114,9 @@ def heading_case_ok(heading: str, expected: str) -> bool:
111
114
  if expected == "sentence_case":
112
115
  return bare[0].isupper() and bare[1:] != bare[1:].upper()
113
116
  if expected == "title_case":
114
- words = [w for w in re.findall(r"[A-Za-z][A-Za-z'-]*", bare)]
115
- return all(word[0].isupper() for word in words[: min(len(words), 6)]) if words else True
117
+ # Title case capitalises every word except a short article, conjunction or preposition after the first.
118
+ words = [w for w in re.findall(r"[A-Za-z][A-Za-z'-]*", bare)][:6]
119
+ return all(word[0].isupper() or (i and word in TITLE_MINOR) for i, word in enumerate(words)) if words else True
116
120
  return True
117
121
 
118
122
 
@@ -207,7 +211,11 @@ def lint_text(md_text: str, publisher: dict, phrase_rules: dict, locale: str) ->
207
211
  for match in re.finditer(re.escape(phrase), section_text):
208
212
  add_finding(findings, full_text, offsets, section_pos + match.start(), "rule-21-ko-ai-phrase", section_name, f"한국어 AI 표현: '{match.group(0)}'", match.group(0))
209
213
 
210
- if section_text.count("—") >= 2 or section_text.count(" - ") >= 1:
214
+ # A spaced hyphen between words reads as a dash; one opening a line is a list marker
215
+ # (a key figure's basis line, a nested point), not a dash.
216
+ # Table rows are data: a dash there marks an empty cell.
217
+ prose = "\n".join(line for line in section_text.split("\n") if not line.lstrip().startswith("|"))
218
+ if prose.count("—") >= 2 or re.search(r"\S - ", prose):
211
219
  add_finding(findings, full_text, offsets, max(section_pos, 0), "rule-02-em-dash-cluster", section_name, "Paragraph uses dash-heavy rhetorical style", section_text[:120])
212
220
 
213
221
  sentences = sentence_split(section_text)
@@ -252,7 +260,8 @@ def lint_text(md_text: str, publisher: dict, phrase_rules: dict, locale: str) ->
252
260
  for match in re.finditer(re.escape(phrase), section_text):
253
261
  add_finding(findings, full_text, offsets, section_pos + match.start(), "rule-24-ko-loanword", section_name, f"외래어 남용 감지: '{match.group(0)}'", match.group(0))
254
262
 
255
- if len(lengths) >= 4 and wc >= 80:
263
+ # A reference list is a run of entries, not prose.
264
+ if len(lengths) >= 4 and wc >= 80 and section_name.strip().lower() not in REFERENCE_SECTIONS:
256
265
  mean = sum(lengths) / len(lengths)
257
266
  variance = sum((item - mean) ** 2 for item in lengths) / len(lengths)
258
267
  if variance ** 0.5 < 6:
@@ -274,8 +283,10 @@ def lint_text(md_text: str, publisher: dict, phrase_rules: dict, locale: str) ->
274
283
  if total_words >= 250 and section_name.lower() == "methods" and section_ratio < 0.15:
275
284
  add_finding(findings, full_text, offsets, max(section_pos, 0), "rule-09-section-balance", section_name, "Methods section is too small relative to the manuscript", section_text[:120])
276
285
 
277
- ttr = type_token_ratio(section_text)
278
- if ttr < 0.45 and wc > 40:
286
+ # TTR falls with length, so it is read per subsection (the smallest headed unit), not over a whole chapter.
287
+ parts = [part for part in re.split(r"(?m)^(?=#{2,6} )", section_text) if word_count(part) > 40]
288
+ ttr = min((type_token_ratio(part) for part in parts), default=1.0)
289
+ if ttr < 0.45:
279
290
  add_finding(findings, full_text, offsets, max(section_pos, 0), "rule-10-lexical-diversity", section_name, f"Lexical diversity is low (TTR={ttr:.2f})", section_text[:120])
280
291
 
281
292
  for pattern in adjective_stacks:
@@ -485,6 +496,7 @@ def parse_args() -> argparse.Namespace:
485
496
 
486
497
  # Rules that describe a journal manuscript's shape. A business report, proposal or plan
487
498
  # has no Introduction/Methods outline to follow, so --kind report leaves them out.
499
+ REFERENCE_SECTIONS = {"references", "reference", "bibliography", "참고문헌", "참고 문헌"}
488
500
  MANUSCRIPT_ONLY_RULES = {"rule-07-citation-density", "rule-08-structure-order", "rule-09-section-balance"}
489
501
 
490
502
 
@@ -0,0 +1,28 @@
1
+ # Brief: a one-to-three page briefing for a decision maker. Conclusion first: the decision request stands right
2
+ # under a compact title block, the summary is a run of numbered points (①-④, a bold lead sentence then plain
3
+ # text), sections stay unnumbered and open under a full-width hairline, one box at most, and only the folio
4
+ # runs at the foot.
5
+ schema_version: 2
6
+ tonality: Brief
7
+ summary: One-to-three page briefings for a decision maker, conclusion first
8
+ dials: {density: 9, variance: 3}
9
+ docx:
10
+ page: {size: A4}
11
+ font: {latin: Pretendard, hangul: Pretendard, heading: Pretendard, body_pt_ko: 10.5, body_pt_en: 10.5}
12
+ design:
13
+ ramp: {h1: 1.3, h2: 1.15, title: 2.5}
14
+ numbering: none
15
+ h1_rule: above
16
+ title_block: masthead
17
+ contents: false
18
+ summary_form: points
19
+ conclusion_first: true
20
+ running_head: {footer: [folio], folio_align: right}
21
+ justify: ko
22
+ palette: {ink: "1A1A1A", ink_muted: "555555", line: "8C8C8C", accent: "2D5A47"}
23
+ accent_on: [title_rule, callout_rule]
24
+ page_geometry: {columns: 1, column_gap_cm: 0.6}
25
+ components:
26
+ allowed: [callout, columns]
27
+ callout_max: 1
28
+ figure_style: {max_height_ratio: 0.40}
@@ -0,0 +1,25 @@
1
+ # Journal: long-form reading in two columns (essays, newsletters, white papers, research summaries not bound
2
+ # for a named journal), close to a publisher profile: a title block with authors, abstract and keywords,
3
+ # decimal section numbers for cross-references, Latin body text in Times New Roman (Hangul and headings in
4
+ # Pretendard), ragged right, the short title and folio in the header.
5
+ schema_version: 2
6
+ tonality: Journal
7
+ summary: Long-form reading in two columns, for essays, newsletters, white papers and research summaries
8
+ dials: {density: 9, variance: 6}
9
+ docx:
10
+ page: {size: A4}
11
+ font: {latin: Times New Roman, hangul: Pretendard, heading: Pretendard, body_pt_ko: 10.5, body_pt_en: 10.5}
12
+ design:
13
+ ramp: {h1: 1.3, h2: 1.15, title: 2.5}
14
+ numbering: decimal
15
+ title_block: masthead
16
+ contents: false
17
+ summary_form: prose
18
+ running_head: {header: [title, folio]}
19
+ justify: none
20
+ palette: {ink: "1A1A1A", ink_muted: "555555", line: "8C8C8C"}
21
+ accent_on: []
22
+ page_geometry: {columns: 2, column_gap_cm: 0.6}
23
+ components:
24
+ allowed: [callout]
25
+ figure_style: {max_height_ratio: 0.40}
@@ -0,0 +1,26 @@
1
+ # Manual: a technical guide or procedure. The title over a document-control block (document, owner,
2
+ # organisation, issued), a short contents list, decimal sections 1 / 1.1, numbered steps, one warning style
3
+ # for every callout, an optional sidebar beside the text, the short title in the header and the folio at the
4
+ # foot.
5
+ schema_version: 2
6
+ tonality: Manual
7
+ summary: How-to guides, procedures and operating manuals with steps, warnings and reference tables
8
+ dials: {density: 9, variance: 6}
9
+ docx:
10
+ page: {size: A4}
11
+ font: {latin: Pretendard, hangul: Pretendard, heading: Pretendard, body_pt_ko: 10.5, body_pt_en: 10.5}
12
+ design:
13
+ ramp: {h1: 1.35, h2: 1.15, title: 2.5}
14
+ numbering: decimal
15
+ title_block: manual
16
+ contents: true
17
+ summary_form: prose
18
+ running_head: {header: [title], footer: [folio], folio_align: right}
19
+ justify: none
20
+ palette: {ink: "1A1A1A", ink_muted: "555555", line: "8C8C8C", accent: "2E4A66"}
21
+ accent_on: [callout_rule, sidebar_rule]
22
+ page_geometry: {columns: 1, column_gap_cm: 0.6}
23
+ components:
24
+ allowed: [callout, sidebar, columns]
25
+ sidebar: {default_width: third, float: right}
26
+ figure_style: {max_height_ratio: 0.45}
@@ -0,0 +1,26 @@
1
+ # Memo: a notice, internal memo, short decision request or letter of one to two pages. No cover: a label, the
2
+ # subject as title and To / From / Date / Subject rows between two hairlines; no components (a callout reads as
3
+ # a bold lead line); unnumbered sections; the folio from page 2.
4
+ schema_version: 2
5
+ tonality: Memo
6
+ summary: Notices, internal memos, short decision requests and letters of one to two pages
7
+ dials: {density: 9, variance: 2}
8
+ docx:
9
+ page: {size: A4}
10
+ font: {latin: Pretendard, hangul: Pretendard, heading: Pretendard, body_pt_ko: 10.5, body_pt_en: 11}
11
+ design:
12
+ ramp: {h1: 1.2, h2: 1.1, title: 1.8}
13
+ numbering: none
14
+ title_block: memo
15
+ contents: false
16
+ summary_form: prose
17
+ running_head: {footer: [folio], folio_align: right}
18
+ justify: ko
19
+ palette: {ink: "1A1A1A", ink_muted: "555555", line: "8C8C8C"}
20
+ accent_on: []
21
+ page_geometry: {columns: 1, column_gap_cm: 0.6}
22
+ components:
23
+ allowed: []
24
+ figure_style: {max_height_ratio: 0.40}
25
+ # A memo stays on one page when its content allows: the low end of the line pitch, tighter heading space.
26
+ spacing: tight
@@ -0,0 +1,26 @@
1
+ # Proposal: a proposal or plan as a document. A typographic cover page (the title left-aligned in the upper
2
+ # third, one short rule, the byline and date at its foot; no colour block, no figures), unnumbered sections,
3
+ # one decision box, key figures once in the summary, a budget table, the folio centred at the foot.
4
+ schema_version: 2
5
+ tonality: Proposal
6
+ summary: Proposals, plans and pitches as documents, with a cover, a decision box, budget and schedule
7
+ dials: {density: 8, variance: 6}
8
+ docx:
9
+ page: {size: A4}
10
+ font: {latin: Pretendard, hangul: Pretendard, heading: Pretendard, body_pt_ko: 10.5, body_pt_en: 11}
11
+ design:
12
+ ramp: {h1: 1.4, h2: 1.2, title: 2.7}
13
+ numbering: none
14
+ title_block: cover
15
+ contents: false
16
+ summary_form: prose
17
+ running_head: {footer: [folio], folio_align: center}
18
+ justify: ko
19
+ palette: {ink: "1A1A1A", ink_muted: "555555", line: "8C8C8C", accent: "6E2639"}
20
+ accent_on: [title_rule, callout_rule]
21
+ page_geometry: {columns: 1, column_gap_cm: 0.6}
22
+ components:
23
+ allowed: [keyfigures, callout, columns]
24
+ callout_max: 1
25
+ keyfigures: {per_row: 3}
26
+ figure_style: {max_height_ratio: 0.60}
@@ -0,0 +1,26 @@
1
+ # Report: periodic results, project reports and analyses read by managers; 4-30 pages, tables carry the evidence.
2
+ # Korean default is institute prose (the manner of a central-bank or think-tank issue note): a typographic title
3
+ # block on page 1, a short contents list from five pages, sections numbered Ⅰ. / 1. / 가. (English 1 / 1.1),
4
+ # booktabs tables with <표 n> captions, units and 자료 lines, a footer with the short title and the folio.
5
+ schema_version: 2
6
+ tonality: Report
7
+ summary: Periodic results, project reports and analyses with tables and figures
8
+ dials: {density: 9, variance: 5}
9
+ docx:
10
+ page: {size: A4}
11
+ font: {latin: Pretendard, hangul: Pretendard, heading: Pretendard, body_pt_ko: 10.5, body_pt_en: 10.5}
12
+ design:
13
+ ramp: {h1: 1.4, h2: 1.2, title: 2.5}
14
+ numbering: {ko: roman-ko, en: decimal}
15
+ title_block: masthead
16
+ contents: true
17
+ summary_form: prose
18
+ running_head: {footer: [title, folio]}
19
+ justify: ko
20
+ palette: {ink: "1A1A1A", ink_muted: "555555", line: "8C8C8C", accent: "1F3A5F"}
21
+ accent_on: [title_rule, callout_rule]
22
+ page_geometry: {columns: 1, column_gap_cm: 0.6}
23
+ components:
24
+ allowed: [keyfigures, callout, columns]
25
+ keyfigures: {per_row: 3}
26
+ figure_style: {max_height_ratio: 0.45}