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
55 changes: 55 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/intent.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
# Intent: Polish and harden the Utter DMG installer

## Problem

The release DMG currently contains only `Utter.app` and an Applications
symlink. Finder chooses the window size, icon placement, and background, so the
installation flow looks unfinished and can vary with the packager's Finder
state. The existing path also has no Retina-aware installation artwork or
deterministic layout metadata.

## Outcome

Opening a release DMG shows a compact Utter-branded Finder window with the app
and Applications folder aligned around a clear drag arrow. Labels remain
readable in the user's current macOS appearance, and the same result is produced
locally and in release CI without scripting Finder.

## Scope

In scope: DMG background rendering, Finder window metadata, icon positions,
volume presentation, deterministic packaging dependencies, and the existing
`build-app.sh` DMG stage.

Out of scope: app runtime UI, application behavior, entitlements, signing
identity selection, notarization policy, release credentials, or installing the
app during validation.

## Constraints

- Preserve the current Swift Package and release-signing flow.
- Keep generated PNG/TIFF files, downloaded Python packages, `.app`, `.dmg`, and
screenshots out of Git.
- Use only macOS-compatible tooling available to local builders and GitHub macOS
runners; fail rather than silently ship an unstyled or unverifiable image.
- Treat local ad-hoc signing as packaging evidence, not public distribution
evidence.

## Acceptance criteria

- The real Finder window opens at 680 by 440 points with Utter and Applications
icons at the intended positions, a readable light background, and no clipped
installation copy.
- The background contains 1x and 2x representations and the DMG has a custom
volume icon.
- Packaging writes Finder layout metadata without launching or controlling
Finder.
- Packaging dependencies are version- and SHA-256-pinned and cached only below
`.build`.
- `verify-release-artifact.sh` and strict mounted-app code-signature validation
accept the locally generated artifact.

## Open questions

None. Human review still decides whether the new packaging dependency and
visual treatment are accepted for release.
33 changes: 33 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/plan.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
# Plan: Polish and harden the Utter DMG installer

## Work items

- [x] Audit the existing DMG stage and release workflow.
- [x] Render restrained 1x and 2x branded installation backgrounds.
- [x] Add deterministic Finder geometry, icon positions, Applications symlink,
and volume icon metadata without Finder automation.
- [x] Pin packaging dependencies by version and SHA-256 below the ignored build
cache.
- [x] Preserve strict mounted-app signature validity and the latest release
artifact verifier.
- [x] Re-run all repository, unit, release-build, artifact, and real-window
checks on the latest `origin/main` base.
- [x] Record final evidence and residual risk for review.

## Verification plan

- [x] `python3 scripts/sdlc.py validate --worktree`
- [x] `bash scripts/ci-basic-checks.sh`
- [x] `DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer swift test`
- [x] `DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer ./scripts/build-app.sh --version=0.0.0 --sign=-`
- [x] Inspect the mounted DMG, strict code signature, layout metadata, Retina
background representations, architecture, symlink, checksum, and real Finder
window.
- [x] `git diff --check`

## Human gates

An independent reviewer must accept the high-risk packaging/dependency change
and its rollback before merge. The protected release environment separately
owns the configured signing identity, Developer ID/notarization path, and public
release authorization.
62 changes: 62 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/spec.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
# Spec: Polish and harden the Utter DMG installer

## Context

`scripts/build-app.sh` already builds with Xcode, assembles `Utter.app`, compiles
appearance-aware app icons, signs the bundle, and verifies release artifacts.
Its previous DMG stage copied the app and Applications symlink into a staging
folder and asked `hdiutil` to compress that folder. It did not create a Finder
layout or installation background.

Finder automation can create `.DS_Store`, but it depends on interactive Finder
state and can be blocked by unrelated Finder dialogs. The packaging path must be
deterministic in both a developer session and GitHub Actions.

## Design

`generate-dmg-background.swift` renders a 680 by 440 AppKit bitmap at 1x and 2x.
The light neutral surface, subtle Utter color accents, centered installation
instruction, and arrow are positioned around the Finder icons rather than
duplicating them in the artwork.

`dmg-settings.py` owns the window rectangle, icon size, icon locations, HFS+
filesystem, UDZO compression, Applications symlink, and volume icon. The build
script installs `dmgbuild`, `ds-store`, and `mac-alias` into
`.build/xcode/dmg-tools` from `dmg-requirements.txt`. Every wheel is pinned by
version and SHA-256, and a versioned stamp avoids repeated installation.

The existing release verifier remains the final build step. No packaging code
changes signing identity selection, entitlements, notarization, or publication.

## Safety and failure modes

- A missing Python 3.10+ runtime, unavailable package index, hash mismatch, or
import failure stops the build; it does not fall back to an unstyled DMG.
- Downloaded code is isolated under ignored `.build` paths and never shipped in
`Utter.app`.
- Finder extension-hiding metadata is not written to `Utter.app`, because that
extended attribute makes strict code-signature verification fail.
- A corrupt tool cache fails during module import or DMG creation. Removing the
ignored cache allows a clean hash-verified reinstall.
- The packaging library currently emits macOS 26 deprecation warnings for its
internal `hdiutil` calls; artifact verification remains the acceptance gate.

## Test strategy

- Validate shell syntax, Python syntax, Swift background rendering, repository
policy, and the complete Swift test suite.
- Build a release-style ad-hoc app and DMG on the latest `main` base.
- Verify the DMG checksum, Applications symlink, mounted app signature, arm64
executable, 1x/2x TIFF representations, and exact `.DS_Store` geometry.
- Open the final image through Finder and inspect the actual title bar, artwork,
icon labels, spacing, arrow, and lower-edge clipping without installing or
launching the app.

## Rollout and rollback

Merge through the normal PR and release review path. The next release workflow
build exercises the same DMG stage with its configured signing identity and
artifact verifier. Stop if clean-runner dependency installation or mounted-DMG
verification fails. Roll back by reverting this change, which restores the
previous plain `hdiutil -srcfolder` image without changing app binaries or user
data.
28 changes: 28 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/state.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
{
"schemaVersion": 1,
"id": "2026-08-26-dmg-installer-polish",
"title": "Polish and harden the Utter DMG installer",
"risk": "high",
"status": "verified",
"owners": [
"repository maintainer"
],
"acceptanceCriteria": [
"Opening the DMG presents a compact branded Finder window with readable labels and an unambiguous drag-to-Applications flow.",
"The DMG layout is generated without depending on interactive Finder state and uses hash-pinned packaging tools.",
"The mounted app preserves its strict code-signature validity and the release artifact verifier accepts the generated DMG.",
"Generated build products, downloaded tools, and visual QA screenshots remain outside the committed change."
],
"governedPaths": [
"scripts/build-app.sh",
"scripts/dmg-requirements.txt",
"scripts/dmg-settings.py",
"scripts/generate-dmg-background.swift"
],
"artifacts": {
"intent": "intent.md",
"spec": "spec.md",
"plan": "plan.md",
"verification": "verification.md"
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
# Verification: Polish and harden the Utter DMG installer

## Evidence

The implementation was rebased onto `origin/main` at `7e0689a`. During the
implementation loop, a real Finder inspection caught two defects before PR
preparation: dark artwork made Finder labels hard to read, and hiding the
`.app` extension added FinderInfo that invalidated strict mounted-app signature
verification. Both were corrected before the latest-base verification run.

| Check | Result | Evidence |
|---|---|---|
| `python3 scripts/sdlc.py validate --worktree` | Pass | Verified bundle covers every governed changed path |
| `bash scripts/ci-basic-checks.sh` | Pass | SDLC, 13 harness tests, versioning, plist, localization, identifiers, lexicon, resources, conflicts, credentials, and symlink checks passed |
| `swift test` | Pass | 564 XCTest tests, 8 skipped, 0 failures; 1 Swift Testing test passed |
| Clean pinned tool install | Pass | `pip --require-hashes` installed `dmgbuild 1.6.7`, `ds-store 1.3.3`, and `mac-alias 2.2.3` into a new temporary directory |
| Release-style app and DMG build | Pass | `build-app.sh --version=0.0.0 --sign=-`; the built-in release artifact verifier passed |
| Mounted artifact verification | Pass | DMG checksum valid; strict deep code-signature verification passed; app is arm64; Applications resolves to `/Applications`; background contains two image representations |
| Finder layout metadata | Pass | 680 by 440 window; toolbar, sidebar, status bar, and path bar hidden; 112-point icons at `(174, 250)` and `(506, 250)`; background image enabled |
| Real Finder window | Pass with recapture limitation | The same final installer design was inspected in a real Finder window and showed readable, unclipped labels and a clear drag flow. A latest-base recapture attempt failed because ScreenCaptureKit could not start; final-artifact metadata and image representations were rechecked independently |
| `git diff --check` | Pass | No whitespace errors |

## Acceptance criteria

- Branded, readable, unclipped real Finder installation window — pass, with the
latest-base screenshot recapture limitation recorded above.
- Finder-independent, hash-pinned packaging — pass in clean temporary install
and repository checks.
- Strict mounted-app signature and release verifier — pass on the latest-base
release-style build.
- Generated products excluded from Git — pass in final worktree inspection.

## Residual risk

Independent high-risk review, configured release-identity verification, and any
Developer ID notarization run remain external/human gates. The packaging library
also emits non-fatal macOS 26 `hdiutil` deprecation warnings, and the latest-base
Finder screenshot should be spot-checked by the reviewer because the recapture
tool was unavailable.

## Decision

Ready for human review. No human approval is claimed here; approval remains a PR
review and protected-release decision.
47 changes: 33 additions & 14 deletions scripts/build-app.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@
# Requirements:
# - macOS with Xcode (full install, not just CLI tools)
# - Swift 6.0+
# - Python 3.10+ (DMG packaging only; pinned tools are cached under .build)
#

set -euo pipefail
Expand DownExpand Up@@ -65,6 +66,7 @@ done

APP_BUNDLE="${DIST_DIR}/${APP_NAME}.app"
DMG_PATH="${DIST_DIR}/${APP_NAME}-${VERSION}.dmg"
DMG_VOLUME_NAME="${APP_NAME} ${VERSION}"

# ─── Helpers ────────────────────────────────────────────────────────────────────

Expand DownExpand Up@@ -201,21 +203,38 @@ step "Creating DMG…"

rm -f "${DMG_PATH}"

DMG_TMP="${DIST_DIR}/.dmg-staging"
rm -rf "${DMG_TMP}"
mkdir -p "${DMG_TMP}"

cp -R "${APP_BUNDLE}" "${DMG_TMP}/"
ln -s /Applications "${DMG_TMP}/Applications"

hdiutil create \
-volname "${APP_NAME}" \
-srcfolder "${DMG_TMP}" \
-ov -format UDZO \
"${DMG_PATH}" \
-quiet
DMG_BACKGROUND_DIR="${DERIVED_DATA}/dmg-background"
DMG_TOOLS_DIR="${DERIVED_DATA}/dmg-tools"
DMG_TOOLS_STAMP="${DMG_TOOLS_DIR}/.utter-dmg-tools-1.6.7"

rm -rf "${DMG_BACKGROUND_DIR}"
mkdir -p "${DMG_BACKGROUND_DIR}"
swift "${SCRIPT_DIR}/generate-dmg-background.swift" \
"${DMG_BACKGROUND_DIR}/installer-background.png" 1
swift "${SCRIPT_DIR}/generate-dmg-background.swift" \
"${DMG_BACKGROUND_DIR}/installer-background@2x.png" 2

if [ ! -f "${DMG_TOOLS_STAMP}" ]; then
rm -rf "${DMG_TOOLS_DIR}"
mkdir -p "${DMG_TOOLS_DIR}"
python3 -m pip install \
--disable-pip-version-check \
--no-deps \
--only-binary=:all: \
--require-hashes \
--target "${DMG_TOOLS_DIR}" \
-r "${SCRIPT_DIR}/dmg-requirements.txt" \
-q
touch "${DMG_TOOLS_STAMP}"
fi

rm -rf "${DMG_TMP}"
PYTHONPATH="${DMG_TOOLS_DIR}" python3 -m dmgbuild \
-s "${SCRIPT_DIR}/dmg-settings.py" \
-D application="${APP_BUNDLE}" \
-D background="${DMG_BACKGROUND_DIR}/installer-background.png" \
-D volume_icon="${APP_BUNDLE}/Contents/Resources/AppIcon.icns" \
"${DMG_VOLUME_NAME}" \
"${DMG_PATH}"

done_msg "DMG created"

Expand Down
6 changes: 6 additions & 0 deletions scripts/dmg-requirements.txt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
dmgbuild==1.6.7 \
--hash=sha256:37ee5771c377beb3203d9164aae8046ffed8531c06edf9227f5788b3c599b1bf
ds-store==1.3.3 \
--hash=sha256:b92a371efbf1b4ccce2a04d1ed13fceacc4736c81ba09cf5aefb74c088160a35
mac-alias==2.2.3 \
--hash=sha256:7362b521d2132ef92f606a37abfed5fcd849ceb2f28b6f9743e014b02af92f0d
20 changes: 20 additions & 0 deletions scripts/dmg-settings.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
application = defines["application"]
background = defines["background"]
icon = defines["volume_icon"]

files = [(application, "Utter.app")]
symlinks = {"Applications": "/Applications"}
icon_locations = {
"Utter.app": (174, 250),
"Applications": (506, 250),
}

window_rect = ((120, 120), (680, 440))
icon_size = 112
text_size = 13
label_pos = "bottom"
show_icon_preview = True

format = "UDZO"
filesystem = "HFS+"
compression_level = 9
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
55 changes: 55 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/intent.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
# Intent: Polish and harden the Utter DMG installer

## Problem

The release DMG currently contains only `Utter.app` and an Applications
symlink. Finder chooses the window size, icon placement, and background, so the
installation flow looks unfinished and can vary with the packager's Finder
state. The existing path also has no Retina-aware installation artwork or
deterministic layout metadata.

## Outcome

Opening a release DMG shows a compact Utter-branded Finder window with the app
and Applications folder aligned around a clear drag arrow. Labels remain
readable in the user's current macOS appearance, and the same result is produced
locally and in release CI without scripting Finder.

## Scope

In scope: DMG background rendering, Finder window metadata, icon positions,
volume presentation, deterministic packaging dependencies, and the existing
`build-app.sh` DMG stage.

Out of scope: app runtime UI, application behavior, entitlements, signing
identity selection, notarization policy, release credentials, or installing the
app during validation.

## Constraints

- Preserve the current Swift Package and release-signing flow.
- Keep generated PNG/TIFF files, downloaded Python packages, `.app`, `.dmg`, and
screenshots out of Git.
- Use only macOS-compatible tooling available to local builders and GitHub macOS
runners; fail rather than silently ship an unstyled or unverifiable image.
- Treat local ad-hoc signing as packaging evidence, not public distribution
evidence.

## Acceptance criteria

- The real Finder window opens at 680 by 440 points with Utter and Applications
icons at the intended positions, a readable light background, and no clipped
installation copy.
- The background contains 1x and 2x representations and the DMG has a custom
volume icon.
- Packaging writes Finder layout metadata without launching or controlling
Finder.
- Packaging dependencies are version- and SHA-256-pinned and cached only below
`.build`.
- `verify-release-artifact.sh` and strict mounted-app code-signature validation
accept the locally generated artifact.

## Open questions

None. Human review still decides whether the new packaging dependency and
visual treatment are accepted for release.
33 changes: 33 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/plan.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
# Plan: Polish and harden the Utter DMG installer

## Work items

- [x] Audit the existing DMG stage and release workflow.
- [x] Render restrained 1x and 2x branded installation backgrounds.
- [x] Add deterministic Finder geometry, icon positions, Applications symlink,
and volume icon metadata without Finder automation.
- [x] Pin packaging dependencies by version and SHA-256 below the ignored build
cache.
- [x] Preserve strict mounted-app signature validity and the latest release
artifact verifier.
- [x] Re-run all repository, unit, release-build, artifact, and real-window
checks on the latest `origin/main` base.
- [x] Record final evidence and residual risk for review.

## Verification plan

- [x] `python3 scripts/sdlc.py validate --worktree`
- [x] `bash scripts/ci-basic-checks.sh`
- [x] `DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer swift test`
- [x] `DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer ./scripts/build-app.sh --version=0.0.0 --sign=-`
- [x] Inspect the mounted DMG, strict code signature, layout metadata, Retina
background representations, architecture, symlink, checksum, and real Finder
window.
- [x] `git diff --check`

## Human gates

An independent reviewer must accept the high-risk packaging/dependency change
and its rollback before merge. The protected release environment separately
owns the configured signing identity, Developer ID/notarization path, and public
release authorization.
62 changes: 62 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/spec.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
# Spec: Polish and harden the Utter DMG installer

## Context

`scripts/build-app.sh` already builds with Xcode, assembles `Utter.app`, compiles
appearance-aware app icons, signs the bundle, and verifies release artifacts.
Its previous DMG stage copied the app and Applications symlink into a staging
folder and asked `hdiutil` to compress that folder. It did not create a Finder
layout or installation background.

Finder automation can create `.DS_Store`, but it depends on interactive Finder
state and can be blocked by unrelated Finder dialogs. The packaging path must be
deterministic in both a developer session and GitHub Actions.

## Design

`generate-dmg-background.swift` renders a 680 by 440 AppKit bitmap at 1x and 2x.
The light neutral surface, subtle Utter color accents, centered installation
instruction, and arrow are positioned around the Finder icons rather than
duplicating them in the artwork.

`dmg-settings.py` owns the window rectangle, icon size, icon locations, HFS+
filesystem, UDZO compression, Applications symlink, and volume icon. The build
script installs `dmgbuild`, `ds-store`, and `mac-alias` into
`.build/xcode/dmg-tools` from `dmg-requirements.txt`. Every wheel is pinned by
version and SHA-256, and a versioned stamp avoids repeated installation.

The existing release verifier remains the final build step. No packaging code
changes signing identity selection, entitlements, notarization, or publication.

## Safety and failure modes

- A missing Python 3.10+ runtime, unavailable package index, hash mismatch, or
import failure stops the build; it does not fall back to an unstyled DMG.
- Downloaded code is isolated under ignored `.build` paths and never shipped in
`Utter.app`.
- Finder extension-hiding metadata is not written to `Utter.app`, because that
extended attribute makes strict code-signature verification fail.
- A corrupt tool cache fails during module import or DMG creation. Removing the
ignored cache allows a clean hash-verified reinstall.
- The packaging library currently emits macOS 26 deprecation warnings for its
internal `hdiutil` calls; artifact verification remains the acceptance gate.

## Test strategy

- Validate shell syntax, Python syntax, Swift background rendering, repository
policy, and the complete Swift test suite.
- Build a release-style ad-hoc app and DMG on the latest `main` base.
- Verify the DMG checksum, Applications symlink, mounted app signature, arm64
executable, 1x/2x TIFF representations, and exact `.DS_Store` geometry.
- Open the final image through Finder and inspect the actual title bar, artwork,
icon labels, spacing, arrow, and lower-edge clipping without installing or
launching the app.

## Rollout and rollback

Merge through the normal PR and release review path. The next release workflow
build exercises the same DMG stage with its configured signing identity and
artifact verifier. Stop if clean-runner dependency installation or mounted-DMG
verification fails. Roll back by reverting this change, which restores the
previous plain `hdiutil -srcfolder` image without changing app binaries or user
data.
28 changes: 28 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/state.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
{
"schemaVersion": 1,
"id": "2026-08-26-dmg-installer-polish",
"title": "Polish and harden the Utter DMG installer",
"risk": "high",
"status": "verified",
"owners": [
"repository maintainer"
],
"acceptanceCriteria": [
"Opening the DMG presents a compact branded Finder window with readable labels and an unambiguous drag-to-Applications flow.",
"The DMG layout is generated without depending on interactive Finder state and uses hash-pinned packaging tools.",
"The mounted app preserves its strict code-signature validity and the release artifact verifier accepts the generated DMG.",
"Generated build products, downloaded tools, and visual QA screenshots remain outside the committed change."
],
"governedPaths": [
"scripts/build-app.sh",
"scripts/dmg-requirements.txt",
"scripts/dmg-settings.py",
"scripts/generate-dmg-background.swift"
],
"artifacts": {
"intent": "intent.md",
"spec": "spec.md",
"plan": "plan.md",
"verification": "verification.md"
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
# Verification: Polish and harden the Utter DMG installer

## Evidence

The implementation was rebased onto `origin/main` at `7e0689a`. During the
implementation loop, a real Finder inspection caught two defects before PR
preparation: dark artwork made Finder labels hard to read, and hiding the
`.app` extension added FinderInfo that invalidated strict mounted-app signature
verification. Both were corrected before the latest-base verification run.

| Check | Result | Evidence |
|---|---|---|
| `python3 scripts/sdlc.py validate --worktree` | Pass | Verified bundle covers every governed changed path |
| `bash scripts/ci-basic-checks.sh` | Pass | SDLC, 13 harness tests, versioning, plist, localization, identifiers, lexicon, resources, conflicts, credentials, and symlink checks passed |
| `swift test` | Pass | 564 XCTest tests, 8 skipped, 0 failures; 1 Swift Testing test passed |
| Clean pinned tool install | Pass | `pip --require-hashes` installed `dmgbuild 1.6.7`, `ds-store 1.3.3`, and `mac-alias 2.2.3` into a new temporary directory |
| Release-style app and DMG build | Pass | `build-app.sh --version=0.0.0 --sign=-`; the built-in release artifact verifier passed |
| Mounted artifact verification | Pass | DMG checksum valid; strict deep code-signature verification passed; app is arm64; Applications resolves to `/Applications`; background contains two image representations |
| Finder layout metadata | Pass | 680 by 440 window; toolbar, sidebar, status bar, and path bar hidden; 112-point icons at `(174, 250)` and `(506, 250)`; background image enabled |
| Real Finder window | Pass with recapture limitation | The same final installer design was inspected in a real Finder window and showed readable, unclipped labels and a clear drag flow. A latest-base recapture attempt failed because ScreenCaptureKit could not start; final-artifact metadata and image representations were rechecked independently |
| `git diff --check` | Pass | No whitespace errors |

## Acceptance criteria

- Branded, readable, unclipped real Finder installation window — pass, with the
latest-base screenshot recapture limitation recorded above.
- Finder-independent, hash-pinned packaging — pass in clean temporary install
and repository checks.
- Strict mounted-app signature and release verifier — pass on the latest-base
release-style build.
- Generated products excluded from Git — pass in final worktree inspection.

## Residual risk

Independent high-risk review, configured release-identity verification, and any
Developer ID notarization run remain external/human gates. The packaging library
also emits non-fatal macOS 26 `hdiutil` deprecation warnings, and the latest-base
Finder screenshot should be spot-checked by the reviewer because the recapture
tool was unavailable.

## Decision

Ready for human review. No human approval is claimed here; approval remains a PR
review and protected-release decision.
47 changes: 33 additions & 14 deletions scripts/build-app.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@
# Requirements:
# - macOS with Xcode (full install, not just CLI tools)
# - Swift 6.0+
# - Python 3.10+ (DMG packaging only; pinned tools are cached under .build)
#

set -euo pipefail
Expand DownExpand Up@@ -65,6 +66,7 @@ done

APP_BUNDLE="${DIST_DIR}/${APP_NAME}.app"
DMG_PATH="${DIST_DIR}/${APP_NAME}-${VERSION}.dmg"
DMG_VOLUME_NAME="${APP_NAME} ${VERSION}"

# ─── Helpers ────────────────────────────────────────────────────────────────────

Expand DownExpand Up@@ -201,21 +203,38 @@ step "Creating DMG…"

rm -f "${DMG_PATH}"

DMG_TMP="${DIST_DIR}/.dmg-staging"
rm -rf "${DMG_TMP}"
mkdir -p "${DMG_TMP}"

cp -R "${APP_BUNDLE}" "${DMG_TMP}/"
ln -s /Applications "${DMG_TMP}/Applications"

hdiutil create \
-volname "${APP_NAME}" \
-srcfolder "${DMG_TMP}" \
-ov -format UDZO \
"${DMG_PATH}" \
-quiet
DMG_BACKGROUND_DIR="${DERIVED_DATA}/dmg-background"
DMG_TOOLS_DIR="${DERIVED_DATA}/dmg-tools"
DMG_TOOLS_STAMP="${DMG_TOOLS_DIR}/.utter-dmg-tools-1.6.7"

rm -rf "${DMG_BACKGROUND_DIR}"
mkdir -p "${DMG_BACKGROUND_DIR}"
swift "${SCRIPT_DIR}/generate-dmg-background.swift" \
"${DMG_BACKGROUND_DIR}/installer-background.png" 1
swift "${SCRIPT_DIR}/generate-dmg-background.swift" \
"${DMG_BACKGROUND_DIR}/installer-background@2x.png" 2

if [ ! -f "${DMG_TOOLS_STAMP}" ]; then
rm -rf "${DMG_TOOLS_DIR}"
mkdir -p "${DMG_TOOLS_DIR}"
python3 -m pip install \
--disable-pip-version-check \
--no-deps \
--only-binary=:all: \
--require-hashes \
--target "${DMG_TOOLS_DIR}" \
-r "${SCRIPT_DIR}/dmg-requirements.txt" \
-q
touch "${DMG_TOOLS_STAMP}"
fi

rm -rf "${DMG_TMP}"
PYTHONPATH="${DMG_TOOLS_DIR}" python3 -m dmgbuild \
-s "${SCRIPT_DIR}/dmg-settings.py" \
-D application="${APP_BUNDLE}" \
-D background="${DMG_BACKGROUND_DIR}/installer-background.png" \
-D volume_icon="${APP_BUNDLE}/Contents/Resources/AppIcon.icns" \
"${DMG_VOLUME_NAME}" \
"${DMG_PATH}"

done_msg "DMG created"

Expand Down
6 changes: 6 additions & 0 deletions scripts/dmg-requirements.txt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
dmgbuild==1.6.7 \
--hash=sha256:37ee5771c377beb3203d9164aae8046ffed8531c06edf9227f5788b3c599b1bf
ds-store==1.3.3 \
--hash=sha256:b92a371efbf1b4ccce2a04d1ed13fceacc4736c81ba09cf5aefb74c088160a35
mac-alias==2.2.3 \
--hash=sha256:7362b521d2132ef92f606a37abfed5fcd849ceb2f28b6f9743e014b02af92f0d
20 changes: 20 additions & 0 deletions scripts/dmg-settings.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
application = defines["application"]
background = defines["background"]
icon = defines["volume_icon"]

files = [(application, "Utter.app")]
symlinks = {"Applications": "/Applications"}
icon_locations = {
"Utter.app": (174, 250),
"Applications": (506, 250),
}

window_rect = ((120, 120), (680, 440))
icon_size = 112
text_size = 13
label_pos = "bottom"
show_icon_preview = True

format = "UDZO"
filesystem = "HFS+"
compression_level = 9
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
55 changes: 55 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/intent.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
# Intent: Polish and harden the Utter DMG installer

## Problem

The release DMG currently contains only `Utter.app` and an Applications
symlink. Finder chooses the window size, icon placement, and background, so the
installation flow looks unfinished and can vary with the packager's Finder
state. The existing path also has no Retina-aware installation artwork or
deterministic layout metadata.

## Outcome

Opening a release DMG shows a compact Utter-branded Finder window with the app
and Applications folder aligned around a clear drag arrow. Labels remain
readable in the user's current macOS appearance, and the same result is produced
locally and in release CI without scripting Finder.

## Scope

In scope: DMG background rendering, Finder window metadata, icon positions,
volume presentation, deterministic packaging dependencies, and the existing
`build-app.sh` DMG stage.

Out of scope: app runtime UI, application behavior, entitlements, signing
identity selection, notarization policy, release credentials, or installing the
app during validation.

## Constraints

- Preserve the current Swift Package and release-signing flow.
- Keep generated PNG/TIFF files, downloaded Python packages, `.app`, `.dmg`, and
screenshots out of Git.
- Use only macOS-compatible tooling available to local builders and GitHub macOS
runners; fail rather than silently ship an unstyled or unverifiable image.
- Treat local ad-hoc signing as packaging evidence, not public distribution
evidence.

## Acceptance criteria

- The real Finder window opens at 680 by 440 points with Utter and Applications
icons at the intended positions, a readable light background, and no clipped
installation copy.
- The background contains 1x and 2x representations and the DMG has a custom
volume icon.
- Packaging writes Finder layout metadata without launching or controlling
Finder.
- Packaging dependencies are version- and SHA-256-pinned and cached only below
`.build`.
- `verify-release-artifact.sh` and strict mounted-app code-signature validation
accept the locally generated artifact.

## Open questions

None. Human review still decides whether the new packaging dependency and
visual treatment are accepted for release.
33 changes: 33 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/plan.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
# Plan: Polish and harden the Utter DMG installer

## Work items

- [x] Audit the existing DMG stage and release workflow.
- [x] Render restrained 1x and 2x branded installation backgrounds.
- [x] Add deterministic Finder geometry, icon positions, Applications symlink,
and volume icon metadata without Finder automation.
- [x] Pin packaging dependencies by version and SHA-256 below the ignored build
cache.
- [x] Preserve strict mounted-app signature validity and the latest release
artifact verifier.
- [x] Re-run all repository, unit, release-build, artifact, and real-window
checks on the latest `origin/main` base.
- [x] Record final evidence and residual risk for review.

## Verification plan

- [x] `python3 scripts/sdlc.py validate --worktree`
- [x] `bash scripts/ci-basic-checks.sh`
- [x] `DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer swift test`
- [x] `DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer ./scripts/build-app.sh --version=0.0.0 --sign=-`
- [x] Inspect the mounted DMG, strict code signature, layout metadata, Retina
background representations, architecture, symlink, checksum, and real Finder
window.
- [x] `git diff --check`

## Human gates

An independent reviewer must accept the high-risk packaging/dependency change
and its rollback before merge. The protected release environment separately
owns the configured signing identity, Developer ID/notarization path, and public
release authorization.
62 changes: 62 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/spec.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
# Spec: Polish and harden the Utter DMG installer

## Context

`scripts/build-app.sh` already builds with Xcode, assembles `Utter.app`, compiles
appearance-aware app icons, signs the bundle, and verifies release artifacts.
Its previous DMG stage copied the app and Applications symlink into a staging
folder and asked `hdiutil` to compress that folder. It did not create a Finder
layout or installation background.

Finder automation can create `.DS_Store`, but it depends on interactive Finder
state and can be blocked by unrelated Finder dialogs. The packaging path must be
deterministic in both a developer session and GitHub Actions.

## Design

`generate-dmg-background.swift` renders a 680 by 440 AppKit bitmap at 1x and 2x.
The light neutral surface, subtle Utter color accents, centered installation
instruction, and arrow are positioned around the Finder icons rather than
duplicating them in the artwork.

`dmg-settings.py` owns the window rectangle, icon size, icon locations, HFS+
filesystem, UDZO compression, Applications symlink, and volume icon. The build
script installs `dmgbuild`, `ds-store`, and `mac-alias` into
`.build/xcode/dmg-tools` from `dmg-requirements.txt`. Every wheel is pinned by
version and SHA-256, and a versioned stamp avoids repeated installation.

The existing release verifier remains the final build step. No packaging code
changes signing identity selection, entitlements, notarization, or publication.

## Safety and failure modes

- A missing Python 3.10+ runtime, unavailable package index, hash mismatch, or
import failure stops the build; it does not fall back to an unstyled DMG.
- Downloaded code is isolated under ignored `.build` paths and never shipped in
`Utter.app`.
- Finder extension-hiding metadata is not written to `Utter.app`, because that
extended attribute makes strict code-signature verification fail.
- A corrupt tool cache fails during module import or DMG creation. Removing the
ignored cache allows a clean hash-verified reinstall.
- The packaging library currently emits macOS 26 deprecation warnings for its
internal `hdiutil` calls; artifact verification remains the acceptance gate.

## Test strategy

- Validate shell syntax, Python syntax, Swift background rendering, repository
policy, and the complete Swift test suite.
- Build a release-style ad-hoc app and DMG on the latest `main` base.
- Verify the DMG checksum, Applications symlink, mounted app signature, arm64
executable, 1x/2x TIFF representations, and exact `.DS_Store` geometry.
- Open the final image through Finder and inspect the actual title bar, artwork,
icon labels, spacing, arrow, and lower-edge clipping without installing or
launching the app.

## Rollout and rollback

Merge through the normal PR and release review path. The next release workflow
build exercises the same DMG stage with its configured signing identity and
artifact verifier. Stop if clean-runner dependency installation or mounted-DMG
verification fails. Roll back by reverting this change, which restores the
previous plain `hdiutil -srcfolder` image without changing app binaries or user
data.
28 changes: 28 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/state.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
{
"schemaVersion": 1,
"id": "2026-08-26-dmg-installer-polish",
"title": "Polish and harden the Utter DMG installer",
"risk": "high",
"status": "verified",
"owners": [
"repository maintainer"
],
"acceptanceCriteria": [
"Opening the DMG presents a compact branded Finder window with readable labels and an unambiguous drag-to-Applications flow.",
"The DMG layout is generated without depending on interactive Finder state and uses hash-pinned packaging tools.",
"The mounted app preserves its strict code-signature validity and the release artifact verifier accepts the generated DMG.",
"Generated build products, downloaded tools, and visual QA screenshots remain outside the committed change."
],
"governedPaths": [
"scripts/build-app.sh",
"scripts/dmg-requirements.txt",
"scripts/dmg-settings.py",
"scripts/generate-dmg-background.swift"
],
"artifacts": {
"intent": "intent.md",
"spec": "spec.md",
"plan": "plan.md",
"verification": "verification.md"
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
# Verification: Polish and harden the Utter DMG installer

## Evidence

The implementation was rebased onto `origin/main` at `7e0689a`. During the
implementation loop, a real Finder inspection caught two defects before PR
preparation: dark artwork made Finder labels hard to read, and hiding the
`.app` extension added FinderInfo that invalidated strict mounted-app signature
verification. Both were corrected before the latest-base verification run.

| Check | Result | Evidence |
|---|---|---|
| `python3 scripts/sdlc.py validate --worktree` | Pass | Verified bundle covers every governed changed path |
| `bash scripts/ci-basic-checks.sh` | Pass | SDLC, 13 harness tests, versioning, plist, localization, identifiers, lexicon, resources, conflicts, credentials, and symlink checks passed |
| `swift test` | Pass | 564 XCTest tests, 8 skipped, 0 failures; 1 Swift Testing test passed |
| Clean pinned tool install | Pass | `pip --require-hashes` installed `dmgbuild 1.6.7`, `ds-store 1.3.3`, and `mac-alias 2.2.3` into a new temporary directory |
| Release-style app and DMG build | Pass | `build-app.sh --version=0.0.0 --sign=-`; the built-in release artifact verifier passed |
| Mounted artifact verification | Pass | DMG checksum valid; strict deep code-signature verification passed; app is arm64; Applications resolves to `/Applications`; background contains two image representations |
| Finder layout metadata | Pass | 680 by 440 window; toolbar, sidebar, status bar, and path bar hidden; 112-point icons at `(174, 250)` and `(506, 250)`; background image enabled |
| Real Finder window | Pass with recapture limitation | The same final installer design was inspected in a real Finder window and showed readable, unclipped labels and a clear drag flow. A latest-base recapture attempt failed because ScreenCaptureKit could not start; final-artifact metadata and image representations were rechecked independently |
| `git diff --check` | Pass | No whitespace errors |

## Acceptance criteria

- Branded, readable, unclipped real Finder installation window — pass, with the
latest-base screenshot recapture limitation recorded above.
- Finder-independent, hash-pinned packaging — pass in clean temporary install
and repository checks.
- Strict mounted-app signature and release verifier — pass on the latest-base
release-style build.
- Generated products excluded from Git — pass in final worktree inspection.

## Residual risk

Independent high-risk review, configured release-identity verification, and any
Developer ID notarization run remain external/human gates. The packaging library
also emits non-fatal macOS 26 `hdiutil` deprecation warnings, and the latest-base
Finder screenshot should be spot-checked by the reviewer because the recapture
tool was unavailable.

## Decision

Ready for human review. No human approval is claimed here; approval remains a PR
review and protected-release decision.
47 changes: 33 additions & 14 deletions scripts/build-app.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@
# Requirements:
# - macOS with Xcode (full install, not just CLI tools)
# - Swift 6.0+
# - Python 3.10+ (DMG packaging only; pinned tools are cached under .build)
#

set -euo pipefail
Expand DownExpand Up@@ -65,6 +66,7 @@ done

APP_BUNDLE="${DIST_DIR}/${APP_NAME}.app"
DMG_PATH="${DIST_DIR}/${APP_NAME}-${VERSION}.dmg"
DMG_VOLUME_NAME="${APP_NAME} ${VERSION}"

# ─── Helpers ────────────────────────────────────────────────────────────────────

Expand DownExpand Up@@ -201,21 +203,38 @@ step "Creating DMG…"

rm -f "${DMG_PATH}"

DMG_TMP="${DIST_DIR}/.dmg-staging"
rm -rf "${DMG_TMP}"
mkdir -p "${DMG_TMP}"

cp -R "${APP_BUNDLE}" "${DMG_TMP}/"
ln -s /Applications "${DMG_TMP}/Applications"

hdiutil create \
-volname "${APP_NAME}" \
-srcfolder "${DMG_TMP}" \
-ov -format UDZO \
"${DMG_PATH}" \
-quiet
DMG_BACKGROUND_DIR="${DERIVED_DATA}/dmg-background"
DMG_TOOLS_DIR="${DERIVED_DATA}/dmg-tools"
DMG_TOOLS_STAMP="${DMG_TOOLS_DIR}/.utter-dmg-tools-1.6.7"

rm -rf "${DMG_BACKGROUND_DIR}"
mkdir -p "${DMG_BACKGROUND_DIR}"
swift "${SCRIPT_DIR}/generate-dmg-background.swift" \
"${DMG_BACKGROUND_DIR}/installer-background.png" 1
swift "${SCRIPT_DIR}/generate-dmg-background.swift" \
"${DMG_BACKGROUND_DIR}/installer-background@2x.png" 2

if [ ! -f "${DMG_TOOLS_STAMP}" ]; then
rm -rf "${DMG_TOOLS_DIR}"
mkdir -p "${DMG_TOOLS_DIR}"
python3 -m pip install \
--disable-pip-version-check \
--no-deps \
--only-binary=:all: \
--require-hashes \
--target "${DMG_TOOLS_DIR}" \
-r "${SCRIPT_DIR}/dmg-requirements.txt" \
-q
touch "${DMG_TOOLS_STAMP}"
fi

rm -rf "${DMG_TMP}"
PYTHONPATH="${DMG_TOOLS_DIR}" python3 -m dmgbuild \
-s "${SCRIPT_DIR}/dmg-settings.py" \
-D application="${APP_BUNDLE}" \
-D background="${DMG_BACKGROUND_DIR}/installer-background.png" \
-D volume_icon="${APP_BUNDLE}/Contents/Resources/AppIcon.icns" \
"${DMG_VOLUME_NAME}" \
"${DMG_PATH}"

done_msg "DMG created"

Expand Down
6 changes: 6 additions & 0 deletions scripts/dmg-requirements.txt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
dmgbuild==1.6.7 \
--hash=sha256:37ee5771c377beb3203d9164aae8046ffed8531c06edf9227f5788b3c599b1bf
ds-store==1.3.3 \
--hash=sha256:b92a371efbf1b4ccce2a04d1ed13fceacc4736c81ba09cf5aefb74c088160a35
mac-alias==2.2.3 \
--hash=sha256:7362b521d2132ef92f606a37abfed5fcd849ceb2f28b6f9743e014b02af92f0d
20 changes: 20 additions & 0 deletions scripts/dmg-settings.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
application = defines["application"]
background = defines["background"]
icon = defines["volume_icon"]

files = [(application, "Utter.app")]
symlinks = {"Applications": "/Applications"}
icon_locations = {
"Utter.app": (174, 250),
"Applications": (506, 250),
}

window_rect = ((120, 120), (680, 440))
icon_size = 112
text_size = 13
label_pos = "bottom"
show_icon_preview = True

format = "UDZO"
filesystem = "HFS+"
compression_level = 9
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
55 changes: 55 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/intent.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
# Intent: Polish and harden the Utter DMG installer

## Problem

The release DMG currently contains only `Utter.app` and an Applications
symlink. Finder chooses the window size, icon placement, and background, so the
installation flow looks unfinished and can vary with the packager's Finder
state. The existing path also has no Retina-aware installation artwork or
deterministic layout metadata.

## Outcome

Opening a release DMG shows a compact Utter-branded Finder window with the app
and Applications folder aligned around a clear drag arrow. Labels remain
readable in the user's current macOS appearance, and the same result is produced
locally and in release CI without scripting Finder.

## Scope

In scope: DMG background rendering, Finder window metadata, icon positions,
volume presentation, deterministic packaging dependencies, and the existing
`build-app.sh` DMG stage.

Out of scope: app runtime UI, application behavior, entitlements, signing
identity selection, notarization policy, release credentials, or installing the
app during validation.

## Constraints

- Preserve the current Swift Package and release-signing flow.
- Keep generated PNG/TIFF files, downloaded Python packages, `.app`, `.dmg`, and
screenshots out of Git.
- Use only macOS-compatible tooling available to local builders and GitHub macOS
runners; fail rather than silently ship an unstyled or unverifiable image.
- Treat local ad-hoc signing as packaging evidence, not public distribution
evidence.

## Acceptance criteria

- The real Finder window opens at 680 by 440 points with Utter and Applications
icons at the intended positions, a readable light background, and no clipped
installation copy.
- The background contains 1x and 2x representations and the DMG has a custom
volume icon.
- Packaging writes Finder layout metadata without launching or controlling
Finder.
- Packaging dependencies are version- and SHA-256-pinned and cached only below
`.build`.
- `verify-release-artifact.sh` and strict mounted-app code-signature validation
accept the locally generated artifact.

## Open questions

None. Human review still decides whether the new packaging dependency and
visual treatment are accepted for release.
33 changes: 33 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/plan.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
# Plan: Polish and harden the Utter DMG installer

## Work items

- [x] Audit the existing DMG stage and release workflow.
- [x] Render restrained 1x and 2x branded installation backgrounds.
- [x] Add deterministic Finder geometry, icon positions, Applications symlink,
and volume icon metadata without Finder automation.
- [x] Pin packaging dependencies by version and SHA-256 below the ignored build
cache.
- [x] Preserve strict mounted-app signature validity and the latest release
artifact verifier.
- [x] Re-run all repository, unit, release-build, artifact, and real-window
checks on the latest `origin/main` base.
- [x] Record final evidence and residual risk for review.

## Verification plan

- [x] `python3 scripts/sdlc.py validate --worktree`
- [x] `bash scripts/ci-basic-checks.sh`
- [x] `DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer swift test`
- [x] `DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer ./scripts/build-app.sh --version=0.0.0 --sign=-`
- [x] Inspect the mounted DMG, strict code signature, layout metadata, Retina
background representations, architecture, symlink, checksum, and real Finder
window.
- [x] `git diff --check`

## Human gates

An independent reviewer must accept the high-risk packaging/dependency change
and its rollback before merge. The protected release environment separately
owns the configured signing identity, Developer ID/notarization path, and public
release authorization.
62 changes: 62 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/spec.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
# Spec: Polish and harden the Utter DMG installer

## Context

`scripts/build-app.sh` already builds with Xcode, assembles `Utter.app`, compiles
appearance-aware app icons, signs the bundle, and verifies release artifacts.
Its previous DMG stage copied the app and Applications symlink into a staging
folder and asked `hdiutil` to compress that folder. It did not create a Finder
layout or installation background.

Finder automation can create `.DS_Store`, but it depends on interactive Finder
state and can be blocked by unrelated Finder dialogs. The packaging path must be
deterministic in both a developer session and GitHub Actions.

## Design

`generate-dmg-background.swift` renders a 680 by 440 AppKit bitmap at 1x and 2x.
The light neutral surface, subtle Utter color accents, centered installation
instruction, and arrow are positioned around the Finder icons rather than
duplicating them in the artwork.

`dmg-settings.py` owns the window rectangle, icon size, icon locations, HFS+
filesystem, UDZO compression, Applications symlink, and volume icon. The build
script installs `dmgbuild`, `ds-store`, and `mac-alias` into
`.build/xcode/dmg-tools` from `dmg-requirements.txt`. Every wheel is pinned by
version and SHA-256, and a versioned stamp avoids repeated installation.

The existing release verifier remains the final build step. No packaging code
changes signing identity selection, entitlements, notarization, or publication.

## Safety and failure modes

- A missing Python 3.10+ runtime, unavailable package index, hash mismatch, or
import failure stops the build; it does not fall back to an unstyled DMG.
- Downloaded code is isolated under ignored `.build` paths and never shipped in
`Utter.app`.
- Finder extension-hiding metadata is not written to `Utter.app`, because that
extended attribute makes strict code-signature verification fail.
- A corrupt tool cache fails during module import or DMG creation. Removing the
ignored cache allows a clean hash-verified reinstall.
- The packaging library currently emits macOS 26 deprecation warnings for its
internal `hdiutil` calls; artifact verification remains the acceptance gate.

## Test strategy

- Validate shell syntax, Python syntax, Swift background rendering, repository
policy, and the complete Swift test suite.
- Build a release-style ad-hoc app and DMG on the latest `main` base.
- Verify the DMG checksum, Applications symlink, mounted app signature, arm64
executable, 1x/2x TIFF representations, and exact `.DS_Store` geometry.
- Open the final image through Finder and inspect the actual title bar, artwork,
icon labels, spacing, arrow, and lower-edge clipping without installing or
launching the app.

## Rollout and rollback

Merge through the normal PR and release review path. The next release workflow
build exercises the same DMG stage with its configured signing identity and
artifact verifier. Stop if clean-runner dependency installation or mounted-DMG
verification fails. Roll back by reverting this change, which restores the
previous plain `hdiutil -srcfolder` image without changing app binaries or user
data.
28 changes: 28 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/state.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
{
"schemaVersion": 1,
"id": "2026-08-26-dmg-installer-polish",
"title": "Polish and harden the Utter DMG installer",
"risk": "high",
"status": "verified",
"owners": [
"repository maintainer"
],
"acceptanceCriteria": [
"Opening the DMG presents a compact branded Finder window with readable labels and an unambiguous drag-to-Applications flow.",
"The DMG layout is generated without depending on interactive Finder state and uses hash-pinned packaging tools.",
"The mounted app preserves its strict code-signature validity and the release artifact verifier accepts the generated DMG.",
"Generated build products, downloaded tools, and visual QA screenshots remain outside the committed change."
],
"governedPaths": [
"scripts/build-app.sh",
"scripts/dmg-requirements.txt",
"scripts/dmg-settings.py",
"scripts/generate-dmg-background.swift"
],
"artifacts": {
"intent": "intent.md",
"spec": "spec.md",
"plan": "plan.md",
"verification": "verification.md"
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
# Verification: Polish and harden the Utter DMG installer

## Evidence

The implementation was rebased onto `origin/main` at `7e0689a`. During the
implementation loop, a real Finder inspection caught two defects before PR
preparation: dark artwork made Finder labels hard to read, and hiding the
`.app` extension added FinderInfo that invalidated strict mounted-app signature
verification. Both were corrected before the latest-base verification run.

| Check | Result | Evidence |
|---|---|---|
| `python3 scripts/sdlc.py validate --worktree` | Pass | Verified bundle covers every governed changed path |
| `bash scripts/ci-basic-checks.sh` | Pass | SDLC, 13 harness tests, versioning, plist, localization, identifiers, lexicon, resources, conflicts, credentials, and symlink checks passed |
| `swift test` | Pass | 564 XCTest tests, 8 skipped, 0 failures; 1 Swift Testing test passed |
| Clean pinned tool install | Pass | `pip --require-hashes` installed `dmgbuild 1.6.7`, `ds-store 1.3.3`, and `mac-alias 2.2.3` into a new temporary directory |
| Release-style app and DMG build | Pass | `build-app.sh --version=0.0.0 --sign=-`; the built-in release artifact verifier passed |
| Mounted artifact verification | Pass | DMG checksum valid; strict deep code-signature verification passed; app is arm64; Applications resolves to `/Applications`; background contains two image representations |
| Finder layout metadata | Pass | 680 by 440 window; toolbar, sidebar, status bar, and path bar hidden; 112-point icons at `(174, 250)` and `(506, 250)`; background image enabled |
| Real Finder window | Pass with recapture limitation | The same final installer design was inspected in a real Finder window and showed readable, unclipped labels and a clear drag flow. A latest-base recapture attempt failed because ScreenCaptureKit could not start; final-artifact metadata and image representations were rechecked independently |
| `git diff --check` | Pass | No whitespace errors |

## Acceptance criteria

- Branded, readable, unclipped real Finder installation window — pass, with the
latest-base screenshot recapture limitation recorded above.
- Finder-independent, hash-pinned packaging — pass in clean temporary install
and repository checks.
- Strict mounted-app signature and release verifier — pass on the latest-base
release-style build.
- Generated products excluded from Git — pass in final worktree inspection.

## Residual risk

Independent high-risk review, configured release-identity verification, and any
Developer ID notarization run remain external/human gates. The packaging library
also emits non-fatal macOS 26 `hdiutil` deprecation warnings, and the latest-base
Finder screenshot should be spot-checked by the reviewer because the recapture
tool was unavailable.

## Decision

Ready for human review. No human approval is claimed here; approval remains a PR
review and protected-release decision.
47 changes: 33 additions & 14 deletions scripts/build-app.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@
# Requirements:
# - macOS with Xcode (full install, not just CLI tools)
# - Swift 6.0+
# - Python 3.10+ (DMG packaging only; pinned tools are cached under .build)
#

set -euo pipefail
Expand DownExpand Up@@ -65,6 +66,7 @@ done

APP_BUNDLE="${DIST_DIR}/${APP_NAME}.app"
DMG_PATH="${DIST_DIR}/${APP_NAME}-${VERSION}.dmg"
DMG_VOLUME_NAME="${APP_NAME} ${VERSION}"

# ─── Helpers ────────────────────────────────────────────────────────────────────

Expand DownExpand Up@@ -201,21 +203,38 @@ step "Creating DMG…"

rm -f "${DMG_PATH}"

DMG_TMP="${DIST_DIR}/.dmg-staging"
rm -rf "${DMG_TMP}"
mkdir -p "${DMG_TMP}"

cp -R "${APP_BUNDLE}" "${DMG_TMP}/"
ln -s /Applications "${DMG_TMP}/Applications"

hdiutil create \
-volname "${APP_NAME}" \
-srcfolder "${DMG_TMP}" \
-ov -format UDZO \
"${DMG_PATH}" \
-quiet
DMG_BACKGROUND_DIR="${DERIVED_DATA}/dmg-background"
DMG_TOOLS_DIR="${DERIVED_DATA}/dmg-tools"
DMG_TOOLS_STAMP="${DMG_TOOLS_DIR}/.utter-dmg-tools-1.6.7"

rm -rf "${DMG_BACKGROUND_DIR}"
mkdir -p "${DMG_BACKGROUND_DIR}"
swift "${SCRIPT_DIR}/generate-dmg-background.swift" \
"${DMG_BACKGROUND_DIR}/installer-background.png" 1
swift "${SCRIPT_DIR}/generate-dmg-background.swift" \
"${DMG_BACKGROUND_DIR}/installer-background@2x.png" 2

if [ ! -f "${DMG_TOOLS_STAMP}" ]; then
rm -rf "${DMG_TOOLS_DIR}"
mkdir -p "${DMG_TOOLS_DIR}"
python3 -m pip install \
--disable-pip-version-check \
--no-deps \
--only-binary=:all: \
--require-hashes \
--target "${DMG_TOOLS_DIR}" \
-r "${SCRIPT_DIR}/dmg-requirements.txt" \
-q
touch "${DMG_TOOLS_STAMP}"
fi

rm -rf "${DMG_TMP}"
PYTHONPATH="${DMG_TOOLS_DIR}" python3 -m dmgbuild \
-s "${SCRIPT_DIR}/dmg-settings.py" \
-D application="${APP_BUNDLE}" \
-D background="${DMG_BACKGROUND_DIR}/installer-background.png" \
-D volume_icon="${APP_BUNDLE}/Contents/Resources/AppIcon.icns" \
"${DMG_VOLUME_NAME}" \
"${DMG_PATH}"

done_msg "DMG created"

Expand Down
6 changes: 6 additions & 0 deletions scripts/dmg-requirements.txt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
dmgbuild==1.6.7 \
--hash=sha256:37ee5771c377beb3203d9164aae8046ffed8531c06edf9227f5788b3c599b1bf
ds-store==1.3.3 \
--hash=sha256:b92a371efbf1b4ccce2a04d1ed13fceacc4736c81ba09cf5aefb74c088160a35
mac-alias==2.2.3 \
--hash=sha256:7362b521d2132ef92f606a37abfed5fcd849ceb2f28b6f9743e014b02af92f0d
20 changes: 20 additions & 0 deletions scripts/dmg-settings.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
application = defines["application"]
background = defines["background"]
icon = defines["volume_icon"]

files = [(application, "Utter.app")]
symlinks = {"Applications": "/Applications"}
icon_locations = {
"Utter.app": (174, 250),
"Applications": (506, 250),
}

window_rect = ((120, 120), (680, 440))
icon_size = 112
text_size = 13
label_pos = "bottom"
show_icon_preview = True

format = "UDZO"
filesystem = "HFS+"
compression_level = 9
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
55 changes: 55 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/intent.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
# Intent: Polish and harden the Utter DMG installer

## Problem

The release DMG currently contains only `Utter.app` and an Applications
symlink. Finder chooses the window size, icon placement, and background, so the
installation flow looks unfinished and can vary with the packager's Finder
state. The existing path also has no Retina-aware installation artwork or
deterministic layout metadata.

## Outcome

Opening a release DMG shows a compact Utter-branded Finder window with the app
and Applications folder aligned around a clear drag arrow. Labels remain
readable in the user's current macOS appearance, and the same result is produced
locally and in release CI without scripting Finder.

## Scope

In scope: DMG background rendering, Finder window metadata, icon positions,
volume presentation, deterministic packaging dependencies, and the existing
`build-app.sh` DMG stage.

Out of scope: app runtime UI, application behavior, entitlements, signing
identity selection, notarization policy, release credentials, or installing the
app during validation.

## Constraints

- Preserve the current Swift Package and release-signing flow.
- Keep generated PNG/TIFF files, downloaded Python packages, `.app`, `.dmg`, and
screenshots out of Git.
- Use only macOS-compatible tooling available to local builders and GitHub macOS
runners; fail rather than silently ship an unstyled or unverifiable image.
- Treat local ad-hoc signing as packaging evidence, not public distribution
evidence.

## Acceptance criteria

- The real Finder window opens at 680 by 440 points with Utter and Applications
icons at the intended positions, a readable light background, and no clipped
installation copy.
- The background contains 1x and 2x representations and the DMG has a custom
volume icon.
- Packaging writes Finder layout metadata without launching or controlling
Finder.
- Packaging dependencies are version- and SHA-256-pinned and cached only below
`.build`.
- `verify-release-artifact.sh` and strict mounted-app code-signature validation
accept the locally generated artifact.

## Open questions

None. Human review still decides whether the new packaging dependency and
visual treatment are accepted for release.
33 changes: 33 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/plan.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
# Plan: Polish and harden the Utter DMG installer

## Work items

- [x] Audit the existing DMG stage and release workflow.
- [x] Render restrained 1x and 2x branded installation backgrounds.
- [x] Add deterministic Finder geometry, icon positions, Applications symlink,
and volume icon metadata without Finder automation.
- [x] Pin packaging dependencies by version and SHA-256 below the ignored build
cache.
- [x] Preserve strict mounted-app signature validity and the latest release
artifact verifier.
- [x] Re-run all repository, unit, release-build, artifact, and real-window
checks on the latest `origin/main` base.
- [x] Record final evidence and residual risk for review.

## Verification plan

- [x] `python3 scripts/sdlc.py validate --worktree`
- [x] `bash scripts/ci-basic-checks.sh`
- [x] `DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer swift test`
- [x] `DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer ./scripts/build-app.sh --version=0.0.0 --sign=-`
- [x] Inspect the mounted DMG, strict code signature, layout metadata, Retina
background representations, architecture, symlink, checksum, and real Finder
window.
- [x] `git diff --check`

## Human gates

An independent reviewer must accept the high-risk packaging/dependency change
and its rollback before merge. The protected release environment separately
owns the configured signing identity, Developer ID/notarization path, and public
release authorization.
62 changes: 62 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/spec.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
# Spec: Polish and harden the Utter DMG installer

## Context

`scripts/build-app.sh` already builds with Xcode, assembles `Utter.app`, compiles
appearance-aware app icons, signs the bundle, and verifies release artifacts.
Its previous DMG stage copied the app and Applications symlink into a staging
folder and asked `hdiutil` to compress that folder. It did not create a Finder
layout or installation background.

Finder automation can create `.DS_Store`, but it depends on interactive Finder
state and can be blocked by unrelated Finder dialogs. The packaging path must be
deterministic in both a developer session and GitHub Actions.

## Design

`generate-dmg-background.swift` renders a 680 by 440 AppKit bitmap at 1x and 2x.
The light neutral surface, subtle Utter color accents, centered installation
instruction, and arrow are positioned around the Finder icons rather than
duplicating them in the artwork.

`dmg-settings.py` owns the window rectangle, icon size, icon locations, HFS+
filesystem, UDZO compression, Applications symlink, and volume icon. The build
script installs `dmgbuild`, `ds-store`, and `mac-alias` into
`.build/xcode/dmg-tools` from `dmg-requirements.txt`. Every wheel is pinned by
version and SHA-256, and a versioned stamp avoids repeated installation.

The existing release verifier remains the final build step. No packaging code
changes signing identity selection, entitlements, notarization, or publication.

## Safety and failure modes

- A missing Python 3.10+ runtime, unavailable package index, hash mismatch, or
import failure stops the build; it does not fall back to an unstyled DMG.
- Downloaded code is isolated under ignored `.build` paths and never shipped in
`Utter.app`.
- Finder extension-hiding metadata is not written to `Utter.app`, because that
extended attribute makes strict code-signature verification fail.
- A corrupt tool cache fails during module import or DMG creation. Removing the
ignored cache allows a clean hash-verified reinstall.
- The packaging library currently emits macOS 26 deprecation warnings for its
internal `hdiutil` calls; artifact verification remains the acceptance gate.

## Test strategy

- Validate shell syntax, Python syntax, Swift background rendering, repository
policy, and the complete Swift test suite.
- Build a release-style ad-hoc app and DMG on the latest `main` base.
- Verify the DMG checksum, Applications symlink, mounted app signature, arm64
executable, 1x/2x TIFF representations, and exact `.DS_Store` geometry.
- Open the final image through Finder and inspect the actual title bar, artwork,
icon labels, spacing, arrow, and lower-edge clipping without installing or
launching the app.

## Rollout and rollback

Merge through the normal PR and release review path. The next release workflow
build exercises the same DMG stage with its configured signing identity and
artifact verifier. Stop if clean-runner dependency installation or mounted-DMG
verification fails. Roll back by reverting this change, which restores the
previous plain `hdiutil -srcfolder` image without changing app binaries or user
data.
28 changes: 28 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/state.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
{
"schemaVersion": 1,
"id": "2026-08-26-dmg-installer-polish",
"title": "Polish and harden the Utter DMG installer",
"risk": "high",
"status": "verified",
"owners": [
"repository maintainer"
],
"acceptanceCriteria": [
"Opening the DMG presents a compact branded Finder window with readable labels and an unambiguous drag-to-Applications flow.",
"The DMG layout is generated without depending on interactive Finder state and uses hash-pinned packaging tools.",
"The mounted app preserves its strict code-signature validity and the release artifact verifier accepts the generated DMG.",
"Generated build products, downloaded tools, and visual QA screenshots remain outside the committed change."
],
"governedPaths": [
"scripts/build-app.sh",
"scripts/dmg-requirements.txt",
"scripts/dmg-settings.py",
"scripts/generate-dmg-background.swift"
],
"artifacts": {
"intent": "intent.md",
"spec": "spec.md",
"plan": "plan.md",
"verification": "verification.md"
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
# Verification: Polish and harden the Utter DMG installer

## Evidence

The implementation was rebased onto `origin/main` at `7e0689a`. During the
implementation loop, a real Finder inspection caught two defects before PR
preparation: dark artwork made Finder labels hard to read, and hiding the
`.app` extension added FinderInfo that invalidated strict mounted-app signature
verification. Both were corrected before the latest-base verification run.

| Check | Result | Evidence |
|---|---|---|
| `python3 scripts/sdlc.py validate --worktree` | Pass | Verified bundle covers every governed changed path |
| `bash scripts/ci-basic-checks.sh` | Pass | SDLC, 13 harness tests, versioning, plist, localization, identifiers, lexicon, resources, conflicts, credentials, and symlink checks passed |
| `swift test` | Pass | 564 XCTest tests, 8 skipped, 0 failures; 1 Swift Testing test passed |
| Clean pinned tool install | Pass | `pip --require-hashes` installed `dmgbuild 1.6.7`, `ds-store 1.3.3`, and `mac-alias 2.2.3` into a new temporary directory |
| Release-style app and DMG build | Pass | `build-app.sh --version=0.0.0 --sign=-`; the built-in release artifact verifier passed |
| Mounted artifact verification | Pass | DMG checksum valid; strict deep code-signature verification passed; app is arm64; Applications resolves to `/Applications`; background contains two image representations |
| Finder layout metadata | Pass | 680 by 440 window; toolbar, sidebar, status bar, and path bar hidden; 112-point icons at `(174, 250)` and `(506, 250)`; background image enabled |
| Real Finder window | Pass with recapture limitation | The same final installer design was inspected in a real Finder window and showed readable, unclipped labels and a clear drag flow. A latest-base recapture attempt failed because ScreenCaptureKit could not start; final-artifact metadata and image representations were rechecked independently |
| `git diff --check` | Pass | No whitespace errors |

## Acceptance criteria

- Branded, readable, unclipped real Finder installation window — pass, with the
latest-base screenshot recapture limitation recorded above.
- Finder-independent, hash-pinned packaging — pass in clean temporary install
and repository checks.
- Strict mounted-app signature and release verifier — pass on the latest-base
release-style build.
- Generated products excluded from Git — pass in final worktree inspection.

## Residual risk

Independent high-risk review, configured release-identity verification, and any
Developer ID notarization run remain external/human gates. The packaging library
also emits non-fatal macOS 26 `hdiutil` deprecation warnings, and the latest-base
Finder screenshot should be spot-checked by the reviewer because the recapture
tool was unavailable.

## Decision

Ready for human review. No human approval is claimed here; approval remains a PR
review and protected-release decision.
47 changes: 33 additions & 14 deletions scripts/build-app.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@
# Requirements:
# - macOS with Xcode (full install, not just CLI tools)
# - Swift 6.0+
# - Python 3.10+ (DMG packaging only; pinned tools are cached under .build)
#

set -euo pipefail
Expand DownExpand Up@@ -65,6 +66,7 @@ done

APP_BUNDLE="${DIST_DIR}/${APP_NAME}.app"
DMG_PATH="${DIST_DIR}/${APP_NAME}-${VERSION}.dmg"
DMG_VOLUME_NAME="${APP_NAME} ${VERSION}"

# ─── Helpers ────────────────────────────────────────────────────────────────────

Expand DownExpand Up@@ -201,21 +203,38 @@ step "Creating DMG…"

rm -f "${DMG_PATH}"

DMG_TMP="${DIST_DIR}/.dmg-staging"
rm -rf "${DMG_TMP}"
mkdir -p "${DMG_TMP}"

cp -R "${APP_BUNDLE}" "${DMG_TMP}/"
ln -s /Applications "${DMG_TMP}/Applications"

hdiutil create \
-volname "${APP_NAME}" \
-srcfolder "${DMG_TMP}" \
-ov -format UDZO \
"${DMG_PATH}" \
-quiet
DMG_BACKGROUND_DIR="${DERIVED_DATA}/dmg-background"
DMG_TOOLS_DIR="${DERIVED_DATA}/dmg-tools"
DMG_TOOLS_STAMP="${DMG_TOOLS_DIR}/.utter-dmg-tools-1.6.7"

rm -rf "${DMG_BACKGROUND_DIR}"
mkdir -p "${DMG_BACKGROUND_DIR}"
swift "${SCRIPT_DIR}/generate-dmg-background.swift" \
"${DMG_BACKGROUND_DIR}/installer-background.png" 1
swift "${SCRIPT_DIR}/generate-dmg-background.swift" \
"${DMG_BACKGROUND_DIR}/installer-background@2x.png" 2

if [ ! -f "${DMG_TOOLS_STAMP}" ]; then
rm -rf "${DMG_TOOLS_DIR}"
mkdir -p "${DMG_TOOLS_DIR}"
python3 -m pip install \
--disable-pip-version-check \
--no-deps \
--only-binary=:all: \
--require-hashes \
--target "${DMG_TOOLS_DIR}" \
-r "${SCRIPT_DIR}/dmg-requirements.txt" \
-q
touch "${DMG_TOOLS_STAMP}"
fi

rm -rf "${DMG_TMP}"
PYTHONPATH="${DMG_TOOLS_DIR}" python3 -m dmgbuild \
-s "${SCRIPT_DIR}/dmg-settings.py" \
-D application="${APP_BUNDLE}" \
-D background="${DMG_BACKGROUND_DIR}/installer-background.png" \
-D volume_icon="${APP_BUNDLE}/Contents/Resources/AppIcon.icns" \
"${DMG_VOLUME_NAME}" \
"${DMG_PATH}"

done_msg "DMG created"

Expand Down
6 changes: 6 additions & 0 deletions scripts/dmg-requirements.txt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
dmgbuild==1.6.7 \
--hash=sha256:37ee5771c377beb3203d9164aae8046ffed8531c06edf9227f5788b3c599b1bf
ds-store==1.3.3 \
--hash=sha256:b92a371efbf1b4ccce2a04d1ed13fceacc4736c81ba09cf5aefb74c088160a35
mac-alias==2.2.3 \
--hash=sha256:7362b521d2132ef92f606a37abfed5fcd849ceb2f28b6f9743e014b02af92f0d
20 changes: 20 additions & 0 deletions scripts/dmg-settings.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
application = defines["application"]
background = defines["background"]
icon = defines["volume_icon"]

files = [(application, "Utter.app")]
symlinks = {"Applications": "/Applications"}
icon_locations = {
"Utter.app": (174, 250),
"Applications": (506, 250),
}

window_rect = ((120, 120), (680, 440))
icon_size = 112
text_size = 13
label_pos = "bottom"
show_icon_preview = True

format = "UDZO"
filesystem = "HFS+"
compression_level = 9
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
55 changes: 55 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/intent.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
# Intent: Polish and harden the Utter DMG installer

## Problem

The release DMG currently contains only `Utter.app` and an Applications
symlink. Finder chooses the window size, icon placement, and background, so the
installation flow looks unfinished and can vary with the packager's Finder
state. The existing path also has no Retina-aware installation artwork or
deterministic layout metadata.

## Outcome

Opening a release DMG shows a compact Utter-branded Finder window with the app
and Applications folder aligned around a clear drag arrow. Labels remain
readable in the user's current macOS appearance, and the same result is produced
locally and in release CI without scripting Finder.

## Scope

In scope: DMG background rendering, Finder window metadata, icon positions,
volume presentation, deterministic packaging dependencies, and the existing
`build-app.sh` DMG stage.

Out of scope: app runtime UI, application behavior, entitlements, signing
identity selection, notarization policy, release credentials, or installing the
app during validation.

## Constraints

- Preserve the current Swift Package and release-signing flow.
- Keep generated PNG/TIFF files, downloaded Python packages, `.app`, `.dmg`, and
screenshots out of Git.
- Use only macOS-compatible tooling available to local builders and GitHub macOS
runners; fail rather than silently ship an unstyled or unverifiable image.
- Treat local ad-hoc signing as packaging evidence, not public distribution
evidence.

## Acceptance criteria

- The real Finder window opens at 680 by 440 points with Utter and Applications
icons at the intended positions, a readable light background, and no clipped
installation copy.
- The background contains 1x and 2x representations and the DMG has a custom
volume icon.
- Packaging writes Finder layout metadata without launching or controlling
Finder.
- Packaging dependencies are version- and SHA-256-pinned and cached only below
`.build`.
- `verify-release-artifact.sh` and strict mounted-app code-signature validation
accept the locally generated artifact.

## Open questions

None. Human review still decides whether the new packaging dependency and
visual treatment are accepted for release.
33 changes: 33 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/plan.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
# Plan: Polish and harden the Utter DMG installer

## Work items

- [x] Audit the existing DMG stage and release workflow.
- [x] Render restrained 1x and 2x branded installation backgrounds.
- [x] Add deterministic Finder geometry, icon positions, Applications symlink,
and volume icon metadata without Finder automation.
- [x] Pin packaging dependencies by version and SHA-256 below the ignored build
cache.
- [x] Preserve strict mounted-app signature validity and the latest release
artifact verifier.
- [x] Re-run all repository, unit, release-build, artifact, and real-window
checks on the latest `origin/main` base.
- [x] Record final evidence and residual risk for review.

## Verification plan

- [x] `python3 scripts/sdlc.py validate --worktree`
- [x] `bash scripts/ci-basic-checks.sh`
- [x] `DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer swift test`
- [x] `DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer ./scripts/build-app.sh --version=0.0.0 --sign=-`
- [x] Inspect the mounted DMG, strict code signature, layout metadata, Retina
background representations, architecture, symlink, checksum, and real Finder
window.
- [x] `git diff --check`

## Human gates

An independent reviewer must accept the high-risk packaging/dependency change
and its rollback before merge. The protected release environment separately
owns the configured signing identity, Developer ID/notarization path, and public
release authorization.
62 changes: 62 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/spec.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
# Spec: Polish and harden the Utter DMG installer

## Context

`scripts/build-app.sh` already builds with Xcode, assembles `Utter.app`, compiles
appearance-aware app icons, signs the bundle, and verifies release artifacts.
Its previous DMG stage copied the app and Applications symlink into a staging
folder and asked `hdiutil` to compress that folder. It did not create a Finder
layout or installation background.

Finder automation can create `.DS_Store`, but it depends on interactive Finder
state and can be blocked by unrelated Finder dialogs. The packaging path must be
deterministic in both a developer session and GitHub Actions.

## Design

`generate-dmg-background.swift` renders a 680 by 440 AppKit bitmap at 1x and 2x.
The light neutral surface, subtle Utter color accents, centered installation
instruction, and arrow are positioned around the Finder icons rather than
duplicating them in the artwork.

`dmg-settings.py` owns the window rectangle, icon size, icon locations, HFS+
filesystem, UDZO compression, Applications symlink, and volume icon. The build
script installs `dmgbuild`, `ds-store`, and `mac-alias` into
`.build/xcode/dmg-tools` from `dmg-requirements.txt`. Every wheel is pinned by
version and SHA-256, and a versioned stamp avoids repeated installation.

The existing release verifier remains the final build step. No packaging code
changes signing identity selection, entitlements, notarization, or publication.

## Safety and failure modes

- A missing Python 3.10+ runtime, unavailable package index, hash mismatch, or
import failure stops the build; it does not fall back to an unstyled DMG.
- Downloaded code is isolated under ignored `.build` paths and never shipped in
`Utter.app`.
- Finder extension-hiding metadata is not written to `Utter.app`, because that
extended attribute makes strict code-signature verification fail.
- A corrupt tool cache fails during module import or DMG creation. Removing the
ignored cache allows a clean hash-verified reinstall.
- The packaging library currently emits macOS 26 deprecation warnings for its
internal `hdiutil` calls; artifact verification remains the acceptance gate.

## Test strategy

- Validate shell syntax, Python syntax, Swift background rendering, repository
policy, and the complete Swift test suite.
- Build a release-style ad-hoc app and DMG on the latest `main` base.
- Verify the DMG checksum, Applications symlink, mounted app signature, arm64
executable, 1x/2x TIFF representations, and exact `.DS_Store` geometry.
- Open the final image through Finder and inspect the actual title bar, artwork,
icon labels, spacing, arrow, and lower-edge clipping without installing or
launching the app.

## Rollout and rollback

Merge through the normal PR and release review path. The next release workflow
build exercises the same DMG stage with its configured signing identity and
artifact verifier. Stop if clean-runner dependency installation or mounted-DMG
verification fails. Roll back by reverting this change, which restores the
previous plain `hdiutil -srcfolder` image without changing app binaries or user
data.
28 changes: 28 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/state.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
{
"schemaVersion": 1,
"id": "2026-08-26-dmg-installer-polish",
"title": "Polish and harden the Utter DMG installer",
"risk": "high",
"status": "verified",
"owners": [
"repository maintainer"
],
"acceptanceCriteria": [
"Opening the DMG presents a compact branded Finder window with readable labels and an unambiguous drag-to-Applications flow.",
"The DMG layout is generated without depending on interactive Finder state and uses hash-pinned packaging tools.",
"The mounted app preserves its strict code-signature validity and the release artifact verifier accepts the generated DMG.",
"Generated build products, downloaded tools, and visual QA screenshots remain outside the committed change."
],
"governedPaths": [
"scripts/build-app.sh",
"scripts/dmg-requirements.txt",
"scripts/dmg-settings.py",
"scripts/generate-dmg-background.swift"
],
"artifacts": {
"intent": "intent.md",
"spec": "spec.md",
"plan": "plan.md",
"verification": "verification.md"
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
# Verification: Polish and harden the Utter DMG installer

## Evidence

The implementation was rebased onto `origin/main` at `7e0689a`. During the
implementation loop, a real Finder inspection caught two defects before PR
preparation: dark artwork made Finder labels hard to read, and hiding the
`.app` extension added FinderInfo that invalidated strict mounted-app signature
verification. Both were corrected before the latest-base verification run.

| Check | Result | Evidence |
|---|---|---|
| `python3 scripts/sdlc.py validate --worktree` | Pass | Verified bundle covers every governed changed path |
| `bash scripts/ci-basic-checks.sh` | Pass | SDLC, 13 harness tests, versioning, plist, localization, identifiers, lexicon, resources, conflicts, credentials, and symlink checks passed |
| `swift test` | Pass | 564 XCTest tests, 8 skipped, 0 failures; 1 Swift Testing test passed |
| Clean pinned tool install | Pass | `pip --require-hashes` installed `dmgbuild 1.6.7`, `ds-store 1.3.3`, and `mac-alias 2.2.3` into a new temporary directory |
| Release-style app and DMG build | Pass | `build-app.sh --version=0.0.0 --sign=-`; the built-in release artifact verifier passed |
| Mounted artifact verification | Pass | DMG checksum valid; strict deep code-signature verification passed; app is arm64; Applications resolves to `/Applications`; background contains two image representations |
| Finder layout metadata | Pass | 680 by 440 window; toolbar, sidebar, status bar, and path bar hidden; 112-point icons at `(174, 250)` and `(506, 250)`; background image enabled |
| Real Finder window | Pass with recapture limitation | The same final installer design was inspected in a real Finder window and showed readable, unclipped labels and a clear drag flow. A latest-base recapture attempt failed because ScreenCaptureKit could not start; final-artifact metadata and image representations were rechecked independently |
| `git diff --check` | Pass | No whitespace errors |

## Acceptance criteria

- Branded, readable, unclipped real Finder installation window — pass, with the
latest-base screenshot recapture limitation recorded above.
- Finder-independent, hash-pinned packaging — pass in clean temporary install
and repository checks.
- Strict mounted-app signature and release verifier — pass on the latest-base
release-style build.
- Generated products excluded from Git — pass in final worktree inspection.

## Residual risk

Independent high-risk review, configured release-identity verification, and any
Developer ID notarization run remain external/human gates. The packaging library
also emits non-fatal macOS 26 `hdiutil` deprecation warnings, and the latest-base
Finder screenshot should be spot-checked by the reviewer because the recapture
tool was unavailable.

## Decision

Ready for human review. No human approval is claimed here; approval remains a PR
review and protected-release decision.
47 changes: 33 additions & 14 deletions scripts/build-app.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@
# Requirements:
# - macOS with Xcode (full install, not just CLI tools)
# - Swift 6.0+
# - Python 3.10+ (DMG packaging only; pinned tools are cached under .build)
#

set -euo pipefail
Expand DownExpand Up@@ -65,6 +66,7 @@ done

APP_BUNDLE="${DIST_DIR}/${APP_NAME}.app"
DMG_PATH="${DIST_DIR}/${APP_NAME}-${VERSION}.dmg"
DMG_VOLUME_NAME="${APP_NAME} ${VERSION}"

# ─── Helpers ────────────────────────────────────────────────────────────────────

Expand DownExpand Up@@ -201,21 +203,38 @@ step "Creating DMG…"

rm -f "${DMG_PATH}"

DMG_TMP="${DIST_DIR}/.dmg-staging"
rm -rf "${DMG_TMP}"
mkdir -p "${DMG_TMP}"

cp -R "${APP_BUNDLE}" "${DMG_TMP}/"
ln -s /Applications "${DMG_TMP}/Applications"

hdiutil create \
-volname "${APP_NAME}" \
-srcfolder "${DMG_TMP}" \
-ov -format UDZO \
"${DMG_PATH}" \
-quiet
DMG_BACKGROUND_DIR="${DERIVED_DATA}/dmg-background"
DMG_TOOLS_DIR="${DERIVED_DATA}/dmg-tools"
DMG_TOOLS_STAMP="${DMG_TOOLS_DIR}/.utter-dmg-tools-1.6.7"

rm -rf "${DMG_BACKGROUND_DIR}"
mkdir -p "${DMG_BACKGROUND_DIR}"
swift "${SCRIPT_DIR}/generate-dmg-background.swift" \
"${DMG_BACKGROUND_DIR}/installer-background.png" 1
swift "${SCRIPT_DIR}/generate-dmg-background.swift" \
"${DMG_BACKGROUND_DIR}/installer-background@2x.png" 2

if [ ! -f "${DMG_TOOLS_STAMP}" ]; then
rm -rf "${DMG_TOOLS_DIR}"
mkdir -p "${DMG_TOOLS_DIR}"
python3 -m pip install \
--disable-pip-version-check \
--no-deps \
--only-binary=:all: \
--require-hashes \
--target "${DMG_TOOLS_DIR}" \
-r "${SCRIPT_DIR}/dmg-requirements.txt" \
-q
touch "${DMG_TOOLS_STAMP}"
fi

rm -rf "${DMG_TMP}"
PYTHONPATH="${DMG_TOOLS_DIR}" python3 -m dmgbuild \
-s "${SCRIPT_DIR}/dmg-settings.py" \
-D application="${APP_BUNDLE}" \
-D background="${DMG_BACKGROUND_DIR}/installer-background.png" \
-D volume_icon="${APP_BUNDLE}/Contents/Resources/AppIcon.icns" \
"${DMG_VOLUME_NAME}" \
"${DMG_PATH}"

done_msg "DMG created"

Expand Down
6 changes: 6 additions & 0 deletions scripts/dmg-requirements.txt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
dmgbuild==1.6.7 \
--hash=sha256:37ee5771c377beb3203d9164aae8046ffed8531c06edf9227f5788b3c599b1bf
ds-store==1.3.3 \
--hash=sha256:b92a371efbf1b4ccce2a04d1ed13fceacc4736c81ba09cf5aefb74c088160a35
mac-alias==2.2.3 \
--hash=sha256:7362b521d2132ef92f606a37abfed5fcd849ceb2f28b6f9743e014b02af92f0d
20 changes: 20 additions & 0 deletions scripts/dmg-settings.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
application = defines["application"]
background = defines["background"]
icon = defines["volume_icon"]

files = [(application, "Utter.app")]
symlinks = {"Applications": "/Applications"}
icon_locations = {
"Utter.app": (174, 250),
"Applications": (506, 250),
}

window_rect = ((120, 120), (680, 440))
icon_size = 112
text_size = 13
label_pos = "bottom"
show_icon_preview = True

format = "UDZO"
filesystem = "HFS+"
compression_level = 9
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
55 changes: 55 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/intent.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
# Intent: Polish and harden the Utter DMG installer

## Problem

The release DMG currently contains only `Utter.app` and an Applications
symlink. Finder chooses the window size, icon placement, and background, so the
installation flow looks unfinished and can vary with the packager's Finder
state. The existing path also has no Retina-aware installation artwork or
deterministic layout metadata.

## Outcome

Opening a release DMG shows a compact Utter-branded Finder window with the app
and Applications folder aligned around a clear drag arrow. Labels remain
readable in the user's current macOS appearance, and the same result is produced
locally and in release CI without scripting Finder.

## Scope

In scope: DMG background rendering, Finder window metadata, icon positions,
volume presentation, deterministic packaging dependencies, and the existing
`build-app.sh` DMG stage.

Out of scope: app runtime UI, application behavior, entitlements, signing
identity selection, notarization policy, release credentials, or installing the
app during validation.

## Constraints

- Preserve the current Swift Package and release-signing flow.
- Keep generated PNG/TIFF files, downloaded Python packages, `.app`, `.dmg`, and
screenshots out of Git.
- Use only macOS-compatible tooling available to local builders and GitHub macOS
runners; fail rather than silently ship an unstyled or unverifiable image.
- Treat local ad-hoc signing as packaging evidence, not public distribution
evidence.

## Acceptance criteria

- The real Finder window opens at 680 by 440 points with Utter and Applications
icons at the intended positions, a readable light background, and no clipped
installation copy.
- The background contains 1x and 2x representations and the DMG has a custom
volume icon.
- Packaging writes Finder layout metadata without launching or controlling
Finder.
- Packaging dependencies are version- and SHA-256-pinned and cached only below
`.build`.
- `verify-release-artifact.sh` and strict mounted-app code-signature validation
accept the locally generated artifact.

## Open questions

None. Human review still decides whether the new packaging dependency and
visual treatment are accepted for release.
33 changes: 33 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/plan.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
# Plan: Polish and harden the Utter DMG installer

## Work items

- [x] Audit the existing DMG stage and release workflow.
- [x] Render restrained 1x and 2x branded installation backgrounds.
- [x] Add deterministic Finder geometry, icon positions, Applications symlink,
and volume icon metadata without Finder automation.
- [x] Pin packaging dependencies by version and SHA-256 below the ignored build
cache.
- [x] Preserve strict mounted-app signature validity and the latest release
artifact verifier.
- [x] Re-run all repository, unit, release-build, artifact, and real-window
checks on the latest `origin/main` base.
- [x] Record final evidence and residual risk for review.

## Verification plan

- [x] `python3 scripts/sdlc.py validate --worktree`
- [x] `bash scripts/ci-basic-checks.sh`
- [x] `DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer swift test`
- [x] `DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer ./scripts/build-app.sh --version=0.0.0 --sign=-`
- [x] Inspect the mounted DMG, strict code signature, layout metadata, Retina
background representations, architecture, symlink, checksum, and real Finder
window.
- [x] `git diff --check`

## Human gates

An independent reviewer must accept the high-risk packaging/dependency change
and its rollback before merge. The protected release environment separately
owns the configured signing identity, Developer ID/notarization path, and public
release authorization.
62 changes: 62 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/spec.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
# Spec: Polish and harden the Utter DMG installer

## Context

`scripts/build-app.sh` already builds with Xcode, assembles `Utter.app`, compiles
appearance-aware app icons, signs the bundle, and verifies release artifacts.
Its previous DMG stage copied the app and Applications symlink into a staging
folder and asked `hdiutil` to compress that folder. It did not create a Finder
layout or installation background.

Finder automation can create `.DS_Store`, but it depends on interactive Finder
state and can be blocked by unrelated Finder dialogs. The packaging path must be
deterministic in both a developer session and GitHub Actions.

## Design

`generate-dmg-background.swift` renders a 680 by 440 AppKit bitmap at 1x and 2x.
The light neutral surface, subtle Utter color accents, centered installation
instruction, and arrow are positioned around the Finder icons rather than
duplicating them in the artwork.

`dmg-settings.py` owns the window rectangle, icon size, icon locations, HFS+
filesystem, UDZO compression, Applications symlink, and volume icon. The build
script installs `dmgbuild`, `ds-store`, and `mac-alias` into
`.build/xcode/dmg-tools` from `dmg-requirements.txt`. Every wheel is pinned by
version and SHA-256, and a versioned stamp avoids repeated installation.

The existing release verifier remains the final build step. No packaging code
changes signing identity selection, entitlements, notarization, or publication.

## Safety and failure modes

- A missing Python 3.10+ runtime, unavailable package index, hash mismatch, or
import failure stops the build; it does not fall back to an unstyled DMG.
- Downloaded code is isolated under ignored `.build` paths and never shipped in
`Utter.app`.
- Finder extension-hiding metadata is not written to `Utter.app`, because that
extended attribute makes strict code-signature verification fail.
- A corrupt tool cache fails during module import or DMG creation. Removing the
ignored cache allows a clean hash-verified reinstall.
- The packaging library currently emits macOS 26 deprecation warnings for its
internal `hdiutil` calls; artifact verification remains the acceptance gate.

## Test strategy

- Validate shell syntax, Python syntax, Swift background rendering, repository
policy, and the complete Swift test suite.
- Build a release-style ad-hoc app and DMG on the latest `main` base.
- Verify the DMG checksum, Applications symlink, mounted app signature, arm64
executable, 1x/2x TIFF representations, and exact `.DS_Store` geometry.
- Open the final image through Finder and inspect the actual title bar, artwork,
icon labels, spacing, arrow, and lower-edge clipping without installing or
launching the app.

## Rollout and rollback

Merge through the normal PR and release review path. The next release workflow
build exercises the same DMG stage with its configured signing identity and
artifact verifier. Stop if clean-runner dependency installation or mounted-DMG
verification fails. Roll back by reverting this change, which restores the
previous plain `hdiutil -srcfolder` image without changing app binaries or user
data.
28 changes: 28 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/state.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
{
"schemaVersion": 1,
"id": "2026-08-26-dmg-installer-polish",
"title": "Polish and harden the Utter DMG installer",
"risk": "high",
"status": "verified",
"owners": [
"repository maintainer"
],
"acceptanceCriteria": [
"Opening the DMG presents a compact branded Finder window with readable labels and an unambiguous drag-to-Applications flow.",
"The DMG layout is generated without depending on interactive Finder state and uses hash-pinned packaging tools.",
"The mounted app preserves its strict code-signature validity and the release artifact verifier accepts the generated DMG.",
"Generated build products, downloaded tools, and visual QA screenshots remain outside the committed change."
],
"governedPaths": [
"scripts/build-app.sh",
"scripts/dmg-requirements.txt",
"scripts/dmg-settings.py",
"scripts/generate-dmg-background.swift"
],
"artifacts": {
"intent": "intent.md",
"spec": "spec.md",
"plan": "plan.md",
"verification": "verification.md"
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
# Verification: Polish and harden the Utter DMG installer

## Evidence

The implementation was rebased onto `origin/main` at `7e0689a`. During the
implementation loop, a real Finder inspection caught two defects before PR
preparation: dark artwork made Finder labels hard to read, and hiding the
`.app` extension added FinderInfo that invalidated strict mounted-app signature
verification. Both were corrected before the latest-base verification run.

| Check | Result | Evidence |
|---|---|---|
| `python3 scripts/sdlc.py validate --worktree` | Pass | Verified bundle covers every governed changed path |
| `bash scripts/ci-basic-checks.sh` | Pass | SDLC, 13 harness tests, versioning, plist, localization, identifiers, lexicon, resources, conflicts, credentials, and symlink checks passed |
| `swift test` | Pass | 564 XCTest tests, 8 skipped, 0 failures; 1 Swift Testing test passed |
| Clean pinned tool install | Pass | `pip --require-hashes` installed `dmgbuild 1.6.7`, `ds-store 1.3.3`, and `mac-alias 2.2.3` into a new temporary directory |
| Release-style app and DMG build | Pass | `build-app.sh --version=0.0.0 --sign=-`; the built-in release artifact verifier passed |
| Mounted artifact verification | Pass | DMG checksum valid; strict deep code-signature verification passed; app is arm64; Applications resolves to `/Applications`; background contains two image representations |
| Finder layout metadata | Pass | 680 by 440 window; toolbar, sidebar, status bar, and path bar hidden; 112-point icons at `(174, 250)` and `(506, 250)`; background image enabled |
| Real Finder window | Pass with recapture limitation | The same final installer design was inspected in a real Finder window and showed readable, unclipped labels and a clear drag flow. A latest-base recapture attempt failed because ScreenCaptureKit could not start; final-artifact metadata and image representations were rechecked independently |
| `git diff --check` | Pass | No whitespace errors |

## Acceptance criteria

- Branded, readable, unclipped real Finder installation window — pass, with the
latest-base screenshot recapture limitation recorded above.
- Finder-independent, hash-pinned packaging — pass in clean temporary install
and repository checks.
- Strict mounted-app signature and release verifier — pass on the latest-base
release-style build.
- Generated products excluded from Git — pass in final worktree inspection.

## Residual risk

Independent high-risk review, configured release-identity verification, and any
Developer ID notarization run remain external/human gates. The packaging library
also emits non-fatal macOS 26 `hdiutil` deprecation warnings, and the latest-base
Finder screenshot should be spot-checked by the reviewer because the recapture
tool was unavailable.

## Decision

Ready for human review. No human approval is claimed here; approval remains a PR
review and protected-release decision.
47 changes: 33 additions & 14 deletions scripts/build-app.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@
# Requirements:
# - macOS with Xcode (full install, not just CLI tools)
# - Swift 6.0+
# - Python 3.10+ (DMG packaging only; pinned tools are cached under .build)
#

set -euo pipefail
Expand DownExpand Up@@ -65,6 +66,7 @@ done

APP_BUNDLE="${DIST_DIR}/${APP_NAME}.app"
DMG_PATH="${DIST_DIR}/${APP_NAME}-${VERSION}.dmg"
DMG_VOLUME_NAME="${APP_NAME} ${VERSION}"

# ─── Helpers ────────────────────────────────────────────────────────────────────

Expand DownExpand Up@@ -201,21 +203,38 @@ step "Creating DMG…"

rm -f "${DMG_PATH}"

DMG_TMP="${DIST_DIR}/.dmg-staging"
rm -rf "${DMG_TMP}"
mkdir -p "${DMG_TMP}"

cp -R "${APP_BUNDLE}" "${DMG_TMP}/"
ln -s /Applications "${DMG_TMP}/Applications"

hdiutil create \
-volname "${APP_NAME}" \
-srcfolder "${DMG_TMP}" \
-ov -format UDZO \
"${DMG_PATH}" \
-quiet
DMG_BACKGROUND_DIR="${DERIVED_DATA}/dmg-background"
DMG_TOOLS_DIR="${DERIVED_DATA}/dmg-tools"
DMG_TOOLS_STAMP="${DMG_TOOLS_DIR}/.utter-dmg-tools-1.6.7"

rm -rf "${DMG_BACKGROUND_DIR}"
mkdir -p "${DMG_BACKGROUND_DIR}"
swift "${SCRIPT_DIR}/generate-dmg-background.swift" \
"${DMG_BACKGROUND_DIR}/installer-background.png" 1
swift "${SCRIPT_DIR}/generate-dmg-background.swift" \
"${DMG_BACKGROUND_DIR}/installer-background@2x.png" 2

if [ ! -f "${DMG_TOOLS_STAMP}" ]; then
rm -rf "${DMG_TOOLS_DIR}"
mkdir -p "${DMG_TOOLS_DIR}"
python3 -m pip install \
--disable-pip-version-check \
--no-deps \
--only-binary=:all: \
--require-hashes \
--target "${DMG_TOOLS_DIR}" \
-r "${SCRIPT_DIR}/dmg-requirements.txt" \
-q
touch "${DMG_TOOLS_STAMP}"
fi

rm -rf "${DMG_TMP}"
PYTHONPATH="${DMG_TOOLS_DIR}" python3 -m dmgbuild \
-s "${SCRIPT_DIR}/dmg-settings.py" \
-D application="${APP_BUNDLE}" \
-D background="${DMG_BACKGROUND_DIR}/installer-background.png" \
-D volume_icon="${APP_BUNDLE}/Contents/Resources/AppIcon.icns" \
"${DMG_VOLUME_NAME}" \
"${DMG_PATH}"

done_msg "DMG created"

Expand Down
6 changes: 6 additions & 0 deletions scripts/dmg-requirements.txt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
dmgbuild==1.6.7 \
--hash=sha256:37ee5771c377beb3203d9164aae8046ffed8531c06edf9227f5788b3c599b1bf
ds-store==1.3.3 \
--hash=sha256:b92a371efbf1b4ccce2a04d1ed13fceacc4736c81ba09cf5aefb74c088160a35
mac-alias==2.2.3 \
--hash=sha256:7362b521d2132ef92f606a37abfed5fcd849ceb2f28b6f9743e014b02af92f0d
20 changes: 20 additions & 0 deletions scripts/dmg-settings.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
application = defines["application"]
background = defines["background"]
icon = defines["volume_icon"]

files = [(application, "Utter.app")]
symlinks = {"Applications": "/Applications"}
icon_locations = {
"Utter.app": (174, 250),
"Applications": (506, 250),
}

window_rect = ((120, 120), (680, 440))
icon_size = 112
text_size = 13
label_pos = "bottom"
show_icon_preview = True

format = "UDZO"
filesystem = "HFS+"
compression_level = 9
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
55 changes: 55 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/intent.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
# Intent: Polish and harden the Utter DMG installer

## Problem

The release DMG currently contains only `Utter.app` and an Applications
symlink. Finder chooses the window size, icon placement, and background, so the
installation flow looks unfinished and can vary with the packager's Finder
state. The existing path also has no Retina-aware installation artwork or
deterministic layout metadata.

## Outcome

Opening a release DMG shows a compact Utter-branded Finder window with the app
and Applications folder aligned around a clear drag arrow. Labels remain
readable in the user's current macOS appearance, and the same result is produced
locally and in release CI without scripting Finder.

## Scope

In scope: DMG background rendering, Finder window metadata, icon positions,
volume presentation, deterministic packaging dependencies, and the existing
`build-app.sh` DMG stage.

Out of scope: app runtime UI, application behavior, entitlements, signing
identity selection, notarization policy, release credentials, or installing the
app during validation.

## Constraints

- Preserve the current Swift Package and release-signing flow.
- Keep generated PNG/TIFF files, downloaded Python packages, `.app`, `.dmg`, and
screenshots out of Git.
- Use only macOS-compatible tooling available to local builders and GitHub macOS
runners; fail rather than silently ship an unstyled or unverifiable image.
- Treat local ad-hoc signing as packaging evidence, not public distribution
evidence.

## Acceptance criteria

- The real Finder window opens at 680 by 440 points with Utter and Applications
icons at the intended positions, a readable light background, and no clipped
installation copy.
- The background contains 1x and 2x representations and the DMG has a custom
volume icon.
- Packaging writes Finder layout metadata without launching or controlling
Finder.
- Packaging dependencies are version- and SHA-256-pinned and cached only below
`.build`.
- `verify-release-artifact.sh` and strict mounted-app code-signature validation
accept the locally generated artifact.

## Open questions

None. Human review still decides whether the new packaging dependency and
visual treatment are accepted for release.
33 changes: 33 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/plan.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
# Plan: Polish and harden the Utter DMG installer

## Work items

- [x] Audit the existing DMG stage and release workflow.
- [x] Render restrained 1x and 2x branded installation backgrounds.
- [x] Add deterministic Finder geometry, icon positions, Applications symlink,
and volume icon metadata without Finder automation.
- [x] Pin packaging dependencies by version and SHA-256 below the ignored build
cache.
- [x] Preserve strict mounted-app signature validity and the latest release
artifact verifier.
- [x] Re-run all repository, unit, release-build, artifact, and real-window
checks on the latest `origin/main` base.
- [x] Record final evidence and residual risk for review.

## Verification plan

- [x] `python3 scripts/sdlc.py validate --worktree`
- [x] `bash scripts/ci-basic-checks.sh`
- [x] `DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer swift test`
- [x] `DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer ./scripts/build-app.sh --version=0.0.0 --sign=-`
- [x] Inspect the mounted DMG, strict code signature, layout metadata, Retina
background representations, architecture, symlink, checksum, and real Finder
window.
- [x] `git diff --check`

## Human gates

An independent reviewer must accept the high-risk packaging/dependency change
and its rollback before merge. The protected release environment separately
owns the configured signing identity, Developer ID/notarization path, and public
release authorization.
62 changes: 62 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/spec.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
# Spec: Polish and harden the Utter DMG installer

## Context

`scripts/build-app.sh` already builds with Xcode, assembles `Utter.app`, compiles
appearance-aware app icons, signs the bundle, and verifies release artifacts.
Its previous DMG stage copied the app and Applications symlink into a staging
folder and asked `hdiutil` to compress that folder. It did not create a Finder
layout or installation background.

Finder automation can create `.DS_Store`, but it depends on interactive Finder
state and can be blocked by unrelated Finder dialogs. The packaging path must be
deterministic in both a developer session and GitHub Actions.

## Design

`generate-dmg-background.swift` renders a 680 by 440 AppKit bitmap at 1x and 2x.
The light neutral surface, subtle Utter color accents, centered installation
instruction, and arrow are positioned around the Finder icons rather than
duplicating them in the artwork.

`dmg-settings.py` owns the window rectangle, icon size, icon locations, HFS+
filesystem, UDZO compression, Applications symlink, and volume icon. The build
script installs `dmgbuild`, `ds-store`, and `mac-alias` into
`.build/xcode/dmg-tools` from `dmg-requirements.txt`. Every wheel is pinned by
version and SHA-256, and a versioned stamp avoids repeated installation.

The existing release verifier remains the final build step. No packaging code
changes signing identity selection, entitlements, notarization, or publication.

## Safety and failure modes

- A missing Python 3.10+ runtime, unavailable package index, hash mismatch, or
import failure stops the build; it does not fall back to an unstyled DMG.
- Downloaded code is isolated under ignored `.build` paths and never shipped in
`Utter.app`.
- Finder extension-hiding metadata is not written to `Utter.app`, because that
extended attribute makes strict code-signature verification fail.
- A corrupt tool cache fails during module import or DMG creation. Removing the
ignored cache allows a clean hash-verified reinstall.
- The packaging library currently emits macOS 26 deprecation warnings for its
internal `hdiutil` calls; artifact verification remains the acceptance gate.

## Test strategy

- Validate shell syntax, Python syntax, Swift background rendering, repository
policy, and the complete Swift test suite.
- Build a release-style ad-hoc app and DMG on the latest `main` base.
- Verify the DMG checksum, Applications symlink, mounted app signature, arm64
executable, 1x/2x TIFF representations, and exact `.DS_Store` geometry.
- Open the final image through Finder and inspect the actual title bar, artwork,
icon labels, spacing, arrow, and lower-edge clipping without installing or
launching the app.

## Rollout and rollback

Merge through the normal PR and release review path. The next release workflow
build exercises the same DMG stage with its configured signing identity and
artifact verifier. Stop if clean-runner dependency installation or mounted-DMG
verification fails. Roll back by reverting this change, which restores the
previous plain `hdiutil -srcfolder` image without changing app binaries or user
data.
28 changes: 28 additions & 0 deletions docs/sdlc/changes/2026-08-26-dmg-installer-polish/state.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
{
"schemaVersion": 1,
"id": "2026-08-26-dmg-installer-polish",
"title": "Polish and harden the Utter DMG installer",
"risk": "high",
"status": "verified",
"owners": [
"repository maintainer"
],
"acceptanceCriteria": [
"Opening the DMG presents a compact branded Finder window with readable labels and an unambiguous drag-to-Applications flow.",
"The DMG layout is generated without depending on interactive Finder state and uses hash-pinned packaging tools.",
"The mounted app preserves its strict code-signature validity and the release artifact verifier accepts the generated DMG.",
"Generated build products, downloaded tools, and visual QA screenshots remain outside the committed change."
],
"governedPaths": [
"scripts/build-app.sh",
"scripts/dmg-requirements.txt",
"scripts/dmg-settings.py",
"scripts/generate-dmg-background.swift"
],
"artifacts": {
"intent": "intent.md",
"spec": "spec.md",
"plan": "plan.md",
"verification": "verification.md"
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
# Verification: Polish and harden the Utter DMG installer

## Evidence

The implementation was rebased onto `origin/main` at `7e0689a`. During the
implementation loop, a real Finder inspection caught two defects before PR
preparation: dark artwork made Finder labels hard to read, and hiding the
`.app` extension added FinderInfo that invalidated strict mounted-app signature
verification. Both were corrected before the latest-base verification run.

| Check | Result | Evidence |
|---|---|---|
| `python3 scripts/sdlc.py validate --worktree` | Pass | Verified bundle covers every governed changed path |
| `bash scripts/ci-basic-checks.sh` | Pass | SDLC, 13 harness tests, versioning, plist, localization, identifiers, lexicon, resources, conflicts, credentials, and symlink checks passed |
| `swift test` | Pass | 564 XCTest tests, 8 skipped, 0 failures; 1 Swift Testing test passed |
| Clean pinned tool install | Pass | `pip --require-hashes` installed `dmgbuild 1.6.7`, `ds-store 1.3.3`, and `mac-alias 2.2.3` into a new temporary directory |
| Release-style app and DMG build | Pass | `build-app.sh --version=0.0.0 --sign=-`; the built-in release artifact verifier passed |
| Mounted artifact verification | Pass | DMG checksum valid; strict deep code-signature verification passed; app is arm64; Applications resolves to `/Applications`; background contains two image representations |
| Finder layout metadata | Pass | 680 by 440 window; toolbar, sidebar, status bar, and path bar hidden; 112-point icons at `(174, 250)` and `(506, 250)`; background image enabled |
| Real Finder window | Pass with recapture limitation | The same final installer design was inspected in a real Finder window and showed readable, unclipped labels and a clear drag flow. A latest-base recapture attempt failed because ScreenCaptureKit could not start; final-artifact metadata and image representations were rechecked independently |
| `git diff --check` | Pass | No whitespace errors |

## Acceptance criteria

- Branded, readable, unclipped real Finder installation window — pass, with the
latest-base screenshot recapture limitation recorded above.
- Finder-independent, hash-pinned packaging — pass in clean temporary install
and repository checks.
- Strict mounted-app signature and release verifier — pass on the latest-base
release-style build.
- Generated products excluded from Git — pass in final worktree inspection.

## Residual risk

Independent high-risk review, configured release-identity verification, and any
Developer ID notarization run remain external/human gates. The packaging library
also emits non-fatal macOS 26 `hdiutil` deprecation warnings, and the latest-base
Finder screenshot should be spot-checked by the reviewer because the recapture
tool was unavailable.

## Decision

Ready for human review. No human approval is claimed here; approval remains a PR
review and protected-release decision.
47 changes: 33 additions & 14 deletions scripts/build-app.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@
# Requirements:
# - macOS with Xcode (full install, not just CLI tools)
# - Swift 6.0+
# - Python 3.10+ (DMG packaging only; pinned tools are cached under .build)
#

set -euo pipefail
Expand DownExpand Up@@ -65,6 +66,7 @@ done

APP_BUNDLE="${DIST_DIR}/${APP_NAME}.app"
DMG_PATH="${DIST_DIR}/${APP_NAME}-${VERSION}.dmg"
DMG_VOLUME_NAME="${APP_NAME} ${VERSION}"

# ─── Helpers ────────────────────────────────────────────────────────────────────

Expand DownExpand Up@@ -201,21 +203,38 @@ step "Creating DMG…"

rm -f "${DMG_PATH}"

DMG_TMP="${DIST_DIR}/.dmg-staging"
rm -rf "${DMG_TMP}"
mkdir -p "${DMG_TMP}"

cp -R "${APP_BUNDLE}" "${DMG_TMP}/"
ln -s /Applications "${DMG_TMP}/Applications"

hdiutil create \
-volname "${APP_NAME}" \
-srcfolder "${DMG_TMP}" \
-ov -format UDZO \
"${DMG_PATH}" \
-quiet
DMG_BACKGROUND_DIR="${DERIVED_DATA}/dmg-background"
DMG_TOOLS_DIR="${DERIVED_DATA}/dmg-tools"
DMG_TOOLS_STAMP="${DMG_TOOLS_DIR}/.utter-dmg-tools-1.6.7"

rm -rf "${DMG_BACKGROUND_DIR}"
mkdir -p "${DMG_BACKGROUND_DIR}"
swift "${SCRIPT_DIR}/generate-dmg-background.swift" \
"${DMG_BACKGROUND_DIR}/installer-background.png" 1
swift "${SCRIPT_DIR}/generate-dmg-background.swift" \
"${DMG_BACKGROUND_DIR}/installer-background@2x.png" 2

if [ ! -f "${DMG_TOOLS_STAMP}" ]; then
rm -rf "${DMG_TOOLS_DIR}"
mkdir -p "${DMG_TOOLS_DIR}"
python3 -m pip install \
--disable-pip-version-check \
--no-deps \
--only-binary=:all: \
--require-hashes \
--target "${DMG_TOOLS_DIR}" \
-r "${SCRIPT_DIR}/dmg-requirements.txt" \
-q
touch "${DMG_TOOLS_STAMP}"
fi

rm -rf "${DMG_TMP}"
PYTHONPATH="${DMG_TOOLS_DIR}" python3 -m dmgbuild \
-s "${SCRIPT_DIR}/dmg-settings.py" \
-D application="${APP_BUNDLE}" \
-D background="${DMG_BACKGROUND_DIR}/installer-background.png" \
-D volume_icon="${APP_BUNDLE}/Contents/Resources/AppIcon.icns" \
"${DMG_VOLUME_NAME}" \
"${DMG_PATH}"

done_msg "DMG created"

Expand Down
6 changes: 6 additions & 0 deletions scripts/dmg-requirements.txt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
dmgbuild==1.6.7 \
--hash=sha256:37ee5771c377beb3203d9164aae8046ffed8531c06edf9227f5788b3c599b1bf
ds-store==1.3.3 \
--hash=sha256:b92a371efbf1b4ccce2a04d1ed13fceacc4736c81ba09cf5aefb74c088160a35
mac-alias==2.2.3 \
--hash=sha256:7362b521d2132ef92f606a37abfed5fcd849ceb2f28b6f9743e014b02af92f0d
20 changes: 20 additions & 0 deletions scripts/dmg-settings.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
application = defines["application"]
background = defines["background"]
icon = defines["volume_icon"]

files = [(application, "Utter.app")]
symlinks = {"Applications": "/Applications"}
icon_locations = {
"Utter.app": (174, 250),
"Applications": (506, 250),
}

window_rect = ((120, 120), (680, 440))
icon_size = 112
text_size = 13
label_pos = "bottom"
show_icon_preview = True

format = "UDZO"
filesystem = "HFS+"
compression_level = 9
Loading
Loading