Skip to Content
PackagesContract Check

Contract check

Reads what a contract says. It finds the defined terms (where each is defined, what the definition says and where it is used) and reports what is wrong with them: a term used but never defined, defined but never used, defined twice, used before its definition. Uses are underlined and hovering one shows its definition; findings are listed in a panel and saved as a Markdown or JSON report. Detection is deterministic: the same document gives the same terms and findings, in the same order, with no model involved.

Packages

PackageContents
@stesura/contract-checkcontractCheckPlugin, buildTermIndex, findings, review state, reports, language profiles, settings
@stesura/contract-check-reactcreateContractCheckUIExtension: the Review-tab button, findings panel, status-bar count, hover card

Setup

import { contractCheckPlugin } from "@stesura/contract-check"; import { contractCheckUIExtension } from "@stesura/contract-check-react"; // in the mount hook's extraPlugins contractCheckPlugin(); // in uiExtensions (module level) const uiExtensions = [contractCheckUIExtension];

To take the organisation’s settings from an endpoint, create the extension with it instead:

import { createContractCheckUIExtension } from "@stesura/contract-check-react"; const contractCheckUI = createContractCheckUIExtension({ settingsEndpoint: "/api/contract-check/settings" });

The plugin rebuilds the index a debounce after the last edit (500 ms by default), in the root editor only, and maps positions in between. Underlines are drawn for what is on screen: the UI extension reports the visible range as the reader scrolls. Without it (headless, tests) the whole document is underlined.

The card opens when the pointer is over a term or the caret is in one; a live region reads it, and Escape closes it. Keyboard users go to the definition with goToDefinitionAtSelection, which the host binds to a key of its choice.

Findings

RuleSeverityReports
CHK-1 Used not definedhintA capitalised phrase used like a term (at least twice, after an article at least half the time) that nothing defines. A phrase before a defined term counts on its own (Subsidiary in “Subsidiary of the Borrower”); a clause’s run-in heading does not. Off for German, where every noun is capitalised.
CHK-2 Defined not usedwarningA definition with no use outside headers, footers and its own body (for a definition in a sentence, the words before the term). Not reported: a party’s name (Blackpebble SARL), or one half of a singular and plural defined together when the other half is used.
CHK-3 Duplicated termwarningA term defined twice in one scope, or singular and plural both defined in different places. Template alternatives and lower-case terms are not duplicates.
CHK-4 Inconsistent capitalizationhintA multi-word term written with other capitals (permitted security). One word in lower case is the ordinary word.
CHK-5 Orphan short formwarningAn all-caps abbreviation that is never defined and not a known short form. Off by default: switch it on in the settings.
CHK-6 Used before definedwarningA term defined in a sentence (not a definitions list) and used before it, unless followed by a forward reference (“as defined below”, “as hereinafter defined”). Not reported when the term is used more before its definition than after it (the definition belongs to a later part, such as a schedule), for a clause’s run-in heading, or for a lower-case word.
CHK-10 Inconsistent separatorshintAn entry whose separator (:, means, a tab) differs from most entries of the same definitions list.
CHK-22 Structure driftwarningA clause number in one language version and not the other (“7.1” only in the French version). Numbers only: limbs and ordinal words are not compared.
CHK-30 Typed referencehintA typed reference (“Clause 5.2(a)”) that resolves to exactly one clause: a live cross-reference would follow renumbering. One finding per target. Fixable when the clause’s counter reads what is typed: the number becomes a live cross-reference.
CHK-31 Reference not founderrorA typed reference to a clause this document does not have. Not reported: references to another document or a statute (of the Intercreditor Agreement, du Code civil, § 5 BGB, artikel 30 lid 5 AVG), a number far past the document’s last clause, units inside the current article (Abs. 2, comma 6, §1 of this article), and limbs typed in the text of a clause that exists.
CHK-32 Ambiguous referencewarningA typed reference that matches several clauses. A schedule’s restarted numbering does not count (the body’s clause wins), nor a smaller list beside the main one.
CHK-33 Broken cross-referenceerrorA live cross-reference whose target no longer exists.
CHK-34 Schedule referenceswarningA schedule, annex, appendix or exhibit cited but missing, or present but never cited. Missing ones are reported only when the document holds at least two of that kind: a document whose schedules are separate cites them freely.
CHK-35 Wrong directionwarningA live “above”/“below” reference whose target lies the other way.
CHK-36 Definition referencewarning“X as defined in Clause N” (or “has the meaning given to it in”, “a le sens qui lui est donné à l’article”, “wie in Ziffer … definiert”, per language) where Clause N, with the clauses numbered under it, does not define X. A list cites every clause in it (“Clauses 2.1 and 1.1”), and “Schedule 1” the whole schedule. The message names where X is defined, or says nothing in the document defines it. A clause counts as defining X when its text quotes, bolds or underlines X, or follows it with a definition verb (“Agent means”): detection misses some definitions. Not reported: a clause that does not exist (CHK-31) or is ambiguous (CHK-32), another document’s clause, a typed reference in an unassembled template, a clause that includes another template, a paragraph of another definition (“paragraph (a) of the definition of Acceptable Bank”), and a definition that itself points at the cited clause.
CHK-40 Incomplete itemswarningA blank left in the text: [●], [•], [ ], [__], ___, [insert …], […], TBD, TBC, {{…}}, and per language [à compléter], [Platzhalter], Dutch PM. XX counts, except right after a reference word (Article XX) in a document that uses two or more Roman references (Article XIX, Article XXI). One finding per kind of blank per paragraph. Highlights are not read: format-lint’s FMT-23 reports highlighted paragraphs.
CHK-41 Unmatched bracketswarningA parenthesis left open or closing nothing in its paragraph or the few around it (a lead-in’s parenthesis may close in a later limb). List labels (a), iv), 13.2.b)) are not brackets. Square brackets and quotation marks pair across paragraphs (a template’s optional text and a quoted clause run over several), under the language’s quotation styles; a straight " pairs with any mark. Paragraphs carrying template markup (<…, `
CHK-42 Words and figures differerrorNumber words that say another number than the figure after them: ten (11), dix (11), zehn (11). The figure is read with the language’s separators (30.000 is thirty thousand in French, thirty in English); Belgian septante and nonante are read. Not reported: words that do not read as one number, an article alone (een (2)), a percentage (cinq pour cent (5)), and words where the part after “and” matches (one hundred and twenty (20)).
CHK-43 Editing artifactswarningStrikethrough set by hand (not a tracked deletion), and drafting notes: NTD, [Note:, [Anm.:, [Noot:, ??. Paragraphs struck through whole and in a row are one finding.

Each finding has an id that hashes its rule, scope and term (plus the block id for CHK-40 to CHK-43), so it survives edits elsewhere and re-imports. Heuristic findings (CHK-1, CHK-5) carry a confidence and a band (recommend, shown as “likely”, or possible). CHK-30 findings can be fixed (fixable): see Cross-references.

The panel lists errors and warnings, then hints, in document order. Each finding can be ignored or flagged; that state is the doc attr contractCheckReview (ignored or flagged per finding id), shared with collaborators (last write wins) and not part of undo. Open errors and warnings are tinted in the text; hovering a finding in the panel outlines its ranges. A finding that points to several places (a term defined twice) steps through them with ‹ n/m › (checkFindingRange(state, id, index), focusCheckFinding(id, index)). The status bar counts open errors and warnings.

Checking can be switched off from the status bar or the Review group’s menu (setContractCheckEnabled(view, on), or contractCheckPlugin({ enabled: false })): off, nothing is built or drawn. The UI keeps the choice as a user preference in localStorage, not in the document, and every mounted editor follows it; until the user switches it on, it is off. Pass contractCheckPlugin({ enabled: readContractCheckEnabled }) (from @stesura/contract-check-react) so an editor the user switched off never runs its first build.

Read them from code with selectCheckFindings(state) (ranges mapped to the current document, review state attached), or headless with runContractCheck(doc).

Cross-references

Typed references are matched against the clause labels core derives from the numbering (getClauseLabels), and read with the same tokenizeReference the AI navigation tools use, so “paragraph (b) of Clause 5.2” and “5.2(b)” are one clause. Numbers typed by hand (“5.2 The Borrower…”, “Art. 7 Durata”) count as clauses too. Labels need the counters the numbering plugin writes: in the editor they are there; headless (a CLI, a server), run the numbering plugin over the imported document before checking it.

A typed reference can be made live: the panel’s Fix (or Fix all) replaces its number with a cross-reference to the clause, the way a Word user types “Clause ” and inserts the number as a field. “paragraph (b) of Clause 5.2” gets two, one to the limb and one to its clause; a schedule’s number stays typed. A number converts only when the live reference would read exactly what is typed: it shows the clause’s number without a trailing full stop, as Word does (“Clause 17.3” against a heading numbered “17.3.” converts; “1.2(a)” against “1.2 (a)” does not). Numbers typed by hand have no counter to point at, and a number with a tracked change inside stays typed. From code, applyCheckFixes(ids | "all") collects the findings again, applies them in one transaction (one undo step) and leaves a summary (getCheckFixSummary). While track changes is on it refuses, since every conversion would be a tracked deletion and insertion; { force: true } applies anyway, and the panel offers “Apply anyway”.

Reports

renderCheckReport(findings, index, { format }) writes Markdown for a reader or an agent, or JSON for a program. Findings are in document order and quotes cite their block with the same {#id} tags read_document emits; there are no timestamps, so the same document gives the same bytes. The panel saves both.

How a term is found

Every candidate scores weighted signals: quotes, bold, a definition verb (means, désigne, bedeutet, …), a parenthesis ((the “Borrower”), (la **Société**), (les Statuts)), a separator, a run of neighbouring definitions, and penalties for headings, form labels and template markup. A score of 5 or more makes a term; 4 to 5 is a possible definition, kept for findings. A lower-case term introduced by “includes” (a “person” includes …, comprend, umfasst, omvat, comprende, incluye) is the construction clause widening an ordinary word, not a term; lower-case terms introduced otherwise (referred to as “family members”) stay terms. The weights were calibrated on a labelled corpus and live in the settings, per language.

Uses match the longest term first (Permitted Security is not also a use of Security), with plurals, possessives and the profile’s other forms. Matching is case-sensitive. Text in a tracked deletion is not read; header and footer text is underlined but never counts as a use.

Languages and scopes

English, French, German, Dutch, Italian and Spanish each have a LanguageProfile. English and French are validated against labelled contracts; German, Dutch, Italian and Spanish ship unvalidated (profile.validated is false): they were measured on one contract each and fall below the precision or recall the other two reach. The language comes from the text, not the document’s proofing attributes, which imports often get wrong. A bilingual document whose versions follow each other gets one scope per version, linked as translations: a French use matches the French definitions, and a footnote belongs to the version its anchor is in.

Settings

CheckSettings is JSON, meant to come from an organisation’s settings endpoint (settingsEndpoint above; parseCheckSettings drops anything malformed). It overrides weights and thresholds, switches rules on or off, and adds common terms (read by CHK-1) and short forms (read by CHK-5) to the profiles.

contractCheckPlugin({ settings: { commonTerms: { fr: ["Commission de Surveillance du Secteur Financier"] }, weights: { de: { bold: 2 } }, thresholds: { recommend: 5.5 }, rules: { "CHK-5": true }, }, });
Last updated on