Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/tooling-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ jobs:
- name: Lint multi-line comments
run: node tooling~/scripts/lint-multiline-comments.mjs

- name: Lint LINQ ban in production code
run: node tooling~/scripts/lint-linq-production.mjs

package-content:
# Clean-install guard for the UPM artifact (issue #22, PLAN.md T02
# "package-content validators"): packs the package exactly like npm/UPM
Expand Down
23 changes: 23 additions & 0 deletions .llm/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,29 @@ frontmatter validity, index freshness, and pointer-file delegation; see
must be one `/*` ... `*/` block instead. Single `//` lines and `///` doc comments stay
legal. Enforced by `npm --prefix tooling~ run lint:multiline-comments` (pre-commit + CI;
`:fix` converts runs, refusing content that contains the block-comment close).
22. No LINQ in production code (`Runtime/`, `Editor/`): every operator allocates
enumerators and closures, and several copy the whole sequence, so shipped code bans it
outright - no `using System.Linq`, no qualified `System.Linq.` calls, no static
`Enumerable.` calls. Write plain loops over the concrete collection type; reuse
caller-owned buffers (`List<T>` fill/`Clear` methods, `CopyTo`) instead of building
intermediate sequences. `List<T>.ToArray()`/`CopyTo` instance methods stay legal.
Tests and `Generator~` tooling are exempt. Enforced by
`npm --prefix tooling~ run lint:linq-production` (pre-commit + CI; no `:fix` by design).
23. Replacing LINQ is not enough - the loop must not re-introduce the allocation. Rules
that came out of the PR #60 allocation review:
- String assembly on repeated paths rents a builder:
`CachedStringBuilder.Rent(capacity)` / `Return(builder)` in `Runtime/Helper/`
(ThreadStatic, capacity retained). Never `new StringBuilder()` per call.
- Derived data drawn every `OnGUI`/editor tick is cached against its source
(reference + count stamp) and rebuilt only when the source changes - e.g. the
popup option arrays and font-key arrays in `TerminalUIEditor`. A fresh array or
list per frame is a regression even when the loop itself is allocation-free.
- Snapshot-then-mutate patterns (clear all variables while iterating a dictionary)
live on the owning type with a cached buffer field (`CommandShell.ClearVariables`),
not in command handlers that build throwaway lists.
- When converting LINQ, enumerate with `foreach` over the concrete type (struct
enumerator, bounds-check elision); keep counting loops only where the index is
genuinely used, per rule 11.

### Unity Package Rules

Expand Down
26 changes: 26 additions & 0 deletions .llm/skills/run-terminal-tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,32 @@ content before trusting a run: the host sync can lag, and stale assemblies
produce misleading failures. Check a canary (a log line, an assert message, or
a shifted line number in the failure stack) against the current file.

## UI test timing (frame-coupled reads)

`TerminalUI` applies programmatic value and caret writes through `RefreshUI` on
`LateUpdate`, and UI Toolkit applies them on its own schedule after that. In a
throttled or freshly initialized panel these passes can lag several frames, so
any assertion read one `yield return null` after `CompleteCommand` or a direct
field write is frame-coupled and flakes under session sequences
(issue #56). Rules:

- After `CompleteCommand` (or any code-driven field write), poll with the
bounded helpers in `TerminalUITokenCompletionTests` - `WaitForInput` /
`WaitForCaret` poll a few frames before asserting - instead of
`yield return null` + immediate read.
- An exhausted poll that still finds the queued position pending is legitimate
for unfocused panels; see the helper comments for what each fallback pins.
- Do not pin "consumed on frame N" behavior: the caret marker consumption
(`ApplyPendingCaret`) depends on real focus landing, which synthetic panels
may never do. Pin that logic synchronously by calling `ApplyPendingCaret`
directly (see `PendingCaretWritesAndKeepsMarkerWhileUnfocused`).
- One failure mode survives everything: long agent sessions can leave the
editor in a state where panel events stop processing entirely (writes
re-clamp or never land, for 30+ frames). It clears with a domain reload
(Assets > Refresh). If a previously green UI suite fails with stale values
across several consecutive runs, refresh first, then re-run before hunting a
code bug.

## Debugging failures

- Errors are queued on the terminal (not only the last one) - assert on the full error set where
Expand Down
14 changes: 14 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ repos:
Two or more consecutive comment-only '//' lines must be one '/*'...'*/' block
comment instead. Single '//' lines and '///' doc comments stay legal. Use
`--fix` to convert runs to block comments.
- id: linq-production-lint
name: Lint LINQ ban in production code (Runtime/, Editor/; PR #60 review)
entry: node tooling~/scripts/lint-linq-production.mjs
language: system
always_run: true
pass_filenames: false
stages:
- pre-commit
- pre-push
description: >
Every LINQ operator allocates enumerators and closures, so shipped code bans
it outright: 'using System.Linq', qualified 'System.Linq.' calls, and static
'Enumerable.' calls all fail under Runtime/ and Editor/. No --fix by design;
rewrite call sites as loops with caller-owned buffers.
- id: llm-instructions-lint
name: Lint .llm instructions (SKILL.md spec, index freshness, pointer delegation)
entry: pwsh -NoProfile -File tooling~/scripts/lint-llm-instructions.ps1
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- New shared `CommandTokenizer` for execution and completion, parity-pinned against `TryEatArgument` by a data-driven corpus.
- `CommandDefinition` commands with `AddToHistory = false` dispatch without rebuilding the history line.
- `CommandArgParsers`, a public static class exposing the culture-invariant parsers behind `CommandArg.TryGet`, one method per built-in type (`CommandArgParsers.Float`, `.Int`, `.DateTime`, ...), callable directly from command handlers and test code.
- Built-in argument parsing for `Bounds`, `BoundsInt`, `RectOffset`, `Plane`, and `Ray` (Unity) plus `System.Numerics.Complex`. Each type parses positional components separated by a single console delimiter — bounds `center x,y,z size x,y,z` (so `0,0,0,1,1,1` is a unit bounds at the origin), boundsInt `position x,y,z size x,y,z`, rectOffset in `RectOffset(left, right, top, bottom)` order, plane `normal x,y,z distance`, ray `origin x,y,z direction x,y,z`, and complex `real, imaginary` — and the Unity `ToString()` forms of bounds, boundsInt, plane, and ray (their `Center:`/`Extents:`/`Position:`/`Size:`/`normal:`/`distance:`/`Origin:`/`Dir:` labels are stripped; bounds extents are doubled to size). The composite parsers (`Vector2` through `RectInt`, `Color`, `Quaternion`) are now public methods on `CommandArgParsers`, so handlers and tests can call them directly.

### Changed

Expand Down
16 changes: 9 additions & 7 deletions Editor/CustomEditors/TerminalThemePackEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ namespace WallstopStudios.DxCommandTerminal.Editor.CustomEditors
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DxCommandTerminal.Helper;
using Extensions;
using Helper;
Expand Down Expand Up @@ -61,7 +60,7 @@ public override void OnInspectorGUI()
continue;
}
_styleCache.Add(theme);
if (!TerminalThemeStyleSheetHelper.GetAvailableThemes(theme).Any())
if (TerminalThemeStyleSheetHelper.GetAvailableThemes(theme).Length == 0)
{
_invalidStyles.Add(theme);
}
Expand All @@ -73,7 +72,7 @@ public override void OnInspectorGUI()
if (
anyInvalidTheme
|| _styleCache.Count != themePack._themes.Count
|| _invalidStyles.Any()
|| 0 < _invalidStyles.Count
)
{
if (GUILayout.Button("Fix Invalid Themes", _impactButtonStyle))
Expand Down Expand Up @@ -153,9 +152,12 @@ void SortThemes()
themePack._themes.SortByName();
themePack._themeNames ??= new List<string>();
themePack._themeNames.Clear();
themePack._themeNames.AddRange(
themePack._themes.SelectMany(TerminalThemeStyleSheetHelper.GetAvailableThemes)
);
foreach (StyleSheet theme in themePack._themes)
{
themePack._themeNames.AddRange(
TerminalThemeStyleSheetHelper.GetAvailableThemes(theme)
);
}
}

void UpdateFromDirectory(string directory)
Expand Down Expand Up @@ -188,7 +190,7 @@ void UpdateFromDirectory(string directory)
);
if (
styleSheet != null
&& TerminalThemeStyleSheetHelper.GetAvailableThemes(styleSheet).Any()
&& 0 < TerminalThemeStyleSheetHelper.GetAvailableThemes(styleSheet).Length
&& _styleCache.Add(styleSheet)
)
{
Expand Down
Loading