Sprint 0: Scaffolding, References, Harness
Prerequisites
None — this is where afs-ld begins. Assumes armfortas and afs-as already exist and compile.
Current state to remediate
The afs-ld/ directory currently sits inside armfortas's working tree as a plain subdirectory. afs-ld/.gitignore was accidentally committed to armfortas history (commit 85d5ba8 "init"), tracking a file that will shortly belong to a separate repo. Before anything else in this sprint, afs-ld must be extracted into its own repo and wired back in as a Git submodule. The tracked .gitignore must be removed from armfortas's index (history can stay; removing a single file at HEAD is clean) so that afs-ld's contents live in the submodule and nowhere else.
Goals
Extract afs-ld into its own repo, wire it back as a submodule, stand up the crate (CLAUDE.md, README, Cargo.toml, skeleton source), clone reference linkers, build the differential harness. End state: cargo test -p afs-ld runs from the parent workspace, at least one test passes meaningfully, and git submodule status lists afs-ld alongside afs-as.
Deliverables
1. Submodule remediation (do first)
Goal: move from "afs-ld is a tracked subdirectory of armfortas" to "afs-ld is a submodule pointing at git@github.com:FortranGoingOnForty/afs-ld.git". Exact sequence:
- Preserve the current afs-ld contents: copy
armfortas/afs-ld/to a temp location (the.docs/overview.mdand sprint files produced in planning are the primary content to preserve;.fackr/is scratch and can be dropped). - Untrack from armfortas:
git rm --cached afs-ld/.gitignore && git commit -m "remove accidentally-tracked afs-ld/.gitignore". Confirmgit ls-files afs-ldreturns empty. - Delete the directory from armfortas's working tree (submodule-add will recreate it):
rm -rf afs-ld/. - Create the external repo
FortranGoingOnForty/afs-ldon GitHub (empty, no README — submodule-add will seed it). - Initialize locally and push: in a scratch directory,
git init afs-ld cd afs-ld <copy preserved contents back> git add -A git commit -m "init" git remote add origin git@github.com:FortranGoingOnForty/afs-ld.git git push -u origin trunk - Add as submodule in armfortas:
Confirmcd <armfortas root> git submodule add git@github.com:FortranGoingOnForty/afs-ld.git afs-ld git commit -m "add afs-ld submodule".gitmodulesgained the stanza:[submodule "afs-ld"] path = afs-ld url = git@github.com:FortranGoingOnForty/afs-ld.git - Verify with
git submodule status— afs-as and afs-ld both listed, each pinned to a commit hash.
Note: the old git rm --cached commit stays in armfortas history. Rewriting history to erase it is more destructive than it's worth for a single .gitignore file. The file at HEAD is gone; that is sufficient.
2. Crate wiring
- Root
Cargo.tomladds"afs-ld"to[workspace] membersalongsideafs-as. afs-ld/Cargo.toml: binary + library, zero external dependencies,edition = "2021". Mirrorafs-as/Cargo.toml— samekeywords,categories,license = "GPL-3.0-only", adjusteddescriptionandrepository.afs-ld/src/lib.rs: public re-exports ofLinker,LinkOptions,OutputKind(types are stubs this sprint).afs-ld/src/main.rs: CLI that prints usage and exits 0 when run with no args; forwards real args toLinker::run(which errors with "not yet implemented" this sprint).
3. CLAUDE.md and README.md
afs-ld/CLAUDE.md: mirrorafs-as/CLAUDE.md, replacing assembler-isms with linker-isms. Non-negotiable rules: Rust std only, exhaustive matching, caret diagnostics, per-chunk commits, no co-authors, no sprint-number references in commits.afs-ld/README.md: one-page intro, supported CLI subset at current state (nothing yet — say so), build/test commands.afs-ld/.gitignore:target/,.fackr/, no.docs/since those files live in the repo. This one is intentionally tracked because it lives inside afs-ld's own repo now, not armfortas's.
4. Reference clones
Add to parent .refs/ (gitignored):
.refs/ld64/—git clone --depth 1 https://github.com/apple-oss-distributions/ld64.git. Apple's last publicly released ld64. Authoritative for Apple-parity edge cases..refs/mold/—git clone --depth 1 https://github.com/rui314/mold.git. Performance reference and a second Rust-adjacent angle on Mach-O.
.refs/llvm/lld/MachO/ already exists from armfortas Sprint 0 — primary architectural reference.
5. Differential harness
afs-ld/tests/common/harness.rs:
pub struct LinkCase {
pub name: &'static str,
pub inputs: Vec<PathBuf>, // .o / .a / .tbd
pub args: Vec<String>, // -o, -e, -syslibroot, -l, ...
}
pub struct LinkOutputs {
pub ours: Vec<u8>, // afs-ld output
pub theirs: Vec<u8>, // system ld output
}
pub fn link_both(case: &LinkCase) -> LinkOutputs;
pub fn diff_macho(ours: &[u8], theirs: &[u8]) -> DiffReport;
DiffReport categorizes byte differences as Tolerated (UUID, timestamp, temp-path hashes) or Critical (anything else). Critical diffs fail the test.
Closeout note: the current Sprint 0 surface is intentionally synthetic. diff_macho exists and is tested, but link_both remains a placeholder until afs-ld can emit real linked output. That means the current harness validates diff categorization logic, not end-to-end linker parity yet.
6. Skeleton CLI and first failing test
afs-ld/src/args.rs: hand-rolled argv parser stub that recognizes-o,-e,-arch, and positional inputs. Unknown flags error loudly with a hint.afs-ld/tests/reader_empty.rs: attempts to link0 inputs → empty output, expects the diagnostic"afs-ld: error: no input files". Passes today by producing that exact string.afs-ld/tests/diff_harness_sanity.rs: exercises the diff surface against two identical synthetic byte slices and expects zero diffs. Passes.afs-ld/tests/diff_harness_finds_critical.rs: feeds the harness two synthetic binaries that differ in a non-tolerated byte range and asserts the harness reportsCritical. Passes.
Testing Strategy
cargo build -p afs-ldcompiles from a fresh clone of the parent withgit submodule update --init --recursive.cargo test -p afs-ldruns harness-sanity, critical-detection, and empty-input tests.cargo clippy -p afs-ld -- -D warningsclean.- Manual verification of submodule state:
git ls-files | grep afs-ldin armfortas prints only the.gitmodulesentry (and nothing underafs-ld/).git submodule statusshows both afs-as and afs-ld with valid commit hashes.git submodule update --init --recursiveon a fresh armfortas clone populates afs-ld correctly.
Definition of Done
- The accidentally-tracked
afs-ld/.gitignoreis removed from armfortas's index at HEAD. - afs-ld exists as a standalone GitHub repo under
FortranGoingOnForty. - afs-ld is wired into armfortas as a Git submodule, visible in
.gitmodulesandgit submodule status. armfortas/Cargo.tomllistsafs-ldin[workspace] members.afs-ld/CLAUDE.md,README.md,Cargo.toml,src/lib.rs,src/main.rs,src/args.rsall committed in the new repo..refs/ld64/and.refs/mold/cloned.- Differential harness substrate runs, correctly reports zero diffs on identical byte slices, correctly reports critical diffs on intentionally-different byte slices.
cargo test --workspacegreen.
View source
| 1 | # Sprint 0: Scaffolding, References, Harness |
| 2 | |
| 3 | ## Prerequisites |
| 4 | None — this is where afs-ld begins. Assumes armfortas and afs-as already exist and compile. |
| 5 | |
| 6 | ## Current state to remediate |
| 7 | |
| 8 | The `afs-ld/` directory currently sits inside armfortas's working tree as a plain subdirectory. `afs-ld/.gitignore` was accidentally committed to armfortas history (commit `85d5ba8 "init"`), tracking a file that will shortly belong to a separate repo. Before anything else in this sprint, afs-ld must be extracted into its own repo and wired back in as a Git submodule. The tracked `.gitignore` must be removed from armfortas's index (history can stay; removing a single file at HEAD is clean) so that afs-ld's contents live in the submodule and nowhere else. |
| 9 | |
| 10 | ## Goals |
| 11 | Extract afs-ld into its own repo, wire it back as a submodule, stand up the crate (CLAUDE.md, README, Cargo.toml, skeleton source), clone reference linkers, build the differential harness. End state: `cargo test -p afs-ld` runs from the parent workspace, at least one test passes meaningfully, and `git submodule status` lists afs-ld alongside afs-as. |
| 12 | |
| 13 | ## Deliverables |
| 14 | |
| 15 | ### 1. Submodule remediation (do first) |
| 16 | |
| 17 | Goal: move from "afs-ld is a tracked subdirectory of armfortas" to "afs-ld is a submodule pointing at `git@github.com:FortranGoingOnForty/afs-ld.git`". Exact sequence: |
| 18 | |
| 19 | 1. **Preserve the current afs-ld contents**: copy `armfortas/afs-ld/` to a temp location (the `.docs/overview.md` and sprint files produced in planning are the primary content to preserve; `.fackr/` is scratch and can be dropped). |
| 20 | 2. **Untrack from armfortas**: `git rm --cached afs-ld/.gitignore && git commit -m "remove accidentally-tracked afs-ld/.gitignore"`. Confirm `git ls-files afs-ld` returns empty. |
| 21 | 3. **Delete the directory from armfortas's working tree** (submodule-add will recreate it): `rm -rf afs-ld/`. |
| 22 | 4. **Create the external repo** `FortranGoingOnForty/afs-ld` on GitHub (empty, no README — submodule-add will seed it). |
| 23 | 5. **Initialize locally and push**: in a scratch directory, |
| 24 | ``` |
| 25 | git init afs-ld |
| 26 | cd afs-ld |
| 27 | <copy preserved contents back> |
| 28 | git add -A |
| 29 | git commit -m "init" |
| 30 | git remote add origin git@github.com:FortranGoingOnForty/afs-ld.git |
| 31 | git push -u origin trunk |
| 32 | ``` |
| 33 | 6. **Add as submodule in armfortas**: |
| 34 | ``` |
| 35 | cd <armfortas root> |
| 36 | git submodule add git@github.com:FortranGoingOnForty/afs-ld.git afs-ld |
| 37 | git commit -m "add afs-ld submodule" |
| 38 | ``` |
| 39 | Confirm `.gitmodules` gained the stanza: |
| 40 | ``` |
| 41 | [submodule "afs-ld"] |
| 42 | path = afs-ld |
| 43 | url = git@github.com:FortranGoingOnForty/afs-ld.git |
| 44 | ``` |
| 45 | 7. **Verify** with `git submodule status` — afs-as and afs-ld both listed, each pinned to a commit hash. |
| 46 | |
| 47 | Note: the old `git rm --cached` commit stays in armfortas history. Rewriting history to erase it is more destructive than it's worth for a single `.gitignore` file. The file at HEAD is gone; that is sufficient. |
| 48 | |
| 49 | ### 2. Crate wiring |
| 50 | |
| 51 | - Root `Cargo.toml` adds `"afs-ld"` to `[workspace] members` alongside `afs-as`. |
| 52 | - `afs-ld/Cargo.toml`: binary + library, zero external dependencies, `edition = "2021"`. Mirror `afs-as/Cargo.toml` — same `keywords`, `categories`, `license = "GPL-3.0-only"`, adjusted `description` and `repository`. |
| 53 | - `afs-ld/src/lib.rs`: public re-exports of `Linker`, `LinkOptions`, `OutputKind` (types are stubs this sprint). |
| 54 | - `afs-ld/src/main.rs`: CLI that prints usage and exits 0 when run with no args; forwards real args to `Linker::run` (which errors with "not yet implemented" this sprint). |
| 55 | |
| 56 | ### 3. CLAUDE.md and README.md |
| 57 | |
| 58 | - `afs-ld/CLAUDE.md`: mirror `afs-as/CLAUDE.md`, replacing assembler-isms with linker-isms. Non-negotiable rules: Rust std only, exhaustive matching, caret diagnostics, per-chunk commits, no co-authors, no sprint-number references in commits. |
| 59 | - `afs-ld/README.md`: one-page intro, supported CLI subset at current state (nothing yet — say so), build/test commands. |
| 60 | - `afs-ld/.gitignore`: `target/`, `.fackr/`, no `.docs/` since those files live in the repo. This one is intentionally tracked because it lives inside afs-ld's own repo now, not armfortas's. |
| 61 | |
| 62 | ### 4. Reference clones |
| 63 | |
| 64 | Add to parent `.refs/` (gitignored): |
| 65 | |
| 66 | - `.refs/ld64/` — `git clone --depth 1 https://github.com/apple-oss-distributions/ld64.git`. Apple's last publicly released ld64. Authoritative for Apple-parity edge cases. |
| 67 | - `.refs/mold/` — `git clone --depth 1 https://github.com/rui314/mold.git`. Performance reference and a second Rust-adjacent angle on Mach-O. |
| 68 | |
| 69 | `.refs/llvm/lld/MachO/` already exists from armfortas Sprint 0 — primary architectural reference. |
| 70 | |
| 71 | ### 5. Differential harness |
| 72 | |
| 73 | `afs-ld/tests/common/harness.rs`: |
| 74 | |
| 75 | ```rust |
| 76 | pub struct LinkCase { |
| 77 | pub name: &'static str, |
| 78 | pub inputs: Vec<PathBuf>, // .o / .a / .tbd |
| 79 | pub args: Vec<String>, // -o, -e, -syslibroot, -l, ... |
| 80 | } |
| 81 | |
| 82 | pub struct LinkOutputs { |
| 83 | pub ours: Vec<u8>, // afs-ld output |
| 84 | pub theirs: Vec<u8>, // system ld output |
| 85 | } |
| 86 | |
| 87 | pub fn link_both(case: &LinkCase) -> LinkOutputs; |
| 88 | pub fn diff_macho(ours: &[u8], theirs: &[u8]) -> DiffReport; |
| 89 | ``` |
| 90 | |
| 91 | `DiffReport` categorizes byte differences as `Tolerated` (UUID, timestamp, temp-path hashes) or `Critical` (anything else). Critical diffs fail the test. |
| 92 | |
| 93 | Closeout note: the current Sprint 0 surface is intentionally synthetic. `diff_macho` exists and is tested, but `link_both` remains a placeholder until afs-ld can emit real linked output. That means the current harness validates diff categorization logic, not end-to-end linker parity yet. |
| 94 | |
| 95 | ### 6. Skeleton CLI and first failing test |
| 96 | |
| 97 | - `afs-ld/src/args.rs`: hand-rolled argv parser stub that recognizes `-o`, `-e`, `-arch`, and positional inputs. Unknown flags error loudly with a hint. |
| 98 | - `afs-ld/tests/reader_empty.rs`: attempts to link `0 inputs → empty output`, expects the diagnostic `"afs-ld: error: no input files"`. Passes today by producing that exact string. |
| 99 | - `afs-ld/tests/diff_harness_sanity.rs`: exercises the diff surface against two identical synthetic byte slices and expects zero diffs. Passes. |
| 100 | - `afs-ld/tests/diff_harness_finds_critical.rs`: feeds the harness two synthetic binaries that differ in a non-tolerated byte range and asserts the harness reports `Critical`. Passes. |
| 101 | |
| 102 | ## Testing Strategy |
| 103 | |
| 104 | - `cargo build -p afs-ld` compiles from a fresh clone of the parent with `git submodule update --init --recursive`. |
| 105 | - `cargo test -p afs-ld` runs harness-sanity, critical-detection, and empty-input tests. |
| 106 | - `cargo clippy -p afs-ld -- -D warnings` clean. |
| 107 | - Manual verification of submodule state: |
| 108 | - `git ls-files | grep afs-ld` in armfortas prints only the `.gitmodules` entry (and nothing under `afs-ld/`). |
| 109 | - `git submodule status` shows both afs-as and afs-ld with valid commit hashes. |
| 110 | - `git submodule update --init --recursive` on a fresh armfortas clone populates afs-ld correctly. |
| 111 | |
| 112 | ## Definition of Done |
| 113 | |
| 114 | - The accidentally-tracked `afs-ld/.gitignore` is removed from armfortas's index at HEAD. |
| 115 | - afs-ld exists as a standalone GitHub repo under `FortranGoingOnForty`. |
| 116 | - afs-ld is wired into armfortas as a Git submodule, visible in `.gitmodules` and `git submodule status`. |
| 117 | - `armfortas/Cargo.toml` lists `afs-ld` in `[workspace] members`. |
| 118 | - `afs-ld/CLAUDE.md`, `README.md`, `Cargo.toml`, `src/lib.rs`, `src/main.rs`, `src/args.rs` all committed in the new repo. |
| 119 | - `.refs/ld64/` and `.refs/mold/` cloned. |
| 120 | - Differential harness substrate runs, correctly reports zero diffs on identical byte slices, correctly reports critical diffs on intentionally-different byte slices. |
| 121 | - `cargo test --workspace` green. |