@cratis/pi 7.103.4 → 7.103.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package/corpus/harnesses/copilot/instructions/local-work-artifacts.instructions.md +7 -7
- package/package/corpus/harnesses/copilot/instructions/specs.csharp.instructions.md +7 -5
- package/package/corpus/hooks/README.md +16 -0
- package/package/corpus/hooks/scripts/cratis-cleanup-worktrees.sh +148 -24
- package/package/corpus/rules/local-work-artifacts.md +7 -7
- package/package/corpus/rules/specs.csharp.md +7 -5
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/SKILL.md +12 -4
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/dependency-map.md +76 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/dotnet-nuget.md +3 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/npm-yarn.md +6 -4
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/train-mode.md +43 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/worked-examples.md +166 -28
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/cratis-local-packages.sh +1065 -60
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/dependency-map.tsv +68 -0
- package/package/corpus/skills/cratis-specifications-csharp/SKILL.md +20 -13
- package/package/corpus/skills/cratis-specifications-csharp/references/running-specs.md +90 -0
- package/package/corpus/skills/cratis-specifications-csharp/references/shared-lifecycle.md +55 -23
- package/package/corpus/skills/cratis-specifications-csharp/verification.json +5 -3
- package/package/corpus/skills/filter-syntax/SKILL.md +3 -1
- package/package/corpus/skills/run-tests/SKILL.md +1 -1
- package/package.json +1 -1
|
@@ -17,8 +17,9 @@ dumps, and similar coordination files. These are **work records, not documentati
|
|
|
17
17
|
If you find one tracked in git, move it into `.ai-work/` and remove it from
|
|
18
18
|
tracking in a dedicated commit.
|
|
19
19
|
- A genuine follow-up that must survive the session is **not** a work record — suggest
|
|
20
|
-
opening a GitHub issue for it
|
|
21
|
-
where everyone can see it, instead of leaving a planning file behind.
|
|
20
|
+
opening a GitHub issue for it so future work is tracked
|
|
21
|
+
where everyone can see it, instead of leaving a planning file behind. A follow-up
|
|
22
|
+
issue includes an `## Acceptance criteria` section of concrete, testable bullets.
|
|
22
23
|
- Knowledge that must outlive the session (real documentation, ADRs, operator
|
|
23
24
|
guides) is written deliberately into the repository's documentation structure
|
|
24
25
|
through normal review — not left behind as a work record.
|
|
@@ -37,16 +38,15 @@ dumps, and similar coordination files. These are **work records, not documentati
|
|
|
37
38
|
replace the prompt.
|
|
38
39
|
- **A worktree is a work record too.** Create it outside the repository tree, for
|
|
39
40
|
example in a sibling `../.worktrees/` directory. A nested worktree inherits the
|
|
40
|
-
parent's `.globalconfig
|
|
41
|
+
parent's `.globalconfig`, causing .NET's `MultipleGlobalAnalyzerKeys`
|
|
41
42
|
errors or silently changing analyzer severities. **Delivered work is cleaned up by
|
|
42
43
|
`.cratis/ai/hooks/scripts/cratis-cleanup-worktrees.sh`, not by memory:** run it from the
|
|
43
44
|
main checkout when a pull request merges or you finish a task (hooks also run it after
|
|
44
45
|
`gh pr merge` and at session start where the harness supports them, and **ship-changes**
|
|
45
46
|
ends with it). It removes a worktree and its branch only on evidence the work reached the
|
|
46
|
-
default branch — pushed is not merged — and never forces.
|
|
47
|
-
directories
|
|
48
|
-
|
|
49
|
-
automatic hooks never opt in. Pin a long-lived worktree with `git worktree lock <path>`;
|
|
47
|
+
default branch — pushed is not merged — and never forces. It keeps worktrees with non-empty
|
|
48
|
+
`.ai-work/` directories; manual `--include-work-records` deletes those records after you
|
|
49
|
+
preserve what you need, and automatic hooks never opt in. Pin a long-lived worktree with `git worktree lock <path>`;
|
|
50
50
|
locked worktrees are always kept, even with opt-in. Other ignored files such as `.env` files
|
|
51
51
|
and local databases can still be deleted with a removed worktree, so move anything you
|
|
52
52
|
still need out first. Do not hand-remove worktrees or `git branch -D` a delivered branch
|
|
@@ -18,9 +18,9 @@ Extends the base [specs.md](./specs.instructions.md) (folder structure, naming,
|
|
|
18
18
|
|
|
19
19
|
## Frameworks
|
|
20
20
|
|
|
21
|
-
- [xUnit](https://xunit.net/) **
|
|
22
|
-
-
|
|
23
|
-
-
|
|
21
|
+
- [xUnit v3](https://xunit.net/docs/getting-started/v3/migration) is the house default for new and migrated spec projects: reference `Cratis.Specifications.XUnit.v3` **5.0** with `xunit.v3.mtp-off`, `Microsoft.NET.Test.Sdk` and `xunit.runner.visualstudio`, and set `<OutputType>Exe</OutputType>` in the project. The package supplies the `Specification` base, convention lifecycle and `ShouldXxx` assertions; the namespace remains `Cratis.Specifications`. Its `Cratis.Specifications` dependency supplies `Catch` and `SpecificationMethods`.
|
|
22
|
+
- 5.0 targets `net10.0` only. A spec project that must stay on net8.0/net9.0 stays on 4.x, and net48 on 3.x. A project that stays on xUnit v2 uses `Cratis.Specifications.XUnit`, with the same base and conventions.
|
|
23
|
+
- `.NUnit`/`.NUnit5`, `.TUnit` and the experimental `.Runner` are alternatives for repositories already built on them.
|
|
24
24
|
- [NSubstitute](https://nsubstitute.github.io/) for mocking collaborators.
|
|
25
25
|
- Spec projects are named `<Source>.Specs`.
|
|
26
26
|
|
|
@@ -48,9 +48,11 @@ public class when_combining_parts : Specification
|
|
|
48
48
|
| `[Fact] void should_*()` | one assertion per fact | use the `ShouldXxx()` extension methods |
|
|
49
49
|
| `void Destroy()` | teardown after each fact by default | once per eligible class with shared lifecycle opt-in |
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
The shared lifecycle is the default on xUnit v2 and v3 since 5.0; the package registers the framework, so delete any hand-written `[assembly: Xunit.TestFramework(...)]` (it fails with `CS0579`). Eligible classes derive from `Specification`, have one public parameterless constructor, do not implement `IDisposable`, and contain only plain facts. Construction, Establish/Because and Destroy run once; all facts share the instance and must not mutate it. Initialization failure fails every fact. Theories and `ITestOutputHelper` constructors keep per-fact execution; `[PerFactSpecificationLifecycle]` opts a class out. `CRSPEC0008` (a fact modifies the shared context) is an error by default: move the change into `Establish` or `Because`. See the **cratis-specifications-csharp** skill's shared-lifecycle reference.
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
Run specifications as the built executable, not `dotnet test`: restore and build once, then run `bin/<Config>/net10.0/<Project>.Specs`. In a solution that mixes Microsoft.Testing.Platform and VSTest projects, run each project on its own; `dotnet test` on the whole solution does not work there. Filters and the 4.x → 5.0 checklist are in the **cratis-specifications-csharp** skill's [running-specs reference](../../../skills/cratis-specifications-csharp/references/running-specs.md).
|
|
54
|
+
|
|
55
|
+
CRSPEC0001–CRSPEC0007 are warnings bundled in `.Specifications`, `.XUnit`, `.XUnit.v3`, and `.NUnit`. With warnings as errors, a non-`should_` fact, multiple specs per file (given contexts exempt), a fact on a given context, a `should_` without a test attribute, explicit base lifecycle calls, non-public specs with facts, or `Because` on a given context fail the build. Follow **cratis-specification-by-example**; do not suppress these diagnostics.
|
|
54
56
|
|
|
55
57
|
## Reusable layered contexts
|
|
56
58
|
|
|
@@ -128,6 +128,22 @@ ignored records. Other ignored files (`node_modules`, build output) go with a de
|
|
|
128
128
|
`--include-work-records` only after preserving any records you still need: it permits deleting
|
|
129
129
|
`.ai-work/` content, but never overrides locks or other safety checks. Hook mode ignores this flag;
|
|
130
130
|
automatic hooks never opt in.
|
|
131
|
+
- **GitHub lookups.** The complete worktree and branch sweep is planned before any removal. Git
|
|
132
|
+
ancestry is checked first; only a plan needing more evidence triggers the single REST batch. It
|
|
133
|
+
reads the latest 100 closed pull requests, so older merged branches absent from that page are simply
|
|
134
|
+
kept. Only merged entries with the exact branch and head commit qualify. Hook runs share an atomic
|
|
135
|
+
stamp and cache under the Git common directory, with at most one request per repository every
|
|
136
|
+
15 minutes, including failed attempts. A fresh successful cache is reused. A failed request, invalid
|
|
137
|
+
response, or unavailable lock without a fresh successful cache aborts **all** removals in that sweep,
|
|
138
|
+
including ancestry-proven worktrees and branches, with a one-line notice. Failed attempts are marked
|
|
139
|
+
for the full window: later plans needing GitHub also abort all removals without retrying, while plans
|
|
140
|
+
needing only ancestry evidence proceed without GitHub. Locks older than 15 minutes can be reclaimed.
|
|
141
|
+
Manual runs are not throttled, but also use at most one request per run and abort all removals if
|
|
142
|
+
that request fails.
|
|
143
|
+
- **Disable GitHub only.** Set `CRATIS_CLEANUP_NO_GH=1` to avoid all GitHub lookups while keeping
|
|
144
|
+
cleanup based on Git ancestry enabled. Missing `gh` or `jq`, a non-GitHub origin, or an invalid
|
|
145
|
+
repository slug also select ancestry-only cleanup before planning: unproven items are kept, with
|
|
146
|
+
no GitHub call, abort, or failed-attempt marker.
|
|
131
147
|
- **Pin a long-lived worktree.** Use `git worktree lock <path>` to keep it out of cleanup; release the
|
|
132
148
|
pin with `git worktree unlock <path>` when ready.
|
|
133
149
|
- **Self-test.** `--self-test` plants delivered, dirty, unmerged, brand-new, work-record and locked cases
|
|
@@ -139,34 +139,128 @@ REMOVED=(); KEPT=(); FREED_KB=0
|
|
|
139
139
|
note_removed() { REMOVED+=("$1"); }
|
|
140
140
|
note_kept() { KEPT+=("$1"); }
|
|
141
141
|
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
142
|
+
# The cache is available only after a successful, valid batch. Failed attempts also stamp the window.
|
|
143
|
+
GH_PULLS=''
|
|
144
|
+
valid_pulls() {
|
|
145
|
+
jq -e -s 'length == 1 and (.[0] | type == "array" and all(.[];
|
|
146
|
+
type == "object" and has("merged_at") and (.merged_at == null or (.merged_at | type == "string")) and
|
|
147
|
+
(.head | type == "object" and (.ref | type == "string") and (.sha | type == "string"))))' "$1" >/dev/null 2>&1
|
|
148
|
+
}
|
|
149
|
+
lock_stat() { stat -c '%i %Y' "$1" 2>/dev/null || stat -f '%i %m' "$1" 2>/dev/null; }
|
|
150
|
+
fresh_stamp() {
|
|
151
|
+
local stamp
|
|
152
|
+
stamp=$(cat "$state/stamp" 2>/dev/null || true)
|
|
153
|
+
[[ "$stamp" =~ ^[0-9]+$ ]] && [ "$now" -lt "$((stamp + 900))" ]
|
|
154
|
+
}
|
|
155
|
+
cached_pulls() {
|
|
156
|
+
fresh_stamp && [ ! -e "$state/failed" ] && valid_pulls "$state/pulls.json" || return 1
|
|
157
|
+
cat "$state/pulls.json"
|
|
158
|
+
}
|
|
159
|
+
take_lock() {
|
|
160
|
+
mkdir "$state/lock" 2>/dev/null && return 0
|
|
161
|
+
local inode modified retired acquired current
|
|
162
|
+
read -r inode modified < <(lock_stat "$state/lock")
|
|
163
|
+
[[ "$modified" =~ ^[0-9]+$ ]] && [ "$now" -ge "$((modified + 900))" ] || return 1
|
|
164
|
+
# Rename atomically: a killed reclaimer can leave only a retired directory, never a blocking marker.
|
|
165
|
+
retired="$state/lock.stale.$$.$RANDOM"
|
|
166
|
+
mv "$state/lock" "$retired" 2>/dev/null || return 1
|
|
167
|
+
read -r current _ < <(lock_stat "$retired")
|
|
168
|
+
if [ "$current" != "$inode" ]; then
|
|
169
|
+
# Another reclaimer replaced the stale directory. Restore its lock if possible;
|
|
170
|
+
# never acquire or delete a directory that was not the stale one we inspected.
|
|
171
|
+
[ -e "$state/lock" ] || mv "$retired" "$state/lock" 2>/dev/null
|
|
172
|
+
return 1
|
|
148
173
|
fi
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
174
|
+
acquired=1
|
|
175
|
+
mkdir "$state/lock" 2>/dev/null && acquired=0
|
|
176
|
+
rm -rf "$retired"
|
|
177
|
+
return "$acquired"
|
|
178
|
+
}
|
|
179
|
+
hook_pulls() (
|
|
180
|
+
local repo="$1" common state now temporary lock_inode current
|
|
181
|
+
common=$(git rev-parse --git-common-dir) || exit 1
|
|
182
|
+
state="$common/cratis-cleanup-gh/$repo"
|
|
183
|
+
now=$(date +%s)
|
|
184
|
+
mkdir -p "$state" 2>/dev/null || exit 1
|
|
185
|
+
if ! take_lock; then cached_pulls; exit $?; fi
|
|
186
|
+
read -r lock_inode _ < <(lock_stat "$state/lock")
|
|
187
|
+
temporary=''
|
|
188
|
+
# A retired owner must never remove its replacement's lock.
|
|
189
|
+
trap '[[ -z "$temporary" ]] || rm -f "$temporary"; read -r current _ < <(lock_stat "$state/lock"); [ "$current" != "$lock_inode" ] || rmdir "$state/lock" 2>/dev/null' EXIT
|
|
190
|
+
if fresh_stamp; then cached_pulls; exit $?; fi
|
|
191
|
+
temporary=$(mktemp "$state/pulls.XXXXXX") || exit 1
|
|
192
|
+
printf '%s\n' "$now" > "$temporary" && mv "$temporary" "$state/failed" || exit 1
|
|
193
|
+
printf '%s\n' "$now" > "$temporary" && mv "$temporary" "$state/stamp" || exit 1
|
|
194
|
+
read -r current _ < <(lock_stat "$state/lock")
|
|
195
|
+
[ "$current" = "$lock_inode" ] || exit 1
|
|
196
|
+
# Only the latest 100 closed PRs are searched; older merged branches are simply kept.
|
|
197
|
+
if gh api "repos/$repo/pulls?state=closed&per_page=100" > "$temporary" 2>/dev/null && valid_pulls "$temporary"; then
|
|
198
|
+
read -r current _ < <(lock_stat "$state/lock")
|
|
199
|
+
[ "$current" = "$lock_inode" ] || exit 1
|
|
200
|
+
mv "$temporary" "$state/pulls.json" && rm -f "$state/failed" || exit 1
|
|
201
|
+
cat "$state/pulls.json"
|
|
202
|
+
else exit 1; fi
|
|
203
|
+
)
|
|
204
|
+
GH_POSSIBLE=0; GH_REPO=''
|
|
205
|
+
configure_github() {
|
|
206
|
+
[ "${CRATIS_CLEANUP_NO_GH:-}" != 1 ] || return 0
|
|
207
|
+
command -v gh >/dev/null 2>&1 && command -v jq >/dev/null 2>&1 || return 0
|
|
208
|
+
local remote repo
|
|
209
|
+
remote=$(git remote get-url origin 2>/dev/null) || return 0
|
|
210
|
+
case "$remote" in
|
|
211
|
+
https://github.com/*) repo=${remote#https://github.com/} ;;
|
|
212
|
+
git@github.com:*) repo=${remote#git@github.com:} ;;
|
|
213
|
+
ssh://git@github.com/*) repo=${remote#ssh://git@github.com/} ;;
|
|
214
|
+
*) return 0 ;;
|
|
215
|
+
esac
|
|
216
|
+
repo=${repo%.git}
|
|
217
|
+
[[ "$repo" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || return 0
|
|
218
|
+
GH_REPO=$repo; GH_POSSIBLE=1
|
|
219
|
+
}
|
|
220
|
+
configure_github
|
|
221
|
+
load_pulls() {
|
|
222
|
+
if [ "$HOOK" = 1 ]; then GH_PULLS=$(hook_pulls "$GH_REPO") || return 1
|
|
223
|
+
else GH_PULLS=$(gh api "repos/$GH_REPO/pulls?state=closed&per_page=100" 2>/dev/null) || return 1; fi
|
|
224
|
+
printf '%s' "$GH_PULLS" | valid_pulls /dev/stdin
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# Ancestry evidence is resolved during planning; GitHub evidence only after the complete plan exists.
|
|
228
|
+
ancestry_delivered() {
|
|
229
|
+
local sha="$1" track="$3"
|
|
230
|
+
if git merge-base --is-ancestor "$sha" "$default" 2>/dev/null; then
|
|
231
|
+
if ! grep -qx "$sha" "$firstparent"; then EVIDENCE="merged into $default"; return 0; fi
|
|
232
|
+
if [ "$track" = "[gone]" ]; then EVIDENCE="merged into $default, upstream gone"; return 0; fi
|
|
153
233
|
fi
|
|
154
234
|
return 1
|
|
155
235
|
}
|
|
236
|
+
github_delivered() {
|
|
237
|
+
printf '%s' "$GH_PULLS" | jq -e --arg br "$1" --arg sha "$2" \
|
|
238
|
+
'any(.[]; .merged_at != null and .head.ref == $br and .head.sha == $sha)' >/dev/null 2>&1
|
|
239
|
+
}
|
|
240
|
+
PLAN_KIND=(); PLAN_PATH=(); PLAN_SHA=(); PLAN_BRANCH=(); PLAN_EVIDENCE=(); NEEDS_GH=0
|
|
241
|
+
plan_item() {
|
|
242
|
+
local kind="$1" path="$2" tip="$3" branch="$4" evidence=''
|
|
243
|
+
if ancestry_delivered "$tip" "$branch" "$([ -n "$branch" ] && track_of "$branch")"; then evidence=$EVIDENCE
|
|
244
|
+
elif [ -n "$branch" ] && [ "$GH_POSSIBLE" = 1 ]; then NEEDS_GH=1; fi
|
|
245
|
+
PLAN_KIND+=("$kind"); PLAN_PATH+=("$path"); PLAN_SHA+=("$tip")
|
|
246
|
+
PLAN_BRANCH+=("$branch"); PLAN_EVIDENCE+=("$evidence")
|
|
247
|
+
}
|
|
156
248
|
track_of() { git for-each-ref --format='%(upstream:track)' "refs/heads/$1" 2>/dev/null; }
|
|
157
249
|
|
|
158
|
-
# ──
|
|
159
|
-
git worktree prune >/dev/null 2>&1
|
|
250
|
+
# ── Plan the entire sweep without removing worktrees or branches ──────────────
|
|
160
251
|
wt=""; sha=""; br=""; locked=0; first=1
|
|
161
252
|
process_worktree() {
|
|
162
253
|
[ -n "$wt" ] || return 0
|
|
163
254
|
if [ "$first" = 1 ]; then first=0; return 0; fi # the main worktree is never removed
|
|
164
|
-
local name="${br:-detached}"
|
|
255
|
+
local name="${br:-detached}"
|
|
165
256
|
if [ "$locked" = 1 ]; then note_kept "worktree $wt ($name): locked"; return 0; fi
|
|
166
257
|
case "$here/" in "$wt"/*) note_kept "worktree $wt ($name): it is the current directory - run the sweep from the main checkout"; return 0 ;; esac
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
258
|
+
plan_item worktree "$wt" "$sha" "$br"
|
|
259
|
+
}
|
|
260
|
+
execute_worktree() {
|
|
261
|
+
local name="${br:-detached}" ev="$1" current
|
|
262
|
+
current=$(git -C "$wt" rev-parse HEAD 2>/dev/null) || return 0
|
|
263
|
+
[ "$current" = "$sha" ] || { note_kept "worktree $wt ($name): tip changed since planning"; return 0; }
|
|
170
264
|
if [ "$INCLUDE_WORK_RECORDS" = 0 ] && [ -d "$wt/.ai-work" ]; then
|
|
171
265
|
local records
|
|
172
266
|
if ! records=$(find "$wt/.ai-work/." -mindepth 1 -maxdepth 1 -print -quit); then
|
|
@@ -191,20 +285,50 @@ while IFS= read -r line; do
|
|
|
191
285
|
done < <(git worktree list --porcelain; echo)
|
|
192
286
|
process_worktree
|
|
193
287
|
|
|
194
|
-
# ── Local branches ────────────────────────────────────────────────────────────
|
|
195
288
|
in_use=$(git worktree list --porcelain | sed -n 's#^branch refs/heads/##p')
|
|
289
|
+
planned_branches=$(printf '%s\n' "${PLAN_BRANCH[@]+"${PLAN_BRANCH[@]}"}")
|
|
196
290
|
while IFS='|' read -r b bsha; do
|
|
197
291
|
if [ -z "$b" ] || [ "$b" = "$default_local" ]; then continue; fi
|
|
198
|
-
|
|
199
|
-
if
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
else note_kept "branch $b: git refused to delete it"; fi
|
|
203
|
-
else
|
|
204
|
-
note_kept "branch $b: work not delivered to $default"
|
|
292
|
+
# Drain producers under pipefail: grep -q can turn a match into a SIGPIPE failure.
|
|
293
|
+
if printf '%s\n' "$in_use" | grep -x "$b" >/dev/null; then
|
|
294
|
+
[ "$DRY" = 0 ] || continue
|
|
295
|
+
printf '%s\n' "$planned_branches" | grep -x "$b" >/dev/null || continue
|
|
205
296
|
fi
|
|
297
|
+
plan_item branch "$b" "$bsha" "$b"
|
|
206
298
|
done < <(git for-each-ref --format='%(refname:short)|%(objectname)' refs/heads)
|
|
207
299
|
|
|
300
|
+
abort_sweep() {
|
|
301
|
+
echo "cratis-cleanup-worktrees: GitHub evidence unavailable; 0 removed, keeping the entire sweep" >&2
|
|
302
|
+
exit 0
|
|
303
|
+
}
|
|
304
|
+
# Resolve every item's evidence before any deletion, including ancestry-proven items.
|
|
305
|
+
if [ "$NEEDS_GH" = 1 ] && ! load_pulls; then abort_sweep; fi
|
|
306
|
+
for ((i=0; i<${#PLAN_KIND[@]}; i++)); do
|
|
307
|
+
if [ -z "${PLAN_EVIDENCE[i]}" ] && [ -n "${PLAN_BRANCH[i]}" ] && [ -n "$GH_PULLS" ]; then
|
|
308
|
+
if github_delivered "${PLAN_BRANCH[i]}" "${PLAN_SHA[i]}"; then PLAN_EVIDENCE[i]="pull request merged at this commit"
|
|
309
|
+
else result=$?; [ "$result" = 1 ] || abort_sweep; fi
|
|
310
|
+
fi
|
|
311
|
+
done
|
|
312
|
+
|
|
313
|
+
# ── Execute the resolved plan, rechecking tips and worktree safety ─────────────
|
|
314
|
+
git worktree prune >/dev/null 2>&1
|
|
315
|
+
for ((i=0; i<${#PLAN_KIND[@]}; i++)); do
|
|
316
|
+
kind=${PLAN_KIND[i]}; path=${PLAN_PATH[i]}; sha=${PLAN_SHA[i]}; br=${PLAN_BRANCH[i]}; ev=${PLAN_EVIDENCE[i]}
|
|
317
|
+
if [ "$kind" = branch ]; then
|
|
318
|
+
git worktree list --porcelain | sed -n 's#^branch refs/heads/##p' | grep -x "$br" >/dev/null && continue
|
|
319
|
+
fi
|
|
320
|
+
if [ -z "$ev" ]; then
|
|
321
|
+
if [ "$kind" = worktree ]; then note_kept "worktree $path (${br:-detached}): work not delivered to $default"
|
|
322
|
+
else note_kept "branch $br: work not delivered to $default"; fi
|
|
323
|
+
continue
|
|
324
|
+
fi
|
|
325
|
+
if [ "$kind" = worktree ]; then wt=$path; execute_worktree "$ev"; continue; fi
|
|
326
|
+
[ "$(git rev-parse -q --verify "refs/heads/$br")" = "$sha" ] || { note_kept "branch $br: tip changed since planning"; continue; }
|
|
327
|
+
if [ "$DRY" = 1 ]; then note_removed "branch $br: would delete, $ev"
|
|
328
|
+
elif git branch -D "$br" >/dev/null 2>&1; then note_removed "branch $br: $ev"
|
|
329
|
+
else note_kept "branch $br: git refused to delete it"; fi
|
|
330
|
+
done
|
|
331
|
+
|
|
208
332
|
# ── Report ────────────────────────────────────────────────────────────────────
|
|
209
333
|
summary="cratis-cleanup-worktrees: ${#REMOVED[@]} removed, ${#KEPT[@]} kept"
|
|
210
334
|
[ "$FREED_KB" -gt 0 ] && summary="$summary, $((FREED_KB / 1024)) MB freed"
|
|
@@ -16,8 +16,9 @@ dumps, and similar coordination files. These are **work records, not documentati
|
|
|
16
16
|
If you find one tracked in git, move it into `.ai-work/` and remove it from
|
|
17
17
|
tracking in a dedicated commit.
|
|
18
18
|
- A genuine follow-up that must survive the session is **not** a work record — suggest
|
|
19
|
-
opening a GitHub issue for it
|
|
20
|
-
where everyone can see it, instead of leaving a planning file behind.
|
|
19
|
+
opening a GitHub issue for it so future work is tracked
|
|
20
|
+
where everyone can see it, instead of leaving a planning file behind. A follow-up
|
|
21
|
+
issue includes an `## Acceptance criteria` section of concrete, testable bullets.
|
|
21
22
|
- Knowledge that must outlive the session (real documentation, ADRs, operator
|
|
22
23
|
guides) is written deliberately into the repository's documentation structure
|
|
23
24
|
through normal review — not left behind as a work record.
|
|
@@ -36,16 +37,15 @@ dumps, and similar coordination files. These are **work records, not documentati
|
|
|
36
37
|
replace the prompt.
|
|
37
38
|
- **A worktree is a work record too.** Create it outside the repository tree, for
|
|
38
39
|
example in a sibling `../.worktrees/` directory. A nested worktree inherits the
|
|
39
|
-
parent's `.globalconfig
|
|
40
|
+
parent's `.globalconfig`, causing .NET's `MultipleGlobalAnalyzerKeys`
|
|
40
41
|
errors or silently changing analyzer severities. **Delivered work is cleaned up by
|
|
41
42
|
`.cratis/ai/hooks/scripts/cratis-cleanup-worktrees.sh`, not by memory:** run it from the
|
|
42
43
|
main checkout when a pull request merges or you finish a task (hooks also run it after
|
|
43
44
|
`gh pr merge` and at session start where the harness supports them, and **ship-changes**
|
|
44
45
|
ends with it). It removes a worktree and its branch only on evidence the work reached the
|
|
45
|
-
default branch — pushed is not merged — and never forces.
|
|
46
|
-
directories
|
|
47
|
-
|
|
48
|
-
automatic hooks never opt in. Pin a long-lived worktree with `git worktree lock <path>`;
|
|
46
|
+
default branch — pushed is not merged — and never forces. It keeps worktrees with non-empty
|
|
47
|
+
`.ai-work/` directories; manual `--include-work-records` deletes those records after you
|
|
48
|
+
preserve what you need, and automatic hooks never opt in. Pin a long-lived worktree with `git worktree lock <path>`;
|
|
49
49
|
locked worktrees are always kept, even with opt-in. Other ignored files such as `.env` files
|
|
50
50
|
and local databases can still be deleted with a removed worktree, so move anything you
|
|
51
51
|
still need out first. Do not hand-remove worktrees or `git branch -D` a delivered branch
|
|
@@ -17,9 +17,9 @@ Extends the base [specs.md](./specs.md) (folder structure, naming, philosophy) w
|
|
|
17
17
|
|
|
18
18
|
## Frameworks
|
|
19
19
|
|
|
20
|
-
- [xUnit](https://xunit.net/) **
|
|
21
|
-
-
|
|
22
|
-
-
|
|
20
|
+
- [xUnit v3](https://xunit.net/docs/getting-started/v3/migration) is the house default for new and migrated spec projects: reference `Cratis.Specifications.XUnit.v3` **5.0** with `xunit.v3.mtp-off`, `Microsoft.NET.Test.Sdk` and `xunit.runner.visualstudio`, and set `<OutputType>Exe</OutputType>` in the project. The package supplies the `Specification` base, convention lifecycle and `ShouldXxx` assertions; the namespace remains `Cratis.Specifications`. Its `Cratis.Specifications` dependency supplies `Catch` and `SpecificationMethods`.
|
|
21
|
+
- 5.0 targets `net10.0` only. A spec project that must stay on net8.0/net9.0 stays on 4.x, and net48 on 3.x. A project that stays on xUnit v2 uses `Cratis.Specifications.XUnit`, with the same base and conventions.
|
|
22
|
+
- `.NUnit`/`.NUnit5`, `.TUnit` and the experimental `.Runner` are alternatives for repositories already built on them.
|
|
23
23
|
- [NSubstitute](https://nsubstitute.github.io/) for mocking collaborators.
|
|
24
24
|
- Spec projects are named `<Source>.Specs`.
|
|
25
25
|
|
|
@@ -47,9 +47,11 @@ public class when_combining_parts : Specification
|
|
|
47
47
|
| `[Fact] void should_*()` | one assertion per fact | use the `ShouldXxx()` extension methods |
|
|
48
48
|
| `void Destroy()` | teardown after each fact by default | once per eligible class with shared lifecycle opt-in |
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
The shared lifecycle is the default on xUnit v2 and v3 since 5.0; the package registers the framework, so delete any hand-written `[assembly: Xunit.TestFramework(...)]` (it fails with `CS0579`). Eligible classes derive from `Specification`, have one public parameterless constructor, do not implement `IDisposable`, and contain only plain facts. Construction, Establish/Because and Destroy run once; all facts share the instance and must not mutate it. Initialization failure fails every fact. Theories and `ITestOutputHelper` constructors keep per-fact execution; `[PerFactSpecificationLifecycle]` opts a class out. `CRSPEC0008` (a fact modifies the shared context) is an error by default: move the change into `Establish` or `Because`. See the **cratis-specifications-csharp** skill's shared-lifecycle reference.
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Run specifications as the built executable, not `dotnet test`: restore and build once, then run `bin/<Config>/net10.0/<Project>.Specs`. In a solution that mixes Microsoft.Testing.Platform and VSTest projects, run each project on its own; `dotnet test` on the whole solution does not work there. Filters and the 4.x → 5.0 checklist are in the **cratis-specifications-csharp** skill's [running-specs reference](../skills/cratis-specifications-csharp/references/running-specs.md).
|
|
53
|
+
|
|
54
|
+
CRSPEC0001–CRSPEC0007 are warnings bundled in `.Specifications`, `.XUnit`, `.XUnit.v3`, and `.NUnit`. With warnings as errors, a non-`should_` fact, multiple specs per file (given contexts exempt), a fact on a given context, a `should_` without a test attribute, explicit base lifecycle calls, non-public specs with facts, or `Because` on a given context fail the build. Follow **cratis-specification-by-example**; do not suppress these diagnostics.
|
|
53
55
|
|
|
54
56
|
## Reusable layered contexts
|
|
55
57
|
|
|
@@ -55,12 +55,14 @@ commands before choosing the build, regression spec and full gate commands.
|
|
|
55
55
|
## Copy this checklist
|
|
56
56
|
|
|
57
57
|
- [ ] Reproduce the consumer bug and select its regression spec/app action.
|
|
58
|
-
- [ ]
|
|
58
|
+
- [ ] Read [references/dependency-map.md](references/dependency-map.md) for the chain between the named repositories, generate one stamp, then pack each producer in upstream order.
|
|
59
59
|
- [ ] If needed, publish the kernel and assets, then build a matching local image.
|
|
60
|
+
- [ ] Run `closure` for the consumer and producers, follow the printed commands, and run its one `verify-pack` after the last pack.
|
|
60
61
|
- [ ] Activate the consumer overlay and select the local image.
|
|
61
62
|
- [ ] Build the consumer, run its regression specs/app action and required gates.
|
|
62
63
|
- [ ] On failure, diagnose, fix the producer, create a distinct stamp and repeat.
|
|
63
|
-
- [ ] Run `check` before every consumer commit and push, and on the producer.
|
|
64
|
+
- [ ] Run `check` before every consumer commit and push, and on the producer; it must report 0 leaks.
|
|
65
|
+
- [ ] Upstream first: a downstream ships only after the upstream has released and its pin is bumped to the released version.
|
|
64
66
|
- [ ] Restore overlays, unset environment overrides, and run `check` again.
|
|
65
67
|
- [ ] Review stamp-scoped `clean` dry-run targets, then apply cleanup.
|
|
66
68
|
|
|
@@ -92,7 +94,12 @@ For npm, complete the repository build before
|
|
|
92
94
|
exit `0`. Inspect package dependencies: every locally changed dependency ID must
|
|
93
95
|
resolve to the stamp, not a published fallback. Chronicle's client solution filter
|
|
94
96
|
can omit `Cratis.Chronicle.CodeAnalysis`; pack its code-analysis package project
|
|
95
|
-
separately.
|
|
97
|
+
separately. The package IDs a producer should ship are listed in
|
|
98
|
+
[dependency-map.tsv](scripts/dependency-map.tsv); `closure CONSUMER=PATH PRODUCER=PATH...` prints
|
|
99
|
+
the stamp, every overlay, one `pack-*` command per map row (with its `--property` arguments) and
|
|
100
|
+
ONE `verify-pack` for the whole plan (see [references/dependency-map.md](references/dependency-map.md)).
|
|
101
|
+
Run the printed commands in order. A successful pack alone does not prove dependency closure;
|
|
102
|
+
`verify-pack` does.
|
|
96
103
|
|
|
97
104
|
### 3. Build an image when needed
|
|
98
105
|
|
|
@@ -237,7 +244,8 @@ minutes, instead of after a release and a pin bump.
|
|
|
237
244
|
- Read [references/npm-yarn.md](references/npm-yarn.md) when packing frontend libraries or restoring Yarn overlays.
|
|
238
245
|
- Read [references/docker-images.md](references/docker-images.md) when publishing a kernel or selecting images in a consumer.
|
|
239
246
|
- Read [references/go-jvm-python-elixir.md](references/go-jvm-python-elixir.md) when the boundary uses another language toolchain.
|
|
240
|
-
- Read [references/worked-examples.md](references/worked-examples.md) when following Chronicle → Studio, Fundamentals → Arc or
|
|
247
|
+
- Read [references/worked-examples.md](references/worked-examples.md) when following Chronicle → Studio, Fundamentals → Arc, Components → Studio, Chronicle → Direct, Screenplay → Stage and Studio, Arc → Chronicle, or Fundamentals → Arc → Chronicle.
|
|
248
|
+
- Read [references/train-mode.md](references/train-mode.md) when verifying a downstream repository against a local release train (`stamp --train`).
|
|
241
249
|
- Read [references/other-environments.md](references/other-environments.md) when running on another developer machine or an agent host.
|
|
242
250
|
|
|
243
251
|
Read `scripts/npm-restore-check.cjs` when inspecting the read-only npm restoration
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<!-- Copyright (c) Cratis. All rights reserved. -->
|
|
2
|
+
<!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
|
|
3
|
+
|
|
4
|
+
# Dependency map
|
|
5
|
+
|
|
6
|
+
`scripts/dependency-map.tsv` is a reference table of the producer-to-consumer edges between
|
|
7
|
+
Cratis repositories, the pack targets of each producer and the package IDs each ships.
|
|
8
|
+
`cratis-local-packages.sh` reads it (or the file in `CRATIS_LOCAL_MAP`, or `--map FILE`) to derive
|
|
9
|
+
one plan: which repositories to pack, in which order, and which IDs the feed must hold. It holds
|
|
10
|
+
no versions; read pins live from the consumer manifests (`Directory.Packages.props`,
|
|
11
|
+
`package.json`, `Dockerfile`).
|
|
12
|
+
|
|
13
|
+
## Rows
|
|
14
|
+
|
|
15
|
+
Tab-separated, `#` starts a comment.
|
|
16
|
+
|
|
17
|
+
| Row | Fields | Meaning |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| `pack` | repo, eco, target, `PROP=VAL,...` | A pack target; the optional MSBuild properties are the `-p:` arguments the producer's own publish workflow passes. `closure` prints each as one `--property NAME=VALUE` argument of that row's `pack-nuget` command |
|
|
20
|
+
| `ids` | repo, eco, comma-separated IDs | The packages the producer ships in that ecosystem |
|
|
21
|
+
| `uses` | consumer, producer, eco, manifest | An edge; the manifest is the pin file, or the Yarn project directory for npm |
|
|
22
|
+
| `image` | repo, dockerfile, tag suffix | A producer image a consumer builds on |
|
|
23
|
+
|
|
24
|
+
`eco` is `nuget`, `npm` or `image`. For npm, `.` means the repository root.
|
|
25
|
+
|
|
26
|
+
## Using it
|
|
27
|
+
|
|
28
|
+
- Find every `uses` row whose producer is the repository you changed; those consumers
|
|
29
|
+
pin its packages. A consumer may also reach the producer through another repository
|
|
30
|
+
(Fundamentals reaches Chronicle through Arc as well as directly).
|
|
31
|
+
- Pack upstream first. Overlay the earlier producer into the next repository with
|
|
32
|
+
`use-nuget`, pack that, then `restore` it. Use one stamp for the whole chain: `stamp`
|
|
33
|
+
takes several producers, so generate it once and reuse the string.
|
|
34
|
+
- Read the closure: `closure [--train] CONSUMER=PATH PRODUCER=PATH...` (producers in dependency
|
|
35
|
+
order) prints the commands to run, in order, from the plan; `plan CONSUMER PRODUCER...` prints
|
|
36
|
+
the plan rows alone. Both exit 2 and print nothing when the plan is invalid.
|
|
37
|
+
- Pack properties are `pack-nuget --property NAME=VALUE` arguments, one per `-p:` of that command
|
|
38
|
+
only; never export them (an exported `IsPackaging=true` reaches Chronicle's nested `dotnet run`
|
|
39
|
+
code generator and fails with a multi-target error).
|
|
40
|
+
- `--train` prints the train stamp (see [train-mode.md](train-mode.md)) instead of a local stamp.
|
|
41
|
+
|
|
42
|
+
## Pack plan and verification scope
|
|
43
|
+
|
|
44
|
+
The plan is a set of `(producer, ecosystem)` pack rows, overlay rows (a producer and the upstream
|
|
45
|
+
producers it consumes) and consume rows (the consumer's own edges), derived from the `uses` rows
|
|
46
|
+
for the names you list. It is rejected (exit 2) for an unknown or repeated name, no edge, a wrong
|
|
47
|
+
order, or a planned pack without `ids` or `pack` rows.
|
|
48
|
+
|
|
49
|
+
- Run `verify-pack STAMP --consumer C --producer P...` once, after the last pack. It checks the
|
|
50
|
+
planned IDs exist at exactly STAMP and each planned NuGet dependency is exactly STAMP, [STAMP] or [STAMP, ) (an unreadable dependency element fails).
|
|
51
|
+
- After `use-*` and restore, `verify-assets CONSUMER-PATH STAMP --consumer C --producer P...`
|
|
52
|
+
checks the same plan inside the consumer: no planned ID resolved at another version, and no
|
|
53
|
+
producer the consumer uses left unconsumed. `--json` lists the resolved IDs only.
|
|
54
|
+
- Each command derives and checks the plan itself and keeps no state between runs.
|
|
55
|
+
- `verify-pack STAMP NAME...` and `verify-assets CONSUMER STAMP` without a plan are manual and
|
|
56
|
+
weaker: they check the named repositories' IDs, or every stamped package the consumer resolves.
|
|
57
|
+
|
|
58
|
+
## Facts about the chains
|
|
59
|
+
|
|
60
|
+
- Arc and Chronicle depend on each other. Arc's Chronicle-facing packages are built against
|
|
61
|
+
a released Chronicle, and Chronicle's clients against a released Arc. The same holds for
|
|
62
|
+
Chronicle and Orleans. Record the nuspec dependencies and do not break the cycle.
|
|
63
|
+
- Image and package pins are coupled. A consumer that builds `FROM` a producer image may
|
|
64
|
+
derive the tag from the producer's NuGet pin; check its publish workflow before tagging a
|
|
65
|
+
local image.
|
|
66
|
+
- A repository you did not name keeps its released version: Stage reaches Arc, Chronicle
|
|
67
|
+
reaches Orleans. A binary break through that hop is a finding about the consumer's pins.
|
|
68
|
+
- The current Screenplay rows list only npm IDs although Stage and Studio consume its NuGet
|
|
69
|
+
packages; `closure` exits 2 for them until the map has the Screenplay NuGet `ids`. Read the
|
|
70
|
+
IDs from the producer's publish workflow and add them first.
|
|
71
|
+
|
|
72
|
+
## Extend it
|
|
73
|
+
|
|
74
|
+
Add `pack` rows only from the producer's publish workflow on the default branch, not a
|
|
75
|
+
stale clone. Add `ids` for every package a consumer must resolve. Add one `uses` row per
|
|
76
|
+
consumer manifest that pins the producer.
|
|
@@ -28,6 +28,9 @@ dotnet restore path/to/consumer.slnx
|
|
|
28
28
|
dotnet build path/to/consumer.slnx -c Release --no-restore -warnaserror
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
Transitive pinning stays as the consumer sets it; `--pin-transitive` forces it on so stamped IDs the
|
|
32
|
+
consumer does not pin still take effect (it can raise NU1109 floors).
|
|
33
|
+
|
|
31
34
|
The printed properties are `DirectoryPackagesPropsPath` and
|
|
32
35
|
`RestoreAdditionalProjectSources`. For an IDE, launch it from that shell, or pass
|
|
33
36
|
the printed `-p:` properties explicitly to its build. Verify restore uses the
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
## Pack the package, not a link
|
|
7
7
|
|
|
8
|
-
Requires node,
|
|
9
|
-
not run
|
|
8
|
+
Requires node, npm and Yarn 4. Run the producer's normal build first. The helper does
|
|
9
|
+
not build or run lifecycle scripts: it packs with `npm pack --ignore-scripts`, the publish
|
|
10
|
+
workflows' tool, so the tarball matches the published one.
|
|
10
11
|
|
|
11
12
|
```bash
|
|
12
13
|
stamp=$(bash "$local" stamp "$producer")
|
|
@@ -15,8 +16,9 @@ bash "$local" pack-npm "$producer" "$stamp"
|
|
|
15
16
|
|
|
16
17
|
It requires clean tracked workspace manifests, temporarily changes all non-private
|
|
17
18
|
workspace versions and packs actual tarballs. It restores those manifests on
|
|
18
|
-
success and failure
|
|
19
|
-
|
|
19
|
+
success and failure. Tarballs are named `<scope>-<name>-<version>.tgz` (`@cratis/foo` becomes
|
|
20
|
+
`cratis-foo-<version>.tgz`). `verify-assets --npm [--dir REL]` reads installed copies under
|
|
21
|
+
`node_modules`, or under `REL` when the consumer's Yarn project is a subfolder.
|
|
20
22
|
Inspect dependency ranges in packed manifests: the helper changes versions, not
|
|
21
23
|
arbitrary inter-package dependency constraints. Include every required workspace
|
|
22
24
|
and verify the consumer resolves the intended tarballs.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!-- Copyright (c) Cratis. All rights reserved. -->
|
|
2
|
+
<!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
|
|
3
|
+
|
|
4
|
+
# Train mode
|
|
5
|
+
|
|
6
|
+
A local release train combines several branches into one pull request, one CI run and one
|
|
7
|
+
release. Verify a downstream repository against the upstream train's packages, not against
|
|
8
|
+
per-branch packages.
|
|
9
|
+
|
|
10
|
+
## Stamp
|
|
11
|
+
|
|
12
|
+
`stamp --train PRODUCER...` prints `X.Y.Z-train.g<sha7>`. X.Y.Z is the highest next patch over the
|
|
13
|
+
producers' latest reachable tags, so no stamped package sorts below a released pin. sha7 is the first
|
|
14
|
+
seven hex characters of the SHA-1 of the sorted `<repository> <full HEAD sha>` lines of every
|
|
15
|
+
producer (the repository is its origin's last path part, else its folder name). There is no minute: one
|
|
16
|
+
combined code state yields one stamp, a change in any producer changes it, and the order of the
|
|
17
|
+
producers does not matter. The grammar is fixed so tools can rely on it. Every command that takes a
|
|
18
|
+
stamp accepts it: `pack-*`, `use-*`, `verify-*`, `check` and `clean --stamp`. A train stamp
|
|
19
|
+
sorts above the released pin like a local stamp does.
|
|
20
|
+
|
|
21
|
+
## Rules
|
|
22
|
+
|
|
23
|
+
- One stamp per train state. The train tool records it in the train manifest; read it from there
|
|
24
|
+
instead of recomputing it for a different HEAD.
|
|
25
|
+
- A fold into the upstream train changes the upstream HEAD and so the stamp. Downstream
|
|
26
|
+
verification against the old stamp is stale: pack again, overlay the new stamp, rebuild and
|
|
27
|
+
re-verify. Do not carry a pass over a fold.
|
|
28
|
+
- Train versions are as local as local stamps. `check` reports `-train.g<sha7>` in changed
|
|
29
|
+
tracked, staged and untracked files, so a train version never reaches a commit or pull request.
|
|
30
|
+
- Upstream first. The downstream ships only after the upstream has released and the downstream pin
|
|
31
|
+
is bumped to the released version; run `check` (0 leaks) before the downstream push.
|
|
32
|
+
|
|
33
|
+
## Recording consumed versions
|
|
34
|
+
|
|
35
|
+
`verify-pack STAMP --consumer C --producer P... --json` and
|
|
36
|
+
`verify-assets CONSUMER STAMP [--npm] [--consumer C --producer P...] --json` print one line:
|
|
37
|
+
`{"stamp":"...","packages":[{"id":"...","version":"..."}]}`. Findings go to stderr and the exit status
|
|
38
|
+
is unchanged, so a train tool can record the consumed versions without scraping text.
|
|
39
|
+
|
|
40
|
+
## Clean
|
|
41
|
+
|
|
42
|
+
`clean --stamp STAMP` lists and removes the feed files, cache folders and Yarn cache entries of
|
|
43
|
+
that one train stamp; a local stamp is not matched. Review the dry run before `--apply`.
|