Ype! Writer (dsh-ype-writer)
[!Tip]
中文版见 README_zh_CN.md。
A DSH article auditor that scores an article paragraph by paragraph against a rule.toml. Every rule opens its own LLM session, so the scoring standard is reproducible, reviewable, and committable.
- Audience / anti-audience: two independent sessions score audience fit and anti-audience legibility, from which a measured "pandering index" is derived and compared with the target.
- One session per rule: anti-patterns, don'ts, and requirements each run in their own session;
llm = Nruns replicas and averages them. - Script rules: Python functions for deterministic checks; no tokens spent.
- No runtime dependencies: the TOML parser ships with the package; nothing extra to install into a profile.
Install and load
Requirements:
- DSH with the plugin manager available (the
plugin_managertool or Creator mode) - Node.js 20 or newer
- Python 3 only if you use script or filter rules
From the DSH CLI
Install into the profile you boot (replace web with your profile name):
dsh plugin --profile web add github:catilgrass/ype-writer
The package is added to dsh.profile.bundles automatically. HMR applies it to the running profile; otherwise restart dsh web. Remove it with:
dsh plugin --profile web remove dsh-ype-writer
From the DSH web UI
- Open Plugins in the sidebar and click Add plugin.
- Enter
https://github.com/catilgrass/ype-writer. - Click Install and wait for "Installed".
Ask an agent
Paste this into a session that can use the plugin manager (Creator mode, or full access):
Install the DSH bundle from https://github.com/catilgrass/ype-writer into this profile with
plugin_manager(install_bundle), then confirmype_checkappears in the tool list andype-writerin the skill catalog. Finish with a smoke check:ype_check(template: "tutorial-guide", text: "This is a smoke test.")
The loader hot-loads the bundle, so ype_check appears in every session using that profile, and the ype-writer skill joins the skill catalog when the profile has one. Working from a local checkout? Use target with the absolute path instead, and reinstall after edits.
Use
ype_check(rule: "examples/rule.toml", path: "examples/article.md")
| Argument | Required | Meaning |
|---|---|---|
rule |
one of rule/template | Path to the rule file, absolute or relative to the session working directory |
template |
one of rule/template | Bundled template name such as health-science or zh_CN/health-science |
text |
one of text/path | Full article text |
path |
one of text/path | Path to the article file |
The tool returns a Markdown report (value.report) plus structured per-paragraph and per-rule results for programmatic use. The report keeps every rule's raw score so the final score can be recomputed.
Try it on this README:
ype_check(rule: "readme-review.toml", path: "README.md")
Visual report
The browser half renders a card after each ype_check with the score, paragraph count, and warning count; "Open report" selects the Article Review Report panel, which draws the score ring, per-paragraph score bars, the rule table, and warnings. Card and panel read one structured projection (presentationMeta), so opening the panel costs no extra Host call.
If this version added the client half while DSH was already running, restart the DSH process once: the client module registry caches a package's client declaration for the process lifetime. After the restart the card appears on the next check.
Temporary rules
If rule points at something that is not valid TOML — a Markdown review spec, a near-miss rule, a broken file — the tool turns it into a valid rule: it keeps the requirements the file states (even in prose or Markdown), repairs a malformed TOML, and infers the rest from the article. The check then runs with that rule, prints it under "Temporary rule" in the report, and adds a warning saying whether the rule was generated or repaired. The draft is never written to disk. This costs one extra model call (two at most, if the first draft fails validation) and is meant for one-off reviews where committing a rule file is not possible. Disable it with autoDraftRules: false in the plugin config.
Filters
Filters narrow the review to the copy that matters. Each filter is a Python script defining filter(text) that returns only the text to check; the returned string is split into paragraphs once and shared by every session. Comment-only reviews and Markdown stripping use the same contract.
[[filters]]
name = "comments only"
code = '''
def filter(text):
return "\n\n".join(line.strip() for line in text.splitlines() if line.strip().startswith("//"))
'''
Three selection scripts ship in filters/: comments.py (only // lines), markdown.py (prose without Markdown scaffolding; fence markers dropped, contents kept), and code.py (only fenced code). Reference one by path, relative to the rule file:
[[filters]]
path = "filters/markdown.py"
Filters run in declared order. A chain that selects nothing fails the check instead of silently scoring an empty article, and the report lists which filters ran and how many characters they removed.
Skill and templates
On load, the plugin registers a ype-writer skill with the profile's skill registry (profiles without a skills service simply get the tool). The skill documents the recommended workflow: write the article → copy a template into rule.toml → adapt it → run ype_check → iterate on the report, plus rule design guidance, how to read the report, and common mistakes.
templates/ ships eight copy-ready rule templates:
| Template | Use for | Main guardrails |
|---|---|---|
health-science.toml |
Patient and family health explainers | cure claims, fear, replacing medication, pseudoscience |
science-explainer.toml |
Science for general readers | correlation as causation, hype, unexplained jargon |
product-review.toml |
Consumer product reviews | undisclosed interests, fake testing, one-sided praise |
tutorial-guide.toml |
Setup and step-by-step guides | skipped steps, unrunnable commands, missing versions |
opinion-piece.toml |
Editorials and argument essays | straw men, personal attacks, fact/judgment blur |
news-report.toml |
Event reporting and interviews | unverified facts, clickbait, single sources |
code-review.toml |
Pull-request and snippet reviews | hard-coded secrets, swallowed errors, leaks |
write-documents.toml |
Project documentation and user-facing copy | marketing hype, negation-first phrasing, filler |
Chinese copies live in templates/zh_CN/ under the same file names; tests keep the two sets in sync. When the skill loads, the absolute path of the installed template directory is appended to its instructions, so an agent can read the templates without guessing where the package lives.
rule.toml reference
See examples/rule.toml for a complete example.
[target]
audience = "backend engineers with 3-5 years of experience who know SQL but not query optimizers"
anti_audience = "product managers with no database experience" # inferred by the LLM when omitted
pandering = 3 # target pandering 1-5, default 3
audience_llm = 1 # audience sessions, default 1
anti_audience_llm = 1 # anti-audience sessions, default 1
[[anti_patterns]] # a hit zeroes that paragraph
name = "Disclaimer"
description = 'paragraph contains "for reference only" or "not advice"'
llm = 1
[[donts]] # violation scored 1-5
name = "Empty adjectives"
instruction = 'uses "ultimate", "perfect", or "revolutionary" without information'
scope = "paragraph" # paragraph (default) | article
llm = 2 # two independent sessions, averaged
[[requirements]] # compliance scored 1-5
name = "Actionable conclusion"
instruction = "the article ends with an executable conclusion"
scope = "article"
[[scripts]] # deterministic script rule
name = "First person"
path = "scripts/no_first_person.py" # relative to rule.toml; or inline code = "..."
Field constraints:
| Field | Constraint |
|---|---|
audience |
Non-empty string, or a non-empty string array (joined with ; ) |
anti_audience |
Same; omitted or empty means the LLM infers it |
pandering |
Number from 1 to 5 |
audience_llm / anti_audience_llm / llm |
Integer from 1 to 8 |
scope |
paragraph (default) or article; scripts only accept paragraph |
[[scripts]] |
Exactly one of path / code; a supplied path must exist |
[[filters]] |
Exactly one of path / code; the script defines filter(text) and returns the text to check |
Unknown fields, wrong types, and TOML syntax errors fail before any model call, with the file name and line/column. Limits: 20 rules per kind, 10 scripts.
Scoring
Pandering index
Each paragraph receives a 1-5 integer from the audience session and another from the anti-audience session:
audienceFit: how well the paragraph serves the target audience (higher is better fit)antiAudienceFit: how easily the anti-audience still follows the paragraph (higher is more legible to them)
pandering index m = 1 + 4 × audienceFit / (audienceFit + antiAudienceFit)
closeness c = clamp(5 - 2 × |m - target|, 0, 5)
m = 3 means both groups understand; m → 5 means only the audience does (in-group jargon, high density); m → 1 means only the anti-audience does. The closer to the target, the higher the closeness.
Rule and paragraph scores
Every rule (anti-pattern, don't, requirement, script) first produces an averaged raw score:
- with N LLM sessions for one rule, each session scores independently and replicas are averaged per paragraph;
- anti-patterns are boolean, so N sessions use a majority vote (strictly more than half must agree);
- script rules return a 0-5 violation per paragraph.
Quality conversion: don't/script quality = 6 - violation (0 violations score 5), requirement quality = compliance, anti-pattern hit scores 0 and a miss scores 5.
paragraph score = anti-pattern hit ? 0
: weighted mean(closeness × 3, each rule's quality × 1)
overall score = weighted mean(paragraph mean × 3, each article rule's quality × 1)
Paragraphs a rule does not apply to are omitted by the session and count as "not participating". A failed session adds a warning and lowers that rule's confidence instead of failing the check; the check only fails when every session fails.
Script rules
A script is a Python file defining check(text):
def check(text: str):
return 0 # violation 0-5; 0 means no violation
# return None # this paragraph does not participate
One Python process is started per script and check is called once per paragraph. Script stdout is redirected to stderr so it cannot pollute the result; out-of-range values, exceptions, and timeouts become warnings. The interpreter is configured with config.python (default python3).
Plugin config
config is optional; unknown fields fail activation so typos cannot be silently ignored.
| Field | Default | Meaning |
|---|---|---|
provider / model |
current session model | Override the route used by scoring sessions (must be set together) |
reasoningEffort |
session value | Override reasoning effort |
maxParagraphs |
80 |
Maximum paragraphs scored; the rest are truncated with a warning |
maxParagraphChars |
2000 |
Maximum characters per paragraph; longer ones are clipped |
maxConcurrentLlm |
6 |
Maximum concurrent model sessions |
llmTimeoutMs |
180000 |
Timeout for one model session |
maxOutputTokens |
12000 |
Output limit for one model session |
python |
python3 |
Interpreter for script rules |
autoDraftRules |
true |
Draft a temporary rule when the supplied rule file is not parseable TOML |
scriptTimeoutMs |
20000 |
Timeout for one script |
Cost
Model calls per check:
1 (anti-audience inference, when anti_audience is omitted)
+ audience_llm
+ anti_audience_llm
+ Σ each rule's llm
Every session receives the whole article, so token cost is roughly article tokens × session count. With defaults (1 audience, 1 anti-audience, a few rules at 1 each) a check is about 4-8 sessions. Start with one session per rule; add replicas only for rules that matter. When the rule file cannot be parsed and autoDraftRules is on, add one call for the draft (two at most).
Implementation and maintenance
- Zero dependencies: only Node built-ins and files in this repository.
lib/toml.jsis a TOML subset parser covering tables, arrays of tables, dotted keys, multi-line strings, inline tables, numbers, and dates (returned as source text). - Public APIs only: registers through
ctx.tools.register, calls models throughctx.llm.stream, never writes session logs or touches private state. - Testable:
lib/is split by responsibility (TOML → rule validation → scoring math → model calls → Python → engine → tool entry). 48node --testcases cover parsing, validation, formulas, aggregation, and failure tolerance without network access. - Browser half:
lib/client.jsdeclaresdsh.client, registers aype_checktool card and theype-reviewmain panel, and localizes through the shell's locale service. The panel readspresentationMeta, never the Markdown report.
Known limitations:
- Scores depend on an LLM, so two checks of the same article can differ;
llm = Nonly reduces variance. - Anti-patterns use a majority vote; vaguer rules make sessions more likely to disagree.
- Long articles are truncated by
maxParagraphs/maxParagraphChars, and the report marks it. - Prompts are English; they generally still work on non-English articles, but no language adaptation is done.
- Script rules need a local Python; without it only the script rules fail and the rest of the check still runs.
No comments yet. Be the first to write one.