turva-llms-txt-validator 0.3.17 → 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 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 only when `##` is followed by a space, so `##` followed by a tab is not read as one. 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.
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
 
@@ -137,10 +137,10 @@ For vulnerability reporting and supported versions, see [SECURITY.md](https://gi
137
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
138
 
139
139
  ```sh
140
- mkdir verify-llms && cd verify-llms && npm init -y && npm install turva-llms-txt-validator@0.3.17 --ignore-scripts && npm audit signatures
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
141
  ```
142
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.17` 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.
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
144
 
145
145
  ## Hosted version and source
146
146
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turva-llms-txt-validator",
3
- "version": "0.3.17",
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 isSetextH1(line, next) {
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 (/^ {0,3}## /.test(lines[i])) break;
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 h2Count = lines.filter((l, i) => !fenced[i] && /^ {0,3}## /.test(l)).length;
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 (/^ {0,3}## /.test(l)) { inSection = true; counted = false; seenH2 = true; itemOpen = false; continue; }
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; }