fix(docs): fix formatting issues in README.md

- adapt meta-/code-/obs- rules to the local Python benchmark pipeline
- replace db-sql-ddl, code-config-env-scope, test-e2e-pytest with pipeline equivalents
- add code-python-style, data-determinism, data-naming-units, bench-methodology, build-pipeline-tasks
- normalize specs and README typography to ASCII per code-data-formatting
- rewrite root CLAUDE.md trigger table; add .cursorrules and .gitignore
- specs: pipeline plan and task specs 00-11
- rules: 19 binding rule files adapted for this project
- docs: CLAUDE.md rule-trigger table
- config: .cursorrules commit convention, .gitignore excluding out/ and .venv/
- docs: rewrite README.md with pipeline diagrams, setup guide, result placeholders
- config: requirements.txt for the closed dependency list
- datagen: make_lab_config.py writes out/config/lab_config.yaml and .done marker
This commit is contained in:
administrator
2026-07-11 13:39:13 -04:00
parent d8353953b0
commit b173ac82a9
55 changed files with 3755 additions and 9 deletions

View File

@@ -0,0 +1,112 @@
# Bench / Methodology - isolation, repetitions, timeouts, honest reporting
**Purpose + scope.** The measurement discipline for the benchmark matrix
(Q1-Q7 x 5 formats, spec [08](../specs/08_benchmark_queries.md) /
[09](../specs/09_benchmark_execution.md)) and for the extrapolation and
reporting built on it (specs 10-11). Binding for `bench_runner.py`, every
query implementation, `extrapolate.py`, and `report.py`.
The product of this project IS these measurements; a sloppy protocol
invalidates everything downstream.
---
## 1. Cell isolation - one subprocess per measurement
- Every benchmark cell (format x query x run) executes in its OWN
subprocess, so peak RSS, CPU time, and IO counters attribute to exactly
that cell.
- The runner samples the child with `psutil` every 50 ms for peak RSS;
CPU time is user+sys at exit; read bytes from `io_counters`.
- Nothing else runs in the child: no logging setup beyond minimum, no
imports the query does not need - the measurement must not pay for the
harness.
## 2. Repetition protocol
- Per cell: **1 warm-up run (discarded) + 3 measured runs.** Report the
median in `results_median.csv`; keep min/max in the raw data.
- **Randomize the cell execution order** and record the actual order in
the raw results - fixed order lets cache effects masquerade as format
differences.
- SQLite additionally reports run 1 (cold) and run 3 (warm) separately
(spec 08).
- Never average TIMEOUT/ERROR runs into medians; a cell with a failed run
reports the failure, not a prettier subset.
## 3. Timeouts and failures are data
- Hard per-run timeout: **30 minutes.** On expiry the runner records
`TIMEOUT` for that cell and CONTINUES the matrix (an expected outcome:
RDF Q5). Never retry a timeout hoping for a better number.
- A crashed cell records `ERROR` the same way (see
[code-error-handling.md](code-error-handling.md) pattern C).
- In extrapolation and scoring, TIMEOUT/FAIL project to score 0 - they
are never silently dropped from tables or charts (spec 10/11 flags:
projection > 1 h = `IMPRACTICAL`, > 24 h = `FAIL`).
## 4. Result correctness gates the measurement
- Every measured run validates its result rows against the canonical
checksum (sorted rows, float tolerance 1e-9) and records `checksum_ok`
(see [test-pipeline-validation.md](test-pipeline-validation.md)).
- A fast run with `checksum_ok = false` is an INVALID measurement - it
must never enter medians, extrapolation, or the report.
- **Q5 reads raw `friction_cycles` in every format** - summary tables and
materialized views are forbidden for it; verify PostgreSQL did not
satisfy it from `track_summary`
(`EXPLAIN (ANALYZE, BUFFERS)` is recorded for PostgreSQL runs anyway).
## 5. Fairness across formats
- Identical result-set contract for all 5 implementations of each query;
no format answers a simplified question.
- Each format uses its idiomatic strength honestly: CSV Q1 reads the one
target file by path; JSON Q1 loads the one coupon file; SQL uses its
indexes. What is forbidden is capping or sampling (`LIMIT`, partial
scans) that changes the answer.
- The pandas CSV variant is measured as a SEPARATE variant, never mixed
into the plain-CSV numbers (spec 08).
- Storage sizes compare like with like: for every format record both the
working footprint and the gzip archival footprint in
`storage_sizes.csv` (spec 09).
## 6. Environment honesty - this machine, stated limits
- Record the hardware and software environment (CPU model, cores, RAM,
disk type, OS, engine versions) once per benchmark session into the
raw results; the report cites it.
- **Cold-cache protocol on this host:** the spec's page-cache drop
(`/proc/sys/vm/drop_caches`) is Linux-only; this project runs on
Windows Server 2025, where it is unavailable. The cold-cache matrix
pass is therefore SKIPPED and marked as such in results and report -
never approximated by a fake (e.g. file re-copy tricks) without a
rules update describing the method.
- Quiesce the machine during measured runs (no parallel pipeline tasks,
no background loads); the runner runs cells sequentially by design.
## 7. Extrapolation discipline (task 10)
- Scaling laws per access pattern are those fixed in spec 10 (O(1),
O(log n + k), O(k log n), O(n), O(n / min(cores, partitions)));
constants calibrate on the measured 150 MB point.
- Every projected number in the report is labeled as projected, with its
scaling assumption; measured and projected values never mix in one
column unlabeled.
- Hardware cost figures come from `hw_prices.yaml`
(see [code-config-yaml.md](code-config-yaml.md)), never inlined.
## 8. Anti-patterns
- **Running cells in-process** - RSS/CPU attribution becomes fiction.
- **Fixed execution order** - cache warmth leaks between formats.
- **Retrying or dropping slow runs** - the slow run IS the result.
- **Averaging over a TIMEOUT** or reporting medians of fewer than 3 valid
runs without flagging it.
- **A "fast" result that fails the checksum** entering any downstream
table.
- **Tuning one format's query while leaving others naive** - fairness is
per-scenario, not per-format-champion.
- **Faking cold-cache on Windows** without a documented, rules-approved
method.
- **Presenting projected numbers as measured** in tables or charts.

View File

@@ -0,0 +1,103 @@
# Build / Pipeline tasks - the ./out tree, .done markers, task order
**Purpose + scope.** How the 11 pipeline tasks compose: where artifacts
live, how completion is signaled, what order tasks run in, and what git
does and does not track. Binding for every task implementation and for
anyone running the pipeline.
---
## 1. The artifact tree - everything under `./out/`
All generated artifacts live under `./out/` relative to the repo root
(spec 00_PLAN):
```
out/
config/ lab_config.yaml, hw_prices.yaml
csv/ the canonical raw corpus + MANIFEST.csv
json/ full/ (per-coupon JSON-LD), hybrid/dataset.jsonld
sqlite/ tribo.db
pg/ tribo.dump (pg_dump -Fc; the live DB is in PostgreSQL)
rdf/ dataset.nt.gz, dataset.ttl, oxigraph_store/
bench/ queries/, expected/, results_raw.csv, results_median.csv,
storage_sizes.csv, extrapolation.csv, hardware_sizing.csv
report/ REPORT.md, charts/, tables/, process_flow.md, diagrams/
.done/ completion markers, one per task
```
- A task writes ONLY under its own output paths; it treats its inputs as
read-only.
- Tasks communicate exclusively through these artifacts - never through
shared in-process state
(see [code-python-style.md](code-python-style.md)).
- **`./out/` is git-ignored** (except nothing - the whole tree). Sources
of truth are the specs, the rules, and the code; artifacts are
reproducible from them ([data-determinism.md](data-determinism.md)).
Never commit generated artifacts "for convenience".
## 2. Completion markers
- Each task NN writes `./out/.done/<NN>.ok` after - and only after - its
outputs are complete and validated. The marker body carries the
headline counts (rows, bytes, triples) downstream tasks check
(see [test-pipeline-validation.md](test-pipeline-validation.md)).
- A task verifies its dependencies' markers before starting and aborts
loudly if one is missing.
- Re-running a task: delete its own marker first, regenerate outputs,
rewrite the marker. If its outputs changed, downstream markers are now
stale - delete them too (the dependency graph below says which).
## 3. Task graph and execution order
```
01 -> 02
01 -> 03 -> (04 | 05 | 06 | 07) -> 08 -> 09 -> 10 -> 11
09 ------> 11
```
- Sequential order: 01, 02, 03, then 04-07 (independent, may run in
parallel), 08, 09, 10, 11.
- 02 (process diagrams) needs only 01 and can run any time after it.
- Task 09 (benchmark execution) must run WITHOUT 04-07 style parallel
work in the background - measurements need a quiet machine
([bench-methodology.md](bench-methodology.md) section 6).
## 4. `storage_sizes.csv` - the shared append contract
- Location: `./out/bench/storage_sizes.csv`; columns:
`format, variant, bytes`.
- Tasks 04-07 APPEND their format's rows (e.g. `json,full,...`,
`json,hybrid,...`, `sqlite,db,...`, `pg,live,...`, `pg,dump,...`,
`rdf,nt_gz,...`, `rdf,store,...`); task 09 appends the gzip archival
variants and consolidates.
- Appends are idempotent per (format, variant): a re-run REPLACES its own
prior rows, never duplicates them and never touches other formats'
rows.
- RDF load wall-time and similar per-format load metrics recorded per
spec 07 go into the task's `.done` marker and the report, not into
extra ad-hoc files.
## 5. Runs are resumable, not restartable-from-zero
- The marker system exists so a failed pipeline resumes at the failed
task; do not delete the whole `./out/` tree to retry one task.
- Conversely, after a deliberate corpus regeneration (03), ALL downstream
markers and artifacts (04-11) are stale and must be removed - a mixed
tree of old and new artifacts is worse than an empty one
([meta-no-assumptions.md](meta-no-assumptions.md) applies before any
such deletion).
## 6. Anti-patterns
- **Writing an artifact outside `./out/`** or a task writing into another
task's output directory.
- **Committing `./out/` content to git.**
- **A marker written on partial success** - see
[code-error-handling.md](code-error-handling.md).
- **Running benchmarks while converters are still running.**
- **Duplicate rows in `storage_sizes.csv`** after re-runs - replace your
own rows.
- **Hand-editing generated artifacts** (a "quick fix" inside
`MANIFEST.csv` or a result CSV) - regenerate them through the owning
task instead.

View File

@@ -0,0 +1,61 @@
# Code comments - write WHY, not WHAT
**Binding** for all source files: Python, SQL, SPARQL, PowerShell, YAML, shell.
## Rule
Default to **no comment**. Add one only when the *why* is non-obvious from
the code. If removing the comment wouldn't confuse a future reader, delete it.
A comment earns its place when it captures something the code can't:
hidden constraint, ordering invariant, workaround for a named bug, vendor
quirk, cross-file invariant the type system can't enforce.
## Delete on sight
```python
# Read the manifest file
manifest = read_manifest(csv_root) # function name says it
```
```python
# Increment counter
counter += 1 # pure restatement
```
```python
# Added for the benchmark rework (see task 09)
def run_cell(...): # belongs in commit message, rots in code
```
```python
# TODO: refactor later
# no owner, no trigger - decoration
```
## Keep
```python
# Q5 must scan raw friction_cycles - track_summary is forbidden for this
# query in every format (spec 08); do not "optimize" this into the summary.
```
Cross-file invariant a refactor must preserve.
```python
# Column order matches MANIFEST.csv writer in generate_data.py - keep in sync.
```
Cross-file invariant the type system can't enforce.
```sql
-- BRIN, not b-tree: cycle values are physically ordered within each
-- partition, so BRIN gives the same pruning at ~1% of the index size.
```
Non-obvious engineering decision with a reason.
## Length
Inline: one line. Block: 1-4 lines. If you need more than a paragraph,
the right home is a rule or spec doc - link it from a one-line comment.
Stale comments are worse than missing ones. When you touch a file, fix or
delete comments whose *why* no longer holds.

View File

@@ -0,0 +1,94 @@
# Code / Config - all tunables live in YAML, no magic numbers, no .env
**Purpose + scope.** Governs where every configurable value lives. Binding
for all Python code in this repository. Any new configurable parameter MUST
follow this rule when deciding where it goes.
The goal: one authoritative source per parameter, so regenerating any
artifact with the same config is byte-identical, and changing a parameter
never requires hunting constants across scripts.
---
## 1. The two YAML configs
| File | Owns | Written by |
|---|---|---|
| `./out/config/lab_config.yaml` | The laboratory model: volumes (batches / wafers / coupons / tracks / cycles), the deposition matrix, physical-model coefficients, nominal test conditions, **the random seed 20260711** | Task 01, from the spec; regenerated only deliberately |
| `./out/config/hw_prices.yaml` | Hardware cost parameters for extrapolation (RAM $/GB, NVMe $/TB, server base prices) | Task 10 seeds editable defaults; the operator may edit |
Rules:
- **Scripts READ these files; they never duplicate values from them.**
Task 03 reads volumes, coefficients, and the seed from
`lab_config.yaml` - it does not hardcode 588 coupons or seed 20260711.
Task 10 reads prices from `hw_prices.yaml` - it does not inline dollar
figures.
- A parameter needed by more than one task lives in the YAML, never
copy-pasted between scripts.
- New tunables go into the appropriate YAML with a self-describing key;
if neither file fits, propose a new config file in a docs/rules update
first.
## 2. Spec-fixed constants - named, sourced, at the top
Values fixed by the specs that are NOT operator-tunable (the 120-200 MB
size gate, the 30-minute cell timeout, the 50k-row transaction batch, the
50 ms RSS sampling interval) are module-level named constants at the top of
the owning script, each with a one-line comment naming the spec:
```python
SIZE_GATE_MB = (120, 200) # abort bounds, spec 03
CELL_TIMEOUT_S = 30 * 60 # hard per-run timeout, spec 09
```
A bare number inline in the logic (`if size > 209715200`) is forbidden.
## 3. The PostgreSQL DSN - one env var, never hardcoded, never logged
The only external resource is the local PostgreSQL 16 instance. Its DSN:
- Comes from the environment variable **`TRIBO_PG_DSN`**; default when
unset: `postgresql://postgres@localhost:5432/tribo`.
- Is read in ONE shared helper (e.g. `common/pg.py: get_dsn()`); scripts
call the helper, never `os.environ` directly.
- Is never hardcoded in a script, never committed in a config file, and
never logged (a DSN may embed a password - see
[obs-python-logging.md](obs-python-logging.md)).
There are no other secrets in this project. There is no `.env` file; do
not introduce one.
## 4. CLI flags - per-run choices only
Command-line flags are for choices that vary per invocation, not for
model parameters: `--log-level`, `--dry-run`, an output-root override, a
`--limit` for smoke runs. A physical coefficient or a volume as a CLI flag
is forbidden - it would silently fork the config out of `lab_config.yaml`.
## 5. Anti-patterns
- **Hardcoding a value that exists in `lab_config.yaml`** (seed, volumes,
coefficients) - the YAML is the single source; drift here breaks
reproducibility silently.
- **Adding a `.env` file or reading `os.environ` scattered through
scripts** - the DSN goes through the one shared helper; nothing else
lives in the environment.
- **Inline magic numbers** for spec-fixed gates and timeouts - name them
at the top with the spec reference.
- **Dollar prices inlined in `extrapolate.py`** - they live in
`hw_prices.yaml` so the operator can re-cost without touching code.
- **A model parameter as a CLI flag** - per-run flags are for run
mechanics, not for the lab model.
- **Committing `./out/config/` generated files to git as if they were
sources** - the YAML under `./out/` is a generated artifact; its source
of truth is spec 01 (see
[build-pipeline-tasks.md](build-pipeline-tasks.md)).
## 6. Related documents
- [data-determinism.md](data-determinism.md) - the seed contract.
- [build-pipeline-tasks.md](build-pipeline-tasks.md) - the `./out/` tree
and what is git-ignored.
- [obs-python-logging.md](obs-python-logging.md) - `--log-level` flag; DSN
never logged.

View File

@@ -0,0 +1,132 @@
# Code / Data / Formatting - ASCII-only typographic characters in source files
**Binding scope.** Strings that end up in any of the following MUST use ASCII
typographic characters; never their Unicode "smart" equivalents:
- Python source, SQL and SPARQL query files
- Generated data and artifacts written to disk (CSV, JSON-LD, N-Triples,
Turtle, MANIFEST and benchmark result CSVs, log messages)
- Configuration files (`.yaml`, `.json`, `.toml`)
- PowerShell / shell scripts (any file processed by Windows tooling)
- Markdown documentation (`docs/`, `README.md`, `CLAUDE.md`) - the specs
were normalized to ASCII typography on 2026-07-11 and stay that way
**Long dashes are an absolute exception, no carve-outs.** Em-dash (U+2014)
and en-dash (U+2013) MUST be replaced with ASCII hyphen-minus (`-`) in
**every** file in this repository, including markdown documentation, code
comments, and report prose. Reason: long dashes are visually
indistinguishable from a hyphen in many monospace fonts, break diffs / grep /
IDE search, and round-trip badly through Windows tooling chains (section 2).
---
## 1. The substitution table
| Unicode char | Codepoint | Name | ASCII replacement |
|---|---|---|---|
| em-dash | U+2014 | em-dash | `-` (single hyphen-minus) |
| en-dash | U+2013 | en-dash | `-` (single hyphen-minus) |
| horizontal bar | U+2015 | horizontal bar | `-` |
| minus sign | U+2212 | minus sign | `-` |
| ellipsis | U+2026 | horizontal ellipsis | `...` (three ASCII dots) |
| left double quote | U+201C | left double quotation mark | `"` |
| right double quote | U+201D | right double quotation mark | `"` |
| left single quote | U+2018 | left single quotation mark | `'` |
| right single quote | U+2019 | right single quotation mark | `'` |
| left guillemet | U+00AB | left guillemet | `"` |
| right guillemet | U+00BB | right guillemet | `"` |
| nbsp | U+00A0 | non-breaking space | regular space |
**Single hyphen, not double.** When replacing an em-dash that separates two
clauses, use a single `-` surrounded by spaces (`text - clause`), NOT `--`.
**Ranges use a single hyphen:** `Q1-Q7`, `120-200 MB`, `0.3-1.1 um` - never
an en-dash.
## 2. Why - documented failure modes
1. **PowerShell 5.1 reads no-BOM files as Windows-1252.** Non-ASCII UTF-8
bytes become mojibake and break string-literal parsing. This project is
developed on Windows; any script or config touched by PowerShell is
exposed.
2. **Windows -> git-bash -> tar / packaging round-trip.** At any step an
unset locale or wrong encoding assumption produces mojibake. A UTF-8 byte
sequence for U+2014 interpreted under a non-UTF-8 locale renders as a
`?` replacement character in the packaged output.
3. **Copy-paste from Word / Google Docs / Notion / web articles.** These
tools auto-substitute typography (`"` -> smart quotes, `'` -> smart
apostrophe, `--` -> em-dash, `...` -> ellipsis) silently. The
substitution survives copy into any editor and lands in source files
unnoticed. The original specs arrived with en/em dashes this way.
4. **Data files must diff and grep cleanly.** The CSV corpus, MANIFEST, and
benchmark results are compared by checksum and processed by streaming
parsers; a stray typographic character breaks byte-level reproducibility.
## 3. Where the rule does NOT apply
- **Scientific and unit notation is content, not typography.** Characters
such as `µ` (micro), `±`, `°C`, `×`, `Ø`, and Greek letters (`τ`, `σ`) in
formulas inside the specs are meaningful scientific notation and are kept.
Note the boundary: in **column names and code identifiers** the units are
always plain ASCII (`_um`, `_nm`, `_GPa` - see
[data-naming-units.md](data-naming-units.md)); the scientific characters
live only in explanatory prose.
- **International language strings** (Cyrillic, CJK, etc.) are meaningful
content, not typographic substitutions. UTF-8 throughout is the right
answer. (Repository artifacts are English; this matters only for quoted
material.)
- **Imported reference materials** in `docs/examples/` are kept verbatim as
received; they are not binding documents.
## 4. Detection / enforcement
There is no automated check for this rule yet - **not in CI yet**. The rule is
enforced by review and by the editing discipline in this file. The intent of a
future detector is documented here so it can be built consistently:
- **What it flags.** Any occurrence of the Unicode codepoints in the section 1
substitution table inside in-scope files. The section 3 exemptions
(scientific notation, international content, `docs/examples/`) are the only
carve-outs.
- **Exit behavior.** Non-zero exit when any flagged codepoint is found under
the scan roots, so it can later gate a commit hook or a build step.
- **Tooling.** When this detector is created it MUST be a PowerShell script
(this is a Windows project) and live under `tools/`.
## 5. Examples
### Good
```python
# safe through every Windows tooling step
print("Corpus size 152.3 MB - within the 120-200 MB gate.")
label = "steady-state COF, cycles 500-1000"
```
### Bad
```python
# em-dash literal (U+2014) - mojibake risk on PowerShell read/write
print("Corpus size 152.3 MB [U+2014] within the gate.")
# en-dash range (U+2013) - breaks grep for "Q1-Q7"
label = "queries Q1[U+2013]Q7"
```
## 6. Anti-patterns
- **Copy-pasting strings from a word processor or web page** into source /
specs / config without normalization.
- **Trusting tar / packaging to "preserve as-is".** They preserve bytes; the
*encoding interpretation* is what gets lost when locale or encoding
declaration is wrong on either end.
- **Assuming the source file encoding alone is enough.** The source file may
be fine - the failure is at downstream consumers (PowerShell read, build
write, OS locale).
- **Using `--` instead of `-` "for emphasis".** Single ASCII hyphen
surrounded by spaces is the project's chosen separator.
- **Putting `µ` or `°` into a column name or identifier** - units in names
are ASCII (`_um`, `_C`); see [data-naming-units.md](data-naming-units.md).

View File

@@ -0,0 +1,181 @@
# Code / Error handling - never swallow, always surface, fail loud
**Purpose + scope.** Governs every `try:` / `except:` / `raise` site in
Python code in this repository (data generation, format converters, benchmark
runner, extrapolation, reporting, helper tools). Binding for all contributors.
The pipeline's core failure principle: **a wrong artifact is worse than no
artifact.** Every task's output feeds downstream tasks; silently degraded
data poisons every result built on it. When in doubt, abort with a clear
message and a non-zero exit code.
---
## 1. The rule
1. **Never `except: pass`.** A bare swallow is always a bug. Forbidden
absolutely.
2. **Never `except Exception: logger.warning("...") + continue`** for
anything that was not an EXPECTED, documented, transient condition. If
you swallow and continue, you must be able to state why the program can
correctly proceed.
3. **Always `logger.error(..., exc_info=True)` in `except` blocks.**
`exc_info=True` captures the traceback into the log record (see
[obs-python-logging.md](obs-python-logging.md)). Without it the original
exception is lost.
4. **Re-raise unless you have a defined recovery path.** If the caller
cannot continue meaningfully without the failed operation, re-raise
after logging.
5. **Surface to the right channel:**
- **CLI script (every pipeline task):** print the error to stderr and
exit non-zero. A task that failed MUST NOT write its
`./out/.done/<task>.ok` marker (see
[build-pipeline-tasks.md](build-pipeline-tasks.md)).
- **Validation gate (row counts, checksums, size bounds):** a failed
gate is an abort, not a warning. Example: task 03 aborts when the
generated tree is outside 120-200 MB; converters abort when row counts
do not match `MANIFEST.csv`.
- **Library / helper function:** re-raise; let the caller decide.
6. **Defensive "this cannot happen" branches must surface, not silently
`return`.** If you reach a defensive branch, log at ERROR with the
branch name and the inputs that got you there, then raise.
## 2. The exception ladder - the only three sanctioned swallow patterns
Code may swallow ONLY in these three documented patterns. Anything else
re-raises after logging.
### Pattern A: documented, recoverable transient (retry)
```python
for attempt in range(MAX_RETRIES):
try:
return do_thing()
except (psycopg.OperationalError, TimeoutError) as exc:
logger.warning("attempt %d/%d failed: %s", attempt + 1, MAX_RETRIES, exc)
time.sleep(backoff(attempt))
# All retries exhausted - surface.
logger.error("do_thing failed after %d retries", MAX_RETRIES)
raise
```
Justified: the per-attempt warning is acceptable; the final failure
re-raises.
### Pattern B: optional best-effort cleanup
```python
def _close_conn(conn) -> None:
try:
conn.close()
except Exception:
# Cleanup is best-effort; the operation already finished, log and continue.
logger.warning("closing connection failed", exc_info=True)
```
Justified: the comment names why continuing is correct; the traceback is
still captured.
### Pattern C: benchmark-cell isolation - record and continue the matrix
```python
try:
result = run_cell(fmt, query, timeout_s=30 * 60)
except CellTimeout:
logger.error("cell %s/%s timed out", fmt, query)
write_result_row(fmt, query, status="TIMEOUT") # recorded, not hidden
except Exception:
logger.error("cell %s/%s failed", fmt, query, exc_info=True)
write_result_row(fmt, query, status="ERROR")
```
Justified: spec 09 requires the benchmark matrix to survive a failing cell
(RDF Q5 is expected to time out). The failure is RECORDED as data
(`TIMEOUT` / `ERROR` in `results_raw.csv`), never silently dropped, and the
run continues with the next cell. This pattern applies ONLY to the benchmark
runner's per-cell boundary - a failure in the runner itself (setup, result
writing) still aborts.
## 3. Loading into SQLite / PostgreSQL - all-or-nothing per table
A converter that half-loads a table leaves a database that LOOKS complete
and silently skews every benchmark run against it.
1. Wrap each table's bulk load in a transaction; on any error roll back
and abort the converter with a non-zero exit.
2. After loading, validate row counts against `MANIFEST.csv` and fail loud
on any mismatch (see
[test-pipeline-validation.md](test-pipeline-validation.md)).
3. Never write the task's `.done` marker after a partial load.
## 4. Anti-patterns
- **`except: pass`** - forbidden absolutely.
- **`except Exception: logger.warning("could not X")` + continue** -
forbidden unless the function documents that "could not X" is a
recoverable expected outcome. If it is, phrase it "X skipped (expected
condition Y)" so a reader does not grep "could not" hunting for bugs.
- **Catching `BaseException`** - forbidden (swallows `KeyboardInterrupt`,
`SystemExit`). Use `Exception`.
- **`logger.error("X")` without `exc_info=True` in an `except` block** -
forbidden. The traceback IS the diagnostic.
- **`logger.error(str(exc))`** - forbidden; redundant with `exc_info=True`
and loses type and stack.
- **Returning `None` to signal failure** from a function whose contract is
"return T" - forbidden. Either raise, or return `T | None` with
documented semantics the caller checks.
- **`raise Exception("...")`** - forbidden. Use a specific class:
`ValueError` / `RuntimeError` / `KeyError` for trivial cases, a custom
domain exception subclass (e.g. `ManifestMismatchError`) for domain
conditions.
- **`raise` chained without context** - inside an `except`, prefer
`raise NewError(...) from exc` so the original cause stays visible in the
chained traceback. A bare `raise` that re-raises the same exception is
fine; `raise SomethingElse(...)` without `from exc` is forbidden.
- **Writing a `.done` marker in a `finally` block** - the marker means
"completed successfully", never "attempted".
- **Clamping / repairing out-of-range generated values silently** - a
physical-model bug must surface, not be masked by a `max(0, ...)` added
"to be safe". Deliberate clamps defined by the spec (e.g. COF floor 0.12)
are model logic, not error handling.
## 5. Defensive branches
```python
# Good:
def open_store(fmt: str) -> Store:
if fmt == "sqlite":
return SqliteStore()
if fmt == "postgresql":
return PgStore()
# Defensive: a new format added without updating this factory lands here.
logger.error("open_store: unknown format=%r", fmt)
raise ValueError(f"unknown storage format: {fmt!r}")
```
The silent-`return None` form lets the unknown value propagate and breaks
downstream code with a confusing `AttributeError` on the wrong line.
## 6. Logging vs surfacing - the decision
For a caught exception, ask in order:
1. **Will the pipeline produce a wrong artifact if I continue?** Re-raise /
abort.
2. **Is this expected and recoverable in the caller's contract?** Log at
WARNING with `exc_info=True`, return a sentinel the caller checks.
3. **Is this an isolated benchmark cell (pattern C)?** Log at ERROR, record
the status as data, continue the matrix.
4. **Is the process going down anyway?** Log at CRITICAL with
`exc_info=True`, then `sys.exit(1)` or let it propagate.
## 7. Related documents
- [obs-python-logging.md](obs-python-logging.md) - logging API and severity
levels.
- [test-pipeline-validation.md](test-pipeline-validation.md) - the
validation gates that turn silent corruption into loud failures.
- [build-pipeline-tasks.md](build-pipeline-tasks.md) - `.done` marker
semantics.
- [bench-methodology.md](bench-methodology.md) - the TIMEOUT contract for
benchmark cells.

View File

@@ -0,0 +1,93 @@
# Code / Python style - tabs, 3.11+, closed dependency list, streaming
**Purpose + scope.** Baseline style and dependency policy for every Python
file in this repository (pipeline tasks 01-11, query implementations,
helper tools). Binding for all contributors.
---
## 1. Language and formatting
- **Python 3.11+.** Use modern stdlib freely (`pathlib`, `dataclasses`,
`tomllib` if needed); no compatibility shims for older versions.
- **Indentation: TABS.** Set by the project owner and repeated in the
specs (00_PLAN, 03, 09). Every Python file in this repository indents
with tab characters, never spaces. Configure the editor per file type;
do not "fix" tab-indented files to spaces.
- `from __future__ import annotations` at the top of every module.
- Type hints on public function signatures; internal helpers at author's
discretion.
- English for all identifiers, docstrings, and comments (see
[meta-language-style.md](meta-language-style.md) - Russian is the
conversation language, not the code language).
- Comments follow [code-comments-policy.md](code-comments-policy.md):
default to none, keep only non-obvious WHY.
## 2. Dependencies - a closed list
The stdlib is the default. The ONLY permitted third-party packages:
| Package | Used by | For |
|---|---|---|
| `numpy` | 03 | vectorized generation, seeded RNG |
| `PyYAML` | 01, 03, 10 | reading/writing the YAML configs |
| `rdflib` | 04, 07 | JSON-LD validation, RDF serialization |
| `oxigraph` | 07, 09 | embedded queryable triplestore |
| `ijson` | 08, 09 | streaming JSON parsing |
| `psycopg` (v3) | 06, 08, 09 | PostgreSQL COPY and queries |
| `psutil` | 09 | RSS / CPU / IO measurement |
| `matplotlib` | 11 | report charts |
| `pandas` | 08 (optional) | ONLY as the separately-measured second CSV query variant; never in the pipeline itself |
Adding any other package requires updating this table first. In
particular forbidden: ORMs, logging frameworks
([obs-python-logging.md](obs-python-logging.md)), CLI frameworks
(argparse suffices), `requests`/HTTP clients (nothing to call).
Task 03 (data generation) is stricter by spec: **stdlib + numpy only.**
## 3. Script shape - every task is a self-contained CLI
- One entry script per pipeline task, runnable as
`python <script>.py [flags]`.
- `argparse` with `--log-level` (see
[obs-python-logging.md](obs-python-logging.md)) and task-appropriate
per-run flags ([code-config-yaml.md](code-config-yaml.md) section 4).
- A module docstring stating the task number, what it produces, and its
spec file.
- `main() -> int` returning the exit code; `sys.exit(main())` under
`if __name__ == "__main__":`.
- Exit code 0 only on full success (outputs written, validation passed,
marker written).
## 4. Streaming discipline - the corpus never fits in memory by design
The generated corpus is ~150 MB and its scaled-up target is 600 GB; code
must behave as if the data never fits in RAM:
- **Write generated rows as you produce them**; never accumulate the full
cycle/loop dataset in a list (spec 03: "stream rows; never hold the
full cycle dataset in memory").
- **Read CSV/JSON with iterators** (`csv.reader` row-by-row, `ijson`
events); never `read()` a bulk file whole or `json.load` the full
corpus.
- Batch database work (`executemany` / `COPY` in chunks), keeping only
the current batch in memory.
- Bounded aggregation state is fine (per-track summaries, counters);
unbounded accumulation of raw rows is the thing being forbidden.
## 5. Anti-patterns
- **Spaces for indentation** in Python files - the project standard is
tabs.
- **An unlisted third-party import** - extend the section 2 table first
or use the stdlib.
- **`pd.read_csv` in pipeline code** - pandas exists only as the
separately-measured Q-variant in spec 08.
- **Loading a bulk artifact whole** (`json.load` on the full corpus,
`readlines()` on a cycles CSV) - stream it.
- **A task importable only from a specific CWD** - resolve paths from a
repo-root anchor, not the current directory.
- **Two tasks sharing mutable module state** - tasks communicate through
`./out/` artifacts only (see
[build-pipeline-tasks.md](build-pipeline-tasks.md)).

View File

@@ -0,0 +1,85 @@
# Data / Determinism - one seed, byte-identical regeneration
**Purpose + scope.** The reproducibility contract for all generated data.
Binding for task 03 (CSV generation) and for every piece of code that
produces values which end up inside the corpus or its conversions.
The entire evaluation rests on this: benchmark results are only comparable
because every format holds EXACTLY the same data, and the study is only
reviewable because anyone can regenerate that data bit-for-bit.
---
## 1. The seed
- **The global seed is `20260711`.** It lives in `lab_config.yaml`
(written by task 01) and is READ from there - never hardcoded in a
generation script (see [code-config-yaml.md](code-config-yaml.md)).
- Changing the seed is a project-level decision, never a side effect. It
invalidates the whole `./out/` tree, every checksum, and every measured
result (see [meta-no-assumptions.md](meta-no-assumptions.md)).
## 2. Sanctioned randomness - seeded generators only
- The ONLY random sources are `random.Random(seed)` and
`numpy.random.default_rng(seed)`, constructed explicitly from the
config seed.
- Derive per-entity generators deterministically when parallel or
out-of-order generation needs them (e.g.
`default_rng([seed, batch_index, coupon_index])`) - never from time,
never from a global unseeded state.
- Forbidden anywhere in generation code: module-level `random.*` calls,
`numpy.random.*` legacy global functions, `os.urandom`, `uuid.uuid4`,
`hash()`-dependent ordering (Python string hashing is salted per
process).
## 3. No wall-clock in the data
- Every timestamp inside the corpus is SIMULATED - computed from the lab
schedule model - never `datetime.now()` / `time.time()`.
- Real wall-clock time may appear only in logs and in measurement records
(benchmark wall times in `./out/bench/` are measurements, not corpus
data).
## 4. Deterministic everything else
Randomness is not the only nondeterminism; all of these are binding:
- **Iteration order.** Iterate in a defined order (sorted paths, explicit
index loops). Never rely on `os.listdir` / `glob` / `set` / dict-of-set
ordering for anything that affects output. Sort directory listings
before use.
- **Float formatting.** Format floats with an explicit spec (e.g.
`f"{x:.6g}"`), chosen once per column family; repr drift between runs
or Python versions must not change file bytes.
- **Line endings and encoding.** Generated files are UTF-8 with `\n`
newlines, on every platform (`open(..., newline="")` for csv writers).
- **Parallelism.** If generation is parallelized, each worker's output is
deterministic (derived generators, fixed partitioning) and the merge
order is fixed.
## 5. The proof - MANIFEST sha256
- `./out/csv/MANIFEST.csv` records sha256 per generated file (spec 03).
**Regenerating with the same `lab_config.yaml` MUST reproduce every
sha256 exactly.** This is the acceptance test for this whole rule -
run generation twice when touching generator code and diff the
manifests.
- Downstream conversions must be equally deterministic in CONTENT (same
rows, same values); byte-identity of database files is not required
(SQLite/PostgreSQL internals may differ), which is why their gate is
row counts + query-result checksums instead
(see [test-pipeline-validation.md](test-pipeline-validation.md)).
## 6. Anti-patterns
- **Hardcoding `20260711`** in a script instead of reading the config.
- **An unseeded or globally-seeded random call** anywhere in generation.
- **`datetime.now()` in corpus values** - simulated schedule only.
- **Ordering that depends on the filesystem or hash salting** - sort
explicitly.
- **"Harmless" float repr changes** - a formatting change is a corpus
change; it must be deliberate and re-baselines every checksum.
- **Regenerating the corpus to "refresh" it** - identical input config
must yield identical output; if bytes changed without a config change,
that is a determinism bug to fix, not a new baseline to accept.

View File

@@ -0,0 +1,120 @@
# Data / Naming and units - IDs, timestamps, SI-explicit column names
**Purpose + scope.** The naming grammar for entities, timestamps, and
measurement columns across ALL representations of the corpus: CSV, JSON-LD,
SQLite, PostgreSQL, RDF. Binding for generation, conversion, queries, and
reporting. This file also fixes two points the specs leave ambiguous
(sections 3 and 4).
---
## 1. Entity identifiers
The hierarchy encodes containment left-to-right; each level appends one
segment:
| Entity | Code grammar | Example |
|---|---|---|
| batch | `B<nnn>` | `B721` |
| wafer | `<batch>-W<n>` | `B721-W1` |
| coupon | `<wafer>-C<nn>` (01..49, the 7x7 grid) | `B721-W1-C07` |
| track | `<coupon>-T<n>` (1..3) | `B721-W1-C07-T2` |
| run | `R<n>` (1..4) | `R3` |
- **CSV** stores the bare code (`B721-W1-C07`) in `<entity>_code`-style
columns.
- **JSON-LD / RDF** use the prefixed CURIE form as the node identity:
`batch:B721`, `wafer:B721-W1`, `coupon:B721-W1-C07`,
`track:B721-W1-C07-T2`, `run:R1` (spec 00_PLAN global conventions).
- **SQL** stores the bare code as the `<singular>_code` TEXT UNIQUE column
next to the integer surrogate key
(see [db-sql-schema.md](db-sql-schema.md)).
- A code is never parsed to recover its parent where a proper reference
exists; the parent relation is explicit in every format (directory
nesting, `partOf`, FK).
## 2. Timestamps
- ISO-8601, UTC, `Z` suffix, second precision:
`2026-03-14T09:30:00Z`.
- All corpus timestamps are simulated
(see [data-determinism.md](data-determinism.md) section 3).
- In SQL: `TIMESTAMPTZ` in PostgreSQL; TEXT in ISO-8601 form in SQLite
(sortable lexicographically by construction).
## 3. Measurement columns - unit suffix is part of the name
Every numeric measurement column ends in an explicit SI unit token. The
canonical tokens (as they appear in CSV headers):
| Token | Unit | Example columns |
|---|---|---|
| `_um` | micrometre | `thickness_um`, `position_um` |
| `_nm` | nanometre | `ra_nm` |
| `_GPa` | gigapascal | `hardness_GPa`, `reduced_modulus_GPa` |
| `_mN` | millinewton | `max_load_mN`, `friction_force_mN` |
| `_wtpct` | weight percent | `au_wtpct` |
| `_um3` | cubic micrometre | `wear_volume_um3` |
| `_m` | metre | `sliding_distance_m` |
| `_mm_s` | millimetre per second | `speed_mm_s` |
| `_V` | volt | `discharge_voltage_V` |
| `_W` | watt | `power_W` |
| `_C` | degree Celsius | `temperature_C` |
| `_pct` | percent (non-composition) | `rh_pct`, `cpu_util_pct` |
| `_s` / `_mb` | seconds / megabytes (benchmark outputs) | `wall_s`, `peak_rss_mb` |
- Unit tokens are plain ASCII - never `µ`, `°`, or Unicode superscripts
(see [code-data-formatting.md](code-data-formatting.md)).
- Dimensionless quantities carry no token (`cof`, `cycle`, `k_archard`).
- New measurement columns pick from this table or add a row to it in the
same commit.
## 4. Canonical case mapping CSV -> SQL (ambiguity resolved)
The specs write CSV headers with SI capitalization (`hardness_GPa`, spec
03) but SQL DDL in lowercase (`hardness_gpa`, spec 05). Binding
resolution:
- **CSV headers keep SI capitalization** (`_GPa`, `_mN`, `_V`, `_W`,
`_C`) - they mirror instrument output conventions.
- **SQL / JSON-LD / RDF property names are the plain lowercase of the CSV
name** (`hardness_gpa`, `max_load_mn`). One mechanical rule, no
per-column table to maintain.
- Converters map with `csv_name.lower()` - never hand-maintained rename
dicts that can drift.
## 5. `au_wtpct_mean` (spec gap closed)
Specs 05/06 index `coupons(au_wtpct_mean)` but spec 03 never names the
column. Binding definition:
- `au_wtpct_mean` = the arithmetic mean of `au_wtpct` over the coupon's
400 XRF map points, computed by task 03.
- It is written into `coupon_info.csv` by generation (not derived later
by converters), so every format inherits the identical value, and Q2
("coupons with mean Au = 10 +/- 0.5 wt%") means the same rows
everywhere.
## 6. Benchmark artifact columns
Result files under `./out/bench/` follow the same discipline:
`results_raw.csv` columns are
`format, query, run, wall_s, peak_rss_mb, cpu_s, cpu_util_pct, read_mb,
rows, checksum_ok` (spec 09); `storage_sizes.csv` columns are
`format, variant, bytes` (see
[build-pipeline-tasks.md](build-pipeline-tasks.md)). Extending either
file extends this list in the same commit.
## 7. Anti-patterns
- **A measurement column without a unit token** (`thickness`, `load`) -
unreadable and un-greppable across formats.
- **Unit in a comment instead of the name** - comments do not survive
format conversion; names do.
- **Parsing `B721-W1-C07` to find the wafer** where a real reference
exists in the format at hand.
- **A hand-maintained CSV->SQL rename mapping** - the mapping is
`lower()`, nothing else.
- **Recomputing `au_wtpct_mean` in a converter** - it is generated once
into `coupon_info.csv`; converters copy it.
- **Unicode units in identifiers** (`µm`, `°C`) - ASCII tokens only.

122
docs/rules/db-sql-schema.md Normal file
View File

@@ -0,0 +1,122 @@
# DB / SQL / Schema - conventions for the SQLite and PostgreSQL benchmark targets
**Purpose.** Naming, key, and loading conventions for the two relational
storage formats under evaluation: the SQLite file (`./out/sqlite/tribo.db`,
spec [05](../specs/05_convert_sqlite.md)) and the PostgreSQL 16 database
`tribo` (spec [06](../specs/06_convert_postgresql.md)).
**Binding scope.** Every table, index, and load routine in either engine.
The two engines carry the SAME logical schema; they differ only in types,
storage options, and load mechanics. The full table list and DDL signatures
live in the specs - this file codifies the conventions the DDL must follow,
and it wins where a spec detail is ambiguous.
---
## 1. Two kinds of tables
- **Dimension tables** (`batches`, `wafers`, `coupons`, `runs`,
`instruments`, `tracks`): small, joined constantly.
- PK: integer surrogate, named `<singular>_id` (`coupon_id`, `track_id`).
Never a bare `id`, never TEXT.
- The natural key from the CSV corpus (`B721`, `B721-W1-C07`,
`B721-W1-C07-T2` - see [data-naming-units.md](data-naming-units.md))
is stored as a TEXT column named `<singular>_code`, declared `UNIQUE`.
It is for lookups and human-readable output, never the join key.
- **Bulk tables** (`friction_cycles`, `friction_loop_points`, `xrf_points`,
`profilometry_points`, `nanoindentation`, `afm`, `wear`): millions of
rows, scanned and filtered.
- Reference dimensions by integer FK columns only. **TEXT foreign keys in
bulk tables are forbidden** - they bloat the file and slow every scan.
- PK is the natural composite where one exists: `(track_id, cycle)` for
cycles, `(track_id, cycle, pt)` for loop points; single-column
`track_id` / `coupon_id` where the grain is one row per entity. In
SQLite, composite-PK bulk tables are `WITHOUT ROWID`.
- No surrogate `bigserial` on bulk tables - it adds bytes and nothing
else.
## 2. Table and column naming
- Tables: lowercase, snake_case, plural (`friction_cycles`, not
`FrictionCycle`).
- Columns: lowercase snake_case; measurement columns carry the SI unit
suffix in **lowercase** (`hardness_gpa`, `max_load_mn`,
`wear_volume_um3`). The CSV corpus keeps SI capitalization
(`hardness_GPa`); the SQL mapping is a plain lowercase of the CSV name -
see the canonical mapping in
[data-naming-units.md](data-naming-units.md).
- Denormalization is deliberate and limited: `environment` and `load_mn`
are copied onto `tracks` (from `runs` / test conditions) because
Q4 and Q7 filter on them constantly. Do not denormalize further without
updating this rule.
## 3. Indexes - only what Q1-Q7 use
- Create indexes AFTER the bulk load, never before.
- The index set is driven by the benchmark queries: `coupons(au_wtpct_mean)`,
`tracks(run_id)`, `tracks(coupon_id)`, `runs(environment)`, composite
`tracks(environment, load_mn)`; PostgreSQL adds BRIN on
`friction_cycles(cycle)` per partition.
- Naming: `ux_<table>_<cols>` for unique, `ix_<table>_<cols>` for
non-unique.
- Do not over-index: an index no query uses is disk footprint that skews
the storage comparison AND load-time cost. Every index must name the
query it serves (a one-line comment in the DDL).
## 4. Loading
- **SQLite:** during load `PRAGMA journal_mode=OFF, synchronous=OFF,
cache_size=-262144`; insert via `executemany` in transactions of 50k
rows; after load `PRAGMA journal_mode=WAL`, `ANALYZE`, `VACUUM`.
- **PostgreSQL:** load via `COPY FROM STDIN` (psycopg3 `copy`), never
row-by-row INSERT; `friction_cycles` and `friction_loop_points` are
declaratively RANGE-partitioned by `track_id` (12 partitions of 120
tracks); `VACUUM ANALYZE` after load.
- Loads are all-or-nothing per table and validated against `MANIFEST.csv`
row counts - see [code-error-handling.md](code-error-handling.md)
section 3 and [test-pipeline-validation.md](test-pipeline-validation.md).
- On every SQLite connection that relies on FK behaviour:
`PRAGMA foreign_keys = ON` (SQLite silently ignores FK clauses without
it). Bulk tables MAY declare FK columns without enforced constraints for
load speed - row-count validation is the integrity gate; whichever choice
is made must be the same in both engines.
## 5. `track_summary` - precomputed, and off-limits to Q5
Both engines carry a `track_summary` (plain table computed during load in
SQLite; materialized view in PostgreSQL) with per-track derived values
(`cof_ss_mean`, `run_in_cycles`, ...). It exists for Q2/Q3/Q4/Q6. **Q5 must
never read it** - Q5 is the forced full-scan benchmark over raw
`friction_cycles` in every format (spec
[08](../specs/08_benchmark_queries.md)).
## 6. Engine parity
- Same logical schema, same table names, same column names, same row
counts in both engines.
- Q1-Q7 executed against SQLite and PostgreSQL MUST return identical
result sets (checksum parity with 1e-9 float tolerance - the canonical
expected checksums are computed from PostgreSQL and cross-validated
against SQLite; see
[test-pipeline-validation.md](test-pipeline-validation.md)).
- A schema change in one engine is a schema change in both, in the same
commit.
## 7. Anti-patterns
- **Bare `id` PK, TEXT PK, or a GUID column** - this is a load-once
analytical schema; surrogate integer `<singular>_id` on dimensions,
natural composites on bulk tables.
- **TEXT FK in a bulk table** (`friction_cycles.track_code`) - integer FKs
only.
- **A `bigserial` surrogate on `friction_cycles` / `friction_loop_points`**
- dead weight at 1.4M / 2.9M rows.
- **Creating indexes before the bulk load** - doubles load time for
nothing.
- **An index without a named query** - footprint that skews the format
comparison.
- **Rewriting Q5 to use `track_summary`** - breaks the benchmark's
full-scan contract.
- **`updated_at` / audit columns / triggers** - this is a read-only
benchmark target rebuilt from CSV; it has no mutation lifecycle.
- **Divergent schemas between the engines** - parity is what makes the
benchmark comparison valid.

View File

@@ -0,0 +1,59 @@
# Meta / Concise answers
**Binding on every response.** Match length to the question. Stop after the answer.
## Rule
Do not append: background, rationale, anticipated follow-ups, recap, summary,
TL;DR, "why this matters", offers to do more. If the user said "concise",
"short", "one line", or "just" - obey literally.
## Length budget
| Question shape | Max response |
|---|---|
| "What is X?" / "Where is Y?" / yes-no | 1 line |
| "How do I do X?" | 1-3 lines, commands only |
| "How do I test X?" | 3-5 lines, commands + check |
| "Show me X" (file, diagram, output) | The thing itself, no preamble |
| "Compare A and B" | 1 short table or 2-3 bullets/side |
| Open-ended architecture / discussion | Full prose acceptable |
Diagrams / lists / tables: **8-line ceiling** unless user asks for more.
No ASCII box-drawing unless asked. When the user says "I don't care about X" -
omit X, do not list it "for completeness".
## Examples
**Bad** - user asks "what does this option do?", assistant returns 6-option
table with pros/cons. **Good** - one sentence about the one option.
**Bad** - user asks "show ER diagram, no fields", assistant returns 80-line
ASCII with cardinality table and "what changes" section. **Good** - 4-5
lines of `entity --< child` arrows for the asked entities only.
**Bad** - yes/no question answered with a paragraph of nuance before the
yes/no. **Good** - yes/no first, nuance only if asked.
## Exceptions (these ARE the answer, no length cap)
File contents requested, code requested, test/log output, final summary
after multi-step tool work, plans the user asked to be detailed.
## Self-check before sending
- Did the user ask a yes/no? -> answer yes/no in the first line, nuance
only after, only if useful.
- Did the user ask for one thing? -> deliver that one thing only.
- About to write more than ~10 lines for a question that needs ~5? -> cut
everything past line 10 unless it is code, output, or a file path.
- Did the user say "concise" / "short" / "one line" / "just"? -> obey
literally: one sentence or one command/path.
## Recovery
If the user complains the output is too long:
1. Acknowledge in one line.
2. Re-answer the original question in one line.
3. Capture it as an anti-pattern in the Examples section above.

View File

@@ -0,0 +1,33 @@
# Ask extremely short, concise questions
Binding on EVERY question put to the user.
## Rules
- **One decision per question.** Do not bundle unrelated questions.
- **Minimal context.** State only what is needed to answer: the situation in one
short clause, the concrete options, and what the answer changes. Nothing else.
- **No preamble.** Do NOT precede a question with analysis, restated reasoning,
background, evidence, or a summary of what you just did. The user has that
context; if a fact is truly needed to decide, compress it to one clause.
- **Prefer the smallest form.** A yes/no, or a 2-3 option pick. Put the question
itself first or make it unmissable.
- **Self-contained but short.** The options must be understandable without opening
another document, but spell them out in a few words, not paragraphs.
- **No useless information.** Every word must help the user decide. Delete anything
that does not.
## Anti-patterns
- A multi-paragraph lead-in before the actual question.
- Re-explaining the problem the user already stated.
- Listing options with a paragraph of trade-offs each.
- Asking two or more separate decisions in one run of prose.
## Shape to aim for
```
<one-clause situation>. <Question>?
1. <option> - <few words>
2. <option> - <few words>
```

View File

@@ -0,0 +1,62 @@
# Meta / Cursor rule artifact - commit-message generation
**Binding** for keeping Cursor's commit-message generation aligned with this
project's commit convention.
The development environment is the Cursor IDE with the Claude Code extension.
Claude Code reads the root `CLAUDE.md` and `docs/rules/` directly and needs no
`.cursorrules`. Cursor's OWN built-in AI (Chat/Composer and the SCM "sparkle"
commit-message generator) does not read those files - it reads `.cursorrules`.
Since the user creates all commits themselves (see
[`meta-git-commits.md`](meta-git-commits.md)) and may use the one-click SCM
generator to draft the message, `.cursorrules` must exist and must encode the
commit convention.
This project ships a SINGLE Cursor rules file: the root
[`.cursorrules`](../../.cursorrules). There is no `.cursor/rules/*.mdc`
directory here, and none should be added. All Cursor rule content lives in
`.cursorrules`.
---
## 1. The requirement
`.cursorrules` MUST encode this project's commit-message convention so that
Cursor's generator emits conforming messages. The authoritative convention
lives in [`meta-git-commits.md`](meta-git-commits.md); `.cursorrules` mirrors
it as plain-text instructions to the generator.
The failure mode this rule prevents: a missing or stale `.cursorrules`, so the
one-click SCM generator falls back to Cursor defaults and emits unprefixed junk
(`Update files`, `Improve project`).
## 2. What `.cursorrules` must encode
1. **First-line format** - `type(scope): description`, lowercase imperative
summary in English, under 72 chars, no trailing period.
2. **Allowed types (closed list)** - `feat fix perf refactor security docs ci
chore test`.
3. **Allowed scopes (closed list)** - `specs rules docs config datagen convert
bench report tools all`, with the area mapping from
[`meta-git-commits.md`](meta-git-commits.md).
4. **Coverage rule** - the subject is only a title; multi-area diffs need a
blank line and a 2-6 bullet body covering every meaningful changed area; use
`all` when no narrower scope honestly fits; split unrelated work.
5. **Anti-examples** - forbid vague/unprefixed lines (`Update files`, `Improve
project`, `clarify rules`).
6. **Allowed special forms** - `Merge ...` commits are allowed as-is;
`release(scope): vX.Y.Z` is allowed for releases.
## 3. Keeping the mirror in sync
No commit-msg hook is installed in this repository; the convention is enforced
by review. `.cursorrules` and [`meta-git-commits.md`](meta-git-commits.md)
must agree: same type list, same scope list, same first-line format. If the
convention changes, update both files in the same change set.
## 4. Scope boundary
This rule governs the commit-message *convention materialization* - not who may
create commits. Commit authorship is governed by
[`meta-git-commits.md`](meta-git-commits.md): rules files describe the
convention; only the user creates actual commits.

View File

@@ -0,0 +1,99 @@
# Meta / Git commits - never on behalf of the user
**Binding** for every contributor (human or AI agent) touching this
repository. Applies to **all** git history-modifying operations.
---
## 1. The rule
**Never create a git commit on behalf of the user. Ever.** The user is the
only entity allowed to materialize a commit in this repo.
This covers, without exception:
- `git commit` (with or without `-a`, `-m`, `--amend`)
- `git merge` - never finalize one
- `git rebase` (interactive or otherwise) - never start one
- `git revert` - never run; the user decides whether a change is undone
- `git cherry-pick`
- `git stash`
- `git tag` (annotated or lightweight)
- `git push` of any kind - pushing is the user's call
- `git reset` / `git checkout` that changes the working tree or refs
- Any wrapper script or `gh` API call that would create a commit / tag /
merge on the user's behalf
For an AI agent, git is **read-only**: reading history (`git log`,
`git diff`, `git status`, `git show`) and inspecting branches is fine;
anything that writes to the index, refs, or `objects/` - including
`git add` - is left to the user. (This matches the machine-level
enforcement: git write commands by the agent are denied by a hook.)
## 2. Why
- Commit authorship and message wording are decisions the user owns - they are
durable, public, and signed against the user's name and email.
- "Tiny" commits that look harmless ("fix typo", "delete unused file") still
rewrite history and cannot be quietly reversed once pushed.
- Generated data and measured results in this project take hours to rebuild;
what enters history and when is a conscious decision, not a side effect.
## 3. What to do instead
- Make the working-tree changes.
- Report what changed and offer a draft commit message **as plain text** in the
conversation, together with the exact `git add` / `git commit` commands for
the user to run themselves.
## 4. Commit-message format
When you draft a message, follow this project's convention:
- **First line** - `type(scope): description`, lowercase imperative summary in
English, under 72 chars, no trailing period.
- **Types (closed list)** - `feat fix perf refactor security docs ci chore
test`.
- **Scopes (closed list, owner-approved 2026-07-11)** - `specs rules docs
config datagen convert bench report tools all`. Pick by the area of the
changed files:
- `specs` = `docs/specs/**`
- `rules` = `docs/rules/**`
- `docs` = other prose docs (`README.md`, root `CLAUDE.md`,
`docs/examples/**`)
- `config` = configuration files (`.gitignore`, `.cursorrules`, editable
YAML defaults such as hardware prices)
- `datagen` = lab-configuration and data-generation code (tasks 01-03:
`lab_config` generation, process diagrams, `generate_data.py`)
- `convert` = format converters (tasks 04-07: JSON-LD, SQLite,
PostgreSQL, RDF)
- `bench` = benchmark queries, runner, and extrapolation (tasks 08-10)
- `report` = reporting code and templates (task 11)
- `tools` = helper scripts under `tools/`
- `all` = multiple areas or in doubt
- **Coverage** - multi-area diffs get a blank line and a 2-6 bullet body
covering every meaningful changed area; split unrelated work.
- **Special forms** - `Merge ...` and `release(scope): vX.Y.Z` are allowed
as-is.
No `commit-msg` hook is installed in this repository yet; the convention is
enforced by review. If a hook is added later, it must implement exactly this
format and this file must be updated in the same change. The same convention
is mirrored into `.cursorrules` for Cursor's built-in commit-message
generator - see [meta-cursor-rules.md](meta-cursor-rules.md).
## 5. Anti-patterns
- **"I'll just commit it so we have a checkpoint."** No. Working-tree state is
the checkpoint; the user decides when it becomes history.
- **Amending the user's last commit to fix something you noticed.** No. Surface
the issue, let the user choose between amend and new commit.
- **Suggesting `git add -A` in the draft commands.** List explicitly named
files, so the user sees exactly what would land.
- **Vague subjects** (`Update files`, `Improve project`, `clarify rules`).
Say what changed and where.
## 6. Exception (closed list)
There are no exceptions. If a workflow seems to require a commit, pause,
explain the dependency, and let the user run the git command.

View File

@@ -0,0 +1,35 @@
# Meta / Communication language and register
**Binding on every response and on all prose directed at the user**
(explanations, status updates, questions, summaries, commit messages, PR text).
## Rule
1. **Language: Russian.** All communication with the user is in standard
literary Russian.
2. **Register: neutral and professional. No jargon, no slang, no profanity.**
- No profanity or vulgar words - in any language, under any circumstances.
- No slang or jargon: street slang, gamer/internet slang, or untranslated
English buzzwords used as slang.
- Prefer the established Russian word over a transliterated English one
whenever a normal Russian equivalent exists.
- Plain, grammatical, complete sentences.
## Allowed terminology (not slang)
Field-standard technical terms are fine - they are terminology, not jargon:
e.g. «коммит», «билд», «дебаг», «релиз», «мерж». When unsure whether a word
is accepted terminology or slang, choose the neutral Russian equivalent.
## Scope
Does **not** apply to source code, identifiers, file paths, code comments,
shell/tool output, or verbatim quotes of errors and logs - those keep the
language of the codebase.
## Canonical source
This file is the project-level binding rule. It restates the project owner's
standing preference (communication in Russian; code, docs, and artifacts in
English). If a global user-level rule ever diverges from this file, the
stricter wording wins.

View File

@@ -0,0 +1,51 @@
# Meta / No assumptions
**Binding** on every interaction. Strictest when the cost of a wrong
guess is high: deleting or regenerating `./out/` artifacts, dropping or
reloading the `tribo` PostgreSQL database, changing the random seed or
`lab_config.yaml`, editing specs in ways that invalidate generated data,
git history, anything with data-loss consequences.
## Rule
Before producing a plan or change:
1. Re-read the user's most recent message. Watch for enumerations
("both", "and also", "in addition to"), explicit purpose statements
("X for A, Y for B"), and distinct entities that sound similar.
2. Don't collapse two distinct entities into one. "X for A and Y for B"
means **two** setups, not one.
3. If anything is ambiguous after careful re-reading - **ask**. Don't
pick the most likely interpretation and proceed.
4. Echo back the parsed interpretation before producing the plan, so a
misread is cheap to correct.
## When the rule is most strict
Strictness scales with the cost of a wrong guess:
| Domain | Why a wrong assumption is expensive |
|---|---|
| **Deleting / regenerating `./out/`** | The artifact tree holds hours of generation, conversion, and benchmark work. Regenerating CSV invalidates every downstream format, benchmark result, and report. Confirm which subtree is affected before deleting or overwriting. |
| **The `tribo` PostgreSQL database** | Dropping or reloading it discards a long `COPY` load and invalidates benchmark results derived from it. Confirm before any `DROP` / `TRUNCATE` / reload. |
| **The random seed / `lab_config.yaml`** | Seed 20260711 and the lab configuration are the root of the whole reproducibility chain. Changing either silently invalidates all generated data, all conversions, all checksums, and all measured results. Never change them as a side effect of another task. |
| **Editing specs (`docs/specs/`)** | Specs are the task contract. A "small clarification" that changes volumes, schemas, or file layouts desynchronizes already-generated artifacts. Confirm intent when an edit is more than wording. |
| **Benchmark results (`./out/bench/`)** | Measured results cannot be re-derived without re-running the full matrix (hours). Never overwrite `results_raw.csv` / `results_median.csv` without confirmation. |
| **git history** | A wrong rebase/reset/force-push rewrites shared history; recovery is manual and error-prone. (The AI agent never runs git write operations at all - see [meta-git-commits.md](meta-git-commits.md).) |
For all of the above: **the cost of one extra clarifying question is
zero; the cost of a wrong assumption is hours-to-days** of recovery.
Always ask.
## Forms a clarifying question takes
- "Confirm: you want X for A AND Y for B (two setups), not just one?"
- "Two readings: (a) ..., (b) ... - which?"
- "What does '<word>' refer to here - (a) ... or (b) ...?"
## When the assumption was already wrong
1. Acknowledge the misread explicitly. Quote the user's words back.
2. Discard the wrong artifact entirely; don't repair piecemeal unless
the user asks.
3. Restart with the corrected interpretation, echo it first.

View File

@@ -0,0 +1,119 @@
# Meta / Rule file naming
**Binding** for files inside [`docs/rules/`](docs/rules/). Adding, renaming, or
splitting a rule file follows this document.
## Layout
- `docs/rules/` - binding rules (this folder): reproducible conventions on HOW
work is done. Reference content that supports a rule also lives here,
named `<rule>-reference.md`.
- `docs/specs/` - the pipeline task specifications (WHAT to build: tasks
01-11, their inputs, outputs, and dependencies). Binding for scope and
deliverables, but conventions that span tasks are codified in `docs/rules/`.
- `docs/examples/` - imported reference materials (master plan, ontology).
Descriptive, not binding.
Files live flat at the top level of `docs/rules/`. No subfolders.
## Naming
```
<domain>-<subdomain>-<topic>[-<aspect>].md
```
- Lowercase, dashes between segments. No underscores, no CamelCase.
- Max 4 segments. Most general first.
- Reading the filename should answer: category -> subcategory -> topic.
## Approved domain prefixes
| Prefix | Covers |
|---|---|
| `bench-` | Benchmark execution, resource measurement, extrapolation, reporting methodology |
| `build-` | Pipeline task orchestration: the `./out/` tree, `.done` markers, task order |
| `code-` | General code style across languages (Python, SQL, PowerShell): comments, formatting, error handling, configuration |
| `data-` | The simulated dataset: determinism, identifiers, units, column naming |
| `db-` | SQLite / PostgreSQL benchmark target schemas, loading, indexing |
| `meta-` | Rules about the rules system and working conventions |
| `obs-` | Observability and logging |
| `test-` | Validation gates, checksums, pytest conventions |
Don't invent a new prefix without adding it to this table in the same
commit.
### Subdomains (second / third segment)
Within a domain, the second segment is a recognised tool, subsystem, or
concern; the third is the concrete topic. Established families:
- `db-sql-*` - relational benchmark targets (SQLite, PostgreSQL): schema,
loading, indexing.
- `data-*` - dataset conventions (`data-determinism`, `data-naming-units`).
- `bench-*` - measurement methodology (`bench-methodology`).
- `build-pipeline-*` - task orchestration and artifact layout.
- `code-*` - general code style (comments, formatting, error handling,
config, Python style).
- `meta-*` - rules about the rules system and working conventions.
- `obs-*` - observability and logging (`obs-python-logging`).
- `test-*` - validation and testing (`test-pipeline-validation`).
## Examples
| Good | Reads as |
|---|---|
| `data-determinism.md` | Data -> deterministic generation |
| `db-sql-schema.md` | DB -> SQL -> schema conventions |
| `bench-methodology.md` | Benchmarks -> measurement methodology |
| `code-python-style.md` | Code -> Python -> style |
| `obs-python-logging.md` | Observability -> Python logging |
| `meta-rules-naming.md` | Meta -> rules system -> naming |
| Bad | Why |
|---|---|
| `BenchMethodology.md` | CamelCase forbidden |
| `data_determinism.md` | Underscores forbidden |
| `sqlite-loading-conventions.md` | Use `db-` prefix, drop redundant `conventions` |
| `bench-execution-measurement-rules-doc.md` | 5+ segments + redundant `rules` suffix |
## Adding a rule
1. Pick the domain prefix; add new ones to the table above first.
2. Check `ls docs/rules/` - extend a sibling rule before creating a new one.
3. Create the file with binding content, examples, and exceptions.
4. Add a row for the new file to the rule-trigger table in the root
[`CLAUDE.md`](../../CLAUDE.md) in the same commit.
5. If replacing an older doc, `git mv` to preserve history and `Grep` the
repo for the old path.
## Rules vs specs vs memory files
Three homes for a statement; pick by its kind:
| Source | Stores |
|---|---|
| `docs/rules/<name>.md` | A reproducible, mandatory behaviour - the same answer for every contributor on every task within the topic. |
| `docs/specs/NN_<task>.md` | The definition of one pipeline task: scope, inputs, outputs, dependencies. |
| `memory/<name>.md` | A fact about *this user*, *this project*, or *the current moment* - preferences, ongoing initiatives, locked decisions, infra specifics. |
- A convention that must hold across several tasks -> `docs/rules/`.
- A deliverable of one task -> its spec in `docs/specs/`.
- A fact about the user or the present state of things -> `memory/`.
A rule file may reference a spec or a memory note for context, but the
binding convention lives in the rule file. When a rule and a spec disagree
on a convention (naming, formats, thresholds), the rule wins and the spec
is updated to match.
## Anti-patterns
- **Duplicating rule content across rule files** - pick one canonical
location and cross-link from siblings instead of copying.
- **Restating a whole spec inside a rule** - the rule codifies the
reusable convention and links the spec for task detail.
- **Adding rules to the body of CLAUDE.md** instead of creating a rule
file - CLAUDE.md carries only the trigger table, the project preamble,
and the canonical-source-of-truth note.
- **Naming a file after the implementation** instead of the domain
(`oxigraph-loading.md` vs `db-rdf-loading.md`) - the implementation may
be replaced; the topic endures.

View File

@@ -0,0 +1,113 @@
# Observability / Python logging - standard library only, namespaced, plain text
**Purpose + scope.** Every Python module in this repository (data generation,
converters, benchmark runner, extrapolation, reporting, helper tools) MUST
use the standard-library `logging` module following these conventions. No
external logging dependency.
---
## 1. The rule
1. **Standard library `logging` only.** No `loguru`, no `structlog`, no
`python-json-logger`. Stdlib is sufficient and keeps the dependency list
closed (see [code-python-style.md](code-python-style.md)).
2. **Namespaced loggers.** Every module obtains its logger via
`logging.getLogger(__name__)`. Never the root logger directly.
3. **`print()` is for the user-facing CLI protocol only.** The specs
explicitly require certain stdout output: progress summaries, the real
generated tree size (task 03), per-cell benchmark progress, final
counts. That protocol output goes through `print()`. Everything
diagnostic - internal flow, warnings, errors, per-row detail - goes
through a logger.
4. **Log level from a CLI flag.** Every pipeline script accepts
`--log-level {DEBUG,INFO,WARNING,ERROR}`, default `INFO`. There is no
settings table and no `.env` in this project (see
[code-config-yaml.md](code-config-yaml.md)).
5. **Plain-text format on stderr.** These are one-shot CLI scripts, not
services; JSON logs are overkill. One line per record via
`logging.basicConfig`. Keeping the logger on stderr leaves stdout clean
for the protocol output of item 3.
6. **Never log secrets.** The only credential material in this project is
the PostgreSQL DSN - never log it (a DSN may embed a password). Log the
database name or host, never the full connection string.
## 2. API by severity
| Method | Use for |
|---|---|
| `logger.critical(...)` | The process cannot continue - before `sys.exit(1)` or an unrecoverable state. |
| `logger.error(...)` | Failed operation, exception caught and handled, broken invariant, a benchmark cell recorded as TIMEOUT/ERROR. Always include `exc_info=True` when an exception is in scope. |
| `logger.warning(...)` | Recoverable problem, retry triggered, suspicious state (e.g. a value near a validation bound), an optional step skipped (e.g. cold-cache pass unavailable). |
| `logger.info(...)` | Normal flow milestones - task start/finish, per-batch progress, row counts written, validation gates passed. |
| `logger.debug(...)` | Verbose tracing - per-coupon detail, generated value samples, SQL statements, per-run raw measurements. Hidden at default `INFO`. |
**Default level: `INFO`.** `DEBUG` is opt-in via `--log-level DEBUG` for a
diagnosis session.
## 3. Logger acquisition and setup
```python
# Top of every module that logs:
import logging
logger = logging.getLogger(__name__)
# Once, in each script's main():
logging.basicConfig(
level=args.log_level,
stream=sys.stderr,
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
)
```
Inside functions:
```python
def load_table(conn, name: str, rows) -> int:
logger.info("loading %s", name)
try:
...
except psycopg.Error:
logger.error("loading %s failed", name, exc_info=True)
raise
```
`__name__` gives the module path as the namespace tree used for filtering.
Never hardcode a logger name string. Clamp noisy third-party libraries
(`psycopg`, `matplotlib`) to `WARNING` in the same setup block if they
pollute the output.
## 4. Progress reporting in long loops
Generation and loading loops run over millions of rows. Report progress at
INFO on a coarse cadence (per batch / per wafer / per table, or every N
seconds), never per row. Per-row output belongs at DEBUG, and even there
prefer sampled output ("first row of each file") over full dumps.
## 5. Anti-patterns
- **`print()` for diagnostics** - forbidden; `print()` is only the
user-facing CLI protocol (progress summaries, final counts, sizes).
- **Diagnostics on stdout** - stdout is the protocol channel; the logger
writes to stderr.
- **Root logger calls** (`logging.info(...)`) - forbidden; always
`logger = logging.getLogger(__name__)`.
- **Third-party logging frameworks** (`loguru`, `structlog`) - forbidden.
- **Hardcoded logger names** (`logging.getLogger("datagen")`) - forbidden;
pass `__name__`.
- **Logging the PostgreSQL DSN / connection string** - forbidden; log
host/database name only.
- **Log level from an environment variable or a config file** - the level
is a per-run diagnostic choice; it comes from `--log-level`.
- **`logger.error(...)` without re-raise** when the function semantically
cannot proceed - see [code-error-handling.md](code-error-handling.md).
- **Per-row INFO logging in bulk loops** - drowns the signal and slows the
run; use coarse milestones.
## 6. Related documents
- [code-error-handling.md](code-error-handling.md) - `exc_info=True`
requirement and the swallow ladder.
- [code-config-yaml.md](code-config-yaml.md) - configuration scope (no
`.env`, no settings table).
- [code-python-style.md](code-python-style.md) - the closed dependency list.

View File

@@ -0,0 +1,125 @@
# Test / Pipeline validation - markers, manifests, checksums
**Purpose + scope.** The validation contract between pipeline tasks: how a
task proves it completed correctly and how downstream tasks verify their
inputs before trusting them. Binding for every task implementation (01-11)
and for any pytest suite added to this repository.
The pipeline has no human QA step between tasks - these gates are the only
thing standing between a silent generation/conversion bug and a report full
of wrong numbers.
---
## 1. Completion markers - written last, verified first
- Every task writes `./out/.done/<NN>.ok` **only after** all its outputs
are complete and its own validation passed. The marker contains the
task's headline counts (rows, bytes, triples) for downstream checks.
- A task MUST verify the `.done` markers of its declared dependencies
before starting, and abort with a clear message if one is missing.
- A task that re-runs deletes its own marker first and rewrites it at the
end. A marker is never written on failure and never from a `finally`
block (see [code-error-handling.md](code-error-handling.md)).
## 2. The CSV corpus manifest - the root of trust
- Task 03 writes `./out/csv/MANIFEST.csv`: one row per generated file with
path, row count, byte size, sha256.
- Every converter (04-07) validates what it read against the manifest: row
counts per source file, and sha256 where the file is consumed whole. A
mismatch is an abort, not a warning.
- The manifest is also the reproducibility proof: regenerating with the
same `lab_config.yaml` must reproduce identical sha256 values (see
[data-determinism.md](data-determinism.md)).
## 3. Canonical expected volumes
The lab configuration fixes the corpus size exactly; validation asserts
these numbers, not "roughly that many":
| Entity | Count |
|---|---|
| batches | 4 |
| wafers | 12 |
| coupons | 588 (480 friction + 108 characterization-only) |
| tracks | 1,440 |
| friction cycle rows | 1,440,000 |
| friction loop points | 2,880,000 |
| XRF points | 235,200 (588 x 400) |
| generated tree size | 120-200 MB (hard abort bounds) |
If a deliberate `lab_config.yaml` change moves these numbers, this table
and the validation code change in the same commit.
## 4. Query-result checksums - all formats agree
- Each benchmark query Q1-Q7 has ONE canonical expected result: rows
sorted, floats compared with tolerance 1e-9, checksummed to
`./out/bench/expected/q<N>.sha256`.
- The canonical checksums are computed from **PostgreSQL** and
cross-validated against **SQLite** before any benchmarking starts; a
disagreement between the two engines is a converter or query bug and
blocks task 09.
- Every measured benchmark run records a `checksum_ok` flag; a cell whose
result does not match the expected checksum is an invalid measurement
regardless of how fast it ran.
## 5. Format smoke tests
Each converter proves basic well-formedness of its output before writing
its marker:
- **JSON-LD (04):** one sample coupon file passes `json.load` AND an
rdflib JSON-LD parse.
- **SQLite (05) / PostgreSQL (06):** row counts per table match the
manifest-derived expectations; `track_summary` row count equals 1,440.
- **RDF (07):** the loaded store answers a SPARQL `COUNT` matching the
serialized triple count recorded in the `.done` marker.
## 6. If a pytest suite is added
The specs define no pytest suite; validation is built into the tasks. If
unit/integration tests are added later:
- **Skip, don't fail, when PostgreSQL is unreachable.** A checkout without
a running PostgreSQL still gets a green (skipped) run;
connection failure in a fixture is `pytest.skip(...)`, not an error.
- **Never mutate real `./out/` artifacts.** Tests that need data build a
small corpus into a temp directory (a reduced `lab_config` is fine) or
copy the artifact; hours of generated/measured data are not test
fixtures to be overwritten.
- **Ephemeral databases only.** A test touching PostgreSQL creates and
drops its own database (e.g. `tribo_test`) next to `tribo`; it never
writes to `tribo` itself.
- **Independent and order-free.** Every test passes run alone; no residue
between tests, no execution-order dependencies.
- **Deterministic assertions.** With the fixed seed, exact counts and
exact values are assertable - prefer exact assertions over "greater than
zero".
## 7. Anti-patterns
- **Writing a `.done` marker before validation, on failure, or in
`finally`** - the marker means "completed and verified".
- **Skipping manifest validation "because generation just ran"** - the
gate exists precisely for the runs where the assumption is wrong.
- **Warning-and-continuing on a count mismatch** - a converter with a row
deficit produces a database that LOOKS complete; abort.
- **Regenerating expected checksums to make a failing format pass** - the
canonical result comes from PostgreSQL cross-validated with SQLite;
a mismatch in another format is a bug in that converter or query.
- **Tests writing into `./out/`** or connecting to the real `tribo`
database.
- **Failing (not skipping) when PostgreSQL is unavailable** in a pytest
run.
## 8. Related documents
- [build-pipeline-tasks.md](build-pipeline-tasks.md) - marker layout and
task order.
- [data-determinism.md](data-determinism.md) - why byte-identical
regeneration is assertable.
- [code-error-handling.md](code-error-handling.md) - fail-loud mechanics.
- [bench-methodology.md](bench-methodology.md) - how `checksum_ok` enters
the measurement protocol.