Skip to content

Commit 0ca54d2

Browse files
committed
Release GERM 0.4.1
1 parent 996812f commit 0ca54d2

16 files changed

Lines changed: 1376 additions & 137 deletions

File tree

.github/workflows/ci.yml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,24 @@ on:
77
permissions:
88
contents: read
99

10+
concurrency:
11+
group: ci-${{ github.workflow }}-${{ github.ref }}
12+
cancel-in-progress: true
13+
1014
jobs:
1115
test:
12-
runs-on: ubuntu-latest
16+
runs-on: ubuntu-24.04
17+
timeout-minutes: 30
1318
steps:
1419
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
20+
with:
21+
persist-credentials: false
1522
- uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5
1623
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
1724
with:
18-
node-version: "22"
25+
node-version: "22.22.3"
1926
- name: Install Python dependencies
20-
run: uv sync --extra dev
27+
run: uv sync --extra dev --locked
2128
- name: Python tests
2229
run: uv run pytest -q
2330
- name: Ruff
@@ -38,7 +45,10 @@ jobs:
3845
- name: Dashboard syntax
3946
run: |
4047
node --check dashboard/static/app.js
48+
node --check dashboard/static/audio_engine.js
49+
node --check dashboard/static/cosmoaudition.js
4150
node --check dashboard/static/dish.js
51+
node --check dashboard/static/docs_theme.js
4252
node --check dashboard/static/micro_forms.js
4353
node --check dashboard/static/micro_render.js
4454
node --check dashboard/static/micro_unicode.js

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
# Changelog
22

3+
## 0.4.1 — Sonic Matter Framework contract corrections
4+
5+
- Fixed the Cosmoaudition frame bridge to consume the published
6+
`outputValue` field, expose an explicit GERM `value`, and retain the control's
7+
status, mapping parameters, confidence, and epistemic note. Real applied,
8+
held, and uncertainty routes are no longer silently withheld.
9+
- Kept canonical `sound_id`, Sonic Lineage, and Earworm identifiers stable
10+
when filenames change; refreshed written MASA sidecars after path changes and
11+
refused renames that would break an immutable Akousmata file locator. Rename
12+
companion writes are transactional, and matching deferred-latent locators
13+
follow their audio without changing identity.
14+
- Restricted bulk file deletion's primary path to supported audio formats,
15+
required the actual metadata companion, preserved Akousmata locators, and
16+
removed only verified written MASA sidecars under explicit companion-delete
17+
semantics.
18+
- Preserved Cosmoaudition frame-level signals and sources beside resolved
19+
routes, and withheld malformed or out-of-range executable controls with an
20+
explicit bridge reason.
21+
- Aligned active project language to **Sonic Matter Framework**: MASA
22+
describes, Cosmoaudition observes and modulates, and GERM cultivates. The
23+
projects remain distinct.
24+
325
## 0.4.0 — Sonic Matter Stack alignment
426

527
- **Fixed a MASA protocol break.** Every sidecar cited

CITATION.cff

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ authors:
55
- family-names: "Isaza"
66
given-names: "eme"
77
affiliation: "Sonic Field Labs"
8-
version: "0.4.0"
9-
date-released: "2026-08-07"
8+
version: "0.4.1"
9+
date-released: "2026-08-09"
1010
license: "MPL-2.0"
1111
repository-code: "https://github.com/sonicfieldlabs/germ"
1212
abstract: "germ is a local generative microsound environment whose generated sounds retain prompts, parents, mutations, listening metadata, and Earworm-compatible lineage."

README.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ listened to, and traced through lineage. A listening from Oída can become a
88
prompt or source in GERM; a successful render can become a descendant in
99
Akousmata and return to Oída for another listening.
1010

11-
Current release: `0.4.0`.
11+
Current release: `0.4.1`.
1212

1313
GERM is part of two Sonic Field Labs constellations: it cultivates within
1414
[The Listening Stack](https://sonicfield.org/stack) and describes its material
15-
through the [Sonic Matter Stack](#sonic-matter-stack-integration), whose
15+
through the [Sonic Matter Framework](#sonic-matter-framework-integration), whose
1616
protocol boundary is MASA 0.1.0.
1717

1818
GERM is an independent Sonic Field Labs project. It can use Stable Audio 3
@@ -127,20 +127,21 @@ promising one universal hardware minimum.
127127
All three scales use the same module graph, semantic FX bridge, sessions,
128128
library, and lineage model.
129129

130-
## Sonic Matter Stack integration
130+
## Sonic Matter Framework integration
131131

132132
GERM belongs to two constellations. The Listening Stack is how a sound is
133-
heard, remembered, and re-listened; the **Sonic Matter Stack** is how sound is
134-
described *as matter* — its provenance, its measurements, its granular and
135-
spectral operations, and the observations that modulate it. GERM keeps those
136-
components distinct while making their contracts usable inside the cultivation
137-
graph:
133+
heard, remembered, and re-listened; the **Sonic Matter Framework** joins three
134+
distinct projects around sound described *as matter*: MASA supplies the
135+
descriptive protocol, Cosmoaudition supplies attributed observations and
136+
modulation decisions, and GERM cultivates sound while retaining provenance and
137+
lineage. GERM keeps those roles distinct while making their contracts usable
138+
inside its cultivation graph:
138139

139140
| Component | Version / contract | Boundary in GERM |
140141
| --- | --- | --- |
141142
| [MASA](https://github.com/sonicfieldlabs/MASA) | 0.1.0 (MIT) | Optional descriptive JSON sidecars under `output/masa/`, citing the published canonical schema at `masa.sonicfield.org`. They never replace Sonic Lineage or turn a successful render into a failure. |
142143
| MASA Processing | `masa-processing-request` 0.1.0 | Every Micro module declares a granular or spectral operation in MASA's engine-neutral terms. GERM states the intention and binds no DSP library. |
143-
| MATERIA || Matter Analysis provides a bounded local analyzer informed by the shared measured / inferred / unavailable distinction; it is not a claim of listening. |
144+
| GERM Matter Analysis || A bounded local analyzer informed by the shared measured / inferred / unavailable distinction; it is not a claim of listening or a fourth framework project. |
144145
| [Cosmoaudition](https://github.com/sonicfieldlabs/cosmoaudition) | `cosmo/modulation/v0.1` | A loopback-only, response-bounded HTTP bridge reads source status, snapshots, and modulation frames. GERM never contacts observation providers directly. |
145146

146147
### Micro modules as MASA processing operations
@@ -173,9 +174,13 @@ the route remains inactive until its state is explicit.
173174
`GET /cosmoaudition/frame` reads one modulation frame and resolves it into
174175
GERM routes. It reads the frame's `controls`, never its bare `values` map:
175176
a value arrives with the decision that produced it, or it does not arrive.
176-
Withheld routes and emitted absences are reported rather than dropped. Server-
177-
Sent Events are deliberately not bridged, because this bridge is a bounded
178-
request/response client; poll `/cosmoaudition/frame` instead.
177+
Out-of-range or structurally invalid controls are withheld with an explicit
178+
bridge reason. Frame-level `signals` and `sources` travel beside the routes so
179+
their source, unit, sphere, epistemic status, and temporal character remain
180+
traceable through `signalId`. Withheld routes and emitted absences are reported
181+
rather than dropped. Server-Sent Events are deliberately not bridged, because
182+
this bridge is a bounded request/response client; poll `/cosmoaudition/frame`
183+
instead.
179184

180185
## Listening Stack integration
181186

apps/macos/script/build_and_run.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ APP_NAME="germ"
66
EXECUTABLE_NAME="germ-macos"
77
BUNDLE_ID="org.sonicfield.germ"
88
MIN_SYSTEM_VERSION="13.0"
9-
MARKETING_VERSION="0.4.0"
10-
BUNDLE_VERSION="6"
9+
MARKETING_VERSION="0.4.1"
10+
BUNDLE_VERSION="7"
1111

1212
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
1313
# Keep the runnable bundle where repository users expect to find apps. The

dashboard/static/app.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3487,7 +3487,7 @@ async function renameFile(audioPath, metadataPath, newStem) {
34873487
async function deleteSelectedFiles() {
34883488
const items = [...rackSelectedKeys].map(rackItemByKey).filter(Boolean);
34893489
if (!items.length) return;
3490-
if (!confirm(`Permanently delete ${items.length} selected sound file(s) from disk? This cannot be undone.`)) return;
3490+
if (!confirm(`Permanently delete ${items.length} selected sound(s) and their linked local metadata/MASA companions from disk? Akousmata-linked sounds are preserved. This cannot be undone.`)) return;
34913491

34923492
beginWork("Deleting Files", `${items.length} items`);
34933493
try {
@@ -3514,7 +3514,7 @@ async function deleteSelectedFiles() {
35143514

35153515
async function deleteSingleFile(audioPath, metadataPath) {
35163516
const filename = audioPath.split("/").pop();
3517-
if (!confirm(`Permanently delete "${filename}" from disk? This cannot be undone.`)) return;
3517+
if (!confirm(`Permanently delete "${filename}" and its linked local metadata/MASA companions from disk? Akousmata-linked sounds are preserved. This cannot be undone.`)) return;
35183518

35193519
beginWork("Deleting File", filename);
35203520
try {

docs/api_reference.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -864,8 +864,15 @@ The server only reveals files inside the configured output directory.
864864
}
865865
```
866866

867-
Renames the audio file (and metadata file, when given) to a sanitized stem and rewrites
868-
the path/id fields inside the metadata JSON, rolling back on failure.
867+
Renames the audio file (and its actual metadata companion, when one exists) to a sanitized stem and
868+
rewrites storage paths inside the metadata JSON. Canonical `sound_id`, lineage,
869+
and Earworm identifiers remain stable. Same-name metadata is discovered even
870+
when the caller omits it, and only a metadata object that actually identifies
871+
the audio can be used. Matching deferred-latent locators are rewritten. A
872+
verified written MASA sidecar is refreshed transactionally in place; a failed
873+
companion write rolls the rename back. Sounds remembered in Akousmata, or with
874+
an unresolved requested memory write, are refused because moving their audio
875+
could break the shared record's immutable file locator.
869876

870877
## POST /files/delete
871878

@@ -880,4 +887,10 @@ the path/id fields inside the metadata JSON, rolling back on failure.
880887
}
881888
```
882889

883-
Best-effort bulk delete of output files (max 500 items per request).
890+
Best-effort bulk delete of audio output files and their explicitly supplied,
891+
verified metadata companions (max 500 items per request). The primary path must
892+
use a supported audio extension. If companion metadata exists, its exact path
893+
is required; a verified written MASA sidecar is then deleted with that local
894+
record. Remembered or unresolved requested Akousmata are refused because their
895+
shared `file://` locator must remain valid. Unrelated JSON and unresolved
896+
sidecar evidence are preserved.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "germ"
3-
version = "0.4.0"
3+
version = "0.4.1"
44
description = "Local-first modular laboratory for generative microsound, Stable Audio workflows, and lineage-aware cultivation."
55
readme = "README.md"
66
license = "MPL-2.0"

server/cosmoaudition.py

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,60 @@ def status(self) -> dict[str, Any]:
187187
#: `skipped` and `refused` carry none; `held` and `uncertainty` carry one that
188188
#: must keep its status when it travels.
189189
EXECUTABLE_FRAME_STATUSES = frozenset({"applied", "held", "uncertainty"})
190+
WITHHELD_FRAME_STATUSES = frozenset({"skipped", "refused"})
191+
192+
193+
def _finite_frame_number(value: Any) -> bool:
194+
if not isinstance(value, (int, float)) or isinstance(value, bool):
195+
return False
196+
try:
197+
return math.isfinite(value)
198+
except OverflowError:
199+
# JSON integers are unbounded in Python but not in Cosmoaudition's
200+
# JavaScript runtime. They cannot become executable control values.
201+
return False
202+
203+
204+
def _frame_object_list(
205+
frame: dict[str, Any],
206+
key: str,
207+
*,
208+
required: bool = False,
209+
) -> list[dict[str, Any]]:
210+
value = frame.get(key)
211+
if value is None:
212+
if required:
213+
raise CosmoauditionBridgeError(f"Cosmoaudition frame is missing its {key}")
214+
return []
215+
if not isinstance(value, list) or any(not isinstance(item, dict) for item in value):
216+
raise CosmoauditionBridgeError(f"Cosmoaudition frame has invalid {key}")
217+
return list(value)
218+
219+
220+
def _control_bridge_reason(control: dict[str, Any], status: Any) -> str | None:
221+
if status not in EXECUTABLE_FRAME_STATUSES:
222+
return None if status in WITHHELD_FRAME_STATUSES else "invalid-status"
223+
if any(
224+
not isinstance(control.get(key), str) or not control[key].strip()
225+
for key in ("mappingId", "signalId", "target")
226+
):
227+
return "invalid-route-identity"
228+
229+
output_value = control.get("outputValue")
230+
if not _finite_frame_number(output_value):
231+
return "invalid-output-value"
232+
output_range = control.get("outputRange")
233+
if (
234+
not isinstance(output_range, (list, tuple))
235+
or len(output_range) != 2
236+
or not all(_finite_frame_number(value) for value in output_range)
237+
or output_range[0] == output_range[1]
238+
):
239+
return "invalid-output-range"
240+
lower, upper = sorted(output_range)
241+
if not lower <= output_value <= upper:
242+
return "output-outside-range"
243+
return None
190244

191245

192246
def modulation_routes_from_frame(frame: dict[str, Any]) -> dict[str, Any]:
@@ -201,32 +255,51 @@ def modulation_routes_from_frame(frame: dict[str, Any]) -> dict[str, Any]:
201255
went missing is a fact about the frame.
202256
"""
203257

204-
controls = frame.get("controls")
205-
if not isinstance(controls, list):
206-
raise CosmoauditionBridgeError("Cosmoaudition frame is missing its controls")
258+
if frame.get("contract") != COSMOAUDITION_MODULATION_CONTRACT:
259+
raise CosmoauditionBridgeError(
260+
"Cosmoaudition frame does not declare "
261+
f"{COSMOAUDITION_MODULATION_CONTRACT}"
262+
)
263+
264+
controls = _frame_object_list(frame, "controls", required=True)
265+
signals = _frame_object_list(frame, "signals")
266+
sources = _frame_object_list(frame, "sources")
207267

208268
routes: list[dict[str, Any]] = []
209269
withheld: list[dict[str, Any]] = []
210270
for control in controls:
211-
if not isinstance(control, dict):
212-
continue
213271
status = control.get("status")
272+
output_value = control.get("outputValue")
273+
bridge_reason = _control_bridge_reason(control, status)
274+
executable = status in EXECUTABLE_FRAME_STATUSES and bridge_reason is None
214275
entry = {
215276
"target": control.get("target"),
216277
"mappingId": control.get("mappingId"),
217278
"signalId": control.get("signalId"),
279+
"layer": control.get("layer"),
218280
"status": status,
219281
"reason": control.get("reason"),
220-
"value": control.get("value"),
221-
"unit": control.get("unit"),
222-
"attribution": control.get("attribution"),
282+
"bridgeReason": bridge_reason,
283+
# ``value`` is GERM's consumer-facing route value. Cosmoaudition's
284+
# modulation contract names the same field ``outputValue``; keep
285+
# both names so the crosswalk is explicit and lossless.
286+
"value": output_value if executable else None,
287+
"outputValue": output_value if executable else None,
288+
"inputValue": control.get("inputValue"),
289+
"normalizedInput": control.get("normalizedInput"),
290+
"rawNormalizedInput": control.get("rawNormalizedInput"),
291+
"mappingAmount": control.get("mappingAmount"),
292+
"outputRange": control.get("outputRange"),
293+
"curve": control.get("curve"),
294+
"smoothingMs": control.get("smoothingMs"),
295+
"missingData": control.get("missingData"),
296+
"confidence": control.get("confidence"),
297+
"epistemicNote": control.get("epistemicNote"),
223298
}
224-
if status in EXECUTABLE_FRAME_STATUSES and isinstance(
225-
control.get("value"), (int, float)
226-
) and not isinstance(control.get("value"), bool):
299+
if executable:
227300
routes.append(entry)
228301
else:
229-
withheld.append({**entry, "value": None})
302+
withheld.append(entry)
230303

231304
absences = frame.get("absences")
232305
return {
@@ -237,6 +310,11 @@ def modulation_routes_from_frame(frame: dict[str, Any]) -> dict[str, Any]:
237310
"originMode": frame.get("originMode"),
238311
"routes": routes,
239312
"withheld": withheld,
313+
# Preserve frame-level source evidence. `signalId` is the join key; the
314+
# input observation's unit/status must not be mistaken for the routed
315+
# output parameter's unit/status by flattening the two objects together.
316+
"signals": signals,
317+
"sources": sources,
240318
"absences": absences if isinstance(absences, list) else [],
241319
"attribution": frame.get("attribution") if isinstance(frame.get("attribution"), list) else [],
242320
"masaRecordHref": frame.get("masaRecordHref"),

server/identity.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
PRODUCT_NAME = "germ"
55
PRODUCT_DESCRIPTION = "open-source modular lab for generative microsound"
6-
__version__ = "0.4.0"
6+
__version__ = "0.4.1"
77
LEGACY_ENGINE_NAME = "Germinator"
88
SOUND_MATTER_CONCEPT = "sound_matter"
99
SOUND_MATTER_SCALES = ["micro", "meso", "macro"]

0 commit comments

Comments
 (0)