Metadata-Version: 2.4
Name: tfm-sjekk
Version: 0.4.0
Summary: Validerer TFM-merking i IFC-modeller mot NS 3457-serien og prosjektets TFM-master
Keywords: ifc,bim,tfm,ns3451,ns3457,buildingsmart
Author: grigr
Author-email: grigr <offline.broker199@passinbox.com>
License-Expression: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Other Audience
Classifier: Natural Language :: Norwegian
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Requires-Dist: ifcopenshell>=0.8.5
Requires-Dist: jinja2>=3.1.6
Requires-Dist: openpyxl>=3.1.5
Requires-Dist: pydantic>=2.13.4
Requires-Dist: typer>=0.27.1
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# tfm-sjekk

[![test](https://github.com/alegri1/tfm-sjekk/actions/workflows/test.yml/badge.svg)](https://github.com/alegri1/tfm-sjekk/actions/workflows/test.yml)
[![bygg](https://github.com/alegri1/tfm-sjekk/actions/workflows/bygg.yml/badge.svg)](https://github.com/alegri1/tfm-sjekk/actions/workflows/bygg.yml)
[![PyPI](https://img.shields.io/pypi/v/tfm-sjekk)](https://pypi.org/project/tfm-sjekk/)
[![Python](https://img.shields.io/pypi/pyversions/tfm-sjekk)](https://pypi.org/project/tfm-sjekk/)

Validerer TFM-merking i IFC-modeller mot NS 3457-serien og prosjektets TFM-master.

> **Status: virker, men er ikke prøvd i et ekte prosjekt.** Elleve kontroller,
> fire rapportformater, og BCF-fila er prøvd i BIMcollab ZOOM. Ingen norsk
> fagmodell har vært gjennom verktøyet, og hypotesen bak det er ikke validert —
> se §11 i spesifikasjonen. Si fra hvis noe ikke stemmer med hvordan dere
> jobber; det er akkurat det jeg vil vite.

---

## Problemet

Norske byggeprosjekter krever tverrfaglig merking (TFM) av objekter i BIM-modellene,
men i praksis er merkingen inkonsistent. Feil oppdages ofte først ved modelleveranse
til arkiv eller ved overlevering til FDV — altså for sent og for dyrt.

En TFM-ID ser slik ut:

```
++115080=3600.001.04-JVZ001%JVZ.001.008
```

Skriv `4310` der `4310.001.00` mangler kursnummer, eller gjenbruk `QLF001` i to
fagmodeller, og ingen oppdager det før i FDV-fasen.

## Rapporten
![HTML-rapporten](docs/rapport.png)
*Demomodellene, som har én tilsiktet feil per kontroll. Dekningstabellen sier
hvor mye som faktisk ble kontrollert — her 17 av 17. Står det 0, er «ingen funn»
ikke det samme som «ingen feil».*

Hver kjøring gir en selvstendig HTML-rapport med sorterbar tabell, en BCF-fil
som åpnes i Solibri, Catenda, Dalux og BIMcollab, en XLSX til analyse, og en
exit-kode til CI.

## Hvorfor ikke bare IDS eller Solibri?

**IDS** er per design begrenset til det som kan avgjøres på ett objekt om gangen.
Det dekker K1–K5. Men det kan ikke uttrykke at ingen to objekter deler
komponentforekomst-ID, at et system finnes i prosjektets master, eller at et
kursnummer stemmer med fordelingen objektet er tilkoblet.

**Solibri** gjør relasjonssjekker, men regelsettet er internasjonalt og generisk —
det kan ikke NS 3451-tabellen, NS 3457-8-kodene eller PA 0805s regler, og det koster
lisens.

Nisjen er norsk-spesifikk, relasjonell, gratis og kjørbar i CI.

## Kontrollene

| # | Kontroll | Springer ut av | Grad |
|---|---|---|---|
| K1 | Alle objekter i konfigurerte IFC-klasser har en TFM-verdi | krav om merking | feil |
| K2 | TFM-ID-en følger grammatikken | TFM-strukturen | feil |
| K3 | Systemkoden finnes i kodetabellen | NS 3451 tabell 8 | feil |
| K4 | Systemkoden er angitt så spesifikt som mulig | PA 0805 | advarsel |
| K5 | Komponentkoden finnes i kodetabellen | NS 3457-8 | feil |
| K6 | Komponentforekomster er unike, også på tvers av fagmodeller | krav om entydig ID | feil |
| K7 | Systemer og typer finnes i prosjektets TFM-master | SIMBA | feil |
| K8 | Elektro: kurs-/sløyfenummer utfylt og konsistent | NS 3451 kap. 4 og 5 | feil |
| K9 | MMI/prosesstatus satt og konsistent innenfor systemet | SIMBA | info |
| T1 | Komponenttypen er den samme i TFM-ID-en og i typefeltet | to felt, én sannhet | feil |
| D1 | Noe ble faktisk kontrollert i hver fagmodell | egen erfaring | advarsel |

K1–K9 er kontrollene i §4 i spesifikasjonen. T1 og D1 kom til etterpå, da det
viste seg at samme opplysning kan stå to steder i en modell, og at fravær av
funn ellers ikke er til å skille fra at ingenting ble undersøkt.

Alle kan slås av eller få endret alvorlighetsgrad i `tfm-sjekk.toml` — TFM-
tolkningene varierer mellom prosjekter, og da må regelsettet være data.

## Installasjon

```bash
pipx install tfm-sjekk
```

**Uten Python:** last ned den frittstående binæren fra siste kjøring av
`bygg`-arbeidsflyten under Actions — artefaktene heter `tfm-sjekk-windows`,
`tfm-sjekk-macos` og `tfm-sjekk-linux`. Én fil, ingen installasjon. Mange
BIM-koordinatorer har ikke Python og får ikke lov til å installere det heller
(§6).

Bygge den selv:

```bash
uv run pyinstaller tfm-sjekk.spec --noconfirm   # → dist/tfm-sjekk[.exe]
```

Binæren blir rundt 57 MB, fordi `ifcopenshell` alene er 82 MB på disk, og
bruker et par sekunder på å starte: én fil betyr at alt pakkes ut i minnet ved
hver kjøring. Det er byttehandelen mot å slippe installasjon.

### Uten kommandolinje

**Dra IFC-filene oppå `tfm-sjekk.exe`** i Utforskeren. Rapportene havner i en
mappe som heter `rapport` ved siden av modellene. Flere filer på én gang
federeres, som er det K6 trenger for å finne duplikater på tvers av fagmodeller.

Uten kodetabeller hopper K3, K4, K5 og K7 over — de trenger `--systemtabell`,
`--komponenttabell` og `--master`, og da må du på kommandolinja. Resten kjører.

**Dobbeltklikk** viser en kort bruksanvisning og lar vinduet stå til du trykker
Enter. Uten det ville Windows lukket konsollen i samme øyeblikk programmet var
ferdig, og du hadde ikke rukket å lese noe.

## Bruk

```bash
tfm-sjekk sjekk rie.ifc riv.ifc
```

Så kort fordi prosjektets oppsett ligger i `tfm-sjekk.toml` ved siden av
modellene. Fila finnes automatisk — hos modellen først, ellers i mappa du står i
— og kjøringen sier hvilken den leste:

```toml
tfm_master = "TFM-master.xlsx"
systemtabell = "tabeller/min-ns3451.csv"
komponenttabell = "tabeller/min-ns3457-8.csv"
```

Stiene er relative til oppsettsfila, ikke til der du står, så mappa kan flyttes
og sendes videre. Har du ikke laget et oppsett ennå, oppgir du det samme som
flagg:

```bash
tfm-sjekk sjekk rie.ifc riv.ifc \
    --systemtabell min-ns3451.csv \
    --komponenttabell min-ns3457-8.csv \
    --master tfm-master.xlsx \
    --ut rapport/
```

Et flagg vinner alltid over fila. Og peker en sti i oppsettet på noe som ikke
finnes, stopper kjøringen — den hopper ikke over kontrollen som om du hadde
valgt å kjøre uten.

Flere filer federeres og kontrolleres samlet — det er slik K6 finner duplikater
på tvers av fagmodeller. Exit-kode 0 ved ingen feil, 1 ved feil, slik at verktøyet
kan stå som port i en leveranseprosess.

Hver kjøring skriver fire filer til `--ut`:

| Fil | Til hva |
|---|---|
| `funn.bcfzip` | BCF 2.1 — åpnes i Solibri, Catenda, Dalux, BIMcollab |
| `rapport.html` | Én selvstendig fil med sorterbar tabell, til deling |
| `funn.xlsx` | For analyse i Excel — frosset overskriftsrad og filter |
| `funn.csv` | Semikolonseparert UTF-8, for skript og pandas |

Prøv det med demomodellene:

```bash
uv run python eksempler/lag_demomodell.py
uv run tfm-sjekk sjekk eksempler/demo-rie.ifc eksempler/demo-riv.ifc eksempler/demo-elektro.ifc \
    --systemtabell eksempler/FIKTIV-systemkoder.csv \
    --komponenttabell eksempler/FIKTIV-komponentkoder.csv \
    --master eksempler/FIKTIV-tfm-master.csv
```

`tfm-sjekk kontroller` lister kontrollene og statusen deres.

### Første møte med et prosjekt

Du har en fagmodell og ingen `tfm-sjekk.toml`. Da er spørsmålet hvor TFM-verdiene
faktisk ligger i denne modellen — og det vet verktøyet allerede, fordi det leter
flere steder enn i det konfigurerte og holder rede på hvor det fant hver verdi.

```bash
tfm-sjekk oppsett rie.ifc riv.ifc > tfm-sjekk.toml
```

Kommandoen kjører ingen kontroller og trenger verken master eller kodetabeller.
Den skriver et utkast som bare inneholder det som avviker fra standardverdiene,
med belegget for hvert forslag i en kommentar over det:

```toml
# TFM-forekomst: egenskapssett
forekomst = [
    "TFM11_Forekomst",
    # 840 objekter, gjenkjent feltnavn i et egenskapssett som ikke er konfigurert
    "Data",
]
```

Antallet er ikke pynt. Forskjellen mellom et egenskapssett brukt på 840 objekter
og ett brukt på 2 er forskjellen mellom en prosjektkonvensjon og en tilfeldighet,
og det er du som avgjør hvilken av dem det er. **Les fila før du tar den i bruk.**

Forslaget dekker også `ifc_klasser`, men bare klasser utenfor omfanget som *har*
TFM-verdier. Det er svaret på «0 av 412»: at en klasse finnes i fila betyr
ingenting, men at objektene er merket betyr at noen mente de skulle med.

Med `--ut` skrives fila i stedet for skjermen, og en fil som finnes fra før
røres ikke uten `--overskriv`.

Demoen lager en modell med verdiene bevisst på avveie, så du kan se hele runden:

```bash
uv run tfm-sjekk sjekk eksempler/avveie.ifc            # 3 av 4 i omfanget
uv run tfm-sjekk oppsett eksempler/avveie.ifc --ut forslag.toml
uv run tfm-sjekk sjekk eksempler/avveie.ifc --config forslag.toml   # 4 av 4
```

Den fjerde er en `IfcBuildingElementProxy` med TFM-verdi — utstyr eksportert i
feil klasse. Forslaget finner den, og etter at det er tatt i bruk er den med.

#### Én grense verdt å kjenne

`oppsett` kan lukke ett hull om gangen, ikke to samtidig. Verdiuttrekket har tre
strategier, og alle trenger minst ett kjent holdepunkt:

| Hvor verdien ligger | Verktøyet | `oppsett` |
|---|---|---|
| Ukjent egenskapssett, kjent feltnavn | finner den | foreslår egenskapssettet |
| Kjent egenskapssett, ukjent feltnavn | finner den | foreslår feltnavnet |
| **Ukjent begge deler** | **ser ingenting** | **kan ikke hjelpe** |

`eksempler/blindsone.ifc` er det siste tilfellet: førti objekter merket helt
korrekt, i `AnleggsData.Anleggskode`. Verktøyet melder førti K1-feil, og
`oppsett` sier at det ikke fant noe å bygge på.

```bash
uv run tfm-sjekk oppsett eksempler/blindsone.ifc
# Ingenting å bygge på: ingen av objektene hadde TFM-verdi.
```

Skjer dette, må du oppgi holdepunktet selv — legg egenskapssettet eller
feltnavnet inn i `tfm-sjekk.toml` for hånd, og kjør `oppsett` på nytt. Da har
den noe å gå ut fra, og finner resten.

Grensen er bevisst. Alternativet er å skanne hvert felt i hvert egenskapssett og
stole på at verdien *ser ut som* en TFM-ID — en langt løsere slutning enn de
øvrige, og en beslutning som fortjener sin egen vurdering.

### Modeller i tidligfase

En tidlig modell har ikke alltid fått byggnummer. Systemet og komponenten er
merket, men `++115080`-delen er ikke bestemt ennå. Med standardoppsettet gir det
et syntaksfunn på hvert eneste objekt — om en del prosjektet ikke har tatt
stilling til.

```bash
uv run tfm-sjekk sjekk eksempler/tidligfase.ifc
# 5 feil — alle «Mangler ++-delen»

uv run tfm-sjekk sjekk eksempler/tidligfase.ifc --config eksempler/tidligfase.toml
# 2 feil — et ekte K6-duplikat som lå skjult under støyen
```

Du trenger ikke kjenne innstillingen på forhånd — verktøyet finner den selv:

```bash
uv run tfm-sjekk oppsett eksempler/tidligfase.ifc
```

```toml
[grammatikk]

# 5 verdier feiler bare fordi plasseringen («++»-delen) mangler.
# Til sammenligning: ingen andre verdier parser.
# Er dette en tidlig fase, er linja under riktig. Er det en merkefeil,
# skal den strykes — verktøyet kan ikke se forskjellen, det kan du.
krev_plassering = false
```

Begge tallene står der med hensikt. 5 mot 0 er en modell som ikke har fått
byggnummer ennå. Var det 3 som feilet mens 40 parset, ville det vært tre objekter
merket feil — og da skal forslaget strykes.

Forslaget gis bare når innstillingen får **hver eneste** verdi til å parse. Løser
den bare noen av dem, er det merkefeil og ikke fase, og verktøyet foreslår
ingenting.

Merk hva som *ikke* skjer: en TFM-ID med feil plassering avvises fortsatt.
Valgfri betyr at delen kan utelates, ikke at den kan være feil. Og K6 måler
unikhet på delene som finnes, så to bygg med samme system og komponent regnes
ikke som duplikater bare fordi plasseringen er valgfri.

Sett verdien tilbake til `true` når byggnummeret er ført inn — da fanges de
objektene som fortsatt mangler det.

### Funn tilbake til Revit

BCF fungerer i en viewer, men rettingen skjer i Revit. `dynamo/` inneholder et
Python-skript for en Dynamo-graf som leser `funn.csv` og skriver avviksteksten
inn i en parameter på hvert element — da får du en schedule som tømmer seg selv
etter hvert som du retter.

Ingen plugin, ingen installasjon. Se [dynamo/LES-MEG.md](dynamo/LES-MEG.md) for
oppsett, og for hvilke to slags funn som ikke kan kobles til et element.

### Prøve BCF-en i en viewer

`eksempler/visning.ifc` har samme innhold som elektromodellen, men med
prosjekt, enheter, romlig struktur og geometri — alt en viewer trenger for å
åpne fila og faktisk vise noe. De øvrige demomodellene har bare objekter med
egenskaper, som er nok for kontrollene, men gir en tom scene i en viewer.

```bash
uv run tfm-sjekk sjekk eksempler/visning.ifc --ut rapport
```

Åpne så `visning.ifc` i Catenda, BIMcollab ZOOM eller Solibri Anywhere og
importer `rapport/funn.bcfzip`. Et emne skal velge nøyaktig det objektet det
gjelder — det er den koblingen som gjør BCF verdt bryet, og den eneste delen
av formatet et skjema ikke kan verifisere.

## Avgrensning

Ingen GUI, ingen 3D-visning, ingen Revit-plugin, ingen webapp, ingen skriving tilbake
til modellen, ingen støtte for samferdsel. Se §3 og §10 i spesifikasjonen.

## Om standardene og kodetabellene

**NS 3451 og NS 3457-serien er betalte standarder fra Standard Norge. Kodetabellene
følger ikke med dette verktøyet, og de skal ikke legges i dette repoet.**

Du peker på dine egne CSV-filer med `--systemtabell` og `--komponenttabell`:

```
kode;beskrivelse
2310;<beskrivelse fra standarden>
```

Filene under `eksempler/` er oppdiktede og ikke-normative — de finnes bare for at
testene og demoen skal kunne kjøre.

Dette gjør verktøyet lovlig å publisere, og det gjør det generelt: en byggherre med
eget kodeverk kan bruke det med sin egen tabell.

---

Avsnittene under er oppslagsverk — de forklarer valg verktøyet har tatt,
og trengs først når noe oppfører seg uventet.

## Komponenttypen står to steder

Komponenttypen kan stå både i `%`-delen av TFM-ID-en og i et eget egenskapssett:

```
TFM11_Forekomst:  ++115080=3600.001.04-JVZ001%JVZ.001.008
TFM11_Type:                                   JVZ.001.008
```

`%`-delen har forrang — den er en del av selve TFM-ID-en. Mangler den, som er
vanlig siden `krev_komponenttype` er `false` som standard, gjelder typefeltet. Uten
den regelen hoppet K7 over hvert objekt uten `%`-del, og sjekket i praksis
komponenttyper mot mastera for en forsvinnende liten del av en modell.

**Er de to uenige, er det en feil (T1).** Da har objektet ingen avklart
komponenttype, og K7 tier om det: et funn om mastera ville hvilt på et vilkårlig
valg mellom to verdier. Rett spriket først.

## Hva «ingen funn» betyr

Rapporten oppgir alltid **hvor mye som ble kontrollert**, per fagmodell:

```
rie.ifc   184 av 210 objekter i omfanget
riv.ifc    96 av 140 objekter i omfanget
ark.ifc     0 av 412 objekter i omfanget   <- ingenting kontrollert
```

Uten det tallet er fravær av funn tvetydig: det kan bety at merkingen er i orden,
eller at ingen kontroll hadde noe å se på. Omfanget bestemmes av `ifc_klasser`, så
en modell uten tekniske fag — eller en eksport som legger utstyr i
`IfcBuildingElementProxy` — gir null i omfanget og en rapport som ser ren ut.

En fagmodell med tomt omfang gir en **advarsel**, ikke en feil. Exit-koden er
uendret, slik at et legitimt kjør på en arkitektmodell ikke stenger porten i CI.
Meldingen nevner klassene fila faktisk inneholder, så den er nok til å rette
`ifc_klasser`.

## Hvordan verktøyet finner TFM-verdien

Norske modeller er rotete, så verktøyet leter i tre trinn — i synkende styrke
på beviset:

| Trinn | Hvor | Sikkerhet |
|---|---|---|
| 1 | konfigurert egenskapssett, konfigurert feltnavn | sikkert |
| 2 | konfigurert feltnavn i et hvilket som helst egenskapssett | bevis nok |
| 3 | konfigurert egenskapssett, ukjent feltnavn | en gjetning |

Trinn 3 er den som kan ta feil, og derfor godtas en verdi der bare når den er
**gjenkjennelig som det feltet skal inneholde** — for TFM betyr det at høyst én
av `++`, `=` og `-` mangler. Uten den regelen leste verktøyet fabrikatnavnet fra
et egenskapssett som het riktig, og meldte det som syntaksfeil i TFM-ID-en.

Et funn som hviler på trinn 2 eller 3 sier hvor verdien faktisk kom fra. Ble
verdien forkastet, sier meldingen hva som sto der i stedet — det er den
opplysningen som lar deg finne ut hvilket felt malen egentlig skriver til.

Feltnavnene i `egenskapsnavn_*` brukes også til søk på tvers av alle
egenskapssett, så de må være distinkte. **Endret:** `Type` er fjernet fra
standardlista for `egenskapsnavn_type`; den traff
`Pset_ManufacturerTypeInformation` i praktisk talt enhver modell. Trenger
prosjektet ditt den, kan den settes i `tfm-sjekk.toml`.

## Elektrokontrollene (K8)

Dette er kontrollen som krever at man forstår både IFC og et kursopplegg, og
den går i tre trinn:

- **K8a** — for NS 3451 kapittel 4 og 5 skal undernummeret være utfylt; det er
  kurs-/sløyfenummeret. Fordelinger er unntatt: tavla er roten kursene går ut
  fra, ikke noe som selv ligger på en kurs, så `=4310.001.00` er riktig der.
- **K8b** — alt som mates fra en fordeling skal tilhøre fordelingens system.
  Sammenligningen går på systemet (`4310.001`), ikke på systemforekomsten
  (`4310.001.12`) — undernummeret er nettopp det som skal variere.
- **K8c** — to *ulike* kurser på samme fordeling skal ikke ha samme kursnummer.
  At ti armaturer deler kurs 12 er normalt; at kurs 12 finnes to ganger er ikke.

Fordelingen finnes ved IFC-klasse (`IfcElectricDistributionBoard` i IFC4,
`IfcElectricDistributionPoint` i 2x3) og hva som henger på den ved å følge
koblingene mellom `IfcDistributionPort`-ene. Søket stopper i neste fordeling,
slik at en underfordeling blir sin egen rot.

K8c trenger at kursene er gruppert i modellen (`IfcDistributionCircuit` /
`IfcElectricalCircuit`). Mangler de, sier verktøyet fra én gang framfor å gjette.
Klassenavnene ligger under `[elektro]` i `tfm-sjekk.toml`.

## Excel: bruk `funn.xlsx`, ikke CSV-en

De to filene har hver sin jobb, og grunnen er verdt å kjenne til.

CSV er tekst, og Excel må gjette to ting: skilletegnet og tegnkodingen.
Skrivebords-Excel deler på listeskilletegnet fra regionsinnstillingene —
semikolon på en norsk maskin — mens **Excel på web antar komma** og legger hele
rapporten i kolonne A. Løsningen på det, en `sep=;`-linje øverst, setter
samtidig Excel på en parse-vei som **ignorerer BOM-en**, og da blir «følger» til
«fÃ¸lger». Innenfor én CSV kan Excel gi riktige tegn eller riktige kolonner,
ikke begge.

`funn.xlsx` har ingenting å gjette: tegn og kolonner ligger strukturert i fila,
og den åpner likt i begge utgavene av Excel. Overskriftsraden er frosset og
filtrerbar. `openpyxl` var allerede en avhengighet for å lese TFM-mastera, så
formatet koster ikke noe ekstra.

`funn.csv` er dermed fri til å være det maskinlesbare formatet: semikolon,
UTF-8 med BOM, ingen direktivlinje. `csv`-modulen, pandas og `Import-Csv` leser
den rett fram.

## BCF-fila

BCF er forskjellen mellom «interessant skript» og «noe vi tar i bruk»: funnene
åpnes i verktøyene folk allerede sitter i, og hvert emne har et viewpoint som
velger objektet det gjelder. Kontroll-ID-en ligger både i tittelen og som
`Labels`, så det er lett å filtrere per kontroll i viewer-en. Samlefunn som
peker på modellen som helhet (K7 og K8c) får emne uten viewpoint — det er
ingenting å zoome til.

Fila skrives direkte som zip + XML, uten BCF-bibliotek. Formatet er lite nok
til at avhengigheten ikke lønner seg, og det holder PyInstaller-binæren mindre.

**Utdata er deterministisk.** Emne-GUID-ene er utledet fra innholdet i funnet,
ikke trukket tilfeldig, og zip-oppføringene har fast tidsstempel. Et emne som
allerede er importert i en viewer beholder derfor identiteten sin mellom
kjøringer.

Det siste som varierer er `CreationDate`. Sett `--opprettet` for å låse den, så
blir hele fila byte-identisk og kan sammenlignes i CI:

```bash
tfm-sjekk sjekk modell.ifc --opprettet 2026-01-01T12:00:00Z
```

Verdien tolkes som ISO 8601 og regnes om til UTC, så `2026-01-01T13:00:00+01:00`
gir samme fil. En verdi uten tidssone leses som UTC, ikke lokal tid — ellers
ville to maskiner fått ulik fil av samme kommando. Uten flagget brukes klokka nå.

## Prosesstatus (K9)

MMI-skalaen varierer mellom byggherrer, så den ligger under `[mmi]` i
`tfm-sjekk.toml`. «MMI 300», «mmi300» og «300» leses som samme nivå.

K9 spør om MMI er satt, om verdien er i skalaen, og om den er konsistent
innenfor systemet (`4310.001` — ikke per kurs; en modenhetsgrad hører til
systemet som helhet). Sprikende MMI rapporteres mot flertallet i systemet, så
meldingen peker på de få objektene som er glemt.

Graden er **info**, ikke feil: et system *skal* ha objekter på ulike nivåer
midt i en prosjekteringsfase. En modell der ingen objekter har MMI antas å ikke
bruke MMI, og gir ingen funn — sett `krev_pa_alle = true` hvis prosjektet
krever det på alt.

## TFM-mastera

`--master` tar prosjektets egen TFM-master som XLSX eller CSV. Formatet er ikke
standardisert, så verktøyet gjenkjenner **kolonneoverskrifter**, ikke arknavn: alle
ark leses, og ark uten en kjent kolonne hoppes over. Overskriftsraden trenger ikke
stå øverst — logo og revisjonstabell over tabellen er greit.

```
systemforekomst;komponenttype
3600.001.04;JVZ.001.008
```

Prefikser folk skriver av gammel vane (`=3600.001.04`, `++115080=3600.001.04`)
normaliseres bort, så mastera og modellen trenger bare være enige om innholdet.
Heter kolonnene noe annet hos deg, settes navnene under `[master]` i
`tfm-sjekk.toml`.

K7 går begge veier. Et system eller en komponenttype modellen bruker uten at det
står i mastera er en **feil**. Oppføringer i mastera som ikke er modellert
rapporteres som **info** og teller ikke mot exit-koden — de kan like gjerne være
prosjektert men ikke tegnet ennå, og å skille det fra utgåtte oppføringer krever
prosesstatus (K9).

## Utvikling

```bash
uv sync
uv run pytest
uv run ruff check .
```

Arkitekturen i korthet: `tfm_sjekk.ifc` er eneste modul som importerer
`ifcopenshell` og returnerer ren, picklebar data. Alt derfra — parser, kontroller,
rapporter — jobber mot `Kontekst`, som holder hele den federerte modellen. Hver
kontroll er en ren funksjon `Kontekst -> list[Funn]`. Det er den grensen som gjør
K6–K8 mulige, og som lar kontrollene testes uten en eneste IFC-fil.

Koblingsgrafen følger samme regel: portene i IFC leses i `loader.py` og legges
igjen der som `IfcObjekt.tilkoblet` — en liste med GlobalId-er. Kontrollene ser
en graf av rene strenger og har aldri hørt om `IfcDistributionPort`.

Full spesifikasjon: [`specification/tfm-sjekk-spesifikasjon.md`](specification/tfm-sjekk-spesifikasjon.md).
Paragrafhenvisninger i koden (§4, §8, …) peker dit. Den er grunnlaget og endres
ikke løpende; `openspec/specs/` beskriver oppførselen verktøyet faktisk har, og
vokser én evne om gangen etter hvert som endringer arkiveres.

## Publisering

`publiser`-arbeidsflyten bygger og laster opp til PyPI når en tag pushes:

```bash
git tag v0.1.0 && git push origin v0.1.0
```

Taggen sjekkes mot `version` i `pyproject.toml` før noe bygges. Et hjul med
feil versjonsnummer kan ikke trekkes tilbake fra PyPI — versjonen er brent for
godt — så det er verdt et eget steg. Testene kjøres i samme jobb, siden en
tag-push ikke utløser `test`-arbeidsflyten.

Autentiseringen er **trusted publishing**: PyPI stoler på en OIDC-billett fra
GitHub Actions, og det finnes ingen API-nøkkel å lekke. Oppsettet gjøres én
gang, før første tag:

1. Lag prosjektet på [pypi.org](https://pypi.org) — eller bruk «pending
   publisher» hvis navnet ennå ikke er tatt.
2. Under **Publishing → Add a new publisher**, velg GitHub og fyll inn eier,
   repo, workflow-fil `publiser.yml` og miljø `pypi`.
3. Lag miljøet `pypi` under repoets **Settings → Environments**. Vil du ha en
   godkjenning før hver utgivelse, legg til deg selv som «required reviewer»
   der.

`workflow_dispatch` kjører bygg og pakkesjekk uten å publisere, så kjeden kan
prøves før du binder deg til et versjonsnummer.

## Hvem som står bak

Jeg er elkraftingeniør og utvikler, og fersk i BIM. Verktøyet finnes fordi K8 —
kursnummer og fordelinger — er noe jeg kan, og fordi ingen andre så ut til å ha
skrevet det. Alt om IFC og TFM har jeg lært underveis, og det er nettopp derfor
jeg vil høre hvordan dere gjør dette i dag.

## Lisens

MIT.
