raw derive/fetch/pending: Länge als dritte Größenachse beim Ingest - eine CHECK-Zeile nennt jeden geschriebenen Text über LONG_TEXT_CHARS (Zeichen der .md wie geschrieben), raw pending markiert eine lange .md/.txt mit long:; ingest-large-tree liest ihn in Teilen nach der Gliederung, je Teil nur den Zeilenbereich, Extrakt als Faktenliste mit Fundstelle und Modalität (#226); 10.1.0-beta.10
CI / verify (push) Successful in 7m55s
CI / pwsh (push) Successful in 2m41s
Converters / converters (push) Successful in 9m32s
Release / converters (push) Successful in 27s
Release / release (push) Successful in 37s
Release / wiki-mirror (push) Skipped
CI / verify (push) Successful in 7m55s
CI / pwsh (push) Successful in 2m41s
Converters / converters (push) Successful in 9m32s
Release / converters (push) Successful in 27s
Release / release (push) Successful in 37s
Release / wiki-mirror (push) Skipped
Files changed: - CHANGES.md - VERSION - docs/how-to/ingest-a-source.md - docs/reference/commands/raw-material-and-uploads.md - instructions/ingest-large-tree.md - instructions/wiki-ingest/SKILL.md - raw/CONTRACT.md - tools/CONTRACT.md - tools/chemenu/commands/raw/_common.py - tools/chemenu/commands/raw/derive.py - tools/chemenu/commands/raw/fetch.py - tools/chemenu/commands/raw/pending.py - tools/chemenu/tests/test_raw_derive.py - tools/chemenu/tests/test_raw_fetch.py - types/source.guidance.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
1 parent
9cb981a4cd
commit
eae4a427d2
15 files changed
+341
-37
No files matched your search
+6
-1
@@ -14,7 +14,7 @@ place only: `instructions/dev/version-parts.md` § The candidate model.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10.1.0-beta.9 - 2026-10-15 - wiki-ingest step 1 sends the session to read-page-images.md before the discussion, so up to the threshold the page images are no question for the user
|
## 10.1.0-beta.10 - 2026-10-15 - Länge als dritte Größenachse beim Ingest: raw derive und raw fetch melden einen Text über der Längenschwelle mit einer CHECK-Zeile, raw pending markiert eine lange Textdatei; ingest-large-tree liest ihn in Teilen nach der Gliederung, mit einer Faktenliste je Teil (#226)
|
||||||
|
|
||||||
**Author:** Torben Nehmer
|
**Author:** Torben Nehmer
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ place only: `instructions/dev/version-parts.md` § The candidate model.
|
|||||||
- Konverter-Testkorpus und Harness ziehen in das private Repository chemenu-testfiles; neuer Workflow Converters, Freigabe-Gate im Release
|
- Konverter-Testkorpus und Harness ziehen in das private Repository chemenu-testfiles; neuer Workflow Converters, Freigabe-Gate im Release
|
||||||
- raw derive PDF: Auszeichnungen auf ein Minimum - Struktur zuerst, ~~ ** * nur wo eindeutig, Bildseiten-CHECK nur für Inhaltsbilder
|
- raw derive PDF: Auszeichnungen auf ein Minimum - Struktur zuerst, ~~ ** * nur wo eindeutig, Bildseiten-CHECK nur für Inhaltsbilder
|
||||||
- raw derive PDF: Kopf nennt die Bildseiten (images) und die nicht zugeordneten Seiten (unmatched); neue Anweisung read-page-images für den gezielten Blick auf diese Seiten
|
- raw derive PDF: Kopf nennt die Bildseiten (images) und die nicht zugeordneten Seiten (unmatched); neue Anweisung read-page-images für den gezielten Blick auf diese Seiten
|
||||||
|
- Länge als dritte Größenachse beim Ingest: raw derive und raw fetch melden einen Text über der Längenschwelle mit einer CHECK-Zeile, raw pending markiert eine lange Textdatei; ingest-large-tree liest ihn in Teilen nach der Gliederung, mit einer Faktenliste je Teil (#226)
|
||||||
|
|
||||||
**Low impact**
|
**Low impact**
|
||||||
- instructions/dev: Konverter-Vergleich – eigene Ableitung gegen Referenzwerkzeuge am privaten Korpus, mit Grundlinie
|
- instructions/dev: Konverter-Vergleich – eigene Ableitung gegen Referenzwerkzeuge am privaten Korpus, mit Grundlinie
|
||||||
@@ -41,6 +42,10 @@ Der Testkorpus mit echten Dokumenten (PDF, Mail, Office, ZIP) und sein Test lieg
|
|||||||
|
|
||||||
Bei einem PDF schreibt `raw derive` die Gliederung als Feld `outline` in den Kopf, je Eintrag mit Ebene, Seite und der Zeile der Seitenmarke in `derived.md`. Im Text stehen drei Auszeichnungen als gewöhnliches Markdown für das, was die Seite nur durch ihre Form sagt: `~~…~~` für Durchstreichung, `**…**` für Textmarker, Fettdruck oder eine deutlich andere Textfarbe, `*…*` für Kursiv. Die Ableitung bestimmt erst die Struktur (Fließtext, Code, Tabellengitter, wiederkehrende Vorlagen) und zeichnet nur aus, wo die Form eindeutig ist; Fett, Kursiv und Farbe zählen nur im Fließtext. Ohne die Auszeichnungen ist der Text derselbe wie zuvor. Das Feld `markings` ist die Legende mit höchstens drei Zeilen, kein Tag überschreitet eine Zeile oder Seitenmarke. Der CHECK nennt nur Seiten mit inhaltlichem Bild oder Diagramm. Das Layout liest `pdfminer.six`, neu in `requirements.txt`; vorhandene `derived.md` bleiben unberührt.
|
Bei einem PDF schreibt `raw derive` die Gliederung als Feld `outline` in den Kopf, je Eintrag mit Ebene, Seite und der Zeile der Seitenmarke in `derived.md`. Im Text stehen drei Auszeichnungen als gewöhnliches Markdown für das, was die Seite nur durch ihre Form sagt: `~~…~~` für Durchstreichung, `**…**` für Textmarker, Fettdruck oder eine deutlich andere Textfarbe, `*…*` für Kursiv. Die Ableitung bestimmt erst die Struktur (Fließtext, Code, Tabellengitter, wiederkehrende Vorlagen) und zeichnet nur aus, wo die Form eindeutig ist; Fett, Kursiv und Farbe zählen nur im Fließtext. Ohne die Auszeichnungen ist der Text derselbe wie zuvor. Das Feld `markings` ist die Legende mit höchstens drei Zeilen, kein Tag überschreitet eine Zeile oder Seitenmarke. Der CHECK nennt nur Seiten mit inhaltlichem Bild oder Diagramm. Das Layout liest `pdfminer.six`, neu in `requirements.txt`; vorhandene `derived.md` bleiben unberührt.
|
||||||
|
|
||||||
|
### Lange Dokumente: Länge als dritte Größenachse (#226)
|
||||||
|
|
||||||
|
Ein einzelnes langes Dokument mit wenigen Themen löste bisher keine der beiden Größenachsen aus und wurde in einem Zug gelesen. Jetzt melden `raw derive` und `raw fetch` jeden geschriebenen Text über der Längenschwelle (Zeichen, nicht Tokens; der Wert steht in `raw derive -h`) mit einer `CHECK`-Zeile, und `raw pending` markiert eine lange `.md`/`.txt` mit `long:` (JSON: `length`). `ingest-large-tree` liest einen solchen Text in Teilen entlang seiner Gliederung, je Teil nur dessen Zeilenbereich, und hält je Teil eine Faktenliste mit Fundstelle, Bedingung, Ausnahme und Modalität fest; die eine Source-Seite entsteht aus allen Listen, nicht aus Zusammenfassungen.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10.0.0 - 2026-10-13 - Einstellungen in drei Stufen, Eingangskanäle, raw fetch mit Site-Handlern und Login, Backup und Exporte
|
## 10.0.0 - 2026-10-13 - Einstellungen in drei Stufen, Eingangskanäle, raw fetch mit Site-Handlern und Login, Backup und Exporte
|
||||||
|
|||||||
@@ -87,6 +87,12 @@ what is waiting and names the entry the next bare `Ingest` will take.
|
|||||||
estimate, proposes a selection, and you choose all, none or a selection. Pages nobody looked at
|
estimate, proposes a selection, and you choose all, none or a selection. Pages nobody looked at
|
||||||
are listed with the reason. Only whole pages are read; a harness that cannot show PDF pages as
|
are listed with the reason. Only whole pages are read; a harness that cannot show PDF pages as
|
||||||
images gets the page numbers named instead.
|
images gets the page numbers named instead.
|
||||||
|
- **Before reading a long document in parts.** When the text `raw derive` or `raw fetch` wrote
|
||||||
|
is longer than one pass reads well, the tool says so in a `CHECK` line, and `raw pending` marks
|
||||||
|
a long text file the same way. The agent then does not read it in one go: it cuts it into parts
|
||||||
|
along its outline (or its pages), shows you the plan in a workshop under `work/`, and after your
|
||||||
|
answer reads one part at a time, noting each fact with where it stands. The document stays one
|
||||||
|
file in `raw/` and gets one source page, written from those notes.
|
||||||
- **Before writing pages**, the agent presents the key takeaways and asks which points matter most
|
- **Before writing pages**, the agent presents the key takeaways and asks which points matter most
|
||||||
and what to emphasise. The pages follow your answer.
|
and what to emphasise. The pages follow your answer.
|
||||||
- **When it publishes**, a source that touches ten or more files stops at the Mass-Update Gate
|
- **When it publishes**, a source that touches ten or more files stops at the Mass-Update Gate
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ tools/wikitool raw fetch --html incoming/post.html --url https://example.org/blo
|
|||||||
- Fetch routes: the `fetch` section of the settings sends a host's pages another way. `session` fetches them with the cookies of the session the user stored with `raw login`, still with the built-in fetch and the bytes as received; the header adds `session: <host>` and never the session's content. The host's site handler names how a page shows it arrived signed in: no stored session, or a page without that sign (an expired session, a teaser), stops with exit 42 and writes nothing. Any other route names an external fetcher of this checkout: run without a shell with the URL as its last argument (120 s, 25 MiB), its stdout is the `.html` byte for byte, and the header says `fetched_by: wikitool raw fetch (fetcher: <name>)` and carries no `http_status`. A fetcher that fails is never replaced by the built-in fetch. A host no route names is fetched as before.
|
- Fetch routes: the `fetch` section of the settings sends a host's pages another way. `session` fetches them with the cookies of the session the user stored with `raw login`, still with the built-in fetch and the bytes as received; the header adds `session: <host>` and never the session's content. The host's site handler names how a page shows it arrived signed in: no stored session, or a page without that sign (an expired session, a teaser), stops with exit 42 and writes nothing. Any other route names an external fetcher of this checkout: run without a shell with the URL as its last argument (120 s, 25 MiB), its stdout is the `.html` byte for byte, and the header says `fetched_by: wikitool raw fetch (fetcher: <name>)` and carries no `http_status`. A fetcher that fails is never replaced by the built-in fetch. A host no route names is fetched as before.
|
||||||
- Rendered pages: a site handler that declares `render` has its pages fetched through a headless browser - with the session, when the host has one - and the `.html` is the page as the browser drew it, not the bytes the server sent; the header says `rendered: true`. No page is rendered without that declaration, and an external fetcher's page never is. It needs the optional browser component; without it the fetch stops with exit 42. `--scope` fetches every page of the bundle the same way, and `_scope.json` names the `session`, `fetcher` or `rendered` it used.
|
- Rendered pages: a site handler that declares `render` has its pages fetched through a headless browser - with the session, when the host has one - and the `.html` is the page as the browser drew it, not the bytes the server sent; the header says `rendered: true`. No page is rendered without that declaration, and an external fetcher's page never is. It needs the optional browser component; without it the fetch stops with exit 42. `--scope` fetches every page of the bundle the same way, and `_scope.json` names the `session`, `fetcher` or `rendered` it used.
|
||||||
- `--html` derives the `.md` beside a page saved to `incoming/` from a logged-in browser - the way past a paywall or a script-rendered page. Its header carries `derived` (when the text was derived) instead of `retrieved`, `final_url`, `http_status` and `content_type`, and the `.html` is left untouched.
|
- `--html` derives the `.md` beside a page saved to `incoming/` from a logged-in browser - the way past a paywall or a script-rendered page. Its header carries `derived` (when the text was derived) instead of `retrieved`, `final_url`, `http_status` and `content_type`, and the `.html` is left untouched.
|
||||||
|
- Length: a `CHECK` line names every `.md` this run wrote - and a `text/plain` or `text/markdown` response stored as received - that is longer than 50,000 characters, the threshold of `raw derive`: more than one ingest reads in one pass, so it is read in parts (`instructions/ingest-large-tree.md`).
|
||||||
- Success prints the `raw accept` line for the written files and the `source_url` for the source page.
|
- Success prints the `raw accept` line for the written files and the `source_url` for the source page.
|
||||||
|
|
||||||
**Properties**
|
**Properties**
|
||||||
@@ -198,6 +199,7 @@ tools/wikitool raw derive incoming/scans.zip
|
|||||||
- A ZIP and a mail are containers. A ZIP at the top of `incoming/` becomes the folder `incoming/<stem>/` holding its files under their own names, each with a `<stem>.md` where a filter reads it; the ZIP is removed and the output names it with its sha256. A mail becomes a folder with `original.eml`, `derived.md` and its attachments under their own names; a ZIP attached to it is unpacked once into a folder of its own, and an attached mail is quoted in `derived.md`, not unpacked. A ZIP inside a ZIP, an absolute or `..` path, a symbolic link, an encrypted entry, more than 1000 entries, a file over 25 MiB or more than 250 MiB in total refuse the whole call. Hidden entries (`__MACOSX/`, `.DS_Store`, anything starting with a dot) are named, not written. A name taken twice, or reserved (`original.*` and `derived.md` at the top of a mail's folder, `_capture.json` anywhere), gets `-2`, `-3` before its extension, and the output lists each rename.
|
- A ZIP and a mail are containers. A ZIP at the top of `incoming/` becomes the folder `incoming/<stem>/` holding its files under their own names, each with a `<stem>.md` where a filter reads it; the ZIP is removed and the output names it with its sha256. A mail becomes a folder with `original.eml`, `derived.md` and its attachments under their own names; a ZIP attached to it is unpacked once into a folder of its own, and an attached mail is quoted in `derived.md`, not unpacked. A ZIP inside a ZIP, an absolute or `..` path, a symbolic link, an encrypted entry, more than 1000 entries, a file over 25 MiB or more than 250 MiB in total refuse the whole call. Hidden entries (`__MACOSX/`, `.DS_Store`, anything starting with a dot) are named, not written. A name taken twice, or reserved (`original.*` and `derived.md` at the top of a mail's folder, `_capture.json` anywhere), gets `-2`, `-3` before its extension, and the output lists each rename.
|
||||||
- A name already taken under `raw/` is a WARN, not a refusal: whether the folder is a later edition of that source or a separate one is the human's call. A later edition of a derived source is replaced file by file with `raw accept --replaces`; there is no `--replaces-bundle` for it.
|
- A name already taken under `raw/` is a WARN, not a refusal: whether the folder is a later edition of that source or a separate one is the human's call. A later edition of a derived source is replaced file by file with `raw accept --replaces`; there is no `--replaces-bundle` for it.
|
||||||
- After a success the output prints the warnings the filters raised, at most three `CHECK` lines per format that was derived in this run - what to look at before the text is trusted - and the `raw accept` line for the result.
|
- After a success the output prints the warnings the filters raised, at most three `CHECK` lines per format that was derived in this run - what to look at before the text is trusted - and the `raw accept` line for the result.
|
||||||
|
- Length: one more `CHECK` line names every `.md` this run wrote that is longer than 50,000 characters, counted in the file as written, header included - more than one ingest reads in one pass, so it is read in parts (`instructions/ingest-large-tree.md`). Characters, not tokens: they do not depend on a model's tokenizer. `raw fetch` and `raw pending` measure against the same value.
|
||||||
- Never edit `derived.md` by hand: a better derivation is a new `raw derive` of the original.
|
- Never edit `derived.md` by hand: a better derivation is a new `raw derive` of the original.
|
||||||
|
|
||||||
**Properties**
|
**Properties**
|
||||||
@@ -371,9 +373,10 @@ tools/wikitool raw pending --json
|
|||||||
- Order: oldest first by modification time. A bundle or folder counts as new as its newest file; a tie goes by name. The mtime is when a document last changed only if it was copied with its timestamps kept (`cp -p`, `rsync -a`, an unpacked archive) - for a download or a `raw fetch` it is merely when it was dropped.
|
- Order: oldest first by modification time. A bundle or folder counts as new as its newest file; a tie goes by name. The mtime is when a document last changed only if it was copied with its timestamps kept (`cp -p`, `rsync -a`, an unpacked archive) - for a download or a `raw fetch` it is merely when it was dropped.
|
||||||
- Each candidate shows its path(s), kind, file count and mtime, and whether `raw accept` would take it as it stands - the same checks, minus `--fidelity`/`--authority`. One it would refuse is listed with the reason and skipped: it needs a human.
|
- Each candidate shows its path(s), kind, file count and mtime, and whether `raw accept` would take it as it stands - the same checks, minus `--fidelity`/`--authority`. One it would refuse is listed with the reason and skipped: it needs a human.
|
||||||
- The default is the first candidate `raw accept` would take, and the output names it.
|
- The default is the first candidate `raw accept` would take, and the output names it.
|
||||||
- `--json` prints the same candidates in the same order: `kind`, `paths`, `files`, `mtime`, `acceptable`, `reason`, `default`, `hint`.
|
- `--json` prints the same candidates in the same order: `kind`, `paths`, `files`, `mtime`, `acceptable`, `reason`, `default`, `hint`, `length`.
|
||||||
- A lone file that a `raw derive` filter reads (a PDF, mail, Office file or web page) is listed with the `raw derive` line for it, in text output and as `hint` in JSON (`null` for every other candidate). The hint changes nothing about the order or the default.
|
- A lone file that a `raw derive` filter reads (a PDF, mail, Office file or web page) is listed with the `raw derive` line for it, in text output and as `hint` in JSON (`null` for every other candidate). The hint changes nothing about the order or the default.
|
||||||
- Reads directory listings and `lstat` only, never a file's content; an empty `incoming/` is exit 0 with nothing to do.
|
- A lone `.md` or `.txt` file longer than the length threshold of `raw derive` and `raw fetch` is listed with its length in characters, in text output and as `length` in JSON (`null` for every other candidate) - that text reaches a session with no derivation in front of it to print the length `CHECK`. Like the hint, it changes nothing about the order or the default.
|
||||||
|
- Reads directory listings and `lstat`, and a file's content only to measure a lone `.md`/`.txt` whose size in bytes is over the threshold - a UTF-8 text has no more characters than bytes; an empty `incoming/` is exit 0 with nothing to do.
|
||||||
|
|
||||||
**Properties**
|
**Properties**
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
type: types/instruction.md
|
type: types/instruction.md
|
||||||
name: ingest-large-tree
|
name: ingest-large-tree
|
||||||
description: Ingest a raw tree too large, or a single source too broad, for one pass - in planned units through a work/ workshop, instead of one oversized source page or a cohort of stub pages.
|
description: Ingest a raw tree too large, or a single source too broad or too long, for one pass - in planned units through a work/ workshop, instead of one oversized source page, a cohort of stub pages or a text read in one go.
|
||||||
---
|
---
|
||||||
# Ingest a large raw tree
|
# Ingest a large raw tree
|
||||||
|
|
||||||
@@ -10,7 +10,9 @@ read, promoted and published on its own. The plan and the intermediate extracts
|
|||||||
`work/` workshop, so the run survives across sessions and days instead of having to fit in one.
|
`work/` workshop, so the run survives across sessions and days instead of having to fit in one.
|
||||||
|
|
||||||
A single source carrying too many subjects lands here too, and takes the workshop but not the
|
A single source carrying too many subjects lands here too, and takes the workshop but not the
|
||||||
cut - see [A broad source is not cut](#a-broad-source-is-not-cut).
|
cut - see [A broad source is not cut](#a-broad-source-is-not-cut). So does a single source too
|
||||||
|
long to read in one pass, which keeps its one raw file and is read in parts - see
|
||||||
|
[A long source is read in parts](#a-long-source-is-read-in-parts).
|
||||||
|
|
||||||
<!-- wikitool:toc -->
|
<!-- wikitool:toc -->
|
||||||
## Contents
|
## Contents
|
||||||
@@ -18,6 +20,7 @@ cut - see [A broad source is not cut](#a-broad-source-is-not-cut).
|
|||||||
- [When to run](#when-to-run)
|
- [When to run](#when-to-run)
|
||||||
- [Tiers](#tiers)
|
- [Tiers](#tiers)
|
||||||
- [A broad source is not cut](#a-broad-source-is-not-cut)
|
- [A broad source is not cut](#a-broad-source-is-not-cut)
|
||||||
|
- [A long source is read in parts](#a-long-source-is-read-in-parts)
|
||||||
- [Steps](#steps)
|
- [Steps](#steps)
|
||||||
- [Decision points](#decision-points)
|
- [Decision points](#decision-points)
|
||||||
- [Scope](#scope)
|
- [Scope](#scope)
|
||||||
@@ -25,7 +28,7 @@ cut - see [A broad source is not cut](#a-broad-source-is-not-cut).
|
|||||||
|
|
||||||
## When to run
|
## When to run
|
||||||
|
|
||||||
Any one of these is enough, on either axis.
|
Any one of these is enough, on any of the three axes.
|
||||||
|
|
||||||
**Volume** - more material than one pass can read:
|
**Volume** - more material than one pass can read:
|
||||||
|
|
||||||
@@ -40,9 +43,26 @@ Any one of these is enough, on either axis.
|
|||||||
concepts together**. Estimate it from the reading, before writing anything; on a finished page
|
concepts together**. Estimate it from the reading, before writing anything; on a finished page
|
||||||
the same number is the length of `entities:` plus `concepts:`.
|
the same number is the length of `entities:` plus `concepts:`.
|
||||||
|
|
||||||
The two axes take different routes through this procedure. Volume is cut: several files become
|
**Length** - one text longer than one pass reads well:
|
||||||
several units, each its own source page. Breadth is not cut at all - it takes the workshop for
|
|
||||||
the extract pass and nothing else (§ [A broad source is not cut](#a-broad-source-is-not-cut)).
|
- `raw derive` or `raw fetch` printed the `CHECK` line naming the text as longer than one ingest
|
||||||
|
reads in one pass, or `raw pending` listed the entry with `long:`. For a `.md` or `.txt` that
|
||||||
|
is not in `incoming/`, compare `wc -m <file>` with the value `tools/wikitool raw derive -h`
|
||||||
|
names.
|
||||||
|
|
||||||
|
Length is an axis of its own because neither of the others fires for one long document on a
|
||||||
|
few subjects, and reading it whole costs more than it seems. A session has already loaded a
|
||||||
|
large fixed context before it opens the source - the harness's own, `AGENTS.md`, the skill, the
|
||||||
|
contracts and conventions an ingest reads. Summaries of long input lose far more by leaving facts
|
||||||
|
out than by getting them wrong, and small models lose recall well before their context window is
|
||||||
|
full. Summarising the summaries of the parts does not recover it either. So the text is read in
|
||||||
|
parts, and what each part yields is a list of facts, not a summary.
|
||||||
|
|
||||||
|
The three axes take different routes through this procedure. Volume is cut: several files become
|
||||||
|
several units, each its own source page. Breadth and length are not cut - one raw file keeps one
|
||||||
|
source page. Breadth takes the workshop for the extract pass
|
||||||
|
(§ [A broad source is not cut](#a-broad-source-is-not-cut)); length takes it for reading in parts
|
||||||
|
and an extract per part (§ [A long source is read in parts](#a-long-source-is-read-in-parts)).
|
||||||
|
|
||||||
Otherwise use `wiki-ingest` unchanged. This procedure costs a workshop and a planning round;
|
Otherwise use `wiki-ingest` unchanged. This procedure costs a workshop and a planning round;
|
||||||
a single, narrow document does not earn it.
|
a single, narrow document does not earn it.
|
||||||
@@ -54,6 +74,7 @@ a single, narrow document does not earn it.
|
|||||||
| Standard | One file, or a small folder | `wiki-ingest`, unchanged |
|
| Standard | One file, or a small folder | `wiki-ingest`, unchanged |
|
||||||
| Tree | A volume trigger above | This instruction |
|
| Tree | A volume trigger above | This instruction |
|
||||||
| Broad | The breadth trigger above | This instruction, § A broad source is not cut |
|
| Broad | The breadth trigger above | This instruction, § A broad source is not cut |
|
||||||
|
| Long | The length trigger above | This instruction, § A long source is read in parts |
|
||||||
| Audited | A unit covering secrets, RBAC, ingress, disaster recovery, or an audit trail | This instruction plus step 5c |
|
| Audited | A unit covering secrets, RBAC, ingress, disaster recovery, or an audit trail | This instruction plus step 5c |
|
||||||
|
|
||||||
## A broad source is not cut
|
## A broad source is not cut
|
||||||
@@ -91,6 +112,25 @@ produces when every name in the source is turned into a page: pages that restate
|
|||||||
pass `lint` (which measures structure, never substance), and read as covered ground to the next
|
pass `lint` (which measures structure, never substance), and read as covered ground to the next
|
||||||
session.
|
session.
|
||||||
|
|
||||||
|
## A long source is read in parts
|
||||||
|
|
||||||
|
A long source is one raw file, so the two rules of
|
||||||
|
[A broad source is not cut](#a-broad-source-is-not-cut) hold for it unchanged: `raw/` keeps it
|
||||||
|
whole, and it gets one source page. The cut is in the reading, never in the file. What the
|
||||||
|
workshop buys:
|
||||||
|
|
||||||
|
1. `tools/wikitool work new --input <the file, or the folder raw derive made>`, then one unit per
|
||||||
|
**part of the text** in `plan.md` - step 3 says where a long source is cut.
|
||||||
|
2. Per unit, steps 5a and 5b: read the unit's line range and nothing else, and extract a fact
|
||||||
|
list.
|
||||||
|
3. One source page, one publish: steps 5d-5e run once, after the last unit, over all the
|
||||||
|
extracts. The page is written from the fact lists of every unit - never from summaries of
|
||||||
|
the units, which is the summary of summaries this route exists to avoid.
|
||||||
|
|
||||||
|
**Long and broad at once?** The same workshop, units by the text's parts. Which subjects earn a
|
||||||
|
page - point 3 of the broad route - is decided over the collected extracts, after the last unit,
|
||||||
|
not unit by unit.
|
||||||
|
|
||||||
## Steps
|
## Steps
|
||||||
|
|
||||||
1. **Survey the tree, do not read it yet.**
|
1. **Survey the tree, do not read it yet.**
|
||||||
@@ -129,6 +169,17 @@ session.
|
|||||||
planned page title, and the reason for the cut. Record what is excluded from the run
|
planned page title, and the reason for the cut. Record what is excluded from the run
|
||||||
entirely, and why.
|
entirely, and why.
|
||||||
|
|
||||||
|
**A long source is cut by its outline**, not by subject: a unit is a page range, cut at the
|
||||||
|
highest outline level whose sections each stay under the length the `CHECK` line names. A
|
||||||
|
section longer than that - a long annex, a commentary part - is cut at the next level down,
|
||||||
|
or at page boundaries where it has none. A derived PDF's `outline` header field gives each
|
||||||
|
entry's page and the line of that page's marker, so the ranges come from the header, not from
|
||||||
|
reading the text. Without an outline the units are page ranges between the
|
||||||
|
`<!-- page N -->` markers (`grep -n '<!-- page ' <file>`); a text without those either - a
|
||||||
|
fetched page, a plain text - is cut at its own headings (`grep -n '^#' <file>`), else at line
|
||||||
|
numbers. Measure a range with `sed -n '<from>,<to>p' <file> | wc -m`. For each unit record its
|
||||||
|
title, its pages and its **line range** in `plan.md`: that range is all step 5a reads.
|
||||||
|
|
||||||
Then fill the `README.md` checklist - one line per unit.
|
Then fill the `README.md` checklist - one line per unit.
|
||||||
|
|
||||||
4. **Agree the plan with the user.** This is the one decision checkpoint for the whole run:
|
4. **Agree the plan with the user.** This is the one decision checkpoint for the whole run:
|
||||||
@@ -144,11 +195,21 @@ session.
|
|||||||
(AGENTS.md invariant 4). A derived PDF whose header has `images:` or `unmatched:` is
|
(AGENTS.md invariant 4). A derived PDF whose header has `images:` or `unmatched:` is
|
||||||
followed by `read-page-images.md`, whose threshold of 20 pages holds per unit.
|
followed by `read-page-images.md`, whose threshold of 20 pages holds per unit.
|
||||||
|
|
||||||
|
**A long source:** read the unit's line range from `plan.md` and nothing else - the header
|
||||||
|
once, in the first unit. Reading the whole file again in every unit is the one pass this
|
||||||
|
route replaces, repeated. The page images are those of the unit's own pages.
|
||||||
|
|
||||||
b. **Extract** into `work/<runkey>/extract-u<N>.md`: the hard facts (IPs, ports, versions,
|
b. **Extract** into `work/<runkey>/extract-u<N>.md`: the hard facts (IPs, ports, versions,
|
||||||
paths, commands, config values), each with the raw file it came from, plus what is
|
paths, commands, config values), each with the raw file it came from, plus what is
|
||||||
*new* relative to what step 1's searches found. Write down what you are dropping and
|
*new* relative to what step 1's searches found. Write down what you are dropping and
|
||||||
why - that becomes the page's `## Not Extracted` section.
|
why - that becomes the page's `## Not Extracted` section.
|
||||||
|
|
||||||
|
**A long source:** the extract is a **fact list**, one entry per fact, each with where it
|
||||||
|
stands - the page, and the rule or section number where the text has one - and, where the
|
||||||
|
text states them, its condition, its exception, its deadline or number, and its modality
|
||||||
|
(*must*, *should*, *may*, *must not*). A normative text - rules, a regulation, a contract -
|
||||||
|
binds through exactly those parts, and they are what a summary drops first.
|
||||||
|
|
||||||
c. **Audited tier only:** before touching any existing page, check the extract back against
|
c. **Audited tier only:** before touching any existing page, check the extract back against
|
||||||
the raw files and record findings in `work/<runkey>/audit.md`, each as
|
the raw files and record findings in `work/<runkey>/audit.md`, each as
|
||||||
`Status: open` / `Status: resolved` with what changed. Do not promote while a finding is
|
`Status: open` / `Status: resolved` with what changed. Do not promote while a finding is
|
||||||
@@ -157,8 +218,8 @@ session.
|
|||||||
|
|
||||||
d. **Promote** with `wiki-ingest` steps 4-10, using the extract as the input rather than the
|
d. **Promote** with `wiki-ingest` steps 4-10, using the extract as the input rather than the
|
||||||
raw files - step 5 (promotion itself) is a no-op here, since the unit's raw file is
|
raw files - step 5 (promotion itself) is a no-op here, since the unit's raw file is
|
||||||
already under `raw/` (§ [When to run](#when-to-run) named the volume/breadth trigger that
|
already under `raw/` (§ [When to run](#when-to-run) named the trigger that put it
|
||||||
put it there). Fill `## Not Extracted` from b.
|
there). Fill `## Not Extracted` from b.
|
||||||
|
|
||||||
This includes step 4's commitment question, asked once **per unit** rather than once for
|
This includes step 4's commitment question, asked once **per unit** rather than once for
|
||||||
the whole tree: a unit is a subject the same way a single-file `wiki-ingest` source is one,
|
the whole tree: a unit is a subject the same way a single-file `wiki-ingest` source is one,
|
||||||
@@ -190,6 +251,10 @@ session.
|
|||||||
- **The breadth trigger fired, but the source reads narrower than it looked?** Close the
|
- **The breadth trigger fired, but the source reads narrower than it looked?** Close the
|
||||||
workshop and run an ordinary `wiki-ingest`. The count is estimated before the reading, so
|
workshop and run an ordinary `wiki-ingest`. The count is estimated before the reading, so
|
||||||
being wrong about it is expected; carrying a workshop nobody needs is the avoidable half.
|
being wrong about it is expected; carrying a workshop nobody needs is the avoidable half.
|
||||||
|
- **The length trigger fired for a text that is mostly not prose** - a long register, tables of
|
||||||
|
figures, an annex of forms? The units still cut the reading. A unit with nothing to extract is
|
||||||
|
recorded as excluded in `plan.md`, with the reason, and that reason goes into
|
||||||
|
`## Not Extracted`.
|
||||||
- **A unit turns out to be a duplicate of an existing page?** Update that page instead of
|
- **A unit turns out to be a duplicate of an existing page?** Update that page instead of
|
||||||
creating a second one, and say so in `plan.md`. That is a result, not a failure.
|
creating a second one, and say so in `plan.md`. That is a result, not a failure.
|
||||||
- **The plan changes mid-run?** Edit `plan.md` and the checklist, and say why in `README.md`.
|
- **The plan changes mid-run?** Edit `plan.md` and the checklist, and say why in `README.md`.
|
||||||
@@ -199,7 +264,7 @@ session.
|
|||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This is about *size*, on the two axes § When to run names: volume and breadth. It is not about
|
This is about *size*, on the three axes § When to run names: volume, breadth and length. It is not about
|
||||||
difficulty - a short but hard source, a specification that needs careful reading, is still an
|
difficulty - a short but hard source, a specification that needs careful reading, is still an
|
||||||
ordinary `wiki-ingest`. And nothing here changes what a page must contain:
|
ordinary `wiki-ingest`. And nothing here changes what a page must contain:
|
||||||
[kb/CONTRACT.md](../kb/CONTRACT.md) and the collection contracts still decide that.
|
[kb/CONTRACT.md](../kb/CONTRACT.md) and the collection contracts still decide that.
|
||||||
@@ -42,7 +42,8 @@ validator complains - and the ticked list is the only record that they happened.
|
|||||||
|
|
||||||
## Steps
|
## Steps
|
||||||
|
|
||||||
1. **Read the source.** Read the file completely, wherever it currently sits - `incoming/` for
|
1. **Read the source.** Read the file completely - unless the length check below fires, which
|
||||||
|
it does before anything is read - wherever it currently sits: `incoming/` for
|
||||||
the normal path, or already under `raw/` when the run started there (a file `capture-session`
|
the normal path, or already under `raw/` when the run started there (a file `capture-session`
|
||||||
just wrote, for instance, which skips step 5 entirely). A PDF, mail, Office file or ZIP: run
|
just wrote, for instance, which skips step 5 entirely). A PDF, mail, Office file or ZIP: run
|
||||||
`tools/wikitool raw derive <path>` first and read the `.md` - its output names what to check.
|
`tools/wikitool raw derive <path>` first and read the `.md` - its output names what to check.
|
||||||
@@ -165,14 +166,19 @@ validator complains - and the ticked list is the only record that they happened.
|
|||||||
reads often, mention the lasting way as well: fetching with their own session
|
reads often, mention the lasting way as well: fetching with their own session
|
||||||
(`docs/how-to/fetch-pages-behind-a-login.md`) - set up by the user, not by you.
|
(`docs/how-to/fetch-pages-behind-a-login.md`) - set up by the user, not by you.
|
||||||
|
|
||||||
**Check the size first, on both axes.** *Volume* - how many raw files this ingest covers -
|
**Check the size first, on all three axes.** *Volume* - how many raw files this ingest
|
||||||
and *breadth* - how many entities and concepts this one source would produce or update.
|
covers -, *breadth* - how many entities and concepts this one source would produce or update -
|
||||||
Either one past the thresholds in `instructions/ingest-large-tree.md` § When to
|
and *length* - whether one text is longer than one pass reads. Length is known before the
|
||||||
|
reading: `raw derive` and `raw fetch` print a `CHECK` line naming such a text, and
|
||||||
|
`raw pending` lists a lone `.md` or `.txt` with `long:`; a text file named outside
|
||||||
|
`incoming/` is measured with `wc -m <file>` against the value `tools/wikitool raw derive -h`
|
||||||
|
names. Any one past the thresholds in `instructions/ingest-large-tree.md` § When to
|
||||||
run is that procedure, not this one: stop and follow it. There, volume is cut into units;
|
run is that procedure, not this one: stop and follow it. There, volume is cut into units;
|
||||||
breadth cannot be cut at all (`raw/` keeps a file whole, and one raw file has one owning
|
breadth and length cannot be cut (`raw/` keeps a file whole, and one raw file has one owning
|
||||||
source page) and buys an extract pass instead, before any page is written. Skipping either
|
source page) - breadth buys an extract pass before any page is written, length a reading in
|
||||||
fails silently: an oversized source page drops most of what it read, and an over-broad one
|
parts with a fact list per part. Skipping any of them fails silently: an oversized source
|
||||||
leaves a cohort of stub pages behind.
|
page drops most of what it read, an over-broad one leaves a cohort of stub pages behind, and
|
||||||
|
a long text read in one go loses its facts by leaving them out.
|
||||||
|
|
||||||
**A trigger firing here promotes now, ahead of step 4's commitment discussion below - the one
|
**A trigger firing here promotes now, ahead of step 4's commitment discussion below - the one
|
||||||
deliberate exception to this skill's ordering.** `ingest-large-tree.md`'s own step 2
|
deliberate exception to this skill's ordering.** `ingest-large-tree.md`'s own step 2
|
||||||
@@ -482,6 +488,11 @@ validator complains - and the ticked list is the only record that they happened.
|
|||||||
- **One source names far more subjects than usual?** That is breadth, not volume. It is not
|
- **One source names far more subjects than usual?** That is breadth, not volume. It is not
|
||||||
split into several sources - it cannot be - and it does not get a page per name either:
|
split into several sources - it cannot be - and it does not get a page per name either:
|
||||||
`instructions/ingest-large-tree.md` § A broad source is not cut.
|
`instructions/ingest-large-tree.md` § A broad source is not cut.
|
||||||
|
- **One source is longer than one pass reads?** That is length - the `CHECK` line of
|
||||||
|
`raw derive` or `raw fetch` says so, or `raw pending`'s `long:`. It is not split into several
|
||||||
|
sources and not summarised whole: it is read in parts along its outline, each part yielding a
|
||||||
|
fact list, and one source page is written from all of them -
|
||||||
|
`instructions/ingest-large-tree.md` § A long source is read in parts.
|
||||||
- **The entry is a new edition of a captured bundle?** Step 1's branch for updating the captured
|
- **The entry is a new edition of a captured bundle?** Step 1's branch for updating the captured
|
||||||
repositories is how a run gets here: `raw status` reported it, `raw capture --update` wrote it,
|
repositories is how a run gets here: `raw status` reported it, `raw capture --update` wrote it,
|
||||||
and `raw accept incoming/<bundle> --replaces-bundle <raw-bundle>` takes it in - the name refusal
|
and `raw accept incoming/<bundle> --replaces-bundle <raw-bundle>` takes it in - the name refusal
|
||||||
|
|||||||
@@ -417,6 +417,15 @@ them is [read-page-images.md](../instructions/read-page-images.md). A PDF withou
|
|||||||
markings, images and unmatched pages is derived as before. Reading the layout needs `pdfminer.six` beside
|
markings, images and unmatched pages is derived as before. Reading the layout needs `pdfminer.six` beside
|
||||||
`pypdf`.
|
`pypdf`.
|
||||||
|
|
||||||
|
**A text too long for one pass is named.** After a success `raw derive` prints one more `CHECK`
|
||||||
|
line naming every `.md` it wrote that is longer than one ingest reads in one pass. Length is
|
||||||
|
counted in characters of the file as written, header included, not in tokens: characters are the
|
||||||
|
same for every model, tokens depend on its tokenizer. `raw fetch` prints the same line for the
|
||||||
|
text it writes, and `raw pending` marks a lone `.md` or `.txt` in `incoming/` the same way, since
|
||||||
|
no derivation stands in front of it to print one. The value is the tool's, and `raw derive -h`
|
||||||
|
names it; what an ingest does with such a text is
|
||||||
|
[ingest-large-tree.md](../instructions/ingest-large-tree.md).
|
||||||
|
|
||||||
**The original stays.** The derivation is the tool's reading of it, never the source: with the
|
**The original stays.** The derivation is the tool's reading of it, never the source: with the
|
||||||
original kept, a claim in `kb/` can still be checked against what the document said when a
|
original kept, a claim in `kb/` can still be checked against what the document said when a
|
||||||
derivation dropped something, and a later version of the tool can derive it better. Both go into
|
derivation dropped something, and a later version of the tool can derive it better. Both go into
|
||||||
|
|||||||
+5
-2
@@ -1495,6 +1495,7 @@ Capture a web page the user names into `incoming/`: the HTML as received plus a
|
|||||||
- Fetch routes: the `fetch` section of the settings sends a host's pages another way. `session` fetches them with the cookies of the session the user stored with `raw login`, still with the built-in fetch and the bytes as received; the header adds `session: <host>` and never the session's content. The host's site handler names how a page shows it arrived signed in: no stored session, or a page without that sign (an expired session, a teaser), stops with exit 42 and writes nothing. Any other route names an external fetcher of this checkout: run without a shell with the URL as its last argument (120 s, 25 MiB), its stdout is the `.html` byte for byte, and the header says `fetched_by: wikitool raw fetch (fetcher: <name>)` and carries no `http_status`. A fetcher that fails is never replaced by the built-in fetch. A host no route names is fetched as before.
|
- Fetch routes: the `fetch` section of the settings sends a host's pages another way. `session` fetches them with the cookies of the session the user stored with `raw login`, still with the built-in fetch and the bytes as received; the header adds `session: <host>` and never the session's content. The host's site handler names how a page shows it arrived signed in: no stored session, or a page without that sign (an expired session, a teaser), stops with exit 42 and writes nothing. Any other route names an external fetcher of this checkout: run without a shell with the URL as its last argument (120 s, 25 MiB), its stdout is the `.html` byte for byte, and the header says `fetched_by: wikitool raw fetch (fetcher: <name>)` and carries no `http_status`. A fetcher that fails is never replaced by the built-in fetch. A host no route names is fetched as before.
|
||||||
- Rendered pages: a site handler that declares `render` has its pages fetched through a headless browser - with the session, when the host has one - and the `.html` is the page as the browser drew it, not the bytes the server sent; the header says `rendered: true`. No page is rendered without that declaration, and an external fetcher's page never is. It needs the optional browser component; without it the fetch stops with exit 42. `--scope` fetches every page of the bundle the same way, and `_scope.json` names the `session`, `fetcher` or `rendered` it used.
|
- Rendered pages: a site handler that declares `render` has its pages fetched through a headless browser - with the session, when the host has one - and the `.html` is the page as the browser drew it, not the bytes the server sent; the header says `rendered: true`. No page is rendered without that declaration, and an external fetcher's page never is. It needs the optional browser component; without it the fetch stops with exit 42. `--scope` fetches every page of the bundle the same way, and `_scope.json` names the `session`, `fetcher` or `rendered` it used.
|
||||||
- `--html` derives the `.md` beside a page saved to `incoming/` from a logged-in browser - the way past a paywall or a script-rendered page. Its header carries `derived` (when the text was derived) instead of `retrieved`, `final_url`, `http_status` and `content_type`, and the `.html` is left untouched.
|
- `--html` derives the `.md` beside a page saved to `incoming/` from a logged-in browser - the way past a paywall or a script-rendered page. Its header carries `derived` (when the text was derived) instead of `retrieved`, `final_url`, `http_status` and `content_type`, and the `.html` is left untouched.
|
||||||
|
- Length: a `CHECK` line names every `.md` this run wrote - and a `text/plain` or `text/markdown` response stored as received - that is longer than 50,000 characters, the threshold of `raw derive`: more than one ingest reads in one pass, so it is read in parts (`instructions/ingest-large-tree.md`).
|
||||||
- Success prints the `raw accept` line for the written files and the `source_url` for the source page.
|
- Success prints the `raw accept` line for the written files and the `source_url` for the source page.
|
||||||
|
|
||||||
**SEE ALSO**
|
**SEE ALSO**
|
||||||
@@ -1613,6 +1614,7 @@ Derive reading text from a PDF, e-mail, Office file or web page in `incoming/`,
|
|||||||
- A ZIP and a mail are containers. A ZIP at the top of `incoming/` becomes the folder `incoming/<stem>/` holding its files under their own names, each with a `<stem>.md` where a filter reads it; the ZIP is removed and the output names it with its sha256. A mail becomes a folder with `original.eml`, `derived.md` and its attachments under their own names; a ZIP attached to it is unpacked once into a folder of its own, and an attached mail is quoted in `derived.md`, not unpacked. A ZIP inside a ZIP, an absolute or `..` path, a symbolic link, an encrypted entry, more than 1000 entries, a file over 25 MiB or more than 250 MiB in total refuse the whole call. Hidden entries (`__MACOSX/`, `.DS_Store`, anything starting with a dot) are named, not written. A name taken twice, or reserved (`original.*` and `derived.md` at the top of a mail's folder, `_capture.json` anywhere), gets `-2`, `-3` before its extension, and the output lists each rename.
|
- A ZIP and a mail are containers. A ZIP at the top of `incoming/` becomes the folder `incoming/<stem>/` holding its files under their own names, each with a `<stem>.md` where a filter reads it; the ZIP is removed and the output names it with its sha256. A mail becomes a folder with `original.eml`, `derived.md` and its attachments under their own names; a ZIP attached to it is unpacked once into a folder of its own, and an attached mail is quoted in `derived.md`, not unpacked. A ZIP inside a ZIP, an absolute or `..` path, a symbolic link, an encrypted entry, more than 1000 entries, a file over 25 MiB or more than 250 MiB in total refuse the whole call. Hidden entries (`__MACOSX/`, `.DS_Store`, anything starting with a dot) are named, not written. A name taken twice, or reserved (`original.*` and `derived.md` at the top of a mail's folder, `_capture.json` anywhere), gets `-2`, `-3` before its extension, and the output lists each rename.
|
||||||
- A name already taken under `raw/` is a WARN, not a refusal: whether the folder is a later edition of that source or a separate one is the human's call. A later edition of a derived source is replaced file by file with `raw accept --replaces`; there is no `--replaces-bundle` for it.
|
- A name already taken under `raw/` is a WARN, not a refusal: whether the folder is a later edition of that source or a separate one is the human's call. A later edition of a derived source is replaced file by file with `raw accept --replaces`; there is no `--replaces-bundle` for it.
|
||||||
- After a success the output prints the warnings the filters raised, at most three `CHECK` lines per format that was derived in this run - what to look at before the text is trusted - and the `raw accept` line for the result.
|
- After a success the output prints the warnings the filters raised, at most three `CHECK` lines per format that was derived in this run - what to look at before the text is trusted - and the `raw accept` line for the result.
|
||||||
|
- Length: one more `CHECK` line names every `.md` this run wrote that is longer than 50,000 characters, counted in the file as written, header included - more than one ingest reads in one pass, so it is read in parts (`instructions/ingest-large-tree.md`). Characters, not tokens: they do not depend on a model's tokenizer. `raw fetch` and `raw pending` measure against the same value.
|
||||||
- Never edit `derived.md` by hand: a better derivation is a new `raw derive` of the original.
|
- Never edit `derived.md` by hand: a better derivation is a new `raw derive` of the original.
|
||||||
|
|
||||||
**SEE ALSO**
|
**SEE ALSO**
|
||||||
@@ -1766,9 +1768,10 @@ List what waits in `incoming/`, oldest first, and name the entry an ingest witho
|
|||||||
- Order: oldest first by modification time. A bundle or folder counts as new as its newest file; a tie goes by name. The mtime is when a document last changed only if it was copied with its timestamps kept (`cp -p`, `rsync -a`, an unpacked archive) - for a download or a `raw fetch` it is merely when it was dropped.
|
- Order: oldest first by modification time. A bundle or folder counts as new as its newest file; a tie goes by name. The mtime is when a document last changed only if it was copied with its timestamps kept (`cp -p`, `rsync -a`, an unpacked archive) - for a download or a `raw fetch` it is merely when it was dropped.
|
||||||
- Each candidate shows its path(s), kind, file count and mtime, and whether `raw accept` would take it as it stands - the same checks, minus `--fidelity`/`--authority`. One it would refuse is listed with the reason and skipped: it needs a human.
|
- Each candidate shows its path(s), kind, file count and mtime, and whether `raw accept` would take it as it stands - the same checks, minus `--fidelity`/`--authority`. One it would refuse is listed with the reason and skipped: it needs a human.
|
||||||
- The default is the first candidate `raw accept` would take, and the output names it.
|
- The default is the first candidate `raw accept` would take, and the output names it.
|
||||||
- `--json` prints the same candidates in the same order: `kind`, `paths`, `files`, `mtime`, `acceptable`, `reason`, `default`, `hint`.
|
- `--json` prints the same candidates in the same order: `kind`, `paths`, `files`, `mtime`, `acceptable`, `reason`, `default`, `hint`, `length`.
|
||||||
- A lone file that a `raw derive` filter reads (a PDF, mail, Office file or web page) is listed with the `raw derive` line for it, in text output and as `hint` in JSON (`null` for every other candidate). The hint changes nothing about the order or the default.
|
- A lone file that a `raw derive` filter reads (a PDF, mail, Office file or web page) is listed with the `raw derive` line for it, in text output and as `hint` in JSON (`null` for every other candidate). The hint changes nothing about the order or the default.
|
||||||
- Reads directory listings and `lstat` only, never a file's content; an empty `incoming/` is exit 0 with nothing to do.
|
- A lone `.md` or `.txt` file longer than the length threshold of `raw derive` and `raw fetch` is listed with its length in characters, in text output and as `length` in JSON (`null` for every other candidate) - that text reaches a session with no derivation in front of it to print the length `CHECK`. Like the hint, it changes nothing about the order or the default.
|
||||||
|
- Reads directory listings and `lstat`, and a file's content only to measure a lone `.md`/`.txt` whose size in bytes is over the threshold - a UTF-8 text has no more characters than bytes; an empty `incoming/` is exit 0 with nothing to do.
|
||||||
|
|
||||||
**SEE ALSO**
|
**SEE ALSO**
|
||||||
|
|
||||||
|
|||||||
@@ -508,6 +508,56 @@ def _derive_hint(path: Path) -> Optional[str]:
|
|||||||
return f"tools/wikitool raw derive {rel_path(path)}"
|
return f"tools/wikitool raw derive {rel_path(path)}"
|
||||||
|
|
||||||
|
|
||||||
|
# The length axis of an ingest (Gitea #226): a text longer than this many characters
|
||||||
|
# is read in parts, through `instructions/ingest-large-tree.md`, not in one pass.
|
||||||
|
# Characters rather than tokens, because they do not depend on a model's tokenizer:
|
||||||
|
# ~15 k tokens of German at ~3.3 characters a token, set early because a session's
|
||||||
|
# fixed load already fills much of its context. The value stands here and nowhere
|
||||||
|
# else - `raw derive`, `raw fetch` and `raw pending` all read it from this line.
|
||||||
|
LONG_TEXT_CHARS = 50_000
|
||||||
|
|
||||||
|
# The files `raw pending` measures: text a session reads as it is, with no derivation
|
||||||
|
# - and so no `raw derive` or `raw fetch` - in front of it to print the length CHECK.
|
||||||
|
_PLAIN_TEXT_SUFFIXES = (".md", ".txt")
|
||||||
|
|
||||||
|
|
||||||
|
def _char_count(data: bytes) -> int:
|
||||||
|
"""Characters of a UTF-8 text, the way a session reads it: an undecodable
|
||||||
|
byte counts as the one replacement character it becomes."""
|
||||||
|
return len(data.decode("utf-8", errors="replace"))
|
||||||
|
|
||||||
|
|
||||||
|
def _long_text_check(texts: list[tuple[Path, bytes]]) -> Optional[str]:
|
||||||
|
"""The one `CHECK` line for the texts longer than `LONG_TEXT_CHARS`, else None.
|
||||||
|
A UTF-8 text has no more characters than bytes, so a short one is not decoded."""
|
||||||
|
over = [
|
||||||
|
(path, count) for path, data in texts
|
||||||
|
if len(data) > LONG_TEXT_CHARS and (count := _char_count(data)) > LONG_TEXT_CHARS
|
||||||
|
]
|
||||||
|
if not over:
|
||||||
|
return None
|
||||||
|
named = ", ".join(f"{rel_path(path)} ({count:,} characters)" for path, count in over)
|
||||||
|
return (
|
||||||
|
f"Longer than one ingest reads in one pass ({LONG_TEXT_CHARS:,} characters): {named}. "
|
||||||
|
"Read it in parts - instructions/ingest-large-tree.md, the length axis."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _plain_text_length(path: Path) -> Optional[int]:
|
||||||
|
"""The characters of a lone `.md`/`.txt` file when they are over
|
||||||
|
`LONG_TEXT_CHARS`, else None. Reads the file only when its size could put
|
||||||
|
it over - `lstat` decides every other case."""
|
||||||
|
if path.suffix.lower() not in _PLAIN_TEXT_SUFFIXES or path.is_symlink() or not path.is_file():
|
||||||
|
return None
|
||||||
|
if path.lstat().st_size <= LONG_TEXT_CHARS:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
count = _char_count(path.read_bytes())
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
return count if count > LONG_TEXT_CHARS else None
|
||||||
|
|
||||||
|
|
||||||
def _unusable_name(name: str) -> bool:
|
def _unusable_name(name: str) -> bool:
|
||||||
"""Not one plain directory name: empty, padded, hidden, a path, a control
|
"""Not one plain directory name: empty, padded, hidden, a path, a control
|
||||||
character, or the manifest's reserved name."""
|
character, or the manifest's reserved name."""
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ from chemenu.commands.raw._common import (
|
|||||||
_Refused,
|
_Refused,
|
||||||
_check_directly_in_incoming,
|
_check_directly_in_incoming,
|
||||||
_folder_collision_message,
|
_folder_collision_message,
|
||||||
|
LONG_TEXT_CHARS,
|
||||||
_incoming_dir,
|
_incoming_dir,
|
||||||
|
_long_text_check,
|
||||||
_occupied_stems,
|
_occupied_stems,
|
||||||
_refusals_fail,
|
_refusals_fail,
|
||||||
_resolve,
|
_resolve,
|
||||||
@@ -284,19 +286,36 @@ def _plan_derive_folder(folder: Path):
|
|||||||
|
|
||||||
|
|
||||||
def _echo_notes(
|
def _echo_notes(
|
||||||
derivations: list[filters.Derivation], container: Optional[filters.Container] = None
|
derivations: list[filters.Derivation],
|
||||||
|
texts: list[tuple[Path, bytes]],
|
||||||
|
container: Optional[filters.Container] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""The filters' warnings and CHECKs, then the length CHECK over `texts` -
|
||||||
|
every `.md` this run wrote, as written."""
|
||||||
warnings = [w for d in derivations for w in d.warnings]
|
warnings = [w for d in derivations for w in d.warnings]
|
||||||
checks = [c for d in derivations for c in d.checks]
|
checks = [c for d in derivations for c in d.checks]
|
||||||
if container is not None:
|
if container is not None:
|
||||||
warnings += container.warnings
|
warnings += container.warnings
|
||||||
checks += container.checks
|
checks += container.checks
|
||||||
|
long_text = _long_text_check(texts)
|
||||||
|
if long_text is not None:
|
||||||
|
checks.append(long_text)
|
||||||
for warning in filters.unique(warnings):
|
for warning in filters.unique(warnings):
|
||||||
typer.echo(f"WARN {warning}")
|
typer.echo(f"WARN {warning}")
|
||||||
for line in filters.unique(checks):
|
for line in filters.unique(checks):
|
||||||
typer.echo(f"CHECK {line}")
|
typer.echo(f"CHECK {line}")
|
||||||
|
|
||||||
|
|
||||||
|
def _written_texts(plan: _FilePlan) -> list[tuple[Path, bytes]]:
|
||||||
|
"""Every `.md` the single-file form writes: `derived.md`, and the `.md`
|
||||||
|
beside each member of a container that a filter read."""
|
||||||
|
texts = [(plan.derived, plan.markdown)] if plan.derived is not None else []
|
||||||
|
if plan.container is not None:
|
||||||
|
members = dict(plan.container.files)
|
||||||
|
texts += [(plan.folder / md, members[md]) for _src, md, _name in plan.container.derived]
|
||||||
|
return texts
|
||||||
|
|
||||||
|
|
||||||
def _container_report(container: filters.Container) -> list[str]:
|
def _container_report(container: filters.Container) -> list[str]:
|
||||||
lines = [f" {src} -> {md} ({name})" for src, md, name in container.derived]
|
lines = [f" {src} -> {md} ({name})" for src, md, name in container.derived]
|
||||||
if container.plain:
|
if container.plain:
|
||||||
@@ -413,6 +432,11 @@ def _derive_next(target: Path) -> str:
|
|||||||
"After a success the output prints the warnings the filters raised, at most three "
|
"After a success the output prints the warnings the filters raised, at most three "
|
||||||
"`CHECK` lines per format that was derived in this run - what to look at before the "
|
"`CHECK` lines per format that was derived in this run - what to look at before the "
|
||||||
"text is trusted - and the `raw accept` line for the result.",
|
"text is trusted - and the `raw accept` line for the result.",
|
||||||
|
f"Length: one more `CHECK` line names every `.md` this run wrote that is longer than "
|
||||||
|
f"{LONG_TEXT_CHARS:,} characters, counted in the file as written, header included - "
|
||||||
|
"more than one ingest reads in one pass, so it is read in parts "
|
||||||
|
"(`instructions/ingest-large-tree.md`). Characters, not tokens: they do not depend on a "
|
||||||
|
"model's tokenizer. `raw fetch` and `raw pending` measure against the same value.",
|
||||||
"Never edit `derived.md` by hand: a better derivation is a new `raw derive` of the original.",
|
"Never edit `derived.md` by hand: a better derivation is a new `raw derive` of the original.",
|
||||||
),
|
),
|
||||||
failures=(
|
failures=(
|
||||||
@@ -479,7 +503,7 @@ def raw_derive_command(
|
|||||||
warning = _collision_warning(resolved.name)
|
warning = _collision_warning(resolved.name)
|
||||||
if warning:
|
if warning:
|
||||||
typer.echo("WARN " + warning)
|
typer.echo("WARN " + warning)
|
||||||
_echo_notes([d.derivation for d in derived])
|
_echo_notes([d.derivation for d in derived], [(d.target, d.markdown) for d in derived])
|
||||||
typer.echo(_derive_next(resolved))
|
typer.echo(_derive_next(resolved))
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -512,5 +536,5 @@ def raw_derive_command(
|
|||||||
warning = _collision_warning(plan.stem)
|
warning = _collision_warning(plan.stem)
|
||||||
if warning:
|
if warning:
|
||||||
typer.echo("WARN " + warning)
|
typer.echo("WARN " + warning)
|
||||||
_echo_notes(plan.derivations, container)
|
_echo_notes(plan.derivations, _written_texts(plan), container)
|
||||||
typer.echo(_derive_next(plan.folder))
|
typer.echo(_derive_next(plan.folder))
|
||||||
@@ -34,7 +34,9 @@ from chemenu.errors import ChemenuError
|
|||||||
from chemenu.version import VersionError, read_version
|
from chemenu.version import VersionError, read_version
|
||||||
from chemenu.commands.raw._common import (
|
from chemenu.commands.raw._common import (
|
||||||
_folder_collision_message,
|
_folder_collision_message,
|
||||||
|
LONG_TEXT_CHARS,
|
||||||
_incoming_dir,
|
_incoming_dir,
|
||||||
|
_long_text_check,
|
||||||
_occupied_stems,
|
_occupied_stems,
|
||||||
_resolve,
|
_resolve,
|
||||||
_shard_dir,
|
_shard_dir,
|
||||||
@@ -154,9 +156,19 @@ def _route_entries(route: fetch_routes.Route) -> dict:
|
|||||||
return found
|
return found
|
||||||
|
|
||||||
|
|
||||||
def _echo_checks(plan: sites.Plan, derivation: web_capture.Derivation, subpages: int = 0) -> None:
|
def _echo_checks(
|
||||||
|
plan: sites.Plan,
|
||||||
|
derivation: web_capture.Derivation,
|
||||||
|
texts: list[tuple[Path, bytes]],
|
||||||
|
subpages: int = 0,
|
||||||
|
) -> None:
|
||||||
|
"""The site handler's CHECKs for the page, then the length CHECK over
|
||||||
|
`texts` - every `.md` this run wrote, as written."""
|
||||||
for line in plan.checks(derivation.decoded.text, subpages):
|
for line in plan.checks(derivation.decoded.text, subpages):
|
||||||
typer.echo(f"CHECK {line}")
|
typer.echo(f"CHECK {line}")
|
||||||
|
long_text = _long_text_check(texts)
|
||||||
|
if long_text is not None:
|
||||||
|
typer.echo(f"CHECK {long_text}")
|
||||||
|
|
||||||
|
|
||||||
def _fetched(plan: sites.Plan) -> str:
|
def _fetched(plan: sites.Plan) -> str:
|
||||||
@@ -188,7 +200,7 @@ def _fetch_html_file(html: Path, source_url: Optional[str]) -> None:
|
|||||||
)
|
)
|
||||||
_write_exclusive([(md_path, derivation.markdown)])
|
_write_exclusive([(md_path, derivation.markdown)])
|
||||||
_warn(derivation, md_path)
|
_warn(derivation, md_path)
|
||||||
_echo_checks(plan, derivation)
|
_echo_checks(plan, derivation, [(md_path, derivation.markdown)])
|
||||||
success(escape(
|
success(escape(
|
||||||
f"Derived {rel_path(md_path)} from {rel_path(path)} (no network access).\n"
|
f"Derived {rel_path(md_path)} from {rel_path(path)} (no network access).\n"
|
||||||
+ _next_lines([path, md_path], source_url)
|
+ _next_lines([path, md_path], source_url)
|
||||||
@@ -355,7 +367,7 @@ def _fetch_scope(url: str, name: Optional[str]) -> None:
|
|||||||
|
|
||||||
for page_name, derivation in derivations:
|
for page_name, derivation in derivations:
|
||||||
_warn(derivation, target / f"{page_name}.md")
|
_warn(derivation, target / f"{page_name}.md")
|
||||||
_echo_checks(plan, index_derivation)
|
_echo_checks(plan, index_derivation, [(target / f"{n}.md", d.markdown) for n, d in derivations])
|
||||||
success(escape(
|
success(escape(
|
||||||
f"Fetched {_fetched(plan)} with {len(urls)} subpage(s) to {rel_path(target)}/ "
|
f"Fetched {_fetched(plan)} with {len(urls)} subpage(s) to {rel_path(target)}/ "
|
||||||
f"({len(entries)} page pairs and {SCOPE_MANIFEST}).\n"
|
f"({len(entries)} page pairs and {SCOPE_MANIFEST}).\n"
|
||||||
@@ -470,6 +482,10 @@ def _fetch_scope(url: str, name: Optional[str]) -> None:
|
|||||||
"- the way past a paywall or a script-rendered page. Its header carries `derived` (when "
|
"- the way past a paywall or a script-rendered page. Its header carries `derived` (when "
|
||||||
"the text was derived) instead of `retrieved`, `final_url`, `http_status` and "
|
"the text was derived) instead of `retrieved`, `final_url`, `http_status` and "
|
||||||
"`content_type`, and the `.html` is left untouched.",
|
"`content_type`, and the `.html` is left untouched.",
|
||||||
|
f"Length: a `CHECK` line names every `.md` this run wrote - and a `text/plain` or "
|
||||||
|
f"`text/markdown` response stored as received - that is longer than "
|
||||||
|
f"{LONG_TEXT_CHARS:,} characters, the threshold of `raw derive`: more than one ingest "
|
||||||
|
"reads in one pass, so it is read in parts (`instructions/ingest-large-tree.md`).",
|
||||||
"Success prints the `raw accept` line for the written files and the `source_url` for the "
|
"Success prints the `raw accept` line for the written files and the `source_url` for the "
|
||||||
"source page.",
|
"source page.",
|
||||||
),
|
),
|
||||||
@@ -628,7 +644,10 @@ def raw_fetch_command(
|
|||||||
)
|
)
|
||||||
_write_exclusive([(html_path, response.body), (md_path, derivation.markdown)])
|
_write_exclusive([(html_path, response.body), (md_path, derivation.markdown)])
|
||||||
_warn(derivation, md_path)
|
_warn(derivation, md_path)
|
||||||
_echo_checks(plan, derivation, len(plan.subpages(derivation.decoded.text, response.final_url)))
|
_echo_checks(
|
||||||
|
plan, derivation, [(md_path, derivation.markdown)],
|
||||||
|
len(plan.subpages(derivation.decoded.text, response.final_url)),
|
||||||
|
)
|
||||||
anonymous = route.kind == fetch_routes.PLAIN and not route.render
|
anonymous = route.kind == fetch_routes.PLAIN and not route.render
|
||||||
success(escape(
|
success(escape(
|
||||||
f"Fetched {_fetched(plan)} to {rel_path(html_path)}, {rel_path(md_path)}\n"
|
f"Fetched {_fetched(plan)} to {rel_path(html_path)}, {rel_path(md_path)}\n"
|
||||||
@@ -640,6 +659,10 @@ def raw_fetch_command(
|
|||||||
target = incoming / f"{stem}{web_capture.extension_for(response.content_type)}"
|
target = incoming / f"{stem}{web_capture.extension_for(response.content_type)}"
|
||||||
_refuse_existing([target])
|
_refuse_existing([target])
|
||||||
_write_exclusive([(target, response.body)])
|
_write_exclusive([(target, response.body)])
|
||||||
|
if kind in web_capture.PLAIN_TEXT_EXTENSIONS:
|
||||||
|
long_text = _long_text_check([(target, response.body)])
|
||||||
|
if long_text is not None:
|
||||||
|
typer.echo(f"CHECK {long_text}")
|
||||||
success(escape(
|
success(escape(
|
||||||
f"Fetched {_fetched(plan)} to {rel_path(target)} ({kind or 'no content type'}, stored as received - "
|
f"Fetched {_fetched(plan)} to {rel_path(target)} ({kind or 'no content type'}, stored as received - "
|
||||||
f"no text derived; retrieved {web_capture.utc_now()}).\n"
|
f"no text derived; retrieved {web_capture.utc_now()}).\n"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import typer
|
|||||||
from chemenu import cli_contract, repo_capture
|
from chemenu import cli_contract, repo_capture
|
||||||
from chemenu.commands._util import rel_path
|
from chemenu.commands._util import rel_path
|
||||||
from chemenu.commands.raw._common import (
|
from chemenu.commands.raw._common import (
|
||||||
|
LONG_TEXT_CHARS,
|
||||||
_Refused,
|
_Refused,
|
||||||
_check_files,
|
_check_files,
|
||||||
_derive_hint,
|
_derive_hint,
|
||||||
@@ -29,6 +30,7 @@ from chemenu.commands.raw._common import (
|
|||||||
_plan_captured_folder,
|
_plan_captured_folder,
|
||||||
_plan_file_moves,
|
_plan_file_moves,
|
||||||
_plan_folder,
|
_plan_folder,
|
||||||
|
_plain_text_length,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -40,6 +42,7 @@ class _Candidate:
|
|||||||
mtime_ns: int
|
mtime_ns: int
|
||||||
reason: Optional[str] # None when `raw accept` would take it as it stands
|
reason: Optional[str] # None when `raw accept` would take it as it stands
|
||||||
hint: Optional[str] = None # the `raw derive` line for a lone file a filter reads
|
hint: Optional[str] = None # the `raw derive` line for a lone file a filter reads
|
||||||
|
length: Optional[int] = None # characters of a lone text file over the length threshold
|
||||||
|
|
||||||
def label(self) -> str:
|
def label(self) -> str:
|
||||||
return ", ".join(rel_path(p) + ("/" if self.kind == "folder" else "") for p in self.paths)
|
return ", ".join(rel_path(p) + ("/" if self.kind == "folder" else "") for p in self.paths)
|
||||||
@@ -111,6 +114,7 @@ def _pending_candidates() -> list[_Candidate]:
|
|||||||
max(p.lstat().st_mtime_ns for p in paths),
|
max(p.lstat().st_mtime_ns for p in paths),
|
||||||
_refusal(_plain_files, paths),
|
_refusal(_plain_files, paths),
|
||||||
_derive_hint(paths[0]) if len(paths) == 1 else None,
|
_derive_hint(paths[0]) if len(paths) == 1 else None,
|
||||||
|
_plain_text_length(paths[0]) if len(paths) == 1 else None,
|
||||||
))
|
))
|
||||||
candidates.sort(key=lambda c: (c.mtime_ns, os.fsencode(c.paths[0].name)))
|
candidates.sort(key=lambda c: (c.mtime_ns, os.fsencode(c.paths[0].name)))
|
||||||
return candidates
|
return candidates
|
||||||
@@ -142,13 +146,19 @@ def _pending_candidates() -> list[_Candidate]:
|
|||||||
"human.",
|
"human.",
|
||||||
"The default is the first candidate `raw accept` would take, and the output names it.",
|
"The default is the first candidate `raw accept` would take, and the output names it.",
|
||||||
"`--json` prints the same candidates in the same order: `kind`, `paths`, `files`, "
|
"`--json` prints the same candidates in the same order: `kind`, `paths`, `files`, "
|
||||||
"`mtime`, `acceptable`, `reason`, `default`, `hint`.",
|
"`mtime`, `acceptable`, `reason`, `default`, `hint`, `length`.",
|
||||||
"A lone file that a `raw derive` filter reads (a PDF, mail, Office file or web page) is "
|
"A lone file that a `raw derive` filter reads (a PDF, mail, Office file or web page) is "
|
||||||
"listed with the `raw derive` line for it, in text output and as `hint` in JSON "
|
"listed with the `raw derive` line for it, in text output and as `hint` in JSON "
|
||||||
"(`null` for every other candidate). The hint changes nothing about the order or the "
|
"(`null` for every other candidate). The hint changes nothing about the order or the "
|
||||||
"default.",
|
"default.",
|
||||||
"Reads directory listings and `lstat` only, never a file's content; an empty "
|
"A lone `.md` or `.txt` file longer than the length threshold of `raw derive` and `raw "
|
||||||
"`incoming/` is exit 0 with nothing to do.",
|
"fetch` is listed with its length in characters, in text output and as `length` in "
|
||||||
|
"JSON (`null` for every other candidate) - that text reaches a session with no "
|
||||||
|
"derivation in front of it to print the length `CHECK`. Like the hint, it changes "
|
||||||
|
"nothing about the order or the default.",
|
||||||
|
"Reads directory listings and `lstat`, and a file's content only to measure a lone "
|
||||||
|
"`.md`/`.txt` whose size in bytes is over the threshold - a UTF-8 text has no more "
|
||||||
|
"characters than bytes; an empty `incoming/` is exit 0 with nothing to do.",
|
||||||
),
|
),
|
||||||
failures=(),
|
failures=(),
|
||||||
examples=(
|
examples=(
|
||||||
@@ -180,6 +190,7 @@ def raw_pending_command(
|
|||||||
"reason": c.reason,
|
"reason": c.reason,
|
||||||
"default": c is default,
|
"default": c is default,
|
||||||
"hint": c.hint,
|
"hint": c.hint,
|
||||||
|
"length": c.length,
|
||||||
}
|
}
|
||||||
for c in candidates
|
for c in candidates
|
||||||
], indent=2, ensure_ascii=False))
|
], indent=2, ensure_ascii=False))
|
||||||
@@ -199,6 +210,11 @@ def raw_pending_command(
|
|||||||
typer.echo(f" {line}")
|
typer.echo(f" {line}")
|
||||||
if c.hint is not None:
|
if c.hint is not None:
|
||||||
typer.echo(f" text: {c.hint} (derives readable text, keeps the original)")
|
typer.echo(f" text: {c.hint} (derives readable text, keeps the original)")
|
||||||
|
if c.length is not None:
|
||||||
|
typer.echo(
|
||||||
|
f" long: {c.length:,} characters, over the {LONG_TEXT_CHARS:,} one ingest "
|
||||||
|
"reads in one pass - read it in parts (instructions/ingest-large-tree.md)"
|
||||||
|
)
|
||||||
if default is None:
|
if default is None:
|
||||||
typer.echo("No candidate can be accepted as it stands - each needs a human (reasons above).")
|
typer.echo("No candidate can be accepted as it stands - each needs a human (reasons above).")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import typer
|
|||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from chemenu import filters, web_capture
|
from chemenu import filters, web_capture
|
||||||
|
from chemenu.commands.raw._common import LONG_TEXT_CHARS
|
||||||
from chemenu.commands.raw.accept import raw_accept_command
|
from chemenu.commands.raw.accept import raw_accept_command
|
||||||
from chemenu.commands.raw.derive import raw_derive_command
|
from chemenu.commands.raw.derive import raw_derive_command
|
||||||
from chemenu.commands.raw.pending import raw_pending_command
|
from chemenu.commands.raw.pending import raw_pending_command
|
||||||
@@ -883,6 +884,71 @@ def test_header_renders_a_list_as_a_flow_sequence_and_a_string_as_before():
|
|||||||
assert yaml.safe_load(rendered.strip("-\n").replace("---", "")) == {"a": "plain", "b": ["x y", "z"], "c": []}
|
assert yaml.safe_load(rendered.strip("-\n").replace("---", "")) == {"a": "plain", "b": ["x y", "z"], "c": []}
|
||||||
|
|
||||||
|
|
||||||
|
# --- the length axis (Gitea #226) -------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _long_checks(out: str) -> list[str]:
|
||||||
|
return [line for line in out.splitlines() if line.startswith("CHECK Longer than one ingest")]
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_length_check_fires_one_character_past_the_constant_and_not_at_it(tree, capsys):
|
||||||
|
# one paragraph, the same file-name length: every header is as long as the probe's
|
||||||
|
_put(tree, "a.docx", make_docx(_p("x")))
|
||||||
|
_derive("incoming/a.docx")
|
||||||
|
overhead = len((tree / "incoming/a/derived.md").read_text(encoding="utf-8")) - 1
|
||||||
|
capsys.readouterr()
|
||||||
|
|
||||||
|
_put(tree, "b.docx", make_docx(_p("x" * (LONG_TEXT_CHARS - overhead))))
|
||||||
|
_derive("incoming/b.docx")
|
||||||
|
assert len((tree / "incoming/b/derived.md").read_text(encoding="utf-8")) == LONG_TEXT_CHARS
|
||||||
|
assert _long_checks(capsys.readouterr().out) == []
|
||||||
|
|
||||||
|
_put(tree, "c.docx", make_docx(_p("x" * (LONG_TEXT_CHARS - overhead + 1))))
|
||||||
|
_derive("incoming/c.docx")
|
||||||
|
[line] = _long_checks(capsys.readouterr().out)
|
||||||
|
assert f"incoming/c/derived.md ({LONG_TEXT_CHARS + 1:,} characters)" in line
|
||||||
|
assert "instructions/ingest-large-tree.md" in line
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_length_is_counted_in_characters_not_in_bytes(tree, capsys):
|
||||||
|
_put(tree, "umlaute.docx", make_docx(_p("ä" * (LONG_TEXT_CHARS // 2 + 1000))))
|
||||||
|
_derive("incoming/umlaute.docx")
|
||||||
|
assert (tree / "incoming/umlaute/derived.md").stat().st_size > LONG_TEXT_CHARS
|
||||||
|
assert _long_checks(capsys.readouterr().out) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_length_check_names_each_long_md_of_a_folder_and_of_a_zip(tree, capsys):
|
||||||
|
folder = tree / "incoming/mappe"
|
||||||
|
folder.mkdir()
|
||||||
|
(folder / "lang.docx").write_bytes(make_docx(_p("x" * (LONG_TEXT_CHARS + 1))))
|
||||||
|
(folder / "kurz.docx").write_bytes(make_docx(_p("x")))
|
||||||
|
_derive("incoming/mappe")
|
||||||
|
[line] = _long_checks(capsys.readouterr().out)
|
||||||
|
assert "incoming/mappe/lang.md (" in line and "kurz.md" not in line
|
||||||
|
|
||||||
|
_put(tree, "paket.zip", _zip({"lang.docx": make_docx(_p("x" * (LONG_TEXT_CHARS + 1)))}))
|
||||||
|
_derive("incoming/paket.zip")
|
||||||
|
[line] = _long_checks(capsys.readouterr().out)
|
||||||
|
assert "incoming/paket/lang.md (" in line
|
||||||
|
|
||||||
|
|
||||||
|
def test_pending_names_the_length_of_a_long_lone_text_file_and_of_nothing_else(tree, capsys):
|
||||||
|
_put(tree, "lang.md", b"x" * (LONG_TEXT_CHARS + 1))
|
||||||
|
_put(tree, "grenze.txt", b"x" * LONG_TEXT_CHARS)
|
||||||
|
_put(tree, "umlaute.md", ("ä" * (LONG_TEXT_CHARS // 2 + 1000)).encode("utf-8"))
|
||||||
|
_put(tree, "page.html", b"x" * (LONG_TEXT_CHARS + 1))
|
||||||
|
_put(tree, "page.md", b"x" * (LONG_TEXT_CHARS + 1))
|
||||||
|
raw_pending_command(json_out=True)
|
||||||
|
by_path = {c["paths"][0]: c for c in json.loads(capsys.readouterr().out)}
|
||||||
|
assert by_path["incoming/lang.md"]["length"] == LONG_TEXT_CHARS + 1
|
||||||
|
assert by_path["incoming/grenze.txt"]["length"] is None
|
||||||
|
assert by_path["incoming/umlaute.md"]["length"] is None
|
||||||
|
assert by_path["incoming/page.html"]["length"] is None # a fetch pair: raw fetch printed it
|
||||||
|
|
||||||
|
raw_pending_command(json_out=False)
|
||||||
|
assert f"long: {LONG_TEXT_CHARS + 1:,} characters" in capsys.readouterr().out
|
||||||
|
|
||||||
|
|
||||||
# --- the nets in raw pending and raw accept ---------------------------------
|
# --- the nets in raw pending and raw accept ---------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import typer
|
|||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from chemenu import web_capture
|
from chemenu import web_capture
|
||||||
|
from chemenu.commands.raw._common import LONG_TEXT_CHARS
|
||||||
from chemenu.commands.raw.accept import raw_accept_command
|
from chemenu.commands.raw.accept import raw_accept_command
|
||||||
from chemenu.commands.raw.fetch import raw_fetch_command
|
from chemenu.commands.raw.fetch import raw_fetch_command
|
||||||
|
|
||||||
@@ -330,6 +331,27 @@ def test_plain_text_is_stored_as_txt_unchanged(tree, site):
|
|||||||
assert (tree / "incoming/notes.txt").read_bytes() == b"line one\r\nline two\n"
|
assert (tree / "incoming/notes.txt").read_bytes() == b"line one\r\nline two\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _long_checks(out: str) -> list[str]:
|
||||||
|
return [line for line in out.splitlines() if line.startswith("CHECK Longer than one ingest")]
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_derived_page_over_the_length_threshold_gets_the_length_check(tree, site, capsys):
|
||||||
|
_fetch(site.html("/short", ARTICLE))
|
||||||
|
assert _long_checks(capsys.readouterr().out) == []
|
||||||
|
|
||||||
|
long_page = b"<html><body><main><p>" + b"x" * (LONG_TEXT_CHARS + 1) + b"</p></main></body></html>"
|
||||||
|
_fetch(site.html("/long", long_page))
|
||||||
|
[line] = _long_checks(capsys.readouterr().out)
|
||||||
|
assert "incoming/long.md (" in line and "instructions/ingest-large-tree.md" in line
|
||||||
|
|
||||||
|
|
||||||
|
def test_plain_text_over_the_length_threshold_gets_the_length_check(tree, site, capsys):
|
||||||
|
site.routes["/notes"] = (200, {"Content-Type": "text/plain; charset=utf-8"}, b"x" * (LONG_TEXT_CHARS + 1))
|
||||||
|
_fetch(site.url("/notes"))
|
||||||
|
[line] = _long_checks(capsys.readouterr().out)
|
||||||
|
assert f"incoming/notes.txt ({LONG_TEXT_CHARS + 1:,} characters)" in line
|
||||||
|
|
||||||
|
|
||||||
# --- --html: a page saved from a browser ------------------------------------
|
# --- --html: a page saved from a browser ------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -45,10 +45,11 @@ some source page - never whether anyone decided about its contents. Left unwritt
|
|||||||
source is renegotiated on every later pass.
|
source is renegotiated on every later pass.
|
||||||
|
|
||||||
- Record every deliberate omission with a **reason**, not just a filename.
|
- Record every deliberate omission with a **reason**, not just a filename.
|
||||||
- Mandatory where the ingest ran through `instructions/ingest-large-tree.md` - on both axes: for
|
- Mandatory where the ingest ran through `instructions/ingest-large-tree.md` - on every axis:
|
||||||
a tree ingest the section records what was not taken from the tree; for a thematically broad
|
for a tree ingest the section records what was not taken from the tree; for a thematically
|
||||||
single source, which named subjects got no page of their own, and why. Optional for a single
|
broad single source, which named subjects got no page of their own, and why; for a long
|
||||||
small file - but an empty section still beats a missing one.
|
source, which of its parts or kinds of content no unit's fact list took over, and why. Optional
|
||||||
|
for a single small file - but an empty section still beats a missing one.
|
||||||
- Belongs on the source page, not in `kb/log.md`: it is a statement about *this* source, and the
|
- Belongs on the source page, not in `kb/log.md`: it is a statement about *this* source, and the
|
||||||
log is chronological rather than per-source.
|
log is chronological rather than per-source.
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user