Skip to content
Closed
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
4 changes: 2 additions & 2 deletions .github/workflows/agents-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ jobs:
github.event_name == 'pull_request_target' &&
steps.eligibility.outputs.should-run == 'true' &&
steps.api_client_base.outputs.available != 'true'
uses: "stranske/Workflows/.github/actions/setup-api-client@62ed0a86b5d57062ac3d04f4519e3998858e2d96" # v1
uses: "stranske/Workflows/.github/actions/setup-api-client@44965d8d784573c119fb63828c05c89256c5f3e1" # v1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Update both fallback action pins to the synced SHA.

Both fallback steps still reference 44965d8d784573c119fb63828c05c89256c5f3e1, but this sync is expected to use dfe0854ae9b1ba1c616e4b57fb498f283ea3216f. As written, the workflow does not apply the intended action bump in either event path.

Suggested patch
-        uses: "stranske/Workflows/.github/actions/setup-api-client@44965d8d784573c119fb63828c05c89256c5f3e1" # v1
+        uses: "stranske/Workflows/.github/actions/setup-api-client@dfe0854ae9b1ba1c616e4b57fb498f283ea3216f" # v1
...
-        uses: "stranske/Workflows/.github/actions/setup-api-client@44965d8d784573c119fb63828c05c89256c5f3e1" # v1
+        uses: "stranske/Workflows/.github/actions/setup-api-client@dfe0854ae9b1ba1c616e4b57fb498f283ea3216f" # v1

Also applies to: 183-183

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/agents-guard.yml at line 114, Update the SHA reference for
the setup-api-client action in both fallback steps of the agents-guard workflow.
Currently, both the fallback step at line 114 and the fallback step at line 183
reference the outdated SHA 44965d8d784573c119fb63828c05c89256c5f3e1. Replace
this SHA with the correct synced version
dfe0854ae9b1ba1c616e4b57fb498f283ea3216f in both occurrences where the
stranske/Workflows/.github/actions/setup-api-client action is used to ensure the
workflow applies the intended action bump in both event paths.

Source: Linked repositories

with:
secrets: ${{ toJSON(secrets) }}
github_token: ${{ github.token }}
Expand Down Expand Up @@ -180,7 +180,7 @@ jobs:
steps.eligibility.outputs.should-run == 'true' &&
github.event_name == 'pull_request' &&
steps.api_client_head.outputs.available != 'true'
uses: "stranske/Workflows/.github/actions/setup-api-client@62ed0a86b5d57062ac3d04f4519e3998858e2d96" # v1
uses: "stranske/Workflows/.github/actions/setup-api-client@44965d8d784573c119fb63828c05c89256c5f3e1" # v1
with:
secrets: ${{ toJSON(secrets) }}
github_token: ${{ github.token }}
Expand Down
6 changes: 6 additions & 0 deletions design-system/PRESENTATION_PATTERNS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ everywhere, beats N one-off PRs — and it's why this is a design-system rollout
## The 6 patterns

### P1 — Light, understated theme by default

**Rule:** never ship a default dark theme; default to `theme-air` (Ink & Air). Dark is opt-in only.
- **web:** load `tokens.css` + `components.css`; root element `class="ds theme-air"` (`theme-paper` for friendlier apps).
- **Streamlit:** `.streamlit/config.toml` `[theme] base="light"` + the shared `ds_streamlit.inject_theme()` (maps the `theme-air` tokens to Streamlit's theme). *Already prototyped for TMP.*
- **Fixes:** default-dark on Trend_Model_Project / Portable-Alpha / Manager-Database / Inv-Man-Intake.

### P2 — Empty state = title + reason + next action

**Rule:** a "no data yet" surface ALWAYS shows a title, a one-line reason, and a **next-action CTA**.
NEVER a bare prompt sitting above already-rendered content, and NEVER an internal filename/path.
- **web:** `.ds .empty-state` (`.es-icon/.es-title/.es-desc/.es-cta`).
Expand All @@ -28,6 +30,7 @@ NEVER a bare prompt sitting above already-rendered content, and NEVER an interna
Manager-Database #1214 (empty default date / "Recent Activity"), LMS #351 (root `/` JSON 404).

### P3 — Errors are human messages + remediation

**Rule:** NEVER surface a raw exception, pydantic/validation dump, internal field name, or stack to a
user. Translate at the boundary to a plain message + a recovery action.
- **web:** `.ds .notice--error` (`.ic` + `.body strong` headline + `.body .act` remediation).
Expand All @@ -37,13 +40,15 @@ user. Translate at the boundary to a plain message + a recovery action.
the broad "raw error" class across the fleet.

### P4 — Dev/diagnostic notices go to logs, not the UI

**Rule:** auth-bypass / trace-sink / observability / "dev mode" notices NEVER render in the main
content. Use logging, or at most a collapsed "Diagnostics" expander.
- **web:** n/a in the main flow; use a `<details>`/debug panel.
- **Streamlit:** `ds_streamlit.dev_note(msg)` → `logging` (not `st.warning`/`st.write`); diagnostics behind `st.expander("Diagnostics")`.
- **Fixes:** Manager-Database #1215 (auth-bypass `st.warning`), Inv-Man-Intake #630 (trace-sink banner).

### P5 — Feature-availability markers (no silent dead-ends)

**Rule:** a tab/control that isn't applicable in the current mode states so up front (a badge/label),
rather than opening into a silent empty/disabled surface.
- **web:** `.ds .badge` on the tab/control (e.g. "multi-period only", "needs setup").
Expand All @@ -52,6 +57,7 @@ rather than opening into a silent empty/disabled surface.
(upload-only pages with no sample path → mark/offer the sample).

### P6 — No raw internal identifiers in user surfaces

**Rule:** decode internal IDs / fixture filenames / record keys into human-readable labels before display.
- **web/Streamlit:** a display-name mapping; keep the raw id as secondary/`title=` metadata if useful.
- **Fixes:** Inv-Man-Intake #629 (opaque `item_id`), #630 (raw fixture filename in selector), PA Results (`Outputs.xlsx`).
Expand Down
7 changes: 3 additions & 4 deletions design-system/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Shared Design System (staging)
# Shared Design System

Two themes on one token foundation, plus an orthogonal density axis.

Expand Down Expand Up @@ -38,8 +38,7 @@ That's the "default with per-app customization" model: the base is canonical; an
|---|---|
| `tokens.css` | Variables — themes + density. **Source of truth for the look.** |
| `components.css` | Component styles, token-driven and theme-agnostic. |
| `index.html` | Link-based reference page. Open locally, or import into Claude Design. |
| `preview.html` | Generated self-contained snapshot for sharing (regenerate with `build_preview.py`). |
| `ds_streamlit.py` | Streamlit-native helpers for the same presentation patterns. |

## Theme → app mapping (proposed)

Expand All @@ -48,4 +47,4 @@ That's the "default with per-app customization" model: the base is canonical; an

## Status & next step

**STAGED** here in the tracker. Graduates to `Workflows/templates/consumer-repo/design-system/` with a `sync-manifest.yml` entry (`is_directory: true`; base tokens `template_sync: exact`) as a deliberate Workflows PR — see [`../PLAN.md`](../PLAN.md) §3.3. Claude Design imports from the Workflows path once graduated, enabling the prototype-in-Design → land-in-repo round-trip.
This kit is managed from `Workflows/templates/consumer-repo/design-system/` and distributed by Maint 68 through `.github/sync-manifest.yml`. Update it here first, then let the sync workflow replace consumer copies.
4 changes: 4 additions & 0 deletions design-system/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
.ds .q { display:inline-flex; width:15px; height:15px; border-radius:50%; border:1px solid var(--border); color:var(--muted); font-size:10px; align-items:center; justify-content:center; vertical-align:1px; margin-left:5px; cursor:help; }
.ds input[type=text], .ds select { width:100%; font:inherit; font-size:var(--fs-base); padding:var(--control-pad-y) var(--control-pad-x); color:var(--text); background:var(--input-bg); border:1px solid var(--border); border-radius:var(--input-radius); }
.ds input[type=range] { width:100%; accent-color:var(--accent); }
.ds a:focus-visible, .ds button:focus-visible, .ds input:focus-visible, .ds select:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }

.ds .btns { display:flex; flex-wrap:wrap; gap:var(--space-2); margin-top:var(--space-1); }
.ds button { font:inherit; font-size:var(--fs-sm); font-weight:600; padding:var(--control-pad-y) 15px; border-radius:var(--btn-radius); cursor:pointer; border:1px solid transparent; }
Expand Down Expand Up @@ -93,3 +94,6 @@
/* skeleton — loading placeholder so a pending surface never looks empty/broken. */
.ds .skeleton { background:linear-gradient(90deg,var(--panel) 25%,var(--border) 37%,var(--panel) 63%); background-size:400% 100%; border-radius:var(--radius-sm); animation:ds-shimmer 1.3s ease infinite; min-height:14px; }
@keyframes ds-shimmer { 0%{background-position:100% 0} 100%{background-position:0 0} }
@media (prefers-reduced-motion: reduce) {
.ds .skeleton { animation:none; background:var(--panel); }
}
38 changes: 27 additions & 11 deletions design-system/ds_streamlit.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

import logging
from collections.abc import Callable, Mapping
from contextlib import contextmanager
from html import escape
from typing import Any

logger = logging.getLogger("ds")
Expand Down Expand Up @@ -88,9 +90,12 @@ def empty_state(
clicked. NEVER pass an internal filename/path as `desc`."""
import streamlit as st

safe_icon = escape(str(icon))
safe_title = escape(str(title))
safe_desc = escape(str(desc))
st.markdown(
f"<div class='ds-empty'><div style='font-size:22px;opacity:.6'>{icon}</div>"
f"<div class='t'>{title}</div><div class='d'>{desc}</div></div>",
f"<div class='ds-empty'><div style='font-size:22px;opacity:.6'>{safe_icon}</div>"
f"<div class='t'>{safe_title}</div><div class='d'>{safe_desc}</div></div>",
unsafe_allow_html=True,
)
if cta_label:
Expand All @@ -107,12 +112,13 @@ def notice(kind: str, title: str = "", body: str = "", action: str | None = None
import streamlit as st

color, bg, ic = _NOTICE_STYLE.get(kind, _NOTICE_STYLE["info"])
head = f"<strong>{title}</strong><br>" if title else ""
act = f"<div style='margin-top:6px'>{action}</div>" if action else ""
head = f"<strong>{escape(str(title))}</strong><br>" if title else ""
act = f"<div style='margin-top:6px'>{escape(str(action))}</div>" if action else ""
safe_body = escape(str(body))
st.markdown(
f"<div class='ds-notice' style='background:{bg};border-color:{color}33'>"
f"<span class='ic' style='color:{color}'>{ic}</span>"
f"<div>{head}{body}{act}</div></div>",
f"<div>{head}{safe_body}{act}</div></div>",
unsafe_allow_html=True,
)

Expand All @@ -128,18 +134,19 @@ def translate_error(exc: Exception) -> tuple[str, str | None]:
Falls back to a generic message; the raw text is logged, not shown."""
logger.warning("ds.translate_error: %s", exc, exc_info=True)
text = str(exc)
text_lower = text.lower()
# Known field-required cases (extend per app as needed).
if "financing_mode" in text:
if "financing_mode" in text_lower:
return (
"Financing mode isn't set for this run.",
"Choose a financing mode (e.g. per-path) and run again.",
)
if "exceeds total capital" in text or "capital buffer" in text:
if "exceeds total capital" in text_lower or "capital buffer" in text_lower:
return (
"The capital allocation isn't feasible.",
"Reduce the internal allocation or volatility multiple to leave margin headroom.",
)
if "No investable funds" in text or "NO_FUNDS" in text:
if "no investable funds" in text_lower or "no_funds" in text_lower:
return (
"No funds matched the selection filters.",
"Try another preset or relax the selection settings.",
Expand All @@ -155,16 +162,25 @@ def dev_note(msg: str) -> None:
logger.info("ds.dev_note: %s", msg)


@contextmanager
def diagnostics_expander(label: str = "Diagnostics", *, expanded: bool = False):
"""P4 — explicit opt-in container for diagnostics that must be visible."""
import streamlit as st

with st.expander(label, expanded=expanded):
yield


def availability_badge(label: str) -> str:
"""P5 — markup for a small availability marker (use in a tab title/caption),
"""P5 — plain Streamlit-safe availability marker for tab titles/captions,
e.g. tab label f"Export {availability_badge('multi-period only')}"."""
return f"<span class='ds-badge'>{label}</span>"
return f" · {str(label).strip()}"


def humanize_id(raw: str, mapping: Mapping[str, str] | None = None) -> str:
"""P6 — decode an internal id to a human label; never show raw keys."""
if mapping and raw in mapping:
return mapping[raw]
# Best-effort: take a trailing human-ish segment, strip hashes.
tail = str(raw).replace("_", " ").split(":")[0].strip()
tail = str(raw).replace("_", " ").split(":")[-1].strip()
Comment on lines 174 to +185

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve backward compatibility for exported helper behavior.

availability_badge() (HTML → plain text) and humanize_id() (first-segment → last-segment) both change public output contracts. In this synced shared module, that is a high-risk integration break for existing consumers expecting prior behavior. Keep old defaults and make new behavior opt-in (or introduce new helper names) to avoid downstream UI regressions.

Proposed compatibility-oriented patch
 def availability_badge(label: str) -> str:
-    """P5 — plain Streamlit-safe availability marker for tab titles/captions,
-    e.g. tab label f"Export {availability_badge('multi-period only')}"."""
-    return f" · {str(label).strip()}"
+    """P5 — availability marker.
+    Backward-compatible default returns styled HTML; plain text is opt-in."""
+    normalized = str(label).strip()
+    return f"<span class='ds-badge'>{escape(normalized)}</span>"
 
 
-def humanize_id(raw: str, mapping: Mapping[str, str] | None = None) -> str:
+def humanize_id(
+    raw: str,
+    mapping: Mapping[str, str] | None = None,
+    *,
+    use_trailing_segment: bool = False,
+) -> str:
     """P6 — decode an internal id to a human label; never show raw keys."""
     if mapping and raw in mapping:
         return mapping[raw]
-    # Best-effort: take a trailing human-ish segment, strip hashes.
-    tail = str(raw).replace("_", " ").split(":")[-1].strip()
+    # Backward-compatible default keeps first segment unless explicitly overridden.
+    parts = str(raw).replace("_", " ").split(":")
+    tail = (parts[-1] if use_trailing_segment else parts[0]).strip()
     return tail or "item"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@design-system/ds_streamlit.py` around lines 174 - 185, The functions
availability_badge() and humanize_id() have changed their output behavior in
ways that break backward compatibility for existing consumers of this shared
module. For availability_badge(), add an optional parameter to control whether
HTML formatting is used (defaulting to the original HTML format). For
humanize_id(), add an optional parameter to control segment selection behavior
(defaulting to the original first-segment behavior). This way, existing callers
continue to work unchanged while new code can opt into the updated behavior by
passing the new parameter values.

Source: Linked repositories

return tail or "item"
4 changes: 2 additions & 2 deletions design-system/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
--radius: 10px; --radius-sm: 8px; --btn-radius: 8px; --input-radius: 8px;

/* ---- type ---- */
--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--font-body: system-ui, -apple-system, "BlinkMacSystemFont", "Segoe UI", "Roboto", "Helvetica", "Arial", sans-serif;
--font-heading: var(--font-body);
--heading-weight: 700;
}
Expand Down Expand Up @@ -71,6 +71,6 @@
--pos-weak:#eef3e6; --neg-weak:#f6e9e6; --warn-weak:#f7efe1; --info-weak:var(--accent-weak);
--card-border: 1px solid var(--border); --shadow: 0 1px 2px rgba(80,60,30,.06);
--radius:8px; --radius-sm:7px; --btn-radius:7px; --input-radius:7px;
--font-heading: Georgia, "Times New Roman", serif;
--font-heading: "Georgia", "Times New Roman", serif;
--heading-weight:600;
}
Loading