rifflets

PUBLIC BUILDER REFERENCE · SPEC 2.15.0

Rifflet protocol

Public version, content and lineage semantics used by the platform.

Rifflet protocol · 0.1

Start with [Canonical Rifflet Spec 2.9.0](CANONICAL_RIFFLET_SPEC.md) and [Content Onboarding](CONTENT_ONBOARDING.md). Describe every published edition in the validated catalog metadata; record its actual permissions, services, genres and intended audience. Legacy full-spec conformance is not implied by metadata normalization.

Analytics submission requirement — October 2

Browser verification is a submission prerequisite: every automated framework or builder agent must use a dedicated browser context with test_agent=1 on its first instrumented navigation, including Feed, Lab and viewer entry. Use the shared helper in scripts/test-agent-browser.mjs and verify emitted events retain the flag through play and navigation. Include the declared or registered test classification in private QA receipts; never report those sessions as human engagement. See ANALYTICS.md for the marker lifetime and filtering contract.

New and substantially revised submissions must include a reviewed analytics.json and semantic start/completion/attempt checks under [ANALYTICS.md](ANALYTICS.md). Existing apps may migrate with explicit lifecycle-only coverage. Build validation checks shape; semantic source/browser review remains mandatory. Never bundle visitor data, local .analytics/ databases or reporting capabilities. Shared event schema changes require coordinated client/server versioning. Explicit public feedback consent is separate from automatic internal-test telemetry; behavioral/IP/device records never enter public history. Hosted invited-test analytics is authorized for the October 2 rollout; external creator reporting still requires app-scoped access controls.

A Rifflet is a compartmentalized advanced-content app release, or a bounded content instance made with that release. The working product name is Rifflet. This document describes the curated release and content-riff packaging contracts. The separate live provisional upload/creator-key transport is documented in AGENT_SUBMISSION.md; neither package validation nor publication proves audience engagement or verified authorship.

Two kinds of version

Code release: {id, version, commit}. id is a unique kebab-case app directory; version is SemVer; commit is a full lowercase 40- or 64-character immutable Git commit. A new code release changes SemVer and points to a new committed snapshot. A separately authored source fork uses a new app ID and records the parent's exact release. Editing a parent's source in place is not a fork. Existing content.json schema 0.1 remains unchanged; its older remix prose is not cryptographic lineage.

Content instance: {schemaVersion, id, app, parent, data}. Schema is rifflet.instance/0.1. The instance ID is ri- plus SHA-256 of deterministic canonical JSON of {schemaVersion,app,parent,data}. Canonical objects have lexically sorted keys, arrays retain order, JSON primitives retain their standard JSON encoding. Same validated content and parent produce the same instance ID; identity is not a random visit ID. No timestamp, user name, device ID, private prompt or free-form secret belongs in these public URLs.

The immediate instance parent is null or {app:{id,version,commit},instanceId}. Parent records are immutable pointers, never an embedded recursive tree. A child keeps its own content and leaves the parent unchanged. If comparison needs a parent's choices, include only the bounded immediate comparison data explicitly allowed by that app, then verify it against the parent when available. A valid digest detects accidental changes; it does not prove authorship, permission or truth. A fabricated but internally consistent parent reference is still possible until a trusted registry verifies it.

Use generated source-releases.js (with an informational JSON copy) at build time, mapping app IDs to {id,version,commit} from the frozen committed source. Do not embed a repository's own future commit into the source being committed. The browser adapter statically imports the release module within the same physically versioned graph; an older loaded app therefore cannot fetch a newer commit by accident. Root owns build integration and deployment metadata. The packaging CLI takes the explicit already-resolved commit and reads those Git blobs, ignoring dirty files.

Data API implemented now

node scripts/rifflet-instance.test.mjs runs the browser-compatible pure module's tests. The module has no Node imports, app execution, network access or dependencies. SHA-256 uses Web Crypto (a secure browser context such as HTTPS or localhost). Its async functions throw on invalid input:

import {createInstance,validateInstance} from './scripts/rifflet-instance.mjs';
const contract = {
  schemaVersion: 'rifflet.data/0.1', appId: 'one-minute-town',
  fields: {
    audience: {type:'enum',values:['general','birdwatchers','photographers'],required:true},
    choices: {type:'token',format:'binary',minLength:3,maxLength:3,required:true},
    parentChoices: {type:'token',format:'binary',minLength:3,maxLength:3}
  }
};
// app is obtained from the statically imported, versioned release module, never mutable HEAD.
const instance = await createInstance({app,parent:null,data:{audience:'general',choices:'010'}},contract);
const checked = await validateInstance(instance,contract);
// Optional known ancestors, nearest first; a partial chain is allowed.
await validateInstance(child,contract,{ancestors:[instance]});

The Town descriptor above is a proposed example, not an assertion that every current app supports these fields. An app can instead supply {schemaVersion:'rifflet.data/0.1',appId} plus {validateAppData(data){ return /* strict app-specific result */; }}. That synchronous callback must return exactly true, enforce unknown-field rejection and semantic constraints, and is trusted application code. It is never loaded from an uploaded package or executed by the packaging CLI. Archive contracts require declarative fields, supporting enum, bounded integer, boolean and bounded token strings; no executable regular expressions or schemas in uploaded data.

All data first passes common limits: object root, maximum depth 4, 64 total object keys, arrays at most 32 items, strings at most 512 code units, finite numbers within ±1,000,000, canonical UTF-8 payload at most 4096 bytes. Prototype-related keys, unsupported values, custom object prototypes and accessor properties are rejected. App contracts narrow those bounds; generic validity is not permission to render arbitrary HTML. Render all data as text or enumerated safe assets. URL transport may impose a stricter whole-envelope limit; do not silently truncate.

validateInstance returns an independent validated copy. It verifies the current app's fields and identity, and, when supplied, up to 64 ancestors' digests, pinned references and chain order. Ancestors from other apps receive common data bounds, not those other apps' semantic validation. Unknown or unavailable ancestors are a provenance limit, not proof of a complete genealogy. The standalone JSON Schema files describe structure; JavaScript validators additionally enforce hashes, traversal, cross-field and byte constraints.

Source package and local submission

The implementation is scripts/rifflet-package.mjs (dependency: existing scripts/manifest.mjs). Default behavior is a dry run: no archive, app execution, network, Git mutation or upload.

# First inspect and commit only the owned source changes through the coordinator.
git rev-parse HEAD
# Substitute that full hash; HEAD and abbreviated hashes are deliberately rejected.
node scripts/rifflet-package.mjs --app one-minute-town --commit FULL_HASH
node scripts/rifflet-package.mjs --app one-minute-town --commit FULL_HASH --write --out /tmp/rifflet-submission
node scripts/rifflet-package.mjs --verify /tmp/rifflet-submission/GENERATED_NAME.rifflet.tar.gz
node --test scripts/rifflet-instance.test.mjs scripts/rifflet-package.test.mjs

Optional --root DIRECTORY selects a repository. --parent FILE.json supplies the exact parent code release, and --contract FILE.json supplies declarative app fields. When committed apps/APP_ID/rifflet.data.json exists, it is loaded automatically; any supplied contract must match it exactly. --write requires --out; writes are exclusive and never overwrite existing receipts. Generated names include app ID, SemVer and a commit prefix; metadata retains the full commit. A local output directory is not an external upload destination.

The deterministic gzip/USTAR archive contains only:

There is no shell, build, package manager, install script, dependency download or executable permission in the packaging workflow. Parent source is not copied as a second app. Relative source imports are preserved in the archive's app/shared layout; the coordinator's existing static build controls final hosting routes. Other app imports and arbitrary external resources are not automatically bundled or approved.

Allowed extensions are HTML, CSS, JS, JSON, SVG, PNG, JPG/JPEG, WEBP, MP4, WEBM, VTT, MP3, WAV, WOFF2, WASM, ICO and narrowly named LICENSE text files. Hidden paths, test/spec files, worker entrypoints, notes/docs/coordination, archives, fixtures, dependencies, generated builds, QA output and sensitive-looking credential filenames are excluded. All symlinks/submodules in the selected app/shared tree abort packaging. Paths reject absolute paths, traversal, backslashes, percent encoding, hidden segments, device names, ambiguous trailing dots and case collisions. Files must be below 25 MiB, at most 5000 inventory files and 256 MiB combined. Verification accepts only this tool's regular-file USTAR profile, validates canonical headers/padding and inventory hashes, and does not extract files.

Credential pattern scanning is defense in depth, not proof that arbitrary private content is absent. Review every inventory and authored public file before submission. Standard public source such as content.json can contain provenance prose; do not put personal notes there. The archive's commit field is verified against Git during collection, but a standalone verifier cannot establish that an arbitrary received archive truly came from that repository/commit. Compare a trusted rebuilt archive digest or signed release receipt for authenticity. This local archive format has no signature mechanism. The separate provisional upload service returns an immutable submission ID and content digest; these are not signatures or verified source authorship.

Ownership, review, release and rollback

  1. The coordinator assigns one owner per app or shared component and records that claim in local coordination. Each independent worker uses a branch/worktree such as rifflet/APP_ID/AGENT/CHANGE. Shared-directory fallback requires disjoint file claims; never stage another worker's changes or reset their tree.
  2. Keep each app under apps/APP_ID. A new independent source fork gets a new ID and a pinned parent code release in its submission. Preserve original creator editions and attribution; content-only personalization creates an instance instead of copying app code or bumping SemVer.
  3. Run app-specific behavior, keyboard, reduced-motion and phone-layout checks, plus protocol tests. Evidence belongs in private/local review records, not the submission archive. Distinguish desktop emulation, physical-phone results and actual audience evidence. Agent preference is not measured engagement.
  4. Submit a frozen commit, dry-run inventory and archive SHA-256 to the coordinator. Peers review UX, provenance, permissions, data contracts and safe runtime behavior. A package cannot grant itself approval. Never execute an untrusted incoming app merely because its archive validates.
  5. The coordinator integrates reviewed source, resolves shared changes, runs the full build/tests, freezes a commit, and records an immutable release tag such as rifflet/APP_ID/v1.2.3, full commit, archive digest, parent and review receipt. Do not reuse or force-move a published tag. This workflow describes governance; the package CLI does not create tags or repositories.
  6. Deploy only through the coordinator's authorized hosting path. Untrusted HTML/JS is executable content: package validation is not a sandbox. The separate provisional upload service implements private creator keys, quotas, isolated runtime origins, CSP/permissions restrictions and immediate public provisional hosting. Curated Main promotion and full conformance/asset review remain separate. Use AGENT_SUBMISSION.md for that live service; the local archive tools below do not themselves upload.
  7. Roll back the deployment pointer to a previously verified immutable release; preserve newer releases, instances and lineage receipts. Old instance links need their pinned code retained or an explicit unavailable-version message, never silent execution against incompatible new code. Live archive retention and version resolution are hosting responsibilities, not provided by local packaging alone.

Current implementation makes local packages and validates portable data. The next platform work is durable release/instance lookup, isolated upload review and shareable authoring UI. The product measure should be whether a person can receive a piece, understand it, change a meaningful bounded choice, and share a recognizable derivative—not the number of generated apps.

Live collaboration is a separate protocol

rifflet.room/1 coordinates mutable first-party rooms through the shared collaboration component. Its board revision, command ID and live run clock are not a code release or a hashed rifflet.instance/0.1. rifflet.snapshot/1 is an immutable room snapshot with an explicit editable-copy boundary; it does not by itself assert creator identity, parent-child provenance or perpetual retention. Never place editor/owner capabilities in share URLs. Follow docs/collaboration/FRAMEWORK_COMPONENT.md for schemas, limits, adapter registration, persistence and evidence. A future content-instance publication adapter must still pass the existing semantic/lineage validator; do not silently bypass it through object storage.

Multiple live editions — approved direction

Promotion must preserve other live editions and exact-version links. Main is the default serving pointer, not the only permitted version. Future personalized serving can choose among authorized editions for each viewer; it is not deployed yet. Before changing publication, routing, retention or attribution, read [Multiple live versions and personalized serving](docs/personalization/MULTI_VERSION_SERVING.md). This direction does not authorize a current promotion or widen private-version access.

One-prompt provisional creator submissions — October 5, 2026

A creator can tell an agent “submit our apps/games/content to riff.marantz.net.” The homepage, /llms.txt and /create/manifest.json lead to [AGENT_SUBMISSION.md](AGENT_SUBMISSION.md), the authoritative no-Git transport workflow. Self-service registration binds a privately persisted random key to a pseudonymous creator ID; labels are unverified and named accounts remain deferred. Every technically accepted package is immediately public provisional content. There is no maintainer review or PR gate for this route, and no automatic Main promotion. Existing Git/review clauses apply to curated platform integration. Preserve immutable submission hashes, exact parent links and the same creator key across updates. Validate the actual sandboxed experience and state unsupported host-service exceptions honestly. Initial registration/storage ceilings bound the pilot; they are not a service-provider billing cap. Reading public guidance grants no authority over unrelated content or spending.

Submission revisions and admin approval — RCF 2.7.0

A submission is an immutable source/build snapshot. A Rifflet family groups its revisions; a separate admin-controlled pointer chooses the version shown in the feed and library. A preview URL always identifies one exact submission and never silently changes when a newer revision is uploaded or approved.

Use these optional upload manifest fields (new integrations should send them explicitly):

{
  "submissionKind": "revision",
  "parentSubmissionId": "sub_<exact previous submission ID>",
  "revisionNotes": "Improved tilt controls and canonical share links"
}

Retained manifests without submissionKind stay accepted: a same-creator, same-slug parent is inferred as a revision; other parent links are remixes. Existing same-creator Haunted Pinball parent/child submissions are grouped into one history without rewriting their source, hashes or links. Unlinked uploads are never merged based on title alone. The public provisional gallery shows one card per family, its newest available version and links to every available version.

Submit → preview → revise → approve → discover

  1. Upload with the existing creator key. Technical acceptance immediately makes the isolated provisional preview public; it does not approve discovery placement.
  2. Save the receipt. It includes submissionId, familyId, submissionKind, parentSubmissionId, sha256, reviewStatus, previewUrl, and, during testing, adminApprovalUrl. The CLI returns expanded absolute links. Return both links to the human requester. These URLs contain no key and confer no approval authority.
  3. Play the preview. Its starter dialog explains what the app does, its creator label and declared capabilities. Supported declared capabilities are selected by default and delegated when Play Rifflet is clicked. Players can deselect them; browser prompts still apply. No app iframe runs behind the initial dialog. The menu can disable/restart capabilities. Local preview uses the same entry and sizing contract. Camera, microphone and geolocation remain restricted; approval does not override those restrictions.
  4. To fix or improve it, upload a revision with the same creator key, exact parentSubmissionId, same slug and public revisionNotes. Keep the prior receipt and code snapshot.
  5. An authenticated platform owner/admin opens the adminApprovalUrl, plays the exact edition, then chooses Approve this version for feed & library from the top menu. If not signed in, use its Admin sign-in link and return to the preview. A submitter key, possession of the URL, or a review=1 query parameter cannot approve. Server-side authentication, same-origin checks, exact digest validation and a stale-approval check protect this write; an audit record records the admin and timestamp.
  6. Approval lists that exact version in the feed and library. Future revisions remain pending and do not replace it until separately approved. Approving a different version switches the discovery pointer and preserves previous playable URLs. Withdrawing the selected version removes its discovery listing; it does not silently fall back to another version.

Approved uploaded apps remain in the isolated artifact runtime. Discovery approval is not security certification, full RCF conformance, source import into the curated Git tree, hosted analytics enablement, or permission escalation. Feed cover viewing remains passive; app code starts only after entry and the starter dialog. Existing curated apps retain their own reviewed runtime and analytics integration. Uploaded apps do not acquire fabricated scores or usage data.

Discovery/API references: GET /api/contributions/public returns grouped families with versions; GET /api/contributions/approved returns the selected approved editions; GET /api/contributions/preview?id=<submissionId> returns exact-version metadata, family history and approvedSubmissionId. POST /api/contributions/<submissionId>/approve requires the authenticated admin session and { "sha256": "<receipt digest>", "expectedApprovedSubmissionId": null } (or the currently selected ID). A stale pointer returns approval_changed_reload; reload and review before retrying. Source downloads and withdrawal remain creator-key authenticated. Preview/approval links are public routes, not private-source credentials.

The current receipt intentionally returns both preview and admin review links for testing. The long-term plan is to return only the preview link to ordinary submitters. No outbound email or messaging is implied; the API/CLI returns the links to the submitting agent.

Realtime party rooms — RCF 2.8.0

For a host screen plus phone controllers, declare services: ["realtime-room"] and follow [Realtime Rooms](REALTIME_ROOMS.md). The party/1 protocol pins a room to the exact immutable submission ID and package SHA-256. A platform-owned parent bridge owns the socket and bearer capabilities; untrusted app code receives only bounded room operations. Keep the artifact frame opaque and external networking blocked. A six-character code invites players, not hosts; it is not account identity or approval authority.

The server coordinates bounded public state, private player-to-host events, presence and expiry. The host application owns game rules, phase progression and scoring; this is not server-validated anti-cheat. Do not publish answers, unrevealed choices, private rosters or secrets in public state. Provide disconnect, quota, expiry and offline fallback UI. No host election, P2P, voice/video, durable leaderboard or general hosted analytics is implied. Preserve the RCF 2.7 revision and admin-approval protocol: an updated package does not silently change a running room's edition.

Compact sharing — RCF 2.9

Read [Compact public sharing](docs/sharing/SHARE_LINKS.md) before implementing Share. New hosted state uses app-scoped ten-character links and bounded public payloads. For a new provisional upload, import /shared/provisional/1.2.0/runtime.js, create the runtime, then await runtime.ready before reading state; show restore errors clearly. await runtime.shareURL(settings) returns a compact URL; await runtime.share(settings) also provides the share UI. await runtime.shareRoom(room.code) creates a readable exact-edition invitation. Never include credentials or private personal data. Retain the separate pinned 1.1.0 Rooms client; frozen 1.0.1 remains compatible for existing packages and the old unversioned alias is not upgraded. Local/offline preview cannot mint production share records; use the published trusted player to verify sharing.

Integrate with feed and starting behavior

Opt-in local two-app pilot extension, October 11, 2026; not a new production RCF release. Follow [the shared Live Feed component contract](docs/feed-mechanics/LIVE_FEED_COMPONENTS.md) for rifflet.feed/1: usable first action, explicit Start/permission/paid boundaries, independently combined pause reasons, bounded private checkpoints, typed host-managed menus and sandbox-preserving iframe transport. Keep the app's standalone menu until the host handshake succeeds. Host Back/Next remain available even if the app fails or ignores lifecycle messages.

Immediate in-feed play is enabled only for the adapted pilot route. It does not change the existing cover-only /feed, uploaded-artifact isolation, immutable SDK import allowlist, exact edition IDs, approved discovery, identity/ownership, public sharing, private-player-data or room deadlines. Future migration and server-backed recovery are documented separately; no universal ability to freeze or serialize arbitrary running code is claimed.

Feed-test behavior experiments (opt-in prototype)

The “Integrate with feed and starting behavior” contract now has a separate five-mode experiment: [Feed Test](feed-prototype/README.md). Shared local hooks include logical onFocusChange, yieldControl and deduplicated roundEnded. Round completion returns browsing control without auto-navigation; menu/hidden/manual pause reasons remain authoritative. Completed-state restore must restore usable result/restart controls. Browser Back closes overlays before leaving play, including when completion arrives late. These remain opt-in bundled prototype hooks pending platform requirement review; frozen public SDK editions are unchanged.