Skip to content

Commit 52c341b

Browse files
fsecada01claude
andauthored
fix(optimistic): apply optimistic UI patch client-side (closes #16) (#17)
* fix(optimistic): apply optimistic UI patch client-side (closes #16) The optimistic-UI protocol was implemented server-side and the client defined applyOptimistic(), but dispatch() never called it — the patch shipped in the same response as the full render, so the UI only ever reflected the authoritative server state after the round-trip. applyOptimistic was dead code and the docstrings promised instant feedback the runtime never delivered. Implements Option A from the issue (declarative client-side prediction): - dispatch() accepts an options.optimistic patch and applies it synchronously before the fetch, reconciling on success and rolling back on error. - Triggers declare predictions via data-optimistic='{...}' or the data-optimistic-toggle="field" shorthand; the bind handler computes the patch and passes it to dispatch(). - applyOptimistic() now also exposes data-optimistic and data-optimistic-<field> attributes as CSS styling hooks. - Ship component-framework.css with [data-loading] / [data-optimistic] default styles (reduced-motion aware). - Correct the docstrings on dispatch(), applyOptimistic(), get_optimistic_patch(), and the .d.ts to describe actual behavior. - Add node:test coverage (tests/js/optimistic.test.mjs) proving the prediction is applied before the response and reconciled/rolled back. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017hdmTV88wAXU8ew1rorMjt * fix(optimistic): address review — wire JS tests into CI, clear stale markers Review of #17 surfaced three gaps: - JS tests were orphaned: `just test` runs pytest only and CI had no Node step, so the suite never ran automatically. Add a `test-js` justfile recipe (+ `test-all`) and a `test-js` CI job (Node 22). - The documented run command was a bare directory, which Node 25 treats as a module path and fails; use a node-expanded glob instead, and fix the in-file run instructions (avoiding `*/` inside the block comment). - `update()` left `data-optimistic*` markers stuck on the element when the server returned empty HTML (element not replaced). Add `_clearOptimistic()` and call it on that early-return path; covered by a new test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017hdmTV88wAXU8ew1rorMjt * chore: normalize trailing whitespace and EOF across tracked files These files (spec-kit scaffolding, docs, an issue template) carried trailing-whitespace / missing-final-newline that the hand-rolled CI lint never checked. Running the pre-commit hooks via prek in CI surfaces them, so normalize once. Mechanical only — no content changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017hdmTV88wAXU8ew1rorMjt * ci(lint): run pinned hooks via prek; pin ty to a known-good version CI's lint job hand-rolled `ruff`/`ty` calls, which could (and did) diverge from local pre-commit. ty is pre-1.0 and a version after 0.0.25 changed its exit code to fail on warnings, turning the intentionally warn-level Django mixin/ORM diagnostics into a red build. - Pin `ty==0.0.25` in the dev deps so CI matches the local/working version that exits 0 on warn-level diagnostics; bump deliberately. - Replace the lint job's manual ruff/ty steps with `prek run --all-files` so CI runs the exact pinned hooks from .pre-commit-config.yaml (single source of truth with local pre-commit). Add `prek` to dev deps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017hdmTV88wAXU8ew1rorMjt * ci: use uv instead of pip (project standard) The CI jobs hand-rolled `pip install -e .[dev]`, but the project standardizes on uv with a committed uv.lock. Switch all Python jobs to `astral-sh/setup-uv` + `uv sync --extra dev --locked` + `uv run`, so CI installs the exact locked dependency set and matches local workflow. Refresh uv.lock for the pyproject changes (adds prek, pins ty==0.0.25). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017hdmTV88wAXU8ew1rorMjt * ci: install via uv pip --system (uv.lock is gitignored) uv.lock is gitignored, so `uv sync --locked` has no lockfile to resolve against in a clean CI checkout. Use `uv pip install --system -e .[dev]` instead — still uv (project standard), resolves fresh like a library, and needs no committed lock. Commands run directly on the runner's Python. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017hdmTV88wAXU8ew1rorMjt --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c4a8a80 commit 52c341b

19 files changed

Lines changed: 517 additions & 136 deletions

.github/ISSUE_TEMPLATE/rd_activity_log.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,3 @@ assignees: fsecada01
6868
- [ ] This work was NOT funded by a specific client contract
6969
- [ ] No client reimbursed these costs
7070
- [ ] This is internal IP owned by FJS Consulting Services LLC / Francis Secada
71-

.github/workflows/ci.yml

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,18 @@ jobs:
1414
- uses: actions/setup-python@v5
1515
with:
1616
python-version: "3.12"
17+
- name: Install uv
18+
uses: astral-sh/setup-uv@v5
19+
with:
20+
enable-cache: true
1721
- name: Install dependencies
18-
run: pip install -e ".[dev]"
19-
- name: Check formatting
20-
run: ruff format --check .
21-
- name: Lint
22-
run: ruff check .
23-
- name: Type check
24-
run: ty check src/
22+
# Project standardizes on uv. uv.lock is gitignored (library resolves
23+
# fresh), so install into the runner's Python rather than syncing a lock.
24+
run: uv pip install --system -e ".[dev]"
25+
- name: Run lint hooks (prek)
26+
# Single source of truth: CI runs the same pinned hooks as local
27+
# pre-commit (ruff, ruff-format, ty) defined in .pre-commit-config.yaml.
28+
run: prek run --all-files --show-diff-on-failure
2529

2630
test:
2731
runs-on: ubuntu-latest
@@ -34,7 +38,21 @@ jobs:
3438
- uses: actions/setup-python@v5
3539
with:
3640
python-version: ${{ matrix.python-version }}
41+
- name: Install uv
42+
uses: astral-sh/setup-uv@v5
43+
with:
44+
enable-cache: true
3745
- name: Install dependencies
38-
run: pip install -e ".[dev]"
46+
run: uv pip install --system -e ".[dev]"
3947
- name: Run tests
4048
run: pytest tests/ -q --tb=short
49+
50+
test-js:
51+
runs-on: ubuntu-latest
52+
steps:
53+
- uses: actions/checkout@v4
54+
- uses: actions/setup-node@v4
55+
with:
56+
node-version: "22"
57+
- name: Run client-side JS tests
58+
run: node --test "tests/js/**/*.test.mjs"

.specify/scripts/powershell/check-prerequisites.ps1

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ OPTIONS:
4242
EXAMPLES:
4343
# Check task prerequisites (plan.md required)
4444
.\check-prerequisites.ps1 -Json
45-
45+
4646
# Check implementation prerequisites (plan.md + tasks.md required)
4747
.\check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks
48-
48+
4949
# Get feature paths only (no validation)
5050
.\check-prerequisites.ps1 -PathsOnly
5151
@@ -59,8 +59,8 @@ EXAMPLES:
5959
# Get feature paths and validate branch
6060
$paths = Get-FeaturePathsEnv
6161

62-
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit:$paths.HAS_GIT)) {
63-
exit 1
62+
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit:$paths.HAS_GIT)) {
63+
exit 1
6464
}
6565

6666
# If paths-only mode, output paths and exit (support combined -Json -PathsOnly)
@@ -113,35 +113,35 @@ if (Test-Path $paths.RESEARCH) { $docs += 'research.md' }
113113
if (Test-Path $paths.DATA_MODEL) { $docs += 'data-model.md' }
114114

115115
# Check contracts directory (only if it exists and has files)
116-
if ((Test-Path $paths.CONTRACTS_DIR) -and (Get-ChildItem -Path $paths.CONTRACTS_DIR -ErrorAction SilentlyContinue | Select-Object -First 1)) {
117-
$docs += 'contracts/'
116+
if ((Test-Path $paths.CONTRACTS_DIR) -and (Get-ChildItem -Path $paths.CONTRACTS_DIR -ErrorAction SilentlyContinue | Select-Object -First 1)) {
117+
$docs += 'contracts/'
118118
}
119119

120120
if (Test-Path $paths.QUICKSTART) { $docs += 'quickstart.md' }
121121

122122
# Include tasks.md if requested and it exists
123-
if ($IncludeTasks -and (Test-Path $paths.TASKS)) {
124-
$docs += 'tasks.md'
123+
if ($IncludeTasks -and (Test-Path $paths.TASKS)) {
124+
$docs += 'tasks.md'
125125
}
126126

127127
# Output results
128128
if ($Json) {
129129
# JSON output
130-
[PSCustomObject]@{
130+
[PSCustomObject]@{
131131
FEATURE_DIR = $paths.FEATURE_DIR
132-
AVAILABLE_DOCS = $docs
132+
AVAILABLE_DOCS = $docs
133133
} | ConvertTo-Json -Compress
134134
} else {
135135
# Text output
136136
Write-Output "FEATURE_DIR:$($paths.FEATURE_DIR)"
137137
Write-Output "AVAILABLE_DOCS:"
138-
138+
139139
# Show status of each potential document
140140
Test-FileExists -Path $paths.RESEARCH -Description 'research.md' | Out-Null
141141
Test-FileExists -Path $paths.DATA_MODEL -Description 'data-model.md' | Out-Null
142142
Test-DirHasFiles -Path $paths.CONTRACTS_DIR -Description 'contracts/' | Out-Null
143143
Test-FileExists -Path $paths.QUICKSTART -Description 'quickstart.md' | Out-Null
144-
144+
145145
if ($IncludeTasks) {
146146
Test-FileExists -Path $paths.TASKS -Description 'tasks.md' | Out-Null
147147
}

.specify/scripts/powershell/common.ps1

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ function Get-RepoRoot {
1010
} catch {
1111
# Git command failed
1212
}
13-
13+
1414
# Fall back to script location for non-git repos
1515
return (Resolve-Path (Join-Path $PSScriptRoot "../../..")).Path
1616
}
@@ -20,7 +20,7 @@ function Get-CurrentBranch {
2020
if ($env:SPECIFY_FEATURE) {
2121
return $env:SPECIFY_FEATURE
2222
}
23-
23+
2424
# Then check git if available
2525
try {
2626
$result = git rev-parse --abbrev-ref HEAD 2>$null
@@ -30,15 +30,15 @@ function Get-CurrentBranch {
3030
} catch {
3131
# Git command failed
3232
}
33-
33+
3434
# For non-git repos, try to find the latest feature directory
3535
$repoRoot = Get-RepoRoot
3636
$specsDir = Join-Path $repoRoot "specs"
37-
37+
3838
if (Test-Path $specsDir) {
3939
$latestFeature = ""
4040
$highest = 0
41-
41+
4242
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
4343
if ($_.Name -match '^(\d{3})-') {
4444
$num = [int]$matches[1]
@@ -48,12 +48,12 @@ function Get-CurrentBranch {
4848
}
4949
}
5050
}
51-
51+
5252
if ($latestFeature) {
5353
return $latestFeature
5454
}
5555
}
56-
56+
5757
# Final fallback
5858
return "main"
5959
}
@@ -72,13 +72,13 @@ function Test-FeatureBranch {
7272
[string]$Branch,
7373
[bool]$HasGit = $true
7474
)
75-
75+
7676
# For non-git repos, we can't enforce branch naming but still provide output
7777
if (-not $HasGit) {
7878
Write-Warning "[specify] Warning: Git repository not detected; skipped branch validation"
7979
return $true
8080
}
81-
81+
8282
if ($Branch -notmatch '^[0-9]{3}-') {
8383
Write-Output "ERROR: Not on a feature branch. Current branch: $Branch"
8484
Write-Output "Feature branches should be named like: 001-feature-name"
@@ -97,7 +97,7 @@ function Get-FeaturePathsEnv {
9797
$currentBranch = Get-CurrentBranch
9898
$hasGit = Test-HasGit
9999
$featureDir = Get-FeatureDir -RepoRoot $repoRoot -Branch $currentBranch
100-
100+
101101
[PSCustomObject]@{
102102
REPO_ROOT = $repoRoot
103103
CURRENT_BRANCH = $currentBranch
@@ -134,4 +134,3 @@ function Test-DirHasFiles {
134134
return $false
135135
}
136136
}
137-

.specify/scripts/powershell/create-new-feature.ps1

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ function Find-RepositoryRoot {
6161

6262
function Get-HighestNumberFromSpecs {
6363
param([string]$SpecsDir)
64-
64+
6565
$highest = 0
6666
if (Test-Path $SpecsDir) {
6767
Get-ChildItem -Path $SpecsDir -Directory | ForEach-Object {
@@ -76,15 +76,15 @@ function Get-HighestNumberFromSpecs {
7676

7777
function Get-HighestNumberFromBranches {
7878
param()
79-
79+
8080
$highest = 0
8181
try {
8282
$branches = git branch -a 2>$null
8383
if ($LASTEXITCODE -eq 0) {
8484
foreach ($branch in $branches) {
8585
# Clean branch name: remove leading markers and remote prefixes
8686
$cleanBranch = $branch.Trim() -replace '^\*?\s+', '' -replace '^remotes/[^/]+/', ''
87-
87+
8888
# Extract feature number if branch matches pattern ###-*
8989
if ($cleanBranch -match '^(\d+)-') {
9090
$num = [int]$matches[1]
@@ -126,7 +126,7 @@ function Get-NextBranchNumber {
126126

127127
function ConvertTo-CleanBranchName {
128128
param([string]$Name)
129-
129+
130130
return $Name.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
131131
}
132132
$fallbackRoot = (Find-RepositoryRoot -StartDir $PSScriptRoot)
@@ -155,7 +155,7 @@ New-Item -ItemType Directory -Path $specsDir -Force | Out-Null
155155
# Function to generate branch name with stop word filtering and length filtering
156156
function Get-BranchName {
157157
param([string]$Description)
158-
158+
159159
# Common stop words to filter out
160160
$stopWords = @(
161161
'i', 'a', 'an', 'the', 'to', 'for', 'of', 'in', 'on', 'at', 'by', 'with', 'from',
@@ -164,17 +164,17 @@ function Get-BranchName {
164164
'this', 'that', 'these', 'those', 'my', 'your', 'our', 'their',
165165
'want', 'need', 'add', 'get', 'set'
166166
)
167-
167+
168168
# Convert to lowercase and extract words (alphanumeric only)
169169
$cleanName = $Description.ToLower() -replace '[^a-z0-9\s]', ' '
170170
$words = $cleanName -split '\s+' | Where-Object { $_ }
171-
171+
172172
# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
173173
$meaningfulWords = @()
174174
foreach ($word in $words) {
175175
# Skip stop words
176176
if ($stopWords -contains $word) { continue }
177-
177+
178178
# Keep words that are length >= 3 OR appear as uppercase in original (likely acronyms)
179179
if ($word.Length -ge 3) {
180180
$meaningfulWords += $word
@@ -183,7 +183,7 @@ function Get-BranchName {
183183
$meaningfulWords += $word
184184
}
185185
}
186-
186+
187187
# If we have meaningful words, use first 3-4 of them
188188
if ($meaningfulWords.Count -gt 0) {
189189
$maxWords = if ($meaningfulWords.Count -eq 4) { 4 } else { 3 }
@@ -227,15 +227,15 @@ if ($branchName.Length -gt $maxBranchLength) {
227227
# Calculate how much we need to trim from suffix
228228
# Account for: feature number (3) + hyphen (1) = 4 chars
229229
$maxSuffixLength = $maxBranchLength - 4
230-
230+
231231
# Truncate suffix
232232
$truncatedSuffix = $branchSuffix.Substring(0, [Math]::Min($branchSuffix.Length, $maxSuffixLength))
233233
# Remove trailing hyphen if truncation created one
234234
$truncatedSuffix = $truncatedSuffix -replace '-$', ''
235-
235+
236236
$originalBranchName = $branchName
237237
$branchName = "$featureNum-$truncatedSuffix"
238-
238+
239239
Write-Warning "[specify] Branch name exceeded GitHub's 244-byte limit"
240240
Write-Warning "[specify] Original: $originalBranchName ($($originalBranchName.Length) bytes)"
241241
Write-Warning "[specify] Truncated to: $branchName ($($branchName.Length) bytes)"
@@ -256,17 +256,17 @@ New-Item -ItemType Directory -Path $featureDir -Force | Out-Null
256256

257257
$template = Join-Path $repoRoot '.specify/templates/spec-template.md'
258258
$specFile = Join-Path $featureDir 'spec.md'
259-
if (Test-Path $template) {
260-
Copy-Item $template $specFile -Force
261-
} else {
262-
New-Item -ItemType File -Path $specFile | Out-Null
259+
if (Test-Path $template) {
260+
Copy-Item $template $specFile -Force
261+
} else {
262+
New-Item -ItemType File -Path $specFile | Out-Null
263263
}
264264

265265
# Set the SPECIFY_FEATURE environment variable for the current session
266266
$env:SPECIFY_FEATURE = $branchName
267267

268268
if ($Json) {
269-
$obj = [PSCustomObject]@{
269+
$obj = [PSCustomObject]@{
270270
BRANCH_NAME = $branchName
271271
SPEC_FILE = $specFile
272272
FEATURE_NUM = $featureNum
@@ -280,4 +280,3 @@ if ($Json) {
280280
Write-Output "HAS_GIT: $hasGit"
281281
Write-Output "SPECIFY_FEATURE environment variable set to: $branchName"
282282
}
283-

.specify/scripts/powershell/setup-plan.ps1

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,16 @@ if ($Help) {
2424
$paths = Get-FeaturePathsEnv
2525

2626
# Check if we're on a proper feature branch (only for git repos)
27-
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit $paths.HAS_GIT)) {
28-
exit 1
27+
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit $paths.HAS_GIT)) {
28+
exit 1
2929
}
3030

3131
# Ensure the feature directory exists
3232
New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null
3333

3434
# Copy plan template if it exists, otherwise note it or create empty file
3535
$template = Join-Path $paths.REPO_ROOT '.specify/templates/plan-template.md'
36-
if (Test-Path $template) {
36+
if (Test-Path $template) {
3737
Copy-Item $template $paths.IMPL_PLAN -Force
3838
Write-Output "Copied plan template to $($paths.IMPL_PLAN)"
3939
} else {
@@ -44,7 +44,7 @@ if (Test-Path $template) {
4444

4545
# Output results
4646
if ($Json) {
47-
$result = [PSCustomObject]@{
47+
$result = [PSCustomObject]@{
4848
FEATURE_SPEC = $paths.FEATURE_SPEC
4949
IMPL_PLAN = $paths.IMPL_PLAN
5050
SPECS_DIR = $paths.FEATURE_DIR

0 commit comments

Comments
 (0)