diff --git a/.gitignore b/.gitignore index 79d44c9..c6eee10 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ site/public/ site/resources/ -site/content/spec/_index.md +site/content/spec/web/ site/build_and_sync .DS_Store diff --git a/.vale.ini b/.vale.ini new file mode 100644 index 0000000..9ba6f35 --- /dev/null +++ b/.vale.ini @@ -0,0 +1,5 @@ +StylesPath = .vale/styles +MinAlertLevel = suggestion + +[*.md] +BasedOnStyles = DC diff --git a/.vale/styles/DC/AICompoundPhrases.yml b/.vale/styles/DC/AICompoundPhrases.yml new file mode 100644 index 0000000..f8f268c --- /dev/null +++ b/.vale/styles/DC/AICompoundPhrases.yml @@ -0,0 +1,139 @@ +# Adapted from tbhb/vale-ai-tells (MIT). See THIRD_PARTY_NOTICES.md. +extends: existence +message: "AI fingerprint: '%s'. Rewrite with concrete specifics instead of this clichéd phrase." +level: error +ignorecase: true +tokens: + - rich tapestry + - rich tapestries + - tapestry of + - intricate tapestry + - complex and multifaceted + - multifaceted and complex + - intricate interplay + - delicate balance + - delicate interplay + - dynamic interplay + - crucial role + - played a crucial role + - plays a crucial role + - vital role + - pivotal role + - key role + - instrumental role + - cutting-edge + - game-changer + - game-changing + - paradigm shift + - sea change + - tipping point + - watershed moment + - double-edged sword + - slippery slope + - tip of the iceberg + - at the forefront + - on the cutting edge + - paving the way + - laid the groundwork + - lays the groundwork + - sets the stage + - set the stage + - a testament to + - stands as a testament + - serve as a reminder + - serves as a reminder + - it remains to be seen + - only time will tell + - the jury is still out + - takes center stage + - take center stage + - took center stage + - paints a picture of + - paint a picture of + - painted a picture of + - is not without its challenges + - are not without their challenges + - is not without challenges + - whether we like it or not + - whether you like it or not + - a cornerstone of + - the transformative power of + - deeply rooted in + - deeply rooted + - the hallmark of + - a hallmark of + - moving the needle + - moves the needle + - moved the needle + - move the needle + - nudge the needle + - nudges the needle + - nudged the needle + - unlocks new + - unlocked new + - unlocking new + - unlock new + - unlocks the potential + - unlock the potential + - unlocks the power + - unlock the power + - unlocks the value + - unlock the value + - unlocks capabilities + - unlock capabilities + - unlocks possibilities + - unlock possibilities + + # "real" as intensifier before an abstract noun. + # AI reaches for "a real choice" / "a real difference" to inflate + # weight where a plain noun would do. Bare "real" is too broad + # (real-time, real estate, real world), so this is enumerated. + - a real choice + - a real option + - a real difference + - a real test + - a real question + - a real chance + - a real problem + - a real shot + - a real risk + - a real cost + - a real benefit + - real value + - real progress + - real flexibility + - real benefits + - real costs + - real impact + - real leverage + - real ownership + - real accountability + - real consequences + - real tradeoffs + - real trade-offs + - real choices + - real options + - real differences + + # Investigation-thread metaphors: AI's preferred way to describe + # "open questions" or "loose ends." Tutorial-blog tone. + - threads to pull on + - thread to pull on + - threads to pull + - thread to pull + - loose threads to pull + - loose threads to chase + - another thread to pull + - more threads to pull + + # Solidification metaphors: the surface/design/contract "hardens" + # before some deadline. AI reaches for this constantly when + # describing in-progress work or pre-release windows. + # Broad regex catches "before/after X hardens" for any 1-6 + # word X. + - (?:before|after) [^.,;:!?]{1,40} hardens + - (?:before|after) [^.,;:!?]{1,40} hardened + - (?:before|after) [^.,;:!?]{1,40} solidifies + - (?:before|after) [^.,;:!?]{1,40} solidified + - (?:before|after) [^.,;:!?]{1,40} ossifies + - (?:before|after) [^.,;:!?]{1,40} ossified diff --git a/.vale/styles/DC/AdverbDensity.yml b/.vale/styles/DC/AdverbDensity.yml new file mode 100644 index 0000000..7fa9452 --- /dev/null +++ b/.vale/styles/DC/AdverbDensity.yml @@ -0,0 +1,32 @@ +# Original rule (not from vale-ai-tells). Flags every non-exception '-ly' +# word, but only in files whose '-ly' density exceeds 2% of word count +# (min 100 words). scope: raw keeps positions accurate; the cost is that +# code blocks and frontmatter count toward the word total. +extends: script +message: "Adverb-heavy file (over 2%% '-ly' words). Cut this one or its neighbors." +level: warning +scope: raw +script: | + text := import("text") + matches := [] + exceptions := {only: true, family: true, likely: true, early: true, + friendly: true, + assembly: true, apply: true, reply: true, supply: true, july: true, + italy: true, daily: true, weekly: true, monthly: true, ugly: true, + fly: true, rely: true, imply: true, multiply: true, ally: true, + rally: true, tally: true, anomaly: true, belly: true, silly: true} + words := text.re_find(`[A-Za-z]+`, scope, -1) + total := 0 + candidates := [] + for w in words { + total += 1 + lower := text.to_lower(w[0].text) + if text.has_suffix(lower, "ly") && !exceptions[lower] { + candidates = append(candidates, w[0]) + } + } + if total >= 100 && len(candidates) * 100 > total * 2 { + for c in candidates { + matches = append(matches, {begin: c.begin, end: c.begin + len(c.text)}) + } + } diff --git a/.vale/styles/DC/ContrastiveFormulas.yml b/.vale/styles/DC/ContrastiveFormulas.yml new file mode 100644 index 0000000..3fddc91 --- /dev/null +++ b/.vale/styles/DC/ContrastiveFormulas.yml @@ -0,0 +1,192 @@ +# Adapted from tbhb/vale-ai-tells (MIT). See THIRD_PARTY_NOTICES.md. +extends: existence +message: "AI contrast: '%s'. State the positive claim directly without the 'not X, but Y' formula." +level: error +tokens: + # "It's not just X; it's Y" patterns + - "[Ii]t'?s not just [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Ii]t isn'?t just [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Ii]t'?s not merely [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Ii]t'?s not simply [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Ii]t'?s not only [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Ii]t'?s not purely [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Ii]t'?s not solely [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Ii]t'?s not entirely [^;,—:]+[;,—:] it'?s [^.!?]+" + + # "It's not about X; it's about Y" patterns + - "[Ii]t'?s not about [^;,—:]+[;,—:] it'?s about [^.!?]+" + - "[Ii]t isn'?t about [^;,—:]+[;,—:] it'?s about [^.!?]+" + - "[Ii]t'?s not really about [^;,—:]+[;,—:] it'?s about [^.!?]+" + - "[Ii]t'?s not just about [^;,—:]+[;,—:] it'?s about [^.!?]+" + - "[Ii]t'?s not merely about [^;,—:]+[;,—:] it'?s about [^.!?]+" + - "[Ii]t'?s not simply about [^;,—:]+[;,—:] it'?s about [^.!?]+" + - "[Ii]t'?s not only about [^;,—:]+[;,—:] it'?s about [^.!?]+" + - "[Ii]t'?s not purely about [^;,—:]+[;,—:] it'?s about [^.!?]+" + - "[Ii]t'?s not solely about [^;,—:]+[;,—:] it'?s about [^.!?]+" + - "[Ii]t'?s not entirely about [^;,—:]+[;,—:] it'?s about [^.!?]+" + # Period-separated versions (two sentences) + - "[Ii]t'?s not about [^.!?]+\\. [Ii]t'?s about [^.!?]+" + - "[Ii]t isn'?t about [^.!?]+\\. [Ii]t'?s about [^.!?]+" + - "[Ii]t'?s not really about [^.!?]+\\. [Ii]t'?s about [^.!?]+" + - "[Ii]t'?s not just about [^.!?]+\\. [Ii]t'?s about [^.!?]+" + - "[Ii]t'?s not merely about [^.!?]+\\. [Ii]t'?s about [^.!?]+" + - "[Ii]t'?s not purely about [^.!?]+\\. [Ii]t'?s about [^.!?]+" + - "[Ii]t'?s not solely about [^.!?]+\\. [Ii]t'?s about [^.!?]+" + - "[Ii]t'?s not entirely about [^.!?]+\\. [Ii]t'?s about [^.!?]+" + + # "This isn't X; it's Y" patterns + - "[Tt]his isn'?t just [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]his isn'?t merely [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]his isn'?t simply [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]his isn'?t only [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]his isn'?t purely [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]his isn'?t solely [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]his isn'?t entirely [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]his is not just [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]his is not merely [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]his is not simply [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]his is not only [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]his is not purely [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]his is not solely [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]his is not entirely [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]his isn'?t merely about [^;,—:]+[;,—:] it'?s about [^.!?]+" + - "[Tt]his isn'?t just about [^;,—:]+[;,—:] it'?s about [^.!?]+" + - "[Tt]his isn'?t simply about [^;,—:]+[;,—:] it'?s about [^.!?]+" + - "[Tt]his isn'?t only about [^;,—:]+[;,—:] it'?s about [^.!?]+" + - "[Tt]his isn'?t purely about [^;,—:]+[;,—:] it'?s about [^.!?]+" + - "[Tt]his isn'?t solely about [^;,—:]+[;,—:] it'?s about [^.!?]+" + - "[Tt]his isn'?t entirely about [^;,—:]+[;,—:] it'?s about [^.!?]+" + - "[Tt]his isn'?t about [^;,—:]+[;,—:] it'?s about [^.!?]+" + # Period-separated versions (two sentences) + - "[Tt]his isn'?t about [^.!?]+\\. [Ii]t'?s about [^.!?]+" + - "[Tt]his isn'?t merely about [^.!?]+\\. [Ii]t'?s about [^.!?]+" + - "[Tt]his isn'?t just about [^.!?]+\\. [Ii]t'?s about [^.!?]+" + - "[Tt]his isn'?t simply about [^.!?]+\\. [Ii]t'?s about [^.!?]+" + - "[Tt]his isn'?t only about [^.!?]+\\. [Ii]t'?s about [^.!?]+" + - "[Tt]his isn'?t purely about [^.!?]+\\. [Ii]t'?s about [^.!?]+" + - "[Tt]his isn'?t solely about [^.!?]+\\. [Ii]t'?s about [^.!?]+" + - "[Tt]his isn'?t entirely about [^.!?]+\\. [Ii]t'?s about [^.!?]+" + - "[Tt]his is not about [^.!?]+\\. [Ii]t'?s about [^.!?]+" + + # "It's more than X; it's Y" patterns + - "[Ii]t'?s more than [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Ii]t'?s more than just [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]his is more than [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]his is more than just [^;,—:]+[;,—:] it'?s [^.!?]+" + + # "It's less about X and more about Y" patterns + - "[Ii]t'?s less about [^;,—:]+ and more about [^.!?]+" + - "[Ii]t'?s less about [^;,—:]+, more about [^.!?]+" + - "[Tt]his is less about [^;,—:]+ and more about [^.!?]+" + + # "X isn't the point; Y is" patterns + - "[^.!?]+ isn'?t the point[;,—:] [^.!?]+ is" + - "[^.!?]+ is not the point[;,—:] [^.!?]+ is" + + # "The real X isn't Y; it's Z" patterns + - "[Tt]he real [^;,—:]+ isn'?t [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]he true [^;,—:]+ isn'?t [^;,—:]+[;,—:] it'?s [^.!?]+" + + # "What matters isn't X; it's Y" patterns + - "[Ww]hat matters isn'?t [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Ww]hat matters is not [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Ww]hat'?s important isn'?t [^;,—:]+[;,—:] it'?s [^.!?]+" + + # "The question isn't X; it's Y" patterns + - "[Tt]he question isn'?t [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]he question is not [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]he issue isn'?t [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]he problem isn'?t [^;,—:]+[;,—:] it'?s [^.!?]+" + - "[Tt]he challenge isn'?t [^;,—:]+[;,—:] it'?s [^.!?]+" + + # "Yes, X, but Y" dismissive acknowledgment + - "[Yy]es, [^,]+, but the real [^.!?]+" + - "[Ss]ure, [^,]+, but what really [^.!?]+" + + # "It's not whether X; it's how/why/when Y" + - "[Ii]t'?s not whether [^;,—:]+[;,—:] it'?s (?:how|why|when|what) [^.!?]+" + - "[Ii]t'?s not if [^;,—:]+[;,—:] it'?s (?:how|why|when|what) [^.!?]+" + + # "[Noun] isn't X. It's Y." pattern (any subject) + - "[A-Z][a-z]+ isn'?t [^.!?]+\\. [Ii]t'?s [^.!?]+" + - "[A-Z][a-z]+ is not [^.!?]+\\. [Ii]t'?s [^.!?]+" + + # "a Y, not a Z" bare appositive: the categorical assertion with + # preemptive correction, verb optional ("a refinement, not a rivalry"). + # Verbless, so it also fires in headings, where the clause-shaped tokens + # above never match. Subsumes the older single-word-subject + # "X is a Y, not a Z" token. + - "\\b[Aa]n? [^,.;:!?\\n]{2,}, [Nn]ot [Aa]n? [^.!?;:,\\n]+" + - "[A-Z][a-z]+ are [^,]+, not [^.]+" + + # "Not only X but also Y" patterns + - "[Nn]ot only [^,;—:]+,? but (?:also )?[^.!?]+" + + # "Not because X, but because Y" causal variant + - "[Nn]ot because [^,;—:]+,? but because [^.!?]+" + + # === Plural subject negation-correction === + + # "These aren't X. They're Y." — period-separated + - "[Tt]hese aren'?t [^.!?]+\\. [Tt]hey'?re [^.!?]+" + - "[Tt]hose aren'?t [^.!?]+\\. [Tt]hey'?re [^.!?]+" + - "[Tt]hese are not [^.!?]+\\. [Tt]hey'?re [^.!?]+" + - "[Tt]hose are not [^.!?]+\\. [Tt]hey'?re [^.!?]+" + - "[Tt]hese are not [^.!?]+\\. [Tt]hey are [^.!?]+" + - "[Tt]hose are not [^.!?]+\\. [Tt]hey are [^.!?]+" + + # With modifiers + - "[Tt]hese aren'?t just [^.!?]+\\. [Tt]hey'?re [^.!?]+" + - "[Tt]hese aren'?t merely [^.!?]+\\. [Tt]hey'?re [^.!?]+" + - "[Tt]hese aren'?t simply [^.!?]+\\. [Tt]hey'?re [^.!?]+" + + # Semicolon/em-dash-joined + - "[Tt]hese aren'?t [^;,—:]+[;,—:] they'?re [^.!?]+" + - "[Tt]hose aren'?t [^;,—:]+[;,—:] they'?re [^.!?]+" + - "[Tt]hese are not [^;,—:]+[;,—:] they'?re [^.!?]+" + - "[Tt]hose are not [^;,—:]+[;,—:] they'?re [^.!?]+" + + # Generic "[Noun] aren't X. They're Y." catch-all + - "[A-Z][a-z]+ aren'?t [^.!?]+\\. [Tt]hey'?re [^.!?]+" + - "[A-Z][a-z]+ are not [^.!?]+\\. [Tt]hey'?re [^.!?]+" + - "[A-Z][a-z]+ are not [^.!?]+\\. [Tt]hey are [^.!?]+" + + # === "Doesn't mean X. It means Y." === + + # Period-separated + - "[Tt]his doesn'?t mean [^.!?]+\\. [Ii]t means [^.!?]+" + - "[Tt]hat doesn'?t mean [^.!?]+\\. [Ii]t means [^.!?]+" + - "[Tt]his does not mean [^.!?]+\\. [Ii]t means [^.!?]+" + - "[Tt]hat does not mean [^.!?]+\\. [Ii]t means [^.!?]+" + + # Semicolon/em-dash-joined + - "[Tt]his doesn'?t mean [^;,—:]+[;,—:] it means [^.!?]+" + - "[Tt]hat doesn'?t mean [^;,—:]+[;,—:] it means [^.!?]+" + + # Generic "[Noun] doesn't mean X. It means Y." + - "[A-Z][a-z]+ doesn'?t mean [^.!?]+\\. [Ii]t means [^.!?]+" + - "[A-Z][a-z]+ does not mean [^.!?]+\\. [Ii]t means [^.!?]+" + + # === Multi-word subject "isn't X. It's Y." === + # Catches "The colophon isn't a disclaimer. It's a feature." + # The single-word "[Noun] isn't X. It's Y." patterns above only match + # single capitalized-word subjects. + + # "The [noun] isn't X. It's Y." + - "[Tt]he [a-z]+ isn'?t [^.!?]+\\. [Ii]t'?s [^.!?]+" + - "[Tt]he [a-z]+ is not [^.!?]+\\. [Ii]t'?s [^.!?]+" + - "[Tt]he [a-z]+ isn'?t [^.!?]+\\. [Ii]t is [^.!?]+" + + # "A/An [noun] isn't X. It's Y." + - "[Aa]n? [a-z]+ isn'?t [^.!?]+\\. [Ii]t'?s [^.!?]+" + - "[Aa]n? [a-z]+ is not [^.!?]+\\. [Ii]t'?s [^.!?]+" + + # "Our/Your/Their [noun] isn't X. It's Y." + - "(?:Our|Your|Their|Its) [a-z]+ isn'?t [^.!?]+\\. [Ii]t'?s [^.!?]+" + - "(?:Our|Your|Their|Its) [a-z]+ is not [^.!?]+\\. [Ii]t'?s [^.!?]+" + + # Multi-word/acronym subjects — catch-all with "isn't...It's" anchor + # "WCAG 2.1 AA isn't a checkbox. It's a constraint." + - "[^.!?]+ isn'?t [^.!?]+\\. [Ii]t'?s [^.!?]+" + - "[^.!?]+ is not [^.!?]+\\. [Ii]t'?s [^.!?]+" + - "[^.!?]+ isn'?t [^.!?]+\\. [Ii]t is [^.!?]+" diff --git a/.vale/styles/DC/EmDash.yml b/.vale/styles/DC/EmDash.yml new file mode 100644 index 0000000..05d4d85 --- /dev/null +++ b/.vale/styles/DC/EmDash.yml @@ -0,0 +1,6 @@ +extends: existence +message: "Em dash used — vary punctuation (parens, colon, semicolon, period, comma) instead. A few per article are fine; heavy use reads as LLM-generated." +level: error +nonword: true +tokens: + - '—' diff --git a/.vale/styles/DC/FillerPhrases.yml b/.vale/styles/DC/FillerPhrases.yml new file mode 100644 index 0000000..ed0e1e5 --- /dev/null +++ b/.vale/styles/DC/FillerPhrases.yml @@ -0,0 +1,82 @@ +# Adapted from tbhb/vale-ai-tells (MIT). See THIRD_PARTY_NOTICES.md. +extends: existence +message: "AI filler: '%s'. Delete this phrase. It adds no meaning." +level: error +ignorecase: true +tokens: + - a wide range of + - a broad range of + - a variety of + - a myriad of + - a plethora of + - a wealth of + - an abundance of + - a multitude of + - countless + - numerous + - innumerable + - in order to + - for the purpose of + - with the aim of + - with a view to + - so as to + - serves to + - tends to + - has the potential to + - has the ability to + - is capable of + - is able to + - due to the fact that + - owing to the fact that + - in light of the fact that + - given the fact that + - despite the fact that + - regardless of the fact that + - the fact that + - it is clear that + - it is evident that + - it is apparent that + - it is obvious that + - it can be seen that + - it can be observed that + - at the end of the day + + # Performative sincerity markers. "to be honest," overlaps DefensiveHedges' + # fuller "To be honest, X, but Y" concessive shape on purpose: this rule owns + # the bare filler, DefensiveHedges owns the hedged-concession reading, and a + # sentence that is both earns both alerts by design. + - and honestly + - and honestly, + - honestly, + - honestly? + - to be honest, + - to be honest? + - and frankly + - and frankly, + - frankly, + - frankly? + - and truthfully + - truthfully, + - if i'm being honest + - if i'm being frank + - to be frank, + - to tell you the truth + - the truth is, + - in all honesty + - in all honesty, + - quite honestly + - quite frankly + - i'll be honest + - i will be honest + - to be perfectly honest + - to be completely honest + - to be totally honest + - to be brutally honest + - to be 100% honest + - the honest truth + - the honest answer + - the honest take + - honest answer + - honest take + - my honest take + - my honest opinion diff --git a/.vale/styles/DC/HedgingPhrases.yml b/.vale/styles/DC/HedgingPhrases.yml new file mode 100644 index 0000000..982fc2d --- /dev/null +++ b/.vale/styles/DC/HedgingPhrases.yml @@ -0,0 +1,92 @@ +# Adapted from tbhb/vale-ai-tells (MIT). See THIRD_PARTY_NOTICES.md. +extends: existence +message: "AI hedge: '%s'. Delete this throat-clearing and state your point directly." +level: error +ignorecase: true +tokens: + - It's important to note that + - It is important to note that + - It is important to understand + - It is important to consider + - It is important to recognize + - It is important to acknowledge + - It is important to emphasize + - It is important to highlight + - It is important to mention + - It's worth noting that + - It is worth noting that + - It's worth mentioning that + - It is worth mentioning that + - It is worth emphasizing that + - It is worth highlighting that + - It is worth pointing out that + - It is worth remembering that + # Ported from Metacommentary: the no-'that' contraction forms so this rule + # owns the whole "It's worth ing" family. + - It'?s worth noting + - It'?s worth mentioning + - It'?s worth pointing out + - It'?s worth emphasizing + - It'?s worth highlighting + - It'?s worth remembering + - It is essential to note + - It is essential to understand + - It is essential to recognize + - It is essential to acknowledge + - It is essential to emphasize + - It is crucial to note + - It is crucial to understand + - It is crucial to recognize + - It is critical to note + - It is critical to understand + - It is critical to recognize + - It is necessary to note + - It is necessary to understand + - It is necessary to recognize + - It should be noted that + - It should be emphasized that + - It should be mentioned that + - It bears mentioning + - It goes without saying + - Needless to say + - Generally speaking + - Broadly speaking + - For the most part + - By and large + - All things considered + - That being said + - That said + - With that said + - Having said that + - On the other hand + - At the same time + - To be sure + - To some extent + - In some ways + - In many ways + - In a sense + - As such + - As it were + - One thing is clear + - One thing is certain + - One thing is for sure + - One thing is for certain + - This much is clear + - This much is certain + - raises important questions + - raises serious questions + - raises questions about + - begs the question + - forces us to ask + - forces us to consider + - invites us to consider + - invites us to ask + - invites us to reflect + - prompts us to ask + - prompts us to consider + - calls into question + - calls on us to + - reminds us that + - reminds us of the importance + - as you might expect + - as you'd expect diff --git a/.vale/styles/DC/OpeningCliches.yml b/.vale/styles/DC/OpeningCliches.yml new file mode 100644 index 0000000..3347cdf --- /dev/null +++ b/.vale/styles/DC/OpeningCliches.yml @@ -0,0 +1,98 @@ +# Adapted from tbhb/vale-ai-tells (MIT). See THIRD_PARTY_NOTICES.md. +extends: existence +message: "AI opening: '%s'. Start with your actual point instead of this generic lead-in." +level: error +ignorecase: true +tokens: + - In today's rapidly evolving + - In today's fast-paced + - In today's digital age + - In today's ever-changing + - In today's interconnected + - In the realm of + - In the world of + - In the landscape of + - In the ever-evolving landscape + - In an era where + - In an age where + - In this day and age + - Embarking on a journey + - Embark on a journey + - Embarking on this journey + - As we navigate + - As we delve into + - As we explore + - When it comes to + - Picture this + - Imagine a world where + - Have you ever wondered + - In recent years + - Throughout history + - Since the dawn of + - From time immemorial + - Before we dive in + - Before we dive into + - By the end of this article + - By the end of this guide + - By the end of this post + - By the end of this tutorial + - By the end of this section + - This guide will walk you through + - This article will walk you through + - This post will walk you through + - This guide will help you + - This article will help you + - This post will help you + - Welcome to this guide + - Welcome to this overview + - Welcome to this exploration + - Welcome to this deep dive + - Without further ado + - Without further ado, let's + - Without further ado, here's + - Gone are the days + - Gone are the days when + - Gone are the days of + - Whether you're a + - Whether you're an + - Whether you're just + - Whether you're new + - Whether you're already + - Whether you're looking + - Whether you're trying + - Whether you're someone + - You might be wondering + - You may be wondering + - You're probably wondering + - You might be asking yourself + - You may be asking yourself + - If you've ever + - If you've ever wondered + - If you've ever found yourself + - Have you ever found yourself + - Chances are + - Chances are you've + - Chances are you're + - You're not alone + - You're not alone if + - We've all been there + - Look no further + - You've come to the right place + - You're in the right place + - Ready to dive in + - Ready to get started + - Ready to learn + - Ready to explore + - In the dynamic world of + - In an increasingly + - In a world where + - In a world of + - As technology continues to evolve + - As technology continues to advance + - As the world becomes + - As the world continues + - As we stand at + - As we stand on the + - We live in an era + - We live in a world + - We live in a time diff --git a/.vale/styles/DC/SycophancyMarkers.yml b/.vale/styles/DC/SycophancyMarkers.yml new file mode 100644 index 0000000..75a2b2a --- /dev/null +++ b/.vale/styles/DC/SycophancyMarkers.yml @@ -0,0 +1,25 @@ +# Adapted from tbhb/vale-ai-tells (MIT). See THIRD_PARTY_NOTICES.md. +# Pruned of bare-word tokens (Absolutely, Certainly, Of course) that +# false-positive on ordinary usage. +extends: existence +message: "AI sycophancy: '%s'. Delete this. It sounds robotic and insincere." +level: error +ignorecase: true +tokens: + - Great question + - Excellent question + - That's a great question + - That's an excellent question + - What a great question + - Fantastic question + - Wonderful question + - I'm glad you asked + - I'm happy to help + - I'd be happy to help + - I'd be glad to help + - Without a doubt + - You're absolutely right + - You make an excellent point + - That's a wonderful idea + - That's a fantastic point + - I couldn't agree more diff --git a/.vale/styles/DC/THIRD_PARTY_NOTICES.md b/.vale/styles/DC/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..177630b --- /dev/null +++ b/.vale/styles/DC/THIRD_PARTY_NOTICES.md @@ -0,0 +1,10 @@ +# Third-party rule sources + +`FillerPhrases.yml`, `HedgingPhrases.yml`, `ContrastiveFormulas.yml`, `OpeningCliches.yml`, +`AICompoundPhrases.yml`, and `SycophancyMarkers.yml` are adapted from +[tbhb/vale-ai-tells](https://github.com/tbhb/vale-ai-tells) (MIT License, Copyright (c) +2025-2026 Tony Burns). `SycophancyMarkers.yml` was pruned of bare-word tokens (`Absolutely`, +`Certainly`, `Of course`) that false-positive on ordinary usage; `FillerPhrases.yml` had +`at the end of the day` appended. + +`Vocabulary.yml` is independently written, not derived from any third-party style package. diff --git a/.vale/styles/DC/Vocabulary.yml b/.vale/styles/DC/Vocabulary.yml new file mode 100644 index 0000000..189300a --- /dev/null +++ b/.vale/styles/DC/Vocabulary.yml @@ -0,0 +1,15 @@ +extends: existence +message: "'%s' is a well-worn LLM tell — use plainer language." +level: error +ignorecase: true +tokens: + - delves? + - delving + - delved + - tapestry + - tapestries + - showcase + - showcases + - showcasing + - showcased + - silently diff --git a/README.md b/README.md index 37105f2..ba1ae7e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ docs agent-friendly. The spec focuses on meeting the technical constraints of agent platforms (truncation limits, content negotiation, discovery); it does not consider qualitative evaluation of content. -**Status**: Draft (v0.5.1) +**Status**: Draft (v0.6.0) **Full spec**: [SPEC.md](SPEC.md) | **Website**: [agentdocsspec.com](https://agentdocsspec.com) @@ -17,7 +17,7 @@ were built for human readers, and agent access patterns are fundamentally different: - Agents retrieve URLs from training data instead of navigating your site -- Web fetch pipelines silently truncate content (Claude Code at ~100K chars; +- Web fetch pipelines truncate content without warning (Claude Code at ~100K chars; the MCP Fetch reference server at just 5K chars by default) - HTML-to-markdown conversion is lossy, and inline CSS can consume the entire truncation budget before content even starts @@ -25,21 +25,22 @@ different: - Cross-host redirects and JavaScript redirects break agent access entirely The result: agents frequently fail to get the documentation content they need, -fall back on training data, or silently work with partial information. +fall back on training data, or work with partial information without +knowing it. ## What the Spec Covers -The spec defines **23 checks across 7 categories**: +The spec defines **28 checks across 7 categories**: | Category | Checks | What it evaluates | |----------|--------|-------------------| | Content Discoverability | 7 | Discovery index exists, is valid, fits in a single fetch, links resolve, links point to markdown, embedded directives in HTML and markdown pointing agents to `llms.txt` | | Markdown Availability | 2 | `.md` URL support, content negotiation via Accept headers | -| Page Size | 4 | Rendering strategy (SPA/CSR detection), markdown size, HTML size (pre/post conversion), content start position | -| Content Structure | 3 | Tabbed content serialization blowup, section header quality, code fence validity | +| Page Size | 6 | Rendering strategy (SPA/CSR detection), markdown size, HTML size (pre/post conversion), served transfer size, content start position, single-fetch completeness | +| Content Structure | 5 | Tabbed content serialization blowup, section header quality, code fence validity, markdown link portability, embedded data serialization | | URL Stability | 2 | Soft 404 detection, redirect behavior | | Observability | 3 | `llms.txt` coverage, markdown/HTML content parity, cache header hygiene | -| Authentication | 2 | Auth gate detection, alternative access paths for gated content | +| Authentication | 3 | Auth gate detection, alternative access paths for gated content, bot-protection interference with automated fetching | Each check has defined pass/warn/fail criteria, an automation level, and severity. See the [Checks Summary](SPEC.md#checks-summary) in the full spec. @@ -65,7 +66,7 @@ If you can only do a few things, these have the highest impact: ## Test Your Docs [`afdocs`](https://afdocs.dev) is a companion CLI tool -and Node.js library that implements this spec. It runs all 23 checks against +and Node.js library that implements this spec. It runs the automated checks against your documentation site and reports what's working, what's not, and what to fix. ```bash diff --git a/SPEC.md b/SPEC.md index 611af1a..148a74e 100644 --- a/SPEC.md +++ b/SPEC.md @@ -3,8 +3,8 @@ | | | |--------------|--------------------------------------------------------------| | **Status** | Draft | -| **Version** | 0.5.1 | -| **Date** | 2026-05-08 | +| **Version** | 0.6.0 | +| **Date** | 2026-09-13 | | **Author** | Dachary Carey + community contributors | | **URL** | https://agentdocsspec.com | | **Repository** | https://github.com/agent-ecosystem/agent-docs-spec | @@ -16,7 +16,7 @@ Documentation sites are increasingly consumed by coding agents rather than human readers, but most sites are not built for this access pattern. Agents hit truncation limits, get walls of CSS instead of content, can't follow cross-host redirects, and don't know about emerging discovery mechanisms like -`llms.txt`. This spec defines 23 checks across 7 categories that evaluate how +`llms.txt`. This spec defines 28 checks across 7 categories that evaluate how well a documentation site serves agent consumers. It is grounded in empirical observation of real agent workflows and is intended as a shared standard for documentation teams, tool builders, and platform providers. @@ -39,19 +39,48 @@ This spec does **not** target: their own retrieval pipelines that may or may not resemble the web fetch pipelines described here. - **RAG pipelines** that pre-index documentation into vector stores. These - ingest content at build time, not at query time, so truncation limits and - real-time fetch behavior are less relevant. + retrieve at query time from an index built ahead of time, so truncation + limits and real-time fetch behavior are less relevant. Their *ingestion* + step is a different matter: it fetches from the site like any automated + client, and many of this spec's checks serve it directly. See + [Serving RAG Ingestion Pipelines](#serving-rag-ingestion-pipelines). The findings and checks in this spec are grounded in empirical observation of coding agents. Some recommendations (like providing `llms.txt` and serving markdown) will benefit other consumers too, but the pass/warn/fail criteria are calibrated for the coding agent use case. +### Related Surfaces + +This spec covers the **delivery** of web-served documentation: whether agents +can discover, fetch, and receive content intact. Agent-friendly documentation +has at least two other surfaces, deliberately out of scope here and planned +as companion specifications as the evidence base for them matures: + +- **Content composition**: what documentation should *contain* to serve + agents well. Factual consistency across pages, structure and density + suited to machine consumption, and how representation across a corpus + shapes what agents effectively know about a product. These properties + require semantic evaluation rather than the mechanical verification this + spec's checks are built on. +- **Repository-local documentation**: docs serving a coding agent that + works *inside* a repository (`README`, `docs/` directories, agent-facing + instruction files). The agent discovers content with local tools (grep, + find, directory listings) and consumes it through file reads, so almost + none of the web spec's checks transfer: there is no fetch pipeline, no + truncation-by-summarizer, and no llms.txt; greppability and file layout + do the work that discovery checks do on the web. + +Keeping these separate protects what makes each credible: this spec's checks +are mechanically verifiable against a live site, and that property should +not be diluted as adjacent guidance develops. + ## Background Agents don't use docs like humans. They retrieve URLs from training data rather than navigating table-of-contents structures. They struggle with HTML-heavy -pages, silently lose content to truncation, and don't know about emerging +pages, lose content to truncation with no indication anything was cut, and +don't know about emerging standards like `llms.txt` unless explicitly told. These checks codify the patterns that empirically help or hinder agent access to documentation content. @@ -91,9 +120,13 @@ and recommendations). The distinction is noted where it matters: - **Normative sections**: Category 1-7 check definitions, Checks Summary table. -- **Informational sections**: Background, Scope, Start Here, "How Agents Get - Content", "Who Actually Uses llms.txt?", Progressive Disclosure - recommendation, "Making Private Docs Agent-Accessible", Appendices. +- **Informational sections**: Background, Scope (including Related + Surfaces), Start Here, "How Agents Get Content", "Who Actually Uses + llms.txt?", Progressive Disclosure recommendation, "Making Private Docs + Agent-Accessible", Interaction Effects, "Serving RAG Ingestion + Pipelines", Appendices. Interaction Effects guides how implementations + present combined results; it defines no pass/warn/fail criteria of its + own. The progressive disclosure pattern for `llms.txt` is a recommendation from this spec, not a normative requirement. Sites that keep their `llms.txt` under @@ -111,15 +144,21 @@ are ordered by impact based on observed agent behavior: to keep each file under the limit. Checks: `llms-txt-exists`, `llms-txt-size` -2. **Serve markdown versions of your pages.** Either via `.md` URL variants or - content negotiation. Markdown is what agents actually want; HTML conversion - is lossy and unpredictable. - Checks: `markdown-url-support`, `content-negotiation` - -3. **Keep pages under 50,000 characters of content.** If a page has tabbed or - dropdown content that serializes into a massive blob, break it into separate - pages or ensure the markdown version stays under the limit. - Checks: `page-size-markdown`, `page-size-html`, `tabbed-content-serialization` +2. **Serve markdown versions of your pages, and verify what you serve.** + Either via `.md` URL variants or content negotiation. Markdown is what + agents actually want; HTML conversion is lossy and unpredictable. Then + treat the markdown generator as a second rendering pipeline that needs + its own QA: it can emit broken links or partial content while the HTML + looks fine, and no human reads the markdown to notice. + Checks: `markdown-url-support`, `content-negotiation`, + `markdown-link-portability`, `single-fetch-completeness` + +3. **Keep pages under 50,000 characters of content.** If a page has tabbed + or dropdown content, or generated data tables, that serialize into a + massive blob, break it into separate pages or ensure the markdown version + stays under the limit. + Checks: `page-size-markdown`, `page-size-html`, + `tabbed-content-serialization`, `embedded-data-serialization` 4. **Put a pointer to your `llms.txt` at the top of every docs page.** A simple blockquote directive that tells agents where to find the documentation index. @@ -131,7 +170,16 @@ are ordered by impact based on observed agent behavior: redirects. Avoid cross-host redirects, JavaScript redirects, and soft 404s. Checks: `http-status-codes`, `redirect-behavior` -6. **Monitor your agent-facing resources.** Treat `llms.txt` and markdown +6. **Make sure bot protection isn't blocking agents.** Bot management tuned + for scraper traffic can challenge, throttle, or stall automated clients + fetching your public docs, and the failure is invisible from a browser. + Exempt documentation routes from behavioral enforcement, or scope + enforcement to interactive product surfaces. For affected sites this + outranks everything else on this list: nothing above matters if agents + can't fetch at all. + Checks: `bot-protection-interference` + +7. **Monitor your agent-facing resources.** Treat `llms.txt` and markdown endpoints like any other production surface: check freshness, verify content parity with HTML, and ensure cache headers allow timely updates. Checks: `llms-txt-coverage`, `markdown-content-parity`, @@ -150,6 +198,15 @@ Each check has: - **Automation**: Whether the check can be fully automated, partially automated (heuristic), or is advisory only. +The canonical source of this spec is a single document +([SPEC.md](https://github.com/agent-ecosystem/agent-docs-spec/blob/main/SPEC.md) +in the repository). The website serves it as per-category pages under +[agentdocsspec.com/spec/web/](https://agentdocsspec.com/spec/web/), following +this spec's own progressive disclosure recommendation: the full document +outgrew the 100,000-character truncation threshold its own checks are built +around, so it now practices what it prescribes. Each page stays well under +the 50,000-character pass threshold. + ### Check Dependencies Some checks depend on the results of others: @@ -171,9 +228,21 @@ Some checks depend on the results of others: - `auth-alternative-access` only runs if `auth-gate-detection` returns warn or fail (the site must have auth-gated content for alternative access paths to be relevant). +- `bot-protection-interference` has no prerequisites, but it inverts the + usual dependency direction: when it returns warn or fail, results from + every multi-page check should be flagged as computed from a partial sample + (see the [Bot Protection Degrading Scan Reliability](#bot-protection-degrading-scan-reliability) + interaction effect). It is also evaluated from evidence gathered across the + entire scan rather than run as a discrete Category 7 step. - `markdown-content-parity` only runs if `markdown-url-support` or `content-negotiation` passes (the site must serve markdown for this check to apply). +- `single-fetch-completeness` and `markdown-link-portability` only run if + the site serves markdown by some detected path: `markdown-url-support` or + `content-negotiation` passes, or `llms-txt-links-markdown` finds markdown + links. A site can serve agent-facing markdown through `llms.txt` alone, + without page-level `.md` variants, and that markdown deserves the same + evaluation. Implementations should run checks in category order (1 through 7) and skip dependent checks when their prerequisites fail. @@ -301,20 +370,31 @@ against every discovered `llms.txt` file. ### `llms-txt-links-resolve` -- **What it checks**: Whether the URLs listed in `llms.txt` actually resolve - (return 200). +- **What it checks**: Whether the URLs listed in `llms.txt` resolve to the + content they promise. - **Why it matters**: A stale `llms.txt` with broken links is worse than no `llms.txt` at all. It sends agents down dead ends with high confidence. - **Result levels**: - - **Pass**: All links resolve (200, following same-host redirects). + - **Pass**: All links resolve (200, following same-host redirects) with no + soft 404s or representation mismatches. - **Warn**: >90% of links resolve. - **Fail**: <=90% of links resolve. - **Recommended action**: Audit and fix or remove broken URLs. A stale `llms.txt` with broken links is worse than no `llms.txt` at all because it sends agents down dead ends with high confidence. -- **Automation**: Full. +- **Automation**: Full. Verification must go beyond status codes: apply + soft-404 heuristics (see `http-status-codes`) to the response body, and + verify the content type for links that promise markdown. A bare status + check passes broken links; in one observed production case, a generated + catalog's `.md` links all soft-404ed as HTML SPA shells while returning + 200 (see `markdown-link-portability`). - **Notes**: Requires making HTTP requests to each URL. For large files, implementations may choose to test a random subset rather than every link. + Links in `llms.txt` should be absolute URLs: the file is copied, cached, + and aggregated by tools that drop its source URL, so relative links share + the portability failure modes described in `markdown-link-portability`. + The [progressive disclosure design principles](#progressive-disclosure-for-large-documentation-sets) + make the same recommendation for links between index levels. ### `llms-txt-size` @@ -559,7 +639,10 @@ the nesting pattern goes further by distributing content across multiple files. - **Warn**: Ensure the directive appears near the top of every markdown page, not just some. - **Fail**: Add a blockquote near the top of each markdown page (e.g., - "> For the complete documentation index, see [llms.txt](/llms.txt)"). + "> For the complete documentation index, see + [llms.txt](https://example.com/llms.txt)"). Use an absolute URL; see + `markdown-link-portability` for why relative links are fragile in + served markdown. - **Automation**: Heuristic. Fetch the markdown version of sampled pages (via `.md` URL or content negotiation) and search for patterns like links to `llms.txt`, phrases like "documentation index", or blockquote directives @@ -622,9 +705,9 @@ which agents consume far more effectively than HTML. ## Category 3: Page Size and Truncation Risk -These checks evaluate whether page content fits within the processing limits of -agent web fetch pipelines. Truncation is silent: the agent doesn't know it's -working with partial data. +These checks evaluate whether page content fits within the transfer and +processing limits of agent web fetch pipelines. Truncation is silent: the +agent doesn't know it's working with partial data. ### How Agents Get Content @@ -653,6 +736,23 @@ Because different agents hit different paths, this spec defines size checks for **both** the markdown response (if available) and the HTML response. A site that's only optimized for the markdown path is leaving most agents behind. +Pipelines also differ in **when** they cut oversized content, and the order +determines which measurement predicts the agent's experience: + +1. **Convert, then truncate.** Scripts and styles are stripped, HTML is + converted to markdown, and the size limit applies to the converted output. + Post-conversion size (`page-size-html`) predicts these pipelines. +2. **Truncate, then convert** (or ingest raw HTML directly). The size limit + applies to the bytes as served, so inline scripts and serialized data + consume the budget before any content does. Served size + (`page-size-transfer`) predicts these pipelines. +3. **Capped fetch.** Independent of processing order, some tools cap the + response bytes they will read, so a page can fail at the transfer layer + before any conversion happens. Served size predicts this too. + +A page can score well on one measurement while failing agents on the other, +which is why the spec measures both. + For empirical observations of how specific platforms (Claude, Cursor, Copilot, Gemini, Windsurf Cascade, and others) handle retrieval, truncation, and summarization in practice, see [Agent platform comparisons](https://agentdocsspec.com/platforms/). @@ -792,6 +892,78 @@ summarization in practice, see [Agent platform comparisons](https://agentdocsspe conversion ratio (e.g., "505KB HTML -> 12KB markdown (98% boilerplate)") as a useful signal for site owners. +### `page-size-transfer` + +- **What it checks**: The served byte size of the HTML document response: + the response body after transfer decoding (decompression), which is what + an agent's HTTP client hands to its processing pipeline. Subresources + (linked CSS, JavaScript, images) are not counted, because agents generally + don't fetch them. Inline scripts, styles, and serialized data payloads + embedded in the document are counted, because agents can't avoid receiving + them. +- **Why it matters**: `page-size-html` measures what survives HTML-to-markdown + conversion, which models pipelines that strip scripts and convert before + truncating. Served size measures what every agent pays before any + processing happens, and it fails differently: + - **Truncate-first and raw-ingestion pipelines** consume script payload as + content. For them, serialization overhead isn't invisible; it is the + page. + - **Fetch caps** apply to response bytes, not converted output. A page can + convert to a few kilobytes of clean markdown and still exceed the byte + budget of the tool fetching it (Claude Code's fetch buffer caps at + ~10MB). + - **Bandwidth and latency costs** apply to every fetch regardless of + pipeline, and multi-page reading sessions multiply them. + + Modern server-rendering frameworks can make the gap between served bytes + and content arbitrarily large. In measurements of production documentation + sites on one hosted platform, pages shipped 75-84% of their bytes as + serialized framework payloads inside inline script tags: component trees, + resolved metadata, and a complete duplicate of the page's markdown source. + Served-bytes-to-content ratios ran from 40:1 to 200:1. Those pages score + well on `page-size-html` because conversion strips the payload, while + every agent fetching them transfers half a megabyte to several megabytes + per page. +- **Result levels**: + - **Pass**: Served size under 1MB. + - **Warn**: Served size between 1MB and 10MB. No documented cap is + exceeded, but truncate-first and raw-ingestion pipelines are consuming + mostly non-content bytes, and multi-page sessions pay a real bandwidth + and latency cost. + - **Fail**: Served size over 10MB. This exceeds the only well-documented + transfer cap (Claude Code's fetch buffer); content beyond the cap is + unreachable regardless of how the pipeline processes it. +- **Recommended action**: + - **Warn**: Identify what the non-content bytes are; in practice they are + usually inline serialization (framework hydration payloads, embedded + duplicate page source, resolved data objects) visible in the page + source. Avoid shipping the same content twice in different formats, + load large data payloads on demand, and confirm markdown variants are + available and discoverable so agents have a cheaper path. + - **Fail**: Same actions, urgently. At this size, at least one major + platform cuts the page off at the transfer layer. + - Markdown availability gives agents that discover it an escape hatch but + does not reduce what HTML-path agents transfer; fixing the served + payload remains important. +- **Automation**: Full. Fetch with an `Accept-Encoding` typical of agent + HTTP clients, decode the response, and measure the decoded body. + Implementations may also report the on-the-wire (compressed) size where + available; serialized payloads compress well, so wire size understates + the processing burden. +- **Report details**: Show served bytes alongside the post-conversion + content size from `page-size-html`, and report the ratio between them + (e.g., "3.4MB served -> 29KB content (~120:1)"). A high ratio on a large + page is an architecture signature (hydration payloads, embedded duplicate + content) rather than a content problem; it tells the site owner the fix + lives in framework configuration, not in the docs themselves. +- **Notes**: This check complements `rendering-strategy`, which catches pages + that ship too little server-rendered content. This check catches the + opposite failure: pages that render content fine but ship many times its + weight in serialization overhead. Byte-level caps are less documented than + character-level truncation limits, so the default thresholds here are + conservative and should be configurable (see + [Appendix A](#appendix-a-known-platform-truncation-limits)). + ### `content-start-position` - **What it checks**: How far into the **post-conversion** content (by character @@ -820,6 +992,92 @@ summarization in practice, see [Agent platform comparisons](https://agentdocsspe by the site should not have boilerplate preamble; if it does, that's a separate issue worth flagging but not something this check targets. +### `single-fetch-completeness` + +- **What it checks**: Whether a markdown response delivers its complete + content in one fetch, and when it doesn't, whether the continuation is + machine-followable: declared where agents will see it, linked with an + absolute URL, and actually working. +- **Why it matters**: Pagination is application-level truncation, and it is + quieter than the platform truncation this category otherwise measures. A + paginated markdown response looks complete: it is well-formed, ends + cleanly, and returns 200. The signals that it is partial are easy for + agent pipelines to lose: + - **Summarization pipelines** process fetched content through a smaller + model before the orchestrating agent sees it. A pagination note may or + may not survive summarization, and the summarizer cannot perform a + follow-up fetch itself; the orchestrator would have to notice the note + and choose to fetch again. + - **Trailing pagination notes** sit at the end of the content, which is + the first region lost to platform truncation. A truncated response + loses the only indication that it was also paginated. + - **RAG pipelines** chunk fetched content for retrieval. A pagination + marker lands in one chunk, unrelated to the content it describes, and + effectively disappears. + + This failure was observed in production on a model catalog's markdown + variant: 100 of 102 entries shown, a pagination note at the bottom of the + file, a continuation URL that was root-relative rather than absolute, and, + when fetched, a continuation response that returned an empty body. An + agent fetching that page gets 98% of the catalog and no working way to + learn what's missing. + + Notably, pagination in a markdown variant is often inherited from the + HTML UI rather than needed by the markdown itself. The catalog above + paginates at 100 entries per page, but the complete set serializes to + roughly 32,000 characters, comfortably under this spec's 50,000-character + pass threshold. The markdown variant imported an interaction pattern from + a surface that has interaction; markdown doesn't. +- **Result levels**: + - **Pass**: The response is complete in one fetch (no pagination signals + detected), or pagination exists and the continuation is declared at the + top of the content with an absolute URL that resolves to the next + segment. + - **Warn**: Pagination exists and the continuation works, but is fragile: + declared only at the bottom of the content, linked with a relative URL, + or discoverable only from response headers. + - **Fail**: The content is partial and the continuation is missing, + relative and unresolvable, or broken (non-success status, empty body, + or a soft 404). +- **Recommended action**: + - **Warn**: Move the continuation declaration to the top of the content + (before anything truncation could remove) and make continuation links + absolute. + - **Fail**: First ask whether the markdown variant needs pagination at + all: complete content that fits within this spec's size thresholds + should be served in one response, even when the HTML UI paginates. If + pagination is genuinely necessary, declare it at the top with absolute + links, and verify the continuation URLs actually serve content. +- **Automation**: Heuristic. Detect pagination signals in markdown + responses: "N of M" phrasing, links or instructions containing pagination + query parameters (`?page=`, `?offset=`), "next page" link text, and + `Link: rel="next"` response headers. When signals are found, fetch the + continuation and verify it returns substantive content of the expected + representation. Absence of signals is treated as complete; a page that + omits content with no marker at all is not detectable by this + check (see `markdown-content-parity` for the cross-representation + comparison that can catch it). +- **Notes**: Only applies to markdown responses (`.md` variants, content + negotiation, and `llms.txt`-linked markdown). HTML pagination is an + interaction pattern agents share with human readers and is out of scope + here. + + **Relationship to the spec's splitting recommendations.** This check + targets windowing rather than the number of fetches. Other parts + of this spec recommend *more* fetches: progressive disclosure splits an + oversized `llms.txt` into section files, and the page-size checks + recommend breaking large pages up. The difference is what each fetch + returns. Splitting creates self-contained units, each with its own topic + and URL, reached by navigation: an agent fetches an index, chooses the + relevant unit, and gets content that is complete as the thing it claims + to be. Pagination slices one logical unit into arbitrary windows: no + window is complete for any question, every window is required to have the + unit at all, and the seams are easy to lose. Multi-fetch by choice is + navigation; multi-fetch by obligation is truncation with extra steps. The + principle behind both: split by meaning, don't window by size. A response + should either be complete as what it claims to be, or say so plainly + where agents will see it. + --- ## Category 4: Content Structure @@ -915,6 +1173,132 @@ heuristics. outside the site owner's control, though implementations may optionally flag them as informational findings. +### `markdown-link-portability` + +- **What it checks**: Whether links in served markdown are absolute URLs, and + whether a sample of them resolves to the representation they promise (a + `.md` link returns markdown content, not an HTML error page). +- **Why it matters**: Relative URL resolution is well-defined (RFC 3986), but + it requires knowing the base URL, and agent pipelines routinely lose it. A + browser always carries the base; markdown fetched by an agent passes + through summarization models, gets chunked for RAG, or gets pasted into a + context where the source URL is gone. Once the base is lost, a + root-relative link is unreconstructable and a path-relative link is + meaningless. This spec already recommends absolute URLs between `llms.txt` + levels for the same reason; served markdown deserves the same rule. + + Link verification must go beyond status codes. In one observed production + case, a catalog's markdown variant emitted over 100 well-formatted links + that all pointed into a wrong internal path prefix, apparently a build-time + substitution error. Every link returned 200 with a body. The body was an + HTML SPA shell whose only acknowledgment of failure was a serialized + framework error digest inside script payload: a soft 404 served as HTML at + a `.md` URL. A checker (or agent) that tested status codes alone would + conclude the links worked; checking the `Content-Type` header alone would + have caught it. +- **Result levels**: + - **Pass**: Links are absolute URLs, and sampled links resolve to the + expected representation. + - **Warn**: Links are root-relative (resolvable while the base URL is + known, fragile once it isn't), or sampled links resolve with minor + mismatches (e.g., a `.md` link that redirects to an HTML page with the + right content). + - **Fail**: Links are path-relative, or sampled links are broken: hard + 404s, soft 404s, or a content type that contradicts the link (`.md` + links returning HTML shells). +- **Recommended action**: + - **Warn**: Emit absolute URLs when generating markdown variants; the + site's canonical host is known at build time. + - **Fail**: Fix the link generation first, then make the links absolute. + Verify generated links in CI by fetching a sample and checking both + status and content type; a link set that is generated is a link set + that can break wholesale. +- **Automation**: Full. Parse links from served markdown, classify as + absolute, root-relative, or path-relative, resolve a sample against the + fetch URL, and verify status, `Content-Type`, and soft-404 heuristics + (reusing the detection from `http-status-codes`). +- **Notes**: Applies to markdown served via `.md` URLs, content + negotiation, and pages discovered through `llms.txt` links. Links in + `llms.txt` itself are covered by `llms-txt-links-resolve`, which applies + the same representation verification. Same-document fragment links + (`#anchor` with no path) are exempt from the absolute-URL requirement: + they resolve within the content the agent already holds, and rewriting + them to absolute URLs adds nothing. + + **Why this check exempts the HTML path.** Relative links in HTML are + correct web practice (they are what makes staging domains, mirrors, and + CDN setups work), and the HTML path does not need the site's help: a + pipeline converting HTML to markdown still holds the fetch URL at + conversion time, so resolving relative links is the pipeline's job, with + full information. The base URL is only lost downstream of conversion. + Served markdown is different on both ends: the generator knows the + canonical host, so absolute links are free to produce, and on the + best-case consumption path (direct delivery of `text/markdown` under the + summarization threshold) the site's bytes reach the model verbatim, with + no conversion step where anything could be resolved. Tool builders + implementing fetch pipelines should resolve relative links during + HTML-to-markdown conversion, and likewise when ingesting raw HTML; + converted content with relative links has all the same failure modes as + served markdown once it leaves the converter. + +### `embedded-data-serialization` + +- **What it checks**: Whether machine-generated bulk data (large uniform + tables, inline JSON or data blobs, base64 payloads) dominates a page's + converted content, and attributes the page's size to the specific elements + responsible. +- **Why it matters**: Dynamic widgets (compatibility matrices, model + catalogs, spec browsers, pricing tables) flatten into static content when + a page is rendered for agents. The result can be a page whose size wildly + exceeds what its author believes they wrote: the author sees a few + paragraphs and a widget; the built page carries hundreds of serialized + rows under them. The size checks in Category 3 catch the symptom but + don't explain it, and without attribution the person who can fix the page + has no idea what to fix, or that anything is wrong at all. + + In one measured production case, a reference page served 302KB of HTML of + which 64% was table markup, including a single generated table of 218 + rows. The page converts to roughly 83,000 characters (this spec's warn + band), while its non-table prose totals about 17,000 characters. The + page's truncation risk is entirely a property of its generated tables, + which is invisible in an aggregate size number. +- **Result levels**: + - **Pass**: No bulk-data elements detected, or bulk elements are present + but the page passes the Category 3 size checks regardless. + - **Warn**: Bulk-data elements are the dominant contributor (for example, + over half of converted content) to a page that lands in the size + checks' warn band. + - **Fail**: Bulk-data elements are the dominant contributor to a page + that exceeds the size checks' fail threshold. Content after the bulk + element is beyond the truncation point for most platforms. +- **Recommended action**: Bulk data is often legitimate content (a support + matrix is the point of a support-matrix page), so the goal is structure, + not removal. Split large generated tables across per-section pages (as + self-contained units reached from an index, each complete for its scope; + paginating one table into windows trades this problem for the one + `single-fetch-completeness` describes), + provide filtered or queryable views, load embedded data blobs on demand, + and place prose before bulk elements so truncation removes data rows + rather than explanation. Report the attribution to content authors: + a page that an author experiences as two paragraphs should not ship as a + hundred kilobytes without the author knowing. +- **Automation**: Heuristic. After HTML-to-markdown conversion (same + pipeline as `page-size-html`), detect bulk elements: tables above a row + threshold with uniform row structure, fenced or inline JSON blobs above a + size threshold, and base64 runs. Report each element's share of the + converted content. Thresholds for "bulk" need calibration against real + pages and should be configurable. This check sees only bulk that + survives conversion into content (tables, code fences, prose-embedded + data); serialized payloads inside `