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]>
12 files changed · +383 −8
.gitignore
+5
−0
| @@ -11,3 +11,8 @@ playwright-report/ | ||
| 11 | 11 | test-results/ |
| 12 | 12 | .playwright/ |
| 13 | 13 | .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 | |
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) | |
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). | |
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). | |
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). | |
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) | |
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. | |
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). | |
src/a31_goals.ts
+93
−1
| @@ -11,7 +11,15 @@ | ||
| 11 | 11 | // still works in one grep: `grep -l "merge_sha: 968890f" goals/`. |
| 12 | 12 | // See /blog/sama-v2-goal-chain-gap for the design rationale. |
| 13 | 13 | |
| 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"; | |
| 15 | 23 | |
| 16 | 24 | export interface GoalEntry { |
| 17 | 25 | slug: string; |
| @@ -30,6 +38,26 @@ export interface GoalEntry { | ||
| 30 | 38 | } |
| 31 | 39 | |
| 32 | 40 | 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 | + }, | |
| 33 | 61 | { |
| 34 | 62 | slug: "git-url-drop-owner", |
| 35 | 63 | title: "Drop redundant :owner segment from /GIT/ URLs", |
| @@ -43,4 +71,68 @@ export const ALL_GOALS: GoalEntry[] = [ | ||
| 43 | 71 | "sama-v2-git-url-refactor-postmortem", |
| 44 | 72 | ], |
| 45 | 73 | }, |
| 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 | + }, | |
| 46 | 138 | ]; |
src/b32_goals_meta.test.ts
+34
−0
| @@ -85,6 +85,40 @@ status: half-done | ||
| 85 | 85 | body`; |
| 86 | 86 | expect(parseGoalFrontmatter(bad)).toBeNull(); |
| 87 | 87 | }); |
| 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 | + }); | |
| 88 | 122 | }); |
| 89 | 123 | |
| 90 | 124 | const sampleGoals: ReadonlyArray<GoalEntry> = [ |
src/b32_goals_meta.ts
+7
−1
| @@ -25,7 +25,13 @@ export interface ParsedGoal { | ||
| 25 | 25 | body: string; |
| 26 | 26 | } |
| 27 | 27 | |
| 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 | +]; | |
| 29 | 35 | |
| 30 | 36 | const parseNullableInt = (v: string | undefined): number | null => { |
| 31 | 37 | if (v === undefined || v === "null" || v === "") return null; |
src/d21_handlers_goals.ts
+37
−6
| @@ -9,10 +9,16 @@ import { htmlResponse, renderNotFound } from "./b51_render_layout.ts"; | ||
| 9 | 9 | |
| 10 | 10 | const STATUS_LABEL: Record<GoalStatus, string> = { |
| 11 | 11 | shipped: "✓ shipped", |
| 12 | + lossy: "⚠ lossy", | |
| 13 | + lost: "✗ lost", | |
| 12 | 14 | pending: "⏳ pending", |
| 13 | 15 | abandoned: "✗ abandoned", |
| 14 | 16 | }; |
| 15 | 17 | |
| 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 | + | |
| 16 | 22 | const prLink = (n: number | null): string => |
| 17 | 23 | n === null ? "—" : `[#${n}](https://github.com/syntaxai/tdd.md/pull/${n})`; |
| 18 | 24 | |
| @@ -59,6 +65,14 @@ export const goalsLandingHandler = async (): Promise<Response> => { | ||
| 59 | 65 | return htmlResponse(html); |
| 60 | 66 | }; |
| 61 | 67 | |
| 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 | + | |
| 62 | 76 | export const goalSlugHandler = async ( |
| 63 | 77 | req: { params: { slug: string } }, |
| 64 | 78 | ): Promise<Response> => { |
| @@ -68,6 +82,23 @@ export const goalSlugHandler = async ( | ||
| 68 | 82 | const html = await renderNotFound(`/goals/${slug}`); |
| 69 | 83 | return htmlResponse(html, 404); |
| 70 | 84 | } |
| 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 | + | |
| 71 | 102 | const file = Bun.file(`./goals/${slug}.md`); |
| 72 | 103 | if (!(await file.exists())) { |
| 73 | 104 | const html = await renderNotFound(`/goals/${slug}`); |
| @@ -80,13 +111,13 @@ export const goalSlugHandler = async ( | ||
| 80 | 111 | return htmlResponse(html, 404); |
| 81 | 112 | } |
| 82 | 113 | |
| 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; | |
| 88 | 119 | |
| 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}`; | |
| 90 | 121 | |
| 91 | 122 | const html = await renderDocsPage({ |
| 92 | 123 | title: `${entry.title} — tdd.md`, |