Commit graph

50 commits

Author SHA1 Message Date
232faf892e README: link the live demo 2026-06-11 15:30:57 +02:00
f50a1a0e06 README: keep the deploy section generic
The public docs describe the image and how to run it; where and how it
is hosted is not the repo's business.
2026-06-11 15:21:15 +02:00
b0bcc7e224 README: the Caddy network on the server is named web 2026-06-11 15:19:55 +02:00
273ee45272 close the healthz probe body eagerly, errcheck fix
The probe reads nothing from the body, so close-and-check beats an
unchecked defer (which golangci-lint rejects).
2026-06-11 15:05:05 +02:00
05b01348d5 Portainer stack docs and a real container healthcheck, slice 4 of M4
The README deploy section documents the GHCR image, the compose stack
that joins Caddy's external network, and that the Caddyfile entry is
managed by hand on the server. distroless has no shell, so the
healthcheck is the binary itself: a new -check flag probes /healthz
and exits 0 or 1, and the Dockerfile bakes it in as HEALTHCHECK.
Verified locally: the container reports healthy.
2026-06-11 14:47:29 +02:00
51af5acf6a push the image to GHCR on merge to main, slice 3 of M4
Tags :latest and the commit SHA, authenticated with the built-in
GITHUB_TOKEN via the packages:write permission. Making the package
public is a one-time manual step in the package settings after the
first push.
2026-06-11 14:23:05 +02:00
33df4d33eb deploy-only Dockerfile, slice 2 of M4
Three stages: npm build, Go build with the real dist copied over the
placeholder before compiling, distroless/static (nonroot) runtime.
The container listens on :8080 (all interfaces, unlike the localhost
dev default). Verified locally: 16.6 MB image, /, /healthz and /api
all answer. Dev loop unchanged, Docker stays out of it.
2026-06-11 14:22:21 +02:00
3684488d75 serve the embedded frontend alongside the API, slice 1 of M4
//go:embed needs internal/webdist/dist to exist at compile time, so a
one-line placeholder index.html is committed there and the real build
(Docker, or a manual copy of web/dist) overwrites it. One mux serves
/, /api, and a /healthz for the compose healthcheck to come.
2026-06-11 14:02:38 +02:00
bbba450c6e docs: amend ui-spec to the implemented design; handoff for M4
The spec stays normative: 5.4 now describes the slower trickle playback
(1000 ms rounds, per-node appearance moments, live counting) that Justin
converged on during implementation, 5.3 the readable seed range, section
6 the taller viewBox and anisotropic layout forces, and StudyPreset its
readingCaption field.

The M4 handoff covers the single-binary embed (with the go:embed vs
gitignored-dist decision called out), Dockerfile, GHCR workflow, and the
Portainer/Caddy stack, plus the traps M3 paid for: lockfile
regeneration, the prettier ignore on generated types, screenshot
helpers, and reviewing perceptual work live.
2026-06-10 21:52:25 +02:00
2b47158b01 web: kill the round-switch flash, dots swap exactly at their moment
The flash came from branch switching at the round tick: every node due
that round dropped its gray unreached dot at once while its rose dot
waited invisibly for an animation delay. The unreached/reached branch is
now gated per frame by the same appearance predicate the live counter
uses (nodeDisplayedAsReached), so each gray dot stays put until the very
frame its rose dot starts fading in. Animation delays are gone; the
fade starts on the branch flip.
2026-06-10 18:28:49 +02:00
3a4e21b701 web: hide native number spinners; slower, fully staggered rounds
Per Justin: the native number-input spinners rendered as light boxes on
the dark theme, so they are gone app-wide (typing and arrow keys keep
working). Playback pacing: rounds now take 1000 ms at 1x, appearance
moments spread across 95% of the interval via a murmur3-style
full-avalanche hash, and each dot fades over 30% of the interval, so a
round reads as a continuous trickle instead of a flash.
2026-06-10 18:26:28 +02:00
d9b3812306 web: accessibility audit fixes, slice 12 of M3
Walked the spec section 8 checklist against the running app. Two gaps
fixed: the menus now close on Tab instead of leaking focus behind the
open menu (the opener restores focus), and the hero's mobile About note
restores focus to its trigger on close.

Audit results, item by item:
- Keyboard: all controls reachable and operable; menus arrow-cycle,
  Esc/Tab close with focus restore; the only trap is the focus modal.
- Focus indicator: global 2 px ink outline with 2 px offset.
- Player keys: Space/arrows/Home/End global except text inputs; the
  scrubber is a native range with aria-valuetext "Round x of y".
- Reduced motion: no autoplay, page settles on the final round,
  discrete swaps, 1000 ms cadence, smooth scrolling and crossfades off.
- Contrast: informative text uses --ink-3 or stronger; --ink-4 appears
  only on text duplicated in the data table or decorative numerals.
- Node state is shape-encoded (disc / donut / small dot / halo ring).
- Network SVGs aria-hidden; each panel is a labelled region whose text
  carries the result; the collapsed ResultsTable is a real table with
  th scope headers behind a keyboard disclosure.
- One polite live region announces runs, pauses, scrub releases, and
  the final state; rounds are not announced during playback.
- FocusModal: dialog semantics, trap, Esc, focus restore. ErrorBanner
  and inline 400 notes use role=alert. lang=en, header/main/footer.
- Touch targets 44 px on mobile; node hit halos 24 px.
- 200% zoom at 1366 px (683 px viewport) reflows to the mobile stack
  with nothing lost (verified by screenshot).
2026-06-10 18:22:11 +02:00
5c4a92b5a0 web: seed rerolls draw from a readable range
Per Justin: a full uint32 made rerolled seeds ten digits long. Rerolls
now draw 0-9999; still thousands of distinct worlds, but the advanced
fields and shared URLs stay legible.
2026-06-10 18:22:11 +02:00
c5cbfd3eea web: export menu, slice 11 of M3
The toolbar's Export button becomes a real menu: Data (JSON) downloads
{ base, panels, results } exactly as held in the store, Per-node CSV
writes one row per panel and node (educated flag, reachedAtRound with
-1 kept), and Picture (PNG) rasterizes each panel's live SVG with the
current theme's token values resolved, drawn side by side with labels
and the live percentage at the current round, plus legend and the
disclaimer footer line, at 2x. Items disable while a run is in flight
so exports always capture settled state.
2026-06-10 18:19:32 +02:00
3cd48ca7ea web: dots trickle in randomly per round, the share counts live
Per Justin: a round's dots no longer start fading together. Every node
gets a deterministic random appearance moment inside the first 70% of
the round interval (the rest is its fade), and a new roundProgress
value, driven by the playback frame loop, feeds the same jitter into
the panel stats: the big percentage and reached count now tick up
exactly as dots start appearing, in the cards, the mobile strip, and
the focus modal. Pausing, stepping, and seeking hold the full round;
reduced motion keeps discrete swaps.
2026-06-10 18:14:35 +02:00
609ba6f0dd web: mobile pass, slice 10 of M3
The MobileScoreStrip lands: sticky under the app bar, one pill per
panel with the round-synced colored share, horizontally scrollable (the
only horizontal scroller, verified at 320 px), tapping scrolls to the
panel (instant under reduced motion, scroll-margin clears the sticky
stack).

Touch targets reach 44 px on mobile: player buttons, panel kebabs, and
toolbar buttons; Export collapses to its icon so the toolbar stays one
row down to 320 px.
2026-06-10 18:09:51 +02:00
e0ecc5c6ba web: node tooltip with cross-panel echo, slice 9 of M3
Each node carries a transparent 12 px hit halo. Hovering shows the
single tooltip instance near the pointer: student number, state in that
panel at the current round (origin / forwarded in round n / educated,
refused / not reached), and the student's friend count from the real
edges. The same student simultaneously gets a 2 px ink echo ring in
every panel: the same kid, reached in one world, safe in another.

Dismissal via pointerleave, tap elsewhere, or Esc. A touch tap on a
node shows the tooltip without opening the focus modal; a mouse click
still falls through to the zoom button.
2026-06-10 18:03:42 +02:00
8929eeddb0 web: focus modal, slice 8 of M3
Clicking a panel's network (cursor zoom-in, a real button for keyboard
users) or its menu's Focus item opens the modal: the same SVG at the
same global round rendered up to 90vw/85vh over the spec scrim, with a
second PlayerBar docked in the footer sharing the store. Only the page
PlayerBar owns the global keyboard map, so keys are never handled
twice.

role=dialog with aria-modal, labelled by the panel name, focus moves in
and is trapped, Esc/X/scrim close, and focus returns to the opener.
focusPanelId already lives in the URL, so shared links open
pre-focused; on mobile the modal becomes a full-screen sheet.
2026-06-10 17:55:36 +02:00
eb1f7427c4 web: panel management, slice 7 of M3
The kebab menu goes live: Edit and Rename open the PanelEditorPopover
(rename focuses the label in its header), Duplicate copies with a
' copy' suffix and the next accent, Remove deletes immediately and is
disabled on the last panel. Add scenario opens the new panel's editor
right away, still capped at six.

The editor offers the strategy select plus every Config field as a row
against the base value: a differing value becomes an override
(highlighted row, per-field reset, chips on the card), typing the base
value back clears it, and runs touch only that panel through the
debounce. Engine 400s appear inline in the popover with the offending
field marked.

Per-panel failures now leave a rose dot on the kebab while the panel
keeps its last good result; the store tracks failed panel ids and
clears them on the next successful run. Field and strategy labels moved
to lib/fieldLabels so controls, editor, and table share one vocabulary.
2026-06-10 17:51:21 +02:00
fdc28ac686 web: world controls, slice 6 of M3
The lower grid completes: reach chart left, World card right. Two
headline levers (chance to forward, education budget as count plus
share) move instantly and rerun through the store's debounce; the
advanced disclosure holds students, friends per student, clique
tendency, first poster, and the three named seeds, each seed with a
dice reroll plus a Reroll world button (one coalesced run).

Engine 400s render inline under the card header in rose with the
offending field marked by name match; network failures keep using the
banner with retry. A graph-seed reroll remounts the network svg keyed
by graph hash, fading the new layout in over 200 ms.
2026-06-10 17:45:29 +02:00
da1d86aba7 web: reach-over-time chart with playhead, slice 5 of M3
Hand-rolled SVG as decided: dashed percent grid, one cumulative curve
per panel in its accent color, end labels via the shared formatPct, and
a playhead line with per-curve dots synced to the global round. The
cumulative-at-round counting moved into lib/reach.ts so the panel stats
and the chart cannot disagree.
2026-06-10 17:39:21 +02:00
e9d57b2082 web: node fade-in spans the whole round, playback never sits still
Per Justin: instead of a 250 ms pop followed by a still frame, each
newly reached node now fades and scales in across the full round
interval (scaled with playback speed, staggered per node), so the
animation reads as continuous motion like the prototype gif. Reduced
motion still collapses to discrete swaps.
2026-06-10 17:36:59 +02:00
85520ca504 web: regenerate package-lock so npm ci sees optional deps
The incrementally updated lock was missing @emnapi/core and
@emnapi/runtime (optional transitive deps skipped by the local
platform), which broke npm ci on CI. Regenerated from scratch.
2026-06-10 17:35:50 +02:00
0e3a14a0b3 web: playback, slice 4 of M3
usePlayback drives one global round on a rAF timer: 700 ms per round at
1x, speed pill cycling 0.5/1/2, play-once autoplay after the first load,
no looping; at the final round the play button becomes replay. Under
prefers-reduced-motion there is no autoplay, the page settles on the
final round, and the cadence slows to 1000 ms.

The PlayerBar is the mockup's centered pill with a native range
scrubber (one tick per round, aria-valuetext, seeking pauses) and the
global keyboard map: Space toggles, arrows step, Home/End jump; text
inputs and native control handling are left alone. A single polite live
region announces run updates, pauses, scrub releases, and the final
state.

Nodes reached in the current round pop in (scale 0.6 to 1, 250 ms
ease-out) with a small deterministic stagger so each round reads as a
wave. Per Justin's feedback the panel percentage and reached count now
follow the playhead instead of sitting on the final outcome; the hero
keeps the final numbers, since its sentence claims outcomes.
2026-06-10 17:15:38 +02:00
71baf10e73 web: the dashboard page, slice 3 of M3
The milestone 2 parity page becomes the spec's dashboard: app bar with
wordmark, disclaimer badge and About popover, theme toggle (persisted,
defaults to prefers-color-scheme, applied pre-mount to avoid a flash),
hero with the preset narrative and live toned percentages, scenario
toolbar, panel cards with skeleton loading and dim-while-refreshing,
legend, and footer. ComparisonTable evolves into the collapsed
ResultsTable (test moved along, plus a formatPct rounding case).

NetworkView renders the real topology with the spec's shape encodings.
The layout is seeded d3-force (mulberry32 random source, seeded initial
positions, 300 synchronous ticks, cached per graph hash); anisotropic
forceX/forceY pulls settle the cloud into the wide card shape so the
fit stays uniform-scale and the spacing organic. Justin asked for a
taller network area (380x230) and a reading caption under the legend;
the caption is study copy, so StudyPreset gains a readingCaption field.

Slices still to come keep their controls visibly parked: Export and the
panel kebab render disabled until slices 11 and 7.
2026-06-10 16:55:14 +02:00
2f21248c20 web: sim store, URL state scheme, debounced parallel runs (M3 slice 2)
useSimStore is the one reactive module singleton from the spec: panels
over a shared base config, runs debounced 400 ms, one POST /api/scenario
per panel in parallel, results swapped in atomically only when every
request succeeds. Failures keep the last good results; engine 400s are
routed to an inline validationError, everything else to the banner
message. Stale in-flight responses lose to newer runs per panel.

URL state follows spec section 7 (readable params, repeated panel=,
focus index, full fallback to the preset on any malformed part) and is
covered by round-trip and rejection tests. The study copy and initial
panels live in the deepfake-school preset module; formatPct is the
single percent-rounding rule.

The vitest tsconfig now keeps lib at ES2022 like the app config, and
prettier skips the tygo-generated src/types (drift guard stays green).
2026-06-10 16:36:26 +02:00
2feb6ea5a3 api: keep the error contract JSON even on encoder failure
writeJSON's last-resort branch (json.Marshal failing, a programmer
error: unsupported type or cyclic data) previously fell back to
http.Error, which answers text/plain and was the one corner where the
API broke its own {"error": ...} contract. It now writes a
hand-written constant JSON literal: still strictly simpler than the
encoder that just failed (the reason writeError, which is built ON
writeJSON, cannot be used there: it would be circular), but contract
consistent. Tested by forcing the branch with a channel payload,
which json.Marshal cannot encode.
2026-06-10 16:22:35 +02:00
b49d92ec50 docs: handoff for the M3 frontend segment (slices 2-12)
Self-contained brief for the frontend implementation session: current
state (slice 1 done, /api/scenario live, parity page to be replaced),
the normative pointers (ui-spec.md, approved mockup), method (drive
Vue fast, the design bar and how to self-check against it with
headless screenshots, tmux dev stack, per-slice commits, a11y as you
go), the four remaining micro-choices to batch into one question
round, and first steps.
2026-06-10 15:57:24 +02:00
9a392b1860 api: POST /api/scenario returns result plus graph topology (M3 slice 1)
One panel's whole world in one call: effective config in, echoed
config + cascade result + undirected edge list out. Edges are
[from, to] pairs with from < to in deterministic node order;
Graph.Edges() walks the adjacency once, GraphEdges(config) rebuilds
the seeded world (~25us) so Result stays lean and /api/comparison
stays untouched. This closes the topology gap the design brief
flagged; the frontend's seeded d3-force layout consumes these pairs.
Go bits: [][2]int is a slice of fixed-size arrays; [2]int is a value
type, comparable, and JSON-marshals to [a, b], exactly the wire
shape the spec asks for.
tygo regen includes a fix: engine.Strategy now maps to the generated
Strategy type instead of decaying to 'any' in ScenarioRequest.
Verified live through the dev stack: 7/120 reached, 351 edges.
2026-06-10 15:48:48 +02:00
8c8d5bca11 dev: dev.sh detects stale servers and offers to kill them
Before starting, each port (8080, 5173) is checked via ss; an existing
listener is reported with PID and command, and an interactive run asks
permission to kill it (default yes). Non-interactive runs abort
instead: never kill processes silently. Born from a real incident: an
orphaned smoke-test server held 8080 and dev.sh only said
'address already in use'.
2026-06-10 14:27:49 +02:00
5196ed270b docs: brief paths from the course workspace root
The design session runs in the VS Code extension from the course
workspace, one level above this repo; spell every referenced path from
there so nothing depends on the session's working directory.
2026-06-10 14:21:43 +02:00
6aeb7b5198 docs: brief for the milestone 3 UI/UX design session
Self-contained setup for a dedicated design session: product goal and
audience, the inherited visual language from the Python prototype,
what exists today (including the API's missing graph-topology
endpoint, a known gap), the locked decisions the spec must respect,
the questions it must answer (accessibility criteria among them, still
undefined), and the required shape of the deliverable, docs/ui-spec.md.
Design before build; the implementation session executes the spec.
2026-06-10 14:18:28 +02:00
61af68a8f7 docs: public-release README; ci: GitHub Actions workflow
README rewritten for a public audience: what the tool is and is not
(the not-validated disclaimer up front), the model with literature
references (Holme & Kim 2002; Kempe, Kleinberg & Tardos 2003), honest
status checklist, quick start, layout, the generated-types rule, and
the prevention-only framing of the subject.
CI mirrors the local checks (go test, gofmt, golangci-lint, vue
type-check/lint/test/build) and adds a drift guard: go generate must
leave web/src/types/ unchanged, so the Go structs stay the single
source of truth in fact, not just in principle.
2026-06-10 14:06:41 +02:00
a57e729c10 dev: one-command local stack via dev.sh
Starts the Go API and the Vite dev server as background jobs; 'wait -n'
returns when the first one exits and the EXIT trap kills the rest, so
one Ctrl-C (or either server crashing) stops everything. Installs
web/node_modules on first run. Verified: both ports respond, SIGINT
leaves no orphan processes.
2026-06-10 13:57:35 +02:00
7c16e6ea4f docs: development workflow in README
Two-terminal dev loop (go run + vite proxy), the full check matrix,
and the regenerate-types rule: web/src/types/ is generated from the
Go structs and must never be edited by hand.
2026-06-10 13:47:21 +02:00
c935071bd1 web: parity page renders the three-scenario comparison from live data
App.vue fetches the default config, posts it to /api/comparison, and
renders ComparisonTable; the Vite dev server proxies /api to the Go
server so the browser sees one origin. src/lib/api.ts is the only
fetch code and uses exclusively generated types: no shape is defined
on the frontend. Scaffold example components removed.
Engine fix surfaced by the smoke test: a nil Go slice marshals to
JSON null, violating the generated 'educated: number[]' contract;
RunScenario now returns an empty slice instead.
Verified end to end: curl through the Vite proxy returns the golden
99/70/7. Vitest covers the table rendering; type-check, oxlint,
eslint clean. This completes the milestone 2 parity check.
2026-06-10 13:47:01 +02:00
e9f36f509e api: JSON API over the engine; server mode in cmd/spreadlab
internal/api stays a translation layer: decode (with
DisallowUnknownFields so typos 400 instead of silently defaulting),
run the engine, encode; all validation stays in the engine. Routes use
Go 1.22 mux patterns ('POST /api/comparison'), so wrong methods get
405 from the stdlib for free. httptest runs handlers fully in memory;
the API test re-pins the 99/70/7 golden values end to end.
ComparisonResponse is added to tygo.yaml with a type mapping so the
generated web/src/types/api.ts reuses the engine's TS types.
cmd/spreadlab now serves on -addr (default localhost:8080); -table
keeps the CLI comparison as a sanity check.
2026-06-10 13:03:12 +02:00
c3f65a4029 docs engine: state the RNG design decision in rng.go
rng.go is a construction point, not an abstraction wall; components
take *rand.Rand directly (idiomatic Go dependency injection) and only
RunScenario creates streams, from config seeds. Decided 2026-06-10
over the alternatives (interface wall, folding newRand into
scenario.go).
2026-06-10 13:01:39 +02:00
9bae4a9ca0 types: generate TypeScript from engine structs with tygo
tygo is pinned via go.mod's tool directive (Go 1.24+): the tool's
version is locked like any dependency and runs as 'go tool tygo', no
global install. 'go generate ./...' regenerates web/src/types/engine.ts
from Config/Result and their json tags; the generated file is committed
so the frontend builds without Go installed. The go:generate directive
anchors in a root generate.go because generate runs commands from the
declaring file's directory and tygo reads tygo.yaml from the cwd.
go.mod's ignore directive (Go 1.25+) keeps ./... from crawling into
web/node_modules, where some npm packages ship stray .go files.
2026-06-10 12:58:13 +02:00
f58f33c3fd web: scaffold Vue 3 + Vite app (create-vue, lean options)
Verbatim output of 'npm create vue@latest web -- --ts --vitest
--eslint --prettier' so this commit shows exactly what the official
scaffold generates; our own changes come separately. Lean per the
agreed choices: no Router/Pinia until a second page or shared state
exists. node_modules is ignored via the scaffold's own web/.gitignore.
2026-06-10 12:55:43 +02:00
91ce0c1ca5 engine: benchmark baseline (HolmeKim, RunScenario)
Manual baseline, not a CI gate: go test -bench=. -benchmem.
First numbers on a Ryzen 7 5800X: HolmeKim ~25us/op (48KB, 671
allocs), full RunScenario ~45us/op (78KB, 681 allocs). Plenty fast
until milestone 5 runs thousands of cascades per request; this is the
'before' picture for that work.
b.Loop() (Go 1.24+) replaces the old 'for i := 0; i < b.N; i++'
pattern and prevents the compiler optimising the loop body away.
2026-06-10 12:50:53 +02:00
2a5b78cb9a refactor cmd: thin main, testable run(io.Writer), AllStrategies in engine
The strategy list moves into the engine (AllStrategies), where the API
and frontend will read it too: one source of truth, per the handoff.
main shrinks to the standard Go shell pattern: all work happens in
run(out io.Writer) error; main only maps the error to stderr and the
exit code. Writing to an interface instead of stdout is what lets
main_test.go capture output in a bytes.Buffer.
errcheck flagged every unchecked Fprintf, so formatting became pure
Sprintf string building with one checked write at the end: nicer than
discarding four errors with '_, _ ='.
2026-06-10 12:39:03 +02:00
e73db0bdd4 refactor engine: decompose Holme-Kim into builder methods
The generator's working state (graph, attachment pool, rng) moves into
a holmeKimBuilder struct so each algorithm step is a small named
method: attachNewNode, link, pickMutualFriend, degreeProportionalSample.
Max nesting drops from five levels to two. The RNG call order is
untouched, and the golden test (99/70/7 reached) plus the fixed-seed
determinism test prove behaviour is bit-for-bit identical.
Go bits: pointer receivers ((b *holmeKimBuilder)) let methods mutate
the builder; (value, ok) multiple returns are the idiom for 'may not
exist', as in pickMutualFriend.
2026-06-10 12:37:57 +02:00
45fdf7ffa4 engine: Config, scenario runner, golden regression tests; demo CLI
Config is the single source of truth for parameters (TS types will be
generated from these structs in milestone 2); all randomness flows from
its three seeds, so identical configs give identical results. Golden
test pins the default world: none=99/120 (82%), random=70/120 (58%),
most-connected=7/120 (6%). Same story as the prototype's 85/64/8 with
different dice; the ordering and the collapse are asserted explicitly,
exact Python numbers are out of scope by design.
'go run ./cmd/spreadlab' prints the three-scenario comparison.
This completes milestone 1 (engine ported, parameterised, tested).
2026-06-10 12:24:47 +02:00
c40e483ee6 engine: education strategies (random vs most-connected)
Two ways to spend the same education budget: a uniform random sample
(spray and pray) vs the highest-degree hubs, ties broken stably towards
lower node numbers so the pick is deterministic. The origin is never
educated; they post the fake. rng.Shuffle does a seeded Fisher-Yates,
so the random pick is reproducible too. min() is a builtin since
Go 1.21.
2026-06-10 12:08:39 +02:00
736cd9070d engine: edge thresholds and the independent cascade
One random threshold per DIRECTED edge, drawn once per world: scenarios
sharing thresholds differ only in who is educated, the prototype's
'same world, different lever' trick. The cascade is plain BFS in rounds;
an educated student receives the fake but never forwards it, which is
the entire effect of the lever. The result stores the first-reached
round per node (exactly what a frontend animation needs).
Tests hand-craft a 4-node line graph with exact thresholds, so every
expectation is exact: spread, directional blocking, educated cutoff.
2026-06-10 12:08:12 +02:00
7f06ea0d6f engine: Holme-Kim network generator (powerlaw_cluster_graph port)
Preferential attachment via an attachment pool that holds one entry per
edge endpoint, so uniform draws are degree-proportional: that is the
whole hub-forming mechanism. A triangle step closes friend-of-a-friend
links with probability triangleProb, giving friend-group clustering.
Semantics ported from networkx, NOT its RNG stream (per the handoff,
no cross-language number matching).
Tests are property-based: size, edge bounds, connectivity, hub
formation across seeds, plus exact determinism for a fixed seed.
'go test ./...', gofmt and golangci-lint all clean.
2026-06-10 12:05:43 +02:00
33a85cb720 engine: undirected simple graph with deterministic neighbour order
Adjacency lives in slices, not maps, on purpose: Go randomises map
iteration order between runs, and the engine's determinism guarantee
needs every graph walk to visit neighbours in the same order. AddEdge
mirrors networkx semantics (duplicates and self loops are no-ops) so
the Holme-Kim port can lean on the same behaviour.
Receiver names stay short per Go convention (g *Graph); everything
else uses descriptive names.
2026-06-10 11:58:45 +02:00
33d40a18ea engine: seeded RNG helper, first tests
math/rand/v2 (Go 1.22+) replaces the old math/rand: PCG generator,
no global Seed(), and rand.New(rand.NewPCG(seed, 0)) gives an isolated
deterministic stream. Each randomness consumer (graph, thresholds,
education sampling) will get its own stream so levers vary independently.
Tests live next to the code as *_test.go; 'go test ./...' runs them all.
'for i := range 100' is Go 1.22 range-over-int.
2026-06-10 11:57:39 +02:00
d8125655e6 batman
go.mod declares the module path (github.com/JustinZeus/spreadlab); every
import inside the repo is spelled relative to it. Layout follows the
standard Go shape: cmd/<binary>/main.go per executable, internal/ for
packages other modules may not import (the compiler enforces this).
No engine code yet, just a placeholder main that proves 'go build' works.
2026-06-10 11:57:10 +02:00