Release planning

CadCraft 1.0 impact map

Every fix and feature lined up for 1.0, and what each one touches: which apps, and which layers of the stack.

ProjectCadCraft
Sheet0.10 impact
Items110
Date2026-11-09
51Fix
22Feature
0Spike
28Platform
0Site ops
8Sanity check
1Decision
≈ 319days in the written 0.10 plans
  • Planned 101 on the plan for the release it is filed under
  • Built 0 built and working in /lab (the dev builds). Whether the public has it is its reach: "in the lab", "Live" (the last gate) or "switched off"; see docs/REACH-LIVE.md
  • Partly built 1 part of it is built in /lab; the rest waits (see "Waits on")
  • Promised 0 the live site already says it is coming
  • Moved 0 moved here from an earlier release or wishlist
  • Found 0 bug found after a release shipped; fixed in a point release or the next one
  • Candidate 3 recommended in a spike or session log; owner to choose
  • Community please test 0 built and in /use; a Discord post gives the steps and the expected result and asks people to try it on their own devices, files or know-how
  • Decision Time 5 a decision, check, file, key, flag or go only you can give; folds into the list once answered

Built means built and working in the lab. Live means the public can use it, and is the last gate: 0 Built · Live, 0 Built · in the lab.

Sheet 1 · impact areas

Where 0.10 lands

How many items touch each app and each layer, by kind. One item can touch several areas, so the rows add up to more than the item count. Pick a row to filter the schedule below.

Impact areaFixFeatureSpikePlatformSite opsSanity checkDecisionAll
Apps
CadCraft 20 17 – 10 – 3 1 51
BimCraft 18 20 – 9 – 4 1 52
ModelCraft 19 20 – 10 – 3 1 53
MarkupCraft 12 16 – 9 – 3 1 41
TakeoffCraft 14 16 – 9 – 3 1 43
CivilCraft 21 18 – 10 – 4 1 54
DetailCraft 15 17 – 9 – 4 1 46
PartCraft 15 17 – 9 – 3 1 45
Studio – 1 – – – – – 1
SuperCad Studio – 6 – 6 – 1 1 14
Layers
Site and services 13 3 – 8 – 1 – 25
Desktop host – 1 – 1 – – – 2
App shell 5 15 – 8 – 2 1 31
App code 23 4 – 3 – – – 30
Our engines 8 – – 1 – 1 – 10
Vendor engines – – – – – – – 0
Drawing surface 3 – – – – – – 3
Help and words 2 1 – – – 1 – 4
Tests and tooling 2 1 – 2 – 3 – 8

Sheet 2 · schedule

Every fix and feature

Grouped by track. App chips carry each app’s layer colour; Studio is in ink. "Waits on" names what has to happen first; the small lines under it say who asked and where the item is written up.

Add an item Fork map (Rust and C++ forks) The roadmap

Issues sync every 10 minutes; last sync Nov 9, 2026 7:00 pm.

ItemWhatAppsLayersStatusDaysWaits onIssues
Performance roadmap — wave 3 60
FixPERF.41 Link to PERF.41Comment on PERF.41 Website: Library card lists run nine queries per card: /library/author 914 queries (897 ms), /library/browse 444 (587 ms) (extends PERF.12)

Measured with bin/admin-perf.php: LibraryItems::display() (LibraryItems.php:254-266) calls LibraryVersions::byId and LibraryAuthors::publicAuthor (Auth::byId plus LibraryAuthors::uuidFor, two statements) for every row browse() already selected; item-card.inc:16 then calls LibraryPreview::forVersion, which runs LibraryVersions::fileAccess (LibraryItems::byId again, LibraryVersions.php:524), LibraryItems::byId (LibraryPreview.php:55) and LibraryFiles::byId. None of the 115 lookup methods in the service layer except one has a per-request memo. Fix (a) S: a static per-request memo keyed by id in Auth::byId, LibraryVersions::byId, LibraryItems::byId, LibraryFiles::byId and LibraryAuthors::uuidFor, cleared by the write methods: removes about 600 of the 914 statements on the author page. (b) M: browse() selects the card columns in one statement (item, current version id, license, preview file, published_at, author name, maker flag, uuid) and display() takes the joined row; fileAccess() takes the item row it already has. Target: browse under 25 statements, author under 20.

– Site and services Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.42 Link to PERF.42Comment on PERF.42 Website: Library browse sorts every published item on a computed key and counts them again on every page; at 100x it is seconds per page

LibraryItems.php:152-157 orders by COALESCE(v.published_at, i.updated_at) DESC after the join, so LIMIT cannot help: EXPLAIN shows all 13,846 rows read through idx_browse, joined, then a filesort (64 ms) plus SELECT COUNT(*) (30 ms) plus categoryCounts (:171, 30 ms) per request; OFFSET 9600 takes 98 ms. At 1 M items the same plan is 5 to 7 s per browse page. Fix: a stored library_items.sort_at kept by LibraryItems::refresh() and on publish (the current version's published_at, else updated_at); indexes idx_browse_sort (visibility, status, sort_at, id), idx_browse_kind (visibility, status, kind, sort_at), idx_browse_cat (visibility, status, category, sort_at); ORDER BY i.sort_at DESC, i.id DESC reads 24 rows; keyset paging (?after=sort_at|id) past page 20; the count and categoryCounts cached 60 s per filter key or kept in a counts table on publish. Note: one account owns 13,844 of 13,846 items today, so the author filter plans say nothing yet about a real multi-author Library.

– Site and services Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.43 Link to PERF.43Comment on PERF.43 Website: Library search is LIKE '%q%' on four columns while an unused FULLTEXT index sits on exactly those columns

LibraryItems.php:200 browseWhere runs (i.name LIKE ? OR i.maker LIKE ? OR i.model LIKE ? OR i.description LIKE ?) with %q%; the schema has ft_search (name, maker, model, description) and sys.schema_unused_indexes lists it. EXPLAIN: 52 ms scanning 13,846 rows today versus 16 ms with MATCH … AGAINST ('door' IN BOOLEAN MODE) reading 1,324 rows from the index; at 1 M items about 3.7 s versus tens of ms. Fix: for queries of three or more characters use MATCH(i.name, i.maker, i.model, i.description) AGAINST (? IN BOOLEAN MODE) with +word* terms, keep LIKE for one or two character queries and as OR i.model LIKE ? for part numbers (innodb_ft_min_token_size is 3); the ngram parser later for CJK names. The same path serves /api/v1/library/items?q=.

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.46 Link to PERF.46Comment on PERF.46 Website: staff pages with uncached GROUP BY scans: /admin/library/metrics 887 ms, /admin/library 422 ms, /admin/hardware 265 ms

LibraryMetrics::report (LibraryMetrics.php:129, :151, :156) is 430 ms of the metrics page, one statement 222 ms (a 41k-row range on uq_once_a_day, a sort and COUNT(DISTINCT who)), linear in rows (2,157 new rows a day, 4 s at 10x). LibraryVersions::possibleDuplicates (LibraryVersions.php:587-604) is 383 ms per /admin/library view, a self-join over 27k versions that finds nothing today. HardwareReports::summary (HardwareReports.php:93) runs 13 GROUP BY scans of all 25,914 rows (240 ms; idx_day cannot help because every row is inside 90 days). MailLog::countsByStatus (MailLog.php:180) runs twice on /admin/mail (53 ms); RoadmapItems::groups runs seven times on /roadmap-issues with the same arguments. Fix: Cache::remember(…, 600) around report, summary and possibleDuplicates (the dashboard already works this way); better for duplicates, set duplicate_of_version_id at publish time in LibraryVersions::create (one indexed lookup on idx_item_sha / idx_source_sha) and list that; for metrics a nightly library_metrics_daily rollup or at least a covering index (day, kind, ref, action, who) and drop the redundant idx_day; call countsByStatus and groups once per request and pass the value down.

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.47 Link to PERF.47Comment on PERF.47 Website: missing, redundant and dead indexes (DDL in the report)

Missing: library_versions.submission_id (LibrarySubmissions::outcome, LibrarySubmissions.php:353, full-scans 27,185 rows, 40 ms per call; 26,537 versions carry a submission id) → ADD KEY idx_submission (submission_id). audit_log.action (Audit::search/searchCount :157-184 with a.action LIKE ?, StripeApprovals::siteRecords :71, DiscordConversations::itemsForMatching :321, Broadcasts::version :463; 52,791 rows growing 1,820 a day) → ADD KEY idx_action_time (action, created_at). roadmap_items.item_key alone (RoadmapSuggestions::findItem :28, ReplyDrafts::usedItems :99 query without release, so uq_release_key cannot serve them; every /suggest review runs it) → ADD KEY idx_item_key (item_key). discord_messages author_id and posted_at (DiscordInbox.php:431, :569, :54, :58; low, reader paused) when the reader resumes. Redundant: library_metrics.idx_day (prefix of uq_once_a_day), broadcasts.idx_status (prefix of idx_due): drop both. Dead weight: session_activity.idx_session (nothing selects by session_id) and the three analytics_visitors indexes (table unread); library_items.ft_search is unused only because the code does not use it (the search row): keep. Each ALTER is an online operation on these sizes; run bin/install.php with a migration.

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.48 Link to PERF.48Comment on PERF.48 Website: ordinary Library page views read every published item (publicStats, LanguageNames::inUse, makers, forUser without LIMIT, a 652 KB cache per page)

LibraryItems::publicStats (LibraryItems.php:278-295) pulls every published row with its author and version into PHP to count authors and categories: 42 ms per view of /library and /library/author. LanguageNames::inUse (LanguageNames.php:77-83) joins all items to their versions (a UNION with addon_listings) on every Library page to learn that two languages exist: 28 to 44 ms. LibraryItems::makers (:268) SELECT DISTINCT maker over all published rows per browse page (fast today, linear later). LibraryItems::forUser (:221) has no LIMIT: the system account's /library/mine loads 13,844 rows. LibraryMetrics::totals is cached, but the cache file is 652 KB of JSON decoded on every card page (that is the 367 ms "PHP" of /library/browse). Fix: publicStats as one GROUP BY or Cache::remember 600 s with the key dropped in LibraryItems::refresh; cache inUse and makers the same way (or compute inUse in the seed); forUser paged by 50 like LibrarySubmissions::forUser; metrics labels read the per-ref rows (idx_ref) for the 48 refs on the page instead of the whole totals file. At 1 M items each of these is seconds; with the fix they are constant.

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.49 Link to PERF.49Comment on PERF.49 Website: every signed-in page view runs the account-menu queries (5 to 6 statements; 12 Licenses::forOrg calls on /account)

navbar.inc:36 → Organizations::listedFor → forUser (Organizations.php:181, a correlated member COUNT), then per workspace Licenses::summary → forOrg (two statements), Licenses::seatFor and StoragePlans::inForce: 5 to 6 statements and about 1.5 ms on each of the 61 pages profiled, 12 forOrg calls on /account. With one workspace per account this is small; with teams it is three statements per workspace per page. Fix: compute listedFor once per request (static memo) and keep the menu's result in $_SESSION with a 60 s stamp, invalidated by Licenses::assign/unassign/grant and Organizations::addMember/removeMember (or the touched() hook the dashboard already uses).

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.54 Link to PERF.54Comment on PERF.54 Website: /library/kits takes 1.3 to 1.45 s and 496 queries; /library/addons 113 queries

FieldPacks::byId() iterates all(), which re-reads and re-checks 74 pack.json files (949 KB) per call, once per kit row (FieldPacks.php:116-137, library/section.php:162): about 5,500 json_decodes per request; coverage() asks AddonLibrary::byListingId per member (405 queries, FieldPacks.php:154) though AddonLibrary::all() is already cached. /library/addons: AddonLibrary::media() once per card (section.php:24, 93 queries). Fix: memoize FieldPacks::all() per request (a static cache like AddonLibrary::$cache), byId from that map, coverage() from AddonLibrary::all()[$mid]; one SELECT … WHERE listing_pk IN (…) for media (mediaCounts() already exists for counts). Expected: kits under 150 ms and about 20 queries. Note for bin/admin-perf.php: rewritten routes need the real file path (/library/section?section=kits).

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.63 Link to PERF.63Comment on PERF.63 Website: a distributed crawl of /roadmap-issues filter combinations is about half of all PHP page requests (485,944 in 8 days, 172 ms and 1.4 MB of HTML each)

Access logs: 485,944 requests to /roadmap-issues in 8 days (184,651 on Nov 6); in the last 5 hours 24,548 requests from 17,356 distinct IPs with browser user agents and fake referers, every permutation of ?app=&channel=&kind=&layer=&release=; 23,831 of the last 49,776 dynamic requests. Each costs 172 ms of PHP, 23 queries and 1,416 KB of HTML (112 KB gzipped): about 23 CPU-hours and 54 GB of egress in 8 days, invisible on /admin/analytics because Analytics::SKIP_PREFIXES has /roadmap-issues. The crawl ignores robots, so per-IP throttling cannot help. Change: render the item list once per release into Cache (60 s) and filter from it (172 to about 20 ms); rel="nofollow" on every filter link and a canonical URL per release; if the page stays reachable by link, filter client-side on the cached list. Removes about 45 % of PHP CPU and most HTML egress today; the page gets 8x cheaper for staff.

– Site and services Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.64 Link to PERF.64Comment on PERF.64 Website: a session file is written for every anonymous page view (312,607 files, 1.3 GB, 92,632 a day, GC 38 s)

sessions/ holds 312,607 files, 98.8 % under 120 bytes (a CSRF token or nothing), 92,632 written in 24 h and still 7,574 an hour after today's crawler change; the directory inode is 142 MB (a directory never shrinks); session-gc takes 38 s every 6 h (38,589 CSRF-only files removed per run). Every page view is open, flock and write of a file; the per-session lock also serializes one visitor's parallel requests. (FX.136 shortened the lifetime; this removes the cause.) Change: start the session lazily, only when a cc_session cookie arrives or something is written (csrf_token(), flash(), sign-in); a signed double-submit CSRF token needs no session for visitors; then recreate sessions/ once. About 90k fewer file writes a day and GC in a second.

– Site and services Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.65 Link to PERF.65Comment on PERF.65 Website: Vary: Referer on every response, assets included, so the browser caches each shared module per referring page and no shared cache can hold them

public_html/.htaccess:28 wraps the lab no-store rule in <If "%{REQUEST_URI} =~ … || %{HTTP_REFERER} =~ …">, and Apache adds Vary: Referer,Accept-Encoding to every response that passes it, cadcraft.js included. A browser's cache entry for a shared file is then keyed by the referring page: the same assets/js/*.js fetched under /use/bimcraft after /use/cadcraft, or /library, misses; a CDN or shared cache cannot store the assets. With 400 requests per app start this counts. Change: keep <If "%{REQUEST_URI} =~ m#^/assets/apps/[^/]+/a Discord member/#"> for a Discord member builds and send Cache-Control: no-store for lab pages from PHP (header.inc when is_a Discord member) instead of the Referer match.

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.69 Link to PERF.69Comment on PERF.69 Website: the developer API makes three writes per keyed call (last_used_at, throttle, request log), health polls included

DevKeys::inspect runs UPDATE a Discord member_api_keys SET last_used_at, Throttle::hit an INSERT and DevApi::log an INSERT on every call, 200s on /v1/health and 404s too; each autocommit is an fsync with innodb_flush_log_at_trx_commit 1. Load test: api host /v1/health 679 req/s, /v1/endpoints 763, /v1/status 364 at c=10, zero failures (the floors are fine: health 3.6 ms). At 1000x (an integrator polling once a second) the writes are the cost. Change: update last_used_at at most once a minute per key, skip the request log for health 200s, keep it for everything else; a Discord member_api_requests is pruned at 30 days already.

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.77 Link to PERF.77Comment on PERF.77 Code: PHP service layer: 1,457 prepare() with no shared helper, LibraryResult unused, 35 classes with their own ok()/fail(), components re-running one query

tools/perf/quality-php.php: the $st = db()->prepare(…); $st->execute([…]); $st->fetch(…) idiom appears 666 times across 204 classes, six classes define a private row() helper of their own, 626 hand-written ['ok' => …] results while LibraryResult exists and is used by zero classes. Pages where the controller and its components call the same service method with the same arguments more than once: /admin/library LibraryReports::countOpen() three times, /admin/messages Contact::countsByStatus() three times, /supercad SuperCadCheckout::seatsLeft() four times, the navbar Downloads::current() twice on every page. Change: db_row/db_rows/db_val/db_exec in core/functions/helpers.php and result_ok/result_fail (or adopt LibraryResult), added once (S) and adopted class by class as they are touched (M); pass page values down to components instead of re-querying. The SQL track's memo row covers the lookup side.

– Site and services Planned 3 – docs/0.9-sessions/perf-wave3/
Suggest an item

Anyone can send one; no account and no name needed. The team reads each idea, and the ones we take on appear on this page. Please leave out web addresses.