JacobLinCool Codex commited on
Commit
5c78c83
·
verified ·
1 Parent(s): 2b2e65d

feat: export submission packet

Browse files

Co-authored-by: Codex <noreply@openai.com>

README.md CHANGED
@@ -82,6 +82,13 @@ turns. Each included turn yields a tool-call example and an advisor-response exa
82
  selected targets, parsed XML tool call, tool observations, and score context preserved. This prepares the Well-Tuned
83
  path without claiming that the adapter has already been trained or published.
84
 
 
 
 
 
 
 
 
85
  ## Prize Ledger
86
 
87
  `/api/prize-ledger` and the in-app Prize Ledger panel expose submission evidence: the documented model stack, total
 
82
  selected targets, parsed XML tool call, tool observations, and score context preserved. This prepares the Well-Tuned
83
  path without claiming that the adapter has already been trained or published.
84
 
85
+ ## Submission Packet
86
+
87
+ The `submission_packet` Gradio API endpoint and `Packet` button export a Markdown submission bundle for the current
88
+ session: live links, snapshot provenance, a timed demo script, artifact checklist, Prize Ledger evidence, model budget,
89
+ session trace summary, social post draft, and open badge gaps. This keeps the final submission story tied to the same
90
+ auditable state as the app instead of a separate hand-curated checklist.
91
+
92
  ## Prize Ledger
93
 
94
  `/api/prize-ledger` and the in-app Prize Ledger panel expose submission evidence: the documented model stack, total
app.py CHANGED
@@ -14,6 +14,7 @@ from hackathon_advisor.data import ProjectIndex
14
  from hackathon_advisor.field_notes import build_field_notes_markdown
15
  from hackathon_advisor.lora_dataset import build_lora_dataset_jsonl
16
  from hackathon_advisor.prize_ledger import prize_ledger
 
17
  from hackathon_advisor.tool_contracts import resolve_tool_call, tool_schemas
18
  from hackathon_advisor.tools import TARGETS
19
  from hackathon_advisor.trace_export import build_trace_jsonl, trace_metadata
@@ -150,6 +151,23 @@ def lora_dataset_artifact(session_json: str = "{}") -> str:
150
  )
151
 
152
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  @app.api(name="agent_turn", concurrency_limit=4, stream_every=0.04)
154
  def agent_turn(message: str, session_json: str = "{}") -> Iterator[str]:
155
  try:
 
14
  from hackathon_advisor.field_notes import build_field_notes_markdown
15
  from hackathon_advisor.lora_dataset import build_lora_dataset_jsonl
16
  from hackathon_advisor.prize_ledger import prize_ledger
17
+ from hackathon_advisor.submission_packet import build_submission_packet_markdown
18
  from hackathon_advisor.tool_contracts import resolve_tool_call, tool_schemas
19
  from hackathon_advisor.tools import TARGETS
20
  from hackathon_advisor.trace_export import build_trace_jsonl, trace_metadata
 
151
  )
152
 
153
 
154
+ @app.api(name="submission_packet", concurrency_limit=8)
155
+ def submission_packet_artifact(session_json: str = "{}") -> str:
156
+ try:
157
+ session = json.loads(session_json or "{}")
158
+ except json.JSONDecodeError:
159
+ session = {}
160
+ runtime_status = engine.runtime_status()
161
+ return build_submission_packet_markdown(
162
+ session,
163
+ {
164
+ **trace_metadata(index),
165
+ "project_count": len(index.projects),
166
+ },
167
+ prize_ledger(runtime_status),
168
+ )
169
+
170
+
171
  @app.api(name="agent_turn", concurrency_limit=4, stream_every=0.04)
172
  def agent_turn(message: str, session_json: str = "{}") -> Iterator[str]:
173
  try:
hackathon_advisor/submission_packet.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Any
5
+
6
+
7
+ SPACE_URL = "https://huggingface.co/spaces/build-small-hackathon/hackathon-advisor"
8
+ LIVE_URL = "https://build-small-hackathon-hackathon-advisor.hf.space"
9
+ GITHUB_URL = "https://github.com/JacobLinCool/hackathon-advisor"
10
+
11
+
12
+ def build_submission_packet_markdown(
13
+ session: dict[str, Any],
14
+ metadata: dict[str, Any],
15
+ ledger: dict[str, Any],
16
+ ) -> str:
17
+ ideas = _list_of_dicts(session.get("ideas"))
18
+ trace = _list_of_dicts(session.get("trace"))
19
+ targets = [str(target) for target in session.get("targets") or []]
20
+ last_plan = [str(step) for step in session.get("last_plan") or []]
21
+ current = _current_idea(session, ideas)
22
+ score = current.get("score") if isinstance(current.get("score"), dict) else {}
23
+ verdict = _clean(score.get("verdict")) if score else "DRAFT"
24
+ overall = _clean(score.get("overall")) if score else "0.0"
25
+ title = _clean(current.get("title") or "Unwritten Page")
26
+
27
+ lines = [
28
+ "# Hackathon Advisor Submission Packet",
29
+ "",
30
+ f"Generated: {datetime.now(timezone.utc).isoformat(timespec='seconds')}",
31
+ "",
32
+ "## Links",
33
+ "",
34
+ f"- Live Space: {LIVE_URL}",
35
+ f"- Hugging Face repository: {SPACE_URL}",
36
+ f"- GitHub repository: {GITHUB_URL}",
37
+ "",
38
+ "## Snapshot",
39
+ "",
40
+ f"- Project snapshot: {_clean(metadata.get('snapshot_generated_at'))}",
41
+ f"- Project count: {_clean(metadata.get('project_count', 'unknown'))}",
42
+ f"- Index: {_clean(metadata.get('index_algorithm'))} at {_clean(metadata.get('index_generated_at'))}",
43
+ f"- Digest: `{_clean(metadata.get('snapshot_digest'))}`",
44
+ "",
45
+ "## Current Demo Page",
46
+ "",
47
+ f"- Title: {title}",
48
+ f"- Verdict: {verdict} {overall}/10",
49
+ f"- Targets: {', '.join(targets) if targets else 'No specific targets'}",
50
+ f"- Pitch: {_clean(current.get('pitch')) or 'No pitch recorded.'}",
51
+ "",
52
+ ]
53
+
54
+ lines.extend(_demo_script(title, verdict, overall, current, trace, last_plan))
55
+ lines.extend(_artifact_checklist(trace, ideas, session))
56
+ lines.extend(_prize_evidence(ledger))
57
+ lines.extend(_model_budget(ledger))
58
+ lines.extend(_session_summary(ideas, trace, last_plan))
59
+ lines.extend(_social_post(title, verdict, ledger))
60
+ lines.extend(_open_gaps(ledger))
61
+
62
+ return "\n".join(lines).rstrip() + "\n"
63
+
64
+
65
+ def _demo_script(
66
+ title: str,
67
+ verdict: str,
68
+ overall: str,
69
+ idea: dict[str, Any],
70
+ trace: list[dict[str, Any]],
71
+ last_plan: list[str],
72
+ ) -> list[str]:
73
+ echoes = _echoes(idea)
74
+ first_echo = echoes[0] if echoes else {}
75
+ project = first_echo.get("project") if isinstance(first_echo.get("project"), dict) else {}
76
+ echo_title = _clean(project.get("title") or project.get("id") or "a cited Space")
77
+ page = _clean(first_echo.get("page_number")) or "?"
78
+ plan_step = _clean(last_plan[0]) if last_plan else "Press Plan to show the build path."
79
+ turn_count = len(trace)
80
+ return [
81
+ "## Demo Script",
82
+ "",
83
+ "| Time | Beat | On-screen proof |",
84
+ "| --- | --- | --- |",
85
+ f"| 0:00 | Open The Unwritten Almanac and point to the local snapshot count. | Snapshot metadata and Prize Ledger are visible. |",
86
+ f"| 0:10 | Type the project instinct for `{title}`. | Streaming response starts, then the seal reads {verdict} at {overall}/10. |",
87
+ f"| 0:30 | Show the nearest echo. | Page {page}: {echo_title}. |",
88
+ f"| 0:45 | Press Gap or Plan to move from diagnosis to build path. | {plan_step} |",
89
+ f"| 1:05 | Export evidence artifacts. | JSONL, Notes, Chapter, LoRA, Packet, and PNG buttons are available after {turn_count} recorded turns. |",
90
+ "| 1:20 | Close with prize honesty. | Ready badges and planned badges are separated in the Prize Ledger. |",
91
+ "",
92
+ ]
93
+
94
+
95
+ def _artifact_checklist(trace: list[dict[str, Any]], ideas: list[dict[str, Any]], session: dict[str, Any]) -> list[str]:
96
+ has_trace = bool(trace)
97
+ has_ideas = bool(ideas)
98
+ has_artifact = isinstance(session.get("last_artifact"), dict) and bool(session["last_artifact"].get("title"))
99
+ rows = [
100
+ ("Tool trace JSONL", has_trace, "trace_artifact"),
101
+ ("Field Notes markdown", has_trace, "field_notes"),
102
+ ("Almanac chapter markdown", has_ideas, "chapter"),
103
+ ("MiniCPM5 LoRA SFT JSONL", has_trace, "lora_dataset"),
104
+ ("Submission packet markdown", True, "submission_packet"),
105
+ ("Fate page PNG", has_artifact, "client-side canvas export"),
106
+ ]
107
+ lines = ["## Artifact Checklist", "", "| Artifact | Status | Source |", "| --- | --- | --- |"]
108
+ for name, ready, source in rows:
109
+ lines.append(f"| {name} | {'ready' if ready else 'needs session'} | {source} |")
110
+ lines.append("")
111
+ return lines
112
+
113
+
114
+ def _prize_evidence(ledger: dict[str, Any]) -> list[str]:
115
+ lines = ["## Prize Evidence", "", "| Prize path | Status | Evidence |", "| --- | --- | --- |"]
116
+ for badge in _list_of_dicts(ledger.get("badges")):
117
+ lines.append(
118
+ "| "
119
+ + " | ".join(
120
+ [
121
+ _clean(badge.get("name")),
122
+ _clean(badge.get("status")),
123
+ _clean(badge.get("evidence")),
124
+ ]
125
+ )
126
+ + " |"
127
+ )
128
+ lines.append("")
129
+ return lines
130
+
131
+
132
+ def _model_budget(ledger: dict[str, Any]) -> list[str]:
133
+ total = _clean(ledger.get("total_params_b"))
134
+ limit = _clean(ledger.get("tiny_titan_limit_b"))
135
+ eligible = "yes" if ledger.get("tiny_titan_eligible") else "no"
136
+ lines = [
137
+ "## Model Budget",
138
+ "",
139
+ f"- Total documented stack: {total}B params",
140
+ f"- Tiny Titan limit: {limit}B params",
141
+ f"- Tiny Titan eligible: {eligible}",
142
+ "",
143
+ "| Role | Model | Params | Status | Runtime |",
144
+ "| --- | --- | --- | --- | --- |",
145
+ ]
146
+ for item in _list_of_dicts(ledger.get("model_stack")):
147
+ lines.append(
148
+ "| "
149
+ + " | ".join(
150
+ [
151
+ _clean(item.get("role")),
152
+ _clean(item.get("model")),
153
+ f"{_clean(item.get('params_b'))}B",
154
+ _clean(item.get("status")),
155
+ _clean(item.get("runtime")),
156
+ ]
157
+ )
158
+ + " |"
159
+ )
160
+ lines.append("")
161
+ return lines
162
+
163
+
164
+ def _session_summary(ideas: list[dict[str, Any]], trace: list[dict[str, Any]], last_plan: list[str]) -> list[str]:
165
+ lines = ["## Session Evidence", ""]
166
+ if ideas:
167
+ lines.extend(["### Idea Board", ""])
168
+ for index, idea in enumerate(ideas[-4:], start=max(1, len(ideas) - 3)):
169
+ score = idea.get("score") if isinstance(idea.get("score"), dict) else {}
170
+ verdict = _clean(score.get("verdict")) if score else "DRAFT"
171
+ overall = _clean(score.get("overall")) if score else "0.0"
172
+ lines.append(f"- {index}. {_clean(idea.get('title'))}: {verdict} {overall}/10")
173
+ else:
174
+ lines.append("- No ideas recorded yet.")
175
+
176
+ lines.extend(["", "### Latest Build Plan", ""])
177
+ if last_plan:
178
+ for index, step in enumerate(last_plan, start=1):
179
+ lines.append(f"{index}. {_clean(step)}")
180
+ else:
181
+ lines.append("No build plan recorded yet.")
182
+
183
+ lines.extend(["", "### Tool Trace", ""])
184
+ if trace:
185
+ for index, event in enumerate(trace[-5:], start=max(1, len(trace) - 4)):
186
+ tools = " -> ".join(
187
+ _clean(tool.get("name")) for tool in _list_of_dicts(event.get("tools")) if tool.get("name")
188
+ )
189
+ lines.append(f"- Turn {index}: {_clean(event.get('input'))} [{tools or 'reply'}]")
190
+ else:
191
+ lines.append("- No tool trace recorded yet.")
192
+ lines.append("")
193
+ return lines
194
+
195
+
196
+ def _social_post(title: str, verdict: str, ledger: dict[str, Any]) -> list[str]:
197
+ ready = sum(1 for badge in _list_of_dicts(ledger.get("badges")) if badge.get("status") == "ready")
198
+ total = len(_list_of_dicts(ledger.get("badges")))
199
+ return [
200
+ "## Social Post Draft",
201
+ "",
202
+ (
203
+ "I built Hackathon Advisor, a small-model originality coach for Build Small. "
204
+ f"The Unwritten Almanac checks `{title}` against a local snapshot of real Spaces, cites overlap, "
205
+ f"and exports the evidence trail. Latest seal: {verdict}. Prize ledger: {ready}/{total} ready."
206
+ ),
207
+ "",
208
+ f"Live: {LIVE_URL}",
209
+ "",
210
+ ]
211
+
212
+
213
+ def _open_gaps(ledger: dict[str, Any]) -> list[str]:
214
+ pending = [
215
+ badge
216
+ for badge in _list_of_dicts(ledger.get("badges"))
217
+ if badge.get("status") not in {"ready", "eligible"}
218
+ ]
219
+ lines = ["## Open Gaps", ""]
220
+ if pending:
221
+ for badge in pending:
222
+ lines.append(f"- {_clean(badge.get('name'))}: {_clean(badge.get('status'))} - {_clean(badge.get('evidence'))}")
223
+ else:
224
+ lines.append("- No non-ready badge states reported by the current ledger.")
225
+ lines.append("")
226
+ return lines
227
+
228
+
229
+ def _current_idea(session: dict[str, Any], ideas: list[dict[str, Any]]) -> dict[str, Any]:
230
+ current_id = _clean(session.get("current_idea_id"))
231
+ if current_id:
232
+ for idea in ideas:
233
+ if _clean(idea.get("id")) == current_id:
234
+ return idea
235
+ if ideas:
236
+ return ideas[-1]
237
+ return {}
238
+
239
+
240
+ def _echoes(idea: dict[str, Any]) -> list[dict[str, Any]]:
241
+ score = idea.get("score") if isinstance(idea.get("score"), dict) else {}
242
+ return _list_of_dicts(score.get("echoes"))
243
+
244
+
245
+ def _list_of_dicts(value: Any) -> list[dict[str, Any]]:
246
+ if not isinstance(value, list):
247
+ return []
248
+ return [item for item in value if isinstance(item, dict)]
249
+
250
+
251
+ def _clean(value: Any) -> str:
252
+ if value is None:
253
+ return ""
254
+ return " ".join(str(value).split())
static/app.js CHANGED
@@ -23,6 +23,7 @@ const exportTraceButton = document.querySelector("#export-trace");
23
  const exportNotesButton = document.querySelector("#export-notes");
24
  const exportChapterButton = document.querySelector("#export-chapter");
25
  const exportLoraButton = document.querySelector("#export-lora");
 
26
  const resetButton = document.querySelector("#reset-session");
27
 
28
  const SESSION_STORAGE_KEY = "hackathon-advisor-session-v1";
@@ -71,6 +72,10 @@ exportLoraButton.addEventListener("click", async () => {
71
  await exportLoraDataset();
72
  });
73
 
 
 
 
 
74
  resetButton.addEventListener("click", () => {
75
  clearSavedSession();
76
  window.location.reload();
@@ -189,6 +194,7 @@ function renderRestoredSession(data) {
189
  exportNotesButton.disabled = !(session.trace?.length);
190
  exportChapterButton.disabled = !(session.ideas?.length);
191
  exportLoraButton.disabled = !(session.trace?.length);
 
192
  }
193
 
194
  function readSavedSession() {
@@ -372,6 +378,7 @@ function handleEvent(event) {
372
  exportNotesButton.disabled = !(session.trace?.length);
373
  exportChapterButton.disabled = !(session.ideas?.length);
374
  exportLoraButton.disabled = !(session.trace?.length);
 
375
  saveSession();
376
  }
377
  }
@@ -548,13 +555,15 @@ function setCommandDisabled(disabled) {
548
  const isNotes = button.id === "export-notes";
549
  const isChapter = button.id === "export-chapter";
550
  const isLora = button.id === "export-lora";
 
551
  button.disabled =
552
  disabled ||
553
  (isArtifact && !currentArtifact) ||
554
  (isTrace && !session.trace?.length) ||
555
  (isNotes && !session.trace?.length) ||
556
  (isChapter && !session.ideas?.length) ||
557
- (isLora && !session.trace?.length);
 
558
  });
559
  }
560
 
@@ -627,6 +636,15 @@ async function exportLoraDataset() {
627
  downloadText("hackathon-advisor-lora-sft.jsonl", String(data || ""));
628
  }
629
 
 
 
 
 
 
 
 
 
 
630
  function exportArtifact(artifact) {
631
  const canvas = document.createElement("canvas");
632
  canvas.width = 1200;
 
23
  const exportNotesButton = document.querySelector("#export-notes");
24
  const exportChapterButton = document.querySelector("#export-chapter");
25
  const exportLoraButton = document.querySelector("#export-lora");
26
+ const exportPacketButton = document.querySelector("#export-packet");
27
  const resetButton = document.querySelector("#reset-session");
28
 
29
  const SESSION_STORAGE_KEY = "hackathon-advisor-session-v1";
 
72
  await exportLoraDataset();
73
  });
74
 
75
+ exportPacketButton.addEventListener("click", async () => {
76
+ await exportSubmissionPacket();
77
+ });
78
+
79
  resetButton.addEventListener("click", () => {
80
  clearSavedSession();
81
  window.location.reload();
 
194
  exportNotesButton.disabled = !(session.trace?.length);
195
  exportChapterButton.disabled = !(session.ideas?.length);
196
  exportLoraButton.disabled = !(session.trace?.length);
197
+ exportPacketButton.disabled = !(session.trace?.length);
198
  }
199
 
200
  function readSavedSession() {
 
378
  exportNotesButton.disabled = !(session.trace?.length);
379
  exportChapterButton.disabled = !(session.ideas?.length);
380
  exportLoraButton.disabled = !(session.trace?.length);
381
+ exportPacketButton.disabled = !(session.trace?.length);
382
  saveSession();
383
  }
384
  }
 
555
  const isNotes = button.id === "export-notes";
556
  const isChapter = button.id === "export-chapter";
557
  const isLora = button.id === "export-lora";
558
+ const isPacket = button.id === "export-packet";
559
  button.disabled =
560
  disabled ||
561
  (isArtifact && !currentArtifact) ||
562
  (isTrace && !session.trace?.length) ||
563
  (isNotes && !session.trace?.length) ||
564
  (isChapter && !session.ideas?.length) ||
565
+ (isLora && !session.trace?.length) ||
566
+ (isPacket && !session.trace?.length);
567
  });
568
  }
569
 
 
636
  downloadText("hackathon-advisor-lora-sft.jsonl", String(data || ""));
637
  }
638
 
639
+ async function exportSubmissionPacket() {
640
+ const client = await clientPromise;
641
+ const result = await client.predict("/submission_packet", {
642
+ session_json: JSON.stringify(session),
643
+ });
644
+ const data = Array.isArray(result.data) ? result.data[0] : result.data;
645
+ downloadText("hackathon-advisor-submission-packet.md", String(data || ""), "text/markdown;charset=utf-8");
646
+ }
647
+
648
  function exportArtifact(artifact) {
649
  const canvas = document.createElement("canvas");
650
  canvas.width = 1200;
static/index.html CHANGED
@@ -36,6 +36,7 @@
36
  <button type="button" id="export-notes" title="Export Field Notes" disabled>Notes</button>
37
  <button type="button" id="export-chapter" title="Export the Almanac chapter" disabled>Chapter</button>
38
  <button type="button" id="export-lora" title="Export the LoRA SFT dataset" disabled>LoRA</button>
 
39
  <button type="button" id="export-artifact" title="Export the current fate page" disabled>PNG</button>
40
  <button type="button" id="reset-session" title="Clear the saved session">Reset</button>
41
  </div>
 
36
  <button type="button" id="export-notes" title="Export Field Notes" disabled>Notes</button>
37
  <button type="button" id="export-chapter" title="Export the Almanac chapter" disabled>Chapter</button>
38
  <button type="button" id="export-lora" title="Export the LoRA SFT dataset" disabled>LoRA</button>
39
+ <button type="button" id="export-packet" title="Export the submission packet" disabled>Packet</button>
40
  <button type="button" id="export-artifact" title="Export the current fate page" disabled>PNG</button>
41
  <button type="button" id="reset-session" title="Clear the saved session">Reset</button>
42
  </div>
tests/test_app.py CHANGED
@@ -10,6 +10,7 @@ from app import (
10
  lora_dataset_artifact,
11
  prize_ledger_endpoint,
12
  runtime,
 
13
  tool_contract_check,
14
  tool_contracts,
15
  trace_artifact,
@@ -94,6 +95,21 @@ def test_lora_dataset_endpoint_exports_sft_jsonl() -> None:
94
  assert lines[2]["example_kind"] == "advisor_response"
95
 
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  def test_tool_contracts_endpoint_exposes_schemas() -> None:
98
  payload = tool_contracts()
99
 
 
10
  lora_dataset_artifact,
11
  prize_ledger_endpoint,
12
  runtime,
13
+ submission_packet_artifact,
14
  tool_contract_check,
15
  tool_contracts,
16
  trace_artifact,
 
95
  assert lines[2]["example_kind"] == "advisor_response"
96
 
97
 
98
+ def test_submission_packet_endpoint_exports_markdown() -> None:
99
+ state = engine.turn(
100
+ "A local-first archive cartographer for family photos",
101
+ {"targets": ["Field Notes"]},
102
+ ).state
103
+ state = engine.turn("make a build plan", state).state
104
+
105
+ payload = submission_packet_artifact(json.dumps(state))
106
+
107
+ assert payload.startswith("# Hackathon Advisor Submission Packet")
108
+ assert "## Demo Script" in payload
109
+ assert "## Prize Evidence" in payload
110
+ assert "Live Space:" in payload
111
+
112
+
113
  def test_tool_contracts_endpoint_exposes_schemas() -> None:
114
  payload = tool_contracts()
115
 
tests/test_submission_packet.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ from hackathon_advisor.agent import AdvisorEngine
4
+ from hackathon_advisor.data import ProjectIndex
5
+ from hackathon_advisor.prize_ledger import prize_ledger
6
+ from hackathon_advisor.submission_packet import build_submission_packet_markdown
7
+ from hackathon_advisor.trace_export import trace_metadata
8
+
9
+
10
+ def test_submission_packet_contains_demo_and_prize_evidence() -> None:
11
+ index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
12
+ engine = AdvisorEngine(index)
13
+ state = {"targets": ["Well-Tuned", "Field Notes"]}
14
+ state = engine.turn("A local-first archive cartographer for family photos", state).state
15
+ state = engine.turn("make a build plan", state).state
16
+
17
+ markdown = build_submission_packet_markdown(
18
+ state,
19
+ {
20
+ **trace_metadata(index),
21
+ "project_count": len(index.projects),
22
+ },
23
+ prize_ledger(engine.runtime_status()),
24
+ )
25
+
26
+ assert markdown.startswith("# Hackathon Advisor Submission Packet")
27
+ assert "## Demo Script" in markdown
28
+ assert "## Artifact Checklist" in markdown
29
+ assert "## Prize Evidence" in markdown
30
+ assert "## Model Budget" in markdown
31
+ assert "## Social Post Draft" in markdown
32
+ assert "Hackathon Advisor" in markdown
33
+ assert "Well-Tuned | dataset-ready" in markdown
34
+ assert "MiniCPM5 LoRA SFT JSONL | ready | lora_dataset" in markdown
35
+ assert "Ready badges and planned badges are separated" in markdown
36
+ assert "A local-first archive cartographer for family photos" in markdown
37
+
38
+
39
+ def test_empty_submission_packet_is_honest_about_missing_session_artifacts() -> None:
40
+ markdown = build_submission_packet_markdown(
41
+ {},
42
+ {
43
+ "snapshot_generated_at": "2026-06-06T00:00:00+00:00",
44
+ "project_count": 100,
45
+ "index_algorithm": "tfidf-sparse-v1",
46
+ "index_generated_at": "2026-06-06T01:00:00+00:00",
47
+ "snapshot_digest": "abc",
48
+ },
49
+ prize_ledger({"backend": "rules", "model_id": "deterministic-tool-router"}),
50
+ )
51
+
52
+ assert "Title: Unwritten Page" in markdown
53
+ assert "Tool trace JSONL | needs session" in markdown
54
+ assert "Submission packet markdown | ready" in markdown
55
+ assert "No ideas recorded yet." in markdown