diff --git a/CHANGELOG.md b/CHANGELOG.md index 596be6615..760653b40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ Notable changes to `Icod.Terminal` are recorded here for consumers who need a concise release history. Detailed design evidence remains in the versioned roadmaps, tranche records, and public-API baseline documents. +## 1.18.0 + +### Unknown-rendition baseline recovery + +- Adds `TerminalScreenPlanner.PlanRenditionBaseline()` for safe recovery from unknown physical rendition state without requiring a caller-supplied known current rendition. +- Derives obligations from raw attribute-entry and color-selection evidence; returns `null` when any exposed axis cannot be restored unconditionally. +- Emits global attribute reset or every required safe specific exit in stable order, then restores original colors. +- Preserves valid zero-byte planning for empty and reset-only profiles while retaining exact byte cost, padding, affected-line, same-session ownership, stale-epoch, cancellation, and transaction semantics. +- Exposes no TermInfo type or raw capability/terminal-string API and leaves retained screen/layout/damage policy above Terminal. +- Finalizes identical `net8.0`/`net9.0`/`net10.0` public API snapshots with fingerprint `48975f2c42f6c544e9c574a9b3d79f7e2b7b3ecb10ab1a5a0b7067749e38e65d`. +- Retains production dependencies `Icod.TermInfo 1.15.0` and `Icod.Timing 1.0.0`. +- Qualifies fresh package consumers, published `Icod.DCurses 1.6.0` compatibility, and a separate TermInfo-free future-renderer package consumer on all target frameworks. + +See `docs/releases/1.18.0.md`, `docs/Public-API-Baseline-1.18.md`, and `Icod.Terminal-1.18.0-Development-Roadmap.md` for the complete contract. + ## 1.17.1 ### Packaged README and release metadata correction diff --git a/Directory.Build.props b/Directory.Build.props index b8f8f90a5..f54380e27 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 1.17.1 + 1.18.0 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix) diff --git a/Icod.Terminal-1.18.0-Development-Roadmap.md b/Icod.Terminal-1.18.0-Development-Roadmap.md new file mode 100644 index 000000000..ebc448e4e --- /dev/null +++ b/Icod.Terminal-1.18.0-Development-Roadmap.md @@ -0,0 +1,173 @@ +# Icod.Terminal 1.18.0 Development Roadmap + +**Release:** `1.18.0` +**Theme:** Unknown-rendition baseline recovery for Terminal-only screen consumers +**Status:** T180-T185 accepted; stable `1.18.0` candidate qualified +**Candidate identity:** `1.18.0` +**Stable compatibility floor:** `1.0.0` +**Current published line:** `1.17.0`; `1.17.1` is the documentation-only patch baseline + +## Release objective + +Version 1.18 closes the single Terminal contract gap found by the `Icod.DCurses 2.0` decoupling readiness gate. A renderer that does not know the terminal's physical rendition state must be able to request one safe, opaque plan that restores every rendition axis exposed by the selected profile's enter/select evidence. + +The governing rule is: + +> Terminal may claim a rendition baseline only when it can unconditionally restore every rendition axis exposed by the selected profile that Terminal can enter or select; otherwise no plan is available. + +The dependency direction remains: + +```text +Icod.DCurses -> Icod.Terminal -> Icod.TermInfo +``` + +`Icod.TermInfo` remains Terminal's private capability-data, expansion, padding, and cost authority. No TermInfo type, capability identifier, terminal string, or expansion program enters the new public contract. + +## Design and implementation authorities + +- [`docs/superpowers/specs/2026-09-18-1.18.0-rendition-baseline-design.md`](docs/superpowers/specs/2026-09-18-1.18.0-rendition-baseline-design.md) +- [`docs/superpowers/plans/2026-09-18-1.18.0-rendition-baseline.md`](docs/superpowers/plans/2026-09-18-1.18.0-rendition-baseline.md) +- [`Icod.DCurses 2.0 PR #32`](https://github.com/uniblab/Icod.DCurses/pull/32) + +## Accepted implementation record + +T180-T185 are complete on PR #63. The implementation adds only `PlanRenditionBaseline()` to the public surface and retains production `Icod.TermInfo 1.15.0` / `Icod.Timing 1.0.0`. + +- T180 recorded the `CS1061` package/source RED witness before the method existed. +- T181 implemented unknown-state restoration with all-or-nothing availability. +- T182 qualified attribute-only, color-only, combined, specific-exit, unsafe, empty, reset-only, padding, deterministic, and side-effect-free behavior. +- T183 qualified same-session ordering, foreign-session rejection, stale output epochs, and pre-commit cancellation. +- T184 froze identical API snapshots at `48975f2c42f6c544e9c574a9b3d79f7e2b7b3ecb10ab1a5a0b7067749e38e65d`, verified the deliberate mismatch rejection, and passed the candidate package, XML, dependency, published DCurses 1.6.0, and TermInfo-free future-renderer consumers across all three target frameworks. +- T185 finalized stable package identity and release-facing metadata, then qualified the stable candidate at commit `56bbc011325e5c88e67f243a9b882b97bae9aac7` in workflow run `35382158657`. + +The retained DCurses PR #32 package witness remains governed by its published-package resume criterion. It resumes after `Icod.Terminal 1.18.0` is published; no unpublished project reference or raw TermInfo workaround is introduced. + +## Stable-candidate qualification + +Workflow run [`35382158657`](https://github.com/uniblab/Icod.Terminal/actions/runs/35382158657) passed all nine jobs at source head `56bbc011325e5c88e67f243a9b882b97bae9aac7`: Windows, Linux, macOS, package candidate, Foundation, Presentation, Semantic and hardening, Stable 1.x release line, and validated package artifact. + +- Unit tests: 2,383 passed, zero failed, on each of `net8.0`, `net9.0`, and `net10.0`. +- TermInfo integration tests: 15 passed, zero failed, on each target framework. +- Stable package: `Icod.Terminal.1.18.0.nupkg`, SHA-256 `e8f2b374fd0865aa151910686197096b344f7daf8d0ef72331520d944b322932`. +- Symbols package: `Icod.Terminal.1.18.0.snupkg`, SHA-256 `8a14d163f2ea0bf030bea0416e0a84a919a2e8c83c692956b3a2a98470d78422`. +- Candidate artifact: ID `10563191796`, uploaded ZIP SHA-256 `b0fc9e57dcd0ccf42befc7d8d6fe91e9b0e19b62691fbfb775164252147fa028`. +- Validated artifact: ID `10562732859`, uploaded ZIP SHA-256 `1ddf8f76fbb6131271dd9123d5cab8043ddc68fd0084dfc1d1e84e8916121537`. +- Public API: identical on all target frameworks, SHA-256 `48975f2c42f6c544e9c574a9b3d79f7e2b7b3ecb10ab1a5a0b7067749e38e65d`. +- Package consumers: stable 1.x smoke, published `Icod.DCurses 1.6.0` soak, and the TermInfo-free future-renderer screen-contract witness passed on all target frameworks. + +## Public contract + +Add one method to the existing session-bound planner: + +```csharp +public TerminalScreenOperationPlan? PlanRenditionBaseline(); +``` + +The method represents an unknown physical rendition, not a known current `TerminalScreenRendition`. It is side-effect free and returns an opaque `Rendition` plan owned by the same planner/session. + +The returned plan: + +- restores all enterable text attributes to their normalized default through a global reset or a complete stable sequence of safe specific exits; +- restores foreground and background colors to terminal defaults whenever Terminal can select colors; +- emits the global attribute reset before original-color restoration; +- includes capability expansion, padding, exact encoded-byte cost, and affected-line accounting; +- remains valid only through the existing same-session transaction ownership rules. + +The method returns `null` if any profile-exposed enterable/selectable rendition axis cannot be restored safely from an unknown state. This obligation is computed from entry/selection evidence before current normalization suppresses unsafe requests. A valid zero-byte plan is allowed only when the profile exposes no enterable attribute and no selectable color axis; reset capabilities alone do not create reachable state. + +Existing `PlanRenditionTransition(...)`, `PlanRenditionReset(current)`, normalization, operation-plan opacity, and transaction semantics remain unchanged. + +## Tranche sequence + +```text +T180 contract freeze, reference snapshot, API-regret gate, and alpha identity +T181 unknown-state rendition-baseline planner and focused red-green tests +T182 capability ordering, padding, cost, zero-byte, and nullability hardening +T183 session ownership, transaction commitment, and adversarial qualification +T184 package/API/XML/docs and DCurses 2.0 package-only acceptance +T185 release-candidate qualification and stable 1.18.0 closure +``` + +## T180 — Contract freeze and development identity + +Record the 1.17.1 baseline, establish `1.18.0-alpha.1`, and freeze the single additive public signature and its XML contract before implementation. + +Acceptance requires: + +- no change to existing 1.x signatures or behavior; +- no raw TermInfo or escape-sequence exposure; +- explicit distinction between unknown physical state and known-current reset; +- exact profile-evidence, safety, nullability, ordering, ownership, and zero-byte rules; +- a package-level failing witness matching the DCurses T2001 blocker, with representative enter/select capabilities added to its synthetic profile so the `` expectation is semantically reachable. + +## T181 — Baseline planner + +Implement `PlanRenditionBaseline()` in `TerminalScreenPlanner` using existing private capability interpretation and opaque plan construction. + +Acceptance covers: + +- attribute-only profiles using global attribute reset; +- multi-attribute profiles without a global reset using every required safe specific exit in stable order; +- color-only profiles using original-color-pair restoration; +- combined profiles emitting attribute reset before color restoration; +- correct `Rendition` kind and one affected line; +- side-effect-free planning followed by exact-byte transaction commitment. + +## T182 — Safety and cost hardening + +Prove that availability describes unconditional recovery rather than a best-effort partial reset. + +Acceptance covers: + +- `null` when any enterable attribute lacks both a global reset and a safe specific exit; +- complete specific-exit coverage and stable underline, standout, italic, then strikeout ordering when no global reset exists; +- `null` when foreground or background selection is available without original-color-pair restoration; +- a zero-byte plan only for a profile with no enterable attribute and no selectable color axis, including a reset-only profile; +- exact padding-sensitive byte cost after capability expansion; +- deterministic segment ordering, overflow safety, and repeated-call stability; +- no dependence on a caller-supplied or retained physical rendition state. + +## T183 — Ownership and transaction qualification + +Exercise the new plan through the existing transaction boundary without inventing another output or state domain. + +Acceptance covers same-session commitment, foreign-session rejection, stale-epoch rejection, cancellation before commitment, serialized output, and unchanged cleanup/failure behavior. Planning itself must perform no writes, flushes, epoch changes, or lifecycle mutation. + +## T184 — Package and downstream acceptance + +Freeze identical public API/XML snapshots for `net8.0`, `net9.0`, and `net10.0`; update the active baseline selector, package smoke, and the local future-DCurses compile/package consumer. That local consumer and the eventual DCurses production boundary must have no direct `Icod.TermInfo` reference. Separately, compile and run the DCurses T2001 exact-byte witness against the candidate package using its already reviewed test-only TermInfo fixture exception. The retained downstream witness's synthetic profile must add representative enter/select capabilities before it can truthfully expect ``; its public API call, transaction path, and exact-byte assertion remain unchanged. + +Published `Icod.DCurses 1.6.0` compatibility remains a separate required witness. Production dependencies remain `Icod.TermInfo 1.15.0` and `Icod.Timing 1.0.0` unless an independently justified qualification issue requires a later reviewed change. + +## T185 — Stable closure + +Complete cross-platform, package, public-API, XML-documentation, dependency, security, and exact-head artifact qualification. Synchronize README, changelog, compatibility authority, release notes, and the main roadmap. + +Tagging, release creation, and publication remain explicit maintainer actions after the stable candidate is accepted. + +Current closure work removes the prerelease suffix, synchronizes consumer/release/architecture/security documentation, runs the complete exact-head workflow, records its artifact/test evidence, and stops before merge, tag, GitHub Release, or publication. + +## Explicit non-goals + +Version 1.18 does not add: + +- retained physical rendition state or automatic state tracking; +- desired-versus-physical screen comparison or repaint policy; +- Terminal-owned cells, windows, layout, clipping, damage, or Unicode width; +- public raw capabilities, terminal strings, or generic control-sequence builders; +- a replacement for `PlanRenditionReset(current)`; +- transaction, output-epoch, hyperlink, raster, input, query, or lifecycle redesign; +- new raster-animation, PTY, process-hosting, or widget features; +- a production dependency update without separate evidence and review. + +## Release gates + +The stable candidate must pass: + +- focused red-green exact-byte rendition-baseline tests; +- the full unit and TermInfo-integration suites on all target frameworks; +- Windows, Linux, and macOS workflow matrices; +- package/public API/XML/license validation; +- fresh package-only screen-contract and DCurses 2.0 readiness consumers; +- published DCurses 1.6 compatibility and hardening; +- exact-head artifact and independent review gates. diff --git a/Icod.Terminal-Development-Roadmap.md b/Icod.Terminal-Development-Roadmap.md index 945c4705e..1f6b5585e 100644 --- a/Icod.Terminal-Development-Roadmap.md +++ b/Icod.Terminal-Development-Roadmap.md @@ -6,7 +6,8 @@ - **Target frameworks:** `net8.0`; `net9.0`; `net10.0` - **Current published feature line:** `1.17.0` — Terminal-owned Screen Output Contracts - **Current patch line:** `1.17.1` — Packaged README and release metadata correction -- **Development status:** 1.17.0 published; 1.17.1 documentation-only patch in qualification +- **Development status:** 1.17.1 published; 1.18.0 stable candidate qualified on PR #63 +- **Active development target:** `1.18.0` — Unknown-rendition baseline recovery for Terminal-only screen consumers - **Stable compatibility floor:** `1.0.0` ## Purpose @@ -23,6 +24,8 @@ The published feature line uses production `Icod.TermInfo 1.15.0` and optional t Version 1.17.1 is a documentation-only patch that corrects the README embedded in 1.17.0 and synchronizes release metadata. It makes no runtime or public-API change. +Version 1.18.0 is a qualified stable source candidate on PR #63. T180-T185 are accepted: the additive unknown-rendition baseline API, hardening/ownership tests, identical three-framework API fingerprint, package/XML gates, published DCurses 1.6.0 soak, TermInfo-free future-renderer package witness, and nine-job stable-candidate workflow are complete. Merge, tag, release creation, and publication remain maintainer actions. + ## Current architecture ```text @@ -71,12 +74,13 @@ Optional integration tests/samples may use `Icod.TermInfo.Inspection 1.15.0`; In 1.16.0 persistent raster animation and frame lifecycle PUBLISHED 1.17.0 Terminal-owned dimensions, screen planning, and transactions PUBLISHED 1.17.1 packaged README and release metadata correction PATCH +1.18.0 unknown-rendition baseline recovery CANDIDATE ``` -The final 1.17 public API fingerprint is: +The 1.18 public API fingerprint is: ```text -c0a051a925d551e526343ef59d8c47d75e41868d84235fa30bfa7debe1b3ceb9 +48975f2c42f6c544e9c574a9b3d79f7e2b7b3ecb10ab1a5a0b7067749e38e65d ``` Permanent ownership authority: [`docs/Persistent-Raster-Ownership.md`](docs/Persistent-Raster-Ownership.md). @@ -212,8 +216,44 @@ Authorities: - [`docs/superpowers/specs/2026-09-17-1.17.0-terminal-screen-output-design.md`](docs/superpowers/specs/2026-09-17-1.17.0-terminal-screen-output-design.md) - [`docs/superpowers/plans/2026-09-17-1.17.0-terminal-screen-output.md`](docs/superpowers/plans/2026-09-17-1.17.0-terminal-screen-output.md) +## 1.18 development line — Unknown-rendition Baseline Recovery + +Version 1.18 is a focused additive release that closes the sole blocking Terminal contract found by the DCurses 2.0 readiness gate. + +The governing rule is: + +> Terminal may claim a rendition baseline only when it can unconditionally restore every rendition axis exposed by the selected profile that Terminal can enter; otherwise no plan is available. + +The release adds one public planner method: + +```csharp +public TerminalScreenOperationPlan? PlanRenditionBaseline(); +``` + +The plan represents unknown physical rendition state, restores attributes before original colors in a safe deterministic order, retains Terminal-owned expansion/padding/cost and same-session transaction ownership, and returns a valid zero-byte plan only when the selected profile exposes no enterable attribute and no selectable color axis. Existing known-state reset and transition behavior remains unchanged. + +T180-T185 are accepted. The stable candidate at `56bbc011325e5c88e67f243a9b882b97bae9aac7` passed the complete Windows/Linux/macOS, package, API/XML, downstream, and artifact matrix in workflow run `35382158657`. Publication remains a separate maintainer action. + +The tranche sequence is: + +```text +T180 contract freeze, reference snapshot, API-regret gate, and alpha identity +T181 unknown-state rendition-baseline planner and focused red-green tests +T182 capability ordering, padding, cost, zero-byte, and nullability hardening +T183 session ownership, transaction commitment, and adversarial qualification +T184 package/API/XML/docs and DCurses 2.0 package-only acceptance +T185 release-candidate qualification and stable 1.18.0 closure +``` + +Authorities: + +- [`Icod.Terminal-1.18.0-Development-Roadmap.md`](Icod.Terminal-1.18.0-Development-Roadmap.md) +- [`docs/superpowers/specs/2026-09-18-1.18.0-rendition-baseline-design.md`](docs/superpowers/specs/2026-09-18-1.18.0-rendition-baseline-design.md) +- [`docs/superpowers/plans/2026-09-18-1.18.0-rendition-baseline.md`](docs/superpowers/plans/2026-09-18-1.18.0-rendition-baseline.md) +- [`Icod.DCurses 2.0 PR #32`](https://github.com/uniblab/Icod.DCurses/pull/32) + ## Later development candidates -After 1.17, independent candidates still include animation-frame composition, absolute screen-coordinate placement, pixel-within-cell positioning, richer terminal-side reconciliation only if a truthful non-destructive primitive exists, image-file decoding/transcoding, and PTY/ConPTY process hosting. +After 1.18, independent candidates still include animation-frame composition, absolute screen-coordinate placement, pixel-within-cell positioning, richer terminal-side reconciliation only if a truthful non-destructive primitive exists, image-file decoding/transcoding, and PTY/ConPTY process hosting. Scene/window/cell ownership and hidden source-raster replay caches remain intentionally outside the Terminal contract. diff --git a/Icod.Terminal.csproj b/Icod.Terminal.csproj index 5fa4ec28e..cc9d1074b 100644 --- a/Icod.Terminal.csproj +++ b/Icod.Terminal.csproj @@ -39,7 +39,7 @@ Icod.Terminal Timothy J. Bruce Managed, cross-platform live-terminal session, endpoint, mode, input, lifecycle, and terminal-control foundation for .NET. - Icod.Terminal 1.17.1 corrects the packaged README and release metadata for the published 1.17 line without changing the runtime or public API. Release notes: https://github.com/uniblab/Icod.Terminal/blob/main/docs/releases/1.17.1.md. Compatibility policy: https://github.com/uniblab/Icod.Terminal/blob/main/docs/Compatibility-and-Versioning.md. + Icod.Terminal 1.18.0 adds safe unknown-rendition baseline planning for Terminal-only screen consumers. Release notes: https://github.com/uniblab/Icod.Terminal/blob/main/docs/releases/1.18.0.md. Compatibility policy: https://github.com/uniblab/Icod.Terminal/blob/main/docs/Compatibility-and-Versioning.md. README.md icon.png https://github.com/uniblab/Icod.Terminal diff --git a/README.md b/README.md index ee484f36c..5fa734326 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,15 @@ ## Status -Current stable release: `Icod.Terminal 1.17.1`. +Current stable release: `Icod.Terminal 1.18.0`. -Version `1.17.1` corrects the packaged README and release metadata for the 1.17 line. It makes no runtime or public-API change from `1.17.0`. +Version 1.18 adds `TerminalScreenPlanner.PlanRenditionBaseline()`, allowing a Terminal-only renderer to establish the normalized default rendition safely when the physical starting state is unknown. The operation returns no plan when any profile-exposed rendition axis cannot be restored unconditionally. Version 1.17 adds Terminal-owned dimensions, an immutable semantic terminal profile, side-effect-free screen-operation planning, and bounded session-bound output transactions. These contracts provide the Terminal-side boundary required for a later `Icod.DCurses 2.0` release to remove its direct `Icod.TermInfo` dependency. -The stable `1.0.0` compatibility floor remains unchanged. Version 1.17 retains the complete 1.16 animation, 1.15 virtual-placeholder, 1.14 lifecycle-observation, 1.13 relative-placement, 1.12 crop/z-order, and earlier persistent-raster contracts. The final 1.17 public API fingerprint is `c0a051a925d551e526343ef59d8c47d75e41868d84235fa30bfa7debe1b3ceb9`. +The stable `1.0.0` compatibility floor remains unchanged. Version 1.18 retains the complete 1.17 screen-planning/transaction surface, 1.16 animation, 1.15 virtual-placeholder, and every earlier stable 1.x contract. The 1.18 public API fingerprint is `48975f2c42f6c544e9c574a9b3d79f7e2b7b3ecb10ab1a5a0b7067749e38e65d`. -The 1.17 line is additive over the complete 1.16 persistent-raster animation surface and every earlier stable 1.x contract. See the [1.17.1 release notes](docs/releases/1.17.1.md) and [changelog](CHANGELOG.md) for release-specific details. +See the [1.18.0 release notes](docs/releases/1.18.0.md) and [changelog](CHANGELOG.md) for release-specific details. ## Support the Project @@ -67,7 +67,7 @@ See [`docs/Architecture.md`](docs/Architecture.md) for the permanent architectur Install the currently published package: ```text -dotnet add package Icod.Terminal --version 1.17.1 +dotnet add package Icod.Terminal --version 1.18.0 ``` Open a managed terminal session, write application text, and read through the authoritative event path: @@ -113,6 +113,22 @@ if ( home is TerminalScreenOperationPlan plan ) { } ``` +When a renderer cannot trust its current physical rendition, version 1.18 can establish a safe baseline before emitting retained content: + +```csharp +TerminalScreenOperationPlan? baseline = + session.Screen.PlanRenditionBaseline(); + +if ( baseline is TerminalScreenOperationPlan plan ) { + TerminalScreenOutputTransaction output = + session.CreateScreenOutputTransaction(); + output.Add( plan ); + await output.CommitAsync(); +} +``` + +A `null` result means at least one rendition axis exposed by the selected profile cannot be restored unconditionally from unknown state; callers must not substitute a claimed known default. + `Profile` contains immutable selected-profile facts, while `GetDimensions()` reports the current Terminal-owned size result. A plan is opaque and session-bound; creating it emits nothing, and the transaction preserves ordering under one output gate and flush boundary. Retained cells, layout, Unicode width, clipping, damage, and repaint policy remain caller-owned. ## Feature Inventory diff --git a/docs/Architecture.md b/docs/Architecture.md index 4fd1784c2..736dc5aae 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -69,6 +69,8 @@ Placeholder cells remain current-cursor text output. Terminal owns their protoco Planning is side-effect free. A plan identifies its semantic operation, resolved terminal-byte cost, and padding-sensitive affected-line count. A higher layer may compare independently safe plans against its own retained-screen state; Terminal does not decide which cells changed or whether rewriting is preferable. +`PlanRenditionBaseline()` represents unknown physical rendition state. It derives obligations from profile entry/selection evidence and returns a plan only when every exposed attribute and color axis can be restored unconditionally. Attribute restoration precedes original-color restoration. A reset-only profile has no reachable rendition state and therefore receives a valid zero-byte plan; a partially restorable profile receives no plan. + `CreateScreenOutputTransaction(...)` captures the session serialized-output epoch and composes plans, application text, strict hyperlinks, and current raster-placeholder cells. Commit rejects stale work before output, holds the existing output gate, optionally emits synchronized-output framing, attempts required cleanup after commitment, and flushes before release. This contract does not transfer cells, windows, pads, layout, clipping, Unicode display width, damage, desired-versus-physical comparison, or repaint policy into Terminal. @@ -351,7 +353,7 @@ Persistent source cropping and virtual-placeholder rendering operate on already- ## 14. Optional TermInfo 1.14 backend planning boundary -The active 1.17 repository uses `Icod.TermInfo 1.15.0`. Optional integration tests and the `Icod.Terminal.TermInfoPersistentRaster.Sample` use `Icod.TermInfo.Inspection 1.15.0`. +The active 1.18 repository uses `Icod.TermInfo 1.15.0`. Optional integration tests and the `Icod.Terminal.TermInfoPersistentRaster.Sample` use `Icod.TermInfo.Inspection 1.15.0`. Inspection 1.14 adds advisory Sixel/Kitty backend availability evidence, candidate evaluation, and explicit backend-selection planning. That planner remains a **consumer/application policy layer**; it is not invoked by `Icod.Terminal` production routing. @@ -377,7 +379,7 @@ A conclusive live `PersistentRasterGraphics` result may be mapped by the caller TermInfo planning does not replace Terminal's live capability checks, routing, commitment, identity ownership, or cleanup. Production `Icod.Terminal` retains no dependency on `Icod.TermInfo.Inspection` or `Icod.TermInfo.Source`. -## 15. Stable exclusions after 1.17 +## 15. Stable exclusions after 1.18 Stable 1.x still does not treat the following as ordinary `Icod.Terminal` responsibilities: @@ -401,7 +403,7 @@ Relative placement, lifecycle observation, virtual placeholders, and resource-ow ## 16. Dependency boundary -`Icod.Terminal.csproj` is the direct NuGet dependency authority. The active 1.17 production graph is: +`Icod.Terminal.csproj` is the direct NuGet dependency authority. The active 1.18 production graph is: ```text Icod.TermInfo 1.15.0 diff --git a/docs/Compatibility-and-Versioning.md b/docs/Compatibility-and-Versioning.md index 2bfda34a0..17e135ec1 100644 --- a/docs/Compatibility-and-Versioning.md +++ b/docs/Compatibility-and-Versioning.md @@ -127,6 +127,14 @@ Existing `GetSize()`, `TerminalLifecycleEvent.Size`, `TerminalSession.Terminal`, Release qualification treats compatibility and future decoupling as separate claims: the 1.17 package must run with published stable `Icod.DCurses 1.6.0`, and an independent package-only consumer must compile against the Terminal-owned screen contracts with no direct TermInfo project reference or source use. The latter is evidence of API sufficiency, not a claim that DCurses 2.0 has already shipped. +### 4.7 1.18 unknown-rendition recovery + +Version 1.18 additively introduces `TerminalScreenPlanner.PlanRenditionBaseline()`. Existing known-state normalization, transition, and reset behavior is unchanged. + +The operation does not reinterpret a default rendition as known physical state. It returns an opaque same-session plan only when every profile-exposed attribute-entry and color-selection axis can be restored unconditionally. It returns `null` rather than weakening this guarantee, and returns a valid zero-byte plan only when no reachable rendition state is exposed. + +The public signature contains no TermInfo type. Package qualification retains the 1.17 compatibility surface, production dependency versions, and all historical API baselines while adding the frozen 1.18 fingerprint. + ## 5. Persistent-raster compatibility guarantees The following remain compatible guarantees: @@ -173,7 +181,7 @@ Endpoint availability remains separate from support knowledge. Static advertisem ## 8. TermInfo 1.15 optional integration compatibility -The active 1.17 direct production dependency graph remains: +The active 1.18 direct production dependency graph remains: ```text Icod.TermInfo 1.15.0 @@ -222,7 +230,7 @@ The following remain implementation details rather than compatibility promises: The optional `Icod.TermInfo.Inspection` backend vocabulary is a separate consumer planning API. Its presence in a sample/test does not expose a caller-selected raw backend switch in `Icod.Terminal` production API. -## 11. Deliberate non-promises after 1.17 +## 11. Deliberate non-promises after 1.18 Stable 1.x does not promise: diff --git a/docs/Public-API-Baseline-1.18.md b/docs/Public-API-Baseline-1.18.md new file mode 100644 index 000000000..000f45510 --- /dev/null +++ b/docs/Public-API-Baseline-1.18.md @@ -0,0 +1,25 @@ +# Icod.Terminal 1.18 Public API Baseline + +This document records the additive public API for the `Icod.Terminal 1.18.0` rendition-baseline release. + +The stable compatibility floor remains `1.0.0`. The complete 1.17 public surface remains available and unchanged. The predecessor 1.17 public API fingerprint is: + +```text +c0a051a925d551e526343ef59d8c47d75e41868d84235fa30bfa7debe1b3ceb9 +``` + +The generated public API snapshots are identical on `net8.0`, `net9.0`, and `net10.0`. The 1.18 fingerprint is: + +```text +48975f2c42f6c544e9c574a9b3d79f7e2b7b3ecb10ab1a5a0b7067749e38e65d +``` + +The machine-readable fingerprint is stored in `docs/Public-API-Baseline-1.18.sha256` and is enforced by the package/public-API gate. + +## Public addition over 1.17 + +`TerminalScreenPlanner.PlanRenditionBaseline()` returns an opaque, session-bound rendition plan that restores the normalized default rendition without assuming a known physical starting state. It returns `null` when any attribute or color axis exposed by the selected terminal profile cannot be restored unconditionally. + +The plan prefers the global attribute reset; otherwise it emits every required safe specific attribute exit in stable order, followed by original-color restoration when a selectable color axis is exposed. A profile with no exposed rendition entry or selection capability receives a valid zero-byte plan, including reset-only profiles. + +The method is additive and exposes no `Icod.TermInfo` type, capability identifier, terminal string, or expansion API. Existing rendition normalization, transition, reset, operation-plan ownership, and transaction behavior remain unchanged. diff --git a/docs/Public-API-Baseline-1.18.sha256 b/docs/Public-API-Baseline-1.18.sha256 new file mode 100644 index 000000000..e54a425a7 --- /dev/null +++ b/docs/Public-API-Baseline-1.18.sha256 @@ -0,0 +1 @@ +48975f2c42f6c544e9c574a9b3d79f7e2b7b3ecb10ab1a5a0b7067749e38e65d diff --git a/docs/Security-and-Privacy.md b/docs/Security-and-Privacy.md index 356d67a67..10e1c36f8 100644 --- a/docs/Security-and-Privacy.md +++ b/docs/Security-and-Privacy.md @@ -191,7 +191,7 @@ The stable semantic surface intentionally does not expose: ## 14. TermInfo 1.14 advisory backend planning -The active 1.17 repository directly depends on `Icod.TermInfo 1.15.0`. Optional integration tests and samples use `Icod.TermInfo.Inspection 1.15.0`. +The active 1.18 repository directly depends on `Icod.TermInfo 1.15.0`. Optional integration tests and samples use `Icod.TermInfo.Inspection 1.15.0`. Inspection's `RasterBackendPlanner` classifies advisory Sixel/Kitty availability and applies explicit caller preference. This is **not** production Terminal routing and is not a security/authentication oracle. @@ -228,7 +228,7 @@ TermInfo backend planning does not justify extra probes by itself; runtime verif ## 18. Dependency boundary -The active 1.17 production package graph is: +The active 1.18 production package graph is: ```text Icod.TermInfo 1.15.0 @@ -260,19 +260,21 @@ Animation acknowledgements remain untrusted terminal input. Correlation routes o Applications must not treat animation state, selected frame, timing, looping, placement, or visual coverage as a security boundary. The terminal controls final rendering and may ignore, evict, reinterpret, record, or externally compose output. -## 20. Semantic screen planning and output commitment — 1.17 +## 20. Semantic screen planning and output commitment — 1.17 and 1.18 Terminal-profile facts are immutable projections of the selected description, not authenticated live observations. Applications must not treat declared screen capabilities, dimensions, cursor position, rendition state, terminal content, or successful output as a security boundary or as proof of what a terminal ultimately rendered. Screen planning is side-effect free and returns opaque reviewed plans. Public callers cannot inject raw capability identifiers, expansion programs, padding directives, or arbitrary terminal strings through the planner. Dimensions, coordinates, counts, colors, regions, payload bytes, operation count, and aggregate transaction payload are validated and bounded before commitment. +Unknown physical rendition is not treated as a known default. `PlanRenditionBaseline()` derives restoration obligations from selected-profile entry/selection evidence and returns `null` if any exposed axis lacks unconditional restoration. This prevents higher layers from silently accepting a partial reset as a safe baseline. Reset and selection strings remain private TermInfo-derived data inside the opaque plan. + Plans, hyperlink content, and raster-placeholder cells retain exact session/owner identity. A transaction rejects foreign, stale, released, or disposed retained items before output. Creation captures the serialized-output epoch; intervening session-owned output invalidates the batch before commitment rather than allowing an outdated retained-screen decision to be emitted. Commit holds the existing output gate across the logical batch, optional synchronized-output framing, required hyperlink cleanup, one final flush, and gate release. Pre-commit cancellation emits nothing. After commitment, cleanup is attempted without ordinary caller cancellation; independent primary and cleanup failures are flattened in deterministic order. Failure does not trigger blind replay or claim that the terminal applied none, some, or all of the bytes. Application text and hyperlink labels remain disclosure surfaces. The caller remains responsible for deciding whether terminal-visible content and hyperlink targets are appropriate. Strict hyperlink validation prevents control-character framing injection but does not authenticate or make a URI safe to follow. -## 21. Stable exclusions after 1.17 +## 21. Stable exclusions after 1.18 Security/privacy behavior does not include promises for: diff --git a/docs/releases/1.18.0-alpha.1.md b/docs/releases/1.18.0-alpha.1.md new file mode 100644 index 000000000..7c48eacc0 --- /dev/null +++ b/docs/releases/1.18.0-alpha.1.md @@ -0,0 +1,15 @@ +# Icod.Terminal 1.18.0-alpha.1 + +## Development identity + +`Icod.Terminal 1.18.0-alpha.1` is an unpublished development identity. It adds no accepted implementation yet. + +## Frozen additive signature + +This RED checkpoint freezes the additive signature for the upcoming rendition-baseline planner: + +```csharp +public TerminalScreenOperationPlan? PlanRenditionBaseline(); +``` + +The signature is intentionally witnessed by the in-repository screen test and the TermInfo-free future DCurses renderer. Production implementation is deliberately deferred. diff --git a/docs/releases/1.18.0.md b/docs/releases/1.18.0.md new file mode 100644 index 000000000..1058aadc9 --- /dev/null +++ b/docs/releases/1.18.0.md @@ -0,0 +1,46 @@ +# Icod.Terminal 1.18.0 + +## Unknown-rendition baseline recovery + +`Icod.Terminal 1.18.0` adds the missing recovery primitive required by a Terminal-only retained renderer: `TerminalScreenPlanner.PlanRenditionBaseline()` safely plans a return from unknown physical rendition state to Terminal's normalized default. + +The release is additive over the stable `1.0.0` compatibility floor and the complete 1.17 API. + +## Baseline planning + +- Derives restoration obligations from the selected profile's attribute-entry and color-selection evidence before reversible normalization can suppress unsafe requests. +- Prefers one global attribute reset when available; otherwise emits every required unconditional specific exit in stable underline, standout, italic, then strikeout order. +- Restores selectable foreground/background color state through original-color-pair restoration after attribute restoration. +- Returns `null` instead of a partial plan when any exposed rendition axis cannot be restored unconditionally. +- Returns a valid zero-byte `Rendition` plan when no attribute entry or color selection is exposed, including reset-only profiles. +- Reports exact emitted-byte cost after TermInfo padding interpretation and one affected line. + +Planning remains side-effect free. The returned opaque plan belongs to the originating `TerminalSession` and can be emitted only through the existing same-session screen-output transaction. + +## Compatibility and ownership + +Existing rendition normalization, `PlanRenditionTransition(...)`, `PlanRenditionReset(current)`, operation-plan ownership, output-epoch validation, cancellation, serialization, and cleanup semantics are unchanged. + +The new signature exposes no `Icod.TermInfo` type, capability identifier, terminal string, or expansion API. TermInfo remains Terminal's private capability-data, expansion, padding, and color authority. Cells, windows, layout, Unicode width, clipping, damage, desired-versus-physical comparison, and repaint policy remain higher-layer responsibilities. + +Production dependencies remain `Icod.TermInfo 1.15.0` and `Icod.Timing 1.0.0`. + +## Qualification + +The public API snapshots are identical across `net8.0`, `net9.0`, and `net10.0`. The 1.18 fingerprint is: + +```text +48975f2c42f6c544e9c574a9b3d79f7e2b7b3ecb10ab1a5a0b7067749e38e65d +``` + +Qualification covers attribute-only, color-only, combined, specific-exit, unsafe, empty, reset-only, padding, determinism, side-effect, same-session, foreign-session, stale-epoch, and cancellation cases. Fresh package consumers validate API/XML/package behavior on all target frameworks. Published `Icod.DCurses 1.6.0` compatibility and a separate TermInfo-free future-renderer package consumer are also exercised on all three frameworks. + +The stable candidate at source commit `56bbc011325e5c88e67f243a9b882b97bae9aac7` passed all nine jobs in [workflow run 35382158657](https://github.com/uniblab/Icod.Terminal/actions/runs/35382158657). Each target framework passed 2,383 unit tests and 15 TermInfo integration tests with zero failures. The validated `Icod.Terminal.1.18.0.nupkg` SHA-256 is `e8f2b374fd0865aa151910686197096b344f7daf8d0ef72331520d944b322932`; the `.snupkg` SHA-256 is `8a14d163f2ea0bf030bea0416e0a84a919a2e8c83c692956b3a2a98470d78422`. + +The candidate artifact is ID `10563191796` with uploaded ZIP SHA-256 `b0fc9e57dcd0ccf42befc7d8d6fe91e9b0e19b62691fbfb775164252147fa028`. The validated artifact is ID `10562732859` with uploaded ZIP SHA-256 `1ddf8f76fbb6131271dd9123d5cab8043ddc68fd0084dfc1d1e84e8916121537`. + +The future-renderer witness proves that the candidate Terminal package supplies the required boundary. It does not claim that DCurses 2.0 has shipped; the retained DCurses T2001 package witness resumes after this package is published. + +Tagging, GitHub Release creation, and NuGet publication remain explicit maintainer actions after candidate acceptance. + +See [`Architecture.md`](../Architecture.md), [`Compatibility-and-Versioning.md`](../Compatibility-and-Versioning.md), [`Security-and-Privacy.md`](../Security-and-Privacy.md), [`Public-API-Baseline-1.18.md`](../Public-API-Baseline-1.18.md), and [`Icod.Terminal-1.18.0-Development-Roadmap.md`](../../Icod.Terminal-1.18.0-Development-Roadmap.md) for the permanent contract and development evidence. diff --git a/docs/superpowers/plans/2026-09-18-1.18.0-rendition-baseline.md b/docs/superpowers/plans/2026-09-18-1.18.0-rendition-baseline.md new file mode 100644 index 000000000..be0c88569 --- /dev/null +++ b/docs/superpowers/plans/2026-09-18-1.18.0-rendition-baseline.md @@ -0,0 +1,157 @@ +# Icod.Terminal 1.18.0 Rendition Baseline Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a safe, package-qualified `PlanRenditionBaseline()` operation that restores unknown physical rendition state and unblocks the TermInfo-free DCurses 2.0 renderer. + +**Architecture:** Extend the existing session-bound `TerminalScreenPlanner` with one additive method. Reuse private TermInfo interpretation, output-segment construction, opaque plan ownership, and transaction commitment; availability is conservative and all-or-nothing across Terminal-reachable attribute and color axes. + +**Tech Stack:** C# 13, .NET 8/9/10, xUnit, Icod.TermInfo 1.15.0, existing package/API/XML verification tools and GitHub Actions matrix. + +**Spec:** `docs/superpowers/specs/2026-09-18-1.18.0-rendition-baseline-design.md` + +## Global Constraints + +- Preserve the stable compatibility floor `1.0.0` and all existing 1.x signatures and behavior. +- Add exactly `public TerminalScreenOperationPlan? PlanRenditionBaseline();` to the production public surface. +- Return `null` rather than a partial plan when any Terminal-reachable rendition axis is not unconditionally restorable. +- Emit a global attribute reset or complete stable sequence of safe specific exits before original-color restoration. +- Permit a zero-byte plan only when the selected profile exposes no enterable attribute and no selectable color axis; reset-only profiles are zero-byte. +- Keep planning side-effect free and reuse existing same-session transaction ownership. +- Expose no `Icod.TermInfo` type, capability identifier, terminal string, or expansion API. +- Keep production dependencies at `Icod.TermInfo 1.15.0` and `Icod.Timing 1.0.0` unless separately reviewed. +- Do not add retained screen/rendition state or unrelated feature families. + +--- + +### Task 1: Freeze the alpha contract and failing witnesses + +**Files:** +- Modify: `Directory.Build.props` +- Create: `docs/releases/1.18.0-alpha.1.md` +- Modify: `tests/Icod.Terminal.Tests/src/Screen/TerminalScreenRenditionPlannerHardeningTests.cs` +- Modify: `tools/dcurses-screen-contracts-acceptance/Source/Program.cs` + +**Interfaces:** +- Consumes: the 1.17.1 `TerminalScreenPlanner` and opaque plan/transaction contracts. +- Produces: the frozen method signature and failing source/package witnesses used by later tasks. + +- [ ] **Step 1: Establish `1.18.0-alpha.1` in the centralized version properties and document that publication is not implied.** +- [ ] **Step 2: Add a focused test that calls `session.Screen.PlanRenditionBaseline()` on a profile containing `EnterBoldMode = ""`, `ExitAttributeMode = ""`, `Colors = 16`, foreground/background selectors, and `OriginalColorPair = ""`.** +- [ ] **Step 3: Assert the plan is `Rendition`, costs 10 bytes, affects one line, performs no planning-time output, and commits ``.** +- [ ] **Step 4: Add the same API call to the Terminal repository's future-DCurses package consumer and assert that the compile/package-only consumer still has no direct TermInfo compile or reference input.** +- [ ] **Step 5: Run the focused tests and package consumer; record the expected compile failure `CS1061` because the method is absent.** +- [ ] **Step 6: Commit the red witness and contract metadata as `test: freeze rendition baseline contract`.** + +### Task 2: Implement the minimal safe baseline planner + +**Files:** +- Modify: `src/Screen/TerminalScreenPlanner.cs` +- Modify: `tests/Icod.Terminal.Tests/src/Screen/TerminalScreenRenditionPlannerHardeningTests.cs` + +**Interfaces:** +- Consumes: existing private color/attribute capability evidence, `TryAddLiteral(...)`, and opaque plan construction. +- Produces: `public TerminalScreenOperationPlan? PlanRenditionBaseline()`. + +- [ ] **Step 1: Add private predicates that answer whether the selected profile exposes each attribute entry and either color selector, using capability evidence before reversible normalization suppresses unsafe requests.** +- [ ] **Step 2: Implement `PlanRenditionBaseline()` so it returns `null` when an enterable attribute has neither `ExitAttributeMode` coverage nor its unconditional specific exit, or when a selectable color axis lacks `OriginalColorPair`.** +- [ ] **Step 3: Build segments in fixed order: prefer one `ExitAttributeMode`; otherwise add every required safe specific exit in the existing attribute order; then add `OriginalColorPair`. Omit a class only when the selected profile exposes no enterable state in it.** +- [ ] **Step 4: Construct the existing opaque `Rendition` plan with `AffectedLines = 1`; return a valid empty-segment plan only when neither class is exposed, including a reset-only profile.** +- [ ] **Step 5: Run the focused test and package witness; verify the red witness turns green and exact bytes are ``.** +- [ ] **Step 6: Run all screen-planner tests for `net8.0`, `net9.0`, and `net10.0`.** +- [ ] **Step 7: Commit as `feat: add rendition baseline planning`.** + +### Task 3: Harden availability, ordering, and cost + +**Files:** +- Modify: `tests/Icod.Terminal.Tests/src/Screen/TerminalScreenRenditionPlannerHardeningTests.cs` +- Modify: `src/Screen/TerminalScreenPlanner.cs` only if a red test requires a correction + +**Interfaces:** +- Consumes: `PlanRenditionBaseline()` from Task 2. +- Produces: executable proof of the all-or-nothing safety invariant. + +- [ ] **Step 1: Add and run a failing attribute-only test expecting exactly ``.** +- [ ] **Step 2: Add and run a failing color-only test expecting exactly ``.** +- [ ] **Step 3: Add and run a failing multi-attribute test with no `ExitAttributeMode`, entry/exit pairs for underline, standout, italic, and strikeout, and assert the baseline emits every specific exit exactly once in stable underline, standout, italic, strikeout order (for example ``).** +- [ ] **Step 4: Add and run failing tests expecting `null` for an enterable attribute without either global or specific reset and for selectable colors without original-color restoration.** +- [ ] **Step 5: Add and run failing empty-profile and reset-only-profile tests expecting a valid zero-byte `Rendition` plan.** +- [ ] **Step 6: Add a padding-bearing reset fixture and assert `ByteCount` counts emitted terminal bytes after terminfo padding is interpreted, while padding uses one affected line.** +- [ ] **Step 7: Add repeated-call assertions proving deterministic bytes/cost and no output, flush, epoch, or lifecycle mutation during planning.** +- [ ] **Step 8: Make only the minimum production corrections required by the red tests, then rerun the full rendition planner suite on all target frameworks.** +- [ ] **Step 9: Commit as `test: harden rendition baseline safety`.** + +### Task 4: Qualify ownership and transaction behavior + +**Files:** +- Modify: `tests/Icod.Terminal.Tests/src/Screen/TerminalScreenOutputTransactionHardeningTests.cs` +- Modify: `tests/Icod.Terminal.Tests/src/Screen/TerminalScreenRenditionPlannerHardeningTests.cs` + +**Interfaces:** +- Consumes: the existing opaque owner embedded in `TerminalScreenOperationPlan`. +- Produces: proof that baseline plans obey unchanged transaction rules. + +- [ ] **Step 1: Add a same-session transaction test that commits the baseline with adjacent application text and verifies serialized order.** +- [ ] **Step 2: Add a foreign-session test and verify validation rejects the complete transaction before any output.** +- [ ] **Step 3: Add stale-epoch and pre-commit cancellation cases and verify zero output.** +- [ ] **Step 4: Run transaction, rendition, lifecycle, and concurrency test groups on all target frameworks.** +- [ ] **Step 5: Commit as `test: qualify rendition baseline ownership`.** + +### Task 5: Freeze public API, XML, package, and downstream acceptance + +**Files:** +- Create: `docs/Public-API-Baseline-1.18.md` +- Create: `docs/Public-API-Baseline-1.18.sha256` +- Modify: `packaging/VerifyPublicApiBaseline.ps1` +- Modify: `tools/package-smoke/Program.cs` +- Modify: `tools/dcurses-screen-contracts-acceptance/Source/Program.cs` +- Modify: `tests/Icod.Terminal.Tests/src/Packaging/DependencyCouplingPolicyTests.cs` +- Modify: release/package workflow inputs only where existing 1.17 version assertions require 1.18 +- External downstream modify: `uniblab/Icod.DCurses: tests/Icod.DCurses.Tests/Icod.DCurses.Tests.csproj` +- External downstream restore/modify: `uniblab/Icod.DCurses: tests/Icod.DCurses.Tests/src/TerminalScreenReadinessTests.cs` + +**Interfaces:** +- Consumes: the candidate package containing `PlanRenditionBaseline()`. +- Produces: identical API/XML snapshots and package-only downstream proof across `net8.0`, `net9.0`, and `net10.0`. + +- [ ] **Step 1: Generate transient public API snapshots for all target frameworks, verify their bytes are identical, and record the canonical result in the 1.18 baseline documents.** +- [ ] **Step 2: Record the 1.18 fingerprint and add XML documentation assertions for the new method's unknown-state and nullability semantics.** +- [ ] **Step 3: Update package smoke to plan and commit a baseline using only packaged Terminal public APIs.** +- [ ] **Step 4: Update the Terminal repository's future-DCurses acceptance consumer to compile-bind `PlanRenditionBaseline()` while remaining a TermInfo-free compile/package-only witness.** +- [ ] **Step 5: In DCurses PR #32, restore `TerminalScreenReadinessTests.cs`, select the candidate/published 1.18 package as appropriate, and add representative attribute entry, `NumericCapability.Colors = 16`, and foreground/background selection capabilities through the already reviewed test-only TermInfo fixture; retain the same public method call, same-session transaction, and exact `` assertion.** +- [ ] **Step 6: Repoint `packaging/VerifyPublicApiBaseline.ps1` from the 1.17 baseline to the new 1.18 fingerprint and verify a deliberate mismatch fails the package gate.** +- [ ] **Step 7: Run the local candidate-package consumer and the external DCurses exact-byte witness on all target frameworks; rerun published `Icod.DCurses 1.6.0` compatibility separately.** +- [ ] **Step 8: Run dependency coupling, package structure, license, XML, API, and artifact validators.** +- [ ] **Step 9: Commit Terminal qualification as `test: qualify Terminal 1.18 package contracts`; preserve downstream evidence in its owning DCurses PR/commit.** + +### Task 6: Close the stable 1.18.0 candidate + +**Files:** +- Modify: `Directory.Build.props` +- Modify: `Icod.Terminal.csproj` +- Modify: `README.md` +- Modify: `CHANGELOG.md` +- Modify: `Icod.Terminal-Development-Roadmap.md` +- Modify: `Icod.Terminal-1.18.0-Development-Roadmap.md` +- Create: `docs/releases/1.18.0.md` +- Modify: `docs/Architecture.md` +- Modify: `docs/Compatibility-and-Versioning.md` +- Modify: `docs/Security-and-Privacy.md` + +**Interfaces:** +- Consumes: the fully qualified 1.18 candidate and exact-head evidence. +- Produces: stable metadata and release documentation; publication remains a separate maintainer action. + +- [ ] **Step 1: Remove the prerelease suffix and synchronize all stable 1.18 metadata and documentation.** +- [ ] **Step 2: Run the complete unit, TermInfo integration, sample, hardening, package, downstream, API/XML, license, and artifact suites locally where supported.** +- [ ] **Step 3: Push the candidate and require every Windows, Linux, macOS, package, downstream, API/XML, and artifact workflow job to pass at the exact head.** +- [ ] **Step 4: Record exact commit, workflow/run, test-count, API-fingerprint, and package-hash evidence in the version roadmap and release note.** +- [ ] **Step 5: Request independent review and resolve every Critical or Important finding before promotion.** +- [ ] **Step 6: Commit the evidence-only closure update as `docs: record Terminal 1.18 qualification`.** +- [ ] **Step 7: Stop before merge, tag, GitHub release, or package publication; those remain explicit maintainer decisions.** + +## Self-review record + +- Every design requirement maps to Tasks 1-5; stable closure maps to Task 6. +- The public signature, nullability, ordering, zero-byte rule, dependency floors, and non-goals are consistent with the design specification. +- No placeholder implementation step or unbounded feature family remains. diff --git a/docs/superpowers/specs/2026-09-18-1.18.0-rendition-baseline-design.md b/docs/superpowers/specs/2026-09-18-1.18.0-rendition-baseline-design.md new file mode 100644 index 000000000..f982bd1a7 --- /dev/null +++ b/docs/superpowers/specs/2026-09-18-1.18.0-rendition-baseline-design.md @@ -0,0 +1,78 @@ +# Icod.Terminal 1.18.0 Rendition Baseline Design + +**Release:** `1.18.0` +**Theme:** safe recovery from unknown physical rendition state +**Stable compatibility floor:** `1.0.0` +**Downstream objective:** unblock the TermInfo-free `Icod.DCurses 2.0` rendering path + +## Goal + +Add one Terminal-owned semantic operation that establishes a known default rendition when the caller cannot truthfully describe the terminal's current physical attributes or colors. + +This is the missing recovery primitive discovered by the DCurses T2001 readiness witness. The existing `PlanRenditionReset(current)` is intentionally insufficient because its safety and emitted bytes depend on a caller-supplied known current state. + +## Public API + +Add to `TerminalScreenPlanner`: + +```csharp +public TerminalScreenOperationPlan? PlanRenditionBaseline(); +``` + +No parameter is accepted because the source state is explicitly unknown. The return remains the existing opaque, session-bound `TerminalScreenOperationPlan`. + +## Safety invariant + +For the selected terminal profile, define a rendition axis as exposed when capability evidence says Terminal can enter that attribute or select that color axis. Compute this evidence before the current normalization path suppresses non-reversible requests. `PlanRenditionBaseline()` is available only if Terminal can restore every exposed axis without knowing which axes are currently active. + +Consequences: + +- If any text attribute is enterable, it must have either global `ExitAttributeMode` coverage or an unconditional specific exit. When no global reset exists, Terminal emits every required specific exit in stable order so the result does not depend on which attributes were active. +- If either foreground or background color is selectable, `OriginalColorPair` is required. +- If both classes are reachable, the plan emits the global reset or complete specific-exit sequence first and original-color restoration second. +- If neither class is exposed, a valid zero-byte rendition plan is returned. Reset capabilities by themselves do not make state reachable. +- If one or more exposed classes cannot be restored, the result is `null`; Terminal never returns a partial baseline. + +The conservative rule is based on the same profile entry/selection evidence Terminal interprets for semantic screen capabilities, not on arbitrary strings present in the underlying terminal description. It deliberately does not use the existing reversible-normalization subset as its input: that would hide exposed but non-restorable state and incorrectly permit an empty baseline. + +## Planning and commitment + +Planning is side-effect free. It performs no output, flush, output-epoch mutation, lifecycle mutation, or retained physical-state update. + +Terminal continues to own: + +- capability lookup and interpretation; +- terminfo parameter expansion and padding removal; +- output segment construction and exact byte cost; +- stable segment order and affected-line accounting; +- plan ownership and same-session validation. + +The plan uses `TerminalScreenOperationKind.Rendition` and `AffectedLines == 1`. Commitment uses the existing `TerminalScreenOutputTransaction`; foreign plans, stale transactions, cancellation, serialization, and failure behavior are unchanged. + +## Compatibility + +The change is additive and therefore a minor 1.x release. Existing `PlanRenditionTransition(...)`, `PlanRenditionReset(current)`, normalization, profile, operation-plan, and transaction behavior remain unchanged. The public signature contains no `Icod.TermInfo` type. + +Production dependencies remain `Icod.TermInfo 1.15.0` and `Icod.Timing 1.0.0` unless separate evidence justifies a reviewed dependency update. + +## Tests + +The implementation requires red-green coverage for: + +- combined `` ordering and exact byte count on a profile that also advertises representative attribute entry and foreground/background selection; +- attribute-only and color-only profiles; +- a multi-attribute profile with no global reset emitting the complete stable specific-exit sequence; +- empty and reset-only profiles returning a zero-byte plan; +- enterable attributes without global reset returning `null`; +- selectable colors without original-color restoration returning `null`; +- padding-sensitive cost and one affected line; +- repeated side-effect-free planning; +- same-session commit and foreign-session rejection; +- package API/XML snapshots across all target frameworks; +- a local future-DCurses compile/package consumer with no direct TermInfo input; +- the external DCurses T2001 exact-byte witness against the candidate package, permitting its documented test-only TermInfo fixture exception while keeping the production boundary Terminal-only; +- published DCurses 1.6 compatibility. + +## Non-goals + +This release does not track physical rendition state, infer an unknown state from prior writes, weaken operation-plan opacity, expose raw capabilities, redesign transactions, or move retained presentation policy into Terminal. It adds no unrelated feature family. diff --git a/packaging/VerifyPublicApiBaseline.ps1 b/packaging/VerifyPublicApiBaseline.ps1 index d064356bc..6964be3fb 100644 --- a/packaging/VerifyPublicApiBaseline.ps1 +++ b/packaging/VerifyPublicApiBaseline.ps1 @@ -1,7 +1,7 @@ param( [string]$Configuration = 'Staging', [string]$OutputDirectory = 'artifacts/public-api', - [string]$BaselinePath = 'docs/Public-API-Baseline-1.17.sha256' + [string]$BaselinePath = 'docs/Public-API-Baseline-1.18.sha256' ) Set-StrictMode -Version Latest diff --git a/src/Screen/TerminalScreenPlanner.cs b/src/Screen/TerminalScreenPlanner.cs index 6455acea4..ea3e02cf7 100644 --- a/src/Screen/TerminalScreenPlanner.cs +++ b/src/Screen/TerminalScreenPlanner.cs @@ -174,6 +174,41 @@ TerminalScreenRendition requested ); } + /// Plans restoration from an unknown physical rendition state to Terminal's normalized default. + /// + /// A session-bound rendition plan, or when any rendition axis + /// exposed by the selected profile cannot be restored unconditionally. + /// + public TerminalScreenOperationPlan? PlanRenditionBaseline() { + TerminalTextAttributes attributes = this.Profile.Screen.SupportedAttributes; + bool colors = this.Profile.Screen.SupportsColor + && ( this.Profile.Screen.SupportsForegroundColor + || this.Profile.Screen.SupportsBackgroundColor ); + List segments = []; + if ( TerminalTextAttributes.None != attributes ) { + if ( !this.TryAddLiteral( segments, StringCapability.ExitAttributeMode, 1 ) ) { + if ( 0 != ( attributes & ( + TerminalTextAttributes.Bold + | TerminalTextAttributes.Dim + | TerminalTextAttributes.Reverse + | TerminalTextAttributes.Blink + | TerminalTextAttributes.Conceal + ) ) || !this.TryAddAttributeExits( segments, attributes ) ) { + return null; + } + } + } + if ( colors + && !this.TryAddLiteral( segments, StringCapability.OriginalColorPair, 1 ) ) { + return null; + } + return this.Create( + TerminalScreenOperationKind.Rendition, + segments, + 1 + ); + } + /// Plans a safe transition between normalized screen renditions. public TerminalScreenOperationPlan? PlanRenditionTransition( TerminalScreenRendition current, diff --git a/tests/Icod.Terminal.Tests/src/Packaging/DependencyCouplingPolicyTests.cs b/tests/Icod.Terminal.Tests/src/Packaging/DependencyCouplingPolicyTests.cs index ce599534e..f8e6acfa8 100644 --- a/tests/Icod.Terminal.Tests/src/Packaging/DependencyCouplingPolicyTests.cs +++ b/tests/Icod.Terminal.Tests/src/Packaging/DependencyCouplingPolicyTests.cs @@ -118,6 +118,13 @@ string configuration StringComparison.Ordinal ); } + Assert.Contains( + ownedSources, + acceptanceSource => File.ReadAllText( acceptanceSource ).Contains( + ".PlanRenditionBaseline()", + StringComparison.Ordinal + ) + ); foreach ( string targetFramework in new[] { "net8.0", "net9.0", "net10.0" } ) { using JsonDocument evaluation = EvaluateProjectItems( diff --git a/tests/Icod.Terminal.Tests/src/Screen/TerminalScreenOutputTransactionHardeningTests.cs b/tests/Icod.Terminal.Tests/src/Screen/TerminalScreenOutputTransactionHardeningTests.cs index a8c66f1c2..b45f7a825 100644 --- a/tests/Icod.Terminal.Tests/src/Screen/TerminalScreenOutputTransactionHardeningTests.cs +++ b/tests/Icod.Terminal.Tests/src/Screen/TerminalScreenOutputTransactionHardeningTests.cs @@ -162,6 +162,112 @@ public async Task TransactionRejectsDefaultAndForeignPlansWithoutMutation() { Assert.Equal( "owned"u8.ToArray(), secondOutput.GetCombinedWrites() ); } + [Fact] + public async Task RenditionBaselineSerializesWithAdjacentApplicationText() { + RecordingTerminalOutput output = new(); + await using TerminalSession session = await OpenSessionAsync( + output, + CreateRenditionBaselineTerminal( "owned-rendition-baseline" ) + ); + TerminalScreenOperationPlan plan = session.Screen.PlanRenditionBaseline() + ?? throw new InvalidOperationException(); + TerminalScreenOutputTransaction transaction = + session.CreateScreenOutputTransaction(); + + transaction.WriteText( "before" ); + transaction.Add( plan ); + transaction.WriteText( "after" ); + await transaction.CommitAsync(); + + Assert.Equal( + "beforeafter"u8.ToArray(), + output.GetCombinedWrites() + ); + Assert.Equal( 1, output.FlushAttemptCount ); + } + + [Fact] + public async Task ForeignRenditionBaselineIsRejectedBeforeAnyOutput() { + RecordingTerminalOutput firstOutput = new(); + RecordingTerminalOutput secondOutput = new(); + await using TerminalSession first = await OpenSessionAsync( + firstOutput, + CreateRenditionBaselineTerminal( "foreign-rendition-baseline" ) + ); + await using TerminalSession second = await OpenSessionAsync( + secondOutput, + CreateRenditionBaselineTerminal( "receiving-rendition-baseline" ) + ); + TerminalScreenOperationPlan foreign = first.Screen.PlanRenditionBaseline() + ?? throw new InvalidOperationException(); + TerminalScreenOutputTransaction transaction = + second.CreateScreenOutputTransaction(); + transaction.WriteText( "before" ); + + ArgumentException exception = Assert.Throws( + () => transaction.Add( foreign ) + ); + + Assert.Equal( "plan", exception.ParamName ); + Assert.Equal( 0, firstOutput.WriteAttemptCount ); + Assert.Equal( 0, firstOutput.FlushAttemptCount ); + Assert.Equal( 0, secondOutput.WriteAttemptCount ); + Assert.Equal( 0, secondOutput.FlushAttemptCount ); + } + + [Fact] + public async Task StaleRenditionBaselineTransactionEmitsNothing() { + RecordingTerminalOutput output = new(); + await using TerminalSession session = await OpenSessionAsync( + output, + CreateRenditionBaselineTerminal( "stale-rendition-baseline" ) + ); + TerminalScreenOutputTransaction stale = + session.CreateScreenOutputTransaction(); + stale.Add( + session.Screen.PlanRenditionBaseline() + ?? throw new InvalidOperationException() + ); + TerminalScreenOutputTransaction intervening = + session.CreateScreenOutputTransaction(); + await intervening.CommitAsync(); + + InvalidOperationException exception = await Assert.ThrowsAsync( + () => stale.CommitAsync().AsTask() + ); + + Assert.Equal( + "The screen-output transaction is stale because intervening session output occurred.", + exception.Message + ); + Assert.Equal( 0, output.WriteAttemptCount ); + Assert.Equal( 1, output.FlushAttemptCount ); + } + + [Fact] + public async Task PreCancelledRenditionBaselineTransactionEmitsNothing() { + RecordingTerminalOutput output = new(); + await using TerminalSession session = await OpenSessionAsync( + output, + CreateRenditionBaselineTerminal( "cancelled-rendition-baseline" ) + ); + TerminalScreenOutputTransaction transaction = + session.CreateScreenOutputTransaction(); + transaction.Add( + session.Screen.PlanRenditionBaseline() + ?? throw new InvalidOperationException() + ); + using CancellationTokenSource cancellation = new(); + cancellation.Cancel(); + + await Assert.ThrowsAnyAsync( + () => transaction.CommitAsync( cancellation.Token ).AsTask() + ); + + Assert.Equal( 0, output.WriteAttemptCount ); + Assert.Equal( 0, output.FlushAttemptCount ); + } + [Fact] public async Task Utf8PayloadBoundaryRejectionDoesNotPoisonLaterAdds() { RecordingTerminalOutput output = new(); @@ -581,7 +687,8 @@ public async Task IndependentPrimaryAndCleanupFailuresAreAggregatedInOrder() { } private static ValueTask OpenSessionAsync( - RecordingTerminalOutput output + RecordingTerminalOutput output, + TerminalDescription? terminalOverride = null ) { ArgumentNullException.ThrowIfNull( output ); TerminalDescription terminal = new TerminalDescriptionBuilder( "screen-output-hardening" ) @@ -594,13 +701,26 @@ RecordingTerminalOutput output new TestTerminalInput(), output, new TerminalSessionOptions { - TerminalOverride = terminal, + TerminalOverride = terminalOverride ?? terminal, ConfigureOutput = false, ObserveLifecycleEvents = false } ); } + private static TerminalDescription CreateRenditionBaselineTerminal( + string name + ) { + return new TerminalDescriptionBuilder( name ) + .SetNumber( NumericCapability.Colors, 16 ) + .SetString( StringCapability.EnterBoldMode, "" ) + .SetString( StringCapability.ExitAttributeMode, "" ) + .SetString( StringCapability.SetForegroundColor, "" ) + .SetString( StringCapability.SetBackgroundColor, "" ) + .SetString( StringCapability.OriginalColorPair, "" ) + .Build(); + } + private sealed class TestTerminalInput : ITerminalInput { public ValueTask ReadAsync( Memory buffer, diff --git a/tests/Icod.Terminal.Tests/src/Screen/TerminalScreenRenditionPlannerHardeningTests.cs b/tests/Icod.Terminal.Tests/src/Screen/TerminalScreenRenditionPlannerHardeningTests.cs index dcd5621ea..76da06fa9 100644 --- a/tests/Icod.Terminal.Tests/src/Screen/TerminalScreenRenditionPlannerHardeningTests.cs +++ b/tests/Icod.Terminal.Tests/src/Screen/TerminalScreenRenditionPlannerHardeningTests.cs @@ -21,12 +21,243 @@ You should have received a copy of the GNU General Public License namespace Icod.Terminal.Tests.Screen; using System.Text; +using System.Threading.Channels; using Icod.Terminal; using Icod.TermInfo; using Xunit; /// Hardens rendition normalization, transition, and reset planning. public sealed class TerminalScreenRenditionPlannerHardeningTests { + [Fact] + public async Task RenditionBaselinePlanIsOpaqueCostedAndExactlyEmittable() { + TerminalDescription terminal = new TerminalDescriptionBuilder( "rendition-baseline" ) + .SetNumber( NumericCapability.Colors, 16 ) + .SetString( StringCapability.EnterBoldMode, "" ) + .SetString( StringCapability.ExitAttributeMode, "" ) + .SetString( StringCapability.SetForegroundColor, "" ) + .SetString( StringCapability.SetBackgroundColor, "" ) + .SetString( StringCapability.OriginalColorPair, "" ) + .Build(); + RecordingTerminalOutput output = new(); + await using TerminalSession session = await OpenSessionAsync( output, terminal ); + + TerminalScreenOperationPlan plan = session.Screen.PlanRenditionBaseline() + ?? throw new InvalidOperationException(); + + Assert.Empty( output.Bytes ); + AssertRenditionPlan( plan, 10 ); + + await CommitAsync( session, plan ); + + Assert.Equal( Encoding.Latin1.GetBytes( "" ), output.Bytes ); + } + + [Fact] + public async Task RenditionBaselineResetsExposedAttributesOnly() { + TerminalDescription terminal = new TerminalDescriptionBuilder( "attribute-baseline" ) + .SetString( StringCapability.EnterBoldMode, "" ) + .SetString( StringCapability.ExitAttributeMode, "" ) + .Build(); + RecordingTerminalOutput output = new(); + await using TerminalSession session = await OpenSessionAsync( output, terminal ); + + TerminalScreenOperationPlan plan = session.Screen.PlanRenditionBaseline() + ?? throw new InvalidOperationException(); + + AssertRenditionPlan( plan, 6 ); + Assert.Empty( output.Bytes ); + + await CommitAsync( session, plan ); + + Assert.Equal( Encoding.Latin1.GetBytes( "" ), output.Bytes ); + } + + [Fact] + public async Task RenditionBaselineRestoresExposedColorsOnly() { + TerminalDescription terminal = new TerminalDescriptionBuilder( "color-baseline" ) + .SetNumber( NumericCapability.Colors, 16 ) + .SetString( StringCapability.SetForegroundColor, "" ) + .SetString( StringCapability.OriginalColorPair, "" ) + .Build(); + RecordingTerminalOutput output = new(); + await using TerminalSession session = await OpenSessionAsync( output, terminal ); + + TerminalScreenOperationPlan plan = session.Screen.PlanRenditionBaseline() + ?? throw new InvalidOperationException(); + + AssertRenditionPlan( plan, 4 ); + Assert.Empty( output.Bytes ); + + await CommitAsync( session, plan ); + + Assert.Equal( Encoding.Latin1.GetBytes( "" ), output.Bytes ); + } + + [Fact] + public async Task RenditionBaselineUsesEverySpecificAttributeExitInStableOrder() { + TerminalDescription terminal = new TerminalDescriptionBuilder( "specific-attribute-baseline" ) + .SetString( StringCapability.EnterUnderlineMode, "" ) + .SetString( StringCapability.ExitUnderlineMode, "" ) + .SetString( StringCapability.EnterStandoutMode, "" ) + .SetString( StringCapability.ExitStandoutMode, "" ) + .SetString( StringCapability.EnterItalicMode, "" ) + .SetString( StringCapability.ExitItalicMode, "" ) + .SetExtendedString( "smxx", "" ) + .SetExtendedString( "rmxx", "" ) + .Build(); + RecordingTerminalOutput output = new(); + await using TerminalSession session = await OpenSessionAsync( output, terminal ); + + TerminalScreenOperationPlan plan = session.Screen.PlanRenditionBaseline() + ?? throw new InvalidOperationException(); + + AssertRenditionPlan( plan, 12 ); + Assert.Empty( output.Bytes ); + + await CommitAsync( session, plan ); + + Assert.Equal( Encoding.Latin1.GetBytes( "" ), output.Bytes ); + } + + [Fact] + public async Task RenditionBaselineRejectsAnUnrestorableExposedAttribute() { + TerminalDescription terminal = new TerminalDescriptionBuilder( "unsafe-attribute-baseline" ) + .SetString( StringCapability.EnterBoldMode, "" ) + .Build(); + RecordingTerminalOutput output = new(); + await using TerminalSession session = await OpenSessionAsync( output, terminal ); + + Assert.Null( session.Screen.PlanRenditionBaseline() ); + Assert.Empty( output.Bytes ); + Assert.Equal( 0, output.FlushCount ); + } + + [Fact] + public async Task RenditionBaselineRejectsAnUnrestorableExposedColorAxis() { + TerminalDescription terminal = new TerminalDescriptionBuilder( "unsafe-color-baseline" ) + .SetNumber( NumericCapability.Colors, 16 ) + .SetString( StringCapability.SetForegroundColor, "" ) + .Build(); + RecordingTerminalOutput output = new(); + await using TerminalSession session = await OpenSessionAsync( output, terminal ); + + Assert.Null( session.Screen.PlanRenditionBaseline() ); + Assert.Empty( output.Bytes ); + Assert.Equal( 0, output.FlushCount ); + } + + [Theory] + [InlineData( false )] + [InlineData( true )] + public async Task RenditionBaselineAllowsZeroByteEmptyAndResetOnlyProfiles( + bool includeUnusedResets + ) { + TerminalDescriptionBuilder builder = new( "zero-byte-baseline" ); + if ( includeUnusedResets ) { + builder + .SetString( StringCapability.ExitAttributeMode, "" ) + .SetString( StringCapability.OriginalColorPair, "" ); + } + RecordingTerminalOutput output = new(); + await using TerminalSession session = await OpenSessionAsync( + output, + builder.Build() + ); + + TerminalScreenOperationPlan plan = session.Screen.PlanRenditionBaseline() + ?? throw new InvalidOperationException(); + + AssertRenditionPlan( plan, 0 ); + Assert.Empty( output.Bytes ); + + await CommitAsync( session, plan ); + + Assert.Empty( output.Bytes ); + Assert.Equal( 1, output.FlushCount ); + } + + [Fact] + public async Task RenditionBaselinePaddingUsesOneAffectedLineWithoutIncreasingCost() { + TerminalDescription terminal = new TerminalDescriptionBuilder( "padded-baseline" ) + .SetNumber( NumericCapability.Colors, 16 ) + .SetString( StringCapability.EnterBoldMode, "B" ) + .SetString( StringCapability.ExitAttributeMode, "A$<2*/>" ) + .SetString( StringCapability.SetForegroundColor, "F%p1%d" ) + .SetString( StringCapability.OriginalColorPair, "C$<3*/>" ) + .Build(); + RecordingDelayProvider delays = new(); + RecordingTerminalOutput output = new(); + await using TerminalSession session = await OpenSessionAsync( + output, + terminal, + delays + ); + + TerminalScreenOperationPlan plan = session.Screen.PlanRenditionBaseline() + ?? throw new InvalidOperationException(); + + AssertRenditionPlan( plan, 2 ); + Assert.Empty( output.Bytes ); + Assert.Empty( delays.Delays ); + Assert.Equal( 0, output.FlushCount ); + + await CommitAsync( session, plan ); + + Assert.Equal( Encoding.Latin1.GetBytes( "AC" ), output.Bytes ); + Assert.Equal( 1, output.FlushCount ); + Assert.Equal( 2, delays.Delays.Count ); + Assert.Equal( TimeSpan.FromMilliseconds( 2 ), delays.Delays[ 0 ].Duration ); + Assert.Equal( TimeSpan.FromMilliseconds( 3 ), delays.Delays[ 1 ].Duration ); + Assert.All( delays.Delays, delay => Assert.True( delay.IsMandatory ) ); + Assert.Equal( 0, delays.SynchronousDelayCount ); + Assert.Equal( 2, delays.AsynchronousDelayCount ); + } + + [Fact] + public async Task RepeatedRenditionBaselinePlanningIsDeterministicAndSideEffectFree() { + TerminalDescription terminal = new TerminalDescriptionBuilder( "repeat-baseline" ) + .SetNumber( NumericCapability.Colors, 16 ) + .SetString( StringCapability.EnterUnderlineMode, "U" ) + .SetString( StringCapability.ExitUnderlineMode, "u" ) + .SetString( StringCapability.SetBackgroundColor, "B%p1%d" ) + .SetString( StringCapability.OriginalColorPair, "O" ) + .Build(); + RecordingDelayProvider delays = new(); + RecordingLifecycleSource lifecycle = new(); + RecordingTerminalOutput output = new(); + await using TerminalSession session = await OpenSessionAsync( + output, + terminal, + delays, + lifecycle + ); + await lifecycle.WaitUntilReadAsync().WaitAsync( TimeSpan.FromSeconds( 5 ) ); + TerminalScreenOutputTransaction transaction = + session.CreateScreenOutputTransaction(); + + TerminalScreenOperationPlan first = session.Screen.PlanRenditionBaseline() + ?? throw new InvalidOperationException(); + TerminalScreenOperationPlan second = session.Screen.PlanRenditionBaseline() + ?? throw new InvalidOperationException(); + + Assert.Equal( first.Kind, second.Kind ); + Assert.Equal( first.ByteCount, second.ByteCount ); + Assert.Equal( first.AffectedLines, second.AffectedLines ); + AssertRenditionPlan( first, 2 ); + Assert.Empty( output.Bytes ); + Assert.Empty( delays.Delays ); + Assert.Equal( 0, output.FlushCount ); + Assert.Equal( 1, lifecycle.ReadCount ); + + transaction.Add( first ); + transaction.Add( second ); + await transaction.CommitAsync(); + + Assert.Equal( Encoding.Latin1.GetBytes( "uOuO" ), output.Bytes ); + Assert.Equal( 1, output.FlushCount ); + Assert.Equal( 1, lifecycle.ReadCount ); + } + [Fact] public async Task DirectRgbBlackInsideRetainedIndexedPrefixDegradesToDefault() { TerminalDescription terminal = CreateDirectColorBuilder( "direct-reserved-zero" ) @@ -677,7 +908,9 @@ string name private static ValueTask OpenSessionAsync( RecordingTerminalOutput output, - TerminalDescription terminal + TerminalDescription terminal, + ITermInfoDelayProvider? delayProvider = null, + ITerminalLifecycleSource? lifecycleSource = null ) { return TerminalSession.OpenAsync( new TestTerminalControlProvider(), @@ -687,11 +920,72 @@ TerminalDescription terminal output, new TerminalSessionOptions { TerminalOverride = terminal, - ObserveLifecycleEvents = false + ObserveLifecycleEvents = false, + LifecycleSource = lifecycleSource, + CapabilityPaddingMode = PaddingMode.Delay, + CapabilityDelayProvider = delayProvider } ); } + private sealed class RecordingLifecycleSource : ITerminalLifecycleSource { + private readonly Channel signals = + Channel.CreateUnbounded(); + private readonly TaskCompletionSource firstRead = new( + TaskCreationOptions.RunContinuationsAsynchronously + ); + + internal int ReadCount { + get; + private set; + } + + internal Task WaitUntilReadAsync() => this.firstRead.Task; + + public ValueTask ReadAsync( + CancellationToken cancellationToken = default + ) { + this.ReadCount++; + this.firstRead.TrySetResult(); + return this.signals.Reader.ReadAsync( cancellationToken ); + } + + public void Dispose() { + this.signals.Writer.TryComplete(); + } + } + + private sealed class RecordingDelayProvider : ITermInfoDelayProvider { + internal List Delays { + get; + } = []; + + internal int SynchronousDelayCount { + get; + private set; + } + + internal int AsynchronousDelayCount { + get; + private set; + } + + public void Delay( TermInfoDelay delay ) { + this.SynchronousDelayCount++; + this.Delays.Add( delay ); + } + + public ValueTask DelayAsync( + TermInfoDelay delay, + CancellationToken cancellationToken = default + ) { + cancellationToken.ThrowIfCancellationRequested(); + this.AsynchronousDelayCount++; + this.Delays.Add( delay ); + return ValueTask.CompletedTask; + } + } + private sealed class TestTerminalInput : ITerminalInput { public ValueTask ReadAsync( Memory buffer, @@ -707,6 +1001,11 @@ internal List Bytes { get; } = []; + internal int FlushCount { + get; + private set; + } + public ValueTask WriteAsync( ReadOnlyMemory buffer, CancellationToken cancellationToken = default @@ -720,6 +1019,7 @@ public ValueTask FlushAsync( CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); + this.FlushCount++; return ValueTask.CompletedTask; } } diff --git a/tools/dcurses-screen-contracts-acceptance/Source/Program.cs b/tools/dcurses-screen-contracts-acceptance/Source/Program.cs index 4c6e2b317..5ca0d37bb 100644 --- a/tools/dcurses-screen-contracts-acceptance/Source/Program.cs +++ b/tools/dcurses-screen-contracts-acceptance/Source/Program.cs @@ -84,6 +84,7 @@ internal static async ValueTask RefreshAsync( ) ?? throw new InvalidOperationException( "The selected terminal cannot position the retained-screen cursor." ); + TerminalScreenOperationPlan? renditionBaseline = planner.PlanRenditionBaseline(); TerminalScreenRendition requested = new( TerminalScreenColor.Indexed( 2 ), @@ -136,6 +137,7 @@ internal static async ValueTask RefreshAsync( } ); output.Add( cursor ); + AddIfSupported( output, renditionBaseline ); AddIfSupported( output, scrollRegion ); AddIfSupported( output, rendition ); AddIfSupported( output, erase ); diff --git a/tools/package-smoke/Program.cs b/tools/package-smoke/Program.cs index e54aed432..50418917d 100644 --- a/tools/package-smoke/Program.cs +++ b/tools/package-smoke/Program.cs @@ -128,12 +128,24 @@ string description && 0 < homePlan.ByteCount, "The package consumer received an invalid cursor plan." ); + TerminalScreenOperationPlan renditionBaseline = session.Screen + .PlanRenditionBaseline() + ?? throw new InvalidOperationException( + "The package consumer could not plan unknown-state rendition restoration." + ); + Require( + TerminalScreenOperationKind.Rendition == renditionBaseline.Kind + && 0 < renditionBaseline.ByteCount + && 1 == renditionBaseline.AffectedLines, + "The package consumer received an invalid rendition-baseline plan." + ); TerminalScreenOutputTransaction screenOutput = session.CreateScreenOutputTransaction( new TerminalScreenOutputTransactionOptions { UseSynchronizedOutput = true } ); + screenOutput.Add( renditionBaseline ); screenOutput.Add( homePlan ); screenOutput.WriteText( "screen-package-smoke" ); screenOutput.WriteHyperlink( "docs", "https://example.com/icod-terminal" ); diff --git a/tools/package-verifier/Program.cs b/tools/package-verifier/Program.cs index fe8ea509b..654c4a11c 100644 --- a/tools/package-verifier/Program.cs +++ b/tools/package-verifier/Program.cs @@ -30,6 +30,8 @@ namespace Icod.Terminal.PackageVerifier; internal static class Program { private const string PackageId = "Icod.Terminal"; private const string RepositoryUrl = "https://github.com/uniblab/Icod.Terminal"; + private const string RenditionBaselineDocumentationMember = + "M:Icod.Terminal.TerminalScreenPlanner.PlanRenditionBaseline"; private static readonly string[] TargetFrameworks = [ "net8.0", @@ -687,6 +689,47 @@ string targetFramework missingQueryDocumentation ) ); + + XElement? renditionBaseline = documentation + .Descendants() + .FirstOrDefault( + element => "member" == element.Name.LocalName + && RenditionBaselineDocumentationMember + == element.Attribute( "name" )?.Value + ); + Require( + renditionBaseline is not null, + $"{documentationPath} is missing rendition-baseline documentation." + ); + string renditionSummary = renditionBaseline! + .Elements() + .FirstOrDefault( element => "summary" == element.Name.LocalName ) + ?.Value + ?? string.Empty; + XElement? renditionReturns = renditionBaseline! + .Elements() + .FirstOrDefault( element => "returns" == element.Name.LocalName ); + Require( + renditionSummary.Contains( + "unknown physical rendition state", + StringComparison.Ordinal + ), + $"{documentationPath} does not document unknown-state baseline semantics." + ); + Require( + renditionReturns is not null + && renditionReturns.Value.Contains( + "cannot be restored unconditionally", + StringComparison.Ordinal + ) + && renditionReturns + .Descendants() + .Any( + element => "see" == element.Name.LocalName + && "null" == element.Attribute( "langword" )?.Value + ), + $"{documentationPath} does not document rendition-baseline nullability semantics." + ); } private static void VerifyAssemblyIdentity(