diff --git a/ROADMAP.md b/ROADMAP.md index 8e058e43f..f4c99d61f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~30 points of "ready for real work" and ~350–620 h away -> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-22 · **Change:** minor (Simplified Chinese Han text in compositions reaches the installed macOS Chinese faces instead of the Japanese face, with regression tests, #453; previous: minor, open-issue count, crash list and localization share re-audited against `upstream/main` 470ea7a3) · **Target:** Adobe After Effects 2026 (26.5.0.89) +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (open-issue count, crash list and localization share re-audited against `upstream/main` 470ea7a3) · **Target:** Adobe After Effects 2026 (26.5.0.89) EffectCraft aims to do what After Effects does, with the same panels, menus and behaviour, written from scratch in Rust. This page is the one-page summary; the evidence is in @@ -117,11 +117,6 @@ Come tell us what matters most to you on [Discord](https://discord.gg/artcraft). ## Progress log -- 2026-10-22: Text: Chinese Han text in compositions now draws with the installed Chinese face - instead of the Japanese one. macOS does not install the on-demand PingFang SC, so the Simplified - Han list names Hiragino Sans GB, Heiti SC, Songti SC and STSong (Heiti TC and Songti TC for - Traditional) before the fallback search can reach a Japanese face (#453). Japanese kana and kanji - are unchanged; a text-layer render test and a macOS fallback test cover both languages. - 2026-10-16: Convert To Bezier Path turns a shape layer's Rectangle, Ellipse or Polystar path into an editable Path 1 with the same geometry, as After Effects does; it used to apply to mask RotoBezier only, so a parametric shape path could not be converted at all @@ -160,7 +155,6 @@ Come tell us what matters most to you on [Discord](https://discord.gg/artcraft). | Date | Change | Summary | |---|---|---| -| 2026-10-22 | minor | Chinese Han text in compositions uses the installed macOS Chinese faces instead of the Japanese face; CJK text-layer and macOS fallback regression tests (#453) | | 2026-10-12 | minor | Open-issue count (134 to 216), crash list and localization share (12% to 18%) updated to match docs/gaps.md | | 2026-10-12 | patch | Roto Brush span validation after trimming; command regressions for both layer edges | | 2026-10-10 | minor | Hours to ≈ 95% for each audience | diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 726c39d39..079274499 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -237,27 +237,6 @@ fn text_layer_renders_glyphs() { assert!(covered > 300, "{covered}"); } -/// #453: a composition's Japanese (and Chinese) text layer draws its glyphs, not missing-glyph -/// boxes. Skipped where no CJK face is installed. -#[test] -fn text_layer_renders_cjk_glyphs() { - let cjk = effectcraft_text::fonts::resolve("Inter", "Regular").face; - if !effectcraft_text::fonts::face(effectcraft_text::fonts::fallback_for('日', cjk)).has_char('日') { - eprintln!("no Japanese face installed; skipping"); - return; - } - for text in ["日本語", "中文"] { - let (mut p, cid, comp) = setup(); - let mut l = build::layer(&mut p, &comp, "Text", LayerSource::Text, (200, 100), None); - l.props.prop_mut("text/sourceText").unwrap().value = Value::Text(Box::new(TextDoc { text: text.into(), size: 46.0, ..Default::default() })); - l.props.prop_mut("transform/position").unwrap().value = Value::Vec3([100.0, 70.0, 0.0]); - p.comp_mut(cid).unwrap().layers.push(l); - let img = render_frame(&p, cid, Tick::ZERO, 1.0); - let covered = img.data.iter().filter(|px| px[3] > 0.5).count(); - assert!(covered > 200, "{text}: {covered} covered pixels"); - } -} - #[test] fn precomp_and_3d_render() { let (mut p, cid, comp) = setup(); diff --git a/crates/text/src/fonts.rs b/crates/text/src/fonts.rs index b9c4a9084..a31a17124 100644 --- a/crates/text/src/fonts.rs +++ b/crates/text/src/fonts.rs @@ -767,12 +767,6 @@ const GEORGIAN_FAMILIES: &[&str] = &["Sylfaen", ".SF Georgian", "SF Georgian", " /// The CJK locale the host asked fallbacks to prefer, when it has one (see [`set_cjk_locale`]). static REQUESTED_CJK_LOCALE: RwLock> = RwLock::new(None); -/// Serialises the tests that change the process-global CJK locale with the tests that read it -/// through [`fallback_for`] (the layout tests), which would otherwise cache the other order's -/// families. -#[cfg(test)] -pub(crate) static TEST_CJK_LOCALE: std::sync::Mutex<()> = std::sync::Mutex::new(()); - /// Ask CJK font fallbacks to prefer `tag`'s language for Han ideographs instead of the locale /// environment, so a Chinese interface is drawn with a Chinese face on a machine whose locale is /// Japanese (and the other way round). The interface sets this from its language setting; an empty diff --git a/crates/text/src/fonts/tests.rs b/crates/text/src/fonts/tests.rs index 079a001e0..e94cf7e63 100644 --- a/crates/text/src/fonts/tests.rs +++ b/crates/text/src/fonts/tests.rs @@ -1,5 +1,14 @@ use super::*; +/// Run `test` again in a child process with `variable` set. The fixture faces below go into the +/// process-global font database and its fallback caches, so only a process that runs one such test +/// on its own can use them. +#[cfg(not(target_arch = "wasm32"))] +fn run_in_child(variable: &str, test: &str) { + let output = std::process::Command::new(std::env::current_exe().unwrap()).args(["--exact", test, "--nocapture"]).env(variable, "1").output().unwrap(); + assert!(output.status.success(), "{}\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)); +} + /// Concurrent language requests must neither choose another context's font nor reuse its /// cached family order. Synthetic fonts in a child process keep this independent of the host /// font installation and of other tests that exercise the process-global database/locale. @@ -8,12 +17,7 @@ use super::*; fn cjk_requests_keep_their_language_and_cache_entries() { const CHILD: &str = "EFFECTCRAFT_TEST_ISOLATED_CJK"; if std::env::var_os(CHILD).is_none() { - let output = std::process::Command::new(std::env::current_exe().unwrap()) - .args(["--exact", "fonts::tests::cjk_requests_keep_their_language_and_cache_entries", "--nocapture"]) - .env(CHILD, "1") - .output() - .unwrap(); - assert!(output.status.success(), "{}\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)); + run_in_child(CHILD, "fonts::tests::cjk_requests_keep_their_language_and_cache_entries"); return; } let families = [("ja", "Yu Gothic"), ("zh-hans", "Microsoft YaHei"), ("zh-hant", "Microsoft JhengHei"), ("ko", "Malgun Gothic")]; @@ -71,6 +75,52 @@ fn cjk_requests_keep_their_language_and_cache_entries() { } } +/// #453: with a Chinese interface, a Han ideograph that both a Chinese and a Japanese face cover +/// must reach the installed Simplified Chinese family first, or Chinese text in a composition is +/// drawn with Japanese glyph forms. Fixture faces named after the macOS families keep this +/// independent of the host's fonts and of rendering. Before the fix the Simplified list named none +/// of the macOS faces a stock install has, so the walk below reached the Japanese face - the +/// regression this guards. +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn zh_hans_han_fallback_puts_the_chinese_family_first() { + const CHILD: &str = "EFFECTCRAFT_TEST_ZH_HANS_ORDER"; + if std::env::var_os(CHILD).is_none() { + run_in_child(CHILD, "fonts::tests::zh_hans_han_fallback_puts_the_chinese_family_first"); + return; + } + let mut ids = Vec::new(); + { + let mut d = db().write().unwrap(); + d.scanned = true; // This test process uses only bundled and synthetic fonts. + // The Japanese face is added first, so scan order cannot be what picks the Chinese one. + for family in ["Hiragino Sans", "Hiragino Sans GB"] { + let mut data = test_font(family); + let table = data[12..12 + 8 * 16].as_chunks::<16>().0.iter().find(|t| &t[..4] == b"cmap").unwrap(); + let cmap = u32::from_be_bytes(table[8..12].try_into().unwrap()) as usize + 12; + // Map 页 to the fixture's triangle glyph instead of its private-use range. + for (offset, value) in [(16, '页' as u16), (24, '页' as u16), (30, 2u16.wrapping_sub('页' as u16))] { + data[cmap + offset..cmap + offset + 2].copy_from_slice(&value.to_be_bytes()); + } + let data = Arc::new(data); + let names = read_faces_bytes(&data).into_iter().next().unwrap(); + ids.push(push(&mut d, info_from(names, FaceData::Shared(data), "test"))); + } + } + let &[japanese, chinese] = ids.as_slice() else { panic!("two fixture faces expected: {ids:?}") }; + let base = resolve("Inter", "Regular").face; + assert!(face(japanese).has_char('页') && face(chinese).has_char('页')); + // The fallback list itself, before any coverage check: the Chinese family comes first for + // zh-hans and second for ja, whether or not either face can draw the probe. + let zh_hans: Vec = family_faces(FallbackScript::Han, &face(base), CjkLocale::Simplified).to_vec(); + assert_eq!(zh_hans, [chinese, japanese], "zh-hans tries the Chinese family first"); + let ja: Vec = family_faces(FallbackScript::Han, &face(base), CjkLocale::Japanese).to_vec(); + assert_eq!(ja, [japanese, chinese], "ja keeps the Japanese family first"); + // ...and the face a layout actually gets for the shared ideograph. + assert_eq!(face(fallback_for_locale('页', base, "zh-hans")).info.family, "Hiragino Sans GB"); + assert_eq!(face(fallback_for_locale('页', base, "ja")).info.family, "Hiragino Sans"); +} + #[test] fn extra_font_dirs_come_from_a_path_list() { assert!(extra_dirs(None).is_empty()); diff --git a/docs/gaps.md b/docs/gaps.md index b0a6b6b1f..beb9c2880 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where EffectCraft falls short -> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-22 · **Change:** minor (gap 11: the Chinese Han fallback reaches the installed macOS Chinese faces, #453; previous: minor, path-editing layer bounds, #1220; issue, PR and catalog references gap 21: percentage guides and effect colour labels in; earlier change: re-audited against `upstream/main` 470ea7a3; ranked gap list from the 10 October re-measure otherwise unchanged) · **Target:** Adobe After Effects 2026 (26.5.0.89) +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-13 · **Change:** minor (path-editing layer bounds, #1220; issue, PR and catalog references gap 21: percentage guides and effect colour labels in; earlier change: re-audited against `upstream/main` 470ea7a3; ranked gap list from the 10 October re-measure otherwise unchanged) · **Target:** Adobe After Effects 2026 (26.5.0.89) An honest assessment of how far EffectCraft is from being a real replacement for After Effects, and the work that closes the gap. This document is meant for contributors and agents choosing @@ -335,7 +335,6 @@ Issues #41–#47 (all from the Linux AppImage 0.1.1, 5 October): panning the vie | Date | Change | Summary | |---|---|---| -| 2026-10-22 | minor | Gap 11: the Chinese Han fallback reaches the installed macOS Chinese faces, so Chinese text in compositions no longer draws Japanese glyph forms (#453) | | 2026-10-16 | minor | Gap 8: #932 closed (Convert To Bezier Path converts a shape layer's parametric path, not only mask RotoBezier) | | 2026-10-13 | patch | G2: layer bounds no longer compete with editable shape/mask path points (#1220) | | 2026-10-12 | minor | Audited against `upstream/main` 470ea7a3: open-issue count and crash list, closed issues removed from rows 1, 3, 5, 6, 8–12, 15–17, 22–24, G8 and G9 (#759 `#include`), localization catalogs recounted (≈ 12% to ≈ 18%); rows 7, 14 and 21 left to open PRs #1102, #792 and #1098 | diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 651e311bb..d642af43c 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,6 +1,6 @@ # Localization parity -> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-23 · **Change:** minor (Georgian: menus, panels and effect names; missing strings show in English; `missing_translations_report` lists them; previous: minor, the Chinese Han fallback reaches the installed Chinese faces, so Chinese text in compositions draws Chinese glyph forms, #453; previous: minor, catalogs recounted at `upstream/main` 470ea7a3: menus in 10 languages, panels in 6, Hangul probe (#599)) · **Target:** Adobe After Effects 2026 +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-23 · **Change:** minor (Georgian: menus, panels and effect names; missing strings show in English; `missing_translations_report` lists them) · **Target:** Adobe After Effects 2026 How much of EffectCraft's interface is translated, language by language, compared with After Effects. [gaps.md](gaps.md) gap 16 and workstream G9 hold the work; @@ -71,7 +71,7 @@ the true percentages are slightly lower. | Language | Code | UI strings translated | Dialogs / tooltips / help | Script support | Native review | Status | To `full` | |---|---|---|---|---|---|---|---| | English | `en` | ≈ 3,900 (100%, source) | All; docs in English | — | yes | full | — | -| Simplified Chinese | `zh-hans` | 1,130 (≈ 29%): menus 624 rows, panels 506 | Panels and dialogs partly; no help, effect names translated (306) and their parameter names not; Home screen literals unwrapped (#574, closed) | System CJK fallback; Han follows the UI language and now reaches the installed Chinese faces (#453); IME in text layers | no | partial | 6–9 h + review | +| Simplified Chinese | `zh-hans` | 1,130 (≈ 29%): menus 624 rows, panels 506 | Panels and dialogs partly; no help, effect names translated (306) and their parameter names not; Home screen literals unwrapped (#574, closed) | System CJK fallback; Han follows the UI language; CJK text in comps renders wrongly (#453); IME in text layers | no | partial | 6–9 h + review | | Spanish | `es` | 624 (≈ 16%) | Menus only (panel catalog in open PR #996) | Latin, fine | no | menus only | 7–10 h + review | | Hindi | `hi` | 0 | — | UI: Devanagari fallback font but no shaping; text layers shape Devanagari | no | none (After Effects: none) | 9–13 h + shaping in the UI | | Arabic | `ar` | 0 | — | UI: Arabic fallback font but no shaping and no RTL layout; text layers: bidi + shaping | no | none (After Effects: none) | 9–13 h + 15–25 h RTL UI | @@ -111,7 +111,6 @@ can give; only Georgian, contributed by a native speaker (#1843), has had it. | Date | Change | Summary | |---|---|---| -| 2026-10-22 | minor | Chinese text in compositions follows the fixed Han fallback: installed Chinese faces are tried before the Japanese face (#453) | | 2026-10-12 | minor | Recounted: Russian, French, German and Czech menus (#873, #987), Brazilian Portuguese and Czech panels (#830, #987), Hangul probe (#599); overall ≈ 12% to ≈ 18%; Korean and Italian still absent | | 2026-10-14 | minor | Georgian (`ka`): menus 628, panels 886 (191 Georgian-only, in `GEORGIAN_PANELS`), effect names and categories in `effects.rs` | | 2026-10-10 | major | First localization parity document: catalog rows counted for all six languages, the twelve-language table, After Effects' 11 shipped languages from its bundle | diff --git a/docs/roadmap.md b/docs/roadmap.md index e2692fd8b..73fdfd235 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap detail -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (the Chinese Han fallback reaches the installed Chinese faces, #453; previous: major, new file: milestones moved from ROADMAP.md, current focus and next steps with Opus 5.5 estimates) · **Target:** Adobe After Effects 2026 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (new file: milestones moved from ROADMAP.md, current focus and next steps with Opus 5.5 estimates) · **Target:** Adobe After Effects 2026 Forward-looking detail behind the [ROADMAP](../ROADMAP.md). The work list itself, one gap at a time, is [gaps.md](gaps.md); the numbers are in [target-app-parity.md](target-app-parity.md). @@ -31,7 +31,7 @@ none blocks the workflow. |---|---|---|---| | 1. Set up: new comp, import footage, stills, PSD/AI and audio, arrange and trim layers | yes | `file.import` and image-sequence tests, `ui_drag_drop`, `ui_project_*`; rough: a still imports as a sequence (#431), an MP3 layer can go silent (#324), multi-file drops (#552) | 2–4 | | 2. Animate: keyframes, easing in the Graph Editor, motion paths, parenting | yes | 168 live samples match After Effects 26.3 ([fidelity/](fidelity/README.md)); `ui_keyframes`; rough: Graph Editor jitter and clipped buttons (#456, #457), negative rotation drag (#569) | 3–6 | -| 3. Motion graphics: text layers with animators, shape layers with operators | yes | `ui_text_edit`, `ui_timeline_shapes`, text and shape engine tests; rough: paragraph alignment inverted (#316), Chinese text in comps follows the Han fallback (#453 fixed) | 3–6 | +| 3. Motion graphics: text layers with animators, shape layers with operators | yes | `ui_text_edit`, `ui_timeline_shapes`, text and shape engine tests; rough: paragraph alignment inverted (#316), CJK text in comps (#453) | 3–6 | | 4. Composite: masks, track mattes, blend modes, keying and effects | yes | Compositor and mask tests, 306 effects with CPU/GPU agreement; rough: mask bugs (#510, #513, #527), 32-bpc blend differences (#494), effect bugs (#580 crash, #514) | 6–12 | | 5. Preview with audio (RAM preview, cache) | yes | `ui_ram_preview`, `ui_live_preview`; rough: stutter on cached frames (#595, #304), late audio (#554, #504) | 10–20 | | 6. Save, reopen and deliver: `.ecproj`, auto-save, Render Queue to H.264 / ProRes / sequences | yes | Versioned-schema reopen tests, crash recovery, render-queue and encoder tests; rough: the macOS default output folder is `/`, so a render fails until an output path is chosen (#446), ProRes 4444 colour tag (#340), projects don't relink when moved | 1–3 | @@ -83,6 +83,5 @@ To beta (≈ 75% ready and `.aep` opening): **≈ 350–620 h**. To full parity: | Date | Change | Summary | |---|---|---| -| 2026-10-22 | minor | The Chinese Han fallback reaches the installed Chinese faces (#453) | | 2026-10-10 | minor | Alpha gate: six core workflows checked; all pass, stage stays alpha | | 2026-10-10 | major | New file: milestones from ROADMAP.md, current focus, next steps with estimates | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 24030f9fd..ff7cc3ef9 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with After Effects -> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-22 · **Change:** minor (the Chinese Han fallback reaches the installed macOS Chinese faces, so Chinese text in compositions no longer draws Japanese glyph forms, #453; previous: minor, issue, catalog and export-format references re-audited against `upstream/main` 470ea7a3; the 10 October re-measure is otherwise unchanged) · **Target:** Adobe After Effects 2026 (26.5.0.89) +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (issue, catalog and export-format references re-audited against `upstream/main` 470ea7a3; the 10 October re-measure is otherwise unchanged) · **Target:** Adobe After Effects 2026 (26.5.0.89) The authoritative assessment of how close EffectCraft is to After Effects, and how much work is left. [gaps.md](gaps.md) lists every known shortfall one at a time; the @@ -119,7 +119,7 @@ compositing and effects dominate; audio is light). Hours are to full parity for | Animation: keyframes, graph editor, motion paths, puppet | 15% | 97% | 55% | 35–60 | Only scalar ease is measured against After Effects (168 samples); puppet lag (#586); #456, #457, #355, #553 and #318 are closed; Proportional Scrubbing (26.2) | | Expressions and scripting | 7% | 93% | 65% | 20–40 | `.jsxbin`, Startup/Shutdown folders, script shortcuts (`#include` landed, #759); external editor (#364); see [scripting-parity.md](scripting-parity.md) | | Shapes, masks and roto | 9% | 93% | 45% | 30–50 | Open mask and shape issues (#1004, #999, #1042, #932); #510, #513, #527, #509 and #303 are closed; paste SVG/AI as shape layers (26.3; SVG markup pastes since #738) | -| Text and typography | 7% | 94% | 50% | 20–35 | Chinese text in comps now reaches the installed Chinese faces in the Han fallback (#453); paragraph alignment inverted (#316); soft text at Fit (#417, mostly fixed by #444); toolbar font options (#550); #483 and #583 are closed; variable-font filter landed (#741) | +| Text and typography | 7% | 94% | 50% | 20–35 | CJK text in comps renders wrongly (#453); paragraph alignment inverted (#316); soft text at Fit (#417, mostly fixed by #444); toolbar font options (#550); #483 and #583 are closed; variable-font filter landed (#741) | | Effects and animation presets | 14% | 85% | 40% | 70–130 | All 306 effects exist, but rendered output is unmeasured against After Effects; 54 built-in presets (text, Transitions, Backgrounds) against After Effects' 621 ([effects-parity.md](effects-parity.md)); open effect bugs (#580 crash, #463, #369, #473, #474; #514 and #526 are closed); effect colour labels (26.5); see [effects-parity.md](effects-parity.md) | | 3D (Classic and Advanced) | 6% | 82% | 35% | 35–65 | No FBX/USD/STL models, no Substance materials, mesh displacement unverified; no 3D transform gizmos (#392, #347); reflections (#261); camera aperture (#260); #489 and #315 are closed | | Tracking and AI-assisted tools | 5% | 80% | 35% | 35–70 | Object Matte (26.2) has the effect, the viewer tool (click / box), background tracking, Freeze and the 26.5 disk cache, not yet compared with After Effects; Roto Brush (MobileSAM) and face tracking (MediaPipe) unmeasured against After Effects; camera-tracker view (#505) | @@ -193,7 +193,7 @@ generative/cloud AI (Object Matte, Firefly), Adobe services and team features, s | Keyframes, Graph Editor, easing, motion paths | 20% | 75% | 168 live samples match After Effects; #456/#457, #553 and #569 were counted here and are now closed | | Expressions: `wiggle`, `loopOut`, linking, sliders | 6% | 80% | `crates/expr` tests; no live expression oracle yet | | Shape layers and masks | 12% | 65% | #510, #513, #527 mask bugs and #303 Mesh Warp were counted here and are now closed; open: #1004, #999, #1042 | -| Text and text animators | 12% | 70% | #316 alignment inverted; #453 CJK fallback fixed; #583 font search (closed) | +| Text and text animators | 12% | 70% | #316 alignment inverted, #453 CJK (open); #583 font search (closed) | | Everyday effects: blurs, glow, fill/tint, curves/levels, keying, drop shadow, displacement | 10% | 70% | All present with GPU paths; unmeasured against After Effects; #514 Apply Color LUT (closed) | | Preview (RAM preview with audio) | 8% | 55% | #595, #304 stutter; #504 late audio (#554 closed) | | Render to H.264 / ProRes / PNG sequences | 6% | 75% | Encoder tests; #340 ProRes 4444 tag (open); #446 default macOS output folder (closed) | @@ -1168,7 +1168,6 @@ top of this page and in items 16–18 above. | Date | Change | Summary | |---|---|---| -| 2026-10-22 | minor | Chinese Han text in compositions uses the installed macOS Chinese faces rather than the Japanese face (#453) | | 2026-10-12 | minor | Audited against `upstream/main` 470ea7a3: open crash list, closed-issue references in the area tables, export formats 12 to 13, localization ≈ 12% to ≈ 18%, `#include`, built-in preset count, 12 October repository counts | | 2026-10-10 | minor | Readiness-by-audience table: hours to ≈ 95% for each of the three numbers; the full number stays an additive weighted sum (unchanged) | | 2026-10-10 | minor | Added mainstream practitioner (≈ 47%) and essentials user (≈ 52%) numbers with weights, discounts and GitHub user evidence; wrote down how the full ready number is built (unchanged at ≈ 45%) |