7c79a9c1dfa5c4f434f93047f0380b556006f8bb diff --git a/.gitignore b/.gitignore index ed69d679b9ce1ea4817696537a81e9864fcfe350..8d95e04b1a4a4a2530ef3045c1f567284b46ba1d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,8 @@ playwright-report/ test-results/ .playwright/ .auth/ + +# /goal scratch buffer — in-flight /goal text is pasted here before +# being committed to goals/.md per the auth-workflow lock-in +# (see /blog/sama-v2-goal-chain-gap). Never commit goal.md itself. +/goal.md diff --git a/goals/build-goals-registry.md b/goals/build-goals-registry.md new file mode 100644 index 0000000000000000000000000000000000000000..44f08586e3beaae14d3ad815c788a6063fce8242 --- /dev/null +++ b/goals/build-goals-registry.md @@ -0,0 +1,67 @@ +--- +slug: build-goals-registry +title: Build /goals registry + site surface (goal #1) +date: 2026-05-25 +branch: goals-registry-build +pr_number: 45 +merge_sha: e923da8 +status: shipped +related_posts: [sama-v2-goal-chain-gap] +--- + +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/ (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. + +Done when: +- 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). +- Frontmatter format defined and parsed: + --- + slug: + title: + date: + branch: + pr_number: + merge_sha: + status: pending|shipped|abandoned + related_posts: [, ...] + --- + +- Layer 0 registry at src/a31_goals.ts: + export interface GoalEntry { slug, title, date, branch, prNumber, mergeSha, status, relatedPosts } + export const ALL_GOALS: GoalEntry[] = [...] + Same shape as ALL_POSTS — drives /goals, /goals/:slug, and the sitemap. +- Layer 1 helper src/b32_goals_meta.ts: pure functions + parseGoalFrontmatter(body: string) → { meta, body } (no I/O) + findGoalByMergeSha(sha: string, all: ReadonlyArray) → GoalEntry | null + 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...). +- Layer 3 handlers in src/d21_handlers_goals.ts: + goalsLandingHandler → /goals index (table: date · title · status · PR · commit) + goalSlugHandler → /goals/:slug detail (rendered markdown, frontmatter badges, links to PR + commit + related posts) +- /goals routes registered in src/d21_app.ts. +- /goals link added to the main nav in src/b51_render_layout.ts:47 (between /sama and /blog). +- 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/; add "/goals" to STATIC_PATHS in src/b32_sitemap.ts. +- /goals/ pages reference their merge commit via the new /GIT/tdd.md/commit/ link AND back-reference any related blog posts in related_posts[]. +- All 388+ tests still pass; new helper test adds 5-7 cases. +- /sama/v2/verify still reports 7/7 ✓ (anti-fudge). +- Deployed; live-verify: + * curl https://tdd.md/goals → 200 with HTML listing + * curl https://tdd.md/goals/git-url-drop-owner → 200 with the /goal body rendered + * curl https://tdd.md/sitemap.xml | grep -c /goals/ → at least 1 + * Goals index appears in main nav on every page + +Constraints (anti-fudge): +- One markdown file per goal — no JSON, no multi-goal files, no goal-text embedded inside another file. +- 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/ is the canonical permalink. Documented in the file header of a31_goals.ts. +- ALL_GOALS is the single source of truth — no second list of goals in handlers or rendered HTML. +- Site language English-only. +- GitHub flow via flatpak-spawn. +- Do NOT change any §4 verifier logic. +- Frontmatter parser stays in Layer 1 (pure string-in, struct-out) — no fs.readFile, no path joining. + +Load-bearing files to read FIRST: +- src/a31_blog.ts (the registry pattern that ALL_GOALS should mirror) +- src/a31_sama.ts (same — second example of the pattern) +- src/d21_handlers_sama.ts (samaLandingHandler + samaSlugHandler — the index + detail pattern) +- src/d21_app.ts (route table — where /goals routes get registered) +- src/b32_sitemap.ts (extend STATIC_PATHS + add goals URLs to the handler's url list in d21_app.ts) +- src/b51_render_layout.ts:47 (nav strip — where /goals link goes) +- 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) diff --git a/goals/graph-depth-polyglot.md b/goals/graph-depth-polyglot.md new file mode 100644 index 0000000000000000000000000000000000000000..5d41349fe38972cf539e2bcd9ff44b3194fca7af --- /dev/null +++ b/goals/graph-depth-polyglot.md @@ -0,0 +1,18 @@ +--- +slug: graph-depth-polyglot +title: Port §5 graphDepth metric to Go (package-directory DAG) + Rust (Cargo crate DAG) +date: 2026-05-24 +branch: sama-v2-graphdepth-polyglot +pr_number: 34 +merge_sha: 832b7c9 +status: lossy +related_posts: [sama-v2-go-project-dive, sama-v2-rust-project-ripgrep] +--- + +**Recovered opening (from prior-session conversation summary, paraphrased):** + +> 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. + +**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. + +**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). diff --git a/goals/polyglot-baseline-n7.md b/goals/polyglot-baseline-n7.md new file mode 100644 index 0000000000000000000000000000000000000000..770c71aa74b6fc2deec9f71b9b8ce8305afb5443 --- /dev/null +++ b/goals/polyglot-baseline-n7.md @@ -0,0 +1,18 @@ +--- +slug: polyglot-baseline-n7 +title: Run polyglot §5 workingSetFit emitter against 5 more CLI tools +date: 2026-05-24 +branch: sama-v2-workingset-cross-repo-baseline +pr_number: 33 +merge_sha: 60056b1 +status: lossy +related_posts: [sama-v2-workingset-cross-repo-baseline] +--- + +**Recovered opening (from prior-session conversation summary, paraphrased):** + +> 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. + +**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. + +**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). diff --git a/goals/sama-rewrite-v2-first.md b/goals/sama-rewrite-v2-first.md new file mode 100644 index 0000000000000000000000000000000000000000..a49e46820c54b5cdbced585b61d298af0943b75e --- /dev/null +++ b/goals/sama-rewrite-v2-first.md @@ -0,0 +1,18 @@ +--- +slug: sama-rewrite-v2-first +title: Rewrite /sama using the SV "latest is default, legacy preserved" pattern +date: 2026-05-24 +branch: sama-v2-first-images +pr_number: 36 +merge_sha: fd50ede +status: lossy +related_posts: [] +--- + +**Recovered opening (from prior-session conversation summary, paraphrased):** + +> 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. + +**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. + +**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). diff --git a/goals/sitemap-xml-impl.md b/goals/sitemap-xml-impl.md new file mode 100644 index 0000000000000000000000000000000000000000..47b0bd4bf9d3d8f9777629e7fe622e21e9f20a84 --- /dev/null +++ b/goals/sitemap-xml-impl.md @@ -0,0 +1,50 @@ +--- +slug: sitemap-xml-impl +title: Add automatically-generated /sitemap.xml from existing registries +date: 2026-05-25 +branch: sitemap-xml-impl +pr_number: 40 +merge_sha: 3280af8 +status: shipped +related_posts: [sama-v2-sitemap-implementation-plan] +--- + +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. + +Done when: +- A new route /sitemap.xml returns 200 with Content-Type "application/xml; charset=utf-8" and a valid sitemaps.org 0.9 document: + + + https://tdd.md/...[YYYY-MM-DD] + ... + +- URLs are derived from the registries (no hand-maintained slug list): + * Every entry in ALL_POSTS → /blog/ with = the post's date field. + * Every entry in ALL_SAMA → /sama/. + * Every guide entry from whichever registry exists (search for ALL_GUIDES, GUIDES, or grep src/d21_app.ts for /guides routes). + * 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). +- All URLs use the absolute base https://tdd.md (use the constant from src/a31_site_config.ts if one exists). +- 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 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). +- 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". +- /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". +- 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. +- All 367+ tests still pass; new helper test adds ~6-8 cases. +- /sama/v2/verify still reports 7/7 ✓ (anti-fudge). +- 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. + +Constraints (anti-fudge): +- URLs MUST come from existing registries — no second source of truth that can drift. +- 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). +- Don't list dynamic/user-specific URLs (/p/:slug, /sama/verify?repo=..., /api/*) — only stable indexable content. +- Cache-Control: public, max-age=3600. Search engines should re-fetch but not hammer. +- Site language English-only. +- GitHub flow via flatpak-spawn (branch → PR → merge → push p620 → deploy via flatpak-spawn --host scripts/p620/deploy-tdd-md.sh). +- Do NOT change any §4 verifier logic. + +Load-bearing files to read FIRST: +- src/a31_blog.ts (the comment at the top confirms ALL_POSTS is meant to drive the sitemap) +- src/a31_sama.ts (ALL_SAMA structure) +- src/d21_app.ts (live route table — confirm which static URLs exist + find a place to register /sitemap.xml + grep for /guides routes) +- src/a31_site_config.ts (canonical base URL constant — use that, don't hard-code "https://tdd.md" in 20 places) +- src/b51_render_layout.ts (the existing escape helper, as reference for the XML-escape function shape) +- public/robots.txt if it exists (check before clobbering) diff --git a/goals/v21-dialects-spec.md b/goals/v21-dialects-spec.md new file mode 100644 index 0000000000000000000000000000000000000000..090f37ee0eaace6b824df5eb0f75246481b2360d --- /dev/null +++ b/goals/v21-dialects-spec.md @@ -0,0 +1,18 @@ +--- +slug: v21-dialects-spec +title: Draft three v2.1 dialects into §6 + profile-parser tolerance +date: 2026-05-24 +branch: sama-v2-1-dialects-drafted +pr_number: 31 +merge_sha: f244dbb +status: lossy +related_posts: [sama-v2-rust-project-ripgrep, sama-v2-go-project-dive] +--- + +**Recovered opening (from prior-session conversation summary, paraphrased):** + +> 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. + +**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. + +**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. diff --git a/goals/working-set-polyglot.md b/goals/working-set-polyglot.md new file mode 100644 index 0000000000000000000000000000000000000000..ad348e5b51f092de60bbb2fba9fd9bc7f530472f --- /dev/null +++ b/goals/working-set-polyglot.md @@ -0,0 +1,18 @@ +--- +slug: working-set-polyglot +title: Port §5 workingSetFit metric to Go + Rust source trees +date: 2026-05-24 +branch: sama-v2-workingset-polyglot-measured +pr_number: 32 +merge_sha: 43c6f7a +status: lossy +related_posts: [sama-v2-go-project-dive, sama-v2-rust-project-ripgrep, sama-v2-workingset-cross-repo-baseline] +--- + +**Recovered opening (from prior-session conversation summary, paraphrased):** + +> 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". + +**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. + +**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). diff --git a/src/a31_goals.ts b/src/a31_goals.ts index a00f04756074cab3038599267918ed745b960bdf..f867ea8d240355e6a9033820b757d37776127316 100644 --- a/src/a31_goals.ts +++ b/src/a31_goals.ts @@ -11,7 +11,15 @@ // still works in one grep: `grep -l "merge_sha: 968890f" goals/`. // See /blog/sama-v2-goal-chain-gap for the design rationale. -export type GoalStatus = "pending" | "shipped" | "abandoned"; +// Five-status taxonomy: +// pending — in-flight; mergeSha/prNumber null; file exists on the branch +// shipped — verbatim /goal text recovered from PR body/conversation; file exists +// lossy — paraphrased recovery (e.g. from conversation summary); file exists +// with a "recovered from conversation summary, not verbatim" banner +// lost — no recoverable source; registry entry only, NO file on disk; +// /goals/ renders metadata-only at 200 +// abandoned — PR closed without merging +export type GoalStatus = "pending" | "shipped" | "lossy" | "lost" | "abandoned"; export interface GoalEntry { slug: string; @@ -30,6 +38,26 @@ export interface GoalEntry { } export const ALL_GOALS: GoalEntry[] = [ + { + slug: "migrate-historical-goals", + title: "Migrate historical /goals + lock down the authoring workflow", + date: "2026-05-25", + branch: "migrate-historical-goals", + prNumber: null, + mergeSha: null, + status: "pending", + relatedPosts: ["sama-v2-goal-chain-gap"], + }, + { + slug: "build-goals-registry", + title: "Build /goals registry + site surface (goal #1)", + date: "2026-05-25", + branch: "goals-registry-build", + prNumber: 45, + mergeSha: "e923da8", + status: "shipped", + relatedPosts: ["sama-v2-goal-chain-gap"], + }, { slug: "git-url-drop-owner", title: "Drop redundant :owner segment from /GIT/ URLs", @@ -43,4 +71,68 @@ export const ALL_GOALS: GoalEntry[] = [ "sama-v2-git-url-refactor-postmortem", ], }, + { + slug: "sitemap-xml-impl", + title: "Add automatically-generated /sitemap.xml from existing registries", + date: "2026-05-25", + branch: "sitemap-xml-impl", + prNumber: 40, + mergeSha: "3280af8", + status: "shipped", + relatedPosts: ["sama-v2-sitemap-implementation-plan"], + }, + { + slug: "graph-depth-polyglot", + title: "Port §5 graphDepth metric to Go + Rust", + date: "2026-05-24", + branch: "sama-v2-graphdepth-polyglot", + prNumber: 34, + mergeSha: "832b7c9", + status: "lossy", + relatedPosts: ["sama-v2-go-project-dive", "sama-v2-rust-project-ripgrep"], + }, + { + slug: "polyglot-baseline-n7", + title: "Run polyglot §5 workingSetFit against 5 more CLI tools (n=7 baseline)", + date: "2026-05-24", + branch: "sama-v2-workingset-cross-repo-baseline", + prNumber: 33, + mergeSha: "60056b1", + status: "lossy", + relatedPosts: ["sama-v2-workingset-cross-repo-baseline"], + }, + { + slug: "working-set-polyglot", + title: "Port §5 workingSetFit metric to Go + Rust source trees", + date: "2026-05-24", + branch: "sama-v2-workingset-polyglot-measured", + prNumber: 32, + mergeSha: "43c6f7a", + status: "lossy", + relatedPosts: [ + "sama-v2-go-project-dive", + "sama-v2-rust-project-ripgrep", + "sama-v2-workingset-cross-repo-baseline", + ], + }, + { + slug: "v21-dialects-spec", + title: "Draft three v2.1 dialects into §6 + profile-parser tolerance", + date: "2026-05-24", + branch: "sama-v2-1-dialects-drafted", + prNumber: 31, + mergeSha: "f244dbb", + status: "lossy", + relatedPosts: ["sama-v2-rust-project-ripgrep", "sama-v2-go-project-dive"], + }, + { + slug: "sama-rewrite-v2-first", + title: "Rewrite /sama using SV \"latest is default, legacy preserved\" pattern", + date: "2026-05-24", + branch: "sama-v2-first-images", + prNumber: 36, + mergeSha: "fd50ede", + status: "lossy", + relatedPosts: [], + }, ]; diff --git a/src/b32_goals_meta.test.ts b/src/b32_goals_meta.test.ts index 2ebe56e87c4ae92adb794b0002de366bd4eea6aa..ed6e29a7ab3f4a4d6d360b4a871ede98b9701e23 100644 --- a/src/b32_goals_meta.test.ts +++ b/src/b32_goals_meta.test.ts @@ -85,6 +85,40 @@ status: half-done body`; expect(parseGoalFrontmatter(bad)).toBeNull(); }); + + test('status: "lossy" parses correctly', () => { + const lossy = `--- +slug: x +title: A lossy goal +date: 2026-05-24 +branch: x +pr_number: 32 +merge_sha: 43c6f7a +status: lossy +--- +Recovered partially from conversation.`; + const parsed = parseGoalFrontmatter(lossy); + expect(parsed).not.toBeNull(); + expect(parsed!.meta.status).toBe("lossy"); + expect(parsed!.meta.prNumber).toBe(32); + expect(parsed!.meta.mergeSha).toBe("43c6f7a"); + }); + + test('status: "lost" parses correctly (registry-only goals still parse if a file exists)', () => { + const lost = `--- +slug: x +title: A lost goal +date: 2026-05-24 +branch: x +pr_number: 31 +merge_sha: f244dbb +status: lost +--- +(intentionally empty body — no /goal text recoverable)`; + const parsed = parseGoalFrontmatter(lost); + expect(parsed).not.toBeNull(); + expect(parsed!.meta.status).toBe("lost"); + }); }); const sampleGoals: ReadonlyArray = [ diff --git a/src/b32_goals_meta.ts b/src/b32_goals_meta.ts index f605aae4a17bf55cf4168be633ee1002b7ab78d4..ef580f3a88bef52710d466d0551c211d20071824 100644 --- a/src/b32_goals_meta.ts +++ b/src/b32_goals_meta.ts @@ -25,7 +25,13 @@ export interface ParsedGoal { body: string; } -const STATUS_VALUES: ReadonlyArray = ["pending", "shipped", "abandoned"]; +const STATUS_VALUES: ReadonlyArray = [ + "pending", + "shipped", + "lossy", + "lost", + "abandoned", +]; const parseNullableInt = (v: string | undefined): number | null => { if (v === undefined || v === "null" || v === "") return null; diff --git a/src/d21_handlers_goals.ts b/src/d21_handlers_goals.ts index 9d75972f1caf79fdcaf1b2e347e41e302e88cf8f..4971faf01eb85a72eb303f94652c85e4de275057 100644 --- a/src/d21_handlers_goals.ts +++ b/src/d21_handlers_goals.ts @@ -9,10 +9,16 @@ import { htmlResponse, renderNotFound } from "./b51_render_layout.ts"; const STATUS_LABEL: Record = { shipped: "✓ shipped", + lossy: "⚠ lossy", + lost: "✗ lost", pending: "⏳ pending", abandoned: "✗ abandoned", }; +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`; + +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`; + const prLink = (n: number | null): string => n === null ? "—" : `[#${n}](https://github.com/syntaxai/tdd.md/pull/${n})`; @@ -59,6 +65,14 @@ export const goalsLandingHandler = async (): Promise => { return htmlResponse(html); }; +const renderBadges = (entry: { status: GoalStatus; date: string; prNumber: number | null; mergeSha: string | null; relatedPosts: ReadonlyArray }): string => { + const badges = `**Status:** ${STATUS_LABEL[entry.status]} · **Date:** ${entry.date} · **PR:** ${prLink(entry.prNumber)} · **Commit:** ${commitLink(entry.mergeSha)}`; + const related = entry.relatedPosts.length === 0 + ? "" + : `\n\n**Related posts:** ${relatedLinks(entry.relatedPosts)}`; + return `${badges}${related}`; +}; + export const goalSlugHandler = async ( req: { params: { slug: string } }, ): Promise => { @@ -68,6 +82,23 @@ export const goalSlugHandler = async ( const html = await renderNotFound(`/goals/${slug}`); return htmlResponse(html, 404); } + + // status: "lost" — registry entry only, no file on disk. Render + // metadata-only at 200 (not 404) with the "source could not be + // recovered" callout. See /blog/sama-v2-goal-chain-gap for context. + if (entry.status === "lost") { + const body = `# ${entry.title}\n\n${renderBadges(entry)}\n\n---\n\n${LOST_BLOCK}`; + const html = await renderDocsPage({ + title: `${entry.title} — tdd.md`, + description: `The /goal command that drove ${entry.title}. Status: lost — source not recoverable.`, + bodyMarkdown: body, + ogPath: `https://tdd.md/goals/${slug}`, + active: "goals", + pathForDocs: `/goals/${slug}`, + }); + return htmlResponse(html); + } + const file = Bun.file(`./goals/${slug}.md`); if (!(await file.exists())) { const html = await renderNotFound(`/goals/${slug}`); @@ -80,13 +111,13 @@ export const goalSlugHandler = async ( return htmlResponse(html, 404); } - const badges = - `**Status:** ${STATUS_LABEL[entry.status]} · **Date:** ${entry.date} · **PR:** ${prLink(entry.prNumber)} · **Commit:** ${commitLink(entry.mergeSha)}`; - const related = entry.relatedPosts.length === 0 - ? "" - : `\n\n**Related posts:** ${relatedLinks(entry.relatedPosts)}`; + // status: "lossy" — prepend the recovered-partially banner so the + // rendered page is unambiguous about provenance. + const bodyContent = entry.status === "lossy" + ? `${LOSSY_BANNER}${parsed.body}` + : parsed.body; - const body = `# ${entry.title}\n\n${badges}${related}\n\n---\n\n${parsed.body}`; + const body = `# ${entry.title}\n\n${renderBadges(entry)}\n\n---\n\n${bodyContent}`; const html = await renderDocsPage({ title: `${entry.title} — tdd.md`,