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 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).
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Limatus: a diagnose-and-steer quality loop for AI-generated content."""
2
+
3
+ __version__ = "0.2.0"
@@ -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