Skip to contents

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 from add_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 of swereg for 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 reads product_table_20261002.xlsx, a byte copy of the 2026-09-24 table, and dataDictionary20261002.xlsx, and no other workbook. It writes every column that add_lmed_v20260924() writes, and adds approach5, rd_approach5_single and rd_approach5_multiple.
  • Approach 5 pools the oestrogen routes of approach 4 and gives the hormonal IUD (E1) its own level. Its levels are estrogen_progesterone, estrogen_other_progestogen, estrogen_iud, estrogen_only, tibolone and local_or_none_mht. The codebook adds 47 approach 5 rules to the post_grouping sheet.
  • The resolver of add_lmed_v20260924() is unchanged. When two approach 5 levels are lit in one week, approach5 reports the level that started most recently. An oestrogen week with E1 and another progestogen lights two levels.
  • vignette("lmed-v20261002") is the one vignette. vignette("lmed-v20260924") moved to archive/ 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.xlsx and product_table_20260924.xlsx, each such count in the people_* and prescriptions_* columns now reads <5: 53 cells on sheet G and 168 on sheet other. 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_person onward, each description sat against the wrong field.
  • The vignettes of add_lmed_v20260902() and add_lmed_v20260922() moved to archive/ 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 of add_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 a stopifnot() 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 of add_lmed_v20260924(), in the structure of vignette("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 reads product_table_20260924.xlsx, the 2026-09-22 table with one note cell reworded, and dataDictionary20260924.xlsx, and no other workbook. It writes every column that add_lmed_v20260922() writes, and adds approach4, rd_approach4_single and rd_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, tibolone and local_or_none_mht. The codebook adds 47 approach 4 rules to the post_grouping sheet.
  • The progesterone levels read C1 (progesterone) or C5 (dydrogesterone), dispensed separately or in B4 and B11. 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, approach4 reports 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. Its stopifnot() 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 reads product_table_20260922.xlsx and dataDictionary20260922.xlsx, and no other workbook. Its output equals that of add_lmed_v20260902(), except for the two changes below.
  • Estradiol Valerate is category A7, an injected oestrogen. The 2026-09-02 table reads it as notmht. A woman with any dispensing of it gets ri_mht_excluded_product = TRUE, with the reason possible gender-affirming therapy.
  • No post_grouping rule reads A7. The codebook drops the 14 rules that A7 lit, and removes A7 from the exclusions of 3 inert rules. 88 rules remain. A week of A7 alone is local_or_none_mht in every approach, and the A7 column 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 what add_lmed_v20260922() does. Its stopifnot() 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, marked is_isoyear == TRUE, with an isoyearweek such as "1946-**". That value is no ISO week, so the run stops. Pass skeleton[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 any FALSE run of four rows or fewer to TRUE. 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.xlsx ships, 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 75 and Estramon 100 are 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 read TRUE or FALSE; and produkt_clean is recomputed from produkt_raw rather than trusted. A classification typed Al for A1 would otherwise materialise no column and leave the person in the reference arm.
  • ri_mht_excluded_product and ri_mht_excluded_reason are new. They mark a person the study removes, and why. The flag is person level and time invariant, which is what ri_ 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.
  • notmht in 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 check reports no NOTE and the CI check job passes. No behaviour changed.
  • vignette("lmed-v20260902") states what add_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 read product_table_20260902.xlsx and dataDictionary20260828.xlsx when the vignette builds, so they cannot disagree with either file.

mht 26.8.29

  • inst/2023-mht/dataDictionary20260828.xlsx ships, 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 returns invisible(skeleton) and keeps the caller’s row order.
  • add_lmed_v20260828() owns the product category columns, approach1 to approach3, and every column whose name starts with rd_approach. It changes no other column of skeleton, 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, and isoyearweek to 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_name names the person identifier column of lmed, and defaults to "lopnr".
  • create_rd = FALSE writes no rd_approach* column, and removes every one that skeleton already carries.
  • A product name matches a codebook name as a prefix, after both are reduced to their lowercase ASCII letters. Estramon 100 reaches the estramon rung. Where one codebook name is a prefix of another the longer one sits first, and dev/check-crosswalk.R asserts 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_groups sheet, 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. lnmn is REQUIRED as soon as a strength-keyed rule names a product in lmed.
  • An exposure interval is closed and inclusive. A prescription dispensed on edatum for d days covers edatum to edatum + 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-checks job was red on 22 lints, and R-CMD-check and pkgdown were 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.R and exposure-v20250909.R. Those files cannot be fixed, because mht changes behaviour by creating a new dated function rather than by editing an existing one.

  • first_non_na() gains an explicit return(). 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.R pins how clashingprescriptions and exclude reach previous. The transition rule that records a woman as a former MHT user fires only when she moves from an active level to local_or_none_mht. Which levels count as active decides whether she can re-enter a new-user analysis, and the two levels behave differently.

  • clashingprescriptions is an active level. A clash that ends in no treatment records the later weeks as previous, exactly as stopping systemic MHT does.

  • exclude is inert, and the file pins that as a defect. The sequence local_or_none_mht to exclude to local_or_none_mht contains no stop for the transition rule to find, so no previous is ever recorded and the untreated weeks after the exclusion are absorbed into it. An overlap repair written by putting exclude into 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 value systemic_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. Divigel and Femanest dispensed 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 on rd_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.R pins 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. DESCRIPTION Config/Needs/website and _pkgdown.yml template: package: both name papadopoulos-lab/pptemplate now. The house style itself is unchanged.
  • A caller of the shared papadopoulos-lab/pptemplate workflow replaces .github/workflows/check-and-pkgdown.yml. The shared workflow runs loc-limit, then R-CMD-check, then pkgdown. No file in R/ is over 1000 code lines, so the caller needs no loc-allowlist.

mht 26.8.6

Licensing

  • DESCRIPTION Authors@R now declares Richard Aubrey White as the copyright holder, with role = "cph". It declared none at all, and neither did any other package in the fleet. Nothing in R CMD check reports that.

  • The copyright year is now 2026. It read 2026.

  • CLAUDE.md now carries a Licensing section, so the year gets checked rather than silently ageing.

  • Prose only. This release rewrites the roxygen documentation, README.md, index.md and NEWS.md to 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 @return warning 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 by dev/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.yml change was needed.
  • The logo is generated, never hand-drawn. dev/logo.R builds it with hexSticker, and dev/ is .Rbuildignored, so neither the script nor its three build-only packages ship. To regenerate the logo, run Rscript dev/logo.R from 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.R pins 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_groups sheet;
    • the category each rung DECLARES, in ladder order and with duplicates.
    All three were generated by running the code and pasted in as literals, so relabelling any rung turns the suite red. They are NOT a clinically approved product-to-category map. They record present behaviour only.
  • 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, C5 and D1 are unreachable for every input. Seven category columns stay FALSE over the codebook product universe: B10, B12, C2, C5, D1, D4 and I1. That count is scoped to the codebook deliberately. I1 is reachable from the spellings MiniPe and Mini Pe. It misses only because the codebook spells the product Mini-Pe, and the hyphen survives normalisation. The ladder produces category G1, but G1 is absent from the column list. A G1 prescription is therefore classified and then reaches no column at all. No approach rule reads categories F1, G1 and H1, 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.table index to the caller’s lmed.
    “Mutates by reference and only adds columns” is not an accurate description, and the contract needs a decision.

mht 26.8.3

  • New codebook version dataDictionary20260803.xlsx ships, and NOTHING reads it yet. It is staged for the next dated entry point. Both existing entry points keep reading dataDictionary20241105.xlsx, unchanged.
  • A dated function is frozen, and so are its inputs. add_lmed_v20230509() and add_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 adopt dataDictionary20260803.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 Rules sheet. MHT_groups, post_grouping, FDDD and covariates are identical between the two files, verified cell by cell. No code reads Rules. So classification, duration and exposure output are unchanged — the 17 golden tests pass untouched, and no reclean is needed.
  • The annotation marks Rules item 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 added minimum_monthly_dose and minimum_months to MHT_groups. The minimum-dose loop in R/lmed-v20230509.R implements 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 fddd is a multiple of minimum_monthly_dose times minimum_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() becomes add_lmed_v20230509(); x2026_add_lmed() becomes add_lmed_v20250909(). The eleven internal helpers take the same suffix and drop the x20xx_ 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 — the rd_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 into swereg. The old x2026 prefix 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/ with identical().
  • See the three-column migration map in README.md if you are coming from either earlier name.

mht 26.7.31

  • First release. mht carries the MHT-specific code that previously lived in swereg. 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 mht infix, because the package name now supplies it. swereg::x2023_mht_add_lmed() becomes mht::x2023_add_lmed(), and swereg::x2026_mht_add_lmed() becomes mht::x2026_add_lmed(). See the migration map in README.md.
  • mht does not depend on swereg. It operates on person-week skeletons of the form swereg produces, but takes them as plain data.table arguments. The two packages can therefore be installed and upgraded independently.