Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
de285b3
docs: add Chip-on-Board packaging design spec
mithro Jun 8, 2026
40667ec
docs: address spec review - read --cob live, drop snapshot field
mithro Jun 8, 2026
f30fc64
docs: model CoB re-check on the precheck-version-change pattern
mithro Jun 8, 2026
57443a9
docs: fix CoB re-check to cancel in-progress checks explicitly
mithro Jun 8, 2026
10e2bfb
docs: note stale create_check_drc_update docstring as impl follow-up
mithro Jun 8, 2026
c025392
docs: revise CoB spec for scheduled superseded cleanup (#261/PR #262)
mithro Jun 10, 2026
02abaaa
docs: fix CoB spec teardown sequence and tighten stale-FINISH rationale
mithro Jun 10, 2026
6a209df
docs: add CoB packaging implementation plan
mithro Jun 10, 2026
26658c8
docs: address plan review - fix red-phase tests and line refs
mithro Jun 10, 2026
316b978
feat: add Project.chip_on_board field (#259)
mithro Jun 10, 2026
98e9f87
feat: add COB_CHANGE manufacturability trigger reason (#259)
mithro Jun 10, 2026
7280b5e
docs: extend plan Task 7 to cover trigger-reason badge chains
mithro Jun 10, 2026
04806a2
feat: add create_check_cob_change model method (#259)
mithro Jun 10, 2026
ac9e265
feat: pass --cob to precheck when chip_on_board is set (#259)
mithro Jun 10, 2026
34243d9
feat: add CoB checkbox to project form (#259)
mithro Jun 10, 2026
804845a
refactor: inherit CoB help text from model field
mithro Jun 10, 2026
f23c5b4
feat: re-run manufacturability check when CoB toggled (#259)
mithro Jun 10, 2026
6ee5ab8
refactor: use named constant and type hints in CoB toggle tests
mithro Jun 10, 2026
264022c
feat: show CoB packaging badges (#259)
mithro Jun 10, 2026
7ca1322
test: tighten CoB badge assertions
mithro Jun 10, 2026
aefbe51
fix: lock source check row in create_check_cob_change
mithro Jun 18, 2026
ae7ec14
fix: don't 500 when CoB re-check creation fails post-save
mithro Jun 18, 2026
ebc828a
fix: label non-CoB packaging as "Bare Die" instead of "Standard"
mithro Jun 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
708 changes: 708 additions & 0 deletions docs/superpowers/plans/2026-06-10-chip-on-board-packaging.md

Large diffs are not rendered by default.

210 changes: 210 additions & 0 deletions docs/superpowers/specs/2026-06-08-chip-on-board-packaging-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# Chip-on-Board (CoB) Packaging Support — Design

- **Date:** 2026-06-08 (revised 2026-06-10 for
[#261](https://github.com/wafer-space/platform.wafer.space/issues/261) /
[PR #262](https://github.com/wafer-space/platform.wafer.space/pull/262))
- **Issue:** [#259](https://github.com/wafer-space/platform.wafer.space/issues/259)
- **Status:** Approved design, pending spec review
- **Assumes:** PR #262 (fixes #261) lands before implementation. It replaces
the never-scheduled `checks_cleanup()` / `_cancel_superseded_checks()` with a
scheduled `checks_cleanup_superseded` beat task (60s) and fixes the
`create_check_drc_update` docstring. This spec is written against that
post-#262 state. The design also works without #262 — it never *relies* on
the superseded-cleanup task — only the backstop notes below would not apply.

## Summary

Let users request **Chip-on-Board (CoB) packaging** for a project. When CoB is
requested, the manufacturability precheck runs with the precheck's CoB option so
the design is validated against the extra CoB compatibility checks. The result
flows through the existing manufacturability pipeline.

## Background: the precheck interface (verified)

The CoB rules live in the separate precheck image repo
`wafer-space/gf180mcu-precheck` (`precheck.py`), **not** in this repo. Verified
against that repo on 2026-06-08:

- `precheck.py` already accepts **`--cob`** — an argparse `store_true` flag:
`parser.add_argument("--cob", action="store_true", help="Use the CoB
(Chip-On-Board) packaging option (extra checks).")`
- It threads a boolean config var **`WS_COB`** through the librelane flow,
sitting alongside `WS_ID` (→ platform `--id`) and `WS_SLOT` (→ platform
`--slot`).
- CoB enables **extra checks within the same flow**, so CoB failures appear as
ordinary manufacturability errors — no new result format to parse.

> Note: issue #259 referred to a `--chip-on-board` flag; the real flag is
> `--cob`. This spec is authoritative. The issue should be updated to match.

The platform already builds the precheck command in
`wafer_space/projects/tasks_checks.py` (`do_starting`, ~line 1070) as an argument
list including `--slot <size>` and `--id <full_id>`. Adding `--cob` is exactly
parallel.

## Design decisions (agreed)

1. **Boolean, not a packaging enum.** The precheck models CoB as on/off
(`WS_COB: bool`). A multi-value `packaging` choice field would be premature
(YAGNI). Use a boolean `chip_on_board`.
2. **Editable with auto re-check.** `chip_on_board` is NOT a `CORE_FIELD`. A
user may toggle it; toggling invalidates the current manufacturability check
and re-runs it with/without `--cob`.
3. **CoB is orthogonal to slot/shuttle.** The precheck treats `WS_COB`
independently of `WS_SLOT`. No added slot-size or shuttle eligibility
constraints.

## Components

### 1. Data model (one migration)

- `Project.chip_on_board: BooleanField(default=False)` — editable; excluded from
`CORE_FIELDS`. Mirrors the precheck's `WS_COB`.

No new field on `ManufacturabilityCheck`. The precheck command reads
`chip_on_board` **live from `check.project`** (see §2), matching how
`--slot`/`--id` already read `check.project.slot_size`/`full_id`
(`tasks_checks.py:1057-1058`). Because toggling CoB cancels any in-flight check
and creates a fresh one (§3), at most one non-cancelled check is ever active for
a file, so a live read of `chip_on_board` reflects what that check was created
with — no snapshot is needed. (A per-check snapshot could be added later if
historical CoB labelling of finished checks becomes necessary; out of scope
here.)

### 2. Precheck command wiring

In `tasks_checks.py` `do_starting`, where the command list is built (the same
place `--slot`/`--id` are appended from `check.project.slot_size`/`full_id`),
append `"--cob"` iff `check.project.chip_on_board` is True. `store_true` → no
value.

### 3. Re-check on toggle (precheck-version-change pattern)

CoB toggling reuses the existing **precheck-version-change** mechanism, not the
file-replacement cancel path:

- Add a model method `ManufacturabilityCheck.create_check_cob_change()`,
parallel to the existing `create_check_drc_update()` (`models.py:2269`). It
validates that `self` is the latest check for its `project_file`; if
`self.is_cancellable` (the latest check is still in progress) it calls
`self.mark_cancelling(reason="Chip-on-Board option changed")`; then it creates
and returns a **new PENDING** `ManufacturabilityCheck` with
`trigger_reason=TriggerReason.COB_CHANGE` and `parent_check=self` (chaining via
`parent_check`/`root_check`, like DRC updates and retries). This is pure
model/ORM logic — no task import. (`mark_cancelling` is a state transition;
the scheduled `checks_cancelling` task (15s) then completes
CANCELLING→CANCELLED, and `checks_cleanup_orphaned_docker` (60s) removes the
container once the check is CANCELLED — the same machinery the
file-replacement path relies on. Unlike `create_check_drc_update` there are
no docker-digest/version guards — the trigger is the user toggling.)
- **Why the explicit cancel:** the scheduled DRC-update requeue only re-checks
*finished* checks (`checks_drc_update_requeue` skips in-progress ones), but a
CoB toggle can happen while a check is RUNNING, so
`create_check_cob_change()` cancels it directly. Post-#262 the scheduled
`checks_cleanup_superseded` task (60s) would *eventually* cancel a superseded
in-progress check, but we still cancel explicitly because:
1. **Immediacy/correctness** — relying on the 60s backstop leaves a window in
which the superseded check keeps running and could even FINISH
(ANALYZING→FINISHED is a legal transition, whereas CANCELLING permits only
→CANCELLED), recording a result computed with the *old* CoB setting that
§1's live read would then mislabel with the new value. Explicit cancel
closes that window at toggle time — a stale FINISH becomes impossible.
2. **Audit precision** — `mark_cancelling(reason="Chip-on-Board option
changed")` records why, instead of the generic superseded-cleanup reason.
3. **Determinism** — the method's behaviour is self-contained and testable
without depending on beat timing.

`checks_cleanup_superseded` remains a defence-in-depth backstop (it would
mop up if the explicit cancel were ever skipped), not the mechanism.
- The new PENDING check is dispatched by the scheduled `checks_pending` task
(15s); the cancelled in-progress check, if any, is marked CANCELLED by the
scheduled `checks_cancelling` task (15s), and its container is removed by
`checks_cleanup_orphaned_docker` (60s). All are existing, scheduled pollers.
- The toggle itself lives in the project edit view's form handling: persist the
changed `Project.chip_on_board`; if the active file has a latest check, call
`latest_check.create_check_cob_change()`. If the project has no check yet
(DRAFT), just persist the flag — the first check reads `chip_on_board` live.

Add `COB_CHANGE = "cob_change", "Chip-on-Board Option Changed"` to
`ManufacturabilityCheck.TriggerReason`.

Because pending-check creation is pure ORM (the queue processor does the
dispatching), there is no models-import-tasks layering concern and no separate
service is required.

### 4. UI

- A "Request Chip-on-Board (CoB) packaging" checkbox on the project **create**
and **edit** forms, with help text explaining it runs extra CoB compatibility
checks. On save, the view creates the re-check (§3) when the value changes.
- Project detail: a badge indicating CoB requested (yes/no), reusing existing
badge components. The manufacturability result already reflects the extra
checks.

## Data flow

1. User checks "Request CoB" on create/edit → project edit view.
2. View persists `Project.chip_on_board`; if the active file has a latest check,
calls `create_check_cob_change()` → a new PENDING `COB_CHANGE` check.
3. `create_check_cob_change()` has already marked any in-progress check as
CANCELLING; the scheduled `checks_cancelling` task marks it CANCELLED
(container removal follows via `checks_cleanup_orphaned_docker`) and
`checks_pending` dispatches the new PENDING check.
4. `do_starting` builds the precheck command with `--cob` read live from
`check.project.chip_on_board`.
5. The precheck runs the extra CoB checks; results flow through the existing
pipeline; the project detail page shows the CoB badge + manufacturability
result.

## Error handling

- Toggling on a DRAFT project (no check yet): persist only; the first check
reads `chip_on_board` live.
- `create_check_cob_change()` raises if called on a non-latest check (mirrors
`create_check_drc_update`'s latest-check guard); the view only calls it on the
active file's latest check.
- An in-progress latest check is cancelled explicitly inside
`create_check_cob_change()` via `mark_cancelling` (gated on `is_cancellable`,
which also avoids `InvalidStateTransitionError`); the scheduled
`checks_cancelling` task completes the transition to CANCELLED, and
`checks_cleanup_orphaned_docker` removes the container. The scheduled
`checks_cleanup_superseded` task (post-#262, 60s) is a defence-in-depth
backstop only — the explicit cancel is the primary mechanism (see §3 for
why).
- The precheck `ValueError`/failure paths are unchanged; CoB failures are
ordinary manufacturability errors.

## Testing (TDD)

- **Model:** `chip_on_board` defaults False; editable (not blocked by
`CORE_FIELDS` immutability). `create_check_cob_change()` creates a PENDING
check with `trigger_reason=COB_CHANGE` and `parent_check` set to the source
check; when the source check is in progress it is marked CANCELLING; when the
source check is already finished no cancel occurs; raises when called on a
non-latest check.
- **Toggle (view):** changing CoB on a project with a latest check creates
exactly one new pending `COB_CHANGE` check; toggling on a DRAFT only persists;
submitting the form with the value unchanged creates no new check.
- **Command builder:** `--cob` appended iff `check.project.chip_on_board` is
True; absent otherwise; placed alongside `--slot`/`--id`.
- **View/form:** the checkbox renders on create + edit; POSTing it sets the flag
and (when changed) creates the re-check; the project detail page shows the
CoB badge when the flag is set (and not when unset).

## Out of scope

- The CoB compatibility **rules** (already implemented in the precheck image).
- A CoB-specific result breakdown UI — CoB failures surface as normal
manufacturability errors; a dedicated breakdown can be a follow-up.
- Per-shuttle or per-slot CoB eligibility constraints.

## Follow-ups

- Update issue #259 to reference the real `--cob` flag.
- Before implementation: rebase this branch onto `main` once PR #262 merges —
#262 touches `tasks_checks.py` and the `create_check_drc_update` area of
`models.py`, both adjacent to this feature's edit sites. (#262 also fixes the
previously misleading `create_check_drc_update` docstring, so no docstring
follow-up remains here; `create_check_cob_change`'s own docstring should
state that it cancels in-progress checks *itself*, unlike
`create_check_drc_update` which defers to `checks_cleanup_superseded`.)
3 changes: 3 additions & 0 deletions wafer_space/projects/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ class Meta:
"name",
"description",
"is_public",
"chip_on_board",
"repository_url",
"license_type",
"other_license_spdx_id",
Expand All @@ -231,6 +232,7 @@ class Meta:
},
),
"is_public": forms.CheckboxInput(attrs={"class": "form-check-input"}),
"chip_on_board": forms.CheckboxInput(attrs={"class": "form-check-input"}),
"repository_url": forms.URLInput(
attrs={
"class": "form-control",
Expand All @@ -255,6 +257,7 @@ class Meta:
"description": "Optional details about your design",
"slot_size": "Select the die slot size for your design",
"is_public": "Make this design publicly visible on the platform",
# chip_on_board: help_text inherited from the model field
"repository_url": "URL to the project's source repository",
"other_license_spdx_id": (
"SPDX identifier (e.g., GPL-3.0-only, LGPL-2.1-or-later)"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.2.6 on 2026-06-10 14:49

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('projects', '0055_add_commit_info_to_precheck_revision'),
]

operations = [
migrations.AddField(
model_name='historicalproject',
name='chip_on_board',
field=models.BooleanField(default=False, help_text='Run extra Chip-on-Board (CoB) compatibility checks during the manufacturability precheck.', verbose_name='Request Chip-on-Board (CoB) packaging'),
),
migrations.AddField(
model_name='project',
name='chip_on_board',
field=models.BooleanField(default=False, help_text='Run extra Chip-on-Board (CoB) compatibility checks during the manufacturability precheck.', verbose_name='Request Chip-on-Board (CoB) packaging'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.6 on 2026-06-10 15:00

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('projects', '0056_historicalproject_chip_on_board_and_more'),
]

operations = [
migrations.AlterField(
model_name='manufacturabilitycheck',
name='trigger_reason',
field=models.CharField(choices=[('initial', 'Initial Check'), ('drc_update', 'DRC Rules Updated'), ('admin_rerun', 'Admin Requested Re-run'), ('retry', 'Retry After Error'), ('cob_change', 'Chip-on-Board Option Changed')], default='initial', help_text='Why this check was triggered', max_length=20),
),
]
56 changes: 56 additions & 0 deletions wafer_space/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator
from django.db import models
from django.db import transaction
from django.utils import timezone
from django.utils.formats import date_format
from simple_history.models import HistoricalRecords
Expand Down Expand Up @@ -160,6 +161,7 @@ class Status(models.TextChoices):
"name",
"description",
"is_public",
"chip_on_board",
"repository_url",
"license_type",
"other_license_spdx_id",
Expand Down Expand Up @@ -243,6 +245,16 @@ class Status(models.TextChoices):
help_text="Whether this design should be publicly visible on the platform",
)

# Chip-on-Board packaging (Issue #259)
chip_on_board = models.BooleanField(
default=False,
verbose_name="Request Chip-on-Board (CoB) packaging",
help_text=(
"Run extra Chip-on-Board (CoB) compatibility checks during the "
"manufacturability precheck."
),
)

# Repository URL (Issue #137)
repository_url = models.URLField(
blank=True,
Expand Down Expand Up @@ -1551,6 +1563,7 @@ class TriggerReason(models.TextChoices):
DRC_UPDATE = "drc_update", "DRC Rules Updated"
ADMIN_RERUN = "admin_rerun", "Admin Requested Re-run"
RETRY = "retry", "Retry After Error"
COB_CHANGE = "cob_change", "Chip-on-Board Option Changed"

class FinishedStatus(models.TextChoices):
"""Sub-status for FINISHED checks indicating manufacturability result."""
Expand Down Expand Up @@ -2301,6 +2314,49 @@ def create_check_drc_update(self) -> "ManufacturabilityCheck":
parent_check=self,
)

def create_check_cob_change(self) -> "ManufacturabilityCheck":
"""Create a new pending check after the project's CoB option changed.

Unlike ``create_check_drc_update`` — which leaves an in-progress check
to the scheduled superseded-check cleanup — this cancels an in-progress
check itself, so the superseded check can never FINISH with a result
computed from the old CoB setting.

Concurrency: the source row is locked with ``select_for_update`` and
re-read inside a transaction before any decision, so two simultaneous
toggles cannot both create a COB_CHANGE check, and a check that finished
in another worker between page load and submit is observed as FINISHED
(and left alone) rather than clobbered back to CANCELLING from a stale
in-memory status.

Returns:
The newly created ManufacturabilityCheck.

Raises:
ValueError: If this check is not the latest check for its file.
"""
with transaction.atomic():
locked_self = ManufacturabilityCheck.objects.select_for_update().get(
pk=self.pk
)

latest = locked_self.project_file.latest_manufacturability_check
if latest != locked_self:
msg = (
"Can only create CoB change check from the latest check for a file"
)
raise ValueError(msg)

if locked_self.is_cancellable:
locked_self.mark_cancelling(reason="Chip-on-Board option changed")

return ManufacturabilityCheck.objects.create(
project=locked_self.project,
project_file=locked_self.project_file,
trigger_reason=self.TriggerReason.COB_CHANGE,
parent_check=locked_self,
)

@property
def queue_wait_seconds(self) -> float | None:
"""Time spent waiting in queue before running (in seconds).
Expand Down
2 changes: 2 additions & 0 deletions wafer_space/projects/tasks_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,8 @@ def do_starting(check: ManufacturabilityCheck) -> dict[str, Any]:
"--id",
full_id,
]
if check.project.chip_on_board:
command.append("--cob")
command_str = " ".join(command)
logger.info("[do_starting] Container command: %s", command_str)

Expand Down
1 change: 1 addition & 0 deletions wafer_space/projects/tests/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
# Test counts
EXPECTED_IP_RANGE_COUNT = 8
EXPECTED_USER_PROJECTS = 2 # Number of projects created for test user
EXPECTED_CHECKS_AFTER_COB_TOGGLE = 2 # original check + COB_CHANGE re-check

# Worker tracking test values
TEST_WORKER_PID = 12345
Expand Down
Loading
Loading