Metadata-Version: 2.4
Name: zh-narrative-guard
Version: 0.1.0
Summary: Zero-dependency deterministic consistency checkers for Chinese narrative text (time-jump directives, weekday arithmetic, clichéd similes)
Project-URL: Homepage, https://github.com/felixchaos/zh-narrative-guard
Project-URL: Issues, https://github.com/felixchaos/zh-narrative-guard/issues
Author: Stellatrix Labs
License-Expression: MIT
License-File: LICENSE
Keywords: chinese,consistency,narrative,nlp,roleplay,timeline,weekday
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: Chinese (Simplified)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Text Processing :: Linguistic
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# 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

```bash
pip install zh-narrative-guard
```

Requires Python 3.10+.

## Quickstart

```python
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:

1. **Dropped book-specific timeline tokens.** The source `_TIME_TOKEN` also
   matched a novel's place-name timeline labels (`柏林` / `图卢兹` / `基地`).
   In ordinary prose those are pure false positives (`柏林墙倒塌那年`, `回到基地`),
   so they are removed here.
2. **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 (`周６`) 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](LICENSE).

## Acknowledgements

Extracted from the RPG Roleplay platform (https://github.com/felixchaos/rpg-roleplay-platform).
