parish-create-service-sheet
Publish the weekly service sheet (the "pořad bohoslužeb" or "ohlášky") on the new farakurim.cz site from the parish's DOCX of the year or its weekly PDF - extract the week's days, services and announc
SKILL.md.claude/skills/parish-create-service-sheet/SKILL.md
GitHubCreate a service sheet
The parish's weekly service sheet ("ROZPIS BOHOSLUŽEB V TÝDNU od … do …") becomes structured content: one sheet,
added by pnpm stage sheet. It can be published any day before its week: the site shows each sheet from its
validFrom (the newer one on the Sunday two sheets share), and the script removes the sheets before the current one.
Finish with parish-publish-content.
1. Read the source
The user gives at least a file name; without a folder, look in ~/Downloads/. The source is one of:
- The DOCX ("Pořad bohoslužeb 2026.docx", preferred): the parish's Word file with every week of the year, the
newest first. The script takes its newest week (
--fromand--topick another), drafts the record from the week's table and announcements, and stages that week's page converted to PDF (Word on Windows, else LibreOffice). The DOCX holds a year of intentions with names: it never goes touploads/or the repo, only the week's page does. - A PDF of the week (page 1 exported by the user), when there is no DOCX or no converter.
From the DOCX, draft the record (it also refuses a week already released, before the current sheet or
overlapping another, and says when the week's sheet is already in service-sheets.json: then the week was staged
before, so take it to parish-correct-service-sheet):
pnpm stage sheet "<source>" --draft <scratchpad>/record.json
It prints the week, its days and the events an announcement can link (for newsId), and writes days and
announcements to record.json. The days a two-week table
leaves out become days without services, printed as "Days without services": tell the user. A table it cannot read (a
date outside the week, a weekday that does not fit the date, times that do not pair with the services, a table that
does not start and end on the heading's dates) stops it with the week and day: ask the user what the document should
say, as the error is in their document. The draft is a starting point: step 2a says what to check in it.
From a PDF, read it with both:
- the
Readtool, to see the table (which rows belong to which day and place), and pdftotext -layout <file> -, for exact wording. In the layout text a day's date and feast sit in the middle of its rows and long intentions wrap onto the next line: use the rendered page to assign them.
Read the whole page: the announcements are the paragraphs below the table.
Get the week, its dates and the events for newsId (it refuses and names a week the same way as --draft):
pnpm stage sheet "<source>" --check
2. Extract the sheet
The sheet is record.json in the session's scratchpad (never in the repo) with days and announcements only:
validFrom and validTo come from the heading, and the script sets rev (the PDF's version, with --corrected).
Follow 2a or 2b, then check the record against the fields below.
Week: the week --draft or --check printed from the heading (not part of the record): enter every date of it
in days. A heading can cover two weeks (around Christmas, Easter or a holiday): it is still one
ServiceSheet with every day of both weeks, never split into two. Everything below that says "the week" means the
sheet's whole period.
2a. From the DOCX: fix the draft
--draft already wrote record.json. What it gets right: every date of the week (the days the table leaves out
without rows), the rows in time order, time as "8:00", place as the parish church's id, the title / detail
split at " – ", the announcements word for word with Word's bold and italic kept and addresses linked. What it
guesses or leaves out, so check each against the fields below and fix record.json in place:
html: one announcement per empty paragraph in the document, so the parish's formatting can split or clutter one. Fix each to read as one notice:- Merge what belongs together: a lead-in ending with a colon ("Svatební ohlášky:", "Zpovídá se:") with its items and closing line, and a letter (greeting, paragraphs, signature) into one announcement.
- Turn lines that list items ("▪ 1. a 2. třída: …", "– ve čtvrtek 3. 6. …", one confession time per line) into
<ul><li>…</li></ul>after the lead-in's<p>, without the bullet characters. - Drop paragraphs without words ("%") and editorial markers ("[V Kuřimi:]"), keeping the wording otherwise.
category: guessed from keywords,infowhen none fits. Check every announcement.mass: set for "mše" or "vigilie" in the title, in any place ("Mše sv. – pohřeb …" too). Check a mass named otherwise.service: set for "adorace", "modlit…", "novéna" or "obřady" anywhere in the title, and for any row with "bohoslužba" or "pobožnost", in any place. Add it to any other mass-like public service.feast: lowercased from capitals, keeping the first word, saints' names after "sv." and names such as Páně, Boží, Panny Marie. A cell's names are joined by " – " ("Uvedení Páně do chrámu – Hromnice"), and so are a day's feasts from its later rows ("Slavnost posvěcení kostela – 28. neděle v mezidobí"); a wrapped name is guessed from the lines' case. Fix an unusual feast's case or split.solemnity: set for Sundays, a bold feast and "slavnost…". Add a solemnity the document did not make bold.<strong>: only where the parish made the text bold. Add it to key dates and times that are not.newsId: never set. Add it where an event matches (see "FindingnewsId" below).
2b. From a PDF: write the record
Write record.json from scratch with the fields below, from the rendered page and the pdftotext wording:
- Assign each row to its day and place from the rendered page: in the layout text the date and feast sit in the middle of the day's rows.
- Join the wrapped lines of a long intention into one
detail. - Enter every day of the week, a day without services with
"rows": []. - Copy the announcements below the table word for word, one per notice (grouped and listed as 2a's
htmlsays), and add their<strong>, links andnewsIdyourself.
The fields
Days (days[], one per date, in order):
| Field | How to fill it |
|---|---|
date |
ISO date of the row's "30. 11." |
feast |
The feast in sentence case: "1. NEDĚLE ADVENTNÍ" → "1. neděle adventní", "sv. František Xaverský". |
solemnity |
true for Sundays and solemnities ("Slavnost …"). |
rows |
The day's services in time order. |
Rows (SheetRow):
| Field | How to fill it |
|---|---|
time |
"8:00", "17:30". |
place |
A parish church's id (places in masses.ts: "kurim", …); free text elsewhere ("Vranov"). |
title |
The part before " – ": "Mše sv.", "Mše sv. se zpěvem scholy", "Adorace", "Pohřeb". |
detail |
The part after " – ", word for word, wrapped lines joined. |
mass |
true for a mass in any place; the countdown ("Nejbližší mše svatá") counts only the parish churches'. |
service |
true for a mass-like public service in any place (below); shown as "Nejbližší bohoslužba". |
changed |
Omit (a mass outside the regular schedule is tagged "změna"); true only for a change the PDF marks. |
Mass-like services (service): "Adorace", "Modlitební večer", "Modlitba chval", "Modlitba sv. růžence",
"Novéna k Duchu svatému", "Májová pobožnost", any "bohoslužba" and "Velkopáteční obřady"; not "Pohřeb", "Křest dětí",
"Zpovídání" or "Křížová cesta".
Announcements (announcements[], word for word, in the PDF's order):
| Field | How to fill it |
|---|---|
category |
change ("nebude", "přesouvá"), confession ("zpovídá", "svátost smíření"), invitation ("zveme", "pouť", "koncert"), thanks ("děkujeme"), info (anything else: "sbírka"). |
html |
<p>…</p>, a list as <ul><li>; key dates and times in <strong>; e-mails as <a href="mailto:…">, web addresses as links. |
newsId |
The id of the matching event, if there is one (adds "Více v aktualitách"; finding it: below). |
Finding newsId: take it from the Events for newsId list step 1 printed (the events not ended two weeks
before the week, in start order): match an announcement of an event by its title, place or date (a pilgrimage on 19. 12. → pout-lukov-trebic-2026). With no clear match, leave newsId out: never guess, never search
src/content/news/ for an event the list lacks, and never add an event on the side (offer parish-create-event if
the user wants one). The script only checks that the id exists, not that it is the right event, so step 3 shows each
match.
Unfinished text in the source ("vynesl …………. Kč") goes to the user: ask for the value, never publish the dots.
3. Confirm with the user
Validate the record first: pnpm stage sheet "<source>" --record <scratchpad>/record.json --check. Then show the
week, a compact day-by-day list of rows, and the announcements with their categories and each newsId with its
event's title and date. Ask about anything uncertain: unreadable rows, cancellations, an announcement with no
matching event.
Wait for the user's yes or corrections to these before staging anything.
Unattended: write this into the report instead, see "Unattended run" below.
4. Stage the PDF and add the sheet
pnpm stage sheet "<source>" --record <scratchpad>/record.json
It validates the record first (the error names each field), stages the PDF, adds the sheet, removes the outdated sheets and runs the service sheet tests. A line it prints that asks the user something goes to the user as printed.
{
"days": [
{
"date": "2026-10-04",
"feast": "27. neděle v mezidobí",
"solemnity": true,
"rows": [
{ "time": "8:00", "place": "kurim", "title": "Mše sv.", "detail": "za Jana Nováka", "mass": true },
{ "time": "11:00", "place": "kurim", "title": "Hodová mše sv.", "detail": "za obec Česká", "mass": true }
]
}
],
"announcements": [
{ "category": "confession", "html": "<p>V Kuřimi se zpovídá ve čtvrtek od <strong>17.30</strong>.</p>" }
]
}
5. Publish
Follow parish-publish-content. Remind the user to mirror changed or cancelled services in the "Mše,
adorace" Google Calendar, which the Kalendář reads.
Unattended run
Only when the prompt says the run is unattended (the daily workflow gives the parish's DOCX and a report file); a local run follows steps 1 to 5 as written. An unattended run follows them too, with these changes:
- It handles the DOCX's newest week only. When
--draftdoes not say the week is already there: steps 1 to 4 as written (Outcome: created). When it does (never openservice-sheets.jsonto check): it is a correction, always, without comparing with the published sheet: draft the week, build the record from the draft as step 2a says (not from the published sheet), and stage it with--correctedwhen the script says the week is already published (Outcome: corrected). Never touch older weeks. - It never asks and never waits. Every "confirm with the user" or "ask the user" step (step 3, a document error, the
placeholder dots, the "Days without services" note) writes into the report instead; the report file is the one the
prompt gives. The scratchpad is the one the prompt gives (
record.json,draft.json; write nothing in the repo); the PDF is staged inuploads/as usual. - The prompt may give the previous report of the open PR (the run then works on that PR's branch). The new report
describes the whole PR, not only this run: copy over each previous week section whose week is in the PR
(
createdorcorrected) as it is, replace the section of the week this run handled, and drop previousrefusedordocument errorsections (those weeks are not in the PR). Keep the question ids and mark eachresolvedorstill open; number new questions after the highest id. - Write the report first, with each week's summary and questions, then stage (step 4,
--correctedwhen the script asks for it), then fill in each week'sOutcome:line from what the script did, and add any line it printed for the user as a question (blocking when its answer changes what is published, else a note).blocking:is final only after that. A week the script refuses (before the current sheet, overlapping another) or cannot draft (a document error) is not staged: itsOutcome:line carries the script's message. - Skip step 3's
--checkrun: step 4 validates the record first and stages nothing when it refuses, so fixrecord.jsonand run it again. - Skip step 5: no
parish-publish-content, no commit, no release. The workflow commits and opens the PR. - A question whose answer changes what is published (an unreadable row, placeholder dots, a doubtful cancellation) is blocking; the run still stages its best reading. For placeholder dots that is the record with the document's text, without the dots: never publish them. Any other question is a note.
The report is English, with site content quoted in Czech, and starts with exactly these two lines:
blocking: yes|no
commit: <message>
blocking: yes when any open question of the report is blocking. commit: covers every week in the PR (it is also
the PR's title, and the squash commit on main). One week: the message parish-publish-content prescribes,
feat(content): publish the service sheet for 19.–25. 10. 2026 for a new week, or
fix(content): correct the service sheet for … for a correction. Several weeks:
<type>(content): publish the service sheets for 19.–25. 10. and 26. 10.–1. 11. 2026 (with only corrections:
correct the service sheets for …); the type is feat if any week was created, else fix.
Then, per week, oldest first:
## <week>(e.g.## 19.–25. 10. 2026);- one line
Outcome: created,Outcome: corrected,Outcome: refused: <message>orOutcome: document error: <message>(the script's message as printed). The workflow fails the run on anyrefusedordocument error; - the announcements with categories and each
newsIdwith its event's title and date (not the rows: the Mirror section lists them); ### Questions, one per line:- **q1** (blocking|note) <where>: <question> — proposal: <proposal>. A later run on the same report keeps the existing ids and marks each questionresolvedorstill open.
Finally ## Mirror in the calendar: a list (Czech rows) of the services of every week in the report, to check against
the "Bohoslužby" calendar.
Common mistakes
- Missing the announcements below the table, or summarising them: they are published word for word.
- Assigning a row to the wrong day: the date sits in the middle of the day's rows in the layout text.
- Leaving a wrapped intention cut in half, or splitting "Mše sv. – za …" into the wrong
title/detail. - Leaving out a day or a mass of the week: during the sheet's week, what is not in
rowsdoes not take place. - Setting
changedby hand on a mass outside the regular schedule (it is computed), or to express a cancellation: leaving the mass out is enough. mass: trueon "Adorace" (that isservice: true).service: trueon "Pohřeb" or "Křest dětí": personal events never count for the countdown.- Publishing placeholder dots from an unfinished source.
- Putting
validFrom,validToorrevintorecord.json: the script reads the dates from the heading and setsrev. - Taking the DOCX draft as final: check every flag and category, and add the
<strong>andnewsIdit cannot know. - Staging the DOCX itself, or copying it anywhere in the repo: only the week's PDF page is published.