4 Commits

Author SHA1 Message Date
5947dcdc86 Add ? prefix for sample files in file-list; fix bootstrap/upgrade
bootstrap.sh wrote .diachron-version to the temp clone directory
instead of the target project, causing upgrade.sh to fail. Fix that
and teach all three scripts (bootstrap, upgrade, diff-upstream) about
the new ? prefix convention in file-list.

Sample files (?-prefixed) are copied on bootstrap but left alone on
upgrade so user modifications are preserved. New samples introduced
in a newer framework version are still copied if absent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 06:58:09 -05:00
f0aca17a0a Pass URL parameters from path-to-regexp match into Call.parameters
Call.parameters was hardcoded to a placeholder object. Now the matched
route parameters are threaded through from the dispatcher to the handler
so handlers can access e.g. call.parameters.word.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 17:24:26 -05:00
5be7b84972 Point CLAUDE.md and AGENTS.md to diachron/AGENTS.md
Ensures coding agents auto-discover the framework guide regardless
of which root-level instruction file their tool reads.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 16:50:07 -05:00
ae077886ba Add diff-upstream.sh and document framework change workflow
New script extracts a diff of framework files against the upstream
ref in .diachron-version.  Both DIACHRON.md and diachron/AGENTS.md
now explain how to use it and recommend keeping framework changes
in discrete, well-described commits to ease upstreaming.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 16:43:43 -05:00
11 changed files with 272 additions and 20 deletions

44
AGENTS.md Normal file
View File

@@ -0,0 +1,44 @@
# Agent Instructions
Read and follow the instructions in `diachron/AGENTS.md`. That file
contains framework conventions, commands, and structure that apply to
all coding agents working on diachron-based projects.
This project uses **bd** (beads) for issue tracking. Run `bd onboard` to get started.
## Quick Reference
```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --status in_progress # Claim work
bd close <id> # Complete work
bd sync # Sync with git
```
## Landing the Plane (Session Completion)
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.
**MANDATORY WORKFLOW:**
1. **File issues for remaining work** - Create issues for anything that needs follow-up
2. **Run quality gates** (if code changed) - Tests, linters, builds
3. **Update issue status** - Close finished work, update in-progress items
4. **PUSH TO REMOTE** - This is MANDATORY:
```bash
git pull --rebase
bd sync
git push
git status # MUST show "up to date with origin"
```
5. **Clean up** - Clear stashes, prune remote branches
6. **Verify** - All changes committed AND pushed
7. **Hand off** - Provide context for next session
**CRITICAL RULES:**
- Work is NOT complete until `git push` succeeds
- NEVER stop before pushing - that leaves work stranded locally
- NEVER say "ready to push when you are" - YOU must push
- If push fails, resolve and retry until it succeeds

View File

@@ -3,6 +3,10 @@
This file provides guidance to Claude Code (claude.ai/code) when working with This file provides guidance to Claude Code (claude.ai/code) when working with
code in this repository. code in this repository.
Read and follow the instructions in `diachron/AGENTS.md`. That file
contains framework conventions, commands, and structure that apply to
all coding agents working on diachron-based projects.
## Project Overview ## Project Overview
Diachron is an opinionated TypeScript/Node.js web framework with a Go-based Diachron is an opinionated TypeScript/Node.js web framework with a Go-based

View File

@@ -88,8 +88,24 @@ There are two owners of files in a diachron project:
`backend/diachron/`, and the top-level scripts (`cmd`, `develop`, `mgmt`, `backend/diachron/`, and the top-level scripts (`cmd`, `develop`, `mgmt`,
`sync.sh`, `check.sh`). `sync.sh`, `check.sh`).
Don't modify framework-owned files. This separation keeps framework upgrades Don't modify framework-owned files unless you need to. This separation
clean. keeps framework upgrades clean. If you do need to change framework files
(especially early on, there are rough edges), you can extract your changes
as a patch:
```bash
./diff-upstream.sh # full diff against upstream
./diff-upstream.sh --stat # just list changed files
```
This diffs every file in `file-list` against the upstream ref recorded in
`.diachron-version`.
When you do change framework files, make each change in its own commit with
a clear message explaining what the change is and why it's needed. Mixing
framework fixes with application work in a single commit makes it much
harder to upstream later. A clean history of discrete, well-explained
framework commits is the easiest thing to turn into contributions.
## Getting started ## Getting started

View File

@@ -61,7 +61,7 @@ const makeApp = ({routes, processTitle}: MakeAppArgs) => {
console.log("DEBUG: trying pattern, match result =", match); console.log("DEBUG: trying pattern, match result =", match);
if (match) { if (match) {
console.log("match", match); console.log("match", match);
const resp = await pr.handler(req); const resp = await pr.handler(req, match.params);
return resp; return resp;
} }

View File

@@ -29,6 +29,7 @@ const processRoutes=(routes:Route[]) :ProcessedRoutes => {
const handler: InternalHandler = async ( const handler: InternalHandler = async (
expressRequest: ExpressRequest, expressRequest: ExpressRequest,
params: Record<string, string>,
): Promise<Result> => { ): Promise<Result> => {
const method = massageMethod(expressRequest.method); const method = massageMethod(expressRequest.method);
@@ -47,7 +48,7 @@ const processRoutes=(routes:Route[]) :ProcessedRoutes => {
pattern: route.path, pattern: route.path,
path: expressRequest.originalUrl, path: expressRequest.originalUrl,
method, method,
parameters: { one: 1, two: 2 }, parameters: params,
request: expressRequest, request: expressRequest,
user: auth.user, user: auth.user,
session: new Session(auth.session, auth.user), session: new Session(auth.session, auth.user),

View File

@@ -29,13 +29,13 @@ export type Call = {
pattern: string; pattern: string;
path: string; path: string;
method: Method; method: Method;
parameters: object; parameters: Record<string, string>;
request: ExpressRequest; request: ExpressRequest;
user: User; user: User;
session: Session; session: Session;
}; };
export type InternalHandler = (req: ExpressRequest) => Promise<Result>; export type InternalHandler = (req: ExpressRequest, params: Record<string, string>) => Promise<Result>;
export type Handler = (call: Call) => Promise<Result>; export type Handler = (call: Call) => Promise<Result>;
export type ProcessedRoute = { export type ProcessedRoute = {

View File

@@ -42,8 +42,8 @@ echo working dir: $PWD
# exit 0 # exit 0
tar cvf - $(cat "$PWD/file-list" | grep -v '^#') | (cd "$here" && tar xf -) tar cvf - $(cat "$PWD/file-list" | grep -v '^#' | sed 's/^?//') | (cd "$here" && tar xf -)
echo "$ref" > .diachron-version echo "$ref" > "$here/.diachron-version"
echo "Now, run the command ./sync.sh" echo "Now, run the command ./sync.sh"

View File

@@ -137,6 +137,14 @@ Do not edit: master/*, logger/*, diachron/*, backend/diachron/*
``` ```
If a task requires framework changes, confirm with the user first. If a task requires framework changes, confirm with the user first.
When framework files are modified, the changes can be extracted as a
diff against upstream with `./diff-upstream.sh` (or `--stat` to list
changed files only).
When committing framework changes, keep them in separate commits from
application code. Each framework commit should have a clear message
explaining what was changed and why. This makes it much easier to
upstream the changes later.
### Command safety tiers ### Command safety tiers
@@ -179,6 +187,21 @@ framework. When you create or delete a file that is part of the project
(not a scratch file or generated output), you must update `file-list` to (not a scratch file or generated output), you must update `file-list` to
match. Keep it sorted alphabetically. match. Keep it sorted alphabetically.
Entries can have a `?` prefix (e.g. `?backend/app.ts`). These are
**sample files** -- starter code that `bootstrap.sh` copies into a new
project but that `upgrade.sh` will not overwrite. Once the user has the
file, it belongs to them. On upgrade, new sample files that don't exist
yet in the project are copied in; existing ones are left untouched.
Unprefixed entries are **framework-owned** and are always replaced on
upgrade. When adding a new file to `file-list`, decide which category
it belongs to:
- Framework-owned (no prefix): infrastructure scripts, framework
library code, build tooling, config that must stay in sync.
- Sample (`?` prefix): application starter code the user is expected
to edit (routes, handlers, services, types, package.json, etc.).
## Things to avoid ## Things to avoid
- Do not introduce `.env` files or `dotenv` without checking with the - Do not introduce `.env` files or `dotenv` without checking with the

116
diff-upstream.sh Executable file
View File

@@ -0,0 +1,116 @@
#!/bin/bash
# Generate a diff of framework files against the upstream version this
# project is based on. Useful for contributing changes back to diachron.
#
# Usage:
# ./diff-upstream.sh # diff against .diachron-version
# ./diff-upstream.sh <ref> # diff against a specific ref
# ./diff-upstream.sh --stat # show changed files only
set -eu
set -o pipefail
IFS=$'\n\t'
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
stat_only=false
ref=""
for arg in "$@"; do
case "$arg" in
--stat) stat_only=true ;;
*) ref="$arg" ;;
esac
done
if [ -z "$ref" ]; then
if [ ! -f "$DIR/.diachron-version" ]; then
echo "Error: .diachron-version not found and no ref specified." >&2
echo "Usage: $0 [--stat] [<ref>]" >&2
exit 1
fi
ref=$(cat "$DIR/.diachron-version")
fi
cached_repo="$HOME/.cache/diachron/v1/repositories/diachron.git"
if [ ! -d "$cached_repo" ]; then
echo "Error: cached repository not found at $cached_repo" >&2
echo "Run ./update-cached-repository.sh first." >&2
exit 1
fi
# Update cached repo
"$DIR/update-cached-repository.sh"
# Verify ref exists
if ! git -C "$cached_repo" rev-parse --verify "$ref^{commit}" >/dev/null 2>&1; then
echo "Error: ref '$ref' not found in cached repository." >&2
exit 1
fi
# Read file-list (strip ? prefix from sample entries)
files=()
while IFS= read -r line; do
[[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ -z "$line" ]] && continue
files+=("${line#\?}")
done < "$DIR/file-list"
# Check out upstream into a temp directory
tmpdir=$(mktemp -d)
cleanup() { rm -rf "$tmpdir"; }
trap cleanup EXIT
git clone --quiet "$cached_repo" "$tmpdir/upstream"
git -C "$tmpdir/upstream" checkout --quiet "$ref"
# Generate diff
if $stat_only; then
diff -rq "$tmpdir/upstream" "$DIR" \
--no-dereference \
2>/dev/null \
| grep -v '^\.' \
|| true
# Simpler: just list files that differ
for f in "${files[@]}"; do
# Skip directories
[ -d "$DIR/$f" ] && continue
upstream="$tmpdir/upstream/$f"
local="$DIR/$f"
if [ ! -f "$upstream" ] && [ -f "$local" ]; then
echo "added: $f"
elif [ -f "$upstream" ] && [ ! -f "$local" ]; then
echo "removed: $f"
elif [ -f "$upstream" ] && [ -f "$local" ]; then
if ! diff -q "$upstream" "$local" >/dev/null 2>&1; then
echo "modified: $f"
fi
fi
done
else
for f in "${files[@]}"; do
[ -d "$DIR/$f" ] && continue
upstream="$tmpdir/upstream/$f"
local="$DIR/$f"
if [ ! -f "$upstream" ] && [ -f "$local" ]; then
diff -u /dev/null "$local" \
--label "a/$f" --label "b/$f" \
|| true
elif [ -f "$upstream" ] && [ ! -f "$local" ]; then
diff -u "$upstream" /dev/null \
--label "a/$f" --label "b/$f" \
|| true
elif [ -f "$upstream" ] && [ -f "$local" ]; then
diff -u "$upstream" "$local" \
--label "a/$f" --label "b/$f" \
|| true
fi
done
fi

View File

@@ -1,31 +1,37 @@
# please keep this file sorted alphabetically # please keep this file sorted alphabetically
#
# Files prefixed with ? are sample/starter files. bootstrap.sh copies them
# into a new project, but upgrade.sh will not overwrite them if the user has
# already modified or replaced them. Unprefixed files are framework-owned
# and are always replaced on upgrade.
.gitignore .gitignore
.go-version .go-version
DIACHRON.md DIACHRON.md
backend/.gitignore backend/.gitignore
backend/.npmrc backend/.npmrc
backend/app.ts ?backend/app.ts
backend/build.sh backend/build.sh
backend/check-deps.ts backend/check-deps.ts
backend/check.sh backend/check.sh
backend/diachron backend/diachron
backend/generated backend/generated
backend/group.ts ?backend/group.ts
backend/handlers.spec.ts ?backend/handlers.spec.ts
backend/handlers.ts ?backend/handlers.ts
backend/package.json ?backend/package.json
backend/pnpm-workspace.yaml backend/pnpm-workspace.yaml
backend/routes.ts ?backend/routes.ts
backend/run.sh backend/run.sh
backend/services.ts ?backend/services.ts
backend/show-config.sh backend/show-config.sh
backend/tsconfig.json backend/tsconfig.json
backend/types.ts ?backend/types.ts
backend/watch.sh backend/watch.sh
bootstrap.sh bootstrap.sh
cmd cmd
develop develop
diff-upstream.sh
diachron diachron
diachron/AGENTS.md diachron/AGENTS.md
file-list file-list

View File

@@ -62,11 +62,17 @@ echo "Upgrading: $old_ref -> $new_ref"
echo "" echo ""
# Read current file-list (files to remove) # Read current file-list (files to remove)
# Entries prefixed with ? are sample files -- we don't remove those on upgrade.
old_files=() old_files=()
old_samples=()
while IFS= read -r line; do while IFS= read -r line; do
[[ "$line" =~ ^[[:space:]]*# ]] && continue [[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ -z "$line" ]] && continue [[ -z "$line" ]] && continue
old_files+=("$line") if [[ "$line" == \?* ]]; then
old_samples+=("${line#\?}")
else
old_files+=("$line")
fi
done < "$DIR/file-list" done < "$DIR/file-list"
# Clone and checkout new version into a temp directory # Clone and checkout new version into a temp directory
@@ -76,25 +82,44 @@ git -C "$tmpdir/diachron" checkout --quiet "$new_ref"
# Read new file-list (files to add) # Read new file-list (files to add)
new_files=() new_files=()
new_samples=()
while IFS= read -r line; do while IFS= read -r line; do
[[ "$line" =~ ^[[:space:]]*# ]] && continue [[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ -z "$line" ]] && continue [[ -z "$line" ]] && continue
new_files+=("$line") if [[ "$line" == \?* ]]; then
new_samples+=("${line#\?}")
else
new_files+=("$line")
fi
done < "$tmpdir/diachron/file-list" done < "$tmpdir/diachron/file-list"
# Remove old framework files # Remove old framework files (not samples -- those belong to the user)
for f in "${old_files[@]}"; do for f in "${old_files[@]}"; do
git -C "$DIR" rm -rf --quiet --ignore-unmatch "$f" git -C "$DIR" rm -rf --quiet --ignore-unmatch "$f"
done done
# Copy in new framework files # Copy in new framework files
(cd "$tmpdir/diachron" && tar cvf - "${new_files[@]}") | (cd "$DIR" && tar xf -) (cd "$tmpdir/diachron" && tar cf - "${new_files[@]}") | (cd "$DIR" && tar xf -)
# Stage them # Stage them
for f in "${new_files[@]}"; do for f in "${new_files[@]}"; do
git -C "$DIR" add "$f" git -C "$DIR" add "$f"
done done
# Handle sample files: copy only if the user doesn't already have them
samples_added=()
samples_skipped=()
for f in "${new_samples[@]}"; do
if [ -e "$DIR/$f" ]; then
samples_skipped+=("$f")
else
# New sample that doesn't exist yet -- copy it in
(cd "$tmpdir/diachron" && tar cf - "$f") | (cd "$DIR" && tar xf -)
git -C "$DIR" add "$f"
samples_added+=("$f")
fi
done
# Update version marker # Update version marker
echo "$new_ref" > "$DIR/.diachron-version" echo "$new_ref" > "$DIR/.diachron-version"
git -C "$DIR" add "$DIR/.diachron-version" git -C "$DIR" add "$DIR/.diachron-version"
@@ -102,6 +127,23 @@ git -C "$DIR" add "$DIR/.diachron-version"
echo "=== Upgrade staged: $old_ref -> $new_ref ===" echo "=== Upgrade staged: $old_ref -> $new_ref ==="
echo "" echo ""
echo "Framework files have been removed, replaced, and staged." echo "Framework files have been removed, replaced, and staged."
if [ ${#samples_added[@]} -gt 0 ]; then
echo ""
echo "New sample files added:"
for f in "${samples_added[@]}"; do
echo " + $f"
done
fi
if [ ${#samples_skipped[@]} -gt 0 ]; then
echo ""
echo "Sample files skipped (you already have these):"
for f in "${samples_skipped[@]}"; do
echo " ~ $f"
done
fi
echo "" echo ""
echo "Next steps:" echo "Next steps:"
echo " 1. Review: git diff --cached" echo " 1. Review: git diff --cached"