syntaxai/tdd.md · commit 7c79a9c

Migrate 7 historical /goals + lock down authoring workflow

Closes the empirical-chain hole opened by /blog/sama-v2-goal-chain-gap.
Past /goals recovered into git; future /goals captured by construction
via a new feedback memory.

Extend GoalStatus to 5 values:
- pending   — in-flight; mergeSha/prNumber null
- shipped   — verbatim from PR body or this session's conversation
- lossy     — paraphrased from prior-session summary; warning banner prepended
- lost      — no recoverable text; registry-only entry, NO file on disk
- abandoned — PR closed without merging (no historical entries yet)

Detail handler (d21_handlers_goals.ts):
- status: "lost" → skip file read, render metadata-only + "Source could
  not be recovered" callout at 200 (not 404)
- status: "lossy" → prepend LOSSY_BANNER to the rendered body

Migration of 30 merged PRs against the 4-marker classification rule
(Goal: + Done when: + Constraints + Load-bearing files):
- shipped (3): git-url-drop-owner, sitemap-xml-impl, build-goals-registry
  — verbatim /goal text from this session's conversation
- lossy (5): v21-dialects-spec, working-set-polyglot, polyglot-baseline-n7,
  graph-depth-polyglot, sama-rewrite-v2-first — paraphrased opening from
  prior-session summary, full Done-when/Constraints/Load-bearing sections
  lost to summarization
- pending (1): migrate-historical-goals — this very /goal, flipped to
  shipped in the final commit before deploy
- lost (0): no PRs ended up with no recoverable text whatsoever
- excluded (~20): blog-post-only PRs, image redesigns, Containerfile
  hotfixes — not /goal-driven, don't pollute /goals with stubs

The /goal's expected lossy/lost mix landed as 5 lossy + 0 lost rather
than the 4-5 lossy + 2-4 lost estimate — honestly classified, every
prior-session goal has at least an opening-sentence recovery.

Workflow lock-in:
- New memory: feedback_goal_authoring_workflow.md. Rule: agent's FIRST
  tool call on /goal is to write verbatim to goals/<slug>.md with
  status: pending, commit as the first commit of the PR; PR body MUST
  include the verbatim /goal under a "## /goal" heading; final commit
  before deploy flips status: shipped + fills merge_sha.
- MEMORY.md indexed between feedback_jolo_mode and feedback_flatpak_host_tools.
- /goal.md (repo-root scratch buffer) added to .gitignore — in-flight
  content stays local, can't leak to commits.

Defense-in-depth: every future /goal lands TWICE in git — once as a
goals/ file, once as the PR body. Corruption of one preserves the other.

Tests: 402/402 pass (400 → 402, +2 new lossy/lost parsing cases).
This very PR dogfoods the new workflow — first commit of the branch
wrote goals/migrate-historical-goals.md before any other work.

Live-verify clause for "at least one lost entry" intentionally skipped:
no PRs in the migration produced lost entries (all had at least an
opening-sentence recovery). The lost codepath is tested in the unit
suite via the new "status: lost parses correctly" case + the handler
branches on status before the file read.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
author
syntaxai <[email protected]>
date
2026-05-25 15:06:38 +01:00
parent
9b78ba7
commit
7c79a9c1dfa5c4f434f93047f0380b556006f8bb

12 files changed · +383 −8

modified .gitignore +5 −0
@@ -11,3 +11,8 @@ playwright-report/
1111 test-results/
1212 .playwright/
1313 .auth/
14+
15+# /goal scratch buffer — in-flight /goal text is pasted here before
16+# being committed to goals/<slug>.md per the auth-workflow lock-in
17+# (see /blog/sama-v2-goal-chain-gap). Never commit goal.md itself.
18+/goal.md
added goals/build-goals-registry.md +67 −0
@@ -0,0 +1,67 @@
1+---
2+slug: build-goals-registry
3+title: Build /goals registry + site surface (goal #1)
4+date: 2026-05-25
5+branch: goals-registry-build
6+pr_number: 45
7+merge_sha: e923da8
8+status: shipped
9+related_posts: [sama-v2-goal-chain-gap]
10+---
11+
12+Goal: Persist /goal slash commands into git as first-class artifacts, mirroring the /blog and /sama patterns. New top-level directory `goals/` holds each goal as a markdown file with YAML frontmatter; the registry lives at src/a31_goals.ts; the site renders /goals (index) and /goals/<slug> (detail) pages; the sitemap automatically picks them up; the workflow change so /goal text always lands as a committed file follows in goal #2. Solves the "we lose /goals" problem at the storage layer first — once /goals/ is the source of truth, future authoring just writes to it.
13+
14+Done when:
15+- New top-level `goals/` directory exists; first migrated goal lives at goals/git-url-drop-owner.md (a placeholder file with the /goal text from PR #42 is fine — goal #2 will do the proper migration).
16+- Frontmatter format defined and parsed:
17+ ---
18+ slug: <kebab-case>
19+ title: <human-readable>
20+ date: <YYYY-MM-DD>
21+ branch: <git branch name used during the work>
22+ pr_number: <int|null>
23+ merge_sha: <short sha|null>
24+ status: pending|shipped|abandoned
25+ related_posts: [<post-slug>, ...]
26+ ---
27+ <goal body as written by the user>
28+- Layer 0 registry at src/a31_goals.ts:
29+ export interface GoalEntry { slug, title, date, branch, prNumber, mergeSha, status, relatedPosts }
30+ export const ALL_GOALS: GoalEntry[] = [...]
31+ Same shape as ALL_POSTS — drives /goals, /goals/:slug, and the sitemap.
32+- Layer 1 helper src/b32_goals_meta.ts: pure functions
33+ parseGoalFrontmatter(body: string) → { meta, body } (no I/O)
34+ findGoalByMergeSha(sha: string, all: ReadonlyArray<GoalEntry>) → GoalEntry | null
35+ Sibling test src/b32_goals_meta.test.ts covers: full frontmatter, missing optional fields, malformed frontmatter (returns null), SHA lookup hit/miss, short-vs-full SHA prefix matching (so /GIT/tdd.md/commit/968890f finds a goal whose merge_sha is 968890f8a3bc...).
36+- Layer 3 handlers in src/d21_handlers_goals.ts:
37+ goalsLandingHandler → /goals index (table: date · title · status · PR · commit)
38+ goalSlugHandler → /goals/:slug detail (rendered markdown, frontmatter badges, links to PR + commit + related posts)
39+- /goals routes registered in src/d21_app.ts.
40+- /goals link added to the main nav in src/b51_render_layout.ts:47 (between /sama and /blog).
41+- Sitemap automatically lists every ALL_GOALS entry via the existing b32_sitemap helper — extend src/d21_app.ts "/sitemap.xml" handler to map ALL_GOALS → /goals/<slug>; add "/goals" to STATIC_PATHS in src/b32_sitemap.ts.
42+- /goals/<slug> pages reference their merge commit via the new /GIT/tdd.md/commit/<merge_sha> link AND back-reference any related blog posts in related_posts[].
43+- All 388+ tests still pass; new helper test adds 5-7 cases.
44+- /sama/v2/verify still reports 7/7 ✓ (anti-fudge).
45+- Deployed; live-verify:
46+ * curl https://tdd.md/goals → 200 with HTML listing
47+ * curl https://tdd.md/goals/git-url-drop-owner → 200 with the /goal body rendered
48+ * curl https://tdd.md/sitemap.xml | grep -c /goals/ → at least 1
49+ * Goals index appears in main nav on every page
50+
51+Constraints (anti-fudge):
52+- One markdown file per goal — no JSON, no multi-goal files, no goal-text embedded inside another file.
53+- Filename is the slug (e.g. git-url-drop-owner.md), NOT the merge SHA. SHA lookup works via the frontmatter field. Rationale: readable URLs, no chicken-and-egg, /goals/<slug> is the canonical permalink. Documented in the file header of a31_goals.ts.
54+- ALL_GOALS is the single source of truth — no second list of goals in handlers or rendered HTML.
55+- Site language English-only.
56+- GitHub flow via flatpak-spawn.
57+- Do NOT change any §4 verifier logic.
58+- Frontmatter parser stays in Layer 1 (pure string-in, struct-out) — no fs.readFile, no path joining.
59+
60+Load-bearing files to read FIRST:
61+- src/a31_blog.ts (the registry pattern that ALL_GOALS should mirror)
62+- src/a31_sama.ts (same — second example of the pattern)
63+- src/d21_handlers_sama.ts (samaLandingHandler + samaSlugHandler — the index + detail pattern)
64+- src/d21_app.ts (route table — where /goals routes get registered)
65+- src/b32_sitemap.ts (extend STATIC_PATHS + add goals URLs to the handler's url list in d21_app.ts)
66+- src/b51_render_layout.ts:47 (nav strip — where /goals link goes)
67+- content/blog/sama-v2-sitemap-implementation-plan.md (the sitemap impl plan post is the closest existing pattern for this kind of registry-driven feature)
added goals/graph-depth-polyglot.md +18 −0
@@ -0,0 +1,18 @@
1+---
2+slug: graph-depth-polyglot
3+title: Port §5 graphDepth metric to Go (package-directory DAG) + Rust (Cargo crate DAG)
4+date: 2026-05-24
5+branch: sama-v2-graphdepth-polyglot
6+pr_number: 34
7+merge_sha: 832b7c9
8+status: lossy
9+related_posts: [sama-v2-go-project-dive, sama-v2-rust-project-ripgrep]
10+---
11+
12+**Recovered opening (from prior-session conversation summary, paraphrased):**
13+
14+> Goal: Port the §5 graphDepth metric to Go (package-directory DAG) and Rust (Cargo-workspace crate DAG). Measure dive and ripgrep at the same pinned SHAs as the workingSetFit run — one repo, two measured metrics, same source tree. The dive audit's hand-estimated graphDepth (~5) is the load-bearing claim to test.
15+
16+**What is preserved from the original /goal:** only the opening sentence above, recovered from the prior-session conversation summary at the start of this conversation. The full *Done when* clauses, *Constraints (anti-fudge)*, and *Load-bearing files* sections did not survive summarization.
17+
18+**What landed:** `src/b32_graph_depth_polyglot.ts` (pure Layer 1, memoised DFS with bounded-cycle handling) + `src/c14_go_graph_depth.ts` + `src/c14_rust_graph_depth.ts` (Layer 2 adapters parsing go.mod and Cargo.toml workspaces respectively). The Rust adapter caught and fixed a `[[bin]]`/`[[test]]` array-of-tables clobbering bug in the scoped TOML subset parser. Measured: dive @d6c69194 = 12 (estimate ~5 was wildly off; subdirectory hops folded into top-level in the eye-estimate); ripgrep @4519153e = 5 (estimate ~5 confirmed exactly via 10-crate DAG hand-trace). 31 new tests. See [PR #34](https://github.com/syntaxai/tdd.md/pull/34).
added goals/polyglot-baseline-n7.md +18 −0
@@ -0,0 +1,18 @@
1+---
2+slug: polyglot-baseline-n7
3+title: Run polyglot §5 workingSetFit emitter against 5 more CLI tools
4+date: 2026-05-24
5+branch: sama-v2-workingset-cross-repo-baseline
6+pr_number: 33
7+merge_sha: 60056b1
8+status: lossy
9+related_posts: [sama-v2-workingset-cross-repo-baseline]
10+---
11+
12+**Recovered opening (from prior-session conversation summary, paraphrased):**
13+
14+> Goal: Run the polyglot §5 workingSetFit emitter against 5 additional popular open-source CLI tools (sharkdp/bat, sharkdp/fd, eza-community/eza, jesseduffield/lazygit, cli/cli) at pinned SHAs. Joining the existing dive + ripgrep measurements, the corpus becomes n=7 cross-repo datapoints (4 Rust + 3 Go) measured against the same [50, 500] LOC bounds. Publish a blog post with the distribution and the convergence question answered.
15+
16+**What is preserved from the original /goal:** only the opening sentence above, recovered from the prior-session conversation summary at the start of this conversation. The full *Done when* clauses, *Constraints (anti-fudge)*, and *Load-bearing files* sections did not survive summarization.
17+
18+**What landed:** 7-datapoint cross-repo baseline with pinned SHAs throughout: tdd.md 80.00% (SAMA-disciplined), cli/gh 73.59%, sharkdp/fd 69.57%, lazygit 67.38%, eza 61.76%, ripgrep 54.00%, dive 52.17%, sharkdp/bat 46.27%. Range 27.32pp, mean 60.68%, sample stddev 10.13pp. The n=2 convergence (dive/ripgrep within 2pp) was confirmed as coincidence; 5 of 7 still cluster in [52%, 70%]. Hand-trace of bat (lowest measurement) included for /sama/v2 §0 auditability. See [PR #33](https://github.com/syntaxai/tdd.md/pull/33).
added goals/sama-rewrite-v2-first.md +18 −0
@@ -0,0 +1,18 @@
1+---
2+slug: sama-rewrite-v2-first
3+title: Rewrite /sama using the SV "latest is default, legacy preserved" pattern
4+date: 2026-05-24
5+branch: sama-v2-first-images
6+pr_number: 36
7+merge_sha: fd50ede
8+status: lossy
9+related_posts: []
10+---
11+
12+**Recovered opening (from prior-session conversation summary, paraphrased):**
13+
14+> Goal: Rewrite /sama using the Silicon Valley "latest is default, legacy is preserved" pattern (Stripe, Vue, React). Same canonical URL, v2-first content above the fold, v1 four-disciplines content preserved below a horizontal rule. Create three new images so the page isn't text-only: sama-hero (1200x630 with live-state strip), sama-layers (four-layer diagram with downward import arrows + §1.2 Law caption), sama-metrics (horizontal bar chart of the n=8 workingSetFit datapoints with tdd.md highlighted as SAMA-disciplined). Each v1 discipline page (/sama/sorted, /sama/architecture, /sama/modeled, /sama/atomic) gets a Stripe-style older-version banner prepended.
15+
16+**What is preserved from the original /goal:** only the opening sentence above, recovered from the prior-session conversation summary at the start of this conversation. The full *Done when* clauses, *Constraints (anti-fudge)*, and *Load-bearing files* sections did not survive summarization.
17+
18+**What landed:** /sama redesigned with v2-first content + three new images. Discipline pages got the Stripe-style v1 banner via `samaSlugHandler` in `d21_handlers_sama.ts`. URLs unchanged; v1 content preserved verbatim, just relocated. 367/367 tests pass; /sama/v2/verify still 7/7 ✓. See [PR #36](https://github.com/syntaxai/tdd.md/pull/36).
added goals/sitemap-xml-impl.md +50 −0
@@ -0,0 +1,50 @@
1+---
2+slug: sitemap-xml-impl
3+title: Add automatically-generated /sitemap.xml from existing registries
4+date: 2026-05-25
5+branch: sitemap-xml-impl
6+pr_number: 40
7+merge_sha: 3280af8
8+status: shipped
9+related_posts: [sama-v2-sitemap-implementation-plan]
10+---
11+
12+Goal: Add an automatically-generated /sitemap.xml so search engines and AI crawlers can index the full site without a hand-maintained URL list. The sitemap is generated on demand from the existing registries (ALL_POSTS, ALL_SAMA, the route table, the guides list wherever it lives), so a new blog post or discipline page lands in the sitemap immediately on deploy with zero human edit. Note: src/a31_blog.ts already declares in its top comment that ALL_POSTS "drives /blog, /blog/:slug, and the sitemap" — this goal makes that comment true.
13+
14+Done when:
15+- A new route /sitemap.xml returns 200 with Content-Type "application/xml; charset=utf-8" and a valid sitemaps.org 0.9 document:
16+ <?xml version="1.0" encoding="UTF-8"?>
17+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
18+ <url><loc>https://tdd.md/...</loc>[<lastmod>YYYY-MM-DD</lastmod>]</url>
19+ ...
20+ </urlset>
21+- URLs are derived from the registries (no hand-maintained slug list):
22+ * Every entry in ALL_POSTS → /blog/<slug> with <lastmod> = the post's date field.
23+ * Every entry in ALL_SAMA → /sama/<slug>.
24+ * Every guide entry from whichever registry exists (search for ALL_GUIDES, GUIDES, or grep src/d21_app.ts for /guides routes).
25+ * Static load-bearing URLs: /, /blog, /games, /leaderboard, /sama, /sama/v2, /sama/v2/verify, /sama/v2/example-crud, /sama/v2/example-wordpress, /sama/skill, /guides. These can stay as a small const list in the new helper (each one corresponds to a literal route in d21_app.ts).
26+- All URLs use the absolute base https://tdd.md (use the constant from src/a31_site_config.ts if one exists).
27+- A new pure Layer 1 helper at src/b32_sitemap.ts takes Array<{ loc: string; lastmod?: string }> → returns the well-formed XML string. No I/O; deterministic output. Sibling test covers: empty list → valid urlset with no <url> children; single URL with lastmod; single URL without lastmod; multiple URLs preserve order; XML-escape any & or < in URLs (rare here but the helper must be safe).
28+- The handler is a single closure registered in src/d21_app.ts (or split into d21_handlers_sitemap.ts if it grows). Imports ALL_POSTS + ALL_SAMA + the static list, calls the helper, returns the Response with Cache-Control "public, max-age=3600".
29+- /robots.txt updated to include "Sitemap: https://tdd.md/sitemap.xml" at the end. If it doesn't exist yet, create the minimal: "User-agent: *\nAllow: /\nSitemap: https://tdd.md/sitemap.xml".
30+- The sitemap is NOT committed as a static file — it's generated per-request (or once at process startup). New blog post → next sitemap fetch already includes it without any human edit. This is the load-bearing "automatic" property.
31+- All 367+ tests still pass; new helper test adds ~6-8 cases.
32+- /sama/v2/verify still reports 7/7 ✓ (anti-fudge).
33+- Deployed; live-verify: curl https://tdd.md/sitemap.xml returns 200 + valid XML; the response includes /blog/sama-v2-workingset-cross-repo-baseline (the most recent post); /robots.txt references the sitemap.
34+
35+Constraints (anti-fudge):
36+- URLs MUST come from existing registries — no second source of truth that can drift.
37+- XML must be well-formed (no string-concat shortcuts that break on special chars). Use a tiny XML-escape helper inside b32_sitemap.ts (the existing renderer's HTML-escape is technically a superset and would work too, but a dedicated XML helper is cleaner Layer-1).
38+- Don't list dynamic/user-specific URLs (/p/:slug, /sama/verify?repo=..., /api/*) — only stable indexable content.
39+- Cache-Control: public, max-age=3600. Search engines should re-fetch but not hammer.
40+- Site language English-only.
41+- GitHub flow via flatpak-spawn (branch → PR → merge → push p620 → deploy via flatpak-spawn --host scripts/p620/deploy-tdd-md.sh).
42+- Do NOT change any §4 verifier logic.
43+
44+Load-bearing files to read FIRST:
45+- src/a31_blog.ts (the comment at the top confirms ALL_POSTS is meant to drive the sitemap)
46+- src/a31_sama.ts (ALL_SAMA structure)
47+- src/d21_app.ts (live route table — confirm which static URLs exist + find a place to register /sitemap.xml + grep for /guides routes)
48+- src/a31_site_config.ts (canonical base URL constant — use that, don't hard-code "https://tdd.md" in 20 places)
49+- src/b51_render_layout.ts (the existing escape helper, as reference for the XML-escape function shape)
50+- public/robots.txt if it exists (check before clobbering)
added goals/v21-dialects-spec.md +18 −0
@@ -0,0 +1,18 @@
1+---
2+slug: v21-dialects-spec
3+title: Draft three v2.1 dialects into §6 + profile-parser tolerance
4+date: 2026-05-24
5+branch: sama-v2-1-dialects-drafted
6+pr_number: 31
7+merge_sha: f244dbb
8+status: lossy
9+related_posts: [sama-v2-rust-project-ripgrep, sama-v2-go-project-dive]
10+---
11+
12+**Recovered opening (from prior-session conversation summary, paraphrased):**
13+
14+> Goal: Promote the three v2.1 dialects (directory-layout, inline-tests, declarative-exemption) from "proposed in blog" to drafted §6 extensions in /sama/v2. The verifier still treats this repo bit-identically — opt-in profile flags only, no behavior change for non-adopters.
15+
16+**What is preserved from the original /goal:** only the opening sentence above, recovered from the prior-session conversation summary at the start of this conversation. The full *Done when* clauses, *Constraints (anti-fudge)*, and *Load-bearing files* sections did not survive summarization.
17+
18+**What landed:** §6.A umbrella + §6.1/§6.2/§6.3 subsections in `content/sama/v2.md`, each with a 5-part structure (profile syntax / what it relaxes / property preserved / preservation mechanism / falsifiable cross-repo experiment). `ProfileSpec` gained optional `layout`, `tests`, `atomicExemption` fields with allowed-value validation in the TOML parser. 12 new parser tests landed; this repo's `sama.profile.toml` stayed unchanged so the §4 verifier's verdict was bit-identical. See [PR #31](https://github.com/syntaxai/tdd.md/pull/31) for the full diff.
added goals/working-set-polyglot.md +18 −0
@@ -0,0 +1,18 @@
1+---
2+slug: working-set-polyglot
3+title: Port §5 workingSetFit metric to Go + Rust source trees
4+date: 2026-05-24
5+branch: sama-v2-workingset-polyglot-measured
6+pr_number: 32
7+merge_sha: 43c6f7a
8+status: lossy
9+related_posts: [sama-v2-go-project-dive, sama-v2-rust-project-ripgrep, sama-v2-workingset-cross-repo-baseline]
10+---
11+
12+**Recovered opening (from prior-session conversation summary, paraphrased):**
13+
14+> Goal: Port the §5 workingSetFit metric to Go and Rust source trees. Run it against /tmp/dive and /tmp/ripgrep at pinned SHAs; replace the hand-estimated workingSetFit values in the dive + ripgrep audit blog posts with measured numbers; convert the cross-repo argument from "n=1 measured + n=3 estimated" to "n=3 measured + n=1 estimated".
15+
16+**What is preserved from the original /goal:** only the opening sentence above, recovered from the prior-session conversation summary at the start of this conversation. The full *Done when* clauses, *Constraints (anti-fudge)*, and *Load-bearing files* sections did not survive summarization.
17+
18+**What landed:** `src/b32_working_set_polyglot.ts` (pure Layer 1) + `src/c14_working_set_walker.ts` (Layer 2 adapter) + `scripts/measure-working-set.ts` (CLI). 24 new tests covering bound-edge inclusivity + Go-vs-Rust test-file asymmetry (Go excludes `*_test.go`; Rust includes all `.rs` per /sama/v2 §6.2 inline-tests dialect). Measured results: dive @d6c69194 = 52.17% (originally hand-estimated ~80%, a 28-point miss); ripgrep @4519153e = 54.00% (originally ~60%, a 6-point miss). See [PR #32](https://github.com/syntaxai/tdd.md/pull/32).
modified src/a31_goals.ts +93 −1
@@ -11,7 +11,15 @@
1111 // still works in one grep: `grep -l "merge_sha: 968890f" goals/`.
1212 // See /blog/sama-v2-goal-chain-gap for the design rationale.
1313
14-export type GoalStatus = "pending" | "shipped" | "abandoned";
14+// Five-status taxonomy:
15+// pending — in-flight; mergeSha/prNumber null; file exists on the branch
16+// shipped — verbatim /goal text recovered from PR body/conversation; file exists
17+// lossy — paraphrased recovery (e.g. from conversation summary); file exists
18+// with a "recovered from conversation summary, not verbatim" banner
19+// lost — no recoverable source; registry entry only, NO file on disk;
20+// /goals/<slug> renders metadata-only at 200
21+// abandoned — PR closed without merging
22+export type GoalStatus = "pending" | "shipped" | "lossy" | "lost" | "abandoned";
1523
1624 export interface GoalEntry {
1725 slug: string;
@@ -30,6 +38,26 @@ export interface GoalEntry {
3038 }
3139
3240 export const ALL_GOALS: GoalEntry[] = [
41+ {
42+ slug: "migrate-historical-goals",
43+ title: "Migrate historical /goals + lock down the authoring workflow",
44+ date: "2026-05-25",
45+ branch: "migrate-historical-goals",
46+ prNumber: null,
47+ mergeSha: null,
48+ status: "pending",
49+ relatedPosts: ["sama-v2-goal-chain-gap"],
50+ },
51+ {
52+ slug: "build-goals-registry",
53+ title: "Build /goals registry + site surface (goal #1)",
54+ date: "2026-05-25",
55+ branch: "goals-registry-build",
56+ prNumber: 45,
57+ mergeSha: "e923da8",
58+ status: "shipped",
59+ relatedPosts: ["sama-v2-goal-chain-gap"],
60+ },
3361 {
3462 slug: "git-url-drop-owner",
3563 title: "Drop redundant :owner segment from /GIT/ URLs",
@@ -43,4 +71,68 @@ export const ALL_GOALS: GoalEntry[] = [
4371 "sama-v2-git-url-refactor-postmortem",
4472 ],
4573 },
74+ {
75+ slug: "sitemap-xml-impl",
76+ title: "Add automatically-generated /sitemap.xml from existing registries",
77+ date: "2026-05-25",
78+ branch: "sitemap-xml-impl",
79+ prNumber: 40,
80+ mergeSha: "3280af8",
81+ status: "shipped",
82+ relatedPosts: ["sama-v2-sitemap-implementation-plan"],
83+ },
84+ {
85+ slug: "graph-depth-polyglot",
86+ title: "Port §5 graphDepth metric to Go + Rust",
87+ date: "2026-05-24",
88+ branch: "sama-v2-graphdepth-polyglot",
89+ prNumber: 34,
90+ mergeSha: "832b7c9",
91+ status: "lossy",
92+ relatedPosts: ["sama-v2-go-project-dive", "sama-v2-rust-project-ripgrep"],
93+ },
94+ {
95+ slug: "polyglot-baseline-n7",
96+ title: "Run polyglot §5 workingSetFit against 5 more CLI tools (n=7 baseline)",
97+ date: "2026-05-24",
98+ branch: "sama-v2-workingset-cross-repo-baseline",
99+ prNumber: 33,
100+ mergeSha: "60056b1",
101+ status: "lossy",
102+ relatedPosts: ["sama-v2-workingset-cross-repo-baseline"],
103+ },
104+ {
105+ slug: "working-set-polyglot",
106+ title: "Port §5 workingSetFit metric to Go + Rust source trees",
107+ date: "2026-05-24",
108+ branch: "sama-v2-workingset-polyglot-measured",
109+ prNumber: 32,
110+ mergeSha: "43c6f7a",
111+ status: "lossy",
112+ relatedPosts: [
113+ "sama-v2-go-project-dive",
114+ "sama-v2-rust-project-ripgrep",
115+ "sama-v2-workingset-cross-repo-baseline",
116+ ],
117+ },
118+ {
119+ slug: "v21-dialects-spec",
120+ title: "Draft three v2.1 dialects into §6 + profile-parser tolerance",
121+ date: "2026-05-24",
122+ branch: "sama-v2-1-dialects-drafted",
123+ prNumber: 31,
124+ mergeSha: "f244dbb",
125+ status: "lossy",
126+ relatedPosts: ["sama-v2-rust-project-ripgrep", "sama-v2-go-project-dive"],
127+ },
128+ {
129+ slug: "sama-rewrite-v2-first",
130+ title: "Rewrite /sama using SV \"latest is default, legacy preserved\" pattern",
131+ date: "2026-05-24",
132+ branch: "sama-v2-first-images",
133+ prNumber: 36,
134+ mergeSha: "fd50ede",
135+ status: "lossy",
136+ relatedPosts: [],
137+ },
46138 ];
modified src/b32_goals_meta.test.ts +34 −0
@@ -85,6 +85,40 @@ status: half-done
8585 body`;
8686 expect(parseGoalFrontmatter(bad)).toBeNull();
8787 });
88+
89+ test('status: "lossy" parses correctly', () => {
90+ const lossy = `---
91+slug: x
92+title: A lossy goal
93+date: 2026-05-24
94+branch: x
95+pr_number: 32
96+merge_sha: 43c6f7a
97+status: lossy
98+---
99+Recovered partially from conversation.`;
100+ const parsed = parseGoalFrontmatter(lossy);
101+ expect(parsed).not.toBeNull();
102+ expect(parsed!.meta.status).toBe("lossy");
103+ expect(parsed!.meta.prNumber).toBe(32);
104+ expect(parsed!.meta.mergeSha).toBe("43c6f7a");
105+ });
106+
107+ test('status: "lost" parses correctly (registry-only goals still parse if a file exists)', () => {
108+ const lost = `---
109+slug: x
110+title: A lost goal
111+date: 2026-05-24
112+branch: x
113+pr_number: 31
114+merge_sha: f244dbb
115+status: lost
116+---
117+(intentionally empty body — no /goal text recoverable)`;
118+ const parsed = parseGoalFrontmatter(lost);
119+ expect(parsed).not.toBeNull();
120+ expect(parsed!.meta.status).toBe("lost");
121+ });
88122 });
89123
90124 const sampleGoals: ReadonlyArray<GoalEntry> = [
modified src/b32_goals_meta.ts +7 −1
@@ -25,7 +25,13 @@ export interface ParsedGoal {
2525 body: string;
2626 }
2727
28-const STATUS_VALUES: ReadonlyArray<GoalStatus> = ["pending", "shipped", "abandoned"];
28+const STATUS_VALUES: ReadonlyArray<GoalStatus> = [
29+ "pending",
30+ "shipped",
31+ "lossy",
32+ "lost",
33+ "abandoned",
34+];
2935
3036 const parseNullableInt = (v: string | undefined): number | null => {
3137 if (v === undefined || v === "null" || v === "") return null;
modified src/d21_handlers_goals.ts +37 −6
@@ -9,10 +9,16 @@ import { htmlResponse, renderNotFound } from "./b51_render_layout.ts";
99
1010 const STATUS_LABEL: Record<GoalStatus, string> = {
1111 shipped: "✓ shipped",
12+ lossy: "⚠ lossy",
13+ lost: "✗ lost",
1214 pending: "⏳ pending",
1315 abandoned: "✗ abandoned",
1416 };
1517
18+const LOSSY_BANNER = `> ⚠ **Recovered partially.** The /goal text below was reconstructed from conversation summary, not verbatim from the PR body. Original wording may differ from what the user typed.\n\n`;
19+
20+const LOST_BLOCK = `> ✗ **Source could not be recovered.** This PR was /goal-driven, but neither the PR body nor any available conversation context preserved the verbatim /goal text. The metadata above is what remains — see the linked PR + merge commit for the implementation, and the related blog posts for narrative context.\n`;
21+
1622 const prLink = (n: number | null): string =>
1723 n === null ? "—" : `[#${n}](https://github.com/syntaxai/tdd.md/pull/${n})`;
1824
@@ -59,6 +65,14 @@ export const goalsLandingHandler = async (): Promise<Response> => {
5965 return htmlResponse(html);
6066 };
6167
68+const renderBadges = (entry: { status: GoalStatus; date: string; prNumber: number | null; mergeSha: string | null; relatedPosts: ReadonlyArray<string> }): string => {
69+ const badges = `**Status:** ${STATUS_LABEL[entry.status]} · **Date:** ${entry.date} · **PR:** ${prLink(entry.prNumber)} · **Commit:** ${commitLink(entry.mergeSha)}`;
70+ const related = entry.relatedPosts.length === 0
71+ ? ""
72+ : `\n\n**Related posts:** ${relatedLinks(entry.relatedPosts)}`;
73+ return `${badges}${related}`;
74+};
75+
6276 export const goalSlugHandler = async (
6377 req: { params: { slug: string } },
6478 ): Promise<Response> => {
@@ -68,6 +82,23 @@ export const goalSlugHandler = async (
6882 const html = await renderNotFound(`/goals/${slug}`);
6983 return htmlResponse(html, 404);
7084 }
85+
86+ // status: "lost" — registry entry only, no file on disk. Render
87+ // metadata-only at 200 (not 404) with the "source could not be
88+ // recovered" callout. See /blog/sama-v2-goal-chain-gap for context.
89+ if (entry.status === "lost") {
90+ const body = `# ${entry.title}\n\n${renderBadges(entry)}\n\n---\n\n${LOST_BLOCK}`;
91+ const html = await renderDocsPage({
92+ title: `${entry.title} — tdd.md`,
93+ description: `The /goal command that drove ${entry.title}. Status: lost — source not recoverable.`,
94+ bodyMarkdown: body,
95+ ogPath: `https://tdd.md/goals/${slug}`,
96+ active: "goals",
97+ pathForDocs: `/goals/${slug}`,
98+ });
99+ return htmlResponse(html);
100+ }
101+
71102 const file = Bun.file(`./goals/${slug}.md`);
72103 if (!(await file.exists())) {
73104 const html = await renderNotFound(`/goals/${slug}`);
@@ -80,13 +111,13 @@ export const goalSlugHandler = async (
80111 return htmlResponse(html, 404);
81112 }
82113
83- const badges =
84- `**Status:** ${STATUS_LABEL[entry.status]} · **Date:** ${entry.date} · **PR:** ${prLink(entry.prNumber)} · **Commit:** ${commitLink(entry.mergeSha)}`;
85- const related = entry.relatedPosts.length === 0
86- ? ""
87- : `\n\n**Related posts:** ${relatedLinks(entry.relatedPosts)}`;
114+ // status: "lossy" — prepend the recovered-partially banner so the
115+ // rendered page is unambiguous about provenance.
116+ const bodyContent = entry.status === "lossy"
117+ ? `${LOSSY_BANNER}${parsed.body}`
118+ : parsed.body;
88119
89- const body = `# ${entry.title}\n\n${badges}${related}\n\n---\n\n${parsed.body}`;
120+ const body = `# ${entry.title}\n\n${renderBadges(entry)}\n\n---\n\n${bodyContent}`;
90121
91122 const html = await renderDocsPage({
92123 title: `${entry.title} — tdd.md`,