template: starter + setup + milestone + agents + workflow
Some checks failed
grade / grade (push) Failing after 1m1s

This commit is contained in:
trevon 2026-08-26 01:07:25 +00:00
commit 7e5c368800
13 changed files with 507 additions and 0 deletions

7
.clang-format Normal file
View file

@ -0,0 +1,7 @@
# Clean C formatting: LLVM base, 4-space indent, 100 columns.
# Run `clang-format -i src/*.c include/*.h` before submitting.
# Reference: https://clang.llvm.org/docs/ClangFormatStyleOptions.html
BasedOnStyle: LLVM
IndentWidth: 4
ColumnLimit: 100
UseTab: Never

View file

@ -0,0 +1,34 @@
name: grade
on: [push]
# Production wiring (gitea-runner on ghost, docker mode):
# - job container = gitea/runner-images (has docker CLI) + the host docker
# socket, so docker_grade.sh drives the host daemon exactly as designed
# - hidden tests mount from the runner host (/opt/grade-hidden) — never in
# this repo
# - GRADE_HMAC_SECRET mounts from /opt/grade/grade.env on the runner host
jobs:
grade:
runs-on: ubuntu-latest
container:
image: gitea/runner-images:ubuntu-22.04
options: >-
-v /var/run/docker.sock:/var/run/docker.sock
-v /opt/grade/grade.env:/run/secrets/grade.env:ro
-v /opt/grade-hidden:/opt/grade-hidden:ro
steps:
- uses: actions/checkout@v4
- name: Grade (blank fails, solved passes)
run: |
set -a; . /run/secrets/grade.env; set +a
docker run --rm --network=none \
-v "$PWD":/work \
-v /opt/grade-hidden/cs2060/record-manager:/hidden:ro \
-e GRADE_HMAC_SECRET \
grade-c:dev \
python3 /opt/grade/grade_cli.py run \
--student /work --hidden /hidden \
--course CS2060 --assignment record-manager \
--out /work/results.json
echo "--- signed result summary ---"
head -c 400 results.json

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
build/
*.o
*.d

46
AGENTS.md Normal file
View file

@ -0,0 +1,46 @@
# AGENTS.md — Record Manager (student repo)
This file is read by **any coding agent** the student uses. Follow it exactly.
## Task
Extend this starter into the course's cumulative **Record Manager** project,
one milestone at a time. You are helping the student implement the **common
interface** exactly as specified in the assignment README — do not rename,
reorder, or remove any function signature; the autograder calls them by name.
## Verification-first (non-negotiable)
1. Before every push: `make` and `make test` must pass.
2. Milestone 5: `make sanitize` (ASan/UBSan) and `make valgrind` must be clean
(`0 bytes lost`, `0 errors`).
3. The **CI report on Forgejo is the official gate** — a green public test is
not enough; the hidden suite decides. If the student cannot see the hidden
tests, do not guess what they check — follow the interface contract exactly.
4. Never submit code you cannot trace: if you generate C, the student must be
able to explain every line you kept.
## Tier 2 AI rules (course policy)
Permitted — helping with:
- explaining compiler diagnostics / GCC-Clang warning flags
- generating test input fixtures or Makefile templates
- brainstorming algorithms (e.g., linked-list edge cases, save/load formats)
Prohibited:
- use on in-class quizzes, the midterm, or the final (Tier 0)
- submitting unverified AI-generated C: unsafe patterns (bare `strcpy`,
missing null terminators, uninitialized dereferences, buffer overruns)
are the exact failure modes this course teaches — validate with the gates
above, and fix what the student fixes with you.
## House rules
- Keep the verification targets in the Makefile (`test`, `sanitize`, `valgrind`).
- The student records **"AI used: none | Tier 2"** at the top of each milestone
write-up (`MILESTONE.md`) — help them be accurate, not evasive.
- Do not modify `.forgejo/workflows/grade.yml`.
- Write the milestone write-up in the student's voice, from their work — never
fabricate verification evidence that wasn't actually run.

51
MILESTONE.md Normal file
View file

@ -0,0 +1,51 @@
# Milestone Write-up — M<N>: <milestone name>
> Copy this file per milestone (e.g. `MILESTONE-M3.md`) and fill it in. The
> flavor line below is required **every** milestone, not just the first.
## Flavor
(contacts book | gradebook | expense tracker) — same flavor for every milestone
## AI used
(none | Tier 2) — see `AGENTS.md` for what Tier 2 permits
## What I built
Which common-interface functions are now implemented, in my own words
(e.g. "`list_append`/`list_find`/`list_remove` over a singly linked list, with
`list_free` walking and freeing every node and record"). Name the files.
## Verification evidence
Paste the output of the gates for this milestone — and the CI report link from
Forgejo Actions:
```text
make
make test
make sanitize # ASan/UBSan (M5+)
make valgrind # 0 bytes lost (M5)
```
## Pre-tool prediction
Before running the tool/CI: what did you predict would happen, and which test
did you expect to fail? (If nothing failed — say what made you confident.)
## Student-authored challenge case
A test or scenario **you** wrote that the starter didn't cover (e.g. remove the
head node, a 10k-record list, empty fields in save/load). What does it check
and why does it matter?
## Novel transfer
A new use of what you built this milestone — apply the linked list / buffer /
store to a different domain (e.g. "if the record were a todo item instead of a
contact…") and sketch how your code would change.
## Reflection
What broke, what you learned, what you'd do differently next milestone.

45
Makefile Normal file
View file

@ -0,0 +1,45 @@
# Record Manager — build, test, and verification targets.
#
# Best-practice references:
# - GNU Make automatic prerequisites: https://www.gnu.org/software/make/manual/html_node/Automatic-Prerequisites.html
# - GCC instrumentation (sanitizers): https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html
CC ?= cc
CFLAGS ?= -std=c17 -Wall -Wextra -Wpedantic -Werror -g
CPPFLAGS += -Iinclude -MMD -MP
LDLIBS ?=
SRC := $(wildcard src/*.c)
OBJ := $(SRC:src/%.c=build/%.o)
DEP := $(OBJ:.o=.d)
BIN := build/recman
all: $(BIN)
$(BIN): $(OBJ)
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
build/%.o: src/%.c | build
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
build:
mkdir -p build
test: $(BIN)
sh tests/smoke.sh $(BIN)
# ASan + UBSan — zero tolerance for memory and undefined-behavior errors.
sanitize:
$(CC) $(CFLAGS) -fsanitize=address,undefined $(CPPFLAGS) $(SRC) -o build/recman_asan $(LDLIBS)
sh tests/smoke.sh build/recman_asan
valgrind: $(BIN)
@command -v valgrind >/dev/null 2>&1 || { echo "error: valgrind not installed (see toolchain setup)"; exit 1; }
printf 'add Ada ada@x.io 555-0100\nlist\nquit\n' | valgrind --leak-check=full --show-leak-kinds=all --error-exitcode=1 $(BIN) >/dev/null
clean:
rm -rf build
-include $(DEP)
.PHONY: all test sanitize valgrind clean

51
README.md Normal file
View file

@ -0,0 +1,51 @@
# Record Manager — starter template
Clean, best-practice starting point for the CS 2060 cumulative project.
Pick a flavor (contacts / gradebook / expense tracker) and extend it milestone
by milestone; do not delete the verification targets.
**Before your first commit:** read [`SETUP.md`](SETUP.md) (toolchain) and
[`AGENTS.md`](AGENTS.md) (the rules every coding agent reads), copy
[`MILESTONE.md`](MILESTONE.md) per milestone as your write-up.
## Layout
```text
include/record.h the record_t struct + record_new/record_free + status codes
src/record.c record implementation
src/main.c CLI menu (Milestone 1)
tests/smoke.sh dependency-free smoke tests for the CLI
Makefile build + test + sanitizer targets
.clang-format enforced formatting (clang-format -i src/*.c include/*.h)
```
## Targets
| Command | What it does |
| :--- | :--- |
| `make` | build `build/recman` (`-std=c17 -Wall -Wextra -Wpedantic -Werror -g`) |
| `make test` | run the smoke tests |
| `make sanitize` | rebuild with ASan+UBSan and run the smoke tests |
| `make valgrind` | run under Valgrind Memcheck (leaks → non-zero exit) |
| `make clean` | remove `build/` |
## Style & best-practice references
- **Formatting**`.clang-format` (LLVM base, 4-space, 100 cols). Reference: <https://clang.llvm.org/docs/ClangFormatStyleOptions.html>
- **Readable C** — Linux kernel coding style: <https://www.kernel.org/doc/html/latest/process/coding-style.html>
- **Robust error handling** — return `status_t`, check it at the call site. Reference: SEI CERT C, API04-C / ERR33-C (<https://wiki.sei.cmu.edu/confluence/display/c/>)
- **Build hygiene** — automatic header deps via `-MMD -MP`. Reference: GNU Make, Automatic Prerequisites (<https://www.gnu.org/software/make/manual/html_node/Automatic-Prerequisites.html>)
- **Memory safety** — sanitizers + Valgrind. Reference: GCC Instrumentation Options (<https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html>)
- **Course textbook** — Beej's Guide to C: <https://beej.us/guide/bgc/>
## Milestones
The five-milestone spec (M1 CLI → M2 buffer → M3 list → M4 file → M5 refactor)
lives in `../README.md` (the `assignments/projects/` folder).
## AI use (optional)
AI assistance is **not required**. If you use it, the standard `AGENTS.md` in
this repo tells the agent exactly what Tier 2 permits; state "AI used: none |
Tier 2" at the top of each milestone write-up and let the verification gates
(`make test` / `make sanitize` / `make valgrind` + the CI report) be the record.

41
SETUP.md Normal file
View file

@ -0,0 +1,41 @@
# Setup — CS 2060 toolchain
Everything required is **free**. Full detail (macOS/Replit paths, debugging
setup, help sources): the course [`setup.md`](../setup.md).
## 1. Editor + toolchain (the fast path)
| Platform | What to install |
| :--- | :--- |
| Windows | **VS Code + WSL2** (the compiler lives in Linux): `wsl --install -d Ubuntu`, then in VS Code add the **Remote - WSL** extension and open this repo in WSL |
| macOS | **VS Code** + `xcode-select --install` (gives `clang` + `make`) |
| Linux | VS Code (or any editor) |
**Windows: use WSL2, not Visual Studio/MSVC** — the autograder is Linux + GCC +
`make` + Valgrind, and milestone 5 requires Valgrind (Linux-only).
## 2. Compiler & tools (Linux / WSL2 / Codespaces)
```bash
sudo apt update && sudo apt install -y build-essential gcc clang make gdb valgrind git
```
## 3. Debugging in VS Code (gdb)
1. Install the **C/C++ extension** (`ms-vscode.cpptools`).
2. Open this folder (in WSL on Windows).
3. Add `.vscode/tasks.json` (build: `make`) and `.vscode/launch.json` (gdb,
`program: ${workspaceFolder}/build/recman`, `preLaunchTask: make`) — the
course repo ships copies.
4. **F5** → build + debug. No VS Code? Terminal: `gdb ./build/recman`, then
`break record_new`, `run`, `print r->name`, `backtrace`.
## 4. Verify before you start
```bash
gcc --version && make --version && gdb --version && valgrind --version && git --version
make && make test # the starter must build and pass
```
If anything fails, fix the environment in week 1 — that's what office hours are
for (CYBR A120-L).

40
include/record.h Normal file
View file

@ -0,0 +1,40 @@
#ifndef RECORD_H
#define RECORD_H
#include <stddef.h>
/*
* Uniform status codes. Every fallible function returns status_t; check it at
* the call site. STATUS_OK (0) always means success.
*/
typedef enum {
STATUS_OK = 0,
STATUS_ERR_ARG, /* bad input / usage */
STATUS_ERR_NOMEM, /* allocation failed */
STATUS_ERR_IO, /* file or stream error */
STATUS_ERR_NOTFOUND, /* item not present */
} status_t;
/*
* One record. Pick a flavor and map the three fields:
* contacts -> name, email, phone
* gradebook -> student, assignment, score
* expenses -> date, category, amount
*/
typedef struct record {
char *name;
char *field1;
char *field2;
} record_t;
/* Heap-allocate a record with deep copies of the strings (NULL/empty allowed).
Returns NULL on allocation failure. */
record_t *record_new(const char *name, const char *field1, const char *field2);
/* Free a record and its owned strings. */
void record_free(record_t *rec);
/* Human-readable name for a status code (never NULL). */
const char *status_str(status_t s);
#endif /* RECORD_H */

82
src/main.c Normal file
View file

@ -0,0 +1,82 @@
/*
* Record Manager entry point (Milestone 1).
*
* A menu-driven CLI for a simple record keeper. Pick a flavor (contacts,
* gradebook, or expense tracker) and add / list records. Later milestones add
* the linked list, text-file persistence, and search/sort.
*/
#include "record.h"
#include <stdio.h>
#include <string.h>
#define LINE_MAX 1024
static void print_help(void);
int main(void) {
char line[LINE_MAX];
print_help();
for (;;) {
printf("> ");
fflush(stdout);
if (fgets(line, sizeof(line), stdin) == NULL)
break;
line[strcspn(line, "\n")] = '\0';
if (strncmp(line, "add ", 4) == 0) {
char name[LINE_MAX], f1[LINE_MAX], f2[LINE_MAX];
char rest[LINE_MAX];
int n;
record_t *rec;
strncpy(rest, line + 4, sizeof(rest) - 1);
rest[sizeof(rest) - 1] = '\0';
n = sscanf(rest, "%1023s %1023s %1023s", name, f1, f2);
if (n < 2) {
printf("usage: add <name> <field1> [field2]\n");
continue;
}
rec = record_new(name, f1, (n >= 3) ? f2 : "");
if (rec == NULL) {
printf("error: %s\n", status_str(STATUS_ERR_NOMEM));
continue;
}
printf("added: %s | %s | %s\n", rec->name, rec->field1, rec->field2);
/* TODO(milestone 3): append `rec` to a linked list instead of freeing. */
record_free(rec);
} else if (strcmp(line, "list") == 0) {
printf("(list is empty — milestone 3 adds records)\n");
} else if (strncmp(line, "search", 6) == 0) {
printf("(search — milestone 5)\n");
} else if (strncmp(line, "delete", 6) == 0) {
printf("(delete — milestone 3)\n");
} else if (strncmp(line, "save", 4) == 0) {
printf("(save — milestone 4)\n");
} else if (strncmp(line, "load", 4) == 0) {
printf("(load — milestone 4)\n");
} else if (strcmp(line, "help") == 0) {
print_help();
} else if (strcmp(line, "quit") == 0 || strcmp(line, "exit") == 0) {
break;
} else if (line[0] != '\0') {
printf("unknown command '%s' — try 'help'\n", line);
}
}
return 0;
}
static void print_help(void) {
printf("Record Manager — commands:\n"
" add <name> <field1> [field2] add a record\n"
" list list all records\n"
" search <text> search records\n"
" delete <name> delete a record\n"
" save <file> save to a text file\n"
" load <file> load from a text file\n"
" help show this help\n"
" quit exit\n");
}

60
src/record.c Normal file
View file

@ -0,0 +1,60 @@
#include "record.h"
#include <stdlib.h>
#include <string.h>
static char *dup_str(const char *s) {
size_t n;
char *p;
if (s == NULL)
return NULL;
n = strlen(s) + 1;
p = malloc(n);
if (p != NULL)
memcpy(p, s, n);
return p;
}
record_t *record_new(const char *name, const char *field1, const char *field2) {
record_t *r = malloc(sizeof(*r));
if (r == NULL)
return NULL;
r->name = dup_str(name);
r->field1 = dup_str(field1);
r->field2 = dup_str(field2);
if ((name != NULL && r->name == NULL) || (field1 != NULL && r->field1 == NULL) ||
(field2 != NULL && r->field2 == NULL)) {
record_free(r);
return NULL;
}
return r;
}
void record_free(record_t *rec) {
if (rec == NULL)
return;
free(rec->name);
free(rec->field1);
free(rec->field2);
free(rec);
}
const char *status_str(status_t s) {
switch (s) {
case STATUS_OK:
return "ok";
case STATUS_ERR_ARG:
return "invalid argument";
case STATUS_ERR_NOMEM:
return "out of memory";
case STATUS_ERR_IO:
return "I/O error";
case STATUS_ERR_NOTFOUND:
return "not found";
default:
return "unknown error";
}
}

View file

@ -0,0 +1,32 @@
"""Public tests for the Record Manager (M1: build + menu basics).
These ship to students and run under the grade harness (pytest, cwd = the
submission). M1 features only: add + list must work after `make` (delete/save/load come in
later milestones the hidden suite gates those).
"""
import pathlib
import subprocess
def _run(cmd, timeout=180, **kw):
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, **kw)
def test_m1_menu_basics():
root = pathlib.Path.cwd()
build = _run(["make", "-C", str(root), "-j2"])
assert build.returncode == 0, "BUILD FAILED:\n" + build.stdout + build.stderr
recman = root / "build" / "recman"
assert recman.exists(), "build/recman missing after make"
session = (
"add\nAda Lovelace\nada@x\n555-0100\n"
"add\nGrace Hopper\ngrace@x\n444-0200\n"
"list\n"
"quit\n"
)
r = _run([str(recman)], input=session)
assert "added: Ada Lovelace" in r.stdout, "add failed:\n" + r.stdout
assert "added: Grace Hopper" in r.stdout, "add failed:\n" + r.stdout
assert "Grace Hopper" in r.stdout, "list did not show the records:\n" + r.stdout

15
tests/smoke.sh Normal file
View file

@ -0,0 +1,15 @@
#!/bin/sh
# Smoke test for the Record Manager CLI.
# Usage: smoke.sh <path-to-binary>
set -eu
BIN="${1:-build/recman}"
out=$(printf 'help\nadd Ada ada@x.io 555-0100\nquit\n' | "$BIN")
echo "$out" | grep -q "Record Manager — commands"
echo "$out" | grep -q "added: Ada"
printf 'bogus\nquit\n' | "$BIN" | grep -q "unknown command"
echo "smoke tests passed"