Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c400ed7908 | |||
| 79dafa38f0 | |||
| 5b1169f9a5 | |||
| 16b5e0767c | |||
| 7739c09163 | |||
| 16afcff09c | |||
| 5aca8a1fe8 | |||
| 5a6928750c | |||
| 15965e7a77 | |||
| 6a7da08002 | |||
| 9049f90cc5 | |||
| 2d86f4f0c0 | |||
| 273dd45128 |
@@ -14,6 +14,7 @@ permissions:
|
||||
jobs:
|
||||
test:
|
||||
name: vet + fmt + tests
|
||||
if: github.event_name != 'schedule'
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
@@ -22,7 +23,6 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Relevant changes
|
||||
id: changes
|
||||
if: github.event_name != 'schedule'
|
||||
run: |
|
||||
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
|
||||
git fetch --no-tags origin "$GITHUB_BASE_REF"
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
else
|
||||
base='${{ github.event.before }}'
|
||||
fi
|
||||
if git diff --quiet "$base"...HEAD -- '*.go' go.mod go.sum Dockerfile .dockerignore .github data release-tooling testdata README.md; then
|
||||
if git diff --quiet "$base"...HEAD -- '*.go' go.mod go.sum Dockerfile .dockerignore .github data release-tooling testdata vulnerability-filing README.md todo.md; then
|
||||
echo "code=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "code=true" >> "$GITHUB_OUTPUT"
|
||||
@@ -42,7 +42,6 @@ jobs:
|
||||
fi
|
||||
echo "base=$base" >> "$GITHUB_OUTPUT"
|
||||
- name: CHANGELOG entry for data changes
|
||||
if: github.event_name != 'schedule'
|
||||
run: |
|
||||
base='${{ steps.changes.outputs.base }}'
|
||||
if ! git diff --quiet "$base"...HEAD -- data testdata/shipped_shape.txt && git diff --quiet "$base"...HEAD -- CHANGELOG.md; then
|
||||
@@ -59,11 +58,6 @@ jobs:
|
||||
- name: Lowest supported Go
|
||||
if: ${{ !cancelled() && steps.changes.outputs.code == 'true' }}
|
||||
run: docker build --build-arg GO_VERSION=1.22.12 --target portable .
|
||||
# Runs on every event, and daily, since a vulnerability is published without a change
|
||||
# here; `--no-cache-filter` keeps a cached pass from hiding one.
|
||||
- name: Known vulnerabilities in this Go release and the module
|
||||
if: ${{ !cancelled() }}
|
||||
run: docker build --no-cache-filter vulnerabilities --target vulnerabilities .
|
||||
- name: Package repositories, signed with throwaway keys
|
||||
if: ${{ !cancelled() && steps.changes.outputs.packaging == 'true' }}
|
||||
run: |
|
||||
@@ -72,9 +66,44 @@ jobs:
|
||||
docker build -f release-tooling/package-repos.Dockerfile --target checks \
|
||||
--secret id=gpg,env=PACKAGES_GPG_KEY --secret id=rsa,env=PACKAGES_RSA_KEY .
|
||||
|
||||
vulnerabilities:
|
||||
name: Known vulnerabilities in this Go version and the required modules
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
actions: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Fetch govulncheck's report from vuln.go.dev
|
||||
run: docker build --no-cache-filter vulnerabilities --target vulnerability-report --output "$RUNNER_TEMP/vulnerabilities" .
|
||||
- name: File each one in todo.md
|
||||
id: file
|
||||
run: python3 vulnerability-filing/file_vulnerabilities.py "$RUNNER_TEMP/vulnerabilities/report.json" todo.md
|
||||
- name: Propose the filed items
|
||||
if: failure() && steps.file.outcome == 'failure' && github.ref == 'refs/heads/main'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
if git diff --quiet todo.md || [ "$(gh pr list --head vulnerabilities --state open --json number --jq length)" != 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
||||
git switch -C vulnerabilities
|
||||
git commit -qam "File the known vulnerabilities govulncheck reports"
|
||||
git push -qf "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" vulnerabilities
|
||||
gh pr create --head vulnerabilities --title "File the known vulnerabilities govulncheck reports" \
|
||||
--body "govulncheck reports known vulnerabilities in the Go version or a required module, and no release is cut until they are gone. This files them atop the next release in todo.md, scored 0.0 until rank_todo.py scores them; each item names the bump that fixes it."
|
||||
# A pull request the workflow's token opens starts no workflow.
|
||||
gh workflow run test.yml --ref vulnerabilities
|
||||
|
||||
release:
|
||||
name: Release from CHANGELOG.md
|
||||
needs: test
|
||||
needs: [test, vulnerabilities]
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rules
|
||||
|
||||
- Go runs only through `docker compose run --rm <test|vet|fmt|build|cyclo|generate>`; the merge gate is `docker build .`, CI's changelog check, its known-vulnerabilities check and, for a change the README's Development section lists, its package-repository checks.
|
||||
- Go runs only through `docker compose run --rm <test|vet|fmt|build|cyclo|generate|vulncheck>`; the merge gate is what the README's Development section lists.
|
||||
- Tests first, in their own commit; the implementation follows in the next. A re-pin of seeded output or of `testdata/shipped_shape.txt` is its own commit.
|
||||
- A change under `data/` or to the shape pin adds its `CHANGELOG.md` entry under `Unreleased` in the same PR, and so does a change to a flag, an exit code, an exported name, a fence, a builtin or the lowest Go; what is major is the README's Versioning table.
|
||||
- One-line commit messages: no ticket prefix, no repo name, no authorship trailers.
|
||||
@@ -124,6 +124,7 @@ In [docs/decisions.md](docs/decisions.md):
|
||||
- A country with no open address register covering it whole ships its places and postal codes without streets
|
||||
- The apt, dnf and apk repositories are static files on GitHub Pages, holding the latest release only
|
||||
- The image is pushed to GHCR and copied to Docker Hub, whose name the README shows
|
||||
- A known vulnerability in the Go version or a required module blocks a release, even in code fejkdata never calls, and blocks no merge
|
||||
|
||||
# Scoring run
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ Every shipped dataset, its source, its licence and the attribution it asks for.
|
||||
| `sv_SE/first-name.tsv`, `last-name.tsv` | [SCB](https://www.scb.se/) names with at least two bearers, 31 December 2022 | CC0 1.0 | "Källa: SCB" | `data-import/names-se.py` |
|
||||
| `en_US/first-name.tsv` | [SSA](https://www.ssa.gov/oact/babynames/) baby names, births 1930 to 2020, through [hackerb9/ssa-baby-names](https://github.com/hackerb9/ssa-baby-names) | public domain | none required | `data-import/names-us.py` |
|
||||
| `en_US/last-name.tsv` | Census Bureau surnames occurring 100 or more times, 2010 | public domain | none required | `data-import/names-us.py` |
|
||||
| `sv_SE/sex.tsv`, `sv_SE/birth-number.tsv`, `sv_SE/title.tsv`, `en_US/sex.tsv`, `en_US/title.tsv`, `misc/car.tsv` | curated; Skatteverket's test birth numbers are facts, and `car.tsv` waits for an international source (goal 14) | — | — | — |
|
||||
| `sv_SE/sex.tsv`, `sv_SE/birth-number.tsv`, `sv_SE/title.tsv`, `en_US/sex.tsv`, `en_US/title.tsv`, `misc/car.tsv` | curated; Skatteverket's test birth numbers are facts, and `car.tsv` waits for an international source (goal 13) | — | — | — |
|
||||
| `misc/currency.tsv` | [datasets/currency-codes](https://github.com/datasets/currency-codes); symbols from [Unicode CLDR](https://github.com/unicode-org/cldr) `en.xml` and `root.xml` | PDDL 1.0; [Unicode License v3](https://www.unicode.org/license.txt) | CLDR: "Copyright © 1991-2025 Unicode, Inc. Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the United States and other countries." | `data-import/currency.py` |
|
||||
| `misc/httpmethod.tsv` | [IANA HTTP Method Registry](https://www.iana.org/assignments/http-methods/) | [public domain](https://www.iana.org/help/licensing-terms) | none required | `data-import/httpmethod.py` |
|
||||
| `misc/httpstatus.tsv` | [IANA HTTP Status Code Registry](https://www.iana.org/assignments/http-status-codes/) | [public domain](https://www.iana.org/help/licensing-terms) | none required | `data-import/httpstatus.py` |
|
||||
|
||||
+11
-1
@@ -14,12 +14,21 @@ RUN go vet ./... && \
|
||||
FROM golang:${GO_VERSION} AS vulnerabilities
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 -scan module
|
||||
RUN go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 -scan module -format json > /report.json
|
||||
|
||||
FROM scratch AS vulnerability-report
|
||||
COPY --from=vulnerabilities /report.json /
|
||||
|
||||
FROM python:3.14.8-slim AS release-tooling
|
||||
COPY release-tooling/ /release-tooling/
|
||||
RUN python3 -m unittest discover -s /release-tooling -p '*_test.py' && touch /tested
|
||||
|
||||
FROM python:3.14.8-slim AS vulnerability-filing
|
||||
COPY vulnerability-filing/ /vulnerability-filing/
|
||||
# The filer's test files an item into the plan itself.
|
||||
COPY todo.md /todo.md
|
||||
RUN python3 -m unittest discover -s /vulnerability-filing -p '*_test.py' && touch /tested
|
||||
|
||||
FROM goreleaser/goreleaser:v2.18.3 AS packaging
|
||||
WORKDIR /check
|
||||
COPY release-tooling/goreleaser.yaml ./
|
||||
@@ -28,6 +37,7 @@ RUN git init -q && git remote add origin https://github.com/larvit/fejkdata.git
|
||||
FROM portable
|
||||
# BuildKit skips a stage nothing copies from.
|
||||
COPY --from=release-tooling /tested /tmp/release-tooling-tested
|
||||
COPY --from=vulnerability-filing /tested /tmp/vulnerability-filing-tested
|
||||
COPY --from=packaging /tested /tmp/packaging-tested
|
||||
RUN unformatted="$(gofmt -l .)"; test -z "$unformatted" || \
|
||||
{ echo "unformatted files:"; echo "$unformatted"; exit 1; }
|
||||
|
||||
@@ -417,9 +417,9 @@ carry sub-fields — `misc.currency.symbol`, `misc.territory.alpha2`,
|
||||
|
||||
Where a script under [`data-import/`](data-import) reads a source, the facts come from
|
||||
that source, and the script drops a row only by a rule it states; elsewhere a small hand-written
|
||||
set ships, and [`todo.md`](todo.md) carries the step that replaces it (goal 14). Copy
|
||||
set ships, and [`todo.md`](todo.md) carries the step that replaces it (goal 13). Copy
|
||||
that states no fact, such as `word` and `sentence`, stays hand-written. Where a category
|
||||
has no path that draws only values reaching nothing real (goal 16), write a fixed value such
|
||||
has no path that draws only values reaching nothing real (goal 15), write a fixed value such
|
||||
as `example.com` into the template; `todo.md` plans the path.
|
||||
|
||||
`misc.httpmethod`, `misc.protocol` and `misc.port` are IANA's registries.
|
||||
@@ -980,7 +980,7 @@ advertises no such path; read that level whole.
|
||||
|
||||
### Performance
|
||||
|
||||
The target is about a microsecond per value (goal 13).
|
||||
The target is about a microsecond per value (goal 12).
|
||||
|
||||
A source with an [index](#data), as the shipped set has, loads nothing in `New`: a
|
||||
category loads on the first `Fake`, `FakeRecord`, `FakeStruct` or template that reads
|
||||
@@ -1053,6 +1053,7 @@ Where the project is heading; the sections before Audience document what ships t
|
||||
1. **The best fake-data tool there is, and the one developers reach for.**
|
||||
1. You can install it the way you install your other tools, and people hear about
|
||||
it where developers read.
|
||||
2. **Great documentation.**
|
||||
2. **Enough data ships built in to fill a real test database: the CLI carries all of it, a
|
||||
library imports what it needs, and nothing is fetched at run time.**
|
||||
1. Each locale covers the common categories: names, addresses, phone numbers, ids,
|
||||
@@ -1117,16 +1118,12 @@ Where the project is heading; the sections before Audience document what ships t
|
||||
1. In the library, the caller supplies everything that can change the output: data,
|
||||
seed and date.
|
||||
11. **No unnecessary runtime dependencies** — use standard library by default.
|
||||
12. **The docs are a map of the template language.**
|
||||
1. Every feature has its own heading.
|
||||
2. Every example runs as a test and shows its output.
|
||||
3. Each rule is stated in one place.
|
||||
13. **It is fast enough that you never notice it with the shipped data.**
|
||||
12. **It is fast enough that you never notice it with the shipped data.**
|
||||
1. A render's cost grows only with the data it reaches.
|
||||
14. **Strongly prioritize data sourced from good online sources**
|
||||
15. **The most common data comes first,** such as names, addresses, phone numbers and
|
||||
13. **Strongly prioritize data sourced from good online sources**
|
||||
14. **The most common data comes first,** such as names, addresses, phone numbers and
|
||||
dates.
|
||||
16. **Values look real by default. You can also ask for values that can never reach
|
||||
15. **Values look real by default. You can also ask for values that can never reach
|
||||
anything real,** such as a domain nobody can register.
|
||||
|
||||
## Layout
|
||||
@@ -1172,6 +1169,7 @@ data-import/ the scripts that rebuild each sourced table (see DATA-LICENSES.m
|
||||
docs/ the decision log, the register research behind the shipped data, the survey of other libraries, panels' scores and rulings on how readable the code is, and what each chunk cost an agent to read
|
||||
release-tooling/ the release CI publishes from the changelog heading, and the binaries, packages, image and package repositories built for it
|
||||
testdata/ the pinned shipped shape (see Versioning)
|
||||
vulnerability-filing/ the script CI runs to file each known vulnerability govulncheck reports as a `todo.md` item
|
||||
```
|
||||
|
||||
## Development
|
||||
@@ -1207,26 +1205,33 @@ docker compose run --rm --user root --entrypoint chown test -R "$(id -u):$(id -g
|
||||
```
|
||||
|
||||
Every pull request runs `docker build .` against both the latest and the lowest
|
||||
supported Go, and must pass before it can be merged — unless it changes none of
|
||||
the files the build and its tests read, nor the workflow itself, in which case
|
||||
it's skipped. That build runs vet, complexity, the format check, the Go tests and the
|
||||
release tooling's tests; the rest of the gate is the changelog check, which CI runs
|
||||
against the PR base, and the package-repository checks below. Run `docker build .`
|
||||
locally before pushing. The lowest Go builds the `portable` stage, which skips the format
|
||||
check and the release tooling's tests:
|
||||
supported Go, and must pass before it can be merged — unless it changes none of the
|
||||
files the build and its tests read, nor the workflow itself, in which case it's skipped.
|
||||
That build runs vet, complexity, the format check, the Go tests, the release tooling's
|
||||
tests and the vulnerability filer's tests, which file an item into `todo.md` itself, so
|
||||
a change to the plan's shape can fail it; the rest of the gate is the changelog check,
|
||||
which CI runs against the PR base, and the package-repository checks below. Run `docker
|
||||
build .` locally before pushing. The lowest Go builds the `portable` stage, which skips
|
||||
the format check and both Python test suites:
|
||||
|
||||
```sh
|
||||
docker build . # latest
|
||||
docker build --build-arg GO_VERSION=1.22.12 --target portable . # lowest supported
|
||||
GO_VERSION=1.22.12 docker compose run --rm test # the same tests, without the image build
|
||||
docker build --no-cache-filter vulnerabilities --target vulnerabilities . # known vulnerabilities
|
||||
```
|
||||
|
||||
The last line runs `govulncheck` over the Go release the gate builds with and the module's
|
||||
requirements, and fails on any known vulnerability, whether or not fejkdata calls it: the
|
||||
release binaries carry that Go release, and image scanners flag it. CI runs it on every pull
|
||||
request and push, and daily, and a release waits for it. A failure is fixed by raising
|
||||
`GO_VERSION`, which Renovate opens a pull request for.
|
||||
A known vulnerability in the Go version the gate builds with, or in a module fejkdata
|
||||
requires, holds the release and no merge. CI checks on every pull request, every push to
|
||||
`main`, and daily. On `main` and in the daily run, CI files each vulnerability as a
|
||||
`security` item atop the next release in `todo.md`, through a pull request from the branch
|
||||
`vulnerabilities` unless one is open; each item names the version to raise to. When the
|
||||
step "Fetch govulncheck's report from vuln.go.dev" fails, vuln.go.dev did not answer:
|
||||
rerun it. When GitHub pauses the daily run after 60 days with no activity, re-enable it
|
||||
under Actions. The same check:
|
||||
|
||||
```sh
|
||||
docker compose run --rm vulncheck
|
||||
```
|
||||
|
||||
A change to Go code, `release-tooling/`, the `Dockerfile`, `.dockerignore`, the licence
|
||||
files or either workflow also runs the package-repository checks. They build the release
|
||||
@@ -1305,6 +1310,21 @@ replaces what it uploaded before. For a tag older than the latest release it lea
|
||||
tap, the bucket, the image's `latest` tags and the repositories alone, since each holds one
|
||||
version.
|
||||
|
||||
### Documentation
|
||||
|
||||
How the README serves goal 1.2:
|
||||
|
||||
- The top shows the CLI and the library side by side, each a short quickstart rendering
|
||||
the same value.
|
||||
- A table of the shipped data follows: every path, an example value, and what makes it
|
||||
valid or where it comes from.
|
||||
- Recipes show common tasks end to end.
|
||||
- Every feature has its own heading, and the docs are a map of the template language.
|
||||
- Every example runs as a test and shows its output.
|
||||
- Each rule is stated in one place.
|
||||
- Edge cases and a category's caveats sit in the reference sections, below what a first
|
||||
reader needs.
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE). Forked from [github.com/Timewave-AB/fakes](https://github.com/Timewave-AB/fakes).
|
||||
|
||||
@@ -35,6 +35,10 @@ services:
|
||||
<<: *go
|
||||
command: go run github.com/fzipp/gocyclo/cmd/gocyclo@v0.6.0 -over 14 -ignore _test .
|
||||
|
||||
vulncheck:
|
||||
<<: *go
|
||||
command: go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 -scan module
|
||||
|
||||
build:
|
||||
<<: *go
|
||||
command: go build ./...
|
||||
|
||||
@@ -682,3 +682,41 @@ Ruling: same
|
||||
|---|---|---|---|---|---|
|
||||
| Mid A | same | same | same | same | same |
|
||||
| Inherited architect | same | same | same | same | same |
|
||||
|
||||
## 2026-10-14T11:34:34Z, PR #225 at f734c9e, against f90b4d6
|
||||
|
||||
Ruling: worse
|
||||
|
||||
| Seat | Navigation | Locality | Shape | Self-sufficiency | Overall |
|
||||
|---|---|---|---|---|---|
|
||||
| Mid A | same | worse | same | same | same |
|
||||
| Inherited architect | same | worse | same | same | worse |
|
||||
| Maintainability senior | same | worse | same | worse | worse |
|
||||
| Junior A | same | worse | same | worse | worse |
|
||||
|
||||
Mid A, decided by:
|
||||
|
||||
- Locality: `Dockerfile:26` `vulnerability-filing`: the filer's test reads the real todo.md inside docker build ., so a plan edit can turn the Go gate red
|
||||
- Locality: `vulnerability-filing/file_vulnerabilities.py:24` `ROW`: the filer splices todo.md as text, assuming its row shape, the Next ID marker and the Details heading
|
||||
- Locality: `.github/workflows/test.yml:106` `release`: a release now also waits on vuln.go.dev's answer, visible only in a needs list
|
||||
|
||||
Inherited architect, decided by:
|
||||
|
||||
- Locality, Overall: `Dockerfile:26` `vulnerability-filing`: the plan is copied into the merge-gate build, so a planning edit that changes the table's shape fails docker build .
|
||||
- Locality: `vulnerability-filing/file_vulnerabilities.py:101` `file_items`: edits todo.md as text; a missing Next ID crashes with exit 1, which reads like a vulnerability already planned
|
||||
- Locality: `.github/workflows/test.yml:69` `vulnerabilities`: whether a release happens depends on vuln.go.dev that day, and every PR shows a red job while one is known
|
||||
|
||||
Maintainability senior, decided by:
|
||||
|
||||
- Locality: `vulnerability-filing/file_vulnerabilities.py:101` `file_items`: finds the release by an unnamed regex group, never checks the Next ID match, and splices Details by a replace that does nothing when the heading is missing
|
||||
- Locality, Self-sufficiency: `.github/workflows/test.yml:83` `File each one in todo.md`: one exit code holds the release and triggers the proposal, which must tell filed, planned and crashed apart by a git diff
|
||||
- Locality: `Dockerfile:26` `vulnerability-filing`: copies todo.md into the gate build, and the path filter lists todo.md
|
||||
- Self-sufficiency: `README.md:1213` "Development": says the build runs the release tooling's tests and leaves out the filer's
|
||||
- Self-sufficiency: `vulnerability-filing/file_vulnerabilities.py:2` module docstring: builds on rank_todo.py, which the repository does not hold
|
||||
|
||||
Junior A, decided by:
|
||||
|
||||
- Locality, Overall: `Dockerfile:26` `vulnerability-filing`: the Go merge gate copies todo.md and files an item into it, so editing the plan can turn the build red
|
||||
- Locality: `.github/workflows/test.yml:106` `release`: whether a push to main releases also depends on vuln.go.dev's answer that day
|
||||
- Self-sufficiency: `vulnerability-filing/file_vulnerabilities.py:24` `ROW`: the regex and the Details splice encode todo.md's layout, which the script never shows
|
||||
- Self-sufficiency: `README.md:1213` "Development": the gate paragraph leaves out the filer's tests while AGENTS.md now defers to it
|
||||
|
||||
+29
-8
@@ -171,7 +171,7 @@ so no path repeats it. Valid until a locale's lowered name clashes with another'
|
||||
|
||||
2026-10-03, a restructure the maintainer approved; widened to any source on 2026-10-09.
|
||||
|
||||
Goal 13: `New` pays nothing for a category a run never reaches. A manifest,
|
||||
Goal 12: `New` pays nothing for a category a run never reaches. A manifest,
|
||||
`.fejkdata.json` at a source's root, may carry an index: each category's parent table and
|
||||
the paths `List` advertises below it. A category of an indexed source loads with every
|
||||
category it references and its whole table family, and binds as a whole load does, so
|
||||
@@ -446,7 +446,7 @@ cost.
|
||||
|
||||
## The performance gate asserts allocations, not wall-clock time
|
||||
|
||||
2026-09-03, Lilleman auf Larv. Goal 13.
|
||||
2026-09-03, Lilleman auf Larv. Goal 12.
|
||||
|
||||
`AllocsPerRun` is deterministic across machines, so a 10% ceiling does not flake under
|
||||
CI load, while time varies with the machine and its neighbours. A rendering slowdown
|
||||
@@ -459,7 +459,7 @@ heap through `pickKey.under`, holds at its baseline with no margin: from a basel
|
||||
|
||||
## Rows live in a TSV, the shape in JSON
|
||||
|
||||
2026-09-17, Lilleman auf Larv. Goals 8 and 13.
|
||||
2026-09-17, Lilleman auf Larv. Goals 8 and 12.
|
||||
|
||||
`New` allocates once per node, so a register of thirty thousand rows written as JSON
|
||||
objects would cost it a second; a TSV is one allocation whose cells are substrings, and
|
||||
@@ -579,7 +579,7 @@ generators of their own.
|
||||
|
||||
2026-09-17, Lilleman auf Larv.
|
||||
|
||||
Goals 10 and 13: a path that fails moves no seeded stream, at the cost of one draw-free
|
||||
Goals 10 and 12: a path that fails moves no seeded stream, at the cost of one draw-free
|
||||
walk per call, compiling its steps into a stack buffer of 16. Valid while that walk
|
||||
costs little against the draw it guards.
|
||||
|
||||
@@ -607,7 +607,7 @@ them; a data package imports only the packages it reads.
|
||||
## The default embed holds every Swedish postort the import can place, with all its streets, and, of the US places that hold most of a postal code's land, every state capital, every place of 100,000 or more and a population-weighted tenth, each with a quarter of its streets, at least ten
|
||||
|
||||
2026-10-11, Lilleman auf Larv; revised the same day, after a tenth of the streets gave
|
||||
Göteborg 163 and Stockholm 37. Goals 2 and 14: ten streets per place put 500 Stockholm
|
||||
Göteborg 163 and Stockholm 37. Goals 2 and 13: ten streets per place put 500 Stockholm
|
||||
customers on ten streets, and US places of 25,000 or more left out every town and
|
||||
village. `geo-us.py` draws with Efraimidis and Spirakis's weighted sampling. It draws
|
||||
only among places holding most of some ZCTA's land, since no other place can carry a
|
||||
@@ -620,7 +620,7 @@ else by a name no other place in its state carries. No census-designated place s
|
||||
so Honolulu is the one capital missing. Every Swedish street is 174k rows; a quarter
|
||||
of the US streets 475k. On 2026-10-11 the embed grew from 5.2 MB to 16 MB, and a CLI
|
||||
run printing the first `en_US.address` took 145 ms, `sv_SE.address` 95 ms and
|
||||
`misc.territory` 3 ms, within goal 13. `--place-share`, `--street-share` and
|
||||
`misc.territory` 3 ms, within goal 12. `--place-share`, `--street-share` and
|
||||
`--min-streets` on the import scripts build another set. Valid while the first reach of
|
||||
a country stays unnoticed beside a test run.
|
||||
|
||||
@@ -748,7 +748,7 @@ frequency it is read, as `misc.timezone` reads GeoNames populations and
|
||||
2026-09-24, Lilleman auf Larv.
|
||||
|
||||
The weight is the population GeoNames records in the zone's cities of 15,000 or more,
|
||||
floored at 15,000, which goal 16 asks for: over 300 seeded draws of
|
||||
floored at 15,000, which goal 15 asks for: over 300 seeded draws of
|
||||
`misc.territory[US].timezone`, the four zones most Americans live in took 278 where an
|
||||
even weight gave them 44, and `America/Indiana/Petersburg`, a town of 2,400, fell from 17
|
||||
to 0. Valid while a draw weighted this way lands where people live.
|
||||
@@ -855,7 +855,7 @@ the direct chain is the one a reader can predict from the tables' parents.
|
||||
|
||||
2026-09-30, larv-review on the comprehension round the maintainer approved on 2026-09-29;
|
||||
replaces "The path walks are separate loops".
|
||||
Goals 3 and 13: `drawSteps` draws every path from its steps, compiled when templates resolve, by
|
||||
Goals 3 and 12: `drawSteps` draws every path from its steps, compiled when templates resolve, by
|
||||
`compiledPath` or, for a caller's path, by `callerPathSteps` into a stack buffer. Both take each
|
||||
step through `compileStep`, and differ only at a choice: `compiledPath` walks every variant,
|
||||
`callerPathSteps` the first. On 2026-09-29 one loop over all three walks, switching on a mode field,
|
||||
@@ -989,3 +989,24 @@ Hub, so a Docker Hub failure holds up no other channel. GHCR also serves CI on s
|
||||
runners, which Docker Hub's per-IP limit on anonymous pulls blocks. Leaving Docker Hub
|
||||
costs each user an edit of every `docker run` line and pinned script that names the bare
|
||||
`larvit/fejkdata`.
|
||||
|
||||
## A known vulnerability in the Go version or a required module blocks a release, even in code fejkdata never calls, and blocks no merge
|
||||
|
||||
2026-10-14, decided by the maintainer after Docker Hub flagged v0.1.0's image. Serves goal
|
||||
1.1. `govulncheck -scan module` fails on any known vulnerability in the Go version the gate
|
||||
builds with or in a module fejkdata requires, even one fejkdata never calls: the release
|
||||
binaries carry that Go version, and image scanners flag it whatever the code reaches. It
|
||||
runs on every pull request and push and daily, as a job the release job needs but branch
|
||||
protection does not require. So a vulnerability published in Go, or an outage of
|
||||
vuln.go.dev, holds the release and leaves unrelated pull requests mergeable. Each
|
||||
vulnerability is filed as a `security` item atop the next release in `todo.md`, by a pull
|
||||
request CI opens, so the plan names it before anyone reads a red run.
|
||||
`vulnerability-filing/file_vulnerabilities.py` writes that item in `todo.md`'s shape with a
|
||||
Score of 0.0, since CI cannot reach `rank_todo.py`, which scores it on the next commit to the
|
||||
plan; a test files one into the repository's own `todo.md`. When the next release has no item left and waits to be cut,
|
||||
`todo.md` names no version for it, so the item lands in the release after; the maintainer
|
||||
moves it. Where no Go version fixes a vulnerability yet, govulncheck has no ignore list: the
|
||||
maintainer drops the job from the release's `needs` until one does, and records why here.
|
||||
The paired ruling against f90b4d6 found the project reads worse for it: the gate now reads
|
||||
`todo.md`, and a release waits on vuln.go.dev. The maintainer merged it anyway, as the cost
|
||||
of filing into the plan. Valid while the binaries are built from the gate's Go version.
|
||||
|
||||
@@ -90,7 +90,7 @@ Recommendation: embed SCB 2022 xlsx-derived lists (top-N per sheet with counts)
|
||||
## 3. Phone
|
||||
|
||||
- Source: PTS "The Swedish numbering plan for telephony according to ITU-T E.164", 2024-01-08 (verified, text extracted): https://pts.se/globalassets/globala-block/nummertillstand/the-swedish-numbering-plan-for-telephony-according-to-itu---2024-01-08.pdf. Swedish "nrplansammanstallning-2026-02-04.pdf" and https://nummer.pts.se/NbrPlanSearch are behind a Radware bot check (both curl and Playwright blocked). No licence stated by PTS anywhere found; pts.se says contact pts@pts.se for data.
|
||||
- Reserved for fiction, PTS "Telefonnummer till böcker och filmer" (verified 2026-09-21; curl and Playwright are Radware-blocked, a rendering fetcher gets through): https://www.pts.se/internet-och-telefoni/telefonnummer-och-adressering/telefonnummer-till-bocker-och-filmer/ — mobile 070-1740605–1740699, geographic 08-46500400–46500499 (Stockholm), 031-3900600–3900699 (Göteborg), 040-6280400–6280499 (Malmö), 0980-319200–319299 (Kiruna). 495 numbers PTS holds back so they reach nobody, which is goal 16's never-reaching path for `sv_SE.phone`. Same licence status as the numbering plan: none stated, facts only, cite PTS. The mobile block fits the shipped `{a} {b} {c}` grouping (`174 06 05`–`99`), as do 031 and 040; Stockholm's 8 subscriber digits and Kiruna's 6 need a format of their own, and 0980 is not among the 20 shipped area codes.
|
||||
- Reserved for fiction, PTS "Telefonnummer till böcker och filmer" (verified 2026-09-21; curl and Playwright are Radware-blocked, a rendering fetcher gets through): https://www.pts.se/internet-och-telefoni/telefonnummer-och-adressering/telefonnummer-till-bocker-och-filmer/ — mobile 070-1740605–1740699, geographic 08-46500400–46500499 (Stockholm), 031-3900600–3900699 (Göteborg), 040-6280400–6280499 (Malmö), 0980-319200–319299 (Kiruna). 495 numbers PTS holds back so they reach nobody, which is goal 15's never-reaching path for `sv_SE.phone`. Same licence status as the numbering plan: none stated, facts only, cite PTS. The mobile block fits the shipped `{a} {b} {c}` grouping (`174 06 05`–`99`), as do 031 and 040; Stockholm's 8 subscriber digits and Kiruna's 6 need a format of their own, and 0980 is not among the 20 shipped area codes.
|
||||
- 264 geographic area codes (PDF text yields 263 "Area code for" rows; one likely split across a page break — flag). Area code = trunk "0" + NDC of 1–3 digits (08 Stockholm, 031 Göteborg, 040 Malmö, 0480 Kalmar). Geographic N(S)N (NDC + subscriber) max 9 digits, min 7 (2-digit NDC) or 8 (3-digit NDC); Stockholm min 7. Each row in the PDF gives NDC, max/min length, "Area code for <name>", so a `{ndc, name}` list can be built from it.
|
||||
- Mobile: NDC 70, 72, 73, 76, 79 — N(S)N exactly 9 digits, i.e. 07X-XXX XX XX (10 digits with trunk 0). Others: 71 mobile broadband/M2M (13 digits), 74 paging, 75 personal numbering, 77 shared cost, 20 freephone, 10 location-independent (10 AXX XX XX, A=1–8), 378 M2M fixed (10 digits).
|
||||
- Formatting (Språkrådet/Isof frågelådan, via search excerpts): hyphen after area code, subscriber digits grouped 2–3 from the left: `08-668 01 50`, `031-123 45 67`, `0480-123 45`, `070-123 45 67` (also `0701-23 45 67` accepted). International: `+46 70 123 45 67` (drop trunk 0).
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
import file_vulnerabilities
|
||||
|
||||
TODO = """# todo
|
||||
|
||||
## Scoring
|
||||
|
||||
`Score = -R - S/4 + 2*A + 2*G*W`
|
||||
|
||||
`Bar = 9`
|
||||
|
||||
`Next ID = 7`
|
||||
|
||||
| Goal | W |
|
||||
|---|---|
|
||||
| 1 | 1.00 |
|
||||
| 1.1 | 0.90 |
|
||||
|
||||
## Items
|
||||
|
||||
| ID | Release | Exempt | Item | R | S | C | A | G | Goals | Score |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 5 | 0.2.0 | | **Add a thing.** | 2 | 2 | 2 | 7 | 8 | 1 | 26.5 |
|
||||
|
||||
## Details
|
||||
|
||||
### 5. Add a thing.
|
||||
|
||||
Why.
|
||||
"""
|
||||
|
||||
|
||||
def report(*findings, osvs=()):
|
||||
messages = [{"config": {"go_version": "go1.27.1", "scan_level": "module"}}]
|
||||
messages += [{"osv": osv} for osv in osvs]
|
||||
messages += [
|
||||
{"finding": {"osv": osv, "fixed_version": fixed, "trace": [{"module": module, "version": version}]}}
|
||||
for osv, module, version, fixed in findings
|
||||
]
|
||||
return "\n".join(json.dumps(m, indent=2) for m in messages)
|
||||
|
||||
|
||||
OSVS = (
|
||||
{"id": "GO-2026-0002", "summary": "Denial of service in net/http", "aliases": ["CVE-2026-2"]},
|
||||
{"id": "GO-2026-0001", "summary": "Cross-site scripting in html/template", "aliases": ["CVE-2026-1"]},
|
||||
{"id": "GO-2026-0003", "summary": "Panic in x/text", "aliases": []},
|
||||
)
|
||||
STDLIB = (
|
||||
("GO-2026-0002", "stdlib", "v1.27.1", "v1.27.2"),
|
||||
("GO-2026-0001", "stdlib", "v1.27.1", "v1.27.10"),
|
||||
("GO-2026-0001", "stdlib", "v1.27.1", "v1.27.10"),
|
||||
)
|
||||
|
||||
|
||||
class FileItems(unittest.TestCase):
|
||||
def test_no_finding_files_nothing(self):
|
||||
self.assertEqual(file_vulnerabilities.file_items(report(osvs=OSVS), TODO), (TODO, 0))
|
||||
|
||||
def test_files_one_security_item_per_module_at_the_top_of_the_next_release(self):
|
||||
todo, found = file_vulnerabilities.file_items(report(*STDLIB, osvs=OSVS), TODO)
|
||||
self.assertEqual(found, 2)
|
||||
self.assertIn("`Next ID = 8`", todo)
|
||||
rows = [line for line in todo.splitlines() if line.startswith("| ") and line[2].isdigit()]
|
||||
self.assertEqual(
|
||||
rows[0],
|
||||
"| 7 | 0.2.0 | security | **Raise Go to 1.27.10, so the release binaries carry none of"
|
||||
" its known vulnerabilities.** | 1 | 1 | 1 | 8 | 8 | 1.1 | 29.1 |",
|
||||
)
|
||||
self.assertTrue(rows[1].startswith("| 5 |"))
|
||||
detail = todo.split("## Details\n\n", 1)[1]
|
||||
self.assertTrue(
|
||||
detail.startswith(
|
||||
"### 7. Raise Go to 1.27.10, so the release binaries carry none of its known vulnerabilities.\n\n"
|
||||
"govulncheck found these in Go 1.27.1:\n\n"
|
||||
"- GO-2026-0001 (CVE-2026-1): Cross-site scripting in html/template\n"
|
||||
"- GO-2026-0002 (CVE-2026-2): Denial of service in net/http\n\n"
|
||||
),
|
||||
detail,
|
||||
)
|
||||
self.assertIn("### 5. Add a thing.", detail)
|
||||
|
||||
def test_a_module_requirement_gets_its_own_item(self):
|
||||
findings = STDLIB + (("GO-2026-0003", "golang.org/x/text", "v0.3.0", "v0.3.8"),)
|
||||
todo, found = file_vulnerabilities.file_items(report(*findings, osvs=OSVS), TODO)
|
||||
self.assertEqual(found, 3)
|
||||
self.assertIn("| 8 | 0.2.0 | security | **Raise `golang.org/x/text` to v0.3.8, so the module requires none of its known vulnerabilities.** |", todo)
|
||||
self.assertIn("- GO-2026-0003: Panic in x/text\n", todo)
|
||||
self.assertIn("`Next ID = 9`", todo)
|
||||
|
||||
def test_a_vulnerability_already_planned_is_not_filed_again(self):
|
||||
filed, _ = file_vulnerabilities.file_items(report(*STDLIB, osvs=OSVS), TODO)
|
||||
self.assertEqual(file_vulnerabilities.file_items(report(*STDLIB, osvs=OSVS), filed), (filed, 2))
|
||||
|
||||
def test_a_plan_with_no_items_is_refused(self):
|
||||
empty = TODO.split("| 5 |")[0].rstrip("\n") + "\n\n## Details\n"
|
||||
with self.assertRaises(SystemExit):
|
||||
file_vulnerabilities.file_items(report(*STDLIB, osvs=OSVS), empty)
|
||||
|
||||
|
||||
class Main(unittest.TestCase):
|
||||
def setUp(self):
|
||||
cwd = os.getcwd()
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmp.cleanup)
|
||||
os.chdir(tmp.name)
|
||||
self.addCleanup(os.chdir, cwd)
|
||||
with open("todo.md", "w", encoding="utf-8") as f:
|
||||
f.write(TODO)
|
||||
|
||||
def run_main(self, text):
|
||||
with open("report.json", "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
code = file_vulnerabilities.main(["report.json", "todo.md"])
|
||||
with open("todo.md", encoding="utf-8") as f:
|
||||
return code, out.getvalue(), f.read()
|
||||
|
||||
def test_a_clean_report_passes_and_leaves_the_plan(self):
|
||||
self.assertEqual(self.run_main(report(osvs=OSVS)), (0, "No known vulnerabilities.\n", TODO))
|
||||
|
||||
def test_a_finding_fails_and_files_it(self):
|
||||
code, out, todo = self.run_main(report(*STDLIB, osvs=OSVS))
|
||||
self.assertEqual(code, 1)
|
||||
self.assertEqual(out, "2 known vulnerabilities; todo.md item 7 plans the fix.\n")
|
||||
self.assertIn("| 7 | 0.2.0 | security |", todo)
|
||||
|
||||
def test_a_finding_already_planned_still_fails(self):
|
||||
self.run_main(report(*STDLIB, osvs=OSVS))
|
||||
code, out, _ = self.run_main(report(*STDLIB, osvs=OSVS))
|
||||
self.assertEqual(code, 1)
|
||||
self.assertEqual(out, "2 known vulnerabilities, already planned in todo.md.\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -23,6 +23,18 @@
|
||||
"matchStrings": [
|
||||
"goreleaser/goreleaser-action@\\S+\\s+with:\\s+version: (?<currentValue>\\S+)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"datasourceTemplate": "go",
|
||||
"description": "Go tools the Dockerfile and compose.yaml run with go run, by their module path",
|
||||
"managerFilePatterns": [
|
||||
"/^Dockerfile$/",
|
||||
"/^compose\\.yaml$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"go run (?<depName>[^@\\s]+?)(/cmd/[^@\\s]+)?@(?<currentValue>v\\S+)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"extends": [
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
|
||||
`Bar = 9`
|
||||
|
||||
`Next ID = 253`
|
||||
`Next ID = 262`
|
||||
|
||||
| Goal | W |
|
||||
|---|---|
|
||||
| 1 | 1.00 |
|
||||
| 1.1 | 1.00 |
|
||||
| 1.2 | 1.00 |
|
||||
| 2 | 0.95 |
|
||||
| 2.1 | 0.95 |
|
||||
| 2.2 | 0.95 |
|
||||
@@ -53,23 +54,22 @@
|
||||
| 11 | 0.50 |
|
||||
| 12 | 0.45 |
|
||||
| 12.1 | 0.45 |
|
||||
| 12.2 | 0.45 |
|
||||
| 12.3 | 0.45 |
|
||||
| 13 | 0.40 |
|
||||
| 13.1 | 0.40 |
|
||||
| 14 | 0.35 |
|
||||
| 15 | 0.30 |
|
||||
| 16 | 0.25 |
|
||||
|
||||
## Items
|
||||
|
||||
| ID | Release | Exempt | Item | R | S | C | A | G | Goals | Score |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 259 | 0.2.0 | | **Follow the quickstarts with a table of every shipped path, its example value and what makes it valid or where it comes from, generated from the data and checked by a test.** | 2 | 4 | 4 | 8 | 8 | 1.2, 14 | 29.0 |
|
||||
| 258 | 0.2.0 | | **Open the README with a CLI quickstart and a Go quickstart side by side, rendering the same value.** | 1 | 1 | 2 | 7 | 7 | 1.2 | 26.8 |
|
||||
| 243 | 0.2.0 | | **Add `en_US.ein`, drawing only the prefixes the IRS assigns.** | 2 | 2 | 2 | 7 | 8 | 2.1, 4 | 26.7 |
|
||||
| 230 | 0.2.0 | | **Add `sv_SE.organisationsnummer`, whose first digit is its legal form's group digit and whose last is a Luhn check digit, and `sv_SE.vat`, `SE` + the number + `01`.** | 2 | 3 | 3 | 7 | 8 | 2.1, 4 | 26.4 |
|
||||
| 260 | 0.2.0 | | **Add a Recipes section: seed a PostgreSQL table, fill a struct in a Go test, make a column unique, and build parent and child records in Go.** | 1 | 3 | 3 | 7 | 7 | 1.2 | 26.2 |
|
||||
| 210 | 0.2.0 | | **Add `fr_FR` with its `geo/FR` tree.** | 3 | 8 | 4 | 8 | 8 | 2 | 26.2 |
|
||||
| 15 | 0.2.0 | | **Add `misc` tables and templates, one row of its detail's table per chunk.** | 3 | 9 | 3 | 8 | 8 | 2, 15, 14 | 25.9 |
|
||||
| 201 | 0.2.0 | | **Place each Swedish street in the postort whose area holds it, so a big postort such as Stockholm carries its own streets.** | 4 | 6 | 6 | 8 | 8 | 2, 16 | 25.7 |
|
||||
| 15 | 0.2.0 | | **Add `misc` tables and templates, one row of its detail's table per chunk.** | 3 | 9 | 3 | 8 | 8 | 2, 14, 13 | 25.9 |
|
||||
| 201 | 0.2.0 | | **Place each Swedish street in the postort whose area holds it, so a big postort such as Stockholm carries its own streets.** | 4 | 6 | 6 | 8 | 8 | 2, 15 | 25.7 |
|
||||
| 206 | 0.2.0 | | **Add `zh_CN` with its `geo/CN` tree.** | 4 | 8 | 4 | 8 | 8 | 2 | 25.2 |
|
||||
| 208 | 0.2.0 | | **Add `es_MX` with its `geo/MX` tree.** | 4 | 8 | 5 | 8 | 8 | 2 | 25.2 |
|
||||
| 212 | 0.2.0 | | **Add `pt_BR` with its `geo/BR` tree.** | 4 | 8 | 4 | 8 | 8 | 2 | 25.2 |
|
||||
@@ -78,6 +78,7 @@
|
||||
| 235 | 0.2.0 | | **Add `sv_SE.bankgiro` and `sv_SE.plusgiro` with their Luhn check digit, and `sv_SE.bankaccount` with a clearing number and the check digit its bank's account type demands.** | 3 | 5 | 5 | 7 | 8 | 4, 2 | 24.9 |
|
||||
| 163 | 0.2.0 | | **Lift the CLI's `--repeat` cap, since the CLI streams each render, and give the builtins' count and decimal caps a technical reason in the README.** | 2 | 2 | 2 | 7 | 7 | 2, 5.7 | 24.8 |
|
||||
| 234 | 0.2.0 | | **Draw `sv_SE.phone` from every area code in PTS's numbering plan, grouped as Språkrådet writes it, and add its international form, `+46 70 123 45 67`.** | 2 | 3 | 3 | 7 | 7 | 2.1, 4 | 24.5 |
|
||||
| 55 | 0.2.0 | | **Document `ErrNoColumns` and `MaxRepeat` in the README's Library section.** | 1 | 2 | 1 | 6 | 7 | 1.2 | 24.5 |
|
||||
| 227 | 0.2.0 | | **Add a README quick start per task, for the bulk fixture author and the Go test author, each rendering one person whose name, sex, id, email and address agree.** | 1 | 2 | 2 | 7 | 6 | 1, 7 | 24.5 |
|
||||
| 202 | 0.2.0 | | **Add `nb_NO` with its `geo/NO` tree.** | 3 | 7 | 3 | 7 | 8 | 2 | 24.4 |
|
||||
| 203 | 0.2.0 | | **Add `da_DK` with its `geo/DK` tree.** | 3 | 7 | 3 | 7 | 8 | 2 | 24.4 |
|
||||
@@ -87,12 +88,13 @@
|
||||
| 211 | 0.2.0 | | **Add `bn_BD` with its `geo/BD` tree.** | 4 | 8 | 4 | 7 | 8 | 2 | 23.2 |
|
||||
| 213 | 0.2.0 | | **Add `id_ID` with its `geo/ID` tree.** | 4 | 8 | 4 | 7 | 8 | 2 | 23.2 |
|
||||
| 214 | 0.2.0 | decision | **Add `ur_PK` with its `geo/PK` tree.** | 4 | 8 | 4 | 7 | 8 | 2 | 23.2 |
|
||||
| 154 | 0.2.0 | | **Add `en_US.routing`, an ABA routing number over the Fed's prefix ranges with its check digit drawn by `{aba()}`.** | 2 | 2 | 2 | 6 | 7 | 2.1, 15 | 22.8 |
|
||||
| 155 | 0.2.0 | | **Add `misc.isin` and `misc.cusip`, built by their structure rules with their check digits drawn by `{isin()}` and `{cusip()}`.** | 2 | 2 | 2 | 6 | 7 | 2, 15 | 22.8 |
|
||||
| 154 | 0.2.0 | | **Add `en_US.routing`, an ABA routing number over the Fed's prefix ranges with its check digit drawn by `{aba()}`.** | 2 | 2 | 2 | 6 | 7 | 2.1, 14 | 22.8 |
|
||||
| 155 | 0.2.0 | | **Add `misc.isin` and `misc.cusip`, built by their structure rules with their check digits drawn by `{isin()}` and `{cusip()}`.** | 2 | 2 | 2 | 6 | 7 | 2, 14 | 22.8 |
|
||||
| 236 | 0.2.0 | | **Add `sv_SE.licenseplate`, `ABC 123` and `ABC 12A`, never drawing a combination Transportstyrelsen blocks.** | 2 | 2 | 4 | 6 | 7 | 2, 4 | 22.8 |
|
||||
| 245 | 0.2.0 | | **Draw `en_US.phone` from every area code NANPA has in service.** | 2 | 2 | 2 | 6 | 7 | 2.1, 4 | 22.8 |
|
||||
| 205 | 0.2.0 | | **Add `is_IS` with its `geo/IS` tree.** | 3 | 6 | 3 | 6 | 8 | 2 | 22.7 |
|
||||
| 26 | 0.2.0 | defect | **Spell `sv_SE.personnummer` with the `+` Skatteverket uses from the year the holder turns 100, judged by a date the caller gives.** | 4 | 6 | 8 | 7 | 8 | 4 | 22.1 |
|
||||
| 261 | 0.2.0 | | **Move the README's edge cases and each category's caveats into the reference sections, below what a first reader needs.** | 2 | 4 | 4 | 5 | 7 | 1.2 | 21.0 |
|
||||
| 34 | 0.2.0 | | **Add `{isin()}`, `{cusip()}`, `{aba()}` and `{vin()}`.** | 2 | 4 | 4 | 6 | 7 | 4 | 20.9 |
|
||||
| 241 | 0.2.0 | | **Give `sv_SE.person` a `birthdate`.** | 2 | 2 | 3 | 6 | 6 | 2.1 | 20.9 |
|
||||
| 218 | 0.2.0 | decision | **Give every country shipped without streets its streets, once the maintainer accepts ODbL data or the country opens a register.** | 3 | 6 | 4 | 6 | 7 | 2.1 | 20.8 |
|
||||
@@ -129,7 +131,6 @@
|
||||
| 157 | 0.2.0 | | **Show in the README's CLI section how PowerShell and cmd.exe pipe a template, one line per shell family.** | 1 | 1 | 1 | 5 | 6 | 7.2 | 17.1 |
|
||||
| 229 | 0.2.0 | | **Name the line and column where JSON parsing stopped, when a template that opens like a JSON object then fails as a format string.** | 2 | 2 | 2 | 5 | 6 | 6, 5.1 | 17.1 |
|
||||
| 53 | 0.2.0 | | **Let a table column carry a `datatype`, so `--format json` writes `"safe": true` and `--format sql` a boolean.** | 4 | 5 | 5 | 6 | 6 | 4 | 16.9 |
|
||||
| 55 | 0.2.0 | | **Document `ErrNoColumns` and `MaxRepeat` in the README's Library section.** | 1 | 2 | 1 | 6 | 7 | 12 | 16.8 |
|
||||
| 181 | 0.2.0 | decision | **Fail a load where two sources define one category, unless the library's option or the CLI's flag says the later replaces the earlier, and then make every read reach the replacement.** | 5 | 4 | 6 | 6 | 9 | 9.3 | 16.8 |
|
||||
| 232 | 0.2.0 | | **Add `sv_SE.sni`, a table of SCB's SNI 2025 industry codes with their Swedish labels.** | 2 | 3 | 2 | 5 | 5 | 2 | 16.8 |
|
||||
| 233 | 0.2.0 | | **Add `sv_SE.ssyk`, a table of SCB's SSYK 2012 occupation codes with their Swedish labels.** | 2 | 3 | 2 | 5 | 5 | 2 | 16.8 |
|
||||
@@ -142,11 +143,12 @@
|
||||
| 20 | 0.2.0 | defect | **Weight `sv_SE.sex` and `en_US.sex` by bearers, as the README's Data section says they are.** | 2 | 2 | 2 | 4 | 6 | 4 | 15.7 |
|
||||
| 50 | 0.2.0 | | **Reword `prove`'s refusal of a typed column reading a row, `{/region}`.** | 1 | 2 | 2 | 4 | 6 | 6 | 15.5 |
|
||||
| 224 | 0.2.0 | | **Name the module in every error about its manifest or its files: today an `fs.FS` module's errors name only the file inside it.** | 1 | 2 | 3 | 4 | 6 | 6 | 15.5 |
|
||||
| 238 | 0.2.0 | | **Draw `sv_SE.word` and `sv_SE.sentence` from SALDO's nouns, verbs, adjectives and adverbs, each part of speech a path of its own.** | 3 | 5 | 4 | 5 | 5 | 2, 14 | 15.2 |
|
||||
| 249 | 0.2.0 | | **Draw `en_US.word` and `en_US.sentence` from Moby or WordNet, each part of speech a path of its own, and add `en_US.paragraph`.** | 3 | 5 | 4 | 5 | 5 | 2, 14 | 15.2 |
|
||||
| 238 | 0.2.0 | | **Draw `sv_SE.word` and `sv_SE.sentence` from SALDO's nouns, verbs, adjectives and adverbs, each part of speech a path of its own.** | 3 | 5 | 4 | 5 | 5 | 2, 13 | 15.2 |
|
||||
| 249 | 0.2.0 | | **Draw `en_US.word` and `en_US.sentence` from Moby or WordNet, each part of speech a path of its own, and add `en_US.paragraph`.** | 3 | 5 | 4 | 5 | 5 | 2, 13 | 15.2 |
|
||||
| 17 | 0.2.0 | | **Give every shipped category a column for each part its format composes, and one record shape across the national ids; re-pin the shape.** | 5 | 6 | 5 | 6 | 6 | 5 | 15.1 |
|
||||
| 165 | 0.2.0 | | **Let a table carry `name` with neither `key` nor `parent`, and list the rows a name matches by their line where it matches several.** | 2 | 2 | 3 | 4 | 6 | 5.7 | 15.1 |
|
||||
| 240 | 0.2.0 | | **Add `sv_SE.territory`, the Swedish name of each `misc.territory` row, keyed by its alpha-2 code.** | 2 | 2 | 2 | 4 | 5 | 2 | 15.0 |
|
||||
| 22 | 0.2.0 | | **Give `url` and `email` a path that draws only domains nobody can register, keeping the wide set as the default.** | 3 | 4 | 3 | 7 | 8 | 15 | 14.8 |
|
||||
| 170 | 0.2.0 | | **Decide in one function whether a read lands in a name's pick, which `refuseTwiceDrawnIn` calls at load and `keptInPick` at render.** | 3 | 3 | 8 | 2 | 8 | 3, 3.2 | 14.7 |
|
||||
| 197 | 0.2.0 | decision | **Replace the namespace mount with one the loader states in one place, and give `datafiles.Source.at` one job.** | 3 | 3 | 7 | 2 | 8 | 3.2, 3 | 14.7 |
|
||||
| 29 | 0.2.0 | | **Let a path go from a selected row into the template beside its family: `geo.US.locality[1714000].address`.** | 4 | 4 | 7 | 5 | 6 | 5 | 14.6 |
|
||||
@@ -155,11 +157,9 @@
|
||||
| 127 | 0.2.0 | | **Give a Swedish street only postal codes whose delivery digit means street or rural delivery, dropping codes whose delivery digit is 9, or record why they stay.** | 3 | 3 | 2 | 4 | 6 | 4 | 14.4 |
|
||||
| 251 | 0.2.0 | | **Export from the library whether a reference body is a lone path, so the CLI's `isPath` stops copying the path grammar.** | 2 | 2 | 3 | 3 | 6 | 3.2 | 14.3 |
|
||||
| 242 | 0.2.0 | | **Add `sv_SE.bloodtype`, weighted by Sweden's distribution.** | 1 | 2 | 1 | 4 | 4 | 2 | 14.1 |
|
||||
| 22 | 0.2.0 | | **Give `url` and `email` a path that draws only domains nobody can register, keeping the wide set as the default.** | 3 | 4 | 3 | 7 | 8 | 16 | 14.0 |
|
||||
| 46 | 0.2.0 | | **Report the same error every load for a table with two bad options.** | 2 | 2 | 2 | 3 | 7 | 6, 10 | 14.0 |
|
||||
| 110 | 0.2.0 | | **Refuse an empty segment in a path `Fake` and `FakeRecord` take, as a reference does: `sv_SE.person.` reports `path has an empty segment`.** | 1 | 1 | 2 | 3 | 6 | 6 | 13.8 |
|
||||
| 158 | 0.2.0 | | **Drop a UTF-8 byte order mark that starts stdin, and say in the README how to stop PowerShell 5.1 sending `Göteborg` as `G?teborg`.** | 2 | 2 | 2 | 4 | 5 | 7.2, 5.1 | 13.5 |
|
||||
| 226 | 0.2.0 | | **Show in the README how to make a column unique, such as an email's local part carrying `{seq()}`.** | 1 | 2 | 1 | 4 | 4 | 4 | 13.3 |
|
||||
| 166 | 0.2.0 | | **Let a key or name cell hold a bracket, a brace, a double quote or a pipe, so the table loads, its row draws at random, and only a selector cannot reach it.** | 2 | 2 | 4 | 3 | 6 | 5.7 | 13.1 |
|
||||
| 171 | 0.2.0 | | **Handle a template that only reads one record column in one place, so `checkColumns`, `itemDatatype`, `proveColumnItem` and `renderLeaf` no longer each special-case `readsColumn`.** | 3 | 3 | 7 | 2 | 7 | 3, 3.2 | 12.8 |
|
||||
| 252 | 0.2.0 | | **Publish `fejkdata-bin` to the AUR from the Packages workflow, so Arch installs with `yay -S fejkdata-bin`.** | 1 | 1 | 1 | 3 | 4 | 1.1 | 12.8 |
|
||||
@@ -178,6 +178,12 @@
|
||||
| 174 | 0.2.0 | | **Spell once the fact that a walk's next step leaves a selected row, which `pathPos.fromRow` and `tableRoute.nextLinked` each spell today.** | 1 | 1 | 3 | 1 | 5 | 3.2 | 9.8 |
|
||||
| 108 | 0.2.0 | defect | **Name item 26 in the decision "No builtin reads the clock, so a date is bounded by days, never by an age", as the item that ends its conflict with goal 4.** | 1 | 1 | 1 | 1 | 4 | 3.4 | 8.0 |
|
||||
| 169 | 0.2.0 | question | **Reword the decision "A parent row with no child row is a load error" to say what each geo import does with such a row: drop it, or exit where `geo-se.py` would lose a kommun or a län.** | 1 | 1 | 2 | 1 | 4 | 3 | 8.0 |
|
||||
| 255 | 0.3.0 | | **Let a repeat or a choice item bind a name to a path through an enclosing name, `{a.object as o}`, so each iteration draws a fresh row inside the enclosing pick.** | 4 | 4 | 7 | 6 | 7 | 4.1, 5.3 | 18.9 |
|
||||
| 256 | 0.3.0 | | **Let `repeat` take a range, such as 5 to 15, drawing the count on each render.** | 2 | 3 | 3 | 6 | 6 | 5 | 18.9 |
|
||||
| 75 | 0.3.0 | | **Run every README `sh`, `go` and `text` example as a test, and compare each "Renders" line to a seeded render.** | 2 | 6 | 5 | 4 | 7 | 1.2 | 18.5 |
|
||||
| 257 | 0.3.0 | | **Add a transform that writes a value escaped for a JSON string, so a text template spelling JSON stays valid whatever a drawn value holds.** | 2 | 2 | 3 | 5 | 6 | 4, 5 | 17.7 |
|
||||
| 253 | 0.3.0 | | **Let `date()` take bounds relative to the date the caller gives, such as 30 days before it to 14 days after it.** | 2 | 3 | 4 | 6 | 6 | 7.3, 10.1 | 17.6 |
|
||||
| 254 | 0.3.0 | | **Let a choice deal its items in exact counts across a run's renders, so 200 issues hold exactly 4 in progress, where weights only approach it.** | 3 | 4 | 6 | 6 | 6 | 5, 10 | 17.6 |
|
||||
| 38 | 0.3.0 | defect | **Compare `calcParser.binary`'s operator as a rune.** | 1 | 1 | 2 | 3 | 6 | 6 | 13.8 |
|
||||
| 45 | 0.3.0 | | **Name the node a selector follows in `childNamed`'s refusal.** | 1 | 1 | 2 | 3 | 6 | 6 | 13.8 |
|
||||
| 49 | 0.3.0 | | **Report a CRLF rows file holding only its header as having no rows.** | 1 | 1 | 2 | 3 | 6 | 6 | 13.8 |
|
||||
@@ -189,21 +195,19 @@
|
||||
| 11 | 0.3.0 | | **Test that every `geo` tree holds the five table names and the `address` columns that port across countries.** | 1 | 2 | 2 | 3 | 5 | 4 | 13.0 |
|
||||
| 35 | 0.3.0 | | **Add `{base64url(n)}` for JWT shapes.** | 1 | 2 | 3 | 4 | 5 | 8 | 13.0 |
|
||||
| 40 | 0.3.0 | | **Parse `defaultTable`'s path with the library's grammar.** | 2 | 2 | 3 | 3 | 5 | 3.2 | 12.5 |
|
||||
| 21 | 0.3.0 | | **List `title` in both locales, and `sv_SE`'s `birth-number`, in the README's Data list of what each locale carries.** | 1 | 1 | 1 | 4 | 6 | 12 | 12.2 |
|
||||
| 23 | 0.3.0 | | **Give every other shipped category that could reach something real a path that never does.** | 3 | 6 | 4 | 6 | 7 | 15 | 11.7 |
|
||||
| 86 | 0.3.0 | | **Read both `en_US.phone` formats' area codes from one category, as their exchange reads `en_US.phone-exchange`.** | 1 | 2 | 2 | 2 | 5 | 3.2 | 11.5 |
|
||||
| 23 | 0.3.0 | | **Give every other shipped category that could reach something real a path that never does.** | 3 | 6 | 4 | 6 | 7 | 16 | 11.0 |
|
||||
| 75 | 0.3.0 | | **Run every README `sh`, `go` and `text` example as a test, and compare each "Renders" line to a seeded render.** | 2 | 6 | 5 | 4 | 7 | 12.2 | 10.8 |
|
||||
| 33 | 0.3.0 | | **Group `data/misc` into folders where a group name makes a path easier to guess.** | 4 | 5 | 4 | 4 | 5 | 5 | 10.8 |
|
||||
| 72 | 0.3.0 | principle | **Fail the merge gate when test coverage falls below the last recorded figure.** | 2 | 3 | 3 | 2 | 5 | 3 | 10.2 |
|
||||
| 30 | 0.3.0 | | **Draw `email.local` from `username`, so the two share one handle list.** | 2 | 2 | 2 | 3 | 5 | 8 | 10.0 |
|
||||
| 19 | 0.3.0 | | **Fill the 398 Swedish localities weighted 200 from SCB småorter.** | 2 | 4 | 4 | 4 | 6 | 13 | 9.8 |
|
||||
| 95 | 0.3.0 | principle | **Hold a struct's columns and the field each fills in one slice.** | 1 | 2 | 3 | 1 | 5 | 3.2 | 9.5 |
|
||||
| 116 | 0.3.0 | | **Turn a row index into its rows file's line in one function.** | 1 | 2 | 2 | 1 | 5 | 3.2 | 9.5 |
|
||||
| 19 | 0.3.0 | | **Fill the 398 Swedish localities weighted 200 from SCB småorter.** | 2 | 4 | 4 | 4 | 6 | 14 | 9.2 |
|
||||
| 56 | 0.3.0 | defect | **Build on a manual run of `test.yml`.** | 1 | 1 | 2 | 1 | 4 | 1 | 8.8 |
|
||||
| 109 | 0.3.0 | principle | **Indent every script under `data-import/` with hard tabs, as `AGENTS.md` requires and `release-tooling/` does.** | 2 | 4 | 1 | 1 | 4 | 3 | 6.2 |
|
||||
| 63 | 0.4.0 | | **Pair a street with its exact postnummer.** | 3 | 6 | 5 | 5 | 8 | 4.1 | 19.1 |
|
||||
| 59 | 0.4.0 | | **Promise in `Generator`'s godoc that its renders run one at a time.** | 1 | 1 | 1 | 5 | 5 | 1.2 | 18.8 |
|
||||
| 62 | 0.4.0 | | **Add `{btc()}` and `{eth()}`.** | 2 | 4 | 5 | 4 | 6 | 4 | 15.2 |
|
||||
| 59 | 0.4.0 | | **Promise in `Generator`'s godoc that its renders run one at a time.** | 1 | 1 | 1 | 5 | 5 | 12 | 13.2 |
|
||||
| 65 | 0.4.0 | | **Hold every change `AGENTS.md` says owes a `CHANGELOG.md` entry to one in CI.** | 2 | 3 | 5 | 4 | 5 | 5.8 | 13.2 |
|
||||
| 66 | 0.4.0 | | **Cut the README's Layout block to the lines that say what a file name cannot.** | 1 | 1 | 1 | 2 | 5 | 3.4 | 11.8 |
|
||||
| 67 | 0.4.0 | | **Cut `AGENTS.md` to the rules only it states, and explain every term its comprehension rule uses.** | 1 | 2 | 2 | 1 | 6 | 3.4 | 11.3 |
|
||||
@@ -215,6 +219,14 @@
|
||||
|
||||
## Details
|
||||
|
||||
### 259. Follow the quickstarts with a table of every shipped path, its example value and what makes it valid or where it comes from, generated from the data and checked by a test.
|
||||
|
||||
"What can I get?" is a reader's first question, and today the answer is one sentence in the Data section, about 440 lines down. Group the rows by `sv_SE`, `en_US`, `misc` and `geo`. Generate them from `List` and one seeded render per path, and have `readme_test.go` fail when the table drifts. The Data section keeps the sources and the linked-table detail. Replaces item 21, so `title` and `birth-number` are listed with the rest.
|
||||
|
||||
### 258. Open the README with a CLI quickstart and a Go quickstart side by side, rendering the same value.
|
||||
|
||||
The opening line says "a Go library and a CLI", but its example is CLI-only, and the Library section starts after about 150 lines of CLI detail. A Go reader learns late that the library composes with code: a loop per parent, a random count, a shuffle for exact counts. README → Development → Documentation states the layout.
|
||||
|
||||
### 243. Add `en_US.ein`, drawing only the prefixes the IRS assigns.
|
||||
|
||||
The IRS's prefix list is facts. [Source research](docs/research/research-sources-world.md).
|
||||
@@ -223,6 +235,10 @@ The IRS's prefix list is facts. [Source research](docs/research/research-sources
|
||||
|
||||
Bolagsverket's group digits and the Luhn rule are facts. [Source research](docs/research/research-sources-se.md).
|
||||
|
||||
### 260. Add a Recipes section: seed a PostgreSQL table, fill a struct in a Go test, make a column unique, and build parent and child records in Go.
|
||||
|
||||
Each recipe runs as a test. The unique column carries `{seq()}`, as in an email's local part (it replaces item 226). The parent and child recipe compiles one template per parent with `NewTemplate`, draws the child count in Go, and marshals with `encoding/json`, as the 2026-10-14 Jira seeding experiment needed.
|
||||
|
||||
### 210. Add `fr_FR` with its `geo/FR` tree.
|
||||
|
||||
France holds more French speakers than DR Congo: 64M to 49M on Wikipedia's "Geographical distribution of French speakers", which credits a 2022 OIF report but flags its table as not taken from that report. The Base Adresse Nationale (Licence Ouverte 2.0) holds every street with its code postal.
|
||||
@@ -367,6 +383,10 @@ The format hard-codes `-`, and its earliest birthdate, 1930-01-01, makes the old
|
||||
- Rewrite the README's `--seed` and `WithSeed` rows to goal 10's wording, and name the new required option in the changelog: `New` breaks again here, the first time after 0.1.0, as the maintainer accepted.
|
||||
- Research: `sv_SE.samordningsnummer` also hard-codes `-` and draws from 1930. Check whether Lag (2022:1697) om samordningsnummer gives it the same `+`; folkbokföringslagen 18 § states the rule for personnummer only.
|
||||
|
||||
### 261. Move the README's edge cases and each category's caveats into the reference sections, below what a first reader needs.
|
||||
|
||||
Examples: the bytes stdin renders, MySQL's `ANSI_QUOTES`, CSV rows spanning lines, `misc.timezone`'s standard offset against DST, and a Swedish street that may sit under a neighbouring postort. A category's caveat may instead be a note in item 259's table. Each rule still sits in one place.
|
||||
|
||||
### 34. Add `{isin()}`, `{cusip()}`, `{aba()}` and `{vin()}`.
|
||||
|
||||
`isin`: Luhn over letters expanded to digits. `aba`: 3-7-1 weights. `vin`: position 9 over the whole; a sample taking the WMI, since the check sits mid-string.
|
||||
@@ -563,6 +583,10 @@ The maintainer ruled so on 2026-10-09. Today `bindOptions` in `internal/rows` re
|
||||
|
||||
From CLDR's `sv` territory names, under the Unicode licence. [Source research](docs/research/research-sources-se.md).
|
||||
|
||||
### 22. Give `url` and `email` a path that draws only domains nobody can register, keeping the wide set as the default.
|
||||
|
||||
17 of the 40 distinct ones shipped today sit on `.se`, `.nu`, `.io` and `.co`, which anyone may register, and only RFC 2606's `example.com`, `.net`, `.org`, `.test`, `.example`, `.invalid` and `.localhost` provably reach nothing.
|
||||
|
||||
### 170. Decide in one function whether a read lands in a name's pick, which `refuseTwiceDrawnIn` calls at load and `keptInPick` at render.
|
||||
|
||||
All 13 seats of the 2026-10-09 panel on b9d1af6 named `keptInPick` and `readUnder` hardest, and 12 named the rule both apply as the code they would least want to change. A key `addressedKeys` builds at load must equal the one `pickKey.under` builds at render, and a mismatch splits a correlated draw without an error. `refuseTwiceDrawnIn` applies the same rule at load in code of its own. In the new function's comment, say why a fresh read's levels start at its head, `levelKeys(…, 1)` in `pathArm`, and a named read's at its target, `levelKeys(full, 0)` in `compileArm`, which three seats rebuilt by hand.
|
||||
@@ -591,10 +615,6 @@ The inherited architect of the 0.1.0 scoring run on 52e0304 named `isPath` (`cli
|
||||
|
||||
geblod.nu's 2007 figures, via sv.wikipedia: A+ 37, O+ 32, B+ 10, A− 7, O− 6, AB+ 5, B− 2, AB− 1 (%). Item 15 plans a global `misc.bloodtype` from AABB. [Source research](docs/research/research-sources-se.md).
|
||||
|
||||
### 22. Give `url` and `email` a path that draws only domains nobody can register, keeping the wide set as the default.
|
||||
|
||||
17 of the 40 distinct ones shipped today sit on `.se`, `.nu`, `.io` and `.co`, which anyone may register, and only RFC 2606's `example.com`, `.net`, `.org`, `.test`, `.example`, `.invalid` and `.localhost` provably reach nothing.
|
||||
|
||||
### 46. Report the same error every load for a table with two bad options.
|
||||
|
||||
`readTableOptions` in `table.go` returns on the first in Go's map order.
|
||||
@@ -671,6 +691,30 @@ Item 26 revises the decision, but the decision does not point back to it.
|
||||
|
||||
Its entry says "The import script drops or fills such rows". There are two geo imports, and `geo-se.py` exits when `geo.fence` drops a kommun or a län, while `geo-us.py` drops counties and states. Rewording a decision is the maintainer's call.
|
||||
|
||||
### 255. Let a repeat or a choice item bind a name to a path through an enclosing name, `{a.object as o}`, so each iteration draws a fresh row inside the enclosing pick.
|
||||
|
||||
A template spelling a parent with its children, an epic and its stories, binds the parent's area once, `{/jira.area as a}`, and wants each child in a `repeat` to name a different object of that area. Today `{a.object}` keeps one pick for every iteration, since a name keeps every level its reads pass, and `{a.object as o}` inside the repeat is refused: `no field "a"; … a binding names a field, or a reference`. The way round is one run per parent, selecting the area by key in a template the caller writes. Revise the decision "Every `{…}` draws afresh, and only a name keeps a pick", whose binding list leaves out a path through a name, and state how the new pick sits beside the enclosing name's memo (`drawMemo.stepDownPins` and "A pick keeps only the levels a read of the name addresses"). `readName` and `keptInPick` are places the comprehension seats found hard; make them easier to read where the change touches them.
|
||||
|
||||
### 256. Let `repeat` take a range, such as 5 to 15, drawing the count on each render.
|
||||
|
||||
An epic holds between 5 and 15 stories, an order between 1 and 8 lines. Today `repeat` is one integer, so a varied count takes a weighted choice of templates that differ only in their `repeat`. The repeat cap counts a range's upper bound. Bench the spelling with the bulk fixture author and the data author, such as `"repeat": [5, 15]` or `"repeat": "{int(5,15)}"`; the README's Repeat section and the decision "The repeat cap bounds renders, not bytes" name it.
|
||||
|
||||
### 75. Run every README `sh`, `go` and `text` example as a test, and compare each "Renders" line to a seeded render.
|
||||
|
||||
`readme_test.go` loads and renders each `json` block, but runs no `sh` example (CLI, Records, Table, Linked tables), no `go` example (Library) and no `text` block of error text, and compares no "Renders e.g." output, so goal 1.2 is not met.
|
||||
|
||||
### 257. Add a transform that writes a value escaped for a JSON string, so a text template spelling JSON stays valid whatever a drawn value holds.
|
||||
|
||||
A nested structure, a parent with an array of children, renders as one text template whose format spells JSON (`{{"summary":"{s}"}}`), since a record's columns are flat. A drawn value holding `"`, `\` or a newline breaks that JSON, as a company name or a sentence may. A transform such as `{json(s)}` writes the value with JSON's string escapes, beside `lowercase`, `uppercase` and `ascii`. Bench its name with the bulk fixture author.
|
||||
|
||||
### 253. Let `date()` take bounds relative to the date the caller gives, such as 30 days before it to 14 days after it.
|
||||
|
||||
Needs item 26, which makes the caller give the date and revises the decision "No builtin reads the clock, so a date is bounded by days, never by an age". Today a due date or a sprint window "around now" is two fixed days, re-pinned whenever the fixture should look current. A relative bound reads the caller's date, never the clock, so the same seed and date still give the same output (goal 10). Bench the spelling with the hand fixture author and the bulk fixture author, such as `date(-30d,+14d,'2006-01-02')`; the README's Functions row and the clock decision change with it.
|
||||
|
||||
### 254. Let a choice deal its items in exact counts across a run's renders, so 200 issues hold exactly 4 in progress, where weights only approach it.
|
||||
|
||||
A fixture often needs a fixed mix: of 200 issues, 4 in progress, 30 done and the rest to do. A weighted choice gives about 4, and the count shifts with the seed, so a test asserting on it, or a board built from it, needs a loader to fix the counts. A dealt choice is a deck, drawn without replacement until it runs out. Decide what a render past the last card does: refuse at load where `--repeat` is known to exceed the deck, start a fresh deck, or fall back to the weights. Decide too which renders share one deck: the run, as `{seq()}` shares one counter, or each `repeat`. Bench the spelling with the bulk fixture author. The deck is per-generator state beside `seq()`'s counters, so it must stay reproducible from one goroutine (goal 10). Where the change touches `render.go`'s `readField` or the pick state in `pick.go`, places the comprehension seats found hard, make it easier to read.
|
||||
|
||||
### 38. Compare `calcParser.binary`'s operator as a rune.
|
||||
|
||||
`byte(p.rs[p.pos])` reads U+012B `ī` as `+`, so `{calc(a ī b)}` compiles as `a + b`.
|
||||
@@ -711,17 +755,13 @@ Nothing checks it, and a new country breaks it silently.
|
||||
|
||||
The CLI counts brackets of its own, so a change to the selector grammar desyncs the default `--table` silently.
|
||||
|
||||
### 86. Read both `en_US.phone` formats' area codes from one category, as their exchange reads `en_US.phone-exchange`.
|
||||
|
||||
Both phone formats carry the same 21-code `area` list, so an edit to one copy can miss the other.
|
||||
|
||||
### 23. Give every other shipped category that could reach something real a path that never does.
|
||||
|
||||
`phone` draws live PTS and NANP ranges — PTS's five fiction series are the Swedish inert set, sourced in [research-sources-se.md](docs/research/research-sources-se.md), and the NANP one still wants a source — and `bankgiro`, `plusgiro` and `routing` draw live prefixes.
|
||||
|
||||
### 75. Run every README `sh`, `go` and `text` example as a test, and compare each "Renders" line to a seeded render.
|
||||
### 86. Read both `en_US.phone` formats' area codes from one category, as their exchange reads `en_US.phone-exchange`.
|
||||
|
||||
`readme_test.go` loads and renders each `json` block, but runs no `sh` example (CLI, Records, Table, Linked tables), no `go` example (Library) and no `text` block of error text, and compares no "Renders e.g." output, so goal 12.2 is not met.
|
||||
Both phone formats carry the same 21-code `area` list, so an edit to one copy can miss the other.
|
||||
|
||||
### 72. Fail the merge gate when test coverage falls below the last recorded figure.
|
||||
|
||||
@@ -747,14 +787,14 @@ Technical principle: indent with hard tabs. All 19 scripts indent with four spac
|
||||
|
||||
Needs an application to Lantmäteriet; today a street goes to the nearest postal code centroid. It rewrites shipped rows, so it is a major once 1.0 is cut and a minor before that.
|
||||
|
||||
### 62. Add `{btc()}` and `{eth()}`.
|
||||
|
||||
They need sha256 and keccak for Base58Check and EIP-55.
|
||||
|
||||
### 59. Promise in `Generator`'s godoc that its renders run one at a time.
|
||||
|
||||
A caller wanting parallel throughput then makes one generator per goroutine.
|
||||
|
||||
### 62. Add `{btc()}` and `{eth()}`.
|
||||
|
||||
They need sha256 and keccak for Base58Check and EIP-55.
|
||||
|
||||
### 65. Hold every change `AGENTS.md` says owes a `CHANGELOG.md` entry to one in CI.
|
||||
|
||||
The check covers `data` and `testdata/shipped_shape.txt` alone, where `AGENTS.md` adds a flag, an exit code, an exported name, a fence, a builtin and the lowest Go.
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
"""File each module's known vulnerabilities as one security item atop todo.md's next release.
|
||||
|
||||
Usage: file_vulnerabilities.py <govulncheck -format json report> <todo.md>
|
||||
Exits 1 while any vulnerability is known, filed now or before, 0 on a clean report, and 2 on
|
||||
a todo.md it cannot read.
|
||||
|
||||
todo.md's `Next ID = N` gives the item's ID, and the release of the plan's top row, the next
|
||||
release, gives its release. Its Score is 0.0 until rank_todo.py, which CI cannot run, scores
|
||||
it; a security item ranks first in its release whatever it scores. The test that files into
|
||||
the repository's own todo.md fails the gate when the plan's shape drifts from what this
|
||||
script reads.
|
||||
|
||||
Scores, as todo.md's table spells them: R (risk), S (size) and C (difficulty) are 1, since
|
||||
the fix is a version bump; A (value to the audience) and G (value to the goal) are 8, the goal
|
||||
being README goal 1.1, installing fejkdata the way one installs other tools, which a flagged
|
||||
binary works against.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
ROW = re.compile(r"^\| (?P<id>\d+) \| (?P<release>[^|]+?) \| [^|]*\| \*\*", re.M)
|
||||
NEXT_ID = re.compile(r"`Next ID = (\d+)`")
|
||||
DETAILS = "## Details\n\n"
|
||||
SCORES = {"R": 1, "S": 1, "C": 1, "A": 8, "G": 8}
|
||||
GOAL = "1.1"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Module:
|
||||
path: str
|
||||
found_in: str
|
||||
fixed: list[str] = field(default_factory=list)
|
||||
unfixed: bool = False
|
||||
osvs: dict[str, dict] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "Go" if self.path == "stdlib" else f"`{self.path}`"
|
||||
|
||||
def version(self, version: str) -> str:
|
||||
return version.removeprefix("v") if self.path == "stdlib" else version
|
||||
|
||||
@property
|
||||
def carried(self) -> str:
|
||||
return f"{self.name} {self.version(self.found_in)}"
|
||||
|
||||
|
||||
def messages(report: str):
|
||||
decoder, at = json.JSONDecoder(), 0
|
||||
while at < len(report):
|
||||
if report[at].isspace():
|
||||
at += 1
|
||||
continue
|
||||
message, at = decoder.raw_decode(report, at)
|
||||
yield message
|
||||
|
||||
|
||||
def vulnerable_modules(report: str) -> list[Module]:
|
||||
"""Each module govulncheck found a vulnerability in, Go (stdlib) first, then by path."""
|
||||
osvs, modules = {}, {}
|
||||
for message in messages(report):
|
||||
if "osv" in message:
|
||||
osv = message["osv"]
|
||||
osvs[osv["id"]] = {"id": osv["id"], "aliases": osv.get("aliases", []), "summary": osv.get("summary", "")}
|
||||
elif "finding" in message:
|
||||
finding = message["finding"]
|
||||
frame = finding["trace"][0]
|
||||
module = modules.setdefault(frame["module"], Module(frame["module"], frame["version"]))
|
||||
module.osvs[finding["osv"]] = osvs[finding["osv"]]
|
||||
if "fixed_version" in finding:
|
||||
module.fixed.append(finding["fixed_version"])
|
||||
else:
|
||||
module.unfixed = True
|
||||
return sorted(modules.values(), key=lambda m: (m.path != "stdlib", m.path))
|
||||
|
||||
|
||||
def version_key(version: str):
|
||||
return [int(part) for part in re.findall(r"\d+", version)]
|
||||
|
||||
|
||||
def tagline(module: Module) -> str:
|
||||
if module.unfixed:
|
||||
return f"Ship no release while fejkdata carries {module.carried}, some of whose known vulnerabilities no version fixes yet."
|
||||
fixed = module.version(max(module.fixed, key=version_key))
|
||||
if module.path == "stdlib":
|
||||
return f"Raise Go to {fixed}, so the release binaries carry none of {module.carried}'s known vulnerabilities."
|
||||
return f"Raise {module.name} to {fixed}, so fejkdata requires no version of it with a known vulnerability."
|
||||
|
||||
|
||||
def detail(module: Module) -> str:
|
||||
lines = []
|
||||
for osv in sorted(module.osvs.values(), key=lambda o: o["id"]):
|
||||
aliases = f" ({', '.join(osv['aliases'])})" if osv["aliases"] else ""
|
||||
summary = f": {osv['summary']}" if osv["summary"] else ""
|
||||
lines.append(f"- {osv['id']}{aliases}{summary}")
|
||||
return f"govulncheck found these in {module.carried}:\n\n" + "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def file_items(modules: list[Module], todo: str) -> tuple[str, list[int]]:
|
||||
"""Return todo.md with an item for each module whose vulnerabilities it does not yet name, and the items' IDs."""
|
||||
first = ROW.search(todo)
|
||||
if first is None:
|
||||
raise ValueError("todo.md has no item to take the next release from")
|
||||
if NEXT_ID.search(todo) is None:
|
||||
raise ValueError("todo.md has no `Next ID = N` line to number the item by")
|
||||
if DETAILS not in todo:
|
||||
raise ValueError(f"todo.md has no {DETAILS.strip()!r} heading to put the item's detail under")
|
||||
release, rows_at = first.group("release"), first.start()
|
||||
details_at = todo.index(DETAILS) + len(DETAILS)
|
||||
filed = []
|
||||
for module in modules:
|
||||
if all(osv in todo for osv in module.osvs):
|
||||
continue
|
||||
next_id = int(NEXT_ID.search(todo).group(1))
|
||||
line = tagline(module)
|
||||
scores = " | ".join(str(SCORES[k]) for k in "RSCAG")
|
||||
row = f"| {next_id} | {release} | security | **{line}** | {scores} | {GOAL} | 0.0 |\n"
|
||||
text = f"### {next_id}. {line}\n\n{detail(module)}\n"
|
||||
todo = todo[:rows_at] + row + todo[rows_at:]
|
||||
rows_at += len(row)
|
||||
details_at += len(row)
|
||||
todo = todo[:details_at] + text + todo[details_at:]
|
||||
details_at += len(text)
|
||||
todo = NEXT_ID.sub(f"`Next ID = {next_id + 1}`", todo, count=1)
|
||||
filed.append(next_id)
|
||||
return todo, filed
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
report_path, todo_path = argv
|
||||
with open(report_path, encoding="utf-8") as f:
|
||||
modules = vulnerable_modules(f.read())
|
||||
if not modules:
|
||||
print("No known vulnerabilities.")
|
||||
return 0
|
||||
count = sum(len(m.osvs) for m in modules)
|
||||
known = f"{count} known {'vulnerability' if count == 1 else 'vulnerabilities'}"
|
||||
with open(todo_path, encoding="utf-8") as f:
|
||||
try:
|
||||
todo, filed = file_items(modules, f.read())
|
||||
except ValueError as err:
|
||||
print(err, file=sys.stderr)
|
||||
return 2
|
||||
if not filed:
|
||||
print(f"{known}, already planned in todo.md.")
|
||||
return 1
|
||||
with open(todo_path, "w", encoding="utf-8") as f:
|
||||
f.write(todo)
|
||||
items = ", ".join(map(str, filed))
|
||||
print(f"{known}; todo.md {'item ' + items + ' plans' if len(filed) == 1 else 'items ' + items + ' plan'} the fix.")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
@@ -0,0 +1,195 @@
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
|
||||
import file_vulnerabilities
|
||||
|
||||
TODO = """# todo
|
||||
|
||||
## Scoring
|
||||
|
||||
`Score = -R - S/4 + 2*A + 2*G*W`
|
||||
|
||||
`Bar = 9`
|
||||
|
||||
`Next ID = 7`
|
||||
|
||||
| Goal | W |
|
||||
|---|---|
|
||||
| 1 | 1.00 |
|
||||
| 1.1 | 0.90 |
|
||||
|
||||
## Items
|
||||
|
||||
| ID | Release | Exempt | Item | R | S | C | A | G | Goals | Score |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 5 | 0.2.0 | | **Add a thing.** | 2 | 2 | 2 | 7 | 8 | 1 | 26.5 |
|
||||
|
||||
## Details
|
||||
|
||||
### 5. Add a thing.
|
||||
|
||||
Why.
|
||||
"""
|
||||
|
||||
OSVS = (
|
||||
{"id": "GO-2026-0002", "summary": "Denial of service in net/http", "aliases": ["GHSA-2222", "CVE-2026-2"]},
|
||||
{"id": "GO-2026-0001", "summary": "Cross-site scripting in html/template", "aliases": ["CVE-2026-1"]},
|
||||
{"id": "GO-2026-0003", "summary": "Panic in x/text"},
|
||||
{"id": "GO-2026-0004"},
|
||||
)
|
||||
STDLIB = (
|
||||
("GO-2026-0002", "stdlib", "v1.27.1", "v1.27.2"),
|
||||
("GO-2026-0001", "stdlib", "v1.27.1", "v1.27.10"),
|
||||
("GO-2026-0001", "stdlib", "v1.27.1", "v1.27.10"),
|
||||
)
|
||||
TEXT = (("GO-2026-0003", "golang.org/x/text", "v0.3.0", "v0.3.8"),)
|
||||
GO_TAGLINE = "Raise Go to 1.27.10, so the release binaries carry none of Go 1.27.1's known vulnerabilities."
|
||||
|
||||
|
||||
def report(*findings):
|
||||
messages = [{"config": {"go_version": "go1.27.1", "scan_level": "module"}}]
|
||||
messages += [{"osv": osv} for osv in OSVS]
|
||||
for osv, module, version, fixed in findings:
|
||||
finding = {"osv": osv, "trace": [{"module": module, "version": version}]}
|
||||
if fixed:
|
||||
finding["fixed_version"] = fixed
|
||||
messages.append({"finding": finding})
|
||||
return "\n".join(json.dumps(m, indent=2) for m in messages)
|
||||
|
||||
|
||||
def file(todo, *findings):
|
||||
return file_vulnerabilities.file_items(file_vulnerabilities.vulnerable_modules(report(*findings)), todo)
|
||||
|
||||
|
||||
def rows(todo):
|
||||
return [line for line in todo.splitlines() if "| **" in line]
|
||||
|
||||
|
||||
class FileItems(unittest.TestCase):
|
||||
def test_no_finding_files_nothing(self):
|
||||
self.assertEqual(file(TODO), (TODO, []))
|
||||
|
||||
def test_files_one_security_item_per_module_atop_the_next_release(self):
|
||||
todo, filed = file(TODO, *STDLIB)
|
||||
self.assertEqual(filed, [7])
|
||||
self.assertIn("`Next ID = 8`", todo)
|
||||
self.assertEqual(rows(todo)[0], f"| 7 | 0.2.0 | security | **{GO_TAGLINE}** | 1 | 1 | 1 | 8 | 8 | 1.1 | 0.0 |")
|
||||
self.assertTrue(rows(todo)[1].startswith("| 5 |"))
|
||||
self.assertTrue(
|
||||
todo.split("## Details\n\n", 1)[1].startswith(
|
||||
f"### 7. {GO_TAGLINE}\n\n"
|
||||
"govulncheck found these in Go 1.27.1:\n\n"
|
||||
"- GO-2026-0001 (CVE-2026-1): Cross-site scripting in html/template\n"
|
||||
"- GO-2026-0002 (GHSA-2222, CVE-2026-2): Denial of service in net/http\n\n"
|
||||
"### 5. Add a thing."
|
||||
)
|
||||
)
|
||||
|
||||
def test_a_required_module_gets_its_own_item(self):
|
||||
todo, filed = file(TODO, *STDLIB, *TEXT)
|
||||
self.assertEqual(filed, [7, 8])
|
||||
self.assertIn(
|
||||
"| 8 | 0.2.0 | security | **Raise `golang.org/x/text` to v0.3.8, so fejkdata requires no version"
|
||||
" of it with a known vulnerability.** |",
|
||||
todo,
|
||||
)
|
||||
self.assertIn("govulncheck found these in `golang.org/x/text` v0.3.0:\n\n- GO-2026-0003: Panic in x/text\n", todo)
|
||||
self.assertIn("`Next ID = 9`", todo)
|
||||
self.assertEqual([row.split(" | ")[0] for row in rows(todo)], ["| 7", "| 8", "| 5"])
|
||||
details = todo.split("## Details\n\n", 1)[1]
|
||||
self.assertLess(details.index("### 7."), details.index("### 8."))
|
||||
|
||||
def test_a_vulnerability_already_planned_is_not_filed_again(self):
|
||||
planned, _ = file(TODO, *STDLIB)
|
||||
self.assertEqual(file(planned, *STDLIB), (planned, []))
|
||||
|
||||
def test_a_module_newly_vulnerable_is_filed_beside_one_already_planned(self):
|
||||
planned, _ = file(TODO, *STDLIB)
|
||||
todo, filed = file(planned, *STDLIB, *TEXT)
|
||||
self.assertEqual(filed, [8])
|
||||
self.assertIn("| 8 | 0.2.0 | security | **Raise `golang.org/x/text`", todo)
|
||||
|
||||
def test_a_vulnerability_no_version_fixes_names_no_target(self):
|
||||
todo, _ = file(TODO, *STDLIB, ("GO-2026-0004", "stdlib", "v1.27.1", ""))
|
||||
self.assertIn("- GO-2026-0004\n", todo)
|
||||
self.assertIn(
|
||||
"| 7 | 0.2.0 | security | **Ship no release while fejkdata carries Go 1.27.1, some of whose known"
|
||||
" vulnerabilities no version fixes yet.** |",
|
||||
todo,
|
||||
)
|
||||
|
||||
def test_a_plan_with_no_next_id_is_refused(self):
|
||||
with self.assertRaises(ValueError):
|
||||
file(TODO.replace("`Next ID = 7`", ""), *STDLIB)
|
||||
|
||||
def test_a_plan_with_no_details_heading_is_refused(self):
|
||||
with self.assertRaises(ValueError):
|
||||
file(TODO.replace("## Details", "## Notes"), *STDLIB)
|
||||
|
||||
def test_a_plan_with_no_items_is_refused(self):
|
||||
empty = TODO.split("| 5 |")[0].rstrip("\n") + "\n\n## Details\n"
|
||||
with self.assertRaises(ValueError):
|
||||
file(empty, *STDLIB)
|
||||
|
||||
def test_the_repositorys_plan_takes_an_item_in_its_own_shape(self):
|
||||
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "todo.md")
|
||||
with open(path, encoding="utf-8") as f:
|
||||
plan = f.read()
|
||||
todo, filed = file(plan, *STDLIB)
|
||||
header = next(line for line in plan.splitlines() if line.startswith("| ID |"))
|
||||
row = rows(todo)[0]
|
||||
self.assertEqual(row.count("|"), header.count("|"))
|
||||
self.assertEqual(row.split(" | ")[1], rows(plan)[0].split(" | ")[1])
|
||||
self.assertRegex(row, rf"^\| {filed[0]} \| .* \| 0\.0 \|$")
|
||||
self.assertIn(f"`Next ID = {filed[0] + 1}`", todo)
|
||||
|
||||
|
||||
class Main(unittest.TestCase):
|
||||
def setUp(self):
|
||||
cwd = os.getcwd()
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmp.cleanup)
|
||||
os.chdir(tmp.name)
|
||||
self.addCleanup(os.chdir, cwd)
|
||||
with open("todo.md", "w", encoding="utf-8") as f:
|
||||
f.write(TODO)
|
||||
|
||||
def run_main(self, *findings):
|
||||
with open("report.json", "w", encoding="utf-8") as f:
|
||||
f.write(report(*findings))
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
code = file_vulnerabilities.main(["report.json", "todo.md"])
|
||||
with open("todo.md", encoding="utf-8") as f:
|
||||
return code, out.getvalue(), f.read()
|
||||
|
||||
def test_a_clean_report_passes_and_leaves_the_plan(self):
|
||||
self.assertEqual(self.run_main(), (0, "No known vulnerabilities.\n", TODO))
|
||||
|
||||
def test_a_finding_fails_and_files_it(self):
|
||||
code, out, todo = self.run_main(*STDLIB)
|
||||
self.assertEqual((code, out), (1, "2 known vulnerabilities; todo.md item 7 plans the fix.\n"))
|
||||
self.assertIn("| 7 | 0.2.0 | security |", todo)
|
||||
|
||||
def test_findings_in_two_modules_name_both_items(self):
|
||||
_, out, _ = self.run_main(*STDLIB, *TEXT)
|
||||
self.assertEqual(out, "3 known vulnerabilities; todo.md items 7, 8 plan the fix.\n")
|
||||
|
||||
def test_a_plan_it_cannot_read_exits_2(self):
|
||||
with open("todo.md", "w", encoding="utf-8") as f:
|
||||
f.write(TODO.replace("`Next ID = 7`", ""))
|
||||
with redirect_stderr(io.StringIO()) as err:
|
||||
self.assertEqual(self.run_main(*STDLIB)[:2], (2, ""))
|
||||
self.assertEqual(err.getvalue(), "todo.md has no `Next ID = N` line to number the item by\n")
|
||||
|
||||
def test_one_finding_already_planned_still_fails(self):
|
||||
self.run_main(*TEXT)
|
||||
self.assertEqual(self.run_main(*TEXT)[:2], (1, "1 known vulnerability, already planned in todo.md.\n"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user