This document defines the engineering practices for implementing the AppScript runtime. All contributors must follow these standards.
Every source file must carry this license header as a comment block at the top of the file, before any other content:
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// <FileName.m> — <one-line description>
// Copyright (C) <year> <author or organisation>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.No source file may be added to the project without this header. Dependencies (third-party libraries) must be licensed under AGPL-compatible terms and their licenses documented in a LICENSES/ directory at the repository root.
All implementation is in Objective-C, or AppScript. Specifically:
- The AppScript framework must be implemented in Objective-C; no Objective-C++, no Swift.
- Objective-C implementation files use the
.mextension. Avoid Objective-C++. - Header files use the
.hextension. - No Swift source files may be added to the framework or test targets. Swift may be used in ancillary tooling (scripts, generators) but not in the runtime itself.
- Target the macOS SDK. Use
NS_ASSUME_NONNULL_BEGIN/NS_ASSUME_NONNULL_ENDin every header to enable nullability checking. Annotate all out-parameters and return types explicitly. - Use
NS_DESIGNATED_INITIALIZERandNS_UNAVAILABLEto make the intended initialiser surface clear. - The AppScriptHarness app must use AppScript for its implementation wherever possible. If it's impossible to build a feature in AppScript, use Objective-C instead but create a task in beads to implement the missing AppScript capability in the framework.
This project builds with both Xcode, and GNUstep. It's CRITICAL that all development work builds successfully and passes tests with both structures.
The repository contains three Xcode targets:
| Target | Type | Purpose |
|---|---|---|
AppScript |
Framework | The runtime: parser, interpreter, runtime classes |
AppScriptTests |
Unit test bundle | XCTest unit tests for every framework class |
AppScriptHarness |
macOS application | Integration test harness (see below) |
Source files are organised to mirror this structure:
AppScript/ # Framework source
AppScriptTests/ # Unit tests (mirrors AppScript/ layout)
AppScriptHarness/ # Test harness app
LICENSES/ # Third-party licence texts
Each source file in AppScript/ has a corresponding test file in AppScriptTests/ with the same base name and a Tests suffix (e.g. ASParser.m → ASParserTests.m).
The repository contains a GNUmakefile with three targets:
| Target | Type | Purpose |
|---|---|---|
AppScript |
Framework | The runtime: parser, interpreter, runtime classes |
AppScriptTests |
Bundle | GNUstep tool-xctest unit tests for every framework class |
AppScriptHarness |
GNUstep app | Integration test harness (see below) |
AppScript uses Objective-C ARC. Manual retain/release/autorelease calls are forbidden.
To avoid memory bugs:
- Retain cycles: Use
weakreferences for delegates, callbacks, and block captures where the referenced object owns the block. Preferweak–strongdance (__weak/__strong) in blocks rather than__unsafe_unretained. - Static analysis: The Xcode build must pass Clang's static analyser (
Analyzebuild action) with zero warnings. CI enforces this. - Address Sanitizer: All test targets run with ASan enabled in CI. No ASan errors are acceptable.
- Instruments: Before each release, run Leaks and Allocations instruments against the test harness. No leaks may be introduced.
The interpreter may be invoked from multiple threads simultaneously (see ARCHITECTURE.md). To avoid thread safety bugs:
- Thread Sanitizer: ASan and TSan cannot be active in the same binary simultaneously. CI runs tests in two separate build configurations — one with ASan, one with TSan — and both must pass. No TSan reports are acceptable.
- Immutability by default: Prefer immutable data structures. Use mutable variants only within a single-owner scope, then hand off an immutable copy.
- Explicit queue ownership: Every class that has internal mutable state must document which dispatch queue owns that state in a header comment. Access that state only from the owning queue.
- GCD for synchronisation: Use
dispatch_queue_t(serial queues) as the primary synchronisation primitive. Avoid@synchronizedexcept for trivial one-liner critical sections. Never useNSLockon the hot path. os_unfair_lock: Use for low-level, non-recursive locking in performance-sensitive code paths. Document the owning lock at every access site.- No
dispatch_get_main_queue()from the framework: The framework must not assume it runs on the main thread and must not dispatch back to the main queue internally. The host application controls threading.
Every public or @package method on every class in the AppScript framework must have at least one corresponding unit test. Private methods are tested indirectly through their public interfaces.
Test files use XCTest. Naming conventions:
- Test class:
<ClassName>Tests(e.g.ASInterpreterTests) - Test method:
test_<methodUnderTest>_<scenario>_<expectedOutcome>- Example:
test_parseExpression_missingClosingBracket_returnsError
- Example:
Each test method tests exactly one behaviour. A test that requires more than ~20 lines of setup is a signal to introduce a helper method or fixture, not to merge test cases.
No production code is written without a failing test that motivates it (test-first). A pull request that adds production code without a corresponding test will not be merged.
The AppScriptHarness app is a macOS application that performs end-to-end integration testing of the full runtime. It is not a unit test bundle; it exists to exercise the complete load → parse → interpret → result pipeline.
Structure:
The harness loads .appscript script files from a bundle. Each script file has a paired .expected file in the same bundle that specifies the expected output. The harness:
- Calls
loadAppScriptsInBundle:on the bundle containing the test scripts. - For each script, instantiates the class defined in that script and calls
-runTestson it. - Calls
-descriptionon the returned object and compares the resulting string to the content of the paired.expectedfile. - Prints a pass/fail summary to stdout and exits with code 0 (all pass) or 1 (any failure).
Every harness script must define exactly one class whose name ends in Test and which implements -runTests. Example:
@class MyFeatureTest : NSObject
- runTests {
// exercise the feature and return a result whose -description matches MyFeature.expected
return self
}
@end
This makes the harness usable in CI without a GUI.
Adding a harness test:
- Create
MyFeature.appscriptinAppScriptHarness/Scripts/. - Create
MyFeature.expectedalongside it with the exact expected output. - Verify the harness exits 0 locally before opening a pull request.
File pairing rules:
- Both
<ClassName>.appscriptand<ClassName>.expectedare required.<ClassName>is the test class name with theTestsuffix stripped (e.g.ArithmeticTest→Arithmetic.appscript+Arithmetic.expected). - A script without its
.expectedfile will not crash the harness, but the test will fail with the messagemissing .expected file for <ClassName> — create AppScriptHarness/Scripts/<ClassName>.expected. This is a test failure, not a harness error. - Do not add any other file types (e.g.
.md,.txt) toAppScriptHarness/Scripts/— that folder's contents are copied verbatim into the app bundle at build time.
Capture-then-assert workflow for .expected files:
When writing a new harness test, don't guess the exact output format. Instead:
- Add
MyFeature.appscriptwith the-runTestsmethod returning the result you want to verify. - Run the harness without a
MyFeature.expectedfile (or with an empty placeholder). - The harness will report a failure and show the actual output in the
actualfield of the result. - Copy that actual output into
MyFeature.expected. - Re-run the harness to confirm it exits 0.
This avoids subtle format mismatches (e.g. how NSArray -description escapes special characters).
Harness tests are required for any behaviour described in ARCHITECTURE.md that cannot be fully exercised by unit tests (e.g. multi-class interactions, the full parse–interpret pipeline, error crash behaviour).
Every public or @package method in the AppScript framework must have a documentation comment in the header file. Use DocC-compatible /// comment syntax:
/// Loads all AppScript source files found in the given bundle.
///
/// - Parameter error: On failure, set to a description of the error. Pass `nil` if you don't need error information.
/// - Returns: `YES` if all scripts loaded successfully; `NO` otherwise.
- (BOOL)loadAppScriptsInBundle:(NSError **)error;The summary line (first /// line) must be a complete sentence that stands alone — it appears in quick-help and symbol listings. Parameter and return-value descriptions are required for any method with non-obvious parameters or a non-obvious return value. Omit them only when the method name makes them fully redundant (e.g. a trivial getter).
Private methods in .m files do not require documentation comments, but may have them where the logic is non-obvious.
Complex end-to-end flows that span multiple classes or methods must be documented as DocC articles in AppScript/Documentation.docc/. An article is required whenever:
- A feature involves three or more collaborating types (e.g. the parse → install → invoke pipeline).
- A feature requires the caller to follow a specific sequence of steps (e.g. loading scripts, then instantiating objects).
- A behaviour is described in a section of ARCHITECTURE.md that has no single obvious entry-point method.
Articles must use DocC's <doc:SymbolName> link syntax to reference every method and type they describe, so that the documentation graph stays connected. Example article front matter:
# Loading and Running AppScript Files
Embed the AppScript framework in your app and execute scripts at runtime.
## Overview
Describe the flow here...
## Topics
### Loading scripts
- <doc:NSBundle/loadAppScriptsInBundle:>
### Defining entry points
- <doc:ASInterpreter>Add a link to each new article from the framework's top-level AppScript.md catalog file so it appears in the DocC navigator.
When a bug report is received, the first question to answer is: is this a code bug or a design bug?
| Situation | Action |
|---|---|
| The code doesn't match ARCHITECTURE.md | Fix the code. No design document change needed. |
| ARCHITECTURE.md is silent on the behaviour | Decide the correct behaviour, update ARCHITECTURE.md to specify it, then fix the code to match. |
| ARCHITECTURE.md specifies the behaviour but the behaviour is wrong | Update ARCHITECTURE.md to specify the corrected behaviour, then fix the code. |
The design document and the code must always agree. A commit that fixes a bug by changing observable behaviour must update ARCHITECTURE.md in the same commit. Reviewers must check this before approving.
Run /ambiguity-resolution on ARCHITECTURE.md whenever a design change is made to catch new ambiguities before they reach the implementation.
Before approving a pull request, verify:
- Every new
.mor.appscriptfile has the AGPL licence header. - Every new public method has at least one unit test.
- Every new public method has a
///documentation comment in its header. - Complex multi-class flows have a DocC article, linked from the catalog.
- The Clang static analyser reports no new warnings.
- ASan and TSan report no errors.
- Any behaviour change is reflected in ARCHITECTURE.md.
- New end-to-end behaviour is covered by a harness test.
- Mutable state has a documented owning queue.
- The framework builds using both xcodebuild and make.
- The unit tests build and pass using both xcodebuild and make check.
- The AppScriptHarness app builds using both xcodebuild and make.
- The AppScriptHarness UI tests build and pass in xcodebuild.