{
const scripts = Array.from(
document.querySelectorAll("script.fcpython-ojs-quiz-config")
);
const script = scripts.find(
(node) => node.dataset.fcpythonRendered !== "true"
);
if (!script) {
return html`<div class="fcpython-quiz fcpython-quiz-warning">
Quiz configuration was not found.
</div>`;
}
script.dataset.fcpythonRendered = "true";
const quiz = JSON.parse(script.textContent);
const container = html`<div class="fcpython-quiz"></div>`;
const title = document.createElement("h3");
title.textContent = quiz.title;
container.appendChild(title);
const instructions = document.createElement("p");
instructions.textContent = quiz.instructions;
container.appendChild(instructions);
const progress = document.createElement("div");
progress.className = "fcpython-quiz-progress";
const counter = document.createElement("p");
counter.className = "fcpython-quiz-counter";
counter.setAttribute("aria-live", "polite");
progress.appendChild(counter);
const tabs = document.createElement("div");
tabs.className = "fcpython-quiz-steps";
tabs.setAttribute("role", "tablist");
tabs.setAttribute("aria-label", "Quiz questions");
progress.appendChild(tabs);
container.appendChild(progress);
const questions = document.createElement("div");
questions.className = "fcpython-quiz-questions";
const feedbackNodes = [];
const questionPanels = [];
const stepButtons = [];
let currentQuestion = 0;
function setStepStatus(questionIndex, status) {
const step = stepButtons[questionIndex];
const question = quiz.questions[questionIndex];
const statuses = {
answered: "answered",
correct: "correct",
incorrect: "incorrect",
unanswered: "not answered",
};
step.classList.remove(
"is-answered",
"is-correct",
"is-incorrect",
"is-unanswered"
);
if (status) {
step.classList.add(`is-${status}`);
}
const statusLabel = status ? `, ${statuses[status]}` : "";
step.setAttribute(
"aria-label",
`Question ${questionIndex + 1}: ${question.prompt}${statusLabel}`
);
}
function showQuestion(questionIndex, focusPanel = false) {
currentQuestion = Math.max(
0,
Math.min(questionIndex, quiz.questions.length - 1)
);
questionPanels.forEach((panel, index) => {
panel.hidden = index !== currentQuestion;
});
stepButtons.forEach((step, index) => {
const isCurrent = index === currentQuestion;
step.classList.toggle("is-current", isCurrent);
step.setAttribute("aria-selected", String(isCurrent));
step.tabIndex = isCurrent ? 0 : -1;
});
counter.textContent = `Question ${currentQuestion + 1} of ${quiz.questions.length}`;
const selected = questionPanels[currentQuestion].querySelector(
"input[type='radio']:checked"
);
const isLastQuestion = currentQuestion === quiz.questions.length - 1;
previous.hidden = currentQuestion === 0;
next.hidden = isLastQuestion || !selected;
check.hidden = !isLastQuestion;
if (focusPanel) {
questionPanels[currentQuestion].focus();
}
}
quiz.questions.forEach((question, questionIndex) => {
const tabId = `${quiz.id}-question-tab-${questionIndex + 1}`;
const panelId = `${quiz.id}-question-panel-${questionIndex + 1}`;
const step = document.createElement("button");
step.type = "button";
step.className = "fcpython-quiz-step";
step.id = tabId;
step.textContent = String(questionIndex + 1);
step.setAttribute("role", "tab");
step.setAttribute("aria-controls", panelId);
step.setAttribute("aria-selected", "false");
step.tabIndex = -1;
step.addEventListener("click", () => showQuestion(questionIndex));
step.addEventListener("keydown", (event) => {
let destination = null;
if (event.key === "ArrowRight") {
destination = (questionIndex + 1) % quiz.questions.length;
} else if (event.key === "ArrowLeft") {
destination =
(questionIndex - 1 + quiz.questions.length) % quiz.questions.length;
} else if (event.key === "Home") {
destination = 0;
} else if (event.key === "End") {
destination = quiz.questions.length - 1;
}
if (destination !== null) {
event.preventDefault();
showQuestion(destination);
stepButtons[destination].focus();
}
});
stepButtons.push(step);
tabs.appendChild(step);
setStepStatus(questionIndex, "");
const panel = document.createElement("div");
panel.className = "fcpython-quiz-panel";
panel.id = panelId;
panel.setAttribute("role", "tabpanel");
panel.setAttribute("aria-labelledby", tabId);
panel.tabIndex = -1;
const fieldset = document.createElement("fieldset");
fieldset.className = "fcpython-quiz-question";
const legend = document.createElement("legend");
legend.textContent = question.prompt;
fieldset.appendChild(legend);
question.options.forEach((option, optionIndex) => {
const label = document.createElement("label");
label.className = "fcpython-quiz-option";
const input = document.createElement("input");
input.type = "radio";
input.name = `${quiz.id}-${question.id}`;
input.value = String(optionIndex);
input.addEventListener("change", () => {
setStepStatus(questionIndex, "answered");
feedbackNodes[questionIndex].textContent = "";
feedbackNodes[questionIndex].className = "fcpython-quiz-feedback";
score.textContent = "";
showQuestion(questionIndex);
});
const text = document.createElement("span");
text.textContent = option;
label.appendChild(input);
label.appendChild(text);
fieldset.appendChild(label);
});
const feedback = document.createElement("p");
feedback.className = "fcpython-quiz-feedback";
feedback.setAttribute("aria-live", "polite");
feedbackNodes.push(feedback);
fieldset.appendChild(feedback);
panel.appendChild(fieldset);
questionPanels.push(panel);
questions.appendChild(panel);
});
container.appendChild(questions);
const actions = document.createElement("div");
actions.className = "fcpython-quiz-actions";
const previous = document.createElement("button");
previous.type = "button";
previous.className = "fcpython-quiz-secondary";
previous.textContent = "Previous";
previous.addEventListener("click", () => {
showQuestion(currentQuestion - 1, true);
});
const next = document.createElement("button");
next.type = "button";
next.textContent = "Next question";
next.addEventListener("click", () => {
showQuestion(currentQuestion + 1, true);
});
const check = document.createElement("button");
check.type = "button";
check.textContent = "Check answers";
const reset = document.createElement("button");
reset.type = "button";
reset.className = "fcpython-quiz-secondary fcpython-quiz-reset";
reset.textContent = "Reset";
const score = document.createElement("p");
score.className = "fcpython-quiz-score";
score.setAttribute("aria-live", "polite");
check.addEventListener("click", () => {
let correctCount = 0;
let firstQuestionToReview = null;
quiz.questions.forEach((question, questionIndex) => {
const selected = container.querySelector(
`input[name="${quiz.id}-${question.id}"]:checked`
);
const feedback = feedbackNodes[questionIndex];
if (!selected) {
feedback.textContent = "Choose an answer before checking.";
feedback.className = "fcpython-quiz-feedback";
setStepStatus(questionIndex, "unanswered");
if (firstQuestionToReview === null) {
firstQuestionToReview = questionIndex;
}
return;
}
const selectedIndex = Number(selected.value);
if (selectedIndex === question.answer_index) {
correctCount += 1;
feedback.textContent = `✅ Correct. ${question.explanation}`;
feedback.className = "fcpython-quiz-feedback is-correct";
setStepStatus(questionIndex, "correct");
} else {
const answer = question.options[question.answer_index];
feedback.textContent = `❌ Not yet. Correct answer: ${answer}. ${question.explanation}`;
feedback.className = "fcpython-quiz-feedback is-incorrect";
setStepStatus(questionIndex, "incorrect");
if (firstQuestionToReview === null) {
firstQuestionToReview = questionIndex;
}
}
});
score.textContent = `Score: ${correctCount}/${quiz.questions.length}`;
if (firstQuestionToReview !== null) {
showQuestion(firstQuestionToReview, true);
}
});
reset.addEventListener("click", () => {
container.querySelectorAll("input[type='radio']").forEach((input) => {
input.checked = false;
});
feedbackNodes.forEach((feedback) => {
feedback.textContent = "";
feedback.className = "fcpython-quiz-feedback";
});
stepButtons.forEach((_, questionIndex) => {
setStepStatus(questionIndex, "");
});
score.textContent = "";
showQuestion(0, true);
});
actions.appendChild(previous);
actions.appendChild(next);
actions.appendChild(check);
actions.appendChild(reset);
container.appendChild(actions);
container.appendChild(score);
showQuestion(0);
return container;
}
FreeCampus Python
Unit Challenge: Restore the Starship Signal Archive
Repair a multilingual multi-file import pipeline so it discovers signals predictably, rejects invalid records with evidence, and publishes verified JSON without hiding defects.
python-foundations
files-paths-external-data
unit-challenge
archive-puzzle
Course progress
0%
1. Reconnect a starship whose archive went silent
The research ship Asteria has emerged from a communications storm. Two signal files survived, but the archive builder now reverses their order, accepts an ID hidden inside extra characters, allows priority zero, calls programming defects “bad records,” and publishes JSON that violates the ship’s text contract.
The implementation is not blank. It looks reasonable and runs far enough to produce an archive—which is exactly why the defects are dangerous. Repair it one contract at a time until the accepted message fragments reveal:
You will work entirely inside a temporary starship workspace:
One CSV begins with a UTF-8 BOM. The records include accented and non-Latin names, a quoted multiline message, an invalid ID wrapper, an empty sender, a nonnumeric priority, and a priority outside the allowed range. The original inputs must remain unchanged.
NoteRepair the archive, not the evidence
Keep the fixture and expected values unchanged. Run the nearest failing stage, write one hypothesis, make one focused change, and rerun all earlier stages. Hints are available, but first use the assertion message, exception type, serialized text, or path list already in front of you.
2. Read the restoration rules
File discovery
- Search only
incoming/signal-*.csvbeneath the supplied workspace root. - Return
Pathobjects in sorted, deterministic order. - Do not include
operator-notes.txt, a previous JSON output, or unrelated CSV files. - Do not mutate or delete any input.
CSV and text boundary
- Open every signal file with
encoding="utf-8-sig"andnewline="". - Require headers in this exact order:
signal_id,sender,message,priority. - Enumerate logical data records from 1. A quoted newline remains part of one message and does not create an extra record.
- A header mismatch is a file-level
ValueError; do not call it one rejected row.
Record validation
signal_idmust fully matchSIG-followed by four ASCII digits.- Strip surrounding whitespace from
signal_id,sender, andmessage. senderandmessagemust remain non-empty after stripping.- Convert
priorityto an integer and require the inclusive range 1 through 5. - When
intraisesValueError, add source and record context withraise ... from error. - Return a new normalized dictionary; do not modify the raw row retained as evidence.
Accepted and rejected records
load_signal_filesreturns(accepted, rejected).- Catch only the contextual
ValueErrorraised for an anticipated bad record. - Each rejection stores
source,record,raw, andreason. - An unexpected
RuntimeError,TypeError, or programming defect must stop the pipeline with its traceback.
Archive output
- Write one JSON document containing
acceptedandrejectedlists. - Use
ensure_ascii=False,indent=2, andsort_keys=True. - End the UTF-8 file with exactly one newline.
- Write a sibling
.tmpartifact, parse it back, compare the restored value, and only then replace the destination. - Remove a leftover temporary artifact if creation or verification fails.
- A second clean write with the same values must produce identical text.
3. Start from the contract
Run the fixture cell first. It creates a fresh workspace and all inputs, so the challenge does not depend on repository files or old notebook state.
import csv
import json
import re
from pathlib import Path
from tempfile import TemporaryDirectory
def create_starship_workspace(root):
"""Create and return a self-contained signal archive fixture beneath root."""
root = Path(root) / "starship-signals"
incoming = root / "incoming"
restored = root / "restored"
incoming.mkdir(parents=True)
restored.mkdir()
alpha_text = (
"signal_id,sender,message,priority\n"
"SIG-1001,Ana,NOVA,3\n"
"xxSIG-1002yy,Luna,DRIFT,2\n"
"SIG-1003,,QUIET,1\n"
'SIG-1004,Renée,"HELLO,\nLUNA",5\n'
)
beta_text = (
"signal_id,sender,message,priority\n"
"SIG-2001,Noor,SAFE,2\n"
"SIG-2002,Íris,DELAY,urgent\n"
"SIG-2003,Kai,AGAIN,0\n"
"SIG-2004,Zoë,HOME,4\n"
)
(incoming / "signal-alpha.csv").write_text(
alpha_text,
encoding="utf-8-sig",
newline="",
)
(incoming / "signal-beta.csv").write_text(
beta_text,
encoding="utf-8",
newline="",
)
(incoming / "operator-notes.txt").write_text(
"Não arquivar automaticamente. 月 channel remains open.\n",
encoding="utf-8",
)
(incoming / "unrelated.csv").write_text(
"this,is,not,a,signal\n",
encoding="utf-8",
)
return root
challenge_temporary_directory = TemporaryDirectory()
challenge_root = create_starship_workspace(challenge_temporary_directory.name)Now run the starter unchanged. Preserve these five public names, signatures, and docstrings. It contains a small set of seeded defects; do not rewrite the whole pipeline before the assertions tell you where each promise breaks.
EXPECTED_SIGNAL_HEADERS = ["signal_id", "sender", "message", "priority"]
SIGNAL_ID_PATTERN = re.compile(r"SIG-[0-9]{4}")
def discover_signal_files(root):
"""Return intended signal CSV paths in deterministic order."""
paths = list((Path(root) / "incoming").glob("signal-*.csv"))
paths.reverse()
return paths
def parse_signal(row, source, record_number):
"""Return one normalized signal or raise contextual ValueError."""
signal_id = row["signal_id"].strip()
sender = row["sender"].strip()
message = row["message"].strip()
if SIGNAL_ID_PATTERN.search(signal_id) is None:
raise ValueError(
f"{source}: record {record_number}: invalid signal_id {signal_id!r}"
)
if not sender:
raise ValueError(f"{source}: record {record_number}: sender is empty")
if not message:
raise ValueError(f"{source}: record {record_number}: message is empty")
try:
priority = int(row["priority"])
except ValueError as error:
raise ValueError(
f"{source}: record {record_number}: invalid priority {row['priority']!r}"
) from error
if priority < 0 or priority > 5:
raise ValueError(
f"{source}: record {record_number}: priority must be 1 through 5"
)
return {
"signal_id": signal_id,
"sender": sender,
"message": message,
"priority": priority,
}
def load_signal_files(paths):
"""Return accepted signals and detailed rejected-record evidence."""
accepted = []
rejected = []
for path in paths:
with Path(path).open(encoding="utf-8-sig", newline="") as handle:
reader = csv.DictReader(handle)
if reader.fieldnames != EXPECTED_SIGNAL_HEADERS:
raise ValueError(
f"{path.name}: expected headers {EXPECTED_SIGNAL_HEADERS!r}; "
f"got {reader.fieldnames!r}"
)
for record_number, row in enumerate(reader, start=1):
try:
signal = parse_signal(row, path.name, record_number)
except Exception as error:
rejected.append(
{
"source": path.name,
"record": record_number,
"raw": dict(row),
"reason": str(error),
}
)
else:
accepted.append(signal)
return accepted, rejected
def write_signal_archive(path, accepted, rejected):
"""Safely write deterministic UTF-8 JSON and return destination path."""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
archive = {"accepted": accepted, "rejected": rejected}
text = json.dumps(archive, indent=2, sort_keys=True)
path.write_text(text, encoding="utf-8")
return path4. Repair one boundary at a time
Treat the archive as four cooperating subsystems:
| Subsystem | Input | Success | Anticipated failure | Evidence |
|---|---|---|---|---|
| discovery | workspace root | sorted intended paths | none in supplied fixture | relative path list |
| record parser | raw row + lineage | normalized dictionary | contextual ValueError |
value, message, __cause__ |
| batch loader | discovered paths | accepted and rejected lists | record ValueError only |
counts, raw rows, reasons |
| writer | lists + destination | verified deterministic JSON | serialization or filesystem failure | parsed output, exact text, final/tmp paths |
Use this order:
- make discovery select and sort the intended paths;
- make identifier recognition consume the whole normalized field;
- repair the lower priority boundary while preserving conversion context;
- narrow the batch exception handler without losing rejections;
- serialize readable Unicode and a final newline;
- add temporary creation, parse-back verification, cleanup, and replacement;
- run the complete archive twice from a clean state.
The same assertion may reveal a later defect after you fix an earlier one. That is progress, not regression.
WarningDo not make the tests pass by weakening the mission
Do not edit expected filenames, accepted IDs, rejection counts, or the beacon phrase. If a check seems wrong, compare it with the written rule and fixture, then record the contradiction before changing an oracle.
5. Run progressive assertions
The helper below captures an anticipated validation failure and clearly fails if the action incorrectly succeeds:
Stage A: discover only intended files in stable order
Run discovery again after creating an old output. Its result must not change:
Stage B: validate one record independently
valid_row = {
"signal_id": " SIG-4242 ",
"sender": " 航海士 月 ",
"message": " HOME ",
"priority": "5",
}
valid_snapshot = valid_row.copy()
assert parse_signal(valid_row, "focus.csv", 1) == {
"signal_id": "SIG-4242",
"sender": "航海士 月",
"message": "HOME",
"priority": 5,
}
assert valid_row == valid_snapshotThe whole identifier field must match:
Check empty text and both priority failure families:
empty_sender_error = captured_value_error(
lambda: parse_signal(
{
"signal_id": "SIG-4243",
"sender": " ",
"message": "QUIET",
"priority": "1",
},
"focus.csv",
3,
)
)
assert "sender" in str(empty_sender_error)
conversion_error = captured_value_error(
lambda: parse_signal(
{
"signal_id": "SIG-4244",
"sender": "Íris",
"message": "DELAY",
"priority": "urgent",
},
"focus.csv",
4,
)
)
assert "focus.csv" in str(conversion_error) and "record 4" in str(conversion_error)
assert isinstance(conversion_error.__cause__, ValueError)
zero_error = captured_value_error(
lambda: parse_signal(
{
"signal_id": "SIG-4245",
"sender": "Kai",
"message": "AGAIN",
"priority": "0",
},
"focus.csv",
5,
)
)
assert "1 through 5" in str(zero_error)
assert zero_error.__cause__ is NoneStage C: preserve CSV records and rejection lineage
accepted, rejected = load_signal_files(paths)
assert [signal["signal_id"] for signal in accepted] == [
"SIG-1001",
"SIG-1004",
"SIG-2001",
"SIG-2004",
]
assert [signal["sender"] for signal in accepted] == [
"Ana",
"Renée",
"Noor",
"Zoë",
]
assert accepted[1]["message"] == "HELLO,\nLUNA"
assert [signal["priority"] for signal in accepted] == [3, 5, 2, 4]
assert len(rejected) == 4
assert [item["source"] for item in rejected] == [
"signal-alpha.csv",
"signal-alpha.csv",
"signal-beta.csv",
"signal-beta.csv",
]
assert [item["record"] for item in rejected] == [2, 3, 2, 3]
assert all(
set(item) == {"source", "record", "raw", "reason"} and item["reason"]
for item in rejected
)
assert (
rejected[0]["raw"]["signal_id"],
rejected[2]["raw"]["priority"],
) == ("xxSIG-1002yy", "urgent")The multiline signal is one logical record. Its presence as accepted record 4 of the alpha file proves physical line breaks were not treated as separate CSV records.
Stage D: let an unexpected parser defect propagate
Temporarily replace the global parser, then restore it even if the experiment fails. The batch loader must not quarantine this RuntimeError.
original_parse_signal = parse_signal
def crashing_parse_signal(row, source, record_number):
raise RuntimeError("simulated navigation-computer defect")
parse_signal = crashing_parse_signal
try:
try:
load_signal_files(paths)
except RuntimeError as error:
assert "navigation-computer" in str(error)
else:
raise AssertionError("unexpected RuntimeError was hidden")
finally:
parse_signal = original_parse_signalRerun Stage C after restoring the real parser.
Stage E: write, parse, and compare the archive
archive_path = challenge_root / "restored" / "signal-archive.json"
written_path = write_signal_archive(archive_path, accepted, rejected)
archive_text = written_path.read_text(encoding="utf-8")
restored_archive = json.loads(archive_text)
assert written_path == archive_path
assert restored_archive == {"accepted": accepted, "rejected": rejected}
assert "Renée" in archive_text
assert "Zoë" in archive_text
assert "\\u00e9" not in archive_text.lower()
assert archive_text.endswith("\n")
assert not archive_text.endswith("\n\n")
assert not (archive_path.parent / "signal-archive.json.tmp").exists()Prove the text is deterministic and a serialization failure preserves the previous good artifact:
first_archive_text = archive_text
write_signal_archive(archive_path, accepted, rejected)
assert archive_path.read_text(encoding="utf-8") == first_archive_text
try:
write_signal_archive(
archive_path,
accepted,
[{"source": "broken", "raw": {"not-json-compatible"}}],
)
except TypeError:
pass
else:
raise AssertionError("expected JSON serialization failure")
assert archive_path.read_text(encoding="utf-8") == first_archive_text
assert not (archive_path.parent / "signal-archive.json.tmp").exists()Stage F: reveal the restored beacon
Finally, restart the runtime, recreate the workspace, load your repaired five functions, and run every stage top to bottom. Confirm the fixture bytes and relative input names are unchanged.
6. Use the hint ladder only when needed
Hint 1
Map the first failed assertion to one boundary. A reversed relative-path list is only discovery. An accepted wrapper ID is only recognition. Priority zero is a domain boundary. A swallowed simulated RuntimeError points to the loader’s except clause. Escaped names or a missing final newline point to serialization.
Hint 2
The relevant operations already appeared in the lessons: sorted, regex fullmatch, an inclusive comparison, except ValueError, json.dumps(..., ensure_ascii=False, indent=2, sort_keys=True) + "\n", and a sibling path whose name ends in .tmp. Apply one only where its contract belongs.
Hint 3
The core repair shapes are:
paths = sorted((Path(root) / "incoming").glob("signal-*.csv"))
if SIGNAL_ID_PATTERN.fullmatch(signal_id) is None:
raise ValueError(...)
if not 1 <= priority <= 5:
raise ValueError(...)
try:
signal = parse_signal(...)
except ValueError as error:
rejected.append(...)
text = json.dumps(
archive,
ensure_ascii=False,
indent=2,
sort_keys=True,
) + "\n"Write text to the sibling temporary path, load it with json.load, compare it to archive, then call temporary_path.replace(path). Cleanup belongs in an exception path that immediately re-raises.
7. Keep debugging evidence
Preserve one failed stage that changed your understanding. The wrapper-ID check or simulated-runtime check makes a useful focused investigation.
| Reproduction | Actual evidence | Hypothesis | One change | Focused rerun | Earlier stages | Clean rerun |
|---|---|---|---|---|---|---|
| exact call or assertion | path list, value, exception, or text | one mechanism that could be false | one expression or boundary | nearest stage result | pass/fail record | top-to-bottom result |
A strong record is specific:
Reproduction: parse_signal with signal_id "xxSIG-4242yy"
Expected: contextual ValueError
Actual: returned a normalized record
Hypothesis: search accepts a valid substring without checking the complete field.
Prediction: replacing search with fullmatch rejects the wrapper while SIG-4242 still passes.Do not write “fixed regex.” Record the input, observed result, contract, hypothesis, and regression evidence. Preserve that record beside the final clean run.
8. Compare with a complete solution
Show the restored archive implementation after attempting every stage
EXPECTED_SIGNAL_HEADERS = ["signal_id", "sender", "message", "priority"]
SIGNAL_ID_PATTERN = re.compile(r"SIG-[0-9]{4}")
def discover_signal_files(root):
"""Return intended signal CSV paths in deterministic order."""
return sorted((Path(root) / "incoming").glob("signal-*.csv"))
def parse_signal(row, source, record_number):
"""Return one normalized signal or raise contextual ValueError."""
signal_id = row["signal_id"].strip()
sender = row["sender"].strip()
message = row["message"].strip()
if SIGNAL_ID_PATTERN.fullmatch(signal_id) is None:
raise ValueError(
f"{source}: record {record_number}: invalid signal_id {signal_id!r}"
)
if not sender:
raise ValueError(f"{source}: record {record_number}: sender is empty")
if not message:
raise ValueError(f"{source}: record {record_number}: message is empty")
try:
priority = int(row["priority"])
except ValueError as error:
raise ValueError(
f"{source}: record {record_number}: invalid priority {row['priority']!r}"
) from error
if not 1 <= priority <= 5:
raise ValueError(
f"{source}: record {record_number}: priority must be 1 through 5"
)
return {
"signal_id": signal_id,
"sender": sender,
"message": message,
"priority": priority,
}
def load_signal_files(paths):
"""Return accepted signals and detailed rejected-record evidence."""
accepted = []
rejected = []
for path in paths:
with Path(path).open(encoding="utf-8-sig", newline="") as handle:
reader = csv.DictReader(handle)
if reader.fieldnames != EXPECTED_SIGNAL_HEADERS:
raise ValueError(
f"{path.name}: expected headers {EXPECTED_SIGNAL_HEADERS!r}; "
f"got {reader.fieldnames!r}"
)
for record_number, row in enumerate(reader, start=1):
try:
signal = parse_signal(row, path.name, record_number)
except ValueError as error:
rejected.append(
{
"source": path.name,
"record": record_number,
"raw": dict(row),
"reason": str(error),
}
)
else:
accepted.append(signal)
return accepted, rejected
def write_signal_archive(path, accepted, rejected):
"""Safely write deterministic UTF-8 JSON and return destination path."""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
temporary_path = path.with_name(path.name + ".tmp")
archive = {"accepted": accepted, "rejected": rejected}
text = json.dumps(
archive,
ensure_ascii=False,
indent=2,
sort_keys=True,
) + "\n"
try:
temporary_path.write_text(text, encoding="utf-8", newline="\n")
with temporary_path.open(encoding="utf-8") as handle:
restored = json.load(handle)
if restored != archive:
raise ValueError("temporary archive did not round-trip")
temporary_path.replace(path)
except Exception:
temporary_path.unlink(missing_ok=True)
raise
return pathWhy these boundaries matter:
- sorted discovery makes file encounter order reproducible;
fullmatchprotects the complete identifier contract;- the inclusive range rejects zero while conversion chaining retains the original
intfailure; - the loader continues for anticipated invalid records but cannot hide a programming defect;
- the writer keeps multilingual names readable, creates exactly one document, verifies parsed values, and replaces only after success; and
- cleanup catches broadly only to remove its own temporary resource, then immediately re-raises the original failure.
Run every assertion in Section 5 unchanged after loading the solution.
9. Predict a changed mission rule
Asteria’s engineers propose archive contract version 3:
- a third file,
signal-gamma.csv, may arrive in UTF-8 without a BOM; - priority 5 signals must include a non-empty
acknowledged_bycolumn; and - the final archive must contain a
sourceslist and a count by priority.
Before editing code, answer:
- Does
utf-8-sigalready handle a UTF-8 file without a BOM? - Which exact header rule changes, and how will older files be versioned?
- Is
acknowledged_bya field rule or a cross-field rule? - Should counts be derived from accepted records or trusted from input?
- Which source order is deterministic?
- Which current assertions remain valuable regression evidence?
- What malformed gamma record would exercise the new rule without confusing it with CSV syntax?
Write new examples and expected values before implementing. Do not weaken the existing ID, sender, message, or priority contracts while adding a field.
10. Check your understanding
11. Decide whether the challenge is complete
Evidence rubric
| Evidence | Ready to record when |
|---|---|
| Discovery | The unchanged path assertion returns only alpha then beta, even after output exists. |
| Parsing | Whole-field ID, non-empty text, conversion cause, and priority range checks pass without mutating raw rows. |
| CSV | The quoted multiline message remains one record and UTF-8/BOM inputs preserve names. |
| Boundary | Four anticipated rows have source, logical record, raw fields, and reasons; simulated RuntimeError propagates. |
| Output | Parsed JSON equals intended values, Unicode is readable, text is deterministic, exactly one final newline exists, and no .tmp remains. |
| Payoff | Accepted fragments reveal NOVA HELLO, LUNA SAFE HOME. |
| Reasoning | One debugging record connects exact evidence, one hypothesis, one controlled change, and focused/full reruns. |
| Reproducibility | A clean temporary workspace passes all progressive stages top to bottom. |
This button stores a self-reported marker only in this browser. It does not submit work, grade it, verify identity, or issue a certificate.
Not yet recorded.
Key points
- Stable discovery, explicit encoding/newline behavior, CSV parsing, and domain validation are distinct contracts in one reproducible import pipeline.
- Full-field recognition and inclusive range checks prevent plausible-looking invalid values from entering accepted records.
- Narrow record handling preserves bad-input evidence without hiding developer failures.
- Deterministic Unicode JSON becomes official only after a temporary artifact parses back to the intended value.
- The strongest restoration evidence is an unchanged top-to-bottom clean run, the complete rejection trail, and a beacon phrase derived from accepted data.