Skip to content

Commit e91c9da

Browse files
astrapi69astrapisixtynineclaude
authored
chore(tooling): bring template-owned files in line with the template (#33)
Copies every file .github/ownership.json gives to the template byte for byte from adaptive-learner-content-template (0b2971a): the dictionary-based prose gate, quality decisions read from .github/quality-state.json, the shared adopted ext: list with its nightly comparison against the app, and the nightly ownership check. .github/quality-state.json records the prose backlog this repository carries (non-blocking, with date and reason); the gate's self-test stays blocking. No lesson, manifest or pin changes. The search index files are owned by the hub and follow once its generator and the template's are merged there. Co-authored-by: Asterios Raptis <asteri.raptis@gmail.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent f7119ed commit e91c9da

23 files changed

Lines changed: 4453 additions & 255 deletions

‎.github/ownership.json‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
{
2+
"description": "Which side owns each path of a content repository. Read by scripts/check_ownership.py (see its docstring for the owner kinds); the Ownership workflow runs it nightly in every repository.",
3+
"owners": {
4+
"template": "astrapi69/adaptive-learner-content-template",
5+
"hub": "astrapi69/adaptive-learner-content"
6+
},
7+
"paths": {
8+
".github/ownership.json": "template",
9+
".github/workflows/engine-currency.yml": "template",
10+
".github/workflows/engine-validate.yml": "template",
11+
".github/workflows/ownership.yml": "template",
12+
".github/workflows/registry-drift.yml": "template",
13+
".github/workflows/schema-drift.yml": "template",
14+
".github/workflows/stable-ids.yml": "template",
15+
".github/workflows/umlaut-data.yml": "template",
16+
".github/workflows/validate-content.yml": "template",
17+
".gitignore": "template",
18+
"Makefile": "template",
19+
"docs/LESSON-FORMAT.md": "template",
20+
"docs/ai-review-prompt-template.md": "template",
21+
"generated/README.md": "template",
22+
"requirements.txt": "template",
23+
"schema/README.md": "template",
24+
"scripts/adopted-extensions.mjs": "template",
25+
"scripts/audit_content.py": "template",
26+
"scripts/build_umlaut_stems.py": "template",
27+
"scripts/check_adopted_extensions.mjs": "template",
28+
"scripts/check_ownership.py": "template",
29+
"scripts/check_prose.py": "template",
30+
"scripts/check_schema_drift.py": "template",
31+
"scripts/export_anki.py": "template",
32+
"scripts/export_quiz_pdf.py": "template",
33+
"scripts/export_set.py": "template",
34+
"scripts/generate_exercises.py": "template",
35+
"scripts/lce_schema.py": "template",
36+
"scripts/quality_state.py": "template",
37+
"scripts/umlaut_stems.json": "template",
38+
"scripts/validate_content.py": "template",
39+
"scripts/validate_with_engine.mjs": "template",
40+
"templates/README.md": "template",
41+
"templates/knowledge/lesson.json": "template",
42+
"templates/language/lesson.json": "template",
43+
"templates/programming/lesson.json": "template",
44+
"tests/fixtures/graded-quiz-sample.json": "template",
45+
"tests/fixtures/lesson-shape-parity.json": "template",
46+
"tests/test_audit_content.py": "template",
47+
"tests/test_check_ownership.py": "template",
48+
"tests/test_check_prose.py": "template",
49+
"tests/test_check_schema_drift.py": "template",
50+
"tests/test_export_anki.py": "template",
51+
"tests/test_export_quiz_pdf.py": "template",
52+
"tests/test_from_cards_pairs_gate.py": "template",
53+
"tests/test_generate_exercises.py": "template",
54+
"tests/test_hint_length_rule.py": "template",
55+
"tests/test_mc_only_exemption.py": "template",
56+
"tests/test_quality_state.py": "template",
57+
"tests/test_shape_parity.py": "template",
58+
".github/workflows/generate-index.yml": "hub",
59+
"schema/search-index.schema.json": "hub",
60+
"scripts/generate_search_index.py": "hub",
61+
"tests/test_generate_search_index.py": "hub",
62+
"tests/test_search_index_ai_validated.py": "hub",
63+
"tests/test_search_index_review_status.py": "hub",
64+
"tests/test_search_index_schema.py": "hub",
65+
"tests/test_search_index_updated_at.py": "hub",
66+
"tests/test_search_index_visibility.py": "hub",
67+
".github/workflows/ai-review.yml": "hub-only",
68+
".github/workflows/validate-registered-repos.yml": "hub-only",
69+
"CONTRIBUTING.md": "hub-only",
70+
"docs/CROSS-REPO-SEARCH.md": "hub-only",
71+
"docs/REGISTER-A-REPO.md": "hub-only",
72+
"examples/inception-effekt/README.md": "hub-only",
73+
"examples/inception-effekt/lesson.json": "hub-only",
74+
"recommended-repos.json": "hub-only",
75+
"schema/recommended-repos.schema.json": "hub-only",
76+
"scripts/ai_review.py": "hub-only",
77+
"scripts/validate_registered_repo.py": "hub-only",
78+
"scripts/validate_registry.py": "hub-only",
79+
"tests/test_ai_review.py": "hub-only",
80+
"tests/test_validate_registry.py": "hub-only",
81+
".github/quality-state.json": "repo",
82+
"LICENSE": "repo",
83+
"README.md": "repo",
84+
"books.yaml": "repo",
85+
"docs/GETTING-STARTED.md": "repo",
86+
"docs/export-set-usage.de.md": "repo",
87+
"docs/export-set-usage.md": "repo",
88+
"docs/review/*": "repo",
89+
"manifest.yaml": "repo",
90+
"media.yaml": "repo",
91+
"schema/engine-version.txt": "repo",
92+
"schema/stable-id-coverage.txt": "repo",
93+
"search-index.json": "repo",
94+
"sets/*": "repo",
95+
"tests/test_export_set.py": "repo",
96+
"tests/test_known_domain.py": "repo",
97+
"tests/test_prompt_duplication_rule.py": "repo",
98+
"tests/test_readme_set_table.py": "repo",
99+
"schema/content-manifest.schema.json": "engine",
100+
"schema/lesson.schema.json": "engine",
101+
"schema/quality-rules.json": "engine"
102+
}
103+
}

‎.github/quality-state.json‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"prose_gate": {
3+
"blocking": false,
4+
"since": "2026-09-23",
5+
"reason": "Rückstand aus der Zeit vor dem Gate: 169 Em-Dashes und 19 Ersatzschreibungen für Umlaute in 33 Dateien, gemessen am 2026-09-23 mit den Wörterbuch-Daten des Templates (adaptive-learner-content-template#75). Der Rückstand liegt in Dateien, die dem Repo gehören (Lektionen, Manifeste, eigene Doku und Tests), und wird im Inhaltsdurchgang dieses Repos abgebaut; danach wird hier blocking auf true gesetzt."
6+
}
7+
}

‎.github/workflows/engine-validate.yml‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ jobs:
4444
# a newly pinned engine release becomes visible on its own: warnings
4545
# otherwise reach nobody unless someone runs `make lint-warnings` by
4646
# hand, so a pin bump changes a version number and nothing on screen.
47+
# The findings go into the job summary, readable without opening the
48+
# log.
4749
continue-on-error: true
4850
run: |
4951
# Keep the runner's exit code: it is 0 when it only finds warnings, so
@@ -54,14 +56,11 @@ jobs:
5456
status=$?
5557
set -e
5658
cat /tmp/lint-warnings.txt
59+
# Per rule, and for every warning the repository decided to keep
60+
# (.github/quality-state.json) the decision next to the number and a
61+
# comparison of the recorded elements with the current ones.
62+
python3 scripts/quality_state.py warnings /tmp/lint-warnings.txt >> "$GITHUB_STEP_SUMMARY"
5763
{
58-
echo "### Author lints (warnings, non-blocking)"
59-
echo
60-
echo "| rule | count |"
61-
echo "|---|---|"
62-
grep -oE '\[W-[A-Z0-9-]+\]' /tmp/lint-warnings.txt \
63-
| sort | uniq -c | sort -rn \
64-
| awk '{gsub(/[][]/, "", $2); print "| `" $2 "` | " $1 " |"}' || true
6564
echo
6665
echo "<details><summary>full list</summary>"
6766
echo

‎.github/workflows/ownership.yml‎

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Ownership: every copy of an owned file is its owner's bytes.
2+
#
3+
# .github/ownership.json names the owner of every path: the template (author
4+
# tooling), the hub adaptive-learner-content (search index and federation),
5+
# the hub alone (registry, AI review), the repository itself, or the engine
6+
# (the schema mirror, compared by schema-drift.yml). scripts/check_ownership.py
7+
# compares this repository's copies with the owners' main branches, using the
8+
# template's current ownership file.
9+
#
10+
# Like the registry drift, this does not belong in the PR gate: an owner moves
11+
# in its own repository, so a PR here would turn red for something this repo
12+
# did not do. It runs nightly, opens or updates one issue while copies differ,
13+
# and closes it when they agree again.
14+
name: Ownership
15+
16+
on:
17+
schedule:
18+
- cron: "15 6 * * *"
19+
workflow_dispatch:
20+
21+
permissions:
22+
contents: read
23+
issues: write
24+
25+
env:
26+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
27+
OWNERSHIP_LABEL: ownership-drift
28+
29+
jobs:
30+
ownership:
31+
runs-on: ubuntu-latest
32+
steps:
33+
- uses: actions/checkout@v5
34+
- uses: actions/checkout@v5
35+
with:
36+
repository: astrapi69/adaptive-learner-content-template
37+
ref: main
38+
path: .owners/template
39+
- uses: actions/checkout@v5
40+
with:
41+
repository: astrapi69/adaptive-learner-content
42+
ref: main
43+
path: .owners/hub
44+
- uses: actions/setup-python@v6
45+
with:
46+
python-version: "3.12"
47+
48+
- name: Compare with the owners
49+
id: compare
50+
run: |
51+
set +e
52+
python3 .owners/template/scripts/check_ownership.py \
53+
--template .owners/template --hub .owners/hub \
54+
--repository "$GITHUB_REPOSITORY" --root . > /tmp/ownership-report.txt 2>&1
55+
status=$?
56+
set -e
57+
cat /tmp/ownership-report.txt
58+
# 0 = in line, 1 = findings, anything else = the check itself is
59+
# broken (owner checkout or ownership file missing or malformed) and
60+
# must fail the job loudly.
61+
if [ "$status" -gt 1 ]; then exit "$status"; fi
62+
echo "drift=$([ "$status" -eq 1 ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
63+
{
64+
echo "### Ownership"
65+
echo
66+
echo '```'
67+
cat /tmp/ownership-report.txt
68+
echo '```'
69+
} >> "$GITHUB_STEP_SUMMARY"
70+
71+
- name: Open or update the ownership issue
72+
if: steps.compare.outputs.drift == 'true'
73+
env:
74+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
75+
run: |
76+
gh label create "$OWNERSHIP_LABEL" \
77+
--description "A copy of an owned file differs from its owner" \
78+
--color ededed 2>/dev/null || true
79+
title="chore(tooling): copies differ from their owners"
80+
body="$(cat <<EOF
81+
\`.github/ownership.json\` (the template's current copy) names the
82+
owner of every path. This repository's copies do not match:
83+
84+
\`\`\`
85+
$(cat /tmp/ownership-report.txt)
86+
\`\`\`
87+
88+
- \`differs from template\` / \`differs from hub\`: bring the copy in
89+
line with the owner, or, if this repository's version is the
90+
better one, move the change into the owner first.
91+
- \`missing\`: copy the owner's file here.
92+
- \`not in <owner>\`: the owner does not carry a file the ownership
93+
file gives it; fix the owner or the row.
94+
- \`only the hub carries it\`: remove it here, or give it a
95+
different owner.
96+
- \`no owner\`: add a row to \`.github/ownership.json\` in the
97+
template.
98+
99+
This issue is maintained by \`.github/workflows/ownership.yml\` and
100+
closes itself once the copies agree.
101+
EOF
102+
)"
103+
existing="$(gh issue list --label "$OWNERSHIP_LABEL" --state open --json number --jq '.[0].number')"
104+
if [ -n "$existing" ]; then
105+
gh issue edit "$existing" --title "$title" --body "$body"
106+
echo "updated issue #$existing" >> "$GITHUB_STEP_SUMMARY"
107+
else
108+
gh issue create --title "$title" --body "$body" --label "$OWNERSHIP_LABEL"
109+
fi
110+
111+
- name: Close the ownership issue once the copies agree
112+
if: steps.compare.outputs.drift == 'false'
113+
env:
114+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
115+
run: |
116+
existing="$(gh issue list --label "$OWNERSHIP_LABEL" --state open --json number --jq '.[0].number' 2>/dev/null || true)"
117+
if [ -n "$existing" ]; then
118+
gh issue close "$existing" \
119+
--comment "Every owned copy matches its owner again. Closed by the ownership check."
120+
fi

‎.github/workflows/umlaut-data.yml‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Umlaut data: the prose gate's umlaut check against the dictionaries.
2+
#
3+
# scripts/umlaut_stems.json is generated from the German and English
4+
# dictionaries (scripts/build_umlaut_stems.py) and committed; the PR gate only
5+
# reads it. The tests that hold the committed data and the real gate against
6+
# those dictionaries (every umlaut word found, no correct German or English
7+
# word flagged) need the dictionary packages, which the regular test step does
8+
# not install. They run here, only when the gate or its data changes, and with
9+
# REQUIRE_DICTIONARIES set a missing dictionary fails instead of skipping.
10+
name: Umlaut data
11+
12+
on:
13+
pull_request:
14+
paths:
15+
- scripts/check_prose.py
16+
- scripts/build_umlaut_stems.py
17+
- scripts/umlaut_stems.json
18+
- tests/test_check_prose.py
19+
- .github/workflows/umlaut-data.yml
20+
push:
21+
branches: [main]
22+
paths:
23+
- scripts/check_prose.py
24+
- scripts/build_umlaut_stems.py
25+
- scripts/umlaut_stems.json
26+
- tests/test_check_prose.py
27+
- .github/workflows/umlaut-data.yml
28+
29+
env:
30+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
31+
32+
jobs:
33+
dictionaries:
34+
runs-on: ubuntu-latest
35+
steps:
36+
- uses: actions/checkout@v5
37+
- uses: actions/setup-python@v6
38+
with:
39+
python-version: "3.12"
40+
- name: Install dictionaries
41+
run: sudo apt-get update && sudo apt-get install -y wngerman wamerican wbritish
42+
- name: Install test dependencies
43+
run: pip install "pytest>=8,<9" regex
44+
- name: Gate against the dictionaries
45+
env:
46+
REQUIRE_DICTIONARIES: "1"
47+
run: python -m pytest tests/test_check_prose.py -q

‎.github/workflows/validate-content.yml‎

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,36 +33,24 @@ jobs:
3333
- name: Install validator dependencies
3434
run: pip install "pyyaml>=6,<7" "jsonschema>=4,<5" "pytest>=8,<9" regex "genanki>=0.13,<0.14"
3535
- name: Prose gate self-test
36-
# Blocking even while the backlog below is open: a gate that does not
37-
# fire on known-bad input is broken, backlog or not.
36+
# Blocking always: a gate that does not fire on known-bad input is
37+
# broken, whatever the repository's backlog.
3838
run: python scripts/check_prose.py --self-test
39-
- name: Prose findings (non-blocking until the cleanup PR)
40-
# This repository still carries a backlog the gate reports: em dashes,
41-
# invisible characters, German written with letter pairs instead of
42-
# its umlauts. The per-repo cleanup PR works it off and removes
43-
# continue-on-error in the same commit; from then on a regression
44-
# fails the PR. Until then the step shows red and the job stays green.
45-
continue-on-error: true
39+
- name: Prose gate (em dash, invisible characters, substituted umlauts)
40+
# Blocking unless the repository records a backlog in its own
41+
# .github/quality-state.json (prose_gate.blocking: false, with a date
42+
# and a reason). The decision lives there and not in this file, so
43+
# this workflow stays identical in every repository. A broken gate
44+
# (any exit code other than 0 or 1) and a malformed state file always
45+
# fail. The summary lists the findings per file and ends with the
46+
# gate's totals.
4647
run: |
4748
set +e
4849
python -u scripts/check_prose.py > /tmp/prose.txt 2>&1
4950
status=$?
5051
set -e
5152
cat /tmp/prose.txt
52-
{
53-
echo "### Prose gate (non-blocking until the cleanup PR)"
54-
echo
55-
echo "| file | findings |"
56-
echo "|---|---|"
57-
grep -E '^[^ ].*:[0-9]+: ' /tmp/prose.txt | cut -d: -f1 | sort | uniq -c | sort -rn > /tmp/prose-files.txt || true
58-
head -50 /tmp/prose-files.txt | awk '{print "| `" $2 "` | " $1 " |"}'
59-
# No silent cap: say how many files the table leaves out.
60-
more=$(( $(wc -l < /tmp/prose-files.txt) - 50 ))
61-
if [ "$more" -gt 0 ]; then echo "| ... and $more more file(s) | |"; fi
62-
echo
63-
grep -E '^(PROSE GATE|prose gate):' /tmp/prose.txt || true
64-
} >> "$GITHUB_STEP_SUMMARY"
65-
exit $status
53+
python scripts/quality_state.py prose --status "$status" --report /tmp/prose.txt >> "$GITHUB_STEP_SUMMARY"
6654
- name: Validate content tree
6755
run: python scripts/validate_content.py
6856
- name: Repo test suite (shape parity #1208, drift-gate units)

‎Makefile‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Makefile für dein Adaptive-Learner-Content-Repo.
22
#
3-
# Ein Befehl genuegt zum Loslegen:
3+
# Ein Befehl genügt zum Loslegen:
44
#
55
# make validate Prüft deine Inhalte (legt beim ersten Mal automatisch
66
# eine lokale Python-Umgebung an, du musst nichts installieren).
@@ -40,7 +40,7 @@ PIP := $(VENV)/bin/pip
4040
ENGINE_PIN := $(shell cat schema/engine-version.txt)
4141
ENGINE_STAMP := node_modules/.engine-$(ENGINE_PIN)
4242

43-
.PHONY: validate lint lint-warnings setup generate export export-anki audit clean help prose-check
43+
.PHONY: validate lint lint-warnings prose-check setup generate export export-anki audit clean help
4444

4545
help:
4646
@echo "make validate - Inhalte prüfen (richtet sich beim ersten Mal selbst ein)"

0 commit comments

Comments
 (0)