Repository navigation
Expand file tree
/
Copy pathjustfile
More file actions
451 lines (411 loc) · 19 KB
/
Copy pathjustfile
File metadata and controls
451 lines (411 loc) · 19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
# algocline — development task runner
# Usage: just <recipe>
_default:
@just --list -u
# ─── Check ──────────────────────────────────────────────────────
# Run all checks (fmt, clippy, test, V0 invariants) — CI equivalent
[group('allow-agent')]
ci: fmt-check lua-fmt-check clippy clippy-nn test test-nn check-invariants check-agent-index
# Lint with clippy (warnings = errors)
[group('allow-agent')]
clippy:
cargo clippy --workspace --all-targets -- -D warnings
# Check formatting (Rust)
[group('allow-agent')]
fmt-check:
cargo fmt --all -- --check
# Check formatting (Lua, stylua)
[group('allow-agent')]
lua-fmt-check:
stylua --check .
# ─── Build ──────────────────────────────────────────────────────
# Type-check without codegen
[group('allow-agent')]
check:
cargo check --workspace --all-targets
# Build release binary
[group('allow-agent')]
build:
cargo build --release
# Install locally (for MCP server reload)
install:
cargo install --path .
# ─── Test ───────────────────────────────────────────────────────
# Run all tests
[group('allow-agent')]
test:
cargo test --workspace
# Run each workspace member's tests, one crate at a time. Same tests as
# `test`, but only one crate's test binaries are linked at a time — for a
# host where a workspace-wide link runs out of memory. Every crate runs
# even after a failure, and the recipe fails if any of them did.
[group('allow-agent')]
test-each:
#!/usr/bin/env bash
set -euo pipefail
members=$(cargo metadata --no-deps --format-version 1 \
| jq -r '.workspace_members[] as $id | .packages[] | select(.id == $id) | .name')
failed=()
for p in $members; do
echo "== cargo test -p $p"
cargo test -p "$p" || failed+=("$p")
done
if [ ${#failed[@]} -gt 0 ]; then
echo "test-each: failed in: ${failed[*]}" >&2
exit 1
fi
# Run tests matching a pattern
[group('allow-agent')]
filter PATTERN:
cargo test --workspace -- {{PATTERN}}
# Run e2e tests only
[group('allow-agent')]
e2e:
cargo test --test e2e
# Run nn-feature tests (candle wrapper unit tests + feature-gated engine bridge
# smoke + feature-gated engine lib unit tests). `just test` does NOT cover the
# nn-feature layer because it lives behind `--features nn`; this recipe closes
# that gap so nn work can be verified with one command.
#
# The `--lib` line was added for L5b-S1 (`nn_wrap.rs` bridge tests). Inline
# `#[cfg(test)] mod ..._bridge_tests { ... }` modules under `--features nn`
# would otherwise be silently skipped by both `just test` and the previous
# `--test nn_bridge_smoke`-only invocation.
[group('allow-agent')]
test-nn:
cargo test -p algocline-nn
cargo test -p algocline-engine --features nn --lib
cargo test -p algocline-engine --features nn --test nn_bridge_smoke
cargo test -p algocline-engine --features nn --test nn_card_test
cargo test -p algocline-engine --features nn --test nn_distill_teacher_card_e2e
cargo test -p algocline-engine --features nn --test nn_gate_smoke
cargo test -p algocline-engine --features nn --test nn_ckpt_hook_e2e
cargo test --features nn --test e2e nn_channel
# Clippy for the nn-feature layer. `just clippy` runs the default (nn off)
# workspace clippy; this recipe closes the gap for the `--features nn` code
# paths (`crates/algocline-engine/src/bridge/nn_card.rs` +
# `crates/algocline-engine/src/bridge/nn_wrap.rs` + `algocline-nn` crate lib).
[group('allow-agent')]
clippy-nn:
cargo clippy -p algocline-nn --all-targets -- -D warnings
cargo clippy -p algocline-engine --features nn --all-targets -- -D warnings
# Install locally with the `nn` feature enabled (alc.nn Lua surface + candle).
# Same overwrite target as `just install` (~/.cargo/bin/alc); the default
# `just install` builds without nn to keep the MCP binary light.
[group('allow-agent')]
install-nn:
cargo install --path . --features nn
# Review insta snapshots (interactive)
snapshots:
cargo insta review
# ─── Format ─────────────────────────────────────────────────────
# Auto-format all code (Rust)
[group('allow-agent')]
fmt:
cargo fmt --all
# Auto-format Lua code (stylua)
[group('allow-agent')]
lua-fmt:
stylua .
# ─── Quality ────────────────────────────────────────────────────
# Full pre-commit check: format, lint, test
ready:
just fmt
just clippy
just test
# ─── Invariants ─────────────────────────────────────────────────
# Check V0 AppDir-guard invariants:
# Inv-1: Service layer (algocline-app) no longer reads HOME / ALC_HOME
# directly — `AppConfig::resolve_app_dir` / `resolve_log_dir` in
# `service/config.rs` are the single whitelisted resolvers.
# Inv-2: Execution layer (algocline-engine, incl. `prelude.lua`) no
# longer reads HOME / ALC_HOME directly.
# Inv-3: `algocline_core::AppDir` / `AppConfig` are not referenced from
# inside the engine crate (engine public API stays free of the
# service-layer abstractions).
# Inv-4: InstalledManifestStore encapsulation — no `installed.json` filesystem
# calls live outside `service/manifest.rs` (the `FsInstalledManifestStore`
# impl block is the single source). Added in Subtask 3b together
# with the `InstalledManifestStore` trait extraction (Subtask 3a).
# Inv-5: `crates/algocline-nn` names no specific game. The crate holds
# general machine-learning machinery; a run's subject belongs to the
# caller. Comments and doc comments count — a doc example naming one
# game is how the next reader learns the crate is "for" it.
[group('allow-agent')]
check-invariants:
#!/usr/bin/env bash
set -euo pipefail
fail=0
# Inv-1: Service layer must route every HOME access through AppConfig.
# Whitelist:
# `config.rs` — single source for AppConfig::resolve_app_dir / resolve_log_dir.
# `test_support.rs` — `FakeHome` test fixture (軸 A defer; guards HOME for
# integration tests while parallel isolation is not yet in place).
if grep -rn -E 'dirs::home_dir\(\)|std::env::var(_os)?\("(HOME|ALC_HOME)"\)' \
crates/algocline-app/src/service/ --include='*.rs' \
| grep -v -E '^crates/algocline-app/src/service/(config|test_support)\.rs:'; then
echo "Inv-1 FAILED: HOME/ALC_HOME read outside service/config.rs (or FakeHome)" >&2
fail=1
fi
# Inv-2 (Rust): Execution layer (engine crate) must not read HOME.
if grep -rn -E 'dirs::home_dir\(\)|std::env::var(_os)?\("(HOME|ALC_HOME)"\)' \
crates/algocline-engine/src/ --include='*.rs'; then
echo "Inv-2 (Rust) FAILED: HOME/ALC_HOME read in engine crate" >&2
fail=1
fi
# Inv-2 (Lua): prod Lua (prelude.lua) must not call os.getenv("HOME"|"ALC_HOME").
if grep -n -E 'os\.getenv\("(HOME|ALC_HOME)"\)' \
crates/algocline-engine/src/prelude.lua; then
echo "Inv-2 (Lua) FAILED: HOME/ALC_HOME read in prod Lua" >&2
fail=1
fi
# Inv-3: engine crate must not import AppDir/AppConfig from core.
#
# Rustdoc intra-doc links inside doc comments (`///` / `//!`) reference
# `algocline_core::AppDir::nn_dir` etc. as documentation about *what the
# service layer resolves for us* — engine keeps the direct dependency in
# Cargo.toml purely for these links. Those are not runtime references,
# so we strip doc-comment lines before deciding failure. Ordinary `//`
# code comments are left in the grep window on purpose so that "sneaking
# a use behind a `// comment`" still gets caught.
if grep -rn -E 'algocline_core::(AppDir|AppConfig)' \
crates/algocline-engine/src/ --include='*.rs' \
| grep -v -E '^[^:]+:[0-9]+:[[:space:]]*(///|//!)'; then
echo "Inv-3 FAILED: engine references algocline_core::AppDir/AppConfig" >&2
fail=1
fi
# Inv-4: InstalledManifestStore encapsulation — every `std::fs::*` call that
# touches `installed.json` / `installed.json.lock` / `installed.json.tmp`
# must sit inside `service/manifest.rs` (the `FsInstalledManifestStore` impl
# block). Call sites in sibling service files may still read the
# *path* via `app_dir.installed_json()` for diagnostics — that is not
# filesystem IO and does not surface here.
#
# Limitation: this grep is literal-only. An indirection pattern like
# let p = app_dir.installed_json();
# std::fs::write(p, ...)
# would evade it because the `installed.json` literal is no longer
# on the `std::fs::*` line. The real guard is the `FsInstalledManifestStore`
# impl boundary itself (the trait confines IO); this grep is a
# belt-and-braces sanity check. The follow-up that plumbs
# `Arc<dyn InstalledManifestStore>` through `AppService` (alongside the
# sibling `HubRepo` / `EvalRepo` extractions) will let us delete
# this grep once the trait boundary is fully exercised.
if grep -rn -E 'std::fs::[A-Za-z_]+[^;]*installed\.json' \
crates/algocline-app/src/service/ --include='*.rs' \
| grep -v -E '^crates/algocline-app/src/service/manifest\.rs:'; then
echo "Inv-4 FAILED: installed.json filesystem access outside service/manifest.rs" >&2
fail=1
fi
# Inv-5: no specific game is named anywhere under crates/algocline-nn.
#
# The list is of *named games*, not of the word "game": general prose
# like "a game or tool" describes the kind of caller the crate serves
# and is exactly what this invariant wants the crate to say instead.
# Matching is case-insensitive so a type name (`ChessBoard`) is caught
# alongside a comment.
#
# Limitation: this is a word list, so a game not on it passes. The list
# grows when a new one shows up; the check is a tripwire on the failure
# that has actually happened (an experiment's subject leaking into the
# library it was run with), not a proof of neutrality.
if grep -rniE '(chess|othello|reversi|shogi|guardian|card_duel)' \
crates/algocline-nn/; then
echo "Inv-5 FAILED: crates/algocline-nn names a specific game (see the file:line above)" >&2
fail=1
fi
# Inv-6: tests never run alc over the developer's ~/.algocline.
# tests/common is the one place the alc binary is started: it drops every
# inherited ALC_* and roots the child at a tempdir home. A test that
# starts the binary itself would inherit the real home — and with it a
# config.toml that can switch the Card backend under the test, and a real
# store the test then writes into. Reading HOME to reach ~/.algocline
# (e.g. installing from the developer's packages) is the same leak from
# the other side. `dirs::home_dir()` stays allowed: it only redacts the
# path out of test output.
if grep -rnE 'CARGO_BIN_EXE_alc|target/debug/alc' tests/ | grep -v '^tests/common/'; then
echo "Inv-6 FAILED: a test starts the alc binary outside tests/common (use common::spawn_alc / isolated_command)" >&2
fail=1
fi
if grep -rnE 'env::var(_os)?\("HOME"\)' tests/; then
echo "Inv-6 FAILED: a test reads HOME (use a fixture under tests/fixtures instead)" >&2
fail=1
fi
if [ "$fail" -ne 0 ]; then
exit 1
fi
echo "All AppDir-guard invariants PASS"
# ─── Publish ────────────────────────────────────────────────────
# Dry-run publish check (dependency order)
#
# NOTE: only the leaves (algocline-core, algocline-nn) actually verify — intermediate crates
# (engine/app/mcp/root) fail on dry-run because they reference the new version
# of upstream crates not yet on crates.io (cargo-release issue #691). Use this
# for syntax / Cargo.toml metadata checks; real verification happens during
# `just publish`.
publish-dry:
cargo publish -p algocline-core --dry-run
cargo publish -p algocline-nn --dry-run
cargo publish -p algocline-engine --dry-run
cargo publish -p algocline-app --dry-run
cargo publish -p algocline-mcp --dry-run
cargo publish -p algocline --dry-run
# Publish to crates.io in dependency order, then tag + push.
#
# Usage: just publish 0.27.0
#
# Pre-check verifies the passed VERSION matches Cargo.toml workspace version
# to prevent typos. Each `cargo publish` is followed by a 60s sleep so the
# crates.io index propagates before the next dependent crate is uploaded
# (CLAUDE.md §crates.io公開).
#
# NOT allow-agent — irreversible upload + git push. Human-only execution.
publish VERSION:
@ACTUAL=$(grep '^version = ' Cargo.toml | head -1 | cut -d'"' -f2); \
if [ "$ACTUAL" != "{{VERSION}}" ]; then \
echo "ERROR: Cargo.toml workspace.package.version is $ACTUAL, but you passed {{VERSION}}"; \
exit 1; \
fi
cargo publish -p algocline-core
sleep 60
cargo publish -p algocline-nn
sleep 60
cargo publish -p algocline-engine
sleep 60
cargo publish -p algocline-app
sleep 60
cargo publish -p algocline-mcp
sleep 60
cargo publish
git tag v{{VERSION}}
git push origin v{{VERSION}}
# ─── Docs ───────────────────────────────────────────────────────
# Verify docs/AGENT_INDEX.md: all linked paths exist + all docs/*.md are
# referenced (except AGENT_INDEX.md itself).
[group('allow-agent')]
check-agent-index:
#!/usr/bin/env bash
set -euo pipefail
index=docs/AGENT_INDEX.md
if [ ! -f "$index" ]; then
echo "FAIL: $index not found" >&2
exit 1
fi
fail=0
# 1. Every markdown link target referenced from the index must exist.
# Match `](path)` for relative paths (skip http/https/anchor-only).
while IFS= read -r target; do
# strip optional #anchor
path="${target%%#*}"
[ -z "$path" ] && continue
case "$path" in
http://*|https://*) continue ;;
esac
resolved="docs/$path"
case "$path" in ../*) resolved="${path#../}" ;; esac
if [ ! -e "$resolved" ]; then
echo "FAIL: linked path missing: $path (resolved=$resolved)" >&2
fail=1
fi
done < <(grep -oE '\]\(([^)]+)\)' "$index" | sed -E 's/^\]\(//; s/\)$//')
# 2. Every docs/*.md must be referenced from the index (except itself).
for f in docs/*.md; do
base=$(basename "$f")
[ "$base" = "AGENT_INDEX.md" ] && continue
if ! grep -qF "$base" "$index"; then
echo "FAIL: docs/$base not referenced in $index" >&2
fail=1
fi
done
if [ "$fail" -ne 0 ]; then
exit 1
fi
echo "AGENT_INDEX.md OK"
# Generate LLM-facing docs (llms.txt / llms-full.txt) under docs/aidoc/
[group('allow-agent')]
aidoc-gen:
cargo aidoc --title algocline
# CI drift + lint gate (exit 2 on drift, --strict promotes lint warnings to errors)
[group('allow-agent')]
aidoc-check:
cargo aidoc --title algocline --check --strict
# ─── Codegen ────────────────────────────────────────────────────
# Regenerate types/alc_shapes.d.lua from embedded alc_shapes Lua sources
[group('allow-agent')]
gen-shapes:
cargo run -p algocline-app --example gen_alc_shapes_dlua
# ─── Vendor Sync ────────────────────────────────────────────────
#
# Sync vendored copies of upstream Lua packages into `crates/**/vendor/`.
# Each recipe wipes the destination dir, copies every `.lua` file from
# the source dir preserving subdir structure, and prepends a 2-line
# origin marker to each vendored file.
#
# Source dirs default to local checkouts under `$HOME/projects/`.
# Override per invocation via env vars:
# ALC_SHAPES_SRC=<abs path> just vendor-alc-shapes
# EVALFRAME_SRC=<abs path> just vendor-evalframe
#
# After a sync, run `just ci` (fmt / clippy / test / lua-fmt-check /
# check-invariants) to verify no regressions before committing.
# Sync vendored alc_shapes.
# Source override: ALC_SHAPES_SRC=<abs path>
[group('allow-agent')]
vendor-alc-shapes:
#!/usr/bin/env bash
src="${ALC_SHAPES_SRC:-$HOME/projects/algocline-bundled-packages/alc_shapes}"
just _vendor-lua-pkg alc_shapes "$src" crates/algocline-app/src/service/gendoc/alc_shapes
# Sync vendored evalframe.
# Source override: EVALFRAME_SRC=<abs path>
[group('allow-agent')]
vendor-evalframe:
#!/usr/bin/env bash
src="${EVALFRAME_SRC:-$HOME/projects/evalframe/evalframe}"
just _vendor-lua-pkg evalframe "$src" crates/algocline-engine/src/vendor/evalframe
# Sync every vendored Lua package.
[group('allow-agent')]
vendor-sync: vendor-alc-shapes vendor-evalframe
# Internal: shared vendor-sync body used by every `vendor-*` recipe.
# Not marked `[group('allow-agent')]` because it is not meant to be
# invoked directly.
_vendor-lua-pkg NAME SRC DST:
#!/usr/bin/env bash
set -euo pipefail
name="{{NAME}}"
src="{{SRC}}"
dst="{{DST}}"
if [ ! -d "$src" ]; then
echo "vendor-sync: source dir '$src' not found (set {{ uppercase(NAME) }}_SRC to override)" >&2
exit 1
fi
# Extract semver from init.lua; falls back to "unknown" when the
# file has no recognisable version field.
version="$(
if [ -f "$src/init.lua" ]; then
grep -hE "(^\s*version\s*=|M\.VERSION\s*=)" "$src/init.lua" \
| head -1 \
| sed -E "s/.*['\"]([0-9]+\.[0-9]+\.[0-9]+[^'\"]*)['\"].*/\1/"
else
echo ""
fi
)"
version="${version:-unknown}"
# Wipe the destination so files removed upstream stop shipping.
rm -rf "$dst"
mkdir -p "$dst"
count=0
while IFS= read -r rel; do
rel="${rel#./}"
subdir=$(dirname "$rel")
mkdir -p "$dst/$subdir"
# Recipe names use hyphens; package names may use underscores
# (e.g. `alc_shapes`). The origin marker points at the recipe.
recipe_name="${name//_/-}"
{
printf -- "-- vendored from %s v%s\n" "$name" "$version"
printf -- "-- DO NOT EDIT here — run \`just vendor-%s\` to resync\n" "$recipe_name"
cat "$src/$rel"
} > "$dst/$rel"
count=$((count + 1))
done < <(cd "$src" && find . -name '*.lua' -type f | sort)
echo "vendor-sync: $name v$version -> $dst ($count files)"