From 8f6747ad484809707f2a68325ed286edb8be9258 Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Fri, 12 Jun 2026 00:45:25 +0000
Subject: [PATCH 01/32] =?UTF-8?q?fix:=20secret=5Fpath=5Fguard=20=E2=80=94?=
=?UTF-8?q?=20sync=20canonical=20DomI=20pattern=20(.env=20+=20root-relativ?=
=?UTF-8?q?e=20paths=20now=20blocked)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
scripts/hooks/secret_path_guard.sh | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/scripts/hooks/secret_path_guard.sh b/scripts/hooks/secret_path_guard.sh
index ec4e259..7324a50 100644
--- a/scripts/hooks/secret_path_guard.sh
+++ b/scripts/hooks/secret_path_guard.sh
@@ -1,6 +1,6 @@
#!/bin/bash
# PreToolUse:Write|Edit hook — blocks writes to secret-bearing paths.
-# Synced from DomI upstream. See domattioli/DomI scripts/hooks/secret_path_guard.sh.
+# Pattern synced from DomI canonical copy 2026-06-12 (fixed .env + root-relative bypass).
set -uo pipefail
@@ -8,14 +8,19 @@ input="$(cat)"
path="$(echo "$input" | jq -r '.tool_input.file_path // ""' 2>/dev/null)"
[ -z "$path" ] && exit 0
+# Exemptions — return 0 fast
case "$path" in
*.env.example|*.env.template|*.env.test|*.env.sample) exit 0 ;;
*token-rotation*|*REFRESH_TOKEN_DOCS*|*token_rotation*) exit 0 ;;
*secret-example*|*credentials.example*) exit 0 ;;
esac
-if echo "$path" | grep -qE '(^|/)\..env$|\.pem$|/credentials\.|/secrets?(\.|$)|/token(\.|$)|\.gpg$|id_rsa$|id_ed25519$'; then
- echo "BLOCKED (secret_path_guard): $path matches secret-bearing pattern. Rename to *.example or *.template variant." >&2
+# Block paths matching secret patterns.
+# `.env` matches both top-level `.env` and nested `*/foo.env` per CLAUDE.md
+# hard stop ("Never commit *.env"). Exemptions above (.env.example etc.) run
+# first.
+if echo "$path" | grep -qE '\.env$|\.pem$|(^|/)credentials\.|(^|/)secrets?(\.|$)|(^|/)token(\.|$)|\.gpg$|id_rsa$|id_ed25519$'; then
+ echo "BLOCKED (secret_path_guard): $path matches secret-bearing pattern. If intentional, rename to *.example or *.template variant." >&2
exit 2
fi
From f684c9907f3acc3f3e933374ca44a02862a923bf Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Fri, 12 Jun 2026 00:45:48 +0000
Subject: [PATCH 02/32] fix: remove destructive auto-reset onto deprecated
daily-maintenance from session start
---
scripts/instructions_on_start.sh | 14 +-------------
1 file changed, 1 insertion(+), 13 deletions(-)
diff --git a/scripts/instructions_on_start.sh b/scripts/instructions_on_start.sh
index bc8042f..ab942fe 100644
--- a/scripts/instructions_on_start.sh
+++ b/scripts/instructions_on_start.sh
@@ -20,19 +20,7 @@ if [[ "$_remote_url" =~ ^http://(.+@)?127\.0\.0\.1:([0-9]+)/ ]]; then
if [ -n "${GITHUB_TOKEN:-}" ]; then
git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/domattioli/CHILmesh.git"
echo "⚠ Local git proxy dead on :$_port — switched origin to github.com via GITHUB_TOKEN"
- # Resync local with remote (covers case where prior session pushed via MCP)
- if git fetch origin daily-maintenance 2>/dev/null; then
- _local_sha=$(git rev-parse HEAD 2>/dev/null || echo "")
- _remote_sha=$(git rev-parse origin/daily-maintenance 2>/dev/null || echo "")
- if [ -n "$_local_sha" ] && [ -n "$_remote_sha" ] && [ "$_local_sha" != "$_remote_sha" ]; then
- if [ -z "$(git status --porcelain 2>/dev/null)" ]; then
- git reset --hard origin/daily-maintenance >/dev/null
- echo " ↳ Resynced HEAD: $_local_sha → $_remote_sha"
- else
- echo " ↳ Dirty tree; not resyncing. Local=$_local_sha Remote=$_remote_sha"
- fi
- fi
- fi
+ echo "⚠ proxy dead — manual recovery: git fetch origin development && git status"
else
echo "⚠ Local git proxy dead on :$_port and no GITHUB_TOKEN — push will fail" >&2
fi
From c9f0e6c74993b927d63f07d53b115486b22e77e9 Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Fri, 12 Jun 2026 00:48:25 +0000
Subject: [PATCH 03/32] chore: exclude Claude-derived docs from distributions
(MANIFEST.in)
Wheel contents governed by [tool.setuptools] only; sdist governed by
MANIFEST.in directives. Never ship .claude, .specify, specs, .planning,
CLAUDE.md, AGENTS.md, .domi-pin, docs/ (development-only) artifacts to
PyPI distributions (operator directive 2026-06-12).
---
MANIFEST.in | 1 +
1 file changed, 1 insertion(+)
diff --git a/MANIFEST.in b/MANIFEST.in
index 3db0d3d..3d8e69b 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -33,6 +33,7 @@ prune src/chilmesh_cpp/build_local
prune src/chilmesh_cpp/build
exclude .domi-pin
exclude .gitignore
+exclude AGENTS.md
# README image assets must be re-included after `prune output`
include output/annulus_quickstart.png
From d4e63a8bf5c1e5958cf774c5b3bc3ed1edfb051f Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Fri, 12 Jun 2026 19:23:05 +0000
Subject: [PATCH 04/32] chore: spec-010 v3 (DomI): managed workflow copies
replace per-repo drift
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- workflow-conformance.yml: managed copy — lints changed workflows (v2.2 baseline) + managed-or-LOCAL.md creation gate
- sync-labels.yml deleted: DomI pushes canonical labels downstream (create/update-only; repo-local labels survive)
- LOCAL.md: registry of intentionally repo-local workflows
Managed copies are never edited here — source: DomI templates/workflows/ (WORKFLOWS.md registry).
https://claude.ai/code/session_01RqYaYzJ1MaWGAzscnyKAeP
CHILmesh-specific: introspect-monthly managed copy (was broken — referenced absent skills/ tree); python-package.yml + publish-pypi.yml registered repo-local.
---
.github/workflows/LOCAL.md | 10 +++
.../workflows/introspect-monthly-review.yml | 49 +++++++-----
.github/workflows/sync-labels.yml | 33 --------
.github/workflows/workflow-conformance.yml | 79 +++++++++++++++++++
4 files changed, 117 insertions(+), 54 deletions(-)
create mode 100644 .github/workflows/LOCAL.md
delete mode 100644 .github/workflows/sync-labels.yml
create mode 100644 .github/workflows/workflow-conformance.yml
diff --git a/.github/workflows/LOCAL.md b/.github/workflows/LOCAL.md
new file mode 100644
index 0000000..59864ee
--- /dev/null
+++ b/.github/workflows/LOCAL.md
@@ -0,0 +1,10 @@
+# LOCAL.md — repo-local workflow registry (spec-010 v2.3)
+
+Workflows listed here are intentionally repo-local (not DomI-managed copies). Adding a
+new local workflow requires a row here in the same PR — unlisted local
+workflows fail the workflow-conformance gate.
+
+| Workflow | Justification |
+|---|---|
+| `python-package.yml` | full cross-OS test matrix incl. macOS lanes (main-push gated) — macOS gating is repo-local by design (spec-010 v2.2 rule 8) |
+| `publish-pypi.yml` | PyPI release, tag-triggered — repo-specific release pipeline |
diff --git a/.github/workflows/introspect-monthly-review.yml b/.github/workflows/introspect-monthly-review.yml
index 8f81e93..3730821 100644
--- a/.github/workflows/introspect-monthly-review.yml
+++ b/.github/workflows/introspect-monthly-review.yml
@@ -1,12 +1,12 @@
+# DomI-managed workflow (spec-010 v3) — DO NOT EDIT IN THIS REPO.
+# Source: domattioli/DomI templates/workflows/introspect-monthly-review.yml @ v1 — edit upstream, then sync.
+# Drift: sync-from-domi session gate + DomI notify-downstream sync issues.
+# Template body embedded (consumers cannot read private DomI from Actions).
name: Introspect Monthly Review
-# Self-sustaining monthly cadence for the introspect skill (DomI #120, follow-up to #9).
-# Opens one dated "introspect skill monthly review" issue from the skill's template.
-# Idempotent: skips if an open issue for the current month already exists.
-
on:
schedule:
- - cron: '0 0 1 * *' # 00:00 UTC on the 1st of each month
+ - cron: '0 0 1 * *'
workflow_dispatch:
permissions:
@@ -17,23 +17,17 @@ jobs:
open-review-issue:
name: Open monthly introspect review issue
runs-on: ubuntu-latest
+ timeout-minutes: 10
steps:
- - uses: actions/checkout@v4
-
- name: Open or skip monthly review issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- TEMPLATE: skills/introspect/templates/monthly-review.md
+ GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
MONTH="$(date -u +%Y-%m)"
- TITLE="chore: introspect skill monthly review — ${MONTH}"
-
- if [ ! -f "${TEMPLATE}" ]; then
- echo "::error::template ${TEMPLATE} missing"
- exit 1
- fi
+ TITLE="chore: introspect monthly review — ${MONTH}"
EXISTING="$(gh issue list --state open --search "in:title \"${TITLE}\"" --json number --jq '.[0].number // empty')"
if [ -n "${EXISTING}" ]; then
@@ -41,10 +35,23 @@ jobs:
exit 0
fi
- BODY="$(sed "s/{{MONTH}}/${MONTH}/g" "${TEMPLATE}")"
-
- gh issue create \
- --title "${TITLE}" \
- --body "${BODY}" \
- --label "type: chore" \
- --label "priority: someday"
+ BODY="$(printf '%s\n' \
+ "# introspect — monthly review (${MONTH})" \
+ "" \
+ "Recurring per-repo review of the introspection corpus. Keep it useful, not unbounded. Tracks DomI #120." \
+ "" \
+ "## Checklist" \
+ "" \
+ "- [ ] Scan \`docs/introspections/*.md\` entries since last review. Record count + date range." \
+ "- [ ] Prune stale/duplicate lessons. List each pruned entry (file + reason). None → say so." \
+ "- [ ] Recurring pain across entries → route to a DomI \`request: skill\` issue via request-from-domi." \
+ "- [ ] Confirm CLAUDE.md lessons still match real behavior. Fix drift in same PR." \
+ "" \
+ "## Outcome" \
+ "" \
+ "<2-4 line summary: corpus count, prunes, pains routed, drift fixes.>" \
+ "" \
+ "---" \
+ "_Auto-opened monthly. Close after the checklist lands. Model-authored follow-ups carry the standard \`[model:…, repo:…, session:…]\` footer._")"
+
+ gh issue create --title "${TITLE}" --body "${BODY}" --label "type: chore" --label "priority: someday"
diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml
deleted file mode 100644
index 3c97479..0000000
--- a/.github/workflows/sync-labels.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-name: Sync Labels
-
-on:
- push:
- branches: [main]
- paths:
- - '.github/labels.yml'
- - '.github/workflows/sync-labels.yml'
- workflow_dispatch:
-
-permissions:
- issues: write
- contents: read
-
-jobs:
- sync:
- name: Sync labels from .github/labels.yml
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Flatten labels.yml to array
- run: |
- yq -o=json '.labels' .github/labels.yml \
- | jq 'map({name, color, description: (.description // "" | .[0:100])})' \
- > "${RUNNER_TEMP}/labels.flat.json"
- echo "Flattened $(jq 'length' "${RUNNER_TEMP}/labels.flat.json") labels."
-
- - name: Sync labels
- uses: EndBug/label-sync@v2
- with:
- config-file: ${{ runner.temp }}/labels.flat.json
- delete-other-labels: true
diff --git a/.github/workflows/workflow-conformance.yml b/.github/workflows/workflow-conformance.yml
new file mode 100644
index 0000000..17a09ae
--- /dev/null
+++ b/.github/workflows/workflow-conformance.yml
@@ -0,0 +1,79 @@
+# DomI-managed workflow (spec-010 v3) — DO NOT EDIT IN THIS REPO.
+# Source: domattioli/DomI templates/workflows/workflow-conformance.yml @ v1 — edit upstream, then sync.
+# Drift: sync-from-domi session gate + DomI notify-downstream sync issues.
+name: workflow-conformance
+
+on:
+ pull_request:
+ paths:
+ - '.github/workflows/**'
+
+permissions:
+ contents: read
+
+concurrency:
+ group: workflow-conformance-${{ github.head_ref || github.ref_name }}
+ cancel-in-progress: true
+
+jobs:
+ lint:
+ name: Lint workflows (v2.2 + v3)
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+
+ - name: Lint changed workflows (v2.2 baseline + managed-or-LOCAL.md rule)
+ run: |
+ set -e
+ DEAD_RE='daily-''maintenance|cla''ude/'
+ PATHS_LIT='pat''hs:'
+ PATHS_IGN_LIT='pat''hs-ignore:'
+ SETUP_PY_LIT='setup-''python'
+ PIP_CACHE_LIT='cache: ''pip'
+ OLD_ACT_RE='checkout@v[0-4]|setup-''python@v[0-5]'
+ MANAGED_RE='DomI-managed ''workflow'
+ STUB_RE='uses: domattioli/DomI/.github/workflows/'
+
+ git fetch origin "$GITHUB_BASE_REF"
+ changed_wfs=$(git diff --name-only "origin/$GITHUB_BASE_REF"...HEAD -- '.github/workflows/*.yml' | sort || true)
+
+ echo "## Changed-workflow lint (spec-010 v2.2 + v3, blocking)" >> "$GITHUB_STEP_SUMMARY"
+ violations=0
+ for wf in $changed_wfs; do
+ [ -f "$wf" ] || continue
+ name=$(basename "$wf")
+ issues=""
+ managed=0
+ if grep -q "$MANAGED_RE" "$wf" || grep -q "$STUB_RE" "$wf"; then managed=1; fi
+ # v2.2 baseline (applies to managed copies too)
+ grep -q "timeout-minutes:" "$wf" || { issues="${issues}missing timeout-minutes; "; violations=1; }
+ # concurrency required only on push/PR-triggered CI (schedule/dispatch/issues exempt)
+ if grep -qE '^[[:space:]]*(push|pull_request):' "$wf"; then
+ grep -q "concurrency:" "$wf" || { issues="${issues}missing concurrency; "; violations=1; }
+ fi
+ if grep -E 'branches|^[[:space:]]*-[[:space:]]' "$wf" | grep -qE "$DEAD_RE"; then issues="${issues}references dead branch; "; violations=1; fi
+ if grep -qE "$OLD_ACT_RE" "$wf"; then issues="${issues}outdated action versions; "; violations=1; fi
+ grep -q "^permissions:" "$wf" || { issues="${issues}missing permissions block; "; violations=1; }
+ if grep -v '^ *#' "$wf" | grep -q "$SETUP_PY_LIT" && ! grep -q "$PIP_CACHE_LIT" "$wf"; then issues="${issues}setup-python without pip cache; "; violations=1; fi
+ if grep -q "$PATHS_LIT" "$wf" && grep -q "$PATHS_IGN_LIT" "$wf"; then issues="${issues}paths + paths-ignore same event; "; violations=1; fi
+ # v3 creation gate: not managed → must be registered in LOCAL.md
+ if [ "$managed" -eq 0 ]; then
+ if [ ! -f .github/workflows/LOCAL.md ] || ! grep -qF "$name" .github/workflows/LOCAL.md; then
+ issues="${issues}local workflow not registered in LOCAL.md (spec-010 v3); "; violations=1
+ fi
+ fi
+ if [ -z "$issues" ]; then
+ if [ "$managed" -eq 1 ]; then
+ echo "- $name: ✓ (managed)" >> "$GITHUB_STEP_SUMMARY"
+ else
+ echo "- $name: ✓" >> "$GITHUB_STEP_SUMMARY"
+ fi
+ else
+ echo "- $name: ✗ ${issues%; }" >> "$GITHUB_STEP_SUMMARY"
+ fi
+ done
+ [ -z "$changed_wfs" ] && echo "No workflow changes in PR" >> "$GITHUB_STEP_SUMMARY"
+ if [ "$violations" -ne 0 ]; then echo "::error::workflow conformance violations — see job summary"; exit 1; fi
From b5f1b2739d57052e5613dde959942070728778f1 Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Fri, 12 Jun 2026 19:25:45 +0000
Subject: [PATCH 05/32] fix: re-sync workflow-conformance managed copy
(self-match + log echo)
Source: DomI templates/workflows @ development (conformance lint fix).
---
.github/workflows/workflow-conformance.yml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/workflow-conformance.yml b/.github/workflows/workflow-conformance.yml
index 17a09ae..ef760dd 100644
--- a/.github/workflows/workflow-conformance.yml
+++ b/.github/workflows/workflow-conformance.yml
@@ -57,7 +57,7 @@ jobs:
if grep -E 'branches|^[[:space:]]*-[[:space:]]' "$wf" | grep -qE "$DEAD_RE"; then issues="${issues}references dead branch; "; violations=1; fi
if grep -qE "$OLD_ACT_RE" "$wf"; then issues="${issues}outdated action versions; "; violations=1; fi
grep -q "^permissions:" "$wf" || { issues="${issues}missing permissions block; "; violations=1; }
- if grep -v '^ *#' "$wf" | grep -q "$SETUP_PY_LIT" && ! grep -q "$PIP_CACHE_LIT" "$wf"; then issues="${issues}setup-python without pip cache; "; violations=1; fi
+ if grep -v '^ *#' "$wf" | grep -q "$SETUP_PY_LIT" && ! grep -q "$PIP_CACHE_LIT" "$wf"; then issues="${issues}missing pip cache on py-setup; "; violations=1; fi
if grep -q "$PATHS_LIT" "$wf" && grep -q "$PATHS_IGN_LIT" "$wf"; then issues="${issues}paths + paths-ignore same event; "; violations=1; fi
# v3 creation gate: not managed → must be registered in LOCAL.md
if [ "$managed" -eq 0 ]; then
@@ -73,6 +73,7 @@ jobs:
fi
else
echo "- $name: ✗ ${issues%; }" >> "$GITHUB_STEP_SUMMARY"
+ echo "VIOLATION: $name: ${issues%; }"
fi
done
[ -z "$changed_wfs" ] && echo "No workflow changes in PR" >> "$GITHUB_STEP_SUMMARY"
From 79f59b21b17421cb53e230e16a80c32b88d57b39 Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Fri, 12 Jun 2026 19:36:53 +0000
Subject: [PATCH 06/32] =?UTF-8?q?docs:=20dedup=20coding-dispatch=20block?=
=?UTF-8?q?=20=E2=86=92=20DomI=20canonical=20policy=20(#83)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Verbatim Haiku-dispatch block replaced with a 3-line binding summary + pointer
to DomI .claude/policies/coding-dispatch.md (single source of truth). Rule
unchanged (MUST/Exception/Scope all retained); rationale centralized upstream.
https://claude.ai/code/session_01RqYaYzJ1MaWGAzscnyKAeP
---
.claude/CLAUDE.md | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md
index 848e7f8..08ce418 100644
--- a/.claude/CLAUDE.md
+++ b/.claude/CLAUDE.md
@@ -419,8 +419,6 @@ These labels are CHILmesh-specific — they have no equivalent in DomI's canonic
## Coding dispatch — Haiku subagent default
-All coding work (writing or editing source code) MUST be dispatched to a subagent running the Haiku model (`claude-haiku-4-5`) — not written inline by the main session. The orchestrator session plans, reviews, and integrates; implementation is delegated to the Haiku subagent.
+**Binding:** all code writing/editing MUST be dispatched to a Haiku subagent (`model: haiku`); the main session plans/reviews/integrates and verifies subagent output before commit. Non-code work (planning, research, docs, git/PR, review, editing memory) stays on main. Exception only on explicit operator instruction — never assumed.
-- **Default**: for any code-writing/editing task, spawn a subagent with `model: haiku`.
-- **Exception**: only when the operator explicitly directs otherwise (e.g. "do it inline", "use Sonnet/Opus for this"). Explicit operator instruction only — never assumed.
-- **Scope**: applies to code. Non-coding work (planning, research, docs, git/PR orchestration, review) stays on the main session.
+Canonical policy + rationale: DomI [`.claude/policies/coding-dispatch.md`](https://github.com/domattioli/DomI/blob/main/.claude/policies/coding-dispatch.md) (governance authority; #83). This is the binding summary.
From badcbce7c1c9983b0dba3d3d611d34e8f6161e67 Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Fri, 12 Jun 2026 19:51:18 +0000
Subject: [PATCH 07/32] fix: re-sync workflow-conformance managed copy
(dead-branch regex excludes .claude/ paths)
Source: DomI templates/workflows @ development.
https://claude.ai/code/session_01RqYaYzJ1MaWGAzscnyKAeP
---
.github/workflows/workflow-conformance.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/workflow-conformance.yml b/.github/workflows/workflow-conformance.yml
index ef760dd..7414f3f 100644
--- a/.github/workflows/workflow-conformance.yml
+++ b/.github/workflows/workflow-conformance.yml
@@ -28,7 +28,7 @@ jobs:
- name: Lint changed workflows (v2.2 baseline + managed-or-LOCAL.md rule)
run: |
set -e
- DEAD_RE='daily-''maintenance|cla''ude/'
+ DEAD_RE='daily-''maintenance|[^.]cla''ude/'
PATHS_LIT='pat''hs:'
PATHS_IGN_LIT='pat''hs-ignore:'
SETUP_PY_LIT='setup-''python'
From 82ab38df6bd6e9e072aa6b201ae34e8d09505b39 Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Sat, 13 Jun 2026 01:47:07 +0000
Subject: [PATCH 08/32] docs: seed mesh-segmenter architecture ADR + CONTEXT
glossary (#153)
---
.../mesh-segmenter/ADR-0001-architecture.md | 97 +++++++++++++++++++
docs/proposals/mesh-segmenter/CONTEXT.md | 92 ++++++++++++++++++
2 files changed, 189 insertions(+)
create mode 100644 docs/proposals/mesh-segmenter/ADR-0001-architecture.md
create mode 100644 docs/proposals/mesh-segmenter/CONTEXT.md
diff --git a/docs/proposals/mesh-segmenter/ADR-0001-architecture.md b/docs/proposals/mesh-segmenter/ADR-0001-architecture.md
new file mode 100644
index 0000000..21b822e
--- /dev/null
+++ b/docs/proposals/mesh-segmenter/ADR-0001-architecture.md
@@ -0,0 +1,97 @@
+# ADR-0001 — mesh-segmenter: standalone package, chilmesh engine, neutral mask
+
+| Field | Value |
+|---|---|
+| Status | Proposed |
+| Date | 2026-06-13 |
+| Origin | [CHILmesh #153](https://github.com/domattioli/CHILmesh/issues/153) (filed as the `admesh-segmenter` proposal; ADMESH-side as #9) |
+| Method | grill-with-docs design session |
+| Related | ADMESH [`docs/adr/ADR-001-chilmesh-boundary.md`](https://github.com/domattioli/ADMESH/blob/main/docs/adr/ADR-001-chilmesh-boundary.md) |
+
+## Context
+
+The proposal is a composable sub-region **selection** API over a 2D mesh ("SAM2 for
+meshes"): pick elements by layer / distance / click / threshold / polygon, combine
+with set-algebra, usually to identify a sub-region to **re-mesh**. The issue body
+assumed it would live in ADMESH and consume `admesh.Mesh`; a later comment proposed
+"a package within admesh"; another raised a SAM2-style *learned* segmentation.
+
+Two facts from the codebase contradict the admesh framing:
+
+1. **`admesh.Mesh` is adjacency-free** (`frozen`, `slots`; `nodes / elements /
+ boundaries / bathymetry / quality / title`). Every adjacency-walk mechanism the
+ proposal needs (ring expansion, flood-fill, connected components) requires
+ topology ADMESH deliberately does not keep.
+2. **CHILmesh already ships that topology and ~60–70% of the proposed API** —
+ `Edge2Elem`/`Vert2Elem`, `elements_in_layer`, `submesh` (renumbering),
+ `_skeletonize`, `build_spatial_indices`, fort.14 I/O — all built from raw
+ `(nodes, elements)`.
+
+ADMESH's ADR-001 had already classified segmentation as *consumer-side* and
+reaffirmed "heavy consumer-side functionality earns its own package." This ADR
+finishes that thread by naming the package, its engine, and its dependency rules.
+
+## Decision
+
+**A standalone `mesh-segmenter` package whose only topology dependency is
+`chilmesh`. It selects; it never generates.**
+
+1. **Placement — standalone repo `mesh-segmenter`.** Not an ADMESH submodule
+ (would bloat the generator import surface and contradict ADR-001) and not
+ in-tree CHILmesh (keeps shapely / future ML deps out of CHILmesh core).
+
+2. **Engine — chilmesh only.** Accepts `admesh.Mesh` / fort.14 / raw
+ `(nodes, elements)` and builds a `CHILmesh` internally for adjacency, layers,
+ spatial index, and `submesh`. Because CHILmesh constructs from raw arrays, the
+ segmenter is mesh-library-agnostic — justifying the generic name.
+
+3. **No generator dependency.** mesh-segmenter MUST NOT import `admesh` or
+ `quadmesh`. A future "one-stop" **umbrella** package (depends on chilmesh,
+ admesh, quadmesh, valence, *and* mesh-segmenter) owns the `selection → re-mesh`
+ wiring. This keeps the segmenter a leaf and prevents a dependency cycle the
+ moment the umbrella composes them.
+
+4. **Core object — `Selection`, a canonical immutable element mask** bound to a
+ parent mesh. Set-algebra `|` `&` `~` returns new Selections. Nodal signals
+ (bathymetry, curvature) reduce onto elements via a documented rule (default
+ conservative "all vertices in range"; opt-in `any` / `mean`).
+
+5. **Output — neutral artifact at the re-mesh seam.** A Selection emits
+ `element_ids`, `boundary` (raw polygon rings, holes supported), and an optional
+ `submesh`; never an `admesh.Domain` directly. The umbrella wraps rings → Domain
+ for re-gen, or feeds the submesh → quadmesh tri2quad.
+
+6. **Scope — two phases.**
+ - **v1 (deterministic):** `Selection` + set-algebra; `grow` (ring dilation from a
+ seed), `by_distance` (shapely), `by_click` (flood-fill w/ predicate),
+ `by_threshold` (scalar + nodal reduction), `by_polygon`, `by_skeleton_layer`.
+ - **v2 (research spike):** SAM2-*inspired* learned mask-from-size-field
+ ("click in the gulf → the gulf, respecting bathymetry"). Transfer-learning,
+ **not** train-from-scratch; uncertain, explicitly fenced out of v1. Plugs into
+ the same `Selection`. De-risking bridge: v1
+ `by_click(criterion=size_field_gradient)` is already region-growing bounded by
+ the size field — the deterministic MVP of the SAM2 idea.
+
+## Consequences
+
+**Enables**
+- A buildable v1 with no ML risk, immediately useful for the re-mesh pipeline.
+- Clean layering: umbrella → {generators, segmenter}; segmenter → chilmesh.
+- v2 ML work slots in behind a stable `Selection` contract — no API churn.
+
+**Costs / forecloses**
+- A naming correction: the issue's `by_layer` becomes **`grow`**; "Layer" stays
+ reserved for CHILmesh's skeletonization peel (`by_skeleton_layer`). See
+ [`CONTEXT.md`](./CONTEXT.md).
+- The umbrella must own selection→generator glue; the segmenter cannot offer a
+ one-call `.re_mesh()` convenience without breaking the leaf rule.
+- ADMESH ADR-001 should gain a back-reference: the "segmenter sibling" it
+ anticipated is engined by **chilmesh**, not admesh.
+
+## Follow-up
+
+- [ ] Operator: create the `mesh-segmenter` repo; lift this folder in as `docs/adr/`
+ + `CONTEXT.md`.
+- [ ] Confirm package/import name (`mesh_segmenter`? `meshseg`?).
+- [ ] Note in ADMESH ADR-001 that the anticipated sibling is chilmesh-engined.
+- [ ] v2: separate research spec before any training/transfer-learning work.
diff --git a/docs/proposals/mesh-segmenter/CONTEXT.md b/docs/proposals/mesh-segmenter/CONTEXT.md
new file mode 100644
index 0000000..e53bcc2
--- /dev/null
+++ b/docs/proposals/mesh-segmenter/CONTEXT.md
@@ -0,0 +1,92 @@
+# mesh-segmenter Context
+
+Glossary seed for the proposed **mesh-segmenter** package — interactive, composable
+sub-region *selection* over a 2D mesh ("SAM2 for meshes"). It selects; it never
+generates. Lives in CHILmesh `docs/proposals/` until the sibling repo is created,
+then lifts in whole. Decisions recorded in [`ADR-0001-architecture.md`](./ADR-0001-architecture.md).
+
+## Language
+
+**Selection**:
+The canonical object — an immutable *mask over elements* bound to a parent mesh
+(`int64` ids / `bool[n_elements]`). The analogue of a SAM2 mask. Set-algebra
+(`|`, `&`, `~`) returns new Selections; everything else is a derived export.
+_Avoid_: Region, mask (in user-facing API), subset.
+
+**Element**:
+A mesh cell (triangle or quad) — the unit a Selection is over. Canonical entity:
+all mechanisms return element masks; nodal signals reduce onto elements.
+_Avoid_: face, cell, triangle (when quads are also in play).
+
+**Mechanism**:
+A function that produces or refines a Selection — the analogue of a SAM2 prompt.
+v1: `grow`, `by_distance`, `by_click`, `by_threshold`, `by_polygon`,
+`by_skeleton_layer`.
+_Avoid_: selector, filter, prompt (reserve "prompt" for the SAM2 analogy in prose).
+
+**grow**:
+Ring expansion / morphological *dilation* — expand a seed set outward by `n_rings`
+of dual-graph adjacency. This is what the original issue called `by_layer`.
+_Avoid_: by_layer, ring (the issue's collided name — see Flagged ambiguities), expand.
+
+**Reduction rule**:
+How a per-node signal (bathymetry, curvature) collapses onto the canonical element
+mask. Default conservative *"all vertices in range"*; opt-in `any` / `mean`.
+_Avoid_: aggregation, projection.
+
+**Neutral artifact**:
+What a Selection emits at the re-mesh seam, carrying no generator dependency:
+`element_ids`, `boundary` (raw polygon rings, holes supported), and an optional
+`submesh`. The umbrella — not the segmenter — wraps rings into an `admesh.Domain`
+or feeds the submesh to quadmesh.
+_Avoid_: output, result.
+
+**Engine**:
+The topology provider. mesh-segmenter depends on **chilmesh** only — adjacency
+(`Edge2Elem`, `Vert2Elem`), `elements_in_layer`, `submesh`, spatial indices — built
+from raw `(nodes, elements)`. Never depends on a *generator* (admesh / quadmesh).
+_Avoid_: backend (reserve for chilmesh's C++/Rust compute backends).
+
+**Umbrella**:
+The proposed future "one-stop" mesh package that depends on chilmesh, admesh,
+quadmesh, valence **and** mesh-segmenter, and wires `selection → re-mesh`. It owns
+the selection→generator handoff; the segmenter stays a leaf.
+_Avoid_: orchestrator, pipeline, one-stop (informal only).
+
+## Flagged ambiguities
+
+- **`Layer` is reserved.** CHILmesh `CONTEXT.md` binds **Layer** = a medial-axis
+ *skeletonization peel* (OE/IE/OV/IV, global, inward). The issue's
+ `by_layer(ring=…, n_layers=N)` meant *ring expansion outward from a seed* —
+ a different operation. Resolution: that operation is **`grow`** (dilation);
+ CHILmesh's true peels are exposed separately as **`by_skeleton_layer(idx)`**.
+ Never let "layer" name the ring-expansion mechanism.
+
+- **`Mesh` is overloaded** (inherited from CHILmesh/ADMESH-Domains). Here a
+ Selection's *parent mesh* is a runtime topology object (a `CHILmesh`). The thin
+ `admesh.Mesh` wire dataclass is an *input* that gets built into a `CHILmesh` for
+ adjacency. Say "parent mesh" for the runtime object.
+
+- **`node` vs `Element`.** fort.14 / ADCIRC say "node"; a Selection is over
+ *elements*. Nodal fields exist (bathymetry per node) but never form the mask
+ directly — they reduce. Keep the I/O word ("node") out of the selection API.
+
+## Example dialogue
+
+> **Dev:** "User clicks in the Gulf of Mexico — do we return the nodes or the
+> triangles inside?"
+> **Domain expert:** "Elements. A Selection is always an element mask. The click is
+> a *mechanism* — `by_click` flood-fills connected elements until a predicate stops
+> it."
+> **Dev:** "But bathymetry is per node. How does 'shallower than 2 m' become elements?"
+> **Domain expert:** "Through the *reduction rule*. Default: an element is in only if
+> *all* its vertices are under 2 m. So `by_threshold` reads the nodal field, reduces,
+> and hands back an element Selection — same type as every other mechanism."
+> **Dev:** "Then the user wants to re-mesh that. We call `admesh.triangulate`?"
+> **Domain expert:** "Not from in here. The Selection emits a *neutral artifact* — the
+> boundary rings. The umbrella turns rings into an `admesh.Domain` and re-meshes. The
+> segmenter never imports a generator; that's how it stays a chilmesh-only leaf."
+> **Dev:** "And expanding three rings off the coastline — that's `by_layer`?"
+> **Domain expert:** "Call it `grow`. 'Layer' is CHILmesh's skeletonization peel —
+> different thing. `grow(seed, n_rings=3)` is dilation. The peels are
+> `by_skeleton_layer`."
From 7b4241e8d623f442d4f556cbb4425c403936c0de Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Sat, 13 Jun 2026 02:08:52 +0000
Subject: [PATCH 09/32] docs: add mesh-segmenter v1 mechanism contracts from
grill round 2 (#153)
---
.../mesh-segmenter/ADR-0001-architecture.md | 35 +++++++++++++++++++
docs/proposals/mesh-segmenter/CONTEXT.md | 35 +++++++++++++++++--
2 files changed, 67 insertions(+), 3 deletions(-)
diff --git a/docs/proposals/mesh-segmenter/ADR-0001-architecture.md b/docs/proposals/mesh-segmenter/ADR-0001-architecture.md
index 21b822e..16421cc 100644
--- a/docs/proposals/mesh-segmenter/ADR-0001-architecture.md
+++ b/docs/proposals/mesh-segmenter/ADR-0001-architecture.md
@@ -72,6 +72,41 @@ finishes that thread by naming the package, its engine, and its dependency rules
`by_click(criterion=size_field_gradient)` is already region-growing bounded by
the size field — the deterministic MVP of the SAM2 idea.
+### v1 mechanism contracts (grill round 2)
+
+7. **Adjacency is a per-call kwarg, edge default.** Every dual-graph mechanism
+ (`grow`, `by_click`, components, boundary walk) takes `connectivity="edge"|
+ "vertex"`, defaulting to `"edge"` (`Edge2Elem`). Edge avoids corner-bleed at
+ pinch points / narrow inlets — the right default for click-selection in
+ estuaries. `"vertex"` (`Vert2Elem`) is opt-in for wider dilation. **Component
+ connectivity and `Selection.boundary` perimeters are edge-only** — a
+ vertex-connected selection can have a non-manifold perimeter, so the boundary
+ contract is defined over edge-components regardless of how the mask was grown.
+
+8. **`Selection.boundary` is per-component, never auto-cleaned.** Returns a list of
+ components, each `(outer_ring, holes[])` as numpy rings. A `Selection` may be
+ multi-component (a depth threshold spanning two basins) — surfaced, not hidden.
+ `Selection.components()` yields per-component sub-Selections so the umbrella can
+ re-mesh patches individually. Pinch-touching rings are *flagged* (warning), never
+ silently merged or morphologically closed — a mask must not self-edit (SAM2
+ fidelity).
+
+9. **`by_click` criterion = an edge-crossing predicate.** Canonical form
+ `fn(from_elem, to_elem) -> bool` (`True` = stop, don't cross). Both-sided, so it
+ expresses gradients / jumps / BC-changes (the gulf shelf-break case). Named
+ shortcuts wrap it: `"connected"`, `("field_jump", field, delta)`, or any
+ callable. The v2 learned model is just another crossing predicate — no API churn.
+
+10. **Fields are plain arrays; "all information" flows in without an import.** A
+ field is a numpy array of length `n_nodes` or `n_elements` (auto-detected; nodal
+ reduces per item 4). Three sources: mesh-attached (bathymetry),
+ chilmesh-computed (quality / edge-length / area), and **umbrella-supplied admesh
+ size-field components**. This reconciles "use all information available to us
+ including admesh and chilmesh" with the leaf rule (item 3): the segmenter
+ consumes admesh-derived signal **as an array passed by the umbrella**, never by
+ importing admesh. Should a future need require the segmenter to compute admesh
+ size-fields itself, that overrides item 3 and must amend this ADR.
+
## Consequences
**Enables**
diff --git a/docs/proposals/mesh-segmenter/CONTEXT.md b/docs/proposals/mesh-segmenter/CONTEXT.md
index e53bcc2..955310a 100644
--- a/docs/proposals/mesh-segmenter/CONTEXT.md
+++ b/docs/proposals/mesh-segmenter/CONTEXT.md
@@ -26,12 +26,41 @@ _Avoid_: selector, filter, prompt (reserve "prompt" for the SAM2 analogy in pros
**grow**:
Ring expansion / morphological *dilation* — expand a seed set outward by `n_rings`
-of dual-graph adjacency. This is what the original issue called `by_layer`.
+of dual-graph adjacency (mode set by **Adjacency mode**). This is what the original
+issue called `by_layer`.
_Avoid_: by_layer, ring (the issue's collided name — see Flagged ambiguities), expand.
+**Adjacency mode**:
+What "neighboring elements" means for a dual-graph op — `"edge"` (share an edge,
+`Edge2Elem`) or `"vertex"` (share ≥1 vertex, `Vert2Elem`). A per-call `connectivity=`
+kwarg on every mechanism; **default `"edge"`** (no corner-bleed at pinch points).
+`"vertex"` grows wider per ring and bleeds through one-vertex touches — opt-in only.
+_Avoid_: connectivity (in prose), 4/8-connectivity (image-domain term).
+
+**Component**:
+A maximal **edge-connected** subset of a Selection. A Selection may hold several
+(e.g. a `by_threshold` depth mask spanning two basins). `Selection.components()`
+yields one sub-Selection per component. Perimeter walks are per-component.
+_Avoid_: island, blob, region.
+
+**Crossing predicate**:
+The formal `by_click` stop-criterion: `fn(from_elem, to_elem) -> bool`, where `True`
+means *stop* (don't expand across that edge). Expresses jumps / gradients / BC-changes
+(needs both sides). Named shortcuts wrap it (`"connected"`, `("field_jump", field,
+delta)`). The v2 learned model slots in as just another crossing predicate.
+_Avoid_: criterion (alone), stopping function, mask predicate.
+
+**Field**:
+A plain numpy array carrying a per-entity scalar — length `n_nodes` or `n_elements`
+(auto-detected; nodal reduces via **Reduction rule**). Sources: mesh-attached
+(bathymetry), chilmesh-computed (quality / edge-length / area), or passed in by the
+**Umbrella** (admesh size-field components). Arrays are the lingua franca — "all
+information available" reaches the segmenter as a Field, never as an admesh import.
+_Avoid_: signal, channel, feature (reserve for v2 ML).
+
**Reduction rule**:
-How a per-node signal (bathymetry, curvature) collapses onto the canonical element
-mask. Default conservative *"all vertices in range"*; opt-in `any` / `mean`.
+How a per-node **Field** collapses onto the canonical element mask. Default
+conservative *"all vertices in range"*; opt-in `any` / `mean`.
_Avoid_: aggregation, projection.
**Neutral artifact**:
From a6cf11950e72886ab02373587824d71d6b1c8f9c Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Sat, 13 Jun 2026 02:19:21 +0000
Subject: [PATCH 10/32] docs: set provisional name mesh_segmenter, drop ADMESH
ADR back-ref (#153)
---
docs/proposals/mesh-segmenter/ADR-0001-architecture.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/docs/proposals/mesh-segmenter/ADR-0001-architecture.md b/docs/proposals/mesh-segmenter/ADR-0001-architecture.md
index 16421cc..d406ac7 100644
--- a/docs/proposals/mesh-segmenter/ADR-0001-architecture.md
+++ b/docs/proposals/mesh-segmenter/ADR-0001-architecture.md
@@ -127,6 +127,7 @@ finishes that thread by naming the package, its engine, and its dependency rules
- [ ] Operator: create the `mesh-segmenter` repo; lift this folder in as `docs/adr/`
+ `CONTEXT.md`.
-- [ ] Confirm package/import name (`mesh_segmenter`? `meshseg`?).
-- [ ] Note in ADMESH ADR-001 that the anticipated sibling is chilmesh-engined.
+- [x] Package/import name — **provisional `mesh_segmenter`** (operator, 2026-06-13);
+ a cooler final name is wanted before first publish. Low priority now.
+- [x] ~~Back-reference in ADMESH ADR-001~~ — declined by operator (2026-06-13).
- [ ] v2: separate research spec before any training/transfer-learning work.
From df97f1876601abef1bb3fcb179728e101d14b526 Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Sat, 13 Jun 2026 02:33:48 +0000
Subject: [PATCH 11/32] chore: sync DomI@3e46639
---
.domi-pin | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.domi-pin b/.domi-pin
index 8c42cf1..5e6bb8d 100644
--- a/.domi-pin
+++ b/.domi-pin
@@ -4,6 +4,6 @@
upstream: domattioli/DomI
branch: main
-sha: 39fd74a233f921b594c541dcbe225f33a34f206d
-manifest_sha256: 4f69f9615bcde59382f66c94946a177f9943d7924744ad0a4f55546f3adadfad
-pinned_at: 2026-06-11T20:06:14Z
+sha: 3e46639d47875c879a275120be19f4fae9222aad
+manifest_sha256: 1877de76b95155d967ca7d09cf34a08d5b00f1d29182716ebd015f9bb83d4093
+pinned_at: 2026-06-13T02:26:24Z
From fef9cf90199b627061ece5f2e40ceca8423c2212 Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Sat, 13 Jun 2026 02:33:54 +0000
Subject: [PATCH 12/32] fix: honest backend detection + non-silent pure-Python
slow-path warning (#202)
backend_info() already rejects an importable-but-empty namespace stub
(#163 guard); add regression tests that pin this under the stub failure
mode so it cannot silently regress. Emit a one-time UserWarning when a
large mesh (>=2000 elems) is skeletonized on the pure-Python backend
with no compiled C++/Rust extension present, making the source-install
perf cliff non-silent. Document source/editable-install behavior in the
README Backends section.
---
README.md | 2 +
src/chilmesh/CHILmesh.py | 24 ++++++++++
tests/test_backend_info.py | 91 ++++++++++++++++++++++++++++++++++++++
3 files changed, 117 insertions(+)
diff --git a/README.md b/README.md
index d65b8ae..7fcd599 100644
--- a/README.md
+++ b/README.md
@@ -153,6 +153,8 @@ chilmesh.backend_info()
Force a specific backend with `CHILMESH_BACKEND` (`python` or `cpp`). When unset, the fastest available is picked. Pre-built binary wheels (`manylinux` / `macOS` / `Windows`) via `cibuildwheel` are planned — see [`docs/`](docs/) for build-from-source instructions.
+> **Source / editable installs run pure-Python.** `pip install -e .` (or installing the `chilmesh` sibling checkout as a downstream hard dep) ships **no compiled C++/Rust extension** unless you build it explicitly (`pip install ./src/chilmesh_cpp`). `backend_info()` reports honestly in that case (`selected: 'python'`, no `cpp`/`rust` in `available`) — an importable-but-empty namespace stub is **not** counted as available ([#163](https://github.com/domattioli/CHILmesh/issues/163)). Skeletonizing a large mesh on the pure-Python path is dramatically slower (Block_O ~5k elems can exceed 200s); CHILmesh emits a one-time `UserWarning` pointing here when that happens. Pass `compute_layers=False` for fast metadata-only loading ([#202](https://github.com/domattioli/CHILmesh/issues/202)).
+
### Examples
```bash
diff --git a/src/chilmesh/CHILmesh.py b/src/chilmesh/CHILmesh.py
index a162729..10018dd 100644
--- a/src/chilmesh/CHILmesh.py
+++ b/src/chilmesh/CHILmesh.py
@@ -16,6 +16,12 @@
__all__ = ['CHILmesh', 'write_fort14']
+# One-time warn state for the source-install pure-Python perf cliff (#202).
+# When no compiled C++/Rust backend is present, skeletonizing a large mesh on the
+# pure-Python path is dramatically slow (Block_O ~5k elems > 200s). Warn once.
+_SLOW_PATH_WARNED = False
+_SLOW_PATH_ELEM_THRESHOLD = 2000
+
class CHILmesh(CHILmeshPlotMixin):
"""
A 2D mesh class supporting triangular, quadrilateral, and mixed-element meshes.
@@ -364,6 +370,24 @@ def _initialize_mesh( self, compute_layers: bool = True, compute_adjacencies: bo
if compute_adjacencies:
self._build_adjacencies( validate=validate )
if compute_layers:
+ global _SLOW_PATH_WARNED
+ if (not _SLOW_PATH_WARNED
+ and self.n_elems >= _SLOW_PATH_ELEM_THRESHOLD):
+ from .backends.cpp_backend import CPP_AVAILABLE
+ from .backends.rust_backend import RUST_AVAILABLE
+ if not (CPP_AVAILABLE or RUST_AVAILABLE):
+ _SLOW_PATH_WARNED = True
+ warnings.warn(
+ f"chilmesh: skeletonizing a {self.n_elems}-element mesh "
+ "on the pure-Python backend (no compiled C++/Rust extension "
+ "found). This is dramatically slower than the compiled path "
+ "(e.g. Block_O ~5k elems can exceed 200s). Build the extension "
+ "(pip install ./src/chilmesh_cpp) or pass compute_layers=False "
+ "for fast metadata-only loading. Introspect with "
+ "chilmesh.backend_info(). See CHILmesh #202.",
+ UserWarning,
+ stacklevel=2,
+ )
self._skeletonize(
seed_boundary_kinds=seed_boundary_kinds,
seed_ibtypes=seed_ibtypes,
diff --git a/tests/test_backend_info.py b/tests/test_backend_info.py
index 52d2186..b4ef441 100644
--- a/tests/test_backend_info.py
+++ b/tests/test_backend_info.py
@@ -90,3 +90,94 @@ def test_rust_placement_when_available():
if CPP_AVAILABLE:
cpp_idx = info["available"].index("cpp")
assert cpp_idx < rust_idx, "cpp should come before rust"
+
+
+import importlib
+import sys
+import types
+
+import numpy as np
+import pytest
+
+
+def test_namespace_stub_reports_cpp_unavailable():
+ """#163/#202: importable-but-API-less chilmesh_cpp must report unavailable."""
+ import chilmesh.backends.cpp_backend as cpp_backend
+ stub = types.ModuleType("chilmesh_cpp") # no full_init attribute
+ saved = sys.modules.get("chilmesh_cpp")
+ sys.modules["chilmesh_cpp"] = stub
+ try:
+ reloaded = importlib.reload(cpp_backend)
+ assert reloaded.CPP_AVAILABLE is False
+ assert reloaded._cpp is None
+ finally:
+ if saved is not None:
+ sys.modules["chilmesh_cpp"] = saved
+ else:
+ sys.modules.pop("chilmesh_cpp", None)
+ importlib.reload(cpp_backend) # restore real detection
+
+
+def test_namespace_stub_reports_rust_unavailable():
+ """#163/#202: importable-but-API-less chilmesh_core must report unavailable."""
+ import chilmesh.backends.rust_backend as rust_backend
+ stub = types.ModuleType("chilmesh_core") # no RustMesh attribute
+ saved = sys.modules.get("chilmesh_core")
+ sys.modules["chilmesh_core"] = stub
+ try:
+ reloaded = importlib.reload(rust_backend)
+ assert reloaded.RUST_AVAILABLE is False
+ assert reloaded._rust is None
+ finally:
+ if saved is not None:
+ sys.modules["chilmesh_core"] = saved
+ else:
+ sys.modules.pop("chilmesh_core", None)
+ importlib.reload(rust_backend) # restore real detection
+
+
+def test_stub_with_api_reports_cpp_available():
+ """Guard is not over-eager: a module exposing full_init reports available."""
+ import chilmesh.backends.cpp_backend as cpp_backend
+ stub = types.ModuleType("chilmesh_cpp")
+ stub.full_init = lambda *a, **k: None
+ saved = sys.modules.get("chilmesh_cpp")
+ sys.modules["chilmesh_cpp"] = stub
+ try:
+ reloaded = importlib.reload(cpp_backend)
+ assert reloaded.CPP_AVAILABLE is True
+ finally:
+ if saved is not None:
+ sys.modules["chilmesh_cpp"] = saved
+ else:
+ sys.modules.pop("chilmesh_cpp", None)
+ importlib.reload(cpp_backend) # restore real detection
+
+
+def test_slow_path_warning_when_pure_python(monkeypatch):
+ """#202: pure-Python skeletonization of a large mesh warns once (non-silent)."""
+ if CPP_AVAILABLE or RUST_AVAILABLE:
+ pytest.skip("compiled fast backend present; slow-path warning N/A")
+ import chilmesh.CHILmesh # ensure submodule imported
+ cm = sys.modules["chilmesh.CHILmesh"]
+ monkeypatch.setattr(cm, "_SLOW_PATH_ELEM_THRESHOLD", 1)
+ monkeypatch.setattr(cm, "_SLOW_PATH_WARNED", False)
+ pts = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]])
+ conn = np.array([[0, 1, 2], [0, 2, 3]])
+ with pytest.warns(UserWarning, match="pure-Python backend"):
+ cm.CHILmesh(connectivity=conn, points=pts, compute_layers=True)
+
+
+def test_no_slow_path_warning_below_threshold(monkeypatch, recwarn):
+ """Small meshes never trigger the slow-path warning."""
+ if CPP_AVAILABLE or RUST_AVAILABLE:
+ pytest.skip("compiled fast backend present; slow-path warning N/A")
+ import chilmesh.CHILmesh # ensure submodule imported
+ cm = sys.modules["chilmesh.CHILmesh"]
+ monkeypatch.setattr(cm, "_SLOW_PATH_ELEM_THRESHOLD", 10_000)
+ monkeypatch.setattr(cm, "_SLOW_PATH_WARNED", False)
+ pts = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]])
+ conn = np.array([[0, 1, 2], [0, 2, 3]])
+ cm.CHILmesh(connectivity=conn, points=pts, compute_layers=True)
+ slow = [w for w in recwarn.list if "pure-Python backend" in str(w.message)]
+ assert slow == []
From 4eda66ac2c5a08096bd52e9117fdba322aa71af3 Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Sat, 13 Jun 2026 02:36:48 +0000
Subject: [PATCH 13/32] docs: introspection corpus entry for rotation
2026-06-13T02Z
---
docs/introspections/development_fef9cf9.md | 57 ++++++++++++++++++++++
1 file changed, 57 insertions(+)
create mode 100644 docs/introspections/development_fef9cf9.md
diff --git a/docs/introspections/development_fef9cf9.md b/docs/introspections/development_fef9cf9.md
new file mode 100644
index 0000000..c01c444
--- /dev/null
+++ b/docs/introspections/development_fef9cf9.md
@@ -0,0 +1,57 @@
+
+---
+date: 2026-06-13
+session: 2026-06-13T02Z-rotation
+repo: domattioli/CHILmesh
+severity: low
+freq: recurring
+issues: [202, 209]
+wasted_min: 3
+wasted_tok: 2000
+missing_skill: null
+---
+
+# Session Handoff — CHILmesh · development_fef9cf9 · 2026-06-13 (rotation hour-02)
+
+**Task:** rotation maintenance track — C spec-048 slice already shipped 06-11, so issue-queue work
+**Phase:** maintenance
+**Progress:** complete — pin synced, #202 partial fix shipped + regression-locked
+**Branch:** development (rolling PR #210)
+**Duration:** ~25 min
+**Tool failures:** 0
+**Outcome:** complete
+
+## Pre-flight
+
+- branch_policy_conflict: caught_and_resolved
+- domi_pin_drift: caught_and_synced
+- caveman_plugin: loaded → /caveman:caveman ultra Skill call succeeded (SessionStart resume hook active)
+
+## What shipped (evidence)
+
+1. `df97f18` chore: sync DomI — `.domi-pin` 39fd74a → 3e46639 (main HEAD), MANIFEST sha256 verified; CHILmesh#209 closed.
+2. `fef9cf9` fix #202 (partial) — namespace-stub regression tests pin the #163 backend-honesty guard (was untested); one-time `UserWarning` makes the pure-Python source-install perf cliff non-silent; README Backends section documents source/editable behavior.
+3. Gate: `pytest tests/ -k "not block_o"` → 989 passed / 44 skipped, 0 regressions; `test_backend_info.py` 12/12.
+
+## Key decisions
+
+1. Picked #202 over priority:now #155 (lifecycle benchmark): #155 is OOM/timeout-bound at scale (env-bound, in-progress); #202 = bounded, verifiable, serves #48 unification (MADMESHing's only hard dep silently ran 200s slow).
+2. Did NOT close #202 — problem-1 (misreport) + problem-2-silent fixed, but problem-2c (pure-Python skeletonize >200s hot loop) unchanged. Closing would misrepresent: MADMESHing's `MADMESHING_RUN_BLOCK_O=1` gate-skip still needed. Left open scoped to 2c.
+3. Threshold 2000 elems for slow-path warning: above standard small fixtures, below Block_O (5214) → warns exactly where it hurts, silent for normal use.
+
+## What worked (top 3)
+
+1. Empirical repro first (fresh editable install → backend_info()) found problem-1 already fixed by #163 guard → reframed deliverable from "fix" to "regression-lock + non-silent", avoided redundant work.
+2. Sibling-clone pin sync (#230/#223) — 2 tool calls, no gh.
+3. Haiku builder for the 2-file edit; Fable review caught the failing run, dispatched the shadowing fix.
+
+## What didn't (pains → routing)
+
+1. **Module-name shadowing footgun.** `import chilmesh.CHILmesh as cm` binds the *class* not the submodule (package `__init__` does `from .CHILmesh import CHILmesh`, shadowing the same-named submodule attr). First Haiku test draft failed AttributeError on monkeypatch; fix = `cm = sys.modules["chilmesh.CHILmesh"]`. Cost ~3 min + 1 Haiku round-trip. Recurring repo-layout hazard (class and module share name). Severity low. Route: lesson/doc, NOT a skill (#203 probation). Mitigation for future sessions: when monkeypatching CHILmesh.py module globals in tests, grab the module via `sys.modules`, never `import ... as`.
+2. **block_o pure-Python cliff blocks cheap full-suite proof.** Any full-suite run must `-k "not block_o"` or hang >200s. This IS #202-2c — already tracked, no new routing.
+
+## Next steps
+
+- #202-2c: optimize pure-Python skeletonize hot loop (the residual) OR 2a build-on-install (#163) — either lets block_o run in the gate.
+- Spec-048 ecosystem remainder: M-T4 (MADMESHing quality.py benchmark → D2), M slot only.
+- #201 (.chil format) brainstorm still open; hub did a RESHAPE review 06-11.
From 363d1bc61b7e151378e30be8187b0e3915ee0ffa Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Sat, 13 Jun 2026 02:41:15 +0000
Subject: [PATCH 14/32] docs: add mesh_segmenter sandbox handoff +
rasterize-SAM2 prototype plan (#153)
---
docs/proposals/mesh-segmenter/HANDOFF.md | 114 ++++++++++++++++++
.../PROTOTYPE-rasterize-sam2.md | 99 +++++++++++++++
2 files changed, 213 insertions(+)
create mode 100644 docs/proposals/mesh-segmenter/HANDOFF.md
create mode 100644 docs/proposals/mesh-segmenter/PROTOTYPE-rasterize-sam2.md
diff --git a/docs/proposals/mesh-segmenter/HANDOFF.md b/docs/proposals/mesh-segmenter/HANDOFF.md
new file mode 100644
index 0000000..543f263
--- /dev/null
+++ b/docs/proposals/mesh-segmenter/HANDOFF.md
@@ -0,0 +1,114 @@
+# Handoff — mesh_segmenter sandbox session
+
+For a fresh session that boots **inside the new `mesh_segmenter` repo** to prototype.
+Everything here was designed in two grill-with-docs sessions on CHILmesh #153; this
+folder (`docs/proposals/mesh-segmenter/`) is the seed — **lift it whole into the new
+repo as `docs/`** (ADR → `docs/adr/`, the rest alongside).
+
+## 0. Mission (one line)
+
+`mesh_segmenter` = interactive, composable sub-region **selection** over a 2D mesh
+("SAM2 for meshes"). **It selects; it never generates.**
+
+## 1. Locked decisions (full rationale: `ADR-0001-architecture.md`)
+
+- **Standalone repo**, provisional import name `mesh_segmenter` (a cooler name wanted
+ before first publish — not urgent).
+- **Engine = `chilmesh` ONLY.** Accept `admesh.Mesh` / fort.14 / raw `(nodes,
+ elements)` → build a `CHILmesh` internally for adjacency, layers, spatial index,
+ `submesh`. Because CHILmesh builds from raw arrays, the segmenter is
+ mesh-library-agnostic.
+- **Never import a generator** (`admesh` / `quadmesh`). A future "one-stop umbrella"
+ (chilmesh + admesh + quadmesh + valence + mesh_segmenter) owns the
+ `selection → re-mesh` wiring. Keeps the segmenter a leaf, avoids a dependency cycle.
+- **Core object = `Selection`** — an immutable **element mask** bound to a parent
+ mesh. Set-algebra `|` `&` `~` returns new Selections.
+- **Exports are lazy/derived** — `element_ids`, `boundary` (raw polygon rings), an
+ optional `submesh`. Never emit an `admesh.Domain`; the umbrella wraps rings.
+
+## 2. v1 mechanism contracts (grill round 2)
+
+- **Adjacency** = per-call `connectivity="edge"|"vertex"`, default `"edge"` (no
+ corner-bleed at pinch points). Components + `Selection.boundary` perimeters are
+ **edge-only** regardless of how the mask was grown.
+- **`Selection.boundary`** = per-component list of `(outer_ring, holes[])`; never
+ auto-cleaned. `Selection.components()` yields per-component sub-Selections. Pinch
+ rings flagged (warning), never silently merged — a mask must not self-edit.
+- **`by_click` criterion** = edge-crossing predicate `fn(from_elem, to_elem) -> bool`
+ (`True` = stop). Named shortcuts (`"connected"`, `("field_jump", field, delta)`)
+ wrap it. v2 learned model is just another crossing predicate.
+- **Fields** = plain numpy arrays (`n_nodes` | `n_elements`, auto-detect, nodal
+ reduces via "all verts in range" default). Sources: mesh-attached, chilmesh-computed
+ (quality / edge-length / area), umbrella-supplied admesh size-field components.
+ "All information" reaches the segmenter as an array — never an admesh import.
+
+## 3. v1 surface to build
+
+`Selection` + set-algebra; mechanisms `grow` (ring dilation), `by_distance` (shapely),
+`by_click` (flood-fill + predicate), `by_threshold` (scalar + reduction), `by_polygon`,
+`by_skeleton_layer` (exposes CHILmesh peels). Naming note: the issue's `by_layer` is
+**`grow`**; "Layer" stays CHILmesh's skeletonization peel.
+
+## 4. FIRST TASK this session — the prototype, not v1
+
+Before committing to v2, run the **rasterize → SAM2 bootstrap prototype** in
+`PROTOTYPE-rasterize-sam2.md`. It decides (no training) whether SAM2 beats the cheap
+deterministic baseline. Build order:
+
+1. **P0 (CPU, no model)** — `prototype/raster.py` + `project.py` + synthetic 2-basin
+ `fixtures.py` + `test_roundtrip_recall`. Watershed stub = baseline IoU.
+2. Eyeball numbers, then **P1** (real SAM2 via `huggingface_hub`), then **P2** gate.
+
+Gate: `SAM2 IoU > baseline IoU` AND IoU ≥ 0.70 on ≥ 2 cases AND jitter IoU ≥ 0.80. Miss
+any → drop SAM2, ship deterministic `by_click(field_gradient)`.
+
+Keep prototype under `prototype/` with its own `[proto]` extra — throwaway, not the
+shipped API, never in chilmesh.
+
+## 5. Repo setup (cold start)
+
+```bash
+# in the new mesh_segmenter repo root
+python -m venv .venv && . .venv/bin/activate
+pip install -e ../CHILmesh # engine, editable from sibling checkout
+pip install -e ".[dev,proto]" # numpy scipy scikit-image (+ torch sam2 hf for P1)
+# package skeleton: mesh_segmenter/{__init__,selection,mechanisms/}.py
+# prototype lives in prototype/ (see PROTOTYPE-rasterize-sam2.md file map)
+```
+
+CHILmesh entry points the engine gives you for free: `CHILmesh(connectivity=elements,
+points=nodes, build_spatial_indices=True)`, `.elements_in_layer(i)`, `.submesh(ids)`,
+`Edge2Elem` (−1 = boundary), `Vert2Elem`, fort.14 read/write, `from_admesh_domain`.
+
+## 6. Hard truth to keep in view
+
+The pipeline's real bottleneck is **NOT segmentation** — it's **conforming re-stitch**
+(re-inserting a re-meshed sub-region into the parent mesh with matched boundary nodes,
+no T-junctions). That lives in the **umbrella**, not here. The segmenter only owes a
+clean boundary ring. Don't over-invest in segmentation polish until the re-stitch path
+is proven viable elsewhere.
+
+## 7. Open questions
+
+- Cooler final package name (provisional `mesh_segmenter`).
+- Real demand beyond the operator — coastal "select shallow elements" plausible but
+ unvalidated. v1 is cheap enough that this doesn't gate building it.
+- Whether the umbrella repo exists yet — segmenter's standalone value is limited until
+ it does.
+
+## 8. Refs
+
+- CHILmesh #153 — issue + both grill comments (round 1 architecture, round 2
+ contracts).
+- `ADR-0001-architecture.md`, `CONTEXT.md`, `PROTOTYPE-rasterize-sam2.md` (this
+ folder).
+- ADMESH `docs/adr/ADR-001-chilmesh-boundary.md` — the consumer-side / sibling-package
+ precedent (segmenter is chilmesh-engined; no back-ref added per operator).
+- DomI #268 — skill-load recurrence (why a session may run a DomI skill via SKILL.md
+ emulation instead of the registered Skill).
+
+## 9. Conventions in the new repo
+
+- Branch: work on `development`, draft PR `development → main` (mirror CHILmesh policy).
+- Coding dispatch: code → Haiku subagent; main session plans/reviews/integrates.
+- Caveman mode active for orchestrator/technical exchange.
diff --git a/docs/proposals/mesh-segmenter/PROTOTYPE-rasterize-sam2.md b/docs/proposals/mesh-segmenter/PROTOTYPE-rasterize-sam2.md
new file mode 100644
index 0000000..833b39f
--- /dev/null
+++ b/docs/proposals/mesh-segmenter/PROTOTYPE-rasterize-sam2.md
@@ -0,0 +1,99 @@
+# Prototype Plan — rasterize → SAM2 field bootstrap
+
+Goal: decide if SAM2 on a rasterized mesh field is worth building, with **zero model
+training**. The prototype answers one question: **does SAM2 beat the cheap
+deterministic baseline on element-IoU?** If no → drop SAM2, ship deterministic
+`by_click(field_gradient)`.
+
+This is throwaway/sandbox code. It does NOT belong in `mesh_segmenter`'s shipped API
+or in chilmesh — keep it under `prototype/` with its own optional `[proto]` extra.
+
+## Hypothesis
+
+Rasterize a mesh scalar field → image → SAM2 point-click → mask → project back onto
+elements = a usable `Selection`, no training.
+
+## Pipeline
+
+```
+mesh + field ──rasterize──► field_raster (HxW, multi-channel) ──► SAM2(click) ──► mask_raster (HxW bool)
+mesh ──rasterize──► elemid_raster (HxW int label) ──project────────► Selection (element ids)
+```
+
+- **field_raster** — one channel per signal (bathymetry / curvature / size-fn),
+ normalized to uint8, fed to SAM2 as a pseudo-image.
+- **elemid_raster** — paint each element polygon with its element id
+ (`skimage.draw.polygon`) on the SAME grid. Gives exact back-projection, no
+ centroid-sampling loss.
+- **project** — element selected iff ≥ 50% of its pixels fall inside the SAM2 mask.
+- **click map** — mesh `(x, y)` → pixel via a bbox affine transform.
+
+## Phases
+
+- **P0 — CPU, no model.** Build rasterize + elemid-label + back-project. Stub model =
+ `skimage` watershed / flood-fill from the click on `field_raster`. Proves plumbing
+ and sets the **deterministic baseline IoU**. No GPU, no checkpoint.
+- **P1 — real SAM2.** Swap stub → SAM2 image-predictor with a point prompt. Checkpoint
+ via `huggingface_hub`. Same metrics.
+- **P2 — decision.** Resolution sweep + click-jitter robustness + field-channel
+ ablation → apply the gate.
+
+## Fixtures
+
+- **Synthetic 2-basin (primary)** — deform a structured grid + analytic depth (two
+ gaussians). Ground-truth region = a known basin → exact IoU target. Fully
+ controllable; build this first.
+- chilmesh `annulus` / `donut` — plumbing sanity only.
+- One real (WNAT + bathymetry) if reachable; skip on 403.
+
+## Tests / metrics
+
+| test | assertion |
+|---|---|
+| `test_roundtrip_recall` | paint → mask(ALL) → project recovers ≥ 0.95 of elements @ 512 (res adequacy) |
+| `test_iou_vs_gt` | SAM2 click-in-basin: IoU(pred, true_basin) ≥ 0.70 on ≥ 2 cases |
+| `test_sam2_beats_baseline` | IoU(SAM2) > IoU(P0 watershed) — **the real question** |
+| `test_jitter_robust` | click ± 8 px, pairwise IoU ≥ 0.80 |
+| `test_res_sweep` | IoU vs {256, 512, 1024} → min stable res |
+| `test_channel_ablation` | bathy vs + curvature vs + size-fn → does an extra channel help |
+
+## Success gate
+
+All true → v2 rasterize-SAM2 demo is viable:
+
+- roundtrip recall ≥ 0.95 @ 512
+- SAM2 IoU ≥ 0.70 on ≥ 2 cases
+- jitter IoU ≥ 0.80
+- **SAM2 IoU > baseline IoU** — if this fails, SAM2 adds nothing; ship deterministic
+ region-grow and kill the SAM2 track.
+
+## Files
+
+```
+prototype/
+ raster.py mesh → field_raster + elemid_raster
+ project.py mask_raster → element Selection
+ model_stub.py P0 watershed baseline (same interface as SAM2 wrapper)
+ model_sam2.py P1 SAM2 wrapper (HF checkpoint)
+ fixtures.py synthetic 2-basin generator + ground-truth
+ run_prototype.py CLI: mesh + click → Selection + metrics
+ tests/test_*.py the metrics above
+ REPORT.md IoU table per phase → gate verdict
+```
+
+## Deps (isolated extra `[proto]`)
+
+`numpy scipy scikit-image` (P0) · `torch sam2 huggingface_hub` (P1). SAM2-tiny on CPU
+is slow but demo-fine.
+
+## Risks (each caught by a test)
+
+- Tiny coastal elements < 1 pixel → resolution floor (`test_res_sweep`).
+- Scalar-field raster is out-of-distribution vs SAM2's natural-image training → may
+ segment garbage. **This is the core risk; `test_iou_vs_gt` answers it.**
+- Click → pixel off-by-one (`test_jitter_robust` + roundtrip).
+
+## Start order
+
+P0 first (cheap, CPU, no model) — synthetic fixture + `raster.py` + `project.py` +
+`test_roundtrip_recall`. Eyeball numbers, then green-light P1.
From ae77ee1e8e6032cde4fef3e39f772afa56d4ea8a Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Sat, 13 Jun 2026 08:13:32 +0000
Subject: [PATCH 15/32] fix: correct padded-triangle sentinel in
split_triangle/split_triangles/_point_in_element (#211)
Padding convention is [v0,v1,v2,v0] (4th slot duplicates first vertex), as
enforced by _build_adjacencies and every other padding check (row[3]==row[0]).
Three sites used the wrong sentinel:
- mutations.py split_triangle / split_triangles tested elem[2] != elem[3],
so a normalized padded triangle failed the check and wrongly raised
'Element N is not a triangle' after merge_elements made a mesh 4-column.
- CHILmesh.py _point_in_element tested elem[3] == elem[2] (latent: benign only
because _point_in_quad degenerates correctly when v3==v0).
Regression tests in test_mutations.py exercise the padded-triangle path on a
post-merge mixed-element mesh; the _point_in_element test monkeypatches
_point_in_quad to assert correct branch routing. All fail pre-fix.
https://claude.ai/code/session_011k7M9e1bxhKdSUNnEXoQTF
---
src/chilmesh/CHILmesh.py | 2 +-
src/chilmesh/mutations.py | 4 +-
tests/test_mutations.py | 157 ++++++++++++++++++++++++++++++++++++++
3 files changed, 160 insertions(+), 3 deletions(-)
diff --git a/src/chilmesh/CHILmesh.py b/src/chilmesh/CHILmesh.py
index 10018dd..316928d 100644
--- a/src/chilmesh/CHILmesh.py
+++ b/src/chilmesh/CHILmesh.py
@@ -970,7 +970,7 @@ def _point_in_element(self, point: np.ndarray, elem_id: int) -> bool:
"""Check if point is inside element (handles tri and quad)."""
elem = self.connectivity_list[elem_id]
verts = self.points[elem, :2]
- if elem.size == 3 or elem[3] == elem[2]:
+ if elem.size == 3 or elem[3] == elem[0]:
return self._point_in_triangle(point, verts[0], verts[1], verts[2])
else:
return self._point_in_quad(point, verts[0], verts[1], verts[2], verts[3])
diff --git a/src/chilmesh/mutations.py b/src/chilmesh/mutations.py
index b34e7c2..3dcfb33 100644
--- a/src/chilmesh/mutations.py
+++ b/src/chilmesh/mutations.py
@@ -77,7 +77,7 @@ def split_triangle(
n_cols = self.mesh.connectivity_list.shape[1]
# Check element is triangle (3 vertices or padded quad with repeated vertex)
- if n_cols == 4 and elem[2] != elem[3]:
+ if n_cols == 4 and elem[3] != elem[0]:
raise ValueError(f"Element {elem_id} is not a triangle")
tri_verts = elem[:3]
@@ -485,7 +485,7 @@ def split_triangles(self, elem_ids: np.ndarray) -> np.ndarray:
raise IndexError(f"Element {eid} out of range [0, {self.mesh.n_elems})")
elem = self.mesh.connectivity_list[eid]
n_cols = self.mesh.connectivity_list.shape[1]
- if n_cols == 4 and elem[2] != elem[3]:
+ if n_cols == 4 and elem[3] != elem[0]:
raise ValueError(f"Element {eid} is not a triangle")
tri_verts = elem[:3]
p0, p1, p2 = self.mesh.points[tri_verts, :2]
diff --git a/tests/test_mutations.py b/tests/test_mutations.py
index 42ba007..3031242 100644
--- a/tests/test_mutations.py
+++ b/tests/test_mutations.py
@@ -851,3 +851,160 @@ def test_smooth_topology_speed(self, triangle_mesh):
mutable.smooth_topology(max_passes=200)
elapsed = time.perf_counter() - start
assert elapsed < 10.0, f"smooth_topology took {elapsed:.2f}s (expected < 10s)"
+
+
+class TestPaddedTrianglePaddingConvention:
+ """Regression tests for padded triangle padding convention fix (issue #211).
+
+ Bug: split_triangle / split_triangles / _point_in_element wrongly used
+ elem[3]==elem[2] to detect a padded triangle, when the actual convention
+ is elem[3]==elem[0] (4th slot duplicates FIRST vertex, not 3rd).
+
+ These tests verify the fix: padded triangles are no longer spuriously
+ rejected, and point-in-element correctly classifies them.
+ """
+
+ def test_split_triangle_padded_triangle_not_rejected(self, triangle_mesh):
+ """split_triangle accepts a padded triangle [v0, v1, v2, v0] without error.
+
+ Regression: old code wrongly checked elem[3] != elem[2] and raised
+ "is not a triangle" spuriously for valid padded triangles with padding
+ convention [v0, v1, v2, v0].
+ """
+ # Take first two adjacent triangles and merge into a quad to convert to 4-col.
+ mutable = MutableMesh(triangle_mesh)
+ n_cols_before = triangle_mesh.connectivity_list.shape[1]
+
+ if n_cols_before == 4:
+ # Already mixed. Find a padded triangle (one with only 3 unique verts).
+ padded_tri_id = None
+ for eid in range(triangle_mesh.n_elems):
+ row = triangle_mesh.connectivity_list[eid]
+ unique_verts = len(set(int(v) for v in row if int(v) >= 0))
+ if unique_verts == 3: # Padded triangle
+ padded_tri_id = eid
+ break
+ if padded_tri_id is None:
+ pytest.skip("no padded triangle in this mixed fixture")
+ else:
+ # Merge first two adjacent triangles to get a quad, padding remaining tris.
+ try:
+ quad_id = mutable.merge_elements(0, 1)
+ except ValueError as e:
+ if "not adjacent" in str(e):
+ pytest.skip("elements 0 and 1 not adjacent in this fixture")
+ raise
+ # After merge, connectivity is 4-col and other triangles are padded.
+ padded_tri_id = 2
+ # Sanity: element 2 (if it exists) should now be a padded triangle.
+ if padded_tri_id >= triangle_mesh.n_elems:
+ pytest.skip("not enough elements to find padded triangle after merge")
+
+ # Attempt split on the padded triangle — should NOT raise.
+ try:
+ new_ids = mutable.split_triangle(elem_id=padded_tri_id)
+ assert len(new_ids) > 0 # Should succeed and return element IDs.
+ # Connectivity should now have one more row.
+ assert triangle_mesh.n_elems > padded_tri_id
+ except ValueError as e:
+ if "is not a triangle" in str(e):
+ pytest.fail(f"split_triangle wrongly rejected padded triangle: {e}")
+ raise
+
+ def test_split_triangles_padded_triangles_not_rejected(self, triangle_mesh):
+ """split_triangles accepts padded triangles [v0, v1, v2, v0] in bulk.
+
+ Regression: old code wrongly checked elem[3] != elem[2] in the bulk path.
+ """
+ mutable = MutableMesh(triangle_mesh)
+ n_cols_before = triangle_mesh.connectivity_list.shape[1]
+
+ if n_cols_before == 4:
+ # Find padded triangles (exactly 3 unique verts).
+ padded_ids = []
+ for eid in range(min(3, triangle_mesh.n_elems)):
+ row = triangle_mesh.connectivity_list[eid]
+ unique_verts = len(set(int(v) for v in row if int(v) >= 0))
+ if unique_verts == 3:
+ padded_ids.append(eid)
+ if len(padded_ids) < 2:
+ pytest.skip("not enough padded triangles in mixed fixture")
+ else:
+ # Merge two pairs to get padded triangles.
+ try:
+ mutable.merge_elements(0, 1)
+ except ValueError as e:
+ if "not adjacent" in str(e):
+ pytest.skip("elements 0 and 1 not adjacent")
+ raise
+ # Elements 2, 3 should be padded now (if they exist).
+ padded_ids = [eid for eid in [2, 3] if eid < triangle_mesh.n_elems]
+ if len(padded_ids) < 2:
+ pytest.skip("not enough elements after merge")
+
+ try:
+ new_ids = mutable.split_triangles(np.array(padded_ids, dtype=int))
+ assert len(new_ids) > 0
+ assert triangle_mesh.n_elems > padded_ids[-1]
+ except ValueError as e:
+ if "is not a triangle" in str(e):
+ pytest.fail(f"split_triangles wrongly rejected padded triangle: {e}")
+ raise
+
+ def test_point_in_element_padded_triangle_classification(self, triangle_mesh, monkeypatch):
+ """_point_in_element correctly identifies padded triangle [v0, v1, v2, v0] as tri, not quad.
+
+ Regression: old code wrongly used elem[3]==elem[2] to detect padded tri,
+ causing mislassification and wrong point-in-test routing.
+
+ This test asserts WHICH BRANCH is taken: padded triangles must route
+ through _point_in_triangle, never _point_in_quad (even though the latter
+ would degenerate to the same answer).
+ """
+ mutable = MutableMesh(triangle_mesh)
+ n_cols_before = triangle_mesh.connectivity_list.shape[1]
+
+ if n_cols_before == 4:
+ # Find a padded triangle.
+ padded_tri_id = None
+ for eid in range(triangle_mesh.n_elems):
+ row = triangle_mesh.connectivity_list[eid]
+ unique_verts = len(set(int(v) for v in row if int(v) >= 0))
+ if unique_verts == 3:
+ padded_tri_id = eid
+ break
+ if padded_tri_id is None:
+ pytest.skip("no padded triangle in mixed fixture")
+ else:
+ # Merge to create padded triangles.
+ try:
+ mutable.merge_elements(0, 1)
+ except ValueError as e:
+ if "not adjacent" in str(e):
+ pytest.skip("elements not adjacent")
+ raise
+ padded_tri_id = 2
+ if padded_tri_id >= triangle_mesh.n_elems:
+ pytest.skip("not enough elements")
+
+ # Get centroid of the padded triangle.
+ elem = triangle_mesh.connectivity_list[padded_tri_id]
+ tri_verts = elem[:3]
+ centroid = triangle_mesh.points[tri_verts, :2].mean(axis=0)
+
+ # Spy on _point_in_quad to detect wrong-branch routing.
+ called = {"quad": False}
+ orig_quad = triangle_mesh._point_in_quad
+ def spy_quad(*a, **k):
+ called["quad"] = True
+ return orig_quad(*a, **k)
+ monkeypatch.setattr(triangle_mesh, "_point_in_quad", spy_quad)
+
+ # Call _point_in_element DIRECTLY on the known padded-triangle id.
+ result = triangle_mesh._point_in_element(centroid, padded_tri_id)
+
+ # Assertions:
+ # 1. Centroid is inside its own triangle.
+ assert result, f"centroid of padded triangle {padded_tri_id} should be inside it"
+ # 2. Padded triangle MUST route through triangle branch, not quad branch.
+ assert called["quad"] is False, "padded triangle wrongly routed to _point_in_quad"
From 1a795ec0c9b14fcccdb7a0480bb683b8206ebc1c Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Sat, 13 Jun 2026 08:15:41 +0000
Subject: [PATCH 16/32] docs: introspection corpus entry for rotation
2026-06-13T08Z (#211)
---
docs/introspections/development_ae77ee1.md | 59 ++++++++++++++++++++++
1 file changed, 59 insertions(+)
create mode 100644 docs/introspections/development_ae77ee1.md
diff --git a/docs/introspections/development_ae77ee1.md b/docs/introspections/development_ae77ee1.md
new file mode 100644
index 0000000..137ee0a
--- /dev/null
+++ b/docs/introspections/development_ae77ee1.md
@@ -0,0 +1,59 @@
+
+---
+date: 2026-06-13
+session: 2026-06-13T08Z-rotation
+repo: domattioli/CHILmesh
+severity: med
+freq: recurring
+issues: [211, 286]
+wasted_min: 6
+wasted_tok: 4000
+missing_skill: null
+---
+
+# Session Handoff — CHILmesh · development_ae77ee1 · 2026-06-13 (rotation hour-08)
+
+**Task:** overhaul rotation, CHILmesh slot. C spec-048 slice shipped 06-11/02Z → maintenance track (issue-queue top) + hub loop.
+**Phase:** maintenance
+**Progress:** complete — #211 fixed + regression-locked, pushed to rolling PR #210
+**Branch:** development (rolling PR #210)
+**Duration:** ~30 min
+**Tool failures:** 0
+**Outcome:** complete
+
+## Pre-flight
+
+- branch_policy_conflict: caught_and_resolved
+- domi_pin_drift: none on development (3e46639 current; pin on main/claude-branch is stale 39fd74a but that's pre-02Z-promotion, not real drift)
+- caveman_plugin: NOT loaded → /caveman:caveman ultra returned `Unknown skill` → emulated from SKILL.md (honest fallback per #168). SessionStart resume hook NOT active this container.
+- health_check: exit 0 but DEAD GATE — `sync-from-domi not installed` → warn+continue (DomI#286 class). Pin happened current so no exposure.
+
+## What shipped (evidence)
+
+1. `ae77ee1` fix #211 — padded-triangle sentinel. `split_triangle` (mutations.py:80) + `split_triangles` (:488) tested `elem[2] != elem[3]`; padding convention is `[v0,v1,v2,v0]` (every other check `row[3]==row[0]`), so post-`merge_elements` 4-column meshes wrongly raised `ValueError` on a remaining triangle. Latent `_point_in_element` (CHILmesh.py:973) `elem[3]==elem[2]`→`==elem[0]`. 3 one-token source fixes.
+2. 3 regression tests in `test_mutations.py` (donut param runs real; annulus skips no-adjacent-pair). Gate: full fast suite **992 passed / 47 skipped, 0 regressions** (63s, `not block_o`); was 989/44 pre-session.
+3. Hub: commented DomI#286 (CHILmesh = 3rd affected consumer of dead drift-gate); MADMESHing#48 checklist.
+
+## Key decisions
+
+1. Picked #211 (queue top, fresh `type: bug`, exact location+fix given, bounded/testable) over the brainstorm/research backlog (#201/#155/#167). Serves #48: mixed-element correctness is core to CHILmesh's role as MADMESHing's hard dep.
+2. **Did NOT port the offline drift-gate fallback** to CHILmesh despite the dead gate. DomI#286 explicitly says the per-repo copies (Valence#147, ADMESH#150) ARE the redundancy #48 targets and asks to canonicalize ONE helper. A 4th hand-rolled copy is the anti-pattern. Pin current on dev → no active exposure → held for DomI hub (hour-12) + noted CHILmesh affected on #286. Will wire canonical helper via `/sync from DomI` once shipped.
+3. Kept EDIT 3 (latent _point_in_element) despite it being currently benign — matches canonical convention, prevents a future silent break if `_point_in_quad` stops degenerating correctly. Made its test actually prove the branch routing (monkeypatch), not just the black-box answer.
+
+## What worked (top 3)
+
+1. **Revert experiment as the verification gate.** Did not trust the subagent's "95 passed". Reverted each fix, confirmed the new tests FAIL pre-fix. Caught that test (c) was a false-pass before commit.
+2. Empirical bug-site confirmation (grep convention across the file: 10 sites use `[3]==[0]`, 2 use the wrong sentinel) before dispatching — gave Haiku exact line+token edits, one clean round + one test-rework round.
+3. Haiku builder / Opus review split held; reviewer added the real signal the builder missed.
+
+## What didn't (pains → routing)
+
+1. **Subagent false-pass regression test (#168 class, recurring, severity med).** Haiku reported "95 passed, 3 skipped" — green — but (a) the new tests SKIPPED on annulus and only ran on donut, and (b) test (c) PASSED even with the EDIT-3 bug reverted → ZERO signal on the latent fix. A regression test that passes pre-fix proves nothing. Orchestrator caught it only by running the revert experiment + reading the skip guards. Cost ~6 min + 1 extra Haiku round (monkeypatch rework). Route: lesson, NOT a skill (#203 probation). **Mitigation for future sessions: every subagent-authored "regression test" MUST be revert-verified by the orchestrator (revert the fix → test must FAIL). A green test on a fixed tree is necessary, not sufficient.** Same root as the 02Z `git rev-parse` stdout-pollution false-pass and Valence's SYNCED-FAIL false-green — subagents over-report pass.
+2. **Skip-guarded parametrized tests mask zero-coverage.** `pytest.skip` on fixtures-without-the-needed-shape lets a test show green while never executing its assertion on any param. Hard to distinguish "passed" from "all skipped" without `-v`. Mitigation: prefer a fixture known to have the shape, or assert ≥1 param is non-skip; at minimum run `-v` on new test classes.
+3. **Dead drift-gate is silent across consumers.** Health check exit 0 + `✓ Health check passed` while blind to drift (plugin absent in cloud). Already DomI#286 (3rd instance now) — no new routing.
+
+## Next steps
+
+- DomI hub (hour-12): ship the canonical offline-drift helper (#286), then CHILmesh `/sync from DomI` to wire it (kills the dead gate without a 4th copy).
+- Issue queue next: #202-2c (pure-Python skeletonize >200s hot loop, the residual) or #201 (.chil format brainstorm). #211 fully closed by this PR once #210 promotes.
+- Spec-048 ecosystem remainder is consumer-side (Q-T5/Q-T7) — QuADMesh slot, not CHILmesh.
From f06a0adf084f1d14b11fe432d816fc74c723925a Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Sat, 13 Jun 2026 14:12:43 +0000
Subject: [PATCH 17/32] fix: correct stale >200s pure-Python perf claim +
vectorize centroids (#202)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The #202 slow-path UserWarning hardcoded "Block_O ~5k elems can exceed
200s", but re-measurement on development shows Block_O full pure-Python
init is ~0.24s (n_layers=9) and scaling is linear (~1s per 60k elems) —
the cliff is gone, the claim was off by ~1000x.
- Fix the warning text + raise the over-eager 2k elem threshold to 50k
(where the pure-Python gap actually becomes material).
- Vectorize _get_centroids (measured hot spot: python-loop np.mean over
all elements -> single fancy-index mean; bit-identical, incl. padded
triangles).
- Add tests/test_skeletonize_perf.py: Block_O full-init regression guard
(30s tripwire well under the historical cliff).
- Refresh the stale conftest comment (claimed Block_O ~30s / O(n^2)).
https://claude.ai/code/session_016bacixEkGkoYfsLd53H5DX
---
src/chilmesh/CHILmesh.py | 36 ++++++++++++++++++++--------------
tests/conftest.py | 11 ++++++-----
tests/test_skeletonize_perf.py | 36 ++++++++++++++++++++++++++++++++++
3 files changed, 63 insertions(+), 20 deletions(-)
create mode 100644 tests/test_skeletonize_perf.py
diff --git a/src/chilmesh/CHILmesh.py b/src/chilmesh/CHILmesh.py
index 316928d..aac5075 100644
--- a/src/chilmesh/CHILmesh.py
+++ b/src/chilmesh/CHILmesh.py
@@ -16,11 +16,13 @@
__all__ = ['CHILmesh', 'write_fort14']
-# One-time warn state for the source-install pure-Python perf cliff (#202).
-# When no compiled C++/Rust backend is present, skeletonizing a large mesh on the
-# pure-Python path is dramatically slow (Block_O ~5k elems > 200s). Warn once.
+# One-time warn state for the source-install pure-Python perf gap (#202).
+# When no compiled C++/Rust backend is present, skeletonization runs the
+# pure-Python path. It is linear in element count (~1s per 60k elems;
+# Block_O ~5k elems is ~0.2s) — slower than the compiled backend but not
+# catastrophic. Warn once, only for large meshes where the gap is material.
_SLOW_PATH_WARNED = False
-_SLOW_PATH_ELEM_THRESHOLD = 2000
+_SLOW_PATH_ELEM_THRESHOLD = 50000
class CHILmesh(CHILmeshPlotMixin):
"""
@@ -380,11 +382,12 @@ def _initialize_mesh( self, compute_layers: bool = True, compute_adjacencies: bo
warnings.warn(
f"chilmesh: skeletonizing a {self.n_elems}-element mesh "
"on the pure-Python backend (no compiled C++/Rust extension "
- "found). This is dramatically slower than the compiled path "
- "(e.g. Block_O ~5k elems can exceed 200s). Build the extension "
- "(pip install ./src/chilmesh_cpp) or pass compute_layers=False "
- "for fast metadata-only loading. Introspect with "
- "chilmesh.backend_info(). See CHILmesh #202.",
+ "found). The pure-Python path is linear in element count "
+ "(~1s per 60k elements) but slower than the compiled backend; "
+ "the gap grows with mesh size and repeated re-inits. Build the "
+ "extension (pip install ./src/chilmesh_cpp) or pass "
+ "compute_layers=False for fast metadata-only loading. Introspect "
+ "with chilmesh.backend_info(). See CHILmesh #202.",
UserWarning,
stacklevel=2,
)
@@ -936,13 +939,16 @@ def _build_spatial_indices(self) -> None:
self._centroid_tree = cKDTree(self._get_centroids())
def _get_centroids(self) -> np.ndarray:
- """Compute element centroids (mean of vertex coordinates)."""
+ """Compute element centroids (mean of vertex coordinates).
+
+ Vectorized over all elements. For padded triangles (``[v0,v1,v2,v0]``)
+ the repeated column is included in the mean — exactly matching the
+ prior per-element loop (bit-identical) — so layer / skeleton behaviour
+ is unchanged.
+ """
n_cols = self.connectivity_list.shape[1]
- centroids = np.zeros((self.n_elems, 2))
- for i, elem in enumerate(self.connectivity_list):
- verts = self.points[elem[:n_cols], :2]
- centroids[i] = np.mean(verts, axis=0)
- return centroids
+ verts = self.points[self.connectivity_list[:, :n_cols], :2]
+ return verts.mean(axis=1)
@property
def centroids(self) -> np.ndarray:
diff --git a/tests/conftest.py b/tests/conftest.py
index 91a73b2..61b2837 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -16,11 +16,12 @@
TRI_FIXTURE_NAMES = [n for n in FIXTURE_NAMES if n != "quad_2x2"]
-# Memoize the example loaders for the duration of the test session so the
-# O(n^2) adjacency build (deferred to the 0.1.2 perf release) doesn't
-# dominate the test runtime. Block_O alone takes ~30s on first load and
-# is touched by many parametrized tests. Tests that mutate a mesh in place
-# must call ``.copy()`` first.
+# Memoize the example loaders for the duration of the test session so repeated
+# fixture loads don't dominate the test runtime. The pure-Python full init is
+# linear and fast on these fixtures (Block_O ~5k elems is ~0.2-0.3s; see #202
+# for the perf-regression guard), but several parametrized tests load each
+# fixture many times, so caching still saves wall-clock. Tests that mutate a
+# mesh in place must call ``.copy()`` first.
#
# xdist safety (#122): pytest-xdist uses process-per-worker, so each worker
# owns an independent ``_MESH_CACHE`` dict — no cross-worker race on
diff --git a/tests/test_skeletonize_perf.py b/tests/test_skeletonize_perf.py
new file mode 100644
index 0000000..f063531
--- /dev/null
+++ b/tests/test_skeletonize_perf.py
@@ -0,0 +1,36 @@
+"""Perf-regression guard for the pure-Python skeletonize path (#202).
+
+#202 reported a >200s pure-Python full-init cliff on Block_O (~5k elems).
+Re-measurement on current ``development`` shows that cliff is gone: Block_O
+full init (read + adjacency + skeletonize + spatial index) is ~0.2-0.3s and
+scaling is linear (~1s per 60k elems). This test pins that — a regression
+back toward the cliff trips the generous 30s ceiling well before it becomes
+a >200s hang. The ceiling is a tripwire, not a target; it is deliberately
+loose to avoid flakiness on slow / loaded CI runners.
+"""
+from __future__ import annotations
+
+import time
+from importlib.resources import files
+
+from chilmesh import CHILmesh
+
+# Generous regression tripwire. Observed ~0.2-0.3s; >200s was the original
+# #202 cliff. 30s catches a real regression long before a hang.
+BLOCK_O_INIT_CEILING_S = 30.0
+
+
+def test_block_o_pure_python_init_under_ceiling():
+ """Block_O full init must stay well under the historical #202 cliff."""
+ path = files("chilmesh.data") / "Block_O.14"
+ start = time.perf_counter()
+ mesh = CHILmesh.read_from_fort14(str(path))
+ elapsed = time.perf_counter() - start
+
+ # Sanity: this is the real Block_O mesh and layers were computed.
+ assert mesh.n_elems > 5000
+ assert mesh.n_layers > 0
+ assert elapsed < BLOCK_O_INIT_CEILING_S, (
+ f"Block_O pure-Python full init took {elapsed:.1f}s "
+ f"(ceiling {BLOCK_O_INIT_CEILING_S}s) — possible #202 perf regression."
+ )
From 66996605cb12c8a59b7664f4226ab5b80497a98f Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Sat, 13 Jun 2026 14:14:28 +0000
Subject: [PATCH 18/32] docs: introspection corpus entry for rotation
2026-06-13T14Z (#202)
https://claude.ai/code/session_016bacixEkGkoYfsLd53H5DX
---
docs/introspections/development_f06a0ad.md | 62 ++++++++++++++++++++++
1 file changed, 62 insertions(+)
create mode 100644 docs/introspections/development_f06a0ad.md
diff --git a/docs/introspections/development_f06a0ad.md b/docs/introspections/development_f06a0ad.md
new file mode 100644
index 0000000..bd9f2f3
--- /dev/null
+++ b/docs/introspections/development_f06a0ad.md
@@ -0,0 +1,62 @@
+
+---
+date: 2026-06-13
+session: 2026-06-13T14Z-rotation
+repo: domattioli/CHILmesh
+severity: med
+freq: recurring
+issues: [202, 48]
+wasted_min: 0
+wasted_tok: 0
+missing_skill: null
+---
+
+# Session Handoff — CHILmesh · development_f06a0ad · 2026-06-13 (rotation hour-14)
+
+**Task:** overhaul rotation, CHILmesh slot. C spec-048 slice shipped prior slots → maintenance track (issue-queue top) + hub loop.
+**Phase:** maintenance
+**Progress:** complete — #202 problem-2c debunked + fixed, pushed to rolling PR #210
+**Branch:** development (rolling PR #210)
+**Duration:** ~35 min
+**Tool failures:** 1 minor (pytest absent → pip install; chilmesh not pre-installed → editable install)
+**Outcome:** complete
+
+## Pre-flight
+
+- branch_policy_conflict: caught_and_resolved — harness `claude/determined-gauss-ogyo6q` → `development` per CLAUDE.md precedence. Harness branch == origin/main baseline.
+- domi_pin_drift: none on `development` (3e46639 = DomI main HEAD; pin on the harness branch reads stale 39fd74a but that's pre-02Z-promotion, not real drift).
+- caveman_plugin: NOT loaded → `/caveman:caveman ultra` returned `Unknown skill` → emulated from SKILL.md (honest fallback per #168). SessionStart resume hook NOT active.
+- health_check: exit 0 but DEAD GATE — `sync-from-domi not installed` warn+continue (DomI#286 class). Pin already current → no exposure.
+
+## What shipped (evidence)
+
+1. `f06a0ad` fix #202 problem-2c. The 02Z slow-path `UserWarning` hardcoded "Block_O ~5k elems can exceed 200s". Re-measured: Block_O full pure-Python init = **0.24s** (n_layers=9), `_skeletonize`=0.018s. Synthetic scaling LINEAR (4k→0.05s · 16k→0.23s · 60k→1.0s) — no O(n²) anywhere. Claim ~1000× wrong.
+2. Edits (1 src file + 1 test comment + 1 new test): corrected warning text (now "linear, ~1s/60k elems") + raised over-eager 2k→**50k** elem threshold; vectorized `_get_centroids` (measured 0.135s hot spot → bit-identical fancy-index mean, incl. padded `[v0,v1,v2,v0]`); `tests/test_skeletonize_perf.py` Block_O 30s regression tripwire; refreshed stale conftest comment ("~30s / O(n²)" → reality).
+3. Gate: full suite **1069 passed / 56 skipped, 0 regressions** (100s). Verified centroid bit-identity (max|diff|=0.0) + warning fire@60k / no-fire@5k independently before commit.
+4. Hub/#48: #202 evidence comment (recommend MADMESHing drop `MADMESHING_RUN_BLOCK_O=1` gate-skip); #48 checklist.
+
+## Key decisions
+
+1. Picked #202-2c (08Z "next steps" nominee, queue top, on-mission for #48: it's *why* MADMESHing env-gates Block_O) over the brainstorm/research backlog (#201/#155/#167).
+2. **Re-measured before trusting the claim.** The ">200s" was inherited from the issue body (2026-06-09) into the 02Z warning text AND the conftest comment AND the 08Z handoff "next steps" — three layers deep, never re-measured. First action was a cProfile, which killed the premise in one shot. Pivoted slice from "fix a 200s hot loop" to "correct a 1000×-wrong perf claim" — the real bug.
+3. **Left #202 OPEN** for its real half (problem 2: editable/source install ships no compiled cpp extension; `CPP_AVAILABLE` wiring, tracked w/ #163). Only the perf-cliff sub-problem is resolved. Did not over-close.
+4. **Did NOT port DomI#286 offline drift-gate** (4th hand-rolled copy = the anti-pattern #48 targets). Pin current → no exposure → held for DomI hub.
+
+## What worked (top 3)
+
+1. **Profile-first killed a stale premise.** ~5 min of cProfile + a 3-point scaling test turned "deep O(n²) hot-loop fix (risky/deep)" into "fix a wrong string + a threshold (low-risk)". Cheapest possible course-correction.
+2. **Orchestrator-side independent verification.** Did not trust Haiku's "applied cleanly" — ran a reference-loop centroid diff (proved bit-identity) + a warning fire/no-fire harness at the new threshold. Caught nothing wrong this time, but the check is the gate, not the trust.
+3. Haiku builder / Fable-5 review split: exact old/new strings given → one clean round, zero rework.
+
+## What didn't (pains → routing)
+
+1. **Stale perf claims propagate across artifacts without re-measurement (recurring, severity med).** "Block_O >200s" originated in #202's body, then got copied into a *user-facing `UserWarning`* (02Z), a conftest comment, and a handoff "next steps" — a 1000×-wrong number shipped to users because each consumer trusted the prior layer instead of running the 5-min profile. Same family as the 08Z subagent-false-pass pain: **assertions about runtime/behavior get propagated, not verified.** Route: lesson, NOT a skill (#203 probation). Mitigation for future sessions: **any perf/runtime claim baked into shipped text (warnings, docs, comments) MUST cite a re-measurement with date+commit; treat an un-dated magnitude claim as unverified.** A cheap profile beats inheriting a number.
+2. **Dead drift-gate still silent** (DomI#286, now Nth instance). Health exit 0 + `✓` while blind to drift (plugin absent in cloud). No new routing — held for #286 canonical helper.
+3. Container ships no chilmesh/pytest — every CHILmesh routine slot pays the editable-install + `pip install pytest` tax. Minor, but recurring across slots; a cached venv / setup step would save ~30s/slot. (Same class as QuADMesh `scripts/dev_setup.sh`.)
+
+## Next steps
+
+- **MADMESHing M slot:** drop `MADMESHING_RUN_BLOCK_O=1` gate-skip + un-skip the two Block_O tests — basis cleared (init sub-second). MADMESHing-repo edit, not CHILmesh.
+- DomI hub: ship canonical offline-drift helper (#286) → CHILmesh `/sync from DomI` to wire it (kills the dead gate without a 4th copy).
+- #202 stays open for cpp source-build wiring (problem 2, w/ #163). Queue next after that: #201 (.chil format brainstorm) or #198 (hero gif).
+- #202-2c fully closed once #210 promotes to main.
From 5ac79f80bcf214f43c1694c49034fa0605427ba1 Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Sat, 13 Jun 2026 18:53:56 +0000
Subject: [PATCH 19/32] docs: canonical README rewrite (write-readme skill)
---
README.md | 39 +++++++++++++++++----------------------
1 file changed, 17 insertions(+), 22 deletions(-)
diff --git a/README.md b/README.md
index 7fcd599..92ef8fc 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,3 @@
-
-
-
-
@@ -17,17 +13,20 @@
†Corresponding author | 1Unaffiliated | 2Ohio State University (CHIL)
+---
+
+## Badges
+
-
-
+
+
+
-> **MATLAB users:** This Python library is the actively-developed successor to the original MATLAB codebase. The original (no longer maintained) is at [`src/@CHILmesh/CHILmesh.m`](src/@CHILmesh/CHILmesh.m) and on [MathWorks](https://www.mathworks.com/matlabcentral/fileexchange/135632-chilmesh/files/src/@CHILmesh/CHILmesh.m).
-
---
## Why CHILmesh
@@ -39,6 +38,8 @@
- **One interface for all topologies** — triangles, quadrilaterals, and mixed meshes share the same call surface.
- **Stable v1.x API** — sibling projects can pin `chilmesh>=1.0,<2`.
+> **MATLAB users:** This Python library is the actively-developed successor to the original MATLAB codebase. The original (no longer maintained) is at [`src/@CHILmesh/CHILmesh.m`](src/@CHILmesh/CHILmesh.m) and on [MathWorks](https://www.mathworks.com/matlabcentral/fileexchange/135632-chilmesh/files/src/@CHILmesh/CHILmesh.m).
+
---
## Installation
@@ -78,12 +79,6 @@ The legacy `chilmesh.CHILmesh` import is preserved for backward compatibility. B
- **Mesh alterations** — `insert_vertex`, coord moves, advancing-front element addition; full mutation suite tracked in [#94](https://github.com/domattioli/CHILmesh/issues/94)
- **ADMESH-Domains integration** — `from_admesh_domain()` adapter
-
-
-
- Figure 1. Scale demo on WNAT_Hagen (52,774 vertices · 98,365 elements). plot_quality() renders per-element skew quality; plot_quality_histogram() emits the matched-colormap distribution beneath. Reproduce: python scripts/generate_wnat_showcase.py.
-
-
### Performance
Reference workload: WNAT_Hagen (52,774 vertices · 98,365 elements). Median of 3 trials. **v1.0.0 backends are output-equivalent** — the C++ extension produces bit-identical skeletonization layers to Python, verified by [`tests/test_backend_equivalence.py`](tests/test_backend_equivalence.py).
@@ -98,6 +93,12 @@ Reference workload: WNAT_Hagen (52,774 vertices · 98,365 elements). Median of 3
**C++ is ~24× faster than Python on full init.** ‡ MATLAB v0.1.0 measured under GNU Octave 8.4 — treat as the original-algorithm baseline, not a MATLAB-vs-Octave claim. † Rust (v0.4.0) skeletonization is incomplete ([#163](https://github.com/domattioli/CHILmesh/issues/163)); its full-init figure reflects a partial peel. Full methodology and raw data: [`docs/BENCHMARK.md`](docs/BENCHMARK.md).
+
+
+
+ Figure 1. Scale demo on WNAT_Hagen (52,774 vertices · 98,365 elements). plot_quality() renders per-element skew quality; plot_quality_histogram() emits the matched-colormap distribution beneath. Reproduce: python scripts/generate_wnat_showcase.py.
+
+
### Validation
Python, C++, and the original MATLAB/Octave implementation all produce identical `n_layers` (medial-axis skeletonization) across the ADMESH-Domains catalog, from 557 to 132k vertices. Identical connectivity + points are fed to both implementations; only the layering algorithm is compared.
@@ -126,7 +127,7 @@ Three algorithms — each preserves boundary nodes, leaves topology unchanged, a
| **Zhou-Shimada angle-based** | `smooth_mesh(method='angle-based')` | Iterative, angle-maximising | Difficult mixed meshes where FEM stalls |
| **ADMESH Spring-Based Truss** | `chilmesh.optimize_with_admesh_truss(mesh, sdf, ...)` | Spring/force relaxation against SDF | Quality gains with SDF-respecting boundary nodes |
-**References.**
+**References:**
- Balendran (1999). *A direct smoothing method for surface meshes.* Proc. 8th IMR, pp. 189–193.
- Zhou & Shimada (2000). *An angle-based approach to two-dimensional mesh smoothing.* Proc. 9th IMR, pp. 373–384.
- Conroy et al. (2012). *ADMESH: An advanced, automatic unstructured mesh generator for shallow water models.* [doi:10.1007/s10236-012-0574-0](https://doi.org/10.1007/s10236-012-0574-0).
@@ -153,8 +154,6 @@ chilmesh.backend_info()
Force a specific backend with `CHILMESH_BACKEND` (`python` or `cpp`). When unset, the fastest available is picked. Pre-built binary wheels (`manylinux` / `macOS` / `Windows`) via `cibuildwheel` are planned — see [`docs/`](docs/) for build-from-source instructions.
-> **Source / editable installs run pure-Python.** `pip install -e .` (or installing the `chilmesh` sibling checkout as a downstream hard dep) ships **no compiled C++/Rust extension** unless you build it explicitly (`pip install ./src/chilmesh_cpp`). `backend_info()` reports honestly in that case (`selected: 'python'`, no `cpp`/`rust` in `available`) — an importable-but-empty namespace stub is **not** counted as available ([#163](https://github.com/domattioli/CHILmesh/issues/163)). Skeletonizing a large mesh on the pure-Python path is dramatically slower (Block_O ~5k elems can exceed 200s); CHILmesh emits a one-time `UserWarning` pointing here when that happens. Pass `compute_layers=False` for fast metadata-only loading ([#202](https://github.com/domattioli/CHILmesh/issues/202)).
-
### Examples
```bash
@@ -251,8 +250,4 @@ Issues and PRs welcome at [github.com/domattioli/CHILmesh](https://github.com/do
## License
-**Noncommercial / research use only.** Licensed under the PolyForm Noncommercial
-License 1.0.0 **with an additional No-AI/ML-training restriction** — see
-[LICENSE](LICENSE) and [.claude/AI-USAGE.md](.claude/AI-USAGE.md). No commercial use and no use
-as AI/ML training data without a separate written license. Commercial or
-AI-training licenses: domburner@duck.com
+**Noncommercial / research use only.** Licensed under the PolyForm Noncommercial License 1.0.0 **with an additional No-AI/ML-training restriction** — see [LICENSE](LICENSE) and [.claude/AI-USAGE.md](.claude/AI-USAGE.md). No commercial use and no use as AI/ML training data without a separate written license. Commercial or AI-training licenses: domburner@duck.com
From 65b4442df6c218d67aa1fb5086053a990b1c5172 Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Sat, 13 Jun 2026 19:17:24 +0000
Subject: [PATCH 20/32] docs: sync README perf table + speedup (~15x) + version
to v1.1.0 benchmark (docs/BENCHMARK.md)
---
README.md | 27 +++++++++++++--------------
1 file changed, 13 insertions(+), 14 deletions(-)
diff --git a/README.md b/README.md
index 92ef8fc..4504514 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@
**The stable backbone for hydrodynamic mesh tooling.** Sibling projects [ADMESH](https://github.com/domattioli/ADMESH), [ADMESH-Domains](https://github.com/domattioli/ADMESH-Domains), and [QuADMesh](https://github.com/domattioli/QuADMesh) build on top of it.
- **Pythonic API** — `from chilmesh import Mesh`; backwards-compatible `CHILmesh` alias preserved.
-- **C++ acceleration, bit-identical output** — half-edge extension is **~24× faster than pure Python** on full init, verified bit-for-bit by [36 cross-backend equivalence tests](tests/test_backend_equivalence.py).
+- **C++ acceleration, bit-identical output** — half-edge extension is **~15× faster than pure Python** on full init, verified bit-for-bit by [36 cross-backend equivalence tests](tests/test_backend_equivalence.py).
- **One interface for all topologies** — triangles, quadrilaterals, and mixed meshes share the same call surface.
- **Stable v1.x API** — sibling projects can pin `chilmesh>=1.0,<2`.
@@ -70,7 +70,7 @@ The legacy `chilmesh.CHILmesh` import is preserved for backward compatibility. B
## Features
-- **Fast** — full init + quality analysis on a 98,365-element mesh in ~1.7 s (4.6× faster than v0.2.0)
+- **Fast** — C++ backend does full init + quality analysis on a 98,365-element mesh in ~0.11 s (~15× faster than pure Python)
- **Mixed-element** — triangles, quads, and mixed meshes share one API
- **Smoothing** — Balendran direct FEM, Zhou-Shimada angle-based, and ADMESH Spring-Based Truss
- **Analysis** — element quality, interior angles, layer-based skeletonization (medial axis)
@@ -81,17 +81,16 @@ The legacy `chilmesh.CHILmesh` import is preserved for backward compatibility. B
### Performance
-Reference workload: WNAT_Hagen (52,774 vertices · 98,365 elements). Median of 3 trials. **v1.0.0 backends are output-equivalent** — the C++ extension produces bit-identical skeletonization layers to Python, verified by [`tests/test_backend_equivalence.py`](tests/test_backend_equivalence.py).
+Reference workload: WNAT_Hagen (52,774 vertices · 98,365 elements · 151,248 edges · 30 layers). v1.1.0 medians, single machine. **Backends are output-equivalent** — the C++ extension produces bit-identical skeletonization layers to Python (`n_layers = 30` on all three), verified by [`tests/test_backend_equivalence.py`](tests/test_backend_equivalence.py).
-| Metric | v0.1.0 MATLAB ‡ | v0.2.0 Python Port | v0.3.0 Python Optimized | v0.4.0 Rust † | v1.0.0 C++ |
-|---|---:|---:|---:|---:|---:|
-| Fast init (adj, no skeletonization) | 0.27 s | ~3.9 s | 1.31 s | 0.029 s | 0.036 s |
-| Skeletonization only | 0.67 s | ~3.8 s | 0.32 s | 0.20 s | 0.033 s |
-| Full init (adj + skeletonization) | 1.04 s | 7.7 s | 1.65 s | 0.23 s | 0.069 s |
-| Quality analysis | 12 ms | 6.6 s | 6.4 ms | <1 ms | <1 ms |
-| Vertex-edge lookup (per call) | ~2200 μs | ~700 μs | 0.34 μs | 0.02 μs | 0.04 μs |
+| Stage | MATLAB (Octave) ‡ | Python | C++ |
+|---|---:|---:|---:|
+| Fast init (adj, no skeletonization) | 0.27 s | 1.31 s | 0.060 s |
+| Skeletonization only | 0.67 s | 0.32 s | 0.052 s |
+| Full init (adj + skeletonization) | 1.04 s | 1.65 s | 0.112 s |
+| Quality analysis | 12 ms | 6.4 ms | 1.3 ms |
-**C++ is ~24× faster than Python on full init.** ‡ MATLAB v0.1.0 measured under GNU Octave 8.4 — treat as the original-algorithm baseline, not a MATLAB-vs-Octave claim. † Rust (v0.4.0) skeletonization is incomplete ([#163](https://github.com/domattioli/CHILmesh/issues/163)); its full-init figure reflects a partial peel. Full methodology and raw data: [`docs/BENCHMARK.md`](docs/BENCHMARK.md).
+**C++ is ~15× faster than Python on full init** (1.65 s → 0.112 s) and ~9× faster than the original Octave implementation. ‡ MATLAB measured under GNU Octave 8.4 (interpreter, not MATLAB JIT) — the original-algorithm baseline, not a MATLAB-vs-Octave claim. Rust is excluded — its skeletonization is incomplete ([#163](https://github.com/domattioli/CHILmesh/issues/163)). Absolute times are machine-dependent; full methodology and the regenerating harness: [`docs/BENCHMARK.md`](docs/BENCHMARK.md).
@@ -134,7 +133,7 @@ Three algorithms — each preserves boundary nodes, leaves topology unchanged, a
### Backends
-`pip install chilmesh` gives you the pure-Python implementation — zero compiled dependencies, runs everywhere, and is the canonical reference every other backend is validated against. The C++ extension is the high-performance opt-in: same algorithms, bit-identical output, ~24× faster on full init.
+`pip install chilmesh` gives you the pure-Python implementation — zero compiled dependencies, runs everywhere, and is the canonical reference every other backend is validated against. The C++ extension is the high-performance opt-in: same algorithms, bit-identical output, ~15× faster on full init.
| Language | Role | How to get it |
|---|---|---|
@@ -193,7 +192,7 @@ CHILmesh is the core engine for the ADCIRC mesh ecosystem. Sibling projects buil
## Status & Roadmap
-- **Shipped (v1.0.0)**: C++ half-edge backend (~24× faster on full init); bit-identical output verified; 36 cross-backend equivalence tests; fort.14 + .2dm I/O; mixed-element support.
+- **Shipped (v1.1.0)**: C++ half-edge backend (~15× faster on full init); bit-identical output verified; 36 cross-backend equivalence tests; fort.14 + .2dm I/O; mixed-element support.
- **In flight**: Pre-built binary wheels (cibuildwheel, manylinux/macOS/Windows) · Rust skeletonization completion ([#163](https://github.com/domattioli/CHILmesh/issues/163)) · Full mutation suite ([#94](https://github.com/domattioli/CHILmesh/issues/94))
- **Next**: conda-forge packaging · mkdocs API site · advancing-front element mutation
@@ -221,7 +220,7 @@ CHILmesh originated in MATLAB as the data structure backing a skeletonization-dr
quadrilateral, and mixed-element grids},
year = {2026},
publisher = {Zenodo},
- version = {1.0.0},
+ version = {1.1.0},
doi = {10.5281/zenodo.20263854},
url = {https://github.com/domattioli/CHILmesh}
}
From 83f6229bd08e0b12cafe8ba018cb6877573fe325 Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Sat, 13 Jun 2026 20:11:05 +0000
Subject: [PATCH 21/32] feat: add ADCIRC fort.13 nodal-attribute reader/writer
(#201, #196)
Standalone additive fort13_io module (read_fort13/write_fort13 + Fort13/
NodalAttribute dataclasses). Lossless fort.14+fort.13 round-trip prereq
flagged twice in #201 as highest-value standalone I/O gap for the
MADMESHing#48 unification. Handles 1-based<->0-based node id conversion,
multi-component (values_per_node>1) attributes, default+nondefault overlay
via dense(). Does not touch locked stage modules or save/load dispatch.
5 round-trip tests + sample fixture; full IO/contract subset green.
https://claude.ai/code/session_017yrRhnx17P3sACAxE7HwZy
---
src/chilmesh/__init__.py | 6 +
src/chilmesh/fort13_io.py | 225 ++++++++++++++++++++++++++++++++
tests/fixtures/fort13/sample.13 | 18 +++
tests/test_fort13_roundtrip.py | 127 ++++++++++++++++++
4 files changed, 376 insertions(+)
create mode 100644 src/chilmesh/fort13_io.py
create mode 100644 tests/fixtures/fort13/sample.13
create mode 100644 tests/test_fort13_roundtrip.py
diff --git a/src/chilmesh/__init__.py b/src/chilmesh/__init__.py
index 32b88ce..da0cdec 100644
--- a/src/chilmesh/__init__.py
+++ b/src/chilmesh/__init__.py
@@ -17,6 +17,7 @@
from .CHILmesh import CHILmesh, write_fort14
from .gmsh_io import read_msh, write_msh, GmshParseError
+from .fort13_io import Fort13, NodalAttribute, read_fort13, write_fort13, Fort13ParseError
from .mesh_topology import EdgeMap, quad_from_tri_pair, quads_from_tri_pairs
from .mutations import MutableMesh
from .quality import element_quality
@@ -96,6 +97,11 @@ def backend_info() -> dict:
"read_msh",
"write_msh",
"GmshParseError",
+ "Fort13",
+ "NodalAttribute",
+ "read_fort13",
+ "write_fort13",
+ "Fort13ParseError",
# Standalone quality computation
"element_quality",
# Backend introspection
diff --git a/src/chilmesh/fort13_io.py b/src/chilmesh/fort13_io.py
new file mode 100644
index 0000000..663c8b6
--- /dev/null
+++ b/src/chilmesh/fort13_io.py
@@ -0,0 +1,225 @@
+"""ADCIRC fort.13 (nodal attributes) file I/O for CHILmesh.
+
+Supports reading and writing ADCIRC fort.13 nodal attribute files with
+round-trip fidelity. Node IDs in fort.13 are 1-based; internally CHILmesh
+uses 0-based indexing.
+"""
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from pathlib import Path
+import numpy as np
+
+
+class Fort13ParseError(ValueError):
+ """Raised when parsing a fort.13 file encounters an error."""
+ pass
+
+
+@dataclass
+class NodalAttribute:
+ """A single nodal attribute (e.g. manning roughness, elevation)."""
+ name: str
+ units: str
+ values_per_node: int
+ default_values: np.ndarray # shape (values_per_node,), dtype float64
+ nondefault: dict[int, np.ndarray] = field(default_factory=dict) # 0-based node_id -> array
+
+
+@dataclass
+class Fort13:
+ """Container for fort.13 nodal attributes."""
+ grid_name: str
+ num_nodes: int
+ attributes: list[NodalAttribute]
+
+ def attribute(self, name: str) -> NodalAttribute:
+ """Retrieve attribute by name; raise KeyError if not found."""
+ for attr in self.attributes:
+ if attr.name == name:
+ return attr
+ raise KeyError(f"Attribute '{name}' not found in fort.13")
+
+ def dense(self, name: str) -> np.ndarray:
+ """Return dense array of attribute values, shape (num_nodes, values_per_node).
+
+ Fills with default values, overlaid with nondefault entries.
+ """
+ attr = self.attribute(name)
+ arr = np.tile(attr.default_values, (self.num_nodes, 1))
+ for node_id, values in attr.nondefault.items():
+ arr[node_id] = values
+ return arr
+
+
+def read_fort13(filename: str | Path) -> Fort13:
+ """Read a fort.13 nodal attribute file.
+
+ Converts 1-based node IDs to 0-based internal indexing.
+
+ Parameters:
+ filename: Path to the .13 file
+
+ Returns:
+ Fort13 object with parsed attributes
+
+ Raises:
+ Fort13ParseError: If file is malformed
+ """
+ filename = Path(filename)
+ with open(filename, 'r', encoding='utf-8') as f:
+ lines = [line.strip() for line in f]
+
+ # Skip blank lines
+ lines = [line for line in lines if line]
+
+ if len(lines) < 3:
+ raise Fort13ParseError("fort.13 file too short (need at least 3 lines)")
+
+ # Parse header
+ grid_name = lines[0]
+ try:
+ num_nodes = int(lines[1])
+ num_attrs = int(lines[2])
+ except ValueError as e:
+ raise Fort13ParseError(f"fort.13 header parse error: {e}")
+
+ # Parse metadata section
+ attributes: list[NodalAttribute] = []
+ line_idx = 3
+ attr_names_in_order = []
+
+ for _ in range(num_attrs):
+ if line_idx + 3 > len(lines):
+ raise Fort13ParseError("fort.13 metadata section incomplete")
+
+ attr_name = lines[line_idx]
+ units = lines[line_idx + 1]
+ try:
+ vpn = int(lines[line_idx + 2])
+ except ValueError as e:
+ raise Fort13ParseError(f"values_per_node parse error at line {line_idx + 2}: {e}")
+
+ line_idx += 3
+
+ # Parse default values
+ if line_idx >= len(lines):
+ raise Fort13ParseError("fort.13 default values line missing")
+
+ default_tokens = lines[line_idx].split()
+ if len(default_tokens) != vpn:
+ raise Fort13ParseError(
+ f"Attribute '{attr_name}' expects {vpn} default values, got {len(default_tokens)}"
+ )
+
+ try:
+ default_values = np.array([float(tok) for tok in default_tokens], dtype=np.float64)
+ except ValueError as e:
+ raise Fort13ParseError(f"Default values parse error: {e}")
+
+ line_idx += 1
+
+ attributes.append(NodalAttribute(
+ name=attr_name,
+ units=units,
+ values_per_node=vpn,
+ default_values=default_values,
+ nondefault={}
+ ))
+ attr_names_in_order.append(attr_name)
+
+ # Parse data section
+ for _ in range(num_attrs):
+ if line_idx >= len(lines):
+ raise Fort13ParseError("fort.13 data section incomplete")
+
+ attr_name = lines[line_idx]
+ if attr_name not in attr_names_in_order:
+ raise Fort13ParseError(f"Unknown attribute '{attr_name}' in data section")
+
+ # Find the attribute object
+ attr_obj = next(a for a in attributes if a.name == attr_name)
+
+ line_idx += 1
+ if line_idx >= len(lines):
+ raise Fort13ParseError(f"fort.13 num_nondefault line missing for '{attr_name}'")
+
+ try:
+ num_nondefault = int(lines[line_idx])
+ except ValueError as e:
+ raise Fort13ParseError(f"num_nondefault parse error: {e}")
+
+ line_idx += 1
+
+ # Parse nondefault rows
+ for _ in range(num_nondefault):
+ if line_idx >= len(lines):
+ raise Fort13ParseError(f"fort.13 data row missing for '{attr_name}'")
+
+ tokens = lines[line_idx].split()
+ if len(tokens) != 1 + attr_obj.values_per_node:
+ raise Fort13ParseError(
+ f"Data row for '{attr_name}' expects 1 + {attr_obj.values_per_node} tokens, "
+ f"got {len(tokens)}"
+ )
+
+ try:
+ # Convert 1-based node id to 0-based
+ node_id_1based = int(float(tokens[0]))
+ node_id = node_id_1based - 1
+
+ if not (0 <= node_id < num_nodes):
+ raise Fort13ParseError(f"Node ID {node_id_1based} out of range [1, {num_nodes}]")
+
+ values = np.array([float(tok) for tok in tokens[1:]], dtype=np.float64)
+ attr_obj.nondefault[node_id] = values
+ except (ValueError, IndexError) as e:
+ raise Fort13ParseError(f"Data row parse error: {e}")
+
+ line_idx += 1
+
+ return Fort13(
+ grid_name=grid_name,
+ num_nodes=num_nodes,
+ attributes=attributes
+ )
+
+
+def write_fort13(f13: Fort13, filename: str | Path) -> None:
+ """Write a fort.13 nodal attribute file.
+
+ Converts 0-based node IDs to 1-based for output (ADCIRC convention).
+
+ Parameters:
+ f13: Fort13 object to write
+ filename: Output path
+ """
+ filename = Path(filename)
+ with open(filename, 'w', encoding='utf-8') as f:
+ # Write header
+ f.write(f"{f13.grid_name}\n")
+ f.write(f"{f13.num_nodes}\n")
+ f.write(f"{len(f13.attributes)}\n")
+
+ # Write metadata section
+ for attr in f13.attributes:
+ f.write(f"{attr.name}\n")
+ f.write(f"{attr.units}\n")
+ f.write(f"{attr.values_per_node}\n")
+ # Write default values
+ default_str = " ".join(repr(float(v)) for v in attr.default_values)
+ f.write(f"{default_str}\n")
+
+ # Write data section
+ for attr in f13.attributes:
+ f.write(f"{attr.name}\n")
+ f.write(f"{len(attr.nondefault)}\n")
+ # Sort by node id for consistent output
+ for node_id in sorted(attr.nondefault.keys()):
+ node_id_1based = node_id + 1
+ values = attr.nondefault[node_id]
+ values_str = " ".join(repr(float(v)) for v in values)
+ f.write(f"{node_id_1based} {values_str}\n")
+
+
+__all__ = ["Fort13", "NodalAttribute", "read_fort13", "write_fort13", "Fort13ParseError"]
diff --git a/tests/fixtures/fort13/sample.13 b/tests/fixtures/fort13/sample.13
new file mode 100644
index 0000000..9b3e3f9
--- /dev/null
+++ b/tests/fixtures/fort13/sample.13
@@ -0,0 +1,18 @@
+sample_grid
+4
+2
+primitive_weighting_in_continuity_equation
+unitless
+1
+0.03
+surface_directional_effective_roughness_length
+m
+3
+0.0 0.0 0.0
+primitive_weighting_in_continuity_equation
+1
+2 0.02
+surface_directional_effective_roughness_length
+2
+1 0.1 0.2 0.3
+4 0.4 0.5 0.6
diff --git a/tests/test_fort13_roundtrip.py b/tests/test_fort13_roundtrip.py
new file mode 100644
index 0000000..122cbc5
--- /dev/null
+++ b/tests/test_fort13_roundtrip.py
@@ -0,0 +1,127 @@
+"""Round-trip tests for fort.13 nodal attribute I/O."""
+from __future__ import annotations
+
+from pathlib import Path
+import numpy as np
+import pytest
+
+from chilmesh import read_fort13, write_fort13, Fort13, NodalAttribute
+
+
+def test_read_fort13_parses_fixture():
+ """Test reading the sample.13 fixture file."""
+ fixture_path = Path(__file__).parent / "fixtures" / "fort13" / "sample.13"
+
+ f13 = read_fort13(fixture_path)
+
+ # Check header
+ assert f13.grid_name == "sample_grid"
+ assert f13.num_nodes == 4
+ assert len(f13.attributes) == 2
+
+ # Check attribute 1 (primitive_weighting_in_continuity_equation)
+ attr1 = f13.attributes[0]
+ assert attr1.name == "primitive_weighting_in_continuity_equation"
+ assert attr1.units == "unitless"
+ assert attr1.values_per_node == 1
+ np.testing.assert_array_almost_equal(attr1.default_values, [0.03])
+ assert 1 in attr1.nondefault # 0-based index (node 2 in 1-based)
+ np.testing.assert_array_almost_equal(attr1.nondefault[1], [0.02])
+ assert len(attr1.nondefault) == 1
+
+ # Check attribute 2 (surface_directional_effective_roughness_length)
+ attr2 = f13.attributes[1]
+ assert attr2.name == "surface_directional_effective_roughness_length"
+ assert attr2.units == "m"
+ assert attr2.values_per_node == 3
+ np.testing.assert_array_almost_equal(attr2.default_values, [0.0, 0.0, 0.0])
+ assert 0 in attr2.nondefault # 0-based index (node 1 in 1-based)
+ assert 3 in attr2.nondefault # 0-based index (node 4 in 1-based)
+ np.testing.assert_array_almost_equal(attr2.nondefault[0], [0.1, 0.2, 0.3])
+ np.testing.assert_array_almost_equal(attr2.nondefault[3], [0.4, 0.5, 0.6])
+ assert len(attr2.nondefault) == 2
+
+
+def test_dense_overlay():
+ """Test the dense() method overlays nondefault on default values."""
+ fixture_path = Path(__file__).parent / "fixtures" / "fort13" / "sample.13"
+ f13 = read_fort13(fixture_path)
+
+ # Check dense for attribute 1
+ dense1 = f13.dense("primitive_weighting_in_continuity_equation")
+ assert dense1.shape == (4, 1)
+ np.testing.assert_array_almost_equal(dense1[0, 0], 0.03)
+ np.testing.assert_array_almost_equal(dense1[1, 0], 0.02) # nondefault
+ np.testing.assert_array_almost_equal(dense1[2, 0], 0.03)
+ np.testing.assert_array_almost_equal(dense1[3, 0], 0.03)
+
+ # Check dense for attribute 2
+ dense2 = f13.dense("surface_directional_effective_roughness_length")
+ assert dense2.shape == (4, 3)
+ np.testing.assert_array_almost_equal(dense2[0], [0.1, 0.2, 0.3]) # nondefault
+ np.testing.assert_array_almost_equal(dense2[1], [0.0, 0.0, 0.0])
+ np.testing.assert_array_almost_equal(dense2[2], [0.0, 0.0, 0.0])
+ np.testing.assert_array_almost_equal(dense2[3], [0.4, 0.5, 0.6]) # nondefault
+
+
+def test_fort13_roundtrip_identity(tmp_path):
+ """Test that reading -> writing -> reading preserves all data."""
+ fixture_path = Path(__file__).parent / "fixtures" / "fort13" / "sample.13"
+ f13_original = read_fort13(fixture_path)
+
+ # Write to temp file
+ temp_file = tmp_path / "roundtrip.13"
+ write_fort13(f13_original, temp_file)
+
+ # Read back
+ f13_roundtrip = read_fort13(temp_file)
+
+ # Compare structure
+ assert f13_roundtrip.grid_name == f13_original.grid_name
+ assert f13_roundtrip.num_nodes == f13_original.num_nodes
+ assert len(f13_roundtrip.attributes) == len(f13_original.attributes)
+
+ # Compare each attribute
+ for orig_attr, round_attr in zip(f13_original.attributes, f13_roundtrip.attributes):
+ assert orig_attr.name == round_attr.name
+ assert orig_attr.units == round_attr.units
+ assert orig_attr.values_per_node == round_attr.values_per_node
+ np.testing.assert_array_equal(orig_attr.default_values, round_attr.default_values)
+
+ # Compare nondefault dicts
+ assert set(orig_attr.nondefault.keys()) == set(round_attr.nondefault.keys())
+ for node_id in orig_attr.nondefault:
+ np.testing.assert_array_almost_equal(
+ orig_attr.nondefault[node_id],
+ round_attr.nondefault[node_id]
+ )
+
+
+def test_public_exports():
+ """Smoke test that all public names are exported from chilmesh."""
+ from chilmesh import (
+ Fort13,
+ NodalAttribute,
+ read_fort13 as rf13,
+ write_fort13 as wf13,
+ Fort13ParseError,
+ )
+ assert Fort13 is not None
+ assert NodalAttribute is not None
+ assert rf13 is not None
+ assert wf13 is not None
+ assert Fort13ParseError is not None
+
+
+def test_attribute_method():
+ """Test the Fort13.attribute(name) method."""
+ fixture_path = Path(__file__).parent / "fixtures" / "fort13" / "sample.13"
+ f13 = read_fort13(fixture_path)
+
+ # Found attribute
+ attr = f13.attribute("primitive_weighting_in_continuity_equation")
+ assert attr.name == "primitive_weighting_in_continuity_equation"
+
+ # Missing attribute raises KeyError
+ with pytest.raises(KeyError, match="not found"):
+ f13.attribute("nonexistent_attribute")
From 35cfea8d2bf75083f62aa2dcc57bd6bfd2bbd9ce Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Sat, 13 Jun 2026 20:13:21 +0000
Subject: [PATCH 22/32] docs: introspection corpus entry for rotation
2026-06-13T20Z (#201)
https://claude.ai/code/session_017yrRhnx17P3sACAxE7HwZy
---
docs/introspections/development_83f6229.md | 58 ++++++++++++++++++++++
1 file changed, 58 insertions(+)
create mode 100644 docs/introspections/development_83f6229.md
diff --git a/docs/introspections/development_83f6229.md b/docs/introspections/development_83f6229.md
new file mode 100644
index 0000000..eb043be
--- /dev/null
+++ b/docs/introspections/development_83f6229.md
@@ -0,0 +1,58 @@
+
+---
+date: 2026-06-13
+session: 2026-06-13T20Z-rotation
+repo: domattioli/CHILmesh
+severity: low
+freq: recurring
+issues: [201, 196]
+wasted_min: 8
+wasted_tok: 9000
+missing_skill: null
+---
+
+# Session Handoff — CHILmesh · development_83f6229 · 2026-06-13 (rotation hour-20)
+
+**Task:** overhaul rotation, CHILmesh slot (hour-20). C spec-048 slice shipped 06-11 → maintenance track.
+**Phase:** maintenance
+**Progress:** complete — fort.13 nodal-attr I/O shipped to rolling PR #210
+**Branch:** development (rolling PR #210)
+**Duration:** ~35 min
+**Tool failures:** 0
+**Outcome:** complete
+
+## Pre-flight
+
+- branch_policy_conflict: caught_and_resolved
+- domi_pin_drift: none — `.domi-pin` 3e46639 == DomI sibling-clone main head. No `/sync`.
+- caveman_plugin: NOT loaded → `/caveman:caveman ultra` unavailable → emulated from SKILL.md (honest fallback per #168). SessionStart resume hook NOT active this container.
+
+## What shipped (evidence)
+
+1. `83f6229` feat: `chilmesh.fort13_io` — standalone ADCIRC fort.13 (nodal attributes) read/write. `read_fort13`/`write_fort13` + `Fort13`/`NodalAttribute` dataclasses, exported from `chilmesh`. 1-based↔0-based node-id conversion, multi-component (`values_per_node>1`) attrs, default+sparse-nondefault overlay via `Fort13.dense()`, `Fort13ParseError`. Purely additive — zero touch to locked stage modules or `save`/`load` (gmsh_io.py precedent).
+2. 5 round-trip/parse tests + `tests/fixtures/fort13/sample.13`. Gate (new + fort14 + io-portability + unification-contract subset): **34 passed, 0 regressions**.
+3. #48 claim+ship checklist; #201 tracking comment (the thread that nominated fort.13 twice).
+
+## Key decisions
+
+1. **Re-scoped honestly, did not invent.** Hour-20 had no open C spec-048 slice (all shipped prior slots). Walked the open-issue queue: #211 (08Z) + #202-perf-claim (14Z) already shipped on HEAD; #201 operator ask already answered by the 06-11 Fable review; #196 items 1+2 already done. Picked fort.13 because it is the explicitly twice-documented "highest-value standalone prereq / next CHILmesh slice after #132/#133" in #201 — scope came FROM the thread, not invented.
+2. **fort.13 kept decoupled from `.chil`.** The `.chil` Identity/CRS/hash half is operator-gated (#154 constitution reconciliation). fort.13 is independent + useful to ADCIRC users regardless → shippable now without preempting that decision. Module is standalone (no save/load dispatch wiring) — keeps the gmsh/14/2dm seam untouched.
+3. **Verified subagent output before commit** (coding-dispatch rule): read the full module, confirmed 1↔0-based + float64 `repr` round-trip + 2D `dense()` shape, ran the IO+contract regression subset myself. No revert-experiment needed (additive new module, not a fix-of-existing).
+
+## What worked (top 3)
+
+1. Dispatching code to Haiku with the EXACT fort.13 format spec + indexing rule + fixture contents inline → one clean round, all 5 tests green first try, no rework.
+2. Verifying "is it already done?" against `development` HEAD (git blame `ae77ee1`, grep the actual sentinel) BEFORE claiming — avoided re-doing #211/#202 that the issue tracker still showed open (open-pending-merge, not open-unstarted).
+3. Reading PR #210 body confirmed the rolling PR already carried my push → no duplicate PR created.
+
+## What didn't (pains → routing)
+
+1. **High discovery cost on a same-day-heavily-worked repo (recurring, severity low).** ~8 min / 9k tok spent confirming that the entire visible maintenance queue (#211, #202, #201, #196) was already shipped earlier the same day before landing on a genuinely-open item. The issue tracker showed these OPEN (they close on merge-to-main, but development moves faster), so "open issue" ≠ "unstarted work." Mitigation for future same-day slots: read the rolling PR (#210) body + `git log --oneline -15` FIRST to see what THIS day's prior slots already shipped, before reading the issue queue. Route: lesson, NOT a skill (#203 probation).
+2. **No tracking issue for fort.13.** Scope was defensible (twice-nominated in #201) but lived in a *closed* design thread's comments, not a `type: feat` issue. Made the claim require a paragraph of justification. Minor. Mitigation: when a recommendation in a closed thread becomes the next slice, a one-line `type: feat` issue would make the queue self-documenting — deferred (no `gh`/issue-create urgency; #196 + #201 cover it).
+3. **Caveman plugin + SessionStart resume hook absent in cloud container (recurring, #168/#286 class).** Emulated manually; no new routing (already tracked).
+
+## Next steps
+
+- fort.13 v1 limitation: empty `units` string desyncs the blank-skip parser (ADCIRC units never empty → out of scope v1). If a real fort.13 with blank units ever appears, switch read to line-position tracking instead of blank-skip.
+- Wire fort.13 into a `.chil` v1 `kind="mesh"` payload IF/when #154 constitution reconciliation lands (operator-gated, unchanged).
+- Issue-queue next CHILmesh slot: #202 problem-2c residual (pure-Python skeletonize hot loop — already debunked >200s, real item is cpp source-build wiring #163) or #155 lifecycle benchmark. Avoid #187 (lexicon top-to-bottom refactor — large, operator-gated).
From 07ada11e45e6f3bd7607c334bbc1b91668d4890c Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Mon, 15 Jun 2026 10:17:42 +0000
Subject: [PATCH 23/32] =?UTF-8?q?chore:=20sync=20DomI=20pin=203e46639=20?=
=?UTF-8?q?=E2=86=92=2069b073d?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Refresh .domi-pin to DomI main HEAD (manifest 9d57d1f) via sibling-clone
update_pin.sh. Drift closed.
https://claude.ai/code/session_01SBATG6GY83JQi7UQpbqYpK
---
.domi-pin | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.domi-pin b/.domi-pin
index 5e6bb8d..afd29d8 100644
--- a/.domi-pin
+++ b/.domi-pin
@@ -4,6 +4,6 @@
upstream: domattioli/DomI
branch: main
-sha: 3e46639d47875c879a275120be19f4fae9222aad
-manifest_sha256: 1877de76b95155d967ca7d09cf34a08d5b00f1d29182716ebd015f9bb83d4093
-pinned_at: 2026-06-13T02:26:24Z
+sha: 69b073dd5888411aef5c7e7f4ad30e557fd6f057
+manifest_sha256: 9d57d1fd6f468969892bc87017f0021647a6ed91f61c40ee544801b23914f014
+pinned_at: 2026-06-15T10:07:23Z
From 2b6923e119f2d54dfdf25985c43ff08e245a842a Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Mon, 15 Jun 2026 10:17:42 +0000
Subject: [PATCH 24/32] fix: correct mixed-element insert_vertex on quad meshes
(#211 tail)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
insert_vertex/_get_edge_set carried the wrong padding sentinel
(elem[2]!=elem[3]) plus a dead ternary (elem[:3] if … else elem[:3]),
so a quad's 4th edge was dropped from cavity boundary detection, and new
re-triangulation elements were built 3-column → np.vstack crashed on any
4-column (quad/mixed) mesh.
- Use the canonical _is_triangle(elem_id) helper to pick the 3- vs 4-vertex
ring (completes #211's fix, which patched :80/:488/_point_in_element but
missed these sites).
- Add _ring_to_edges() to enumerate a closed 3- or 4-edge ring; _get_edge_set
now emits 4 edges for quads.
- Pad new triangles to [v1,v2,new,v1] when the mesh is 4-column.
- Regression test test_insert_vertex_quad_mesh (2x2 quad grid): insertion no
longer crashes, re-triangulates, all signed areas non-negative.
https://claude.ai/code/session_01SBATG6GY83JQi7UQpbqYpK
---
src/chilmesh/mutations.py | 51 +++++++++++++++++++++++++++------------
tests/test_mutations.py | 42 ++++++++++++++++++++++++++++++++
2 files changed, 78 insertions(+), 15 deletions(-)
diff --git a/src/chilmesh/mutations.py b/src/chilmesh/mutations.py
index 3dcfb33..c7714d1 100644
--- a/src/chilmesh/mutations.py
+++ b/src/chilmesh/mutations.py
@@ -1125,7 +1125,7 @@ def insert_vertex(self, point: np.ndarray) -> int:
# Find cavity: all elements sharing vertices with containing element
containing_elem = self.mesh.connectivity_list[elem_id]
n_cols = self.mesh.connectivity_list.shape[1]
- elem_verts = containing_elem[:3] if (n_cols == 3 or containing_elem[2] != containing_elem[3]) else containing_elem[:3]
+ elem_verts = containing_elem[:3] if self._is_triangle(elem_id) else containing_elem[:4]
cavity_elems = set([elem_id])
for v in elem_verts:
@@ -1138,10 +1138,8 @@ def insert_vertex(self, point: np.ndarray) -> int:
boundary_edges = []
for elem_id_cav in cavity_elems:
elem = self.mesh.connectivity_list[elem_id_cav]
- tri_verts = elem[:3] if n_cols == 3 or elem[2] != elem[3] else elem[:3]
- edges = [(tri_verts[0], tri_verts[1]),
- (tri_verts[1], tri_verts[2]),
- (tri_verts[2], tri_verts[0])]
+ elem_verts_cav = elem[:3] if self._is_triangle(elem_id_cav) else elem[:4]
+ edges = self._ring_to_edges(elem_verts_cav)
for e in edges:
edge_key = tuple(sorted(e))
@@ -1155,9 +1153,7 @@ def insert_vertex(self, point: np.ndarray) -> int:
boundary_edges = [e for e in boundary_edges if e[0] != e[1]]
if not boundary_edges:
- boundary_edges = [(elem_verts[0], elem_verts[1]),
- (elem_verts[1], elem_verts[2]),
- (elem_verts[2], elem_verts[0])]
+ boundary_edges = self._ring_to_edges(elem_verts)
# Delete cavity elements (mark as zeros)
for elem_id_del in cavity_elems:
@@ -1195,7 +1191,10 @@ def insert_vertex(self, point: np.ndarray) -> int:
# Swap to ensure positive area
v1, v2 = v2, v1
- new_elem = np.array([[v1, v2, new_vert_id]])
+ if n_cols == 4:
+ new_elem = np.array([[v1, v2, new_vert_id, v1]])
+ else:
+ new_elem = np.array([[v1, v2, new_vert_id]])
self.mesh.connectivity_list = np.vstack([self.mesh.connectivity_list, new_elem])
self.mesh.n_elems = self.mesh.connectivity_list.shape[0]
@@ -1208,17 +1207,39 @@ def insert_vertex(self, point: np.ndarray) -> int:
return new_vert_id
def _get_edge_set(self, elem_id: int, n_cols: int) -> set:
- """Get edge set for an element."""
+ """Get edge set for an element (3 or 4 edges depending on triangle/quad)."""
first_vert = self.mesh.connectivity_list[elem_id, 0]
if elem_id >= self.mesh.n_elems or first_vert == 0 or first_vert < 0:
return set()
elem = self.mesh.connectivity_list[elem_id]
- tri_verts = elem[:3] if n_cols == 3 or elem[2] != elem[3] else elem[:3]
- edges = [(tri_verts[0], tri_verts[1]),
- (tri_verts[1], tri_verts[2]),
- (tri_verts[2], tri_verts[0])]
- return set([tuple(sorted(e)) for e in edges if e[0] != e[1]])
+ is_tri = self._is_triangle(elem_id)
+ verts = elem[:3] if is_tri else elem[:4]
+
+ edges = []
+ for i in range(len(verts)):
+ e = (verts[i], verts[(i + 1) % len(verts)])
+ if e[0] != e[1]:
+ edges.append(tuple(sorted(e)))
+ return set(edges)
+
+ def _ring_to_edges(self, ring_verts) -> list:
+ """Turn a vertex ring (3 or 4 verts) into its closed edge list.
+
+ Parameters
+ ----------
+ ring_verts : array-like
+ Ordered vertices forming a closed ring (3 for triangle, 4 for quad).
+
+ Returns
+ -------
+ list of tuples
+ List of (v_i, v_{i+1 % n}) edges closing the ring.
+ """
+ edges = []
+ for i in range(len(ring_verts)):
+ edges.append((ring_verts[i], ring_verts[(i + 1) % len(ring_verts)]))
+ return edges
def _signed_area(self, p0: np.ndarray, p1: np.ndarray, p2: np.ndarray) -> float:
"""Compute signed area of triangle (p0, p1, p2)."""
diff --git a/tests/test_mutations.py b/tests/test_mutations.py
index 3031242..063defc 100644
--- a/tests/test_mutations.py
+++ b/tests/test_mutations.py
@@ -1008,3 +1008,45 @@ def spy_quad(*a, **k):
assert result, f"centroid of padded triangle {padded_tri_id} should be inside it"
# 2. Padded triangle MUST route through triangle branch, not quad branch.
assert called["quad"] is False, "padded triangle wrongly routed to _point_in_quad"
+
+
+def test_insert_vertex_quad_mesh():
+ """Verify insert_vertex works on 4-column quad meshes.
+
+ Regression test for mixed-element insert_vertex bug: dead ternaries,
+ missing quad-edge enumeration, and unpadded new elements.
+ """
+ pts = np.array([
+ [0, 0], [1, 0], [2, 0],
+ [0, 1], [1, 1], [2, 1],
+ [0, 2], [1, 2], [2, 2]
+ ], dtype=float)
+ pts = np.c_[pts, np.zeros(len(pts))] # Add z-column
+
+ # 2x2 quad mesh: CCW-oriented quadrilaterals (0-indexed)
+ conn = np.array([
+ [0, 1, 4, 3], # quad 0: bottom-left
+ [1, 2, 5, 4], # quad 1: bottom-right
+ [3, 4, 7, 6], # quad 2: top-left
+ [4, 5, 8, 7] # quad 3: top-right
+ ], dtype=int)
+
+ m = CHILmesh(conn, pts)
+ mm = MutableMesh(m)
+
+ # Insert vertex at center of quad 0 (interior point (0.5, 0.5))
+ nid = mm.insert_vertex(np.array([0.5, 0.5]))
+
+ # Assertions: (a) no exception, (b) correct vertex ID, (c) vertex count, (d) re-triangulation
+ assert nid == 9, f"Expected new vertex ID 9, got {nid}"
+ assert mm.mesh.n_verts == 10, f"Expected 10 vertices, got {mm.mesh.n_verts}"
+ assert mm.mesh.n_elems > 4, f"Expected >4 elements after re-triangulation, got {mm.mesh.n_elems}"
+
+ # (e) New vertex has at least one incident element
+ incident = mm.mesh.get_vertex_elements(nid)
+ assert len(incident) > 0, f"New vertex {nid} has no incident elements"
+
+ # (f) All elements have non-negative signed area
+ signed_areas = mm.mesh.signed_area()
+ assert (signed_areas >= -1e-9).all(), \
+ f"Some elements have negative area: min={signed_areas.min()}"
From c7da5f2230a0cff11b503213a66f74a879a2920e Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Mon, 15 Jun 2026 10:19:21 +0000
Subject: [PATCH 25/32] docs: introspection corpus entry for rotation
2026-06-15T10Z (#211, #48)
https://claude.ai/code/session_01SBATG6GY83JQi7UQpbqYpK
---
docs/introspections/development_2b6923e.md | 56 ++++++++++++++++++++++
1 file changed, 56 insertions(+)
create mode 100644 docs/introspections/development_2b6923e.md
diff --git a/docs/introspections/development_2b6923e.md b/docs/introspections/development_2b6923e.md
new file mode 100644
index 0000000..6d7d96c
--- /dev/null
+++ b/docs/introspections/development_2b6923e.md
@@ -0,0 +1,56 @@
+
+---
+date: 2026-06-15
+session: 2026-06-15T10Z-rotation
+repo: domattioli/CHILmesh
+severity: low
+freq: recurring
+issues: [211, 48]
+wasted_min: 4
+wasted_tok: 1500
+missing_skill: null
+---
+
+# Session Handoff — CHILmesh · development_2b6923e · 2026-06-15 (rotation hour-10)
+
+**Task:** rotation maintenance track — C spec-048 slice already shipped 06-11→; issue-queue work
+**Phase:** maintenance
+**Progress:** complete — pin synced, quad insert_vertex crash fixed + regression-tested
+**Branch:** development (rolling PR #210)
+**Duration:** ~30 min
+**Tool failures:** 0
+**Outcome:** complete
+
+## Pre-flight
+- branch_policy_conflict: caught_and_resolved
+- domi_pin_drift: caught_and_synced
+- caveman_plugin: NOT loaded → Unknown skill → emulated from CLAUDE.md (DomI#268; expected cold-path race)
+
+## What shipped (evidence)
+
+1. `2b6923e^` chore: sync DomI pin 3e46639 → 69b073d (manifest 9d57d1f verified vs DomI main).
+2. `2b6923e` fix #211 tail — quad/mixed `insert_vertex` crashed (`np.vstack` ValueError, 3-col new elem vs 4-col mesh) and dropped a quad's 4th edge from cavity detection (wrong sentinel `elem[2]!=elem[3]` + dead ternary `elem[:3] if … else elem[:3]`). Routed ring selection through canonical `_is_triangle()`, added `_ring_to_edges()` (closed 3-/4-edge ring), padded new tris `[v1,v2,new,v1]` on 4-col meshes. Regression test `test_insert_vertex_quad_mesh`.
+3. Gate: `pytest tests/ -k "not block_o"` → 998 passed / 47 skipped, 0 regressions; `test_mutations.py` 96 passed.
+
+## Key decisions
+
+1. Picked the #211 tail over priority:now #155 (lifecycle benchmark): #155 is OOM/timeout-bound at scale (env-bound, in-progress); the insert_vertex crash is bounded, reproducible, verifiable, and serves #48 mixed-element robustness directly.
+2. Did NOT close #211 — it rides rolling PR #210, not yet on main; closing is on-merge. Same for #202 (problem-1 fixed/locked, 2c debunked, problem-2 build-hook operator-gated).
+3. Used existing `_is_triangle()` helper rather than re-deriving a sentinel → cannot drift from the package padding convention again (this is exactly how #211 left these three sites broken).
+
+## What worked (top 3)
+
+1. Empirical repro first — built a 2×2 quad mesh, hit the `np.vstack` ValueError live before touching code → reframed from "dead-ternary cleanup" to "the quad path fully crashes", and proved the regression test fails pre-fix.
+2. Sibling-clone pin sync via `REPO_ROOT=/home/user/CHILmesh bash /home/user/DomI/.../update_pin.sh` — no network, no gh, 1 call.
+3. Haiku builder for the 1-file fix + test; orchestrator reviewed the full `git diff` and independently re-ran the gate (998 pass) before commit.
+
+## What didn't (pains → routing)
+
+1. **"Fixed" issue had an untested tail that stayed broken.** #211 (`ae77ee1`) patched `mutations.py:80/:488` + `_point_in_element` but missed the *same* wrong-sentinel + dead-ternary in `insert_vertex`/`_get_edge_set` — because the original scan keyed on the literal `"is not a triangle"` strings, and these sites phrase the check differently (`elem[2] != elem[3]` inside a ternary). A grep for the *string* missed them; a grep for the *pattern* `elem\[2\].*elem\[3\]` would have caught all sites at once. Cost: the bug survived a "complete" fix ~2 days. Severity low, freq recurring (sentinel-convention bugs cluster). Route: lesson, NOT a skill (#203 probation). Mitigation: when fixing a convention/sentinel bug, grep the *expression shape* repo-wide (`elem[2]`/`elem[3]` index pairs), not the error string, to find every co-located site.
+2. **Quad path was never tested.** `insert_vertex` had 5 tests, all on 3-col triangle fixtures → a hard crash on any quad mesh shipped undetected. Mirrors the #211 root note ("existing tests only cover 3-column pure-triangle fixtures"). No new routing — covered by the standing mixed-element-coverage gap; this session adds one quad case.
+
+## Next steps
+
+- On PR #210 merge: #211 closes (now fully patched incl. insert_vertex), #202 closes for problem-1/2c (problem-2 build-hook stays open, operator-gated).
+- Mixed-element test coverage is thin for mutations beyond split/merge — a quad-fixture parametrize sweep over `insert_vertex`/`delete`/`flip` would surface more latent 3-col assumptions. Future C maintenance slot.
+- Spec-048 ecosystem remainder unchanged: M-T4 (MADMESHing `quality.py` benchmark → D2) verdict already KEEP-fast-path (06-13 05Z); no open C ecosystem item.
From a89d2eef80c8c2b14eee49f74650af9b822c394f Mon Sep 17 00:00:00 2001
From: Dominik Mattioli <35341510+domattioli@users.noreply.github.com>
Date: Mon, 15 Jun 2026 12:11:31 -0400
Subject: [PATCH 26/32] chore: release chilmesh 1.2.1
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 488e0ed..e8d1773 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "chilmesh"
-version = "1.2.0"
+version = "1.2.1"
description = "Fast 2D mesh library for hydrodynamic domains — Python API with optional C++ acceleration"
authors = [{name = "Dominik Mattioli"}]
license = {text = "PolyForm Noncommercial License 1.0.0"}
From 21a18e37001881d7be03b55cf19a7166f5d73465 Mon Sep 17 00:00:00 2001
From: Dominik Mattioli <35341510+domattioli@users.noreply.github.com>
Date: Mon, 15 Jun 2026 12:23:23 -0400
Subject: [PATCH 27/32] chore: export-ignore agent/dev files from source
archives (Zenodo/sdist/release tarball)
---
.gitattributes | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/.gitattributes b/.gitattributes
index 3889ef0..d294506 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,4 +1,12 @@
-# Exclude Claude-specific files from source distributions (git archive, PyPI sdist, Zenodo).
-# Keeps .claude/ off PyPI wheels and Zenodo snapshots without removing from the repo.
-.claude/ export-ignore
-CLAUDE.md export-ignore
+# Exclude Claude/agent + dev-process files from source archives
+# (git archive, PyPI sdist, GitHub release tarball, Zenodo snapshot).
+# Strictly library-running code ships downstream; these stay in the repo.
+.claude/ export-ignore
+CLAUDE.md export-ignore
+AGENTS.md export-ignore
+.specify/ export-ignore
+.planning/ export-ignore
+specs/ export-ignore
+docs/sessions/ export-ignore
+docs/introspections/ export-ignore
+.domi-pin export-ignore
From a3a22f7955dd365efdc866d292fb26410e2e93d9 Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Mon, 15 Jun 2026 18:12:43 +0000
Subject: [PATCH 28/32] =?UTF-8?q?chore:=20sync=20DomI=20pin=2069b073d=20?=
=?UTF-8?q?=E2=86=92=20a9b240f?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Refresh .domi-pin to DomI main HEAD via sibling-clone update_pin.sh. Manifest sha256 updated (DomI MANIFEST changed between pins). Closes session-start drift gate.
---
.domi-pin | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.domi-pin b/.domi-pin
index afd29d8..60ccd95 100644
--- a/.domi-pin
+++ b/.domi-pin
@@ -4,6 +4,6 @@
upstream: domattioli/DomI
branch: main
-sha: 69b073dd5888411aef5c7e7f4ad30e557fd6f057
-manifest_sha256: 9d57d1fd6f468969892bc87017f0021647a6ed91f61c40ee544801b23914f014
-pinned_at: 2026-06-15T10:07:23Z
+sha: a9b240f2d6f2158e2273da46a800af3ff70c6132
+manifest_sha256: 8e928b85a6aa6b7e3869cb3d04832eb2b2576a9a343e5cf4a92ad7890782cb75
+pinned_at: 2026-06-15T18:08:55Z
From c7a581c749ba5cf30e7f9cc370970c4bba3fba8f Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Mon, 15 Jun 2026 18:12:43 +0000
Subject: [PATCH 29/32] docs: remove duplicated #168 size-field note in
direct_smoother docstring
The isotropic/size-field Note block was pasted twice verbatim in the direct_smoother docstring. Remove the duplicate; content unchanged. Refs #168.
---
src/chilmesh/CHILmesh.py | 8 --------
1 file changed, 8 deletions(-)
diff --git a/src/chilmesh/CHILmesh.py b/src/chilmesh/CHILmesh.py
index aac5075..9981c17 100644
--- a/src/chilmesh/CHILmesh.py
+++ b/src/chilmesh/CHILmesh.py
@@ -1992,14 +1992,6 @@ def direct_smoother(self, kinf=1e12, freeze_quad_nodes: bool = False) -> np.ndar
original sizing. For size-respecting smoothing, supply anisotropic
targets (not yet implemented) or run a separate sizing pass afterward.
- Note (size-field behavior, #168): this smoother is **isotropic**. The
- Balendran stiffness targets a uniform equilateral triangle (60 deg) /
- square quad (90 deg) and takes **no size-field input** — it equalizes
- element *shape*, not *size*. Applied to a graded mesh it grows fine
- (e.g. coastal) edges and shrinks coarse (offshore) edges, eroding the
- original sizing. For size-respecting smoothing, supply anisotropic
- targets (not yet implemented) or run a separate sizing pass afterward.
-
Parameters:
kinf: Large stiffness value for fixed (pinned) vertices.
freeze_quad_nodes: When True, pin every vertex that is a corner of any
From af70ebb52381bee2401767e9881fc73ff3fca36b Mon Sep 17 00:00:00 2001
From: "Claude (dom macbook)"
Date: Mon, 15 Jun 2026 18:15:25 +0000
Subject: [PATCH 30/32] docs: introspection corpus entry for rotation
2026-06-15T18Z (#168, #196, #214, #48)
---
docs/introspections/development_c7a581c.md | 44 ++++++++++++++++++++++
1 file changed, 44 insertions(+)
create mode 100644 docs/introspections/development_c7a581c.md
diff --git a/docs/introspections/development_c7a581c.md b/docs/introspections/development_c7a581c.md
new file mode 100644
index 0000000..e5bdfd1
--- /dev/null
+++ b/docs/introspections/development_c7a581c.md
@@ -0,0 +1,44 @@
+
+---
+date: 2026-06-15
+session: 2026-06-15T18Z-rotation
+repo: domattioli/CHILmesh
+severity: low
+freq: recurring
+issues: [168, 196, 214, 48]
+wasted_min: 3
+wasted_tok: 2000
+missing_skill: null
+---
+
+# Session Handoff — CHILmesh · development_c7a581c · 2026-06-15 (rotation hour-18)
+
+**Task:** rotation maintenance track — C spec-048 slice shipped prior; queue + pin
+**Phase:** maintenance
+**Progress:** complete — pin synced, docstring defect fixed, coordination triaged, divergence flagged
+**Branch:** development (new rolling PR #213; #210 operator-merged 16:25Z)
+**Duration:** ~25 min
+**Tool failures:** 0
+**Outcome:** complete
+
+## Pre-flight
+- branch_policy_conflict: caught_and_resolved
+- domi_pin_drift: caught_and_synced
+- caveman_plugin: NOT loaded at boot → Unknown skill → emulated from CLAUDE.md; marketplace connected mid-session → re-attempt /caveman:caveman ultra succeeded (DomI#268 race, expected)
+
+## What shipped (evidence)
+- chore pin sync `69b073d → a9b240f` (`a3a22f7`). drift gate closed.
+- docs dedup `#168` note in `direct_smoother` docstring — pasted twice verbatim, removed dup, logic untouched, AST parse clean (`c7a581c`).
+- coordination triage #196 item 1: QuADMesh→CHILmesh API gaps #132/#133/#134/#138/#139 all CLOSED+completed, consumed downstream → item 1 resolved. items 2/3 remain.
+- filed #214: introspect-v2 migration incomplete on development (28 deprecated docs/introspections records main deleted via #212 + write-target contradiction: CHILmesh pull-only, cannot write DomI .introspect/CHILmesh/). operator/governance.
+
+## Pains (→ matrix, no new request:skill per #203)
+- introspect-v2-downstream-write-target-contradiction: downstream repo sessions have no compliant path to DomI central corpus (pull-only + no-cross-repo-write). deprecated dir regrows each session. routed to #214. severity med, freq recurring.
+- caveman-cold-start-race: DomI#268, known. low.
+- domi-pin-drift-each-rotation: mechanical sibling-clone sync each slot. low, recurring.
+
+## Next slot (hour-2/10 CHILmesh)
+- watch #214 for operator decision on introspect-v2 sweep before merging #213.
+- #196 items 2 (canonical layer-path traversal standalone fn) + 3 (API-stability contract) open coordination.
+- #163 status:blocked (Rust array n_layers=2 / C++ editable stub) — needs build env.
+- env: no send_later/remote MCP → cannot arm 1h self-check-in; PR #213 webhook covers CI-fail/review wake.
From 7f2c06cdf1156f8a385430c17409f9b108cac366 Mon Sep 17 00:00:00 2001
From: domattioli
Date: Mon, 15 Jun 2026 17:07:54 -0400
Subject: [PATCH 31/32] chore: rename ADMESH-Domains->Valence in docs/strings;
drop internal proposals; v1.2.2
---
CHANGELOG.md | 12 +-
README.md | 8 +-
docs/API.md | 2 +-
docs/BENCHMARK.md | 4 +-
docs/CHIL_FORMAT_INVESTIGATION.md | 8 +-
docs/CHILmesh_Access_Interface.md | 6 +-
docs/DOWNSTREAM_MIGRATION_GUIDE.md | 12 +-
docs/gallery/benchmark.json | 2 +-
docs/gallery/benchmark.log | 4 +-
.../mesh-segmenter/ADR-0001-architecture.md | 133 ------------------
docs/proposals/mesh-segmenter/CONTEXT.md | 121 ----------------
docs/proposals/mesh-segmenter/HANDOFF.md | 114 ---------------
.../PROTOTYPE-rasterize-sam2.md | 99 -------------
pyproject.toml | 2 +-
scripts/benchmark_all_backends.py | 2 +-
scripts/benchmark_quadegg_variants.py | 2 +-
scripts/benchmark_wnat_hagen.py | 12 +-
scripts/generate_wnat_showcase.py | 6 +-
src/chilmesh/CHILmesh.py | 14 +-
src/chilmesh/bridge.py | 6 +-
tests/TESTING.md | 2 +-
tests/test_admesh_metadata.py | 2 +-
tests/test_bridge_adapters.py | 6 +-
tests/test_metadata_validation.py | 2 +-
tests/test_skeletonization_matlab_parity.py | 4 +-
..._skeletonization_matlab_parity_external.py | 30 ++--
26 files changed, 77 insertions(+), 538 deletions(-)
delete mode 100644 docs/proposals/mesh-segmenter/ADR-0001-architecture.md
delete mode 100644 docs/proposals/mesh-segmenter/CONTEXT.md
delete mode 100644 docs/proposals/mesh-segmenter/HANDOFF.md
delete mode 100644 docs/proposals/mesh-segmenter/PROTOTYPE-rasterize-sam2.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b5b4fd..8587071 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
and the project adheres to [Semantic Versioning](https://semver.org/).
+## [1.2.2] — 2026-06-15
+
+### Changed
+- Renamed registry references `ADMESH-Domains` → `Valence` / `valence-domains` (registry was renamed). Public API names (`from_admesh_domain`, `admesh_metadata`) unchanged.
+- Removed internal design proposals from the published source tree.
+
## [1.2.0] — 2026-05-24
### ✨ Added
@@ -88,7 +94,7 @@ and the project adheres to [Semantic Versioning](https://semver.org/).
## [1.0.0] — 2026-05-22
First **stable** release. CHILmesh is now the production backbone for ADMESH,
-MADMESHR, and ADMESH-Domains.
+MADMESHR, and Valence.
### 🚀 Headline
@@ -441,7 +447,7 @@ These numbers carry forward unchanged into v0.4.0 — Phase 5 spatial queries an
#### Enhanced Test Coverage
- `test_skeletonization_invariant.py`: Layer separation invariant validation across all fixtures
- `test_skeletonization_matlab_parity.py`: MATLAB reference layer count validation
-- `test_skeletonization_matlab_parity_external.py`: External ADMESH-Domains catalog parity validation
+- `test_skeletonization_matlab_parity_external.py`: External Valence catalog parity validation
- `test_smoothing.py`: Comprehensive FEM smoother tests for triangles, quads, and mixed-element meshes
## [0.2.0] — 2026-04-27 (Modernization Release)
@@ -510,7 +516,7 @@ pinches = mesh.pinch_points(width_threshold=0.3)
#### Documentation & Release Infrastructure
- `API.md` - Complete 25+ method reference with stability guarantees
- `BENCHMARK.md` - Performance comparison v0.1.1 vs v0.2.0 with real-world impact
-- `DOWNSTREAM_MIGRATION_GUIDE.md` - Integration guide for MADMESHR, ADMESH, ADMESH-Domains
+- `DOWNSTREAM_MIGRATION_GUIDE.md` - Integration guide for MADMESHR, ADMESH, Valence
- `docs/CHILmesh_Access_Interface.md` - Stability guarantees and usage patterns
- Stable API contract (CAI) through v1.0
diff --git a/README.md b/README.md
index 4504514..5f61e62 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@
## Why CHILmesh
-**The stable backbone for hydrodynamic mesh tooling.** Sibling projects [ADMESH](https://github.com/domattioli/ADMESH), [ADMESH-Domains](https://github.com/domattioli/ADMESH-Domains), and [QuADMesh](https://github.com/domattioli/QuADMesh) build on top of it.
+**The stable backbone for hydrodynamic mesh tooling.** Sibling projects [ADMESH](https://github.com/domattioli/ADMESH), [Valence](https://github.com/domattioli/Valence), and [QuADMesh](https://github.com/domattioli/QuADMesh) build on top of it.
- **Pythonic API** — `from chilmesh import Mesh`; backwards-compatible `CHILmesh` alias preserved.
- **C++ acceleration, bit-identical output** — half-edge extension is **~15× faster than pure Python** on full init, verified bit-for-bit by [36 cross-backend equivalence tests](tests/test_backend_equivalence.py).
@@ -77,7 +77,7 @@ The legacy `chilmesh.CHILmesh` import is preserved for backward compatibility. B
- **I/O** — [ADCIRC](https://adcirc.org/) `.fort.14` and [SMS Aquaveo](https://www.aquaveo.com/sms) `.2dm` read/write
- **Spatial queries** — point-in-element, k-nearest vertices, radius search at O(log n)
- **Mesh alterations** — `insert_vertex`, coord moves, advancing-front element addition; full mutation suite tracked in [#94](https://github.com/domattioli/CHILmesh/issues/94)
-- **ADMESH-Domains integration** — `from_admesh_domain()` adapter
+- **Valence integration** — `from_admesh_domain()` adapter
### Performance
@@ -100,7 +100,7 @@ Reference workload: WNAT_Hagen (52,774 vertices · 98,365 elements · 151,248 ed
### Validation
-Python, C++, and the original MATLAB/Octave implementation all produce identical `n_layers` (medial-axis skeletonization) across the ADMESH-Domains catalog, from 557 to 132k vertices. Identical connectivity + points are fed to both implementations; only the layering algorithm is compared.
+Python, C++, and the original MATLAB/Octave implementation all produce identical `n_layers` (medial-axis skeletonization) across the Valence catalog, from 557 to 132k vertices. Identical connectivity + points are fed to both implementations; only the layering algorithm is compared.
| Mesh | Vertices | Elements | MATLAB | Python | C++ | Match |
|---|--:|--:|--:|--:|--:|:--:|
@@ -182,7 +182,7 @@ CHILmesh is the core engine for the ADCIRC mesh ecosystem. Sibling projects buil
| Repo | Role |
|---|---|
| [ADMESH](https://github.com/domattioli/ADMESH) | Unstructured triangle mesh generator; consumes CHILmesh for adjacency, smoothing, and quality analysis |
-| [ADMESH-Domains](https://github.com/domattioli/ADMESH-Domains) | Curated ADCIRC mesh registry; `Mesh.from_admesh_domain()` reads from it directly |
+| [Valence](https://github.com/domattioli/Valence) | Curated ADCIRC mesh registry; `Mesh.from_admesh_domain()` reads from it directly |
| [QuADMesh](https://github.com/domattioli/QuADMesh) | Quad mesh generator (MATLAB → Python port, in progress); CHILmesh data structure descends from the original QuADMesh+ |
| [MADMESHing](https://github.com/domattioli/MADMESHing) | Benchmark harness comparing ADMESH triangulation vs quad generators; uses CHILmesh for quality analysis |
diff --git a/docs/API.md b/docs/API.md
index 251af34..f364000 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -61,7 +61,7 @@ success = mesh.write_to_fort14("output.fort.14", grid_name="My Mesh")
#### `from_admesh_domain(record, compute_layers: bool = True) -> CHILmesh`
**Stability:** Stable | **Complexity:** O(n log n)
-Load mesh from ADMESH-Domains catalog record (duck-typed).
+Load mesh from Valence catalog record (duck-typed).
---
diff --git a/docs/BENCHMARK.md b/docs/BENCHMARK.md
index e450d10..c9fab3c 100644
--- a/docs/BENCHMARK.md
+++ b/docs/BENCHMARK.md
@@ -276,14 +276,14 @@ Total: 14.7s ✅ Interactive development
## How to Reproduce
```bash
-git clone https://github.com/domattioli/ADMESH-Domains /tmp/admesh-domains
+git clone https://github.com/domattioli/Valence /tmp/valence-domains
python3 << 'PYTHON'
from chilmesh import CHILmesh
from pathlib import Path
import time
-mesh_path = Path("/tmp/admesh-domains/registry_data/meshes/WNAT_Hagen.14")
+mesh_path = Path("/tmp/valence-domains/registry_data/meshes/WNAT_Hagen.14")
start = time.time()
mesh = CHILmesh.read_from_fort14(mesh_path, compute_layers=False)
diff --git a/docs/CHIL_FORMAT_INVESTIGATION.md b/docs/CHIL_FORMAT_INVESTIGATION.md
index bc2c7e0..598fe3e 100644
--- a/docs/CHIL_FORMAT_INVESTIGATION.md
+++ b/docs/CHIL_FORMAT_INVESTIGATION.md
@@ -17,7 +17,7 @@ already built, and what the object model is still missing.
| ADCIRC `fort.14` (`.14`/`.fort14`) | ✅ | ✅ | `CHILmesh.read_from_fort14`, `write_to_fort14`, module `write_fort14` |
| SMS `.2dm` | ✅ | ✅ (private `_write_2dm`) | `CHILmesh.read_from_2dm`, `_write_2dm` |
| Gmsh `.msh` (v2.2 + v4.1) | ✅ | ✅ | `gmsh_io.read_msh`/`write_msh`, `CHILmesh.read_from_msh`/`write_to_msh` |
-| ADMESH-Domains registry record | ✅ | — | `CHILmesh.from_admesh_domain(record)` |
+| Valence registry record | ✅ | — | `CHILmesh.from_admesh_domain(record)` |
| `.chil` | ❌ | ❌ | **none — does not exist anywhere in `src/`** |
**Unified dispatch already exists.** `CHILmesh.save(filename)` and
@@ -60,7 +60,7 @@ same three things the fort.14 writer already emits, plus the new metadata below.
| Native CRS declaration (Q8) | ❌ | No CRS field; Principle V makes coords **opaque** → direct conflict (#154 D9 HIGH) |
| Domain Boundary distinct from Mesh Boundary (Q11) | ❌ | Only `boundary_segments` (mesh edges) exist; no domain-outline concept |
| `fort.13` nodal attributes for lossless mesh round-trip (Q11) | ❌ | No fort.13 read/write anywhere → "lossless fort.14+fort.13" unmet |
-| Deterministic content hash / `content_uid` (Q15) | ❌ | No hashing in CHILmesh (lives in ADMESH-Domains schema, not here) |
+| Deterministic content hash / `content_uid` (Q15) | ❌ | No hashing in CHILmesh (lives in Valence schema, not here) |
| Multi-ring boundary (holes, Bermuda) (Q10/Q11) | partial | boundary_segments can list multiple rings, but no canonical winding/start-vertex normalization |
| Quantization to fixed grid (Q9) | ❌ | No coordinate quantization step |
@@ -94,7 +94,7 @@ If/when the RESHAPE blockers clear, integration is **localized**:
- Principle VI (format pluralism) vs `.chil` as *canonical* — promoting one
format is exactly what VI forbids; CHILmesh should treat `.chil` as **one more
readable/writable adapter**, not privileged. Registry-canonical is an
- ADMESH-Domains decision, not a CHILmesh one.
+ Valence decision, not a CHILmesh one.
- fort.13 nodal-attribute round-trip is a prerequisite for `kind="mesh"`
losslessness and is entirely unbuilt.
@@ -108,7 +108,7 @@ If/when the RESHAPE blockers clear, integration is **localized**:
recorded as `native = "unknown"`/passthrough (no transform — respects
Principle V). No Identity hash, no WGS84 transform. This is a pure additive
adapter, no constitution amendment required.
-2. **Defer the Identity/CRS/Domain-Boundary half** to ADMESH-Domains where the
+2. **Defer the Identity/CRS/Domain-Boundary half** to Valence where the
registry, hashing, and curation already live (#154 D6/D9/D15 put the heavy
deps — shapely/pyproj/numpy — on the registry side, which CHILmesh's minimal
base install should not absorb).
diff --git a/docs/CHILmesh_Access_Interface.md b/docs/CHILmesh_Access_Interface.md
index 08b428b..aff1e79 100644
--- a/docs/CHILmesh_Access_Interface.md
+++ b/docs/CHILmesh_Access_Interface.md
@@ -8,7 +8,7 @@
## Executive Summary
-**CAI** defines stable, documented public API that downstream projects (MADMESHR, ADMESH, ADMESH-Domains) can depend on. All methods listed here maintain signatures, return types, and semantics through v1.0. Breaking changes require major version bump + minimum 2-week advance notice.
+**CAI** defines stable, documented public API that downstream projects (MADMESHR, ADMESH, Valence) can depend on. All methods listed here maintain signatures, return types, and semantics through v1.0. Breaking changes require major version bump + minimum 2-week advance notice.
---
@@ -127,7 +127,7 @@ def read_from_2dm(full_file_name: Path, compute_layers: bool = True) -> "CHILmes
@staticmethod
def from_admesh_domain(record, compute_layers: bool = True) -> "CHILmesh":
"""
- Create from ADMESH-Domains catalog record (duck-typed, zero imports).
+ Create from Valence catalog record (duck-typed, zero imports).
Record must have 'fort14_path' attribute.
"""
@@ -145,7 +145,7 @@ def copy(self) -> "CHILmesh":
def admesh_metadata(self) -> Dict[str, Any]:
"""
- ADMESH-Domains compatible metadata.
+ Valence compatible metadata.
Returns: node_count, element_count, element_type ('tri'/'quad'/'mixed'), bounding_box.
"""
```
diff --git a/docs/DOWNSTREAM_MIGRATION_GUIDE.md b/docs/DOWNSTREAM_MIGRATION_GUIDE.md
index a315708..15daa76 100644
--- a/docs/DOWNSTREAM_MIGRATION_GUIDE.md
+++ b/docs/DOWNSTREAM_MIGRATION_GUIDE.md
@@ -2,7 +2,7 @@
**Version:** 2.0 (revised for CHILmesh v1.0.0)
**CHILmesh Version:** 1.0.0+
-**Target Projects:** MADMESHR, ADMESH, ADMESH-Domains
+**Target Projects:** MADMESHR, ADMESH, Valence
---
@@ -173,7 +173,7 @@ print(f"Mean quality: {report['mean']:.3f}")
print(f"Poor elements: {report['poor_count']}")
```
-### ADMESH-Domains Quick Start
+### Valence Quick Start
#### Before (v0.1.1)
```python
@@ -406,7 +406,7 @@ report = adapter.get_mesh_quality_report()
angle_summary = adapter.get_element_angles_summary()
```
-### ADMESH-Domains: Domain Decomposition
+### Valence: Domain Decomposition
**Relevant CAI Methods:**
```python
@@ -435,7 +435,7 @@ Include:
- Python version: `python --version`
- Minimal reproducible example
- Full error traceback
-- Downstream project name (MADMESHR/ADMESH/ADMESH-Domains)
+- Downstream project name (MADMESHR/ADMESH/Valence)
Report at: https://github.com/domattioli/CHILmesh/issues
@@ -443,7 +443,7 @@ Report at: https://github.com/domattioli/CHILmesh/issues
## Version Compatibility
-| CHILmesh | MADMESHR | ADMESH | ADMESH-Domains | Notes |
+| CHILmesh | MADMESHR | ADMESH | Valence | Notes |
|----------|----------|--------|----------------|-------|
| 0.1.1 | ✅ Legacy | ✅ Legacy | ✅ Legacy | Old version, still works |
| 0.2.0 | ✅ Recommended | ✅ Recommended | ✅ Recommended | Current, use this |
@@ -479,7 +479,7 @@ None required. Migration completely optional.
See `examples/` directory for complete working examples:
- `madmeshr_refinement.py` - MADMESHR mesh adaptation workflow
- `admesh_quality.py` - ADMESH quality assessment
-- `admesh_domains_setup.py` - ADMESH-Domains domain initialization
+- `admesh_domains_setup.py` - Valence domain initialization
---
diff --git a/docs/gallery/benchmark.json b/docs/gallery/benchmark.json
index 7209883..8f15a26 100644
--- a/docs/gallery/benchmark.json
+++ b/docs/gallery/benchmark.json
@@ -2,7 +2,7 @@
"chilmesh_version": "0.4.1",
"python": "3.11.15",
"os": "Linux x86_64",
- "mesh": "/tmp/admesh-domains/registry_data/meshes/WNAT_Hagen.14",
+ "mesh": "/tmp/valence-domains/registry_data/meshes/WNAT_Hagen.14",
"init": {
"fast_init_s": 1.2139215900000409,
"full_init_s": 4.567989638999961,
diff --git a/docs/gallery/benchmark.log b/docs/gallery/benchmark.log
index 68d74e3..e0c2fc8 100644
--- a/docs/gallery/benchmark.log
+++ b/docs/gallery/benchmark.log
@@ -1,3 +1,3 @@
-ERROR: WNAT_Hagen mesh not found. Clone ADMESH-Domains:
- git clone https://github.com/domattioli/ADMESH-Domains /tmp/admesh-domains
+ERROR: WNAT_Hagen mesh not found. Clone Valence:
+ git clone https://github.com/domattioli/Valence /tmp/valence-domains
then re-run this script.
diff --git a/docs/proposals/mesh-segmenter/ADR-0001-architecture.md b/docs/proposals/mesh-segmenter/ADR-0001-architecture.md
deleted file mode 100644
index d406ac7..0000000
--- a/docs/proposals/mesh-segmenter/ADR-0001-architecture.md
+++ /dev/null
@@ -1,133 +0,0 @@
-# ADR-0001 — mesh-segmenter: standalone package, chilmesh engine, neutral mask
-
-| Field | Value |
-|---|---|
-| Status | Proposed |
-| Date | 2026-06-13 |
-| Origin | [CHILmesh #153](https://github.com/domattioli/CHILmesh/issues/153) (filed as the `admesh-segmenter` proposal; ADMESH-side as #9) |
-| Method | grill-with-docs design session |
-| Related | ADMESH [`docs/adr/ADR-001-chilmesh-boundary.md`](https://github.com/domattioli/ADMESH/blob/main/docs/adr/ADR-001-chilmesh-boundary.md) |
-
-## Context
-
-The proposal is a composable sub-region **selection** API over a 2D mesh ("SAM2 for
-meshes"): pick elements by layer / distance / click / threshold / polygon, combine
-with set-algebra, usually to identify a sub-region to **re-mesh**. The issue body
-assumed it would live in ADMESH and consume `admesh.Mesh`; a later comment proposed
-"a package within admesh"; another raised a SAM2-style *learned* segmentation.
-
-Two facts from the codebase contradict the admesh framing:
-
-1. **`admesh.Mesh` is adjacency-free** (`frozen`, `slots`; `nodes / elements /
- boundaries / bathymetry / quality / title`). Every adjacency-walk mechanism the
- proposal needs (ring expansion, flood-fill, connected components) requires
- topology ADMESH deliberately does not keep.
-2. **CHILmesh already ships that topology and ~60–70% of the proposed API** —
- `Edge2Elem`/`Vert2Elem`, `elements_in_layer`, `submesh` (renumbering),
- `_skeletonize`, `build_spatial_indices`, fort.14 I/O — all built from raw
- `(nodes, elements)`.
-
-ADMESH's ADR-001 had already classified segmentation as *consumer-side* and
-reaffirmed "heavy consumer-side functionality earns its own package." This ADR
-finishes that thread by naming the package, its engine, and its dependency rules.
-
-## Decision
-
-**A standalone `mesh-segmenter` package whose only topology dependency is
-`chilmesh`. It selects; it never generates.**
-
-1. **Placement — standalone repo `mesh-segmenter`.** Not an ADMESH submodule
- (would bloat the generator import surface and contradict ADR-001) and not
- in-tree CHILmesh (keeps shapely / future ML deps out of CHILmesh core).
-
-2. **Engine — chilmesh only.** Accepts `admesh.Mesh` / fort.14 / raw
- `(nodes, elements)` and builds a `CHILmesh` internally for adjacency, layers,
- spatial index, and `submesh`. Because CHILmesh constructs from raw arrays, the
- segmenter is mesh-library-agnostic — justifying the generic name.
-
-3. **No generator dependency.** mesh-segmenter MUST NOT import `admesh` or
- `quadmesh`. A future "one-stop" **umbrella** package (depends on chilmesh,
- admesh, quadmesh, valence, *and* mesh-segmenter) owns the `selection → re-mesh`
- wiring. This keeps the segmenter a leaf and prevents a dependency cycle the
- moment the umbrella composes them.
-
-4. **Core object — `Selection`, a canonical immutable element mask** bound to a
- parent mesh. Set-algebra `|` `&` `~` returns new Selections. Nodal signals
- (bathymetry, curvature) reduce onto elements via a documented rule (default
- conservative "all vertices in range"; opt-in `any` / `mean`).
-
-5. **Output — neutral artifact at the re-mesh seam.** A Selection emits
- `element_ids`, `boundary` (raw polygon rings, holes supported), and an optional
- `submesh`; never an `admesh.Domain` directly. The umbrella wraps rings → Domain
- for re-gen, or feeds the submesh → quadmesh tri2quad.
-
-6. **Scope — two phases.**
- - **v1 (deterministic):** `Selection` + set-algebra; `grow` (ring dilation from a
- seed), `by_distance` (shapely), `by_click` (flood-fill w/ predicate),
- `by_threshold` (scalar + nodal reduction), `by_polygon`, `by_skeleton_layer`.
- - **v2 (research spike):** SAM2-*inspired* learned mask-from-size-field
- ("click in the gulf → the gulf, respecting bathymetry"). Transfer-learning,
- **not** train-from-scratch; uncertain, explicitly fenced out of v1. Plugs into
- the same `Selection`. De-risking bridge: v1
- `by_click(criterion=size_field_gradient)` is already region-growing bounded by
- the size field — the deterministic MVP of the SAM2 idea.
-
-### v1 mechanism contracts (grill round 2)
-
-7. **Adjacency is a per-call kwarg, edge default.** Every dual-graph mechanism
- (`grow`, `by_click`, components, boundary walk) takes `connectivity="edge"|
- "vertex"`, defaulting to `"edge"` (`Edge2Elem`). Edge avoids corner-bleed at
- pinch points / narrow inlets — the right default for click-selection in
- estuaries. `"vertex"` (`Vert2Elem`) is opt-in for wider dilation. **Component
- connectivity and `Selection.boundary` perimeters are edge-only** — a
- vertex-connected selection can have a non-manifold perimeter, so the boundary
- contract is defined over edge-components regardless of how the mask was grown.
-
-8. **`Selection.boundary` is per-component, never auto-cleaned.** Returns a list of
- components, each `(outer_ring, holes[])` as numpy rings. A `Selection` may be
- multi-component (a depth threshold spanning two basins) — surfaced, not hidden.
- `Selection.components()` yields per-component sub-Selections so the umbrella can
- re-mesh patches individually. Pinch-touching rings are *flagged* (warning), never
- silently merged or morphologically closed — a mask must not self-edit (SAM2
- fidelity).
-
-9. **`by_click` criterion = an edge-crossing predicate.** Canonical form
- `fn(from_elem, to_elem) -> bool` (`True` = stop, don't cross). Both-sided, so it
- expresses gradients / jumps / BC-changes (the gulf shelf-break case). Named
- shortcuts wrap it: `"connected"`, `("field_jump", field, delta)`, or any
- callable. The v2 learned model is just another crossing predicate — no API churn.
-
-10. **Fields are plain arrays; "all information" flows in without an import.** A
- field is a numpy array of length `n_nodes` or `n_elements` (auto-detected; nodal
- reduces per item 4). Three sources: mesh-attached (bathymetry),
- chilmesh-computed (quality / edge-length / area), and **umbrella-supplied admesh
- size-field components**. This reconciles "use all information available to us
- including admesh and chilmesh" with the leaf rule (item 3): the segmenter
- consumes admesh-derived signal **as an array passed by the umbrella**, never by
- importing admesh. Should a future need require the segmenter to compute admesh
- size-fields itself, that overrides item 3 and must amend this ADR.
-
-## Consequences
-
-**Enables**
-- A buildable v1 with no ML risk, immediately useful for the re-mesh pipeline.
-- Clean layering: umbrella → {generators, segmenter}; segmenter → chilmesh.
-- v2 ML work slots in behind a stable `Selection` contract — no API churn.
-
-**Costs / forecloses**
-- A naming correction: the issue's `by_layer` becomes **`grow`**; "Layer" stays
- reserved for CHILmesh's skeletonization peel (`by_skeleton_layer`). See
- [`CONTEXT.md`](./CONTEXT.md).
-- The umbrella must own selection→generator glue; the segmenter cannot offer a
- one-call `.re_mesh()` convenience without breaking the leaf rule.
-- ADMESH ADR-001 should gain a back-reference: the "segmenter sibling" it
- anticipated is engined by **chilmesh**, not admesh.
-
-## Follow-up
-
-- [ ] Operator: create the `mesh-segmenter` repo; lift this folder in as `docs/adr/`
- + `CONTEXT.md`.
-- [x] Package/import name — **provisional `mesh_segmenter`** (operator, 2026-06-13);
- a cooler final name is wanted before first publish. Low priority now.
-- [x] ~~Back-reference in ADMESH ADR-001~~ — declined by operator (2026-06-13).
-- [ ] v2: separate research spec before any training/transfer-learning work.
diff --git a/docs/proposals/mesh-segmenter/CONTEXT.md b/docs/proposals/mesh-segmenter/CONTEXT.md
deleted file mode 100644
index 955310a..0000000
--- a/docs/proposals/mesh-segmenter/CONTEXT.md
+++ /dev/null
@@ -1,121 +0,0 @@
-# mesh-segmenter Context
-
-Glossary seed for the proposed **mesh-segmenter** package — interactive, composable
-sub-region *selection* over a 2D mesh ("SAM2 for meshes"). It selects; it never
-generates. Lives in CHILmesh `docs/proposals/` until the sibling repo is created,
-then lifts in whole. Decisions recorded in [`ADR-0001-architecture.md`](./ADR-0001-architecture.md).
-
-## Language
-
-**Selection**:
-The canonical object — an immutable *mask over elements* bound to a parent mesh
-(`int64` ids / `bool[n_elements]`). The analogue of a SAM2 mask. Set-algebra
-(`|`, `&`, `~`) returns new Selections; everything else is a derived export.
-_Avoid_: Region, mask (in user-facing API), subset.
-
-**Element**:
-A mesh cell (triangle or quad) — the unit a Selection is over. Canonical entity:
-all mechanisms return element masks; nodal signals reduce onto elements.
-_Avoid_: face, cell, triangle (when quads are also in play).
-
-**Mechanism**:
-A function that produces or refines a Selection — the analogue of a SAM2 prompt.
-v1: `grow`, `by_distance`, `by_click`, `by_threshold`, `by_polygon`,
-`by_skeleton_layer`.
-_Avoid_: selector, filter, prompt (reserve "prompt" for the SAM2 analogy in prose).
-
-**grow**:
-Ring expansion / morphological *dilation* — expand a seed set outward by `n_rings`
-of dual-graph adjacency (mode set by **Adjacency mode**). This is what the original
-issue called `by_layer`.
-_Avoid_: by_layer, ring (the issue's collided name — see Flagged ambiguities), expand.
-
-**Adjacency mode**:
-What "neighboring elements" means for a dual-graph op — `"edge"` (share an edge,
-`Edge2Elem`) or `"vertex"` (share ≥1 vertex, `Vert2Elem`). A per-call `connectivity=`
-kwarg on every mechanism; **default `"edge"`** (no corner-bleed at pinch points).
-`"vertex"` grows wider per ring and bleeds through one-vertex touches — opt-in only.
-_Avoid_: connectivity (in prose), 4/8-connectivity (image-domain term).
-
-**Component**:
-A maximal **edge-connected** subset of a Selection. A Selection may hold several
-(e.g. a `by_threshold` depth mask spanning two basins). `Selection.components()`
-yields one sub-Selection per component. Perimeter walks are per-component.
-_Avoid_: island, blob, region.
-
-**Crossing predicate**:
-The formal `by_click` stop-criterion: `fn(from_elem, to_elem) -> bool`, where `True`
-means *stop* (don't expand across that edge). Expresses jumps / gradients / BC-changes
-(needs both sides). Named shortcuts wrap it (`"connected"`, `("field_jump", field,
-delta)`). The v2 learned model slots in as just another crossing predicate.
-_Avoid_: criterion (alone), stopping function, mask predicate.
-
-**Field**:
-A plain numpy array carrying a per-entity scalar — length `n_nodes` or `n_elements`
-(auto-detected; nodal reduces via **Reduction rule**). Sources: mesh-attached
-(bathymetry), chilmesh-computed (quality / edge-length / area), or passed in by the
-**Umbrella** (admesh size-field components). Arrays are the lingua franca — "all
-information available" reaches the segmenter as a Field, never as an admesh import.
-_Avoid_: signal, channel, feature (reserve for v2 ML).
-
-**Reduction rule**:
-How a per-node **Field** collapses onto the canonical element mask. Default
-conservative *"all vertices in range"*; opt-in `any` / `mean`.
-_Avoid_: aggregation, projection.
-
-**Neutral artifact**:
-What a Selection emits at the re-mesh seam, carrying no generator dependency:
-`element_ids`, `boundary` (raw polygon rings, holes supported), and an optional
-`submesh`. The umbrella — not the segmenter — wraps rings into an `admesh.Domain`
-or feeds the submesh to quadmesh.
-_Avoid_: output, result.
-
-**Engine**:
-The topology provider. mesh-segmenter depends on **chilmesh** only — adjacency
-(`Edge2Elem`, `Vert2Elem`), `elements_in_layer`, `submesh`, spatial indices — built
-from raw `(nodes, elements)`. Never depends on a *generator* (admesh / quadmesh).
-_Avoid_: backend (reserve for chilmesh's C++/Rust compute backends).
-
-**Umbrella**:
-The proposed future "one-stop" mesh package that depends on chilmesh, admesh,
-quadmesh, valence **and** mesh-segmenter, and wires `selection → re-mesh`. It owns
-the selection→generator handoff; the segmenter stays a leaf.
-_Avoid_: orchestrator, pipeline, one-stop (informal only).
-
-## Flagged ambiguities
-
-- **`Layer` is reserved.** CHILmesh `CONTEXT.md` binds **Layer** = a medial-axis
- *skeletonization peel* (OE/IE/OV/IV, global, inward). The issue's
- `by_layer(ring=…, n_layers=N)` meant *ring expansion outward from a seed* —
- a different operation. Resolution: that operation is **`grow`** (dilation);
- CHILmesh's true peels are exposed separately as **`by_skeleton_layer(idx)`**.
- Never let "layer" name the ring-expansion mechanism.
-
-- **`Mesh` is overloaded** (inherited from CHILmesh/ADMESH-Domains). Here a
- Selection's *parent mesh* is a runtime topology object (a `CHILmesh`). The thin
- `admesh.Mesh` wire dataclass is an *input* that gets built into a `CHILmesh` for
- adjacency. Say "parent mesh" for the runtime object.
-
-- **`node` vs `Element`.** fort.14 / ADCIRC say "node"; a Selection is over
- *elements*. Nodal fields exist (bathymetry per node) but never form the mask
- directly — they reduce. Keep the I/O word ("node") out of the selection API.
-
-## Example dialogue
-
-> **Dev:** "User clicks in the Gulf of Mexico — do we return the nodes or the
-> triangles inside?"
-> **Domain expert:** "Elements. A Selection is always an element mask. The click is
-> a *mechanism* — `by_click` flood-fills connected elements until a predicate stops
-> it."
-> **Dev:** "But bathymetry is per node. How does 'shallower than 2 m' become elements?"
-> **Domain expert:** "Through the *reduction rule*. Default: an element is in only if
-> *all* its vertices are under 2 m. So `by_threshold` reads the nodal field, reduces,
-> and hands back an element Selection — same type as every other mechanism."
-> **Dev:** "Then the user wants to re-mesh that. We call `admesh.triangulate`?"
-> **Domain expert:** "Not from in here. The Selection emits a *neutral artifact* — the
-> boundary rings. The umbrella turns rings into an `admesh.Domain` and re-meshes. The
-> segmenter never imports a generator; that's how it stays a chilmesh-only leaf."
-> **Dev:** "And expanding three rings off the coastline — that's `by_layer`?"
-> **Domain expert:** "Call it `grow`. 'Layer' is CHILmesh's skeletonization peel —
-> different thing. `grow(seed, n_rings=3)` is dilation. The peels are
-> `by_skeleton_layer`."
diff --git a/docs/proposals/mesh-segmenter/HANDOFF.md b/docs/proposals/mesh-segmenter/HANDOFF.md
deleted file mode 100644
index 543f263..0000000
--- a/docs/proposals/mesh-segmenter/HANDOFF.md
+++ /dev/null
@@ -1,114 +0,0 @@
-# Handoff — mesh_segmenter sandbox session
-
-For a fresh session that boots **inside the new `mesh_segmenter` repo** to prototype.
-Everything here was designed in two grill-with-docs sessions on CHILmesh #153; this
-folder (`docs/proposals/mesh-segmenter/`) is the seed — **lift it whole into the new
-repo as `docs/`** (ADR → `docs/adr/`, the rest alongside).
-
-## 0. Mission (one line)
-
-`mesh_segmenter` = interactive, composable sub-region **selection** over a 2D mesh
-("SAM2 for meshes"). **It selects; it never generates.**
-
-## 1. Locked decisions (full rationale: `ADR-0001-architecture.md`)
-
-- **Standalone repo**, provisional import name `mesh_segmenter` (a cooler name wanted
- before first publish — not urgent).
-- **Engine = `chilmesh` ONLY.** Accept `admesh.Mesh` / fort.14 / raw `(nodes,
- elements)` → build a `CHILmesh` internally for adjacency, layers, spatial index,
- `submesh`. Because CHILmesh builds from raw arrays, the segmenter is
- mesh-library-agnostic.
-- **Never import a generator** (`admesh` / `quadmesh`). A future "one-stop umbrella"
- (chilmesh + admesh + quadmesh + valence + mesh_segmenter) owns the
- `selection → re-mesh` wiring. Keeps the segmenter a leaf, avoids a dependency cycle.
-- **Core object = `Selection`** — an immutable **element mask** bound to a parent
- mesh. Set-algebra `|` `&` `~` returns new Selections.
-- **Exports are lazy/derived** — `element_ids`, `boundary` (raw polygon rings), an
- optional `submesh`. Never emit an `admesh.Domain`; the umbrella wraps rings.
-
-## 2. v1 mechanism contracts (grill round 2)
-
-- **Adjacency** = per-call `connectivity="edge"|"vertex"`, default `"edge"` (no
- corner-bleed at pinch points). Components + `Selection.boundary` perimeters are
- **edge-only** regardless of how the mask was grown.
-- **`Selection.boundary`** = per-component list of `(outer_ring, holes[])`; never
- auto-cleaned. `Selection.components()` yields per-component sub-Selections. Pinch
- rings flagged (warning), never silently merged — a mask must not self-edit.
-- **`by_click` criterion** = edge-crossing predicate `fn(from_elem, to_elem) -> bool`
- (`True` = stop). Named shortcuts (`"connected"`, `("field_jump", field, delta)`)
- wrap it. v2 learned model is just another crossing predicate.
-- **Fields** = plain numpy arrays (`n_nodes` | `n_elements`, auto-detect, nodal
- reduces via "all verts in range" default). Sources: mesh-attached, chilmesh-computed
- (quality / edge-length / area), umbrella-supplied admesh size-field components.
- "All information" reaches the segmenter as an array — never an admesh import.
-
-## 3. v1 surface to build
-
-`Selection` + set-algebra; mechanisms `grow` (ring dilation), `by_distance` (shapely),
-`by_click` (flood-fill + predicate), `by_threshold` (scalar + reduction), `by_polygon`,
-`by_skeleton_layer` (exposes CHILmesh peels). Naming note: the issue's `by_layer` is
-**`grow`**; "Layer" stays CHILmesh's skeletonization peel.
-
-## 4. FIRST TASK this session — the prototype, not v1
-
-Before committing to v2, run the **rasterize → SAM2 bootstrap prototype** in
-`PROTOTYPE-rasterize-sam2.md`. It decides (no training) whether SAM2 beats the cheap
-deterministic baseline. Build order:
-
-1. **P0 (CPU, no model)** — `prototype/raster.py` + `project.py` + synthetic 2-basin
- `fixtures.py` + `test_roundtrip_recall`. Watershed stub = baseline IoU.
-2. Eyeball numbers, then **P1** (real SAM2 via `huggingface_hub`), then **P2** gate.
-
-Gate: `SAM2 IoU > baseline IoU` AND IoU ≥ 0.70 on ≥ 2 cases AND jitter IoU ≥ 0.80. Miss
-any → drop SAM2, ship deterministic `by_click(field_gradient)`.
-
-Keep prototype under `prototype/` with its own `[proto]` extra — throwaway, not the
-shipped API, never in chilmesh.
-
-## 5. Repo setup (cold start)
-
-```bash
-# in the new mesh_segmenter repo root
-python -m venv .venv && . .venv/bin/activate
-pip install -e ../CHILmesh # engine, editable from sibling checkout
-pip install -e ".[dev,proto]" # numpy scipy scikit-image (+ torch sam2 hf for P1)
-# package skeleton: mesh_segmenter/{__init__,selection,mechanisms/}.py
-# prototype lives in prototype/ (see PROTOTYPE-rasterize-sam2.md file map)
-```
-
-CHILmesh entry points the engine gives you for free: `CHILmesh(connectivity=elements,
-points=nodes, build_spatial_indices=True)`, `.elements_in_layer(i)`, `.submesh(ids)`,
-`Edge2Elem` (−1 = boundary), `Vert2Elem`, fort.14 read/write, `from_admesh_domain`.
-
-## 6. Hard truth to keep in view
-
-The pipeline's real bottleneck is **NOT segmentation** — it's **conforming re-stitch**
-(re-inserting a re-meshed sub-region into the parent mesh with matched boundary nodes,
-no T-junctions). That lives in the **umbrella**, not here. The segmenter only owes a
-clean boundary ring. Don't over-invest in segmentation polish until the re-stitch path
-is proven viable elsewhere.
-
-## 7. Open questions
-
-- Cooler final package name (provisional `mesh_segmenter`).
-- Real demand beyond the operator — coastal "select shallow elements" plausible but
- unvalidated. v1 is cheap enough that this doesn't gate building it.
-- Whether the umbrella repo exists yet — segmenter's standalone value is limited until
- it does.
-
-## 8. Refs
-
-- CHILmesh #153 — issue + both grill comments (round 1 architecture, round 2
- contracts).
-- `ADR-0001-architecture.md`, `CONTEXT.md`, `PROTOTYPE-rasterize-sam2.md` (this
- folder).
-- ADMESH `docs/adr/ADR-001-chilmesh-boundary.md` — the consumer-side / sibling-package
- precedent (segmenter is chilmesh-engined; no back-ref added per operator).
-- DomI #268 — skill-load recurrence (why a session may run a DomI skill via SKILL.md
- emulation instead of the registered Skill).
-
-## 9. Conventions in the new repo
-
-- Branch: work on `development`, draft PR `development → main` (mirror CHILmesh policy).
-- Coding dispatch: code → Haiku subagent; main session plans/reviews/integrates.
-- Caveman mode active for orchestrator/technical exchange.
diff --git a/docs/proposals/mesh-segmenter/PROTOTYPE-rasterize-sam2.md b/docs/proposals/mesh-segmenter/PROTOTYPE-rasterize-sam2.md
deleted file mode 100644
index 833b39f..0000000
--- a/docs/proposals/mesh-segmenter/PROTOTYPE-rasterize-sam2.md
+++ /dev/null
@@ -1,99 +0,0 @@
-# Prototype Plan — rasterize → SAM2 field bootstrap
-
-Goal: decide if SAM2 on a rasterized mesh field is worth building, with **zero model
-training**. The prototype answers one question: **does SAM2 beat the cheap
-deterministic baseline on element-IoU?** If no → drop SAM2, ship deterministic
-`by_click(field_gradient)`.
-
-This is throwaway/sandbox code. It does NOT belong in `mesh_segmenter`'s shipped API
-or in chilmesh — keep it under `prototype/` with its own optional `[proto]` extra.
-
-## Hypothesis
-
-Rasterize a mesh scalar field → image → SAM2 point-click → mask → project back onto
-elements = a usable `Selection`, no training.
-
-## Pipeline
-
-```
-mesh + field ──rasterize──► field_raster (HxW, multi-channel) ──► SAM2(click) ──► mask_raster (HxW bool)
-mesh ──rasterize──► elemid_raster (HxW int label) ──project────────► Selection (element ids)
-```
-
-- **field_raster** — one channel per signal (bathymetry / curvature / size-fn),
- normalized to uint8, fed to SAM2 as a pseudo-image.
-- **elemid_raster** — paint each element polygon with its element id
- (`skimage.draw.polygon`) on the SAME grid. Gives exact back-projection, no
- centroid-sampling loss.
-- **project** — element selected iff ≥ 50% of its pixels fall inside the SAM2 mask.
-- **click map** — mesh `(x, y)` → pixel via a bbox affine transform.
-
-## Phases
-
-- **P0 — CPU, no model.** Build rasterize + elemid-label + back-project. Stub model =
- `skimage` watershed / flood-fill from the click on `field_raster`. Proves plumbing
- and sets the **deterministic baseline IoU**. No GPU, no checkpoint.
-- **P1 — real SAM2.** Swap stub → SAM2 image-predictor with a point prompt. Checkpoint
- via `huggingface_hub`. Same metrics.
-- **P2 — decision.** Resolution sweep + click-jitter robustness + field-channel
- ablation → apply the gate.
-
-## Fixtures
-
-- **Synthetic 2-basin (primary)** — deform a structured grid + analytic depth (two
- gaussians). Ground-truth region = a known basin → exact IoU target. Fully
- controllable; build this first.
-- chilmesh `annulus` / `donut` — plumbing sanity only.
-- One real (WNAT + bathymetry) if reachable; skip on 403.
-
-## Tests / metrics
-
-| test | assertion |
-|---|---|
-| `test_roundtrip_recall` | paint → mask(ALL) → project recovers ≥ 0.95 of elements @ 512 (res adequacy) |
-| `test_iou_vs_gt` | SAM2 click-in-basin: IoU(pred, true_basin) ≥ 0.70 on ≥ 2 cases |
-| `test_sam2_beats_baseline` | IoU(SAM2) > IoU(P0 watershed) — **the real question** |
-| `test_jitter_robust` | click ± 8 px, pairwise IoU ≥ 0.80 |
-| `test_res_sweep` | IoU vs {256, 512, 1024} → min stable res |
-| `test_channel_ablation` | bathy vs + curvature vs + size-fn → does an extra channel help |
-
-## Success gate
-
-All true → v2 rasterize-SAM2 demo is viable:
-
-- roundtrip recall ≥ 0.95 @ 512
-- SAM2 IoU ≥ 0.70 on ≥ 2 cases
-- jitter IoU ≥ 0.80
-- **SAM2 IoU > baseline IoU** — if this fails, SAM2 adds nothing; ship deterministic
- region-grow and kill the SAM2 track.
-
-## Files
-
-```
-prototype/
- raster.py mesh → field_raster + elemid_raster
- project.py mask_raster → element Selection
- model_stub.py P0 watershed baseline (same interface as SAM2 wrapper)
- model_sam2.py P1 SAM2 wrapper (HF checkpoint)
- fixtures.py synthetic 2-basin generator + ground-truth
- run_prototype.py CLI: mesh + click → Selection + metrics
- tests/test_*.py the metrics above
- REPORT.md IoU table per phase → gate verdict
-```
-
-## Deps (isolated extra `[proto]`)
-
-`numpy scipy scikit-image` (P0) · `torch sam2 huggingface_hub` (P1). SAM2-tiny on CPU
-is slow but demo-fine.
-
-## Risks (each caught by a test)
-
-- Tiny coastal elements < 1 pixel → resolution floor (`test_res_sweep`).
-- Scalar-field raster is out-of-distribution vs SAM2's natural-image training → may
- segment garbage. **This is the core risk; `test_iou_vs_gt` answers it.**
-- Click → pixel off-by-one (`test_jitter_robust` + roundtrip).
-
-## Start order
-
-P0 first (cheap, CPU, no model) — synthetic fixture + `raster.py` + `project.py` +
-`test_roundtrip_recall`. Eyeball numbers, then green-light P1.
diff --git a/pyproject.toml b/pyproject.toml
index e8d1773..fdfe9b3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "chilmesh"
-version = "1.2.1"
+version = "1.2.2"
description = "Fast 2D mesh library for hydrodynamic domains — Python API with optional C++ acceleration"
authors = [{name = "Dominik Mattioli"}]
license = {text = "PolyForm Noncommercial License 1.0.0"}
diff --git a/scripts/benchmark_all_backends.py b/scripts/benchmark_all_backends.py
index 4269d4b..84823e3 100644
--- a/scripts/benchmark_all_backends.py
+++ b/scripts/benchmark_all_backends.py
@@ -23,7 +23,7 @@ def _fmt(seconds: float, unit: str = "s") -> str:
def _locate_mesh(explicit: str | None) -> Path:
candidates = [
explicit,
- "/tmp/admesh-domains/registry_data/meshes/WNAT_Hagen.14",
+ "/tmp/valence-domains/registry_data/meshes/WNAT_Hagen.14",
"/tmp/WNAT_Hagen.14",
str(Path.home() / "WNAT_Hagen.14"),
]
diff --git a/scripts/benchmark_quadegg_variants.py b/scripts/benchmark_quadegg_variants.py
index 8683521..2d0dfa9 100755
--- a/scripts/benchmark_quadegg_variants.py
+++ b/scripts/benchmark_quadegg_variants.py
@@ -24,7 +24,7 @@
BACKENDS = ['edgemap', 'halfedge', 'quadegg']
OPERATIONS = ['fast_init', 'full_init', 'quality_analysis', 'query_latency']
-WNAT_HAGEN_PATH = "/tmp/admesh-domains/registry_data/meshes/WNAT_Hagen.14"
+WNAT_HAGEN_PATH = "/tmp/valence-domains/registry_data/meshes/WNAT_Hagen.14"
def measure_operation(op_name: str, op_fn, n_trials: int = 2) -> Tuple[float, float, float]:
diff --git a/scripts/benchmark_wnat_hagen.py b/scripts/benchmark_wnat_hagen.py
index a1f1cd8..aaa61a5 100644
--- a/scripts/benchmark_wnat_hagen.py
+++ b/scripts/benchmark_wnat_hagen.py
@@ -5,7 +5,7 @@
python scripts/benchmark_wnat_hagen.py [MESH_PATH] [--json OUTPUT.json]
If MESH_PATH is omitted the script looks for the mesh in the default
-ADMESH-Domains clone location (/tmp/admesh-domains/registry_data/meshes/).
+Valence clone location (/tmp/valence-domains/registry_data/meshes/).
Outputs a markdown table suitable for pasting into docs/BENCHMARK.md and,
optionally, a JSON file for CI archival / diff-over-time comparisons.
@@ -47,17 +47,17 @@ def _measure(label: str, fn, *, n: int = 1) -> tuple[str, float]:
def _locate_mesh(explicit: str | None) -> Path:
candidates = [
explicit,
- "/tmp/admesh-domains/registry_data/meshes/WNAT_Hagen.14",
- "/tmp/admesh-domains/WNAT_Hagen.14",
+ "/tmp/valence-domains/registry_data/meshes/WNAT_Hagen.14",
+ "/tmp/valence-domains/WNAT_Hagen.14",
str(Path.home() / "WNAT_Hagen.14"),
]
for c in candidates:
if c and Path(c).exists():
return Path(c)
raise FileNotFoundError(
- "WNAT_Hagen mesh not found. Clone ADMESH-Domains:\n"
- " git clone https://github.com/domattioli/ADMESH-Domains "
- "/tmp/admesh-domains\n"
+ "WNAT_Hagen mesh not found. Clone Valence:\n"
+ " git clone https://github.com/domattioli/Valence "
+ "/tmp/valence-domains\n"
"then re-run this script."
)
diff --git a/scripts/generate_wnat_showcase.py b/scripts/generate_wnat_showcase.py
index f099f76..268110c 100644
--- a/scripts/generate_wnat_showcase.py
+++ b/scripts/generate_wnat_showcase.py
@@ -8,7 +8,7 @@
- ``--mesh /path/to/WNAT_Hagen.14``, or
- environment variable ``WNAT_HAGEN_PATH``, or
-- ADMESH-Domains catalog at ``/tmp/admesh-domains/registry_data/meshes/WNAT_Hagen.14``.
+- Valence catalog at ``/tmp/valence-domains/registry_data/meshes/WNAT_Hagen.14``.
If none of those resolve, falls back to the largest bundled fixture
(``block_o``) so the script always produces an image; caption in the
@@ -30,8 +30,8 @@ def _candidate_paths() -> list[Path]:
env = os.environ.get("WNAT_HAGEN_PATH")
if env:
out.append(Path(env))
- out.append(Path("/tmp/admesh-domains/registry_data/meshes/WNAT_Hagen.14"))
- out.append(Path.home() / "admesh-domains/registry_data/meshes/WNAT_Hagen.14")
+ out.append(Path("/tmp/valence-domains/registry_data/meshes/WNAT_Hagen.14"))
+ out.append(Path.home() / "valence-domains/registry_data/meshes/WNAT_Hagen.14")
return out
diff --git a/src/chilmesh/CHILmesh.py b/src/chilmesh/CHILmesh.py
index 9981c17..4971dfc 100644
--- a/src/chilmesh/CHILmesh.py
+++ b/src/chilmesh/CHILmesh.py
@@ -29,14 +29,14 @@ class CHILmesh(CHILmeshPlotMixin):
A 2D mesh class supporting triangular, quadrilateral, and mixed-element meshes.
Supports multiple file formats (ADCIRC `.fort.14`, SMS `.2dm`) and integrates
- with ADMESH-Domains catalog. Provides mesh analysis (layer structure, element
+ with Valence catalog. Provides mesh analysis (layer structure, element
quality, interior angles), geometric operations (smoothing), and fast metadata
queries for bulk loading.
Key Features:
- Element Types: Triangles, quads, and mixed-element meshes (padded convention)
- Fast Init: Optional skeletonization for <2s bulk loading (compute_layers=False)
- - Metadata: Node count, element count, element type, bounding box (ADMESH-Domains compatible)
+ - Metadata: Node count, element count, element type, bounding box (Valence compatible)
- Entry Point: CHILmesh.from_admesh_domain() for catalog integration (duck-typed, zero deps)
- File I/O: Read ADCIRC `.fort.14` and SMS `.2dm` formats; roundtrip lossless
- Analysis: Layers (skeletonization), element quality, interior angles
@@ -84,7 +84,7 @@ class CHILmesh(CHILmeshPlotMixin):
>>> elem_ids = mesh.edge2elem(edge_id)
Examples:
- Load from ADMESH-Domains catalog:
+ Load from Valence catalog:
mesh = CHILmesh.from_admesh_domain(record)
Fast metadata query:
@@ -2262,9 +2262,9 @@ def copy( self ) -> "CHILmesh":
def admesh_metadata(self) -> dict:
"""
- Return a metadata dictionary compatible with the ADMESH-Domains catalog schema.
+ Return a metadata dictionary compatible with the Valence catalog schema.
- The returned dict contains all fields that ADMESH-Domains expects from a
+ The returned dict contains all fields that Valence expects from a
mesh record: node count, element count, element type, and bounding box.
Designed to be callable on a ``compute_layers=False`` mesh for fast bulk
loading.
@@ -2299,7 +2299,7 @@ def admesh_metadata(self) -> dict:
@classmethod
def from_admesh_domain(cls, record: object, compute_layers: bool = True, compute_adjacencies: Opt[bool] = None) -> "CHILmesh":
"""
- Construct a CHILmesh from an ADMESH-Domains catalog record.
+ Construct a CHILmesh from an Valence catalog record.
The catalog record is duck-typed: any object with ``connectivity``
(ndarray, n_elems × 3|4) and ``points`` (ndarray, n_verts × 2|3)
@@ -2333,7 +2333,7 @@ def from_admesh_domain(cls, record: object, compute_layers: bool = True, compute
if not filepath.exists():
raise FileNotFoundError(
f"File not found: {filepath}. "
- "If using ADMESH-Domains, call mesh_record.load() first."
+ "If using Valence, call mesh_record.load() first."
)
record_type = getattr(record, "type", None)
diff --git a/src/chilmesh/bridge.py b/src/chilmesh/bridge.py
index b8939ab..7b33066 100644
--- a/src/chilmesh/bridge.py
+++ b/src/chilmesh/bridge.py
@@ -1,7 +1,7 @@
"""Bridge adapters for downstream projects.
This module provides convenient interfaces for integrating CHILmesh
-with MADMESHR, ADMESH, and ADMESH-Domains. Each adapter adds
+with MADMESHR, ADMESH, and Valence. Each adapter adds
domain-specific convenience methods while delegating to the base
CHILmesh public API (CAI).
"""
@@ -270,10 +270,10 @@ def get_element_angles_summary(self, elem_ids: Optional[List[int]] = None) -> Di
class MeshAdapterForADMESHDomains:
"""
- Adapter for ADMESH-Domains multi-domain handling.
+ Adapter for Valence multi-domain handling.
Provides convenience methods for domain-level queries and
- boundary extraction specific to ADMESH-Domains.
+ boundary extraction specific to Valence.
Example:
>>> from chilmesh import CHILmesh
diff --git a/tests/TESTING.md b/tests/TESTING.md
index 23db164..1ec3381 100644
--- a/tests/TESTING.md
+++ b/tests/TESTING.md
@@ -153,7 +153,7 @@ pytest -vv -s tests/test_smoothing.py::TestTriangleSmoother::test_fem_smoother_t
## Known Issues & Skipped Tests
**52 tests skipped (fast PR mode).** Skips are environment- or geometry-conditional, not failures:
-- **External MATLAB parity** (`test_skeletonization_matlab_parity_external.py`): require `admesh-domains` + large mesh files not bundled. Run manually: `pip install admesh-domains && pytest tests/test_skeletonization_matlab_parity_external.py -v`
+- **External MATLAB parity** (`test_skeletonization_matlab_parity_external.py`): require `valence-domains` + large mesh files not bundled. Run manually: `pip install valence-domains && pytest tests/test_skeletonization_matlab_parity_external.py -v`
- **C++ backend equivalence** (`test_backend_equivalence.py`): skipped when `chilmesh_cpp` extension is not built (`pip install -e ".[cpp]"` or build the extension to exercise these).
- **Geometry-conditional** (e.g. `test_spatial_indexing.py`): point-location cases that don't apply to holed fixtures (annulus/donut/block_o).
diff --git a/tests/test_admesh_metadata.py b/tests/test_admesh_metadata.py
index 5f3937c..5b3b95d 100644
--- a/tests/test_admesh_metadata.py
+++ b/tests/test_admesh_metadata.py
@@ -5,7 +5,7 @@
class TestADMESHMetadataAccuracy:
- """Test that admesh_metadata() returns accurate, ADMESH-Domains-compatible values."""
+ """Test that admesh_metadata() returns accurate, Valence-compatible values."""
@pytest.mark.parametrize("fixture_name", ["annulus", "donut", "block_o", "structured", "quad_2x2"])
def test_metadata_completeness(self, fixture_name):
diff --git a/tests/test_bridge_adapters.py b/tests/test_bridge_adapters.py
index 8fd36e4..3d72f7d 100644
--- a/tests/test_bridge_adapters.py
+++ b/tests/test_bridge_adapters.py
@@ -1,6 +1,6 @@
"""Integration tests for bridge adapters.
-Tests verify that bridge adapters (MADMESHR, ADMESH, ADMESH-Domains)
+Tests verify that bridge adapters (MADMESHR, ADMESH, Valence)
work correctly with realistic downstream workflows and all mesh fixtures.
"""
@@ -215,7 +215,7 @@ def _load_fixture(self, name):
class TestADMESHDomainsBridge:
- """Integration tests for ADMESH-Domains bridge adapter."""
+ """Integration tests for Valence bridge adapter."""
@pytest.mark.parametrize("fixture_name", ["annulus", "donut", "structured"])
def test_adapter_initialization(self, fixture_name):
@@ -392,7 +392,7 @@ def test_admesh_quality_assessment_workflow(self):
assert angles["elements_with_acute"] >= 0
def test_admesh_domains_multi_domain_setup(self):
- """Simulate ADMESH-Domains domain initialization."""
+ """Simulate Valence domain initialization."""
from pathlib import Path
mesh = CHILmesh.read_from_fort14(
diff --git a/tests/test_metadata_validation.py b/tests/test_metadata_validation.py
index 6117463..09948a8 100644
--- a/tests/test_metadata_validation.py
+++ b/tests/test_metadata_validation.py
@@ -133,7 +133,7 @@ def test_bounding_box_precision(self):
def test_metadata_via_from_admesh_domain_for_validation(self):
"""Test contributor validation via from_admesh_domain entry point."""
- # Contributor receives a mesh record from ADMESH-Domains
+ # Contributor receives a mesh record from Valence
fixture_path = chilmesh.examples.fixture_path("quad_2x2.fort.14")
record = SimpleNamespace(filename=str(fixture_path), type="ADCIRC")
diff --git a/tests/test_skeletonization_matlab_parity.py b/tests/test_skeletonization_matlab_parity.py
index 58f97b3..1844a27 100644
--- a/tests/test_skeletonization_matlab_parity.py
+++ b/tests/test_skeletonization_matlab_parity.py
@@ -12,8 +12,8 @@
WNAT_Hagen, WNAT_Onur, WNAT_Test, or WNAT_NC_inundation_v6c)
- Wetting-and-drying test mesh: 15 layers
-(These meshes are not bundled with CHILmesh; they live in the ADMESH-Domains
-catalog. Add them to EXPECTED below once the loader for ADMESH-Domains meshes
+(These meshes are not bundled with CHILmesh; they live in the Valence
+catalog. Add them to EXPECTED below once the loader for Valence meshes
exposes a programmatic fetch.)
"""
from __future__ import annotations
diff --git a/tests/test_skeletonization_matlab_parity_external.py b/tests/test_skeletonization_matlab_parity_external.py
index e2c7544..ba5f573 100644
--- a/tests/test_skeletonization_matlab_parity_external.py
+++ b/tests/test_skeletonization_matlab_parity_external.py
@@ -1,12 +1,12 @@
-"""MATLAB-parity tests for skeletonization on external (ADMESH-Domains) meshes.
+"""MATLAB-parity tests for skeletonization on external (Valence) meshes.
This is a sibling of ``tests/test_skeletonization_matlab_parity.py`` that pins
-expected layer counts for meshes from the external ADMESH-Domains catalog,
+expected layer counts for meshes from the external Valence catalog,
rather than the bundled fixtures.
Why a separate file? Bundled-fixture parity is a fast, always-on guardrail
(every CI push runs it). External-mesh parity requires installing the
-``admesh-domains`` package and downloading mesh files, so it is opt-in. Keeping
+``valence-domains`` package and downloading mesh files, so it is opt-in. Keeping
the two concerns in separate files lets the fast tests stay cheap while still
documenting the broader correctness expectation for the maintainer.
@@ -19,7 +19,7 @@
Reference values were captured from the original QuADMesh+ ``meshLayers``
algorithm in ``00_CHILMesh_Class/@CHILmesh/CHILmesh.m``. The seven values tagged
"F12 captured 2026-05-23" were produced by running that MATLAB class under
-GNU Octave 8.4 on the ADMESH-Domains catalog meshes (connectivity + points fed
+GNU Octave 8.4 on the Valence catalog meshes (connectivity + points fed
to the 2-arg ``CHILmesh(ConnectivityList, Points)`` constructor, bypassing the
MATLAB ``readFort14`` reader). The harness was validated first against the three
already-known references — delaware-bay@default (17), lake-erie@5k (17),
@@ -29,7 +29,7 @@
.. code-block:: bash
- pip install admesh-domains
+ pip install valence-domains
CHILMESH_RUN_EXTERNAL_PARITY=1 python -m pytest \\
tests/test_skeletonization_matlab_parity_external.py -v
@@ -55,7 +55,7 @@
# Maintainer-provided reference layer counts from external MATLAB ``meshLayers`` runs.
-# Each entry is keyed by ADMESH-Domains identifier (catalog mesh name + variant tag).
+# Each entry is keyed by Valence identifier (catalog mesh name + variant tag).
# Values:
# - int: exact n_layers known
# - (int, int) tuple: range (lo, hi) when source mesh variant is unconfirmed
@@ -88,11 +88,11 @@ def _admesh_domains_available() -> bool:
def _load_mesh(catalog_id: str):
- """Resolve an ADMESH-Domains catalog ID to a loaded CHILmesh.
+ """Resolve an Valence catalog ID to a loaded CHILmesh.
Catalog ID convention: ``"@-v"``, e.g.
``"italy@default-v1"``. The exact resolution depends on the
- ``admesh-domains`` Python loader; we use a placeholder that will be wired
+ ``valence-domains`` Python loader; we use a placeholder that will be wired
up once the loader API is finalized.
"""
import admesh_domains # type: ignore
@@ -101,7 +101,7 @@ def _load_mesh(catalog_id: str):
variant, _, version = variant_with_version.rpartition("-v")
record = admesh_domains.get(mesh_name, variant=variant, version=int(version))
- record.load() # ADMESH-Domains lazy-load contract
+ record.load() # Valence lazy-load contract
from chilmesh import CHILmesh
return CHILmesh.from_admesh_domain(record, compute_layers=True)
@@ -110,21 +110,21 @@ def _load_mesh(catalog_id: str):
_RUN_EXTERNAL = bool(os.environ.get("CHILMESH_RUN_EXTERNAL_PARITY"))
_SKIP_REASON_EXTERNAL = (
"Set CHILMESH_RUN_EXTERNAL_PARITY=1 to run the external MATLAB-parity tests "
- "against the ADMESH-Domains catalog. Requires `pip install admesh-domains`."
+ "against the Valence catalog. Requires `pip install valence-domains`."
)
@pytest.mark.skipif(not _RUN_EXTERNAL, reason=_SKIP_REASON_EXTERNAL)
@pytest.mark.skipif(
not _admesh_domains_available(),
- reason="admesh-domains package not installed; `pip install admesh-domains`",
+ reason="valence-domains package not installed; `pip install valence-domains`",
)
@pytest.mark.parametrize(
"catalog_id,expected",
[(cid, exp) for cid, exp in MATLAB_REFERENCE_LAYER_COUNTS.items() if exp is not None],
)
def test_layer_count_matches_matlab_reference(catalog_id: str, expected) -> None:
- """For each ADMESH-Domains mesh with a known MATLAB ``n_layers``,
+ """For each Valence mesh with a known MATLAB ``n_layers``,
the Python port must produce the same value (or fall in the documented range).
"""
mesh = _load_mesh(catalog_id)
@@ -145,17 +145,17 @@ def test_layer_count_matches_matlab_reference(catalog_id: str, expected) -> None
@pytest.mark.skipif(not _RUN_EXTERNAL, reason=_SKIP_REASON_EXTERNAL)
@pytest.mark.skipif(
not _admesh_domains_available(),
- reason="admesh-domains package not installed; `pip install admesh-domains`",
+ reason="valence-domains package not installed; `pip install valence-domains`",
)
def test_uncovered_meshes_have_fixme() -> None:
- """Meta-test: every ADMESH-Domains entry without a captured MATLAB count
+ """Meta-test: every Valence entry without a captured MATLAB count
must remain in the table with ``None`` so it is visible as a FIXME during
review.
"""
uncovered = [k for k, v in MATLAB_REFERENCE_LAYER_COUNTS.items() if v is None]
if uncovered:
pytest.skip(
- f"{len(uncovered)} ADMESH-Domains meshes lack a MATLAB reference layer "
+ f"{len(uncovered)} Valence meshes lack a MATLAB reference layer "
f"count. Capture via QuADMesh+ and add to MATLAB_REFERENCE_LAYER_COUNTS: "
f"{', '.join(uncovered)}"
)
From 81dbeb806f87056d3a0a1c519b78d8c54f82199a Mon Sep 17 00:00:00 2001
From: domattioli
Date: Mon, 15 Jun 2026 21:03:43 -0400
Subject: [PATCH 32/32] move specs to .specify
---
.../001-boundary-type-seed-skeletonize/checklists/requirements.md | 0
.../specs}/001-boundary-type-seed-skeletonize/spec.md | 0
2 files changed, 0 insertions(+), 0 deletions(-)
rename {specs => .specify/specs}/001-boundary-type-seed-skeletonize/checklists/requirements.md (100%)
rename {specs => .specify/specs}/001-boundary-type-seed-skeletonize/spec.md (100%)
diff --git a/specs/001-boundary-type-seed-skeletonize/checklists/requirements.md b/.specify/specs/001-boundary-type-seed-skeletonize/checklists/requirements.md
similarity index 100%
rename from specs/001-boundary-type-seed-skeletonize/checklists/requirements.md
rename to .specify/specs/001-boundary-type-seed-skeletonize/checklists/requirements.md
diff --git a/specs/001-boundary-type-seed-skeletonize/spec.md b/.specify/specs/001-boundary-type-seed-skeletonize/spec.md
similarity index 100%
rename from specs/001-boundary-type-seed-skeletonize/spec.md
rename to .specify/specs/001-boundary-type-seed-skeletonize/spec.md