From 79a161e73a45b58eec096b4868ae6f53adf54464 Mon Sep 17 00:00:00 2001 From: m5r Date: Tue, 11 Aug 2026 23:37:20 +0200 Subject: [PATCH] spec milestone 21: re-verify anchors against m20, record authority-mode interaction --- specs/milestone-21.md | 80 +++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/specs/milestone-21.md b/specs/milestone-21.md index 0e79cf9..457b44f 100644 --- a/specs/milestone-21.md +++ b/specs/milestone-21.md @@ -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 `.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.