From 8798632b92f13a2cf3179dadc62763923edba2fa Mon Sep 17 00:00:00 2001 From: Mike Stankavich Date: Sun, 30 Aug 2026 05:32:43 -0500 Subject: [PATCH 1/3] chore: never track superpowers specs/plans; adopt the canonical ignore block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ignore rule here pointed at spec/superpowers/ — the old path, swept in dca90c0 on 2026-08-03. A newer version of the superpowers skills had already started writing to superpowers/ at repo root three days before that sweep, so the sweep cleaned and ignored a location nothing writes to any more while the tree at the new one was never in scope. 29 files accumulated there unignored. Adopt the canonical block agreed across the estate, matching trakrf/platform byte for byte so no repo is subtly different again. superpowers/ and .superpowers/ carry no internal slash and so match at any depth, which is what lets one block be correct in both layouts; docs/superpowers/ has one and anchors to the root deliberately. docs/notes/ lands beside them as the sanctioned home for working notes, so a session has somewhere to put an investigation record instead of inventing a directory each time. An ignore pattern does nothing to files already in the index, so the tree is removed from disk as well as untracked. Verified with git check-ignore -v that all four patterns actually fire against real paths, rather than eyeballing that they are present — a pattern can be present and still not match. Also drops the orphaned "# CSW local symlink" comment, a header left with nothing under it when dca90c0 removed the pattern. No CSW pattern is reintroduced: CSW is a plugin now, not a local checkout. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RWAqULDa91P8QxVD6hRaer --- .gitignore | 19 +- .../2026-05-17-tra-762-bb-cycle-selector.md | 728 -------------- ...26-05-22-tra-799-asset-location-removal.md | 418 -------- .../plans/2026-05-23-tra-816-tag-cascade.md | 197 ---- .../2026-05-27-tra-551-loc-auto-mint-docs.md | 347 ------- ...6-05-28-tra-848-oauth-auth-docs-cutover.md | 632 ------------ ...859-bb-oauth-client-credentials-cutover.md | 256 ----- ...tra-885-id-vs-external-key-distribution.md | 60 -- ...5-30-tra-891-api-identity-join-guidance.md | 211 ---- .../2026-05-30-tra-892-docs-hygiene-bundle.md | 142 --- ...2026-07-29-tra-1045-app-tour-regenerate.md | 947 ------------------ .../2026-07-30-tra-1048-webhooks-v1-docs.md | 596 ----------- ...2026-05-15-tra-737-bb41-followup-design.md | 49 - .../2026-05-16-tra-745-bb44-f1f2f3-design.md | 71 -- ...-05-17-tra-762-bb-cycle-selector-design.md | 126 --- .../2026-05-17-tra-768-bb57-docs-design.md | 77 -- ...-set-equality-and-errors-example-design.md | 60 -- ...026-05-18-tra-777-audit-followup-design.md | 58 -- .../2026-05-18-tra-780-bb63-docs-design.md | 77 -- .../2026-05-18-tra-782-bb64-docs-design.md | 82 -- ...2026-05-18-tra-783-bb64-followup-design.md | 96 -- .../2026-05-18-tra-789-bb67-docs-design.md | 84 -- ...2-tra-799-asset-location-removal-design.md | 255 ----- .../2026-05-23-tra-816-tag-cascade-design.md | 173 ---- ...-tra-848-oauth-auth-docs-cutover-design.md | 86 -- ...-id-vs-external-key-distribution-design.md | 93 -- ...a-891-api-identity-join-guidance-design.md | 115 --- ...5-30-tra-892-docs-hygiene-bundle-design.md | 78 -- ...-29-tra-1045-app-tour-regenerate-design.md | 228 ----- ...-07-30-tra-1048-webhooks-v1-docs-design.md | 185 ---- 30 files changed, 14 insertions(+), 6532 deletions(-) delete mode 100644 superpowers/plans/2026-05-17-tra-762-bb-cycle-selector.md delete mode 100644 superpowers/plans/2026-05-22-tra-799-asset-location-removal.md delete mode 100644 superpowers/plans/2026-05-23-tra-816-tag-cascade.md delete mode 100644 superpowers/plans/2026-05-27-tra-551-loc-auto-mint-docs.md delete mode 100644 superpowers/plans/2026-05-28-tra-848-oauth-auth-docs-cutover.md delete mode 100644 superpowers/plans/2026-05-29-tra-859-bb-oauth-client-credentials-cutover.md delete mode 100644 superpowers/plans/2026-05-30-tra-885-id-vs-external-key-distribution.md delete mode 100644 superpowers/plans/2026-05-30-tra-891-api-identity-join-guidance.md delete mode 100644 superpowers/plans/2026-05-30-tra-892-docs-hygiene-bundle.md delete mode 100644 superpowers/plans/2026-07-29-tra-1045-app-tour-regenerate.md delete mode 100644 superpowers/plans/2026-07-30-tra-1048-webhooks-v1-docs.md delete mode 100644 superpowers/specs/2026-05-15-tra-737-bb41-followup-design.md delete mode 100644 superpowers/specs/2026-05-16-tra-745-bb44-f1f2f3-design.md delete mode 100644 superpowers/specs/2026-05-17-tra-762-bb-cycle-selector-design.md delete mode 100644 superpowers/specs/2026-05-17-tra-768-bb57-docs-design.md delete mode 100644 superpowers/specs/2026-05-18-tra-775-bb61-tags-set-equality-and-errors-example-design.md delete mode 100644 superpowers/specs/2026-05-18-tra-777-audit-followup-design.md delete mode 100644 superpowers/specs/2026-05-18-tra-780-bb63-docs-design.md delete mode 100644 superpowers/specs/2026-05-18-tra-782-bb64-docs-design.md delete mode 100644 superpowers/specs/2026-05-18-tra-783-bb64-followup-design.md delete mode 100644 superpowers/specs/2026-05-18-tra-789-bb67-docs-design.md delete mode 100644 superpowers/specs/2026-05-22-tra-799-asset-location-removal-design.md delete mode 100644 superpowers/specs/2026-05-23-tra-816-tag-cascade-design.md delete mode 100644 superpowers/specs/2026-05-28-tra-848-oauth-auth-docs-cutover-design.md delete mode 100644 superpowers/specs/2026-05-30-tra-885-id-vs-external-key-distribution-design.md delete mode 100644 superpowers/specs/2026-05-30-tra-891-api-identity-join-guidance-design.md delete mode 100644 superpowers/specs/2026-05-30-tra-892-docs-hygiene-bundle-design.md delete mode 100644 superpowers/specs/2026-07-29-tra-1045-app-tour-regenerate-design.md delete mode 100644 superpowers/specs/2026-07-30-tra-1048-webhooks-v1-docs-design.md diff --git a/.gitignore b/.gitignore index 9790a64..8cdfe56 100644 --- a/.gitignore +++ b/.gitignore @@ -37,14 +37,23 @@ build/ # Playwright MCP intermediate files .playwright-mcp/ -# CSW local symlink - # Black-box fixture-maintenance SQL — persistent across reboots, not committed. # README.md inside the directory is exempted so the directory's purpose is documented in-repo. tests/blackbox/.fixtures/* !tests/blackbox/.fixtures/README.md -# Superpowers design specs + implementation plans: generated per-session, -# swept in periodic explicit cleanups rather than tracked. -spec/superpowers/ \ No newline at end of file +# Superpowers design specs + implementation plans: generated per-session as +# working artifacts, never tracked. Both paths: the skills default to +# docs/superpowers/, and repos whose docs/ is published content use the root. +superpowers/ +docs/superpowers/ +.superpowers/ + +# Working notes — investigation records, scratch analysis, session notes. Same +# rationale as docs/superpowers/ above: they earn their keep while a ticket is +# being built and reviewed, then go stale fast (PIDs, bench state, host facts) +# while still reading as authoritative because they are checked in. Durable +# conclusions belong in CLAUDE.md, an ADR or a code comment; the narrative and +# evidence belong on the ticket. +docs/notes/ diff --git a/superpowers/plans/2026-05-17-tra-762-bb-cycle-selector.md b/superpowers/plans/2026-05-17-tra-762-bb-cycle-selector.md deleted file mode 100644 index 4ffc096..0000000 --- a/superpowers/plans/2026-05-17-tra-762-bb-cycle-selector.md +++ /dev/null @@ -1,728 +0,0 @@ -# TRA-762 `bb_cycle` Selector Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Extend `just bb_cycle` to support both mint and pre-key tracks from a single entry point, with sniffed positional args (selector `BB[1-3]`, num `[0-9]+`, either order), per-track filtered `.env.local`, and the matching wrapper named in the session-start command. - -**Architecture:** Modify the existing bash-shebang recipe in `justfile`. Move host `.env.local` sourcing to the top of the recipe (gated by `BB_SOURCE_ENV` test override). Add an arg-sniffing loop that classifies each positional into `selector` or `n`. Compute the target as `bb-NN[-{selector}]`. Auto-num scans `bb-NN*` ignoring suffixes; walks forward while the specific target dir exists. After copying `tests/blackbox/.`, replace the copied `.env.local` with a freshly written file containing only the vars for the chosen track. Pick `BB_MINT_KEY.md` or `BB_PRE_KEY.md` in the printed `csp` command. - -**Tech Stack:** justfile + bash. Tests in `scripts/test_bb_cycle.sh` (assert helpers, synthetic env fixtures via `BB_SOURCE_ENV`). - -**Spec:** `docs/superpowers/specs/2026-05-17-tra-762-bb-cycle-selector-design.md` - ---- - -## File Structure - -- **Modify:** `tests/blackbox/BB_PRE_KEY.md` — wrapper consumes flat `$BB_API_KEY` / `$BB_ORG_ID` instead of `${BB_ORG}_API_KEY` indirection. -- **Modify:** `justfile` (the `bb_cycle` recipe, lines 33–125) — arg sniffing, suffixed target, auto-num across suffixes, top-of-recipe env sourcing, filtered `.env.local` write, wrapper-aware session-start command. -- **Modify:** `scripts/test_bb_cycle.sh` — refactor existing tests to use a synthetic `BB_SOURCE_ENV` fixture; add new test cases. - -No new files. - ---- - -### Task 1: Update BB_PRE_KEY.md to consume flat vars - -**Files:** -- Modify: `tests/blackbox/BB_PRE_KEY.md` - -- [ ] **Step 1: Edit §Environment** - -Replace the §Environment section (everything from the `## Environment` heading through the line that says "Do not attempt to use `API_TEST_LOGIN` / `API_TEST_PASS`...") with: - -```markdown -## Environment - -`.envrc` + `.env.local` expose: - -- `API_TEST_APP_URL` — app + API base -- `API_TEST_DOCS_URL` — public docs site -- `BB_ORG` — `BB1`, `BB2`, or `BB3`. The label for the fixture org this session is pinned to. Used for traceability in FINDINGS.md. -- `BB_API_KEY` — the persistent JWT for the assigned fixture org. Pass it as `Authorization: Bearer $BB_API_KEY` on every API call. -- `BB_ORG_ID` — numeric `org_id` for the assigned fixture (for cross-checks). - -Record `$BB_ORG` and `$BB_ORG_ID` in the FINDINGS.md context block so triage can correlate runs across parallel sessions. - -If `$BB_API_KEY` or `$BB_ORG` is unset, stop and report — the harness did not assign this session a fixture, and guessing is wrong. - -There is no SPA login on this track. The fixture key is everything you need; `API_TEST_LOGIN` / `API_TEST_PASS` are not in the env on this track. -``` - -- [ ] **Step 2: Edit §Mission** - -Replace the §Mission line that currently reads: - -``` -Resolve `$BB_ORG`, load `${BB_ORG}_API_KEY`, confirm you can reach `/health.json` and `/api/openapi.yaml`, then **read [BB.md](./BB.md) top to bottom and execute the shared methodology** against the fixture. -``` - -with: - -``` -Load `$BB_API_KEY`, confirm you can reach `/health.json` and `/api/openapi.yaml`, then **read [BB.md](./BB.md) top to bottom and execute the shared methodology** against the fixture. -``` - -- [ ] **Step 3: Verify no remaining `${BB_ORG}_API_KEY` indirection** - -Run: `grep -n '\${BB_ORG}_' tests/blackbox/BB_PRE_KEY.md` -Expected: no output (no matches). - -- [ ] **Step 4: Commit** - -```bash -git add tests/blackbox/BB_PRE_KEY.md -git commit -m "$(cat <<'EOF' -docs(bb): TRA-762 BB_PRE_KEY.md consumes flat \$BB_API_KEY/\$BB_ORG_ID - -The bb_cycle recipe will write a filtered per-track .env.local into the -isolated /tmp target. Each pre-key session sees exactly one fixture key, -exposed flat as \$BB_API_KEY (no \${BB_ORG}_API_KEY indirection). \$BB_ORG -stays as the human-readable label for FINDINGS.md traceability. - -Co-Authored-By: Claude Opus 4.7 (1M context) -EOF -)" -``` - ---- - -### Task 2: Selector arg parsing + suffixed target + auto-num across suffixes - -**Files:** -- Modify: `justfile` (recipe `bb_cycle`, lines 33–67) -- Modify: `scripts/test_bb_cycle.sh` (add new tests; existing tests stay unchanged for now) - -- [ ] **Step 1: Write the failing tests (selector + target naming + auto-num)** - -Add the following block at the end of `scripts/test_bb_cycle.sh`, **before** the final `echo` summary and `[ "$fail" -eq 0 ]` line: - -```bash -# --- TRA-762: selector arg, suffixed target, auto-num across suffixes --- - -# T9. Selector-only: bb_cycle BB2 (no num) → bb-1-BB2/ -prefix=$(make_prefix) -out=$(BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle BB2 2>&1) -[ -d "$prefix/bb-1-BB2" ] -assert_true "selector: BB2 alone yields bb-1-BB2" "$?" -rm -rf "$prefix" - -# T10. Selector + num (num first): bb_cycle 33 BB1 → bb-33-BB1/ -prefix=$(make_prefix) -out=$(BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle 33 BB1 2>&1) -[ -d "$prefix/bb-33-BB1" ] -assert_true "selector+num: '33 BB1' yields bb-33-BB1" "$?" -rm -rf "$prefix" - -# T11. Selector + num (selector first): bb_cycle BB1 33 → bb-33-BB1/ (order-agnostic) -prefix=$(make_prefix) -out=$(BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle BB1 33 2>&1) -[ -d "$prefix/bb-33-BB1" ] -assert_true "selector+num: 'BB1 33' yields bb-33-BB1 (order-agnostic)" "$?" -rm -rf "$prefix" - -# T12. Auto-num same-cycle reuse: bb-3-BB1 exists → bb_cycle BB2 picks 3 -prefix=$(make_prefix); mkdir -p "$prefix/bb-3-BB1" -out=$(BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle BB2 2>&1) -[ -d "$prefix/bb-3-BB2" ] -assert_true "auto: bb-3-BB1 present, BB2 picks 3 (not 4)" "$?" -rm -rf "$prefix" - -# T13. Auto-num across-track: bb-3 (mint) + bb-3-BB1 exist → bb_cycle BB2 picks 3 -prefix=$(make_prefix); mkdir -p "$prefix/bb-3" "$prefix/bb-3-BB1" -out=$(BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle BB2 2>&1) -[ -d "$prefix/bb-3-BB2" ] -assert_true "auto: bb-3 + bb-3-BB1 present, BB2 picks 3" "$?" -rm -rf "$prefix" - -# T14. Auto-num increment when target exists: full cycle 3 → bb_cycle BB1 picks 4 -prefix=$(make_prefix); mkdir -p "$prefix/bb-3" "$prefix/bb-3-BB1" "$prefix/bb-3-BB2" "$prefix/bb-3-BB3" -out=$(BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle BB1 2>&1) -[ -d "$prefix/bb-4-BB1" ] -assert_true "auto: cycle 3 full, BB1 increments to 4" "$?" -rm -rf "$prefix" - -# T15. Mint auto-num still works when only pre-key dirs exist -prefix=$(make_prefix); mkdir -p "$prefix/bb-3-BB1" -out=$(BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle 2>&1) -[ -d "$prefix/bb-3" ] -assert_true "auto (mint): only bb-3-BB1 present, mint picks bb-3" "$?" -rm -rf "$prefix" - -# T16. Invalid selectors rejected -for bad in BB4 BB bb1 foo; do - prefix=$(make_prefix) - set +e - BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle "$bad" >/dev/null 2>&1 - rc=$? - set -e - assert_false "validate: '$bad' rejected" "$rc" - rm -rf "$prefix" -done - -# T17. Two selectors rejected -prefix=$(make_prefix) -set +e -BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle BB1 BB2 >/dev/null 2>&1 -rc=$? -set -e -assert_false "validate: two selectors rejected" "$rc" -rm -rf "$prefix" - -# T18. Two numbers rejected -prefix=$(make_prefix) -set +e -BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle 33 34 >/dev/null 2>&1 -rc=$? -set -e -assert_false "validate: two numbers rejected" "$rc" -rm -rf "$prefix" -``` - -- [ ] **Step 2: Run new tests, verify they fail** - -Run: `just test` -Expected: tests T9 through T18 fail (the recipe currently rejects `BB2` as a non-numeric arg, and treats two args as an error or ignores the second). - -- [ ] **Step 3: Replace the arg-parsing + auto-num + target-build block in `justfile`** - -In `justfile`, replace the recipe signature on line 34 — currently: - -```just -bb_cycle num="": -``` - -with: - -```just -bb_cycle arg1="" arg2="": -``` - -Then replace lines 41–61 of the recipe body — currently: - -```bash - # 1. Determine cycle number - if [ -n "{{num}}" ]; then - n="{{num}}" - if ! [[ "$n" =~ ^[0-9]+$ ]]; then - echo "ERROR: cycle number must be a positive integer, got: $n" >&2 - exit 1 - fi - else - max=0 - shopt -s nullglob - for d in "$prefix"/bb-[0-9]*; do - [ -d "$d" ] || continue - suffix="${d##*/bb-}" - if [[ "$suffix" =~ ^[0-9]+$ ]] && (( suffix > max )); then - max=$suffix - fi - done - shopt -u nullglob - n=$(( max + 1 )) - fi - - target="$prefix/bb-$n" -``` - -with the new block: - -```bash - # 1. Sniff args into (selector, n). Each arg may be a selector (BB1/BB2/BB3), - # a positive integer (cycle number), or empty. Order-agnostic. - selector="" - n="" - for a in "{{arg1}}" "{{arg2}}"; do - [ -z "$a" ] && continue - if [[ "$a" =~ ^BB[1-3]$ ]]; then - if [ -n "$selector" ]; then - echo "ERROR: two selectors provided ($selector, $a)" >&2 - exit 1 - fi - selector="$a" - elif [[ "$a" =~ ^[0-9]+$ ]]; then - if [ -n "$n" ]; then - echo "ERROR: two cycle numbers provided ($n, $a)" >&2 - exit 1 - fi - n="$a" - else - echo "ERROR: unrecognized arg: '$a' (expected BB[1-3] or a positive integer)" >&2 - exit 1 - fi - done - - # 2. Determine cycle number. If not explicit, scan existing bb-NN[-BBn] dirs - # and pick the smallest NN where our specific target doesn't yet exist. - if [ -z "$n" ]; then - max=0 - shopt -s nullglob - for d in "$prefix"/bb-[0-9]*; do - [ -d "$d" ] || continue - suffix="${d##*/bb-}" - if [[ "$suffix" =~ ^([0-9]+)(-BB[1-3])?$ ]]; then - num="${BASH_REMATCH[1]}" - if (( num > max )); then - max=$num - fi - fi - done - shopt -u nullglob - candidate=$(( max == 0 ? 1 : max )) - while :; do - target_check="$prefix/bb-$candidate" - [ -n "$selector" ] && target_check="$target_check-$selector" - [ ! -e "$target_check" ] && break - candidate=$(( candidate + 1 )) - done - n=$candidate - fi - - # 3. Build target path. Pre-key dirs carry the selector as a suffix. - target="$prefix/bb-$n" - [ -n "$selector" ] && target="$target-$selector" -``` - -The "Refuse if target exists" block (currently lines 63–67) stays unchanged and still applies — it now checks the suffixed target for pre-key invocations. - -- [ ] **Step 4: Update the comment block above the recipe** - -Replace the Usage section in the recipe comments (currently lines 25–28) — currently: - -``` -# Usage: -# just bb_cycle # auto-increment from existing /tmp/bb-NN dirs -# just bb_cycle 32 # explicit cycle number -``` - -with: - -``` -# Usage: -# just bb_cycle # mint track, auto-num -# just bb_cycle 32 # mint track, explicit cycle number -# just bb_cycle BB1 # pre-key track for BB1 org, auto-num -# just bb_cycle BB1 32 # pre-key track for BB1, cycle 32 -# just bb_cycle 32 BB1 # same as above (order-agnostic) -``` - -- [ ] **Step 5: Run tests, verify all pass** - -Run: `just test` -Expected: all tests pass, including new T9 through T18. Existing tests T1–T8 also still pass (the new sniffing logic accepts numeric args identically to the old `num=""` form). - -- [ ] **Step 6: Commit** - -```bash -git add justfile scripts/test_bb_cycle.sh -git commit -m "$(cat <<'EOF' -feat(bb): TRA-762 bb_cycle accepts BB[1-3] selector for pre-key track - -bb_cycle now takes two optional positional args, sniffed independently: -BB[1-3] for the pre-key fixture selector, [0-9]+ for the cycle number, -either order. Pre-key dirs land at bb-NN-BBn/, mint dirs stay at bb-NN/. -Auto-num scans bb-NN* across both forms so three parallel bb_cycle BB1/ -BB2/BB3 invocations naturally share a cycle number. Invalid selectors, -two-of-same args, and unrecognized args are rejected with clear errors. -Filtered .env.local and wrapper-aware session-start command come in -follow-up commits. - -Co-Authored-By: Claude Opus 4.7 (1M context) -EOF -)" -``` - ---- - -### Task 3: Filtered `.env.local` + source-env test harness refactor - -**Files:** -- Modify: `justfile` (recipe `bb_cycle`) -- Modify: `scripts/test_bb_cycle.sh` (add `make_env` helper; existing tests adopt it; new env-content tests) - -- [ ] **Step 1: Add the `make_env` helper to the test harness** - -In `scripts/test_bb_cycle.sh`, add this helper just below `make_prefix()` (after line 14): - -```bash -# Write a synthetic host .env.local for the recipe to read via BB_SOURCE_ENV. -# Each test creates its own copy so contents are isolated. -make_env() { - local prefix="$1" - local env_path="$prefix/.env.local" - cat > "$env_path" <<'EOF' -API_TEST_APP_URL=https://app.test.example -API_TEST_DOCS_URL=https://docs.test.example -API_TEST_LOGIN=test-admin@example.com -API_TEST_PASS=test-password -BB1_ORG_ID=111 -BB1_API_KEY=jwt-bb1-fake -BB2_ORG_ID=222 -BB2_API_KEY=jwt-bb2-fake -BB3_ORG_ID=333 -BB3_API_KEY=jwt-bb3-fake -EOF - echo "$env_path" -} -``` - -- [ ] **Step 2: Update every existing test to set `BB_SOURCE_ENV`** - -For each `just bb_cycle ...` invocation in `scripts/test_bb_cycle.sh` (both pre-existing T1–T8 and the new T9–T18 from Task 2), prepend `BB_SOURCE_ENV` set to the result of `make_env "$prefix"`. - -Pattern — find every line of the form: - -```bash -out=$(BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle ... 2>&1) -``` - -and rewrite to: - -```bash -env_file=$(make_env "$prefix") -out=$(BB_SOURCE_ENV="$env_file" BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle ... 2>&1) -``` - -Likewise for the `set +e; ... ; set -e` wrapped invocations: - -```bash -env_file=$(make_env "$prefix") -set +e -BB_SOURCE_ENV="$env_file" BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle ... >/dev/null 2>&1 -rc=$? -set -e -``` - -Important: the `make_env` call must come **after** any `mkdir -p "$prefix/bb-..."` setup so the prefix dir exists. - -- [ ] **Step 3: Add new env-content tests** - -Append to `scripts/test_bb_cycle.sh`, **before** the final `echo` summary: - -```bash -# T19. Pre-key env contents: only URLs + BB_ORG/BB_API_KEY/BB_ORG_ID, no login/pass -prefix=$(make_prefix); env_file=$(make_env "$prefix") -BB_SOURCE_ENV="$env_file" BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle BB2 >/dev/null 2>&1 -target_env="$prefix/bb-1-BB2/.env.local" -[ -f "$target_env" ] -assert_true "filter (pre-key): target .env.local exists" "$?" -grep -q "^BB_ORG=BB2$" "$target_env" -assert_true "filter (pre-key): BB_ORG=BB2" "$?" -grep -q "^BB_API_KEY=jwt-bb2-fake$" "$target_env" -assert_true "filter (pre-key): BB_API_KEY matches BB2 source" "$?" -grep -q "^BB_ORG_ID=222$" "$target_env" -assert_true "filter (pre-key): BB_ORG_ID matches BB2 source" "$?" -! grep -q "^API_TEST_LOGIN=" "$target_env" -assert_true "filter (pre-key): no API_TEST_LOGIN" "$?" -! grep -q "^API_TEST_PASS=" "$target_env" -assert_true "filter (pre-key): no API_TEST_PASS" "$?" -! grep -q "^BB1_API_KEY=" "$target_env" -assert_true "filter (pre-key): no other orgs' keys (BB1)" "$?" -! grep -q "^BB3_API_KEY=" "$target_env" -assert_true "filter (pre-key): no other orgs' keys (BB3)" "$?" -rm -rf "$prefix" - -# T20. Mint env contents: URLs + LOGIN/PASS, no BB_* vars -prefix=$(make_prefix); env_file=$(make_env "$prefix") -BB_SOURCE_ENV="$env_file" BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle >/dev/null 2>&1 -target_env="$prefix/bb-1/.env.local" -[ -f "$target_env" ] -assert_true "filter (mint): target .env.local exists" "$?" -grep -q "^API_TEST_LOGIN=test-admin@example.com$" "$target_env" -assert_true "filter (mint): API_TEST_LOGIN present" "$?" -grep -q "^API_TEST_PASS=test-password$" "$target_env" -assert_true "filter (mint): API_TEST_PASS present" "$?" -! grep -q "^BB_ORG=" "$target_env" -assert_true "filter (mint): no BB_ORG" "$?" -! grep -q "^BB_API_KEY=" "$target_env" -assert_true "filter (mint): no BB_API_KEY" "$?" -! grep -q "^BB[1-3]_API_KEY=" "$target_env" -assert_true "filter (mint): no BBn_API_KEY" "$?" -rm -rf "$prefix" - -# T21. Missing source key for selected org → fail with clear error -prefix=$(make_prefix) -env_path="$prefix/.env.local" -cat > "$env_path" <<'EOF' -API_TEST_APP_URL=https://app.test.example -API_TEST_DOCS_URL=https://docs.test.example -BB1_ORG_ID=111 -BB1_API_KEY=jwt-bb1-fake -EOF -set +e -err=$(BB_SOURCE_ENV="$env_path" BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle BB2 2>&1) -rc=$? -set -e -assert_false "validate (pre-key): missing BB2_API_KEY rejected" "$rc" -echo "$err" | grep -q "BB2_API_KEY" -assert_true "validate (pre-key): error message names BB2_API_KEY" "$?" -rm -rf "$prefix" - -# T22. Missing login/pass for mint track → fail with clear error -prefix=$(make_prefix) -env_path="$prefix/.env.local" -cat > "$env_path" <<'EOF' -API_TEST_APP_URL=https://app.test.example -API_TEST_DOCS_URL=https://docs.test.example -EOF -set +e -err=$(BB_SOURCE_ENV="$env_path" BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle 2>&1) -rc=$? -set -e -assert_false "validate (mint): missing API_TEST_LOGIN rejected" "$rc" -echo "$err" | grep -q "API_TEST_LOGIN" -assert_true "validate (mint): error message names API_TEST_LOGIN" "$?" -rm -rf "$prefix" -``` - -- [ ] **Step 4: Run the new tests, verify they fail** - -Run: `just test` -Expected: T19–T22 fail (recipe doesn't yet write a filtered `.env.local`; T21/T22 don't yet validate source vars). - -- [ ] **Step 5: Move host `.env.local` sourcing to the top of the recipe** - -In `justfile`, after the existing `prefix="${BB_TMP_PREFIX:-/tmp}"` line, add this block: - -```bash - # Source the host .env.local (or BB_SOURCE_ENV override for tests). - # Required for both preflight and the filtered .env.local write step. - source_env="${BB_SOURCE_ENV:-tests/blackbox/.env.local}" - if [ -f "$source_env" ]; then - set -a - # shellcheck disable=SC1091 - . "$source_env" - set +a - fi -``` - -Then delete the redundant in-preflight sourcing block (currently lines 80–85 of the original file, which read): - -```bash - # Load API_TEST_* from tests/blackbox/.env.local. Required by the - # preflight script. .env.local is gitignored and holds the preview - # URLs and test credentials. - if [ -f tests/blackbox/.env.local ]; then - set -a - # shellcheck disable=SC1091 - . tests/blackbox/.env.local - set +a - fi -``` - -- [ ] **Step 6: Replace the copy step with copy + filtered-env write** - -Currently the recipe's copy step (around line 117) reads: - -```bash - # 4. Isolate: copy tests/blackbox/ (including hidden .envrc + .env.local) - # into the target dir. trailing /. makes cp include dotfiles. - echo "==> Isolating to $target" - mkdir -p "$target" - cp -r tests/blackbox/. "$target/" -``` - -Replace with: - -```bash - # 4. Isolate: copy tests/blackbox/ into the target, then replace the - # copied .env.local with a track-specific filtered file. Each session - # sees exactly the env an external integrator would. - echo "==> Isolating to $target" - mkdir -p "$target" - cp -r tests/blackbox/. "$target/" - rm -f "$target/.env.local" - - { - echo "API_TEST_APP_URL=${API_TEST_APP_URL:-}" - echo "API_TEST_DOCS_URL=${API_TEST_DOCS_URL:-}" - if [ -n "$selector" ]; then - src_key_var="${selector}_API_KEY" - src_id_var="${selector}_ORG_ID" - src_key="${!src_key_var:-}" - src_id="${!src_id_var:-}" - if [ -z "$src_key" ]; then - echo "ERROR: $src_key_var is empty or unset in $source_env" >&2 - exit 1 - fi - if [ -z "$src_id" ]; then - echo "ERROR: $src_id_var is empty or unset in $source_env" >&2 - exit 1 - fi - echo "BB_ORG=$selector" - echo "BB_API_KEY=$src_key" - echo "BB_ORG_ID=$src_id" - else - if [ -z "${API_TEST_LOGIN:-}" ]; then - echo "ERROR: API_TEST_LOGIN is empty or unset in $source_env" >&2 - exit 1 - fi - if [ -z "${API_TEST_PASS:-}" ]; then - echo "ERROR: API_TEST_PASS is empty or unset in $source_env" >&2 - exit 1 - fi - echo "API_TEST_LOGIN=$API_TEST_LOGIN" - echo "API_TEST_PASS=$API_TEST_PASS" - fi - } > "$target/.env.local" -``` - -Note on the redirection: the validation error `echo "ERROR: …" >&2; exit 1` lines inside the `{ ... }` block must NOT redirect to `$target/.env.local`. Because they're inside the `{ … } > "$target/.env.local"` group, their stdout would be redirected; but `>&2` sends to stderr (which is not redirected) and `exit 1` exits the recipe. This works correctly — verified by T21/T22. - -- [ ] **Step 7: Run all tests, verify all pass** - -Run: `just test` -Expected: all tests pass — existing T1–T8 (now using `BB_SOURCE_ENV`), T9–T18 from Task 2, and new T19–T22. - -- [ ] **Step 8: Sanity-check a real invocation locally** - -Run from the worktree root: - -```bash -BB_TMP_PREFIX=/tmp/bb-test-real BB_SKIP_PREFLIGHT=1 just bb_cycle BB1 -cat /tmp/bb-test-real/bb-1-BB1/.env.local -rm -rf /tmp/bb-test-real -``` - -Expected: the printed `.env.local` contains the real `BB1_API_KEY` value from the user's host `tests/blackbox/.env.local`, plus `BB_ORG=BB1`, `BB_ORG_ID=`, and the two URL vars. No `API_TEST_LOGIN`, `API_TEST_PASS`, `BB2_*`, or `BB3_*` lines. - -- [ ] **Step 9: Commit** - -```bash -git add justfile scripts/test_bb_cycle.sh -git commit -m "$(cat <<'EOF' -feat(bb): TRA-762 filtered .env.local per track + source-env validation - -Recipe now sources the host .env.local at the top (overridable via -BB_SOURCE_ENV for tests) and writes a track-specific .env.local into the -isolated target instead of copying the host file. Mint targets get -API_TEST_LOGIN/PASS + URLs; pre-key targets get flat BB_ORG/BB_API_KEY/ -BB_ORG_ID + URLs for the selected fixture only. Missing source vars -(BBn_API_KEY for the chosen selector, or LOGIN/PASS for mint) abort with -a clear error naming the missing var. Test harness gains a make_env -helper and all tests now point at a synthetic env via BB_SOURCE_ENV. - -Co-Authored-By: Claude Opus 4.7 (1M context) -EOF -)" -``` - ---- - -### Task 4: Session-start command picks wrapper - -**Files:** -- Modify: `justfile` (recipe `bb_cycle`, final echo block) -- Modify: `scripts/test_bb_cycle.sh` (add wrapper-name assertions) - -- [ ] **Step 1: Write failing tests** - -Append to `scripts/test_bb_cycle.sh`, before the final `echo` summary: - -```bash -# T23. Session-start command names BB_MINT_KEY.md on mint track -prefix=$(make_prefix); env_file=$(make_env "$prefix") -out=$(BB_SOURCE_ENV="$env_file" BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle 2>&1) -echo "$out" | grep -q "BB_MINT_KEY.md" -assert_true "session-start (mint): names BB_MINT_KEY.md" "$?" -echo "$out" | grep -qv "BB_PRE_KEY.md" -assert_true "session-start (mint): does not name BB_PRE_KEY.md" "$?" -rm -rf "$prefix" - -# T24. Session-start command names BB_PRE_KEY.md on pre-key track -prefix=$(make_prefix); env_file=$(make_env "$prefix") -out=$(BB_SOURCE_ENV="$env_file" BB_TMP_PREFIX="$prefix" BB_SKIP_PREFLIGHT=1 just bb_cycle BB1 2>&1) -echo "$out" | grep -q "BB_PRE_KEY.md" -assert_true "session-start (pre-key): names BB_PRE_KEY.md" "$?" -echo "$out" | grep -qv "BB_MINT_KEY.md" -assert_true "session-start (pre-key): does not name BB_MINT_KEY.md" "$?" -rm -rf "$prefix" -``` - -- [ ] **Step 2: Run tests, verify they fail** - -Run: `just test` -Expected: T23/T24 fail — the recipe currently emits `BB.md` regardless of track. - -- [ ] **Step 3: Parameterize the wrapper name in the session-start echo** - -In `justfile`, replace the final echo block (currently lines 119–124, which read): - -```bash - # 5. Session-start command for copy-paste - echo - echo "==> Ready. Start the session with:" - echo - echo " cd $target && direnv allow && csp 'run blackbox tests per BB.md'" - echo -``` - -with: - -```bash - # 5. Session-start command for copy-paste. The wrapper file matches the - # chosen track; the wrapper itself points back at BB.md for the - # shared methodology. - if [ -n "$selector" ]; then - wrapper="BB_PRE_KEY.md" - else - wrapper="BB_MINT_KEY.md" - fi - echo - echo "==> Ready. Start the session with:" - echo - echo " cd $target && direnv allow && csp 'run blackbox tests per $wrapper'" - echo -``` - -- [ ] **Step 4: Run all tests, verify all pass** - -Run: `just test` -Expected: every test passes — T1–T8 (existing, updated for `BB_SOURCE_ENV`), T9–T18 (Task 2), T19–T22 (Task 3), T23–T24 (this task). - -- [ ] **Step 5: Spot-check the printed command** - -Run: - -```bash -BB_TMP_PREFIX=/tmp/bb-spot-check BB_SKIP_PREFLIGHT=1 just bb_cycle -BB_TMP_PREFIX=/tmp/bb-spot-check BB_SKIP_PREFLIGHT=1 just bb_cycle BB1 -rm -rf /tmp/bb-spot-check -``` - -Expected: first command prints `csp 'run blackbox tests per BB_MINT_KEY.md'`; second prints `csp 'run blackbox tests per BB_PRE_KEY.md'`. - -- [ ] **Step 6: Commit** - -```bash -git add justfile scripts/test_bb_cycle.sh -git commit -m "$(cat <<'EOF' -feat(bb): TRA-762 bb_cycle prints wrapper matching the chosen track - -Session-start command now names BB_MINT_KEY.md (mint, default) or -BB_PRE_KEY.md (pre-key selector). Each wrapper sets up its track-specific -preamble and points back at BB.md for the shared methodology. - -Co-Authored-By: Claude Opus 4.7 (1M context) -EOF -)" -``` - ---- - -## Verification - -After all four tasks land: - -1. `just test` passes cleanly (all 24+ tests). -2. `just bb_cycle BB1` from the repo root creates `/tmp/bb-NN-BB1/` with a filtered `.env.local` containing `BB_ORG=BB1`, `BB_API_KEY=`, `BB_ORG_ID=`, and the URL vars only; prints a `csp` command referencing `BB_PRE_KEY.md`. -3. `just bb_cycle` (no args) from the repo root creates `/tmp/bb-NN/` with a filtered `.env.local` containing `API_TEST_LOGIN`/`API_TEST_PASS`/URLs only; prints a `csp` command referencing `BB_MINT_KEY.md`. -4. Three concurrent `just bb_cycle BB1` / `BB2` / `BB3` invocations (from three terminals) all pick the same cycle number and land in distinct dirs. - -## Out of scope - -This plan does not: -- Aggregate `FINDINGS.md` across parallel sessions. -- Run the BB methodology itself (that's `csp 'run blackbox tests per '`, downstream of the recipe). -- Manage fixture data (scans, seed cleanup, etc.). -- Extend the recipe with additional tracks beyond mint and pre-key. diff --git a/superpowers/plans/2026-05-22-tra-799-asset-location-removal.md b/superpowers/plans/2026-05-22-tra-799-asset-location-removal.md deleted file mode 100644 index 079a03b..0000000 --- a/superpowers/plans/2026-05-22-tra-799-asset-location-removal.md +++ /dev/null @@ -1,418 +0,0 @@ -# TRA-799 — Asset current-location removal docs sweep — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to -> implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax. - -**Goal:** Update `docs/api/` so the asset resource no longer documents a current -location — read it from the scan-data endpoints — and `POST` / `PATCH` reject -`location_id` / `location_external_key` uniformly on presence. - -**Architecture:** Prose-only sweep across seven `docs/api/` files plus a new -changelog entry. The hard part is `resource-identifiers.md`, which uses asset -`location_*` as its primary teaching example for several patterns; those examples -re-anchor on `LocationView.parent_*` and the `/reports/asset-locations` row. -Verification is `pnpm build` (catches broken internal links) plus a `git grep` -audit. Design spec: -`superpowers/specs/2026-05-22-tra-799-asset-location-removal-design.md`. - -**Tech Stack:** Docusaurus, Markdown / MDX, pnpm. - ---- - -## Task 1: Commit the design artifacts - -**Files:** -- Create: `superpowers/specs/2026-05-22-tra-799-asset-location-removal-design.md` (done) -- Create: `superpowers/plans/2026-05-22-tra-799-asset-location-removal.md` (this file) - -- [ ] **Step 1: Commit** - -```bash -git add superpowers/specs/2026-05-22-tra-799-asset-location-removal-design.md \ - superpowers/plans/2026-05-22-tra-799-asset-location-removal.md -git commit -m "docs(api): TRA-799 — design spec + plan for asset current-location removal" -``` - ---- - -## Task 2: `docs/api/data-model.md` - -The asset detail view stops being a scan-data consumption endpoint. - -**Files:** -- Modify: `docs/api/data-model.md` - -- [ ] **Step 1: Scan-data consumption table** — remove the - `GET /api/v1/assets/{asset_id}` row (the one whose Shape cell reads "Current - location embedded on the asset view (`location_id`, `location_external_key`)"). - Table keeps the `/assets/{id}/history` and `/reports/asset-locations` rows. - -- [ ] **Step 2: Lineage paragraph after the table** — the sentence "The asset - view's embedded location fields share the same lineage; the difference is read - shape, not data source." is no longer true. Rewrite the paragraph so it - describes the two `tracking:read` endpoints as the projections of the - scan-event stream without referencing an asset-view location field. - -- [ ] **Step 3: "single-asset view exposes location as flat scalars" paragraph** - — remove it entirely. The asset view exposes no location field; the - flat-scalar FK-pair cross-reference now belongs to `LocationView.parent_*`, - documented on the resource-identifiers page. - -- [ ] **Step 4: "What you cannot do" — `PATCH` bullet** — rewrite. Current text - invokes the accept-if-matches / reject-if-differs rule and a "verbatim - `GET` → `PATCH` round-trip that echoes the current location back is silently - accepted." With location off the read shape there is nothing to echo: `PATCH` - with `location_id` / `location_external_key` in the body returns - `400 read_only` on presence, identical to `POST`. Keep the `POST` bullet and - the "no public endpoint to record a scan event" bullet as-is. - -- [ ] **Step 5: "Consumption pattern guidance" closing sentence** — the last - sentence of the batch-lookup paragraph reads "For a single asset, - `GET /api/v1/assets/{asset_id}` returns the same location fields directly on - the asset view." Rewrite: a single asset's current location comes from - `/reports/asset-locations` filtered by `asset_id` (or `asset_external_key`), - or from the latest row of `/assets/{asset_id}/history`. - -- [ ] **Step 6: Verify** — `git grep -n "location" docs/api/data-model.md`; every - remaining hit is a path param, a `parent_*` reference, a `/reports/` or - `/history` reference, or scan-data prose — none describes a field on the asset - read shape. - -- [ ] **Step 7: Commit** - -```bash -git add docs/api/data-model.md -git commit -m "docs(api): TRA-799 — drop asset-view location from data-model scan-data section" -``` - ---- - -## Task 3: `docs/api/resource-identifiers.md` - -Heaviest file. Work top-to-bottom so anchor references stay coherent. - -**Files:** -- Modify: `docs/api/resource-identifiers.md` - -- [ ] **Step 1: `*_external_key` foreign-key bullet (intro list)** — the example - "`location_external_key` on assets, `parent_external_key` on locations" — drop - the assets example; keep `parent_external_key` on locations. - -- [ ] **Step 2: "List filters accept both forms" section** — re-anchor. The - prose "For the assets list, filter by current location using either - `location_id` or `location_external_key`" and its two curl examples move to - `GET /api/v1/reports/asset-locations` (which keeps both repeatable location - filter forms). State that the assets list itself no longer filters by - location. - -- [ ] **Step 3: "Foreign-key fields in responses come as flat scalar pairs"** — - re-anchor the JSON example from an asset response to a `LocationView` - response carrying `parent_id` (int) + `parent_external_key` (string). The - surrounding prose ("Both fields are populated whenever the relationship - exists ... unset → both `null`") stays true for `parent_*`. Rewrite the - closing "two paired-FK shapes look symmetric ... write surfaces differ" - sentence: the surviving response FK pair is location parentage, fully - writable master-data; asset location is no longer on any response. - -- [ ] **Step 4: "Always present / Present as `null`" table** — remove - `location_id`, `location_external_key` from the **Present as `null`** row; - keep `parent_id`, `parent_external_key`, `description`, `valid_to`. - -- [ ] **Step 5: `AssetView` / `AssetLocationItem` required-fields paragraph** — - remove asset-side location references. `AssetLocationItem` (the - `/reports/asset-locations` row) keeps `location_id` / `location_external_key` - as required-and-nullable — that sentence stays. - -- [ ] **Step 6: Ancestor-identifiers-across-tombstones section** — remove the - sentence "The same projection applies to `location_external_key` on - `GET /api/v1/assets?include_deleted=true` when the asset's current location - has been soft-deleted." Keep the `parent_external_key` projection on - `/locations` and the `/reports/asset-locations` null-projection exception. - -- [ ] **Step 7: ":::important Scope of `POST` and `PATCH /api/v1/assets/{asset_id}`" - admonition** — rewrite the location sentences. `location_id` / - `location_external_key` are not part of the asset resource at all: not the - read shape, not `CreateAssetWithTagsRequest`, not `UpdateAssetRequest`. Both - `POST` and `PATCH` reject either field on presence with - `400 validation_error` / `code: read_only`; there is no accept-if-matches - case because there is no read value to match. Keep the ingestion-path error - detail; add that current location is read from `/reports/asset-locations` or - `/assets/{id}/history`. - -- [ ] **Step 8: "Accept-if-matches, reject-if-differs" rule paragraph** — remove - "the scan-derived `location_id` / `location_external_key`" from the - read-only-field set governed by this rule. The set is `id`, `created_at`, - `updated_at`, `deleted_at` (return `read_only`) and `external_key`, `tags` - (return `invalid_context`). Add a short note that asset `location_*` is a - separate case — not on the read shape, rejected on presence on `POST` and - `PATCH`. - -- [ ] **Step 9: Read-only-field hint table** — the `location_id` and - `location_external_key` rows. Keep both rows (the fields still reject) but the - table's framing is "fields rejected on PATCH"; ensure the Surface column reads - `POST /assets`, `PATCH /assets` and the prose/caption distinguishes these from - the accept-if-matches rows — they reject on presence, not on divergence. - -- [ ] **Step 10: Asset/location asymmetry paragraph(s)** (the "`UpdateAssetRequest` - declares only [...]" bullet and the "asset / location asymmetry is by design" - paragraph and the "practical consequence" paragraph) — adjust so they no - longer state or imply that `location_id` appears on the asset read shape. The - master-data / scan-data framing stays; `location_*` is absent from the read - shape and both write schemas; only a hand-built payload inventing the field - hits the `400 read_only`. Remove "scrubbed client-side by construction" / - "send the unscrubbed read shape and see the 400" wording where it implies the - field is on the GET response. - -- [ ] **Step 11: Verbatim `GET` → `PATCH` round-trip example** — the bash comment - listing read-only fields ("id, timestamps, tags, external_key, location_id, - location_external_key, and (for locations) parent_external_key") — remove - `location_id, location_external_key`; keep the rest. - -- [ ] **Step 12: "Paired-key behavior per verb" section + matrix** — asset - location is no longer a paired FK relationship for read or filter purposes. - Rewrite: the intro "Each paired FK relationship" sentence drops the asset - example. In the matrix, the `POST /assets` and `PATCH /assets/{id}` rows - collapse to a uniform "`400 read_only` on presence of either field"; remove - `/assets` from the `GET` list-filter row (only `/locations` and - `/reports/asset-locations` carry location/parent filter pairs). Rewrite the - two prose paragraphs after the matrix (`POST /assets` is not a paired-key - surface; `PATCH` follows accept-if-matches) so the asset side is described as - reject-on-presence on both verbs, not accept-if-matches. The `fk_not_found` - paragraph: `location_id` / `location_external_key` no longer appear on an - asset write body that resolves FKs — keep `fk_not_found` for the location - `parent_*` case and the `/reports/asset-locations` filters. - -- [ ] **Step 13: "clear a relationship on `PATCH`" paragraph** — it already says - "Asset location is **not** a writable-nullable — clearing it is not a PATCH - operation; record a scan event instead." Confirm this stays correct and the - cross-reference anchor still resolves; no change expected beyond the anchors - touched above. - -- [ ] **Step 14: Locations delete-semantics** — remove the sentence "The same - shape would appear on assets placed at a deleted location (`location_id` - populated, `location_external_key` null)." Keep the `parent_*` invariant - prose. - -- [ ] **Step 15: "Active placed assets" lookup line** — change - `GET /api/v1/assets?location_id={location_id}` to - `GET /api/v1/reports/asset-locations?location_id={location_id}`; drop the - now-removed `/assets` filter form. - -- [ ] **Step 16: `external_key` value-rules paragraph** — the sentence mapping - `external_key`-typed filters to endpoints ("`?external_key=`, - `?location_external_key=`, `?parent_external_key=` on `/assets`, - `/locations`, and `/reports/asset-locations`") — `?location_external_key=` is - now only on `/reports/asset-locations`. Tighten the per-endpoint mapping. - -- [ ] **Step 17: Verify** — re-grep: - `git grep -nE 'location_id|location_external_key' docs/api/resource-identifiers.md`. - Every hit must be a path param, a `parent_*` line, a `LocationView` field, an - `AssetLocationItem` field, or a `/reports/asset-locations` filter — none an - asset read-shape field or `/assets` list filter. - -- [ ] **Step 18: Commit** - -```bash -git add docs/api/resource-identifiers.md -git commit -m "docs(api): TRA-799 — re-anchor FK-pair docs off asset location in resource-identifiers" -``` - ---- - -## Task 4: `docs/api/pagination-filtering-sorting.md` - -**Files:** -- Modify: `docs/api/pagination-filtering-sorting.md` - -- [ ] **Step 1: Filter-param table** — `GET /api/v1/assets` row: remove - `location_id` (repeatable) and `location_external_key` (repeatable). Resulting - filters: `external_key` (repeatable), `is_active`, `include_deleted`, `q`. - The `/reports/asset-locations` row keeps its location pair. - -- [ ] **Step 2: "Paired-by-id-and-by-natural-key filters are mutually - exclusive" section** — the opening sentence enumerates pairs "— - `location_id` / `location_external_key` on `/assets` and on - `/reports/asset-locations`, `parent_id` / `parent_external_key` on - `/locations`, `asset_id` / `asset_external_key` on - `/reports/asset-locations`". Drop `on /assets` — the location pair survives on - `/reports/asset-locations` only. Re-check the rest of the section for other - `/assets` location-filter mentions. - -- [ ] **Step 3: "Repeatable filters" examples** — the two curl examples - ("Assets currently at LOC-A OR LOC-B" against `/api/v1/assets?location_*`) — - re-point at `GET /api/v1/reports/asset-locations`, which keeps repeatable - `location_external_key` / `location_id`. The comma-separated-values caveat - paragraph below stays (it is about `external_key`-typed filters generally). - -- [ ] **Step 4: "Validator behavior on writes" — first paragraph** — remove "the - scan-derived `location_id` / `location_external_key` on assets" from the - accept-if-matches / reject-if-differs field set. The `code: read_only` clause - keeps `id` and timestamps. - -- [ ] **Step 5: "Validator behavior on writes" — second paragraph** — currently - `POST`-only ("On `POST`, the asset `location_*` fields are absent from - `CreateAssetWithTagsRequest` ..."). Generalize to both verbs: `location_id` / - `location_external_key` are absent from `CreateAssetWithTagsRequest` *and* - `UpdateAssetRequest`; sending either on `POST` or `PATCH` returns - `400 validation_error` / `code: read_only` on presence — no accept-if-matches - half on either verb. - -- [ ] **Step 6: "Worked examples per resource → Assets"** — the example "List - active assets currently at one of two locations (by `external_key`) ..." uses - `location_external_key` on `/assets`. Replace with a location-free asset-list - example (active assets, newest first, 100 per page: - `?is_active=true&sort=-created_at&limit=100`) and add a one-line pointer to - the Asset-locations report example below for filtering by location. - -- [ ] **Step 7: Verify** — grep the file; every `location_*` hit is a path param, - a `/reports/asset-locations` filter, a `parent_*` ref, or history prose. - -- [ ] **Step 8: Commit** - -```bash -git add docs/api/pagination-filtering-sorting.md -git commit -m "docs(api): TRA-799 — drop asset location filter from pagination/filtering page" -``` - ---- - -## Task 5: Downstream cross-reference cleanup - -`quickstart.mdx`, `errors.md`, `design-notes.md` each carry one or more -sentences asserting asset `location_*` is a read-shape field under -accept-if-matches. Fix all three together. - -**Files:** -- Modify: `docs/api/quickstart.mdx` -- Modify: `docs/api/errors.md` -- Modify: `docs/api/design-notes.md` - -- [ ] **Step 1: `quickstart.mdx` §3 round-trip paragraph** — remove - `location_id`, `location_external_key` from the read-only-fields list that - "echoing them straight back from a `GET` response is safe." The list keeps - `id` / `created_at` / `updated_at` / `deleted_at`, `tags`, `external_key`. - -- [ ] **Step 2: `quickstart.mdx` following paragraph** — the clause "Differing - values on the truly server-managed fields (`id`, `created_at`, `updated_at`, - `deleted_at`, and the scan-derived `location_id` / `location_external_key` on - assets) return `code: read_only`" — drop the asset `location_*` mention from - this accept-if-matches-framed sentence. The earlier standalone sentence - "Asset location specifically cannot be set via `PATCH` under any form; - current location is derived from scan events." stays. - -- [ ] **Step 3: `errors.md` `invalid_context` catalog entry** — the - list-endpoint-filter enumeration includes "`location_id` / - `location_external_key`". These are now `/reports/asset-locations` filters - only; adjust the enumeration so it does not imply `/assets` carries them. - -- [ ] **Step 4: `errors.md` `ambiguous_fields` catalog entry** — remove `/assets` - from the surfaces emitting the code on the `location_id` vs - `location_external_key` pair (the `/assets` location filter is gone — the - pair survives on `/reports/asset-locations`). Update the - `PATCH /api/v1/assets/{asset_id}` sentence: asset `location_*` reject on - presence, not "follow accept-if-matches ... a differing value generates a - per-field `read_only` entry." - -- [ ] **Step 5: `errors.md` `read_only` catalog entry** — the entry says asset - `location_*` fire `read_only` on `POST` whenever present and on `PATCH` only - when the value differs (accept-if-matches). Update: they fire on **presence** - on both `POST` and `PATCH`. Remove the "echoing the current value back is - silently stripped" half as it applies to asset `location_*` (it still applies - to the four server-managed fields — keep that). The ingestion-path detail - string stays. - -- [ ] **Step 6: `design-notes.md` accept-if-matches field list** — the list - "(`id`, `created_at`, `deleted_at`, `location_id`, `location_external_key`, - `tags`, `external_key`)" — remove `location_id`, `location_external_key`. - Re-check the surrounding sentence about the `code` split (server-managed → - `read_only`, subresource-settable → `invalid_context`) still reads correctly - without the asset location example. - -- [ ] **Step 7: Verify** — grep all three files; no remaining sentence frames - asset `location_*` as a read-shape / accept-if-matches field. - -- [ ] **Step 8: Commit** - -```bash -git add docs/api/quickstart.mdx docs/api/errors.md docs/api/design-notes.md -git commit -m "docs(api): TRA-799 — fix asset-location read-shape claims in quickstart/errors/design-notes" -``` - ---- - -## Task 6: `docs/api/changelog.md` - -**Files:** -- Modify: `docs/api/changelog.md` - -- [ ] **Step 1: Add a new entry** at the top of the `## v1.0 — Launch (TBD)` - section, immediately above the `### BB66 docs sweep` heading. The entry - records: `location_id` / `location_external_key` removed from the asset - response (`GET /api/v1/assets`, `GET /api/v1/assets/{asset_id}`); the - `?location_id` / `?location_external_key` filters removed from - `GET /api/v1/assets`; `POST` and `PATCH` now reject `location_id` / - `location_external_key` uniformly on presence with `400` / `code: read_only`; - current location is read from `GET /api/v1/reports/asset-locations` and - `GET /api/v1/assets/{asset_id}/history`. Note it is a pre-launch change with - no `v1.0.0`-or-later wire baseline to break. Cross-link Data model and - Resource identifiers. Match the voice and depth of the existing BB40 - master-data / scan-data entry. Do **not** edit any existing entry. - -- [ ] **Step 2: Verify** — `pnpm build` resolves every internal link in the new - entry. - -- [ ] **Step 3: Commit** - -```bash -git add docs/api/changelog.md -git commit -m "docs(api): TRA-799 — changelog entry for asset location removal" -``` - ---- - -## Task 7: Build verification and final audit - -**Files:** none (verification only) - -- [ ] **Step 1: Production build** - -Run: `pnpm build` -Expected: build completes with no errors and no broken-link warnings. - -- [ ] **Step 2: `git grep` audit** - -Run: `git grep -nE 'location_id|location_external_key' docs/api/` -Expected: every hit is one of — a path parameter (`{location_id}`), a -`parent_id` / `parent_external_key` reference, a `LocationView` field, an -`AssetLocationItem` / `AssetHistoryItem` row field, or a -`/reports/asset-locations` / `/history` filter. No hit describes a field on the -asset read shape or a `GET /api/v1/assets` list filter. - -- [ ] **Step 3: Lint (if configured)** - -Run: `pnpm lint` -Expected: passes (or no lint script — skip). - -- [ ] **Step 4: Open the PR** — push the branch and open a PR against `main` - titled `docs(api): TRA-799 — remove asset current-location from the API docs`. - PR body summarizes the three contract changes and links platform PR #384. - Do not merge. - ---- - -## Self-review notes - -- **Spec coverage:** every per-file change in the spec maps to a task — - data-model (T2), resource-identifiers (T3), pagination-filtering-sorting (T4), - quickstart/errors/design-notes (T5), changelog (T6). Out-of-scope files - (date-fields, id-format, http-method-coverage, authentication, OpenAPI spec) - are correctly absent. -- **No placeholders:** every step names the exact file, the exact section, and - the exact change. New prose is written at execution time against the named - framing — acceptable here because the executor is the same agent holding the - spec's editorial decisions. -- **Consistency:** the accept-if-matches field set after removal — `id`, - `created_at`, `updated_at`, `deleted_at`, `external_key`, `tags` — is stated - identically in T3 Step 8, T4 Step 4, T5 Steps 1–2/6. The post-change asset - contract — "reject on presence on `POST` and `PATCH`" — is worded consistently - across T2/T3/T4/T5. diff --git a/superpowers/plans/2026-05-23-tra-816-tag-cascade.md b/superpowers/plans/2026-05-23-tra-816-tag-cascade.md deleted file mode 100644 index 2f0182d..0000000 --- a/superpowers/plans/2026-05-23-tra-816-tag-cascade.md +++ /dev/null @@ -1,197 +0,0 @@ -# TRA-816 Tag-Cascade Docs Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Document the soft-delete-cascade-to-tags fix shipped in platform PR #397 — one new changelog entry plus one tightening sentence in the resource-identifiers Tag CRUD section. - -**Architecture:** Two text-only edits in `docs/api/`. No spec, no schema, no new field. Each edit anchors on existing prose; both are pre-launch, no `v1.0.0`-baseline break. - -**Tech Stack:** Docusaurus (markdown / MDX), pnpm. - ---- - -### Task 1: Add Tag CRUD cascade sentence to resource-identifiers.md - -**Files:** - -- Modify: `docs/api/resource-identifiers.md` (Tag CRUD section, around line 647, between the "Tag writes use the parent resource's write scope" paragraph and the "The same endpoint accepts all three kinds" paragraph) - -- [ ] **Step 1: Read the surrounding prose to confirm exact line context** - -Run: `grep -n "Tag writes use the parent\|The same endpoint accepts all three kinds\|Tag CRUD" docs/api/resource-identifiers.md` -Expected: shows `### Tag CRUD {#tag-crud}` heading, the `Tag writes use the parent…` line, and the `The same endpoint accepts all three kinds…` line in adjacent positions. - -- [ ] **Step 2: Insert the cascade sentence** - -Use Edit tool on `docs/api/resource-identifiers.md`. - -`old_string`: - -``` -Tag writes use the parent resource's write scope, not a separate `tags:write` — there is no per-tag scope. - -The same endpoint accepts all three kinds — the discriminator travels in the body: -``` - -`new_string`: - -``` -Tag writes use the parent resource's write scope, not a separate `tags:write` — there is no per-tag scope. - -Soft-deleting a parent asset or location cascades to its attached tag rows in the same transaction with the same `deleted_at`, releasing the `(org_id, tag_type, value)` natural-key slot for immediate reattachment on another entity in the same organization. The cascaded tag rows follow the same [soft-delete visibility](#soft-delete-visibility) rules as their parent — hidden by default on the parent's read shape, surfaceable through `?include_deleted=true` on list surfaces that accept it. - -The same endpoint accepts all three kinds — the discriminator travels in the body: -``` - -- [ ] **Step 3: Verify the anchor `#soft-delete-visibility` exists on the page** - -Run: `grep -n '#soft-delete-visibility' docs/api/resource-identifiers.md` -Expected: at least two hits — the heading definition (`### Soft-delete visibility on lists {#soft-delete-visibility}`) and one or more existing cross-links. - -- [ ] **Step 4: Commit** - -```bash -git add docs/api/resource-identifiers.md -git commit -m "$(cat <<'EOF' -docs(api): TRA-816 — note parent soft-delete cascades to attached tag rows - -One sentence added to the Tag CRUD section: when a parent asset or -location is soft-deleted, its attached tag rows are soft-deleted in the -same transaction with the same deleted_at, so the (org_id, tag_type, -value) natural-key slot is released for immediate reattachment on -another entity in the same org. Cross-links the existing soft-delete -visibility section for the read-side counterpart. -EOF -)" -``` - ---- - -### Task 2: Add changelog entry for the cascade fix - -**Files:** - -- Modify: `docs/api/changelog.md` (top of `## v1.0 — Launch (TBD)` section, above `### BB1 hygiene` at line 14) - -- [ ] **Step 1: Confirm the BB1 hygiene heading is still at line 14** - -Run: `grep -n "^### BB1 hygiene\|^### Asset current location" docs/api/changelog.md | head -5` -Expected: `### BB1 hygiene` appears as the first entry under `## v1.0 — Launch (TBD)`; `### Asset current location removed from the asset resource` appears immediately below it. - -- [ ] **Step 2: Insert the new entry above the BB1 hygiene one** - -Use Edit tool on `docs/api/changelog.md`. - -`old_string`: - -``` -Initial public API release. Stable contract for paths, field names, response shapes, and error envelopes per the [v1 stability commitment](./versioning). - -### BB1 hygiene — Try-it wording dropped, `invalid_context` added to versioning open-enums list, `datamodel-codegen` version string unmangled -``` - -`new_string`: - -``` -Initial public API release. Stable contract for paths, field names, response shapes, and error envelopes per the [v1 stability commitment](./versioning). - -### Tag rows cascade-soft-delete with their parent asset or location - -Soft-deleting an asset or location now cascades to the attached tag rows in the same transaction with the same `deleted_at`, releasing the `(org_id, tag_type, value)` partial-unique slot the tag was holding. Brings runtime behavior in line with the existing [Tags use a composite natural key](./resource-identifiers#tags-use-a-composite-natural-key) contract — the page documented that soft-delete frees the natural-key handle for reuse, the platform was leaving orphan tag rows live and blocking reuse. Pre-launch fix; no `v1.0.0`-or-later wire baseline to break. - -- **Cascade on parent soft-delete.** `DELETE /api/v1/assets/{asset_id}` and `DELETE /api/v1/locations/{location_id}` soft-delete every tag row attached to the parent at the same instant, sharing the parent's `deleted_at`. After the cascade the tag's `(org_id, tag_type, value)` slot is free, and the same value can be attached to another asset or location in the org without a 409. See [Resource identifiers → Tag CRUD](./resource-identifiers#tag-crud) for the prose. -- **One-shot sweep for the existing orphan footprint.** A migration ships in the same release that soft-deletes the pre-fix orphan rows (tag rows whose parent asset or location is already soft-deleted), so the natural-key slots they were occupying are released as part of the deploy. Integrators who saw a `409 conflict` on `POST /api/v1/{resource}/{id}/tags` naming a tag holder they could not see in any list (the `?external_key=...` lookup returned an empty array, the path-param `GET` returned `404 not_found`) can re-try the attach after the deploy. -- **Conflict 409 detail does not leak hidden entity names.** Defence-in-depth: the server-side conflict lookup that builds the `409 conflict` detail string now filters soft-deleted parents on the join, so any future regression where an orphan tag row slips through returns the generic 409 (`detail: "tag : already exists"`, per the [errors page](./errors#error-type-catalog)) rather than naming a parent the caller has no way to address. The cascade above closes the path that produced these messages; the join filter ensures the message shape stays correct if a new code path ever reintroduces the bug. -``` - -- [ ] **Step 3: Verify the changelog still parses (no broken anchors, no duplicated headings)** - -Run: `grep -nE "^### " docs/api/changelog.md | head -10` -Expected: the new `### Tag rows cascade-soft-delete with their parent asset or location` is the first entry; `### BB1 hygiene` is second; `### Asset current location removed from the asset resource` is third. - -- [ ] **Step 4: Commit** - -```bash -git add docs/api/changelog.md -git commit -m "$(cat <<'EOF' -docs(api): TRA-816 — changelog entry for tag-cascade soft-delete fix - -New entry under v1.0 Launch: parent soft-delete now cascades to -attached tag rows, the existing orphan footprint is swept by the -release migration, and the conflict-detail join filter ensures -soft-deleted parent names cannot leak into 409 detail strings. Frames -the fix as bringing runtime in line with the existing -soft-delete-frees-the-handle contract on the Tag CRUD page. -EOF -)" -``` - ---- - -### Task 3: Build verification - -**Files:** - -- None (read-only verification) - -- [ ] **Step 1: Run the Docusaurus production build** - -Run: `pnpm build 2>&1 | tail -30` -Expected: build completes with `[SUCCESS]` line; no `Error:` lines; no broken internal links flagged on `resource-identifiers` or `changelog`. - -- [ ] **Step 2: Sanity-grep the edits** - -Run: `grep -nE 'cascade|orphan' docs/api/changelog.md docs/api/resource-identifiers.md` -Expected: the new cascade-related hits appear in `changelog.md` (the new entry) and `resource-identifiers.md` (the new Tag CRUD sentence). No pre-existing prose contradicts them. - -- [ ] **Step 3: Confirm no stale references** - -Run: `grep -nE 'soft.?delete.*tag|tag.*soft.?delete' docs/api/*.md docs/api/*.mdx | grep -v changelog.md | grep -v resource-identifiers.md | head -20` -Expected: no hits that describe the *old* (orphan-leaking) behavior. Hits that document the general soft-delete contract for assets and locations are fine. - ---- - -### Task 4: Push branch and open PR - -**Files:** - -- None (git + gh) - -- [ ] **Step 1: Push the branch** - -Run: `git push -u origin docs/tra-816-tag-cascade` -Expected: branch created on origin, no errors. - -- [ ] **Step 2: Open the PR** - -Run: - -```bash -gh pr create --title "docs(api): TRA-816 — tag-cascade soft-delete docs" --body "$(cat <<'EOF' -## Summary - -Docs companion to platform PR #397 (TRA-816, merged). Narrow scope: - -- New changelog entry under `## v1.0 — Launch (TBD)` describing the parent-soft-delete cascade to attached tags, the one-shot sweep migration, and the conflict-detail join filter. -- One sentence added to `docs/api/resource-identifiers.md` Tag CRUD section noting the cascade and cross-linking the existing soft-delete visibility paragraph. - -The documented contract on the Tag CRUD page already said the natural-key slot is freed on soft-delete; the platform was failing to match that on the parent-cascade path. These edits close the inference gap and log the fix. - -Closes [TRA-816](https://linear.app/trakrf/issue/TRA-816). - -## Test plan - -- [x] `pnpm build` succeeds with no broken internal links. -- [x] `git grep` audit: no stale prose contradicts the new cascade contract. -- [ ] Reviewer eyes on the changelog phrasing and the placement of the new Tag CRUD sentence. - -🤖 Generated with [Claude Code](https://claude.com/claude-code) -EOF -)" -``` - -Expected: PR URL printed; PR opens in draft-or-ready state per repo default. - -- [ ] **Step 3: Capture the PR URL** - -Echo or note the URL returned by `gh pr create` for the final session summary. diff --git a/superpowers/plans/2026-05-27-tra-551-loc-auto-mint-docs.md b/superpowers/plans/2026-05-27-tra-551-loc-auto-mint-docs.md deleted file mode 100644 index 52be14a..0000000 --- a/superpowers/plans/2026-05-27-tra-551-loc-auto-mint-docs.md +++ /dev/null @@ -1,347 +0,0 @@ -# TRA-551 — location auto-mint docs (LOC-NNN finalization) Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Update the public docs (`docs/api/*`) to match the finalized location `external_key` auto-mint format shipped in platform PR #418 — `LOC-NNN` (3 digits, first mint `LOC-001`), narrower than `ASSET-NNNN` (4 digits) by deliberate triage decision because locations are typically named-and-known artifacts where auto-mint is the exception, not the norm. - -**Architecture:** Surgical edit pass across four pages — `resource-identifiers.md`, `errors.md`, `date-fields.md`, `changelog.md`. The auto-mint section on `resource-identifiers.md` is the load-bearing surface and gains a one-sentence asymmetry-rationale note plus the digit-count-agnostic clarification (the namespace pattern accommodates `LOC-1000+` once the 3-digit space is exhausted; the `^LOC-\d{3,}$` pattern that the backend tests against is the contract, not strict 3-digit width). The other three pages are find-and-replace of the literal `LOC-NNNN` / `LOC-0001` strings. New changelog entry sits under `## v1.0 — Launch (TBD)`, peer to the other unlabeled launch-window entries (no BB-cycle prefix; this is closing out a previously-shipped item, not a BB-cycle ship). No Linear ticket identifiers appear in user-facing copy. - -**Tech Stack:** Docusaurus 3, MDX/Markdown only — no code changes. Validate with `pnpm build` and `pnpm typecheck`. - ---- - -## File Structure - -Modified files (no creates, no deletes): - -- `docs/api/resource-identifiers.md` — auto-mint section, intro line, code-comment, system-of-record paragraph -- `docs/api/errors.md` — idempotency bullet for `POST /assets`, `POST /locations` -- `docs/api/date-fields.md` — example payload `external_key` values -- `docs/api/changelog.md` — new entry under `## v1.0 — Launch (TBD)` - -Plan artifact: - -- `superpowers/plans/2026-05-27-tra-551-loc-auto-mint-docs.md` (this file — already created, not part of the work) - ---- - -## Task 1: Update the load-bearing auto-mint section in `resource-identifiers.md` - -**Files:** -- Modify: `docs/api/resource-identifiers.md:16` (one inline reference) -- Modify: `docs/api/resource-identifiers.md:485-492` (auto-mint intro paragraph + table + recycle note) -- Modify: `docs/api/resource-identifiers.md:509` (bash code-comment) -- Modify: `docs/api/resource-identifiers.md:521` (system-of-record paragraph) - -- [ ] **Step 1: Edit line 16 — change the namespace pair in the intro** - -Replace: - -``` -Both are write-routable: caller-supplied on create, or server-minted from a per-organization `ASSET-NNNN` / `LOC-NNNN` namespace when `external_key` is omitted — see [auto-mint behavior](#external_key-is-optional-on-create). -``` - -with: - -``` -Both are write-routable: caller-supplied on create, or server-minted from a per-organization `ASSET-NNNN` / `LOC-NNN` namespace when `external_key` is omitted — see [auto-mint behavior](#external_key-is-optional-on-create). -``` - -- [ ] **Step 2: Edit line 485 — the prose lead-in for the auto-mint table** - -Replace: - -``` -`external_key` is **optional on `POST` for assets and locations alike**. Supply your own value to anchor the row to a partner-side handle (a SKU, an ERP code, an operator-typed location label, a row from a planned-layout export), or omit the field on the request body and the server assigns the lowest unused slot in the per-organization `ASSET-NNNN` / `LOC-NNNN` namespace. Each resource type has its own format and its own namespace: -``` - -with: - -``` -`external_key` is **optional on `POST` for assets and locations alike**. Supply your own value to anchor the row to a partner-side handle (a SKU, an ERP code, an operator-typed location label, a row from a planned-layout export), or omit the field on the request body and the server assigns the lowest unused slot in the per-organization `ASSET-NNNN` / `LOC-NNN` namespace. Each resource type has its own format and its own namespace: -``` - -- [ ] **Step 3: Edit the resource table (line 490) + add asymmetry note immediately after** - -Replace the existing table: - -``` -| Resource | Auto-minted format | Namespace scope | -| -------- | ------------------ | ---------------- | -| Asset | `ASSET-NNNN` | per-organization | -| Location | `LOC-NNNN` | per-organization | -``` - -with: - -``` -| Resource | Auto-minted format | Namespace scope | -| -------- | ------------------ | ---------------- | -| Asset | `ASSET-NNNN` | per-organization | -| Location | `LOC-NNN` | per-organization | - -The location format is intentionally narrower than the asset format — locations are typically named-and-known artifacts (warehouse rooms, dock doors, zones) for which the partner-side handle already exists in facilities documentation, so auto-mint is the exception rather than the norm and a 3-digit slot suffices for the typical "ad-hoc-from-the-SPA" volume. Both formats are digit-count-agnostic on the read side: the auto-mint contract is "fixed prefix, decimal slot," not "fixed total width." Once the 3-digit space is exhausted the next mint is `LOC-1000`, then `LOC-1001`, with no migration or zero-pad reflow; the same property holds for `ASSET-NNNN` past `ASSET-9999`. Don't anchor client-side parsing on `\d{3}` or `\d{4}` strictness — match `^LOC-\d{3,}$` / `^ASSET-\d{4,}$` (or, more durably, ignore the slot count and treat the value as opaque). -``` - -- [ ] **Step 4: Edit line 509 — the bash example comment** - -Replace: - -``` -# Same shape on locations — omit external_key to receive a LOC-NNNN value -``` - -with: - -``` -# Same shape on locations — omit external_key to receive a LOC-NNN value -``` - -- [ ] **Step 5: Edit line 521 — system-of-record paragraph** - -Replace: - -``` -**When integrating with a system of record (an ERP, a WMS, a partner database, a layout / floor-plan tool), supply the partner-side handle on create** — don't rely on the auto-mint. Auto-minted `ASSET-NNNN` / `LOC-NNNN` values are per-organization-unique among live rows but they won't join cleanly to a SKU, a facility code, an ERP location, or any other handle a downstream system already uses, and they may recycle a slot vacated by a soft-deleted row (see the namespace note above) — neither property is what a partner-side audit log expects from an `id`-shaped identifier. -``` - -with: - -``` -**When integrating with a system of record (an ERP, a WMS, a partner database, a layout / floor-plan tool), supply the partner-side handle on create** — don't rely on the auto-mint. Auto-minted `ASSET-NNNN` / `LOC-NNN` values are per-organization-unique among live rows but they won't join cleanly to a SKU, a facility code, an ERP location, or any other handle a downstream system already uses, and they may recycle a slot vacated by a soft-deleted row (see the namespace note above) — neither property is what a partner-side audit log expects from an `id`-shaped identifier. -``` - -- [ ] **Step 6: Verify no stale `LOC-NNNN` strings remain in the file** - -Run: `grep -n "LOC-NNNN" docs/api/resource-identifiers.md` -Expected: no output (exit 1). - -- [ ] **Step 7: Commit** - -```bash -git add docs/api/resource-identifiers.md -git commit -m "docs(api): finalize location auto-mint format to LOC-NNN - -Aligns the resource-identifiers page with the location external_key -auto-mint format shipped server-side: LOC-NNN (3 digits, first mint -LOC-001) instead of LOC-NNNN (4 digits). Adds a paragraph after the -auto-mint table explaining the asset/location asymmetry — locations are -typically named-and-known artifacts so a 3-digit slot suffices for the -typical ad-hoc volume — and notes that both formats are digit-count- -agnostic past the natural width (LOC-1000+, ASSET-10000+) so client -parsers should match \\\\d{3,} / \\\\d{4,} rather than fixed widths." -``` - ---- - -## Task 2: Update the idempotency reference in `errors.md` - -**Files:** -- Modify: `docs/api/errors.md:327` - -- [ ] **Step 1: Edit line 327** - -Replace: - -``` -- **`POST /assets`, `POST /locations`** — retrying with the same `external_key` hits the partial unique index `(org_id, external_key) WHERE deleted_at IS NULL` and returns `409 conflict`. Detect the 409, then `GET /api/v1/{resource}?external_key=...` and read `.data[0].id` to recover the canonical `id`, then `PATCH` to reconcile. **If you omit `external_key` on a retry, you may create duplicates** — the server will mint a fresh value (`ASSET-NNNN` for assets, `LOC-NNNN` for locations) on each attempt. For retry-critical workflows, always supply an `external_key`. -``` - -with: - -``` -- **`POST /assets`, `POST /locations`** — retrying with the same `external_key` hits the partial unique index `(org_id, external_key) WHERE deleted_at IS NULL` and returns `409 conflict`. Detect the 409, then `GET /api/v1/{resource}?external_key=...` and read `.data[0].id` to recover the canonical `id`, then `PATCH` to reconcile. **If you omit `external_key` on a retry, you may create duplicates** — the server will mint a fresh value (`ASSET-NNNN` for assets, `LOC-NNN` for locations) on each attempt. For retry-critical workflows, always supply an `external_key`. -``` - -- [ ] **Step 2: Verify** - -Run: `grep -n "LOC-NNNN" docs/api/errors.md` -Expected: no output (exit 1). - -- [ ] **Step 3: Commit** - -```bash -git add docs/api/errors.md -git commit -m "docs(api): use LOC-NNN in idempotency retry-mint reference - -The auto-mint format for locations is LOC-NNN (3 digits), not LOC-NNNN. -Matches resource-identifiers.md." -``` - ---- - -## Task 3: Update the example payload in `date-fields.md` - -**Files:** -- Modify: `docs/api/date-fields.md:35` (one example), `docs/api/date-fields.md:42` (second example) - -- [ ] **Step 1: Edit line 35** - -Replace: - -``` - "external_key": "LOC-0001", -``` - -with: - -``` - "external_key": "LOC-001", -``` - -- [ ] **Step 2: Edit line 42** - -Replace: - -``` - "external_key": "LOC-0002", -``` - -with: - -``` - "external_key": "LOC-002", -``` - -- [ ] **Step 3: Verify** - -Run: `grep -n "LOC-0001\|LOC-0002" docs/api/date-fields.md` -Expected: no output (exit 1). - -- [ ] **Step 4: Commit** - -```bash -git add docs/api/date-fields.md -git commit -m "docs(api): use LOC-001/LOC-002 in date-fields outbound example - -Match the LOC-NNN (3 digit) location external_key format." -``` - ---- - -## Task 4: Add a changelog entry under `## v1.0 — Launch (TBD)` - -**Files:** -- Modify: `docs/api/changelog.md` — insert a new `###` section immediately after `## v1.0 — Launch (TBD)` (line 10) and the section's intro paragraph (line 12), so the new entry sits above the existing "Tag rows cascade-soft-delete with their parent asset or location" entry. Pre-launch wave entries are unordered narratively; placing the newest at the top of the v1.0 section matches the existing pattern (the cascade entry is itself the most recent pre-launch ship). - -- [ ] **Step 1: Read existing changelog lines 10-25 to confirm anchor for insert** - -Run: `sed -n '10,25p' docs/api/changelog.md` -Expected: line 10 is `## v1.0 — Launch (TBD)`, line 12 is the intro paragraph "Initial public API release. Stable contract for paths, field names, response shapes, and error envelopes per the [v1 stability commitment](./versioning).", line 14 is `### Tag rows cascade-soft-delete with their parent asset or location`. - -- [ ] **Step 2: Insert the new entry above the cascade entry** - -Use Edit with `old_string` containing the cascade-entry header and `new_string` containing the new entry followed by the cascade-entry header. - -`old_string`: - -``` -### Tag rows cascade-soft-delete with their parent asset or location -``` - -`new_string`: - -``` -### Location `external_key` auto-mint finalized to `LOC-NNN` - -The location auto-mint format is `LOC-NNN` (3 digits, first mint `LOC-001` in a fresh organization), parallel to but intentionally narrower than `ASSET-NNNN`. The asymmetry reflects how the two resources are used in practice: assets accumulate ad-hoc creates without a pre-known partner-side handle (a quick scan-in from the SPA, a row pasted in from a CSV with no upstream SKU), while locations are typically named-and-known artifacts already documented in a facilities export, so auto-mint is the exception rather than the norm and the smaller slot suffices for the typical volume. Pre-launch correction; the [Resource identifiers](./resource-identifiers#external_key-is-optional-on-create) page previously described the location format as `LOC-NNNN` from the prior interim plumbing — no `v1.0.0`-or-later wire baseline to break. - -- **Format and first mint.** Auto-mint produces `LOC-001` for the first omit-on-create in a fresh organization, then `LOC-002`, `LOC-003`, and so on, governed by the same "lowest unused slot among live rows" rule the [recycle paragraph on Resource identifiers](./resource-identifiers#external_key-is-optional-on-create) already documented for both resources. Soft-deleting the live row holding `LOC-005` makes that slot eligible for the next mint while soft-deleted rows can still hold the value; the partial unique index `(org_id, external_key) WHERE deleted_at IS NULL` is the system of record for "what slots are live." -- **Digit-count-agnostic past the natural width.** The `LOC-NNN` shape is the shape of the typical mint, not a width constraint on the value. Once the 3-digit space is exhausted in an organization the next mint is `LOC-1000`, then `LOC-1001`, with no migration or zero-pad reflow; the same property holds for `ASSET-NNNN` past `ASSET-9999`. Client-side parsers should match `^LOC-\d{3,}$` / `^ASSET-\d{4,}$` (or, more durably, treat the value as opaque) rather than anchoring on fixed-width digit groups. The new paragraph adjacent to the [auto-mint table](./resource-identifiers#external_key-is-optional-on-create) spells this out so a code generator emitting a strict-width validator doesn't reject a legitimately-minted `LOC-1042` after the 1000th create. -- **System-of-record guidance unchanged.** The load-bearing recommendation (supply the partner-side handle on create when integrating with an ERP / WMS / floor-plan tool — don't rely on the auto-mint) carries over verbatim; the auto-mint is the right call only for ad-hoc creates where no upstream handle yet exists. The recycle property and the namespace-shaped rather than counter-shaped wording from the [prior changelog entry on non-monotonic auto-mint](#bb52-docs--validation_error-vs-bad_request-envelope-split-non-monotonic-auto-mint-wording) both apply unchanged to the narrower location format. - -### Tag rows cascade-soft-delete with their parent asset or location -``` - -- [ ] **Step 3: Verify the entry inserted cleanly and the file still parses** - -Run: `grep -n "^### " docs/api/changelog.md | head -5` -Expected: the first `### ` line is `### Location \`external_key\` auto-mint finalized to \`LOC-NNN\``, the second is `### Tag rows cascade-soft-delete with their parent asset or location`. - -Run: `grep -c "LOC-NNNN" docs/api/changelog.md` -Expected: `1` — the single legacy mention in the BB52 paragraph at line 120 stays as-is (it is a historical reference to what the page previously said about both resources sharing the `ASSET-NNNN` / `LOC-NNNN` shape, accurate at the time the entry was written, and replacing it would rewrite history). The new entry above supersedes it for the location format. - -- [ ] **Step 4: Commit** - -```bash -git add docs/api/changelog.md -git commit -m "docs(api): changelog entry for LOC-NNN auto-mint finalization - -Documents the 3-digit location auto-mint format and the deliberate -asymmetry with ASSET-NNNN. Pre-launch correction superseding the -previously-documented LOC-NNNN format." -``` - ---- - -## Task 5: Build and link-check the docs site - -**Files:** None modified. - -- [ ] **Step 1: Run typecheck** - -Run: `pnpm typecheck` -Expected: clean exit, no TypeScript errors. - -- [ ] **Step 2: Run lint** - -Run: `pnpm lint` -Expected: clean exit (or no new findings beyond the pre-existing baseline). - -- [ ] **Step 3: Run production build** - -Run: `pnpm build` -Expected: clean exit; Docusaurus reports no broken links and no MDX compile errors. In particular the new in-page anchor `#external_key-is-optional-on-create` referenced from the changelog entry must resolve (it already exists in `resource-identifiers.md:483`). - -- [ ] **Step 4: Spot-check no stale `LOC-NNNN` or `LOC-0001`/`LOC-0002` remain anywhere in docs** - -Run: `grep -rn "LOC-NNNN\|LOC-0001\|LOC-0002" docs/` -Expected: a single hit on the BB52 historical paragraph in `docs/api/changelog.md` (preserved deliberately per Task 4 step 3); no hits anywhere else. - -- [ ] **Step 5: Final summary commit (skip if no untracked artifacts)** - -If `git status` is clean, this task is complete with no additional commit. Do not amend prior commits. - ---- - -## Task 6: Open the docs PR against `main` - -**Files:** None modified. - -- [ ] **Step 1: Push the branch** - -Run: `git push -u origin docs/tra-551-loc-auto-mint` - -- [ ] **Step 2: Open the PR with `gh pr create`** - -Run: - -```bash -gh pr create --base main --title "docs(api): finalize location external_key auto-mint to LOC-NNN" --body "$(cat <<'EOF' -## Summary - -Docs follow-on to platform PR trakrf/platform#418. The location `external_key` auto-mint format shipped as `LOC-NNN` (3 digits, first mint `LOC-001`), narrower than `ASSET-NNNN` (4 digits) by deliberate triage decision — locations are typically named-and-known artifacts (warehouse rooms, dock doors, zones) where auto-mint is the exception rather than the norm. - -- `docs/api/resource-identifiers.md` — auto-mint table updated; new paragraph after the table explains the asset/location asymmetry rationale and the digit-count-agnostic property of both formats (past the natural width — `LOC-1000+`, `ASSET-10000+` — the pattern continues without zero-pad reflow, so client parsers should match `^LOC-\d{3,}$` / `^ASSET-\d{4,}$` rather than fixed-width groups). -- `docs/api/errors.md` — the retry-mint reference in the `POST /assets`, `POST /locations` idempotency bullet now reads `LOC-NNN`. -- `docs/api/date-fields.md` — outbound example payload uses `LOC-001` / `LOC-002`. -- `docs/api/changelog.md` — new entry under `## v1.0 — Launch (TBD)` documenting the finalization. No Linear identifiers in user-facing copy. - -The BB52 historical paragraph in the changelog that mentions the previously-documented `LOC-NNNN` shape is preserved as-written — replacing it would rewrite history. The new entry above supersedes it for the current format. - -## Test plan - -- [x] `pnpm typecheck` — clean -- [x] `pnpm lint` — clean -- [x] `pnpm build` — clean, no broken links, MDX compiles -- [ ] Visual spot-check of the rendered auto-mint section once the preview deploy is up — confirm the new paragraph reads cleanly after the table and the in-page anchor from the changelog entry resolves -- [ ] Merge gated on platform PR #418 reaching preview (per project convention "docs ship behind preview, not prod") - -🤖 Generated with [Claude Code](https://claude.com/claude-code) -EOF -)" -``` - -- [ ] **Step 3: Note the PR URL for the user** - -Capture stdout from the `gh pr create` invocation and surface the URL in the session summary. diff --git a/superpowers/plans/2026-05-28-tra-848-oauth-auth-docs-cutover.md b/superpowers/plans/2026-05-28-tra-848-oauth-auth-docs-cutover.md deleted file mode 100644 index b0718d2..0000000 --- a/superpowers/plans/2026-05-28-tra-848-oauth-auth-docs-cutover.md +++ /dev/null @@ -1,632 +0,0 @@ -# TRA-848 — OAuth2 Auth Docs Cutover Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Rewrite the integrator auth docs from the legacy "API key is a JWT, used directly as Bearer" model to the OAuth2 `client_credentials` flow (mint `client_id`/opaque `client_secret` → exchange at `/oauth/token` → short-lived access-token Bearer → refresh). - -**Architecture:** `docs/api/authentication.md` is rewritten as the canonical reference. The two onboarding walkthroughs are updated where they bootstrap auth. Incidental curl examples on other pages get a mechanical env-var rename so we never teach hardcoding a 15-minute token. Key-management endpoints stay internal (UI-only minting). A changelog entry records the change. - -**Tech Stack:** Docusaurus (Markdown/MDX), pnpm. No app code. "Tests" = `pnpm build` / `pnpm typecheck` / `pnpm lint`, grep guards for forbidden strings, and live curl probes against preview. - -**Reference:** Design spec at `superpowers/specs/2026-05-28-tra-848-oauth-auth-docs-cutover-design.md`. Verified contract values are reproduced in each task below. - -**Working directory:** worktree `/home/mike/trakrf-docs/.claude/worktrees/tra-848-oauth-auth-cutover` on branch `worktree-tra-848-oauth-auth-cutover`. - ---- - -## Verified contract (single source of truth for all tasks) - -- **Mint** (app UI → Account menu → API Keys → New key) returns ONCE: - - `client_id` = the key's `jti` (a UUID), e.g. `6f1c2a8e-7d3b-4e90-9a11-2c4d5e6f7a8b` - - `client_secret` = opaque `trakrf_` + 64 hex chars, e.g. `trakrf_9f8e7d6c5b4a39281706f5e4d3c2b1a0ffeeddccbbaa99887766554433221100`; SHA-256 hashed server-side; shown once. -- **Exchange** `POST /api/v1/oauth/token` (JSON in, `error` envelope out): - - `client_credentials`: body `{grant_type, client_id, client_secret}` → `{access_token, refresh_token, token_type:"Bearer", expires_in:900}` - - `refresh_token`: body `{grant_type, refresh_token}` → same shape, rotated -- `access_token` = short-lived JWT (15 min). `refresh_token` = opaque 64-hex (30 days), single-use; replay → 401 + revokes the chain. -- Status codes on `/oauth/token`: 200 / 400 (validation, unsupported `grant_type`) / 401 (invalid creds or refresh token). -- Example env var going forward: `$TRAKRF_ACCESS_TOKEN` (a short-lived access token), replacing `$TRAKRF_API_KEY`. - ---- - -## Baseline (do first) - -- [ ] **Step B1: Install deps in the worktree** - -Run: `cd /home/mike/trakrf-docs/.claude/worktrees/tra-848-oauth-auth-cutover && pnpm install` -Expected: completes; `node_modules/` present. - -- [ ] **Step B2: Confirm a clean build baseline before edits** - -Run: `pnpm build` -Expected: build succeeds. If it fails on `main` before any edits, STOP and report — do not attribute a pre-existing failure to this work. - -- [ ] **Step B3: Verify the live contract on preview (so examples match reality)** - -```bash -# Empty body → 400 -curl -s -o /dev/null -w "%{http_code}\n" -X POST https://app.preview.trakrf.id/api/v1/oauth/token -H "Content-Type: application/json" -d '{}' -# Bad client_credentials → 401 invalid client credentials -curl -s -X POST https://app.preview.trakrf.id/api/v1/oauth/token -H "Content-Type: application/json" \ - -d '{"grant_type":"client_credentials","client_id":"nope","client_secret":"nope"}' -``` -Expected: `400`, then a 401 `error` envelope with `"detail":"Invalid client credentials"`. Confirms the endpoint and shapes before writing examples. - ---- - -## Task 1: Rewrite `docs/api/authentication.md` (canonical) - -**Files:** -- Modify (full rewrite): `docs/api/authentication.md` - -- [ ] **Step 1.1: Replace the entire file with the new content below** - -Write `docs/api/authentication.md` to exactly: - -````markdown ---- -sidebar_position: 1 ---- - -# Authentication - -The TrakRF API uses the **OAuth2 `client_credentials`** flow. You mint a long-lived `client_id` + `client_secret` pair once from the web app, exchange that pair for a short-lived **access token** at `POST /api/v1/oauth/token`, and send the access token as an `Authorization: Bearer` header on every request. Access tokens are JSON Web Tokens (JWTs) scoped to a single organization and a set of permissions. - -## How authentication works - -1. **Mint credentials** in the app — you receive a `client_id` and an opaque `client_secret`. This pair is long-lived; store it in a secrets manager. -2. **Exchange** the pair for an access token at `POST /api/v1/oauth/token` (`grant_type=client_credentials`). -3. **Call the API** with `Authorization: Bearer `. -4. **Refresh** before the access token expires (`grant_type=refresh_token`), or just request a new pair-exchange. - -The `client_id`/`client_secret` are your durable credential; access tokens are short-lived (15 minutes) and disposable. - -## Mint your first API key {#mint-your-first-api-key} - -API credentials are minted from the SPA's **Account menu → API Keys → New Key**. The `client_secret` is shown once at creation — save it immediately. - -Minting happens in the web app, not via the API. Programmatic key issuance is intentionally not exposed on the public surface: possession of a key-minting API would defeat the trust boundary, since any compromised credential could mint a more-privileged one. The session-authenticated mint flow keeps issuance tied to a signed-in user, consistent with how Stripe and similar services gate dashboard-only credential issuance. - -If you have a use case that genuinely requires programmatic provisioning (per-tenant SaaS automation that mints a credential per customer org), [contact us](mailto:support@trakrf.id) — we'll evaluate exposing it. - -1. Sign in (production: [app.trakrf.id](https://app.trakrf.id); preview: [app.preview.trakrf.id](https://app.preview.trakrf.id)). Both hosts run the same UI and flow — use the one that matches your account. See [Base URL](#base-url) for the matching API host. -2. Open the **Account menu** in the top-right corner and choose **API Keys**. (The left-nav **Settings** page is for device configuration — signal power, session, worker log level — not key management.) -3. **If your account belongs to multiple organizations,** credentials are scoped to whichever organization is currently selected in the Account menu. Check the organization switcher before clicking **New key** — a credential minted under the wrong organization cannot be reassigned. -4. Click **New key**. Give it a descriptive name (e.g. `"prod-integration"` or `"local-dev"`) and pick the scopes the integration needs — only the scopes required for the endpoints you'll call. See the [Scopes](#scopes) table below. -5. Submit. The response shows a `client_id` and a `client_secret` **once**: - - `client_id` — a stable UUID identifying the credential (e.g. `6f1c2a8e-7d3b-4e90-9a11-2c4d5e6f7a8b`). - - `client_secret` — an opaque secret of the form `trakrf_` followed by 64 hex characters. It is stored only as a hash and **cannot be shown again**. Copy it to your secrets store immediately. -6. Exchange the pair for an access token (next section) and use that token as `Authorization: Bearer `. - -:::tip Misminted scopes? Revoke and re-mint -Scopes are fixed at creation and cannot be edited afterward — there is no "edit key" flow. If you mint a credential with the wrong scopes (or against the wrong organization, or with the wrong expiration), revoke it from the same **Account menu → API Keys** view and mint a fresh one. The old and new credentials are both valid until you revoke, so the cutover is non-disruptive: mint the new credential, swap it into your secrets store, then revoke the old one. -::: - -## Get an access token - -Exchange your `client_id` + `client_secret` for an access token at `POST /api/v1/oauth/token` with `grant_type=client_credentials`. The request and response are JSON; set `$BASE_URL` per [Base URL](#base-url). - -```bash -curl -X POST "$BASE_URL/api/v1/oauth/token" \ - -H "Content-Type: application/json" \ - -d '{ - "grant_type": "client_credentials", - "client_id": "6f1c2a8e-7d3b-4e90-9a11-2c4d5e6f7a8b", - "client_secret": "trakrf_9f8e7d6c5b4a39281706f5e4d3c2b1a0ffeeddccbbaa99887766554433221100" - }' -``` - -A successful exchange returns: - -```json -{ - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", - "refresh_token": "f3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "token_type": "Bearer", - "expires_in": 900 -} -``` - -- `access_token` — a short-lived JWT (15 minutes). Send it as `Authorization: Bearer `. -- `refresh_token` — an opaque, single-use token (30-day lifetime) used to obtain the next access token without re-sending your `client_secret`. See [Refresh an access token](#refresh-an-access-token). -- `expires_in` — access-token lifetime in seconds (`900` = 15 minutes). - -A `400` means the body failed validation or `grant_type` was unsupported; a `401` (`detail: "Invalid client credentials"`) means the `client_id`/`client_secret` pair did not verify. Errors use the standard [error envelope](./errors). - -## Request header - -Every authenticated request must include the **access token** as a Bearer token in the `Authorization` header: - -``` -Authorization: Bearer -``` - -The header name is `Authorization`; the scheme is `Bearer`. The access-token JWT directly follows the scheme with a single space separator. Do **not** send your `client_secret` here — only the access token returned from `/oauth/token`. - -:::caution `X-API-Key` is not accepted -The server only honors the `Authorization: Bearer` form. Sending the token as `X-API-Key: ` (or any other header) returns `401 unauthorized` with detail `"Use Authorization: Bearer "`. If you see that detail, check the header name and scheme. -::: - -### 401 response detail strings {#unauthorized-detail-strings} - -The 401 envelope carries one of these `error.detail` strings depending on the failure mode. All return `error.type: "unauthorized"` and the standard `WWW-Authenticate: Bearer realm="trakrf-api"` header per [RFC 7235](https://datatracker.ietf.org/doc/html/rfc7235): - -| Failure mode | `error.detail` | -| ------------------------------------------- | ------------------------------------- | -| Missing `Authorization` header | `"Missing authorization header"` | -| Malformed bearer or invalid / expired token | `"Invalid or expired token"` | -| Wrong scheme (e.g. `X-API-Key: `) | `"Use Authorization: Bearer "` | - -Branch on `error.type` for the canonical signal — that's the field that locks to a stable contract. The `detail` strings are accurate diagnostic prose suitable for logging, but they are not part of the response contract and may evolve in wording. If your integration classifies 401s for routing or retry, key on `type` and treat `detail` as human-readable context. - -An expired access token returns `401` with `detail: "Invalid or expired token"` — that's the signal to refresh (see below) and retry. - -## Refresh an access token {#refresh-an-access-token} - -Before the 15-minute access token expires, exchange your `refresh_token` for a fresh pair — no `client_secret` required: - -```bash -curl -X POST "$BASE_URL/api/v1/oauth/token" \ - -H "Content-Type: application/json" \ - -d '{ - "grant_type": "refresh_token", - "refresh_token": "f3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - }' -``` - -The response shape is identical to the `client_credentials` exchange: a new `access_token` **and a new `refresh_token`**. Refresh tokens are **single-use** — each refresh rotates the token, so store the new `refresh_token` and discard the old one. Presenting an already-used refresh token returns `401` and **revokes the entire token chain** as a replay-detection measure; if that happens, start over from a `client_credentials` exchange. - -## Security properties - -- **Short-lived access tokens.** Access tokens live 15 minutes, limiting the blast radius of a leaked token. -- **Opaque, hashed secret.** The `client_secret` is a high-entropy opaque value stored only as a SHA-256 hash and shown exactly once. TrakRF cannot recover it; if lost, revoke and re-mint. -- **Single-use refresh rotation.** Every refresh issues a new `refresh_token`; replaying an old one is treated as a compromise signal and revokes the chain. -- **Scope-limited.** Each credential carries only the scopes selected at mint time (see [Scopes](#scopes)); the access token inherits them. -- **Store credentials in a secrets manager** — never in source control. Treat the `client_secret` like a password and the `access_token` like a session token. - -## Scopes - -Each credential is issued with one or more scopes. The API rejects requests whose access token lacks the scope required by the endpoint (`403 forbidden` with `"Missing required scope: "`). The scopes are fixed on the credential at mint time and carried into every access token minted from it. - -### UI labels vs scope strings {#ui-labels} - -The **New key** form in the web app lets you pick a resource (Assets / Locations / Tracking) and an access level (None / Read / Read + Write). Each combination maps to one or two of the scope strings used throughout these docs and in API responses. - -| UI form (resource × level) | Scopes granted | -| -------------------------- | ----------------------------------- | -| Assets → Read | `assets:read` | -| Assets → Read + Write | `assets:read`, `assets:write` | -| Locations → Read | `locations:read` | -| Locations → Read + Write | `locations:read`, `locations:write` | -| Tracking → Read | `tracking:read` | - -Selecting **None** for a resource grants no scope for that resource. Selecting **Read + Write** always grants both the read and the write scope — there is no write-only level today. - -The table below is the human-readable summary; the machine-readable canonical source is the [`x-required-scopes` extension](#x-required-scopes-on-operations) on each operation in the OpenAPI spec. The runtime enforces what the spec declares, and codegen ingestors should read the extension directly. If the table ever drifts from the extension, the spec wins. - -| Scope | Access | Endpoints (representative) | -| ----------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `assets:read` | Read | `GET /assets`, `GET /assets/{asset_id}` | -| `assets:write` | Write | `POST /assets`, `PATCH /assets/{asset_id}`, `POST /assets/{asset_id}/rename`, `DELETE /assets/{asset_id}`, `POST /assets/{asset_id}/tags`, `DELETE /assets/{asset_id}/tags/{tag_id}` | -| `locations:read` | Read | `GET /locations`, `GET /locations/{location_id}` | -| `locations:write` | Write | `POST /locations`, `PATCH /locations/{location_id}`, `POST /locations/{location_id}/rename`, `DELETE /locations/{location_id}`, `POST /locations/{location_id}/tags`, `DELETE /locations/{location_id}/tags/{tag_id}` | -| `tracking:read` | Read | `GET /reports/asset-locations`, `GET /assets/{asset_id}/history` | - -`tracking:read` gates the two endpoints that answer "where are things — now and over time." It covers both the time-series history feed (`/assets/{asset_id}/history`) and the current-state snapshot report (`/reports/asset-locations`), because both views are derived from the same underlying scan-event stream. The name reflects that data lineage: it's permission to read tracking data, not just historical data. - -A few non-obvious pairings worth calling out: - -- **Tag subresource operations** inherit their parent resource's `:write` scope. Attaching or detaching a tag on an asset (`POST /api/v1/assets/{asset_id}/tags`, `DELETE /api/v1/assets/{asset_id}/tags/{tag_id}`) requires `assets:write`; the location-side equivalents require `locations:write`. There is no separate `tags:write` scope — granting it would split the write authority for a single resource across two scope strings, which the platform deliberately avoids. -- **`/reports/asset-locations`** is gated by **`tracking:read`**, not `locations:read` (and not `assets:read`). The endpoint's URL says "reports," the response rows are asset-at-location pairs, but the scope follows the **data lineage**: every field on every row is derived from the scan-event stream — `asset_last_seen` is the timestamp of the most recent scan event for that asset, and the `location_id` / `location_external_key` on the row is the location of that scan. Granting `assets:read` or `locations:read` does **not** unlock this endpoint; you need `tracking:read`. -- **`/assets/{asset_id}/history`** is gated by **`tracking:read`** for the same reason — it's a projection of scan events, not a property of the asset. - -Additional scopes may be added in any v1 release. Clients should tolerate unknown scope strings without breaking (see [Versioning → Open enums](./versioning#open-extensible-enums-in-v1)). - -### `x-required-scopes` on operations - -Every operation in the public spec carries an `x-required-scopes` extension listing the scope strings the endpoint requires (e.g. `x-required-scopes: [assets:write]` on `POST /api/v1/assets`). An empty array — `x-required-scopes: []` — means "any authenticated token works, no scope required" and currently appears only on `GET /api/v1/orgs/me`, the lightweight health-check endpoint integrators hit to confirm a token is live. The extension is present on every operation precisely so the absence of a required scope is a positive signal (empty array) rather than a missing-field ambiguity. - -This is the canonical machine-readable scope source. Codegen ingestors, policy tooling, and scope-aware partners minting minimal-scope credentials should read this extension rather than parsing the **Required scope:** marker in each operation's prose description. The OpenAPI spec's `BearerAuth` scheme (HTTP Bearer, JWT format) can't express scope-per-operation by itself; the extension fills that gap. - -The table above and the **Required scope:** markers in operation descriptions are the canonical reference for human readers. Both views are auto-derived from the same server-side annotations at spec-publish time and must stay in sync — any drift between prose and extension is a spec-generation bug, not a documentation choice. - -### Internal scope: `keys:admin` {#internal-scopes} - -A sixth scope, `keys:admin`, exists in the platform but does not appear in the public spec or the **New Key** picker. It gates the SPA-side key administration surface (mint, list, revoke). The scope is granted implicitly to authenticated session JWTs inside the web app; it is not selectable when minting an API credential and is not required by any documented public endpoint. Integrators do not need to request, hold, or branch on this scope. It is documented here only so the five-row table above isn't read as the platform's complete scope list. - -## Example requests - -Examples use `$BASE_URL` — set it to `https://app.trakrf.id` for production or `https://app.preview.trakrf.id` for preview accounts. See [Base URL](#base-url). They assume `$TRAKRF_ACCESS_TOKEN` holds a current access token from [Get an access token](#get-an-access-token). - -### curl - -```bash -curl -H "Authorization: Bearer $TRAKRF_ACCESS_TOKEN" \ - "$BASE_URL/api/v1/assets" -``` - -### Python (requests) - -```python -import os -import requests - -base_url = os.environ["TRAKRF_BASE_URL"] -headers = {"Authorization": f"Bearer {os.environ['TRAKRF_ACCESS_TOKEN']}"} -response = requests.get(f"{base_url}/api/v1/assets", headers=headers) -response.raise_for_status() -print(response.json()) -``` - -### JavaScript (fetch) - -```javascript -const baseUrl = process.env.TRAKRF_BASE_URL; -const res = await fetch(`${baseUrl}/api/v1/assets`, { - headers: { Authorization: `Bearer ${process.env.TRAKRF_ACCESS_TOKEN}` }, -}); -if (!res.ok) throw new Error(`API error: ${res.status}`); -const data = await res.json(); -``` - -## Credential lifecycle - -All lifecycle actions — creation, listing, rotation, revocation — happen in the SPA. There is no programmatic API for credential management; see [Mint your first API key](#mint-your-first-api-key) for the design rationale. - -- **Creation:** credentials are minted by an organization administrator. The `client_secret` is shown **once** at creation time — copy it immediately to your secrets store. -- **Listing:** a credential's `client_id` and metadata (name, scopes, created / last-used timestamps) remain visible to administrators; the `client_secret` is never shown again. -- **Rotation:** create a new credential, update your integration, then revoke the old one. TrakRF does not support in-place secret rotation; create-new-revoke-old keeps both credentials valid during the cutover. -- **Revocation:** an administrator can revoke a credential at any time. Revoked credentials fail the `client_credentials` exchange (`401`), and any outstanding refresh-token chains are invalidated. -- **Expiration:** credentials do not expire by default — leaving the field blank (the **Never** option in the SPA picker) mints a permanent credential. Picking an explicit expiration sets an expiry after which the `client_credentials` exchange returns `401`. For any credential beyond a throwaway local-dev one, set an explicit expiration (e.g. 90 days) and schedule the rotation. - -### Listing and revocation are SPA-side {#listing-revocation-spa-side} - -Listing existing credentials, viewing metadata (name, scopes, created / last-used / expires timestamps), and revoking a credential are browser affordances in the SPA's **Account menu → API Keys** view, not public API endpoints. Any rotation workflow that needs to enumerate or revoke prior credentials has to drive the SPA flow (manual or scripted via a session login), or maintain its own out-of-band record of which credential handles map to which integrations. If you have a use case that genuinely requires programmatic listing or revocation, [contact us](mailto:support@trakrf.id). - -## Base URL - -- **Production:** `https://app.trakrf.id` -- **Preview (per-PR test deploys):** `https://app.preview.trakrf.id` - -All API endpoints live under the `/api/v1/` prefix. The interactive reference at [`/api`](/api) lists the complete endpoint catalog. Shell examples in these docs use a `$BASE_URL` env var so the same commands work against either environment: - -```bash -# Production -export BASE_URL=https://app.trakrf.id - -# Preview -export BASE_URL=https://app.preview.trakrf.id -``` - -Preview-scoped credentials will not authenticate against production and vice versa — make sure `BASE_URL` matches the environment your credential was minted on. - -**The API and the SPA share the same origin.** `$BASE_URL` resolves to the application host that also serves the web app — `app.trakrf.id` is both the SPA URL and the API host, and the OpenAPI spec's `servers[]` entries match these hosts one-for-one. Generated clients can point at `servers[0]` directly; the `/api/v1/` prefix is the only path differentiation between SPA routes and API routes. - -## Server-to-server design {#server-to-server} - -The TrakRF API is intended for **server-to-server** integration. Responses do not include `Access-Control-Allow-Origin` headers, so browser-based JavaScript calls are blocked by the browser's CORS policy. Call the API from a backend service and never ship credentials or access tokens in client-side code — the CORS block is also a guardrail against leaking credentials to end-user devices. - -**Session JWTs are also accepted** on most public endpoints (same `Authorization: Bearer ` form), because the web app and the API share a router. A session JWT is effectively unscoped for its short lifetime and is only convenient for ad-hoc UI-driven requests; integrators should use the `client_credentials` flow so that auth is durable and scope-limited. (One exception: `/orgs/me` accepts API-credential access tokens only — see [Private endpoints → Response shape: `/orgs/me`](./private-endpoints#orgs-me).) - -## Environment variables - -Store your `client_id`/`client_secret` in a secrets manager — **never in source control**. The access token is short-lived; obtain it at runtime via the `client_credentials` exchange rather than committing it. The examples on this site assume `$TRAKRF_ACCESS_TOKEN` holds a current access token: - -```bash -# Obtain an access token (see "Get an access token") and capture it: -export TRAKRF_ACCESS_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." -``` - -Because access tokens expire after 15 minutes, scripts that run longer should re-exchange (or refresh) rather than relying on a stale exported value. - -## Testing connectivity - -Once you have an access token, verify it with curl: - -```bash -curl -i -H "Authorization: Bearer $TRAKRF_ACCESS_TOKEN" \ - "$BASE_URL/api/v1/assets?limit=1" -``` - -A `200 OK` with a JSON body confirms the token and scope are correct. A `401 unauthorized` indicates a missing, malformed, or expired token (re-exchange and retry); `403 forbidden` indicates the credential lacks the scope required for that endpoint. -```` - -- [ ] **Step 1.2: Guard against leftover legacy framing** - -Run: -```bash -grep -nE "API key.*is a JWT|your-api-key-jwt|use (it|the returned JWT) as|TRAKRF_API_KEY" docs/api/authentication.md -``` -Expected: no matches. If any match, fix the prose before continuing. - -- [ ] **Step 1.3: Build + lint** - -Run: `pnpm build && pnpm lint` -Expected: both pass. - -- [ ] **Step 1.4: Commit** - -```bash -git add docs/api/authentication.md -git commit -m "docs(api): rewrite authentication for OAuth2 client_credentials flow (TRA-848)" -``` - ---- - -## Task 2: Update `docs/api/quickstart.mdx` auth bootstrap - -**Files:** -- Modify: `docs/api/quickstart.mdx` (auth-bootstrap prose + all `$TRAKRF_API_KEY` example occurrences) - -- [ ] **Step 2.1: Update the intro TL;DR (line ~20)** - -Replace: -``` -If you're already familiar with API-key-authenticated REST APIs, the TL;DR is: send your key as `Authorization: Bearer ` and hit `$BASE_URL/api/v1/...`. The full walkthrough follows. -``` -With: -``` -If you're already familiar with OAuth2 `client_credentials`, the TL;DR is: POST your `client_id`/`client_secret` to `$BASE_URL/api/v1/oauth/token`, then send the returned `access_token` as `Authorization: Bearer ` to `$BASE_URL/api/v1/...`. The full walkthrough follows. -``` - -- [ ] **Step 2.2: Rewrite §2 "Verify your key works" credential setup (lines ~34–53)** - -Replace the section from the `## 2. Verify your key works` heading through the first `curl ... /orgs/me` block (the lines covering the `:::note Browser required` admonition, the `export TRAKRF_API_KEY=...` block, and the intro sentence before the curl) with: - -````markdown -## 2. Verify your credentials work - -If you don't already have credentials, sign in to the app, open the **Account menu → API Keys → New Key**, name the key, pick scopes, and submit. The `client_id` and `client_secret` are shown once at creation — copy the `client_secret` immediately; it cannot be shown again. Full walkthrough: [Authentication → Mint your first API key](./authentication#mint-your-first-api-key). - -:::note Browser required to mint credentials -Minting credentials requires an interactive browser session — there is no programmatic mint endpoint by design. CI/headless setups should mint credentials out-of-band, store the `client_id`/`client_secret` in a secret store (env var, vault, GitHub Actions secret), and reference them from there. See [Authentication → Mint your first API key](./authentication#mint-your-first-api-key) for the rationale and a contact path if your use case genuinely needs programmatic provisioning. -::: - -Exchange your credentials for a short-lived access token, and capture it for the rest of this page: - -```bash -export TRAKRF_ACCESS_TOKEN=$(curl -s -X POST "$BASE_URL/api/v1/oauth/token" \ - -H "Content-Type: application/json" \ - -d '{ - "grant_type": "client_credentials", - "client_id": "6f1c2a8e-7d3b-4e90-9a11-2c4d5e6f7a8b", - "client_secret": "trakrf_9f8e7d6c5b4a39281706f5e4d3c2b1a0ffeeddccbbaa99887766554433221100" - }' | python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])") -# Or, if jq is installed: ... | jq -r '.access_token' -``` - -The access token lives 15 minutes; re-run this exchange (or use the `refresh_token`) when it expires. Full detail: [Authentication → Get an access token](./authentication#get-an-access-token). - -`GET /api/v1/orgs/me` (`getCurrentOrg` in generated clients) returns the organization your credentials are scoped to. It's the canonical "tell me about myself" endpoint, requires no specific scope, and confirms end-to-end that your access token authenticates against the right environment. **Use an API-credential access token here — `/orgs/me` rejects session JWTs from the web app, even though most other public endpoints accept them.** See [Private endpoints → Response shape: `/orgs/me`](./private-endpoints#orgs-me) for the precise rule. -```` - -- [ ] **Step 2.3: Update the §2 troubleshooting bullet about `X-API-Key` (line ~79)** - -Replace `the JWT was sent under `X-API-Key`` wording so it reads "the access token was sent under `X-API-Key`". Exact replace: -``` -- `401 unauthorized` with `detail: "Use Authorization: Bearer "` — the JWT was sent under `X-API-Key` (or another header). The server only accepts the `Authorization: Bearer` form, despite the credential being called an "API key." See [Authentication → Request header](./authentication#request-header). -``` -With: -``` -- `401 unauthorized` with `detail: "Use Authorization: Bearer "` — the token was sent under `X-API-Key` (or another header). The server only accepts the `Authorization: Bearer` form. See [Authentication → Request header](./authentication#request-header). -``` - -- [ ] **Step 2.4: Swap all remaining `$TRAKRF_API_KEY` → `$TRAKRF_ACCESS_TOKEN` in this file** - -Run: `sed -i 's/\$TRAKRF_API_KEY/$TRAKRF_ACCESS_TOKEN/g' docs/api/quickstart.mdx` -Then verify: `grep -n "TRAKRF_API_KEY" docs/api/quickstart.mdx` → expected: no matches. - -- [ ] **Step 2.5: Update §4 Postman var description (line ~169)** - -Replace `- \`bearerToken\` → the JWT from step 2` with `- \`bearerToken\` → the \`access_token\` from step 2 (refresh it when it expires)`. - -- [ ] **Step 2.6: Update §5 codegen credential sentence (line ~187)** - -Replace `pass the API key minted in step 2 wherever the generated client expects an access token.` with `pass the access token obtained in step 2 wherever the generated client expects an access token; refresh it via the \`client_credentials\` / \`refresh_token\` exchange as needed.` - -- [ ] **Step 2.7: Build, guard, commit** - -Run: -```bash -pnpm build && grep -n "TRAKRF_API_KEY" docs/api/quickstart.mdx -``` -Expected: build passes; no `TRAKRF_API_KEY` matches. -```bash -git add docs/api/quickstart.mdx -git commit -m "docs(api): update quickstart for OAuth2 token exchange (TRA-848)" -``` - ---- - -## Task 3: Update `docs/getting-started/api.mdx` auth bootstrap - -**Files:** -- Modify: `docs/getting-started/api.mdx` - -- [ ] **Step 3.1: Update "What you'll need" credential bullet (line ~18)** - -Replace: -``` -- A TrakRF account and an API key. Sign in (or sign up) at the app, then mint a key from the **Account menu → API Keys → New Key** ([detail](../api/authentication#mint-your-first-api-key)). The token is shown once at creation — copy it immediately. -``` -With: -``` -- A TrakRF account and API credentials. Sign in (or sign up) at the app, then mint a `client_id`/`client_secret` from the **Account menu → API Keys → New Key** ([detail](../api/authentication#mint-your-first-api-key)). The `client_secret` is shown once at creation — copy it immediately. -``` - -- [ ] **Step 3.2: Rewrite §2 "Make your first call" credential setup (lines ~36–47)** - -Replace from `Save your API key to an environment variable for the rest of this page:` through the `export TRAKRF_API_KEY=...` block and into the `/orgs/me` intro sentence with: - -````markdown -Exchange your credentials for a short-lived access token and save it for the rest of this page: - -```bash -export TRAKRF_ACCESS_TOKEN=$(curl -s -X POST "$BASE_URL/api/v1/oauth/token" \ - -H "Content-Type: application/json" \ - -d '{ - "grant_type": "client_credentials", - "client_id": "6f1c2a8e-7d3b-4e90-9a11-2c4d5e6f7a8b", - "client_secret": "trakrf_9f8e7d6c5b4a39281706f5e4d3c2b1a0ffeeddccbbaa99887766554433221100" - }' | python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])") -# Or, if jq is installed: ... | jq -r '.access_token' -``` - -Access tokens live 15 minutes; re-exchange when one expires. Full detail: [Authentication → Get an access token](../api/authentication#get-an-access-token). - -The `/api/v1/orgs/me` endpoint returns the organization your credentials are scoped to. It's the canonical "tell me about myself" probe — requires no specific scope, depends on no prior data, and confirms end-to-end that your access token authenticates against the right environment. **Use an API-credential access token here — `/orgs/me` rejects session JWTs from the web app, even though most other public endpoints accept them.** See [Private endpoints → Response shape: `/orgs/me`](../api/private-endpoints#orgs-me) for the precise rule. -```` - -- [ ] **Step 3.3: Swap remaining `$TRAKRF_API_KEY` → `$TRAKRF_ACCESS_TOKEN`** - -Run: `sed -i 's/\$TRAKRF_API_KEY/$TRAKRF_ACCESS_TOKEN/g' docs/getting-started/api.mdx` -Verify: `grep -n "TRAKRF_API_KEY" docs/getting-started/api.mdx` → no matches. - -- [ ] **Step 3.4: Build, guard, commit** - -Run: `pnpm build && grep -n "TRAKRF_API_KEY" docs/getting-started/api.mdx` (expect: pass, no matches) -```bash -git add docs/getting-started/api.mdx -git commit -m "docs(getting-started): update API front-door for OAuth2 token exchange (TRA-848)" -``` - ---- - -## Task 4: Mechanical sweep of incidental curl examples - -These pages only reference `$TRAKRF_API_KEY` inside `Authorization: Bearer` curl examples — no auth prose to change. - -**Files:** -- Modify: `docs/api/http-method-coverage.md` (2), `docs/api/pagination-filtering-sorting.md` (13), `docs/api/data-model.md` (1), `docs/api/date-fields.md` (2), `docs/api/resource-identifiers.md` (21) - -- [ ] **Step 4.1: Swap the env var across all five files** - -```bash -sed -i 's/\$TRAKRF_API_KEY/$TRAKRF_ACCESS_TOKEN/g' \ - docs/api/http-method-coverage.md \ - docs/api/pagination-filtering-sorting.md \ - docs/api/data-model.md \ - docs/api/date-fields.md \ - docs/api/resource-identifiers.md -``` - -- [ ] **Step 4.2: Verify none remain anywhere under docs/** - -Run: `grep -rn "TRAKRF_API_KEY" docs/` -Expected: no matches (authentication.md, quickstart, getting-started already handled in Tasks 1–3). - -- [ ] **Step 4.3: Build + commit** - -Run: `pnpm build` (expect pass) -```bash -git add docs/api/http-method-coverage.md docs/api/pagination-filtering-sorting.md docs/api/data-model.md docs/api/date-fields.md docs/api/resource-identifiers.md -git commit -m "docs(api): use short-lived access-token env var in examples (TRA-848)" -``` - ---- - -## Task 5: Reconcile `docs/api/private-endpoints.md` and `docs/api/postman.mdx` prose - -**Files:** -- Modify: `docs/api/private-endpoints.md` (Programmatic access section) -- Modify: `docs/api/postman.mdx` (token wording) - -- [ ] **Step 5.1: Update private-endpoints "Programmatic access" (lines ~13–17)** - -Replace: -``` -For server-to-server or scripted integrations, the supported credential is an **API key** issued via the in-app **Account menu → API Keys** flow (see [Authentication](./authentication)). Session JWTs minted by `POST /api/v1/auth/login` exist to keep the first-party SPA logged in and may change without notice — they are not a public auth path. -``` -With: -``` -For server-to-server or scripted integrations, mint a `client_id`/`client_secret` pair via the in-app **Account menu → API Keys** flow and exchange it for a short-lived access token at `POST /api/v1/oauth/token` (see [Authentication](./authentication)). Session JWTs minted by `POST /api/v1/auth/login` exist to keep the first-party SPA logged in and may change without notice — they are not a public auth path. -``` - -- [ ] **Step 5.2: Verify the `/orgs/me` JWT references still read correctly** - -The `/orgs/me` section refers to "the presented bearer" and "the JWT's `sub` claim" — these remain accurate because the access token IS a JWT. No change needed. Confirm by reading lines 50–63; do not edit unless a reference is now wrong. - -- [ ] **Step 5.3: Update postman.mdx token wording** - -Run: `grep -n "JWT\|API key\|bearerToken\|api key" docs/api/postman.mdx` -For each hit where the Postman `bearerToken` variable or "API key" is described as the credential sent on requests, adjust the prose so the bearer value is the **access token** obtained from `/oauth/token` (not the minted key directly). Keep spec-import and collection-variable mechanics intact. Make the minimal prose edits that remove the "the JWT/key is your bearer" implication. - -- [ ] **Step 5.4: Build + commit** - -Run: `pnpm build` (expect pass) -```bash -git add docs/api/private-endpoints.md docs/api/postman.mdx -git commit -m "docs(api): reconcile programmatic-access + Postman prose to token exchange (TRA-848)" -``` - ---- - -## Task 6: Add a changelog entry - -**Files:** -- Modify: `docs/api/changelog.md` (prepend a new entry; do NOT rewrite past entries) - -- [ ] **Step 6.1: Read the file to match its existing entry format** - -Run: `sed -n '1,40p' docs/api/changelog.md` and mirror the heading/date style already in use. - -- [ ] **Step 6.2: Add a top entry describing the auth model change** - -Content (adapt heading style to match existing entries): -```markdown -## 2026-05-28 — OAuth2 `client_credentials` authentication - -API authentication moved from "use your API key JWT directly as a Bearer token" to the OAuth2 `client_credentials` flow: - -- Minting an API key now returns a `client_id` + opaque `client_secret` pair (shown once) instead of a JWT. -- Exchange the pair at `POST /api/v1/oauth/token` for a short-lived access token (15 min) plus a 30-day single-use refresh token. -- Send the **access token** as `Authorization: Bearer`. A long-lived key JWT is no longer accepted as a Bearer credential. - -See [Authentication](./authentication) for the full flow. -``` - -- [ ] **Step 6.3: Build + commit** - -Run: `pnpm build` (expect pass) -```bash -git add docs/api/changelog.md -git commit -m "docs(api): changelog entry for OAuth2 auth cutover (TRA-848)" -``` - ---- - -## Task 7: Final verification - -- [ ] **Step 7.1: Full quality gate** - -Run: `pnpm build && pnpm typecheck && pnpm lint` -Expected: all pass. - -- [ ] **Step 7.2: Repo-wide forbidden-string guard** - -Run: -```bash -grep -rn "TRAKRF_API_KEY" docs/ -grep -rnE "use (your key|the returned JWT) as|API key is a JWT|your-api-key-jwt" docs/ -``` -Expected: no matches in either. - -- [ ] **Step 7.3: Live contract spot-check against preview** - -Re-run the Baseline B3 probes and confirm the documented status codes/shapes still hold. (Full `client_credentials` round-trip requires a real minted credential; if one is available in the test org, exchange it and confirm `access_token`/`refresh_token`/`expires_in:900`. Otherwise the 400/401 probes plus the published spec are sufficient evidence.) - -- [ ] **Step 7.4: Visual spot-check** - -Run: `pnpm serve` (or `pnpm dev`) and open the rendered Authentication, Quickstart, and Getting-started→API pages. Confirm: the four-step flow renders, code blocks are intact, internal anchors (`#get-an-access-token`, `#refresh-an-access-token`, `#mint-your-first-api-key`) resolve. Note explicitly if you cannot launch a browser to verify. - -- [ ] **Step 7.5: Finish the branch** - -Use superpowers:finishing-a-development-branch to open the PR (target `main`, conventional-commit title). Do NOT merge without explicit user confirmation. - ---- - -## Self-review (against the spec) - -- **Spec coverage:** authentication.md rewrite (Task 1) ✓; quickstart + getting-started updates (Tasks 2–3) ✓; light sweep (Task 4) ✓; private-endpoints + postman prose (Task 5) ✓; security-properties section (Task 1, §"Security properties") ✓; management endpoints kept internal (no task documents them) ✓; changelog (Task 6) ✓. -- **Placeholders:** worked example values are concrete and verified; access_token/refresh_token JWT vs opaque shapes are explicit. No TBD/TODO. -- **Type/name consistency:** env var is `$TRAKRF_ACCESS_TOKEN` everywhere; anchors `#get-an-access-token`, `#refresh-an-access-token`, `#mint-your-first-api-key` are defined in Task 1 and referenced in Tasks 2–3 and the verification guard. -- **Out of scope honored:** no backend/spec change; `/orgs/{id}/api-keys` not documented publicly. diff --git a/superpowers/plans/2026-05-29-tra-859-bb-oauth-client-credentials-cutover.md b/superpowers/plans/2026-05-29-tra-859-bb-oauth-client-credentials-cutover.md deleted file mode 100644 index 1e2c54a..0000000 --- a/superpowers/plans/2026-05-29-tra-859-bb-oauth-client-credentials-cutover.md +++ /dev/null @@ -1,256 +0,0 @@ -# TRA-859 — BB harness cutover to OAuth2 client_credentials - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Cut the blackbox (BB) harness + methodology from the dead "API key is a bearer JWT" model to OAuth2 `client_credentials`: the harness hands each contract-track session a durable `{client_id, client_secret}` pair, and the BB agent exchanges it at `POST /api/v1/oauth/token` for a short-lived access token used as `Authorization: Bearer`, re-minting on 401. - -**Architecture:** The `just bb_cycle` recipe filters the host `.env.local` down to one org's credentials per session; `scripts/test_bb_cycle.sh` is its self-test. The three markdown wrappers (`BB.md` shared, `BB_PRE_KEY.md` contract track, `BB_MINT_KEY.md` onboarding track) tell the BB agent how to authenticate. All five files reference the old key model and must move to the pair+exchange model. `tests/blackbox/.env.local` is already updated (gitignored, out of scope here). - -**Tech Stack:** bash (justfile recipe + bats-free shell self-test), Markdown methodology docs. "Tests" = `bash scripts/test_bb_cycle.sh` (asserts the filtered env) + grep guards for stale tokens. - -**Working dir:** worktree `/home/mike/trakrf-docs/.claude/worktrees/tra-859-bb-oauth-cutover` on branch `worktree-tra-859-bb-oauth-cutover`. **pnpm only** if building (no build needed here). - -**Contract (ground truth for all tasks):** -- Per-org durable creds: `BBn_CLIENT_ID` (UUID) + `BBn_CLIENT_SECRET` (`trakrf_`+64hex) + `BBn_ORG_ID`. -- Exchange: `POST $API_TEST_APP_URL/api/v1/oauth/token`, header `Content-Type: application/json`, body `{"grant_type":"client_credentials","client_id":"…","client_secret":"…"}` → `{access_token, refresh_token, token_type:"Bearer", expires_in:900}`. -- Use `access_token` as `Authorization: Bearer`; on `401` (expiry, ~15 min) re-exchange. (Form-urlencoded → 415 today; JSON only for now.) - ---- - -## Task 1: `just bb_cycle` recipe + its self-test → pass the credential pair - -**Files:** -- Modify: `justfile` (the `bb_cycle` recipe: pre-key validation block ~L112–124, filtered-env write ~L186–190) -- Modify: `scripts/test_bb_cycle.sh` (synthetic env `make_env` ~L27–37; the duplicate inline env ~L286–289; pre-key filter assertions T19 ~L233–256; mint negative-filter assertions ~L274–278; validate assertions T21 ~L287–296) - -- [ ] **Step 1: Update the self-test FIRST (TDD) — synthetic envs + assertions expect the pair** - -In `scripts/test_bb_cycle.sh`: -- In `make_env` (the `cat > "$env_path" <<'EOF'` block), replace the three `BBn_API_KEY=jwt-bbn-fake` lines with two lines each: - ``` - BB1_ORG_ID=111 - BB1_CLIENT_ID=client-bb1-fake - BB1_CLIENT_SECRET=trakrf_bb1fakesecret - BB2_ORG_ID=222 - BB2_CLIENT_ID=client-bb2-fake - BB2_CLIENT_SECRET=trakrf_bb2fakesecret - BB3_ORG_ID=333 - BB3_CLIENT_ID=client-bb3-fake - BB3_CLIENT_SECRET=trakrf_bb3fakesecret - ``` -- In the T21 inline env (~L286–289, the smaller `cat` that omits BB2 to test the missing-var path), replace `BB1_API_KEY=jwt-bb1-fake` with `BB1_CLIENT_ID=client-bb1-fake` + `BB1_CLIENT_SECRET=trakrf_bb1fakesecret` (keep `BB1_ORG_ID=111`; keep BB2 absent). -- T19 pre-key filter assertions (~L244–256): change the `BB_API_KEY` checks to the new shape. Replace: - - `grep -q "^BB_API_KEY=jwt-bb2-fake$"` → `grep -q "^BB_CLIENT_ID=client-bb2-fake$"` (rename the assert label too), and ADD a sibling assert `grep -q "^BB_CLIENT_SECRET=trakrf_bb2fakesecret$"`. - - The "no other orgs' keys" negatives: `! grep -q "^BB1_API_KEY="` / `"^BB3_API_KEY="` → `! grep -q "^BB1_CLIENT_ID="` and `! grep -q "^BB1_CLIENT_SECRET="` (and BB3 likewise). Keep asserting `BB_ORG_ID` matches. -- Mint negative-filter assertions (~L274–278): `! grep -q "^BB_API_KEY="` → `! grep -q "^BB_CLIENT_ID="` and `! grep -q "^BB_CLIENT_SECRET="`; `! grep -q "^BB[1-3]_API_KEY="` → `! grep -q "^BB[1-3]_CLIENT_ID="` and `^BB[1-3]_CLIENT_SECRET="`. -- T21 validate assertions (~L294–296): `missing BB2_API_KEY rejected` / `error message names BB2_API_KEY` → `BB2_CLIENT_ID` (the recipe validates client_id first; see Step 3). Update both the assert label and the `grep -q "BB2_..."` on the error text. - -- [ ] **Step 2: Run the self-test, expect FAILURE** - -Run: `bash scripts/test_bb_cycle.sh` -Expected: FAILs on the pre-key filter + validate assertions (recipe still emits `BB_API_KEY`, not `BB_CLIENT_ID`/`BB_CLIENT_SECRET`). This confirms the test now drives the new behavior. - -- [ ] **Step 3: Update the `bb_cycle` recipe to read + emit the pair** - -In `justfile`, the pre-key validation block (currently): -```bash - src_key_var="${selector}_API_KEY" - src_id_var="${selector}_ORG_ID" - src_key="${!src_key_var:-}" - src_id="${!src_id_var:-}" - if [ -z "$src_key" ]; then - echo "ERROR: $src_key_var is empty or unset in $source_env" >&2 - exit 1 - fi - if [ -z "$src_id" ]; then - echo "ERROR: $src_id_var is empty or unset in $source_env" >&2 - exit 1 - fi -``` -Replace with: -```bash - src_client_id_var="${selector}_CLIENT_ID" - src_client_secret_var="${selector}_CLIENT_SECRET" - src_id_var="${selector}_ORG_ID" - src_client_id="${!src_client_id_var:-}" - src_client_secret="${!src_client_secret_var:-}" - src_id="${!src_id_var:-}" - if [ -z "$src_client_id" ]; then - echo "ERROR: $src_client_id_var is empty or unset in $source_env" >&2 - exit 1 - fi - if [ -z "$src_client_secret" ]; then - echo "ERROR: $src_client_secret_var is empty or unset in $source_env" >&2 - exit 1 - fi - if [ -z "$src_id" ]; then - echo "ERROR: $src_id_var is empty or unset in $source_env" >&2 - exit 1 - fi -``` -And the filtered-env write block (currently): -```bash - if [ -n "$selector" ]; then - echo "BB_ORG=$selector" - echo "BB_API_KEY=$src_key" - echo "BB_ORG_ID=$src_id" - else -``` -Replace the pre-key branch with: -```bash - if [ -n "$selector" ]; then - echo "BB_ORG=$selector" - echo "BB_CLIENT_ID=$src_client_id" - echo "BB_CLIENT_SECRET=$src_client_secret" - echo "BB_ORG_ID=$src_id" - else -``` - -- [ ] **Step 4: Run the self-test, expect PASS** - -Run: `bash scripts/test_bb_cycle.sh` -Expected: all assertions pass (final line reports 0 failures). - -- [ ] **Step 5: Commit** - -```bash -git add justfile scripts/test_bb_cycle.sh -git commit -m "test(bb): hand contract-track sessions client_id/secret pair (TRA-859)" -``` - ---- - -## Task 2: `BB_PRE_KEY.md` — contract-track methodology to pair+exchange - -**Files:** -- Modify: `tests/blackbox/BB_PRE_KEY.md` - -- [ ] **Step 1: Rewrite the credential model** - -Make these prose changes (preserve surrounding structure/tone): -- L3: "You have a pre-minted **API key** for one of three parallelism-fixture orgs." → "You have a pre-minted **`{client_id, client_secret}` credential pair** for one of three parallelism-fixture orgs." -- Env-vars list (~L9–14): replace the `BB_API_KEY — the persistent JWT … Pass it as Authorization: Bearer $BB_API_KEY` bullet with: - - `BB_CLIENT_ID` / `BB_CLIENT_SECRET` — the durable OAuth2 client_credentials for the assigned fixture org. **Not** a bearer token; you exchange them for a short-lived access token (next paragraph). - - `BB_ORG` / `BB_ORG_ID` — unchanged. - Then add the exchange instruction: - > Mint an access token once at session start: - > ```bash - > ACCESS_TOKEN=$(curl -s -X POST "$API_TEST_APP_URL/api/v1/oauth/token" \ - > -H "Content-Type: application/json" \ - > -d "{\"grant_type\":\"client_credentials\",\"client_id\":\"$BB_CLIENT_ID\",\"client_secret\":\"$BB_CLIENT_SECRET\"}" \ - > | python3 -c "import json,sys;print(json.load(sys.stdin)['access_token'])") - > ``` - > Send `Authorization: Bearer $ACCESS_TOKEN` on every API call. The token lives 15 minutes (`expires_in: 900`); if a call returns `401` with `detail: "Invalid or expired token"`, re-run the exchange and retry. (`/oauth/token` requires `Content-Type: application/json` — form-urlencoded returns 415.) -- L19 unset guard: "If `$BB_API_KEY` or `$BB_ORG` is unset, stop and report" → "If `$BB_CLIENT_ID`, `$BB_CLIENT_SECRET`, or `$BB_ORG` is unset, stop and report". -- L44: "The fixture key on each org is `bb-parallel-permanent`, no expiry, with scopes:" → "The fixture credential on each org is named `bb-parallel-permanent`, with scopes:" (drop "no expiry"; the credential pair is durable, but the access tokens it mints are short-lived). -- L62: "the fixture **key** is intentionally broad" → "the fixture **credential** is intentionally broad" (keep the 403-unreachable point intact). -- L69 orientation step: "Load `$BB_API_KEY`, confirm you can reach `/health.json` and `/api/openapi.yaml`" → "Mint your access token (above), confirm you can reach `/health.json` and `/api/openapi.yaml`". - -- [ ] **Step 2: Guard — no stale references** - -Run: `grep -nE "BB_API_KEY|persistent JWT|Bearer \\\$BB_API_KEY|no expiry" tests/blackbox/BB_PRE_KEY.md` -Expected: no matches. - -- [ ] **Step 3: Commit** - -```bash -git add tests/blackbox/BB_PRE_KEY.md -git commit -m "docs(bb): contract track exchanges client_credentials for access token (TRA-859)" -``` - ---- - -## Task 3: `BB.md` — shared methodology wording - -**Files:** -- Modify: `tests/blackbox/BB.md` - -- [ ] **Step 1: Reconcile key→credential/token wording (do not change methodology substance)** - -- L5–6 (track summaries): "Start without an API key … mint a key" → "Start without credentials … mint a credential pair"; "Start with a pre-minted **API key** pinned to a … org" → "Start with a pre-minted **credential pair** pinned to a … org". -- L8: "Where the sections below refer to "your API key," the wrapper has already specified its origin." → "Where the sections below refer to "your access token," the wrapper has already specified how you obtained it (minted in the SPA on the mint track; exchanged from a client_credentials pair on the contract track)." -- L39 bullet heading "**No programmatic API key mint.**": keep the claim (minting is SPA-bound, no public programmatic seam) but reword "mint a key" → "mint credentials" within the bullet; leave the Stripe analogy and YAGNI note. -- L281 cleanup: "Delete any API keys or artifacts you create … platform-managed fixture keys (e.g., the persistent `bb-parallel-permanent` keys on the BB1/BB2/BB3 orgs), which are reused across cycles and not yours to revoke." → "Delete any credentials or artifacts you create … platform-managed fixture credentials (the `bb-parallel-permanent` credential on each BB1/BB2/BB3 org), which are reused across cycles and not yours to revoke." (Access tokens you mint expire on their own — nothing to clean up.) -- L116/L117 (401/403 probe list) and L149 (codegen auth-attach): these remain accurate; only change the literal phrase "the API key" → "the access token" where it refers to the value sent on the wire. Leave `X-API-Key` (it's the wrong-scheme example) as-is. - -- [ ] **Step 2: Guard** - -Run: `grep -nE "your API key|mint a key|bb-parallel-permanent keys|pre-minted API key" tests/blackbox/BB.md` -Expected: no matches. - -- [ ] **Step 3: Commit** - -```bash -git add tests/blackbox/BB.md -git commit -m "docs(bb): shared methodology uses access-token/credential terminology (TRA-859)" -``` - ---- - -## Task 4: `BB_MINT_KEY.md` — onboarding track aligns to SPA mint + exchange - -**Files:** -- Modify: `tests/blackbox/BB_MINT_KEY.md` - -- [ ] **Step 1: Align the onboarding flow to the shipped auth docs** - -The public `authentication.md` now teaches: SPA **New Key** returns a `{client_id, client_secret}` (secret shown once) → exchange at `/oauth/token` → use the access token as Bearer. Update this wrapper to match: -- L5 / "Mission" (L34) / L38: "mint **a key**" / "Set up an **API key**" / "Treat your minted **key** as "your API key"" → mint **credentials** (`client_id` + `client_secret`, secret shown once), then **exchange them at `POST /api/v1/oauth/token` (`grant_type=client_credentials`) for a short-lived access token**, and treat that **access token** as "your access token" wherever BB.md refers to one. Keep the "onboarding fails before you can authenticate = that is the report" framing. -- Keep the SPA login/password handling section and the Playwright literal-password exception **unchanged** — they are about logging into the SPA to reach the mint UI, which still applies. -- Preserve the "you control the scope at mint time / full CRUD incl DELETE is yours" point (still true — scopes are chosen on the credential at mint). - -- [ ] **Step 2: Guard** - -Run: `grep -nE "mint a key|Set up an API key|your minted key" tests/blackbox/BB_MINT_KEY.md` -Expected: no matches (phrasing moved to credentials/access-token). - -- [ ] **Step 3: Commit** - -```bash -git add tests/blackbox/BB_MINT_KEY.md -git commit -m "docs(bb): onboarding track mints credentials then exchanges for token (TRA-859)" -``` - ---- - -## Task 5: Final verification - -- [ ] **Step 1: Self-test passes** - -Run: `bash scripts/test_bb_cycle.sh` -Expected: 0 failures. - -- [ ] **Step 2: Repo-wide stale-reference guard (excluding the plan/spec + .env.local)** - -Run: -```bash -grep -rnE "BB_API_KEY|BBn_API_KEY|bb-parallel-permanent keys|persistent JWT|use (it|the key) as .*Bearer" \ - justfile scripts/test_bb_cycle.sh tests/blackbox/BB.md tests/blackbox/BB_PRE_KEY.md tests/blackbox/BB_MINT_KEY.md -``` -Expected: no matches (the recipe + docs no longer reference the dead model). `BB_CLIENT_ID`/`BB_CLIENT_SECRET` are the new names. - -- [ ] **Step 3: Dry-run the documented contract-track exchange against preview** - -Using the real cached creds from the MAIN checkout (`/home/mike/trakrf-docs/tests/blackbox/.env.local`, not the worktree), confirm the exact command block in `BB_PRE_KEY.md` works: -```bash -set -a; . /home/mike/trakrf-docs/tests/blackbox/.env.local; set +a -BB_CLIENT_ID=$BB1_CLIENT_ID BB_CLIENT_SECRET=$BB1_CLIENT_SECRET bash -c ' - AT=$(curl -s -X POST "$API_TEST_APP_URL/api/v1/oauth/token" -H "Content-Type: application/json" \ - -d "{\"grant_type\":\"client_credentials\",\"client_id\":\"$BB_CLIENT_ID\",\"client_secret\":\"$BB_CLIENT_SECRET\"}" \ - | python3 -c "import json,sys;print(json.load(sys.stdin)[\"access_token\"])") - curl -s -o /dev/null -w "orgs/me: %{http_code}\n" -H "Authorization: Bearer $AT" "$API_TEST_APP_URL/api/v1/orgs/me"' -``` -Expected: `orgs/me: 200`. (Confirms the methodology's literal commands are correct, not just the prose.) - -- [ ] **Step 4: Finish the branch** - -Use superpowers:finishing-a-development-branch → push + PR to `main`, title referencing TRA-859. Do NOT merge without explicit user confirmation. - ---- - -## Self-review - -- **Spec coverage:** recipe+test (Task 1), BB_PRE_KEY contract track (Task 2), BB.md shared (Task 3), BB_MINT_KEY onboarding (Task 4), verification incl live dry-run (Task 5). All four files from the inventory + the recipe are covered. -- **Placeholders:** none — every edit gives concrete old→new strings and the exchange command is literal. -- **Naming consistency:** `BB_CLIENT_ID` / `BB_CLIENT_SECRET` / `BB_ORG_ID` / `BB_ORG` used identically in the recipe (Task 1 Step 3), the self-test (Task 1 Step 1), and the contract-track doc (Task 2). Recipe validates `CLIENT_ID` first, so the T21 "missing → names BB2_CLIENT_ID" assertion matches. -- **Out of scope (correctly):** `.env.local` (already updated, gitignored); the `check-deploy-lag.sh` staleness-alarm improvement (separate concern); form-urlencoded support (platform-side). diff --git a/superpowers/plans/2026-05-30-tra-885-id-vs-external-key-distribution.md b/superpowers/plans/2026-05-30-tra-885-id-vs-external-key-distribution.md deleted file mode 100644 index 92400b2..0000000 --- a/superpowers/plans/2026-05-30-tra-885-id-vs-external-key-distribution.md +++ /dev/null @@ -1,60 +0,0 @@ -# TRA-885 — `id` vs `external_key` Implementation Plan - -> **For agentic workers:** Consumer-docs prose change across three files. No unit-test harness for -> docs; verification is `pnpm build` (Redocusaurus + broken-link check) plus a read-through -> against the spec. Source of truth: bb-253 round-2.5 §0 convergence + Mike's in-session -> confirmation (the TRA-885 ticket is stale). - -**Goal:** State the identifier contract precisely — surrogate `id` is globally unique / opaque / -permanent (never reused); `external_key` auto-mint is `MAX(live key) + 1` (permanent middle-delete -gaps, only highest-freed reused) — and remove the stale "randomly distributed" and "lowest unused -slot / gap-filling" wording. - -**Architecture:** Three files. id-format.md (field-name the distribution claim + a distinction -note); resource-identifiers.md (globally-unique `id` framing + MAX+1 auto-mint mechanism); -changelog.md (new entry + fix two stale "lowest unused slot" assertions). No structural change. - -**Tech Stack:** Docusaurus, Markdown, Redocusaurus, pnpm. - ---- - -## Constraints - -- `id`: globally unique across all resource types, opaque, permanent, **never reused** (no hard - delete; single shared id sequence never reseeded). Gated: PR does not merge until TRA-886 is - prod-live + Mike's diff approval. -- `external_key`: `MAX(live key) + 1`, zero-padded `ASSET-%04d` / `LOC-%03d`; permanent - middle-delete gaps; only the current highest live key's number is freed for re-issue; separate - caller-resupply path for soft-deleted key strings. Keep "non-monotonic / not a Postgres - sequence"; drop "lowest unused slot," "gap-filling," "recyclable-implies-refill." -- Docs-only. - -## Task 1 — `docs/api/id-format.md` ✅ - -- Replaced the standalone "randomly distributed" sentence (`:26`) with the field-named `id` - framing (high-entropy, globally unique, opaque, permanent/never-reused). -- Added a `:::note` pinning `id` ≠ `external_key`, cross-linking the auto-mint section. - -## Task 2 — `docs/api/resource-identifiers.md` ✅ - -- "Numeric `id` is a surrogate key" para: per-type-scoping → globally unique / opaque / permanent; - use `id` as durable FK. -- Auto-mint section: replaced the "lowest unused slot among live rows" paragraph with the `MAX(live - key) + 1` mechanism (permanent gaps, highest-freed reused, zero-padding), plus the contrast to - the never-reused `id`. -- System-of-record note: "may recycle a slot" → "not a stable reference; `MAX+1` can re-issue a - deleted top-of-range number." - -## Task 3 — `docs/api/changelog.md` ✅ - -- New top entry: `id` globally unique/opaque/permanent (tied to single shared sequence) + - `external_key` `MAX+1` correction. -- Fixed the LOC-NNN-finalize entry and the BB52 non-monotonic-wording entry: "lowest unused slot" - → `MAX(live key) + 1`. - -## Task 4 — Verify & commit - -- [ ] `pnpm build` passes (Redocusaurus + broken-link check; new anchors resolve). -- [ ] Read-through: no "randomly distributed" standalone; no "lowest unused slot / gap-filling" - except where quoted-as-corrected in the new changelog entry; every claim names its field. -- [ ] Commit; open PR; **hold for Mike's diff review** (no merge until TRA-886 prod-live). diff --git a/superpowers/plans/2026-05-30-tra-891-api-identity-join-guidance.md b/superpowers/plans/2026-05-30-tra-891-api-identity-join-guidance.md deleted file mode 100644 index ce4d081..0000000 --- a/superpowers/plans/2026-05-30-tra-891-api-identity-join-guidance.md +++ /dev/null @@ -1,211 +0,0 @@ -# TRA-891 — API Identity Join Guidance Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Correct the published advice that integrators should use the surrogate `id` as their durable foreign key; reframe `id` as an internal reconciliation anchor, add natural-key join guidance, and demote `id` on the versioning page. - -**Architecture:** Docs-only prose edits across three Docusaurus Markdown pages plus the changelog. No code, no OpenAPI/platform edits (verified: the wrong advice lives only in docs prose; `external_key` is confirmed present+required on both `AssetView` and `LocationView`; `customer_identifier`/`slug`/public users resources do not exist on the API). "Tests" for a docs change are `pnpm build`, `pnpm lint`, internal-link resolution, and a manual read-through. - -**Tech Stack:** Docusaurus 3, Markdown/MDX, Redocusaurus (fetches the live OpenAPI spec at build), pnpm. - ---- - -## File Structure - -- `docs/api/resource-identifiers.md` — fix the wrong `id`-as-FK sentence; add a "Joining your system of record" subsection. (Primary edit site.) -- `docs/api/changelog.md` — reword the TRA-885 changelog entry to drop the FK prescription. -- `docs/api/versioning.md` — add an `id`-demotion note under the stability commitment. - -All three are existing prose pages; no new files, no restructure. - ---- - -### Task 1: Remove the actively-wrong `id`-as-foreign-key sentence (launch-relevant, do first) - -**Files:** -- Modify: `docs/api/resource-identifiers.md` (the "Numeric `id` is a surrogate key" section, ~line 58) - -- [ ] **Step 1: Locate the exact current sentence** - -Run: `grep -n "Use \`id\` as your durable foreign key when you mirror TrakRF data." docs/api/resource-identifiers.md` -Expected: one match at the end of the surrogate-key paragraph. - -- [ ] **Step 2: Replace the final sentence of that paragraph** - -Find this trailing sentence (keep everything before it unchanged): - -``` -client code matches ids to their entity type as standard surrogate-key discipline. Use `id` as your durable foreign key when you mirror TrakRF data. -``` - -Replace with: - -``` -client code matches ids to their entity type as standard surrogate-key discipline. `id` is a stable internal anchor — server-assigned, opaque, and not arbitrarily rekeyed — which makes it useful as a sync/reconciliation handle when you mirror TrakRF data. It is **not** your business foreign key: key your own system of record on the natural key (`external_key`) where one exists, and reach for `id` only as the durable handle when no natural key is available. See [Joining your system of record](#joining-your-system-of-record) for the per-resource rule. -``` - -- [ ] **Step 3: Verify the wrong advice is gone** - -Run: `grep -rn "durable foreign key" docs/api/resource-identifiers.md` -Expected: no match (the phrase is removed from this file). - -- [ ] **Step 4: Commit** - -```bash -git add docs/api/resource-identifiers.md -git commit -m "docs(api): stop advising id as the durable foreign key (TRA-891)" -``` - ---- - -### Task 2: Add the "Joining your system of record" subsection - -**Files:** -- Modify: `docs/api/resource-identifiers.md` (insert between the end of the "Numeric `id` is a surrogate key" section and the "## Natural-key lookup uses `?external_key=`" heading, ~line 61) - -- [ ] **Step 1: Find the insertion point** - -Run: `grep -n "## Natural-key lookup uses" docs/api/resource-identifiers.md` -Expected: one match (`## Natural-key lookup uses \`?external_key=\``). Insert the new subsection immediately *before* this heading, after the blank line that follows the int64 paragraph (the paragraph ending "…typed-client codegen."). - -- [ ] **Step 2: Insert the subsection** - -Insert (note: `###` keeps it under the `## Path-param lookup uses \`id\`` section, beside "Numeric `id` is a surrogate key"): - -```markdown -### Joining your system of record {#joining-your-system-of-record} - -When you mirror TrakRF data into your own system, join on the **natural key**, not the surrogate `id`: - -- **Assets and locations** — join on `external_key`, your own handle (a SKU, an asset tag, an ERP code, a facility code). It is the value your warehouse software, ERP, or operator already recognizes, and it round-trips on every response. -- **General rule** — join on a stable natural key where one exists; where none does, `id` is the durable handle. Use `id` as a reconciliation anchor in that case, not as a business key you export into other systems. - -The public integration surface is assets, locations, and tags. Users and organization administration are not public joinable resources in v1 — an integration authenticates as a single organization through its API key (see [`/orgs/me`](./private-endpoints#orgs-me)), so there is no cross-org or cross-user join to maintain. -``` - -- [ ] **Step 3: Verify the anchor target exists for the Task 1 cross-link** - -Run: `grep -n "joining-your-system-of-record" docs/api/resource-identifiers.md` -Expected: two matches — the `#joining-your-system-of-record` link from Task 1 and the `{#joining-your-system-of-record}` heading anchor here. - -- [ ] **Step 4: Commit** - -```bash -git add docs/api/resource-identifiers.md -git commit -m "docs(api): add per-resource natural-key join guidance (TRA-891)" -``` - ---- - -### Task 3: Reword the TRA-885 changelog entry - -**Files:** -- Modify: `docs/api/changelog.md` (~line 18, the globally-unique-id bullet) - -- [ ] **Step 1: Locate the FK prescription in the changelog** - -Run: `grep -n "use it as your durable foreign key" docs/api/changelog.md` -Expected: one match within the "surrogate `id` is globally unique" bullet. - -- [ ] **Step 2: Replace the trailing clause** - -Find: - -``` -That cross-type collision is eliminated: ids are now minted from one shared sequence, so global uniqueness holds by construction. Treat `id` as opaque (don't parse it, order by it, or infer a count or creation time) and use it as your durable foreign key. See [ID format](./id-format) -``` - -Replace with: - -``` -That cross-type collision is eliminated: ids are now minted from one shared sequence, so global uniqueness holds by construction. Treat `id` as opaque (don't parse it, order by it, or infer a count or creation time) — it is a stable internal anchor for reconciliation, not your business foreign key; join your own systems on the natural key (`external_key`). See [ID format](./id-format) -``` - -- [ ] **Step 3: Verify no FK prescription remains** - -Run: `grep -rn "durable foreign key" docs/api/` -Expected: no matches anywhere under `docs/api/`. - -- [ ] **Step 4: Commit** - -```bash -git add docs/api/changelog.md -git commit -m "docs(changelog): drop id-as-foreign-key advice from TRA-885 entry (TRA-891)" -``` - ---- - -### Task 4: Demote `id` on the versioning page - -**Files:** -- Modify: `docs/api/versioning.md` (insert after the "## Stability commitment (v1)" section, before "## Open vs closed enums") - -- [ ] **Step 1: Find the insertion point** - -Run: `grep -n "^## Open vs closed enums" docs/api/versioning.md` -Expected: one match. Insert the new subsection immediately before this heading (after the "Clients written against v1 will continue to work… so don't." paragraph). - -- [ ] **Step 2: Insert the demotion note** - -```markdown -### The surrogate `id` is an internal anchor, not your join key - -The `id` field is stable and won't be arbitrarily rekeyed, which makes it a usable sync / reconciliation anchor. It is **not** the integrator's business foreign key — don't key your own system of record on it. Join on the natural key (`external_key`) where one exists; see [Resource identifiers → Joining your system of record](./resource-identifiers#joining-your-system-of-record). The field-stability commitment above (names and types of returned fields don't change without a major version) is the contract that applies to `id`; TrakRF does not publish a permanence guarantee beyond it, because treating `id` as a durable external business key re-introduces the coupling the natural-key model is designed to avoid. -``` - -- [ ] **Step 3: Verify the cross-link target matches Task 2's anchor** - -Run: `grep -n "resource-identifiers#joining-your-system-of-record" docs/api/versioning.md` -Expected: one match — the slug must equal the `{#joining-your-system-of-record}` anchor created in Task 2. - -- [ ] **Step 4: Commit** - -```bash -git add docs/api/versioning.md -git commit -m "docs(api): demote surrogate id on versioning page (TRA-891)" -``` - ---- - -### Task 5: Validate the build and links, then finalize - -**Files:** none (verification only) - -- [ ] **Step 1: Lint** - -Run: `pnpm lint` -Expected: passes (no new errors). If Prettier reports formatting on the edited files, run `pnpm format` (or the repo's documented formatter) and amend the relevant commit. - -- [ ] **Step 2: Typecheck** - -Run: `pnpm typecheck` -Expected: passes. - -- [ ] **Step 3: Production build (catches broken internal links / anchors)** - -Run: `pnpm build` -Expected: build succeeds. Docusaurus fails the build on broken Markdown links, so a clean build confirms the `#joining-your-system-of-record` cross-links (Task 1, Task 4) and the `./private-endpoints#orgs-me` link resolve. - -- [ ] **Step 4: Read-through verification** - -Run: `grep -rn "durable foreign key\|as your.*foreign key\|use \`id\` as your" docs/api/` -Expected: no matches. Manually confirm: resource-identifiers.md reframe reads coherently; the new subsection states the assets/locations→`external_key` rule and the general rule; versioning.md note demotes `id` without adding a permanence guarantee. - -- [ ] **Step 5: No further commit needed** unless Step 1 required a formatting fix; the four content commits from Tasks 1–4 stand. - ---- - -## Self-Review - -**Spec coverage:** -- "Remove `id`-as-FK advice (do first)" → Task 1 ✓ -- Per-entity join guidance (assets/locations on `external_key`; general rule) → Task 2 ✓ -- Reframe `id` (stable anchor, not business FK) → Task 1 + Task 2 + Task 4 ✓ -- Versioning page: breaking-change policy (already present) + demote `id`, no absolute permanence guarantee → Task 4 ✓ -- Delta sync → not advertised; nothing to add (per spec verification) — intentionally no task ✓ -- Users/orgs join guidance → intentionally omitted (not public joinable resources); the omission is captured by Task 2's "not public joinable resources in v1" note ✓ -- Edit-site/PR shape: docs-only, no platform edit → reflected in plan (no OpenAPI tasks) ✓ - -**Placeholder scan:** No TBD/TODO; every edit shows exact before/after prose. ✓ - -**Type/anchor consistency:** The anchor slug `joining-your-system-of-record` is defined in Task 2 and referenced identically in Task 1 and Task 4. The `./private-endpoints#orgs-me` anchor matches the existing `{#orgs-me}` in private-endpoints.md. ✓ diff --git a/superpowers/plans/2026-05-30-tra-892-docs-hygiene-bundle.md b/superpowers/plans/2026-05-30-tra-892-docs-hygiene-bundle.md deleted file mode 100644 index 14fa764..0000000 --- a/superpowers/plans/2026-05-30-tra-892-docs-hygiene-bundle.md +++ /dev/null @@ -1,142 +0,0 @@ -# TRA-892 Docs-Hygiene Bundle Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Land four BB-fleet doc-hygiene corrections (3 wording one-liners + remove one stale backlog row) and groom the `contact.url` deferral in Linear. - -**Architecture:** Docs-only edits in `trakrf/docs`. No spec/platform change — `info.contact.url` is already correct live. Verification is `pnpm build` + `pnpm lint` (no test suite for prose) plus targeted greps confirming each before/after. - -**Tech Stack:** Docusaurus / Redocusaurus, pnpm, Markdown/MDX. Linear MCP for the grooming comment. - ---- - -## File Structure - -- Modify: `tests/blackbox/BB_PRE_KEY.md` — fixture-data wording (item 1) -- Modify: `docs/api/quickstart.mdx` — minimal-PATCH comment (item 2) -- Modify: `tests/blackbox/BB.md` — context-block version check (item 3) -- Modify: `tests/blackbox/BACKLOG.md` — remove obsolete deferral row (item 4a) -- Linear: comment on TRA-743 (item 4b) - ---- - -### Task 1: `BB_PRE_KEY.md` — location is a scan projection, not an asset field - -**Files:** - -- Modify: `tests/blackbox/BB_PRE_KEY.md:47` and `:49` - -- [ ] **Step 1: Edit line 47.** Replace: - -``` -- Every asset has `location_id` resolved from scan history — no asset on this fixture reads as `location_id: null`. -``` - -with: - -``` -- Every asset has a resolvable current location, but location is **not** a field on the asset object — it is resolved from scan history via the asset-locations report (`GET /api/v1/reports/asset-locations`) and per-asset history (`GET /api/v1/assets/{asset_id}/history`), both gated `tracking:read`. No fixture asset reads as location-unknown. -``` - -- [ ] **Step 2: Reconcile line 49** so it does not re-imply a field on the asset. Replace: - -``` -**Scans:** populated. Each org carries ~25 `asset_scans` per asset over a 90-day window (~675 per org), and the materialized `location_id` on every asset reflects the most-recent scan. -``` - -with: - -``` -**Scans:** populated. Each org carries ~25 `asset_scans` per asset over a 90-day window (~675 per org), and the current location reported for every asset (via the scan-data endpoints above) reflects its most-recent scan. -``` - -(Leave the rest of line 49 — the `tracking:read` sentence — unchanged.) - -- [ ] **Step 3: Verify.** Run: `grep -n "location_id\` resolved\|materialized \`location_id\`" tests/blackbox/BB_PRE_KEY.md` - Expected: no matches (both imprecise phrasings gone). - ---- - -### Task 2: `quickstart.mdx` — `{}` no-op qualifier - -**Files:** - -- Modify: `docs/api/quickstart.mdx:146` - -- [ ] **Step 1: Edit line 146.** Replace: - -``` -# nullable field; `{}` is a documented no-op. -``` - -with: - -``` -# nullable field; `{}` is a documented no-op against settable fields (it still advances `updated_at`). -``` - -- [ ] **Step 2: Verify.** Run: `grep -n "documented no-op" docs/api/quickstart.mdx` - Expected: the line now contains "against settable fields (it still advances `updated_at`)". - ---- - -### Task 3: `BB.md` — point version check at the app origin - -**Files:** - -- Modify: `tests/blackbox/BB.md:209` - -- [ ] **Step 1: Edit line 209.** Replace: - -``` -- Spec/build version — fetch `$API_TEST_DOCS_URL/health.json` and record what it returns. If `/health.json` 404s, that's a finding; record what you did get. -``` - -with: - -``` -- Spec/build version — fetch `$API_TEST_APP_URL/health.json` (the app origin carries `version` + `spec_refreshed_at`; the docs origin's `/health.json` carries only the docs build stamp). Record what it returns. If `/health.json` 404s, that's a finding; record what you did get. -``` - -- [ ] **Step 2: Verify.** Run: `grep -n "Spec/build version" tests/blackbox/BB.md` - Expected: line now references `$API_TEST_APP_URL/health.json`. - ---- - -### Task 4: `BACKLOG.md` — remove the obsolete `contact.url` deferral row - -**Files:** - -- Modify: `tests/blackbox/BACKLOG.md:47` - -- [ ] **Step 1: Delete the entire table row** beginning `| Restore \`info.contact.url\` on OpenAPI spec` (line 47). Remove the whole line; leave the table header (41-42) and the three preceding rows (43-46) intact. - -- [ ] **Step 2: Verify.** Run: `grep -n "info.contact.url" tests/blackbox/BACKLOG.md` - Expected: no matches. Run: `grep -c "^| " tests/blackbox/BACKLOG.md` and confirm the deferred-work table dropped by exactly one row. - ---- - -### Task 5: Build + lint - -- [ ] **Step 1:** Run: `pnpm build` Expected: success (no spec change; Redocusaurus fetches live spec). -- [ ] **Step 2:** Run: `pnpm lint` Expected: pass. - ---- - -### Task 6: Commit + groom Linear - -- [ ] **Step 1: Commit** the four file edits: - -```bash -git add tests/blackbox/BB_PRE_KEY.md docs/api/quickstart.mdx tests/blackbox/BB.md tests/blackbox/BACKLOG.md -git commit -m "docs(api): TRA-892 BB-fleet hygiene — location-projection wording, PATCH no-op qualifier, BB version-check origin, retire contact.url deferral" -``` - -- [ ] **Step 2: Groom TRA-743 (already Done).** Add a Linear comment recording that `info.contact.url` was restored by design (TRA-882, Stripe pattern `https://trakrf.id`), so the `BACKLOG.md` "restore when a helpdesk lands" deferral has been removed as obsolete. No state change. - ---- - -### Task 7: Push + PR (hold for approval before merge) - -- [ ] **Step 1:** Push branch `docs/tra-892-docs-bundle`. -- [ ] **Step 2:** Open PR with the spec/plan link and the four-item summary. **Do not merge** — hold for user approval. diff --git a/superpowers/plans/2026-07-29-tra-1045-app-tour-regenerate.md b/superpowers/plans/2026-07-29-tra-1045-app-tour-regenerate.md deleted file mode 100644 index 464cbd4..0000000 --- a/superpowers/plans/2026-07-29-tra-1045-app-tour-regenerate.md +++ /dev/null @@ -1,947 +0,0 @@ -# TRA-1045 App Tour Regenerate — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Bring the customer docs in line with the shipped UI — Inventory renamed to Scan, Home and Barcode tabs removed, Scan tiles relabelled — and regenerate the App Tour from live observation with clean, prod-like screenshots. - -**Architecture:** Docs-only changes in `trakrf/docs`, plus a locally-run instance of `trakrf/platform` used purely as a screenshot rig. Structure lands first (pages, sidebar, redirects) so the build stays green; then the capture environment; then prose written from observation; then a terminology sweep; then acceptance verification. - -**Tech Stack:** Docusaurus 3.9.2, `@docusaurus/plugin-client-redirects`, pnpm, Playwright MCP, `ble-mcp-test` bridge, PostgreSQL/TimescaleDB via `just`. - -## Global Constraints - -- **pnpm exclusively.** Never `npm` or `npx`; use `pnpm dlx` in place of `npx`. -- **Never push to main.** All work lands on `docs/tra-1045-app-tour-regenerate` via PR. -- **Do not merge the PR** until TRA-1046 deploys this UI to prod. Opening it is fine and desirable — sync-preview re-merges open PRs, so the work is reviewable on the preview docs site. -- **"Inventory" is reserved vocabulary** for the future Inventory module. It must never describe the scanning surface. Exception: it may describe a *customer's own* third-party systems (e.g. `docs/getting-started/index.md:24`, "integrating TrakRF into existing systems (inventory, ERP, custom dashboards)") — that usage stays. -- **"Barcode"** survives only as a scan mode and as the API `tag_type` value. Never a tab or screen. -- **"Home"** never names a screen. -- **Tile names and order:** Scans → Assets → Found → Missing → Extra. With no reconcile list loaded, only **Scans** and **Assets** render. -- **No Linear ticket references in published docs.** `TRA-NNN` is fine in this plan and in specs; never in `docs/`. -- **All screenshots viewport-only** (never full-browser), captured from local dev, in one consistent theme. -- **No writes to the preview or prod databases.** Seeding targets the local database only. -- Conventional commits: `docs:`, `feat:`, `fix:`, `chore:`. - ---- - -### Task 1: Infra-ops passthrough in the justfile - -Gives the docs repo `just psql preview`, `just logs`, `just gcp-auth`, and `just ops ` by forwarding to the infra justfile, mirroring the TRA-1053 pattern already in `~/platform/justfile`. - -**Files:** -- Modify: `justfile` -- Modify: `.env.example` - -**Interfaces:** -- Consumes: nothing. -- Produces: `just ops ` available for later verification steps. - -- [ ] **Step 1: Read the reference implementation** - -Run: `sed -n '54,100p' ~/platform/justfile` - -Note the resolution order: `TRAKRF_INFRA_DIR`, else the main worktree's sibling `infra` directory. The docs repo uses `.claude/worktrees/` exactly like platform, so the main-worktree resolution matters — `../infra` from inside a worktree would resolve wrongly. - -- [ ] **Step 2: Add the passthrough to `justfile`** - -```just -# ============================================================================ -# Infra Ops Passthrough -# ============================================================================ -# Cluster, namespace, pod and CNPG knowledge lives in trakrf/infra. These -# recipes only forward to its justfile — they never restate any of it. - -# Run an infra ops recipe (`just ops logs preview 1h`); bare `just ops` lists them -ops *ARGS: - #!/usr/bin/env bash - set -euo pipefail - infra_dir="${TRAKRF_INFRA_DIR:-}" - if [ -z "$infra_dir" ]; then - main_dir=$(git worktree list --porcelain 2>/dev/null \ - | awk '/^worktree /{path=$2} /^branch refs\/heads\/main$/{print path; exit}') - [ -n "$main_dir" ] || main_dir="{{ justfile_directory() }}" - infra_dir="$(dirname "$main_dir")/infra" - fi - if [ ! -f "$infra_dir/justfile" ]; then - echo "ERROR: no infra checkout at $infra_dir" >&2 - echo " Set TRAKRF_INFRA_DIR to your trakrf/infra checkout." >&2 - exit 1 - fi - just --justfile "$infra_dir/justfile" {{ ARGS }} - -# Authenticate to GCP and point kubectl at the cluster (no-op if already valid) -gcp-auth *ARGS: - @just ops gcp-auth {{ ARGS }} - -# Interactive psql on a CNPG primary: `just psql preview` -psql *ARGS: - @just ops psql {{ ARGS }} - -# Follow backend logs: `just logs preview 1h` -logs *ARGS: - @just ops logs {{ ARGS }} -``` - -- [ ] **Step 3: Document the required variable in `.env.example`** - -Append: - -```bash -# Path to your trakrf/infra checkout, used by the `just ops` passthrough. -# The sibling-directory fallback assumes ~/infra; set this if yours differs -# (e.g. ~/trakrf-infra). .envrc loads .env.local. -# TRAKRF_INFRA_DIR=/home/you/trakrf-infra -``` - -- [ ] **Step 4: Set it locally** - -Append `TRAKRF_INFRA_DIR=$HOME/trakrf-infra` to `.env.local` (gitignored — never commit it). - -- [ ] **Step 5: Verify the passthrough resolves** - -Run: `just ops --list | head -20` -Expected: infra's recipe list, including `psql` and `logs`. If it errors with "no infra checkout", `TRAKRF_INFRA_DIR` is unset in the current shell — `direnv reload` or export it. - -- [ ] **Step 6: Verify a real forwarded command** - -Run: `echo "select 1;" | just psql preview` -Expected: a `1` in the output. A "Unable to use a TTY" warning on stderr is normal and harmless. - -- [ ] **Step 7: Commit** - -```bash -git add justfile .env.example -git commit -m "chore: forward psql/logs/gcp-auth/ops to the infra justfile" -``` - ---- - -### Task 2: Page structure, sidebar, grid, and redirects - -Moves and deletes pages, restructures navigation, and installs redirects — all before any prose is written, so the build never goes red for long. New pages land as minimal stubs here and get real content in Tasks 6 and 7. - -**Files:** -- Rename: `docs/app-tour/inventory.md` → `docs/app-tour/scan.md` -- Delete: `docs/app-tour/home.md`, `docs/app-tour/barcode.md` -- Create: `docs/app-tour/readers.md`, `docs/app-tour/live-feed.md`, `docs/app-tour/outputs.md`, `docs/app-tour/geofence-defaults.md` -- Modify: `sidebars.ts`, `src/components/AppTourGrid.tsx`, `docusaurus.config.ts`, `package.json` - -**Interfaces:** -- Consumes: nothing. -- Produces: doc ids `app-tour/scan`, `app-tour/readers`, `app-tour/live-feed`, `app-tour/outputs`, `app-tour/geofence-defaults`; image paths `/img/app-tour/-desktop.png` and `-mobile.png` that Task 4 must satisfy. - -- [ ] **Step 1: Install the redirects plugin** - -Run: `pnpm add @docusaurus/plugin-client-redirects@3.9.2` - -Version must match the pinned `@docusaurus/core` 3.9.2 exactly. - -- [ ] **Step 2: Move and delete pages with git** - -```bash -git mv docs/app-tour/inventory.md docs/app-tour/scan.md -git rm docs/app-tour/home.md docs/app-tour/barcode.md -``` - -Use `git mv` rather than create-plus-delete so the rename is visible in history. - -- [ ] **Step 3: Retitle `scan.md`** - -Replace the frontmatter and heading. Leave the body prose alone — Task 5 rewrites it from observation. - -```markdown ---- -sidebar_position: 1 -title: Scan -description: Read RFID tags or barcodes and reconcile them against a list. ---- - -# Scan -``` - -- [ ] **Step 4: Create the four new stub pages** - -Each gets frontmatter, a heading, and screenshot placeholders. `readers.md`: - -```markdown ---- -sidebar_position: 7 -title: Readers -description: Register and manage fixed RFID readers. ---- - -# Readers - -## Desktop - -![Readers desktop screenshot](/img/app-tour/readers-desktop.png) - -## Mobile - -![Readers mobile screenshot](/img/app-tour/readers-mobile.png) - -## What this page does - -Placeholder — written from live observation in Task 7. -``` - -Repeat for `live-feed.md` (position 8, title "Live feed", description "Watch tag reads arrive in real time."), `outputs.md` (position 9, title "Outputs", description "Configure output devices triggered by geofence events."), and `geofence-defaults.md` (position 10, title "Geofence defaults", description "Org-wide geofence tuning applied to every portal."). - -- [ ] **Step 5: Restructure `sidebars.ts`** - -Replace the `appTourSidebar` array. The Settings category mirrors how the app nests these four entries beneath Settings in its own sidebar. - -```ts - appTourSidebar: [ - "app-tour/index", - "app-tour/scan", - "app-tour/locate", - "app-tour/assets", - "app-tour/locations", - "app-tour/reports", - { - type: "category", - label: "Settings", - link: { type: "doc", id: "app-tour/settings" }, - items: [ - "app-tour/readers", - "app-tour/live-feed", - "app-tour/outputs", - "app-tour/geofence-defaults", - ], - }, - "app-tour/help", - "app-tour/authoring", - ], -``` - -- [ ] **Step 6: Rewrite the `ENTRIES` array in `src/components/AppTourGrid.tsx`** - -```tsx -const ENTRIES: TourEntry[] = [ - { - id: "scan", - title: "Scan", - description: - "Read RFID tags or barcodes and reconcile them against a list.", - }, - { - id: "locate", - title: "Locate", - description: - "Find a specific item by walking the area with a handheld reader.", - }, - { - id: "assets", - title: "Assets", - description: "Create, view, and track asset records.", - }, - { - id: "locations", - title: "Locations", - description: "Create and organize the places where assets live.", - }, - { - id: "reports", - title: "Reports", - description: "View asset location reports and movement history.", - }, - { - id: "settings", - title: "Settings", - description: "Configure device and application settings.", - }, - { - id: "readers", - title: "Readers", - description: "Register and manage fixed RFID readers.", - }, - { - id: "live-feed", - title: "Live feed", - description: "Watch tag reads arrive in real time.", - }, - { - id: "outputs", - title: "Outputs", - description: "Configure output devices triggered by geofence events.", - }, - { - id: "geofence-defaults", - title: "Geofence defaults", - description: "Org-wide geofence tuning applied to every portal.", - }, - { - id: "help", - title: "Help", - description: "Quick answers to common questions.", - }, -]; -``` - -- [ ] **Step 7: Add the redirects to `docusaurus.config.ts`** - -There is no `plugins` array today. Add one as a sibling of `presets`, immediately before it: - -```ts - plugins: [ - [ - "@docusaurus/plugin-client-redirects", - { - redirects: [ - { from: "/app-tour/inventory", to: "/app-tour/scan" }, - { from: "/app-tour/home", to: "/app-tour/scan" }, - { from: "/app-tour/barcode", to: "/app-tour/scan" }, - ], - }, - ], - ], -``` - -- [ ] **Step 8: Verify the build fails on the stale images** - -Run: `pnpm build` -Expected: FAIL. `onBrokenLinks: "throw"` plus the missing `/img/app-tour/scan-desktop.png` and the four new pages' images will surface. This is the failing test for Task 4. - -Record the exact errors — they are the checklist Task 4 must clear. - -- [ ] **Step 9: Copy placeholder images so the build can go green** - -The real captures land in Task 4. To keep the tree buildable in the meantime, copy an existing image into each new path: - -```bash -cd static/img/app-tour -for n in scan readers live-feed outputs geofence-defaults; do - cp inventory-desktop.png "${n}-desktop.png" - cp inventory-mobile.png "${n}-mobile.png" -done -git rm home-desktop.png home-mobile.png barcode-desktop.png barcode-mobile.png \ - inventory-desktop.png inventory-mobile.png -``` - -- [ ] **Step 10: Verify the build passes** - -Run: `pnpm build` -Expected: PASS, no broken links. - -- [ ] **Step 11: Verify the redirects were emitted** - -Run: `ls build/app-tour/inventory/index.html build/app-tour/home/index.html build/app-tour/barcode/index.html` -Expected: all three exist. - -Run: `grep -o 'app-tour/scan' build/app-tour/inventory/index.html | head -1` -Expected: `app-tour/scan`. The plugin emits redirects at build time only — they do not exist under `pnpm dev`. - -- [ ] **Step 12: Commit** - -```bash -git add -A -git commit -m "docs(app-tour): restructure tabs for Scan rename and Home/Barcode removal - -Placeholder images stand in until the capture pass replaces them." -``` - ---- - -### Task 3: Local capture environment and seed data - -Stands up platform locally with the reader bridge, creates the docs org, and seeds realistic data. Nothing here touches the docs repo except the checked-in seed script. - -**Files:** -- Create: `scripts/seed-docs-org.sql` - -**Interfaces:** -- Consumes: nothing. -- Produces: a local app at `http://localhost:5173` with an org named `TrakRF Docs`, the `geofence` capability granted, a location tree, and named assets — the fixture every screenshot in Task 4 depends on. - -- [ ] **Step 1: Start the local stack** - -```bash -cd ~/platform -just dev # database + backend + migrations -``` - -Expected: "Development environment ready". - -- [ ] **Step 2: Start the frontend against the reader bridge** - -```bash -cd ~/platform -just frontend dev-bridge -``` - -Expected: the dev server on `http://localhost:5173`, with the bridge host logged. `BLE_MCP_HOST` is inherited from `~/platform/.env.local` via `.envrc` — do not set it again. - -If the bridge is unreachable, the reader is off or `ble-mcp-test` is not running on that host. Screenshots requiring a connected device (Task 4, Steps 8–10) need it; everything else does not. - -- [ ] **Step 3: Confirm the app renders prod-like** - -Open `http://localhost:5173` and check two things: **no purple environment banner**, and the browser tab reads `TrakRF` with no `[DEV]`/`[PRE]` prefix. This is the whole reason captures happen locally — if a banner appears, the backend is injecting `__APP_CONFIG__` and the capture source is wrong. Stop and diagnose before continuing. - -- [ ] **Step 4: Create the account and org through the UI** - -Sign up at `http://localhost:5173/#signup`. Use organization name exactly `TrakRF Docs` — the seed script matches on that string. Record the email and password; they go in `.env` in Task 9. - -Signing up through the UI (rather than inserting rows) is deliberate: `org_users` membership and org defaults get built by the application. - -- [ ] **Step 5: Confirm the local psql invocation** - -```bash -cd ~/platform -just database psql # or: docker compose exec -T postgres psql -U postgres -d trakrf -``` - -Verify the org exists: - -```sql -select id, name from trakrf.organizations where name = 'TrakRF Docs'; -``` - -Expected: exactly one row. If empty, signup did not complete. - -- [ ] **Step 6: Write `scripts/seed-docs-org.sql`** - -Resolves the org by name inside a `DO` block, so no psql variables are needed and the script is re-runnable. - -```sql --- Seeds the TrakRF Docs org used for documentation screenshots. --- Idempotent: safe to re-run. Targets a LOCAL database only. --- Usage: just database psql < scripts/seed-docs-org.sql -\set ON_ERROR_STOP on - -DO $$ -DECLARE - v_org BIGINT; - v_receiving BIGINT; - v_warehouse BIGINT; - v_bay3 BIGINT; - v_office BIGINT; - v_lab BIGINT; -BEGIN - SELECT id INTO v_org - FROM trakrf.organizations - WHERE name = 'TrakRF Docs' AND deleted_at IS NULL; - - IF v_org IS NULL THEN - RAISE EXCEPTION 'Org "TrakRF Docs" not found — sign up through the UI first'; - END IF; - - -- Geofence unlocks the Outputs and Geofence defaults pages. Without it they - -- render as locked upsell pages. Mustering and kitting stay ungranted on - -- purpose: those surfaces are out of scope and must not appear unlocked. - INSERT INTO trakrf.org_capabilities (org_id, capability, granted_at) - VALUES (v_org, 'geofence', now()) - ON CONFLICT DO NOTHING; - - IF EXISTS (SELECT 1 FROM trakrf.assets - WHERE org_id = v_org AND external_key = 'ASSET-CAMERA' - AND deleted_at IS NULL) THEN - RAISE NOTICE 'Seed data already present — skipping'; - RETURN; - END IF; - - SELECT location_id INTO v_receiving FROM trakrf.create_location_with_tags( - v_org, 'LOC-RECEIVING', 'Receiving', 'Inbound dock', - NULL, now(), NULL, TRUE, '{}'::jsonb, '[]'::jsonb); - - SELECT location_id INTO v_warehouse FROM trakrf.create_location_with_tags( - v_org, 'LOC-WAREHOUSE-A', 'Warehouse A', 'Main storage', - NULL, now(), NULL, TRUE, '{}'::jsonb, '[]'::jsonb); - - SELECT location_id INTO v_bay3 FROM trakrf.create_location_with_tags( - v_org, 'LOC-BAY-3', 'Bay 3', 'Warehouse A, bay 3', - v_warehouse, now(), NULL, TRUE, '{}'::jsonb, - '[{"type":"rfid","value":"E280689400005015C9E1A101"}]'::jsonb); - - SELECT location_id INTO v_office FROM trakrf.create_location_with_tags( - v_org, 'LOC-OFFICE', 'Office', 'Front office', - NULL, now(), NULL, TRUE, '{}'::jsonb, '[]'::jsonb); - - SELECT location_id INTO v_lab FROM trakrf.create_location_with_tags( - v_org, 'LOC-LAB', 'Lab', 'Test bench', - NULL, now(), NULL, TRUE, '{}'::jsonb, '[]'::jsonb); - - PERFORM trakrf.create_asset_with_tags(v_org, 'ASSET-CAMERA', 'Camera', - 'Canon EOS R6 body', now(), NULL, TRUE, '{}'::jsonb, - '[{"type":"rfid","value":"E280689400005015C9E1B201"}]'::jsonb); - - PERFORM trakrf.create_asset_with_tags(v_org, 'ASSET-LAPTOP', 'Laptop', - 'ThinkPad X1 Carbon', now(), NULL, TRUE, '{}'::jsonb, - '[{"type":"rfid","value":"E280689400005015C9E1B202"}]'::jsonb); - - PERFORM trakrf.create_asset_with_tags(v_org, 'ASSET-TABLET', 'Tablet', - 'iPad Pro 11-inch', now(), NULL, TRUE, '{}'::jsonb, - '[{"type":"rfid","value":"E280689400005015C9E1B203"}]'::jsonb); - - PERFORM trakrf.create_asset_with_tags(v_org, 'ASSET-PROJECTOR', 'Projector', - 'Epson conference room projector', now(), NULL, TRUE, '{}'::jsonb, - '[{"type":"rfid","value":"E280689400005015C9E1B204"}]'::jsonb); - - PERFORM trakrf.create_asset_with_tags(v_org, 'ASSET-TOOLBOX', 'Toolbox', - 'Field service toolkit', now(), NULL, TRUE, '{}'::jsonb, - '[{"type":"rfid","value":"E280689400005015C9E1B205"}]'::jsonb); - - PERFORM trakrf.create_asset_with_tags(v_org, 'ASSET-PALLET-JACK', 'Pallet Jack', - 'Electric pallet jack', now(), NULL, TRUE, '{}'::jsonb, - '[{"type":"rfid","value":"E280689400005015C9E1B206"}]'::jsonb); - - PERFORM trakrf.create_asset_with_tags(v_org, 'ASSET-MICROSCOPE', 'Microscope', - 'Lab inspection microscope', now(), NULL, TRUE, '{}'::jsonb, - '[{"type":"rfid","value":"E280689400005015C9E1B207"}]'::jsonb); - - PERFORM trakrf.create_asset_with_tags(v_org, 'ASSET-LABEL-PRINTER', 'Label Printer', - 'Zebra ZT411 tag printer', now(), NULL, TRUE, '{}'::jsonb, - '[{"type":"rfid","value":"E280689400005015C9E1B208"}]'::jsonb); - - RAISE NOTICE 'Seeded org % with 5 locations and 8 assets', v_org; -END $$; -``` - -The EPC values are placeholders for display. Step 8 replaces the ones you intend to physically scan with EPCs from real tags. - -- [ ] **Step 7: Run the seed** - -```bash -cd ~/platform && just database psql < ~/trakrf-docs/.claude/worktrees/tra-1045-app-tour-regenerate/scripts/seed-docs-org.sql -``` - -Expected: `NOTICE: Seeded org with 5 locations and 8 assets`. - -- [ ] **Step 8: Align tag values with the physical tags** - -Read the EPCs your bench tags actually broadcast — scan them in the app, or check `~/platform/frontend/tests/e2e/test-utils/constants.ts` for the known test-tag definitions. Update the matching `value` fields in the seed script and re-run after deleting the seeded assets, so scanned reads resolve to named assets instead of showing as unknown EPCs. - -This is what makes the Found/Missing/Extra tiles meaningful: reads that match a registered asset land in Found, registered assets not seen land in Missing, and unregistered EPCs land in Extra. - -- [ ] **Step 9: Verify the seed landed and capabilities are right** - -```sql -select (select count(*) from trakrf.assets a - where a.org_id = o.id and a.deleted_at is null) as assets, - (select count(*) from trakrf.locations l - where l.org_id = o.id and l.deleted_at is null) as locations, - (select string_agg(capability, ',') from trakrf.org_capabilities c - where c.org_id = o.id) as caps - from trakrf.organizations o - where o.name = 'TrakRF Docs'; -``` - -Expected: `assets = 8`, `locations = 5`, `caps = geofence`. If `caps` contains `mustering` or `kitting`, remove them — those surfaces must render locked. - -- [ ] **Step 10: Confirm the gated pages unlocked** - -Reload the app. In the sidebar, **Outputs** and **Geofence defaults** must no longer carry "Not enabled for your organization", while **Mustering** still does. - -- [ ] **Step 11: Commit** - -```bash -git add scripts/seed-docs-org.sql -git commit -m "feat(scripts): add local seed for documentation screenshots" -``` - ---- - -### Task 4: Capture screenshots - -Captures every image from local dev via Playwright MCP. Screenshots land in the repo root by default and must be moved. - -**Files:** -- Create/replace: `static/img/app-tour/{scan,locate,assets,locations,reports,settings,readers,live-feed,outputs,geofence-defaults,help}-{desktop,mobile}.png` -- Replace: `static/img/user-guide/*.png` (every image showing the app) - -**Interfaces:** -- Consumes: the seeded local app from Task 3; the image paths declared in Task 2. -- Produces: real screenshots at every path Task 2's pages reference. - -- [ ] **Step 1: Log in and confirm the capture persona** - -Log into `http://localhost:5173` as the Task 3 account. Open the account menu and confirm it lists exactly one organization and has **no "All Organizations" item**. If that item appears, the account is a superadmin and must not be used — superadmin surfaces must never reach customer documentation. - -- [ ] **Step 2: Set the desktop viewport** - -Resize the browser to 1440×900. - -- [ ] **Step 3: Capture each desktop page** - -For each of `scan, locate, assets, locations, reports, settings, readers, live-feed, outputs, geofence-defaults, help`: navigate to the page, wait 1–2 seconds for it to settle, then screenshot. - -The app's own hash routes are not identical to the doc ids. Confirm each from the sidebar rather than guessing — `Readers` is `#scan-devices` and `Geofence defaults` is `#org-geofence-defaults`, not `#readers` / `#geofence-defaults`. - -Move each capture into place: - -```bash -mv page-*.png static/img/app-tour/-desktop.png -``` - -- [ ] **Step 4: Capture each mobile page** - -Resize to 390×844 and **re-navigate** before each capture — the app's layout depends on viewport size at mount, so resizing alone yields a scaled desktop view. Save as `-mobile.png`. - -- [ ] **Step 5: Verify no image leaks browser chrome** - -Open two or three captures and confirm they show only the app viewport — no address bar, no bookmarks bar, no OS chrome. The image being replaced, `inventory-scanning.png`, leaked a personal bookmarks bar into published docs; do not reintroduce that. - -- [ ] **Step 6: Verify the tile states** - -In `scan-desktop.png`, with no reconcile list loaded, exactly two tiles must be visible: **Scans** and **Assets**. If five appear, a list is loaded — clear it and recapture. - -- [ ] **Step 7: Recapture the user-guide images** - -Replace every `static/img/user-guide/*.png` that shows the app, matching each one's existing purpose (see the alt text in `docs/user-guide/asset-management.md` and `reader-setup.md`): empty and populated Locations, empty and populated Assets, the create-asset modal, the reports page, and device setup. - -Rename the two scanning images to match the new tab name: - -```bash -git mv static/img/user-guide/inventory-connected-idle.png static/img/user-guide/scan-connected-idle.png -git mv static/img/user-guide/inventory-scanning.png static/img/user-guide/scan-scanning.png -``` - -Update the references at `docs/user-guide/asset-management.md:66` and `:78`. - -- [ ] **Step 8: Connect the reader** - -Click **Connect Device** and pair through the bridge. Confirm the device-status chip reads **Connected**. If the bridge is down, skip Steps 8–10 and note in the PR which images still need a hardware pass. - -- [ ] **Step 9: Capture the connected-idle state** - -With the reader connected and the Scanned list empty, capture the Scan page as `static/img/user-guide/scan-connected-idle.png`. - -- [ ] **Step 10: Capture the mid-scan and reconcile states** - -Load the asset list via **Reconcile**, run a scan against the bench tags, and capture: -- `static/img/user-guide/scan-scanning.png` — populated Scanned list with per-row signal and count -- `static/img/app-tour/scan-desktop.png` is the *empty* state and must not be overwritten here - -Confirm all five tiles now render in the order **Scans → Assets → Found → Missing → Extra**. If the Location column reads `-` for scanned assets, the `asset_scan_latest` continuous aggregate has not refreshed — assets carry no `location_id` column, so location is derived from scan history: - -```sql -CALL refresh_continuous_aggregate('trakrf.asset_scan_latest', NULL, NULL); -``` - -- [ ] **Step 11: Verify the build** - -Run: `pnpm build` -Expected: PASS with no broken links. - -- [ ] **Step 12: Commit images separately from prose** - -```bash -git add static/img -git commit -m "feat: recapture app-tour and user-guide screenshots - -Captured from local dev: no environment banner, viewport-only, seeded -data with real asset names." -``` - ---- - -### Task 5: Rewrite `scan.md` from observation - -**Files:** -- Modify: `docs/app-tour/scan.md` - -**Interfaces:** -- Consumes: `scan-desktop.png` / `scan-mobile.png` from Task 4. -- Produces: the tile vocabulary every other page must match. - -- [ ] **Step 1: Observe the page** - -With the app open on the Scan tab, list what is actually present: the RFID/Barcode mode toggle, the toolbar buttons, the location-tag selector, the tile row, and the empty-state copy. Do not copy the old prose forward — it documents a **Sample** button that no longer exists. - -- [ ] **Step 2: Write "What this page does"** - -Two to four sentences naming concrete controls. It must state the toolbar accurately (Start, Reconcile, Clear, the audio toggle, Share, Save — and **no Sample**), name the RFID/Barcode toggle as a mode switch rather than a separate tab, mention the location-tag selector, and give the tiles as **Scans → Assets → Found → Missing → Extra** with the note that only Scans and Assets render until a reconcile list is loaded. - -- [ ] **Step 3: Write "How it fits in the app"** - -One or two sentences connecting Scan to Locate and Assets. Must not use the word "Inventory" to describe scanning. - -- [ ] **Step 4: Remove the first-pass note** - -Delete the `:::note This page was generated as a first-pass tour. Human enhancement welcome. :::` block — this page is no longer a first pass. - -- [ ] **Step 5: Verify the forbidden strings are gone** - -Run: `grep -nE 'Inventory|Barcode tab|Sample|Not Listed|Total Scanned|Saveable' docs/app-tour/scan.md` -Expected: no output. A `Barcode` match is acceptable only where it names the scan mode. - -- [ ] **Step 6: Commit** - -```bash -git add docs/app-tour/scan.md -git commit -m "docs(app-tour): rewrite the Scan page from the shipped UI" -``` - ---- - -### Task 6: Rewrite the remaining existing tour pages - -**Files:** -- Modify: `docs/app-tour/index.md`, `locate.md`, `assets.md`, `locations.md`, `reports.md`, `settings.md`, `help.md` - -**Interfaces:** -- Consumes: tile vocabulary from Task 5; images from Task 4. -- Produces: cross-references the terminology sweep in Task 9 will assert against. - -- [ ] **Step 1: Rewrite each page from observation** - -For each, refresh both sections against the live screen and remove the first-pass `:::note` block. Every cross-reference to the old tabs must go: `assets.md`, `locate.md`, `locations.md`, `reports.md`, and `settings.md` each currently reference Inventory, Home, or Barcode. - -- [ ] **Step 2: Handle `help.md:23` carefully** - -The line maps docs sections to in-app Help questions: "Its questions mirror the scanning tabs (Inventory = My Items, Locate = Find Item)". - -Open the Help tab and read the actual accordion labels. If the in-app copy still says "Inventory", that string lives in the **platform** repo — do not paper over it here. Describe what the in-app Help actually shows and file a platform ticket for the stale copy. Note the ticket number in the PR description. - -- [ ] **Step 3: Verify no page describes a removed tab** - -Run: `grep -rnE '\b(Home|Inventory)\b' docs/app-tour/` -Expected: no output. - -Run: `grep -rn 'Barcode' docs/app-tour/` -Expected: only matches naming the scan mode. - -- [ ] **Step 4: Commit** - -```bash -git add docs/app-tour -git commit -m "docs(app-tour): refresh tour pages against the shipped UI" -``` - ---- - -### Task 7: Author the four new tour pages - -**Files:** -- Modify: `docs/app-tour/readers.md`, `live-feed.md`, `outputs.md`, `geofence-defaults.md` - -**Interfaces:** -- Consumes: the `geofence` grant from Task 3 (without it, two of these pages render as locked upsell pages and cannot be documented). -- Produces: nothing downstream. - -- [ ] **Step 1: Confirm the pages render unlocked** - -All four must show real UI, not "This feature isn't enabled for your organization." If Outputs or Geofence defaults still show the upsell, the grant from Task 3 did not apply — fix that before writing. - -- [ ] **Step 2: Write each page** - -Replace the placeholder "What this page does" with 2–4 sentences of live observation, then add a "How it fits in the app" section, matching the structure of the existing tour pages. - -For `geofence-defaults.md`, the app's own nav tooltip describes the surface as org-wide geofence tuning — RSSI, age-out, auto-off, and mode — applied to every portal unless an output overrides it. Verify each of those controls exists on the page before describing it. - -- [ ] **Step 3: Note the capability gate on the gated pages** - -Outputs and Geofence defaults are capability-gated. Each page must state plainly that the feature is enabled per organization and that orgs without it see a locked page directing them to support — a reader whose sidebar shows a padlock needs to know that is expected, not broken. - -Do not name Mustering or Kits anywhere; both are out of scope. - -- [ ] **Step 4: Verify the build** - -Run: `pnpm build` -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add docs/app-tour -git commit -m "docs(app-tour): document Readers, Live feed, Outputs, and Geofence defaults" -``` - ---- - -### Task 8: Author the organization-management page - -**Files:** -- Modify: `docs/user-guide/organization-management.md` - -**Interfaces:** -- Consumes: the local app and account from Task 3. -- Produces: nothing downstream. - -- [ ] **Step 1: Observe the account-menu surface** - -Open the account menu and walk each item: Organization Settings, Members, API Keys, and Create Organization. - -- [ ] **Step 2: Replace the placeholder** - -The page is currently a 13-line "Coming Soon" stub. Replace it entirely with sections covering those four surfaces, written from observation, keeping the existing `sidebar_position: 5`. - -- [ ] **Step 3: Exclude Webhooks** - -The account menu also contains **Webhooks**. Leave it undocumented — it belongs to the separate webhooks docs ticket. Do not mention it, and do not leave a "coming soon" stub in its place. - -- [ ] **Step 4: Keep the API framing correct** - -The users and org-admin *API* surfaces are internal-only. This page documents the UI. Do not imply a public API exists for managing members or organizations, and do not link to API reference pages for these operations. - -- [ ] **Step 5: Verify the placeholder is gone** - -Run: `grep -n 'Coming Soon' docs/user-guide/organization-management.md` -Expected: no output. - -- [ ] **Step 6: Commit** - -```bash -git add docs/user-guide/organization-management.md -git commit -m "docs(user-guide): document organization management" -``` - ---- - -### Task 9: Terminology sweep and authoring workflow - -**Files:** -- Modify: `docs/user-guide/asset-management.md`, `docs/getting-started/ui.mdx`, `docs/app-tour/authoring.md`, `scripts/refresh-screenshots.sh`, `.env.example` - -**Interfaces:** -- Consumes: the tile vocabulary from Task 5. -- Produces: a repo that satisfies Task 10's acceptance greps. - -- [ ] **Step 1: Fix `docs/user-guide/asset-management.md`** - -Five distinct edits: -- `:4` — the description reads "save an inventory". Reword; this is the common-noun collision the reserve rule targets. -- `:9` — "running a scan session on the **Inventory** screen" → the **Scan** screen. -- `:57` — "show the raw EPC under 'Not Listed'" → under **Extra**. -- `:64` — "Open **Inventory** from the left nav" → **Scan**. -- `:75-77` — the three tile definitions change: **Not Listed** → **Extra** ("tags seen that weren't on the list"), **Total Scanned** → **Scans** ("unique EPCs seen this session"), **Saveable** → **Assets** ("of those, how many match a registered asset"). - -Also fix `:110` and `:114`, which use "Inventory" and "an inventory scan" to mean scanning. - -- [ ] **Step 2: Fix `docs/getting-started/ui.mdx`** - -- `:34` — "You'll land on the **Home** dashboard — a device-status summary, links to **Inventory / Locate / Barcode**, and a 'Watch Demo' card." The Home dashboard no longer exists; signing in lands on **Scan**. Rewrite against the live app. -- `:55` — "unregistered tags show up under **Not Listed**" → under **Extra**. -- `:60` — "Open **Inventory** (left nav)" → **Scan**. - -- [ ] **Step 3: Leave `docs/getting-started/index.md:24` alone** - -It reads "integrating TrakRF into existing systems (inventory, ERP, custom dashboards)" — the customer's own systems, not a TrakRF surface. This usage is explicitly permitted. - -- [ ] **Step 4: Update `scripts/refresh-screenshots.sh`** - -The `TABS` array on line 24 lists the old tabs. Replace with the current set, and update the login/capture flow to match the new capture story — the script drives Rodney against `app.preview.trakrf.id`, which is no longer where screenshots come from. - -Either repoint it at `http://localhost:5173` with the local account, or delete it and let `authoring.md` describe the local flow. If you delete it, remove its mention from `authoring.md` too. Do not leave a script that silently captures from the wrong environment. - -- [ ] **Step 5: Rewrite `docs/app-tour/authoring.md`** - -It documents a workflow that no longer applies. It must now cover: the platform checkout and Docker prerequisites, `just dev` and `just frontend dev-bridge`, the bridge host coming from `~/platform/.env.local`, signing up to create the `TrakRF Docs` org, running `scripts/seed-docs-org.sql`, the `geofence` grant and why Mustering stays locked, capturing viewport-only at both viewports, and the requirement to run the platform commit that matches production. - -Record which capture path was used — the reader bridge or simulated notifications. - -Keep the two existing warnings that still apply: Playwright MCP writes screenshots to the working directory, and credentials stay local and uncommitted. - -- [ ] **Step 6: Update `.env.example`** - -The docs-tour preview credentials no longer drive screenshots. Replace them with the local-capture variables, keeping `TRAKRF_INFRA_DIR` from Task 1, and mirror the change into `.env`. - -- [ ] **Step 7: Verify the sweep** - -Run: `grep -rnE 'Not Listed|Total Scanned|Saveable' docs/` -Expected: no output. - -Run: `grep -rniE '\binventory\b' docs/ --include='*.md' --include='*.mdx' | grep -v 'docs/api/'` -Expected: only `docs/getting-started/index.md:24`. - -- [ ] **Step 8: Commit** - -```bash -git add docs scripts .env.example -git commit -m "docs: retire Inventory/Barcode/Home vocabulary and rewrite the authoring flow" -``` - ---- - -### Task 10: Acceptance verification and PR - -**Files:** none modified unless a check fails. - -**Interfaces:** -- Consumes: everything above. - -- [ ] **Step 1: Clean build** - -```bash -pnpm clear && pnpm build -``` - -Expected: PASS, no broken-link errors. `onBrokenLinks: "throw"` means any dangling reference fails the build. - -- [ ] **Step 2: Run every acceptance grep** - -```bash -grep -rnE 'Not Listed|Total Scanned|Saveable' docs/ ; echo "tiles: $?" -grep -rnE '\b(Home|Inventory)\b' docs/app-tour/ ; echo "tabs: $?" -grep -rn 'TRA-[0-9]' docs/ ; echo "ticket refs: $?" -``` - -Expected: each prints `1` (grep found nothing). Any `0` means a violation remains. - -- [ ] **Step 3: Verify redirects** - -```bash -for p in inventory home barcode; do - test -f "build/app-tour/$p/index.html" && grep -q 'app-tour/scan' "build/app-tour/$p/index.html" \ - && echo "$p OK" || echo "$p FAILED" -done -``` - -Expected: three `OK` lines. - -- [ ] **Step 4: Verify no image leaks personal data** - -Open every changed file under `static/img/` and confirm none shows browser chrome, bookmarks, an email address other than the docs account, or a superadmin menu. - -- [ ] **Step 5: Serve and eyeball** - -```bash -pnpm serve -``` - -Walk the App Tour sidebar top to bottom. Confirm the Settings category expands to the four sub-pages, every image loads, and no page describes a control that is not in its screenshot. - -- [ ] **Step 6: Push and open the PR** - -```bash -git push -u origin docs/tra-1045-app-tour-regenerate -gh pr create --title "docs: propagate Scan rename, Home/Barcode removal, and tile relabels" --body "$(cat <<'EOF' -Regenerates the App Tour against the shipped UI. - -## Do not merge yet - -Blocked on the prod release. Docs must not describe a UI users don't have. -Preview serves open PRs, so this is reviewable on the preview docs site now. - -## Beyond the original scope - -- The `Sample` button was documented but no longer exists -- The Scan tab's location-tag selector was undocumented -- `inventory-scanning.png` published a personal bookmarks bar -- Every app image carried a purple "Preview Environment" banner - -Screenshots now come from local dev: no banner, viewport-only, seeded data -with real asset names. Nothing was written to the preview database. - -## Excluded - -Mustering, Kits, and Webhooks. - -🤖 Generated with [Claude Code](https://claude.com/claude-code) -EOF -)" -``` - -- [ ] **Step 7: Do not merge** - -Leave the PR open until the prod release lands. Confirm in the PR that the branch is not set to auto-merge. - ---- - -## Self-Review - -**Spec coverage:** page map (Task 2), redirects (Task 2), terminology rules (Tasks 5, 6, 9), screenshots and capture source (Tasks 3, 4), seeded org and grant (Task 3), org management (Task 8), infra passthrough (Task 1), verification (Task 10), sequencing (Task 10 Step 7). The spec's note about the local version string is folded into Task 4's observation steps. - -**Known gaps, deliberately left to execution:** the app's hash routes for the four new pages are confirmed only for Readers (`#scan-devices`) and Geofence defaults (`#org-geofence-defaults`); Task 4 Step 3 instructs reading the rest from the sidebar rather than guessing. Prose is written from live observation, so the plan specifies what each page must state rather than supplying final copy. - -**Out of scope, file separately:** `PG_URL_PREVIEW` in `~/platform/.env.local` still points at Timescale Cloud and refuses connections since preview moved to CNPG on GKE. diff --git a/superpowers/plans/2026-07-30-tra-1048-webhooks-v1-docs.md b/superpowers/plans/2026-07-30-tra-1048-webhooks-v1-docs.md deleted file mode 100644 index f6377ce..0000000 --- a/superpowers/plans/2026-07-30-tra-1048-webhooks-v1-docs.md +++ /dev/null @@ -1,596 +0,0 @@ -# TRA-1048 Webhooks v1 Documentation — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Replace the inaccurate "Planned for v1.x" webhooks page with hand-written documentation of the webhooks that actually shipped, and purge stale "planned" webhook language from the rest of the docs. - -**Architecture:** One rewritten page (`docs/api/webhooks.md`) carries the whole feature — payload, headers, signature verification, delivery semantics, endpoint requirements, and the in-app registration walkthrough. The walkthrough lives on the API page rather than in the app tour because `docs/api/authentication.md` already sets that precedent for API-key minting. Four other pages get stale claims corrected, one nav entry moves, one changelog entry is added. Screenshots follow in a separate commit so the prose can merge the instant the platform deploy lands. - -**Tech Stack:** Docusaurus 3, Markdown/MDX, pnpm, Prettier. - -## Global Constraints - -- **pnpm exclusively.** Never `npm` or `npx`. `npx` → `pnpm dlx`. -- **No documented field or behavior that is not in the shipped code.** Where the ticket text and the platform code disagree, the code wins. -- **No Linear ticket references** (`TRA-NNN`) in any published prose under `docs/`. -- **Nothing from Phase 2 documented as "coming soon" with specifics** — no event-type names, payload shapes, or dates for unshipped work. -- **`onBrokenLinks: "throw"`** in `docusaurus.config.ts` — a broken internal link fails the build. `pnpm build` is the link test. -- **Prettier formats embedded code blocks in Markdown.** Do not hand-align JSON or table columns; write it plainly and let `pnpm lint:fix` normalize. Always run `pnpm lint:fix` before committing. -- **Do not merge.** This branch is held open deliberately until the production platform deploy. Push and open the PR; never merge. -- Conventional commits; never squash; branch is `docs/tra-1048-webhooks-v1`. - -## File Structure - -| File | Responsibility | Change | -| --- | --- | --- | -| `docs/api/webhooks.md` | The entire webhooks feature, for an integrator writing a receiver | Rewrite (full replace) | -| `docs/api/README.mdx` | API section index; one-line link descriptions | Modify line 39 | -| `docs/api/resource-identifiers.md` | Scan-event vocabulary; forward reference to webhooks | Modify line 709 | -| `docs/api/versioning.md` | Open/closed enum policy; uses a scope name as an example | Modify line 66 | -| `docs/api/private-endpoints.md` | Catalog of session-auth internal routes | Add 6 table rows | -| `docs/api/changelog.md` | Integrator-facing change record | Add one `###` entry | -| `sidebars.ts` | API sidebar ordering | Move one array element | - ---- - -### Task 1: Rewrite the webhooks page - -The substance of the work. Everything else is cleanup. - -**Files:** -- Modify: `docs/api/webhooks.md` (full replace of lines 1–59) -- Test: `pnpm build` (link checking), `pnpm lint` - -**Interfaces:** -- Consumes: nothing from earlier tasks. -- Produces: the heading anchors `#what-fires-an-event`, `#registering-a-webhook`, `#the-payload`, `#headers`, `#verifying-the-signature`, `#delivery-semantics`, `#endpoint-requirements`, `#reconciling-missed-events`, and `#events-went-quiet`. Task 2 and Task 3 link to `./webhooks` only (no anchors), so no cross-task anchor dependency exists — but do not rename these headings without updating this plan. - -**Frontmatter note:** Preserve the existing frontmatter block (`sidebar_position: 3`) verbatim. `sidebars.ts` lists `apiSidebar` items explicitly, so `sidebar_position` is inert for this page; changing it is churn with no rendered effect. Task 3 handles the real reorder. - -- [ ] **Step 1: Replace the file contents** - -Write `docs/api/webhooks.md` with exactly this content: - -````markdown ---- -sidebar_position: 3 ---- - -# Webhooks - -TrakRF can push an event to an HTTPS endpoint you host whenever an asset moves to a different location — the push alternative to polling [`GET /api/v1/reports/asset-locations`](/api) on a schedule. - -One event type ships in v1: `asset.moved`. - -Webhooks are registered from the app rather than the API, because registering one is an organization-settings action rather than something an integrator's API key does. There is no public API for webhook management. - -## What fires an event - -An `asset.moved` event fires when an asset is scanned at a location **different from the one it was last seen at**. That is the whole rule, and it has a consequence worth internalizing before you write a receiver: - -**Rescans at the same location send nothing.** A fixed reader that sees a stationary tag hundreds of times an hour produces zero events. This is a movement feed — not a scan feed, and not a heartbeat. Silence means nothing has moved, not that the pipeline is down. - -The volume difference is substantial. On the TrakRF demo carousel, 815 scans produced 8 events. - -A scan that carries no location produces no event. - -If you need the full scan stream rather than movements, poll [`GET /api/v1/assets/{asset_id}/history`](/api) instead. - -## Registering a webhook - -Open the **Account menu → Webhooks** (directly below API Keys), or go to **Organization Settings → Manage webhooks →**. The **admin** role is required. - -v1 supports **one webhook per organization**. - -### The signing secret is shown exactly once - -Creating a webhook generates a signing secret and displays it once, on the create confirmation. Secrets carry a `whsec_` prefix: - -``` -whsec_1f0c1d4e7a2b9c8d3e6f5a0b4c7d2e9f8a1b6c3d0e5f2a7b4c9d6e3f0a5b2c7d -``` - -There is no way to read it back. Every later view shows a masked form — `whsec_…2c7d` — which is enough to tell two secrets apart and useless for signing. - -**There is no secret rotation in v1.** If you lose the secret, the only recovery is to delete the webhook and create a new one, which issues a fresh secret and requires updating your receiver. Copy it into your secrets store before leaving the page. - -### Send test event - -**Send test event** delivers a synthetic `asset.moved` to your endpoint and reports the HTTP status code it returned. Use it to prove your receiver and your signature verification work before any real asset moves. - -The test payload uses obviously-fake identifiers — an `external_key` of `TEST-ASSET`, moving from "Test Origin" to "Test Destination" — so a test fire cannot be mistaken for real movement in your system. - -### The enable toggle - -Turning a webhook off stops delivery without deleting the registration or changing the secret. Events that occur while it is off are dropped, not queued — turning it back on does not replay them. - -## The payload - -Delivered as `POST` with `Content-Type: application/json`. - -```json -{ - "event": "asset.moved", - "delivery_id": "9f2c8e14-0000-0000-0000-000000000001", - "occurred_at": "2026-07-27T16:26:26.412Z", - "data": { - "asset": { - "id": 3947115820491, - "external_key": "FORK-7", - "name": "Forklift 7" - }, - "from_location": { "id": 1882094551203, "name": "Receiving" }, - "to_location": { "id": 2740518866937, "name": "Bay 3" } - } -} -``` - -| Field | Notes | -| --- | --- | -| `event` | Always `asset.moved` in v1. Match on it anyway — more types will be added, and an unknown value should be ignored rather than treated as an error. | -| `delivery_id` | UUID identifying one detected move. Stable across the retries of a single delivery — use it as your idempotency key. | -| `occurred_at` | The scan instant that produced the move, in server time (RFC 3339, UTC). **Order by this**, not by arrival. | -| `data.asset.id` | The surrogate `id`, identical to the one the REST API returns for that asset. | -| `data.asset.external_key` | Your natural key for the asset. | -| `data.from_location` | The location the asset was last seen at, or `null` for a genuine first-ever sighting. | -| `data.to_location` | The location it was just scanned at. Never null. | - -Three things about the shape: - -- **`from_location` is present and explicitly `null`** on a first sighting. The key is never omitted, so a receiver can read it unconditionally. -- **Ids are large.** The surrogate `id` is an opaque integer up to 2⁵², so store it in a 64-bit integer type — a 32-bit field will overflow. Treat it as opaque: don't parse it, order by it, or infer a count or creation time from it. Join your own systems on `external_key`. See [ID format](./id-format) and [Resource identifiers](./resource-identifiers#numeric-id-is-a-surrogate-key). -- **The payload is logical data only.** No scan point, no antenna, no EPC, no scan-event id — the physical layer stays internal. If you need physical provenance, it isn't here and won't be. - -There is **no `sequence` field**, deliberately. See [Delivery semantics](#delivery-semantics). - -## Headers - -| Header | Value | -| --- | --- | -| `X-TrakRF-Event` | `asset.moved` | -| `X-TrakRF-Delivery` | UUID, stable across the retries of one delivery. Matches `delivery_id` in the body. | -| `X-TrakRF-Timestamp` | Unix seconds — the signed timestamp. | -| `X-TrakRF-Signature` | `sha256=` | -| `User-Agent` | `TrakRF-Webhooks/1` | - -## Verifying the signature - -Every delivery is signed with the webhook's secret. Verify it on every request — an unverified endpoint will accept anything anyone posts to it. - -The signature is **HMAC-SHA256 over `timestamp + "." + rawBody`**, hex-encoded and prefixed `sha256=`, where `timestamp` is the `X-TrakRF-Timestamp` header value. - -:::warning Sign the raw body, not a re-serialized parse - -You must compute the HMAC over the **exact bytes** of the request body. Parsing the JSON and re-encoding it changes whitespace and key order, which changes the hash, and verification will fail for every delivery. - -Most frameworks discard the raw body once they have parsed it. Capture it first — Express needs `express.raw()` or a `verify` hook on the JSON parser; Flask needs `request.get_data()`; Rails needs `request.raw_post`. This is by a wide margin the most common webhook integration mistake. - -::: - -### Node - -```js -const crypto = require("crypto"); - -function verify(rawBody, headers, secret) { - const ts = headers["x-trakrf-timestamp"]; - const expected = - "sha256=" + - crypto - .createHmac("sha256", secret) - .update(ts + "." + rawBody) - .digest("hex"); - const got = headers["x-trakrf-signature"]; - return ( - got.length === expected.length && - crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected)) - ); -} -``` - -### Python - -```python -import hashlib, hmac - -def verify(raw_body: bytes, headers, secret: str) -> bool: - ts = headers["X-TrakRF-Timestamp"] - expected = "sha256=" + hmac.new( - secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256 - ).hexdigest() - return hmac.compare_digest(headers["X-TrakRF-Signature"], expected) -``` - -Both compare in constant time — `crypto.timingSafeEqual` and `hmac.compare_digest`. A plain `==` returns as soon as it finds a differing byte, and the timing of those failures leaks how much of a guessed signature was correct, which is enough to reconstruct a valid one over many attempts. Note that `timingSafeEqual` throws on length mismatch, hence the length check before it. - -### Check the timestamp too - -The timestamp is inside the signed material so that a captured delivery cannot be replayed indefinitely. Reject a delivery whose `X-TrakRF-Timestamp` falls outside a tolerance window — five minutes is a reasonable default — in addition to checking the signature. - -**The signed timestamp is `occurred_at`, not the moment of transmission.** It is the scan instant, and it does not change when a delivery is retried. Your tolerance window is therefore measured from the physical scan, and it has to accommodate the pipeline latency between the scan and the delivery attempt, not just network time. Don't set it so tight that a normally-delayed event fails. - -## Delivery semantics - -Document these honestly in your own runbook, because they constrain what you can build. - -**At-most-once.** A failed delivery is retried after roughly one second and roughly five seconds — both jittered — and then dropped. There is no dead-letter queue and no replay in v1. **You will eventually miss an event.** Design the receiver so a missed movement is recoverable, and see [Reconciling missed events](#reconciling-missed-events). - -**Order is not guaranteed.** Concurrent scans can deliver out of order. Order by `occurred_at`, never by arrival. There is deliberately no `sequence` field — a counter would imply an ordering guarantee TrakRF does not provide. - -**Duplicates are possible.** If your endpoint processes a request but responds too slowly, the delivery is retried and you will see the same event twice. Deduplicate on `delivery_id`, which is stable across the retries of one delivery. - -**Expect a 2xx.** Any other status counts as a failure and burns a retry. A 3xx is a failure too — see below. - -**The timeout is 5 seconds**, covering the whole request. Acknowledge immediately and do your real work asynchronously; a slow handler converts into duplicate deliveries and then into dropped events. - -**Delivery stops when a subscription lapses.** Webhook delivery is an outbound request, so nothing in your integration will report an authorization failure — the events simply stop. Skipped events are dropped, not buffered: reinstating the subscription resumes new deliveries but does not replay the gap. - -## Endpoint requirements - -- **HTTPS only.** A plain `http` target is rejected — it would expose both the payload and the signature. -- **Publicly reachable.** Targets that resolve into private address space are refused: loopback, RFC 1918, link-local (including the cloud metadata address), unique-local IPv6, and CGNAT. -- **Redirects are not followed.** A 302 is a failed delivery, not a hop. Register the final URL. - -The address check runs against the **resolved IP on every connection**, not against the URL at registration time. Registration validates only the scheme and host, so a URL pointing at an internal host registers cleanly and then fails on every delivery. If you are testing against something on your own network, that is the symptom you will see — the fix is a publicly reachable endpoint or a tunnel, not a support ticket. - -## Reconciling missed events - -At-most-once delivery makes periodic reconciliation part of a correct integration rather than a fallback. Poll [`GET /api/v1/reports/asset-locations`](/api) on a slow schedule — hourly is usually enough — and correct any asset whose location disagrees with what your webhook-derived state believes. - -This costs little: the report is a current-state snapshot rather than a replay of the scan stream, and it is the same endpoint you would have polled continuously without webhooks. Webhooks reduce the polling interval from seconds to hours; they do not eliminate the need to poll. - -See [Authentication](./authentication) for how to authenticate that call. - -## Events went quiet - -In rough order of likelihood: - -1. **Nothing moved.** Delivery is delta-only. Confirm against [`GET /api/v1/reports/asset-locations`](/api) that assets have actually changed location. -2. **The webhook is disabled.** Check the enable toggle on the Webhooks screen. -3. **The subscription lapsed.** Delivery stops for an organization that is no longer entitled, and nothing in your integration surfaces it. Check billing. -4. **Your endpoint started failing.** Use **Send test event** — it reports the status code your endpoint returned. Anything but a 2xx, including a redirect, is a failed delivery. -5. **The scans carry no location.** Reads that resolve to no location produce no event. - -## Not in v1 - -Multiple subscriptions per organization, per-event filters, additional event types, a longer retry ladder, a dead-letter queue, per-delivery logs, secret rotation, and IP allowlisting are all recognized gaps rather than settled designs. If one of them blocks your integration, [email support](mailto:support@trakrf.id) — real customer demand is what drives scheduling. -```` - -- [ ] **Step 2: Format and verify no stale language survives** - -```bash -pnpm lint:fix -grep -in "at-least-once\|asset.scanned\|location.entered\|location.exited\|delivery log\|not available yet\|Planned for" docs/api/webhooks.md -``` - -Expected: the `grep` prints **nothing** (exit status 1). Every hit is stale roadmap language that must not survive the rewrite. - -- [ ] **Step 3: Verify the build and link checking pass** - -```bash -pnpm build -``` - -Expected: build succeeds. `onBrokenLinks: "throw"` means any broken internal link — `./id-format`, `./resource-identifiers#numeric-id-is-a-surrogate-key`, `./authentication`, `/api` — fails here. If `./resource-identifiers#numeric-id-is-a-surrogate-key` errors, confirm the anchor with `grep -n "numeric-id-is-a-surrogate-key" docs/api/resource-identifiers.md` and correct the link rather than removing it. - -- [ ] **Step 4: Commit** - -```bash -git add docs/api/webhooks.md -git commit -m "docs(api): document webhooks v1 as shipped - -The page described a roadmap feature with four event types, -at-least-once delivery, a 24-hour retry ladder, and a delivery log. -What shipped is delta-only asset.moved, at-most-once, two jittered -retries, and no log. Rewritten against the implementation. - -Co-Authored-By: Claude Opus 5 (1M context) " -``` - ---- - -### Task 2: Correct stale webhook claims on the other API pages - -Three pages describe webhooks as unshipped; one omits the routes entirely. A reviewer could reasonably accept Task 1 and reject this, so it commits separately. - -**Files:** -- Modify: `docs/api/README.mdx:39` -- Modify: `docs/api/resource-identifiers.md:709` -- Modify: `docs/api/versioning.md:66` -- Modify: `docs/api/private-endpoints.md` (endpoint list table, after line 33) -- Test: `pnpm build`, `pnpm lint` - -**Interfaces:** -- Consumes: the rewritten `docs/api/webhooks.md` from Task 1. Links target `./webhooks` with no anchor. -- Produces: nothing later tasks depend on. - -- [ ] **Step 1: Update the API index link description** - -In `docs/api/README.mdx`, replace line 39: - -```markdown -- **[Webhooks](./webhooks)** — status of outbound delivery and the interim polling pattern -``` - -with: - -```markdown -- **[Webhooks](./webhooks)** — outbound `asset.moved` delivery, signature verification, and delivery guarantees -``` - -- [ ] **Step 2: Move the resource-identifiers reference to present tense** - -In `docs/api/resource-identifiers.md`, replace line 709: - -```markdown -When [webhooks](./webhooks) ship, events will fire on scan events but the payloads address **assets and locations**, not scan events directly — there's no scan-event id to subscribe to or look up. An ingestor planning a scan-driven workflow should think in terms of asset history and current location, not in terms of a scan-event resource. -``` - -with: - -```markdown -[Webhooks](./webhooks) follow the same rule: an `asset.moved` event is triggered by a scan, but its payload addresses **assets and locations**, not scan events directly — there's no scan-event id to subscribe to or look up. An ingestor planning a scan-driven workflow should think in terms of asset history and current location, not in terms of a scan-event resource. -``` - -- [ ] **Step 3: Replace the misleading scope example in versioning** - -`versioning.md` offers `webhooks:write` as a hypothetical future scope. Webhook management shipped with no scope at all — it is session-authenticated and admin-gated — so the example now implies a public surface that does not exist. - -In `docs/api/versioning.md` line 66, replace this fragment: - -```markdown -TrakRF may introduce new scopes (e.g. `reports:read`, `webhooks:write`) in any v1 release -``` - -with: - -```markdown -TrakRF may introduce new scopes (e.g. `reports:read`, `alarms:write`) in any v1 release -``` - -Leave the rest of line 66 untouched. - -- [ ] **Step 4: Add the webhook routes to the private-endpoints catalog** - -In `docs/api/private-endpoints.md`, insert these six rows into the endpoint list table immediately after the `/api/v1/assets/bulk` row (line 32) and before the `/api/v1/orgs/me` row (line 33). Do not hand-align the columns; Prettier reflows the table. - -```markdown -| `/api/v1/webhooks` | GET, POST | SPA webhook registration (admin) | Internal | Internal | -| `/api/v1/webhooks/{webhook_id}` | GET, PATCH, DELETE | SPA webhook management (admin) | Internal | Internal | -| `/api/v1/webhooks/{webhook_id}/test` | POST | SPA **Send test event** (admin) | Internal | Internal | -``` - -Then add this paragraph immediately after the table (before the `## Response shape: /orgs/me` heading on line 35): - -```markdown -Webhook management is internal by design rather than by omission: registering a delivery endpoint is an organization-settings action performed by an admin in the browser, not something an integrator's API key does. The delivery side — the payload TrakRF posts to your endpoint, and how to verify it — is fully documented in [Webhooks](./webhooks). -``` - -- [ ] **Step 5: Format, then verify no "planned webhooks" language survives anywhere** - -```bash -pnpm lint:fix -grep -rin "when webhooks ship\|webhooks:write\|interim polling" docs/ -``` - -Expected: the `grep` prints **nothing**. - -- [ ] **Step 6: Verify the build passes** - -```bash -pnpm build -``` - -Expected: build succeeds, no broken links. - -- [ ] **Step 7: Commit** - -```bash -git add docs/api/README.mdx docs/api/resource-identifiers.md docs/api/versioning.md docs/api/private-endpoints.md -git commit -m "docs(api): retire pre-ship webhook language across the API pages - -Present-tense the resource-identifiers forward reference, rewrite the -index link description, drop webhooks:write as a hypothetical scope -example now that webhook CRUD shipped without one, and add the six -session-auth webhook routes to the private-endpoints catalog. - -Co-Authored-By: Claude Opus 5 (1M context) " -``` - ---- - -### Task 3: Move the nav entry and record the change - -Webhooks sits in the meta/tools tail of the sidebar because it was a status page. It is a capability now and belongs with the functional reference. - -**Files:** -- Modify: `sidebars.ts:55` -- Modify: `docs/api/changelog.md` (insert after line 12) -- Test: `pnpm build`, `pnpm typecheck`, `pnpm lint` - -**Interfaces:** -- Consumes: `docs/api/webhooks.md` from Task 1 — the doc id `api/webhooks` must still resolve. -- Produces: nothing. - -- [ ] **Step 1: Move `api/webhooks` in the API sidebar** - -In `sidebars.ts`, the `apiSidebar` items array currently reads: - -```ts - "api/rate-limits", - "api/design-notes", - "api/versioning", - "api/changelog", - "api/webhooks", - "api/postman", - "api/private-endpoints", -``` - -Change it to place `api/webhooks` directly after `api/rate-limits`, closing the functional-reference run before the meta pages: - -```ts - "api/rate-limits", - "api/webhooks", - "api/design-notes", - "api/versioning", - "api/changelog", - "api/postman", - "api/private-endpoints", -``` - -- [ ] **Step 2: Add the changelog entry** - -Entries in `docs/api/changelog.md` sit under `## v1.0 — Launch (TBD)` newest-first. Insert this as the new top entry — after the section's intro paragraph (line 12) and immediately before the existing `### \`id\` is globally unique and opaque…` heading on line 14: - -```markdown -### Webhooks: outbound `asset.moved` delivery {#webhooks-asset-moved} - -TrakRF can now push an event to an HTTPS endpoint you host when an asset is scanned at a location different from the one it was last seen at. One event type (`asset.moved`), one webhook per organization, registered from the app's **Account menu → Webhooks** rather than through the API — webhook management is an admin browser action and has no public API surface. This is additive; nothing about the existing polling endpoints changes. - -- **Delivery is at-most-once and delta-only.** A failed delivery is retried at roughly one and five seconds, jittered, then dropped — no dead-letter queue and no replay. Rescans at an unchanged location emit nothing at all, so this is a movement feed rather than a scan feed. Receivers must tolerate a missed event and reconcile periodically against `GET /api/v1/reports/asset-locations`. -- **Ordering is not guaranteed and there is no `sequence` field.** Concurrent scans can deliver out of order; order by `occurred_at`. Duplicates are possible on the retry path — deduplicate on `delivery_id`. -- **Every delivery is signed.** `X-TrakRF-Signature` carries an HMAC-SHA256 over `timestamp + "." + rawBody`, hex-encoded, with the signed timestamp in `X-TrakRF-Timestamp` so a captured delivery cannot be replayed indefinitely. The signature must be computed over the raw request bytes, not a re-serialized parse. See [Webhooks](./webhooks). -``` - -- [ ] **Step 3: Format and verify the build, types, and nav** - -```bash -pnpm lint:fix -pnpm typecheck -pnpm build -``` - -Expected: all three succeed. `typecheck` catches a malformed `sidebars.ts`; `build` catches a doc id that no longer resolves. - -- [ ] **Step 4: Confirm the sidebar renders in the new order** - -```bash -grep -A9 '"api/rate-limits"' sidebars.ts -``` - -Expected: `"api/webhooks"` appears on the line immediately after `"api/rate-limits"`, and appears exactly once in the file. - -- [ ] **Step 5: Commit** - -```bash -git add sidebars.ts docs/api/changelog.md -git commit -m "docs(api): promote webhooks into the reference nav, log the change - -Webhooks sat between changelog and postman because it was a roadmap -status page. It is a shipped capability now, so it moves up to close -the functional reference run after rate-limits. - -Co-Authored-By: Claude Opus 5 (1M context) " -``` - ---- - -### Task 4: Screenshots (follow-up commit — do not block the PR on this) - -Deferred deliberately. Tasks 1–3 must be mergeable the moment the platform production deploy lands; these captures need a working preview session and can arrive afterward on the same PR. - -**Files:** -- Create: `static/img/webhooks-empty-state.png` -- Create: `static/img/webhooks-secret-reveal.png` -- Create: `static/img/webhooks-test-result.png` -- Modify: `docs/api/webhooks.md` (embed the three images) -- Test: `pnpm build` - -**Interfaces:** -- Consumes: the `## Registering a webhook`, `### The signing secret is shown exactly once`, and `### Send test event` sections from Task 1. -- Produces: nothing. - -**Prerequisite:** confirm with the operator that a preview SPA session is available for an admin-role org before starting. Do not attempt credential recovery unprompted. - -- [ ] **Step 1: Confirm the existing screenshot convention** - -```bash -ls static/img/ | head -30 -grep -rn "static/img\|!\[" docs/app-tour/assets.md | head -10 -``` - -Match whatever path form and alt-text style the app-tour pages already use. Do not invent a new convention. - -- [ ] **Step 2: Capture the three screenshots against preview** - -Use the Playwright MCP browser at a 1680x900 desktop viewport, matching the app-tour captures. - -1. **Empty state** — Account menu → Webhooks with no webhook registered. -2. **Secret reveal** — the create confirmation showing the reveal-once secret banner. -3. **Test-fire result** — the status-code result after **Send test event**. - -**Create a throwaway webhook for this and delete it afterward.** Never capture a real signing secret. If the reveal-once banner shows a live secret in the frame, redact it before saving the file — the whole point of the capture is the banner, not the value. - -- [ ] **Step 3: Embed the images** - -Add each image directly beneath the section it illustrates in `docs/api/webhooks.md` — empty state under `## Registering a webhook`, secret banner under `### The signing secret is shown exactly once`, test result under `### Send test event`. Write descriptive alt text stating what the screen shows, not "screenshot of webhooks". - -- [ ] **Step 4: Verify the build resolves the image paths** - -```bash -pnpm lint:fix -pnpm build -``` - -Expected: build succeeds with no missing-asset warnings. - -- [ ] **Step 5: Confirm no live secret was committed** - -```bash -git diff --cached --stat -grep -rn "whsec_" docs/ | grep -v "whsec_1f0c1d4e\|whsec_…" -``` - -Expected: the `grep` prints nothing. The only `whsec_` strings in the docs are the illustrative example and the masked form from Task 1. - -- [ ] **Step 6: Commit** - -```bash -git add static/img/webhooks-*.png docs/api/webhooks.md -git commit -m "docs(api): add webhooks UI screenshots - -Co-Authored-By: Claude Opus 5 (1M context) " -``` - ---- - -## Finishing - -After Task 3, push and open the PR. **Do not merge.** - -```bash -git push -u origin docs/tra-1048-webhooks-v1 -gh pr create --title "docs: document webhooks v1 as shipped" --body "$(cat <<'EOF' -Replaces the "Planned for v1.x — not available yet" webhooks page with -documentation of the feature that actually shipped, and clears stale -"planned webhooks" language from the rest of the API section. - -The old page was actively misleading: it promised four event types, -at-least-once delivery, a 24-hour exponential-backoff retry ladder, and -a queryable delivery log. What shipped is one event type, at-most-once -delivery, two jittered retries, and no log. An integrator building -against the old page would silently lose events. - -Content verified against the platform implementation rather than the -ticket text. - -## HOLD — do not merge before the production platform deploy - -Webhooks exist on preview only; production is on v1.2. Merging publishes -to docs.trakrf.id, which would document a feature production users do -not have. Merge alongside #206 immediately after the deploy lands. - -🤖 Generated with [Claude Code](https://claude.com/claude-code) -EOF -)" -``` - -Screenshots (Task 4) land on this branch afterward, and do not gate the merge. - -## Self-Review - -**Spec coverage.** Every section of the spec maps to a task: the page rewrite and all eleven of its sections → Task 1; the five collateral items → Tasks 2 and 3 (README, resource-identifiers, versioning, private-endpoints in Task 2; sidebars and changelog in Task 3); screenshots → Task 4; release coupling → Finishing. The three Ground-truth refinements are each carried into Task 1's prose — the `occurred_at` timestamp nuance in "Check the timestamp too", the 52-bit id magnitude in "The payload", and the `whsec_` format plus masking in "The signing secret is shown exactly once". The per-connection SSRF framing is in "Endpoint requirements" and the drop-never-buffer behavior is in "Delivery semantics". - -**Placeholder scan.** No TBD/TODO, no "similar to Task N", no "add error handling". Every prose change quotes the exact before-and-after text; the page rewrite carries its full final content. - -**Consistency.** Heading anchors referenced in Task 1's cross-links (`#delivery-semantics`, `#reconciling-missed-events`) match headings defined in the same task. The `whsec_1f0c1d4e…` example secret in Task 1 is the same string Task 4's leak check whitelists. Delivery guarantees are stated identically in Task 1 and the Task 3 changelog entry (at-most-once, ~1s/~5s jittered, no DLQ, no replay, order by `occurred_at`, dedupe on `delivery_id`). diff --git a/superpowers/specs/2026-05-15-tra-737-bb41-followup-design.md b/superpowers/specs/2026-05-15-tra-737-bb41-followup-design.md deleted file mode 100644 index aebb2a8..0000000 --- a/superpowers/specs/2026-05-15-tra-737-bb41-followup-design.md +++ /dev/null @@ -1,49 +0,0 @@ -# TRA-737 — BB41 docs follow-up - -## Source - -`FINDINGS.md` attached to [TRA-737](https://linear.app/trakrf/issue/TRA-737/api-v1-bb41-test-findings). BB41 black-box run against preview at `docs.commit=cf42657` / `platform.commit=8420660` (2026-05-15). - -## Disposition summary - -| # | Subject | Finding disposition | Action this PR | -| --- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | -| F1 | `openapi-fetch` (TS) doesn't auto-detect `application/merge-patch+json`; every PATCH 415s on first try | hygiene; defer | **Ship a discoverability fix** — add a tip in quickstart §3 pointing TS readers to the existing §5.1 middleware | -| F2 | Pydantic `datamodel-codegen` drops `nullable: true` | confirms `BACKLOG.md` "OpenAPI 3.1 migration" + `design-notes.md` "Nullable fields…" | none — verified | -| F3 | Pydantic emits `TagType`/`TagType2`/`TagType4` per-variant enums | confirms `design-notes.md` "Tag schema uses three single-value enums…" | none — verified | -| F4 | HEAD supported on every GET; spec deliberately omits it | confirms `design-notes.md` "HEAD method not declared…" | none — verified | -| F5 | `descendant_count_affected: 0` on asset rename | confirms `design-notes.md` "`descendant_count_affected` on `RenameAssetResponse`…" (shipped in TRA-736 F5, `cf42657`) | none — verified | -| F6 | `metadata` PATCH replaces wholesale | confirms `resource-identifiers.md#metadata-opaque` | none — verified | -| F7 | `id` is int64 on wire, int32 runtime | confirms `id-format.md` + `BACKLOG.md` "Bigint storage migration" | none — verified | - -## F1 — what the gap actually is - -The substantive fix shipped in TRA-718 (`833446d`) / TRA-716 (`e2d6de8`): `docs/api/quickstart.mdx` §5.1 "TypeScript with `openapi-fetch`" documents the 415 trap and provides a copyable middleware (`mergePatchMiddleware`) plus a `createTrakrfClient` wrapper. The platform-side reference file at `trakrf/platform:docs/codegen/typescript/merge-patch.ts` is in place. - -The BB41 tester ran cleanly against §3's curl walkthrough first, then translated those calls to `openapi-fetch` in a smoke script before reaching §5. The 415 hit in their §10 codegen test reflects that ordering: §5.1 exists, but isn't surfaced where a TS reader is most likely to write their first PATCH. - -The finding text already names the fix: _"a short note in the quickstart's 'Step 3 — Update'"_. - -## Change - -Add a single `:::tip` admonition in `docs/api/quickstart.mdx` §3, immediately after the curl create/read/update/delete block, cross-linking to the existing `#openapi-fetch` anchor in §5. Wording follows TrakRF's existing prose style — concise, names the failure mode, links to the canonical fix. - -```mdx -:::tip TypeScript with `openapi-fetch`? -`openapi-fetch` is schema-agnostic at runtime — it won't read `application/merge-patch+json` from the spec, and every `PATCH` returns `415 unsupported_media_type` unless you override `Content-Type` per call or register a middleware. See [§5 — TypeScript with `openapi-fetch`](#openapi-fetch) below for the drop-in middleware that handles every `PATCH` site automatically. -::: -``` - -Plus a one-bullet BB41 entry in `docs/api/changelog.md` recording the cross-reference (pre-launch docs-only; no wire change). - -## Out of scope - -- Platform changes — none needed; F1 is purely a docs discoverability nit and F2–F7 are confirmations of existing design notes. -- Reflowing the existing §5.1 prose — already concise and complete. -- Adding TS-specific notes to `docs/getting-started/api.mdx` — that page stops at "verify your key works" and explicitly hands off to the API Quickstart for PATCH/update content (line 66). -- Touching `design-notes.md`, `id-format.md`, `resource-identifiers.md`, `BACKLOG.md` — all F2–F7 references are accurate, current, and load-bearing as-is. - -## Verification - -- `pnpm build` (Docusaurus build catches MDX/link errors). -- Manual: serve the build, confirm the §3 tip renders, and confirm the `#openapi-fetch` anchor resolves to §5.1. diff --git a/superpowers/specs/2026-05-16-tra-745-bb44-f1f2f3-design.md b/superpowers/specs/2026-05-16-tra-745-bb44-f1f2f3-design.md deleted file mode 100644 index 8531ee3..0000000 --- a/superpowers/specs/2026-05-16-tra-745-bb44-f1f2f3-design.md +++ /dev/null @@ -1,71 +0,0 @@ -# TRA-745 — BB44 F1+F2+F3 docs sweep + BACKLOG.md soft-delete fixture entry - -Pure docs PR against `trakrf/docs`. No platform, no OpenAPI, no `spec_refreshed_at`. - -## Scope (4 edits across 3 files) - -### F1 — `is_active` filter prose scope - -**File:** `docs/api/resource-identifiers.md`, section "Effective dating and `is_active`" (h2 at line 547). - -Current shape: a single bullet list enumerates 5 surfaces that apply the currently-effective predicate, then a paragraph says "pass `?is_active=true` (or `false`) to narrow further." Reads as if all 5 accept the filter — only `/assets` and `/locations` do. - -Fix: split into two clearly labeled bullet lists. - -1. Keep the existing 5-surface list as "Endpoints that apply the currently-effective predicate by default" (no content change; clarifying prose lead-in). -2. Add a second focused list "Endpoints that accept `?is_active=` as a caller filter" with only `GET /api/v1/assets` and `GET /api/v1/locations`. -3. Rewrite the "`is_active` is an independent filter dimension..." paragraph to scope the filter sentence to the two endpoints; keep the substring-search (`?q=`) sentence in place. - -Don't restructure the section into h3 subsections — the existing h3 at line 572 (`### /reports/asset-locations scopes...`) is a different concept; nesting two new h3s above it would be heavier than the prose-with-two-lists shape. - -### F2 — `/api/v1/assets/bulk` classification - -**File:** `docs/api/private-endpoints.md`, Endpoint list table (line 21). - -Add row at the end of the Internal block (before the `/orgs/me` Public row, which sits last): - -``` -| `/api/v1/assets/bulk` | POST | SPA asset bulk-CSV upload (`multipart/form-data`) | Internal | Internal | -``` - -The existing table has no content-type column; embed it parenthetically in "Used by" rather than refactor the table schema. The `changelog.md:125` reference to `/api/v1/assets/bulk` stays — it's now consistent with the policy. - -### F3 — Already satisfied; verify and no-op - -**File:** `docs/api/resource-identifiers.md` line 134. - -The "Always present" row already lists `is_active`: - -``` -| **Always present** | `id`, `name`, `external_key`, `created_at`, `updated_at`, `is_active`, `valid_from` (and most scalars) | the value itself | -``` - -A prior PR landed this between BB44 cycle execution and ticket authorship. No edit needed. PR description will call out the verification. - -### BACKLOG.md — soft-deleted fixture accumulation - -**File:** `tests/blackbox/BACKLOG.md`. - -Add a new section "Deliberate fixture states" between the existing "Internal-only deliberate states" and "Deferred work" sections. Single entry recording: - -- State: ~270 soft-deleted assets vs. 8 live in the test org at BB44; smaller-scale accumulation on locations. -- Disposition: not a service finding — surface only as fixture-maintenance for the orchestrator. -- Eventual fix (post-launch): build a deliberately scoped fixture (~hundreds of assets, 20–30 locations with 2-deep hierarchy) and reset the test org as part of provisioning. - -Place this above "Deferred work" because it's a _state observation with future fix_, not a _deferred work item with a trigger_ — distinct from the existing Deferred-work entries which have explicit revisit triggers tied to external events. - -## Adjacent-fix sweep - -Checked: - -- `docs/api/pagination-filtering-sorting.md` — filter table (lines 105–107) already correctly scopes `is_active` to `/assets` and `/locations` only. Boolean-filters prose (line 92–98) is generic and doesn't claim universal applicability. No edit needed. -- `docs/api/changelog.md:125` — references `/api/v1/assets/bulk` as historical change context. Becomes properly cross-referenceable once F2 lands the row in private-endpoints; no edit needed. -- No other docs files reference `/api/v1/assets/bulk` or imply `is_active` is universally accepted. - -## Acceptance - -- Single PR against `main` -- `pnpm build` clean -- `pnpm typecheck` clean if applicable -- F1, F2, BACKLOG edits applied per shapes above; F3 verified as pre-existing -- PR description names the four scope items, calls out F3 as already-satisfied, names the adjacent-fix sweep result diff --git a/superpowers/specs/2026-05-17-tra-762-bb-cycle-selector-design.md b/superpowers/specs/2026-05-17-tra-762-bb-cycle-selector-design.md deleted file mode 100644 index cfab383..0000000 --- a/superpowers/specs/2026-05-17-tra-762-bb-cycle-selector-design.md +++ /dev/null @@ -1,126 +0,0 @@ -# TRA-762 — `bb_cycle` selector arg (mint vs. pre-key tracks) - -## Context - -The trakrf API black-box methodology now splits into two tracks (see `tests/blackbox/BB.md`, `BB_MINT_KEY.md`, `BB_PRE_KEY.md`): - -- **Mint track:** start without an API key, log in to the SPA, mint a key, run the methodology. Exercises onboarding end-to-end. Single-instance. -- **Pre-key track:** start with a pre-minted fixture key pinned to one of three parallelism orgs (`BB1`, `BB2`, `BB3`). Skip the mint flow, run the methodology. Parallelizable across orgs. - -The platform-managed fixture keys live in `tests/blackbox/.env.local` as `BB{1,2,3}_API_KEY` / `BB{1,2,3}_ORG_ID`. The user runs parallel pre-key sessions by opening N terminals and invoking the recipe N times, once per org. The justfile recipe must support both tracks from a single entry point and keep the parallel sessions cleanly isolated from each other. - -## Signature - -`bb_cycle [arg1] [arg2]` — two optional positional args, order-agnostic via sniffing: - -| Arg matches | Meaning | -| -------------- | ------------------------------------ | -| `BB[1-3]` | Selector → pre-key track for that org | -| `[0-9]+` | Explicit cycle number | -| (empty/unset) | Defaults: mint track + auto-num | - -Examples: - -``` -just bb_cycle # mint, auto-num -just bb_cycle 33 # mint, cycle 33 (back-compat preserved) -just bb_cycle BB1 # pre-key BB1, auto-num -just bb_cycle BB1 33 # pre-key BB1, cycle 33 -just bb_cycle 33 BB1 # same as above (order-agnostic) -``` - -Invalid forms (rejected with a clear error): - -- Two selectors (`bb_cycle BB1 BB2`). -- Two numbers (`bb_cycle 33 34`). -- Unrecognized arg (`bb_cycle foo`, `bb_cycle BB4`, `bb_cycle bb1`). - -## Directory naming - -- **Mint track:** `$BB_TMP_PREFIX/bb-NN/` (no suffix). -- **Pre-key track:** `$BB_TMP_PREFIX/bb-NN-{selector}/` (suffix is the literal selector, e.g. `BB1`). - -Mint and pre-key share the numeric counter. Cycle 33 can hold any combination of `bb-33`, `bb-33-BB1`, `bb-33-BB2`, `bb-33-BB3` — the orchestrator treats them as one logical cycle. - -## Auto-num algorithm - -When `[num]` is not provided: - -1. Scan `$BB_TMP_PREFIX` for directories matching `bb-NN` or `bb-NN-*`. Take the max `NN` seen (ignore the suffix when computing the max). If no `bb-NN*` dir exists, `NN = 1`. -2. Build the target path for this invocation (`bb-NN` for mint, `bb-NN-{selector}` for pre-key). -3. If the target already exists, increment `NN` by 1 and rebuild the path; repeat until the target does not exist. - -This makes three parallel `bb_cycle BB{1,2,3}` invocations land on the same `NN` (because each one's specific target doesn't exist yet) while still incrementing cleanly when a track is rerun against an existing cycle. - -## Filtered isolated environment - -Today the recipe runs `cp -r tests/blackbox/. $target/`, which copies the entire host `.env.local` (login/pass + all three BB-N keys) into the target. Replace this with two steps: - -1. **Copy non-env files** from `tests/blackbox/` into `$target/` (BB.md, BB_MINT_KEY.md, BB_PRE_KEY.md, BACKLOG.md, `.envrc`, `check-deploy-lag.sh`, and anything else the directory contains). Exclude `.env.local`. -2. **Write a track-specific `.env.local`** in `$target/`: - -### Mint track `.env.local` - -``` -API_TEST_APP_URL= -API_TEST_DOCS_URL= -API_TEST_LOGIN= -API_TEST_PASS= -``` - -### Pre-key track `.env.local` (selector = `BBn`) - -``` -API_TEST_APP_URL= -API_TEST_DOCS_URL= -BB_ORG=BBn -BB_API_KEY= -BB_ORG_ID= -``` - -Required source vars are read from the host `.env.local` (already loaded earlier in the recipe for the deploy-lag preflight). If any required source var is missing or empty, the recipe must abort with a clear error naming the missing var. - -Rationale: each session sees exactly the surface a customer-developer counterpart would see — no admin credentials in a pre-key session, no other orgs' keys in a BB1 session, no fixture keys in a mint session. - -## Wrapper update — BB_PRE_KEY.md - -Because the filtered pre-key `.env.local` exposes flat `$BB_API_KEY` and `$BB_ORG_ID`, update `tests/blackbox/BB_PRE_KEY.md`: - -- Replace the `${BB_ORG}_API_KEY` indirection wording in §Environment with direct references to `$BB_API_KEY` and `$BB_ORG_ID`. -- Keep `$BB_ORG` listed — it's still set, still useful as the human-readable label in FINDINGS.md context. -- Drop the language enumerating the three keys; the wrapper's perspective is per-session and only sees one key. - -The shared methodology (`BB.md`) doesn't need to change. - -## Session-start command - -The final line printed by the recipe selects the wrapper based on track: - -- Mint: `cd $target && direnv allow && csp 'run blackbox tests per BB_MINT_KEY.md'` -- Pre-key: `cd $target && direnv allow && csp 'run blackbox tests per BB_PRE_KEY.md'` - -## Tests - -Extend `scripts/test_bb_cycle.sh` with cases covering the new behavior. Existing cases (auto-num progression, refuse-if-exists, hidden-file copy) stay; the new cases use a synthetic host `.env.local` placed inside `$BB_TMP_PREFIX` (or an env-override mechanism) so the test doesn't depend on the real one. - -New cases: - -1. **Selector-only:** `just bb_cycle BB2` → target is `bb-1-BB2/`. Target's `.env.local` contains `BB_ORG=BB2`, `BB_API_KEY=`, `BB_ORG_ID=`, and the two URL vars. Does NOT contain `API_TEST_LOGIN`, `API_TEST_PASS`, or the other orgs' keys. -2. **Selector + num, num first:** `just bb_cycle 33 BB1` → `bb-33-BB1/`. -3. **Selector + num, selector first:** `just bb_cycle BB1 33` → `bb-33-BB1/` (same outcome as case 2). -4. **Auto-num same-cycle reuse:** with `bb-3-BB1/` already present, `just bb_cycle BB2` → `bb-3-BB2/` (NN=3, not 4). -5. **Auto-num across-track:** with `bb-3/` (mint) and `bb-3-BB1/` (pre-key) both present, `just bb_cycle BB2` → `bb-3-BB2/`. -6. **Auto-num increment when target exists:** with `bb-3/`, `bb-3-BB1/`, `bb-3-BB2/`, `bb-3-BB3/` all present, `just bb_cycle BB1` → `bb-4-BB1/`. -7. **Mint track auto-num unchanged:** with `bb-3-BB1/` present (no `bb-3/`), `just bb_cycle` → `bb-3/`. -8. **Mint track env shape:** `just bb_cycle` writes `.env.local` with `API_TEST_LOGIN` / `API_TEST_PASS` and the URL vars; no `BB_*` vars. -9. **Invalid selectors rejected:** `bb_cycle BB4`, `bb_cycle BB`, `bb_cycle bb1`, `bb_cycle foo` all exit non-zero with a message naming the offending arg. -10. **Two selectors rejected:** `bb_cycle BB1 BB2` exits non-zero. -11. **Two numbers rejected:** `bb_cycle 33 34` exits non-zero. -12. **Missing source key rejected:** synthetic host `.env.local` omits `BB2_API_KEY`; `just bb_cycle BB2` exits non-zero with a message naming `BB2_API_KEY`. - -## Out of scope - -- Parallel-session orchestration. The user manually opens N terminals. -- FINDINGS.md aggregation across parallel runs. Each session writes its own FINDINGS.md to its target dir; aggregation is a downstream/triage concern. -- DELETE coverage on the pre-key track. Fixture key scope omits `*:delete`; this is documented in BB_PRE_KEY.md and the spec covers it via the §Mission note, not by recipe behavior. -- New tracks beyond mint and pre-key. If a future fixture model needs different env, this spec doesn't pre-allocate for it. diff --git a/superpowers/specs/2026-05-17-tra-768-bb57-docs-design.md b/superpowers/specs/2026-05-17-tra-768-bb57-docs-design.md deleted file mode 100644 index 9315c04..0000000 --- a/superpowers/specs/2026-05-17-tra-768-bb57-docs-design.md +++ /dev/null @@ -1,77 +0,0 @@ -# TRA-768 — BB57 docs: metadata asymmetry design note + changelog updates for TRA-767 + F5 housekeeping - -## Context - -Sibling docs ticket to TRA-767 (platform PR [trakrf/platform#368](https://github.com/trakrf/platform/pull/368), merged). The platform PR shipped three error-string changes (F1 `valid_from` recommendation, F2 body-decode `bad_request` enrichment, F3 PATCH `external_key` rename hint body shape) and took the **docs-narrow fallback** on F4 (validator aggregation) because the apparent "short-circuit" traces to `encoding/json`'s `UnmarshalJSON` contract — aggregating cross-field errors would require a parallel `map[string]json.RawMessage` decode pass, well beyond the ticket's scope framing. - -The platform PR also handed off **F5 housekeeping** to this PR: persist the SQL cleanup scripts to `tests/blackbox/.fixtures/` (gitignored) and update `BB_PRE_KEY.md`'s asset count (was 31, actual seeded count is 27). - -## Scope - -Single PR against `trakrf/docs`. - -### 1. F1 — assets/locations `metadata` asymmetry design note - -Add a new section to `docs/api/design-notes.md`. Wording follows the ticket draft: - -> **Locations omit free-form metadata by design** -> -> The asset surface (Create/Update/View) carries a `metadata` object for arbitrary integration-defined attributes. The location surface does not — `metadata` is intentionally absent from `CreateLocationWithTagsRequest`, `UpdateLocationRequest`, and `LocationView`. Locations are hierarchical anchors and are expected to stay austere; application-specific labels on a location should be attached via the location's `tags` subresource (`POST /api/v1/locations/{location_id}/tags`), and application-specific data about what's _at_ a location should live on the asset rows scanned there. If you find yourself wanting `location.metadata`, consider whether the data belongs on tags or on the assets instead. - -Voice-match to the existing design-notes entries (terse, integrator-second-person, cross-link the relevant doc surfaces). Place after the existing `descendant_count_affected` section. - -### 2. F2 — changelog entry for TRA-767 platform changes - -Add a new dated section to `docs/api/changelog.md` styled on existing "fix wave" entries (BB42, BB39, BB36). Title: - -> `### BB57 fix wave — error UX hygiene (valid_from, bad_request, rename hint, validator aggregation docs-narrow)` - -Four sub-bullets: - -1. **`valid_from` sentinel-rejection wording aligned with null-rejection wording on non-nullable timestamps.** Cite the contradictory-guidance failure mode the prior wording produced. POST gets "omit the field to use the server default"; PATCH gets "omit the field to leave unchanged." Nullable timestamps (`valid_to`) still recommend `"use JSON null"`. Programmatic clients branch on `error.type` / `fields[].code` — `detail` is explanatory only — so the change is wire-shape-neutral. -2. **Body-decode `bad_request` `detail` now names the expected JSON type when the decoder knows it.** Example: `Body field "is_active" could not be decoded as the expected type (boolean)`. Closes the asymmetric-diagnostic gap between the validation-stage envelope (which surfaces type via `params`) and the decode-stage envelope (which previously withheld it). Envelope `type` and `fields[]` shape are unchanged — clients that branch on `error.type` continue to work. -3. **PATCH read-only `external_key` error hint now includes the rename body shape inline.** New wording: `external_key is immutable via PATCH; use POST /api/v1/{resource}/{id}/rename with body {"external_key": ""} to change it`. Applies symmetrically on assets and locations. Saves the round-trip where integrators substitute `new_external_key` and bounce off the rename validator. -4. **`Errors → Envelope shape` (`docs/api/errors.md#envelope-shape`) prose narrowed to match service behavior on validator aggregation.** Most field validators short-circuit at the first miss (structural to Go's `encoding/json` behavior); cross-field validators — notably `ambiguous_fields` on a paired natural-key conflict — emit multiple `fields[]` entries with the `(and N more validation errors)` suffix on `detail`. The prior wording read as if aggregation was uniform across all body-validation paths; clients should branch on the `fields[]` array length to detect the multi-field case rather than assuming all violations on a body land in a single response. - -Cross-link to `/docs/api/errors` from items 1, 2, and 4. - -### 3. F4 docs-narrow — `docs/api/errors.md` prose update - -Rewrite the load-bearing claim at `errors.md:26`: - -> Previous: "On `validation_error`, `detail` echoes the first offending field's `message` verbatim (and appends `(and N more validation errors)` when more than one field is invalid). The per-field structure lives in `fields[]`." -> -> New: "On `validation_error`, `detail` echoes the first offending field's `message` verbatim. Most field-level validators short-circuit at the first miss, so `fields[]` typically carries a single entry on a body invalid on multiple fields; cross-field validators (notably `ambiguous_fields` on a paired natural-key conflict) emit multiple `fields[]` entries and append `(and N more validation errors)` to `detail`. Branch on `fields[]` array length to detect the multi-field case. The per-field structure lives in `fields[]` — see the Validation errors section below." - -The matching paragraph at `errors.md:146` ("`detail` bubbles the first field's `message`...") gets a parallel adjustment so the two sections agree on the same short-circuit-vs-cross-field framing. - -Audit other on-page mentions of multi-field aggregation for consistency; bring them into alignment if found. - -### 4. F5 — fixture cleanup script persistence + count fix - -Three changes: - -- **`.gitignore`**: add `tests/blackbox/.fixtures/` with an exception for `tests/blackbox/.fixtures/README.md` so the directory's purpose can be committed while the SQL artifacts stay gitignored. -- **`tests/blackbox/.fixtures/README.md`**: explain the directory's purpose — persistent, gitignored landing zone for fixture-maintenance SQL produced during BB cycles. Reachable across reboots; not part of the published docs build. -- **`tests/blackbox/BB_PRE_KEY.md`**: change "Assets — 31 per org" to "Assets — 27 per org" and update the OC/CD breakdown. Per the platform PR's post-cleanup count: 11 OC-origin (`ASSET-0001..0011`), 9 CD-origin under the `ASSET-NNNN` namespace above the OC ceiling (`ASSET-0012..0020`), and 7 CD-origin under the `CD-ASSET-NNNN` namespace (the rows that would have collided with OC). 11 + 9 + 7 = 27. - -The actual SQL cleanup script gets written to the **main checkout** at `/home/mike/trakrf-docs/tests/blackbox/.fixtures/bb57-cleanup.sql` via an absolute path (not into the worktree, which would be discarded with the worktree at PR merge). The file is gitignored once the PR merges, so the path is stable across reboots and worktree churn. - -The SQL bundles F5a (delete 4 cruft assets per org with scan-history cascade), F5b (reset `ASSET-0001.name` to `"Asset 1"`), and F5c (fix `CD-WHS-01.description` typo `Primary warehiouse` → `Primary warehouse`). The platform PR already ran this cleanup against the live preview; this script is the reproducible record for future fixture-maintenance work. - -## Out of scope - -- No platform code changes. F4 service-path refactor is explicitly handed off as a future consideration (the platform PR description has the rationale). -- No Linear ticket references in the public docs surface (`docs/api/*`). Internal docs and the spec/plan files may reference TRA-NNN freely. -- No further refactoring of the changelog page format. - -## Acceptance - -- `pnpm build` clean. -- `pnpm lint` clean (Prettier). -- Design note appears in design-notes preview render at `/api/design-notes`. -- Changelog entry appears in changelog preview render at `/api/changelog`. -- `errors.md` envelope-shape claim now accurately describes service behavior post-fallback. -- `BB_PRE_KEY.md` count line reads `27 per org` with the correct OC/CD breakdown. -- `tests/blackbox/.fixtures/` exists with a README; `bb57-cleanup.sql` lives in the main checkout. -- PR opened against `main`; reviewer-checkable preview deploy. diff --git a/superpowers/specs/2026-05-18-tra-775-bb61-tags-set-equality-and-errors-example-design.md b/superpowers/specs/2026-05-18-tra-775-bb61-tags-set-equality-and-errors-example-design.md deleted file mode 100644 index 7744ced..0000000 --- a/superpowers/specs/2026-05-18-tra-775-bb61-tags-set-equality-and-errors-example-design.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -ticket: TRA-775 -date: 2026-05-18 ---- - -# TRA-775 BB61 docs — tags PATCH echo set-equality changelog + errors.md worked example - -Docs-side of the paired TRA-775 ticket. Platform PR (trakrf/platform#371) ships the behavior change — `tags` PATCH echo now compared as set-equality on full tag content via `httputil.SameTagSet`. This PR documents it. - -## Scope - -Two changes against `trakrf/docs`: - -1. **F1 — changelog entry** on `docs/api/changelog.md` for the tags PATCH echo set-equality behavior change. Integrator-visible, strictly more permissive. -2. **F2 — worked example** on `docs/api/errors.md` in the `validation_error` vs `bad_request` section. Concrete `is_active: "true"` (bad_request) vs `name: ""` + `valid_from: ""` (validation_error) comparison, addressing a third-time-surfaced discoverability gap. - -Also bundled (separate commit): the `just bb_cycle` recipe auto-launch change touched up earlier in this session — replaces the copy-paste session-start line with `direnv allow $target && cd $target && exec claude …`, plus `BB_NO_LAUNCH=1` test hook. Pure DX; orthogonal to the docs content but ships in the same PR because the user pre-staged it. - -## F1 — changelog entry - -New section at the top of the `## v1.0 — Launch (TBD)` block, above the BB58 section currently in pole position: - -> ### BB61 fix wave — tags PATCH echo compared as set, concrete `validation_error` vs `bad_request` example - -Single platform bullet on the tags echo, then a docs bullet on the errors example. Format mirrors the BB58 wave entry: bold lead sentence stating the behavior change, paragraph explaining the integrator surface, "Strictly more permissive" framing on the F1 item, cross-link to `[Errors → read_only](./errors#error-type-catalog)` and `[Resource identifiers → Tags](./resource-identifiers#tags-use-a-composite-natural-key)`. - -The F2 bullet stays bundled in the same wave entry — it's the same triage cycle (BB61-3) and a docs change rather than a service change, but the changelog already pairs docs items with service items in the same wave entry (BB58 carries a service item plus a spec-emission item; BB57 carries three service items plus one docs-narrow item). - -## F2 — errors.md worked example - -Insertion point: immediately after the `:::warning Typed clients: don't iterate fields[] unconditionally` admonition at `errors.md:96-98`, before the existing prose paragraph at `:100` that starts "Type mismatches on body fields take this `bad_request` path…". - -The existing section already shows JSON envelopes for `bad_request` (lines 102-113 field-level, 115-128 top-level). What's missing is a side-by-side worked example that lets a reader probing boolean coercion or value validation see their specific shape immediately. The BB61-3 author hit this with `is_active: "true"`, reproduced the exact example the BB52 paragraph already covered abstractly, and didn't bridge it — the docs need the bridge stated, not implied. - -Block shape: a fenced "Example: comparing the two 400 envelopes" pair showing - -- `POST /api/v1/assets` with `{"name": "x", "is_active": "true"}` → `400 bad_request` (decode failure, no `fields[]`) -- `POST /api/v1/assets` with `{"name": "", "valid_from": ""}` → `400 validation_error` (constraint failures with populated `fields[]`) - -Close with a sentence reinforcing "branch on `error.type` before inspecting `fields[]`" — same point as the warning admonition, but reached via the concrete envelope shapes rather than the abstract typed-client framing. The two reach the same conclusion from different angles, which is the discoverability shape we want. - -## Out of scope - -- No edits to platform/spec — that's PR #371 (already merged into preview). -- No new pages, no nav changes, no restructuring of errors.md beyond the inserted block. -- No Linear ticket references in the prose (per repo convention). - -## Verification - -- `pnpm typecheck` and `pnpm build` clean — Docusaurus catches broken anchors and dead cross-links. -- Manual: render `/docs/api/errors` and `/docs/api/changelog` locally; confirm the new block lands in the right spot and the existing JSON examples below still flow. -- Preview deploy via Cloudflare Pages on merge; subsequent BB cycle against the deployed preview verifies integrator-visible behavior matches the changelog wording (orthogonal to this PR — platform PR #371 already shipped the behavior). - -## Commits - -1. `docs(api): TRA-775 BB61 F2 — concrete validation_error vs bad_request example in errors.md` -2. `docs(api): TRA-775 BB61 F1 — changelog entry for tags PATCH echo set-equality` -3. `chore(bb): just bb_cycle auto-launches the session — replaces copy-paste with exec claude` - -Order matches reading order in the PR diff — content first, infrastructure last. The bb_cycle commit is split because it lives outside `docs/` and reviewers shouldn't have to scroll past it to read the docs change. diff --git a/superpowers/specs/2026-05-18-tra-777-audit-followup-design.md b/superpowers/specs/2026-05-18-tra-777-audit-followup-design.md deleted file mode 100644 index 61832b6..0000000 --- a/superpowers/specs/2026-05-18-tra-777-audit-followup-design.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -name: tra-777-audit-followup-docs -description: Docs follow-on for trakrf/platform#373 — broaden invalid_context catalog entry; fix one stale code reference -date: 2026-05-18 ---- - -# TRA-777 audit follow-on — docs sweep for the broadened `invalid_context` - -## Origin - -Platform PR [trakrf/platform#373](https://github.com/trakrf/platform/pull/373) merged 2026-05-18 as an audit follow-up to the main TRA-777 ship. The original TRA-777 fix special-cased `include_deleted` only; the audit broadened `invalid_context` to every parameter that is a known list-filter on a public-API list endpoint — `external_key`, `is_active`, `q`, `location_id`, `location_external_key`, `parent_id`, `parent_external_key`, `asset_id`, `asset_external_key`. Sending any of these to a detail / write endpoint now emits `code: invalid_context` rather than `unknown_field`. - -The audit comment on TRA-777 flagged the docs amendment as optional ("no required docs changes"). Two factual-accuracy items make it slightly more than optional: - -1. The `invalid_context` catalog entry in `docs/api/errors.md` says "Currently emitted on `include_deleted` against detail endpoints" — now too narrow. -2. `docs/api/resource-identifiers.md` still says `?include_deleted` on a detail endpoint returns `code: unknown_field` — stale; should be `invalid_context` (the original TRA-777 docs PR missed this site). -3. The TRA-777 changelog entry claims "an audit of the rest of the public surface found no other parameters with the same wrong-code drift today" — now contradicted by #373. - -## Approach - -Three targeted edits. No new sections, no new examples beyond what already exists. - -### 1. `docs/api/errors.md` — `invalid_context` catalog entry - -Rewrite the second sentence of the entry from a single-example framing to a category framing with representative names. Keep the `include_deleted` example because it carries the most specific diagnostic. Add the rest of the public list-filter set as a comma-joined list. - -Replacement structure (CC's wording): - -> `invalid_context` — a known parameter was sent on a surface where it is not allowed. Emitted when any of the public-API list-endpoint filter parameters (`external_key`, `is_active`, `parent_id`, `q`, the `*_external_key` natural-key forms, `include_deleted`, …) lands on a detail, write, or sub-resource endpoint that does not declare it. The accompanying `message` names the list endpoint where the parameter is honored (e.g. `GET /api/v1/assets`) when one can be derived from the request path; `include_deleted` carries its existing specialized "soft-deleted records are not retrievable by id" wording. Strict-typed clients switching over `code` should add an arm for this value to distinguish "known parameter, wrong context here" from `unknown_field` ("parameter the API does not declare at all"). - -Keep the cross-link to `unknown_field` (already in entry). - -### 2. `docs/api/changelog.md` — TRA-777 BB62 entry - -Replace the sentence: - -> An audit of the rest of the public surface found no other parameters with the same wrong-code drift today; the shared validator helper that emits the code makes future additions a one-liner. - -With: - -> The shared validator helper now applies the new code uniformly: any of the public-API list-endpoint filter parameters landing on a detail, write, or sub-resource endpoint emits `invalid_context` (`include_deleted` keeps its specialized natural-key diagnostic; the rest carry a generic `see GET /api/v1/{resource}` message). Truly unknown query keys (`?wat=1`) continue to emit `unknown_field`. - -Same release wave; editing the entry rather than adding a separate one keeps the changelog readable. - -### 3. `docs/api/resource-identifiers.md` — `?include_deleted` on detail - -Single-token fix: change `code: unknown_field` → `code: invalid_context` in the existing sentence at line 157. The surrounding prose ("with a diagnostic naming the recovery path") stays correct. - -## Out of scope - -- The `ParseListParams` `default:` branch documented in PR #373 as a deliberate carve-out — still emits `unknown_field` on sub-resource paths like `/api/v1/locations/{id}/ancestors`. Not user-visible enough today to warrant a doc note; if a future BB cycle surfaces it, file as its own ticket. -- The `see GET /api/v1/{resource}` message wording in the platform error envelope is not reproduced verbatim in the docs catalog — the catalog describes what the code means and points integrators at the list endpoint generically; reproducing the exact wording would couple docs to platform string churn. - -## Verification - -- `pnpm build` clean -- `pnpm lint` clean (prettier) -- Browser-render the three affected pages — `/docs/api/errors`, `/docs/api/changelog`, `/docs/api/resource-identifiers` — and confirm no orphan-link or anchor-rot from the edits. diff --git a/superpowers/specs/2026-05-18-tra-780-bb63-docs-design.md b/superpowers/specs/2026-05-18-tra-780-bb63-docs-design.md deleted file mode 100644 index 5989a6d..0000000 --- a/superpowers/specs/2026-05-18-tra-780-bb63-docs-design.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -ticket: TRA-780 -date: 2026-05-18 ---- - -# TRA-780 BB63 docs — `WWW-Authenticate` 401 header, empty-object PATCH, changelog wave entry - -Docs-side of the paired TRA-780 ticket. Platform PR (trakrf/platform#375) ships four service / spec changes (F1 readOnly annotations on AssetView/LocationView, F2 `ErrorEnvelope` hoist, F3 name validator message rewrite, F4 `read_only` → `invalid_context` semantic split for sub-resource-mutable fields). This PR adds two prose items and one changelog wave entry covering the integrator-visible F1 / F2 / F4 surface. - -## Scope - -Three changes against `trakrf/docs`: - -1. **F5 — `WWW-Authenticate` prose** on `docs/api/errors.md`. The spec has always declared the header on every 401, and the service has always emitted it; the human-prose page never named it. Add a small subsection within the `Error type catalog` H2 documenting the RFC 7235 challenge contract alongside the envelope. -2. **F6 — empty-object PATCH prose** on `docs/api/errors.md`. The page documents the `null` PATCH rejection (`400 bad_request`) but never explicitly names the `{}` case (`200`, unchanged resource per RFC 7396). Extend the existing null-body paragraph to cover both top-level shapes back-to-back. -3. **Changelog wave entry** on `docs/api/changelog.md` for the platform PR's three integrator-visible items. F1 (readOnly annotations), F2 (`ErrorEnvelope` named schema), F4 (`read_only` / `invalid_context` split). F3 (name validator message wording) is below the changelog threshold per the platform owner — the four rejection classes are already documented in the BB62 follow-up entry, and the message text is explanatory, not contractual. - -## F5 — `WWW-Authenticate` prose - -Insertion point: a new H3 subsection `### 401 challenge: WWW-Authenticate header` inside the `## Error type catalog` H2, placed between the existing `### HTTP method coverage` subsection and `### validation_error vs bad_request`. Sits adjacent to the catalog's 401 row, ahead of the rest of the catalog discussion. - -The new subsection covers two layers of the 401 contract — the envelope (already on the catalog row) and the standard RFC 7235 challenge header. Names the realm, names which clients care, and points handlers at `error.detail` for the specific cause string (the canonical wording already lives in the BB36 changelog entry). - -Draft prose: - -> Every 401 response includes a `WWW-Authenticate: Bearer realm="trakrf-api"` header per RFC 7235, alongside the envelope `error.detail` describing the specific cause (missing header, malformed scheme, expired token, revoked key — see the BB36 detail-string harmonization on `./changelog`). Clients that challenge the user for credentials should surface the `realm` value; clients retrying programmatically should branch on `error.type` (`unauthorized`) and read `error.detail` for the specific cause string. - -Adjusted to the established voice on the page (third-person, present-tense, no second-person "you" where the rest of the section uses indirect address). - -## F6 — empty-object PATCH prose - -Insertion point: the existing paragraph at `errors.md:150` already covers the `null`-body case. Extend that paragraph with a sentence on `{}` so a reader scanning either keyword finds both top-level shapes back-to-back. No new subsection — same logical block (`validation_error` vs `bad_request` → bottom of the residual `bad_request` discussion). - -Draft additions to the existing paragraph: - -> A `PATCH` request whose body is the literal JSON token `null` is a related but distinct case … (existing prose preserved) … not a top-level `null` body. -> -> A `PATCH` body of the empty object `{}` is the RFC 7396 identity transform — the server applies a no-op merge and returns `200` with the resource unchanged (no `updated_at` advance, no envelope). Distinct from the rejected-as-`bad_request` `null` body above: `{}` is a valid JSON object containing zero merge directives, which RFC 7396 defines as a no-op. Clients that have nothing to update should skip the round-trip entirely; clients that need a "touch" semantic for cache invalidation should rely on `GET` rather than PATCH-with-`{}`. - -Two paragraphs because the existing one is already dense; splitting keeps each rejection / acceptance case visually distinct. - -## Changelog wave entry - -New section at the top of the `## v1.0 — Launch (TBD)` block, above the `### BB62 follow-up` entry currently in pole position. Heading: - -> `### BB63 fix wave — readOnly annotations on Asset / Location read views, ErrorEnvelope named schema, read_only vs invalid_context split` - -Single intro paragraph framing the three items as a contract-class fix (F1 — annotation gap on four fields), a spec-hygiene refactor (F2 — `ErrorResponse.error` hoist), and a semantic split on a code-overload (F4). Pre-launch breaking on F2 (class rename on generators) and F4 (clients branching on the specific `code` value for these fields) — both worth taking now rather than post-launch when partners have already pinned against the older shape. - -Three bullets, one per F item, modeled on the BB62 fix wave entry pattern: - -- **F1 bullet** — names the four fields (`AssetView.location_id`, `AssetView.location_external_key`, `AssetView.tags`, `LocationView.tags`), names the OpenAPI 3.0 `readOnly: true` semantic, names the strict-typed-client effect (PATCH-construction-time client-side rejection), reaffirms runtime unchanged. Cross-link to `Resource identifiers → Read shape vs. write shape`. -- **F2 bullet** — names the new `ErrorEnvelope` schema, names the `$ref` rewiring on `ErrorResponse.error`, names the generator-side class-rename effect (`ErrorResponseError` → `ErrorEnvelope` on `openapi-generator-cli` python), notes the description-text preservation. Wire shape unchanged. Pre-launch is the right time to take this churn. Cross-link to `Errors → Envelope shape`. -- **F4 bullet** — names the semantic split, enumerates the four affected (field × verb) cells, contrasts against the fields that stay on `read_only`, names the detail-string preservation (clients displaying `detail` continue to work). Handlers branching on `code` for these specific fields should update their `read_only` arm to also accept `invalid_context`. Cross-link to `Errors → Validation errors` (where the `invalid_context` catalog entry was broadened in the BB62 audit follow-on). - -Exact wording matches voice / density of the BB62 entry — bold lead sentence, integrator-impact paragraph, cross-links to the load-bearing per-field-code section and the read/write-shape section. F3 (name validator message wording) is intentionally omitted — message text is explanatory, not contractual, and the underlying rejection classes are already documented in the BB62 follow-up entry directly above. - -## Out of scope - -- No spec / runtime edits — those ship in platform PR #375. -- No new pages, no nav changes, no restructuring of `errors.md` beyond the inserted subsection and the appended paragraph. -- No Linear ticket references in the prose (per repo convention). -- F3 changelog entry — below threshold per platform owner; the rejection classes are documented in the BB62 follow-up entry, and the message text is explanatory not contractual. - -## Verification - -- `pnpm typecheck` and `pnpm build` clean — Docusaurus catches broken anchors and dead cross-links. -- Manual: render `/docs/api/errors` and `/docs/api/changelog` locally; confirm the new F5 subsection lands between the existing `HTTP method coverage` block and `validation_error vs bad_request`; confirm the F6 paragraph extends the null-body discussion cleanly; confirm the BB63 wave entry sits above BB62 follow-up. -- Preview deploy via Cloudflare Pages on merge; subsequent BB cycle against the deployed preview verifies platform behavior matches the changelog wording (orthogonal to this PR — platform PR #375 already ships the behavior). - -## Commits - -1. `docs(api): TRA-780 BB63 F5 — document WWW-Authenticate challenge header on 401 responses` -2. `docs(api): TRA-780 BB63 F6 — document empty-object PATCH as RFC 7396 identity transform` -3. `docs(api): TRA-780 BB63 — changelog entry for readOnly annotations, ErrorEnvelope hoist, read_only/invalid_context split` - -Order matches reading order in the PR diff — content first, changelog last (so a reviewer scrolling top-to-bottom in the diff sees the new docs prose before the summary entry that describes the platform-side changes). diff --git a/superpowers/specs/2026-05-18-tra-782-bb64-docs-design.md b/superpowers/specs/2026-05-18-tra-782-bb64-docs-design.md deleted file mode 100644 index ddb7a0b..0000000 --- a/superpowers/specs/2026-05-18-tra-782-bb64-docs-design.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -ticket: TRA-782 -date: 2026-05-18 ---- - -# TRA-782 BB64 docs-only — quickstart PATCH echo prose tighten + `updated_at`-as-optimistic-concurrency-token design note - -First docs-only ticket in the paired-ticket era. No platform PR, no changelog — both items document existing service behavior; no behavior change to record. - -## Scope - -Two changes against `trakrf/docs`: - -1. **F1 — PATCH read-only echo prose tighten + post-TRA-780 code split.** Quickstart's round-trip section already correctly frames the accept-if-matches / reject-if-differs rule (lines 141, 143 — including a one-sentence mention of optimistic concurrency on `updated_at`). The remaining drift sits at quickstart line 158 ("every value echoes silently" reads as "all values silently ignored" rather than "matching values match-strip"), and across multiple pages the rejection `code` is still uniformly stated as `read_only` even though TRA-780 F4 split the runtime emission into `read_only` (truly server-managed) and `invalid_context` (sub-resource-mutable). -2. **F2 — new `/docs/api/design-notes` entry on `updated_at`-as-optimistic-concurrency-token.** Quickstart's round-trip paragraph briefly mentions optimistic concurrency on `updated_at`; a write-heavy integrator scanning design-notes for "what's the concurrency model?" should land on a dedicated entry that names the pattern, gives a worked example, and explains why the spec annotation (`readOnly: true`) doesn't convey the runtime contract on stale-value rejection. - -## F1 — Audit and corrections - -Three classes of edit: - -### F1a — Tighten "every value echoes silently" phrasing - -`quickstart.mdx:158`: the phrase "every value echoes silently" reads as universal-silent-ignore rather than match-strip-only. Tighten to "every read-only field whose value matches the current state echoes silently" or similar. This is the literal "silently ignores" framing the ticket names (paraphrased from quickstart prose, not lifted verbatim). - -### F1b — Post-TRA-780 code-split corrections - -Update specific references to `code: read_only` on fields that now emit `code: invalid_context` (per the TRA-780 platform F4 split): - -| File | Line | Current | Post-TRA-780 | -| -- | -- | -- | -- | -| `quickstart.mdx` | 158 | `external_key` PATCH → `400 read_only` | `400 invalid_context` | -| `quickstart.mdx` | 158 | `tags` collection PATCH → `400 read_only` | `400 invalid_context` | -| `resource-identifiers.md` | 386 | `external_key` PATCH → `code: read_only` | `code: invalid_context` | -| `data-model.md` | 25 | `external_key` PATCH → `400 read_only` | `400 invalid_context` | - -The truly-server-managed fields (`id`, `created_at`, `updated_at`, `deleted_at`, `location_id`, `location_external_key`) keep `code: read_only` per F4. The `location_*` cases on `PATCH /assets` (`resource-identifiers.md` table around line 317, prose at line 214, table cells at line 315/317) stay on `read_only` and need no edit. - -### F1c — Generic accept-if-matches statements - -`resource-identifiers.md:235` and `:253` describe the rule generically with a blanket `code: read_only`. The field-level split needs to surface here too — the cleanest move is one sentence at line 235 calling out the two codes and pointing at the table immediately below, then leaving the blanket `read_only` references in the surrounding prose intact (the table is the canonical mapping; the prose lines downstream of the table can continue to use `read_only` as the generic example without misleading anyone who's read the framing sentence first). - -The reject-if-differs table at lines 237-246 currently maps field → reject-message. Adding a `code` column tightens the docs further but expands the change surface meaningfully. **Skipping the table column for this PR** — the framing sentence at :235 plus the per-field correction at :386 captures the load-bearing information. A follow-up audit could revisit if needed. - -`resource-identifiers.md:257` ("a differing instant still rejects with `read_only`") is specifically about `created_at` / `updated_at` / `deleted_at` (the timestamp set), all of which stay on `read_only`. No edit. - -`resource-identifiers.md:317` (table cell for `PATCH /assets/{id}` body with `location_id` / `location_external_key`) is about the scan-derived FK pair, which stays on `read_only`. No edit. - -## F2 — Design-notes entry - -New entry on `/docs/api/design-notes.md`, placed **after the existing Timestamps section** (`### Timestamps on the wire carry fixed millisecond precision`) for topical adjacency to `updated_at`. Section heading: - -> ## `updated_at` is an optimistic-concurrency token on `PATCH` - -Body sections: - -1. **The spec annotation tells you half the story.** `readOnly: true` on `updated_at` correctly signals server-managed-on-write — codegen tools omit it from request shapes. What the annotation doesn't convey is the runtime contract when a hand-rolled caller (or a caller round-tripping the full read shape) does include it. -2. **The runtime contract: accept-if-matches / reject-if-differs.** Submitting the value last seen on GET is silently normalized out (PATCH proceeds as if the field was absent). Submitting a stale value (one another writer has since superseded) returns `400 validation_error` / `code: read_only` with a detail naming the mismatch — the exact lost-update detection signal a write-heavy integrator needs. -3. **Worked example.** GET / mutate / PATCH with `updated_at` from the GET response asserted in the body. Show the success path (no concurrent writer) and the rejection path (another writer landed first → 400 → refetch / reconcile / retry). -4. **Opting out.** If your client doesn't need lost-update detection (single-writer integrations, last-writer-wins acceptable), omit `updated_at` from the PATCH body — semantically equivalent to a fresh GET-and-echo since the server advances it on every successful write anyway. -5. **Cross-reference to the accept-if-matches rule.** The pattern is one instance of the uniform `accept-if-matches, reject-if-differs` rule that covers every read-only field, but `updated_at` is the one that **advances on every PATCH** and thus serves the concurrency-token role. The other read-only fields (`id`, `created_at`, `deleted_at`, `location_*`, `tags`, `external_key`) don't change on PATCH and reject only on integrator-side bugs. - -Cross-link from `quickstart.mdx:143` (the existing one-sentence mention of optimistic concurrency) — add a brief inline `Design notes → updated_at is an optimistic-concurrency token` pointer so a reader following the quickstart round-trip lands on the dedicated entry. - -## Out of scope - -- No platform / spec edits — none required (both items document existing behavior). -- No changelog entry — no behavior change to record. -- No reject-if-differs table column expansion on `resource-identifiers.md` — captured as a possible follow-up if a future cycle surfaces the same gap. -- No Linear ticket references in the prose (per repo convention). - -## Verification - -- `pnpm typecheck` and `pnpm build` clean — Docusaurus broken-link check covers the new design-note anchor and the quickstart cross-link. -- Manual: render the three affected pages locally; confirm the F1 corrections read cleanly in context and the F2 design-note lands at the intended position with the worked-example codeblock formatted correctly. -- Preview deploy via Cloudflare Pages on merge. - -## Commits - -1. `docs(api): TRA-782 BB64 F1 — quickstart PATCH echo prose tighten ("matching values match-strip"); post-TRA-780 code split (external_key / tags → invalid_context)` -2. `docs(api): TRA-782 BB64 F2 — design-notes entry on updated_at as optimistic-concurrency token` - -Two commits keep the F1 corrections (multi-file audit) separate from the F2 new-content addition. F1 first since it's the smaller surface area edit; F2 second since it adds a self-contained new section that doesn't depend on F1. diff --git a/superpowers/specs/2026-05-18-tra-783-bb64-followup-design.md b/superpowers/specs/2026-05-18-tra-783-bb64-followup-design.md deleted file mode 100644 index e68c1cd..0000000 --- a/superpowers/specs/2026-05-18-tra-783-bb64-followup-design.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -ticket: TRA-783 -date: 2026-05-18 ---- - -# TRA-783 BB64 follow-up — drop wire-idempotency; every accepted PATCH advances `updated_at` - -Post-merge follow-up to BB64. Platform PR (trakrf/platform#376) ships the behavior change — every accepted PATCH advances `updated_at`, including the empty-body and verbatim-writable-echo cases — replacing the prior `IS DISTINCT FROM` short-circuit. The docs side cleans up the wire-idempotency claims threaded across four pages plus an integrator-visible changelog entry. - -**PR base:** `docs/tra-782-bb64` (the open TRA-782 docs PR). The two PRs touch adjacent prose; basing TRA-783 on TRA-782 keeps the diff to the actual TRA-783 corrections. When TRA-782 merges, retarget to `main`. - -## Scope - -Five edits across four pages: - -1. **`resource-identifiers.md`** — replace the wire-idempotency paragraph at `:255` with uniform-advance prose. Drop the byte-stable claim; describe the new model (every accepted PATCH advances `updated_at`); preserve the cross-link to errors.md idempotency for the retry-pattern consequence. Optionally tighten `:231` (the empty-body `{}` mention) to name that `updated_at` advances. -2. **`quickstart.mdx`** — rewrite the middle of the round-trip paragraph at `:143`. The current text claims "verbatim no-op body doesn't drift `updated_at`," describes the create-then-echo µs→ms-precision edge case as "the one exception," and asserts wire-idempotency on subsequent echoes. The new model has no exceptions and no special create-then-echo case — every accepted PATCH advances `updated_at`. The optimistic-concurrency framing stays (still accurate); the cross-link to the design-notes entry stays. -3. **`design-notes.md`** — audit the `updated_at`-as-optimistic-concurrency-token entry added in TRA-782. Initial read: the entry doesn't claim wire-idempotency anywhere. The pattern works regardless of whether matched-value PATCHes advance the token. Verify and adjust only if a wire-idempotency assumption slipped in. -4. **`errors.md`** — two updates: - - **`:156`** — the F6 (TRA-780) paragraph documenting `{}` as RFC 7396 identity transform claims `updated_at` does not advance. Now wrong. Update to say `200` with no settable-field change but `updated_at` advances (the filesystem `touch` analog). Cross-link replaced from the BB39 follow-up entry (now superseded) to the new BB64 follow-up entry. - - **`:324`** — the PATCH bullet in the Idempotency section claims "byte-equal including `updated_at`" and "cached-body retries that didn't race a concurrent write are safe." The byte-equal claim is gone; the cached-body retry guidance needs a substantive update because a retry whose body includes `updated_at` from a successful first call will now fail with stale-token rejection. Restate: PATCH is semantically idempotent on settable-field state; `updated_at` advances on every accepted PATCH; cached-body retries are safe only when the body omits `updated_at` (or the client re-GETs to refresh it before retrying). -5. **`changelog.md`** — new BB64 follow-up entry above the prior BB63 wave (which sits above BB62 follow-up). Single-bullet wave entry per the established pattern; explicit mention that this supersedes the BB39 follow-up R1 claim, with cross-link. - -## Replacement prose drafts - -### F1 — `resource-identifiers.md:255` replacement paragraph - -> Any accepted PATCH advances `updated_at` to the current server time on success. The model is uniform across body shapes — empty body (`{}`), verbatim writable echo of current values, partial mutation, and full mutation all advance the timestamp. The 400-rejection paths (read-only field mismatch with `code: read_only` or `code: invalid_context`, validation failure on a settable field, etc.) don't advance `updated_at` because the operation never reached storage. The mental model is filesystem `touch` semantics: any successful write event advances the modification time regardless of whether content changed. See `Errors → Idempotency` for the consequence on cached-body retry patterns — specifically, a retry whose body echoes `updated_at` from a successful first call will fail with stale-token rejection. - -### F2 — `quickstart.mdx:143` replacement middle of paragraph - -The existing paragraph spans several sentences mixing accept-if-matches framing, the wire-idempotency claim, the create-then-echo edge case, the µs→ms precision detail, the cached-body retry guidance, and the optimistic-concurrency framing. The new version keeps the optimistic-concurrency framing (still accurate) and the cross-link to the design note (still useful), but drops the wire-idempotency claim and the create-then-echo edge case (no longer exceptional). Replacement: - -> When you cache a GET response and `PATCH` later, `updated_at` is the field that drifts on every accepted PATCH — the server advances it to the current time on every successful write, regardless of whether the body contained real mutations or matched-value echoes. A real intervening write by another caller is the case to plan for: the cached `updated_at` is now stale, so re-`GET` immediately before `PATCH` to refresh it, or strip it from the cached body before sending. This is optimistic concurrency working as designed — the accept-if-matches rule on `updated_at` is how the API detects lost updates from concurrent edits, and the always-advance behavior matches filesystem `touch` semantics (any successful write advances the modification time). For the lost-update detection pattern and a worked example, see `Design notes → updated_at is an optimistic-concurrency token on PATCH`. - -### F3 — `design-notes.md` audit - -Verify the TRA-782 entry doesn't carry wire-idempotency assumptions. On re-read: the entry's opt-out paragraph says "the server will advance it on every successful write regardless" — that aligns with the new model. The rest of the entry describes the concurrency-token pattern (accept-if-matches on the body's `updated_at` value), which is independent of whether matched-value bodies advance the token. **No edit required** unless a closer re-read surfaces a stale claim. - -### F4a — `errors.md:156` replacement - -The current TRA-780 F6 prose: - -> A `PATCH` body of the empty object `{}` is the [RFC 7396](https://datatracker.ietf.org/doc/html/rfc7396) identity transform — a valid JSON object carrying zero merge directives, which the spec defines as a no-op. The service applies the merge and returns `200` with the resource unchanged; `updated_at` does not advance (the storage `IS DISTINCT FROM` short-circuit covered in [BB39 follow-up — PATCH no-op short-circuit] zero-rows the UPDATE on a verbatim no-op body, and the empty body is a strict subset of that case). Distinct from the rejected `null`-body case above: `null` is a deletion directive on the target; `{}` is a directive list with no directives. Clients that have nothing to change should skip the round-trip rather than PATCH-with-`{}`; clients deliberately exercising the identity-transform path (smoke tests verifying connectivity or auth scope) can rely on the `200` shape. - -Replacement: - -> A `PATCH` body of the empty object `{}` is the [RFC 7396](https://datatracker.ietf.org/doc/html/rfc7396) identity transform — a valid JSON object carrying zero merge directives, which the spec defines as a no-op against the resource's settable-field state. The service applies the merge and returns `200`; no settable field changes, but `updated_at` advances to the current server time (the uniform behavior shipped in the [BB64 follow-up below] — every accepted PATCH advances `updated_at` regardless of body content, matching filesystem `touch` semantics). Distinct from the rejected `null`-body case above: `null` is a deletion directive on the target; `{}` is a directive list with no directives. Clients that have nothing to change should skip the round-trip rather than PATCH-with-`{}` — the round-trip cost plus the `updated_at` advance is observable churn on the row's last-modified timestamp; clients deliberately exercising the identity-transform path (smoke tests verifying connectivity or auth scope) can rely on the `200` shape. - -### F4b — `errors.md:324` replacement bullet - -Current: - -> **`PATCH /assets/{asset_id}`, `PATCH /locations/{location_id}`** — JSON Merge Patch (RFC 7396) bodies are fully idempotent at the wire layer: applying the same patch twice yields the same final state as applying it once, **and** a verbatim no-op body (every settable field at its current value) leaves the row byte-equal including `updated_at`. The storage layer applies `IS DISTINCT FROM` per settable column and skips the UPDATE when nothing differs. Cached-body `PATCH` retries that didn't race a real concurrent write are safe; real changes still advance `updated_at` as normal. - -Replacement: - -> **`PATCH /assets/{asset_id}`, `PATCH /locations/{location_id}`** — JSON Merge Patch (RFC 7396) bodies are semantically idempotent on the resource's settable-field state: applying the same patch twice converges on the same final values. `updated_at` is not part of that idempotency guarantee — every accepted PATCH advances it to the current server time (see the [BB64 follow-up changelog entry] for the model and rationale). The retry-safety implication: a cached `PATCH` body that echoes `updated_at` from a successful first call will fail with `400 validation_error` / `code: read_only` on retry because the cached `updated_at` is now stale (the first call advanced it). Safe retry patterns are (a) omit `updated_at` from the body entirely — the server advances it regardless — or (b) re-`GET` immediately before the retry to refresh `updated_at`. Real-mutation retries face the same constraint, with the additional consideration that the retry may race a real intervening write by another caller, which the optimistic-concurrency token surfaces as the same stale-token rejection. - -### F5 — Changelog entry - -New section above `### BB63 fix wave — readOnly annotations on Asset and Location read views, ErrorEnvelope named schema, read_only vs invalid_context semantic split` (currently in pole position from the TRA-780 wave). Heading: - -> `### BB64 follow-up — every accepted PATCH advances updated_at (drops wire-idempotency for no-op bodies)` - -One-paragraph intro + single bullet. The intro names the predecessor (BB39 follow-up R1) being superseded, the integrator-visible consequence (cached-body retries that include `updated_at` now need refresh), and the filesystem-`touch` mental model. The bullet covers the behavior change with cross-links to the resource-identifiers update, the design-note, and the errors.md idempotency section. - -Body: - -> Single behavior change from a BB64 docs-side probe verification. The prior model — shipped in the BB39 follow-up wave below — short-circuited the storage UPDATE when no settable field's value differed from the current state, leaving `updated_at` byte-equal across verbatim no-op PATCHes. Probe verification post-BB64 found the short-circuit broke for `valid_from` / `valid_to` whenever the stored value carried sub-millisecond precision the wire didn't see (server-defaulted timestamps, sub-ms client input): the `IS DISTINCT FROM` check ran in storage coordinates against bytes the wire-truncated body didn't fully express, so a verbatim wire-level echo could still advance `updated_at`. Rather than carry the edge cases, the model simplifies: every accepted PATCH advances `updated_at`, no exceptions. -> -> - **Every accepted PATCH advances `updated_at`.** Empty body (`{}`), verbatim writable echo of current values, partial mutation, and full mutation all advance `updated_at` to the current server time on success. Rejected PATCHes (400 from a read-only field mismatch, validation failure on a settable field, or any other 4xx) do not advance — the operation never reached storage. The model matches filesystem `touch` semantics: any successful write event advances the modification time regardless of whether content changed. The `optimistic-concurrency-token contract on updated_at` (see the design note) is unchanged — submitting a stale `updated_at` still returns `400 validation_error` / `code: read_only` with the mismatch detail — but cached-body `PATCH` retries that include `updated_at` now need to refresh the token from a fresh `GET` before retrying (or omit `updated_at` from the body, letting the server advance it implicitly). Supersedes the BB39 follow-up R1 entry below ("PATCH idempotent on a verbatim no-op body"); the no-op short-circuit is gone. See `Resource identifiers → Read shape vs. write shape` for the round-trip framing and `Errors → Idempotency` for the cached-body retry-pattern consequence. - -## Out of scope - -- No platform / spec edits — platform PR #376 already ships the behavior change. -- No rename-endpoint changes — BB39 fix wave's same-value rename short-circuit is on a different code path and is unaffected. -- No edit to the prior BB39 follow-up R1 changelog entry — per Keep a Changelog convention, prior entries are historical. The new BB64 follow-up entry explicitly notes the supersession. -- No Linear ticket references in the prose (per repo convention). - -## Verification - -- `pnpm typecheck` and `pnpm build` clean — Docusaurus broken-link check covers the new cross-links (especially the in-file `#bb64-follow-up-*` anchor and the existing `./design-notes#updated_at-...`, `./errors#idempotency` anchors). -- Manual: render the five affected pages on the Cloudflare Pages preview; confirm prose reads cleanly and the wire-idempotency claims are uniformly replaced. Re-read the design-note to confirm no stale assumptions slipped in. -- Subsequent BB cycle against the deployed preview verifies the integrator-visible model matches the changelog wording (orthogonal — platform PR #376 already ships the behavior). - -## Commits - -1. `docs(api): TRA-783 BB64 follow-up — drop wire-idempotency claim across resource-identifiers, quickstart, errors` -2. `docs(api): TRA-783 BB64 follow-up — changelog entry for every-accepted-PATCH-advances-updated_at` - -Two commits to keep the audit-driven prose corrections separate from the summary changelog entry. F3 (design-note audit) likely contributes zero diff; F4a / F4b are in errors.md so they fold into the first commit alongside F1 / F2. - -## PR base - -Open the PR with `base: docs/tra-782-bb64`. Reviewer sees only the TRA-783 diff. When TRA-782 merges to main, retarget base to `main` (or accept whatever GitHub auto-suggests). No rebase needed because TRA-782's commits become merge ancestors of main on merge. diff --git a/superpowers/specs/2026-05-18-tra-789-bb67-docs-design.md b/superpowers/specs/2026-05-18-tra-789-bb67-docs-design.md deleted file mode 100644 index 38a7863..0000000 --- a/superpowers/specs/2026-05-18-tra-789-bb67-docs-design.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: TRA-789 BB67 docs — Errors-page read_only/invalid_context catalog correction, parent_external_key writability, q parameter documentation -date: 2026-05-18 -linear: TRA-789 -parent_linear: TRA-788 -status: in-flight ---- - -# TRA-789 BB67 docs design - -Three docs-only items rolled up from the BB67 multi-org parallel triage. No platform PR — the committed customer-facing spec already encodes the post-TRA-780 / TRA-787 contract (verified in TRA-789 platform-audit comment). Pure trakrf-docs work. - -## Scope - -### F1 — Errors page mislabels external_key / tags PATCH rejection code - -The `read_only` catalog entry in `docs/api/errors.md` still claims `external_key` (assets, locations) and `tags` (assets, locations) emit `code: read_only` on PATCH divergence. Post-TRA-780 (BB63 F4), both fields emit `code: invalid_context` — they are settable on the surface, just not via PATCH (the canonical write paths are `POST /api/v1/{resource}/{id}/rename` for `external_key`, `POST /api/v1/{resource}/{id}/tags` and `DELETE .../tags/{tag_id}` for `tags`). - -The mislabel is contract-class for typed clients: a codegen-driven `switch` over `FieldErrorCode` written from the Errors page would never reach the `read_only` arm for the two most common natural-mistake PATCH bodies (verbatim GET → PATCH echoes of `external_key` or `tags`). Those rejections fall into the typed client's `default` or fallthrough arm despite being the case the page singles out for typed branching. - -The `invalid_context` catalog entry only documents the TRA-777 use case (list-filter parameter on a detail endpoint). It needs the second framing too — fields handed off to a dedicated write subresource. - -### F2 — Errors page lists parent_external_key as read-only; it's writable - -The same `read_only` entry on `docs/api/errors.md` lists `parent_external_key` (locations) among the natural-key reference fields that emit `code: read_only`. This is wrong: `parent_external_key` is fully writable on `PATCH /api/v1/locations/{location_id}` (re-parenting via natural key), symmetric with `parent_id` and with the `CreateLocationRequest` write surface. `ambiguous_fields` is the only validation that fires on the pair, and only when the two forms disagree (matching values are silently normalized to a single re-parent). - -A docs-driven integrator reading the Errors page would conclude PATCHing `parent_external_key` will be rejected and either defensively avoid the field (lose the natural-key write path) or never re-parent via PATCH. The page should simply not list it. - -### F3 — Document the q query parameter - -The `q` query parameter on `GET /api/v1/assets`, `GET /api/v1/locations`, and `GET /api/v1/reports/asset-locations` is already documented in `docs/api/pagination-filtering-sorting.md` (substring-search section with a per-endpoint field-set matrix). Two gaps remain after the platform-audit comment on the ticket: - -1. **Asymmetry not made explicit.** The matrix shows the reports endpoint matches fewer fields than assets/locations (no `description`, asset-side identifying fields only), but doesn't call out the asymmetry as a thing integrators should watch for. A reader assuming uniform behavior could write code that breaks when crossing endpoint families. -2. **Single-value semantics undocumented.** The OpenAPI declares `q` as a single string; the runtime takes the first `?q=` value if multiple are supplied. The current prose doesn't say so, leaving the multi-value-semantics question open. - -## Audit sweep (per F1 verification) - -Drift across sibling pages where the same outdated `read_only` claim or `parent_external_key`-as-read-only listing appears: - -- **`docs/api/pagination-filtering-sorting.md:234`** — "Validator behavior on writes" section lists `tags`, `external_key`, and `parent_external_key` under "read-only fields" and claims a uniform `code: read_only` rejection. Needs the BB63 split per F1 + the parent_external_key removal per F2. -- **`docs/api/http-method-coverage.md:44`** — PATCH content-type section claims `code: read_only` on rejection with `/rename` and `/tags` subresource hints. Wrong code post-BB63. Mentions `parent_external_key` as "natural-key reference (read-only)". Needs F1 + F2 fix. -- **`docs/api/resource-identifiers.md:300`** — In the `UpdateLocationRequest` paragraph: "A differing `external_key` field is still surfaced as `400 read_only`". Drift — should be `400 invalid_context` post-BB63. (The parent_external_key half of this paragraph is already correct.) -- **`docs/api/resource-identifiers.md:301`** — Asset write-schema paragraph mentions a uniform `400 read_only` covering `external_key`, `location_id`, `location_external_key`, `tags`. The location_* half is still correct (scan-derived → `read_only`); the `external_key` / `tags` half is drift. Light-touch fix. -- **`docs/api/resource-identifiers.md:257`** — Brief lumping of `tags`, `external_key`, `*_external_key`, `location_*` as "other read-only fields" that admit a single byte form. Spirit OK (they don't accept variant byte forms the way datetime instants do), but lumps fields under "read-only" without acknowledging the code split. Light-touch fix. -- **`docs/api/design-notes.md:76`** — `updated_at` optimistic-concurrency note characterizes `id`, `created_at`, `deleted_at`, `location_id`, `location_external_key`, `tags`, `external_key` as "other read-only fields" that don't change on PATCH. Spirit is correct (none change on PATCH; reject-if-differs is integrator bug, not concurrency conflict), but the framing is loose. Light-touch fix: parenthetical that some emit `code: invalid_context` rather than `code: read_only`. - -Pages already correct (per BB63 split, verified by audit): `data-model.md` (line 25, 52-53), `quickstart.mdx` (line 141, 158), `resource-identifiers.md` (lines 235, 253, 255, 214, 303, 323), `date-fields.md` (line 10), `errors.md` (line 217 — `read_only` mention is correctly scoped to asset `location_*`), `errors.md` (line 324 — `updated_at` is server-managed). - -## Approach - -**Surgical edits only.** The Errors page's `read_only` and `invalid_context` catalog entries are the canonical reference; the audit-sweep sites are derivative drift. Fix the canonical first, then walk the drift list. No restructuring, no new sections, no recap text that duplicates the Errors-page catalog. - -**Canonical references on the Errors page**: -- `read_only` field list: `id`, `created_at`, `updated_at`, `deleted_at` (both resources); asset `location_id`, `location_external_key`. -- `invalid_context` framing: two semantic categories — (a) known parameter in wrong surface (TRA-777, unchanged); (b) field handed off to a dedicated write subresource (TRA-780 — new) — list `external_key` (via `/rename`), `tags` (via `/tags` subresource on POST and `DELETE …/tags/{tag_id}`). - -**Code-split phrasing on derivative pages**: use a uniform short hook like "rejects with `code: read_only` for server-managed fields, `code: invalid_context` for sub-resource-mutable fields (`external_key`, `tags`)" rather than repeating the full BB63 split. - -**F3 changes**: extend the existing substring-search subsection with an explicit asymmetry callout and a single-value-semantics line. Keep the existing matrix. - -## No changelog - -None of F1/F2/F3 is a runtime behavior change. The wire contract is what it has been since BB63 / BB66; only the prose is being aligned. No changelog entry. - -## Verification - -- Render `/docs/api/errors`: `read_only` list = six fields (no `external_key`, no `tags`, no `parent_external_key`). -- `invalid_context` entry has both semantic categories (filter-in-wrong-context + subresource-handoff) with fields and write paths. -- Render `/docs/api/pagination-filtering-sorting`: `q` matrix unchanged, asymmetry callout present, single-value note present. -- Audit-sweep sites all show `invalid_context` for `external_key`/`tags` divergence and `read_only` only for server-managed / asset `location_*`. -- `pnpm build` succeeds with no broken links. -- No Linear ticket references in the rendered docs body (per project convention). - -## Acceptance - -- Single docs PR against `trakrf/docs:main`. -- Preview build renders cleanly. -- Ticket stays open until docs ship per project rule; closure is paired with the merge / cycle close. - -## Cross-references - -- Parent: TRA-788 (BB67 multi-org triage rollup). -- BB63 F4 (TRA-780) introduced the `read_only` → `invalid_context` split for sub-resource-mutable fields. This is the residual prose-audit drift. -- BB66 (TRA-787) closed several adjacent prose drifts; the Errors-page catalog wasn't in that wave. diff --git a/superpowers/specs/2026-05-22-tra-799-asset-location-removal-design.md b/superpowers/specs/2026-05-22-tra-799-asset-location-removal-design.md deleted file mode 100644 index 833e21a..0000000 --- a/superpowers/specs/2026-05-22-tra-799-asset-location-removal-design.md +++ /dev/null @@ -1,255 +0,0 @@ -# TRA-799 — Remove asset current-location from the documented API surface - -## Origin - -Platform PR #384 (TRA-799) makes an asset's current location *fact data*, not a -dimension attribute. Three integrator-visible contract changes ship together: - -1. **Asset response shape** — `location_id` and `location_external_key` are - removed from the asset resource. `GET /api/v1/assets` and - `GET /api/v1/assets/{asset_id}` no longer return any location field. Current - location is read only from the scan-data surface that already exists: - `GET /api/v1/reports/asset-locations` (latest-per-asset) and - `GET /api/v1/assets/{asset_id}/history` (the latest row). -2. **Asset write** — `location_id` / `location_external_key` in a `POST` or - `PATCH` body are rejected `400 validation_error` / `code: read_only` on - *presence*. Previously create rejected them on presence and PATCH echo-checked - them (accept-if-matches / reject-if-differs). With location gone from the read - shape there is nothing to echo, so both verbs now pre-decode reject uniformly. -3. **Asset list filter** — the `?location_id` / `?location_external_key` query - filters on `GET /api/v1/assets` are removed. Filtering assets by current - location is itself fact-shaped; it moves to - `GET /api/v1/reports/asset-locations?location_id=...`. - -The reporting endpoints (`/reports/asset-locations`, `/assets/{id}/history`) are -**unchanged** — they remain the system of record for asset location, and their -`AssetLocationItem` / `AssetHistoryItem` rows keep `location_id` / -`location_external_key`. - -Pre-launch change — removing response fields is free now, breaking once external -integrators are on the API. - -## Scope - -This is the **docs-only** companion to platform PR #384. The platform code, -migration, delete-guard rewrite, and frontend changes land in PR #384. This repo -carries the prose changes to `docs/api/`. The OpenAPI-driven interactive -reference at `/api` is generated from the spec synced from the platform repo and -updates with that sync — it is **not** hand-edited here. - -## What makes this non-trivial - -`docs/api/resource-identifiers.md` uses the asset `location_id` / -`location_external_key` pair as its **primary teaching example** for several -patterns: foreign-key flat-scalar pairs in responses, the "present as `null`" -behavior, the accept-if-matches / reject-if-differs PATCH rule, and "list filters -accept both forms." Removing the field is not a delete — those examples must be -re-anchored on FK pairs that still exist. - -### Editorial decision: re-anchoring teaching examples - -Two real FK pairs survive TRA-799 and can carry the teaching load: - -- **`parent_id` / `parent_external_key` on `LocationView`** — a flat-scalar FK - pair on a *response*, present-as-`null` for a root location. This is the - re-anchor for "FK fields come as flat scalar pairs" and the "present as - `null`" table. It is writable (re-parent on PATCH), so it does *not* - illustrate the read-only / accept-if-matches behavior. -- **`location_id` / `location_external_key` on `AssetLocationItem`** (the - `/reports/asset-locations` row) — a flat-scalar FK pair, read-only, - present-as-`null` when the location is soft-deleted. This is the re-anchor for - "list filters accept both forms" (the report keeps both filter forms). - -Asset `location_id` / `location_external_key` stop being a "paired FK -relationship" in the response/filter sense. On the write surface they remain two -fields that both reject on presence — documented as a uniform `read_only` -rejection on `POST` and `PATCH`, no longer as accept-if-matches. - -### Editorial decision: changelog entry - -TRA-799 removes response fields — the most integrator-impacting class of -contract change. A new entry is added at the top of the `v1.0 — Launch` section -of `docs/api/changelog.md`, consistent with how the analogous BB40 master-data / -scan-data bifurcation was logged. Prior entries (BB40, BB63's `readOnly` -annotation entry) are historical record and are **not** rewritten. - -## Per-file changes - -### `docs/api/data-model.md` - -The asset detail view is no longer a scan-data consumption endpoint. - -- Scan-data consumption table: drop the `GET /api/v1/assets/{asset_id}` row; - the table keeps `/assets/{id}/history` and `/reports/asset-locations`. -- "The asset view's embedded location fields share the same lineage" sentence - and the "single-asset view exposes location as flat scalars" paragraph: - remove — the asset view no longer embeds location. -- "What you cannot do" bullets: the `POST` bullet stays. Rewrite the `PATCH` - bullet — drop the accept-if-matches / verbatim-round-trip wording; `PATCH` - now rejects `location_id` / `location_external_key` on presence, same as - `POST`. The intro sentence ("If you tried to `POST` or `PATCH` `location_id`") - stays correct. -- "Consumption pattern guidance": the last sentence of the batch-lookup - paragraph ("For a single asset, `GET /api/v1/assets/{asset_id}` returns the - same location fields directly on the asset view") — rewrite to point a - single-asset location lookup at `/reports/asset-locations` (filter by - `asset_id`) or the per-asset `/history`. - -### `docs/api/resource-identifiers.md` - -- **"List filters accept both forms"** — re-anchor the worked example from - `GET /api/v1/assets?location_id=` to `GET /api/v1/reports/asset-locations` - (filter by `location_id` / `location_external_key`). -- **"Foreign-key fields in responses come as flat scalar pairs"** — re-anchor - the JSON example from an asset response to a `LocationView` response showing - `parent_id` / `parent_external_key`. Rewrite the "two paired-FK shapes" / - read-vs-write contrast: the surviving response-shape FK pair is location - parentage (writable master-data); the asset side is gone. -- **"Present as `null`" table** — remove `location_id`, `location_external_key` - from the row (no longer on `AssetView`); keep `parent_id`, - `parent_external_key`, `description`, `valid_to`. -- **`AssetView` / `AssetLocationItem` nullability paragraph** — drop asset-side - location references; the `AssetLocationItem` (report) location nullability - stays. -- **Ancestor-identifiers-across-tombstones section** — remove the sentence - about `location_external_key` projecting through on - `GET /api/v1/assets?include_deleted=true`; keep the `parent_external_key` on - locations behavior and the `/reports/asset-locations` null-projection. -- **"Scope of `POST` and `PATCH /api/v1/assets/{asset_id}`" admonition** — - rewrite: `location_id` / `location_external_key` are not part of the asset - resource at all (not the read shape, not `CreateAssetWithTagsRequest`, not - `UpdateAssetRequest`); both verbs reject on presence with - `400 validation_error` / `code: read_only`. Keep the ingestion-path error - detail; add the pointer to the reporting endpoints. -- **"Read shape vs. write shape" accept-if-matches rule + table** — remove asset - `location_*` from the accept-if-matches / reject-if-differs set (no read value - to match). The set becomes `id`, `created_at`, `updated_at`, `deleted_at`, - `external_key`, `tags`. Keep a table row for asset `location_id` / - `location_external_key` but document it as reject-on-presence on `POST` and - `PATCH`, distinct from the accept-if-matches fields. -- **Write-surface bullet for asset `location_*`** — drop - `GET /api/v1/assets/{id}` from the consumption-endpoint list; keep - `/assets/{id}/history` and `/reports/asset-locations`. -- **Verbatim `GET` → `PATCH` round-trip example** — remove `location_id`, - `location_external_key` from the read-only-fields comment (they are not on the - GET response anymore); keep `tags`, `external_key`, `parent_external_key`. -- **`UpdateAssetRequest` bullet** — `location_*` is absent from the read shape - *and* both write schemas; only a hand-built payload that invents the field - hits `read_only`. Adjust wording accordingly. -- **Asset / location asymmetry paragraph** — keep the master-data / scan-data - framing; adjust so it no longer implies asset location appears on the read - shape. -- **"Paired-key behavior per verb" section + matrix** — asset location is no - longer a paired FK relationship for read or filter purposes. Rewrite the - matrix so the paired-FK rows cover location `parent_id` / - `parent_external_key` and the `/reports/asset-locations` filter pairs; collapse - the asset `location_*` contract to a single statement: rejected on presence on - `POST` and `PATCH`, not a read field, not a list filter. -- **Locations delete-semantics** — remove the "assets placed at a deleted - location (`location_id` populated, `location_external_key` null)" sentence; - the asset no longer carries the pair. -- **"Active placed assets" lookup line** — change - `GET /api/v1/assets?location_id={location_id}` to - `GET /api/v1/reports/asset-locations?location_id={location_id}` (the - `/assets` filter is gone). -- **`external_key`-typed filter enumeration** — `?location_external_key=` is now - only on `/reports/asset-locations`, not `/assets`; tighten the per-endpoint - filter mapping sentence. - -### `docs/api/pagination-filtering-sorting.md` - -- **Filter-param table** — remove `location_id` (repeatable) / - `location_external_key` (repeatable) from the `GET /api/v1/assets` row; the - `/reports/asset-locations` row keeps both. -- **"Paired-by-id-and-by-natural-key filters are mutually exclusive"** — drop - `/assets` from the list of endpoints carrying a `location_id` / - `location_external_key` filter pair; the pairs are now on - `/reports/asset-locations` (asset and location) and `/locations` (parent). -- **"Repeatable filters" examples** — the two curl examples filter `/assets` by - `location_external_key` / `location_id`; re-point at - `/reports/asset-locations` (which still has repeatable location filters). -- **"Validator behavior on writes"** — remove asset `location_*` from the - accept-if-matches / reject-if-differs prose; the `POST`-side paragraph - already states presence-rejection — generalize it to cover `PATCH` as well. -- **"Worked examples per resource → Assets"** — the example filters `/assets` by - `location_external_key`; replace with a location-free asset-list example - (e.g. `?is_active=true&sort=-created_at`) and, if useful, a cross-reference to - the asset-locations report example for location filtering. - -### `docs/api/quickstart.mdx` - -- **§3 round-trip PATCH paragraph** — remove `location_id`, - `location_external_key` from the read-only-fields-that-echo-safely list; they - are not on the read shape. -- **Following paragraph** — the "Differing values on the truly server-managed - fields (... the scan-derived `location_id` / `location_external_key` on - assets) return `code: read_only`" clause: drop the asset `location_*` mention - from the accept-if-matches framing. The "Asset location specifically cannot be - set via `PATCH` ... derived from scan events" sentence stays (still true). - -### `docs/api/errors.md` - -- **`invalid_context` — list-filter-on-wrong-endpoint** — the filter-param - enumeration lists `location_id` / `location_external_key`; they are now - reports-endpoint filters only, not `/assets`. Adjust the enumeration so it no - longer implies `/assets` carries them. -- **`ambiguous_fields`** — remove `/assets` from the surfaces that emit the code - on the `location_id` vs `location_external_key` pair (the `/assets` location - filter is gone). Update the `PATCH /api/v1/assets/{asset_id}` clause: asset - `location_*` reject on presence, not under accept-if-matches. -- **`read_only`** — asset `location_id` / `location_external_key` now fire - `read_only` on presence on **both** `POST` and `PATCH` (the prose currently - says `PATCH` fires only on divergence under accept-if-matches). Update. - -### `docs/api/design-notes.md` - -- The accept-if-matches / reject-if-differs field list ("`id`, `created_at`, - `deleted_at`, `location_id`, `location_external_key`, `tags`, `external_key`") - — remove asset `location_id` / `location_external_key`; they are no longer - read-shape fields subject to that rule. - -### `docs/api/changelog.md` - -Add one new entry at the top of `## v1.0 — Launch (TBD)`, above the BB66 entry: -asset location removed from the asset resource response and asset-list filter; -read it from `/reports/asset-locations` / `/assets/{id}/history`; `POST` and -`PATCH` reject `location_id` / `location_external_key` uniformly on presence. -Pre-launch; no `v1.0.0`-or-later wire baseline to break. - -## Out of scope - -- `docs/api/date-fields.md` — its `location_id` / `location_external_key` JSON - samples are `/reports/asset-locations` and history rows, which are unchanged. -- `docs/api/id-format.md` — its `location_id` references describe the - reports-endpoint filter and the asset write-surface presence-rejection, both - still accurate. -- `docs/api/http-method-coverage.md`, `authentication.md` — no asset-location - references. -- `static/api/openapi.{json,yaml}` and the `/api` interactive reference — spec - synced from the platform repo; updates with that sync. -- Manual location entry / location-observation provenance model — a deferred - future epic on the platform side; no doc change here. - -## Verification - -- `pnpm build` succeeds (Docusaurus build catches broken internal links). -- `git grep -nE 'location_id|location_external_key' docs/api/` — every - remaining hit is a path parameter (`{location_id}`), a `parent_*` reference, a - `LocationView` field, or a `/reports/asset-locations` / `/history` filter or - row. No hit describes an asset response field or an `/assets` list filter. -- No internal anchor links left dangling by removed/renamed sections. - -## Acceptance - -- [ ] Asset response shape documented with no `location_id` / - `location_external_key`; current location documented as read from - `/reports/asset-locations` and `/assets/{id}/history`. -- [ ] `POST` and `PATCH` documented as rejecting `location_id` / - `location_external_key` on presence with `400` / `code: read_only`. -- [ ] `?location_id` / `?location_external_key` removed from the documented - `GET /api/v1/assets` filter surface; reports endpoint documented as the - location-filter path. -- [ ] FK-pair / present-as-`null` / accept-if-matches teaching examples - re-anchored on surviving FK pairs; no dangling asset-location example. -- [ ] Changelog entry added; prior entries untouched. -- [ ] `pnpm build` passes; `git grep` audit clean. diff --git a/superpowers/specs/2026-05-23-tra-816-tag-cascade-design.md b/superpowers/specs/2026-05-23-tra-816-tag-cascade-design.md deleted file mode 100644 index da54254..0000000 --- a/superpowers/specs/2026-05-23-tra-816-tag-cascade-design.md +++ /dev/null @@ -1,173 +0,0 @@ -# TRA-816 — Soft-delete cascades to attached tags - -## Origin - -Platform PR #397 (TRA-816, merged) fixes a soft-delete cascade bug. When an -asset or location is soft-deleted, the rows in `trakrf.tags` that point to it -are now soft-deleted in the same transaction with the same `deleted_at`. -Previously the tag rows stayed live with their parent id still pointing at the -soft-deleted parent, which kept the `(org_id, tag_type, value) WHERE -deleted_at IS NULL` partial-unique slot occupied — so the tag's natural-key -value could not be reattached to any other entity in the same org. A one-shot -migration sweeps the existing orphan rows (190 on preview at write time → 0). - -Two server-side message defences ship alongside the cascade: - -1. `lookupTagConflict` now filters soft-deleted parents on the JOIN, so a - stale orphan that slips through cannot leak a hidden entity's name in the - `409 conflict` detail string. After the cascade fix this code path is - unreachable; the filter is defence-in-depth against future orphan - regressions. -2. Frontend `checkTagConflict` shows a "no longer have access" message when - the lookup envelope is missing entity context. Frontend-internal; no public - API surface. - -## Scope - -This is the **docs-only** companion to platform PR #397. The platform code, -migration, backend cascade, and frontend defences land in PR #397. This repo -carries the prose changes to `docs/api/`. The OpenAPI-driven interactive -reference at `/api` is generated from the spec synced from the platform repo -and updates with that sync — it is **not** hand-edited here. - -The fix is **narrow on the public docs surface**. The docs already describe -the intended behavior: `docs/api/resource-identifiers.md` states that natural -keys are enforced by partial unique indexes on `WHERE deleted_at IS NULL`, so -a soft-delete frees the handle for reuse. The platform was not matching that -contract for tags whose parent was soft-deleted; the cascade brings runtime -behavior in line with documented behavior. There is no field-shape change, no -new field, no new endpoint, no new error code. - -## Editorial decisions - -### Changelog entry - -The cascade closes a real integrator-visible inconsistency — a tag's natural -key was not reusable after a parent soft-delete despite the page on tags -claiming it would be. A changelog entry under `## v1.0 — Launch (TBD)` calls -out (a) the cascade as the durable fix, (b) the one-shot sweep migration as -the way the existing orphan footprint is cleared, and (c) the -`lookupTagConflict` defence as the reason an integrator who saw an orphan- -named 409 detail in preview won't see one again. Framed as a pre-launch fix; -no `v1.0.0`-or-later wire baseline to break. Consistent with how prior -"docs-already-said-the-right-thing, platform now matches" entries have been -logged (the BB42 `tag_type` strict-required fix is the nearest precedent). - -### Tags-section tightening - -`docs/api/resource-identifiers.md` Tag CRUD section (`#tag-crud`, around line -636) lists the four tag write endpoints and their idempotency semantics but -does not state what happens to attached tag rows when the **parent** asset or -location is soft-deleted. The current prose implies the answer through the -general "soft-delete frees the handle for reuse" framing on line 28, but for -tags the answer requires a cascade step that's worth one explicit sentence: -when a parent is soft-deleted, the platform soft-deletes its attached tag -rows in the same transaction with the same `deleted_at`, releasing the -natural-key slot for immediate reattachment on another entity in the same -org. One sentence appended to the Tag CRUD prose below the endpoint table. - -### Out of scope: errors.md - -The `409 conflict` row in the `docs/api/errors.md` error type catalog (line -80) and the tag-conflict detail-string changelog entry (line 291) already -describe the contract accurately: a duplicate `(tag_type, value)` returns -`409 conflict` with `detail` naming the conflicting `tag` (not "identifier"). -The orphan-named detail string the platform was emitting on the buggy path is -not, and was never, the documented contract — it was a leak from a JOIN that -forgot to filter `deleted_at`. The cascade plus the `lookupTagConflict` JOIN -fix bring the runtime back to the documented shape; no errors.md edit -follows. - -### Out of scope: data-model.md, design-notes.md - -Neither page describes tag-parent cascade behavior. Adding the concept here -would be net-new prose not anchored in the current page's framing — the tag -cascade fits the resource-identifiers Tag CRUD section, which already covers -tag write semantics. Leave both pages alone. - -### Out of scope: frontend conflict UX - -The `checkTagConflict` fallback message change is frontend-internal — it -affects the TrakRF web app's conflict toast, not any wire-level shape an API -integrator would see. The trakrf-docs site documents the public API surface; -the web app's UI prose is out of scope here. - -## Per-file changes - -### `docs/api/changelog.md` - -Add one entry at the top of `## v1.0 — Launch (TBD)`, above the existing -`### BB1 hygiene` entry. Title: `Tag rows cascade-soft-delete with their -parent asset/location`. Body: one short framing paragraph + 2-3 bullets. The -bullets cover: - -- The cascade itself: parent soft-delete soft-deletes attached tag rows in - the same transaction with the same `deleted_at`. Cross-link to - [Resource identifiers → Tag CRUD](./resource-identifiers#tag-crud) where - the behavior is documented in prose. -- The one-shot sweep migration: existing orphans are cleared as part of the - fix, so the natural-key slots they were occupying are released. Integrators - on preview who saw a "tag already exists" 409 naming an entity they could - not see in any list can re-try the attach. -- The `409 conflict` detail string: after the cascade the orphan-name leak - path is unreachable; a defence-in-depth JOIN filter in - `lookupTagConflict` ensures any future regression returns the generic 409 - rather than naming a hidden entity. No documented detail-string shape - changes — the contract on the [errors page](./errors#error-type-catalog) - was already the post-fix shape. - -Framing line: pre-launch fix; brings runtime in line with the existing -[partial-unique-on-`WHERE deleted_at IS NULL`](./resource-identifiers#tag-crud) -contract for tags. - -### `docs/api/resource-identifiers.md` - -One sentence added to the Tag CRUD section (`#tag-crud`), placed after the -endpoint table and the "Tag writes use the parent resource's write scope" -sentence (around line 647), before the `# Attach an RFID tag...` curl block. -Substance: when a parent asset or location is soft-deleted, attached tag rows -are soft-deleted in the same transaction with the same `deleted_at`, so the -`(org_id, tag_type, value)` slot is released for immediate reattachment on -another entity in the same organization. Cross-link to the -`include_deleted=true` paragraph on [Soft-delete visibility on lists](#soft-delete-visibility) -for the read-side counterpart (a soft-deleted tag is hidden from parent -read shapes by default and appears under `?include_deleted=true` on list -surfaces that accept it). - -## Out of scope - -- Frontend `checkTagConflict` fallback prose — web-app UI, not API docs. -- Spec / `static/api/openapi.{json,yaml}` edits — no schema, response, or - error envelope change. -- `docs/api/data-model.md`, `docs/api/design-notes.md` — neither currently - describes tag-parent cascade; no targeted improvement that serves this fix. -- `docs/api/errors.md` — the documented 409 shape is already the post-fix - shape; the buggy detail-string variant was never the contract. -- A `BB`-numbered hygiene label — TRA-816 was surfaced incidentally while - setting up the TRA-812 partial-failure walk, not from a black-box cycle - finding. The changelog entry uses a descriptive heading, matching how - TRA-799 ("Asset current location removed from the asset resource") was - logged. - -## Verification - -- `pnpm build` succeeds (Docusaurus build catches broken internal links and - anchors). -- `git grep -nE 'cascade|orphan|soft.delete' docs/api/` — every new hit is - the new changelog entry or the new Tag CRUD sentence; no stale wording - contradicts the cascade contract. -- The new Tag CRUD sentence references an existing anchor - (`#soft-delete-visibility`); the build link-check verifies it resolves. - -## Acceptance - -- [ ] Changelog entry added at the top of `## v1.0 — Launch (TBD)`, above - the `### BB1 hygiene` entry, with the cascade + sweep + 409-defence - bullets and the cross-link to Tag CRUD. -- [ ] One sentence added to `docs/api/resource-identifiers.md` Tag CRUD - section stating that parent soft-delete cascades to attached tag rows, - with a cross-link to the `Soft-delete visibility on lists` paragraph. -- [ ] No other `docs/api/*.md` or `*.mdx` file edited. -- [ ] `pnpm build` passes; `git grep` audit clean. -- [ ] PR opened on `docs/tra-816-tag-cascade` against `main`, conventional- - commit titles, links to TRA-816 in the PR body (not in public docs). diff --git a/superpowers/specs/2026-05-28-tra-848-oauth-auth-docs-cutover-design.md b/superpowers/specs/2026-05-28-tra-848-oauth-auth-docs-cutover-design.md deleted file mode 100644 index d15f131..0000000 --- a/superpowers/specs/2026-05-28-tra-848-oauth-auth-docs-cutover-design.md +++ /dev/null @@ -1,86 +0,0 @@ -# TRA-848 — Integrator auth docs cutover to OAuth2 client_credentials - -**Date:** 2026-05-28 -**Status:** Design approved, spec under review -**Depends on:** TRA-846 (oauth/token grant flow) and TRA-847 (opaque client_secret + api-key creation) — both merged + verified on preview. - -## Problem - -The integrator auth docs describe the pre-846/847 model: an API key *is* a long-lived JWT, sent directly as `Authorization: Bearer`. The platform now uses an OAuth2 `client_credentials` flow: - -- Credentials are an opaque `{client_id, client_secret}` pair (not a JWT). -- They are exchanged at `POST /api/v1/oauth/token` for a short-lived access token (a JWT). -- A long-lived JWT presented as Bearer is now rejected. - -The docs must teach the new flow so a new integrator (TeamCentral.ai, first partner, launch ~2026-06-01) can authenticate end-to-end against preview from the docs alone, without reading source. - -## Confirmed implementation contract - -Verified against `~/platform` code and the published spec at `${appHost}/api/openapi.yaml` (the path `docusaurus.config.ts` ingests). - -- **Mint** (app UI, Account menu → API Keys → New key): returns once - - `client_id` = the key's `jti` (UUID) - - `client_secret` = opaque `trakrf_` + 64 hex chars; SHA-256 hashed server-side; shown exactly once -- **Exchange** `POST /api/v1/oauth/token`, JSON body, `ErrorResponse` on failure: - - `grant_type=client_credentials` + `client_id` + `client_secret` → `{access_token, refresh_token, token_type:"Bearer", expires_in:900}` - - `grant_type=refresh_token` + `refresh_token` → same shape, rotated -- **Tokens:** - - `access_token` = short-lived JWT, 15 min (`expires_in: 900`) - - `refresh_token` = opaque 64-hex, 30-day TTL, **single-use rotation** — replaying a used refresh token returns 401 and revokes the whole chain -- **Status codes** on `/oauth/token`: 200 ok, 400 validation/unsupported `grant_type`, 401 invalid credentials/refresh token. -- Schemas in spec: `auth.TokenRequest`, `auth.TokenResponse`. - -## Design decisions - -1. **Minting stays UI-only in public docs.** Real HTTP key-management endpoints exist (`POST/GET/DELETE /api/v1/orgs/{id}/api-keys`) but are tagged `internal` (absent from the public spec) and gated on `keys:admin` or a session-admin. `keys:admin` is not in the public scope picker, so public integrators cannot reach them. This matches industry norms (Stripe/Twilio/GitHub: dashboard-minted credentials, programmatic everything-else). -2. **TeamCentral topology justifies UI-only.** They integrate with a known/small set of orgs, so one-time UI minting per org is acceptable. Multi-tenant programmatic provisioning is a deferred future enhancement, not a launch requirement. -3. **Source of truth = published public spec**, not the contract sketch. Worked examples use verified placeholder values. - -## Scope - -**Rewrite (canonical):** -- `docs/api/authentication.md` - -**Update (onboarding paths that actively teach auth end-to-end):** -- `docs/api/quickstart.mdx` -- `docs/getting-started/api.mdx` - -**Light sweep (incidental examples):** -- Other API pages that use a `$TRAKRF_API_KEY` env var in curl/code examples. Replace with a short-lived access-token variable (`$TRAKRF_ACCESS_TOKEN`) so we never teach hardcoding a 15-min token. Mechanical; preserves surrounding prose. Pages identified: `postman.mdx`, `http-method-coverage.md`, `pagination-filtering-sorting.md`, `data-model.md`, `date-fields.md`, `resource-identifiers.md`, `changelog.md` (each reviewed individually — only example blocks change). -- `private-endpoints.md` needs slightly more than an example swap: its "Programmatic access" section frames the API key itself as the credential. Reconcile that prose to "mint `client_id`/`client_secret`, exchange for an access token" while keeping the public/internal endpoint table and the `/orgs/me` section intact. - -**Out of scope:** -- Documenting `/orgs/{id}/api-keys` management endpoints (stay internal). -- Any backend/spec change (e.g., making `keys:admin` grantable). -- Multi-tenant key provisioning. - -## New `authentication.md` structure - -1. **Overview** — OAuth2 `client_credentials`: mint a `client_id`/`client_secret` once, exchange for a short-lived access token, send that token as `Authorization: Bearer`. -2. **Mint credentials** — Account menu → API Keys → New key → choose scopes → receive `client_id` + opaque `client_secret` (shown once; store immediately). Org-scoping and revoke/re-mint guidance retained. -3. **Get an access token** — `POST /api/v1/oauth/token`, `grant_type=client_credentials`; worked curl + JSON response. -4. **Use the access token** — `Authorization: Bearer `; curl/Python/JS examples updated to obtain-then-use. -5. **Refresh** — `grant_type=refresh_token`; worked curl; new pair returned each time. -6. **Security properties** (ticket-required) — 15-min access tokens; single-use refresh rotation; replay → 401 + chain revocation; secret hashed server-side and shown once; store credentials in a secrets manager. -7. **Retained, lightly adapted** — Scopes table + `x-required-scopes`; Base URL; server-to-server/CORS; environment variables; 401 detail strings; testing connectivity. -8. **Removed** — "API key is a JWT"; "use the returned JWT directly as Bearer"; the "no programmatic minting by trust-boundary" rationale framing. - -## Worked example values (verified) - -- `client_id`: `6f1c2a8e-7d3b-4e90-9a11-2c4d5e6f7a8b` -- `client_secret`: `trakrf_9f8e7d6c5b4a39281706f5e4d3c2b1a0ffeeddccbbaa99887766554433221100` -- `access_token`: a JWT placeholder (`eyJ…`) -- `refresh_token`: 64-hex placeholder -- `expires_in`: `900`, `token_type`: `Bearer` - -## Acceptance - -- A new integrator following `authentication.md` can mint credentials, obtain an access token, call a public endpoint, and refresh — against preview — without reading source. -- No page instructs using a long-lived JWT directly as Bearer. -- Docs prose matches the published OpenAPI spec. -- `pnpm build`, `pnpm typecheck`, `pnpm lint` pass. - -## Verification - -- Run the worked curl examples against `https://app.preview.trakrf.id` to confirm shapes/status codes. -- Build the site and spot-check the rendered auth page + onboarding pages. diff --git a/superpowers/specs/2026-05-30-tra-885-id-vs-external-key-distribution-design.md b/superpowers/specs/2026-05-30-tra-885-id-vs-external-key-distribution-design.md deleted file mode 100644 index 6eb1800..0000000 --- a/superpowers/specs/2026-05-30-tra-885-id-vs-external-key-distribution-design.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -title: TRA-885 — Distinguish the globally-unique surrogate id from the MAX+1 external_key -date: 2026-05-30 -ticket: TRA-885 -status: converged -source: bb-253 round-2.5 convergence (§0 / locked copy); operator-confirmed by Mike in-session ---- - -# TRA-885 — `id` vs `external_key`: state the contract precisely - -## Source of truth - -**The TRA-885 ticket text is stale.** Mike directed (in dispatch and in-session) that the -authoritative wording comes from the **bb-253 round-2.5 convergence (§0 locked copy)** — that -session did the post-ticket investigation and wording-refinement. Where the ticket and §0 -disagree, §0 + Mike's in-session confirmation win. Mike okayed updating the ticket itself. - -Two places the ticket is stale: - -1. Ticket says "Make NO claim about cross-type id uniqueness either way." → **Superseded.** Mike - confirmed directly: **state global uniqueness explicitly** ("better to be explicit and head - off potential confusion"). TRA-886 (single shared sequence, PR #443, Done) makes it true by - construction. -2. Ticket / existing docs frame `external_key` auto-mint as "lowest unused slot / gap-filling." - → **Wrong.** Platform code-confirmed (`assets.go:60` `GetNextAssetSequence`, `locations.go:57` - `GetNextLocationSequence`): it is **`MAX(live key)+1`**. Independently code-read by docs, - triple-confirmed by platform via bb-253. - -## The two identifiers (locked §0 framing) - -- **`id`** — high-entropy, server-assigned integer. **Globally unique across all resources** - (no two rows of any type share an `id`) and **opaque** (don't parse it, order by it, or infer - count or time from it). **Permanent: never changes, never reused — even after delete.** Use it - as the durable foreign key when mirroring TrakRF data; address a resource via its typed - endpoint (`GET /assets/{id}`). -- **`external_key`** — human-readable handle, unique within your org for a given resource type, - among **live** (non-deleted) rows. Supply your own, or the server auto-assigns. **Auto-assigned - values are not a stable sequence:** they are `MAX(live key)+1`, so deletes leave **permanent - gaps** and a freed value can be reused — a server-assigned `external_key` you saw before can - later refer to a *different* resource. Don't cache it as a long-lived reference or infer counts - from it; use `id` for that. - -## external_key auto-mint mechanism (platform-confirmed) - -- Auto-assigned key = `MAX(existing live ASSET-NNNN / LOC-NNN for your org) + 1`, zero-padded - `ASSET-%04d` / `LOC-%03d`. -- **Middle-delete gaps are permanent.** Delete `ASSET-0002` while `0003` lives → next mint = - `MAX(0003)+1` = `0004`, never `0002`. Not backfilled. -- **Only deleting the current highest live key frees its number.** Delete top `0003` → next mint - re-issues `0003`. -- **Separate, caller-initiated reuse path:** a caller may re-supply a soft-deleted row's key - string (the partial unique index is `WHERE deleted_at IS NULL`, so deleted keys sit outside - it). Distinct from auto-mint. -- Keep the "non-monotonic / not a Postgres sequence" framing (accurate, consistent with - changelog.md:152). Drop "lowest unused slot," "gap-filling," and any - "backfilled / recyclable-implies-refill" wording. - -## Build scope - -1. **`docs/api/id-format.md`** — replace the standalone "randomly distributed" sentence (:26) - with the field-named `id` framing (high-entropy, **globally unique**, opaque, permanent). - Add the reinforcement line: a dense / low-valued `external_key` implies nothing about `id` — - the two are allocated independently. -2. **`docs/api/resource-identifiers.md`** — two fixes: - - "Numeric `id` is a surrogate key" section: replace "unique within their entity type, not - across types / the same integer can be both an asset id and a tag id" → **globally unique, - opaque, permanent**. - - auto-mint section + system-of-record note: replace "lowest unused slot / gap-filling / - recyclable" with the `MAX+1` mechanism above. -3. **`docs/api/changelog.md`** — fix the stale "lowest unused slot among live rows" assertions in - the existing LOC-NNN-finalize and non-monotonic-wording entries; add a new entry recording the - `id` global-uniqueness framing (tied to TRA-886) and the `external_key` `MAX+1` correction. - -## Scope boundary - -- Docs-only, consumer-facing. -- `id` global-uniqueness claim is **gated**: the PR does not merge until TRA-886 is live (prod) - and Mike approves the diff. Timing handled by the gate, not by omitting the claim. - -## Verification - -- `pnpm build` passes (Redocusaurus + broken-link check). -- Read-through: no "randomly distributed" standalone line; no "lowest unused slot / gap-filling" - anywhere; every distribution/allocation claim names its field; global uniqueness stated in - id-format and resource-identifiers, consistent. - -## Evidence - -- Platform code: `assets.go:60` / `locations.go:57` → `SELECT MAX(...) WHERE deleted_at IS NULL` - then `+1`. -- TRA-886 (PR #443, Done): single shared sequence → globally unique `id` by construction. -- Live BB evidence (from ticket): BB2 `LOC-004/005` + distributed `id`; BB3 `ASSET-0021` + - 36-sample scatter, spread 4.1e15, 0 collisions. diff --git a/superpowers/specs/2026-05-30-tra-891-api-identity-join-guidance-design.md b/superpowers/specs/2026-05-30-tra-891-api-identity-join-guidance-design.md deleted file mode 100644 index 4d23f23..0000000 --- a/superpowers/specs/2026-05-30-tra-891-api-identity-join-guidance-design.md +++ /dev/null @@ -1,115 +0,0 @@ -# TRA-891 — Correct API integration identity guidance: join on natural keys, demote surrogate `id` - -**Date:** 2026-05-30 -**Ticket:** TRA-891 (revises TRA-885) -**Type:** Docs-only PR (verified — see Edit-site determination) - -## Problem - -TRA-885 shipped a line advising integrators to use the surrogate `id` as their durable -foreign key when mirroring TrakRF data. That contradicts the intended model: integrators -should join on natural/business keys (`external_key`), and the surrogate `id` is a stable -internal anchor for reconciliation — **not** the integrator's business foreign key. Telling -partners to key their system of record on our `id` manufactures a dependency on an opaque -internal identifier, which is exactly the coupling we want to avoid before launch. - -This spec corrects that line, reframes `id`, adds join guidance, and demotes `id` on the -versioning page. - -## Prerequisite verification (done before writing) - -The ticket requires docs to describe real behavior, not aspiration. Verified against the -live spec at `https://app.preview.trakrf.id/api/openapi.yaml` (the source Redocusaurus -builds from) and the current docs prose: - -| Claim to verify | Finding | Consequence | -| --- | --- | --- | -| `external_key` present + returned on assets | `AssetView.external_key`, `required` | Document join on `external_key` ✓ | -| `external_key` present + returned on locations | `LocationView.external_key`, `required` (ticket flagged this "unconfirmed") | **Confirmed present** — safe to document the locations join ✓ | -| `customer_identifier` field exists on the API | **0 occurrences** in the spec | The API field is `external_key`. The ticket's "(customer_identifier)" is platform/DB column terminology — do **not** introduce `customer_identifier` into public docs | -| Users are a public, joinable resource | `/api/v1/users/me` and `/users/me/current-org` are **Internal**; no public `User`/`UserView` schema; no `email` join surface | **Do not** add per-entity "join users on email/id" guidance — there is no public users resource to join | -| Organizations are a public, joinable resource with a `slug` | Only `/api/v1/orgs/me` is public (singleton, key-scoped); `OrgView` = `{id, name, scopes, api_key_id}`; **no `slug`**, no public org list/get-by-id | **Do not** add "join orgs on slug/id" guidance — `slug` does not exist on the public surface, and an integrator is always scoped to its own org | -| Wrong "durable foreign key" advice also in OpenAPI field descriptions? | **No** — only in docs-repo prose (`resource-identifiers.md`, `changelog.md`) | This is a **docs-only PR**; no paired platform edit needed | -| Delta / incremental sync advertised anywhere? | **Not advertised** (no `delta`, `updated_since`, cursor-sync capability claims) | Nothing to retract and nothing to add — do not introduce a "not-yet-supported" mention for a capability we never claimed | - -### Deviation from the ticket's literal scope (intentional) - -The ticket lists per-entity join guidance for four entity types. Verification shows only -**assets** and **locations** are public joinable resources carrying the relevant field -(`external_key`). **Users** and **organizations** are not part of the public integration -surface (users are internal-only; the public org surface is the singleton `/orgs/me` with -no `slug`). Per the ticket's own instruction — *"do not document a join key that is not -present"* / *"describe real behavior, not aspiration"* — the user/org email-vs-id and -slug-vs-id guidance is **omitted** rather than published as aspiration. The general rule -(join on a stable natural key where one exists; where none does, `id` is the durable -handle) is stated, which covers the intent without inventing a public surface. - -This is flagged here and in the PR for reviewer (ticket-author) sign-off at the merge gate. - -## Changes - -All edits are docs-repo prose. Branch: `docs/tra-891-identity-join-guidance`. - -### 1. `docs/api/resource-identifiers.md` — the launch-relevant fix (do first) - -**§ "Numeric `id` is a surrogate key" (line ~58).** Remove the final sentence -*"Use `id` as your durable foreign key when you mirror TrakRF data."* Replace with a reframe: - -- `id` is server-assigned, opaque, stable, and not arbitrarily rekeyed — usable as a - **sync / reconciliation anchor** when you mirror TrakRF data. -- It is **not** your business foreign key. Join your system of record on the natural key - (`external_key`) where one exists; reach for `id` only as the durable handle when no - natural key is available. - -Keep the existing factual properties (globally unique, opaque, minted from a non-reseeded -shared sequence) — those are correct and were just affirmed by TRA-885. Only the -prescriptive "use it as your FK" framing changes. Do **not** escalate the wording into an -absolute permanence guarantee. - -**§ intro (line ~7).** Light touch only — the existing framing already names `external_key` -as "canonical for partner-side joins / the natural key," which is correct. Ensure nothing in -the intro implies `id` is the partner-side join key. - -### 2. New subsection in `resource-identifiers.md` — per-entity join guidance - -Add a short subsection near the surrogate-key section stating the rule explicitly: - -- **Assets / locations:** join on `external_key` (your handle: SKU, asset tag, ERP code, - facility code). -- **General rule:** join on a stable natural key where one exists; where none does, `id` is - the durable handle — used as a reconciliation anchor, not exported as a business key. -- Note that the public integration surface is assets, locations, and tags; users and - organization administration are not public joinable resources in v1 (an integration is - scoped to a single organization via its API key). - -### 3. `docs/api/changelog.md` (line ~18) - -Reword the TRA-885 changelog entry to drop *"and use it as your durable foreign key"* / -*"use it as your durable foreign key."* Keep the global-uniqueness/opaque/permanent facts; -remove only the FK prescription so the changelog doesn't re-assert the corrected advice. - -### 4. `docs/api/versioning.md` — demote `id` - -Add a short note (under the stability commitment) that the surrogate `id` is a stable -internal anchor, **not** the integrator's business foreign key — integrators should key -their own systems on natural keys (`external_key`). The page already states the -additive-only / RFC 8594 breaking-change policy, so no new policy is needed. **Do not** add -an absolute `id`-permanence guarantee to the contract — that would re-sell the dependency -this ticket removes. - -## Out of scope - -- `external_key` uniqueness enforcement (a platform constraint) — separate follow-up. -- Membership / access audit log — separate follow-up. -- Any OpenAPI field-description edits (none needed; advice lives only in docs prose). -- Per-entity join guidance for users/orgs as joinable resources (not present on the public - surface — see verification). - -## Validation - -- `pnpm build` succeeds (Redocusaurus fetches the live spec; the prose edits are - Markdown-only and must not break the build). -- `pnpm typecheck` / `pnpm lint` clean. -- Internal doc links resolve (the new subsection anchor, and any cross-links to it). -- Manual read-through: the corrected pages no longer advise `id`-as-foreign-key anywhere, - and the reframe reads coherently against the surrounding text. diff --git a/superpowers/specs/2026-05-30-tra-892-docs-hygiene-bundle-design.md b/superpowers/specs/2026-05-30-tra-892-docs-hygiene-bundle-design.md deleted file mode 100644 index db9c0b6..0000000 --- a/superpowers/specs/2026-05-30-tra-892-docs-hygiene-bundle-design.md +++ /dev/null @@ -1,78 +0,0 @@ -# TRA-892 — Docs-hygiene bundle: 3 one-line API-docs fixes + groom the `contact.url` deferral - -**Date:** 2026-05-30 -**Ticket:** TRA-892 -**Type:** Docs-only PR (4 wording/backlog edits) + one Linear grooming comment - -## Problem - -The 2.6.1 black-box fleet (runs bb-261/262/263, svc `v1.1.1-165-g8ca09556`) surfaced four -documentation-hygiene items. **None gate the prod launch** — zero service/contract defects -were found. Each is a one-line wording correction or backlog grooming, safe to land any time. - -## Prerequisite verification (done before writing) - -Verified each claim against the live preview (`app.preview.trakrf.id`) and the current docs: - -| Claim to verify | Finding | Consequence | -| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -| `AssetView` has no `location_id` field | Confirmed — current location is exposed only on `GET /reports/asset-locations` and `GET /assets/{id}/history` (`tracking:read`); `resource-identifiers.md` already states this correctly | Fix only the BB onboarding-wrapper shorthand (item 1) | -| Empty `PATCH {}` still advances `updated_at` | Confirmed by ticket's live evidence (200, `updated_at` drift); `quickstart.mdx:167` already documents the always-advance behavior in full | Add a one-line qualifier at the compressed mention `quickstart.mdx:145-146` (item 2) | -| Docs-origin `/health.json` lacks API/spec version | Confirmed — docs origin returns only the docs build stamp; `app` origin `/health.json` carries `version` + `spec_refreshed_at` | Point BB's version check at the app origin (item 3) | -| `info.contact.url` restored by design | Confirmed — live spec serves `info.contact.url: https://trakrf.id` (Stripe pattern: marketing root + `support@trakrf.id`), restored via platform TRA-882 (PR #440) | The `BACKLOG.md` "restore the URL when a helpdesk lands" deferral is now obsolete (item 4) | - -## Scope — four edits - -### Item 1 — `tests/blackbox/BB_PRE_KEY.md` (fixture-data section) - -`:47` currently reads as if `location_id` is a field on the asset object. Reword to state the -current location is resolved from scan history via the asset-locations report / history -endpoints, **not a field on the asset object**. Also reconcile `:49`'s parallel -"materialized `location_id` on every asset" phrasing so the doc stays internally consistent -after the item-1 fix (otherwise the two lines contradict). Both are minimal; the determinism -promise (every fixture asset has a resolvable current location) is preserved, only the -"it's a field" implication is removed. - -### Item 2 — `docs/api/quickstart.mdx` (§ minimal PATCH) - -`:145-146` compresses "`{}` is a documented no-op" too far. Reword to "a documented no-op -**against settable fields** (it still advances `updated_at`)". Chosen over "link to the design -note" because the same code block's prose at `:167` already deep-links -`design-notes#updated_at-is-an-optimistic-concurrency-token-on-patch`; the inline qualifier -closes the gap where a reader stops at the comment. - -### Item 3 — `tests/blackbox/BB.md` (context-block version check) - -`:209` tells the BB harness to fetch the spec/build version from `$API_TEST_DOCS_URL/health.json`, -which carries only the docs build stamp. Repoint it at `$API_TEST_APP_URL/health.json` (the -authoritative `version` + `spec_refreshed_at` surface) and note that the docs origin only -carries the docs build stamp, so the cohort stops filing "version missing" where-to-look nits. -Chosen over surfacing the API/spec version on the docs host — that is an infra/build change -outside doc-hygiene scope, and the ticket explicitly offers the BB.md correction as the -alternative. - -### Item 4 — groom the `contact.url` deferral - -TRA-743 (which removed `info.contact.url`) is **already Done**; the URL has since been restored -by design (TRA-882). So "close as resolved" reduces to grooming the residue: - -- **Docs:** remove the now-stale `tests/blackbox/BACKLOG.md:47` row ("Restore `info.contact.url` - … when a helpdesk lands"). The deferred work is done — keeping the row would advertise a - removal that no longer reflects reality. -- **Linear:** add a comment to TRA-743 recording the course reversal (URL restored by design, - Stripe pattern, `https://trakrf.id`) and that the BACKLOG deferral has been removed as obsolete. - No state change — the ticket is already Done. - -## Out of scope - -- Surfacing API/spec version on the docs host (item 3's other option) — infra/build change. -- Any platform/spec change — `info.contact.url` is already correct on the live spec; no paired - platform edit is needed for any item. -- Touching `tests/blackbox/.env.local` — already re-keyed this session, unrelated. - -## Verification - -- `pnpm build` and `pnpm lint` pass (Redocusaurus fetches the live spec at build time; no spec - change here, so the build is unaffected). -- The three doc edits are wording-only; the BACKLOG edit removes one table row. -- TRA-743 grooming comment posted. diff --git a/superpowers/specs/2026-07-29-tra-1045-app-tour-regenerate-design.md b/superpowers/specs/2026-07-29-tra-1045-app-tour-regenerate-design.md deleted file mode 100644 index 4abc009..0000000 --- a/superpowers/specs/2026-07-29-tra-1045-app-tour-regenerate-design.md +++ /dev/null @@ -1,228 +0,0 @@ -# TRA-1045 — App Tour regenerate: Scan rename, Home/Barcode removal, tile relabels - -**Date:** 2026-07-29 -**Ticket:** TRA-1045 (blocked by TRA-1046, prod release) -**Branch:** `docs/tra-1045-app-tour-regenerate` - -## Problem - -The nav restructure (TRA-1029), the RFID/Barcode mode toggle (TRA-1031), and the Scan-tile -declutter (TRA-1036) all shipped to preview. The customer docs still describe the old UI. - -Verified against preview `v1.2.0-535-g7cbd1a31` on 2026-07-29: - -- `#home`, `#inventory`, and `#barcode` all redirect to `#scan` -- The page header reads **Scan**; the sidebar entry is **Scan** -- The RFID / Barcode mode toggle is live in the Scan toolbar -- With no reconcile list loaded, exactly two tiles render: **Scans** ("Live scan results") and - **Assets** ("Recognized assets") - -Two drifts the ticket's grep could not detect, because they are absences rather than strings: - -- `docs/app-tour/inventory.md:19` documents a **Sample** button. It no longer exists. The live - toolbar is Start · Reconcile · Clear · audio · Share · Save. -- The Scan tab has a **location-tag selector** ("No location tag detected" + Select) that no - page mentions. - -## Scope - -**In:** full app-tour regenerate from live observation; org-management authoring; terminology -sweep; redirects; screenshot recapture; an infra-ops passthrough in the justfile. - -**Out:** Mustering and Kits (explicitly excluded by the operator); Webhooks (TRA-1048); the -OpenAPI reference, which is fetched live at build; the stale `v1.1` language in -`docs/api/resource-identifiers.md:216` and `docs/api/changelog.md:307`. - -The `barcode` hits in `docs/api/*` are the `tag_type` enum value (`rfid` / `ble` / `barcode`), -not the removed tab. They stay. - -## Page map — `docs/app-tour/` - -| Action | Page | -| --- | --- | -| `git mv` | `inventory.md` → `scan.md` | -| delete | `home.md`, `barcode.md` | -| new | `readers.md`, `live-feed.md`, `outputs.md`, `geofence-defaults.md` | -| rewrite from observation | `index.md`, `locate.md`, `assets.md`, `locations.md`, `reports.md`, `settings.md`, `help.md` | -| update | `authoring.md` | - -`sidebars.ts` mirrors the app's own nesting: a **Settings** category (linking to `settings`) -containing Readers, Live feed, Outputs, and Geofence defaults, since that is how the app groups -them under Settings in the sidebar. - -`src/components/AppTourGrid.tsx` hardcodes all nine tour entries in an `ENTRIES` array. It is -rewritten to match the new page set. - -## Redirects - -`@docusaurus/plugin-client-redirects` is **not installed** — `docusaurus.config.ts` has a -`presets` block and no `plugins` array at all. Add the dependency and a `plugins` entry: - -- `/app-tour/inventory` → `/app-tour/scan` -- `/app-tour/home` → `/app-tour/scan` -- `/app-tour/barcode` → `/app-tour/scan` - -This mirrors the hash-route redirects the app itself performs, all three verified in preview. - -## Terminology rules - -1. **"Inventory" is reserved vocabulary** for the future Inventory module (one of the four - capabilities: `inventory`, `geofence`, `mustering`, `kitting`). It must not describe scanning - anywhere, including common-noun uses such as `docs/user-guide/asset-management.md:4` - ("save an inventory"). Otherwise this rename is re-done when the module ships. -2. **"Barcode"** survives only as a scan mode and as an API `tag_type`. Never a tab. -3. **"Home"** never names a screen. -4. Tiles are documented in shipped order — **Scans → Assets → Found → Missing → Extra** — with - the two-tile no-reconcile state stated explicitly. - -## Screenshots - -### Current state is worse than the ticket describes - -- `static/img/user-guide/inventory-scanning.png` is a **full-browser capture including the - author's personal bookmarks bar** — Gmail, Google Calendar, "Your AI credentials…", - Jobs/gigs, BizDev, and a profile avatar are all legible. This is published customer - documentation. It is a privacy defect independent of the UI drift. -- `static/img/user-guide/assets-populated.png` is viewport-only (no leak) but shows placeholder - asset names — `sss`, `rr`, `qq`, `pp`, `oo`, `nn`, `mm`, `ll`, `kk`, `jj`. -- The set is visually inconsistent: `inventory-scanning.png` is dark theme, - `assets-populated.png` is light. -- **Every** published app image carries a purple "Preview Environment" band and a `[PRE]` page - title. No customer's app looks like that. - -Therefore recapture covers **every user-guide image showing the app**, not only the two the -ticket names. All captures are viewport-only via Playwright MCP (never full-browser), in a -single consistent theme. - -### Capture source: platform local dev - -All images are captured against **platform running locally**, not preview. Three properties make -this strictly better than capturing from preview: - -1. **Prod-like chrome.** `environmentLabel` comes from `window.__APP_CONFIG__`, injected by the - backend at serve time (TRA-853). Vite dev has no injection, so the label defaults to `''` and - `isNonProd('')` is false — no purple banner, page title stays `TrakRF`. Preview cannot - suppress its banner. -2. **Automated scanner access.** `ble-mcp-test` is already a frontend dev dependency with two - usable modes. A **bridge server** fronts a real CS108 — one is running on `knuckles.local`, - verified reachable on WS 8080 and HTTP 8081 — selected via `BLE_MCP_HOST` plus - `VITE_BLE_BRIDGE_ENABLED=true` (see `frontend/tests/config/ble-bridge.config.ts` and - `vite.config.ts`). Failing that, the `@ui-only` path needs no hardware at all: - `navigator.bluetooth.testing.simulateNotification` via - `frontend/tests/e2e/helpers/trigger-utils.ts`, plus `connectToDevice()` / - `waitForConnectionStatus()` in `helpers/connection.ts`. - - Prefer the bridge with real hardware — genuine EPCs, signal strengths, and read counts beat - synthesized ones in customer documentation. Fall back to simulation if the reader is - unavailable, and say which was used in `authoring.md`. -3. **No writes to shared preview.** Seeding targets the local database, so BB tests are - untouched, no superadmin action is needed, and the "preview re-roll wipes the seeded org" - risk disappears. - -The cost is a heavier prerequisite: a platform checkout plus Docker (`just dev` brings up the -database and backend and runs migrations). `authoring.md` documents this, including the -requirement to run the commit that ships in TRA-1046 so the docs match what users get. - -Launch is `just frontend dev-bridge` (→ `VITE_BLE_BRIDGE_ENABLED=true node scripts/dev-bridge.js`), -which connects the app to the physical reader through the bridge. This needs no additional -setup: `scripts/dev-bridge.js:34` resolves `BLE_MCP_HOST || BLE_MCP_WS_HOST || 'localhost'`, and -`BLE_MCP_HOST` is already set in `~/platform/.env.local` (loaded by `.envrc` via -`dotenv_if_exists`, so every recipe in the platform tree inherits it). The -`public/web-ble-mock.bundle.js` symlink into `node_modules` is likewise already in place. - -Bridge settings live at the platform root, not in `frontend/.env.local` — the latter does not -exist. Alongside the host, the root file sets `BLE_MCP_WS_PORT`, `BLE_MCP_HTTP_PORT`, -`BLE_MCP_HTTP_TOKEN`, and `BLE_DEVICE_NAME=CS108`. - -The sidebar renders a version string, which will differ in local dev. Confirm what it shows and, -if it reads as a dev artifact, note it rather than pretending otherwise. - -### Seeded org - -Seeded into the **local** database via SQL, using the schema's own helpers rather than raw -INSERTs: - -- `trakrf.create_asset_with_tags(p_org_id, p_external_key, p_name, p_description, p_valid_from, - p_valid_to, p_is_active, p_metadata, p_tags)` -- `trakrf.create_location_with_tags(...)` — same shape plus `p_parent_location_id` - -Ids need no manual minting: `generate_obfuscated_id()` is a BEFORE-INSERT trigger. - -Asset names are plausible trackable equipment — Camera, Laptop, Tablet, Projector, Toolbox, -Pallet Jack — across a small location tree (Receiving / Warehouse A / Bay 3 / Office / Lab). -This replaces both the empty docs-tour org and the BB-contaminated Organized Chaos, which holds -rows such as `BB54-PY-CODEGEN-2` and `BB-1778367224-BB-ok`. - -The account is created by signing up through the local UI, so `org_users` membership and org -defaults are built by the app rather than hand-assembled. - -The capability grant is a single row: - -```sql -insert into trakrf.org_capabilities (org_id, capability, granted_at) -values (:org_id, 'geofence', now()); -``` - -Without it the org starts at zero grants (TRA-1024, no backfill) and Outputs and Geofence -defaults render as locked upsell pages, making them undocumentable. Grant only `geofence` — -leaving `mustering` and `kitting` ungranted keeps those surfaces out of the captures, matching -the agreed scope. - -The seed is checked in as `scripts/seed-docs-org.sql` so the whole environment is reproducible. - -### Known artifacts in captures - -A locked **Mustering** tile ("Not enabled for your organization") appears in every -nav-inclusive screenshot. This is accurate — it is what a real ungranted org sees — and is left -as-is rather than engineered away. - -### Reconcile state - -The seeded asset list plus simulated tag reads make the **five-tile** reconcile state reachable -(Scans → Assets → Found → Missing → Extra). Without a loaded list and a connected reader, only -the two-tile state can be captured. - -## Org management - -`docs/user-guide/organization-management.md` is currently a 13-line "Coming Soon" stub. It is -authored from live observation as +t2, covering the account-menu surface: Organization Settings, -Members, API Keys, and Create Organization. - -**Webhooks is excluded** — it belongs to TRA-1048. - -Note that TRA-891 settled that the users and org-admin *API* surfaces are internal-only. That -constrains the API docs, not this UI walkthrough, but the prose must not imply a public API for -membership management. - -## Infra-ops passthrough - -Copy the TRA-1053 delegation pattern from `~/platform/justfile` into the docs repo: `psql`, -`logs`, `gcp-auth`, and a general `ops `, each forwarding to the infra justfile via -`just --justfile "$infra_dir/justfile"`. The pattern deliberately does not restate cluster, -namespace, or CNPG knowledge, and does not mirror infra's recipe names. - -The sibling-directory fallback (`$(dirname main_worktree)/infra`) does **not** resolve on this -machine — the checkout is `~/trakrf-infra`, not `~/infra`. So `TRAKRF_INFRA_DIR` must be set in -`.env.local`, and documented in `.env.example`. Platform already depends on this. - -Related, out of scope, worth filing: `PG_URL_PREVIEW` in `~/platform/.env.local` is stale. It -points at Timescale Cloud, which refuses connections; preview is CNPG on GKE now. - -## Verification - -- `pnpm build` clean, no broken internal links -- No `Inventory`, `Barcode`, or `Home` as a tab or screen name under `docs/` -- No `Not Listed`, `Total Scanned`, or `Saveable` anywhere -- Tile docs match shipped order and the two-tile no-reconcile state -- All three redirects resolve -- No browser chrome or personal data in any published image -- Screenshots show plausible equipment names, not placeholder strings - -## Sequencing and risk - -The PR **opens but does not merge** until TRA-1046 deploys this UI to prod, so docs never -describe a UI users lack. Because sync-preview re-merges open PRs, the work is reviewable on the -preview docs site throughout the hold. - -Preview re-rolls will wipe the seeded org; `scripts/seed-docs-org.sql` plus a note in -`authoring.md` is the mitigation. diff --git a/superpowers/specs/2026-07-30-tra-1048-webhooks-v1-docs-design.md b/superpowers/specs/2026-07-30-tra-1048-webhooks-v1-docs-design.md deleted file mode 100644 index 5d46ac7..0000000 --- a/superpowers/specs/2026-07-30-tra-1048-webhooks-v1-docs-design.md +++ /dev/null @@ -1,185 +0,0 @@ -# TRA-1048 — Webhooks v1 documentation - -Customer-facing documentation for the webhooks shipped in TRA-1043 (platform PR #526). - -## Problem - -`docs/api/webhooks.md` already exists, and it is wrong in almost every particular. It was -written as a roadmap placeholder ("Planned for v1.x — not available yet") describing a -webhook system TrakRF intended to build. What shipped is narrower and has different -guarantees: - -| The existing page claims | What shipped | -| ------------------------------------------------------ | --------------------------------------------- | -| Four event types at launch | One: `asset.moved` | -| At-least-once delivery | **At-most-once** | -| Exponential backoff with jitter over ~24 hours | Two retries (~1s, ~5s, jittered), then dropped | -| A queryable delivery log | None in v1 | -| Optional per-event filters at registration | None in v1 | -| Payloads carry scan device details | Logical data only — no device, EPC, or scan id | - -Shipping the site with this page intact would be worse than having no webhooks page at -all: it promises reliability guarantees TrakRF does not provide, and an integrator who -builds against "at-least-once with a 24-hour retry ladder" will silently lose events. - -This is therefore a **rewrite in place**, not a new page. Three other pages carry stale -webhook claims, and the webhook CRUD routes are missing from the private-endpoints -catalog. - -The webhook surface documents itself nowhere. The CRUD routes are deliberately tagged -`webhooks,internal` with no `public` swagger tag and no `RequireScope` — registering a -webhook is an org-settings action, not something an integrator's API key does — so they -are absent from the generated OpenAPI reference that Redocusaurus renders. Every word of -this is hand-written prose. - -## Audience - -An integrator writing a receiver. The page is read top-to-bottom once, then used as a -reference while debugging a handler that isn't firing. - -## Ground truth - -Verified directly against the shipped implementation rather than taken from the ticket -text. Sources: `backend/internal/webhook/{payload,sign,client,sink}.go`, -`backend/internal/assetevent/{event,dispatcher,evaluator}.go`, -`backend/internal/handlers/webhooks/webhooks.go`, -`backend/internal/models/webhook/webhook.go`, `backend/migrations/`. - -Three findings refine what the ticket specified: - -1. **The signed timestamp is `occurred_at`, not send time.** `client.go` derives - `X-TrakRF-Timestamp` from `ev.OccurredAt.UTC().Unix()`. It is therefore stable across - the retries of one delivery, and a receiver's tolerance window is measured against the - scan instant, not the transmission instant. The ticket's "~5 minute tolerance" advice - is still right, but the page must say what the timestamp actually is or a receiver - author will assume send time and set the window too tight. - -2. **Ids are obfuscated at the database layer, not in application code.** `assets.id` is - `BIGINT` populated by the `trakrf.generate_obfuscated_id()` trigger (migration - `000006`), whose output range is `[0, 2^52)`. The webhook payload selects `id` straight - from the table, so the wire id **is** the obfuscated surrogate id and does match the id - the REST API returns. The consequence for the docs: the ticket's example ids (`555`, - `10`, `20`) are unrealistically small and would lead an integrator to size a 32-bit - field. Per operator decision, the example uses realistic 52-bit values instead of the - ticket's verbatim payload. - -3. **Signing secrets are `whsec_` + 64 hex characters**, and every response after create - returns `Mask(secret)` — prefix, ellipsis, last four. The ticket says the secret is - shown once; the format and the masking are worth stating so a user recognizes the - masked form as expected rather than as corruption. - -Everything else in the ticket verified as written: the envelope shape, all five headers, -the HMAC construction, at-most-once, the `{1s, 5s}` jittered ladder, the 5-second timeout, -refused redirects, and the SSRF guard. - -One further detail the code makes clear and the page should carry: the address guard runs -in the dialer's `Control` hook, so it inspects the **resolved IP per connection**. -`ValidateTargetURL` at registration only checks scheme and host. A customer can therefore -register a target that resolves into a blocked range with no error, and see only silent -delivery failures afterward. That asymmetry is exactly the kind of thing that becomes a -support ticket. - -Also: when an org's entitlement lapses, skipped events are **dropped, never buffered** — -reinstating a subscription does not replay the gap. - -## Design - -### `docs/api/webhooks.md` — rewrite - -Section order follows the receiver author's path: - -1. **What it is.** Delta-only `asset.moved` push delivery; the alternative to polling - `GET /api/v1/reports/asset-locations`. Available v1.3. -2. **What fires an event.** A pure delta — an event fires only when an asset is scanned at - a location different from where it was last seen. Rescans at the same location send - nothing; this is not a heartbeat. Carries the demo-carousel measurement (8 events from - 815 scans) as the concrete illustration, which is the fastest way to correct the - "webhook = scan feed" misreading. -3. **Registering a webhook.** Account menu → **Webhooks**, or Organization Settings → - **Manage webhooks →**. Admin required. One per organization. The signing secret is - displayed exactly once on create, with no read-back and no rotation — losing it means - delete and re-create. **Send test event** fires a synthetic `asset.moved` using - obviously-fake identifiers (`TEST-ASSET`, "Test Origin" → "Test Destination") and - reports the status code returned. The enable toggle stops delivery without deleting the - registration. Screenshots land here. -4. **The payload.** The envelope, then a field-reference table. `from_location` is present - and explicitly `null` — never omitted — for a genuine first-ever sighting. No - `sequence` field. Logical data only. -5. **Headers.** Five-row table. -6. **Verifying the signature.** The construction (`HMAC-SHA256` over - `timestamp + "." + rawBody`, hex, `sha256=` prefix); the raw-body-not-reparsed warning - as a prominent callout; Node and Python snippets; why both use a constant-time compare; - the tolerance window plus the `occurred_at` nuance from Ground truth. -7. **Delivery semantics.** At-most-once; two jittered retries then dropped, no DLQ and no - replay; ordering not guaranteed, order by `occurred_at`; duplicates possible on the - retry path, use `delivery_id` for idempotency; anything but 2xx burns a retry; 5-second - timeout so acknowledge fast and work asynchronously; no event when the new scan has no - location. -8. **Endpoint requirements.** https only, publicly reachable; refused address ranges, with - the per-connection-at-delivery-time framing from Ground truth; redirects not followed, - a 302 is a failed delivery. -9. **Reconciling missed events.** Replaces the old "What you can do today" section. - Reframed from interim workaround to standing requirement: under at-most-once, periodic - reconciliation against `GET /api/v1/reports/asset-locations` is part of a correct - integration, not a stopgap. -10. **Events went quiet.** Troubleshooting checklist, leading with lapsed subscription and - the enable toggle — an integrator whose events stop needs billing on the list of - causes, not just their own infrastructure. -11. **Not in v1.** One short paragraph. No shapes, no dates — the Phase 2 designs are not - settled and naming specifics would create expectations. - -### Collateral - -- `docs/api/README.mdx` — the Webhooks link description still reads "status of outbound - delivery and the interim polling pattern". Rewrite for a shipped feature. -- `docs/api/resource-identifiers.md` — "When webhooks ship, events **will** fire…" moves - to present tense. -- `docs/api/private-endpoints.md` — add the six `/api/v1/webhooks*` routes to the endpoint - list. They are session-authenticated internal routes, which is precisely what that page - catalogs, and their absence is a gap now that the feature is live. -- `docs/api/versioning.md` — uses `webhooks:write` as a hypothetical example of a future - scope. Misleading now that webhook CRUD has shipped with no scope at all. Swap the - example for a different plausible one. -- `sidebars.ts` — move `api/webhooks` from the meta/tools tail (currently between - `changelog` and `postman`) to immediately after `rate-limits`, closing the functional - reference section. It is a capability now, not a status page. -- `docs/api/changelog.md` — add an entry. The page scopes itself to changes under - `/api/v1/` and webhook CRUD is internal, but outbound delivery is a new integrator-facing - contract, which is what the changelog exists to record. - -### Screenshots - -Three captures against preview, in a follow-up commit on the same PR so the prose is ready -to merge the moment TRA-1046 lands: the Webhooks screen empty state, the reveal-once secret -banner, and a test-fire result. Captured from a throwaway webhook — never a real secret. - -## Constraints - -- No documented field or behavior that is not in the shipped code. Where the ticket and the - code disagree, the code wins. -- Nothing from TRA-398 Phase 2 documented as "coming soon" with specifics. -- No Linear ticket references in the published prose. -- Docs build clean, no broken internal links. - -## Out of scope - -TRA-398 Phase 2 in its entirety: N subscriptions, per-event filters, additional event types, -the full retry ladder, DLQ, per-delivery logs, secret rotation, IP allowlisting. Email/SMS -notification (TRA-1044) is a different feature with different delivery guarantees. - -## Acceptance - -- A webhooks page exists under the API section, reachable from the docs nav. -- Payload, all five headers, and both verification snippets are present and correct against - the shipped implementation. -- At-most-once, no-ordering-guarantee, and lapsed-subscription behavior are each stated - explicitly rather than implied. -- The raw-body-not-reparsed warning is present. -- No stale "planned" webhook language survives anywhere in `docs/`. -- `pnpm build` succeeds with no broken internal links. - -## Release coupling - -Webhooks exist on preview only; production is on v1.2. This work must not reach -`docs.trakrf.id` before the TRA-1046 production deploy. The PR is held open deliberately and -merged, alongside the TRA-1045 PR, immediately after that deploy. From e0eed3e21989a5e2a0061140ec4b2ccc00b20128 Mon Sep 17 00:00:00 2001 From: Mike Stankavich Date: Sun, 30 Aug 2026 05:37:18 -0500 Subject: [PATCH 2/3] style: prettier the three docs/api pages that were failing the lint gate Formatting only, kept in its own commit so the ignore/untrack sweep beside it stays reviewable as a pure mechanical change. `prettier --check .` reported 24 failures on main; 21 of them were the tracked superpowers files, which the previous commit removed. These three are the real remainder and are customer-facing, so they get fixed rather than ignored. Changes are whitespace: table column realignment in id-format.md and resource-identifiers.md, one over-long table row rewrapped in errors.md, and one blank line moved in the errors.md validation-code list where Prettier normalises list spacing. No prose, no links and no code samples were touched. `pnpm lint` now exits zero, which is what makes the `validate` command declared in .claude/csw.json usable as a gate. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RWAqULDa91P8QxVD6hRaer --- docs/api/errors.md | 4 ++-- docs/api/id-format.md | 16 ++++++++-------- docs/api/resource-identifiers.md | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/api/errors.md b/docs/api/errors.md index 38ce50e..8bb1557 100644 --- a/docs/api/errors.md +++ b/docs/api/errors.md @@ -72,7 +72,7 @@ When filing a support ticket, include the `error.request_id` value (or the equiv | `bad_request` | 400 | Request body couldn't be parsed or attributed to a field — invalid JSON syntax, a top-level type mismatch (e.g. an array where an object is expected), or a `PATCH` body that is the literal JSON token `null`. No `fields[]`; `detail` describes the failure. Field-attributable type mismatches (`is_active: "true"` where boolean is expected) return `validation_error` with `fields[]` instead — see [validation_error vs bad_request](#validation_error-vs-bad_request). | No — fix the request | | `unauthorized` | 401 | Missing, malformed, revoked, or expired API key. The specific cause (missing header, wrong scheme, expired token, revoked key) is in `detail`. | No — re-auth | | `forbidden` | 403 | Valid key but insufficient scope for this endpoint | No — needs a key with the right scope | -| `not_found` | 404 | Resource lookup failed — an in-range path-param `id` that doesn't resolve (`GET /api/v1/assets/99999`, or a large but in-range value like `GET /api/v1/assets/2147483648`) or a sub-resource path doesn't exist (`GET /api/v1/assets/99999/history`). Surrogate ids no longer range-reject, so a non-resolving in-range id is a `404`, not a validation error — see [ID format](./id-format). A zero, negative, non-numeric, or int64-overflow path-param `id` (e.g. `0`, `-5`, `abc`, a 20-digit value) returns `400 validation_error` against the spec's path-param bounds, not 404 — see [path-parameter validation errors](#path-and-query-parameter-validation-errors). A list filter that resolves to zero rows returns 200 with empty `data[]`, not 404. | No — check the identifier | +| `not_found` | 404 | Resource lookup failed — an in-range path-param `id` that doesn't resolve (`GET /api/v1/assets/99999`, or a large but in-range value like `GET /api/v1/assets/2147483648`) or a sub-resource path doesn't exist (`GET /api/v1/assets/99999/history`). Surrogate ids no longer range-reject, so a non-resolving in-range id is a `404`, not a validation error — see [ID format](./id-format). A zero, negative, non-numeric, or int64-overflow path-param `id` (e.g. `0`, `-5`, `abc`, a 20-digit value) returns `400 validation_error` against the spec's path-param bounds, not 404 — see [path-parameter validation errors](#path-and-query-parameter-validation-errors). A list filter that resolves to zero rows returns 200 with empty `data[]`, not 404. | No — check the identifier | | `method_not_allowed` | 405 | The route does not accept the HTTP method you used (e.g. `PATCH` on a collection that only supports `GET` and `POST`). Allowed methods are listed in the response `Allow` header and mirrored in `detail` (e.g. `Allowed methods: GET, HEAD, POST`). | No — use a supported method | | `conflict` | 409 | Two cause classes. **Unique-constraint violation** — typically a duplicate `external_key` on `POST` or `POST /…/rename` for assets/locations, or a duplicate `(tag_type, value)` on tags. **Referential-integrity violation** — `DELETE /api/v1/locations/{location_id}` rejects when the location has active descendant locations or active placed assets; see [Locations: delete semantics](./resource-identifiers#locations-delete-semantics). | No — reconcile (unique: `GET` then `PATCH`; referential: remove dependents first) | | `unsupported_media_type` | 415 | Request body was sent with a `Content-Type` the endpoint doesn't accept, or the header is missing entirely. `POST` requires `application/json`; `PATCH` requires `application/merge-patch+json` ([RFC 7396](https://datatracker.ietf.org/doc/html/rfc7396)) exclusively — sending `application/json` to a `PATCH` endpoint returns `415` with `detail: "Content-Type must be application/merge-patch+json on PATCH operations"`, and sending `application/merge-patch+json` to a `POST` endpoint returns `415` with `detail: "Content-Type must be application/json"` (no method suffix on the POST side). A missing `Content-Type` header is rejected on every write method. See [HTTP method coverage → Request body Content-Type per method](./http-method-coverage#patch-content-type). | No — fix the `Content-Type` | @@ -206,11 +206,11 @@ Current `code` values (extensible): - `invalid_value` — a value-validation failure: the value was sent as explicit `null` on a non-nullable field, isn't one of the allowed values, fails a format check (email, URL, UUID), fails an enum check, or fails a validation TrakRF has not mapped to a more specific code. Use this code's path when the field _name_ was recognized but the value was wrong; see `unknown_field` for the unrecognized-name case. Integrators branching on `code` per the [tip above](#validation-errors) should treat the null-on-non-nullable case as a value error, not a missing-field error. - `unknown_field` — the request body contains a top-level key the schema does not declare. Distinct from `invalid_value` so integrators can branch on "typo'd field name" vs. "wrong value." Emitted by the strict-decoder pass that drives [`additionalProperties: false`](./pagination-filtering-sorting#validator-behavior-on-writes) on write request bodies; `fields[].field` names the offending key. Distinct from `invalid_context` (next entry) — `unknown_field` is "the API does not declare this field anywhere," while `invalid_context` is "the field exists elsewhere on the surface but is not allowed in this position." - `invalid_context` — a known field or parameter was supplied on a surface where it is not allowed. Two semantic categories share this code: - 1. **List-endpoint filter on a detail or write endpoint.** Any of the public-API list-endpoint filter parameters (`external_key`, `is_active`, `q`, `parent_id` / `parent_external_key`, `location_id` / `location_external_key`, `asset_id` / `asset_external_key`, `include_deleted`) emits `invalid_context` when it lands on a detail or write endpoint that doesn't declare it (e.g. `GET /api/v1/assets/{asset_id}?external_key=ABC`, `GET /api/v1/locations/{location_id}?include_deleted=true`). The accompanying `message` points at the list-endpoint sibling where the parameter is honored (e.g. `GET /api/v1/assets`) when one can be derived from the request path; `include_deleted` carries its specialized "soft-deleted records are not retrievable by id" wording so the natural-key recovery path stays discoverable. 2. **Field handed off to a dedicated write subresource on `PATCH`.** A field that is settable on the resource but only via a different verb emits `invalid_context` on `PATCH` when the body value differs from the current resource state. The fields: `external_key` (assets and locations) — mutable via `POST /api/v1/{resource}/{id}/rename`; and the `tags` collection (assets and locations) — mutable via `POST /api/v1/{resource}/{id}/tags` and `DELETE /api/v1/{resource}/{id}/tags/{tag_id}`. The accompanying `message` names the dedicated write path. Echoing the current value back is silently stripped under the [accept-if-matches](./resource-identifiers#read-shape-vs-write-shape) half of the rule — `invalid_context` only fires when the body value diverges from the live resource. Strict-typed clients switching over `code` should add an arm for this value to distinguish "known field on this API, wrong surface or verb here" from `unknown_field` ("the API does not declare this field anywhere") and from `read_only` ("field is truly server-managed; no partner-side write path exists at all"). See [Resource identifiers → Read shape vs. write shape](./resource-identifiers#read-shape-vs-write-shape) for the per-field code matrix. + - `too_short` — the field was present in the request body with a length below the documented minimum (e.g. `"name": ""` on a `min_length: 1` string field). Distinct from `required` (absent-key case) and from `invalid_value` (explicit-`null`-on-non-nullable case). `params.min_length` carries the constraint value. - `too_long` — string or collection length above the maximum - `too_small` — numeric value below the minimum diff --git a/docs/api/id-format.md b/docs/api/id-format.md index 8173988..05af006 100644 --- a/docs/api/id-format.md +++ b/docs/api/id-format.md @@ -39,18 +39,18 @@ This page is about the surrogate `id`. The string `external_key` (`ASSET-NNNN` / Surrogate ids no longer carry a dedicated "too large" rejection. A syntactically valid id that doesn't resolve takes the ordinary lookup path for its surface: -| Surface | Example | Result | -| ---------------------- | ------------------------------------------------ | --------------------------------------------------------------------------- | -| **Path parameter** | `GET /api/v1/assets/2147483648` | `404 not_found` — in range, simply doesn't resolve to a row | -| **Query `_id` filter** | `GET /api/v1/locations?parent_id=2147483648` | `200` with an empty `data[]` page — a filter that matches nothing | +| Surface | Example | Result | +| ---------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------- | +| **Path parameter** | `GET /api/v1/assets/2147483648` | `404 not_found` — in range, simply doesn't resolve to a row | +| **Query `_id` filter** | `GET /api/v1/locations?parent_id=2147483648` | `200` with an empty `data[]` page — a filter that matches nothing | | **Request-body FK** | `POST /api/v1/locations {"parent_id": 2147483648}` | `400 validation_error` / `code: fk_not_found` — the referenced row doesn't exist | The bounds controls are unchanged: -| Input | Result | -| ------------------------------------------------------- | ----------------------------------------------- | -| Path `0` or `-5` | `400 validation_error` / `code: too_small` | -| Non-numeric (`abc`) or an int64 overflow (20+ digits) | `400 validation_error` / `code: invalid_value` | +| Input | Result | +| ----------------------------------------------------- | ---------------------------------------------- | +| Path `0` or `-5` | `400 validation_error` / `code: too_small` | +| Non-numeric (`abc`) or an int64 overflow (20+ digits) | `400 validation_error` / `code: invalid_value` | ```http GET /api/v1/assets/2147483648 diff --git a/docs/api/resource-identifiers.md b/docs/api/resource-identifiers.md index 0a8e9b5..7763deb 100644 --- a/docs/api/resource-identifiers.md +++ b/docs/api/resource-identifiers.md @@ -505,7 +505,7 @@ Auto-mint computes **`MAX(live key) + 1`**: it takes the highest `ASSET-NNNN` / - **Middle-of-range deletes leave permanent gaps.** Soft-delete `ASSET-0002` while `ASSET-0003` is still live and the next auto-mint is `MAX(0003) + 1 = ASSET-0004`, never `ASSET-0002` — the gap is not backfilled. - **Only deleting the current highest live key frees its number.** Soft-delete the top `ASSET-0003` with nothing higher live and the next auto-mint re-issues `ASSET-0003`. -Because the number can be re-issued this way, a server-assigned `external_key` you saw before can, after a delete, later refer to a *different* resource. Don't cache an auto-minted key as a long-lived reference or infer a count from it — supply your own handle (below) when you need a stable, never-recycled identifier. (The surrogate `id`, by contrast, is globally unique and never reused — see [Numeric `id` is a surrogate key](#numeric-id-is-a-surrogate-key).) +Because the number can be re-issued this way, a server-assigned `external_key` you saw before can, after a delete, later refer to a _different_ resource. Don't cache an auto-minted key as a long-lived reference or infer a count from it — supply your own handle (below) when you need a stable, never-recycled identifier. (The surrogate `id`, by contrast, is globally unique and never reused — see [Numeric `id` is a surrogate key](#numeric-id-is-a-surrogate-key).) ```bash # Caller-supplied external_key From 6a0e3d7ec84456e9ca3197a87e0982b6c7b3d416 Mon Sep 17 00:00:00 2001 From: Mike Stankavich Date: Sun, 30 Aug 2026 05:38:19 -0500 Subject: [PATCH 3/3] docs: split shared working agreements into AGENTS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLAUDE.md carried two kinds of instruction with nothing marking the boundary: agreements shared across the trakrf repos, and rules specific to this being a Docusaurus site. Each repo kept its own copy of the shared half, so they drifted independently and none of them mentioned spec artifacts at all. AGENTS.md holds the shared half, byte-identical in docs, platform and infra. CLAUDE.md keeps only what is true here and imports it. AGENTS.md rather than a bespoke filename so any agent tool reads it, not only Claude Code; at the repo root rather than under .claude/ because a directory that reads as tool config is not where someone cloning the repo would look. Tracked and discoverable are different properties and only the first was covered. The split is justified by shared byte-identical content across three repos, not by size — imported files load at launch, so an import costs the same context as inlining. Both files are written to state rather than explain, on the same grounds: this text loads every session in three repos, so rationale that can be re-derived is not worth carrying. It covers materially more than the CLAUDE.md it replaces (spec and note policy, verification rules, the publishing gate) in fewer lines. Scope is narrow by the same logic: only what a fresh session would otherwise get wrong. Contributing rules already live in CONTRIBUTING.md and now have a single source there instead of a second copy. Worktrees are not mentioned — that is a workflow choice driven by .claude/csw.json, not one to impose on anyone cloning a public repo. ble-mcp-test is deliberately not in this family — separate org, published npm package, different governance — and keeps its own instructions. Pins the spec and plan output path, which is the point of the exercise rather than documentation for its own sake: the tooling defaults to docs/superpowers/, but docs/ here is the published tree, and Docusaurus globs it at build, so an ignored-but-present file renders locally while CI's fresh clone never sees it. CONTRIBUTING.md gains the merge policy it was missing and its branch-naming guidance is corrected. It documented feature/add-xyz while .claude/csw.json specifies /- and every branch actually cut uses the latter. Outside contributors have no ticket, so the plain / form is named for them. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RWAqULDa91P8QxVD6hRaer --- AGENTS.md | 25 ++++++++++++++++++ CLAUDE.md | 68 +++++++++++-------------------------------------- CONTRIBUTING.md | 17 +++++++++---- 3 files changed, 52 insertions(+), 58 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..bd94ac2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,25 @@ +# AGENTS.md — trakrf working agreements + +Identical in `docs`, `platform` and `infra` — change it in all three or none. +These repos are public. Add only what a fresh session gets wrong otherwise. +Branches, commits, merges: `CONTRIBUTING.md`. Tooling: `.claude/csw.json`. + +## Specs, plans and notes are never tracked + +Per-session artifacts, disposable by decision. Ignored: `superpowers/`, +`docs/superpowers/`, `.superpowers/`, `docs/notes/` — each repo's `CLAUDE.md` +names which to write to. A plan worth a reviewer's time goes in the PR body. + +**Re-used, or merely re-read?** Re-used lives in the repo under a _behaviour_ +name — `describe` blocks, log prefixes and artifact dirs included. Merely +re-read belongs on the ticket, with the narrative and evidence. Durable +conclusions go to `CLAUDE.md`, an ADR or a code comment. Keep ticket ids out of +published prose. + +## Working rules + +- Ask when requirements are unclear; never delete code without instruction +- Verify before claiming done: run it, read the output, report what it said +- Verify the premise, not just the citation +- Search wide, then filter — absence from a narrowed search is not absence +- No invented imports; keep files under 500 lines diff --git a/CLAUDE.md b/CLAUDE.md index 8a74488..f845145 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,55 +1,17 @@ # CLAUDE.md -This file provides guidance to Claude when working with code in this repository. - -## Project Overview - -Standalone **Docusaurus documentation site** for trakrf (`trakrf/docs`). Content-focused — no application logic, no backend, no monorepo. Just docs and configuration. - -## Package Manager - -**pnpm EXCLUSIVELY** — never use `npm` or `npx`. - -- `npx` → `pnpm dlx` -- `npm run` → `pnpm` - -## Common Commands - -```bash -pnpm dev # Local dev server -pnpm build # Production build -pnpm serve # Serve production build locally -pnpm typecheck # Type checking -pnpm lint # Linting -``` - -## Git Workflow - -**NEVER PUSH DIRECTLY TO MAIN BRANCH** - -1. ALL changes go through a Pull Request — no exceptions -2. Always create a feature/fix branch: `feature/add-xyz`, `fix/broken-xyz`, `docs/update-xyz` -3. NEVER squash merge — preserve individual commit history -4. NEVER merge without explicit confirmation -5. Use conventional commits: `feat:`, `fix:`, `docs:`, `chore:` -6. Prefer incremental commits over amending - -## Worktrees - -Use **`.claude/worktrees/`** for git worktrees — this is the canonical location across all trakrf repos. Claude Code's native worktree tool (`EnterWorktree`) writes there and cannot be redirected, so the convention matches the tool rather than fighting it. The directory is gitignored (narrowly — the rest of `.claude/` stays tracked). Do not reintroduce a root-level `.worktrees/` or a symlink bridge. - -## Style & Conventions - -- **TypeScript** for any config or custom components -- **ES Modules only** — no CommonJS -- **Prettier** for formatting -- **Conventional Commits** for git messages -- Keep files under 500 lines - -## AI Behavior Rules - -- Ask questions when requirements are unclear -- Never delete code without explicit instruction -- Verify builds pass before claiming completion -- Report actual status — no false optimism -- Use only verified packages — no hallucinated imports +@AGENTS.md + +Docusaurus documentation site (`trakrf/docs`) — content and config only. +Branches, commits and merges: [CONTRIBUTING.md](CONTRIBUTING.md). Tooling: +`.claude/csw.json`. + +- **pnpm exclusively** — `pnpm dlx` not `npx`, `pnpm` not `npm run` +- `pnpm dev` · `build` · `serve` · `typecheck` · `lint` (prettier, gates `validate`) +- TypeScript and ES modules for config and components +- **Specs and plans go to `superpowers/` at the root, never under `docs/`** — + Docusaurus globs `docs/`, so a stray file renders locally but never in CI +- Merging to `main` publishes to customers immediately; unreleased behaviour + waits as a draft PR. Preview builds against the preview API and is separate +- The OpenAPI spec is fetched from the live API at build, not checked in — spec + changes originate in `trakrf/platform` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 32954aa..d879086 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,15 +34,17 @@ pnpm dev ### 1. Create a Branch -```bash -# Branch naming: -# - feature/add-xyz (new features) -# - fix/broken-xyz (bug fixes) -# - docs/update-xyz (documentation content) +Branch as `/`, using a [conventional commit](https://www.conventionalcommits.org) +type — `feat`, `fix`, `docs`, `chore`, `style`, `refactor`, `test`. +```bash git checkout -b docs/update-getting-started ``` +Maintainers working from a tracked issue insert the ticket: +`/-`, e.g. `docs/tra-0000-update-getting-started`. Outside +contributors have no ticket and should use the plain `/` form. + ### 2. Write Your Content - Documentation lives in `docs/` as Markdown or MDX files @@ -110,6 +112,11 @@ Rules for the customer-facing release notes: - [ ] Links are not broken - [ ] Commit messages use conventional format +4. **How PRs are merged:** + + PRs are merged with `--merge`, never squashed and never rebased, so that + individual commit history is preserved. + ## Getting Help - **Questions?** Open a GitHub Discussion