Prepare 0.5.0 quality milestone: updater, CSV safety, a11y, release gates

This commit is contained in:
h3p 2026-03-06 20:20:14 +01:00
parent fc05bed461
commit e1c0f1f617
7 changed files with 115 additions and 18 deletions

View file

@ -361,7 +361,7 @@
CODE_SIGNING_ALLOWED = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 403;
CURRENT_PROJECT_VERSION = 404;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;
@ -444,7 +444,7 @@
CODE_SIGNING_ALLOWED = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 403;
CURRENT_PROJECT_VERSION = 404;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;

View file

@ -855,17 +855,63 @@ final class AppUpdateManager: ObservableObject {
let stagedDir = root.appendingPathComponent("v\(safeVersion)-\(UUID().uuidString)", isDirectory: true)
try fm.createDirectory(at: stagedDir, withIntermediateDirectories: true)
let stagedAppURL = stagedDir.appendingPathComponent("Neon Vision Editor.app", isDirectory: true)
do {
if fm.fileExists(atPath: stagedAppURL.path) {
try fm.removeItem(at: stagedAppURL)
let expectedVersion = readBundleShortVersionString(of: appBundle)
var lastError: Error?
for attempt in 1...2 {
do {
if fm.fileExists(atPath: stagedAppURL.path) {
try fm.removeItem(at: stagedAppURL)
}
let (dittoStatus, dittoStderr) = runDittoCopy(from: appBundle, to: stagedAppURL)
if dittoStatus == 0 {
appendUpdaterLog("Staging via ditto succeeded (attempt \(attempt)).")
} else {
appendUpdaterLog("Staging via ditto failed (attempt \(attempt), exit \(dittoStatus)). \(dittoStderr)")
try fm.copyItem(at: appBundle, to: stagedAppURL)
appendUpdaterLog("Staging fallback via FileManager.copyItem succeeded (attempt \(attempt)).")
}
guard try verifyCodeSignatureStrictCLI(of: stagedAppURL) else {
throw UpdateError.installUnsupported("Staged app failed signature validation.")
}
if let expectedVersion {
let stagedVersion = readBundleShortVersionString(of: stagedAppURL)
guard stagedVersion == expectedVersion else {
throw UpdateError.installUnsupported("Staged app version mismatch.")
}
}
return stagedAppURL
} catch {
lastError = error
appendUpdaterLog("Staging attempt \(attempt) failed: \(error.localizedDescription)")
try? fm.removeItem(at: stagedAppURL)
}
try fm.copyItem(at: appBundle, to: stagedAppURL)
appendUpdaterLog("Staging via FileManager.copyItem succeeded.")
} catch {
appendUpdaterLog("Staging copy failed: \(error.localizedDescription). Source: \(appBundle.path)")
throw UpdateError.installUnsupported("Failed to stage downloaded app for background install.")
}
return stagedAppURL
appendUpdaterLog("Staging copy failed after retries. Source: \(appBundle.path)")
if let lastError {
throw lastError
}
throw UpdateError.installUnsupported("Failed to stage downloaded app for background install.")
}
private nonisolated static func runDittoCopy(from sourceURL: URL, to destinationURL: URL) -> (Int32, String) {
let process = Process()
let stderrPipe = Pipe()
process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto")
process.arguments = [sourceURL.path, destinationURL.path]
process.standardError = stderrPipe
do {
try process.run()
process.waitUntilExit()
let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile()
let stderrText = String(data: stderrData, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return (process.terminationStatus, stderrText)
} catch {
return (-1, error.localizedDescription)
}
}
private nonisolated static func writeInstallerScript(
@ -906,7 +952,10 @@ final class AppUpdateManager: ObservableObject {
done
/bin/rm -rf "$TMP"
/usr/bin/ditto "$SRC" "$TMP"
if ! /usr/bin/ditto "$SRC" "$TMP"; then
echo "ditto failed; trying fallback copy."
/bin/cp -R "$SRC" "$TMP"
fi
/bin/rm -rf "$OLD"
if [ -e "$DST" ]; then
/bin/mv "$DST" "$OLD"

View file

@ -60,6 +60,8 @@ struct AppUpdaterDialog: View {
appUpdateManager.openUpdaterLog()
}
.buttonStyle(.bordered)
.accessibilityLabel("Show installer log")
.accessibilityHint("Opens the updater log file for troubleshooting")
#endif
}
.padding(14)
@ -150,6 +152,9 @@ struct AppUpdaterDialog: View {
.font(.caption2)
.foregroundStyle(.secondary)
}
.accessibilityElement(children: .combine)
.accessibilityLabel("Update install progress")
.accessibilityValue("\(Int((appUpdateManager.installProgress * 100).rounded())) percent")
}
if let installMessage = appUpdateManager.installMessage {

View file

@ -215,6 +215,8 @@ extension ContentView {
Image(systemName: "plus.square.on.square")
}
.help("New Tab (Cmd+T)")
.accessibilityLabel("New tab")
.accessibilityHint("Creates a new editor tab")
.keyboardShortcut("t", modifiers: .command)
}
@ -224,6 +226,8 @@ extension ContentView {
Image(systemName: "gearshape")
}
.help("Settings (Cmd+,)")
.accessibilityLabel("Settings")
.accessibilityHint("Opens app settings")
.keyboardShortcut(",", modifiers: .command)
}
@ -253,6 +257,8 @@ extension ContentView {
}
.labelsHidden()
.help("Language")
.accessibilityLabel("Language picker")
.accessibilityHint("Choose syntax language for the current tab")
.frame(width: isIPadToolbarLayout ? 112 : iPhoneLanguagePickerWidth)
.fixedSize(horizontal: true, vertical: false)
.layoutPriority(2)
@ -309,6 +315,8 @@ extension ContentView {
Image(systemName: "folder")
}
.help("Open File… (Cmd+O)")
.accessibilityLabel("Open file")
.accessibilityHint("Opens a file picker")
.keyboardShortcut("o", modifiers: .command)
}
@ -328,6 +336,8 @@ extension ContentView {
}
.disabled(viewModel.selectedTab == nil)
.help("Save File (Cmd+S)")
.accessibilityLabel("Save file")
.accessibilityHint("Saves the current tab")
.keyboardShortcut("s", modifiers: .command)
}

View file

@ -14,7 +14,9 @@ private enum EditorRuntimeLimits {
// Above this, keep editing responsive by skipping regex-heavy syntax passes.
static let syntaxMinimalUTF16Length = 1_200_000
static let htmlFastProfileUTF16Length = 250_000
static let csvFastProfileUTF16Length = 180_000
static let csvFastProfileUTF16Length = 120_000
static let csvFastProfileLongLineUTF16 = 4_000
static let csvFastProfileScanLimitUTF16 = 120_000
static let scopeComputationMaxUTF16Length = 300_000
static let cursorRehighlightMaxUTF16Length = 220_000
static let nonImmediateHighlightMaxUTF16Length = 220_000
@ -22,6 +24,27 @@ private enum EditorRuntimeLimits {
static let bindingDebounceDelay: TimeInterval = 0.18
}
private func shouldUseCSVFastProfile(_ nsText: NSString) -> Bool {
if nsText.length >= EditorRuntimeLimits.csvFastProfileUTF16Length {
return true
}
let scanLimit = min(nsText.length, EditorRuntimeLimits.csvFastProfileScanLimitUTF16)
guard scanLimit > 0 else { return false }
var currentLineLength = 0
for idx in 0..<scanLimit {
let codeUnit = nsText.character(at: idx)
if codeUnit == 10 { // '\n'
currentLineLength = 0
continue
}
currentLineLength += 1
if currentLineLength >= EditorRuntimeLimits.csvFastProfileLongLineUTF16 {
return true
}
}
return false
}
struct EditorTextMutation {
let documentID: UUID
let range: NSRange
@ -2501,7 +2524,7 @@ struct CustomTextEditor: NSViewRepresentable {
if lower == "html" && nsText.length >= EditorRuntimeLimits.htmlFastProfileUTF16Length {
return .htmlFast
}
if lower == "csv" && nsText.length >= EditorRuntimeLimits.csvFastProfileUTF16Length {
if lower == "csv" && shouldUseCSVFastProfile(nsText) {
return .csvFast
}
return .full
@ -3726,7 +3749,7 @@ struct CustomTextEditor: UIViewRepresentable {
if lower == "html" && nsText.length >= EditorRuntimeLimits.htmlFastProfileUTF16Length {
return .htmlFast
}
if lower == "csv" && nsText.length >= EditorRuntimeLimits.csvFastProfileUTF16Length {
if lower == "csv" && shouldUseCSVFastProfile(nsText) {
return .csvFast
}
return .full

View file

@ -199,9 +199,12 @@ Availability legend: `Full` = complete support, `Partial` = available with platf
## Roadmap (Near Term)
- Improve iPad settings layout density and reduce scrolling friction. Tracking: [#12](https://github.com/h3pdesign/Neon-Vision-Editor/issues/12)
- Expand Markdown preview parity and interaction stability across Apple platforms. Tracking: [#13](https://github.com/h3pdesign/Neon-Vision-Editor/issues/13)
- Improve toolbar consistency and action discoverability across window sizes. Tracking: [#14](https://github.com/h3pdesign/Neon-Vision-Editor/issues/14)
- 0.5.0 milestone: quality + trust release (updater reliability, CSV safety, cross-platform polish, accessibility, release gating). Tracking: [Milestone 0.5.0](https://github.com/h3pdesign/Neon-Vision-Editor/milestone/1)
- Auto-update reliability hardening. Tracking: [#36](https://github.com/h3pdesign/Neon-Vision-Editor/issues/36)
- CSV/large-file safety and table-mode path. Tracking: [#25](https://github.com/h3pdesign/Neon-Vision-Editor/issues/25), [#26](https://github.com/h3pdesign/Neon-Vision-Editor/issues/26)
- Toolbar consistency and action discoverability across sizes. Tracking: [#14](https://github.com/h3pdesign/Neon-Vision-Editor/issues/14)
- Accessibility completion pass (VoiceOver + keyboard focus). Tracking: [#37](https://github.com/h3pdesign/Neon-Vision-Editor/issues/37)
- Release engineering lock-in checks for 0.5.0. Tracking: [#38](https://github.com/h3pdesign/Neon-Vision-Editor/issues/38)
## Known Issues

View file

@ -21,10 +21,17 @@ if grep -nE "^- TODO$" /tmp/release-notes-"$TAG".md >/dev/null; then
echo "CHANGELOG section for ${TAG} still contains TODO entries." >&2
exit 1
fi
if grep -nEi "\bTODO\b" /tmp/release-notes-"$TAG".md >/dev/null; then
echo "CHANGELOG section for ${TAG} still contains unresolved TODO markers." >&2
exit 1
fi
grep -nE "^> Latest release: \\*\\*${TAG}\\*\\*\\r?$" README.md >/dev/null
grep -nE "^- Latest release: \\*\\*${TAG}\\*\\*\\r?$" README.md >/dev/null
grep -nE "^### ${TAG} \\(summary\\)\\r?$" README.md >/dev/null
echo "Validating README download metrics freshness..."
scripts/update_download_metrics.py --check
SAFE_TAG="$(echo "$TAG" | tr -c 'A-Za-z0-9_' '_')"
WORK_DIR="/tmp/nve_release_preflight_${SAFE_TAG}"
rm -rf "$WORK_DIR"