update doc structure

This commit is contained in:
2026-09-24 13:54:28 +08:00
parent 0d42dc5acc
commit 685b4135df
93 changed files with 190 additions and 12735 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
# Physical acceptance records
Start with [real-hardware setup](../../docs/hardware-setup.md) on the assembled Pi.
Then follow [the complete procedure](../../docs/developer/stress-testing.md), including bay
Start with [real-hardware setup](../../fds-os.wiki/Hardware-Setup.md) on the assembled Pi.
Then follow [the complete procedure](../../fds-os.wiki/Developer-Stress-Testing.md), including bay
calibration, repeatable device populations, explicit SAFE handling and separate
external timing measurements.
+6 -1
View File
@@ -28,7 +28,12 @@ for tool in ['fds-cartridge','fds-emulator']:
output=subprocess.check_output([str(root/'usr/bin'/tool),'--version'],text=True).strip()
assert output==f'{tool} {version}',output
subprocess.run([str(root/'usr/bin'/tool),'--help'],check=True,stdout=subprocess.DEVNULL)
assert (root/'usr/share/doc/fds-tools/docs/workstation.md').is_file()
assert (root/'usr/share/doc/fds-tools/fds-os.wiki/Workstation.md').is_file()
manual = root/'usr/share/doc/fds-tools/fds-os.wiki'
for source in (project/'fds-os.wiki').iterdir():
if source.suffix in ('.md', '.png'):
assert (manual/source.name).read_bytes() == source.read_bytes(), source
assert not (manual/'.git').exists()
assert (root/'usr/share/licenses/fds-tools/RUST-NOTICES.txt').stat().st_size>0
installed=subprocess.check_output(['pacman','--config',str(config),'--root',str(root),'--dbpath',str(root/'var/lib/pacman'),'-Q','fds-tools'],text=True).strip()
assert installed==f'fds-tools {version}-1',installed
+7
View File
@@ -32,6 +32,13 @@ lock = {'format': 1, 'version': '0.1.0', 'void_commit': 'a' * 40, 'source_epoch'
snapshot = work / 'snapshot'
(snapshot / 'project').mkdir(parents=True)
(snapshot / 'project/source.txt').write_bytes(b'bounded input fixture\n')
# Freeze the real manual as ordinary files, never a pointer to this checkout's
# Git metadata. Archive round-trip and lock checks below cover these files too.
assert 'fds-os.wiki' in frozen.OWNED
frozen.copy(project / 'fds-os.wiki', snapshot / 'project/fds-os.wiki')
assert not (snapshot / 'project/fds-os.wiki/.git').exists()
for source in (project / 'fds-os.wiki').glob('*.md'):
assert (snapshot / 'project/fds-os.wiki' / source.name).read_bytes() == source.read_bytes()
(snapshot / 'temporary').mkdir()
(snapshot / 'temporary').chmod(0o1777)
(snapshot / 'project/executable').write_bytes(b'archive mode fixture\n')
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Validate the authoritative Gitea wiki without generating or publishing files."""
from html import unescape
from pathlib import Path
import re
import sys
from urllib.parse import unquote, urlsplit
PROJECT = Path(__file__).resolve().parents[2]
WIKI = PROJECT / 'fds-os.wiki'
SOURCE_URL = 'http://gitea.home.arpa/felis/fds-os/src/branch/main/'
WIKI_URL = 'http://gitea.home.arpa/felis/fds-os/wiki/'
FENCE = re.compile(r'^ {0,3}(`{3,}|~{3,})(.*)$')
LINK = re.compile(r'!?\[[^\]]*\]\((<[^>\n]+>|[^\s)]+)(?:\s+["\'][^\n]*?["\'])?\)')
REFERENCE = re.compile(r'^ {0,3}\[[^\]\n]+\]:[ \t]*(<[^>\n]+>|\S+)', re.M)
def prose(text):
lines, fence = [], None
for line in text.splitlines(keepends=True):
found = FENCE.match(line)
if fence:
if found and found[1][0] == fence[0] and len(found[1]) >= len(fence) and not found[2].strip():
fence = None
continue
if found:
fence = found[1]
elif not line.startswith((' ', '\t')):
lines.append(line)
return ''.join(lines)
def anchors(text):
result, counts = set(), {}
for heading in re.findall(r'^#{1,6}\s+(.+?)\s*#*$', prose(text), re.M):
heading = re.sub(r'\[([^\]]+)\]\([^)]*\)', r'\1', heading)
heading = re.sub(r'<[^>]*>', '', heading)
slug = re.sub(r'[^\w\- ]', '', unescape(heading).lower()).replace(' ', '-')
count = counts.get(slug, 0)
counts[slug] = count + 1
result.add(slug + (f'-{count}' if count else ''))
return result
def links(text):
text = re.sub(r'(`+)(?!`)[^\n]*?(?<!`)\1(?!`)', '', prose(text))
return [m[1].strip('<>') for pattern in (LINK, REFERENCE) for m in pattern.finditer(text)]
def main():
if not (WIKI / 'Home.md').is_file():
sys.exit('ERROR: missing wiki; run git submodule update --init fds-os.wiki')
pages = {p.stem: p for p in WIKI.glob('*.md')}
errors = []
require = lambda condition, message: errors.append(message) if not condition else None
require(len({name.casefold() for name in pages}) == len(pages), 'Page names collide ignoring case')
for name in ('Home', 'Overview', 'Page-Index', '_Sidebar', '_Footer'):
require(name in pages, f'Missing required page: {name}.md')
for name, page in pages.items():
require(bool(re.fullmatch(r'(?:[A-Z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*|_Sidebar|_Footer)', name)),
f'Invalid page name: {page.name}')
if not name.startswith('_'):
require(page.read_text().startswith('---\ninclude_toc: true\n---\n'),
f'{page.name}: missing table-of-contents frontmatter')
page_anchors = {name: anchors(page.read_text()) for name, page in pages.items()}
index_links = set(links(pages['Page-Index'].read_text())) if 'Page-Index' in pages else set()
for name in pages.keys() - {'Page-Index', '_Sidebar', '_Footer'}:
require(name in index_links, f'{name}.md is missing from Page-Index.md')
checked = 0
for page in [*pages.values(), PROJECT / 'README.md']:
for target in links(page.read_text()):
url = urlsplit(target)
if target.startswith(SOURCE_URL):
relative = unquote(url.path.removeprefix(urlsplit(SOURCE_URL).path))
path = (PROJECT / relative).resolve()
require(path.is_relative_to(PROJECT) and path.is_file(),
f'{page.name}: missing source file: {target}')
checked += 1
continue
if target.startswith(WIKI_URL):
url = urlsplit(target[len(WIKI_URL):])
elif url.scheme or url.netloc:
continue
name = unquote(url.path)
if name:
require('/' not in name and name != '..', f'{page.name}: non-flat wiki link: {target}')
if Path(name).suffix:
require(Path(name).suffix != '.md', f'{page.name}: wiki page links must omit .md: {target}')
require((WIKI / name).is_file(), f'{page.name}: missing asset: {target}')
checked += 1
continue
else:
name = page.stem
require(name in pages, f'{page.name}: missing wiki page: {target}')
if url.fragment:
require(unquote(url.fragment) in page_anchors.get(name, set()),
f'{page.name}: missing section: {target}')
checked += 1
if errors:
sys.exit('\n'.join('ERROR: ' + message for message in errors))
print(f'PASS: {len(pages)} wiki pages; {checked} page, section, asset and source links')
if __name__ == '__main__':
main()