Skip to content

Commit 3d48401

Browse files
rdryfooscursoragent
andcommitted
Add craft gates (Sonar policy, SwiftLint, unified CI) and split craft docs.
Consolidate Gate 0 and Gate 2 into ci.yml so build, lint, and traceability run on every push; version Sonar suppressions and engineering conventions separately from operational dev-notes. Co-authored-by: Cursor <[email protected]>
1 parent faad081 commit 3d48401

12 files changed

Lines changed: 391 additions & 89 deletions

File tree

.cursor/rules/craft-check.mdc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
description: Craft gates after Swift or shell changes
3+
globs: ios/HomeFlow/**/*.swift,ios/HomeFlowTests/**/*.swift,scripts/*.sh
4+
alwaysApply: false
5+
---
6+
7+
# Craft check
8+
9+
After substantive Swift or shell changes:
10+
11+
1. `shellcheck scripts/*.sh` (if scripts touched)
12+
2. `cd ios && swiftlint lint --config .swiftlint.yml` (if production Swift touched)
13+
3. `bash scripts/check-traceability.sh` (if IDs, tasks, `@covers`, or `test_AC_*` touched)
14+
15+
Before hiring/release pushes: `bash scripts/check-traceability.sh --refresh` and commit `coverage.md` if the public snapshot should change.
16+
17+
Conventions: `specs/001-mvp/craft-conventions.md`

.github/workflows/ci.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: CI (Craft Gate 0)
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
shellcheck:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- name: Shellcheck gate scripts
14+
run: shellcheck -S warning scripts/*.sh
15+
16+
traceability:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v4
20+
- name: Gate 2 — golden thread
21+
run: bash scripts/check-traceability.sh
22+
23+
ios:
24+
runs-on: macos-15
25+
steps:
26+
- uses: actions/checkout@v4
27+
- name: Prepare Debug secrets for CI build
28+
run: cp ios/HomeFlow/Resources/Secrets.xcconfig.example ios/HomeFlow/Resources/Secrets.xcconfig
29+
- name: Generate Xcode project
30+
run: cd ios && xcodegen generate
31+
- name: SwiftLint
32+
run: |
33+
brew install swiftlint
34+
cd ios && swiftlint lint --config .swiftlint.yml --strict
35+
- name: Build
36+
run: |
37+
cd ios
38+
SIM_ID=$(xcrun simctl list devices available | grep -E '^\s+iPhone' | head -1 | grep -oE '[0-9A-F-]{36}')
39+
echo "Using simulator id=${SIM_ID}"
40+
xcodebuild build \
41+
-project HomeFlow.xcodeproj \
42+
-scheme HomeFlow \
43+
-destination "platform=iOS Simulator,id=${SIM_ID}" \
44+
CODE_SIGNING_ALLOWED=NO
45+
- name: Unit tests
46+
run: |
47+
cd ios
48+
SIM_ID=$(xcrun simctl list devices available | grep -E '^\s+iPhone' | head -1 | grep -oE '[0-9A-F-]{36}')
49+
xcodebuild test \
50+
-project HomeFlow.xcodeproj \
51+
-scheme HomeFlow \
52+
-destination "platform=iOS Simulator,id=${SIM_ID}" \
53+
-only-testing:HomeFlowTests \
54+
CODE_SIGNING_ALLOWED=NO

.github/workflows/traceability.yml

Lines changed: 0 additions & 14 deletions
This file was deleted.

README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ No orphan code, no silent scope, no untracked debt: an acceptance criterion is e
3434
| **`HomesFlow.prd.md`** | Product requirements, user stories, acceptance criteria |
3535
| **`.specify/memory/constitution.md`** | Non-negotiable architectural and process laws |
3636
| **`traceability.md`** | How IDs flow from PRD → spec → tasks → code → tests |
37-
| **`specs/001-mvp/dev-notes.md`** | Engineering operational notes (environments, signing, gaps) |
37+
| **`specs/001-mvp/craft-conventions.md`** | Swift, shell, lint, and Sonar policy (craft gates) |
38+
| **`specs/001-mvp/dev-notes.md`** | Environments, deployment, feature breadcrumbs, platform backlog |
3839

3940
## Repository layout
4041

@@ -76,8 +77,22 @@ export SPECIFY_FEATURE_DIRECTORY=specs/001-mvp
7677

7778
## Quality checks
7879

79-
- **Traceability (Gate 2, enforced)**: `bash scripts/check-traceability.sh` verifies the PRD → spec → tasks → `@covers` → tests golden thread; CI fails on every push if broken (`.github/workflows/traceability.yml`). Optional: `--matrix` refreshes the portfolio snapshot in `specs/001-mvp/coverage.md`; `--json` for machine-readable per-ID status; `--refresh` for matrix + gate + local canvas.
80-
- **Static analysis (Phase 1 integration)**: [SonarCloud dashboard](https://sonarcloud.io/project/overview?id=rdryfoos_HomeFlow) actively tracks code smells and security hotspots on every push. Roadmap: hard-blocking CI branch protection once baseline thresholds are finalized.
80+
| Gate | What | CI |
81+
|------|------|-----|
82+
| **Gate 0** | Build + unit tests + SwiftLint + shellcheck | `.github/workflows/ci.yml` |
83+
| **Gate 2** | Golden thread (`check-traceability.sh`) | same workflow (ubuntu job) |
84+
| **SonarCloud** | Static analysis ([dashboard](https://sonarcloud.io/project/overview?id=rdryfoos_HomeFlow)); policy in `sonar-project.properties` | SonarCloud on push |
85+
86+
Local:
87+
88+
```bash
89+
bash scripts/check-traceability.sh # Gate 2
90+
bash scripts/check-traceability.sh --matrix # portfolio snapshot (coverage.md)
91+
shellcheck scripts/*.sh
92+
cd ios && swiftlint lint --config .swiftlint.yml
93+
```
94+
95+
Craft conventions: `specs/001-mvp/craft-conventions.md`. Sonar suppressions: `specs/001-mvp/sonar-disposition.md`.
8196

8297
## Run locally (Phase 0)
8398

ios/.swiftlint.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Minimal craft lint — see specs/001-mvp/craft-conventions.md
2+
included:
3+
- HomeFlow
4+
excluded:
5+
- HomeFlowTests
6+
- HomeFlowUITests
7+
disabled_rules:
8+
- line_length
9+
- file_length
10+
- type_name
11+
- identifier_name
12+
- function_body_length
13+
- cyclomatic_complexity
14+
- type_body_length
15+
- function_parameter_count
16+
- nesting
17+
- unused_closure_parameter
18+
opt_in_rules:
19+
- unused_optional_binding
20+
- empty_count
21+
- force_cast
22+
- force_try
23+
- redundant_discardable_let

ios/HomeFlow/Core/Sync/LocalDataStore.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ enum LocalDataStore {
5050
try? modelContext.save()
5151
}
5252

53-
private static func deleteAll<T: PersistentModel>(_ type: T.Type, in modelContext: ModelContext) {
53+
private static func deleteAll<T: PersistentModel>(_: T.Type, in modelContext: ModelContext) {
5454
guard let rows = try? modelContext.fetch(FetchDescriptor<T>()) else { return }
5555
rows.forEach { modelContext.delete($0) }
5656
}

scripts/check-traceability.sh

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -76,23 +76,23 @@ is_tested() { grep -qx "$1" "$tmp/test_acs.txt"; }
7676

7777
status_for() { # $1=id $2=done_tasks $3=pending_tasks
7878
local id="$1" done_t="$2" pend_t="$3" type="${1%%-*}"
79-
if [ "$type" = "AC" ]; then
79+
if [[ "$type" == "AC" ]]; then
8080
if is_tested "$id"; then echo "verified"; return; fi
8181
if is_covered "$id"; then
82-
if [ -n "$pend_t" ]; then echo "implemented-test-pending"; else echo "gap"; fi
82+
if [[ -n "$pend_t" ]]; then echo "implemented-test-pending"; else echo "gap"; fi
8383
return
8484
fi
8585
else
8686
if is_covered "$id"; then
87-
if [ -n "$pend_t" ]; then echo "in-progress"; else echo "implemented"; fi
87+
if [[ -n "$pend_t" ]]; then echo "in-progress"; else echo "implemented"; fi
8888
return
8989
fi
9090
fi
91-
if [ -n "$pend_t" ]; then
92-
if [ -n "$done_t" ]; then echo "in-progress"; else echo "planned"; fi
91+
if [[ -n "$pend_t" ]]; then
92+
if [[ -n "$done_t" ]]; then echo "in-progress"; else echo "planned"; fi
9393
return
9494
fi
95-
if [ -n "$done_t" ]; then echo "done-no-covers"; return; fi
95+
if [[ -n "$done_t" ]]; then echo "done-no-covers"; return; fi
9696
echo "unmapped"
9797
}
9898

@@ -109,12 +109,12 @@ write_json_file() { # $1=output path
109109
done_t=$(tasks_for "$id" "x")
110110
pend_t=$(tasks_for "$id" " ")
111111
tests=""
112-
if [ "${id%%-*}" = "AC" ]; then tests=$(tests_for "$id"); fi
112+
if [[ "${id%%-*}" == "AC" ]]; then tests=$(tests_for "$id"); fi
113113
covered=false
114114
if is_covered "$id"; then covered=true; fi
115115
status=$(status_for "$id" "$done_t" "$pend_t")
116116
domain=$(echo "$id" | cut -d- -f2)
117-
if [ $first -eq 0 ]; then echo ","; fi
117+
if [[ $first -eq 0 ]]; then echo ","; fi
118118
first=0
119119
printf ' {"id":"%s","type":"%s","domain":"%s","status":"%s","covered":%s,"doneTasks":[%s],"pendingTasks":[%s],"tests":[%s]}' \
120120
"$id" "${id%%-*}" "$domain" "$status" "$covered" \
@@ -143,20 +143,20 @@ emit_matrix() {
143143
local type="$1" heading="$2" with_tests="$3"
144144
echo "## $heading"
145145
echo
146-
if [ "$with_tests" = "yes" ]; then
146+
if [[ "$with_tests" == "yes" ]]; then
147147
echo "| ID | Status | Done tasks | Pending tasks | Tests |"
148148
echo "|----|--------|------------|---------------|-------|"
149149
else
150150
echo "| ID | Status | Done tasks | Pending tasks |"
151151
echo "|----|--------|------------|---------------|"
152152
fi
153153
while IFS= read -r id; do
154-
[ "${id%%-*}" = "$type" ] || continue
154+
[[ "${id%%-*}" == "$type" ]] || continue
155155
local done_t pend_t tests status
156156
done_t=$(tasks_for "$id" "x")
157157
pend_t=$(tasks_for "$id" " ")
158158
status=$(label_for "$(status_for "$id" "$done_t" "$pend_t")")
159-
if [ "$with_tests" = "yes" ]; then
159+
if [[ "$with_tests" == "yes" ]]; then
160160
tests=$(tests_for "$id" | tr ' ' '\n' | { grep -v '^$' || true; } | sed 's/.*/`&`/' \
161161
| awk '{ printf "%s%s", (NR > 1 ? "<br>" : ""), $0 } END { print "" }')
162162
echo "| $id | $status | ${done_t:-—} | ${pend_t:-—} | ${tests:-—} |"
@@ -245,14 +245,14 @@ SVGEOF
245245
# ---------------------------------------------------------------------------
246246
# Mode routing
247247
# ---------------------------------------------------------------------------
248-
if [ "$MODE" = "--json" ]; then
248+
if [[ "$MODE" == "--json" ]]; then
249249
write_json_file /dev/stdout
250250
exit 0
251251
fi
252252

253-
if [ "$MODE" = "--canvas" ]; then
253+
if [[ "$MODE" == "--canvas" ]]; then
254254
write_json_file "$tmp/data.json"
255-
if [ ! -f "$CANVAS" ]; then
255+
if [[ ! -f "$CANVAS" ]]; then
256256
echo "Canvas not found: $CANVAS" >&2
257257
echo "Set GOLDEN_THREAD_CANVAS or open the Golden Thread Coverage canvas once in Cursor." >&2
258258
exit 1
@@ -261,14 +261,14 @@ if [ "$MODE" = "--canvas" ]; then
261261
exit 0
262262
fi
263263

264-
if [ "$MODE" = "--matrix" ]; then
264+
if [[ "$MODE" == "--matrix" ]]; then
265265
emit_matrix
266266
exit 0
267267
fi
268268

269-
if [ "$MODE" = "--refresh" ]; then
269+
if [[ "$MODE" == "--refresh" ]]; then
270270
emit_matrix
271-
if [ -f "$CANVAS" ]; then
271+
if [[ -f "$CANVAS" ]]; then
272272
write_json_file "$tmp/data.json"
273273
python3 scripts/update-golden-thread-canvas.py "$tmp/data.json" "$CANVAS"
274274
else
@@ -291,20 +291,20 @@ done
291291

292292
# --- 2. Tasks without a Traces field ----------------------------------------
293293
untraced_tasks=$(grep -En '^- \[[ x]\] T[0-9]+' "$TASKS" | grep -v '\*\*Traces\*\*' || true)
294-
if [ -n "$untraced_tasks" ]; then
294+
if [[ -n "$untraced_tasks" ]]; then
295295
err "tasks missing a Traces field:"
296296
echo "$untraced_tasks" | sed 's/^/ /' >&2
297297
fi
298298

299299
# --- 3. Untraced scope (code/tests referencing unknown IDs) ------------------
300300
orphan_covers=$(comm -13 "$tmp/prd.txt" "$tmp/covers.txt")
301-
if [ -n "$orphan_covers" ]; then
301+
if [[ -n "$orphan_covers" ]]; then
302302
err "@covers IDs not in the PRD registry (untraced scope):"
303303
echo "$orphan_covers" | sed 's/^/ /' >&2
304304
fi
305305

306306
orphan_tests=$(comm -13 <(grep '^AC-' "$tmp/prd.txt") "$tmp/test_acs.txt")
307-
if [ -n "$orphan_tests" ]; then
307+
if [[ -n "$orphan_tests" ]]; then
308308
err "test names encode AC IDs not in the PRD registry (untraced scope):"
309309
echo "$orphan_tests" | sed 's/^/ /' >&2
310310
fi
@@ -331,7 +331,7 @@ echo " Untested ACs are tracked as pending tasks unless flagged above."
331331
echo " Per-ID detail on demand: bash scripts/check-traceability.sh --json"
332332
echo " Portfolio snapshot: bash scripts/check-traceability.sh --matrix"
333333

334-
if [ "$fail" -ne 0 ]; then
334+
if [[ "$fail" -ne 0 ]]; then
335335
echo "Gate 2: FAILED — golden thread broken (see FAIL lines above)." >&2
336336
exit 1
337337
fi

sonar-project.properties

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# SonarCloud — see specs/001-mvp/craft-conventions.md and sonar-disposition.md
2+
sonar.projectKey=rdryfoos_HomeFlow
3+
sonar.organization=rdryfoos
4+
5+
sonar.sources=ios/HomeFlow,scripts
6+
sonar.tests=ios/HomeFlowTests,ios/HomeFlowUITests
7+
8+
sonar.exclusions=supabase/**,specs/**,docs/**,archive/**,.specify/**,**/*.md,**/*.svg,**/*.png,**/*.xcconfig
9+
10+
# Rule suppressions aligned with craft conventions (not arbitrary waivers).
11+
sonar.issue.ignore.multicriteria=s100_tests,s115_api_keys,s1075_tests,s1186_swiftui
12+
13+
sonar.issue.ignore.multicriteria.s100_tests.ruleKey=swift:S100
14+
sonar.issue.ignore.multicriteria.s100_tests.resourceKey=**/HomeFlowTests/**
15+
16+
sonar.issue.ignore.multicriteria.s115_api_keys.ruleKey=swift:S115
17+
sonar.issue.ignore.multicriteria.s115_api_keys.resourceKey=**/ios/**
18+
19+
sonar.issue.ignore.multicriteria.s1075_tests.ruleKey=swift:S1075
20+
sonar.issue.ignore.multicriteria.s1075_tests.resourceKey=**/HomeFlowTests/**
21+
22+
sonar.issue.ignore.multicriteria.s1186_swiftui.ruleKey=swift:S1186
23+
sonar.issue.ignore.multicriteria.s1186_swiftui.resourceKey=**/ios/HomeFlow/Features/**

0 commit comments

Comments
 (0)