viafrei 1.3.12 → 1.3.16

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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,688 @@
3
3
  All notable changes to this package are documented here. The format follows Keep
4
4
  a Changelog and the versions follow Semantic Versioning.
5
5
 
6
+ ## [Unreleased]
7
+
8
+ Nothing yet.
9
+
10
+ ## [1.3.16] - 2026-09-27
11
+
12
+ **This is the release that delivers the licence corrections to the people the
13
+ licences point at.** `NOTICE` is the file Apache-2.0 § 4(d) makes every downstream
14
+ redistributor carry, and until this publish the corrected text existed only in the
15
+ repository: the 1.3.15 tarball on the registry still carries the previous wording. So
16
+ publishing is not a side effect of the correction, it **is** the correction — which
17
+ is the whole reason this release exists, because nothing in `dist/` changes.
18
+
19
+ The number matches the running service rather than counting the changes here.
20
+ Production answered `initialize` with `1.3.16` when it was asked on 2026-09-27, and
21
+ this package is the bridge to that server, so somebody comparing the two now reads
22
+ one number instead of wondering which is behind.
23
+
24
+ **No behaviour changes.** `dist/` is built rather than committed, and no file under
25
+ `src/` has changed since the 1.3.15 tag — which is the evidence for the claim, since
26
+ a diff of an untracked directory would be no evidence at all. So no flag, default or
27
+ exit code moves. **Six** of the seven `files` entries do change —
28
+ `README.md`, `LICENSE`, `NOTICE`, `SOURCES.md`, `CHANGELOG.md` and the new
29
+ `API.md` — and the tarball goes from twenty paths to twenty-one. (Six *entries*,
30
+ not six files: `dist` is a directory.)
31
+
32
+ This paragraph has now been wrong twice, which is worth leaving on the record
33
+ rather than tidying away. The first draft called these "the repository's own
34
+ development scripts" and said the next tarball would differ in two files — true of
35
+ the first change here, false once the licence work landed beside it. The second
36
+ draft said **four** files and omitted `LICENSE` — and the commit that wrote that
37
+ sentence is the same commit that changed `LICENSE`, so it was false at the moment
38
+ it was written, in the paragraph written to correct a false count. The count moved
39
+ once more at the cut, because `API.md` joined `files` in this release. Every
40
+ version of it has been computed from `package.json`'s `files` intersected with
41
+ `git diff --name-only v1.3.15..HEAD`, with the path count from `npm pack --json`,
42
+ rather than counted by eye — which is the only way this sentence has ever been
43
+ right.
44
+
45
+ ### Added
46
+
47
+ - **A sweep that keeps the twenty-first call site from being written by accident**
48
+ (`scripts/tools.test.mjs`, `npm run test:tools`, and a step in CI). Eighteen
49
+ cases: what `resolveTool` accepts and refuses, that its directory list is
50
+ root-owned and not group- or other-writable (the property the module relies on,
51
+ rather than the list that is supposed to have it), that its contents cannot be
52
+ extended at runtime, that an `npm_execpath` which is absolute, real and readable
53
+ but not npm is refused, and then a sweep of all thirty-two source files,
54
+ walked recursively, for a spawn whose program is a bare quoted name.
55
+
56
+ It carries three preconditions, because a gate whose input is absent reports
57
+ success about what it never read: **every** source root must have contributed a
58
+ file and an unreadable root is a failure rather than an absence; the expression
59
+ must locate a program argument in a real file of this repository, proved by
60
+ putting a real call's program back to a literal in memory and requiring it to be
61
+ found; and it must go red on a planted bad call, with the fixtures assembled at
62
+ runtime so no literal in this file can satisfy its own sweep.
63
+
64
+ Both of the last two exist because the first drafts did not do what their own
65
+ comments claimed. The floor was a floor on the *sum*, and `scripts` and `test`
66
+ clear it between them — so `src/` could be deleted entirely and the run still
67
+ printed ok at 21 files. And the "it can see a call" precondition evaluated the
68
+ real expression only on the negative case; the positive half used a different,
69
+ simpler pattern and never asked the real one to read a file.
70
+
71
+ Proved the way a gate has to be: one real call site reverted to a bare name
72
+ turns the sweep red, one source root removed turns precondition 1 red, and the
73
+ files were restored byte-identical afterwards.
74
+
75
+ - **[API.md](API.md): a reference for all 18 tools, generated rather than written.**
76
+ `catalogue.json` is what the production server answered when it was asked to
77
+ describe itself — `initialize` plus `tools/list`, `resources/list`,
78
+ `resources/templates/list` and `prompts/list`, captured on 2026-09-27 from
79
+ server `1.3.16`. `scripts/gen-api-doc.mjs` renders the document from it
80
+ (`npm run docs:api`), and `npm run check:docs` in CI fails if the two have
81
+ drifted. Every tool description in it is the server's own text, verbatim,
82
+ because that text is what an assistant reads when it decides which tool to
83
+ call — paraphrasing it would document a different server.
84
+
85
+ **No tool was invoked to produce the snapshot.** Listing is metadata; calling is
86
+ traffic, and one of the providers behind this service sets a floor on how often a
87
+ station may be queried, with the access itself at risk if it is exceeded. The
88
+ capture is therefore five list methods and nothing else.
89
+
90
+ **What generating it does and does not fix**, because README.md already argued
91
+ the opposite case and that argument was right. Drift between the document and the
92
+ snapshot is now impossible to keep — CI regenerates and compares. Drift between
93
+ the snapshot and the live server is not fixed by anything, because a capture is a
94
+ point in time. So API.md's own header says it is a dated snapshot, carries the
95
+ date, and names the running server as the source of truth — in that file and not
96
+ only in README.md. Both of them ship in the tarball, so that is not the reason;
97
+ the reason is that a qualification has to travel with the document it qualifies,
98
+ because somebody who opens the catalogue to look up a parameter has no occasion
99
+ to read the page beside it.
100
+
101
+ Two substitutions in the snapshot, both recorded in its own `$comment` rather
102
+ than left as silent differences from what the server sent: the 18 per-tool
103
+ `$schema` declarations are dropped as one constant repeated 18 times, and two
104
+ 288-character date patterns are stored as their LENGTH. The second was forced by
105
+ the leak sweep and the interesting part is which side gave way — such a regex
106
+ spells its arithmetic as character classes of selected digits, which a digit-run
107
+ scanner cannot tell from a five-digit internal value. Both remedies on the
108
+ sweep's side would have been falsehoods: narrowing the scanner weakens it for
109
+ every file, and an allow-list entry would record that a character class is a
110
+ number this project publishes. So the text that cannot be scanned is not stored.
111
+ Nothing is hidden by it — the document already summarised a pattern over 60
112
+ characters by its length, and the server hands anyone the full expression.
113
+
114
+ One parameter is an object whose seven keys each carry their own bounds, and the
115
+ first draft rendered it as a dash in both the default and the constraints
116
+ column — the table's strongest claim broken on the row with the most to say. The
117
+ cause defeated the safety net as well: `properties` sat in the set of keywords
118
+ the renderer treats as handled, so the fallback that lists an unrecognised
119
+ keyword as a bare name never fired. The keyword was known; it was simply never
120
+ rendered.
121
+
122
+ - **A self-test for the API-reference generator** (`scripts/gen-api-doc.test.mjs`,
123
+ `npm run test:docs`, and a step in CI). Its cases include the drift check refusing a
124
+ hand-edited file, never writing during `--check`, and naming `docs:api` when the
125
+ document is absent; eight snapshot mutations each refused with a named reason; a
126
+ malformed `required` rendering rather than throwing; a planted nested `default`
127
+ reaching the document; a union type in a table cell and in a bullet; an
128
+ array-of-scalar; and the reported line count agreeing with `wc -l`. The run prints
129
+ the total, which is the number to trust — this file has been wrong about counts
130
+ more than once, so it states none here.
131
+
132
+ It exists because the generator was the only script here making claims with nothing
133
+ checking them, and the same six mutants were being re-run by hand across three
134
+ review rounds — a check performed by remembering is not a check. It also earned
135
+ itself immediately, by catching two defects nothing else could:
136
+
137
+ The whitespace-flattening expression, rewritten to remove a super-linear
138
+ backtracking pattern, **was not equivalent to what it replaced.** It handled only
139
+ spaces and tabs beside the newline, so a `\r\n` line ending would have left a stray
140
+ carriage return in the document. API.md regenerated byte-identical, `--check`
141
+ passed, every gate was green — because this snapshot contains no CRLF. An output
142
+ comparison can only speak about the input it was given, so the test compares the two
143
+ **expressions** over every string up to length four drawn from a whitespace-heavy
144
+ alphabet, plus random longer ones, and asserts first that the alphabet can expose
145
+ the bug that shipped.
146
+
147
+ And a **union type was escaped twice**: `typeOf` joined with an already-escaped pipe
148
+ and the cell renderer escaped that pipe again, giving `string \\| null` — a literal
149
+ backslash, and a bare pipe left to end the table row early. In a bullet, which is not
150
+ a table, it produced a stray backslash instead. No tool in this snapshot declares a
151
+ union type, so neither was reachable and no comparison of documents could have found
152
+ it. Escaping now happens in one place, the one that knows it is writing a table cell.
153
+
154
+ - **[SUPPORT.md](SUPPORT.md)**, so GitHub's issue chooser has somewhere to point:
155
+ where a question, a wrong answer and a security report each go, and what makes a
156
+ report actionable.
157
+
158
+ - **README gains a Documentation table and five worked use cases** — a motorway
159
+ briefing, a broken commute including a station lift that is out, an EV weekend, a
160
+ dispatcher's morning brief, and a local-guide agent — each named by the question
161
+ it answers rather than by the tools it calls.
162
+
163
+ ### Changed
164
+
165
+ - **One entry of 13 characters left the leak sweep's private-name list, under that
166
+ list's own rule 1.** The rule is stated in `scripts/rules.json`: an entry must not
167
+ be a substring of text this repository legitimately prints, because such an entry
168
+ can never be satisfied, and the only ways out are deleting it or narrowing the
169
+ scanner — which is strictly worse. There is no mechanical test for it; the sweep
170
+ going red *is* the test, and it went red. Declared narrowing: one spelling of 13
171
+ characters is no longer matched anywhere. The list's second rule is not engaged,
172
+ which decides that nothing else follows: the audit recorded in that file says
173
+ nothing on the list is from the class whose harm is confirming a guess —
174
+ credentials, tokens, session or contract identifiers, access-granting hostnames,
175
+ personal data — so there is no rotation and no rename here.
176
+
177
+ Recorded that way on purpose, and it is a change of practice rather than of
178
+ style. The two removals recorded under 0.0.9 described what the entries were
179
+ ABOUT, and taken together those descriptions narrowed the candidate space for a
180
+ live entry further than a hash does — the same caption failure the number
181
+ allow-list refuses on the facing page. The file now ends with the ruling: record
182
+ the rule, the lengths and the narrowing, never the subject. This is the first
183
+ entry written under it. The older wordings stay where they are, because editing
184
+ a published file does not unpublish it.
185
+
186
+ Coverage was asked of the change rather than asserted, since going from six
187
+ entries to five must not leave the sweep reporting PASS about what it no longer
188
+ looks for. The matcher was exercised per slot without needing any real name:
189
+ the list replaced by five planted names, each planted in text, five reds
190
+ required, and a negative control that stays clean. The sweep also prints the
191
+ list's size on every run, so the change is visible rather than silent, and the
192
+ gate self-test still refuses an emptied list. The window `minTokenLength` and
193
+ `maxTokenLength` is unchanged at 7 and 15: both are stored literals, neither is
194
+ derived from the list, and the removed entry sat at neither bound, so no
195
+ published number narrowed.
196
+
197
+ - **`numbers.allowed` admits four values the SERVER publishes about itself** — two
198
+ inside tool descriptions and two as bounds in its own input schemas — under one
199
+ general rule written into that file rather than a caption each, because a caption
200
+ per value is the construction the list exists to avoid. It is safe to state in
201
+ general terms because the sweep already refuses an allowed number that occurs
202
+ nowhere in the tree, so an exemption cannot outlive its reason.
203
+
204
+ - **Four statements about other people's licences were wrong, and they are the kind
205
+ a reader acts on.** Every one was verified against its own source before it was
206
+ changed, and each correction says that an earlier version had it wrong rather than
207
+ quietly reading better.
208
+ - **Share-alike reaches adaptations, not aggregations.** `SOURCES.md`, `NOTICE`
209
+ and `README.md` all said DELFI data must not be blended into a result under a
210
+ different licence. CC BY-SA 4.0 art. 3(b) is expressed only over Adapted
211
+ Material (art. 1(a)); showing licensed data beside another source's data is an
212
+ aggregation, and Creative Commons states in terms that the condition "applies
213
+ only for works considered adaptations under copyright law, not simply in
214
+ collections with other works". Worse than generous in two ways: art. 3(b)(3)
215
+ forbids imposing terms that restrict rights the licence grants, which is what
216
+ telling you an aggregation is forbidden does — and the rule as written condemned
217
+ this service, since a weather answer can name `["dwd","osm"]`.
218
+ - **The DELFI licence version was sourced to nothing.** Measured at both levels
219
+ on 2026-09-27: the GovData record for the realtime feed carries the unversioned
220
+ licence URI and an empty dataset-level licence, and the national access point's
221
+ own metadata reports the same unversioned term. The publisher's sibling feeds in
222
+ the same catalogue *are* versioned, so it states a version when it means one.
223
+ The page now names the family, shows the evidence, and says to treat the
224
+ share-alike as applying regardless — because the version decides what a
225
+ recipient may put on a derivative (art. 3(b)(1)), and 3.0 unported has no
226
+ express database-rights clause where 4.0 art. 4 does.
227
+ - **§ 7 DWD-Gesetz prescribes a duty, not a wording.** Fetched verbatim: the
228
+ section is headed *Quellenschutz*, requires distribution to be "nur unter
229
+ Angabe der Quelle zulässig" and sets no text — and its second sentence adds
230
+ that fuller protection under the Urheberrechtsgesetz "bleibt davon unberührt",
231
+ so attribution is the statute's minimum rather than the whole of what may be
232
+ owed. The three words come from the DWD's own guidance, which also permits the
233
+ logo form — so a reader told the statute fixes the characters might refuse a
234
+ form the publisher expressly allows.
235
+ - **`NOTICE` understated the ODbL duty, and it is the file that republishes
236
+ itself** (Apache-2.0 § 4(d) makes every downstream redistributor carry it). It
237
+ said "address results" where the predicate is which *table* answered — place
238
+ resolution falls through the gazetteer to the OSM tables, so any answer about a
239
+ place may be OSM-derived — and offered one extract where the § 4.6 offer covers
240
+ both addresses and points of interest.
241
+
242
+ ### Fixed
243
+
244
+ - **Every external program these scripts run is now resolved to an absolute path
245
+ instead of being looked up on `$PATH`** (`scripts/tools.mjs`). `git`, `tar`,
246
+ `npm` and a `mkdir` were spawned by bare name, which means the environment — not
247
+ this repository — decided which program actually ran. That matters more here
248
+ than it would elsewhere: one of these scripts is the leak sweep that decides
249
+ whether a commit may be published, and another is the hygiene gate that reads
250
+ the tarball about to be uploaded to the registry. A gate whose implementation
251
+ the caller can substitute is not a gate. SonarCloud reported seven of the call
252
+ sites as `javascript:S4036` and the project's Security Rating on new code stood
253
+ at B because of them.
254
+ - `git` and `tar` now come from `/usr/bin` or `/bin` only. `/usr/local/bin` and
255
+ `/opt/homebrew/bin` are deliberately not searched: they are writable by the
256
+ logged-in user on a normal developer machine, so admitting them would
257
+ reinstate the substitution this change removes. A tool that is genuinely
258
+ elsewhere makes the scripts refuse and say where they looked, which is a
259
+ better failure than quietly running something else.
260
+ - `npm` is no longer treated as a program at all. It is a JavaScript file, so it
261
+ is run as `<absolute node> <absolute npm-cli.js>`; node's own path is
262
+ `process.execPath`, which nothing can substitute. `mkdir` was replaced by
263
+ `fs.mkdirSync` — no subprocess at all.
264
+ - **Twenty call sites changed, not the seven that were reported.** The other
265
+ thirteen are in the two self-tests, which SonarCloud does not analyse. Leaving
266
+ them would have left the rule true of the code and false of the repository,
267
+ and a rule with a quiet exemption is the one nobody remembers when adding the
268
+ next call. (Counted per file in the finished tree: `check-leaks.mjs` 2,
269
+ `check-tarball.mjs` 4, `npm-pack-json.mjs` 1 — Sonar's seven — then
270
+ `check-leaks.test.mjs` 3 and `check-tarball.test.mjs` 10. The first version of
271
+ this entry said nineteen, because it was counted with a single-line grep that
272
+ cannot see the one call whose program argument sits on its own line. A count
273
+ taken with the wrong instrument, in a release whose own subject is exactly
274
+ that.)
275
+
276
+ - **And the repair for that duplicated it, which the quality gate caught before
277
+ the merge.** The precondition was *copied* from one self-test into the other —
278
+ eleven lines — and SonarCloud failed the pull request on **3.1% duplication on
279
+ new code** against a 3% limit, over exactly that block. Copying was the wrong
280
+ half of the right idea: the answer was always one implementation used twice.
281
+ It now lives in `scripts/fixture-root.mjs` as `missingFixtureImports()`, and each
282
+ self-test keeps its own refusal wording, because the two name different builders
283
+ and exit by different routes — a difference that is real rather than incidental.
284
+ Re-proved in each: dropping `tools.mjs` from a caller's file list makes that file
285
+ refuse by name, and each names its own builder.
286
+
287
+ **Concentrating the guarantee doubled its blast radius, so it got the assertion
288
+ it never had.** One function now stands behind every self-test that uses it, so a
289
+ silent `return []` disarms all of them at once and restores the wrong-reason pass
290
+ that started this thread — a sweep that cannot start, reporting no findings. The
291
+ number of those callers is deliberately not written here or in the module: it grew
292
+ again inside this release, and the sentence that said "both" went stale unnoticed
293
+ in five places while the corrected wording lived in one. On an ordinary run they
294
+ only ever exercise the complete-fixture path, so
295
+ until now the "missing" branch was proved solely by hand-mutating a file list:
296
+ four times by two people, and never again by anything. Two cases cover both
297
+ directions on a temporary directory, and they are mutation-proved — a planted
298
+ `return []` reddens one, and ignoring the directory argument reddens both.
299
+
300
+ Worth recording as the shape rather than the incident. A missing precondition was
301
+ fixed by adding one; adding it introduced a duplicate of it; the gate caught the
302
+ duplicate. Three links, and every one of them was found by something other than
303
+ the test suite, which was green at each step. The file count in this entry moved
304
+ from twenty-eight to twenty-nine because of it, then to thirty when this release
305
+ added a source file of its own, and then to **thirty-two** as it added the generator's
306
+ self-test and the module they share. The first two were re-derived from `npm run test:tools` rather than
307
+ incremented by hand. The third was not, and the review round found it stale. The
308
+ fourth was not typed either: it was read out of `npm run test:tools`'s own output
309
+ and the edit refused to write a number the command did not report. That was a
310
+ one-off script in a scratchpad, not something this repository carries — said plainly
311
+ because an earlier draft of this very sentence claimed a committed artefact that
312
+ does not exist. This paragraph has been wrong before, and that was the first time
313
+ it invented an artefact rather than a number.
314
+
315
+ - **The leak sweep's own self-test had no precondition on the fixture it builds,
316
+ and reported a scope regression instead.** Both self-tests copy a named list of
317
+ files into a throwaway repository, and `tools.mjs` was missing from both lists.
318
+ `check-tarball.test.mjs` refused by name, which is what a precondition is for.
319
+ `check-leaks.test.mjs` had no such check: **measured**, three of its four cases
320
+ failed with messages about scope — "expected a clean exit, got 1", "the run does
321
+ not say which scope it used" — because every invocation died of
322
+ `ERR_MODULE_NOT_FOUND`, and the fourth case *passed*, since a sweep that cannot
323
+ start also cannot report a finding. That is worse than a red suite: it sends the
324
+ next reader after the ruleset instead of after a missing file. It now refuses by
325
+ name, proved by dropping the file again.
326
+
327
+ - **`npm_execpath` was trusted if it was merely absolute and ended in `.js`** —
328
+ which reversed this change's own thesis for npm. The decider had moved from
329
+ `$PATH` to an environment variable, and the local review demonstrated it by
330
+ pointing the variable at a hand-written file, which the tarball gate would then
331
+ have run as npm. The value must now also be *shaped* like npm's CLI, ending in
332
+ `node_modules/npm/bin/npm-cli.js`. What that does not claim, because a security
333
+ note that overstates its reach is worse than none: anyone who can both set your
334
+ environment and create a file at that path is still obeyed — and anyone able to
335
+ do both can usually substitute node itself. The narrowing is from "any writable
336
+ path" to "a path that looks like a real npm installation": **it stops accidents,
337
+ not an attacker**, and it is not a privilege barrier. An earlier draft said
338
+ "stray, mistaken or opportunistic", which the sentence after it contradicted — an
339
+ opportunistic value costs one `mkdir -p`.
340
+
341
+ - **`npmCliPath()` refused to find npm on a Homebrew Mac.** Its first draft knew
342
+ only the `../lib/node_modules/…` layout, which is what the GitHub-hosted runner
343
+ and nvm use — so it passed in CI and failed on a developer's machine, where
344
+ Homebrew puts npm under `../libexec/lib/node_modules/…`. Both layouts are tried
345
+ now, each from the given path and again through `realpathSync`, since a node
346
+ reached by a symlink resolves its siblings from the real location. Caught by the
347
+ new self-test on the first run, which is the argument for having written it.
348
+
349
+ - **The false-positive fix opened a worse false negative, and the second review
350
+ round caught it.** Excluding `RE.exec('git')` by putting a `(?<!\.)` lookbehind
351
+ in front of the whole alternation also excluded every *member-expression* spawn —
352
+ `child_process.execFileSync('git', …)`, `cp.execSync('git status')` — which is a
353
+ shape SonarJS does report and a contributor can write without doing anything
354
+ unusual. So a fix aimed at a shape nothing writes blinded the sweep to a shape
355
+ people do. The lookbehind now applies to the bare name `exec` alone, measured
356
+ over twelve shapes: the all-names form was wrong on four of them, this one on
357
+ none. The one trade it still makes — `obj.exec('git')`, a method named `exec` on
358
+ something that is not a regular expression — is in the limits list, because no
359
+ expression can tell it from `RE.exec('git')` without a parser. Both directions
360
+ are pinned in the preconditions now rather than left incidental, since a review
361
+ round changed this behaviour by accident once already.
362
+
363
+ Two smaller things fell out of it. The comment describing the lookbehind said
364
+ `(?!\.)` where the code says `(?<!\.)` — a look*ahead* there would match nothing
365
+ useful, so it was the one comment in the file that would mislead somebody
366
+ "fixing" the code to match it. And the worked examples added to document the
367
+ twelve shapes turned the file red, because an example of a bare call, spelled as
368
+ one, *is* a bare call as far as the sweep is concerned; they carry no quote
369
+ characters now. The sweep catching its own documentation is the least ambiguous
370
+ evidence available that the member-expression form works.
371
+
372
+ - **Two documents that enumerate the gates had gone stale in the same commit that
373
+ added one.** `CONTRIBUTING.md` said "four more checks exist, and CI runs all
374
+ four" — five now, and the new one was missing from the block a contributor is
375
+ sent to, which also made the next paragraph's "a fifth command" read as a
376
+ contradiction. The pull-request checklist named two gates as "the
377
+ public-repository gates", so a contributor following it would not have run the
378
+ sweep that exists to catch exactly the call they might be adding.
379
+
380
+ ## [1.3.15] - 2026-09-27
381
+
382
+ The first release with features in it since the bridge got code. Three
383
+ capabilities, and the documentation caught up with a service that had quietly
384
+ grown past it.
385
+
386
+ **If you only use `npx -y viafrei`, nothing changes and nothing breaks.** No flag
387
+ was removed, no default moved, no exit code changed meaning. The three additions
388
+ are opt-in or invisible.
389
+
390
+ ### Added
391
+
392
+ - **A contributor surface: `CODE_OF_CONDUCT.md`, three issue templates with a
393
+ chooser, and a pull request template.** The Code of Conduct is Contributor
394
+ Covenant 2.1, and a report with no GitHub account goes to the Impressum e-mail
395
+ rather than to the **postal** address printed there — that address is still a
396
+ visible placeholder, and a reporting route has to be one that works. The three
397
+ forms are a bug, an idea, and the one report this service cannot make about
398
+ itself: a tool answered and the answer was useless. Beside them the chooser
399
+ carries the three routes that are not forms — the private security advisory,
400
+ Discussions, and `SOURCES.md` for a data or licence question. None of these
401
+ existed in 1.3.12.
402
+ - **`VIAFREI_MCP_HEADER` sets HTTP headers from the environment.** Many MCP
403
+ clients can only give a command an `env` block, never arguments, which left
404
+ those users no way to send a header at all. It takes the same `Name: value`
405
+ syntax as `--header`, refuses the same transport-owned names, and separates
406
+ several headers with a **newline**, which can never appear in a header name or
407
+ value, so nothing you might need to send is unrepresentable - a comma, a
408
+ semicolon and a space all occur inside real header values. A `--header` of the
409
+ same name wins; one of a different name is added alongside. An error names
410
+ `VIAFREI_MCP_HEADER`, not the flag you did not type.
411
+ - **`Retry-After` is honoured on the single retry, with a cap.** Both legal forms
412
+ are read - a number of seconds and an HTTP-date - and a value that cannot be
413
+ parsed, or names a moment already past, falls back to the fixed delay instead
414
+ of throwing. **The cap is the per-request timeout**: a server answering
415
+ `Retry-After: 3600` does not make `npx viafrei` sit silently for an hour; the
416
+ retry is abandoned and the ordinary one-line failure, which already names the
417
+ URL and the status, stands. `429` is retried now that the delay it asks for is
418
+ respected - and **only when the header gives a usable delay**. A 429 carrying
419
+ nothing readable is not retried at all, because the only thing left would be the
420
+ fixed delay, and a fixed delay is what makes a rate limit worse. For the same
421
+ reason a *thrown* 429 is still not retryable: there the header is gone. A 503 in
422
+ the same state IS retried, and a test pins the difference so it cannot be
423
+ flattened by accident.
424
+ - **A pathless `--url` that gets a 404 is told where the endpoint usually is.**
425
+ `--url http://127.0.0.1:3000` now fails with `... HTTP 404 Not Found - the URL
426
+ has no path; the MCP endpoint is usually /mcp`. **It is a hint and not a
427
+ rewrite**: quietly appending the path would send the request somewhere nobody
428
+ asked for and hide a different mistake later. The hint appears only when the
429
+ path is empty or `/`, never on another status, and a URL that will not parse
430
+ skips the hint rather than making the failure message itself throw.
431
+
432
+ ### Changed
433
+
434
+ - **The README leads with how to connect, and says why to build on it.** The page
435
+ opened by explaining that you probably do not need this package, which is true
436
+ and was the first thing a developer read. The three transports now come first,
437
+ with the address to use, and a section sets out what the service is actually
438
+ offering: one protocol in place of twenty-one sources from sixteen publishers,
439
+ the licence obligations travelling with each answer, machine-readable failures,
440
+ and answers in the language of the question.
441
+ - **The tool count was wrong and is now a capability table.** The page said
442
+ thirteen tools; the server registers **eighteen**. The five geocoding tools had
443
+ shipped on the platform before the previous bridge release was even cut. The
444
+ table groups tools by the question they answer, which is stable, and the page
445
+ still refuses to paste the catalogue itself - a published tarball cannot be
446
+ corrected, so `tools/list` on the running server remains the only source of
447
+ truth.
448
+ - **The one completeness claim the new table introduced was measured before it
449
+ shipped.** The Places row says "nationwide, all sixteen Länder", which no tool
450
+ reports and which would rot at a partial import - the same objection that removed
451
+ the two address figures under **Fixed** ("Two address figures nobody could check
452
+ are gone"). It was kept because it was measured
453
+ rather than inferred, and it now carries the date: on 2026-09-27 the service's own
454
+ per-Land verdict - which reads both OSM tables, names any missing Land and exits
455
+ non-zero on one - reported all sixteen imported with every row scoped to its Land,
456
+ and two address lookups in the two Länder least likely to have been staged —
457
+ Mecklenburg-Vorpommern and Saarland, asked in Rostock and Saarbrücken — both
458
+ answered through the public endpoint with `osm` in `_meta.sources`. `SOURCES.md` carries the measurement, and one caveat that nearly
459
+ misled it: a single address that does not resolve says nothing about its Land.
460
+ - **The README now says that a slow call may be a retried call.** A `429`, `502`,
461
+ `503` or `504` is tried again **once** after the delay the server asked for, as
462
+ is a connection that failed outright; a `429` with no usable `Retry-After`, and
463
+ any delay longer than the per-request timeout, are not retried at all. The page
464
+ also now says what that timeout does and does not bound, because the first draft
465
+ of the paragraph got it wrong in the direction a user would act on: it is attached
466
+ per HTTP request, so the second attempt gets a fresh one and so does each hop of a
467
+ same-origin redirect, and **no setting here caps the whole call**. Two drafts of
468
+ that one clause were wrong, in the same direction and in the sentence a reader
469
+ would act on: the first said a retried call "can never exceed the timeout you gave
470
+ it", and the correction that replaced it said to halve the timeout for a hard
471
+ ceiling — but two attempts plus a delay that may itself be as long as the timeout
472
+ is three times it, not two, and more again if the endpoint redirects. Both were
473
+ caught in review rather than published, which is the argument for rounds that cost
474
+ nothing, and the second is the sharper lesson: a corrected number is still a
475
+ number, and nothing was reading it. That behaviour shipped earlier and the
476
+ page did not mention it, so a user watching a call take longer than expected had
477
+ nothing to read. Nothing about the behaviour changed in this release.
478
+ - **Both examples on the page are now captured output rather than plausible
479
+ output.** The departures block is a real answer from `mcp.viafrei.de` on
480
+ 2026-09-27, and the failure line is copied from a run.
481
+ - **The badge hosts are admitted to the leak ruleset as their own group, with
482
+ their one cost written down.** A shields.io badge is the only entry on that
483
+ allow-list that causes a request from somebody else's browser, so the ruleset
484
+ now says that, says the admission test, and says it is the reason to refuse the
485
+ next image host rather than to admit it by precedent. The list itself stays
486
+ sorted alphabetically rather than grouped, and the ruleset now says why: one flat
487
+ sorted list is what makes a duplicate, or a near-miss spelling of a host already
488
+ on it, visible at a glance.
489
+ - **`numbers.allowed` is back to six entries, and the two rules governing it are
490
+ separated.** A seventh was added during this release to accommodate two bare
491
+ millisecond literals, then removed once both were written `1_000`: an entry that
492
+ covers nothing in the tree is a caption pointing at a value, which is the one
493
+ thing the inverse construction exists to avoid. (The entry is not named here,
494
+ because naming it would put the bare form back in the tree and the sweep would
495
+ be right to say so — it said so about the first draft of this very bullet.) The ruleset now distinguishes **the rule** (an
496
+ entry occurs in the tree and is not an internal value — which `3600` and `9110`,
497
+ both prose in a comment, satisfy) from **the preference** (write a numeric
498
+ separator where one is available, so the literal never reaches the list at all).
499
+ Citing the second as if it were the first is what made the list look arbitrary
500
+ for one review round.
501
+ - **Two `allowedHosts` entries covering nothing are gone.** Bare `shields.io` and
502
+ bare `contributor-covenant.org` sat beside `img.shields.io` and
503
+ `www.contributor-covenant.org`, and no URL in the tree uses either; the bare
504
+ spellings occur only in prose about them, and the host extractor reads a
505
+ scheme-prefixed URL and nothing else. That is the same shape as the
506
+ allow-list number this release removed: an entry that permits nothing is a
507
+ permission nobody can audit. The history-residue block is also back to one line
508
+ per entry; reformatting it to five four-line objects changed no meaning and cost
509
+ twenty-five lines of diff in the file a reviewer reads hardest.
510
+ - **A re-wrap put a private name into a public file, and the sweep caught it —
511
+ which is also a demonstration of the one limit it declares loudest.** The gate
512
+ reads **one line at a time**, and says so in its own limits list. A paragraph
513
+ of this release's prose was re-flowed, and prose that had passed every earlier
514
+ run matched a live entry the moment the wrapping changed. Nothing was ever
515
+ published: it reached a working tree and no further. What the offending text
516
+ was is deliberately not recorded — `scripts/rules.json` says to record the
517
+ rule, the lengths and the narrowing and never the subject, because a
518
+ description narrows the candidate space for a live entry further than the hash
519
+ alone does, and this file cannot be unpublished. The lesson is the gate's, not
520
+ the prose's — a line-oriented scanner's verdict depends on where the wrapping
521
+ falls, so its PASS is about this wrapping and not about this text, and a
522
+ re-wrap is a change the sweep has to see again.
523
+
524
+ ### Fixed
525
+
526
+ - **`package-lock.json` still said 1.3.12 while `package.json` said 1.3.15, and
527
+ nothing in this repository would have noticed.** Found in the last pre-push check
528
+ of the release, by reading the two files rather than the diff: seven review rounds
529
+ had confirmed the lockfile was *unchanged*, which is true and is not the same as
530
+ correct. The publish workflow asserts that the TAG and `package.json` agree and
531
+ says so in its own step name; no gate compares the lockfile to either, and the
532
+ lockfile is not in the published tarball, so the only thing that reads it is the
533
+ `npm ci` the release build runs. It is now 1.3.15, regenerated with
534
+ `--package-lock-only` so the change is exactly the two version fields and no
535
+ dependency moved. **The missing gate is deliberately not added here**: it belongs
536
+ in `publish.yml`, and an untested edit to the workflow that publishes, inside the
537
+ commit it publishes from, is the wrong trade a day before the tag — it is filed
538
+ for the next release and rehearsed through the `workflow_dispatch` dry run, beside
539
+ the two other workflow items this release also deferred.
540
+ - **SOURCES.md described a service three capabilities smaller than the one that
541
+ is running.** Address lookup, lift and escalator status, and public-transport
542
+ realtime were all documented as not answering on the public service. All three
543
+ answer, and each was re-confirmed by a call whose result named the source. The
544
+ BKG administrative gazetteer was listed as merely licence-read and is in fact
545
+ named by answers from five different tools. Every status in the catalogue was
546
+ re-measured on 2026-09-27 by calling **fifteen of the server's sixteen
547
+ read-only tools** - every one except `find_cheapest_fuel` - once each and
548
+ reading `_meta.sources`. That one exception is **fuel**, which is deliberately not
549
+ re-measured because MTS-K sets a per-station floor and its terms make needless
550
+ querying a risk to the access itself. That row is labelled `not re-measured`
551
+ rather than dressed up as today's measurement.
552
+ - **SOURCES.md contradicted itself, and one of the contradictions was a licence
553
+ statement.** The table was re-measured; three paragraphs beneath it were not,
554
+ and they still told a reader that address lookup, facility status and
555
+ public-transport realtime do not answer. The worst of them sat in the ODbL
556
+ obligations section and said a share-alike obligation *"binds nobody using that
557
+ service right now"* - measurably false, on the one page whose job is to state
558
+ obligations, inside a tarball that cannot be corrected after publication. Every
559
+ such paragraph now states today's measurement and says which of the two
560
+ versions a reader saw. The status legend lost the value no row carries any more,
561
+ rather than leaving a status somebody could still be relying on.
562
+ - **The README promised rail disruptions "on a line or at a stop".** The server
563
+ declares the opposite in its own tool description and in `viafrei://coverage`:
564
+ it never answers whether a named line, trip or stop is on time. The row now says
565
+ region-wide and says never one line or stop.
566
+ - **Two address figures nobody could check are gone.** The Places row quoted a
567
+ count of addresses and POIs that no tool reports and that would rot at the next
568
+ import - the exact argument the paragraph below it makes against pasting a
569
+ catalogue.
570
+ - **The departures example names both of its edits.** It said the only edit was
571
+ shortening two URIs; five of the ten departures were dropped as well.
572
+ - **The OSM attribution line printed in `SOURCES.md` was not the line the server
573
+ emits.** The code fence said `Geokodierung: …`; no answer has carried that prefix
574
+ since `find_poi` and `find_address` began returning an OSM row **as** the answer
575
+ — a name, a brand, a door — rather than only a coordinate resolved from one. The
576
+ correct line is `OSM-Standortdaten: © OpenStreetMap-Mitwirkende, ODbL 1.0`, and
577
+ 1.3.12 carried it nowhere: the page was wrong about the one string it exists to
578
+ publish, inside a tarball that cannot be corrected. A draft of this release then
579
+ printed both spellings at once, which is how it was caught. Every other fenced
580
+ attribution line was swept against the server's register; this was the only
581
+ mismatch.
582
+ - **"An answer about a place does not carry the ODbL line" — three words missing,
583
+ and the sentence reverses.** That wording is this release's own: 1.3.12 carried a
584
+ different wrong version of the same sentence (see the bullet below), and this one
585
+ was written while fixing that one and caught in review. The platform's own
586
+ wording is "a result about a
587
+ place **from our own gazetteer**, a station or a motorway is not built from
588
+ OpenStreetMap". Place resolution falls through the gazetteer to `osm_pois` and
589
+ then `osm_addresses`, so **every** tool that takes a `place` can return an
590
+ OSM-derived answer: measured on 2026-09-27, a weather warning for
591
+ `Zeiss-Großplanetarium` named `["dwd","osm"]` and carried the line, and one for
592
+ `Allianz Arena` named `["osm"]` alone. Both places on this page now carry the
593
+ qualifier and state the predicate — **which table answered**, not which tool
594
+ was called. Two wrong versions of this sentence have now been caught, and both
595
+ erred towards telling a reader an obligation did not apply to them. The last
596
+ round found the same error in the two places nobody re-reads after fixing a
597
+ paragraph: the **section heading** and its opening sentence still framed the
598
+ obligation as something that applies "if you get an address or a point of
599
+ interest back", so a reader who scans headings could conclude the section was
600
+ not theirs. Heading, lead and both bodies now state the predicate, and the
601
+ back-reference to the section was moved in the same edit as the heading.
602
+ - **Twenty-one sources, not nineteen — and the table was missing a row it needs
603
+ to be the register it claims to be.** This release added the station car parks
604
+ row and recorded it in its own bullet below, without correcting the number it
605
+ invalidated; the review
606
+ then found the DELFI disruption feed has a licence row in the server's register
607
+ and no row here. It is in the table now, `read`, at **CC BY-SA 4.0**. Re-counted
608
+ by hand: 21 rows, 16 distinct publisher cells. The README's dare — "count them
609
+ in that table" — is a good one and now survives being taken up.
610
+ - **The cleared-but-unused DELFI sentence was wrong in both halves, and it is a
611
+ licence statement.** It said "two more DELFI datasets … both CC BY 4.0 — the
612
+ share-alike is on the realtime feed". There are **three**, and two of them are
613
+ share-alike: the trip updates we serve and the disruption reports we do not.
614
+ Anybody planning for the day the disruption feed appears was being told to plan
615
+ for CC BY.
616
+ - **The station car parks row had no attribution line, on a page that promises one
617
+ per source** — a defect this release created and closed. 1.3.12 has no car-parks
618
+ row anywhere, and its "three products" sentence was correct for what it listed;
619
+ adding the row, in the bullet that raised the source count, made that sentence wrong, so the Deutsche Bahn
620
+ section said "three products · CC BY 4.0" over four products and two licences. Both fixed, with the BahnPark attribution
621
+ line written out — `read`, so no answer carries it today, and it is here so
622
+ nobody has to go looking on the day one does.
623
+ - **"No tool exposes the police traffic events yet" was a claim about the code,
624
+ and the code says otherwise.** That sentence is also this release's own — 1.3.12
625
+ carries the row at `read` and says nothing about reach. The road analysis already reads that feed, and
626
+ whether a source is NAMED is gated on a live catalogue row, so the row could
627
+ start carrying its attribution line with no release at all. The page now says
628
+ what it measured — `check_road_status` on the A40, A3, A1, A57 and A46 named
629
+ only the motorway interface and the BASt roadworks feed — and says out loud
630
+ that this is a statement about answers and not about reach. The row therefore
631
+ reads `in the service` and not `read`, and the page says not to design around
632
+ it.
633
+ - **Two dead intra-document anchors, both created by this release's own
634
+ corrections.** The `no API key` badge — on the npm package page and the
635
+ repository front page — pointed at `#quick-start`, a heading this release
636
+ renamed to "Connect in one line"; and `SOURCES.md`'s back-reference to the
637
+ § 4.6 offer kept the old slug of a heading the previous review round renamed.
638
+ Every intra-document link in the tree was checked; these were the only two, and
639
+ both were ours. A later round found the shape that sweep could not see: an
640
+ issue **form** renders at `/issues/new?template=idea.yml`, so the relative
641
+ `../../blob/main/SOURCES.md` in it resolved one level short of the repository
642
+ and 404ed. It is an absolute URL now, like the one `config.yml` already used
643
+ for the same document.
644
+ - **The one place where `SOURCES.md` is now NEWER than the server's register is
645
+ named on the page.** BKG is `live` there on five measured answers, while
646
+ `viafrei://attribution` still flags it as planned — "licence read, data not
647
+ ingested yet". The page's own rule is that the resource wins and the page is
648
+ stale; that rule is right in general and wrong for this row today, so both the
649
+ rule and the row now say so. No bridge release can close it: it is tracked where
650
+ the server is developed, and what a reader of the published page can act on is
651
+ the answer itself - `_meta.sources` and the attribution lines it carries.
652
+ - **The charging answer does not count the sites without a status.** The page said
653
+ "a result says how many nearby sites had no status". It marks each one
654
+ `keine Statusdaten` and closes by saying that means unknown and not free —
655
+ measured, asking for Leipzig. What it refuses to do is leave them out or call
656
+ them free, which is the part that matters when you act on the answer.
657
+ - **The test for "which answers carry the ODbL line" was the question, and it
658
+ should have been the answer.** `SOURCES.md` said the line is carried "on every
659
+ answer whose input was an address, and on no other answer". `find_poi` asked for
660
+ a name and a city returns `_meta.sources: ["osm"]` and the line — so a reader
661
+ using that sentence to decide whether a share-alike obligation had arisen would
662
+ have concluded it had not. The rule is now stated as the platform states it, and
663
+ the page says to read `_meta.sources` on the answer rather than infer anything
664
+ from the shape of the question.
665
+ - **Our own § 4.6 offer was described more narrowly than it is.** It covers **both**
666
+ ODbL databases — addresses and points of interest, one file each under a single
667
+ licence notice — and the page named only the addresses. A recipient who derived
668
+ from POI results is entitled to the POI database.
669
+ - **The README promised car parking the public service does not answer.** Asked for
670
+ a `car_park` in Köln, Hamburg and Leipzig on 2026-09-27, every facility returned
671
+ was a lorry park or unclassified, and none carried occupancy. The row now says
672
+ lorry parking and a note says what is missing and why — the same over-claim as
673
+ the rail row, one row up, in the table this release rewrote to stop over-claiming.
674
+ - **The count of unconfirmed rows said two and there are three** — in a draft of
675
+ this release. 1.3.12 said **four**, counting a different set under a legend this
676
+ release replaced: under its own legend five rows said a source does not answer
677
+ today, and four was the number of `read` rows, which that legend defined as
678
+ "nothing uses it yet". The police-events row moved into the unconfirmed state
679
+ here, the changelog recorded the move and the count did not follow it. The station car-park source, in the server's register with no row
680
+ on the page at all, now has its `read` row.
681
+ - **The Code of Conduct sent a reporter without a GitHub account to a postal
682
+ address that does not exist** — in its first draft, in this release. The document
683
+ is new here (see `### Added`), so no published version ever sent anybody
684
+ anywhere. The Impressum's street and city are still visible placeholders; only
685
+ the e-mail address is real, and that is what the document names. Nothing linked
686
+ to it either; the README's Contributing section does.
687
+
6
688
  ## [1.3.12] - 2026-09-23
7
689
 
8
690
  The bridge's code is unchanged since 0.0.9. This release replaces 1.3.10,
@@ -266,6 +948,8 @@ for it, so the number is free; the bridge will use it when the platform does.
266
948
  commits, and a squash makes them unreachable from `main` - which would turn
267
949
  the check red on `main` for everybody, for something no contributor did.
268
950
 
951
+ [1.3.16]: https://github.com/mavrovde/viafrei-bridge/releases/tag/v1.3.16
952
+ [1.3.15]: https://github.com/mavrovde/viafrei-bridge/releases/tag/v1.3.15
269
953
  [1.3.12]: https://github.com/mavrovde/viafrei-bridge/releases/tag/v1.3.12
270
954
  [1.3.10]: https://github.com/mavrovde/viafrei-bridge/releases/tag/v1.3.10
271
955
  [0.0.9]: https://github.com/mavrovde/viafrei-bridge/releases/tag/v0.0.9