limatus 0.2.0__tar.gz
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.
- limatus-0.2.0/LICENSE +21 -0
- limatus-0.2.0/PKG-INFO +86 -0
- limatus-0.2.0/README.md +70 -0
- limatus-0.2.0/pyproject.toml +48 -0
- limatus-0.2.0/setup.cfg +4 -0
- limatus-0.2.0/src/limatus/__init__.py +3 -0
- limatus-0.2.0/src/limatus/_util.py +21 -0
- limatus-0.2.0/src/limatus/cli.py +43 -0
- limatus-0.2.0/src/limatus/editorial_commands.py +111 -0
- limatus-0.2.0/src/limatus/editorial_corpus.py +51 -0
- limatus-0.2.0/src/limatus/editorial_density.py +242 -0
- limatus-0.2.0/src/limatus/editorial_diagnosis.py +680 -0
- limatus-0.2.0/src/limatus/editorial_diagnosis_schema.py +168 -0
- limatus-0.2.0/src/limatus/editorial_llm.py +73 -0
- limatus-0.2.0/src/limatus/editorial_markup.py +145 -0
- limatus-0.2.0/src/limatus/editorial_options_schema.py +179 -0
- limatus-0.2.0/src/limatus/editorial_rewrite_options.py +384 -0
- limatus-0.2.0/src/limatus/editorial_signals.py +100 -0
- limatus-0.2.0/src/limatus/editorial_style.py +360 -0
- limatus-0.2.0/src/limatus/editorial_text.py +47 -0
- limatus-0.2.0/src/limatus.egg-info/PKG-INFO +86 -0
- limatus-0.2.0/src/limatus.egg-info/SOURCES.txt +24 -0
- limatus-0.2.0/src/limatus.egg-info/dependency_links.txt +1 -0
- limatus-0.2.0/src/limatus.egg-info/entry_points.txt +2 -0
- limatus-0.2.0/src/limatus.egg-info/requires.txt +4 -0
- limatus-0.2.0/src/limatus.egg-info/top_level.txt +1 -0
limatus-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anthus AI Solutions
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
limatus-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: limatus
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A diagnose-and-steer quality loop for AI-generated content.
|
|
5
|
+
Author: Anthus AI Solutions
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/AnthusAI/Limatus
|
|
8
|
+
Project-URL: Repository, https://github.com/AnthusAI/Limatus
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: PyYAML<7.0.0,>=6.0.2
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: behave>=1.2.6; extra == "dev"
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# Limatus
|
|
18
|
+
|
|
19
|
+
Limatus is a diagnose-and-steer quality loop for AI-generated content — prose today, web pages soon — built to catch generic AI slop and enforce a specific publication or product's own style rules, not just generic patterns.
|
|
20
|
+
|
|
21
|
+
## Press Release
|
|
22
|
+
|
|
23
|
+
**Anthus AI Solutions introduces Limatus, an open-source tool that tells writers exactly where AI-generated prose sounds generic — without rewriting it for them**
|
|
24
|
+
|
|
25
|
+
Teams publishing AI-assisted content face a recurring problem: drafts that are technically correct but sound like every other AI-generated draft — hedge words, empty lead-ins, uniform sentence cadence, unsupported certainty, and a voice that matches no one's actual house style. Fixing this by hand doesn't scale, and letting a model "just rewrite it" trades one set of generic patterns for another.
|
|
26
|
+
|
|
27
|
+
Limatus is a command-line tool that closes this gap in two separate steps. `limatus diagnose` reads a draft against a publication's own style profile — its voice, its lexicon, its banned phrases and patterns — and returns a structured list of findings: vague claims, empty lead-ins, uniform cadence, list-shaped prose, unsupported certainty, redundancy, and voice mismatches. It never rewrites anything; findings are signal, not verdicts. A human or agent then marks each finding skip, rewrite, delete, or keep, and only for the findings marked "rewrite" does `limatus options` generate a small set of constrained rewrite candidates — never a single mandated replacement, and never a fabricated fact, statistic, or anecdote.
|
|
28
|
+
|
|
29
|
+
"We built this because we were publishing AI-assisted content ourselves and got tired of the same tells showing up in every draft," said the Limatus team at Anthus AI Solutions. "The insight wasn't that AI writing is bad — it's that treating every publication's voice as the same generic 'professional tone' is what makes it read as AI-generated. Limatus makes house style an explicit, versioned, machine-checkable artifact instead of something an editor has to remember and enforce by hand every time."
|
|
30
|
+
|
|
31
|
+
Limatus is extracted from the editorial engine Anthus AI Solutions built and ran across dozens of real articles on its own Anth.us and Pilobolus publications, and is available today as an MIT-licensed, publication-agnostic Python package at [github.com/AnthusAI/Limatus](https://github.com/AnthusAI/Limatus).
|
|
32
|
+
|
|
33
|
+
## FAQ
|
|
34
|
+
|
|
35
|
+
**Is Limatus an AI-content detector?**
|
|
36
|
+
No. Limatus doesn't try to guess whether text was written by a model — it checks text against a style profile you define, regardless of who or what wrote it. It deliberately refuses to accept "detector scores" as part of a style profile; the goal is to enforce your voice, not to pass someone else's AI-detection heuristic.
|
|
37
|
+
|
|
38
|
+
**Does Limatus rewrite my content for me?**
|
|
39
|
+
Not automatically, and not without a human or agent explicitly opting each finding in. `limatus diagnose` only reports findings — it never touches the draft file. `limatus options` only generates rewrite *candidates* for findings you've separately marked "rewrite," and always offers more than one option so the choice of which to accept stays with an editor.
|
|
40
|
+
|
|
41
|
+
**What does a "style profile" look like?**
|
|
42
|
+
A YAML file naming a publication's voice, audience, tone, sentence style, structure, a preferred/avoided lexicon, evidence rules, and five to ten reference samples of real approved prose. It's the same profile used to catch drift, so it's meant to be version-controlled alongside the content it governs.
|
|
43
|
+
|
|
44
|
+
**Why is this a separate package from Papyrus?**
|
|
45
|
+
Papyrus is Anthus AI Solutions' own content and newsroom system, tightly coupled to Anth.us's publishing pipeline. Limatus is the general-purpose diagnose engine underneath it, extracted so any publication or product — not just Anthus's own — can adopt it without adopting the rest of Papyrus.
|
|
46
|
+
|
|
47
|
+
**What's next?**
|
|
48
|
+
A second capability is planned: a usability and accessibility diagnose loop for AI-generated web pages, built on Playwright and axe-core, checking WCAG 2.2 A/AA compliance across themes, viewports, and interactive states — the same diagnose-first, never-auto-fix philosophy applied to markup instead of prose.
|
|
49
|
+
|
|
50
|
+
**Is it on PyPI yet?**
|
|
51
|
+
Not yet — see Status below.
|
|
52
|
+
|
|
53
|
+
## Status
|
|
54
|
+
|
|
55
|
+
This package was newly extracted from Anth.us's Papyrus content system and is under active development. It is not yet on PyPI.
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install -e ".[dev]"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
(PyPI installation via `pip install limatus` will be available once the package is published — see Status above.)
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
limatus diagnose --draft path/to/draft.md --profile path/to/style-profile.yml
|
|
69
|
+
limatus options --draft path/to/draft.md --profile path/to/style-profile.yml \
|
|
70
|
+
--diagnosis path/to/diagnosis.json --decisions path/to/decisions.json \
|
|
71
|
+
--skill path/to/rewrite-skill.yml
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Run `limatus --help` for the full command reference.
|
|
75
|
+
|
|
76
|
+
## Testing
|
|
77
|
+
|
|
78
|
+
Limatus's test suite is written in Gherkin and run with [Behave](https://behave.readthedocs.io/):
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
behave
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT — see [LICENSE](LICENSE).
|
limatus-0.2.0/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Limatus
|
|
2
|
+
|
|
3
|
+
Limatus is a diagnose-and-steer quality loop for AI-generated content — prose today, web pages soon — built to catch generic AI slop and enforce a specific publication or product's own style rules, not just generic patterns.
|
|
4
|
+
|
|
5
|
+
## Press Release
|
|
6
|
+
|
|
7
|
+
**Anthus AI Solutions introduces Limatus, an open-source tool that tells writers exactly where AI-generated prose sounds generic — without rewriting it for them**
|
|
8
|
+
|
|
9
|
+
Teams publishing AI-assisted content face a recurring problem: drafts that are technically correct but sound like every other AI-generated draft — hedge words, empty lead-ins, uniform sentence cadence, unsupported certainty, and a voice that matches no one's actual house style. Fixing this by hand doesn't scale, and letting a model "just rewrite it" trades one set of generic patterns for another.
|
|
10
|
+
|
|
11
|
+
Limatus is a command-line tool that closes this gap in two separate steps. `limatus diagnose` reads a draft against a publication's own style profile — its voice, its lexicon, its banned phrases and patterns — and returns a structured list of findings: vague claims, empty lead-ins, uniform cadence, list-shaped prose, unsupported certainty, redundancy, and voice mismatches. It never rewrites anything; findings are signal, not verdicts. A human or agent then marks each finding skip, rewrite, delete, or keep, and only for the findings marked "rewrite" does `limatus options` generate a small set of constrained rewrite candidates — never a single mandated replacement, and never a fabricated fact, statistic, or anecdote.
|
|
12
|
+
|
|
13
|
+
"We built this because we were publishing AI-assisted content ourselves and got tired of the same tells showing up in every draft," said the Limatus team at Anthus AI Solutions. "The insight wasn't that AI writing is bad — it's that treating every publication's voice as the same generic 'professional tone' is what makes it read as AI-generated. Limatus makes house style an explicit, versioned, machine-checkable artifact instead of something an editor has to remember and enforce by hand every time."
|
|
14
|
+
|
|
15
|
+
Limatus is extracted from the editorial engine Anthus AI Solutions built and ran across dozens of real articles on its own Anth.us and Pilobolus publications, and is available today as an MIT-licensed, publication-agnostic Python package at [github.com/AnthusAI/Limatus](https://github.com/AnthusAI/Limatus).
|
|
16
|
+
|
|
17
|
+
## FAQ
|
|
18
|
+
|
|
19
|
+
**Is Limatus an AI-content detector?**
|
|
20
|
+
No. Limatus doesn't try to guess whether text was written by a model — it checks text against a style profile you define, regardless of who or what wrote it. It deliberately refuses to accept "detector scores" as part of a style profile; the goal is to enforce your voice, not to pass someone else's AI-detection heuristic.
|
|
21
|
+
|
|
22
|
+
**Does Limatus rewrite my content for me?**
|
|
23
|
+
Not automatically, and not without a human or agent explicitly opting each finding in. `limatus diagnose` only reports findings — it never touches the draft file. `limatus options` only generates rewrite *candidates* for findings you've separately marked "rewrite," and always offers more than one option so the choice of which to accept stays with an editor.
|
|
24
|
+
|
|
25
|
+
**What does a "style profile" look like?**
|
|
26
|
+
A YAML file naming a publication's voice, audience, tone, sentence style, structure, a preferred/avoided lexicon, evidence rules, and five to ten reference samples of real approved prose. It's the same profile used to catch drift, so it's meant to be version-controlled alongside the content it governs.
|
|
27
|
+
|
|
28
|
+
**Why is this a separate package from Papyrus?**
|
|
29
|
+
Papyrus is Anthus AI Solutions' own content and newsroom system, tightly coupled to Anth.us's publishing pipeline. Limatus is the general-purpose diagnose engine underneath it, extracted so any publication or product — not just Anthus's own — can adopt it without adopting the rest of Papyrus.
|
|
30
|
+
|
|
31
|
+
**What's next?**
|
|
32
|
+
A second capability is planned: a usability and accessibility diagnose loop for AI-generated web pages, built on Playwright and axe-core, checking WCAG 2.2 A/AA compliance across themes, viewports, and interactive states — the same diagnose-first, never-auto-fix philosophy applied to markup instead of prose.
|
|
33
|
+
|
|
34
|
+
**Is it on PyPI yet?**
|
|
35
|
+
Not yet — see Status below.
|
|
36
|
+
|
|
37
|
+
## Status
|
|
38
|
+
|
|
39
|
+
This package was newly extracted from Anth.us's Papyrus content system and is under active development. It is not yet on PyPI.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install -e ".[dev]"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
(PyPI installation via `pip install limatus` will be available once the package is published — see Status above.)
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
limatus diagnose --draft path/to/draft.md --profile path/to/style-profile.yml
|
|
53
|
+
limatus options --draft path/to/draft.md --profile path/to/style-profile.yml \
|
|
54
|
+
--diagnosis path/to/diagnosis.json --decisions path/to/decisions.json \
|
|
55
|
+
--skill path/to/rewrite-skill.yml
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Run `limatus --help` for the full command reference.
|
|
59
|
+
|
|
60
|
+
## Testing
|
|
61
|
+
|
|
62
|
+
Limatus's test suite is written in Gherkin and run with [Behave](https://behave.readthedocs.io/):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
behave
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "limatus"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "A diagnose-and-steer quality loop for AI-generated content."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Anthus AI Solutions" }
|
|
10
|
+
]
|
|
11
|
+
dependencies = [
|
|
12
|
+
"PyYAML>=6.0.2,<7.0.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
dev = [
|
|
17
|
+
"behave>=1.2.6",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/AnthusAI/Limatus"
|
|
22
|
+
Repository = "https://github.com/AnthusAI/Limatus"
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
limatus = "limatus.cli:main"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["setuptools>=68", "wheel"]
|
|
29
|
+
build-backend = "setuptools.build_meta"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|
|
33
|
+
|
|
34
|
+
[tool.semantic_release]
|
|
35
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
36
|
+
version_variables = ["src/limatus/__init__.py:__version__"]
|
|
37
|
+
build_command = "python -m pip install build && python -m build"
|
|
38
|
+
commit_parser = "conventional"
|
|
39
|
+
major_on_zero = false
|
|
40
|
+
|
|
41
|
+
[tool.semantic_release.branches.main]
|
|
42
|
+
match = "main"
|
|
43
|
+
|
|
44
|
+
[tool.semantic_release.changelog]
|
|
45
|
+
exclude_commit_patterns = []
|
|
46
|
+
|
|
47
|
+
[tool.semantic_release.remote]
|
|
48
|
+
type = "github"
|
limatus-0.2.0/setup.cfg
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def hash_stable(value: Any) -> str:
|
|
9
|
+
if isinstance(value, str):
|
|
10
|
+
payload = value
|
|
11
|
+
else:
|
|
12
|
+
payload = json.dumps(value, sort_keys=True, separators=(",", ":"), default=str)
|
|
13
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def hash_short(value: Any) -> str:
|
|
17
|
+
return hash_stable(value)[:16]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Default model for constrained rewrite-option generation.
|
|
21
|
+
DEFAULT_EDITORIAL_REWRITE_MODEL = "gpt-5.4-mini"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Limatus CLI entry point.
|
|
2
|
+
|
|
3
|
+
limatus diagnose --draft <file> --profile <style-profile.yml> [...]
|
|
4
|
+
limatus options --draft <file> --profile <style-profile.yml> \\
|
|
5
|
+
--diagnosis <diagnosis.json> --decisions <decisions.json> \\
|
|
6
|
+
--skill <editorial-rewrite-skill.yml> [...]
|
|
7
|
+
|
|
8
|
+
See editorial_commands.py for each subcommand's full flag set.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
from .editorial_commands import editorial_diagnose, editorial_options
|
|
16
|
+
|
|
17
|
+
COMMANDS = {
|
|
18
|
+
"diagnose": editorial_diagnose,
|
|
19
|
+
"options": editorial_options,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main(argv: list[str] | None = None) -> int:
|
|
24
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
25
|
+
if not args or args[0] in {"-h", "--help"}:
|
|
26
|
+
print(__doc__)
|
|
27
|
+
return 0
|
|
28
|
+
if args[0] in {"-V", "--version"}:
|
|
29
|
+
print(f"limatus {__version__}")
|
|
30
|
+
return 0
|
|
31
|
+
|
|
32
|
+
command, flags = args[0], args[1:]
|
|
33
|
+
handler = COMMANDS.get(command)
|
|
34
|
+
if handler is None:
|
|
35
|
+
print(f"limatus: unknown command '{command}'. Try one of: {', '.join(sorted(COMMANDS))}")
|
|
36
|
+
return 1
|
|
37
|
+
|
|
38
|
+
handler(flags)
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
sys.exit(main())
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .editorial_diagnosis import diagnose_draft
|
|
9
|
+
from .editorial_diagnosis_schema import validate_diagnosis
|
|
10
|
+
from .editorial_markup import render_annotated_markus, render_annotated_xml
|
|
11
|
+
from .editorial_options_schema import validate_decisions
|
|
12
|
+
from .editorial_rewrite_options import generate_rewrite_options
|
|
13
|
+
from .editorial_style import load_style_profile
|
|
14
|
+
from ._util import DEFAULT_EDITORIAL_REWRITE_MODEL
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def editorial_diagnose(flags: list[str]) -> None:
|
|
18
|
+
parser = argparse.ArgumentParser(prog="limatus diagnose")
|
|
19
|
+
input_group = parser.add_mutually_exclusive_group(required=True)
|
|
20
|
+
input_group.add_argument("--draft", help="Path to the draft file (read-only).")
|
|
21
|
+
input_group.add_argument("--text", help="Draft text to diagnose without reading a file.")
|
|
22
|
+
parser.add_argument("--profile", required=True, help="Path to the style profile YAML.")
|
|
23
|
+
parser.add_argument("--output", default="", help="Optional path to write diagnostic JSON.")
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"--markup-out",
|
|
26
|
+
default="",
|
|
27
|
+
help="Optional path to write Markus-annotated Markdown (editorial-finding directives).",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--xml-out",
|
|
31
|
+
default="",
|
|
32
|
+
help="Optional path to write editorial annotation XML.",
|
|
33
|
+
)
|
|
34
|
+
args = parser.parse_args(flags)
|
|
35
|
+
|
|
36
|
+
profile_path = Path(args.profile).resolve()
|
|
37
|
+
if args.draft:
|
|
38
|
+
draft_path = Path(args.draft).resolve()
|
|
39
|
+
if not draft_path.is_file():
|
|
40
|
+
raise ValueError(f"Draft file not found: {draft_path}")
|
|
41
|
+
draft_text = draft_path.read_text(encoding="utf-8")
|
|
42
|
+
else:
|
|
43
|
+
draft_text = args.text
|
|
44
|
+
|
|
45
|
+
style_profile = load_style_profile(profile_path)
|
|
46
|
+
diagnosis = diagnose_draft(draft_text, style_profile=style_profile)
|
|
47
|
+
rendered = json.dumps(diagnosis, indent=2) + "\n"
|
|
48
|
+
|
|
49
|
+
if args.markup_out:
|
|
50
|
+
markup_path = Path(args.markup_out).resolve()
|
|
51
|
+
markup_path.write_text(render_annotated_markus(draft_text, diagnosis), encoding="utf-8")
|
|
52
|
+
|
|
53
|
+
if args.xml_out:
|
|
54
|
+
xml_path = Path(args.xml_out).resolve()
|
|
55
|
+
xml_path.write_text(render_annotated_xml(draft_text, diagnosis), encoding="utf-8")
|
|
56
|
+
|
|
57
|
+
if args.output:
|
|
58
|
+
output_path = Path(args.output).resolve()
|
|
59
|
+
output_path.write_text(rendered, encoding="utf-8")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
sys.stdout.write(rendered)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def editorial_options(flags: list[str]) -> None:
|
|
66
|
+
parser = argparse.ArgumentParser(prog="limatus options")
|
|
67
|
+
parser.add_argument("--draft", required=True, help="Path to the draft file (read-only).")
|
|
68
|
+
parser.add_argument("--profile", required=True, help="Path to the style profile YAML.")
|
|
69
|
+
parser.add_argument("--diagnosis", required=True, help="Path to validated diagnostic JSON.")
|
|
70
|
+
parser.add_argument("--decisions", required=True, help="Path to steering decisions JSON.")
|
|
71
|
+
parser.add_argument("--output", default="", help="Optional path to write options JSON.")
|
|
72
|
+
parser.add_argument("--model", default=DEFAULT_EDITORIAL_REWRITE_MODEL, help="OpenAI model id.")
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--skill",
|
|
75
|
+
required=True,
|
|
76
|
+
help="Path to editorial rewrite skill YAML.",
|
|
77
|
+
)
|
|
78
|
+
args = parser.parse_args(flags)
|
|
79
|
+
|
|
80
|
+
draft_path = Path(args.draft).resolve()
|
|
81
|
+
if not draft_path.is_file():
|
|
82
|
+
raise ValueError(f"Draft file not found: {draft_path}")
|
|
83
|
+
draft_text = draft_path.read_text(encoding="utf-8")
|
|
84
|
+
|
|
85
|
+
diagnosis_path = Path(args.diagnosis).resolve()
|
|
86
|
+
decisions_path = Path(args.decisions).resolve()
|
|
87
|
+
diagnosis_payload = json.loads(diagnosis_path.read_text(encoding="utf-8"))
|
|
88
|
+
decisions_payload = json.loads(decisions_path.read_text(encoding="utf-8"))
|
|
89
|
+
if not isinstance(decisions_payload, list):
|
|
90
|
+
raise ValueError("Decisions JSON must be a list.")
|
|
91
|
+
|
|
92
|
+
style_profile = load_style_profile(Path(args.profile).resolve())
|
|
93
|
+
validate_diagnosis(diagnosis_payload)
|
|
94
|
+
validate_decisions(decisions_payload)
|
|
95
|
+
|
|
96
|
+
options = generate_rewrite_options(
|
|
97
|
+
draft_text,
|
|
98
|
+
style_profile=style_profile,
|
|
99
|
+
diagnosis=diagnosis_payload,
|
|
100
|
+
decisions=decisions_payload,
|
|
101
|
+
model=args.model,
|
|
102
|
+
skill_path=args.skill,
|
|
103
|
+
)
|
|
104
|
+
rendered = json.dumps(options, indent=2) + "\n"
|
|
105
|
+
|
|
106
|
+
if args.output:
|
|
107
|
+
output_path = Path(args.output).resolve()
|
|
108
|
+
output_path.write_text(rendered, encoding="utf-8")
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
sys.stdout.write(rendered)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from .editorial_style import StyleProfileValidationError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_editorial_corpus_manifest(path: str | Path) -> dict[str, list[dict[str, Any]]]:
|
|
12
|
+
manifest_path = Path(path).resolve()
|
|
13
|
+
raw = yaml.safe_load(manifest_path.read_text(encoding="utf-8"))
|
|
14
|
+
if not isinstance(raw, dict):
|
|
15
|
+
raise StyleProfileValidationError(f"Editorial corpus manifest must be a mapping: {manifest_path}")
|
|
16
|
+
|
|
17
|
+
manifest: dict[str, list[dict[str, Any]]] = {"mustFail": [], "mustPass": []}
|
|
18
|
+
for section in ("mustFail", "mustPass"):
|
|
19
|
+
entries = raw.get(section)
|
|
20
|
+
if entries is None:
|
|
21
|
+
continue
|
|
22
|
+
if not isinstance(entries, list):
|
|
23
|
+
raise StyleProfileValidationError(f"{section} must be a list in {manifest_path}")
|
|
24
|
+
normalized: list[dict[str, Any]] = []
|
|
25
|
+
for index, entry in enumerate(entries):
|
|
26
|
+
if not isinstance(entry, dict):
|
|
27
|
+
raise StyleProfileValidationError(f"{section}[{index}] must be a mapping in {manifest_path}")
|
|
28
|
+
entry_id = str(entry.get("id", "")).strip()
|
|
29
|
+
rel_path = str(entry.get("path", "")).strip()
|
|
30
|
+
if not entry_id or not rel_path:
|
|
31
|
+
raise StyleProfileValidationError(
|
|
32
|
+
f"{section}[{index}] requires id and path in {manifest_path}"
|
|
33
|
+
)
|
|
34
|
+
draft_path = (manifest_path.parent / rel_path).resolve()
|
|
35
|
+
if not draft_path.is_file():
|
|
36
|
+
raise StyleProfileValidationError(f"Corpus draft not found: {draft_path}")
|
|
37
|
+
normalized_entry = {"id": entry_id, "path": rel_path, "draftPath": str(draft_path)}
|
|
38
|
+
if section == "mustFail":
|
|
39
|
+
expect_terms = entry.get("expectTerms")
|
|
40
|
+
if not isinstance(expect_terms, list) or not expect_terms:
|
|
41
|
+
raise StyleProfileValidationError(
|
|
42
|
+
f"{section}[{index}].expectTerms must be a non-empty list in {manifest_path}"
|
|
43
|
+
)
|
|
44
|
+
normalized_entry["expectTerms"] = [str(term).strip() for term in expect_terms if str(term).strip()]
|
|
45
|
+
else:
|
|
46
|
+
source_sample = entry.get("sourceSample")
|
|
47
|
+
if isinstance(source_sample, str) and source_sample.strip():
|
|
48
|
+
normalized_entry["sourceSample"] = source_sample.strip()
|
|
49
|
+
normalized.append(normalized_entry)
|
|
50
|
+
manifest[section] = normalized
|
|
51
|
+
return manifest
|