yuno-proofreader 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +435 -0
- data/exe/yuno-proofreader +16 -0
- data/lib/yuno/proofreader/analysis/adjustments.rb +45 -0
- data/lib/yuno/proofreader/analysis/crawler.rb +95 -0
- data/lib/yuno/proofreader/analysis/entries.rb +73 -0
- data/lib/yuno/proofreader/analysis/extractor.rb +209 -0
- data/lib/yuno/proofreader/analysis/graph.rb +115 -0
- data/lib/yuno/proofreader/analysis/judge.rb +103 -0
- data/lib/yuno/proofreader/analysis/judge.yml +27 -0
- data/lib/yuno/proofreader/analysis/judge_prompt.rb +72 -0
- data/lib/yuno/proofreader/analysis/judgments.rb +70 -0
- data/lib/yuno/proofreader/analysis/links.rb +54 -0
- data/lib/yuno/proofreader/analysis/list_order.rb +33 -0
- data/lib/yuno/proofreader/analysis/reasons.rb +49 -0
- data/lib/yuno/proofreader/analysis/report.rb +110 -0
- data/lib/yuno/proofreader/analysis/runner.rb +171 -0
- data/lib/yuno/proofreader/analysis/scorer.rb +128 -0
- data/lib/yuno/proofreader/analysis/settings.rb +90 -0
- data/lib/yuno/proofreader/analysis/signals.rb +102 -0
- data/lib/yuno/proofreader/analysis/structured_data.rb +57 -0
- data/lib/yuno/proofreader/analysis/url_tree.rb +49 -0
- data/lib/yuno/proofreader/analysis/weights.rb +77 -0
- data/lib/yuno/proofreader/analysis/weights.yml +39 -0
- data/lib/yuno/proofreader/audit/agenda.rb +86 -0
- data/lib/yuno/proofreader/audit/agenda_report.rb +65 -0
- data/lib/yuno/proofreader/audit/answers.rb +74 -0
- data/lib/yuno/proofreader/audit/attempt.rb +69 -0
- data/lib/yuno/proofreader/audit/batch.rb +64 -0
- data/lib/yuno/proofreader/audit/batch_report.rb +87 -0
- data/lib/yuno/proofreader/audit/cadence.rb +67 -0
- data/lib/yuno/proofreader/audit/instructions.rb +30 -0
- data/lib/yuno/proofreader/audit/log_fields.rb +28 -0
- data/lib/yuno/proofreader/audit/prompt.rb +157 -0
- data/lib/yuno/proofreader/audit/prompts/instructions.yml +46 -0
- data/lib/yuno/proofreader/audit/prompts/steps.yml +67 -0
- data/lib/yuno/proofreader/audit/report.rb +153 -0
- data/lib/yuno/proofreader/audit/runner.rb +144 -0
- data/lib/yuno/proofreader/audit/steps.rb +56 -0
- data/lib/yuno/proofreader/cli/analysis_commands.rb +43 -0
- data/lib/yuno/proofreader/cli/audit_commands.rb +92 -0
- data/lib/yuno/proofreader/cli/command_log.rb +42 -0
- data/lib/yuno/proofreader/cli/flags.rb +48 -0
- data/lib/yuno/proofreader/cli/init_command.rb +116 -0
- data/lib/yuno/proofreader/cli/model_commands.rb +34 -0
- data/lib/yuno/proofreader/cli/queue_commands.rb +89 -0
- data/lib/yuno/proofreader/cli/sheet_commands.rb +57 -0
- data/lib/yuno/proofreader/cli.rb +218 -0
- data/lib/yuno/proofreader/config.rb +25 -0
- data/lib/yuno/proofreader/context.rb +55 -0
- data/lib/yuno/proofreader/env_file.rb +76 -0
- data/lib/yuno/proofreader/errors.rb +17 -0
- data/lib/yuno/proofreader/findings/finding.rb +95 -0
- data/lib/yuno/proofreader/findings/matching.rb +34 -0
- data/lib/yuno/proofreader/findings/merger.rb +43 -0
- data/lib/yuno/proofreader/findings/outline.rb +73 -0
- data/lib/yuno/proofreader/findings/plan.rb +24 -0
- data/lib/yuno/proofreader/findings/reconciler.rb +219 -0
- data/lib/yuno/proofreader/findings/source.rb +79 -0
- data/lib/yuno/proofreader/findings/text.rb +37 -0
- data/lib/yuno/proofreader/http/client.rb +131 -0
- data/lib/yuno/proofreader/http/net_http_transport.rb +55 -0
- data/lib/yuno/proofreader/http/robots.rb +72 -0
- data/lib/yuno/proofreader/init/ask.rb +67 -0
- data/lib/yuno/proofreader/init/detector.rb +100 -0
- data/lib/yuno/proofreader/init/interview.rb +210 -0
- data/lib/yuno/proofreader/init/parsing.rb +47 -0
- data/lib/yuno/proofreader/init/web.rb +33 -0
- data/lib/yuno/proofreader/llm/client.rb +140 -0
- data/lib/yuno/proofreader/llm/defaults.rb +22 -0
- data/lib/yuno/proofreader/llm/defaults.yml +6 -0
- data/lib/yuno/proofreader/llm/errors.rb +26 -0
- data/lib/yuno/proofreader/llm/factory.rb +45 -0
- data/lib/yuno/proofreader/llm/http_transport.rb +41 -0
- data/lib/yuno/proofreader/llm/json_text.rb +23 -0
- data/lib/yuno/proofreader/llm/model_commands.rb +34 -0
- data/lib/yuno/proofreader/llm/provider.rb +58 -0
- data/lib/yuno/proofreader/llm/providers/gemini.rb +146 -0
- data/lib/yuno/proofreader/llm/providers.rb +38 -0
- data/lib/yuno/proofreader/llm/types.rb +13 -0
- data/lib/yuno/proofreader/llm/usage.rb +17 -0
- data/lib/yuno/proofreader/log.rb +129 -0
- data/lib/yuno/proofreader/pages/fetcher.rb +75 -0
- data/lib/yuno/proofreader/pages/report.rb +45 -0
- data/lib/yuno/proofreader/pages/snapshot.rb +48 -0
- data/lib/yuno/proofreader/pages/trimmer.rb +36 -0
- data/lib/yuno/proofreader/settings_file.rb +51 -0
- data/lib/yuno/proofreader/site.rb +74 -0
- data/lib/yuno/proofreader/site_reader.rb +131 -0
- data/lib/yuno/proofreader/sitemap/aligner.rb +78 -0
- data/lib/yuno/proofreader/sitemap/importer.rb +31 -0
- data/lib/yuno/proofreader/sitemap/loader.rb +127 -0
- data/lib/yuno/proofreader/sitemap/mismatch.rb +76 -0
- data/lib/yuno/proofreader/sitemap/pairer.rb +223 -0
- data/lib/yuno/proofreader/sitemap/parser.rb +73 -0
- data/lib/yuno/proofreader/sitemap/report.rb +80 -0
- data/lib/yuno/proofreader/store/cells.rb +67 -0
- data/lib/yuno/proofreader/store/google_gateway.rb +198 -0
- data/lib/yuno/proofreader/store/memory.rb +90 -0
- data/lib/yuno/proofreader/store/records.rb +28 -0
- data/lib/yuno/proofreader/store/schema.rb +109 -0
- data/lib/yuno/proofreader/store/sheets.rb +183 -0
- data/lib/yuno/proofreader/store/tab_setup.rb +74 -0
- data/lib/yuno/proofreader/version.rb +7 -0
- data/lib/yuno/proofreader.rb +101 -0
- data/proofreader.example.yml +134 -0
- metadata +194 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2b9c1ba0d2da16cc90dd1280f5c90ed7174c16402def0aa09707495d0bccd0a2
|
|
4
|
+
data.tar.gz: 3c174ff4a4cf13f8864a1b10ff09374d488b4b14dd818569b1926a091c77b56d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: cfa0b61cd10747628ce5dce680d3c2b1b01daaaeeb706ec936b80c71e576b7a581c5c43e0c86ed3254a8163b2efa9654806ff6877eca7e23b42b9b2bfdd8dd8c
|
|
7
|
+
data.tar.gz: e99babee2e1dae3e11f808048647fb6bd6b972e16ffe7f922c6f8bcefc5d1054093bb047f87fa8f834bdb21af00dc934792934aca9e1aa040be1f8ddafd48ad6
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0]
|
|
4
|
+
2026/10/26
|
|
5
|
+
|
|
6
|
+
First release. It is early, and details may still change before 1.0.
|
|
7
|
+
|
|
8
|
+
- Reads your sitemap and language versions, and helps you set up your settings file.
|
|
9
|
+
- Ranks your pages by importance and works out which are due for another audit, so the most important ones are checked most often.
|
|
10
|
+
- Audits the pages of a website with an AI model (Gemini) and an independent second review, and lists the suggested fixes in a Google Sheet for manual review.
|
|
11
|
+
- Checks that every suggestion quotes text that is really on the page, and closes suggestions whose text is gone from the live page.
|
|
12
|
+
|
|
13
|
+
[0.1.0]: https://gitlab.com/alsen-ca/yuno-proofreader/-/tags/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sebastian Alsen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
# yuno-proofreader
|
|
2
|
+
|
|
3
|
+
AI-powered website proofreader that collects suggestions in Google Sheets.
|
|
4
|
+
|
|
5
|
+
It reads your public web pages, asks an AI model (only Gemini so far) to look for typos, grammar problems, HTML structure issues, SEO basics and differences between language versions, and lists the suggested fixes in a Google Sheet for you to review.
|
|
6
|
+
Only Makes suggestions and doesn't do any actual changes to the website.
|
|
7
|
+
|
|
8
|
+
## How it works
|
|
9
|
+
|
|
10
|
+
1. Reads your sitemap and pairs each page with its language versions.
|
|
11
|
+
2. Audits one page per run in two independent passes: a step-by-step audit, then a second review by a different model that has not seen the first result. A page that has not changed since its last complete audit is skipped without calling a model.
|
|
12
|
+
3. Merges both answers, checks that every quoted text really appears on the page, and writes new suggestions to the sheet. Suggestions it already knows are not added again.
|
|
13
|
+
4. On later runs, closes suggestions whose text is no longer on the live page.
|
|
14
|
+
|
|
15
|
+
You review the sheet and mark each suggestion as `open`, `on staging`, `done` or `rejected`.
|
|
16
|
+
|
|
17
|
+
## What you need
|
|
18
|
+
|
|
19
|
+
- Ruby 4.0 or newer, or a container runtime such as Podman or Docker
|
|
20
|
+
- An API key for a supported AI model provider (Gemini so far)
|
|
21
|
+
- A Google Cloud service account with the Sheets API enabled, and a spreadsheet shared with that account
|
|
22
|
+
- A sitemap on the site you want to check
|
|
23
|
+
|
|
24
|
+
## Getting started
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
gem install yuno-proofreader
|
|
28
|
+
yuno-proofreader init # asks about your site and writes proofreader.yml
|
|
29
|
+
yuno-proofreader settings # what is still needed for an audit
|
|
30
|
+
yuno-proofreader sitemap # check that your pages are found and paired by language
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The commands that only read your site (`init`, `settings`, `sitemap`, `fetch` and `prompt`) need nothing more. The ones that use your sheet and your model need access, which lives outside `proofreader.yml`. You can put it in a `.env` file in the folder where you run the tool (see "Configuration"):
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
SPREADSHEET_ID=...
|
|
37
|
+
GEMINI_API_KEY=...
|
|
38
|
+
GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`SPREADSHEET_ID` is the part of your sheet's address between `/d/` and `/edit`. `GEMINI_API_KEY` is the API key of your model provider. `GOOGLE_APPLICATION_CREDENTIALS` points to the credentials file of a Google account that may edit the sheet, so share the sheet with that account as an editor. On Google Cloud it should not be needed, because the service account the job runs as is used instead. See "Connecting to Google". `yuno-proofreader init` lists which of these are set, `yuno-proofreader models` tests your model key, and `prepare` is the first command that tests the access to the sheet.
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
yuno-proofreader prepare # creates the Pages, Suggestions and Analysis tabs in your sheet
|
|
45
|
+
yuno-proofreader pages import # adds your pages to the sheet
|
|
46
|
+
yuno-proofreader analyze # ranks the pages by importance (optional, but it orders the audits)
|
|
47
|
+
yuno-proofreader queue # shows which pages are due, and why
|
|
48
|
+
yuno-proofreader audit --next # audits the page that is due first
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Start with `audit --next --dry-run` and a single page, and read what it would do: every audit makes two model calls, so cost depends on your pages and your models.
|
|
52
|
+
Review the suggestions in the sheet, set each one's `status`, and call `audit --next` again whenever you like: it picks the next page that is due. A cron job, a Kubernetes CronJob or Cloud Scheduler can do that for you.
|
|
53
|
+
|
|
54
|
+
## Privacy and responsible use
|
|
55
|
+
|
|
56
|
+
- The content of your pages, and the text of the settings that describe your site, is sent to the model provider you configure. Providers' terms sometimes differ between free and paid use, including whether prompts may be used to improve their products, so read the current terms before you start.
|
|
57
|
+
- Only check sites you own or have permission to fetch.
|
|
58
|
+
- You pay for the model calls yourself. Cost depends on the model and on the size of your pages.
|
|
59
|
+
- `yuno-proofreader prompt PAGE` shows exactly what is sent to the model for a page, so you can check it before any request is made.
|
|
60
|
+
- `yuno-proofreader audit` sends the pages and the settings that describe your site to the model provider, and writes to your sheet. Try it on one small page first, and read `yuno-proofreader prompt PAGE` to see what is sent.
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
yuno-proofreader <command>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Command | What it does |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `help` | Shows the commands |
|
|
71
|
+
| `version` | Prints the version |
|
|
72
|
+
| `doctor` | Checks that `SPREADSHEET_ID` is set and prints the configuration |
|
|
73
|
+
| `sitemap [FILE] [--all]` | Reads the sitemap of the site described in `FILE` (default `./proofreader.yml`) and shows how its pages were paired by language. `--all` lists every page. Needs no API keys. |
|
|
74
|
+
| `fetch PAGE [FILE] [--html=LANG]` | Downloads all language versions of one page and shows status, size and a content fingerprint of each. `PAGE` is a name from `sitemap --all`, or any address of the page. `--html=LANG` also prints the cleaned HTML that would be sent to the model. |
|
|
75
|
+
| `settings [FILE]` | Shows which of the settings that describe your site are filled in, and whether an audit can start. Exits with 1 when it cannot. |
|
|
76
|
+
| `models [FILE]` | Lists the models your API key can use for text generation, marks the ones in use, and shows whether the list comes from your settings or the built-in default. Needs the API key of your provider. |
|
|
77
|
+
| `prompt PAGE [FILE] [--review] [--full]` | Shows exactly what would be sent to the model for one page, with a size estimate, without calling it. Needs no API key. Refuses to run while the settings that describe your site are unfinished. |
|
|
78
|
+
| `audit PAGE [FILE] [--dry-run] [--force] [--verbose]` | Audits one page in all its language versions and updates the sheet. `--dry-run` writes nothing, but the model is still called and costs the same. `--force` audits a page that has not changed. `--verbose` lists everything that was decided. Needs a model API key, the sheet and the finished settings. |
|
|
79
|
+
| `prepare [FILE]` | Creates the `Pages` and `Suggestions` tabs in your Google Sheet. Safe to run again. |
|
|
80
|
+
| `pages import [FILE]` | Adds the pages of the sitemap to the `Pages` tab. Safe to run again. |
|
|
81
|
+
| `analyze [FILE] [--dry-run] [--no-ai] [--reuse-ai]` | Ranks every page by importance and writes `importance`, `tier` and `importance_notes` to the Pages tab, and the measurements to the Analysis tab. `--dry-run` writes nothing, `--no-ai` skips the AI models, `--reuse-ai` takes their judgement from the last run instead of asking again. |
|
|
82
|
+
| `init [FILE] [OPTIONS]` | Asks about your site and writes a settings file (default `proofreader.yml`). With `--template` it shows the full commented template instead, or writes it to `FILE` when one is named. For scripts: `--website`, `--language CODE[:PREFIX]`, `--note CODE=TEXT`, `--business`, `--audience`, `--tone`, `--sitemap`, `--skip-path`, `--rule`, `--ignore-term` and `--difference`. `--yes` accepts what was detected, `--offline` makes no request. Does not overwrite a file. |
|
|
83
|
+
|
|
84
|
+
Exit codes: `0` success, `1` an error such as missing configuration, `2` unknown command.
|
|
85
|
+
|
|
86
|
+
How to run it:
|
|
87
|
+
|
|
88
|
+
- As an installed gem: `gem install yuno-proofreader`, then `yuno-proofreader help`
|
|
89
|
+
- In a checkout or the dev container: `bundle exec exe/yuno-proofreader help`
|
|
90
|
+
|
|
91
|
+
## Configuration
|
|
92
|
+
|
|
93
|
+
Secrets and ids come from environment variables. The settings of the site you check come from a YAML file.
|
|
94
|
+
|
|
95
|
+
| Variable | Required | Meaning |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `GEMINI_API_KEY` | for the Gemini models | The API key of your model provider. Each provider names its own variable, and only the one you use is needed. |
|
|
98
|
+
| `SPREADSHEET_ID` | for the sheet commands and `doctor` | The id of the Google Sheet that receives the suggestions |
|
|
99
|
+
| `GOOGLE_APPLICATION_CREDENTIALS` | locally | Path to a Google credentials file. Where the tool runs on Google Cloud, the attached service account is used instead. |
|
|
100
|
+
| `LOG_LEVEL` | no | `debug`, `info`, `warn` (default) or `error`. At `info`, every run, batch and audited page gets a line. At `debug`, each model call gets a line as well, with its model, seconds and tokens. At `warn`, only problems. At `error`, only failures. |
|
|
101
|
+
| `LOG_FORMAT` | no | `text` (default, `key=value` pairs) or `json` (one JSON object per line, which Google Cloud Logging reads as a structured entry) |
|
|
102
|
+
|
|
103
|
+
**Where the variables come from.** Set them the way you set any environment variable: in your shell, in the configuration of your CI or job, or in the secret settings of your platform (on Google Cloud, Secret Manager). On your own machine you can also put them in a `.env` file in the folder where you run the tool, which it reads when it starts:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
SPREADSHEET_ID=...
|
|
107
|
+
GEMINI_API_KEY=...
|
|
108
|
+
GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A variable that is already set always wins over the file, and `PROOFREADER_ENV_FILE=/path/to/file` names another file.
|
|
112
|
+
|
|
113
|
+
For the site, run `yuno-proofreader init` (see "Setting up your site"). The commands read `proofreader.yml` from the current folder, or from the path you give as `FILE`. `yuno-proofreader init --template` shows every setting with an explanation. Keep your `proofreader.yml` out of version control if it holds anything you don't want to share.
|
|
114
|
+
|
|
115
|
+
## Setting up your site
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
yuno-proofreader init
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Asks about your site and writes `proofreader.yml` in the current folder, or the file you name. It does not overwrites a file that already exists. What it does:
|
|
122
|
+
|
|
123
|
+
1. Asks for the address of the site, and reads the homepage once, honoring. If the address leads to another host, such as the `www` form, it offers to use that one.
|
|
124
|
+
2. Proposes the languages from the `hreflang` tags of the homepage, with the prefix of each and the default first. If the page announces none, or you decline, you type them: `de, es:/es, en:/en`, the default first.
|
|
125
|
+
3. Asks for a note per language (variant and form of address), what the site offers (suggesting the meta description of the homepage), the audience and the tone, and then four lists with one entry per line: pages to skip, writing rules, words that must never be flagged, and intended differences between the languages. An empty answer skips what is optional.
|
|
126
|
+
4. Looks for your sitemap. If none is found, asks for its address.
|
|
127
|
+
5. Writes a short file with your answers. Then it says what an audit still needs, which of `SPREADSHEET_ID` and the API key of your provider are set, and what to run next.
|
|
128
|
+
|
|
129
|
+
### Without questions
|
|
130
|
+
|
|
131
|
+
Without a terminal, or with `--yes`, `init` asks nothing and takes everything from options:
|
|
132
|
+
|
|
133
|
+
| Option | What it sets |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `--website ADDRESS` | The address of the site. Needed when nobody can be asked. |
|
|
136
|
+
| `--language CODE[:PREFIX]` | A language of the site, once for each, the default first: `--language de --language es:/es`. When left out, the languages the homepage announces are used. |
|
|
137
|
+
| `--note CODE=TEXT` | A note for one language: its variant and form of address |
|
|
138
|
+
| `--business TEXT` | What the site is and offers. An audit needs it. |
|
|
139
|
+
| `--audience TEXT`, `--tone TEXT` | Who reads the site, and how the text should sound |
|
|
140
|
+
| `--sitemap ADDRESS` | Your sitemap, when it is not found on its own |
|
|
141
|
+
| `--skip-path PATH`, `--rule TEXT`, `--ignore-term TEXT`, `--difference TEXT` | One entry of a list each. Repeat the option for more entries. |
|
|
142
|
+
| `--yes` | Accepts what was detected and asks nothing, also on a terminal |
|
|
143
|
+
| `--offline` | Makes no request, so nothing is detected: give `--website` and `--language` |
|
|
144
|
+
| `--template` | Shows the full commented template instead, with every setting explained. With a file name (`--template NAME.yml`) it writes the template there |
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
yuno-proofreader init --website example.com --language de --language en:/en --business "A small shop." --yes
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
This reads the homepage and the sitemap of the site, which is how the languages and the sitemap are checked. Add `--offline` to make no request at all.
|
|
151
|
+
|
|
152
|
+
Good to know:
|
|
153
|
+
|
|
154
|
+
- Languages are told apart by the start of the address (`/en/...`). A version on another domain, or deeper than one path segment, can't be described, and `init` says which versions it left out.
|
|
155
|
+
- Language codes are written in lowercase (`de-DE` becomes `de-de`). Run `yuno-proofreader sitemap` afterwards to check that your pages are paired.
|
|
156
|
+
- With `--yes`, the meta description of your homepage becomes the description of the business, unless you give `--business`. It is marketing copy, so read it. If the page has no description, or one longer than 300 characters, the business stays empty, and `yuno-proofreader settings` says that an audit still needs it.
|
|
157
|
+
- The file holds only your answers. Settings you were not asked about, such as the models, `analysis` and `cadence`, are not in it and use their defaults. `yuno-proofreader init --template` shows every setting.
|
|
158
|
+
|
|
159
|
+
## Describing your site
|
|
160
|
+
|
|
161
|
+
Besides the technical settings, `proofreader.yml` tells the tool what your site is about, so the model can judge text in context. Everything here is sent to the AI model together with each page, so keep it short and free of secrets. Every setting is explained in `proofreader.example.yml`.
|
|
162
|
+
|
|
163
|
+
| Setting | Required | Purpose |
|
|
164
|
+
|---|---|---|
|
|
165
|
+
| `business` | for an audit | What the site is and offers, in a sentence or two |
|
|
166
|
+
| `audience` | no | Who reads the site |
|
|
167
|
+
| `tone` | no | How the text should sound overall, such as warm and professional |
|
|
168
|
+
| `rules` | no | Writing rules that apply to every language, one per entry, such as how prices or dates are written |
|
|
169
|
+
| `ignore_terms` | no | Words that are correct as written and must not be flagged, such as brand and place names |
|
|
170
|
+
| `intentional_differences` | no | Deliberate differences between language versions, such as a section that exists in one language only |
|
|
171
|
+
| `notes` (inside each language) | no | What is specific to that language: its variant (Latin American Spanish) and its form of address (Sie or du, usted or tú) |
|
|
172
|
+
|
|
173
|
+
The example file marks each of these with `[FILL IN]`. The audit refuses to start while any value still contains that text: replace it with your own, or delete the setting. `yuno-proofreader settings` shows what is set and what is still missing.
|
|
174
|
+
|
|
175
|
+
Limits: 500 characters per text, 200 per list entry or language note, and 50 entries per list.
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
## Choosing the models
|
|
179
|
+
|
|
180
|
+
The tool calls an AI model through a small wrapper for each family of models. Only `gemini` is included so far. Which models it uses is up to you, in `proofreader.yml`:
|
|
181
|
+
|
|
182
|
+
```yaml
|
|
183
|
+
provider: gemini
|
|
184
|
+
models:
|
|
185
|
+
- first-choice-model
|
|
186
|
+
- second-choice-model
|
|
187
|
+
review_models:
|
|
188
|
+
- another-model
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
- `models` is tried in order. A model that is overloaded, over its limit or not available to your key is skipped, and the next one is used.
|
|
192
|
+
- `review_models` is optional. The independent second review prefers a model different from the first audit. Without this setting it uses `models`.
|
|
193
|
+
- Without `models`, the tool falls back on a built-in list. Model names change often, so set your own. `yuno-proofreader models` shows which models your key can use.
|
|
194
|
+
- Leave out `provider` while only one is installed.
|
|
195
|
+
|
|
196
|
+
## Previewing what is sent
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
yuno-proofreader prompt PAGE [FILE] [--review] [--full]
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Shows exactly what would be sent to the model for one page, before any model is called.
|
|
203
|
+
It makes no model request and needs neither an API key nor a spreadsheet.
|
|
204
|
+
It only fetches the page (its `robots.txt`, your sitemap and each language version of the page), so use it on sites you own.
|
|
205
|
+
|
|
206
|
+
- `PAGE` is a page name from `sitemap --all`, or any address of the page.
|
|
207
|
+
- `FILE` is your settings file. Without it, `proofreader.yml` in the current folder is read.
|
|
208
|
+
- `--review` shows the prompt of the independent second review instead of the audit.
|
|
209
|
+
- `--full` prints the whole HTML of each language version. Without it, each version is shortened to its first 300 characters on screen. The sizes in the first lines always count the full text.
|
|
210
|
+
|
|
211
|
+
The command refuses to run while the settings that describe your site are unfinished (see "Describing your site"), because the audit would refuse too.
|
|
212
|
+
`yuno-proofreader settings` shows what is missing.
|
|
213
|
+
|
|
214
|
+
## Auditing a page
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
yuno-proofreader audit PAGE [FILE] [--dry-run] [--force] [--verbose]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Audits one page, in all its language versions, and updates the sheet. It needs the tabs from `prepare`, the pages from `pages import`, a model API key and the finished settings that describe your site. `PAGE` is a page name from the first column of the `Pages` tab, or any address of the page.
|
|
221
|
+
|
|
222
|
+
What happens:
|
|
223
|
+
|
|
224
|
+
1. The page is fetched in every language. If its content has not changed since its last complete audit, it is skipped and no model is called.
|
|
225
|
+
2. A model audits it step by step. A second model, different from the first when your `models` list has another, reviews it independently without seeing the first result.
|
|
226
|
+
3. Both lists are merged. Each new suggestion records who found it in `found_by`: `both`, `audit`, `review`, or `audit (unreviewed)` when the review failed.
|
|
227
|
+
4. The findings are compared with the suggestions already in the sheet for that page:
|
|
228
|
+
- A suggestion that is already `open` or `on staging` is counted as seen again (`last_seen`, `amount_seen`), not added twice.
|
|
229
|
+
- One you marked `rejected` is not added again. One you marked `done` whose text comes back is added as a new row, with "[Reappeared after being marked done]" in the reason.
|
|
230
|
+
- An `open` or `on staging` suggestion whose quoted text is no longer on the live page is closed and set to `done`. It only closes when the page loaded completely and the suggestion's text had been found on the page when it was written.
|
|
231
|
+
5. The page's row records `last_audited` and, after a complete audit, a `fingerprint`.
|
|
232
|
+
|
|
233
|
+
Reading a suggestion: `verified` says whether the quoted text is on the page (`yes`), only matches when spaces are ignored (`loose`) or is not there at all (`no`, which usually means the model misquoted). `approximate_place` is the heading path of the text, worked out by the tool.
|
|
234
|
+
|
|
235
|
+
An audit is complete when every language version was fetched and not cut at the size limit, every audit step was answered, and the review answered. Only then is the fingerprint stored. Anything less is written, but the page is audited again next time. `last_audited` means the last time the tool checked the page, and it is also set when an unchanged page is skipped.
|
|
236
|
+
|
|
237
|
+
Options:
|
|
238
|
+
|
|
239
|
+
- `--dry-run` runs everything except the writing. The model is still called, so it costs the same as a real run.
|
|
240
|
+
- `--force` audits the page even if it has not changed. Use it after you change your settings or the prompt files, which the fingerprint does not notice.
|
|
241
|
+
- `--verbose` lists every decision, including suggestions that were seen again or suppressed, and the items in the model answers that could not be used.
|
|
242
|
+
|
|
243
|
+
Each audit makes two model calls with the whole HTML of every language version, so cost depends on your pages and models. The report shows the tokens of each call.
|
|
244
|
+
|
|
245
|
+
### What is in the output
|
|
246
|
+
|
|
247
|
+
The first lines give the size of the prompt: its characters and a very rough token estimate (four characters per token).
|
|
248
|
+
Then come two parts:
|
|
249
|
+
|
|
250
|
+
- **System prompt:** the same for every page of your site. It holds the role, the description of your site and your language notes, your rules, ignore terms and intentional differences (each only if you set them), the rules about effort and about what to report, the audit steps with the categories they may use, and the exact JSON shape the answer must have. For the review, the steps give way to a checklist and one flat list.
|
|
251
|
+
- **User message:** one block per language version of the page, from `=== DE | address ===` to `=== END DE ===`. The header names the versions included. A version that could not be fetched is listed as not available, so the model does not report differences it cannot check. A version that was cut at the size limit says so.
|
|
252
|
+
|
|
253
|
+
The text the model sees is the page's HTML with scripts, styles, SVG images and comments removed, and runs of whitespace joined. Everything else stays, including the head, the navigation and the footer.
|
|
254
|
+
|
|
255
|
+
The explanation the model gives for each suggestion (the reason column) is written in English, even when your site is in German or Spanish.
|
|
256
|
+
The quoted text and the suggested replacement stay in the language of your page.
|
|
257
|
+
|
|
258
|
+
### Changing the wording
|
|
259
|
+
|
|
260
|
+
The instructions are plain files in `lib/yuno/proofreader/audit/prompts/`:
|
|
261
|
+
|
|
262
|
+
- `instructions.yml`: the role, the rules and the two procedures.
|
|
263
|
+
- `steps.yml`: the checks of the audit, in order. Each has a `number`, a `name`, the `categories` its findings may have, and a `text` that says what to look for. A step with the category `parity` is left out for a site with one language.
|
|
264
|
+
|
|
265
|
+
Edit them in a checkout or a fork, since an installed gem keeps them in its own folder. Run `prompt` to see the effect.
|
|
266
|
+
|
|
267
|
+
Everything in the output is sent to your model provider. See "Privacy and responsible use".
|
|
268
|
+
|
|
269
|
+
### Respecting robots.txt
|
|
270
|
+
|
|
271
|
+
Before it fetches a page of your site, the tool checks your `robots.txt`. It identifies itself as `yuno-proofreader`, and it does not fetch a page that the rules for that name, or for all crawlers (`User-agent: *`), disallow. When rules conflict, the longest matching path wins, and `Allow` wins a tie. It also waits half a second between requests. There is nothing to configure, and no setting turns this off.
|
|
272
|
+
|
|
273
|
+
A page that is blocked is reported as "blocked by robots.txt". `fetch` shows it, an audit treats that language version as not available (the model is told, no differences involving it are reported, and the audit does not count as complete), and the analysis leaves the page out.
|
|
274
|
+
|
|
275
|
+
If the tool cannot read a page of your own site, check your `robots.txt`. To let it in while keeping other crawlers out, add a group for it:
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
User-agent: yuno-proofreader
|
|
279
|
+
Allow: /
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Ranking the pages by importance
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
yuno-proofreader analyze [FILE] [--dry-run] [--no-ai] [--reuse-ai]
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Ranks every page of the site by how much it matters, so the most important pages can be audited most often. It needs the tabs from `prepare` and the pages from `pages import`.
|
|
289
|
+
|
|
290
|
+
How a page is scored:
|
|
291
|
+
|
|
292
|
+
- **The structure of the site** (most of the score): how the homepage leads to the page through its navigation and the lists of the index pages, where earlier entries count more; whether the homepage shows it; how many pages list it; and how recently it changed. Lists are read from structured data (`ItemList`, `OfferCatalog`), and from the cards of a page when there is none. Only the default language is read, usually a few dozen pages.
|
|
293
|
+
- **The judgement of an AI model:** how well known the places are among your audience, and how much a reader would want each page. It sees only the names the site lists the pages under, not the pages themselves. A second model judges independently. The less the structure of the site says, the more the judgement counts.
|
|
294
|
+
- **Your own points:** `priority_paths` in the `analysis:` block of `proofreader.yml`, and the `boost` column of the Pages tab.
|
|
295
|
+
|
|
296
|
+
What is written:
|
|
297
|
+
|
|
298
|
+
- **Pages tab:** `importance` (0 to 100), `tier` (1 is the most important, about the top tenth), `importance_notes` (why, in a few words) and `analyzed_at`. `boost` is yours: a number of points added to the score. The tool reads it and never writes it.
|
|
299
|
+
- **Analysis tab:** one row per page with each measurement and the model's view (kind, place, value, certainty, reason, and how far apart the two models were). It is rewritten on every run.
|
|
300
|
+
|
|
301
|
+
The homepage always ranks first. Run it again after big changes to the site. To try other weights, `priority_paths` or boosts without asking the models again, use `--reuse-ai`, which takes their last judgement from the Analysis tab. The weights are in `lib/yuno/proofreader/analysis/weights.yml`, and the model's wording in `analysis/judge.yml`.
|
|
302
|
+
|
|
303
|
+
## Logging
|
|
304
|
+
|
|
305
|
+
The tool writes a log to stderr, next to the report it prints on stdout. It is meant for runs nobody watches: it says which page was audited, what came of it and what the models used. Set `LOG_LEVEL=info` for one line per run, batch and page, or `debug` to add a line per model call. The default, `warn`, shows only problems.
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
time=2026-10-25T12:00:05Z level=info msg="page audited" run=3fa91c2e page=about status=audited complete=true added=3 closed=1 model_calls=2 input_tokens=62000 output_tokens=9000
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
With `LOG_FORMAT=json` each line is one JSON object with `severity`, `message` and `time`, plus the same fields. Every line of a run carries the same `run` id.
|
|
312
|
+
|
|
313
|
+
| Message | Level | What it tells you |
|
|
314
|
+
|---|---|---|
|
|
315
|
+
| `run started`, `run finished` | info | The command, and `result=ok` with the exit code and seconds |
|
|
316
|
+
| `run failed` | error | A run that did not succeed, with its exit code (and the error, when it reached the log) |
|
|
317
|
+
| `page audited`, `page skipped` | info | The page, what was added, seen again, closed or suppressed, and the models' tokens |
|
|
318
|
+
| `audit incomplete` | warn | Why the page's fingerprint was not recorded, so it will be tried again |
|
|
319
|
+
| `page failed` | error | A page that failed for its own reason and was set aside |
|
|
320
|
+
| `batch finished` | info | Pages audited, skipped and failed, how many are still due, and the tokens |
|
|
321
|
+
| `batch stopped` | error | A problem that ended the batch, such as no model being available |
|
|
322
|
+
| `nothing is due` | info | The queue was empty |
|
|
323
|
+
| `analysis finished` | info | What `analyze` ranked and wrote |
|
|
324
|
+
| `model failed`, `model paused`, `model answer unusable` | warn | A model that errored, was set aside for a while, or answered without usable JSON |
|
|
325
|
+
| `model answered` | debug | One model call: the model, seconds and tokens |
|
|
326
|
+
|
|
327
|
+
Page content, prompts, model answers and keys are never logged.
|
|
328
|
+
|
|
329
|
+
On Google Cloud, set `LOG_LEVEL=info` and `LOG_FORMAT=json`. Cloud Logging reads a JSON line on stdout or stderr as a structured entry: `severity` becomes the entry's severity, `message` its text, and the other fields can be searched, for example `jsonPayload.page="about"`. An alert on entries with `severity>=ERROR` catches failed runs and failed pages. The exit code (`0` when a run succeeded, `1` for an error, `2` for a usage error) is what marks a Cloud Run job execution as failed.
|
|
330
|
+
|
|
331
|
+
## How the sitemap is read
|
|
332
|
+
|
|
333
|
+
The tool needs your sitemap to know which pages exist and which of them are language versions of each other.
|
|
334
|
+
|
|
335
|
+
**Finding it.** Unless `sitemap_url` is set, the tool reads the `Sitemap:` lines of your `robots.txt` (all of them), and falls back to `/sitemap.xml`.
|
|
336
|
+
A sitemap index is followed two levels deep. At most 25 sitemaps and 50,000 pages are read.
|
|
337
|
+
Sitemaps on another host are ignored, and `www.example.com` counts as the same host as `example.com`.
|
|
338
|
+
Compressed sitemaps (`.xml.gz`) are not supported yet.
|
|
339
|
+
The `sitemap` command shows what was found and any warnings.
|
|
340
|
+
|
|
341
|
+
**Pairing the languages.** Each page must be matched with its other language versions. The tool uses the first method that fits:
|
|
342
|
+
|
|
343
|
+
1. **hreflang alternates in the sitemap:** if at least half of the entries list alternates, they decide the pairs.
|
|
344
|
+
2. **Language prefix and position:** otherwise, pages are grouped by the prefixes in your `languages` setting and matched by their position. All pages of one language must be listed in the same order as in the other languages, and every language must have the same number of pages.
|
|
345
|
+
|
|
346
|
+
Pages that exist in one language only go under `default_language_only_paths`, and pages you don't want checked under `skip_paths`. If the lists don't match up, the tool says so and writes nothing, because a wrong pairing would produce wrong findings on every page.
|
|
347
|
+
|
|
348
|
+
## Working with the sheet
|
|
349
|
+
|
|
350
|
+
The tool keeps its work in one Google Sheet: a queue of pages and a list of suggestions.
|
|
351
|
+
Both commands below need `SPREADSHEET_ID` and working Google access (see "Connecting to Google"), and neither needs a Gemini key.
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
yuno-proofreader prepare
|
|
355
|
+
yuno-proofreader pages import
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Run them in this order, from the folder that contains your `proofreader.yml` (or pass the path of the settings file as the last argument).
|
|
359
|
+
|
|
360
|
+
### prepare
|
|
361
|
+
|
|
362
|
+
Creates two tabs in your spreadsheet:
|
|
363
|
+
|
|
364
|
+
- **Pages**: one row per page of your site. Columns: `page_name`, one `<code>_url` column per language of your settings, `last_audited`, `fingerprint`, `note` and `sitemap_lastmod`.
|
|
365
|
+
- **Suggestions**: one row per suggested fix. Columns: `timestamp`, `page`, `language`, `category`, `effort`, `approximate_place`, `original`, `suggested`, `reason`, `verified`, `status`, `found_by`, `last_seen` and `amount_seen`. `status` is a dropdown with `open`, `on staging`, `done` and `rejected`.
|
|
366
|
+
|
|
367
|
+
Both tabs get a frozen header row and date formats. An empty default tab (`Sheet1`) is removed.
|
|
368
|
+
|
|
369
|
+
It is safe to run again. A tab that already has a header row is left as it is, and the tool only checks that its required columns exist.
|
|
370
|
+
To apply the formatting again, delete the tab and run `prepare` once more.
|
|
371
|
+
|
|
372
|
+
### pages import
|
|
373
|
+
|
|
374
|
+
Reads your sitemap and adds each page to the `Pages` tab, with its address in every language. It needs the tabs from `prepare`.
|
|
375
|
+
|
|
376
|
+
- Pages that are already in the sheet are skipped (they are recognised by their address in the default language), so running it again only adds new pages.
|
|
377
|
+
- Nothing is overwritten. Pages that have left the sitemap stay in the sheet, and the command tells you how many.
|
|
378
|
+
- If the pages cannot be paired by language, it writes nothing. The `sitemap` command shows why.
|
|
379
|
+
- A row you delete comes back at the next import. To exclude a page for good, add it to `skip_paths` in your settings file.
|
|
380
|
+
|
|
381
|
+
### Editing the sheet
|
|
382
|
+
|
|
383
|
+
Columns are found by their header names, so you can reorder them or add columns of your own. Don't rename the columns listed above. Times are stored in UTC.
|
|
384
|
+
|
|
385
|
+
## Development
|
|
386
|
+
|
|
387
|
+
Assuming Podman:
|
|
388
|
+
|
|
389
|
+
```
|
|
390
|
+
cp .env.example .env # then fill in your values
|
|
391
|
+
podman build -t yuno-proofreader:dev -f Dockerfile.dev .
|
|
392
|
+
sh start-dev.sh
|
|
393
|
+
podman exec -it proofreader-dev bash
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Inside the container:
|
|
397
|
+
|
|
398
|
+
```
|
|
399
|
+
bundle exec rake test
|
|
400
|
+
bundle exec rubocop
|
|
401
|
+
gem build yuno-proofreader.gemspec
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
To build the production image:
|
|
405
|
+
|
|
406
|
+
```
|
|
407
|
+
podman build -t yuno-proofreader .
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Trying it on your own site
|
|
411
|
+
|
|
412
|
+
```
|
|
413
|
+
bundle exec ruby -Ilib scripts/try_sitemap.rb
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
The sitemap command shows how the pages were paired.
|
|
417
|
+
This script also fetches a sample of pages in every language and compares each page’s own hreflang tags with the pairing.
|
|
418
|
+
Options are environment variables:
|
|
419
|
+
|
|
420
|
+
| Variable | Default | Meaning |
|
|
421
|
+
|---|---|---|
|
|
422
|
+
| `SAMPLE` | `2,2,4` | First pages, last pages and random pages to check (`0,0,0` skips the page checks) |
|
|
423
|
+
| `SEED` | random | Repeat a random sample |
|
|
424
|
+
| `DELAY` | `0.5` | Seconds to wait between requests |
|
|
425
|
+
|
|
426
|
+
To see how the tool reads one page (its headings, and where a text sits under them), run `bundle exec ruby -Ilib scripts/try_place.rb PAGE LANG ["quoted text"]`.
|
|
427
|
+
|
|
428
|
+
## Connecting to Google
|
|
429
|
+
This gem connects to Google Sheets and an AI model provider (Gemini so far).
|
|
430
|
+
I will not try to document the way to do it, because it differs by permissions, type of Accounts, etc.
|
|
431
|
+
Misconfiguring it could become costly, so make sure you do it correctly.
|
|
432
|
+
|
|
433
|
+
## Contributing
|
|
434
|
+
|
|
435
|
+
See CONTRIBUTING.md. It is provided as is, and replies may take a while.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "yuno/proofreader"
|
|
5
|
+
|
|
6
|
+
$stdout.sync = true
|
|
7
|
+
$stderr.sync = true
|
|
8
|
+
|
|
9
|
+
begin
|
|
10
|
+
Yuno::Proofreader::EnvFile.load&.warnings&.each { |warning| warn "Warning: #{warning}" }
|
|
11
|
+
rescue Yuno::Proofreader::ConfigError => e
|
|
12
|
+
warn "Error: #{e.message}"
|
|
13
|
+
exit 1
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
exit Yuno::Proofreader::CLI.run(ARGV)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yuno
|
|
4
|
+
module Proofreader
|
|
5
|
+
module Analysis
|
|
6
|
+
# Points a person adds to the score of a page, and where they come from.
|
|
7
|
+
Adjustment = Data.define(:points, :notes)
|
|
8
|
+
|
|
9
|
+
# Turns priority_paths from the settings and the boost of each page into adjustments, keyed by page key.
|
|
10
|
+
# The most specific path wins, and a boost adds to it. Pages without either get no adjustment.
|
|
11
|
+
module Adjustments
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# boosts: the boost of each page, keyed by page key.
|
|
15
|
+
def call(pages:, language:, settings:, boosts: {})
|
|
16
|
+
pages.each_with_object({}) do |page, found|
|
|
17
|
+
key = Links.key(page.urls[language] || page.urls.values.first)
|
|
18
|
+
parts = [priority(Reasons.shown(key), settings.priority_paths), boost(boosts[key])].compact
|
|
19
|
+
found[key] = Adjustment.new(points: parts.sum(&:first), notes: parts.map(&:last)) unless parts.empty?
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def priority(path, rules)
|
|
24
|
+
match = rules.select { |prefix, _points| covers?(prefix, path) }.max_by { |prefix, _points| prefix.length }
|
|
25
|
+
return if match.nil? || match.last.zero?
|
|
26
|
+
|
|
27
|
+
[match.last, "#{signed(match.last)} from priority_paths (#{match.first})"]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def covers?(prefix, path) = prefix == "/" || path == prefix || path.start_with?("#{prefix}/")
|
|
31
|
+
|
|
32
|
+
def boost(value)
|
|
33
|
+
return if value.nil? || value.zero?
|
|
34
|
+
|
|
35
|
+
[value, "#{signed(value)} from boost"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def signed(number)
|
|
39
|
+
shown = number == number.round ? number.round : number.round(1)
|
|
40
|
+
number.positive? ? "+#{shown}" : shown.to_s
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yuno
|
|
4
|
+
module Proofreader
|
|
5
|
+
module Analysis
|
|
6
|
+
# What the crawl found. facts and failures are keyed by Links.key of a page's address. unread holds the index
|
|
7
|
+
# pages that were left out because the limit was reached.
|
|
8
|
+
Inventory = Data.define(:home, :facts, :failures, :tree, :unread) do
|
|
9
|
+
def facts_for(url) = facts[Links.key(url)]
|
|
10
|
+
def read?(url) = facts.key?(Links.key(url))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Reads the structure of a site, starting at its homepage: the homepage, the pages its header, navigation and
|
|
14
|
+
# footer link to, any index pages named in the settings, the pages that have other pages under them, and the
|
|
15
|
+
# entries of the lists it reads that are index pages themselves. Only the default language is read, because
|
|
16
|
+
# the pairing already knows each page's other language versions
|
|
17
|
+
class Crawler
|
|
18
|
+
MAX_PAGES = 80
|
|
19
|
+
INDEX_TYPES = %w[CollectionPage ItemList].freeze
|
|
20
|
+
|
|
21
|
+
def initialize(site:, pages:, fetcher:, extra_hubs: [], max_pages: MAX_PAGES, progress: ->(_message) {})
|
|
22
|
+
@site = site
|
|
23
|
+
@language = site.default_language
|
|
24
|
+
@tree = UrlTree.new(pages.filter_map { |page| page.urls[@language] || page.urls.values.first })
|
|
25
|
+
@fetcher = fetcher
|
|
26
|
+
@extra = extra_hubs.map { |path| "#{site.website}/#{path.to_s.delete_prefix('/')}" }
|
|
27
|
+
@max = max_pages
|
|
28
|
+
@progress = progress
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def call
|
|
32
|
+
@facts = {}
|
|
33
|
+
@failures = {}
|
|
34
|
+
read(home)
|
|
35
|
+
queue = unique([*from_home, *@extra, *@tree.hubs])
|
|
36
|
+
until queue.empty? || visited >= @max
|
|
37
|
+
url = queue.shift
|
|
38
|
+
next if done?(url)
|
|
39
|
+
|
|
40
|
+
read(url)
|
|
41
|
+
queue = unique(queue + listed_index_pages(url))
|
|
42
|
+
end
|
|
43
|
+
Inventory.new(home:, facts: @facts, failures: @failures, tree: @tree,
|
|
44
|
+
unread: queue.reject { |url| done?(url) })
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# The default language's homepage, written as the sitemap writes it when the sitemap has it.
|
|
50
|
+
def home
|
|
51
|
+
@home ||= begin
|
|
52
|
+
address = "#{@site.website}#{@site.prefixes[@language]}"
|
|
53
|
+
@tree.urls.find { |url| Links.key(url) == Links.key(address) } || address
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def from_home
|
|
58
|
+
facts = @facts[Links.key(home)]
|
|
59
|
+
facts ? facts.chrome_links.values.flatten.select { |url| @tree.include?(url) } : []
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Entries of the lists a page publishes that are index pages themselves: typed as a collection, or with
|
|
63
|
+
# pages under them. Only pages the tool tracks are followed.
|
|
64
|
+
def listed_index_pages(url)
|
|
65
|
+
facts = @facts[Links.key(url)]
|
|
66
|
+
return [] unless facts
|
|
67
|
+
|
|
68
|
+
entries = facts.lists.flat_map(&:list_members)
|
|
69
|
+
entries.select { |entry| @tree.include?(entry.url) && index_like?(entry) }.map(&:url)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def index_like?(entry)
|
|
73
|
+
entry.types.intersect?(INDEX_TYPES) || !@tree.children(entry.url).empty?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def read(url)
|
|
77
|
+
@progress.call("Reading #{url}")
|
|
78
|
+
page = Sitemap::Page.new(name: url, urls: { @language => url }, lastmod: "", notes: [])
|
|
79
|
+
snapshot = @fetcher.fetch(page).snapshots.first
|
|
80
|
+
if snapshot.ok?
|
|
81
|
+
@facts[Links.key(url)] = Extractor.call(snapshot.raw, url:, base: snapshot.final_url)
|
|
82
|
+
else
|
|
83
|
+
@failures[Links.key(url)] = snapshot.problem
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def done?(url) = @facts.key?(Links.key(url)) || @failures.key?(Links.key(url))
|
|
88
|
+
|
|
89
|
+
def visited = @facts.size + @failures.size
|
|
90
|
+
|
|
91
|
+
def unique(urls) = urls.uniq { |url| Links.key(url) }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|