Skip to content

fix(panels,flags): name the page after its subscriber on PasarGuard and Rebecca; read alpha-2 codes - #9

Merged
iitzSeriZdev merged 10 commits into
mainfrom
fix/v1.3.0-pasarguard-identity-and-flags
Sep 27, 2026
Merged

iitzSeriZdev merged 10 commits into
mainfrom
fix/v1.3.0-pasarguard-identity-and-flags

Conversation

@iitzSeriZdev

@iitzSeriZdev iitzSeriZdev commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Summary

Three defects found while hardening 1.3.0 against the panels' own source, plus the audit trail for all three, plus a Linux CI workflow that actually runs the release checks. Nothing changes for an existing 3X-UI install; no adapter and no installer file is touched.

1. PasarGuard: the page titled itself "Subscription"

PasarGuard's Jinja2 page context is exactly {user, links, announce, announce_url, apps} plus now — the panel-wide subscription settings are not in it. The prelude set subTitle = '', so every PasarGuard page fell back to the generic word.

The page is now named by the admin's configured profile title when there is one, and by the subscriber (user.username) otherwise — the order the panel itself resolves a title in (SubscriptionOperation._format_profile_title prefers the admin's profile_title over sub_settings.profile_title).

  • user.username is the only deterministic subscriber identifier in the context (email does not exist; id, note, subscription_url, auto_delete_in_days are exclude=True).
  • user.ip is still never read.
  • A profile title that still carries a format placeholder ({DATA_LIMIT}, {EXPIRE_DATE}, …) is refused rather than printed literally — the page cannot resolve those variables.
  • Every value is escaped by the {%- autoescape true -%} block that already wraps the island.

2. Rebecca 1.x: the page titled itself "Subscription" too

Same symptom, different cause, and the cause is the interesting part: Rebecca's page context carries no profile title at all.

  • subscriptionHeaders() writes the panel-wide subscription_profile_title into the /info header — profile-title: base64:… (internal/app/user/subscription.go:1120). That is the value the adapter reads into its title field.
  • The page render path is separate: renderSubscriptionHTML() calls renderSubscriptionPageTemplate(...) (:1131, :1180), and subscriptionTemplateContext() (:3742) passes only {user, links, links_text, usage_url, support_url, token, current_timestamp, remaining_days} plus the VPN keys.

So the adapter's header-derived title is correct but invisible to the page, and the page had nothing to read. subTitle is now derived from the only in-context identity, user.username:

{%- set subTitle = "" -%}
{%- if user.username|wordcount > 0 -%}{%- set subTitle = user.username -%}{%- endif -%}
  • The gate is wordcount > 0, not != "": pongo2 v6.1.0 has no trim and no string filter, and wordcount is 0 for nil, empty and whitespace-only without ever raising — so a missing, empty or blank name keeps the generic fallback instead of printing an empty title.
  • user.username is validated by Rebecca itself against ^[a-zA-Z0-9._@-]+$, 3–32 characters (internal/app/user/validation.go:15,308-310). Nothing else is read — no id, no token, no subscription URL, no internal value.
  • No {-placeholder guard is needed on this path (unlike PasarGuard): Rebecca never runs a title through str.format_map, and { is not in the username charset.
  • pongo2 autoescapes by default and the island is already wrapped in {%- autoescape on -%}, so the value is escaped, never markup.
  • subSupportUrl = support_url was already correct and is unchanged.

PasarGuard is untouched by this fix — no PasarGuard prelude, context or test changed.

3. Country flags: a node name carrying an ISO alpha-2 code found no flag

flagOf only recognised a regional-indicator pair, so TR | Istanbul, RU-01, GB-LON-1, FI-01, DE returned '' and the row fell to the monogram (T-I, R-U, G-B, F-I) — which reads as the code.

This is an input extension only: the return shape is still the pair, cfg.flag is still '🇩🇪', CODES is still the validity source, the monogram fallback and cleanName are unchanged. A token is read as a country only when it is uppercase, stands alone between non-letters, and names a real country in the same 258-code registry. LON, USA, A1B, ZZ-01 and the lowercase English words that are themselves codes (no, it, us, in) are all refused. An explicit emoji still wins and the label is never rewritten.

Country/city names are not resolved — a name table is well over 1 KB against 95 B of headroom, and guessing from an ambiguous name is worse than a monogram. The tests assert the refusal. A node named only by a city (Istanbul, Frankfurt) therefore still gets a monogram in v1.3.0; that is a documented fallback, not a defect, and docs/design/FLAG-RENDERER-AUDIT.md §17.5 records the boundary and the three reasons for it. This change does not widen name detection.

Design decision worth reviewing

A separate subscriber_display_name island field was designed and rejected on measurement: a fourteenth data-* attribute plus its model and render paths costs ~157 B, the flag fix costs 179 B, and pulsenova had 274 B of headroom against a ceiling that may not be raised. The prelude route costs zero artifact bytes, leaves 3X-UI's Go view model untouched (a missing Go map key would print the literal "<no value>" into every 3X-UI page), and keeps the PasarGuard assumption out of shared rendering. Reasoning is recorded in the prelude headers.

Artifact impact

All 15 3X-UI artifacts are re-baselined: +179 B each, because flag.js is in the shared app script. pulsenova is 204,705 B with 95 B of headroom, inside the 204,800 B ceiling. The ceiling was not raised and no other bytes were reclaimed.

The panel shells are not size-locked — FROZEN_ARTIFACTS covers only the 13 3X-UI designs — so the Rebecca prelude change moves no locked byte.

Tests

  • tests/panels-engines.test.mjs +16 (15 → 31): +9 for PasarGuard (through the shipped prelude on the real Jinja2 — subscriber name; admin profile title over it; a placeholder refused; a missing name keeping the fallback; no admin; hostile HTML; a non-Latin name; a 128-character name; the page context's exact key set) and +7 for Rebecca (through the shipped prelude on the real pongo2 — the page names the subscriber; the /info header title does not reach the page; a missing name keeps the generic fallback across null/undefined/''/' '; a Persian name is carried as text; a hostile name is escaped and never becomes markup; a 32-character name is carried whole, not truncated; the page context's exact key set, proving no profile title is in it).
  • tests/flag.test.mjs +7 (13 → 20) — the reported cases; all 676 letter pairs against the 258-code registry; the uppercase/bounded/assigned refusals; emoji-beats-code; a re-assertion that no previous answer moved; the country-name boundary.
  • tests/build.test.mjs — the 4 individual byte-locks and the 13-row FROZEN_ARTIFACTS table.
  • tools/engines.mjs — the PasarGuard page context can state the admin's own columns (native.admin), because the page context is not the /info payload.

Results — Linux (GitHub Actions)

.github/workflows/release-validation.yml (new) runs the release checks on ubuntu-latest — Node 22, Go 1.22, python3-jinja2, ShellCheck — with each of the last three steps guarded by if: ${{ !cancelled() }}, so all four report even when an earlier one fails.

Run 36328376214, head a6c3f51:

Command Result
npm test fail — # tests 784, # pass 781, # fail 3, 70.6 s
npm run verify ok — all checks passed
npm run lint:sh ok — shellcheck: 12 scripts clean
npm run build:panels ok — 51 shell(s) written to dist/panels/ and dist/shells/, G1 green — the Go path is lossless

Before this workflow the release checks had never run on Linux; lint:sh had never run at all.

The three failures, isolated

# Test Assertion Present at
1 install, verify, rebrand, switch design, roll back and uninstall on a PasarGuard host installer-panel-pasarguard.test.mjs:378 7e9d705 (this PR's first commit)
2 without sqlite3, activation places the page and says exactly what to set installer-panel-rebecca.test.mjs:425 ebb10a5 (main)
3 install, verify, rebrand, switch design, roll back and uninstall on a Rebecca host installer-panel-rebecca.test.mjs:440 7e9d705 (this PR's first commit)

Measured, not assumed:

Tree Run Result
main @ ebb10a5 36326235588 761 tests, 760 pass, 1 fail (#2 only)
this PR @ 7e9d705 (before the Rebecca fix) 36327603350 777 tests, 774 pass, 3 fail (the same three)
this PR @ 523441f (with the Rebecca fix) 36328085719 784 tests, 781 pass, 3 fail (the same three)
this PR @ a6c3f51 (current head) 36328376214 784 tests, 781 pass, 3 fail (the same three)

So the Rebecca fix adds 7 tests and 0 new failures, and #2 is pre-existing on main. The two commits added after 523441f (a CHANGELOG entry and a workflow path) move no test count and change no result.

Why #1 and #3 fail on a networked runner

rt_complete_install (installer/lib/row-template.sh:2573) finishes an install that is short of what its own version ships: when the store is missing designs, it downloads the release pinned to the installed version (…/releases/download/v1.3.0) and rebuilds the store from it.

Both tests install a deliberately two-design payload (ids: ['row', 'editorial']), so completion fires, the store ends up holding the released row artifact, and rt_cmd_verify compares it against $RT_DIST, which came from the locally built payload:

FAIL canonical artifact does not match the selected template (row).

On main the two are byte-identical, because v1.3.0 was cut from main. This PR changes flag.js and template/index.html, so the built artifact no longer equals the released one, and the comparison fails. It reproduces only where the release is reachable — on a machine that cannot reach GitHub, completion warns and leaves the store alone, so the tests pass.

This is a harness assumption — the working tree builds the latest release — rather than a product regression: a real install completes from the release that ships with its own installer. It is worth knowing that the assumption breaks for any artifact-changing PR, and will keep failing on main after this merges until a new release is cut. tests/installer-template-store.test.mjs:395 already shows the hermetic form (stub rt_fetch_release); this PR does not touch it, to keep the change to its subject.

Docs

  • docs/design/PGCLOCK-AUDIT.md (new) — read-only audit of the third-party PGClock template: it confirms user.username as the exposed identity, has no service-name/support-URL/flag logic, and is unlicensed, so nothing was derived and PROVENANCE.md is unchanged.
  • docs/design/PASARGUARD-ADAPTER-AUDIT.md §8 — the page context is not the /info payload.
  • docs/design/REBECCA-ADAPTER-AUDIT.md §2 — the same distinction for Rebecca, against subscriptionHeaders and subscriptionTemplateContext: there is no profile title in the page context, so the page's title and the adapter's header-derived title may legitimately differ.
  • docs/design/PANEL-COMPATIBILITY-AUDIT.md §5 — the subTitle row in the mapping table is the /info mapping, not the page's.
  • docs/design/FLAG-RENDERER-AUDIT.md §17 — the alpha-2 path, its cost, and its boundary.

…lpha-2 codes

Two defects found while hardening 1.3.0 against the panels' own source, plus the
audit trail for both. Nothing changes for an existing 3X-UI install, and no
adapter or panel-adapter behaviour is involved.

PasarGuard: the page titled itself "Subscription"

PasarGuard's page context is `{user, links, announce, announce_url, apps}` plus
Jinja2's `now` -- the panel-wide subscription settings are NOT in it. The prelude
read `subTitle` from nothing, so every PasarGuard page fell back to the generic
word. The page is now named by the admin's configured profile title when there is
one, and by the subscriber (`user.username`) otherwise -- the order the panel
itself resolves a title in (`SubscriptionOperation._format_profile_title` prefers
the admin's `profile_title` over `sub_settings.profile_title`).

A profile title that still carries a PasarGuard format placeholder
(`{USERNAME}`, `{DATA_LIMIT}`, ...) is refused rather than printed literally: the
page cannot resolve those variables, and a literal placeholder is worse than the
subscriber's own name. `user.ip` is still never read.

`user.username` is the only deterministic subscriber identifier in the context
(`email` does not exist; `id`, `note`, `subscription_url` and
`auto_delete_in_days` are `exclude=True`), and `UserValidator.validate_username`
bounds it to 3-128 characters of `[a-zA-Z0-9-_@.]`.

A separate `subscriber_display_name` island field was designed and rejected on
measurement, not on taste: a fourteenth `data-*` attribute plus its model and
render paths costs ~157 B, the flag fix below costs 179 B, and `pulsenova` had
274 B of headroom against a ceiling that may not be raised. The two do not fit
together. The prelude route costs zero artifact bytes, leaves 3X-UI's Go view
model untouched (a missing Go map key would print the literal "<no value>" into
every 3X-UI page), and keeps the PasarGuard assumption out of the shared
rendering. The reasoning is recorded in the prelude header.

Country flags: a node name carrying an ISO alpha-2 code found no flag

`flagOf` only ever looked for a regional-indicator pair, so `TR | Istanbul`,
`RU-01`, `GB-LON-1`, `FI-01` and `DE` returned '' and the row fell to the
monogram -- `T-I`, `R-U`, `G-B`, `F-I` -- which reads as the code.

This is an INPUT extension only. The return shape is still the regional-indicator
pair, `cfg.flag` is still `'🇩🇪'`, `CODES` is still the validity source, the
monogram fallback is unchanged and `cleanName` is unchanged, so the Phase 0.6
correction ("no alpha-2 change", which is about the return value) still holds.
The gradient workstream's "flagOf() unchanged" row is not a freeze on the input
contract.

A two-letter token is read as a country only when it is UPPERCASE, stands alone
between non-letters, and names a real country in the SAME 258-code registry the
emoji path uses. `LON` out of `GB-LON-1`, `USA`, `A1B`, `ZZ-01` and the lowercase
English words that are themselves assigned codes (`no`, `it`, `us`, `in`) are all
refused. An explicit flag emoji still wins, and the displayed name is never
rewritten -- an emoji means a flag and nothing else, but two letters can be an
operator's own wording.

Country names and city names (`Turkey - Istanbul`, `Türkiye`, `Finland Helsinki`)
still draw the monogram. A name table is well over 1 KB against 95 B of remaining
headroom, and guessing from an ambiguous name is worse than a monogram; the tests
assert the refusal so the boundary is recorded rather than implied.

All 15 artifacts are re-baselined: +179 B each, because `flag.js` is in the
shared app script. `pulsenova` is 204,705 B with 95 B of headroom, inside the
204,800 B ceiling. The ceiling was not raised and no other bytes were reclaimed.

Tests

- `tests/flag.test.mjs` (+7): the reported cases; all 676 letter pairs against the
  258-code registry; the uppercase/bounded/assigned refusals; emoji-beats-code; a
  re-assertion that no previous answer moved; the country-name boundary. The
  existing 13 tests pass verbatim.
- `tests/panels-engines.test.mjs` (+10): the subscriber name; the admin's profile
  title over it; a format placeholder refused; a missing name keeping the generic
  fallback; no admin at all; hostile HTML; a non-Latin name; a 128-character name;
  and an assertion that the page context carries exactly
  `{user, links, announce, announce_url, now}`.
- `tests/build.test.mjs`: the 4 individual byte-locks and the 13-row
  `FROZEN_ARTIFACTS` table.
- `tools/engines.mjs`: the PasarGuard page context can now state the admin's own
  columns (`native.admin`), because the page context is not the /info payload --
  the headers carry the values the panel resolved, the context carries the
  admin's columns and nothing else.

Docs

- `docs/design/PGCLOCK-AUDIT.md` (new): read-only audit of the third-party PGClock
  template. It independently confirms `user.username` as the exposed subscriber
  identity and the `.env` + `CUSTOM_TEMPLATES_DIRECTORY` install mechanism; it has
  no service-name, support-URL or flag logic; and it is unlicensed (no LICENSE
  file, no header), so nothing was derived from it and `PROVENANCE.md` is
  unchanged.
- `docs/design/PASARGUARD-ADAPTER-AUDIT.md` §8: the page context is not the /info
  payload.
- `docs/design/FLAG-RENDERER-AUDIT.md` §17: the alpha-2 path, its cost, and the
  boundary it does not cross.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Fixes PasarGuard page titling so a page names itself by the admin's configured profile title, falling back to the subscriber (user.username) when there is none, and refuses a title still carrying an unresolved PasarGuard format placeholder rather than printing it literally. Extends flagOf with an input-side alpha-2 path so an uppercase two-letter token that stands alone between non-letters and is in the 258-code CODES registry (TR | Istanbul, RU-01, GB-LON-1, DE) resolves to a country flag, while lowercase words, city fragments, unassigned pairs, and country/city names still draw the monogram and the displayed name is never rewritten; an explicit emoji still wins. Re-baselines all 15 byte-locks (+179 B each, leaving pulsenova with 95 B headroom) and adds the PGClock, PasarGuard-page-context, and alpha-2 audit sections documenting that the return shape, CODES, and fallbacks are unchanged.

Worth a look

  • v1.1.0 Row byte-lock was moved to a new artifact — tests/build.test.mjs:140 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Alpha-2 token regex treats lowercase letters as separators — docs/design/FLAG-RENDERER-AUDIT.md:1025 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 228 functions depend on the 173 functions this change touches.

Health — this change adds coupling hotspots:

  • new: setAttr() — 20 callers, 4 callees
  • new: island() — 3 callers, 7 callees
  • new: copyValue() — 3 callers, 6 callees
  • new: island() — 3 callers, 6 callees
  • new: applyLabels() — 2 callers, 8 callees
  • new: applyRows() — 3 callers, 5 callees
  • new: renderExplorer() — 5 callers, 3 callees
  • new: render() — 2 callers, 7 callees
  • …and 19 more — each is listed as a finding

Verification — 228 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 205 function(s) in the blast radius were not formally verified this run

Docs that may be stale (advisory)

…and 10 more.

Formal verification

Could not verify: Could not verify flagOf.

The verifier did not have enough to check flagOf, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous

Could not verify: Could not verify b64.

The verifier did not have enough to check b64, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: could not isolate function

· 1 grounded finding(s) anchored inline below; 26 more finding(s) on lines outside this diff (see the check run).

Comment thread src/scripts/flag.js
@@ -98,7 +111,8 @@ function letter(cp) {
spurious code — "🇿🇿🇺🇸" reads past the tofu pair and finds US, it does not
read the middle two as ZU. A lone trailing indicator matches nothing. */
export function flagOf(text) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — flagOf()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Rebecca's page titled itself with the generic word "Subscription". Its render
context carries no profile title at all: subscriptionHeaders() writes the
panel-wide subscription_profile_title into the /info `profile-title` HEADER, and
renderSubscriptionHTML -> renderSubscriptionPageTemplate never passes it, so the
title the adapter reads from that header is invisible to the page. The one
identity the page does receive is user.username, so that now names the page --
the same fallback the PasarGuard prelude takes when no profile title is
configured.

A value with no word in it at all (absent, empty, whitespace-only) is not a name
and the page keeps its generic fallback; the value is never rewritten. pongo2
has no trim filter and no string filter, so the test is `wordcount`, which is 0
for every one of those forms and never errors. No brace guard is needed here,
unlike PasarGuard: Rebecca never runs a title through str.format_map.

PasarGuard and 3X-UI are untouched, and no artifact byte-lock moves: flag.js and
the 3X-UI layouts are not involved, and no panel shell is size-locked.

Tests: tests/panels-engines.test.mjs gains seven cases through the SHIPPED
prelude on the real pongo2 -- the subscriber names the page; the /info header
title does not reach the page; a missing name keeps the fallback; a non-Latin
name is carried as text; a hostile name is escaped and never becomes markup; a
long name is carried whole; and the page context really carries no profile
title. rbExpected() no longer pins the page title to the header-derived ''.

CI: .github/workflows/release-validation.yml runs the four release commands on
ubuntu-latest, with the three tools the suite needs and a Node runtime does not
provide -- Go, Python 3 with Jinja2, and ShellCheck. The existing Docs workflow
never ran any of them, so a green check on a pull request said nothing about the
suite.

Docs: REBECCA-ADAPTER-AUDIT.md §2, PANEL-COMPATIBILITY-AUDIT.md §5 and the
CHANGELOG record the page-vs-header distinction. FLAG-RENDERER-AUDIT.md §17.2 and
§17.6 are corrected to the real test counts (13 -> 20, +7).

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

No blocking issues surfaced.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 334 functions depend on the 279 functions this change touches.

Health — this change adds coupling hotspots:

  • new: setAttr() — 20 callers, 4 callees
  • new: island() — 3 callers, 7 callees
  • new: copyValue() — 3 callers, 6 callees
  • new: island() — 3 callers, 6 callees
  • new: applyLabels() — 2 callers, 8 callees
  • new: applyRows() — 3 callers, 5 callees
  • new: renderExplorer() — 5 callers, 3 callees
  • new: render() — 2 callers, 7 callees
  • …and 19 more — each is listed as a finding

Verification — 334 functions in the blast radius were not formally verified this run (proofs are advisory here).

Health delta baseline: last indexed commit ebb10a5 (diverged from this PR's base — delta is approximate).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 311 function(s) in the blast radius were not formally verified this run

Docs that may be stale (advisory)

…and 10 more.

· 1 grounded finding(s) anchored inline below; 26 more finding(s) on lines outside this diff (see the check run).

Comment thread src/scripts/flag.js
@@ -98,7 +111,8 @@ function letter(cp) {
spurious code — "🇿🇿🇺🇸" reads past the tofu pair and finds US, it does not
read the middle two as ZU. A lone trailing indicator matches nothing. */
export function flagOf(text) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — flagOf()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

The only conflict was one CHANGELOG heading: main renamed
"## [1.3.0] - 2026-09-26" to "## [1.3.0] - Unreleased" (1.3.0 has not
shipped), while this branch had added a "## [Unreleased]" section directly
above it. Both sides are kept: the branchs hardening section stays, and mains
newer heading wins. Nothing else conflicted, and main changed no file under
src/, tests/, tools/, installer/ or template/, so the branches test results
stand unchanged.

This also unblocks CI: GitHub does not run pull_request workflows while a pull
request has a merge conflict, which is why the new Release validation workflow
had not started.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

No blocking issues surfaced.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 334 functions depend on the 279 functions this change touches.

Health — this change adds coupling hotspots:

  • new: setAttr() — 20 callers, 4 callees
  • new: island() — 3 callers, 7 callees
  • new: copyValue() — 3 callers, 6 callees
  • new: island() — 3 callers, 6 callees
  • new: applyLabels() — 2 callers, 8 callees
  • new: applyRows() — 3 callers, 5 callees
  • new: renderExplorer() — 5 callers, 3 callees
  • new: render() — 2 callers, 7 callees
  • …and 19 more — each is listed as a finding

Verification — 334 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 311 function(s) in the blast radius were not formally verified this run

Docs that may be stale (advisory)

…and 10 more.

· 1 grounded finding(s) anchored inline below; 26 more finding(s) on lines outside this diff (see the check run).

Comment thread src/scripts/flag.js
@@ -98,7 +111,8 @@ function letter(cp) {
spurious code — "🇿🇿🇺🇸" reads past the tofu pair and finds US, it does not
read the middle two as ZU. A lone trailing indicator matches nothing. */
export function flagOf(text) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — flagOf()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

iitzSeriZ added 2 commits September 27, 2026 18:28
A red npm test was hiding npm run verify, npm run lint:sh and
npm run build:panels. The job is about the four results.
main gained two commits that put the released date back on the 1.3.0
heading (`addd4df`, `13fb6c8`): v1.3.0 is published, tag `v1.3.0` ->
`e103f74`, and the heading had been changed to "Unreleased" by #10 after
the release. CHANGELOG.md conflicted, because this branch adds an
`## [Unreleased]` section of its own at the top of the file.

Resolved minimally: this branch's `## [Unreleased]` section (the
PasarGuard and Rebecca title fixes) stays at the top, and the 1.3.0
section keeps main's restored `## [1.3.0] - 2026-09-26` heading, so the
file matches the tagged release again. No other file conflicted and no
content was dropped from either side.
@iitzSeriZdev iitzSeriZdev changed the title fix(pasarguard,flags): name the page after its subscriber, and read alpha-2 codes fix(pasarguard,rebecca,flags): name the page after its subscriber on both panels, and read alpha-2 codes Sep 27, 2026

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

No blocking issues surfaced.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 334 functions depend on the 279 functions this change touches.

Health — this change adds coupling hotspots:

  • new: setAttr() — 20 callers, 4 callees
  • new: island() — 3 callers, 7 callees
  • new: copyValue() — 3 callers, 6 callees
  • new: island() — 3 callers, 6 callees
  • new: applyLabels() — 2 callers, 8 callees
  • new: applyRows() — 3 callers, 5 callees
  • new: renderExplorer() — 5 callers, 3 callees
  • new: render() — 2 callers, 7 callees
  • …and 19 more — each is listed as a finding

Verification — 334 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 311 function(s) in the blast radius were not formally verified this run

Docs that may be stale (advisory)

…and 10 more.

· 1 grounded finding(s) anchored inline below; 26 more finding(s) on lines outside this diff (see the check run).

Comment thread src/scripts/flag.js
@@ -98,7 +111,8 @@ function letter(cp) {
spurious code — "🇿🇿🇺🇸" reads past the tofu pair and finds US, it does not
read the middle two as ZU. A lone trailing indicator matches nothing. */
export function flagOf(text) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — flagOf()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@iitzSeriZdev iitzSeriZdev changed the title fix(pasarguard,rebecca,flags): name the page after its subscriber on both panels, and read alpha-2 codes fix(panels,flags): name the page after its subscriber on PasarGuard and Rebecca; read alpha-2 codes Sep 27, 2026

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

No blocking issues surfaced.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 335 functions depend on the 280 functions this change touches.

Health — this change adds coupling hotspots:

  • new: setAttr() — 20 callers, 4 callees
  • new: island() — 3 callers, 7 callees
  • new: copyValue() — 3 callers, 6 callees
  • new: island() — 3 callers, 6 callees
  • new: applyLabels() — 2 callers, 8 callees
  • new: applyRows() — 3 callers, 5 callees
  • new: renderExplorer() — 5 callers, 3 callees
  • new: render() — 2 callers, 7 callees
  • …and 19 more — each is listed as a finding

Verification — 335 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 312 function(s) in the blast radius were not formally verified this run

Docs that may be stale (advisory)

…and 10 more.

· 1 grounded finding(s) anchored inline below; 26 more finding(s) on lines outside this diff (see the check run).

Comment thread src/scripts/flag.js
@@ -98,7 +111,8 @@ function letter(cp) {
spurious code — "🇿🇿🇺🇸" reads past the tofu pair and finds US, it does not
read the middle two as ZU. A lone trailing indicator matches nothing. */
export function flagOf(text) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — flagOf()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

…lated

Both installer panel suites fail on ubuntu-latest and pass on the authoring
host, for two reasons that have nothing to do with the adapters they cover.
Both are harness defects, and neither is fixed by changing production code.

1. sqlite3 leaked in from the runner.

   `sqlite: false` meant only "write no sqlite3 shim". The adapters decide
   with `command -v sqlite3` (rt_panel_rebecca_db_ready), and ubuntu-latest
   ships a real sqlite3, so the gate opened anyway, activation took the
   automatic path, and the manual-activation test read

       inactive
       outcome=auto
       ...
       (Automatic activation needs the sqlite3 command and Rebecca's SQLite database.)

   where it expected `manual`. Shadowing sqlite3 with a shim that fails
   cannot fix this: the gate asks whether the command exists, not whether it
   works.

   `sqlite: false` now removes every directory that holds a sqlite3 from the
   child's PATH, replacing each with a mirror of itself (the same entries,
   symlinked, minus sqlite3) so nothing else on the host stops resolving.
   A new test plants a real-looking sqlite3 -- plus a second tool, to prove
   the substituted directory still carries what it held -- and asserts that
   `command -v sqlite3` fails in the child while the other tool resolves.

2. The network leaked in from the payload.

   The payload was two designs. RT_TEMPLATES_AVAILABLE names seventeen, so
   rt_complete_install always judged the install short of what its version
   ships and downloaded the release pinned to that version. On a runner with
   network the download succeeded, the template store was rebuilt from the
   PUBLISHED v1.3.0 release, and verify then compared that release's artifact
   against this working tree's:

       Completing the Row-Template 1.3.0 installation (designs and installer files)...
       ok   Installation completed: 17 design(s) available.
       FAIL canonical artifact does not match the selected template (row).

   The suites were therefore validating a published release against the
   branch, and doing it over the network -- which is the opposite of what
   they are for, and it made them depend on whatever v1.3.0 happened to
   contain.

   The payload is now the complete release (availableTemplateIds(), the same
   seventeen ids as RT_TEMPLATES_AVAILABLE), so rt_complete_install has
   nothing to complete and is never reached. curl and wget are denied in
   every fake host -- they record the attempt and fail -- so a regression
   fails the suite loudly instead of silently reaching the internet. The two
   life-cycle tests assert the recorded attempts are empty, so "this suite
   needs no network" is measured rather than assumed.

Production behaviour is untouched: the installer, the adapters and the
release tooling are byte-identical. The payload is a real release, and the
only thing that changed is that the tests now hand the installer all of it.
iitzSeriZ and others added 2 commits September 27, 2026 22:46
Two defects in the previous commit, both in test code. Found by the Linux
run: three failures down to one, and the one was this commit's own new
test.

bashRun spreads its first argument, so a string became one line per
character:

    "set -Eeuo pipefail\ns\n=\n0\n;\n \ne\nc\nh\no\n \nA"

The new sqlite3 test passed a joined string, so its script died under
`set -e` -- the first one-character line is a command that does not exist
-- before printing anything, and the assertion read an empty stdout
(`actual: ''`). Every other caller passes an array. bashRun now accepts
either shape, as the suites' own `run` helpers already do, so the
footgun cannot recur; the test passes an array, and reports the exit code
and stderr when it fails.

A mirror is now stamped once it is complete. A run killed by a signal (a
timeout, a cancelled job) never reaches the exit hook, and a half-built
mirror would otherwise be reused on the next run -- failing, if `bash`
were the entry that never got linked, in a way that has nothing to do
with sqlite3.
test(panels): make the panel life-cycle suites offline and sqlite-isolated
@iitzSeriZdev
iitzSeriZdev merged commit 4e436a4 into main Sep 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant