@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.
- package/CHANGELOG.md +12 -0
- package/README.md +14 -8
- package/README_ko-KR.md +13 -8
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/hooks.md +3 -2
- package/package.json +1 -1
- package/plugins/litclaude/.claude-plugin/plugin.json +1 -1
- package/plugins/litclaude/bin/litclaude-hook.js +4 -4
- package/plugins/litclaude/bin/litclaude-mcp.js +1 -1
- package/plugins/litclaude/lib/canonical-skill-resources.mjs +91 -31
- package/plugins/litclaude/skills/frontend-ui-ux/ATTRIBUTION.md +3 -6
- package/plugins/litclaude/skills/lit-docx/SKILL.md +50 -19
- package/plugins/litclaude/skills/lit-docx/references/components.md +207 -0
- package/plugins/litclaude/skills/lit-docx/references/direction-step.md +277 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/01-report-ko.md +166 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/02-brief-ko.md +105 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/03-manual-ko.md +164 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/04-proposal-ko.md +138 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/05-memo-en.md +55 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/06-journal-en.md +130 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/07-report-en.md +123 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/08-proposal-en.md +123 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/09-memo-ko.md +45 -0
- package/plugins/litclaude/skills/lit-docx/references/page-composition.md +221 -0
- package/plugins/litclaude/skills/lit-docx/references/tonalities/brief.md +143 -0
- package/plugins/litclaude/skills/lit-docx/references/tonalities/journal.md +142 -0
- package/plugins/litclaude/skills/lit-docx/references/tonalities/manual.md +148 -0
- package/plugins/litclaude/skills/lit-docx/references/tonalities/memo.md +137 -0
- package/plugins/litclaude/skills/lit-docx/references/tonalities/proposal.md +140 -0
- package/plugins/litclaude/skills/lit-docx/references/tonalities/report.md +156 -0
- package/plugins/litclaude/skills/lit-docx/scripts/convert_md_to_docx.py +274 -73
- package/plugins/litclaude/skills/lit-docx/scripts/docx_design.py +2449 -0
- package/plugins/litclaude/skills/lit-docx/scripts/docx_layout.py +959 -0
- package/plugins/litclaude/skills/lit-docx/scripts/qa_docx.py +66 -9
- package/plugins/litclaude/skills/lit-docx/scripts/slop_lint.py +18 -6
- package/plugins/litclaude/skills/lit-docx/templates/tonalities/brief.yaml +28 -0
- package/plugins/litclaude/skills/lit-docx/templates/tonalities/journal.yaml +25 -0
- package/plugins/litclaude/skills/lit-docx/templates/tonalities/manual.yaml +26 -0
- package/plugins/litclaude/skills/lit-docx/templates/tonalities/memo.yaml +26 -0
- package/plugins/litclaude/skills/lit-docx/templates/tonalities/proposal.yaml +26 -0
- package/plugins/litclaude/skills/lit-docx/templates/tonalities/report.yaml +26 -0
- package/plugins/litclaude/skills/lit-pptx/SKILL.md +105 -48
- package/plugins/litclaude/skills/lit-pptx/references/agents/analyst.md +1 -1
- package/plugins/litclaude/skills/lit-pptx/references/agents/thesis.md +1 -1
- package/plugins/litclaude/skills/lit-pptx/references/anti-slop-checklist.md +59 -30
- package/plugins/litclaude/skills/lit-pptx/references/authoring-guide.md +119 -42
- package/plugins/litclaude/skills/lit-pptx/references/consensus-protocol.md +1 -1
- package/plugins/litclaude/skills/lit-pptx/references/data-display-cookbook.md +13 -6
- package/plugins/litclaude/skills/lit-pptx/references/density-and-fill.md +164 -0
- package/plugins/litclaude/skills/lit-pptx/references/design-system-azure.md +7 -4
- package/plugins/litclaude/skills/lit-pptx/references/direction-step.md +186 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/01-pitch-signal-ko.md +210 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/02-pitch-studio-ko.md +147 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/03-business-review-ledger-ko.md +199 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/04-business-review-night-en.md +203 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/05-research-talk-paper-en.md +179 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/06-lecture-chalk-ko.md +199 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/07-data-review-night-en.md +194 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/08-image-led-atlas-ko.md +175 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/09-briefing-gazette-ko.md +204 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/10-status-update-ledger-ko.md +179 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/11-portfolio-studio-en.md +164 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/12-research-tutorial-chalk-en.md +174 -0
- package/plugins/litclaude/skills/lit-pptx/references/interview-protocol.md +1 -1
- package/plugins/litclaude/skills/lit-pptx/references/layout-families.md +198 -0
- package/plugins/litclaude/skills/lit-pptx/references/stages/autopilot.md +1 -1
- package/plugins/litclaude/skills/lit-pptx/references/stages/interview.md +2 -2
- package/plugins/litclaude/skills/lit-pptx/references/stages/plan.md +1 -1
- package/plugins/litclaude/skills/lit-pptx/references/title-treatments.md +109 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/atlas.md +139 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/chalk.md +148 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/gazette.md +149 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/ledger.md +149 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/night.md +147 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/paper.md +151 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/signal.md +150 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/studio.md +132 -0
- package/plugins/litclaude/skills/lit-pptx/scripts/compile-deck.js +64 -16
- package/plugins/litclaude/skills/lit-pptx/scripts/craft_extras.py +664 -3
- package/plugins/litclaude/skills/lit-pptx/scripts/deck_craft.py +48 -6
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/grid-resolver.js +521 -0
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/layout-resolver.js +362 -11
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/markdown-loader.js +6 -4
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/render-adapter-pptx.js +211 -65
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/render-pack.js +3559 -0
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/slide-ast.js +10 -6
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/template-registry.js +286 -1
- package/plugins/litclaude/skills/lit-pptx/scripts/qa_deck.py +39 -10
- package/plugins/litclaude/skills/lit-pptx/scripts/validate_pptx.py +22 -7
- package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-A2Z/layout-mapping.yaml +1 -1
- package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-A2Z/template.yaml +4 -1
- package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-PRO/layout-mapping.yaml +1 -1
- package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-PRO/template.yaml +4 -1
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +92 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +97 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +93 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +94 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/night/pack.yaml +93 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/paper/pack.yaml +98 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/signal/pack.yaml +94 -0
- 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
|
-
|
|
19
|
-
|
|
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 = "
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
278
|
-
|
|
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}
|