diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 00000000..ebe29471 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,34 @@ +name: release-please +# Keeps a Release PR open that bumps the version (app/package.json, version.txt) and +# prepends CHANGELOG.md from the conventional commits merged since the last release. +# Merging that PR (owner only) tags vX.Y.Z[-beta.N] and publishes the GitHub release +# (A99). Config: release-please-config.json + .release-please-manifest.json. After +# v2.0.0-beta.1 is out, delete "release-as" and "last-release-sha" from the config +# (they only seed the first release); see README "Releasing". +# +# Token: a PR opened with GITHUB_TOKEN triggers no workflows, so the gate does not run +# on the Release PR (it only touches CHANGELOG.md and version files). To run CI on it, +# add a fine-grained PAT (this repo only; Contents and Pull requests: read and write) +# as the secret RELEASE_PLEASE_TOKEN; it is used when present. Without it, the repo +# setting "Allow GitHub Actions to create and approve pull requests" must be on. +on: + push: + branches: [main] + workflow_dispatch: +permissions: {} +concurrency: + group: release-please + cancel-in-progress: false +jobs: + release-please: + if: github.repository == 'CMaintz/tech-atlas' + runs-on: ubuntu-latest + permissions: + contents: write # release commits, tags, GitHub releases + pull-requests: write # the Release PR + steps: + - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 + with: + token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..37fcefaa --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "1.0.0" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..c8be3333 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,75 @@ +# Changelog + +Atlas follows [semantic versioning](https://semver.org) with beta pre-releases +(v2.0.0-beta.1, beta.2, ... then v2.0.0 at launch). New entries are written by +release-please from conventional commits; see README "Releasing". + +## v1.0.0 to v2.0.0-beta.1: summary + +Written by hand. Atlas changed from a searchable dictionary into a bilingual map for +learning; everything below landed after v1.0.0 (PRs #2 to #70). + +### Content + +- Over 430 terms across security, computer science, AI and platforms, up from the v1 + seed: batch 3 (150 terms), AI and platform domains, batch 5 (appsec, detection and + response, identity, EU/DK regulation), batch 6, full course-compendium coverage and + an AI dictionary of 104 terms, plus 16 new machine-learning terms after a web-verified + review. +- Long-form articles (EN + DA), era years for the timeline, an extended technical deep + dive with sources for every term, and a prose refinement pass over all terms and + articles. +- Auto-linked prose, mentions, disambiguation pages and search intents. + +### Explorer + +- A 2D and 3D graph explorer with depth axis, routes between terms and learn-first + paths; the Time layout and a swim-lane timeline. +- Domain colour families, flowing directed edges, a stable backbone overview with + lanes and 3D galaxies, and the relationship types that carry structure shown by + default. +- A term side panel with in-place expand, previous/next through connections and + history. +- A floating control bar (responsive, never two rows), a collapsible legend, hover + cards, drag feedback, WASD keyboard navigation and named 3D galaxies. +- A hidden visual lab to compare old and new effects with a frame meter. + +### Search + +- Static semantic search across English and Danish, later moved to a backend + (Supabase pgvector and an Edge Function) with looser deadlines. +- "Find a term" in the Explorer matches by name and by meaning, like the home search. + +### Learning + +- A study layer: generated quizzes, spaced repetition and a personal knowledge map. +- A hand-written question bank (180 questions), domain quizzes, questions never + answered by their own page, and a review queue. +- A guided tour with eased motion. + +### Accounts and privacy + +- Optional sign-in with synced learner progress (Supabase), sign-in first, with + LinkedIn (OIDC); email sign-in hidden. +- A privacy page. Atlas is free forever: no ads, no paywall, no tracking. + +### Security + +- Secret scanning, a Content Security Policy, least-privilege CI and a security review. +- Dependabot for npm and GitHub Actions, with a cooldown and grouped updates. + +### Design + +- A home page, mobile navigation and a UX baseline; a complete light theme with a + cream map; a language menu; a BETA ribbon (a badge on phones). +- A mobile pass: 44px tap targets, dynamic viewport units, safe areas and 16px inputs. +- An About dialog with credits; no em or en dashes in anything a reader sees. +- Open data (JSON, CSV, Anki, graph), RSS feeds, SEO, a 404 page, an A-Z index and + short URLs. + +### Docs and CI + +- The Foundry gate (lint, typecheck, tests with a full build, audit) on every PR, and + a deploy that ships only what passed the gate. +- A README with a demo GIF and screenshots, a content licence (CC BY-SA 4.0) and + regular reviews of the decision log. diff --git a/README.md b/README.md index 55a64fd2..95453ed7 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,40 @@ was drafted with AI assistance and is being reviewed by hand, entry by entry; un entry is reviewed it is marked as a draft on the site. Corrections are welcome as issues or pull requests. +## Releasing + +Versions follow [semantic versioning](https://semver.org) with beta pre-releases: +v2.0.0-beta.1, beta.2, beta.3 and so on, then v2.0.0 at launch. The version lives in +`app/package.json` and is shown in the site footer and the About dialog, linked to +[CHANGELOG.md](CHANGELOG.md). + +[release-please](https://github.com/googleapis/release-please) +(`.github/workflows/release-please.yml`) runs on every push to `main` and keeps one +**Release PR** open, titled like `chore(main): release 2.0.0-beta.2`. It bumps the +version (`app/package.json`, its lock file, `version.txt`, `.release-please-manifest.json`) +and prepends the new entry to `CHANGELOG.md`, built from the conventional commits since +the last release: `feat` (Features), `fix` (Fixes), `content` (Content), `docs` +(Documentation); `chore`, `ci`, `refactor`, `test` are left out and do not start a +release on their own. + +- **Cut a release**: the owner merges the Release PR. release-please then tags the + merge (e.g. `v2.0.0-beta.2`) and publishes a GitHub pre-release; the push to `main` + runs the gate and deploys, so the footer shows the new version. +- **CI on the Release PR**: a PR opened with the default `GITHUB_TOKEN` triggers no + workflows, so the gate does not run on it (it only touches the changelog and version + files). To run it, add a fine-grained token (this repository only; Contents and Pull + requests: read and write) as the secret `RELEASE_PLEASE_TOKEN`; the workflow uses it + when present. Without that secret, **Settings > Actions > General > Allow GitHub + Actions to create and approve pull requests** must be on, or the workflow cannot open + the PR. +- **After v2.0.0-beta.1**: delete `release-as` and `last-release-sha` from + `release-please-config.json`. They only seed the first release (the hand-written + summary of everything since v1.0.0 is already in the changelog); left in, every + release would propose beta.1 again. +- **Launch**: in the config set `"release-as": "2.0.0"` and `"prerelease": false`, + and delete `versioning` and `prerelease-type`; merge the Release PR, then delete + `release-as` again. + ## Data The site publishes its content as open data: every term as JSON (`/api/terms.json`, diff --git a/app/package-lock.json b/app/package-lock.json index 822432d8..9439c6d4 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "lexicon", - "version": "0.0.1", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lexicon", - "version": "0.0.1", + "version": "1.0.0", "dependencies": { "@astrojs/preact": "^6.0.5", "@supabase/supabase-js": "^2.117.0", diff --git a/app/package.json b/app/package.json index ca371cff..2eb85b3e 100644 --- a/app/package.json +++ b/app/package.json @@ -1,7 +1,7 @@ { "name": "lexicon", "type": "module", - "version": "0.0.1", + "version": "1.0.0", "private": true, "scripts": { "dev": "astro dev", diff --git a/app/src/components/AboutButton.tsx b/app/src/components/AboutButton.tsx index 8f7807d9..551cead3 100644 --- a/app/src/components/AboutButton.tsx +++ b/app/src/components/AboutButton.tsx @@ -55,7 +55,7 @@ const Avatar = ({ name, photo, position }: { name: string; photo?: string; posit * preventDefault, so the tour and term panel stand down), backdrop click, and focus * back on the opener. */ -export default function AboutButton({ ui, links, people, variant = 'circle' }: Props) { +export default function AboutButton({ ui, links, people, version, variant = 'circle' }: Props) { const dialog = useRef(null); const opener = useRef(null); const titleId = useId(); @@ -182,6 +182,21 @@ export default function AboutButton({ ui, links, people, variant = 'circle' }: P
{section('credits', ui.credits)} {section('thanks', ui.thanks)} +

+ + Atlas {version.label} + + {' '} + ({ui.releaseNotes}) {ui.opensInNewTab} + + +

diff --git a/app/src/layouts/Base.astro b/app/src/layouts/Base.astro index 5ac9d380..63a2eaf7 100644 --- a/app/src/layouts/Base.astro +++ b/app/src/layouts/Base.astro @@ -8,6 +8,7 @@ import { ACCOUNTS, LANGS, REPO, TOUR_STEPS, UI, swapLang, url, type Lang } from import { isCurrentNav } from '../lib/prefs'; import { aboutProps } from '../lib/about-assets'; import { UI_EXTRA } from '../lib/ui-extra'; +import { CHANGELOG_URL, VERSION } from '../lib/version'; interface Props { lang?: Lang; @@ -369,6 +370,10 @@ const tourUi = { {ui.tourStart} + + {VERSION} + ({extra.releaseNotes}) + )} {!wide && ( diff --git a/app/src/lib/about-assets.ts b/app/src/lib/about-assets.ts index 3dbd7efc..6af9cd87 100644 --- a/app/src/lib/about-assets.ts +++ b/app/src/lib/about-assets.ts @@ -10,6 +10,7 @@ import { existsSync } from 'node:fs'; import { resolve } from 'node:path'; import { ABOUT_LINKS, ABOUT_PEOPLE, ABOUT_UI, url, type Lang } from './site'; import { visibleLinks } from './about'; +import { CHANGELOG_URL, VERSION } from './version'; export const aboutProps = (lang: Lang) => ({ ui: { ...ABOUT_UI[lang] }, @@ -26,6 +27,7 @@ export const aboutProps = (lang: Lang) => ({ : undefined, photoPosition: p.photoPosition ?? 'center', })), + version: { label: VERSION, href: CHANGELOG_URL }, }); export type AboutProps = ReturnType; diff --git a/app/src/lib/site.ts b/app/src/lib/site.ts index fa76e640..c21634fa 100644 --- a/app/src/lib/site.ts +++ b/app/src/lib/site.ts @@ -889,6 +889,7 @@ const ABOUT_EN = { thanks: 'Thanks', close: 'Close', opensInNewTab: '(opens in a new tab)', + releaseNotes: 'Release notes', }; export const ABOUT_UI: Record> = { @@ -904,5 +905,6 @@ export const ABOUT_UI: Record> = { thanks: 'Tak til', close: 'Luk', opensInNewTab: '(åbner i en ny fane)', + releaseNotes: 'Udgivelsesnoter', }, }; diff --git a/app/src/lib/ui-extra.ts b/app/src/lib/ui-extra.ts index e761c030..95f1f8aa 100644 --- a/app/src/lib/ui-extra.ts +++ b/app/src/lib/ui-extra.ts @@ -30,6 +30,7 @@ export const UI_EXTRA = { azIntro: '{n} terms, alphabetically.', notFoundAz: 'Browse all terms A-Z', licenceFooter: 'Content CC BY-SA 4.0', + releaseNotes: 'Release notes', tiers: { standard: 'Standards & official texts', 'official-doc': 'Official documentation', @@ -63,6 +64,7 @@ export const UI_EXTRA = { azIntro: '{n} begreber i alfabetisk rækkefølge.', notFoundAz: 'Se alle begreber A-Å', licenceFooter: 'Indhold CC BY-SA 4.0', + releaseNotes: 'Udgivelsesnoter', tiers: { standard: 'Standarder og officielle tekster', 'official-doc': 'Officiel dokumentation', diff --git a/app/src/lib/version.ts b/app/src/lib/version.ts new file mode 100644 index 00000000..0117010f --- /dev/null +++ b/app/src/lib/version.ts @@ -0,0 +1,20 @@ +/** + * The site's version, read from app/package.json when the site is built (A99). + * release-please bumps that version in its Release PR, so the footer and the About + * dialog always show the release that is deployed. Server-only (it reads the file + * system): import it from .astro frontmatter or about-assets.ts, never from an island. + */ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { REPO } from './site'; + +// Builds run from app/ (npm scripts, mise `dir = "app"`), as in about-assets.ts. +const pkg = JSON.parse(readFileSync(resolve(process.cwd(), 'package.json'), 'utf8')) as { + version: string; +}; + +/** e.g. "v2.0.0-beta.1". */ +export const VERSION = `v${pkg.version}`; + +/** Release notes. The CHANGELOG on main always exists, unlike a tag's page on release day. */ +export const CHANGELOG_URL = `${REPO}/blob/main/CHANGELOG.md`; diff --git a/design/AUTONOMOUS_DECISIONS.md b/design/AUTONOMOUS_DECISIONS.md index b5e7b1fc..cef53f5c 100644 --- a/design/AUTONOMOUS_DECISIONS.md +++ b/design/AUTONOMOUS_DECISIONS.md @@ -157,6 +157,7 @@ Closed Vocabulary lint (E1) + full rule set; GitHub Pages deploy. | A96 | **A hidden visual lab for the Explorer** at `//lab/5f69d757b126/2d/` and `/3d/` (EN + DA; noindex so out of the sitemap, linked from nowhere, no tour, like A91), so the owner can see and measure old visual effects live without deploying test branches. Each page renders the real `Explorer` (bar, legend, About "i", term panel unchanged) and a floating **Lab** panel whose toggles restyle in place, never relayout. **2D**: backbone curve (straight haystack, today / bezier / unbundled-bezier from the edge's own offset × a strength slider), source-to-target domain-colour gradients, one-way flow (overlay dots, today / the old Cytoscape `line-dash-offset` marching dashes on every visible one-way edge / none) with a speed slider, hover (today / the old `attachHover` that restyles every element, plus the old fade transitions), node glow, "show all", label density (none / hubs / culled, today / all) and the viewport snapshot. **3D**: merged lines (today) or a tube and arrow cone per link (old), link curvature (re-bends the merged web and the comets too), flow (comets, today / the old per-link particles / none) with speed, glow (point cloud, today / a sprite per term, old), bloom (`UnrealBloomPass` + `OutputPass` and an opaque scene background, else the transparent canvas turns grey), auto-rotate, fog, node spacing (the scene scales about its centre while terms and labels keep their size). **Meter**: rAF-based, current fps, 1% low (1000 / 99th-percentile frame time) and the longest frame over the last 5 s; **Benchmark** runs a scripted 5 s pan and zoom (2D, straight through `cy.viewport`, so every frame is a full redraw: the snapshot only applies to user gestures) or one orbit (3D) and reports the average fps and the active toggles. Toggles start from the address (`?curve=bezier&gradient=1&bench=1`), so a headless run is a list of URLs; nothing is stored (no new storage keys). **Production hook, optional and inert by default**: `Explorer` takes `lab?: { mode, showAll, onMaps }`; `createMap2D` returns `lab.dots` (a live copy of `EXPLORER.dots`) and `setDots`, `startDots` takes that copy; `createMap3D` returns `lab` (its three.js objects, the flow settings copy, the curve buffer and its link predicates). Old effects live in `ExplorerLab.tsx` / `explorer-lab.ts`, not behind production flags. **Measured** (headless Edge, 1440×900, 2× CPU throttle, RTX 3080; the PR has the table): 2D scripted pan and zoom (full redraws) ≈13 fps today, curves or gradients ≈11, marching dashes ≈9, all three ≈7, "show all" + dashes ≈5, no flow ≈16; 3D orbit 60 today, tubes ≈13, per-link particles ≈25, sprite glow ≈33, bloom 59 (1% low 30), tubes + particles + sprites ≈8. **Later additions (owner)**: the lab follows the page theme through the maps' own `retheme` (it re-reads the restyled base and lays its rules over it again; bloom is off on the cream map, where it only washes out); **emphasis by importance** (both views): importance = a per-type rank (requires, kind of, part of 1.0; mitigates, exploits, causes, mandates 0.85; implements, supersedes 0.7; contrasts with, alternative to 0.5; used with 0.35) × the square root of the link's own `weight` normalised to the heaviest, shown as opacity, colour (saturation and lightness towards the background), width (2D and 3D tubes) or all three, with a spread slider (importance ^ spread); **relayout** (lab only): a minimum-distance slider that re-runs the spacing pass (`createMap2D` keeps each island's fcose result and exposes `lab.relayout`; 3D re-spaces from the original layout) and sub-domain clusters (2D: islands 0.8 × and 70 px further apart; 3D: clusters 1.4 × out from their domain's centre and 0.7 × tighter, with optional faint cluster names); **cream-map contrast** sliders (term saturation / lightness, edge darkness / opacity, shadow strength, 2D label weight / halo) with a "Copy values" button. 3D exposes inert `webGain` / `webTint` per link, `linkLength`, `radius` and `labels` for these. | The owner wants to judge effects by eye and by frame rate before choosing; a lab that reuses the real component measures the real cost, and keeping the old code paths out of production keeps the Explorer diff to a few inert lines. | | A97 | **Explorer: keyboard navigation (WASD) and 3D lines behind receded terms** (owner requests). **3D draw order**: every sphere is transparent (`nodeOpacity` 0.95), so three.js sorted each against the one merged web by distance, and a receded sphere (dimmed by a selection, hover or route) that happened to draw first wrote depth and erased every line behind it; only comets and glow showed through. Now a fixed `renderOrder`: glow, solid spheres (they still write depth, so they hide what is behind them), lines (the web and 3d-force-graph's focused links), receded spheres (no depth write, below `EXPLORER.three.solidOpacity`), comets. The sphere materials are 3d-force-graph's and swapped on its schedule, so a `scene.onBeforeRender` pass sets this each frame. **Keys** (`explorer-keys.ts`, unit-tested): held keys give a target velocity (opposite keys cancel, Shift ×3), eased exponentially (frame-rate independent; no easing under reduced motion). 2D: W A S D / arrows pan, Q/E or -/+ zoom about the centre of the part the panel leaves clear. 3D: W/S forward and back (towards the orbit centre, pushing it on ahead once `near`), A/D sideways and Q/E (Space: up) down and up, both moving the orbit centre with the camera, arrows orbit; the OrbitControls target stays in sync, so a mouse orbit afterwards turns about what is in front. Letters and arrows by physical key (`code`), + and - by character (Danish layout). **Ctrl as "down" is left out**: Ctrl+W closes the tab and Ctrl+drag pans. Keys act only when focus is inside the map host (now `tabindex=0`, `role="application"`, a "use W A S D" `aria-label`) or nothing has focus and the pointer is over the map; never with Alt/Ctrl/Cmd, and never in a field, the bar, a popover or the term panel (whose ←/→ keep stepping through connections). Held keys clear on blur and when the tab is hidden. The open legend ends with a one-line key hint for the current view (EN+DA; the term page's legend has none). Tunables in `EXPLORER.keys`. **Measured** (headless Edge, 1440×900, 2× CPU throttle, before/after): 2D pan 46/49, zoom 46/54, hover 59/59; 3D orbit 49/60, idle 59/59, hover 59/54 (run-to-run noise). | The owner asked for game-like movement; physical keys keep WASD on any layout. The focus/pointer guard keeps the map from taking keys meant for anything else. | | A98 | **Mobile pass: 44px tap targets below `sm`/`md`, a BETA badge instead of the ribbon on phones, dvh + safe areas, 16px inputs.** An emulated audit (Edge, 390x844 / 360x800 / 768x1024, touch, both themes and languages) found no horizontal overflow but undersized controls everywhere. (1) **BETA**: the fixed corner ribbon is `md` and up only; below that a small amber badge sits next to the logo, so nothing covers content, the tour pill or the timeline popover. (2) **Tap targets**: header buttons, menu items, chips, quiz and status buttons, footer links and page links get `min-h-11` (44px) on phones and are reset at `sm`/`md`, so desktop is unchanged. Footer links use a `[data-site-footer]` rule in `global.css`, not `[&>*]`, which would also display the `