fix: rename useSoonIngredients to soonIngredients for ESLint compliance - #257
Merged
Merged
Conversation
…rdening - study.js: point-in-time replay scoring arise vs double-progression, linear-progression and flat baselines on identical transitions; paired win/loss stats, experience-level segmentation, sample gates - prospective ledger: programme-stamped records, coverage report with deficits, substitution validation, deload decision audit; ledger now travels in backups and sync merges (resolved beats unresolved) - MoreView: progression-evidence panel (coverage gaps, replay comparison, deload audit), consent-gated and lazily computed - engine correctness: session-level plateau analysis, DST-safe streaks, upserted session completion, corrupt-store quarantine, guarded import comparators, telemetry identity pinning, honest planned-volume deltas - equipment: onboarding collects dumbbell pairs and machine/cable stack increments; swap sheet honours movement preferences and blocks A-to-A cycles; rest timer +/- adjust; set-progress live region - PWA: network-first navigations (stale HTML fix), reload-only-on-update, cross-tab store refresh; boot-crash fixes from in-flight refactor benchmark:study runs the comparative harness over the synthetic corpus.
…-eval infra, public dashboard, plugin API v1
Core: truncate head/tail overlap fix, ANSI CSI final-byte range, prioritize
budget fill + Windows path refs, filter cap marker + Set perf, atomic stats
writes, structural O(n^2) removals, NDJSON single-parse.
Parsers: shared util (CRLF-safe split, uniform results), preservation fixes
(eslint file headers, cargo/go panics, maven/gradle stack frames, terraform
boxes, docker legacy/BuildKit errors, k8s pod rows), ruff multi-letter rule
codes, mvn/mvnw detection, next.js cap + index-based ordering.
CLI/tooling: child-flag passthrough (-- stops at first non-flag after
subcommand), Windows .cmd spawn retry + spawn-error diagnostics, stdout/newline
normalization, JWT/PEM/Google-key redaction, --exit-code for stdin, subcommand
--help, invalid-level exit 2, safe formatCost, config warning on unreadable
JSON, init gitignores .rtk/, plugin auto-load gated behind RTK_ALLOW_PLUGINS=1,
atomic stats save never clobbers wrapped exit code.
Product v0.4.0: --auto adaptive strength, machine-readable removals metadata,
PARSER_BEHAVIOR_VERSION semver, stable require('rtk/api') surface, slimmed
prepublishOnly, benchmarks out of published tarball.
Benchmarks: 33-tool live-capture catalog w/ provenance (sha256/host/command)
+ contributor ingest; parser fuzz harness (10 categories); external-benchmark
harness (independent-task manifests, seeded randomization, blind-judge
scrubbing, model tiers, framework adapters, Wilson/TOST reporting); public
dashboard generator + sanitized failure-corpus export. Tests: 137 -> 223.
…n, privacy retention, evidence split
Measured-only metrics over any window (default 28d): weekly spend · waste value+weight · discarded items · takeaway frequency (+honest-null spend) · plan adherence · leftover reuse · pantry accuracy incl depletion-vs-expiry deltas · list completion · planning-time median · unused ingredients · substitution frequency · savings confidence graded by trips x calendar coverage (seasonality needs 8 weeks). beforeAfterOutcomes gates pre/post comparison on a noise floor in both windows.
…arness - loyalty/multibuy savings captured as data (informational, never arithmetic); coupons and refunds are payment events that adjust the net total - weighed goods keep kg units and defer per-kg rates to the printed line total; quantity-prefixed single figures are computed and flagged low-confidence; substitutions flagged; independent shops named honestly - per-line confidence + flags; whole-receipt receiptConfidence() grading - receipt-benchmark corpus: Tesco/Sainsbury's/Aldi/Lidl/Morrisons/Asda/ Waitrose/Co-op/independent seed cases covering every line type incl. unreadable rejection; per-field/per-feature/per-retailer metrics
…invariants - split exercises into src/lib/data/exercises/* (one module per muscle group), re-exported from data.js - add 38 hand-authored exercises with reciprocal substitution chains; close bodyweight dead ends (Back, Arms) - lint:content now enforces progression enums, per-record cues, >=3 exercises per muscle and >=1 pure-bodyweight option per muscle - declare explicit strategies in priors for new ids (endurance for time-based work, strength compounds, hypertrophy overrides where the name regex would misfire) - tests 196 -> 201: library size, coverage floors, graph reciprocity, enum/cue validation
- cycle opt-in grants the optional tool at setup and from Goals - reminder preset flows seed everything-mode (presets stay mode-scoped) - waste-planner penalises fractional whole-unit splits so whole-unit plans win ties - live-region audit: quiet paragraphs demoted; load-bearing announcements kept - typography: skip-link font-size back to relative units
rankPlans() scores candidate plans on six transparent dimensions — pantry coverage, fed-in waste score, expiry coverage, budget fit, time fit, equipment fit (+ pack rounding) — each reported or null; nulls drop out of the weighting instead of faking 100. Shares are normalised to a 0-100 scale before blending; strict-equipment mode benches infeasible plans with an explained reason. chooseOptimalPlan returns the winner with per-candidate breakdown and plain-language reasons. optimiseBasket() consolidates the list by store, proposes single-trip plans when one shop covers 80%+, suggests cheaper substitutes from the household's own receipt history, and rounds quantities up to whole packs.
buildPlan gains a multiObjective option (default false — zero behaviour change until opted in). When on, chooseCandidate feeds the waste model's per-candidate scores into rankPlans as the wasteScores seam, letting pantry coverage and expiry urgency blend with pack-waste instead of waste deciding alone. Legacy shape preserved: waste fields stay spread at top level; optimiserScore/optimiserReasons added alongside. chooseCandidate is now exported for direct testing.
The planner now blends pantry coverage and expiry urgency with the pack-waste model for live generation, instead of waste deciding alone. Time and equipment remain hard filters upstream; budget stays a per-serving chip until a price table feeds the engine's budgetFit.
- ingredient-prepass lib: alias-aware normalisation, duplicate grouping, same-dimension quantity merging; mismatched dimensions never add - offline-sync: immutable queue + ordered replay; multi-user merge with LWW, lexical tie-breaks, version-conflict resolution shapes - measure properties: cross-dimension conversion refused, scaling linear/multiplicative, unit-price linearity in pack size - recipe scaling properties: frozen proportions, multiplicativity, dimension survival, identity factor - large-pantry perf floors: 600-row pantry, 8 candidate plans, 150-item basket — generous CI-safe budgets - axe-core Playwright sweep across primary tabs (e2e suite)
# Conflicts: # apps/arise/README.md # apps/arise/docs/IMPROVEMENTS.md # apps/arise/index.html # apps/arise/package.json # apps/arise/public/sw.js # apps/arise/src/App.jsx # apps/arise/src/components/AppShell.jsx # apps/arise/src/components/MoreView.jsx # apps/arise/src/components/Onboarding.jsx # apps/arise/src/components/ProgressView.jsx # apps/arise/src/components/SessionRunner.jsx # apps/arise/src/components/TrainView.jsx # apps/arise/src/lib/analytics.js # apps/arise/src/lib/attributes.js # apps/arise/src/lib/backtesting.js # apps/arise/src/lib/data.js # apps/arise/src/lib/export.js # apps/arise/src/lib/priors.js # apps/arise/src/lib/programming.js # apps/arise/src/lib/progression.js # apps/arise/src/lib/schedule.js # apps/arise/src/lib/sessionGenerator.js # apps/arise/src/lib/store.js # apps/arise/src/lib/substitutions.js # apps/arise/src/lib/sync.js # apps/arise/src/lib/telemetry.js # apps/arise/tests/data.test.js # apps/arise/tests/export.test.js # apps/arise/tests/plates.test.js # apps/arise/tests/programme-generator.test.js # apps/arise/tests/programming.test.js # apps/arise/tests/progression.test.js # apps/arise/tests/store.test.js # apps/emotion-tracker/README.md # apps/emotion-tracker/src/app/api/reflect/route.ts # apps/emotion-tracker/src/app/layout.tsx # apps/emotion-tracker/src/app/page.tsx # apps/emotion-tracker/src/components/ApiKeyBar.tsx # apps/emotion-tracker/src/components/HistoryView.tsx # apps/emotion-tracker/src/components/LongitudinalPanel.tsx # apps/emotion-tracker/src/components/NewEntryForm.tsx # apps/emotion-tracker/src/components/PatternsView.tsx # apps/emotion-tracker/src/components/PrivacyBar.tsx # apps/emotion-tracker/src/components/ReflectionSession.tsx # apps/emotion-tracker/src/components/SettingsView.tsx # apps/emotion-tracker/src/components/SummaryView.tsx # apps/emotion-tracker/src/components/Toast.tsx # apps/emotion-tracker/src/lib/adversarial.ts # apps/emotion-tracker/src/lib/corrections.ts # apps/emotion-tracker/src/lib/crypto.ts # apps/emotion-tracker/src/lib/evidenceReport.ts # apps/emotion-tracker/src/lib/gemini.ts # apps/emotion-tracker/src/lib/humanReview.ts # apps/emotion-tracker/src/lib/importExport.ts # apps/emotion-tracker/src/lib/longitudinal.test.ts # apps/emotion-tracker/src/lib/longitudinal.ts # apps/emotion-tracker/src/lib/memory.ts # apps/emotion-tracker/src/lib/observationVsInference.ts # apps/emotion-tracker/src/lib/outcomeEvidence.ts # apps/emotion-tracker/src/lib/outcomeStudy.ts # apps/emotion-tracker/src/lib/patternEvidence.ts # apps/emotion-tracker/src/lib/privacy.ts # apps/emotion-tracker/src/lib/privacyAudit.ts # apps/emotion-tracker/src/lib/pulse.ts # apps/emotion-tracker/src/lib/rateLimit.test.ts # apps/emotion-tracker/src/lib/rateLimit.ts # apps/emotion-tracker/src/lib/search.ts # apps/emotion-tracker/src/lib/useEntries.ts # apps/emotion-tracker/src/lib/useLocalStorage.ts # apps/emotion-tracker/src/lib/validation.ts # apps/food-shopping-os/src/components/DemoWalkthrough.jsx # apps/food-shopping-os/src/components/FamilyPanel.jsx # apps/food-shopping-os/src/components/LogCapture.jsx # apps/food-shopping-os/src/components/Onboarding.jsx # apps/food-shopping-os/src/components/PlanGenerator.jsx # apps/food-shopping-os/src/components/PrivacyPanel.jsx # apps/food-shopping-os/src/components/ProfileTab.jsx # apps/food-shopping-os/src/components/SmartFeaturesPanel.jsx # apps/food-shopping-os/src/le-studio.css # apps/food-shopping-os/src/lib/health-actions.js # apps/food-shopping-os/src/lib/planner.js # apps/food-shopping-os/src/lib/receipt.js # apps/food-shopping-os/src/lib/waste-planner.js # apps/food-shopping-os/tests/advanced-flows.test.jsx # apps/food-shopping-os/tests/advanced.test.js # apps/food-shopping-os/tests/reminder-flows.test.jsx # apps/rapport/package-lock.json # apps/rapport/src/ai/cache.ts # apps/rapport/src/ai/provider.ts # apps/rapport/src/ai/tasks.ts # apps/rapport/src/app/api/ai/route.ts # apps/rapport/src/app/coach/page.tsx # apps/rapport/src/app/evidence/page.tsx # apps/rapport/src/app/page.tsx # apps/rapport/src/app/practise/[scenarioId]/page.tsx # apps/rapport/src/app/progress/page.tsx # apps/rapport/src/app/settings/page.tsx # apps/rapport/src/components/reflection-form.tsx # apps/rapport/src/components/simulation-runner.tsx # apps/rapport/src/components/training-plan.tsx # apps/rapport/src/data/pulse-history.ts # apps/rapport/src/data/repository.ts # apps/rapport/src/domain/agreement.ts # apps/rapport/src/domain/analytics.ts # apps/rapport/src/domain/confidence.ts # apps/rapport/src/domain/donation.ts # apps/rapport/src/domain/evaluation.ts # apps/rapport/src/domain/events.ts # apps/rapport/src/domain/floor.ts # apps/rapport/src/domain/gaming.ts # apps/rapport/src/domain/interruption.ts # apps/rapport/src/domain/mastery.ts # apps/rapport/src/domain/progress.ts # apps/rapport/src/domain/recommender-benchmark.ts # apps/rapport/src/domain/scheduling.ts # apps/rapport/src/domain/simulator.ts # apps/rapport/src/domain/transfer.ts # apps/rapport/src/domain/types.ts # apps/rapport/src/domain/weekly-review.ts # apps/rapport/src/state/store.tsx # apps/rapport/tests/agreement.test.ts # apps/rapport/tests/analytics.test.ts # apps/rapport/tests/benchmark-persistence.test.ts # apps/rapport/tests/donation.test.ts # apps/rapport/tests/gaming.test.ts # apps/rapport/tests/persistence.test.ts # apps/rapport/tests/transfer.test.ts # apps/rtk/README.md # apps/rtk/benchmark/corpus/manifest.json # apps/rtk/package.json # apps/rtk/src/ansi.js # apps/rtk/src/cli.js # apps/rtk/src/config.js # apps/rtk/src/filter.js # apps/rtk/src/init.js # apps/rtk/src/parsers/cargo.js # apps/rtk/src/parsers/docker.js # apps/rtk/src/parsers/eslint.js # apps/rtk/src/parsers/gotest.js # apps/rtk/src/parsers/gradle.js # apps/rtk/src/parsers/index.js # apps/rtk/src/parsers/k8s.js # apps/rtk/src/parsers/maven.js # apps/rtk/src/parsers/next.js # apps/rtk/src/parsers/pytest.js # apps/rtk/src/parsers/ruff.js # apps/rtk/src/parsers/terraform.js # apps/rtk/src/plugins.js # apps/rtk/src/prioritize.js # apps/rtk/src/redact.js # apps/rtk/src/run.js # apps/rtk/src/stats.js # apps/rtk/src/structural.js # apps/rtk/src/truncate.js
- /api/ai prefers OpenRouter :free models when OPENROUTER_API_KEY is set (catalog-cached picker, preferred families first); free models bypass the monthly AI budget — rate-limit only as an abuse guard — and fall back to the paid relay when no free slot answers - recipe favourite: solid red fill, white heart, ring-pulse on select alongside the existing pop animation
GLM 5.2 → Nemotron 3 Ultra → 3.5 Lightning → 3 Super → Gemma 4 31B → 26B A4B → Nano 30B A3B → Nano 12B VL → Nano 9B → LFM2.5 → North Mini Code → Inkling Small → Inkling → Laguna → Dots3. Embeddings, rerankers, TTS and safety classifiers are excluded from chat selection entirely. freeChat walks the ranking on rate limits/outages and refuses after a 401 (a bad key never fixes itself on the next model).
activeProvider() prefers NVIDIA_API_KEY (integrate.api.nvidia.com, no :free suffix filter) over OpenRouter. Ranking ladder now leads with Ultra-tier Nemotron, then GLM/Lightning/Super/size tiers — matching the NIM catalog. Same failover, non-chat exclusion and budget bypass.
createLedgerEntry freezes the prediction-time record (recommendation reasons/cost/time, purchase, execution, leftovers, waste, feedback). amendEntry produces new frozen revisions with a full audit trail — the prediction is never rewritten. ledgerInsights() answers the questions that matter from settled entries: did Forq save money (vs prediction and optional per-meal baseline), do pantry-first meals get cooked more often (cook-rate lift), which recommendation reasons waste less (wasted-portion % by reason), does batch cooking save time (mins/serving), are cook-time estimates accurate (mean abs error + bias language), what predicts a skipped meal (reason lifts vs overall skip rate). Every answer states its sample size; nulls mean no evidence, never zero. Live loop wired: PlanGenerator opens an entry per recommended meal with predicted cost/time and required-vs-owned item counts; markMealPlanOutcome mirrors cooked/substituted/skipped onto the matching open entry. openLedgerEntry/amendLedgerEntry exposed for the remaining capture points (purchases, leftover portions, feedback).
weeklyOutcomeSnapshot(state) computes one Monday-start week entirely on-device: food waste £/household (north star, with per-member view), planned meals cooked + adherence %, shopping-list→purchased accuracy, pantry accuracy (shared confidence classifier), weekly spend, unused purchased ingredients, leftover utilisation (ledger portions first, lifecycle events as fallback), cooking-time prediction error, recipe satisfaction from your own ratings. outcomeTrends() builds oldest-first weekly snapshots with week-on-week deltas. Pure functions of state — nothing uploads, nothing persists outside the household's own store.
…s Forq Four strategies face the same situation: random eligible (seeded PRNG), cheapest, pantry-first, and the live multi-objective engine. Every pick is measured on one vector: cost, pantry utilisation %, expiring-stock use, predicted time, repetition, constraint satisfaction. WHY THIS WON explanations in household language: 'uses Spinach expiring tomorrow · you already own 100% of the ingredients · normally takes you ~32 minutes · household rated it 4.3/5 · beats Fancy fish by £2.70 a serving' — real minutes from cooking history, ratings from the household's own scores. strategyOutcomes() closes the loop from ledger entries recorded with source 'experiment:<X>': cook rate, waste portions and actual spend per strategy. Engine matching upgrades included: stemmed keys (onions ≈ onion) and quantity-less ingredients treated as presence checks in coverage.
createForqDb opens forq-db v1 with the full target schema (households, profiles, pantryItems, shoppingItems, purchaseRecords, receipts, mealPlans, mealOutcomes, foodLogs, leftovers, wasteRecords, prices, customRecipes, preferences, measurements, experiments, syncMetadata). Promise wrappers handle request-vs-promise returns; completion is a durability gate with a 25 ms floor so test fakes can never hang callers. store.persist now dual-writes the Outcome Ledger into mealOutcomes via syncCollection — per-row diffing (new/changed/deleted) instead of serialising the whole app. localStorage keeps the preference shell during migration; remaining collections move over incrementally. tests/helpers/fake-idb.js provides a deterministic synchronous IDB.
… quick check pantryTruth(row) replaces have/don't-have with: estimated amount, confidence %, last-confirmed date and an expected range that widens as certainty falls. Confidence is an evidence ledger with visible drivers: time since confirmation decays it; uncounted consumption pulls it down; weighed cooking deductions and receipt evidence build it up; manual entry and approximate quantities are penalised honestly (clamped 15-97, never fake certainty). quickCheck(rows) turns the least-trusted rows into typed one-tap questions ranked by lowest confidence: amount unknown → 'roughly how much?', possibly gone → 'still have it?', decent estimate → 'still around 620 ml?'. Confident rows are never asked.
…t gating Corpus grows to 23 labelled cases across Tesco/Aldi/Lidl/Sainsbury's/ Morrisons/Asda/Waitrose/Co-op/independent/Ocado, now covering long receipts, partial photos, folds, poor lighting, loyalty+coupon combinations and substitutions — plus REAL_CORPUS for transcribed real receipts as collected. Reporting matches the headline table: store recognition, date extraction, product line detection, product matching, price extraction, basket total — with per-condition/per-retailer breakdowns, discount/coupon/refund accuracy and unreadable-receipt rejection rate. The honesty gate: evidenceTier() refuses to call anything evidence-grade without 300+ REAL receipts across 8+ retailers at 95%+ per field (basket ≥99.5%). A 100%-accurate synthetic corpus still reports 'useful-convenience' — exactly as it should.
…ates
scoreExternalPrice() turns external price provenance into HIGH/MEDIUM/LOW
with visible reasons: exact barcode match (+18), observed yesterday (+20),
3+ supporting observations (+14), same retailer (+8) — vs stale (−26),
name-only match (0), single observation, unverified scrape (−6).
Clamped 0-100; never fake certainty.
estimateBasket(items, priceResolver) returns { estimate, low, high,
coveragePct, level } where ranges widen with uncertainty: ±4% HIGH,
±8% MEDIUM, ±15% LOW. Level is bottlenecked by coverage — 60% coverage
is never HIGH.
estimateBaskets(items, { shops, observedByKey }) prices the list at every
known store: receipt prices first (strongest), community observations as
fallback (weaker). Sorted by highest coverage first. Returns sources
breakdown so the UI can say '2 receipts · 1 observed'.
PriceCompare renders a new 'Estimated baskets, with uncertainty' section
showing £X–£Y ranges per store with confidence pills and honest coverage
percentages, plus per-item HIGH/MEDIUM/LOW pills on observed rows.
…y tested youth-ai.js implements the pipeline: request → age/profile policy → context filter → AI call → response validation → safety check. Server-side: deriveYouthAiPolicy re-derives youth mode from age/profile (the client's claim is never trusted). filterContextForYouth strips weight, deficit, BMI, fasting, measurements and other restricted keys from the context before the model sees them. promptViolatesYouthPolicy refuses requests that try to bypass safety. youthAiGate composes the full pre-call gate; validateAiResponseForYouth sanitises the response on the way back (sentence-level removal of restricted advice with an audit trail). Wired into /api/ai: both OpenRouter free-tier and paid relay paths go through the same gate. Youth responses carry hadViolations + violations count so the UI can flag what was cleaned. Adversarial tests: 7 blocked prompt patterns (deficit, keto, fasting, BMI comparison, alcohol, earn-calories-by-exercise, cut-to-target kg), 4 clean prompts that must pass through, 5 restricted-response patterns, nested body-key stripping, adult pass-through, and end-to-end gate pipeline tests.
…planning tests closed-loop: hardcoded DAY='2026-08-03' became stale (>21d), triggering time-decay from definite→probable. Now uses dayStamp() for a relative date that never goes stale. planning: getByText found multiple 'Teriyaki Salmon Bowls' elements because the multi-objective optimiser can render the same recipe name in both the dialog and the page behind it. Switched to getAllByText with [0] indexing.
…e Ledger Every entry now carries a recommendationId that survives the entire lifecycle (plan -> shop -> cook -> leftovers -> outcome), separate from the entry id. predictionMetrics(entries) computes ten core metrics from settled entries: cost and cooking-time prediction errors (mean absolute + bias), plan adherence, unnecessary purchases, pantry utilisation, leftover utilisation, food waste (GBP and grams), recommendation acceptance, recommendation success, and repeat satisfaction. Each metric states its sample size; null means no evidence.
FEATURE_MATRIX in src/data/feature-matrix.js is the source of truth: every user-facing capability, its entry point, core/optional status, toolId, and primary action. 6 core + 8 optional tools documented. reachability.spec.js walks the real UI: fresh onboarding → visible tabs/buttons → optional tool enablement → primary action verification. This catches the AddToolsPanel bug class: feature exists, tests pass, UI works in isolation, but no real user can find it. Two invariants: (1) every optional tool must be discoverable in the tools UI, (2) every core feature must render content without any tools enabled. Runs with Playwright against a live server.
…g needs one more pass
…gh F_forq candidates
outcomeDashboard(state) composes weeklyOutcomeSnapshot, outcomeTrends, predictionMetrics and householdOutcomes into a single on-device dashboard: north star (food waste £/household/week with trend), four primary percentages (plan adherence, shopping-list accuracy, pantry utilisation, leftover reuse), secondary metrics (weekly spend, unused ingredients, cook-time MAE, prediction errors, recipe satisfaction), avoided waste from consumed-before-expiry items, predicted-vs-actual spend delta against budget. Pure function of state; never uploaded.
…rpus Corpus grows to 25 cases adding duplicate-product and strange- abbreviation coverage. All six headline metrics meet their targets: retailer recognition 100% (≥99), date extraction 100% (≥98), line detection 100% (≥97), price extraction 100% (≥98), basket total 100% (≥99), rejection accuracy 100%. Enforced in CI via test assertions. Duplicate handling: parser preserves both instances of a duplicated product rather than deduplicating them — the household bought two, the pantry should show two.
enforceYouthInvariants() runs inside hydrate(), which is the single chokepoint for ALL state entering the app: localStorage boot, backup restore, and cloud sync pull. It uses youthPolicy().on (not just age) so child household profiles trigger it too. Strips deficit goals, weekly calorie debt, custom target modes, and fasting data for under-18s regardless of how those values arrived. Adversarial tests cover backup restore smuggling, cloud sync pull, and child-profile activation — plus adult pass-through.
…ok rules The function is pure derivation logic, not a React hook. The use* prefix caused eslint-plugin-react-hooks to flag it as a rules-of-hooks violation, blocking CI at the lint stage. Also adds lint and typecheck scripts to package.json.
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The function is pure derivation logic, not a React hook. The use* prefix caused eslint-plugin-react-hooks to flag it as a rules-of-hooks violation, blocking CI at the lint stage. Also adds lint/typecheck scripts.