Skip to content

feat(project): short address for GitHub projects (G-A1b) - #183

Merged
trakhimenok merged 9 commits into
mainfrom
apps-g-a1b-route
Oct 3, 2026
Merged

trakhimenok merged 9 commits into
mainfrom
apps-g-a1b-route

Conversation

@trakhimenok

Copy link
Copy Markdown
Contributor

What this does

A GitHub project opens at datatug.app/project/github.com/<owner>/<repo>[/tree/<ref>[/<dir>]/-/<page>] with the existing project pages unchanged (task G-A1b, design datatug/backstage docs/design/demo-as-github-project.md 3.3, 3.4, 3.4a, 6.5). The old /store/github.com/project/<id>/… form still works, with no redirect (G-A1d stays held). Closes the three G-A1b items of #180.

Landing this deploys to datatug.app. Nothing visible changes for existing addresses (screenshots below), and the demo flag is not touched.

Flag-off coexistence rule (short route vs. the demo holding page)

Decided in apps/datatug-app/src/app/demo-handoff-asked.ts (showsHoldingPage), wired in datatug-app-routes.ts:52 (handoffOrRoot), DataTug profile only.

Address Result
/demo, /Demo, /demo;x=1, … (with any query or none) holding page, always (as today)
…/project/github.com/<o>/<r>/chat or …/tree/<ref>/-/chat that arrived with a question (msg, or q) holding page, as today: question echoed only for the trusted repo on its default branch, neutral wording for every other repo or ref; the query is stripped before analytics (index.html is untouched)
the same addresses without a question (no query, ?lang=ru, ?msg=, ?utm_source=x) the project's own chat page at its short address
every other short address (/project/github.com/<o>/<r>, …/queries, …/tree/HEAD/<dir>/-/chat, …) the project pages
Incidentius unchanged: hand-off addresses go to /; the short route does not exist there (unmatched, as before)

"Arrived with a question" is read where the route table is read from what index.html's script kept (the router never sees the query): the stash for this page load, else what this page load already settled, else (a reload or Back) the copy in sessionStorage for the same address; a fresh visit ignores an earlier visit's copy; blocked storage means "no question" (the page then shows the project, as the holding page shows its no-question copy after a reload today). For an address that may not echo its question, storage now keeps ?lang=xx&asked=1 instead of ?lang=xx, so a reload still shows the neutral holding page and not the project chat.

The flag (isDemoEnabled) is deliberately not read: demo-flag.spec.ts forbids the hand-off/route files from importing it, and the project chat cannot run a question under either flag value yet, so the holding page is the only way not to lose it. G-A4b/G-A5 turn this into !isDemoEnabled() when the chat can run msg (design 6.5 table). demo-flag.spec.ts now also pins that which page a hand-off address gets does not depend on the override.

Acceptance, item by item

Acceptance Where Proof
matcher route, project pages as children, supplies storeId and projectId libs/datatug/main/src/lib/routes/github-project-routes.ts:38 (githubProjectMatcher), :90 (githubProjectRoutes, lazy DatatugProjectRoutingModule), registered at datatug-routing.module.ts:107 ahead of store/:storeId github-project-routes.spec.ts:49 (consumed segments + canonical ids per row), :195 (real router: params reach the page, children open); prod-build screenshots of the short address identical to the old form
canonicalisation redirects of 3.4a github-project-address-check.ts:207 (decide), glue github-project-routes.ts:66 (githubAddressCanMatch: UrlTree, query and fragment kept) github-project-address-check.spec.ts:255 (every row: case, .git, tree/HEAD, tree/HEAD/-/page, blob/…/datatug-project.json, redirect target is itself canonical), github-project-routes.spec.ts redirect table and trailing slash; e2e demo-handoff.spec.ts "non-canonical spelling"
messages for unsupported inputs github-address-problem-page.component.ts:33 (messageForUnsupported), :72 (messageForNotFound), page :170 github-address-problem-page.component.spec.ts (every reason, as text, markup test); github-project-address-check.spec.ts:421 (file link, @ in dir/ref, - directory, no ref, .., bad owner/repo incl. U+212A), :469 (no project file, branch-with-slash hint, moved repo). Screenshots below.
default-branch lookup and its refused path github-project-address-check.ts:102 (GithubDefaultBranchLookup, one GET /repos/<o>/<r> per repo, only when the address names a ref), :148 (GithubAddressNotices, toast), :207 github-project-address-check.spec.ts:146 (found / 404 / moved / 403 / 429 / 5xx / bad JSON / over-cap / network / memo), :328 (default branch redirects to HEAD with dir and page, other versions open, refused leaves the address and tells the visit); real GitHub run: tree/main/demo-project-1 -> tree/HEAD/demo-project-1
a GitHub project opens at its short address with the pages unchanged as above + datatug-nav-context.service.ts:281 (legacyShapeOf: the side menu and nav context see the project) screenshots: overview, queries, chat of a GitHub project at the short address are pixel-identical to the old form on origin/main, at 1280 and 390; datatug-nav-context.service.spec.ts (new describe)
the old form still works, without a redirect store/:storeId untouched github-project-routes.spec.ts ("old form is untouched", "opens as before, no redirect, nothing asked of GitHub"); e2e "old form … no redirect"; old-form screenshots identical to main
Incidentius has no short route githubProjectRoutes canMatch + githubAddressCanMatch check the profile github-project-routes.spec.ts:382, datatug-app-routes.spec.ts ("Incidentius … goes to the root")

Issue #180, the items marked for G-A1b

Item Where Proof
replace isEchoTrusted with the real trust function on parsed values (Kelvin sign) demo-handoff-capture.ts:59 (isTrustedHandoff: parseProjectUrl + isTrustedProjectAddress; the local trusted list is gone) demo-handoff-capture.spec.ts:555 (U+212A in repo, owner, encoded, with tree/HEAD; question dropped, language kept, URL still stripped; plain ASCII spellings still trusted); before/after screenshots below
tryProjectUrl must encode page libs/datatug/project-address/src/lib/project-url.ts:206 project-url.spec.ts:772 (space, %, ?, #, non-ASCII, \, %2e%2e; both shapes; still refuses ./../empty), the round-trip property test
new-project form's folder validation github-project-address.ts:135 (readNewProjectFolder), new-project-form.component.ts:241 (before anything is created, a new repository included) github-project-address.spec.ts:305, new-project-form.component.spec.ts:354

What else changed, and why

  • New library @datatug/project-address (libs/datatug/project-address): github-project-address.ts and the project-URL part of nav-models.ts, moved with their specs (git mv). The app's eager code may not import the lazy-loaded datatug-main statically (@nx/enforce-module-boundaries), so without this the trust check could only keep its own copy of the trusted list. datatug-main keeps every import path (nav/github-project-address.ts re-exports; nav-models.ts re-exports the project URL functions). The reader files are untouched.
  • Routes live in datatug-main's lazy route table, not in the app's: nothing new in the eager bundle except the small demo-handoff-asked.ts (the question-asked decision).
  • DatatugNavContextService.legacyShapeOf overlaps G-A1c's named item ("the two regular expressions", design 3.4 point 2): without it the short address has no project in the side menu, which is not "pages unchanged". Only that regex site is touched; the link builders, main.ts:103, servers-page, environment-page and the e2e/journey suites are left for G-A1c.

Initial bundle (nx run datatug-app:build, production)

before (origin/main) after
Initial total 2.09 MB raw, 469.94 kB transfer 2.09 MB raw, 470.56 kB transfer (+0.62 kB)
main-*.js 1,225,533 B raw, 259.95 kB transfer 1,226,695 B raw (+1,162 B), 260.58 kB transfer

No budget warning. Everything else is in lazy chunks.

Screenshots (prod build, fake GitHub and agent through Playwright routes; not committed)

Directory: /private/tmp/claude-501/-Users-alex-projects/27bc30d5-3ff1-4e50-a5d9-9c67e328b1e3/scratchpad/apps-g-a1b/shots/. Overview, queries and chat of an agent project and of a GitHub project (old form), at 1280 and 390: origin/main vs this branch, all 12 pixel-identical; the GitHub project at its short address vs the old form on main: all 6 identical; the holding page (/demo en/ru, trusted and untrusted project chat with msg) identical on both. The one difference, intended: …/chinoo%E2%84%AA-demo/chat?msg=Secret shows the question on main (the Kelvin hole) and neutral wording here.

Tests run

nx run-many -t test lint -p datatug-app datatug-main datatug-project-address green (app 419, main 1831, project-address 379); check:zoneless green; demo-handoff e2e against the production build: 43 passed (new cases: no-question chat address is the project, ?lang=/?msg= only, reload and fresh-visit rules, canonical redirect, old form). One run against the real GitHub: tree/HEAD/demo-project-1 opens, tree/main/demo-project-1 redirects to tree/HEAD/…, datatug/chinook-demo opens.

Where the design and the code differ, and what I chose

  • Design 3.4 says the in-app canonical redirect is history.replaceState; I used a router redirect (UrlTree from canMatch), which replaces on the initial navigation and, in-app, never commits the typed address, so the history has one entry either way.
  • Design 6.5's table makes the holding page depend on the flag; the flag is not read (see the rule above).
  • …/tree/<ref>/<dir>/-/chat?msg=… is not a hand-off address (neither index.html nor handoffTarget knows a directory there), so its query is not stripped before analytics and it opens the project chat. Untrusted by construction (a trusted project has no folder), not a regression (on main it was an unmatched route), and not widened here; flagging it for G-A4b, which owns what msg does in the chat.
  • …/tree/main/-/chat?msg= is treated as untrusted for the holding page even when main is the default branch (the default-branch lookup is not made for the holding page, which opens no project). Same as main.
  • The design's "No DataTug project here" had no UI yet (the reader throws GithubProjectNotFoundError); this PR shows it, as part of the 3.4a messages. It costs no extra request: the check reads the project summary the pages read anyway (cached), once per project id per page load.

🤖 Generated with Claude Code

OpenVaultDB and others added 4 commits October 2, 2026 22:41
…g/project-address; close two #180 items in them

The app's eager start-up code may not import the lazy-loaded datatug-main
library statically (@nx/enforce-module-boundaries), so the hand-off trust check
could not use the real trust function and kept its own copy of the trusted
repository list. The id, address and trust functions (github-project-address.ts
and the project URL part of nav-models.ts) are pure and have no Angular, so they
move, with their specs, to libs/datatug/project-address. datatug-main keeps
every import path: nav/github-project-address.ts re-exports the library and
nav-models.ts re-exports the project URL functions.

Two follow-ups of issue #180 land in the moved code:
- tryProjectUrl/projectUrl take `page` as plain text and percent-encode every
  segment (a space, `%`, `?`, `#` or a non-ASCII letter used to be written
  as typed, or refused for a bare `%`); the result must still read back as
  that page.
- readNewProjectFolder: the new-project form's folder field, read as the
  reader will accept it (traversal, a leading slash, `..`, a `-` or empty
  segment, `@` and the rest are refused).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…refuse (#180)

The folder field of the GitHub tab is checked with readNewProjectFolder before
anything is created on GitHub (before a new repository too): `..`, a leading
slash, a backslash, an empty or `-` segment, `@`, `%`, `?` and `#` give a form
error instead of writing to a folder that cannot be opened. The checked,
trimmed folder is what the create service receives.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A GitHub project opens at /project/github.com/<owner>/<repo>[/tree/<ref>[/<dir>]/-/<page>]
with the existing project pages unchanged. The old /store/github.com/project/<id>
form keeps working, with no redirect (G-A1d is held).

datatug-main:
- a matcher route (githubProjectMatcher) consumes the locator and supplies
  storeId and projectId; the project pages are its children;
- a second matcher route and canMatch (github-project-address-check.ts) decide,
  per design 3.4a: redirect any other spelling to the canonical address (query and
  fragment kept), look up the default branch once when the address names a branch
  and redirect to the HEAD spelling when it is that branch (a refused lookup
  leaves the address as typed and tells the visit in a toast), and show a page
  that says so for each unsupported input and for a repository with no project
  file ("No DataTug project here", a hint for a branch name with a slash);
- DatatugNavContextService reads a short address that the short route opened as
  the old form of the same project, so the side menu and the pages that read
  the nav context see the same store, project, environment and table;
- the DataTug profile only: Incidentius does not match the short route.

datatug-app (the holding page keeps working exactly as today):
- /demo always shows the holding page; a project chat address shows it only when
  it arrived with a question (msg, or q), decided from what index.html kept of the
  query, so the same address with no question is the project's own chat page;
  after a reload it is the same (storage keeps the mark `asked=1` for an
  address that may not echo its question). The demo flag is not consulted;
- isEchoTrusted is replaced by isTrustedHandoff, which parses the address and asks
  the one trust function (isTrustedProjectAddress): the Kelvin sign U+212A no
  longer passes for `k` (live on main), with the regression test (#180).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@trakhimenok

Copy link
Copy Markdown
Contributor Author

[review r1 #183] Adversarial review (Opus): production builds of this head and of main served side by side, about 60 addresses and sequences in Playwright with third-party hosts stubbed, 949 targeted tests.

Reviewed-Head: 071eddf

Held up: the trust rule (Kelvin sign, long s, dotless i, full-width letters, prefix repos, 40-hex ref all untrusted), hand-off privacy for every hand-off address, existing routes, canonical redirects without loops, problem pages render text only, the library extraction is a pure move, all three #180 items closed. Not consulting the demo flag is safe and goes to G-A4b/G-A5.

Blocker

  • B1. Case variants of a hand-off chat address with no question now crash; main shows the holding page. libs/datatug/main/src/lib/routes/github-project-address-check.ts:50 compares project / github.com / tree / the page case-sensitively while handoffUrlMatcher and index.html are case-insensitive. /Project/GitHub.com/datatug/chinook-demo/chat, /PROJECT/github.com/datatug/chinook-demo/chat?lang=ru, /project/github.com/datatug/chinook-demo/Chat, /project/github.com/datatug/chinook-demo/Tree/HEAD/-/chat raise NG04002, report to Sentry, open the crash dialog and rewrite the URL to /. Fix: when handoffTarget() matches but showsHoldingPage() is false, redirect to the lower-cased canonical address, or keep the holding page unless the short route will match. Add the four to the e2e list.

Majors

  • S1. A fresh bare visit no longer clears the stored question, so a reload swaps the project chat for the holding page (apps/datatug-app/src/app/demo-handoff-asked.ts:109 leaves sessionStorage['datatug.demo.handoff.v1']; on main captureDemoHandoff removed it). …/chat?msg=Q → bare …/chat → reload shows the holding page with Q, on every reload; for an untrusted repo the project's chat is replaced by "This page is not available yet." Fix: remove the stored copy when the answer is no; extend the e2e with the reload.
  • S2. msg/q on short addresses that are not hand-off addresses stay in the URL (…/tree/HEAD/<dir>/-/chat?msg=, …/queries?msg=, …/<owner>/<repo>?msg=). New on this PR: the app's own page-view event and gtag screen carry urlAfterRedirects on NavigationEnd, which main never reached for these. Fix: strip msg/q in index.html for every /project/github.com/… path.

Minors

  1. The refused-lookup toast fires on every navigation (github-project-address-check.ts:232); once per repo and ref.
  2. canMatch blocks on the project probe (:240): with raw.githubusercontent.com hanging the page is blank for 40 s; the old form shows in 1.1 s. Race the probe against a short timeout.
  3. Parentheses in a folder plus any redirect opens another project (github-project-routes.ts:77 re-parses the canonical path; ( reads as an outlet group). Build the UrlTree from segments, or refuse.
  4. A 40-hex ref still costs one GET /repos/<o>/<r>.
  5. …/chat/extra makes two GitHub calls before failing to match.
  6. window.__datatugHandoffSearch is never cleared when no question was asked.
  7. …/chinook-demo.git/chat?msg= now echoes the question (same repo; consistent with 3.4a).
  8. With storage blocked, a reload shows the project chat (design 6.5: holding page).
  9. …/chat?lang=ru with no question drops the language.
  10. readNewProjectFolder accepts a 5000-character folder and look-alike dots (literal names, not traversal).
  11. The e2e "a project page other than the chat opens the project" only checks an ion-header, which the problem page also has.
  12. A typed ?asked=1 forces the holding page (same on main).

Not verified: GA and PostHog page views over https (code reading only), Incidentius in a browser, real GitHub, whether CI's e2e job includes the demo-handoff config.

VERDICT: blockers=1 majors=2 minors=12 land=no

🤖 Generated with Claude Code

OpenVaultDB and others added 2 commits October 2, 2026 23:28
…parsing, probe timeout, toast once (G-A1b review r1)

B1: the fixed segments of a short address (project, github.com, tree and the
first page) are read in any letter case, as the hand-off route and index.html
read them, and are another spelling of the canonical address: the route
redirects to the lower-case address, query and fragment kept, instead of
matching no route (NG04002, crash dialog). Only ASCII letters are folded.

Minor 3: a redirect is built from segments, not parsed from a string, so a
folder with ( or ) no longer opens another project (the router read it as an
outlet group).

Minor 1: the refused default-branch-lookup toast is shown once per repository
and ref for the life of the page.

Minor 2: the project probe is raced against a named 3 s timeout (injectable
timer); when GitHub does not answer the route matches and the pages show their
own loading or error state, as the old form does.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ry project address (G-A1b review r1)

S1: when the page decision answers "no question" (a fresh visit, or a query
with no question), the copy kept in sessionStorage and the window stash are
removed, as captureDemoHandoff did on main for a bare visit. A reload now keeps
showing the project chat instead of bringing the holding page back.

S2: index.html's inline script takes msg and q out of the address for every
path under /project/github.com (any letter case), before analytics; the rest of
the query and the fragment stay, and nothing is stashed or stored for them.
Hand-off addresses are handled exactly as before.

e2e: the case variants of the hand-off chat address with and without a
question (B1), the question then bare visit then reload, in the same tab and in
a tab opened from it, for the demo project and another repository (S1),
msg stripped and not reported on tree, queries and repo-root addresses (S2),
and "a project page other than the chat" now asserts the page's own title
instead of an ion-header the problem page also has.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@trakhimenok

Copy link
Copy Markdown
Contributor Author

[fix r1 #183]

Fix-Head: 6993299 (two commits on top of the reviewed 071eddf: 634c02d libs, 6993299 app and e2e). Each fix started from a failing test (B1, S1, minor 1, 2, 3 in unit specs; S2 in the index.html script spec and e2e).

Fixed

  • B1. libs/datatug/main/src/lib/routes/github-project-address-check.ts:64 (readShortGithubAddress): project, github.com, tree and the first page segment are read in any letter case (ASCII letters only, so no look-alike letter passes) and are another spelling of the canonical address, so the existing canonical redirect takes /Project/GitHub.com/…/Chat to /project/github.com/…/chat, query and fragment kept, one redirect. The holding page for an address with a question is untouched. The four addresses of the review, with and without msg, are in the e2e list (with: the holding page with the question, as on main; without: the project chat at the lower-case address, no NG04002, no crash dialog). Only the first page segment is lower-cased (every first page segment of the project routes is a lower-case literal), what follows it is left as typed.
  • S1. apps/datatug-app/src/app/demo-handoff-asked.ts:82,119,128: an answer of "no question" from the stash (a query with no question) or from a fresh navigate load removes sessionStorage['datatug.demo.handoff.v1']; the stash window.__datatugHandoffSearch is deleted when no question was asked (minor 6; never for /demo, whose page reads the language from it). e2e: …/chat?msg=Q then bare …/chat then reload stays the project chat, for the demo project and for someone/else, and in a tab opened with window.open from the first (which starts with a copy of its sessionStorage; this is where the stored question would come back).
  • S2. apps/datatug-app/src/index.html:48: for every path under /project/github.com (any case, matrix parameters ignored) that is not a hand-off address, the inline script drops msg and q (also percent-encoded keys such as m%73g), keeps every other parameter and the fragment, stores and stashes nothing. Hand-off addresses are handled exactly as before. Unit: the script is run against the existing list of paths (demo-handoff-capture.spec.ts) plus a table of query shapes. e2e: for …/tree/HEAD/dir/-/chat, …/queries, …/<owner>/<repo> and a mixed-case tree address with ?msg=M&q=M&x=1#frag: no msg or q in the address bar, x=1 kept, no outgoing request (all third-party hosts stubbed and recorded; the stand-in for Google Analytics reports location.href; Sentry receives a probe error) carries the marker, and nothing in sessionStorage or localStorage does. The …/queries page rewrites its own address (adds order-tags-by and tab, drops the fragment), on the old form of the address too; the e2e does not assert the fragment there.
  • Minor 1. github-project-address-check.ts:283: the refused-lookup toast is shown once per repo and ref per page lifetime.
  • Minor 2. github-project-address-check.ts:234,318: the probe is raced against GITHUB_PROBE_TIMEOUT_MS (3000) through an injectable GITHUB_PROBE_TIMER; on timeout the route matches and the pages show their own state. A timed-out probe counts as inconclusive like any non-"not found" failure (not repeated on later navigations).
  • Minor 3. libs/datatug/main/src/lib/routes/github-project-routes.ts:70 (urlTreeOfPath): the redirect tree is built from decoded segments, no string is parsed. Tests: /project/github.com/Acme/demo/tree/HEAD/a%28b%29/-/queries goes to …/acme/demo/tree/HEAD/a%28b%29/-/queries with project id demo@acme@a(b); the default-branch redirect with the same folder and ?x=1#f; a lone ) and a folder of only parentheses.
  • Minor 11. the e2e "a project page other than the chat opens the project" asserts the page's own ion-title (Queries, Chat) and that neither problem heading is on the page.

Left (owners are my proposal where no task is named; the landing owner confirms)

  • Minor 4 (a 40-hex ref costs one GET /repos/<o>/<r>): proposed owner, a G-A1b follow-up in this route's check.
  • Minor 5 (…/chat/extra makes two GitHub calls before failing to match): same.
  • Minor 7 (….git/chat?msg= echoes the question; same repo, consistent with 3.4a): no owner needed, as designed.
  • Minor 8 (storage blocked plus reload shows the project chat, design 6.5 wants the holding page): proposed G-A4b, which decides what msg does in the chat.
  • Minor 9 (lang dropped on a no-question chat address): proposed G-A4b.
  • Minor 10 (readNewProjectFolder accepts a 5000-character folder and look-alike dots): proposed owner, the new-project form (touched in Demo project foundations: follow-ups the wiring tasks must close #180).
  • Minor 12 (a typed ?asked=1 forces the holding page, same on main): no owner needed.
  • The demo flag is not consulted: G-A4b/G-A5.

Checks (this head)

  • CI on 6993299: build, test, e2e (7m49s, which ran the demo-handoff suite) and journey e2e all pass.
  • Unit: datatug-main routes and nav (243), datatug-project-address (379), datatug-app (451): green.
  • pnpm e2e:demo-handoff against the production build (serve:production): 59 passed.
  • lint (datatug-app, datatug-main, datatug-project-address): clean, no findings. pnpm run check:zoneless: OK.
  • Holding page screenshots (new-holding-* against base-holding-*, 390 and 1280 px, five states): identical to main, except holding-kelvin-msg (the Kelvin-sign repository now shows the neutral page, as in r0 and as the review confirmed). new-holding-* of this head are byte-identical to those of r0.
  • Initial bundle against main (production build): initial total 2.09 MB raw, transfer 469.94 kB to 470.52 kB (+0.58 kB; r0 was +0.62 kB); the main chunk is 1,225,533 to 1,226,656 bytes raw (+1,123).
  • CI's e2e job does run the demo-handoff config: .github/workflows/ci.yml runs pnpm nx affected --target=e2e-demo-handoff in the e2e job, and that target runs pnpm e2e:demo-handoff. It is affected-gated, so it runs when the app or its libraries changed (as here). CI not changed.
  • The branch is BEHIND main (clean merge, checked with git merge-tree); not merged, as asked. wb ci wait --pr 183 refuses a head that does not contain the current main, so the landing step needs the branch brought up to date first.

🤖 Generated with Claude Code

@trakhimenok

Copy link
Copy Markdown
Contributor Author

[review r2 #183] Second review (Opus): production builds of this head and of main side by side, about 120 addresses and sequences in Playwright with third-party hosts stubbed and history, storage, title and requests instrumented; 1,482 targeted tests.

Reviewed-Head: 414a7b4

All round-1 items are fixed (B1 case variants, S1 reload, S2 for regular paths, minors 1, 2, 3 for encoded parentheses, 11). Existing routes identical to main; trust rule unchanged; the merge with main is coherent and the probe works with the new reader. CI green.

Blocker

  • R2-B1. A doubled-slash or parenthesised hand-off address leaves the question in the address bar and history, and the project chat replaces the holding page; main showed the holding page and removed the question. //project/github.com/datatug/chinook-demo/chat?msg=Q, //project/github.com/acme/demo/chat?msg=Q, /(project/github.com/datatug/chinook-demo/chat)?msg=Q, /(project/github.com/acme/demo/chat)?msg=Q. The inline script (apps/datatug-app/src/index.html:33-48) splits location.pathname itself (//project gives s[0] === '', /(project gives '(project') while Angular's parser reads both as /project/github.com/…; handoffAsked (demo-handoff-asked.ts:114-129) finds no stash and answers "no question". The same gap leaves msg on //project/…/queries?msg=Q, ///project/…, /(project/…/queries)?msg=Q. Fix: (1) a router-level safety net in githubAddressCanMatch (github-project-routes.ts:94-115): when typed.queryParams has msg or q, redirect to the same path without them; (2) when the router matches a hand-off address with msg/q in the navigation's query and there is no stash, treat it as asked (stash the router's query) so the holding page captures and strips it as on main; (3) optionally normalise empty segments and (/) in the script. Add the six addresses to the e2e list.

New minors

  1. A timed-out probe is cached as "project exists" for the page lifetime (github-project-address-check.ts:308); a later navigation never shows "No DataTug project here".
  2. The named-ref default-branch lookup is not raced against the timer (:275): 5.6 s blank with api.github.com hanging.
  3. Literal parentheses in a folder (/project/github.com/Acme/demo/tree/HEAD/a(b)/-/queries, as pasted from GitHub) silently open folder a; show the "unsupported" problem page when the URL has an outlet group.
  4. Matrix parameters on a canonical short address are kept (/project;a=1/github.com/…, …/demo;msg=Q/queries, …/queries;msg=Q); the page renders without project context and ;msg=Q stays. Treat them as non-canonical so the redirect drops them.

Round-1 minors 4, 5, 7, 8, 9, 10, 12 remain open as proposed.

Not verified: GA and PostHog over https (address bar and history checked instead), real GitHub, Incidentius in a browser.

VERDICT: blockers=1 majors=0 minors=11 land=no

🤖 Generated with Claude Code

OpenVaultDB and others added 2 commits October 3, 2026 00:30
…t groups and matrix parameters (G-A1b review r2)

R2-B1: the inline script of index.html read the path its own way (`//project/...`,
`/(project/...)`), the router another, so the question stayed in the address bar
and the history. Three layers now:
- the short route (githubAddressCanMatch) redirects every navigation it matches,
  in-app ones included, to the same address without `msg` and `q`, before GitHub
  is asked anything;
- the app's hand-off decision treats a router-matched hand-off address with a
  question in the navigation's query and no stash as asked, and stashes that query
  so the holding page captures and strips it;
- the script and demo-handoff-path.ts read the path as the router does (leading
  and repeated empty segments, a root group is its content), and replaceState
  gets the origin for a `//` path (it was a cross-origin address, refused).
Minor 1: a probe that timed out is not cached as "the project exists"; its late
answer is kept and told on the next navigation. Minor 3: an unnamed outlet group
(`a(b)`) shows the unsupported-address page. Minor 4: matrix parameters on a
short address are dropped by the canonical redirect.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…dev server has no SPA fallback for it)

CI's nx serve answered 'Cannot GET' for .../blob/main/x.txt, so the app never started
and the one e2e case failed there; it passed against the static server used locally.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@trakhimenok

Copy link
Copy Markdown
Contributor Author

[fix r2 #183] Head 1935045 (r1 head 414a7b4 plus two commits: the fix 2ba0f42, and a test-only follow-up; main is already in the branch). CI green on this head (build, test, e2e, journey e2e).

R2-B1 (blocker, privacy): fixed in three layers.

  • a. Router safety net, libs/datatug/main/src/lib/routes/github-project-routes.ts (githubAddressCanMatch, withoutQuestion): the first thing the short route does, before GitHub is asked anything, is redirect (replace) to the same URL without msg and q, other parameters and fragment kept. It covers every address the short route matches, in-app navigations included (router.navigateByUrl, unit-tested).
  • b. Hand-off addresses read differently by script and router: apps/datatug-app/src/app/demo-handoff-asked.ts (handoffAsked, showsHoldingPage): a router-matched hand-off address with msg/q in the navigation's query and no stash is asked, and the router's query is stashed so the holding page captures and strips it (datatug-app-routes.ts passes getCurrentNavigation().extractedUrl.queryParams). apps/datatug-app/src/index.html and demo-handoff-path.ts (routeSegments, handoffTargetOfPath) now read the path as the parser does: one trailing slash dropped, leading // collapsed, cut at an empty segment, a root ( … ) group is its content; the script stays ES5 and dependency free, its specs run it against the TypeScript. Found by the e2e, not by the review: history.replaceState was given //project/… as the address, which the browser reads as another host, refuses (SecurityError, swallowed), so even a script that recognised the path stripped nothing. The script and captureDemoHandoff now give such a path its origin.
  • c. e2e (apps/datatug-app/e2e/demo-handoff.spec.ts, production build, third-party hosts stubbed and recorded, GA stand-in, Sentry probe): the four blocker addresses plus ///, tree and Tree/HEAD/-/chat forms, and //…/queries, ///…/queries, /(…/queries), //…/demo: address bar, every history call, every entry of the session history (Navigation API), title, storage, own and third-party requests carry no marker; hand-off ones show the holding page (question only for datatug/chinook-demo on its default branch, neutral otherwise). One in-app navigation test (Back/Forward path into the router, because a production build has no handle on Router; navigateByUrl itself is in the unit specs) ends without msg. 82 e2e tests pass (59 before, 23 new).
  • Stated limits: for a trusted hand-off address the question stays in this tab's sessionStorage, as on main, so a reload shows the holding page again (the e2e allows exactly that and nothing else). A question in a matrix parameter (…;msg=Q) is dropped by the router's canonical redirect from the address and history, but index.html does not look for it, so an analytics tag that reads the address before the router starts could see it.

Minor 1 github-project-address-check.ts (firstRead): a timed-out probe is not marked as probed; it is not waited for again while it is pending, and its late answer is kept (missing/moved shown once on the next navigation, a project marks it probed). Tests with the injected timer.

Minor 3 github-project-routes.ts (urlHasOutletGroup, shared hasOutletGroup/pathHasOutletGroup in libs/datatug/project-address/src/lib/outlet-group.ts, also used by the hand-off matcher): …/tree/HEAD/a(b)/-/queries opened as the browser has it shows the unsupported-address page. Note: the parser keeps nothing of an unnamed group (not in the URL tree), so it is read from the address as typed, only when the browser's address is this navigation's; a named group (chat(menu:x)) is in the tree but the router fails on it with NG04002 as on main, unchanged. Order: the hand-off route comes first and a root group /(project/…/chat) is not an outlet group, so /(project/github.com/acme/demo/chat)?msg=Q shows the holding page and the same without a question is the project chat; both tested (unit and e2e).

Minor 4 github-project-address-check.ts (decide, matrixParameters) with githubAddressCanMatch: matrix parameters on a canonical short address redirect to the canonical path (/project;a=1/…, …/demo;msg=Q/queries, …/queries;msg=Q); hand-off addresses with matrix parameters keep the holding page (hand-off route first).

Existing routes: hand-off e2e (/demo, unmatched route, /demo(menu:x), …/chat/extra fail to match as on main) pass; screenshots /, GitHub project old form (overview, queries) and agent project (overview) at 1280 and 390 are identical to main, except one 1280 GitHub overview shot that differs in a 210x10 px strip between two runs of the same build (timing), identical to main in the rerun.

Holding page screenshots new-holding-* against base-holding-* (/demo en and ru, trusted and untrusted project chat with msg, 1280 and 390, regenerated from both builds and also against the r0 base-holding-* files): identical. Initial bundle: 2.10 MB raw, 470.79 kB estimated transfer (main 469.94 kB, r1 470.52 kB): +0.85 kB against main.

Red first: the new unit specs fail 135 tests against the r1 sources (94 app, 41 datatug-main routes) and pass with the change. Checks: unit specs app 572, datatug-main 1990 (whole lib), datatug-project-address 399, all pass; lint clean on the three projects (no finding, so no lint commit); pnpm run check:zoneless OK.

Left as before: new minor 2 (named-ref default-branch lookup not raced against the timer) and round-1 minors 4, 5, 7, 8, 9, 10, 12.

The first CI run of 2ba0f42 failed one new e2e case: CI's nx serve answers 404 to an address that looks like a file (…/blob/main/x.txt), so the app never started; the case now uses …/blob/main/dir/notes (same file-link problem). Test-only change.

Not verified: GA and PostHog over https (stand-ins used), real GitHub, Incidentius in a browser. The e2e run used a static server I started on 127.0.0.1:4332 (SPA fallback, production build) with the playwright config's reuseExistingServer; stopped afterwards.

🤖 Generated with Claude Code

@trakhimenok

Copy link
Copy Markdown
Contributor Author

[review r3 #183] Third review (Opus): production builds of this head and of main 8ca3ed1 behind a test host so the analytics snippet runs, all 118 round-2 addresses plus 47 new ones, reload/Back/second-tab sequences and in-app navigations, with a GA stand-in, cookies, Navigation API entries and decoded PostHog and Sentry bodies recorded; 2,961 targeted tests; two mutation checks.

Reviewed-Head: 1935045

R2-B1 is fixed: on every address the router matches under /project/github.com/, the question is gone from the address bar, every history entry, title, storage, cookies and outgoing requests, apart from the trusted hand-off's sessionStorage copy as on main. No address was found where this head is worse than main. Minors 1, 3, 4 fixed. The inline script is a separate ES5 block with no throwing path, rewrites only under /project/github.com/ and hand-off paths, and only to the same origin. Existing routes identical to main; trust rule unchanged; CI green.

Minors for a follow-up issue (none worse than main):

  1. A question in a matrix parameter (…/queries;msg=Q, …/chinook-demo/chat;msg=Q) is in the address when the analytics snippet and PostHog start (apps/datatug-app/src/index.html:35-61 ignores matrix parameters); the router then removes it, but Sentry's navigation breadcrumb keeps it. Main has the same exposure plus more. Fix: drop ;msg= and ;q= in the script's /project/github.com branch. The sites hand off with ?msg=.
  2. Address forms the router does not match keep the question and crash as on main (/project//github.com/…, /project%2Fgithub.com/…, …/queries%3Fmsg=Q, other parameter names).
  3. The unsupported page for a literal a(b) folder shows the router's reading in the address bar.
  4. Still open: round-2 new minor 2 (named-ref lookup not raced against the timer) and round-1 minors 4, 5, 7, 8, 9, 10, 12.

Not verified: real GA and PostHog over https, real GitHub, Incidentius in a browser, router.navigateByUrl in a production build (unit specs only).

VERDICT: blockers=0 majors=0 minors=4 land=yes

🤖 Generated with Claude Code

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