Skip to content

Web playground (WASM) for driving the complement engine from a phone - #65

Merged
PhysShell merged 1 commit into
mainfrom
claude/web-playground-mvp
Jun 16, 2026
Merged

Web playground (WASM) for driving the complement engine from a phone#65
PhysShell merged 1 commit into
mainfrom
claude/web-playground-mvp

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 16, 2026

Copy link
Copy Markdown
Owner

A browser front for the complement arranger, built so the engine can be driven — and heard — from a phone, no install. ADR-0024 (added here). Rebased onto main after #64, so the diff is only the web/gp-gate/ADR-0024 files.

What it is (MVP, ADR-0024 §2)

A deliberately thin, throwaway front — no wasm-bindgen, no framework. griff-web is an import-free cdylib exporting three C-ABI functions; the static page (web/static/) loads the .wasm with WebAssembly.instantiate(bytes, {}) and marshals a small JSON result through linear memory. The canonical egui frontend (ADR-0016) replaces it at M2.

Live controls: mode (incl. counter_melody), seed, register offset, and pitch spread (the merged ADR-0023 VariationControl, audible on the grid-locked modes). A canvas piano-roll (A blue / B amber) + WebAudio playback. Deterministic: same controls → same result.

How it stays import-free (~90 KiB)

Adds a default-on gp feature to griff-core, gating the Guitar Pro importer (guitarpro/ziptime/getrandomwasm-bindgen/js-sys). web depends on core with default-features = false, dropping that whole subtree — the module needs no JS glue and instantiates with {}. The CLI and tests keep gp on and are unchanged (host build + clippy verified both on and off).

Infra

  • web/build.shweb/dist (just cargo build --target wasm32 + copy static files; no Trunk/wasm-bindgen).
  • .github/workflows/web.yml → builds and deploys to GitHub Pages (enable Pages → "GitHub Actions" in repo settings).
  • web/ is wasm32-only and excluded from the root workspace (like fuzz/), so stable --workspace builds/clippy/tests never touch it.

Verification

  • Host workspace builds (gp on); web wasm builds import-free (0 imports); 4 host-side unit tests cover the JSON path (every mode well-formed, counter_melody succeeds, pitch_spread moves rhythm_lock, determinism); clippy/fmt clean.
  • I can't run a browser here, so the first on-device run is yours (or via the Pages deploy).

Notes / next

  • Audio is a placeholder WebAudio synth (sawtooth + envelope, A left / B right) — a real guitar SoundFont is a follow-up.
  • Input is a fixed in-code sample part A; a file picker / drag-drop comes later.
  • The web crate uses inline #[cfg(test)] unit tests (idiomatic for a small cdylib) rather than a separate test file.

https://claude.ai/code/session_01TTUbGjzD8ysnVnCJnZJE95

Summary by CodeRabbit

  • New Features

    • Browser-based playground now available for testing on mobile devices and modern browsers.
  • Chores

    • Automated build and deployment pipeline for the web playground via GitHub Actions.
    • Guitar Pro file import is now optional for customized builds.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 99a96705-6895-4c87-bae9-ad6f2cdc2982

📥 Commits

Reviewing files that changed from the base of the PR and between 72ae97e and 79bcb1a.

📒 Files selected for processing (15)
  • .github/workflows/web.yml
  • Cargo.toml
  • core/Cargo.toml
  • core/src/import.rs
  • core/src/lib.rs
  • docs/adr/0024-web-wasm-frontend-for-mobile.md
  • docs/adr/README.md
  • web/.gitignore
  • web/Cargo.toml
  • web/README.md
  • web/build.sh
  • web/src/lib.rs
  • web/static/app.js
  • web/static/index.html
  • web/static/style.css
✅ Files skipped from review due to trivial changes (4)
  • web/static/index.html
  • web/.gitignore
  • docs/adr/README.md
  • web/README.md
🚧 Files skipped from review as they are similar to previous changes (8)
  • core/src/import.rs
  • core/src/lib.rs
  • web/build.sh
  • web/Cargo.toml
  • Cargo.toml
  • core/Cargo.toml
  • web/src/lib.rs
  • web/static/app.js

📝 Walkthrough

Walkthrough

Adds a gp Cargo feature to griff-core that gates the Guitar Pro import path and module behind a compile-time flag. Introduces a new griff-web cdylib crate compiled to wasm32-unknown-unknown, exposing arrange/arrange_len C-ABI functions that return JSON over WASM linear memory. Adds a static browser playground with piano-roll canvas and Web Audio playback, a build script, a GitHub Pages CI workflow, and ADR-0024.

Changes

Web WASM Playground with gp Feature Gate

Layer / File(s) Summary
gp Cargo feature gate in griff-core
Cargo.toml, core/Cargo.toml, core/src/lib.rs, core/src/import.rs
Excludes web/ from the root workspace; declares guitarpro as an optional dependency wired to a new default-enabled gp feature; gates the gp module declaration and ImportError::Gp variant behind cfg(feature = "gp"); refactors import_score_auto so the GP parse attempt only runs when the feature is enabled.
griff-web crate manifest and workspace isolation
web/Cargo.toml, web/.gitignore
Defines the griff-web crate as cdylib+rlib, disables griff-core default features (skipping gp for an import-free WASM build), sets a size-optimized release profile (opt-level = "z", LTO, strip), and creates an isolated sub-workspace.
WASM C-ABI entrypoint and JSON serialization
web/src/lib.rs
Implements sample_part_a for a deterministic C-natural-minor score, relation_mode to map u32 to RelationMode, manual JSON helpers (push_notes, json_escape, build_json), and the arrange/arrange_len no-mangle extern-C exports backed by a thread_local byte buffer. Includes unit tests for all modes, part B emission, pitch-spread effect, and determinism.
Browser playground HTML, JS, and CSS
web/static/index.html, web/static/app.js, web/static/style.css
Adds a static page with mode/seed/offset/variation controls and transport buttons; implements WASM loading, JSON decode from linear memory, piano-roll canvas rendering with DPR scaling and bar gridlines, and Web Audio oscillator playback with an animated playhead; applies dark-mode CSS with touch-optimized control and canvas styles.
Build script, GitHub Pages CI, and documentation
web/build.sh, .github/workflows/web.yml, web/README.md, docs/adr/0024-web-wasm-frontend-for-mobile.md, docs/adr/README.md
Adds web/build.sh to build and assemble web/dist; adds a two-job CI workflow (builddeploy) publishing to GitHub Pages on pushes to main affecting web/** or core/**; documents the ABI and run instructions in web/README.md; records the decision in ADR-0024 and updates the ADR index.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser (index.html)
    participant AppJS as app.js
    participant WASMModule as griff_web.wasm
    participant CoreLogic as arrange_complement_varied

    Browser->>AppJS: user changes controls / clicks Generate
    AppJS->>WASMModule: arrange(mode, seed, offset, variation)
    WASMModule->>CoreLogic: sample_part_a + ComplementSpec + VariationControl
    CoreLogic-->>WASMModule: VariedComplement or VariationError
    WASMModule-->>AppJS: *const u8 (JSON in linear memory)
    AppJS->>WASMModule: arrange_len()
    WASMModule-->>AppJS: byte length
    AppJS->>AppJS: decode JSON, draw piano-roll canvas
    Browser->>AppJS: click Play
    AppJS->>AppJS: schedule Web Audio oscillators, animate playhead
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PhysShell/griff#54: Introduces import_score_auto and ImportError::Gp routing in core/src/import.rs—the same code path this PR gates behind the gp feature flag.
  • PhysShell/griff#64: Introduces arrange_complement_varied and VariationControl in core/src/complement.rs, which web/src/lib.rs directly constructs and calls.

Poem

🐇 A bunny hops to the browser's shore,
With WASM magic and a piano-roll floor,
The gp gate swings open or stays shut tight,
While arrange sends JSON through linear light.
Deploy to Pages — the playground takes flight! 🎵

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a WASM web playground for the complement engine to enable mobile browser testing, which aligns with the substantial new web/ directory, GitHub Pages workflow, and documentation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/web-playground-mvp

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/Cargo.toml (1)

29-32: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Re-apply unsafe_code = "forbid" in this isolated workspace.

This crate is intentionally excluded from the root workspace, so it no longer inherits the root lint policy that forbids unsafe code. Add an explicit lint rule here to keep the workspace-wide guarantee intact.

Suggested manifest patch
 [workspace]
+
+[lints.rust]
+unsafe_code = "forbid"

As per coding guidelines, “Forbid unsafe code workspace-wide; no exceptions without an ADR.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/Cargo.toml` around lines 29 - 32, The isolated workspace section in
web/Cargo.toml no longer inherits the root workspace's lint policies since it's
excluded from the root workspace. Add an explicit lint configuration to the
workspace section to re-apply the unsafe_code = "forbid" policy, ensuring the
workspace-wide guarantee that unsafe code is forbidden remains in effect for
this isolated workspace.

Source: Coding guidelines

🧹 Nitpick comments (2)
core/tests/complement.rs (2)

275-290: ⚡ Quick win

Strengthen the identity-window test to validate full behavioral identity, not just pitch values.

This test currently checks only note_pitches, but the stated contract is byte-identical behavior at full spread. Add onset/duration (or full B-track event-group) equality assertions so regressions in non-pitch fields are caught.

Proposed test tightening
     assert_eq!(
         note_pitches(&varied.complement.score, varied.complement.part_b_index),
         note_pitches(&plain.score, plain.part_b_index),
         "VariationControl::FULL must be the identity window",
     );
+    assert_eq!(
+        note_onsets(&varied.complement.score, varied.complement.part_b_index),
+        note_onsets(&plain.score, plain.part_b_index),
+        "FULL must preserve onset placement",
+    );
+    assert_eq!(
+        note_durations(&varied.complement.score, varied.complement.part_b_index),
+        note_durations(&plain.score, plain.part_b_index),
+        "FULL must preserve note durations",
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/tests/complement.rs` around lines 275 - 290, The test
variation_full_spread_equals_plain_arrange currently only validates pitch
equality via note_pitches when comparing the output of arrange_complement_varied
with VariationControl::FULL against the plain arrange_complement result. To
properly validate the identity-window contract stating that FULL spread should
produce byte-identical behavior, add additional assertions beyond note_pitches
to verify onset and duration values (or the complete B-track event-group
structure) also match between varied.complement and plain. This ensures
regressions in non-pitch fields are caught by the test.

260-389: ⚡ Quick win

VariationControl coverage is limited to RhythmLock; add coverage for the other ladder-threaded modes.

Implementation threads control through RegisterContrast and CallResponse too, but the new variation tests only exercise one mode. Add at least one LOCKED/FULL assertion per mode to protect the wiring contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/tests/complement.rs` around lines 260 - 389, The VariationControl tests
currently only exercise the RhythmLock mode via the RHYTHM_LOCK constant, but
the implementation threads control through RegisterContrast and CallResponse
modes as well. Add ComplementSpec constants for RegisterContrast and
CallResponse modes (similar to the existing RHYTHM_LOCK constant), then create
or extend test functions to exercise arrange_complement_varied with at least one
VariationControl::LOCKED assertion and one VariationControl::FULL assertion for
each of these additional modes to ensure the wiring contract is protected across
all ladder-threaded modes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/web.yml:
- Around line 12-15: The workflow-level permissions block currently includes
`pages: write` and `id-token: write` permissions that are only required by the
deploy job, not the build job. Remove `pages: write` and `id-token: write` from
the top-level permissions section, keeping only `contents: read` at the workflow
level. Then add a dedicated `permissions` section to the deploy job in the jobs
section with the full set of permissions it needs: `contents: read`, `pages:
write`, and `id-token: write`. This ensures the build job operates with minimal
required permissions while the deploy job has access to the elevated permissions
it requires.
- Around line 26-51: Replace all GitHub Actions version references with
full-length commit SHAs to prevent supply-chain attacks from mutable tag
references. Update actions/checkout@v4 on line 26 to use its commit SHA,
actions/cache@v4 on line 30 to use its commit SHA,
actions/upload-pages-artifact@v3 on line 39 to use its commit SHA, and
actions/deploy-pages@v4 on line 51 to use its commit SHA. Additionally, add
persist-credentials: false as an option to the checkout action on line 26 to
prevent the GITHUB_TOKEN from being stored in the git config, reducing the risk
of token exfiltration.

In `@core/src/complement.rs`:
- Around line 90-104: The current commit combines both implementation and tests
together, which violates Rust coding guidelines. Split this into two separate
commits: first, create a tests-only commit containing all test coverage for the
new public API (VariationControl struct with its FULL and LOCKED constants,
VariedComplement, VariationError, arrange_complement_varied, and any related
test functions), written as failing tests. Then, in a second commit, add the
actual implementation of the VariationControl struct, its constants
pitch_spread, FULL, and LOCKED, along with all the supporting implementation
code. This ensures tests are defined before the implementation they verify.

In `@docs/adr/0024-web-wasm-frontend-for-mobile.md`:
- Around line 38-41: Update the wording in the M1 MVP description to correctly
reflect the number of exported C-ABI functions. Change "exports three C-ABI
functions" to "exports two C-ABI functions" and list the actual exported
contract: the two C-ABI functions arrange and arrange_len, plus the linear
memory export. This ensures the ADR text accurately matches the implemented
contract.

In `@web/src/lib.rs`:
- Around line 185-191: The error text is being directly interpolated into a JSON
string using `{:?}` format without escaping special characters. This can produce
invalid JSON if the error message contains quotes, backslashes, or control
characters, causing browser parsing to fail. Before embedding the error into the
JSON output in the error branch of the write! macro, properly escape the error
string to ensure it produces valid JSON. Consider using a JSON escaping function
or library that converts the error debug representation to a properly escaped
JSON string value.
- Around line 209-264: The public C-ABI functions arrange and arrange_len were
introduced in the same commit as their test cases, violating the commit split
requirement that tests must be committed separately from their implementation.
To fix this, reset your current commit and split it into two: first commit the
test module (the tests block containing
every_mode_emits_part_a_and_well_formed_header,
counter_melody_succeeds_on_the_uniform_sample,
pitch_spread_changes_rhythm_lock_output, and deterministic_for_identical_args),
then in a separate commit add the arrange and arrange_len public extern
functions along with the supporting code they depend on.

---

Outside diff comments:
In `@web/Cargo.toml`:
- Around line 29-32: The isolated workspace section in web/Cargo.toml no longer
inherits the root workspace's lint policies since it's excluded from the root
workspace. Add an explicit lint configuration to the workspace section to
re-apply the unsafe_code = "forbid" policy, ensuring the workspace-wide
guarantee that unsafe code is forbidden remains in effect for this isolated
workspace.

---

Nitpick comments:
In `@core/tests/complement.rs`:
- Around line 275-290: The test variation_full_spread_equals_plain_arrange
currently only validates pitch equality via note_pitches when comparing the
output of arrange_complement_varied with VariationControl::FULL against the
plain arrange_complement result. To properly validate the identity-window
contract stating that FULL spread should produce byte-identical behavior, add
additional assertions beyond note_pitches to verify onset and duration values
(or the complete B-track event-group structure) also match between
varied.complement and plain. This ensures regressions in non-pitch fields are
caught by the test.
- Around line 260-389: The VariationControl tests currently only exercise the
RhythmLock mode via the RHYTHM_LOCK constant, but the implementation threads
control through RegisterContrast and CallResponse modes as well. Add
ComplementSpec constants for RegisterContrast and CallResponse modes (similar to
the existing RHYTHM_LOCK constant), then create or extend test functions to
exercise arrange_complement_varied with at least one VariationControl::LOCKED
assertion and one VariationControl::FULL assertion for each of these additional
modes to ensure the wiring contract is protected across all ladder-threaded
modes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 72d2cfc0-84b8-4d62-b6ef-970720c3ca03

📥 Commits

Reviewing files that changed from the base of the PR and between 33f2595 and 72ae97e.

📒 Files selected for processing (18)
  • .github/workflows/web.yml
  • Cargo.toml
  • core/Cargo.toml
  • core/src/complement.rs
  • core/src/import.rs
  • core/src/lib.rs
  • core/tests/complement.rs
  • docs/adr/0023-variation-control-for-complement.md
  • docs/adr/0024-web-wasm-frontend-for-mobile.md
  • docs/adr/README.md
  • web/.gitignore
  • web/Cargo.toml
  • web/README.md
  • web/build.sh
  • web/src/lib.rs
  • web/static/app.js
  • web/static/index.html
  • web/static/style.css

Comment thread .github/workflows/web.yml
Comment on lines +12 to +15
permissions:
contents: read
pages: write
id-token: write

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope elevated permissions to the deploy job only.

Line 14 (pages: write) and Line 15 (id-token: write) are set workflow-wide; the build job does not require them. Move these permissions to jobs.deploy.permissions and keep top-level/job-build permissions read-only.

Suggested permission scoping
-permissions:
-  contents: read
-  pages: write
-  id-token: write
+permissions:
+  contents: read
@@
   build:
     runs-on: ubuntu-latest
+    permissions:
+      contents: read
@@
   deploy:
     needs: build
     runs-on: ubuntu-latest
+    permissions:
+      pages: write
+      id-token: write
🧰 Tools
🪛 zizmor (1.25.2)

[error] 14-14: overly broad permissions (excessive-permissions): pages: write is overly broad at the workflow level

(excessive-permissions)


[error] 15-15: overly broad permissions (excessive-permissions): id-token: write is overly broad at the workflow level

(excessive-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/web.yml around lines 12 - 15, The workflow-level
permissions block currently includes `pages: write` and `id-token: write`
permissions that are only required by the deploy job, not the build job. Remove
`pages: write` and `id-token: write` from the top-level permissions section,
keeping only `contents: read` at the workflow level. Then add a dedicated
`permissions` section to the deploy job in the jobs section with the full set of
permissions it needs: `contents: read`, `pages: write`, and `id-token: write`.
This ensures the build job operates with minimal required permissions while the
deploy job has access to the elevated permissions it requires.

Source: Linters/SAST tools

Comment thread .github/workflows/web.yml
Comment on lines +26 to +51
- uses: actions/checkout@v4
- name: Install wasm target
run: rustup target add wasm32-unknown-unknown
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
web/target
key: web-wasm-${{ hashFiles('web/Cargo.toml', 'core/Cargo.toml') }}
- name: Build playground
run: ./web/build.sh
- uses: actions/upload-pages-artifact@v3
with:
path: web/dist

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- id: deploy
uses: actions/deploy-pages@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/web.yml | head -60

Repository: PhysShell/griff

Length of output: 1648


🌐 Web query:

GitHub Actions security best practices pin commit SHA vs tag references official documentation

💡 Result:

GitHub’s official security guidance strongly recommends pinning GitHub Actions to a full-length commit SHA as the only way to ensure an action is used as an immutable release [1][2]. Key security considerations include: 1. Immutability: A commit SHA uniquely and immutably identifies a specific version of an action’s code [3][4]. In contrast, tags (e.g., @v1) and branch references (e.g., @main) are mutable; they can be moved or deleted by an action's maintainers at any time, potentially introducing malicious or breaking code into your workflows without your knowledge [1][3][5]. 2. Risk Mitigation: Pinning to a SHA mitigates the risk of supply chain attacks where a compromised account or repository adds backdoors or malicious payloads to an existing tag [1][6]. While tags rely on trust in the creator, SHAs provide cryptographic assurance that you are running the exact code you reviewed [1][7]. 3. Organizational Enforcement: GitHub supports organizational and repository-level policies that mandate the use of full-length commit SHAs [1][8]. When enforced, any workflow attempting to use an action pinned to a tag or branch will fail [8][4]. 4. Best Practices for Maintenance: Because commit SHAs are opaque and do not automatically receive updates, it is recommended to include the human-readable tag as a comment in your YAML file to maintain clarity [6][7]. Many organizations use automation tools like Renovate or similar dependency managers to automatically create pull requests that update SHA pins when the referenced tags move, balancing security with ease of maintenance [8][6][9]. Recommendation: Always pin third-party actions to a full-length commit SHA [1][6]. While you may continue to use tags if you explicitly trust the creator, understand that this exposes your pipeline to potential upstream compromises [1][5]. If you maintain your own actions, ensure that any internal references to sub-actions are also pinned to SHAs to prevent breaking workflows for users who have enforced SHA-pinning policies [4].

Citations:


🌐 Web query:

GitHub Actions checkout persist-credentials false security best practices token exposure

💡 Result:

Setting persist-credentials: false in the actions/checkout GitHub Action is a security best practice recommended to prevent unintended token exposure [1][2][3]. By default, actions/checkout sets persist-credentials to true, which stores the GITHUB_TOKEN (or a provided personal access token) in the local.git/config file on the runner [4][5][3]. This enables subsequent steps in the same job to execute authenticated Git commands (such as git fetch or push) [4][6][7]. However, this also makes the token accessible to any process, script, or third-party action running in that job [8][1][9]. The primary security risks associated with keeping the default (persist-credentials: true) include: 1. Artifact Exposure (ArtiPACKED): If a workflow uploads the repository workspace as an artifact (e.g., using actions/upload-artifact), the.git directory—containing the persisted credentials—may be included [10][11][12]. If this artifact is public or accessible to unauthorized users, the token can be exfiltrated and misused [2][10][12]. 2. Increased Attack Surface: Any compromised or malicious third-party action within the workflow can read the.git/config file to obtain the token, potentially escalating privileges beyond what was intended for that specific action [1][9]. Recommendation: - Set persist-credentials: false for any checkout step that does not explicitly require authenticated Git operations (e.g., jobs focused solely on testing, building, or scanning) [1][9]. - If your workflow must perform Git operations (like pushing commits), consider using persist-credentials: false and managing authentication through other methods, or ensure that artifacts are configured to strictly exclude the.git directory (e.g., by using paths to target only specific build output folders) [2][10][12]. Using security auditing tools like zizmor or custom security policies can help detect and enforce the use of persist-credentials: false across your workflows [12][13].

Citations:


Pin all GitHub Actions by commit SHA and disable checkout credential persistence.

Lines 26, 30, 39, and 51 currently use tag refs (@v*) instead of full-length commit SHAs. Tag references are mutable—they can be moved, deleted, or compromised by a malicious maintainer—exposing your workflow to supply-chain attacks. Additionally, set persist-credentials: false on the checkout step to prevent the GITHUB_TOKEN from being stored in .git/config, which reduces the risk of token exfiltration if a third-party action is compromised or if artifacts leak.

Suggested hardening patch
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@<full_commit_sha>
+        with:
+          persist-credentials: false
@@
-        uses: actions/cache@v4
+        uses: actions/cache@<full_commit_sha>
@@
-      - uses: actions/upload-pages-artifact@v3
+      - uses: actions/upload-pages-artifact@<full_commit_sha>
@@
-        uses: actions/deploy-pages@v4
+        uses: actions/deploy-pages@<full_commit_sha>
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 26-26: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 26-26: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 30-30: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 39-39: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 51-51: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/web.yml around lines 26 - 51, Replace all GitHub Actions
version references with full-length commit SHAs to prevent supply-chain attacks
from mutable tag references. Update actions/checkout@v4 on line 26 to use its
commit SHA, actions/cache@v4 on line 30 to use its commit SHA,
actions/upload-pages-artifact@v3 on line 39 to use its commit SHA, and
actions/deploy-pages@v4 on line 51 to use its commit SHA. Additionally, add
persist-credentials: false as an option to the checkout action on line 26 to
prevent the GITHUB_TOKEN from being stored in the git config, reducing the risk
of token exfiltration.

Source: Linters/SAST tools

Comment thread core/src/complement.rs
Comment on lines +90 to +104
pub struct VariationControl {
/// Fraction of the band's scale ladder B may use, `0.0..=1.0`. `0.0` pins
/// every note to the band's anchor degree (a static line, still locked to
/// A's grid); `1.0` uses the whole band — the unconstrained default that
/// matches [`arrange_complement`].
pub pitch_spread: f64,
}

impl VariationControl {
/// The identity control: the whole band, i.e. [`arrange_complement`]'s
/// behaviour.
pub const FULL: Self = Self { pitch_spread: 1.0 };
/// A static line: B collapses onto the band's anchor degree.
pub const LOCKED: Self = Self { pitch_spread: 0.0 };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Chronological commits touching implementation or tests =="
git log --reverse --date=short \
  --pretty=format:'COMMIT %H %ad %s' \
  --name-only -- core/src/complement.rs core/tests/complement.rs

echo
echo "== Commits that touched BOTH implementation and tests in the same commit =="
git log --date=short --pretty=format:'%H|%ad|%s' \
  --name-only -- core/src/complement.rs core/tests/complement.rs \
| awk '
BEGIN { RS=""; FS="\n" }
{
  header=$1; impl=0; test=0;
  for (i=2; i<=NF; i++) {
    if ($i=="core/src/complement.rs") impl=1;
    if ($i=="core/tests/complement.rs") test=1;
  }
  if (impl && test) print header;
}'

Repository: PhysShell/griff

Length of output: 454


Revert to separate commits for tests and implementation.

Your commit 72ae97e7dad3605fbe4ad4cc535aeaf9f71995e3 (2026-06-16) modified both core/src/complement.rs and core/tests/complement.rs together. The Rust coding guidelines require that new pub fn or pub struct implementations must be committed separately from their test coverage.

To comply:

  1. Create a tests-only commit first with failing tests for the new public API (VariationControl, VariedComplement, VariationError, arrange_complement_varied, and other affected functions)
  2. Then create a separate commit with the implementation

Affected lines: 90-104, 116-134, 698-716

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/complement.rs` around lines 90 - 104, The current commit combines
both implementation and tests together, which violates Rust coding guidelines.
Split this into two separate commits: first, create a tests-only commit
containing all test coverage for the new public API (VariationControl struct
with its FULL and LOCKED constants, VariedComplement, VariationError,
arrange_complement_varied, and any related test functions), written as failing
tests. Then, in a second commit, add the actual implementation of the
VariationControl struct, its constants pitch_spread, FULL, and LOCKED, along
with all the supporting implementation code. This ensures tests are defined
before the implementation they verify.

Source: Coding guidelines

Comment on lines +38 to +41
2. **The M1 MVP is a thin, throwaway front, not egui** — an *import-free*
`cdylib` (`web/`, no `wasm-bindgen`, no framework) that exports three C-ABI
functions, plus a static `index.html` + `app.js` that loads the `.wasm` with
`WebAssembly.instantiate(bytes, {})` and marshals a small JSON result through

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

ABI wording mismatch: this MVP exports two C-ABI functions, not three.

Please update the ADR text to match the implemented/exported contract (arrange, arrange_len, plus linear memory export).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/adr/0024-web-wasm-frontend-for-mobile.md` around lines 38 - 41, Update
the wording in the M1 MVP description to correctly reflect the number of
exported C-ABI functions. Change "exports three C-ABI functions" to "exports two
C-ABI functions" and list the actual exported contract: the two C-ABI functions
arrange and arrange_len, plus the linear memory export. This ensures the ADR
text accurately matches the implemented contract.

Comment thread web/src/lib.rs
Comment on lines +185 to +191
Err(e) => {
// Surface the typed error; still return A so the page can draw it.
let _ = write!(
json,
"\"realized_spread\":0,\"error\":\"{:?}\",\"tracks\":[",
e
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Escape error text before embedding it in JSON.

The error branch interpolates {:?} directly into a quoted JSON string. If the debug text contains quotes/backslashes/control chars, the payload becomes invalid JSON and the browser parse path fails.

Suggested fix
         Err(e) => {
             // Surface the typed error; still return A so the page can draw it.
+            let err = json_escape(&format!("{e:?}"));
             let _ = write!(
                 json,
-                "\"realized_spread\":0,\"error\":\"{:?}\",\"tracks\":[",
-                e
+                "\"realized_spread\":0,\"error\":\"{}\",\"tracks\":[",
+                err
             );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/lib.rs` around lines 185 - 191, The error text is being directly
interpolated into a JSON string using `{:?}` format without escaping special
characters. This can produce invalid JSON if the error message contains quotes,
backslashes, or control characters, causing browser parsing to fail. Before
embedding the error into the JSON output in the error branch of the write!
macro, properly escape the error string to ensure it produces valid JSON.
Consider using a JSON escaping function or library that converts the error debug
representation to a properly escaped JSON string value.

Comment thread web/src/lib.rs
Comment on lines +209 to +264
pub extern "C" fn arrange(mode: u32, seed: u32, offset: i32, variation: f32) -> *const u8 {
let json = build_json(mode, u64::from(seed), offset, variation);
OUT.with(|o| {
*o.borrow_mut() = json.into_bytes();
o.borrow().as_ptr()
})
}

/// Length in bytes of the JSON stored by the last [`arrange`] call.
#[no_mangle]
pub extern "C" fn arrange_len() -> usize {
OUT.with(|o| o.borrow().len())
}

#[cfg(test)]
mod tests {
use super::build_json;

#[test]
fn every_mode_emits_part_a_and_well_formed_header() {
for mode in 0..6 {
let j = build_json(mode, 5, 0, 1.0);
assert!(
j.starts_with("{\"ppqn\":480,\"tempo\":120"),
"mode {mode}: {j:.60}"
);
assert!(j.contains("\"tracks\":["), "mode {mode}: has tracks");
assert!(j.contains("\"role\":\"a\""), "mode {mode}: has part A");
assert!(j.ends_with('}'), "mode {mode}: closed object");
}
}

#[test]
fn counter_melody_succeeds_on_the_uniform_sample() {
// mode 5 = counter_melody; the sample is uniform 4/4, so no NonUniformTimeline.
let j = build_json(5, 0, 0, 1.0);
assert!(j.contains("\"error\":null"), "expected success: {j:.120}");
assert!(j.contains("\"role\":\"b\""), "counter_melody emits part B");
}

#[test]
fn pitch_spread_changes_rhythm_lock_output() {
// mode 0 = rhythm_lock: the knob must move B's pitches.
let locked = build_json(0, 5, 0, 0.0);
let full = build_json(0, 5, 0, 1.0);
assert_ne!(
locked, full,
"pitch_spread must change a grid-locked complement"
);
}

#[test]
fn deterministic_for_identical_args() {
assert_eq!(build_json(5, 7, -12, 0.5), build_json(5, 7, -12, 0.5));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Public C-ABI function line commits:"
git blame -L 208,220 -- web/src/lib.rs | awk '{print $1}' | sort -u

echo
echo "Test block line commits:"
git blame -L 223,264 -- web/src/lib.rs | awk '{print $1}' | sort -u

echo
echo "Overlapping commits between API and tests (non-empty needs review):"
comm -12 \
  <(git blame -L 208,220 -- web/src/lib.rs | awk '{print $1}' | sort -u) \
  <(git blame -L 223,264 -- web/src/lib.rs | awk '{print $1}' | sort -u)

echo
echo "Chronological file history for manual confirmation:"
git log --reverse --oneline -- web/src/lib.rs

Repository: PhysShell/griff

Length of output: 270


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Commit blame for public C-ABI functions (lines 208-220) ==="
git blame -L 208,220 -- web/src/lib.rs | head -5

echo
echo "=== Commit blame for test block (lines 223-264) ==="
git blame -L 223,264 -- web/src/lib.rs | head -5

echo
echo "=== Get unique commits for each section ==="
PUBLIC_COMMITS=$(git blame -L 208,220 -- web/src/lib.rs | awk '{print $1}' | sort -u)
TEST_COMMITS=$(git blame -L 223,264 -- web/src/lib.rs | awk '{print $1}' | sort -u)

echo "Public function commits: $PUBLIC_COMMITS"
echo "Test block commits: $TEST_COMMITS"

if [ "$PUBLIC_COMMITS" = "$TEST_COMMITS" ]; then
    echo "SAME COMMITS: Both sections were introduced in the same commit(s)"
else
    echo "DIFFERENT COMMITS: Sections were introduced separately"
fi

echo
echo "=== Full commit log for web/src/lib.rs ==="
git log --oneline -- web/src/lib.rs | head -20

Repository: PhysShell/griff

Length of output: 1234


Commit split violation confirmed: public API and tests introduced together.

The public C-ABI functions arrange and arrange_len (lines 209–217) were introduced in the same commit as the tests covering them (lines 223–264). Both sections carry commit hash 72ae97e ("Add web playground (WASM) MVP for mobile testing"), violating the requirement that "new pub fn or pub struct implementation" not be committed alongside their covering tests.

The commits must be split: tests should be committed first (in a failing state if possible per TDD), followed by a separate commit introducing the public functions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/lib.rs` around lines 209 - 264, The public C-ABI functions arrange
and arrange_len were introduced in the same commit as their test cases,
violating the commit split requirement that tests must be committed separately
from their implementation. To fix this, reset your current commit and split it
into two: first commit the test module (the tests block containing
every_mode_emits_part_a_and_well_formed_header,
counter_melody_succeeds_on_the_uniform_sample,
pitch_spread_changes_rhythm_lock_output, and deterministic_for_identical_args),
then in a separate commit add the arrange and arrange_len public extern
functions along with the supporting code they depend on.

Source: Coding guidelines

ADR-0024: a browser front so the complement engine can be driven — and heard —
from a phone, no install. The MVP is a deliberately thin, throwaway front
(ADR-0024 §2): an import-free `cdylib` (`web/`, no wasm-bindgen, no Trunk)
exporting three C-ABI functions, plus a static HTML/canvas/WebAudio page that
loads the .wasm with `WebAssembly.instantiate(bytes, {})` and reads a JSON
result from linear memory. Live controls for mode, seed, register offset, and
the ADR-0023 pitch_spread knob; deterministic.

To make the module import-free, gate griff-core's Guitar Pro importer behind a
default-on `gp` feature (guitarpro/zip → time/getrandom → wasm-bindgen). `web`
builds with `default-features = false`, leaving a ~90 KiB import-free wasm; the
CLI and tests keep `gp` on and are unchanged (verified clippy/tests on and off).

Host-side unit tests cover the JSON path (every mode well-formed, counter_melody
succeeds on the uniform sample, pitch_spread moves rhythm_lock, determinism).
Includes web/build.sh, a GitHub Pages workflow, and README. Audio is a
placeholder WebAudio synth; the egui frontend (ADR-0016) and a SoundFont are M2.

Stacked on the VariationControl branch (PR #63), which it depends on.

https://claude.ai/code/session_01TTUbGjzD8ysnVnCJnZJE95
@PhysShell
PhysShell force-pushed the claude/web-playground-mvp branch from 72ae97e to 79bcb1a Compare June 16, 2026 16:30
@PhysShell PhysShell changed the title Add VariationControl: deterministic pitch-spread knob for grid-locked complements Web playground (WASM) for driving the complement engine from a phone Jun 16, 2026
@PhysShell
PhysShell merged commit b122a96 into main Jun 16, 2026
1 check passed
PhysShell pushed a commit that referenced this pull request Jun 16, 2026
…I count

CodeRabbit on #65:
- web/src/lib.rs: json_escape the error Debug repr before embedding it in the
  result JSON (quotes/backslashes would otherwise break the browser parse).
- .github/workflows/web.yml: drop pages/id-token from the workflow-level
  permissions (keep contents: read), scope them to the deploy job, and set
  persist-credentials: false on checkout (zizmor: excessive-permissions,
  artipacked).
- docs/adr/0024 + web/README: the crate exports two C-ABI functions
  (arrange, arrange_len) plus the linear memory, not three.

https://claude.ai/code/session_01TTUbGjzD8ysnVnCJnZJE95
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants