Spaces:
Sleeping
Sleeping
Pointf5ive commited on
Commit ·
efebee3
1
Parent(s): 08ae10d
Ground TOTEM scoring in fingerprint evidence
Browse files- app.py +26 -91
- src/totem_bridge.py +100 -9
app.py
CHANGED
|
@@ -244,67 +244,6 @@ footer { display: none !important; }
|
|
| 244 |
display: none !important;
|
| 245 |
}
|
| 246 |
|
| 247 |
-
/* ── Private scoring controls — dark themed (Stage 10) ─────────────────── */
|
| 248 |
-
#score-panel {
|
| 249 |
-
max-width: 1180px;
|
| 250 |
-
margin: 8px auto 24px auto;
|
| 251 |
-
padding: 0 18px;
|
| 252 |
-
}
|
| 253 |
-
|
| 254 |
-
#score-panel > .label-wrap,
|
| 255 |
-
#score-panel > details > summary,
|
| 256 |
-
#score-panel summary {
|
| 257 |
-
background: rgba(13, 19, 48, 0.95) !important;
|
| 258 |
-
color: rgba(246, 241, 232, 0.9) !important;
|
| 259 |
-
border: 1px solid rgba(242, 193, 78, 0.25) !important;
|
| 260 |
-
border-radius: 10px !important;
|
| 261 |
-
font-family: var(--font-ui) !important;
|
| 262 |
-
font-size: 13px !important;
|
| 263 |
-
padding: 10px 16px !important;
|
| 264 |
-
}
|
| 265 |
-
|
| 266 |
-
#score-panel .score-card {
|
| 267 |
-
border: 1px solid rgba(242, 193, 78, 0.18);
|
| 268 |
-
background: rgba(7, 18, 45, 0.97);
|
| 269 |
-
border-radius: 10px;
|
| 270 |
-
padding: 18px;
|
| 271 |
-
}
|
| 272 |
-
|
| 273 |
-
#score-panel h3 {
|
| 274 |
-
margin: 0 0 12px 0;
|
| 275 |
-
font-size: 16px;
|
| 276 |
-
color: var(--totem-gold);
|
| 277 |
-
font-family: var(--font-display);
|
| 278 |
-
}
|
| 279 |
-
|
| 280 |
-
#score-panel button {
|
| 281 |
-
border-radius: 8px !important;
|
| 282 |
-
font-weight: 700 !important;
|
| 283 |
-
background: rgba(138, 77, 255, 0.25) !important;
|
| 284 |
-
border: 1px solid rgba(138, 77, 255, 0.5) !important;
|
| 285 |
-
color: #f6f1e8 !important;
|
| 286 |
-
}
|
| 287 |
-
|
| 288 |
-
#score-panel .wrap {
|
| 289 |
-
gap: 12px;
|
| 290 |
-
}
|
| 291 |
-
|
| 292 |
-
#score-panel label,
|
| 293 |
-
#score-panel .block {
|
| 294 |
-
background: rgba(7, 18, 45, 0.8) !important;
|
| 295 |
-
border-color: rgba(246, 241, 232, 0.12) !important;
|
| 296 |
-
color: rgba(246, 241, 232, 0.9) !important;
|
| 297 |
-
border-radius: 8px !important;
|
| 298 |
-
}
|
| 299 |
-
|
| 300 |
-
#score-panel input,
|
| 301 |
-
#score-panel textarea {
|
| 302 |
-
background: rgba(4, 8, 26, 0.9) !important;
|
| 303 |
-
color: #f6f1e8 !important;
|
| 304 |
-
border-color: rgba(242, 193, 78, 0.2) !important;
|
| 305 |
-
border-radius: 6px !important;
|
| 306 |
-
}
|
| 307 |
-
|
| 308 |
/* ── Global Gradio component dark overrides (Stage 9) ──────────────────── */
|
| 309 |
.gradio-container input,
|
| 310 |
.gradio-container textarea,
|
|
@@ -3147,8 +3086,14 @@ def _bridge_verification_notes(debug: dict, llm_state: dict) -> dict:
|
|
| 3147 |
warnings.append("Fake audit provider returned these numbers; this should never be enabled on Hugging Face Spaces.")
|
| 3148 |
if not usage_reported:
|
| 3149 |
warnings.append("Provider did not report token usage, so API completion cannot be cost-audited from usage fields.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3150 |
if not str(llm_state.get("evidence_summary") or "").strip():
|
| 3151 |
warnings.append("Model returned no evidence_summary; score grounding is weak.")
|
|
|
|
|
|
|
| 3152 |
|
| 3153 |
scores = llm_state.get("scores") if isinstance(llm_state.get("scores"), dict) else {}
|
| 3154 |
rounded_scores = [score for score in scores.values() if isinstance(score, int) and score % 5 == 0]
|
|
@@ -3234,10 +3179,20 @@ def analyze_manuscript_for_dashboard(file_obj, active_path: str):
|
|
| 3234 |
works_sampled=works_sampled,
|
| 3235 |
)
|
| 3236 |
manuscript_ctx = extract_manuscript_context(file_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3237 |
manuscript_state = {
|
| 3238 |
"path": manuscript_ctx.path,
|
| 3239 |
"cleaned_text": manuscript_ctx.cleaned_text,
|
| 3240 |
"word_count": manuscript_ctx.word_count,
|
|
|
|
|
|
|
| 3241 |
}
|
| 3242 |
dashboard_status = (
|
| 3243 |
f"{status} | Draft Mode active: extraction complete. "
|
|
@@ -3267,8 +3222,14 @@ def run_totem_analysis_from_context(active_path: str, manuscript_context_json: s
|
|
| 3267 |
if not cleaned_text:
|
| 3268 |
raise BridgeError("Manuscript cleaned text is empty.")
|
| 3269 |
ctx_path = str(ctx_payload.get("path") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3270 |
if ctx_path and Path(ctx_path).exists():
|
| 3271 |
ctx = extract_manuscript_context(ctx_path)
|
|
|
|
|
|
|
| 3272 |
else:
|
| 3273 |
ctx = ManuscriptContext(
|
| 3274 |
path=ctx_path or "(session)",
|
|
@@ -3276,6 +3237,8 @@ def run_totem_analysis_from_context(active_path: str, manuscript_context_json: s
|
|
| 3276 |
cleaned_text=cleaned_text,
|
| 3277 |
word_count=len(cleaned_text.split()),
|
| 3278 |
page_trace=[],
|
|
|
|
|
|
|
| 3279 |
)
|
| 3280 |
|
| 3281 |
path = _validate_workbook_path(Path(active_path) if active_path else _preferred_workbook_path())
|
|
@@ -3295,6 +3258,8 @@ def run_totem_analysis_from_context(active_path: str, manuscript_context_json: s
|
|
| 3295 |
"api_key_env": debug.get("api_key_env"),
|
| 3296 |
"manuscript_words": debug.get("word_count"),
|
| 3297 |
"manuscript_path": ctx.path,
|
|
|
|
|
|
|
| 3298 |
"rubric_metrics": len(rubric.get("metrics", [])),
|
| 3299 |
"skill_loaded": debug.get("skill_loaded"),
|
| 3300 |
"skill_path": debug.get("skill_path"),
|
|
@@ -3483,26 +3448,6 @@ with gr.Blocks(title="TOTEM Studio") as demo:
|
|
| 3483 |
path_input = gr.Textbox(label="Local workbook path", value=str(_preferred_workbook_path()))
|
| 3484 |
path_button = gr.Button("Load Local Path", variant="primary")
|
| 3485 |
|
| 3486 |
-
with gr.Accordion("Private scoring controls", open=False, elem_id="score-panel"):
|
| 3487 |
-
gr.HTML("<div class='score-card'><h3>Live Score A Block</h3></div>")
|
| 3488 |
-
with gr.Row():
|
| 3489 |
-
sequence = gr.Textbox(label="Sequence", value="Live pass")
|
| 3490 |
-
stanza_id = gr.Textbox(label="Stanza ID", value="New block")
|
| 3491 |
-
draft_pass = gr.Textbox(label="Draft / Pass", value="First score")
|
| 3492 |
-
with gr.Row():
|
| 3493 |
-
clarity = gr.Slider(1, 10, value=7, step=0.5, label="Clarity")
|
| 3494 |
-
rhythm = gr.Slider(1, 10, value=7, step=0.5, label="Rhythm")
|
| 3495 |
-
flow = gr.Slider(1, 10, value=7, step=0.5, label="Read-aloud Flow")
|
| 3496 |
-
with gr.Row():
|
| 3497 |
-
emotional_truth = gr.Slider(1, 10, value=7, step=0.5, label="Emotional Truth")
|
| 3498 |
-
visual_strength = gr.Slider(1, 10, value=7, step=0.5, label="Visual Strength")
|
| 3499 |
-
commercial = gr.Slider(1, 10, value=7, step=0.5, label="Commercial Publishability")
|
| 3500 |
-
notes = gr.Textbox(label="Notes", lines=2)
|
| 3501 |
-
with gr.Row():
|
| 3502 |
-
single_button = gr.Button("Score Block", variant="primary")
|
| 3503 |
-
recalc_button = gr.Button("Recalculate Gates")
|
| 3504 |
-
export_button = gr.Button("Download Updated Workbook")
|
| 3505 |
-
single_df = gr.Dataframe(label="Latest scorecard", interactive=False, visible=False)
|
| 3506 |
score_status = gr.Markdown(elem_id="hidden-status")
|
| 3507 |
exported_file = gr.File(label="Export appears here", elem_id="hidden-export")
|
| 3508 |
|
|
@@ -3660,16 +3605,6 @@ with gr.Blocks(title="TOTEM Studio") as demo:
|
|
| 3660 |
outputs=[bridge_diag_log],
|
| 3661 |
js="""(v) => { if (v) { navigator.clipboard.writeText(v); } return v; }""",
|
| 3662 |
)
|
| 3663 |
-
recalc_button.click(recalc_log, inputs=[log_state, active_path],
|
| 3664 |
-
outputs=[dashboard, log_state, score_status])
|
| 3665 |
-
export_button.click(export_log, inputs=[log_state, active_path], outputs=[exported_file])
|
| 3666 |
-
single_button.click(
|
| 3667 |
-
single_score,
|
| 3668 |
-
inputs=[active_path, sequence, stanza_id, draft_pass, clarity, rhythm,
|
| 3669 |
-
flow, emotional_truth, visual_strength, commercial, notes],
|
| 3670 |
-
outputs=[single_df, score_status],
|
| 3671 |
-
)
|
| 3672 |
-
|
| 3673 |
# Codex Extractor tab
|
| 3674 |
codex_extract_btn.click(
|
| 3675 |
run_codex_extraction,
|
|
|
|
| 244 |
display: none !important;
|
| 245 |
}
|
| 246 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
/* ── Global Gradio component dark overrides (Stage 9) ──────────────────── */
|
| 248 |
.gradio-container input,
|
| 249 |
.gradio-container textarea,
|
|
|
|
| 3086 |
warnings.append("Fake audit provider returned these numbers; this should never be enabled on Hugging Face Spaces.")
|
| 3087 |
if not usage_reported:
|
| 3088 |
warnings.append("Provider did not report token usage, so API completion cannot be cost-audited from usage fields.")
|
| 3089 |
+
if not debug.get("fingerprint_supplied"):
|
| 3090 |
+
warnings.append("Manuscript fingerprint was not supplied to the API, so scores are not VM-grounded.")
|
| 3091 |
+
elif int(debug.get("fingerprint_metric_count") or 0) < 10:
|
| 3092 |
+
warnings.append("Manuscript fingerprint contains fewer VM metrics than expected; score grounding may be partial.")
|
| 3093 |
if not str(llm_state.get("evidence_summary") or "").strip():
|
| 3094 |
warnings.append("Model returned no evidence_summary; score grounding is weak.")
|
| 3095 |
+
if not isinstance(llm_state.get("score_evidence"), dict):
|
| 3096 |
+
warnings.append("Model returned no per-score evidence object; scores cannot be traced to workbook/fingerprint inputs.")
|
| 3097 |
|
| 3098 |
scores = llm_state.get("scores") if isinstance(llm_state.get("scores"), dict) else {}
|
| 3099 |
rounded_scores = [score for score in scores.values() if isinstance(score, int) and score % 5 == 0]
|
|
|
|
| 3179 |
works_sampled=works_sampled,
|
| 3180 |
)
|
| 3181 |
manuscript_ctx = extract_manuscript_context(file_path)
|
| 3182 |
+
try:
|
| 3183 |
+
fingerprint_payload = json.loads(json_out) if json_out else {}
|
| 3184 |
+
if not isinstance(fingerprint_payload, dict):
|
| 3185 |
+
fingerprint_payload = {}
|
| 3186 |
+
except Exception:
|
| 3187 |
+
fingerprint_payload = {}
|
| 3188 |
+
manuscript_ctx.fingerprint = fingerprint_payload
|
| 3189 |
+
manuscript_ctx.fingerprint_report = report
|
| 3190 |
manuscript_state = {
|
| 3191 |
"path": manuscript_ctx.path,
|
| 3192 |
"cleaned_text": manuscript_ctx.cleaned_text,
|
| 3193 |
"word_count": manuscript_ctx.word_count,
|
| 3194 |
+
"fingerprint": fingerprint_payload,
|
| 3195 |
+
"fingerprint_report": report,
|
| 3196 |
}
|
| 3197 |
dashboard_status = (
|
| 3198 |
f"{status} | Draft Mode active: extraction complete. "
|
|
|
|
| 3222 |
if not cleaned_text:
|
| 3223 |
raise BridgeError("Manuscript cleaned text is empty.")
|
| 3224 |
ctx_path = str(ctx_payload.get("path") or "").strip()
|
| 3225 |
+
fingerprint_payload = ctx_payload.get("fingerprint")
|
| 3226 |
+
if not isinstance(fingerprint_payload, dict):
|
| 3227 |
+
fingerprint_payload = {}
|
| 3228 |
+
fingerprint_report = str(ctx_payload.get("fingerprint_report") or "")
|
| 3229 |
if ctx_path and Path(ctx_path).exists():
|
| 3230 |
ctx = extract_manuscript_context(ctx_path)
|
| 3231 |
+
ctx.fingerprint = fingerprint_payload
|
| 3232 |
+
ctx.fingerprint_report = fingerprint_report
|
| 3233 |
else:
|
| 3234 |
ctx = ManuscriptContext(
|
| 3235 |
path=ctx_path or "(session)",
|
|
|
|
| 3237 |
cleaned_text=cleaned_text,
|
| 3238 |
word_count=len(cleaned_text.split()),
|
| 3239 |
page_trace=[],
|
| 3240 |
+
fingerprint=fingerprint_payload,
|
| 3241 |
+
fingerprint_report=fingerprint_report,
|
| 3242 |
)
|
| 3243 |
|
| 3244 |
path = _validate_workbook_path(Path(active_path) if active_path else _preferred_workbook_path())
|
|
|
|
| 3258 |
"api_key_env": debug.get("api_key_env"),
|
| 3259 |
"manuscript_words": debug.get("word_count"),
|
| 3260 |
"manuscript_path": ctx.path,
|
| 3261 |
+
"fingerprint_supplied": debug.get("fingerprint_supplied"),
|
| 3262 |
+
"fingerprint_metric_count": debug.get("fingerprint_metric_count"),
|
| 3263 |
"rubric_metrics": len(rubric.get("metrics", [])),
|
| 3264 |
"skill_loaded": debug.get("skill_loaded"),
|
| 3265 |
"skill_path": debug.get("skill_path"),
|
|
|
|
| 3448 |
path_input = gr.Textbox(label="Local workbook path", value=str(_preferred_workbook_path()))
|
| 3449 |
path_button = gr.Button("Load Local Path", variant="primary")
|
| 3450 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3451 |
score_status = gr.Markdown(elem_id="hidden-status")
|
| 3452 |
exported_file = gr.File(label="Export appears here", elem_id="hidden-export")
|
| 3453 |
|
|
|
|
| 3605 |
outputs=[bridge_diag_log],
|
| 3606 |
js="""(v) => { if (v) { navigator.clipboard.writeText(v); } return v; }""",
|
| 3607 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3608 |
# Codex Extractor tab
|
| 3609 |
codex_extract_btn.click(
|
| 3610 |
run_codex_extraction,
|
src/totem_bridge.py
CHANGED
|
@@ -62,6 +62,8 @@ class ManuscriptContext:
|
|
| 62 |
cleaned_text: str
|
| 63 |
word_count: int
|
| 64 |
page_trace: list[dict[str, Any]]
|
|
|
|
|
|
|
| 65 |
|
| 66 |
|
| 67 |
@dataclass
|
|
@@ -132,6 +134,16 @@ def load_totem_skill(skill_path: str | Path | None = None) -> tuple[str, str]:
|
|
| 132 |
def dashboard_output_contract() -> dict[str, Any]:
|
| 133 |
return {
|
| 134 |
"scores": {field: "integer 0..100" for field in SCORE_FIELDS},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
"gate": "HARD FAIL | SOFT FAIL | READ-ALOUD BLOCK | COMMERCIAL CHECK | GREENLIGHT | REVISE",
|
| 136 |
"weakest_metric": "one of: Clarity, Rhythm, Read-aloud Flow, Emotional Truth, Visual Strength, Commercial Viability",
|
| 137 |
"dashboard_message": "short dashboard status message, max 220 characters",
|
|
@@ -168,7 +180,7 @@ def run_totem_skill(
|
|
| 168 |
You are the hidden TOTEM Analysis skill inside TOTEM Studio.
|
| 169 |
Use the skill instructions below as the scoring method, but do not create a DOCX report for this dashboard run.
|
| 170 |
Return strict JSON only. No prose. No markdown. No code fences.
|
| 171 |
-
Use the workbook matrix as scoring authority and manuscript text as evidence.
|
| 172 |
|
| 173 |
--- TOTEM SKILL INSTRUCTIONS ---
|
| 174 |
{skill_text}
|
|
@@ -178,12 +190,17 @@ Use the workbook matrix as scoring authority and manuscript text as evidence.
|
|
| 178 |
user_payload = {
|
| 179 |
"task": "score_manuscript_against_workbook_matrix_for_dashboard",
|
| 180 |
"rubric_matrix": rubric_matrix,
|
|
|
|
|
|
|
| 181 |
"manuscript_cleaned_text": manuscript.cleaned_text,
|
| 182 |
"scoring_dimensions": SCORE_LABELS,
|
| 183 |
"required_output_contract": contract,
|
| 184 |
"instruction": (
|
| 185 |
"Return exactly one JSON object matching required_output_contract. "
|
| 186 |
"Scores must be 0..100 dashboard values for the six dimensions. "
|
|
|
|
|
|
|
|
|
|
| 187 |
"Do not write a report and do not include manuscript excerpts."
|
| 188 |
),
|
| 189 |
}
|
|
@@ -221,6 +238,8 @@ Use the workbook matrix as scoring authority and manuscript text as evidence.
|
|
| 221 |
"skill_path": skill_path,
|
| 222 |
"skill_loaded": True,
|
| 223 |
"schema_pass": True,
|
|
|
|
|
|
|
| 224 |
"token_audit_path": audit_path,
|
| 225 |
"token_audit_transaction_id": transaction_id,
|
| 226 |
"raw_model_payload": raw_payload,
|
|
@@ -350,6 +369,8 @@ Use the workbook matrix as scoring authority and manuscript text as evidence.
|
|
| 350 |
"skill_loaded": True,
|
| 351 |
"schema_pass": True,
|
| 352 |
"actual_tokens": actual_tokens,
|
|
|
|
|
|
|
| 353 |
"token_audit_path": audit_path,
|
| 354 |
"token_audit_transaction_id": transaction_id,
|
| 355 |
"raw_model_payload": parsed,
|
|
@@ -435,6 +456,7 @@ def _write_token_audit(
|
|
| 435 |
error: str | None,
|
| 436 |
) -> str:
|
| 437 |
rubric_json = compact_json(rubric_matrix)
|
|
|
|
| 438 |
contract_json = compact_json(contract)
|
| 439 |
user_wrapper_json = compact_json(
|
| 440 |
{
|
|
@@ -447,7 +469,7 @@ def _write_token_audit(
|
|
| 447 |
You are the hidden TOTEM Analysis skill inside TOTEM Studio.
|
| 448 |
Use the skill instructions as the scoring method, but do not create a DOCX report for this dashboard run.
|
| 449 |
Return strict JSON only. No prose. No markdown. No code fences.
|
| 450 |
-
Use the workbook matrix as scoring authority and manuscript text as evidence.
|
| 451 |
""".strip()
|
| 452 |
|
| 453 |
component_estimates = estimate_components(
|
|
@@ -455,6 +477,8 @@ Use the workbook matrix as scoring authority and manuscript text as evidence.
|
|
| 455 |
"system_wrapper": system_wrapper,
|
| 456 |
"skill_md": skill_text,
|
| 457 |
"workbook_rubric_json": rubric_json,
|
|
|
|
|
|
|
| 458 |
"manuscript_cleaned_text": manuscript.cleaned_text,
|
| 459 |
"output_contract_json": contract_json,
|
| 460 |
"user_wrapper_json": user_wrapper_json,
|
|
@@ -487,6 +511,12 @@ Use the workbook matrix as scoring authority and manuscript text as evidence.
|
|
| 487 |
"chars": len(manuscript.cleaned_text),
|
| 488 |
"word_count": manuscript.word_count,
|
| 489 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
"estimated_tokens": {
|
| 491 |
**component_estimates,
|
| 492 |
"wire_prompt": {
|
|
@@ -562,6 +592,11 @@ def validate_dashboard_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
| 562 |
}
|
| 563 |
out.update(out["scores"])
|
| 564 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
rpq = payload.get("revision_priority_queue")
|
| 566 |
if not isinstance(rpq, list):
|
| 567 |
raise BridgeValidationError("revision_priority_queue must be a list.")
|
|
@@ -574,6 +609,45 @@ def validate_dashboard_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
| 574 |
return out
|
| 575 |
|
| 576 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 577 |
def recompute_gate(metrics: dict[str, int], rubric_matrix: dict[str, Any]) -> str:
|
| 578 |
weights = rubric_matrix.get("weights", {}) if isinstance(rubric_matrix, dict) else {}
|
| 579 |
if not isinstance(weights, dict) or not weights:
|
|
@@ -656,14 +730,25 @@ def _fake_dashboard_payload(manuscript: ManuscriptContext) -> dict[str, Any]:
|
|
| 656 |
word_count = max(1, manuscript.word_count)
|
| 657 |
base = max(45, min(88, 62 + (word_count // 160)))
|
| 658 |
long_sentence_penalty = 8 if word_count > 850 else 3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
return {
|
| 660 |
-
"scores":
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
|
|
|
|
|
|
|
|
|
| 667 |
},
|
| 668 |
"gate": "REVISE",
|
| 669 |
"weakest_metric": "Rhythm",
|
|
@@ -711,3 +796,9 @@ def _clamp_score(value: Any) -> int:
|
|
| 711 |
except Exception as exc:
|
| 712 |
raise BridgeValidationError(f"Invalid numeric score value: {value!r}") from exc
|
| 713 |
return max(0, min(100, x))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
cleaned_text: str
|
| 63 |
word_count: int
|
| 64 |
page_trace: list[dict[str, Any]]
|
| 65 |
+
fingerprint: dict[str, Any] | None = None
|
| 66 |
+
fingerprint_report: str = ""
|
| 67 |
|
| 68 |
|
| 69 |
@dataclass
|
|
|
|
| 134 |
def dashboard_output_contract() -> dict[str, Any]:
|
| 135 |
return {
|
| 136 |
"scores": {field: "integer 0..100" for field in SCORE_FIELDS},
|
| 137 |
+
"score_evidence": {
|
| 138 |
+
field: {
|
| 139 |
+
"score": "integer 0..100 matching scores.<field>",
|
| 140 |
+
"fingerprint_metrics_used": ["VM metric ids used, e.g. VM-001, VM-010, VM-025"],
|
| 141 |
+
"workbook_rule": "target/threshold/rubric rule used from workbook matrix",
|
| 142 |
+
"manuscript_evidence": "short paraphrased manuscript evidence; no long excerpts",
|
| 143 |
+
"reason": "why this score follows from the workbook rule and fingerprint metrics",
|
| 144 |
+
}
|
| 145 |
+
for field in SCORE_FIELDS
|
| 146 |
+
},
|
| 147 |
"gate": "HARD FAIL | SOFT FAIL | READ-ALOUD BLOCK | COMMERCIAL CHECK | GREENLIGHT | REVISE",
|
| 148 |
"weakest_metric": "one of: Clarity, Rhythm, Read-aloud Flow, Emotional Truth, Visual Strength, Commercial Viability",
|
| 149 |
"dashboard_message": "short dashboard status message, max 220 characters",
|
|
|
|
| 180 |
You are the hidden TOTEM Analysis skill inside TOTEM Studio.
|
| 181 |
Use the skill instructions below as the scoring method, but do not create a DOCX report for this dashboard run.
|
| 182 |
Return strict JSON only. No prose. No markdown. No code fences.
|
| 183 |
+
Use the workbook matrix as scoring authority, manuscript fingerprint VM metrics as measured evidence, and manuscript text only as supporting evidence.
|
| 184 |
|
| 185 |
--- TOTEM SKILL INSTRUCTIONS ---
|
| 186 |
{skill_text}
|
|
|
|
| 190 |
user_payload = {
|
| 191 |
"task": "score_manuscript_against_workbook_matrix_for_dashboard",
|
| 192 |
"rubric_matrix": rubric_matrix,
|
| 193 |
+
"manuscript_fingerprint": manuscript.fingerprint or {},
|
| 194 |
+
"manuscript_fingerprint_report": manuscript.fingerprint_report[:6000] if manuscript.fingerprint_report else "",
|
| 195 |
"manuscript_cleaned_text": manuscript.cleaned_text,
|
| 196 |
"scoring_dimensions": SCORE_LABELS,
|
| 197 |
"required_output_contract": contract,
|
| 198 |
"instruction": (
|
| 199 |
"Return exactly one JSON object matching required_output_contract. "
|
| 200 |
"Scores must be 0..100 dashboard values for the six dimensions. "
|
| 201 |
+
"Every score_evidence entry must cite at least one VM metric id from manuscript_fingerprint "
|
| 202 |
+
"and one workbook rule/target from rubric_matrix. If the fingerprint is missing or insufficient, "
|
| 203 |
+
"return gate REVISE and explain the missing evidence in dashboard_message. "
|
| 204 |
"Do not write a report and do not include manuscript excerpts."
|
| 205 |
),
|
| 206 |
}
|
|
|
|
| 238 |
"skill_path": skill_path,
|
| 239 |
"skill_loaded": True,
|
| 240 |
"schema_pass": True,
|
| 241 |
+
"fingerprint_supplied": bool(manuscript.fingerprint),
|
| 242 |
+
"fingerprint_metric_count": _fingerprint_metric_count(manuscript.fingerprint),
|
| 243 |
"token_audit_path": audit_path,
|
| 244 |
"token_audit_transaction_id": transaction_id,
|
| 245 |
"raw_model_payload": raw_payload,
|
|
|
|
| 369 |
"skill_loaded": True,
|
| 370 |
"schema_pass": True,
|
| 371 |
"actual_tokens": actual_tokens,
|
| 372 |
+
"fingerprint_supplied": bool(manuscript.fingerprint),
|
| 373 |
+
"fingerprint_metric_count": _fingerprint_metric_count(manuscript.fingerprint),
|
| 374 |
"token_audit_path": audit_path,
|
| 375 |
"token_audit_transaction_id": transaction_id,
|
| 376 |
"raw_model_payload": parsed,
|
|
|
|
| 456 |
error: str | None,
|
| 457 |
) -> str:
|
| 458 |
rubric_json = compact_json(rubric_matrix)
|
| 459 |
+
fingerprint_json = compact_json(manuscript.fingerprint or {})
|
| 460 |
contract_json = compact_json(contract)
|
| 461 |
user_wrapper_json = compact_json(
|
| 462 |
{
|
|
|
|
| 469 |
You are the hidden TOTEM Analysis skill inside TOTEM Studio.
|
| 470 |
Use the skill instructions as the scoring method, but do not create a DOCX report for this dashboard run.
|
| 471 |
Return strict JSON only. No prose. No markdown. No code fences.
|
| 472 |
+
Use the workbook matrix as scoring authority, manuscript fingerprint VM metrics as measured evidence, and manuscript text only as supporting evidence.
|
| 473 |
""".strip()
|
| 474 |
|
| 475 |
component_estimates = estimate_components(
|
|
|
|
| 477 |
"system_wrapper": system_wrapper,
|
| 478 |
"skill_md": skill_text,
|
| 479 |
"workbook_rubric_json": rubric_json,
|
| 480 |
+
"manuscript_fingerprint_json": fingerprint_json,
|
| 481 |
+
"manuscript_fingerprint_report": manuscript.fingerprint_report or "",
|
| 482 |
"manuscript_cleaned_text": manuscript.cleaned_text,
|
| 483 |
"output_contract_json": contract_json,
|
| 484 |
"user_wrapper_json": user_wrapper_json,
|
|
|
|
| 511 |
"chars": len(manuscript.cleaned_text),
|
| 512 |
"word_count": manuscript.word_count,
|
| 513 |
},
|
| 514 |
+
"fingerprint": {
|
| 515 |
+
"supplied": bool(manuscript.fingerprint),
|
| 516 |
+
"sha256": stable_hash(manuscript.fingerprint or {}),
|
| 517 |
+
"metric_count": _fingerprint_metric_count(manuscript.fingerprint),
|
| 518 |
+
"report_chars": len(manuscript.fingerprint_report or ""),
|
| 519 |
+
},
|
| 520 |
"estimated_tokens": {
|
| 521 |
**component_estimates,
|
| 522 |
"wire_prompt": {
|
|
|
|
| 592 |
}
|
| 593 |
out.update(out["scores"])
|
| 594 |
|
| 595 |
+
score_evidence = payload.get("score_evidence")
|
| 596 |
+
if not isinstance(score_evidence, dict):
|
| 597 |
+
raise BridgeValidationError("score_evidence must be a JSON object keyed by scoring dimension.")
|
| 598 |
+
out["score_evidence"] = _normalize_score_evidence(score_evidence, out["scores"])
|
| 599 |
+
|
| 600 |
rpq = payload.get("revision_priority_queue")
|
| 601 |
if not isinstance(rpq, list):
|
| 602 |
raise BridgeValidationError("revision_priority_queue must be a list.")
|
|
|
|
| 609 |
return out
|
| 610 |
|
| 611 |
|
| 612 |
+
def _normalize_score_evidence(evidence: dict[str, Any], scores: dict[str, int]) -> dict[str, dict[str, Any]]:
|
| 613 |
+
normalized: dict[str, dict[str, Any]] = {}
|
| 614 |
+
missing: list[str] = []
|
| 615 |
+
weak: list[str] = []
|
| 616 |
+
for field in SCORE_FIELDS:
|
| 617 |
+
item = evidence.get(field)
|
| 618 |
+
if not isinstance(item, dict):
|
| 619 |
+
missing.append(field)
|
| 620 |
+
continue
|
| 621 |
+
|
| 622 |
+
metrics = item.get("fingerprint_metrics_used") or item.get("vm_metrics_used") or []
|
| 623 |
+
if isinstance(metrics, str):
|
| 624 |
+
metrics = [metrics]
|
| 625 |
+
metric_ids = [str(x).strip() for x in metrics if str(x).strip()]
|
| 626 |
+
if not metric_ids or not any(m.upper().startswith("VM-") for m in metric_ids):
|
| 627 |
+
weak.append(field)
|
| 628 |
+
|
| 629 |
+
workbook_rule = str(item.get("workbook_rule") or "").strip()[:260]
|
| 630 |
+
manuscript_evidence = str(item.get("manuscript_evidence") or "").strip()[:260]
|
| 631 |
+
reason = str(item.get("reason") or "").strip()[:320]
|
| 632 |
+
if not workbook_rule or not reason:
|
| 633 |
+
weak.append(field)
|
| 634 |
+
|
| 635 |
+
normalized[field] = {
|
| 636 |
+
"score": scores[field],
|
| 637 |
+
"fingerprint_metrics_used": metric_ids[:8],
|
| 638 |
+
"workbook_rule": workbook_rule,
|
| 639 |
+
"manuscript_evidence": manuscript_evidence,
|
| 640 |
+
"reason": reason,
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
if missing:
|
| 644 |
+
raise BridgeValidationError(f"Missing score_evidence fields: {', '.join(missing)}")
|
| 645 |
+
if weak:
|
| 646 |
+
deduped = ", ".join(dict.fromkeys(weak))
|
| 647 |
+
raise BridgeValidationError(f"score_evidence lacks VM metric/workbook grounding for: {deduped}")
|
| 648 |
+
return normalized
|
| 649 |
+
|
| 650 |
+
|
| 651 |
def recompute_gate(metrics: dict[str, int], rubric_matrix: dict[str, Any]) -> str:
|
| 652 |
weights = rubric_matrix.get("weights", {}) if isinstance(rubric_matrix, dict) else {}
|
| 653 |
if not isinstance(weights, dict) or not weights:
|
|
|
|
| 730 |
word_count = max(1, manuscript.word_count)
|
| 731 |
base = max(45, min(88, 62 + (word_count // 160)))
|
| 732 |
long_sentence_penalty = 8 if word_count > 850 else 3
|
| 733 |
+
scores = {
|
| 734 |
+
"clarity": max(0, min(100, base - long_sentence_penalty)),
|
| 735 |
+
"rhythm": max(0, min(100, base - 11)),
|
| 736 |
+
"read_aloud_flow": max(0, min(100, base - 7)),
|
| 737 |
+
"emotional_truth": max(0, min(100, base + 5)),
|
| 738 |
+
"visual_strength": max(0, min(100, base + 8)),
|
| 739 |
+
"commercial_viability": max(0, min(100, base + 1)),
|
| 740 |
+
}
|
| 741 |
return {
|
| 742 |
+
"scores": scores,
|
| 743 |
+
"score_evidence": {
|
| 744 |
+
field: {
|
| 745 |
+
"score": score,
|
| 746 |
+
"fingerprint_metrics_used": ["VM-024"],
|
| 747 |
+
"workbook_rule": "Fake provider test rule; real runs must cite workbook targets.",
|
| 748 |
+
"manuscript_evidence": "Fake provider uses word-count-only smoke evidence.",
|
| 749 |
+
"reason": f"Fake audit score for {field} derived from {word_count} words.",
|
| 750 |
+
}
|
| 751 |
+
for field, score in scores.items()
|
| 752 |
},
|
| 753 |
"gate": "REVISE",
|
| 754 |
"weakest_metric": "Rhythm",
|
|
|
|
| 796 |
except Exception as exc:
|
| 797 |
raise BridgeValidationError(f"Invalid numeric score value: {value!r}") from exc
|
| 798 |
return max(0, min(100, x))
|
| 799 |
+
|
| 800 |
+
|
| 801 |
+
def _fingerprint_metric_count(fingerprint: dict[str, Any] | None) -> int:
|
| 802 |
+
if not isinstance(fingerprint, dict):
|
| 803 |
+
return 0
|
| 804 |
+
return sum(1 for key in fingerprint if str(key).upper().startswith("VM-"))
|