Metadata-Version: 2.4
Name: refdes
Version: 0.4.0
Summary: Reference documentation for hardware design decisions: typed items, units-aware math, and traceability.
Author: Squishiba
License-Expression: MIT
Project-URL: Homepage, https://github.com/Squishiba/refdes
Project-URL: Documentation, https://github.com/Squishiba/refdes/tree/main/docs
Project-URL: Source, https://github.com/Squishiba/refdes
Project-URL: Issues, https://github.com/Squishiba/refdes/issues
Keywords: hardware,documentation,requirements,traceability,electronics,pcb,units,engineering
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Manufacturing
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Documentation
Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
Classifier: Topic :: Software Development :: Documentation
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pyyaml>=6.0
Requires-Dist: jinja2>=3.1
Requires-Dist: pint>=0.23
Requires-Dist: markdown-it-py>=3.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"
Dynamic: license-file

# Refdes

Reference documentation for hardware design decisions. Typed, linked items like
sphinx-needs; authoring ergonomics closer to Quarto; a Doxygen-shaped reference
site. The part that is neither: the math in a document is *evaluated*, carries
units, and is checked against your constraints at build time.

A spec change propagates through the arithmetic and tells you which decisions it
just invalidated.

**Full documentation is in [`docs/`](docs/index.md)** — and it is built with
Refdes itself:

```bash
cd docs-site && refdes build   # -> _docs/index.html
```

Start with [getting started](docs/getting-started.md) for a ten-minute tutorial, or
[concepts](docs/concepts.md) for the model behind it. What follows is a summary.

## Install

```bash
python -m venv .venv && ./.venv/Scripts/python.exe -m pip install -e .
```

## Commands

```bash
refdes build     # render _site/ and items.json
refdes check     # validate without rendering; non-zero exit on errors
refdes index     # print items.json to stdout, for tooling
refdes id        # allocate IDs for items that have none, writing them back
refdes audit     # list suppressed fields, resealed entries, board moves, and imports
```

## Editor support

A VS Code extension lives in [`editors/vscode/`](editors/vscode/README.md) —
inline calc results, live diagnostics, ID completion, hover previews, and
go-to-definition. Open that folder and press <kbd>F5</kbd>; there is no build step.

For squiggles without installing anything, [`.vscode/tasks.json`](.vscode/tasks.json)
carries problem matchers that work out of the box.

## Authoring

One object model, two serializations. Rich items get a file; bulk items get a list.

**`items/**/*.md`** — front-matter plus a markdown body, for decisions and anything
with prose, calcs, or options. Not limited to one item: a further `---` starts a
new item's front-matter, and an optional leading block whose only key is
`defaults:` applies to every item that follows, the same way `defaults:` works in
a list file:

```markdown
---
defaults:
  type: decision
  prefix: DEC-PWR
---
id: DEC-PWR-001
title: 3V3 rail regulator topology
---

Body of the first decision.

---
title: LDO thermal fallback, rejected
---

Body of the second decision. Each item keeps its own body — the next item's
front-matter is where this one ends.
```

**`items/**/*.yaml`** — a list sharing `defaults:`, for bulk requirements:

```yaml
defaults:
  type: requirement
  prefix: REQ-PWR
  owner: J. Bin
items:
  - text: The unit shall operate from 9 V to 36 V.
  - text: Converter efficiency shall exceed 90 % at half load.
```

Leave the `id:` off. `refdes id` allocates the next free number and writes it
into the file. IDs are never derived from position, so inserting a requirement at
the top does not shift anything below it.

## Math

Restricted expression DSL — assignments, arithmetic, units, and a whitelist of
functions. No loops, conditionals, imports, or attribute access, so a document
cannot execute code and every result is deterministic.

````markdown
```calc
V_out            = 3.3 V
I_load           = 1.2 A
eff              = 0.93
P_diss  : W      = V_out * I_load * (1/eff - 1)
A_board          = 1.4 inch * 0.9 inch
P_dens  : W/in^2 = P_diss / A_board
```
````

Reference a result inline with `{{P_diss}}`. Units are the type system: `V * A`
yields watts, and `V + A` is a build error rather than a silent wrong answer.

Tolerances propagate as intervals:

```
V_in = 12 V ± 5%     ->   12 V   (11.4 V … 12.6 V)
```

### Writing units

A bare token after a number is always read as a unit. Write them without internal
spaces (`2 W/in^2`, `9.81 m/s^2`) and use `·` for products (`N·m`).

**Brackets are the escape hatch.** `0.5 [h]` is unambiguously half an hour even
when a variable named `h` is in scope, and anything goes inside them, including
`*`:

```
tq = 2 [N*m]
```

Single-letter variables collide with SI units constantly — `A` for area, `C` for
capacitance, `L` for inductance, `R` for resistance. That is normal engineering
notation, so a collision is a **warning, not an error**: juxtaposition never means
multiplication, so `1.2 A` has exactly one parse and the unit reading always wins.
The warning exists only in case you meant `1.2 * A`. Brackets silence it.

Units a compound like `W/h` contains are never flagged — a segment inside a
compound cannot be anything but a unit.

### Unit assertions

`name : unit = expression` declares what the result should be, and fails the build
if the algebra drifts:

```
P : W = V_out / I_load     ->  error: declared as W but the expression evaluates to V/A
```

It also pins the display unit, which is why `P_dens : W/in^2` reports
`0.2366 W/in²` rather than `236.6 mW/in²` — matching the constraint it is checked
against. Annotations are optional; use them where getting the dimension wrong would
be expensive.

## Checks

A constraint declares a limit; a decision declares what it is checking:

```yaml
# in the constraint
limit: "<= 0.15 W/in^2"

# in the decision
checks:
  - value: P_dens
    against: CON-THM-001
```

`refdes check` fails the build when the value violates the limit, evaluated at
the **worst-case tolerance bound**, not the nominal.

## Change tracking

Every field declares what a change to it means:

| mode | timeline | baseline diff | invalidates downstream |
|---|---|---|---|
| `invalidate` | yes | yes | yes |
| `log` | yes | no | no |
| `ignore` | no | no | no |

Set per field in `refdes.yaml`, overridable per item (with a required
`reason:`). The content hash is computed over `invalidate` fields only, which is
what stops an owner change from marking fifty links suspect.

`refdes audit` lists everything currently suppressed. Suppression is allowed;
invisible suppression is not.

## The design log

A dated, append-only record of how the design actually got where it is — the
measurements, the dead ends, the reasoning between a requirement being handed to
you and a decision being made. A `decision` is the settled conclusion; a `log`
entry is a step on the way to one.

```yaml
defaults:
  type: log
  prefix: LOG-A
  board: board-a
  author: J. Bin
items:
  - id: LOG-A-005
    date: 2026-03-16
    summary: Thermal check fails; power stage is over the density budget.
    addresses: [CON-THM-001]
    body: |
      Three ways out, none chosen yet: widen the allocation, improve efficiency,
      or renegotiate the 0.15 W/in² figure...
```

That `board: board-a` is the `log` type's own hand-typed field, not the
`boards:` registry's reserved `board:` override (below) — a type's own field
always wins, so a `log` entry stays plain-text-tagged rather than board-scoped
until that field is retired in favor of the reserved key.

Entries are **sealed on first build**. Editing one afterwards fails the build:

```
ERROR  LOG-A-003 is append-only and has been modified since it was sealed.
       Append a new entry with `amends: [LOG-A-003]` instead, or run with
       --reseal if the edit is deliberate.
```

Corrections are appended, exactly as in a paper notebook where you strike through
and initial rather than erase. `--reseal` exists for deliberate overrides and is
reported by `refdes audit`, so an override is always visible.

This cannot *prevent* an edit — no file-based tool can. It detects one, which is
what actually matters.

## Coverage

Three separate questions, deliberately not collapsed into one flag:

| stage | meaning |
|---|---|
| `open` | nothing references it at all |
| `addressed` | a log entry works on it |
| `satisfied` | a decision claims to meet it |
| `verified` | a test proves it |

A requirement can be satisfied without being verified, and addressed without being
satisfied. Collapsing those is how open work goes missing. `coverage.html` sorts
the least-covered first, and the same data is in `items.json` under `coverage`.

## Multiple boards

For a family of boards in one repo, use folders and per-board prefixes
(`REQ-A-PWR`, `REQ-B-PWR`, `IFC-*` for anything shared). Links, checks, and
back-links all work across folders with no extra machinery.

Register the boards to get more than organisation — per-board pages, a token
lint, and drift tracking when a file moves:

```yaml
boards:
  board-a:
    label: "Board A"
    token: A          # optional; checked against item id prefixes
  board-b:
    label: "Board B"
    token: B
```

A board is the first path segment under `items/`, matched against this
registry. `boards:` is entirely **opt-in** — absent, nothing here does
anything, and every item's board stays unset. Override the path for one item
(or a whole file, in `defaults:`) with the reserved `board:` key, the same way
`prefix:` overrides a file's default prefix; naming an unregistered board is a
build error, not a silent no-op.

Each registered board gets its own scoped `document-<board>.html`,
`coverage-<board>.html`, `log-<board>.html`, and `summary-<board>.html`
alongside the unchanged project-wide versions — handing `document-board-a.html`
to Board A's team shows only their items. If a board declares `token:`, the
build warns when an item's id prefix does not contain it — a lint, not a
rename. `.refdes/boards.yaml` records which board each item was on at the last
build (commit it, the same as `.refdes/ids.yaml`); moving a file into a
different board's folder warns on the next build instead of silently
re-scoping it. This repo's own [`items/`](items/) and [`refdes.yaml`](refdes.yaml)
register two boards as a worked example. See [multiple boards](docs/multi-board.md)
for the rest, including per-board token linting and drift acceptance.

When boards need to ship, version, or be owned separately, split them into projects
and import the shared one:

```yaml
imports:
  - name: platform
    items: ../platform-interfaces/_site/items.json
    version: "2026.3"
```

You import the built **artifact**, not the source tree, because a shared interface
spec is a dependency with a version — you qualify a board against rev C and upgrade
deliberately. Reading a live source folder gives you a spec that shifts under you
between builds.

Imported items are read-only: you link to them, check against their limits, and get
a reference page showing which of *your* items depend on them. They are excluded
from your coverage and validation, and they keep the content hash their own project
computed. A version mismatch or an ID collision is a hard error.

**IDs must be unique across every project you import.** Give each project its own
prefix. If you may ever split boards apart, adopt board-token prefixes now
(`REQ-A-PWR-001`) — it costs nothing today and IDs are frozen once baselined.

## Project lifecycle

Three states, two commands, no flags on either. **draft** is the state a
project is in when nothing has been stamped — not a command, nothing to
run; `check`/`build` stay exactly as permissive as always.

```bash
refdes revision rev-c   # cuts an internal checkpoint, unconditionally
refdes release  rev-b   # runs the full readiness gate, stamps only if it passes
```

`release` fails safely and says exactly what's blocking — running it when
you're not ready *is* the check, which is why there's no `--dry-run`.
Both write `.refdes/baselines/<name>.yaml`, a content-hash snapshot of
every local item; re-stamping the same name with the same content is a
no-op, with different content it's an error — a name is a permanent label
once written. `refdes audit` reports what's changed since the last
revision and since the last release. See [project lifecycle](docs/lifecycle.md).

## Output

`_site/` is static HTML — no server, no build step for the reader, works with JS
disabled. Cross-references get hover previews (keyboard-accessible, Escape to
dismiss, tap on touch) showing the target's fields and current check state.

`summary.html` is the design-review page. `index.html` says what exists; the summary
says what to worry about:

- **Margins** — every check ranked by worst-case slack against its limit, tightest
  first. Pass/fail hides the difference between clearing a thermal limit by 3% and
  clearing it by 200%; only one of those survives a hot day.
- **Computed values** — every number every calc block produces, in one table.
- **Not linked to anything** — where traceability quietly stops. A constraint that is
  *checked against* counts as traced, even though a check creates no link edge.

`_site/items.json` is the machine-readable export, margins included. Anything
downstream should read that, not the HTML.

## Not built yet

- **Git history layer** — field-level diffs, item timelines, and true
  edge-scoped suspect links. Baselines (`refdes revision`/`refdes release`)
  and the item-scoped diff between them *are* built, and need no git reader
  at all — see [project lifecycle](docs/lifecycle.md). What's left needs
  actual history: *what* changed within an item, not just that it did.
- **Federated view** — one combined site across several *projects* (not to
  be confused with [workspaces](docs/workspaces.md), which group boards
  inside one project), with cross-project back-links and an
  interface-compliance matrix. Individual projects import and build
  correctly today; the federated view does not exist.
- **Solving for unknowns** — `sympy` symbolic solve. Forward evaluation only today.
- **Client-side search** and query blocks in narrative pages.
- **Typeset math** — calc blocks render as clean tables; KaTeX can be vendored later.
- **Spreadsheet import**, KiCad/BOM extraction, Monte Carlo, trig functions.

## Known limitations

- **Torque reads as energy.** `N·m` and `J` are dimensionally identical, so a
  torque collapses to joules on display. Pin it with an assertion (`tq : N*m = …`)
  if it matters. Every units library has this problem; none solve it without a
  separate notion of quantity kind.
- **Interval widths are conservative.** A variable appearing more than once in an
  expression is treated as independent at each occurrence, so `x - x` reports a
  non-zero width. Exact for monotonic expressions, loose otherwise.

## Tests

```bash
./.venv/Scripts/python.exe -m pytest tests/ -q
```

The suite covers the invariants that must not quietly break: IDs never shift or get
reused, the DSL cannot execute code, the content hash follows the `on_change`
policy exactly, and checks use the worst-case bound.

## Releasing

The CLI ships to PyPI and the extension to the VS Code Marketplace. They version
independently — a calc fix bumps only `pyproject.toml`, an autocomplete fix only
`editors/vscode/package.json`.

```bash
python release.py cli 0.1.1
python release.py extension 0.2.0
```

The script bumps the version, wipes stale build output, rebuilds, and validates —
in that order, because building before bumping ships stale metadata, and
`twine upload dist/*` will happily upload a leftover wheel next to the new one. It
refuses on a dirty tree, on a version that is not higher than the current one, and
on a version already published to PyPI.

It stops before uploading and prints the remaining commands. **Neither registry ever
lets you overwrite or reuse a version**, so publishing stays a separate, deliberate
step. Add `--dry-run` to run the checks and change nothing.
