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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .codex/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ command = "./Scripts/run-swansong-mcp.sh"
cwd = "."
startup_timeout_sec = 120
tool_timeout_sec = 1800
default_tools_approval_mode = "prompt"
default_tools_approval_mode = "auto"

[mcp_servers.swansong.tools.swansong_status]
approval_mode = "auto"
10 changes: 10 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,13 @@
Engine/*.patch -whitespace
testroms/** -whitespace
Dependencies/SPARKLE_LICENSE -whitespace

# Story Forge is developed in this monorepo, while the signed desktop app ships
# only its reusable framework. Keep the large game/art/reference library in Git
# and out of SwanSong's corresponding-source release archive.
StoryForge/assets/ export-ignore
StoryForge/audio/ export-ignore
StoryForge/games/ export-ignore
StoryForge/novels/ export-ignore
StoryForge/projects/ export-ignore
StoryForge/release-materials/ export-ignore
4 changes: 2 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
persist-credentials: false

- name: Initialize CodeQL
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4
uses: github/codeql-action/init@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5
with:
languages: ${{ matrix.language }}
build-mode: manual
Expand All @@ -49,6 +49,6 @@ jobs:
--product SwanSongChecks

- name: Analyze
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4
uses: github/codeql-action/analyze@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5
with:
category: "/language:${{ matrix.language }}"
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ Evidence/
*.swsdiag
*.ram.bin

# Story Forge now lives in this repository. Its own nested .gitignore keeps
# ROMs, release archives, runtime mirrors, and stale evidence out of Git.
!StoryForge/
!StoryForge/games/
!StoryForge/games/**

# The repository's small, open-source fixtures are intentional exceptions.
!testroms/**/*.ws
!testroms/**/*.wsc
Expand Down
38 changes: 37 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,41 @@ records those user-visible changes. Published releases use semantic versioning.

## [Unreleased]

## [0.9.4] - 2026-08-03

**The Forge Comes Home.** SwanSong 0.9.4 gives Story Forge one inspectable
source home and carries its fixed writing framework inside the signed app.

### Added

- Bundles a minimal, hash-manifested Story Forge schema-v3 framework, including
its typed writing commands and complete `forge-light-novels` skill, so normal
use no longer depends on a separate framework checkout.
- Adds an authenticated Original-frame seal before private source-provenance
work and a bounded data-producer trace that can prove one exact cartridge
source without returning SRAM bytes or private producer coordinates.
- Adds bounded multi-rectangle source contexts for layouts that cannot be
represented honestly by one rectangle.

### Changed

- Makes `StoryForge/` the canonical source home for the framework, authored
workshop, approved art, games, and current evidence while keeping games,
manuscripts, ROMs, and release archives out of the signed Mac app.
- Gives the explicitly enabled, owner-only local automation server persistent
access to its fixed project-writing allowlist without repeated per-call
prompts; path, schema, size, identity, and privacy checks remain enforced.
- Expands the live engine provenance contract and clean-room fixtures for the
exact Original-frame, General DMA, atomic-region, and data-producer lanes.

### Fixed

- Verifies the included Story Forge payload during build, packaging, archive
validation, signature checks, and public artifact verification so missing,
changed, linked, extra, or wrongly identified framework files fail closed.
- Updates the bundled Sparkle framework to 2.9.5, including its complete
destination-symlink hardening for delta updates.

## [0.9.3] - 2026-07-31

**Keep the Proof Moving.** SwanSong 0.9.3 removes two runtime dead ends from
Expand Down Expand Up @@ -821,7 +856,8 @@ silicon and Intel Macs, with the first local Translation workflow built in.
- Private translation artifacts are bounded, owner-only, link-checked, and
validated again at write boundaries.

[Unreleased]: https://github.com/RegionallyFamous/SwanSong-Desktop/compare/v0.9.3...HEAD
[Unreleased]: https://github.com/RegionallyFamous/SwanSong-Desktop/compare/v0.9.4...HEAD
[0.9.4]: https://github.com/RegionallyFamous/SwanSong-Desktop/compare/v0.9.3...v0.9.4
[0.9.3]: https://github.com/RegionallyFamous/SwanSong-Desktop/compare/v0.9.2...v0.9.3
[0.9.2]: https://github.com/RegionallyFamous/SwanSong-Desktop/compare/v0.9.1...v0.9.2
[0.9.1]: https://github.com/RegionallyFamous/SwanSong-Desktop/compare/v0.9.0...v0.9.1
Expand Down
4 changes: 2 additions & 2 deletions Dependencies/THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ Source revision: `449b93716fb162632de2fd43bf2eba2064fa43f2`.

## Sparkle 2

SwanSong uses Sparkle for signed application updates. Sparkle 2.9.4 is pinned
to source revision `b6496a74a087257ef5e6da1c5b29a447a60f5bd7`. Its complete license and
SwanSong uses Sparkle for signed application updates. Sparkle 2.9.5 is pinned
to source revision `79bc9e872948e47877e76f194cb0c8e0412b0b90`. Its complete license and
bundled third-party notices are reproduced in `SPARKLE_LICENSE` and in the
application's Resources directory. Exact source is included in every official
corresponding-source archive under `Dependencies/sparkle-source`.
Expand Down
6 changes: 3 additions & 3 deletions Dependencies/sparkle.lock.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"repository": "https://github.com/sparkle-project/Sparkle.git",
"version": "2.9.4",
"commit": "b6496a74a087257ef5e6da1c5b29a447a60f5bd7",
"swiftPackageArtifactSHA256": "cb6fdbdc8884f15d62a616e79face92b08322410fd2d425edc6596ccbf4ba3b0"
"version": "2.9.5",
"commit": "79bc9e872948e47877e76f194cb0c8e0412b0b90",
"swiftPackageArtifactSHA256": "34b9b2071f3de0012eca3faa3a9290bb94e62131e9a74f6dc91514a000097a6c"
}
110 changes: 109 additions & 1 deletion Engine/Tests/ares_smoke.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ int main(int argc, char** argv) {
SWAN_CAPABILITY_DISPLAY_SPRITE_ATTRIBUTE_PROVENANCE) != 0;
const bool consumed_prefetch_provenance_ok =
(capabilities & SWAN_CAPABILITY_CONSUMED_PREFETCH_PROVENANCE) != 0;
const bool data_producer_provenance_ok =
(capabilities & SWAN_CAPABILITY_DATA_PRODUCER_PROVENANCE) != 0;

if (!backend_ok || !execution_ok || !audio_ok || !provenance_ok ||
!source_provenance_ok || !source_selection_ok || !source_read_context_ok ||
Expand All @@ -78,9 +80,11 @@ int main(int argc, char** argv) {
std::strcmp(argv[1], "--dma-provenance-fixture") == 0;
const bool input_frame_fixture =
argc == 3 && std::strcmp(argv[1], "--input-frame-fixture") == 0;
const bool data_producer_fixture =
argc == 3 && std::strcmp(argv[1], "--data-producer-fixture") == 0;
const char* rom_path = provenance_fixture || mono_palette_fixture ||
mapper_window_fixture || static_analysis_seed_v2_fixture ||
dma_provenance_fixture || input_frame_fixture
dma_provenance_fixture || input_frame_fixture || data_producer_fixture
? argv[2] : (argc > 1 ? argv[1] : nullptr);
const uint8_t expected_raster_bytes = provenance_fixture
? static_cast<uint8_t>(std::strcmp(argv[3], "packed") == 0 ? 1 : 4)
Expand Down Expand Up @@ -124,6 +128,27 @@ int main(int argc, char** argv) {
return 1;
}

swan_data_producer_probe_options_t data_producer_options{};
if (data_producer_fixture) {
if (!data_producer_provenance_ok) {
std::fputs("ares did not advertise bounded data-producer provenance\n",
stderr);
swan_engine_destroy(engine);
return 1;
}
data_producer_options.struct_size = sizeof(data_producer_options);
data_producer_options.address = 0x13456;
data_producer_options.byte_count = 6;
result = swan_engine_begin_data_producer_probe(
engine, &data_producer_options);
if (result != SWAN_RESULT_OK) {
std::fprintf(stderr, "could not arm data-producer provenance: %s\n",
swan_engine_last_error(engine));
swan_engine_destroy(engine);
return 1;
}
}

size_t staged_readback_size = 0;
result = swan_engine_persistence_size(
engine, SWAN_PERSISTENCE_CONSOLE_EEPROM, &staged_readback_size);
Expand Down Expand Up @@ -174,6 +199,89 @@ int main(int argc, char** argv) {
}
}

if (data_producer_fixture) {
size_t trace_count = 0;
result = swan_engine_data_producer_probe(
engine, &data_producer_options, nullptr, 0, &trace_count);
std::vector<swan_data_producer_trace_t> traces(trace_count);
size_t written = 0;
if (result == SWAN_RESULT_OK) {
result = swan_engine_data_producer_probe(
engine, &data_producer_options,
traces.data(), traces.size(), &written);
}
constexpr std::array<uint8_t, 6> kExpectedSource = {
0x31, 0x7a, 0xc4, 0x09, 0xe2, 0x5d,
};
const auto source = std::search(
rom.begin(), rom.end(),
kExpectedSource.begin(), kExpectedSource.end());
const auto duplicate = source == rom.end() ? rom.end() : std::search(
source + kExpectedSource.size(), rom.end(),
kExpectedSource.begin(), kExpectedSource.end());
const uint32_t source_lower = source == rom.end()
? UINT32_MAX : static_cast<uint32_t>(source - rom.begin());
const uint32_t source_upper = source_lower + kExpectedSource.size();
std::array<bool, 6> covered{};
std::optional<uint32_t> writer;
bool exact = result == SWAN_RESULT_OK && trace_count == written &&
trace_count >= kExpectedSource.size() &&
trace_count <= data_producer_options.byte_count * 8 &&
source != rom.end() && duplicate == rom.end();
for (const auto& trace : traces) {
if (trace.struct_size != sizeof(trace) ||
trace.target_byte_count != 1 ||
trace.target_address < data_producer_options.address ||
trace.target_address >= data_producer_options.address +
data_producer_options.byte_count) {
exact = false;
continue;
}
const uint64_t range_upper =
static_cast<uint64_t>(trace.cartridge_offset) +
trace.cartridge_length;
if (trace.cartridge_offset >= source_upper ||
source_lower >= range_upper) continue;
const uint32_t index = trace.target_address -
data_producer_options.address;
covered[index] = true;
const uint32_t required =
static_cast<uint32_t>(SWAN_DATA_PRODUCER_FLAG_WRITER_PRESENT) |
static_cast<uint32_t>(SWAN_DISPLAY_SOURCE_FLAG_EXACT);
const uint32_t forbidden =
static_cast<uint32_t>(
SWAN_DATA_PRODUCER_FLAG_NO_CARTRIDGE_SOURCE) |
static_cast<uint32_t>(
SWAN_DISPLAY_SOURCE_FLAG_UNKNOWN_DEPENDENCY) |
static_cast<uint32_t>(SWAN_DISPLAY_SOURCE_FLAG_RANGE_OVERFLOW) |
static_cast<uint32_t>(
SWAN_DISPLAY_SOURCE_FLAG_CONSERVATIVE_DATAFLOW);
exact = exact && (trace.flags & required) == required &&
(trace.flags & forbidden) == 0 &&
trace.read_context_flags ==
SWAN_DISPLAY_SOURCE_READ_CONTEXT_EXECUTED &&
trace.read_context_initiator ==
SWAN_DISPLAY_SOURCE_READ_INITIATOR_CPU;
if (!writer) writer = trace.writer_pc;
exact = exact && *writer == trace.writer_pc;
}
exact = exact && std::all_of(
covered.begin(), covered.end(), [](bool value) { return value; });
if (!exact) {
std::fprintf(
stderr,
"data-producer provenance was incomplete result=%u count=%zu/%zu source=%08x\n",
result, trace_count, written, source_lower);
swan_engine_destroy(engine);
return 1;
}
std::printf(
"PASS data-producer provenance target-bytes=6 source-bytes=6 writers=1 raw-memory=0 source=%08x\n",
source_lower);
swan_engine_destroy(engine);
return 0;
}

if (dma_provenance_fixture) {
if (frame_width != 237 || frame_height != 144) {
std::fputs("general-DMA fixture exposed the wrong native frame\n", stderr);
Expand Down
19 changes: 18 additions & 1 deletion Engine/ares-headless.patch
Original file line number Diff line number Diff line change
Expand Up @@ -1958,6 +1958,22 @@ index 8bd2d35d..599dc370 100644

if(has.flash && (flash.idmode || flash.fastmode)) {
n16 data = flash.read(offset >> 1, true);
diff --git a/ares/ws/cartridge/memory.cpp b/ares/ws/cartridge/memory.cpp
index 8bd2d35d..c85d3ee7 100644
--- a/ares/ws/cartridge/memory.cpp
+++ b/ares/ws/cartridge/memory.cpp
@@ -40,6 +40,11 @@ auto Cartridge::readRAM(n20 address) -> n8 {
}

auto Cartridge::writeRAM(n20 address, n8 data) -> void {
+ if(platform) {
+ platform->wonderSwanSourceWrite(
+ 3, address, (((u32)cpu.PS << 4) + cpu.PC) & 0xfffff
+ );
+ }
openbus.byte(0) = data;

n32 offset = io.sramBank << 16 | (n16)address;
diff --git a/ares/ws/cartridge/rtc.cpp b/ares/ws/cartridge/rtc.cpp
index f4ca9df5..df0fbb59 100644
--- a/ares/ws/cartridge/rtc.cpp
Expand Down Expand Up @@ -2014,7 +2030,7 @@ index 50e94ec5..1bf89070 100644
+ case 0x0:
+ return platform->wonderSwanDataRead(1, address & 0xffff);
+ case 0x1:
+ return platform->wonderSwanDataRead(0, address);
+ return platform->wonderSwanDataRead(3, address);
+ default:
+ if(cpu.io.cartridgeRomWidth != cartridge.has.wordWidth) {
+ if(cartridge.has.wordWidth) address &= ~1;
Expand Down Expand Up @@ -6515,3 +6531,4 @@ zv<>koMoUb!N7WJr#@AL_T8db1)@m7mhZYhbkfhCK8t4OHtu_YQZ(0T)xFc|Y2YjWa
z0FTOT5e6W9vZ;Pa^uCah0N=IDbbHhc!IzfOH1Go}1#~pJ(^?wggV%^2Y$uF=CmO#w
cGJeDcB!^qIF_H1dqc({BZyr4OW^w-i174kF(*OVf

# End of SwanSong's headless ares patch.
9 changes: 5 additions & 4 deletions PRIVACY.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# SwanSong privacy policy

Effective July 29, 2026
Effective August 3, 2026

This policy describes SwanSong 0.9.3. Versioned release notes document the
This policy describes SwanSong 0.9.4. Versioned release notes document the
exact behavior of earlier published builds.

<!-- homebrew-catalog-status: published -->
Expand Down Expand Up @@ -183,8 +183,9 @@ only while the app is in the background, and includes only the task name and
result. Project paths, ROM names, diagnostics, frames, audio, and evidence are
excluded.

Story Forge reads and writes only the framework repository, catalog, and novel
folders you explicitly select. Its fixed local command
Story Forge reads its hash-verified framework from the signed app and reads and
writes only catalog and novel folders you explicitly select. A development
override may use another explicitly selected framework folder. Its fixed local command
allowlist can create a project; validate stages; prepare proposal-only Story
Room packets; build maps and live scene context; explicitly save manuscripts;
create immutable revisions and decisions; exchange consented reader packets;
Expand Down
6 changes: 3 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ let package = Package(
dependencies: [
.package(
url: "https://github.com/sparkle-project/Sparkle.git",
exact: "2.9.4"
exact: "2.9.5"
),
],
targets: [
Expand Down
4 changes: 2 additions & 2 deletions Packaging/EngineService-Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>0.9.3</string>
<string>0.9.4</string>
<key>CFBundleVersion</key>
<string>21</string>
<string>22</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>NSAppTransportSecurity</key>
Expand Down
4 changes: 2 additions & 2 deletions Packaging/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.9.3</string>
<string>0.9.4</string>
<key>CFBundleVersion</key>
<string>21</string>
<string>22</string>
<key>GCSupportedGameControllers</key>
<array>
<dict>
Expand Down
Loading
Loading