diff --git a/Shared/ScriptWidgetRuntime/Resource/Script.bundle/template/Activity Rings/main.jsx b/Shared/ScriptWidgetRuntime/Resource/Script.bundle/template/Activity Rings/main.jsx
index 353319b3..5ae2accf 100644
--- a/Shared/ScriptWidgetRuntime/Resource/Script.bundle/template/Activity Rings/main.jsx
+++ b/Shared/ScriptWidgetRuntime/Resource/Script.bundle/template/Activity Rings/main.jsx
@@ -1,97 +1,74 @@
-//
-// ScriptWidget
-// https://xnu.app/scriptwidget
-//
-// Activity Rings: three goal rings and a week of move calories.
-// Replace the sample values with your own (or $health data).
-//
-
+// Activity Rings · an offline design example with editable sample values.
+// These values are illustrative, not live Apple Health readings.
const size = $getenv("widget-size");
+const dark = $device.isdarkmode();
+const palette = {
+ background: dark ? "#172522" : "#F5FAF7",
+ text: dark ? "#F0F7F3" : "#203D32",
+ secondary: dark ? "#B0C5BA" : "#5C7568",
+ track: dark ? "#2B4138" : "#E0EBE4",
+};
const goals = [
- { name: "Move", value: 420, goal: 500, unit: "CAL", color: "#FA114F", track: "#3A0B18" },
- { name: "Exercise", value: 24, goal: 30, unit: "MIN", color: "#92E82A", track: "#20330A" },
- { name: "Stand", value: 9, goal: 12, unit: "HRS", color: "#1EEAEF", track: "#0A3334" },
+ { name: "Move", value: 420, goal: 500, unit: "cal", color: dark ? "#F2AC83" : "#AC582D" },
+ { name: "Exercise", value: 24, goal: 30, unit: "min", color: dark ? "#74C8A2" : "#287A59" },
+ { name: "Stand", value: 9, goal: 12, unit: "hr", color: dark ? "#8BBBE0" : "#3D739E" },
];
const week = [["M", 510], ["T", 380], ["W", 620], ["T", 450], ["F", 290], ["S", 540], ["S", 420]];
-
+const Header = () => (
+
+ Activity
+
+ Sample
+
+);
const Rings = ({ side, thickness }) => (
{goals.map((goal, index) => {
- const inset = index * (thickness + 2);
- return ;
+ const inset = index * (thickness + 3);
+ return ;
})}
);
-const Stat = ({ goal, font }) => (
-
- {goal.name}
- {goal.value}/{goal.goal}{goal.unit}
-
+const Stat = ({ goal }) => (
+
+
+ {goal.name}
+
+ {goal.value}
+ {goal.unit}
+
+);
+const Summary = () => (
+
+ {goals.map((goal) =>
+ {goal.value}{goal.unit}
+ {goal.name}
+ )}
+
);
const Week = () => (
-
- MOVE THIS WEEK
-
- {week.map(([, calories]) => = goals[0].goal ? "#FA114F" : "#5C1527"} />).flatMap((bar, index) => (index ? [, bar] : [bar]))}
+
+ MOVE THIS WEEK500 cal goal
+
+ {week.map(([, calories]) => = goals[0].goal ? goals[0].color : palette.track} />).flatMap((bar, index) => index ? [, bar] : [bar])}
- {week.map(([day]) => {day}).flatMap((label, index) => (index ? [, label] : [label]))}
+ {week.map(([day]) => {day}).flatMap((label, index) => index ? [, label] : [label])}
);
-
if (size === "accessoryCircular") {
$render({Math.round(100 * goals[0].value / goals[0].goal)}%);
} else if (size === "accessoryRectangular") {
- $render(
-
-
-
- {goals.map((goal) => {goal.value}/{goal.goal} {goal.unit})}
-
-
- );
+ $render({goals.map((goal) => {goal.value}/{goal.goal} {goal.unit})});
} else if (size === "accessoryInline") {
$render(Move {goals[0].value}/{goals[0].goal} · Exercise {goals[1].value} min);
} else if (size === "small") {
- $render(
-
-
-
- {goals.map((goal) => {goal.value})}
-
-
- );
+ $render({goals.map((goal) => {goal.value}{goal.unit})});
} else if (size === "medium") {
- $render(
-
-
-
- {goals.map((goal) => )}
-
-
- );
+ $render({goals.map((goal) => )});
} else if (size === "extraLarge") {
- $render(
-
-
-
- {goals.map((goal) => )}
-
-
-
- );
+ $render({goals.map((goal) => )});
} else {
- $render(
-
-
-
-
- {goals.map((goal) => )}
-
-
-
-
-
- );
+ $render();
}
diff --git a/Tests/ScriptWidgetRuntimeTests/RuntimeExecutionTests.swift b/Tests/ScriptWidgetRuntimeTests/RuntimeExecutionTests.swift
index bce4bdea..19dbe847 100644
--- a/Tests/ScriptWidgetRuntimeTests/RuntimeExecutionTests.swift
+++ b/Tests/ScriptWidgetRuntimeTests/RuntimeExecutionTests.swift
@@ -675,35 +675,21 @@ final class RuntimeExecutionTests: XCTestCase {
}
}
- /// CPU seconds this process consumes over `interval`.
- private func processCPUSeconds(over interval: TimeInterval) -> Double {
- func cpuSeconds() -> Double {
- var usage = rusage()
- getrusage(RUSAGE_SELF, &usage)
- func seconds(_ time: timeval) -> Double { Double(time.tv_sec) + Double(time.tv_usec) / 1_000_000 }
- return seconds(usage.ru_utime) + seconds(usage.ru_stime)
- }
- let before = cpuSeconds()
- Thread.sleep(forTimeInterval: interval)
- return cpuSeconds() - before
- }
-
/// Asserts the runaway script released its thread and its VM.
/// JavaScriptCore serialises entry into a VM, so while a loop spins a
/// follow-up execution on the same runtime blocks until it, too, hits the
/// execution limit. Runtimes are single-use, so the follow-up fails on
/// redeclaring util.js globals; reaching that error promptly proves the
- /// VM accepted new work.
+ /// VM accepted new work. Process-wide CPU sampling is not evidence about
+ /// this VM: compiler, WebKit and other hosted-test work can run concurrently.
private func assertRuntimeStopped(_ runtime: ScriptWidgetRuntime, file: StaticString = #filePath, line: UInt = #line) {
- let idleCPU = processCPUSeconds(over: 1)
- XCTAssertLessThan(idleCPU, 0.5, "a runaway script is still burning CPU", file: file, line: line)
-
let start = Date()
let (_, error) = runtime.executeJSXSyncForWidget("$render(after);")
XCTAssertLessThan(Date().timeIntervalSince(start), 3, "runtime VM still locked", file: file, line: line)
- if case .resourceLimit = error {
- XCTFail("runtime VM still busy: \(String(describing: error))", file: file, line: line)
+ guard case .scriptException(let message) = error else {
+ return XCTFail("Expected the same VM to report a JavaScript redeclaration, got \(String(describing: error))", file: file, line: line)
}
+ XCTAssertTrue(message.hasPrefix("SyntaxError:"), "Expected JavaScriptCore to accept follow-up work, got \(message)", file: file, line: line)
}
func testRunawaySynchronousLoopIsTerminatedAtTimeLimit() {
@@ -4401,6 +4387,26 @@ final class LibrarySessionExecutionTests: XCTestCase {
try prepareFunctionalTranspile(captured.source)
try prepareFunctionalTranspile(try XCTUnwrap(captured.package.readFile(relativePath:"lib/weather.js").0), wrapMain:false)
let runtime=ScriptWidgetRuntime(package:captured.package,environments:["widget-size":"medium"],executionSnapshot:captured)
+ // This test covers HTTP/cache persistence, not the simulator's
+ // location daemon. Even a manually configured city checks location
+ // availability so it can discard revoked location cache entries.
+ let locationChecked = expectation(description: "Weather uses isolated location availability")
+ runtime.validationHostInstaller = { context in
+ let unavailable: @convention(block) () -> Bool = {
+ locationChecked.fulfill()
+ return false
+ }
+ context["__weatherLocationUnavailable"] = unsafeBitCast(unavailable, to: JSValue.self)
+ context.evaluateScript("""
+ globalThis.$location = Object.freeze({
+ isAvailable: __weatherLocationUnavailable,
+ authorizationStatus: () => 'unavailable',
+ isAuthorizedForWidgetUpdates: () => false,
+ current: () => { throw new Error('Manual weather must not request location'); }
+ });
+ delete globalThis.__weatherLocationUnavailable;
+ """)
+ }
let configuration=URLSessionConfiguration.ephemeral;configuration.protocolClasses=[NativeIdentityURLProtocol.self]
let transport = ScriptWidgetFetchManager(configuration: configuration)
// Exercise persistence through the real bridge, without timing cold
@@ -4423,7 +4429,9 @@ final class LibrarySessionExecutionTests: XCTestCase {
}
runtime.runningState?.fetchManagerForTesting = transport
let result=runtime.executeJSXSyncForWidget(captured.source)
- XCTAssertNil(result.1,result.1?.displayMessage ?? "")
+ XCTAssertEqual(XCTWaiter.wait(for: [locationChecked], timeout: 0), .completed)
+ let trace = ScriptWidgetRuntimeTrace.recentSnapshots().last
+ XCTAssertNil(result.1, "\(result.1?.displayMessage ?? ""); HTTP calls: \(calls); phases: \(trace?.durations ?? [:]); counters: \(trace?.counters ?? [:])")
return text(try XCTUnwrap(result.0))
}
XCTAssertTrue(try execute(first).contains("18°C"));XCTAssertEqual(calls,1)
diff --git a/Tests/ScriptWidgetRuntimeTests/RuntimeUnitTests.swift b/Tests/ScriptWidgetRuntimeTests/RuntimeUnitTests.swift
index e6ac0990..282fc4e6 100644
--- a/Tests/ScriptWidgetRuntimeTests/RuntimeUnitTests.swift
+++ b/Tests/ScriptWidgetRuntimeTests/RuntimeUnitTests.swift
@@ -26,6 +26,7 @@ import ImageIO
import CryptoKit
import Darwin
import SwiftUI
+import UniformTypeIdentifiers
#if os(iOS)
import UIKit
#elseif os(macOS)
@@ -1512,6 +1513,22 @@ final class ScriptManagerLifecycleTests: XCTestCase {
XCTAssertEqual(manager.listScripts().map(\.name), ["Renamed Remix"])
}
+ func testExportedWidgetArchiveIsSelectableByZIPImporter() throws {
+ let manager = try makeManager("ArchiveFileType")
+ XCTAssertTrue(manager.createScript(content: "$render(Archive)",
+ recommendPackageName: "Archive", imageCopyPath: nil).0)
+ let model = try XCTUnwrap(manager.listScripts().first)
+ let ext = URL(fileURLWithPath: model.exportFileName).pathExtension
+ XCTAssertEqual(ext, "swt", "Keep existing exported archives compatible")
+ let declarations = try XCTUnwrap(Bundle.main.object(forInfoDictionaryKey: "UTExportedTypeDeclarations") as? [[String: Any]])
+ let archive = try XCTUnwrap(declarations.first { ($0["UTTypeIdentifier"] as? String) == "com.everettjf.scriptwidget.archive" })
+ XCTAssertEqual(archive["UTTypeConformsTo"] as? [String], [UTType.zip.identifier])
+ let tags = try XCTUnwrap(archive["UTTypeTagSpecification"] as? [String: Any])
+ XCTAssertEqual(tags["public.filename-extension"] as? [String], [ext])
+ let type = try XCTUnwrap(UTType(filenameExtension: ext))
+ XCTAssertTrue(type.conforms(to: .zip), "The system ZIP picker must accept the app's own .swt export")
+ }
+
func testExportImportRoundTripDeduplicatesWithoutOverwrite() throws {
let source = try makeManager("ExportSource")
XCTAssertTrue(source.createScript(
diff --git a/Web/preview-engine/test/parity/native-trees.json b/Web/preview-engine/test/parity/native-trees.json
index c5d7b95e..f754e828 100644
--- a/Web/preview-engine/test/parity/native-trees.json
+++ b/Web/preview-engine/test/parity/native-trees.json
@@ -214,8 +214,8 @@
"gallery/weather-now": {
"tree": "{\"$element\":\"vstack\",\"props\":{\"background\":\"#eff6ff\",\"frame\":\"max\",\"padding\":\"14\",\"spacing\":\"7\"},\"children\":[{\"$element\":\"icon\",\"props\":{\"color\":\"#3b82f6\",\"size\":\"26\",\"systemName\":\"cloud\"},\"children\":[]},{\"$element\":\"text\",\"props\":{\"color\":\"#1e3a8a\",\"font\":\"headline\"},\"children\":[\"Choose a saved city\"]},{\"$element\":\"text\",\"props\":{\"color\":\"#475569\",\"font\":\"caption2\"},\"children\":[\"Open Records & Configuration. Location is optional.\"]}]}"
},
- "template/Activity Rings": {
- "tree": "{\"$element\":\"hstack\",\"props\":{\"background\":\"black\",\"padding\":\"16\",\"spacing\":\"18\"},\"children\":[{\"$element\":\"zstack\",\"props\":{\"frame\":\"112,112\"},\"children\":[[{\"$element\":\"ring\",\"props\":{\"color\":\"#FA114F\",\"frame\":\"112,112\",\"thickness\":14,\"trackColor\":\"#3A0B18\",\"value\":0.84},\"children\":[]},{\"$element\":\"ring\",\"props\":{\"color\":\"#92E82A\",\"frame\":\"80,80\",\"thickness\":14,\"trackColor\":\"#20330A\",\"value\":0.8},\"children\":[]},{\"$element\":\"ring\",\"props\":{\"color\":\"#1EEAEF\",\"frame\":\"48,48\",\"thickness\":14,\"trackColor\":\"#0A3334\",\"value\":0.75},\"children\":[]}]]},{\"$element\":\"vstack\",\"props\":{\"alignment\":\"leading\",\"frame\":\"max,leading\",\"spacing\":\"6\"},\"children\":[[{\"$element\":\"vstack\",\"props\":{\"alignment\":\"leading\",\"spacing\":\"0\"},\"children\":[{\"$element\":\"text\",\"props\":{\"color\":\"white\",\"font\":\"caption2\",\"fontWeight\":\"semibold\"},\"children\":[\"Move\"]},{\"$element\":\"hstack\",\"props\":{\"alignment\":\"bottom\",\"spacing\":\"3\"},\"children\":[{\"$element\":\"text\",\"props\":{\"color\":\"#FA114F\",\"font\":\"title3\",\"fontWeight\":\"semibold\"},\"children\":[420,\"/\",500]},{\"$element\":\"text\",\"props\":{\"color\":\"#FA114F\",\"font\":\"caption\",\"fontWeight\":\"semibold\"},\"children\":[\"CAL\"]}]}]},{\"$element\":\"vstack\",\"props\":{\"alignment\":\"leading\",\"spacing\":\"0\"},\"children\":[{\"$element\":\"text\",\"props\":{\"color\":\"white\",\"font\":\"caption2\",\"fontWeight\":\"semibold\"},\"children\":[\"Exercise\"]},{\"$element\":\"hstack\",\"props\":{\"alignment\":\"bottom\",\"spacing\":\"3\"},\"children\":[{\"$element\":\"text\",\"props\":{\"color\":\"#92E82A\",\"font\":\"title3\",\"fontWeight\":\"semibold\"},\"children\":[24,\"/\",30]},{\"$element\":\"text\",\"props\":{\"color\":\"#92E82A\",\"font\":\"caption\",\"fontWeight\":\"semibold\"},\"children\":[\"MIN\"]}]}]},{\"$element\":\"vstack\",\"props\":{\"alignment\":\"leading\",\"spacing\":\"0\"},\"children\":[{\"$element\":\"text\",\"props\":{\"color\":\"white\",\"font\":\"caption2\",\"fontWeight\":\"semibold\"},\"children\":[\"Stand\"]},{\"$element\":\"hstack\",\"props\":{\"alignment\":\"bottom\",\"spacing\":\"3\"},\"children\":[{\"$element\":\"text\",\"props\":{\"color\":\"#1EEAEF\",\"font\":\"title3\",\"fontWeight\":\"semibold\"},\"children\":[9,\"/\",12]},{\"$element\":\"text\",\"props\":{\"color\":\"#1EEAEF\",\"font\":\"caption\",\"fontWeight\":\"semibold\"},\"children\":[\"HRS\"]}]}]}]]}]}"
+ "template/Activity Rings" : {
+ "tree" : "{\"$element\":\"vstack\",\"props\":{\"background\":\"#F5FAF7\",\"frame\":\"max\",\"padding\":\"16\",\"spacing\":\"10\"},\"children\":[{\"$element\":\"hstack\",\"props\":{\"frame\":\"max,14\",\"spacing\":\"4\"},\"children\":[{\"$element\":\"text\",\"props\":{\"color\":\"#203D32\",\"font\":\"caption\",\"fontWeight\":\"semibold\"},\"children\":[\"Activity\"]},{\"$element\":\"spacer\",\"props\":null,\"children\":[]},{\"$element\":\"text\",\"props\":{\"color\":\"#5C7568\",\"font\":\"caption2\"},\"children\":[\"Sample\"]}]},{\"$element\":\"hstack\",\"props\":{\"frame\":\"max,96\",\"spacing\":\"20\"},\"children\":[{\"$element\":\"zstack\",\"props\":{\"frame\":\"96,96\"},\"children\":[[{\"$element\":\"ring\",\"props\":{\"color\":\"#AC582D\",\"frame\":\"96,96\",\"thickness\":10,\"trackColor\":\"#E0EBE4\",\"value\":0.84},\"children\":[]},{\"$element\":\"ring\",\"props\":{\"color\":\"#287A59\",\"frame\":\"70,70\",\"thickness\":10,\"trackColor\":\"#E0EBE4\",\"value\":0.8},\"children\":[]},{\"$element\":\"ring\",\"props\":{\"color\":\"#3D739E\",\"frame\":\"44,44\",\"thickness\":10,\"trackColor\":\"#E0EBE4\",\"value\":0.75},\"children\":[]}]]},{\"$element\":\"vstack\",\"props\":{\"frame\":\"max,96,leading\",\"spacing\":\"10\"},\"children\":[[{\"$element\":\"hstack\",\"props\":{\"frame\":\"max,20\",\"spacing\":\"5\"},\"children\":[{\"$element\":\"circle\",\"props\":{\"color\":\"#AC582D\",\"frame\":\"5,5\"},\"children\":[]},{\"$element\":\"text\",\"props\":{\"color\":\"#5C7568\",\"font\":\"caption\"},\"children\":[\"Move\"]},{\"$element\":\"spacer\",\"props\":null,\"children\":[]},{\"$element\":\"text\",\"props\":{\"color\":\"#203D32\",\"font\":\"headline\",\"fontWeight\":\"semibold\"},\"children\":[420]},{\"$element\":\"text\",\"props\":{\"color\":\"#5C7568\",\"font\":\"caption2\"},\"children\":[\"cal\"]}]},{\"$element\":\"hstack\",\"props\":{\"frame\":\"max,20\",\"spacing\":\"5\"},\"children\":[{\"$element\":\"circle\",\"props\":{\"color\":\"#287A59\",\"frame\":\"5,5\"},\"children\":[]},{\"$element\":\"text\",\"props\":{\"color\":\"#5C7568\",\"font\":\"caption\"},\"children\":[\"Exercise\"]},{\"$element\":\"spacer\",\"props\":null,\"children\":[]},{\"$element\":\"text\",\"props\":{\"color\":\"#203D32\",\"font\":\"headline\",\"fontWeight\":\"semibold\"},\"children\":[24]},{\"$element\":\"text\",\"props\":{\"color\":\"#5C7568\",\"font\":\"caption2\"},\"children\":[\"min\"]}]},{\"$element\":\"hstack\",\"props\":{\"frame\":\"max,20\",\"spacing\":\"5\"},\"children\":[{\"$element\":\"circle\",\"props\":{\"color\":\"#3D739E\",\"frame\":\"5,5\"},\"children\":[]},{\"$element\":\"text\",\"props\":{\"color\":\"#5C7568\",\"font\":\"caption\"},\"children\":[\"Stand\"]},{\"$element\":\"spacer\",\"props\":null,\"children\":[]},{\"$element\":\"text\",\"props\":{\"color\":\"#203D32\",\"font\":\"headline\",\"fontWeight\":\"semibold\"},\"children\":[9]},{\"$element\":\"text\",\"props\":{\"color\":\"#5C7568\",\"font\":\"caption2\"},\"children\":[\"hr\"]}]}]]}]}]}"
},
"template/Air Quality Now": {
"skipped": "live-data"
diff --git a/docs/icloud-core-qa-2026-10-25.md b/docs/icloud-core-qa-2026-10-25.md
new file mode 100644
index 00000000..4d4c1a75
--- /dev/null
+++ b/docs/icloud-core-qa-2026-10-25.md
@@ -0,0 +1,60 @@
+# iCloud core-flow acceptance — 2026-10-25
+
+After this QA, the user authorized preparing and submitting the 27.1.5 repair release (new build 27016). Record synchronization and automatic library migration remain deferred. This check used the installed TestFlight 27.1.5 (27015) on an iPhone 17 Pro running iOS 27.0.1, signed in to a real iCloud account. iCTRL 1.98.12 drove the UI over USB. No account sign-out, app uninstall, or deletion of existing user widgets was performed.
+
+## Physical-device results
+
+| Flow | Observed result |
+| --- | --- |
+| Open the existing iCloud library | Ready; the existing Activity Rings package remained listed |
+| Create Habit Heatmap from the bundled template | Created in the selected iCloud library; preview finished successfully |
+| Open nested resources | `lib/records.js` appeared in the package file list and was loaded by the running template |
+| Save configuration | A custom habit name persisted and appeared on the Home Screen |
+| Add and configure Home Screen widget | Edit Widget was available; the script selector listed the installed packages |
+| Render small, medium, and large widgets | All three sizes displayed the actual heatmap; size changes briefly showed a placeholder before rendering |
+| Real widget actions | Read and Run each changed from 0 to 1 day; their cells lit and their buttons changed to Done today |
+| Repeat an already completed action | History remained two entries, one per habit on the same date |
+| Terminate/reopen the app | The package, configuration and saved history remained available |
+| Rename a habit | The existing record remained associated with the habit ID and the new display name appeared |
+| Record backup and restore | Exported the test record JSON; reimport preview showed one record; confirmed restore and verified that record remained |
+| Switch iCloud → local → iCloud | Local initially empty; created Datetime Current locally; switching back restored the two iCloud packages and the heatmap history |
+| Create and rename an auxiliary package file | Created `qa-notes.txt`, renamed it to `qa-notes-final.txt`, and saw it in the saved package and subsequent preview |
+| Remix and delete the test copy | The copy started with History (0); deleting that copy left the original package and records intact after relaunch |
+| Export an individual package | Saved the test package as `Habit Heatmap.swt` in iCloud Downloads |
+| Import the exported package through the Files picker | **Failed in 27015:** `.swt` was not selectable because the importer accepts ZIP and the app did not declare the `.swt` ZIP subtype |
+
+The auxiliary file and Habit Heatmap test package are QA-created data. The local Datetime Current package is also QA-created. The pre-existing Activity Rings source was not edited on the phone. The phone was returned to the iCloud library with a medium Home Screen test widget.
+
+## Findings and bounded follow-up
+
+1. **Archive import:** register `com.everettjf.scriptwidget.archive`, with extension `swt` and conformance to `public.zip-archive`, on both native apps. Preserve the existing archive format, extraction preflight, limits and transaction logic. Add a regression check that the host's exported filename is accepted by the ZIP content-type filter. The installed phone build remains 27015 until a separate build is installed; do not describe the fixed picker as physically verified yet.
+2. **Duplicate library choices:** the chooser displayed the same system location using `/var/...` and `/private/var/...`. The chooser deduplicates raw path strings, whereas profile activation resolves canonical paths and directory receipts. This is a reproducible presentation issue; the tested switch-back preserved the existing records. Follow up with canonical, receipt-aware deduplication without changing stored identities or treating replacement directories as the same library.
+3. **Datetime Current small preview:** the built-in clock ran without an execution error, but its date/time text wrapped tightly in the small preview. Keep a separate layout follow-up; this does not demonstrate an iCloud failure.
+
+## Current behavior and recommended iCloud experience
+
+Current source behavior: a new library prefers available iCloud; an existing ready local selection stays local when iCloud later becomes available. Explicit selection switches independent libraries and does not move packages or records. `$storage` values remain local even when source files live in iCloud; see [the storage contract](storage-api.md).
+
+Recommended follow-up, not implemented in this task:
+
+- Offer a one-time migration prompt when iCloud becomes available to an existing local user. Provide “Move my widgets to iCloud” and “Keep using this device”. Never switch silently to an empty library.
+- Copy and verify package sources/assets together with the appropriate local configuration/record bindings, then switch the selection only after the complete operation succeeds. Preserve a recoverable local copy and support interruption recovery.
+- Preview conflicts and keep both copies by default. Do not overwrite same-name packages or merge record ownership by name.
+- Preserve or deliberately rebind Home Screen selections during migration, with deterministic regression coverage.
+- Explain separately that iCloud source-file sync does not currently sync `$storage` history between devices. Cross-device record synchronization needs its own contract and conflict policy.
+
+## Coverage limits and evidence
+
+This session does not prove cross-device convergence, iCloud account toggling, eviction/offline behavior, reinstall recovery, lock-screen widget behavior, Health/Calendar/Photos authorization, GitHub login, or all runtime APIs. Deterministic iCloud/cache and archive tests cover additional cases; these must not be called physical-device coverage.
+
+Raw timestamped iCTRL responses and private screenshots are retained locally under `/tmp/scriptwidget-iphone17-qa/` (a copy of the JSON evidence is under ignored `__Release/capabilities/iphone17-icloud-qa/`). They are not published because screenshots contain unrelated personal apps and Files listings. Core widget screenshots include `04-checkin-one-day.jpg`, `07-large-home-settled.jpg` and `08-small-home.jpg`. The failed picker capture is `09-swt-import-disabled.jpg`.
+
+Before the archive-type fix, the Activity Rings redesign passed the complete local readiness gate: macOS 713 passed / 0 failed / 32 skipped; iOS Simulator 719 passed / 0 failed / 31 skipped. Log: `/tmp/sw-rings-readiness.log`. Archive-type regression and final gate results are recorded separately after validation.
+
+CI-only stabilization at `bf632af` passed [Release Readiness run 40264311950](https://github.com/everettjf/scriptwidget/actions/runs/40264311950), including iPad/iCloud. This run predates the uncommitted visual and archive metadata changes and is not evidence for those changes.
+
+Archive-type focused verification: iOS Simulator passed both `testExportedWidgetArchiveIsSelectableByZIPImporter` and `testExportImportRoundTripDeduplicatesWithoutOverwrite` (2 passed, 0 failed), result bundle `Test-ScriptWidgetRuntimeTests-2026.10.25_21-29-59--0700.xcresult`. An earlier attempted baseline run stalled before launching the test host and was stopped; it is not counted as a product failure or a completed regression test. Restarting only the dedicated QA simulator allowed the focused run to complete. The real 27015 picker failure and the pre-fix system type lookup (`swt` was a dynamic type, not ZIP) are the before-fix evidence.
+
+At the end of device testing, the original heatmap still showed History (2) after deleting its QA remix and cold-relaunching. The phone was left in the iCloud Studio list, and the iCTRL server/USB forwarding session were stopped. The signed TestFlight app and account settings were not replaced.
+
+Final local release-readiness gate completed successfully (exit 0), including editor/Web/browser checks, native probes, both app builds and the shared runtime suites. macOS: 714 passed / 0 failed / 32 skipped (`Test-ScriptWidgetRuntimeTests-2026.10.25_21-32-27--0700.xcresult`). iOS Simulator: 720 passed / 0 failed / 31 skipped (`Test-ScriptWidgetRuntimeTests-2026.10.25_21-34-46--0700.xcresult`). Log: `/tmp/sw-icloud-qa-final-readiness.log`. Skipped checks remain skipped. `plutil -lint`, documentation-link checks and `git diff --check` also passed. This is local evidence for the current changes, not a new remote CI run or a new TestFlight upload.
diff --git a/docs/widget-capabilities-todo.md b/docs/widget-capabilities-todo.md
index db1b73f0..be76cb34 100644
--- a/docs/widget-capabilities-todo.md
+++ b/docs/widget-capabilities-todo.md
@@ -1,5 +1,16 @@
# Widget A/B TODO
+## 最新核心验收指令(2026-10-25)
+
+核心验收后,用户已要求发布 **27.1.5 修复版本(新构建 27016)**,替代此前暂不送审的指令。打卡记录 iCloud 同步与自动迁移本轮不开发。本次使用 iPhone 17 Pro 的 TestFlight 27.1.5 (27015),实际结果见 [验收与体验评估](icloud-core-qa-2026-10-25.md)。未执行的个人数据授权、跨设备同步及重装测试不得算作通过。
+
+- [ ] **IC01** `.swt` 文件类型登记已修复并通过双端全量回归;剩余:安装下一构建后补真机导出→导入验收。27015 真机已复现导出文件不可选。
+- [ ] **IC02** 资料库选择器消除 `/var` 与 `/private/var` 的同路径重复项;保持目录 receipt 和存储身份校验。
+- [ ] **IC03** 规划本地用户后来开启 iCloud 的一次性迁移提示、冲突预览、可恢复副本及桌面绑定保留;本轮不开发迁移功能。
+- [ ] **IC04** 跨设备记录同步独立设计;明确当前 iCloud 同步源文件,`$storage` 记录仍保存在本机。
+- [ ] **UI01** 后续整理 Datetime Current 小尺寸布局;本轮先优化 Activity Rings 的浅色/深色及尺寸适配,不覆盖已安装的用户源码。
+
+
更新:2026-10-23。用户已要求只收尾本次发版必须项,其余后移;此前要求 A+B 完整交付,移除 Spotify,取消 C/D。只维护当前 [A/B 规划](widget-capabilities-plan.md),不保留已取消方向的候选任务。
## 最新发布指令(2026-10-23)
diff --git a/iOS/ScriptWidget.xcodeproj/project.pbxproj b/iOS/ScriptWidget.xcodeproj/project.pbxproj
index 0ae9330b..1ecbdcfd 100644
--- a/iOS/ScriptWidget.xcodeproj/project.pbxproj
+++ b/iOS/ScriptWidget.xcodeproj/project.pbxproj
@@ -2472,7 +2472,7 @@
CODE_SIGN_ENTITLEMENTS = ScriptWidget/ScriptWidget.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 27015;
+ CURRENT_PROJECT_VERSION = 27016;
DEVELOPMENT_ASSET_PATHS = "\"ScriptWidget/Preview Content\"";
DEVELOPMENT_TEAM = YPV49M8592;
ENABLE_PREVIEWS = YES;
@@ -2505,7 +2505,7 @@
CODE_SIGN_ENTITLEMENTS = ScriptWidget/ScriptWidget.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 27015;
+ CURRENT_PROJECT_VERSION = 27016;
DEVELOPMENT_ASSET_PATHS = "\"ScriptWidget/Preview Content\"";
DEVELOPMENT_TEAM = YPV49M8592;
ENABLE_PREVIEWS = YES;
@@ -2533,7 +2533,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = ScriptWidgetWidget/ScriptWidgetWidget.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 27015;
+ CURRENT_PROJECT_VERSION = 27016;
DEVELOPMENT_TEAM = YPV49M8592;
INFOPLIST_FILE = ScriptWidgetWidget/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
@@ -2559,7 +2559,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = ScriptWidgetWidget/ScriptWidgetWidget.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 27015;
+ CURRENT_PROJECT_VERSION = 27016;
DEVELOPMENT_TEAM = YPV49M8592;
INFOPLIST_FILE = ScriptWidgetWidget/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
@@ -2584,7 +2584,7 @@
CODE_SIGN_ENTITLEMENTS = ScriptWidgetShare/ScriptWidgetShare.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 27015;
+ CURRENT_PROJECT_VERSION = 27016;
DEVELOPMENT_TEAM = YPV49M8592;
INFOPLIST_FILE = ScriptWidgetShare/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
@@ -2608,7 +2608,7 @@
CODE_SIGN_ENTITLEMENTS = ScriptWidgetShare/ScriptWidgetShare.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 27015;
+ CURRENT_PROJECT_VERSION = 27016;
DEVELOPMENT_TEAM = YPV49M8592;
INFOPLIST_FILE = ScriptWidgetShare/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
diff --git a/iOS/ScriptWidget/Info.plist b/iOS/ScriptWidget/Info.plist
index 07caa4ac..fdd42801 100644
--- a/iOS/ScriptWidget/Info.plist
+++ b/iOS/ScriptWidget/Info.plist
@@ -2,6 +2,22 @@
+ UTExportedTypeDeclarations
+
+
+ UTTypeIdentifier
+ com.everettjf.scriptwidget.archive
+ UTTypeDescription
+ ScriptWidget Archive
+ UTTypeConformsTo
+ public.zip-archive
+ UTTypeTagSpecification
+
+ public.filename-extension
+ swt
+
+
+
NSAppleMusicUsageDescription
Show the current Music app track and playback state in your widgets. ScriptWidget does not scan or change your music library.
ScriptWidgetServiceKeychainGroup
diff --git a/macOS/ScriptWidgetMac.xcodeproj/project.pbxproj b/macOS/ScriptWidgetMac.xcodeproj/project.pbxproj
index be552df9..c9dda1fc 100644
--- a/macOS/ScriptWidgetMac.xcodeproj/project.pbxproj
+++ b/macOS/ScriptWidgetMac.xcodeproj/project.pbxproj
@@ -1682,7 +1682,7 @@
CODE_SIGN_ENTITLEMENTS = ScriptWidgetMac/macOS.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 27015;
+ CURRENT_PROJECT_VERSION = 27016;
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readonly;
@@ -1720,7 +1720,7 @@
CODE_SIGN_ENTITLEMENTS = ScriptWidgetMac/macOS.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 27015;
+ CURRENT_PROJECT_VERSION = 27016;
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readonly;
@@ -1756,7 +1756,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = ScriptWidgetMacWidget/ScriptWidgetMacWidget.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 27015;
+ CURRENT_PROJECT_VERSION = 27016;
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readonly;
@@ -1790,7 +1790,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = ScriptWidgetMacWidget/ScriptWidgetMacWidget.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 27015;
+ CURRENT_PROJECT_VERSION = 27016;
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readonly;
diff --git a/macOS/ScriptWidgetMac/Info.plist b/macOS/ScriptWidgetMac/Info.plist
index c61fe6a9..3090b9d2 100644
--- a/macOS/ScriptWidgetMac/Info.plist
+++ b/macOS/ScriptWidgetMac/Info.plist
@@ -2,6 +2,22 @@
+ UTExportedTypeDeclarations
+
+
+ UTTypeIdentifier
+ com.everettjf.scriptwidget.archive
+ UTTypeDescription
+ ScriptWidget Archive
+ UTTypeConformsTo
+ public.zip-archive
+ UTTypeTagSpecification
+
+ public.filename-extension
+ swt
+
+
+
ScriptWidgetServiceKeychainGroup
$(AppIdentifierPrefix)com.everettjf.scriptwidget.services
NSPhotoLibraryUsageDescription