docs(okf): measure the tags/description gap against the pinned SPEC and corpus
Order 20260906T213322Z: measure and propose, no src, no bump. Measured against SPEC _okf-canonical @ ad30107 and the corpus _okf-upstream @ 3fcbb9f (denominator 53, extracted at the pin because the work tree had moved on to 9a15b13). Three claims in the tags/description entry are stale against the 1.3.0 parser. The parser does have a sequence type -- _consume_block_list parses block lists of scalars -- what it lacks is tolerance for the flush-left indentation 36/53 of the corpus uses. Removing tags alone now lets 6/53 pass, not the 4/53 recorded, and the entry's headline claim does not hold at all: description alone unblocks 0/53, and with all three surface forms closed 44/53 still stop on generated as a top-level block mapping. The entry oversells its own reach by a factor of seven. The SPEC has no depth rule. no-nesting-past-depth-1 is entirely ours; the conformance floor is only "a parseable YAML frontmatter block" (SS11.1). Candidates were measured by normalizing the surface form onto a shape the parser already accepts, then importing parse_frontmatter -- the predicate is never re-implemented, only the input's spelling is rewritten. P1 (scalar flow sequence) takes the corpus from 0/53 to 6/53 without spending depth-1; P2 and P3 buy 0/53 each and 6/53 stacked on P1. Recommendation is P1 alone, and the note says out loud that this does not open the corpus. The label "punkt 44" is not grep-able: it came from a count-after-insertion, and the entry is today the 12th of 45 at docs/LIMITATIONS.md:126. No src change, no version bump, no push. 868 passed, coverage 130/130 + 6/6 gaps, redos sweep exit 0 -- all after git add.
This commit is contained in:
parent
44e2b31afd
commit
6e7c8d2b98
5 changed files with 579 additions and 0 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -24,3 +24,6 @@ coverage/
|
||||||
*.local.md
|
*.local.md
|
||||||
*.local.sh
|
*.local.sh
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# --- scratchpad: measurement scripts are tracked, the extracted corpus copy is not ---
|
||||||
|
/scratchpad/corpus/
|
||||||
|
|
|
||||||
317
docs/2026-09-07-limitations-44-maaling.md
Normal file
317
docs/2026-09-07-limitations-44-maaling.md
Normal file
|
|
@ -0,0 +1,317 @@
|
||||||
|
# Beslutningsgrunnlag — LIMITATIONS «punkt 44» (tags/description-gapet)
|
||||||
|
|
||||||
|
**Målt:** 2026-09-07, mot `HEAD` = `44e2b31` (v1.3.0, urørt) og pinnet SPEC
|
||||||
|
`_okf-canonical` @ `ad30107`. **Korpus:** `_okf-upstream` @ `3fcbb9f`.
|
||||||
|
**Hva dette er:** underlaget for én operatørbeslutning — hvilket predikat, om
|
||||||
|
noe, skal slippe inn den formen `tags` faktisk har i korpuset.
|
||||||
|
**Hva dette ikke er:** beslutningen. Ingen fil i `src/` er endret, ingen
|
||||||
|
versjon er bumpet, ingenting er pushet (0 upushede commits ved øktstart).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Referenten: «punkt 44» er ikke grepbart
|
||||||
|
|
||||||
|
Ordren og STATE peker på «LIMITATIONS punkt 44». Målt:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ grep -c '^- \*\*' docs/LIMITATIONS.md
|
||||||
|
45
|
||||||
|
$ grep -n '^- \*\*' docs/LIMITATIONS.md | awk -F: 'NR==44 {print $1}'
|
||||||
|
756 # -> ZWJ-oppføringen, ikke tags/description
|
||||||
|
```
|
||||||
|
|
||||||
|
Tallet stammer fra commit `0184df9` (25.08), som skrev «43 -> 44 items» i
|
||||||
|
commit-meldingen: **44 var antallet oppføringer etter innsettingen, ikke
|
||||||
|
oppføringens posisjon.** Oppføringen ble satt inn midt i dokumentet og er i dag
|
||||||
|
den **12. av 45**, på `docs/LIMITATIONS.md:126`. Etiketten «punkt 44» løser seg
|
||||||
|
altså ikke opp verken ved posisjon eller ved `grep`, og den vil peke feil igjen
|
||||||
|
neste gang listen vokser. Referenten er entydig i prosa («tags/description»),
|
||||||
|
men nummeret bør ikke brukes videre.
|
||||||
|
|
||||||
|
## 2. Hva punkt 44 sier i dag, ordrett
|
||||||
|
|
||||||
|
`docs/LIMITATIONS.md:126-144`:
|
||||||
|
|
||||||
|
> **`tags` and `description` block the OKF import corpus universally, before the
|
||||||
|
> trust layer is even reached.** The line-flat frontmatter parser has no
|
||||||
|
> sequence-value type at all: `tags` is present in 53/53 upstream concept
|
||||||
|
> documents — 9/53 as a flow sequence (`[a, b, c]`, rejected on the `[`
|
||||||
|
> indicator) and 44/53 as a block sequence (`- a` / `- b`, rejected as
|
||||||
|
> `"malformed frontmatter line"`) — 100% rejection regardless of form.
|
||||||
|
> `description` is present in 53/53; 29/53 is a folded plain scalar continuing
|
||||||
|
> on an indented second line, which the parser has no continuation-line model
|
||||||
|
> for and misreads as `"nested mappings are not supported"` (the remaining
|
||||||
|
> 24/53 are single-line and parse fine). Measured directly on the upstream
|
||||||
|
> reference bundles (`_okf-upstream/okf` @ `3fcbb9f`): removing `tags` alone
|
||||||
|
> lets 4/53 documents pass; removing both `tags` and `description` together
|
||||||
|
> (trust layer untouched) lets the same 4/53 pass, and all four then parse
|
||||||
|
> `generated` correctly as a mapping. **Independent of the mapping-form work
|
||||||
|
> above:** neither `1.2.0`'s flow mapping nor `1.3.0`'s `sources` carriers move
|
||||||
|
> anything on this corpus, because `tags`/`description` reject before `sources`
|
||||||
|
> is ever read. No sequence-value type or continuation-line model exists in the
|
||||||
|
> stdlib-only parser to close this with.
|
||||||
|
|
||||||
|
## 3. SPEC, ordrett fra pinnet kopi (`ad30107`)
|
||||||
|
|
||||||
|
**§2 Terminology** (`SPEC.md:81`):
|
||||||
|
|
||||||
|
> - **Frontmatter**: A YAML metadata block delimited by `---` at the top of
|
||||||
|
> a markdown file.
|
||||||
|
|
||||||
|
**§4 Concept documents** (`SPEC.md:157-159`):
|
||||||
|
|
||||||
|
> 1. A **YAML frontmatter block**, delimited by `---` on its own line at the
|
||||||
|
> start of the file and a closing `---` on its own line.
|
||||||
|
|
||||||
|
**§4.1 Frontmatter**, skjelettet (`SPEC.md:163-173`):
|
||||||
|
|
||||||
|
> ```yaml
|
||||||
|
> ---
|
||||||
|
> type: <Type name> # REQUIRED
|
||||||
|
> title: <Optional display name>
|
||||||
|
> description: <Optional one-line summary>
|
||||||
|
> resource: <Optional canonical URI for the underlying asset>
|
||||||
|
> tags: [<tag>, <tag>, ...] # Optional
|
||||||
|
> ```
|
||||||
|
|
||||||
|
**§4.1, de to bærende definisjonene** (`SPEC.md:194-199`):
|
||||||
|
|
||||||
|
> - `description`: A single sentence summarizing the concept. Used by
|
||||||
|
> `index.md` generators, search snippets, and previews.
|
||||||
|
> - `tags`: A YAML list of short strings for cross-cutting categorization.
|
||||||
|
|
||||||
|
**§11 Conformance** (`SPEC.md:738-742`):
|
||||||
|
|
||||||
|
> A bundle is **conformant** with OKF v0.2 if:
|
||||||
|
>
|
||||||
|
> 1. Every non-reserved `.md` file in the tree contains a parseable YAML
|
||||||
|
> frontmatter block.
|
||||||
|
|
||||||
|
### 3.1 Det spec-en ikke sier
|
||||||
|
|
||||||
|
`grep -n -i 'nest\|depth\|indent' SPEC.md` gir **null** treff som uttrykker en
|
||||||
|
dybde- eller innrykksregel (13 treff, alle i andre betydninger — «flat list of»
|
||||||
|
i §9 og §13, «indent» ingen). **SPEC-en har ingen dybderegel.** «Ingen nesting
|
||||||
|
forbi dybde 1» er utelukkende vår egen sikkerhetsegenskap. Konformans-gulvet er
|
||||||
|
«a parseable YAML frontmatter block» — altså *hele* YAML.
|
||||||
|
|
||||||
|
To presiseringer som følger av ordlyden:
|
||||||
|
|
||||||
|
- `tags` er definert som «A YAML list» — **ikke** som flow-formen. Skjelettets
|
||||||
|
`[<tag>, ...]` er ett eksempel, ikke formkravet. Blokkformen er like konform.
|
||||||
|
- `description` er «A single sentence». En setning brutt over to linjer som
|
||||||
|
foldet plain scalar er samme setning; spec-en stiller ingen linjekrav.
|
||||||
|
|
||||||
|
## 4. Måleoppsett
|
||||||
|
|
||||||
|
Korpuset er hentet ut ved pinnen, ikke fra arbeidstreet — `_okf-upstream` står
|
||||||
|
i dag på `9a15b13`, og `3fcbb9f` er en ekte forgjenger (`git merge-base
|
||||||
|
--is-ancestor` → 0), med 23 filer endret i `okf/` mellom dem.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git -C _okf-upstream archive 3fcbb9f okf/bundles | tar -x -C scratchpad/corpus
|
||||||
|
$ find scratchpad/corpus -name '*.md' ! -name index.md ! -name log.md | wc -l
|
||||||
|
53
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nevner = 53.** Definisjonen er «hver `.md` under `okf/bundles/` som ikke er et
|
||||||
|
reservert strukturnavn» — som er nøyaktig §11.1s «every non-reserved `.md` file».
|
||||||
|
|
||||||
|
**Positiv kontroll.** Et null-resultat må sjekkes mot et kjent-positivt tilfelle
|
||||||
|
før det konsumeres. Følgende dokument parser i dag, mot samme `HEAD`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: Metric
|
||||||
|
description: Recognized revenue for a period.
|
||||||
|
tags:
|
||||||
|
- finance # INNRYKKET blokksekvens
|
||||||
|
- revenue
|
||||||
|
generated: { by: reference_agent/gemini-2.5-pro, at: 2026-06-30T14:00:00Z }
|
||||||
|
sources:
|
||||||
|
- id: revenue-policy
|
||||||
|
resource: policies/revenue-recognition.md
|
||||||
|
```
|
||||||
|
→ `tags` blir `['finance', 'revenue']`, `sources` blir en liste av dicter.
|
||||||
|
Instrumentet avviser altså ikke alt; `tags` som blokksekvens **fungerer allerede
|
||||||
|
i dag**, forutsatt innrykk.
|
||||||
|
|
||||||
|
## 5. Målingen
|
||||||
|
|
||||||
|
### 5.1 Formsensus over de 53 (`scratchpad/shapes.py`, exit 0)
|
||||||
|
|
||||||
|
| nøkkel | form | antall |
|
||||||
|
|---|---|---|
|
||||||
|
| `tags` | blokksekvens **uten innrykk** (`- a` i kolonne 0) | **36/53** |
|
||||||
|
| `tags` | flow-sekvens `[a, b, c]` | 9/53 |
|
||||||
|
| `tags` | enkeltlinje-skalar | 8/53 |
|
||||||
|
| `description` | skalar + innrykket fortsettelseslinje | 29/53 |
|
||||||
|
| `description` | enkeltlinje-skalar | 24/53 |
|
||||||
|
| `generated` | **topp-nivå blokk-mapping** (` by:` på neste linje) | **44/53** |
|
||||||
|
| `generated` | flow-mapping `{ ... }` | 9/53 |
|
||||||
|
| `sources` | blokksekvens uten innrykk | 44/53 |
|
||||||
|
| `sources` | blokksekvens med innrykk | 5/53 |
|
||||||
|
|
||||||
|
### 5.2 Baseline og kandidater (`scratchpad/candidates.py`, exit 0)
|
||||||
|
|
||||||
|
Kandidatene er målt ved å **normalisere overflateformen** inn i en form
|
||||||
|
parseren allerede godtar, og så importere `parse_frontmatter`. Predikatet er
|
||||||
|
aldri re-implementert; bare stavemåten på inputen er skrevet om.
|
||||||
|
|
||||||
|
| variant | passerer | dominerende residual |
|
||||||
|
|---|---|---|
|
||||||
|
| baseline (v1.3.0 som utgitt) | **0/53** | 32× nested mappings, 12× malformed line, 9× flow sequence |
|
||||||
|
| **P1** skalar-flow-sekvens | **6/53** | 32× nested mappings, 12× malformed line |
|
||||||
|
| **P2** blokksekvens uten innrykk | 0/53 | 44× nested mappings |
|
||||||
|
| **P3** foldet plain scalar | 0/53 | 36× malformed line |
|
||||||
|
| P1+P3 | 6/53 | 36× malformed line |
|
||||||
|
| P1+P2 | 6/53 | 44× nested mappings |
|
||||||
|
| **P1+P2+P3** | **6/53** | **44× nested mappings, 3× allowlist** |
|
||||||
|
|
||||||
|
De 6 som passerer med P1 er alle i `acme_retail`; de er 6 av de 9 med
|
||||||
|
flow-sekvens-`tags`, og de tre siste stoppes av allowlisten
|
||||||
|
(`parameters.name` ×2, `not.term` ×1), ikke av `tags`.
|
||||||
|
|
||||||
|
### 5.3 Taket
|
||||||
|
|
||||||
|
Med alle tre predikatene er taket **6/53**. Residualet er 44× topp-nivå
|
||||||
|
blokk-mapping på `generated` og 3× allowlist. En diagnostisk kjøring som også
|
||||||
|
normaliserte topp-nivå blokk-mapping til flow-mapping traff neste vegg med én
|
||||||
|
gang: 44× «a quoted scalar inside a flow mapping is not a supported form» —
|
||||||
|
korpuset skriver `at: '2026-07-10T23:16:06+00:00'` med enkeltfnutter.
|
||||||
|
|
||||||
|
## 6. Tre påstander i punkt 44 er målt feil
|
||||||
|
|
||||||
|
1. **«The line-flat frontmatter parser has no sequence-value type at all.»**
|
||||||
|
Usant siden 1.3.0. `_consume_block_list` (`okf.py:662`) parser blokklister av
|
||||||
|
rene skalarer, og den positive kontrollen i §4 beviser det. Det som mangler
|
||||||
|
er ikke sekvenstypen, men **innrykkskravet**: `_consume_block_list` krever
|
||||||
|
`raw[:1] in (" ", "\t")`, og korpuset skriver `- a` i kolonne 0.
|
||||||
|
Feilklassen «44/53 rejected as malformed frontmatter line» er i dag 12/53,
|
||||||
|
fordi de øvrige treffer `description`-fortsettelsen først.
|
||||||
|
|
||||||
|
2. **«removing `tags` alone lets 4/53 documents pass … removing both … the same
|
||||||
|
4/53.»** Målt i dag: **6/53** med `tags` fjernet, og **6/53** med begge
|
||||||
|
fjernet. Tallet 4 var riktig for 1.2.0-parseren; 1.3.0s `sources`-bærere
|
||||||
|
flyttet to dokumenter til.
|
||||||
|
|
||||||
|
3. **«`tags`/`description` reject before `sources` is ever read» → derfor er
|
||||||
|
dette «det ENESTE residualet som blokkerer hele korpuset».** Den slutningen
|
||||||
|
holder ikke. `description` alene løsner **0/53** — fjerner man bare
|
||||||
|
`description`, passerer ingenting. Og lukker man *alle tre* formene, står
|
||||||
|
**44/53** fortsatt på `generated` som topp-nivå blokk-mapping. Den bindende
|
||||||
|
skranken på dette korpuset er altså **ikke** tags/description, men den
|
||||||
|
topp-nivå blokk-mappingen vi bevisst avviser. Punkt 44 overselger sin egen
|
||||||
|
betydning med en faktor på over sju (6 mot 53).
|
||||||
|
|
||||||
|
Punkt 44 bør skrives om etter at operatøren har bestemt seg — det er en
|
||||||
|
dokumentasjonsendring som hører sammen med predikatvalget, ikke før det.
|
||||||
|
|
||||||
|
## 7. Kandidatpredikatene
|
||||||
|
|
||||||
|
Ordren spurte etter predikatet som slipper inn en ren skalar-flow-sekvens.
|
||||||
|
Det er P1. P2 og P3 tas med fordi målingen viser at P1 alene er en liten
|
||||||
|
gevinst, og beslutningen bør se hva naboene koster.
|
||||||
|
|
||||||
|
### P1 — flow-sekvens av rene skalarer
|
||||||
|
|
||||||
|
Predikat: i `_parse_flow_sequence`, når første ikke-blanke tegn i et element
|
||||||
|
ikke er `{`, les elementet som en plain scalar dersom det ikke inneholder noen
|
||||||
|
av `{ } [ ] : , " ' #` og ikke er tomt. Blandet sekvens (skalar + mapping)
|
||||||
|
avvises, slik blokklisten allerede gjør.
|
||||||
|
|
||||||
|
- **(a) Slipper inn:** `tags: [finance, revenue, headline-metric]` — 9/53 i
|
||||||
|
korpuset. Passeringen går fra 0/53 til **6/53**.
|
||||||
|
- **(b) Avviser fortsatt:** siterte elementer (`['a', 'b']`), elementer med
|
||||||
|
kolon eller komma i seg, tom sekvens `[]`, uavsluttet `[a, b`, blandet
|
||||||
|
`[a, {b: c}]`, og nestet `[[a]]` — alle på tegn-nivå, uten YAML-semantikk.
|
||||||
|
- **(c) Dybde-1:** **bruker den ikke opp.** Elementene er blad; ingen ny
|
||||||
|
nestingsgrad oppstår. Det er samme dybde blokklisten av skalarer allerede har.
|
||||||
|
- **(d) Testen som pinner den:** `tags: [a, b]` → `["a", "b"]`; `tags: ['a']`
|
||||||
|
raiser; `tags: [a, {b: c}]` raiser med blandingsfeilen; `tags: []` raiser;
|
||||||
|
`sources: [{ id: x }]` parser uendret (ingen regresjon på G30-bæreren).
|
||||||
|
|
||||||
|
### P2 — blokksekvens uten innrykk
|
||||||
|
|
||||||
|
Predikat: i `_consume_block_list`, godta også `raw[:1] == "-"` når linjen
|
||||||
|
starter med `- ` og forrige toppnøkkel hadde tom verdi.
|
||||||
|
|
||||||
|
- **(a) Slipper inn:** `tags:` + `- a` i kolonne 0 — 36/53. Men også `sources:`
|
||||||
|
i samme form — 44/53.
|
||||||
|
- **(b) Avviser fortsatt:** alt innholdet i elementet avviser i dag; allowlisten
|
||||||
|
og `_reject_mapping_construct` er uendret.
|
||||||
|
- **(c) Dybde-1:** **bruker den ikke opp** for skalarelementer, men den er ikke
|
||||||
|
gratis: den åpner samtidig den ikke-innrykkede blokk-mapping-bæreren for
|
||||||
|
`sources`, hvor elementenes fortsettelseslinjer *er* innrykket. Det er en
|
||||||
|
større flate enn `tags`, og den bør vurderes for seg.
|
||||||
|
- **(d) Testen:** `tags:\n- a\n- b` → `["a", "b"]`; `sources:\n- id: x\n title: y`
|
||||||
|
→ én dict; en linje `- a` uten forutgående tom toppnøkkel raiser fortsatt;
|
||||||
|
`tags:\n- a\n- {b: c}` raiser med blandingsfeilen.
|
||||||
|
- **Målt effekt alene: 0/53.** Den løsner ingenting uten P1 eller uten at
|
||||||
|
`generated` også åpnes.
|
||||||
|
|
||||||
|
### P3 — foldet plain scalar (fortsettelseslinje)
|
||||||
|
|
||||||
|
Predikat: etter en toppnøkkel med ikke-tom, ikke-`[`/`{` verdi, slå sammen
|
||||||
|
etterfølgende innrykkede linjer som *ikke* starter med `- ` og *ikke* inneholder
|
||||||
|
en uquotet `": "`, med ett mellomrom som skjøt.
|
||||||
|
|
||||||
|
- **(a) Slipper inn:** `description` brutt over to linjer — 29/53.
|
||||||
|
- **(b) Avviser fortsatt:** en innrykket linje som ser ut som `k: v` treffer
|
||||||
|
fremdeles nested-mapping-avvisningen; `- ` treffer fremdeles listeruten.
|
||||||
|
`_reject_dangerous_value` kjører på den sammenslåtte verdien, ikke på
|
||||||
|
fragmentene.
|
||||||
|
- **(c) Dybde-1:** **bruker den ikke opp** — resultatet er én skalar.
|
||||||
|
Men den svekker et vern: i dag er *enhver* innrykket linje uten aktiv listenøkkel
|
||||||
|
et avvist nestet uttrykk. Etter P3 er den regelen betinget av at linjen ikke
|
||||||
|
inneholder `": "` — altså samme heuristikk som `_reject_mapping_construct`,
|
||||||
|
gjenbrukt til å *slippe gjennom* i stedet for til å avvise.
|
||||||
|
- **(d) Testen:** `description: en setning\n som fortsetter` → én streng med
|
||||||
|
ett mellomrom; `description: x\n y: z` raiser fortsatt som nested mapping;
|
||||||
|
`description: x\n - a` raiser fortsatt.
|
||||||
|
- **Målt effekt alene: 0/53.**
|
||||||
|
|
||||||
|
## 8. Anbefaling
|
||||||
|
|
||||||
|
**P1 alene. Ikke P2, ikke P3, ikke nå.**
|
||||||
|
|
||||||
|
Begrunnelsen er tallene, ikke smaken:
|
||||||
|
|
||||||
|
- P1 er den eneste av de tre som flytter passeringstallet i det hele tatt
|
||||||
|
(0 → 6). P2 og P3 gir hver for seg **0/53**, og lagt oppå P1 gir de fortsatt
|
||||||
|
**6/53**. De koster parserflate og kjøper null målt konformans.
|
||||||
|
- P1 er den minste flaten: den er et tegn-nivå-predikat inne i en funksjon som
|
||||||
|
allerede eksisterer, og den bruker ikke opp dybde-1-regelen.
|
||||||
|
- P1 lukker et gap STATE allerede fører som bevisst («`tags: [a, b]`
|
||||||
|
konformansgap»), og den bringer parseren i linje med §4.1s eget skjelett —
|
||||||
|
den ene formen spec-en faktisk skriver ut.
|
||||||
|
- P2 og P3 bør ikke besluttes på dette korpuset, fordi korpuset ikke kan skille
|
||||||
|
dem: **44/53 stopper på `generated` som topp-nivå blokk-mapping uansett.**
|
||||||
|
Å bygge P2 og P3 nå ville være å betale for to predikater og måle null.
|
||||||
|
|
||||||
|
**Den ærlige konsekvensen, som må sies høyt:** P1 tar korpuset fra 0/53 til
|
||||||
|
6/53. Det lukker ikke «hele korpuset». Skal 53/53 nås, er den neste
|
||||||
|
beslutningen en helt annen og mye tyngre en — topp-nivå blokk-mapping (44/53)
|
||||||
|
pluss siterte skalarer (44/53) — og *den* bruker opp dybde-1-regelen. Det er en
|
||||||
|
sikkerhetsbeslutning, ikke en parserdetalj, og den hører ikke i denne ordren.
|
||||||
|
|
||||||
|
## 9. Verifiseringslogg
|
||||||
|
|
||||||
|
| Påstand | Kommando | Exit | Resultat |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Nevner = 53 | `find scratchpad/corpus -name '*.md' ! -name index.md ! -name log.md \| wc -l` | 0 | 53 |
|
||||||
|
| Pinnen er ekte forgjenger | `git -C _okf-upstream merge-base --is-ancestor 3fcbb9f HEAD` | 0 | ja (HEAD = `9a15b13`) |
|
||||||
|
| SPEC-pinnen er ren | `git -C _okf-canonical rev-parse --short HEAD; git status --porcelain` | 0 | `ad30107`, rent tre |
|
||||||
|
| SPEC har ingen dybderegel | `grep -n -i 'nest\|depth\|indent' SPEC.md` | 0 | 13 treff, 0 relevante |
|
||||||
|
| Baseline 0/53 | `PYTHONPATH=src .venv/bin/python scratchpad/measure.py` | 0 | 0 passerer |
|
||||||
|
| Formsensus | `PYTHONPATH=src .venv/bin/python scratchpad/shapes.py` | 0 | tabell §5.1 |
|
||||||
|
| Kandidattall | `PYTHONPATH=src .venv/bin/python scratchpad/candidates.py` | 0 | tabell §5.2 |
|
||||||
|
| Positiv kontroll | inline, se §4 | 0 | parser, `tags == ['finance','revenue']` |
|
||||||
|
| LIMITATIONS-antall | `grep -c '^- \*\*' docs/LIMITATIONS.md` | 0 | 45 |
|
||||||
|
| Upushet ved øktstart | `git rev-list --count origin/main..HEAD` | 0 | **0** |
|
||||||
|
|
||||||
|
**Ikke målt:** om P1 påvirker ytelse eller ReDoS-marginen — predikatet er
|
||||||
|
tegn-for-tegn uten regex, men ingen sveip er kjørt, siden ingen kode er skrevet.
|
||||||
|
Kandidatene er målt ved overflatenormalisering, ikke ved en patchet parser: det
|
||||||
|
er en trofast simulering av *hva* som slippes inn, men den beviser ikke at en
|
||||||
|
implementasjon av P1 avviser nøyaktig (b)-listen. Testene i (d) er det som ville
|
||||||
|
pinne det.
|
||||||
155
scratchpad/candidates.py
Normal file
155
scratchpad/candidates.py
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
"""What each candidate predicate would admit, measured by normalizing the SURFACE
|
||||||
|
form onto a shape the CURRENT parser already accepts. The predicate is imported,
|
||||||
|
never re-implemented: only the input's spelling is rewritten."""
|
||||||
|
import os, re, sys, collections
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||||
|
from llm_ingestion_guard import okf
|
||||||
|
|
||||||
|
ROOT = os.path.join(os.path.dirname(__file__), "corpus", "okf", "bundles")
|
||||||
|
|
||||||
|
def docs():
|
||||||
|
out = []
|
||||||
|
for dirpath, _d, fns in os.walk(ROOT):
|
||||||
|
for fn in sorted(fns):
|
||||||
|
if fn.endswith(".md") and fn not in ("index.md", "log.md"):
|
||||||
|
out.append(os.path.join(dirpath, fn))
|
||||||
|
return sorted(out)
|
||||||
|
|
||||||
|
def split_fm(text):
|
||||||
|
lines = text.split("\n")
|
||||||
|
if not lines or lines[0].strip() != "---":
|
||||||
|
return None, None
|
||||||
|
for i in range(1, len(lines)):
|
||||||
|
if lines[i].strip() == "---":
|
||||||
|
return lines[1:i], lines[i + 1:]
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
TOPKEY = re.compile(r"^([A-Za-z0-9_]+):(.*)$")
|
||||||
|
|
||||||
|
def n_flow_scalar_seq(fm):
|
||||||
|
"""P1: `k: [a, b]` where every element is a plain scalar -> indented block list."""
|
||||||
|
out = []
|
||||||
|
for raw in fm:
|
||||||
|
m = TOPKEY.match(raw)
|
||||||
|
if m and m.group(2).strip().startswith("[") and m.group(2).strip().endswith("]"):
|
||||||
|
inner = m.group(2).strip()[1:-1]
|
||||||
|
elems = [e.strip() for e in inner.split(",")]
|
||||||
|
if inner and all(e and "{" not in e and "}" not in e and "[" not in e for e in elems):
|
||||||
|
out.append("%s:" % m.group(1))
|
||||||
|
out.extend(" - %s" % e for e in elems)
|
||||||
|
continue
|
||||||
|
out.append(raw)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def n_flush_block_seq(fm):
|
||||||
|
"""P2: a flush-left `- item` run under a bare `k:` -> the same run, indented."""
|
||||||
|
out, i, n = [], 0, len(fm)
|
||||||
|
while i < n:
|
||||||
|
raw = fm[i]
|
||||||
|
m = TOPKEY.match(raw)
|
||||||
|
if m and m.group(2).strip() == "" and i + 1 < n and fm[i + 1][:1] == "-":
|
||||||
|
out.append(raw)
|
||||||
|
i += 1
|
||||||
|
while i < n and (fm[i][:1] == "-" or fm[i][:1] in (" ", "\t")):
|
||||||
|
out.append(" " + fm[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
out.append(raw)
|
||||||
|
i += 1
|
||||||
|
return out
|
||||||
|
|
||||||
|
def n_folded_scalar(fm):
|
||||||
|
"""P3: `k: text` continued on more-indented plain lines -> one joined line."""
|
||||||
|
out, i, n = [], 0, len(fm)
|
||||||
|
while i < n:
|
||||||
|
raw = fm[i]
|
||||||
|
m = TOPKEY.match(raw)
|
||||||
|
if m and m.group(2).strip() and not m.group(2).strip()[0] in "[{":
|
||||||
|
acc = raw
|
||||||
|
i += 1
|
||||||
|
while i < n and fm[i][:1] in (" ", "\t") and not fm[i].strip().startswith("- "):
|
||||||
|
acc = acc.rstrip() + " " + fm[i].strip()
|
||||||
|
i += 1
|
||||||
|
out.append(acc)
|
||||||
|
continue
|
||||||
|
out.append(raw)
|
||||||
|
i += 1
|
||||||
|
return out
|
||||||
|
|
||||||
|
def parses(fm, body):
|
||||||
|
text = "---\n" + "\n".join(fm) + "\n---\n" + "\n".join(body)
|
||||||
|
try:
|
||||||
|
okf.parse_frontmatter(text)
|
||||||
|
return None
|
||||||
|
except okf.OKFFrontmatterError as exc:
|
||||||
|
return str(exc)
|
||||||
|
|
||||||
|
COMBOS = [
|
||||||
|
("baseline (v1.3.0 as shipped)", []),
|
||||||
|
("P1 scalar flow sequence", [n_flow_scalar_seq]),
|
||||||
|
("P2 flush block sequence", [n_flush_block_seq]),
|
||||||
|
("P3 folded plain scalar", [n_folded_scalar]),
|
||||||
|
("P1+P3", [n_flow_scalar_seq, n_folded_scalar]),
|
||||||
|
("P2+P3", [n_flush_block_seq, n_folded_scalar]),
|
||||||
|
("P1+P2", [n_flow_scalar_seq, n_flush_block_seq]),
|
||||||
|
("P1+P2+P3", [n_flow_scalar_seq, n_flush_block_seq, n_folded_scalar]),
|
||||||
|
]
|
||||||
|
|
||||||
|
def main():
|
||||||
|
paths = docs()
|
||||||
|
print("denominator: %d documents (pin 3fcbb9f, parser HEAD)" % len(paths))
|
||||||
|
for name, fns in COMBOS:
|
||||||
|
ok = 0
|
||||||
|
residual = collections.Counter()
|
||||||
|
for p in paths:
|
||||||
|
fm, body = split_fm(open(p, encoding="utf-8").read())
|
||||||
|
for fn in fns:
|
||||||
|
fm = fn(fm)
|
||||||
|
err = parses(fm, body)
|
||||||
|
if err is None:
|
||||||
|
ok += 1
|
||||||
|
else:
|
||||||
|
residual[err.split(":")[0]] += 1
|
||||||
|
top = "; ".join("%dx %s" % (c, r) for r, c in residual.most_common(3))
|
||||||
|
print(" %-30s %2d/%d residual: %s" % (name, ok, len(paths), top or "-"))
|
||||||
|
|
||||||
|
main()
|
||||||
|
|
||||||
|
# --- diagnostic only (NOT a proposal): size the real binding constraint ---
|
||||||
|
def n_block_mapping(fm):
|
||||||
|
"""D4: a top-level `k:` followed by indented `a: b` lines -> one flow mapping."""
|
||||||
|
out, i, n = [], 0, len(fm)
|
||||||
|
while i < n:
|
||||||
|
raw = fm[i]
|
||||||
|
m = TOPKEY.match(raw)
|
||||||
|
if m and m.group(2).strip() == "" and i + 1 < n and fm[i + 1][:1] in (" ", "\t") \
|
||||||
|
and not fm[i + 1].strip().startswith("- "):
|
||||||
|
pairs = []
|
||||||
|
i += 1
|
||||||
|
while i < n and fm[i][:1] in (" ", "\t") and not fm[i].strip().startswith("- "):
|
||||||
|
pairs.append(fm[i].strip())
|
||||||
|
i += 1
|
||||||
|
out.append("%s: { %s }" % (m.group(1), ", ".join(pairs)))
|
||||||
|
continue
|
||||||
|
out.append(raw)
|
||||||
|
i += 1
|
||||||
|
return out
|
||||||
|
|
||||||
|
print("\n--- diagnostic: what caps the corpus ABOVE the tags/description gap ---")
|
||||||
|
for name, fns in [
|
||||||
|
("D4 top-level block mapping", [n_block_mapping]),
|
||||||
|
("P1+P2+P3+D4", [n_flow_scalar_seq, n_flush_block_seq, n_folded_scalar, n_block_mapping]),
|
||||||
|
]:
|
||||||
|
ok = 0
|
||||||
|
residual = collections.Counter()
|
||||||
|
for p in docs():
|
||||||
|
fm, body = split_fm(open(p, encoding="utf-8").read())
|
||||||
|
for fn in fns:
|
||||||
|
fm = fn(fm)
|
||||||
|
err = parses(fm, body)
|
||||||
|
if err is None:
|
||||||
|
ok += 1
|
||||||
|
else:
|
||||||
|
residual[err.split(":")[0]] += 1
|
||||||
|
top = "; ".join("%dx %s" % (c, r) for r, c in residual.most_common(3))
|
||||||
|
print(" %-30s %2d/53 residual: %s" % (name, ok, top or "-"))
|
||||||
45
scratchpad/measure.py
Normal file
45
scratchpad/measure.py
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
"""Measure the pinned OKF corpus against the CURRENT parser. No src/ changes."""
|
||||||
|
import os, sys, collections
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||||
|
from llm_ingestion_guard import okf
|
||||||
|
|
||||||
|
ROOT = os.path.join(os.path.dirname(__file__), "corpus", "okf", "bundles")
|
||||||
|
|
||||||
|
def docs():
|
||||||
|
out = []
|
||||||
|
for dirpath, _dirnames, filenames in os.walk(ROOT):
|
||||||
|
for fn in sorted(filenames):
|
||||||
|
if not fn.endswith(".md"):
|
||||||
|
continue
|
||||||
|
if fn in ("index.md", "log.md"):
|
||||||
|
continue
|
||||||
|
out.append(os.path.join(dirpath, fn))
|
||||||
|
return sorted(out)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
paths = docs()
|
||||||
|
ok, fail = [], []
|
||||||
|
reasons = collections.Counter()
|
||||||
|
for p in paths:
|
||||||
|
with open(p, encoding="utf-8") as fh:
|
||||||
|
text = fh.read()
|
||||||
|
try:
|
||||||
|
okf.parse_frontmatter(text)
|
||||||
|
ok.append(p)
|
||||||
|
except okf.OKFFrontmatterError as exc:
|
||||||
|
fail.append((p, str(exc)))
|
||||||
|
reasons[str(exc).split(":")[0]] += 1
|
||||||
|
print("denominator: %d documents" % len(paths))
|
||||||
|
print("parse OK : %d" % len(ok))
|
||||||
|
print("parse FAIL : %d" % len(fail))
|
||||||
|
print("--- failure classes ---")
|
||||||
|
for r, c in reasons.most_common():
|
||||||
|
print("%4d %s" % (c, r))
|
||||||
|
print("--- passing documents (positive control) ---")
|
||||||
|
for p in ok:
|
||||||
|
print(" " + os.path.relpath(p, ROOT))
|
||||||
|
print("--- first 8 failures verbatim ---")
|
||||||
|
for p, e in fail[:8]:
|
||||||
|
print(" %s\n %s" % (os.path.relpath(p, ROOT), e))
|
||||||
|
|
||||||
|
main()
|
||||||
59
scratchpad/shapes.py
Normal file
59
scratchpad/shapes.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
"""Shape census of the pinned OKF corpus: which surface form each key is written in."""
|
||||||
|
import os, re, sys, collections
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||||
|
from llm_ingestion_guard import okf
|
||||||
|
|
||||||
|
ROOT = os.path.join(os.path.dirname(__file__), "corpus", "okf", "bundles")
|
||||||
|
|
||||||
|
def docs():
|
||||||
|
out = []
|
||||||
|
for dirpath, _d, fns in os.walk(ROOT):
|
||||||
|
for fn in sorted(fns):
|
||||||
|
if fn.endswith(".md") and fn not in ("index.md", "log.md"):
|
||||||
|
out.append(os.path.join(dirpath, fn))
|
||||||
|
return sorted(out)
|
||||||
|
|
||||||
|
def split_fm(text):
|
||||||
|
lines = text.split("\n")
|
||||||
|
if not lines or lines[0].strip() != "---":
|
||||||
|
return None, None
|
||||||
|
for i in range(1, len(lines)):
|
||||||
|
if lines[i].strip() == "---":
|
||||||
|
return lines[1:i], lines[i + 1:]
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def key_shape(fm, key):
|
||||||
|
for i, raw in enumerate(fm):
|
||||||
|
m = re.match(r"^(%s):(.*)$" % re.escape(key), raw)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
val = m.group(2).strip()
|
||||||
|
follow = fm[i + 1] if i + 1 < len(fm) else ""
|
||||||
|
if val.startswith("["):
|
||||||
|
return "flow-sequence"
|
||||||
|
if val.startswith("{"):
|
||||||
|
return "flow-mapping"
|
||||||
|
if val:
|
||||||
|
if follow[:1] in (" ", "\t") and not follow.strip().startswith("- "):
|
||||||
|
return "scalar+continuation"
|
||||||
|
return "single-line-scalar"
|
||||||
|
if follow.strip().startswith("- "):
|
||||||
|
return "block-seq-indented" if follow[:1] in (" ", "\t") else "block-seq-flush"
|
||||||
|
if follow[:1] in (" ", "\t"):
|
||||||
|
return "block-mapping"
|
||||||
|
return "empty"
|
||||||
|
return "ABSENT"
|
||||||
|
|
||||||
|
def main():
|
||||||
|
paths = docs()
|
||||||
|
print("denominator: %d" % len(paths))
|
||||||
|
for key in ("tags", "description", "generated", "verified", "sources"):
|
||||||
|
c = collections.Counter()
|
||||||
|
for p in paths:
|
||||||
|
fm, _ = split_fm(open(p, encoding="utf-8").read())
|
||||||
|
c[key_shape(fm, key)] += 1
|
||||||
|
print("\n%s:" % key)
|
||||||
|
for shape, n in c.most_common():
|
||||||
|
print(" %-22s %d/%d" % (shape, n, len(paths)))
|
||||||
|
|
||||||
|
main()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue