Instructions for AI agents and human contributors working on didwebvh-dart, a faithful Dart port of
didwebvh-java.
didwebvh-dart is a pure-Dart (no Flutter) implementation of the
did:webvh v1.0 specification: create, resolve, update, migrate,
and deactivate did:webvh DIDs, with pluggable key management. It is a port of the Java reference library and
must behave identically to it. It is organized as a Dart pub workspace monorepo (Dart 3.6+).
PORTING-GUIDE.md— how the port works and the human-review rule (read first).PORTING-DECISIONS.md— the locked technical decisions and the Java→Dart mapping.PORTING-STATUS.md— tiny iteration index; pick the next one. Full detail per iteration is initerations/— read only the one you're working on.ARCHITECTURE.md— language-neutral design + spec algorithms (from the Java reference).PROMPT.md— the ready-to-paste prompt that runs the next iteration.
The Java source lives, git-ignored, in reference/didwebvh-java/ (see ../reference/README.md). The spec TXT
is at docs/spec/Webvh v1.0.txt.
- Identical observable behaviour to Java. This is a translation. Read the Java source for any behaviour question; don't redesign while porting. The shared cross-language test vectors — not Java's internal implementation shape — are the contract: any code path must produce the same bytes / same outcomes as the vectors. Mirroring how Java does something internally (e.g. its JSON round-trips) is not itself a goal.
- Idiomatic Dart first. When a faithful port and idiomatic Dart pull in different directions, prefer
idiomatic Dart, provided the vectors still pass byte-for-byte. The priority order is: (1) Dart best
practices, then (2) matching Java's exact internal approach. These almost never conflict — but when they
do, choose Dart and document the divergence in
PORTING-DECISIONS.md. The interop vectors are above both and are never traded away. (First applied:Jcs.canonicalizeValuecanonicalizes a decoded value directly instead of replaying Java's encode→parse round-trip — see PORTING-DECISIONS.md §8.) - Simplicity over abstraction. A reader should understand the code without tracing many layers. Patterns
only when they earn their keep (the
Signeradapter is the canonical example). - SOLID, not academic. No interfaces for single implementations beyond the documented extension points.
- Spec fidelity.
docs/spec/Webvh v1.0.txtis the ultimate source of truth; reference section numbers in comments for non-obvious logic. - Test-driven. Every public method has tests; spec logic is gated on the shared vectors.
- Build/monorepo: pub workspaces (Dart 3.6+), no Melos. Root
pubspec.yamlwithworkspace:list. - Packages:
didwebvh,didwebvh_signing_local,didwebvh_wizard. - Crypto:
cryptography(Ed25519;DartEd25519for in-core verification — itsverifyis async-only, so proof verification returnsFuture<bool>),crypto(SHA-256). JCS, multihash, base58btc, multikey are ported internally (byte-exact). - Do NOT use the pub.dev
canonical_jsonpackage — it is OLPC, not RFC 8785, and breaks interop. - JCS API:
Jcs.canonicalizeValue(Object?)is the primary entry point — it canonicalizes an already-decoded JSON value directly (the shape did:webvh code actually holds).Jcs.canonicalize(String)is the convenience for text-only inputs (the SCID{SCID}string-replace step) and just decodes then delegates. This intentionally drops Java's encode→parse round-trip; output is byte-identical (see PORTING-DECISIONS.md §8). - JSON:
dart:convert+ hand-writtentoJson/fromJson; precise null-omit control for canonical lines. No codegen /build_runner. - Config-builder call styles (library-wide): every configurable operation builder (
CreateDidConfig, and by the same decision the futureUpdateDidConfig/MigrateDidConfig/DeactivateDidConfig) supports three interchangeable call styles — (1) fluent chaining (faithful to the Java builder; settersreturn this), (2) cascade (.., free since..ignores the return value), and (3) named parameters at construction (a delegating constructor that applies each non-null argument through its like-named setter — one source of truth for copy/normalization; also forwarded through theDidWebVh.*facade). Keeping the fluent style requires setters thatreturn thisand a positional boolean toggle (e.g.portable(true)), soavoid_returning_thisandavoid_positional_boolean_parametersare suppressed scoped to that builder'slib/src/<op>/directory only (same narrow-scoping as themodel/andwitness/equality exemptions).avoid_positional_boolean_parametersis a recognized false-positive for a single, well-named boolean setter; the alternatives each cost something (drop the fluent style, lose explicitfalse, or mix styles). Value-type configs that are not builders (e.g.ResolveOptions) stay plain named-parameter value types — do not add builder machinery for symmetry. Document the styles once as a library-wide convention (README Usage preamble + one worked example), never per-method. (Decided in iteration 6; seedocs/iterations/06-create.mdandPORTING-DECISIONS.md§8.) - HTTP:
package:httpbehind aRemoteDidFetcher(10s timeout, 200KB cap, as in Java). - CLI:
package:args(CommandRunner);WizardIoabstraction for testable prompts. - Tests:
package:test+mocktail+http'sMockClient. Lint:very_good_analysis. - The intentional delta from Java: the
Signeris async (Future<Uint8List> sign(...)), which ripples intoFuture-returning create/update operations. Proof verification is also async (Future<bool>) — not by design but becausecryptography'sDartEd25519.verifyis async-only (see PORTING-DECISIONS.md §2, corrected in iteration 4); this in turn makes the log-chain validation loop async.
abstract interface class Signer {
String get keyType; // "Ed25519"
String get verificationMethod; // "did:key:z6Mk...#z6Mk..."
Future<Uint8List> sign(Uint8List data);
}This is the primary extension point: local Ed25519 keys (didwebvh_signing_local), AWS KMS, external signing
services, HSMs.
snake_casefile and package names; PascalCase class names (unchanged from Java).- Package-private by default: implementation in
lib/src/; onlylib/<pkg>.dartre-exports the public API (the analog of Java's package-private discipline). - Doc comments (
///) on public classes and methods. - Immutable models where practical; factory/builder construction.
- Null-safety throughout; be explicit about nullable fields, especially where JSON null-omission matters.
- No wildcard re-exports beyond the intended public surface.
- Pass
dart analyzewith zero issues undervery_good_analysis.
package:testfor all tests;mocktailfor mocks;MockClientfor HTTP.- Shared vectors in
packages/didwebvh/test/vectors/(test-vectors/+interop/) are copied verbatim from Java and are the cross-language interop contract. Never edit them. - Unit tests per public class; end-to-end tests for create → update → resolve → validate.
- The gate (run after every change):
tool/verify.sh. This is the single source of truth that nothing is broken — the Dart analog of Java's./mvnw clean verify. It runsdart pub get, workspace-widedart analyze --fatal-infos, anddart testfor every package that has atest/dir (runningdart testfrom the workspace root only prints help, so never rely on that). Passtool/verify.sh --coverageto also emitpackages/didwebvh/coverage/lcov.info. Report its real result (VERIFY OK/VERIFY FAILED, with output). Do not hand-roll the individual commands — use the script so new test folders are always included. - Coverage ≥ 80% on
didwebvh(Codecov);signing_localandwizardexcluded, as in Java.
- ci.yml: on push to
mainand all PRs; SDK matrix (stable / declared minimum / beta); runs the same gate astool/verify.sh(dart analyze+dart test --coverage) → Codecov. - publish.yml: tag-triggered, pub.dev automated publishing via GitHub Actions OIDC (no GPG). After all three
packages publish, a
github_releasejob creates the GitHub Release, using the matching root-CHANGELOG.mdsection (extracted bytool/changelog-extract.sh) as the release notes — mirroring the didwebvh-java reference, which derives its release body from the changelog.
- Lockstep versioning. All three packages share one version and bump together; inter-package constraints use
^<that version>. Never bump one package alone. - Bump with the wizard. Run
tool/bump-version.sh— an interactive tool that updates everypubspec.yamlversion:, the inter-package^constraints, the README install snippet, the generatedversion.g.dart, and every CHANGELOG (promotingUnreleased→ the new dated version and adding a fresh emptyUnreleased). It asks patch/minor/major or an explicit version, always confirms, and double-confirms downgrades. Don't hand-edit versions across files. - Two changelog styles, by design:
- Root
CHANGELOG.mdfollows Keep a Changelog:## [Unreleased]and## [X.Y.Z] - DATE, withAdded/Changed/Fixedgroupings. This is the project-level log and the source of GitHub release notes. - Per-package
CHANGELOG.mduses the plain Dart-idiomatic style thatdart createemits and pub.dev expects:## Unreleasedand## X.Y.Z - DATE, with a simple bullet list. - pub.dev accepts either style; the split keeps each package's log conventional while the root stays a richer
Keep a Changelog.
tool/changelog-extract.shunderstands both.
- Root
- The wizard's version is read from its
pubspec.yamlvia the generatedlib/src/version.g.dart(regenerate withdart run tool/generate_version.dart);tool/verify.shfails if that file is stale.
After completing any change, the agent MUST:
- Run the gate
tool/verify.shand confirmVERIFY OK(report the real output if it fails). This is the mandatory end-of-change check that nothing is broken. - Update the changelog(s) — the root
CHANGELOG.mdunder## [Unreleased](Added / Changed / Fixed), and any affected package'sCHANGELOG.mdunder its plain## Unreleasedbullet list — referencing the spec section or the ported Java class for non-obvious behaviour. See Versioning & Changelogs above. - Propose a Conventional Commits message — and stop.
Nothing is committed without a human review, and the agent never commits on the human's behalf. The agent
leaves the iteration [~]; the human reviews against the Java reference, commits, flips the iteration to
[x], and records the commit in the Progress log. See PORTING-GUIDE.md.
- Read
PORTING-GUIDE.md, thenPORTING-DECISIONS.md. - Ensure
reference/didwebvh-java/is present (../reference/README.md). - Open
PORTING-STATUS.md, take the first[ ]iteration (only one in flight); read itsiterations/NN-*.mddetail file. - Port faithfully from the Java reference; read the Java source for any behaviour question.
- Run the gate (
tool/verify.sh); report the real result; copy the shared vectors where the iteration calls for it. - Update the changelog, propose a commit message, and stop for human review.