Skip to content

Commit b5b124a

Browse files
ReScript→AffineScript migration: finish the compiler/src port (modules 7–17) (#30)
* error-lang: replace fabricated ABI "proofs" with machine-checked ones The three "Safety Proofs" in src/abi/Foreign.idr (stabilityBounded, positionalDeterministic, paradoxMonotonic) were not proofs: each fabricated its evidence with cast ()/cast Refl over an IO action. Replace them with genuine, self-contained Idris2 modules, machine-checked under Idris 2 v0.8.0 (no believe_me / assert_total / cast / postulate): - Stability.idr : stability score is bounded in [0,100] (clamp model of compiler/src/Types.res calculateStability). - Positional.idr : positional-operator behaviour is deterministic over the pure model of the Zig FFI (a genuine Refl, not IO cast Refl). - Paradox.idr : the two threshold-gated factors are monotone; the blanket "paradox detection monotonic" claim is RETRACTED -- proving it honestly surfaced that scope_leakage is prime-gated and therefore non-monotone (line 7 prime, line 8 not). Foreign.idr is reduced to an honest, self-contained ABI binding layer. Add error-lang-abi.ipkg and verification/check-proofs.sh (idris2 --check all four modules). Rewrite PROOF-NEEDS.md to record what is proved, what is retracted, the toolchain, and open conformance obligations. The language's satirical "100% production-ready / formally verified" self-presentation in README/WHITEPAPER is intentional and left intact; this change only makes the underlying proof artifacts real and honest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195yA45jSSP7YDPwJSpw4bM * docs: design Error-Lang as a Trope IR front end (trope-particularity integration) Adds docs/Trope-Particularity-Integration.adoc — a design (not yet implemented) for lowering Error-Lang's Echo operations and stability factors to the language-neutral Trope IR (hyperpolymath/trope-checker, v0.1 prevent profile) and consuming the verified verdict + witness. - Object/effect/grade correspondence: Echo<A,B> <-> Trope[Phi], EchoR <-> FloatingQuality, echo/echo_to_residue/echo_input/echo_output <-> preserve/ detach/project. echo_to_residue IS detach (bond=Severed, irrecoverable), matching the [Stab-Erase] debit and "decomposition must be visible". - stabilityFactor -> grade mapping; the silent instabilities (GlobalState, RaceCondition) land on the deceptive Conflated bottom -> a lowering fault under the prevent profile. - Verdict mapping: scalar calculateStability -> use-model floor + p-sufficient/ p-insufficient + witness edge (the invariant-path argmin Stability.idr already reasons about). - Architecture (reference, never vendor; schema is the trust boundary), the per-front-end O2 lowering-correctness obligations (L-Echo/L-Grade/L-Silent/ L-Floor), and a 4-phase plan. Builds on docs/Echo-Decomposition.adoc; references echo-types, trope-checker and trope-particularity-workbench by URL only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195yA45jSSP7YDPwJSpw4bM * docs: qualify external repo paths to clear structural_drift (SD022) Hypatia structural_drift flagged `src/idris2/` and `src/aggregate/` in the trope-integration design as dangling references "surviving a directory rename". Both are deliberately *external*: the trope-checker repo's Idris2 core and the panic-attack repo's aggregate module. Reword as unambiguously external (drop the bare `src/<dir>/` form) so the heuristic no longer reads them as internal error-lang tree paths. No substantive content change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195yA45jSSP7YDPwJSpw4bM * compiler: begin ReScript->AffineScript migration — port Types.res First module of the compiler's migration to AffineScript (the Hyperpolymath language policy bans ReScript). Adds compiler/src/Types.affine, a faithful port of compiler/src/Types.res, verified green with `affinescript check`: - all token / AST / error / stability types (structs + enums + match) - ReScript inline-record variants lowered to positional constructor args - token variants Float/String renamed FloatTok/StringTok (reserved type keywords in AffineScript) - Echo types (TyEcho / TyEchoResidue) shaped Trope-IR-ready per docs/Trope-Particularity-Integration.adoc (Phase 0) - make_default_state, stability_impact, calculate_stability, error_code_to_string Toolchain, so the .affine sources are reproducibly CI-verifiable: - scripts/install-affinescript-toolchain.sh — builds the AffineScript compiler from distro OCaml packages (independent of opam.ocaml.org) + installs the binary and stdlib under a discoverable share/ path - verification/check-affinescript.sh — typechecks all compiler/src/*.affine Types.res is retained until its dependents migrate; format_diagnostic is deferred pending the string / affine-borrow pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195yA45jSSP7YDPwJSpw4bM * compiler: fix AffineScript module system to unblock the per-module port The .res -> .affine port needs sibling compiler modules to import the shared AST (Location/Token/Position/...) from Types. AffineScript's module resolver exported imported enum constructors but dropped imported struct/alias type definitions, so cross-module struct field access failed ("Field not found"). Per the chosen approach (fix the resolver, not single-file/accessors): - patches/affinescript-module-struct-fields.patch: threads imported modules' type_env + constructor_env into the importing module's typecheck context (typecheck.ml check_program gains ?import_type_env/?import_constructor_env; resolve.ml import_type_defs copies them across all three import forms; bin/main.ml passes them at the check/compile/eval entry points). Documented in patches/README.adoc; pending upstream to hyperpolymath/affinescript. - compiler/src/Types.affine: now a proper `module Types;` with `pub` exports. - verification/check-affinescript.sh: checks from compiler/src so `use Types::{...}` resolves via the loader's current-dir search. - scripts/install-affinescript-toolchain.sh: applies the patch after cloning, before building (idempotent). Verified: a module importing Types' structs with nested field access, struct construction, and enum-field matching type-checks; affinescript's own stdlib cross-module imports (http_fetch/option/io) still pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195yA45jSSP7YDPwJSpw4bM * compiler: port Lexer.res -> Lexer.affine Second compiler module. Lexer.affine imports Types (`use Types::*;`) and ports the tokenizer in AffineScript's functional style: with no mutable struct fields or record-spread, state-mutating helpers take a LexerState and return a new one, and the scanners + driver loop a `let mut` local. Single chars are Char (char_at / char_to_int); lexemes and the escape buffer use substring; numeric literals use the parse_int / parse_float builtins; the keyword Dict becomes a string-equality lookup. Verified green with `affinescript check`. Faithful for the core path (identifiers/keywords, decimal int + float + exponent, strings with escapes, all operators/delimiters, comments, newline + EOF, error recovery + diagnostics E0001-E0004). Documented parity gaps for a follow-up pass: hex/binary integer prefixes, triple-quoted strings + \0 escape, smart-quote E0007. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195yA45jSSP7YDPwJSpw4bM * compiler: port Cst.res -> Cst.affine (concrete syntax tree + trivia) Third module. Trivia-preserving CST with source round-trip. Functional port: classify_trivia loops a `let mut` local; lex_with_trivia folds the raw tokens carrying the last token in hand, so newline-trailing-trivia and trailing source attach without array-index mutation. Selective `Types` import (Cst's node kinds collide with Types' Decl/Stmt constructors). Verified with `affinescript check`. Omitted, documented in-file: - node_at: returning a deepest *subtree* needs a node used both to recurse into AND to return — not expressible under affine ownership without a shared/clone type. Auxiliary (IDE cursor lookup); deferred. - run_tests: the Console-based test harness is not compiler code. Encountered an affinescript resolver bug along the way: a local `let len = ...` shadows the builtin `len` *globally* (so a later `len(xs)` typed as Int). Worked around by renaming the local; worth an upstream fix in affinescript. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195yA45jSSP7YDPwJSpw4bM * compiler: port Parser.res -> Parser.affine (recursive-descent parser) Fourth and largest module (1150 lines). Complete error-tolerant recursive-descent parser, faithful grammar and precedence. Functional state-threading: ParserState is immutable; producers return (Option<X>, ParserState) and callers thread via `let (x, st) = f(st)`; ReScript `ref` accumulators became `let mut` locals with (acc, state) while-loops. Full coverage: the expression ladder (ternary -> logical-or/and -> equality -> comparison -> term -> factor -> unary -> postfix -> primary), array/lambda/grouped primaries, call/index/member postfix chains; type expressions incl. Echo<A,B> / EchoR<A,B> and the >>-split close-angle; the full statement set incl. gutter-block error recovery; function/struct/main declarations; and the `parse` driver returning (Program, [Diagnostic]). Verified green with `affinescript check` and the full harness (Types/Lexer/Cst/Parser all ok). Documented faithful deviations: the >>-split token rewrite reproduced via array rebuild (tokens are immutable); the pre-existing node-`start` quirk reproduced exactly rather than corrected; minor error-recovery loop hardening to guarantee progress on a stray token. AffineScript checker bug found + worked around: an if-statement immediately followed by a tuple literal in tail position fails with E0104 "Expected a function, got Unit" (the if's () is applied to the tuple). Used explicit `return (...)`. Third affinescript bug this migration (after the module-resolver struct-field gap and the global `len` builtin-shadow); all worth upstreaming. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195yA45jSSP7YDPwJSpw4bM * compiler: port TypeChecker.res + TypeSuperposition.res -> .affine Modules 5 + 6 (1123 + 323 lines), both verified green with affinescript check and the full harness (all six modules ok). TypeChecker.affine: the static checker. The .res's shared-mutable env (fresh-var counter + substitution map) and checkResult.errors are unified into one immutable, threaded CheckState, returned via (result, CheckState) tuples. The lexical environment is a frame-stack [[(String, Binding)]] (head = innermost); the substitution map is an Int-keyed assoc list. Unification is faithful arm-for-arm including numeric widening and the Echo/EchoR rule: same-kind structural only, Echo<_,_> deliberately NOT unifying with EchoR<_,_> (erasure is irreversible). The Echo builtins (echo / echo_to_residue / residue_strictly_loses / echo_input / echo_output) mirror the .res including the [Stab-Erase] diagnostics. Internal `ty` constructors are C-prefixed (CInt..CVar) to avoid colliding with Types' TypeExpr. TypeSuperposition.affine: the quantum-type demo (Collapsed | Superposition(...)); the deterministic collapse index is byte-faithful. Documented faithful deviations: the .res `exprLoc` helper is omitted — `infer_expr` returns the expression's own Location (a copyable summary) alongside the type, so diagnostics keep exact locations without re-traversing a consumed node; Array.zip/every/forEach over state-mutating callbacks become state-threading folds; minor local renames to avoid global-constructor collisions. No type rule, diagnostic, Echo/EchoR behaviour, or collapse arithmetic was changed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195yA45jSSP7YDPwJSpw4bM * compiler: port Bytecode.res + Codegen.res + VM.res -> .affine Modules 7-9 (251 + 526 + 727 = 1504 lines), all verified green with affinescript check and the full 9-module harness. Completes the core compiler execution path. Bytecode.affine: 44 opcodes (stack/vars/arith-with-position-metadata/comparison/ control-flow/arrays/haptics/the five Echo ops/print/halt) + Value/Chunk/ BytecodeProgram + value_to_string/opcode_to_string. paradoxType constructors Px-prefixed to avoid colliding with Types' StabilityFactor::NullPropagation. Codegen.affine: AST -> bytecode. Immutable Compiler threaded; compile_expr returns the expression Location (so ExprStmt's trailing OpPop needs no re-traversal of a consumed node); jump backpatching via array rebuild (set_code_at). Echo lowering pushes args left-to-right then emits the dedicated OpEcho* op. VM.affine: stack interpreter. Immutable Vm; stack is a [Value] with an sp mirror; globals an assoc list. Runtime errors are values (Result + ExecResult = ExContinue|ExHalt|ExError), not exceptions -- error strings preserved verbatim. Echo runtime semantics byte-faithful: OpEchoToResidue debits echo_erase_cost (15.0), erases the witness, pushes VResidue (idempotent on a residue); OpEchoInput errors on a residue; OpEchoOutput returns the retained output. Pop-order, OpArray ordering, and OpSub/OpDiv operand order all match VM.res arm-for-arm. Documented faithful deviations: float pow uses repeated multiplication (integer- exact; the stdlib has no ln/log) -- the only numeric divergence; the .res "Not implemented" ops (OpLte/OpGte, OpCall) reproduced exactly; the Console *debug* disassembler omitted (its pure helpers kept); real VM print/IO kept. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195yA45jSSP7YDPwJSpw4bM * compiler: port the remaining 8 compiler/src modules -> .affine Completes the compiler/src ReScript->AffineScript port: all 17 modules are now .affine. 2785 lines across Stability, Pretty, TokenStream, LayerNavigator, Analyzer, FiveWhys, IncrementalLexer, IncrementalParser. All verified green with affinescript check and the full 17-source harness. - Stability: paradox/consequence/discovery detection + stability arithmetic. - Pretty: AST->source pretty-printer (immutable Printer threaded through pp_*). - TokenStream: proc-macro-style token-stream API + of_string lexer. - LayerNavigator: 5-layer (Grammar/Parser/AST/Semantics/Runtime) views; owns Layer. - Analyzer: paradox/report formatters, causality, forensic trace. - FiveWhys: automated root-cause why-chains. - IncrementalLexer / IncrementalParser: edit-driven re-lex / re-parse + splice. Documented omissions (one AffineScript limitation, three call sites): the record-constructor functions generate_report (Stability), analyze_program (Analyzer) and create_layer_views (LayerNavigator) build records whose fields are typed Dict<K,V> in the shipped types. Dict<K,V> is effectively a PHANTOM TYPE in AffineScript -- accepted in annotations but with no value-level introduction (no literal, no constructor, no Dict-returning builtin; the stdlib `dict` is a distinct assoc-list type that does not unify). Those records therefore cannot be constructed; every function that READS such a field is ported. Closing them needs the Dict fields switched to the idiomatic assoc-list representation [(K,V)] (or affinescript's Dict made constructable) -- tracked as a follow-up. format_diagnostic is reimplemented locally (byte-faithful) since the shipped Types.affine omitted it. Confirmed all three prior affinescript bugs (builtin-shadow bit once via a `length` param). The Dict phantom-type is the fourth distinct affinescript limitation found this migration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195yA45jSSP7YDPwJSpw4bM --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 99ed0b6 commit b5b124a

11 files changed

Lines changed: 4289 additions & 0 deletions

compiler/src/Analyzer.affine

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Analyzer.affine — static analysis & paradox detection
3+
// (ported from compiler/src/Analyzer.res).
4+
//
5+
// ── AffineScript port conventions ──
6+
// * `open Types` + `open Stability` -> `use Types::*;` + `use Stability::*;`.
7+
// The `Paradox`/`Consequence`/`Discovery` enums and the
8+
// `operator_behavior_from_position` / `alternative_operator_behaviors` /
9+
// `is_context_collapse` / `stability_bar` / `consequence_emoji` helpers
10+
// are reused from Stability.affine verbatim.
11+
// * ReScript inline-record variants -> positional (matching Stability's
12+
// positional constructors and Types' positional AST/StabilityFactor ctors).
13+
// * Backtick `${…}` templates with embedded newlines -> `++` concatenation
14+
// with `\n`; emoji/box glyphs preserved. `Array.map(...)->Array.joinWith` ->
15+
// `join(map(...), sep)`; `Array.slice(~start,~end)` -> `xs[a:b]`;
16+
// `Array.mapWithIndex` -> an index-carrying loop.
17+
//
18+
// ── Faithfulness notes: documented gaps / simplifications vs Analyzer.res ──
19+
// * `analyze_program` is OMITTED. It constructs an `AnalysisResult` whose
20+
// `stabilityReport` field is a `Types::StabilityReport`, and it does so by
21+
// calling `Stability::generate_report` — both of which are unconstructable
22+
// because `StabilityReport.breakdown : Dict<String,Int>` has no value-level
23+
// introduction in AffineScript (full rationale in Stability.affine's
24+
// header). The AST walk it performs (paradox detection via
25+
// `is_context_collapse` on let-names and `operator_behavior_from_position`
26+
// on +/-/* binaries, plus mutable-state factor accumulation) cannot be
27+
// surfaced without building that result record, so it is not ported.
28+
// Consumers that already hold an `AnalysisResult` work: `forensic_analysis`
29+
// (which only READS `analysis.stabilityReport.factors`) is ported, as is
30+
// `format_stability_report` (which READS a `StabilityReport`).
31+
// * Everything else is ported with identical strings/arithmetic: the
32+
// `Paradox`/`Consequence`/etc. constructors, `trace_causality`,
33+
// `generate_alternatives`, `forensic_analysis`, `format_paradox`, and
34+
// `format_stability_report` (including the top-3 recommendation slice).
35+
// * No `runTests`/Console code exists in Analyzer.res, so nothing else omitted.
36+
37+
module Analyzer;
38+
39+
use prelude::*;
40+
use string::{join};
41+
use Types::*;
42+
use Stability::*;
43+
44+
// ============================================
45+
// Program Analysis (result type)
46+
// ============================================
47+
48+
// `stabilityReport` is a `Types::StabilityReport` (phantom `Dict` field — see
49+
// header); the struct can be declared but not constructed here.
50+
pub struct AnalysisResult {
51+
paradoxes: [Paradox],
52+
consequences: [Consequence],
53+
stabilityReport: StabilityReport,
54+
discoveries: [Discovery]
55+
}
56+
57+
// NOTE: `analyze_program` (Analyzer.res) is intentionally not ported — it
58+
// builds an `AnalysisResult` containing a `Types::StabilityReport`, which is
59+
// unconstructable in AffineScript (see header / Stability.affine).
60+
61+
// ============================================
62+
// Causality Tracing
63+
// ============================================
64+
65+
pub struct CausalityChain {
66+
symptom: String,
67+
symptomLocation: Location,
68+
chain: [(String, Location)],
69+
rootCause: String,
70+
rootLocation: Location
71+
}
72+
73+
pub fn trace_causality(symptom: String, symptom_loc: Location, prog: Program) -> CausalityChain {
74+
// Simplified causality tracing — in a real implementation this would track
75+
// data flow through the AST.
76+
#{ symptom: symptom,
77+
symptomLocation: symptom_loc,
78+
chain: [("Type mismatch propagated from", symptom_loc)],
79+
rootCause: "Unvalidated user input",
80+
rootLocation: symptom_loc }
81+
}
82+
83+
// ============================================
84+
// Alternative Code Generation
85+
// ============================================
86+
87+
pub struct Alternative {
88+
description: String,
89+
code: String,
90+
stabilityScore: Int,
91+
improvements: [String]
92+
}
93+
94+
pub fn generate_alternatives(stmt: Stmt, current_stability: Int) -> [Alternative] {
95+
match stmt {
96+
LetStmt(mutable_, name, _type, _value, _loc) =>
97+
if mutable_ {
98+
[
99+
#{ description: "Use immutable binding",
100+
code: "let " ++ name ++ " = ...",
101+
stabilityScore: current_stability + 15,
102+
improvements: [
103+
"No mutation risk",
104+
"Thread-safe by default",
105+
"Easier to reason about"
106+
] }
107+
]
108+
} else {
109+
[]
110+
},
111+
_ => []
112+
}
113+
}
114+
115+
// ============================================
116+
// Forensic Analysis (Deep Dive)
117+
// ============================================
118+
119+
pub struct ForensicTrace {
120+
target: String,
121+
targetLocation: Location,
122+
instabilityFactors: [(String, Int, String)], // (description, impact, reason)
123+
probabilityMap: [(String, Int)], // (scenario, probability %)
124+
suggestedFix: String
125+
}
126+
127+
fn factor_description(factor: StabilityFactor) -> String {
128+
match factor {
129+
MutableState(_m, _r) => "Mutable state detected",
130+
TypeInstability(_x) => "Type changed dynamically",
131+
_ => "Unknown factor"
132+
}
133+
}
134+
135+
pub fn forensic_analysis(target: String, target_loc: Location, analysis: AnalysisResult) -> ForensicTrace {
136+
// Find all factors affecting this target.
137+
let factors = map(analysis.stabilityReport.factors, |factor| {
138+
let impact = stability_impact(factor);
139+
(factor_description(factor), impact, "See stability report")
140+
});
141+
142+
let probability_map = [
143+
("Standard behavior", 40),
144+
("Positional override", 30),
145+
("Context collapse", 20),
146+
("Temporal corruption", 10)
147+
];
148+
149+
#{ target: target,
150+
targetLocation: target_loc,
151+
instabilityFactors: factors,
152+
probabilityMap: probability_map,
153+
suggestedFix: "Move to different position or add explicit type" }
154+
}
155+
156+
// ============================================
157+
// Visualization Formatters
158+
// ============================================
159+
160+
pub fn format_paradox(paradox: Paradox) -> String {
161+
match paradox {
162+
ContextCollapseKeyword(keyword, line, _depth, _isKw, reason) =>
163+
"⚡ QUANTUM KEYWORD at line " ++ int_to_string(line) ++ "\n" ++
164+
" '" ++ keyword ++ "' is both keyword AND identifier\n" ++
165+
" Reason: " ++ reason,
166+
167+
PositionalOperator(operator, line, column, behavior, alternatives) =>
168+
"🎲 POSITIONAL OPERATOR at line " ++ int_to_string(line) ++ ":" ++ int_to_string(column) ++ "\n" ++
169+
" '" ++ operator ++ "' behaves as: " ++ behavior ++ "\n" ++
170+
" Alternative positions:\n" ++
171+
join(map(alternatives, |pair| {
172+
let (col, beh) = pair;
173+
" Col " ++ int_to_string(col) ++ ": " ++ beh
174+
}), "\n"),
175+
176+
TypeSuperposition(variable, _line, possible_types, collapse_target) =>
177+
"🌀 TYPE SUPERPOSITION\n" ++
178+
" Variable '" ++ variable ++ "' exists as: " ++ join(possible_types, " | ") ++ "\n" ++
179+
" Will collapse to: " ++ collapse_target,
180+
181+
ScopeLeakage(variable, declared_line, access_line, _runNumber, leak_reason) =>
182+
"🕐 SCOPE LEAKAGE\n" ++
183+
" Variable '" ++ variable ++ "' declared at line " ++ int_to_string(declared_line) ++ "\n" ++
184+
" Accessible at line " ++ int_to_string(access_line) ++ "\n" ++
185+
" Reason: " ++ leak_reason,
186+
187+
TemporalCorruption(variable, _line, _affectedByRun, mechanism) =>
188+
"⏰ TEMPORAL CORRUPTION\n" ++
189+
" Variable '" ++ variable ++ "' affected by historical state\n" ++
190+
" Mechanism: " ++ mechanism
191+
}
192+
}
193+
194+
fn factor_line(factor: StabilityFactor) -> String {
195+
let emoji = consequence_emoji(factor);
196+
let impact = stability_impact(factor);
197+
let desc = match factor {
198+
MutableState(mutations, readers) =>
199+
"Mutable state (" ++ int_to_string(mutations) ++ " mutations, " ++ int_to_string(readers) ++ " readers)",
200+
TypeInstability(reassignments) =>
201+
"Type instability (" ++ int_to_string(reassignments) ++ " reassignments)",
202+
NullPropagation(depth) =>
203+
"Null propagation (depth " ++ int_to_string(depth) ++ ")",
204+
_ => "Other factor"
205+
};
206+
emoji ++ " " ++ desc ++ ": " ++ int_to_string(impact) ++ " points"
207+
}
208+
209+
pub fn format_stability_report(report: StabilityReport) -> String {
210+
let bar = stability_bar(report.score);
211+
212+
let factor_list = join(map(report.factors, factor_line), "\n ");
213+
214+
// Top-3 recommendations with 1-based numbering.
215+
let recs = report.recommendations;
216+
let top = if len(recs) > 3 { recs[0:3] } else { recs };
217+
let mut rec_lines = "";
218+
let mut i = 0;
219+
let n = len(top);
220+
while i < n {
221+
if i > 0 { rec_lines = rec_lines ++ "\n"; }
222+
rec_lines = rec_lines ++ " " ++ int_to_string(i + 1) ++ ". " ++ top[i];
223+
i = i + 1;
224+
}
225+
226+
"\n🎯 STABILITY REPORT\n" ++
227+
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" ++
228+
bar ++ "\n\n" ++
229+
"Breakdown:\n" ++
230+
" " ++ factor_list ++ "\n\n" ++
231+
"💡 Top Recommendations:\n" ++
232+
rec_lines ++ "\n"
233+
}

0 commit comments

Comments
 (0)