fux-engine 0.3.2__tar.gz → 0.4.0__tar.gz
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.
- {fux_engine-0.3.2 → fux_engine-0.4.0}/PKG-INFO +61 -10
- {fux_engine-0.3.2 → fux_engine-0.4.0}/README.md +58 -9
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/__init__.py +1 -1
- fux_engine-0.4.0/fux/baseline.py +35 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/check.py +12 -3
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/cli.py +18 -2
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/clicmds.py +11 -6
- fux_engine-0.4.0/fux/cliconstitution.py +61 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/cliquery.py +1 -1
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/config.py +5 -0
- fux_engine-0.4.0/fux/constitution.py +100 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/costledger.py +20 -9
- fux_engine-0.4.0/fux/critic.py +45 -0
- fux_engine-0.4.0/fux/criticllm.py +33 -0
- fux_engine-0.4.0/fux/criticloop.py +77 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/schema.json +14 -0
- fux_engine-0.4.0/fux/data/skills/critic/SKILL.md +55 -0
- fux_engine-0.4.0/fux/data/skills/debate/SKILL.md +68 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/skills/fux/SKILL.md +10 -2
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/skills/savings/SKILL.md +10 -5
- fux_engine-0.4.0/fux/findings.py +47 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/fmwrite.py +3 -3
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/gate.py +23 -6
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/gitutil.py +5 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/mcpserver.py +1 -1
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/savings.py +36 -14
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux_engine.egg-info/PKG-INFO +61 -10
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux_engine.egg-info/SOURCES.txt +13 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux_engine.egg-info/requires.txt +3 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/pyproject.toml +4 -0
- fux_engine-0.4.0/tests/test_constitution_integrity.py +75 -0
- fux_engine-0.4.0/tests/test_constitution_tier.py +72 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_costledger.py +13 -0
- fux_engine-0.4.0/tests/test_critic_loop.py +65 -0
- fux_engine-0.4.0/tests/test_critic_split.py +57 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_lint_stats_gate.py +3 -1
- fux_engine-0.4.0/tests/test_no_llm_imports.py +58 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_savings.py +26 -0
- fux_engine-0.3.2/fux/findings.py +0 -26
- {fux_engine-0.3.2 → fux_engine-0.4.0}/LICENSE +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/__main__.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/assets/fux-icon.svg +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/assets/fux-lockup.svg +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/assets/fux-mark.svg +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/assets/graph_boot.js +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/assets/graph_template.html +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/astextract.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/bench.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/build.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/capture.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/cligraph.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/cliutil.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/community.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/components.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/context.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/coverage.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/copilot/prompts/fux-plan.prompt.md +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/copilot/prompts/fux.prompt.md +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/global/README.md +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/global/rules/async-everywhere.md +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/global/rules/doc-per-code-change.md +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/global/rules/files-max-100-lines.md +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/global/rules/no-secrets-in-vcs.md +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/hooks/_common.sh +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/hooks/post_tool_use.sh +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/hooks/session_start.sh +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/hooks/stop.sh +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/hooks/user_prompt_submit.sh +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/packs/indian-markets-tax/pack.toml +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/packs/indian-markets-tax/rules/capital-gains-equity.md +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/packs/indian-markets-tax/rules/market-hours-nse.md +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/skills/adr/SKILL.md +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/skills/distill/SKILL.md +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/skills/fetch-rules/SKILL.md +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/skills/plan/SKILL.md +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/data/skills/trace/SKILL.md +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/drift.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/embed.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/explain.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/feedback.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/fetchrules.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/fix.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/frontmatter.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/globs.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/governance.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/graph.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/graphhtml.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/graphquery.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/hookinstall.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/hookio.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/hooks.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/hybrid.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/impact.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/importer.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/index.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/initcmd.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/lint.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/loader.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/mine.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/model.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/narrative.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/pack.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/parity.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/paths.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/recall.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/report.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/scaffold.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/scalars.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/schema.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/seal.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/serve.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/settings.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/stats.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/templates/formula.md +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/templates/spec.md +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/touch.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/tour.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/uispec.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/usage.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/verify.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux/vexamples.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux_engine.egg-info/dependency_links.txt +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux_engine.egg-info/entry_points.txt +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/fux_engine.egg-info/top_level.txt +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/setup.cfg +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_ast_backend.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_astextract.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_bm25f_expand.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_capture_governance.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_centrality.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_check_fix.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_components.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_crossfile_calls.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_edge_confidence.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_embed_rerank.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_examples.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_feedback.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_fetch_rules.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_frontmatter.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_fuzz_mine.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_globs.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_graph_determinism.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_graphhtml.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_graphhtml_links.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_hookinstall.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_hybrid.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_impact.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_mcp.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_mcp_extra.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_pack.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_parity_import.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_recall_build_verify.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_recall_eval.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_resolution.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_schema_scaffold_init.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_seal.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_serve_sanitize.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_uispec.py +0 -0
- {fux_engine-0.3.2 → fux_engine-0.4.0}/tests/test_verify_hardening.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fux-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Fux — a portable agent-aware knowledge engine: rules, memory, narrative, and graph in one frontmatter substrate.
|
|
5
5
|
Author-email: arpit arya <arpitarya.me@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -31,6 +31,8 @@ Requires-Dist: tree-sitter>=0.23; extra == "ast"
|
|
|
31
31
|
Requires-Dist: tree-sitter-language-pack>=0.7; extra == "ast"
|
|
32
32
|
Provides-Extra: pdf
|
|
33
33
|
Requires-Dist: pypdf>=4.0; extra == "pdf"
|
|
34
|
+
Provides-Extra: critic
|
|
35
|
+
Requires-Dist: anthropic>=0.40; extra == "critic"
|
|
34
36
|
Provides-Extra: dev
|
|
35
37
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
36
38
|
Requires-Dist: build>=1.0; extra == "dev"
|
|
@@ -41,15 +43,34 @@ Dynamic: license-file
|
|
|
41
43
|
|
|
42
44
|
# Fux
|
|
43
45
|
|
|
44
|
-
>
|
|
45
|
-
>
|
|
46
|
-
>
|
|
47
|
-
>
|
|
46
|
+
> **Fux records *why* your code is the way it is — and checks it's still true.**
|
|
47
|
+
> A portable, agent-aware knowledge engine: one frontmatter substrate → derived
|
|
48
|
+
> **index**, **graph**, and **memory** views. `$0` and deterministic — zero
|
|
49
|
+
> third-party deps, no mandatory LLM calls. Author each rule once; your agent
|
|
50
|
+
> reads a cheap one-line index first and opens the full rule only when it's
|
|
51
|
+
> relevant — and `fux seal` lets `fux check` tell you when the governed code
|
|
52
|
+
> drifted out from under it.
|
|
53
|
+
|
|
54
|
+
> **New in v0.4.0 — the constitutional-app engine.** Govern where trust lives, opt-in and
|
|
55
|
+
> `$0`: give a rule a `tier` (`constitutional` · `standard` · `advisory`); author principles
|
|
56
|
+
> by two-agent **`/fux debate`**; **`fux ratify`** a rule into a tamper-evident constitution
|
|
57
|
+
> (sealed in **`.fux/constitution.lock`** — any later edit/add/delete is an always-blocking
|
|
58
|
+
> `tampered` finding); critique changes against the constitution with **`fux critic`** before
|
|
59
|
+
> they land (deterministic pass first, no LLM); and **`fux gate`** reports every ungoverned
|
|
60
|
+
> path (report-first, never blocks). The AI self-critique ships behind an opt-in **`[critic]`**
|
|
61
|
+
> extra — the default install stays model-free. Adopting it changes nothing until you opt in.
|
|
62
|
+
|
|
63
|
+
<!-- launch: replace the line below with the demo GIF — `fux why day-pnl` → rule + why +
|
|
64
|
+
governed code, then the Solar Terminal graph igniting the `governs` links.
|
|
65
|
+
Storyboard + capture script: docs/launch/gif-storyboard.md -->
|
|
66
|
+
<p align="center"><em>▶ demo GIF goes here — see <a href="docs/launch/gif-storyboard.md">docs/launch/gif-storyboard.md</a></em></p>
|
|
67
|
+
|
|
68
|
+
**Pronounced "fox."** (Say it like the animal — *fux* → *fox*.)
|
|
48
69
|
|
|
49
70
|
Named after *Johann Joseph Fux*, author of *Gradus ad Parnassum* (1725) — the
|
|
50
71
|
counterpoint treatise every composer learned the rules from. A tool that codifies
|
|
51
|
-
and enforces rules, named after the man who wrote *the* rulebook.
|
|
52
|
-
`wagner`, `bach`, `orff`.
|
|
72
|
+
and enforces rules, named after the man who wrote *the* rulebook. (The name is
|
|
73
|
+
deliberate.) Sits beside `wagner`, `bach`, `orff`.
|
|
53
74
|
|
|
54
75
|
Fux **unifies and replaces** three things a project usually runs separately — the
|
|
55
76
|
structural graph (graphify), cross-session memory, and the narrative docs — and
|
|
@@ -73,12 +94,14 @@ So: write the *why* down once → it's found fast, stays correct, and never gets
|
|
|
73
94
|
## Why
|
|
74
95
|
|
|
75
96
|
The *why* behind a formula — why current value not invested cost, why
|
|
76
|
-
|
|
97
|
+
dollar-normalize first, which cost-basis method — usually lives only as an inline
|
|
77
98
|
comment, invisible until someone greps for it. Fux makes that knowledge
|
|
78
99
|
**first-class**: one entry, authored once, served back through a tiny index (read
|
|
79
100
|
first) plus lazily-opened rules (read only when relevant). Lookups run ~5–10×
|
|
80
101
|
cheaper and more correct on every later session — and you don't have to take that
|
|
81
|
-
on faith: **`fux savings`** measures the multiplier from your own file sizes
|
|
102
|
+
on faith: **`fux savings`** measures the multiplier from your own file sizes and
|
|
103
|
+
prices the win in **real dollars** (configurable `usd_per_mtok`, default = Claude
|
|
104
|
+
Opus 4.8's $5/M input rate), per lookup and as a cumulative ledger.
|
|
82
105
|
|
|
83
106
|
## Install
|
|
84
107
|
|
|
@@ -102,10 +125,13 @@ fux why day-pnl [--history] # explain a rule (+ how its *why* evolved, via gi
|
|
|
102
125
|
fux refs src/aggregator.py # which rules govern this file
|
|
103
126
|
fux recall "how is day P&L computed" --hybrid # BM25F; RRF-fuse lexical+semantic+graph
|
|
104
127
|
fux seal --all # bind rules to an AST fingerprint of their code
|
|
128
|
+
fux debate "<rule>" (skill) # two-agent free debate → you ratify the result
|
|
129
|
+
fux ratify <id> --by Arpit # ratify a constitutional rule (tamper-evident; the only path)
|
|
130
|
+
fux critic "<change>" # critique a change vs principles before it lands (deterministic pass; $0)
|
|
105
131
|
fux coverage # % of important files with a governing rule
|
|
106
132
|
fux verify --fuzz # run invariant `check:`; boundary-fuzz for div-by-zero
|
|
107
133
|
fux mine # surface candidate rules latent in the code (drafts)
|
|
108
|
-
fux savings "how is day P&L computed" # measured token
|
|
134
|
+
fux savings "how is day P&L computed" # measured token + dollar cost win (+ cumulative ledger)
|
|
109
135
|
fux lint # rule *quality*: missing why / code_refs / edges
|
|
110
136
|
fux stats # knowledge-health dashboard + score
|
|
111
137
|
fux gate --install # wire a git pre-commit enforcement hook
|
|
@@ -170,6 +196,31 @@ Beyond authoring, Fux **enforces and reports**: `fux lint` grades rule quality,
|
|
|
170
196
|
`fux stats` scores knowledge health, `fux gate` blocks drift at commit/CI time,
|
|
171
197
|
and `fux mcp` exposes the whole substrate to agents over MCP.
|
|
172
198
|
|
|
199
|
+
**Constitutional tier (opt-in):** a rule's `tier` (`constitutional` · `standard` ·
|
|
200
|
+
`advisory`) sets how hard it bites — constitutional rules block in any mode. A principle
|
|
201
|
+
*becomes* law through `/fux debate "<rule>"` — a skill that spawns **two sub-agents** (no
|
|
202
|
+
assigned sides, blind first passes, anti-sycophancy gates) and escalates to **you** as
|
|
203
|
+
tie-breaker — then `fux ratify` makes it tamper-evident: it stamps a `content_seal` (and the
|
|
204
|
+
debate's `debate_hash`) and records the rule in a committed `.fux/constitution.lock`, so any
|
|
205
|
+
in-place edit, add, or delete becomes an always-blocking `tampered` finding. The debate is
|
|
206
|
+
the *host session's* tokens; Fux's own code never calls an LLM (a guard test proves the
|
|
207
|
+
maintenance path is model-free). All deterministic and `$0`. Default behaviour is unchanged
|
|
208
|
+
until you ratify (`tier` defaults to `standard`).
|
|
209
|
+
|
|
210
|
+
Each principle is tagged `enforcement: deterministic` (money/PII/numbers — decided by a
|
|
211
|
+
`check:`/seal, **never** sent to the AI critic) or `judgment` (tone/completeness — decided
|
|
212
|
+
by AI self-critique, **never** faked as a machine check); the split is enforced structurally
|
|
213
|
+
by a `$0` router, and `fux check` flags untagged rules that look like principles so backfill
|
|
214
|
+
is guided, not guessed.
|
|
215
|
+
|
|
216
|
+
At the action boundary (PreToolUse / pre-commit), `fux critic "<change>"` runs the
|
|
217
|
+
**deterministic pass first** (hard-invariant fails block, no LLM), then the host agent
|
|
218
|
+
self-critiques the `judgment` principles with its own tokens (the `critic` skill drives the
|
|
219
|
+
bounded revise / escalate / `/fux debate` loop); verdicts land in `.fux/out/critic.jsonl`,
|
|
220
|
+
and `fux gate` reports any ungoverned `important_globs` path (report-first, never blocks).
|
|
221
|
+
A headless AI critic for no-session/runtime use ships behind an opt-in `[critic]` extra
|
|
222
|
+
(mirroring `[embeddings]`) — the default install stays model-free.
|
|
223
|
+
|
|
173
224
|
For cross-session memory it stays **authored, not captured**: an opt-in `capture`
|
|
174
225
|
hook queues *which* files changed for `fux distill` (human-confirmed) rather than
|
|
175
226
|
auto-summarising, and `type: memory` entries **decay** after a TTL so stale notes
|
|
@@ -2,15 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
# Fux
|
|
4
4
|
|
|
5
|
-
>
|
|
6
|
-
>
|
|
7
|
-
>
|
|
8
|
-
>
|
|
5
|
+
> **Fux records *why* your code is the way it is — and checks it's still true.**
|
|
6
|
+
> A portable, agent-aware knowledge engine: one frontmatter substrate → derived
|
|
7
|
+
> **index**, **graph**, and **memory** views. `$0` and deterministic — zero
|
|
8
|
+
> third-party deps, no mandatory LLM calls. Author each rule once; your agent
|
|
9
|
+
> reads a cheap one-line index first and opens the full rule only when it's
|
|
10
|
+
> relevant — and `fux seal` lets `fux check` tell you when the governed code
|
|
11
|
+
> drifted out from under it.
|
|
12
|
+
|
|
13
|
+
> **New in v0.4.0 — the constitutional-app engine.** Govern where trust lives, opt-in and
|
|
14
|
+
> `$0`: give a rule a `tier` (`constitutional` · `standard` · `advisory`); author principles
|
|
15
|
+
> by two-agent **`/fux debate`**; **`fux ratify`** a rule into a tamper-evident constitution
|
|
16
|
+
> (sealed in **`.fux/constitution.lock`** — any later edit/add/delete is an always-blocking
|
|
17
|
+
> `tampered` finding); critique changes against the constitution with **`fux critic`** before
|
|
18
|
+
> they land (deterministic pass first, no LLM); and **`fux gate`** reports every ungoverned
|
|
19
|
+
> path (report-first, never blocks). The AI self-critique ships behind an opt-in **`[critic]`**
|
|
20
|
+
> extra — the default install stays model-free. Adopting it changes nothing until you opt in.
|
|
21
|
+
|
|
22
|
+
<!-- launch: replace the line below with the demo GIF — `fux why day-pnl` → rule + why +
|
|
23
|
+
governed code, then the Solar Terminal graph igniting the `governs` links.
|
|
24
|
+
Storyboard + capture script: docs/launch/gif-storyboard.md -->
|
|
25
|
+
<p align="center"><em>▶ demo GIF goes here — see <a href="docs/launch/gif-storyboard.md">docs/launch/gif-storyboard.md</a></em></p>
|
|
26
|
+
|
|
27
|
+
**Pronounced "fox."** (Say it like the animal — *fux* → *fox*.)
|
|
9
28
|
|
|
10
29
|
Named after *Johann Joseph Fux*, author of *Gradus ad Parnassum* (1725) — the
|
|
11
30
|
counterpoint treatise every composer learned the rules from. A tool that codifies
|
|
12
|
-
and enforces rules, named after the man who wrote *the* rulebook.
|
|
13
|
-
`wagner`, `bach`, `orff`.
|
|
31
|
+
and enforces rules, named after the man who wrote *the* rulebook. (The name is
|
|
32
|
+
deliberate.) Sits beside `wagner`, `bach`, `orff`.
|
|
14
33
|
|
|
15
34
|
Fux **unifies and replaces** three things a project usually runs separately — the
|
|
16
35
|
structural graph (graphify), cross-session memory, and the narrative docs — and
|
|
@@ -34,12 +53,14 @@ So: write the *why* down once → it's found fast, stays correct, and never gets
|
|
|
34
53
|
## Why
|
|
35
54
|
|
|
36
55
|
The *why* behind a formula — why current value not invested cost, why
|
|
37
|
-
|
|
56
|
+
dollar-normalize first, which cost-basis method — usually lives only as an inline
|
|
38
57
|
comment, invisible until someone greps for it. Fux makes that knowledge
|
|
39
58
|
**first-class**: one entry, authored once, served back through a tiny index (read
|
|
40
59
|
first) plus lazily-opened rules (read only when relevant). Lookups run ~5–10×
|
|
41
60
|
cheaper and more correct on every later session — and you don't have to take that
|
|
42
|
-
on faith: **`fux savings`** measures the multiplier from your own file sizes
|
|
61
|
+
on faith: **`fux savings`** measures the multiplier from your own file sizes and
|
|
62
|
+
prices the win in **real dollars** (configurable `usd_per_mtok`, default = Claude
|
|
63
|
+
Opus 4.8's $5/M input rate), per lookup and as a cumulative ledger.
|
|
43
64
|
|
|
44
65
|
## Install
|
|
45
66
|
|
|
@@ -63,10 +84,13 @@ fux why day-pnl [--history] # explain a rule (+ how its *why* evolved, via gi
|
|
|
63
84
|
fux refs src/aggregator.py # which rules govern this file
|
|
64
85
|
fux recall "how is day P&L computed" --hybrid # BM25F; RRF-fuse lexical+semantic+graph
|
|
65
86
|
fux seal --all # bind rules to an AST fingerprint of their code
|
|
87
|
+
fux debate "<rule>" (skill) # two-agent free debate → you ratify the result
|
|
88
|
+
fux ratify <id> --by Arpit # ratify a constitutional rule (tamper-evident; the only path)
|
|
89
|
+
fux critic "<change>" # critique a change vs principles before it lands (deterministic pass; $0)
|
|
66
90
|
fux coverage # % of important files with a governing rule
|
|
67
91
|
fux verify --fuzz # run invariant `check:`; boundary-fuzz for div-by-zero
|
|
68
92
|
fux mine # surface candidate rules latent in the code (drafts)
|
|
69
|
-
fux savings "how is day P&L computed" # measured token
|
|
93
|
+
fux savings "how is day P&L computed" # measured token + dollar cost win (+ cumulative ledger)
|
|
70
94
|
fux lint # rule *quality*: missing why / code_refs / edges
|
|
71
95
|
fux stats # knowledge-health dashboard + score
|
|
72
96
|
fux gate --install # wire a git pre-commit enforcement hook
|
|
@@ -131,6 +155,31 @@ Beyond authoring, Fux **enforces and reports**: `fux lint` grades rule quality,
|
|
|
131
155
|
`fux stats` scores knowledge health, `fux gate` blocks drift at commit/CI time,
|
|
132
156
|
and `fux mcp` exposes the whole substrate to agents over MCP.
|
|
133
157
|
|
|
158
|
+
**Constitutional tier (opt-in):** a rule's `tier` (`constitutional` · `standard` ·
|
|
159
|
+
`advisory`) sets how hard it bites — constitutional rules block in any mode. A principle
|
|
160
|
+
*becomes* law through `/fux debate "<rule>"` — a skill that spawns **two sub-agents** (no
|
|
161
|
+
assigned sides, blind first passes, anti-sycophancy gates) and escalates to **you** as
|
|
162
|
+
tie-breaker — then `fux ratify` makes it tamper-evident: it stamps a `content_seal` (and the
|
|
163
|
+
debate's `debate_hash`) and records the rule in a committed `.fux/constitution.lock`, so any
|
|
164
|
+
in-place edit, add, or delete becomes an always-blocking `tampered` finding. The debate is
|
|
165
|
+
the *host session's* tokens; Fux's own code never calls an LLM (a guard test proves the
|
|
166
|
+
maintenance path is model-free). All deterministic and `$0`. Default behaviour is unchanged
|
|
167
|
+
until you ratify (`tier` defaults to `standard`).
|
|
168
|
+
|
|
169
|
+
Each principle is tagged `enforcement: deterministic` (money/PII/numbers — decided by a
|
|
170
|
+
`check:`/seal, **never** sent to the AI critic) or `judgment` (tone/completeness — decided
|
|
171
|
+
by AI self-critique, **never** faked as a machine check); the split is enforced structurally
|
|
172
|
+
by a `$0` router, and `fux check` flags untagged rules that look like principles so backfill
|
|
173
|
+
is guided, not guessed.
|
|
174
|
+
|
|
175
|
+
At the action boundary (PreToolUse / pre-commit), `fux critic "<change>"` runs the
|
|
176
|
+
**deterministic pass first** (hard-invariant fails block, no LLM), then the host agent
|
|
177
|
+
self-critiques the `judgment` principles with its own tokens (the `critic` skill drives the
|
|
178
|
+
bounded revise / escalate / `/fux debate` loop); verdicts land in `.fux/out/critic.jsonl`,
|
|
179
|
+
and `fux gate` reports any ungoverned `important_globs` path (report-first, never blocks).
|
|
180
|
+
A headless AI critic for no-session/runtime use ships behind an opt-in `[critic]` extra
|
|
181
|
+
(mirroring `[embeddings]`) — the default install stays model-free.
|
|
182
|
+
|
|
134
183
|
For cross-session memory it stays **authored, not captured**: an opt-in `capture`
|
|
135
184
|
hook queues *which* files changed for `fux distill` (human-confirmed) rather than
|
|
136
185
|
auto-summarising, and `type: memory` entries **decay** after a TTL so stale notes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""§5b migration guard — snapshot findings, surface only the NEW ones ($0).
|
|
2
|
+
|
|
3
|
+
A *transient* upgrade guard, not a regression subsystem: `fux check --baseline-write`
|
|
4
|
+
snapshots current findings (canonical: kind, rule_id, message) pre-upgrade; `fux gate
|
|
5
|
+
--baseline` re-runs and fails only on findings absent from the snapshot, so a backward-
|
|
6
|
+
compatible upgrade is provably a no-op. Reuses the `Finding` serialization; stdlib only.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from fux.findings import Finding
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _sig(f: Finding) -> str:
|
|
16
|
+
return f"{f.kind}\t{f.rule_id}\t{f.message}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def write(path: Path, findings: list[Finding]) -> int:
|
|
20
|
+
"""Snapshot findings canonically sorted. Returns the count written."""
|
|
21
|
+
sigs = sorted(_sig(f) for f in findings)
|
|
22
|
+
path.write_text("\n".join(sigs) + ("\n" if sigs else ""), encoding="utf-8")
|
|
23
|
+
return len(sigs)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _read(path: Path) -> set[str]:
|
|
27
|
+
if not path.exists():
|
|
28
|
+
return set()
|
|
29
|
+
return {ln for ln in path.read_text(encoding="utf-8").splitlines() if ln.strip()}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def new_findings(path: Path, findings: list[Finding]) -> list[Finding]:
|
|
33
|
+
"""Findings absent from the baseline snapshot — the upgrade's new ones."""
|
|
34
|
+
base = _read(path)
|
|
35
|
+
return [f for f in findings if _sig(f) not in base]
|
|
@@ -4,7 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
import re
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
from fux import config, gitutil, governance, loader, paths,
|
|
7
|
+
from fux import (config, constitution, critic, gitutil, governance, loader, paths,
|
|
8
|
+
schema, seal)
|
|
8
9
|
from fux.findings import Finding
|
|
9
10
|
from fux.model import Rule
|
|
10
11
|
|
|
@@ -25,6 +26,13 @@ def run(root: Path) -> list[Finding]:
|
|
|
25
26
|
findings += _seal(r, root)
|
|
26
27
|
findings += _conflicts(layered)
|
|
27
28
|
findings += _extractor_drift(fp)
|
|
29
|
+
findings += constitution.check_tamper(rs.rules) # constitution layer (plan §6, §7a)
|
|
30
|
+
findings += constitution.check_lock(root, rs.rules)
|
|
31
|
+
findings += critic.untagged_candidates(rs.rules) # advisory backfill guide (plan §3, §5b)
|
|
32
|
+
tier_of = {r.id: str(r.fm.get("tier", "standard")) for r in rs.rules}
|
|
33
|
+
for f in findings:
|
|
34
|
+
f.tier = tier_of.get(f.rule_id, "standard") # constitution layer (plan §6)
|
|
35
|
+
findings.sort(key=lambda f: (f.kind, f.rule_id, f.message)) # canonical → baseline diff
|
|
28
36
|
_write_drift(fp, findings)
|
|
29
37
|
return findings
|
|
30
38
|
|
|
@@ -133,8 +141,9 @@ def _conflicts(layered: list[Rule]) -> list[Finding]:
|
|
|
133
141
|
def _write_drift(fp: paths.Footprint, findings: list[Finding]) -> None:
|
|
134
142
|
lines = ["# Fux DRIFT report", "",
|
|
135
143
|
f"_{len(findings)} finding(s)._" if findings else "_No drift — all rules current._", ""]
|
|
136
|
-
for kind in ("schema", "dead-ref", "conflict", "stale", "plan-drift",
|
|
137
|
-
"memory-stale", "unsealed", "
|
|
144
|
+
for kind in ("tampered", "schema", "dead-ref", "conflict", "stale", "plan-drift",
|
|
145
|
+
"invariant", "memory-stale", "unsealed", "untagged-candidate",
|
|
146
|
+
"extractor-drift"):
|
|
138
147
|
group = [f for f in findings if f.kind == kind]
|
|
139
148
|
if group:
|
|
140
149
|
lines.append(f"## {kind} ({len(group)})")
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import argparse
|
|
5
5
|
|
|
6
|
-
from fux import __version__, clicmds, cligraph, cliquery, hooks
|
|
6
|
+
from fux import __version__, clicmds, cliconstitution, cligraph, cliquery, hooks
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -20,6 +20,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
20
20
|
bld.set_defaults(fn=clicmds.cmd_build)
|
|
21
21
|
chk = sub.add_parser("check", help="validate schema/refs/staleness/conflicts")
|
|
22
22
|
chk.add_argument("--fix", action="store_true", help="apply mechanical $0 repairs")
|
|
23
|
+
chk.add_argument("--baseline-write", metavar="FILE",
|
|
24
|
+
help="snapshot current findings for the §5b migration gate (then exit)")
|
|
23
25
|
chk.set_defaults(fn=clicmds.cmd_check)
|
|
24
26
|
|
|
25
27
|
sub.add_parser("context", help="emit the compact INDEX (SessionStart hook)").set_defaults(fn=clicmds.cmd_context)
|
|
@@ -42,6 +44,18 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
42
44
|
sl.add_argument("--all", action="store_true", help="seal every rule with resolvable code")
|
|
43
45
|
sl.set_defaults(fn=cliquery.cmd_seal)
|
|
44
46
|
|
|
47
|
+
rt = sub.add_parser("ratify", help="ratify a constitutional rule (stamp + seal + lock; no LLM)")
|
|
48
|
+
rt.add_argument("id")
|
|
49
|
+
rt.add_argument("--by", help="named human ratifier (default: git user.name)")
|
|
50
|
+
rt.add_argument("--date", help="ISO ratification date (default: today)")
|
|
51
|
+
rt.add_argument("--debate", metavar="FILE",
|
|
52
|
+
help="debate transcript (from /fux debate) to hash into ratification.debate_hash")
|
|
53
|
+
rt.set_defaults(fn=cliconstitution.cmd_ratify)
|
|
54
|
+
|
|
55
|
+
cr = sub.add_parser("critic", help="critique a proposed change against principles (deterministic pass first; $0)")
|
|
56
|
+
cr.add_argument("proposal", help="the proposed change / commit message / diff summary to critique")
|
|
57
|
+
cr.set_defaults(fn=cliconstitution.cmd_critic)
|
|
58
|
+
|
|
45
59
|
refs = sub.add_parser("refs", help="reverse lookup: which rules govern this file")
|
|
46
60
|
refs.add_argument("file")
|
|
47
61
|
refs.set_defaults(fn=cliquery.cmd_refs)
|
|
@@ -58,7 +72,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
58
72
|
vf.set_defaults(fn=cliquery.cmd_verify)
|
|
59
73
|
sub.add_parser("tour", help="emit an ordered ONBOARDING.md").set_defaults(fn=cliquery.cmd_tour)
|
|
60
74
|
|
|
61
|
-
sv = sub.add_parser("savings", help="estimate the token
|
|
75
|
+
sv = sub.add_parser("savings", help="estimate the token + dollar cost win of Fux ($0)")
|
|
62
76
|
sv.add_argument("query", nargs="?", help="optional: cost a specific lookup")
|
|
63
77
|
sv.add_argument("--top", type=int, default=3)
|
|
64
78
|
sv.add_argument("--reset", action="store_true", help="clear the cumulative cost ledger")
|
|
@@ -82,6 +96,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
82
96
|
gt = sub.add_parser("gate", help="CI / pre-commit enforcement (exit 2 on blocking)")
|
|
83
97
|
gt.add_argument("--install", action="store_true", help="install a git pre-commit hook")
|
|
84
98
|
gt.add_argument("--strict-lint", action="store_true", help="treat lint findings as blocking")
|
|
99
|
+
gt.add_argument("--baseline", metavar="FILE",
|
|
100
|
+
help="§5b migration gate: fail only on findings new since this snapshot")
|
|
85
101
|
gt.set_defaults(fn=clicmds.cmd_gate)
|
|
86
102
|
|
|
87
103
|
sub.add_parser("mcp", help="serve the substrate over MCP (stdio JSON-RPC)").set_defaults(fn=clicmds.cmd_mcp)
|
|
@@ -5,8 +5,8 @@ import shutil
|
|
|
5
5
|
import subprocess
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
from fux import (build, check, config, context, fix, gate, hookinstall,
|
|
9
|
-
initcmd, mcpserver, paths, serve)
|
|
8
|
+
from fux import (baseline, build, check, config, context, fix, gate, hookinstall,
|
|
9
|
+
importer, initcmd, mcpserver, paths, serve)
|
|
10
10
|
from fux.cliutil import root
|
|
11
11
|
from fux.findings import blocking
|
|
12
12
|
|
|
@@ -51,6 +51,10 @@ def cmd_build(args) -> int:
|
|
|
51
51
|
def cmd_check(args) -> int:
|
|
52
52
|
here = root()
|
|
53
53
|
findings = check.run(here)
|
|
54
|
+
if getattr(args, "baseline_write", None):
|
|
55
|
+
n = baseline.write(Path(args.baseline_write), findings)
|
|
56
|
+
print(f"✔ baseline written: {n} finding(s) → {args.baseline_write}")
|
|
57
|
+
return 0
|
|
54
58
|
if args.fix:
|
|
55
59
|
for n in fix.apply(here, findings):
|
|
56
60
|
print(f"✔ fixed: {n}")
|
|
@@ -77,7 +81,8 @@ def cmd_gate(args) -> int:
|
|
|
77
81
|
print(f"✔ pre-commit gate installed → {hook}")
|
|
78
82
|
print(" it runs `fux gate` on every commit; bypass once with `git commit --no-verify`.")
|
|
79
83
|
return 0
|
|
80
|
-
|
|
84
|
+
base = Path(args.baseline) if getattr(args, "baseline", None) else None
|
|
85
|
+
code, report = gate.run(here, strict_lint=args.strict_lint, baseline=base)
|
|
81
86
|
print(report)
|
|
82
87
|
return code
|
|
83
88
|
|
|
@@ -166,15 +171,15 @@ def cmd_setup(_args) -> int:
|
|
|
166
171
|
skills_src = data / "skills"
|
|
167
172
|
_copy_skill(skills_src / "fux", skills_dir / "fux")
|
|
168
173
|
print(f"✔ /fux skill → {skills_dir / 'fux'}/")
|
|
169
|
-
for name in ("plan", "adr", "trace", "savings", "distill", "fetch-rules"):
|
|
174
|
+
for name in ("plan", "adr", "debate", "critic", "trace", "savings", "distill", "fetch-rules"):
|
|
170
175
|
src = skills_src / name
|
|
171
176
|
if src.exists():
|
|
172
177
|
_copy_skill(src, skills_dir / f"fux-{name}")
|
|
173
|
-
print(f"✔ sub-skills → {skills_dir}/fux-{{plan,adr,trace,savings,distill,fetch-rules}}/")
|
|
178
|
+
print(f"✔ sub-skills → {skills_dir}/fux-{{plan,adr,debate,critic,trace,savings,distill,fetch-rules}}/")
|
|
174
179
|
|
|
175
180
|
codex_skills_dir.mkdir(parents=True, exist_ok=True)
|
|
176
181
|
_copy_skill(skills_src / "fux", codex_skills_dir / "fux")
|
|
177
|
-
for name in ("plan", "adr", "trace", "savings", "distill", "fetch-rules"):
|
|
182
|
+
for name in ("plan", "adr", "debate", "critic", "trace", "savings", "distill", "fetch-rules"):
|
|
178
183
|
src = skills_src / name
|
|
179
184
|
if src.exists():
|
|
180
185
|
_copy_skill(src, codex_skills_dir / f"fux-{name}")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""CLI handlers for the constitution layer — `ratify` + `critic` (deterministic, no LLM)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
from datetime import date as _date
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from fux import config, constitution, criticloop, gitutil, loader, paths
|
|
9
|
+
from fux.cliutil import root
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def cmd_ratify(args) -> int:
|
|
13
|
+
"""Stamp ratification + freeze the seal + update the lock — the only path to the apex."""
|
|
14
|
+
here = root()
|
|
15
|
+
cfg = config.load(paths.Footprint(here).config)
|
|
16
|
+
rules = loader.resolve(here, cfg).rules
|
|
17
|
+
by = args.by or gitutil.user_name(here) or ""
|
|
18
|
+
if not by:
|
|
19
|
+
print("fux: no ratifier — pass --by <name> (or set git user.name)")
|
|
20
|
+
return 1
|
|
21
|
+
when = args.date or _date.today().isoformat()
|
|
22
|
+
dhash = None
|
|
23
|
+
if args.debate:
|
|
24
|
+
dpath = Path(args.debate)
|
|
25
|
+
if not dpath.is_file():
|
|
26
|
+
print(f"fux: debate transcript not found: {args.debate}")
|
|
27
|
+
return 1
|
|
28
|
+
dhash = hashlib.sha256(dpath.read_bytes()).hexdigest()[:16]
|
|
29
|
+
try:
|
|
30
|
+
r = constitution.ratify(here, rules, args.id, by=by, date=when, debate_hash=dhash)
|
|
31
|
+
except KeyError:
|
|
32
|
+
print(f"fux: no rule with id '{args.id}'")
|
|
33
|
+
return 1
|
|
34
|
+
except ValueError as e:
|
|
35
|
+
print(f"fux: {e}")
|
|
36
|
+
return 1
|
|
37
|
+
print(f"✔ ratified {r.id} → constitutional (by {by}, {when})")
|
|
38
|
+
print(f" content_seal frozen{f' + debate_hash {dhash}' if dhash else ''} + "
|
|
39
|
+
".fux/constitution.lock updated — the only path into the apex.")
|
|
40
|
+
return 0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def cmd_critic(args) -> int:
|
|
44
|
+
"""Critique a proposed change: deterministic pass first, then list judgment principles the
|
|
45
|
+
host agent must self-critique. $0, no LLM — the agent (or `[critic]` extra) is the judge."""
|
|
46
|
+
here = root()
|
|
47
|
+
result = criticloop.critique(here, args.proposal)
|
|
48
|
+
criticloop.record(here, result)
|
|
49
|
+
for v in result.verdicts:
|
|
50
|
+
mark = {"pass": "✔", "fail": "✗", "needs-judgment": "?"}.get(v.status, "·")
|
|
51
|
+
print(f" {mark} [{v.status}] {v.principle}: {v.rationale}".rstrip())
|
|
52
|
+
pend = result.pending
|
|
53
|
+
if pend:
|
|
54
|
+
print(f"\n{len(pend)} judgment principle(s) need self-critique — review each against the "
|
|
55
|
+
"proposal, revise, escalate if borderline (skills/critic/SKILL.md).")
|
|
56
|
+
if result.blocked:
|
|
57
|
+
print("\n✗ critic: a deterministic principle is violated — fix before proceeding.")
|
|
58
|
+
return 2
|
|
59
|
+
if not pend:
|
|
60
|
+
print("\n✔ critic: deterministic pass clean.")
|
|
61
|
+
return 0
|
|
@@ -106,7 +106,7 @@ def cmd_savings(args) -> int:
|
|
|
106
106
|
return 0
|
|
107
107
|
rep = savings.build(here, query=getattr(args, "query", None), top=args.top)
|
|
108
108
|
print(savings.render(rep))
|
|
109
|
-
print(costledger.render_summary(costledger.load(here)), end="")
|
|
109
|
+
print(costledger.render_summary(costledger.load(here), per_mtok=rep.usd_per_mtok), end="")
|
|
110
110
|
return 0
|
|
111
111
|
|
|
112
112
|
|
|
@@ -30,6 +30,8 @@ DEFAULTS = {
|
|
|
30
30
|
"memory_ttl_days": 180, # type: memory decays after this many untouched days (§17.3)
|
|
31
31
|
"usage_tracking": False, # opt-in: record served rules → usage-weighted decay (§17.20c)
|
|
32
32
|
"cost_tracking": False, # opt-in: record each lookup's savings → cumulative cost.json (§12)
|
|
33
|
+
"usd_per_mtok": 5.0, # $/million input tokens for `fux savings` dollar figures (§12);
|
|
34
|
+
# default = Claude Opus 4.8 input price. Model-agnostic — override per project.
|
|
33
35
|
"parity_stay": [], # docs that stay/are out-of-scope for `fux parity` (§17.17)
|
|
34
36
|
"context_budget_tokens": 0, # >0 ⇒ knapsack-pack the SessionStart INDEX (§17.25)
|
|
35
37
|
"graph_editor": "vscode", # editor URI scheme for clickable graph.html node links:
|
|
@@ -78,6 +80,9 @@ def default_toml() -> str:
|
|
|
78
80
|
"usage_tracking = false\n\n"
|
|
79
81
|
"# Opt-in: accumulate each lookup's token savings into .fux/cost.json ($0).\n"
|
|
80
82
|
"cost_tracking = false\n\n"
|
|
83
|
+
"# $/million input tokens used to price `fux savings` in dollars (model-agnostic).\n"
|
|
84
|
+
"# Default = Claude Opus 4.8 input price; set to your model's rate.\n"
|
|
85
|
+
"usd_per_mtok = 5.0\n\n"
|
|
81
86
|
"# Docs that stay / are out-of-scope for `fux parity` (beyond conventions,\n"
|
|
82
87
|
"# guardrails) — e.g. process docs that get deleted, not migrated to narrative.\n"
|
|
83
88
|
"parity_stay = []\n\n"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Constitutional integrity — tamper-evidence, ratification & the lock ($0, deterministic).
|
|
2
|
+
A ratified constitutional rule (§6) carries `ratification.content_seal`, recorded in
|
|
3
|
+
`.fux/constitution.lock`; `fux check` recomputes both → always-blocking `tampered`; `fux
|
|
4
|
+
ratify` is the only path that stamps them. No LLM, no new deps."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from fux import seal
|
|
11
|
+
from fux.findings import Finding
|
|
12
|
+
from fux.model import Rule
|
|
13
|
+
|
|
14
|
+
LOCK = "constitution.lock" # under .fux/
|
|
15
|
+
_VOLATILE = {"ratification", "seal", "updated"} # change without changing meaning
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def content_seal(rule: Rule) -> str:
|
|
19
|
+
"""Tamper fingerprint: hash of the rule's normalized body + governing frontmatter
|
|
20
|
+
(volatile seal/ratification/updated excluded). Reuses seal.py's hash + whitespace fold."""
|
|
21
|
+
gov = {k: v for k, v in rule.fm.items() if k not in _VOLATILE}
|
|
22
|
+
blob = json.dumps(gov, sort_keys=True, ensure_ascii=False, default=str)
|
|
23
|
+
return seal._hash(blob + "\n" + " ".join(rule.body.split()))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _constitutional(rules: list[Rule]) -> list[Rule]:
|
|
27
|
+
return [r for r in rules if str(r.fm.get("tier")) == "constitutional"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def check_tamper(rules: list[Rule]) -> list[Finding]:
|
|
31
|
+
"""`tampered` for any ratified constitutional rule whose recomputed content_seal no
|
|
32
|
+
longer matches its stamped `ratification.content_seal` (a body/meaning edit)."""
|
|
33
|
+
out: list[Finding] = []
|
|
34
|
+
for r in _constitutional(rules):
|
|
35
|
+
stored = (r.fm.get("ratification") or {}).get("content_seal")
|
|
36
|
+
if stored and content_seal(r) != stored:
|
|
37
|
+
out.append(Finding("tampered", r.id,
|
|
38
|
+
"body/frontmatter changed since ratification — constitutional "
|
|
39
|
+
"rules never change in place; supersede + re-ratify"))
|
|
40
|
+
return out
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def lock_manifest(rules: list[Rule]) -> dict[str, str]:
|
|
44
|
+
"""The expected lock — {id: stamped content_seal} for every ratified constitutional rule."""
|
|
45
|
+
out = {r.id: (r.fm.get("ratification") or {}).get("content_seal")
|
|
46
|
+
for r in _constitutional(rules)}
|
|
47
|
+
return {k: v for k, v in sorted(out.items()) if v}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _lock_path(root: Path) -> Path:
|
|
51
|
+
return root / ".fux" / LOCK
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _read_lock(root: Path) -> dict[str, str]:
|
|
55
|
+
p = _lock_path(root)
|
|
56
|
+
if not p.exists():
|
|
57
|
+
return {}
|
|
58
|
+
try:
|
|
59
|
+
return json.loads(p.read_text(encoding="utf-8"))
|
|
60
|
+
except (OSError, ValueError):
|
|
61
|
+
return {}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def check_lock(root: Path, rules: list[Rule]) -> list[Finding]:
|
|
65
|
+
"""`tampered` wherever `.fux/constitution.lock` and the live ratified set diverge —
|
|
66
|
+
a constitutional rule added, deleted, or re-stamped outside `fux ratify`."""
|
|
67
|
+
locked, current = _read_lock(root), lock_manifest(rules)
|
|
68
|
+
out: list[Finding] = []
|
|
69
|
+
for rid in sorted(set(locked) | set(current)):
|
|
70
|
+
lo, cu = locked.get(rid), current.get(rid)
|
|
71
|
+
if lo == cu:
|
|
72
|
+
continue
|
|
73
|
+
why = ("missing/un-ratified on disk — restore + `fux ratify`" if cu is None
|
|
74
|
+
else "absent from the lock — `fux ratify` to record" if lo is None
|
|
75
|
+
else "content_seal differs from the lock — changed outside `fux ratify`")
|
|
76
|
+
out.append(Finding("tampered", rid, "constitution.lock mismatch: " + why))
|
|
77
|
+
return out
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def ratify(root: Path, rules: list[Rule], rule_id: str, by: str, date: str,
|
|
81
|
+
debate_hash: str | None = None) -> Rule:
|
|
82
|
+
"""The only path into the constitutional tier: stamp ratification.{by,date,content_seal,
|
|
83
|
+
debate_hash?}, freeze the code seal, rewrite `.fux/constitution.lock`. Raises if id absent
|
|
84
|
+
or not constitutional. Deterministic — no LLM, no clock (caller supplies date + hash)."""
|
|
85
|
+
from fux import fmwrite
|
|
86
|
+
rule = next((r for r in rules if r.id == rule_id), None)
|
|
87
|
+
if rule is None:
|
|
88
|
+
raise KeyError(rule_id)
|
|
89
|
+
if str(rule.fm.get("tier")) != "constitutional":
|
|
90
|
+
raise ValueError(f"{rule_id} is tier={rule.fm.get('tier', 'standard')}, not constitutional")
|
|
91
|
+
rat = {"by": by, "date": date, "content_seal": content_seal(rule)}
|
|
92
|
+
rule.fm["ratification"] = {**rat, "debate_hash": debate_hash} if debate_hash else rat
|
|
93
|
+
if (code_seal := seal.current(root, rule)):
|
|
94
|
+
rule.fm["seal"] = code_seal
|
|
95
|
+
rule.path.write_text(fmwrite.dump(rule.fm, rule.body), encoding="utf-8")
|
|
96
|
+
lock = _lock_path(root)
|
|
97
|
+
lock.parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
lock.write_text(json.dumps(lock_manifest(rules), indent=2, sort_keys=True) + "\n",
|
|
99
|
+
encoding="utf-8")
|
|
100
|
+
return rule
|