JPL Power-of-Ten Audit — srmech C library¶
Standard: Holzmann, G. J. (2006). "The Power of Ten — Rules for Developing Safety Critical Code." IEEE Computer 39(6), 95-99.
Audited at: v0.1.1rc8 (Task #201 Phase B6); reentrancy delta re-audited at v0.6.0rc5 (#772).
Scope: the source files under c/src/*.c and the public header
c/include/srmech.h. The original Phase B6 audit covered the three
baseline files below; the per-class C surfaces added since
(srmech_cyclic.c, srmech_laplacian.c, srmech_primes.c,
srmech_rational.c, srmech_kepler.c, srmech_hdc.c,
srmech_dispatch.c, srmech_catalog.c, srmech_template.c,
srmech_tlv.c, srmech_search.c, srmech_cascade.c,
srmech_bus.c, srmech_parallel.c, srmech_kuramoto.c,
srmech_platform.c, srmech_json.c, srmech_genome.c — the PAL, rc4,
the OS sibling of the srmech_simd.c HAL, plus the §41 genome-persistence
JSON mirror and the §41 genome-persistence disk surface itself;
srmech_sedenion.c and srmech_cd_register.c — the DENSE reversibility
gate and the general N-slot Cayley–Dickson address layer (rc464 #T1188
removed the 16-slot half of the first), the latter added in
v0.9.0rc297 (#934) with no Rule-5 exemption: its one predicate helper
cdr_dim_ok asserts the CAP's own shape rather than being exempted,
so a future SRMECH_CD_MAX_DIM change that broke the power-of-two
invariant would fire instead of silently admitting a dim with no basis
table) are held to the same rules by the mechanical ratchet
tests/test_jpl_audit.py.
c/src/srmech_meta.c— version + ABI accessors (Phase B3).c/src/srmech_sha256.c— FIPS 180-4 SHA-256 (Phase B3).c/src/srmech_ndjson.c— streaming NDJSON line reader (Phase B4).
Discipline: Same pattern as ephemerides-spectral's
JPL_AUDIT.md.
This document is the audit record. The SRMECH_PEDANTIC=ON CMake
build (Rule 10) runs in CI as an always-on ratchet. The Python-side
test tests/test_jpl_audit.py (Phase B6 ship) pins the
mechanically-detectable counts as one-way ratchets — they can go
DOWN but not UP.
Summary¶
| Rule | Description | Baseline | Status |
|---|---|---|---|
| 1 | No goto / setjmp / longjmp / recursion | goto 0; recursion 9 | ⚠️ partial — goto/setjmp/longjmp clean; 9 recursion cycles under a seeded down-only ratchet (rc441, #T1148; see Rule 1 below) |
| 2 | All loops have fixed upper bounds | 0 | ✅ pass |
| 3 | No dynamic allocation after init | 0 | ✅ pass |
| 4 | Functions ≤ 60 lines | 0 | ✅ pass (was 1; fixed in this ship by extracting srmech_ndjson_process_chunk) |
| 5 | ≥ 2 assertions per non-trivial function | 0 | ✅ pass (trivial accessors exempt per documented rationale; inline arithmetic helpers exempt) |
| 6 | Smallest possible scope for data | 0 | ✅ pass |
| 7 | Return values checked / parameters validated | 0 | ✅ pass |
| 8 | Limited preprocessor (no multiline macros) | 0 | ✅ pass |
| 9 | Pointer dereference depth ≤ 1; no function ptrs* | deref 0; fn-ptr 10 | ⚠️ partial — deref depth clean; 10 function-pointer declarator sites under a seeded down-only ratchet (rc452; see Rule 9 below) |
| 10 | Compile clean at most-pedantic warning level | 0 | ✅ pass (SRMECH_PEDANTIC=ON CMake + CI matrix) |
* Rule 9's function-pointer half was UNMEASURED until rc452, and the "one deliberate deviation" this line used to claim was false long before that — see the measured census in the Rule 9 section below.
Headline (corrected at v0.9.0rc441 #T1148, and again at rc452): Eight of
the ten rules are clean. Rule 1 is PARTIAL — the goto/setjmp/longjmp half
is clean, the recursion half carries a measured population of 9 depth-bounded
cycles under a down-only ratchet. Rule 9 is PARTIAL — the
dereference-depth half is clean, the function-pointer half carries a measured
population of 10 declarator sites under a seeded down-only ratchet (this
document said "one deliberate deviation" while the tree carried 12; rc452
measured it, drained 4, and gated the rest).
This headline read "All ten JPL Power-of-Ten rules satisfied" through rc440.
It was not true, and the reason it survived is worth recording: the Rule 1
ratchet never looked for recursion, so nothing contradicted the sentence. Two
further blind spots were measured and closed in the same rc — the function
scanner counted braces inside '{' / '}' char literals, which ran its
brace depth off the end of the file and reported three real functions as
lines=1 (invisible to Rule 4) while their assert counts ran away to
458 / 456 / 35 (vacuously satisfying Rule 5). A rule whose
instrument cannot return a finding is not a rule that passes; it is a rule
nobody measured. See Rule 1, Rule 4 and Rule 5 below.
Rule 1 — Restrict control flow¶
"Restrict all code to very simple control flow constructs — do not use goto statements, setjmp or longjmp constructs, and direct or indirect recursion."
Violations: goto/setjmp/longjmp — 0. Recursion — 9 cycles, seeded down-only ratchet.¶
grep -n "goto" c/src/*.c→ no matches.grep -n "setjmp\|longjmp" c/src/*.c c/include/srmech.h→ no matches.
⚠️ Corrected at v0.9.0rc441 (#T1148). Through rc440 this section read
"Manual review: no function in srmech calls itself directly or indirectly"
and claimed a clean pass. That was false, and nothing could catch it:
tests/test_jpl_audit.py::test_rule_1_no_goto grepped for
goto|setjmp|longjmp and did not look for recursion at all, so the half
of Rule 1 that this line asserted was the half no instrument measured. The
claim rested on a manual review that had long since gone stale — the C tree
grew from three files to 138 — and a surface with no gate does not trickle,
it is simply believed absent.
The first mechanical census (rc441, whole tree: 138 files, 3092 functions) found nine recursion cycles — one direct, eight mutual:
| Cycle | File | Kind | Depth bound |
|---|---|---|---|
iv_emit_node |
srmech_invoke.c |
direct | explicit depth argument |
dsl_run_combinator ↔ dsl_run_loop ↔ dsl_run_stage_array |
srmech_dsl_chain_run.c |
mutual | chain nesting the descriptor declares |
dv_from_desc ↔ dv_from_list |
srmech_dsl_chain_run.c |
mutual | DV_MAX_DEPTH |
dv_to_desc ↔ dv_list_to_desc |
srmech_dsl_chain_run.c |
mutual | DV_MAX_DEPTH |
mm_from_json ↔ mm_from_json_array ↔ mm_from_json_object |
srmech_mcp_marshal.c |
mutual | JSON nesting cap |
mm_serialise ↔ mm_serialise_dict ↔ mm_serialise_list |
srmech_mcp_marshal.c |
mutual | JSON nesting cap |
toml_to_json ↔ toml_tbl_to_json ↔ toml_arr_to_json |
srmech_dsl_chain_run.c |
mutual | TOML nesting cap |
toml_finalize_atable ↔ toml_finalize_btable ↔ toml_finalize_bvalue |
srmech_toml.c |
mutual | TOML nesting cap |
toml_parse_array ↔ toml_parse_inline_table ↔ toml_parse_value |
srmech_toml.c |
mutual | TOML nesting cap |
Every one is depth-bounded in the source, which is why the population was tolerable while it was invisible. Rule 1's objection is nevertheless to the construct: a bound enforced by a parameter rather than by the call structure is a bound a later edit can move, and on a fixed stack the cost of being wrong is not a wrong answer but a smashed frame.
Enforcement is now mechanical and one-way — test_rule_1_no_new_recursion
is strict on the decidable class (any cycle outside RULE_1_RECURSION_SEEDED,
including an existing cycle that gained a member, fails outright) and
down-only on the count via CEIL_RULE_1_RECURSION = 9.
test_rule_1_recursion_ceiling_is_not_slack keeps the ceiling equal to the
live measurement so no unclaimed headroom accumulates, and
test_rule_1_recursion_detector_is_not_vacuous asserts the detector still
returns a nonzero reading — an instrument that cannot report a finding is not
a measurement.
⚠️ Pass on goto/setjmp/longjmp; recursion is a RECORDED DEBT under a down-only ratchet, not a pass. Unwinding a cycle to an explicit bounded stack lowers the ceiling.
Rule 2 — All loops must have fixed bounds¶
"All loops must have a fixed upper-bound. It must be trivially possible for a checking tool to prove statically that a preset upper-bound on the number of iterations of a loop cannot be exceeded."
Violations: 0¶
Every loop in srmech's C has either a compile-time constant bound or a caller-supplied size_t bound:
| Loop | Bound | Source |
|---|---|---|
sha256 schedule prep for (i = 0; i < 16; i++) |
16 | FIPS 180-4 block size |
sha256 schedule extend for (i = 16; i < 64; i++) |
64 | FIPS 180-4 round count |
sha256 rounds for (i = 0; i < 64; i++) |
64 | FIPS 180-4 round count |
sha256 state→hex for (i = 0; i < 8; i++) |
8 | SHA-256 produces 8 words |
sha256 hex inner-out[base+0..7] |
8 unrolled | hex chars per word |
| sha256 hash full blocks | data_len / 64 |
caller's size_t data_len |
ndjson chunk-by-chunk while (!eof_reached) |
file size | bounded by fread returning 0 at EOF |
ndjson byte loop for (i = 0; i < n_read; i++) |
n_read ≤ 64 KiB |
SRMECH_NDJSON_CHUNK_BYTES |
The while (!eof_reached) deserves note: the explicit eof_reached
flag + break on zero-read makes the termination condition
mechanically obvious. We never use while(1) or for(;;).
✅ Pass.
Rule 3 — No dynamic allocation after init¶
"Do not use dynamic memory allocation after initialization."
Violations: 0¶
grep -n "malloc\|calloc\|realloc\|free" c/src/*.c→ no matches.grep -n "alloca\|vla\|variable.length.array" c/src/*.c→ no matches.- All buffers are either:
- Stack-allocated, compile-time-constant-sized (e.g.
uint8_t chunk[SRMECH_NDJSON_CHUNK_BYTES]). - Thread-local static, compile-time-constant-sized (e.g.
the 1 MiB NDJSON line-assembly buffer in
srmech_ndjson_iterand the ~1 MiB Hermitian-eigendecomp working matrixHworkinsrmech_hermitian_eigendecompose— bothstatic SRMECH_THREAD_LOCAL). - Caller-supplied (e.g.
char *out_hextosrmech_sha256_hex;double *workspacetosrmech_hermitian_eigendecompose_ws).
Static-scope scratch is itself a legitimate Rule-3 method — the
buffer has static storage duration, never touches the allocator, and
is sized at compile time. The #772 conversion of the two former
shared-static scratch buffers (srmech_ndjson.c's g_line_buf
and srmech_laplacian.c's Hwork) was therefore a reentrancy
trade, NOT a Rule-3 fix: they were already Rule-3-clean. The change
adds the SRMECH_THREAD_LOCAL qualifier (and, for the eigendecomp, a
new srmech_hermitian_eigendecompose_ws entry taking a caller-
supplied workspace) so the buffers are per-thread / caller-owned
rather than process-wide-shared. This makes the entire op surface
safe to drive concurrently (the #771 multi-threaded plugin) while
keeping the large (~1 MiB) buffers OFF the stack — _Thread_local /
__declspec(thread) static storage is both Rule-3-clean and
reentrant-across-threads. No malloc was introduced.
Generated pure-DATA translation units (GENERATED_DATA_FILES in
tests/test_jpl_audit.py; rc184: srmech_tool_registry.c) are excluded
from the Rule 3 (and Rule 1) string-content regex scans. They contain
ZERO executable code — only a const srmech_tool_entry_t[] table — but
their string VALUES are the (English) ToolEntry summaries, which contain
prose tokens like free( inside a data string. The regex cannot tell a
data string from code, so it would mis-flag the prose. These files remain
in-scope for the function-scan rules (Rule 4 / Rule 5), where they find
ZERO functions — a guard that the file stays data-only. Regenerate with
c/tools/gen_tool_registry.py.
✅ Pass.
Rule 4 — Functions ≤ 60 lines¶
"No function should be longer than what can be printed on a single sheet of paper in a standard format with one line per statement and one line per declaration. Typically, this means no more than about 60 lines of code per function."
Violations: 0 (was 1 — fixed in Phase B6)¶
⚠️ Scanner blindness closed at v0.9.0rc441 (
#T1148). This count was measured by a scanner that could not see three of the functions it was counting._scan_functionslocates a function's end by counting{and}per line — and it counted them inside'{'/'}'char literals too. In a JSON or TOML scanner written in C those literals are the whole point of the code, so the brace depth never returned to zero, the scan ran to end-of-file, and the function was reported aslines=1: comfortably under the 60-line limit by an accident of arithmetic rather than by being short.Measured on rc440:
genome_json_object_spanandgenome_find_attest_span(srmech_genome.c) andtoml_parse_value(srmech_toml.c) all reportedlines=1. Their true lengths are 24, 20 and 35 — all genuinely under 60, so the verdict was right; the measurement was not, and a measurement that is right by luck cannot be relied on for the next function that lands in one of those files.The fix masks comments, string literals and char literals before counting (
_mask_c_literals). Whole-tree effect (138 files, 3092 functions): the detected function set is unchanged (0 added, 0 removed), exactly five functions change their numbers, and no new Rule 4 violation appears — the fourth change istoml_parse_inline_table, whose real length is 58, not the 54 the old counter reported. That one had four lines of headroom it did not actually have.
Per-function line counts (definition lines, body brace to body
brace; counted by awk script in tests/test_jpl_audit.py):
| Function | Lines | Status |
|---|---|---|
srmech_version |
4 | ✅ |
srmech_abi_version |
9 | ✅ |
| sha256 inline helpers (ror, ch, maj, sigmas) | 4-7 each | ✅ |
srmech_sha256_compress |
54 | ✅ |
srmech_sha256_state_to_hex |
20 | ✅ |
srmech_sha256_hex |
57 | ✅ |
srmech_ndjson_emit |
20 | ✅ |
srmech_ndjson_process_chunk |
46 | ✅ (gained char *line_buf param in #772) |
srmech_ndjson_iter |
55 | ✅ (thread-local line_buf added in #772; still < 60) |
srmech_hermitian_run_sweeps |
27 | ✅ (extracted from eigendecompose in #772) |
srmech_hermitian_eigendecompose_ws |
44 | ✅ (#772 reentrant caller-workspace entry) |
srmech_hermitian_eigendecompose |
19 | ✅ (#772 thin thread-local-workspace wrapper) |
srmech_parallel__stream_transform |
20 | ✅ (rc6 Klein-4 T_s involution; #771/#778) |
srmech_parallel__run_sector |
22 | ✅ (rc6 per-sector worker, disjoint slices) |
srmech_parallel__validate |
25 | ✅ (rc6 arg-validation + post-validation invariants) |
srmech_parallel__serial |
13 | ✅ (rc6 thread-less serial fallback) |
srmech_parallel__job_run |
7 | ✅ (rc6 threaded-job shim) |
srmech_parallel__thread_posix |
7 | ✅ (rc6 pthread entry trampoline) |
srmech_parallel__threaded (POSIX) |
31 | ✅ (rc6 pthread spawn-join, [4] stack handles) |
srmech_parallel__thread_win |
7 | ✅ (rc6 CreateThread entry trampoline) |
srmech_parallel__threaded (Win) |
33 | ✅ (rc6 CreateThread spawn / WaitForMultipleObjects) |
srmech_cascade_parallel_sector_dispatch |
19 | ✅ (rc6 public four-sector entry) |
srmech_kuramoto__coupling_sum |
9 | ✅ (rc9 O(n²) coupling sum factored out; #778) |
srmech_cascade_kuramoto_step_f64 |
17 | ✅ (rc9 public Kuramoto forward-Euler step) |
srmech_kuramoto__general_sum |
12 | ✅ (rc14 generalised Σ_j A_ij·sin(θ_j−θ_i−α); §11.1) |
srmech_cascade_kuramoto_step_general_f64 |
22 | ✅ (rc14 public Kuramoto-Sakaguchi step: adjacency + α + pinning) |
srmech_loop__mul2 |
7 | ✅ (rc7 complex dim-2 Cayley-Dickson product) |
srmech_loop__mul4 |
17 | ✅ (rc7 quaternion dim-4 product via two mul2 levels) |
srmech_loop__mul8 |
18 | ✅ (rc7 octonion dim-8 product via two mul4 levels) |
srmech_loop_bind_f64 |
12 | ✅ (rc7 public octonion loop_bind; n==8) |
srmech_loop_conj_f64 |
14 | ✅ (rc7 public octonion conjugate) |
srmech_loop_inv_f64 |
21 | ✅ (rc7 public Moufang inverse x̄/⟨x,x⟩) |
srmech_cross7_f64 |
13 | ✅ (rc7 public 7-D cross product Im(loop_bind)) |
srmech_g2_three_form_f64 |
20 | ✅ (rc7 public G2 calibration 3-form scalar) |
srmech_autocorrelation_f64 |
13 | ✅ (rc8 Class-L circular autocorrelation; direct O(n²) sum) |
srmech_sha256_batch + srmech_sha256_batch.c internals |
≤45 | ✅ (rc10 F292 N-way SIMD SHA-256: fill_block / load_words / compress1 / digest1 / compress4 / hash4 / compress8 / hash8 / batch — each ≤45 lines; SIMD sigma ops are SINGLE-line macros per Rule 8) |
srmech_simd.c HAL (srmech_simd_has_avx2 / _avx / _sse2 / _shani / srmech_simd_tier) |
≤22 | ✅ (rc11 SIMD optimize-path HAL — cpuid/xgetbv probes + env-tier clamp; the ONE home of the machine-specific detection bits; rc18 adds the _shani leaf7-bit29 probe) |
srmech_sha256_shani + srmech_sha256_shani.c internals |
≤55 | ✅ (rc18 F292 SHA-NI single-stream: fill_block / store_digest / scalar ror/load_words/compress1/digest / pack / unpack / rounds_0_15 / group_full / group_final / compress_block / digest_shani / public entry — each ≤55 lines; the 64 SHA-NI rounds factored via a cyclic message-register group; SHA-NI ops are SINGLE-line macros per Rule 8) |
srmech_loop_bind_hd_f64 + srmech_loopbind_hd.c internals |
≤45 | ✅ (rc11 F292 N-way SIMD block-octonion HD bind: vmul2/4/8 a-/s- kernels + avx/sse2 group loops + public entry — each ≤45 lines; SIMD ops are SINGLE-line macros per Rule 8) |
Fix shipped in this audit pass¶
srmech_ndjson_iter at rc6 was 76 lines (Rule 4 violation). The
chunk-byte-loop body was extracted into a new static helper
srmech_ndjson_process_chunk along its natural seam (per-chunk
state update). Behaviour byte-identical pre/post; the 18 pytest
parity tests in tests/test_native_ndjson.py re-ran clean against
the refactored code.
✅ Pass.
Rule 5 — ≥ 2 assertions per function¶
"The assertion density of the code should average to a minimum of two assertions per function. Assertions are used to check for anomalous conditions that should never happen in real-life executions. Assertions must always be side-effect free."
Violations: 0 (with documented exemptions)¶
⚠️ The worse half of the rc441 scanner blindness landed HERE. The same runaway brace scan described under Rule 4 did not merely mis-report a length — it kept counting
assert(all the way to end-of-file. So the three blind functions reported 458, 456 and 35 assertions respectively, and Rule 5's>= 2floor was satisfied vacuously in exactly the places the scanner had lost its place. A floor cleared by a number the instrument invented is not a floor.Post-fix the three report 2 assertions each, which genuinely clears the floor. One further correction fell out:
srmech_bigint_pow_bounddrops from 3 to 2 — one of itsassert(occurrences was inside a comment. Still ≥ 2, so no new Rule 5 violation appears and the strict-zero ratchet stays strict-zero. Both counts are now measured over literal-masked text.
Per-function assertion counts:
| Function | Asserts | Notes |
|---|---|---|
srmech_version |
0 | EXEMPT — trivial accessor returning a constant string. No preconditions to assert. |
srmech_abi_version |
0 | EXEMPT — trivial accessor returning a constant integer. No preconditions to assert. |
srmech_plat_has_threads |
0 | EXEMPT — PAL (srmech_platform.c, rc4) trivial accessor: returns a compile-time 1/0 (is a threading backend present?). No state/preconditions to assert. The other PAL fns (srmech_plat_thread_spawn/join) carry ≥2 asserts. |
srmech_plat_has_streams |
0 | EXEMPT — PAL (srmech_platform.c, rc5) trivial accessor: compile-time 1/0 (is a stream-IPC backend present?). No state to assert; the stream listen/accept/connect/read/write fns carry ≥2 asserts. |
srmech_plat_has_filesystem |
0 | EXEMPT — PAL (srmech_platform.c, rc161) trivial accessor: compile-time 1/0 (is a filesystem backend present?). No state to assert; the file read/write/size fns carry ≥2 asserts. |
srmech_plat_has_stdio |
0 | EXEMPT — PAL (srmech_platform.c, rc186) trivial accessor: compile-time 1/0 (is a stdin/stdout backend present?). No state to assert; the srmech_plat_stdin_read / srmech_plat_stdout_write fns carry ≥2 asserts (the MCP stdio loop's consumer). |
srmech_plat_has_dirlist |
0 | EXEMPT — PAL (srmech_platform.c, rc163) trivial accessor: compile-time 1/0 (is a directory-iteration backend present?). No state to assert; the dir open/next/close fns carry ≥2 asserts. |
srmech_plat_has_tcp |
0 | EXEMPT — PAL (srmech_platform.c, rc194) trivial accessor: compile-time 1/0 (is a TCP backend present? POSIX-first). No state to assert; the srmech_plat_tcp_listen/accept/read_some/write_all/server_close fns carry ≥2 asserts (the MCP HTTP+SSE server's consumer). |
srmech_plat_sleep_ms |
1 | EXEMPT — PAL (srmech_platform.c, rc194) trivial OS-timer wrapper: the POSIX (nanosleep) / Windows (Sleep) bodies carry ≥2 asserts; the bare-metal stub is a 1-assert no-op with no pointer/bounds invariant, a second would be a tautology. Consumer: the MCP HTTP+SSE keepalive scanner. |
srmech_mcp_sse_port |
1 | EXEMPT — rc194 (srmech_mcp_sse.c): a trivial const accessor returning the running server's bound TCP port (NULL-graceful → 0); already asserts h != NULL; no state to assert beyond the guard, like the exempt srmech_version / PAL has_* accessors. |
| sha256 inline helpers | 0 | EXEMPT — static inline arithmetic primitives (ror32, ch, maj, big/small sigma). Per the rule's spirit (anomalous conditions in real-life), 4-line bit-rotation helpers have no real-world failure mode worth asserting; the FIPS 180-4 algorithm is the only caller, and its preconditions on these helpers are validated at the srmech_sha256_compress entry. |
rtsch_class_of |
1 | EXEMPT — rc81 (srmech_riemann_theta.c, the Schottky-form counter): a pure value classifier mapping a doubled inner product to its class index 0..4 (or −1 off-shell) over a single scalar argument; no pointer/bounds invariant to assert (same kind as the exempt sha256 ror / toml_is_ws char classifiers). Already asserts RTSCH_NCLASS == 5. |
rtsch_ctz |
1 | EXEMPT — rc81: counts the trailing zeros (lowest set-bit index) of a nonzero uint64; a pure value op over a single scalar, like the exempt sha256 __ror. Already asserts x != 0u. |
fac_is_prime |
1 | EXEMPT — rc165 (srmech_factor_poly.c, the Zassenhaus prime selection): a pure value predicate (trial-division primality of n < 2^32) over a single scalar; already asserts the domain bound n < 2^32 (the loop-safety invariant so d*d cannot overflow); no pointer/bounds invariant for a second, a tautology would be cargo-cult. Same kind as the exempt sha256 ror / toml_is_ws char classifiers. |
srmech_sha256_compress |
2 | ✅ |
srmech_sha256_state_to_hex |
2 | ✅ |
srmech_sha256_hex |
3 | ✅ |
srmech_ndjson_emit |
2 | ✅ |
srmech_ndjson_process_chunk |
5 | ✅ (gained line_buf != NULL assert in #772) |
srmech_ndjson_iter |
2 | ✅ |
srmech_hermitian_run_sweeps |
3 | ✅ (Hwork / V non-NULL + n ≤ MAX_NODES) |
srmech_hermitian_eigendecompose_ws |
4 | ✅ (out ptrs + n bound + ws_len ≥ total) |
srmech_hermitian_eigendecompose |
2 | ✅ (thin wrapper; out-ptr asserts) |
srmech_parallel__stream_transform |
2 | ✅ (label != NULL; buf != NULL \|\| n == 0) |
srmech_parallel__run_sector |
4 | ✅ (body / out / scratch non-NULL + sector < CAP) |
srmech_parallel__validate |
2 | ✅ (post-validation: n_sectors bound + scratch_len) |
srmech_parallel__serial |
2 | ✅ (body non-NULL + n_sectors bound) |
srmech_parallel__job_run |
2 | ✅ (job + job->body non-NULL) |
srmech_parallel__thread_posix |
2 | ✅ (arg + job->body non-NULL) |
srmech_parallel__threaded (POSIX) |
2 | ✅ (body non-NULL + n_sectors bound) |
srmech_parallel__thread_win |
2 | ✅ (arg + job->body non-NULL) |
srmech_parallel__threaded (Win) |
2 | ✅ (body non-NULL + n_sectors bound) |
srmech_cascade_parallel_sector_dispatch |
2 | ✅ (post-validation: body + out/scratch non-NULL) |
srmech_kuramoto__coupling_sum |
2 | ✅ (theta != NULL; i < n) |
srmech_cascade_kuramoto_step_f64 |
2 | ✅ (out non-NULL + theta/omega-vs-n aliasing pre) |
srmech_kuramoto__general_sum |
2 | ✅ (theta != NULL; i < n) |
srmech_cascade_kuramoto_step_general_f64 |
2 | ✅ (out non-NULL + theta/omega-vs-n aliasing pre) |
srmech_loop__mul2 / __mul4 / __mul8 |
2 each | ✅ (rc7 inputs + out non-NULL) |
srmech_loop_bind_f64 |
2 | ✅ (rc7 x/y/out non-NULL + n==8) |
srmech_loop_conj_f64 |
2 | ✅ (rc7 x/out non-NULL + n==8) |
srmech_loop_inv_f64 |
2 | ✅ (rc7 x/out non-NULL + n==8) |
srmech_cross7_f64 |
2 | ✅ (rc7 x/y/out non-NULL + n==8) |
srmech_g2_three_form_f64 |
2 | ✅ (rc7 x/y/z/out non-NULL + n==8) |
srmech_autocorrelation_f64 |
2 | ✅ (rc8 x/out non-NULL when n>0 + out-vs-x aliasing pre) |
srmech_sha256_batch |
2 | ✅ (rc10 msgs/lens/out non-NULL when n>0 + tier∈{0,1,2}) |
srmech_sha256_batch.c block/compress/hash internals |
2 each | ✅ (rc10 fill_block/load_words/compress1/digest1/compress4/hash4/compress8/hash8 — pointer + buffer pre-conditions) |
srmech_loop_bind_hd_f64 |
2 | ✅ (rc11 x/y/out non-NULL when nb>0 + tier∈{0,1,2}) |
srmech_loopbind_hd.c vmul/group internals |
2 each | ✅ (rc11 vmul2a/4a/8a + vmul2s/4s/8s + avx/sse2 group loops — x/y/out non-NULL) |
srmech_simd_tier (HAL) |
2 | ✅ (rc11 env_var != NULL + max_tier >= 0) |
srmech_sha256_shani |
2 | ✅ (rc18 out_digest non-NULL + data non-NULL when len>0; tier∈{0,1}) |
srmech_sha256_shani.c pad/scalar/SHA-NI internals |
2 each | ✅ (rc18 fill_block/store_digest/load_words/compress1/digest_scalar + pack/unpack/rounds_0_15/group_full/group_final/compress_block/digest_shani — pointer + cur<4 pre-conditions) |
Rule 5 EXEMPT: srmech_sha256b__ror (rc10) + srmech_sha256ni__ror (rc18)
— 1-line rotates, like the exempt scalar srmech_ror32. srmech_simd_has_avx2
/ srmech_simd_has_avx / srmech_simd_has_sse2 / srmech_simd_has_shani
(rc11/rc18 SIMD optimize-path HAL — pure cpuid CPU-feature detectors returning
0/1, no pointer/bounds invariant to assert; the rc11 trio REPLACED the per-file
srmech_sha256b__avx2_supported/__tier copies, so the HAL nets FEWER exempt
functions, and srmech_simd_tier is NOT exempt; rc18 adds the _shani
leaf7-bit29 probe). The SRMECH_{SHA256,LOOP_HD,SHANI}_FORCE_TIER env overrides
+ bounded-tier clamp live in the non-exempt srmech_simd_tier. Mirrored in
tests/test_jpl_audit.py::RULE_5_EXEMPT_FUNCTIONS.
Rule 5 EXEMPT: toml_is_ws + toml_is_bare_key_char (srmech_toml.c)
— pure char -> bool predicates over a single value argument (is the char
ASCII whitespace / a bare-key char). No pointer, no bounds, no state; every
char is a valid input, so there is no anomalous condition to assert and a
tautology assert would be cargo-cult per the exemption policy below. Same
shape as the exempt sha256 ch/maj inline primitives. Every OTHER function
in srmech_toml.c (the arena allocator, the lexer skips, the value/array/
table parsers, the builder-tree allocators, the finaliser recursion, and the
public srmech_toml_parse / srmech_toml_table_get entries) carries ≥ 2
asserts (an entry-pointer/contract assert + a structural-invariant assert).
Rule 5 EXEMPT (srmech_json.c, §41 genome-persistence JSON mirror):
json_is_ws + json_is_digit — 4-line char classifiers returning 0/1 over a
single char, no pointer/bounds invariant to assert (the same exemption class
as the TOML parser's toml_is_ws). Every other function in srmech_json.c —
the arena bump allocator, the explicit-stack parser/emitter, the string
escape/decode helpers, the canonical writer, the builder constructors — carries
≥ 2 asserts. The parser + writer are NON-recursive (Rule 1): both walk the value
tree with an explicit depth-bounded stack capped at SRMECH_JSON_MAX_DEPTH.
Mirrored in tests/test_jpl_audit.py::RULE_5_EXEMPT_FUNCTIONS.
Rule 5 — srmech_genome.c (§41 genome-persistence disk surface) adds NO
new exemptions. Every function in srmech_genome.c — the path helper
(genome_join) + the file helpers (genome_write_file / genome_read_file /
genome_read_region / genome_file_size, which rc162 turned into thin
delegations to the PAL FILE surface — no raw stdio in the genome), the manifest
builders (genome_build_the_one /
_chrom / _data / _attest / _render / _manifest / _manifest_tree), the
§44 inline-cap body scan (genome_decode_label / genome_scan_chroms) + the
string-block fill (genome_fill_strings, genome_hex), the §44 manifest-optional
acquirer (genome_obtain_manifest — parse if present, else rebuild by scan — and
its rc338/#T956 rebuild tail genome_rebuild_manifest_tree, split out so the
scan's genome_strings_t can be arena-resident rather than a stack local the
returned tree would outlive), the
catalog/load/window/append entries + their accessors (genome_data_get /
genome_str_eq / genome_find_chrom / genome_check_new_label /
genome_read_bound_body / genome_grow_body / genome_save_validate), the §45
in-place edits (srmech_genome_remove / srmech_genome_replace — splice a
chromosome's byte span out of / into the body, then re-save), the §43
file-management surface (srmech_genome_export / srmech_genome_import — bundle a
chromosome as a self-contained MPR-attested .chr, re-import it self-verifying,
with the .chr builders genome_jstr / genome_find_chrom_obj / genome_chr_subobj
/ _build_data / _build_attest / _build_render / _build_file / genome_chr_meta
/ genome_chr_consts, and the import helpers genome_unhex / genome_chr_decode_verify
/ genome_chr_verify_extract / genome_body_exists / genome_chr_append), and the
§43 loose↔packed surface (srmech_genome_explode / srmech_genome_pack — git's
object model: explode a packed genome to a dir of <label>.chr loose bundles,
pack a *.chr dir back into one genome in canonical sorted-label order, with the
helpers genome_label_filename_safe / genome_collect_labels / genome_chr_name_ok
/ genome_list_chr / genome_chr_peek_label / genome_sort_by_label) — carries ≥ 2 asserts and is ≤ 60 lines. No
recursion (the JSON tree is built/walked by the non-recursive
srmech_json builder/parser/writer); every loop is bounded by
n_chroms (a caller-arena-allocated count — no compiled-in cap; the
genome C carves ALL scratch from the caller ws arena) or a caller size_t
(rc162: the file read/write/region/size go through the PAL, whose read loop
carries the pass <= cap over-bound; the §43 genome_list_chr directory scan
carries an explicit guard < 65536 over-bound on top of the ≤ max_n
collected-.chr cap, Rule 2). File I/O is stdio (Rule 3 bans malloc, not
files), and as of rc162 the genome's file read/write/region/size go
through the PAL (srmech_plat_file_* — no raw fopen/fread/fwrite in
the genome); the only remaining platform-specific touch in this file is the
§43 pack's *.chr directory enumeration (POSIX dirent / Win32
FindFirstFile, ifdef'd like srmech_platform.c; no malloc — a candidate for
a future PAL dir-iteration surface); the caller arena is for the JSON tree
only; path strings + digests live in fixed stack/static buffers.
The Hermitian-eigendecomp _ws entry additionally validates the new
workspace parameters at runtime (workspace != NULL →
SRMECH_ERR_NULL_ARG; ws_len < 2*n*n → SRMECH_ERR_OVERFLOW) in
addition to the debug-build asserts, per Rule 7.
Total: 18 assertions across the NDJSON + Hermitian-reentrancy
functions; every non-exempt function stays ≥ 2. Exceeds the 2.0
floor. (The repo-wide ratchet tests/test_jpl_audit.py enforces this
mechanically across all c/src/*.c.)
Exemption policy¶
The two exempt categories above (trivial accessors, inline
arithmetic primitives) follow the same exemption policy
ephemerides-spectral applies. The rule's intent is "anomalous
conditions in real-life executions"; functions that take no inputs
and return a compile-time constant have no anomalous conditions to
check, and asserting a tautology (assert(true)) would be cargo-
cult compliance rather than substantive code quality.
✅ Pass.
Rule 6 — Smallest possible scope for data¶
"Data objects must be declared at the smallest possible level of scope."
Violations: 0¶
Manual review of every variable declaration:
- Loop indices declared inside the
for (size_t i = 0u; ...)head — minimal possible scope, C99/C11 idiom. - Local intermediates (e.g.
const uint32_t t1,t2in sha256 rounds) declaredconstand at the inner loop body scope where they're used. - Working state buffers (
uint32_t w[64]in sha256 compress;uint8_t chunk[]in ndjson_iter) declared at the function entry — minimal-scope wouldn't help; they're used throughout the function body. - The NDJSON line-assembly buffer must persist across
srmech_ndjson_process_chunkinvocations during onesrmech_ndjson_itercall (a line can straddle chunk boundaries). As of #772 it is a function-localstatic SRMECH_THREAD_LOCALbuffer insidesrmech_ndjson_iter(no longer a file-scopeg_line_bufglobal), threaded intoprocess_chunk/emitas achar *line_bufparameter so no helper references a file-scope global. At 1 MiB it stays static-duration (too large to stack safely per call) but is now per-thread → reentrant across threads. Thesrmech_hermitian_eigendecomposeworking matrixHworkgot the same treatment (function-localstatic SRMECH_THREAD_LOCAL), and additionally exposessrmech_hermitian_eigendecompose_wswith a caller-supplied workspace for callers that want to own the buffer. The earlier single-thread contract for these two buffers is thereby retired.
✅ Pass.
Rule 7 — Check return values, validate parameters¶
"The return value of non-void functions must be checked by each calling function, and the validity of parameters must be checked inside each function."
Violations: 0¶
Parameter validation at every public entry point:
| Function | Parameter validation |
|---|---|
srmech_sha256_hex |
out_hex == NULL → SRMECH_ERR_NULL_ARG. data == NULL with data_len != 0 → same. |
srmech_ndjson_iter |
path == NULL or cb == NULL → SRMECH_ERR_NULL_ARG. |
srmech_version |
No parameters. |
srmech_abi_version |
No parameters. |
Return-value checks at every internal-callsite:
- Every
fopenchecked for NULL →SRMECH_ERR_IO. - Every
freadchecked forn_read == 0+ferror(fp). - Every callback invocation's return propagated immediately on
non-
SRMECH_OK. srmech_ndjson_process_chunk's return checked at the call site insrmech_ndjson_iter.srmech_sha256_compressreturnsvoid(state mutation only) — exempt.memcpy/memsetreturns ignored per standard-library convention.
✅ Pass.
Rule 8 — Limited preprocessor¶
"The use of the preprocessor must be limited to the inclusion of header files and simple macro definitions. Token pasting, variable argument lists (ellipses), and recursive macro calls are not allowed."
Violations: 0¶
grep -n "##\|__VA_ARGS__\|\.\.\." c/src/*.c c/include/srmech.h→ no matches.- All
#definedirectives are simple constants (SRMECH_VERSION_*,SRMECH_NDJSON_CHUNK_BYTES,SRMECH_NDJSON_MAX_LINE_BYTES,SRMECH_ABI_VERSION,SRMECH_THREAD_LOCAL) or include guards (#ifndef SRMECH_H). SRMECH_THREAD_LOCAL(#772) is a single-token object-like macro selecting the platform TLS keyword (_Thread_local/__declspec(thread)/__thread) via#if; no token-paste, no varargs, no line continuation.- One function-like macro exists:
SRMECH_HERMITIAN_WS_LEN(n)(#772), a single-line pure-arithmetic constant helper (((size_t)(n) * (size_t)(n) * 2u)) that lets a caller size the Hermitian-eigendecomp workspace. It is side-effect-free, expands on one line, and uses neither token-paste nor varargs — within the spirit of Rule 8 (which prohibits multi-line / recursive / token- pasting macros, not all parameterised constants). (rc161 removed the object-likeSRMECH_HERMITIAN_WS_MAX/_WS_MAX_NODEScaps — the Hermitian node ceiling is now the runtime config valuesrmech_config_hermitian_max_nodes(), default 2048; only the arithmetic_WS_LEN(n)helper remains.) - No multi-line macros. No recursive / token-pasting macros.
srmech_json.c(§41 JSON mirror) addsSRMECH_JSON_MAX_DEPTH(object-like int recursion-depth guard — there is no child-count cap since rc160: the writer key-sort scratch is caller-arena-backed) and the four structural-byte constantsJSON_LBRACE/JSON_RBRACE/JSON_LBRACK/JSON_RBRACK(single-token ASCII-code object-like macros,0x7B/0x7D/0x5B/0x5D). They exist so no brace/bracket char literal ('{''}''['']') appears in a function body — keeping brace-balance unambiguous for tooling and readers. All single-line, no token-paste, no varargs.#ifdef __cplusplusonly forextern "C"block — standard.
✅ Pass.
Rule 9 — Pointer dereference depth ≤ 1; no function pointers¶
"The use of pointers should be restricted. Specifically, no more than one level of dereferencing should be used. Pointer dereference operations may not be hidden in macro definitions or inside typedef declarations. Function pointers are not permitted."
Status: Partial — measured population of 10 under a seeded down-only ratchet (rc452)¶
Dereference depth: 0 violations¶
No **ptr syntax appears anywhere; all pointer indirection is
single-level. The size_t *line_len_inout, size_t *lineno_inout
parameters to srmech_ndjson_process_chunk are single-level —
the caller passes addresses of local stack variables.
(cr_value_t **out output parameters are a write-through-one-level
out-slot, the same shape.)
Function pointers: the rc452 census, and what this section used to claim¶
Through rc451 this section said "1 deliberate deviation"
(srmech_ndjson_line_cb). That sentence was written at Phase B6, when it was
true, and it survived because nothing measured the rule:
tests/test_jpl_audit.py mechanically checks Rules ⅓/⅘/8, and Rule 9 had
no detector at all — the same "believed absent rather than known clean" shape
Rule 1's recursion half had until rc441. Meanwhile the tree grew — including
IV_VTABLE in src/srmech_invoke.c, a 38-row name-to-function-pointer
dispatch table shipped since ~rc189 — and nothing contradicted the sentence.
The census (rc452; masked scan over src/*.c + src/*.h +
include/*.h, 150 files, comments/strings/chars blanked, declarator pattern
(*name)():
- Before the A1 dispatch: 14 declarator sites across 5 files, of which
12 predated rc452 — so the "passes with one deviation" claim was
already false before this rc; rc452's first cut (the
CR_OP_REGfunction-pointer columns) had grown the undocumented population from 12 to 14. - After the A1 dispatch: 10 sites across 4 files.
src/srmech_compose_run.cwent 4 → 0: the two rc452-new table typedefs (cr_op_fn_t/cr_bin_fn_t), plus two sites that predated rc452 (cr_series_fn_tandcr_op_dseq's inline function-pointer parameter), all replaced by small-int enum columns / parameters plus bounded per-domain switches with nodefault:arm — so gcc/clang's-Wswitchunder-Werror, and MSVC's/w44062under/WX(C4062 is off by default even at/W4; measured, and added toSRMECH_PEDANTIC), make "row added, case forgotten" a compile error. The A1 shape is also smaller: the fourteen seven-line uniform-shape wrapper functions existed only to feed the function-pointer column, and deleted with it.
The remaining population, seeded (RULE_9_FN_PTR_SEEDED in
tests/test_jpl_audit.py; strict on novel sites, down-only ceiling 10):
| Site | File | Why it stands |
|---|---|---|
iv_thunk_t (IV_VTABLE, 38 rows) |
src/srmech_invoke.c |
THE NEXT DRAIN, by the identical A1 recipe (enum + bounded switch, no default:). Deliberately not drained in the same change as the census, so the drain lands reviewed on its own. |
fiedler_rec_cb |
src/srmech_laplacian.c |
Internal recording callback; drainable after IV_VTABLE by the same recipe. |
srmech_plat_thread_fn |
src/srmech_platform.h |
The thread-start shape the platform shim hands to pthreads / Win32 — the OS API is a function-pointer contract. |
srmech_ndjson_line_cb |
include/srmech.h |
The original Phase B6 documented deviation (rationale below). |
srmech_progress_tick_cb_t, srmech_cascade_op_callback_f64_t, srmech_cascade_body_f64, srmech_bus_handler_callback_t, srmech_bus_subscriber_callback_t, srmech_progress_cb_t |
include/srmech.h |
Public callback typedefs — ctypes CFUNCTYPE wire contract; several drove ABI bumps (v2–v6). Draining these is an API redesign, not a refactor. |
Enforcement (new at rc452): tests/test_jpl_audit.py gains
test_rule_9_no_new_function_pointers — the masked declarator scan, STRICT
on any site not in the seeded set, with a down-only ceiling
(CEIL_RULE_9_FN_PTR = 10) and a slack check pinning the ceiling to the live
count — plus test_rule_9_detector_is_not_vacuous, which requires the scan
to find srmech_ndjson_line_cb: a scanner that cannot find the known
deviation is not a scanner.
Rationale for the srmech_ndjson_line_cb deviation (Phase B6, still
valid): the callback enables the Python ctypes binding to receive lines
without copying through an intermediate C-side dynamic structure (which would
violate Rule 3). The alternatives — a caller-supplied output array (requires
knowing the line count in advance) or batched IO (a callback with extra
bookkeeping) — impose Pyrrhic constraints on the caller. The same reasoning
extends to the other include/srmech.h callback typedefs: each is the wire
by which the Python side observes or steers a C-side computation.
⚠️ Partial — dereference-depth clean; function-pointer population 10,
seeded, down-only, next drain named (IV_VTABLE).
Rule 10 — Compile clean at most-pedantic warning level¶
"All code must compile, from the first day of development, with all compiler warnings enabled at the compiler's most pedantic setting. All code must compile with these settings without any warnings."
Violations: 0¶
Implementation:
CMakeLists.txtSRMECH_PEDANTICoption (default OFF for casual local builds, ON for CI). When ON, gcc/clang add-Werrorand MSVC adds/WX.- Default flags:
-Wall -Wextra -Wpedantic -O2(gcc/clang) or/W4 /O2(MSVC). - CI matrix runs
SRMECH_PEDANTIC=ONon Linux gcc + macOS clang + Windows MSVC. Any new warning fails the build.
Phase B6 ship: the CI workflow gains a dedicated pedantic-build
job alongside the existing build-wheels matrix. The pedantic
build runs cmake directly with -DSRMECH_PEDANTIC=ON and only
asserts the build succeeds — it doesn't ship a wheel, it's purely
the toolchain-level Rule-10 ratchet.
✅ Pass.
Implementation history¶
- Phase B3 (rc5) — first native code shipped (sha256). The Power-of-Ten audit was already partially compliant in the code comments but unverified.
- Phase B4 (rc6) — ndjson reader. Same comment-level discipline.
- Phase B5 (rc7) — no new C code; only Python wiring. No audit delta.
- Phase B6 (rc8, this ship) — formal audit. Surfaced one
mechanical Rule 4 violation (
srmech_ndjson_iterat 76 lines) and zero substantive issues elsewhere. Fixed Rule 4 by extractingsrmech_ndjson_process_chunk. Documented the Rule 9 callback deviation. Wrote this document. Added CI pedantic-build job (Rule 10 ratchet). - v0.6.0rc5 (#772) — full-core reentrancy. A full-core audit for
shared mutable static data (
grepoverc/src/*.c) found exactly two process-wide-shared scratch buffers:srmech_ndjson.c'sg_line_bufandsrmech_laplacian.c'sHwork. Both were already Rule-3-clean (static-duration, no malloc) — the change is a reentrancy trade, not a Rule-3 fix.g_line_bufbecame a function-localstatic SRMECH_THREAD_LOCALbuffer threaded intoprocess_chunk/emitas a parameter (no API/ABI change).Hworkbecame per-thread the same way, and a new ABI-additive exported symbolsrmech_hermitian_eigendecompose_wsexposes a caller- supplied workspace (validated: non-NULL +ws_len ≥ 2*n*n); the originalsrmech_hermitian_eigendecomposenow routes through that core via a thread-local workspace. The sweep loop was extracted intosrmech_hermitian_run_sweepsto keep all three functions under Rule 4's 60-line limit. New portable TLS macroSRMECH_THREAD_LOCAL(_Thread_local/__declspec(thread)/__thread).SRMECH_ABI_VERSIONunchanged at 3 (adding a symbol never bumps ABI). No new mechanical violations; ratchet stays at 0. - v0.6.0rc6 (#771/#778) —
srmech_parallel.cKlein-4 four-sector dispatch. The C peer forsrmech.amsc.cascade.parallel.parallel_sector_dispatch: runs ONE caller-supplied cascadebodyacross its ≤4 Klein-4 (Z₂ × Z₂) chirality sectors and writes the four sector duals. Adds 10 functions (the publicsrmech_cascade_parallel_sector_dispatch+ 9 static helpers, including platform-conditional POSIX/Windows__threadedvariants). A portable threading shim mirrorssrmech_bus.c:pthread_create/pthread_joinon POSIX,CreateThread/WaitForMultipleObjectson Windows, and a serial fallback that preserves the full four-sector capability bit-for-bit on a thread-less microcontroller. Thread handles + jobs live in fixed[SRMECH_PARALLEL_SECTOR_CAP]([4]) stack arrays — no malloc (Rule 3). Sectors write only their own disjoint output + scratch slices (the F233 independence that makes serial == threaded). Longest function 33 lines (Windows__threaded); every function ≥ 2 asserts.SRMECH_ABI_VERSIONunchanged at 3 (new symbol only). No new mechanical violations; ratchet stays at 0. -
v0.6.0rc9 (#778) —
srmech_kuramoto.cnative Kuramoto step. One forward-Euler step of the canonical Kuramoto coupled-oscillator model (Kuramoto 1975; Acebrón et al. 2005, Rev. Mod. Phys. 77:137):θ_i(t+dt) = θ_i + dt·[ω_i + (K/N) Σ_j sin(θ_j − θ_i)]. Closes a C/Python parity gap so the dispatch-clock / coupled-oscillator step runs natively (libmsin, assrmech_kepler.calready does). 2 functions: the publicsrmech_cascade_kuramoto_step_f64(17 lines) plus the staticsrmech_kuramoto__coupling_sumhelper (9 lines) into which the O(n²) inner coupling sum was factored to keep the public step ≤ 60 lines (Rule 4). Pure function over caller buffers (outmust not alias θ/ω) — no malloc, reentrant, no shared static state; both functions carry 2 asserts.SRMECH_ABI_VERSIONunchanged at 3 (new symbol only). No new mechanical violations; ratchet stays at 0. -
v0.6.0rc14 (§11.1) — generalised Kuramoto-Sakaguchi step.
dθ_i/dt = ω_i + Σ_j A_ij·sin(θ_j − θ_i − α) [ + p_i·sin(ψ_i − θ_i) ]added tosrmech_kuramoto.c: a row-majoradjacencymatrix (NULL → uniformK/N; non-symmetric → directed coupling), a Sakaguchi frustrationα, and optional per-oscillator pinning. 2 new functions: the publicsrmech_cascade_kuramoto_step_general_f64(22 lines) plus the staticsrmech_kuramoto__general_sumhelper (12 lines, into which the O(n²) weighted coupling sum is factored to keep the public step ≤ 60). Pure functions over caller buffers (outmust not alias θ/ω/adjacency/ pin arrays) — no malloc, reentrant, no shared static state; both carry 2 asserts; NO Python callback (co-equal parity — the C path runs C, the Python path runs Python).SRMECH_ABI_VERSIONunchanged at 3 (new symbol only). No new mechanical violations; ratchet stays at 0. -
§41 genome-persistence JSON mirror —
srmech_json.c. A malloc-free JSON parser + canonical writer: the parser builds a value tree from a caller-supplied arena/workspace (the samevoid *ws, size_t ws_lenbump allocator the TOML parser uses); the writer emits bytes BYTE-IDENTICAL to CPythonjson.dumps(obj, sort_keys=True, ensure_ascii=False)for null / bool / int / string / object / array trees (exactly what an MPR manifest / genome catalog is — they are float-free). DOUBLE values are best-effort (%.17g, normalised to carry a.); float byte-parity with Python'srepr(float)is explicitly NOT guaranteed (out of scope; manifests are float-free). Both the parser and the writer are NON-recursive (Rule 1): each walks the tree with an explicit stack of frames bounded bySRMECH_JSON_MAX_DEPTH(64): the parser frames + the writer's emit-frame stack and its per-frame key-order pool are ALL carved from the caller arena (rc160 —srmech_json_write_ws/srmech_json_write_arena_bytes), so there is no child-count cap; an object is bounded only by the arena. Every loop has a fixed bound (Rule 2): the container loops are bounded by the input length / the object width (itself ≤ the arena). No malloc (Rule 3 — caller arena), no libm, no<complex.h>. Two char classifiers (json_is_ws/json_is_digit) are Rule-5 exempt (see the Rule 5 section); every other function carries ≥ 2 asserts and is ≤ 60 lines.SRMECH_ABI_VERSIONunchanged at 3 (new symbols + a struct + macros only). No new mechanical violations; ratchet stays at 0. -
§41 genome-persistence disk surface —
srmech_genome.c. The C mirror ofsrmech.biology.genome's disksave/load/catalog/append/window. A genome directory holdsmanifest.json(an MPRRecord, MPR v1, built with thesrmech_jsonBUILDER + serialised withsrmech_json_write, BYTE-IDENTICAL to the Pythongenome_savemanifest'sjson.dumps(payload, sort_keys=True, ensure_ascii=False)) andturns.bin(the append-only flat body — every strand element a FIXED-WIDTHleaf_dim-byte block, verbatim). Bounding == integrity: every read re-hashes the bytes it touched (viasrmech_sha256_hex, Class A) and compares the lowercase-hex digest against the manifest's stored hex (whole-bodybody_sha256, a windowed chromosome'scap_sha256); a mismatch isSRMECH_ERR_BAD_INPUT— theGenomeBoundingErroranalogue. No abs(), no float, no libm. The §41 attestation / rendering constants (source_doi/source_url/license/retrieved_at/collector_descriptor_path+human_readable_name/cite_as/purpose) are copied VERBATIM fromgenome.py_manifest_record; thecite_ascarries the U+00A7§as the 2-byte UTF-8 sequence0xC2 0xA7(ensure_ascii=False). Theparser_rule_hashissha256("genome_persistence/v1")andcollector_descriptor_hashissha256("srmech://schema/genome_manifest/v1"). File I/O is stdio (Rule 3 bans malloc, not file I/O); the caller arenawsis for the JSON tree only; path strings + digests + the manifest-write buffer + the append body scratch live in fixed stack /static SRMECH_THREAD_LOCALbuffers (Rule-3-clean static storage; per-thread reentrant). No recursion (the JSON tree is built/walked by the non-recursivesrmech_jsonbuilder/parser/writer); every loop bounded (Rule 2 — the file-read loop carries an explicitpass <= capover-bound). Every function carries ≥ 2 asserts and is ≤ 60 lines — NO new Rule-5 exemptions.SRMECH_ABI_VERSIONunchanged at 3 (new symbols + a struct + macros only). No new mechanical violations; ratchet stays at 0.
Both srmech_parallel.c (rc6) and srmech_kuramoto.c (rc9 + rc14) pass the
tests/test_jpl_audit.py mechanical ratchet (Rules 1 / 3 / 4 / 5 / 8)
and the 3-cell pedantic -Werror / -Wpedantic build (Linux gcc /
macOS clang / Windows MSVC), verified green in CI. srmech_json.c and
srmech_genome.c are held to the same ratchet + pedantic build.
srmech_config.c (rc161 — config-driven library limits) + the
srmech_platform.c FILE surface pass the same ratchet + pedantic
-Werror build. The config layer (srmech_config_hermitian_max_nodes
getter / srmech_config_load_toml / srmech_config_load_file /
srmech_config_reset_defaults / the static config_apply helper) is
JPL-clean: no goto, no malloc (the TOML parse uses the caller arena,
and load_file carves the file-read buffer from the front half of that
same arena), ≤ 60-line functions, ≥ 2 asserts each, every status
checked. The PAL FILE primitives (srmech_plat_file_read /
_read_region / _write / _size) are portable stdio (one POSIX/Win
implementation; a bare-metal #else stubs each to SRMECH_ERR_IO),
each ≤ 60 lines with ≥ 2 asserts; only the trivial accessor
srmech_plat_has_filesystem is Rule-5 exempt (a compile-time 1/0,
beside has_threads/has_streams). rc161 also removed the orphan
srmech_hermitian_eigendecompose (no-_ws) overload + its 1 MiB
thread-local static and the SRMECH_HERMITIAN_WS_MAX / _WS_MAX_NODES
caps from srmech_laplacian.c / srmech.h — the Hermitian node ceiling
is now the runtime config value. SRMECH_ABI_VERSION unchanged at 3
(additive symbols; the ctypes shim binds them hasattr-guarded).
rc163 adds the fourth PAL surface — directory iteration
(srmech_plat_dir_open / _dir_next / _dir_close, POSIX
opendir/readdir or Win32 FindFirstFile/FindNextFile, bare-metal
#else → SRMECH_ERR_IO), each ≤ 60 lines with ≥ 2 asserts; only the
trivial accessor srmech_plat_has_dirlist is Rule-5 exempt (compile-time
1/0, beside has_threads/has_streams/has_filesystem).
srmech_genome.c::genome_list_chr now drives that iterator, so the
genome carries no #ifdef — every OS touch lives in the PAL TU.
SRMECH_ABI_VERSION unchanged at 3 (additive symbol; hasattr-guarded).
rc164 adds the streaming-read surface (srmech_plat_rstream_open /
_read / _close) under the existing FILE backend — portable stdio,
no new accessor (it shares srmech_plat_has_filesystem), each ≤ 60
lines with ≥ 2 asserts. srmech_ndjson.c (the §B4 line tokeniser) drops
its own fopen/fread/fclose for that surface, so a grep for raw
stdio across src/ is now zero outside srmech_platform.c — every
OS touch in the C library (files / dirs / streaming reads) lives in the
one PAL TU. No Rule-5 exempt added. SRMECH_ABI_VERSION unchanged at 3.
Total mechanically-detectable violations: 1 → 0 (held at 0 through v0.6.0rc14).
The pin test tests/test_jpl_audit.py enforces the zero count
going forward — PRs that introduce a new function > 60 lines or
remove an assertion from a function below the 2-assert floor will
fail CI.