mirror of
https://github.com/vczh-libraries/Release.git
synced 2026-10-01 18:22:58 +08:00
442 lines
49 KiB
Markdown
442 lines
49 KiB
Markdown
# !!!LEARNING!!!
|
||
|
||
# Orders
|
||
|
||
- Verify generated artifacts with downstream consumer checks [29]
|
||
- Keep design documentation aligned with code after refactoring [25]
|
||
- Proactively remove code made redundant by refactoring [24]
|
||
- Process staged tasks one by one with verification [23]
|
||
- Port fixes from imports to source repositories [21]
|
||
- Verify and localize portability on every target OS [18]
|
||
- Crash early instead of adding error-tolerance fallbacks [16]
|
||
- Extract abstractions only for real shared behavior [16]
|
||
- Fix behavior at the owning state instead of patching symptoms [13]
|
||
- Validate expectations against implementation and existing tests [12]
|
||
- Make `Stop()` drain asynchronous work before returning [8]
|
||
- Do not assume async callback owners are heap allocated [5]
|
||
- Use `WString::IndexOf` with `wchar_t` (not `const wchar_t*`) [4]
|
||
- Use `collections::BinarySearchLambda` on contiguous buffers (guard empty) [4]
|
||
- Use `vl::Exception` for expected semantic failures and `CHECK_ERROR` for invariants [4]
|
||
- Treat environment correlation as evidence, not a cause [4]
|
||
- Don't assume observable changes are batched [3]
|
||
- Use RAII scope cleanup instead of manual catch cleanup [3]
|
||
- Capture dependent lambdas explicitly [2]
|
||
- Use `ERROR_MESSAGE_PREFIX` for meaningful `CHECK_ERROR` / `CHECK_FAIL` messages [2]
|
||
- Prefer simple calls before interface casts [2]
|
||
- Treat Debug memory leak dumps as required failures [2]
|
||
- Prefer well-defined tests over ambiguous edge cases [2]
|
||
- Prefer raw pointers unless shared ownership is required [2]
|
||
- Start async callbacks after most-derived construction [2]
|
||
- Sort serialization metadata by deterministic keys, not pointer addresses [2]
|
||
- Prefer `operator<=> = default` for lexicographic key structs [1]
|
||
- Prefer two-pointer merge for sorted range maps [1]
|
||
- Use named sentinel constants instead of raw values [1]
|
||
- Use `Variant<T...>::Index()` to check active alternative [1]
|
||
- Avoid references into containers when mutating them [1]
|
||
- Prefer designated initializers for aggregate-like structs [1]
|
||
- Construct `Nullable<WString>` explicitly in function calls [1]
|
||
- Do not rely on `Event<T>` handler invocation order [1]
|
||
- `collections::Dictionary` copy assignment is deleted (use move/swap) [1]
|
||
- Dereference `Ptr<T>` via `.Obj()` (not `*ptr`) [1]
|
||
- `vl::regex` separator regex: `L"[\\/\\\\]+"` [1]
|
||
- Use 2-space indentation in embedded XML/JSON literals [1]
|
||
- `collections::List` has deleted copy constructor; use `std::move()` for structs with `List` members [1]
|
||
- Compare type descriptors by pointer when descriptor identity is available [1]
|
||
- Parse and validate before queuing asynchronous work [1]
|
||
- Use Win32 messages for native dialogs when UIA is unavailable [1]
|
||
- Keep generated makefiles platform-invariant [1]
|
||
- Group non-template C++ implementations by class in `.cpp` files [1]
|
||
- Use reentrant POSIX date-time conversions [1]
|
||
- `IRpcJsonMessageDispatcher::InjectException` is persistent and last-write-wins [1]
|
||
- Measure browser images from their own load/error event [1]
|
||
- Keep generator compilation independent from runtime-backed tests [1]
|
||
- Normalize checkout line endings at golden comparison boundaries [1]
|
||
- Preserve ordered HTTP messages at upload and completion boundaries [1]
|
||
- Preserve an existing `Ptr` counter across asynchronous ownership handoffs [1]
|
||
- Reuse established MSBuild project configurations [1]
|
||
- Use explicit positive compiler and platform guards [1]
|
||
- Keep encoding and exception adapters at the C++/JavaScript boundary [1]
|
||
- Revalidate mutable external state immediately before mutation [1]
|
||
|
||
# Refinements
|
||
|
||
## Capture dependent lambdas explicitly
|
||
|
||
When a C++ lambda uses another local lambda (or any local variable), capture it explicitly (e.g. `[&]` or `[CreateBindableTree]`). Lambdas do not implicitly capture other lambdas, and missing captures can show up as confusing compile errors.
|
||
|
||
## Don't assume observable changes are batched
|
||
|
||
When verifying callbacks from an observable collection, do not assume multiple operations collapse into a single notification. For example, for `vl::collections::ObservableList<T>`, `Clear()` followed by multiple `Add()` calls triggers one callback pair per operation; test expectations should match the actual per-operation granularity.
|
||
|
||
The same caution applies to asynchronous queue refactors. Moving consumption behind UI completions or batching same-cycle submissions can preserve final functional assertions while changing allocation, callback, and rendering order. Treat stable scheduling-sensitive artifacts as evidence that the operations were observably distinct.
|
||
|
||
## Crash early instead of adding error-tolerance fallbacks
|
||
|
||
For public API arguments that callers can validate with query methods, fail with `CHECK_ERROR` when the argument is invalid instead of accepting impossible protocol states.
|
||
|
||
When an invariant says a value must exist or a conversion must succeed, prefer using it directly or using a strong cast so a violation crashes or throws immediately. Do not add speculative null checks, weak casts, or silent fallbacks that hide protocol or ownership bugs. Fix the real cause instead of making the code tolerant of states that should be impossible.
|
||
|
||
Build-generation scripts must also preserve failures from compiler or dependency probes across command substitutions and formatting pipelines. Stop before emitting tracked output when a required header or package is missing, and declare the dependency in the canonical environment bootstrap so the original diagnostic is reported instead of a malformed generated rule.
|
||
|
||
For HTTP automation endpoints, return protocol errors such as 404 only for recognized request rejection. Unexpected parser, automation, callback, or invariant failures should reach a process-terminating boundary instead of being translated into a healthy-looking endpoint response.
|
||
|
||
Test applications should also preserve their intended executable boundary. When a lower layer already injects a terminal RPC exception, let the real proxy call escape and crash a fail-fast test app instead of wrapping it in an application-layer close flag, catch adapter, or exit-code shim. Keep a separate error-delivery boundary only for applications that must notify another active process before terminating.
|
||
|
||
## Process staged tasks one by one with verification
|
||
|
||
When a request is split into explicit tasks, complete and verify each task before starting the next one. This keeps commits easy to understand and review, limits side effects to the current task, and avoids having to diagnose many unrelated issues at the same time. If a task has its own finishing instructions, finish that task properly before moving on.
|
||
|
||
When a task boundary says to commit and push, do that before starting the next task so each task remains independently reviewable.
|
||
|
||
When requested for an XML refactor, commit the authored XML changes together first, then commit source, generated artifacts and other remaining changes separately. Honor that review boundary even when the XML commit depends on the following implementation commit.
|
||
|
||
## Use `ERROR_MESSAGE_PREFIX` for meaningful `CHECK_ERROR` / `CHECK_FAIL` messages
|
||
|
||
For source-code `CHECK_ERROR` / `CHECK_FAIL` calls with real diagnostic messages, follow the repo pattern: define `ERROR_MESSAGE_PREFIX` at the beginning of the function with the full class/function context, use it in the message, and undefine it at the end. Simple unsupported stubs such as `CHECK_FAIL(L"Not Supported!")` can stay as-is.
|
||
|
||
## Prefer simple calls before interface casts
|
||
|
||
Do not add explicit interface casting (e.g. `dynamic_cast<IFoo*>(...)`) just because a similar file does it. Start with the simplest direct call, and only introduce interface casts when the compiler or access rules actually require it; unnecessary casts make tests noisier and can obscure which API surface is being exercised.
|
||
|
||
## Prefer well-defined tests over ambiguous edge cases
|
||
|
||
When a scenario’s expected behavior is unclear or undocumented (e.g. calling an operation while an object is in an “invisible”/non-effective state), avoid turning it into a test requirement. Prefer fewer tests that validate the public contract and real-world usage; if an edge case is important, first clarify the intended semantics from implementation and/or documentation.
|
||
|
||
Use timing to diagnose a performance problem, not as a lasting pass/fail assertion. When a dedicated deterministic test already owns startup-retry behavior, shared protocol tests should synchronize incidental listener startup and retain their meaningful repetition, FIFO, framing, routing, and shutdown checks instead of depending on a scheduler race.
|
||
|
||
## Do not assume async callback owners are heap allocated
|
||
|
||
Asynchronous callbacks and handle-close paths must not rely on the owning object being allocated with `new` or outliving callbacks by convention. Track active operations explicitly and make shutdown drain or detach every callback path that can reference the object.
|
||
|
||
When registered waits or async request callbacks can race with `Stop()`, give each wait/request its own context and use atomics to transfer ownership between registration, callback execution, and stop. Do not close handles or buffers until the owner path has either unregistered a not-yet-started callback or waited for a started callback to finish.
|
||
|
||
When a local callback functor owns the last reference to its controller or connection, destroy that functor while the thread still advertises the matching callback-reentrant context. Restoring the callback marker first can make destruction call a wait routine that waits on the current worker itself, leaking or deadlocking the entire lifecycle.
|
||
|
||
## Extract abstractions only for real shared behavior
|
||
|
||
When refactoring client/server or similar paired implementations, extract common state and helper behavior only when it genuinely simplifies both sides. Preserve intentional differences in small derived redirects or callbacks instead of forcing an abstraction just to increase reuse.
|
||
|
||
For role-specific implementations, extract a base that contains only truly shared state and operations. Keep transport-only or role-only members in the concrete subtype so no implementation inherits fields that do not apply to it.
|
||
|
||
When several platform implementations must satisfy the same behavioral contract, define and register each scenario once in a platform-neutral runner and parameterize only the concrete types and genuine platform seams. Duplicating test registrations per platform invites feature drift.
|
||
|
||
Do not turn fixed owner decisions or concrete state observations into callbacks or virtual policy. If the owning process controls whether an operation is called, enforce that choice at the call site; if a predicate only reports internal state, keep it nonvirtual. Reserve virtual functions for behavior that derived implementations genuinely need to change.
|
||
|
||
Likewise, do not pass one-shot callbacks merely to hide mutually exclusive concrete call sites. Prefer typed ownership that exposes the real server or service to the operation that performs the fixed sequence, and use compile-time or overload separation when some application modes genuinely have no such dependency.
|
||
|
||
For environment or lifecycle invalidation, prefer forwarding one application-level notification through the existing ownership tree over registering every affected object as a global listener. This keeps notifications scoped to objects that are actually installed or active.
|
||
|
||
Keep application-specific choices in the application that owns them. When several entry points share one showcase, expose its selection event and attach one shared handler at each entry point instead of adding a callback plugin, selector API or availability state to a reusable skin. A dependency arrangement should make required behavior available in each consumer without teaching the lower layer the showcase policy.
|
||
|
||
When macOS or WebAssembly can reuse most of a Linux implementation, keep the shared implementation in the existing `*.Linux.*` file with explicit positive platform guards and small conditional differences. Delete duplicate platform files and their project/filter registrations; retain separate files for genuinely different implementations such as a browser console bridge.
|
||
|
||
## Make `Stop()` drain asynchronous work before returning
|
||
|
||
If an API exposes `Stop()`, callers should be able to rely on it as the shutdown boundary: after it returns, no pending action, wait callback, overlapped I/O, or async completion should still touch the object. Do not paper over a broken `Stop()` with sleeps in tests; fix the stop path.
|
||
|
||
Use the platform's final callback boundary when available. For WinHTTP async requests, `WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING` is the point to release pending request tracking for successfully submitted requests; release immediately only for failures before submission. For registered wait handles, `Stop()` should unregister not-started waits or wait for active callbacks, then close dependent handles and state.
|
||
|
||
Renderer clients should explicitly stop their network transport before stack-owned channel wrappers are destroyed, so callback shutdown completes before local wrapper storage goes out of scope.
|
||
|
||
Named-pipe shutdown should cancel pending overlapped pipe I/O before waiting for read callbacks to drain, especially when the remote side closes the pipe first. Channel-client destruction should also avoid stopping an already-disconnected transport connection.
|
||
|
||
If `Stop()` is called from the object's own active callback, publish terminal state and cancel pending work immediately, but do not wait on or destroy callback-owned handles from that callback. Defer final draining and handle closure to a later external idempotent `Stop()` or destruction boundary, which must still provide the normal complete-shutdown guarantee.
|
||
|
||
## Port fixes from imports to source repositories
|
||
|
||
Do not treat files copied into `Import` or generated release files as the source of truth. When a fix affects imported `Vlpp` files, make the upstream change in `Vlpp`, regenerate its release output, and then copy the generated files downstream. When a `.github` instruction or script fix is needed, port it through `Tools/Copilot`.
|
||
|
||
When a downstream repo such as `GacUI` exposes a bug in imported `VlppOS` inter-process code, fix and verify it in `VlppOS`, regenerate `VlppOS\Release`, and then import the generated release files downstream.
|
||
|
||
When validating GacUI remoting reveals a transport issue, keep the same source-of-truth rule: fix `VlppOS`, regenerate its release, copy the generated output into `GacUI\Import`, then validate the downstream scenario again.
|
||
|
||
For dependency release syncs, copy generated files from the upstream `Release` folder into the downstream `Import` folder and exclude `IncludeOnly` unless the task explicitly requires it. Do not hand-edit the downstream import copy.
|
||
|
||
When importing multiple dependency releases into GacUI, keep the chain explicit: regenerate and import `VlppOS` and `Workflow` release artifacts, then validate the GacUI remoting scenarios that consume both imported APIs.
|
||
|
||
Shared Ubuntu build infrastructure must be changed in the canonical `Tools/Ubuntu` source first, committed and pushed there, and then propagated into a repository with `vgo uci REPO`. Do not make a propagated repository-local copy the source of truth.
|
||
|
||
If a Workflow task exposes a `VlppReflection` collection-wrapper issue, fix the wrapper behavior in `VlppReflection`, regenerate and verify its release output, then update the Workflow import from that release instead of patching Workflow's imported copy.
|
||
|
||
When a VlppOS public namespace refactor changes released APIs, regenerate the VlppOS release and update Workflow and GacUI from that release before repairing downstream build breaks; do not patch imported copies.
|
||
|
||
## Validate expectations against implementation and existing tests
|
||
|
||
Before encoding expectations (especially for return value conventions and error semantics), read the relevant implementation and check existing tests for established patterns. This reduces churn from mismatched assumptions (e.g. public API returning a normalized error value even if internals use different sentinel codes).
|
||
|
||
This also applies to enums and API surface: verify that enum values and method names/signatures actually exist before using them.
|
||
|
||
Check existing command grammar when a request appears to name an operation incorrectly. Preserve the established command name when the user confirms the wording was a mistake instead of introducing a duplicate alias or unnecessary rename.
|
||
|
||
For regression tests, verify that the proposed case actually distinguishes broken and fixed behavior. A positive control can pass under both implementations, so retain a root-cause-sensitive reproduction and assert the relevant observable boundary, such as callback count and range, rather than only a coarse aggregate result.
|
||
|
||
For performance regressions, establish a same-machine baseline and instrument construction, connection, exchange, draining, and shutdown separately. Before diagnosing a deadlock, identify the exact test that appears stuck and measure how long it runs. Rule out each plausible hypothesis with direct state and timing evidence before changing production code; a plausible platform theory or a nearby commit is not a substitute for measuring the actual slow boundary.
|
||
|
||
## Prefer `operator<=> = default` for lexicographic key structs
|
||
|
||
When a struct is a pure lexicographic key (e.g. `{begin, end}`), prefer `auto operator<=>(const T&) const = default;` to generate a correct, consistent ordering and equality set automatically, instead of hand-writing `<`, `==`, etc.
|
||
|
||
## Use `collections::BinarySearchLambda` on contiguous buffers (guard empty)
|
||
|
||
`collections::BinarySearchLambda` expects a contiguous buffer pointer and count (e.g. `&keys[0]`, `keys.Count()`), plus a search item, an out `index`, and an orderer that returns `std::strong_ordering` or `std::weak_ordering`. When using it on `Dictionary::Keys()`, guard the empty-map case before taking `&Keys()[0]`.
|
||
|
||
If you are searching for “overlap” rather than exact ordering, provide a custom orderer that defines “before / after / overlap” semantics for your ranges.
|
||
|
||
If multiple entries can satisfy “overlap”, binary search can return any matching entry. Scan backward (and/or forward) from the returned index to locate the first overlapping entry you intend to process.
|
||
|
||
For nearest-neighbor lookup on a sorted key buffer, implement helpers for largest key `<= item` and smallest key `> item`, and use those helpers for line/range iteration instead of mixing text positions with raw array indices.
|
||
|
||
## Prefer two-pointer merge for sorted range maps
|
||
|
||
When combining or diffing two maps that are already sorted by range keys, iterate both in one pass using a two-pointer “merge sort merge phase” approach. This avoids nested scans and keeps merge/diff logic linear in the number of runs.
|
||
|
||
## Treat Debug memory leak dumps as required failures
|
||
|
||
On Windows Debug unit-test runs with memory leak checking enabled, a test can pass all assertions and still fail engineering acceptance if the final execution log contains a CRT leak dump. Read the end of the log after tests pass and fix leaks instead of ignoring the appended report.
|
||
|
||
GacUI helper threads and stack-allocated callbacks are common sources of these post-pass leak dumps. Join helper threads and detach callbacks before scope exit instead of treating the final leak report as unrelated noise.
|
||
|
||
## Use named sentinel constants instead of raw values
|
||
|
||
When a protocol needs a sentinel value such as an administrator or system client id, use a named constant and validate it explicitly. Do not scatter raw values like `-1` through call sites; it makes sender/receiver semantics hard to audit and easy to misuse.
|
||
|
||
## Use `Variant<T...>::Index()` to check active alternative
|
||
|
||
For `Variant<T...>`, use `Index()` (active alternative index) to branch on the stored type; do not assume helper methods like `GetType()` exist.
|
||
|
||
## Avoid references into containers when mutating them
|
||
|
||
When iterating a container and performing mutations like `Remove()`/`Add()`, avoid holding references (`auto&&`) to elements across mutations, because internal storage can be reallocated or reordered. Copy keys/values you still need to local variables before modifying the container.
|
||
|
||
## Prefer designated initializers for aggregate-like structs
|
||
|
||
For small structs used as value objects (especially those with default member initializers), prefer designated initializers like `{ .field = value }` for clarity and to avoid compile-time issues from positional aggregate initialization.
|
||
|
||
## Prefer raw pointers unless shared ownership is required
|
||
|
||
When passing an object without transferring or extending ownership, prefer a raw pointer parameter over constructing `Ptr<T>` only to adapt the call. Reserve `Ptr<T>` for APIs that need shared ownership, lifetime extension, or storage in shared-pointer-managed state.
|
||
|
||
## Construct `Nullable<WString>` explicitly in function calls
|
||
|
||
When passing string literals to a function parameter typed as `Nullable<WString>`, wrap them in `WString(...)` (or otherwise construct a `WString`) to make the conversion explicit. Direct assignment to a `Nullable<WString>` field may compile via an assignment operator, but function-call argument conversion can require explicit construction.
|
||
|
||
## Sort serialization metadata by deterministic keys, not pointer addresses
|
||
|
||
When serializing metadata into stable binary output, do not let pointer-address ordering decide indices or item order. Collect items for membership checks if needed, then sort the serialized lists by deterministic keys such as type names or owner-qualified member signatures before assigning indices and writing the stream. This applies to type descriptors, methods, properties, events, and generated custom-type lists whose order would otherwise depend on allocation order or ASLR.
|
||
|
||
## Start async callbacks after most-derived construction
|
||
|
||
Objects that dispatch asynchronous callbacks should not begin listening or queue callbacks from base constructors. Initialize state in constructors, then expose an explicit `Start()` boundary that callers invoke after the most-derived object is fully constructed, so callbacks can safely dispatch to final overrides.
|
||
|
||
When a listener requires a non-owning callback, supply it as part of `Start()` and store it before enabling listener delivery. Do not require subclassing a concrete native server or add a separate callback-installation step that leaves a window where work can arrive without the callback.
|
||
|
||
## Do not rely on `Event<T>` handler invocation order
|
||
|
||
When multiple handlers attached to the same `Event<T>` can both observe, mutate, or throw, do not assume attach order controls which handler runs first. Existing `Event<void(...)>` storage can make invocation order depend on handler pointer ordering, so fixes should remove order dependence instead of moving logic into another handler or making tests depend on allocator behavior.
|
||
|
||
## Use RAII scope cleanup instead of manual catch cleanup
|
||
|
||
When a helper temporarily suppresses callbacks, changes ownership flags, or otherwise establishes scoped state, use a small scope object whose destructor restores state during normal return and exception unwinding. Avoid manual `try`/`catch` blocks that only restore state and rethrow; C++ stack unwinding should own that cleanup.
|
||
|
||
Use the same pattern for recursive readers that push non-owning current-input state before visitor dispatch. A scope object must pop that state on both successful return and nested conversion failure so later recursion never observes a stale input object.
|
||
|
||
Do not add a scope wrapper solely to run normal-path finalization after an exception in a fail-fast test app. When the exception is intended to terminate the process and the OS releases the endpoint, keep ordinary shutdown calls straight-line at the end. Retain scoped ownership where destruction order prevents callbacks from reaching expired stack objects.
|
||
|
||
## `collections::Dictionary` copy assignment is deleted (use move/swap)
|
||
|
||
`collections::Dictionary` does not support copy assignment. When you need to replace one dictionary with another, use move semantics (when appropriate), or rebuild/swap explicitly instead of `a = b`.
|
||
|
||
## Dereference `Ptr<T>` via `.Obj()` (not `*ptr`)
|
||
|
||
`Ptr<T>` is a smart pointer wrapper; to dereference it, use `ptr.Obj()` to get a raw pointer first (e.g. `*ptr.Obj()`). The `*ptr` syntax is not supported.
|
||
|
||
## Use `WString::IndexOf` with `wchar_t` (not `const wchar_t*`)
|
||
|
||
`vl::WString::IndexOf` searches for a single character when given a character parameter; pass character literals like `L'\r'` / `L'\n'`, not string literals like `L"\r"` / `L"\n"` (which are `const wchar_t*`).
|
||
|
||
## Use 2-space indentation in embedded XML/JSON literals
|
||
|
||
When writing XML or JSON inside a C++ string literal (e.g. `LR"GacUISrc(... )GacUISrc"` resources), indent the XML/JSON with 2 spaces (not tabs) to match the repo’s formatting rules for embedded structured text.
|
||
|
||
## Verify generated artifacts with downstream consumer checks
|
||
|
||
When a generator produces artifacts for another language or toolchain, validate the generated output with that downstream consumer instead of only checking that the generator runs. For example, generated TypeScript declarations for JSON output should be type-checked against real generated JSON examples with `tsc --noEmit`.
|
||
|
||
When generated RPC JSON values or request/response transcripts are part of the contract, run the preparation step that copies/regenerates them, include the new folders in the downstream project configuration, type-check them, and sample or audit the generated files for protocol invariants such as request/response id pairing.
|
||
|
||
For generated build files, compare the selected source list with the owning project metadata and platform add/remove rules, then perform a clean build. This catches stale tracked output and generators that appear to succeed after silently losing a compiler or dependency-scanner failure.
|
||
|
||
When generated build metadata must be platform-invariant, generate it with native and simulated host selection and compare tracked `makefile` and `vmake.txt` output byte for byte. Then perform clean and no-op incremental builds and inspect host-local dependency files, ensuring stable tracked output does not break platform-specific includes or incremental rebuilds.
|
||
|
||
For a generated reader that reverses a generated writer, reuse real serialized outputs at every existing logged or golden site, parse them through the public input parser, perform an exact writer-reader-writer round trip, and require byte-for-byte equality. Add focused valid and malformed inputs for every generated field category and validation branch so a successful generator run cannot hide an incomplete reader.
|
||
|
||
When a generator produces runnable sample applications, verify the generated output through the actual app workflow too. For example, generated ChatBot RPC code should be checked by running the server and multiple clients through joins, chat messages, client exit, and server shutdown, not only by confirming generation succeeds.
|
||
|
||
When CodePack ownership is split, inspect the exact generated code pairs and their dependency boundaries. Confirm neutral output excludes platform-only dependencies, ordinary library pairs do not absorb the helper layer, and generated formatting is preserved rather than hand-edited.
|
||
|
||
When a shared dispatcher schema such as `Rpc.d.ts` changes, type-check the shared schema itself as well as generated fixtures so envelope changes are caught even before concrete generated values instantiate every request shape.
|
||
|
||
For a released VlppOS namespace change, validate Workflow through the ChatBot SOP and validate GacUI through `RemotingTest_Core /RPT /Http` with `RemotingTest_Rendering_Win32 /Http`, plus GacJS against the HTTP remoting core. An upstream build alone does not prove the imported public surface works.
|
||
|
||
When relocating manually maintained configuration beside generated types, validate both development architectures and the normal and IncludeOnly release consumers. Generate prerequisite types before packing and verify that each canonical configuration implementation appears exactly once; a successful development build can otherwise conceal stale release dependencies.
|
||
|
||
When a new platform backend is packed into an existing release pair, compile and run a separate consumer of that generated pair. A passing source-project build cannot prove that release guards expose supported APIs exactly once or that deliberately unavailable services still fail as specified.
|
||
|
||
For compiler refactors intended to preserve valid resource behavior, rebuild the deployed release tools, invalidate resource caches that do not track compiler binaries, and inspect each downstream architecture's outputs. A driver that catches errors and continues cannot be validated by its exit alone. Explain the complete generated diff, including unchanged consumers, and avoid inserting runtime checks for constraints that the authoring compiler can validate directly.
|
||
|
||
## `vl::regex` separator regex: `L"[\\/\\\\]+"`
|
||
|
||
In `vl::regex::Regex`, both `/` and `\\` are escaping characters, and incorrect escaping inside `[]` can throw errors like `Illegal character set definition.`
|
||
|
||
To split paths by either `/` or `\\`, a verified pattern is `L"[\\/\\\\]+"`, and using `Regex::Split(..., keepEmptyMatch=false, ...)` conveniently drops empty components (so `//` behaves like `/`).
|
||
|
||
## `collections::List` has deleted copy constructor; use `std::move()` for structs with `List` members
|
||
|
||
When a C++ struct contains `vl::collections::List` fields, the struct's implicit copy constructor is deleted. Appending such structs to a `vl::collections::List` by copy will fail at compile time with `error C2280`. Use `std::move()` when adding these structs to destination lists (e.g. `list.Add(std::move(model))`). Note: there is no repo-provided `Move()` utility; always use `std::move()` from `<utility>`.
|
||
|
||
## Use `vl::Exception` for expected semantic failures and `CHECK_ERROR` for invariants
|
||
|
||
When a failure is part of the public or script-visible semantics and tests are expected to catch it as a recoverable error, throw `vl::Exception`. Reserve `CHECK_ERROR` / `CHECK_FAIL` / `vl::Error` for internal invariant violations and states that indicate implementation corruption. For example, duplicate RPC registration can remain a catchable semantic exception when samples intentionally verify it, while impossible local type ids should fail as invariants.
|
||
|
||
At script-visible reflection boundaries, translate recoverable collection operation failures into `vl::Exception` so Workflow `catch` blocks and RPC exception transport can observe them normally; do not expose `vl::Error` for expected user-data access failures.
|
||
|
||
Generated parsers and deserializers should also throw `vl::Exception` for malformed external input, including unknown or duplicate fields, wrong value kinds, unknown discriminants, and incompatible nested types. Keep `CHECK_ERROR` for generator or runtime states that valid generated code cannot reach.
|
||
|
||
## Compare type descriptors by pointer when descriptor identity is available
|
||
|
||
`GetTypeDescriptor<T>()` and `GetTypeDescriptor(typeName)` guarantee one descriptor instance per type in a loaded type manager, so prefer direct `ITypeDescriptor*` pointer comparison over comparing type-name strings. Use `TypeInfo<T>::content.typeName` only where the type manager cannot be loaded yet; if a name lookup is unavoidable, resolve the descriptor once and compare pointers inside hot or repeated paths.
|
||
|
||
## Parse and validate before queuing asynchronous work
|
||
|
||
When an API needs to report syntax or validation errors synchronously but execute accepted work asynchronously, split those phases explicitly. Parse and validate the request on the caller/transport path, return errors immediately, then queue only a parsed command object for main-thread or background execution. This keeps modal or blocking work off the response path without hiding malformed input behind an async boundary.
|
||
|
||
## Use Win32 messages for native dialogs when UIA is unavailable
|
||
|
||
When native Windows dialogs must be handled under conditions where UI Automation is unreliable or unavailable, use screenshots and Win32 enumeration/messages from a separate process. Enumerate top-level and child windows, read text/class/control ids, then use standard messages such as `BM_CLICK`, `WM_SETTEXT`, and combo-box selection messages to confirm, cancel, or fill controls. Do not wait indefinitely on the application under test while a native modal dialog is open; that app may be blocked until the external Win32 operation closes the dialog.
|
||
|
||
## Proactively remove code made redundant by refactoring
|
||
|
||
When a change makes a construction unnecessary or no longer meaningful, delete it as part of the same change instead of leaving it behind. This includes redirection/adapter layers that only forward, transport methods that exist solely for a now-collapsed path, specialized helpers superseded by a generic one, duplicated post-processing a callee already performs, and null checks the callee already handles (e.g. when `BoxValue`/`UnboxValue` already accept null, or when an `Invoke*` helper already reads/checks the result). Do not leave redirections that exist only because of history. It is acceptable to take the risk of breaking tests while removing redundant code, and then fix the tests afterward, rather than preserving dead structure to keep tests green.
|
||
|
||
Remove unused dependency parameters and fields as part of the refactor too. If every meaningful call path already receives a preconverted representation, do not keep serializer or adapter state only for a fallback that current callers neither need nor should use.
|
||
|
||
Preserve helper layers that still own observable behavior. For example, flat RPC dispatcher wrappers that only forward can be removed, but JSON wrappers that record generated TypeScript artifacts should stay until their recording responsibility is moved elsewhere.
|
||
|
||
When a destructor only resets `Ptr`, shared-pointer, or similar owning members to null, remove that destructor/reset code and let member destruction release resources naturally.
|
||
|
||
When splitting a monolithic implementation into focused files, delete empty source stubs and duplicate state fields in the same cleanup. Update project metadata and includes immediately so the old file names do not remain as stale references.
|
||
|
||
For application refactors, remove helper wrappers that only duplicate an already-clear direct call. For example, direct `GetChannels()[WString::Unmanaged(RpcChannel)]` access is preferable to a `GetRpcChannel` helper when the intended behavior is still to fail if the channel is missing.
|
||
|
||
When a shared event-information type can own a newly common field, move the field there and delete modifier-only wrapper types and parallel overload plumbing that no longer represent a distinct concept.
|
||
|
||
When a documented build invariant guarantees that `CPP_TARGET` and its package outputs are files under `Bin`, removing `Bin` already cleans them. Remove redundant file-by-file cleanup instead of preserving commands for target layouts the build no longer supports.
|
||
|
||
Before adding persistent validation state, check whether the existing input contract can provide the complete set of values in one batch. If it can, validate with local state and remove the state map, wrapper type and alternate authoring paths that existed only to support incremental arrival. Preserve ordinary runtime mutation when the restriction belongs specifically to authoring.
|
||
|
||
## Keep design documentation aligned with code after refactoring
|
||
|
||
When a refactoring changes architecture or behavior, update the corresponding design documents in the same task rather than deferring it. After a structural change, re-read the related documents and reconcile anything that became misaligned (for example, descriptions of a transport path that no longer exists). Treat documentation drift left by a previous refactoring as part of the current cleanup.
|
||
|
||
Public namespace refactors require corresponding knowledge-base and documentation-site updates; publish the site when its generated or documented surface actually changes.
|
||
|
||
Once a shared behavioral suite already exists, follow-up platform implementation task documents should point to binding and running that suite rather than requesting duplicate test cases. Reuse authoritative platform details from existing design documents and preserve explicit out-of-scope sections.
|
||
|
||
Keep API comments and sample handler names synchronized with refactored event signatures and names; mechanically working examples can still teach an obsolete public model.
|
||
|
||
When a defect or user correction exposes misleading authoring guidance, correct the owning guidance along with the implementation. Keep historical verification records labeled as historical instead of rewriting them to claim that a later behavior was already tested.
|
||
|
||
## Fix behavior at the owning state instead of patching symptoms
|
||
|
||
When a bug is caused by state attached to a temporary or overly broad owner, move the state to the object or region that semantically owns it. Avoid compensating fixes such as forcing override values on every affected descendant, duplicating a template/resource just to mask inherited state, or adding side-channel code that only makes the symptom disappear. If a proposed fix looks like a patch, revisit the ownership boundary and preserve naturally correct inherited/default behavior for unaffected parts.
|
||
|
||
When an incremental API receives encoded code units but downstream logic consumes complete logical values, keep partial assembly or decoding state in the stream-processing object. Do not change the public input unit or force every caller to assemble complete values merely to fit an internal helper.
|
||
|
||
When a lifecycle guarantee applies to every subclass, publish completion and final state in the common native entry/exit path. Do not patch one derived class with a private completion event or duplicate the guarantee only in factory-created wrappers.
|
||
|
||
When shared code and tests pass on other platforms but fail on one target, fix the failing platform-specific implementation instead of weakening shared behavior or changing already-correct tests.
|
||
|
||
When a layered transport needs a stricter response policy than its general parser, enforce that policy in the object that owns the physical connection. Keep the lower parser reusable, and let the connection owner classify the response, report a structured failure, and stop or retry the transport as appropriate.
|
||
|
||
When HTTP long-poll delivery needs a bounded acknowledgement, put the deadline on the transport's acknowledgement transition instead of adding a timeout to a transport-independent RPC response wait. A locally successful response submission does not prove peer receipt. Arm the deadline only after delivering a nonempty server message, use the client's replacement poll as the implicit acknowledgement, cancel the deadline when that poll arrives, and report expiry through the transport's local-error path. Keep idle long polls unbounded when the protocol intentionally has no heartbeat.
|
||
|
||
When a native window-style update changes keyboard focus or responder ownership as a side effect, preserve and restore the previous owner at the style-changing boundary. Restore focus only when the affected view already owned it; do not activate an inactive window or steal focus from another responder. Standalone startup may hide the problem by activating the window afterward, while an already-visible remote renderer receives no later activation to repair the lost state.
|
||
|
||
When a compiler diagnostic lacks a source location, repair the argument or syntax metadata where the position should be produced. Do not make a downstream loader guess a location that the common compilation pipeline already knows.
|
||
|
||
## Treat environment correlation as evidence, not a cause
|
||
|
||
When a bug reproduces on one machine but not another, do not assume hardware speed or timing is causal merely because the machines differ. Reproduce the smallest differing state or response shape deterministically, then identify the missing transition or dependency that the other environment happens to mask.
|
||
|
||
The same standard applies when severe CPU usage coincides with a debugger or terminal session: treat the tool/output feedback path as a hypothesis until resource evidence identifies the consuming process or thread. Do not turn an unobserved incident into a durable application-thread-leak diagnosis.
|
||
|
||
## Verify and localize portability on every target OS
|
||
|
||
Run the relevant tests on every target operating system whose behavior is being claimed, and report only the platforms actually exercised. Use contrasts between passing and failing platforms to narrow investigation toward the failing platform's implementation before changing shared code, while retaining cross-platform regression verification. Instructions may be prepared for an untested operating system only when they are clearly labeled untested; never report that platform as verified.
|
||
|
||
When asked to prepare Unix `vmake` configuration on Windows without executing it, review the authored configuration and its source paths only. Do not fabricate `makefile` or `vmake.txt` outputs. Likewise, distinguish pseudoconsole input/output verification from physical desktop input and displayed terminal appearance.
|
||
|
||
Portability applies to test synchronization APIs too. When a timed thread wait exists only on one platform, use a cross-platform completion primitive such as `EventObject` for the bounded deadlock guard, signal it on every expected completion path, and still join the worker afterward. Do not weaken a bounded test into an unbounded join merely to make it compile elsewhere.
|
||
|
||
Browser automation can change the environment being tested. Verify effective security preferences and worker-local settings instead of assuming automation defaults or page-context overrides match a normal browser. For example, restore Firefox's normal strict file-origin policy when reproducing local-module failures, and probe the worker's actual timezone before claiming timezone coverage. Serve Wasm module packages over HTTP for their supported run and distinguish that result from opening the HTML through `file://`.
|
||
|
||
## Use reentrant POSIX date-time conversions
|
||
|
||
On POSIX platforms, never retain pointers returned by `localtime()` or `gmtime()` across calls or threads because both may share process-wide `tm` storage. Use caller-owned stack values populated by `localtime_r()` and `gmtime_r()` instead, with separate values for simultaneous local and UTC conversions.
|
||
|
||
## Keep generated makefiles platform-invariant
|
||
|
||
Tracked `makefile` and `vmake.txt` output should remain identical across Linux and macOS. Keep every platform-guarded translation unit in the stable source list; the inactive translation unit should compile harmlessly through its preprocessor guards. Put monorepo-wide platform policy in the canonical shared `makefile-cpp` and select it at make execution time—for example, Darwin compile/framework options and Linux `-luring`—with libraries placed after object prerequisites.
|
||
|
||
Do not embed host-preprocessed `clang++ -MM` dependency output in tracked makefiles. Continue running the dependency probe as fail-fast validation, discard its host-specific output, and emit stable source-only object rules. Generate ignored `Obj/*.d` files during actual compilation with `-MMD -MP` and include them for correct host-local incremental dependency tracking. Do not make object files depend on the shared `makefile-cpp`: its environment-dependent settings, such as `VCPROOT`, can vary without requiring a rebuild, and `build.sh -f` is the explicit full-rebuild mechanism.
|
||
|
||
## Group non-template C++ implementations by class in `.cpp` files
|
||
|
||
Move non-template method bodies out of headers into the matching `.cpp` file while leaving template definitions in headers. In each implementation file, keep a complete class definition and all methods belonging to that class together, and separate class groups with the repository's block-comment pattern. Place namespace-level forward declarations, variables, helper structs and other non-class items before the class groups. Apply the same organization consistently to platform-specific implementation files.
|
||
|
||
## `IRpcJsonMessageDispatcher::InjectException` is persistent and last-write-wins
|
||
|
||
Treat an injected RPC dispatcher exception as durable terminal state, separate from ordinary response messages. Serialize injection with response commitment, let the newest injected message replace the previous one, wake response and startup waits, and throw `RpcInjectedException` on the original caller thread at every dispatcher-controlled checkpoint. Do not consume or clear the failure after one request.
|
||
|
||
## Measure browser images from their own load/error event
|
||
|
||
When metadata depends on an `HTMLImageElement`, create a fresh element for each measurement, install `load` and `error` handlers before assigning `src`, and read `naturalWidth` / `naturalHeight` only from that element's matching `load` event. Do not treat an early `decode()` rejection or stale dimensions from a shared measuring element as proof that the image is invalid; reserve fallback metadata for the actual `error` event.
|
||
|
||
## Keep generator compilation independent from runtime-backed tests
|
||
|
||
An import-phase code generator must be compilable before packages produced by later build phases exist. Keep the import phase limited to generator source compilation and source linting; move tests that import a generated runtime into the later test phase, and declare that runtime as a test-only development dependency.
|
||
|
||
Preserve an acyclic documented phase order such as import, code generation, runtime build, and tests. A fresh workspace must not need a later runtime merely to compile the generator that helps produce it.
|
||
|
||
## Normalize checkout line endings at golden comparison boundaries
|
||
|
||
When generated output has a canonical line ending but Git may check a golden file out with platform-dependent CRLF, normalize only the golden's checkout representation before byte comparison. Keep the generator output and tracked golden semantics unchanged, and continue verifying that all supported input line-ending variants produce the same canonical bytes.
|
||
|
||
## Preserve ordered HTTP messages at upload and completion boundaries
|
||
|
||
Concurrent HTTP requests can reorder both client-to-server request bodies and server-to-client messages piggybacked in response bodies. Preserve send order by advancing queued submissions when the preceding request upload completes, allowing response round trips to overlap instead of serializing an entire connection.
|
||
|
||
Assign a stable sequence to each submitted request and buffer completed responses until every predecessor reaches final success or terminal failure. Retries retain their original sequence. Verify both directions with a burst that requires exact FIFO receipt before immediate shutdown; limiting connection count can hide the ordering race while starving protocols that need overlapping requests.
|
||
|
||
## Preserve an existing `Ptr` counter across asynchronous ownership handoffs
|
||
|
||
When a callback or adapter must retain an object beyond the caller's synchronized scope, carry the producer's existing owning `Ptr<T>` through every layer. Never construct another `Ptr<T>` from a raw callback argument or from `.Obj()`; with the default `ReferenceCounterOperator` in `Vlpp/Source/Primitives/Pointer.h`, that creates an independent reference counter and can double-delete the same object. Use `.Obj()` only for a temporary operation that explicitly requires a raw pointer.
|
||
|
||
## Reuse established MSBuild project configurations
|
||
|
||
When adding a solution or project, start from a comparable existing `.vcxproj` configuration and adapt its dependencies and source inventory. Reuse established architecture, runtime, compiler and linker settings instead of reconstructing the build configuration from scratch.
|
||
|
||
## Use explicit positive compiler and platform guards
|
||
|
||
Compiler/platform selection must enumerate supported branches with `#if` and `#elif`; do not assume that everything outside MSVC is native GCC/Clang. Select Emscripten explicitly as `VCZH_WASM`, independently of `VCZH_MSVC` and `VCZH_GCC`, while `VCZH_APPLE` refines the native GCC branch. Correct encountered platform-selection `#ifdef` / `#else` shortcuts, including unrelated ones, without changing ordinary header guards or unrelated feature switches. Unsupported compilers should fail instead of silently entering a fallback.
|
||
|
||
## Keep encoding and exception adapters at the C++/JavaScript boundary
|
||
|
||
Preserve Emscripten's SDK-compatible 32-bit `wchar_t` and use `WString` throughout C++; do not force `-fshort-wchar`. Convert to temporary `U16String` only at the JavaScript boundary, and convert received strings back immediately. Use `U8String` only when UTF-16 cannot be bound easily and document that limitation. Honor explicit code-unit lengths and copy temporary buffers before the C++ call returns.
|
||
|
||
Prefer `EMSCRIPTEN_BINDINGS` for C++ exports and small `EM_JS` calls to named JavaScript helpers for imports. Keep application logic in normal C++ or JavaScript/TypeScript functions; direct JavaScript built-ins are appropriate for general operations. Separate the normal C++ function from its exception-catching `wasm_` export, and translate failures to return values in both directions so exceptions do not cross the boundary.
|
||
|
||
## Revalidate mutable external state immediately before mutation
|
||
|
||
A UI selection or cached snapshot is not authoritative for external state that can change independently. Re-read the actual state at the operation boundary and reject a mismatch before any mutation. Report the selected and actual state, including special states such as detached HEAD, rather than silently ignoring the command or claiming a merge conflict. Keep a matching-state positive control and verify rejected operations leave all relevant external state unchanged.
|