feat(project): short address for GitHub projects (G-A1b) - #183
Conversation
…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>
|
[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
Majors
Minors
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 VERDICT: blockers=1 majors=2 minors=12 land=no 🤖 Generated with Claude Code |
…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>
|
[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
Left (owners are my proposal where no task is named; the landing owner confirms)
Checks (this head)
🤖 Generated with Claude Code |
|
[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
New minors
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 |
…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>
|
[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.
Minor 1 Minor 3 Minor 4 Existing routes: hand-off e2e ( Holding page screenshots 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); 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 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 🤖 Generated with Claude Code |
|
[review r3 #183] Third review (Opus): production builds of this head and of main Reviewed-Head: 1935045 R2-B1 is fixed: on every address the router matches under Minors for a follow-up issue (none worse than main):
Not verified: real GA and PostHog over https, real GitHub, Incidentius in a browser, VERDICT: blockers=0 majors=0 minors=4 land=yes 🤖 Generated with Claude Code |
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, designdatatug/backstagedocs/design/demo-as-github-project.md3.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 indatatug-app-routes.ts:52(handoffOrRoot), DataTug profile only./demo,/Demo,/demo;x=1, … (with any query or none)…/project/github.com/<o>/<r>/chator…/tree/<ref>/-/chatthat arrived with a question (msg, orq)?lang=ru,?msg=,?utm_source=x)/project/github.com/<o>/<r>,…/queries,…/tree/HEAD/<dir>/-/chat, …)/; 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=1instead 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.tsforbids 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 runmsg(design 6.5 table).demo-flag.spec.tsnow also pins that which page a hand-off address gets does not depend on the override.Acceptance, item by item
storeIdandprojectIdlibs/datatug/main/src/lib/routes/github-project-routes.ts:38(githubProjectMatcher),:90(githubProjectRoutes, lazyDatatugProjectRoutingModule), registered atdatatug-routing.module.ts:107ahead ofstore/:storeIdgithub-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 formgithub-project-address-check.ts:207(decide), gluegithub-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.tsredirect table and trailing slash; e2edemo-handoff.spec.ts"non-canonical spelling"github-address-problem-page.component.ts:33(messageForUnsupported),:72(messageForNotFound), page:170github-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.github-project-address-check.ts:102(GithubDefaultBranchLookup, oneGET /repos/<o>/<r>per repo, only when the address names a ref),:148(GithubAddressNotices, toast),:207github-project-address-check.spec.ts:146(found / 404 / moved / 403 / 429 / 5xx / bad JSON / over-cap / network / memo),:328(default branch redirects toHEADwith 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-1datatug-nav-context.service.ts:281(legacyShapeOf: the side menu and nav context see the project)origin/main, at 1280 and 390;datatug-nav-context.service.spec.ts(new describe)store/:storeIduntouchedgithub-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 maingithubProjectRoutescanMatch+githubAddressCanMatchcheck the profilegithub-project-routes.spec.ts:382,datatug-app-routes.spec.ts("Incidentius … goes to the root")Issue #180, the items marked for G-A1b
isEchoTrustedwith 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, withtree/HEAD; question dropped, language kept, URL still stripped; plain ASCII spellings still trusted); before/after screenshots belowtryProjectUrlmust encodepagelibs/datatug/project-address/src/lib/project-url.ts:206project-url.spec.ts:772(space,%,?,#, non-ASCII,\,%2e%2e; both shapes; still refuses./../empty), the round-trip property testgithub-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:354What else changed, and why
@datatug/project-address(libs/datatug/project-address):github-project-address.tsand the project-URL part ofnav-models.ts, moved with their specs (git mv). The app's eager code may not import the lazy-loadeddatatug-mainstatically (@nx/enforce-module-boundaries), so without this the trust check could only keep its own copy of the trusted list.datatug-mainkeeps every import path (nav/github-project-address.tsre-exports;nav-models.tsre-exports the project URL functions). The reader files are untouched.datatug-main's lazy route table, not in the app's: nothing new in the eager bundle except the smalldemo-handoff-asked.ts(the question-asked decision).DatatugNavContextService.legacyShapeOfoverlaps 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-pageand the e2e/journey suites are left for G-A1c.Initial bundle (
nx run datatug-app:build, production)origin/main)main-*.jsNo 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/mainvs 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 (/demoen/ru, trusted and untrusted project chat withmsg) identical on both. The one difference, intended:…/chinoo%E2%84%AA-demo/chat?msg=Secretshows 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-addressgreen (app 419, main 1831, project-address 379);check:zonelessgreen;demo-handoffe2e 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-1opens,tree/main/demo-project-1redirects totree/HEAD/…,datatug/chinook-demoopens.Where the design and the code differ, and what I chose
history.replaceState; I used a router redirect (UrlTreefromcanMatch), which replaces on the initial navigation and, in-app, never commits the typed address, so the history has one entry either way.…/tree/<ref>/<dir>/-/chat?msg=…is not a hand-off address (neither index.html norhandoffTargetknows 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 whatmsgdoes in the chat.…/tree/main/-/chat?msg=is treated as untrusted for the holding page even whenmainis the default branch (the default-branch lookup is not made for the holding page, which opens no project). Same as main.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