Skip to content

Instantly share code, notes, and snippets.

@transcendr
Last active May 26, 2026 18:28
Show Gist options
  • Select an option

  • Save transcendr/bc190808a48f1c42274dc91ae01d27ca to your computer and use it in GitHub Desktop.

Select an option

Save transcendr/bc190808a48f1c42274dc91ae01d27ca to your computer and use it in GitHub Desktop.
ICD Design Doc Example

Design: Exactly 32 char encrypted value fix

Intent / problem statement

A migrated Go UserEntries read can return invalid replacement-character text in the review field for entries whose stored encrypted review value is exactly 32 hexadecimal characters. Flutter treats a non-empty review for free write, highlight, and lowlight entries as JSON and calls jsonDecode(review), so the malformed value can crash the journal page after entries download.

The user outcome is: journal entry list reads should preserve the legacy JS-visible contract where an encrypted empty review decrypts to the empty string, allowing Flutter to use Review.none() instead of parsing garbage as JSON.

Goal initialization and repository guidance

  • Active goal state was inspected with get_goal; it is already initialized from the reusable iterative-code-design goal template.
  • Target design document: ~/dev/work/reflection/go/reflection_backend/.ai/docs/js-go-migration/len-32-cyphertext-fix.md.
  • Minimum effort floor: 5 minutes. This is an earliest-eligible floor only; the design is complete based on evidence and audit, not elapsed time alone.
  • Repository guidance inspected:
    • AGENTS.md: requires searching PROJECT.md before exploratory repository search; this was done first.
    • PROJECT.md: confirms DDD migration direction, encryption compatibility requirement, Secret Manager ownership, and targeted test practices.
    • .ai/docs/architecture-and-system-design-style.md: absent.
    • .ai/docs/code-style.md: absent.
  • Design-only scope: this document may be edited; runtime/source implementation is not part of this goal.

Scope and non-goals

In scope

  • Define the safest implementation shape for exactly-32-character encrypted values.
  • Preserve Go/JS encryption compatibility for migrated entry read paths.
  • Minimize blast radius by changing one ownership boundary instead of adding per-endpoint or frontend special cases.
  • Define deterministic unit tests and live validation gates for a future implementation.

Non-goals

  • No source-code implementation in this design goal.
  • No Firestore data migration or document rewrite.
  • No Flutter parser workaround unless later evidence disproves the backend encryption-format cause.
  • No redesign of the UserEntries wire contract.
  • No broad clean-up of encryption logging, Secret Manager setup, or entry mapper architecture beyond this defect.

Current evidence and inspected files

Repository and guidance evidence

  • AGENTS.md: project-specific search discipline and validation/review expectations.
  • PROJECT.md: Go backend architecture, DDD migration status, test setup rules, Secret Manager ownership, encryption compatibility, and targeted HTTP test pattern.
  • .ai/docs/graphql-to-go-direct-http-migration-framework.md: migrated endpoints should keep thin HTTP handlers, domain/application ownership, targeted validation, and stable client-visible contracts.
  • .ai/issues/open/ISSUE-001-entry-user-entries-http-list-fetch.md: UserEntries must preserve Flutter-consumed fields, including review, and keep entry parsing stable.
  • .ai/issues/open/ISSUE-006-entry-migration-cross-cutting-shared-seams.md: entry read shaping is intentionally shared; decryption/defaulting belongs in a shared read seam, not per endpoint.
  • .ai/docs/shared-direct-http-client-architecture.md: entries already use direct HTTP through Go Cloud Functions.

Backend code evidence

  • internal/infrastructure/encryption/encryption_service.go:
    • Encrypt emits hex(16-byte IV) + hex(ciphertext).
    • Decrypt currently tries standard format only when len(value) > 32.
    • Values with len(value) == 32 skip standard parsing and fall into legacy parsing.
    • Legacy parsing treats the first 16 characters as an 8-byte IV and the remaining 16 characters as ciphertext, then pads an 8-byte IV to 16 bytes before AES-CTR.
  • internal/infrastructure/encryption/encryption_service_test.go and internal/infrastructure/encryption/encryption_test.go:
    • compatibility tests exist for normal non-empty encrypted values.
    • there is no deterministic test for an IV-only encrypted representation.
  • internal/entries/application/entry_read_shaper.go:
    • Text and Review both use decryptFieldSafely.
    • empty stored fields return empty without decrypting.
    • decrypt errors fall back to ciphertext, but the current exact-32 failure is worse because legacy decrypt returns garbage without an error.
  • internal/entries/application/entry_write_normalizer.go:
    • migrated Go writes encrypt review only when the request review is non-empty.
    • this avoids creating new encrypted-empty reviews from Go, but does not fix legacy/prod data already stored as IV-only encrypted review values.
  • internal/entries/domain/user_entries_types.go:
    • UserEntryResponse.Review is a string field in the stable list response.
  • internal/entries/infrastructure/stored_entry_mapper.go:
    • Firestore review is preserved as a string and passed to the read shaper.
  • http_user_entries_handler.go and internal/entries/interfaces/cloudfunction/user_entries_handler.go:
    • top-level handler wiring delegates to the entry application service and returns success: true plus entries.

Flutter and legacy JS evidence

  • ~/.codex/worktrees/4f55/flutter/lib/features/entries/backend/entry_list_http_client.dart:
    • decodes the Go HTTP envelope and passes each entry row to the parser.
  • ~/.codex/worktrees/4f55/flutter/lib/features/entries/services/entry_remote_service.dart:
    • maps payload['review'] to a string and constructs Entry with reviewFromTypeAndJson.
    • parse failures are logged per entry, but the journal flow can still surface a web crash when bad UTF-8 / invalid JSON propagates through the browser runtime.
  • ~/.codex/worktrees/4f55/flutter/lib/features/entries/models/entry.dart:
    • reviewFromTypeAndJson returns Review.none() only for null or empty review.
    • for free write, highlight, and lowlight, a non-empty review is decoded as JSON.
  • ~/dev/work/reflection/js/reflection_backend/functions/src/helpers/encryption.js:
    • legacy JS treats strings with length at least 32 and no spaces as encrypted and delegates to Cryptr decrypt.
    • this supports the interpretation that 32-character Cryptr values are encrypted values, not plaintext.
  • ~n/dev/work/reflection/js/reflection_backend/functions/src/helpers/encryption.spec.js:
    • legacy tests cover non-empty encrypt/decrypt but not empty encrypt/decrypt.

Validation probes already performed for design evidence

  • Firestore REST read of the cited entry key 055b418d-e5fb-4014-8a44-e8a3fbf1c794 showed:
    • text length 196, standard encrypted shape, decrypts cleanly to valid HTML.
    • review length 32, hex-looking IV-only shape.
  • Local OpenSSL AES-256-CTR probe using the production Secret Manager key material only in-process, without recording the secret, showed:
    • standard parse of that 32-character review as IV + empty ciphertext yields empty plaintext.
    • current legacy-style parse of the same value yields invalid UTF-8 bytes that render as replacement characters.
  • Firestore projection query against the current prod test user, selecting only key, type, and review, found 25 of 50 sampled entries with review length 32 and 25 with empty review. No journal text or review content was selected.
  • Attempted local JS helper execution failed because the local legacy JS workspace lacks the cryptr dependency, so legacy behavior is grounded in inspected source and existing tests rather than a local JS runtime run.

Baseline design inventory

id source decision location current assumption why it matters now pressure signal
B1 existing code Encryption format ownership is centralized in EncryptionService, which exposes only Encrypt and Decrypt to callers internal/infrastructure/encryption/encryption_service.go Callers should not know IV/ciphertext layout details The exact-32 ambiguity is a crypto-format interpretation problem, so the first design question is whether to fix it at this boundary or leak format checks into entry shaping If a future field needs field-specific crypto semantics, EncryptionService may be too generic and need a typed encrypted-field wrapper
B2 existing code Go standard encryption emits hex(16-byte IV) + hex(ciphertext) Encrypt in internal/infrastructure/encryption/encryption_service.go Any valid standard encrypted value has at least a 32-character IV prefix; empty ciphertext is representable as exactly 32 characters The current Decrypt condition len(value) > 32 excludes the valid IV-only representation If evidence shows the Go/JS standard cannot produce IV-only values, this design should be revisited
B3 existing code Decrypt attempts standard parsing only for values longer than 32 characters, then falls back to legacy parsing Decrypt in internal/infrastructure/encryption/encryption_service.go Encrypted values always contain at least one ciphertext byte after the IV This assumption is too narrow for encrypted empty strings and causes exact-32 values to be treated as legacy ciphertext If production data contains legitimate 8-byte-IV legacy values of length 32, exact-32 parsing is ambiguous and needs an explicit compatibility decision
B4 existing code The legacy fallback supports 8-byte IVs by padding them to AES block size Decrypt in internal/infrastructure/encryption/encryption_service.go Some older stored values may not use the standard 16-byte IV format The fix must not delete broad legacy support for values that genuinely fail standard parsing If logs or fixtures prove common 8-byte-IV values, add explicit tests before changing fallback behavior
B5 existing code Empty stored entry fields bypass decryption; decrypt errors fall back to ciphertext EntryReadShaper.decryptFieldSafely in internal/entries/application/entry_read_shaper.go Returning stored ciphertext is safer than failing the whole entry list when decryption errors Exact-32 values currently do not error; they produce garbage plaintext, so this safety net does not activate If more bad decrypts return syntactically valid but semantically wrong text, the shaper may need validation after decryption
B6 existing code Go create/update normalizer only encrypts review when the incoming review is non-empty internal/entries/application/entry_write_normalizer.go New Go writes should not create encrypted-empty review values The bug is primarily about reading legacy/prod data, not new Go write behavior If Flutter starts sending meaningful empty-review tombstones that must be encrypted, write normalization may need a separate decision
B7 existing code UserEntries response preserves the legacy client-visible review string field internal/entries/domain/user_entries_types.go, internal/entries/interfaces/cloudfunction/user_entries_handler.go Flutter owns model parsing; backend owns decrypted field values and stable field names The fix should make review empty when there is no review data, not alter the envelope or add a new field If Flutter changes to tolerate malformed review strings, backend should still preserve the cleaner contract rather than rely on client tolerance
B8 existing code Firestore mapping passes review through as a string without interpreting encryption format internal/entries/infrastructure/stored_entry_mapper.go Persistence mapping should coerce storage types, not decrypt or validate field semantics The mapper should remain unchanged; moving the fix here would mix persistence coercion with crypto semantics If Firestore starts storing typed encrypted blobs instead of strings, mapper ownership would need review
B9 existing code Flutter treats empty review as Review.none() and non-empty review for standard entry types as JSON ~/.codex/worktrees/4f55/flutter/lib/features/entries/models/entry.dart Backend returns either empty string or valid review JSON for standard entry types Returning invalid non-empty review violates this contract and triggers the observed crash path If Flutter changes the model contract to accept opaque review text, the backend contract still needs explicit renegotiation
B10 existing code Legacy JS encryption helper treats strings with length at least 32 and no spaces as encrypted values ~/dev/work/reflection/js/reflection_backend/functions/src/helpers/encryption.js A 32-character no-space string is a decryptable encrypted representation, not plaintext The Go compatibility layer should not reject exact-32 encrypted values before standard decrypt If legacy Cryptr is proven to use a different empty-string format in production, this assumption must be retested
B11 existing tests Existing Go encryption tests cover non-empty compatibility but not IV-only ciphertext internal/infrastructure/encryption/encryption_service_test.go Round-trip tests with non-empty text are enough to prove compatibility The current bug passed because exact-32 was not in the test matrix If tests remain only integration/live-data based, this bug can regress silently
B12 issue doc Entry migration requires one shared read-shaping seam for decrypting stored entry fields and endpoint-specific DTOs above it .ai/issues/open/ISSUE-006-entry-migration-cross-cutting-shared-seams.md Read decryption/defaulting should not be duplicated per endpoint A field-specific workaround in UserEntries would violate the shared seam and leave future entry read paths inconsistent If only one endpoint ever reads this field, shared seam pressure weakens, but current docs say shared read ownership is intentional

Proposed behavior

For a future implementation, EncryptionService.Decrypt should treat a valid 32-character hexadecimal string as the standard AES-CTR representation hex(16-byte IV) + hex(empty ciphertext) and return the empty string.

More precisely:

  1. Preserve the current behavior for value == "": return the existing invalid-data error. Entry read shaping already converts empty stored fields to empty strings before decrypting.
  2. Attempt standard format parsing for any value with len(value) >= 32, not only len(value) > 32.
  3. Standard parse succeeds only when:
    • the first 32 characters decode to a 16-byte IV;
    • the remaining string is valid hex, including the allowed empty remainder;
    • the remainder length is even.
  4. If standard parse succeeds with an empty ciphertext remainder, AES-CTR decrypt returns "".
  5. If standard parse fails, preserve the existing legacy fallback path and 8-byte-IV padding behavior.
  6. Do not change the UserEntries response shape, Flutter parser, Firestore data, or entry mapper.

This changes the narrowest correct ownership boundary: the crypto-format parser. It avoids adding len(review) == 32 checks in EntryReadShaper, UserEntries, or Flutter.

Architecture and file ownership

Primary implementation owner

  • internal/infrastructure/encryption/encryption_service.go
    • Owns AES-256-CTR format parsing and compatibility with legacy Cryptr-like stored values.
    • Should receive the semantic fix.
    • A small private parsing helper is acceptable if it improves readability and makes tests precise, for example parseStandardCiphertext(value string) (iv []byte, ciphertext []byte, ok bool, err error).

Primary test owner

  • internal/infrastructure/encryption/encryption_service_test.go
    • Should add deterministic tests for the exact-32 IV-only case using the existing mocked secret-manager test service.
    • Should add a regression test that invalid short/non-hex values still error as before.
    • Should add a compatibility assertion that a known non-empty legacy fixture still decrypts.

Non-owners for the fix

  • internal/entries/application/entry_read_shaper.go
    • Should remain a field shaper and error fallback boundary, not a crypto-format parser.
  • internal/entries/infrastructure/stored_entry_mapper.go
    • Should remain storage coercion only.
  • internal/entries/interfaces/cloudfunction/user_entries_handler.go and http_user_entries_handler.go
    • Should remain thin HTTP wiring.
  • Flutter files under ~/.codex/worktrees/4f55/flutter
    • Should not carry backend decryption compatibility workarounds for this defect.

Proposed design decision ledger

id decision location reason assumption related baseline ids
D1 Fix exact-32 handling inside EncryptionService.Decrypt by allowing standard parsing for len(value) >= 32 internal/infrastructure/encryption/encryption_service.go The encrypted-value format is owned by the encryption service; this keeps crypto mechanics out of entry and Flutter layers Exact-32 valid hex is the standard IV-only representation for encrypted empty plaintext B1, B2, B3, B10
D2 Preserve the legacy fallback path for values whose standard parse fails internal/infrastructure/encryption/encryption_service.go Avoids broad compatibility regression for older stored values while correcting the standard parser gate Genuine legacy 8-byte-IV values either fail standard parsing or are rare enough to be covered by explicit fixtures B3, B4
D3 Do not add a review-specific workaround in EntryReadShaper or UserEntries internal/entries/application/entry_read_shaper.go, internal/entries/interfaces/cloudfunction/user_entries_handler.go A local workaround would duplicate crypto-format knowledge and leave other decrypt callers inconsistent The bug is format-level, not entry-list-specific B5, B7, B8, B12
D4 Add deterministic unit coverage for IV-only encrypted values in the encryption package internal/infrastructure/encryption/encryption_service_test.go The current test matrix missed the exact boundary case; a package unit test prevents false green live-only validation The existing mocked test service can construct a service with stable key material without Secret Manager B11
D5 Use live validation only as a post-unit confirmation and never mutate Firestore UserEntries deployed endpoint, Firestore projection probes The defect manifests with production data, but the safe fix should be read-only and data-preserving A read of the cited entry after deployment is sufficient to prove client-visible shape changed from garbage to empty review B5, B6, B7, B9
D6 Keep the UserEntries wire contract unchanged: review remains a string and is empty when no review data exists internal/entries/domain/user_entries_types.go, Flutter entry.dart parser The migrated endpoint promise is stable legacy-compatible row shape; changing DTO fields would widen blast radius Flutter expects empty string or valid review JSON for standard entry types B7, B9, B12

Compression review

review id decision id trigger finding action design update required
C1 B1 The proposed fix touches encryption parsing The existing central encryption boundary is exactly the right place for the behavior; adding entry-level parsing would create a parallel crypto path keep Final design keeps exact-32 parsing in EncryptionService.Decrypt through D1
C2 B2 Exact-32 values are valid under the documented standard IV layout The existing assumption that standard values must be longer than 32 is too narrow rewrite D1 rewrites the parser gate from > 32 to >= 32 while preserving the same standard format
C3 B3 The current fallback receives exact-32 values before standard parsing Keeping this decision unchanged forces the implementation to add field workarounds elsewhere rewrite D1 and D2 update the parser order so standard parse gets first chance for exact-32 values and fallback remains only after standard parse failure
C4 B4 Legacy fallback remains relevant The design reuses the same fallback behavior and does not delete 8-byte-IV compatibility keep D2 explicitly preserves fallback behavior for values that fail standard parsing
C5 B5 Entry read shaper currently cannot detect garbage plaintext returned without error Adding plaintext validation in the shaper would be broader and less precise than fixing the parser keep D3 keeps the shaper unchanged because D1 prevents this specific garbage plaintext
C6 B6 Go writes skip empty review encryption This decision still fits and limits future creation of exact-32 review values by Go writes keep No code shape change required; validation should still cover legacy/prod reads
C7 B7 Wire contract must stay stable Changing the DTO would force Flutter and backend contract changes near the same path keep D6 keeps review as a string with empty meaning no review data
C8 B8 Mapper passes raw review string forward Persistence mapping does not need crypto knowledge; keeping it avoids mixing responsibilities keep D3 leaves mapper ownership unchanged
C9 B9 Flutter parses non-empty standard-entry review as JSON The backend must satisfy the existing client contract rather than requiring Flutter tolerance keep D6 explicitly targets empty string or valid JSON
C10 B10 Legacy JS treats length-32 no-space strings as encrypted The Go parser should be widened to match compatibility expectations; local JS execution was unavailable, so source evidence and prod data guide this decision keep D1 implements the compatibility interpretation in Go; validation includes live endpoint proof
C11 B11 Existing tests missed exact boundary Keeping only non-empty tests would leave the same false-green risk rewrite D4 adds deterministic IV-only coverage
C12 B12 Shared read seam discourages per-endpoint fixes A UserEntries-only branch would violate the shared seam and create nearby duplicated behavior keep D3 keeps endpoint/read-shaper code free of crypto-format special cases
C13 D1 Proposed parser gate could affect all decrypt callers This is intentional because the invalid assumption lives in the shared parser; blast radius is constrained by tests and by only changing successful standard parsing for valid IV-only values keep Validation plan includes encryption package tests plus a live UserEntries probe
C14 D2 Legacy and standard exact-32 formats are theoretically ambiguous The design preserves legacy fallback for parse failures but exact-32 valid hex cannot be both standard-detected and legacy-detected without external metadata defer Risk owner: implementation reviewer. Before merge, scan or sample production for known 8-byte-IV length-32 values outside empty reviews if evidence suggests such values exist; otherwise accept standard-first compatibility as the cleaner contract
C15 D3 Avoiding a local workaround means fixing a shared service Shared service fix is smaller and more consistent than a review-field branch keep No additional local branch is needed
C16 D4 New unit tests touch existing package test setup The package already has one TestMain; tests must reuse it and not add another keep Implementation guidance calls out no additional TestMain
C17 D5 Live validation reads production data Read-only projection and endpoint probes are sufficient; no data write or migration is needed keep Validation plan forbids Firestore mutation
C18 D6 Keeping the wire contract unchanged constrains any frontend workaround That constraint is correct because the issue is backend compatibility, not a new client contract keep Final behavior keeps the row shape stable

Compression actions taken in this design:

  • Rewrote the too-narrow standard parser assumption from len > 32 to len >= 32 in the proposed behavior.
  • Rewrote the test strategy to add deterministic boundary coverage at the encryption package, rather than relying on live Flutter reproduction.
  • Deferred only the theoretical exact-32 legacy 8-byte-IV ambiguity, with a named implementation-review owner and risk check.

Design iteration log

Iteration 1: root-cause and ownership

  1. What functional behavior must change? Exact-32 encrypted values that represent empty plaintext must decrypt to "", so Flutter sees Review.none() rather than invalid JSON.
  2. Does B3 make an existing assumption too narrow? Yes. len(value) > 32 assumes at least one ciphertext byte; encrypted empty plaintext violates that.
  3. Should the fix live in entry read shaping? No. B1 and B12 point to centralized encryption and shared read seams; a field workaround would be a parallel path.
  4. What edge case matters most? Ambiguous exact-32 valid hex could theoretically be 8-byte-IV legacy data. The design handles this as a review risk, not by scattering workaround branches.
  5. What proof is needed? Unit proof of IV-only decrypt plus live proof that the cited UserEntries row returns empty review.

Research added in this iteration: PROJECT.md, AGENTS.md, encryption service/tests, entry read shaper, entry write normalizer, user entries types, Flutter entry parser, legacy JS encryption helper, and Firestore/OpenSSL probes.

Iteration 2: blast-radius isolation and validation hardening

  1. Does D1 touch a broad shared service? Yes, but it touches the exact ownership boundary for encrypted format parsing and avoids broader endpoint/client changes.
  2. How does D2 protect B4? Legacy fallback remains for values that fail standard parsing, including short or malformed standard values.
  3. Can D4 avoid adding fragile Secret Manager dependence? Yes. Existing mocked secret-manager tests can construct EncryptionService; no new TestMain is needed.
  4. Does B6 mean Go cannot create this defect going forward? Mostly for review fields, but legacy/prod data remains and other encrypted fields can still hit the parser boundary.
  5. What false-green risk remains? A test that encrypts only non-empty strings would pass while exact-32 remains broken. The validation plan requires an explicit constructed exact-32 fixture.

Research added in this iteration: issue docs for entry list and shared seams, direct HTTP architecture, Firestore review length projection, and git history/blame for the decrypt parser boundary.

State, replay, lifecycle considerations

  • No persistent application state should be migrated.
  • No Firestore documents should be updated.
  • No replay/idempotency path changes are required.
  • Existing entry create/update replay hashes are unaffected because this is read-path decryption behavior only.
  • Cloud Function lifecycle is unchanged; only deployed code behavior inside the existing encryption service would change after implementation.

UI, command, tool, provider, and platform considerations

  • Flutter UI behavior should improve without any UI code change: affected entries should parse instead of crashing or being dropped due to invalid review JSON.
  • No command/tool API is affected.
  • Google Secret Manager remains the provider for encryption key retrieval; the design does not change secrets access.
  • Firestore remains the persistence provider; the design uses only read-only validation probes.
  • Browser/web platform impact is limited to avoiding invalid replacement-character strings reaching jsonDecode in Flutter web.

Edge cases and failure modes

case expected future behavior validation
Empty stored string EntryReadShaper returns "" before decrypt; direct Decrypt("") keeps existing invalid-data error Existing shaper tests plus no encryption behavior change
Valid exactly 32 hex characters Standard parse as IV-only encrypted value and return "" New deterministic encryption unit test
Valid longer standard encrypted value Decrypt as before Existing round-trip/fixture tests
Invalid non-hex string shorter than 16 Return invalid encrypted value error as before Existing decrypt test
Invalid non-hex string length 16 or more Attempt legacy and return decode error as before Existing/new regression test
Genuine legacy 8-byte-IV value longer than 32 If standard parse fails, legacy fallback handles it as before Existing legacy fixture test
Theoretical genuine legacy 8-byte-IV value exactly 32 Ambiguous; standard-first design would return empty if all 32 chars are valid hex Deferred risk owned by implementation reviewer; sample data or fixture evidence should trigger reassessment
Decrypt returns invalid UTF-8 without error from another malformed value Existing shaper still cannot detect this generically Out of scope unless reproduced after exact-32 fix

Validation plan

Deterministic backend unit tests for future implementation

Run from the backend repo:

go test ./internal/infrastructure/encryption -run 'TestCryptrService_Decrypt|TestCryptrService_EncryptionCompatibility|TestCryptrService_LegacyDecryptionCompatibility' -count=1

Required new/updated test assertions:

  1. Construct an encryptionService with a deterministic test key or use the existing createTestService helper.
  2. Build an exact-32 value from a known 16-byte IV and no ciphertext.
  3. Assert Decrypt(exact32) returns "" with no error.
  4. Assert non-empty encrypted round trips still pass.
  5. Assert the known legacy fixture still decrypts.
  6. Assert invalid values from the existing test table keep returning errors.
  7. Do not add a second TestMain in internal/infrastructure/encryption; the package already owns one in encryption_test.go.

Targeted entry service validation

If implementation changes only encryption service, entry application tests do not need new format-specific stubs. Optional but useful:

go test ./internal/entries/... -count=1

This confirms shared entry read shaping still compiles and existing list/shared tests keep passing. It does not replace the encryption package boundary test because entry tests use stubs and cannot prove AES parsing.

Live validation after deployment

Read-only validation only:

  1. Call deployed UserEntries for the authenticated test account that reproduced the crash.
  2. Inspect the cited entry key 055b418d-e5fb-4014-8a44-e8a3fbf1c794 in the response.
  3. Confirm its review is "" and its text is still valid decrypted HTML.
  4. Reload Flutter web journal page and confirm entries load without the replacement-character JSON error.
  5. Optionally run a Firestore projection query selecting only key, type, and review lengths before/after for sample accounting; do not select journal text and do not mutate data.

False-green risks

  • Passing go test ./internal/entries/... alone can be false green because entry tests stub encryption.
  • Passing non-empty encryption round trips alone can be false green because the boundary bug is exact length 32.
  • A successful endpoint HTTP 200 can be false green if individual review field values are not inspected.
  • A Flutter reload can be false green if the local database cache is not refreshed from remote entries.

Implementation sequence / patch-shape guidance

  1. Open internal/infrastructure/encryption/encryption_service.go.
  2. Refactor the standard parse condition from len(value) > 32 to len(value) >= 32, preferably via a small private helper that makes the exact-32 case obvious.
  3. Ensure an empty value[32:] remainder is valid ciphertext input and results in an empty plaintext byte slice.
  4. Preserve the existing legacy fallback when standard parse fails.
  5. Add the deterministic exact-32 decrypt unit test in internal/infrastructure/encryption/encryption_service_test.go without adding a new package TestMain.
  6. Run the encryption package test gate.
  7. Run targeted entry tests if implementation touched any entry code; otherwise run them as a confidence check, not as the primary proof.
  8. Deploy the affected endpoints only after unit proof is green. Since EncryptionService is linked into multiple functions, at minimum redeploy UserEntries; consider whether other already-deployed entry read functions also need redeploy if they share the same defect path.
  9. Perform read-only live validation for the cited entry and Flutter journal page.

Open questions / decisions needed

  • The only unresolved risk is the theoretical existence of meaningful 8-byte-IV legacy values whose total encoded length is exactly 32. The implementation reviewer owns the decision to accept standard-first behavior or pause for a broader production sample if new evidence appears. Current evidence favors standard-first because the reproduced production values are empty reviews and legacy JS treats length-32 no-space strings as encrypted values.

Completion audit against project guides

  • PROJECT.md DDD guidance: satisfied. The design keeps HTTP handlers thin, avoids adding logic to top-level handlers, and places encryption-format behavior in infrastructure.
  • PROJECT.md encryption compatibility guidance: satisfied. The design restores compatibility for a valid IV-only encrypted representation.
  • PROJECT.md testing guidance: satisfied. The design uses targeted package tests and avoids broad root-level tests that can trigger unrelated init/Secret Manager failures.
  • AGENTS.md search guidance: satisfied. PROJECT.md was searched before exploratory repository search.
  • Entry migration issue guidance: satisfied. The design preserves stable UserEntries row shape and shared read-shaping boundaries.
  • Style guide files: .ai/docs/architecture-and-system-design-style.md and .ai/docs/code-style.md are absent, so no additional local style constraints apply.
description Create a high-quality code design document through an iterative research/design/audit workflow
aliases code-design,design-doc,iterative-design,architecture-design
usage /goal code-design --doc .ai/docs/realtime-voice/design-feature-name.md --title "Feature name" --minutes 7 -- Describe the issue, feature, constraints, known files, and desired outcome
examples /goal code-design --doc .ai/docs/realtime-voice/design-copy-capture-command.md --title "Copy capture command" --minutes 7 -- Design a command that copies the current capture buffer to the clipboard; /goal iterative-design --doc .ai/docs/realtime-voice/design-utterance-aggregation-delay.md --title "Utterance aggregation delay" --minutes 10 -- Design configurable transcript aggregation before capture/backend routing
allow_commands true
command_timeout_ms 10000
command_output_limit 30000

Create a code design document for the requested issue, feature, improvement, or architectural change.

<design_goal_request>

<title>{{title}}</title> {{doc}} {{minutes}} {{args}}

This is a design-document goal, not an implementation goal. Do not edit runtime/source code for the feature unless the user explicitly authorizes implementation in the goal context. Design artifacts, validation probes for the design document, and documentation edits directly needed for the design goal are allowed when appropriate.

Phase 1: Goal initialization and readiness

  1. Confirm that this goal is already initialized from the reusable iterative-code-design goal template.
  2. Confirm the target design document path: {{doc}}.
  3. Confirm the minimum effort floor: {{minutes}} minutes.
    • Treat this as a hard minimum eligibility floor only.
    • Do not use sleep to satisfy the floor.
    • Take all time necessary for a clean, high-quality architectural design.
    • Do not stop just because the floor is met.
  4. Read AGENTS.md and any project-local instructions relevant to the target files/workstream.
  5. Read project design/style guides when present:
    • .ai/docs/architecture-and-system-design-style.md
    • .ai/docs/code-style.md
  6. If this template itself is being revised, read .ai/docs/prompt-template-authoring.md first.
  7. Explicitly restate the design objectives, scope, constraints, and repository guidance before drafting.

Use this initial read-only repository context, then inspect targeted files directly as needed:

<repo_status> !git status --short --untracked-files=all </repo_status>

<existing_design_docs> !find .ai/docs -type f -name 'design-*.md' 2>/dev/null | sort | tail -80 </existing_design_docs>

Phase 2: Design scope and objectives

Design the cleanest, most robust implementation possible for {{title}}, grounded in the provided context and inspected code/docs.

The design must keep the architecture:

  • Encapsulated: separate provider/model/platform mechanics from core domain/routing logic or equivalent ownership boundaries.
  • Provider-neutral where appropriate: avoid vendor-locking behavior in core domain layers. Keep provider-specific behavior under provider-owned modules.
  • Decoupled: minimize scattered conditionals by relying on typed behavior/configuration boundaries.
  • Testable: define deterministic validation, live validation where required, and false-green risks.
  • Maintainable: keep file ownership clear, avoid vague managers/abstractions, and prefer small concrete modules.
  • Design-compressed: when later evidence shows an existing code shape is too narrow, prefer reshaping that existing decision over adding a second nearby path.

The design document must answer:

  • What user/problem outcome is being designed?
  • What current code/docs/evidence are relevant?
  • What architecture and file ownership should be used?
  • What existing code or design decisions constrain this work?
  • Which existing assumptions might become too narrow because of this work?
  • What state, replay, lifecycle, UI, provider, or OS boundaries are affected?
  • What edge cases and failure modes matter?
  • What validation proves the design and later implementation?
  • What is explicitly out of scope?

Phase 3: Baseline design inventory

Before drafting the proposed design, create a Baseline design inventory section in {{doc}}.

This is mandatory. Do not skip it. Do not treat it as optional background research.

The inventory must list the existing design decisions in the current repo that constrain, shape, or risk being invalidated by the requested design.

A baseline decision can come from:

  • current code structure;
  • current file or module ownership;
  • existing types, commands, tools, events, state, lifecycle hooks, provider boundaries, UI boundaries, or persistence paths;
  • prior design docs;
  • issue docs or validation probes;
  • git history for the target files when useful;
  • earlier implementation choices visible in the current code.

For every relevant existing decision, write one row in this format:

id source decision location current assumption why it matters now pressure signal
B1 existing code Realtime requests enter Pi through pi-realtime.request custom messages instead of normal user messages .pi/extensions/pi-realtime/runtime.ts, .pi/extensions/pi-realtime/service.ts Realtime-originated work must remain visibly distinct from typed user turns The new design must preserve backend routing semantics If the design adds another request route or wraps this path, review whether the original route should be widened instead

Rules:

  1. id must be stable within the document: B1, B2, B3.
  2. source must name where the decision came from, such as existing code, design doc, issue doc, validation probe, or git history.
  3. decision must describe the actual design choice, not a vague topic.
  4. location must name concrete files, symbols, docs, or probes.
  5. current assumption must state what the existing design appears to believe.
  6. why it matters now must connect the existing decision to this requested design.
  7. pressure signal must say what kind of future finding would force this decision to be reviewed.

If no baseline decision is found, the design document must include a Baseline design inventory section with:

  • files/docs/probes inspected;
  • why none of them constrain the requested design;
  • what evidence would change that conclusion.

Do not proceed to the main proposed design until this baseline section exists.

Phase 4: Iterative design loop

Iterate continuously through this loop until the design is semantically complete.

Each iteration must include all six steps below. Do not skip the design compression review.

1. Interrogate

For each iteration, formulate and answer the 5 most critical technical questions about the design.

Cover:

  • functional requirements;
  • architecture;
  • code ownership;
  • baseline decisions from the Baseline design inventory;
  • edge cases;
  • maintainability;
  • extensibility;
  • project standards;
  • validation;
  • known risks.

At least one question in each iteration must explicitly reference one or more baseline decision ids, such as B1 or B3.

2. Research

Inspect the codebase, docs, examples, design guides, validation probes, prior issue/design docs, and external docs if needed.

Do not rely on memory when concrete file evidence is available.

Record the important files/docs inspected inside the design document.

When research touches a file, symbol, behavior, state path, provider path, lifecycle hook, command, or tool already named in the Baseline design inventory, record that connection in the design document.

3. Synthesize

Integrate findings directly into {{doc}}:

  • design choices;
  • rejected alternatives;
  • API/type/state sketches when useful;
  • file-by-file implementation shape;
  • validation plan;
  • live-validation requirements and caveats;
  • risks and mitigations;
  • unresolved questions or blockers.

Every new proposed design choice that constrains implementation must be recorded in a Proposed design decision ledger.

Use this format:

id decision location reason assumption related baseline ids
D1 Add a provider-neutral capture buffer service instead of putting capture state in the command handler .pi/extensions/pi-realtime/service.ts, .pi/extensions/pi-realtime/runtime.ts Keeps command wiring separate from capture state Capture buffering is core realtime domain behavior B2, B4

Rules:

  1. id must be stable within the document: D1, D2, D3.
  2. decision must name the actual implementation-shaping choice.
  3. location must name concrete files, symbols, docs, or probes.
  4. reason must explain why this shape is better than a local patch.
  5. assumption must state what the proposed design depends on.
  6. related baseline ids must list every baseline decision this proposed choice touches. Use none only when the choice truly does not touch an existing decision.

4. Design compression review

This step is mandatory in every iteration after synthesis.

Review every row in:

  • Baseline design inventory;
  • Proposed design decision ledger.

For each row, answer these exact checks:

  1. Does the current proposed design touch the same location?
  2. Does the current proposed design reuse the same behavior, state, event, tool, command, provider boundary, UI boundary, lifecycle hook, or persistence path?
  3. Does the current proposed design make the recorded current assumption or assumption too narrow?
  4. Would keeping this decision force the implementation plan to add a new branch, helper, adapter, mode, fallback, wrapper, or parallel path near the same location?
  5. Would rewriting, splitting, or merging this decision make the final implementation smaller, clearer, or more consistent?

For every yes, add or update a Compression review section in {{doc}}.

Use this format:

review id decision id trigger finding action design update required
C1 B2 New command also needs capture text Existing command-local capture shape would create a second capture path rewrite Move capture ownership into provider-neutral service before designing the command

The action must be exactly one of:

  • keep: the existing or proposed decision still fits the final design.
  • rewrite: replace the decision with a broader or simpler design.
  • split: separate two responsibilities that were combined too early.
  • merge: fold the new behavior into an existing path instead of adding another path.
  • defer: leave the pressure unresolved, with the exact reason and risk named.

Operational rules:

  1. If action is rewrite, split, or merge, update the proposed design before continuing to the next iteration.
  2. If action is keep, the finding must explain why the current shape still fits.
  3. If action is defer, the design must name the exact risk left for implementation or future work.
  4. Do not write vague findings like reviewed, looks fine, or no issue.
  5. Do not call the design complete while a compression review says rewrite, split, or merge and the document has not been updated to match that action.

5. Reformat

Review the whole design document for coherence.

Restructure unclear sections so the document becomes the best possible draft so far, not a chronological scratchpad.

The baseline inventory, proposed decision ledger, and compression review must remain readable as durable design evidence. They may be reorganized, but they must not be deleted unless their content is replaced by an equally explicit section.

6. Repeat

Continue iterating until all are true:

  • the {{minutes}} minute floor has been met;
  • the Baseline design inventory exists and is grounded in inspected files/docs/probes;
  • the Proposed design decision ledger covers every implementation-shaping proposed decision;
  • the Compression review has evaluated every baseline and proposed decision touched by the final design;
  • every rewrite, split, or merge action has been reflected in the final proposed design;
  • the design document is complete, internally coherent, and strong enough to guide implementation without major architecture decisions left to the implementer.

Phase 5: Design document requirements

Write the final design document to:

{{doc}}

Recommended structure:

  1. # Design: {{title}}
  2. Intent / problem statement.
  3. Scope and non-goals.
  4. Current evidence and inspected files.
  5. Baseline design inventory.
  6. Proposed behavior.
  7. Architecture and file ownership.
  8. Proposed design decision ledger.
  9. Compression review.
  10. State/replay/lifecycle considerations if relevant.
  11. UI/command/tool/provider/platform considerations if relevant.
  12. Edge cases and failure modes.
  13. Validation plan.
  14. Implementation sequence or patch-shape guidance.
  15. Open questions / decisions needed, if any.
  16. Completion audit against project guides.

Do not include placeholder sections like TODO, TBD, or empty headings. If a section is not applicable, explicitly say why or omit it.

The Baseline design inventory, Proposed design decision ledger, and Compression review sections are not optional. If one of them has no rows, the section must explain why, using inspected evidence.

Phase 6: Final validation and closeout

Before marking the goal complete:

  1. Audit {{doc}} against:
    • AGENTS.md;
    • .ai/docs/architecture-and-system-design-style.md when present;
    • .ai/docs/code-style.md when present;
    • any workstream-specific docs discovered during research.
  2. Verify the document exists and has no placeholders:
    • test -f {{doc}}
    • rg -n "TODO|TBD|FIXME|placeholder" {{doc}} should find nothing unless discussing those words as concepts.
  3. Verify the document is visible or intentionally ignored according to repo policy:
    • git status --short --untracked-files=all
    • git check-ignore -v {{doc}} || true
  4. Verify that the design-compression sections exist:
    • rg -n "Baseline design inventory|Proposed design decision ledger|Compression review" {{doc}}
  5. Verify that compression actions are resolved:
    • No rewrite, split, or merge action may remain without a matching design update described in the document.
    • Every defer action must name the risk, reason, and owner of the future decision.
  6. If the design proposes code changes, specify the exact validation gates a future implementation must run.
  7. Mark the goal complete only after all completion gates are met:
    • minimum floor met;
    • design semantically complete;
    • baseline design inventory completed;
    • proposed design decision ledger completed;
    • compression review completed;
    • unresolved compression actions either applied or explicitly deferred with risk.

Final response must include:

  • design doc path;
  • key design decisions;
  • baseline decisions reviewed;
  • compression actions taken;
  • files/docs inspected;
  • validation/check commands run;
  • unresolved decisions, if any;
  • whether the document is tracked or ignored by git policy.

IMPORTANT:

Do not treat the minimum minute mark as a completion gate, instead of treat it as only the earliest possible wrap-up point.

  1. Iterate until the design is actually high-quality and complete.
  2. Use the N-minute floor only as a minimum eligibility check, not as a completion gate.
  3. If the floor is met but the design still has unresolved structure, weak sections, missing research, or insufficient audit, keep going.
  4. Only mark the goal complete after both are true:
    • floor met
    • design semantically complete

The instruction “take all the time necessary” should override any impulse to stop at the minimum.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment