zh-narrative-guard
Zero-dependency, deterministic consistency checkers for Chinese narrative text — the kind of self-consistency bugs an LLM produces while writing roleplay or fiction. Pure functions in, findings out. No LLM, no I/O, no state machine.
- No dependencies. Pure standard library (
re,dataclasses). - Deterministic. Same text always yields the same findings — safe to run in a hot path.
- Advisory, not destructive. Every checker reports; it never rewrites your text.
- Dormant when irrelevant. The weekday checker stays silent unless the text actually pins a concrete weekday, so calendar-free settings pay nothing.
The design philosophy: prefer a miss to a misfire (宁漏勿误)
Chinese single characters are hopelessly polysemous as signals:
| Character | Time meaning | But also… |
|---|---|---|
| 周 | week | 四周 = all around |
| 后 | after / later | 后先用真气 = then first uses qi |
| 次 | time(s) | 第二次 = the second attempt |
| 天 | day | 天空 = the sky |
Treating any single character as a time signal produces false positives, and these detectors were forged by fixing three successive production bug families that were all exactly that. So the rule throughout is: a missed issue is recoverable downstream, a false alarm directly harasses the writer. Every checker here errs toward silence.
Concretely, a time value is only accepted when it is a time-shaped token — a
quantity + unit + after/before (三天后), an ordinal chapter/day (第3章 /
第二天), a year (公元1024年), a clock time (八点半), or a named
time-of-day / season (清晨 / 傍晚 / 入冬). A lone polysemous character is
never enough. Likewise a recall/flashback/fantasy framing (回想 / 梦见 /
想象) suppresses time-jump detection entirely — reminiscing is not jumping.
Install
pip install zh-narrative-guard
Requires Python 3.10+.
Quickstart
from zh_narrative_guard import (
detect_time_directives,
is_recall_framing,
detect_weekday_violations,
detect_cliche_violations,
)
# 1. Explicit "jump the timeline to <T>" directives.
d = detect_time_directives("时间跳到三天后")
assert d[0].target == "三天后"
# ...but action narration and flashbacks are NOT jumps:
assert detect_time_directives("进入后先用真气感知四周环境") == []
assert is_recall_framing("我继续回想:在进入主神空间前的日子")
# 2. Weekday arithmetic the model got wrong.
v = detect_weekday_violations("今天周日,明天周六")
assert v[0]["expected_name"] == "周一" and v[0]["claimed_name"] == "周六"
# Dormant with no concrete "today = 周X" anchor (fantasy / xianxia):
assert detect_weekday_violations("三天后我们在城门口集合") == []
# 3. Clichéd simile phrasing (surface it, do not strip).
assert detect_cliche_violations("他像投石入水般沉了下去")
# Literal usage and function words are never flagged:
assert detect_cliche_violations("他用投石机攻城") == []
assert detect_cliche_violations("如果投石入水就糟了") == []
API
timeline
| Function | Signature | Description |
|---|---|---|
detect_time_directives |
(text: str) -> list[TimeDirective] |
Explicit "jump to <T>" requests. Suppressed entirely if the text reads as a flashback. Each TimeDirective has .target and .raw. |
is_recall_framing |
(text: str) -> bool |
Is the writer reminiscing / dreaming / imagining (a flashback), rather than advancing the clock? |
looks_like_time_value |
(value: str) -> bool |
Does value name a time (a time-shaped token, no personal pronoun), rather than an action clause or a place? |
clean_time_value |
(value: str) -> str |
Trim punctuation, strip leading prepositions and trailing action verbs from a captured value. |
is_time_key |
(key: str) -> bool |
Does a state-key name denote a timeline-anchor field? |
weekday
| Function | Signature | Description |
|---|---|---|
parse_today_weekday |
(text: str) -> int | None |
The concrete "今天 = 周X" anchor (0=Mon … 6=Sun), or None. Only 今天/今日/当天/本日 count; a relative day is never a base. |
detect_weekday_violations |
(text: str, base_weekday: int | None = None) -> list[dict] |
Relative-day mentions (明天/后天/…) paired with an arithmetically wrong weekday. Pass base_weekday to supply an external "today"; otherwise the text's own "今天 = 周X" is the base. No anchor → [] (dormant). |
Each weekday violation is {"rel", "match", "claimed", "expected", "claimed_name", "expected_name", "base_name"}.
cliche
| Function | Signature | Description |
|---|---|---|
detect_cliche_violations |
(text: str) -> list[dict] |
Clichéd "throwing a stone" (投石) simile phrasing. Matches the simile construction only — literal words (投石机 / 投石车 / 用投石砸) and function words (如果 / 例如) are never flagged. |
Each cliché violation is {"pattern_label", "match", "position"}.
Behavior differences from the source platform
This library is extracted from a Chinese roleplay platform. Two deliberate changes were made for the general case:
- Dropped book-specific timeline tokens. The source
_TIME_TOKENalso matched a novel's place-name timeline labels (柏林/图卢兹/基地). In ordinary prose those are pure false positives (柏林墙倒塌那年,回到基地), so they are removed here. - Fixed a simile false positive. The source simile marker
如同?also matched the lone如inside the common function words如果(if),例如(for example),假如(if),如今(nowadays), flagging non-similes such as "如果投石入水就糟了". The marker is now guarded (如同plus a bare如with look-behind/look-ahead against those function words), while real similes —像投石,如同投石, literary bare如投石问路— still match.
Known limitations
Consistent with prefer a miss to a misfire: the weekday token accepts only
ASCII 1-7, so full-width (周6) or Arabic-Indic (周٦) digits are silently
missed rather than mis-checked (a miss is the safe direction). Chinese
numerals (周三) and ASCII (周3) are fully supported.
License
MIT — see LICENSE.
Acknowledgements
Extracted from the RPG Roleplay platform (https://github.com/felixchaos/rpg-roleplay-platform).
Metadata
Release files for zh-narrative-guard 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| zh_narrative_guard-0.1.0.tar.gz | 18.1 kB | With provenance | Details for zh_narrative_guard-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| zh_narrative_guard-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for zh_narrative_guard-0.1.0-py3-none-any.whl |
Total release size: 32.2 kB
Release files / zh_narrative_guard-0.1.0.tar.gz
| Download URL | zh_narrative_guard-0.1.0.tar.gz |
|---|---|
| Size | 18.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9d54dbdccad50d012f3c42895a48ede8d2bc39f12d11b338a804cf997e20c0af
|
|
BLAKE2b-256 checksum How to use checksums |
701fa7dcaacfd61ed2e1b5df5cdd7470ee9aa4a31d7cacaec81785f08dd50e4b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 14, 2026.
Transparency logRelease files / zh_narrative_guard-0.1.0-py3-none-any.whl
| Download URL | zh_narrative_guard-0.1.0-py3-none-any.whl |
|---|---|
| Size | 14.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fa715af6348670214f73a762f34fffcd02264b31d02af0a3a1f0b3eb31bd75ad
|
|
BLAKE2b-256 checksum How to use checksums |
c4fa045e01765e7ec729e46f16ecedd777389c745f36e42a6096461171439ad8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 14, 2026.
Transparency logSecurity for zh-narrative-guard 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for zh_narrative_guard-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 14, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.