Note
This page is the repository file docs/DECISIONS.md, included at build time. Edit it there, not here.
Decisions and deviations¶
Entries 1 to 88 use the product's former name, Waypoint. It was renamed to Guidepost in entry 89; the old text is kept as written.
| # | Decision | Reason |
|---|---|---|
| 1 | pnpm 12 with allowBuilds for esbuild |
pnpm 12 blocks dependency build scripts by default |
| 2 | Zod 3 (not 4) with zod-to-json-schema | JSON Schema export tooling targets Zod 3 |
| 3 | Preact 10, TypeScript 5.9, Vitest 3, ESLint 9 | Pinned to versions the toolchain (WXT 0.21, typescript-eslint) supports together |
| 4 | matchOriginAsFallback for content script frames |
Needed to reach about:blank / srcdoc frames |
| 5 | Content script messaging is Zod-free | Keeps content.js at ~1.4 kB gzip against the 15 kB budget |
| 6 | Three build flavours via WAYPOINT_BUILD (store, managed, dev) |
Managed build bakes in approved origins; dev is used by e2e |
| 7 | Content scripts registered at runtime for granted origins only | Optional host permissions, no blanket access |
| 8 | Fixture servers on 5101-5104 (spec lists 5101-5103) | The claims wizard needed its own port |
| 9 | Backend is single tenant | Owner decision 2026-10-05; phases 7-9 need no tenant partitioning, tenant claims or per-tenant keys |
| 10 | MVP has no role/audience targeting and no SSO | Owner decision 2026-10-05; MVP is record, author, link docs, render on any app. auth.status keeps returning all roles; audience fields stay in the schema but are not evaluated. Targeting source (Entra claims suggested) and SSO move to step two |
| 11 | Hex-run rule in the stability filter only fires when the run contains a digit | Spec says "hex run of 6+ chars"; that rejects real words such as "facade" and "decade" |
| 12 | Normalize-target ignores tabindex="-1", role=presentation and role=none |
A click on plain text inside a focus-managed modal would otherwise climb to the whole modal |
| 13 | A positional xpath hit must score at the fuzzy bar (0.75) and share at least one ancestor heading with the stored fingerprint |
With the spec's weights a different "Edit" button in another section scores about 0.77 on text, tag, attrs and nearby position, and xpath hits whatever sits at that position |
| 14 | Strategy uniqueness uses visible matches only, in both capture and resolve | Hidden template copies and inactive wizard stages must not make a locator ambiguous |
| 15 | Field values and select options are never put in the fingerprint | Spec section 1 non-goal: no capture of field values |
| 16 | Theme token types are strings (fontSize, radius) and re-exported from @waypoint/schema |
One source of truth; the schema already stores CSS strings |
| 17 | Renderer tests run under jsdom, not happy-dom | happy-dom's DOMPurify keeps onerror, javascript: hrefs and iframes |
| 18 | Esc and arrow keys are handled at document level only when the target is not editable or interactive | Typing in a host form field must never advance the tour |
| 19 | Spotlight is non-blocking (pointer-events: none); BlockOutside provides the click-block with a shake |
Spec separates highlighting from blocking |
| 20 | Renderer components use useState, not @preact/signals |
Props are plain values from the engine; signals added weight with no benefit. Revisit in the engine |
| 21 | Platform independence is a hard requirement: nothing assumes a framework, language or server stack | Targets include Dynamics 365, Angular, .NET WebForms, Java, Duck Creek. Locator already ranks stable attributes over generated ids, handles iframes and shadow DOM. A WebForms-style e2e fixture (generated ctl00_ ids, __doPostBack, full-page postbacks, ViewState) is added to cover server-rendered stacks |
| 22 | The engine uses its own path matcher (:name one segment, * anything, path?param=val requires query params, #... matches the hash) instead of URLPattern |
URLPattern is not available in every supported browser build and has no query or hash shorthand; WebForms and Dynamics tours need ?step=2 style matching |
| 23 | The player is an unlisted script injected by the service worker through scripting.executeScript; the content script is only a 2 kB gate |
No remote or dynamic-import code (spec rule), and pages with no matching tour pay for one small script and zero DOM nodes |
| 24 | Cross-origin iframes use a service-worker relay: the frame agent resolves the locator tail and returns a rectangle; the top frame places an invisible ghost anchor and refreshes it every 300 ms | A cross-origin frame cannot be read from the top document, and the renderer needs a real element to anchor to |
| 25 | Origin matching ignores ports, like Chrome match patterns, and supports *. subdomain wildcards |
Trust checks must agree with what the browser actually grants |
| 26 | The player's shadow root is open only in the dev flavour (WAYPOINT_BUILD=dev); the store build keeps it closed |
Playwright needs to look inside; production hosts should not be able to read tour UI |
| 27 | For server-rendered forms the recommended advance is any[form-submitted, page-matches(next URL)] |
__doPostBack calls form.submit(), which fires no submit event; resume after reload completes the step |
| 28 | The picker runs in every frame (allFrames) and is re-injected by tabs.onUpdated while a session is stored in storage.session |
Recording has to survive full-page postbacks and iframes; the session entry is cleared on stop and when the tab closes |
| 29 | A pick inside a cross-origin frame is normalised by the service worker, which asks the top frame for the <iframe> hop (waypoint.picker.hop); one hop level only |
A cross-origin frame cannot name its own element. Deeper nesting is deferred |
| 30 | Preview sends the draft tour and draft resources to the player (waypoint.preview.start); it writes no progress and emits no events |
Authors must be able to test unsaved work without polluting analytics or per-user state |
| 31 | The studio is lazy-loaded behind the author-mode gate | Viewers should not download the editor |
| 32 | Walkthrough.startStepId may be an empty string while a draft has no steps; the studio checks block publishing in that state |
A new walkthrough is created empty, then filled by picking or recording |
| 33 | The frame registry write in the service worker is serialised through a promise chain | Frames register simultaneously; an unserialised read-modify-write lost entries and made cross-origin anchoring flaky |
| 34 | Ambient content (tips, launchers, hotspots, announcements, resource center, stepper) renders in a second host, separate from the walkthrough host | A tour can be active while a field tip or launcher stays on the page; each host has its own shadow root and z-order |
| 35 | (Superseded by 75.) Ambient anchors are resolved in the top document and same-origin context only, and re-evaluated on URL change; no cross-origin iframe tips in the MVP | The cross-origin relay (decision 24) returns rectangles for one tour step; per-field tips would need a ghost anchor per tip |
| 36 | Process progress stores the current stage in stageId and the completed stage ids in path |
Reuses the Progress shape; the eligibility check resumes only on stepId, so the player handles process resume itself |
| 37 | Process stage walkthroughs should use manual-only triggers |
A stage walkthrough must start when the process reaches the stage and the page matches, not on its own page-load |
| 38 | Validation tips appear after the field loses focus and re-check on every input | Showing an error while the user is still typing the first character is hostile |
| 39 | (Superseded by 69.) Preview is walkthrough-only | Ambient content has no step sequence to preview; authors verify it on the live page |
| 40 | Announcements auto-show only for a page-load trigger; a launcher or resource center start-content action forces one regardless of frequency |
Manual starts must always work |
| 41 | The ui-lab and extension e2e run against localhost:5116 (and 5106 for the lab) instead of 127.0.0.1 |
The dev build grants host permissions to localhost only; 127.0.0.1 would need a broader grant that the store build must not have |
| 42 | F8 (inline banner that pushes the page down) stays test.fixme |
It needs a new Banner schema field, which is a schema change outside this pass. The stepper already avoids a top banner through the published --wp-banner-top-h variable |
| 43 | While a host <dialog> is open modally, the renderer host is reparented into it and moved back on close |
Content outside the top-layer dialog is inert; a tooltip left in <body> cannot be clicked |
| 44 | pickFrame in the frame lookup lets the newest registered frame win |
A reloaded or navigated iframe re-registers; the older entry points at a dead frame |
| 45 | Esc dismisses a step only when allowSkip is true |
A mandatory step must not be closable from the keyboard when its Skip button is hidden |
| 46 | A corner-message auto-hide does not store a dismissal (onAutoHide) |
Timing out is not a user choice; storing it would stop the message ever showing again and skew analytics |
| 47 | Slideouts use logical sides (inset-inline-start/end); side: right means the inline-end edge |
Matches how RTL pages mirror their own panels |
| 48 | Beacon, tip icon, inline hint and launchers read direction from the anchor (isRtl, which wraps dirOf: nearest [dir], then the document dir, then the computed CSS direction) and mirror their offset |
The host page decides the writing direction, not the extension |
| 49 | The cross-origin ghost carries data-wp-synthetic; the runner neither scrolls nor guards it |
It is transparent and sits under the frame, so the visibility guard would re-resolve it every tick |
| 50 | The player uses isRenderable (not just "exists") when resolving anchors |
An element hidden or collapsed by a sticky or clipped container is not a usable anchor; resolve keeps waiting or falls back per onMissing |
| 51 | The banner publishes its height as --wp-banner-top-h on the ambient root; the stepper offsets itself with a :has rule |
Lets two independent layers avoid each other without knowing about each other |
| 52 | A left or right tooltip placement may flip to top or bottom when neither side fits; top and bottom keep best-fit | Prevents a 240 px tooltip leaving a 390 px viewport without changing the default behaviour |
| 53 | A tip icon with no room beside its field is drawn inside the field's end edge | A full-width field on a phone leaves no gutter |
| 54 | The e2e measure iframe anchors in the top document from the frame rect and the inner rect scaled by rect.width / offsetWidth |
Playwright's frame boundingBox ignores CSS zoom on a frame's ancestors, so it cannot be the reference in the zoom environment |
| 55 | Per-app custom CSS is limited to an allowlist of --wp-* custom properties (CSS_VAR_ALLOWLIST in @waypoint/theme). Values containing ;, braces, url(, @import, expression( or javascript: are dropped. The check runs in the studio (error shown) and again in the player |
Arbitrary CSS in a shadow root can hide content, exfiltrate through url() requests or break layout in ways the author cannot see. Tokens cover colour, font, radius and tooltip width, which is what theming asks for |
| 56 | The theme is re-extracted on class, style, data-theme, data-bs-theme and data-color-mode changes of html and body, and on prefers-color-scheme changes, debounced 150 ms. applyTheme sets inline token variables, so per-app cssVars are re-applied after every re-extraction |
Dark-mode toggles in the host must not leave a light tooltip on a dark page; without the re-apply, the app override would be lost on the first toggle |
| 57 | Trigger model: no trigger, or page-load without delayMs, starts immediately. Every other trigger is armed once per page pass (armTrigger) and fires through the normal eligibility check. Walkthroughs and processes are marked triggered; announcements and hotspots are forced after the check |
One code path for frequency, segments and schedule regardless of how the content was started. A trigger that has fired does not re-arm until the next URL change |
| 58 | schedule.from and schedule.to are whole days, inclusive at both ends, in the reader's local time; enforced by the player before eligibility, copied into the manifest by the store |
Authors think in "until Friday", not timestamps. Server-side enforcement is not possible with a local store |
| 59 | A checklist item completes when its completedWhen goal is met: page reached, element clicked, field filled, element appears, or content completed. Completed item ids are written to the progress path of the checklist; the checklist's state becomes completed when all are done. Goals are watched while the checklist is on screen, and a 1.5 s refresh covers goals met in another content item |
Reuses the Progress shape and the storage the player already persists; no new store |
| 60 | Checklist.display = 'launcher' shows a standalone pill with a progress bar, independent of the resource center. The default stays resource-center |
Some apps want the checklist without any help bubble |
| 61 | The unseen-announcement badge counts announcements with a resource-center trigger that match the page, are within schedule and have no completed progress. Opening one from the help bubble marks it seen |
"Unseen" needs one definition. Reusing the completed state means a seen announcement also respects frequency: once elsewhere |
| 62 | App.helpBubble.enabled now defaults to true in makeApp (it was false). The bubble is still only drawn when there is something to show (resource center, checklist or resume). position is physical (bottom-left or bottom-right); launchers, TipIcon, Beacon and InlineHint offsets are logical and mirror when the anchor reads right-to-left (isRtl, decision 48) |
A disabled-by-default bubble made the setting invisible. Physical position for the bubble matches what authors see in the studio; offsets next to a field must follow the field's reading direction |
| 63 | UI strings use one key table (DEFAULT_STRINGS). A locale is picked from navigator.languages, full tag first then language only, then the content locale, then English; missing keys fall back to English. Author text (titles, bodies, labels) is not translated by this mechanism |
Covers the labels the player adds itself (Next, Back, Got it, step counter, checklist progress) without a translation workflow for authored text, which stays step-two |
| 64 | Validation tips accept an optional list of extra rules with a combine mode: all shows the message when any rule fails (default), any only when every rule fails. Each rule can carry its own message |
Real fields need "required and matches pattern"; a single rule per tip forced duplicate tips on the same field |
| 65 | Item 6 dispositions: rows 26, 81 and 82 belong to the F8/F9 work and are not touched here. Rows 50 and 59 (surveys, segments) are step two and stay Missing. Announcements now also auto-show for delayed or event triggers, which extends decision 40 | Keeps the in-MVP Missing list at zero without pulling step-two scope forward |
| 66 | Hint position is the author's choice: auto \| below \| above \| after-end \| before-start (HintPositionSchema, default auto) on the inline-hint step display, SmartTip.hintPosition and validation.hintPosition (validation value wins, then the tip's, then auto). Old content saved after; a preprocess reads it as after-end, so no schema version bump. auto keeps the collision-aware search. An explicit position is honoured exactly: if it leaves the viewport the hint is shifted and clamped, never flipped to another side. after-end and before-start mirror in RTL. A guidance tip's popover maps the same value to a tooltip placement (hintPlacement) |
What sits above, below and beside a field differs per field, so no single search order is right; only the author knows. An explicit side that lands on content is the author's call, so the renderer reports it (data-wp-overlap="true" and a console.warn) instead of silently moving the hint. The studio has no cheap page-measuring path (preview plays whole tours), so it explains that the preview flags overlap |
| 67 | Inline banner: Announcement.inline (optional boolean, banner displays only). When true the Banner adds its measured height to document.body padding on its edge (top or bottom) on top of the page's existing computed padding, follows height changes with a ResizeObserver, and restores the body's original inline value on unmount. It still publishes --wp-banner-top-h. The studio shows a "Push the page down" checkbox for banner displays. Supersedes decision 42 |
Covering the host header hides the app's own navigation; padding on body is the least invasive way to make room without touching host layout rules. Sticky or fixed host chrome keeps its own offsets, so a page whose header is position: fixed is not pushed; that is a known limit |
| 68 | F10 media under a strict host CSP needs no extension-page iframe. With ?csp=strict (default-src 'self'; img-src 'self'; frame-src 'self') a step's data: image and a third-party video iframe, both created by the content script inside its shadow root, load and render, while the host page's own cross-origin frame is blocked by the same policy. e2e/specs/csp-media.spec.ts guards this, including the blocked control frame, so the test fails if the policy stops being enforced. The media.html / web_accessible_resources design in UI-COVERAGE F10 is not built |
Chromium does not apply the page CSP to resources requested by an extension's content script for its own UI. Adding an iframe page would add surface (web-accessible page, extra entrypoint) for no gain. Only Chromium was tested; re-run the spec if another browser is ever targeted |
| 69 | Studio "Preview on page" works for tip sets, launchers, hotspots, announcements, resource centers, checklists and processes, not only walkthroughs. preview.play already carried any content item; preview.stop (new, {tabId}) removes an ambient preview and stops a walkthrough one. The player mounts the draft as ambient content that ignores match rules, frequency and trigger, writes no progress and emits no events, and evaluate/evaluateAmbient are no-ops until stopPreview, which clears everything and re-evaluates. The editors show Preview on page and End preview buttons. Supersedes decision 39 |
Authors need to see an unsaved tip set or announcement on the real page without publishing. A preview that could record progress or events would pollute analytics and suppress the real content afterwards |
| 70 | A checklist previews as its launcher pill whatever its display; a process previews through startProcess with the stage walkthroughs passed as draft resources (previewResources). A resource has nothing to render and does nothing. preview.play retries tabs.sendMessage for up to 3 s because the freshly injected player registers its listener after its first worker round trips |
The pill is the only standalone checklist surface; the resource-center variant needs a bubble that may not be on. The retry fixes a race that made the first preview after injection fail with "Receiving end does not exist" |
| 71 | Show all / hide all for tip-set hints: when a page has tip icons the ambient layer draws a HintsToggle chip (strings ui.hideHints, ui.showHints, localisable). The choice is stored in sessionStorage under waypoint:hints-hidden; blocked storage degrades to in-memory. Only icon tips hide; hotspots, launchers and open tooltips are unaffected |
A form full of icons is noisy once learned; tab-session scope avoids a permanent opt-out that the author never sees. sessionStorage needs no permission and no sync |
| 72 | Announcement media: content.media is rendered for modal and slideout displays. An image is a hero above the modal title (Modal.hero) or at the top of the slideout body (.wp-slide-hero, max 200 px, cover); a video goes in the body through the existing MediaView. Images are loaded through asset.get into a data: URL; a failed load drops the media but still shows the announcement. Banners, toasts and corner messages ignore media. Works under a strict host CSP (decision 68). The studio shows an image or video field for those displays and validates it |
Reuses the step media path and its CSP finding. Banners and toasts have no room for media; showing it would break their one-line layout |
| 73 | Banner rich text: the message is sanitised Markdown. When it contains a link (Markdown link or bare URL, hasLink) the message wraps with a two-line clamp (.wp-banner-msg--wrap); without a link it stays one ellipsised line. Bold and links render inline |
A link inside a truncated line cannot be reached; two lines keep it visible at 390 px without letting a long notice grow the banner without bound |
| 74 | Announcement throttle: App.maxAnnouncements (optional int >= 1, no SCHEMA_VERSION bump, same approach as decision 67), delivered through app.context. Applied after eligibility, so a queued announcement is not reported as shown until it appears. Explicitly started items (forced) go first. A dismissal re-runs the evaluation so the next queued announcement appears, and a preview never counts toward it. Default is no limit. The studio sets it in App settings |
Stacked announcements are the main complaint about guidance tools. Putting the cap at the app level avoids per-item fields and matches how Chameleon-style throttles are configured |
| 75 | Ambient content anchored inside a cross-origin iframe: each anchored tip, launcher and hotspot owns a createCrossFrameResolver (one ghost box each), keyed set:tip, l:id or h:id, created when find reports cross-origin-frame and released when the spec leaves the set (pruneFrameResolvers, anchorKeys). The frame-agent relay and ghost anchor (decisions 24 and 49) are unchanged. partials.spec.ts checks a tip icon and a launcher on the ui-lab cross-origin frame. Supersedes decision 35 |
One shared ghost cannot serve several anchors in the same frame. The relay already existed for walkthrough steps, so ambient content only needed a per-anchor resolver |
| 76 | The built manifest's host_permissions is set by a build:manifestGenerated hook (hostPermissionsFor in build-flavour.ts): store none, dev http://localhost/*, managed only WAYPOINT_APPROVED_ORIGINS. Edge uses the store rules. Fresh arrays are built on every call |
WXT copies the content script's runtime matches into host_permissions, and the store build shipped https://*/* plus localhost, contradicting decision 7 and PERMISSIONS.md and making every https origin look granted. WXT also mutates the array it is handed, so a shared array made the first fix a no-op |
| 77 | The picker reads the real event source with composedPath()[0] (fallback target) in its move, click and change handlers |
Document-level listeners see events from inside an open shadow root retargeted to the host element, so the picker highlighted and captured the custom element instead of the button inside it. Found by the nested shadow DOM e2e |
| 78 | WAYPOINT_BROWSER (default chromium) sets the Playwright channel in launchWithExtension; msedge runs the e2e suite in Edge. The manifest declares picker-start and picker-pause commands but no commands.onCommand listener exists, so the browser-level shortcuts do nothing; only the in-page Alt+Shift+Space pause (picker keydown) works and is what the e2e covers |
Edge was not installed on the build machine, so Edge is verified only statically (decision 76 manifest, build output). Wiring the commands is a separate piece of work |
| 79 | The manifest key is no longer committed. wxt.config.ts sets it from WAYPOINT_MANIFEST_KEY (manifestKeyFor in build-flavour.ts), only for the dev and managed flavours and only when the variable is set; the store and Edge builds never carry it. e2e needs no fixed id (every spec reads the id from the service worker URL), so nothing sets the variable by default. RUNBOOK explains how to generate a key |
Chrome Web Store rejects an upload whose manifest contains key. The store assigns its own id. A managed deployment that force-installs by id still needs a stable id, so it supplies its own key; a public key is not a secret, but the project does not own one worth committing |
| 80 | The manifest version is derived from apps/extension/package.json in the manifest hook (manifestVersionFrom): the numeric core goes in version, and a pre-release or build suffix goes in version_name unchanged. Pre-releases are published to GitHub only, never uploaded to a store. The root and extension package.json versions move together (release-please extra-files) |
Chrome accepts one to four dot-separated integers in version. Mapping a suffix into a fourth integer would order pre-releases above the final release, so the suffix is kept for display only |
| 81 | The identity permission is removed from the manifest and PERMISSIONS.md. Nothing calls chrome.identity or browser.identity (grepped across apps, packages and e2e). It returns with the Entra SSO work |
An unused permission is a prompt and a review question with no benefit. Declaring it in advance also widened the install warning list for every user |
| 82 | Dependency updates use Renovate (renovate.json), not Dependabot. WXT, Vite and Preact update in one group; Playwright, the lint stack and Vitest are grouped too. Every GitHub Action is pinned to a full commit SHA with a version comment, and Renovate keeps the pins current. @types/chrome is pinned to an exact version |
A WXT bump usually carries a Vite range change and the Preact preset targets both, so separate PRs fail CI in pairs. SHA pins stop a moved tag from changing what runs in CI with a release token in reach. latest made builds depend on the day they ran |
| 83 | CI runs as parallel jobs: lint and typecheck, unit with coverage artifact, build (store, managed with a dummy origin, edge) each followed by the bundle budget and a manifest check, e2e on Chromium, licence check, and a reproducibility check. CodeQL and dependency review run in their own workflows. The Edge e2e job runs nightly and on demand with continue-on-error: true |
Splitting the jobs shortens the feedback loop and shows which gate failed. Edge has never had a clean full run (decision 78), so it reports without blocking until it is stable; then the flag comes off and the job joins the required checks |
| 84 | Bundle budgets live in one block (BUDGETS in scripts/bundle-budget.mjs): gzip limits for content.js (15 kB), player.js (100 kB), picker.js (32 kB), frame-agent.js (10 kB) and 900 kB unpacked for the whole output folder. About 25 to 30 percent headroom over the sizes measured on 2026-10-06 (2.1, 75.2, 23.9, 7.6 gzip; 695 kB total). The script prints a table and appends it to $GITHUB_STEP_SUMMARY |
Growth should be a deliberate change to a number in review, not a surprise. The headroom absorbs normal feature work |
| 85 | Release flow: release-please opens a release PR from conventional commits and updates versions and CHANGELOG.md; merging it creates the v* tag. release.yml verifies (lint, typecheck, unit, licences, e2e, reproducibility), builds the store, Edge and managed zips with SOURCE_DATE_EPOCH from the tag commit, writes SHA256SUMS, a CycloneDX SBOM (pnpm sbom) and THIRD-PARTY-NOTICES.md, attests build provenance in a job that alone holds id-token and attestations: write, then attaches everything to the GitHub Release. The managed zip is built only when the repository variable WAYPOINT_APPROVED_ORIGINS is set. The initial Unreleased section of CHANGELOG.md is deleted by hand when the first release PR is merged |
A tag pushed with the default GITHUB_TOKEN does not start workflows, so release-please.yml uses the secret RELEASE_PLEASE_TOKEN when present, and release.yml can be run by hand against an existing tag. Keeping the write-scoped steps in separate small jobs limits what a compromised build step can reach |
| 86 | publish-stores.yml is written but disabled: both jobs need the repository variable STORE_PUBLISH_ENABLED == 'true', run only from workflow_dispatch, and sit behind the chrome-web-store and edge-addons environments (required reviewer set in repository settings, store credentials stored as environment secrets). Chrome Web Store API v2 upload uses STAGED_PUBLISH; Edge uses Add-ons API v1.1 and submits only when asked. The jobs were linted with actionlint and shellcheck and have never been run against a real store |
Store publishing is not happening yet. Writing the jobs now fixes the shape (human approval, checksum verification, no secrets in the repo) so enabling them is a settings change plus a first supervised run |
| 87 | The store zip is reproducible on one machine: scripts/check-reproducible.mjs builds it twice from a clean .output and compares SHA-256; both Chrome and Edge matched on 2026-10-06. WXT writes fixed zip timestamps, so no code change was needed. Not yet checked: a different checkout path, a different OS, or a different Node patch version. CI runs the check on Ubuntu, and the release workflow gates on it |
A reproducibility claim is only as wide as what was tested. The gap is recorded so nobody quotes it as "builds are reproducible everywhere" |
| 88 | Licence: MIT, copyright Nitin Singh. No CLA. A Signed-off-by line (DCO style) is encouraged in commits and documented in CONTRIBUTING.md, not enforced by a check. The project name and logo are not licensed by MIT (TRADEMARKS.md). The core stays MIT; a hosted offering, if it comes, is a separate service on top. CI fails on GPL or AGPL dependencies (scripts/check-licenses.mjs) and reports LGPL |
Contributors keep their copyright and license under the project licence by submitting. A licence check on the dependency tree is cheaper than discovering a copyleft package after a release |
| 89 | The product is renamed from Waypoint to Guidepost (maintainer's decision). Everything the repo owns moves: the @guidepost/* package scope, GUIDEPOST_BUILD, GUIDEPOST_APPROVED_ORIGINS, GUIDEPOST_MANIFEST_KEY, GUIDEPOST_BROWSER, __GUIDEPOST_DEV__, the guidepost-root, guidepost-picker and guidepost-ghost elements, the guidepost.* internal message types, the IndexedDB name guidepost, the guidepost:hints-hidden session key, the alarm name, release asset names, the manifest and locale strings, and the docs site. The wp- CSS prefix and data-wp-* attributes stay (internal, no product name). Existing data is kept: on first open of the default database LocalStore copies the old IndexedDB database into guidepost inside the creating upgrade transaction (all stores or nothing), skips the copy when guidepost already exists, and never deletes or edits the old database (packages/store/src/legacy-db.ts). Extension storage is handled by migrateStorageKeys in apps/extension/lib/rename-migration.ts: keys prefixed with the old name are copied to the new prefix in storage.local and storage.session, never overwriting, then renameMigrated is set in storage.local. No extension storage key actually carried the old name (settings, frames:*, picker:* are unchanged), so today the function is a safety net and the copy is a no-op. Bundles: the zip layout (bundle.json, assets/, signature.json) has no product marker and is unchanged, so .waypoint.zip files import as before; exports are named <appKey>.guidepost.zip, the file picker accepts both suffixes (packages/store/src/bundle-compat.ts), and tests cover importing a legacy-named bundle. The extension id is unaffected by the name; the Chrome Web Store listing and any force-install policy keep working. Old release assets and CHANGELOG entries keep the old name |
The new name was chosen before any store listing or public release, so the only people affected are local installs. Copying instead of moving keeps a downgrade possible and removes the risk of a half-finished delete losing content |
| 90 | The picker-start (Alt+Shift+P) and picker-pause (Alt+Shift+Space) manifest commands now have a handler, replacing the gap recorded in 78. browser.commands.onCommand calls handleCommand (apps/extension/lib/commands.ts), which acts only when author mode is on, the active tab is http or https, permissions.contains says the origin is granted, and, for pause, a picker session exists in storage.session for the tab. Start reuses the side panel's path (startPickerSession, the same function behind the picker.start message) with mode element. Pause keeps its state in storage.session (picker-paused:<tabId>) and sends the page-side picker an explicit guidepost.picker.pause with the wanted state, so frames cannot drift apart as they could with a toggle. The state is cleared on stop, on restart and when the tab closes. The dev flavour exposes globalThis.__guidepostTest.runCommand so Playwright, which cannot press extension command keys, drives the same function (e2e/specs/commands.spec.ts) |
Declared shortcuts that do nothing are a defect. Gating on author mode and granted origins keeps the shortcut from injecting into a site the user never approved |
| 91 | The e2e fixture claims-wizard (port 5104) is renamed multi-step-form, its URL is /forms/new, and its copy and test ids are generic (reference number, category, details, applicant) because it is a generic multi-stage form, not an insurance app. Specs that used it (real-form, locator, features-extra) and the runbook were updated together. The other fixtures keep their claims-style sample copy; the showcase scenes and docs/screenshots/ui do not use this fixture, so no screenshot changed |
The old name suggested the product targets insurance only |
| 92 | Hosted-edition seam, no behaviour change. createContentStore(config) in packages/store is the only place a store is built; today it returns LocalStore. AuthProvider is an interface with a fixed local user. The managed build declares storage.managed_schema (public/managed-schema.json, reserved backendUrl and tenantId) so admin policy can reach the extension later; the store and dev builds do not. The schema must never gain a "run script" step type (remote-code policy). |
|
| 93 | The store build's optional_host_permissions now lists http://localhost/* and http://127.0.0.1/* next to https://*/* (optionalHostPermissionsFor in build-flavour.ts). Before this, permissions.request for any http origin failed with "Only permissions specified in the manifest may be requested", so on a local dev server such as ng serve the origin was never granted, no content script was registered, and neither preview nor a published tour appeared. The build still holds no host_permissions; each site is granted by the user from a click. Other plain-http sites (an intranet) stay unsupported in the store build and use the managed build. The studio now says so (siteProblem) and turns the missing-access error into a plain sentence (friendlyAccessError). |
Authors try the extension on localhost first, and the failure was silent. Loopback is the narrowest http addition that fixes it |
| 94 | Drafts autosave: both editor hooks (useDraft, WalkthroughEditor) save 600 ms after the last edit, on pagehide and visibilitychange, and on unmount, through one queue (useAutosave) so two saves never race on the etag. The etag lives in a ref and dirty is only cleared when no edit happened during the save. Back saves first instead of asking to discard. A published item can be switched off for the site without touching its draft or history: ContentStore.setLive sets paused on the head record, getManifest skips paused heads, ContentSummary.live feeds the Show on site checkbox in the Content list, and publishing again turns it back on. The change takes effect when the page reloads, because the content script reads the manifest once per page load. |
Edits made in the side panel were lost when it closed, and there was no way to hide published content from a demo site without archiving it |
| 95 | Recorded and picked steps get readable defaults. nameOf (lib/picker/names.ts) prefers aria-label, aria-labelledby, the associated label, placeholder, a submit button's value, its own text, then a tidied name or id (humanize turns ctl00_txtClaimNumber into "Claim number"). stepTitle and stepBody write "Click Save", "Enter Claim number", "Choose Status" and a one-line instruction in the body, so a recorded tour reads well before any editing. The studio stylesheet gets one input style for text, number, date and time fields, hover and focus states, and softer cards. |
A recorded tour showed raw element names such as "name" with an empty body, which looked unfinished in a demo |
| 96 | Resource centre tabs are defined and configurable. Help lists the sections; a section is shown only on pages its optional showWhen matches (before this the field only fed autoSuggest, and the studio never set it). Tasks shows ResourceCenter.tasks.checklistIds in the author's order, gated by tasks.showWhen; with no list it keeps the old rule (the first non-launcher checklist whose own pages match). Search indexes every item in a visible section plus the tasks of the shown checklists (matching title, summary and keywords; walkthroughs and processes use their name and description). A section's optional tab is help (default) or search; search indexes its items without listing them under Help. Sections cannot belong to Tasks, because Tasks is a checklist surface (progress, goals), not a list of links. All three fields are optional, so there is no SCHEMA_VERSION bump (same approach as decisions 67 and 74). Segments and per-user rules do not filter the centre; only published, enabled content is reachable, because items resolve through the manifest. The studio editor explains the tabs, edits showWhen with PagesField, picks Tasks checklists and limits section items to walkthroughs, processes and resources (the only kinds the player draws) |
The user could not tell which tab content lands in, and showWhen was documented as restricting a section while the player ignored it. An explicit checklist list overrides the checklist's own page match because the author chose it for this centre. tasks.showWhen and section showWhen use the same page pattern UI as everything else, so authors learn one control |
| 97 | The player reads the page theme from what the user sees, not from body and html. extractThemeDetailed samples the elements under five points around the viewport centre (skipping our own guidepost-* hosts), composites translucent backgrounds down to the first opaque layer, then the canvas, and takes the median luminance so one dark sidebar or hero cannot decide the scheme. The canvas is the html background, else the body background (it propagates to the viewport when html has none), else white, or Chrome's #121212 when color-scheme names dark and either excludes light or the OS prefers dark. Text colour and font come from the nearest non-control element at the centre. watchTheme also re-reads on window load and when the class or style of body's direct children changes. The studio gets the same reading through the worker message theme.get, which injects the player into the tab and asks it, so the preview matches what the player will do |
A light app over a near-black body (a common overscroll trick) produced a black tooltip, a white wrapper on a transparent body depended on the canvas, and a color-scheme: light dark page in a dark OS was called light while it painted dark. The median over several points is cheaper and more robust than trying to find "the" main element |
| 98 | The App settings card has a style preview: the real Tooltip, Modal and Banner from @guidepost/renderer, inside an open shadow root in the side panel, over a stand-in page. A transform on each stage makes it the containing block of the renderer's position: fixed layers, so no renderer code changed. The host is inert so the modal's focus trap cannot take focus from the textarea. It is fed by the unsaved textarea text through the same sanitizeCssVars and applyCssVars the player uses, on top of Light, Dark or the open tab's extracted theme. The side panel now bundles the renderer (it was only in the player) |
Authors typed variables blind and found out on the page after saving and reloading. Reusing the real components means the preview cannot drift from what ships |
| 99 | Importing a bundle switches the studio to the application the imported items belong to and reports what happened in a sentence (summariseImport: items added, skipped, missing references) instead of the raw report. The Content list shows one application key at a time, so importing northbridge while the box said demo looked like a failed import. AGENTS.md is the single guide for AI assistants; CLAUDE.md imports it and .github/copilot-instructions.md summarises it for GitHub Copilot. |
A demo import appeared to do nothing, and assistants other than Claude had no project context |
| 100 | The resource centre Tasks tab is a working task list (all schema additions optional, no SCHEMA_VERSION bump, as in decisions 67 and 74). (1) Checklists shown in the centre are watched by the player whether the centre is open or closed: clicks through a capture listener, and field, URL, element-appears and content goals through the existing 1.5 s poll, which now also runs for the centre. The poll does work only while a shown list has an open goal of those kinds. (2) Ticks are stored as the list of done item ids in the checklist's own progress record, which is local storage only. On load the ids that still exist in the published version are kept, whatever version the record was written under, so a publish does not wipe anyone's ticks; Checklist.resetOnPublish (default false) restores the old behaviour for lists that should restart. (3) ChecklistItem.contentId is optional and may name a walkthrough, process, announcement or resource (Show me); description is one line under the label; a manual goal is ticked and unticked by the reader (a real checkbox button, so it works from the keyboard); Checklist.hideCompleted collapses a finished list to one Done line, announced politely; ResourceCenter.tasks.badge (default false) adds the open task count to the bubble badge. A last-step-completed goal with no Show me target cannot ever complete, so Checks now blocks publishing it. (4) Tab labels the author types now reach the renderer and win; a label left at its English default falls back to the strings table, which gains ui.tasks, ui.noTasks, ui.showMe, ui.taskMarkDone, ui.taskMarkNotDone and ui.taskDone. (5) Authoring: the centre editor creates, edits, orders and removes task lists inline (a new list is a checklist with display resource-center, saved as its own draft the moment it is added, so it exists in the library even if the centre is never saved); Remove from center never deletes the checklist; the item editing body is one shared component used by the checklist editor too. Publish rule: publishing a centre also publishes every list in tasks.checklistIds that is new or has unpublished edits, after checking each one, and a list with an error blocks the publish and is named in Checks; unchanged published lists are left alone. Warnings say beforehand which lists will go out. The alternative, publishing only the centre and warning, leaves a published centre with an empty Tasks tab whose list is invisible to readers, which is the surprise we are removing. (6) The help bubble position gains top-right and top-left: a compact 32 px header button at the top edge, plus an optional helpBubble.offset of x and y pixels moving it inward (positive whole numbers, capped at 1000 in the renderer) so it can clear the host page's top bar. Corner toasts are lifted only for bottom placements, and the standalone checklist launcher stays on the right for header placements. The App settings style preview does not draw the bubble, so it is unchanged. |
Authors could not tell that the Tasks tab never ticked a checklist that was not a launcher, every publish reset all progress, typed tab names were ignored, and building a task list meant leaving the centre editor. Ticks follow the task id because the id is the stable identity of a task across edits; the version only tells us a publish happened. Keeping everything in local storage matches the local-only edition; the hosted edition syncs the same progress record later. A header button was asked for because a floating button at the bottom collides with some line-of-business layouts |
| 101 | Resource centre search is a small hand-written scorer (packages/renderer/src/search.ts, about 1.5 kB gzip), not MiniSearch or Fuse. Text is lowercased and accent-folded (NFKD); every query term must prefix-match a word; per term the best field wins: title 3, keywords and synonyms 2, summary, walkthrough step titles and link name 1; an item on the current page gets +2 (its section's showWhen fits, or its own non-catch-all page patterns fit); ties keep the listed order. Indexed: title, summary, keywords, task labels of the checklists shown in Tasks, walkthrough step titles and process stage labels (read on the first keystroke through CenterView.loadSteps, not at load), and the host name and file name of a document, link or video URL. Walkthroughs and processes gain an optional keywords (fallback tags), edited in the studio next to resources. ResourceCenter.search is optional: synonyms (symmetric groups, matched at score 2 so a literal title hit ranks first), exclude (hides from results only), popular (item ids shown before typing; this replaces any usage telemetry, and nothing about searches is collected) and noResults (text and an optional http, https or mailto link). The input is hidden when the Search tab is off; otherwise it is an ARIA combobox over a listbox (aria-activedescendant, arrows, Enter opens the active or first result, first Esc clears the query, second closes the centre) with a polite result count. Strings are ui.searchPlaceholder, ui.searchCount, ui.searchCountOne, ui.searchNone and ui.searchPopular. All fields are optional, so no SCHEMA_VERSION bump. Not in v1 (v2 candidates): step bodies, announcement text, tip text, document full text, typo tolerance, step-level deep links from a result into the middle of a walkthrough |
A search box that finds only exact substrings of titles misses how people ask ("first notice" for FNOL, an accented name, the file name of a PDF). Authors can fix recall with keywords and synonyms and curate the empty state with popular without us collecting anything, which the local-only edition forbids. A dependency would cost more than the scorer for a few hundred items, and the prefix rule keeps results explainable in the studio |
| 102 | Bundle import and the studio layout. (1) An item imported as a draft always lands at version 0, whatever version it had where it was exported: nothing is published here, so the list shows no Show on site switch and the first publish is v1. Before, a published-then-edited item arrived as "draft v2" with a live switch that threw "Published version not found". Export still carries only the latest saved draft, not published versions, so an edited item travels as a draft (documented in the bundles guide). (2) BundleCodec.read throws BundleReadError with a reason (not-zip, no-manifest, bad-json, newer, invalid); the studio maps each to one sentence (describeImportError) and never shows parser or Zod text. A bundle or item schemaVersion above ours is rejected as newer; before, the reader overwrote the bundle's version with ours and failed later with a schema dump. (3) The import sentence separates drafts that stay drafts, published items that became new versions and drafts that replaced drafts. The file input is cleared after each pick so the same file can be chosen twice. (4) Editors share an ActionBar (status, change note, Save draft, Publish) that is position: sticky at the bottom of the panel and says how many errors block Publish, so a long form no longer hides it. .stack and label grids use minmax(0, 1fr), and links and chips wrap anywhere, so a long unbroken name or a wide select cannot make the panel scroll sideways at 300 px. The documentation link editor now autosaves like the others and refreshes its etag after publishing (a second Save raised a revision conflict). Dark mode gets its own --ok, --warn and --err for contrast |
The import path was the first thing a colleague tries and it misreported drafts; the layout problems showed up only at 300 to 360 px with real names. A typed reason keeps UI wording out of the store package |
| 103 | Tasks and Search review fixes. (1) A synonym phrase typed in part now matches: first notice finds items for the phrase first notice of loss, as long as the typed words reach the end of the query (variants in search.ts). Before, a reader got nothing until the whole phrase was typed. (2) search.exclude also removes an item from the Popular list. (3) A resource center needs at least one section or one task list, not a section always, so a Tasks-only center can be published. (4) The element pickers in the studio no longer share a stale tab id: an idle usePickSession forgot nothing after its pick, so the next pick made by another picker (a second task goal) overwrote the first one's element. The session now clears its tab and handler on stop and on picker-stopped. (5) The Done when choices carry one plain sentence each (GOAL_HELP) and the Search card help is split into what is searched and how it matches. No schema change. |
Found by building a center from scratch as a first-time author: two element tasks both ended up pointing at the last picked element, a partially typed synonym found nothing, and a Tasks-only center could not be published |