Skip to content

Commit 893a0d6

Browse files
committed
chore: merge develop into test (pipeline and scripts only, DOCS excluded)
2 parents 269c8ca + 51b6c3d commit 893a0d6

727 files changed

Lines changed: 3666 additions & 44442 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.Rprofile

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# CLMS Technical Library — local PDF preview font setup.
2+
#
3+
# Makes the bundled Typst fonts (_meta/theme/typst-fonts/) discoverable so that
4+
# rendering a DOCS/*.qmd to PDF — e.g. via the RStudio "Render" button — uses the
5+
# CLMS fonts (Lato, Liberation Sans, JetBrains Mono) without installing anything
6+
# system-wide. Quarto's typst format has no font-path option, so we set Typst's
7+
# native TYPST_FONT_PATHS env var here; RStudio runs this file on session start,
8+
# and the quarto render it launches inherits the variable.
9+
#
10+
# Preview only — the CI build (build-docs.sh) does not use this.
11+
local({
12+
font_dir <- normalizePath(
13+
file.path(getwd(), "_meta", "theme", "typst-fonts"),
14+
mustWork = FALSE
15+
)
16+
if (dir.exists(font_dir)) {
17+
existing <- Sys.getenv("TYPST_FONT_PATHS")
18+
paths <- if (nzchar(existing)) {
19+
strsplit(existing, .Platform$path.sep, fixed = TRUE)[[1]]
20+
} else {
21+
character(0)
22+
}
23+
if (!(font_dir %in% paths)) {
24+
Sys.setenv(
25+
TYPST_FONT_PATHS = paste(c(font_dir, paths), collapse = .Platform$path.sep)
26+
)
27+
message("CLMS: TYPST_FONT_PATHS set to bundled fonts for local PDF preview.")
28+
}
29+
}
30+
})

.github/non_browsable_doc_map.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@
1515
"source": "Copernicus_Land_Data_Store_CLDS/Survey_Evaluation_v1.qmd",
1616
"base": "dr3yejtzxollhygq0lwfgjxf755jvzyly2lemmvpgiqrzf5own2074qpdivug54c",
1717
"url": "/dr3yejtzxollhygq0lwfgjxf755jvzyly2lemmvpgiqrzf5own2074qpdivug54c.html"
18+
},
19+
{
20+
"source": "CDSE_Migration/CLMS_CDSE_Migration_Status.qmd",
21+
"base": "54ab2b07e82f82143780f24e2704ef211394c00216e83839e7c1dc42ba8e3a91",
22+
"url": "/54ab2b07e82f82143780f24e2704ef211394c00216e83839e7c1dc42ba8e3a91.html"
23+
},
24+
{
25+
"source": "CDSE_Migration/CLMS_CDSE_Migration_Dashboard.qmd",
26+
"base": "a3e44009d6b7375f60a58b11278cbe079d93407249c0c5e1c97f00e12e98c09a",
27+
"url": "/a3e44009d6b7375f60a58b11278cbe079d93407249c0c5e1c97f00e12e98c09a.html"
1828
}
1929
]
2030
}

.github/runners/Dockerfile.quarto-doc-builder

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ RUN arch=$(dpkg --print-architecture) && \
5555
# files.upload(file=), see gemini_client.py)
5656
# tiktoken -> token counting for the LLM rate limiter
5757
RUN pip install --no-cache-dir --break-system-packages \
58-
panflute PyYAML google-genai==2.6.0 tiktoken==0.13.0 && \
58+
panflute>=2.3.1 PyYAML google-genai==2.6.0 tiktoken==0.13.0 && \
5959
python3 -m pip cache purge 2>/dev/null || true && \
6060
rm -rf /root/.cache /tmp/* /var/tmp/*
6161

.github/scripts/ai/update_versions_and_changelogs.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,8 @@ def _process(sub_batch, _i=i):
861861

862862
def calculate_new_version(current_version, bump_type, major_from_filename):
863863
"""Calculate new version based on bump type"""
864+
# First published version is 1.0.0, so a _v0 filename floors to major 1.
865+
major_from_filename = max(major_from_filename, 1)
864866
try:
865867
parts = current_version.split(".")
866868
major = int(parts[0])
@@ -946,7 +948,7 @@ def initialize_first_release(all_files):
946948
print(f"[ERROR] {filepath}: {e}")
947949
continue
948950

949-
initial_version = f"{major_version}.0.0"
951+
initial_version = f"{max(major_version, 1)}.0.0"
950952

951953
update_qmd_version_only(filepath, initial_version)
952954

@@ -1072,7 +1074,7 @@ def main():
10721074
file_info[filepath] = {
10731075
"major_version": major_version,
10741076
"current_version": versions_metadata.get(filepath, {}).get(
1075-
"current_version", f"{major_version}.0.0"
1077+
"current_version", f"{max(major_version, 1)}.0.0"
10761078
),
10771079
}
10781080

.github/scripts/build/apply_cached_intros.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
SCRIPTS_ROOT = SCRIPT_DIR.parent
1616
sys.path.insert(0, str(SCRIPTS_ROOT))
1717

18-
from helpers.qmd_utils import find_qmd_files # noqa: E402
18+
from helpers.qmd_utils import find_qmd_files, read_qmd_frontmatter # noqa: E402
1919
from helpers.file_updater import apply_all_updates # noqa: E402
20+
from helpers.doc_types import element_off # noqa: E402
2021

2122
ROOT_DIR = (SCRIPT_DIR / "../../..").resolve()
2223
CACHE_DIR = (ROOT_DIR / ".llm_cache").resolve()
@@ -52,6 +53,20 @@ def main() -> int:
5253
print(f"[apply_cached_intros] no .qmd files under {input_dir}")
5354
return 0
5455

56+
# Skip types that take neither intro nor keywords (dashboard) so nothing is
57+
# injected, not even into the origin_DOCS copy. They come as one bundle,
58+
# hence the "both off" check — split it if a type ever wants only one.
59+
def _wants_intro(qmd: Path) -> bool:
60+
fm, _ = read_qmd_frontmatter(qmd)
61+
dtype = fm.get("type")
62+
return not (element_off(dtype, "keywords") and element_off(dtype, "description"))
63+
64+
kept = {q for q in qmd_files if _wants_intro(q)}
65+
skipped = len(qmd_files) - len(kept)
66+
if skipped:
67+
print(f"[apply_cached_intros] skipping {skipped} file(s) whose type omits intros/keywords")
68+
qmd_files = kept
69+
5570
stats = apply_all_updates(
5671
qmd_files,
5772
CACHE_DIR,

.github/scripts/build/build-docs.sh

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ step() {
3131
_STEP_PREV=$now; _STEP_NAME="$*"
3232
}
3333

34+
# Snapshot the pristine source before the build mutates it, so a local build is
35+
# easy to undo. After testing, restore your working tree with:
36+
# rm -rf DOCS origin_DOCS && mv source_DOCS DOCS
37+
rm -rf source_DOCS && cp -rp DOCS source_DOCS
38+
3439
# Apply cached intros/keywords before the rename - the cache is keyed by original path.
3540
echo "Injecting cached intros & keywords (no API)..."
3641
python3 .github/scripts/build/apply_cached_intros.py DOCS
@@ -73,6 +78,13 @@ python3 ../.github/scripts/build/fill_version.py .
7378
echo "Balancing table column widths..."
7479
python3 ../.github/scripts/qmd-tools/fix_table_colwidths.py .
7580

81+
# Promote bare "Table N:"/"Figure N:" captions (left as plain text by the
82+
# converters) into .tbl-caption divs / image alt text, so they render as styled
83+
# captions instead of body text. Runs after the grid->pipe conversion above so a
84+
# caption next to a (now pipe) table is recognised. Build-copy only; idempotent.
85+
echo "Promoting bare table/figure captions..."
86+
python3 ../.github/scripts/qmd-tools/promote_bare_captions.py .
87+
7688
# Bake image descriptions into the qmd source now, so the render doesn't re-hash
7789
# every image once per format (see the script). This was a Lua filter.
7890
echo "Baking image descriptions into qmd source..."
@@ -83,9 +95,12 @@ python3 ../.github/scripts/build/inject_image_descriptions.py .
8395
cp _quarto-no-headers.yml _quarto.yml
8496

8597
step "[3/6] Rendering all documents (HTML + Typst PDF + gfm) in one pass..."
86-
# Render every format in the config - html (site), typst (PDFs), gfm
87-
# (the .llms.md sidecars). One pass over the files instead of one per format.
98+
# # Temporary move out of docs before render to avoid Jupyter engine selections crashes
99+
# echo " [BUILD BYPASS] Temporarily moving Ice products out of the build context..."
100+
# mv products/products_Algorithm_theoretical_basis_document_-_High_Resolution_Ice_products_Europe.qmd ../origin_DOCS/
88101
quarto_render --no-clean
102+
# echo " [BUILD BYPASS] Restoring Ice products..."
103+
# mv ../origin_DOCS/products_Algorithm_theoretical_basis_document_-_High_Resolution_Ice_products_Europe.qmd products/
89104

90105
# Back up sitemap.xml and llms.txt - the index.qmd renders below regenerate
91106
# them, and we want to keep the values from this first render.
@@ -99,8 +114,10 @@ python3 ../.github/scripts/build/generate_index_all.py
99114
step "[5/6] Rendering index.qmd files..."
100115
mv _quarto.yml _quarto_not_used.yml
101116
mv _quarto-index.yml _quarto.yml
117+
# Serial (-P1): parallel renders raced on shared _site files (sitemap/search/
118+
# listings) and intermittently failed the whole build with xargs exit 123.
102119
find ./ -type f -name index.qmd -print0 | \
103-
xargs -0 -P4 -I{} \
120+
xargs -0 -P1 -I{} \
104121
bash -c 'quarto render "$1" --profile index --to html --no-clean --quiet 2>&1 | grep -v -e "Unknown meta key .* specified in a metadata Shortcode" -e "^Output created:"; exit ${PIPESTATUS[0]}' _ {}
105122
mv _quarto.yml _quarto-index.yml
106123
cp _quarto_not_used.yml _quarto.yml && rm _quarto_not_used.yml
@@ -110,6 +127,10 @@ cp _site/sitemap.xml.bkp _site/sitemap.xml
110127
rm -f _site/sitemap.xml.bkp
111128
mv _site/llms.txt.bkp _site/llms.txt
112129

130+
# Drop .llms.md for llms-off types (dashboard), before the llm sitemap so it
131+
# falls back to the HTML URL.
132+
python3 ../.github/scripts/build/strip_llms_sidecars.py . _site
133+
113134
# Remove non-browsable links from sitemap.xml and llms.txt
114135
python3 ../.github/scripts/build/remove_non_browsable.py _site/sitemap.xml
115136
python3 ../.github/scripts/build/remove_non_browsable.py --format llms _site/llms.txt
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env python3
2+
"""Strip residual block-level HTML wrappers from generated .llms.md sidecars.
3+
4+
The gfm writer + the simplify_tables_gfm.lua filter already linearize tables and
5+
remove styling, but Quarto injects crossref float wrappers (`<div id="tbl-…">` …
6+
`</div>`) and the occasional layout `<div style="overflow-x:…">` AFTER the pandoc
7+
filters run, so they can't be removed at filter level. These bare wrapper lines are
8+
pure noise in the plain-text companion the RAG ingests. This post-render pass removes
9+
standalone block-level `<div>`/`</div>` lines (inline tags like <sub>/<sup> in prose
10+
are left intact).
11+
12+
Idempotent. Usage:
13+
python3 clean_llms_md.py _site # walk *.llms.md under a directory
14+
python3 clean_llms_md.py a.llms.md b.md # specific files
15+
"""
16+
17+
import re
18+
import sys
19+
from pathlib import Path
20+
21+
# A line that is ONLY an opening/closing <div …> (optionally indented). Block-level
22+
# wrapper noise — never matches inline tags mid-prose.
23+
_BARE_DIV = re.compile(r"^[ \t]*</?div\b[^>]*>[ \t]*$")
24+
25+
26+
def clean_text(text: str) -> str:
27+
out = [ln for ln in text.split("\n") if not _BARE_DIV.match(ln)]
28+
cleaned = "\n".join(out)
29+
# collapse the blank-line runs a removed wrapper can leave behind (3+ → 2)
30+
cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
31+
return cleaned
32+
33+
34+
def clean_file(path: Path) -> bool:
35+
original = path.read_text(encoding="utf-8")
36+
cleaned = clean_text(original)
37+
if cleaned != original:
38+
path.write_text(cleaned, encoding="utf-8")
39+
return True
40+
return False
41+
42+
43+
def _iter_targets(args):
44+
for a in args:
45+
p = Path(a)
46+
if p.is_dir():
47+
yield from p.rglob("*.llms.md")
48+
elif p.exists():
49+
yield p
50+
51+
52+
def main(argv) -> int:
53+
if not argv:
54+
print(__doc__)
55+
return 1
56+
changed = 0
57+
for path in _iter_targets(argv):
58+
if clean_file(path):
59+
changed += 1
60+
print(f"clean_llms_md: cleaned {changed} file(s)")
61+
return 0
62+
63+
64+
if __name__ == "__main__":
65+
sys.exit(main(sys.argv[1:]))

.github/scripts/build/fill_version.py

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,22 @@
66
version, so we fill it each build, like intros and changelogs.
77
88
The value is the tracked version from .llm_cache/versions.json, looked up via the
9-
original-filename field. No record yet -> {major}.0.0 from the _vN.qmd name. No
9+
original-filename field. The doc's own `version:` frontmatter is never trusted as
10+
input - it is output we overwrite. No record yet -> seed one (max(major,1).0.0,
11+
so a _v0 or a name without _vN starts at 1.0.0) so the doc becomes tracked. No
1012
bump is computed here.
1113
"""
1214

1315
import argparse
16+
import json
1417
import re
1518
import sys
19+
from datetime import date
1620
from pathlib import Path
1721

1822
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # .github/scripts
1923
from helpers.json_io import load_json_or_empty
24+
from helpers.doc_types import element_off
2025

2126
VERSIONS_FILE = ".llm_cache/versions.json"
2227

@@ -68,7 +73,8 @@ def main():
6873
vf = Path(args.versions_file) if args.versions_file else repo_root / VERSIONS_FILE
6974
versions = load_json_or_empty(vf, label="versions")
7075

71-
filled = baseline = 0
76+
filled = baseline = seeded = 0
77+
today = date.today().isoformat()
7278
for qmd in sorted(Path(args.docs_dir).rglob("*.qmd")):
7379
if {"_site", ".quarto", "_meta"} & set(qmd.parts):
7480
continue
@@ -78,32 +84,54 @@ def main():
7884
continue
7985
_, end = bounds
8086

81-
src = fm_value(lines, end, "original-filename")
82-
major = major_from_name(src) if src else None
83-
if major is None:
84-
major = major_from_name(qmd.name)
85-
if major is None:
86-
print(f"[fill_version] {qmd}: no _vN in filename, skipping")
87+
# version-off types keep their own `version:` header, if any; don't touch.
88+
if element_off(fm_value(lines, end, "type"), "version"):
8789
continue
8890

91+
src = fm_value(lines, end, "original-filename")
92+
mj = major_from_name(src) if src else None
93+
if mj is None:
94+
mj = major_from_name(qmd.name)
95+
# First published version is 1.0.0: a _v0 or a name without _vN starts
96+
# at 1.0.0, while _v4 etc. keep their filename major.
97+
baseline_major = max(mj, 1) if mj is not None else 1
98+
99+
key = f"DOCS/{src}" if src else None
89100
tracked = None
90-
if src:
91-
entry = versions.get(f"DOCS/{src}") or versions.get(src)
101+
if key:
102+
entry = versions.get(key) or versions.get(src)
92103
tracked = (entry or {}).get("current_version")
93104

94-
if tracked and tracked.split(".")[0] == str(major):
105+
if tracked and (mj is None or tracked.split(".")[0] == str(mj)):
95106
version = tracked
96107
else:
97-
version = f"{major}.0.0"
108+
version = f"{baseline_major}.0.0"
98109
baseline += 1
110+
# No record yet: seed one so the doc becomes tracked, rather than
111+
# re-deriving the fallback every build. Keyed by original-filename.
112+
if key and key not in versions:
113+
versions[key] = {
114+
"current_version": version,
115+
"last_bump": "initial",
116+
"last_bump_reason": "First release",
117+
"last_release_tag": "initial",
118+
"last_updated": today,
119+
"major_from_filename": baseline_major,
120+
}
121+
seeded += 1
99122

100123
if set_version(lines, end, version):
101124
qmd.write_text("".join(lines), encoding="utf-8")
102125
filled += 1
103126

127+
if seeded:
128+
vf.parent.mkdir(parents=True, exist_ok=True)
129+
with vf.open("w", encoding="utf-8") as f:
130+
json.dump(versions, f, indent=2, sort_keys=True)
131+
104132
print(
105133
f"[fill_version] set version on {filled} files "
106-
f"({baseline} fell back to {{major}}.0.0)"
134+
f"({baseline} used max(major,1).0.0 baseline, {seeded} new cache entries)"
107135
)
108136

109137

0 commit comments

Comments
 (0)