# Contributing Keep pull requests focused on observable behavior. Explain the change, cover new failure/rejection paths, and update the relevant English and Chinese guides together. Preserve contributor history and keep real credentials out of fixtures. Agent-assisted maintainers should start with [AGENTS.md](AGENTS.md) and the [DSH Mobile maintenance skill](.agent/skills/dsh-mobile-maintenance/SKILL.md). Its references cover compatibility/packaging, runtime/ADB, test-fixture reliability and collaboration/releases; load only the route needed for the task. Update demonstrated reusable guidance alongside the owning change, without turning transient release state into permanent instructions. ## Local setup and baseline Use the Node engine range in [package.json](package.json). From this repository: ```sh npm ci npm run verify ``` `verify` checks version alignment, documentation links/structure, bundled licenses, mobile assets, TypeScript, tests and build/package output. `npm run check:docs` is the quick documentation-only check; it does not judge prose accuracy, external services or rendered appearance. ## Select behavior checks Browser checks consume built client files. Run `npm run build` and `npx playwright install chromium --only-shell`, then choose the affected checks: | Change | Check | | --- | --- | | Question cards | `npm run smoke:question-fixes` | | Layout, drawers and panels | `npm run smoke:native-layout` | | Conversation scrollport and older viewport units | `node scripts/smoke-conversation-scroll.mjs` | | Composer focus and keyboard | `npm run smoke:composer-keyboard` | | Composer controls, typography and wrapping | `npm run smoke:composer-overflow` | | Real InputBar with optional left/right/activity controls | `npm run smoke:composer-toolbar` | | Dictation lifecycle and focus | `npm run smoke:voice-session` | | Browser authentication Cookies | `npm run smoke:browser-auth-cookies` | | Real HTTP/mux recovery and retained document/draft | `npm run smoke:transport-recovery` | | Real cross-client pending questions and explicit answer retry | `npm run smoke:transport-recovery -- --interactions` | | API fallback on older WebViews | `npm run smoke:mobile-compat` | | Async access-panel forms | `npm run smoke:control-state` | | Module-selection recovery | `npm run smoke:module-recovery` | | Local extension Worker execution and explicit recovery | `npm run smoke:extension-workers` | These fixtures do not submit a model request or read the normal DSH home. Browser evidence does not establish physical Android keyboard, camera or device-lifecycle behavior. Transport recovery uses the same isolated packed DSH runtime described below and a loopback-only fault proxy. It requires healthy HTTP during an actual mux interruption, a new 101 handshake and fresh Workspace baseline, and an unchanged document/editor/draft. `--interactions` adds a test-only Host question through the real Loader, checks desktop/mobile delivery and reconnection, and rejects one answer request before an explicit retry. It does not simulate an approval audit or establish a reporter's Cloudflare deployment. `--negative-control-mux` deliberately leaves the mux unavailable and must fail with `Mux recovery negative control detected`; another setup error is not a passing negative control. `--negative-control-import` makes the owned classic-script client fail at its top level before registering its module. The script must download successfully, fail during initial mobile boot, and be identified by its module and resource mapping; only completed cleanup permits the `Module evaluation negative control detected` marker. Both negative controls refuse their marker if diagnostic collection, evidence persistence or cleanup also fails. Startup failures retain bounded boot, response, request-failure and execution evidence under `local/mobile-boot-diagnostics/`. HTTP success alone does not prove that a module executed or registered. ## Test the packed plugin in DSH Use an isolated DSH installation rather than replacing the plugin's development dependencies. This PowerShell example uses the current checked version: ```powershell $dshMobileTestRuntime = Join-Path $env:TEMP ('dsh-mobile-test-runtime-' + [Guid]::NewGuid().ToString('N')) npm install --prefix $dshMobileTestRuntime --no-save --package-lock=false @deepseek-ai/dsh@0.2.0-rc.2 $env:DSH_BOOT_SMOKE_BIN = Join-Path $dshMobileTestRuntime 'node_modules/@deepseek-ai/dsh/lib/bin.js' npx playwright install chromium --only-shell npm run smoke:dsh-boot npm run smoke:dsh-composer ``` The smoke installs the actual npm tarball into an owned temporary profile before pairing. It does not substitute checkout files for bundled components. CI owns the complete version/platform matrix in [.github/workflows/ci.yml](.github/workflows/ci.yml). The shared mobile-profile fixture imports package files with pnpm's `packageImportMethod: copy`. Independent profile directories otherwise can share a content-store hardlink: another import changes its `ctime` while preserving the client bytes, and DSH's metadata-based HMR can publish a different revision during boot. Copies isolate the installed file identities and metadata; this is a development-fixture setting, not a change to user profiles. `node scripts/probe-pnpm-client-metadata.mjs` requires explicit `DSH_BOOT_SMOKE_BIN` and `DSH_BOOT_SMOKE_MOBILE_TARBALL` files and compares `auto` with `copy` in an owned private store. It records dev/inode/link counts, source hashes and live registry revisions; resource reads use `cache: no-store` so a browser's immutable cache cannot establish a server response. For installation changes, run `npm run smoke:plugin-installation` against the same isolated DSH runtime, with `pnpm` available on PATH as required by DSH's plugin manager. CI installs the pinned pnpm version explicitly. The smoke uses DSH's live plugin-manager API and real pnpm installation, verifies Host and Client discovery, and toggles the question-card component independently. Set `DSH_BOOT_SMOKE_NODE_LINKER=isolated` to cover pnpm's isolated layout, or `DSH_BOOT_SMOKE_PREVIOUS_MOBILE_TARBALL` to a verified older plugin tarball to test upgrading. The component is embedded in the main tarball and loaded relative to `cordis.patch.yml`; do not publish it separately or restore a nested `file:` dependency. `npm run smoke:mobile-settings` checks the mounted DSH General settings, local typography and narrow-screen controls. Its native bridge is simulated; physical WebView zoom, keyboard and system-bar behavior require ADB evidence. Compatibility changes also run `npm run smoke:dsh-boot -- --legacy-webview`. The `--negative-control-compat` variation must fail with its expected missing-API marker after the compatibility script is blocked; an installation or fixture failure is not a successful negative control. The conversation scroll check uses the same isolated runtime and packed plugin. Its `--negative-control-viewport` variation removes the height fallback and must fail with `Viewport negative control detected`; it cannot substitute for testing the reporter's actual WebView. The compatibility, viewport and missing-companion negative lanes emit a final `DSH_MOBILE_EXPECTED_FAILURE` receipt only after observing the deliberate fault and completing cleanup. CI validates that exact receipt with `scripts/negative-control.mjs`; a matching error message inside an AggregateError is insufficient. Cleanup failures remain test failures. `npm run capture:screenshots -- --out ` produces credential-masked pairing, conversation, drawer and settings PNGs. `--overwrite` permits replacing existing captures; generated files are not committed automatically. ## Android and optional components Follow the [Android build instructions](apps/mobile/README.md#build) and keep both app manuals aligned. Preserve application identity, stable signing and stored pairings. Never uninstall or clear a user's app to bypass a signing mismatch. Ask before altering real device data; use a separate test package for destructive test flows. Review the internal [design reference](design-system/dsh-mobile/MASTER.md) for native screens and plugin-owned controls. Do not change DSH source to implement plugin features without a separately authorized upstream change. For macOS cloudflared changes, build and run `npm run smoke:cloudflared-component`. It verifies official archives and reinitialization in temporary state, then removes its owned files; it never executes downloaded Mach-O binaries. `-- --archive-dir ` reuses independently verified official archives. The optional Caddy workflow keeps one pinned Windows/Linux x64 double-build/native-test matrix and one generic publisher. PR/ordinary dispatch runs are read-only; both publication inputs default false and simultaneous opt-ins fail early. The fork-only review path remains a labelled TEST prerelease. The official path is restricted to public `saya-ch/dsh-mobile` and the existing exact non-v tag `caddy-component-2.11.6-tencentcloud-0.4.3`; it publishes a non-prerelease, non-latest **component distribution**, never npm/plugin/APK or enabled installation. This is two-stage preparation only. First maintainers must review this workflow PR on protected, reviewed `main`, configure the `caddy-component-release` environment with nonempty required reviewers and prevention of self-review, prohibit admin approval bypass, authorize publication, and dispatch the stable tag. The actual environment policy is fetched before any release mutation; environment naming is not authorization. The contents-write-only token relies on GitHub's documented public-resource exception for environment reads; authenticated 403/other API failures are fatal with no fallback, scope escalation or extra secret. Tag SHA must equal the same-run build SHA and main comparison must establish ancestry (not code review). Actual Actions approval and live authenticated endpoint capability are not proven by synthetic tests. The publisher refuses occupied releases, compares every draft asset, publishes with `--latest=false`, then independently checks every public asset's metadata/bytes through the installer HTTPS redirect validator and ensures the component is not latest. Failure after publication leaves the release public and emits no candidate; no rollback/delete retry. Both review and official candidates keep `productionCatalogEnabled: false`. Later, independently verified official assets require a **separate reviewed catalog PR** with hardcoded source/size/hash pins before installation can be enabled; `CADDY_COMPONENT_RELEASES` remains exactly `Object.freeze({})` in this preparation. Remote JSON never writes runtime trust. Support is Windows/Linux x64 and Tencent Cloud DNS only; replacing a private preview with an empty-catalog official plugin does not promise managed restart. Human live evidence is not final-candidate CI or certificate-renewal verification. See [managed Caddy](docs/CADDY_MANAGED.en.md#maintainer-build-and-validation) and its matching Chinese guide. The Caddy publisher shell test needs Bash on POSIX and Git for Windows on Windows. Windows resolves Bash from the Git installation selected by `git --exec-path`, including custom installation directories; it does not select WSL's `bash` from PATH or load inherited Bash startup hooks. Shell startup failures, timeouts and signals fail the test instead of counting as expected publication rejections. ## Prepare and publish a release Release preparation is not publication. Keep the candidate marked unreleased and retain working stable APK links until publication is authorized. 1. Align the npm package and lockfile versions; use `npm run check:version`. Keep Android `versionName` and `versionCode` unchanged for plugin-only changes. The checker validates Android separately against the published App descriptor in [apps/mobile/release.json](apps/mobile/release.json). 2. Review release notes, app manuals, compatibility statements, contributors, third-party notices and package contents. Preserve original author commits when incorporating community PRs. 3. After publication is authorized, finalize the CHANGELOG date and plugin stable version in both root READMEs before the final validation. Keep APK links and App manuals on the published App version. `check:release-tag` validates these independently and refuses unfinished plugin release documentation. 4. Pass relevant local checks and every applicable CI job for that final candidate, including its release-documentation changes, not only checks required by branch protection. Later changes require the checks relevant to their diff; do not use an earlier commit's green checks as the final candidate's evidence. Record device/network coverage without treating local probes as public-route evidence. 5. Merge the tested candidate, tag that commit with `v` and let [.github/workflows/release.yml](.github/workflows/release.yml) build and publish the plugin. Verify npm/GitHub package equality and checksums afterward. Link the current App download in the plugin release notes; an unchanged App does not get a new APK. If npm already contains the tagged version after partial publication, the workflow compares its public `dist.integrity` with the candidate tarball before continuing GitHub publication. Matching bytes permit completion; a mismatch or registry error stops the workflow. Keep the already published package intact and reconcile the completed stages before retrying. For native changes, increase Android `versionName` and `versionCode`, document the App changes, complete Android validation and use an `android-v` tag. [The Android workflow](.github/workflows/android-release.yml) builds and publishes the signed APK independently, without publishing npm or replacing GitHub's latest plugin release. The APK name remains `dsh-mobile-android-v.apk`. After verifying the published APK's checksum, identity, versionCode and established signer, copy the generated `dsh-mobile-android-release.json` into `apps/mobile/release.json` in a reviewed commit and update the App manuals and root APK links. This descriptor names an already published APK; do not advance it for an unpublished candidate. Its legacy `v` tags remain valid, so existing downloads do not move. The plugin checks the public descriptor and falls back to its bundled copy when that lookup is unavailable. Stable Android releases require `ANDROID_KEYSTORE_BASE64`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS` and `ANDROID_KEY_PASSWORD`; npm publication requires `NPM_TOKEN`. Never copy their values into the repository. The Android workflow uses a temporary runner and validates the established single signer. Missing signing configuration fails the Android release; plugin-only publication does not require Android signing credentials. ## Reports and community conduct Describe versions, connection type and reproducible steps in ordinary issues. Redact tokens, Cookies, QR pairing values and private paths. Use [private vulnerability reporting](SECURITY.md#reporting-a-vulnerability) for suspected security flaws. The project follows the [Contributor Covenant](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). Be respectful and thank reports and code contributions without claiming unverified fixes or tests.