Skip to content

Use local MistKit in CelestraCloud and update workflows for main branch#36

Draft
leogdion wants to merge 45 commits into
mainfrom
mistkit
Draft

Use local MistKit in CelestraCloud and update workflows for main branch#36
leogdion wants to merge 45 commits into
mainfrom
mistkit

Conversation

@leogdion

@leogdion leogdion commented Feb 5, 2026

Copy link
Copy Markdown
Member

This change aligns CelestraCloud with BushelCloud's pattern for MistKit dependency management:

Changes:

  • Package.swift: Switch from remote URL to local path dependency (.package(name: "MistKit", path: "../.."))
  • CelestraCloud.yml: Add sed substitution steps for Ubuntu (sed -i) and macOS (sed -i '') to replace with remote main branch in CI
  • codeql.yml: Add sed substitution step for macOS security scanning
  • update-feeds.yml: Add sed step and update cache key to exclude Package.resolved
  • update-subrepo.sh: Add informational message about local MistKit dependencies

Benefits:

  • Local development uses ../.. for tight MistKit integration
  • CI tests against MistKit's main branch (not version tags)
  • Catches breaking changes early
  • Package.resolved regenerated in CI with main branch dependency

Perform an AI-assisted review on CodePeer.com

This change aligns CelestraCloud with BushelCloud's pattern for MistKit
dependency management:

Changes:
- Package.swift: Switch from remote URL to local path dependency
  (.package(name: "MistKit", path: "../.."))
- CelestraCloud.yml: Add sed substitution steps for Ubuntu (sed -i) and
  macOS (sed -i '') to replace with remote main branch in CI
- codeql.yml: Add sed substitution step for macOS security scanning
- update-feeds.yml: Add sed step and update cache key to exclude
  Package.resolved
- update-subrepo.sh: Add informational message about local MistKit
  dependencies

Benefits:
- Local development uses ../.. for tight MistKit integration
- CI tests against MistKit's main branch (not version tags)
- Catches breaking changes early
- Package.resolved regenerated in CI with main branch dependency

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Feb 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: db690a30-120f-4c4f-9092-8e1e688eeb00

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mistkit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Feb 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 65.21739% with 128 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.30%. Comparing base (8f7fb66) to head (54c82be).

Files with missing lines Patch % Lines
...raCloudKit/Configuration/ConfigurationLoader.swift 0.00% 54 Missing ⚠️
...raCloudKit/Protocols/CloudKitRecordOperating.swift 0.00% 31 Missing ⚠️
...aCloudTests/Mocks/MockCloudKitRecordOperator.swift 64.86% 13 Missing ⚠️
...raCloudKit/Services/CloudKitService+Celestra.swift 0.00% 12 Missing ⚠️
...estraCloudKit/Models/UpdateReport+JSONOutput.swift 0.00% 6 Missing ⚠️
Sources/CelestraCloudKit/Models/UpdateReport.swift 0.00% 5 Missing ⚠️
...rces/CelestraCloudKit/Services/CelestraError.swift 88.09% 5 Missing ⚠️
Sources/CelestraCloudKit/CelestraConfig.swift 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #36      +/-   ##
==========================================
+ Coverage   86.55%   89.30%   +2.74%     
==========================================
  Files          43       48       +5     
  Lines        2857     2888      +31     
==========================================
+ Hits         2473     2579     +106     
+ Misses        384      309      -75     
Flag Coverage Δ
spm 89.30% <65.21%> (+2.74%) ⬆️
swift-6.2-jammy 89.30% <65.21%> (+2.74%) ⬆️
swift-6.2-noble 89.30% <65.21%> (+2.74%) ⬆️
swift-6.3-jammy 89.30% <65.21%> (?)
swift-6.3-jammynightly ?
swift-6.3-noble 89.30% <65.21%> (?)
swift-6.3-noblenightly ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@claude

claude Bot commented Feb 5, 2026

Copy link
Copy Markdown

Pull Request Review

This PR successfully implements a local-to-remote MistKit dependency pattern for development and CI workflows. Overall, the changes are well-structured and follow the BushelCloud pattern as described.

✅ Strengths

  1. Consistent Pattern Application: The sed substitution is correctly applied across all CI workflows with appropriate platform-specific flags
  2. Proper Timing: Package.swift substitution happens at the right stage in each workflow
  3. Cache Key Optimization: update-feeds.yml cache key correctly excludes Package.resolved
  4. Complete Coverage: All workflows that build Swift code are updated

🔍 Areas for Consideration

1. Windows Build Strategy Change

Location: .github/workflows/CelestraCloud.yml:49

The PR removes fail-fast: false from the Windows build matrix. This means if Windows 2022 fails, Windows 2025 won't run. The macOS build matrix still has fail-fast: false at line 82. If this was unintentional, restore it for consistency.

2. Error Handling in sed Commands

The sed commands don't have error handling. If the pattern doesn't match, the build silently continues with the wrong dependency. Consider adding verification after substitution.

3. Package.resolved Committed State

The PR commits Package.resolved with MistKit removed. This is correct for local path dependencies but worth documenting in contributing guidelines.

🚀 Recommendation

Approve with minor suggestions. Core changes are solid. Only potential issue is the fail-fast removal. Otherwise ready to merge.

Great work aligning CelestraCloud with the BushelCloud dependency pattern! 🎉

…(#192)

The IN and NOT_IN filters were stripping type metadata from list items,
causing CloudKit Web Services to reject queries with HTTP 400 errors.

Changes:
- Fix FilterBuilder to use CustomFieldValuePayload preserving types
- Re-enable GUID queries in CelestraCloud ArticleSyncService
- Optimize ArticleCloudKitService to combine filters at query time
- Update tests to reflect server-side filtering behavior

All tests passing: MistKit (311/311), CelestraCloud (115/115)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@claude

claude Bot commented Apr 14, 2026

Copy link
Copy Markdown

PR Review

Overall this is a solid approach that mirrors the BushelCloud pattern for tight local MistKit development. The re-enablement of GUID-based server-side filtering is a meaningful correctness improvement. A few things worth addressing before merging:


Bug: Copy-paste errors in setup-cloudkit-schema.sh

The new --help output and error messages reference Bushel instead of Celestra:

# Line added in help text:
echo "  CLOUDKIT_CONTAINER_ID   CloudKit container ID (default: iCloud.com.brightdigit.Bushel)"
# Should be: iCloud.com.brightdigit.Celestra

# Line added in import failure instructions:
echo "     d. Store it securely (e.g., ~/.cloudkit/bushel-private-key.pem)"
# Should be: celestra-private-key.pem

These look like copy-paste artifacts from BushelCloud.


Concern: unsafeFlags silently commented out

The PR description doesn't mention this change, but Package.swift comments out the entire .unsafeFlags block:

// .unsafeFlags([
//   "-warn-concurrency",
//   "-enable-actor-data-race-checks",
//   "-strict-concurrency=complete",
//   "-enable-testing",
//   ...
// ])

CLAUDE.md explicitly lists -strict-concurrency=complete as an active project setting and requires concurrency-safe code with proper actor isolation. Was this intentional (e.g., MistKit main branch has incompatibilities with these flags)? If so, it should be called out in the PR description. If this was inadvertent, these should be restored.


Minor: fail-fast: false removed from Windows matrix

-      fail-fast: false
       matrix:
         runs-on: [windows-2022, windows-2025]

This wasn't mentioned in the PR description. Without fail-fast: false, a failure on windows-2022 will cancel the windows-2025 run, losing visibility into whether the failure is runner-specific. This looks unintentional.


Positive changes

  • Re-enabling GUID deduplication (ArticleSyncService.swift): The previously commented-out queryArticlesByGUIDs call is properly restored now that MistKit issue #192 is fixed. This restores the intended deduplication behavior.
  • Server-side feedRecordName filtering (ArticleCloudKitService.swift): Moving the filter from in-memory post-processing to a query-time EQUALS filter is more efficient and reduces unnecessary data transfer.
  • Test updates: The test correctly reflects the new 2-filter expectation (IN + EQUALS) and the simplified mock setup.
  • --dry-run flag: A useful addition for validating schema changes before committing them.
  • CI substitution approach: The platform-aware sed / PowerShell steps are correct — sed -i for Ubuntu, sed -i '' for macOS (confirmed the CodeQL job runs on macos-26), and PowerShell for Windows.

Replaces hardcoded sed/PowerShell inline steps with a reusable composite
action (.github/actions/setup-mistkit/action.yml) in each example subrepo.
The branch input defaults to empty (no swap); workflows pass branch: main
explicitly. To target a different MistKit branch, update the with: input
in the relevant workflow step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Branch is now set once in each subrepo's action.yml default rather than
repeated across every workflow file. Workflows call setup-mistkit with
no arguments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@claude

claude Bot commented Apr 14, 2026

Copy link
Copy Markdown

Code Review

Note: This PR is currently in DRAFT state. Review provided for early feedback.

Overview

This PR bundles two concerns:

  1. Dependency pattern change — Switches MistKit from a pinned remote version to a local path dependency (../..) with a CI composite action that substitutes the remote branch before building.
  2. Bug fix — Re-enables the previously disabled GUID-based deduplication query in ArticleSyncService by moving the feedRecordName filter from in-memory to query time (resolving a CloudKit .in() combination limitation fixed in MistKit issue #192).

Issues

Bugs: Copy-Paste Errors from BushelCloud

  • Scripts/setup-cloudkit-schema.sh help text: The CLOUDKIT_CONTAINER_ID default shown reads iCloud.com.brightdigit.Bushel — should be iCloud.com.brightdigit.Celestra.
  • Scripts/setup-cloudkit-schema.sh success message: New line says ~/.cloudkit/bushel-private-key.pem — should reference celestra not bushel.

setup-mistkit action: default branch is a feature branch, not main

The action's default branch is 192-query-filter-in, not main. The PR description says "CI tests against MistKit's main branch" — this is inaccurate. Leaving a to-be-deleted feature branch as the default will eventually break CI. Update the default to main once MistKit #192 merges.

Commented-out unsafeFlags block

The strict concurrency flags are commented out rather than removed or replaced:

// .unsafeFlags([
//   "-warn-concurrency",
//   "-strict-concurrency=complete",
//   ...
// ])

This silently drops important compile-time safety checks with no explanation. If these fail against the new MistKit branch, document it with a TODO. Otherwise, remove the dead code or replace with the SwiftSetting.strictConcurrency structured API (which avoids unsafeFlags entirely).

fail-fast removal in Windows matrix

Removing fail-fast: false changes the default to true — if one Windows version fails, the other is cancelled. If intentional (CI cost savings), add a comment explaining the decision.


Suggestions

Shell injection in composite action: The branch name is interpolated directly into the sed command. Pass it via an env var for safety with special characters:

env:
  BRANCH: ${{ inputs.branch }}
run: |
  sed -i '' "s|...|branch: \"$BRANCH\"|g" Package.swift

Cache key trade-off: Dropping Package.resolved from the cache key is correct given it's regenerated in CI, but it means builds that differ only in resolved dependency versions (e.g., MistKit main advancing between runs) may serve a stale binary. Worth noting in a comment.


Positive Changes

  • The composite action cleanly handles platform differences (macOS sed -i '', Linux sed -i, Windows Set-Content).
  • Re-enabling GUID deduplication in ArticleSyncService is a meaningful correctness fix — the let existingArticles: [Article] = [] placeholder caused all articles to appear "new" on every update run, producing duplicate CloudKit writes.
  • Tests correctly validate the new two-filter-at-query-time behavior.
  • --dry-run flag on setup-cloudkit-schema.sh is a useful usability addition.

Summary

Two blockers before leaving draft:

  1. Fix the BushelCloud copy-paste errors in setup-cloudkit-schema.sh (wrong container ID in help, wrong key path in success message).
  2. Update the setup-mistkit default branch from 192-query-filter-in to main once MistKit #192 merges, or add a comment explaining the temporary coupling to that feature branch.

Use uniquingKeysWith instead of uniqueKeysWithValues when building the
existingMap in ArticleCategorizer to avoid a fatal crash when CloudKit
returns two Article records with the same guid.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@claude

claude Bot commented Apr 14, 2026

Copy link
Copy Markdown

Code Review

This is a draft PR that bundles several meaningful changes. Here's a breakdown by area.


Summary of Changes

  1. Switch MistKit to a local path dependency for development, with CI swapping it for a remote branch via a new composite action
  2. Re-enable the CloudKit GUID deduplication query (was temporarily disabled due to MistKit issue #192)
  3. Move feedRecordName filtering from in-memory to query-time now that #192 is resolved
  4. Crash fix: uniquingKeysWith for duplicate GUIDs in ArticleCategorizer
  5. --dry-run flag for setup-cloudkit-schema.sh
  6. Commented-out unsafeFlags in Package.swift

Issues

🔴 Critical: unsafeFlags commented out (Package.swift:63-74)

// .unsafeFlags([
//   "-warn-concurrency",
//   "-enable-actor-data-race-checks",
//   "-strict-concurrency=complete",
//   ...
// ])

This silently removes strict concurrency checking from all builds. The project's CLAUDE.md calls out Swift 6.2 strict concurrency as a core requirement. Commenting out rather than fixing suggests there may be a concurrency incompatibility with the local MistKit path. This needs to be either:

  • Restored and the underlying incompatibility fixed in MistKit, or
  • Replaced with the Swift 6 equivalent settings that don't require unsafeFlags

As-is this is a regression and should block merge.

🔴 Wrong default branch in composite action (.github/actions/setup-mistkit/action.yml:8)

default: 192-query-filter-in

The PR description says "CI tests against MistKit's main branch" but the default is 192-query-filter-in — a feature branch. Feature branches can be rebased, force-pushed, or deleted. The default should be main to match the stated intent and for long-term stability. All workflows call the action with no arguments (relying on this default), so this is what CI will use.

🔴 Copy/paste artifacts from BushelCloud (Scripts/setup-cloudkit-schema.sh)

Two places reference the wrong project:

  • Line ~24 (help text): iCloud.com.brightdigit.Bushel should be iCloud.com.brightdigit.Celestra
  • Line ~184 (error message): bushel-private-key.pem should be celestra-private-key.pem

These look like the --dry-run feature was ported from BushelCloud without updating the project-specific strings.


Minor Issues

🟡 fail-fast: false removed from Windows matrix (CelestraCloud.yml:44)

# Was:
strategy:
  fail-fast: false
  matrix:
    runs-on: [windows-2022, windows-2025]

Removing fail-fast: false means if windows-2022 fails, windows-2025 is cancelled (and vice versa). This reduces signal: you lose the ability to see if a failure is Windows-version-specific vs universal. Unclear if this removal is intentional.

🟡 public import Foundationimport Foundation (ArticleCategorizer.swift:31)

Not a bug — Foundation is transitively available — but worth noting this is a deliberate access control tightening. Fine to keep.


Good Changes

✅ Re-enabling GUID deduplication (ArticleSyncService.swift:75-81)
Restoring the real implementation (previously let existingArticles: [Article] = []) is the most important functional fix here. The workaround meant every article was treated as new on every update run, causing unbounded CloudKit writes.

✅ Server-side feedRecordName filtering (ArticleCloudKitService.swift:113-120)
Moving the filter from in-memory to query-time is the right call now that IN + other filters work. Reduces unnecessary data transfer and is architecturally cleaner.

✅ Crash fix for duplicate GUIDs (ArticleCategorizer.swift:69-71)

// Before (would crash if CloudKit returned duplicate GUIDs):
Dictionary(uniqueKeysWithValues: existingArticles.map { ($0.guid, $0) })

// After:
Dictionary(existingArticles.map { ($0.guid, $0) }, uniquingKeysWith: { first, _ in first })

uniqueKeysWithValues traps on duplicate keys. The uniquingKeysWith approach handles CloudKit data inconsistencies gracefully. The comment explaining why (// Use uniquingKeysWith to handle duplicate GUIDs in CloudKit data gracefully) is a good addition.

✅ Composite action pattern (.github/actions/setup-mistkit/action.yml)
Extracting the sed/PowerShell logic into a composite action avoids duplicating it across CelestraCloud.yml, codeql.yml, and update-feeds.yml. The cross-platform handling (macOS sed -i '' vs Linux sed -i) is correct.

✅ Cache key excludes Package.resolved (update-feeds.yml:148)
Since Package.resolved is regenerated in CI when using a branch dependency, including it in the cache key would cause a cache miss every time the resolved file changed. Removing it is the right fix.

✅ Updated tests reflect new behavior
The test changes in ArticleCloudKitService+Query.swift correctly verify that 2 filters are now sent (GUID IN + feedRecordName EQUALS) instead of 1.


Summary

The functional fixes (duplicate GUID crash, GUID deduplication re-enabled, server-side filtering) are solid and ready. The CI/infrastructure changes need three fixes before merge:

  1. Restore or properly replace the commented-out unsafeFlags concurrency settings
  2. Change the composite action default branch from 192-query-filter-in to main
  3. Fix the BushelCloud copy/paste strings in setup-cloudkit-schema.sh

- Regenerate Types.swift from updated openapi.yaml (adds typed
  _typePayload enum to FieldValueRequest instead of manual _listType)
- Update FilterBuilder to use generated enum cases instead of raw strings
- Add rule to CLAUDE.md: never manually edit Generated files
- Move filter string parsing into MistDemoConfiguration.filterStrings(forKey:)
- Add internal access modifier to ArticleCategorizer Foundation import
- Remove unintended _world permission grant from MistDemo schema.ckdb
- Fix CLOUDKIT_WEBAUTH_TOKEN → CLOUDKIT_WEB_AUTH_TOKEN across docs
- Add shared setup-mistkit GitHub Action to MistKit root (.github/actions/)
- Update filter/list type tests to use enum cases instead of strings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@claude

claude Bot commented Apr 15, 2026

Copy link
Copy Markdown

Code Review: PR #36 — Use local MistKit in CelestraCloud and update workflows for main branch

This PR follows the BushelCloud pattern for tight local MistKit development by switching to a path: "../.." dependency locally and swapping it to a remote branch reference in CI. The core intent is sound. A few things worth addressing before marking ready for review:


Issues

1. setup-mistkit default branch points to a feature branch, not main

.github/actions/setup-mistkit/action.yml:7

default: 192-query-filter-in

The PR description says CI will test against "MistKit's main branch," but the action's default is 192-query-filter-in. This means all CI runs currently test against that feature branch. Once it is merged into MistKit, this default will point to a deleted or stale branch and builds will fail. Either:

  • Change the default to main and override it in callers that need the feature branch, or
  • Pin a specific tag/commit once the fix is released

2. Commented-out unsafeFlags block in Package.swift

Package.swift:63–76

Commented-out code should be removed, not left in. If these flags were causing build failures in combination with the local path dependency, that should be documented in a comment explaining why they were removed—or the flags should be moved to a non-unsafeFlags approach.

3. Copy-paste artifacts from BushelCloud in setup-cloudkit-schema.sh

Two references still mention Bushel/Bushel paths:

  • Help text (--help output): CLOUDKIT_CONTAINER_ID default is shown as iCloud.com.brightdigit.Bushel — should be iCloud.com.brightdigit.Celestra
  • Instruction text (line ~181): "Store it securely (e.g., ~/.cloudkit/bushel-private-key.pem)" — should reference celestra

4. Removal of fail-fast: false from the Windows matrix

CelestraCloud.yml:44

-      fail-fast: false

Removing this means a failure in windows-2022 will immediately cancel windows-2025 (and vice versa). The original false was presumably intentional to let both matrix entries run to completion so you get a full picture of failures across both Windows versions. This looks like an accidental deletion.


Minor / Suggestions

5. uniquingKeysWith in ArticleCategorizer silently drops duplicates

ArticleCategorizer.swift:70

uniquingKeysWith: { first, _ in first }

This is the correct defensive fix for duplicate GUIDs in CloudKit data, but duplicates are dropped silently. Adding a log line (via CelestraLogger) when a duplicate is encountered would aid debugging if data integrity issues arise.

6. Test for combined query filter could be more specific

ArticleCloudKitService+Query.swift:137–139

#expect(mock.queryCalls[0].filters?.count == 2)

The test verifies that two filters are sent, but doesn't assert their values (e.g., that the second filter is an .equals("feedRecordName", .string("feed-123"))). A value assertion would prevent a regression where two wrong filters are passed.

7. PowerShell encoding risk on Windows

.github/actions/setup-mistkit/action.yml:26

(Get-Content Package.swift) -replace ... | Set-Content Package.swift

Set-Content in older PowerShell versions defaults to UTF-16 LE, which would corrupt Package.swift. Safer to add -Encoding utf8:

... | Set-Content Package.swift -Encoding utf8

What's well done

  • Extracting the sed/replace logic into a reusable composite action (setup-mistkit) is the right call — it keeps all four workflows DRY.
  • The conditional guard (if: steps.cache-binary.outputs.cache-hit != 'true') in update-feeds.yml is a nice efficiency win — no need to rewrite Package.swift when the cached binary is reused.
  • Restoring the commented-out GUID deduplication query in ArticleSyncService (dependent on issue #192 being fixed) is well-scoped and the code path is clean.
  • Removing Package.resolved from the update-feeds.yml cache key is the correct adjustment now that MistKit is a local/branch dependency that won't appear in Package.resolved.

@claude

claude Bot commented Apr 30, 2026

Copy link
Copy Markdown

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

Celestra is a command-line RSS reader that demonstrates MistKit's CloudKit integration capabilities. It fetches RSS feeds, stores them in CloudKit's public database, and implements comprehensive web etiquette best practices including rate limiting, robots.txt checking, and conditional HTTP requests.

Tech Stack: Swift 6.2, MistKit (CloudKit wrapper), CelestraKit (shared models & services), SyndiKit (RSS parsing), Swift Configuration (configuration management)

Common Commands

Build and Run

# Build the project
swift build

# Run with environment variables
source .env
swift run celestra-cloud <command>

# Add a feed
swift run celestra-cloud add-feed https://example.com/feed.xml

# Update feeds with filters
swift run celestra-cloud update
swift run celestra-cloud update --update-last-attempted-before 2025-01-01T00:00:00Z
swift run celestra-cloud update --update-min-popularity 10 --update-delay 3.0
swift run celestra-cloud update --update-limit 5 --update-max-failures 0

# Clear all data
swift run celestra-cloud clear --confirm

# Using both environment variables and CLI arguments (CLI wins)
UPDATE_DELAY=2.0 swift run celestra-cloud update --update-delay 3.0

Environment Setup

Required environment variables (see .env.example):

  • CLOUDKIT_KEY_ID - Server-to-Server key ID from Apple Developer Console
  • CLOUDKIT_PRIVATE_KEY_PATH - Path to .pem private key file

Optional environment variables:

  • CLOUDKIT_CONTAINER_ID - CloudKit container identifier (default: iCloud.com.brightdigit.Celestra)
  • CLOUDKIT_ENVIRONMENT - Either development or production (default: development)

CloudKit Schema Management

# Automated schema deployment (requires cktool)
export CLOUDKIT_CONTAINER_ID="iCloud.com.brightdigit.Celestra"
export CLOUDKIT_TEAM_ID="YOUR_TEAM_ID"
export CLOUDKIT_ENVIRONMENT="development"
./Scripts/setup-cloudkit-schema.sh

Schema is defined in schema.ckdb using CloudKit's text-based schema language.

Architecture

High-Level Structure

Sources/CelestraCloud/
├── Celestra.swift              # CLI entry point
└── Commands/                   # CLI subcommands
    ├── AddFeedCommand.swift    # Parse and add RSS feeds
    ├── UpdateCommand.swift     # Fetch/update feeds (shows MistKit QueryFilter)
    └── ClearCommand.swift      # Delete all records

Sources/CelestraCloudKit/
├── Configuration/              # Swift Configuration integration
│   ├── CelestraConfiguration.swift          # Root config struct
│   ├── CloudKitConfiguration.swift          # CloudKit credentials config
│   ├── UpdateCommandConfiguration.swift     # Update command options
│   ├── ConfigurationLoader.swift            # Multi-source config loader
│   └── ConfigurationError.swift             # Enhanced errors
├── CelestraConfig.swift        # CloudKit service factory
├── Services/
│   ├── CloudKitService+Celestra.swift  # MistKit operations
│   ├── CelestraError.swift             # Error types
│   └── CelestraLogger.swift            # Structured logging
├── Models/
│   └── BatchOperationResult.swift      # Batch operation tracking
└── Extensions/
    ├── Feed+MistKit.swift      # Feed ↔ CloudKit conversion
    └── Article+MistKit.swift   # Article ↔ CloudKit conversion

External Dependencies: The Feed and Article models, along with RateLimiter and RobotsTxtService, are provided by the CelestraKit package for reuse across CLI and other clients.

Key Architectural Patterns

1. MistKit Integration

CloudKitService is configured in CelestraConfig.createCloudKitService():

  • Server-to-Server authentication using PEM keys
  • Public database access for shared feeds
  • Environment-based configuration (dev/prod)

All CloudKit operations are in CloudKitService+Celestra.swift extension:

  • queryFeeds() - Demonstrates QueryFilter and QuerySort APIs
  • createArticles() / updateArticles() - Batch operations with chunking
  • queryArticlesByGUIDs() - Duplicate detection queries

2. Field Mapping Pattern

Models use direct field mapping with validation (CloudKitConvertible protocol):

// To CloudKit
func toFieldsDict() -> [String: FieldValue] {
    var fields: [String: FieldValue] = [
        "title": .string(title),
        "isActive": .int64(isActive ? 1 : 0)  // Booleans as INT64
    ]
    // Optional fields only added if present
    if let description = description {
        fields["description"] = .string(description)
    }
    return fields
}

// From CloudKit - with validation (throws CloudKitConversionError)
init(from record: RecordInfo) throws {
    // Required fields throw if missing or empty
    guard case .string(let title) = record.fields["title"],
          !title.isEmpty else {
        throw CloudKitConversionError.missingRequiredField(
            fieldName: "title",
            recordType: "Feed"
        )
    }

    // Boolean extraction with default
    if case .int64(let value) = record.fields["isActive"] {
        self.isActive = value != 0
    } else {
        self.isActive = true  // Default for optional fields
    }
}

Validation Behavior:

  • Required fields (feedURL, title for Feed; feedRecordName, guid, title, url for Article) throw CloudKitConversionError if missing or empty
  • Invalid articles are skipped with warning logs; one bad article won't fail the entire feed update
  • Feed conversion errors propagate (fail-fast for feed metadata)

3. Duplicate Detection Strategy

UpdateCommand implements GUID-based duplicate detection:

  1. Extract GUIDs from fetched articles
  2. Query CloudKit for existing articles with those GUIDs (queryArticlesByGUIDs)
  3. Separate into new vs modified articles (using contentHash comparison)
  4. Create new articles, update modified ones, skip unchanged

This minimizes CloudKit writes and prevents duplicate content.

4. Batch Operations

Articles are processed in batches of 10 (conservative to keep payload size manageable with full content):

  • Non-atomic operations allow partial success
  • Each batch tracked in BatchOperationResult
  • Provides success rate, failure count, and detailed error tracking
  • See createArticles() / updateArticles() in CloudKitService+Celestra.swift

5. Web Etiquette Implementation

Celestra is a respectful RSS client:

  • Rate Limiting: Uses RateLimiter actor from CelestraKit - configurable delays between feeds (default 2s), per-domain tracking
  • Robots.txt: Uses RobotsTxtService actor from CelestraKit - parses and respects robots.txt rules
  • Conditional Requests: Uses If-Modified-Since/ETag headers, handles 304 Not Modified
  • Failure Tracking: Tracks consecutive failures per feed, can filter by max failures
  • Update Intervals: Respects feed's minUpdateInterval to avoid over-fetching
  • User-Agent: Identifies as "Celestra/1.0 (MistKit RSS Reader; +https://github.com/brightdigit/MistKit)"

All web etiquette features are demonstrated in UpdateCommand.swift using services from CelestraKit.

CloudKit Schema

Two record types in public database:

Feed: RSS feed metadata

  • Key fields: feedURL (QUERYABLE SORTABLE), title (SEARCHABLE)
  • Metrics: totalAttempts, successfulAttempts, subscriberCount
  • Web etiquette: etag, lastModified, failureCount, minUpdateInterval
  • Booleans stored as INT64: isActive, isFeatured, isVerified

Article: RSS article content

  • Key fields: guid (QUERYABLE SORTABLE), feedRecordName (STRING)
  • Content: title, excerpt, content, contentText (all SEARCHABLE)
  • Deduplication: contentHash (SHA256), guid
  • TTL: expiresAt (QUERYABLE SORTABLE) for cleanup

Relationship Design: Uses string-based feedRecordName instead of CKReference for simplicity and clearer querying patterns. Trade-off: Manual cascade delete vs automatic with CKReference.

Swift Configuration (v1.0.0)

CelestraCloud uses Apple's Swift Configuration library for unified configuration management across environment variables and command-line arguments.

Configuration Architecture

Priority Order: CLI arguments > Environment variables > Defaults

// ConfigurationLoader uses CommandLineArgumentsProvider
let loader = ConfigurationLoader()
let config = await loader.loadConfiguration()

Built-in Providers Used

  1. CommandLineArgumentsProvider - Automatic CLI argument parsing (highest priority)
  2. EnvironmentVariablesProvider - System environment variables

Package Trait: CommandLineArguments trait is enabled in Package.swift to support CommandLineArgumentsProvider.

Configuration Reference

CloudKit Configuration

CloudKit authentication credentials must be provided via environment variables:

Environment Variable Type Default Required Description
CLOUDKIT_CONTAINER_ID String iCloud.com.brightdigit.Celestra No CloudKit container identifier
CLOUDKIT_KEY_ID String None Yes Server-to-Server key ID from Apple Developer Console
CLOUDKIT_PRIVATE_KEY_PATH String None Yes Absolute path to .pem private key file
CLOUDKIT_ENVIRONMENT String development No CloudKit environment: development or production

Note: CloudKit credentials (CLOUDKIT_KEY_ID and CLOUDKIT_PRIVATE_KEY_PATH) are marked as secrets and automatically redacted from logs.

Update Command Configuration (Optional)

All update command settings are optional and can be provided via environment variables OR CLI arguments:

Option Env Variable CLI Argument Type Default Description
Delay UPDATE_DELAY --update-delay <seconds> Double 2.0 Delay between feed updates in seconds
Skip Robots UPDATE_SKIP_ROBOTS_CHECK --update-skip-robots-check Bool false Skip robots.txt validation (flag)
Max Failures UPDATE_MAX_FAILURES --update-max-failures <count> Int None Skip feeds above this failure threshold
Min Popularity UPDATE_MIN_POPULARITY --update-min-popularity <count> Int None Only update feeds with minimum subscribers
Last Attempted Before UPDATE_LAST_ATTEMPTED_BEFORE --update-last-attempted-before <iso8601> Date None Only update feeds attempted before this date
Limit UPDATE_LIMIT --update-limit <count> Int None Maximum number of feeds to query and update
JSON Output Path UPDATE_JSON_OUTPUT_PATH --update-json-output-path <path> String None Path to write JSON report with detailed results

Date Format: ISO8601 (e.g., 2025-01-01T00:00:00Z)

Configuration Key Mapping

Command-line arguments use kebab-case:

  • --cloudkit-container-idcloudkit.container_id
  • --update-delayupdate.delay
  • --update-skip-robots-checkupdate.skip_robots_check

Environment variables use SCREAMING_SNAKE_CASE:

  • CLOUDKIT_CONTAINER_IDcloudkit.container_id
  • UPDATE_DELAYupdate.delay
  • UPDATE_SKIP_ROBOTS_CHECKupdate.skip_robots_check

Usage Examples

Note: Examples below assume celestra-cloud is in your PATH. If running from source, prefix commands with swift run (e.g., swift run celestra-cloud update).

Via environment variables:

export CLOUDKIT_CONTAINER_ID="iCloud.com.brightdigit.Celestra"
export UPDATE_DELAY=3.0
export UPDATE_MAX_FAILURES=5
celestra-cloud update

Via command-line arguments:

celestra-cloud update \
  --update-delay 3.0 \
  --update-max-failures 5 \
  --update-min-popularity 10

Mixed (CLI overrides ENV):

# Environment has UPDATE_DELAY=2.0, but CLI overrides to 5.0
UPDATE_DELAY=2.0 celestra-cloud update --update-delay 5.0
# Uses 5.0 (CLI wins)

Filtering by date:

# Only update feeds last attempted before January 1, 2025
celestra-cloud update --update-last-attempted-before 2025-01-01T00:00:00Z

With JSON output for detailed reporting:

# Generate JSON report with per-feed results and summary statistics
celestra-cloud update --update-json-output-path /tmp/feed-update-report.json

# Combine with other options for CI/CD workflows
celestra-cloud update \
  --update-limit 10 \
  --update-delay 1.0 \
  --update-json-output-path ./build/feed-update-results.json

Adding New Configuration Options

To add a new configuration option (e.g., --concurrency):

  1. Add to configuration struct:
// In UpdateCommandConfiguration.swift
public var concurrency: Int = 1
  1. Update ConfigurationLoader:
// In ConfigurationLoader.loadConfiguration()
let update = UpdateCommandConfiguration(
    // ... existing fields
    concurrency: readInt(forKey: "update.concurrency") ?? 1
)
  1. Access in command:
// In UpdateCommand.swift
let config = try await loader.loadConfiguration()
let concurrency = config.update.concurrency

No manual parsing needed! Users can now use:

  • --update-concurrency 3 (CLI - kebab-case)
  • UPDATE_CONCURRENCY=3 (environment - SCREAMING_SNAKE_CASE)

Key Documentation

  • See .claude/https_-swiftpackageindex.com-apple-swift-configuration-1.0.0-documentation-configuration.md for complete Swift Configuration API reference
  • Provider hierarchy documentation: Configuration providers are queried in order, first non-nil value wins

Swift 6.2 Features

Package.swift enables extensive Swift 6.2 upcoming and experimental features:

  • Strict concurrency checking (-strict-concurrency=complete)
  • Existential any keyword
  • Typed throws
  • Noncopyable generics
  • Move-only types
  • Variadic generics

Code must be concurrency-safe with proper actor isolation.

Development Guidelines

When Adding Features:

  • MistKit operations go in CloudKitService+Celestra.swift extension
  • Configuration options: Add to appropriate struct in Configuration/ directory, update ConfigurationLoader
  • All CloudKit field types: Use FieldValue enum (.string, .int64, .date, .double, etc.)
  • Booleans: Always store as INT64 (0/1) in CloudKit schema
  • Batch operations: Chunk into batches of 10 for large payloads, use non-atomic for partial success
  • Logging: Use CelestraLogger categories (cloudkit, rss, operations, errors)

External Dependencies:

  • RateLimiter, RobotsTxtService, and RSSFetcherService are from CelestraKit - contributions should be made to that repository
  • Feed and Article models are also in CelestraKit for reuse across the Celestra ecosystem
  • Web etiquette suite (rate limiting, robots.txt, RSS fetching) is now complete in CelestraKit

Testing CloudKit Operations:

  • Use development environment first
  • Schema changes require redeployment via ./Scripts/setup-cloudkit-schema.sh
  • Clear data with celestra clear --confirm between tests

Key Documentation:

  • .claude/IMPLEMENTATION_NOTES.md - Design decisions, patterns, and technical context
  • .claude/AI_SCHEMA_WORKFLOW.md - CloudKit schema design guide for AI agents
  • .claude/CLOUDKIT_SCHEMA_SETUP.md - Schema deployment instructions

Pull Request Testing

Integration tests automatically validate the update-feeds workflow on all pull requests to main:

Test Scope:

  • Runs against CloudKit development environment only (production never touched)
  • Limited smoke test: Maximum 5 feeds, zero failures allowed
  • Completes in ~2-5 minutes (vs. production's 60-120 minute runs)
  • Uses same binary caching as production workflow

Behavior:

  • Repository branch PRs: Full integration test runs automatically
  • Fork PRs: Tests skipped gracefully (GitHub security prevents secret access)
  • Fails fast on errors (unlike production which continues on error)

Workflow Details:

  • Workflow: .github/workflows/update-feeds.yml (shared with scheduled production runs)
  • Tier: pr-test (alongside high, standard, stale tiers)
  • Filter: --update-limit 5 --update-max-failures 0 --update-delay 1.0
  • Timeout: 10 minutes maximum

External Contributors:
Fork PRs cannot run integration tests due to GitHub's security model (secrets unavailable). Maintainers can create repository branches for contributors to run tests before merge, or tests will validate after merge.

Important Patterns

QueryFilter Examples (see CloudKitService+Celestra.swift:44-68):

var filters: [QueryFilter] = []
filters.append(.lessThan("lastAttempted", .date(cutoffDate)))
filters.append(.greaterThanOrEquals("subscriberCount", .int64(minPopularity)))

let records = try await queryRecords(
    recordType: "Feed",
    filters: filters.isEmpty ? nil : filters,
    sortBy: [.ascending("feedURL")],
    limit: limit
)

Duplicate Detection (see UpdateCommand.swift:192-236):

let guids = articles.map { $0.guid }
let existingArticles = try await service.queryArticlesByGUIDs(guids, feedRecordName: recordName)
let existingMap = Dictionary(uniqueKeysWithValues: existingArticles.map { ($0.guid, $0) })

for article in articles {
    if let existing = existingMap[article.guid] {
        if existing.contentHash != article.contentHash {
            modifiedArticles.append(article.withRecordName(existing.recordName))
        }
    } else {
        newArticles.append(article)
    }
}

Server-to-Server Auth (see CelestraConfig.swift):

let privateKeyPEM = try String(contentsOfFile: privateKeyPath, encoding: .utf8)
let tokenManager = try ServerToServerAuthManager(keyID: keyID, pemString: privateKeyPEM)
let service = try CloudKitService(
    containerIdentifier: containerID,
    tokenManager: tokenManager,
    environment: environment,
    database: .public
)

@claude

claude Bot commented Apr 30, 2026

Copy link
Copy Markdown

Test comment from review

@claude

claude Bot commented Apr 30, 2026

Copy link
Copy Markdown

Code Review - PR 36

CRITICAL: The setup-mistkit action never triggers. All workflows call it without a branch input, so the sed substitution never runs and CI will fail resolving the ../.. local path dependency.

Fix: add 'with: branch: main' to each setup-mistkit call in the workflows.

MEDIUM: setup-cloudkit-schema.sh --help has two copy-paste Bushel references (container ID default and key path example) that should say Celestra.

MEDIUM: unsafeFlags block is commented out without explanation, silently disabling -strict-concurrency=complete. If MistKit needs these removed, fix it upstream; otherwise add a TODO with the issue number.

LOW: inputs.branch is interpolated directly into the sed shell command - assign to env var first to prevent injection on unusual branch names.

LOW: Removing Package.resolved from the update-feeds.yml cache key means MistKit main branch updates won't bust the binary cache.

Good: ArticleSyncService GUID deduplication restored (correctness fix). ArticleCategorizer uniquingKeysWith prevents runtime trap on duplicate GUIDs. ArticleCloudKitService server-side feedRecordName filtering is more efficient; tests correctly updated. internal import Foundation is right.

leogdion added 2 commits May 1, 2026 20:01
…#239, #246, #249, #252)

Reworks main-repo CI per #249: gate heavy matrices behind main / semver
branches / PRs targeting them, cutting feature-branch jobs from ~20+ to ~3.
Move CodeQL from macOS to Linux. Add cleanup-caches.yml that wipes Actions
caches on branch deletion. Add a dedicated MistDemo workflow at the repo
root (#239); MistDemo is in-repo so a workflow inside Examples/MistDemo
would not be honored. Bump every action and Swift/Xcode pin (#252):
brightdigit/swift-build@v1, codecov/codecov-action@v6,
sersoft-gmbh/swift-coverage-action@v5, actions/checkout@v6, actions/cache@v5,
actions/github-script@v9, jlumbroso/free-disk-space@v1.3.1; Xcode 26.4 and
Apple OSes 26.4; Swift matrix now stable-only [6.1, 6.2, 6.3] with the
latest stable as the reduced-matrix entry. swift-source-compat.yml drops
its nightly entry and aligns to [6.1, 6.2, 6.3] — closes #246 (existing
workflow is sufficient).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s (#251, #252)

Same updates as BushelCloud: switch to brightdigit/MistKit/.github/actions/setup-mistkit@main
with MISTKIT_BRANCH env, bump brightdigit/swift-build to @v1, codecov to v6,
swift-coverage-action to v5, checkout to v6, cache to v5. Update Xcode/Apple OS
pins to 26.4 and add Swift 6.1 alongside 6.2 and 6.3 in the Ubuntu matrix.
Windows matrix moves to 6.3-RELEASE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented May 2, 2026

Copy link
Copy Markdown

Code Review

Summary: This PR aligns CelestraCloud with BushelCloud's pattern for MistKit dependency management — switching to a local path dependency for development and using a composite setup-mistkit action in CI to swap in the remote main branch. It also re-enables the previously disabled GUID deduplication logic (now that MistKit issue #192 is fixed) and adds --dry-run support to the schema setup script.


Bugs / Issues

Critical: Missing .github/actions/setup-mistkit local action

Both codeql.yml and update-feeds.yml reference uses: ./.github/actions/setup-mistkit, but this file is not in the PR's changed files and does not appear to exist in the repo. This will cause those workflows to fail at runtime. CelestraCloud.yml correctly uses the remote form brightdigit/MistKit/.github/actions/setup-mistkit@main — the other two workflows should either use the same remote reference or the local composite action needs to be added.

BushelCloud copy-paste artifacts in setup-cloudkit-schema.sh

The new help text and error message contain two wrong references:

  • Line ~17: "(default: iCloud.com.brightdigit.Bushel)" — should be Celestra
  • Line ~181: "~/.cloudkit/bushel-private-key.pem" — should be celestra-private-key.pem

Significant Concerns

Commented-out unsafeFlags block in Package.swift

The entire unsafeFlags block — including -strict-concurrency=complete and -enable-actor-data-race-checks — has been commented out. CLAUDE.md specifically calls out strict concurrency checking as a project requirement under "Swift 6.2 Features." Even if some flags overlap with Swift 6 defaults, disabling them all silently reduces the safety guarantees the codebase relies on. At minimum, please document why these were removed, or restore the non-redundant ones (e.g., -warn-long-function-bodies=100).

Removal of fail-fast: false from Windows build matrix

The Windows matrix previously had fail-fast: false to allow both windows-2022 and windows-2025 to complete independently. Removing it means a failure on one will cancel the other, losing diagnostic signal. This seems unintentional — was it meant to stay?


Positive Changes

ArticleSyncService.swift — deduplication re-enabled

The temporary workaround (let existingArticles: [Article] = []) is finally replaced with the real GUID query. This is the core functional improvement in the PR and is a meaningful correctness fix.

ArticleCloudKitService.swift — server-side filter

Moving feedRecordName filtering from in-memory to a CloudKit query filter (now that issue #192 is fixed) reduces data transfer and is a clean improvement. The comment explaining the previous limitation and its resolution is helpful.

ArticleCategorizer.swift — safe duplicate handling

Switching from uniqueKeysWithValues: (which crashes on duplicate keys) to uniquingKeysWith: { first, _ in first } is a good defensive fix for unexpected duplicate GUIDs in CloudKit data.

Foundation import visibility — changing from public import to internal import is the correct access level for an implementation detail.


Minor Notes

  • The Package.resolved removal of the MistKit pin is expected and correct.
  • Bumping action versions (checkout@v6, codecov-action@v6, etc.) is fine, though for security-sensitive workflows like codeql.yml, SHA-pinned refs are preferable to mutable version tags.
  • The Swift matrix simplification (removing the nightly: true struct in favor of a flat string array) is cleaner, but verify that swift:6.3-noble and swift:6.3-jammy images actually exist on Docker Hub before merging.

Summary

The core logic changes (deduplication fix, server-side filtering, duplicate GUID safety) are solid. The PR is blocked on two items before it should leave draft: the missing .github/actions/setup-mistkit local action that will break codeql.yml and update-feeds.yml, and the BushelCloud text artifacts in the schema script. The commented-out strict-concurrency flags also need justification or restoration.

…epos, drop visionOS pin

- BushelCloud and CelestraCloud Package.swift declare swift-tools-version: 6.2,
  so Swift 6.1 builds errored with "package is using Swift tools version 6.2.0
  but the installed version is 6.1.3". Drop 6.1 from both subrepo Ubuntu
  matrices (per project guidance: keep 6.1 only where it actually compiles)
  and add fail-fast: false so one Ubuntu failure stops cancelling siblings.
- visionOS 26.4 simulator is not pre-installed on macos-26 runners, while
  watchOS 26.4 / tvOS 26.4 / iOS 26.4 are. Drop the explicit visionOS
  osVersion in MistKit.yml and both subrepo workflows so xcodebuild
  auto-selects the latest installed runtime — survives runner image refreshes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented May 2, 2026

Copy link
Copy Markdown

Code Review

This PR aligns CelestraCloud with BushelCloud's pattern for local MistKit development — using a path dependency locally with CI substitution to the remote `main` branch. It also re-enables GUID-based deduplication that was previously blocked by a MistKit bug. Overall the direction is solid, but there are a few issues to address before marking this ready.


🐛 Bugs / Copy-Paste Errors

Scripts/setup-cloudkit-schema.sh has Bushel references:

Two lines in the new `--help` output reference BushelCloud instead of Celestra:

CLOUDKIT_CONTAINER_ID   CloudKit container ID (default: iCloud.com.brightdigit.Bushel)
Store it securely (e.g., ~/.cloudkit/bushel-private-key.pem)

These should read `iCloud.com.brightdigit.Celestra` and `celestra-private-key.pem` respectively. This is a straightforward copy-paste from BushelCloud.


⚠️ Potential Issues

Inconsistent setup-mistkit action references across workflows:

  • CelestraCloud.yml uses the remote action: brightdigit/MistKit/.github/actions/setup-mistkit@main
  • codeql.yml and update-feeds.yml use a local action: ./.github/actions/setup-mistkit

If .github/actions/setup-mistkit/ doesn't exist in this repository, CodeQL scanning and feed update jobs will fail at runtime. Please verify which pattern is intentional and standardize across all three workflows.

Package.swift: Commented-out unsafeFlags block without explanation:

// .unsafeFlags([
//   "-warn-concurrency",
//   "-enable-actor-data-race-checks",
//   "-strict-concurrency=complete",
//   ...
// ])

CLAUDE.md states "Code must be concurrency-safe with proper actor isolation." Silently disabling -strict-concurrency=complete weakens that guarantee. If these flags are now redundant (e.g., Swift 6.2's module-level strict concurrency makes them unnecessary), remove the block entirely and document why. Leaving commented code creates confusion about intent.

Windows CI: fail-fast behavior reversed:

  • Ubuntu gained fail-fast: false (good — lets all matrix entries run)
  • Windows lost fail-fast: false — now defaults to true, so a failure on windows-2022 will cancel windows-2025 mid-run. If this is intentional (only one Windows config matters), a comment would help; otherwise restore it.

Local path dependency requires sibling directory layout:

Package.swift now uses .package(name: "MistKit", path: "../.."), which assumes MistKit is checked out two directories above CelestraCloud. A developer who clones only this repo will get an immediate build failure. Consider adding a note to the README or .env.example that ../.. must be a MistKit checkout, or provide a fallback script.


✅ Positive Changes

Re-enabling GUID deduplication (ArticleSyncService.swift):

Removing the TEMPORARY workaround and properly querying existing articles is a meaningful correctness fix. Articles were previously always treated as new (no dedup), which would inflate CloudKit write counts.

Server-side feedRecordName filtering (ArticleCloudKitService.swift):

Moving from in-memory filtering to a query-time filter is the right call now that issue #192 is resolved. Fewer records transferred over the wire, less memory allocation.

Duplicate GUID handling (ArticleCategorizer.swift):

let existingMap = Dictionary(
  existingArticles.map { ($0.guid, $0) },
  uniquingKeysWith: { first, _ in first }
)

Defensive improvement. The comment explains the intent well.

internal import Foundation:

Correct access control — Foundation should not be re-exported as part of this module's public API.

Cache key excludes Package.resolved:

Makes sense given that Package.resolved won't pin MistKit anymore and would cause spurious cache misses in CI.


Summary

Priority Issue
🔴 Must fix Bushel copy-paste in setup-cloudkit-schema.sh help text
🟠 Should fix Inconsistent setup-mistkit action path (remote vs local)
🟠 Should fix Commented-out unsafeFlags — remove or explain
🟡 Worth noting Windows CI lost fail-fast: false
🟡 Worth noting Local path dependency needs developer setup docs

Two issues from the full-matrix CI run on PR #277:

- actions/checkout@v6 fails inside Linux Swift Docker containers with
  "The following required dependencies are missing: curl" — the swift:6.x
  images are minimal and don't ship curl that v6+ requires. v4 supports
  Node 20 and works in those containers. Reverted across all workflows.
  This walks back part of #252 but the bump wasn't load-bearing.

- visionOS 26.4 simulator is not pre-installed on macos-26 runners
  (only iOS / watchOS / tvOS 26.x ship), and -downloadPlatform visionOS
  does not reliably populate it. swift-build builds an
  -destination "...,OS=<value>" string that fails with "missing value
  for key 'OS'" when osVersion is empty, so dropping the pin alone
  doesn't fix it. Comment out the visionOS matrix entries in
  MistKit.yml and both subrepo workflows with a TODO to re-enable
  once the runner image catches up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented May 2, 2026

Copy link
Copy Markdown

Code Review: PR #36 — Local MistKit Dependency + CloudKit Query Fix

Overview

This PR does two distinct things bundled together:

  1. Dependency management: Switches MistKit from a versioned remote dependency to a local path (../..) for development, with CI substitution to use main branch
  2. Bug fix: Restores the previously-disabled GUID query in ArticleSyncService now that MistKit issue #192 is fixed, and moves feedRecordName filtering to the query level

The bug fix is a meaningful improvement. The dependency change is well-patterned (matching BushelCloud) but has a few issues detailed below.


🔴 Issues That Need Addressing

1. Copy-paste errors from BushelCloud in setup-cloudkit-schema.sh

The --help output and error message contain BushelCloud references:

-  echo "  CLOUDKIT_CONTAINER_ID   CloudKit container ID (default: iCloud.com.brightdigit.Bushel)"
+  echo "  CLOUDKIT_CONTAINER_ID   CloudKit container ID (default: iCloud.com.brightdigit.Celestra)"

And at line 184:

-    echo "     d. Store it securely (e.g., ~/.cloudkit/bushel-private-key.pem)"
+    echo "     d. Store it securely (e.g., ~/.cloudkit/celestra-private-key.pem)"

2. Local action reference may not exist

codeql.yml and update-feeds.yml use:

uses: ./.github/actions/setup-mistkit

But other workflows use the remote action brightdigit/MistKit/.github/actions/setup-mistkit@main. If .github/actions/setup-mistkit/ doesn't exist in this repo, both CodeQL scanning and production feed updates will fail when a cache miss occurs. Either add the composite action file or use the same remote reference as the other workflows.

3. Commented-out unsafeFlags in Package.swift

The strict concurrency flags (including -strict-concurrency=complete) are commented out with no explanation:

// .unsafeFlags([
//   "-warn-concurrency",
//   "-enable-actor-data-race-checks",
//   "-strict-concurrency=complete",
//   ...
// ])

This silently weakens the concurrency safety guarantees that CLAUDE.md requires. If these flags no longer compile (e.g., they moved to proper SwiftSetting APIs in Swift 6.2), replace them with their first-class equivalents rather than removing them. If they're temporarily disabled to fix a compilation issue, document why and track it as a follow-up.


🟡 Notable Issues

4. fail-fast: false asymmetry in Windows vs Ubuntu

The PR adds fail-fast: false to the Ubuntu matrix (good), but removes it from the Windows matrix:

 build-windows:
   strategy:
-    fail-fast: false   # removed

With two Windows runners (windows-2022, windows-2025), a failure on one will now cancel the other, making it harder to detect platform-specific failures. Looks like an accidental omission.

5. Implicit dependency on MistKit issue #192 being merged

ArticleSyncService.swift removes the TEMPORARY: Skip GUID query workaround based on the assumption that a MistKit upstream fix is already in main. The PR should either:

  • Reference the specific MistKit commit/PR where #192 was fixed
  • Or include a note in the PR description linking to the upstream fix for traceability

6. Local path dependency affects cloneability

Package.swift now has .package(name: "MistKit", path: "../.."), which means swift build will fail for anyone who doesn't have MistKit checked out as a sibling directory. The CI handles this via setup-mistkit, but the README/CLAUDE.md should document the local development setup requirement.


✅ Good Changes

  • Duplicate GUID handling in ArticleCategorizer: switching to uniquingKeysWith: { first, _ in first } is a defensive improvement that prevents a crash if CloudKit returns duplicate GUIDs
  • Server-side feedRecordName filtering in ArticleCloudKitService is a meaningful efficiency gain (reduces data transfer vs in-memory filter)
  • Restoring GUID deduplication in ArticleSyncService — the existingArticles: [] workaround was a correctness hole; this fixes it properly
  • Action version bumps (codecov v4→v6, swift-coverage v4→v5, cache v4→v5) are welcome maintenance
  • Simplified Swift matrix (swift: ["6.2", "6.3"] vs struct with version/nightly keys) is cleaner
  • --dry-run and --help flags for setup-cloudkit-schema.sh are a nice usability addition
  • internal import Foundation in ArticleCategorizer — correct access level, no need to re-export Foundation
  • visionOS disable comment is well-documented with the reason and a re-enable note

Summary

The CloudKit query fix is solid and the CI dependency pattern is sound, but the three 🔴 items (BushelCloud copy-paste errors, missing local action file, commented-out concurrency flags) should be resolved before merging. The fail-fast asymmetry and missing upstream fix reference are worth a quick fix too.

leogdion added 2 commits May 1, 2026 20:34
- Bump actions/checkout back to @v6 across all workflows.
- Install curl + ca-certificates as the first run-step in every Linux-
  container Ubuntu job (MistKit, MistDemo, BushelCloud, CelestraCloud).
  The codecov uploader requires curl, and the swift:6.x Docker images
  on Noble/Jammy don't include it by default. Guarded by `apt-get`
  presence so it's a no-op outside Debian/Ubuntu containers.
- Re-enable the visionOS matrix entry in MistKit.yml, BushelCloud, and
  CelestraCloud with osVersion: "26.4" and download-platform: true so
  xcodebuild fetches the visionOS Simulator runtime when it's missing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…erage output

Move the "Install curl" step to after swift-build and gate it on
steps.build.outputs.contains-code-coverage so the apt-get only runs when
coverage is actually being uploaded. For BushelCloud and CelestraCloud,
add `id: build` to swift-build and gate the existing
sersoft-gmbh/swift-coverage-action and codecov-action steps on the same
output (matches the MistKit.yml pattern; previously they ran
unconditionally and would have errored on no-coverage configs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented May 2, 2026

Copy link
Copy Markdown

Code Review

Overview

This PR adopts a local path dependency for MistKit (../..) during development, with CI workflows patching it back to the remote main branch at build time — matching the BushelCloud pattern. It also re-enables GUID-based duplicate detection in ArticleSyncService now that MistKit issue #192 (combining .in() with other filters) is reportedly fixed.


Bugs / Correctness Issues

setup-cloudkit-schema.sh help text references wrong project

The --help output mentions iCloud.com.brightdigit.Bushel (copied from BushelCloud):

echo "  CLOUDKIT_CONTAINER_ID   CloudKit container ID (default: iCloud.com.brightdigit.Bushel)"

Should reference iCloud.com.brightdigit.Celestra.

Inconsistent setup-mistkit action references

CelestraCloud.yml uses the remote action:

uses: brightdigit/MistKit/.github/actions/setup-mistkit@main
with:
  branch: ${{ env.MISTKIT_BRANCH }}

But codeql.yml and update-feeds.yml use a local action:

uses: ./.github/actions/setup-mistkit

If ./.github/actions/setup-mistkit/action.yml doesn't already exist in this repo, both the CodeQL scan and the production update-feeds workflow will fail when they try to resolve the local action. The changed files list in this PR doesn't include that action file, so please verify it's already present on the target branch.

ArticleSyncService.swift re-enabling depends on an unverified fix

The TEMPORARY workaround (let existingArticles: [Article] = []) is removed, but the comment only says "Now that issue #192 is fixed." If that fix isn't yet landed on MistKit main, this silently reverts to creating duplicates every update cycle — a data correctness regression. Please link to the MistKit commit/PR that introduced the fix so reviewers can confirm it's actually on main.


Code Quality

unsafeFlags block entirely commented out (Package.swift)

The entire block below is commented out, removing -strict-concurrency=complete, -warn-concurrency, and -enable-actor-data-race-checks:

// .unsafeFlags([
//   "-warn-concurrency",
//   "-strict-concurrency=complete",
//   ...
// ])

CLAUDE.md explicitly states: "Code must be concurrency-safe with proper actor isolation." Silently dropping these checks makes it easier to introduce data races that won't surface until runtime. Even if unsafeFlags needs to go away long-term (e.g., to avoid build system warnings), the correct path is migrating these to their proper SwiftSetting equivalents:

  • .enableUpcomingFeature("StrictConcurrency") or .strictConcurrency(.complete) (available in Swift 5.10+)
  • -enable-testing → use @testable import instead

fail-fast behavior is now asymmetric

fail-fast: false was removed from the Windows matrix while being added to the Ubuntu matrix. This seems inverted from the typical default (you usually want fail-fast: false on cross-platform jobs so a single runner failure doesn't cancel all other results). Please confirm this is intentional.

actions/checkout@v6 — major version jump

This jumps from v4 directly to v6 across all three workflows. As of this writing, the latest stable release is v4. If v6 doesn't yet exist, every workflow job would fail on the checkout step. Please verify this version exists and is stable before merging.


Positive Changes

  • ArticleCategorizer.swift: Switching from Dictionary(uniqueKeysWithValues:) to Dictionary(_:uniquingKeysWith:) is a correctness improvement — the old form crashes on duplicate GUIDs, the new form degrades gracefully.
  • internal import Foundation: Tightening the import visibility is the right call; Foundation types don't need to leak into the module's public API.
  • ArticleCloudKitService.swift: Moving the feedRecordName filter server-side is a good efficiency improvement (fewer records transferred over the wire).
  • Test updates: The test correctly reflects the new two-filter behavior and was updated to match the implementation rather than the old workaround.
  • --dry-run flag for schema setup: Useful for validation in CI pipelines without risking real CloudKit mutations.

Summary

Category Finding
Bug Wrong project name in --help output
Risk Local vs. remote action inconsistency may break CodeQL / update-feeds
Risk MistKit issue #192 fix not confirmed on main
Concern Strict concurrency flags commented out — safety regression
Concern actions/checkout@v6 version needs verification
Question fail-fast asymmetry between Windows and Ubuntu intentional?

The core concept (local path + CI sed-substitution pattern) is sound and mirrors BushelCloud successfully. The issues above should be addressed before merging, particularly the action reference inconsistency and the confirmation that issue #192 is resolved on MistKit main.

@claude

claude Bot commented May 2, 2026

Copy link
Copy Markdown

Code Review — PR #36: Use local MistKit + update workflows

Note: This PR is currently in DRAFT state. Review comments are provided for when it's ready.


Overview

This PR aligns CelestraCloud with BushelCloud's dependency pattern for MistKit, switching from a versioned remote dependency to a local path (../..) for development, with CI using sed to swap it back to the main branch. It also fixes a previously disabled CloudKit .in() + filter combination (issue #192) and bumps several CI action versions.


Bugs / Issues

1. Copy-paste error in setup-cloudkit-schema.sh help text

echo "  CLOUDKIT_CONTAINER_ID   CloudKit container ID (default: iCloud.com.brightdigit.Bushel)"

This says Bushel instead of Celestra. Looks like it was copied from BushelCloud without updating the default value.

2. codeql.yml uses a local composite action that doesn't exist in this repo

- name: Setup MistKit
  uses: ./.github/actions/setup-mistkit

The other workflows correctly reference the action from the external MistKit repo:

uses: brightdigit/MistKit/.github/actions/setup-mistkit@main

Unless .github/actions/setup-mistkit/action.yml exists in this repository (it doesn't appear to be in the diff), the CodeQL workflow will fail. This should be:

uses: brightdigit/MistKit/.github/actions/setup-mistkit@main

3. unsafeFlags block silently commented out in Package.swift

The PR comments out the entire unsafeFlags block, removing:

  • -strict-concurrency=complete
  • -warn-concurrency
  • -enable-actor-data-race-checks

The PR description makes no mention of this change. These flags are part of the project's stated Swift 6.2 strict concurrency stance (per CLAUDE.md). Dropping them without explanation could allow concurrency regressions to silently slip through. If they were removed to fix a build issue, that should be documented. If they're redundant with the swiftSettings above (e.g., StrictConcurrency.minimal), please add a comment explaining why.

4. fail-fast asymmetry between Ubuntu and Windows builds

The PR adds fail-fast: false to Ubuntu builds but removes it from Windows builds. This looks unintentional — if one Windows variant fails, the other will now be cancelled. Recommend applying fail-fast: false consistently across both.


Code Quality

ArticleCategorizer.swiftuniquingKeysWith fix (positive)

Switching from uniqueKeysWithValues: (which traps on duplicate keys) to uniquingKeysWith: { first, _ in first } is a good defensive improvement. The first wins strategy for duplicates is reasonable since the earlier-returned CloudKit record should be preferred.

ArticleCloudKitService.swift / ArticleSyncService.swift — issue #192 fix (positive)

Removing the in-memory feedRecordName filter in favour of server-side query composition is the right call now that the .in() + .equals() combination works. The previously-commented-out query in ArticleSyncService is correctly restored. The tests are updated to match the new 2-filter behaviour.

internal import Foundation in ArticleCategorizer.swift (positive)

Changing from public import to internal import is correct access-level hygiene — callers of this module don't need Foundation re-exported through it.


Performance

update-feeds.yml cache key drops Package.resolved

# Before
key: celestra-cloud-...-${{ hashFiles('Sources/**/*.swift', 'Package.swift', 'Package.resolved') }}-...
# After
key: celestra-cloud-...-${{ hashFiles('Sources/**/*.swift', 'Package.swift') }}-...

This is necessary since CI regenerates Package.resolved for MistKit's main branch. However, it means the binary cache will not be invalidated when only a non-MistKit dependency version changes (e.g., a CelestraKit or swift-log bump). If a dependency update is important enough to pick up in production, the old cached binary would be used instead. Consider adding the MISTKIT_BRANCH env var into the cache key if you want changes to the target branch to bust the cache.


Summary

Issue Severity
Copy-paste "Bushel" in help text Minor
codeql.yml references missing local action Blocking — will break CodeQL CI
unsafeFlags silently commented out Should be explained — removes strict concurrency checks
fail-fast asymmetry on Windows builds Minor
Cache key doesn't include Package.resolved for other deps Low

The core logic changes (issue #192 fix, uniquingKeysWith, access-level fix) look correct and well-tested. The main blockers are the local action reference in codeql.yml and the unexplained removal of strict concurrency flags.

@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Code Review: PR #36 — Local MistKit Dependency & Workflow Updates

Overview

This PR aligns CelestraCloud with BushelCloud's pattern for MistKit dependency management — using a local path dependency for development and CI-time sed substitution to point at a remote branch in GitHub Actions. It also refactors several Swift source files, modernizes tooling (Mint → mise), and upgrades CI action versions.


Positives

  • Stringly-typed status eliminated: Replacing status: "success" with status: .success (typed SimpleStatus enum) is a genuine improvement in type-safety.
  • Format injection fix in header.sh: Properly escaping % characters before passing user inputs to printf prevents format-string injection. Good defensive change.
  • Refactoring into smaller files: Extracting UpdateSummary, ExitError, FeedUpdateProcessor+Fetch, UpdateCommand+Reporting, and ConfigurationError each into their own files improves navigability.
  • CI optimization: Splitting build-macos into a lightweight always-run job and a build-macos-platforms full-matrix job (gated on main/semver) reduces CI cost on feature branches.
  • --dry-run for setup-cloudkit-schema.sh: Useful safety valve for validating schema without importing.
  • curl install step in Ubuntu CI: Proactively installing curl before Codecov upload avoids silent failures in minimal containers.

Issues & Concerns

Bug: Help text references wrong container in setup-cloudkit-schema.sh

In the newly added --help output:

echo "  CLOUDKIT_CONTAINER_ID   CloudKit container ID (default: iCloud.com.brightdigit.Bushel)"

This says Bushel, not Celestra. Looks like copy-paste from BushelCloud.


Inconsistency: PR description vs. actual MISTKIT_BRANCH value

The PR description states CI tests against MistKit's main branch (not version tags), but the workflows set:

MISTKIT_BRANCH: v1.0.0-beta.2

This is a tag reference, not main. The codeql.yml also hardcodes branch: v1.0.0-beta.2 directly. Clarify whether the intent is to track main continuously or pin to a specific beta.


CelestraKit pinned to a branch name, not a version requirement

.package(url: "https://github.com/brightdigit/CelestraKit.git", branch: "v0.0.3"),

Using a branch-named dependency is mutable — if v0.0.3 is ever force-pushed or deleted, the build silently breaks. Prefer exact: "0.0.3" or from: "0.0.3" for reproducibility.


Commented-out code in Package.swift should be deleted

The unsafeFlags block is commented out rather than removed. If these flags are now redundant (Swift 6 language mode enforces strict concurrency by default), delete the block entirely. Dead commented code creates confusion about intent.


Visibility widening in FeedUpdateProcessor

Several private properties were widened to internal to support the new FeedUpdateProcessor+Fetch.swift extension:

- private let fetcher: RSSFetcherService
+ internal let fetcher: RSSFetcherService
// ... (all 6 properties)

This leaks implementation details to the module. Consider whether the extension could be kept in the same file (or use fileprivate), or if the refactoring boundary is worth the wider visibility.


actions/checkout@v6 — verify stability

All checkout steps are upgraded from @v4 to @v6. Confirm v6 is stable and not a pre-release before merging.


FileScopedDeclarationPrivacy: false in .swift-format

Changed from true to false. This disables the rule that required fileprivate over private at file scope. Given the cross-file extension refactoring, this is consistent — just note it weakens the default encapsulation preference.


Summary

The core dependency management approach is sound and matches the BushelCloud pattern. The code refactoring is clean (typed status enum, split files, moved types). The main actionable items before merging are:

  1. Fix the help text referencing iCloud.com.brightdigit.Bushel in setup-cloudkit-schema.sh
  2. Clarify MISTKIT_BRANCH — pin to main or a specific tag, and update the PR description to match
  3. Switch CelestraKit to a version requirement rather than a branch pin
  4. Delete the commented-out unsafeFlags block from Package.swift

Generated with Claude Code

@claude

claude Bot commented May 22, 2026

Copy link
Copy Markdown

Code Review

Overview

This PR aligns CelestraCloud with BushelCloud's MistKit dependency pattern: local path in Package.swift for development, with CI sed steps swapping it to the remote main branch. It also brings a significant CI overhaul (dynamic matrices, mise replacing Mint, action version bumps) and mechanical Swift cleanup (copyright year, internal import, @available removal, code splitting to satisfy the new one_declaration_per_file SwiftLint rule).


Issues

Bug: Wrong product name in setup-cloudkit-schema.sh help text

echo "  CLOUDKIT_CONTAINER_ID   CloudKit container ID (default: iCloud.com.brightdigit.Bushel)"

This is a copy-paste artifact from BushelCloud. Should read iCloud.com.brightdigit.Celestra.


Inconsistency: codeql.yml hardcodes v1.0.0-beta.2 while CelestraCloud.yml uses ${{ env.MISTKIT_BRANCH }}

CelestraCloud.yml correctly centralises the MistKit ref in MISTKIT_BRANCH: v1.0.0-beta.2. codeql.yml still has it hardcoded:

# codeql.yml
branch: v1.0.0-beta.2

These will drift. Move it to env in codeql.yml the same way, or document that they must be updated together.


Regressed safety: .unsafeFlags block commented out, not replaced

// .unsafeFlags([
//   "-warn-concurrency",
//   "-enable-actor-data-race-checks",
//   "-strict-concurrency=complete",
//   ...
// ])

CLAUDE.md states: "Code must be concurrency-safe with proper actor isolation." Commenting these out silently disables strict concurrency checking. Swift 6.2 supports these as proper settings:

.enableUpcomingFeature("StrictConcurrency"),

Or via a proper SwiftSetting rather than unsafeFlags. The right fix is to replace with a supported equivalent, not to remove the guard entirely.


Widened access: FeedUpdateProcessor properties promoted from private to internal

All six fields (fetcher, robotsService, rateLimiter, skipRobotsCheck, articleSync, metadataBuilder) are now internal so FeedUpdateProcessor+Fetch.swift can access them. This is the right structural move when splitting into extension files, but consider whether these need to be this visible. Since FeedUpdateProcessor is already internal, this doesn't leak into the public API, so the blast radius is contained — just worth being aware of.


Notes / Minor Items

MISTKIT_BRANCH is misnamed — it holds a tag ref (v1.0.0-beta.2), not a branch name. The naming is misleading if someone reads it as a mutable tracking branch. MISTKIT_REF or MISTKIT_TAG would be clearer.

setup-mistkit@main — the action is pinned to main with no SHA or version tag, so an upstream push to MistKit's main branch can silently change CI behaviour. Consider pinning to a commit SHA or a stable ref for reproducibility.

CelestraKit uses branch: "v0.0.3" — this is a branch reference, not a tagged release. Package.swift already has the revision pinned in Package.resolved, so it resolves deterministically for now, but swift package update will float to whatever v0.0.3 branch HEAD points to. If this is intended as a semver tag, use .exact("0.0.3") or from: "0.0.3" instead.

brightdigit/swift-build@v1 (floating) — intentionally unpinned for auto-updates within v1.x, which is the standard pattern for first-party actions. Fine as long as the action maintains a stable @v1 moving tag.


What Works Well

  • The header.sh format-specifier injection fix is a solid security improvement.
  • UpdateSummary, ExitError, UpdateCommand+Reporting, and FeedUpdateProcessor+Fetch are clean extractions that each satisfy one_declaration_per_file without sacrificing readability.
  • The dynamic CI matrix (configure job → slimmer PR builds, full matrix on main) is a nice optimisation — faster feedback on PRs without sacrificing coverage on merge.
  • UpdateReport.FeedResult.status moving from raw String to the typed SimpleStatus enum closes a whole class of typo bugs.
  • Removing @available(macOS 13.0, *) / @available(macOS 11.0, ...) annotations cleans up a lot of noise now that the deployment target is enforced by swift-tools-version: 6.2.

Summary

Three things to fix before merging: the "Bushel" copy-paste in the script help text, the codeql.yml hardcoded branch diverging from the central env var, and the commented-out concurrency flags (either restore them with a supported Swift 6.2 mechanism, or explicitly document that strict concurrency is now enforced by the language version itself and the flags are truly redundant). The rest are polish-level items.

Exercise the MistKit TIMESTAMP write/read fix (brightdigit/MistKit#375) in CI
before it lands on v1.0.0-beta.2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented May 22, 2026

Copy link
Copy Markdown

Code Review: Use local MistKit in CelestraCloud and update workflows for main branch

Overview

This PR aligns CelestraCloud with BushelCloud's pattern by switching from a remote versioned MistKit dependency to a local path (../..). It also adapts to a breaking MistKit API change (per-call database selection), fixes several long-standing bugs, and modernizes the CI matrix. The scope is broader than the title suggests — there are meaningful bug fixes and Swift code improvements alongside the dependency management change.


Issues & Findings

🔴 Critical

Branch inconsistency between workflows — likely to cause CI failures

CelestraCloud.yml and update-feeds.yml both set:

env:
  MISTKIT_BRANCH: 375-fieldvaluerequest-timestamp

But codeql.yml hardcodes a different branch:

- name: Setup MistKit
  uses: brightdigit/MistKit/.github/actions/setup-mistkit@main
  with:
    branch: v1.0.0-beta.2  # ← different from other workflows

375-fieldvaluerequest-timestamp is a feature branch — it can be rebased, force-pushed, or deleted once it merges. The PR description says "CI tests against MistKit's main branch" but neither v1.0.0-beta.2 nor 375-fieldvaluerequest-timestamp is main. The MISTKIT_BRANCH env var should be the target branch for ongoing CI (likely main once the feature merges), and codeql.yml should be consistent with it.


🟡 Notable Concerns

unsafeFlags block commented out instead of removed or migrated

Package.swift now comments out the entire unsafeFlags([...]) block, which included -strict-concurrency=complete, -warn-concurrency, and -enable-actor-data-race-checks. The CLAUDE.md explicitly lists strict concurrency checking as an enabled project feature. These flags may have been removed to compile against the new MistKit API, but silently disabling them is a regression in safety. If they're incompatible with Swift 6.2's built-in strict concurrency, they should be removed entirely (Swift 6 provides this via swiftSettings without unsafe flags). Leaving them as comments risks them being forgotten.

FileScopedDeclarationPrivacy switched from true to false

This disables the swift-format rule that requires file-scoped declarations to use private instead of internal. No rationale is given. This was likely needed to satisfy the new one_declaration_per_file SwiftLint rule (a file with one internal type no longer needs private), but the trade-off — giving up file-scope encapsulation enforcement — is worth a brief note.

FeedUpdateProcessor fields changed from private to internal

In FeedUpdateProcessor.swift, fetcher, robotsService, rateLimiter, skipRobotsCheck, articleSync, and metadataBuilder were all private and are now internal. This was required to access them from FeedUpdateProcessor+Fetch.swift. However, widening access is a lasting API decision — if the extension file is within the same module, internal is the right access level, but it exposes these details to all consumers of the module. Confirm this is intentional and not just a workaround for the extension split.


🟢 Good Fixes

GUID deduplication workaround finally removed

ArticleSyncService.syncArticles previously hard-coded let existingArticles: [Article] = [] with a TEMPORARY + TODO comment. The fix in issue #192 is now applied and the proper GUID query is restored. This was a correctness bug (all articles were treated as new on every run), so removing the workaround is important.

deleteAllFeeds pagination is now correct

The old loop used if feeds.count < 200 { break } to detect the last page, which fails when CloudKit returns exactly 200 records as the final batch. The new code uses continuationMarker correctly.

queryFeeds upgraded to queryAllRecords

Using queryAllRecords instead of queryRecords means feeds beyond the first page are no longer silently dropped. Substantial reliability improvement for accounts with many feeds.

ArticleCategorizer now handles duplicate GUIDs gracefully

Switching from uniqueKeysWithValues: to uniquingKeysWith: { first, _ in first } prevents a crash when CloudKit returns records with duplicate GUIDs. The comment explains the intent clearly.

header.sh format-string injection fix

The original printf "$header_template" "$filename" "$package" ... was vulnerable to format-specifier injection if any input (e.g. a company name or package name) contained %s, %d, etc. The fix correctly escapes % in all user-provided values before passing them to printf. Good catch.

queryArticleBatch now filters by feedRecordName at query time

Previously filtered in-memory after fetching all articles. The new code appends .equals("feedRecordName", ...) to the CloudKit query, reducing data transfer.


🔵 Minor / Style

  • New test files (CelestraErrorTests+Description.swift, CelestraErrorTests+RecoverySuggestion.swift) carry Copyright © 2025 while every other file in the PR was updated to 2026. Inconsistent.
  • stride(from:to:by:).map { Array(...) } appears three times replacing chunked(into:). This is correct but somewhat verbose. A file-private helper or a one-liner extension would reduce duplication without adding a public API.
  • FeedUpdateResult now defines SimpleStatus inside the enum where it originally appeared after the cases. This is a cleaner organization.
  • New internal import declarations throughout are consistent with Swift's module import hygiene best practices — good.
  • Removal of @available(macOS 11.0, ...) annotations is appropriate since Swift 6.2 targets imply a later baseline.

Summary

The core MistKit integration changes are well-executed and fix real bugs. The main risk before merging is the CI branch inconsistency (375-fieldvaluerequest-timestamp in main CI vs v1.0.0-beta.2 in CodeQL), which should be resolved to a single stable reference. Revisiting the commented-out concurrency flags would also be worthwhile.

🤖 Generated with Claude Code

The compiled-binary cache keyed only on CelestraCloud's own sources, so a new
MistKit commit on the pinned branch produced a stale cache hit and the integration
test ran against the old binary. Resolve MISTKIT_BRANCH to its HEAD commit and fold
it into the cache key so MistKit changes force a rebuild.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented May 23, 2026

Copy link
Copy Markdown

Code Review — PR #36: Use local MistKit in CelestraCloud and update workflows for main branch

Overview

This PR aligns CelestraCloud with BushelCloud's pattern by switching the MistKit dependency from a versioned remote to a local path (../..) for development, with CI workflows substituting the remote main branch at build time. It also ships a significant workflow modernisation (dynamic build matrix, mise replacing mint, upgraded action versions) plus several Swift source improvements (typed status enums, internal import, file extraction, removed @available guards).


Issues & Concerns

🔴 High Priority

1. Hard-coded feature branch in MISTKIT_BRANCH

In both CelestraCloud.yml and update-feeds.yml:

MISTKIT_BRANCH: 375-fieldvaluerequest-timestamp

This is a feature branch name, not main. CI is currently tracking a development branch across all jobs, which means:

  • Any force-push or rebase on that branch will silently break CI
  • The branch may be deleted after the MistKit feature lands
  • The PR description says "CI tests against MistKit's main branch" — this contradicts the actual value

Recommendation: Change to main before merge, or make it an explicit main reference that the PR title promises.

2. codeql.yml uses a pinned version tag, not the branch

# codeql.yml
branch: v1.0.0-beta.2

All other workflows use MISTKIT_BRANCH (375-fieldvaluerequest-timestamp), but CodeQL is pinned to a different version entirely. This means CodeQL scans against a different MistKit than what builds/tests run against — defeating the "catch breaking changes early" goal.

Recommendation: Use the same MISTKIT_BRANCH env variable here for consistency.

3. Package.swift: unsafeFlags block commented out without replacement

The entire block of strict concurrency compiler flags has been removed:

// .unsafeFlags([
//   "-strict-concurrency=complete",
//   ...
// ])

This silently drops Swift 6 data-race detection from local builds. The PR description doesn't mention this change, and CLAUDE.md explicitly states the project must use -strict-concurrency=complete. If the flags were moved to a swiftSettings API alternative (.enableUpcomingFeature, .strictConcurrency), that should be shown; if they were removed entirely this is a regression.

Recommendation: Either restore via the proper Swift 6.2 API (.swiftSetting(.strictConcurrency(.complete))) or document clearly why they were removed.

🟡 Medium Priority

4. FeedUpdateProcessor: private → internal fields without justification

// Before
private let fetcher: RSSFetcherService
private let robotsService: RobotsTxtService
...

// After
internal let fetcher: RSSFetcherService
internal let robotsService: RobotsTxtService

These fields are only used by the extension in FeedUpdateProcessor+Fetch.swift. Making them internal for cross-file extension access within the same module is valid Swift, but they're effectively module-private implementation details. Since both files are in the same CelestraCloud target, internal works — but it unnecessarily widens the visible API surface of the struct. Consider whether these should be package access or left private with a different factoring.

5. setup-cloudkit-schema.sh --help refers to BushelCloud

echo "  CLOUDKIT_CONTAINER_ID   CloudKit container ID (default: iCloud.com.brightdigit.Bushel)"

This is copy-pasted from BushelCloud and should reference Celestra. A cosmetic issue but confusing for users.

6. Cache key excludes Package.resolved without a compensating strategy for stable deps

# Before
key: ...-${{ hashFiles('Sources/**/*.swift', 'Package.swift', 'Package.resolved') }}-...
# After  
key: ...-${{ hashFiles('Sources/**/*.swift', 'Package.swift') }}-${{ steps.mistkit-sha.outputs.sha }}-...

Removing Package.resolved from the hash makes sense when MistKit is a local path (no resolved entry), but other pinned dependencies (CelestraKit, swift-crypto, etc.) are still in Package.resolved. A new version of those dependencies won't bust the binary cache. This is an acceptable trade-off for this PR's goals, but should be documented.

7. build-macos drops the macOS platform build type

The original matrix included type: macos in build-macos. The new PR splits this into build-macos (SPM + iOS, always runs) and build-macos-platforms (macOS/watchOS/tvOS/visionOS, full matrix only). The macOS build type is now in build-macos-platforms — ensure the conditional needs.configure.outputs.full-matrix == 'true' is satisfied for PRs targeting main, or macOS-specific tests will be skipped on those PRs. The configure job logic looks correct for this, but it's worth verifying in practice.

🟢 Low Priority / Positive Observations

Improvements worth calling out:

  • Typed status enums (status: .success vs status: "success") — solid improvement, eliminates stringly-typed comparisons and catches mistakes at compile time.
  • internal import throughout — correct use of Swift 6 access-level imports, reduces transitive leakage.
  • ExitError extraction to its own file — good adherence to one_declaration_per_file SwiftLint rule now added.
  • UpdateSummary / UpdateCommand+Reporting extractionsUpdateCommand.swift was too large; the split is clean.
  • header.sh %-escaping — real security/correctness fix for printf format injection via filenames or package names.
  • mise replacing mint — reduces CI complexity by removing the self-bootstrapping git clone + swift run mint dance.
  • concurrency: cancel-in-progress — correct addition, prevents redundant queued runs on rapid pushes.
  • @available(macOS 13.0, *) guards removed — the minimum deployment target must be >= 13 globally, so these were noise.
  • fail-fast: false on Ubuntu matrix — good; a failure on one Swift version shouldn't cancel sibling runs.
  • actions/checkout@v6 — version bump across the board is correct.

Summary

The architectural approach (local path dep + CI sed substitution) is sound and consistent with BushelCloud. The Swift source changes are well-motivated and improve type safety. The main blockers before merge are:

  1. MISTKIT_BRANCH: 375-fieldvaluerequest-timestamp should be main (or the branch this PR is tracking in MistKit should be identified explicitly in the PR description as intentional)
  2. codeql.yml using a different MistKit version than CI builds — needs alignment
  3. The removed unsafeFlags concurrency block needs either restoration via proper API or explicit explanation

The stale "Bushel" reference in the schema script help text is a quick fix.

The 375-fieldvaluerequest-timestamp source branch was deleted after PR
#377 squash-merged into v1.0.0-beta.2 (a7a5654), leaving the CelestraCloud
workflow envs pointing at a missing ref.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented May 23, 2026

Copy link
Copy Markdown

Code Review: PR #36 — Use local MistKit in CelestraCloud and update workflows for main branch

Overview

This PR makes two interrelated sets of changes:

  1. Dependency management: Switches MistKit from a versioned remote dependency to a local path (../..), with CI workflows patching it back to a remote branch before building
  2. MistKit API adaptation: Updates all call sites to match a new MistKit API where database: is now selected per-call rather than set at construction time, and queryRecords now returns QueryResult with pagination support

The PR also delivers several quality improvements: a typed Status enum replacing raw strings in UpdateReport.FeedResult, re-enabled GUID deduplication queries (previously disabled due to a now-fixed CloudKit bug), toolchain upgrades (Xcode 26.4, swift 6.3 stable), and migration from Mint to mise for tooling.


What Works Well

Typed status enum (UpdateReport.FeedResult.Status) — replacing raw strings like "success", "notModified" with a proper enum is a clear improvement. It makes the JSON output's valid values self-documenting and catches typos at compile time.

Re-enabled GUID deduplication — removing the // TEMPORARY: Skip GUID query workaround and restoring the real query path (ArticleSyncService.swift:154-158) is a meaningful fix. The comment explaining why the workaround existed (MistKit issue #192) is appropriately updated.

Graceful duplicate GUID handling — using uniquingKeysWith: { first, _ in first } in ArticleCategorizer instead of uniqueKeysWithValues: prevents crashes when CloudKit returns duplicate GUIDs.

successRate moved to stored property — computing it once in init instead of every access (especially during JSON encoding) is a small but correct improvement.

FeedUpdateProcessor member visibility — the privateinternal promotion for service dependencies enables the new FeedUpdateProcessor+Fetch.swift extension to access them from a separate file without going through a parameter pass. This is a reasonable trade-off for splitting a large type, though see notes below.

one_declaration_per_file SwiftLint rule — a good enforcement of the project's file-per-type convention.

Cache key fix for update-feeds.yml — resolving the MistKit branch SHA and including it in the cache key (steps.mistkit-sha.outputs.sha) prevents stale cache hits when MistKit changes on the same branch. This is the right solution.


Issues and Suggestions

1. setup-cloudkit-schema.sh help text references wrong project name

echo "  CLOUDKIT_CONTAINER_ID   CloudKit container ID (default: iCloud.com.brightdigit.Bushel)"

The default shown is iCloud.com.brightdigit.Bushel (BushelCloud), not iCloud.com.brightdigit.Celestra. This looks like a copy-paste artifact from BushelCloud.

2. setup-cloudkit-schema.sh error message references wrong project

echo "     d. Store it securely (e.g., ~/.cloudkit/bushel-private-key.pem)"

Same copy-paste issue — the path suggests bushel-private-key.pem rather than a Celestra-specific filename.

3. MISTKIT_BRANCH is hardcoded in two places

CelestraCloud.yml sets MISTKIT_BRANCH: v1.0.0-beta.2, and codeql.yml hardcodes branch: v1.0.0-beta.2 directly in the step rather than reading from an env var. This means updating the branch requires edits in two separate workflow files. Consider extracting it to a single place or having codeql.yml reference the same env var.

4. modifyRecords error contract is subtly changed

The new CloudKitRecordOperating conformance in CloudKitRecordOperating.swift maps per-record .failure results to a thrown CloudKitError. The comment describes this as "all-or-nothing batch handling." However, createArticles/updateArticles in ArticleCloudKitService uses non-atomic operations specifically to allow partial success, and accumulates results into BatchOperationResult. If any single record in a batch fails, the entire batch now throws rather than recording partial success. This changes the existing batch error-handling behavior. Is this intentional?

5. updateFeedMetadata success/error logic

return metadata.failureCount == 0
    ? .success(articlesCreated: articlesCreated, articlesUpdated: articlesUpdated)
    : .error(message: "Feed update had failures")

If metadata.failureCount > 0 but articles were still created/updated, the caller loses the article counts (both are reported as 0 in the .error case). The summary statistics will under-report actual article throughput. Consider returning .success with the partial counts, or creating a .partialSuccess case if partial failure is worth distinguishing.

6. FeedUpdateProcessor fields promoted to internal without documentation

The fields fetcher, robotsService, rateLimiter, skipRobotsCheck, articleSync, and metadataBuilder were private and are now internal to enable the FeedUpdateProcessor+Fetch.swift extension. This is a valid Swift approach, but since FeedUpdateProcessor is already internal (not public), there's no module-boundary concern — the change is mostly about file-level access. The comment explaining why this was done would help future readers distinguish intentional API from an accidental exposure.

7. UpdateSummary and related types are now internal but referenced from internal extension

UpdateSummary.swift exposes the type as internal but UpdateCommand+Reporting.swift (also internal) takes it as a parameter. This is fine, but the import CelestraCloudKit in UpdateSummary.swift is a package-local import for FeedUpdateResult — ensure the import access level (internal import) matches usage.

8. Commented-out unsafeFlags in Package.swift

The strict concurrency flags (-strict-concurrency=complete, -warn-concurrency, etc.) are now commented out rather than removed. If they're temporarily disabled while adapting to the new MistKit API, a TODO comment explaining when they should be re-enabled would be helpful. If they're permanently removed, deleting the block is cleaner than leaving 15 lines of commented code.

9. actions/checkout@v6 — non-existent version

At time of writing, actions/checkout@v4 is the latest stable release. @v6 does not exist and will fail at runtime. All workflow files in this PR use @v6 (e.g., CelestraCloud.yml lines 130, 187, 248, 324). This should be reverted to @v4.

10. FileScopedDeclarationPrivacy disabled in .swift-format

This rule was true (enforce private or fileprivate for file-scoped declarations) and is now false. Given that the PR promotes several private members to internal to support cross-file extensions, this change avoids formatter complaints. However, disabling it project-wide means declarations intended to be private no longer get enforced by the formatter. If the intent is to accommodate the extension pattern, a more targeted approach would be preferred over disabling the rule entirely.


Minor Observations

  • MistKit.git is now a branch: pin for CelestraKit (branch: "v0.0.3") rather than a version tag. This makes the build non-reproducible without a locked commit — acceptable for development, but worth tracking.
  • The Mintfile deletion and migration to mise is clean. The lint.sh simplification (removing all the Mint path detection logic) is a net improvement.
  • Copyright year updates from 2025 → 2026 are correct given today's date.
  • Smart-quote substitution in header.sh ("Software""Software") is a nice cleanup.
  • The % escaping fix in header.sh (using sed 's/%/%%/g') is a legitimate security improvement — it prevents format specifier injection when filenames or company names contain %.

Summary

The core intent of this PR is sound: aligning with BushelCloud's dependency pattern enables tight local development against MistKit while still testing against the latest main in CI. The MistKit API adaptation (per-call database: selection, queryAllRecords protocol extension) is well-structured. The main action items before merge:

  1. Fix actions/checkout@v6@v4 in all workflow files (this will cause CI failures)
  2. Fix the BushelCloud copy-paste in setup-cloudkit-schema.sh
  3. Clarify whether the modifyRecords batch error-contract change is intentional
  4. Decide whether to restore or remove (not comment out) the unsafeFlags block

Scripts/lint.sh now applies swift-format/swiftlint/header checks to Tests
as well as Sources. header.sh runs over both trees, adding license headers
to previously-bare test files and refreshing the copyright year.

Remove unused code surfaced by periphery: FeedUpdateResult.SimpleStatus and
its property, assign-only QueryCall fields in MockCloudKitRecordOperator, and
several unused test helpers.

Refactor CelestraError retriability/description/recovery classification to be
data-driven: a payload-free CaseID mirror keys lookup tables for static
descriptions, recovery suggestions, and retriable cases; errorDescription uses
a guard-let over the table with a switch fallback for payload-carrying cases;
isCloudKitErrorRetriable collapses its non-retriable arm to a default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented May 29, 2026

Copy link
Copy Markdown

Code Review — PR #36: Use local MistKit in CelestraCloud

Overview

This PR aligns CelestraCloud with BushelCloud's pattern for local MistKit development: Package.swift points to ../.. (local path), and CI workflows use a setup-mistkit action plus sed to swap in the real remote URL. It also does a lot of housekeeping: workflow modernization, Mint → mise, @available guard removal, refactoring types into their own files, and a security fix in Scripts/header.sh.

The core idea is sound and the CI cache-invalidation story (resolving MistKit branch SHA before caching the binary) is well-designed.


Issues

Hardcoded version in codeql.yml vs. env var elsewhere

# codeql.yml line 61
branch: v1.0.0-beta.2   # ← literal string

# CelestraCloud.yml and update-feeds.yml
branch: ${{ env.MISTKIT_BRANCH }}  # ← env var

codeql.yml is the only workflow that doesn't use ${{ env.MISTKIT_BRANCH }}. When the branch is bumped to beta.3, this will silently test different MistKit versions in security scanning vs. build CI. It should reference the same env var.


FeedUpdateProcessor properties widened from private to internal

fetcher, robotsService, rateLimiter, skipRobotsCheck, articleSync, and metadataBuilder are now internal to allow FeedUpdateProcessor+Fetch.swift to access them. The standard way to handle same-module cross-file access is to keep the properties internal within the module — but since these are already in a non-public internal struct, this works. The real question is whether all six needed widening. skipRobotsCheck, rateLimiter, and fetcher are only used in processOneFeed (in the original file), not in the new extension. If so, those three can remain private.


setup-cloudkit-schema.sh help text references the wrong project

echo "     d. Store it securely (e.g., ~/.cloudkit/bushel-private-key.pem)"

This is copy-pasted from BushelCloud — it says bushel instead of celestra.


Local path dependency breaks standalone checkout

Package.swift now requires MistKit to exist at ../.. relative to the repo root. Anyone who clones only CelestraCloud (the common case for contributors) will get an immediate build failure. The CI sed workaround is well-documented in the workflows, but CLAUDE.md and README.md should mention that local development requires MistKit cloned as a sibling directory, or provide the sed commands developers need to run manually.


CelestraKit pinned to a branch, not a tag

.package(url: "https://github.com/brightdigit/CelestraKit.git", branch: "v0.0.3"),

branch: means the resolved revision can advance silently if new commits land on v0.0.3. Package.resolved captures a specific revision, but the intent of a version-named branch is ambiguous — if this is meant to track a release, from: "0.0.3" or .exact("0.0.3") is more explicit and reproducible.


unsafeFlags block commented out rather than removed

// .unsafeFlags([
//   "-warn-concurrency",
//   ...
// ])

This is dead code. If these flags are superseded by Swift 6.2's built-in strict concurrency checking (which is the likely reason), a one-line comment stating that is more useful than 12 commented-out lines. Suggest removing the block and adding a brief comment if the context matters.


Suggestions

FileScopedDeclarationPrivacy set to false

The previous true enforced fileprivate over private for file-scoped declarations. Turning it off is a reasonable choice (especially now that types are split across files), but it's worth confirming this is intentional since it changes linting behavior project-wide.

one_declaration_per_file rule added to SwiftLint

This pairs well with the file-splitting refactor in this PR. Good addition.

header.sh format-string injection fix

# Before (vulnerable)
header=$(printf "$header_template" "$filename" "$package" ...)

# After (safe)
filename=$(basename "$file" | sed 's/%/%%/g')
header=$(printf "$header_template" "$filename" "$package_safe" ...)

Solid security fix. The heredoc approach for the template is also cleaner.

Conditional coverage upload (if: steps.build.outputs.contains-code-coverage == 'true')

Good defensive improvement — avoids failing CI on platforms that don't produce coverage output.

MistKit SHA resolution for cache key

SHA=$(git ls-remote https://github.com/brightdigit/MistKit.git "$MISTKIT_BRANCH" | head -n1 | cut -f1)

This is clever — the binary cache now busts correctly when a new MistKit commit lands on the branch, without needing Package.resolved in the key. Well done.


Summary

The approach is solid. The main actionable items before merging:

  1. Fix codeql.yml to use ${{ env.MISTKIT_BRANCH }} instead of the hardcoded string
  2. Fix the bushel typo in setup-cloudkit-schema.sh
  3. Consider pinning CelestraKit to a tag rather than a branch
  4. Document the local MistKit sibling-directory requirement for contributors
  5. Remove the commented-out unsafeFlags block

The code quality improvements (type extraction, internal import, @available removal, status enum) are all positive changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented May 29, 2026

Copy link
Copy Markdown

Code Review

Overview

This PR aligns CelestraCloud with BushelCloud's pattern for MistKit dependency management: local path (../..) in Package.swift for development, with a CI composite action (setup-mistkit) that performs the branch substitution before building. It also brings significant CI/tooling improvements and Swift source refactoring.


What's Working Well ✅

  • MistKit composite action: Using brightdigit/MistKit/.github/actions/setup-mistkit@main is cleaner than inline sed commands in each job.
  • Dynamic CI matrix: The configure job that gates full vs. minimal builds on branch/PR target is a smart resource optimization.
  • Concurrency groups: Adding cancel-in-progress: true prevents duplicate runs on rapid pushes.
  • Mise migration: Dropping the manual Mint bootstrap in favor of jdx/mise-action simplifies tooling significantly.
  • internal import: Proper use of Swift 6.2's module boundary imports across the codebase.
  • one_declaration_per_file: Adding this SwiftLint rule and splitting ExitError, UpdateSummary, and UpdateCommand+Reporting into separate files is consistent and clean.
  • Typed status enum: Replacing raw strings ("success", "error") with typed enum cases in UpdateReport.FeedResult is a clear type safety win.
  • header.sh security fix: Escaping % characters in user-provided values before passing to printf correctly prevents format-string injection. Good catch.
  • --dry-run for schema setup: Useful addition for validation without side effects.
  • queryAllRecords pagination: The maxPages safety limit on the new paginated query method is a good guard against unbounded loops.

Issues and Concerns

🔴 High Priority

1. actions/checkout@v6 may not exist

All jobs use actions/checkout@v6. As of the current stable release train, v4 is the latest major version of this action. Using a non-existent version would cause all CI jobs to fail. Please verify this is intentional and that v6 is available.

# Verify this version exists before merging
- uses: actions/checkout@v6

2. Strict concurrency checks silently removed

The unsafeFlags block in Package.swift is commented out rather than replaced, which removes -strict-concurrency=complete and the actor data race checks. Swift 6.2 provides a first-class API for this:

// Replace the commented-out block with:
.enableUpcomingFeature("StrictConcurrency")
// or in Package.swift with Swift 6.0+ tools version:
.unsafeFlags(["-strict-concurrency=complete"])  // keep until SwiftSetting API is stable

Silently dropping these checks risks concurrency regressions slipping through without compiler feedback.

🟡 Medium Priority

3. MISTKIT_BRANCH is a version tag, not a branch

The env variable is set to v1.0.0-beta.2 in both CelestraCloud.yml and update-feeds.yml. Version tags are not branches. The variable name is misleading and could cause confusion when resolving the SHA via git ls-remote. Consider renaming to MISTKIT_REF or MISTKIT_TAG.

4. codeql.yml hard-codes the MistKit ref

# codeql.yml - hard-coded
branch: v1.0.0-beta.2

# CelestraCloud.yml - uses env variable
branch: ${{ env.MISTKIT_BRANCH }}

When bumping the MistKit version, codeql.yml must be updated separately. It should reference the same env variable or a shared workflow input to stay in sync.

5. Removed error description tests without replacement

The following tests were deleted from CelestraErrorTests.swift:

  • testQuotaExceededDescription
  • testNetworkUnavailableDescription
  • testPermissionDeniedDescription
  • testInvalidFeedDataDescription
  • testRecordNotFoundDescription

If these error descriptions are still present in the codebase, there's now no test coverage for them. If the underlying cases were removed, that's fine — but please confirm.

6. queryAllRecords mock ignores maxPages

The mock implementation in MockCloudKitService satisfies the protocol but completely ignores maxPages and returns all results in one go. Tests for the actual pagination behavior (multi-page traversal, maxPages enforcement) are absent. Given this is new functionality, coverage would be valuable.

🟢 Low Priority

7. FileScopedDeclarationPrivacy: false rationale unclear

This was true (enforcing private over fileprivate) and is now false. The commit doesn't explain why. If it's to support extension-based file splitting (where fileprivate is meaningful), a brief comment in .swift-format or the PR description would help future maintainers.

8. Unquoted $PACKAGE_DIR in lint.sh

# Current (unquoted - breaks on paths with spaces)
pushd $PACKAGE_DIR

# Should be:
pushd "$PACKAGE_DIR"

Minor, but could cause issues on machines where the checkout path contains spaces.

9. Package.swift local path with no developer guidance

The path: "../.." dependency requires MistKit to be checked out as a sibling directory. The update-subrepo.sh mentions this, but a developer cloning only CelestraCloud will get a confusing error. Consider adding a note to the README or .env.example describing the required directory layout.


Summary

The core concept is solid and aligns with the BushelCloud pattern. The CI restructuring is a meaningful improvement. The main blockers before merge are:

  1. Verify actions/checkout@v6 exists
  2. Restore strict concurrency checking via a proper SwiftSetting API
  3. Consolidate the MistKit ref between codeql.yml and CelestraCloud.yml

🤖 Review generated by Claude Code

@claude

claude Bot commented May 31, 2026

Copy link
Copy Markdown

Code Review: Use local MistKit in CelestraCloud and update workflows for main branch

Overview

This PR modernizes CelestraCloud's MistKit integration by switching to a local path dependency for tighter development iteration, and includes a substantial CI overhaul, MistKit API adaptations, and code-quality improvements. The overall direction is solid — the dynamic matrix, concurrency cancellation, and mise migration are all good choices. A few issues below warrant attention before merging.


Issues

🐛 header.sh will fail with set -eo pipefail

read -r -d '' header_template <<'EOF'

read -r -d '' reads until a NUL byte, which it never finds in a heredoc; it always exits with code 1 (EOF before delimiter). Since the script opens with set -eo pipefail, this will cause an immediate failure. The fix is to suppress the exit code:

read -r -d '' header_template <<'EOF' || true

🐛 Copy-paste artifacts from BushelCloud in setup-cloudkit-schema.sh

Two references to "Bushel" were left in the help text:

  • echo " CLOUDKIT_CONTAINER_ID CloudKit container ID (default: iCloud.com.brightdigit.**Bushel**)" (line ~968 in the diff)
  • echo " d. Store it securely (e.g., ~/.cloudkit/**bushel**-private-key.pem)" (line ~1014)

These should reference Celestra.

⚠️ codeql.yml hardcodes branch instead of using env var

- name: Setup MistKit
  uses: brightdigit/MistKit/.github/actions/setup-mistkit@main
  with:
    branch: v1.0.0-beta.2   # ← hardcoded

CelestraCloud.yml and update-feeds.yml correctly use ${{ env.MISTKIT_BRANCH }}, but codeql.yml hardcodes the value. A version bump will require updating it separately, creating drift.

⚠️ Commented-out compiler flags in Package.swift

The .unsafeFlags block is commented out rather than removed:

// .unsafeFlags([
//   "-warn-concurrency",
//   "-enable-actor-data-race-checks",
//   "-strict-concurrency=complete",
//   ...
// ])

These flags were providing valuable safety checks. If they conflict with the new MistKit version, remove them cleanly and explain why. If they're still desirable, use proper Swift 6.2 settings (e.g., .enableUpcomingFeature("StrictConcurrency")) instead. Dead commented code in Package.swift is particularly confusing.

⚠️ CelestraKit uses a mutable branch dependency

.package(url: "https://github.com/brightdigit/CelestraKit.git", branch: "v0.0.3"),

Branch references are mutable — a force-push to v0.0.3 changes what CI resolves without any signal in this repo. Using from: "0.0.3" (a semver tag) or a .upToNextMajor version range gives reproducibility. If 0.0.3 isn't tagged yet, tag it before merging.


Observations (non-blocking)

FeedUpdateProcessor properties made internal — This was done to support the new FeedUpdateProcessor+Fetch.swift extension, which is a valid Swift pattern. The properties are still module-internal so the API surface increase is contained.

FeedUpdateResult.status typed enum — Replacing the old string-based status ("success", "notModified") with a typed .error / .success enum is a clear improvement and removes a class of runtime bugs.

Removal of @available(macOS 13.0, *) annotations — Clean-up enabled by the MistKit API simplification. Appropriate.

Mise replacing Mint — Dropping the Mintfile/Mint bootstrap for jdx/mise-action is a reasonable modernization. Ensure the local developer setup docs (CLAUDE.md / README) are updated to reflect mise install for new contributors.

Dynamic CI matrix — The configurebuild-* dependency pattern is well-structured and correctly scopes full-matrix builds to main, semver branches, and PRs targeting them.

header.sh security fix — Escaping % characters in printf format strings is a good catch.

Binary cache key — Removing Package.resolved from the cache key and substituting the MistKit branch SHA is the right call given the local path dependency.


Summary

The PR is well-motivated and the code quality improvements are welcome. The header.sh heredoc exit-code bug is the only functional blocker. The BushelCloud copy-paste strings and codeql.yml hardcoded branch are easy fixes. The commented-out unsafe flags and mutable branch dependency should be resolved before this ships to avoid future confusion.

Replace the string-pair constants in ConfigurationKeys.swift with typed
ConfigKey/OptionalConfigKey definitions, and collapse ConfigurationLoader's
per-option `?? ...Env ?? default` fallback chains into generic read(_:)
helpers that iterate ConfigKeySource.allCases (matching MistDemo/BushelCloud).

Base keys use dashes throughout for consistency and conventional kebab-case
CLI flags (e.g. --cloudkit-container-id, --update-skip-robots-check). ENV var
names and defaults are unchanged; this also makes the loader's
--cloudkit-key-id / --cloudkit-private-key-path secrets specifier match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jun 2, 2026

Copy link
Copy Markdown

Code Review: Use local MistKit in CelestraCloud and update workflows

Overview

This PR aligns CelestraCloud with BushelCloud's development pattern by switching MistKit (and a new ConfigKeyKit) to local path dependencies for tight local development, while CI automatically substitutes remote URLs before building. It also introduces a composite MistKit setup action, upgrades CI tooling (mise replaces Mint, action version bumps), refactors configuration key handling to use a type-safe ConfigKeyKit abstraction, and cleans up access modifiers/file structure across the Swift sources.


Blocking / High Priority

1. Local path dependencies break clone-and-build for contributors

Package.swift now requires local paths (../.. for MistKit, ../../Packages/ConfigKeyKit for ConfigKeyKit). Anyone who clones only this repo gets a build failure immediately. The CI sed-substitution workaround is described in the PR body but not in CLAUDE.md or any contributor-facing docs. Consider adding a note to CLAUDE.md's Environment Setup section explaining the mono-repo layout required for local development.

2. unsafeFlags block commented out in Package.swift

The project documentation and README advertise "Swift 6 strict concurrency with complete checking", but the flags that enforced this (-strict-concurrency=complete, -warn-concurrency, -enable-actor-data-race-checks) are now commented out with no explanation. If MistKit v1.0.0-beta.2 exposes equivalent settings via package traits or the compiler defaults changed, add a brief comment explaining why they are no longer needed. Otherwise this silently lowers the safety bar.

3. codeql.yml hardcodes branch while other workflows use an env var

CelestraCloud.yml and update-feeds.yml define MISTKIT_BRANCH: v1.0.0-beta.2 and reference it consistently. codeql.yml hardcodes branch: v1.0.0-beta.2 in the Setup MistKit step instead of using ${{ env.MISTKIT_BRANCH }}. This will be missed when the next MistKit release comes out. The fix is one line since codeql.yml uses a normal job (workflow-level env is available in with fields).


Medium Priority

4. CelestraKit pinned to a branch name that looks like a tag

.package(url: ..., branch: "v0.0.3")v0.0.3 is conventionally a tag. branch: has mutable semantics and won't update Package.resolved like a version pin would. If this is a real tag, use .package(url: ..., from: "0.0.3") instead.

5. FeedUpdateProcessor implementation fields promoted from private to internal

fetcher, robotsService, rateLimiter, skipRobotsCheck, articleSync, and metadataBuilder are all promoted to internal to allow access from FeedUpdateProcessor+Fetch.swift. This exposes implementation details to the whole module. If file-splitting is the only goal, fileprivate would limit exposure to the file pair, or the extension could remain in the same file as the type.

6. UpdateReport.Summary.successRate changed from computed to stored

The previous computed property (Double(successCount) / Double(totalFeeds) * 100) was always self-consistent. The new stored property requires callers to supply the value, making it possible to construct a Summary where successRate disagrees with successCount/totalFeeds. If the motivation is Codable round-trip fidelity, a custom CodingKeys that serializes the computed value would preserve correctness.

7. Floating CI action tags

The PR bumps actions/checkout@v4 to @v6 and brightdigit/swift-build@v1.4.2 to @v1. The previous @v1.4.2 was a specific pinned version; @v1 will silently pick up any future v1.x breaking change. Verify this is intentional.


Positive Observations

  • header.sh security fix: The heredoc template + %-escaping of user-provided values before printf prevents format-specifier injection. Good catch.
  • ConfigKeyKit integration: Replacing parallel *Key/*Env string pairs with typed ConfigKey/OptionalConfigKey makes ConfigurationLoader call sites noticeably cleaner and consolidates the fallback logic.
  • one_declaration_per_file SwiftLint rule: Good hygiene addition, consistent with the new file structure.
  • File decomposition: Splitting UpdateSummary, FeedUpdateProcessor+Fetch, UpdateCommand+Reporting, and ExitError into their own files improves navigability significantly.
  • @available cleanup: Removing macOS 11/13 availability guards is correct given the project already requires macOS 26+.
  • Bool CLI flag semantics: read(_ key: ConfigKey<Bool>) correctly treats flag presence as true, matching standard CLI conventions.

Summary

The direction is sound. The three items most worth addressing before merge: (1) add contributor docs for the local-path repo layout, (2) explain why strict-concurrency flags are commented out, and (3) fix the hardcoded branch in codeql.yml.

The ConfigKeyKit dependency uses a path that only resolves inside the
MistKit monorepo (../../Packages/ConfigKeyKit). In CelestraCloud's
standalone CI the package isn't present, so swift build failed with
"the package at '.../Packages/ConfigKeyKit' cannot be accessed".

Mirror BushelCloud: pair each setup-mistkit step with a setup-configkeykit
step (brightdigit/ConfigKeyKit) which rewrites the local path dependency to
the published https://github.com/brightdigit/ConfigKeyKit.git URL. Adds
CONFIGKEYKIT_BRANCH=main to CelestraCloud.yml and update-feeds.yml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jun 2, 2026

Copy link
Copy Markdown

Code Review: Use local MistKit in CelestraCloud (#36)

Overview

This PR switches MistKit and ConfigKeyKit to local path dependencies (../..) for tighter development integration, with CI using custom composite actions (setup-mistkit, setup-configkeykit) to check out the correct branch before building. It also significantly overhauls the CI matrix, migrates from Mint to mise for tooling, introduces typed configuration keys via ConfigKeyKit, and improves code organization throughout.


✅ Strengths

  • Typed configuration keys – Replacing magic strings ("update.delay") with ConfigKey<Double> / OptionalConfigKey<Int> in ConfigurationKeys.swift eliminates a whole class of typo bugs and makes the config surface self-documenting.
  • Typed FeedResult.Status enum – Replacing status: String with status: Status in UpdateReport.FeedResult is a clear improvement; the old string values ("success", "notModified") were not validated at compile time.
  • Smart CI matrix – The new configure job running a full matrix only on main, semver branches, and their PRs is a solid pattern that reduces unnecessary CI minutes on feature branches.
  • MistKit SHA in cache key – Resolving the branch to a commit SHA for the binary cache key (update-feeds.yml:470) correctly prevents stale cache hits when MistKit's branch HEAD moves.
  • header.sh security fix – Escaping % characters in user-provided values before passing them to printf eliminates a format-specifier injection.
  • --dry-run flag for schema setup – Good ergonomic addition for validation without side effects.
  • Code organization – Extracting UpdateSummary, ExitError, UpdateCommand+Reporting, and FeedUpdateProcessor+Fetch into their own files correctly satisfies the new one_declaration_per_file SwiftLint rule.
  • duration and successRate stored at init – Moving these from computed properties to values computed once and stored is correct: computed properties on Codable structs can produce inconsistent serialization if the source values change; storing them at init is the right pattern.

🐛 Bugs / Correctness Issues

1. Copy-paste "Bushel" strings in setup-cloudkit-schema.sh

Two help strings still reference Bushel (the sister project) rather than Celestra:

# Line ~1015 (help output):
"  CLOUDKIT_CONTAINER_ID   CloudKit container ID (default: iCloud.com.brightdigit.Bushel)"

# Line ~1057 (post-failure instructions):
"     d. Store it securely (e.g., ~/.cloudkit/bushel-private-key.pem)"

Both should say Celestra / celestra-private-key.pem.

2. codeql.yml hardcodes branch names instead of referencing env vars

CelestraCloud.yml defines MISTKIT_BRANCH: v1.0.0-beta.2 and CONFIGKEYKIT_BRANCH: main as workflow-level env vars, but codeql.yml duplicates the same values as literals. If you bump the MistKit version you'll need to update two files. Consider extracting to a shared location or at minimum pointing to vars.MISTKIT_BRANCH if defined as a repository variable.


⚠️ Concerns

3. No .mise.toml visible in the diff

Mintfile is deleted and lint.sh now calls swift-format, swiftlint, and periphery directly (expecting them on $PATH via mise). The CI lint job uses jdx/mise-action@v4. But without a .mise.toml (or .tool-versions) committed to the repo, mise has no manifest to install tools from. Unless this file exists on the branch but wasn't included in the diff, the lint job will fail with "command not found".

4. CONFIGKEYKIT_BRANCH: main is non-deterministic

Pinning MistKit to v1.0.0-beta.2 is good. Using main for ConfigKeyKit means any breaking change on that branch will silently break this repo's CI. Consider pinning to a tag or commit SHA, or at minimum add a comment explaining why main is intentional here.

5. unsafeFlags block commented out

// .unsafeFlags([
//   "-strict-concurrency=complete",
//   "-enable-actor-data-race-checks",
//   ...
// ])

Swift 6 enables strict concurrency by default, so the -strict-concurrency=complete flag is redundant, which makes removing it reasonable. But -warn-long-function-bodies and -warn-long-expression-type-checking are genuine developer ergonomics flags that have nothing to do with unsafeFlags being unsafe to ship — they're informational warnings. If they're being removed to fix a build error in Swift 6.2/6.3, a short comment explaining that would prevent someone re-adding them later.

6. FeedUpdateProcessor properties widened from private to internal

// Before:
private let fetcher: RSSFetcherService
private let robotsService: RobotsTxtService
// After:
internal let fetcher: RSSFetcherService
internal let robotsService: RobotsTxtService

These appear to be widened solely to allow FeedUpdateProcessor+Fetch.swift (in the same module) to access them. In Swift, private is file-scoped, so an extension in a different file needs at least internal. This is technically necessary given the file split. However, it's worth noting that keeping the extension in the same file would restore encapsulation. The current approach is acceptable but trades encapsulation for file organization.

7. Verify actions/checkout@v6 exists

All build jobs were updated from actions/checkout@v4 to @v6. As of early 2026, confirm this version is GA and not a pre-release — unexpected breakage from a non-existent major version would silently fail all CI. If v4 is the current stable, prefer @v4 or pin to a specific commit SHA.


💡 Minor Suggestions

  • FeedUpdateResult: The // MARK: - Cases comment added above the enum cases is slightly unusual — Swift convention usually reserves MARK for separating conceptually distinct sections. Cases in a small enum are self-evident; the comment adds noise.

  • UpdateReport.FeedResult.Status.notModified – The JSON serialization will encode this as "notModified" (lowerCamelCase). If downstream consumers expect "not_modified" (snake_case), consider adding CodingKeys or a custom RawRepresentable. Document the intended serialized form if this is part of a public contract.

  • MistKit in update-feeds.yml: Setup MistKit/ConfigKeyKit steps run unconditionally in CelestraCloud.yml build jobs but are correctly gated on cache-hit != 'true' only in update-feeds.yml. For consistency and faster CI on cache hits in the main workflow, consider the same conditional guard there too.


Summary

The core idea (local path dependency + CI composite actions) is clean and follows an established pattern. The typed configuration keys and typed status enum are meaningful quality improvements. The main action items before merge are the Bushel copy-paste strings (definitely wrong), confirming a .mise.toml exists, and deciding whether to pin ConfigKeyKit to a stable ref. The rest are low-risk suggestions.

… (#405)

The history rewrite that re-staged docs/ for beta.3 replaced the MistKit
'v1.0.0-beta.2' tag with '1.0.0-beta.2' (no v-prefix). setup-mistkit's
`git ls-remote` then resolves nothing and falls back to a branch: pin, so
SwiftPM fails with "could not find a branch named 'v1.0.0-beta.2'". Point
MISTKIT_BRANCH (and codeql's hardcoded ref) at the tag that exists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jun 2, 2026

Copy link
Copy Markdown

Code Review

Overview

This PR aligns CelestraCloud with BushelCloud's pattern for MistKit dependency management: local path dependencies in Package.swift for tight integration during development, with CI workflows that substitute them back to remote branches at build time. It also includes MistKit beta.2 API adaptations, a migration from Mint to mise for tool management, and several code quality improvements.


Potential Issues

1. actions/checkout@v6 does not exist

All four CI jobs now reference actions/checkout@v6. The latest stable release is v4. Using v6 will cause every workflow run to fail immediately with a resolution error. This needs to be reverted to @v4.

2. unsafeFlags block is commented out

This silently disables the Swift 6 strict concurrency checks that were enforcing correctness guarantees. The package still declares swift-tools-version: 6.2 but without -strict-concurrency=complete and -enable-actor-data-race-checks the enforcement is gone. If this was necessary to work around a MistKit beta.2 compilation issue, it should be tracked in a follow-up issue and re-enabled once resolved — otherwise correctness guarantees regress silently.

3. Hardcoded branches in codeql.yml

CelestraCloud.yml and update-feeds.yml define MISTKIT_BRANCH / CONFIGKEYKIT_BRANCH as workflow-level env vars. codeql.yml uses hardcoded literals (1.0.0-beta.2 / main) instead. When the branch is bumped next time, codeql.yml will fall behind and scan stale code. Prefer ${{ env.MISTKIT_BRANCH }} after defining those env vars in codeql.yml too.

4. CONFIGKEYKIT_BRANCH: main is non-deterministic

Pinning ConfigKeyKit to main means a push to that repo can silently change build behavior mid-run. Consider pinning to a tag or commit SHA similar to how MistKit is pinned to 1.0.0-beta.2, and add SHA resolution for the ConfigKeyKit cache key as is already done for MistKit.

5. Undocumented local path layout requirement

Package.swift now requires MistKit at ../.. and ConfigKeyKit at ../../Packages/ConfigKeyKit. This is a hard prerequisite for anyone cloning the repo for local development. CLAUDE.md and README.md should document this — otherwise swift build fails with an opaque "no such module" error.

6. FeedUpdateProcessor members widened from private to internal

Fields like fetcher, robotsService, rateLimiter, etc. were private and are now internal to allow the new FeedUpdateProcessor+Fetch.swift extension to access them. The correct Swift approach is either to merge the extension back into the same file (keeping private), or mark them package-level if the file separation must be maintained. internal exposes these to the entire module unnecessarily.


Code Quality (Positive Changes)

  • Typed Status enum on UpdateReport.FeedResult — replacing the raw String field with a proper enum prevents invalid values and enables exhaustive switching. Good improvement.
  • successRate as a stored property in UpdateReport.Summary — ensures the JSON output includes the value without requiring a computed property on the decoded side. Correct for Codable consistency.
  • Server-side feedRecordName filter in queryArticleBatch — the old code fetched all GUIDs and filtered in-memory; now the filter is pushed to CloudKit. Resolves issue #192 cleanly.
  • uniquingKeysWith in ArticleCategorizer — defensive fix for duplicate GUIDs that would previously crash with uniqueKeysWithValues.
  • header.sh format-string injection fix — escaping % in user-supplied values before passing to printf closes an injection path.
  • Mint to mise migration — cleaner developer experience, no Mint bootstrapping required.
  • Dynamic CI matrix (slim for PRs, full for main/semver branches) — smart optimization; feature branches get faster feedback without sacrificing coverage on main.
  • Windows build matrix now uses release tags (6.2-RELEASE, 6.3-RELEASE) instead of development snapshots — good stability improvement.

Minor Notes

  • readDouble and readBool helpers were removed from ConfigurationLoader but readString and readInt remain. The asymmetry is slightly surprising but not a bug.
  • .swift-format FileScopedDeclarationPrivacy changed from true to false — this relaxes a rule requiring file-scoped declarations to be private. Worth confirming this is intentional and not a workaround for the access-level widening in issue 6 above.
  • one_declaration_per_file SwiftLint rule is a good addition and consistent with the file-splitting done in this PR.

Summary

The core dependency management pattern is solid, and the Swift code improvements (typed enums, server-side query filtering, duplicate-GUID handling, injection fix) are all genuine improvements. Three blocking items before merge:

  1. Fix actions/checkout@v6 to @v4 — will break all CI
  2. Document the local path layout in README/CLAUDE.md so contributors know what sibling checkouts are required
  3. Address the unsafeFlags regression — either track a follow-up issue or explain why permanent removal is safe

After adopting ConfigKeyKit, several configuration types are no longer
reachable:

- Drop the deprecated, unused CelestraConfig.createCloudKitService()
  no-arg overload that read ProcessInfo env vars directly; all call
  sites use createCloudKitService(from:) via ConfigurationLoader.
- Delete Errors/ConfigurationError.swift — only referenced by that
  deprecated method.
- Delete Configuration/ConfigSource.swift — the ConfigSource enum was
  only referenced by EnhancedConfigurationError.source, which was never
  set non-nil; drop that property/parameter too.
- Downgrade CelestraConfig's Foundation import to internal (no longer in
  a public signature).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jun 3, 2026

Copy link
Copy Markdown

PR Review: Local MistKit Dependency + Workflow Refactor

Overall this is a solid PR that aligns with the BushelCloud pattern, fixes real bugs (the .in() CloudKit issue workaround, format-string injection in header.sh), and modernizes the CI significantly. A few items need attention before merging.


Bugs / Correctness Issues

1. modifyRecords conformance silently turns partial success into all-or-nothing

CloudKitRecordOperating.swift (line ~3152):

for result in results {
  records.append(try result.get())  // ← throws on first failure
}

The original design used atomic: false specifically to allow partial batch success (BatchOperationResult tracks successCount/failureCount). With this change, the first record-level failure throws and discards all previously-accumulated successful records. The comment acknowledges this ("conservative all-or-nothing handling") but it's a behavioral regression worth calling out explicitly — callers relying on partial success will now see full batch failures instead.

Consider collecting errors and returning successful records alongside a summary, or surfacing this trade-off in the protocol's doc comment.


2. queryRecords conformance silently drops paginated results

CloudKitRecordOperating.swift (line ~3166):

let result: QueryResult = try await queryRecords(
  ...
  continuationMarker: nil,
  ...
)
return result.records  // ← continuation marker discarded

This only returns the first page. Feeds with many articles could silently miss records. The new queryAllRecords protocol requirement was added to handle pagination — but queryRecords in the protocol conformance still has this silent truncation. Any call site using queryRecords instead of queryAllRecords will get incomplete results without any error. Worth auditing call sites or adding a comment warning about the page-size limit.


3. setup-cloudkit-schema.sh help text references Bushel, not Celestra

Line ~1057:

echo "     d. Store it securely (e.g., ~/.cloudkit/bushel-private-key.pem)"

Copy-paste from BushelCloud. Should be celestra-private-key.pem.


4. codeql.yml hardcodes branch versions instead of using env vars

- name: Setup MistKit
  uses: brightdigit/MistKit/.github/actions/setup-mistkit@main
  with:
    branch: 1.0.0-beta.2   # ← hardcoded, not ${{ env.MISTKIT_BRANCH }}

CelestraCloud.yml sets MISTKIT_BRANCH: 1.0.0-beta.2 as a workflow-level env var so it's a single update point. codeql.yml duplicates the value instead of referencing it, creating a maintenance drift risk. Suggest using the same env var pattern (or a shared inputs: if the env var isn't available in this context).


Code Quality

5. unsafeFlags block fully commented out

Package.swift (line ~749):

// .unsafeFlags([
//   "-strict-concurrency=complete",
//   ...
// ])

Commenting this out disables all strict concurrency checking for the build. The PR description doesn't mention this change. If MistKit beta 2 doesn't yet support strict concurrency, it's reasonable for now, but it should be tracked: strict concurrency was a core part of this project's Swift 6 story per CLAUDE.md. A TODO comment with the reason (e.g., // TODO: re-enable once MistKit 1.0.0-beta.2 is stable with strict concurrency) would help future contributors.

6. FeedUpdateProcessor fields changed from private to internal

This was done to support FeedUpdateProcessor+Fetch.swift in a separate file (driven by the new one_declaration_per_file SwiftLint rule). The fields don't need to be part of the module's API surface. An alternative that keeps them private: put processSuccessfulFetch and updateFeedMetadata in the same file as the struct (they're extensions on the same type, not a separate concern), or use a single large file with // MARK: sections and disable one_declaration_per_file for this file.


Minor Documentation Gap

7. CLAUDE.md configuration key table still shows old underscore format

The PR updates CLAUDE.md for the CloudKitService init change, but the configuration key mapping table still shows:

--cloudkit-container-id → cloudkit.container_id

With the migration to ConfigKeyKit, the internal keys now use dashes (cloudkit.container-id). The env var mapping (CLOUDKIT_CONTAINER_ID) appears unchanged (assuming StandardNamingStyle converts correctly), so the user-facing docs are probably still accurate — but the internal key format in the table is stale if anyone references it.


What's Working Well

  • The ArticleSyncService fix (removing the // TEMPORARY: Skip GUID query workaround) is a meaningful improvement — good to have this now that the MistKit .in() filter bug is resolved.
  • ArticleCategorizer duplicate GUID handling with uniquingKeysWith is a correct defensive fix.
  • UpdateReport.FeedResult.status promoted from String to a typed Status enum — eliminates the stringly-typed status values.
  • header.sh format-string injection fix is a real security improvement.
  • The CI matrix split (minimal always-on vs full matrix on main/semver PRs) is a sensible optimization.
  • Replacing Mint with mise simplifies the toolchain considerably.

leogdion added 2 commits July 8, 2026 18:23
Removes the `Packages/ConfigKeyKit` git-subrepo (vendored copy of
brightdigit/ConfigKeyKit) and switches the example backends to the remote
package, pinned to the released tag:

  .package(name: "ConfigKeyKit", path: "../../Packages/ConfigKeyKit")
  → .package(url: "https://github.com/brightdigit/ConfigKeyKit.git", from: "1.0.0-beta.2")

- Examples/BushelCloud, Examples/CelestraCloud, Examples/MistDemo manifests +
  Package.resolved updated (ConfigKeyKit pinned at 1.0.0-beta.2 / 6949abb).
- MistKit core never depended on ConfigKeyKit; Packages/ is now empty and removed.
- Removed the now-dead `setup-configkeykit` action steps and `CONFIGKEYKIT_BRANCH`
  env from the BushelCloud and CelestraCloud workflows (BushelCloud.yml, codeql.yml,
  bushel-cloud-build.yml, cloudkit-sync composite action incl. its configkeykit-branch
  input; CelestraCloud.yml, codeql.yml, update-feeds.yml). SwiftPM now fetches
  ConfigKeyKit directly, so the fresh-build fallback action is unnecessary.

Verified: `swift package resolve` succeeds for all three examples (ConfigKeyKit
resolves to 1.0.0-beta.2) and the `mistdemo` executable builds. All seven edited
workflow/action YAML files still parse. The pre-existing MistDemoApp SwiftUI build
errors are unrelated to this change.

Tracks brightdigit/ConfigKeyKit#6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
brightdigit/swift-build@v1 no longer needs an explicit scheme — the
MistKit and MistDemo workflows already omit it. Remove the now-obsolete
`scheme:` input from the BushelCloud and CelestraCloud workflows to match.

These are subrepo workflows; propagate upstream via `git subrepo push`
after review. (ConfigKeyKit is excluded — that subrepo is being removed
entirely in a separate PR.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review

This PR is primarily a mechanical alignment change (local MistKit path dependency, CI workflow rework, internal import adoption, file splitting for one_declaration_per_file, copyright/quote-character cleanup) plus a few real behavior changes bundled in. Overall it's well-organized, but a few things are worth a look before merge.

Potential correctness issue: queryFeeds may no longer honor --update-limit

Sources/CelestraCloudKit/Services/CloudKitService+Celestra.swift (queryFeeds) switched from:

queryRecords(recordType: "Feed", ..., limit: limit)

to:

queryAllRecords(recordType: "Feed", ..., pageSize: limit, database: .public(.prefers(.serverToServer)))

Per the new protocol doc-comment in CloudKitRecordOperating.swift, queryAllRecords "automatically paginat[es] through continuation markers" up to maxPages. If that's accurate, pageSize now bounds the size of each page fetched, not the total number of feeds returned — so --update-limit 5 (documented in CLAUDE.md as "Maximum number of feeds to query and update") could silently return far more than 5 feeds. That would affect production tiering (high/standard/stale) and the PR smoke test, which relies on --update-limit 5 --update-max-failures 0 to bound its 10-minute CI budget.

Worth double-checking against MistKit's actual queryAllRecords semantics (not visible in this repo since MistKit is now a local path dependency). Also note this method isn't covered by the mock-based unit tests — MockCloudKitRecordOperator.queryAllRecords just returns a single canned result regardless of pageSize/maxPages, so this behavior isn't actually exercised by Tests/.

Copy-paste leftovers from BushelCloud in Scripts/setup-cloudkit-schema.sh

The new --help output says:

CLOUDKIT_CONTAINER_ID   CloudKit container ID (default: iCloud.com.brightdigit.Bushel)

and further down:

d. Store it securely (e.g., ~/.cloudkit/bushel-private-key.pem)

Both reference "Bushel" instead of "Celestra" — apparent artifacts from porting this script from BushelCloud (as described in the PR body). Per CLAUDE.md the actual default container is iCloud.com.brightdigit.Celestra.

Local path dependency isn't documented for contributors

Package.swift now pins MistKit via .package(name: "MistKit", path: "../..") instead of a remote URL. This is consistent with the "BushelCloud pattern" described in the PR body, and CI workflows compensate via the new setup-mistkit composite action. However, CLAUDE.md's "Build and Run" section still leads with a bare swift build as the first command for anyone cloning the repo — that will fail to resolve unless the contributor happens to have a MistKit checkout two directories up. Consider a short note in CLAUDE.md/README about this expectation (or a local Package.swift override example) for non-CI contributors.

Package.swift: strict concurrency checking silently disabled

The unsafeFlags block enabling -strict-concurrency=complete, -warn-concurrency, -enable-actor-data-race-checks, etc. has been commented out rather than removed:

// .unsafeFlags([
//   ...
// ])

CLAUDE.md explicitly documents "Strict concurrency checking (-strict-concurrency=complete)" as a Swift 6.2 feature this project relies on. If this was disabled intentionally (e.g., incompatibility with the new toolchain/local dependency setup), it'd help to leave a comment explaining why; if it was leftover from debugging, it should probably be re-enabled or removed rather than commented out.

Nice fixes bundled in here

  • ArticleSyncService.syncArticles re-enables the GUID-based existing-article query that had been stubbed out to [] with a TEMPORARY/TODO comment (blocked on CloudKit Web Services issue #192). Good catch restoring real duplicate detection — worth confirming with an integration run since this previously meant every article was being treated as new.
  • ArticleCloudKitService.queryArticleBatch now combines the GUID .in() filter with an .equals("feedRecordName", ...) filter server-side instead of filtering in-memory, now that the underlying CloudKit issue is fixed — matches the test update in ArticleCloudKitService+Query.swift.
  • CelestraError's errorDescription/recoverySuggestion/isRetriable were refactored to table-driven lookups keyed by a CaseID enum. Cute pattern, and the assertionFailure default branch is a reasonable safety net, but double check it's worth the added indirection over the original exhaustive switch — a future case addition has to remember to update three separate tables plus the caseID mapping instead of getting an exhaustiveness-check nudge on one switch.

Minor: duplicated chunking logic

ArticleCloudKitService.swift now has the same stride(from:to:by:).map { Array(...) } chunking snippet inlined three times (for guids, and twice for article batches), replacing what looks like a single chunked(into:) helper that's no longer available. Consider extracting a small internal Array.chunked(into:) extension to avoid the duplication (and the off-by-one risk of hand-rolling Swift.min($0 + size, count) three times).

Nit

UpdateReport.FeedResult.status moving from a raw String ("success"/"error"/"skipped"/"notModified") to a typed Status enum is a solid type-safety improvement — nice catch-up on that TODO-shaped comment that used to live next to the old field.

Nothing here looks blocking except the queryFeeds/--update-limit behavior, which I'd want confirmed against MistKit's actual pagination semantics before merge, given it affects both production scheduling and CI cost.

leogdion and others added 3 commits July 10, 2026 13:45
Update all macOS CI jobs and CodeQL to Xcode_26.6.app and move
simulator runtimes to 26.5 (latest pre-created on macos-26 runner).
Add dependency-policy workflow gating PRs to main to tagged deps only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov's uploader GPG signature verification is failing on the
distribution side (BAD signature from cli.codecov.io), failing CI
deterministically. Set fail_ci_if_error: false on the Ubuntu and
Windows jobs so coverage upload issues no longer fail the build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fix Bushel copy-paste in setup-cloudkit-schema.sh help text
  (container default and example key path now say Celestra)
- Reconcile CLAUDE.md Swift 6.2 section: strict concurrency comes
  from the language-mode default; the explicit
  -strict-concurrency=complete flag is commented out in Package.swift
- Add ArticleSyncService sync tests covering the restored GUID dedup
  orchestration (combined IN(guid)+EQUALS(feedRecordName) query before
  create/update batches)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Summary

This PR is far larger in scope than its description states. The description talks about switching MistKit to a local path dependency plus a few CI sed tweaks, but the actual diff (85 files, ~40 squashed commits per gh pr view --json commits) also includes: a full ConfigKeyKit adoption/config-loader rewrite, an error-handling refactor in CelestraError, a new dependency-policy.yml gate, a Mint→mise migration, license-header cleanup, and a Scripts/header.sh injection fix. Worth updating the PR description so reviewers aren't scoping against stale info — several commits reference fixing earlier commits in the same branch (e.g. "CI fixups round 2", "Fix broken local setup-mistkit references"), which suggests this is an integration branch rather than a single reviewed change.

🚩 Potential blocker: Package.swift conflicts with the new dependency-policy.yml gate

Package.swift now declares:
```swift
.package(name: "MistKit", path: "../.."),
.package(url: "https://github.com/brightdigit/CelestraKit.git", branch: "v0.0.3"),
```

  • MistKit is a local path dependency and CelestraKit is a branch dependency (not a tag/from: version).
  • This same PR adds .github/workflows/dependency-policy.yml, which runs on pull_requestmain and fails if Package.swift has any fileSystem (local path) or branch/revision sourceControl dependency.
  • Since this PR itself targets main, it looks like it would fail its own new gate. Please confirm the dependency-policy check is actually green on this PR — if it's failing (or was skipped), that needs resolving (e.g., bump CelestraKit to a real tagged release, land MistKit as a version dependency) before merge, otherwise the new policy is dead on arrival.

🚩 path: "../.." assumes a monorepo layout the repo's own docs don't describe

CLAUDE.md documents swift build as the first command for a standalone clone of brightdigit/CelestraCloud. With MistKit pinned to path: "../..", that only resolves if this repo is checked out two directories below a local MistKit checkout (matching the Examples/... layout referenced in the BushelCloud-related commits). A plain git clone of this repo, as the docs instruct, will fail to resolve the MistKit dependency and swift build will error out immediately for any external contributor. CI presumably works around this via the setup-mistkit composite action, but that means CI green doesn't validate the documented local dev workflow. Worth calling this out explicitly in CLAUDE.md/README if the repo is no longer meant to build standalone, or reconsidering the local path in favor of CI-only MISTKIT_BRANCH wiring (keep Package.swift pointed at a remote dependency).

Code quality: CelestraError lookup-table refactor trades away exhaustiveness safety

Sources/CelestraCloudKit/Services/CelestraError.swift replaces the exhaustive switch statements in errorDescription/recoverySuggestion/isRetriable with a parallel CaseID enum plus [CaseID: String] dictionaries. The compiler still forces every CelestraError case to map to a CaseID, but nothing forces a new CaseID to be added to staticDescriptions/recoverySuggestions/retriableCaseIDs. Concretely: add a new payload-free CelestraError case, map it to a new CaseID, and forget to add it to staticDescriptionserrorDescription silently falls through to default: assertionFailure(...); return nil. assertionFailure compiles out in release builds, so production would silently return nil instead of failing to compile as the old exhaustive switch would have. This looks like a real regression in safety for a refactor unrelated to the PR's stated MistKit-dependency purpose — consider keeping the original exhaustive switches, or at least keeping the fallback branch exhaustive rather than a table lookup with a silent default.

Nice catches / good practice

  • Scripts/header.sh: escaping % in filename/package/creator/year/company before feeding them into printf "$header_template" ... closes a real format-string injection bug (a filename or -c/-o value containing %s/%n could previously corrupt or crash header injection). Good fix.
  • .github/workflows/CelestraCloud.yml: adding concurrency: cancel-in-progress, paths-ignore for docs-only changes, and gating the expensive platform matrices (build-macos-platforms, Windows, dual-Ubuntu) behind a configure job that only runs the full matrix on main/semver/PRs into those is a solid cost/time optimization.
  • Making Codecov upload fail_ci_if_error: false and conditioning coverage steps on contains-code-coverage avoids flaky CI failures from an unrelated third-party service.
  • Scripts/setup-cloudkit-schema.sh --dry-run is a nice safety addition for validating schema changes without touching a live CloudKit container.

Minor / nits

  • ConfigurationLoader.read(_ key: ConfigKeyKit.ConfigKey<Bool>) treats CLI flag presence as true but only accepts "true"/"1"/"yes" (case-insensitive) from the environment — an unexpected value (e.g. a typo like =yesplease) silently falls back to the default rather than warning. Might be worth a one-line doc note or a warning log.
  • Package.resolved's CelestraKit pin moved from a tagged 0.0.2 release to branch: "v0.0.3" — same category of concern as the MistKit path issue above; if v0.0.3 is meant to be a tag, it should be declared with from:/exact: rather than branch:.
  • Test coverage looks proportionate to the refactor — new services (ArticleSyncService, ArticleOperationBuilder, FeedMetadataBuilder, etc.) each have corresponding test files, and the configuration/error refactors have matching test updates.

Overall: the CI/workflow modernization and the header.sh fix are solid, but the dependency-policy vs. Package.swift contradiction should be resolved (or explained) before merge, and it's worth double-checking the CelestraError lookup-table refactor doesn't quietly drop error copy for future cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant