From 7e5c36880010b5c1e4ba2831f9b8366b584fa769 Mon Sep 17 00:00:00 2001 From: trevon Date: Wed, 26 Aug 2026 01:07:25 +0000 Subject: [PATCH] template: starter + setup + milestone + agents + workflow --- .clang-format | 7 +++ .forgejo/workflows/grade.yml | 34 +++++++++++++++ .gitignore | 3 ++ AGENTS.md | 46 ++++++++++++++++++++ MILESTONE.md | 51 ++++++++++++++++++++++ Makefile | 45 ++++++++++++++++++++ README.md | 51 ++++++++++++++++++++++ SETUP.md | 41 ++++++++++++++++++ include/record.h | 40 ++++++++++++++++++ src/main.c | 82 ++++++++++++++++++++++++++++++++++++ src/record.c | 60 ++++++++++++++++++++++++++ tests/public/test_public.py | 32 ++++++++++++++ tests/smoke.sh | 15 +++++++ 13 files changed, 507 insertions(+) create mode 100644 .clang-format create mode 100644 .forgejo/workflows/grade.yml create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 MILESTONE.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 SETUP.md create mode 100644 include/record.h create mode 100644 src/main.c create mode 100644 src/record.c create mode 100644 tests/public/test_public.py create mode 100644 tests/smoke.sh diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..28fe98b --- /dev/null +++ b/.clang-format @@ -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 diff --git a/.forgejo/workflows/grade.yml b/.forgejo/workflows/grade.yml new file mode 100644 index 0000000..9e4d6c2 --- /dev/null +++ b/.forgejo/workflows/grade.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3f78a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +*.o +*.d diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ec4a834 --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/MILESTONE.md b/MILESTONE.md new file mode 100644 index 0000000..9df3163 --- /dev/null +++ b/MILESTONE.md @@ -0,0 +1,51 @@ +# Milestone Write-up — M: + +> 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4aa4b94 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d0d3d5 --- /dev/null +++ b/README.md @@ -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: +- **Readable C** — Linux kernel coding style: +- **Robust error handling** — return `status_t`, check it at the call site. Reference: SEI CERT C, API04-C / ERR33-C () +- **Build hygiene** — automatic header deps via `-MMD -MP`. Reference: GNU Make, Automatic Prerequisites () +- **Memory safety** — sanitizers + Valgrind. Reference: GCC Instrumentation Options () +- **Course textbook** — Beej's Guide to C: + +## 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. diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..bc96bdc --- /dev/null +++ b/SETUP.md @@ -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). diff --git a/include/record.h b/include/record.h new file mode 100644 index 0000000..6aff6ad --- /dev/null +++ b/include/record.h @@ -0,0 +1,40 @@ +#ifndef RECORD_H +#define RECORD_H + +#include + +/* + * 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 */ diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..a4a74d0 --- /dev/null +++ b/src/main.c @@ -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 +#include + +#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 [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 [field2] add a record\n" + " list list all records\n" + " search search records\n" + " delete delete a record\n" + " save save to a text file\n" + " load load from a text file\n" + " help show this help\n" + " quit exit\n"); +} diff --git a/src/record.c b/src/record.c new file mode 100644 index 0000000..8db70ca --- /dev/null +++ b/src/record.c @@ -0,0 +1,60 @@ +#include "record.h" + +#include +#include + +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"; + } +} diff --git a/tests/public/test_public.py b/tests/public/test_public.py new file mode 100644 index 0000000..025dc07 --- /dev/null +++ b/tests/public/test_public.py @@ -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 diff --git a/tests/smoke.sh b/tests/smoke.sh new file mode 100644 index 0000000..410042b --- /dev/null +++ b/tests/smoke.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Smoke test for the Record Manager CLI. +# Usage: smoke.sh +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"