spec milestone 21: re-verify anchors against m20, record authority-mode interaction
Gates / test (push) Successful in 1m27s
Gates / test-aarch64 (push) Successful in 5m56s
Gates / frontend (push) Successful in 49s
Gates / package (push) Successful in 3m54s
Gates / container (push) Failing after 2m20s
CI / gates (push) Failing after 14m27s

This commit is contained in:
2026-08-11 23:37:20 +02:00
parent d76afc147a
commit 79a161e73a
+54 -26
View File
@@ -5,10 +5,10 @@ entries scoped below operator rules and above list blocks (tier 1), and add a
`regex` rule kind for operator rules backed by a homegrown linear-time engine
(tier 2). Nothing else from the ABP syntax enters scope.
Design written 2026-08-09 against HEAD `ffc3ca6`. Every `file:line` anchor
below was read at that commit. The milestone amends PLAN §2.2, which currently
rules regex out permanently; the amendment is part of session S3, not a
side effect.
Design written 2026-08-09 against HEAD `ffc3ca6`; every anchor re-verified
2026-08-11 against `a8e0fe4` after milestone 20 landed. The milestone amends
PLAN §2.2, which currently rules regex out permanently; the amendment is part
of session S3, not a side effect.
## Implementation contract (read first)
@@ -17,7 +17,7 @@ side effect.
only `std` (`src/filter/parsers.zig:1-14`). The new `src/filter/regex.zig`
obeys the same constraint.
- Every new `src/**.zig` file must be listed in `src/tests.zig`
(`build.zig:495-540` fatals otherwise).
(`build.zig:494-539` fatals otherwise).
- Frozen DDL is frozen: schema changes are new migration steps
(`src/storage/config_schema.zig:1-6`, `src/storage/migrations.zig:21-22`).
- After any API shape change, regenerate the contract samples
@@ -65,19 +65,21 @@ covers the bodies in order list, wild, allow — because SHA-256 of
already-published checksum stays valid, and `Manager.loadSource`
(`src/filter/manager.zig:543-598`) treats a missing `<id>.allow` file as an
empty body. No refetch is forced by upgrading. `bodyChecksum`
(manager.zig:1564) follows the same order; `source_file_suffixes`
(manager.zig:1581) gains `.allow.tmp` and `.allow` with longest-suffix-first
order preserved; the on-disk header (manager.zig:222-242) gains
(manager.zig:1572) follows the same order; `source_file_suffixes`
(manager.zig:1589) gains `.allow.tmp` and `.allow` with longest-suffix-first
order preserved; the on-disk header (manager.zig:221-241) gains
`# exceptions {d}` after the `# wildcards` line and the pinning test at
manager.zig:1872-1900 is extended, not weakened.
manager.zig:1914-1946 is extended, not weakened.
### 4. Exception counts persist and surface
Migration step 3 (`ddl_v3`, appended at `src/storage/migrations.zig:23-26`):
`ALTER TABLE blocklist_sources ADD COLUMN exception_count INTEGER NOT NULL
DEFAULT 0;`. `sources_repo.SourceRow` and `updateSourceStats`
(`src/storage/repositories/sources_repo.zig:91-166`) carry it;
`SourceStatus` rehydration (`manager.zig:1457-1494`) restores it alongside the
(`src/storage/repositories/sources_repo.zig:80-93,159-169`) carry it, and the
checksum doc line at sources_repo.zig:100 is updated alongside
`bodyChecksum`'s per ruling 3;
`SourceStatus` rehydration (`manager.zig:1458-1502`) restores it alongside the
existing three counts; `StatusView` (`src/web/handlers/blocklists.zig:52-78`)
gains `exceptions: u32`; the blocklists UI shows it where `skipped_regex`
already shows (`web/src/features/blocklists/SourceStatusSection.tsx`,
@@ -112,11 +114,15 @@ time). The engine is a fuzz-module root like parsers.zig and imports only
Migration step 4 (`ddl_v4`): the 12-step rebuild of `rules` with
`CHECK(kind IN ('exact','wildcard','regex'))` — the frozen v1 DDL
(`src/storage/config_schema.zig:65-72`) cannot be edited. `model.RuleKind`
(`src/config/model.zig:253-269`) gains `.regex`; the exhaustive switches in
`config/validate.zig:1067-1098` (compile the pattern, report
(`src/storage/config_schema.zig:65-72`) cannot be edited. The rebuilt table
keeps the name `rules`: `config_schema.table_names`
(config_schema.zig:112-119) and the invariant tests at
config_schema.zig:120-127 and migrations.zig:356 assert the schema's table
set, and a rename would fail both. `model.RuleKind`
(`src/config/model.zig:259-275`) gains `.regex`; the exhaustive switches in
`config/validate.zig:1099-1130` (compile the pattern, report
`"... is not a valid regex pattern"` through the existing error path at
validate.zig:862-869) and `src/filter/rules.zig:62-68` extend. `RuleSet`
validate.zig:891-902) and `src/filter/rules.zig:62-68` extend. `RuleSet`
(`rules.zig:28-36`) grows `regex_allow` and `regex_block` slices holding
compiled `Program`s plus their pattern texts (for `Decision.matched`);
`bucketOf` (rules.zig:133-143) becomes a six-bucket layout;
@@ -135,12 +141,12 @@ the per-query cost lands on cache misses only.
`src/web/handlers/rules.zig`: `toInput` accepts `"regex"`; the 400 string at
rules.zig:42 becomes `"kind must be 'exact', 'wildcard' or 'regex'"`.
`src/web/openapi.yaml:1875-1917`: all three `enum: [exact, wildcard]` become
`src/web/openapi.yaml:1948,1962,1976`: all three `enum: [exact, wildcard]` become
`[exact, wildcard, regex]`. `web/src/lib/types.ts:195`:
`RuleKind = "exact" | "wildcard" | "regex"`; the rules page kind selector
gains the option. Contract samples regenerated. `nxdns export` / `import`
round-trip the new kind with no extra work once `RuleKind.toDb/fromDb` extend
— the existing round-trip test at model.zig:722-723 is extended to prove it.
— the enum round-trip test at model.zig:782 is extended to prove it.
### 8. PLAN amendments land with the code, in S3
@@ -151,10 +157,28 @@ counted and skipped; `$` modifiers (except the `$important` suffix of ruling
out. PLAN.md:26 (§2.1 filtering sentence), PLAN.md:106 (§3.9), PLAN.md:108-117
(§3.10 precedence) and PLAN.md:700 (decision B) are updated to match rulings
2 and 6. In-code echoes of the old §2.2 move with it:
`src/filter/wildcard.zig:6-8,22-24`, `src/filter/parsers.zig:25`,
`src/filter/wildcard.zig:6-7,20-23`, `src/filter/parsers.zig:25`,
`src/filter/parser_abp.zig:5-6`, `src/filter/compiler.zig:129-131`.
### 9. Fuzz invariants move, never lapse
### 9. Milestone 20's authority modes bound where rules are written
m20 classifies POST/PUT/DELETE `/api/rules` as `.config_write`
(`src/web/routes.zig:94-97`), and under `.managed_file` authority the router
answers 403 (`src/web/router.zig:173-178`). Everything in this milestone
works in both modes, but the write path differs: in `.database` mode regex
rules arrive through the API; in `.managed_file` mode they arrive through the
config file and `src/config/reconcile.zig` (`reconcileRules`,
reconcile.zig:566-598), whose enum comparison carries `.regex` with no code
change. S3 owns a reconcile test proving a file-declared regex rule converges
into the table. Every S3 API acceptance check runs a server in `.database`
authority (no `--config`). The `SELECT *` dump helpers
(`src/config/import.zig:167-186`, `src/config/reconcile.zig:981`) will emit
the new `exception_count` column after `ddl_v3`; golden dump assertions in
those suites are updated in S1, which owns that migration. Reconcile's
runtime-column protection (reconcile.zig:11-15,435) covers `exception_count`
with no change — the column survives reconciles untouched.
### 10. Fuzz invariants move, never lapse
`tests/fuzz/blocklist_fuzz.zig` header invariant "covers_apex only on
`.wildcard`" (its stated form at :4-28) becomes "only on `.wildcard` or
@@ -183,7 +207,10 @@ Owns: `src/filter/parsers.zig`, `src/filter/parser_abp.zig`,
sentence only), `src/web/openapi.yaml` (StatusView shape only),
`web/src/features/blocklists/*`, `web/src/lib/types.ts` (source-stat fields
only), `web/src/lib/contractSamples.gen.ts`,
`tests/fuzz/blocklist_fuzz.zig`, `tests/fuzz/compiler_fuzz.zig`.
`tests/fuzz/blocklist_fuzz.zig`, `tests/fuzz/compiler_fuzz.zig`, and the
dump-golden assertions in `src/config/import.zig` and
`src/config/reconcile.zig` test suites only (ruling 9's `exception_count`
fallout from `ddl_v3`; S1 touches no reconcile logic).
- S1.1 `Kind.exception` in parsers.zig; parser_abp emits it per ruling 1;
parser_hosts and parser_domains never emit it (no change beyond the enum).
@@ -191,7 +218,7 @@ only), `web/src/lib/contractSamples.gen.ts`,
- S1.3 manager: suffixes, header line, `bodyChecksum`, `loadSource`
missing-file-is-empty, `Snapshot.Compiled.allow_body`,
`prepareRefresh`/`publishRefresh`/`applyLoadOutcomes` carry the count.
`rejectedWithoutEntries` (manager.zig:1559-1562) treats a compile with only
`rejectedWithoutEntries` (manager.zig:1563-1570) treats a compile with only
exceptions as loadable, not rejected.
- S1.4 matcher: `SourceSets.exceptions`, `Reason.blocklist_exception`,
the new evaluate level per ruling 2, `memoryBytes` includes the new sets.
@@ -227,7 +254,7 @@ target only).
matching; negated classes; `{n,m}` bounds; each error case; the
pathological backtracker-killers (`(a+)+b` against `aaaaaaaaaaaaaaaaaaaaX`,
nested alternation) complete within the step bound.
- S2.3 the fuzz target per ruling 9.
- S2.3 the fuzz target per ruling 10.
Acceptance (S2):
- [ ] `zig build test` passes with the new file in `src/tests.zig`.
@@ -242,9 +269,10 @@ Owns: `src/storage/migrations.zig` (step 4), `src/storage/config_schema.zig`
`src/storage/repositories/rules_repo.zig`, `src/web/handlers/rules.zig`,
`src/web/handlers/mutations.zig` (only if `checkRule` needs the kind),
`src/web/openapi.yaml`, `web/src/features/rules/*`, `web/src/lib/types.ts`,
`web/src/lib/contractSamples.gen.ts`, `PLAN.md`, `src/filter/wildcard.zig`
(comments), `src/filter/parsers.zig` (comment), `src/filter/parser_abp.zig`
(comment), `src/filter/compiler.zig` (comment), `tools/bench.zig`.
`web/src/lib/contractSamples.gen.ts`, `src/config/reconcile.zig` (test per
ruling 9), `PLAN.md`, `src/filter/wildcard.zig` (comments),
`src/filter/parsers.zig` (comment), `src/filter/parser_abp.zig` (comment),
`src/filter/compiler.zig` (comment), `tools/bench.zig`.
- S3.1 migration step 4 per ruling 6; repo and model layers.
- S3.2 validate at both edges (config import, API) per rulings 6 and 7.
@@ -280,7 +308,7 @@ and `/api/lookup`. Record deviations in `## Recorded (implementation)`.
New files:
- `src/filter/regex.zig` — the linear-time engine (ruling 5).
- `tests/fuzz/regex_fuzz.zig` — its fuzz target (ruling 9).
- `tests/fuzz/regex_fuzz.zig` — its fuzz target (ruling 10).
Deleted surface: none.