mht 26.10.5
-
vignette("lmed-v20261002")opens with suggested methods text for the exposure section of an article. Every number in it is measured fromadd_lmed_v20261002()or read from its two workbooks when the vignette builds, and a check stops the build where the function disagrees with a case the prose names. It cites the register, the national MHT guidelines behind the progestogen thresholds, and the gap-filling method. A list below it names what the study states itself: the new-user washout, the approach and variant analysed, and any follow-up limit. It links to the manuscript methods ofsweregfor the trial emulation design. The function and its workbooks are unchanged.
mht 26.10.2
-
add_lmed_v20261002()is the 2026-10-02 entry point. It readsproduct_table_20261002.xlsx, a byte copy of the 2026-09-24 table, anddataDictionary20261002.xlsx, and no other workbook. It writes every column thatadd_lmed_v20260924()writes, and addsapproach5,rd_approach5_singleandrd_approach5_multiple. -
Approach 5 pools the oestrogen routes of approach 4 and gives the hormonal IUD (
E1) its own level. Its levels areestrogen_progesterone,estrogen_other_progestogen,estrogen_iud,estrogen_only,tiboloneandlocal_or_none_mht. The codebook adds 47 approach 5 rules to thepost_groupingsheet. -
The resolver of
add_lmed_v20260924()is unchanged. When two approach 5 levels are lit in one week,approach5reports the level that started most recently. An oestrogen week withE1and another progestogen lights two levels. -
vignette("lmed-v20261002")is the one vignette.vignette("lmed-v20260924")moved toarchive/in the source repository. Its approach 5 section checks each approach 5 rule, and the category of each product it names, against the workbooks. The website’s “Get started” button opens it. - Approaches 1 to 4 and their 135 rules are unchanged.
add_lmed_v20260924()and its two workbooks are unchanged.
mht 26.9.29
-
The published product tables no longer show a count from 1 to 4. In
product_table_20260902.xlsx,product_table_20260922.xlsxandproduct_table_20260924.xlsx, each such count in thepeople_*andprescriptions_*columns now reads<5: 53 cells on sheetGand 168 on sheetother. Two notes that quoted a small count are reworded. No function reads these columns, so every entry point returns what it returned before. -
The README sheet of each product table describes the right column. From
exclude_entire_persononward, each description sat against the wrong field. -
The vignettes of
add_lmed_v20260902()andadd_lmed_v20260922()moved toarchive/in the source repository. They are no longer built or on the website.vignette("lmed-v20260924")is the one vignette. -
The website’s “Get started” button opens
vignette("lmed-v20260924"). It pointed to the reference page ofadd_lmed_v20250909().
mht 26.9.28
-
vignette("lmed-v20260924")states why the function bridges a gap of up to four weeks, and what a gap becomes. A new case shows a route switch after a 160-week episode, and a table covers each kind of gap. The function and its workbooks are unchanged.
mht 26.9.27
-
vignette("lmed-v20260924")shows how a short gap during a route switch is handled in approaches 2, 3 and 4. The function and its workbooks are unchanged.
mht 26.9.26
-
vignette("lmed-v20260924")shows each behaviour it states. New worked cases cover name lookup, unknown products, table validation, week boundaries, refills, missing quantities and dates, skeleton errors, the four-week bridge, same-week starts, the 156-week cutoff, approach 3b, flag reasons and the workbook sheets the function reads. Each case has astopifnot()check. The approach 1 to 4 rule descriptions are checked against categories read from the workbooks.
mht 26.9.25
-
vignette("lmed-v20260924")now stands alone. It states every step ofadd_lmed_v20260924(), in the structure ofvignette("lmed-v20260922"), with approach 4 added to the sections on treatment levels and exposure variables. Its tables and checks read the two 2026-09-24 workbooks. The function and its workbooks are unchanged.
mht 26.9.24
-
add_lmed_v20260924()is the 2026-09-24 entry point. It readsproduct_table_20260924.xlsx, the 2026-09-22 table with one note cell reworded, anddataDictionary20260924.xlsx, and no other workbook. It writes every column thatadd_lmed_v20260922()writes, and addsapproach4,rd_approach4_singleandrd_approach4_multiple. -
Approach 4 splits systemic oestrogen by route and by progestogen type. Its levels are
oral_estrogen_progesterone,oral_estrogen_other_progestogen,transdermal_estrogen_progesterone,transdermal_estrogen_other_progestogen,estrogen_only,tiboloneandlocal_or_none_mht. The codebook adds 47 approach 4 rules to thepost_groupingsheet. -
The progesterone levels read
C1(progesterone) orC5(dydrogesterone), dispensed separately or inB4andB11. Every other progestogen that approach 4 reads is “other”, including contraceptive progestogens and hormonal IUDs. A progestogen alone lights no level. -
The resolver of
add_lmed_v20260922()is unchanged. When two approach 4 levels are lit in one week,approach4reports the level that started most recently. Only two levels that start in the same week are a clash. - Approaches 1 to 3 and their 88 rules are unchanged.
add_lmed_v20260922(), its two workbooks and its vignette are unchanged. -
vignette("lmed-v20260924")explains approach 4. Itsstopifnot()chunks check the rules of each level and each worked case, so the vignette fails to build if a claim is false.
mht 26.9.23
-
add_lmed_v20260922()is the 2026-09-22 entry point. It readsproduct_table_20260922.xlsxanddataDictionary20260922.xlsx, and no other workbook. Its output equals that ofadd_lmed_v20260902(), except for the two changes below. -
Estradiol Valerateis categoryA7, an injected oestrogen. The 2026-09-02 table reads it asnotmht. A woman with any dispensing of it getsri_mht_excluded_product = TRUE, with the reasonpossible gender-affirming therapy. -
No
post_groupingrule readsA7. The codebook drops the 14 rules thatA7lit, and removesA7from the exclusions of 3 inert rules. 88 rules remain. A week ofA7alone islocal_or_none_mhtin every approach, and theA7column still marks it. -
A flagged woman keeps her exposure columns. The function removes nobody. A study removes her only through an exclusion that it names, which reads
ri_mht_excluded_product. -
vignette("lmed-v20260922")states whatadd_lmed_v20260922()does. Itsstopifnot()chunks check both changes on the shipped workbooks and the fixture, so the vignette fails to build if a claim is false. -
add_lmed_v20260902(), its two workbooks and its vignette are unchanged.
mht 26.9.3
-
The skeleton MUST hold weekly rows only, and both 2026 entry points now say so. A skeleton from
swereg::create_skeleton()also carries one annual row per person per year, markedis_isoyear == TRUE, with anisoyearweeksuch as"1946-**". That value is no ISO week, so the run stops. Passskeleton[is_isoyear == FALSE], and fill the annual rows in the caller. - A new test pins that contract. An annual row stops the run, and the weekly subset runs. A later dated entry point that accepts an annual row has to change the test to do so.
-
add_lmed_v20250909()has no such check, and reads an annual row as a week. Its gap bridging flips anyFALSErun of four rows or fewer toTRUE. A person with only a handful of rows could then read as on systemic MHT, with no prescription at all. The 2026-08-28 bridge needs a treated week on each side, which already prevents it. - Documentation and one test. No behaviour changed.
mht 26.9.2
-
add_lmed_v20260902()is the 2026-09-02 entry point. A product gets its category from a shipped table, looked up on an exact key, rather than from a 116-rung prefix ladder. It also writes two person-level exclusion columns that no earlier entry point wrote. Everything else is the 2026-08-28 behaviour. -
inst/2023-mht/product_table_20260902.xlsxships, and the 2026-09-02 function reads it. One row per raw register product name across both LMED deliveries, with the ATC, the dosage form, the route, the classification, its meaning in English, and where the classification came from. - A delivered product with no row is an ERROR. The run stops and names the products, and it checks the delivery BEFORE restricting to the people in the skeleton, so a product held only by people outside the cohort still has to be one somebody has ruled on. An unclassified product reads as no MHT, so the woman enters as an unexposed control.
- The match is exact on the NORMALISED name, which is not the same as exact on the raw name. Normalisation keeps the lowercase ASCII letters and drops everything else, so a raw spelling differing only in digits or punctuation collapses onto an existing key and is accepted rather than reported. That is deliberate, because it is what makes three strengths one product; it also means the check cannot prove every raw spelling has its own reviewed row.
-
Several raw names may share one lookup key, and rows sharing a key MUST agree on the classification, on the exclusion flag and on its reason.
Estramon 25,Estramon 75andEstramon 100are one product at three strengths. The lookup matches the key and cannot see which raw name produced it. -
Every cell of the table is validated. A classification must be a category the grid materialises, one the frozen ladder returns, or
notmht; the exclusion cell must readTRUEorFALSE; andprodukt_cleanis recomputed fromprodukt_rawrather than trusted. A classification typedAlforA1would otherwise materialise no column and leave the person in the reference arm. -
ri_mht_excluded_productandri_mht_excluded_reasonare new. They mark a person the study removes, and why. The flag is person level and time invariant, which is whatri_means in the pipelines that read it. It is set from holding the product, so it does not depend on that prescription having a usable duration. -
The function removes nobody. It reports. Removing a person changes the cohort, and this layer owns columns of
skeleton, never its rows. The caller decides, so the two pipelines can act differently. A flagged person is not excluded until a spec acts on the flag. -
notmhtin the table is a recorded decision that a product is not MHT. That is a different thing from a product nobody has ruled on, which is an error rather than a silent pass. - The category lookup is exact; the DURATION rules are still prefix matched. The layer calls the frozen 2026-08-28 fixed-duration, strength and minimum-dose helpers, and those select a rule by longest prefix. An exactly-classified product can still take another product’s duration rule.
- The layer calls the frozen 2026-08-28 helpers rather than copying them. The freeze rule forbids editing a dated artefact, not calling one, and every helper it reaches carries its own date. That is a convention, not isolation: a prohibited edit to a shared 2026-08-28 helper would change both entry points. The normaliser is reused because the table’s keys were built with it, so a divergent second copy would miss the lookups it affected without erroring.
-
The 2026-09-02 functions declare their data.table names, so
R CMD checkreports no NOTE and the CI check job passes. No behaviour changed. -
vignette("lmed-v20260902")states whatadd_lmed_v20260902()does. It lists every product of the table with its category and its exclusion flag, every duration rule and every approach rule. The tables readproduct_table_20260902.xlsxanddataDictionary20260828.xlsxwhen the vignette builds, so they cannot disagree with either file.
mht 26.8.29
-
inst/2023-mht/dataDictionary20260828.xlsxships, and the 2026-08-28 functions read it. Every older codebook stays in place, read by the entry point that already names it. -
add_lmed_v20260828()is the 2026-08-28 entry point. It returnsinvisible(skeleton)and keeps the caller’s row order. -
add_lmed_v20260828()owns the product category columns,approach1toapproach3, and every column whose name starts withrd_approach. It changes no other column ofskeleton, so a caller column that shares a name with a working column survives. -
add_lmed_v20260828()requires each person to hold each ISO week once, with no gap, andisoyearweekto read"YYYY-WW". Gap bridging and the three-year rule both count rows, so one row MUST be one observed week. -
A second call to
add_lmed_v20260828()gives the table the first call gives. It recomputes every output column from the product categories up. id_namenames the person identifier column oflmed, and defaults to"lopnr".create_rd = FALSEwrites nord_approach*column, and removes every one thatskeletonalready carries.-
A product name matches a codebook name as a prefix, after both are reduced to their lowercase ASCII letters.
Estramon 100reaches theestramonrung. Where one codebook name is a prefix of another the longer one sits first, anddev/check-crosswalk.Rasserts that ordering. - One codebook rule applies to each prescription, and the longest matching product name wins. Two rules of equal name length that both apply are an error.
-
The duration layer reads every rule from the codebook’s own cells. Of the 116 product rows of the
MHT_groupssheet, 24 carry a rule: 6 a fixed duration in days, and 18 a whole-months pair. A codebook edit changes the answer with no code change. -
Two of those 18 rules are keyed on a strength band, and the strength comes from
lnmn, the register product name.lnmnis REQUIRED as soon as a strength-keyed rule names a product inlmed. -
An exposure interval is closed and inclusive. A prescription dispensed on
edatumforddays coversedatumtoedatum + d - 1. - A prescription with no category, no dispensing date or no positive duration contributes nothing. The run reports one aggregate warning.
- Tibolone is its own treatment group, in all three approaches.
-
Two treatments that start in the same week mark that week
clashingprescriptions, through to the last treated week of that episode. The first untreated week after it is an ordinary stop, so the woman is recorded as a former user. - The four-week bridge needs a treated week on each side. A leading or a trailing run of untreated weeks is not a gap.
mht 26.8.26
The lint gate is wired up. The shared workflow’s
static-checksjob was red on 22 lints, andR-CMD-checkandpkgdownwere skipped behind it. Eleven<path>:<linter>pairs are now allowlisted in.github/workflows/r-package.yml. Every one is in a dated, frozen artefact:lmed-v20230509.R,lmed-v20250909.Randexposure-v20250909.R. Those files cannot be fixed, becausemhtchanges behaviour by creating a new dated function rather than by editing an existing one.first_non_na()gains an explicitreturn(). It was the only lint in a file that is not frozen, so it is fixed rather than allowlisted.tests/testthat/test-clash-and-exclude.Rpins howclashingprescriptionsandexcludereachprevious. The transition rule that records a woman as a former MHT user fires only when she moves from an active level tolocal_or_none_mht. Which levels count as active decides whether she can re-enter a new-user analysis, and the two levels behave differently.clashingprescriptionsis an active level. A clash that ends in no treatment records the later weeks asprevious, exactly as stopping systemic MHT does.excludeis inert, and the file pins that as a defect. The sequencelocal_or_none_mhttoexcludetolocal_or_none_mhtcontains no stop for the transition rule to find, so nopreviousis ever recorded and the untreated weeks after the exclusion are absorbed into it. An overlap repair written by puttingexcludeinto the clashing weeks would therefore never lift.A clash that resolves into a group is pinned as a defect. Where clashing weeks are followed by
systemic_mht, no earlier week carries the valuesystemic_mht. The target-trial specs exclude prior use by testing that one value, so such a woman passes as treatment-naive and enrols as an initiator while already treated.Two tests drive the real entry point, so the classifier produces the clash rather than receiving it.
DivigelandFemanestdispensed on the same day start together and their run lengths stay equal, so the run-length rule cannot separate them.Approach 1 can never clash, and the file pins that. A clash needs two groups tied on run length, the resolver skips
local_or_none_mht, and approach 1 has one group left to time. Approaches 2 and 3 have two and three. This bounds the defect: it cannot reach an analysis that enrols onrd_approach1_single. (Corrected 2026-09-10: since 26.8.29 tibolone is a second approach-1 group, so approach 1 can clash;test-v20260828-approaches.Rpins it.)The flag outliving the overlap is pinned as a defect. Two prescriptions covering 365 days each overlap for about 52 weeks, and the flag then covers 278 of 300 weeks.
mht 26.8.21
-
The pkgdown template package is renamed to
pptemplate.DESCRIPTIONConfig/Needs/websiteand_pkgdown.ymltemplate: package:both namepapadopoulos-lab/pptemplatenow. The house style itself is unchanged. -
A caller of the shared
papadopoulos-lab/pptemplateworkflow replaces.github/workflows/check-and-pkgdown.yml. The shared workflow runsloc-limit, thenR-CMD-check, thenpkgdown. No file inR/is over 1000 code lines, so the caller needs noloc-allowlist.
mht 26.8.6
Licensing
DESCRIPTIONAuthors@Rnow declares Richard Aubrey White as the copyright holder, withrole = "cph". It declared none at all, and neither did any other package in the fleet. Nothing inR CMD checkreports that.The copyright year is now 2026. It read 2026.
CLAUDE.mdnow carries a Licensing section, so the year gets checked rather than silently ageing.Prose only. This release rewrites the roxygen documentation,
README.md,index.mdandNEWS.mdto ASD-STE100 (Simplified Technical English). No package code changed.No claim changed. The sweep found documented claims that the code does not support. It left every one of those claims in place, and reported it.
The rewrite splits long sentences, prefers the active voice, and uses one term for each concept. Sequences that sat inside one prose sentence became bulleted lists. The
@returnwarning on both entry points now uses the RFC-2119 keyword MUST NOT.
mht 26.8.3.2
-
Adds the hex logo at
man/figures/logo.png, generated bydev/logo.R. The package was the only one in the family without one, so the pkgdown hero band had an empty art column. The template picks the file up on its own. No_pkgdown.ymlchange was needed. -
The logo is generated, never hand-drawn.
dev/logo.Rbuilds it withhexSticker, anddev/is.Rbuildignored, so neither the script nor its three build-only packages ship. To regenerate the logo, runRscript dev/logo.Rfrom the package root. - No package code changed. No exported function, argument or return value is affected.
mht 26.8.3.1
-
New test file
tests/testthat/test-classifier-invariants.Rpins the whole exposure pipeline stage by stage, defects included. No code changed. Every expectation was obtained by running the exported entry points. None was read out of the codebook. None was derived from the ladder source. When a repair lands, the diff of that file is the record of what changed. -
The pin is three frozen literal tables. They are:
- the category the classifier returns for each of the ladder’s own pattern literals;
- the category it returns for each product name in the codebook’s
MHT_groupssheet; - the category each rung DECLARES, in ladder order and with duplicates.
- The declared table is what covers a SHADOWED rung. A rung that can never fire has no observable behaviour to change. Relabelling it is therefore invisible to the other two tables. Yet a future reordering repair unshadows exactly those shadowed rungs, and a label that drifted in the meantime would land the wrong category silently. The frozen declared sequence also pins the ladder’s length and its per-pattern multiplicity. Nobody can then add a second, shadowed rung for an existing pattern unnoticed.
- Pinned per stage:
- which rows enter;
- what a raw product name becomes;
- which ladder rung a name reaches under first-match ordering;
- which categories can be materialised as skeleton columns;
- how the minimum-dose loop resolves;
- how the duration rules take precedence over each other;
- which approach columns a category set produces;
- what reaches the caller’s skeleton.
-
The pinned defects. Three ladder rungs are shadowed by an earlier rung and can never fire, so
B10,C5andD1are unreachable for every input. Seven category columns stayFALSEover the codebook product universe:B10,B12,C2,C5,D1,D4andI1. That count is scoped to the codebook deliberately.I1is reachable from the spellingsMiniPeandMini Pe. It misses only because the codebook spells the productMini-Pe, and the hyphen survives normalisation. The ladder produces categoryG1, butG1is absent from the column list. AG1prescription is therefore classified and then reaches no column at all. No approach rule reads categoriesF1,G1andH1, so adding one of them to a person changes no approach value. - The minimum-dose loop compounds. It writes the duration once per matching codebook row, and each iteration reads the previous result. A product name matching two rows therefore has the rule applied twice. The loop also matches the RAW product name, while the ladder matches the normalised one. The loop writes last, so it overrides both the IUD and the Jaydess duration rules rather than yielding to them.
- The duration pins run from two anchor dates, because one anchor is directional. The exposure duration is observable only to the ISO week. Every pinned duration is a multiple of seven days, so the interval ends on the anchor’s own weekday. A Sunday anchor therefore detects one day LONGER, and is blind to one day shorter. A Monday anchor is the mirror image. Running both gives one-day sensitivity in each direction. Measured, then verified by mutation: 1680 to 1681 is red from the Sunday anchor, 1680 to 1679 from the Monday one. Each anchor’s blind direction is asserted too, for both the IUD and the Jaydess duration. A blind assertion is not a tautology. It holds only while the duration stays inside that ISO week, so it still detects a change at week resolution. Also verified by mutation, by moving each constant a full week. Pins that name no anchor use the Sunday default and detect lengthening only, which the test file states.
-
The documented contract is pinned as it really behaves. An entry point returns a length-1 logical, not the skeleton. It does three things to the caller’s objects:
- it reorders the caller’s skeleton;
- it deletes caller columns whose names collide with its own working columns;
- it attaches a
data.tableindex to the caller’slmed.
mht 26.8.3
-
New codebook version
dataDictionary20260803.xlsxships, and NOTHING reads it yet. It is staged for the next dated entry point. Both existing entry points keep readingdataDictionary20241105.xlsx, unchanged. -
A dated function is frozen, and so are its inputs.
add_lmed_v20230509()andadd_lmed_v20250909()are dated, and therefore immutable. Repointing either at a new codebook would change a frozen artefact, even when the change is behaviourally inert. To adoptdataDictionary20260803.xlsx, you MUST create a new dated entry point that reads it. You MUST NOT edit an existing one. -
The only change to the codebook itself is an annotation on the
Rulessheet.MHT_groups,post_grouping,FDDDandcovariatesare identical between the two files, verified cell by cell. No code readsRules. So classification, duration and exposure output are unchanged — the 17 golden tests pass untouched, and no reclean is needed. - The annotation marks
Rulesitem 4 superseded, item 4 only. Item 4 rounds down to whole treatment periods. A later study decision, taken in November 2024, replaced it with rounding down to whole months. That decision addedminimum_monthly_doseandminimum_monthstoMHT_groups. The minimum-dose loop inR/lmed-v20230509.Rimplements the months rule. It did so from November 2024 onward. Items 1, 2, 3 and 5 are unaffected. - The annotation states the difference in full. The two rules agree only when
fdddis a multiple ofminimum_monthly_dosetimesminimum_months. Of the 18 products carrying a rule, 15 are affected. The decision itself, and the correspondence recording it, are held in the study’s own project record. That record is not public.
mht 26.7.31.1
-
Both exported entry points are renamed, and the suffix dates the METHODOLOGY.
x2023_add_lmed()becomesadd_lmed_v20230509();x2026_add_lmed()becomesadd_lmed_v20250909(). The eleven internal helpers take the same suffix and drop thex20xx_prefix. - The
v<YYYYMMDD>suffix is not the study year and not the release date. It is the date on which that definition’s methodology was created:-
v20230509— the approach layer (post_grouping, the approach definitions) was created 2023-05-09, in a private analysis repository. -
v20250909— therd_approach{1,2,3}_*methodology and its sensitivity variants were created 2025-09-09, in a private analysis repository. That is five months before the code was written intoswereg. The oldx2026prefix was therefore misleading: 2026 was the study year, not the year the method was devised.
-
- Behaviour is unchanged. This release is a rename only, proven against the frozen goldens in
inst/testdata/withidentical(). - See the three-column migration map in
README.mdif you are coming from either earlier name.
mht 26.7.31
- First release.
mhtcarries the MHT-specific code that previously lived inswereg. That code moved into a package of its own, so the MHT exposure definitions can be versioned and reviewed separately from the general registry tooling. - Function names drop the
mhtinfix, because the package name now supplies it.swereg::x2023_mht_add_lmed()becomesmht::x2023_add_lmed(), andswereg::x2026_mht_add_lmed()becomesmht::x2026_add_lmed(). See the migration map inREADME.md. -
mhtdoes not depend onswereg. It operates on person-week skeletons of the formsweregproduces, but takes them as plaindata.tablearguments. The two packages can therefore be installed and upgraded independently.
