Skip to content

fix(orchestrator): land M3 -- switch knobs, anti-keyword case, atomic prefs, bounded audit, symlink scan#11

Merged
Ghost-Frame merged 15 commits into
chore/ci-foundationfrom
feat/land-m3
Jun 30, 2026
Merged

fix(orchestrator): land M3 -- switch knobs, anti-keyword case, atomic prefs, bounded audit, symlink scan#11
Ghost-Frame merged 15 commits into
chore/ci-foundationfrom
feat/land-m3

Conversation

@Ghost-Frame

Copy link
Copy Markdown
Owner

Summary

Five correctness fixes the M3 intelligent-selection work shipped with, each with a regression test. Stacked on #10 (CI foundation), so this PR's own checks run the new CI.

Fixes

  • policy.rs -- anti-keyword matching is now case-insensitive. Task tokens are lowercased during tokenization, but anti_keywords were compared with a case-sensitive contains(), so an uppercase manifest entry never matched and the penalty silently never fired.
  • feedback.rs -- preference save is atomic (temp file in the same dir + rename), so a crash or concurrent writer cannot leave a half-written JSON file that load() would then fail to parse.
  • audit.rs -- load streams the JSONL line-by-line via BufReader and retains only the most recent MAX_AUDIT_ENTRIES (50k) in a VecDeque, instead of reading the whole file into memory.
  • context.rs -- the project scan uses DirEntry::file_type() (which does not follow symlinks) and skips symlinks, so a planted symlink cannot redirect the walk outside the project tree.
  • controller.rs -- SwitchPolicy::min_confidence and switch_margin were advertised but ignored. from_sensitivity now maps them (0.0 at the 0.5 midpoint and above, rising to a 0.3 confidence floor / 0.15 margin at the conservative end), and decide() applies them: a candidate below min_confidence never activates, and a challenger that does not beat the current active by switch_margin holds. Default/high-sensitivity behavior is unchanged.

Verification (local)

  • cargo clippy -p frameshift-orchestrator --all-targets -- -D warnings: clean
  • cargo test -p frameshift-orchestrator: 71 passed, 0 failed (6 new tests; no existing test broken)

…d audit, symlink-safe scan

Four correctness fixes the M3 selection work shipped with:

- policy.rs: anti-keyword matching is now case-insensitive. Task tokens are
  lowercased during tokenization, but anti_keywords were compared with a
  case-sensitive contains(), so an uppercase manifest entry (e.g. "CSS")
  never matched and the penalty silently never fired.
- feedback.rs: preference save is now atomic (temp file in the same dir +
  rename) so a crash or concurrent writer cannot leave a half-written JSON
  file that load() would then fail to parse.
- audit.rs: load streams the JSONL line-by-line via BufReader and keeps only
  the most recent MAX_AUDIT_ENTRIES (50k) in a VecDeque, instead of reading
  the entire file into memory -- a long-lived log no longer exhausts memory.
- context.rs: the project scan uses DirEntry::file_type() (which does not
  follow symlinks) and skips symlinks, so a planted symlink cannot redirect
  the walk outside the project tree.

Each fix has a regression test. clippy -D warnings clean; 69 orchestrator
tests pass.
…switch_margin

SwitchPolicy exposed min_confidence and switch_margin but decide() never read
them -- they were hardcoded to 0.0 in from_sensitivity and ignored in the
decision logic, so the documented knobs did nothing.

Now from_sensitivity maps them from sensitivity (0.0 at the 0.5 midpoint and
above, rising to a 0.3 confidence floor / 0.15 margin at the conservative end),
and decide() applies them: a candidate below min_confidence never activates,
and a challenger that does not beat the current active by switch_margin holds.
Default and high-sensitivity behavior is unchanged because both floors are 0.0
there; the gates only bite at low sensitivity.

Adds two regression tests (low-confidence activation blocked, marginal
challenger held). clippy -D warnings clean; 71 orchestrator tests pass.
Two more spots where local Arch rustfmt and CI upstream rustfmt diverge at the
formatting boundary: audit.rs imported VecDeque so the declaration fits one
line for both builds; the feedback.rs temp-file assertion uses a single-arg
assert + comment (upstream explodes a 2-arg assert with a message that Arch
keeps inline). Behavior unchanged; 71 orchestrator tests still pass.
These hold raw multi-model audit findings with local filesystem paths,
unpublished vulnerability locations, and internal tool identifiers that
do not belong in a public repo.
Clears all 8 cargo-audit vulnerabilities that were gating the Security
audit job, and ratchets that job from continue-on-error to blocking.

- diesel 2.2 -> 2.3.10, diesel-async 0.5 -> 0.9.2, diesel_migrations
  2.2 -> 2.3, bb8 0.8 -> 0.9 (diesel-async 0.9 implements
  bb8::ManageConnection only for bb8 0.9). Clears RUSTSEC-2026-0136 and
  -0137 plus the diesel/diesel-async unsound advisories.
- cargo update pulls postgres-protocol 0.6.12 (RUSTSEC-2026-0179/-0180),
  tokio-postgres 0.7.18 (RUSTSEC-2026-0178), quinn-proto 0.11.15
  (RUSTSEC-2026-0185).
- frameshift-server: prometheus default-features=false drops protobuf
  2.28 (RUSTSEC-2024-0437); the server only emits the text format.
- dev: testcontainers 0.23 -> 0.27, testcontainers-modules 0.11 -> 0.15;
  drops unmaintained tokio-tar (RUSTSEC-2025-0111, no upstream fix) for
  the maintained astral-tokio-tar fork, and rustls-pemfile.
- ci: audit job now blocking, ignoring only RUSTSEC-2024-0370
  (proc-macro-error, build-only via age, removable only by the age 0.11
  beta that backs vault crypto).

diesel-async 0.9 migration: transaction() now takes an AsyncFnOnce, so
register_pack_version uses an `async move |conn|` closure instead of the
removed `|conn| Box::pin(async move { .. })` form; ManagerConfig is now
generic, so pool.rs names AsyncPgConnection explicitly. rust-version
1.75 -> 1.86 to match the new floor (diesel 2.3 + async closures).

clippy -D warnings clean; cargo test --workspace 616 passed, 0 failed.
`git push --mirror` deletes refs absent from the local checkout. Because
actions/checkout only fetches the triggering branch, the mirror push
tried to delete every other ref on the target -- and the remote rejects
deleting its default branch `main`, so the job failed on every push.

Fetch all source branches into remote-tracking refs, then force-push
branches and tags with explicit refspecs (additive, no deletion).
Context sensing previously ranked personas against a static whole-repo
file-extension census, which a file-count-dominant language can swamp.
sense() now also consults the active git working set (staged, unstaged,
and untracked changes via `git status --porcelain`) and boosts those
languages above the census, so selection favors what is being edited
right now: a polyglot repo where you are editing Rust no longer ranks a
JavaScript-heavy persona first.

Best-effort and dependency-free. A non-git directory or a missing git
binary yields no boost and prior behavior is unchanged. The boost weight
(1.5) sits between the census ceiling (1.0) and the prose sentinel (2.0),
so it never overrides an explicit prose-writing signal.

Dependency-manifest parsing (dep names to task tokens) is the next
increment; it needs toml/serde_json on the currently serde-only crate.

cargo test -p frameshift-orchestrator: 77 passed, 0 failed (6 new);
clippy -D warnings clean.
Local rustfmt wraps the success arm into a block; CI rustfmt collapses it
onto one line. A two-statement block with a named stdout binding is stable
across both rustfmt versions and reads clearer.
Context sensing now parses root manifests (Cargo.toml, package.json) for
dependency names and exposes them as ContextSignal.context_tokens. The
policy scores them as a small capped additive bonus when they match a
persona's keywords, so framework-level signals (axum vs actix, react vs
vue) sharpen selection. Dependency-free line-based parsing keeps the
orchestrator crate serde-only.

The bonus is deliberately kept out of the lexical IDF channel: folding
dependency names into task_tokens would inflate the IDF denominator with
high-weight non-matching deps and dilute genuine task-token matches. As
an independent additive term (cap 0.12) it can only raise a persona that
matches project dependencies, never redistribute the primary weights.

cargo test -p frameshift-orchestrator: 84 passed, 0 failed (7 new);
clippy -D warnings clean.
ContextSignal.frameworks (cargo/npm/go/python, detected from marker files)
was populated by context sensing but never read by the scorer. Route it
through the existing capped context-signal bonus alongside dependency
tokens, so a persona whose keywords name a project's build framework gets
the same small boost. No new fields, no weight change.

cargo test -p frameshift-orchestrator: 85 passed, 0 failed (1 new);
clippy -D warnings clean.
`frameshift use <name>` activated a persona but never recorded a
preference, so the orchestrator's bias machinery never learned from manual
choices. After a successful activation, record the persona into the shared
automate-prefs.json (the same store `select`, the daemon, and the `prefs`
command read), nudging future automatic selection toward personas the user
actually picks.

Best-effort: a preferences load/save failure warns but does not fail the
command, since activation has already succeeded.

cargo test -p frameshift-cli use_persona: 2 passed; clippy and fmt clean.
The catalog `total_downloads` counter shown on the marketplace was never
incremented by any route: the download handler only recorded a `pack_downloads`
event (which feeds the 7-day Trending ranking), while the counter that backs
`total_downloads` is bumped exclusively by `increment_download_counter`, which
no handler called. As a result every pack reported 0 total downloads.

Wire `increment_download_counter` into `download_pack_bytes` alongside the
existing `record_download` call, as a best-effort step that warns and continues
on failure so a download the client already received is never failed. Document
why the signed `/dl/{hash}` path cannot increment the counter (it has only the
content hash, not name/version; the official client uses the direct route).

Add an integration test asserting a successful download increments the counter.
…ing scaffold

Two sharpen-arc slices for automate-mode persona selection.

A1 -- daemon learns from manual overrides. evaluate_and_apply now compares the
SwitchController's tracked auto-pick against the on-disk active marker before
deciding. When they diverge (the user switched by hand), it rewards the chosen
persona and decays the rejected auto-pick via Preferences::record_override,
persists the shared automate-prefs.json, and re-baselines the controller through
the new SwitchController::adopt_active so the same override is not re-learned on
every tick. A new SwitchController::active_persona exposes the tracked pick.

A2 phase 1 -- dependency-free semantic scoring scaffold. A new embed module
defines the Embedder trait plus cosine_similarity/semantic_similarity. The scorer
gains a semantic ScoreComponents channel: rank_with_embedder folds a cosine
bonus (task text vs persona text, scaled by SEMANTIC_WEIGHT) additively into the
blend, and select_with_embedder threads an optional embedder through selection.
rank/select keep their signatures and delegate with no embedder, so the semantic
channel is 0.0 by default and there is zero behavior change. A real embedding
engine and model distribution are phase 2, deferred pending sign-off.

clippy -D warnings clean; 92 orchestrator + 13 daemon tests pass, including a
manual-override regression test and semantic-channel tests with a mock embedder.
… call

The CI Format job (cargo fmt --all -- --check) was failing on feat/land-m3:
b39816d landed the download-counter integration test with a method-chain
state.packs.insert that rustfmt rewrites to multi-line call args, and aa90d8d
added a manually-wrapped rank_with_embedder call that fits on one line. Apply
the canonical rustfmt forms for both. No behavior change.
…cally

CI was catching formatting and security-advisory failures that should never
have been pushed. scripts/preflight.sh mirrors the CI gates (fmt, clippy, test,
cargo audit with the same RUSTSEC-2024-0370 ignore); .githooks/pre-push runs the
cheap no-compile subset (fmt + audit) on every push so the two failure modes
that have actually slipped through are blocked before they reach CI.

Enable per clone with: git config core.hooksPath .githooks
Run the full local mirror before a PR with: scripts/preflight.sh
@Ghost-Frame
Ghost-Frame merged commit 67ece82 into chore/ci-foundation Jun 30, 2026
6 checks passed
@Ghost-Frame
Ghost-Frame deleted the feat/land-m3 branch July 12, 2026 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant