turva-llms-txt-validator 0.3.16 → 0.3.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/package.json +1 -1
- package/src/index.mjs +35 -5
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ Completed validation returns `{ target, summary, checks }`. Each check contains
|
|
|
71
71
|
| 7 | File is small enough to read cheaply | none | Over 50 KB, or truncated at the 256 KB read limit |
|
|
72
72
|
| 8 | File contains no HTML markup | none | An opening tag with its closing `>`, a closing tag or an HTML comment is found anywhere in the file, including inside code spans and fenced code. An attribute name is any run of characters other than white space, quotes, `<`, `>`, `/` and `=` that holds a Unicode letter, a Unicode digit or the character `_`, so `<div @click="x">` warns. Autolinks such as `<https://example.com>` and prose such as `a <b + c` are not tags. A tag still open at the end of the file, such as `<div class="x"`, has no closing `>` and does not warn |
|
|
73
73
|
|
|
74
|
-
Links are read the way CommonMark reads them for the forms listed here. A link inside inline code, after an escaped bracket or in an image does not count, and a link in an ordered list, a target in angle brackets and a target with a title in double or single quotes do. A title in parentheses is not read, so `[Guide](https://example.org/guide (Overview))` is not collected. An unbracketed target ends at its first closing parenthesis, so `https://example.org/docs_(v2)` is read only up to `https://example.org/docs_(v2`. Write a target that contains parentheses in angle brackets. A link reference definition or label that spans several lines, and an image used as a reference, are not handled. A quote escaped with a backslash inside a link title, as in `[A](https://example.org/a "Ti\"tle\"")`, is not recognised, so that link is dropped. A title that spans a line break is dropped the same way. Nested brackets in a link label, as in `[A [x] B](https://example.org/a)`, drop the link. A heading is an H2
|
|
74
|
+
Links are read the way CommonMark reads them for the forms listed here. A link inside inline code, after an escaped bracket or in an image does not count, and a link in an ordered list, a target in angle brackets and a target with a title in double or single quotes do. A title in parentheses is not read, so `[Guide](https://example.org/guide (Overview))` is not collected. An unbracketed target ends at its first closing parenthesis, so `https://example.org/docs_(v2)` is read only up to `https://example.org/docs_(v2`. Write a target that contains parentheses in angle brackets. A link reference definition or label that spans several lines, and an image used as a reference, are not handled. A quote escaped with a backslash inside a link title, as in `[A](https://example.org/a "Ti\"tle\"")`, is not recognised, so that link is dropped. A title that spans a line break is dropped the same way. Nested brackets in a link label, as in `[A [x] B](https://example.org/a)`, drop the link. A heading is an H2 when `##` is followed by a space or a tab, or when it is one line of text underlined with hyphens. The text line must follow a blank line or the start of the file. A list item above a line of hyphens is not an H2, and neither is a line of hyphens after a blank line. A link in a four-space indented code block counts as a link, but such a block does not count as a file list under an H2. A backslash escape in a target is resolved before the URL test, so `https\://example.org/guide` is read as `https://example.org/guide`. A link label cannot span a blank line. A link on a line that continues a list item, such as the line after a bare `-`, counts as that item's link. After a bare marker the next line counts only when it is indented to the item's content column, which is 2 for `-` and 3 for `1.`, and a blank line right after a bare marker ends the item. After a blank line inside an item, a line counts only when it is indented to the content column, where the item's text starts, so `- item` needs 4 columns. A code fence indented less than that column ends the item. A fence indented to it keeps the item open, and the line after the fence then counts only when it is indented to that column too. CR and CRLF line endings are read as LF. A setext title, which is a text line underlined with `=`, is read only when it is a single line.
|
|
75
75
|
|
|
76
76
|
Some failures stop the file checks early. For example, an HTML response is reported as a failed plain-text check rather than parsed as Markdown.
|
|
77
77
|
|
|
@@ -132,6 +132,16 @@ The default validation requests two documents: `https://<host>/llms.txt` and `ht
|
|
|
132
132
|
|
|
133
133
|
For vulnerability reporting and supported versions, see [SECURITY.md](https://github.com/erekola/llms-txt-validator/blob/main/SECURITY.md).
|
|
134
134
|
|
|
135
|
+
## Verify a release
|
|
136
|
+
|
|
137
|
+
GitHub Actions publishes every version from 0.1.3 on with npm trusted publishing, and each one carries a provenance attestation. To check one, install it in an empty directory and ask npm to verify the signatures. Replace the version with the one you want to check.
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
mkdir verify-llms && cd verify-llms && npm init -y && npm install turva-llms-txt-validator@0.3.18 --ignore-scripts && npm audit signatures
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`npm audit signatures` checks the registry signature and the provenance attestation of each installed package that has one. The attestation of this package names three things to compare with what you expect: the repository `github.com/erekola/llms-txt-validator`, the workflow file `.github/workflows/publish.yml` and the commit that produced the tarball. The npm version page shows them under Provenance. Tag `v0.3.18` must point at that same commit, and `gh api repos/erekola/llms-txt-validator/commits/v0.3.17 --jq .sha` prints it. A mismatch is a reason not to use that version. Provenance proves where a release was built and from which commit. It does not prove that the code is safe, so reading the source and the dependencies stays your job.
|
|
144
|
+
|
|
135
145
|
## Hosted version and source
|
|
136
146
|
|
|
137
147
|
The [hosted validator](https://turva.dev/llms-txt-validator) accepts any public domain in the browser. It requests the same two documents, `/llms.txt` and the home page, and runs the same checks. Its source is in the [turva.dev Cloudflare Worker](https://github.com/erekola/turva-worker). The hosted validator is the canonical implementation: if results diverge, this package is updated to match it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turva-llms-txt-validator",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
4
4
|
"description": "Validate a site's llms.txt structure: eight checks as pass/warn/fail plus two llms.txt v2 discovery checks that read pass or info and never move the summary, JSON output. Same checks as the hosted validator at turva.dev/llms-txt-validator.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.mjs",
|
package/src/index.mjs
CHANGED
|
@@ -530,11 +530,38 @@ function decodeDestination(s) {
|
|
|
530
530
|
// starts a block of its own (heading, blockquote, list item, fence) is not paragraph text, so it
|
|
531
531
|
// never becomes a setext title. Only a one-line title is read; a title over several lines is not
|
|
532
532
|
// (Tek-560).
|
|
533
|
-
function
|
|
534
|
-
if (typeof next !== "string" || !/^ {0,3}=+[ \t]*$/.test(next)) return false;
|
|
533
|
+
function isSetextText(line) {
|
|
535
534
|
if (!/^ {0,3}\S/.test(line)) return false;
|
|
536
535
|
return !/^ {0,3}(?:#|>|[-*+](?:[ \t]|$)|\d{1,9}[.)](?:[ \t]|$)|`{3,}|~{3,})/.test(line);
|
|
537
536
|
}
|
|
537
|
+
function isSetextH1(line, next) {
|
|
538
|
+
if (typeof next !== "string" || !/^ {0,3}=+[ \t]*$/.test(next)) return false;
|
|
539
|
+
return isSetextText(line);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Which lines are H2 headings, outside fences. One reader for every place that asks "is this an
|
|
543
|
+
// H2": the section count, the section membership and the place where the late-summary scan stops.
|
|
544
|
+
// An ATX H2 is "##" followed by a space or a tab (a bare "##" is not one, and "###" is deeper). A
|
|
545
|
+
// setext H2 is one line of paragraph text, read by the same predicate as the setext H1 above,
|
|
546
|
+
// that has a blank line or the start of the file before it and a run of hyphens directly after it.
|
|
547
|
+
// A list item above "---", a thematic break and a line inside a fence are not H2 text. The
|
|
548
|
+
// underline is returned separately because it is not content (Tek-562, outside review W30).
|
|
549
|
+
function h2Lines(lines, fenced) {
|
|
550
|
+
const h2 = new Array(lines.length).fill(false);
|
|
551
|
+
const under = new Array(lines.length).fill(false);
|
|
552
|
+
for (let i = 0; i < lines.length; i++) {
|
|
553
|
+
if (fenced[i]) continue;
|
|
554
|
+
const l = lines[i];
|
|
555
|
+
if (/^ {0,3}##[ \t]/.test(l)) { h2[i] = true; continue; }
|
|
556
|
+
const next = lines[i + 1];
|
|
557
|
+
if (typeof next !== "string" || fenced[i + 1] || !/^ {0,3}-+[ \t]*$/.test(next)) continue;
|
|
558
|
+
if (!isSetextText(l) || /^ {0,3}([-*_])(?: *\1){2,} *$/.test(l)) continue;
|
|
559
|
+
if (i > 0 && lines[i - 1].trim() !== "") continue;
|
|
560
|
+
h2[i] = true;
|
|
561
|
+
under[i + 1] = true;
|
|
562
|
+
}
|
|
563
|
+
return { h2, under };
|
|
564
|
+
}
|
|
538
565
|
|
|
539
566
|
// The text of an ATX H1, or null when the line is not one. One to three leading spaces, a
|
|
540
567
|
// single "#", then a space, a tab or the end of the line. A closing run of "#" preceded by a
|
|
@@ -662,10 +689,11 @@ export function validateLlmsTxt(f) {
|
|
|
662
689
|
// v3.176.0). The status is warn either way, because the format places the summary directly
|
|
663
690
|
// after the title.
|
|
664
691
|
const fence = fenceMask(lines);
|
|
692
|
+
const lateH2 = h2Lines(lines, fence).h2;
|
|
665
693
|
let late = -1;
|
|
666
694
|
for (let i = h1End + 1; i < lines.length; i++) {
|
|
667
695
|
if (fence[i]) continue;
|
|
668
|
-
if (
|
|
696
|
+
if (lateH2[i]) break;
|
|
669
697
|
if (/^ {0,3}>[ \t]*\S/.test(lines[i])) { late = i; break; }
|
|
670
698
|
}
|
|
671
699
|
add("summary", "warn", "Blockquote summary after the title", late === -1
|
|
@@ -706,7 +734,8 @@ export function validateLlmsTxt(f) {
|
|
|
706
734
|
linkedLineIdx.add(idx);
|
|
707
735
|
}
|
|
708
736
|
}
|
|
709
|
-
const
|
|
737
|
+
const hm = h2Lines(lines, fenced);
|
|
738
|
+
const h2Count = hm.h2.filter(Boolean).length;
|
|
710
739
|
// A section counts when it carries a file list. An H2 followed by a paragraph satisfied
|
|
711
740
|
// this check until 2026-08-29, and the format puts each section's links in a list.
|
|
712
741
|
// A heading between the title and the first H2, or a second H1 anywhere, is out of place,
|
|
@@ -729,7 +758,8 @@ export function validateLlmsTxt(f) {
|
|
|
729
758
|
if (itemOpen) afterFence = true;
|
|
730
759
|
continue;
|
|
731
760
|
}
|
|
732
|
-
if (
|
|
761
|
+
if (hm.under[i]) continue;
|
|
762
|
+
if (hm.h2[i]) { inSection = true; counted = false; seenH2 = true; itemOpen = false; continue; }
|
|
733
763
|
const h1 = /^ {0,3}#(?:[ \t]|$)/.test(l);
|
|
734
764
|
if (misplaced === -1 && i > firstIdx && (h1 || (!seenH2 && /^ {0,3}#{3,6}(?:[ \t]|$)/.test(l)))) misplaced = i;
|
|
735
765
|
if (h1) { inSection = false; itemOpen = false; continue; }
|