diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0278497..8a3cf1c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,6 +18,10 @@ Run `task fixtures-check`, `task test`, and `task lint` before opening a pull re - Keep the core gateway free of `client-go`; Kubernetes integration belongs in `gateway/kube`. - Keep the browser client framework-free and free of runtime dependencies. - Keep credentials, application identity, authorization policy, and writes in the host application. +- Keep one current name for each API. Before 1.0, remove superseded names and forwarding packages; + update callers and document the replacement instead of adding compatibility shims. +- Add public surface only for a demonstrated use case within the library’s scope. Prefer composing + existing primitives, and test the observable guarantee each addition promises. - Treat `spec/v1.md` and `conformance/` as the shared contract between gateway and client. ## Changing behavior diff --git a/README.md b/README.md index 3aa879a..5dcc6c6 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,6 @@ patch through its own save endpoint, which is the one place a write can happen. | `github.com/ConfigButler/krm-stream/gateway` | Dependency-free Go stream gateway and SSE handler. | | `github.com/ConfigButler/krm-stream/gateway/kube` | Optional `client-go` backend and SubjectAccessReview authorizer. | | `@configbutler/krm-stream` | Official dependency-free ESM client store and transports. | -| `krm-stream@0.1.0` | Deprecated, frozen compatibility name claim. Use the scoped package instead. | | [`spec/v1.md`](spec/v1.md) | Normative protocol contract. | | [`conformance/`](conformance/) | Shared fixtures exercised by the Go gateway and TypeScript client. | diff --git a/docs/auth.md b/docs/auth.md index da2bd00..96c315a 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -161,7 +161,7 @@ It needs your server's service account to hold `create` on `subjectaccessreviews `system:auth-delegator` role). It does **not** need impersonate rights: it asks a question *about* a user, it does not act *as* one. And because the gateway re-authorizes every snapshot cycle, this is also how a revocation reaches a stream that is already open. Timed checks bound quiet-stream revocation. -The old `SSARAuthorizer` name remains a deprecated alias; it never created SelfSubjectAccessReview. +Use `SubjectAccessReviewAuthorizer`; it creates SubjectAccessReview requests, not SelfSubjectAccessReview. ## What this library never does diff --git a/docs/proposals/0005-kubernetes-stream-and-save-semantics.md b/docs/proposals/0005-kubernetes-stream-and-save-semantics.md index 1b393bc..fff0565 100644 --- a/docs/proposals/0005-kubernetes-stream-and-save-semantics.md +++ b/docs/proposals/0005-kubernetes-stream-and-save-semantics.md @@ -1,108 +1,17 @@ -# Proposal 0005: Kubernetes stream semantics and conditional editing +# Proposal 0005: Stream and conditional-save tradeoffs -**Status: design discussion; no protocol or runtime change approved by this document.** +**Status: design rationale. Normative convergence clarification remains pending.** -The follow-up [implementation plan](0006-stream-and-save-implementation-plan.md) incorporates the -latest review and supersedes the phase list below for sequencing and merge gates. +[Proposal 0006](0006-stream-and-save-implementation-plan.md) owns the remaining work and acceptance +criteria. Current adoption behavior belongs in the [saving guide](../saving.md). Managed recovery, +bounded reauthorization and the conditional editor shipped in 0.3.0; their implementation phases +are superseded by the current work plan. -This follows the second review of PR #25. It distinguishes Kubernetes behavior, the contract in -[spec/v1.md](../../spec/v1.md), the implementation, and proposed changes. The immediate recommendation -is to clarify the contract and save guidance, retain Kubernetes concurrency checks, and add focused -integration tests. Resume support, new wire events, and a general write abstraction should not be -bundled into that correction. +Kubernetes owns identity and conditional writes. The library supplies projected reads and one draft +store; the host owns credentials, write policy and presentation. The sections below explain the +tradeoffs that still constrain the remaining work. -The original priorities remain managed recovery, bounded authorization rechecks for shared streams, -and a complete conditional-save integration. Existing watch sharing and draft reconciliation remain -in place. Credentials, writes, application policy, and field ownership stay with the host. - -## 1. The boundary we should preserve - -Kubernetes owns object identity, resource versions, persistence, authorization decisions and write -conflict enforcement. The gateway provides a projected read stream for a UI; the store maintains a -local draft. These are related responsibilities, not interchangeable authorities. - -```mermaid -flowchart LR - K["Kubernetes API server
Objects, versions, RBAC, writes"] - G["Gateway
Watch translation, sharing, projection"] - S["Client store
Last delivered view and local draft"] - U["Editor
User intent and conflict presentation"] - H["Host save endpoint
Identity, validation, write policy"] - K -->|"Kubernetes watch"| G - G -->|"Projected SSE snapshots and changes"| S - S --> U - U -->|"Captured save intent"| H - H -->|"Conditional PATCH or deliberate apply policy"| K - K -->|"Write result"| H -``` - -**Recommended boundary:** keep the projected read stream and draft helpers small. Do not make the -SSE sequence, a projection digest, or a redaction revision into a substitute for Kubernetes write -concurrency. An optional managed connection is lifecycle convenience, not a new consistency model. - -Kubernetes documents version-based watch continuation, recovery when history expires, and conditional -writes. A default GET requests most-recent semantics; supported API servers may serve consistent -reads from their watch cache. “Most recent” should not be documented as a guarantee of a physical -etcd quorum read for every request. [Kubernetes API concepts](https://kubernetes.io/docs/reference/using-api/api-concepts/) - -## 2. What the feedback gets right, and where I disagree - -| Feedback | Assessment | Planned response | -|---|---|---| -| Healthy connections now replenish retries and reset backoff | Agree; preserve this behavior | Simplify its tests without expanding the public API | -| Cycle teardown now recovers while genuine denial remains terminal | Agree | Add one subscriber-visible test through `Gateway.Stream` | -| Suppression plus conditional saves can repeatedly produce 409s | Agree; this is a usability and progress problem | Explain it by projection and distinguish write conflicts from draft conflicts | -| resourceVersion preconditions are wrong for `krm-spec/v1` | Disagree as a safety claim | They remain safe, but can be expensive or make progress difficult under churn | -| The reversal is correct only for `krm-full/v1` | Too absolute | Full includes status but still suppresses changes to ignored metadata; any delivered version can also become stale in transit | -| Only a snapshot can refresh the held version | Incorrect for the current store | An accepted `captureReconciliation` GET updates the server object, including its version; a delivered upsert also updates it | -| Name Server-Side Apply as an alternative | Agree, with substantial qualification | Describe ownership semantics and its separate validation requirements | -| SSA is the server analogue of our three-way draft conflicts | Incomplete | Same-manager concurrent writers do not gain stale-read detection merely by using SSA | -| Unknown redaction paths require a fresh connection | Too strong | A later delivered upsert for that UID can supply authoritative revisions; a fresh snapshot is an explicit recovery option | -| Every reconnect sends a full snapshot | Agree, but distinguish two connections | Document upstream and downstream recovery costs separately | -| UID immutability may yield 422 rather than 409 | Plausible, path-dependent | Reproduce the actual endpoint race; never translate every 422 into a conflict | -| More test coverage is useful | Agree | Prioritize projection/save interaction and public stream recovery over broad new infrastructure | - -The suppression mechanism predates PR #25: it is specified in -[proposal 0004](0004-views-and-bytes.md) and implemented by -[`visibleDigest`](../../gateway/stream.go). What this PR adds is the conditional-save composition and -stronger guidance, exposing the tension more directly. - -### Follow-up review: scope and qualifications - -The reviewer now agrees that accepted GETs advance the base, all projections can suppress RV -changes, and SSA ownership is not per-user stale-read protection. Split the normative convergence -amendment from PR #25, but keep focused save/recovery regression tests with the example correction. -The [implementation plan](0006-stream-and-save-implementation-plan.md) defines that merge gate. - -Routine upstream closure fans out resnapshot work to all shared subscribers, making upstream -continuation a named follow-up. Calling it the dominant source of resets still requires measurement. -The five-second timeout in auth.md is an explicit override; its prose already states the ten-second -default. Label the example rather than change the default. - -## 3. Divergence map - -Some differences are intentional protocol choices. Others are ambiguities in our own prose. They -should not all be described as departures from Kubernetes correctness. - -| Area | Kubernetes behavior or authority | Current krm-stream behavior | Classification and consequence | -|---|---|---|---| -| Initial state | Streaming initial events or list followed by watch at the list version | Backend uses those mechanisms; gateway emits `reset`, `added`, `synced` | Translation; preserves the snapshot/watch boundary | -| Live changes | Versioned resource events | Complete projected objects; intermediate updates may be coalesced | Intentional view service, not an audit log | -| Suppression | Resource version identifies an object revision | Digest excludes resourceVersion for **all** projections | Intentional optimization; held version is from the last delivered object | -| Status omission | Status writes can advance object version | `krm-spec/v1` omits status and suppresses status-only changes | Intentional projection; systematically stale save bases under controller churn | -| Upstream reconnect | A client can resume from an observed watch checkpoint while history is retained | An ended backend watch returns `ErrWatchClosed`; gateway resnapshots | Conservative implementation choice; potentially avoidable work | -| Browser reconnect | Kubernetes does not define our SSE protocol | Every new SSE connection snapshots; no `Last-Event-ID` | Explicit v1 contract; warm cache reduces upstream cost, not browser bytes | -| Gap detection | Kubernetes watch version semantics | A per-connection `seq` checks emitted-frame continuity | Additional transport check; not a resource version or resume token | -| Redaction revisions | Kubernetes has no equivalent UI counter | Per-connection counters describe observed hidden-value changes | Deliberate disclosure feature; stateless GET cannot invent them | -| Shared authorization | Upstream watch operates as its authenticated identity | Shared watch uses host identity; subscribers get separate SAR checks | Necessary host/gateway boundary when sharing; not transparent impersonation | -| Draft reconciliation | API server enforces write semantics | Store merges user edits against delivered server views | UI assistance; cannot authorize or guarantee a write | -| Retry exhaustion | Client retry policy is not a Kubernetes wire guarantee | Managed connector bounds failures and replenishes after sustained health | Optional operational policy; preserve low-level transport access | - -Code anchors: [Kubernetes backend](../../gateway/kube/backend.go), -[stream loop](../../gateway/stream.go), [shared backend](../../gateway/shared.go), -[authorization gate](../../gateway/reauthorize.go), [managed connector](../../packages/krm-stream/src/connection.ts). - -## 4. The main tension: quiet views versus current write versions +## Quiet views and write versions Consider a Deployment editor using `krm-spec/v1`. The numbers below are illustrative labels. The browser echoes resourceVersion strings; it must not parse them or infer ordering from them. @@ -144,91 +53,10 @@ There are three different facts a UI must not collapse into one “conflict” l delivered view at an editable path. - **An ownership conflict exists:** an apply operation disputes another field manager's ownership. -The current example returns `"conflict"` for a 409 even when reconciliation leaves no draft conflicts. -That is a misleading result for a human editor. It should describe refresh/review state separately -from an actual field conflict, without promising automatic save success. - -### Projection-specific expectations - -| Projection | Updates intentionally invisible | Conditional merge-patch implications | -|---|---|---| -| `krm-full/v1` | Removed bookkeeping metadata; Secret values are withheld but their revisions are reported | Good general editor default; no guarantee that the delivered RV remains current | -| `krm-spec/v1` | The above, plus status changes | Safe optimistic concurrency, but repeated refresh/409 cycles may dominate controller-heavy resources | -| `krm-raw/v1` | Still removes managedFields and last-applied configuration | Not a raw Kubernetes watch and not a concurrency solution; it can disclose Secret values | - -No projection makes a streamed resourceVersion into a lease. Calling a stale precondition “unsafe” -would reverse the safety argument: the rejection is exactly what prevents an old array replacement -or field value from silently overwriting a newer write. - -### Should status updates be the client's choice? - -Yes, within the host's authorization policy. We should not make a new library-wide decision that -every client must receive status, or that every client must ignore it. A dashboard may need status -continuously; a configuration viewer may prefer silence when only status changes. An editor may want -to omit status content while still learning about version changes that affect conditional saves. - -The current API already accepts a requested projection. The host's `ProjectionPolicy` selects the -authorized projection for the principal and scope; a host can also pin a static projection. Thus -client choice means requesting a supported view, not overriding host policy. The stream's `reset` -declares the effective projection. Keep `krm-full/v1` as the general default. - -There are two distinct questions, currently bundled into named projection contracts: - -1. **Content:** which fields should the client receive? -2. **Notifications:** which changes should cause the gateway to deliver an update? - -“I do not need to display status” does not necessarily mean “I do not want to hear that the object -version changed.” Conversely, sending version notifications to every spec-only viewer would remove -a useful reason to choose that projection. - -| Client need | Content and delivery behavior | Availability | -|---|---|---| -| Observe configuration and status | Include status and emit visible changes; bookkeeping-only RV changes can still be suppressed | Current `krm-full/v1` | -| Observe configuration with less notification traffic | Omit status and suppress status-only changes | Current `krm-spec/v1` | -| Edit configuration while learning about status-driven version changes | Omit status but deliver updates when RV changes, subject to ordinary stream coalescing | Possible future opt-in contract, not implemented | - -```mermaid -flowchart TD - C["Client requests a supported projection"] --> H["Host policy selects an authorized projection"] - H --> E["Reset declares the effective projection"] - E --> P["Projection contract bundles two decisions today"] - P --> F["Content: include or omit status"] - P --> N["Delivery: which changes trigger updates?"] - F --> Q["Future question: omit status but report version changes?"] - N --> Q - Q --> M["Measure benefit and cost before adding an explicit opt-in contract"] -``` - -**Proposed decision:** preserve client requests and host control, document the existing coupling, -and do not add independent configuration switches yet. If the third use case proves valuable, -compare one additional named contract with a separate delivery option. Either design must make the -effective behavior explicit and remain simple for clients and hosts to implement. It must not -silently change what existing `krm-spec/v1` subscribers receive. +The [saving guide](../saving.md#what-the-person-editing-sees) maps these distinctions to the +shipped editor outcomes. A refreshed base enables review; it cannot promise the next save succeeds. -Omitting status saves payload bytes; suppressing status-only updates also saves notifications and -browser work. A version-only message could save bytes compared with a complete projected object, -but still causes notifications and requires additional protocol handling. More frequent version -delivery can reduce avoidable stale saves, but cannot prevent a write racing the next save: 409 -handling remains necessary. This is a choice about stream delivery, not a different write guarantee. - -### The constraint we cannot abstract away - -```mermaid -flowchart TD - A["Status-only write advances Kubernetes resourceVersion"] - A --> B["Send nothing to the browser"] - B --> C["Browser cannot learn the new version from this stream"] - C --> D["Old-version conditional save may return 409"] - A --> E["Send a complete object or version-bearing message"] - E --> F["Browser learns a later version"] - F --> G["Traffic and wakeups return, a later write can still race"] -``` - -We cannot promise zero notifications for ignored writes, always-current browser write versions, and -unconditional progress for object-version CAS writes at the same time. That is an information and -concurrency constraint, not a missing merge algorithm. - -## 5. Our own spec needs a precise statement of equality +## Convergence needs precise equality [Spec §3](../../spec/v1.md#3-projection-and-redaction--part-of-the-wire-not-an-implementation-detail) explicitly permits status suppression. Proposal 0004 explicitly excludes resourceVersion from the @@ -258,16 +86,16 @@ invariant. Call it out in release notes. If the project instead wants exact proj including version metadata, it needs a different emission policy; that cannot be presented as a wording-only fix. -## 6. Write strategies: choices the host must make +## Host write strategies -The following is a design comparison, not a proposal to implement four save engines. +The following is a design comparison, not a proposal to implement more save engines. | Strategy | Protects | Tradeoff | Recommendation | |---|---|---|---| | Merge patch with captured UID and resourceVersion | Identity and object-version concurrency | Unrelated writes can reject it; arrays are replaced as units | Keep the supported baseline and demonstrate its limitations | -| Refresh, reconcile, then submit a newly reviewed conditional intent | Retains object CAS while allowing a newer base | Still races; review flow must distinguish actual field disagreements | Improve the example first; no blind version substitution | +| Refresh, reconcile, then submit a newly reviewed conditional intent | Retains object CAS while allowing a newer base | Still races; review flow must distinguish actual field disagreements | Supported review flow; no blind version substitution | | JSON Patch with deliberate `test` operations | Can guard specific values atomically | Paths, missing values, list indices and whole-array dependencies require care | Document as host-owned advanced work, not a new core patch compiler | -| Server-Side Apply with deliberate field ownership | Declarative ownership conflicts | Different semantics from stale-read detection; needs an apply-specific validator and intent model | Name it and scope a separate example, not a drop-in replacement | +| Server-Side Apply with deliberate field ownership | Declarative ownership conflicts | Different semantics from stale-read detection; needs an apply-specific validator and intent model | Separate host design only for a concrete ownership model | | Merge patch with no concurrency condition | Only the patch's write scope | Deliberate last-writer-wins behavior can lose edits | Only when explicitly chosen by the host; do not silently select it by projection | Merge-patch array replacement and JSON Patch value tests are defined by @@ -318,7 +146,7 @@ A future SSA example must establish manager lifetime, permitted fields, deletion create-on-apply behavior, list topology, and conflict handling. Default to no forced ownership transfer. Preserve the existing projection boundary on both requests and responses. -## 7. Recovery has two layers, with different costs +## Recovery layers and costs ```mermaid flowchart TB @@ -347,7 +175,7 @@ cannot establish continuity. The upstream [client-go reflector](https://github.c is the reference to study before implementing more watch machinery. It must track the upstream checkpoint, including appropriate bookmarks, rather than infer one from a browser's last visible resource. -**Downstream:** §7 deliberately prohibits SSE resume in v1. SharedBackend can serve a warm snapshot +**Downstream:** spec §7 deliberately prohibits SSE resume in v1. SharedBackend can serve a warm snapshot without opening one upstream watch per subscriber, but serialization, network bytes and browser reconciliation still occur for each subscriber. A downstream replay protocol would need its own cursor, scope/identity/projection binding, retained history, expiry, authorization rechecks and @@ -360,7 +188,7 @@ for all subscribers, even with healthy browser connections: `sharedScope.die` en with a recoverable resync. Measure reset counts by cause before calling either source dominant. Upstream continuation is a named follow-up, ahead of any downstream replay design. -## 8. Version delivery options and the abstraction cost +## Version delivery options | Option | Wire impact | Benefit | Cost / conflict with current contract | |---|---|---|---| @@ -381,118 +209,15 @@ rather than imply that it is the universally best editing mode. The root adoptio `krm-full/v1` as the default, with client requests subject to host policy. Any future version-delivery mode should be an explicit choice, not a global switch that restores status notifications for everyone. -## 9. UID recreation and error classification +## UID recreation and error classification The endpoint checks UID before patching and includes the captured UID in the patch. Deletion and recreation between those operations must never make the old draft modify the new object. Kubernetes validates UID immutability, but validation/storage ordering can affect the returned error. [Metadata update validation](https://github.com/kubernetes/apimachinery/blob/master/pkg/api/validation/objectmeta.go) -The review's precise 422 claim is not established by our existing 409 test. Add a real API-server test +The UID-race response is not established by the existing stale-RV 409 test. Add a real API-server test that forces this race, records the structured Kubernetes Status and verifies the replacement is unchanged. If the host normalizes an identity-mismatch error to a conflict response, classify that specific case. Keep schema/admission validation failures as validation errors; do not map every 422 to 409. Preserve useful structured error information without exposing a raw protected object. - -## 10. Implementation plan and acceptance criteria - -The phases below explain the design work. The [implementation plan](0006-stream-and-save-implementation-plan.md) -separates the normative amendment, defines required tests, and promotes upstream continuation to its -own follow-up. Use that document for implementation order. - -### Phase A — clarify the existing contract before adding mechanisms - -1. Update saving.md with projection-specific costs and the distinction between safety and progress. - Explain client-requested versus host-selected projections, and distinguish content selection from - notification behavior. State that these are bundled today, not independently configurable. -2. Amend spec §6 and proposal 0004 together to define the RV exception precisely, or explicitly choose - to change emission behavior instead. Clarify §3's SSA wording and ownership/omission semantics. -3. Explain GET/upsert recovery for redaction metadata. A new stream is an option, not a requirement - after every rejected read; a quiet stream may still need explicit recovery. -4. Name SSA and JSON Patch as host-owned alternatives. State which helpers do not support them. -5. Keep public naming and runtime behavior unchanged in this phase; cross-link this decision. - -**Done when:** README, spec, proposal and save example make compatible claims about object versions, -projection completeness, redactions and writes. A reader can identify who chooses the projection, -whether status-only writes produce notifications, and cannot mistake “live” for “my save will pass.” - -### Phase B — test the composition at the public boundary - -1. Simplify healthy-retry tests using the existing test runner's fake timers: one budget-reset test, - one backoff-reset test. Avoid adding a public injectable-clock API just to accommodate tests. -2. Add a `Gateway.Stream` test: recheck in flight, routine upstream close, `RESYNC_REQUIRED`, fresh - `reset`/`synced`, no terminal error. Keep explicit-denial and timeout tests. -3. Add `krm-spec/v1` plus conditional-save coverage: status update suppressed, stale save returns a - real 409, projected GET updates RV without a draft conflict, new reviewed intent can succeed when - the controller is quiet. A continuing status writer must not cause an unbounded write retry loop. - Also overlap the post-409 GET with a resnapshot: pending snapshots and changed snapshot epochs - reject reconciliation. Preserve that guard and drafts, show recovery, and verify later progress. - A refused GET does not always require another read: a newer watch may already have won. -4. Add a contrast case for full projection and a bookkeeping-only change showing that full is not an - always-current-version promise either. -5. Force the UID replacement race and classify its actual response before changing error mapping. - -**Done when:** tests assert observable behavior and winner preservation, not merely helper return -values. They explain normal 409s without requiring imaginary user-visible conflicts. - -### Phase C — improve the host example without making the core a writer - -```mermaid -flowchart TD - S["Capture and submit conditional intent"] --> R{"Response"} - R -->|204| W["Saved, watch echo settles the store"] - R -->|409| G["Guarded most-recent projected read"] - R -->|"Other error"| E["Show classified host error"] - G --> A{"Read accepted?"} - A -->|No| N["Newer stream state or metadata recovery takes precedence"] - A -->|Yes| C{"Draft conflicts?"} - C -->|Yes| V["Review specific field conflicts"] - C -->|No| F["Base refreshed, explain changed version and offer a new save"] - F --> S - N --> Q{"Same UID and authoritative metadata ready?"} - Q -->|Yes| F - Q -->|No| P["Wait for recovery or open replacement as a new editor"] -``` - -The arrow back to Save is a new user action, not an automatic infinite retry. For unknown redaction -paths, wait for authoritative upsert metadata or deliberately request a fresh snapshot before retrying. -An identity replacement requires a new editor for the new UID, not another attempt with the old draft. - -Change the example's outcomes to distinguish stale version, actual draft conflicts, missing/replaced -resource and transport/validation error. This is host presentation, not a new wire event taxonomy. -Only add automatic conflict-free retry later with frozen-intent semantics, a finite budget and tests -for arrays and edits made while saving. Avoid casually broadening `captureSave` into a save controller. - -**Done when:** no user is asked to resolve nonexistent field conflicts, no replacement UID is written, -and no old patch is assigned a fresh RV without checking its intended changes. - -### Phase D — measure and decide separately - -Measure projected bytes, resnapshot counts, watch recycling, 409 rate by projection, and the fraction -of rejected writes that reconcile without field conflicts. Keep telemetry low-cardinality; avoid UID -or username labels. - -Evaluate upstream continuation before downstream replay. Design an SSA example only for a concrete -host ownership model. Consider a version-delivery mode only if measured write usability justifies -its notification cost. Compare status-aware dashboards, quiet spec viewers and spec editors before -choosing a default or adding a mode. Resolve whether a named projection or a separate delivery option -communicates the choice most simply, how the host constrains it, and how clients learn the effective -contract. Each can be a separate proposal and PR, with its own compatibility decision. - -## 11. Verification and scope of this document - -For implementation phases, run fixture checks, Go race tests, TypeScript tests, Vue tests, browser -and wire tests, lint and package validation. Run the projection/save and UID-race cases against a -real API server; fake clients do not establish Kubernetes conflict behavior. Push the implementation -changes according to the separate implementation plan and verify CI on their actual final heads. -PR #25 gates on corrected guidance, save outcomes and focused regression evidence; the normative -spec amendment and upstream continuation have separate review scopes. - -This document itself adds no runtime behavior, changes no named projection, and does not expand the -write API. The reviewer reports independent probes of retry and teardown recovery; those reports -are useful supporting feedback, not a substitute for the planned reproducible tests in this repo. -The UID-race status and any throughput estimates remain unverified here. - -The decision proposed for the current PR is deliberately small: make our guarantees precise, explain -the projection/write interaction, and strengthen the boundary tests. Do not remove Kubernetes write -preconditions or add an ownership/replay abstraction to conceal the tradeoff. diff --git a/docs/proposals/0006-stream-and-save-implementation-plan.md b/docs/proposals/0006-stream-and-save-implementation-plan.md index 7cda49a..4373cb5 100644 --- a/docs/proposals/0006-stream-and-save-implementation-plan.md +++ b/docs/proposals/0006-stream-and-save-implementation-plan.md @@ -1,208 +1,104 @@ -# Proposal 0006: Implementation plan for stream and save semantics - -**Status: proposed work breakdown. This document does not implement or approve runtime changes.** - -[Proposal 0005](0005-kubernetes-stream-and-save-semantics.md) explains the tradeoffs. This document -supersedes its phase list for sequencing, merge gates and acceptance criteria. Keep Kubernetes in -charge of identity and conditional writes, keep credentials and writes host-owned, and compose the -existing stream, shared backend and draft store. The original priorities remain managed recovery, -bounded subscriber reauthorization and a complete conditional-save example. - -## 1. Review decisions - -| Feedback | Decision | -|---|---| -| Separate the normative spec amendment from the feature PR | Agree. Review the convergence invariant in a dedicated PR, with release-note visibility. | -| Gate PR #25 on guidance and save outcomes | Agree, with focused regression tests included in the gate. Tests proving the corrected composition should not be deferred wholesale. | -| Copy the existing fake-timer pattern | Agree. `connection.test.ts` already demonstrates it; no public clock injection is needed. | -| Routine upstream closure resnapshots every shared subscriber | Confirmed in `sharedScope.pump` and `sharedScope.die`. Make continuation a named follow-up. | -| Upstream closure is the dominant source of resnapshots | Not established. It is a fan-out multiplier; dominance depends on closure frequency, browser reconnects and slow-consumer overflow. Measure before claiming it. | -| A snapshot can reject the post-409 GET | Confirmed. The guard also rejects responses spanning a snapshot epoch, even if that snapshot has finished. Preserve the guard and explain recovery. | -| A refused GET means the user must click again | Qualify. A newer watch may already have supplied the base. Re-read current state; wait when recovery is incomplete. Never infer the rejection reason from the boolean alone. | -| The five-second auth example disagrees with the default | No behavioral mismatch: the example explicitly overrides the timeout and the adjacent prose says zero uses ten seconds. Label the override more clearly. | - -The review's example of an identical SSA reapply advancing resourceVersion is not a guaranteed -fixture: a server may treat an operation as a no-op. Tests must demonstrate an actual persisted -change and a changed RV before asserting suppression. Neither SSA nor more frequent version -notifications replaces the conditional-save behavior we need to demonstrate. - -## 2. Work boundaries and merge gate - -**PR #25 is ready to merge when save outcomes and guidance are corrected, the focused recovery/save -regressions below pass, and CI passes on its final commit; normative spec changes and upstream -continuation belong to separately reviewed PRs.** - -This is the proposed gate, not a report that these checks have already run. If #25 has merged by -implementation time, put this same bounded correction in a follow-up PR. - -```mermaid -flowchart TD - A["PR 25 correction: guidance, save outcomes, regression tests"] --> G["Focused validation and final-commit CI"] - G --> M["Feature merge"] - S["Separate spec PR: convergence invariant and release note"] --> R["Explicit contract review"] - M --> U["Named follow-up: upstream watch continuation"] - U --> T["Continuity, cancellation, auth and fan-out tests"] - T --> V["Measure resnapshot and save behavior"] - V --> O["Optional later proposals only with a concrete need"] -``` - -The spec PR can be prepared alongside the correction. PR #25 should link the known invariant issue -and describe actual delivery behavior without silently rewriting the normative text. Resolve the -spec PR before the next release that presents this contract as settled. Separating review is not a -reason to leave contradictory guarantees indefinitely. - -## 3. PR #25 correction: guidance and a small host example - -### Documentation changes - -| Files | Concrete change | -|---|---| -| [Saving guide](../saving.md), [conditional-save README](../../examples/conditional-save/README.md) | Describe held RV as the last delivered revision for every projection. Explain safe rejection versus progress under churn, accepted GET base advancement, and arrays as whole-value merge-patch replacements. | -| [Store API comments](../../packages/krm-stream/src/store.ts) | Explain that an authoritative same-UID upsert can restore redaction metadata; a new snapshot is one recovery route. Keep response guards explicit. | -| [Auth guide](../auth.md) | Label five seconds as an example override of the ten-second default. Do not change the default to make an example match. | -| [Examples index](../../examples/README.md) and root README | Link one complete adoption path and distinguish optional managed recovery from low-level transport access. Avoid repeating the whole save guide. | - -Name SSA and deliberate JSON Patch as host-owned alternatives, with links to proposal 0005's -tradeoffs. Do not add another save engine. Explain that client-requested projections remain subject -to host policy and currently bundle content and notification behavior. Keep current projection -names and the full default in this correction. - -### Save outcomes - -Change [editor.ts](../../examples/conditional-save/editor.ts), with tests that exercise this exported -example directly. Keep a local result type in the example, not a new core API or wire event family. -The exact spelling may change during implementation; the following distinctions must survive: - -| Outcome | Meaning and host presentation | -|---|---| -| `unchanged` / `busy` | No patch, or a save is already in flight. | -| `saved` | The write succeeded. Keep drafts and let the watch settle the store. | -| `draft-conflict` | Current store state contains conflicting editable paths. Show those paths. | -| `version-stale` | The write returned 409 and current state is ready for a newly captured intent, without draft conflicts. Explain the changed base and offer Save again. | -| `recovering` | A usable base is not established. Preserve the draft and wait for stream/metadata recovery. | -| `unavailable` | The old UID is missing or the read identifies its replacement. Open a replacement as a separate editor. | - -Transport and validation failures should remain explicit host errors with useful status information; -they must not become `draft-conflict`. A successful PATCH response must never overwrite newer watch -state or clear edits typed while the request was in flight. - -The example already requires a live connection. Make that requirement executable through a small -host-supplied readiness callback using the existing managed connection state. Recheck it after awaits; -being live at the first click does not imply being live when the GET returns. Do not create a second -connection controller in the example. - -On 409, capture the reconciliation guard before GET, then inspect its result and current store state. -A rejected GET can mean newer data won, the resource disappeared, a snapshot intervened, or redaction -metadata is insufficient. The boolean is not an error taxonomy. If readiness cannot be established -from existing state, conservatively return `recovering`; do not add a public diagnostic API merely -to label every rejection. The example conservatively requires a later guarded GET accepted while -live before permitting a new write. A recovery click performs only that read; it does not clear the wait by itself. This -avoids subscriptions and diagnostic APIs at the cost of an extra read when a watch already won. - -```mermaid -flowchart TD - P["Submit captured UID, RV and patch"] --> H{"HTTP result"} - H -->|Success| S["Saved, preserve edits awaiting watch"] - H -->|409| G["Guarded projected GET"] - H -->|Other failure| E["Host error presentation"] - G --> I{"Old UID still available?"} - I -->|No| U["Unavailable, do not write replacement"] - I -->|Yes| R{"Current base ready after reconciliation?"} - R -->|No or uncertain| W["Recovering, preserve draft"] - R -->|Yes| C{"Current draft conflicts?"} - C -->|Yes| D["Review conflicting paths"] - C -->|No| V["Explain stale version, offer a new Save"] - V --> N["User captures a fresh intent"] - N --> P -``` - -No automatic write retry. No replacement of an old patch's RV with the GET's RV. No GET admitted as -snapshot membership. These constraints keep the example useful without turning it into a writer -framework. - -## 4. Regression coverage and follow-ups - -The final review narrows the immediate gate to adopter guidance, example outcomes with direct tests, -and the `Gateway.Stream` teardown/recovery regression. Connection-test simplification, real-API -status/save composition, and UID-race classification are follow-up hardening, not merge blockers. -The table below retains that broader backlog; it does not assert that every row shipped in PR #25. - -| Test location | Scenario and assertion | -|---|---| -| [Connection tests](../../packages/krm-stream/test/connection.test.ts) | Reuse `t.mock.timers` and the existing asynchronous flush pattern. Verify sustained health resets both budget and backoff, brief live periods still exhaust, and close/reset cancels the health timer. Preserve coverage while simplifying setup. | -| [Reauthorization tests](../../gateway/reauthorize_test.go) | Drive `Gateway.Stream`: recheck pending, upstream closes, subscriber sees recoverable error followed by reset/synced. Use synchronization barriers, not sleeps. Explicit denial and timeout remain terminal for only the affected subscriber. | -| Example tests, wired into the existing TypeScript test task | Exercise the actual `conditionalEditor` through a fake host request and real store: 409 with no field conflict, real draft conflict, newer watch/GET winning, edits during save, missing/replaced UID and request failure. No new test framework. | -| [Saving tests](../../packages/krm-stream/test/saving.test.ts) plus example tests | GET arriving during a snapshot and GET spanning a completed snapshot are refused. Drafts survive and snapshot pruning remains correct. A later authoritative update can restore readiness; no blind retry or endless click loop. | -| [Gateway stream tests](../../gateway/stream_test.go) | Spec suppresses a status-only RV change; full delivers that visible change. Both suppress an RV change whose only other changes are stripped bookkeeping metadata. | -| [Real API tests](../../gateway/kube/e2e_test.go) and [host handler tests](../../gateway/kube/examples/conditionalsave/handler_test.go) | Prove actual stale-RV 409 and preservation of the winning object. Combine wire/store tests with this API evidence rather than treating a fake 409 as proof of Kubernetes behavior. | - -For the status/save composition, use a resource with a real status subresource and an existing host -validation pattern. Do not pretend a ConfigMap's arbitrary `status` field establishes that behavior, -and do not generalize the ConfigMap example into an unrestricted write endpoint just for testing. -Require: status update advances RV, spec update is suppressed, stale PATCH returns 409, guarded GET -advances the base without a draft conflict, and a fresh intent succeeds once churn stops. - -Add one schedule combining churn and resnapshot: save starts while live, controller advances RV, -upstream closure starts a snapshot, and the post-409 GET arrives before synced. Assert recovery -presentation and later progress rather than asking the user to resolve an empty conflict list. - -Force deletion/recreation between the host's preflight read and PATCH using a test-only client -wrapper/barrier. Record the structured API Status and verify the replacement UID's contents remain -unchanged. Keep exact 409/422 classification evidence-based. Only normalize a specifically proven -identity-mismatch case; ordinary validation errors retain their meaning. This race test and any -resulting classification belong in follow-up hardening; do not change normalization before the -evidence exists. - -**Acceptance:** the original winner and drafts are preserved, outcomes describe current state, and -normal version rejection never requires imaginary field conflicts. The added tests run from existing -tasks and CI rather than relying on an unrecorded manual probe. - -## 5. Separate PR: define convergence precisely - -Change [spec/v1.md](../../spec/v1.md), [proposal 0004](0004-views-and-bytes.md), affected conformance -wording/tests and release notes together. Adopt proposal 0005's recommended convergence over projected -content excluding `metadata.resourceVersion`, plus redaction records, unless explicit review chooses -a different emission contract. - -State that delivered RV belongs to the delivered revision, suppressed updates do not refresh it, -and it is neither a freshness guarantee nor a downstream cursor. Preserve snapshot completeness, -pruning, ordering and per-connection redaction semantics. Distinguish stream quiescence from immediate -equality while updates are still in flight. - -Add a contract case where the final write changes only ignored metadata: no event is emitted, visible -content converges, and held RV remains older. Add the spec-only status counterpart. Existing event -fixtures need no changes unless the reviewed decision actually changes emissions. - -**Acceptance:** invariant, suppression rules, conformance assertions and save guidance agree. The -release note explicitly identifies the narrowed invariant. This is a normative correction with -unchanged intended runtime behavior, not a feature hidden in editorial cleanup. - -## 6. Named follow-up: continue upstream watches without resnapshotting subscribers +# Proposal 0006: Remaining stream and save work + +**Status: active follow-up plan, reviewed 2026-09-11 against 0.3.0 and adopter feedback.** + +Follow the standing [design rules](../../CONTRIBUTING.md#design-rules) and +[release policy](../releasing.md). [Proposal 0005](0005-kubernetes-stream-and-save-semantics.md) +explains the unresolved tradeoffs; this document owns work order and acceptance criteria. + +## Baseline and order + +Managed recovery, bounded reauthorization, differentiated save outcomes and the copyable Vue adapter +shipped in [0.3.0](../../packages/krm-stream/CHANGELOG.md). Use the +[adoption guide](../adopting.md), [saving guide](../saving.md) and +[conditional editor](../../examples/conditional-save/README.md) for current behavior. + +| Priority | Remaining work | Completion evidence | +|---|---|---| +| 1 | Define convergence precisely | Spec, suppression rules, conformance and release notes agree. | +| 2 | Publish tested deletion-recovery and keep-local recipes | Executed examples preserve unsaved work and unrelated conflicts. | +| 3 | Harden real-API save composition and identity races | Exact-commit API evidence, separate from fake-client CI. | +| 4 | Measure and implement upstream continuation | Same-workload comparison proves continuity, bounded recovery and authorization. | + +Review these as separate changes. Baseline measurement can run alongside priorities 1–3. Complete +the normative amendment before the next release presenting convergence as settled. Adopter-reported +unit tests support adoption, but do not establish real-cluster composition, consumer readiness or +200-attendee capacity. + +## 1. Define convergence precisely + +Change [spec/v1.md](../../spec/v1.md), [proposal 0004](0004-views-and-bytes.md), conformance and release +notes together. Define convergence over projected content excluding `metadata.resourceVersion`, +plus redaction records, unless explicit contract review chooses a different emission policy. + +Delivered RVs belong to delivered revisions, remain opaque in the browser and are valid conditional +write preconditions. Suppression does not refresh them; they promise neither freshness nor downstream +resume. Preserve snapshot completeness, pruning, ordering and per-connection redaction semantics. +Distinguish convergence after quiescence from equality while updates are in flight. Clarify spec §3 +that SSA needs a host-owned managed-field and omission policy; merge-patch helpers do not supply it. + +**Acceptance:** add final-write cases for ignored bookkeeping metadata and spec-only status churn: +no event, converged visible content, older held RV. Invariant, suppression and save guidance agree. +Release notes identify the narrowed invariant; unchanged emissions do not make this merely editorial. + +## 2. Tested adoption recipes + +Keep the existing [user-facing outcomes](../saving.md#what-the-person-editing-sees) and host-owned +[receipt contract](../saving.md#answer-204-or-a-receipt-and-let-the-watch-echo-it) in the saving guide. +The remaining work is executable guidance, using existing store and Vue example tests. + +### Deletion recovery copy + +Demonstrate a host subscription that snapshots detached `store.draft(uid)` on each notification while +that fixed UID exists, including the initial state. When removal or snapshot pruning makes it absent, +retain the last copy for explicit copy-out; do not try to read the removed draft. Capturing only on +Save misses later typing. Specify subscription cleanup, identity-scoped retention and expiry. + +**Acceptance:** edits immediately before deletion and snapshot pruning remain recoverable; a +replacement UID opens separately and never inherits the old draft. Test initial capture, edit-time +updates, null-state handling and disposal in the actual recipe. Keep one active draft store; the +recovery copy is not another reconciler. Link the tested recipe from the saving and Vue guides. + +### Explicit keep-local resolution + +Demonstrate capturing the chosen local value (including absence), resolving that path with +`takeTheirs`, then synchronously reapplying it through `setValue` or `removeKey`. Verify the recipe +against current behavior before publishing executable guidance. + +**Acceptance:** cover nested paths, deletion, whole-array replacement and policy/redaction refusal; +preserve unrelated edits and conflicts. Capture a fresh save intent after review. Add a small helper +only if repeated consumer code warrants it; do not create another conflict registry. + +## 3. Real-API composition and identity races + +The [existing real-API stale-RV test](../../gateway/kube/e2e_test.go) proves ordinary rejection, not +these compositions. Extend it alongside [store/example tests](../../packages/krm-stream/test/saving.test.ts) +and [host handler tests](../../gateway/kube/examples/conditionalsave/handler_test.go). + +- **Status churn:** use a real status subresource. Prove a persisted status change advances RV, + spec projection suppresses its notification, stale PATCH returns 409, guarded projected GET + advances the base without field conflicts, and a fresh intent succeeds when churn stops. Contrast + full projection and bookkeeping-only suppression. A no-op SSA reapply is not a valid fixture. +- **Overlapping recovery:** save starts live, status advances RV, upstream closure begins a snapshot, + and the post-409 GET arrives before synced. Assert refused reconciliation, retained drafts, + recovery presentation and later progress without blind PATCH retries. +- **UID race:** force deletion/recreation between host preflight GET and PATCH with a test-only + barrier. Preserve structured API Status and prove the replacement is unchanged. Classify the + observed identity mismatch specifically; never normalize all 422 validation errors to conflicts. + +**Acceptance:** observable winner/draft preservation and accurate outcomes, with the exact tested +commit, server version, commands, scenarios and results attached to the PR. Use existing host +validation boundaries; do not generalize the ConfigMap endpoint just to build a test. + +Wire focused real-API cases into CI or an explicitly invoked workflow whose successful run is merge +evidence. Keep broader aggregated-API coverage available through `task test-cluster`. Record skips +and unrun cases separately from fake-client results; update task/workflow comments to match coverage. + +## 4. Measured upstream continuation Implement behind [the Kubernetes backend](../../gateway/kube/backend.go) and its existing `Watcher` -seam. Kubernetes supports continuing from a retained watch version and rebuilding state after history -expires. Use that upstream mechanism; downstream v1 remains snapshot-based on a new connection. -[Kubernetes watch semantics](https://kubernetes.io/docs/reference/using-api/api-concepts/#efficient-detection-of-changes) - -First assess the client-go watch utilities available in the repository's pinned version. Prefer a -compatible upstream utility when it preserves our initial-snapshot boundary, cancellation and error -semantics. Do not introduce an informer/cache hierarchy merely to restart a watch. Document why the -chosen utility fits, or why a small internal loop is needed. - -```mermaid -flowchart TD - I["Initial streaming list or list then watch"] --> B["Snapshot boundary established"] - B --> L["Consume live events and retain upstream checkpoint"] - L --> X{"Watch outcome"} - X -->|Routine close or retryable failure| R["Cancellable bounded backoff, reopen at checkpoint"] - R -->|Continuity retained| L - R -->|History expired or continuity lost| S["Existing RESYNC_REQUIRED and snapshot recovery"] - X -->|Terminal auth failure| T["Propagate terminal outcome"] - X -->|Stop or cancellation| C["Close watcher and timers"] - S --> I -``` - -Implementation constraints: +seam. Assess the pinned client-go utilities first; explain why one fits or why a small internal loop +is needed. Downstream v1 remains snapshot-based on a new connection. - Retain checkpoints from upstream events and bookmarks before projection/suppression. Never derive them from a browser object, SSE seq or shared projected view. Do not require bookmark cadence. @@ -227,40 +123,30 @@ forbidden response, repeated transient failure, cancellation and subscriber depa initialization paths against the real API server where supported. Measure baseline and changed runs with the same workload: upstream reopen count, downstream reset -count by cause, serialized bytes, recovery duration and save 409 rate. Use existing observations where -possible. A 1 MB snapshot delivered to 200 subscribers costs roughly 200 MB before compression whether -triggered by browser reconnects or a shared upstream recycle; this is an illustration, not a measured -production rate. Successful continuation avoids that particular reset, not every possible resnapshot. +count by cause, snapshot bytes and duration, browser deserialization/reconciliation cost, recovery +latency, access-review rate/latency and save 409 rate. Also record the fraction of 409s with no field +conflicts and whether the next deliberately captured save succeeds once churn stops. Include routine +recycling, browser reconnects, slow subscribers and access revocation. Keep metric labels bounded; +do not label by UID, username or opaque RV. One shared watch still incurs a snapshot transfer and +browser reconciliation per subscriber; measure those costs rather than inferring capacity from +upstream watch count. **Acceptance:** retained-history recycling preserves downstream continuity, lost history still recovers safely, retries stop correctly, and authorization/credential lifecycle expectations remain explicit. No new SSE events or downstream replay protocol. -## 7. Verification and completion - -For the correction, use the existing task definitions for client/example tests, Go tests with race -detection, wire/browser integration, lint, fixture checks and package validation. Verify any added -example test is actually discovered, not merely typechecked. Run the real API cases via -`task test-cluster`; attach the server version and results to the implementation PR. - -Current CI does not execute the real-cluster suite on every PR. In the follow-up hardening PR, add -a focused real-API job for the new save/identity cases using the existing cluster tooling, or wire them into an explicitly invoked -workflow whose successful run is part of the merge evidence. Keep the broader aggregated-API suite -available through `task test-cluster`; don't imply a fake-client job covers API behavior. Update Task -and workflow comments to match whichever coverage is implemented. - -After pushing each implementation PR, inspect checks on that exact commit and report required checks, -failures and any explicitly separate integration run. A previous green commit is not evidence for the -new code. Documentation-only edits need link and Mermaid validation, not a cluster rebuild. +## Verification and scope -## 8. Deliberately deferred +Use [existing verification tasks](../../CONTRIBUTING.md#test-levels) appropriate to each change, +including package and browser/wire checks for runtime work. Inspect CI on the final pushed commit; +previous green commits and adopter reports are supporting history, not current validation. +Documentation-only edits need link and diagram checks, not a cluster rebuild. -Version-only wire events, independent content/delivery switches, downstream replay, write tickets, -automatic conflict-free retry and a general SSA save abstraction need separate concrete use cases. -A future “omit status, report versions” contract must be client-requestable within host policy and -must not silently change spec-only subscribers' current quiet behavior. +Consumer acceptance remains separate: pin npm and both Go modules, check consumer CI/image +toolchains, and exercise concurrent editing, later typing during saves, recovery, session expiry and +UID replacement in the browser. Voter's 30s recheck / 5s timeout and 60s termination target require +measurement under its actual 200-attendee workload with bounded callbacks and sinks. -Keep the optional Vue adapter thin and outside core dependencies. Keep the clearer authorizer naming -already introduced; no further naming sweep is needed to solve these problems. The next useful work -is a precise contract and an example people can copy safely, followed by upstream continuation through -Kubernetes' existing watch mechanisms. +Version-only events, independent content/delivery switches, downstream replay, write tickets, +automatic conflict-free retry and a general SSA abstraction remain deferred until a concrete use +case and measurements justify them. Preserve current named-projection semantics and host policy. diff --git a/docs/releasing.md b/docs/releasing.md index 0ba7787..0d04daf 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -22,8 +22,14 @@ produces these tags and packages: | Go Kubernetes adapter | `gateway/kube/vX.Y.Z` tag | | Official browser client | `@configbutler/krm-stream` on npm | -`krm-stream@0.1.0` is a deprecated, frozen name claim that points to the official package. It is not -part of CI or future releases; new consumers should install `@configbutler/krm-stream`. +The historical `krm-stream@0.1.0` publication is outside the maintained release surface. Its local +forwarding package has been removed; install `@configbutler/krm-stream`. Removing local source does +not change an already published npm artifact. + +Before 1.0, remove superseded API names and forwarding packages instead of maintaining compatibility +shims. Record each removal and its replacement in release notes, and update repository callers, +examples and documentation together. Changes to wire semantics still require explicit spec and +conformance review. ## Publishing setup @@ -39,7 +45,7 @@ run can therefore leave GitHub/Go tags present without an npm package. Fix sourc new release rather than moving public tags. Configure npm trusted publishing for `ConfigButler/krm-stream`, workflow `release.yml`. No -long-lived `NPM_TOKEN` is required. `krm-stream` is frozen and needs no publisher configuration. +long-lived `NPM_TOKEN` is required. Configure only the maintained scoped package. ## Before merging a release PR diff --git a/docs/saving.md b/docs/saving.md index c265ce3..75afd2e 100644 --- a/docs/saving.md +++ b/docs/saving.md @@ -59,7 +59,33 @@ actual draft conflicts separately; when none exist, explain the refreshed base a captured save. If reconciliation is refused, preserve the draft and recover before writing again. Do not remove concurrency protection or blindly retry the old patch with a newer version. -## Answer 204 and let the watch echo it +## What the person editing sees + +These presentations map to the copyable conditional editor's outcomes. The host owns wording, +authentication and any receipt or Git workflow; connection state is not a save guarantee. + +| Situation / outcome | Suggested presentation | Host action | +|---|---|---| +| `version-stale`, no field conflicts | “Configuration refreshed. Your edits are intact; review and save again.” | Capture a new intent on the next deliberate Save. Do not show an empty conflict panel. | +| `draft-conflict` | Show local and current values at each conflicting field. | Offer explicit resolution through store APIs; keep the rest of the form visible. | +| Connection retrying or `recovering` | “Reconnecting. Your unsaved changes are still here.” | Disable writes until live; after a refused GET, the example requires a later accepted guarded read before another write. | +| `saved`, watch confirmation pending | “Saved to Kubernetes; waiting for live confirmation.” | Preserve later typing. Track any receipt separately from draft state. | +| Session expiry or access denial | Explain sign-in or access outcome. | Handle identity-scoped recovery; do not retry terminal auth failures indefinitely or label them field conflicts. | +| `unavailable`, deleted/recreated UID | “This configuration was removed. A replacement must be opened separately.” | Offer copy-out from a previously retained recovery copy; never apply the old draft automatically to the replacement. | + +“Unsaved changes are still here” applies while the UID remains in the store. `removeResource` and +snapshot pruning discard deleted-object drafts. If recovery after deletion matters, retain a detached +copy as edits change, **before** removal; observing a missing UID is too late to read its old draft. +Keep recovery copies scoped to the original identity and UID, with a host-defined lifetime and cleanup. +They are for recovery, not a second draft to reconcile against incoming snapshots. An executed +subscription recipe, including edit-time capture and pruning, remains +[planned work](proposals/0006-stream-and-save-implementation-plan.md#deletion-recovery-copy). + +For explicit keep-local resolution, the planned tested recipe is tracked in +[proposal 0006](proposals/0006-stream-and-save-implementation-plan.md#explicit-keep-local-resolution). +There is currently no dedicated keep-local helper; avoid a second application conflict registry. + +## Answer 204 or a receipt and let the watch echo it The object a Kubernetes write returns is a *raw* object: `managedFields`, the last-applied annotation, `status`, and the Secret values your projection withholds. Writing it to the response hands the @@ -71,6 +97,12 @@ stream as an ordinary `modified` event: projected, redacted, and three-way merge user is still holding. Dirty state is derived from `draft` versus `server`, so there is nothing to clear and nothing to adopt. The echo settles it. +A **receipt-only HTTP 200** is also valid: define and validate a host receipt schema containing only +intended acknowledgment fields, and keep it separate from the resource store. The copyable editor +accepts successful HTTP status but does not parse a receipt; add parsing in the host when needed. +For a Git-backed workflow, Kubernetes write acceptance, CommitRequest acceptance and an observed Git +commit are separate milestones. A receipt must not imply that all three have happened. + ## If you must answer with the object Prefer 204. If a host returns an object, capture `store.captureReconciliation(uid)` **before** the diff --git a/docs/vue.md b/docs/vue.md index 53478b7..400e1f7 100644 --- a/docs/vue.md +++ b/docs/vue.md @@ -13,7 +13,14 @@ const { resource, state } = useLiveResource(store, uid, connection); ``` Use `resource.value?.draft` in script and `resource?.draft` in a template. A missing/deleted UID gives -`null`. For a changing selection, remount a keyed editor or create a new effect scope. Use store +`null`. For deletion recovery, capture the initial draft and subscribe to store notifications, retaining +`store.draft(uid)` while the fixed UID exists and keeping the last copy once it is absent. Capturing +only when Save is clicked loses subsequent edits. The null notification is too late to read the +removed draft. A [tested subscription recipe](proposals/0006-stream-and-save-implementation-plan.md#deletion-recovery-copy) +is pending; follow the [retention guidance](saving.md#what-the-person-editing-sees) and never apply a +recovery copy automatically to a replacement UID. + +For a changing selection, remount a keyed editor or create a new effect scope. Use store methods such as `setValue`, `removeKey` and conflict resolution methods for edits: do not bind `v-model` directly to the draft snapshot. Store reads are detached copies, and editing them bypasses policy checks and change notifications. diff --git a/examples/conditional-save/README.md b/examples/conditional-save/README.md index 1875c46..d8bb5ea 100644 --- a/examples/conditional-save/README.md +++ b/examples/conditional-save/README.md @@ -43,7 +43,10 @@ status-only updates. This is safe rejection, but sustained churn can hinder save GET advances the base without requiring a snapshot. Reconcile first; never transplant an old patch onto the latest version. No automatic write retry is performed. -Successful writes return 204. The stream echo settles the saved values while retaining later edits. +This endpoint returns 204 for successful writes. A host may instead return a receipt-only HTTP 200 +under the [saving guide’s receipt contract](../../docs/saving.md#answer-204-or-a-receipt-and-let-the-watch-echo-it); +the client example accepts success but leaves receipt parsing to the host. The stream echo settles +the saved values while retaining later edits. If the echo is delayed, dirty state remains visible; prevent repeated saves until your host's chosen acknowledgment UX allows them. Save results never feed raw Kubernetes objects back into the store. @@ -64,3 +67,6 @@ metadata, then the next Save performs only a guarded GET. An accepted read retur (or actual draft conflicts); a subsequent user action captures a fresh write intent. This conservative example may perform an extra read when a newer watch already won. It never guesses why the guard returned false, installs a background retry loop, or retries a PATCH automatically. + +Use the [user-facing outcome table](../../docs/saving.md#what-the-person-editing-sees) when wiring the +editor UI. This controller is copyable host code, not a core package export. diff --git a/gateway/README.md b/gateway/README.md index 7a8fd2c..3fe7f8f 100644 --- a/gateway/README.md +++ b/gateway/README.md @@ -102,4 +102,4 @@ For shared streams, set `Options.ReauthorizationInterval` (for example, 30 secon `Options.ReauthorizationTimeout` (for example, 5 seconds). Each subscriber is rechecked independently, including during quiet periods; denial or timeout stops only that subscriber. Zero interval keeps cycle-only checks. See [authorization](../docs/auth.md) for callback contracts and capacity planning. -Use `kube.SubjectAccessReviewAuthorizer`; `SSARAuthorizer` is a deprecated compatibility alias. +Use `kube.SubjectAccessReviewAuthorizer` for Kubernetes-backed subscriber authorization. diff --git a/gateway/kube/authz.go b/gateway/kube/authz.go index 801a22d..f5aa094 100644 --- a/gateway/kube/authz.go +++ b/gateway/kube/authz.go @@ -131,9 +131,3 @@ func groupResource(s gateway.Scope) string { } return s.Group + "/" + s.Resource } - -// SSARAuthorizer is the historical name for SubjectAccessReviewAuthorizer. -// Deprecated: use SubjectAccessReviewAuthorizer; this creates SubjectAccessReview, not SelfSubjectAccessReview. -func SSARAuthorizer(cs kubernetes.Interface, subjectFor SubjectFor) gateway.Authorizer { - return SubjectAccessReviewAuthorizer(cs, subjectFor) -} diff --git a/gateway/kube/authz_test.go b/gateway/kube/authz_test.go index 82882d0..ba38214 100644 --- a/gateway/kube/authz_test.go +++ b/gateway/kube/authz_test.go @@ -170,13 +170,3 @@ func TestANamedScopeAsksAboutThatName(t *testing.T) { } } } - -func TestDeprecatedAuthorizerAlias(t *testing.T) { - cs, asked := reviewer(func(*authzv1.SubjectAccessReview) (bool, bool) { return true, false }) - if err := kube.SSARAuthorizer(cs, subjectOf).Authorize(t.Context(), alice, configmapScope); err != nil { - t.Fatal(err) - } - if len(*asked) != 2 { - t.Fatal("alias must check list and watch") - } -} diff --git a/packages/krm-stream-compat/CHANGELOG.md b/packages/krm-stream-compat/CHANGELOG.md deleted file mode 100644 index 5607f65..0000000 --- a/packages/krm-stream-compat/CHANGELOG.md +++ /dev/null @@ -1,12 +0,0 @@ -# Changelog - -## 0.1.0 (2026-07-14) - - -### ⚠ BREAKING CHANGES - -* `gateway.RedactedPlaceholder` is removed, and a redacted value is no longer present on the wire in any form. A consumer that rendered the placeholder from the object must render it from `redactedPaths` instead (the TS client exposes `store.redactedPaths(uid)`). No back-compat shim: there are no users yet, and keeping the landmine around to be polite to nobody would be the whole mistake repeated. - -### Features - -* add KRM resource streaming library ([#1](https://github.com/ConfigButler/krm-stream/issues/1)) ([f415a97](https://github.com/ConfigButler/krm-stream/commit/f415a97023a75b20a88436c483eca564b991fe85)) diff --git a/packages/krm-stream-compat/README.md b/packages/krm-stream-compat/README.md deleted file mode 100644 index d79698b..0000000 --- a/packages/krm-stream-compat/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# krm-stream - -Compatibility package for the scoped client: - -```bash -npm install @configbutler/krm-stream -``` - -New code should import from `@configbutler/krm-stream`. This unscoped package re-exports the scoped -package so older snippets that import `krm-stream` keep working. diff --git a/packages/krm-stream-compat/index.d.ts b/packages/krm-stream-compat/index.d.ts deleted file mode 100644 index 22d27ef..0000000 --- a/packages/krm-stream-compat/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@configbutler/krm-stream"; diff --git a/packages/krm-stream-compat/index.js b/packages/krm-stream-compat/index.js deleted file mode 100644 index 22d27ef..0000000 --- a/packages/krm-stream-compat/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from "@configbutler/krm-stream"; diff --git a/packages/krm-stream-compat/package.json b/packages/krm-stream-compat/package.json deleted file mode 100644 index 1ff83ff..0000000 --- a/packages/krm-stream-compat/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "krm-stream", - "version": "0.1.0", - "deprecated": "Deprecated: use @configbutler/krm-stream instead.", - "description": "Compatibility forwarder for @configbutler/krm-stream.", - "keywords": [ - "kubernetes", - "krm", - "sse", - "watch", - "live", - "three-way-merge", - "resource-stream" - ], - "license": "Apache-2.0", - "repository": { - "type": "git", - "url": "git+https://github.com/ConfigButler/krm-stream.git", - "directory": "packages/krm-stream-compat" - }, - "type": "module", - "main": "./index.js", - "types": "./index.d.ts", - "exports": { - ".": { - "types": "./index.d.ts", - "default": "./index.js" - } - }, - "files": [ - "index.js", - "index.d.ts", - "README.md" - ], - "dependencies": { - "@configbutler/krm-stream": "0.2.1" - }, - "engines": { - "node": ">=22" - } -} diff --git a/packages/krm-stream/README.md b/packages/krm-stream/README.md index 9cb96f1..e4659f2 100644 --- a/packages/krm-stream/README.md +++ b/packages/krm-stream/README.md @@ -46,8 +46,8 @@ against a real `EventSource`. Neither has a runtime dependency. ## Status -The unscoped `krm-stream` package is a compatibility forwarder. This project is pre-1.0: the protocol -and the API may still change before 1.0. See the repository [README](../../README.md), +Install `@configbutler/krm-stream`. This project is pre-1.0: the protocol and API may still change +before 1.0. Renamed APIs are removed rather than retained as compatibility aliases. See the repository [README](../../README.md), [client state model](../../docs/client-state-model.md), and [release guide](../../docs/releasing.md). ## Managed connections and conditional saves