ascribe check, build, preview, and fmt
Check a project’s sources, build its outputs, preview its pages, and format its Markdown: options, output, and exit codes.
For AI agents: the documentation index is at llms.txt, and this page is available as Markdown.
The commands that check, build, preview, and format a project. The options every command accepts are in the command reference.
ascribe check
Checks every source file for problems, without building anything: every file-level diagnostic, then the page-level ones for every build in ascribe.toml. The editor and ascribe build run the same checks, so they report the same diagnostics.
[PATHS]...: Files and directories to report on, relative to the current directory. The whole project is still checked, since links, includes, and ids need it, and only the problems that count for these paths are shown: those in a file under one of them, or with a related place in one (a problem a fragment causes on the page that includes it). Such a problem in a fragment is shown once, at its first include. Without--config, the project is the nearestascribe.tomlat or above the first path, and every path must be in it. By default, the whole project.--stdin: Check standard input as the text of the file--pathnames. The text is laid over the project on disk, which isn’t changed; the file doesn’t have to exist. Only the problems that count for it are shown.--path <PATH>: The file standard input is checked as, relative to the current directory. Only with--stdin.--build <NAME>: Check only this build. Repeat it for several. By default, every build inascribe.toml. An unknown build name is a usage error, and the message lists the builds.--editor-build: Run the page-level checks of the editor’s build only, as the editor does as you type. The editor’s build is[editor] buildinascribe.toml, or the first build. With paths that name files, only those files and the pages that include them are checked, which is quick: for a check after every edit. A fullascribe checkstill covers every build.--code <CODE>: Show only the problems with this code (ASC036) or name (link-target-missing), asascribe explaintakes it. Repeat it for several. The whole project is still checked, and the rest is left out of the report, as with paths: the counts, the exit code, a prompt, and the fixes--fixapplies are about what’s left. An unknown code is a usage error, and the message names the closest.--summary: Show how many problems each code and each file has, most first, instead of listing them, then the command that lists the first code’s problems.--format <FORMAT>: How to show the results.text(the default): Problems with source snippets, for people.concise: One line per problem,file:line: [code] message, grouped by file: for an agent. At most 50, or as many as--limitsays, then how many more and the command that lists them all.json: One JSON document, for tools.github: A GitHub Actions workflow command per problem, which shows it on the pull request’s lines, with the path from the repository’s root, then the summary line: for CI.prompt: A prompt for an agent that fixes the problems: about the file, when the paths name one file, else about the project or the paths. Nothing when there are no problems.
--limit <N>: List at most this many problems with--format concise, orallof them. By default, 50. When some are left out, the list ends with how many, and the command that lists them all. Only with--format concise, and not with--summary, which lists none.--exit-code[=<LEVEL>]: Exit with 1 on problems at this level or above:error, the default, orwarning, for CI. Errors fail the command without it. Advice never fails it.error: An error.warning: A warning or an error.
--deny-warnings: Make warnings fail the command too:--exit-code=warning.--base <REV>: The revision whose addresses the check compares the project’s with, when it hasascribe.addresses: anything git accepts (a branch, a tag, a commit). An address the base published that no build publishes now, and that no entry ofascribe.addressesaccounts for, isaddress-lost. By default, the repository’s default branch, the first oforigin/HEAD,origin/main,origin/master,main, andmasterthat exists, from its merge base withHEAD, asascribe diffchooses it. A base that can’t be read isaddresses-base-unreadable, and the rest of the check is the same. Only when the check covers the whole project: with paths,--stdin, or--editor-build, the base isn’t read. A project withoutascribe.addressesreads the base’s only when this names the base:addresses-file-deletedwhen it has one.--base-exact: Compare the addresses with the base revision itself instead of its merge base withHEAD: in CI on a push, the commit before it.--fix: Apply the fixes marked safe, then report what’s left. A fix is safe when applying it can’t change what the page says and leaves nothing to decide, such as linking to a page’s file instead of its route. Only the fixes of the diagnostics reported are applied, so with paths, those that count for them. Unsafe fixes are never applied. The report, and the exit code, are the check after the fixes.--dry-run: Say what it would change, and change nothing.--force: Change files even when git couldn’t put them back as they are: ones with changes not committed, ones git doesn’t track, or ones in no repository.--vale: Check the prose with Vale too, as[checks.vale]inascribe.tomlsets it up. Vale runs once, on the files reported on. Its alerts areprosediagnostics, at their place in the source. A Vale that can’t be run is oneprose-not-checkedadvice, and the rest of the check is the same.[checks.vale] in-check = truechecks the prose without this option. Without[checks.vale], it’s a usage error.--no-baseline: Report the adviceascribe.baselineholds back too, without reading the file. By default, a problem the baseline lists isn’t reported while it’s advice, and is counted instead, and an entry that matches nothing is reported, asbaseline-entry-unmatched. A baseline that can’t be read stops the check, unless this is given.
How diagnostics are reported:
- Each problem is reported once, however many builds it appears in. One that isn’t in every build names the builds it’s in, at the end of its message (
only in build cloud). - A problem in included content is reported at the include, with its place in the fragment as related information.
- Content that no build publishes is checked too, so a problem in an arm no build selects isn’t missed. Its diagnostics say “in content that no build publishes”. Not with
--build, which is about one build.
Every diagnostic, with its fix, is in the diagnostics reference.
Checking as of another day
Two checks depend on the day they run: recheck-overdue reports a page whose recheck date has passed, and release-line-ended a live release line whose ends date has passed, each as of today’s date in UTC. Set ASCRIBE_TODAY=YYYY-MM-DD to check as of another day, such as in a test that mustn’t change with the calendar. Every command that checks, the editor and the hook’s check server included, reads it.
Checking some files
ascribe check docs/guides/install.md reports on that file alone, from a repository’s root as well as from inside the project. A fragment’s problem is reported at every page that includes it, so checking the fragment shows it once, and its repeats says at how many other includes it appears too. The problems shown are the whole check’s, in the same order.
Checking text before it’s saved
--stdin checks text that isn’t on disk yet, such as an editor’s unsaved buffer or an agent’s draft. The file --path names has to be where a source file could be: a .md file in the content root. A link from another page that the new text breaks isn’t shown, since it doesn’t count for that file.
npx ascribe check --stdin --path docs/guides/new.md < draft.md
Checking the prose
--vale reports Vale’s suggestion, warning, and error alerts as advice, warnings, and errors, up to [checks.vale] max-level. Vale is given the prose alone, and a Vale that takes more than ten minutes is given up, as prose-not-checked. See Prose, through Vale.
A quick check after each edit
--editor-build leaves out what the editor doesn’t report: the pass over content no build publishes, the image checks (image-unused and image-large), and, with paths that name files, the content checks across the project. It’s quick enough to run after every edit; the timings are in tests/corpora/RESULTS.md. It can’t be combined with --build. The summary line, and builds_checked in the JSON, name the build, so a clean result says what it covers.
Output for agents
--format concise writes a line for each problem, grouped by file in file order, and by line within a file, and ends with the summary line. A problem with a fix ends its line with → and what the fix does, the help the full output gives:
docs/keys.md:8: [ASC041] this looks like the published route of `install-agent.md`; link to the file instead: `/install-agent.md` → Link to the page's file instead of its route
checked 8 files for every build, reported on 1 file in the paths, 1 with problems: 0 errors, 1 warning, 0 adviceIt lists at most 50 problems. When there are more, the line before the summary says how many it left out, and the command that lists them all: the same check with --limit all (and 11 more: ascribe check docs --code ASC212 --format concise --limit all). --limit <n> sets how many it lists, and --limit all lists every one. --limit is only for --format concise; with another format, or with --summary, it’s a usage error.
--format prompt writes the prompt Prompt your agent describes. It lists at most 20 problems, or 20 files, then names the command that lists the rest. With --stdin, it says the file has unsaved changes.
--summary works in any format. The text lists at most 20 files, then the command whose JSON lists them all. On a project with hundreds of warnings, it shows which rule or file to work through first.
Output for GitHub Actions
--format github writes a GitHub Actions workflow command for each problem, in the same order as --format concise, and ends with the summary line. The workflow run shows each one on the pull request’s line: an error as an error, a warning as a warning, and advice as a notice. The path is from the repository’s root, where GitHub looks for it, even when the project is in a folder of the repository, and the title is the diagnostic’s name and code. The message is one line, without the help or the source snippet:
::warning file=handbook/docs/keys.md,line=8,col=15,endLine=8,endColumn=30,title=link-route (ASC041)::this looks like the published route of `install-agent.md`; link to the file instead: `/install-agent.md`
checked 8 files for every build, 1 with problems: 0 errors, 1 warning, 0 adviceA problem that spans lines has its first and last line, and no columns, which GitHub takes only on one line. It needs no permissions and no upload step, and Checks in CI uses it. Without --format, the output is the text one, in GitHub Actions too.
Checking one rule at a time
--code lets an agent, or a person, fix one rule across the project instead of working file by file. The summary line names the codes (3 with problems for code `ASC037` ), and the commands a report names keep them: the concise list’s and N more:, and the JSON’s next_command.
npx ascribe check --code link-id-missing --format concise
Applying the safe fixes
An unsafe fix, such as the nearest spelling of a misspelled name, is never applied by --fix: it stays in the report for you to decide. ascribe fmt is the other command that rewrites what it can.
npx ascribe check --fix
The output starts with a line for each file changed (fixed 2 problems in docs/guides/install.md). It changes only files git could put back, as every command that writes does: a page with changes not committed is refused, and named, unless you pass --force. With --dry-run, it says what it would fix (would fix 2 problems in docs/guides/install.md), changes nothing, and reports the check of the files as they are. Two fixes that would edit the same text are one too many: the first is applied, and the other’s problem is reported again, for the next run. --fix can’t be combined with --stdin, whose text isn’t on disk.
In VS Code, Source Action… → Apply the safe fixes in this page applies a page’s safe fixes the same way, to the text in the editor, saved or not, for the problems the editor shows, and can run each time you save. See Apply the safe fixes.
Failing on warnings
--exit-code=<level> says the least problem that fails the command: error, which --exit-code alone means and the command does without it, or warning, for CI. --deny-warnings is the same as --exit-code=warning, and when both are given, the last one counts. The level follows =, so ascribe check --exit-code docs/guides checks the folder. Advice never fails the command, so advice isn’t a level.
$ ascribe check --exit-code=warning
Exit codes
| Code | Meaning |
|---|---|
0 |
No errors. Warnings don’t fail the command unless you pass --exit-code=warning or --deny-warnings, and advice never does. |
1 |
There are errors, or warnings with --exit-code=warning or --deny-warnings. |
2 |
The project couldn’t be checked: a usage error, a --code no diagnostic has, no ascribe.toml, a content model with errors (they’re shown, and nothing else is checked), a path that doesn’t exist or isn’t in the project, an ascribe.baseline with a line that doesn’t parse (the error names it; --no-baseline checks without the file), or, with --fix, a file it fixed that can’t be written. A source file that can’t be read, or isn’t UTF-8, is a source-unreadable error in the list instead, and the rest of the project is still checked. |
Text output
Each diagnostic shows its code, message, and source, and its next step as Help:: the title of its first fix, or, with none, the fix advice the JSON gives as help. Help that’s the same for every diagnostic with a code is printed in full once; later diagnostics with that code point back to it, as Help: as for ASC252 above (ascribe explain ASC252). The output ends with a summary in one form: the files checked, the builds (for every build, or with --build, for build `cloud`), with paths, how many files they name (reported on 2 files in the paths), how many files have a problem reported, and the counts of errors, warnings, and advice (checked 12 files for every build, 1 with problems: 1 error, 0 warnings, 0 advice). The JSON’s files_reported counts the files reported on, all of them when no paths are given:
[ASC041] Warning: this looks like the published route of `install-agent.md`; link to the file instead: `/install-agent.md`
╭─[ docs/keys.md:8:15 ]
│
8 │ See [Install](/install-agent/).
│ ───────┬───────
│ ╰───────── link-route
│
│ Help: Link to the page's file instead of its route
───╯
checked 8 files for every build, 1 with problems: 0 errors, 1 warning, 0 adviceA project with no pages yet, and nothing to report, says so in place of the summary, with the content root: No pages in docs/ yet. It exits with 0. The JSON is the same as for any check, with files_checked 0.
Diagnostics go to standard output. A failure that stops the command (exit code 2) goes to standard error.
Advice, a diagnostic below a warning, is listed after the errors and warnings, in every format, and the summary counts it apart: 0 errors, 2 warnings, 5 advice. It never fails the command, even with --deny-warnings. A project sets how loudly a check about its content’s quality speaks in [checks].
A problem acknowledged as intended isn’t listed, and doesn’t fail the command; the summary counts it when there is one: 0 errors, 2 warnings, 0 advice, 1 acknowledged. ascribe build counts it the same way.
Advice the project’s baseline, ascribe.baseline, holds back isn’t listed either, and the summary counts it: 0 errors, 0 warnings, 2 advice, 3412 in the baseline, and the JSON’s summary as baselined. An entry of the baseline that matches nothing is reported, as baseline-entry-unmatched, when the check covers the whole project and every build. --no-baseline lists everything, without reading the file.
Quieting a problem
There are four ways to quiet a problem, each with its own rules:
| Way | What it quiets | For how long | Reason | Read by |
|---|---|---|---|---|
@intended, the intended frontmatter key, or [[intended]] |
One problem of a review check, at its place | For good | Required | Everything |
[checks] <check> = "off" |
A content check, in the whole project | Until it’s turned back on | None | Everything |
[checks] <check> = "advice" or "warning" |
Lowers a content check, in the whole project | Until it’s set again | None | Everything |
ascribe.baseline |
Many advice, each by its problem | Until it’s fixed and pruned | None | ascribe check, the editor, the hooks, and the MCP server; not ascribe build or ascribe report, for now |
- An error about whether the project is valid is never quieted, by any of the four: a broken link, an unclosed container, or an unknown attribute makes a page wrong, and is fixed instead. These are the specification’s diagnostics (SPEC §8.2), and none is configurable. The four are for the content checks, Ascribe’s own checks of the content’s quality.
- A content check raised to an error isn’t the same.
[checks]can raise a content check, such aspage-orphan = "error": its problems then failascribe checkand stopascribe build, as an error does, but the page is still valid by the specification. A review check raised this way can still be acknowledged with@intended, at whatever level the project gives it. - The baseline holds advice only. A check raised to a warning or an error is reported, whatever the baseline lists. To put a check’s warnings off, lower it to
"advice", then runascribe baseline writeagain.
Which to use: acknowledge a problem the team has looked at and means to keep, with why, beside it. Turn off or lower a check the project doesn’t need, which a reviewer sees in ascribe.toml. Put a backlog of advice in the baseline, to fix at the team’s own pace, so ascribe check reports only what’s new.
JSON output
--format json writes one JSON document to standard output, whatever the outcome, so a tool can always parse it. Its schema has every field, under the rules the JSON report contract sets for every document, such as ignoring fields you don’t know:
{
"schema_version": 1,
"ascribe_version": "0.4.4",
"error": null,
"files_checked": 8,
"files_reported": 8,
"builds_checked": [
"site",
"site@3.4",
"cloud"
],
"diagnostics": [
{
"code": "ASC041",
"slug": "link-route",
"severity": "warning",
"next": "choose",
"message": "this looks like the published route of `install-agent.md`; link to the file instead: `/install-agent.md`",
"file": "docs/keys.md",
"range": {
"start": {
"line": 8,
"column": 15,
"offset": 132
},
"end": {
"line": 8,
"column": 30,
"offset": 147
}
},
"related": [],
"fixes": [
{
"title": "Link to the page's file instead of its route",
"file": "docs/keys.md",
"edits": [
{
"range": {
"start": {
"line": 8,
"column": 15,
"offset": 132
},
"end": {
"line": 8,
"column": 30,
"offset": 147
}
},
"new_text": "/install-agent.md"
}
],
"applicability": "safe"
}
],
"builds": [],
"unpublished": false,
"help": "Link to the page's file, not its URL: Ascribe writes each output's URLs. The editor's quick fix makes the change.",
"docs": "https://ascribed-dev.com/go/ASC041/",
"repeats": 0
}
],
"truncated": false,
"shown": 1,
"total": 1,
"next_command": null,
"summary": {
"errors": 0,
"warnings": 1,
"advice": 0
}
}ascribe build
Checks the project, then writes each build’s outputs. The checks run first, exactly as ascribe check runs them. If any check finds an error, nothing is written, for any build.
--build <NAME>: Build only this build. Repeat it for several. By default, every build inascribe.toml.--emit <OUTPUTS>: Which outputs to write, separated by commas. All of them by default.site: Markdown plus web components, for an Astro site.plain: Fully resolved CommonMark with no HTML.json: The resolved tree as JSON.
--format <FORMAT>: How to show the checks’ results, as forascribe check.text(the default): Diagnostics with source snippets, for people.json: One JSON document, for tools.
--anchors: Mark each block of the site output with the source file and lines it came from, for review. Each Markdown block gets an<!--ascribe-anchor …-->comment before it, and each element Ascribe writes getsdata-ascribe-source(withdata-ascribe-viafor a block from a fragment). The site output’s manifest records"anchors": true. Without it, the output has no anchors. The other outputs are the same either way.--no-baseline: Report the adviceascribe.baselineholds back too, without reading the file. By default, the checks hold back what the baseline lists while it’s advice, and count it, asascribe checkdoes. A baseline that can’t be read stops the build, unless this is given.
The Astro integration turns the source anchors that --anchors writes into attributes on every block’s element (site-render contract).
The build prints each error in full, since errors stop it. Warnings and advice are only counted, in the check’s summary line, with the command that lists each: checked 8 files in every build: 0 errors, 1 warning, 3 advice: ascribe check for each, or checked 8 files in every build: no problems. The advice the baseline holds back isn’t counted with the rest, but after it, as ascribe check counts it: no problems, 3412 in the baseline. --format json prints exactly what ascribe check --format json does.
Then it prints one line for each build it wrote, with the outputs it wrote, on standard error, with its warnings:
built site: 6 pages, 2 assets (site, plain, json)
built cloud: 3 pages, 2 assets (site, plain, json)
Outputs
Each build writes each output to <output-dir>/<build>/<output>/, .ascribe/build/ being the default output directory, with a manifest beside it (<output>.manifest.json) listing every file it wrote. Every output is self-contained: images and other files that pages use are copied into it.
| Output | What it is | For |
|---|---|---|
site |
Markdown with web components (<ascribe-note>, <ascribe-tabs>, and the rest), heading ids and image attributes as markers the Astro integration applies, and the generated Zod schema in _ascribe/schema.ts |
An Astro site, through @ascribed/astro (Astro) |
plain |
Fully resolved CommonMark with no HTML: notes as quotes, variants as labeled sections, availability as text, and links as absolute URLs | Search indexes, LLMs, and export |
json |
The resolved tree of each page | Your own tools |
Without [consumer] site in ascribe.toml, plain-Markdown links are root-relative instead of absolute, and the build says so.
A rebuild replaces its previous output. It removes only files its own manifest listed, and never overwrites a file it didn’t write: a file in the way is an error. Two builds can’t write to one output directory at once.
Exit codes
| Code | Meaning |
|---|---|
0 |
Built |
1 |
The checks found errors; nothing was written |
2 |
The build couldn’t run: a usage error, no ascribe.toml, a content model with errors, an ascribe.baseline with a line that doesn’t parse (--no-baseline builds without the file), another build writing to the output directory, a file in the output directory that Ascribe didn’t write and would have to overwrite, or two pages of the site output with the same route |
ascribe preview
Shows a build’s pages as readers see them, with nothing but the ascribe command: no Node, no editor, and no Astro. It renders them into .ascribe/preview/, serves the folder on this computer, and renders it again as you edit.
It checks the build as ascribe check --build does, and prints the problems as that prints them. Then it renders every page into .ascribe/preview/ with the renderer ascribe versions export uses: each page is one HTML file with the element library’s styles, a sidebar of the build’s navigation, the page’s contents, the previous and next pages, and a search box. A page with errors is rendered as far as it goes: a link that leads nowhere is written as its text, and an image that isn’t there as its description.
Then it serves the folder at http://127.0.0.1:4880/, and prints the address to open:
checked 1 file for every build, 0 with problems: 0 errors, 0 warnings, 0 advice
rendered build `site`: 1 page, 0 assets, in .ascribe/preview
previewing build `site` at http://127.0.0.1:4880/
it renders again as the project's files change; Ctrl-C stops it
When a page, a fragment, ascribe.toml, or a file a snippet takes code from changes, it checks and renders the build again, and prints what it found; reload the page in the browser to see it. It runs until you stop it.
--build <NAME>: The build to preview. Needed whenascribe.tomlhas more than one build. Of a versioned build, its current line.--port <PORT>: The port to serve the preview on, at 127.0.0.1. By default 4880, or a free port the system picks when 4880 is taken.
The preview isn’t the published site, which looks the way its Astro project styles it (Astro), and isn’t the editor’s page preview or site preview, two of the four previews. Only this computer can open its address: it listens at 127.0.0.1 alone, answers only a request addressed to 127.0.0.1 or localhost with its port, so a web page elsewhere can’t read it, and connects to nothing. .ascribe/ is the folder .gitignore already leaves out.
| Code | Meaning |
|---|---|
2 |
It couldn’t start: no ascribe.toml, a content model with errors, a build that isn’t the model’s, several builds and no --build, a preview that can’t be written, or a port that can’t be listened on |
Once it’s serving, problems in the pages never stop it, and a content model that stops loading leaves the last preview served until it loads again.
ascribe fmt
Rewrites Ascribe constructs into canonical form: the spacing of directive lines and attribute blocks, attribute order, quoting, and blank lines between directives and their blocks. It changes nothing else, never how a page renders, and leaves alone a construct that has an error. See Canonical form.
It lists each file it changed. It changes only files git could put back, as every command that writes does: a page with changes not committed is refused, and named, unless you pass --force.
With --dry-run, it changes nothing and lists each change it would make: the file and line, then the lines as they are, marked -, and as they’d be, marked +:
docs/install.md:5
- @note{ type = tip }:Run it from the project root.
+ @note {type=tip}: Run it from the project root.
--check changes nothing too, writes the changes as a unified diff, which git apply applies, and exits with 1 when a file would change: for CI. --diff is its old spelling, kept for a release.
--check: Change nothing, and show what would change as a unified diff, whichgit applyapplies. Exits with 1 if any file would change. Use it in CI.--dry-run: Say what it would change, and change nothing.--force: Change files even when git couldn’t put them back as they are: ones with changes not committed, ones git doesn’t track, or ones in no repository.[PATHS]...: Files and directories to format: the.mdfiles among them. By default, every.mdfile under the content root. Directories whose names start with.,node_modules, and directories inside the searched ones that hold anascribe.tomlother than the project’s own (another project, formatted under its own model) are skipped. So is a file in the content root reached through a symbolic link that leads to a file that isn’t a source file of the content root, and it’s reported.--format <FORMAT>: How to show what changed.jsonlists each file with the edits that format it; with--check, that’s the edits without writing them.github, only with--check, is for GitHub Actions.text(the default): Short text, for people and agents to read.json: One JSON document, for tools.github: A GitHub Actions workflow command per file that would change, which shows it on the pull request at its first change, with the path from the repository’s root: for CI.
With --format json and --check, it writes the edits it would make and no file: what a tool that applies edits itself needs, such as the MCP server’s ascribe_fmt. An edit has the shape of a fix’s edit in ascribe check’s JSON, and the schema has every field.
With --format github and --check, it writes a GitHub Actions workflow command for each file that would change, at the line of its first change, with the path from the repository’s root, as ascribe check’s are. --format github without --check is a usage error.
::error file=handbook/docs/keys.md,line=28,title=ascribe fmt::`ascribe fmt` would change this file in 1 place; run `ascribe fmt` to format itA symbolic link it leaves alone is reported on standard error, as check reports it (source-unreadable), and the other files are still formatted.
| Code | Meaning |
|---|---|
0 |
Every file was formatted, or, with --check, none needed it; with --dry-run, whatever would change |
1 |
With --check: a file would change |
2 |
A problem: no ascribe.toml, a content model with errors, a path or file that can’t be read, a symbolic link that leads out of the content root, or a file git couldn’t put back, without --force |