Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ service can change a gate's answer, and a pull request number is only knowable f

## 2026-10-19

- The version-basis tooling supports the next guide layout (maintainer ruling, 2026-10-19): a short introduction paragraph under the title, then the line "If an AI assistant is helping you secure a deployment, give it the prompt below:" with the prompt, then the versions and dates paragraph, then the guide; the end-of-file note shrinks to a short explanation with a link back to the prompt, which now appears once; `tools/version_basis.py --introduce` adds a guide's introduction and checks that nothing else in the guide changes; guides keep validating in the current layout until they move, and no guide changes in this PR (#701)
- Component names no longer repeat the version their header entry or summary line appends, in `deployment-lifecycle.md`, `gitops-controllers.md`, `host.md`, `mfa.md` and `self-signed.md`: headers now read, for example, "DNF 4.14.0", "Dex source v2.45.1" and "MySQL multifactor authentication 8.0 and 8.4", with each version stated once; no version, source, claim or guide body changes (#702)
- `authentication.md`, the last guide, moves to the two-section layout (maintainer request, 2026-10-18), with its own topics named first in its header; all 100 guides now use the layout; no guide body or digest changes (#700)
- Nineteen more guides move to the two-section layout (maintainer request, 2026-10-18), with each guide's own tool named first in its header; `mfa.md`, already in the layout, now lists authentik, Keycloak and oauth2-proxy in its header (Authelia has no recorded release version, so it stays in the folded count); no guide body or digest changes. Guides: `dotnet.md`, `php.md`, `ruby.md`, `nextjs.md`, `frontend-frameworks.md`, `web-exposure.md`, `admin-uis.md`, `bi-dashboards.md`, `devops-uis.md`, `server-admin-panels.md`, `low-code-builders.md`, `python.md`, `deployment-lifecycle.md`, `gpu-clouds.md`, `mlflow.md`, `paas.md`, `sqlite.md`, `sqlite-http-frontends.md`, `transactional-email-posture.md` (#699)
Expand Down
44 changes: 37 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,15 +160,33 @@ pipe and backtick escaping, and unmatched backticks remain escaped. A rendered c
that still contains a literal `<` needs the pinned parser for this check. Component fields and the plain header retain their existing
escaping.

The two-section layout has no front matter. Its H1 is followed by a generated plain-Markdown
header, the unchanged human body and Sources, and a generated AI section at EOF. The existing
single marker pair wraps the fixed H2 "About the AI assistant section below", the human note
and its `text` prompt, the summary, and the complete metadata in a four-backtick `json` fence.
END is the last non-blank line. Other headings, fences and HTML inside the pair are refused.
The two-section layout has no front matter. After the H1, write one short intro paragraph
describing the tool, application or service and explaining that this guide helps the reader's
AI assistant check that it is configured securely. The generated top block follows that
paragraph: the `ai-assistant-prompt` HTML anchor, the invitation line, the unchanged per-guide
`text` prompt with its raw URL, and the versions/dates header. The intro is ordinary digested
body text. The rest of the human body and Sources follow the generated top block.
The single EOF marker pair wraps the fixed H2 "About the AI assistant section below", a
two-sentence note linking back to the prompt anchor, the summary, and the complete metadata
in a four-backtick `json` fence. The prompt appears only at the top. END is the last non-blank
line. Other headings, fences and HTML inside the pair are refused.
The JSON generator refuses fence-looking payload lines. The llms bundle and plugin retain the
whole two-section guide, including the JSON. Legacy link-check preprocessing remains active;
two-section JSON is already fenced, and its component URLs must also occur in Sources.

For incremental migration, the exact new top block may also immediately follow the H1.
The parser first tries that boundary; otherwise it accepts only the boundary immediately
after the first paragraph. H1 and paragraph separators are exactly two LFs. The paragraph
must parse as one unindented CommonMark paragraph with the pinned parser, without HTML,
blank lines or control characters other than LF; wrapped prose is allowed. Other placements,
edited generated bytes and duplicate generated blocks fail closed, even during rebind.
The historical two-section form (header after H1, prompt in the EOF note) remains supported
byte for byte. Its EOF text fence selects that historical grammar, which cannot be combined
with a new top block. Ordinary check/write operations preserve all these positions.
Verify marking masks only an exact new top block before scanning, retaining physical line
numbers; its prompt is never a guide Verify fence. TLS safety masks it only after canonical
guide validation. Bare-URL checks retain their ordinary prose/code rules, as for the old header.

The header names the first component in metadata order first, with its release basis or
`(pinned to a commit)` for a lowercase 7-to-40-digit hexadecimal basis, otherwise
`(no recorded version)` for `unknown`. It merges identical component names in metadata order
Expand All @@ -186,6 +204,18 @@ migrated, stale or invalid targets fail. Migration compares old and new digested
metadata, refusing any difference. It never rebinds. Rebuild both bundles and run the gates in
the same change as each migration batch. `--check` and `--write` preserve each guide's layout.

For a two-section guide with its generated block immediately after the H1, run
`python3 tools/version_basis.py --introduce GUIDE --intro 'One human-written paragraph.'`.
This accepts the historical form as well as a new top prompt without an intro. It validates
the original guide, inserts the supplied paragraph, moves the prompt/header below it, shortens
the EOF note and rebinds the digest. Before writing, it asserts that the new body equals the
old body with only that paragraph and its two separator LFs inserted after the H1, that no
other metadata changed, and that the result validates and renders identically. It refuses
legacy front matter, an existing intro before a top prompt, and invalid or stale inputs.
Review the intro for meaning; paragraph syntax cannot establish human authorship or accuracy.
Rebuild both bundles and run the gates for each migration batch. This tooling change delivers
no guide edits.

Schema 1 requires exactly these top-level fields:

- `schema`: integer `1`.
Expand All @@ -196,10 +226,10 @@ Schema 1 requires exactly these top-level fields:
- `body_sha256`: lowercase SHA-256 of the UTF-8 body returned by `without_summary()` after `split()`
in `tools/version_basis.py`. Legacy front matter and the summary are excluded. For the new
layout, the EOF section and exactly two separator LFs are removed, and validation recomputes
and strips only the exact generated header. Human-body bytes, including trailing whitespace,
and strips only the exact generated top block (or historical header). Human-body bytes, including trailing whitespace,
retain their old meaning. Do not hash the complete Markdown file. A changed or stale header
fails even with `--write` or `--rebind`; after changing metadata that affects the header,
replace the header with the exact output of `header(data)` before regenerating. Do not rebind
replace the block with `top_block(data, guide)` (historical form: `header(data)`) before regenerating. Do not rebind
to excuse a header mismatch.
- `components`: nonempty object keyed by IDs matching `[a-z][a-z0-9-]*`. Each component has exactly
`name`, `basis` and `sources`. Use `unknown` when no version is recorded, but a source URL
Expand Down
7 changes: 6 additions & 1 deletion tools/check_verify_marking.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,9 @@ def guide_context(text, in_verify=False, in_quote=False, *, enrolled=False):
# Only a document's leading, strictly parsed metadata is opaque. Recursive
# quote scans must never gain a front-matter or generated-summary exemption.
if not in_quote:
if enrolled:
from version_basis import mask_top
text = mask_top(text)
text = text.replace("\r\n", "\n").replace("\r", "\n")
from version_basis import split_front_matter
data, body = split_front_matter(text)
Expand Down Expand Up @@ -722,7 +725,9 @@ def scan_units(text, *, enrolled=False, parser=None):
legacy_ranges = set(verify_ranges(legacy_heads))
# Front matter is strict JSON metadata, not Markdown. Preserve maps
# while masking it; no declaration or heading is selected from source.
from version_basis import split_front_matter, START, END, NOTE_TITLE
from version_basis import split_front_matter, START, END, NOTE_TITLE, mask_top
if enrolled:
text = mask_top(text)
text = text.replace("\r\n", "\n").replace("\r", "\n")
try:
data, body = split_front_matter(text)
Expand Down
1 change: 1 addition & 0 deletions tools/check_verify_safety.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ def logical_lines(text, guide="GUIDE.md"):
try:
section = vb.eof_section(text)
if vb.updated(text, guide) == text:
text = vb.mask_top(text)
text = text[:text.index(section)]
except (OSError, ValueError, TypeError, KeyError):
pass
Expand Down
7 changes: 7 additions & 0 deletions tools/run_all_checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,13 @@ heading_slugs() {
import re, sys

text = open(sys.argv[1], encoding="utf-8").read()
sys.path.insert(0, "tools")
from version_basis import PROMPT_ANCHOR, mask_top
try:
if mask_top(text, sys.argv[1]) != text:
print(PROMPT_ANCHOR)
except (ValueError, TypeError, KeyError):
pass # Invalid generated content cannot supply the prompt anchor.
text = re.sub(r"<!--.*?-->", "", text, flags=re.S)

in_fence = False
Expand Down
2 changes: 1 addition & 1 deletion tools/test_verify_marking.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def run(body, expected, name, message="", *, unit_expected=None):

# The EOF exception admits only the fixed H2 and the two data fences.
from version_basis import NOTE_TITLE, note
ai = (START + '\n' + note('guide.md') + '\n\n**Version basis**\n\n'
ai = (START + '\n' + note('guide.md', historical=True) + '\n\n**Version basis**\n\n'
'````json\n{}\n````\n' + END + '\n')
bottom = marked_pilot + '\n\n' + ai
run(bottom, 0, 'EOF note ends Verify without adding units')
Expand Down
Loading
Loading