Publish values from the analysis through the Python or R emitter, write them into the manuscript as bindings, and get an unbound number out of the red. Use when writing or changing an analysis script in a manuscript-guard project, when putting a result, table or figure into the text, or when check reports unclassified-number, unresolved-binding, value-splits-paragraph, unquoted-result, hand-authored-table, results-edited, script-newer, input-changed or duplicate-quantity.
Install
npx skillscat add basilechretien/manuscript-guard/results-binding Install via the SkillsCat registry.
Binding the numbers
The rule the whole toolkit rests on: in manuscript source, a bare number is a defect
unless it is a recognised convention or a structural reference. A number from the
analysis cannot be typed at all. It is published by the analysis and bound by name, so it
cannot go stale and cannot pass by coinciding with something.
That takes two halves: the analysis publishes, and the manuscript binds.
1. Publish from the analysis
Python:
from manuscript_guard.emit import Emitter
em = Emitter(__file__, inputs=["data/cohort.csv"]) # inputs relative to the project root
em.value("cohort.n", len(rows)) # an int displays as itself: 4000
em.value("cohort.n_exposed", n, display=f"{n:,}") # when the journal wants 4,000
em.value("case.pct_serious", pct, digits=1) # a float needs digits or display
em.value("model.p", p, display="<0.001") # a comparator only has to hold
em.interval("ror", point, low, high, digits=2) # ror.point, ror.ci_low, ror.ci_high
em.value("table2x2.a", a, quoted=False) # published but not quoted in the text
em.write() # results/<script stem>.json + .sha256R, after remotes::install_github("BasileChretien/manuscript-guard", subdir = "r/manuscriptguard")
or R CMD INSTALL on the same directory of a clone:
library(manuscriptguard)
em <- mg_emitter("analysis/02_model.R", inputs = "data/cohort.csv") # run from the project root
em$value("model.n", 412L)
em$interval("model.or", 2.5, 1.8, 3.4, digits = 2)
em$write()What the emitter holds you to:
- Rounding is decided here, once.
displayis fixed at emit time, so one quantity cannot
be rounded two ways in two sections. There is no formatting option in the manuscript. If a
coarser rounding is needed somewhere, emit a second key. - Keys are lower case, dotted:
cohort.n_reports. Table keys take no dots. A bad key is
not caught when emitting; the whole fragment then fails the schema atcheck, and none of
its values load. - One namespace covers all scripts. A key published by two scripts is
duplicate-key. - Inputs are declared, and their digests recorded. Change the data and G1 says which
results are stale. - Units are not rendered. Write
{{results.case.pct_serious}}%in the text. - NumPy integers are refused; pass
int(x). Years and study periods are numbers like any
other: emit them. - An R string holding digits is not refused the way Python refuses it. Do not publish
"12.3 (95% CI 10.1 to 14.9)"as one value; publish the three numbers.
Never edit results/. The fragments are machine-written and digested. An edited one isresults-edited, the plugin refuses the write, and manuscript-guard verify re-runs the
analysis in a scratch copy and compares value by value.
2. Bind in the manuscript
The database held {{results.cohort.n_reports}} reports. The reporting odds ratio was
{{results.ror.point}} (95% CI {{results.ror.ci_low}} to {{results.ror.ci_high}}), and
{{results.case.pct_serious}}% of cases were serious [@smith2020Cohort].
{{table.baseline}}
{{figure.forest}}{{results.…}}resolves againstresults/;{{lit.…}}againstliterature/ledger.yaml
andattested.yaml(the literature-verify skill).{{table.key}}goes on its own line and becomes the table with its caption.{{figure.key}}insertsfigures/<key>.<ext>. Neither is numbered for you. A mistyped
figure key passescheckand fails atbuild; a mistyped table key is named only atbuild, thoughcheckreports the intended table asunplaced-table.- Cross-references such as "Table 2" and "Figure 1" are accepted as structural and never
checked against the real numbering. Number them by hand and look. - Text inside HTML comments is ignored and never reaches the document. A number inside a
fenced code block is judged as code, and a number in a string literal there is a claim. manuscript/supplementary/is checked the same way and built as its own document.
3. Tables and figures come from results
A pipe table typed with numbers in it is hand-authored-table. Emit it:
em.table(
"baseline",
columns=["Characteristic", "Exposed", "Unexposed"],
rows=[
["Reports", n_exp, n_unexp],
["Female", em.cell("{} ({})", f_exp, (100 * f_exp / n_exp, 1)),
em.cell("{} ({})", f_unexp, (100 * f_unexp / n_unexp, 1))],
],
caption="Characteristics by exposure. Values are n (%) unless stated.",
)A cell holding more than one number must be built with em.cell, which records its parts.
An f-string produces the same characters and none of the provenance, and is refused astyped-composite-cell.
A figure script lives in figures/, reads the results, and types no data values. Numbers in
the rendered figure must be result displays, conventions, or declared ticks infigures/<name>.guard.yaml, each with a why. With matplotlib, setrcParams["svg.fonttype"] = "none" or the figure has no text to check. Reviewing the
rendered figure is the figure-review skill.
4. Getting a number out of the red
manuscript-guard explain manuscript/main.md # every number, and the rule that took it
manuscript-guard bind # each unbound number, and the ways outThere are four ways out, and usually it is the first:
It is already published and was typed instead of bound.
bindnames the key whose
display matches.bind --apply --only manuscript/main.md:42applies the suggestions on
line 42 only, and is refused if any number on that line has no single match; the path is
relative to the project root.bind --applyalone takes every unambiguous
match, and a match is a guess: "10 of them" can be bound to a ten-year study period.
Where two keys display the same,bindrefuses to choose, and so should you.It should be published. Emit it from the analysis and bind it.
It comes from the literature. Ledger entry, with the quote and the stored source.
It is a convention of scientific writing, not a result. The shipped conventions
cover things likep < 0.05in the Methods,95% CIandper 1,000 person-years. For
a convention of your own field, add one topaper.yamlwith a reason, pinned to the
value rather than to a shape:conventions: - pattern: '(?i)minor allele frequency (?:below|<) 1%' why: Standard genotype quality-control threshold, stated in the Methods. added_on: 2026-09-24A name that contains digits, such as a gene or a product code, goes under
terms:.If
p < 0.05is reported although it is in the Methods, look above it for a line that
starts with#, or sits over a line of-or=, inside a paragraph, a list item or a
quotation. Pandoc prints that line as text, not as a heading, and the gate ends the
Methods there. Do not add a convention. If the line is text ("# of reports" moved to the
start of a line by a hard wrap), rewrap it or write\#. If it was meant as a heading,
put a blank line before it. If the-line under it was meant as a rule, put the blank
line before the rule.Every run reports how many numbers the project's own rules accounted for. A pattern
wide enough to cover results is allowed, and it is visible, and it switches the check
off.
A value that matches is a suggestion, never evidence. The gate never accepts a literal
because it matches a result. bind uses the match only to propose a binding, which is then
checked like any other.
5. The findings
| Code | Ask |
|---|---|
unclassified-number |
bind it, emit it, cite it, or declare the convention |
unresolved-binding |
the key does not exist; the finding suggests the nearest one |
value-splits-paragraph |
the value prints display maths, a LaTeX environment, an HTML block tag or a line break into its sentence, where it can break the paragraph in parts in Word. Write that part in the .md, as a block of its own, and bind only the numbers in it |
unquoted-result |
a published value is quoted nowhere: quote it, or emit with quoted=False |
unplaced-table |
an emitted table is never placed with {{table.key}} |
hand-authored-table |
emit the table |
script-newer, input-changed |
re-run the script that wrote the results |
input-missing |
restore the input, or stop declaring it, then re-run |
results-edited, no-digest |
re-run through the emitter; never edit the fragment |
duplicate-quantity |
a warning: two quoted keys hold the same value. One quantity? Publish it once. A coincidence? Nothing to do |
divergent-display |
the same value is displayed two ways; pick one display |
declared-same-but-differs |
two keys declared one quantity with same_as= have drifted apart |
interval-reversed |
the text quotes the upper bound first; swap the two bindings |
estimate-outside-interval, interval-inverted |
fix the interval in the analysis, with interval() |
If you are a model doing this
Never type a number the analysis produced, even one you can read straight off the results
file. Bind it. The check would catch the literal, and binding is the whole point.
Never run bind --apply without --only on a manuscript you did not just write. Read each
suggestion, and apply the ones whose meaning, not just value, is right.