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
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:
+54
-26
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user