Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ jobs:
- name: Verify specialist references without model downloads
run: |
pip install pytest safetensors pyte onnx==1.23.2 onnxruntime==1.31.0 onnxscript==0.7.2 'transformers>=4.51,<5' 'timestral @ git+https://github.com/mistralai/timestral.git@a534bdbd244d64f3d9d743f46c7bf6fc330ed4ff'
python -m pytest tests/test_specialist_labs.py tests/test_specialist_workflow.py tests/test_specialist_terminal_portability.py tests/test_specialist_native_export.py tests/test_specialist_forecast_portability.py tests/test_specialist_decision_portability.py -q
python -m pytest tests/test_specialist_labs.py tests/test_specialist_workflow.py tests/test_specialist_terminal_portability.py tests/test_specialist_native_export.py tests/test_specialist_native_scoring.py tests/test_specialist_forecast_portability.py tests/test_specialist_decision_portability.py -q

- name: Upload coverage
uses: actions/upload-artifact@v7.0.1
Expand Down
35 changes: 35 additions & 0 deletions docs/factory/specialist-labs.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,41 @@ not numerical or calibration parity. Refit temperature and rerun primary and
regression gates before using native confidence. No model is promoted by this
conversion.

For a source-bound native score run, re-export the adapter with training
identities retained, then use the existing local executable and model files:

```sh
uv run --project scripts/specialists python scripts/specialists/decision.py score-native --binary <existing-native-cli> --base <local-base> --adapter <new-adapter.lora> --source-revision <declared-full-commit> --data <frozen-split.jsonl> --seconds 60 --out <new-native-scores.jsonl>
```

The scorer observes the executable, base/tokenizer, adapter and sidecar hashes,
and binds tokenizer versions and the exact prompt implementation into a distinct
native model identity. The source revision is operator-declared, not verified
build provenance. Every exact-token response must match its request. Changed
files, unknown adapter training provenance, malformed output and expired budgets
fail closed. Native adapter sidecars exported before training identities were
preserved require re-export from their original source adapter.
Merged bases retain their own training exclusions too. Standalone tokenizer
files and the resolved chat template are part of this identity. Checksums run at
the start and before final output; per-row file fingerprints detect intervening
changes. Every row cold-loads the native model in a separate bounded process.
The total time budget includes those loads; failure produces no final score
file. Use a small split and retain failed-run diagnostics separately.
All rows are checked against the native 256-token cap before any model process.
Metal libraries, bundles and dynamic libraries beside the executable or in its
packaged app's `Contents/Resources` and `Contents/Frameworks` are hashed too.
Missing Metal libraries block scoring. This does not attest external system
libraries or drivers. Exports with unknown
training provenance remain loadable diagnostic artifacts; re-exporting such a
source cannot reconstruct its training identities or qualify confidence.

Use `decision.py calibrate` on native calibration scores only, with train,
primary and regression scores supplied through `--exclude`; score those splits
separately. Existing calibration and eval leakage checks remain authoritative.
MLX and browser calibration cannot qualify this different runtime identity.
The subprocess fixtures test the scoring boundary; actual native runs and a
frontier-qualified frozen primary/regression comparison remain separate proof.

## Portable Qwen decisions

Use a local tied-embedding Qwen3 MLX affine q4/group64 base and, optionally,
Expand Down
32 changes: 30 additions & 2 deletions scripts/specialists/decision.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,14 @@ def argument_parser():
child.add_argument("--base", required=True)
child.add_argument("--adapter", required=True)
child.add_argument("--out", required=True)
child = sub.add_parser("score-native")
child.add_argument("--binary", required=True)
child.add_argument("--base", required=True)
child.add_argument("--source-revision", required=True)
child.add_argument("--adapter")
child.add_argument("--data", required=True)
child.add_argument("--out", required=True)
child.add_argument("--seconds", type=float, default=60)
child = sub.add_parser("export-browser")
child.add_argument("--base", required=True)
child.add_argument("--adapter")
Expand Down Expand Up @@ -572,12 +580,32 @@ def run_model(args):
print(json.dumps(budget.measurements()))


def main():
args = argument_parser().parse_args()
def run_native(args):
if args.command == "export-native":
from decision_native import export

print(json.dumps(export(args.base, args.adapter, args.out)))
return
from decision_native_score import NativeDecisionModel

budget = Budget(args.seconds)
rows = read_rows(args.data)
if Path(args.out).exists():
raise FileExistsError(args.out)
model = NativeDecisionModel(
args.binary, args.base, args.source_revision, args.adapter, budget
)
model.preflight(rows)
scores = [model.score(row) for row in rows]
model.finish()
write_rows(args.out, scores)
print(json.dumps(budget.measurements()))


def main():
args = argument_parser().parse_args()
if args.command in ("score-native", "export-native"):
run_native(args)
elif args.command == "export-browser":
from decision_browser import export

Expand Down
1 change: 1 addition & 0 deletions scripts/specialists/decision_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ def export(base, adapter, out):
"base_identity": metadata["base_identity"],
"source_adapter_sha256": metadata["adapter_sha256"],
"native_adapter_sha256": file_hash(out),
"train_identities": metadata.get("train_identities"),
"entries": len(entries),
"quality": "unqualified; recalibrate after runtime conversion",
}
Expand Down
Loading
Loading