🌱

Garden OS Scoring System Map

Planner v4.3 Β· Raw config β†’ traits β†’ score β†’ validity β†’ UI

Garden Planner Intelligence

The planner does not score raw cage toggles anymore. It scores meaning.

In v4.3, the cage and bed configuration still define geometry and structure, but the scoring engine is meant to consume a cleaner layer of truth stamped onto each cell. That means render, auto fill, warnings, inspect, and crop validity all read the same structural facts instead of recomputing trellis logic in multiple places.

Core idea

  • Raw config stores what the user turned on.
  • Derived traits translate that into structural meaning per cell.
  • Scoring and validity decide what fits, what fails, and what is merely suboptimal.
  • UI surfaces explain the consequences clearly.

Why this matters

  • Less drift between score, render, and warnings.
  • Cleaner future work for multi bed, calendar, and explainability.
  • Much easier tuning of trellis, protection, and access logic.

End to End Flow

The whole planner pipeline in one pass. This is the clean mental model for debugging, reviewing, or extending the scoring system.

Data Flow
Step 1 Inputs

Bed size, wall side (locked to back in cage mode), season, trellis toggle, goal, crop library, cage config, and current planted state.

β†’
Step 2 buildZoneMap(inputs)

Computes zone facts once, including trellis row, protected cells, support availability, critter safety, and access priority.

β†’
Step 3 applyZones(zones)

Stamps those derived traits directly onto each cell so the rest of the app reads the same structural truth.

Step 4 scoreCropInCell + adjScore

Evaluates crop fit using cell traits, crop metadata, neighbors, season, and access rather than ad hoc cage checks.

β†’
Step 5 cropValidity

Separates hard failures from advisories. A crop can be structurally impossible, merely suboptimal, or fully valid.

β†’
Step 6 Decision + UI

Auto fill, manual paint feedback, warnings, issues, inspect badges, and zone classes all consume the same result.

Design rule

Only the zone derivation layer should interpret raw cage structure. Everything downstream should read cell traits, not re-decide what the cage means.

Architecture Layers

Each layer has one job. That is what keeps the planner explainable instead of turning into a pile of overlapping conditionals.

Separation of Concerns
Layer 1

Raw Config Layer

Stores the literal user and app state: cage.enabled, rearTrellis, wireSides, bedW, bedH, wallSide (locked to "back" when cage is on), season, goal, and crop metadata.

PersistenceUI state
Layer 2

Zone Derivation Layer

Translates raw config into planner semantics. This is where structure becomes usable intelligence.

Computed onceGeometry aware
Layer 3

Cell State Layer

Applies derived traits to each cell so score, render, warnings, and inspect all see the same facts.

Shared truthLow drift
Layer 4

Rules Layer

Uses crop metadata plus cell traits to compute numeric fit, adjacency fit, and structural validity.

Fit logicFailure logic
Layer 5

Decision Layer

Auto fill and manual placement both route through the same scoring and validity logic, which means one planner brain instead of two.

AutofillManual paint
Layer 6

Output Layer

Bed rendering, warning cards, inspect rows, issue icons, and score summaries visualize what the planner already knows.

ExplainableConsistent UI
Layer Owns Should know raw cage state? Should recompute trellis row?
Input Storage, toggles, persistence, user settings Yes No
Zone derivation Structural meaning per coordinate Yes Yes, once
Cell state Trait stamping No No
Scoring Suitability and fit No No
Validity Hard fail vs advisory No No
UI Classes, badges, notes, inspect No No

Derived Cell Traits

This is the key abstraction. Cells should know their own structural reality so the planner can stop looking sideways at UI state every time it needs to reason.

Trait Model
Trait

cell.isTrellisRow

True when the cell is on the dedicated climbing row (always row 0, since wall side is locked to "back" in cage mode).

Trait

cell.isProtected

True when the cell sits in the wire enclosed interior rows, excluding the trellis row and the open front row. The front row is critter safe but not protected, because it is the primary access and harvest side.

Trait

cell.hasVerticalSupport

True when a crop can actually climb here, whether because it is the trellis row or because legacy support logic still allows it.

Trait

cell.isCritterSafe

True when wire protection exists for that structural zone, even if it is not necessarily the planner’s main protected crop zone.

Trait

cell.accessPriority

A normalized reachability score. Front row cells are easier to harvest often, so succession crops can get an access bonus.

Rule

Computed once

These values should be generated in one place, then reused everywhere else without reinterpretation.

Most important distinction

Protected and critter safe are not the same thing. Protected means interior rows shielded by wire on all sides β€” it excludes the trellis row (row 0) and the open front row (last row). Critter safe is broader: any cell where wire mesh exists, including the front row. Keeping those separate avoids muddy scoring later.

Trait Type Allowed values Contract
isTrellisRow boolean true | false True only for the effective trellis row after runtime wall-side override.
isProtected boolean true | false True only for interior protected rows. Never true on row 0 or the front row.
hasVerticalSupport boolean true | false Derived support capability. May reflect stored trellis state when cage mode is off.
isCritterSafe boolean true | false True where wire protection exists, even if the cell is not fully protected.
accessPriority number 0.0 to 1.0 Normalized reachability score. Higher means easier harvest access from the front side.

Example Cell State

Cell: row 1, col 2 (8Γ—4 bed, cage on)

isTrellisRow:       false   (row 0 is trellis)
isProtected:        true    (interior row, not open front row)
hasVerticalSupport: false
isCritterSafe:      true    (wire sides on)
accessPriority:     0.67

This single object should contain everything the scoring system needs. If scoring requires additional cage logic, the trait model is incomplete.

wallSide constraint

When cage mode is enabled, wallSide is locked to "back" and the select is disabled. The physical cage has its trellis panel bolted to the back wall β€” left, right, front, and none are structurally invalid in cage mode. This simplifies zone derivation: trellis row is always row 0, open front row is always the last row.

Scoring, Validity, and Recommendation Logic

The scoring engine now decides fit in two passes: one numeric and one structural. That gives the planner a better vocabulary than just β€œgood” or β€œbad.”

Decision Engine
Numeric fit

scoreCropInCell(ck, cell, inputs)

  • Base fit: light, season, crop type, goal alignment.
  • Structural fit: trellis row bonus, no support penalties, protected zone bonus, critter safety bonus.
  • Access fit: succession friendly crops gain value in easier reach cells.
  • Adjacency fit: companion and conflict logic comes from neighboring crops.
Validity pass

cropValidity(ck, cell, inputs)

  • Hard invalid: crop fundamentally fails here. Example: pole beans with no vertical support.
  • Advisory: crop can live here, but the placement wastes a better structural zone.
  • UI effect: hard invalid gets a red border and blocker semantics, advisory gets a warning tone.
Rule area Example input Outcome User facing effect
Trellis reward Cherry tomato in trellis row Structural bonus increases score Strong recommendation, green fit
No support penalty Pole beans in non support cell Score tanks and validity becomes hard fail β›” border, issue icon, warning text
Protected reward Lettuce in interior row Protected zone bonus plus critter safety bonus Higher score, better sanctuary placement
Space waste advisory Lettuce in trellis row Mild penalty, still plantable ⚠ advisory: wasting prime climbing space
Access bonus Radish near open front Small bonus for frequent harvest convenience Better sorting in auto fill and inspect
Factor Weight Description Typical Impact
Season fit +40 / -80 Crop matches active season window Hard filter if completely wrong season
Vertical support +35 / -120 Trellis crops must have support Hard invalid when missing
Protected zone +15 Greens and herbs in interior rows gain protection benefit Improves ranking for sanctuary crops
Critter safety +10 Wire protection reduces loss risk (all cage cells) Minor ranking improvement
Access priority +5 Frequent harvest crops prefer easy reach (front side) Minor optimization
Adjacency Β±10 Companion or conflict effects Small tuning adjustment

Auto fill should follow the same brain

The recommendation builder should prioritize the ideal zone pool first, then fall back to generally valid crops, and skip hard invalid placements entirely. That keeps automated layouts realistic instead of forcing a zone theory that starves the rest of the bed.

Where the Score Shows Up

The planner feels smart only when the same logic is visible everywhere the user looks. These are the main surfaces that should consume the shared trait and validity model.

User Surfaces
Render

Bed Grid

Trellis and protected classes should come straight from cell.isTrellisRow and cell.isProtected. Hard invalid cells get the visual stop sign treatment.

Inspect

Cell Detail Panel

Shows trait badges, support status, critter protection, access cues, and validity reason so the score is legible, not mysterious.

Warnings

Notes / Advisory Feed

Aggregates hard failures and softer placement advice without repeating the same message three different ways.

Issues

Per Cell Issue Icons

Hard invalid gets β›”. Advisory gets ⚠. This is the fast scan layer for spotting planner trouble across the whole bed.

Autofill

Recommendation Engine

Zones, score, and validity should all push the automatic plan toward climbers in back, protection crops in interior rows, and frequent harvest crops near the front side.

QA

Regression Check

If score, render, and warning surfaces disagree, the trait model is being bypassed somewhere. That is the first bug smell.

Planner Decision Funnel

The planner narrows from all possible crops down to one recommendation per cell. Each stage eliminates or reranks candidates.

Decision Funnel
Candidate pool Crop Library

All crops available for the season.

β†’
Filter Validity Gate

Remove crops that structurally cannot grow in the cell. Hard invalid crops are eliminated here.

β†’
Score Fit Evaluation

Compute numeric suitability based on traits and crop metadata.

β†’
Rank Recommendation

Sort remaining crops by best fit. Advisory flags surface but don't block.

Funnel invariant

Validity runs before scoring. A crop that is hard invalid should never appear in score results, even with a high base fit. The funnel only narrows β€” it never widens downstream.

Planner Debug Checklist

When something looks wrong, start here. Each question points to the layer most likely responsible.

Diagnostics
  • Score incorrect? Check cell traits first β€” open inspect and verify isTrellisRow, isProtected, hasVerticalSupport match expectations for that row.
  • Crop marked invalid? Inspect structural support flags. If hasVerticalSupport is false but the cell should have it, the zone derivation layer has a bug.
  • Zone rendering wrong? Verify buildZoneMap output. Protected should only appear on interior rows (not row 0, not last row).
  • Warnings inconsistent? UI may be bypassing trait model and reading raw cage state directly. Search for cage.enabled outside zone derivation.
  • Autofill behaving oddly? Compare scoring vs validity pass. If hard invalid crops appear in recommendations, the funnel filter is being skipped.
  • wallSide not locked? If cage is enabled but wallSide is not "back", syncCageUI or the toggle handler missed the constraint.
  • Front row marked protected? isProtected should exclude the last row (front side). If it doesn't, the isFrontRow check in buildZoneMap is broken.