Spec index
- Check whether your corpus is indexed and current
- Browse the documents in a repository
- Open one document
- Build a table of contents
- Read a document's markdown
- Find every identifier the corpus declares
- Look up one identifier and everyone who cites it
- Find broken links, and everything that links here
- Read the findings from the last check
- Query typed rows across every document
- Pull the unfinished work out of the corpus
- List the endpoints your specs declare
- See which documents depend on which
- List every document across a group
- Check a whole group's corpus at a glance
- Make a broken corpus fail a merge request
Overview
Gitmoap reads the markdown you already keep in your repository — everything under specs/, docs/, legal/ and blog/ — and turns it into data you can ask questions of: a board of every document with its status, owner and review date; an index of every identifier your writing declares and everyone who cites it; backlinks; a list of links that go nowhere; and a rollup of every unchecked box in the corpus. Push a change and the index moves with it, so which rule says that, who depends on this document and what is still unfinished stop being questions you answer by grepping.
The same pass that builds the index checks the corpus, and it reports on every branch. A merge request whose documents cite a rule nothing declares, link to a file that is not there, claim an id another document already owns, or leave required frontmatter out gets a red check beside its tests, before it lands.
Nothing here writes to your documents. They stay in git, authored in an editor and reviewed as prose in a merge request; the index is a projection of them at one commit, and every operation on this page reads it.
Quick start
Index your first spec
Add a markdown file under
specs/. A header like this one makes it a checked document rather than only a browsable one:--- gsf: 1 id: quotas title: Quotas kind: domain status: draft --- # Quotas
Commit it and push it to the default branch. It is indexed once the push lands, and appears under Specs.
git add specs/ && git commit -m "Add the first spec" && git push origin main
Before you start
- A repository you can read.The index follows the repository's visibility exactly as its code does: a public repository answers anyone, an internal one answers anyone signed in, and a private one answers only members with Reporter or above. A private repository answers
404rather than403to any other signed-in caller, so that its existence stays private, and401to a caller with no credential at all. - Reporter on a private repository, and nothing more — every operation here is a read, and there is no level that grants more of the index than another.
- Reporter on a private group, for the two rollups that span one. The group itself is authorized first, and then each repository decides for itself: a group you can read contributes only the repositories you can read, and the counts you get back are the counts you are entitled to. Use your own credentials for those two — repository membership is held by people, so a service account sees only the public and internal repositories in the subtree however high its group access is.
- A token carrying
tree:read, if you are calling the API or the CLI rather than clicking. That is the same scope that reads the file tree and blobs, because indexed specs are repository content in a different shape; a token that can read your files can read what they say. - Markdown committed under
specs/,docs/,legal/orblog/.Those four directories are the whole of what gets indexed. Markdown anywhere else in the repository — changelogs, issue templates, vendored documentation — is left alone deliberately, because indexing it would answer questions nobody asked. A top-levelblog/is indexed too, so a static-site blog that keeps its posts there appears in the index and in spec search, and counts toward the whole-tree bound in Limits; a file that does not declaregsf: 1is indexed without being checked, so it adds no findings. - A push, or a merged merge request, that touches one of those files.The index is built by the push that changes the corpus. A repository whose specs have never changed since it was created gets its first index on the next push that touches one. Only branch pushes count: pushing a tag indexes nothing, and deleting a branch withdraws nothing, because the files still exist wherever they existed.
- The default branch, for the index to change.Every branch is parsed and reported on; only the default branch rewrites what the API serves.
- Frontmatter, if you want the checks.A document that opens with
gsf: 1is validated in full. One that does not is still indexed, still browsable and still searchable — it is simply never told it is wrong about a vocabulary it never adopted.
How it works
What a document turns into
Every document resolves into a document row, a tree of blocks, and three kinds of thing hanging off those blocks. Knowing which is which is most of knowing the API, because nearly every operation here reads one of them and says so in its name.
document specs/quotas.md id: quotas · kind: reference · status: implemented · reviewed 2026-08-15 └ block "## 6. Business Rules" anchor #6-business-rules · heading number "6", stored as data ├ record a table row kind errors · { status: 429, code: QUOTA_EXCEEDED, condition: … } ├ record an unchecked box "[ ] enforce the per-namespace cap" ├ id QUOTA-CAP-COUNTER declared here; cited in eleven other documents └ ref SEC-AUTH-FAIL-CLOSED an identifier citation, resolvedA document is one markdown file plus its frontmatter. A block is a heading and everything under it until the next heading of the same or shallower level. A record is a typed row inside a block — a table row whose column signature is recognised, or a checkbox item. An identifier is a name your writing declares, and a reference is a citation of one, or a link to a document, a section or a file in the repository. Records, identifiers and references each have their own listing, and none of them is a special case of another: asking the record listing for identifiers returns nothing.
Every one of those rows belongs to a corpus, and the row carries its corpus_id. A corpus is one set of documents with one history: your repository's specs/, docs/, legal/ and blog/ trees are a single corpus, not four, which is why an identifier declared under specs/ and cited under docs/ resolves rather than dangling. A repository's corpus takes the repository's own id, so if you were reading repository_id off a document row before, corpus_id in the same position holds the same value.
The distinction earns its keep as soon as something other than a repository has documents — a wiki, whose pages are markdown at a commit in a place that is not a repository, and which gets the same document rows, the same table of contents, the same backlinks and the same findings from the same index.
The heading number is stored and returned, and nothing is keyed on it: a block is identified by its normalised name with the number stripped, so renumbering ## 6. Business rules to ## 7. Business rules leaves it the same section in the index, with the same records and the same identifiers. Its anchor is a different matter — an anchor is slugged from the whole heading, number included, so renumbering does change it and does break a link built from the old one. That makes a cross-reference written as a bare section number the fragile kind: renumber, and it quietly lands on a different section, or on none — and only the second of those is caught, as a GSF_DANGLING_SECTION_REF warning whose message says a number is a position and positions drift. Link to a section by its anchor, or cite an identifier the section declares.
What a document declares
Frontmatter is a YAML mapping delimited by --- as the very first line of the file. Every key is optional except the three that a conformance claim requires.
| Key | Meaning |
|---|---|
gsf | The format version, and its presence is the claim to be checked. 1 is the accepted value |
id | The document's stable identity, lower-case and hyphenated, unique across the corpus. Required once you claim gsf: 1 |
kind | The document's shape. The default vocabulary accepts note, reference, index and hub; a richer one accepts more. A value outside the vocabulary reading the document is an error finding whose message lists what that vocabulary does accept. Required with a claim |
status | One of draft, review, approved, implemented, partial, superseded, living, none. Required with a claim |
title | Display title. Defaults to the H1 |
status_note | Free prose qualifying the status. Stored verbatim, never parsed |
profile | Which vocabulary reads this document. Inherited from the nearest hub above it, then generic |
owner | A username, or a group path — anything containing / is read as a path. Resolved against your organisation when the index is built; a name that matches nobody is kept and left unresolved |
created, last_reviewed | YYYY-MM-DD, and a date the calendar actually has |
parent, part_of | Lineage, by document id: what this sits under, and which hub's numbering it participates in |
depends_on | The upstream documents this one claims to depend on |
supersedes, superseded_by | Replacement, by document id |
tags | Free classification |
An unrecognised key is kept and reported as a warning rather than dropped, so a typo produces a finding instead of quietly deleting half your metadata. The two date keys are stricter: a value that is not a calendar date is an error finding and the value itself is dropped, because a date that is not a date cannot be stored as one — the rest of the document still indexes, and the finding names the file and the line. An all-capitals placeholder such as YYYY-MM-DD is read as a template rather than a mistake and produces nothing.
The identity is the id, not the path
Documents are listed, addressed and linked by id. Move a file into a subdirectory and keep its id and nothing breaks: the path is an attribute, and every identifier it declares, every backlink into it and every reference out of it stays where it was. Change the id and you have described a different document — the old one becomes an orphan and the new one starts empty.
A document that declares no id falls back to its path below specs/, docs/, legal/ or blog/ with the extension dropped and the slashes turned into hyphens, so specs/issues/roadmap.md is issues-roadmap and a folder's README.md takes the folder's own name. That fallback is a path, which means moving such a file is a rename. Declaring an id is what buys you the freedom to move it.
Deleting a file does not delete its row. The document is marked orphaned and keeps its identity, so an old link resolves to this moved rather than to nothing. Orphaned documents are left out of listings unless you ask for them, and the identifiers they declared are retained — an identifier is never released, or a rename performed as a delete and a re-add would silently free every name the document owned.
Every branch is checked; the default branch is what you read
┌── any branch ─────▶ parse ─▶ resolve ─▶ gitmoap/specs check on the commitpush, or a merged MR ───┤ └── default branch ─▶ parse ─▶ resolve ─▶ check ─▶ the index is rewrittenA push that changes no markdown under those four directories costs one file-list comparison and stops there, so the overwhelming majority of pushes do no work at all. A push that does change one parses the files that moved, resolves them against everything already indexed, and posts a gitmoap/specs commit status: green with the counts, red with the first failure and its file and line.
That check is the point of validating branches. A dangling identifier citation is invisible in a diff and nobody runs a linter over prose, so the feedback arrives where a reviewer is already looking.
A finding is about a document somebody edited; a dangling link is about the corpus
The two reports look similar and are not the same thing, and the difference will surprise you exactly once.
Findings come from parsing. They belong to the document that was read, and severity = error is what turns the commit status red.
Unresolved references come from resolution across the whole corpus. When you retire an identifier or rename a document, every citation of it anywhere becomes unresolved immediately and shows up in the dangling report — but the documents holding those citations were not re-read, so their own checks stay green until something edits them. That is deliberate: attributing a diagnostic to a file whose bytes nobody looked at would be a worse lie than the gap. Watch the dangling report after a rename; do not wait for a red check that belongs to someone else's next commit.
When the index is behind
Every rollup carries the commit it describes and whether anything is behind. A document is marked stale for one reason: a corpus over the file bound, walked as far as the bound, leaves everything past it holding whatever the previous pass recorded. That only arises on a pass that walks the whole tree — the push that creates a branch, or one whose diff against the commit before it cannot be worked out — because an ordinary push reads exactly the files that changed and knows the rest are unchanged. Staleness is recorded rather than inferred, and a later pass finishes the job.
How to
Check whether your corpus is indexed and current
Start here whenever an answer looks wrong. The rollup gives you the commit the index was built from, when that happened, whether anything is behind, how many documents are orphaned, how many references dangle, and the counts by kind, status, posture and severity — enough to tell "there are no findings" apart from "nothing has been indexed".
Open the repository and choose Specs in the side navigation. The banner across the top of the view names the indexed commit and the time it was built, and shows the document, block, record, identifier and reference counts beside a findings breakdown. A Behind the branch badge appears when any document is stale; hovering it names how many.
gitmoap specs get-spec-index-status --repo-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/specs/status \ -H "Authorization: Bearer $GITMOAP_TOKEN"An absent commit means nothing has ever been indexed in this repository — push a change to a markdown file under specs/, docs/, legal/ or blog/ on the default branch and it will be.
Browse the documents in a repository
The document listing is the board you would otherwise maintain by hand: every file with its title, kind, status, review date, owner, block and record counts, and how many findings it carries. It is ordered by document id and paged with a cursor, so a page boundary is stable while you walk it.
Combine the filters to answer the question you actually have. ?status=implemented&stale=true finds documents claiming to be finished that the last pass never reached. ?posture=untyped finds the files that opted out of checking. ?search=quota matches on title, summary or id. ?include_orphaned=true brings back the documents whose files are gone, which is how you find where something moved to.
Open the repository and choose Specs. Documents is the first tab: one row per file, with its title, kind, status pill, owner, last review date and finding count. The filter bar above the table carries a control per column, and the search box matches titles, summaries and ids. Rows for documents whose file has disappeared are hidden until you switch on Show orphaned, and appear struck through with the date they vanished.
gitmoap specs list-spec-docs --repo-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/specs \ -H "Authorization: Bearer $GITMOAP_TOKEN"Open one document
One call gets you the document's metadata, its full heading outline and up to a hundred of its findings. It does not include the prose — that is a separate read, so that a table of contents does not cost a document. A document carrying more than a hundred findings is unusual and worth reading through the findings listing instead, which pages.
Click any row in Documents. The document view opens with its frontmatter along the top, its outline down the left, and its findings — if it has any — pinned above the content with the line each refers to.
gitmoap specs get-spec-doc --repo-id <id> --doc-id <doc id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/specs/docs/{doc_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"Build a table of contents
When you want the outline and nothing else — a navigation sidebar, a link-checking script, a jump list — read the blocks on their own. Each block carries its level, its heading verbatim, its normalised name, its resolved kind, its line range, its first paragraph as a summary, and its anchor.
Use the anchor, never the heading number. The anchor is what a git host generates for the same heading, so a link built from it works in the web app and in a raw markdown view of the same file.
The outline in the left-hand column of the document view is this list. Each entry links to its anchor, and the numbers you see beside headings are read out of the heading text rather than counted, so they match the file exactly.
gitmoap specs list-spec-blocks --repo-id <id> --doc-id <doc id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/specs/docs/{doc_id}/blocks \ -H "Authorization: Bearer $GITMOAP_TOKEN"Read a document's markdown
The content read returns the file's prose with its frontmatter stripped, at the commit the index was built from — not at the branch tip. That pinning is a correctness property rather than a caching choice: records are addressed by their position in the document as it was parsed, so prose fetched at any other commit would put a highlight on the wrong table with nothing to notice.
The response carries the commit it came from and an entity tag, so a client that already has the document gets a 304 and a header exchange rather than the file. To read a document at any other ref, read the blob instead: that is the code browser's job, and it returns the file exactly as it is in git, frontmatter included.
The document view renders this content in its main column, with the typed rows the index extracted overlaid on the tables they came from. The commit it was read at is shown in the header beside a View in code browser link, which opens the same file with its frontmatter intact.
gitmoap specs get-spec-doc-content --repo-id <id> --doc-id <doc id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/specs/docs/{doc_id}/content \ -H "Authorization: Bearer $GITMOAP_TOKEN"Find every identifier the corpus declares
If your writing names things — rules, tenets, invariants, decision records — this is the index that makes them real. Every declaration comes back with its family, its title, the document and line that declares it, whether it is tombstoned, and how many times the corpus cites it. A rule with zero citations is visible as a rule nobody cites.
Filter by family with ?id_kind=rule, by state with ?tombstoned=true to see what has been retired or moved, and by substring with ?q=AUTH to find every identifier whose name contains it.
Open Specs, then the Identifiers tab. The table lists every declaration with its family, title, declaring document and citation count, sorted by family and then by identifier. The Retired toggle switches between live declarations and tombstoned ones, and the citation count links through to the citations themselves.
gitmoap specs list-spec-ids --repo-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/specs/ids \ -H "Authorization: Bearer $GITMOAP_TOKEN"Look up one identifier and everyone who cites it
Ask for an identifier by its family and its exact text. You get its title, where it is declared, whether it is tombstoned, its typed body where the family has one — a rule parses into its why, its how and what enforces it — and the list of citations with a document and line each.
The citation list is capped at 200 and says so with a flag when it is cut, because the identifier everybody cites is exactly the one whose page must stay bounded. Page the remainder through the reference listing filtered by target identifier.
A 404 here is itself a useful answer: it means nothing in the corpus declares that identifier, which is the mechanical form of is this rule real?
Click any row in Identifiers. The detail page shows the declaration's body in the three-part form its family uses, the file and line it lives at, and a Cited by list grouped by document with a line link each. When the citation list is truncated, a See all citations link opens the reference view already filtered to this identifier.
gitmoap specs get-spec-id \ --repo-id <id> \ --id-kind <id kind> \ --identifier <identifier>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/specs/ids/{id_kind}/{identifier} \ -H "Authorization: Bearer $GITMOAP_TOKEN"Find broken links, and everything that links here
Every cross-reference the corpus makes is stored with its raw text, its line, the form it took, and whether it resolved. That one table answers three different questions depending on how you filter it.
?resolved=falseis the dangling report — every link, path, section reference and identifier citation that points at nothing.?target_doc_id=<id>is backlinks — what links to this document.?target_id=<identifier>is who cites this rule, and the full list behind a truncated identifier page.
?ref_kind= narrows to one form: document, section, anchor, identifier, code or operation.
Open Specs, then the References tab. It opens on the dangling report, since that is the list worth acting on; the Resolved switch shows everything. The Links to picker turns the same view into backlinks for a document, and the form filter separates a broken file path from a broken identifier citation, which usually have different owners.
gitmoap specs list-spec-refs --repo-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/specs/refs \ -H "Authorization: Bearer $GITMOAP_TOKEN"Read the findings from the last check
Findings are the check's output, kept and queryable rather than scrolled past in a log. Each one carries a code, a severity, a message and — where the problem has a location — a line. Filter by ?severity=error to see only what turns the check red, by ?code= to work through one class at a time, or by ?doc_id= to see one file's.
Open Specs, then the Findings tab. Findings are grouped by document with a severity pill each, and the counts by severity are repeated in the header so you can see at a glance whether anything would fail a check. Clicking a finding opens its document at the line.
gitmoap specs list-spec-findings --repo-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/specs/findings \ -H "Authorization: Bearer $GITMOAP_TOKEN"Query typed rows across every document
This is the operation that pays for the whole feature. Tables in your documents that share a column signature are recognised as records of a kind, and one filtered read returns rows of that kind from every document at once. Every error status the corpus documents, every invariant, every parameter table, every unchecked box — the same call with a different kind.
?kind=errors&q=409 gets every documented 409 across the corpus. ?kind=checkbox&doc_id=<id> gets one document's checklist, in order. ?block_kind= restricts to rows inside a section of a given kind, so the parameter tables in Endpoints sections is expressible without naming a document.
Each row comes back with its recognised slots in core, everything else the table carried in extra, its position, and whether the row was struck through.
Open Specs, then the Records tab. Pick a record kind from the left-hand list — the count beside each is how many rows the corpus holds — and the table renders those rows with the recognised columns first. The search box matches across the recognised values, and each row links back to the table it came from in its own document.
gitmoap specs list-spec-records --repo-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/specs/records \ -H "Authorization: Bearer $GITMOAP_TOKEN"Pull the unfinished work out of the corpus
Every unchecked checkbox in every document, with its text, its document, its enclosing section and its line. A per-file convention becomes a backlog you can read in one place, and the same list narrowed with ?doc_id= is one document's remaining work.
Checked items are not returned — this list is what is outstanding, not a history of what was done.
Open Specs, then Open questions. Items are grouped by document, indented to their nesting, and each links to its line. The count in the tab is the corpus total, which is the number worth watching over a release.
gitmoap specs list-spec-open-questions --repo-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/specs/open-questions \ -H "Authorization: Bearer $GITMOAP_TOKEN"List the endpoints your specs declare
Where a heading names an operation as a backticked method and path, that declaration is extracted: method, path, the document and anchor that declare it, and the section's summary. It is the API surface as written down, which is what you need to compare against the API you actually serve.
The list says nothing about whether an endpoint is served. Whether a given path exists in a running service is a fact about your repository, not about your prose, and Gitmoap does not guess at it.
Open Specs, then the Endpoints tab. Declarations are grouped by document and listed in the order they appear in it, with the method shown as a coloured pill and the declaring section's summary beneath. The Document filter narrows to one file.
gitmoap specs list-spec-endpoints --repo-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/specs/endpoints \ -H "Authorization: Bearer $GITMOAP_TOKEN"See which documents depend on which
The graph is built from references that actually resolved, so it describes what your documents really cite rather than what they claim. Alongside it you get the depends_on each document declares, returned separately and never merged in — the disagreement between the two is the output. A declared dependency with no reference behind it is stale or aspirational; a document cited forty times and declared nowhere is a real dependency nobody wrote down.
Name a root to walk outward from one document, or omit it for the whole graph. depth clamps to six hops and defaults to six, so asking for forty gets you six rather than an error, and the response is capped at 500 nodes with a flag saying whether the cap was hit — past it you are looking at a sample in the shape of a graph, which is worth knowing before you draw conclusions from it.
Open Specs, then the Graph tab. The picture opens on the whole corpus; clicking a node re-roots the walk on it and the depth slider controls how far it reaches. Observed edges are drawn solid with their weight as thickness; declared dependencies are drawn as dashed edges, so an edge that is dashed and not solid is a claim nothing backs.
gitmoap specs get-spec-graph --repo-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/specs/graph \ -H "Authorization: Bearer $GITMOAP_TOKEN"List every document across a group
The group rollup spans every repository in a group's subtree, including nested groups, and returns the same document rows with the repository each belongs to. It takes the same filters as the repository listing, so every draft document in the whole organisation, sorted and paged is one call.
Rows you cannot see are removed inside the query rather than after it, so a page is never short because it was filtered, and the cursor stays honest.
Open the group and choose Specs. The document table looks like a repository's, with an extra Repository column and a repository filter beside the others. Only repositories you can read contribute rows.
gitmoap specs list-group-spec-docs --group-id <id>curl -X GET https://api.gitmoap.example/groups/{group_id}/specs \ -H "Authorization: Bearer $GITMOAP_TOKEN"Check a whole group's corpus at a glance
The estate rollup: how many repositories in the subtree have a corpus at all, the total documents, records and identifiers, how many documents are behind, and findings by severity. It is the number to put on a dashboard, and the one to watch after a large refactor.
Every count folds the same visibility rule the listing does. Withholding rows while publishing a total would still leak the existence of a private repository, so the totals are the totals you can see.
Open the group and choose Specs. The cards across the top of the view are this rollup — repositories with a corpus, documents, records, identifiers, and findings split by severity — and each card links through to the listing behind it.
gitmoap specs get-group-spec-status --group-id <id>curl -X GET https://api.gitmoap.example/groups/{group_id}/specs/status \ -H "Authorization: Bearer $GITMOAP_TOKEN"Make a broken corpus fail a merge request
There is nothing to switch on. Every push to a branch and every merge that touches the corpus is checked, and the result is posted to the commit as a status named gitmoap/specs — green with the counts, red with the first error, its file and its line. It appears in the merge request's checks list beside your build and your tests.
Findings of severity error are what turn it red; warnings are recorded, stay queryable, and leave the check green. A document that could not be parsed at all also turns it red, naming its own file. The status reports — it is a signal beside the merge request's other checks rather than a gate that refuses a merge on its own, so treat a red one the way your team treats a red build.
Note
A merge that Gitmoap performs on the server does not travel through a push, and the index is built from both — so a merge request that lands without anybody pushing still updates the index and still writes its check.
Reference
The vocabularies below are what the filters on these operations accept. Everything else in this section — paths, parameters, shapes, scopes and errors — is generated from the API itself.
Document statuses. draft, review, approved, implemented, partial, superseded, living, none. partial and living exist because real corpora need them: work that is half shipped, and documents that are never finished by design.
Postures. declared for a document carrying gsf: 1, whose frontmatter, identifiers, blocks and references are all checked. untyped for one that makes no claim: parsed, indexed, searchable, linkable, and never a finding. There is no third posture and no partial mode.
Identifier families. rule, tenet, principle, invariant, dogfooding and generic. Which shapes declare which family comes from the vocabulary reading the document; under the default vocabulary a heading that begins with an upper-case hyphenated token followed by an em dash — RFC-0012 — Retry policy — declares a generic identifier, and citations of it anywhere in the corpus resolve to it. Principles are document-scoped, since P1 means a different thing in every document that has one; every other family is unique across the whole corpus, which is what makes a duplicate declaration an error rather than a coincidence.
Reference forms. document, section, anchor, identifier, code and operation. A code reference is a backticked repository path — `app/server.ts` — checked against the tree at the indexed commit. A path that names another commit says so on the line with a marker: *(planned)* for one a later commit will carry, *(removed)* for one an earlier one did, *(moved)*, *(example)* for a path in somebody else's repository, and *(generated)* for a build artifact no commit contains. A marked path is still indexed and still appears in the graph; the marker suppresses the finding, not the reference. A backticked path whose first segment names no top-level entry of your repository is read as ordinary prose rather than as a reference at all, which is what keeps a shell fragment or a path in somebody else's project out of the dangling report without anyone marking it.
Record kinds. Twenty-eight named table shapes, plus checkbox for checklist items and unclassified for a table whose signature nothing claims. Under the default vocabulary three universal shapes are recognised — a section index, a parameter table (name, type, description, with optional required and default), and a two-column key/value table — and everything else is unclassified, which is a correct description rather than a defect. Richer vocabularies recognise more.
Finding codes. The ones you will meet. Severity error turns the check red; warning leaves it green:
| Code | Severity | What it means |
|---|---|---|
GSF_MISSING_REQUIRED_KEY | error | A document claiming gsf: 1 has no id, kind or status |
GSF_UNKNOWN_STATUS / GSF_UNKNOWN_KIND | error | A frontmatter value outside its vocabulary |
GSF_UNKNOWN_PROFILE | warning | A named vocabulary nothing provides; the document is read under the default one |
GSF_UNKNOWN_KEY | warning | An unrecognised frontmatter key, kept verbatim |
GSF_INVALID_DATE | error | created: or last_reviewed: is not a calendar date |
GSF_DUPLICATE_ID | error | Two documents claim one id, or two headings declare one identifier |
GSF_DANGLING_ID_REF | error | A cited identifier that nothing declares |
GSF_DANGLING_DOC_REF | error | A link or path to a document that does not exist |
GSF_DANGLING_SECTION_REF | warning | A section reference that lands nowhere |
GSF_DANGLING_CODE_REF | warning | A repository path absent from the commit and carrying no marker |
GSF_INCOMPLETE_RULE_BODY | warning | A rule declaration missing one of its body parts |
GSF_RAGGED_TABLE | warning | A table row with a different cell count from its header |
GSF_LEGACY_HEADER | warning | Metadata written as a blockquote under the H1 instead of frontmatter |
GSF_TRUNCATED | warning | A per-document bound was reached and the excess was dropped |
GSF_MALFORMED_FRONTMATTER | refusal | --- opened and never closed, frontmatter that is not valid YAML or not a mapping, or a gsf: that is not a whole number |
GSF_UNSUPPORTED_VERSION | refusal | A gsf: value higher than this platform reads |
GSF_TOO_LARGE | refusal | The document is over the per-file size bound |
The last three are the ones that behave differently, and it is worth knowing before you go looking. A refusal means the document could not be read at all, so it contributes no blocks, no records, no identifiers and no finding row — it reddens the check with its own file name and message, and the findings listing stays empty for it. A file that is not valid UTF-8 is refused the same way, as GSF_NOT_UTF8. Those four are the whole of the refusal set; everything above them is a finding on a document that still parses and still indexes. refusal is not a severity you can filter on, either: ?severity= takes error, warning or info and refuses anything else, because a refusal never produced a row to filter.
A red check does not hold the index back. A document with an error finding is still written, because the index is a projection of what the corpus says — including when what it says is wrong — and one refused file does not stop the others in the same push from indexing.
15 endpoints
GET/repositories/{repo_id}/specs/statustree:read
Summarise a repository's spec index — counts, findings and how current it is
Token scope
tree:read
Send
Path
- repo_iduuidThe repository whose corpus is indexed
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent
Receive
- blocksint64
- by_kindobject
- by_postureobject
- by_statusobject
- dangling_refsint64
- documentsint64
- findingsobject
- idsint64
- last_seen_shaobjectoptionalThe commit the index describes. Absent when nothing has ever been indexed.
- orphaned_documentsint64
- recordsint64
- refsint64
- stalebooleanTrue when any document is marked stale, so a client can tell the index is behind the branch.
- stale_documentsint64
- synced_atobjectoptional
GET/repositories/{repo_id}/specstree:read
List a repository's indexed spec documents with their computed status
Token scope
tree:read
Send
Path
- repo_iduuidThe repository whose corpus is indexed
Query
- kindstringoptionalFilter by document kind (open vocabulary: hub, domain, …)
- statusstringoptionalFilter by declared status
- posturestringoptionalFilter by conformance posture: declared or untyped
- profilestringoptionalFilter by the profile that read the document
- owner_group_idstringoptionalFilter by the group the frontmatter names as owner
- stalebooleanoptionalFilter by whether the latest indexing reached the document
- include_orphanedbooleanoptionalInclude documents whose file has disappeared (excluded by default)
- searchstringoptionalMatch the title, summary or a doc_id substring
- limitint32optionalPage size (1-100, default 20)
- next_tokenstringoptionalPagination cursor
Refuses
400Unknown filter value or malformed cursor401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent
Receive
items[]object24 fields
- block_countint32
- byte_lenint32
- corpus_iduuid
- createdobjectoptional
- doc_idstring
- finding_countint32
- gsf_versionint32optional
- kindobjectoptional
- last_reviewedobjectoptionalISO
YYYY-MM-DD, verbatim from the frontmatter. - last_seen_shastring
- orphaned_atobjectoptionalWhen the file stopped being present in the repository. The record is kept rather than deleted, so an existing link still resolves and tells you it moved.
- owner_group_idobjectoptional
- owner_principal_idobjectoptional
- parent_doc_idobjectoptional
- part_of_doc_idobjectoptional
- pathstring
- posturedeclareduntypedWhether a document claims to follow the spec format. A
declareddocument carriesgsf: 1in its frontmatter and is checked in full; anuntypedone made no claim and is never reported for not following it. - profilestring
- record_countint32
- staleboolean
- statusobjectoptional
- status_noteobjectoptional
- stemstring
- summaryobjectoptional
GET/repositories/{repo_id}/specs/docs/{doc_id}tree:read
Fetch one spec document with its block outline and findings
Token scope
tree:read
Send
Path
- repo_iduuidThe repository whose corpus is indexed
- doc_idstringThe document's frontmatter id, or its path-derived fallback
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent
Receive
blocks[]object12 fields
- anchorstring
- end_lineint32
- headingstring
- heading_numberobjectoptional
- kindstring
- levelint32
- markerobjectoptional
- namestring
- ordinalint32
- parent_ordinalint32optionalThe enclosing block's
ordinal, not its id — the tree is addressed the way it is read. - start_lineint32
- summaryobjectoptional
docobject12 fields
- block_countint32
- byte_lenint32
- corpus_iduuid
- createdobjectoptional
- doc_idstring
- finding_countint32
- gsf_versionint32optional
- kindobjectoptional
- last_reviewedobjectoptionalISO
YYYY-MM-DD, verbatim from the frontmatter. - last_seen_shastring
- orphaned_atobjectoptionalWhen the file stopped being present in the repository. The record is kept rather than deleted, so an existing link still resolves and tells you it moved.
- owner_group_idobjectoptional
GET/repositories/{repo_id}/specs/docs/{doc_id}/blockstree:read
List one document's headings, for a table of contents without the document payload
Token scope
tree:read
Send
Path
- repo_iduuidThe repository whose corpus is indexed
- doc_idstringThe document's id
Query
- limitint32optionalPage size (1-100, default 20)
- next_tokenstringoptionalPagination cursor
Refuses
400Malformed cursor401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent
Receive
items[]object12 fields
- anchorstring
- end_lineint32
- headingstring
- heading_numberobjectoptional
- kindstring
- levelint32
- markerobjectoptional
- namestring
- ordinalint32
- parent_ordinalint32optionalThe enclosing block's
ordinal, not its id — the tree is addressed the way it is read. - start_lineint32
- summaryobjectoptional
- next_tokenobjectoptional
GET/repositories/{repo_id}/specs/docs/{doc_id}/contenttree:read
Fetch a spec document's markdown at the commit the index was built from
get_spec_doc_contentGET /repositories/{repo_id}/specs/docs/{doc_id}/contentOpen in the API referenceToken scope
tree:read
Send
Path
- repo_iduuidThe repository whose corpus is indexed
- doc_idstringThe document's id
Refuses
304The index has not advanced since the caller's copy400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent409The document is not UTF-8 text
Receive
- contentstring
- doc_idstring
- pathstring
- shastring
GET/repositories/{repo_id}/specs/idstree:read
List identifier declarations with how often the corpus cites each
Token scope
tree:read
Send
Path
- repo_iduuidThe repository whose corpus is indexed
Query
- id_kindstringoptionalRestrict to one family: rule, tenet, principle, invariant, dogfooding, generic
- tombstonedbooleanoptionalSelect only retired/moved declarations, or only live ones
- qstringoptionalSubstring match on the identifier itself
- limitint32optionalPage size (1-100, default 20)
- next_tokenstringoptionalPagination cursor
Refuses
400Unknown identifier kind or malformed cursor401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent
Receive
items[]object9 fields
- anchorobjectoptional
- citationsint64
- doc_idobjectoptionalThe document declaring the identifier. Absent when that document has been removed; the identifier is kept and cannot be reused.
- idstring
- kinddogfoodinggenericinvariantprincipleruletenetWhich identifier family a declaration belongs to.
- lineint32optional
- scopestring
- titleobjectoptional
- tombstoneobjectoptional
- next_tokenobjectoptional
GET/repositories/{repo_id}/specs/ids/{id_kind}/{identifier}tree:read
Fetch one identifier declaration with its typed body and every citation
Token scope
tree:read
Send
Path
- repo_iduuidThe repository whose corpus is indexed
- id_kinddogfoodinggenericinvariantprincipleruletenetThe identifier family: rule, tenet, principle, invariant, dogfooding, generic
- identifierstringThe identifier itself, as the corpus spells it
Refuses
400Unknown identifier family401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent
Receive
- anchorobjectoptional
- bodyobject
{why, how, enforced_by}for a rule; shape follows the family. citations[]object3 fields
- anchorobjectoptional
- doc_idstring
- lineint32
- citations_truncatedbooleanTrue when the identifier is cited more than 200 times and
citationslists only the first 200. List the references withtarget_idset to see them all. - declared_at_shaobjectoptional
- doc_idobjectoptional
- idstring
- kinddogfoodinggenericinvariantprincipleruletenetWhich identifier family a declaration belongs to.
- scopestring
- titleobjectoptional
- tombstoneobjectoptional
GET/repositories/{repo_id}/specs/refstree:read
List cross-references, including backlinks and the dangling report
Token scope
tree:read
Send
Path
- repo_iduuidThe repository whose corpus is indexed
Query
- doc_idstringoptionalRestrict to references made by one document
- target_doc_idstringoptionalBacklinks: references that resolved to this document
- target_idstringoptionalWho cites this identifier
- ref_kindstringoptionalRestrict to one reference form: document, section, anchor, identifier, code, operation
- resolvedbooleanoptionalfalse is the dangling report
- limitint32optionalPage size (1-100, default 20)
- next_tokenstringoptionalPagination cursor
Refuses
400Unknown reference kind or malformed cursor401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent
Receive
items[]object12 fields
- anchorobjectoptional
- doc_idstring
- lineint32
- rawstring
- ref_kindanchorcodedocumentidentifieroperationsectionThe form a cross-reference took in the source.
- resolvedboolean
- target_anchorobjectoptional
- target_doc_idobjectoptional
- target_idobjectoptional
- target_methodobjectoptional
- target_pathobjectoptional
- target_sectionobjectoptional
- next_tokenobjectoptional
GET/repositories/{repo_id}/specs/findingstree:read
List the conformance findings from the latest indexing
Token scope
tree:read
Send
Path
- repo_iduuidThe repository whose corpus is indexed
Query
- severitystringoptionalRestrict to error, warning or info
- codestringoptionalRestrict to one finding code
- doc_idstringoptionalRestrict to one document
- limitint32optionalPage size (1-100, default 20)
- next_tokenstringoptionalPagination cursor
Refuses
400Unknown severity or malformed cursor401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent
Receive
items[]object5 fields
- codestring
- doc_idstring
- lineint32optional
- messagestring
- severityerrorinfowarningHow serious a finding is.
- next_tokenobjectoptional
GET/repositories/{repo_id}/specs/recordstree:read
List typed entries extracted from spec tables, filtered by record kind
Token scope
tree:read
Send
Path
- repo_iduuidThe repository whose corpus is indexed
Query
- kindstringoptionalRestrict to one record kind: errors, param, invariant, instrument, checkbox, …
- doc_idstringoptionalRestrict to one document
- block_kindstringoptionalRestrict to records inside a section of this kind
- qstringoptionalSubstring match across the recognised slot values
- limitint32optionalPage size (1-100, default 20)
- next_tokenstringoptionalPagination cursor
Refuses
400Unknown record kind, oversized query or malformed cursor401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent
Receive
items[]object11 fields
- anchorobjectoptional
- block_kindobjectoptional
- coreobject
- doc_idstring
- extraobject
- kindstring
- lineint32
- markerobjectoptional
- row_ordinalint32
- struckboolean
- table_ordinalint32optional
- next_tokenobjectoptional
GET/repositories/{repo_id}/specs/open-questionstree:read
List every unchecked implementation item across the corpus
Token scope
tree:read
Send
Path
- repo_iduuidThe repository whose corpus is indexed
Query
- doc_idstringoptionalRestrict to one document
- limitint32optionalPage size (1-100, default 20)
- next_tokenstringoptionalPagination cursor
Refuses
400Malformed cursor401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent
Receive
items[]object6 fields
- anchorobjectoptional
- block_nameobjectoptional
- depthint32
- doc_idstring
- lineint32
- textstring
- next_tokenobjectoptional
GET/repositories/{repo_id}/specs/endpointstree:read
List every endpoint a spec heading declares
Token scope
tree:read
Send
Path
- repo_iduuidThe repository whose corpus is indexed
Query
- doc_idstringoptionalRestrict to one document
- limitint32optionalPage size (1-100, default 20)
- next_tokenstringoptionalPagination cursor
Refuses
400Malformed cursor401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent
Receive
items[]object5 fields
- anchorstring
- doc_idstring
- methodstring
- pathstring
- summaryobjectoptional
- next_tokenobjectoptional
GET/repositories/{repo_id}/specs/graphtree:read
Walk the document dependency graph, observed edges beside declared ones
Token scope
tree:read
Send
Path
- repo_iduuidThe repository whose corpus is indexed
Query
- rootstringoptionalWalk outward from one document; omitted returns the whole graph, node-capped
- depthint32optionalHops to follow from the root (1-6, clamped)
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent
Receive
declared_edges[]object3 fields
- from_doc_idstring
- resolvedbooleanFalse when the named document is not in this repository's index.
- to_doc_idstring
- depthint32
nodes[]object4 fields
- depthint32Hops from the root, or
0for every node when no root was named. - doc_idstring
- kindobjectoptional
- titlestring
- depthint32Hops from the root, or
observed_edges[]object4 fields
- cites_idboolean
- from_doc_idstring
- to_doc_idstring
- weightint64
- rootobjectoptional
- truncatedbooleanTrue when the graph stopped at its limit of 500 documents, so it shows only part of the real graph.
GET/groups/{group_id}/specstree:read
List indexed spec documents across a group's subtree
Token scope
tree:read
Send
Path
- group_iduuidThe group whose subtree is rolled up
Query
- kindstringoptionalFilter by document kind
- statusstringoptionalFilter by declared status
- posturestringoptionalFilter by conformance posture
- profilestringoptionalFilter by the profile that read the document
- owner_group_idstringoptionalFilter by the group the frontmatter names as owner
- stalebooleanoptionalFilter by whether the latest indexing reached the document
- include_orphanedbooleanoptionalInclude documents whose file has disappeared
- searchstringoptionalMatch the title, summary or a doc_id substring
- limitint32optionalPage size (1-100, default 20)
- next_tokenstringoptionalPagination cursor
Refuses
400Unknown filter value or malformed cursor401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent
Receive
The same shape list_spec_docs returns — 1 field. Go to it
Show it here
items[]object24 fields
- block_countint32
- byte_lenint32
- corpus_iduuid
- createdobjectoptional
- doc_idstring
- finding_countint32
- gsf_versionint32optional
- kindobjectoptional
- last_reviewedobjectoptionalISO
YYYY-MM-DD, verbatim from the frontmatter. - last_seen_shastring
- orphaned_atobjectoptionalWhen the file stopped being present in the repository. The record is kept rather than deleted, so an existing link still resolves and tells you it moved.
- owner_group_idobjectoptional
- owner_principal_idobjectoptional
- parent_doc_idobjectoptional
- part_of_doc_idobjectoptional
- pathstring
- posturedeclareduntypedWhether a document claims to follow the spec format. A
declareddocument carriesgsf: 1in its frontmatter and is checked in full; anuntypedone made no claim and is never reported for not following it. - profilestring
- record_countint32
- staleboolean
- statusobjectoptional
- status_noteobjectoptional
- stemstring
- summaryobjectoptional
GET/groups/{group_id}/specs/statustree:read
Summarise spec coverage across a group's subtree
Token scope
tree:read
Send
Path
- group_iduuidThe group whose subtree is rolled up
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent
Receive
- documentsint64
- findingsobject
- idsint64
- recordsint64
- repositories_with_corpusint64
- stale_documentsint64
Limits
None of these operations creates anything, and none of them counts against a quota — no plan changes what you can read here. The bounds are on size, and they exist so that one enormous document cannot make an index unbuildable.
Two bounds refuse. A single markdown file over 512 KB is not indexed at all — the check goes red naming that file, and because a refused document produces no finding row, the red status is the only place you will see it. Past 1 MB the file is not read at all, so it does not even get that far: a document that grows over a megabyte drops out of the index the way a deleted one does, and is marked orphaned. Split the file well before either bound, or accept that it is browsable only in the code browser. A whole-tree pass reads at most 2,000 files, and every indexed document it did not reach is marked as behind for a later pass to finish; the truncation is reported rather than left for you to infer from a count.
Every other bound truncates, because a document with a 600-row table is unusual rather than invalid, and dropping it from the index over a formatting choice would be worse than keeping the first 512 rows. Four of those truncations record a GSF_TRUNCATED warning against the document so you find out; three cut quietly, and are worth knowing about precisely because nothing will tell you.
| Bound | Value | At the bound |
|---|---|---|
| Bytes in one document | 512 KB | The document is refused; the check names it, and no finding is recorded |
| Bytes read from one file | 1 MB | Past this the file is not read at all, and a document that grows over it is marked orphaned as a deleted one would be |
| Documents in one whole-tree pass | 2,000 | The walk stops; every document it did not reach is marked behind |
| Headings per document | 512 | The tail of the outline is dropped, with a finding |
| Tables per document | 256 | Later tables are not extracted, with a finding |
| Body rows per table | 512 | Later rows are not extracted, with a finding |
| Identifier declarations per document | 512 | Later declarations are not stored, with a finding |
| Columns per table | 16 | Later columns are dropped, and nothing says so |
| Bytes in one table cell | 4 KB | The cell is cut on a character boundary, and nothing says so |
| References scanned per document | 4,096 | Later references are not stored, and nothing says so |
| Characters kept as a block's summary | 500 | The summary is cut |
And the bounds on what a single response returns:
| Response bound | Value | Behaviour |
|---|---|---|
| Page size on any listing | 20 by default, 100 maximum | A larger limit clamps down rather than failing — ask for 500 and you get 100 |
| Graph depth | 6 hops | A larger depth clamps down |
| Graph nodes | 500 | The response says truncated, and what you have is a sample |
| Citations returned with an identifier | 200 | The response says citations_truncated; page the rest through the reference listing |
| Findings returned with a document | 100 | The document read stops there; page the rest through the findings listing filtered by document |
| Outline returned with a document | 512 | The same number as the heading bound above, so the document read always carries the whole outline |
| Characters in the record search filter | 256 | Refused with 400 rather than silently shortened, since a cut search returns rows you did not ask about |
Troubleshooting
400 BAD_REQUESTnaming a filter value
- Cause
- A closed filter —
status,posture,severity,ref_kind,id_kind, andkindon the record listing — was given a value outside its vocabulary, or the family segment in an identifier lookup's path was. Filters refuse rather than ignoring, so a typo cannot hand you an unfiltered page wearing a filter - What to do
- Check the value against the vocabularies in Reference. Note
proseis not a reference form: a prose mention is the resolver saying this was never a reference, so there is nothing to filter on. The document listing's ownkindandprofileare open sets and never refuse — an unmatched value there returns an empty page, which is the honest answer
400 BAD_REQUESTsaying it expected true or false
- Cause
stale,resolvedortombstonedwas given something other thantrue,false,1or0. All three are three-state, which is why they refuse rather than defaulting- What to do
- Send
trueorfalse. To ask for both, leave the parameter out altogether — an absent value, and an empty one, are what mean no filter
400 BAD_REQUESTon the record search
- Cause
- The search string is over 256 characters
- What to do
- Shorten it. A long substring match is almost always better expressed as a
kindfilter plus a short search
400 BAD_REQUESTmentioning repo_id or owner_group_id
- Cause
- An id in the URL or the query is not a well-formed identifier — usually a path like
my-group/my-repopasted where an id belongs - What to do
- Use the resource's id, or look it up by path first
400 BAD_REQUESTnaming the pagination cursor
- Cause
- The
next_tokenwas edited, shortened, or copied from a listing whose cursor is a different shape — the outline's is not the records listing's - What to do
- Send
next_tokenback exactly as it came, and never build or trim one by hand. Changing a filter part-way needs a fresh first page too: a cursor is a position in the result set that produced it, so reusing it under new filters resumes somewhere arbitrary rather than failing
?limit=500 returns 100 rows and no error
- Cause
- Page size clamps down rather than refusing, because a client asking for 500 wants the maximum
- What to do
- Page with the cursor. Nothing is missing — carry on with
next_token
401 UNAUTHORIZED
- Cause
- No credential, or an expired one, against a repository that is not public
- What to do
- Sign in, or refresh the token. A public repository answers these reads with no credential at all
403 FORBIDDEN
- Cause
- Your token is bound to one repository or one group subtree and you pointed it at a different one, or its binding names a resource that has since been deleted. Binding is checked before visibility, so a bound token cannot read even a public repository outside its scope
- What to do
- Check what the token is bound to in your token list, then use one bound to this resource or an unbound one. A membership too low to read never lands here — that answers
404instead, so a403is always about the token
403 INSUFFICIENT_SCOPE
- Cause
- Your token is valid but was minted without
tree:read - What to do
- Mint a new token carrying
tree:read. Scopes are fixed when a token is created and cannot be added afterwards
404 NOT_FOUNDon a repository you know exists
- Cause
- It is private and you are not a member. A private repository answers
404to non-members so its existence does not leak - What to do
- Ask for Reporter on it, then retry. If you are already a member, check the id
404 NOT_FOUNDfor a document that is definitely in the tree
- Cause
- It has never been indexed — the file is outside
specs/,docs/,legal/andblog/, does not end in.mdor.markdown, is over the size bounds, or has only ever been pushed to a branch that is not the default — or you are asking by path rather than by id - What to do
- Read the document listing and take the
doc_idfrom it. If the listing is empty too, push the file to the default branch and check its size against Limits
404 NOT_FOUNDreading the markdown of a document that still opens
- Cause
- Its file was deleted or its
idchanged, so the row is orphaned. The document itself still answers and carries the date its file disappeared; there is simply no prose left to serve - What to do
- Ask the listing for orphaned documents to find where the content went, then read the new id. Read the old file at an older commit through the code browser if you need the text itself
404 NOT_FOUNDfor an identifier you can see in a file
- Cause
- The identifier's declaration does not match the shape its family requires, or you asked under the wrong family segment, or the spelling differs — identifiers are matched exactly
- What to do
- Find it in the identifier listing with a substring search, then use the family and text it reports verbatim
404 NOT_FOUNDfor a principle such as P1
- Cause
- Principles are document-scoped, so the identifier is the document's id, a
#, then the principle —rust-conventions#P1, notP1. Every other family is corpus-wide and is asked for bare - What to do
- Take the exact text from the identifier listing and percent-encode the
#as%23in the path
409 CONFLICTreading a document's content
- Cause
- The file is not UTF-8 text — usually a stray encoding, occasionally a binary file with a
.mdname - What to do
- Re-save the file as UTF-8 and push. Until then, read it through the code browser, which hands a non-text blob back base64-encoded rather than refusing
A 304 instead of the document
- Cause
- Your client sent an entity tag and the index has not moved since
- What to do
- Nothing to do — use the copy you have. It is still the document at the indexed commit
Nothing indexed after a push
- Cause
- The push touched no markdown under
specs/,docs/,legal/orblog/, it went to a branch that is not the default, or it was a tag push or a branch deletion — neither of which changes the index - What to do
- Push the change to the default branch. Pushes to other branches are checked and reported, and deliberately do not rewrite what the API serves
The check went red with GSF_DUPLICATE_ID
- Cause
- Two documents claim one
id, or two headings declare one identifier. Identity is unique across the corpus and always has been — this is the first thing that noticed - What to do
- Change one of the two, or, where a document is deliberately restating an identifier another one owns, mark the restating heading
*(restated)*so it counts as a citation with a body rather than a second declaration
The check is red but the findings list is empty
- Cause
- A document could not be parsed at all — over the per-file size bound, frontmatter that opened and never closed, a
gsf:version above the one this platform reads, or a file that is not UTF-8. A refused document produces no finding row, only the red status - What to do
- Read the status message: it names the file and what was wrong with it. Fix that one file and push; every other document in the corpus indexed normally
A document indexed, but its created or last_reviewed came back empty
- Cause
- The value is not a
YYYY-MM-DDcalendar date, so it was dropped rather than stored, andGSF_INVALID_DATEwas recorded against the file - What to do
- Fix the date and push. Filter the findings by
GSF_INVALID_DATEto find every other file with the same problem in one pass
A document is marked stale, or the rollup says the index is behind
- Cause
- A whole-tree pass walked as far as the 2,000-file bound and stopped, leaving everything past it holding what an earlier pass recorded
- What to do
- Compare the document count against the 2,000-file bound. Under it, push any change to the corpus and the next pass catches up; over it, split the corpus or move the documents you do not need indexed out of the four indexed directories
The dangling report grew but every check is green
- Cause
- Somebody retired an identifier or renamed a document, which broke citations in files their commit never touched. Those documents were not re-read, so no finding was attributed to them
- What to do
- Work the dangling report directly; it is the live view. The citing documents redden their own checks the next time somebody edits them
A code path is reported dangling though the file is plainly there
- Cause
- The path is not in the commit — a build output kept out by
.gitignoreis the common case, and a path that names a commit other than this one is the other - What to do
- Mark the line:
*(generated)*for a build artifact,*(planned)*,*(removed)*,*(moved)*or*(example)*for the others. The reference stays indexed; the finding goes away
A table comes back as unclassified
- Cause
- Its column signature matches no shape the vocabulary reading that document recognises
- What to do
- Rename the columns to a recognised signature, or query it as
unclassified— the rows are all there, just unnamed. An unclassified table is a correct description, not a fault
The identifier index is empty in a repository that is full of rules
- Cause
- The declarations do not match a shape any family recognises — under the default vocabulary that is a heading beginning with two or more upper-case or numeric segments joined by hyphens, then a separator, then the title
- What to do
- Reformat one heading to
ABC-SLUG — Titleand push; if it appears, the shape was the problem. A double hyphen surrounded by spaces works as the separator too, if the em dash is awkward to type
The graph is missing edges you know exist
- Cause
- The node cap was reached and the response says
truncated, or the references in question have not resolved - What to do
- Re-root the walk on the document you care about and reduce the depth. A truncated graph is a sample, and reading it as complete is the mistake the flag exists to prevent
An identifier's citation list looks short
- Cause
- It is capped at 200 and the response says
citations_truncated - What to do
- Page the full list through the reference listing filtered by target identifier
FAQ
Does this slow my pushes down?
No. A push whose changed files include no markdown under those four directories does one file-list comparison and stops. A push that does touch the corpus reads only the files that changed and re-checks only the references whose answer could have moved. Retiring a name that thousands of citations point at is the one exception: the re-check widens to the whole corpus, and the green status line says so rather than hiding it.
Can I turn it off?
There is no switch, and there is nothing to switch off: a repository with no markdown under those two directories has no corpus and gets no check. A red check reports; it does not refuse a merge by itself, so a corpus you are mid-way through cleaning up costs you a red status and nothing else.
What happens to a document I delete?
Its row is kept and marked orphaned, its records, references and findings are removed, and the identifiers it declared are retained. Listings hide it unless you ask for orphaned documents, and its id keeps resolving so that old links say this moved instead of failing.
Can I use my own vocabulary?
Documents are read under the default vocabulary unless the document names one in profile:, or the nearest kind: hub document above it in the tree does. That default is deliberately not empty — it recognises section-index, parameter and key/value tables, and it declares an identifier family for upper-case hyphenated headings — so a corpus that names things gets an identifier index, citation resolution and dangling-citation detection having declared nothing at all. Naming a vocabulary nothing provides falls back to the default with a warning rather than failing.
Why is the document's markdown pinned to one commit?
Because the typed rows are addressed by where they sat in the document when it was parsed. Serving prose from a different commit would line those rows up against tables that have moved, and the answer would be wrong in the shape of a correct one. Read any other ref through the code browser.
Related
- Repositoriesvisibility, and who can read what: every operation here inherits those rules exactly. Also the code browser, for reading any file at any ref with its frontmatter intact
- Repository rulescommit statuses, which is what the
gitmoap/specscheck is, and protecting the default branch the index is built from - Merge requestswhere the check appears, beside your build and your tests
- Searchindexed documents are searchable alongside work items, merge requests, releases, comments and services
- Access tokensminting a token with
tree:read, and why scopes are fixed when a token is created