libpd-wasm npm MVP tracker

Intent

Build a small npm package that lets web-app authors load Pd patches in the
browser using prebuilt libpd-wasm runtime packages, and optionally analyze Pd
patches before runtime to see which packages they appear to need.

This file is a working tracker, not a public roadmap. Keep it updated as pieces
land.

MVP scope

- Provide a TypeScript API for parsing and analyzing one Pd patch source string.
- Ship the generated object manifest created by the wasm build.
- Use the manifest to classify patch object usage as supported, unsupported, or
  unknown.
- Expose enough result data for apps to compare patch requirements with the
  packages they chose to include.
- Let runtime users explicitly specify which prebuilt packages they want loaded,
  such as vanilla, cyclone, or else.
- Map requested packages to the runtime bundles that actually exist today
  instead of implying arbitrary dynamic linking.
- Document the supported setup used by this package instead of attempting broad
  bundler compatibility.

Out of scope for the first npm MVP

- Supporting every bundler configuration.
- Recursive abstraction resolution from the filesystem or network.
- Dynamic patch loading semantics beyond reporting unknown/dynamic object names.
- Full Pd editor round-tripping.
- GUI patch rendering.
- Runtime package auto-configuration for arbitrary host apps. The user selects
  packages explicitly; analysis can advise, but should not hide that decision.

API design rules

- Array fields are required and use [] for empty values. Do not use optional or
  nullable arrays in the TypeScript API.
- Prefer explicit package lists over implicit runtime guessing.
- Use string[] for package names and other build-defined identifiers. Avoid
  union literal types for names that will grow as new externals/packages are
  added.
- Keep diagnostics in returned values rather than throwing by default. Reserve
  thrown errors for invalid API usage or unrecoverable runtime setup failures.

Runtime package model

- A package is a build-defined runtime capability. Initial package names are:
    vanilla
    cyclone
    else
- TypeScript accepts a required package list from the caller and maps it to one
  of the packaged runtime bundles that exists today.
- Current runtime bundles are:
    basic: libpd + vanilla/Pd built-ins
    cyclone: basic + cyclone
    else: basic + else
    full: basic + cyclone + else
- Current package-to-bundle mapping:
    [] -> basic
    [vanilla] -> basic
    [cyclone] -> cyclone
    [vanilla, cyclone] -> cyclone
    [else] -> else
    [vanilla, else] -> else
    [cyclone, else] -> full
    [vanilla, cyclone, else] -> full
- If a patch uses an object from a package the caller did not include, runtime
  loading/object creation may fail. The analyzer exists to catch this earlier
  and report a useful explanation.
- The generated manifest is the source of truth for which objects belong to
  which package.
- The MVP should ship generated runtime assets in the npm package instead of
  expecting consumers to fetch them separately from release artifacts.
- Runtime helpers should not silently fall back from a requested package bundle
  to basic. The current demo does that for convenience, but the npm API should
  report a clear setup error.

Supported setup note

- The initial package should document the setup it is built and tested with.
- If users need another setup, they can open a PR or adapt the open-source repo.
- Avoid adding compatibility code until there is a concrete, tested need.
- Use one documented example app/setup for the first release: a small Vite app
  that exercises the npm package as a consumer would. The in-repo demo proves
  the repository build works; the Vite example should prove that an external
  app can install and bundle the npm package.

Initial public TypeScript shape

checkPatch(options: {
  packages: string[];
  files: Record<string, string>;
  entry: string;
  manifest?: PdObjectManifest;
}): PatchCheck

createPd(options: {
  packages: string[];
  files: Record<string, string>;
  entry: string;
}): Promise<Pd>

getControls(options: {
  files: Record<string, string>;
  entry: string;
}): PdControl[]

exposeControls(options: {
  files: Record<string, string>;
  entry: string;
}): {
  files: Record<string, string>;
  controls: PdControl[];
}

The runtime object shape stays small at first. The important boundary is that
packages, files, and entry are explicit. Runtime bundle selection is internal:
public callers choose packages, not worklet bundle names.

checkPatch uses the bundled generated manifest when options.manifest is omitted.
Callers can provide a custom manifest for tests, custom builds, or future
per-library package setups without changing the function shape.

type PatchCheck = {
  ok: boolean;
  messages: string[];
}

The public manifest format is versioned:

type PdObjectManifest = {
  version: number;
  libraries: Record<string, { objects: string[] }>;
}

The runtime helper should expose the capabilities that worklet.js currently
supports:

- load a virtual file collection and open one entry patch
- close the current patch
- bind and unbind receivers
- send bang messages
- send float messages
- receive Pd print lines
- receive float messages from bound receivers

Symbol, list, and arbitrary Pd message sends are part of the runtime helper
because the worklet protocol supports them.

Follow-up TypeScript shape

analyzePatchSet(options: {
  entry: string;
  files: Record<string, string>;
  packages: string[];
}): PdRuntimeReport

Patch-set analysis should let callers provide a virtual file collection instead
of asking OCaml to read paths, fetch URLs, or know about host-specific project
layouts.

Patch-set analysis v1 should require an explicit source map. Do not emulate
Pd's full path/search semantics in the first version.

Milestones

[x] Finalize parser/analyzer public API
    - Keep the OCaml-facing functions JSON-based initially.
    - Public TypeScript exposes checkPatch(options): PatchCheck instead of the
      detailed parser/analyzer report.
    - PatchCheck keeps the stable public contract small:
        ok: boolean
        messages: string[]
    - The public manifest shape is versioned. The TS wrapper translates it to
      the flat JSON shape currently consumed by OCaml.
    - Do not expose parsePdFile, analyzePatch, normalizePackages, or Melange
      core bindings from the main package API.

[x] Build Melange output
    - Build Melange output for the browser-facing parser/analyzer.
    - Current npm build invokes melc directly through
      npm/scripts/build-melange.mjs. This avoids the current Nix/Dune package
      mismatch while still compiling the OCaml parser/analyzer to JS.
    - flake.nix + direnv are the supported build environment. The Melange
      compiler comes from the flake-provided melc. The npm melange/melange.js
      packages remain runtime dependencies because generated JS imports them.
    - Confirm the generated JS can be imported from TypeScript.
    - Keep the native manifest generator separate from the Melange build.

[x] Package generated manifest
    - Build step should produce build-wasm/pd-object-manifest.json.
    - The generated manifest should use:
        { version: 1, libraries: { [name]: { objects: string[] } } }
    - npm build runs npm/scripts/sync-manifest.mjs before TypeScript. It turns
      build-wasm/pd-object-manifest.json into npm/src/generatedManifest.ts.
    - Parser-only development builds use an explicit empty fallback when the
      wasm manifest artifact does not exist yet. Use npm run build:manifest:strict
      when packaging should require the artifact.
    - analyzePatch(source, { packages }) should use the packaged manifest by
      default.

[x] Add TypeScript package skeleton
    - Decide package location, likely npm/ to avoid disturbing the C/libpd repo
      layout.
    - Add package.json, tsconfig, src/index.ts, src/types.ts.
    - Add a build command that produces dist JS and declarations.

[x] Implement TypeScript analyzer wrapper
    - checkPatch(options) imports the Melange-generated analyzer from npm/dist
      after npm build.
    - Parse JSON returned by OCaml.
    - Return a stable ok/messages result for casual browser users.
    - The bundled manifest export is generated by npm/scripts/sync-manifest.mjs.

[ ] Patch-set analysis
    - Add analyzePatchSet({ entry, files, packages }) after the single-source
      analyzer works.
    - Let TypeScript, Node, browser upload flows, or app code gather files.
      Keep OCaml pure and browser-safe by analyzing only provided source text.
    - Resolve possible abstractions conservatively:
        manifest hit -> supported runtime object
        source-map hit -> local abstraction, analyze recursively
        dynamic name or missing source -> unknown/unresolved
    - Report visited files, recursive object usage, unresolved abstractions, and
      diagnostics without throwing by default.

[x] Runtime bundle selection
    - createPd({ packages, files, entry }) is the public runtime entrypoint.
    - The caller chooses packages explicitly. Do not silently infer or mutate
      the package list from analysis results.
    - Package lists are internally mapped to the available prebuilt worklet
      artifacts.
    - selectRuntimeBundle is intentionally not public API. Bundles are current
      implementation details; packages are the public concept.
    - Reject package requests clearly when no prebuilt bundle can satisfy them.

[x] Package wasm/worklet artifacts
    - Ship the generated artifacts needed by the documented setup:
        assets/libpd-worklet.js
        assets/libpd-worklet-cyclone.js
        assets/libpd-worklet-else.js
        assets/libpd-worklet-full.js
    - Keep generated artifacts out of source control unless release packaging
      requires otherwise.
    - npm/scripts/copy-assets.mjs stages worklet assets from the repository
      build output into npm/assets during npm run build.
    - package.json exports ./assets/* so browser bundlers can resolve worklet
      assets from the package instead of from demo/.
    - First-release integration should document a Vite-tested asset URL such as
      importing libpd-wasm/assets/libpd-worklet.js?url or equivalent. Keep the
      API browser-only.
    - Split libpd.js/libpd.wasm packaging can be revisited later if we expose a
      non-worklet integration path.

[x] Minimal runtime helper
    - Include a small browser helper for the documented supported setup.
    - Avoid general bundler abstractions at this stage.
    - Runtime helper failures should make missing package/runtime setup issues
      clear, but analysis remains the nicer preflight path.
    - createPd loads an AudioWorklet, opens a virtual file collection, exposes
      node/audioContext, supports close/loadPatch, and can send/subscribe to Pd
      messages.

[x] Runtime message API expansion
    - Add worklet protocol support for symbol, list, and arbitrary Pd messages.
    - Add matching TypeScript helpers once the worklet supports those messages.
    - Cover message encoding with focused tests so lists, symbols, selectors,
      numbers, and escaped atoms behave predictably.

[ ] Verification before publish
    - dune runtest --force from pd_parse/
    - build-wasm
    - npm build
    - npm test
    - scripts/build-demo-lib
    - npm pack --dry-run
    - Smoke-test install from the packed tarball in a tiny Vite example app.
    - CI should run the current build/test/package dry-run path on pushes and
      pull requests. The Vite tarball smoke test should be added when the
      example app exists.

[ ] External Vite example
    - Add a small examples/vite app that imports the package the way a browser
      app would.
    - Keep it separate from demo/: demo/ is the repository demo and can use
      in-repo generated artifacts; examples/vite should catch package-consumer
      problems such as missing published assets, unresolved Melange runtime
      imports, or worklet URL mistakes.
    - Use the same minimal feature set as the package API: createPd, checkPatch,
      getControls/exposeControls, explicit packages, and a small virtual patch.
    - Next step: adapt examples/vite to import worklet assets from
      libpd-wasm/assets/* now that npm build stages those files.

[ ] Release package metadata and docs
    - Pick the npm package name, initial version, license metadata, repository
      metadata, and package description.
    - Remove "private": true only when npm pack contents and the Vite smoke test
      are correct.
    - Add package README or README section for the browser-only setup:
        install
        import
        choosing packages
        providing files/entry
        connecting audio
        sending controls/messages
        serving/copying runtime assets
    - Keep the public API small for the first release. Do not expose parser
      internals or runtime bundle selection to solve packaging documentation
      problems.

Resolved design choices

- The first useful npm package should include both analyzer helpers and a
  minimal runtime helper, because the package goal is making Pd usable from web
  apps, not only inspecting patches.
- Generated worklet/wasm artifacts should be included in the npm package for
  the documented setup. They should remain build artifacts, not manually edited
  source files.
- The documented setup should be a single tested setup. Do not attempt broad
  bundler support in the MVP.
- The documented setup should be a small Vite example using the npm package as
  a normal consumer, not only the existing in-repo demo.
- The npm package does not bundle the Melange runtime into its own dist output
  for the MVP. Generated JS imports melange/melange.js runtime modules, and
  the documented Vite setup is expected to bundle them into the app.
- Packages are public API intent; bundles are current implementation artifacts.
  The MVP maps package lists to the finite bundles already produced by
  worklet.js/build-wasm instead of dynamically linking packages.
- selectRuntimeBundle is not public API. createPd owns bundle selection.
- Runtime package fallback should be explicit opt-in later, not default npm API
  behavior.
- Patch-set analysis v1 should require callers to provide an explicit virtual
  file map. Pd-like path/search behavior can be added later when there is a
  concrete need.
- Demo patches can be browser-adapted assets. If a curated patch needs stable
  receive names, manifest defaults, or a direct dac~ output path, keep that
  change in demo fixtures and tests. Do not add patch-specific exceptions to
  createPd, getControls, or worklet.js.
- `$0`-scoped Pd symbols are internal patch implementation details, not good
  browser API names. Demo/app-facing patches should expose stable receive names
  such as play, cutoff, tempo, or width.
- Demo and package builds that include external packages must use a strict
  generated object manifest. Empty manifest fallback is only for parser-only
  development before the wasm build has run.

Future considerations

- Verify the combined cyclone + else bundle in a browser demo with a patch that
  uses objects from both packages.
- Add Pd-like search/path behavior for patch-set analysis after the explicit
  source-map API is working and tested.
- Add package/backend support beyond the documented setup when someone brings a
  concrete use case or PR.
- Consolidate package-prefix normalization. The current working implementation
  strips prefixes for top-level runtime files and for embedded abstractions, but
  those paths should eventually share one tested implementation or one generator
  step.
- Keep build scripts thin. Build scripts can orchestrate CMake, OCaml, npm, and
  asset copying, but parsing, manifest extraction, and Pd source normalization
  should move into testable modules/tools when the behavior grows.
